File size: 3,621 Bytes
3dbd21f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Does computing the STE gradient THROUGH the verified units (block-scaled int8)
// instead of in float damage convergence?
//
// Identical seeds, identical data, identical optimizer. The only difference is
// whether backward()'s matmuls run in f32 or through mul8. Runs on the CPU LUT
// mirrors, so this measures the arithmetic question and nothing else.
const fs = require("fs");
const path = require("path");
const T = require("./public/traincore.js");
const V = require("./public/verified_core.js");
const X = require("./public/transformer.js");

const p = (f) => path.join(__dirname, "public", f);
const L = { mul: new Int16Array(fs.readFileSync(p("mul_lut.bin")).buffer.slice(0)),
            requant: new Int8Array(fs.readFileSync(p("requant_lut.bin")).buffer.slice(0)),
            relu: new Int8Array(fs.readFileSync(p("relu_lut.bin")).buffer.slice(0)) };

// the real Spikewhale tokenizer if asked — 16512 tokens is where gradient
// dynamic range actually hurts, so the 96-char fallback would flatter the result
if (process.env.REAL_TOK) {
  X.loadTokenizerData(JSON.parse(fs.readFileSync(p("tokenizer.json"), "utf8")));
  console.log(`tokenizer: ${X.tokenizerName()}`);
}
const STEPS = +(process.env.STEPS || 150);
const base = { c: 32, t: 32, b: 8, layers: 2, heads: 2, steps: STEPS, lr: 0.02 };

// deterministic batches so both runs see byte-identical data
function makeBatches(n, cfg) {
  let seed = 1234;
  const rnd = () => { seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
  const out = [];
  for (let s = 0; s < n; s++) {
    const ids = [];
    for (let i = 0; i < cfg.b; i++) ids.push(Math.floor(rnd() * 1e6));
    out.push(ids);
  }
  return out;
}

async function run(label, unitBackward, dataSeed, lr) {
  const cfg = { ...base, unitBackward, lr: lr || base.lr };
  const m = X.init(cfg, L, null);
  const opt = T.makeAdam(m.nParams, { lr: cfg.lr });
  // pin Math.random so both runs draw identical batch windows
  let seed = dataSeed || 777;
  const orig = Math.random;
  Math.random = () => { seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
  const curve = [];
  const t0 = Date.now();
  for (let s = 0; s < STEPS; s++) {
    const r = await X.trainStep(m);
    X.applyUpdate(m, opt.step(r.grad));
    curve.push(r.loss);
  }
  Math.random = orig;
  const ms = (Date.now() - t0) / STEPS;
  const tail = curve.slice(-10).reduce((a, b) => a + b) / 10;
  return { label, curve, tail, ms, first: curve[0] };
}

(async () => {
  console.log(`\nvocab=${X.vocabSize()}  steps=${STEPS}  (CPU LUT mirrors, identical seeds)\n`);
  const f = await run("float backward", false);
  const u = await run("unit backward ", true);
  console.log("step        float      units");
  for (const s of [0, 25, 50, 75, 100, Math.min(125, STEPS - 1), STEPS - 1]) {
    if (s >= STEPS) continue;
    console.log(`${String(s).padStart(4)}   ${f.curve[s].toFixed(4).padStart(9)}  ${u.curve[s].toFixed(4).padStart(9)}`);
  }
  console.log(`\nfinal (avg last 10)   float ${f.tail.toFixed(4)}   units ${u.tail.toFixed(4)}`);
  console.log(`ms/step               float ${f.ms.toFixed(0)}       units ${u.ms.toFixed(0)}`);
  const ratio = u.tail / f.tail;
  console.log(`\nunits/float loss ratio: ${ratio.toFixed(3)}  (1.00 = no damage)`);
  const converged = u.tail < u.first * 0.75 && u.tail < Math.log(X.vocabSize());
  console.log(converged ? "unit backward CONVERGES" : "unit backward FAILED TO CONVERGE");
  console.log(ratio < 1.15 ? "damage within 15% — viable" : "damage exceeds 15% — needs work (try 2-pass gradients)");
})();