Quazim0t0 commited on
Commit
3dbd21f
·
verified ·
1 Parent(s): cad0868

Web demo: lr default 0.01, batched per-head backward GEMMs

Browse files
web/public/index.html CHANGED
@@ -116,8 +116,8 @@
116
  <input type="range" id="cfgB" min="2" max="32" step="2" value="8">
117
  <label class="slbl">Steps <span class="sval" id="vcfgSteps">300</span></label>
118
  <input type="range" id="cfgSteps" min="100" max="10000" step="100" value="300">
119
- <label class="slbl">Learning rate ×1000 <span class="sval" id="vcfgLr">20</span></label>
120
- <input type="range" id="cfgLr" min="5" max="50" step="5" value="20">
121
  <label class="slbl" for="cfgData">Dataset (HuggingFace)</label>
122
  <input type="text" id="cfgData" value="HuggingFaceFW/fineweb-edu" spellcheck="false"
123
  style="width:100%;padding:9px 11px;border-radius:8px;border:1px solid var(--card-border);background:transparent;color:inherit;font-family:'Courier New',monospace;font-size:.9rem">
 
116
  <input type="range" id="cfgB" min="2" max="32" step="2" value="8">
117
  <label class="slbl">Steps <span class="sval" id="vcfgSteps">300</span></label>
118
  <input type="range" id="cfgSteps" min="100" max="10000" step="100" value="300">
119
+ <label class="slbl">Learning rate ×1000 <span class="sval" id="vcfgLr">10</span></label>
120
+ <input type="range" id="cfgLr" min="5" max="50" step="5" value="10">
121
  <label class="slbl" for="cfgData">Dataset (HuggingFace)</label>
122
  <input type="text" id="cfgData" value="HuggingFaceFW/fineweb-edu" spellcheck="false"
123
  style="width:100%;padding:9px 11px;border-radius:8px;border:1px solid var(--card-border);background:transparent;color:inherit;font-family:'Courier New',monospace;font-size:.9rem">
web/public/transformer.js CHANGED
@@ -157,7 +157,7 @@
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,
@@ -185,20 +185,38 @@
185
  return { X, Y };
186
  }
187
 
188
- // per-head slice helpers (q: BT×C row-major; head h occupies cols [h*hd, (h+1)*hd))
189
- function headSlice(q, bIdx, h, T, C, hd) {
190
- const out = new Float32Array(T * hd);
191
- for (let ti = 0; ti < T; ti++)
192
- for (let j = 0; j < hd; j++) out[ti * hd + j] = q[(bIdx * T + ti) * C + h * hd + j];
 
 
 
 
 
 
 
 
193
  return out;
194
  }
195
- function headUnslice(dst, src, bIdx, h, T, C, hd, accumulate) {
196
- for (let ti = 0; ti < T; ti++)
197
- for (let j = 0; j < hd; j++) {
198
- const di = (bIdx * T + ti) * C + h * hd + j;
199
- dst[di] = (accumulate ? dst[di] : 0) + src[ti * hd + j];
 
200
  }
201
  }
 
 
 
 
 
 
 
 
 
202
 
203
  // ---- forward THROUGH the verified units (caches kept for STE backward) -----
