Quazim0t0 commited on
Commit
f92cfa9
Β·
verified Β·
1 Parent(s): 00fdb1d

Web demo: split-K backward + gather-fused attention

Browse files
web/public/transformer.js CHANGED
@@ -119,10 +119,6 @@
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) -------------------------------------------------
128
  function lnFwd(x, rows, C) {
@@ -158,7 +154,8 @@
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,
@@ -218,26 +215,16 @@
218
  const k = await vmm(l1.y, bl.Wk, BT, C, C, ctx);
219
  const v = await vmm(l1.y, bl.Wv, BT, C, C, ctx);
220
  cb.q = q; cb.k = k; cb.v = v;
221
- const ctxOut = new Float32Array(BT * C);
222
- cb.att = []; // per (b,h): softmax probs
223
  const scale = 1 / Math.sqrt(hd);
224
- // gather every head into batched contiguous layouts, then run ALL BΓ—H
225
- // score GEMMs in ONE batched dispatch (and all attnΒ·V in another) β€”
226
- // instead of 2Β·BΒ·H tiny sequential matmuls
227
- const BH = B * heads;
228
- const qb = new Float32Array(BH * T * hd), kb = new Float32Array(BH * T * hd);
229
- const vb = new Float32Array(BH * T * hd), ktb = new Float32Array(BH * hd * T);
230
- for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
231
- const bz = bi * heads + h;
232
- for (let ti = 0; ti < T; ti++) for (let j = 0; j < hd; j++) {
233
- const src = (bi * T + ti) * C + h * hd + j;
234
- qb[(bz * T + ti) * hd + j] = q[src];
235
- kb[(bz * T + ti) * hd + j] = k[src];
236
- vb[(bz * T + ti) * hd + j] = v[src];
237
- ktb[(bz * hd + j) * T + ti] = k[src]; // kα΅€ per head, for scores
238
- }
239
- }
240
- const sAll = await vmmBatched(qb, ktb, T, hd, T, BH, ctx); // scores through the units
241
  const aAll = new Float32Array(BH * T * T); // causal softmax
242
  for (let bz = 0; bz < BH; bz++) {
243
  const so = bz * T * T;
@@ -249,16 +236,11 @@
249
  for (let tj = 0; tj <= ti; tj++) aAll[so + ti * T + tj] /= z;
250
  }
251
  }
252
- const cAll = await vmmBatched(aAll, vb, T, T, hd, BH, ctx); // attnΒ·V through the units
253
- for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
254
- const bz = bi * heads + h;
255
- for (let ti = 0; ti < T; ti++) for (let j = 0; j < hd; j++)
256
- ctxOut[(bi * T + ti) * C + h * hd + j] = cAll[(bz * T + ti) * hd + j];
257
- cb.att.push({ a: aAll.subarray(bz * T * T, (bz + 1) * T * T),
258
- qh: qb.subarray(bz * T * hd, (bz + 1) * T * hd),
259
- kh: kb.subarray(bz * T * hd, (bz + 1) * T * hd),
260
- vh: vb.subarray(bz * T * hd, (bz + 1) * T * hd) });
261
- }
262
  cb.ctxOut = ctxOut;
263
  const attnOut = await vmm(ctxOut, bl.Wo, BT, C, C, ctx);
264
  const x2 = new Float32Array(BT * C);
@@ -295,15 +277,26 @@
295
  }
296
 
297
  // ---- STE backward (float), mirrors forward exactly --------------------------
