Scale examples to 1-4 s of GPU work so tic/toc comparisons are meaningful
8 changed files+85−32
CLAUDE.mdmodified+4−0View file
@@ -79,3 +79,7 @@ test/cases.ts One suite, two harnesses: scripts/test-node.ts (Dawn via
7979 - `npm run test:gpu` — same suite, headless Chrome.
8080 - `npx vite-node scripts/bench-probe.ts` — timing sanity (fused loop ~1
8181 kernel/iteration, GEMM GFLOP/s printout).
82+- `npx vite-node scripts/time-examples.ts` — runs every examples/*.m on Dawn
83+ with per-tic..toc timings. The examples are sized to take ~1-4 s each on
84+ this machine's Intel iGPU so tic/toc comparisons aren't noise; keep them in
85+ that range when editing.
README.mdmodified+4−3View file
@@ -10,18 +10,19 @@ third column without leaving the page.
1010 **Live page:** https://concept-collection.github.io/math-webgpu-sandbox/
1111
1212 ```matlab
13-n = 4000000;
13+n = 8000000;
1414 x = rand(n, 1);
1515 y = zeros(n, 1);
1616 tic;
17-for k = 1:200
17+for k = 1:600
1818 y = y + 0.1*sin(x + k) .* exp(-x) + x.^2;
1919 end
2020 toc
2121 fprintf('checksum %.4f\n', mean(y));
2222 ```
2323
24-That loop body is **one** GPU kernel, compiled once and replayed 200 times.
24+That loop body is **one** GPU kernel, compiled once and replayed 600 times —
25+about five billion element-updates, a few seconds on an integrated GPU.
2526
2627 ## How it works
2728
examples/fused_loop.mmodified+7−6View file
@@ -1,13 +1,14 @@
1-% A long chain of elementwise math on 4 million elements, fused into ONE
2-% GPU kernel per source line and replayed 200 times.
3-% Paste this whole script into MATLAB to compare timings.
4-n = 4000000;
1+% A long chain of elementwise math on 8 million elements, fused into ONE
2+% GPU kernel per source line and replayed 600 times (about 5 billion updates).
3+% Paste this whole script into MATLAB to compare (lower niter if slow).
4+n = 8000000;
5+niter = 600;
56 x = rand(n, 1);
67 y = zeros(n, 1);
78 tic;
8-for k = 1:200
9+for k = 1:niter
910 y = y + 0.1*sin(x + k) .* exp(-x) + x.^2;
1011 end
1112 t = toc;
12-fprintf('%.0f million element-updates/sec\n', 200*n/1e6/t);
13+fprintf('%.0f million element-updates/sec\n', niter*n/1e6/t);
1314 fprintf('checksum %.4f (rand differs from MATLAB; expect ~equal, not equal)\n', mean(y));
examples/logistic_ensemble.mmodified+8−6View file
@@ -1,11 +1,13 @@
1-% One million logistic maps x <- r x (1 - x) iterated in lockstep.
2-% The loop body compiles once; 500 iterations replay on the GPU.
3-n = 1000000;
1+% Two million logistic maps x <- r x (1 - x) iterated 4000 times in
2+% lockstep (8 billion updates). The loop body compiles once and replays.
3+n = 2000000;
4+niter = 4000;
45 r = 3.5 + 0.5*rand(n, 1);
56 x = rand(n, 1);
67 tic;
7-for k = 1:500
8+for k = 1:niter
89 x = r .* x .* (1 - x);
910 end
10-toc
11-fprintf('mean x after 500 iterations: %.4f\n', mean(x));
11+t = toc;
12+fprintf('%.0f million map-updates/sec\n', niter*n/1e6/t);
13+fprintf('mean x after %d iterations: %.4f\n', niter, mean(x));
examples/matmul.mmodified+6−4View file
@@ -1,14 +1,16 @@
1-% Tiled f32 matrix multiply (the GPU has no f64).
1+% Tiled f32 matrix multiply (the GPU has no f64): 30 products of
2+% 2048 x 2048, about half a teraflop of work.
23 % For an apples-to-apples MATLAB run, also try single precision there:
34 % A = rand(n, n, 'single'); B = rand(n, n, 'single');
4-n = 1024;
5+n = 2048;
6+reps = 30;
57 A = rand(n, n);
68 B = rand(n, n);
79 C = A * B; % warm-up
810 tic;
9-for k = 1:20
11+for k = 1:reps
1012 C = A * B;
1113 end
1214 t = toc;
13-fprintf('%.1f GFLOP/s over 20 multiplies of %dx%d\n', 2*n^3*20/1e9/t, n, n);
15+fprintf('%.1f GFLOP/s over %d multiplies of %dx%d\n', 2*n^3*reps/1e9/t, reps, n, n);
1416 fprintf('checksum %.2f\n', sum(C(:))/n^2);
examples/monte_carlo_pi.mmodified+14−7View file
@@ -1,9 +1,16 @@
1-% Monte Carlo estimate of pi: comparisons and logicals fuse too, so the
2-% mask never touches memory -- one fused pass plus the reduction.
1+% Monte Carlo estimate of pi from 20 BILLION samples, drawn in 2000 batches.
2+% (Pasting into MATLAB? Cut batches to ~50 -- generating this many doubles
3+% takes CPUs minutes, which is rather the point.)
4+% The draws, the comparison and the mean all fuse into one reduction pass,
5+% so each batch's 10 million points are generated, tested and counted
6+% without ever touching GPU memory.
37 n = 10000000;
8+batches = 2000;
9+s = zeros(1, 1);
410 tic;
5-x = 2*rand(n, 1) - 1;
6-y = 2*rand(n, 1) - 1;
7-pi_est = 4*mean((x.^2 + y.^2) <= 1);
8-toc
9-fprintf('pi ~ %.6f (n = %d)\n', pi_est, n);
11+for k = 1:batches
12+ s = s + mean(((2*rand(n,1) - 1).^2 + (2*rand(n,1) - 1).^2) <= 1);
13+end
14+t = toc;
15+fprintf('pi ~ %.6f (%.1f billion samples, %.0f million samples/sec)\n', ...
16+ 4*s/batches, batches*n/1e9, batches*n/1e6/t);
examples/reductions.mmodified+11−6View file
@@ -1,11 +1,16 @@
1-% Reductions take a fused loader: dot, norm and max each read their
2-% operands exactly once -- sum(a.*b) is one pass, not two.
1+% Reductions with fused loaders, 300 times over 10 million elements:
2+% dot, norm and max each read their operands exactly once per pass --
3+% sum(a.*b) is one sweep of memory, not two.
34 n = 10000000;
5+reps = 300;
46 a = rand(n, 1);
57 b = rand(n, 1);
68 tic;
7-d = dot(a, b);
8-nrm = norm(a - b);
9-mx = max(abs(a - b));
10-toc
9+for k = 1:reps
10+ d = dot(a, b);
11+ nrm = norm(a - b);
12+ mx = max(abs(a - b));
13+end
14+t = toc;
1115 fprintf('dot %.0f norm %.2f max %.6f\n', d, nrm, mx);
16+fprintf('%.1f GB/s effective\n', reps*6*n*4/1e9/t);
scripts/time-examples.tsadded+31−0View file
@@ -0,0 +1,31 @@
1+/* Time every example on desktop Dawn: npx vite-node scripts/time-examples.ts */
2+import { readFileSync, readdirSync } from 'node:fs';
3+import { installWebGpu } from './nodeWebGpu.ts';
4+import { runScript } from '../src/mgpu/session.ts';
5+import { formatFailure } from '../src/mgpu/errors.ts';
6+
7+async function main(): Promise<void> {
8+ console.log(await installWebGpu());
9+ const adapter = await navigator.gpu.requestAdapter();
10+ const device = await adapter!.requestDevice({
11+ requiredLimits: {
12+ maxStorageBufferBindingSize: adapter!.limits.maxStorageBufferBindingSize,
13+ maxBufferSize: adapter!.limits.maxBufferSize,
14+ },
15+ });
16+ for (const f of readdirSync('examples').sort()) {
17+ if (!f.endsWith('.m')) continue;
18+ const source = readFileSync(`examples/${f}`, 'utf8');
19+ try {
20+ const run = await runScript(device, source);
21+ const segs = run.result.segments.map((s) => s.seconds.toFixed(3)).join(', ');
22+ console.log(`\n=== ${f} [tic..toc: ${segs} s · total ${run.result.totalSeconds.toFixed(3)} s]`);
23+ console.log(run.result.output.trimEnd());
24+ if (run.result.error) console.log('ERROR:', run.result.error);
25+ } catch (e) {
26+ console.log(`\n=== ${f} COMPILE FAIL: ${formatFailure(e, source)}`);
27+ }
28+ }
29+ process.exit(0);
30+}
31+main().catch((e) => { console.error(e); process.exit(1); });