// 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; }