Download web/test_b2b.js from DaisyChainAI/DaisyChain-Train: direct link, hf CLI and curl.
- Browser
- Download file 8.32 kB
-
https://huggingface.co/DaisyChainAI/DaisyChain-Train/resolve/1a34a534bbf5fecd0e6fa7bc530decea45e50d04/web/test_b2b.js
- Command line
-
hf download hf://DaisyChainAI/DaisyChain-Train@1a34a534bbf5fecd0e6fa7bc530decea45e50d04/web/test_b2b.js
-
curl -L -o test_b2b.js https://huggingface.co/DaisyChainAI/DaisyChain-Train/resolve/1a34a534bbf5fecd0e6fa7bc530decea45e50d04/web/test_b2b.js
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); | |
| })(); | |