Web trainer upgrade: sync guard (leader roster + weight-hash divergence check), 3xINT8 fast-accurate GEMM, DP4A hardware path (verified vs units), Spikewhale tokenizer (16.5k vocab), FineWeb-Edu streaming, gradient/checkpoint fragmentation, inference kit, generation tester, contribution logs
Browse files- web/public/app.js +316 -36
- web/public/index.html +19 -4
- web/public/tokenizer.json +0 -0
- web/public/transformer.js +91 -22
- web/public/verified_core.js +39 -1
- web/public/webgpu.js +109 -21
- web/test_transformer.js +2 -2
web/public/app.js
CHANGED
|
@@ -29,6 +29,12 @@ const ui = {
|
|
| 29 |
cfgB: document.getElementById("cfgB"),
|
| 30 |
cfgSteps: document.getElementById("cfgSteps"),
|
| 31 |
cfgLr: document.getElementById("cfgLr"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
};
|
| 33 |
function log(m) { ui.log.textContent = `${new Date().toLocaleTimeString()} ${m}\n` + ui.log.textContent; }
|
| 34 |
function setStatus(s) { ui.status.textContent = s; }
|
|
@@ -52,10 +58,24 @@ 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 |
|
| 57 |
function room() { return new URLSearchParams(location.search).get("room"); } // null -> group by network
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
// ---- signaling + WebRTC ----------------------------------------------------
|
| 61 |
function connectSignaling() {
|
|
@@ -75,6 +95,7 @@ function connectSignaling() {
|
|
| 75 |
else if (room()) setStatus(`accepted into private room "${room()}"`);
|
| 76 |
// I'm newest: initiate to everyone already here
|
| 77 |
for (const p of msg.peers) { names.set(p.id, p.name); initiatePeer(p.id); }
|
|
|
|
| 78 |
} else if (msg.type === "waiting") {
|
| 79 |
setStatus("knocking — waiting for the room's host to let you in…");
|
| 80 |
} else if (msg.type === "denied") {
|
|
@@ -88,6 +109,7 @@ function connectSignaling() {
|
|
| 88 |
addJoinRequest(msg.id, msg.name);
|
| 89 |
} else if (msg.type === "peer-joined") {
|
| 90 |
names.set(msg.id, msg.name);
|
|
|
|
| 91 |
log(`${msg.name} joined (they will connect to me)`);
|
| 92 |
} else if (msg.type === "peer-left") {
|
| 93 |
log(`${nmeOf(msg.id)} left`); cleanupPeer(msg.id); names.delete(msg.id); updatePeers();
|
|
@@ -173,8 +195,10 @@ 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.
|
| 177 |
-
|
|
|
|
|
|
|
| 178 |
return { c, t, steps, flat: new Float32Array(buf.slice(24)) };
|
| 179 |
}
|
| 180 |
function applyCheckpoint(ck, from) {
|
|
@@ -185,7 +209,7 @@ function applyCheckpoint(ck, from) {
|
|
| 185 |
if (ck.flat.length !== model.nParams) throw new Error("truncated checkpoint");
|
| 186 |
Transformer.setFlatParams(model, ck.flat);
|
| 187 |
trainedSteps = ck.steps;
|
| 188 |
-
|
| 189 |
ui.step.textContent = `${ck.steps} baked in`;
|
| 190 |
log(`checkpoint loaded (${ck.steps} steps) ${from ? "from " + from : "from file"} — all set to resume`);
|
| 191 |
}
|
|
@@ -195,9 +219,65 @@ function broadcastCheckpoint() {
|
|
| 195 |
new Int32Array(msg, 0, 1)[0] = CKPT_SENTINEL;
|
| 196 |
new Uint8Array(msg, 4).set(new Uint8Array(ck));
|
| 197 |
let n = 0;
|
| 198 |
-
for (const dc of chans.values()) if (dc.readyState === "open") {
|
| 199 |
-
log(`checkpoint pushed to ${n} device(s)`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
}
|
|
|
|
| 201 |
function saveCheckpoint() {
|
| 202 |
const blob = new Blob([packCheckpoint()], { type: "application/octet-stream" });
|
| 203 |
const a = document.createElement("a");
|
|
@@ -210,8 +290,10 @@ function saveCheckpoint() {
|
|
| 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; };
|
|
@@ -220,37 +302,109 @@ function showCfgInUI(cfg) {
|
|
| 220 |
set(ui.cfgLr, "vcfgLr", Math.round(cfg.lr * 1000));
|
| 221 |
}
|
| 222 |
function broadcastConfig(cfg) {
|
| 223 |
-
const buf = new ArrayBuffer(
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|
| 243 |
-
// ----
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
new Int32Array(buf, 0, 1)[0] = step;
|
| 247 |
-
new
|
|
|
|
|
|
|
| 248 |
return buf;
|
| 249 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
const waiters = new Set(); // pending waitForGrads checkers
|
| 251 |
function wake() { for (const w of waiters) w(); }
|
| 252 |
function onGrad(peerId, buf) {
|
| 253 |
const step = new Int32Array(buf, 0, 1)[0];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
if (step === CFG_SENTINEL) { onConfig(peerId, buf); return; }
|
| 255 |
if (step === CKPT_SENTINEL) { // a peer pushed a checkpoint
|
| 256 |
if (training) { log(`ignored checkpoint from ${nmeOf(peerId)} (training in progress)`); return; }
|
|
@@ -258,27 +412,35 @@ function onGrad(peerId, buf) {
|
|
| 258 |
catch (e) { log(`bad checkpoint from ${nmeOf(peerId)}: ${e.message}`); }
|
| 259 |
return;
|
| 260 |
}
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
if (!incoming.has(step)) incoming.set(step, new Map());
|
| 263 |
incoming.get(step).set(peerId, grad);
|
|
|
|
|
|
|
| 264 |
wake(); // resolve waits immediately (no polling)
|
| 265 |
}
|
| 266 |
-
function broadcastGrad(step, grad) {
|
| 267 |
// Event-driven: re-checked on every gradient arrival and peer departure, plus a
|
| 268 |
// coarse fallback timer (background tabs throttle timers to ~1s, so the old
|
| 269 |
// 15ms poll was the bottleneck there). Peers that left are dropped from the
|
| 270 |
// wait — a device dying no longer costs the full timeout every step.
|
| 271 |
-
function
|
| 272 |
return new Promise((resolve) => {
|
| 273 |
const t0 = Date.now();
|
| 274 |
let timer = null;
|
| 275 |
const check = () => {
|
| 276 |
-
const
|
| 277 |
-
|
| 278 |
-
const have = live.filter(id => got.has(id));
|
| 279 |
-
if (have.length === live.length || Date.now() - t0 > timeoutMs) {
|
| 280 |
waiters.delete(check); clearInterval(timer);
|
| 281 |
-
resolve(
|
| 282 |
}
|
| 283 |
};
|
| 284 |
waiters.add(check);
|
|
@@ -286,6 +448,15 @@ function waitForGrads(step, cohort, timeoutMs = 8000) {
|
|
| 286 |
check();
|
| 287 |
});
|
| 288 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
// ---- compute: one async training step THROUGH the verified units -----------
|
| 291 |
async function localStep() {
|
|
@@ -297,40 +468,117 @@ async function localStep() {
|
|
| 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);
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) {
|
| 316 |
-
ui.loss.textContent =
|
| 317 |
ui.step.textContent = `${s + 1} / ${steps}`;
|
| 318 |
ui.bar.style.width = `${Math.round(100 * (s + 1) / steps)}%`;
|
| 319 |
await new Promise(r => setTimeout(r, 0)); // yield to UI
|
| 320 |
}
|
|
|
|
|
|
|
|
|
|
| 321 |
}
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
}
|
|
|
|
|
|
|
| 329 |
training = false;
|
| 330 |
-
|
| 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
|
|
@@ -386,12 +634,25 @@ function buildModel(cfg) {
|
|
| 386 |
ui.start.disabled = true;
|
| 387 |
return; // no signaling, no training
|
| 388 |
}
|
| 389 |
-
ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label} · through verified
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = "";
|
|
@@ -404,13 +665,32 @@ function buildModel(cfg) {
|
|
| 404 |
}
|
| 405 |
updatePeers();
|
| 406 |
connectSignaling();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
ui.start.onclick = () => {
|
| 408 |
const cfg = readCfgFromUI();
|
|
|
|
| 409 |
buildModel(cfg);
|
| 410 |
broadcastConfig(cfg); // everyone follows these settings
|
| 411 |
train(cfg);
|
| 412 |
};
|
| 413 |
ui.save.onclick = saveCheckpoint;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
ui.loadBtn.onclick = () => { if (training) { log("can't load a checkpoint mid-training"); return; } ui.load.click(); };
|
| 415 |
ui.load.onchange = async () => {
|
| 416 |
const f = ui.load.files[0]; ui.load.value = "";
|
|
|
|
| 29 |
cfgB: document.getElementById("cfgB"),
|
| 30 |
cfgSteps: document.getElementById("cfgSteps"),
|
| 31 |
cfgLr: document.getElementById("cfgLr"),
|
| 32 |
+
dataset: document.getElementById("dataset"),
|
| 33 |
+
tokenizer: document.getElementById("tokenizer"),
|
| 34 |
+
kit: document.getElementById("kit"),
|
| 35 |
+
genPrompt: document.getElementById("genPrompt"),
|
| 36 |
+
genBtn: document.getElementById("genBtn"),
|
| 37 |
+
genOut: document.getElementById("genOut"),
|
| 38 |
};
|
| 39 |
function log(m) { ui.log.textContent = `${new Date().toLocaleTimeString()} ${m}\n` + ui.log.textContent; }
|
| 40 |
function setStatus(s) { ui.status.textContent = s; }
|
|
|
|
| 58 |
const incoming = new Map(); // step -> Map(peerId -> Float32Array)
|
| 59 |
let model = null, training = false; // the mini transformer (transformer.js)
|
| 60 |
let trainedSteps = 0; // steps baked into the current weights
|
| 61 |
+
// ---- sync guard state -------------------------------------------------------
|
| 62 |
+
// The peer that presses Start is the round leader. Every step the leader
|
| 63 |
+
// publishes the exact set of contributors to average (the roster); followers
|
| 64 |
+
// apply that set verbatim or stop — no device may silently average a
|
| 65 |
+
// different set, which is what used to fork the weights.
|
| 66 |
+
let leaderId = null; // null => I lead (I pressed Start)
|
| 67 |
+
const rosters = new Map(); // step -> [peerId,...] from the leader
|
| 68 |
+
const peerHashes = new Map(); // step -> Map(peerId -> uint32 pre-step weight hash)
|
| 69 |
function nmeOf(id) { return names.get(id) || id; }
|
| 70 |
|
| 71 |
function room() { return new URLSearchParams(location.search).get("room"); } // null -> group by network
|
| 72 |
+
// show EVERY device the room knows about, not just open data channels —
|
| 73 |
+
// a peer whose WebRTC is still connecting (or failed) must stay visible
|
| 74 |
+
function updatePeers() {
|
| 75 |
+
if (!names.size) { ui.peers.textContent = "(none yet)"; return; }
|
| 76 |
+
ui.peers.textContent = [...names.entries()]
|
| 77 |
+
.map(([id, n]) => `${n} ${chans.has(id) ? "✓" : "(connecting…)"}`).join(", ");
|
| 78 |
+
}
|
| 79 |
|
| 80 |
// ---- signaling + WebRTC ----------------------------------------------------
|
| 81 |
function connectSignaling() {
|
|
|
|
| 95 |
else if (room()) setStatus(`accepted into private room "${room()}"`);
|
| 96 |
// I'm newest: initiate to everyone already here
|
| 97 |
for (const p of msg.peers) { names.set(p.id, p.name); initiatePeer(p.id); }
|
| 98 |
+
updatePeers();
|
| 99 |
} else if (msg.type === "waiting") {
|
| 100 |
setStatus("knocking — waiting for the room's host to let you in…");
|
| 101 |
} else if (msg.type === "denied") {
|
|
|
|
| 109 |
addJoinRequest(msg.id, msg.name);
|
| 110 |
} else if (msg.type === "peer-joined") {
|
| 111 |
names.set(msg.id, msg.name);
|
| 112 |
+
updatePeers();
|
| 113 |
log(`${msg.name} joined (they will connect to me)`);
|
| 114 |
} else if (msg.type === "peer-left") {
|
| 115 |
log(`${nmeOf(msg.id)} left`); cleanupPeer(msg.id); names.delete(msg.id); updatePeers();
|
|
|
|
| 195 |
const magic = String.fromCharCode(...new Uint8Array(buf, 0, 8));
|
| 196 |
if (magic !== CKPT_MAGIC) throw new Error("not a DaisyChain v2 checkpoint");
|
| 197 |
const [c, t, vocab, steps] = new Int32Array(buf, 8, 4);
|
| 198 |
+
if (vocab !== Transformer.vocabSize())
|
| 199 |
+
throw new Error(`tokenizer mismatch — checkpoint has a ${vocab}-token vocab, this device loaded ` +
|
| 200 |
+
`${Transformer.vocabSize()} (${Transformer.tokenizerName()}); use matching builds`);
|
| 201 |
+
if (c < 16 || c > 128 || t < 16 || t > 128) throw new Error(`bad dims in checkpoint (width ${c}, seq ${t})`);
|
| 202 |
return { c, t, steps, flat: new Float32Array(buf.slice(24)) };
|
| 203 |
}
|
| 204 |
function applyCheckpoint(ck, from) {
|
|
|
|
| 209 |
if (ck.flat.length !== model.nParams) throw new Error("truncated checkpoint");
|
| 210 |
Transformer.setFlatParams(model, ck.flat);
|
| 211 |
trainedSteps = ck.steps;
|
| 212 |
+
modelReady();
|
| 213 |
ui.step.textContent = `${ck.steps} baked in`;
|
| 214 |
log(`checkpoint loaded (${ck.steps} steps) ${from ? "from " + from : "from file"} — all set to resume`);
|
| 215 |
}
|
|
|
|
| 219 |
new Int32Array(msg, 0, 1)[0] = CKPT_SENTINEL;
|
| 220 |
new Uint8Array(msg, 4).set(new Uint8Array(ck));
|
| 221 |
let n = 0;
|
| 222 |
+
for (const dc of chans.values()) if (dc.readyState === "open") { dcSend(dc, msg); n++; }
|
| 223 |
+
log(`checkpoint pushed to ${n} device(s) (${(msg.byteLength / 1048576).toFixed(1)} MB)`);
|
| 224 |
+
}
|
| 225 |
+
// ---- inference kit: one self-contained HTML file with the trained weights --
|
| 226 |
+
// Bundles the model code + current checkpoint (base64) + a prompt box into a
|
| 227 |
+
// single file that runs generations offline — no server, nothing to install.
|
| 228 |
+
// The mul8 LUT is rebuilt in the file itself (it is exactly a×b for int8).
|
| 229 |
+
async function downloadInferenceKit() {
|
| 230 |
+
const srcs = await Promise.all(["traincore.js", "verified_core.js", "transformer.js"]
|
| 231 |
+
.map(f => fetch(f).then(r => r.text())));
|
| 232 |
+
let tokData = "null"; // embed the tokenizer (offline file)
|
| 233 |
+
try { tokData = await (await fetch("tokenizer.json")).text(); } catch (e) {}
|
| 234 |
+
const ck = new Uint8Array(packCheckpoint());
|
| 235 |
+
let b64 = ""; const CH = 0x8000;
|
| 236 |
+
for (let i = 0; i < ck.length; i += CH) b64 += String.fromCharCode(...ck.subarray(i, i + CH));
|
| 237 |
+
b64 = btoa(b64);
|
| 238 |
+
const esc = (s) => s.replace(/<\/script/gi, "<\\/script");
|
| 239 |
+
const html = `<!doctype html><html><head><meta charset="utf-8">
|
| 240 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 241 |
+
<title>DaisyChain inference — ${trainedSteps} steps</title>
|
| 242 |
+
<style>body{font-family:sans-serif;max-width:680px;margin:0 auto;padding:24px 16px;background:#efe4c9;color:#2a1d0a}
|
| 243 |
+
input{width:100%;padding:10px;border-radius:8px;border:1px solid #8b6f47;font-family:monospace;box-sizing:border-box}
|
| 244 |
+
button{margin-top:10px;padding:10px 24px;border:0;border-radius:8px;background:#4a7c2e;color:#f5ecd9;font-weight:700;cursor:pointer}
|
| 245 |
+
pre{background:#fbf6e8;border-radius:8px;padding:12px;white-space:pre-wrap;min-height:60px}</style></head><body>
|
| 246 |
+
<h2>🌼 DaisyChain model — inference</h2>
|
| 247 |
+
<p>Trained ${trainedSteps} steps · width ${model.cfg.c} · seq ${model.cfg.t} · dataset: ${Transformer.datasetName()}.
|
| 248 |
+
Runs entirely in this file through the verified INT8 units (CPU LUT).</p>
|
| 249 |
+
<input id="p" value="the "><button id="g">Generate</button><pre id="o"></pre>
|
| 250 |
+
<script>${esc(srcs[0])}<\/script><script>${esc(srcs[1])}<\/script><script>${esc(srcs[2])}<\/script>
|
| 251 |
+
<script>
|
| 252 |
+
const mul = new Int16Array(65536);
|
| 253 |
+
for (let a = 0; a < 256; a++) for (let b = 0; b < 256; b++)
|
| 254 |
+
mul[a*256+b] = ((a>127?a-256:a) * (b>127?b-256:b));
|
| 255 |
+
const L = { mul };
|
| 256 |
+
const TOKDATA = ${esc(tokData)};
|
| 257 |
+
if (TOKDATA) Transformer.loadTokenizerData(TOKDATA);
|
| 258 |
+
const bytes = Uint8Array.from(atob("${b64}"), c => c.charCodeAt(0));
|
| 259 |
+
const buf = bytes.buffer;
|
| 260 |
+
const dims = new Int32Array(buf, 8, 4); // c, t, vocab, steps
|
| 261 |
+
const m = Transformer.init({ c: dims[0], t: dims[1], b: 1, steps: 0, lr: 0 }, L,
|
| 262 |
+
(Xq, Wq, mm, kk, nn, LL) => Verified.lutMatmulJS(Xq, Wq, mm, kk, nn, LL));
|
| 263 |
+
Transformer.setFlatParams(m, new Float32Array(buf.slice(24)));
|
| 264 |
+
document.getElementById("g").onclick = async () => {
|
| 265 |
+
const b = document.getElementById("g"), o = document.getElementById("o");
|
| 266 |
+
b.disabled = true; o.textContent = "generating…";
|
| 267 |
+
try { o.textContent = await Transformer.generate(m, document.getElementById("p").value || "the ", 150); }
|
| 268 |
+
catch (e) { o.textContent = "error: " + e.message; }
|
| 269 |
+
b.disabled = false;
|
| 270 |
+
};
|
| 271 |
+
<\/script></body></html>`;
|
| 272 |
+
const blob = new Blob([html], { type: "text/html" });
|
| 273 |
+
const a = document.createElement("a");
|
| 274 |
+
a.href = URL.createObjectURL(blob);
|
| 275 |
+
a.download = `daisychain-inference-step${trainedSteps}.html`;
|
| 276 |
+
a.click();
|
| 277 |
+
URL.revokeObjectURL(a.href);
|
| 278 |
+
log("inference kit downloaded — a single HTML file: open it anywhere, type a prompt, generate");
|
| 279 |
}
|
| 280 |
+
|
| 281 |
function saveCheckpoint() {
|
| 282 |
const blob = new Blob([packCheckpoint()], { type: "application/octet-stream" });
|
| 283 |
const a = document.createElement("a");
|
|
|
|
| 290 |
// ---- training config: whoever presses Start sets it for the whole group ----
|
| 291 |
const CFG_SENTINEL = -3; // wire: [int32 -3][int32 c,t,b,steps][f32 lr]
|
| 292 |
function readCfgFromUI() {
|
| 293 |
+
// lr crosses the wire as float32 — fround here so the leader trains with
|
| 294 |
+
// the exact same value the followers decode (else Adam forks the weights)
|
| 295 |
return { c: +ui.cfgC.value, t: +ui.cfgT.value, b: +ui.cfgB.value,
|
| 296 |
+
steps: +ui.cfgSteps.value, lr: Math.fround(+ui.cfgLr.value / 1000) };
|
| 297 |
}
|
| 298 |
function showCfgInUI(cfg) {
|
| 299 |
const set = (el, vid, val) => { el.value = val; document.getElementById(vid).textContent = val; };
|
|
|
|
| 302 |
set(ui.cfgLr, "vcfgLr", Math.round(cfg.lr * 1000));
|
| 303 |
}
|
| 304 |
function broadcastConfig(cfg) {
|
| 305 |
+
const buf = new ArrayBuffer(28);
|
| 306 |
new Int32Array(buf, 0, 5).set([CFG_SENTINEL, cfg.c, cfg.t, cfg.b, cfg.steps]);
|
| 307 |
new Float32Array(buf, 20, 1)[0] = cfg.lr;
|
| 308 |
+
new Int32Array(buf, 24, 1)[0] = Transformer.vocabSize(); // tokenizer must match
|
| 309 |
+
broadcast(buf);
|
| 310 |
}
|
| 311 |
function onConfig(peerId, buf) {
|
| 312 |
if (training) { log(`ignored settings from ${nmeOf(peerId)} (already training)`); return; }
|
| 313 |
const [, c, t, b, steps] = new Int32Array(buf, 0, 5);
|
| 314 |
const lr = new Float32Array(buf, 20, 1)[0];
|
| 315 |
+
const vocab = buf.byteLength >= 28 ? new Int32Array(buf, 24, 1)[0] : -1;
|
| 316 |
+
if (vocab !== Transformer.vocabSize()) {
|
| 317 |
+
log(`NOT JOINING: ${nmeOf(peerId)} uses a ${vocab}-token tokenizer, this device has ` +
|
| 318 |
+
`${Transformer.vocabSize()} (${Transformer.tokenizerName()}) — refresh so all devices match`);
|
| 319 |
+
return;
|
| 320 |
+
}
|
| 321 |
+
if (!(c >= 16 && c <= 128 && c % 2 === 0 && t >= 16 && t <= 128 && b >= 1 && b <= 32 &&
|
| 322 |
steps >= 1 && steps <= 10000 && lr > 0 && lr <= 0.2)) {
|
| 323 |
log(`rejected bad settings from ${nmeOf(peerId)}`); return;
|
| 324 |
}
|
| 325 |
const cfg = { c, t, b, steps, lr };
|
| 326 |
+
leaderId = peerId; // the starter leads sync for this run
|
| 327 |
showCfgInUI(cfg);
|
| 328 |
log(`${nmeOf(peerId)} started the group: width=${c}, seq=${t}, batch=${b}, ${steps} steps, lr=${lr}`);
|
| 329 |
buildModel(cfg);
|
| 330 |
train(cfg); // follow automatically
|
| 331 |
}
|
| 332 |
|
| 333 |
+
// ---- big-message fragmentation ----------------------------------------------
|
| 334 |
+
// WebRTC data channels cap a single message (~256KB Chrome, 64KB elsewhere).
|
| 335 |
+
// Gradients with the Spikewhale vocab are multi-MB, and checkpoints always
|
| 336 |
+
// were at width ≥ 64 — so anything large goes out in 48KB chunks:
|
| 337 |
+
// [int32 -5][int32 msgId][int32 seq][int32 total][bytes...]
|
| 338 |
+
// Channels are ordered+reliable, so chunks arrive in order per peer.
|
| 339 |
+
const FRAG_SENTINEL = -5, FRAG_CHUNK = 48 * 1024;
|
| 340 |
+
let fragSeq = 1;
|
| 341 |
+
const fragIn = new Map(); // peerId -> {id, parts, got, total}
|
| 342 |
+
function dcSend(dc, buf) {
|
| 343 |
+
if (buf.byteLength <= FRAG_CHUNK) { dc.send(buf); return; }
|
| 344 |
+
const id = fragSeq++, src = new Uint8Array(buf);
|
| 345 |
+
const total = Math.ceil(src.length / FRAG_CHUNK);
|
| 346 |
+
for (let s = 0; s < total; s++) {
|
| 347 |
+
const part = src.subarray(s * FRAG_CHUNK, Math.min((s + 1) * FRAG_CHUNK, src.length));
|
| 348 |
+
const msg = new ArrayBuffer(16 + part.length);
|
| 349 |
+
new Int32Array(msg, 0, 4).set([FRAG_SENTINEL, id, s, total]);
|
| 350 |
+
new Uint8Array(msg, 16).set(part);
|
| 351 |
+
dc.send(msg);
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
function broadcast(buf) { for (const dc of chans.values()) if (dc.readyState === "open") dcSend(dc, buf); }
|
| 355 |
+
function onFragment(peerId, buf) { // returns full message when complete
|
| 356 |
+
const [, id, seq, total] = new Int32Array(buf, 0, 4);
|
| 357 |
+
let st = fragIn.get(peerId);
|
| 358 |
+
if (!st || st.id !== id) { st = { id, parts: [], got: 0, total }; fragIn.set(peerId, st); }
|
| 359 |
+
st.parts[seq] = new Uint8Array(buf, 16).slice(0);
|
| 360 |
+
st.got++;
|
| 361 |
+
if (st.got < st.total) return null;
|
| 362 |
+
fragIn.delete(peerId);
|
| 363 |
+
let len = 0; for (const p of st.parts) len += p.length;
|
| 364 |
+
const out = new Uint8Array(len);
|
| 365 |
+
let off = 0; for (const p of st.parts) { out.set(p, off); off += p.length; }
|
| 366 |
+
return out.buffer;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
// ---- gradient wire format: [int32 step][uint32 whash][f32 loss][f32 grad...]
|
| 370 |
+
// whash = FNV-1a of the sender's weights BEFORE this step's update, so every
|
| 371 |
+
// peer can verify the whole group is still training the same model.
|
| 372 |
+
// loss = the sender's local batch loss, so everyone can show the true
|
| 373 |
+
// cluster-average loss and log what each device contributes.
|
| 374 |
+
function packGrad(step, whash, loss, grad) {
|
| 375 |
+
const buf = new ArrayBuffer(12 + grad.byteLength);
|
| 376 |
new Int32Array(buf, 0, 1)[0] = step;
|
| 377 |
+
new Uint32Array(buf, 4, 1)[0] = whash;
|
| 378 |
+
new Float32Array(buf, 8, 1)[0] = loss;
|
| 379 |
+
new Float32Array(buf, 12).set(grad);
|
| 380 |
return buf;
|
| 381 |
}
|
| 382 |
+
function hashWeights() { // FNV-1a over the flat param bytes
|
| 383 |
+
const f = Transformer.getFlatParams(model);
|
| 384 |
+
const b = new Uint8Array(f.buffer, f.byteOffset, f.byteLength);
|
| 385 |
+
let h = 0x811c9dc5;
|
| 386 |
+
for (let i = 0; i < b.length; i++) { h ^= b[i]; h = Math.imul(h, 0x01000193); }
|
| 387 |
+
return h >>> 0;
|
| 388 |
+
}
|
| 389 |
+
// roster wire: [int32 -4][int32 step][int32 n][int32 peerNums...]
|
| 390 |
+
const ROSTER_SENTINEL = -4;
|
| 391 |
+
function broadcastRoster(step, ids) {
|
| 392 |
+
const nums = ids.map(id => +id.slice(1)); // "p12" -> 12
|
| 393 |
+
const buf = new ArrayBuffer(12 + 4 * nums.length);
|
| 394 |
+
const iv = new Int32Array(buf);
|
| 395 |
+
iv[0] = ROSTER_SENTINEL; iv[1] = step; iv[2] = nums.length;
|
| 396 |
+
iv.set(nums, 3);
|
| 397 |
+
broadcast(buf);
|
| 398 |
+
}
|
| 399 |
const waiters = new Set(); // pending waitForGrads checkers
|
| 400 |
function wake() { for (const w of waiters) w(); }
|
| 401 |
function onGrad(peerId, buf) {
|
| 402 |
const step = new Int32Array(buf, 0, 1)[0];
|
| 403 |
+
if (step === FRAG_SENTINEL) { // chunk of a large message
|
| 404 |
+
const whole = onFragment(peerId, buf);
|
| 405 |
+
if (whole) onGrad(peerId, whole);
|
| 406 |
+
return;
|
| 407 |
+
}
|
| 408 |
if (step === CFG_SENTINEL) { onConfig(peerId, buf); return; }
|
| 409 |
if (step === CKPT_SENTINEL) { // a peer pushed a checkpoint
|
| 410 |
if (training) { log(`ignored checkpoint from ${nmeOf(peerId)} (training in progress)`); return; }
|
|
|
|
| 412 |
catch (e) { log(`bad checkpoint from ${nmeOf(peerId)}: ${e.message}`); }
|
| 413 |
return;
|
| 414 |
}
|
| 415 |
+
if (step === ROSTER_SENTINEL) { // the leader's contributor set for a step
|
| 416 |
+
if (training && peerId !== leaderId) return; // only the run's leader may steer sync
|
| 417 |
+
const iv = new Int32Array(buf);
|
| 418 |
+
rosters.set(iv[1], [...iv.slice(3, 3 + iv[2])].map(n => "p" + n));
|
| 419 |
+
wake(); return;
|
| 420 |
+
}
|
| 421 |
+
const whash = new Uint32Array(buf, 4, 1)[0];
|
| 422 |
+
const loss = new Float32Array(buf, 8, 1)[0];
|
| 423 |
+
const grad = new Float32Array(buf.slice(12));
|
| 424 |
if (!incoming.has(step)) incoming.set(step, new Map());
|
| 425 |
incoming.get(step).set(peerId, grad);
|
| 426 |
+
if (!peerHashes.has(step)) peerHashes.set(step, new Map());
|
| 427 |
+
peerHashes.get(step).set(peerId, { hash: whash, loss });
|
| 428 |
wake(); // resolve waits immediately (no polling)
|
| 429 |
}
|
| 430 |
+
function broadcastGrad(step, whash, loss, grad) { broadcast(packGrad(step, whash, loss, grad)); }
|
| 431 |
// Event-driven: re-checked on every gradient arrival and peer departure, plus a
|
| 432 |
// coarse fallback timer (background tabs throttle timers to ~1s, so the old
|
| 433 |
// 15ms poll was the bottleneck there). Peers that left are dropped from the
|
| 434 |
// wait — a device dying no longer costs the full timeout every step.
|
| 435 |
+
function waitFor(pred, timeoutMs) { // resolves true if pred held, false on timeout
|
| 436 |
return new Promise((resolve) => {
|
| 437 |
const t0 = Date.now();
|
| 438 |
let timer = null;
|
| 439 |
const check = () => {
|
| 440 |
+
const ok = pred();
|
| 441 |
+
if (ok || Date.now() - t0 > timeoutMs) {
|
|
|
|
|
|
|
| 442 |
waiters.delete(check); clearInterval(timer);
|
| 443 |
+
resolve(!!ok);
|
| 444 |
}
|
| 445 |
};
|
| 446 |
waiters.add(check);
|
|
|
|
| 448 |
check();
|
| 449 |
});
|
| 450 |
}
|
| 451 |
+
async function waitForGradIds(step, cohort, timeoutMs = 8000) {
|
| 452 |
+
await waitFor(() => {
|
| 453 |
+
const live = cohort.filter(id => chans.has(id)); // prune departed peers
|
| 454 |
+
const got = incoming.get(step) || new Map();
|
| 455 |
+
return live.every(id => got.has(id));
|
| 456 |
+
}, timeoutMs);
|
| 457 |
+
const got = incoming.get(step) || new Map();
|
| 458 |
+
return cohort.filter(id => chans.has(id) && got.has(id));
|
| 459 |
+
}
|
| 460 |
|
| 461 |
// ---- compute: one async training step THROUGH the verified units -----------
|
| 462 |
async function localStep() {
|
|
|
|
| 468 |
async function train(cfg) {
|
| 469 |
if (training) return; training = true; ui.start.disabled = true;
|
| 470 |
cfgSliders().forEach(el => el.disabled = true);
|
| 471 |
+
incoming.clear(); rosters.clear(); peerHashes.clear(); // no stale grads from a previous run
|
| 472 |
+
const iLead = leaderId === null; // set by Start (null) or onConfig (starter's id)
|
| 473 |
const cohort = [...chans.keys()]; // lock the cohort (departed peers are pruned per-step)
|
| 474 |
const steps = cfg.steps;
|
| 475 |
const opt = TrainCore.makeAdam(model.nParams, { lr: cfg.lr });
|
| 476 |
log(`training started — cohort ${cohort.length} peer(s), world ${cohort.length + 1}, ` +
|
| 477 |
+
`width=${cfg.c} seq=${cfg.t} batch=${cfg.b}×${cohort.length + 1}, optimizer ${opt.name}` +
|
| 478 |
+
(cohort.length ? `, sync ${iLead ? "led by me" : "led by " + nmeOf(leaderId)}` : "") +
|
| 479 |
+
` · data: ${Transformer.datasetName()}` +
|
| 480 |
+
` · grad payload ${(model.nParams * 4 / 1048576).toFixed(1)} MB/step/peer`);
|
| 481 |
+
if ((cfg.c > 64 || cfg.t > 64) && cohort.length + 1 < 4)
|
| 482 |
+
log(`⚠ large model (width ${cfg.c}, seq ${cfg.t}) with only ${cohort.length + 1} device(s) — ` +
|
| 483 |
+
`steps will be slow and the effective batch small. 4+ devices recommended at this size.`);
|
| 484 |
+
let halted = null; // sync-guard stop reason
|
| 485 |
+
const contrib = new Map([[myId, 0]]); // peerId -> grads contributed
|
| 486 |
+
for (const id of cohort) contrib.set(id, 0);
|
| 487 |
+
try {
|
| 488 |
for (let s = 0; s < steps; s++) {
|
| 489 |
+
const whash = hashWeights(); // pre-step fingerprint, sent with the grad
|
| 490 |
const { loss, grad } = await localStep();
|
| 491 |
+
broadcastGrad(s, whash, loss, grad);
|
| 492 |
+
let all;
|
| 493 |
+
if (!cohort.length) {
|
| 494 |
+
all = [grad]; // solo run
|
| 495 |
+
} else if (iLead) {
|
| 496 |
+
const ids = await waitForGradIds(s, cohort);
|
| 497 |
+
// sync guard: publish the exact contributor set so every device
|
| 498 |
+
// averages the same gradients (or stops) — never a silent fork
|
| 499 |
+
broadcastRoster(s, [myId, ...ids]);
|
| 500 |
+
all = [grad, ...ids.map(id => incoming.get(s).get(id))];
|
| 501 |
+
rosters.set(s, [myId, ...ids]);
|
| 502 |
+
} else {
|
| 503 |
+
// follower: apply the leader's roster verbatim, or stop
|
| 504 |
+
await waitFor(() => rosters.has(s) || !chans.has(leaderId), 15000);
|
| 505 |
+
if (!rosters.has(s)) {
|
| 506 |
+
halted = chans.has(leaderId)
|
| 507 |
+
? `no roster from ${nmeOf(leaderId)} for step ${s + 1}`
|
| 508 |
+
: `the sync leader (${nmeOf(leaderId)}) disconnected`;
|
| 509 |
+
break;
|
| 510 |
+
}
|
| 511 |
+
const roster = rosters.get(s);
|
| 512 |
+
const need = roster.filter(id => id !== myId);
|
| 513 |
+
const ok = await waitFor(() => need.every(id => (incoming.get(s) || new Map()).has(id)), 10000);
|
| 514 |
+
if (!ok) {
|
| 515 |
+
halted = `missing a roster gradient at step ${s + 1} — applying a partial average would fork the weights`;
|
| 516 |
+
break;
|
| 517 |
+
}
|
| 518 |
+
all = need.map(id => incoming.get(s).get(id));
|
| 519 |
+
if (roster.includes(myId)) all.unshift(grad); // leader may have dropped my late grad — then I skip it too
|
| 520 |
+
}
|
| 521 |
+
// divergence check: every contributor's pre-step weight hash must match mine
|
| 522 |
+
const hs = peerHashes.get(s);
|
| 523 |
+
const roster = rosters.get(s) || [myId];
|
| 524 |
+
if (hs) {
|
| 525 |
+
const bad = roster.find(id => id !== myId && hs.has(id) && hs.get(id).hash !== whash);
|
| 526 |
+
if (bad) { halted = `weights diverged from ${nmeOf(bad)} (detected at step ${s + 1})`; break; }
|
| 527 |
+
}
|
| 528 |
+
// cluster-average loss over this step's actual contributors
|
| 529 |
+
let lossSum = 0, lossN = 0;
|
| 530 |
+
for (const id of roster) {
|
| 531 |
+
if (id === myId) { lossSum += loss; lossN++; contrib.set(myId, (contrib.get(myId) || 0) + 1); }
|
| 532 |
+
else if (hs && hs.has(id)) { lossSum += hs.get(id).loss; lossN++; contrib.set(id, (contrib.get(id) || 0) + 1); }
|
| 533 |
+
}
|
| 534 |
+
const clusterLoss = lossSum / Math.max(1, lossN);
|
| 535 |
const avg = TrainCore.averageGrads(all);
|
| 536 |
const upd = opt.step(avg); // DaisyAdam on the cluster-avg grad
|
| 537 |
Transformer.applyUpdate(model, upd); // W -= upd (lr folded into upd)
|
| 538 |
+
incoming.delete(s); rosters.delete(s); peerHashes.delete(s);
|
| 539 |
trainedSteps++;
|
| 540 |
if (s % 10 === 0 || s === steps - 1) {
|
| 541 |
+
ui.loss.textContent = clusterLoss.toFixed(5);
|
| 542 |
ui.step.textContent = `${s + 1} / ${steps}`;
|
| 543 |
ui.bar.style.width = `${Math.round(100 * (s + 1) / steps)}%`;
|
| 544 |
await new Promise(r => setTimeout(r, 0)); // yield to UI
|
| 545 |
}
|
| 546 |
+
if (cohort.length && (s + 1) % 50 === 0) // who is contributing what
|
| 547 |
+
log(`step ${s + 1}: contributions — ` +
|
| 548 |
+
[...contrib.entries()].map(([id, n]) => `${id === myId ? "me" : nmeOf(id)} ${n}/${s + 1}`).join(", "));
|
| 549 |
}
|
| 550 |
+
} catch (e) { // a device that errors REPORTS it
|
| 551 |
+
halted = `error during training: ${e.message}`; // (instead of freezing at "—")
|
| 552 |
+
console.error(e);
|
| 553 |
+
}
|
| 554 |
+
if (cohort.length)
|
| 555 |
+
log(`contribution totals — ` +
|
| 556 |
+
[...contrib.entries()].map(([id, n]) => `${id === myId ? "me" : nmeOf(id)} ${n} grad(s)`).join(", "));
|
| 557 |
+
if (halted) {
|
| 558 |
+
log(`SYNC GUARD: stopped — ${halted}. The model here is intact (${trainedSteps} steps); ` +
|
| 559 |
+
`to re-sync the group, load/push a checkpoint and start again.`);
|
| 560 |
+
ui.diff.textContent = "stopped by the sync guard — see log";
|
| 561 |
+
} else {
|
| 562 |
+
log(`training done — final loss ${ui.loss.textContent}`);
|
| 563 |
+
try {
|
| 564 |
+
const sample = await Transformer.generate(model, "the ", 70);
|
| 565 |
+
ui.diff.textContent = `the model speaks: “${sample.trim()}”`;
|
| 566 |
+
} catch (e) {
|
| 567 |
+
ui.diff.textContent = "done — trained through the verified units; all peers share one model.";
|
| 568 |
+
}
|
| 569 |
}
|
| 570 |
+
incoming.clear(); rosters.clear(); peerHashes.clear();
|
| 571 |
+
leaderId = null;
|
| 572 |
training = false;
|
| 573 |
+
modelReady();
|
| 574 |
ui.start.disabled = false;
|
| 575 |
cfgSliders().forEach(el => el.disabled = false);
|
| 576 |
}
|
| 577 |
+
function modelReady() { // trained weights exist: enable save/kit/generate
|
| 578 |
+
ui.save.disabled = false;
|
| 579 |
+
ui.kit.disabled = false;
|
| 580 |
+
ui.genBtn.disabled = false;
|
| 581 |
+
}
|
| 582 |
function cfgSliders() { return [ui.cfgC, ui.cfgT, ui.cfgB, ui.cfgSteps, ui.cfgLr]; }
|
| 583 |
|
| 584 |
// (re)build the mini transformer — deterministic shared init (same seeds on
|
|
|
|
| 634 |
ui.start.disabled = true;
|
| 635 |
return; // no signaling, no training
|
| 636 |
}
|
| 637 |
+
ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label} · 3×INT8 fast-accurate through verified units`;
|
| 638 |
+
// tokenizer must load BEFORE the model is built (it sets the vocab size)
|
| 639 |
+
try {
|
| 640 |
+
const name = await Transformer.loadTokenizer();
|
| 641 |
+
ui.tokenizer.textContent = name;
|
| 642 |
+
log(`tokenizer: ${name}`);
|
| 643 |
+
} catch (e) {
|
| 644 |
+
ui.tokenizer.textContent = Transformer.tokenizerName();
|
| 645 |
+
log(`tokenizer.json unavailable (${e.message}) — using the ${Transformer.tokenizerName()} vocab`);
|
| 646 |
+
}
|
| 647 |
+
if (compute.backend === "cpu" && Transformer.vocabSize() > 1000)
|
| 648 |
+
log(`⚠ CPU backend with a ${Transformer.vocabSize()}-token vocab — steps will take seconds; ` +
|
| 649 |
+
`a WebGPU-capable browser will be much faster`);
|
| 650 |
// slider readouts
|
| 651 |
for (const [el, v] of [[ui.cfgC, "vcfgC"], [ui.cfgT, "vcfgT"], [ui.cfgB, "vcfgB"],
|
| 652 |
[ui.cfgSteps, "vcfgSteps"], [ui.cfgLr, "vcfgLr"]])
|
| 653 |
el.oninput = () => document.getElementById(v).textContent = el.value;
|
| 654 |
buildModel(readCfgFromUI());
|
| 655 |
+
ui.start.disabled = false; // solo training works too
|
| 656 |
ui.me.textContent = deviceName;
|
| 657 |
if (room()) {
|
| 658 |
ui.roomInfo.style.display = "";
|
|
|
|
| 665 |
}
|
| 666 |
updatePeers();
|
| 667 |
connectSignaling();
|
| 668 |
+
// dataset: stream FineWeb-Edu from HuggingFace; built-in corpus if offline
|
| 669 |
+
ui.dataset.textContent = "streaming FineWeb-Edu…";
|
| 670 |
+
Transformer.streamFineWebEdu().then(({ name, chars }) => {
|
| 671 |
+
ui.dataset.textContent = "FineWeb-Edu (streamed)";
|
| 672 |
+
log(`dataset: ${name} — ${(chars / 1000).toFixed(0)}k chars loaded (each device streams its own slice)`);
|
| 673 |
+
}).catch((e) => {
|
| 674 |
+
ui.dataset.textContent = "built-in corpus (offline)";
|
| 675 |
+
log(`dataset: FineWeb-Edu stream unavailable (${e.message}) — using the built-in corpus`);
|
| 676 |
+
});
|
| 677 |
ui.start.onclick = () => {
|
| 678 |
const cfg = readCfgFromUI();
|
| 679 |
+
leaderId = null; // I pressed Start: I lead sync
|
| 680 |
buildModel(cfg);
|
| 681 |
broadcastConfig(cfg); // everyone follows these settings
|
| 682 |
train(cfg);
|
| 683 |
};
|
| 684 |
ui.save.onclick = saveCheckpoint;
|
| 685 |
+
ui.kit.onclick = downloadInferenceKit;
|
| 686 |
+
ui.genBtn.onclick = async () => {
|
| 687 |
+
if (training) { log("can't generate mid-training"); return; }
|
| 688 |
+
ui.genBtn.disabled = true;
|
| 689 |
+
ui.genOut.textContent = "generating…";
|
| 690 |
+
try { ui.genOut.textContent = await Transformer.generate(model, ui.genPrompt.value || "the ", 150); }
|
| 691 |
+
catch (e) { ui.genOut.textContent = `error: ${e.message}`; }
|
| 692 |
+
ui.genBtn.disabled = false;
|
| 693 |
+
};
|
| 694 |
ui.loadBtn.onclick = () => { if (training) { log("can't load a checkpoint mid-training"); return; } ui.load.click(); };
|
| 695 |
ui.load.onchange = async () => {
|
| 696 |
const f = ui.load.files[0]; ui.load.value = "";
|
web/public/index.html
CHANGED
|
@@ -90,6 +90,8 @@
|
|
| 90 |
<div class="device" id="me">—</div>
|
| 91 |
<div class="row" style="margin-top:8px"><span class="k">Status</span><span class="v" id="status">starting…</span></div>
|
| 92 |
<div class="row"><span class="k">Compute</span><span class="v" id="backend">detecting…</span></div>
|
|
|
|
|
|
|
| 93 |
</div>
|
| 94 |
|
| 95 |
<div class="card">
|
|
@@ -106,21 +108,22 @@
|
|
| 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="
|
| 110 |
<label class="slbl">Sequence length <span class="sval" id="vcfgT">32</span></label>
|
| 111 |
-
<input type="range" id="cfgT" min="16" max="
|
|
|
|
| 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">
|
| 122 |
<button id="start" disabled>Start training</button>
|
| 123 |
-
<p class="note" style="margin:.6rem 0 0">
|
| 124 |
</div>
|
| 125 |
|
| 126 |
<div class="card">
|
|
@@ -131,10 +134,22 @@
|
|
| 131 |
<div class="row" style="margin-top:10px"><span class="diff" id="diff"></span></div>
|
| 132 |
</div>
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
<div class="card">
|
| 135 |
<div class="lbl">💾 Model checkpoint</div>
|
| 136 |
<div style="display:flex;gap:10px;flex-wrap:wrap;justify-content:center">
|
| 137 |
<button id="save" disabled>Download model (.pt)</button>
|
|
|
|
| 138 |
<button id="loadBtn">Load checkpoint…</button>
|
| 139 |
<input type="file" id="load" accept=".pt" style="display:none">
|
| 140 |
</div>
|
|
|
|
| 90 |
<div class="device" id="me">—</div>
|
| 91 |
<div class="row" style="margin-top:8px"><span class="k">Status</span><span class="v" id="status">starting…</span></div>
|
| 92 |
<div class="row"><span class="k">Compute</span><span class="v" id="backend">detecting…</span></div>
|
| 93 |
+
<div class="row"><span class="k">Dataset</span><span class="v" id="dataset">—</span></div>
|
| 94 |
+
<div class="row"><span class="k">Tokenizer</span><span class="v" id="tokenizer">loading…</span></div>
|
| 95 |
</div>
|
| 96 |
|
| 97 |
<div class="card">
|
|
|
|
| 108 |
<div class="card">
|
| 109 |
<div class="lbl">🎛 Training settings</div>
|
| 110 |
<label class="slbl">Model width <span class="sval" id="vcfgC">32</span></label>
|
| 111 |
+
<input type="range" id="cfgC" min="16" max="128" step="16" value="32">
|
| 112 |
<label class="slbl">Sequence length <span class="sval" id="vcfgT">32</span></label>
|
| 113 |
+
<input type="range" id="cfgT" min="16" max="128" step="16" value="32">
|
| 114 |
+
<p class="note" style="margin:.4rem 0 0"><b>Width or sequence above 64?</b> Bring more devices — big settings on 1–3 devices mean slow steps and a small effective batch. 4+ devices recommended.</p>
|
| 115 |
<label class="slbl">Batch per device <span class="sval" id="vcfgB">8</span></label>
|
| 116 |
<input type="range" id="cfgB" min="2" max="32" step="2" value="8">
|
| 117 |
<label class="slbl">Steps <span class="sval" id="vcfgSteps">300</span></label>
|
| 118 |
<input type="range" id="cfgSteps" min="100" max="10000" step="100" value="300">
|
| 119 |
<label class="slbl">Learning rate ×1000 <span class="sval" id="vcfgLr">20</span></label>
|
| 120 |
<input type="range" id="cfgLr" min="5" max="50" step="5" value="20">
|
| 121 |
+
<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. Training text is streamed from <b>FineWeb-Edu</b> (HuggingFace); offline devices fall back to a built-in corpus. Whoever presses Start sets the settings for the whole group; total batch scales with every device that joins.</p>
|
| 122 |
</div>
|
| 123 |
|
| 124 |
<div class="card" style="text-align:center">
|
| 125 |
<button id="start" disabled>Start training</button>
|
| 126 |
+
<p class="note" style="margin:.6rem 0 0">Works solo — every device that joins adds its batch to the group.</p>
|
| 127 |
</div>
|
| 128 |
|
| 129 |
<div class="card">
|
|
|
|
| 134 |
<div class="row" style="margin-top:10px"><span class="diff" id="diff"></span></div>
|
| 135 |
</div>
|
| 136 |
|
| 137 |
+
<div class="card">
|
| 138 |
+
<div class="lbl">🗣 Test the model</div>
|
| 139 |
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
| 140 |
+
<input type="text" id="genPrompt" value="the " spellcheck="false"
|
| 141 |
+
style="flex:1;min-width:160px;padding:10px 12px;border-radius:8px;border:1px solid var(--card-border);background:transparent;color:inherit;font-family:'Courier New',monospace">
|
| 142 |
+
<button id="genBtn" disabled style="padding:10px 18px">Generate</button>
|
| 143 |
+
</div>
|
| 144 |
+
<pre id="genOut" style="margin-top:10px;min-height:44px"></pre>
|
| 145 |
+
<p class="note" style="margin:.5rem 0 0">Enabled after training finishes or a checkpoint is loaded. The “inference kit” below downloads a single HTML file with these weights baked in — open it anywhere to run generations offline.</p>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
<div class="card">
|
| 149 |
<div class="lbl">💾 Model checkpoint</div>
|
| 150 |
<div style="display:flex;gap:10px;flex-wrap:wrap;justify-content:center">
|
| 151 |
<button id="save" disabled>Download model (.pt)</button>
|
| 152 |
+
<button id="kit" disabled>Download inference kit</button>
|
| 153 |
<button id="loadBtn">Load checkpoint…</button>
|
| 154 |
<input type="file" id="load" accept=".pt" style="display:none">
|
| 155 |
</div>
|
web/public/tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
web/public/transformer.js
CHANGED
|
@@ -28,29 +28,96 @@
|
|
| 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 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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) -------------------------------------------------
|
|
@@ -84,9 +151,9 @@
|
|
| 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:
|
| 88 |
ctx: { L, matmulInt8 },
|
| 89 |
-
emb: add("emb", mk(
|
| 90 |
pos: add("pos", mk(cfg.t * c, 0.02)),
|
| 91 |
blocks: [], params, names,
|
| 92 |
};
|
|
@@ -96,7 +163,7 @@
|
|
| 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 *
|
| 100 |
m.nParams = params.reduce((a, p) => a + p.length, 0);
|
| 101 |
return m;
|
| 102 |
}
|
|
@@ -289,7 +356,7 @@
|
|
| 289 |
// greedy sampling — watch the model actually speak
|
| 290 |
async function generate(m, prompt, nChars) {
|
| 291 |
const { t: T } = m.cfg;
|
| 292 |
-
let ids = [...
|
| 293 |
for (let step = 0; step < nChars; step++) {
|
| 294 |
const win = ids.slice(-T);
|
| 295 |
const X = new Int32Array(T), Y = new Int32Array(T);
|
|
@@ -302,10 +369,12 @@
|
|
| 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
|
| 306 |
}
|
| 307 |
|
| 308 |
-
const api = { init, trainStep, applyUpdate, getFlatParams, setFlatParams, generate,
|
|
|
|
|
|
|
| 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);
|
|
|
|
| 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 |
+
// ---- tokenizer -------------------------------------------------------------
|
| 32 |
+
// Spikewhale tokenizer (tokenizer.json): byte-level greedy longest-match
|
| 33 |
+
// ("length-max"), ~16.5k tokens. Until it loads (or if the file is missing)
|
| 34 |
+
// a 96-char byte-level vocab keeps the app working — but ALL devices in a
|
| 35 |
+
// group must use the same tokenizer (the config broadcast enforces it).
|
| 36 |
+
const FALLBACK_CHARS = [...Array(95)].map((_, i) => String.fromCharCode(32 + i)).concat(["\n"]);
|
| 37 |
+
let tok = {
|
| 38 |
+
name: "char-96 (fallback)",
|
| 39 |
+
vocab: Object.fromEntries(FALLBACK_CHARS.map((c, i) => [c, i])),
|
| 40 |
+
ids: FALLBACK_CHARS, maxLen: 1, size: FALLBACK_CHARS.length,
|
| 41 |
+
unk: 0, specials: new Set(),
|
| 42 |
+
};
|
| 43 |
+
tok.unk = tok.vocab[" "];
|
| 44 |
+
function vocabSize() { return tok.size; }
|
| 45 |
+
function tokenizerName() { return tok.name; }
|
| 46 |
+
function loadTokenizerData(d) { // plain {vocab, vocab_size, max_token_len}
|
| 47 |
+
const ids = new Array(d.vocab_size);
|
| 48 |
+
for (const [t, i] of Object.entries(d.vocab)) ids[i] = t;
|
| 49 |
+
tok = { name: `Spikewhale length-max (${d.vocab_size} tokens)`,
|
| 50 |
+
vocab: d.vocab, ids, maxLen: d.max_token_len || 24, size: d.vocab_size,
|
| 51 |
+
unk: d.vocab["<unk>"] ?? 1,
|
| 52 |
+
specials: new Set(["<pad>", "<unk>", "<bos>", "<eos>", ...(d.special_tokens || [])]) };
|
| 53 |
+
IDS = encode(CORPUS); // re-tokenize whatever corpus is loaded
|
| 54 |
+
return tok.name;
|
| 55 |
+
}
|
| 56 |
+
async function loadTokenizer(url) {
|
| 57 |
+
const r = await fetch(url || "tokenizer.json");
|
| 58 |
+
if (!r.ok) throw new Error(`tokenizer.json HTTP ${r.status}`);
|
| 59 |
+
return loadTokenizerData(await r.json());
|
| 60 |
+
}
|
| 61 |
+
function toLatin1(s) { const b = new TextEncoder().encode(s); let o = ""; for (const x of b) o += String.fromCharCode(x); return o; }
|
| 62 |
+
function encode(text) { // greedy longest match over bytes
|
| 63 |
+
const s = toLatin1(text), out = [];
|
| 64 |
+
let i = 0;
|
| 65 |
+
while (i < s.length) {
|
| 66 |
+
let m = null;
|
| 67 |
+
for (let L = Math.min(tok.maxLen, s.length - i); L > 0; L--) {
|
| 68 |
+
const sub = s.substr(i, L);
|
| 69 |
+
if (sub in tok.vocab) { m = sub; break; }
|
| 70 |
+
}
|
| 71 |
+
if (m === null) { out.push(tok.unk); i++; continue; }
|
| 72 |
+
out.push(tok.vocab[m]); i += m.length;
|
| 73 |
+
}
|
| 74 |
+
return Int32Array.from(out);
|
| 75 |
+
}
|
| 76 |
+
function decode(idArr) {
|
| 77 |
+
let s = "";
|
| 78 |
+
for (const id of idArr) {
|
| 79 |
+
const t = tok.ids[id];
|
| 80 |
+
if (t === undefined || tok.specials.has(t)) continue;
|
| 81 |
+
s += t;
|
| 82 |
+
}
|
| 83 |
+
const bytes = Uint8Array.from([...s].map(c => c.charCodeAt(0)));
|
| 84 |
+
return new TextDecoder().decode(bytes);
|
| 85 |
+
}
|
| 86 |
+
let CORPUS = buildCorpus();
|
| 87 |
+
let IDS = encode(CORPUS);
|
| 88 |
+
let DATASET = "built-in corpus";
|
| 89 |
+
|
| 90 |
+
// Stream real training text: FineWeb-Edu, via the public HuggingFace
|
| 91 |
+
// datasets-server rows API. Each device pulls its own random slice (that's
|
| 92 |
+
// data parallelism — batches were always per-device anyway). Offline or on
|
| 93 |
+
// API failure the built-in corpus stays in place.
|
| 94 |
+
async function streamFineWebEdu() {
|
| 95 |
+
const offset = Math.floor(Math.random() * 9900);
|
| 96 |
+
const url = "https://datasets-server.huggingface.co/rows?dataset=HuggingFaceFW%2Ffineweb-edu" +
|
| 97 |
+
`&config=default&split=train&offset=${offset}&length=100`;
|
| 98 |
+
const r = await fetch(url);
|
| 99 |
+
if (!r.ok) throw new Error(`datasets-server HTTP ${r.status}`);
|
| 100 |
+
const j = await r.json();
|
| 101 |
+
const text = j.rows.map(x => x.row.text).join("\n").replace(/[^\x20-\x7e\n]/g, " ");
|
| 102 |
+
if (text.length < 10000) throw new Error("too little text returned");
|
| 103 |
+
CORPUS = text.slice(0, 500000);
|
| 104 |
+
IDS = encode(CORPUS);
|
| 105 |
+
DATASET = "FineWeb-Edu (streamed from HuggingFace)";
|
| 106 |
+
return { name: DATASET, chars: CORPUS.length };
|
| 107 |
+
}
|
| 108 |
+
function datasetName() { return DATASET; }
|
| 109 |
|
| 110 |
// ---- verified matmul: float in -> quantize -> LUT multiply -> dequant -----
|
| 111 |
// weights/projections may go through WebGPU; attention (per-head, many small
|
| 112 |
// matmuls) uses the CPU LUT path — same verified units, no dispatch overhead.
|
| 113 |
+
// 3xINT8 fast-accurate GEMM (CUTLASS 3xTF32 scheme through the verified
|
| 114 |
+
// units): three exact LUT GEMMs recover ~14-bit precision from the 8-bit
|
| 115 |
+
// units — see Verified.lutMatmul3.
|
| 116 |
async function vmm(Xf, Wf, m, k, n, ctx) {
|
| 117 |
+
return V.lutMatmul3(Xf, Wf, m, k, n, ctx.L, ctx.matmulInt8);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
}
|
| 119 |
function vmmCPU(Xf, Wf, m, k, n, ctx) {
|
| 120 |
+
return V.lutMatmul3JS(Xf, Wf, m, k, n, ctx.L);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
|
| 123 |
// ---- layernorm (no affine) -------------------------------------------------
|
|
|
|
| 151 |
const params = [], names = [];
|
| 152 |
const add = (name, w) => { params.push(w); names.push(name); return w; };
|
| 153 |
const m = {
|
| 154 |
+
cfg: { ...cfg, layers, heads, hidden, vocab: vocabSize() },
|
| 155 |
ctx: { L, matmulInt8 },
|
| 156 |
+
emb: add("emb", mk(vocabSize() * c, 0.08)),
|
| 157 |
pos: add("pos", mk(cfg.t * c, 0.02)),
|
| 158 |
blocks: [], params, names,
|
| 159 |
};
|
|
|
|
| 163 |
Wv: add(`b${l}.Wv`, mk(c * c, 0.08)), Wo: add(`b${l}.Wo`, mk(c * c, 0.08)),
|
| 164 |
W1: add(`b${l}.W1`, mk(c * hidden, 0.08)), W2: add(`b${l}.W2`, mk(hidden * c, 0.08)),
|
| 165 |
});
|
| 166 |
+
m.Wu = add("Wu", mk(c * vocabSize(), 0.08));
|
| 167 |
m.nParams = params.reduce((a, p) => a + p.length, 0);
|
| 168 |
return m;
|
| 169 |
}
|
|
|
|
| 356 |
// greedy sampling — watch the model actually speak
|
| 357 |
async function generate(m, prompt, nChars) {
|
| 358 |
const { t: T } = m.cfg;
|
| 359 |
+
let ids = [...encode(prompt)];
|
| 360 |
for (let step = 0; step < nChars; step++) {
|
| 361 |
const win = ids.slice(-T);
|
| 362 |
const X = new Int32Array(T), Y = new Int32Array(T);
|
|
|
|
| 369 |
for (let j = 0; j < m.cfg.vocab; j++) if (logits[row + j] > bv) { bv = logits[row + j]; best = j; }
|
| 370 |
ids.push(best);
|
| 371 |
}
|
| 372 |
+
return decode(ids);
|
| 373 |
}
|
| 374 |
|
| 375 |
+
const api = { init, trainStep, applyUpdate, getFlatParams, setFlatParams, generate,
|
| 376 |
+
streamFineWebEdu, datasetName, loadTokenizer, loadTokenizerData,
|
| 377 |
+
vocabSize, tokenizerName, encode, decode };
|
| 378 |
if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); V = require("./verified_core.js"); module.exports = api; }
|
| 379 |
else { TC = root.TrainCore; V = root.Verified; root.Transformer = api; }
|
| 380 |
})(typeof self !== "undefined" ? self : this);
|
web/public/verified_core.js
CHANGED
|
@@ -28,6 +28,44 @@
|
|
| 28 |
return C;
|
| 29 |
}
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
// one verified layer forward; returns float out (+ cache for STE backward).
|
| 32 |
// Every product goes through the verified INT8 multiply (mul8 LUT) with exact
|
| 33 |
// int32 accumulation — i.e. an emulated INT8 tensor-core GEMM — then dequant.
|
|
@@ -78,7 +116,7 @@
|
|
| 78 |
for (let j = 0; j < W2.length; j++) W2[j] -= lr * gAvg[W1.length + j];
|
| 79 |
}
|
| 80 |
|
| 81 |
-
const api = { quantize, lutMatmulJS, linearFwd, forward, backward, splitApply };
|
| 82 |
if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); module.exports = api; }
|
| 83 |
else { TC = root.TrainCore; root.Verified = api; }
|
| 84 |
})(typeof self !== "undefined" ? self : this);
|
|
|
|
| 28 |
return C;
|
| 29 |
}
|
| 30 |
|
| 31 |
+
// ---- 3xINT8 fast-accurate GEMM --------------------------------------------
|
| 32 |
+
// The CUTLASS example-27 "3xTF32" scheme, ported to the verified units:
|
| 33 |
+
// split each float into a coarse int8 part plus an int8-quantized residual,
|
| 34 |
+
// run three EXACT LUT GEMMs (hi·hi, hi·lo, lo·hi), drop the negligible
|
| 35 |
+
// lo·lo, and recombine. Same big/small decomposition NVIDIA uses to recover
|
| 36 |
+
// near-fp32 accuracy from TF32 tensor cores — here it recovers ~14-bit
|
| 37 |
+
// accuracy from the 8-bit units, at 3× the unit ops. Every product still
|
| 38 |
+
// goes through the verified mul8 LUT.
|
| 39 |
+
function quantize2(X) {
|
| 40 |
+
const hi = quantize(X);
|
| 41 |
+
const r = new Float32Array(X.length);
|
| 42 |
+
for (let i = 0; i < X.length; i++) r[i] = X[i] - hi.q[i] * hi.scale;
|
| 43 |
+
const lo = quantize(r);
|
| 44 |
+
return { hi, lo };
|
| 45 |
+
}
|
| 46 |
+
function combine3(hh, hl, lh, x, w, len) {
|
| 47 |
+
const out = new Float32Array(len);
|
| 48 |
+
const shh = x.hi.scale * w.hi.scale, shl = x.hi.scale * w.lo.scale, slh = x.lo.scale * w.hi.scale;
|
| 49 |
+
for (let i = 0; i < len; i++) out[i] = hh[i] * shh + hl[i] * shl + lh[i] * slh;
|
| 50 |
+
return out;
|
| 51 |
+
}
|
| 52 |
+
function lutMatmul3JS(Xf, Wf, m, k, n, L) { // sync, CPU LUT path
|
| 53 |
+
const x = quantize2(Xf), w = quantize2(Wf);
|
| 54 |
+
return combine3(lutMatmulJS(x.hi.q, w.hi.q, m, k, n, L),
|
| 55 |
+
lutMatmulJS(x.hi.q, w.lo.q, m, k, n, L),
|
| 56 |
+
lutMatmulJS(x.lo.q, w.hi.q, m, k, n, L), x, w, m * n);
|
| 57 |
+
}
|
| 58 |
+
async function lutMatmul3(Xf, Wf, m, k, n, L, matmulInt8) { // any backend
|
| 59 |
+
const x = quantize2(Xf), w = quantize2(Wf);
|
| 60 |
+
const mm = matmulInt8 || lutMatmulJS;
|
| 61 |
+
const [hh, hl, lh] = await Promise.all([
|
| 62 |
+
mm(x.hi.q, w.hi.q, m, k, n, L),
|
| 63 |
+
mm(x.hi.q, w.lo.q, m, k, n, L),
|
| 64 |
+
mm(x.lo.q, w.hi.q, m, k, n, L),
|
| 65 |
+
]);
|
| 66 |
+
return combine3(hh, hl, lh, x, w, m * n);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
// one verified layer forward; returns float out (+ cache for STE backward).
|
| 70 |
// Every product goes through the verified INT8 multiply (mul8 LUT) with exact
|
| 71 |
// int32 accumulation — i.e. an emulated INT8 tensor-core GEMM — then dequant.
|
|
|
|
| 116 |
for (let j = 0; j < W2.length; j++) W2[j] -= lr * gAvg[W1.length + j];
|
| 117 |
}
|
| 118 |
|
| 119 |
+
const api = { quantize, quantize2, lutMatmulJS, lutMatmul3JS, lutMatmul3, linearFwd, forward, backward, splitApply };
|
| 120 |
if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); module.exports = api; }
|
| 121 |
else { TC = root.TrainCore; root.Verified = api; }
|
| 122 |
})(typeof self !== "undefined" ? self : this);
|
web/public/webgpu.js
CHANGED
|
@@ -3,10 +3,19 @@
|
|
| 3 |
// without WebGPU (e.g. old PCs via Supermium). initCompute() returns
|
| 4 |
// { backend, label, matmulInt8(Xq, Wq, m, k, n, L) -> Int32Array }
|
| 5 |
// matching Verified.lutMatmulJS, so the trainer is device-blind.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
(function (root) {
|
| 7 |
"use strict";
|
| 8 |
|
| 9 |
-
const
|
| 10 |
@group(0) @binding(0) var<storage, read> Xq : array<i32>; // int8 byte per elem
|
| 11 |
@group(0) @binding(1) var<storage, read> Wq : array<i32>;
|
| 12 |
@group(0) @binding(2) var<storage, read> lut : array<i32>; // 65536 signed products
|
|
@@ -26,6 +35,25 @@
|
|
| 26 |
C[row * n + col] = s;
|
| 27 |
}`;
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
async function loadLUTs(base) {
|
| 30 |
base = base || "";
|
| 31 |
const [mulB, reqB, reluB, meta] = await Promise.all([
|
|
@@ -38,6 +66,21 @@
|
|
| 38 |
relu: new Int8Array(reluB), shift: meta.shift };
|
| 39 |
}
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
async function initCompute(L) {
|
| 42 |
const cpu = { backend: "cpu", label: "CPU (JS)",
|
| 43 |
matmulInt8: (Xq, Wq, m, k, n, LL) => root.Verified.lutMatmulJS(Xq, Wq, m, k, n, LL) };
|
|
@@ -46,32 +89,45 @@
|
|
| 46 |
const adapter = await navigator.gpu.requestAdapter();
|
| 47 |
if (!adapter) return cpu;
|
| 48 |
const device = await adapter.requestDevice();
|
| 49 |
-
const
|
| 50 |
-
const
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
| 52 |
const lut32 = new Int32Array(L.mul); // widen int16 -> int32
|
| 53 |
const lutBuf = device.createBuffer({ size: lut32.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
| 54 |
device.queue.writeBuffer(lutBuf, 0, lut32);
|
| 55 |
-
const
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
} catch (e) { console.warn("WebGPU init failed, CPU fallback:", e); return cpu; }
|
| 59 |
}
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
const bufX = mk(device, X32.byteLength, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 64 |
-
const bufW = mk(device, W32.byteLength, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 65 |
const bytesC = m * n * 4;
|
| 66 |
const bufC = mk(device, bytesC, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
|
| 67 |
-
const
|
| 68 |
-
|
| 69 |
-
device.queue.writeBuffer(bufW, 0, W32);
|
| 70 |
-
device.queue.writeBuffer(bufD, 0, new Uint32Array([m, k, n]));
|
| 71 |
-
const bind = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [
|
| 72 |
-
{ binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
|
| 73 |
-
{ binding: 2, resource: { buffer: lutBuf } }, { binding: 3, resource: { buffer: bufC } },
|
| 74 |
-
{ binding: 4, resource: { buffer: bufD } } ] });
|
| 75 |
const enc = device.createCommandEncoder();
|
| 76 |
const pass = enc.beginComputePass();
|
| 77 |
pass.setPipeline(pipeline); pass.setBindGroup(0, bind);
|
|
@@ -82,10 +138,42 @@
|
|
| 82 |
await read.mapAsync(GPUMapMode.READ);
|
| 83 |
const out = new Int32Array(read.getMappedRange().slice(0));
|
| 84 |
read.unmap();
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
|
|
|
| 88 |
function mk(device, size, usage) { return device.createBuffer({ size, usage }); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
root.Compute = { initCompute, loadLUTs };
|
| 91 |
})(self);
|
|
|
|
| 3 |
// without WebGPU (e.g. old PCs via Supermium). initCompute() returns
|
| 4 |
// { backend, label, matmulInt8(Xq, Wq, m, k, n, L) -> Int32Array }
|
| 5 |
// matching Verified.lutMatmulJS, so the trainer is device-blind.
|
| 6 |
+
//
|
| 7 |
+
// High-throughput path: when the browser exposes WGSL's
|
| 8 |
+
// packed_4x8_integer_dot_product feature, we use dot4I8Packed — it compiles to
|
| 9 |
+
// the GPU's DP4A/INT8 dot-product hardware (the same units tensor-core INT8
|
| 10 |
+
// paths are built on): 4 exact int8 MACs per instruction, int32 accumulation,
|
| 11 |
+
// and 4× less memory traffic from packing. Because int8×int8→int32 is exact,
|
| 12 |
+
// it is bit-identical to the verified mul8 LUT — and we PROVE that at init by
|
| 13 |
+
// cross-checking random matmuls against the LUT before trusting it. If the
|
| 14 |
+
// hardware ever disagrees with the units, we fall back to the LUT shader.
|
| 15 |
(function (root) {
|
| 16 |
"use strict";
|
| 17 |
|
| 18 |
+
const WGSL_LUT = `
|
| 19 |
@group(0) @binding(0) var<storage, read> Xq : array<i32>; // int8 byte per elem
|
| 20 |
@group(0) @binding(1) var<storage, read> Wq : array<i32>;
|
| 21 |
@group(0) @binding(2) var<storage, read> lut : array<i32>; // 65536 signed products
|
|
|
|
| 35 |
C[row * n + col] = s;
|
| 36 |
}`;
|
| 37 |
|
| 38 |
+
// X packed 4×int8 along k; W transposed then packed the same way, so each
|
| 39 |
+
// thread streams two contiguous rows of u32 words through dot4I8Packed.
|
| 40 |
+
const WGSL_DP4 = `
|
| 41 |
+
@group(0) @binding(0) var<storage, read> Xp : array<u32>;
|
| 42 |
+
@group(0) @binding(1) var<storage, read> Wp : array<u32>; // Wᵀ, packed
|
| 43 |
+
@group(0) @binding(2) var<storage, read_write> C : array<i32>;
|
| 44 |
+
@group(0) @binding(3) var<uniform> dims : vec3<u32>; // m, kw=ceil(k/4), n
|
| 45 |
+
@compute @workgroup_size(8, 8)
|
| 46 |
+
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
|
| 47 |
+
let m = dims.x; let kw = dims.y; let n = dims.z;
|
| 48 |
+
let row = gid.x; let col = gid.y;
|
| 49 |
+
if (row >= m || col >= n) { return; }
|
| 50 |
+
var s : i32 = 0;
|
| 51 |
+
for (var p = 0u; p < kw; p = p + 1u) {
|
| 52 |
+
s = s + dot4I8Packed(Xp[row * kw + p], Wp[col * kw + p]);
|
| 53 |
+
}
|
| 54 |
+
C[row * n + col] = s;
|
| 55 |
+
}`;
|
| 56 |
+
|
| 57 |
async function loadLUTs(base) {
|
| 58 |
base = base || "";
|
| 59 |
const [mulB, reqB, reluB, meta] = await Promise.all([
|
|
|
|
| 66 |
relu: new Int8Array(reluB), shift: meta.shift };
|
| 67 |
}
|
| 68 |
|
| 69 |
+
// pack a row-major int8 matrix (rows×cols) into u32 words of 4 bytes along
|
| 70 |
+
// cols, zero-padded to kw words per row (zeros contribute 0 to the dot)
|
| 71 |
+
function packRows(Q, rows, cols, kw) {
|
| 72 |
+
const out = new Uint32Array(rows * kw);
|
| 73 |
+
const bytes = new Uint8Array(out.buffer);
|
| 74 |
+
for (let r = 0; r < rows; r++)
|
| 75 |
+
for (let c = 0; c < cols; c++) bytes[(r * kw * 4) + c] = Q[r * cols + c] & 0xFF;
|
| 76 |
+
return out;
|
| 77 |
+
}
|
| 78 |
+
function transposeI8(Q, rows, cols) {
|
| 79 |
+
const out = new Int8Array(rows * cols);
|
| 80 |
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out[c * rows + r] = Q[r * cols + c];
|
| 81 |
+
return out;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
async function initCompute(L) {
|
| 85 |
const cpu = { backend: "cpu", label: "CPU (JS)",
|
| 86 |
matmulInt8: (Xq, Wq, m, k, n, LL) => root.Verified.lutMatmulJS(Xq, Wq, m, k, n, LL) };
|
|
|
|
| 89 |
const adapter = await navigator.gpu.requestAdapter();
|
| 90 |
if (!adapter) return cpu;
|
| 91 |
const device = await adapter.requestDevice();
|
| 92 |
+
const info = adapter.info || {};
|
| 93 |
+
const gpuName = info.description || info.vendor || "WebGPU";
|
| 94 |
+
|
| 95 |
+
// LUT pipeline (always built — the fallback and the verification oracle)
|
| 96 |
+
const lutModule = device.createShaderModule({ code: WGSL_LUT });
|
| 97 |
+
const lutPipe = device.createComputePipeline({ layout: "auto", compute: { module: lutModule, entryPoint: "main" } });
|
| 98 |
const lut32 = new Int32Array(L.mul); // widen int16 -> int32
|
| 99 |
const lutBuf = device.createBuffer({ size: lut32.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
| 100 |
device.queue.writeBuffer(lutBuf, 0, lut32);
|
| 101 |
+
const viaLUT = { backend: "webgpu", label: `${gpuName} (LUT shader)`,
|
| 102 |
+
matmulInt8: (Xq, Wq, m, k, n) => gpuMatmulLUT(device, lutPipe, lutBuf, Xq, Wq, m, k, n) };
|
| 103 |
+
|
| 104 |
+
// DP4A pipeline — only if the WGSL feature exists AND it reproduces the
|
| 105 |
+
// verified units exactly on random self-tests
|
| 106 |
+
if (!(navigator.gpu.wgslLanguageFeatures && navigator.gpu.wgslLanguageFeatures.has("packed_4x8_integer_dot_product")))
|
| 107 |
+
return viaLUT;
|
| 108 |
+
const dp4Module = device.createShaderModule({ code: WGSL_DP4 });
|
| 109 |
+
const dp4Pipe = device.createComputePipeline({ layout: "auto", compute: { module: dp4Module, entryPoint: "main" } });
|
| 110 |
+
const dp4mm = (Xq, Wq, m, k, n) => gpuMatmulDP4(device, dp4Pipe, Xq, Wq, m, k, n);
|
| 111 |
+
for (let trial = 0; trial < 3; trial++) { // hardware vs verified units
|
| 112 |
+
const m0 = 5 + trial, k0 = 7 + 3 * trial, n0 = 6 + trial;
|
| 113 |
+
const Xq = new Int8Array(m0 * k0), Wq = new Int8Array(k0 * n0);
|
| 114 |
+
for (let i = 0; i < Xq.length; i++) Xq[i] = (Math.random() * 256 - 128) | 0;
|
| 115 |
+
for (let i = 0; i < Wq.length; i++) Wq[i] = (Math.random() * 256 - 128) | 0;
|
| 116 |
+
const hw = await dp4mm(Xq, Wq, m0, k0, n0);
|
| 117 |
+
const ref = root.Verified.lutMatmulJS(Xq, Wq, m0, k0, n0, L);
|
| 118 |
+
for (let i = 0; i < ref.length; i++)
|
| 119 |
+
if (hw[i] !== ref[i]) { console.warn("DP4A disagreed with the verified units — using LUT shader"); return viaLUT; }
|
| 120 |
+
}
|
| 121 |
+
return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot — HW verified vs units)`, matmulInt8: dp4mm };
|
| 122 |
} catch (e) { console.warn("WebGPU init failed, CPU fallback:", e); return cpu; }
|
| 123 |
}
|
| 124 |
|
| 125 |
+
// shared dispatch/readback plumbing
|
| 126 |
+
async function runPass(device, pipeline, entries, m, n) {
|
|
|
|
|
|
|
| 127 |
const bytesC = m * n * 4;
|
| 128 |
const bufC = mk(device, bytesC, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
|
| 129 |
+
const bind = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0),
|
| 130 |
+
entries: entries(bufC) });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
const enc = device.createCommandEncoder();
|
| 132 |
const pass = enc.beginComputePass();
|
| 133 |
pass.setPipeline(pipeline); pass.setBindGroup(0, bind);
|
|
|
|
| 138 |
await read.mapAsync(GPUMapMode.READ);
|
| 139 |
const out = new Int32Array(read.getMappedRange().slice(0));
|
| 140 |
read.unmap();
|
| 141 |
+
return { out, bufC, read };
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
async function gpuMatmulLUT(device, pipeline, lutBuf, Xq, Wq, m, k, n) {
|
| 145 |
+
const X32 = Int32Array.from(Xq), W32 = Int32Array.from(Wq); // byte -> i32
|
| 146 |
+
const bufX = up(device, X32, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 147 |
+
const bufW = up(device, W32, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 148 |
+
const bufD = up(device, new Uint32Array([m, k, n, 0]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
|
| 149 |
+
const r = await runPass(device, pipeline, (bufC) => [
|
| 150 |
+
{ binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
|
| 151 |
+
{ binding: 2, resource: { buffer: lutBuf } }, { binding: 3, resource: { buffer: bufC } },
|
| 152 |
+
{ binding: 4, resource: { buffer: bufD } } ], m, n);
|
| 153 |
+
[bufX, bufW, bufD, r.bufC, r.read].forEach(b => b.destroy());
|
| 154 |
+
return r.out;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
async function gpuMatmulDP4(device, pipeline, Xq, Wq, m, k, n) {
|
| 158 |
+
const kw = Math.ceil(k / 4);
|
| 159 |
+
const Xp = packRows(Xq, m, k, kw);
|
| 160 |
+
const Wp = packRows(transposeI8(Wq, k, n), n, k, kw); // Wᵀ so col j is a row
|
| 161 |
+
const bufX = up(device, Xp, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 162 |
+
const bufW = up(device, Wp, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 163 |
+
const bufD = up(device, new Uint32Array([m, kw, n, 0]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
|
| 164 |
+
const r = await runPass(device, pipeline, (bufC) => [
|
| 165 |
+
{ binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
|
| 166 |
+
{ binding: 2, resource: { buffer: bufC } }, { binding: 3, resource: { buffer: bufD } } ], m, n);
|
| 167 |
+
[bufX, bufW, bufD, r.bufC, r.read].forEach(b => b.destroy());
|
| 168 |
+
return r.out;
|
| 169 |
}
|
| 170 |
+
|
| 171 |
function mk(device, size, usage) { return device.createBuffer({ size, usage }); }
|
| 172 |
+
function up(device, arr, usage) {
|
| 173 |
+
const b = mk(device, Math.max(16, arr.byteLength), usage);
|
| 174 |
+
device.queue.writeBuffer(b, 0, arr);
|
| 175 |
+
return b;
|
| 176 |
+
}
|
| 177 |
|
| 178 |
root.Compute = { initCompute, loadLUTs };
|
| 179 |
})(self);
|
web/test_transformer.js
CHANGED
|
@@ -20,7 +20,7 @@ const cfg = { c: 32, t: 32, b: 8, layers: 2, heads: 2, steps: 120, lr: 0.02 };
|
|
| 20 |
(async function () {
|
| 21 |
const A = X.init(cfg, L, matmulInt8);
|
| 22 |
const B = X.init(cfg, L, matmulInt8);
|
| 23 |
-
console.log(`vocab=${X.
|
| 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;
|
|
@@ -40,7 +40,7 @@ const cfg = { c: 32, t: 32, b: 8, layers: 2, heads: 2, steps: 120, lr: 0.02 };
|
|
| 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.
|
| 44 |
console.log(ok ? "TRANSFORMER TEST PASSED" : "TRANSFORMER TEST FAILED");
|
| 45 |
process.exit(ok ? 0 : 1);
|
| 46 |
})();
|
|
|
|
| 20 |
(async function () {
|
| 21 |
const A = X.init(cfg, L, matmulInt8);
|
| 22 |
const B = X.init(cfg, L, matmulInt8);
|
| 23 |
+
console.log(`vocab=${X.vocabSize()}, params=${A.nParams}, baseline loss=${Math.log(X.vocabSize()).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;
|
|
|
|
| 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.vocabSize()) && diff === 0;
|
| 44 |
console.log(ok ? "TRANSFORMER TEST PASSED" : "TRANSFORMER TEST FAILED");
|
| 45 |
process.exit(ok ? 0 : 1);
|
| 46 |
})();
|