/ concept-collection / turing-surface
Sign in
concept-collection / turing-surface
238 lines · 8.4 KBCodeBlameHistory
2 * Browser validation, in the environment the demo actually ships to.
3 *
4 * Runs the same four 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 {
15 parseArgs,
16 modelForSpec,
17 geometryForSpec,
18 formatCommand,
19 DEFAULT_NITER,
20} from '../src/bench/runSpec.ts';
21import {
22 mGeometryByKey,
23 defaultGeometryParams,
24 DEFAULT_GEOMETRY_KEY,
25} from '../src/geom/registry.ts';
c90d0e2Check reference files in the browser's compare modeJeremy Magland 26import * as h5wasm from 'h5wasm';
0af3386Reaction-diffusion on spherical-harmonic surfacesJeremy Magland 27import { transformChecks } from './transformChecks.ts';
28import { analyticChecks } from './analyticChecks.ts';
29import { modelChecks } from './modelChecks.ts';
30import { geometryChecks } from './geometryChecks.ts';
591a4f5Reduce the Laplace-Beltrami matvec to 6 transforms per species per iterationDan Fortunato 31import { fluxChecks } from './fluxChecks.ts';
f467ffaCompare several solver settings side by side, on one clockJeremy Magland 32import { compareChecks } from './compareChecks.ts';
c90d0e2Check reference files in the browser's compare modeJeremy Magland 33import { referenceChecks, type H5Rt } from './referenceChecks.ts';
35declare global {
36 interface Window {
37 __RESULTS__?: { ok: boolean; fatal?: string; lines: string[] };
38 /** Set by the ?state= mode, for scripts/compare-env.mjs. */
39 __STATE__?: { digest: StateDigest; state: number[] };
40 /** Set by the ?soak= mode, for scripts/compare-perf.mjs. */
41 __SOAK__?: {
42 lmax: number;
43 steps: number;
44 batch: number;
45 solverMsPerStep: number;
46 encodeMsPerStep: number;
47 adapter: string;
48 fourier: 'fft' | 'dft';
49 };
50 }
53const logEl = document.getElementById('log')!;
54const lines: string[] = [];
55let failures = 0;
57function log(s: string): void {
58 lines.push(s);
59 logEl.textContent = lines.join('\n');
60 console.log(s);
63function check(name: string, ok: boolean, detail: string): void {
64 log(`${ok ? 'PASS' : 'FAIL'} ${name} ${detail}`);
65 if (!ok) failures++;
68/**
69 * Solver-only soak, selected with ?soak=<steps>&lmax=<n>.
70 *
71 * No three.js at all, so this is the browser's honest solver rate: the same
72 * batched, no-readback measurement the desktop benchmark reports. If this number
73 * matches the benchmark's but the app's frame cost does not, the difference is
74 * the readback and competing with the renderer for the GPU, not the computation.
75 */
76async function soak(steps: number, lmax: number): Promise<void> {
77 const device = await requestShtDevice();
78 const model = mModels[0];
79 // The desktop benchmark this is compared against resolves its geometry and
80 // iteration count from the same two constants. They have to agree: the
81 // iteration count is unrolled into the step, so a mismatch would compare
82 // two different amounts of work and call the difference "the browser".
83 const geometry = mGeometryByKey(DEFAULT_GEOMETRY_KEY)!;
84 const session = await ModelSession.create({
85 device,
86 model,
87 params: defaultParams(model),
88 lmax,
89 geometry,
90 geometryParams: defaultGeometryParams(geometry),
91 niter: DEFAULT_NITER,
92 });
0ae15cfSeed runs from smooth random fields, and add the blob geometryDan Fortunato 93 await session.seed(5);
95 `soak: ${steps} steps at lmax ${lmax} ` +
96 `(grid ${session.cfg.nlat}x${session.cfg.nphi}, ${geometry.key}, ` +
97 `${DEFAULT_NITER} solve iter, ${session.describe().step.length} ops/step), solver only`,
98 );
100 const BATCH = 25;
101 // Timed separately from the sampling: `solverMs` counts only submitted steps
102 // waited for, never read back, so it is comparable to `npm run bench`.
103 let solverMs = 0;
104 let solverSteps = 0;
105 let encodeMs = 0;
106 const t0 = performance.now();
107 for (let s = 0; s < steps; s += BATCH) {
108 const n = Math.min(BATCH, steps - s);
109 const b0 = performance.now();
110 session.step(n);
111 // CPU-side command encoding, separated from GPU execution: in a browser each
112 // WebGPU call crosses Blink's bindings and Dawn's validation, so on a fast
113 // GPU the encoding can be what actually limits the step rate.
114 const b1 = performance.now();
115 encodeMs += b1 - b0;
116 await session.sync();
117 solverMs += performance.now() - b0;
118 solverSteps += n;
119 const u = await session.read(model.species[0]);
120 let lo = Infinity;
121 let hi = -Infinity;
122 for (const v of u) {
123 if (v < lo) lo = v;
124 if (v > hi) hi = v;
125 }
126 if ((s + BATCH) % 100 === 0) {
127 const mem = (performance as Performance & { memory?: { usedJSHeapSize: number } })
128 .memory;
129 log(
130 ` step ${session.steps} u in [${lo.toFixed(4)}, ${hi.toFixed(4)}]` +
131 (mem ? ` heap ${(mem.usedJSHeapSize / 1048576).toFixed(1)} MB` : ''),
132 );
133 // yield so the page stays responsive and the runner can poll
134 await new Promise((r) => setTimeout(r, 0));
135 }
136 }
137 const ms = (performance.now() - t0) / steps;
139 const final = await session.read(model.species[0]);
140 let finite = true;
141 for (const v of final) if (!Number.isFinite(v)) finite = false;
142 const solverPerStep = solverMs / solverSteps;
143 const encodePerStep = encodeMs / solverSteps;
144 check(
145 `soak: ${steps} steps survived`,
146 finite,
147 `solver ${solverPerStep.toFixed(2)} ms/step (batches of ${BATCH}, no readback), ` +
148 `of which ${encodePerStep.toFixed(3)} ms/step CPU encoding, ` +
149 `${ms.toFixed(2)} ms/step incl. sampling readback`,
150 );
151 log(
152 ` compare 'solver' with the ms/step from \`npm run bench -- --lmax ${lmax}\`:\n` +
153 ` same .m, same kernels, no rendering on either side.`,
154 );
156 window.__SOAK__ = {
157 lmax,
158 steps,
159 batch: BATCH,
160 solverMsPerStep: solverPerStep,
161 encodeMsPerStep: encodePerStep,
162 adapter: await describeAdapter(device),
163 fourier: session.sht.fourierMode,
164 };
165 session.destroy();
166 window.__RESULTS__ = { ok: failures === 0, lines };
167 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
170/**
171 * Run one exact spec and post its final state, for scripts/compare-env.mjs to
172 * compare against the same spec run on the desktop. Query parameters map
173 * straight onto the benchmark's flags — `?state=1&lmax=31&steps=200` — and go
174 * through the same parseArgs, so neither side can quietly use different
175 * defaults.
176 */
177async function dumpState(q: URLSearchParams): Promise<void> {
178 const argv: string[] = [];
179 for (const [k, v] of q) {
180 if (k === 'state') continue;
181 argv.push(`--${k}`, v);
182 }
183 const spec = parseArgs(argv);
184 const model = modelForSpec(spec);
186 const device = await requestShtDevice();
187 const adapter = await describeAdapter(device);
188 const session = await ModelSession.create({
189 device,
190 model,
191 params: spec.params,
192 lmax: spec.lmax,
193 geometry: geometryForSpec(spec),
194 geometryParams: spec.geometryParams,
195 niter: spec.niter,
196 });
0ae15cfSeed runs from smooth random fields, and add the blob geometryDan Fortunato 197 await session.seed(spec.seed);
0af3386Reaction-diffusion on spherical-harmonic surfacesJeremy Magland 198 session.step(spec.steps);
199 await session.sync();
200 const state = await session.read(model.state[0]);
201 const digest = digestOf(state, session.sht.fourierMode, adapter);
203 log(`${formatCommand(spec)}\n`);
204 log(`state after ${spec.steps} steps from seed ${spec.seed}:`);
205 log(` ${formatDigest(digest)}`);
206 log(` adapter: ${adapter}`);
207 window.__STATE__ = { digest, state: [...state] };
208 session.destroy();
211async function main(): Promise<void> {
212 const q = new URLSearchParams(location.search);
213 if (q.has('state')) return dumpState(q);
214 if (q.has('soak')) {
215 return soak(Number(q.get('soak')) || 500, Number(q.get('lmax')) || 63);
216 }
217 const device = await requestShtDevice();
219 await transformChecks(device, check, log);
220 await analyticChecks(device, check, log);
221 await modelChecks(device, check, log);
3b2f40cFix the GPU suite in CI: keep it short, and split the metric toleranceJeremy Magland 222 // The sweep is opt-in here (?sweep=1): it is a few seconds on desktop Dawn
223 // but minutes in a browser, where each session recompiles its unrolled step.
224 await geometryChecks(device, check, log, { sweep: q.has('sweep') });
591a4f5Reduce the Laplace-Beltrami matvec to 6 transforms per species per iterationDan Fortunato 225 await fluxChecks(device, check, log, { ab: q.has('sweep') });
f467ffaCompare several solver settings side by side, on one clockJeremy Magland 226 await compareChecks(device, check, log);
c90d0e2Check reference files in the browser's compare modeJeremy Magland 227 // '/' is the wasm module's in-memory filesystem — nothing touches disk.
228 await referenceChecks(h5wasm as unknown as H5Rt, (name) => `/${name}`, check, log);
230 window.__RESULTS__ = { ok: failures === 0, lines };
231 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
234main().catch((e) => {
235 const msg = e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e);
236 log(`fatal: ${msg}`);
237 window.__RESULTS__ = { ok: false, fatal: msg, lines };
238});
moveopenescclose