Quazim0t0 commited on
Commit
93d9b2e
·
verified ·
1 Parent(s): f92cfa9

Web demo: exact kernel gates + continuous cross-device kernel verification

Browse files
web/public/app.js CHANGED
@@ -565,19 +565,40 @@ function onFragment(peerId, buf) { // returns full message when co
565
  return out.buffer;
566
  }
567
 
568
- // ---- gradient wire format: [int32 step][uint32 whash][f32 loss][f32 grad...]
569
- // whash = FNV-1a of the sender's weights BEFORE this step's update, so every
570
- // peer can verify the whole group is still training the same model.
571
- // loss = the sender's local batch loss, so everyone can show the true
572
- // cluster-average loss and log what each device contributes.
573
- function packGrad(step, whash, loss, grad) {
574
- const buf = new ArrayBuffer(12 + grad.byteLength);
 
 
 
 
575
  new Int32Array(buf, 0, 1)[0] = step;
576
- new Uint32Array(buf, 4, 1)[0] = whash;
577
- new Float32Array(buf, 8, 1)[0] = loss;
578
- new Float32Array(buf, 12).set(grad);
579
  return buf;
580
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
  function hashWeights() { // FNV-1a over the flat param bytes
582
  const f = Transformer.getFlatParams(model);
583
  const b = new Uint8Array(f.buffer, f.byteOffset, f.byteLength);
@@ -618,16 +639,16 @@ function onGrad(peerId, buf) {
618
  rosters.set(iv[1], [...iv.slice(3, 3 + iv[2])].map(n => "p" + n));
619
  wake(); return;
620
  }
621
- const whash = new Uint32Array(buf, 4, 1)[0];
622
- const loss = new Float32Array(buf, 8, 1)[0];
623
- const grad = new Float32Array(buf.slice(12));
624
  if (!incoming.has(step)) incoming.set(step, new Map());
625
  incoming.get(step).set(peerId, grad);
626
  if (!peerHashes.has(step)) peerHashes.set(step, new Map());
627
- peerHashes.get(step).set(peerId, { hash: whash, loss });
628
  wake(); // resolve waits immediately (no polling)
629
  }
630
- function broadcastGrad(step, whash, loss, grad) { return broadcast(packGrad(step, whash, loss, grad)); }
631
  // Event-driven: re-checked on every gradient arrival and peer departure, plus a
632
  // coarse fallback timer (background tabs throttle timers to ~1s, so the old
633
  // 15ms poll was the bottleneck there). Peers that left are dropped from the
@@ -680,6 +701,8 @@ async function train(cfg, resume) {
680
  const opt = TrainCore.makeAdam(model.nParams, { lr: cfg.lr });
681
  if (resume) opt.setState(resume.adam); // bit-identical moments from the leader
682
  pendingJoins.clear();
 
 
683
  log(`training started — cohort ${cohort.length} peer(s), world ${cohort.length + 1}, ` +
684
  `width=${cfg.c} seq=${cfg.t} batch=${cfg.b}×${cohort.length + 1}, optimizer ${opt.name}` +
685
  (cohort.length ? `, sync ${iLead ? "led by me" : "led by " + nmeOf(leaderId)}` : "") +
@@ -712,9 +735,14 @@ async function train(cfg, resume) {
712
  if (lastLocal && lastLocal.s === s) {
713
  ({ loss, grad } = lastLocal);
714
  } else {
 
 
 
 
715
  ({ loss, grad } = await localStep());
 
716
  lastLocal = { s, loss, grad };
717
- await broadcastGrad(s, whash, loss, grad);
718
  }
719
  }
720
  let all;
@@ -782,6 +810,16 @@ async function train(cfg, resume) {
782
  if (hs) {
783
  const bad = roster.find(id => id !== myId && hs.has(id) && hs.get(id).hash !== whash);
784
  if (bad) { halted = `weights diverged from ${nmeOf(bad)} (detected at step ${s + 1})`; break; }
 
 
 
 
 
 
 
 
 
 
785
  }
786
  // cluster-average loss over this step's actual contributors
787
  let lossSum = 0, lossN = 0;
@@ -842,7 +880,7 @@ function cfgSliders() { return [ui.cfgC, ui.cfgT, ui.cfgB, ui.cfgSteps, ui.cfgLr
842
  // (re)build the mini transformer — deterministic shared init (same seeds on
843
  // every peer); each device samples its own batch windows from the shared corpus
844
  function buildModel(cfg) {
845
- model = Transformer.init(cfg, L, compute); // compute.bgemm = fused batched GPU path
846
  trainedSteps = 0;
847
  }
848
 
@@ -894,6 +932,11 @@ function buildModel(cfg) {
894
  return; // no signaling, no training
895
  }
896
  ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label} · block-scaled INT8, batched + fused epilogue, through verified units`;
 
 
 
 
 
897
  // tokenizer must load BEFORE the model is built (it sets the vocab size)
898
  try {
899
  const name = await Transformer.loadTokenizer();
 
565
  return out.buffer;
566
  }
567
 
568
+ // ---- gradient wire: [i32 step][u32 whash][u32 phash][f32 loss][f32 grad...]
569
+ // whash = FNV-1a of the sender's weights BEFORE this step's update — proves the
570
+ // whole group is still applying the same updates to the same model.
571
+ // phash = FNV-1a of the sender's canonical kernel probe — proves the sender's
572
+ // arithmetic still agrees with the fleet's. whash CANNOT do this: a device
573
+ // with a broken kernel broadcasts a bad gradient, everyone averages the same
574
+ // bad bytes, and every replica stays bit-identical. Replica agreement is not
575
+ // kernel correctness, so the two hashes check genuinely different things.
576
+ // loss = the sender's local batch loss, for the true cluster-average display.
577
+ function packGrad(step, whash, phash, loss, grad) {
578
+ const buf = new ArrayBuffer(16 + grad.byteLength);
579
  new Int32Array(buf, 0, 1)[0] = step;
580
+ new Uint32Array(buf, 4, 2).set([whash, phash]);
581
+ new Float32Array(buf, 12, 1)[0] = loss;
582
+ new Float32Array(buf, 16).set(grad);
583
  return buf;
584
  }
585
+ // ---- continuous kernel verification ----------------------------------------
586
+ // The boot gate only ever sees toy shapes at one moment. These two keep proving
587
+ // it while the run is live: a random-cell audit against the units at the REAL
588
+ // shapes, and the canonical probe re-run on a cadence so thermal/driver drift
589
+ // shows up mid-run rather than never.
590
+ const PROBE_EVERY = 25; // steps between probe refreshes
591
+ let probeHash = 0, auditFailure = null;
592
+ const gemmAudit = {
593
+ cells: 6,
594
+ due: () => Math.random() < 0.02, // ~2% of GEMMs, a few cells each
595
+ fail: (msg) => { if (!auditFailure) { auditFailure = msg; log(`KERNEL AUDIT FAILED: ${msg}`); } },
596
+ };
597
+ async function refreshProbe() {
598
+ try { probeHash = await Compute.kernelProbe(compute, L); }
599
+ catch (e) { log(`kernel probe failed to run: ${e.message}`); }
600
+ return probeHash;
601
+ }
602
  function hashWeights() { // FNV-1a over the flat param bytes
603
  const f = Transformer.getFlatParams(model);
604
  const b = new Uint8Array(f.buffer, f.byteOffset, f.byteLength);
 
639
  rosters.set(iv[1], [...iv.slice(3, 3 + iv[2])].map(n => "p" + n));
640
  wake(); return;
641
  }
642
+ const [whash, phash] = new Uint32Array(buf, 4, 2);
643
+ const loss = new Float32Array(buf, 12, 1)[0];
644
+ const grad = new Float32Array(buf.slice(16));
645
  if (!incoming.has(step)) incoming.set(step, new Map());
646
  incoming.get(step).set(peerId, grad);
647
  if (!peerHashes.has(step)) peerHashes.set(step, new Map());
648
+ peerHashes.get(step).set(peerId, { hash: whash, phash, loss });
649
  wake(); // resolve waits immediately (no polling)
650
  }
651
+ function broadcastGrad(step, whash, phash, loss, grad) { return broadcast(packGrad(step, whash, phash, loss, grad)); }
652
  // Event-driven: re-checked on every gradient arrival and peer departure, plus a
653
  // coarse fallback timer (background tabs throttle timers to ~1s, so the old
654
  // 15ms poll was the bottleneck there). Peers that left are dropped from the
 
701
  const opt = TrainCore.makeAdam(model.nParams, { lr: cfg.lr });
702
  if (resume) opt.setState(resume.adam); // bit-identical moments from the leader
703
  pendingJoins.clear();
704
+ auditFailure = null;
705
+ await refreshProbe(); // fresh kernel proof for this run
706
  log(`training started — cohort ${cohort.length} peer(s), world ${cohort.length + 1}, ` +
707
  `width=${cfg.c} seq=${cfg.t} batch=${cfg.b}×${cohort.length + 1}, optimizer ${opt.name}` +
708
  (cohort.length ? `, sync ${iLead ? "led by me" : "led by " + nmeOf(leaderId)}` : "") +
 
735
  if (lastLocal && lastLocal.s === s) {
736
  ({ loss, grad } = lastLocal);
737
  } else {
738
+ // re-prove this device's kernel against the units on a cadence, so
739
+ // drift that appears mid-run (thermal, driver, shape) is caught while
740
+ // it runs rather than only at boot on toy inputs
741
+ if (s % PROBE_EVERY === 0) await refreshProbe();
742
  ({ loss, grad } = await localStep());
743
+ if (auditFailure) { halted = auditFailure; break; }
744
  lastLocal = { s, loss, grad };
745
+ await broadcastGrad(s, whash, probeHash, loss, grad);
746
  }
747
  }
748
  let all;
 
810
  if (hs) {
811
  const bad = roster.find(id => id !== myId && hs.has(id) && hs.get(id).hash !== whash);
812
  if (bad) { halted = `weights diverged from ${nmeOf(bad)} (detected at step ${s + 1})`; break; }
813
+ // kernel disagreement: same seeded probe, same exact integer arithmetic
814
+ // expected on every backend. This is the check the weight hash can't make.
815
+ const badK = roster.find(id => id !== myId && hs.has(id) && hs.get(id).phash &&
816
+ probeHash && hs.get(id).phash !== probeHash);
817
+ if (badK) {
818
+ halted = `kernel disagreement with ${nmeOf(badK)} at step ${s + 1} — their verified-unit probe ` +
819
+ `hashes ${hs.get(badK).phash} vs mine ${probeHash}. One of us is computing different ` +
820
+ `arithmetic; the shared weights would look fine either way, so stopping.`;
821
+ break;
822
+ }
823
  }
824
  // cluster-average loss over this step's actual contributors
825
  let lossSum = 0, lossN = 0;
 
880
  // (re)build the mini transformer — deterministic shared init (same seeds on
881
  // every peer); each device samples its own batch windows from the shared corpus
882
  function buildModel(cfg) {
883
+ model = Transformer.init(cfg, L, compute, gemmAudit); // compute.bgemm = fused batched GPU path
884
  trainedSteps = 0;
885
  }
886
 
 
932
  return; // no signaling, no training
933
  }
934
  ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label} · block-scaled INT8, batched + fused epilogue, through verified units`;
935
+ // canonical kernel probe: a fixed seeded GEMM through this device's live
936
+ // kernel. Every device that agrees with the units produces the same number,
937
+ // GPU or CPU — so it is comparable across the fleet, and re-run mid-training.
938
+ await refreshProbe();
939
+ log(`kernel probe: ${probeHash} (exact int32 through the units — every honest device gets this same number)`);
940
  // tokenizer must load BEFORE the model is built (it sets the vocab size)
941
  try {
942
  const name = await Transformer.loadTokenizer();
web/public/transformer.js CHANGED
@@ -117,7 +117,8 @@
117
  // the kernel epilogue (ex. 12). Replaces the per-tensor 3-pass: same outlier
118
  // robustness at one third of the unit ops.
119
  async function vmm(Xf, Wf, m, k, n, ctx, relu) {
120
- return V.vgemmBlock(Xf, Wf, { m, k, n, batch: 1, relu: !!relu }, ctx.L, ctx.bgemm);
 
121
  }
122
 
123
  // ---- layernorm (no affine) -------------------------------------------------
@@ -146,7 +147,7 @@
146
  // cfg: { c: width, t: seq len, b: batch/device, layers, heads, steps, lr }
147
  // engine: the Compute backend object ({bgemm} for the fused WebGPU path) or a
148
  // legacy matmulInt8 function (Node tests, inference kit) -> CPU LUT mirror
149
- function init(cfg, L, engine) {
150
  const c = cfg.c, layers = cfg.layers || 2, heads = cfg.heads || 2, hidden = 2 * c;
151
  let seed = 100;
152
  const mk = (nEl, scale) => { const w = randn(nEl, mulberry32(seed++)); for (let i = 0; i < nEl; i++) w[i] *= scale; return w; };
@@ -155,7 +156,8 @@
155
  const m = {
156
  cfg: { ...cfg, layers, heads, hidden, vocab: vocabSize() },
157
  ctx: { L, bgemm: (engine && engine.bgemm) || null,
158
- att: (engine && engine.att) || null, fgemm: (engine && engine.fgemm) || null },
 
159
  emb: add("emb", mk(vocabSize() * c, 0.08)),
160
  pos: add("pos", mk(cfg.t * c, 0.02)),
161
  blocks: [], params, names,
 
117
  // the kernel epilogue (ex. 12). Replaces the per-tensor 3-pass: same outlier
118
  // robustness at one third of the unit ops.
119
  async function vmm(Xf, Wf, m, k, n, ctx, relu) {
120
+ // ctx.audit re-checks random cells of this LIVE GEMM against the units
121
+ return V.vgemmBlock(Xf, Wf, { m, k, n, batch: 1, relu: !!relu }, ctx.L, ctx.bgemm, ctx.audit);
122
  }
123
 
124
  // ---- layernorm (no affine) -------------------------------------------------
 
147
  // cfg: { c: width, t: seq len, b: batch/device, layers, heads, steps, lr }
148
  // engine: the Compute backend object ({bgemm} for the fused WebGPU path) or a
149
  // legacy matmulInt8 function (Node tests, inference kit) -> CPU LUT mirror
150
+ function init(cfg, L, engine, audit) {
151
  const c = cfg.c, layers = cfg.layers || 2, heads = cfg.heads || 2, hidden = 2 * c;
152
  let seed = 100;
153
  const mk = (nEl, scale) => { const w = randn(nEl, mulberry32(seed++)); for (let i = 0; i < nEl; i++) w[i] *= scale; return w; };
 
156
  const m = {
157
  cfg: { ...cfg, layers, heads, hidden, vocab: vocabSize() },
158
  ctx: { L, bgemm: (engine && engine.bgemm) || null,
159
+ att: (engine && engine.att) || null, fgemm: (engine && engine.fgemm) || null,
160
+ audit: audit || null },
161
  emb: add("emb", mk(vocabSize() * c, 0.08)),
162
  pos: add("pos", mk(cfg.t * c, 0.02)),
163
  blocks: [], params, names,
web/public/verified_core.js CHANGED
@@ -100,12 +100,23 @@
100
  return { q, s };
101
  }
102
 
 
 
 
 
 
 
 
 
 
103
  // CPU mirror of the fused GPU kernel: batched int8 GEMM through the LUT with
104
  // the epilogue (block dequant + optional ReLU) applied before returning —
105
- // exactly what the WGSL kernel does on-device.
 
106
  function bgemmJS(Xq, Wq, rs, cs, d, L) {
107
  const { m, k, n } = d, batch = d.batch || 1, relu = !!d.relu, mul = L.mul;
108
- const out = new Float32Array(batch * m * n);
 
109
  const acc = new Int32Array(n);
110
  for (let bz = 0; bz < batch; bz++) {
111
  const xo = bz * m * k, wo = bz * k * n, oo = bz * m * n, co = bz * n;
@@ -116,9 +127,11 @@
116
  const au = (Xq[xrow + p] & 0xFF) * 256, wrow = wo + p * n;
117
  for (let j = 0; j < n; j++) acc[j] += mul[au + (Wq[wrow + j] & 0xFF)];
118
  }
119
- const rscale = rs[bz * m + i], orow = oo + i * n;
 
 
120
  for (let j = 0; j < n; j++) {
121
- const v = acc[j] * rscale * cs[co + j];
122
  out[orow + j] = relu && v < 0 ? 0 : v;
123
  }
124
  }
@@ -126,11 +139,32 @@
126
  return out;
127
  }
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  // block-scaled verified GEMM, float in → float out.
130
  // d = { m, k, n, batch=1, relu=false }; X is (batch·m)×k, W is batch×(k×n)
131
  // gpuBgemm (from webgpu.js) runs the batched kernel with the fused epilogue;
132
  // without it the CPU LUT mirror runs. Every product goes through the units.
133
- async function vgemmBlock(Xf, Wf, d, L, gpuBgemm) {
134
  const { m, k, n } = d, batch = d.batch || 1;
135
  const x = quantizeRows(Xf, batch * m, k);
136
  let wq, ws;
@@ -143,7 +177,16 @@
143
  wq.set(w.q, bz * k * n); ws.set(w.s, bz * n);
144
  }
145
  }
146
- if (gpuBgemm) return gpuBgemm(x.q, wq, x.s, ws, d);
 
 
 
 
 
 
 
 
 
147
  return bgemmJS(x.q, wq, x.s, ws, d, L);
148
  }
149
 
@@ -176,9 +219,10 @@
176
  return { q, s };
177
  }
178
  // scores S[bz,ti,tj] = q_row(bi,ti,h) · k_row(bi,tj,h), every product via the LUT
 
179
  function attScoresJS(qq, kq, qs, ks, d, L) {
180
- const { B, T, heads, hd } = d, C = heads * hd, mul = L.mul;
181
- const out = new Float32Array(B * heads * T * T);
182
  for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
183
  const bz = bi * heads + h;
184
  for (let ti = 0; ti < T; ti++) {
@@ -187,7 +231,7 @@
187
  const ko = (bi * T + tj) * C + h * hd;
188
  let acc = 0;
189
  for (let p = 0; p < hd; p++) acc += mul[(qq[qo + p] & 0xFF) * 256 + (kq[ko + p] & 0xFF)];
190
- out[(bz * T + ti) * T + tj] = acc * rscale * ks[(bi * T + tj) * heads + h];
191
  }
192
  }
193
  }
@@ -195,8 +239,8 @@
195
  }
196
  // ctx[(bi,ti),(h,j)] = Σ_tj a[bz,ti,tj]·v[(bi,tj),(h,j)] — scatter fused into BT×C
197
  function attCtxJS(aq, vq, as, vs, d, L) {
198
- const { B, T, heads, hd } = d, C = heads * hd, mul = L.mul;
199
- const out = new Float32Array(B * T * C);
200
  for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
201
  const bz = bi * heads + h;
202
  for (let ti = 0; ti < T; ti++) {
@@ -205,7 +249,7 @@
205
  let acc = 0;
206
  for (let tj = 0; tj < T; tj++)
207
  acc += mul[(aq[ao + tj] & 0xFF) * 256 + (vq[(bi * T + tj) * C + h * hd + j] & 0xFF)];
208
- out[(bi * T + ti) * C + h * hd + j] = acc * rscale * vs[(bi * heads + h) * hd + j];
209
  }
210
  }
211
  }
@@ -263,7 +307,7 @@
263
  }
264
 
265
  const api = { quantize, quantize2, quantizeRows, quantizeCols, quantizeHeadCols, lutMatmulJS, lutMatmul3JS, lutMatmul3,
266
- bgemmJS, vgemmBlock, attScoresJS, attCtxJS, linearFwd, forward, backward, splitApply };
267
  if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); module.exports = api; }