204
  async function forward(m, X, Y) {
@@ -283,20 +301,42 @@
283
  // available (CUTLASS ex. 06) — same float math, off the JS thread.
284
  async function backward(m, cache) {
285
  const { c: C, t: T, b: B, layers, heads, hidden, vocab } = m.cfg;
286
- const BT = B * T, hd = C / heads, mm = TC.matmul, tr = TC.transpose;
287
  const g = m.params.map(p => new Float32Array(p.length));
288
  const gi = Object.fromEntries(m.names.map((n, i) => [n, i]));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  // tied unembed: logits = lnf @ embᵀ, so the unembedding gradient flows
290
  // straight into emb — dlogitsᵀ @ lnf is V×C, emb's own shape
291
  let dlnfIn;
292
- if (m.ctx.fgemm) {
293
  [g[gi.emb], dlnfIn] = await Promise.all([
294
  m.ctx.fgemm(cache.dlogits, cache.lnf.y, { m: vocab, k: BT, n: C, transA: true }),
295
  m.ctx.fgemm(cache.dlogits, m.emb, { m: BT, k: vocab, n: C }), // split-K shape
296
  ]);
297
  } else {
298
- g[gi.emb] = mm(tr(cache.dlogits, BT, vocab), cache.lnf.y, vocab, BT, C);
299
- dlnfIn = mm(cache.dlogits, m.emb, BT, vocab, C);
300
  }
301
  let dx = lnBwd(dlnfIn, cache.lnf.y, cache.lnf.sig, BT, C);
302
  const scale = 1 / Math.sqrt(hd);
@@ -304,44 +344,52 @@
304
  const bl = m.blocks[l], cb = cache.blocks[l];
305
  // mlp: x3 = x2 + relu(ln2 @ W1) @ W2
306
  const dmlpOut = dx; // residual passthrough handled below
307
- g[gi[`b${l}.W2`]] = mm(tr(cb.h1, BT, hidden), dmlpOut, hidden, BT, C);
308
- const dh1 = mm(dmlpOut, tr(bl.W2, hidden, C), BT, C, hidden);
309
  for (let i = 0; i < dh1.length; i++) if (!cb.mask[i]) dh1[i] = 0;
310
- g[gi[`b${l}.W1`]] = mm(tr(cb.ln2.y, BT, C), dh1, C, BT, hidden);
311
- const dln2in = lnBwd(mm(dh1, tr(bl.W1, C, hidden), BT, hidden, C), cb.ln2.y, cb.ln2.sig, BT, C);
312
  const dx2 = new Float32Array(BT * C);
313
  for (let i = 0; i < dx2.length; i++) dx2[i] = dx[i] + dln2in[i];
314
  // attention: x2 = xin + (ctxOut @ Wo)
315
- g[gi[`b${l}.Wo`]] = mm(tr(cb.ctxOut, BT, C), dx2, C, BT, C);
316
- const dctx = mm(dx2, tr(bl.Wo, C, C), BT, C, C);
317
- const dq = new Float32Array(BT * C), dk = new Float32Array(BT * C), dv = new Float32Array(BT * C);
318
- for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
319
- const bz = bi * heads + h;
320
- const a = cb.aAll.subarray(bz * T * T, (bz + 1) * T * T);
321
- const qh = headSlice(cb.q, bi, h, T, C, hd);
322
- const kh = headSlice(cb.k, bi, h, T, C, hd);
323
- const vh = headSlice(cb.v, bi, h, T, C, hd);
324
- const dch = headSlice(dctx, bi, h, T, C, hd); // T×hd
325
- const dvh = mm(tr(a, T, T), dch, T, T, hd); // aᵀ @ dctx
326
- const da = mm(dch, tr(vh, T, hd), T, hd, T); // dctx @ vᵀ
327
- const ds = new Float32Array(T * T); // softmax bwd (causal)
 
 
 
 
 
328
  for (let ti = 0; ti < T; ti++) {
329
  let dot = 0;
330
- for (let tj = 0; tj <= ti; tj++) dot += da[ti * T + tj] * a[ti * T + tj];
331
- for (let tj = 0; tj <= ti; tj++) ds[ti * T + tj] = a[ti * T + tj] * (da[ti * T + tj] - dot) * scale;
 
332
  }
333
- const dqh = mm(ds, kh, T, T, hd); // ds @ k
334
- const dkh = mm(tr(ds, T, T), qh, T, T, hd); // dsᵀ @ q
335
- headUnslice(dq, dqh, bi, h, T, C, hd, true);
336
- headUnslice(dk, dkh, bi, h, T, C, hd, true);
337
- headUnslice(dv, dvh, bi, h, T, C, hd, true);
338
  }
339
- g[gi[`b${l}.Wq`]] = mm(tr(cb.ln1.y, BT, C), dq, C, BT, C);
340
- g[gi[`b${l}.Wk`]] = mm(tr(cb.ln1.y, BT, C), dk, C, BT, C);
341
- g[gi[`b${l}.Wv`]] = mm(tr(cb.ln1.y, BT, C), dv, C, BT, C);
 
 
 
 
 
 
 
342
  const dln1in = new Float32Array(BT * C);
343
- const addIn = (dsrc, W) => { const t2 = mm(dsrc, tr(W, C, C), BT, C, C); for (let i = 0; i < dln1in.length; i++) dln1in[i] += t2[i]; };
344
- addIn(dq, bl.Wq); addIn(dk, bl.Wk); addIn(dv, bl.Wv);
345
  const dxin = lnBwd(dln1in, cb.ln1.y, cb.ln1.sig, BT, C);
346
  dx = new Float32Array(BT * C);
347
  for (let i = 0; i < dx.length; i++) dx[i] = dx2[i] + dxin[i];
 
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, unitBackward: !!cfg.unitBackward },
161
  emb: add("emb", mk(vocabSize() * c, 0.08)),
