DaisyChain-Train / web /test_b2b.js
Quazim0t0's picture
Upload web/test_b2b.js with huggingface_hub
0517008 verified
Raw
History Blame Contribute Delete
8.32 kB
// B2B MLP chain (CUTLASS ex. 13 + 23): tests for the respecced quantize and
// the chained forward.
//
// What must hold EXACTLY:
// - the per-row absmax + scale derivation matches quantizeRows' internals
// bit for bit (ex. 23's reduction changes where max runs, not what it is)
// - the chain is deterministic and structurally equal to its parts
// - gemm1 (hence h1, hence the ReLU mask) is byte-identical to the old path
// What changes BY DESIGN (and must stay bounded):
// - quantizeRowsInv rounds via floor(f32(x*inv)+0.5) instead of
// round(x_f64/scale): every int8 moves by AT MOST one step, only on
// values that sit within float noise of a rounding boundary
// The GPU side of the same claims is enforced at init by gateMlp in webgpu.js
// (exact !== against the mirror chain, ragged shapes, pack-tail padding).
const fs = require("fs");
const path = require("path");
const p = (f) => path.join(__dirname, "public", f);
const V = require("./public/verified_core.js");
const T = require("./public/traincore.js");
const NEW = require("./public/transformer.js");
// The pre-chain transformer is a LOCAL regression baseline (not shipped —
// shipping a frozen copy of dead forward code would be the correctness
// illusion again). When absent, the old-vs-new convergence comparison is
// skipped and every self-contained assertion still runs.
let OLD = null;
try { OLD = require("./public/_transformer_prefusion.js"); } catch (e) {}
const L = { mul: new Int16Array(fs.readFileSync(p("mul_lut.bin")).buffer.slice(0)) };
const rnd = (len) => Float32Array.from({ length: len }, () => Math.random() * 4 - 2);
let pass = true;
const ok = (c, msg) => { console.log(`${c ? " ok " : " FAIL"} ${msg}`); if (!c) pass = false; };
(async () => {
// 1) ex. 23 reduction: absmax + scales identical to quantizeRows' own
{
let bad = 0, n = 0;
for (let t = 0; t < 200; t++) {
const rows = 1 + (Math.random() * 40 | 0), cols = 1 + (Math.random() * 90 | 0);
const X = rnd(rows * cols);
if (Math.random() < 0.1) for (let c = 0; c < cols; c++) X[c] = 0; // zero row -> 1e-8 floor
const sOld = V.quantizeRows(X, rows, cols).s;
const sNew = V.scalesFromAbsMax(V.rowAbsMax(X, rows, cols)).scale;
for (let i = 0; i < rows; i++) { n++; if (sOld[i] !== sNew[i]) bad++; }
}
ok(bad === 0, `scales from the fused absmax are bit-identical to quantizeRows (${n} rows incl. zero rows)`);
}
// 2) the respec moves an int8 by at most ONE step — and must move at least
// one somewhere, or every "bit-identical to the mirror" claim downstream
// is untested. Boundary hits are ~1 in 10^5 values, so scan ADAPTIVELY:
// keep drawing until one is found (a fixed small sample made this test
// flip a coin — it failed 2 runs in 3, which is its own lesson about
// asserting the existence of rare events from bounded randomness).
{
let maxd = 0, diff = 0, n = 0;
while (diff === 0 && n < 3_000_000) {
const rows = 1 + (Math.random() * 30 | 0), cols = 1 + (Math.random() * 70 | 0);
const X = rnd(rows * cols);
const old = V.quantizeRows(X, rows, cols);
const inv = V.scalesFromAbsMax(V.rowAbsMax(X, rows, cols)).inv;
const q2 = V.quantizeRowsInv(X, rows, cols, inv);
for (let i = 0; i < q2.length; i++) {
n++;
const d = Math.abs(q2[i] - old.q[i]);
if (d) diff++;
if (d > maxd) maxd = d;
}
}
ok(maxd <= 1, `respecced quantize differs by at most 1 step (max ${maxd} over ${n} values)`);
ok(diff > 0, `the respec is a real change (${diff} boundary value(s) in ${n} scanned) — the mirror equivalence is not vacuous`);
}
// 2b) fma-contraction immunity: WGSL permits contracting `x*inv + 0.5` into
// a hardware FMA (one rounding, e.g. RDNA2 V_FMA_F32), and no gate can
// forbid the compiler that choice. Prove it cannot matter: fused and
// stepped rounding differ only in the last ulp at binade crossings, and
// the RNE tie parity keeps both on the same side of every integer — so
// floor(), hence the int8, is identical. Assert (a) raw last-ulp
// differences DO occur (the claim is about floor, not vacuous absence
// of anomalies) and (b) not one of them survives floor.
{
const f32 = Math.fround;
let raw = 0, fl = 0, n = 0;
for (let k = 1; k <= 7; k++) { // binade edges 2..128, from below and above
const u = Math.pow(2, k - 23);
for (let t = 0; t < 400000; t++) {
n++;
const iv = f32(0.25 + Math.random() * 500);
const x = f32((Math.pow(2, k) - 0.5 + (Math.random() * 8 - 6) * u) / iv);
const p = x * iv; // f32×f32 is exact in f64 -> true fused input
const two = f32(f32(p) + 0.5), fused = f32(p + 0.5);
if (two !== fused) { raw++; if (Math.floor(two) !== Math.floor(fused)) fl++; }
}
}
ok(raw > 0, `fused-vs-stepped last-ulp differences occur at binade edges (${raw} in ${n} draws) — the immunity claim is not vacuous`);
ok(fl === 0, `none survives floor(): the +0.5 quantize is fma-contraction-immune (0/${raw} floor-visible)`);
}
// 3) chain == its parts, and gemm1 is byte-identical to the un-chained GEMM
{
const d = { m: 17, k: 32, h: 20, n: 9 };
const X = rnd(d.m * d.k), W1 = rnd(d.k * d.h), W2 = rnd(d.h * d.n);
const r = await V.vmlpBlock(X, W1, W2, d, L, null, null);
const x = V.quantizeRows(X, d.m, d.k), w1 = V.quantizeCols(W1, d.k, d.h), w2 = V.quantizeCols(W2, d.h, d.n);
const h1ref = V.bgemmJS(x.q, w1.q, x.s, w1.s, { m: d.m, k: d.k, n: d.h, batch: 1, relu: true }, L);
let same = true;
for (let i = 0; i < h1ref.length; i++) if (r.h1[i] !== h1ref[i]) { same = false; break; }
ok(same, "chain gemm1 (hence h1 and the ReLU mask) is byte-identical to the un-chained GEMM");
const sc = V.scalesFromAbsMax(V.rowAbsMax(r.h1, d.m, d.h));
const outref = V.bgemmJS(V.quantizeRowsInv(r.h1, d.m, d.h, sc.inv), w2.q, sc.scale, w2.s, { m: d.m, k: d.h, n: d.n, batch: 1 }, L);
let same2 = true;
for (let i = 0; i < outref.length; i++) if (r.out[i] !== outref[i]) { same2 = false; break; }
ok(same2, "chain output equals the manual composition of its stages");
const r2 = await V.vmlpBlock(X, W1, W2, d, L, null, null);
ok(r.out.every((v, i) => v === r2.out[i]), "chain is deterministic (two runs bitwise equal)");
}
// 4) the transformer trains through the chain, and the spec change does not
// hurt convergence: same seeds, old forward vs chained forward
{
const cfg = { c: 32, t: 32, b: 8, layers: 2, heads: 2, steps: 40, lr: 0.01 };
const run = async (X) => {
const m = X.init(cfg, L, null);
const opt = T.makeAdam(m.nParams, { lr: cfg.lr });
let seed = 31337;
const orig = Math.random;
Math.random = () => { seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
let first = 0, last = 0;
for (let s = 0; s < cfg.steps; s++) {
const r = await X.trainStep(m);
if (s === 0) first = r.loss;
last = r.loss;
X.applyUpdate(m, opt.step(r.grad));
}
Math.random = orig;
return { first, last };
};
const b = await run(NEW);
ok(b.last < b.first * 0.75, `chained transformer converges (${b.first.toFixed(4)} -> ${b.last.toFixed(4)})`);
if (OLD) {
const a = await run(OLD);
ok(a.first === b.first, `step-0 loss identical before any quantize divergence compounds (${a.first.toFixed(6)})`);
const rel = Math.abs(a.last - b.last) / a.last;
ok(rel < 0.10, `convergence unchanged by the respec: old ${a.last.toFixed(4)} vs new ${b.last.toFixed(4)} (${(100 * rel).toFixed(1)}% apart)`);
console.log(" note the runs are NOT bit-identical past step 0 — the quantize respec is a real (bounded) spec change, which is why old and new builds must not co-train");
} else {
console.log(" note pre-chain baseline (_transformer_prefusion.js) not present — old-vs-new convergence comparison skipped (recorded result: old 2.0500 vs new 2.0512, 0.1% apart)");
}
}
console.log(pass ? "\nB2B MLP CHAIN TEST PASSED" : "\nB2B MLP CHAIN TEST FAILED");
process.exit(pass ? 0 : 1);
})();