298
- function backward(m, cache) {
 
 
299
  const { c: C, t: T, b: B, layers, heads, hidden, vocab } = m.cfg;
300
  const BT = B * T, hd = C / heads, mm = TC.matmul, tr = TC.transpose;
301
  const g = m.params.map(p => new Float32Array(p.length));
302
  const gi = Object.fromEntries(m.names.map((n, i) => [n, i]));
303
  // tied unembed: logits = lnf @ embα΅€, so the unembedding gradient flows
304
  // straight into emb β€” dlogitsα΅€ @ lnf is VΓ—C, emb's own shape
305
- g[gi.emb] = mm(tr(cache.dlogits, BT, vocab), cache.lnf.y, vocab, BT, C);
306
- let dx = lnBwd(mm(cache.dlogits, m.emb, BT, vocab, C), cache.lnf.y, cache.lnf.sig, BT, C);
 
 
 
 
 
 
 
 
 
307
  const scale = 1 / Math.sqrt(hd);
308
  for (let l = layers - 1; l >= 0; l--) {
309
  const bl = m.blocks[l], cb = cache.blocks[l];
@@ -320,9 +313,12 @@
320
  g[gi[`b${l}.Wo`]] = mm(tr(cb.ctxOut, BT, C), dx2, C, BT, C);
321
  const dctx = mm(dx2, tr(bl.Wo, C, C), BT, C, C);
322
  const dq = new Float32Array(BT * C), dk = new Float32Array(BT * C), dv = new Float32Array(BT * C);
323
- let ai = 0;
324
  for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
325
- const { a, qh, kh, vh } = cb.att[ai++];
 
 
 
 
326
  const dch = headSlice(dctx, bi, h, T, C, hd); // TΓ—hd
327
  const dvh = mm(tr(a, T, T), dch, T, T, hd); // aα΅€ @ dctx
328
  const da = mm(dch, tr(vh, T, hd), T, hd, T); // dctx @ vα΅€
@@ -364,7 +360,7 @@
364
  async function trainStep(m) {
365
  const { X, Y } = sampleBatch(m.cfg);
366
  const { loss, cache } = await forward(m, X, Y);
367
- const grad = backward(m, cache);
368
  return { loss, grad };
369
  }
370
 
 
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) -------------------------------------------------
124
  function lnFwd(x, rows, C) {
 
154
  const add = (name, w) => { params.push(w); names.push(name); return w; };
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,
 
215
  const k = await vmm(l1.y, bl.Wk, BT, C, C, ctx);
216
  const v = await vmm(l1.y, bl.Wv, BT, C, C, ctx);
217
  cb.q = q; cb.k = k; cb.v = v;
 
 
218
  const scale = 1 / Math.sqrt(hd);
219
+ // gather-FUSED attention (CUTLASS ex. 36/52): the kernels read q/k/v in
220
+ // their natural BTΓ—C layout with head-strided indexing and scatter ctx
221
+ // straight back β€” no JS gather copies, no kα΅€ transpose. All BΓ—H heads in
222
+ // one dispatch per stage, every product through the verified units.
223
+ const BH = B * heads, dAtt = { B, T, heads, hd };
224
+ // per-(token,head) row quantization: the (BTΒ·heads)Γ—hd view IS the buffer
225
+ const qq = V.quantizeRows(q, BT * heads, hd), kq = V.quantizeRows(k, BT * heads, hd);
226
+ const sAll = ctx.att ? await ctx.att.scores(qq.q, kq.q, qq.s, kq.s, dAtt)
227
+ : V.attScoresJS(qq.q, kq.q, qq.s, kq.s, dAtt, ctx.L);
 
 
 
 
 
 
 
 
228
  const aAll = new Float32Array(BH * T * T); // causal softmax
229
  for (let bz = 0; bz < BH; bz++) {
230
  const so = bz * T * T;
 
236
  for (let tj = 0; tj <= ti; tj++) aAll[so + ti * T + tj] /= z;
237
  }
238
  }
239
+ const aq = V.quantizeRows(aAll, BH * T, T);
240
+ const vq = V.quantizeHeadCols(v, B, T, heads, hd);
241
+ const ctxOut = ctx.att ? await ctx.att.ctx(aq.q, vq.q, aq.s, vq.s, dAtt)
242
+ : V.attCtxJS(aq.q, vq.q, aq.s, vq.s, dAtt, ctx.L);
243
+ cb.aAll = aAll; // backward slices heads from q/k/v/aAll
 
 
 
 
 
244
  cb.ctxOut = ctxOut;
245
  const attnOut = await vmm(ctxOut, bl.Wo, BT, C, C, ctx);
246
  const x2 = new Float32Array(BT * C);
 
277
  }
278
 
279
  // ---- STE backward (float), mirrors forward exactly --------------------------
