Quazim0t0 commited on
Commit
32f59c2
·
verified ·
1 Parent(s): 56e2384

Web demo: block-scaled INT8 + batched/fused GEMM kernels

Browse files
web/public/app.js CHANGED
@@ -806,7 +806,7 @@ function cfgSliders() { return [ui.cfgC, ui.cfgT, ui.cfgB, ui.cfgSteps, ui.cfgLr
806
  // (re)build the mini transformer — deterministic shared init (same seeds on
807
  // every peer); each device samples its own batch windows from the shared corpus
808
  function buildModel(cfg) {
809
- model = Transformer.init(cfg, L, compute.matmulInt8);
810
  trainedSteps = 0;
811
  }
812
 
@@ -856,7 +856,7 @@ function buildModel(cfg) {
856
  ui.start.disabled = true;
857
  return; // no signaling, no training
858
  }
859
- ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label} · INT8 fast-accurate through verified units`;
860
  // tokenizer must load BEFORE the model is built (it sets the vocab size)
861
  try {
862
  const name = await Transformer.loadTokenizer();
 
806
  // (re)build the mini transformer — deterministic shared init (same seeds on
807
  // every peer); each device samples its own batch windows from the shared corpus
808
  function buildModel(cfg) {
809
+ model = Transformer.init(cfg, L, compute); // compute.bgemm = fused batched GPU path
810
  trainedSteps = 0;
811
  }
812
 
 
856
  ui.start.disabled = true;
857
  return; // no signaling, no training
858
  }
859
+ ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label} · block-scaled INT8, batched + fused epilogue, through verified units`;
860
  // tokenizer must load BEFORE the model is built (it sets the vocab size)
861
  try {
862
  const name = await Transformer.loadTokenizer();
web/public/transformer.js CHANGED
@@ -111,17 +111,17 @@
111
  const streamFineWebEdu = () => streamDataset(DEFAULT_DS);
112
  function datasetName() { return DATASET; }
113
 
114
- // ---- verified matmul: float in -> quantize -> LUT multiply -> dequant -----
115
- // weights/projections may go through WebGPU; attention (per-head, many small
116
- // matmuls) uses the CPU LUT path same verified units, no dispatch overhead.
117
- // 3xINT8 fast-accurate GEMM (CUTLASS 3xTF32 scheme through the verified
118
- // units): three exact LUT GEMMs recover ~14-bit precision from the 8-bit
119
- // units see Verified.lutMatmul3.
120
- async function vmm(Xf, Wf, m, k, n, ctx) {
121
- return V.lutMatmul3(Xf, Wf, m, k, n, ctx.L, ctx.matmulInt8);
122
  }
123
- function vmmCPU(Xf, Wf, m, k, n, ctx) {
124
- return V.lutMatmul3JS(Xf, Wf, m, k, n, ctx.L);
 
125
  }
126
 
127
  // ---- layernorm (no affine) -------------------------------------------------
@@ -148,7 +148,9 @@
148
 
149
  // ---- model -----------------------------------------------------------------
150
  // cfg: { c: width, t: seq len, b: batch/device, layers, heads, steps, lr }
151
- function init(cfg, L, matmulInt8) {
 
 
152
  const c = cfg.c, layers = cfg.layers || 2, heads = cfg.heads || 2, hidden = 2 * c;
153
  let seed = 100;
154
  const mk = (nEl, scale) => { const w = randn(nEl, mulberry32(seed++)); for (let i = 0; i < nEl; i++) w[i] *= scale; return w; };
@@ -156,7 +158,7 @@
156
  const add = (name, w) => { params.push(w); names.push(name); return w; };
157
  const m = {
158
  cfg: { ...cfg, layers, heads, hidden, vocab: vocabSize() },
159
- ctx: { L, matmulInt8 },
160
  emb: add("emb", mk(vocabSize() * c, 0.08)),
161
  pos: add("pos", mk(cfg.t * c, 0.02)),
162
  blocks: [], params, names,
@@ -217,22 +219,43 @@
217
  const ctxOut = new Float32Array(BT * C);
218
  cb.att = []; // per (b,h): softmax probs
219
  const scale = 1 / Math.sqrt(hd);
 
 
 
 
 
 
220
  for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
221
- const qh = headSlice(q, bi, h, T, C, hd), kh = headSlice(k, bi, h, T, C, hd), vh = headSlice(v, bi, h, T, C, hd);
222
- const kT = TC.transpose(kh, T, hd); // hd×T
223
- const s = vmmCPU(qh, kT, T, hd, T, ctx); // scores through the units
224
- for (let i = 0; i < T * T; i++) s[i] *= scale;
225
- const a = new Float32Array(T * T); // causal softmax
 
 
 
 
 
 
 
 
226
  for (let ti = 0; ti < T; ti++) {
227
  let mx = -1e30;
228
- for (let tj = 0; tj <= ti; tj++) mx = Math.max(mx, s[ti * T + tj]);
229
  let z = 0;
230
- for (let tj = 0; tj <= ti; tj++) { const e = Math.exp(s[ti * T + tj] - mx); a[ti * T + tj] = e; z += e; }
231
- for (let tj = 0; tj <= ti; tj++) a[ti * T + tj] /= z;
232
  }
233
- const ch = vmmCPU(a, vh, T, T, hd, ctx); // attn·V through the units
234
- headUnslice(ctxOut, ch, bi, h, T, C, hd, false);
235
- cb.att.push({ a, qh, kh, vh });
 
 
 
 
 
 
 
236
  }
237
  cb.ctxOut = ctxOut;
238
  const attnOut = await vmm(ctxOut, bl.Wo, BT, C, C, ctx);
@@ -240,9 +263,9 @@
240
  for (let i = 0; i < x2.length; i++) x2[i] = x[i] + attnOut[i];
241
  cb.x2 = x2;
242
  const l2 = lnFwd(x2, BT, C); cb.ln2 = l2;
243
- const h1 = await vmm(l2.y, bl.W1, BT, C, hidden, ctx);
244
  const mask = new Uint8Array(h1.length);
245
- for (let i = 0; i < h1.length; i++) { if (h1[i] > 0) mask[i] = 1; else h1[i] = 0; }
246
  cb.h1 = h1; cb.mask = mask;
247
  const mlpOut = await vmm(h1, bl.W2, BT, hidden, C, ctx);
248
  x = new Float32Array(BT * C);
 
111
  const streamFineWebEdu = () => streamDataset(DEFAULT_DS);
112
  function datasetName() { return DATASET; }
113
 
114
+ // ---- verified matmul: block-scaled INT8 through the units ------------------
115
+ // CUTLASS ex. 67/81 blockwise scaling: per-row activation scales × per-column
116
+ // weight scales, one exact LUT/DP4A GEMM, dequant (+ optional fused ReLU) in
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
+ // batched: one dispatch for all B×H attention-head problems (ex. 05/24)
123
+ async function vmmBatched(Xf, Wf, m, k, n, batch, ctx) {
124
+ return V.vgemmBlock(Xf, Wf, { m, k, n, batch }, ctx.L, ctx.bgemm);
125
  }
126
 
127
  // ---- layernorm (no affine) -------------------------------------------------
 
148
 
149
  // ---- model -----------------------------------------------------------------
150
  // cfg: { c: width, t: seq len, b: batch/device, layers, heads, steps, lr }
151
+ // engine: the Compute backend object ({bgemm} for the fused WebGPU path) or a
152
+ // legacy matmulInt8 function (Node tests, inference kit) -> CPU LUT mirror
153
+ function init(cfg, L, engine) {
154
  const c = cfg.c, layers = cfg.layers || 2, heads = cfg.heads || 2, hidden = 2 * c;
155
  let seed = 100;
156
  const mk = (nEl, scale) => { const w = randn(nEl, mulberry32(seed++)); for (let i = 0; i < nEl; i++) w[i] *= scale; return w; };
 
158
  const add = (name, w) => { params.push(w); names.push(name); return w; };
159
  const m = {
160
  cfg: { ...cfg, layers, heads, hidden, vocab: vocabSize() },
161
+ ctx: { L, bgemm: (engine && engine.bgemm) || null },
162
  emb: add("emb", mk(vocabSize() * c, 0.08)),
163
  pos: add("pos", mk(cfg.t * c, 0.02)),
164
  blocks: [], params, names,
 
219
  const ctxOut = new Float32Array(BT * C);
220
  cb.att = []; // per (b,h): softmax probs
221
  const scale = 1 / Math.sqrt(hd);
222
+ // gather every head into batched contiguous layouts, then run ALL B×H
223
+ // score GEMMs in ONE batched dispatch (and all attn·V in another) —
224
+ // instead of 2·B·H tiny sequential matmuls
225
+ const BH = B * heads;
226
+ const qb = new Float32Array(BH * T * hd), kb = new Float32Array(BH * T * hd);
227
+ const vb = new Float32Array(BH * T * hd), ktb = new Float32Array(BH * hd * T);
228
  for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
229
+ const bz = bi * heads + h;
230
+ for (let ti = 0; ti < T; ti++) for (let j = 0; j < hd; j++) {
231
+ const src = (bi * T + ti) * C + h * hd + j;
232
+ qb[(bz * T + ti) * hd + j] = q[src];
233
+ kb[(bz * T + ti) * hd + j] = k[src];
234
+ vb[(bz * T + ti) * hd + j] = v[src];
235
+ ktb[(bz * hd + j) * T + ti] = k[src]; // kᵀ per head, for scores
236
+ }
237
+ }
238
+ const sAll = await vmmBatched(qb, ktb, T, hd, T, BH, ctx); // scores through the units
239
+ const aAll = new Float32Array(BH * T * T); // causal softmax
240
+ for (let bz = 0; bz < BH; bz++) {
241
+ const so = bz * T * T;
242
  for (let ti = 0; ti < T; ti++) {
243
  let mx = -1e30;
244
+ for (let tj = 0; tj <= ti; tj++) mx = Math.max(mx, sAll[so + ti * T + tj] * scale);
245
  let z = 0;
246
+ for (let tj = 0; tj <= ti; tj++) { const e = Math.exp(sAll[so + ti * T + tj] * scale - mx); aAll[so + ti * T + tj] = e; z += e; }
247
+ for (let tj = 0; tj <= ti; tj++) aAll[so + ti * T + tj] /= z;
248
  }
249
+ }
250
+ const cAll = await vmmBatched(aAll, vb, T, T, hd, BH, ctx); // attn·V through the units
251
+ for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
252
+ const bz = bi * heads + h;
253
+ for (let ti = 0; ti < T; ti++) for (let j = 0; j < hd; j++)
254
+ ctxOut[(bi * T + ti) * C + h * hd + j] = cAll[(bz * T + ti) * hd + j];
255
+ cb.att.push({ a: aAll.subarray(bz * T * T, (bz + 1) * T * T),
256
+ qh: qb.subarray(bz * T * hd, (bz + 1) * T * hd),
257
+ kh: kb.subarray(bz * T * hd, (bz + 1) * T * hd),
258
+ vh: vb.subarray(bz * T * hd, (bz + 1) * T * hd) });
259
  }
260
  cb.ctxOut = ctxOut;
261
  const attnOut = await vmm(ctxOut, bl.Wo, BT, C, C, ctx);
 
263
  for (let i = 0; i < x2.length; i++) x2[i] = x[i] + attnOut[i];
264
  cb.x2 = x2;
265
  const l2 = lnFwd(x2, BT, C); cb.ln2 = l2;
266
+ const h1 = await vmm(l2.y, bl.W1, BT, C, hidden, ctx, true); // ReLU fused in the epilogue
267
  const mask = new Uint8Array(h1.length);
268
+ for (let i = 0; i < h1.length; i++) if (h1[i] > 0) mask[i] = 1;
269
  cb.h1 = h1; cb.mask = mask;
270
  const mlpOut = await vmm(h1, bl.W2, BT, hidden, C, ctx);
271
  x = new Float32Array(BT * C);
web/public/verified_core.js CHANGED
@@ -66,6 +66,87 @@
66
  return combine3(hh, hl, lh, x, w, m * n);
67
  }
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  // one verified layer forward; returns float out (+ cache for STE backward).
70
  // Every product goes through the verified INT8 multiply (mul8 LUT) with exact
71
  // int32 accumulation — i.e. an emulated INT8 tensor-core GEMM — then dequant.
@@ -116,7 +197,8 @@
116
  for (let j = 0; j < W2.length; j++) W2[j] -= lr * gAvg[W1.length + j];
117
  }
118
 
119
- const api = { quantize, quantize2, lutMatmulJS, lutMatmul3JS, lutMatmul3, linearFwd, forward, backward, splitApply };
 
120
  if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); module.exports = api; }
