/ proof-of-concept / cymsimweb
Sign in
proof-of-concept / cymsimweb
cymsimweb / tests / gpu.check.ts
70 lines · 3.9 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 cpu = new CpuSimulator(setup), gpu = new GpuSimulator(device, setup, 4096);
29 const pc: number[] = [], pg: number[] = [];
30 for (let done = 0; done < nSamples; done += chunk) {
31 const n = Math.min(chunk, nSamples - done);
32 const rc = await cpu.runChunk(n), rg = await gpu.runChunk(n);
33 assert.equal(rg.firstSample, rc.firstSample); assert.equal(rg.pressureFront.length, rc.pressureFront.length);
34 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]); }
35 }
36 const dc = await cpu.diagnostics(), dg = await gpu.diagnostics();
37 gpu.dispose();
38 return { setup, pc, pg, dc, dg };
41const rel = (a: number, b: number) => Math.abs(a - b) / Math.max(Math.abs(b), 1e-30);
43test("GPU kernels match the CPU loop on a linear run", async () => {
44 const cfg = structuredClone(fixture.config);
45 cfg.numerics.nonlinear = false;
46 const { pc, pg, dc, dg } = await runBoth(cfg, 420, 150);
47 assert.ok(rel(dg.hits[0].peakForce, dc.hits[0].peakForce) < 1e-4, `peak ${dg.hits[0].peakForce} vs ${dc.hits[0].peakForce}`);
48 assert.equal(Math.round(dg.hits[0].contactTime / 1e-6), Math.round(dc.hits[0].contactTime / 1e-6));
49 assert.ok(rel(dg.hits[0].velocity, dc.hits[0].velocity) < 1e-4);
50 assert.ok(rel(dg.kinetic, dc.kinetic) < 1e-3 && rel(dg.membrane, dc.membrane) < 1e-3 && rel(dg.bending, dc.bending) < 1e-3,
51 `energies ${[dg.kinetic, dg.membrane, dg.bending]} vs ${[dc.kinetic, dc.membrane, dc.bending]}`);
52 assert.ok(rel(dg.maxWoverH, dc.maxWoverH) < 1e-3);
53 let num = 0, den = 0;
54 for (let i = 0; i < pc.length; i++) { num += (pc[i] - pg[i]) ** 2; den += pc[i] ** 2; }
55 assert.ok(pc.length > 200 && Math.sqrt(num / den) < 1e-3, `pressure rel error ${Math.sqrt(num / den)}`);
56});
58test("GPU kernels match the CPU loop on the nonlinear run (float32, short)", async () => {
59 const { pc, pg, dc, dg } = await runBoth(fixture.config, 300, 100);
60 assert.ok(rel(dg.hits[0].peakForce, dc.hits[0].peakForce) < 1e-3, `peak ${dg.hits[0].peakForce} vs ${dc.hits[0].peakForce}`);
61 assert.ok(rel(dg.kinetic + dg.membrane + dg.bending, dc.kinetic + dc.membrane + dc.bending) < 1e-2);
62 let num = 0, den = 0;
63 for (let i = 0; i < pc.length; i++) { num += (pc[i] - pg[i]) ** 2; den += pc[i] ** 2; }
64 assert.ok(Math.sqrt(num / den) < 1e-2, `pressure rel error ${Math.sqrt(num / den)}`);
65});
67console.log(failures ? `${failures} check(s) failed` : "all GPU checks passed");
68// tear the device down and leave explicitly: Dawn aborts when destroyed by the natural exit
69(await getDevice()).destroy();
70process.exit(failures ? 1 : 0);
↑↓ move↵openescclose