Metabolism: decision-cadence (think slower than you move) + empty-block skip; flat typed-array bytecode; WebGPU decision-layer slice (experimental) + heavy-world benchmark
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
// The heavy-world benchmark: same song, same seed, same planted population —
|
||||
// only the decision cadence varies. Sim-only (no render), field 128.
|
||||
import { buildSong } from '../src/song/song.js';
|
||||
import { analyse } from '../src/audio/analyse.js';
|
||||
import { decodeWav, mono } from '../src/audio/wav.js';
|
||||
import { World } from '../src/sim/world.js';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const name = process.argv[2] ?? 'runner';
|
||||
const frames = Number(process.argv[3] ?? 300);
|
||||
const wav = decodeWav(readFileSync(new URL(`../../flow-state/test/songs/${name}.wav`, import.meta.url)));
|
||||
const song = buildSong(analyse(mono(wav.channels), wav.sampleRate), 12345);
|
||||
|
||||
function plant(w) {
|
||||
// identical starting points: a dense 80×80 metropolis west of centre
|
||||
const F = w.field;
|
||||
for (let y = 24; y < 104; y++) {
|
||||
for (let x = 24; x < 104; x++) {
|
||||
const i = y * F + x;
|
||||
for (let s = 0; s < w.m; s++) w.pop[s][i] = 1.6;
|
||||
}
|
||||
}
|
||||
w.recount();
|
||||
}
|
||||
|
||||
for (const vmEvery of [1, 2, 3]) {
|
||||
const w = new World(song, { fps: 30, field: 128, genesis: 0, vmEvery });
|
||||
plant(w);
|
||||
const start = w.count;
|
||||
// warm-up
|
||||
for (let f = 0; f < 30; f++) w.step();
|
||||
const t0 = process.hrtime.bigint();
|
||||
for (let f = 0; f < frames; f++) w.step();
|
||||
const ms = Number(process.hrtime.bigint() - t0) / 1e6 / frames;
|
||||
console.log(`vmEvery=${vmEvery} sim ${ms.toFixed(1)} ms/frame (start ${Math.round(start)}, now ${Math.round(w.count)}, peak-ish budget ${Math.round(w.cap).toLocaleString()})`);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// A browser-side validation + benchmark of the GPU decision layer.
|
||||
// Open this through the demo server (e.g. `npx vite` then /gpu-check.html)
|
||||
// or paste it into the demo console — it needs a WebGPU device, which node
|
||||
// does not have.
|
||||
//
|
||||
// It runs the SAME compiled strain programs on the CPU (the reference) and
|
||||
// on the GPU (the experiment) over identical senses, and reports the worst
|
||||
// disagreement plus the timing of both paths.
|
||||
|
||||
import { buildSong } from '../src/song/song.js';
|
||||
import { ensureFlat, runProgram, SENSE_COUNT, GRAD_COUNT, ACTION_COUNT, REGS } from '../src/song/strains.js';
|
||||
import { GpuDecisionLayer } from '../src/sim/gpu.js';
|
||||
import { analyse } from '../src/audio/analyse.js';
|
||||
import { decodeWav, mono } from '../src/audio/wav.js';
|
||||
|
||||
export async function gpuCheck(song) {
|
||||
const report = { available: GpuDecisionLayer.available };
|
||||
if (!report.available) {
|
||||
report.note = 'no WebGPU device — the CPU path remains the simulation';
|
||||
return report;
|
||||
}
|
||||
const gpu = new GpuDecisionLayer();
|
||||
report.ready = await gpu.init();
|
||||
if (!report.ready) { report.note = 'device request failed'; return report; }
|
||||
|
||||
const world = { fc: 4096, FC: 64, m: song.strains.length, strains: song.strains };
|
||||
for (const strain of song.strains) ensureFlat(strain);
|
||||
gpu.uploadPrograms(world);
|
||||
|
||||
// synthetic but plausible senses: random-ish, deterministic
|
||||
const fc = world.fc, m = world.m;
|
||||
const senses = new Float32Array(m * fc * SENSE_COUNT);
|
||||
const grads = new Float32Array(m * fc * GRAD_COUNT);
|
||||
let seed = 12345;
|
||||
const rnd = () => (seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;
|
||||
for (let i = 0; i < senses.length; i++) senses[i] = rnd();
|
||||
for (let i = 0; i < grads.length; i++) grads[i] = Math.sign(rnd() - 0.5);
|
||||
|
||||
// CPU reference
|
||||
const cpuAct = new Float32Array(m * fc * ACTION_COUNT);
|
||||
const t0 = performance.now();
|
||||
for (let s = 0; s < m; s++) {
|
||||
const strain = song.strains[s];
|
||||
const flat = ensureFlat(strain);
|
||||
const mem = new Float32Array(REGS);
|
||||
for (let b = 0; b < fc; b++) {
|
||||
const ss = senses.slice(s * fc * SENSE_COUNT + b * SENSE_COUNT,
|
||||
s * fc * SENSE_COUNT + (b + 1) * SENSE_COUNT);
|
||||
const gg = grads.slice(s * fc * GRAD_COUNT + b * GRAD_COUNT,
|
||||
s * fc * GRAD_COUNT + (b + 1) * GRAD_COUNT);
|
||||
const act = new Float32Array(ACTION_COUNT);
|
||||
runProgram(mem, 0, ss, gg, act, flat._flatI, flat._flatF, flat._flatN);
|
||||
for (let a = 0; a < ACTION_COUNT; a++) {
|
||||
cpuAct[(s * fc + b) * ACTION_COUNT + a] = act[a];
|
||||
}
|
||||
}
|
||||
}
|
||||
report.cpuMs = performance.now() - t0;
|
||||
|
||||
// GPU run (the registers start zero; the CPU reference used zeroed mem too)
|
||||
const t1 = performance.now();
|
||||
const gpuAct = await gpu.decide(world, senses, grads);
|
||||
report.gpuMs = performance.now() - t1;
|
||||
|
||||
let worst = 0;
|
||||
let worstAt = -1;
|
||||
for (let i = 0; i < cpuAct.length; i++) {
|
||||
const d = Math.abs(cpuAct[i] - gpuAct[i]);
|
||||
if (d > worst) { worst = d; worstAt = i; }
|
||||
}
|
||||
report.worstDelta = worst;
|
||||
report.worstAt = worstAt;
|
||||
report.verdict = worst < 1e-4 ? 'MATCH (bit-close)' : `DIVERGES (worst ${worst.toFixed(6)})`;
|
||||
return report;
|
||||
}
|
||||
Reference in New Issue
Block a user