121
  else { TC = root.TrainCore; root.Verified = api; }
122
  })(typeof self !== "undefined" ? self : this);
 
66
  return combine3(hh, hl, lh, x, w, m * n);
67
  }
68
 
69
+ // ---- block-scaled verified GEMM (CUTLASS ex. 67/81 blockwise scaling) ------
70
+ // Per-ROW scales for the activations and per-COLUMN scales for the weights:
71
+ // the integer math through the mul8 LUT is completely unchanged — only the
72
+ // dequant uses rs[row]·cs[col] instead of one tensor-wide product, so a single
73
+ // outlier no longer crushes the quantization resolution of every other
74
+ // row/column. One LUT pass at this granularity beats the per-tensor 3-pass.
75
+ function quantizeRows(X, rows, cols) {
76
+ const q = new Int8Array(rows * cols), s = new Float32Array(rows);
77
+ for (let r = 0; r < rows; r++) {
78
+ let mx = 0;
79
+ for (let c = 0; c < cols; c++) { const a = Math.abs(X[r * cols + c]); if (a > mx) mx = a; }
80
+ const sc = Math.max(mx / 127, 1e-8); s[r] = sc;
81
+ for (let c = 0; c < cols; c++) {
82
+ const v = Math.round(X[r * cols + c] / sc);
83
+ q[r * cols + c] = v < -128 ? -128 : v > 127 ? 127 : v;
84
+ }
85
+ }
86
+ return { q, s };
87
+ }
88
+ function quantizeCols(W, rows, cols) {
89
+ const q = new Int8Array(rows * cols), s = new Float32Array(cols);
90
+ for (let c = 0; c < cols; c++) {
91
+ let mx = 0;
92
+ for (let r = 0; r < rows; r++) { const a = Math.abs(W[r * cols + c]); if (a > mx) mx = a; }
93
+ s[c] = Math.max(mx / 127, 1e-8);
94
+ }
95
+ for (let r = 0; r < rows; r++)
96
+ for (let c = 0; c < cols; c++) {
97
+ const v = Math.round(W[r * cols + c] / s[c]);
98
+ q[r * cols + c] = v < -128 ? -128 : v > 127 ? 127 : v;
99
+ }
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;
112
+ for (let i = 0; i < m; i++) {
113
+ acc.fill(0);
114
+ const xrow = xo + i * k;
115
+ for (let p = 0; p < k; p++) {
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
+ }
125
+ }
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;
137
+ if (batch === 1) {
138
+ const w = quantizeCols(Wf, k, n); wq = w.q; ws = w.s;
139
+ } else {
140
+ wq = new Int8Array(batch * k * n); ws = new Float32Array(batch * n);
141
+ for (let bz = 0; bz < batch; bz++) {
142
+ const w = quantizeCols(Wf.subarray(bz * k * n, (bz + 1) * k * n), k, n);
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
+
150
  // one verified layer forward; returns float out (+ cache for STE backward).
151
  // Every product goes through the verified INT8 multiply (mul8 LUT) with exact
152
  // int32 accumulation — i.e. an emulated INT8 tensor-core GEMM — then dequant.
 
197
  for (let j = 0; j < W2.length; j++) W2[j] -= lr * gAvg[W1.length + j];
198
  }
199
 
200
+ const api = { quantize, quantize2, quantizeRows, quantizeCols, lutMatmulJS, lutMatmul3JS, lutMatmul3,
201
+ bgemmJS, vgemmBlock, linearFwd, forward, backward, splitApply };
202
  if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); module.exports = api; }