162
  pos: add("pos", mk(cfg.t * c, 0.02)),
163
  blocks: [], params, names,
 
185
  return { X, Y };
186
  }
187
 
188
+ // ---- head layout helpers ---------------------------------------------------
189
+ // q/k/v live as BT×C with head h owning columns [h*hd, (h+1)*hd). The backward
190
+ // wants every head as its own GEMM problem, so gather once into BH×T×hd and
191
+ // scatter back at the end one pass each, instead of slicing per head inside
192
+ // the loop and paying a GPU dispatch per tiny matmul.
193
+ function gatherHeads(x, B, T, C, heads, hd) { // BT×C -> BH×T×hd
194
+ const out = new Float32Array(B * heads * T * hd);
195
+ for (let bi = 0; bi < B; bi++)
196
+ for (let h = 0; h < heads; h++) {
197
+ const bz = bi * heads + h;
198
+ for (let ti = 0; ti < T; ti++)
199
+ for (let j = 0; j < hd; j++) out[(bz * T + ti) * hd + j] = x[(bi * T + ti) * C + h * hd + j];
200
+ }
201
  return out;
202
  }
203
+ function scatterHeadsAcc(dst, src, B, T, C, heads, hd) { // BH×T×hd -> += BT×C
204
+ for (let bi = 0; bi < B; bi++)
205
+ for (let h = 0; h < heads; h++) {
206
+ const bz = bi * heads + h;
207
+ for (let ti = 0; ti < T; ti++)
208
+ for (let j = 0; j < hd; j++) dst[(bi * T + ti) * C + h * hd + j] += src[(bz * T + ti) * hd + j];
209
  }
210
  }
211
+ function batchedTranspose(x, batch, rows, cols) { // per-batch rows×cols -> cols×rows
212
+ const out = new Float32Array(batch * rows * cols);
213
+ for (let b = 0; b < batch; b++) {
214
+ const o = b * rows * cols;
215
+ for (let r = 0; r < rows; r++)
216
+ for (let c = 0; c < cols; c++) out[o + c * rows + r] = x[o + r * cols + c];
217
+ }
218
+ return out;
219
+ }
220
 
221
  // ---- forward THROUGH the verified units (caches kept for STE backward) -----
