/ concept-collection / turing-sphere
Sign in
concept-collection / turing-sphere
175 lines · 6.2 KBBlameHistoryRaw
1/**
2 * Browser validation: the fp32 WebGPU solver against the f64 CPU solver.
3 * Runs identical seeded simulations on both backends and compares fields.
4 * Results are posted to window.__RESULTS__ for the headless runner.
5 */
6import { GpuBackend, CpuBackend, requestShtDevice } from '../src/solver/backend.ts';
7import { Simulation, gridForLmax } from '../src/solver/simulation.ts';
8import { models, defaultParams } from '../src/solver/models.ts';
9import { randomSpectrum } from '../src/sht/reference.ts';
11declare global {
12 interface Window {
13 __RESULTS__?: { ok: boolean; fatal?: string; lines: string[] };
14 }
17const logEl = document.getElementById('log')!;
18const lines: string[] = [];
19let failures = 0;
21function log(s: string): void {
22 lines.push(s);
23 logEl.textContent = lines.join('\n');
24 console.log(s);
27function check(name: string, ok: boolean, detail: string): void {
28 log(`${ok ? 'PASS' : 'FAIL'} ${name} ${detail}`);
29 if (!ok) failures++;
32function relL2(a: ArrayLike<number>, b: ArrayLike<number>): number {
33 let num = 0;
34 let den = 0;
35 for (let i = 0; i < a.length; i++) {
36 const d = a[i] - b[i];
37 num += d * d;
38 den += b[i] * b[i];
39 }
40 return Math.sqrt(num / Math.max(den, 1e-300));
43/**
44 * Solver-only soak (no rendering), selected with ?soak=<steps>&lmax=<n>.
45 * Isolates the GPU transform loop from the three.js renderer.
46 */
47async function soak(steps: number, lmax: number): Promise<void> {
48 const device = await requestShtDevice();
49 const schnak = models[0];
50 const { nlat, nphi } = gridForLmax(lmax, schnak.pdeg);
51 const gpu = await GpuBackend.create(device, { lmax, mmax: lmax, nlat, nphi });
52 const sim = new Simulation(gpu, schnak, defaultParams(schnak));
53 await sim.init(5);
54 log(`soak: ${steps} steps at lmax ${lmax} (grid ${nlat}x${nphi}), solver only`);
56 const t0 = performance.now();
57 for (let s = 0; s < steps; s++) {
58 await sim.step();
59 if ((s + 1) % 100 === 0) {
60 let lo = Infinity;
61 let hi = -Infinity;
62 for (const v of sim.V[0]) {
63 if (v < lo) lo = v;
64 if (v > hi) hi = v;
65 }
66 const mem = (performance as Performance & { memory?: { usedJSHeapSize: number } }).memory;
67 log(
68 ` step ${s + 1} u in [${lo.toFixed(4)}, ${hi.toFixed(4)}]` +
69 (mem ? ` heap ${(mem.usedJSHeapSize / 1048576).toFixed(1)} MB` : ''),
70 );
71 // yield so the page stays responsive and the runner can poll
72 await new Promise((r) => setTimeout(r, 0));
73 }
74 }
75 const ms = (performance.now() - t0) / steps;
76 let finite = true;
77 for (const v of sim.V[0]) if (!Number.isFinite(v)) finite = false;
78 check(`soak: ${steps} steps survived`, finite, `${ms.toFixed(1)} ms/step`);
79 gpu.destroy();
80 window.__RESULTS__ = { ok: failures === 0, lines };
81 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
84async function main(): Promise<void> {
85 const q = new URLSearchParams(location.search);
86 if (q.has('soak')) {
87 return soak(Number(q.get('soak')) || 500, Number(q.get('lmax')) || 63);
88 }
89 const device = await requestShtDevice();
91 // --- transform cross-check: GPU vs CPU on a random spectrum ---
92 {
93 const lmax = 31;
94 const { nlat, nphi } = gridForLmax(lmax, 1);
95 const cfg = { lmax, mmax: lmax, nlat, nphi };
96 const gpu = await GpuBackend.create(device, cfg);
97 const cpu = new CpuBackend(cfg);
98 const q = randomSpectrum(cfg, 42);
99 const q64 = new Float64Array(q);
100 const sGpu = await gpu.synth(q64);
101 const sCpu = await cpu.synth(q64);
102 const errSynth = relL2(sGpu, sCpu);
103 const aGpu = await gpu.analys(new Float64Array(sCpu));
104 const aCpu = await cpu.analys(new Float64Array(sCpu));
105 const errAnalys = relL2(aGpu, aCpu);
106 check('transforms: GPU vs CPU', errSynth < 1e-4 && errAnalys < 1e-4,
107 `synth ${errSynth.toExponential(2)}, analys ${errAnalys.toExponential(2)}`);
108 gpu.destroy();
109 }
111 // --- solver cross-check: identical seeded runs on both backends ---
112 {
113 const schnak = models[0];
114 const params = defaultParams(schnak);
115 const lmax = 31;
116 const { nlat, nphi } = gridForLmax(lmax, schnak.pdeg);
117 const cfg = { lmax, mmax: lmax, nlat, nphi };
118 const gpu = await GpuBackend.create(device, cfg);
119 const cpu = new CpuBackend(cfg);
120 const simGpu = new Simulation(gpu, schnak, { ...params });
121 const simCpu = new Simulation(cpu, schnak, { ...params });
122 await simGpu.init(7);
123 await simCpu.init(7);
124 const nsteps = 10;
125 const t0 = performance.now();
126 for (let s = 0; s < nsteps; s++) await simGpu.step();
127 const gpuMs = (performance.now() - t0) / nsteps;
128 for (let s = 0; s < nsteps; s++) await simCpu.step();
129 let worst = 0;
130 for (let k = 0; k < simGpu.nspecies; k++) {
131 worst = Math.max(worst, relL2(simGpu.V[k], simCpu.V[k]));
132 }
133 let nan = false;
134 for (let k = 0; k < simGpu.nspecies; k++) {
135 for (const v of simGpu.V[k]) if (!Number.isFinite(v)) nan = true;
136 }
137 check('solver: GPU vs CPU after 10 steps', worst < 2e-3 && !nan,
138 `worst rel L2 ${worst.toExponential(2)}${nan ? ', NaN!' : ''} (${gpuMs.toFixed(1)} ms/step GPU)`);
139 gpu.destroy();
140 }
142 // --- longer GPU-only run stays finite and patterned ---
143 {
144 const schnak = models[0];
145 const params = defaultParams(schnak);
146 const lmax = 63;
147 const { nlat, nphi } = gridForLmax(lmax, schnak.pdeg);
148 const gpu = await GpuBackend.create(device, { lmax, mmax: lmax, nlat, nphi });
149 const sim = new Simulation(gpu, schnak, params);
150 await sim.init(3);
151 const nsteps = 100;
152 const t0 = performance.now();
153 for (let s = 0; s < nsteps; s++) await sim.step();
154 const ms = (performance.now() - t0) / nsteps;
155 let lo = Infinity;
156 let hi = -Infinity;
157 for (const v of sim.V[0]) {
158 if (v < lo) lo = v;
159 if (v > hi) hi = v;
160 }
161 const finite = Number.isFinite(lo) && Number.isFinite(hi);
162 check('solver: 100 steps at lmax 63 stay finite', finite && lo > -10 && hi < 10,
163 `u range [${lo.toFixed(4)}, ${hi.toFixed(4)}], ${ms.toFixed(1)} ms/step`);
164 gpu.destroy();
165 }
167 window.__RESULTS__ = { ok: failures === 0, lines };
168 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
171main().catch((e) => {
172 const msg = e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e);
173 log(`fatal: ${msg}`);
174 window.__RESULTS__ = { ok: false, fatal: msg, lines };
175});
moveopenescclose