Web demo: lr default 0.01, batched per-head backward GEMMs
Browse files- web/public/index.html +2 -2
- web/public/transformer.js +92 -44
- web/test_unit_backward.js +77 -0
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">
|
| 120 |
-
<input type="range" id="cfgLr" min="5" max="50" step="5" value="
|
| 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 |
-
//
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
return out;
|
| 194 |
}
|
| 195 |
-
function
|
| 196 |
-
for (let
|
| 197 |
-
for (let
|
| 198 |
-
const
|
| 199 |
-
|
|
|
|
| 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,
|
| 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] =
|
| 299 |
-
dlnfIn =
|
| 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`]] =
|
| 308 |
-
const dh1 =
|
| 309 |
for (let i = 0; i < dh1.length; i++) if (!cb.mask[i]) dh1[i] = 0;
|
| 310 |
-
g[gi[`b${l}.W1`]] =
|
| 311 |
-
const dln2in = lnBwd(
|
| 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`]] =
|
| 316 |
-
const dctx =
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
for (let ti = 0; ti < T; ti++) {
|
| 329 |
let dot = 0;
|
| 330 |
-
for (let tj = 0; tj <= ti; tj++) dot +=
|
| 331 |
-
for (let tj = 0; tj <= ti; tj++)
|
|
|
|
| 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 |
-
|
| 340 |
-
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
const dln1in = new Float32Array(BT * C);
|
| 343 |
-
const addIn = (dsrc, W) => { const t2 =
|
| 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×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 |
+
})();
|