concept-collection / fastandaccurate
fastandaccurate / test / gpu-test.ts
76 lines · 2.9 KBBlameHistoryRaw
1// Convergence test for the WebGPU-runtime solvers, run where a WebGPU
2// device can be had: npx tsx test/gpu-test.ts
3// Exits quietly with a notice when there is none, which is the usual case
4// on a headless machine and in CI.
5//
6// The expectations live in test/expected.ts, keyed by source directory, as
7// for the other two suites. mfs-gpu's floors are deliberately far looser
8// than mfs's, because WebGPU has no f64; MUST_NOT_REACH pins that down from
9// the other side, so a run that suddenly got double precision fails here
10// rather than quietly changing what the pair of curves means.
12import { getInstance, INSTANCES } from "../src/problems/laplace2d/spec";
13import { SOLVERS, solverSourceDir, sweepNFor } from "../src/solvers";
14import { runSweepGpu } from "../src/harness/webgpuRun";
15import { gpuUnavailableReason, requestGpu } from "../src/harness/webgpuDevice";
16import { DEFAULT_TIMING } from "../src/harness/timing";
17import { MUST_NOT_REACH, MUST_REACH } from "./expected";
19const why = await gpuUnavailableReason();
20if (why !== null) {
21 console.log(`no WebGPU device here; skipping the WebGPU solver tests\n ${why}`);
22 process.exit(0);
24const { adapter, via } = await requestGpu();
25console.log(`WebGPU: ${adapter} (${via})`);
27// Accuracy, not speed: one timed run per point is enough here.
28const TEST_TIMING = { ...DEFAULT_TIMING, minTimedRuns: 1, timeBudgetSeconds: 0 };
30let failures = 0;
31for (const solver of SOLVERS.filter((s) => s.runtime === "webgpu")) {
32 const dir = solverSourceDir(solver);
33 for (const inst of INSTANCES) {
34 const reach = MUST_REACH[dir]?.[inst.id];
35 if (reach === undefined) continue;
36 console.log(`\n== ${inst.id} / ${solver.id} (WebGPU)`);
37 console.log(" n relMax relL2 solve(s)");
38 let best = Infinity;
39 const points = await runSweepGpu({
40 instance: getInstance(inst.id),
41 solver,
42 timing: TEST_TIMING,
43 onPoint: (p) => {
44 best = Math.min(best, p.relMax);
45 console.log(
46 ` ${String(p.n).padStart(4)} ${p.relMax.toExponential(3)} ` +
47 `${p.relL2.toExponential(3)} ${p.solveSeconds.toFixed(4)}`
48 );
49 },
50 });
51 if (points.length !== sweepNFor(solver, inst.id).length) {
52 console.log(" FAIL: sweep returned the wrong number of points");
53 failures++;
54 continue;
55 }
56 const notReach = MUST_NOT_REACH[dir]?.[inst.id];
57 if (best > reach) {
58 console.log(` FAIL: best relMax ${best.toExponential(2)} > ${reach}`);
59 failures++;
60 } else if (notReach !== undefined && best < notReach) {
61 console.log(
62 ` FAIL: best relMax ${best.toExponential(2)} < ${notReach} ` +
63 "(single precision no longer caps this solver)"
64 );
65 failures++;
66 } else {
67 console.log(` ok (best relMax ${best.toExponential(2)})`);
68 }
69 }
72if (failures > 0) {
73 console.error(`\n${failures} failure(s)`);
74 process.exit(1);
76console.log("\nall WebGPU checks passed");