// Verified INT8 compute — the emulated GPU logic, in the browser. // A layer's forward runs THROUGH the units: quantize -> LUT multiply -> requant // -> optional ReLU -> dequant. Backward is a straight-through estimator (the // integer path has no gradient), so ordinary float weights still learn. // Same units as the Python/Docker DaisyChain; here they're lookup tables. (function (root) { "use strict"; let TC; // TrainCore (matmul/transpose) — resolved per environment at the end function quantize(X) { let mx = 0; for (let i = 0; i < X.length; i++) { const a = Math.abs(X[i]); if (a > mx) mx = a; } const scale = Math.max(mx / 127, 1e-8); const q = new Int8Array(X.length); for (let i = 0; i < X.length; i++) { let v = Math.round(X[i] / scale); q[i] = v < -128 ? -128 : v > 127 ? 127 : v; } return { q, scale }; } // int8 matmul via the verified multiply LUT: acc(m×n) = sum_k mulLUT[Xq,Wq] function lutMatmulJS(Xq, Wq, m, k, n, L) { const C = new Int32Array(m * n), mul = L.mul; for (let i = 0; i < m; i++) { for (let p = 0; p < k; p++) { const au = (Xq[i * k + p] & 0xFF) * 256, wo = p * n, co = i * n; for (let j = 0; j < n; j++) C[co + j] += mul[au + (Wq[wo + j] & 0xFF)]; } } return C; } // ---- 3xINT8 fast-accurate GEMM -------------------------------------------- // The CUTLASS example-27 "3xTF32" scheme, ported to the verified units: // split each float into a coarse int8 part plus an int8-quantized residual, // run three EXACT LUT GEMMs (hi·hi, hi·lo, lo·hi), drop the negligible // lo·lo, and recombine. Same big/small decomposition NVIDIA uses to recover // near-fp32 accuracy from TF32 tensor cores — here it recovers ~14-bit // accuracy from the 8-bit units, at 3× the unit ops. Every product still // goes through the verified mul8 LUT. function quantize2(X) { const hi = quantize(X); const r = new Float32Array(X.length); for (let i = 0; i < X.length; i++) r[i] = X[i] - hi.q[i] * hi.scale; const lo = quantize(r); return { hi, lo }; } function combine3(hh, hl, lh, x, w, len) { const out = new Float32Array(len); const shh = x.hi.scale * w.hi.scale, shl = x.hi.scale * w.lo.scale, slh = x.lo.scale * w.hi.scale; for (let i = 0; i < len; i++) out[i] = hh[i] * shh + hl[i] * shl + lh[i] * slh; return out; } function lutMatmul3JS(Xf, Wf, m, k, n, L) { // sync, CPU LUT path const x = quantize2(Xf), w = quantize2(Wf); return combine3(lutMatmulJS(x.hi.q, w.hi.q, m, k, n, L), lutMatmulJS(x.hi.q, w.lo.q, m, k, n, L), lutMatmulJS(x.lo.q, w.hi.q, m, k, n, L), x, w, m * n); } async function lutMatmul3(Xf, Wf, m, k, n, L, matmulInt8) { // any backend const x = quantize2(Xf), w = quantize2(Wf); const mm = matmulInt8 || lutMatmulJS; const [hh, hl, lh] = await Promise.all([ mm(x.hi.q, w.hi.q, m, k, n, L), mm(x.hi.q, w.lo.q, m, k, n, L), mm(x.lo.q, w.hi.q, m, k, n, L), ]); return combine3(hh, hl, lh, x, w, m * n); } // ---- block-scaled verified GEMM (CUTLASS ex. 67/81 blockwise scaling) ------ // Per-ROW scales for the activations and per-COLUMN scales for the weights: // the integer math through the mul8 LUT is completely unchanged — only the // dequant uses rs[row]·cs[col] instead of one tensor-wide product, so a single // outlier no longer crushes the quantization resolution of every other // row/column. One LUT pass at this granularity beats the per-tensor 3-pass. function quantizeRows(X, rows, cols) { const q = new Int8Array(rows * cols), s = new Float32Array(rows); for (let r = 0; r < rows; r++) { let mx = 0; for (let c = 0; c < cols; c++) { const a = Math.abs(X[r * cols + c]); if (a > mx) mx = a; } const sc = Math.max(mx / 127, 1e-8); s[r] = sc; for (let c = 0; c < cols; c++) { const v = Math.round(X[r * cols + c] / sc); q[r * cols + c] = v < -128 ? -128 : v > 127 ? 127 : v; } } return { q, s }; } function quantizeCols(W, rows, cols) { const q = new Int8Array(rows * cols), s = new Float32Array(cols); for (let c = 0; c < cols; c++) { let mx = 0; for (let r = 0; r < rows; r++) { const a = Math.abs(W[r * cols + c]); if (a > mx) mx = a; } s[c] = Math.max(mx / 127, 1e-8); } for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) { const v = Math.round(W[r * cols + c] / s[c]); q[r * cols + c] = v < -128 ? -128 : v > 127 ? 127 : v; } return { q, s }; } // ---- B2B MLP chain (CUTLASS ex. 13 two-GEMM fusion + ex. 23 epilogue // reduction), respecced for cross-device exactness --------------------------- // The MLP is the one back-to-back GEMM pair with no layernorm/softmax between // (ReLU is already fused in the epilogue), so the intermediate h1 can be // quantized ON the GPU and fed straight to the second GEMM. Two rules make // that fleet-safe: // 1. The per-row |max| (ex. 23) uses only comparisons — exact on any // hardware, order-independent — and comes back to JS as ~1KB. // 2. Scale DERIVATION (two divisions) stays in JS f64, which IEEE requires // to be exactly rounded and is therefore identical on every device. // WGSL division is only 2.5 ULP — a fork waiting to happen — but WGSL // multiply/add are correctly rounded and floor/clamp are exact. So the // quantize step is respecced from round(x / scale) to // floor(f32(x * invScale) + 0.5) — floor(x+0.5) IS Math.round's tie // rule — and the fround-stepped mirror below is bit-identical to the // GPU kernel, which is exact-gated against it at init. // NOTE this changes which int8 a value on a rounding boundary lands on // (≤1 step) vs quantizeRows, so old and new builds cannot co-train — the // divergence guard stops such mixed groups by design. function rowAbsMax(X, rows, cols) { const mx = new Float32Array(rows); for (let r = 0; r < rows; r++) { let m = 0; for (let c = 0; c < cols; c++) { const a = Math.abs(X[r * cols + c]); if (a > m) m = a; } mx[r] = m; } return mx; } function scalesFromAbsMax(mx) { // f64 divisions: exactly rounded, device-identical const scale = new Float32Array(mx.length), inv = new Float32Array(mx.length); for (let i = 0; i < mx.length; i++) { scale[i] = Math.max(mx[i] / 127, 1e-8); inv[i] = 1 / scale[i]; // recip of the STORED f32 scale } return { scale, inv }; } function quantizeRowsInv(X, rows, cols, inv) { // bit-exact mirror of the GPU quantize kernel const q = new Int8Array(rows * cols); for (let r = 0; r < rows; r++) { const iv = inv[r]; for (let c = 0; c < cols; c++) { const n = Math.floor(f32(f32(X[r * cols + c] * iv) + 0.5)); q[r * cols + c] = n < -128 ? -128 : n > 127 ? 127 : n; } } return q; } // the chained MLP: X @ W1 -> ReLU (fused) -> absmax -> quantize -> @ W2. // d = { m, k, h, n }; gpuMlp (from webgpu.js) runs both GEMMs + the // on-GPU quantize with one tiny absmax readback between; without it the CPU // mirror chain runs — SAME math, so mixed GPU/CPU fleets stay bit-identical. async function vmlpBlock(Xf, W1f, W2f, d, L, gpuMlp, audit) { const x = quantizeRows(Xf, d.m, d.k); const w1 = quantizeCols(W1f, d.k, d.h); const w2 = quantizeCols(W2f, d.h, d.n); if (gpuMlp) { const r = await gpuMlp(x.q, w1.q, w2.q, x.s, w1.s, w2.s, d); if (audit && audit.due()) { // audit BOTH live GEMMs: gemm1 against the units directly; gemm2 by // reconstructing its exact operand through the proven quantize mirror const bad1 = auditTile(x.q, w1.q, x.s, w1.s, { m: d.m, k: d.k, n: d.h, relu: true }, r.h1, L, audit.cells); if (bad1) { audit.fail("mlp gemm1: " + bad1); return r; } const sc = scalesFromAbsMax(rowAbsMax(r.h1, d.m, d.h)); const hq = quantizeRowsInv(r.h1, d.m, d.h, sc.inv); const bad2 = auditTile(hq, w2.q, sc.scale, w2.s, { m: d.m, k: d.h, n: d.n }, r.out, L, audit.cells); if (bad2) audit.fail("mlp gemm2: " + bad2); } return r; } const h1 = bgemmJS(x.q, w1.q, x.s, w1.s, { m: d.m, k: d.k, n: d.h, batch: 1, relu: true }, L); const sc = scalesFromAbsMax(rowAbsMax(h1, d.m, d.h)); const hq = quantizeRowsInv(h1, d.m, d.h, sc.inv); const out = bgemmJS(hq, w2.q, sc.scale, w2.s, { m: d.m, k: d.h, n: d.n, batch: 1 }, L); return { h1, out }; } // ---- epilogue mirror ------------------------------------------------------- // BIT-EXACT mirror of the WGSL epilogue `f32(s) * a * b`. WGSL rounds to f32 // after the int->float conversion and after EACH multiply; plain JS would do // the whole chain in f64 and round once, which differs in the last ulp. That // last-ulp gap is what used to force a tolerance into the kernel gates — // mirroring the rounding exactly is what lets the gates compare with `!==`. const f32 = Math.fround; function epi(s, a, b) { return f32(f32(f32(s) * a) * b); } // f32 equality at the BIT level: `!==` says -0 === 0, but replicas are // compared by hashing raw bytes, so an audit that can't see the sign of zero // could pass a device that later forks the fleet. (Real ISAs have non-IEEE // modes that flush -0 to +0 — e.g. RDNA2 output modifiers / legacy muls.) const _fb = new Float32Array(1), _ub = new Uint32Array(_fb.buffer); function bitDiff(a, b) { _fb[0] = a; const u = _ub[0]; _fb[0] = b; return u !== _ub[0]; } // CPU mirror of the fused GPU kernel: batched int8 GEMM through the LUT with // the epilogue (block dequant + optional ReLU) applied before returning — // exactly what the WGSL kernel does on-device. d.acc=true returns the raw // int32 accumulator instead (the exact oracle the fused kernel normally hides). function bgemmJS(Xq, Wq, rs, cs, d, L) { const { m, k, n } = d, batch = d.batch || 1, relu = !!d.relu, mul = L.mul; const raw = !!d.acc; const out = raw ? new Int32Array(batch * m * n) : new Float32Array(batch * m * n); const acc = new Int32Array(n); for (let bz = 0; bz < batch; bz++) { const xo = bz * m * k, wo = bz * k * n, oo = bz * m * n, co = bz * n; for (let i = 0; i < m; i++) { acc.fill(0); const xrow = xo + i * k; for (let p = 0; p < k; p++) { const au = (Xq[xrow + p] & 0xFF) * 256, wrow = wo + p * n; for (let j = 0; j < n; j++) acc[j] += mul[au + (Wq[wrow + j] & 0xFF)]; } const orow = oo + i * n; if (raw) { for (let j = 0; j < n; j++) out[orow + j] = acc[j]; continue; } const rscale = rs[bz * m + i]; for (let j = 0; j < n; j++) { const v = epi(acc[j], rscale, cs[co + j]); out[orow + j] = relu && v < 0 ? 0 : v; } } } return out; } // Recompute a handful of RANDOM output cells of a live GEMM through the LUT // mirror and compare against what the kernel produced. Sampling cells instead // of whole matrices makes this cheap enough to run continuously, at the real // shapes training uses — not once at boot on toy inputs. // STRATIFIED sampling. Uniformly random cells are the wrong instrument for // the bugs that actually occur here: a bounds-guard off-by-one or a pack-tail // padding bug lives on the LAST row/column, and uniform sampling finds that // with probability ~1/n per cell — at the 16512-wide logits GEMM, never. So // the first cells are the structurally dangerous ones (corners, last row, // last column, last batch) chosen deterministically, and the remainder are // random interior cells that catch diffuse bugs. Same principle as poisoning // the buffer pool: construct the dangerous case, don't wait to land on it. function auditTile(Xq, Wq, rs, cs, d, got, L, nCells) { const { m, k, n } = d, batch = d.batch || 1, relu = !!d.relu, mul = L.mul; const N = nCells || 8; const edges = [[0, m - 1, n - 1], [0, 0, n - 1], [0, m - 1, 0], [0, 0, 0], [batch - 1, m - 1, n - 1], [batch - 1, 0, 0]]; for (let t = 0; t < N; t++) { let bz, i, j; if (t < edges.length) { bz = edges[t][0]; i = edges[t][1]; j = edges[t][2]; } else { bz = (Math.random() * batch) | 0; i = (Math.random() * m) | 0; j = (Math.random() * n) | 0; } let acc = 0; const xrow = bz * m * k + i * k, wo = bz * k * n; for (let p = 0; p < k; p++) acc += mul[(Xq[xrow + p] & 0xFF) * 256 + (Wq[wo + p * n + j] & 0xFF)]; let v = epi(acc, rs[bz * m + i], cs[bz * n + j]); if (relu && v < 0) v = 0; const idx = (bz * m + i) * n + j; if (bitDiff(got[idx], v)) return `GEMM audit failed at [b${bz},${i},${j}] shape ${m}x${k}x${n}: kernel ${Object.is(got[idx], -0) ? "-0" : got[idx]} vs units ${Object.is(v, -0) ? "-0" : v}`; } return null; } // ---- exact mirror of the split-K f32 GEMM ---------------------------------- // The f32 backward GEMM was the last kernel gated by a TOLERANCE (allclose at // 1e-3) — and this project's own gate mutation test shows allclose waving // through real bugs. The reason was real though: split-K accumulates in a // different ORDER than a naive reference, so bit-equality against the naive // one is impossible. The fix is the same as the epilogue mirror: reproduce // the kernel's order exactly, then compare with `!==`. // partials: for z in 0..S-1, sum p in [z*ks, min(k,(z+1)*ks)) in order // reduce: sum the S partials in ascending z // `fma` selects the rounding schedule for `s + a*b`: WGSL PERMITS a compiler // to contract that into a fused multiply-add (one rounding) instead of two. // Which one the device does is a fact about the device, so the gate tries // both and reports which matches rather than assuming. function fgemmMirror(A, Bm, d, fma) { const { m, k, n } = d, transA = !!d.transA; const S = k > 4096 ? Math.min(16, Math.ceil(k / 2048)) : 1; const ks = Math.ceil(k / S); const out = new Float32Array(m * n); for (let row = 0; row < m; row++) for (let col = 0; col < n; col++) { let acc = 0; // reduce pass, ascending z for (let z = 0; z < S; z++) { const p0 = z * ks, p1 = Math.min(k, p0 + ks); let s = 0; // one partial, in order for (let p = p0; p < p1; p++) { const a = transA ? A[p * m + row] : A[row * k + p]; s = fma ? f32(s + a * Bm[p * n + col]) // single rounding : f32(s + f32(a * Bm[p * n + col])); } acc = f32(acc + s); } out[row * n + col] = acc; } return out; } // ---- live audits for the fused attention kernels --------------------------- // The attention kernels had exact INIT gates but nothing at live shapes — // the exact gap the GEMM audit exists to close, left open on the kernels with // the trickiest indexing (head-strided gather, scatter write-back). These // recompute individual output cells from the units, stratified like // auditTile: last/first token pair, last head, last channel first, then // random. Cost is hd (or T) multiply-adds per cell. function auditAttScores(qq, kq, qs, ks, d, got, L, nCells) { const { B, T, heads, hd } = d, C = heads * hd, mul = L.mul, raw = !!d.acc; const N = nCells || 8; const edges = [[B - 1, heads - 1, T - 1, T - 1], [0, 0, 0, 0], [0, heads - 1, T - 1, 0], [B - 1, 0, 0, T - 1]]; for (let t = 0; t < N; t++) { let bi, h, ti, tj; if (t < edges.length) { bi = edges[t][0]; h = edges[t][1]; ti = edges[t][2]; tj = edges[t][3]; } else { bi = (Math.random() * B) | 0; h = (Math.random() * heads) | 0; ti = (Math.random() * T) | 0; tj = (Math.random() * T) | 0; } const bz = bi * heads + h; const qo = (bi * T + ti) * C + h * hd, ko = (bi * T + tj) * C + h * hd; let acc = 0; for (let p = 0; p < hd; p++) acc += mul[(qq[qo + p] & 0xFF) * 256 + (kq[ko + p] & 0xFF)]; const v = raw ? acc : epi(acc, qs[(bi * T + ti) * heads + h], ks[(bi * T + tj) * heads + h]); const idx = (bz * T + ti) * T + tj; if (raw ? got[idx] !== v : bitDiff(got[idx], v)) return `att.scores audit failed at [b${bi},h${h},${ti},${tj}] B${B}T${T}H${heads}d${hd}: kernel ${got[idx]} vs units ${v}`; } return null; } function auditAttCtx(aq, vq, as, vs, d, got, L, nCells) { const { B, T, heads, hd } = d, C = heads * hd, mul = L.mul, raw = !!d.acc; const N = nCells || 8; const edges = [[B - 1, heads - 1, T - 1, hd - 1], [0, 0, 0, 0], [0, heads - 1, T - 1, 0], [B - 1, 0, 0, hd - 1]]; for (let t = 0; t < N; t++) { let bi, h, ti, j; if (t < edges.length) { bi = edges[t][0]; h = edges[t][1]; ti = edges[t][2]; j = edges[t][3]; } else { bi = (Math.random() * B) | 0; h = (Math.random() * heads) | 0; ti = (Math.random() * T) | 0; j = (Math.random() * hd) | 0; } const bz = bi * heads + h, ao = (bz * T + ti) * T; let acc = 0; for (let tj = 0; tj < T; tj++) acc += mul[(aq[ao + tj] & 0xFF) * 256 + (vq[(bi * T + tj) * C + h * hd + j] & 0xFF)]; const v = raw ? acc : epi(acc, as[bz * T + ti], vs[(bi * heads + h) * hd + j]); const idx = (bi * T + ti) * C + h * hd + j; if (raw ? got[idx] !== v : bitDiff(got[idx], v)) return `att.ctx audit failed at [b${bi},h${h},${ti},${j}] B${B}T${T}H${heads}d${hd}: kernel ${got[idx]} vs units ${v}`; } return null; } // block-scaled verified GEMM, float in → float out. // d = { m, k, n, batch=1, relu=false }; X is (batch·m)×k, W is batch×(k×n) // gpuBgemm (from webgpu.js) runs the batched kernel with the fused epilogue; // without it the CPU LUT mirror runs. Every product goes through the units. async function vgemmBlock(Xf, Wf, d, L, gpuBgemm, audit) { const { m, k, n } = d, batch = d.batch || 1; const x = quantizeRows(Xf, batch * m, k); let wq, ws; if (batch === 1) { const w = quantizeCols(Wf, k, n); wq = w.q; ws = w.s; } else { wq = new Int8Array(batch * k * n); ws = new Float32Array(batch * n); for (let bz = 0; bz < batch; bz++) { const w = quantizeCols(Wf.subarray(bz * k * n, (bz + 1) * k * n), k, n); wq.set(w.q, bz * k * n); ws.set(w.s, bz * n); } } if (gpuBgemm) { const out = await gpuBgemm(x.q, wq, x.s, ws, d); // continuous re-verification at LIVE shapes: the boot gate only ever saw // toy inputs, so sample a few real cells against the units as we go if (audit && audit.due()) { const bad = auditTile(x.q, wq, x.s, ws, d, out, L, audit.cells); if (bad) audit.fail(bad); } return out; } return bgemmJS(x.q, wq, x.s, ws, d, L); } // ---- gather-fused attention through the units (CUTLASS ex. 36/52) ---------- // The kernels read q/k/v/ctx directly in their natural BT×C layout with // head-strided indexing — no JS gather copies, no kᵀ transpose, and the // context write scatters straight back into BT×C. Quantization stays // block-scaled: q/k/a per (token,head) row, v per (head,channel) column. // The (BT·heads)×hd row view of q/k IS the contiguous buffer, so // quantizeRows(q, BT·heads, hd) gives per-(token,head) scales for free. function quantizeHeadCols(v, B, T, heads, hd) { // per (batch,head,channel) column const C = heads * hd; const q = new Int8Array(B * T * C), s = new Float32Array(B * heads * hd); for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) for (let j = 0; j < hd; j++) { let mx = 0; for (let ti = 0; ti < T; ti++) { const a = Math.abs(v[(bi * T + ti) * C + h * hd + j]); if (a > mx) mx = a; } const sc = Math.max(mx / 127, 1e-8); s[(bi * heads + h) * hd + j] = sc; for (let ti = 0; ti < T; ti++) { const idx = (bi * T + ti) * C + h * hd + j; const w = Math.round(v[idx] / sc); q[idx] = w < -128 ? -128 : w > 127 ? 127 : w; } } return { q, s }; } // scores S[bz,ti,tj] = q_row(bi,ti,h) · k_row(bi,tj,h), every product via the LUT // d.acc=true returns the raw int32 accumulator (exact oracle for the kernel gate) function attScoresJS(qq, kq, qs, ks, d, L) { const { B, T, heads, hd } = d, C = heads * hd, mul = L.mul, raw = !!d.acc; const out = raw ? new Int32Array(B * heads * T * T) : new Float32Array(B * heads * T * T); for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) { const bz = bi * heads + h; for (let ti = 0; ti < T; ti++) { const qo = (bi * T + ti) * C + h * hd, rscale = qs[(bi * T + ti) * heads + h]; for (let tj = 0; tj < T; tj++) { const ko = (bi * T + tj) * C + h * hd; let acc = 0; for (let p = 0; p < hd; p++) acc += mul[(qq[qo + p] & 0xFF) * 256 + (kq[ko + p] & 0xFF)]; out[(bz * T + ti) * T + tj] = raw ? acc : epi(acc, rscale, ks[(bi * T + tj) * heads + h]); } } } return out; } // ctx[(bi,ti),(h,j)] = Σ_tj a[bz,ti,tj]·v[(bi,tj),(h,j)] — scatter fused into BT×C function attCtxJS(aq, vq, as, vs, d, L) { const { B, T, heads, hd } = d, C = heads * hd, mul = L.mul, raw = !!d.acc; const out = raw ? new Int32Array(B * T * C) : new Float32Array(B * T * C); for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) { const bz = bi * heads + h; for (let ti = 0; ti < T; ti++) { const ao = (bz * T + ti) * T, rscale = as[bz * T + ti]; for (let j = 0; j < hd; j++) { let acc = 0; for (let tj = 0; tj < T; tj++) acc += mul[(aq[ao + tj] & 0xFF) * 256 + (vq[(bi * T + tj) * C + h * hd + j] & 0xFF)]; out[(bi * T + ti) * C + h * hd + j] = raw ? acc : epi(acc, rscale, vs[(bi * heads + h) * hd + j]); } } } return out; } // one verified layer forward; returns float out (+ cache for STE backward). // Every product goes through the verified INT8 multiply (mul8 LUT) with exact // int32 accumulation — i.e. an emulated INT8 tensor-core GEMM — then dequant. async function linearFwd(X, W, m, k, n, L, useRelu, matmulInt8) { const xq = quantize(X), wq = quantize(W); const acc = await (matmulInt8 || lutMatmulJS)(xq.q, wq.q, m, k, n, L); // verified multiply const dq = xq.scale * wq.scale; const out = new Float32Array(m * n); const mask = useRelu ? new Uint8Array(m * n) : null; for (let i = 0; i < m * n; i++) { let v = acc[i] * dq; if (useRelu) { if (v > 0) mask[i] = 1; else v = 0; } out[i] = v; } return { out, mask }; } // 2-layer MLP: X→H (relu) →dout. Forward through verified units, MSE loss. async function forward(X, y, W1, W2, D, L, matmulInt8) { const { n, din, h, dout } = D; const l1 = await linearFwd(X, W1, n, din, h, L, true, matmulInt8); const l2 = await linearFwd(l1.out, W2, n, h, dout, L, false, matmulInt8); const resid = new Float32Array(n * dout); let loss = 0; for (let i = 0; i < resid.length; i++) { const r = l2.out[i] - y[i]; resid[i] = r; loss += r * r; } loss /= resid.length; return { loss, resid, z1: l1.out, mask1: l1.mask }; } // STE backward (verified matmul treated as float X@W). Returns flat [gW1, gW2]. function backward(X, W1, W2, fwd, D) { const { n, din, h, dout } = D; const { resid, z1, mask1 } = fwd; const s = 2 / n; const dout_ = new Float32Array(resid.length); for (let i = 0; i < resid.length; i++) dout_[i] = resid[i] * s; const mm = TC.matmul, tr = TC.transpose; const gW2 = mm(tr(z1, n, h), dout_, h, n, dout); // z1ᵀ @ dout const dz1 = mm(dout_, tr(W2, h, dout), n, dout, h); // dout @ W2ᵀ for (let i = 0; i < dz1.length; i++) if (!mask1[i]) dz1[i] = 0; // relu grad const gW1 = mm(tr(X, n, din), dz1, din, n, h); // Xᵀ @ dz1 const g = new Float32Array(gW1.length + gW2.length); g.set(gW1, 0); g.set(gW2, gW1.length); return g; } function splitApply(W1, W2, gAvg, lr) { for (let i = 0; i < W1.length; i++) W1[i] -= lr * gAvg[i]; for (let j = 0; j < W2.length; j++) W2[j] -= lr * gAvg[W1.length + j]; } const api = { quantize, quantize2, quantizeRows, quantizeCols, quantizeHeadCols, lutMatmulJS, lutMatmul3JS, lutMatmul3, bgemmJS, vgemmBlock, auditTile, epi, attScoresJS, attCtxJS, linearFwd, forward, backward, splitApply, rowAbsMax, scalesFromAbsMax, quantizeRowsInv, vmlpBlock, bitDiff, auditAttScores, auditAttCtx, fgemmMirror }; if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); module.exports = api; } else { TC = root.TrainCore; root.Verified = api; } })(typeof self !== "undefined" ? self : this);