Quazim0t0 commited on
Commit
b9cbc2e
·
verified ·
1 Parent(s): 4bb3f7c

Web demo: mini transformer LM + scaling sliders

Browse files
web/public/app.js CHANGED
@@ -2,8 +2,8 @@
2
  // train a shared model together — averaging gradients over the data channels.
3
  "use strict";
4
 
5
- const DIN = 16, DOUT = 4, NPER = 128;
6
- let D = { n: NPER, din: DIN, h: 16, dout: DOUT }; // h is set by the training-settings slider
7
  const STUN = [{ urls: "stun:stun.l.google.com:19302" }];
8
 
9
  const ui = {
@@ -24,7 +24,9 @@ const ui = {
24
  roomInfo: document.getElementById("roomInfo"),
25
  roomCode: document.getElementById("roomCode"),
26
  copyLink: document.getElementById("copyLink"),
27
- cfgH: document.getElementById("cfgH"),
 
 
28
  cfgSteps: document.getElementById("cfgSteps"),
29
  cfgLr: document.getElementById("cfgLr"),
30
  };
@@ -48,7 +50,7 @@ let myId = null, compute = null, ws = null, L = null, wasDenied = false;
48
  const pcs = new Map(), chans = new Map(); // peerId -> RTCPeerConnection / DataChannel
49
  const names = new Map(); // peerId -> device name
50
  const incoming = new Map(); // step -> Map(peerId -> Float32Array)
51
- let W1, W2, Xdata, Ydata, training = false; // 2-layer verified model
52
  let trainedSteps = 0; // steps baked into the current weights
53
  function nmeOf(id) { return names.get(id) || id; }
54
 
@@ -154,32 +156,35 @@ function cleanupPeer(id) { const pc = pcs.get(id); if (pc) pc.close(); pcs.delet
154
 
155
  // ---- checkpoints ------------------------------------------------------------
156
  // File layout (also the broadcast payload after the sentinel):
157
- // 8 bytes magic "DAISYPT1" | int32 din,h,dout,steps | f32 W1 | f32 W2
158
  // DaisyChain's own format (not torch-pickle) — .pt extension for familiarity.
159
- const CKPT_MAGIC = "DAISYPT1";
160
  const CKPT_SENTINEL = -2; // wire: [int32 -2][checkpoint bytes]
161
 
162
  function packCheckpoint() {
163
- const buf = new ArrayBuffer(8 + 16 + (W1.length + W2.length) * 4);
 
164
  new Uint8Array(buf, 0, 8).set([...CKPT_MAGIC].map(c => c.charCodeAt(0)));
165
- new Int32Array(buf, 8, 4).set([DIN, D.h, DOUT, trainedSteps]);
166
- new Float32Array(buf, 24, W1.length).set(W1);
167
- new Float32Array(buf, 24 + W1.length * 4, W2.length).set(W2);
168
  return buf;
169
  }
170
  function parseCheckpoint(buf) {
171
  const magic = String.fromCharCode(...new Uint8Array(buf, 0, 8));
172
- if (magic !== CKPT_MAGIC) throw new Error("not a DaisyChain checkpoint");
173
- const [din, h, dout, steps] = new Int32Array(buf, 8, 4);
174
- if (din !== DIN || dout !== DOUT || h < 8 || h > 64)
175
- throw new Error(`shape mismatch: file is ${din}×${h}×${dout}, this build is ${DIN}×(8–64)×${DOUT}`);
176
- if (buf.byteLength !== 24 + (din * h + h * dout) * 4) throw new Error("truncated checkpoint");
177
- return { h, steps, w1: new Float32Array(buf.slice(24, 24 + din * h * 4)),
178
- w2: new Float32Array(buf.slice(24 + din * h * 4)) };
179
  }
180
  function applyCheckpoint(ck, from) {
181
- if (ck.h !== D.h) { buildModel(ck.h); showCfgInUI({ h: ck.h, steps: +ui.cfgSteps.value, lr: +ui.cfgLr.value / 100 }); }
182
- W1.set(ck.w1); W2.set(ck.w2); trainedSteps = ck.steps;
 
 
 
 
 
183
  ui.save.disabled = false;
184
  ui.step.textContent = `${ck.steps} baked in`;
185
  log(`checkpoint loaded (${ck.steps} steps) ${from ? "from " + from : "from file"} — all set to resume`);
@@ -197,38 +202,41 @@ function saveCheckpoint() {
197
  const blob = new Blob([packCheckpoint()], { type: "application/octet-stream" });
198
  const a = document.createElement("a");
199
  a.href = URL.createObjectURL(blob);
200
- a.download = `daisychain-${DIN}x${D.h}x${DOUT}-step${trainedSteps}.pt`;
201
  a.click();
202
  URL.revokeObjectURL(a.href);
203
  }
204
 
205
  // ---- training config: whoever presses Start sets it for the whole group ----
206
- const CFG_SENTINEL = -3; // wire: [int32 -3][int32 h,steps][f32 lr]
207
  function readCfgFromUI() {
208
- return { h: +ui.cfgH.value, steps: +ui.cfgSteps.value, lr: +ui.cfgLr.value / 100 };
 
209
  }
210
  function showCfgInUI(cfg) {
211
- ui.cfgH.value = cfg.h; document.getElementById("vcfgH").textContent = cfg.h;
212
- ui.cfgSteps.value = cfg.steps; document.getElementById("vcfgSteps").textContent = cfg.steps;
213
- ui.cfgLr.value = Math.round(cfg.lr * 100); document.getElementById("vcfgLr").textContent = Math.round(cfg.lr * 100);
 
214
  }
215
  function broadcastConfig(cfg) {
216
- const buf = new ArrayBuffer(16);
217
- new Int32Array(buf, 0, 3).set([CFG_SENTINEL, cfg.h, cfg.steps]);
218
- new Float32Array(buf, 12, 1)[0] = cfg.lr;
219
  for (const dc of chans.values()) if (dc.readyState === "open") dc.send(buf);
220
  }
221
  function onConfig(peerId, buf) {
222
  if (training) { log(`ignored settings from ${nmeOf(peerId)} (already training)`); return; }
223
- const [, h, steps] = new Int32Array(buf, 0, 3);
224
- const lr = new Float32Array(buf, 12, 1)[0];
225
- if (!(h >= 8 && h <= 64 && steps >= 1 && steps <= 10000 && lr > 0 && lr <= 1)) {
 
226
  log(`rejected bad settings from ${nmeOf(peerId)}`); return;
227
  }
228
- const cfg = { h, steps, lr };
229
  showCfgInUI(cfg);
230
- log(`${nmeOf(peerId)} started the group: hidden=${h}, ${steps} steps, lr=${lr}`);
231
- buildModel(cfg.h);
232
  train(cfg); // follow automatically
233
  }
234
 
@@ -281,20 +289,19 @@ function waitForGrads(step, cohort, timeoutMs = 8000) {
281
 
282
  // ---- compute: one async training step THROUGH the verified units -----------
283
  async function localStep() {
284
- // forward runs through the verified INT8 multiply (WebGPU or CPU); STE backward
285
- const fwd = await Verified.forward(Xdata, Ydata, W1, W2, D, L, compute.matmulInt8);
286
- const grad = Verified.backward(Xdata, W1, W2, fwd, D); // flat [gW1, gW2]
287
- return { loss: fwd.loss, grad };
288
  }
289
 
290
  // ---- the training loop -----------------------------------------------------
291
  async function train(cfg) {
292
  if (training) return; training = true; ui.start.disabled = true;
293
- [ui.cfgH, ui.cfgSteps, ui.cfgLr].forEach(el => el.disabled = true);
294
  const cohort = [...chans.keys()]; // lock the cohort (departed peers are pruned per-step)
295
  const steps = cfg.steps;
296
- const opt = TrainCore.makeAdam(W1.length + W2.length, { lr: cfg.lr });
297
- log(`training started — cohort ${cohort.length} peer(s), world ${cohort.length + 1}, hidden=${cfg.h}, optimizer ${opt.name}`);
 
298
  for (let s = 0; s < steps; s++) {
299
  const { loss, grad } = await localStep();
300
  broadcastGrad(s, grad);
@@ -302,7 +309,7 @@ async function train(cfg) {
302
  const all = [grad, ...remote];
303
  const avg = TrainCore.averageGrads(all);
304
  const upd = opt.step(avg); // DaisyAdam on the cluster-avg grad
305
- Verified.splitApply(W1, W2, upd, 1); // W -= 1 * upd (lr folded into upd)
306
  incoming.delete(s);
307
  trainedSteps++;
308
  if (s % 10 === 0 || s === steps - 1) {
@@ -312,30 +319,24 @@ async function train(cfg) {
312
  await new Promise(r => setTimeout(r, 0)); // yield to UI
313
  }
314
  }
315
- ui.diff.textContent = `done — trained through the verified units; all peers share one model.`;
316
  log(`training done — final loss ${ui.loss.textContent}`);
 
 
 
 
 
 
317
  training = false;
318
  ui.save.disabled = false;
319
  ui.start.disabled = false;
320
- [ui.cfgH, ui.cfgSteps, ui.cfgLr].forEach(el => el.disabled = false);
321
- }
322
-
323
- // 2-layer float target (matches the model shape) for a learnable task
324
- function target(X, h) {
325
- const Wt1 = randn(DIN * h, mulberry32(42)), Wt2 = randn(h * DOUT, mulberry32(43));
326
- const hpre = TrainCore.matmul(X, Wt1, NPER, DIN, h);
327
- for (let i = 0; i < hpre.length; i++) hpre[i] = Math.max(0, hpre[i]);
328
- return TrainCore.matmul(hpre, Wt2, NPER, h, DOUT);
329
  }
 
330
 
331
- // (re)build the model for a hidden size — deterministic shared init (same seeds
332
- // on every peer), fresh per-peer data shard
333
- function buildModel(h) {
334
- D = { n: NPER, din: DIN, h, dout: DOUT };
335
- W1 = randn(DIN * h, mulberry32(7));
336
- W2 = randn(h * DOUT, mulberry32(8));
337
- Xdata = randn(NPER * DIN);
338
- Ydata = target(Xdata, h);
339
  trainedSteps = 0;
340
  }
341
 
@@ -387,9 +388,10 @@ function buildModel(h) {
387
  }
388
  ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label} · through verified INT8 units`;
389
  // slider readouts
390
- for (const [el, v] of [[ui.cfgH, "vcfgH"], [ui.cfgSteps, "vcfgSteps"], [ui.cfgLr, "vcfgLr"]])
 
391
  el.oninput = () => document.getElementById(v).textContent = el.value;
392
- buildModel(+ui.cfgH.value);
393
  ui.me.textContent = deviceName;
394
  if (room()) {
395
  ui.roomInfo.style.display = "";
@@ -404,7 +406,7 @@ function buildModel(h) {
404
  connectSignaling();
405
  ui.start.onclick = () => {
406
  const cfg = readCfgFromUI();
407
- buildModel(cfg.h);
408
  broadcastConfig(cfg); // everyone follows these settings
409
  train(cfg);
410
  };
 
2
  // train a shared model together — averaging gradients over the data channels.
3
  "use strict";
4
 
5
+ // model + task now live in transformer.js a mini transformer LM trained
6
+ // through the verified INT8 units. Settings come from the sliders.
7
  const STUN = [{ urls: "stun:stun.l.google.com:19302" }];
8
 
9
  const ui = {
 
24
  roomInfo: document.getElementById("roomInfo"),
25
  roomCode: document.getElementById("roomCode"),
26
  copyLink: document.getElementById("copyLink"),
27
+ cfgC: document.getElementById("cfgC"),
28
+ cfgT: document.getElementById("cfgT"),
29
+ cfgB: document.getElementById("cfgB"),
30
  cfgSteps: document.getElementById("cfgSteps"),
31
  cfgLr: document.getElementById("cfgLr"),
32
  };
 
50
  const pcs = new Map(), chans = new Map(); // peerId -> RTCPeerConnection / DataChannel
51
  const names = new Map(); // peerId -> device name
52
  const incoming = new Map(); // step -> Map(peerId -> Float32Array)
53
+ let model = null, training = false; // the mini transformer (transformer.js)
54
  let trainedSteps = 0; // steps baked into the current weights
55
  function nmeOf(id) { return names.get(id) || id; }
56
 
 
156
 
157
  // ---- checkpoints ------------------------------------------------------------
158
  // File layout (also the broadcast payload after the sentinel):
159
+ // 8 bytes magic "DAISYPT2" | int32 c,t,vocab,steps | f32 flat params
160
  // DaisyChain's own format (not torch-pickle) — .pt extension for familiarity.
161
+ const CKPT_MAGIC = "DAISYPT2";
162
  const CKPT_SENTINEL = -2; // wire: [int32 -2][checkpoint bytes]
163
 
164
  function packCheckpoint() {
165
+ const flat = Transformer.getFlatParams(model);
166
+ const buf = new ArrayBuffer(8 + 16 + flat.length * 4);
167
  new Uint8Array(buf, 0, 8).set([...CKPT_MAGIC].map(c => c.charCodeAt(0)));
168
+ new Int32Array(buf, 8, 4).set([model.cfg.c, model.cfg.t, model.cfg.vocab, trainedSteps]);
169
+ new Float32Array(buf, 24).set(flat);
 
170
  return buf;
171
  }
172
  function parseCheckpoint(buf) {
173
  const magic = String.fromCharCode(...new Uint8Array(buf, 0, 8));
174
+ if (magic !== CKPT_MAGIC) throw new Error("not a DaisyChain v2 checkpoint");
175
+ const [c, t, vocab, steps] = new Int32Array(buf, 8, 4);
176
+ if (vocab !== Transformer.VOCAB) throw new Error(`vocab mismatch (file ${vocab}, this build ${Transformer.VOCAB})`);
177
+ if (c < 16 || c > 64 || t < 16 || t > 64) throw new Error(`bad dims in checkpoint (width ${c}, seq ${t})`);
178
+ return { c, t, steps, flat: new Float32Array(buf.slice(24)) };
 
 
179
  }
180
  function applyCheckpoint(ck, from) {
181
+ if (!model || model.cfg.c !== ck.c || model.cfg.t !== ck.t) {
182
+ buildModel({ ...readCfgFromUI(), c: ck.c, t: ck.t });
183
+ showCfgInUI({ ...readCfgFromUI(), c: ck.c, t: ck.t });
184
+ }
185
+ if (ck.flat.length !== model.nParams) throw new Error("truncated checkpoint");
186
+ Transformer.setFlatParams(model, ck.flat);
187
+ trainedSteps = ck.steps;
188
  ui.save.disabled = false;
189
  ui.step.textContent = `${ck.steps} baked in`;
190
  log(`checkpoint loaded (${ck.steps} steps) ${from ? "from " + from : "from file"} — all set to resume`);
 
202
  const blob = new Blob([packCheckpoint()], { type: "application/octet-stream" });
203
  const a = document.createElement("a");
204
  a.href = URL.createObjectURL(blob);
205
+ a.download = `daisychain-lm-w${model.cfg.c}-step${trainedSteps}.pt`;
206
  a.click();
207
  URL.revokeObjectURL(a.href);
208
  }
209
 
210
  // ---- training config: whoever presses Start sets it for the whole group ----
211
+ const CFG_SENTINEL = -3; // wire: [int32 -3][int32 c,t,b,steps][f32 lr]
212
  function readCfgFromUI() {
213
+ return { c: +ui.cfgC.value, t: +ui.cfgT.value, b: +ui.cfgB.value,
214
+ steps: +ui.cfgSteps.value, lr: +ui.cfgLr.value / 1000 };
215
  }
216
  function showCfgInUI(cfg) {
217
+ const set = (el, vid, val) => { el.value = val; document.getElementById(vid).textContent = val; };
218
+ set(ui.cfgC, "vcfgC", cfg.c); set(ui.cfgT, "vcfgT", cfg.t); set(ui.cfgB, "vcfgB", cfg.b);
219
+ set(ui.cfgSteps, "vcfgSteps", cfg.steps);
220
+ set(ui.cfgLr, "vcfgLr", Math.round(cfg.lr * 1000));
221
  }
222
  function broadcastConfig(cfg) {
223
+ const buf = new ArrayBuffer(24);
224
+ new Int32Array(buf, 0, 5).set([CFG_SENTINEL, cfg.c, cfg.t, cfg.b, cfg.steps]);
225
+ new Float32Array(buf, 20, 1)[0] = cfg.lr;
226
  for (const dc of chans.values()) if (dc.readyState === "open") dc.send(buf);
227
  }
228
  function onConfig(peerId, buf) {
229
  if (training) { log(`ignored settings from ${nmeOf(peerId)} (already training)`); return; }
230
+ const [, c, t, b, steps] = new Int32Array(buf, 0, 5);
231
+ const lr = new Float32Array(buf, 20, 1)[0];
232
+ if (!(c >= 16 && c <= 64 && c % 2 === 0 && t >= 16 && t <= 64 && b >= 1 && b <= 32 &&
233
+ steps >= 1 && steps <= 10000 && lr > 0 && lr <= 0.2)) {
234
  log(`rejected bad settings from ${nmeOf(peerId)}`); return;
235
  }
236
+ const cfg = { c, t, b, steps, lr };
237
  showCfgInUI(cfg);
238
+ log(`${nmeOf(peerId)} started the group: width=${c}, seq=${t}, batch=${b}, ${steps} steps, lr=${lr}`);
239
+ buildModel(cfg);
240
  train(cfg); // follow automatically
241
  }
242
 
 
289
 
290
  // ---- compute: one async training step THROUGH the verified units -----------
291
  async function localStep() {
292
+ // transformer forward runs through the verified INT8 multiply; STE backward
293
+ return Transformer.trainStep(model);
 
 
294
  }
295
 
296
  // ---- the training loop -----------------------------------------------------
297
  async function train(cfg) {
298
  if (training) return; training = true; ui.start.disabled = true;
299
+ cfgSliders().forEach(el => el.disabled = true);
300
  const cohort = [...chans.keys()]; // lock the cohort (departed peers are pruned per-step)
301
  const steps = cfg.steps;
302
+ const opt = TrainCore.makeAdam(model.nParams, { lr: cfg.lr });
303
+ log(`training started — cohort ${cohort.length} peer(s), world ${cohort.length + 1}, ` +
304
+ `width=${cfg.c} seq=${cfg.t} batch=${cfg.b}×${cohort.length + 1}, optimizer ${opt.name}`);
305
  for (let s = 0; s < steps; s++) {
306
  const { loss, grad } = await localStep();
307
  broadcastGrad(s, grad);
 
309
  const all = [grad, ...remote];
310
  const avg = TrainCore.averageGrads(all);
311
  const upd = opt.step(avg); // DaisyAdam on the cluster-avg grad
312
+ Transformer.applyUpdate(model, upd); // W -= upd (lr folded into upd)
313
  incoming.delete(s);
314
  trainedSteps++;
315
  if (s % 10 === 0 || s === steps - 1) {
 
319
  await new Promise(r => setTimeout(r, 0)); // yield to UI
320
  }
321
  }
 
322
  log(`training done — final loss ${ui.loss.textContent}`);
323
+ try {
324
+ const sample = await Transformer.generate(model, "the ", 70);
325
+ ui.diff.textContent = `the model speaks: “${sample.trim()}”`;
326
+ } catch (e) {
327
+ ui.diff.textContent = "done — trained through the verified units; all peers share one model.";
328
+ }
329
  training = false;
330
  ui.save.disabled = false;
331
  ui.start.disabled = false;
332
+ cfgSliders().forEach(el => el.disabled = false);
 
 
 
 
 
 
 
 
333
  }
334
+ function cfgSliders() { return [ui.cfgC, ui.cfgT, ui.cfgB, ui.cfgSteps, ui.cfgLr]; }
335
 
336
+ // (re)build the mini transformer — deterministic shared init (same seeds on
337
+ // every peer); each device samples its own batch windows from the shared corpus
338
+ function buildModel(cfg) {
339
+ model = Transformer.init(cfg, L, compute.matmulInt8);
 
 
 
 
340
  trainedSteps = 0;
341
  }
342
 
 
388
  }
389
  ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label} · through verified INT8 units`;
390
  // slider readouts
391
+ for (const [el, v] of [[ui.cfgC, "vcfgC"], [ui.cfgT, "vcfgT"], [ui.cfgB, "vcfgB"],
392
+ [ui.cfgSteps, "vcfgSteps"], [ui.cfgLr, "vcfgLr"]])
393
  el.oninput = () => document.getElementById(v).textContent = el.value;
394
+ buildModel(readCfgFromUI());
395
  ui.me.textContent = deviceName;
396
  if (room()) {
397
  ui.roomInfo.style.display = "";
 
406
  connectSignaling();
407
  ui.start.onclick = () => {
408
  const cfg = readCfgFromUI();
409
+ buildModel(cfg);
410
  broadcastConfig(cfg); // everyone follows these settings
411
  train(cfg);
412
  };
web/public/index.html CHANGED
@@ -105,13 +105,17 @@
105
 
106
  <div class="card">
107
  <div class="lbl">🎛 Training settings</div>
108
- <label class="slbl">Hidden units <span class="sval" id="vcfgH">16</span></label>
109
- <input type="range" id="cfgH" min="8" max="64" step="8" value="16">
 
 
 
 
110
  <label class="slbl">Steps <span class="sval" id="vcfgSteps">300</span></label>
111
- <input type="range" id="cfgSteps" min="100" max="1000" step="100" value="300">
112
- <label class="slbl">Learning rate ×100 <span class="sval" id="vcfgLr">20</span></label>
113
  <input type="range" id="cfgLr" min="5" max="50" step="5" value="20">
114
- <p class="note" style="margin:.5rem 0 0">Whoever presses Start sets the settings for the whole group every device follows automatically.</p>
115
  </div>
116
 
117
  <div class="card" style="text-align:center">
@@ -147,6 +151,7 @@
147
 
148
  <script src="traincore.js"></script>
149
  <script src="verified_core.js"></script>
 
150
  <script src="webgpu.js"></script>
151
  <script src="app.js"></script>
152
  </body>
 
105
 
106
  <div class="card">
107
  <div class="lbl">🎛 Training settings</div>
108
+ <label class="slbl">Model width <span class="sval" id="vcfgC">32</span></label>
109
+ <input type="range" id="cfgC" min="16" max="64" step="16" value="32">
110
+ <label class="slbl">Sequence length <span class="sval" id="vcfgT">32</span></label>
111
+ <input type="range" id="cfgT" min="16" max="64" step="16" value="32">
112
+ <label class="slbl">Batch per device <span class="sval" id="vcfgB">8</span></label>
113
+ <input type="range" id="cfgB" min="2" max="32" step="2" value="8">
114
  <label class="slbl">Steps <span class="sval" id="vcfgSteps">300</span></label>
115
+ <input type="range" id="cfgSteps" min="100" max="10000" step="100" value="300">
116
+ <label class="slbl">Learning rate ×1000 <span class="sval" id="vcfgLr">20</span></label>
117
  <input type="range" id="cfgLr" min="5" max="50" step="5" value="20">
118
+ <p class="note" style="margin:.5rem 0 0">A mini transformer language model — attention, MLP blocks, next-character prediction — every multiply through the verified INT8 units. Whoever presses Start sets the settings for the whole group; total batch scales with every device that joins.</p>
119
  </div>
120
 
121
  <div class="card" style="text-align:center">
 
151
 
152
  <script src="traincore.js"></script>
153
  <script src="verified_core.js"></script>
154
+ <script src="transformer.js"></script>
155
  <script src="webgpu.js"></script>
156
  <script src="app.js"></script>
157
  </body>
web/public/transformer.js ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // A miniature transformer language model that trains THROUGH the verified INT8
2
+ // units: every matrix product in the forward pass — QKV projections, attention
3
+ // scores, attention·values, output projection, the MLP, and the unembedding —
4
+ // runs through the verified multiply LUT (an emulated INT8 tensor core).
5
+ // Backward is a straight-through estimator in float (the integer path has no
6
+ // gradient), exactly like the Python VerifiedLinear.
7
+ //
8
+ // Task: next-character prediction on a deterministic, self-generated corpus
9
+ // (every peer builds the same text from the same seed — nothing to download).
10
+ (function (root) {
11
+ "use strict";
12
+
13
+ let TC, V; // TrainCore / Verified — resolved per environment at the end
14
+
15
+ function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
16
+ function randn(n, rng) { const o = new Float32Array(n); for (let i = 0; i < n; i += 2) { let u = 0, v = 0; while (u === 0) u = rng(); while (v === 0) v = rng(); const m = Math.sqrt(-2 * Math.log(u)); o[i] = m * Math.cos(2 * Math.PI * v); if (i + 1 < n) o[i + 1] = m * Math.sin(2 * Math.PI * v); } return o; }
17
+
18
+ // ---- corpus: deterministic cottagecore prose, identical on every peer -----
19
+ const W_ADJ = ["mossy", "golden", "amber", "quiet", "little", "misty", "sunny", "wild", "cozy", "dusty", "merry", "brave"];
20
+ const W_NOUN = ["fox", "hare", "owl", "badger", "toad", "sparrow", "otter", "deer", "mushroom", "acorn", "willow", "robin", "river", "meadow", "garden", "lantern"];
21
+ const W_VERB = ["naps", "sings", "wanders", "hides", "dreams", "waits", "dances", "listens", "rests", "grows"];
22
+ const W_PREP = ["by", "under", "near", "beside", "beyond", "inside"];
23
+ function buildCorpus() {
24
+ const rng = mulberry32(20260712);
25
+ const pick = (a) => a[Math.floor(rng() * a.length)];
26
+ let s = "";
27
+ while (s.length < 60000)
28
+ s += `the ${pick(W_ADJ)} ${pick(W_NOUN)} ${pick(W_VERB)} ${pick(W_PREP)} the ${pick(W_ADJ)} ${pick(W_NOUN)}. `;
29
+ return s;
30
+ }
31
+ const CORPUS = buildCorpus();
32
+ const CHARS = [...new Set(CORPUS)].sort();
33
+ const VOCAB = CHARS.length;
34
+ const STOI = Object.fromEntries(CHARS.map((c, i) => [c, i]));
35
+ const IDS = new Int32Array(CORPUS.length);
36
+ for (let i = 0; i < CORPUS.length; i++) IDS[i] = STOI[CORPUS[i]];
37
+
38
+ // ---- verified matmul: float in -> quantize -> LUT multiply -> dequant -----
39
+ // weights/projections may go through WebGPU; attention (per-head, many small
40
+ // matmuls) uses the CPU LUT path — same verified units, no dispatch overhead.
41
+ async function vmm(Xf, Wf, m, k, n, ctx) {
42
+ const xq = V.quantize(Xf), wq = V.quantize(Wf);
43
+ const acc = await ctx.matmulInt8(xq.q, wq.q, m, k, n, ctx.L);
44
+ const dq = xq.scale * wq.scale, out = new Float32Array(m * n);
45
+ for (let i = 0; i < out.length; i++) out[i] = acc[i] * dq;
46
+ return out;
47
+ }
48
+ function vmmCPU(Xf, Wf, m, k, n, ctx) {
49
+ const xq = V.quantize(Xf), wq = V.quantize(Wf);
50
+ const acc = V.lutMatmulJS(xq.q, wq.q, m, k, n, ctx.L);
51
+ const dq = xq.scale * wq.scale, out = new Float32Array(m * n);
52
+ for (let i = 0; i < out.length; i++) out[i] = acc[i] * dq;
53
+ return out;
54
+ }
55
+
56
+ // ---- layernorm (no affine) -------------------------------------------------
57
+ function lnFwd(x, rows, C) {
58
+ const y = new Float32Array(rows * C), sig = new Float32Array(rows);
59
+ for (let r = 0; r < rows; r++) {
60
+ let mu = 0; for (let j = 0; j < C; j++) mu += x[r * C + j]; mu /= C;
61
+ let v = 0; for (let j = 0; j < C; j++) { const d = x[r * C + j] - mu; v += d * d; }
62
+ const s = Math.sqrt(v / C + 1e-5); sig[r] = s;
63
+ for (let j = 0; j < C; j++) y[r * C + j] = (x[r * C + j] - mu) / s;
64
+ }
65
+ return { y, sig };
66
+ }
67
+ function lnBwd(dy, y, sig, rows, C) {
68
+ const dx = new Float32Array(rows * C);
69
+ for (let r = 0; r < rows; r++) {
70
+ let mdy = 0, mdyy = 0;
71
+ for (let j = 0; j < C; j++) { mdy += dy[r * C + j]; mdyy += dy[r * C + j] * y[r * C + j]; }
72
+ mdy /= C; mdyy /= C;
73
+ for (let j = 0; j < C; j++) dx[r * C + j] = (dy[r * C + j] - mdy - y[r * C + j] * mdyy) / sig[r];
74
+ }
75
+ return dx;
76
+ }
77
+
78
+ // ---- model -----------------------------------------------------------------
79
+ // cfg: { c: width, t: seq len, b: batch/device, layers, heads, steps, lr }
80
+ function init(cfg, L, matmulInt8) {
81
+ const c = cfg.c, layers = cfg.layers || 2, heads = cfg.heads || 2, hidden = 2 * c;
82
+ let seed = 100;
83
+ const mk = (nEl, scale) => { const w = randn(nEl, mulberry32(seed++)); for (let i = 0; i < nEl; i++) w[i] *= scale; return w; };
84
+ const params = [], names = [];
85
+ const add = (name, w) => { params.push(w); names.push(name); return w; };
86
+ const m = {
87
+ cfg: { ...cfg, layers, heads, hidden, vocab: VOCAB },
88
+ ctx: { L, matmulInt8 },
89
+ emb: add("emb", mk(VOCAB * c, 0.08)),
90
+ pos: add("pos", mk(cfg.t * c, 0.02)),
91
+ blocks: [], params, names,
92
+ };
93
+ for (let l = 0; l < layers; l++)
94
+ m.blocks.push({
95
+ Wq: add(`b${l}.Wq`, mk(c * c, 0.08)), Wk: add(`b${l}.Wk`, mk(c * c, 0.08)),
96
+ Wv: add(`b${l}.Wv`, mk(c * c, 0.08)), Wo: add(`b${l}.Wo`, mk(c * c, 0.08)),
97
+ W1: add(`b${l}.W1`, mk(c * hidden, 0.08)), W2: add(`b${l}.W2`, mk(hidden * c, 0.08)),
98
+ });
99
+ m.Wu = add("Wu", mk(c * VOCAB, 0.08));
100
+ m.nParams = params.reduce((a, p) => a + p.length, 0);
101
+ return m;
102
+ }
103
+
104
+ function sampleBatch(cfg) {
105
+ const { b, t } = cfg;
106
+ const X = new Int32Array(b * t), Y = new Int32Array(b * t);
107
+ for (let i = 0; i < b; i++) {
108
+ const off = Math.floor(Math.random() * (IDS.length - t - 1));
109
+ for (let j = 0; j < t; j++) { X[i * t + j] = IDS[off + j]; Y[i * t + j] = IDS[off + j + 1]; }
110
+ }
111
+ return { X, Y };
112
+ }
113
+
114
+ // per-head slice helpers (q: BT×C row-major; head h occupies cols [h*hd, (h+1)*hd))
115
+ function headSlice(q, bIdx, h, T, C, hd) {
116
+ const out = new Float32Array(T * hd);
117
+ for (let ti = 0; ti < T; ti++)
118
+ for (let j = 0; j < hd; j++) out[ti * hd + j] = q[(bIdx * T + ti) * C + h * hd + j];
119
+ return out;
120
+ }
121
+ function headUnslice(dst, src, bIdx, h, T, C, hd, accumulate) {
122
+ for (let ti = 0; ti < T; ti++)
123
+ for (let j = 0; j < hd; j++) {
124
+ const di = (bIdx * T + ti) * C + h * hd + j;
125
+ dst[di] = (accumulate ? dst[di] : 0) + src[ti * hd + j];
126
+ }
127
+ }
128
+
129
+ // ---- forward THROUGH the verified units (caches kept for STE backward) -----
130
+ async function forward(m, X, Y) {
131
+ const { c: C, t: T, b: B, layers, heads, hidden, vocab } = m.cfg;
132
+ const BT = B * T, hd = C / heads, ctx = m.ctx;
133
+ const cache = { X, Y, blocks: [] };
134
+ let x = new Float32Array(BT * C);
135
+ for (let i = 0; i < BT; i++) {
136
+ const id = X[i], tpos = i % T;
137
+ for (let j = 0; j < C; j++) x[i * C + j] = m.emb[id * C + j] + m.pos[tpos * C + j];
138
+ }
139
+ for (let l = 0; l < layers; l++) {
140
+ const bl = m.blocks[l], cb = { xin: x };
141
+ const l1 = lnFwd(x, BT, C); cb.ln1 = l1;
142
+ const q = await vmm(l1.y, bl.Wq, BT, C, C, ctx);
143
+ const k = await vmm(l1.y, bl.Wk, BT, C, C, ctx);
144
+ const v = await vmm(l1.y, bl.Wv, BT, C, C, ctx);
145
+ cb.q = q; cb.k = k; cb.v = v;
146
+ const ctxOut = new Float32Array(BT * C);
147
+ cb.att = []; // per (b,h): softmax probs
148
+ const scale = 1 / Math.sqrt(hd);
149
+ for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
150
+ const qh = headSlice(q, bi, h, T, C, hd), kh = headSlice(k, bi, h, T, C, hd), vh = headSlice(v, bi, h, T, C, hd);
151
+ const kT = TC.transpose(kh, T, hd); // hd×T
152
+ const s = vmmCPU(qh, kT, T, hd, T, ctx); // scores through the units
153
+ for (let i = 0; i < T * T; i++) s[i] *= scale;
154
+ const a = new Float32Array(T * T); // causal softmax
155
+ for (let ti = 0; ti < T; ti++) {
156
+ let mx = -1e30;
157
+ for (let tj = 0; tj <= ti; tj++) mx = Math.max(mx, s[ti * T + tj]);
158
+ let z = 0;
159
+ for (let tj = 0; tj <= ti; tj++) { const e = Math.exp(s[ti * T + tj] - mx); a[ti * T + tj] = e; z += e; }
160
+ for (let tj = 0; tj <= ti; tj++) a[ti * T + tj] /= z;
161
+ }
162
+ const ch = vmmCPU(a, vh, T, T, hd, ctx); // attn·V through the units
163
+ headUnslice(ctxOut, ch, bi, h, T, C, hd, false);
164
+ cb.att.push({ a, qh, kh, vh });
165
+ }
166
+ cb.ctxOut = ctxOut;
167
+ const attnOut = await vmm(ctxOut, bl.Wo, BT, C, C, ctx);
168
+ const x2 = new Float32Array(BT * C);
169
+ for (let i = 0; i < x2.length; i++) x2[i] = x[i] + attnOut[i];
170
+ cb.x2 = x2;
171
+ const l2 = lnFwd(x2, BT, C); cb.ln2 = l2;
172
+ const h1 = await vmm(l2.y, bl.W1, BT, C, hidden, ctx);
173
+ const mask = new Uint8Array(h1.length);
174
+ for (let i = 0; i < h1.length; i++) { if (h1[i] > 0) mask[i] = 1; else h1[i] = 0; }
175
+ cb.h1 = h1; cb.mask = mask;
176
+ const mlpOut = await vmm(h1, bl.W2, BT, hidden, C, ctx);
177
+ x = new Float32Array(BT * C);
178
+ for (let i = 0; i < x.length; i++) x[i] = x2[i] + mlpOut[i];
179
+ cache.blocks.push(cb);
180
+ }
181
+ const lf = lnFwd(x, BT, C); cache.lnf = lf; cache.xf = x;
182
+ const logits = await vmm(lf.y, m.Wu, BT, C, vocab, ctx);
183
+ // cross-entropy + dlogits
184
+ let loss = 0;
185
+ const dlogits = new Float32Array(BT * vocab);
186
+ for (let i = 0; i < BT; i++) {
187
+ let mx = -1e30;
188
+ for (let j = 0; j < vocab; j++) mx = Math.max(mx, logits[i * vocab + j]);
189
+ let z = 0;
190
+ for (let j = 0; j < vocab; j++) z += Math.exp(logits[i * vocab + j] - mx);
191
+ const lz = Math.log(z) + mx;
192
+ loss += lz - logits[i * vocab + Y[i]];
193
+ for (let j = 0; j < vocab; j++)
194
+ dlogits[i * vocab + j] = (Math.exp(logits[i * vocab + j] - lz) - (j === Y[i] ? 1 : 0)) / BT;
195
+ }
196
+ loss /= BT;
197
+ cache.dlogits = dlogits;
198
+ return { loss, cache, logits };
199
+ }
200
+
201
+ // ---- STE backward (float), mirrors forward exactly --------------------------
202
+ function backward(m, cache) {
203
+ const { c: C, t: T, b: B, layers, heads, hidden, vocab } = m.cfg;
204
+ const BT = B * T, hd = C / heads, mm = TC.matmul, tr = TC.transpose;
205
+ const g = m.params.map(p => new Float32Array(p.length));
206
+ const gi = Object.fromEntries(m.names.map((n, i) => [n, i]));
207
+ // unembed
208
+ g[gi.Wu] = mm(tr(cache.lnf.y, BT, C), cache.dlogits, C, BT, vocab);
209
+ let dx = lnBwd(mm(cache.dlogits, tr(m.Wu, C, vocab), BT, vocab, C), cache.lnf.y, cache.lnf.sig, BT, C);
210
+ const scale = 1 / Math.sqrt(hd);
211
+ for (let l = layers - 1; l >= 0; l--) {
212
+ const bl = m.blocks[l], cb = cache.blocks[l];
213
+ // mlp: x3 = x2 + relu(ln2 @ W1) @ W2
214
+ const dmlpOut = dx; // residual passthrough handled below
215
+ g[gi[`b${l}.W2`]] = mm(tr(cb.h1, BT, hidden), dmlpOut, hidden, BT, C);
216
+ const dh1 = mm(dmlpOut, tr(bl.W2, hidden, C), BT, C, hidden);
217
+ for (let i = 0; i < dh1.length; i++) if (!cb.mask[i]) dh1[i] = 0;
218
+ g[gi[`b${l}.W1`]] = mm(tr(cb.ln2.y, BT, C), dh1, C, BT, hidden);
219
+ const dln2in = lnBwd(mm(dh1, tr(bl.W1, C, hidden), BT, hidden, C), cb.ln2.y, cb.ln2.sig, BT, C);
220
+ const dx2 = new Float32Array(BT * C);
221
+ for (let i = 0; i < dx2.length; i++) dx2[i] = dx[i] + dln2in[i];
222
+ // attention: x2 = xin + (ctxOut @ Wo)
223
+ g[gi[`b${l}.Wo`]] = mm(tr(cb.ctxOut, BT, C), dx2, C, BT, C);
224
+ const dctx = mm(dx2, tr(bl.Wo, C, C), BT, C, C);
225
+ const dq = new Float32Array(BT * C), dk = new Float32Array(BT * C), dv = new Float32Array(BT * C);
226
+ let ai = 0;
227
+ for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
228
+ const { a, qh, kh, vh } = cb.att[ai++];
229
+ const dch = headSlice(dctx, bi, h, T, C, hd); // T×hd
230
+ const dvh = mm(tr(a, T, T), dch, T, T, hd); // aᵀ @ dctx
231
+ const da = mm(dch, tr(vh, T, hd), T, hd, T); // dctx @ vᵀ
232
+ const ds = new Float32Array(T * T); // softmax bwd (causal)
233
+ for (let ti = 0; ti < T; ti++) {
234
+ let dot = 0;
235
+ for (let tj = 0; tj <= ti; tj++) dot += da[ti * T + tj] * a[ti * T + tj];
236
+ for (let tj = 0; tj <= ti; tj++) ds[ti * T + tj] = a[ti * T + tj] * (da[ti * T + tj] - dot) * scale;
237
+ }
238
+ const dqh = mm(ds, kh, T, T, hd); // ds @ k
239
+ const dkh = mm(tr(ds, T, T), qh, T, T, hd); // dsᵀ @ q
240
+ headUnslice(dq, dqh, bi, h, T, C, hd, true);
241
+ headUnslice(dk, dkh, bi, h, T, C, hd, true);
242
+ headUnslice(dv, dvh, bi, h, T, C, hd, true);
243
+ }
244
+ g[gi[`b${l}.Wq`]] = mm(tr(cb.ln1.y, BT, C), dq, C, BT, C);
245
+ g[gi[`b${l}.Wk`]] = mm(tr(cb.ln1.y, BT, C), dk, C, BT, C);
246
+ g[gi[`b${l}.Wv`]] = mm(tr(cb.ln1.y, BT, C), dv, C, BT, C);
247
+ const dln1in = new Float32Array(BT * C);
248
+ 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]; };
249
+ addIn(dq, bl.Wq); addIn(dk, bl.Wk); addIn(dv, bl.Wv);
250
+ const dxin = lnBwd(dln1in, cb.ln1.y, cb.ln1.sig, BT, C);
251
+ dx = new Float32Array(BT * C);
252
+ for (let i = 0; i < dx.length; i++) dx[i] = dx2[i] + dxin[i];
253
+ }
254
+ // embedding + positional
255
+ const ge = g[gi.emb], gp = g[gi.pos];
256
+ for (let i = 0; i < BT; i++) {
257
+ const id = cache.X[i], tpos = i % T;
258
+ for (let j = 0; j < C; j++) { ge[id * C + j] += dx[i * C + j]; gp[tpos * C + j] += dx[i * C + j]; }
259
+ }
260
+ // flatten
261
+ const flat = new Float32Array(m.nParams);
262
+ let off = 0;
263
+ for (const t of g) { flat.set(t, off); off += t.length; }
264
+ return flat;
265
+ }
266
+
267
+ async function trainStep(m) {
268
+ const { X, Y } = sampleBatch(m.cfg);
269
+ const { loss, cache } = await forward(m, X, Y);
270
+ const grad = backward(m, cache);
271
+ return { loss, grad };
272
+ }
273
+
274
+ function applyUpdate(m, upd) { // W -= upd (lr folded in by the optimizer)
275
+ let off = 0;
276
+ for (const p of m.params) { for (let i = 0; i < p.length; i++) p[i] -= upd[off + i]; off += p.length; }
277
+ }
278
+ function getFlatParams(m) {
279
+ const flat = new Float32Array(m.nParams);
280
+ let off = 0;
281
+ for (const p of m.params) { flat.set(p, off); off += p.length; }
282
+ return flat;
283
+ }
284
+ function setFlatParams(m, flat) {
285
+ let off = 0;
286
+ for (const p of m.params) { p.set(flat.subarray(off, off + p.length)); off += p.length; }
287
+ }
288
+
289
+ // greedy sampling — watch the model actually speak
290
+ async function generate(m, prompt, nChars) {
291
+ const { t: T } = m.cfg;
292
+ let ids = [...prompt].map(ch => STOI[ch] ?? 0);
293
+ for (let step = 0; step < nChars; step++) {
294
+ const win = ids.slice(-T);
295
+ const X = new Int32Array(T), Y = new Int32Array(T);
296
+ for (let i = 0; i < win.length; i++) X[T - win.length + i] = win[i];
297
+ const save = m.cfg.b; m.cfg.b = 1;
298
+ const { logits } = await forward(m, X, Y);
299
+ m.cfg.b = save;
300
+ const row = (T - 1) * m.cfg.vocab;
301
+ let best = 0, bv = -1e30;
302
+ for (let j = 0; j < m.cfg.vocab; j++) if (logits[row + j] > bv) { bv = logits[row + j]; best = j; }
303
+ ids.push(best);
304
+ }
305
+ return ids.map(i => CHARS[i]).join("");
306
+ }
307
+
308
+ const api = { init, trainStep, applyUpdate, getFlatParams, setFlatParams, generate, VOCAB, CORPUS };
309
+ if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); V = require("./verified_core.js"); module.exports = api; }
310
+ else { TC = root.TrainCore; V = root.Verified; root.Transformer = api; }
311
+ })(typeof self !== "undefined" ? self : this);
web/test_transformer.js ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Mini transformer through the verified units: (a) loss drops well below the
2
+ // uniform baseline ln(V), (b) two replicas fed the same averaged gradients stay
3
+ // bit-identical, (c) generation produces corpus-like text.
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const T = require("./public/traincore.js");
7
+ const V = require("./public/verified_core.js");
8
+ const X = require("./public/transformer.js");
9
+
10
+ function loadLUTs() {
11
+ const p = (f) => path.join(__dirname, "public", f);
12
+ return { mul: new Int16Array(fs.readFileSync(p("mul_lut.bin")).buffer.slice(0)),
13
+ requant: new Int8Array(fs.readFileSync(p("requant_lut.bin")).buffer.slice(0)),
14
+ relu: new Int8Array(fs.readFileSync(p("relu_lut.bin")).buffer.slice(0)) };
15
+ }
16
+ const L = loadLUTs();
17
+ const matmulInt8 = (Xq, Wq, m, k, n, LL) => V.lutMatmulJS(Xq, Wq, m, k, n, LL);
18
+ const cfg = { c: 32, t: 32, b: 8, layers: 2, heads: 2, steps: 120, lr: 0.02 };
19
+
20
+ (async function () {
21
+ const A = X.init(cfg, L, matmulInt8);
22
+ const B = X.init(cfg, L, matmulInt8);
23
+ console.log(`vocab=${X.VOCAB}, params=${A.nParams}, baseline loss=${Math.log(X.VOCAB).toFixed(3)}`);
24
+ const oa = T.makeAdam(A.nParams, { lr: cfg.lr });
25
+ const ob = T.makeAdam(B.nParams, { lr: cfg.lr });
26
+ let first = 0, loss = 0;
27
+ for (let s = 0; s < cfg.steps; s++) {
28
+ const ra = await X.trainStep(A);
29
+ const rb = await X.trainStep(B);
30
+ const avg = T.averageGrads([ra.grad, rb.grad]);
31
+ X.applyUpdate(A, oa.step(avg));
32
+ X.applyUpdate(B, ob.step(avg));
33
+ loss = (ra.loss + rb.loss) / 2;
34
+ if (s === 0) first = loss;
35
+ if (s % 30 === 0 || s === cfg.steps - 1) console.log(` step ${s} loss ${loss.toFixed(4)}`);
36
+ }
37
+ const pa = X.getFlatParams(A), pb = X.getFlatParams(B);
38
+ let diff = 0;
39
+ for (let i = 0; i < pa.length; i++) diff = Math.max(diff, Math.abs(pa[i] - pb[i]));
40
+ const sample = await X.generate(A, "the ", 60);
41
+ console.log(`sample: "${sample}"`);
42
+ console.log(`replica max param diff: ${diff.toExponential(3)}`);
43
+ const ok = loss < first * 0.75 && loss < Math.log(X.VOCAB) && diff === 0;
44
+ console.log(ok ? "TRANSFORMER TEST PASSED" : "TRANSFORMER TEST FAILED");
45
+ process.exit(ok ? 0 : 1);
46
+ })();