concept-collection / math-webgpu-sandbox
math-webgpu-sandbox / test / cases.ts
307 lines · 7.1 KBBlameHistoryRaw
1/**
2 * The correctness suite, shared by both GPU stacks: `npm run test:node`
3 * drives it through desktop Dawn, `npm run test:gpu` through headless
4 * Chrome's own WebGPU (SwiftShader when there is no hardware).
5 *
6 * Each case compiles and runs a MATLAB script and checks the values it
7 * prints (via fprintf) against references computed here in f64. Tolerances
8 * are f32-scale: the GPU computes in single precision.
9 */
10import { runScript } from '../src/mgpu/session.ts';
11import { formatFailure } from '../src/mgpu/errors.ts';
13export interface Case {
14 name: string;
15 source: string;
16 /** Expected values for each %-printed number, with relative tolerance. */
17 expect?: { value: number; rel?: number; abs?: number }[];
18 /** Substrings that must appear in the output. */
19 contains?: string[];
20 /** Expected compile failure instead of a run. */
21 fails?: RegExp;
24const N = (v: number, rel = 2e-5): { value: number; rel: number } => ({ value: v, rel });
26/** f64 reference for the fused-chain case. */
27function refFused(): number {
28 const n = 10000;
29 let acc = 0;
30 for (let i = 0; i < n; i++) {
31 const x = i / (n - 1);
32 acc += 3 * x * x - 2 * x + Math.sin(2 * Math.PI * x) / (1 + x * x);
33 }
34 return acc / n;
37export const cases: Case[] = [
38 {
39 name: 'fused elementwise chain',
40 source: `
41n = 10000;
42x = linspace(0, 1, n);
43y = 3*x.^2 - 2*x + sin(2*pi*x)./(1 + x.^2);
44fprintf('%.6f\\n', sum(y(:)) / n);
45`,
46 // 3e-4 relative: SwiftShader's sin/exp are a touch less accurate than
47 // hardware drivers'.
48 expect: [N(refFused(), 3e-4)],
49 },
50 {
51 name: 'gemm small vs f64 reference',
52 source: `
53n = 64;
54A = zeros(n, n) + 1;
55B = zeros(n, n) + 2;
56C = A * B;
57fprintf('%.1f %.1f\\n', C(:)'*C(:)/(n*n), sum(C(:)));
58`,
59 expect: [N(128 * 128), N(128 * 64 * 64)],
60 },
61 {
62 name: 'reductions full and by columns',
63 source: `
64m = 300; n = 5;
65A = zeros(m, n) + 3;
66s = sum(A);
67fprintf('%.1f %.1f %.1f %.1f\\n', s(:)'*s(:)/n, mean(A(:)), max(A(:)), prod(zeros(3,1)+2));
68`,
69 expect: [N(900 * 900), N(3), N(3), N(8)],
70 },
71 {
72 name: 'indexing is a clear compile error',
73 source: `
74n = 1000;
75x = zeros(n, 1);
76for k = 1:50
77 x = x + k;
78end
79fprintf('%.1f\\n', x(1 + 0*x(:)'*x(:)));
80`,
81 fails: /index|slic|supported/i,
82 },
83 {
84 name: 'loop replay accumulates',
85 source: `
86n = 1000;
87x = zeros(n, 1);
88for k = 1:50
89 x = x + k;
90end
91fprintf('%.1f\\n', mean(x));
92`,
93 expect: [N(1275)],
94 },
95 {
96 name: 'rand statistics and loop-fresh draws',
97 source: `
98n = 200000;
99a = rand(n, 1);
100s = zeros(1, 1);
101for k = 1:3
102 s = s + mean(rand(n, 1));
103end
104fprintf('%.3f %.3f %.3f %.3f\\n', mean(a), mean(a.^2), s/3, mean(randn(n,1)));
105`,
106 expect: [
107 { value: 0.5, abs: 0.01 },
108 { value: 1 / 3, abs: 0.01 },
109 { value: 0.5, abs: 0.01 },
110 { value: 0, abs: 0.02 },
111 ],
112 },
113 {
114 name: 'comparisons, logicals, masks (monte carlo pi)',
115 source: `
116n = 400000;
117x = 2*rand(n, 1) - 1;
118y = 2*rand(n, 1) - 1;
119inside = (x.^2 + y.^2) <= 1;
120fprintf('%.3f\\n', 4*mean(inside));
121`,
122 expect: [{ value: Math.PI, abs: 0.03 }],
123 },
124 {
125 name: 'transpose and matrix identities',
126 source: `
127m = 33; n = 17;
128A = rand(m, n);
129B = A';
130d1 = sum(A(:).^2);
131d2 = sum(B(:).^2);
132E = eye(m);
133C = E * A;
134d3 = sum(abs(C(:) - A(:)));
135fprintf('%.6f %.6f\\n', d1 - d2, d3);
136`,
137 expect: [{ value: 0, abs: 1e-3 }, { value: 0, abs: 1e-4 }],
138 },
139 {
140 name: 'gemm matrix-vector agrees with itself',
141 source: `
142n = 48;
143A = rand(n, n);
144v = rand(n, 1);
145w = A * v;
146d = sum(w) - sum(A * v);
147fprintf('%.6f\\n', d);
148`,
149 expect: [{ value: 0, abs: 1e-4 }],
150 },
151 {
152 name: 'tic toc segments and echo',
153 source: `
154n = 100000;
155tic;
156x = rand(n, 1);
157s = mean(x);
158t = toc;
159tic
160y = x + 1;
161toc
162z = 3.5
163`,
164 contains: ['Elapsed time is', 'z =', '3.5'],
165 },
166 {
167 name: 'dot and norm',
168 source: `
169n = 5000;
170a = zeros(n,1) + 2;
171b = zeros(n,1) + 3;
172fprintf('%.1f %.4f\\n', dot(a, b), norm(a) / sqrt(n));
173`,
174 expect: [N(30000), N(2)],
175 },
176 {
177 name: 'min/max elementwise two-arg',
178 source: `
179n = 1000;
180x = linspace(-1, 1, n);
181y = max(x, 0) + min(x, 0);
182fprintf('%.6f\\n', sum(abs(y - x)));
183`,
184 expect: [{ value: 0, abs: 1e-4 }],
185 },
186 {
187 name: 'mod and integer powers',
188 source: `
189x = linspace(0, 10, 101);
190y = mod(x, 3);
191fprintf('%.4f %.4f\\n', max(y(:)), sum((0:4).^2));
192`,
193 expect: [{ value: 2.9, abs: 0.001 }, N(30)],
194 },
195 {
196 name: 'literal row vector uploads',
197 source: `
198v = [1 2 3 4 5];
199fprintf('%.1f\\n', sum(v));
200`,
201 expect: [N(15)],
202 },
203 {
204 name: 'in-place update (aliased kernel) is correct',
205 source: `
206n = 100;
207u = zeros(n, 1) + 1;
208u = u + u.^2;
209u = u * 2;
210fprintf('%.1f\\n', mean(u));
211`,
212 expect: [N(4)],
213 },
214 {
215 name: 'while is a clear compile error',
216 source: `
217x = 1;
218while x < 10
219 x = x + 1;
220end
221`,
222 fails: /while/i,
223 },
224 {
225 name: 'variable-size rand is a clear compile error',
226 source: `
227n = rand() * 100;
228A = rand(n, 1);
229`,
230 fails: /compile time/i,
231 },
232];
234/** The %-formatted numbers a run printed (echo/timing lines stripped). */
235function printedNumbers(output: string): number[] {
236 const out: number[] = [];
237 const kept = output
238 .split('\n')
239 .filter((l) => !/Elapsed time|=/.test(l))
240 .join('\n');
241 for (const m of kept.matchAll(/-?\d+(?:\.\d+)?(?:e[+-]?\d+)?/gi)) {
242 out.push(Number(m[0]));
243 }
244 return out;
247const indent = (s: string): string => s.replace(/^/gm, ' ');
249/** Run every case; log a line per case; return the failure count. */
250export async function runCases(
251 device: GPUDevice,
252 log: (line: string) => void,
253): Promise<number> {
254 let failures = 0;
255 for (const c of cases) {
256 try {
257 const run = await runScript(device, c.source);
258 if (c.fails) {
259 log(`FAIL ${c.name}: expected a compile error, but it ran`);
260 failures++;
261 continue;
262 }
263 if (run.result.error) {
264 log(`FAIL ${c.name}: runtime error: ${run.result.error}`);
265 failures++;
266 continue;
267 }
268 const nums = printedNumbers(run.result.output);
269 let ok = true;
270 (c.expect ?? []).forEach((e, i) => {
271 const got = nums[i];
272 const tol = e.abs ?? Math.abs(e.value) * (e.rel ?? 1e-5) + 1e-12;
273 if (got === undefined || Math.abs(got - e.value) > tol) {
274 log(`FAIL ${c.name}: printed[${i}] = ${got}, want ${e.value} ±${tol}`);
275 ok = false;
276 }
277 });
278 for (const s of c.contains ?? []) {
279 if (!run.result.output.includes(s)) {
280 log(`FAIL ${c.name}: output lacks ${JSON.stringify(s)}`);
281 ok = false;
282 }
283 }
284 if (!ok) {
285 log(` output was:\n${indent(run.result.output)}`);
286 log(` plan:\n${indent(run.planDescription.join('\n'))}`);
287 failures++;
288 } else {
289 log(`ok ${c.name}`);
290 }
291 } catch (e) {
292 if (c.fails) {
293 const msg = formatFailure(e, c.source);
294 if (c.fails.test(msg)) {
295 log(`ok ${c.name} (declined: ${msg.split('\n')[0].slice(0, 90)})`);
296 } else {
297 log(`FAIL ${c.name}: wrong error: ${msg}`);
298 failures++;
299 }
300 } else {
301 log(`FAIL ${c.name}: ${formatFailure(e, c.source)}`);
302 failures++;
303 }
304 }
305 }
306 return failures;