Quazim0t0 commited on
Commit
00fdb1d
·
verified ·
1 Parent(s): 32f59c2

Web demo: weight tying + WebRTC resilience (relay fallback, keepalive)

Browse files
Files changed (3) hide show
  1. web/public/app.js +52 -6
  2. web/public/transformer.js +8 -5
  3. web/server.js +25 -2
web/public/app.js CHANGED
@@ -13,6 +13,7 @@ const STUN = [
13
  { urls: "turn:openrelay.metered.ca:443", username: "openrelayproject", credential: "openrelayproject" },
14
  { urls: "turns:openrelay.metered.ca:443?transport=tcp", username: "openrelayproject", credential: "openrelayproject" },
15
  ];
 
16
 
17
  const ui = {
18
  status: document.getElementById("status"),
@@ -129,6 +130,10 @@ function connectSignaling() {
129
  log(`${nmeOf(msg.id)} left`); cleanupPeer(msg.id); names.delete(msg.id); updatePeers();
130
  } else if (msg.type === "signal") {
131
  await onSignal(msg.from, msg.data);
 
 
 
 
132
  }
133
  };
134
  }
@@ -157,10 +162,18 @@ function addJoinRequest(id, name) {
157
  }
158
 
159
  function newPC(peerId) {
160
- const pc = new RTCPeerConnection({ iceServers: STUN });
161
  pc.onicecandidate = (e) => { if (e.candidate) signal(peerId, { candidate: e.candidate }); };
162
  pc.onconnectionstatechange = () => {
163
  if (pc.connectionState === "failed") { cleanupPeer(peerId); scheduleReconnect(peerId); }
 
 
 
 
 
 
 
 
164
  };
165
  pcs.set(peerId, pc);
166
  return pc;
@@ -171,14 +184,37 @@ function newPC(peerId) {
171
  // them), redial with backoff. Only the higher-numbered peer initiates, so both
172
  // sides don't collide (offer glare). If the peer truly left, names loses them
173
  // and the retries stop on their own.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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);
@@ -817,6 +853,7 @@ function buildModel(cfg) {
817
  // to create their own room (they become its host) or join one by code.
818
  try {
819
  const mode = await (await fetch("mode")).json();
 
820
  if (mode.forceRooms && !room()) {
821
  const lobby = document.getElementById("lobby");
822
  for (const c of document.querySelectorAll(".card")) if (c !== lobby) c.style.display = "none";
@@ -887,6 +924,15 @@ function buildModel(cfg) {
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 () => {
 
13
  { urls: "turn:openrelay.metered.ca:443", username: "openrelayproject", credential: "openrelayproject" },
14
  { urls: "turns:openrelay.metered.ca:443?transport=tcp", username: "openrelayproject", credential: "openrelayproject" },
15
  ];
16
+ let rtcConfig = { iceServers: STUN }; // /mode can override (own TURN etc.)
17
 
18
  const ui = {
19
  status: document.getElementById("status"),
 
130
  log(`${nmeOf(msg.id)} left`); cleanupPeer(msg.id); names.delete(msg.id); updatePeers();
131
  } else if (msg.type === "signal") {
132
  await onSignal(msg.from, msg.data);
133
+ } else if (msg.type === "ping") { // keepalive (proxies close idle sockets)
134
+ ws.send(JSON.stringify({ type: "pong" }));
135
+ } else if (msg.type === "relay") { // payload relayed via the server (WS fallback)
136
+ onGrad(msg.from, bufFromB64(msg.data));
137
  }
138
  };
139
  }
 
162
  }
163
 
164
  function newPC(peerId) {
165
+ const pc = new RTCPeerConnection(rtcConfig);
166
  pc.onicecandidate = (e) => { if (e.candidate) signal(peerId, { candidate: e.candidate }); };
167
  pc.onconnectionstatechange = () => {
168
  if (pc.connectionState === "failed") { cleanupPeer(peerId); scheduleReconnect(peerId); }
169
+ // "disconnected" often self-heals; give it a grace period, then treat as failed
170
+ if (pc.connectionState === "disconnected")
171
+ setTimeout(() => {
172
+ if (pcs.get(peerId) === pc && pc.connectionState === "disconnected") {
173
+ log(`${nmeOf(peerId)} connection did not recover — redialing`);
174
+ cleanupPeer(peerId); scheduleReconnect(peerId);
175
+ }
176
+ }, 4000);
177
  };
178
  pcs.set(peerId, pc);
179
  return pc;
 
184
  // them), redial with backoff. Only the higher-numbered peer initiates, so both
185
  // sides don't collide (offer glare). If the peer truly left, names loses them
186
  // and the retries stop on their own.
187
+ // ---- WebSocket relay fallback (PairDrop's WSPeer pattern) ---------------------
188
+ // A peer still in the room after 5 failed WebRTC attempts is reachable through
189
+ // the signaling server even though no direct path exists (symmetric NATs, no
190
+ // TURN). Install a pseudo data channel that relays binary over the WS instead
191
+ // of dropping them. Devices that actually left are removed via peer-left.
192
+ function b64FromBuf(buf) {
193
+ const u = new Uint8Array(buf); let s = "";
194
+ for (let i = 0; i < u.length; i += 0x8000) s += String.fromCharCode(...u.subarray(i, i + 0x8000));
195
+ return btoa(s);
196
+ }
197
+ function bufFromB64(s) { return Uint8Array.from(atob(s), c => c.charCodeAt(0)).buffer; }
198
+ function makeRelayChannel(peerId) {
199
+ return {
200
+ isRelay: true,
201
+ get readyState() { return ws && ws.readyState === 1 && names.has(peerId) ? "open" : "closed"; },
202
+ get bufferedAmount() { return ws ? ws.bufferedAmount : 0; },
203
+ send(buf) { ws.send(JSON.stringify({ type: "relay", to: peerId, data: b64FromBuf(buf) })); },
204
+ close() { chans.delete(peerId); },
205
+ };
206
+ }
207
+
208
  const reconnectTimers = new Map(); // peerId -> attempt count
209
  function scheduleReconnect(peerId, attempt = 0) {
210
  if (!names.has(peerId) || chans.has(peerId) || reconnectTimers.has(peerId)) return;
211
+ if (attempt >= 5) { // no direct path fall back to server relay
212
+ if (names.has(peerId)) {
213
+ log(`no direct WebRTC path to ${nmeOf(peerId)} after 5 attempts — relaying through the server instead`);
214
+ chans.set(peerId, makeRelayChannel(peerId));
215
+ updatePeers(); wake();
216
+ ui.start.disabled = false;
217
+ }
218
  return;
219
  }
220
  const delay = 1500 * Math.pow(2, attempt);
 
853
  // to create their own room (they become its host) or join one by code.
854
  try {
855
  const mode = await (await fetch("mode")).json();
856
+ if (mode.rtc) rtcConfig = mode.rtc; // operator-provided ICE config
857
  if (mode.forceRooms && !room()) {
858
  const lobby = document.getElementById("lobby");
859
  for (const c of document.querySelectorAll(".card")) if (c !== lobby) c.style.display = "none";
 
924
  }
925
  updatePeers();
926
  connectSignaling();
927
+ // phones suspend the page (screen off, app switch) — reconnect the moment we
928
+ // come back, and on network changes (wifi <-> cellular), PairDrop-style
929
+ document.addEventListener("visibilitychange", () => {
930
+ if (!document.hidden && (!ws || ws.readyState > 1)) { log("page woke — reconnecting"); connectSignaling(); }
931
+ });
932
+ if (navigator.connection && navigator.connection.addEventListener)
933
+ navigator.connection.addEventListener("change", () => {
934
+ if (!ws || ws.readyState > 1) { log("network changed — reconnecting"); connectSignaling(); }
935
+ });
936
  // dataset: stream the chosen dataset from HuggingFace; built-in corpus if offline
937
  loadDataset((ui.cfgData.value || "").trim());
938
  ui.start.onclick = async () => {
web/public/transformer.js CHANGED
@@ -169,7 +169,9 @@
169
  Wv: add(`b${l}.Wv`, mk(c * c, 0.08)), Wo: add(`b${l}.Wo`, mk(c * c, 0.08)),
170
  W1: add(`b${l}.W1`, mk(c * hidden, 0.08)), W2: add(`b${l}.W2`, mk(hidden * c, 0.08)),
171
  });
172
- m.Wu = add("Wu", mk(c * vocabSize(), 0.08));
 
 
173
  m.nParams = params.reduce((a, p) => a + p.length, 0);
174
  return m;
175
  }
@@ -273,7 +275,7 @@
273
  cache.blocks.push(cb);
274
  }
275
  const lf = lnFwd(x, BT, C); cache.lnf = lf; cache.xf = x;
276
- const logits = await vmm(lf.y, m.Wu, BT, C, vocab, ctx);
277
  // cross-entropy + dlogits
278
  let loss = 0;
279
  const dlogits = new Float32Array(BT * vocab);
@@ -298,9 +300,10 @@
298
  const BT = B * T, hd = C / heads, mm = TC.matmul, tr = TC.transpose;
299
  const g = m.params.map(p => new Float32Array(p.length));
300
  const gi = Object.fromEntries(m.names.map((n, i) => [n, i]));
301
- // unembed
302
- g[gi.Wu] = mm(tr(cache.lnf.y, BT, C), cache.dlogits, C, BT, vocab);
303
- let dx = lnBwd(mm(cache.dlogits, tr(m.Wu, C, vocab), BT, vocab, C), cache.lnf.y, cache.lnf.sig, BT, C);
 
304
  const scale = 1 / Math.sqrt(hd);
305
  for (let l = layers - 1; l >= 0; l--) {
306
  const bl = m.blocks[l], cb = cache.blocks[l];
 
169
  Wv: add(`b${l}.Wv`, mk(c * c, 0.08)), Wo: add(`b${l}.Wo`, mk(c * c, 0.08)),
170
  W1: add(`b${l}.W1`, mk(c * hidden, 0.08)), W2: add(`b${l}.W2`, mk(hidden * c, 0.08)),
171
  });
172
+ // weight-tied unembedding: logits use embᵀ (no separate Wu). Halves the
173
+ // vocab-sized parameters — and with a 16k vocab that's ~half of ALL
174
+ // parameters, so gradients over the wire shrink ~2× too.
175
  m.nParams = params.reduce((a, p) => a + p.length, 0);
176
  return m;
177
  }
 
275
  cache.blocks.push(cb);
276
  }
