Web demo: room-first lobby + group training settings sliders
Browse files- web/public/app.js +110 -21
- web/public/index.html +30 -0
- web/server.js +6 -0
web/public/app.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
| 2 |
// train a shared model together β averaging gradients over the data channels.
|
| 3 |
"use strict";
|
| 4 |
|
| 5 |
-
const DIN = 16,
|
| 6 |
-
|
| 7 |
const STUN = [{ urls: "stun:stun.l.google.com:19302" }];
|
| 8 |
|
| 9 |
const ui = {
|
|
@@ -21,6 +21,12 @@ const ui = {
|
|
| 21 |
load: document.getElementById("load"),
|
| 22 |
loadBtn: document.getElementById("loadBtn"),
|
| 23 |
requests: document.getElementById("requests"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
};
|
| 25 |
function log(m) { ui.log.textContent = `${new Date().toLocaleTimeString()} ${m}\n` + ui.log.textContent; }
|
| 26 |
function setStatus(s) { ui.status.textContent = s; }
|
|
@@ -156,7 +162,7 @@ const CKPT_SENTINEL = -2; // wire: [int32 -2][checkpoint by
|
|
| 156 |
function packCheckpoint() {
|
| 157 |
const buf = new ArrayBuffer(8 + 16 + (W1.length + W2.length) * 4);
|
| 158 |
new Uint8Array(buf, 0, 8).set([...CKPT_MAGIC].map(c => c.charCodeAt(0)));
|
| 159 |
-
new Int32Array(buf, 8, 4).set([DIN,
|
| 160 |
new Float32Array(buf, 24, W1.length).set(W1);
|
| 161 |
new Float32Array(buf, 24 + W1.length * 4, W2.length).set(W2);
|
| 162 |
return buf;
|
|
@@ -165,13 +171,14 @@ function parseCheckpoint(buf) {
|
|
| 165 |
const magic = String.fromCharCode(...new Uint8Array(buf, 0, 8));
|
| 166 |
if (magic !== CKPT_MAGIC) throw new Error("not a DaisyChain checkpoint");
|
| 167 |
const [din, h, dout, steps] = new Int32Array(buf, 8, 4);
|
| 168 |
-
if (din !== DIN ||
|
| 169 |
-
throw new Error(`shape mismatch: file is ${din}Γ${h}Γ${dout}, this build is ${DIN}Γ
|
| 170 |
if (buf.byteLength !== 24 + (din * h + h * dout) * 4) throw new Error("truncated checkpoint");
|
| 171 |
-
return { steps, w1: new Float32Array(buf.slice(24, 24 + din * h * 4)),
|
| 172 |
w2: new Float32Array(buf.slice(24 + din * h * 4)) };
|
| 173 |
}
|
| 174 |
function applyCheckpoint(ck, from) {
|
|
|
|
| 175 |
W1.set(ck.w1); W2.set(ck.w2); trainedSteps = ck.steps;
|
| 176 |
ui.save.disabled = false;
|
| 177 |
ui.step.textContent = `${ck.steps} baked in`;
|
|
@@ -190,11 +197,41 @@ function saveCheckpoint() {
|
|
| 190 |
const blob = new Blob([packCheckpoint()], { type: "application/octet-stream" });
|
| 191 |
const a = document.createElement("a");
|
| 192 |
a.href = URL.createObjectURL(blob);
|
| 193 |
-
a.download = `daisychain-${DIN}x${
|
| 194 |
a.click();
|
| 195 |
URL.revokeObjectURL(a.href);
|
| 196 |
}
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
// ---- gradient wire format: [int32 step][float32 grad...] -----------------
|
| 199 |
function packGrad(step, grad) {
|
| 200 |
const buf = new ArrayBuffer(4 + grad.byteLength);
|
|
@@ -206,6 +243,7 @@ const waiters = new Set(); // pending waitForGrads checker
|
|
| 206 |
function wake() { for (const w of waiters) w(); }
|
| 207 |
function onGrad(peerId, buf) {
|
| 208 |
const step = new Int32Array(buf, 0, 1)[0];
|
|
|
|
| 209 |
if (step === CKPT_SENTINEL) { // a peer pushed a checkpoint
|
| 210 |
if (training) { log(`ignored checkpoint from ${nmeOf(peerId)} (training in progress)`); return; }
|
| 211 |
try { applyCheckpoint(parseCheckpoint(buf.slice(4)), nmeOf(peerId)); }
|
|
@@ -250,12 +288,13 @@ async function localStep() {
|
|
| 250 |
}
|
| 251 |
|
| 252 |
// ---- the training loop -----------------------------------------------------
|
| 253 |
-
async function train() {
|
| 254 |
if (training) return; training = true; ui.start.disabled = true;
|
|
|
|
| 255 |
const cohort = [...chans.keys()]; // lock the cohort (departed peers are pruned per-step)
|
| 256 |
-
const steps =
|
| 257 |
-
const opt = TrainCore.makeAdam(W1.length + W2.length, { lr:
|
| 258 |
-
log(`training started β cohort ${cohort.length} peer(s), world ${cohort.length + 1}, optimizer ${opt.name}`);
|
| 259 |
for (let s = 0; s < steps; s++) {
|
| 260 |
const { loss, grad } = await localStep();
|
| 261 |
broadcastGrad(s, grad);
|
|
@@ -278,18 +317,55 @@ async function train() {
|
|
| 278 |
training = false;
|
| 279 |
ui.save.disabled = false;
|
| 280 |
ui.start.disabled = false;
|
|
|
|
| 281 |
}
|
| 282 |
|
| 283 |
// 2-layer float target (matches the model shape) for a learnable task
|
| 284 |
-
function target(X) {
|
| 285 |
-
const Wt1 = randn(DIN *
|
| 286 |
-
const hpre = TrainCore.matmul(X, Wt1, NPER, DIN,
|
| 287 |
for (let i = 0; i < hpre.length; i++) hpre[i] = Math.max(0, hpre[i]);
|
| 288 |
-
return TrainCore.matmul(hpre, Wt2, NPER,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
}
|
| 290 |
|
| 291 |
// ---- boot ------------------------------------------------------------------
|
| 292 |
(async function () {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
// Neural Units are mandatory: no LUTs -> no training, period. There is no
|
| 294 |
// float fallback path anywhere in this app; both backends (WebGPU shader and
|
| 295 |
// CPU JS) compute every product through the verified mul8 LUT.
|
|
@@ -310,15 +386,28 @@ function target(X) {
|
|
| 310 |
return; // no signaling, no training
|
| 311 |
}
|
| 312 |
ui.backend.textContent = `${compute.backend.toUpperCase()} β ${compute.label} Β· through verified INT8 units`;
|
| 313 |
-
//
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
Ydata = target(Xdata);
|
| 318 |
ui.me.textContent = deviceName;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
updatePeers();
|
| 320 |
connectSignaling();
|
| 321 |
-
ui.start.onclick =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
ui.save.onclick = saveCheckpoint;
|
| 323 |
ui.loadBtn.onclick = () => { if (training) { log("can't load a checkpoint mid-training"); return; } ui.load.click(); };
|
| 324 |
ui.load.onchange = async () => {
|
|
|
|
| 2 |
// train a shared model together β averaging gradients over the data channels.
|
| 3 |
"use strict";
|
| 4 |
|
| 5 |
+
const DIN = 16, DOUT = 4, NPER = 128;
|
| 6 |
+
let D = { n: NPER, din: DIN, h: 16, dout: DOUT }; // h is set by the training-settings slider
|
| 7 |
const STUN = [{ urls: "stun:stun.l.google.com:19302" }];
|
| 8 |
|
| 9 |
const ui = {
|
|
|
|
| 21 |
load: document.getElementById("load"),
|
| 22 |
loadBtn: document.getElementById("loadBtn"),
|
| 23 |
requests: document.getElementById("requests"),
|
| 24 |
+
roomInfo: document.getElementById("roomInfo"),
|
| 25 |
+
roomCode: document.getElementById("roomCode"),
|
| 26 |
+
copyLink: document.getElementById("copyLink"),
|
| 27 |
+
cfgH: document.getElementById("cfgH"),
|
| 28 |
+
cfgSteps: document.getElementById("cfgSteps"),
|
| 29 |
+
cfgLr: document.getElementById("cfgLr"),
|
| 30 |
};
|
| 31 |
function log(m) { ui.log.textContent = `${new Date().toLocaleTimeString()} ${m}\n` + ui.log.textContent; }
|
| 32 |
function setStatus(s) { ui.status.textContent = s; }
|
|
|
|
| 162 |
function packCheckpoint() {
|
| 163 |
const buf = new ArrayBuffer(8 + 16 + (W1.length + W2.length) * 4);
|
| 164 |
new Uint8Array(buf, 0, 8).set([...CKPT_MAGIC].map(c => c.charCodeAt(0)));
|
| 165 |
+
new Int32Array(buf, 8, 4).set([DIN, D.h, DOUT, trainedSteps]);
|
| 166 |
new Float32Array(buf, 24, W1.length).set(W1);
|
| 167 |
new Float32Array(buf, 24 + W1.length * 4, W2.length).set(W2);
|
| 168 |
return buf;
|
|
|
|
| 171 |
const magic = String.fromCharCode(...new Uint8Array(buf, 0, 8));
|
| 172 |
if (magic !== CKPT_MAGIC) throw new Error("not a DaisyChain checkpoint");
|
| 173 |
const [din, h, dout, steps] = new Int32Array(buf, 8, 4);
|
| 174 |
+
if (din !== DIN || dout !== DOUT || h < 8 || h > 64)
|
| 175 |
+
throw new Error(`shape mismatch: file is ${din}Γ${h}Γ${dout}, this build is ${DIN}Γ(8β64)Γ${DOUT}`);
|
| 176 |
if (buf.byteLength !== 24 + (din * h + h * dout) * 4) throw new Error("truncated checkpoint");
|
| 177 |
+
return { h, steps, w1: new Float32Array(buf.slice(24, 24 + din * h * 4)),
|
| 178 |
w2: new Float32Array(buf.slice(24 + din * h * 4)) };
|
| 179 |
}
|
| 180 |
function applyCheckpoint(ck, from) {
|
| 181 |
+
if (ck.h !== D.h) { buildModel(ck.h); showCfgInUI({ h: ck.h, steps: +ui.cfgSteps.value, lr: +ui.cfgLr.value / 100 }); }
|
| 182 |
W1.set(ck.w1); W2.set(ck.w2); trainedSteps = ck.steps;
|
| 183 |
ui.save.disabled = false;
|
| 184 |
ui.step.textContent = `${ck.steps} baked in`;
|
|
|
|
| 197 |
const blob = new Blob([packCheckpoint()], { type: "application/octet-stream" });
|
| 198 |
const a = document.createElement("a");
|
| 199 |
a.href = URL.createObjectURL(blob);
|
| 200 |
+
a.download = `daisychain-${DIN}x${D.h}x${DOUT}-step${trainedSteps}.pt`;
|
| 201 |
a.click();
|
| 202 |
URL.revokeObjectURL(a.href);
|
| 203 |
}
|
| 204 |
|
| 205 |
+
// ---- training config: whoever presses Start sets it for the whole group ----
|
| 206 |
+
const CFG_SENTINEL = -3; // wire: [int32 -3][int32 h,steps][f32 lr]
|
| 207 |
+
function readCfgFromUI() {
|
| 208 |
+
return { h: +ui.cfgH.value, steps: +ui.cfgSteps.value, lr: +ui.cfgLr.value / 100 };
|
| 209 |
+
}
|
| 210 |
+
function showCfgInUI(cfg) {
|
| 211 |
+
ui.cfgH.value = cfg.h; document.getElementById("vcfgH").textContent = cfg.h;
|
| 212 |
+
ui.cfgSteps.value = cfg.steps; document.getElementById("vcfgSteps").textContent = cfg.steps;
|
| 213 |
+
ui.cfgLr.value = Math.round(cfg.lr * 100); document.getElementById("vcfgLr").textContent = Math.round(cfg.lr * 100);
|
| 214 |
+
}
|
| 215 |
+
function broadcastConfig(cfg) {
|
| 216 |
+
const buf = new ArrayBuffer(16);
|
| 217 |
+
new Int32Array(buf, 0, 3).set([CFG_SENTINEL, cfg.h, cfg.steps]);
|
| 218 |
+
new Float32Array(buf, 12, 1)[0] = cfg.lr;
|
| 219 |
+
for (const dc of chans.values()) if (dc.readyState === "open") dc.send(buf);
|
| 220 |
+
}
|
| 221 |
+
function onConfig(peerId, buf) {
|
| 222 |
+
if (training) { log(`ignored settings from ${nmeOf(peerId)} (already training)`); return; }
|
| 223 |
+
const [, h, steps] = new Int32Array(buf, 0, 3);
|
| 224 |
+
const lr = new Float32Array(buf, 12, 1)[0];
|
| 225 |
+
if (!(h >= 8 && h <= 64 && steps >= 1 && steps <= 10000 && lr > 0 && lr <= 1)) {
|
| 226 |
+
log(`rejected bad settings from ${nmeOf(peerId)}`); return;
|
| 227 |
+
}
|
| 228 |
+
const cfg = { h, steps, lr };
|
| 229 |
+
showCfgInUI(cfg);
|
| 230 |
+
log(`${nmeOf(peerId)} started the group: hidden=${h}, ${steps} steps, lr=${lr}`);
|
| 231 |
+
buildModel(cfg.h);
|
| 232 |
+
train(cfg); // follow automatically
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
// ---- gradient wire format: [int32 step][float32 grad...] -----------------
|
| 236 |
function packGrad(step, grad) {
|
| 237 |
const buf = new ArrayBuffer(4 + grad.byteLength);
|
|
|
|
| 243 |
function wake() { for (const w of waiters) w(); }
|
| 244 |
function onGrad(peerId, buf) {
|
| 245 |
const step = new Int32Array(buf, 0, 1)[0];
|
| 246 |
+
if (step === CFG_SENTINEL) { onConfig(peerId, buf); return; }
|
| 247 |
if (step === CKPT_SENTINEL) { // a peer pushed a checkpoint
|
| 248 |
if (training) { log(`ignored checkpoint from ${nmeOf(peerId)} (training in progress)`); return; }
|
| 249 |
try { applyCheckpoint(parseCheckpoint(buf.slice(4)), nmeOf(peerId)); }
|
|
|
|
| 288 |
}
|
| 289 |
|
| 290 |
// ---- the training loop -----------------------------------------------------
|
| 291 |
+
async function train(cfg) {
|
| 292 |
if (training) return; training = true; ui.start.disabled = true;
|
| 293 |
+
[ui.cfgH, ui.cfgSteps, ui.cfgLr].forEach(el => el.disabled = true);
|
| 294 |
const cohort = [...chans.keys()]; // lock the cohort (departed peers are pruned per-step)
|
| 295 |
+
const steps = cfg.steps;
|
| 296 |
+
const opt = TrainCore.makeAdam(W1.length + W2.length, { lr: cfg.lr });
|
| 297 |
+
log(`training started β cohort ${cohort.length} peer(s), world ${cohort.length + 1}, hidden=${cfg.h}, optimizer ${opt.name}`);
|
| 298 |
for (let s = 0; s < steps; s++) {
|
| 299 |
const { loss, grad } = await localStep();
|
| 300 |
broadcastGrad(s, grad);
|
|
|
|
| 317 |
training = false;
|
| 318 |
ui.save.disabled = false;
|
| 319 |
ui.start.disabled = false;
|
| 320 |
+
[ui.cfgH, ui.cfgSteps, ui.cfgLr].forEach(el => el.disabled = false);
|
| 321 |
}
|
| 322 |
|
| 323 |
// 2-layer float target (matches the model shape) for a learnable task
|
| 324 |
+
function target(X, h) {
|
| 325 |
+
const Wt1 = randn(DIN * h, mulberry32(42)), Wt2 = randn(h * DOUT, mulberry32(43));
|
| 326 |
+
const hpre = TrainCore.matmul(X, Wt1, NPER, DIN, h);
|
| 327 |
for (let i = 0; i < hpre.length; i++) hpre[i] = Math.max(0, hpre[i]);
|
| 328 |
+
return TrainCore.matmul(hpre, Wt2, NPER, h, DOUT);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
// (re)build the model for a hidden size β deterministic shared init (same seeds
|
| 332 |
+
// on every peer), fresh per-peer data shard
|
| 333 |
+
function buildModel(h) {
|
| 334 |
+
D = { n: NPER, din: DIN, h, dout: DOUT };
|
| 335 |
+
W1 = randn(DIN * h, mulberry32(7));
|
| 336 |
+
W2 = randn(h * DOUT, mulberry32(8));
|
| 337 |
+
Xdata = randn(NPER * DIN);
|
| 338 |
+
Ydata = target(Xdata, h);
|
| 339 |
+
trainedSteps = 0;
|
| 340 |
}
|
| 341 |
|
| 342 |
// ---- boot ------------------------------------------------------------------
|
| 343 |
(async function () {
|
| 344 |
+
// Room-first deployments (the HF Space sets DAISY_FORCE_ROOMS): there is no
|
| 345 |
+
// LAN auto-grouping between strangers β visitors without a room code choose
|
| 346 |
+
// to create their own room (they become its host) or join one by code.
|
| 347 |
+
try {
|
| 348 |
+
const mode = await (await fetch("mode")).json();
|
| 349 |
+
if (mode.forceRooms && !room()) {
|
| 350 |
+
const lobby = document.getElementById("lobby");
|
| 351 |
+
for (const c of document.querySelectorAll(".card")) if (c !== lobby) c.style.display = "none";
|
| 352 |
+
lobby.style.display = "";
|
| 353 |
+
document.getElementById("createRoom").onclick = () => {
|
| 354 |
+
const code = (ADJ[Math.floor(Math.random() * ADJ.length)] + "-" +
|
| 355 |
+
NOUN[Math.floor(Math.random() * NOUN.length)] + "-" +
|
| 356 |
+
(100 + Math.floor(Math.random() * 900))).toLowerCase();
|
| 357 |
+
location.href = "?room=" + code;
|
| 358 |
+
};
|
| 359 |
+
const join = () => {
|
| 360 |
+
const code = document.getElementById("joinCode").value.trim().toLowerCase();
|
| 361 |
+
if (code) location.href = "?room=" + encodeURIComponent(code);
|
| 362 |
+
};
|
| 363 |
+
document.getElementById("joinRoom").onclick = join;
|
| 364 |
+
document.getElementById("joinCode").onkeydown = (e) => { if (e.key === "Enter") join(); };
|
| 365 |
+
return; // wait for the choice
|
| 366 |
+
}
|
| 367 |
+
} catch (e) {} // no /mode: local default
|
| 368 |
+
|
| 369 |
// Neural Units are mandatory: no LUTs -> no training, period. There is no
|
| 370 |
// float fallback path anywhere in this app; both backends (WebGPU shader and
|
| 371 |
// CPU JS) compute every product through the verified mul8 LUT.
|
|
|
|
| 386 |
return; // no signaling, no training
|
| 387 |
}
|
| 388 |
ui.backend.textContent = `${compute.backend.toUpperCase()} β ${compute.label} Β· through verified INT8 units`;
|
| 389 |
+
// slider readouts
|
| 390 |
+
for (const [el, v] of [[ui.cfgH, "vcfgH"], [ui.cfgSteps, "vcfgSteps"], [ui.cfgLr, "vcfgLr"]])
|
| 391 |
+
el.oninput = () => document.getElementById(v).textContent = el.value;
|
| 392 |
+
buildModel(+ui.cfgH.value);
|
|
|
|
| 393 |
ui.me.textContent = deviceName;
|
| 394 |
+
if (room()) {
|
| 395 |
+
ui.roomInfo.style.display = "";
|
| 396 |
+
ui.roomCode.textContent = room();
|
| 397 |
+
ui.copyLink.onclick = async () => {
|
| 398 |
+
try { await navigator.clipboard.writeText(location.href); ui.copyLink.textContent = "Copied!"; }
|
| 399 |
+
catch { ui.copyLink.textContent = location.href; }
|
| 400 |
+
setTimeout(() => ui.copyLink.textContent = "Copy invite link", 2000);
|
| 401 |
+
};
|
| 402 |
+
}
|
| 403 |
updatePeers();
|
| 404 |
connectSignaling();
|
| 405 |
+
ui.start.onclick = () => {
|
| 406 |
+
const cfg = readCfgFromUI();
|
| 407 |
+
buildModel(cfg.h);
|
| 408 |
+
broadcastConfig(cfg); // everyone follows these settings
|
| 409 |
+
train(cfg);
|
| 410 |
+
};
|
| 411 |
ui.save.onclick = saveCheckpoint;
|
| 412 |
ui.loadBtn.onclick = () => { if (training) { log("can't load a checkpoint mid-training"); return; } ui.load.click(); };
|
| 413 |
ui.load.onchange = async () => {
|
web/public/index.html
CHANGED
|
@@ -62,6 +62,9 @@
|
|
| 62 |
font-family: 'Courier New', monospace; }
|
| 63 |
@media (prefers-color-scheme: dark) { pre { background: rgba(0,0,0,0.25); } }
|
| 64 |
.note { color: var(--text-soft); font-size: .82rem; }
|
|
|
|
|
|
|
|
|
|
| 65 |
.note b { color: var(--warn); }
|
| 66 |
.diff { color: var(--accent-deep); font-weight: 600; font-size: .88rem; }
|
| 67 |
@media (prefers-color-scheme: dark) { .diff { color: var(--accent); } }
|
|
@@ -71,6 +74,17 @@
|
|
| 71 |
<h1>πΌ DaisyChain-Web</h1>
|
| 72 |
<p class="sub">Open this on your other devices <b>on the same network</b> and they train a shared model together β peer-to-peer, right in the browser, through the emulated GPU logic. 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>
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
<div class="card">
|
| 75 |
<div class="lbl">π² This device</div>
|
| 76 |
<div class="device" id="me">β</div>
|
|
@@ -81,9 +95,25 @@
|
|
| 81 |
<div class="card">
|
| 82 |
<div class="lbl">π Devices in your group</div>
|
| 83 |
<div class="row"><span class="v" id="peers" style="text-align:left">(none yet)</span></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
<div id="requests"></div>
|
| 85 |
</div>
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
<div class="card" style="text-align:center">
|
| 88 |
<button id="start" disabled>Start training</button>
|
| 89 |
<p class="note" style="margin:.6rem 0 0">Enabled once another device joins. (Or open a second tab to try it.)</p>
|
|
|
|
| 62 |
font-family: 'Courier New', monospace; }
|
| 63 |
@media (prefers-color-scheme: dark) { pre { background: rgba(0,0,0,0.25); } }
|
| 64 |
.note { color: var(--text-soft); font-size: .82rem; }
|
| 65 |
+
.slbl { display: flex; justify-content: space-between; font-size: .92rem; margin: 10px 0 4px; font-weight: 600; }
|
| 66 |
+
.sval { font-family: 'Courier New', monospace; color: var(--accent); font-weight: 700; }
|
| 67 |
+
input[type=range] { width: 100%; accent-color: var(--accent); }
|
| 68 |
.note b { color: var(--warn); }
|
| 69 |
.diff { color: var(--accent-deep); font-weight: 600; font-size: .88rem; }
|
| 70 |
@media (prefers-color-scheme: dark) { .diff { color: var(--accent); } }
|
|
|
|
| 74 |
<h1>πΌ DaisyChain-Web</h1>
|
| 75 |
<p class="sub">Open this on your other devices <b>on the same network</b> and they train a shared model together β peer-to-peer, right in the browser, through the emulated GPU logic. 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>
|
| 79 |
+
<p class="note" style="margin:0 0 12px">Create a room and invite your devices, or join a room someone shared with you.</p>
|
| 80 |
+
<button id="createRoom">Create a room</button>
|
| 81 |
+
<div style="display:flex;gap:8px;justify-content:center;margin-top:12px;flex-wrap:wrap">
|
| 82 |
+
<input type="text" id="joinCode" placeholder="room code" spellcheck="false"
|
| 83 |
+
style="padding:10px 12px;border-radius:8px;border:1px solid var(--card-border);background:transparent;color:inherit;font-family:'Courier New',monospace">
|
| 84 |
+
<button id="joinRoom" style="padding:10px 18px">Join</button>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
<div class="card">
|
| 89 |
<div class="lbl">π² This device</div>
|
| 90 |
<div class="device" id="me">β</div>
|
|
|
|
| 95 |
<div class="card">
|
| 96 |
<div class="lbl">π Devices in your group</div>
|
| 97 |
<div class="row"><span class="v" id="peers" style="text-align:left">(none yet)</span></div>
|
| 98 |
+
<div id="roomInfo" style="display:none;margin-top:8px">
|
| 99 |
+
<div class="row"><span class="k">Room</span><span class="v" id="roomCode"></span></div>
|
| 100 |
+
<div style="text-align:center;margin-top:6px"><button id="copyLink" style="padding:6px 14px;font-size:.85rem">Copy invite link</button></div>
|
| 101 |
+
<p class="note" style="margin:.5rem 0 0;text-align:center">Open the link on your other devices β you approve each one before it joins.</p>
|
| 102 |
+
</div>
|
| 103 |
<div id="requests"></div>
|
| 104 |
</div>
|
| 105 |
|
| 106 |
+
<div class="card">
|
| 107 |
+
<div class="lbl">π Training settings</div>
|
| 108 |
+
<label class="slbl">Hidden units <span class="sval" id="vcfgH">16</span></label>
|
| 109 |
+
<input type="range" id="cfgH" min="8" max="64" step="8" value="16">
|
| 110 |
+
<label class="slbl">Steps <span class="sval" id="vcfgSteps">300</span></label>
|
| 111 |
+
<input type="range" id="cfgSteps" min="100" max="1000" step="100" value="300">
|
| 112 |
+
<label class="slbl">Learning rate Γ100 <span class="sval" id="vcfgLr">20</span></label>
|
| 113 |
+
<input type="range" id="cfgLr" min="5" max="50" step="5" value="20">
|
| 114 |
+
<p class="note" style="margin:.5rem 0 0">Whoever presses Start sets the settings for the whole group β every device follows automatically.</p>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
<div class="card" style="text-align:center">
|
| 118 |
<button id="start" disabled>Start training</button>
|
| 119 |
<p class="note" style="margin:.6rem 0 0">Enabled once another device joins. (Or open a second tab to try it.)</p>
|
web/server.js
CHANGED
|
@@ -32,6 +32,12 @@ const TYPES = { ".html": "text/html", ".js": "text/javascript",
|
|
| 32 |
|
| 33 |
const server = http.createServer((req, res) => {
|
| 34 |
let p = decodeURIComponent(req.url.split("?")[0]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
if (p === "/") p = "/index.html";
|
| 36 |
const file = path.join(PUB, path.normalize(p));
|
| 37 |
if (!file.startsWith(PUB)) { res.writeHead(403); return res.end(); }
|
|
|
|
| 32 |
|
| 33 |
const server = http.createServer((req, res) => {
|
| 34 |
let p = decodeURIComponent(req.url.split("?")[0]);
|
| 35 |
+
if (p === "/mode") { // deployment flags for the client
|
| 36 |
+
res.writeHead(200, { "Content-Type": "application/json" });
|
| 37 |
+
// DAISY_FORCE_ROOMS=1 (set on the HF Space): no LAN auto-grouping β every
|
| 38 |
+
// visitor creates their own private room and invites devices by link
|
| 39 |
+
return res.end(JSON.stringify({ forceRooms: !!process.env.DAISY_FORCE_ROOMS }));
|
| 40 |
+
}
|
| 41 |
if (p === "/") p = "/index.html";
|
| 42 |
const file = path.join(PUB, path.normalize(p));
|
| 43 |
if (!file.startsWith(PUB)) { res.writeHead(403); return res.end(); }
|