aminediroHF HF Staff commited on
Commit
daa5370
·
verified ·
1 Parent(s): 51b06ea

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. README.md +6 -5
  2. index.html +659 -18
README.md CHANGED
@@ -1,10 +1,11 @@
1
  ---
2
- title: Delta Weight Sync Diff
3
- emoji: 💻
4
- colorFrom: green
5
- colorTo: purple
6
  sdk: static
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Delta Weight Sync · Diff Transfer
3
+ emoji: 🧬
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: static
7
  pinned: false
8
+ short_description: End-to-end animation of one weight sync, delta vs NCCL
9
  ---
10
 
11
+ Companion figure for the [Delta Weight Sync blogpost](https://huggingface.co/blog/aminediroHF/delta-weight-sync). Source PR: [huggingface/trl#5417](https://github.com/huggingface/trl/pull/5417).
index.html CHANGED
@@ -1,19 +1,660 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Delta weight transfer</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
11
+ background: #fff;
12
+ color: #1a1a2e;
13
+ display: flex;
14
+ justify-content: center;
15
+ padding: 24px 16px;
16
+ }
17
+ .container { width: 100%; max-width: 820px; }
18
+
19
+ h2 { font-size: 16px; font-weight: 700; color: #111; letter-spacing: -0.2px; margin-bottom: 2px; }
20
+ p.sub { font-size: 12px; color: #6b7280; margin-bottom: 18px; line-height: 1.45; }
21
+
22
+ .controls {
23
+ display: flex;
24
+ gap: 12px;
25
+ align-items: center;
26
+ padding: 12px 14px;
27
+ background: #f8fafc;
28
+ border: 1px solid #e2e8f0;
29
+ border-radius: 8px;
30
+ margin-bottom: 16px;
31
+ flex-wrap: wrap;
32
+ }
33
+ .controls .group { display: flex; align-items: center; gap: 8px; }
34
+ .controls label { font-size: 11.5px; font-weight: 600; color: #334155; }
35
+
36
+ .toggle {
37
+ display: inline-flex;
38
+ background: #fff;
39
+ border: 1px solid #cbd5e1;
40
+ border-radius: 6px;
41
+ overflow: hidden;
42
+ }
43
+ .toggle button {
44
+ font-size: 11px;
45
+ padding: 5px 11px;
46
+ background: transparent;
47
+ border: none;
48
+ cursor: pointer;
49
+ font-weight: 600;
50
+ color: #475569;
51
+ font-family: inherit;
52
+ }
53
+ .toggle button.active { background: #1a1a2e; color: #fff; }
54
+
55
+ .play {
56
+ margin-left: auto;
57
+ font-size: 11.5px;
58
+ padding: 6px 14px;
59
+ background: #4a90d9;
60
+ color: #fff;
61
+ border: none;
62
+ border-radius: 6px;
63
+ cursor: pointer;
64
+ font-weight: 700;
65
+ font-family: inherit;
66
+ }
67
+ .play:hover { background: #3b7ec0; }
68
+ .play.playing { background: #dc2626; }
69
+
70
+ .stage {
71
+ background: #fafafa;
72
+ border: 1px solid #e2e8f0;
73
+ border-radius: 8px;
74
+ padding: 18px;
75
+ margin-bottom: 14px;
76
+ }
77
+ svg.stage-svg { display: block; width: 100%; height: 420px; overflow: visible; }
78
+
79
+ .stats {
80
+ display: grid;
81
+ grid-template-columns: 1fr 1fr 1fr;
82
+ gap: 10px;
83
+ margin-top: 12px;
84
+ }
85
+ .stat {
86
+ background: #fafafa;
87
+ border: 1px solid #e2e8f0;
88
+ border-radius: 6px;
89
+ padding: 10px 12px;
90
+ }
91
+ .stat .lbl { font-size: 10px; color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; }
92
+ .stat .val { font-family: 'JetBrains Mono', monospace; font-size: 15px; font-weight: 700; color: #0f172a; margin-top: 3px; }
93
+ .stat.delta .val { color: #16a34a; }
94
+ .stat.pause .val { color: #dc2626; }
95
+ </style>
96
+ </head>
97
+ <body>
98
+ <div class="container">
99
+
100
+ <h2>One sync, end to end (frontier-scale model)</h2>
101
+ <p class="sub">
102
+ Llama-3.1-405B in bf16 is 810 GB on disk. At RL learning rates,
103
+ <a href="https://arxiv.org/html/2602.03839v2" target="_blank" style="color: #4a90d9;">PULSE (Mihai &amp; Belilovsky)</a>
104
+ reports ~99% per-step bf16 sparsity, so the actual delta is on the order of ~6 GB. Watch the two modes side by side.
105
+ </p>
106
+
107
+ <div class="controls">
108
+ <div class="group">
109
+ <label>mode</label>
110
+ <div class="toggle" id="modeToggle">
111
+ <button class="active" data-mode="delta">delta + bucket</button>
112
+ <button data-mode="nccl">NCCL broadcast</button>
113
+ </div>
114
+ </div>
115
+ <button class="play" id="playBtn">▶ play</button>
116
+ </div>
117
+
118
+ <div class="stage">
119
+ <svg class="stage-svg" id="stage" viewBox="0 0 820 420" preserveAspectRatio="xMidYMid meet"></svg>
120
+ </div>
121
+
122
+ <div class="stats">
123
+ <div class="stat">
124
+ <div class="lbl">payload sent</div>
125
+ <div class="val" id="statPayload">— MB</div>
126
+ </div>
127
+ <div class="stat pause">
128
+ <div class="lbl">inference paused</div>
129
+ <div class="val" id="statPause">— s</div>
130
+ </div>
131
+ <div class="stat delta">
132
+ <div class="lbl">reduction vs full</div>
133
+ <div class="val" id="statReduction">—</div>
134
+ </div>
135
+ </div>
136
+
137
+ </div>
138
+
139
+ <script>
140
+ const svg = document.getElementById('stage');
141
+ const NS = 'http://www.w3.org/2000/svg';
142
+ let mode = 'delta';
143
+ let playing = false;
144
+ let rafId = null;
145
+ let startMs = null;
146
+
147
+ function el(tag, attrs = {}, parent = svg) {
148
+ const e = document.createElementNS(NS, tag);
149
+ for (const k in attrs) e.setAttribute(k, attrs[k]);
150
+ parent.appendChild(e);
151
+ return e;
152
+ }
153
+
154
+ // ===== Layout =====
155
+ const TRAINER = { x: 40, y: 70, gridX: 60, gridY: 140, cell: 8, N: 16 };
156
+ const REPLICA = { x: 640, y: 70, gridX: 660, gridY: 140, cell: 8, N: 16 };
157
+ const BUCKET = { x: 355, y: 110, w: 110, h: 200 };
158
+ const STRIP = { cell: 9, gap: 1, yTrainer: 290, yBucket: 220, yReplica: 290 };
159
+
160
+ // ===== Cell model =====
161
+ // 16x16 = 256 cells. PULSE paper (arXiv:2602.03839) reports ~99% mean sparsity per step at RL
162
+ // learning rates, so well under 1% of bf16 elements actually flip. We show ~4 cells = 1.5% so
163
+ // the dots are visible without overstating it.
164
+ let cells = [];
165
+ let changed = [];
166
+ const CHANGED_FRAC = 4 / 256;
167
+
168
+ function initCells() {
169
+ cells = [];
170
+ changed = [];
171
+ // deterministic-ish random for repeatability across replays
172
+ const N = TRAINER.N;
173
+ const flips = new Set();
174
+ while (flips.size < Math.round(N * N * CHANGED_FRAC)) {
175
+ flips.add(Math.floor(Math.random() * N * N));
176
+ }
177
+ let packIdx = 0;
178
+ for (let r = 0; r < N; r++) {
179
+ for (let c = 0; c < N; c++) {
180
+ const idx = r * N + c;
181
+ const isChanged = flips.has(idx);
182
+ const cell = { idx, row: r, col: c, changed: isChanged, packedIdx: -1 };
183
+ if (isChanged) {
184
+ cell.packedIdx = packIdx++;
185
+ changed.push(cell);
186
+ }
187
+ cells.push(cell);
188
+ }
189
+ }
190
+ }
191
+
192
+ // ===== Position helpers =====
193
+ function gridPos(side, row, col) {
194
+ const g = (side === 'trainer') ? TRAINER : REPLICA;
195
+ return { x: g.gridX + col * g.cell, y: g.gridY + row * g.cell };
196
+ }
197
+
198
+ function stripPos(loc, packedIdx) {
199
+ const stripWidth = changed.length * (STRIP.cell + STRIP.gap);
200
+ let cx;
201
+ if (loc === 'trainer') cx = TRAINER.gridX + TRAINER.N * TRAINER.cell / 2;
202
+ else if (loc === 'bucket') cx = BUCKET.x + BUCKET.w / 2;
203
+ else cx = REPLICA.gridX + REPLICA.N * REPLICA.cell / 2;
204
+ const x0 = cx - stripWidth / 2;
205
+ let y;
206
+ if (loc === 'trainer') y = STRIP.yTrainer;
207
+ else if (loc === 'bucket') y = STRIP.yBucket;
208
+ else y = STRIP.yReplica;
209
+ return { x: x0 + packedIdx * (STRIP.cell + STRIP.gap), y };
210
+ }
211
+
212
+ function lerp(a, b, t) { return a + (b - a) * t; }
213
+ function ease(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t + 2, 2) / 2; }
214
+ function lerpPt(p, q, t) { const e = ease(t); return { x: lerp(p.x, q.x, e), y: lerp(p.y, q.y, e) }; }
215
+
216
+ // ===== Timeline =====
217
+ // Each phase has a name, duration, and (implicit) animations.
218
+ const DELTA_PHASES = [
219
+ { name: 'idle', dur: 400, title: 'idle' },
220
+ { name: 'step', dur: 700, title: '1. optimizer step · ~5% of bf16 elements flip' },
221
+ { name: 'extract', dur: 1100, title: '2. pack changed elements into a sparse safetensors patch' },
222
+ { name: 'upload', dur: 900, title: '3. upload patch to HF bucket (inference still serving)' },
223
+ { name: 'settle', dur: 500, title: '4. patch sits in the bucket' },
224
+ { name: 'pause', dur: 500, title: '5. pause inference, signal /update_weights' },
225
+ { name: 'download', dur: 900, title: '6. replica pulls the patch from the bucket' },
226
+ { name: 'unpack', dur: 1100, title: '7. unpack: indices route values back to their positions' },
227
+ { name: 'resume', dur: 500, title: '8. resume inference' },
228
+ ];
229
+
230
+ const NCCL_PHASES = [
231
+ { name: 'idle', dur: 400, title: 'idle' },
232
+ { name: 'step', dur: 700, title: '1. optimizer step' },
233
+ { name: 'pause', dur: 400, title: '2. pause inference (every replica blocks)' },
234
+ { name: 'clone', dur: 600, title: '3. serialize the entire model' },
235
+ { name: 'broadcast',dur: 1800, title: '4. NCCL broadcast: 810 GB across the wire' },
236
+ { name: 'apply', dur: 700, title: '5. apply all weights on the replica' },
237
+ { name: 'resume', dur: 500, title: '6. resume inference' },
238
+ ];
239
+
240
+ function phasesFor(mode) { return mode === 'delta' ? DELTA_PHASES : NCCL_PHASES; }
241
+
242
+ function phaseAtTime(phases, t) {
243
+ let acc = 0;
244
+ for (let i = 0; i < phases.length; i++) {
245
+ if (t < acc + phases[i].dur) {
246
+ return { phase: phases[i], local: (t - acc) / phases[i].dur, idx: i };
247
+ }
248
+ acc += phases[i].dur;
249
+ }
250
+ return { phase: phases[phases.length - 1], local: 1, idx: phases.length - 1 };
251
+ }
252
+
253
+ function totalDuration(phases) { return phases.reduce((s, p) => s + p.dur, 0); }
254
+
255
+ // ===== Rendering =====
256
+ function drawDefs() {
257
+ const defs = el('defs');
258
+ defs.innerHTML = `
259
+ <marker id="ah-up" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto">
260
+ <path d="M0,0 L8,4 L0,8 Z" fill="#16a34a"/>
261
+ </marker>
262
+ <marker id="ah-down" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto">
263
+ <path d="M0,0 L8,4 L0,8 Z" fill="#4a90d9"/>
264
+ </marker>
265
+ <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
266
+ <feGaussianBlur stdDeviation="2" result="b"/>
267
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
268
+ </filter>
269
+ `;
270
+ }
271
+
272
+ function drawNodeBoxes(opts) {
273
+ const { trainerActive, replicaPaused } = opts;
274
+ // Trainer
275
+ el('rect', {
276
+ x: TRAINER.x, y: TRAINER.y, width: 200, height: 230, rx: 8,
277
+ fill: '#fff', stroke: trainerActive ? '#16a34a' : '#94a3b8', 'stroke-width': 1.5
278
+ });
279
+ el('text', { x: TRAINER.x + 12, y: TRAINER.y + 20, 'font-size': 12, 'font-weight': 700, fill: '#1a1a2e' })
280
+ .textContent = 'trainer';
281
+ el('text', { x: TRAINER.x + 12, y: TRAINER.y + 36, 'font-size': 9, fill: '#64748b', 'font-family': 'JetBrains Mono, monospace' })
282
+ .textContent = 'Llama-3.1-405B · bf16';
283
+ el('text', { x: TRAINER.x + 12, y: TRAINER.y + 50, 'font-size': 9, fill: '#64748b', 'font-family': 'JetBrains Mono, monospace' })
284
+ .textContent = '405B params · 810 GB';
285
+
286
+ // Replica
287
+ el('rect', {
288
+ x: REPLICA.x, y: REPLICA.y, width: 160, height: 230, rx: 8,
289
+ fill: replicaPaused ? '#fef2f2' : '#fff',
290
+ stroke: replicaPaused ? '#dc2626' : '#94a3b8',
291
+ 'stroke-width': 1.5
292
+ });
293
+ el('text', { x: REPLICA.x + 12, y: REPLICA.y + 20, 'font-size': 12, 'font-weight': 700, fill: '#1a1a2e' })
294
+ .textContent = 'vLLM rollout';
295
+ el('text', { x: REPLICA.x + 12, y: REPLICA.y + 36, 'font-size': 9, fill: '#64748b', 'font-family': 'JetBrains Mono, monospace' })
296
+ .textContent = replicaPaused ? 'paused' : 'serving';
297
+ }
298
+
299
+ function drawBucket(opts = {}) {
300
+ const { bx, by, bw, bh } = { bx: BUCKET.x, by: BUCKET.y, bw: BUCKET.w, bh: BUCKET.h };
301
+ // bottom ellipse first
302
+ el('ellipse', { cx: bx + bw/2, cy: by + bh, rx: bw/2, ry: 12, fill: '#dbeafe', stroke: '#4a90d9', 'stroke-width': 1.5 });
303
+ el('rect', { x: bx, y: by, width: bw, height: bh, fill: '#eff6ff', stroke: '#4a90d9', 'stroke-width': 1.5 });
304
+ el('ellipse', { cx: bx + bw/2, cy: by + bh, rx: bw/2, ry: 12, fill: '#dbeafe', stroke: '#4a90d9', 'stroke-width': 1.5, opacity: 0.4 });
305
+ el('ellipse', { cx: bx + bw/2, cy: by, rx: bw/2, ry: 12, fill: '#fff', stroke: '#4a90d9', 'stroke-width': 1.5 });
306
+ el('text', { x: bx + bw/2, y: by + 38, 'text-anchor': 'middle', 'font-size': 11, 'font-weight': 700, fill: '#1e40af' })
307
+ .textContent = 'HF bucket';
308
+ el('text', { x: bx + bw/2, y: by + 54, 'text-anchor': 'middle', 'font-size': 9, fill: '#1e40af', 'font-family': 'JetBrains Mono, monospace' })
309
+ .textContent = 'Xet';
310
+ }
311
+
312
+ function drawGridFill(side, predicate, color, opacity = 1) {
313
+ const g = (side === 'trainer') ? TRAINER : REPLICA;
314
+ for (const c of cells) {
315
+ if (!predicate(c)) continue;
316
+ el('rect', {
317
+ x: g.gridX + c.col * g.cell + 0.5,
318
+ y: g.gridY + c.row * g.cell + 0.5,
319
+ width: g.cell - 1,
320
+ height: g.cell - 1,
321
+ fill: color,
322
+ opacity
323
+ });
324
+ }
325
+ }
326
+
327
+ function drawGridFrame(side) {
328
+ const g = (side === 'trainer') ? TRAINER : REPLICA;
329
+ const W = g.cell * g.N;
330
+ el('rect', {
331
+ x: g.gridX - 1, y: g.gridY - 1, width: W + 2, height: W + 2,
332
+ fill: 'none', stroke: '#cbd5e1', 'stroke-width': 1, rx: 2
333
+ });
334
+ }
335
+
336
+ function drawFlyingCells(localT, fromLoc, toLoc, applied=false) {
337
+ // Each changed cell at lerp(from, to, localT)
338
+ for (const c of changed) {
339
+ let p0, p1;
340
+ if (fromLoc.kind === 'grid') p0 = gridPos(fromLoc.side, c.row, c.col);
341
+ else p0 = stripPos(fromLoc.loc, c.packedIdx);
342
+ if (toLoc.kind === 'grid') p1 = gridPos(toLoc.side, c.row, c.col);
343
+ else p1 = stripPos(toLoc.loc, c.packedIdx);
344
+ const p = lerpPt(p0, p1, localT);
345
+ const sz = lerp(
346
+ fromLoc.kind === 'grid' ? TRAINER.cell - 1 : STRIP.cell,
347
+ toLoc.kind === 'grid' ? REPLICA.cell - 1 : STRIP.cell,
348
+ localT
349
+ );
350
+ el('rect', {
351
+ x: p.x + 0.5, y: p.y + 0.5,
352
+ width: sz, height: sz,
353
+ fill: applied ? '#16a34a' : '#dc2626',
354
+ filter: 'url(#glow)'
355
+ });
356
+ }
357
+ }
358
+
359
+ function drawStrip(loc, label, sizeLabel) {
360
+ if (!changed.length) return;
361
+ const stripWidth = changed.length * (STRIP.cell + STRIP.gap);
362
+ const first = stripPos(loc, 0);
363
+ const last = stripPos(loc, changed.length - 1);
364
+ // Frame
365
+ el('rect', {
366
+ x: first.x - 3, y: first.y - 3,
367
+ width: (last.x - first.x) + STRIP.cell + 6, height: STRIP.cell + 6,
368
+ fill: '#fff', stroke: '#16a34a', 'stroke-width': 1.5, rx: 2
369
+ });
370
+ // Cells
371
+ for (const c of changed) {
372
+ const p = stripPos(loc, c.packedIdx);
373
+ el('rect', {
374
+ x: p.x + 0.5, y: p.y + 0.5,
375
+ width: STRIP.cell - 1, height: STRIP.cell - 1,
376
+ fill: '#dc2626'
377
+ });
378
+ }
379
+ // Label below
380
+ const cx = first.x + (last.x - first.x + STRIP.cell) / 2;
381
+ if (label) {
382
+ el('text', {
383
+ x: cx, y: first.y + STRIP.cell + 16,
384
+ 'text-anchor': 'middle', 'font-size': 9, 'font-weight': 700, fill: '#15803d',
385
+ 'font-family': 'JetBrains Mono, monospace'
386
+ }).textContent = label;
387
+ }
388
+ if (sizeLabel) {
389
+ el('text', {
390
+ x: cx, y: first.y - 6,
391
+ 'text-anchor': 'middle', 'font-size': 9.5, 'font-weight': 700, fill: '#15803d',
392
+ 'font-family': 'JetBrains Mono, monospace'
393
+ }).textContent = sizeLabel;
394
+ }
395
+ }
396
+
397
+ function drawFlyingStrip(t, locA, locB, sizeLabel) {
398
+ // Group flying with cells inside
399
+ const stripWidth = changed.length * (STRIP.cell + STRIP.gap);
400
+ const a = stripPos(locA, 0);
401
+ const b = stripPos(locB, 0);
402
+ const p = lerpPt(a, b, t);
403
+ // Frame
404
+ el('rect', {
405
+ x: p.x - 3, y: p.y - 3,
406
+ width: stripWidth + 6, height: STRIP.cell + 6,
407
+ fill: '#fff', stroke: '#16a34a', 'stroke-width': 1.5, rx: 2, filter: 'url(#glow)'
408
+ });
409
+ // Cells
410
+ for (let i = 0; i < changed.length; i++) {
411
+ el('rect', {
412
+ x: p.x + i * (STRIP.cell + STRIP.gap) + 0.5, y: p.y + 0.5,
413
+ width: STRIP.cell - 1, height: STRIP.cell - 1,
414
+ fill: '#dc2626'
415
+ });
416
+ }
417
+ // Label
418
+ if (sizeLabel) {
419
+ el('text', {
420
+ x: p.x + stripWidth / 2, y: p.y - 6,
421
+ 'text-anchor': 'middle', 'font-size': 9.5, 'font-weight': 700, fill: '#15803d',
422
+ 'font-family': 'JetBrains Mono, monospace'
423
+ }).textContent = sizeLabel;
424
+ }
425
+ }
426
+
427
+ function drawMiniGrid(x, y, scale, label, opacity = 1) {
428
+ const N = TRAINER.N;
429
+ const cell = TRAINER.cell * scale;
430
+ const W = N * cell;
431
+ const g = el('g', { opacity });
432
+ // Frame
433
+ el('rect', { x: x - 2, y: y - 2, width: W + 4, height: W + 4, fill: '#fff', stroke: '#dc2626', 'stroke-width': 1.5, rx: 2, filter: 'url(#glow)' }, g);
434
+ for (const c of cells) {
435
+ const fill = c.changed ? '#dc2626' : '#cbd5e1';
436
+ el('rect', {
437
+ x: x + c.col * cell, y: y + c.row * cell,
438
+ width: cell - (cell > 3 ? 0.5 : 0), height: cell - (cell > 3 ? 0.5 : 0),
439
+ fill
440
+ }, g);
441
+ }
442
+ if (label) {
443
+ el('text', { x: x + W/2, y: y - 6, 'text-anchor': 'middle', 'font-size': 10, 'font-weight': 700, fill: '#b91c1c', 'font-family': 'JetBrains Mono, monospace' }, g)
444
+ .textContent = label;
445
+ }
446
+ }
447
+
448
+ // ===== Phase renderer =====
449
+ function renderDelta(phaseInfo) {
450
+ const { phase, local } = phaseInfo;
451
+ const name = phase.name;
452
+
453
+ // Replica grid: cells appear green only in unpack/resume
454
+ const replicaApplied = (c) => {
455
+ if (name === 'unpack') return local > 0.85 && c.changed;
456
+ if (name === 'resume') return c.changed;
457
+ return false;
458
+ };
459
+ const replicaPaused = (name === 'pause' || name === 'download' || name === 'unpack');
460
+
461
+ // ---- bucket (always visible in delta mode) ----
462
+ drawBucket();
463
+
464
+ // ---- node boxes ----
465
+ drawNodeBoxes({ trainerActive: name === 'step', replicaPaused });
466
+
467
+ // ---- trainer grid frame + base cells ----
468
+ drawGridFrame('trainer');
469
+ // Always draw all gray cells (unchanged)
470
+ drawGridFill('trainer', c => !c.changed, '#cbd5e1');
471
+ // Draw changed cells at trainer grid position ONLY if they haven't left yet
472
+ const changedAtTrainer = (name === 'idle' || name === 'step');
473
+ if (changedAtTrainer) {
474
+ const op = name === 'step' ? Math.min(1, local * 1.4) : 0.0;
475
+ drawGridFill('trainer', c => c.changed, '#dc2626', op);
476
+ }
477
+
478
+ // ---- replica grid ----
479
+ drawGridFrame('replica');
480
+ drawGridFill('replica', c => !c.changed, '#cbd5e1');
481
+ drawGridFill('replica', c => replicaApplied(c), '#16a34a');
482
+ // During unpack, draw the unchanged-replica cells where changed cells "will" land
483
+ if (name === 'unpack' && local <= 0.85) {
484
+ drawGridFill('replica', c => c.changed, '#cbd5e1');
485
+ } else if (name !== 'resume' && name !== 'unpack') {
486
+ drawGridFill('replica', c => c.changed, '#cbd5e1');
487
+ }
488
+
489
+ // ---- in-flight stuff ----
490
+ if (name === 'extract') {
491
+ // Cells fly from trainer grid (row,col) to trainer-side strip (packedIdx)
492
+ drawFlyingCells(local, { kind: 'grid', side: 'trainer' }, { kind: 'strip', loc: 'trainer' }, false);
493
+ } else if (name === 'upload') {
494
+ // Strip flies from trainer to bucket
495
+ drawFlyingStrip(local, 'trainer', 'bucket', '~6 GB');
496
+ } else if (name === 'settle') {
497
+ // Strip in bucket
498
+ drawStrip('bucket', 'delta.safetensors', '~6 GB');
499
+ } else if (name === 'pause') {
500
+ // Strip in bucket, replica becomes red
501
+ drawStrip('bucket', 'delta.safetensors', '~6 GB');
502
+ } else if (name === 'download') {
503
+ // Strip flies from bucket to replica
504
+ drawStrip('bucket', '', ''); // ghost in bucket
505
+ drawFlyingStrip(local, 'bucket', 'replica', '~6 GB');
506
+ } else if (name === 'unpack') {
507
+ if (local < 0.15) {
508
+ drawStrip('replica', 'delta.safetensors', '~6 GB');
509
+ } else {
510
+ // Cells fly from replica-side strip back to (row, col) in replica grid
511
+ const cellT = Math.min(1, (local - 0.15) / 0.85);
512
+ drawFlyingCells(cellT, { kind: 'strip', loc: 'replica' }, { kind: 'grid', side: 'replica' }, cellT > 0.9);
513
+ }
514
+ } else if (name === 'resume') {
515
+ // Apply complete
516
+ }
517
+
518
+ // ---- title ----
519
+ el('text', { x: 410, y: 30, 'text-anchor': 'middle', 'font-size': 12.5, 'font-weight': 700, fill: '#1a1a2e' })
520
+ .textContent = phase.title;
521
+ }
522
+
523
+ function renderNccl(phaseInfo) {
524
+ const { phase, local } = phaseInfo;
525
+ const name = phase.name;
526
+ const replicaPaused = (name !== 'idle' && name !== 'step' && name !== 'resume');
527
+
528
+ drawNodeBoxes({ trainerActive: name === 'step', replicaPaused });
529
+
530
+ // No bucket in NCCL: draw a "direct broadcast" hint
531
+ el('rect', { x: BUCKET.x - 10, y: BUCKET.y + 70, width: BUCKET.w + 20, height: 60, fill: 'transparent', stroke: 'none' });
532
+ if (name === 'idle' || name === 'step' || name === 'pause' || name === 'resume') {
533
+ el('text', { x: BUCKET.x + BUCKET.w/2, y: BUCKET.y + 110, 'text-anchor': 'middle', 'font-size': 10, fill: '#94a3b8', 'font-style': 'italic' })
534
+ .textContent = 'direct NCCL';
535
+ el('text', { x: BUCKET.x + BUCKET.w/2, y: BUCKET.y + 125, 'text-anchor': 'middle', 'font-size': 10, fill: '#94a3b8', 'font-style': 'italic' })
536
+ .textContent = 'no shared storage';
537
+ }
538
+
539
+ // Trainer grid
540
+ drawGridFrame('trainer');
541
+ drawGridFill('trainer', c => !c.changed, '#cbd5e1');
542
+ if (name === 'step') {
543
+ drawGridFill('trainer', c => c.changed, '#dc2626', Math.min(1, local * 1.4));
544
+ } else if (name !== 'idle') {
545
+ drawGridFill('trainer', c => c.changed, '#dc2626');
546
+ }
547
+
548
+ // Replica grid
549
+ drawGridFrame('replica');
550
+ drawGridFill('replica', c => !c.changed, '#cbd5e1');
551
+ if (name === 'apply' && local > 0.7) drawGridFill('replica', c => c.changed, '#16a34a');
552
+ else if (name === 'resume') drawGridFill('replica', c => c.changed, '#16a34a');
553
+ else drawGridFill('replica', c => c.changed, '#cbd5e1');
554
+
555
+ // In-flight: clone -> broadcast
556
+ // The whole grid duplicates and slides across
557
+ if (name === 'clone') {
558
+ const scale = 0.5;
559
+ const fx = TRAINER.x + 210, fy = TRAINER.y + 80;
560
+ drawMiniGrid(fx, fy, scale, '810 GB', local);
561
+ } else if (name === 'broadcast') {
562
+ const scale = 0.5;
563
+ const W = TRAINER.cell * TRAINER.N * scale;
564
+ const startX = TRAINER.x + 210;
565
+ const endX = REPLICA.x - 20 - W;
566
+ const y = TRAINER.y + 80;
567
+ const x = lerp(startX, endX, ease(local));
568
+ // Fat pipe behind
569
+ el('line', {
570
+ x1: TRAINER.x + 200, y1: TRAINER.y + 100,
571
+ x2: REPLICA.x, y2: REPLICA.y + 100,
572
+ stroke: '#dc2626', 'stroke-width': 6, opacity: 0.18, 'stroke-linecap': 'round'
573
+ });
574
+ drawMiniGrid(x, y, scale, '810 GB', 1);
575
+ } else if (name === 'apply') {
576
+ const scale = 0.5;
577
+ const fx = REPLICA.x - 20 - TRAINER.cell * TRAINER.N * scale;
578
+ const fy = TRAINER.y + 80;
579
+ const fade = Math.max(0, 1 - local * 1.5);
580
+ drawMiniGrid(fx, fy, scale, '810 GB', fade);
581
+ }
582
+
583
+ el('text', { x: 410, y: 30, 'text-anchor': 'middle', 'font-size': 12.5, 'font-weight': 700, fill: '#1a1a2e' })
584
+ .textContent = phase.title;
585
+ }
586
+
587
+ function renderAt(t) {
588
+ svg.innerHTML = '';
589
+ drawDefs();
590
+ const phases = phasesFor(mode);
591
+ const info = phaseAtTime(phases, t);
592
+ if (mode === 'delta') renderDelta(info);
593
+ else renderNccl(info);
594
+ }
595
+
596
+ // ===== Stats =====
597
+ function updateStats() {
598
+ if (mode === 'delta') {
599
+ document.getElementById('statPayload').textContent = '~6 GB';
600
+ document.getElementById('statPause').textContent = '~2 s';
601
+ document.getElementById('statReduction').textContent = '135×';
602
+ } else {
603
+ document.getElementById('statPayload').textContent = '810 GB';
604
+ document.getElementById('statPause').textContent = '~10 s';
605
+ document.getElementById('statReduction').textContent = '1×';
606
+ }
607
+ }
608
+
609
+ // ===== Playback =====
610
+ function play() {
611
+ if (playing) return;
612
+ initCells();
613
+ playing = true;
614
+ document.getElementById('playBtn').textContent = '⏸ stop';
615
+ document.getElementById('playBtn').classList.add('playing');
616
+ const phases = phasesFor(mode);
617
+ const total = totalDuration(phases);
618
+ startMs = performance.now();
619
+ function tick(now) {
620
+ if (!playing) return;
621
+ const t = now - startMs;
622
+ if (t >= total) {
623
+ renderAt(total - 1);
624
+ playing = false;
625
+ document.getElementById('playBtn').textContent = '↻ play again';
626
+ document.getElementById('playBtn').classList.remove('playing');
627
+ return;
628
+ }
629
+ renderAt(t);
630
+ rafId = requestAnimationFrame(tick);
631
+ }
632
+ rafId = requestAnimationFrame(tick);
633
+ }
634
+
635
+ function reset() {
636
+ if (rafId) cancelAnimationFrame(rafId);
637
+ playing = false;
638
+ initCells();
639
+ renderAt(0);
640
+ document.getElementById('playBtn').textContent = '▶ play';
641
+ document.getElementById('playBtn').classList.remove('playing');
642
+ updateStats();
643
+ }
644
+
645
+ document.getElementById('playBtn').addEventListener('click', () => {
646
+ if (playing) { reset(); } else { reset(); play(); }
647
+ });
648
+ document.getElementById('modeToggle').querySelectorAll('button').forEach(b => {
649
+ b.addEventListener('click', () => {
650
+ document.getElementById('modeToggle').querySelectorAll('button').forEach(x => x.classList.remove('active'));
651
+ b.classList.add('active');
652
+ mode = b.dataset.mode;
653
+ reset();
654
+ });
655
+ });
656
+
657
+ reset();
658
+ </script>
659
+ </body>
660
  </html>