Web demo: leader failover + dataset picker + reconnect policy
Browse files- web/public/app.js +83 -22
- web/public/index.html +6 -1
- web/public/transformer.js +14 -10
web/public/app.js
CHANGED
|
@@ -37,6 +37,7 @@ const ui = {
|
|
| 37 |
cfgB: document.getElementById("cfgB"),
|
| 38 |
cfgSteps: document.getElementById("cfgSteps"),
|
| 39 |
cfgLr: document.getElementById("cfgLr"),
|
|
|
|
| 40 |
dataset: document.getElementById("dataset"),
|
| 41 |
tokenizer: document.getElementById("tokenizer"),
|
| 42 |
kit: document.getElementById("kit"),
|
|
@@ -173,7 +174,13 @@ function newPC(peerId) {
|
|
| 173 |
const reconnectTimers = new Map(); // peerId -> attempt count
|
| 174 |
function scheduleReconnect(peerId, attempt = 0) {
|
| 175 |
if (!names.has(peerId) || chans.has(peerId) || reconnectTimers.has(peerId)) return;
|
| 176 |
-
if (attempt >=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
const delay = 1500 * Math.pow(2, attempt);
|
| 178 |
reconnectTimers.set(peerId, setTimeout(() => {
|
| 179 |
reconnectTimers.delete(peerId);
|
|
@@ -353,26 +360,51 @@ function readCfgFromUI() {
|
|
| 353 |
// lr crosses the wire as float32 β fround here so the leader trains with
|
| 354 |
// the exact same value the followers decode (else Adam forks the weights)
|
| 355 |
return { c: +ui.cfgC.value, t: +ui.cfgT.value, b: +ui.cfgB.value,
|
| 356 |
-
steps: +ui.cfgSteps.value, lr: Math.fround(+ui.cfgLr.value / 1000)
|
|
|
|
| 357 |
}
|
| 358 |
function showCfgInUI(cfg) {
|
| 359 |
const set = (el, vid, val) => { el.value = val; document.getElementById(vid).textContent = val; };
|
| 360 |
set(ui.cfgC, "vcfgC", cfg.c); set(ui.cfgT, "vcfgT", cfg.t); set(ui.cfgB, "vcfgB", cfg.b);
|
| 361 |
set(ui.cfgSteps, "vcfgSteps", cfg.steps);
|
| 362 |
set(ui.cfgLr, "vcfgLr", Math.round(cfg.lr * 1000));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
}
|
| 364 |
function broadcastConfig(cfg) {
|
| 365 |
-
const
|
|
|
|
| 366 |
new Int32Array(buf, 0, 5).set([CFG_SENTINEL, cfg.c, cfg.t, cfg.b, cfg.steps]);
|
| 367 |
new Float32Array(buf, 20, 1)[0] = cfg.lr;
|
| 368 |
new Int32Array(buf, 24, 1)[0] = Transformer.vocabSize(); // tokenizer must match
|
|
|
|
|
|
|
| 369 |
broadcast(buf);
|
| 370 |
}
|
| 371 |
-
function onConfig(peerId, buf) {
|
| 372 |
if (training) { log(`ignored settings from ${nmeOf(peerId)} (already training)`); return; }
|
| 373 |
const [, c, t, b, steps] = new Int32Array(buf, 0, 5);
|
| 374 |
const lr = new Float32Array(buf, 20, 1)[0];
|
| 375 |
const vocab = buf.byteLength >= 28 ? new Int32Array(buf, 24, 1)[0] : -1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
if (vocab !== Transformer.vocabSize()) {
|
| 377 |
log(`NOT JOINING: ${nmeOf(peerId)} uses a ${vocab}-token tokenizer, this device has ` +
|
| 378 |
`${Transformer.vocabSize()} (${Transformer.tokenizerName()}) β refresh so all devices match`);
|
|
@@ -382,10 +414,12 @@ function onConfig(peerId, buf) {
|
|
| 382 |
steps >= 1 && steps <= 10000 && lr > 0 && lr <= 0.2)) {
|
| 383 |
log(`rejected bad settings from ${nmeOf(peerId)}`); return;
|
| 384 |
}
|
| 385 |
-
const cfg = { c, t, b, steps, lr };
|
| 386 |
leaderId = peerId; // the starter leads sync for this run
|
| 387 |
showCfgInUI(cfg);
|
| 388 |
-
log(`${nmeOf(peerId)} started the group: width=${c}, seq=${t}, batch=${b}, ${steps} steps, lr=${lr}`
|
|
|
|
|
|
|
| 389 |
buildModel(cfg);
|
| 390 |
train(cfg); // follow automatically
|
| 391 |
}
|
|
@@ -601,7 +635,10 @@ async function train(cfg, resume) {
|
|
| 601 |
// resume runs keep `incoming` β grads that arrived while the bundle was
|
| 602 |
// downloading are exactly the steps we need to catch up with
|
| 603 |
if (!resume) { incoming.clear(); rosters.clear(); peerHashes.clear(); }
|
| 604 |
-
|
|
|
|
|
|
|
|
|
|
| 605 |
const cohort = [...chans.keys()]; // grows when a synced-in peer starts contributing
|
| 606 |
const steps = cfg.steps;
|
| 607 |
const opt = TrainCore.makeAdam(model.nParams, { lr: cfg.lr });
|
|
@@ -633,8 +670,16 @@ async function train(cfg, resume) {
|
|
| 633 |
const skipMine = !iLead && known && !known.includes(myId);
|
| 634 |
let loss = 0, grad = null;
|
| 635 |
if (!skipMine) {
|
| 636 |
-
|
| 637 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
}
|
| 639 |
let all;
|
| 640 |
if (!cohort.length && iLead) {
|
|
@@ -659,9 +704,28 @@ async function train(cfg, resume) {
|
|
| 659 |
// follower: apply the leader's roster verbatim, or stop
|
| 660 |
await waitFor(() => rosters.has(s) || !chans.has(leaderId), 15000);
|
| 661 |
if (!rosters.has(s)) {
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
break;
|
| 666 |
}
|
| 667 |
const roster = rosters.get(s);
|
|
@@ -678,6 +742,7 @@ async function train(cfg, resume) {
|
|
| 678 |
// divergence check: every contributor's pre-step weight hash must match mine
|
| 679 |
const hs = peerHashes.get(s);
|
| 680 |
const roster = rosters.get(s) || [myId];
|
|
|
|
| 681 |
if (hs) {
|
| 682 |
const bad = roster.find(id => id !== myId && hs.has(id) && hs.get(id).hash !== whash);
|
| 683 |
if (bad) { halted = `weights diverged from ${nmeOf(bad)} (detected at step ${s + 1})`; break; }
|
|
@@ -822,17 +887,13 @@ function buildModel(cfg) {
|
|
| 822 |
}
|
| 823 |
updatePeers();
|
| 824 |
connectSignaling();
|
| 825 |
-
// dataset: stream
|
| 826 |
-
ui.
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
log(`dataset: ${name} β ${(chars / 1000).toFixed(0)}k chars loaded (each device streams its own slice)`);
|
| 830 |
-
}).catch((e) => {
|
| 831 |
-
ui.dataset.textContent = "built-in corpus (offline)";
|
| 832 |
-
log(`dataset: FineWeb-Edu stream unavailable (${e.message}) β using the built-in corpus`);
|
| 833 |
-
});
|
| 834 |
-
ui.start.onclick = () => {
|
| 835 |
const cfg = readCfgFromUI();
|
|
|
|
|
|
|
| 836 |
leaderId = null; // I pressed Start: I lead sync
|
| 837 |
buildModel(cfg);
|
| 838 |
broadcastConfig(cfg); // everyone follows these settings
|
|
|
|
| 37 |
cfgB: document.getElementById("cfgB"),
|
| 38 |
cfgSteps: document.getElementById("cfgSteps"),
|
| 39 |
cfgLr: document.getElementById("cfgLr"),
|
| 40 |
+
cfgData: document.getElementById("cfgData"),
|
| 41 |
dataset: document.getElementById("dataset"),
|
| 42 |
tokenizer: document.getElementById("tokenizer"),
|
| 43 |
kit: document.getElementById("kit"),
|
|
|
|
| 174 |
const reconnectTimers = new Map(); // peerId -> attempt count
|
| 175 |
function scheduleReconnect(peerId, attempt = 0) {
|
| 176 |
if (!names.has(peerId) || chans.has(peerId) || reconnectTimers.has(peerId)) return;
|
| 177 |
+
if (attempt >= 5) { // 5 tries, then they're out of the group
|
| 178 |
+
log(`${nmeOf(peerId)} unreachable after 5 reconnect attempts β removed from the group`);
|
| 179 |
+
cleanupPeer(peerId);
|
| 180 |
+
names.delete(peerId);
|
| 181 |
+
updatePeers();
|
| 182 |
+
return;
|
| 183 |
+
}
|
| 184 |
const delay = 1500 * Math.pow(2, attempt);
|
| 185 |
reconnectTimers.set(peerId, setTimeout(() => {
|
| 186 |
reconnectTimers.delete(peerId);
|
|
|
|
| 360 |
// lr crosses the wire as float32 β fround here so the leader trains with
|
| 361 |
// the exact same value the followers decode (else Adam forks the weights)
|
| 362 |
return { c: +ui.cfgC.value, t: +ui.cfgT.value, b: +ui.cfgB.value,
|
| 363 |
+
steps: +ui.cfgSteps.value, lr: Math.fround(+ui.cfgLr.value / 1000),
|
| 364 |
+
ds: (ui.cfgData.value || "").trim() };
|
| 365 |
}
|
| 366 |
function showCfgInUI(cfg) {
|
| 367 |
const set = (el, vid, val) => { el.value = val; document.getElementById(vid).textContent = val; };
|
| 368 |
set(ui.cfgC, "vcfgC", cfg.c); set(ui.cfgT, "vcfgT", cfg.t); set(ui.cfgB, "vcfgB", cfg.b);
|
| 369 |
set(ui.cfgSteps, "vcfgSteps", cfg.steps);
|
| 370 |
set(ui.cfgLr, "vcfgLr", Math.round(cfg.lr * 1000));
|
| 371 |
+
if (cfg.ds) ui.cfgData.value = cfg.ds;
|
| 372 |
+
}
|
| 373 |
+
// switch the training text to a chosen dataset (whole group uses the same one);
|
| 374 |
+
// on failure the built-in corpus stays and we log why
|
| 375 |
+
async function loadDataset(ds) {
|
| 376 |
+
if (!ds) return;
|
| 377 |
+
ui.dataset.textContent = `streaming ${ds}β¦`;
|
| 378 |
+
try {
|
| 379 |
+
const { name, chars } = await Transformer.streamDataset(ds);
|
| 380 |
+
ui.dataset.textContent = name;
|
| 381 |
+
log(`dataset: ${name} β ${(chars / 1000).toFixed(0)}k chars (this device's own slice)`);
|
| 382 |
+
} catch (e) {
|
| 383 |
+
ui.dataset.textContent = "built-in corpus";
|
| 384 |
+
log(`dataset "${ds}" unavailable (${e.message}) β using the built-in corpus`);
|
| 385 |
+
}
|
| 386 |
}
|
| 387 |
function broadcastConfig(cfg) {
|
| 388 |
+
const dsBytes = new TextEncoder().encode((cfg.ds || "").slice(0, 96));
|
| 389 |
+
const buf = new ArrayBuffer(32 + dsBytes.length);
|
| 390 |
new Int32Array(buf, 0, 5).set([CFG_SENTINEL, cfg.c, cfg.t, cfg.b, cfg.steps]);
|
| 391 |
new Float32Array(buf, 20, 1)[0] = cfg.lr;
|
| 392 |
new Int32Array(buf, 24, 1)[0] = Transformer.vocabSize(); // tokenizer must match
|
| 393 |
+
new Int32Array(buf, 28, 1)[0] = dsBytes.length;
|
| 394 |
+
new Uint8Array(buf, 32).set(dsBytes);
|
| 395 |
broadcast(buf);
|
| 396 |
}
|
| 397 |
+
async function onConfig(peerId, buf) {
|
| 398 |
if (training) { log(`ignored settings from ${nmeOf(peerId)} (already training)`); return; }
|
| 399 |
const [, c, t, b, steps] = new Int32Array(buf, 0, 5);
|
| 400 |
const lr = new Float32Array(buf, 20, 1)[0];
|
| 401 |
const vocab = buf.byteLength >= 28 ? new Int32Array(buf, 24, 1)[0] : -1;
|
| 402 |
+
let ds = "";
|
| 403 |
+
if (buf.byteLength >= 32) {
|
| 404 |
+
const dsLen = new Int32Array(buf, 28, 1)[0];
|
| 405 |
+
if (dsLen > 0 && dsLen <= 96 && buf.byteLength === 32 + dsLen)
|
| 406 |
+
ds = new TextDecoder().decode(new Uint8Array(buf, 32, dsLen));
|
| 407 |
+
}
|
| 408 |
if (vocab !== Transformer.vocabSize()) {
|
| 409 |
log(`NOT JOINING: ${nmeOf(peerId)} uses a ${vocab}-token tokenizer, this device has ` +
|
| 410 |
`${Transformer.vocabSize()} (${Transformer.tokenizerName()}) β refresh so all devices match`);
|
|
|
|
| 414 |
steps >= 1 && steps <= 10000 && lr > 0 && lr <= 0.2)) {
|
| 415 |
log(`rejected bad settings from ${nmeOf(peerId)}`); return;
|
| 416 |
}
|
| 417 |
+
const cfg = { c, t, b, steps, lr: Math.fround(lr), ds };
|
| 418 |
leaderId = peerId; // the starter leads sync for this run
|
| 419 |
showCfgInUI(cfg);
|
| 420 |
+
log(`${nmeOf(peerId)} started the group: width=${c}, seq=${t}, batch=${b}, ${steps} steps, lr=${lr}` +
|
| 421 |
+
(ds ? `, dataset=${ds}` : ""));
|
| 422 |
+
if (ds) await loadDataset(ds); // same dataset as the starter
|
| 423 |
buildModel(cfg);
|
| 424 |
train(cfg); // follow automatically
|
| 425 |
}
|
|
|
|
| 635 |
// resume runs keep `incoming` β grads that arrived while the bundle was
|
| 636 |
// downloading are exactly the steps we need to catch up with
|
| 637 |
if (!resume) { incoming.clear(); rosters.clear(); peerHashes.clear(); }
|
| 638 |
+
let iLead = !resume && leaderId === null; // set by Start (null) or onConfig/onResume
|
| 639 |
+
let lastRoster = null; // the promotion electorate: last applied roster
|
| 640 |
+
let lastLocal = null; // this step's own grad (reused on failover redo)
|
| 641 |
+
const deadLeaders = new Set(); // leaders that left this run
|
| 642 |
const cohort = [...chans.keys()]; // grows when a synced-in peer starts contributing
|
| 643 |
const steps = cfg.steps;
|
| 644 |
const opt = TrainCore.makeAdam(model.nParams, { lr: cfg.lr });
|
|
|
|
| 670 |
const skipMine = !iLead && known && !known.includes(myId);
|
| 671 |
let loss = 0, grad = null;
|
| 672 |
if (!skipMine) {
|
| 673 |
+
// compute each step's gradient EXACTLY once: a leader-failover redo
|
| 674 |
+
// must reuse the grad already broadcast β recomputing would give peers
|
| 675 |
+
// a different gradient than the one they hold, forking the weights
|
| 676 |
+
if (lastLocal && lastLocal.s === s) {
|
| 677 |
+
({ loss, grad } = lastLocal);
|
| 678 |
+
} else {
|
| 679 |
+
({ loss, grad } = await localStep());
|
| 680 |
+
lastLocal = { s, loss, grad };
|
| 681 |
+
await broadcastGrad(s, whash, loss, grad);
|
| 682 |
+
}
|
| 683 |
}
|
| 684 |
let all;
|
| 685 |
if (!cohort.length && iLead) {
|
|
|
|
| 704 |
// follower: apply the leader's roster verbatim, or stop
|
| 705 |
await waitFor(() => rosters.has(s) || !chans.has(leaderId), 15000);
|
| 706 |
if (!rosters.has(s)) {
|
| 707 |
+
if (!chans.has(leaderId)) {
|
| 708 |
+
// leader failover: everyone promotes the same next peer β the
|
| 709 |
+
// lowest-numbered member of the LAST APPLIED roster (identical on
|
| 710 |
+
// every device) that hasn't already died as leader. All followers
|
| 711 |
+
// hold bit-identical weights + Adam state, so the new leader
|
| 712 |
+
// continues the run seamlessly; no state transfer needed.
|
| 713 |
+
deadLeaders.add(leaderId);
|
| 714 |
+
const electorate = (lastRoster || [myId, leaderId]);
|
| 715 |
+
const next = electorate.filter(id => !deadLeaders.has(id))
|
| 716 |
+
.sort((a, b) => +a.slice(1) - +b.slice(1))[0];
|
| 717 |
+
if (!next) { halted = "the sync leader left and no one remains to promote"; break; }
|
| 718 |
+
if (next === myId) {
|
| 719 |
+
iLead = true; leaderId = null;
|
| 720 |
+
cohort.length = 0; cohort.push(...chans.keys());
|
| 721 |
+
log(`the leader left β I take over sync leadership at step ${s + 1} (cohort ${cohort.length})`);
|
| 722 |
+
} else {
|
| 723 |
+
leaderId = next;
|
| 724 |
+
log(`the leader left β ${nmeOf(next)} takes over sync from step ${s + 1}`);
|
| 725 |
+
}
|
| 726 |
+
s--; continue; // redo this step under the new leader
|
| 727 |
+
}
|
| 728 |
+
halted = `no roster from ${nmeOf(leaderId)} for step ${s + 1}`;
|
| 729 |
break;
|
| 730 |
}
|
| 731 |
const roster = rosters.get(s);
|
|
|
|
| 742 |
// divergence check: every contributor's pre-step weight hash must match mine
|
| 743 |
const hs = peerHashes.get(s);
|
| 744 |
const roster = rosters.get(s) || [myId];
|
| 745 |
+
lastRoster = roster; // promotion electorate if the leader dies
|
| 746 |
if (hs) {
|
| 747 |
const bad = roster.find(id => id !== myId && hs.has(id) && hs.get(id).hash !== whash);
|
| 748 |
if (bad) { halted = `weights diverged from ${nmeOf(bad)} (detected at step ${s + 1})`; break; }
|
|
|
|
| 887 |
}
|
| 888 |
updatePeers();
|
| 889 |
connectSignaling();
|
| 890 |
+
// dataset: stream the chosen dataset from HuggingFace; built-in corpus if offline
|
| 891 |
+
loadDataset((ui.cfgData.value || "").trim());
|
| 892 |
+
ui.start.onclick = async () => {
|
| 893 |
+
if (training) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
const cfg = readCfgFromUI();
|
| 895 |
+
ui.start.disabled = true;
|
| 896 |
+
if (cfg.ds) await loadDataset(cfg.ds); // my pick becomes the group's dataset
|
| 897 |
leaderId = null; // I pressed Start: I lead sync
|
| 898 |
buildModel(cfg);
|
| 899 |
broadcastConfig(cfg); // everyone follows these settings
|
web/public/index.html
CHANGED
|
@@ -72,7 +72,7 @@
|
|
| 72 |
</head>
|
| 73 |
<body>
|
| 74 |
<h1>πΌ DaisyChain-Web</h1>
|
| 75 |
-
<p class="sub">Open this on your other devices <b>on the same network</b> and they
|
| 76 |
|
| 77 |
<div class="card" id="lobby" style="display:none;text-align:center">
|
| 78 |
<div class="lbl">π‘ Get started</div>
|
|
@@ -118,6 +118,11 @@
|
|
| 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 |
|
|
|
|
| 72 |
</head>
|
| 73 |
<body>
|
| 74 |
<h1>πΌ DaisyChain-Web</h1>
|
| 75 |
+
<p class="sub">Open this on your other devices <b>on the same network</b> and they <b>pretrain a language model from scratch</b> together β peer-to-peer, right in the browser, through the emulated GPU logic. This is pretraining, not fine-tuning: every run starts from random weights. Only devices on your network are grouped (like Snapdrop). To invite people across networks, everyone opens <code>?room=YOUR-CODE</code> β the person who created the room approves each device before it can join.</p>
|
| 76 |
|
| 77 |
<div class="card" id="lobby" style="display:none;text-align:center">
|
| 78 |
<div class="lbl">π‘ Get started</div>
|
|
|
|
| 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 |
+
<label class="slbl" for="cfgData">Dataset (HuggingFace)</label>
|
| 122 |
+
<input type="text" id="cfgData" value="HuggingFaceFW/fineweb-edu" spellcheck="false"
|
| 123 |
+
style="width:100%;padding:9px 11px;border-radius:8px;border:1px solid var(--card-border);background:transparent;color:inherit;font-family:'Courier New',monospace;font-size:.9rem">
|
| 124 |
+
<p class="note" style="margin:.4rem 0 0">Any public dataset with a <code style="background:rgba(74,124,46,.12);padding:1px 4px;border-radius:4px">text</code> column works β each device streams its own random slice. Whoever presses Start picks it for the whole group; if streaming fails, the built-in corpus is used.</p>
|
| 125 |
+
<p class="note" style="margin:.6rem 0 0"><b>This is pretraining, not fine-tuning</b> β every run trains a brand-new model from random weights. You are watching a language model learn from scratch.</p>
|
| 126 |
<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>
|
| 127 |
</div>
|
| 128 |
|
web/public/transformer.js
CHANGED
|
@@ -87,24 +87,28 @@
|
|
| 87 |
let IDS = encode(CORPUS);
|
| 88 |
let DATASET = "built-in corpus";
|
| 89 |
|
| 90 |
-
// Stream real training text
|
| 91 |
-
// datasets-server rows API. Each device pulls its own
|
| 92 |
-
// data parallelism β batches were always per-device
|
| 93 |
-
// API failure the built-in corpus stays in place.
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
| 95 |
const offset = Math.floor(Math.random() * 9900);
|
| 96 |
-
const url =
|
| 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 =
|
| 106 |
return { name: DATASET, chars: CORPUS.length };
|
| 107 |
}
|
|
|
|
| 108 |
function datasetName() { return DATASET; }
|
| 109 |
|
| 110 |
// ---- verified matmul: float in -> quantize -> LUT multiply -> dequant -----
|
|
@@ -373,7 +377,7 @@
|
|
| 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; }
|
|
|
|
| 87 |
let IDS = encode(CORPUS);
|
| 88 |
let DATASET = "built-in corpus";
|
| 89 |
|
| 90 |
+
// Stream real training text from any public HuggingFace dataset with a
|
| 91 |
+
// `text` column, via the datasets-server rows API. Each device pulls its own
|
| 92 |
+
// random slice (that's data parallelism β batches were always per-device
|
| 93 |
+
// anyway). Offline or on API failure the built-in corpus stays in place.
|
| 94 |
+
const DEFAULT_DS = "HuggingFaceFW/fineweb-edu";
|
| 95 |
+
async function streamDataset(ds) {
|
| 96 |
+
ds = (ds || DEFAULT_DS).trim();
|
| 97 |
+
if (!/^[\w.-]+\/[\w.-]+$/.test(ds)) throw new Error(`invalid dataset id "${ds}"`);
|
| 98 |
const offset = Math.floor(Math.random() * 9900);
|
| 99 |
+
const url = `https://datasets-server.huggingface.co/rows?dataset=${encodeURIComponent(ds)}` +
|
| 100 |
`&config=default&split=train&offset=${offset}&length=100`;
|
| 101 |
const r = await fetch(url);
|
| 102 |
if (!r.ok) throw new Error(`datasets-server HTTP ${r.status}`);
|
| 103 |
const j = await r.json();
|
| 104 |
+
const text = j.rows.map(x => String(x.row.text ?? "")).join("\n").replace(/[^\x20-\x7e\n]/g, " ");
|
| 105 |
+
if (text.length < 10000) throw new Error("too little text returned (does the dataset have a `text` column?)");
|
| 106 |
CORPUS = text.slice(0, 500000);
|
| 107 |
IDS = encode(CORPUS);
|
| 108 |
+
DATASET = `${ds} (streamed)`;
|
| 109 |
return { name: DATASET, chars: CORPUS.length };
|
| 110 |
}
|
| 111 |
+
const streamFineWebEdu = () => streamDataset(DEFAULT_DS);
|
| 112 |
function datasetName() { return DATASET; }
|
| 113 |
|
| 114 |
// ---- verified matmul: float in -> quantize -> LUT multiply -> dequant -----
|
|
|
|
| 377 |
}
|
| 378 |
|
| 379 |
const api = { init, trainStep, applyUpdate, getFlatParams, setFlatParams, generate,
|
| 380 |
+
streamFineWebEdu, streamDataset, datasetName, loadTokenizer, loadTokenizerData,
|
| 381 |
vocabSize, tokenizerName, encode, decode };
|
| 382 |
if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); V = require("./verified_core.js"); module.exports = api; }
|
| 383 |
else { TC = root.TrainCore; V = root.Verified; root.Transformer = api; }
|