Remove CPU comparison run and timing table; tighten footnote
7 changed files+31−227
.github/workflows/ci.ymlmodified+7−11View file
@@ -15,11 +15,11 @@ jobs:
1515 cache: npm
1616 # numbl is a `file:../../numbl` dependency: the GPU compiler reaches its
1717 # JIT internals (parser, lowerer, IR, inline pass, builtin registry)
18- # through the `numbl-src` vite alias — those need no build — and the CPU
19- # comparison imports `numbl/browser`, whose dist-browser/ is NOT
20- # committed, so it is built here. Pinned so a change to the internals
21- # cannot silently break this repo — the surface we rely on is written
22- # down in src/mgpu/numbl.d.ts.
18+ # through the `numbl-src` vite alias. Clone it where that relative path
19+ # expects it. Pinned so a change to those internals cannot silently
20+ # break this repo — the surface we rely on is written down in
21+ # src/mgpu/numbl.d.ts. numbl's own dependencies are NOT needed: the
22+ # slice we import is self-contained TypeScript.
2323 - name: Check out numbl (sibling dependency)
2424 env:
2525 NUMBL_REF: 38ce14046d64d03ecf05cb57def53057a6bc64ab
@@ -27,12 +27,8 @@ jobs:
2727 git clone --filter=blob:none --no-checkout \
2828 https://github.com/flatironinstitute/numbl.git "$GITHUB_WORKSPACE/../../numbl"
2929 git -C "$GITHUB_WORKSPACE/../../numbl" checkout --quiet "$NUMBL_REF"
30- # --ignore-scripts both times: npm would run numbl's `prepare` (husky).
31- - name: Build numbl/browser (dist-browser)
32- run: |
33- cd "$GITHUB_WORKSPACE/../../numbl"
34- npm ci --ignore-scripts
35- npm run build:browser
30+ # --ignore-scripts: npm runs a linked package's `prepare` script, and
31+ # numbl's is husky, which is not installed here.
3632 - run: npm ci --ignore-scripts
3733 # The suite compiles MATLAB to compute shaders, so it needs a GPU; the
3834 # browser run below falls back to SwiftShader when there is none.
.github/workflows/deploy.ymlmodified+1−6View file
@@ -25,7 +25,7 @@ jobs:
2525 with:
2626 node-version: 24
2727 cache: npm
28- # See ci.yml for why numbl is cloned and dist-browser built.
28+ # See ci.yml for why numbl is cloned (pinned; no numbl build needed).
2929 - name: Check out numbl (sibling dependency)
3030 env:
3131 NUMBL_REF: 38ce14046d64d03ecf05cb57def53057a6bc64ab
@@ -33,11 +33,6 @@ jobs:
3333 git clone --filter=blob:none --no-checkout \
3434 https://github.com/flatironinstitute/numbl.git "$GITHUB_WORKSPACE/../../numbl"
3535 git -C "$GITHUB_WORKSPACE/../../numbl" checkout --quiet "$NUMBL_REF"
36- - name: Build numbl/browser (dist-browser)
37- run: |
38- cd "$GITHUB_WORKSPACE/../../numbl"
39- npm ci --ignore-scripts
40- npm run build:browser
4136 - run: npm ci --ignore-scripts
4237 - run: npm run test:node -- --skip-without-gpu
4338 - run: npm run build
CLAUDE.mdmodified+5−8View file
@@ -33,9 +33,6 @@ src/mgpu/plan.ts IR statements -> static op sequence: buffers, pipelines
3333 src/mgpu/run.ts Executor: streams ops into command encoders; tic/toc
3434 flush + await onSubmittedWorkDone (that's what makes toc
3535 MATLAB-comparable); readbacks for printing.
36-src/cpu/cpuRunner.ts Optional CPU column via createNumblSession
37- (numbl/browser) — numbl's own worker, f64, lazy-loaded
38- because it's a ~3 MB chunk.
3936 test/cases.ts One suite, two harnesses: scripts/test-node.ts (Dawn via
4037 the `webgpu` npm package) and test/test-page.ts +
4138 scripts/test-gpu.mjs (headless Chrome, SwiftShader
@@ -50,9 +47,9 @@ test/cases.ts One suite, two harnesses: scripts/test-node.ts (Dawn via
5047 registry TWICE — patches.ts then patches one instance while the lowerer
5148 consults the other ("JS-JIT 'rand' supports only the scalar form"), and
5249 raw-realpath requests trip the fs allow list.
53-- **The registry patch is per-module-instance.** The CPU runner is safe from
54- it because numbl/browser runs in its own worker; don't move patching
55- somewhere the CPU path could share.
50+- **The registry patch is per-module-instance** — anything that would run
51+ numbl's own engine in this page (a CPU-comparison feature was removed in
52+ 2026-08) must live in a separate worker so it never sees patched builtins.
5653 - **Everything is f32 and column-major.** `A(:)`, `reshape` and vector
5754 transpose are buffer views (or plain copies when the source is reassigned);
5855 matrix transpose is a real kernel.
@@ -68,8 +65,8 @@ test/cases.ts One suite, two harnesses: scripts/test-node.ts (Dawn via
6865 identical values. Don't "simplify" that away.
6966 - numbl HEAD is pinned in both workflows (`NUMBL_REF`); the compiler surface
7067 this repo relies on is declared in src/mgpu/numbl.d.ts, so a numbl change
71- breaks the build here with a type diff. CI builds numbl's dist-browser
72- (`npm run build:browser`) because it is not committed.
68+ breaks the build here with a type diff. CI needs no numbl build — the
69+ imported compiler slice is self-contained TypeScript.
7370
7471 ## Testing
7572
README.mdmodified+3−5View file
@@ -26,8 +26,7 @@ about five billion element-updates, a few seconds on an integrated GPU.
2626
2727 ## How it works
2828
29-The compiler front end is numbl's own: the script is parsed and lowered by
30-numbl's JIT pipeline (reached through a `numbl-src` vite alias, the same
29+The script is parsed and lowered by numbl's JIT pipeline (reached through a `numbl-src` vite alias, the same
3130 arrangement [turing-surface](https://github.com/concept-collection/turing-surface)
3231 uses), which fixes every type and shape at compile time — `n = 2048;
3332 A = rand(n);` pins static shapes via exact-value propagation. The back end is
@@ -92,9 +91,8 @@ npm run test:node # correctness suite on desktop WebGPU (Dawn)
9291 npm run test:gpu # same suite in headless Chrome (SwiftShader fallback)
9392 ```
9493
95-The CPU-comparison button needs numbl's `dist-browser` build
96-(`npm run build:browser` in the numbl checkout); the GPU path runs from
97-numbl's TypeScript sources directly and needs no numbl build.
94+The GPU compiler runs from numbl's TypeScript sources directly (through the
95+`numbl-src` alias), so no numbl build is needed.
9896
9997 ## License
10098
index.htmlmodified+4−22View file
@@ -115,16 +115,6 @@
115115 }
116116 .console .err { color: var(--err); }
117117 .console .meta { color: var(--ink-2); }
118- #cpuconsole { border-top: 1px solid var(--line); max-height: 14em; display: none; }
119- #timings {
120- margin-top: 12px; font-size: 13.5px; font-variant-numeric: tabular-nums;
121- border-collapse: collapse; display: none;
122- }
123- #timings td, #timings th {
124- border: 1px solid var(--line); padding: 4px 12px; text-align: right;
125- }
126- #timings th { background: var(--pane-bg); font-weight: 600; }
127- #timings td:first-child, #timings th:first-child { text-align: left; }
128118 details { margin-top: 12px; font-size: 13px; }
129119 details pre {
130120 margin: 6px 0 0; padding: 8px 10px; overflow: auto;
@@ -158,7 +148,6 @@
158148 <select id="example"></select>
159149 </label>
160150 <button id="run" class="primary">Run on GPU</button>
161- <button id="runcpu" title="Run the same script through numbl's CPU engine in a worker">Run on CPU (numbl)</button>
162151 <button id="copy" title="Copy the script for pasting into MATLAB">Copy script</button>
163152 <span id="device"></span>
164153 </div>
@@ -173,24 +162,17 @@
173162 <div class="box" id="outbox">
174163 <div class="box-head"><span>output</span><span id="runstate"></span></div>
175164 <pre id="console" class="console"></pre>
176- <pre id="cpuconsole" class="console"></pre>
177165 </div>
178166 </div>
179- <table id="timings"></table>
180167 <details id="plandetails" hidden>
181168 <summary>compiled GPU plan</summary>
182169 <pre id="plan"></pre>
183170 </details>
184171 <p id="blurb">
185- The GPU computes in <b>f32</b> (WebGPU has no f64) while MATLAB defaults to double —
186- timings compare fairly, values agree to single precision. For the closest MATLAB
187- comparison use <code>single</code> arrays. <code>rand</code> here is a deterministic
188- counter-based generator: statistics match MATLAB's, individual draws do not.
189- Supported: elementwise math (fused per source line, including comparisons and
190- logicals), <code>A*B</code>, transpose, <code>sum/mean/prod/max/min/norm/dot</code>,
191- <code>A(:)</code>/<code>reshape</code> (free), <code>for</code> loops (replayed, not
192- unrolled), <code>tic/toc/disp/fprintf</code>. Not (yet) supported: indexing/slicing
193- beyond <code>(:)</code>, <code>if/while</code>, complex numbers, user functions.
172+ The GPU computes in f32 (WebGPU has no f64) — for the closest MATLAB comparison,
173+ use <code>single</code> arrays there. <code>rand</code> is deterministic here:
174+ statistics match MATLAB's, individual draws don't. Unsupported constructs fail
175+ with a compile error rather than a wrong answer.
194176 </p>
195177 </main>
196178 <script type="module" src="/src/main.ts"></script>
src/cpu/cpuRunner.tsdeleted+0−76View file
@@ -1,76 +0,0 @@
1-/**
2- * The optional CPU comparison: run the very same script through numbl's
3- * normal engine (interpreter + its own JIT) in a worker numbl manages.
4- *
5- * tic/toc, fprintf and disp behave natively there, so the output — including
6- * the "Elapsed time is ..." lines — is directly comparable to the GPU pane
7- * and to real MATLAB. numbl computes in f64, which is also a useful
8- * cross-check on the GPU's f32 results.
9- *
10- * A fresh session per run keeps workspace state from leaking between runs.
11- * mip is disabled: sandbox scripts are self-contained MATLAB, and skipping
12- * the bootstrap keeps first-run latency down.
13- */
14-import { createNumblSession } from 'numbl/browser';
15-
16-export interface CpuRunResult {
17- output: string;
18- /** Wall time of the whole script, as seen from the host. */
19- totalSeconds: number;
20- error?: string;
21- aborted?: boolean;
22-}
23-
24-export interface CpuRunHandle {
25- result: Promise<CpuRunResult>;
26- /** Cooperative stop (needs cross-origin isolation to preempt loops). */
27- cancel: () => void;
28-}
29-
30-export function runOnCpu(
31- source: string,
32- onOutput?: (text: string) => void,
33-): CpuRunHandle {
34- let disposed = false;
35- let sessionRef: { dispose(): void; interrupt(): void } | null = null;
36-
37- const result = (async (): Promise<CpuRunResult> => {
38- let output = '';
39- const session = await createNumblSession({
40- mip: false,
41- persistSystem: false,
42- displayResults: true,
43- onOutput: (text: string) => {
44- output += text;
45- onOutput?.(text);
46- },
47- });
48- sessionRef = session;
49- if (disposed) {
50- session.dispose();
51- return { output: '', totalSeconds: 0, aborted: true };
52- }
53- const t0 = performance.now();
54- try {
55- const res = await session.execute(source);
56- const totalSeconds = (performance.now() - t0) / 1000;
57- return {
58- output,
59- totalSeconds,
60- error: res.ok ? undefined : (res.error ?? 'numbl error'),
61- aborted: res.aborted === true,
62- };
63- } finally {
64- session.dispose();
65- }
66- })();
67-
68- return {
69- result,
70- cancel: () => {
71- disposed = true;
72- sessionRef?.interrupt();
73- sessionRef?.dispose();
74- },
75- };
76-}
src/main.tsmodified+11−99View file
@@ -1,11 +1,11 @@
11 /**
2- * The sandbox page: editor on the left, output on the right, a timing table
3- * underneath that lines up GPU tic/toc segments with the CPU run's.
2+ * The sandbox page: editor on the left, output on the right. The tic/toc
3+ * numbers the script prints ARE the timing report — compare them with a
4+ * MATLAB run of the same script.
45 */
56 import { CodeEditor } from './editor/codeEditor.ts';
67 import { requestSandboxDevice, runScript, type SandboxGpu } from './mgpu/session.ts';
78 import { formatFailure } from './mgpu/errors.ts';
8-import type { CpuRunHandle } from './cpu/cpuRunner.ts';
99 import fusedLoop from '../examples/fused_loop.m?raw';
1010 import matmul from '../examples/matmul.m?raw';
1111 import monteCarloPi from '../examples/monte_carlo_pi.m?raw';
@@ -27,12 +27,9 @@ const $ = <T extends HTMLElement>(id: string): T => {
2727 };
2828
2929 const consoleEl = $<HTMLPreElement>('console');
30-const cpuConsoleEl = $<HTMLPreElement>('cpuconsole');
3130 const runBtn = $<HTMLButtonElement>('run');
32-const runCpuBtn = $<HTMLButtonElement>('runcpu');
3331 const copyBtn = $<HTMLButtonElement>('copy');
3432 const exampleSel = $<HTMLSelectElement>('example');
35-const timingsEl = $<HTMLTableElement>('timings');
3633 const planDetails = $<HTMLElement>('plandetails');
3734 const planEl = $<HTMLPreElement>('plan');
3835 const runState = $<HTMLElement>('runstate');
@@ -59,51 +56,18 @@ exampleSel.addEventListener('change', () => {
5956 });
6057 editor.value = EXAMPLES[0].source;
6158
62-const append = (el: HTMLElement, text: string, cls?: string): void => {
59+const append = (text: string, cls?: string): void => {
6360 if (cls) {
6461 const span = document.createElement('span');
6562 span.className = cls;
6663 span.textContent = text;
67- el.appendChild(span);
64+ consoleEl.appendChild(span);
6865 } else {
69- el.appendChild(document.createTextNode(text));
66+ consoleEl.appendChild(document.createTextNode(text));
7067 }
71- el.scrollTop = el.scrollHeight;
68+ consoleEl.scrollTop = consoleEl.scrollHeight;
7269 };
7370
74-/** Latest timing results, merged into the table by tic..toc pair. */
75-const lastSeconds: { gpu: number[]; cpu: number[] } = { gpu: [], cpu: [] };
76-let gpuTotal: number | null = null;
77-let cpuTotal: number | null = null;
78-
79-function renderTimings(): void {
80- const n = Math.max(lastSeconds.gpu.length, lastSeconds.cpu.length);
81- if (n === 0 && gpuTotal === null && cpuTotal === null) {
82- timingsEl.style.display = 'none';
83- return;
84- }
85- const fmt = (s: number | undefined | null): string =>
86- s === undefined || s === null ? '—' : s >= 0.1 ? `${s.toFixed(3)} s` : `${(s * 1000).toFixed(2)} ms`;
87- const rows: string[] = [
88- '<tr><th></th><th>GPU</th><th>CPU (numbl)</th><th>CPU / GPU</th></tr>',
89- ];
90- for (let i = 0; i < n; i++) {
91- const g = lastSeconds.gpu[i];
92- const c = lastSeconds.cpu[i];
93- const ratio = g !== undefined && c !== undefined ? `${(c / g).toFixed(1)}×` : '—';
94- rows.push(
95- `<tr><td>tic…toc #${i + 1}</td><td>${fmt(g)}</td><td>${fmt(c)}</td><td>${ratio}</td></tr>`,
96- );
97- }
98- const totalRatio =
99- gpuTotal !== null && cpuTotal !== null ? `${(cpuTotal / gpuTotal).toFixed(1)}×` : '—';
100- rows.push(
101- `<tr><td>whole script</td><td>${fmt(gpuTotal)}</td><td>${fmt(cpuTotal)}</td><td>${totalRatio}</td></tr>`,
102- );
103- timingsEl.innerHTML = rows.join('');
104- timingsEl.style.display = 'table';
105-}
106-
10771 let gpu: SandboxGpu | null = null;
10872 (async () => {
10973 try {
@@ -119,8 +83,7 @@ let gpu: SandboxGpu | null = null;
11983 gpuWarn.hidden = false;
12084 gpuWarn.innerHTML =
12185 `<b>No WebGPU here.</b> ${e instanceof Error ? e.message : String(e)} — ` +
122- `Chrome/Edge have it on by default; Firefox and Safari are rolling it out. ` +
123- `The CPU run still works.`;
86+ `Chrome/Edge have it on by default; Firefox and Safari are rolling it out.`;
12487 runBtn.disabled = true;
12588 }
12689 })();
@@ -133,83 +96,32 @@ async function runGpu(): Promise<void> {
13396 runBtn.disabled = true;
13497 runState.textContent = 'compiling…';
13598 consoleEl.textContent = '';
136- lastSeconds.gpu = [];
137- gpuTotal = null;
138- renderTimings();
13999 planDetails.hidden = true;
140100 const source = editor.value;
141101 try {
142- const t0 = performance.now();
143102 const run = await runScript(gpu.device, source, (text) => {
144103 runState.textContent = 'running…';
145- append(consoleEl, text);
104+ append(text);
146105 });
147- void t0;
148106 if (run.result.error) {
149- append(consoleEl, `\n${run.result.error}\n`, 'err');
107+ append(`\n${run.result.error}\n`, 'err');
150108 }
151109 append(
152- consoleEl,
153110 `\n[compile ${(run.compileSeconds * 1000).toFixed(0)} ms · ` +
154111 `run ${run.result.totalSeconds.toFixed(3)} s · f32 · ${gpu.description}]\n`,
155112 'meta',
156113 );
157- lastSeconds.gpu = run.result.segments.map((s) => s.seconds);
158- gpuTotal = run.result.totalSeconds;
159114 planEl.textContent = run.planDescription.join('\n') || '(no GPU ops)';
160115 planDetails.hidden = false;
161- renderTimings();
162116 runState.textContent = '';
163117 } catch (e) {
164- append(consoleEl, formatFailure(e, source) + '\n', 'err');
118+ append(formatFailure(e, source) + '\n', 'err');
165119 runState.textContent = '';
166120 } finally {
167121 runBtn.disabled = !gpu;
168122 }
169123 }
170124
171-let cpuHandle: CpuRunHandle | null = null;
172-runCpuBtn.addEventListener('click', () => {
173- if (cpuHandle) {
174- cpuHandle.cancel();
175- cpuHandle = null;
176- runCpuBtn.textContent = 'Run on CPU (numbl)';
177- return;
178- }
179- void runCpu();
180-});
181-async function runCpu(): Promise<void> {
182- cpuConsoleEl.style.display = 'block';
183- cpuConsoleEl.textContent = '';
184- append(cpuConsoleEl, '[CPU · numbl engine in a worker · f64]\n', 'meta');
185- lastSeconds.cpu = [];
186- cpuTotal = null;
187- renderTimings();
188- runCpuBtn.textContent = 'Stop CPU run';
189- // numbl's browser engine is a couple of megabytes; load it on first use.
190- const { runOnCpu } = await import('./cpu/cpuRunner.ts');
191- const handle = runOnCpu(editor.value, (text) => append(cpuConsoleEl, text));
192- cpuHandle = handle;
193- try {
194- const res = await handle.result;
195- if (res.error) append(cpuConsoleEl, `\n${res.error}\n`, 'err');
196- if (res.aborted) append(cpuConsoleEl, `\n[stopped]\n`, 'meta');
197- else {
198- append(cpuConsoleEl, `[total ${res.totalSeconds.toFixed(3)} s]\n`, 'meta');
199- // Line the CPU's tic..toc pairs up with the GPU's by order.
200- lastSeconds.cpu = [...res.output.matchAll(/Elapsed time is ([0-9.eE+-]+) seconds/g)]
201- .map((m) => Number(m[1]));
202- cpuTotal = res.totalSeconds;
203- renderTimings();
204- }
205- } catch (e) {
206- append(cpuConsoleEl, `${e instanceof Error ? e.message : String(e)}\n`, 'err');
207- } finally {
208- if (cpuHandle === handle) cpuHandle = null;
209- runCpuBtn.textContent = 'Run on CPU (numbl)';
210- }
211-}
212-
213125 copyBtn.addEventListener('click', () => {
214126 void navigator.clipboard.writeText(editor.value).then(() => {
215127 copyBtn.textContent = 'Copied ✓';