666db68matmul-bench: browser GEMM benchmark (JS, WebGPU, custom C WASM, libFLAME/BLIS WASM)Jeremy Magland 1# CLAUDE.md
3Tips for future agents working in this repo.
5## Architecture
7```
8wasm/matmul.c naive + blocked/SIMD (f64x2) kernels, plus a pthreads
9 matmul_blocked_mt (rows split across threads, -DMATMUL_MT).
10 wasm/build-wasm.sh (emsdk) builds it twice:
11 - st ES module -> wasm/dist/matmul.{js,wasm}
12 - threaded -> public/matmul/matmul_mt.{js,wasm}
13blis/matmul_blis.c thin wrapper over libFLAME's dgemm_ (BLIS-backed); built
14 st + mt (pthreads) by blis/build-blis-wasm.sh, linking the
15 prebuilt .a libraries from the sibling libflame2wasm
16 checkout (../../../libflame2wasm), into
17 public/matmul/matmul_blis_{st,mt}.{js,wasm} (committed)
18native/bench_native.c same style dgemm benchmark, linked against OpenBLAS,
19 run standalone outside the browser
20src/methods/ one file per method, all implementing the MatmulMethod
21 interface in types.ts (id, precision, available(), run?())
22src/methods/benchWorker.ts + workerClient.ts
23 JS and both non-threaded custom WASM kernels run inside a
24 Vite module Web Worker — naive JS/WASM at n=2048 take ~10s
25 and would freeze the tab on the main thread. WebGPU runs on
26 the main thread (async). The worker regenerates inputs from
27 (n, seed) rather than receiving them over postMessage.
28src/methods/threadedClient.ts + public/matmul/worker.js
29 ALL pthread-capable modules (threaded custom C, BLIS st +
30 mt) run in ONE shared classic worker (not a Vite module
31 worker) — threaded builds spawn pthread workers from it
32 (nested workers), and importScripts of the emscripten glue
33 is the proven path (mirrors libflame2wasm's bench_worker.js).
34 worker.js has a kind->module REGISTRY; every module exports
35 a (a,b,c,n,nthreads) function (st builds ignore nthreads).
36 Served verbatim from public/, out of Vite's module graph.
37src/components/ BenchmarkRunner (orchestrates runs, thread selector) +
38 ResultsTable
39src/data/nativeReference.ts hardcoded numbers from native/bench_native,
40 hand-copied in — no runtime fetch
41public/coi-serviceworker.js vendored (gzuidhof, MIT) — adds COOP/COEP so
42 SharedArrayBuffer works on GitHub Pages (no header control)
43```
45## Key gotchas
47- **WASM .wasm URL resolution.** `wasm/dist/matmul.js` (emscripten glue) is
48 imported directly; its sibling `matmul.wasm` is resolved via Vite's `?url`
49 import and passed through `locateFile` — same pattern qhull-wasm-demo uses
50 for `qhull-wasm/dist/qhull.wasm`. Don't rely on emscripten's default
51 `import.meta.url`-relative lookup working under Vite's bundler.
52- **Two threaded builds, one classic worker.** The threaded custom kernel
53 (`matmul_mt`, from `wasm/matmul.c` with `-pthread -DMATMUL_MT`) and the
54 threaded BLIS build (`matmul_blis_mt`) both use pthreads/SharedArrayBuffer
55 and both dispatch through `public/matmul/worker.js`. The non-threaded custom
56 kernels (`matmul.c` st build) have no `-pthread` and run in the Vite module
57 worker instead.
58- **Cross-origin isolation for threaded methods.** `matmul_mt` and
59 `matmul_blis_mt` need `crossOriginIsolated === true` (COOP: same-origin,
60 COEP: require-corp). Vite dev/preview set these headers (see vite.config.ts).
61 GitHub Pages can't set headers, so `public/coi-serviceworker.js` installs
62 them via a service worker (page reloads once on first visit to gain control).
63 Their `available()` gates on `crossOriginIsolated`; without it the cell = n/a.
64- **What's committed vs CI-built under public/matmul/.** The BLIS modules
65 (`matmul_blis_{st,mt}.*`) are committed — `blis/build-blis-wasm.sh` links the
66 ~30 MB of prebuilt `.a` files in `../../../libflame2wasm`, which aren't in
67 this repo and can't be rebuilt in CI. The threaded custom module
68 (`matmul_mt.*`) is gitignored and CI-built (only needs `matmul.c` + emsdk,
69 like `wasm/dist`). `worker.js` is a committed source file. Rebuild BLIS
70 locally after changing `matmul_blis.c` or the upstream libs.
71- **Row-major via operand swap.** dgemm_ is column-major; `matmul_blis` calls
72 it with operands swapped — `dgemm_(B, A)` computes row-major C = A*B in the
73 same flat buffer — so BLIS `c[0]` matches the other (row-major) methods.
74- **Deterministic seeded inputs, not transferred arrays.** Every method for
75 a given `n` multiplies bit-identical A/B (from `generateMatrix(n, seed)`);
76 the worker regenerates them from the same seed rather than receiving the
77 arrays over `postMessage`, since structured-cloning multi-megabyte
78 `Float64Array`s per run would be slower than regenerating.
79- Rebuilding `wasm/dist/*` requires emsdk on PATH or at `~/emsdk`
80 (`wasm/build-wasm.sh` sources `~/emsdk/emsdk_env.sh` if `emcc` isn't found).
81 CI installs it via `mymindstorm/setup-emsdk`.
83## Testing
85- `npm run dev`, click through each method's run button at a small size
86 (n=128) first and check the cross-check panel — the f64 methods (JS, the
87 three WASM kernels, both BLIS builds) should agree to ~1e-9, WebGPU (f32)
88 should be close but not identical. The "cross-origin isolated ✓" chip must
89 be green for the threaded methods to be available.
90- `wasm/build-wasm.sh && npm run build` to verify the CI path locally
91 (build-wasm.sh emits both the st module and the threaded matmul_mt).
92- `blis/build-blis-wasm.sh` rebuilds the committed BLIS artifacts (needs the
93 sibling libflame2wasm checkout + emsdk).
94- `native/build.sh && native/bench_native` regenerates the values pasted into
95 `src/data/nativeReference.ts` (no automated round-trip — hand-copy after
96 running).