/ concept-collection / turing-sphere
Sign in
concept-collection / turing-sphere
203 lines · 6.8 KBCodeBlameHistory
35d91faDelete the TypeScript solver; the .m models are the only implementationJeremy Magland 2 * Browser validation, in the environment the demo actually ships to.
3 *
4 * Runs the same three check modules as `npm run test:node` — so both GPU stacks
5 * (Dawn on the desktop, the browser's own here) get the same guarantees — plus a
6 * long soak that only makes sense in a page.
7 *
2dedc35turing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPUJeremy Magland 8 * Results are posted to window.__RESULTS__ for the headless runner.
9 */
0f3abbdSeparate solver time from frame time, and add a cross-environment checkJeremy Magland 10import { requestShtDevice, describeAdapter } from '../src/sht/sht.ts';
35d91faDelete the TypeScript solver; the .m models are the only implementationJeremy Magland 11import { ModelSession } from '../src/mgpu/session.ts';
12import { mModels, defaultParams } from '../src/mgpu/registry.ts';
0f3abbdSeparate solver time from frame time, and add a cross-environment checkJeremy Magland 13import { digestOf, formatDigest, type StateDigest } from '../src/mgpu/digest.ts';
14import { parseArgs, modelForSpec, formatCommand } from '../src/bench/runSpec.ts';
35d91faDelete the TypeScript solver; the .m models are the only implementationJeremy Magland 15import { transformChecks } from './transformChecks.ts';
16import { analyticChecks } from './analyticChecks.ts';
17import { modelChecks } from './modelChecks.ts';
19declare global {
20 interface Window {
21 __RESULTS__?: { ok: boolean; fatal?: string; lines: string[] };
0f3abbdSeparate solver time from frame time, and add a cross-environment checkJeremy Magland 22 /** Set by the ?state= mode, for scripts/compare-env.mjs. */
23 __STATE__?: { digest: StateDigest; state: number[] };
17db8f1Add scripts/compare-perf.mjs, and rule out CPU command encodingJeremy Magland 24 /** Set by the ?soak= mode, for scripts/compare-perf.mjs. */
25 __SOAK__?: {
26 lmax: number;
27 steps: number;
28 batch: number;
29 solverMsPerStep: number;
30 encodeMsPerStep: number;
31 adapter: string;
32 fourier: 'fft' | 'dft';
33 };
37const logEl = document.getElementById('log')!;
38const lines: string[] = [];
39let failures = 0;
41function log(s: string): void {
42 lines.push(s);
43 logEl.textContent = lines.join('\n');
44 console.log(s);
47function check(name: string, ok: boolean, detail: string): void {
48 log(`${ok ? 'PASS' : 'FAIL'} ${name} ${detail}`);
49 if (!ok) failures++;
cde22eaStop the per-frame sync from inflating the app's solver numberJeremy Magland 53 * Solver-only soak, selected with ?soak=<steps>&lmax=<n>.
54 *
55 * No three.js at all, so this is the browser's honest solver rate: the same
56 * batched, no-readback measurement the desktop benchmark reports. If this number
57 * matches the benchmark's but the app's frame cost does not, the difference is
58 * the readback and competing with the renderer for the GPU, not the computation.
60async function soak(steps: number, lmax: number): Promise<void> {
61 const device = await requestShtDevice();
63 const session = await ModelSession.create({
64 device,
65 model,
66 params: defaultParams(model),
67 lmax,
68 });
69 session.seed(5);
70 log(
71 `soak: ${steps} steps at lmax ${lmax} ` +
72 `(grid ${session.cfg.nlat}x${session.cfg.nphi}), solver only`,
73 );
cde22eaStop the per-frame sync from inflating the app's solver numberJeremy Magland 76 // Timed separately from the sampling: `solverMs` counts only submitted steps
77 // waited for, never read back, so it is comparable to `npm run bench`.
78 let solverMs = 0;
79 let solverSteps = 0;
e7bcd70Add soak, live-check and solver-only soak toolingJeremy Magland 81 const t0 = performance.now();
35d91faDelete the TypeScript solver; the .m models are the only implementationJeremy Magland 82 for (let s = 0; s < steps; s += BATCH) {
cde22eaStop the per-frame sync from inflating the app's solver numberJeremy Magland 83 const n = Math.min(BATCH, steps - s);
84 const b0 = performance.now();
85 session.step(n);
17db8f1Add scripts/compare-perf.mjs, and rule out CPU command encodingJeremy Magland 86 // CPU-side command encoding, separated from GPU execution: in a browser each
87 // WebGPU call crosses Blink's bindings and Dawn's validation, so on a fast
88 // GPU the encoding can be what actually limits the step rate.
89 const b1 = performance.now();
90 encodeMs += b1 - b0;
cde22eaStop the per-frame sync from inflating the app's solver numberJeremy Magland 91 await session.sync();
92 solverMs += performance.now() - b0;
93 solverSteps += n;
35d91faDelete the TypeScript solver; the .m models are the only implementationJeremy Magland 94 const u = await session.read(model.species[0]);
95 let lo = Infinity;
96 let hi = -Infinity;
97 for (const v of u) {
98 if (v < lo) lo = v;
99 if (v > hi) hi = v;
100 }
101 if ((s + BATCH) % 100 === 0) {
102 const mem = (performance as Performance & { memory?: { usedJSHeapSize: number } })
103 .memory;
35d91faDelete the TypeScript solver; the .m models are the only implementationJeremy Magland 105 ` step ${session.steps} u in [${lo.toFixed(4)}, ${hi.toFixed(4)}]` +
e7bcd70Add soak, live-check and solver-only soak toolingJeremy Magland 106 (mem ? ` heap ${(mem.usedJSHeapSize / 1048576).toFixed(1)} MB` : ''),
107 );
108 // yield so the page stays responsive and the runner can poll
109 await new Promise((r) => setTimeout(r, 0));
110 }
111 }
112 const ms = (performance.now() - t0) / steps;
114 const final = await session.read(model.species[0]);
e7bcd70Add soak, live-check and solver-only soak toolingJeremy Magland 115 let finite = true;
35d91faDelete the TypeScript solver; the .m models are the only implementationJeremy Magland 116 for (const v of final) if (!Number.isFinite(v)) finite = false;
cde22eaStop the per-frame sync from inflating the app's solver numberJeremy Magland 117 const solverPerStep = solverMs / solverSteps;
17db8f1Add scripts/compare-perf.mjs, and rule out CPU command encodingJeremy Magland 118 const encodePerStep = encodeMs / solverSteps;
120 `soak: ${steps} steps survived`,
121 finite,
122 `solver ${solverPerStep.toFixed(2)} ms/step (batches of ${BATCH}, no readback), ` +
17db8f1Add scripts/compare-perf.mjs, and rule out CPU command encodingJeremy Magland 123 `of which ${encodePerStep.toFixed(3)} ms/step CPU encoding, ` +
cde22eaStop the per-frame sync from inflating the app's solver numberJeremy Magland 124 `${ms.toFixed(2)} ms/step incl. sampling readback`,
125 );
126 log(
127 ` compare 'solver' with the ms/step from \`npm run bench -- --lmax ${lmax}\`:\n` +
128 ` same .m, same kernels, no rendering on either side.`,
129 );
132 lmax,
133 steps,
134 batch: BATCH,
135 solverMsPerStep: solverPerStep,
136 encodeMsPerStep: encodePerStep,
137 adapter: await describeAdapter(device),
138 fourier: session.sht.fourierMode,
139 };
e7bcd70Add soak, live-check and solver-only soak toolingJeremy Magland 141 window.__RESULTS__ = { ok: failures === 0, lines };
142 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
146 * Run one exact spec and post its final state, for scripts/compare-env.mjs to
147 * compare against the same spec run on the desktop. Query parameters map
148 * straight onto the benchmark's flags — `?state=1&lmax=31&steps=200` — and go
149 * through the same parseArgs, so neither side can quietly use different
150 * defaults.
151 */
152async function dumpState(q: URLSearchParams): Promise<void> {
153 const argv: string[] = [];
154 for (const [k, v] of q) {
155 if (k === 'state') continue;
156 argv.push(`--${k}`, v);
157 }
158 const spec = parseArgs(argv);
159 const model = modelForSpec(spec);
161 const device = await requestShtDevice();
162 const adapter = await describeAdapter(device);
163 const session = await ModelSession.create({
164 device,
165 model,
166 params: spec.params,
167 lmax: spec.lmax,
168 });
169 session.seed(spec.seed);
170 session.step(spec.steps);
171 await session.sync();
172 const state = await session.read(model.state[0]);
173 const digest = digestOf(state, session.sht.fourierMode, adapter);
175 log(`${formatCommand(spec)}\n`);
176 log(`state after ${spec.steps} steps from seed ${spec.seed}:`);
177 log(` ${formatDigest(digest)}`);
178 log(` adapter: ${adapter}`);
179 window.__STATE__ = { digest, state: [...state] };
180 session.destroy();
2dedc35turing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPUJeremy Magland 183async function main(): Promise<void> {
e7bcd70Add soak, live-check and solver-only soak toolingJeremy Magland 184 const q = new URLSearchParams(location.search);
0f3abbdSeparate solver time from frame time, and add a cross-environment checkJeremy Magland 185 if (q.has('state')) return dumpState(q);
e7bcd70Add soak, live-check and solver-only soak toolingJeremy Magland 186 if (q.has('soak')) {
187 return soak(Number(q.get('soak')) || 500, Number(q.get('lmax')) || 63);
188 }
2dedc35turing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPUJeremy Magland 189 const device = await requestShtDevice();
35d91faDelete the TypeScript solver; the .m models are the only implementationJeremy Magland 191 await transformChecks(device, check, log);
192 await analyticChecks(device, check, log);
193 await modelChecks(device, check, log);
2dedc35turing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPUJeremy Magland 195 window.__RESULTS__ = { ok: failures === 0, lines };
196 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
199main().catch((e) => {
200 const msg = e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e);
201 log(`fatal: ${msg}`);
202 window.__RESULTS__ = { ok: false, fatal: msg, lines };
203});
moveopenescclose