Upload folder using huggingface_hub
Browse files- README.md +6 -5
- index.html +659 -18
README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
---
|
| 2 |
-
title: Delta Weight Sync Diff
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
| 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 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 & 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>
|