203
  else { TC = root.TrainCore; root.Verified = api; }
204
  })(typeof self !== "undefined" ? self : this);
web/public/webgpu.js CHANGED
@@ -54,6 +54,60 @@
54
  C[row * n + col] = s;
55
  }`;
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  async function loadLUTs(base) {
58
  base = base || "";
59
  const [mulB, reqB, reluB, meta] = await Promise.all([
@@ -98,8 +152,11 @@
98
  const lut32 = new Int32Array(L.mul); // widen int16 -> int32
99
  const lutBuf = device.createBuffer({ size: lut32.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
100
  device.queue.writeBuffer(lutBuf, 0, lut32);
 
 
101
  const viaLUT = { backend: "webgpu", label: `${gpuName} (LUT shader)`,
102
- matmulInt8: (Xq, Wq, m, k, n) => gpuMatmulLUT(device, lutPipe, lutBuf, Xq, Wq, m, k, n) };
 
103
 
104
  // DP4A pipeline — only if the WGSL feature exists AND it reproduces the
105
  // verified units exactly on random self-tests
@@ -118,7 +175,29 @@
118
  for (let i = 0; i < ref.length; i++)
119
  if (hw[i] !== ref[i]) { console.warn("DP4A disagreed with the verified units — using LUT shader"); return viaLUT; }
120
  }
121
- return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot — HW verified vs units)`, matmulInt8: dp4mm };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  } catch (e) { console.warn("WebGPU init failed, CPU fallback:", e); return cpu; }
123
  }
