1// Runs the WGSL kernels under Dawn (the `webgpu` package) and compares them with the float64 CPU
2// loop. This is a plain script rather than a node:test file: Dawn's threads abort the process
3// under the node:test runner. Run with `node tests/gpu.check.ts`; a failed check exits non-zero.
4import assert from "node:assert/strict";
5import { create, globals } from "webgpu";
6import { buildSetup } from "../src/model/setup.ts";
7import { CpuSimulator } from "../src/cpu/simulator.ts";
8import { GpuSimulator } from "../src/gpu/simulator.ts";
9import { fixture } from "./helpers.ts";
11Object.assign(globalThis, globals);
13// one Dawn instance and device for the whole file (a second instance crashes the process)
14const gpuInstance = create([]);
15const adapterP = gpuInstance.requestAdapter().then((a) => { if (!a) throw new Error("no WebGPU adapter"); return a.requestDevice(); });
16async function getDevice(): Promise<GPUDevice> { return adapterP; }
18let failures = 0;
19async function test(name: string, fn: () => Promise<void>) {
20 const t0 = performance.now();
21 try { await fn(); console.log(`ok - ${name} (${(performance.now() - t0).toFixed(0)} ms)`); }
22 catch (err) { failures++; console.log(`not ok - ${name}\n ${(err as Error).message}`); }
23}
25async function runBoth(cfg: any, nSamples: number, chunk: number) {
26 const setup = await buildSetup(cfg);
27 const device = await getDevice();
28 const cpu = new CpuSimulator(setup, 7), gpu = new GpuSimulator(device, setup, 4096, 7);
29 const pc: number[] = [], pg: number[] = [];
30 let framesChecked = 0;
31 for (let done = 0; done < nSamples; done += chunk) {
32 const n = Math.min(chunk, nSamples - done);
33 const rc = await cpu.runChunk(n), rg = await gpu.runChunk(n);
34 assert.equal(rg.firstSample, rc.firstSample); assert.equal(rg.pressureFront.length, rc.pressureFront.length);
35 assert.deepEqual(rg.frameSamples, rc.frameSamples);
36 for (let f = 0; f < rc.frames.length; f++) {
37 let num = 0, den = 0;
38 for (let i = 0; i < rc.frames[f].length; i++) { num += (rc.frames[f][i] - rg.frames[f][i]) ** 2; den += rc.frames[f][i] ** 2; }
39 if (den > 0) { assert.ok(Math.sqrt(num / den) < 1e-2, `frame ${f} rel error ${Math.sqrt(num / den)}`); framesChecked++; }
40 }
41 for (let i = 0; i < rc.pressureFront.length; i++) { pc.push(rc.pressureFront[i] - rc.pressureBack[i]); pg.push(rg.pressureFront[i] - rg.pressureBack[i]); }
42 }
43 const dc = await cpu.diagnostics(), dg = await gpu.diagnostics();
44 gpu.dispose();
45 assert.ok(framesChecked > 10, `only ${framesChecked} movie frames compared`);
46 return { setup, pc, pg, dc, dg };
47}
49const rel = (a: number, b: number) => Math.abs(a - b) / Math.max(Math.abs(b), 1e-30);
51test("GPU kernels match the CPU loop on a linear run", async () => {
52 const cfg = structuredClone(fixture.config);
53 cfg.numerics.nonlinear = false;
54 const { pc, pg, dc, dg } = await runBoth(cfg, 420, 150);
55 assert.ok(rel(dg.hits[0].peakForce, dc.hits[0].peakForce) < 1e-4, `peak ${dg.hits[0].peakForce} vs ${dc.hits[0].peakForce}`);
56 assert.equal(Math.round(dg.hits[0].contactTime / 1e-6), Math.round(dc.hits[0].contactTime / 1e-6));
57 assert.ok(rel(dg.hits[0].velocity, dc.hits[0].velocity) < 1e-4);
58 assert.ok(rel(dg.kinetic, dc.kinetic) < 1e-3 && rel(dg.membrane, dc.membrane) < 1e-3 && rel(dg.bending, dc.bending) < 1e-3,
59 `energies ${[dg.kinetic, dg.membrane, dg.bending]} vs ${[dc.kinetic, dc.membrane, dc.bending]}`);
60 assert.ok(rel(dg.maxWoverH, dc.maxWoverH) < 1e-3);
61 let num = 0, den = 0;
62 for (let i = 0; i < pc.length; i++) { num += (pc[i] - pg[i]) ** 2; den += pc[i] ** 2; }
63 assert.ok(pc.length > 200 && Math.sqrt(num / den) < 1e-3, `pressure rel error ${Math.sqrt(num / den)}`);
64});
66test("GPU kernels match the CPU loop on the nonlinear run (float32, short)", async () => {
67 const { pc, pg, dc, dg } = await runBoth(fixture.config, 300, 100);
68 assert.ok(rel(dg.hits[0].peakForce, dc.hits[0].peakForce) < 1e-3, `peak ${dg.hits[0].peakForce} vs ${dc.hits[0].peakForce}`);
69 assert.ok(rel(dg.kinetic + dg.membrane + dg.bending, dc.kinetic + dc.membrane + dc.bending) < 1e-2);
70 let num = 0, den = 0;
71 for (let i = 0; i < pc.length; i++) { num += (pc[i] - pg[i]) ** 2; den += pc[i] ** 2; }
72 assert.ok(Math.sqrt(num / den) < 1e-2, `pressure rel error ${Math.sqrt(num / den)}`);
73});
75console.log(failures ? `${failures} check(s) failed` : "all GPU checks passed");
76// tear the device down and leave explicitly: Dawn aborts when destroyed by the natural exit
77(await getDevice()).destroy();
78process.exit(failures ? 1 : 0);