280
+ // The two vocab-sized matmuls run on the split-K f32 GPU kernel when
281
+ // available (CUTLASS ex. 06) β€” same float math, off the JS thread.
282
+ async function backward(m, cache) {
283
  const { c: C, t: T, b: B, layers, heads, hidden, vocab } = m.cfg;
284
  const BT = B * T, hd = C / heads, mm = TC.matmul, tr = TC.transpose;
285
  const g = m.params.map(p => new Float32Array(p.length));
286
  const gi = Object.fromEntries(m.names.map((n, i) => [n, i]));
287
  // tied unembed: logits = lnf @ embα΅€, so the unembedding gradient flows
288
  // straight into emb β€” dlogitsα΅€ @ lnf is VΓ—C, emb's own shape
289
+ let dlnfIn;
290
+ if (m.ctx.fgemm) {
291
+ [g[gi.emb], dlnfIn] = await Promise.all([
292
+ m.ctx.fgemm(cache.dlogits, cache.lnf.y, { m: vocab, k: BT, n: C, transA: true }),
293
+ m.ctx.fgemm(cache.dlogits, m.emb, { m: BT, k: vocab, n: C }), // split-K shape
294
+ ]);
295
+ } else {
296
+ g[gi.emb] = mm(tr(cache.dlogits, BT, vocab), cache.lnf.y, vocab, BT, C);
297
+ dlnfIn = mm(cache.dlogits, m.emb, BT, vocab, C);
298
+ }
299
+ let dx = lnBwd(dlnfIn, cache.lnf.y, cache.lnf.sig, BT, C);
300
  const scale = 1 / Math.sqrt(hd);
301
  for (let l = layers - 1; l >= 0; l--) {
302
  const bl = m.blocks[l], cb = cache.blocks[l];
 
313
  g[gi[`b${l}.Wo`]] = mm(tr(cb.ctxOut, BT, C), dx2, C, BT, C);
314
  const dctx = mm(dx2, tr(bl.Wo, C, C), BT, C, C);
315
  const dq = new Float32Array(BT * C), dk = new Float32Array(BT * C), dv = new Float32Array(BT * C);
 
316
  for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
317
+ const bz = bi * heads + h;
318
+ const a = cb.aAll.subarray(bz * T * T, (bz + 1) * T * T);
319
+ const qh = headSlice(cb.q, bi, h, T, C, hd);
320
+ const kh = headSlice(cb.k, bi, h, T, C, hd);
321
+ const vh = headSlice(cb.v, bi, h, T, C, hd);
322
  const dch = headSlice(dctx, bi, h, T, C, hd); // TΓ—hd
323
  const dvh = mm(tr(a, T, T), dch, T, T, hd); // aα΅€ @ dctx
324
  const da = mm(dch, tr(vh, T, hd), T, hd, T); // dctx @ vα΅€
 
360
  async function trainStep(m) {
361
  const { X, Y } = sampleBatch(m.cfg);
362
  const { loss, cache } = await forward(m, X, Y);
363
+ const grad = await backward(m, cache);
364
  return { loss, grad };
365
  }
366
 
web/public/verified_core.js CHANGED
@@ -147,6 +147,71 @@
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,8 +262,8 @@
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);
 
147
  return bgemmJS(x.q, wq, x.s, ws, d, L);
148
  }
149
 
150
+ // ---- gather-fused attention through the units (CUTLASS ex. 36/52) ----------
151
+ // The kernels read q/k/v/ctx directly in their natural BTΓ—C layout with
152
+ // head-strided indexing β€” no JS gather copies, no kα΅€ transpose, and the
153
+ // context write scatters straight back into BTΓ—C. Quantization stays
154
+ // block-scaled: q/k/a per (token,head) row, v per (head,channel) column.
155
+ // The (BTΒ·heads)Γ—hd row view of q/k IS the contiguous buffer, so
156
+ // quantizeRows(q, BTΒ·heads, hd) gives per-(token,head) scales for free.
157
+ function quantizeHeadCols(v, B, T, heads, hd) { // per (batch,head,channel) column
158
+ const C = heads * hd;
159
+ const q = new Int8Array(B * T * C), s = new Float32Array(B * heads * hd);
160
+ for (let bi = 0; bi < B; bi++)
161
+ for (let h = 0; h < heads; h++)
162
+ for (let j = 0; j < hd; j++) {
163
+ let mx = 0;
164
+ for (let ti = 0; ti < T; ti++) {
165
+ const a = Math.abs(v[(bi * T + ti) * C + h * hd + j]);
166
+ if (a > mx) mx = a;
167
+ }
168
+ const sc = Math.max(mx / 127, 1e-8);
169
+ s[(bi * heads + h) * hd + j] = sc;
170
+ for (let ti = 0; ti < T; ti++) {
171
+ const idx = (bi * T + ti) * C + h * hd + j;
172
+ const w = Math.round(v[idx] / sc);
173
+ q[idx] = w < -128 ? -128 : w > 127 ? 127 : w;
174
+ }
175
+ }
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++) {
185
+ const qo = (bi * T + ti) * C + h * hd, rscale = qs[(bi * T + ti) * heads + h];
186
+ for (let tj = 0; tj < T; tj++) {
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
+ }
194
+ return out;
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++) {
203
+ const ao = (bz * T + ti) * T, rscale = as[bz * T + ti];
204
+ for (let j = 0; j < hd; j++) {
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
+ }
212
+ return out;
213
+ }
214
+
215
  // one verified layer forward; returns float out (+ cache for STE backward).