268
  else { TC = root.TrainCore; root.Verified = api; }
269
  })(typeof self !== "undefined" ? self : this);
 
100
  return { q, s };
101
  }
102
 
103
+ // ---- epilogue mirror -------------------------------------------------------
104
+ // BIT-EXACT mirror of the WGSL epilogue `f32(s) * a * b`. WGSL rounds to f32
105
+ // after the int->float conversion and after EACH multiply; plain JS would do
106
+ // the whole chain in f64 and round once, which differs in the last ulp. That
107
+ // last-ulp gap is what used to force a tolerance into the kernel gates —
108
+ // mirroring the rounding exactly is what lets the gates compare with `!==`.
109
+ const f32 = Math.fround;
110
+ function epi(s, a, b) { return f32(f32(f32(s) * a) * b); }
111
+
112
  // CPU mirror of the fused GPU kernel: batched int8 GEMM through the LUT with
113
  // the epilogue (block dequant + optional ReLU) applied before returning —
114
+ // exactly what the WGSL kernel does on-device. d.acc=true returns the raw
115
+ // int32 accumulator instead (the exact oracle the fused kernel normally hides).
116
  function bgemmJS(Xq, Wq, rs, cs, d, L) {
117
  const { m, k, n } = d, batch = d.batch || 1, relu = !!d.relu, mul = L.mul;
118
+ const raw = !!d.acc;
119
+ const out = raw ? new Int32Array(batch * m * n) : new Float32Array(batch * m * n);
120
  const acc = new Int32Array(n);
121
  for (let bz = 0; bz < batch; bz++) {
122
  const xo = bz * m * k, wo = bz * k * n, oo = bz * m * n, co = bz * n;
 
127
  const au = (Xq[xrow + p] & 0xFF) * 256, wrow = wo + p * n;
128
  for (let j = 0; j < n; j++) acc[j] += mul[au + (Wq[wrow + j] & 0xFF)];
129
  }
130
+ const orow = oo + i * n;
131
+ if (raw) { for (let j = 0; j < n; j++) out[orow + j] = acc[j]; continue; }
132
+ const rscale = rs[bz * m + i];
133
  for (let j = 0; j < n; j++) {
134
+ const v = epi(acc[j], rscale, cs[co + j]);
135
  out[orow + j] = relu && v < 0 ? 0 : v;
136
  }
137
  }
 
