1// Runs a solver whose manifest declares runtime "webgpu": one that is
2// TypeScript and WGSL rather than a MATLAB file, and executes on a WebGPU
3// device. Used from the browser (where the device is the page's) and from
4// the command line (where it is Dawn's, through the optional `webgpu`
5// package).
6//
7// The protocol is the one in docs/problems/laplace-dirichlet-2d.md and the
8// counting is identical to the numbl and MATLAB runners: one timed warmup
9// reported as the cold time, a second untimed one, then timed runs until
10// the policy is satisfied, of which the fastest is reported. What differs
11// is the clock. A GPU solver has no tic/toc inside its own runtime, so the
12// time is host wall clock around a run that ends by awaiting the device:
13// the work is submitted and the result read back before the clock stops,
14// which is the same synchronization point MATLAB's synchronous tic/toc
15// gives. Shader compilation and pipeline creation happen once per device
16// rather than per run, so, like numbl's JIT, they land outside the timed
17// runs.
19import { buildProblem } from "../problems/laplace2d/problem";
20import { evalErrors } from "../problems/laplace2d/exact";
21import type { Laplace2dInstance } from "../problems/laplace2d/spec";
22import { getWebgpuSolver } from "../solvers/webgpuSolvers";
23import { sweepNFor, type SolverManifest } from "../solvers";
24import { DEFAULT_TIMING, type TimingPolicy } from "./timing";
25import type { RunPoint } from "./runner";
27export const GPU_TIMER = "host clock around submit and read-back";
29export interface GpuPointRequest {
30 instance: Laplace2dInstance;
31 solverId: string;
32 n: number;
33 timing?: TimingPolicy;
34 wantGrid?: boolean;
35}
37export async function runPointGpu(req: GpuPointRequest): Promise<RunPoint> {
38 const timing = req.timing ?? DEFAULT_TIMING;
39 const wantGrid = req.wantGrid ?? false;
40 const solver = await getWebgpuSolver(req.solverId);
41 const prob = buildProblem(req.instance, wantGrid);
42 const { n } = req;
44 const t0 = performance.now();
45 let out = await solver.run(prob, n, wantGrid);
46 const coldSeconds = (performance.now() - t0) / 1000;
47 out = await solver.run(prob, n, wantGrid);
49 const times: number[] = [];
50 let total = 0;
51 while (
52 times.length < timing.maxTimedRuns &&
53 (times.length < timing.minTimedRuns || total < timing.timeBudgetSeconds)
54 ) {
55 const t = performance.now();
56 out = await solver.run(prob, n, wantGrid);
57 const dt = (performance.now() - t) / 1000;
58 times.push(dt);
59 total += dt;
60 }
62 const { relMax, relL2 } = evalErrors(req.instance, out.uEval);
63 return {
64 n,
65 solveSeconds: Math.min(...times),
66 solveSecondsAll: times,
67 coldSeconds,
68 relMax,
69 relL2,
70 uEval: out.uEval,
71 uGrid: out.uGrid,
72 };
73}
75export interface GpuSweepOptions {
76 instance: Laplace2dInstance;
77 solver: SolverManifest;
78 timing?: TimingPolicy;
79 maxN?: number;
80 onPoint?: (point: RunPoint, index: number, total: number) => void;
81}
83export async function runSweepGpu(opts: GpuSweepOptions): Promise<RunPoint[]> {
84 const ns = sweepNFor(opts.solver, opts.instance.id).filter(
85 (n) => opts.maxN === undefined || n <= opts.maxN
86 );
87 const points: RunPoint[] = [];
88 for (const [i, n] of ns.entries()) {
89 const p = await runPointGpu({
90 instance: opts.instance,
91 solverId: opts.solver.id,
92 n,
93 timing: opts.timing,
94 });
95 points.push(p);
96 opts.onPoint?.(p, i, ns.length);
97 }
98 return points;
99}