222
  async function forward(m, X, Y) {
 
301
  // available (CUTLASS ex. 06) — same float math, off the JS thread.
302
  async function backward(m, cache) {
303
  const { c: C, t: T, b: B, layers, heads, hidden, vocab } = m.cfg;
304
+ const BT = B * T, hd = C / heads, tr = TC.transpose;
305
  const g = m.params.map(p => new Float32Array(p.length));
306
  const gi = Object.fromEntries(m.names.map((n, i) => [n, i]));
307
+ // Every matmul here goes through `bmm`. With ctx.unitBackward the STE
308
+ // gradient is computed BY the verified units (block-scaled int8, exact int32
309
+ // accumulate) instead of in float. STE is a claim about the math — pretend
310
+ // the quantizer was the identity — not about the datatype that evaluates it,
311
+ // so the two are orthogonal and this stays a correct STE.
312
+ const units = !!m.ctx.unitBackward;
313
+ const bmm = units
314
+ ? (A, Bm, mm_, k, n) => vmm(A, Bm, mm_, k, n, m.ctx)
315
+ : async (A, Bm, mm_, k, n) => TC.matmul(A, Bm, mm_, k, n);
316
+ // batched: all `batch` problems in ONE dispatch (CUTLASS ex. 05/24). The
317
+ // per-head backward is 4 GEMMs x B x heads of tiny matrices; issued one at a
318
+ // time the GPU spends all its time on dispatch overhead rather than math.
319
+ const bmmB = units
320
+ ? (A, Bm, rows, k, n, batch) =>
321
+ V.vgemmBlock(A, Bm, { m: rows, k, n, batch }, m.ctx.L, m.ctx.bgemm, m.ctx.audit)
322
+ : async (A, Bm, rows, k, n, batch) => {
323
+ const out = new Float32Array(batch * rows * n);
324
+ for (let bz = 0; bz < batch; bz++)
325
+ out.set(TC.matmul(A.subarray(bz * rows * k, (bz + 1) * rows * k),
326
+ Bm.subarray(bz * k * n, (bz + 1) * k * n), rows, k, n), bz * rows * n);
327
+ return out;
328
+ };
329
  // tied unembed: logits = lnf @ embᵀ, so the unembedding gradient flows
330
  // straight into emb — dlogitsᵀ @ lnf is V×C, emb's own shape
331
  let dlnfIn;
332
+ if (m.ctx.fgemm && !units) {
333
  [g[gi.emb], dlnfIn] = await Promise.all([
334
  m.ctx.fgemm(cache.dlogits, cache.lnf.y, { m: vocab, k: BT, n: C, transA: true }),
335
  m.ctx.fgemm(cache.dlogits, m.emb, { m: BT, k: vocab, n: C }), // split-K shape
336
  ]);
337
  } else {
338
+ g[gi.emb] = await bmm(tr(cache.dlogits, BT, vocab), cache.lnf.y, vocab, BT, C);
339
+ dlnfIn = await bmm(cache.dlogits, m.emb, BT, vocab, C);
340
  }
341
  let dx = lnBwd(dlnfIn, cache.lnf.y, cache.lnf.sig, BT, C);
342
  const scale = 1 / Math.sqrt(hd);
 
344
  const bl = m.blocks[l], cb = cache.blocks[l];
345
  // mlp: x3 = x2 + relu(ln2 @ W1) @ W2
346
  const dmlpOut = dx; // residual passthrough handled below
347
+ g[gi[`b${l}.W2`]] = await bmm(tr(cb.h1, BT, hidden), dmlpOut, hidden, BT, C);
348
+ const dh1 = await bmm(dmlpOut, tr(bl.W2, hidden, C), BT, C, hidden);
349
  for (let i = 0; i < dh1.length; i++) if (!cb.mask[i]) dh1[i] = 0;
350
+ g[gi[`b${l}.W1`]] = await bmm(tr(cb.ln2.y, BT, C), dh1, C, BT, hidden);
351
+ const dln2in = lnBwd(await bmm(dh1, tr(bl.W1, C, hidden), BT, hidden, C), cb.ln2.y, cb.ln2.sig, BT, C);
352
  const dx2 = new Float32Array(BT * C);
353
  for (let i = 0; i < dx2.length; i++) dx2[i] = dx[i] + dln2in[i];
354
  // attention: x2 = xin + (ctxOut @ Wo)
355
+ g[gi[`b${l}.Wo`]] = await bmm(tr(cb.ctxOut, BT, C), dx2, C, BT, C);
356
+ const dctx = await bmm(dx2, tr(bl.Wo, C, C), BT, C, C);
357
+ // gather every head once, then run each stage as ONE batched GEMM over all
358
+ // B*heads problems: 4 dispatches per layer instead of 4 per head.
359
+ const BH = B * heads;
360
+ const qb = gatherHeads(cb.q, B, T, C, heads, hd);
361
+ const kb = gatherHeads(cb.k, B, T, C, heads, hd);
362
+ const vb = gatherHeads(cb.v, B, T, C, heads, hd);
363
+ const dchb = gatherHeads(dctx, B, T, C, heads, hd);
364
+ const aT = batchedTranspose(cb.aAll, BH, T, T); // BH×T
365
+ const vT = batchedTranspose(vb, BH, T, hd); // BH×hd×T
366
+ const dvAll = await bmmB(aT, dchb, T, T, hd, BH); // aᵀ @ dctx
367
+ const daAll = await bmmB(dchb, vT, T, hd, T, BH); // dctx @ vᵀ
368
+ // softmax backward is elementwise + a causal row reduction: stays in float
369
+ // (no matrix math here, so nothing for the units to do)
370
+ const dsAll = new Float32Array(BH * T * T);
371
+ for (let bz = 0; bz < BH; bz++) {
372
+ const o = bz * T * T;
373
  for (let ti = 0; ti < T; ti++) {
374
  let dot = 0;
375
+ for (let tj = 0; tj <= ti; tj++) dot += daAll[o + ti * T + tj] * cb.aAll[o + ti * T + tj];
376
+ for (let tj = 0; tj <= ti; tj++)
377
+ dsAll[o + ti * T + tj] = cb.aAll[o + ti * T + tj] * (daAll[o + ti * T + tj] - dot) * scale;
378
  }
 
 
 
 
 
379
  }
380
+ const dsT = batchedTranspose(dsAll, BH, T, T);
381
+ const dqAll = await bmmB(dsAll, kb, T, T, hd, BH); // ds @ k
382
+ const dkAll = await bmmB(dsT, qb, T, T, hd, BH); // dsᵀ @ q
383
+ const dq = new Float32Array(BT * C), dk = new Float32Array(BT * C), dv = new Float32Array(BT * C);
384
+ scatterHeadsAcc(dq, dqAll, B, T, C, heads, hd);
385
+ scatterHeadsAcc(dk, dkAll, B, T, C, heads, hd);
386
+ scatterHeadsAcc(dv, dvAll, B, T, C, heads, hd);
387
+ g[gi[`b${l}.Wq`]] = await bmm(tr(cb.ln1.y, BT, C), dq, C, BT, C);
388
+ g[gi[`b${l}.Wk`]] = await bmm(tr(cb.ln1.y, BT, C), dk, C, BT, C);
389
+ g[gi[`b${l}.Wv`]] = await bmm(tr(cb.ln1.y, BT, C), dv, C, BT, C);
390
  const dln1in = new Float32Array(BT * C);
391
+ const addIn = async (dsrc, W) => { const t2 = await bmm(dsrc, tr(W, C, C), BT, C, C); for (let i = 0; i < dln1in.length; i++) dln1in[i] += t2[i]; };
392
+ await addIn(dq, bl.Wq); await addIn(dk, bl.Wk); await addIn(dv, bl.Wv);
393
  const dxin = lnBwd(dln1in, cb.ln1.y, cb.ln1.sig, BT, C);
394
  dx = new Float32Array(BT * C);
395
  for (let i = 0; i < dx.length; i++) dx[i] = dx2[i] + dxin[i];
web/test_unit_backward.js ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Does computing the STE gradient THROUGH the verified units (block-scaled int8)
2
+ // instead of in float damage convergence?
3
+ //
4
+ // Identical seeds, identical data, identical optimizer. The only difference is
5
+ // whether backward()'s matmuls run in f32 or through mul8. Runs on the CPU LUT
6
+ // mirrors, so this measures the arithmetic question and nothing else.
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const T = require("./public/traincore.js");
10
+ const V = require("./public/verified_core.js");
11
+ const X = require("./public/transformer.js");
12
+
13
+ const p = (f) => path.join(__dirname, "public", f);
14
+ const L = { mul: new Int16Array(fs.readFileSync(p("mul_lut.bin")).buffer.slice(0)),
15
+ requant: new Int8Array(fs.readFileSync(p("requant_lut.bin")).buffer.slice(0)),
16
+ relu: new Int8Array(fs.readFileSync(p("relu_lut.bin")).buffer.slice(0)) };
17
+
18
+ // the real Spikewhale tokenizer if asked — 16512 tokens is where gradient
19
+ // dynamic range actually hurts, so the 96-char fallback would flatter the result
20
+ if (process.env.REAL_TOK) {
21
+ X.loadTokenizerData(JSON.parse(fs.readFileSync(p("tokenizer.json"), "utf8")));
22
+ console.log(`tokenizer: ${X.tokenizerName()}`);
23
+ }
24
+ const STEPS = +(process.env.STEPS || 150);
25
+ const base = { c: 32, t: 32, b: 8, layers: 2, heads: 2, steps: STEPS, lr: 0.02 };
26
+
27
+ // deterministic batches so both runs see byte-identical data
28
+ function makeBatches(n, cfg) {
29
+ let seed = 1234;
30
+ const rnd = () => { seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
31
+ const out = [];
32
+ for (let s = 0; s < n; s++) {
33
+ const ids = [];
34
+ for (let i = 0; i < cfg.b; i++) ids.push(Math.floor(rnd() * 1e6));
35
+ out.push(ids);
36
+ }
37
+ return out;
38
+ }
39
+
40
+ async function run(label, unitBackward, dataSeed, lr) {
41
+ const cfg = { ...base, unitBackward, lr: lr || base.lr };
42
+ const m = X.init(cfg, L, null);
43
+ const opt = T.makeAdam(m.nParams, { lr: cfg.lr });
44
+ // pin Math.random so both runs draw identical batch windows
45
+ let seed = dataSeed || 777;
46
+ const orig = Math.random;
47
+ Math.random = () => { seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
48
+ const curve = [];
49
+ const t0 = Date.now();
50
+ for (let s = 0; s < STEPS; s++) {
51
+ const r = await X.trainStep(m);
52
+ X.applyUpdate(m, opt.step(r.grad));
53
+ curve.push(r.loss);
54
+ }
55
+ Math.random = orig;
56
+ const ms = (Date.now() - t0) / STEPS;
57
+ const tail = curve.slice(-10).reduce((a, b) => a + b) / 10;
58
+ return { label, curve, tail, ms, first: curve[0] };
59
+ }
60
+
61
+ (async () => {
62
+ console.log(`\nvocab=${X.vocabSize()} steps=${STEPS} (CPU LUT mirrors, identical seeds)\n`);
63
+ const f = await run("float backward", false);
64
+ const u = await run("unit backward ", true);
65
+ console.log("step float units");
66
+ for (const s of [0, 25, 50, 75, 100, Math.min(125, STEPS - 1), STEPS - 1]) {
67
+ if (s >= STEPS) continue;
68
+ console.log(`${String(s).padStart(4)} ${f.curve[s].toFixed(4).padStart(9)} ${u.curve[s].toFixed(4).padStart(9)}`);
69
+ }
70
+ console.log(`\nfinal (avg last 10) float ${f.tail.toFixed(4)} units ${u.tail.toFixed(4)}`);
71
+ console.log(`ms/step float ${f.ms.toFixed(0)} units ${u.ms.toFixed(0)}`);
72
+ const ratio = u.tail / f.tail;
73
+ console.log(`\nunits/float loss ratio: ${ratio.toFixed(3)} (1.00 = no damage)`);
74
+ const converged = u.tail < u.first * 0.75 && u.tail < Math.log(X.vocabSize());
75
+ console.log(converged ? "unit backward CONVERGES" : "unit backward FAILED TO CONVERGE");
76
+ console.log(ratio < 1.15 ? "damage within 15% — viable" : "damage exceeds 15% — needs work (try 2-pass gradients)");
77
+ })();