CLAUDE.md#
Tips for future agents working in this repo.
Architecture#
src/mgpu/compile.ts MATLAB source -> numbl IR: parseMFile + lowerProgram
(whole-script lowering, NOT turing-surface's per-function
specialization), numbl's inlinePass, then fuse.ts.
src/mgpu/patches.ts Type-rule patches applied to numbl's JIT builtin
registry (registerBuiltin overwrites by name): precise
shapes for comparisons, tensor &/|/~, two-arg max/min,
matrix rand, and randn registered from scratch. Only
types — numbl's emitters never run here.
src/mgpu/fuse.ts Folds single-use _mtoc2_* temps numbl's inline pass
declines (tensor-producing Calls like sin(x), logicals),
so one source line = one kernel.
src/mgpu/wgsl.ts Fused elementwise emitter -> one WGSL kernel per Assign.
Also: inline generators (rand/randn/linspace/ranges/eye
computed from the linear index), loop-var uniforms,
runtime scalars as 1-element buffers, exact-value
folding at every node.
src/mgpu/kernels.ts Non-elementwise WGSL: tiled column-major GEMM, tiled
transpose, 2-pass full reduction + per-column reduction,
all with shapes baked in as constants.
src/mgpu/plan.ts IR statements -> static op sequence: buffers, pipelines
(cached by WGSL text), bind groups, aliasing (X(:) etc.
as views when the source has exactly one assignment),
scratch+copy-back for in-place updates, `for` bodies
planned once with per-iteration dynamic-offset uniform
slots (256 B each), host ops (tic/toc/disp/fprintf/echo).
src/mgpu/run.ts Executor: streams ops into command encoders; tic/toc
flush + await onSubmittedWorkDone (that's what makes toc
MATLAB-comparable); readbacks for printing.
src/cpu/cpuRunner.ts Optional CPU column via createNumblSession
(numbl/browser) — numbl's own worker, f64, lazy-loaded
because it's a ~3 MB chunk.
test/cases.ts One suite, two harnesses: scripts/test-node.ts (Dawn via
the `webgpu` npm package) and test/test-page.ts +
scripts/test-gpu.mjs (headless Chrome, SwiftShader
fallback).
Key gotchas#
resolve.preserveSymlinks: truein vite.config.ts is load-bearing. numbl is afile:symlink; without it the dev server canonicalizes the symlink for some import chains but not others, loading numbl's builtin registry TWICE — patches.ts then patches one instance while the lowerer consults the other ("JS-JIT 'rand' supports only the scalar form"), and raw-realpath requests trip the fs allow list.- The registry patch is per-module-instance. The CPU runner is safe from it because numbl/browser runs in its own worker; don't move patching somewhere the CPU path could share.
- Everything is f32 and column-major.
A(:),reshapeand vector transpose are buffer views (or plain copies when the source is reassigned); matrix transpose is a real kernel. - Aliased writes need scratch+copy-back (
u = u + 1, GEMM with output aliasing an input): WebGPU forbids binding one buffer as both read-only and read-write in a bind group. tocas a bare statement lowers to a Call namedtoc_print, nottoc. String literals carry aChartype kind, not Numeric.- Loop bodies may not contain host ops (tic/toc/disp/fprintf/echoes) — they're compiled once and replayed; the planner declines with a message.
- rand inside loops: generator kernels bind every enclosing loop's iteration counter and mix it into the hash, or every iteration would draw identical values. Don't "simplify" that away.
- numbl HEAD is pinned in both workflows (
NUMBL_REF); the compiler surface this repo relies on is declared in src/mgpu/numbl.d.ts, so a numbl change breaks the build here with a type diff. CI builds numbl's dist-browser (npm run build:browser) because it is not committed.
Testing#
npm run test:node— 17-case correctness suite on desktop Dawn (real GPU on this machine). References computed in f64 in the test file; tolerances are f32-scale, loosened to 3e-4 where SwiftShader's transcendentals lag.npm run test:gpu— same suite, headless Chrome.npx vite-node scripts/bench-probe.ts— timing sanity (fused loop ~1 kernel/iteration, GEMM GFLOP/s printout).