216
  // Every product goes through the verified INT8 multiply (mul8 LUT) with exact
217
  // int32 accumulation β€” i.e. an emulated INT8 tensor-core GEMM β€” then dequant.
 
262
  for (let j = 0; j < W2.length; j++) W2[j] -= lr * gAvg[W1.length + j];
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);
web/public/webgpu.js CHANGED
@@ -108,6 +108,91 @@
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([
@@ -154,9 +239,55 @@
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
@@ -193,11 +324,11 @@
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,6 +378,69 @@
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;
 
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>) {
124
+ let T = dims.x; let heads = dims.y; let hd = dims.z;
125
+ let ti = gid.x; let tj = gid.y; let bz = gid.z;
126
+ if (ti >= T || tj >= T) { return; }
127
+ let bi = bz / heads; let h = bz % heads;
128
+ let C = heads * hd;
129
+ let qo = (bi * T + ti) * C + h * hd;
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>) {
144
+ let T = dims.x; let heads = dims.y; let hd = dims.z;
145
+ let ti = gid.x; let j = gid.y; let bz = gid.z;
146
+ if (ti >= T || j >= hd) { return; }
147
+ let bi = bz / heads; let h = bz % heads;
148
+ let C = heads * hd;
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
156
+ // was always float (the integer path has no gradient); this just moves that
157
+ // exact float math off the JS thread. Split-K matters for dlnf: M=256, N=32,
158
+ // K=16512 β€” 8k outputs with a huge inner loop would idle the GPU, so slices
159
+ // of K run on separate workgroups and a second tiny pass reduces partials.
160
+ const WGSL_FGEMM = `
161
+ @group(0) @binding(0) var<storage, read> A : array<f32>;
162
+ @group(0) @binding(1) var<storage, read> Bm : array<f32>;
163
+ @group(0) @binding(2) var<storage, read_write> P : array<f32>; // S partials
164
+ @group(0) @binding(3) var<uniform> dims : vec4<u32>; // m, k, n, flags(bit0=transA, rest=S)
165
+ @compute @workgroup_size(8, 8, 1)
166
+ fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
167
+ let m = dims.x; let k = dims.y; let n = dims.z;
168
+ let transA = (dims.w & 1u) == 1u;
169
+ let S = dims.w >> 1u;
170
+ let row = gid.x; let col = gid.y; let z = gid.z;
171
+ if (row >= m || col >= n) { return; }
172
+ let ks = (k + S - 1u) / S;
173
+ let p0 = z * ks;
174
+ let p1 = min(k, p0 + ks);
175
+ var s : f32 = 0.0;
176
+ for (var p = p0; p < p1; p = p + 1u) {
177
+ let a = select(A[row * k + p], A[p * m + row], transA);
178
+ s = s + a * Bm[p * n + col];
179
+ }
180
+ P[(z * m + row) * n + col] = s;
181
+ }`;
182
+ const WGSL_FREDUCE = `
183
+ @group(0) @binding(0) var<storage, read> P : array<f32>;
184
+ @group(0) @binding(1) var<storage, read_write> O : array<f32>;
185
+ @group(0) @binding(2) var<uniform> dims : vec4<u32>; // mn, S, _, _
186
+ @compute @workgroup_size(64)
187
+ fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
188
+ let mn = dims.x; let S = dims.y;
189
+ let i = gid.x;
190
+ if (i >= mn) { return; }
191
+ var s : f32 = 0.0;
192
+ for (var z = 0u; z < S; z = z + 1u) { s = s + P[z * mn + i]; }
193
+ O[i] = s;
194
+ }`;
195
+
196
  async function loadLUTs(base) {
197
  base = base || "";
198
  const [mulB, reqB, reluB, meta] = await Promise.all([
 
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;
269
+ }
270
+
271
+ // split-K f32 GEMM for the STE backward (self-tested vs JS float matmul)
272
+ const fPipes = { gemm: mkPipe(WGSL_FGEMM), reduce: mkPipe(WGSL_FREDUCE) };
273
+ let fgemm = (A, Bm, d) => gpuFgemm(device, fPipes, A, Bm, d);
274
+ try {
275
+ const m0 = 7, k0 = 4500, n0 = 5; // k big enough to exercise split-K
276
+ const A = Float32Array.from({ length: m0 * k0 }, () => Math.random() - 0.5);
277
+ const Bm = Float32Array.from({ length: k0 * n0 }, () => Math.random() - 0.5);
278
+ const hw = await fgemm(A, Bm, { m: m0, k: k0, n: n0 });
279
+ const ref = root.TrainCore.matmul(A, Bm, m0, k0, n0);
280
+ for (let i = 0; i < ref.length; i++)
281
+ if (Math.abs(hw[i] - ref[i]) > Math.abs(ref[i]) * 1e-3 + 1e-3) throw new Error("fgemm mismatch");
282
+ } catch (e) {
283
+ console.warn("split-K f32 GEMM failed verification β€” backward stays in JS:", e.message);
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
 
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
 
 
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;
384
+ const bufQ = up(device, Int32Array.from(qq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
385
+ const bufK = up(device, Int32Array.from(kq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
386
+ const bufQs = up(device, qs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
387
+ const bufKs = up(device, ks, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
388
+ const bufD = up(device, new Uint32Array([d.T, d.heads, d.hd, 0]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
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
+ }
396
+ async function gpuAttCtx(device, pipeline, aq, vq, as, vs, d) {
397
+ const { B, T, heads, hd } = d;
398
+ const bufA = up(device, Int32Array.from(aq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
399
+ const bufV = up(device, Int32Array.from(vq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
400
+ const bufAs = up(device, as, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
401
+ const bufVs = up(device, vs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
402
+ const bufD = up(device, new Uint32Array([T, heads, hd, 0]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
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
+ }
410
+
411
+ // split-K f32 GEMM (backward): partial pass + reduce pass
412
+ async function gpuFgemm(device, pipes, A, Bm, d) {
413
+ const { m, k, n } = d, transA = d.transA ? 1 : 0;
414
+ const S = k > 4096 ? Math.min(16, Math.ceil(k / 2048)) : 1;
415
+ const bufA = up(device, A, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
416
+ const bufB = up(device, Bm, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
417
+ const bufP = mk(device, S * m * n * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
418
+ const bufD1 = up(device, new Uint32Array([m, k, n, transA | (S << 1)]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
419
+ const bufO = mk(device, m * n * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
420
+ const bufD2 = up(device, new Uint32Array([m * n, S, 0, 0]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
421
+ const enc = device.createCommandEncoder();
422
+ let pass = enc.beginComputePass();
423
+ pass.setPipeline(pipes.gemm);
424
+ pass.setBindGroup(0, device.createBindGroup({ layout: pipes.gemm.getBindGroupLayout(0), entries: [
425
+ { binding: 0, resource: { buffer: bufA } }, { binding: 1, resource: { buffer: bufB } },
426
+ { binding: 2, resource: { buffer: bufP } }, { binding: 3, resource: { buffer: bufD1 } } ] }));
427
+ pass.dispatchWorkgroups(Math.ceil(m / 8), Math.ceil(n / 8), S); pass.end();
428
+ pass = enc.beginComputePass();
429
+ pass.setPipeline(pipes.reduce);
430
+ pass.setBindGroup(0, device.createBindGroup({ layout: pipes.reduce.getBindGroupLayout(0), entries: [
431
+ { binding: 0, resource: { buffer: bufP } }, { binding: 1, resource: { buffer: bufO } },
432
+ { binding: 2, resource: { buffer: bufD2 } } ] }));
433
+ pass.dispatchWorkgroups(Math.ceil(m * n / 64)); pass.end();
434
+ const read = mk(device, m * n * 4, GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ);
435
+ enc.copyBufferToBuffer(bufO, 0, read, 0, m * n * 4);
436
+ device.queue.submit([enc.finish()]);
437
+ await read.mapAsync(GPUMapMode.READ);
438
+ const out = new Float32Array(read.getMappedRange().slice(0));
439
+ read.unmap();
440
+ [bufA, bufB, bufP, bufD1, bufO, bufD2, read].forEach(b => b.destroy());
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;