/ concept-collection / turing-sphere
Sign in
concept-collection / turing-sphere
176 lines · 5.9 KBBlameHistoryRaw
1/**
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 *
8 * Results are posted to window.__RESULTS__ for the headless runner.
9 */
10import { requestShtDevice, describeAdapter } from '../src/sht/sht.ts';
11import { ModelSession } from '../src/mgpu/session.ts';
12import { mModels, defaultParams } from '../src/mgpu/registry.ts';
13import { digestOf, formatDigest, type StateDigest } from '../src/mgpu/digest.ts';
14import { parseArgs, modelForSpec, formatCommand } from '../src/bench/runSpec.ts';
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[] };
22 /** Set by the ?state= mode, for scripts/compare-env.mjs. */
23 __STATE__?: { digest: StateDigest; state: number[] };
24 }
27const logEl = document.getElementById('log')!;
28const lines: string[] = [];
29let failures = 0;
31function log(s: string): void {
32 lines.push(s);
33 logEl.textContent = lines.join('\n');
34 console.log(s);
37function check(name: string, ok: boolean, detail: string): void {
38 log(`${ok ? 'PASS' : 'FAIL'} ${name} ${detail}`);
39 if (!ok) failures++;
42/**
43 * Solver-only soak, selected with ?soak=<steps>&lmax=<n>.
44 *
45 * No three.js at all, so this is the browser's honest solver rate: the same
46 * batched, no-readback measurement the desktop benchmark reports. If this number
47 * matches the benchmark's but the app's frame cost does not, the difference is
48 * the readback and competing with the renderer for the GPU, not the computation.
49 */
50async function soak(steps: number, lmax: number): Promise<void> {
51 const device = await requestShtDevice();
52 const model = mModels[0];
53 const session = await ModelSession.create({
54 device,
55 model,
56 params: defaultParams(model),
57 lmax,
58 });
59 session.seed(5);
60 log(
61 `soak: ${steps} steps at lmax ${lmax} ` +
62 `(grid ${session.cfg.nlat}x${session.cfg.nphi}), solver only`,
63 );
65 const BATCH = 25;
66 // Timed separately from the sampling: `solverMs` counts only submitted steps
67 // waited for, never read back, so it is comparable to `npm run bench`.
68 let solverMs = 0;
69 let solverSteps = 0;
70 const t0 = performance.now();
71 for (let s = 0; s < steps; s += BATCH) {
72 const n = Math.min(BATCH, steps - s);
73 const b0 = performance.now();
74 session.step(n);
75 await session.sync();
76 solverMs += performance.now() - b0;
77 solverSteps += n;
78 const u = await session.read(model.species[0]);
79 let lo = Infinity;
80 let hi = -Infinity;
81 for (const v of u) {
82 if (v < lo) lo = v;
83 if (v > hi) hi = v;
84 }
85 if ((s + BATCH) % 100 === 0) {
86 const mem = (performance as Performance & { memory?: { usedJSHeapSize: number } })
87 .memory;
88 log(
89 ` step ${session.steps} u in [${lo.toFixed(4)}, ${hi.toFixed(4)}]` +
90 (mem ? ` heap ${(mem.usedJSHeapSize / 1048576).toFixed(1)} MB` : ''),
91 );
92 // yield so the page stays responsive and the runner can poll
93 await new Promise((r) => setTimeout(r, 0));
94 }
95 }
96 const ms = (performance.now() - t0) / steps;
98 const final = await session.read(model.species[0]);
99 let finite = true;
100 for (const v of final) if (!Number.isFinite(v)) finite = false;
101 const solverPerStep = solverMs / solverSteps;
102 check(
103 `soak: ${steps} steps survived`,
104 finite,
105 `solver ${solverPerStep.toFixed(2)} ms/step (batches of ${BATCH}, no readback), ` +
106 `${ms.toFixed(2)} ms/step incl. sampling readback`,
107 );
108 log(
109 ` compare 'solver' with the ms/step from \`npm run bench -- --lmax ${lmax}\`:\n` +
110 ` same .m, same kernels, no rendering on either side.`,
111 );
113 session.destroy();
114 window.__RESULTS__ = { ok: failures === 0, lines };
115 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
118/**
119 * Run one exact spec and post its final state, for scripts/compare-env.mjs to
120 * compare against the same spec run on the desktop. Query parameters map
121 * straight onto the benchmark's flags — `?state=1&lmax=31&steps=200` — and go
122 * through the same parseArgs, so neither side can quietly use different
123 * defaults.
124 */
125async function dumpState(q: URLSearchParams): Promise<void> {
126 const argv: string[] = [];
127 for (const [k, v] of q) {
128 if (k === 'state') continue;
129 argv.push(`--${k}`, v);
130 }
131 const spec = parseArgs(argv);
132 const model = modelForSpec(spec);
134 const device = await requestShtDevice();
135 const adapter = await describeAdapter(device);
136 const session = await ModelSession.create({
137 device,
138 model,
139 params: spec.params,
140 lmax: spec.lmax,
141 });
142 session.seed(spec.seed);
143 session.step(spec.steps);
144 await session.sync();
145 const state = await session.read(model.state[0]);
146 const digest = digestOf(state, session.sht.fourierMode, adapter);
148 log(`${formatCommand(spec)}\n`);
149 log(`state after ${spec.steps} steps from seed ${spec.seed}:`);
150 log(` ${formatDigest(digest)}`);
151 log(` adapter: ${adapter}`);
152 window.__STATE__ = { digest, state: [...state] };
153 session.destroy();
156async function main(): Promise<void> {
157 const q = new URLSearchParams(location.search);
158 if (q.has('state')) return dumpState(q);
159 if (q.has('soak')) {
160 return soak(Number(q.get('soak')) || 500, Number(q.get('lmax')) || 63);
161 }
162 const device = await requestShtDevice();
164 await transformChecks(device, check, log);
165 await analyticChecks(device, check, log);
166 await modelChecks(device, check, log);
168 window.__RESULTS__ = { ok: failures === 0, lines };
169 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
172main().catch((e) => {
173 const msg = e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e);
174 log(`fatal: ${msg}`);
175 window.__RESULTS__ = { ok: false, fatal: msg, lines };
176});
moveopenescclose