Web demo: mini transformer LM + scaling sliders
Browse files- web/public/app.js +65 -63
- web/public/index.html +10 -5
- web/public/transformer.js +311 -0
- web/test_transformer.js +46 -0
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 |
-
|
| 6 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 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 "
|
| 158 |
// DaisyChain's own format (not torch-pickle) — .pt extension for familiarity.
|
| 159 |
-
const CKPT_MAGIC = "
|
| 160 |
const CKPT_SENTINEL = -2; // wire: [int32 -2][checkpoint bytes]
|
| 161 |
|
| 162 |
function packCheckpoint() {
|
| 163 |
-
const
|
|
|
|
| 164 |
new Uint8Array(buf, 0, 8).set([...CKPT_MAGIC].map(c => c.charCodeAt(0)));
|
| 165 |
-
new Int32Array(buf, 8, 4).set([
|
| 166 |
-
new Float32Array(buf, 24
|
| 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 [
|
| 174 |
-
if (
|
| 175 |
-
|
| 176 |
-
|
| 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 (
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-${
|
| 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
|
| 207 |
function readCfgFromUI() {
|
| 208 |
-
return {
|
|
|
|
| 209 |
}
|
| 210 |
function showCfgInUI(cfg) {
|
| 211 |
-
|
| 212 |
-
ui.
|
| 213 |
-
ui.
|
|
|
|
| 214 |
}
|
| 215 |
function broadcastConfig(cfg) {
|
| 216 |
-
const buf = new ArrayBuffer(
|
| 217 |
-
new Int32Array(buf, 0,
|
| 218 |
-
new Float32Array(buf,
|
| 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 [,
|
| 224 |
-
const lr = new Float32Array(buf,
|
| 225 |
-
if (!(
|
|
|
|
| 226 |
log(`rejected bad settings from ${nmeOf(peerId)}`); return;
|
| 227 |
}
|
| 228 |
-
const cfg = {
|
| 229 |
showCfgInUI(cfg);
|
| 230 |
-
log(`${nmeOf(peerId)} started the group:
|
| 231 |
-
buildModel(cfg
|
| 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
|
| 285 |
-
|
| 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 |
-
|
| 294 |
const cohort = [...chans.keys()]; // lock the cohort (departed peers are pruned per-step)
|
| 295 |
const steps = cfg.steps;
|
| 296 |
-
const opt = TrainCore.makeAdam(
|
| 297 |
-
log(`training started — cohort ${cohort.length} peer(s), world ${cohort.length + 1},
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 332 |
-
//
|
| 333 |
-
function buildModel(
|
| 334 |
-
|
| 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.
|
|
|
|
| 391 |
el.oninput = () => document.getElementById(v).textContent = el.value;
|
| 392 |
-
buildModel(
|
| 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
|
| 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">
|
| 109 |
-
<input type="range" id="
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
<label class="slbl">Steps <span class="sval" id="vcfgSteps">300</span></label>
|
| 111 |
-
<input type="range" id="cfgSteps" min="100" max="
|
| 112 |
-
<label class="slbl">Learning rate ×
|
| 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
|
| 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 |
+
})();
|