277
  const lf = lnFwd(x, BT, C); cache.lnf = lf; cache.xf = x;
278
+ const logits = await vmm(lf.y, TC.transpose(m.emb, vocab, C), BT, C, vocab, ctx); // tied: embᵀ
279
  // cross-entropy + dlogits
280
  let loss = 0;
281
  const dlogits = new Float32Array(BT * vocab);
 
300
  const BT = B * T, hd = C / heads, mm = TC.matmul, tr = TC.transpose;
301
  const g = m.params.map(p => new Float32Array(p.length));
302
  const gi = Object.fromEntries(m.names.map((n, i) => [n, i]));
303
+ // tied unembed: logits = lnf @ embᵀ, so the unembedding gradient flows
304
+ // straight into emb dlogits @ lnf is V×C, emb's own shape
305
+ g[gi.emb] = mm(tr(cache.dlogits, BT, vocab), cache.lnf.y, vocab, BT, C);
306
+ let dx = lnBwd(mm(cache.dlogits, m.emb, BT, vocab, C), cache.lnf.y, cache.lnf.sig, BT, C);
307
  const scale = 1 / Math.sqrt(hd);
308
  for (let l = layers - 1; l >= 0; l--) {
309
  const bl = m.blocks[l], cb = cache.blocks[l];
web/server.js CHANGED
@@ -30,13 +30,28 @@ const PUB = path.join(__dirname, "public");
30
  const TYPES = { ".html": "text/html", ".js": "text/javascript",
31
  ".css": "text/css", ".json": "application/json" };
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));
@@ -89,6 +104,14 @@ wss.on("connection", (ws, req) => {
89
  if (msg.type === "signal" && msg.to && room.peers.has(id)) { // relay WebRTC signaling
90
  const target = room.peers.get(msg.to);
91
  if (target) send(target.ws, { type: "signal", from: id, data: msg.data });
 
 
 
 
 
 
 
 
92
  } else if (msg.type === "admit" && id === room.host) { // host verdict on a joiner
93
  const p = room.pending.get(msg.id);
94
  if (!p) return;
 
30
  const TYPES = { ".html": "text/html", ".js": "text/javascript",
31
  ".css": "text/css", ".json": "application/json" };
32
 
33
+ // keepalive: proxies (incl. HuggingFace's) close idle WebSockets — ping every
34
+ // 30s keeps the pipe warm and detects dead clients within a minute
35
+ setInterval(() => {
36
+ for (const room of rooms.values())
37
+ for (const [pid, v] of room.peers) {
38
+ if (v.ws.isAlive === false) { v.ws.terminate(); continue; }
39
+ v.ws.isAlive = false;
40
+ send(v.ws, { type: "ping" });
41
+ }
42
+ }, 30000);
43
+
44
  const server = http.createServer((req, res) => {
45
  let p = decodeURIComponent(req.url.split("?")[0]);
46
  if (p === "/mode") { // deployment flags for the client
47
  res.writeHead(200, { "Content-Type": "application/json" });
48
  // DAISY_FORCE_ROOMS=1 (set on the HF Space): no LAN auto-grouping — every
49
+ // visitor creates their own private room and invites devices by link.
50
+ // DAISY_RTC_CONFIG: optional JSON RTCPeerConnection config (own TURN etc.,
51
+ // PairDrop-style server-provided rtcConfig).
52
+ let rtc = null;
53
+ try { if (process.env.DAISY_RTC_CONFIG) rtc = JSON.parse(process.env.DAISY_RTC_CONFIG); } catch (e) {}
54
+ return res.end(JSON.stringify({ forceRooms: !!process.env.DAISY_FORCE_ROOMS, rtc }));
55
  }
56
  if (p === "/") p = "/index.html";
57
  const file = path.join(PUB, path.normalize(p));
 
104
  if (msg.type === "signal" && msg.to && room.peers.has(id)) { // relay WebRTC signaling
105
  const target = room.peers.get(msg.to);
106
  if (target) send(target.ws, { type: "signal", from: id, data: msg.data });
107
+ } else if (msg.type === "pong") { // keepalive reply
108
+ ws.isAlive = true;
109
+ } else if (msg.type === "relay" && msg.to && room.peers.has(id)) {
110
+ // PairDrop-style WS fallback: when two devices can't form a direct
111
+ // WebRTC path (symmetric NATs, no TURN), the training payloads flow
112
+ // through here instead of dropping the peer
113
+ const target = room.peers.get(msg.to);
114
+ if (target) send(target.ws, { type: "relay", from: id, data: msg.data });
115
  } else if (msg.type === "admit" && id === room.host) { // host verdict on a joiner
116
  const p = room.pending.get(msg.id);
117
  if (!p) return;