139
  return out;
140
  }
141
 
142
+ // Recompute a handful of RANDOM output cells of a live GEMM through the LUT
143
+ // mirror and compare against what the kernel produced. Sampling cells instead
144
+ // of whole matrices makes this cheap enough to run continuously, at the real
145
+ // shapes training uses — not once at boot on toy inputs.
146
+ function auditTile(Xq, Wq, rs, cs, d, got, L, nCells) {
147
+ const { m, k, n } = d, batch = d.batch || 1, relu = !!d.relu, mul = L.mul;
148
+ for (let t = 0; t < (nCells || 8); t++) {
149
+ const bz = (Math.random() * batch) | 0;
150
+ const i = (Math.random() * m) | 0, j = (Math.random() * n) | 0;
151
+ let acc = 0;
152
+ const xrow = bz * m * k + i * k, wo = bz * k * n;
153
+ for (let p = 0; p < k; p++) acc += mul[(Xq[xrow + p] & 0xFF) * 256 + (Wq[wo + p * n + j] & 0xFF)];
154
+ let v = epi(acc, rs[bz * m + i], cs[bz * n + j]);
155
+ if (relu && v < 0) v = 0;
156
+ const idx = (bz * m + i) * n + j;
157
+ if (got[idx] !== v)
158
+ return `GEMM audit failed at [b${bz},${i},${j}] shape ${m}x${k}x${n}: kernel ${got[idx]} vs units ${v}`;
159
+ }
160
+ return null;
161
+ }
162
+
163
  // block-scaled verified GEMM, float in → float out.
164
  // d = { m, k, n, batch=1, relu=false }; X is (batch·m)×k, W is batch×(k×n)
165
  // gpuBgemm (from webgpu.js) runs the batched kernel with the fused epilogue;
166
  // without it the CPU LUT mirror runs. Every product goes through the units.
167
+ async function vgemmBlock(Xf, Wf, d, L, gpuBgemm, audit) {
168
  const { m, k, n } = d, batch = d.batch || 1;
169
  const x = quantizeRows(Xf, batch * m, k);
170
  let wq, ws;
 
177
  wq.set(w.q, bz * k * n); ws.set(w.s, bz * n);
178
  }
179
  }
180
+ if (gpuBgemm) {
181
+ const out = await gpuBgemm(x.q, wq, x.s, ws, d);
182
+ // continuous re-verification at LIVE shapes: the boot gate only ever saw
183
+ // toy inputs, so sample a few real cells against the units as we go
184
+ if (audit && audit.due()) {
185
+ const bad = auditTile(x.q, wq, x.s, ws, d, out, L, audit.cells);
186
+ if (bad) audit.fail(bad);
187
+ }
188
+ return out;
189
+ }
190
  return bgemmJS(x.q, wq, x.s, ws, d, L);
191
  }
192
 
 
219
  return { q, s };
220
  }
221
  // scores S[bz,ti,tj] = q_row(bi,ti,h) · k_row(bi,tj,h), every product via the LUT
222
+ // d.acc=true returns the raw int32 accumulator (exact oracle for the kernel gate)
223
  function attScoresJS(qq, kq, qs, ks, d, L) {
224
+ const { B, T, heads, hd } = d, C = heads * hd, mul = L.mul, raw = !!d.acc;
225
+ const out = raw ? new Int32Array(B * heads * T * T) : new Float32Array(B * heads * T * T);
226
  for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
227
  const bz = bi * heads + h;
228
  for (let ti = 0; ti < T; ti++) {
 
231
  const ko = (bi * T + tj) * C + h * hd;
232
  let acc = 0;
233
  for (let p = 0; p < hd; p++) acc += mul[(qq[qo + p] & 0xFF) * 256 + (kq[ko + p] & 0xFF)];
234
+ out[(bz * T + ti) * T + tj] = raw ? acc : epi(acc, rscale, ks[(bi * T + tj) * heads + h]);
235
  }
236
  }
237
  }
 
239
  }
240
  // ctx[(bi,ti),(h,j)] = Σ_tj a[bz,ti,tj]·v[(bi,tj),(h,j)] — scatter fused into BT×C
241
  function attCtxJS(aq, vq, as, vs, d, L) {
242
+ const { B, T, heads, hd } = d, C = heads * hd, mul = L.mul, raw = !!d.acc;
243
+ const out = raw ? new Int32Array(B * T * C) : new Float32Array(B * T * C);
244
  for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
245
  const bz = bi * heads + h;
246
  for (let ti = 0; ti < T; ti++) {
 
249
  let acc = 0;
250
  for (let tj = 0; tj < T; tj++)
251
  acc += mul[(aq[ao + tj] & 0xFF) * 256 + (vq[(bi * T + tj) * C + h * hd + j] & 0xFF)];
252
+ out[(bi * T + ti) * C + h * hd + j] = raw ? acc : epi(acc, rscale, vs[(bi * heads + h) * hd + j]);
253
  }
254
  }
255
  }
 
307
  }
308
 
309
  const api = { quantize, quantize2, quantizeRows, quantizeCols, quantizeHeadCols, lutMatmulJS, lutMatmul3JS, lutMatmul3,
310
+ bgemmJS, vgemmBlock, auditTile, epi, attScoresJS, attCtxJS, linearFwd, forward, backward, splitApply };
311
  if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); module.exports = api; }
312
  else { TC = root.TrainCore; root.Verified = api; }
313
  })(typeof self !== "undefined" ? self : this);
web/public/webgpu.js CHANGED
@@ -35,36 +35,32 @@
35
  C[row * n + col] = s;
36
  }`;
37
 
38
- // X packed 4×int8 along k; W transposed then packed the same way, so each
39
- // thread streams two contiguous rows of u32 words through dot4I8Packed.
40
- const WGSL_DP4 = `
41
- @group(0) @binding(0) var<storage, read> Xp : array<u32>;
42
- @group(0) @binding(1) var<storage, read> Wp : array<u32>; // Wᵀ, packed
43
- @group(0) @binding(2) var<storage, read_write> C : array<i32>;
44
- @group(0) @binding(3) var<uniform> dims : vec3<u32>; // m, kw=ceil(k/4), n
45
- @compute @workgroup_size(8, 8)
46
- fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
47
- let m = dims.x; let kw = dims.y; let n = dims.z;
48
- let row = gid.x; let col = gid.y;
49
- if (row >= m || col >= n) { return; }
50
- var s : i32 = 0;
51
- for (var p = 0u; p < kw; p = p + 1u) {
52
- s = s + dot4I8Packed(Xp[row * kw + p], Wp[col * kw + p]);
53
- }
54
- C[row * n + col] = s;
55
- }`;
56
 
57
  // Batched block-scaled GEMM with a FUSED EPILOGUE (CUTLASS ex. 05/24 + 12):
58
  // grid z = batch index, so ALL attention heads run in ONE dispatch, and the
59
  // epilogue (block dequant rs·cs + optional ReLU) happens before the data
60
  // leaves the GPU — f32 out, no int32 readback, no second pass in JS.
61
- const WGSL_BG_LUT = `
 
 
 
 
 
 
 
 
62
  @group(0) @binding(0) var<storage, read> Xq : array<i32>; // int8 byte per elem
