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