124
 
@@ -168,6 +247,62 @@
168
  return r.out;
169
  }
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  function mk(device, size, usage) { return device.createBuffer({ size, usage }); }
172
  function up(device, arr, usage) {
173
  const b = mk(device, Math.max(16, arr.byteLength), usage);
 
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>) {
71
+ let m = dims.x; let k = dims.y; let n = dims.z;
72
+ let row = gid.x; let col = gid.y; let bz = gid.z;
73
+ if (row >= m || col >= n) { return; }
74
+ var s : i32 = 0;
75
+ let xo = (bz * m + row) * k;
76
+ let wo = bz * k * n + col;
77
+ for (var p = 0u; p < k; p = p + 1u) {
78
+ let au = u32(Xq[xo + p] & 255);
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>) {
97
+ let m = dims.x; let kw = dims.y; let n = dims.z;
98
+ let row = gid.x; let col = gid.y; let bz = gid.z;
99
+ if (row >= m || col >= n) { return; }
100
+ var s : i32 = 0;
101
+ let xo = (bz * m + row) * kw;
102
+ let wo = (bz * n + col) * kw;
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
  async function loadLUTs(base) {
112
  base = base || "";
113
  const [mulB, reqB, reluB, meta] = await Promise.all([
 
152
  const lut32 = new Int32Array(L.mul); // widen int16 -> int32
153
  const lutBuf = device.createBuffer({ size: lut32.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
154
  device.queue.writeBuffer(lutBuf, 0, lut32);
155
+ const bgLutModule = device.createShaderModule({ code: WGSL_BG_LUT });
156
+ const bgLutPipe = device.createComputePipeline({ layout: "auto", compute: { module: bgLutModule, entryPoint: "main" } });
157
  const viaLUT = { backend: "webgpu", label: `${gpuName} (LUT shader)`,
158
+ matmulInt8: (Xq, Wq, m, k, n) => gpuMatmulLUT(device, lutPipe, lutBuf, Xq, Wq, m, k, n),
159
+ bgemm: (Xq, Wq, rs, cs, d) => gpuBgemmLUT(device, bgLutPipe, lutBuf, Xq, Wq, rs, cs, d) };
160
 
161
  // DP4A pipeline — only if the WGSL feature exists AND it reproduces the
162
  // verified units exactly on random self-tests
 
175
  for (let i = 0; i < ref.length; i++)
176
  if (hw[i] !== ref[i]) { console.warn("DP4A disagreed with the verified units — using LUT shader"); return viaLUT; }
177
  }
178
+ // fused/batched DP4A kernel, gated by the same verification: it must
179
+ // reproduce the CPU LUT mirror bit-for-bit on the integer accumulate
180
+ const bgDp4Module = device.createShaderModule({ code: WGSL_BG_DP4 });
181
+ const bgDp4Pipe = device.createComputePipeline({ layout: "auto", compute: { module: bgDp4Module, entryPoint: "main" } });
182
+ const bg = (Xq, Wq, rs, cs, d) => gpuBgemmDP4(device, bgDp4Pipe, Xq, Wq, rs, cs, d);
183
+ {
184
+ const d0 = { m: 5, k: 9, n: 6, batch: 3, relu: true };
185
+ const Xq = new Int8Array(d0.batch * d0.m * d0.k), Wq = new Int8Array(d0.batch * d0.k * d0.n);
186
+ for (let i = 0; i < Xq.length; i++) Xq[i] = (Math.random() * 256 - 128) | 0;
187
+ for (let i = 0; i < Wq.length; i++) Wq[i] = (Math.random() * 256 - 128) | 0;
188
+ const rs = new Float32Array(d0.batch * d0.m).fill(0).map(() => Math.random() + 0.5);
189
+ const cs = new Float32Array(d0.batch * d0.n).fill(0).map(() => Math.random() + 0.5);
190
+ const hw = await bg(Xq, Wq, rs, cs, d0);
191
+ const ref = root.Verified.bgemmJS(Xq, Wq, rs, cs, d0, L);
192
+ for (let i = 0; i < ref.length; i++)
193
+ if (Math.abs(hw[i] - ref[i]) > Math.abs(ref[i]) * 1e-6 + 1e-6) {
194
+ console.warn("batched DP4A disagreed with the verified units — using LUT bgemm");
195
+ return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot — HW verified vs units)`,
196
+ matmulInt8: dp4mm, bgemm: viaLUT.bgemm };
197
+ }
198
+ }
199
+ return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot — HW verified vs units)`,
200
+ matmulInt8: dp4mm, bgemm: bg };
201
  } catch (e) { console.warn("WebGPU init failed, CPU fallback:", e); return cpu; }
202
  }
203
 
 
247
  return r.out;
248
  }
249
 
250
+ // fused batched dispatch: f32 out, epilogue done on-device
251
+ async function runBgPass(device, pipeline, entries, m, n, batch) {
252
+ const bytesO = batch * m * n * 4;
253
+ const bufO = mk(device, bytesO, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
254
+ const bind = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: entries(bufO) });
255
+ const enc = device.createCommandEncoder();
256
+ const pass = enc.beginComputePass();
257
+ pass.setPipeline(pipeline); pass.setBindGroup(0, bind);
258
+ pass.dispatchWorkgroups(Math.ceil(m / 8), Math.ceil(n / 8), batch); pass.end();
259
+ const read = mk(device, bytesO, GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ);
260
+ enc.copyBufferToBuffer(bufO, 0, read, 0, bytesO);
261
+ device.queue.submit([enc.finish()]);
262
+ await read.mapAsync(GPUMapMode.READ);
263
+ const out = new Float32Array(read.getMappedRange().slice(0));
264
+ read.unmap();
265
+ return { out, bufO, read };
266
+ }
267
+
268
+ async function gpuBgemmLUT(device, pipeline, lutBuf, Xq, Wq, rs, cs, d) {
269
+ const { m, k, n } = d, batch = d.batch || 1, flags = d.relu ? 1 : 0;
270
+ const bufX = up(device, Int32Array.from(Xq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
271
+ const bufW = up(device, Int32Array.from(Wq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
272
+ const bufR = up(device, rs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
273
+ const bufS = up(device, cs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
274
+ const bufD = up(device, new Uint32Array([m, k, n, flags]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
275
+ const r = await runBgPass(device, pipeline, (bufO) => [
276
+ { binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
277
+ { binding: 2, resource: { buffer: lutBuf } }, { binding: 3, resource: { buffer: bufR } },
278
+ { binding: 4, resource: { buffer: bufS } }, { binding: 5, resource: { buffer: bufO } },
279
+ { binding: 6, resource: { buffer: bufD } } ], m, n, batch);
280
+ [bufX, bufW, bufR, bufS, bufD, r.bufO, r.read].forEach(b => b.destroy());
281
+ return r.out;
282
+ }
283
+
284
+ async function gpuBgemmDP4(device, pipeline, Xq, Wq, rs, cs, d) {
285
+ const { m, k, n } = d, batch = d.batch || 1, flags = d.relu ? 1 : 0;
286
+ const kw = Math.ceil(k / 4);
287
+ const Xp = packRows(Xq, batch * m, k, kw); // rows are (batch·m)
288
+ const Wp = new Uint32Array(batch * n * kw); // per-batch Wᵀ, packed
289
+ for (let bz = 0; bz < batch; bz++) {
290
+ const wt = transposeI8(Wq.subarray(bz * k * n, (bz + 1) * k * n), k, n);
291
+ Wp.set(packRows(wt, n, k, kw), bz * n * kw);
292
+ }
293
+ const bufX = up(device, Xp, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
294
+ const bufW = up(device, Wp, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
295
+ const bufR = up(device, rs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
296
+ const bufS = up(device, cs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
297
+ const bufD = up(device, new Uint32Array([m, kw, n, flags]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
298
+ const r = await runBgPass(device, pipeline, (bufO) => [
299
+ { binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
300
+ { binding: 2, resource: { buffer: bufR } }, { binding: 3, resource: { buffer: bufS } },
301
+ { binding: 4, resource: { buffer: bufO } }, { binding: 5, resource: { buffer: bufD } } ], m, n, batch);
302
+ [bufX, bufW, bufR, bufS, bufD, r.bufO, r.read].forEach(b => b.destroy());
303
+ return r.out;
304
+ }
305
+
306
  function mk(device, size, usage) { return device.createBuffer({ size, usage }); }
307
  function up(device, arr, usage) {
308
  const b = mk(device, Math.max(16, arr.byteLength), usage);