63
  @group(0) @binding(1) var<storage, read> Wq : array<i32>;
64
  @group(0) @binding(2) var<storage, read> lut : array<i32>;
65
  @group(0) @binding(3) var<storage, read> rs : array<f32>; // per (batch,row)
66
  @group(0) @binding(4) var<storage, read> cs : array<f32>; // per (batch,col)
67
- @group(0) @binding(5) var<storage, read_write> O : array<f32>;
68
  @group(0) @binding(6) var<uniform> dims : vec4<u32>; // m, k, n, flags(1=relu)
69
  @compute @workgroup_size(8, 8, 1)
70
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
@@ -79,18 +75,19 @@
79
  let bu = u32(Wq[wo + p * n] & 255);
80
  s = s + lut[au * 256u + bu];
81
  }
 
82
  var v = f32(s) * rs[bz * m + row] * cs[bz * n + col];
83
  if ((dims.w & 1u) == 1u && v < 0.0) { v = 0.0; }
84
- O[(bz * m + row) * n + col] = v;
85
  }`;
86
 
87
  // same fused/batched kernel on the DP4A hardware path (Wᵀ packed per batch)
88
- const WGSL_BG_DP4 = `
89
  @group(0) @binding(0) var<storage, read> Xp : array<u32>;
90
  @group(0) @binding(1) var<storage, read> Wp : array<u32>; // per-batch Wᵀ, packed
91
  @group(0) @binding(2) var<storage, read> rs : array<f32>;
92
  @group(0) @binding(3) var<storage, read> cs : array<f32>;
93
- @group(0) @binding(4) var<storage, read_write> O : array<f32>;
94
  @group(0) @binding(5) var<uniform> dims : vec4<u32>; // m, kw, n, flags
95
  @compute @workgroup_size(8, 8, 1)
96
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
@@ -103,21 +100,22 @@
103
  for (var p = 0u; p < kw; p = p + 1u) {
104
  s = s + dot4I8Packed(Xp[xo + p], Wp[wo + p]);
105
  }
 
106
  var v = f32(s) * rs[bz * m + row] * cs[bz * n + col];
107
  if ((dims.w & 1u) == 1u && v < 0.0) { v = 0.0; }
108
- O[(bz * m + row) * n + col] = v;
109
  }`;
110
 
111
  // Gather-fused attention (CUTLASS ex. 36/52): kernels index q/k/v directly in
112
  // their BT×C layout (head-strided) — no gather copies, no kᵀ transpose — and
113
  // the ctx kernel scatters straight back into BT×C. int8×int8→i32 is exact, so
114
  // these are bit-identical to the LUT mirrors (proved at init before use).
115
- const WGSL_ATT_SCORES = `
116
  @group(0) @binding(0) var<storage, read> Q : array<i32>; // int8 per elem, BT×C
117
  @group(0) @binding(1) var<storage, read> K : array<i32>;
118
  @group(0) @binding(2) var<storage, read> qs : array<f32>; // per (token,head)
119
  @group(0) @binding(3) var<storage, read> ks : array<f32>;
120
- @group(0) @binding(4) var<storage, read_write> O : array<f32>;
121
  @group(0) @binding(5) var<uniform> dims : vec4<u32>; // T, heads, hd, _
122
  @compute @workgroup_size(8, 8, 1)
123
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
@@ -130,14 +128,15 @@
130
  let ko = (bi * T + tj) * C + h * hd;
131
  var s : i32 = 0;
132
  for (var p = 0u; p < hd; p = p + 1u) { s = s + Q[qo + p] * K[ko + p]; }
133
- O[(bz * T + ti) * T + tj] = f32(s) * qs[(bi * T + ti) * heads + h] * ks[(bi * T + tj) * heads + h];
 
134
  }`;
135
- const WGSL_ATT_CTX = `
136
  @group(0) @binding(0) var<storage, read> A : array<i32>; // int8, BH×T×T
137
  @group(0) @binding(1) var<storage, read> V : array<i32>; // int8, BT×C
138
  @group(0) @binding(2) var<storage, read> as_ : array<f32>; // per (bz,row)
139
  @group(0) @binding(3) var<storage, read> vs : array<f32>; // per (batch,head,chan)
140
- @group(0) @binding(4) var<storage, read_write> O : array<f32>; // BT×C (scatter fused)
141
  @group(0) @binding(5) var<uniform> dims : vec4<u32>; // T, heads, hd, _
142
  @compute @workgroup_size(8, 8, 1)
143
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
@@ -149,7 +148,8 @@
149
  let ao = (bz * T + ti) * T;
150
  var s : i32 = 0;
151
  for (var tj = 0u; tj < T; tj = tj + 1u) { s = s + A[ao + tj] * V[(bi * T + tj) * C + h * hd + j]; }
152
- O[(bi * T + ti) * C + h * hd + j] = f32(s) * as_[bz * T + ti] * vs[(bi * heads + h) * hd + j];
 
153
  }`;
154
 
155
  // Split-K f32 GEMM (CUTLASS ex. 06) for the STE BACKWARD only — the backward
@@ -237,32 +237,58 @@
237
  const lut32 = new Int32Array(L.mul); // widen int16 -> int32
238
  const lutBuf = device.createBuffer({ size: lut32.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
239
  device.queue.writeBuffer(lutBuf, 0, lut32);
240
- const bgLutModule = device.createShaderModule({ code: WGSL_BG_LUT });
241
- const bgLutPipe = device.createComputePipeline({ layout: "auto", compute: { module: bgLutModule, entryPoint: "main" } });
242
-
243
- // gather-fused attention kernels — must reproduce the LUT mirrors exactly
244
  const mkPipe = (code) => device.createComputePipeline({ layout: "auto",
245
  compute: { module: device.createShaderModule({ code }), entryPoint: "main" } });
246
- const scoresPipe = mkPipe(WGSL_ATT_SCORES), ctxPipe = mkPipe(WGSL_ATT_CTX);
247
- let att = { scores: (qq, kq, qs, ks, d) => gpuAttScores(device, scoresPipe, qq, kq, qs, ks, d),
248
- ctx: (aq, vq, as, vs, d) => gpuAttCtx(device, ctxPipe, aq, vq, as, vs, d) };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  try {
250
- const d0 = { B: 2, T: 8, heads: 2, hd: 8 };
251
- const nQ = d0.B * d0.T * d0.heads * d0.hd;
252
- const qq = new Int8Array(nQ), kq = new Int8Array(nQ), vq = new Int8Array(nQ);
253
- for (let i = 0; i < nQ; i++) { qq[i] = (Math.random() * 256 - 128) | 0; kq[i] = (Math.random() * 256 - 128) | 0; vq[i] = (Math.random() * 256 - 128) | 0; }
254
- const qs = Float32Array.from({ length: d0.B * d0.T * d0.heads }, () => Math.random() + 0.5);
255
- const ks = Float32Array.from(qs, () => Math.random() + 0.5);
256
- const aq = new Int8Array(d0.B * d0.heads * d0.T * d0.T);
257
- for (let i = 0; i < aq.length; i++) aq[i] = (Math.random() * 127) | 0;
258
- const as = Float32Array.from({ length: d0.B * d0.heads * d0.T }, () => Math.random() + 0.5);
259
- const vs = Float32Array.from({ length: d0.B * d0.heads * d0.hd }, () => Math.random() + 0.5);
260
- const [hwS, hwC] = await Promise.all([att.scores(qq, kq, qs, ks, d0), att.ctx(aq, vq, as, vs, d0)]);
261
- const refS = root.Verified.attScoresJS(qq, kq, qs, ks, d0, L);
262
- const refC = root.Verified.attCtxJS(aq, vq, as, vs, d0, L);
263
- const close = (a, b) => Math.abs(a - b) <= Math.abs(b) * 1e-6 + 1e-6;
264
- for (let i = 0; i < refS.length; i++) if (!close(hwS[i], refS[i])) throw new Error("scores mismatch");
265
- for (let i = 0; i < refC.length; i++) if (!close(hwC[i], refC[i])) throw new Error("ctx mismatch");
 
 
 
 
 
 
 
 
266
  } catch (e) {
267
  console.warn("fused attention kernels failed verification — using CPU LUT mirrors:", e.message);
268
  att = null;
@@ -284,51 +310,55 @@
284
  fgemm = null;
285
  }
286
 
287
- const viaLUT = { backend: "webgpu", label: `${gpuName} (LUT shader)`,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  matmulInt8: (Xq, Wq, m, k, n) => gpuMatmulLUT(device, lutPipe, lutBuf, Xq, Wq, m, k, n),
289
- bgemm: (Xq, Wq, rs, cs, d) => gpuBgemmLUT(device, bgLutPipe, lutBuf, Xq, Wq, rs, cs, d),
290
- att, fgemm };
291
 
292
- // DP4A pipeline — only if the WGSL feature exists AND it reproduces the
293
- // verified units exactly on random self-tests
294
  if (!(navigator.gpu.wgslLanguageFeatures && navigator.gpu.wgslLanguageFeatures.has("packed_4x8_integer_dot_product")))
295
  return viaLUT;
296
- const dp4Module = device.createShaderModule({ code: WGSL_DP4 });
297
- const dp4Pipe = device.createComputePipeline({ layout: "auto", compute: { module: dp4Module, entryPoint: "main" } });
298
- const dp4mm = (Xq, Wq, m, k, n) => gpuMatmulDP4(device, dp4Pipe, Xq, Wq, m, k, n);
299
- for (let trial = 0; trial < 3; trial++) { // hardware vs verified units
300
- const m0 = 5 + trial, k0 = 7 + 3 * trial, n0 = 6 + trial;
301
- const Xq = new Int8Array(m0 * k0), Wq = new Int8Array(k0 * n0);
302
- for (let i = 0; i < Xq.length; i++) Xq[i] = (Math.random() * 256 - 128) | 0;
303
- for (let i = 0; i < Wq.length; i++) Wq[i] = (Math.random() * 256 - 128) | 0;
304
- const hw = await dp4mm(Xq, Wq, m0, k0, n0);
305
- const ref = root.Verified.lutMatmulJS(Xq, Wq, m0, k0, n0, L);
306
- for (let i = 0; i < ref.length; i++)
307
- if (hw[i] !== ref[i]) { console.warn("DP4A disagreed with the verified units — using LUT shader"); return viaLUT; }
308
- }
309
- // fused/batched DP4A kernel, gated by the same verification: it must
310
- // reproduce the CPU LUT mirror bit-for-bit on the integer accumulate
311
- const bgDp4Module = device.createShaderModule({ code: WGSL_BG_DP4 });
312
- const bgDp4Pipe = device.createComputePipeline({ layout: "auto", compute: { module: bgDp4Module, entryPoint: "main" } });
313
- const bg = (Xq, Wq, rs, cs, d) => gpuBgemmDP4(device, bgDp4Pipe, Xq, Wq, rs, cs, d);
314
- {
315
- const d0 = { m: 5, k: 9, n: 6, batch: 3, relu: true };
316
- const Xq = new Int8Array(d0.batch * d0.m * d0.k), Wq = new Int8Array(d0.batch * d0.k * d0.n);
317
- for (let i = 0; i < Xq.length; i++) Xq[i] = (Math.random() * 256 - 128) | 0;
318
- for (let i = 0; i < Wq.length; i++) Wq[i] = (Math.random() * 256 - 128) | 0;
319
- const rs = new Float32Array(d0.batch * d0.m).fill(0).map(() => Math.random() + 0.5);
320
- const cs = new Float32Array(d0.batch * d0.n).fill(0).map(() => Math.random() + 0.5);
321
- const hw = await bg(Xq, Wq, rs, cs, d0);
322
- const ref = root.Verified.bgemmJS(Xq, Wq, rs, cs, d0, L);
323
- for (let i = 0; i < ref.length; i++)
324
- if (Math.abs(hw[i] - ref[i]) > Math.abs(ref[i]) * 1e-6 + 1e-6) {
325
- console.warn("batched DP4A disagreed with the verified units — using LUT bgemm");
326
- return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot — HW verified vs units)`,
327
- matmulInt8: dp4mm, bgemm: viaLUT.bgemm, att, fgemm };
328
- }
329
  }
