/ proof-of-concept / cymsimweb
Sign in
proof-of-concept / cymsimweb
cymsimweb / tests / gpu.check.ts
94 lines · 6.1 KBBlameHistoryRaw
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}`); }
25async function runBoth(cfg: any, nSamples: number, chunk: number) {
26 const setup = await buildSetup(cfg);
27 const device = await getDevice();
28 const opts = { movieEvery: 7, dispEvery: 50, recordAcceleration: true };
29 const cpu = new CpuSimulator(setup, opts), gpu = new GpuSimulator(device, setup, 4096, opts);
30 const pc: number[] = [], pg: number[] = [];
31 let framesChecked = 0, accChecked = 0, dispChecked = 0;
32 const relErr = (a: ArrayLike<number>, b: ArrayLike<number>) => { let num = 0, den = 0; for (let i = 0; i < a.length; i++) { num += (a[i] - b[i]) ** 2; den += a[i] ** 2; } return den > 0 ? Math.sqrt(num / den) : 0; };
33 for (let done = 0; done < nSamples; done += chunk) {
34 const n = Math.min(chunk, nSamples - done);
35 const rc = await cpu.runChunk(n), rg = await gpu.runChunk(n);
36 assert.equal(rg.firstSample, rc.firstSample); assert.equal(rg.pressureFront.length, rc.pressureFront.length);
37 assert.deepEqual(rg.frameSamples, rc.frameSamples);
38 for (let f = 0; f < rc.frames.length; f++) {
39 let num = 0, den = 0;
40 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; }
41 if (den > 0) { assert.ok(Math.sqrt(num / den) < 1e-2, `frame ${f} rel error ${Math.sqrt(num / den)}`); framesChecked++; }
42 }
43 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]); }
44 // the acceleration record and the file displacement frames, sample by sample
45 assert.equal(rg.acceleration!.length, rc.acceleration!.length);
46 if (rc.acceleration!.length) { const err = relErr(rc.acceleration!, rg.acceleration!); assert.ok(err < 1e-2, `acceleration record rel error ${err}`); accChecked += rc.pressureFront.length; }
47 assert.deepEqual(rg.dispSamples, rc.dispSamples);
48 for (let f = 0; f < rc.dispFrames.length; f++) { const err = relErr(rc.dispFrames[f], rg.dispFrames[f]); assert.ok(err < 1e-2, `disp frame ${f} rel error ${err}`); dispChecked++; }
49 }
50 const dc = await cpu.diagnostics(), dg = await gpu.diagnostics();
51 assert.ok(framesChecked > 10, `only ${framesChecked} movie frames compared`);
52 assert.ok(accChecked > 100 && dispChecked >= 5, `only ${accChecked} acceleration samples and ${dispChecked} displacement frames compared`);
53 // the exported state restarts the GPU loop where it was: the next chunk matches a fresh run of the same length
54 const st = await gpu.exportState();
55 const gpu2 = new GpuSimulator(device, setup, 4096, opts);
56 await gpu2.importState(st);
57 // long enough for the listener delay, so that state missing from the export would show in the pressure
58 const [a, b] = [await gpu.runChunk(300), await gpu2.runChunk(300)];
59 assert.equal(a.firstSample, b.firstSample);
60 assert.ok(relErr(a.pressureFront, b.pressureFront) < 1e-5, "resumed GPU run diverges from the continued one");
61 gpu2.dispose(); gpu.dispose();
62 return { setup, pc, pg, dc, dg };
65const rel = (a: number, b: number) => Math.abs(a - b) / Math.max(Math.abs(b), 1e-30);
67test("GPU kernels match the CPU loop on a linear run", async () => {
68 const cfg = structuredClone(fixture.config);
69 cfg.numerics.nonlinear = false;
70 const { pc, pg, dc, dg } = await runBoth(cfg, 420, 150);
71 assert.ok(rel(dg.hits[0].peakForce, dc.hits[0].peakForce) < 1e-4, `peak ${dg.hits[0].peakForce} vs ${dc.hits[0].peakForce}`);
72 assert.equal(Math.round(dg.hits[0].contactTime / 1e-6), Math.round(dc.hits[0].contactTime / 1e-6));
73 assert.ok(rel(dg.hits[0].velocity, dc.hits[0].velocity) < 1e-4);
74 assert.ok(rel(dg.kinetic, dc.kinetic) < 1e-3 && rel(dg.membrane, dc.membrane) < 1e-3 && rel(dg.bending, dc.bending) < 1e-3,
75 `energies ${[dg.kinetic, dg.membrane, dg.bending]} vs ${[dc.kinetic, dc.membrane, dc.bending]}`);
76 assert.ok(rel(dg.maxWoverH, dc.maxWoverH) < 1e-3);
77 let num = 0, den = 0;
78 for (let i = 0; i < pc.length; i++) { num += (pc[i] - pg[i]) ** 2; den += pc[i] ** 2; }
79 assert.ok(pc.length > 200 && Math.sqrt(num / den) < 1e-3, `pressure rel error ${Math.sqrt(num / den)}`);
80});
82test("GPU kernels match the CPU loop on the nonlinear run (float32, short)", async () => {
83 const { pc, pg, dc, dg } = await runBoth(fixture.config, 300, 100);
84 assert.ok(rel(dg.hits[0].peakForce, dc.hits[0].peakForce) < 1e-3, `peak ${dg.hits[0].peakForce} vs ${dc.hits[0].peakForce}`);
85 assert.ok(rel(dg.kinetic + dg.membrane + dg.bending, dc.kinetic + dc.membrane + dc.bending) < 1e-2);
86 let num = 0, den = 0;
87 for (let i = 0; i < pc.length; i++) { num += (pc[i] - pg[i]) ** 2; den += pc[i] ** 2; }
88 assert.ok(Math.sqrt(num / den) < 1e-2, `pressure rel error ${Math.sqrt(num / den)}`);
89});
91console.log(failures ? `${failures} check(s) failed` : "all GPU checks passed");
92// tear the device down and leave explicitly: Dawn aborts when destroyed by the natural exit
93(await getDevice()).destroy();
94process.exit(failures ? 1 : 0);
↑↓ move↵openescclose