CLAUDE.md#
Tips for future agents working in this repo.
Architecture#
wasm/matmul.c naive + blocked/SIMD (f64x2) kernels, plus a pthreads
matmul_blocked_mt (rows split across threads, -DMATMUL_MT).
wasm/build-wasm.sh (emsdk) builds it twice:
- st ES module -> wasm/dist/matmul.{js,wasm}
- threaded -> public/matmul/matmul_mt.{js,wasm}
blis/matmul_blis.c thin wrapper over libFLAME's dgemm_ (BLIS-backed); built
st + mt (pthreads) by blis/build-blis-wasm.sh into
public/matmul/matmul_blis_{st,mt}.{js,wasm}. It links the
WASM .a libraries from a sibling concept-collection/
libflame2wasm checkout (LIBFLAME=..., default
../../../libflame2wasm), which are built by that repo's
build-all-wasm.sh. NOT committed — CI builds them.
native/bench_native.c same style dgemm benchmark, linked against OpenBLAS,
run standalone outside the browser
src/methods/ one file per method, all implementing the MatmulMethod
interface in types.ts (id, precision, available(), run?())
src/methods/benchWorker.ts + workerClient.ts
JS and both non-threaded custom WASM kernels run inside a
Vite module Web Worker — naive JS/WASM at n=2048 take ~10s
and would freeze the tab on the main thread. WebGPU runs on
the main thread (async). The worker regenerates inputs from
(n, seed) rather than receiving them over postMessage.
src/methods/threadedClient.ts + public/matmul/worker.js
ALL pthread-capable modules (threaded custom C, BLIS st +
mt) run in ONE shared classic worker (not a Vite module
worker) — threaded builds spawn pthread workers from it
(nested workers), and importScripts of the emscripten glue
is the proven path (mirrors libflame2wasm's bench_worker.js).
worker.js has a kind->module REGISTRY; every module exports
a (a,b,c,n,nthreads) function (st builds ignore nthreads).
Served verbatim from public/, out of Vite's module graph.
src/components/ BenchmarkRunner (orchestrates runs, thread selector) +
ResultsTable
src/data/nativeReference.ts hardcoded numbers from native/bench_native,
hand-copied in — no runtime fetch
public/coi-serviceworker.js vendored (gzuidhof, MIT) — adds COOP/COEP so
SharedArrayBuffer works on GitHub Pages (no header control)
Key gotchas#
- WASM .wasm URL resolution.
wasm/dist/matmul.js(emscripten glue) is imported directly; its siblingmatmul.wasmis resolved via Vite's?urlimport and passed throughlocateFile— same pattern qhull-wasm-demo uses forqhull-wasm/dist/qhull.wasm. Don't rely on emscripten's defaultimport.meta.url-relative lookup working under Vite's bundler. - Two threaded builds, one classic worker. The threaded custom kernel
(
matmul_mt, fromwasm/matmul.cwith-pthread -DMATMUL_MT) and the threaded BLIS build (matmul_blis_mt) both use pthreads/SharedArrayBuffer and both dispatch throughpublic/matmul/worker.js. The non-threaded custom kernels (matmul.cst build) have no-pthreadand run in the Vite module worker instead. - Cross-origin isolation for threaded methods.
matmul_mtandmatmul_blis_mtneedcrossOriginIsolated === true(COOP: same-origin, COEP: require-corp). Vite dev/preview set these headers (see vite.config.ts). GitHub Pages can't set headers, sopublic/coi-serviceworker.jsinstalls them via a service worker (page reloads once on first visit to gain control). Theiravailable()gates oncrossOriginIsolated; without it the cell = n/a. - Everything under public/matmul/ except worker.js is CI-built (gitignored).
The threaded custom module (
matmul_mt.*) needs onlymatmul.c+ emsdk. The BLIS modules (matmul_blis_*.*) need the WASM libflame/BLIS.afiles, which CI produces by cloning the sibling concept-collection/libflame2wasm repo and running itsbuild-all-wasm.sh(result cached, keyed on that repo's SHA — see.github/workflows/deploy.yml).worker.jsis the only committed file there. Locally: check out libflame2wasm beside this repo, run itsbuild-all-wasm.shonce, thenblis/build-blis-wasm.sh. - Row-major via operand swap. dgemm_ is column-major;
matmul_bliscalls it with operands swapped —dgemm_(B, A)computes row-major C = A*B in the same flat buffer — so BLISc[0]matches the other (row-major) methods. - Deterministic seeded inputs, not transferred arrays. Every method for
a given
nmultiplies bit-identical A/B (fromgenerateMatrix(n, seed)); the worker regenerates them from the same seed rather than receiving the arrays overpostMessage, since structured-cloning multi-megabyteFloat64Arrays per run would be slower than regenerating. - Rebuilding
wasm/dist/*requires emsdk on PATH or at~/emsdk(wasm/build-wasm.shsources~/emsdk/emsdk_env.shifemccisn't found). CI installs it viamymindstorm/setup-emsdk.
Testing#
npm run dev, click through each method's run button at a small size (n=128) first and check the cross-check panel — the f64 methods (JS, the three WASM kernels, both BLIS builds) should agree to ~1e-9, WebGPU (f32) should be close but not identical. The "cross-origin isolated ✓" chip must be green for the threaded methods to be available.wasm/build-wasm.sh && npm run buildto verify the CI path locally (build-wasm.sh emits both the st module and the threaded matmul_mt).blis/build-blis-wasm.shrebuilds the BLIS modules (needs emsdk + a sibling libflame2wasm checkout already built via itsbuild-all-wasm.sh; override the location withLIBFLAME=/path/to/libflame2wasm).native/build.sh && native/bench_nativeregenerates the values pasted intosrc/data/nativeReference.ts(no automated round-trip — hand-copy after running).