330
- return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot — HW verified vs units)`,
331
- matmulInt8: dp4mm, bgemm: bg, att, fgemm };
332
  } catch (e) { console.warn("WebGPU init failed, CPU fallback:", e); return cpu; }
333
  }
334
 
@@ -364,20 +394,6 @@
364
  return r.out;
365
  }
366
 
367
- async function gpuMatmulDP4(device, pipeline, Xq, Wq, m, k, n) {
368
- const kw = Math.ceil(k / 4);
369
- const Xp = packRows(Xq, m, k, kw);
370
- const Wp = packRows(transposeI8(Wq, k, n), n, k, kw); // Wᵀ so col j is a row
371
- const bufX = up(device, Xp, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
372
- const bufW = up(device, Wp, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
373
- const bufD = up(device, new Uint32Array([m, kw, n, 0]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
374
- const r = await runPass(device, pipeline, (bufC) => [
375
- { binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
376
- { binding: 2, resource: { buffer: bufC } }, { binding: 3, resource: { buffer: bufD } } ], m, n);
377
- [bufX, bufW, bufD, r.bufC, r.read].forEach(b => b.destroy());
378
- return r.out;
379
- }
380
-
381
  // attention kernels: int8 (widened i32) in, f32 out, strided head indexing
382
  async function gpuAttScores(device, pipeline, qq, kq, qs, ks, d) {
383
  const { B, T, heads } = d;
@@ -389,7 +405,7 @@
389
  const r = await runBgPass(device, pipeline, (bufO) => [
390
  { binding: 0, resource: { buffer: bufQ } }, { binding: 1, resource: { buffer: bufK } },
391
  { binding: 2, resource: { buffer: bufQs } }, { binding: 3, resource: { buffer: bufKs } },
392
- { binding: 4, resource: { buffer: bufO } }, { binding: 5, resource: { buffer: bufD } } ], T, T, B * heads);
393
  [bufQ, bufK, bufQs, bufKs, bufD, r.bufO, r.read].forEach(b => b.destroy());
394
  return r.out;
395
  }
@@ -403,7 +419,7 @@
403
  const r = await runBgPass(device, pipeline, (bufO) => [
404
  { binding: 0, resource: { buffer: bufA } }, { binding: 1, resource: { buffer: bufV } },
405
  { binding: 2, resource: { buffer: bufAs } }, { binding: 3, resource: { buffer: bufVs } },
406
- { binding: 4, resource: { buffer: bufO } }, { binding: 5, resource: { buffer: bufD } } ], T, hd, B * heads);
407
  [bufA, bufV, bufAs, bufVs, bufD, r.bufO, r.read].forEach(b => b.destroy());
408
  return r.out;
409
  }
@@ -441,8 +457,9 @@
441
  return out;
442
  }
443
 
444
- // fused batched dispatch: f32 out, epilogue done on-device
445
- async function runBgPass(device, pipeline, entries, m, n, batch) {
 
446
  const bytesO = batch * m * n * 4;
447
  const bufO = mk(device, bytesO, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
448
  const bind = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: entries(bufO) });
@@ -454,7 +471,8 @@
454
  enc.copyBufferToBuffer(bufO, 0, read, 0, bytesO);
455
  device.queue.submit([enc.finish()]);
456
  await read.mapAsync(GPUMapMode.READ);
457
- const out = new Float32Array(read.getMappedRange().slice(0));
 
458
  read.unmap();
459
  return { out, bufO, read };
460
  }
@@ -470,7 +488,7 @@
470
  { binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
471
  { binding: 2, resource: { buffer: lutBuf } }, { binding: 3, resource: { buffer: bufR } },
472
  { binding: 4, resource: { buffer: bufS } }, { binding: 5, resource: { buffer: bufO } },
473
- { binding: 6, resource: { buffer: bufD } } ], m, n, batch);
474
  [bufX, bufW, bufR, bufS, bufD, r.bufO, r.read].forEach(b => b.destroy());
475
  return r.out;
476
  }
@@ -492,7 +510,7 @@
492
  const r = await runBgPass(device, pipeline, (bufO) => [
493
  { binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
494
  { binding: 2, resource: { buffer: bufR } }, { binding: 3, resource: { buffer: bufS } },
495
- { binding: 4, resource: { buffer: bufO } }, { binding: 5, resource: { buffer: bufD } } ], m, n, batch);
496
  [bufX, bufW, bufR, bufS, bufD, r.bufO, r.read].forEach(b => b.destroy());
497
  return r.out;
498
  }
@@ -504,5 +522,37 @@
504
  return b;
505
  }
506
 
507
- root.Compute = { initCompute, loadLUTs };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  })(self);
 
35
  C[row * n + col] = s;
36
  }`;
37
 
38
+ // NOTE: the un-batched DP4A matmul that used to live here was removed. It was
39
+ // the only kernel with an exact gate, but the transformer stopped calling it
40
+ // when the block-scaled path landed — so it sat here passing its own gate
41
+ // while verifying nothing that ran. A gate on a kernel nobody calls is worse
42
+ // than no gate: it reads like coverage. The batched kernels below are the
43
+ // ones training uses, and they now carry that exact gate instead.
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  // Batched block-scaled GEMM with a FUSED EPILOGUE (CUTLASS ex. 05/24 + 12):
46
  // grid z = batch index, so ALL attention heads run in ONE dispatch, and the
47
  // epilogue (block dequant rs·cs + optional ReLU) happens before the data
48
  // leaves the GPU — f32 out, no int32 readback, no second pass in JS.
