/ proof-of-concept / cymmodes
Sign in
proof-of-concept / cymmodes
cymmodes / tests / gpu.check.ts
97 lines · 5.2 KBBlameHistoryRaw
1// Runs the WGSL kernels under Dawn (the `webgpu` package) and compares them with the float64 CPU
2// loop: same modal initial condition or drive, same projection, no hits. This is a plain script
3// rather than a node:test file because Dawn's threads abort the process under the test runner.
4// Run with `node tests/gpu.check.ts`; a failed check exits non-zero.
5import assert from "node:assert/strict";
6import { create, globals } from "webgpu";
7import { cloneConfig } from "../src/config/yaml.ts";
8import { CpuSimulator } from "../src/cpu/simulator.ts";
9import { buildDrive, buildProjection, modalAmplitude, modalInitialState, modeShape, type DriveSpec } from "../src/drive.ts";
10import { GpuSimulator } from "../src/gpu/simulator.ts";
11import { buildSetup } from "../src/model/setup.ts";
12import type { BackendOptions } from "../src/model/simulator.ts";
13import { buildCondensationOperator } from "../src/precompute/assemble.ts";
14import { computeModes } from "../src/precompute/pipeline.ts";
15import { miniConfig, noProgress, OPTIONS } from "./helpers.ts";
17Object.assign(globalThis, globals);
19const gpuInstance = create([]);
20const deviceP = gpuInstance.requestAdapter().then((a) => { if (!a) throw new Error("no WebGPU adapter"); return a.requestDevice(); });
22let failures = 0;
23async function test(name: string, fn: () => Promise<void>) {
24 const t0 = performance.now();
25 try { await fn(); console.log(`ok - ${name} (${(performance.now() - t0).toFixed(0)} ms)`); }
26 catch (err) { failures++; console.log(`not ok - ${name}\n ${(err as Error).message}`); }
29const base = miniConfig();
30const modes = computeModes(base, OPTIONS, noProgress);
31const MODE = 3;
33function mw(setup: Awaited<ReturnType<typeof buildSetup>>): Float64Array {
34 const N = setup.mesh.n, Mw = new Float64Array(N);
35 for (let n = 0; n < N; n++) Mw[n] = setup.shell.M[5 * n + 2];
36 return Mw;
39async function runBoth(nonlinear: boolean, mechanism: "decay" | "forced", nSamples: number, chunk: number) {
40 const cfg = cloneConfig(base);
41 cfg.numerics.nonlinear = nonlinear;
42 const setup = await buildSetup(cfg);
43 const shape = modeShape(modes, MODE, 1);
44 const projection = buildProjection(modes, mw(setup), 1, 16);
45 const opts: BackendOptions = { projection, traceMode: MODE };
46 let initial: { q: Float64Array; v: Float64Array } | null = null;
47 if (mechanism === "decay") {
48 const cond = buildCondensationOperator(setup.mesh, setup.shell, noProgress);
49 initial = modalInitialState(shape, cond.complement(shape), modalAmplitude(modes, shape, 2));
50 } else {
51 const spec: DriveSpec = { mode: MODE, mechanism: "forced", amplitude: 0.3, detuneCents: 0, rampTime: 0.003, driveTime: 1 };
52 opts.drive = buildDrive(spec, modes, shape, mw(setup), cfg.material.sigma0, 2 * Math.PI * modes.freq[MODE], setup.dt);
53 }
54 const device = await deviceP;
55 const cpu = new CpuSimulator(setup, opts), gpu = new GpuSimulator(device, setup, 4096, opts);
56 if (initial) { cpu.setState(initial.q, initial.v); gpu.setState(initial.q, initial.v); }
57 let projChecked = 0, traceErr = 0, traceRef = 0;
58 for (let done = 0; done < nSamples; done += chunk) {
59 const n = Math.min(chunk, nSamples - done);
60 const rc = await cpu.runChunk(n), rg = await gpu.runChunk(n);
61 assert.deepEqual(rg.projSamples, rc.projSamples);
62 assert.equal(rg.pressureFront.length, rc.pressureFront.length);
63 for (let s = 0; s < rc.proj.length; s++) {
64 let num = 0, den = 0;
65 for (let i = 0; i < rc.proj[s].length; i++) { num += (rc.proj[s][i] - rg.proj[s][i]) ** 2; den += rc.proj[s][i] ** 2; }
66 if (den > 0) { assert.ok(Math.sqrt(num / den) < 1e-2, `snapshot ${projChecked} rel error ${Math.sqrt(num / den)}`); projChecked++; }
67 }
68 assert.equal(rg.traceBase, rc.traceBase);
69 assert.equal(rg.trace.length, rc.trace.length);
70 for (let i = 0; i < rc.trace.length; i++) { traceErr += (rc.trace[i] - rg.trace[i]) ** 2; traceRef += rc.trace[i] ** 2; }
71 }
72 if (traceRef > 0) assert.ok(Math.sqrt(traceErr / traceRef) < 1e-2, `trace rel error ${Math.sqrt(traceErr / traceRef)}`);
73 const dc = await cpu.diagnostics(), dg = await gpu.diagnostics();
74 gpu.dispose();
75 assert.ok(projChecked > 5, `only ${projChecked} modal snapshots compared`);
76 return { dc, dg };
79const rel = (a: number, b: number) => Math.abs(a - b) / Math.max(Math.abs(b), 1e-30);
81await test("GPU matches the CPU loop on a nonlinear free decay with projection", async () => {
82 const { dc, dg } = await runBoth(true, "decay", 400, 150);
83 assert.ok(rel(dg.kinetic, dc.kinetic) < 2e-3, `kinetic ${dg.kinetic} vs ${dc.kinetic}`);
84 assert.ok(rel(dg.membrane, dc.membrane) < 2e-3, `membrane ${dg.membrane} vs ${dc.membrane}`);
85 assert.ok(rel(dg.bending, dc.bending) < 2e-3, `bending ${dg.bending} vs ${dc.bending}`);
86 assert.ok(rel(dg.maxWoverH, dc.maxWoverH) < 2e-3);
87});
89await test("GPU matches the CPU loop on a linear forced run", async () => {
90 const { dc, dg } = await runBoth(false, "forced", 400, 130);
91 assert.ok(rel(dg.kinetic, dc.kinetic) < 2e-3, `kinetic ${dg.kinetic} vs ${dc.kinetic}`);
92 assert.ok(rel(dg.bending, dc.bending) < 2e-3, `bending ${dg.bending} vs ${dc.bending}`);
93});
95if (failures) { console.error(`${failures} check(s) failed`); process.exit(1); }
96console.log("gpu checks passed");
97process.exit(0);
↑↓ move↵openescclose