49
+ // Each kernel is emitted in two variants from ONE source string: the live one
50
+ // (fused epilogue, f32 out) and a `verify` one that writes the raw int32
51
+ // accumulator instead. The indexing — the part that actually goes wrong — is
52
+ // textually identical, so gating the verify variant genuinely gates the live
53
+ // kernel, and the comparison is EXACT (int8xint8->int32 has no rounding to
54
+ // hide in) instead of an allclose that whole bug classes walk through.
55
+ const OUT_DECL = (v, b) => `@group(0) @binding(${b}) var<storage, read_write> O : array<${v ? "i32" : "f32"}>;`;
56
+
57
+ const WGSL_BG_LUT = (verify) => `
58
  @group(0) @binding(0) var<storage, read> Xq : array<i32>; // int8 byte per elem
59
  @group(0) @binding(1) var<storage, read> Wq : array<i32>;
60
  @group(0) @binding(2) var<storage, read> lut : array<i32>;
61
  @group(0) @binding(3) var<storage, read> rs : array<f32>; // per (batch,row)
62
  @group(0) @binding(4) var<storage, read> cs : array<f32>; // per (batch,col)
63
+ ${OUT_DECL(verify, 5)}
64
  @group(0) @binding(6) var<uniform> dims : vec4<u32>; // m, k, n, flags(1=relu)
65
  @compute @workgroup_size(8, 8, 1)
66
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
 
75
  let bu = u32(Wq[wo + p * n] & 255);
76
  s = s + lut[au * 256u + bu];
77
  }
78
+ ${verify ? `O[(bz * m + row) * n + col] = s;` : `
79
  var v = f32(s) * rs[bz * m + row] * cs[bz * n + col];
80
  if ((dims.w & 1u) == 1u && v < 0.0) { v = 0.0; }
81
+ O[(bz * m + row) * n + col] = v;`}
82
  }`;
83
 
84
  // same fused/batched kernel on the DP4A hardware path (Wᵀ packed per batch)
85
+ const WGSL_BG_DP4 = (verify) => `
86
  @group(0) @binding(0) var<storage, read> Xp : array<u32>;
87
  @group(0) @binding(1) var<storage, read> Wp : array<u32>; // per-batch Wᵀ, packed
88
  @group(0) @binding(2) var<storage, read> rs : array<f32>;
89
  @group(0) @binding(3) var<storage, read> cs : array<f32>;
90
+ ${OUT_DECL(verify, 4)}
91
  @group(0) @binding(5) var<uniform> dims : vec4<u32>; // m, kw, n, flags
92
  @compute @workgroup_size(8, 8, 1)
93
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
 
100
  for (var p = 0u; p < kw; p = p + 1u) {
101
  s = s + dot4I8Packed(Xp[xo + p], Wp[wo + p]);
102
  }
103
+ ${verify ? `O[(bz * m + row) * n + col] = s;` : `
104
  var v = f32(s) * rs[bz * m + row] * cs[bz * n + col];
105
  if ((dims.w & 1u) == 1u && v < 0.0) { v = 0.0; }
106
+ O[(bz * m + row) * n + col] = v;`}
107
  }`;
108
 
109
  // Gather-fused attention (CUTLASS ex. 36/52): kernels index q/k/v directly in
110
  // their BT×C layout (head-strided) — no gather copies, no kᵀ transpose — and
111
  // the ctx kernel scatters straight back into BT×C. int8×int8→i32 is exact, so
112
  // these are bit-identical to the LUT mirrors (proved at init before use).
113
+ const WGSL_ATT_SCORES = (verify) => `
114
  @group(0) @binding(0) var<storage, read> Q : array<i32>; // int8 per elem, BT×C
115
  @group(0) @binding(1) var<storage, read> K : array<i32>;
116
  @group(0) @binding(2) var<storage, read> qs : array<f32>; // per (token,head)
117
  @group(0) @binding(3) var<storage, read> ks : array<f32>;
118
+ ${OUT_DECL(verify, 4)}
119
  @group(0) @binding(5) var<uniform> dims : vec4<u32>; // T, heads, hd, _
120
  @compute @workgroup_size(8, 8, 1)
121
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
 
128
  let ko = (bi * T + tj) * C + h * hd;
129
  var s : i32 = 0;
130
  for (var p = 0u; p < hd; p = p + 1u) { s = s + Q[qo + p] * K[ko + p]; }
131
+ ${verify ? `O[(bz * T + ti) * T + tj] = s;`
132
+ : `O[(bz * T + ti) * T + tj] = f32(s) * qs[(bi * T + ti) * heads + h] * ks[(bi * T + tj) * heads + h];`}
133
  }`;
134
+ const WGSL_ATT_CTX = (verify) => `
135
  @group(0) @binding(0) var<storage, read> A : array<i32>; // int8, BH×T×T
136
  @group(0) @binding(1) var<storage, read> V : array<i32>; // int8, BT×C
137
  @group(0) @binding(2) var<storage, read> as_ : array<f32>; // per (bz,row)
138
  @group(0) @binding(3) var<storage, read> vs : array<f32>; // per (batch,head,chan)
139
+ ${OUT_DECL(verify, 4)} // BT×C (scatter fused)
140
  @group(0) @binding(5) var<uniform> dims : vec4<u32>; // T, heads, hd, _
141
  @compute @workgroup_size(8, 8, 1)
142
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
 
148
  let ao = (bz * T + ti) * T;
149
  var s : i32 = 0;
150
  for (var tj = 0u; tj < T; tj = tj + 1u) { s = s + A[ao + tj] * V[(bi * T + tj) * C + h * hd + j]; }
151
+ ${verify ? `O[(bi * T + ti) * C + h * hd + j] = s;`
152
+ : `O[(bi * T + ti) * C + h * hd + j] = f32(s) * as_[bz * T + ti] * vs[(bi * heads + h) * hd + j];`}
153
  }`;
154
 
155
  // Split-K f32 GEMM (CUTLASS ex. 06) for the STE BACKWARD only — the backward
 
237
  const lut32 = new Int32Array(L.mul); // widen int16 -> int32
238
  const lutBuf = device.createBuffer({ size: lut32.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
239
  device.queue.writeBuffer(lutBuf, 0, lut32);
 
 
 
 
240
  const mkPipe = (code) => device.createComputePipeline({ layout: "auto",
241
  compute: { module: device.createShaderModule({ code }), entryPoint: "main" } });
242
+ // The verify variant doesn't reference the scale buffers, so `layout:auto`
243
+ // would strip those bindings and the bind group would silently mismatch.
244
+ // An EXPLICIT layout keeps both variants binding-compatible — which is the
245
+ // point: they must differ only in the final write, nothing else.
246
+ const mkLayout = (spec) => device.createBindGroupLayout({
247
+ entries: spec.map((t, i) => ({ binding: i, visibility: GPUShaderStage.COMPUTE,
248
+ buffer: { type: t === "u" ? "uniform" : t === "rw" ? "storage" : "read-only-storage" } })) });
249
+ const mkPipeL = (code, layout) => device.createComputePipeline({
250
+ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
251
+ compute: { module: device.createShaderModule({ code }), entryPoint: "main" } });
252
+ const bgLutLayout = mkLayout(["r", "r", "r", "r", "r", "rw", "u"]);
253
+ const bgDp4Layout = mkLayout(["r", "r", "r", "r", "rw", "u"]);
254
+ const attLayout = mkLayout(["r", "r", "r", "r", "rw", "u"]);
255
+ // live + verify variants, compiled from the same source (see WGSL_* above)
256
+ const bgLutPipe = mkPipeL(WGSL_BG_LUT(false), bgLutLayout), bgLutVPipe = mkPipeL(WGSL_BG_LUT(true), bgLutLayout);
257
+ const scoresPipe = mkPipeL(WGSL_ATT_SCORES(false), attLayout), scoresVPipe = mkPipeL(WGSL_ATT_SCORES(true), attLayout);
258
+ const ctxPipe = mkPipeL(WGSL_ATT_CTX(false), attLayout), ctxVPipe = mkPipeL(WGSL_ATT_CTX(true), attLayout);
259
+
260
+ // gather-fused attention kernels. The gate runs the VERIFY variant of the
261
+ // same source and compares the int32 accumulator with `!==` — exact, no
262
+ // tolerance — then checks the fused epilogue against a bit-exact JS mirror
263
+ // of the WGSL rounding. Swept over several shapes, incl. odd/ragged ones,
264
+ // because head-strided addressing is where these kernels can go wrong.
265
+ let att = { scores: (qq, kq, qs, ks, d) => gpuAttScores(device, d.acc ? scoresVPipe : scoresPipe, qq, kq, qs, ks, d),
266
+ ctx: (aq, vq, as, vs, d) => gpuAttCtx(device, d.acc ? ctxVPipe : ctxPipe, aq, vq, as, vs, d) };
267
  try {
268
+ for (const d0 of [{ B: 2, T: 8, heads: 2, hd: 8 }, { B: 1, T: 32, heads: 2, hd: 16 },
269
+ { B: 3, T: 7, heads: 3, hd: 5 }, { B: 2, T: 33, heads: 4, hd: 8 }]) {
270
+ const nQ = d0.B * d0.T * d0.heads * d0.hd;
271
+ const qq = new Int8Array(nQ), kq = new Int8Array(nQ), vq = new Int8Array(nQ);
272
+ for (let i = 0; i < nQ; i++) { qq[i] = (Math.random() * 256 - 128) | 0; kq[i] = (Math.random() * 256 - 128) | 0; vq[i] = (Math.random() * 256 - 128) | 0; }
273
+ const qs = Float32Array.from({ length: d0.B * d0.T * d0.heads }, () => Math.random() + 0.5);
274
+ const ks = Float32Array.from({ length: d0.B * d0.T * d0.heads }, () => Math.random() + 0.5);
275
+ const aq = new Int8Array(d0.B * d0.heads * d0.T * d0.T);
276
+ for (let i = 0; i < aq.length; i++) aq[i] = (Math.random() * 127) | 0;
277
+ const as = Float32Array.from({ length: d0.B * d0.heads * d0.T }, () => Math.random() + 0.5);
278
+ const vs = Float32Array.from({ length: d0.B * d0.heads * d0.hd }, () => Math.random() + 0.5);
279
+ const dv = { ...d0, acc: true };
280
+ const [accS, accC, hwS, hwC] = await Promise.all([
281
+ att.scores(qq, kq, qs, ks, dv), att.ctx(aq, vq, as, vs, dv),
282
+ att.scores(qq, kq, qs, ks, d0), att.ctx(aq, vq, as, vs, d0)]);
283
+ const refAccS = root.Verified.attScoresJS(qq, kq, qs, ks, dv, L);
284
+ const refAccC = root.Verified.attCtxJS(aq, vq, as, vs, dv, L);
285
+ for (let i = 0; i < refAccS.length; i++) if (accS[i] !== refAccS[i]) throw new Error(`scores accumulator mismatch @${i} shape ${JSON.stringify(d0)}`);
286
+ for (let i = 0; i < refAccC.length; i++) if (accC[i] !== refAccC[i]) throw new Error(`ctx accumulator mismatch @${i} shape ${JSON.stringify(d0)}`);
287
+ const refS = root.Verified.attScoresJS(qq, kq, qs, ks, d0, L);
288
+ const refC = root.Verified.attCtxJS(aq, vq, as, vs, d0, L);
289
+ for (let i = 0; i < refS.length; i++) if (hwS[i] !== refS[i]) throw new Error(`scores epilogue mismatch @${i}`);
290
+ for (let i = 0; i < refC.length; i++) if (hwC[i] !== refC[i]) throw new Error(`ctx epilogue mismatch @${i}`);
291
+ }
292
  } catch (e) {
293
  console.warn("fused attention kernels failed verification — using CPU LUT mirrors:", e.message);
294
  att = null;
 
310
  fgemm = null;
311
  }
312
 
313
+ // Shared exact gate for a bgemm implementation. Sweeps shapes (including
314
+ // ragged ones and a k long enough to matter), compares the int32
315
+ // accumulator from the verify variant with `!==`, then compares the fused
316
+ // f32 epilogue against the bit-exact JS mirror. Returns null if clean.
317
+ async function gateBgemm(bgFn) {
318
+ for (const d0 of [{ m: 5, k: 9, n: 6, batch: 3, relu: true },
319
+ { m: 32, k: 64, n: 32, batch: 1, relu: false },
320
+ { m: 7, k: 253, n: 5, batch: 2, relu: true },
321
+ { m: 1, k: 4, n: 1, batch: 1, relu: false },
322
+ { m: 17, k: 33, n: 9, batch: 1, relu: true }]) {
323
+ const Xq = new Int8Array(d0.batch * d0.m * d0.k), Wq = new Int8Array(d0.batch * d0.k * d0.n);
324
+ for (let i = 0; i < Xq.length; i++) Xq[i] = (Math.random() * 256 - 128) | 0;
325
+ for (let i = 0; i < Wq.length; i++) Wq[i] = (Math.random() * 256 - 128) | 0;
326
+ const rs = Float32Array.from({ length: d0.batch * d0.m }, () => Math.random() + 0.5);
327
+ const cs = Float32Array.from({ length: d0.batch * d0.n }, () => Math.random() + 0.5);
328
+ const shape = `${d0.m}x${d0.k}x${d0.n}b${d0.batch}`;
329
+ const accHw = await bgFn(Xq, Wq, rs, cs, { ...d0, acc: true });
330
+ const accRef = root.Verified.bgemmJS(Xq, Wq, rs, cs, { ...d0, acc: true }, L);
331
+ for (let i = 0; i < accRef.length; i++)
332
+ if (accHw[i] !== accRef[i]) return `accumulator mismatch @${i} (${shape}): ${accHw[i]} vs ${accRef[i]}`;
333
+ const hw = await bgFn(Xq, Wq, rs, cs, d0);
334
+ const ref = root.Verified.bgemmJS(Xq, Wq, rs, cs, d0, L);
335
+ for (let i = 0; i < ref.length; i++)
336
+ if (hw[i] !== ref[i]) return `epilogue mismatch @${i} (${shape}): ${hw[i]} vs ${ref[i]}`;
337
+ }
338
+ return null;
339
+ }
340
+
341
+ const bgLut = (Xq, Wq, rs, cs, d) => gpuBgemmLUT(device, d.acc ? bgLutVPipe : bgLutPipe, lutBuf, Xq, Wq, rs, cs, d);
342
+ // the LUT bgemm is the fallback AND the oracle's shader twin — gate it too
343
+ const lutBad = await gateBgemm(bgLut);
344
+ if (lutBad) { console.warn("LUT bgemm shader failed verification — CPU mirrors only:", lutBad); return cpu; }
345
+ const viaLUT = { backend: "webgpu", label: `${gpuName} (LUT shader · exact-gated)`,
346
  matmulInt8: (Xq, Wq, m, k, n) => gpuMatmulLUT(device, lutPipe, lutBuf, Xq, Wq, m, k, n),
347
+ bgemm: bgLut, att, fgemm };
 
348
 
349
+ // DP4A pipeline — only if the WGSL feature exists AND its batched kernel
350
+ // reproduces the verified units exactly across the shape sweep
351
  if (!(navigator.gpu.wgslLanguageFeatures && navigator.gpu.wgslLanguageFeatures.has("packed_4x8_integer_dot_product")))
352
  return viaLUT;
353
+ const bgDp4Pipe = mkPipeL(WGSL_BG_DP4(false), bgDp4Layout), bgDp4VPipe = mkPipeL(WGSL_BG_DP4(true), bgDp4Layout);
354
+ const bg = (Xq, Wq, rs, cs, d) => gpuBgemmDP4(device, d.acc ? bgDp4VPipe : bgDp4Pipe, Xq, Wq, rs, cs, d);
355
+ const dp4Bad = await gateBgemm(bg);
356
+ if (dp4Bad) {
357
+ console.warn("batched DP4A disagreed with the verified units — using LUT bgemm:", dp4Bad);
358
+ return viaLUT;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  }
360
+ return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot · exact-gated vs units)`,
361
+ bgemm: bg, att, fgemm };
362
  } catch (e) { console.warn("WebGPU init failed, CPU fallback:", e); return cpu; }
363
  }
364
 
 
394
  return r.out;
395
  }
396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  // attention kernels: int8 (widened i32) in, f32 out, strided head indexing
398
  async function gpuAttScores(device, pipeline, qq, kq, qs, ks, d) {
399
  const { B, T, heads } = d;
 
405
  const r = await runBgPass(device, pipeline, (bufO) => [
406
  { binding: 0, resource: { buffer: bufQ } }, { binding: 1, resource: { buffer: bufK } },
407
  { binding: 2, resource: { buffer: bufQs } }, { binding: 3, resource: { buffer: bufKs } },
408
+ { binding: 4, resource: { buffer: bufO } }, { binding: 5, resource: { buffer: bufD } } ], T, T, B * heads, d.acc);
409
  [bufQ, bufK, bufQs, bufKs, bufD, r.bufO, r.read].forEach(b => b.destroy());
410
  return r.out;
411
  }
 
419
  const r = await runBgPass(device, pipeline, (bufO) => [
420
  { binding: 0, resource: { buffer: bufA } }, { binding: 1, resource: { buffer: bufV } },
421
  { binding: 2, resource: { buffer: bufAs } }, { binding: 3, resource: { buffer: bufVs } },
422
+ { binding: 4, resource: { buffer: bufO } }, { binding: 5, resource: { buffer: bufD } } ], T, hd, B * heads, d.acc);
423
  [bufA, bufV, bufAs, bufVs, bufD, r.bufO, r.read].forEach(b => b.destroy());
424
  return r.out;
425
  }
 
457
  return out;
458
  }
459
 
460
+ // fused batched dispatch: f32 out, epilogue done on-device (raw=true reads the
461
+ // verify variant's int32 accumulator instead)
462
+ async function runBgPass(device, pipeline, entries, m, n, batch, raw) {
463
  const bytesO = batch * m * n * 4;
464
  const bufO = mk(device, bytesO, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
465
  const bind = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: entries(bufO) });
 
471
  enc.copyBufferToBuffer(bufO, 0, read, 0, bytesO);
472
  device.queue.submit([enc.finish()]);
473
  await read.mapAsync(GPUMapMode.READ);
474
+ const buf = read.getMappedRange().slice(0);
475
+ const out = raw ? new Int32Array(buf) : new Float32Array(buf);
476
  read.unmap();
477
  return { out, bufO, read };
478
  }
 
488
  { binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
489
  { binding: 2, resource: { buffer: lutBuf } }, { binding: 3, resource: { buffer: bufR } },
490
  { binding: 4, resource: { buffer: bufS } }, { binding: 5, resource: { buffer: bufO } },
491
+ { binding: 6, resource: { buffer: bufD } } ], m, n, batch, d.acc);
492
  [bufX, bufW, bufR, bufS, bufD, r.bufO, r.read].forEach(b => b.destroy());
493
  return r.out;
494
  }
 
510
  const r = await runBgPass(device, pipeline, (bufO) => [
511
  { binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
512
  { binding: 2, resource: { buffer: bufR } }, { binding: 3, resource: { buffer: bufS } },
513
+ { binding: 4, resource: { buffer: bufO } }, { binding: 5, resource: { buffer: bufD } } ], m, n, batch, d.acc);
514
  [bufX, bufW, bufR, bufS, bufD, r.bufO, r.read].forEach(b => b.destroy());
515
  return r.out;
516
  }
 
522
  return b;
523
  }
524
 
525
+ // ---- canonical kernel probe -------------------------------------------------
526
+ // The weight hash CANNOT catch a device whose kernel is wrong: weights only
527
+ // depend on the gradient bytes everyone receives, so a fleet averaging one
528
+ // device's bad gradient stays bit-identical and perfectly happy. This is the
529
+ // check that can. Every device runs the SAME seeded int8 GEMM through its own
530
+ // live kernel (verify variant -> raw int32 accumulator, exact on every
531
+ // backend — GPU DP4A, GPU LUT, CPU mirror alike) and hashes the result. Same
532
+ // input + correct kernels => same hash, regardless of hardware. A device that
533
+ // disagrees is computing different arithmetic than the rest of the fleet.
534
+ const PROBE = { m: 24, k: 96, n: 24, batch: 2 };
535
+ function probeInputs() {
536
+ let seed = 0x5EED; // fixed: identical on every device
537
+ const rnd = () => { seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
538
+ const { m, k, n, batch } = PROBE;
539
+ const Xq = new Int8Array(batch * m * k), Wq = new Int8Array(batch * k * n);
540
+ for (let i = 0; i < Xq.length; i++) Xq[i] = Math.round(rnd() * 254 - 127);
541
+ for (let i = 0; i < Wq.length; i++) Wq[i] = Math.round(rnd() * 254 - 127);
542
+ const rs = Float32Array.from({ length: batch * m }, () => 1);
543
+ const cs = Float32Array.from({ length: batch * n }, () => 1);
544
+ return { Xq, Wq, rs, cs, d: { ...PROBE, acc: true } };
545
+ }
546
+ async function kernelProbe(compute, L) {
547
+ const { Xq, Wq, rs, cs, d } = probeInputs();
548
+ const out = compute && compute.bgemm
549
+ ? await compute.bgemm(Xq, Wq, rs, cs, d)
550
+ : root.Verified.bgemmJS(Xq, Wq, rs, cs, d, L);
551
+ let h = 0x811c9dc5; // FNV-1a over the exact int32 results
552
+ const b = new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
553
+ for (let i = 0; i < b.length; i++) { h ^= b[i]; h = Math.imul(h, 0x01000193); }
554
+ return h >>> 0;
555
+ }
556
+
557
+ root.Compute = { initCompute, loadLUTs, kernelProbe };
558
  })(self);
web/test_gates.js ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Mutation test for the kernel admission gate.
2
+ //
3
+ // A gate that has never rejected anything is decoration. This injects deliberately
4
+ // buggy "kernels" and asserts the gate rejects each one — and, separately, shows
5
+ // two things the OLD gate (single tiny shape, allclose) let through.
6
+ //
7
+ // Runs in Node against the CPU mirrors, so the buggy kernels stand in for what a
8
+ // wrong GPU shader would produce.
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const V = require("./public/verified_core.js");
12
+
13
+ const mul = new Int16Array(fs.readFileSync(path.join(__dirname, "public", "mul_lut.bin")).buffer.slice(0));
14
+ const L = { mul };
15
+
16
+ // ---- the gate under test (mirrors webgpu.js gateBgemm) ----------------------
17
+ const SWEEP = [
18
+ { m: 5, k: 9, n: 6, batch: 3, relu: true },
19
+ { m: 32, k: 64, n: 32, batch: 1, relu: false },
20
+ { m: 7, k: 253, n: 5, batch: 2, relu: true },
21
+ { m: 1, k: 4, n: 1, batch: 1, relu: false },
22
+ { m: 17, k: 33, n: 9, batch: 1, relu: true },
23
+ ];
24
+ const OLD_SHAPE = [{ m: 5, k: 9, n: 6, batch: 3, relu: true }]; // what we used to test
25
+ const rnd = (n, f) => { const a = new Int8Array(n); for (let i = 0; i < n; i++) a[i] = f(); return a; };
26
+
27
+ function inputs(d) {
28
+ const Xq = rnd(d.batch * d.m * d.k, () => (Math.random() * 256 - 128) | 0);
29
+ const Wq = rnd(d.batch * d.k * d.n, () => (Math.random() * 256 - 128) | 0);
30
+ const rs = Float32Array.from({ length: d.batch * d.m }, () => Math.random() + 0.5);
31
+ const cs = Float32Array.from({ length: d.batch * d.n }, () => Math.random() + 0.5);
32
+ return { Xq, Wq, rs, cs };
33
+ }
34
+
35
+ // exact gate: int32 accumulator with !==, then f32 epilogue with !==
36
+ function gateExact(kernel, shapes) {
37
+ for (const d of shapes) {
38
+ const { Xq, Wq, rs, cs } = inputs(d);
39
+ const accHw = kernel(Xq, Wq, rs, cs, { ...d, acc: true }, L);
40
+ const accRef = V.bgemmJS(Xq, Wq, rs, cs, { ...d, acc: true }, L);
41
+ for (let i = 0; i < accRef.length; i++) if (accHw[i] !== accRef[i]) return `acc @${i}`;
42
+ const hw = kernel(Xq, Wq, rs, cs, d, L);
43
+ const ref = V.bgemmJS(Xq, Wq, rs, cs, d, L);
44
+ for (let i = 0; i < ref.length; i++) if (hw[i] !== ref[i]) return `epilogue @${i}`;
45
+ }
46
+ return null;
47
+ }
48
+ // the old gate: one shape, allclose on the fused f32 output only
49
+ function gateOld(kernel) {
50
+ for (const d of OLD_SHAPE) {
51
+ const { Xq, Wq, rs, cs } = inputs(d);
52
+ const hw = kernel(Xq, Wq, rs, cs, d, L);
53
+ const ref = V.bgemmJS(Xq, Wq, rs, cs, d, L);
54
+ for (let i = 0; i < ref.length; i++)
55
+ if (Math.abs(hw[i] - ref[i]) > Math.abs(ref[i]) * 1e-6 + 1e-6) return `allclose @${i}`;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ // ---- buggy kernels ----------------------------------------------------------
61
+ // Each is bgemmJS with exactly one realistic defect injected.
62
+ function mutant(bug) {
63
+ return function (Xq, Wq, rs, cs, d, LL) {
64
+ const { m, k, n } = d, batch = d.batch || 1, relu = !!d.relu, mulT = LL.mul;
65
+ const raw = !!d.acc;
66
+ const out = raw ? new Int32Array(batch * m * n) : new Float32Array(batch * m * n);
67
+ const acc = new Int32Array(n);
68
+ for (let bz = 0; bz < batch; bz++) {
69
+ const xo = bz * m * k, oo = bz * m * n, co = bz * n;
70
+ // BUG: batch stride dropped on W — invisible unless batch > 1
71
+ const wo = bug === "batchStride" ? 0 : bz * k * n;
72
+ for (let i = 0; i < m; i++) {
73
+ acc.fill(0);
74
+ const xrow = xo + i * k;
75
+ // BUG: last element of the dot product skipped
76
+ const kEnd = bug === "shortK" ? k - 1 : k;
77
+ for (let p = 0; p < kEnd; p++) {
78
+ const au = (Xq[xrow + p] & 0xFF) * 256, wrow = wo + p * n;
79
+ for (let j = 0; j < n; j++) acc[j] += mulT[au + (Wq[wrow + j] & 0xFF)];
80
+ }
81
+ const orow = oo + i * n;
82
+ if (raw) { for (let j = 0; j < n; j++) out[orow + j] = acc[j]; continue; }
83
+ const rscale = rs[bz * m + i];
84
+ for (let j = 0; j < n; j++) {
85
+ // BUG: column scale ignored / ReLU dropped / f64 rounding (the real one)
86
+ let v;
87
+ if (bug === "missingColScale") v = V.epi(acc[j], rscale, 1);
88
+ else if (bug === "f64Rounding") v = acc[j] * rscale * cs[co + j]; // rounds once, not thrice
89
+ else v = V.epi(acc[j], rscale, cs[co + j]);
90
+ out[orow + j] = (relu && bug !== "noRelu" && v < 0) ? 0 : v;
91
+ }
92
+ }
93
+ }
94
+ return out;
95
+ };
96
+ }
97
+
98
+ let pass = true;
99
+ const ok = (cond, msg) => { console.log(`${cond ? " ok " : " FAIL"} ${msg}`); if (!cond) pass = false; };
100
+
101
+ console.log("\nthe gate must reject every injected bug:");
102
+ for (const bug of ["batchStride", "shortK", "missingColScale", "noRelu", "f64Rounding"])
103
+ ok(gateExact(mutant(bug), SWEEP) !== null, `rejects ${bug} (${gateExact(mutant(bug), SWEEP) || "NOT CAUGHT"})`);
104
+
105
+ console.log("\nthe gate must accept the correct kernel:");
106
+ ok(gateExact(V.bgemmJS, SWEEP) === null, "accepts the real mirror across the shape sweep");
107
+
108
+ console.log("\nwhat the OLD gate (one tiny shape, allclose) let through:");
109
+ const oldMissedRounding = gateOld(mutant("f64Rounding")) === null;
110
+ const newCatchesRounding = gateExact(mutant("f64Rounding"), SWEEP) !== null;
111
+ ok(oldMissedRounding && newCatchesRounding,
112
+ "f64-vs-f32 epilogue rounding: allclose PASSES it, exact gate catches it");
113
+ const oldMissedStride = gateOld(mutant("batchStride")) === null;
114
+ console.log(` note batchStride under the old gate: ${oldMissedStride ? "MISSED" : "caught (batch>1 was in the shape)"}`);
115
+
116
+ console.log(pass ? "\nGATE TEST PASSED — the gate rejects real bugs and accepts the real kernel."
117
+ : "\nGATE TEST FAILED");
118
+ process.exit(pass ? 0 : 1);