aminediroHF's picture
aminediroHF HF Staff
Upload folder using huggingface_hub
daa5370 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Delta weight transfer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: #fff;
color: #1a1a2e;
display: flex;
justify-content: center;
padding: 24px 16px;
}
.container { width: 100%; max-width: 820px; }
h2 { font-size: 16px; font-weight: 700; color: #111; letter-spacing: -0.2px; margin-bottom: 2px; }
p.sub { font-size: 12px; color: #6b7280; margin-bottom: 18px; line-height: 1.45; }
.controls {
display: flex;
gap: 12px;
align-items: center;
padding: 12px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.controls .group { display: flex; align-items: center; gap: 8px; }
.controls label { font-size: 11.5px; font-weight: 600; color: #334155; }
.toggle {
display: inline-flex;
background: #fff;
border: 1px solid #cbd5e1;
border-radius: 6px;
overflow: hidden;
}
.toggle button {
font-size: 11px;
padding: 5px 11px;
background: transparent;
border: none;
cursor: pointer;
font-weight: 600;
color: #475569;
font-family: inherit;
}
.toggle button.active { background: #1a1a2e; color: #fff; }
.play {
margin-left: auto;
font-size: 11.5px;
padding: 6px 14px;
background: #4a90d9;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 700;
font-family: inherit;
}
.play:hover { background: #3b7ec0; }
.play.playing { background: #dc2626; }
.stage {
background: #fafafa;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 18px;
margin-bottom: 14px;
}
svg.stage-svg { display: block; width: 100%; height: 420px; overflow: visible; }
.stats {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-top: 12px;
}
.stat {
background: #fafafa;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 10px 12px;
}
.stat .lbl { font-size: 10px; color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; }
.stat .val { font-family: 'JetBrains Mono', monospace; font-size: 15px; font-weight: 700; color: #0f172a; margin-top: 3px; }
.stat.delta .val { color: #16a34a; }
.stat.pause .val { color: #dc2626; }
</style>
</head>
<body>
<div class="container">
<h2>One sync, end to end (frontier-scale model)</h2>
<p class="sub">
Llama-3.1-405B in bf16 is 810 GB on disk. At RL learning rates,
<a href="https://arxiv.org/html/2602.03839v2" target="_blank" style="color: #4a90d9;">PULSE (Mihai &amp; Belilovsky)</a>
reports ~99% per-step bf16 sparsity, so the actual delta is on the order of ~6 GB. Watch the two modes side by side.
</p>
<div class="controls">
<div class="group">
<label>mode</label>
<div class="toggle" id="modeToggle">
<button class="active" data-mode="delta">delta + bucket</button>
<button data-mode="nccl">NCCL broadcast</button>
</div>
</div>
<button class="play" id="playBtn">▶ play</button>
</div>
<div class="stage">
<svg class="stage-svg" id="stage" viewBox="0 0 820 420" preserveAspectRatio="xMidYMid meet"></svg>
</div>
<div class="stats">
<div class="stat">
<div class="lbl">payload sent</div>
<div class="val" id="statPayload">— MB</div>
</div>
<div class="stat pause">
<div class="lbl">inference paused</div>
<div class="val" id="statPause">— s</div>
</div>
<div class="stat delta">
<div class="lbl">reduction vs full</div>
<div class="val" id="statReduction"></div>
</div>
</div>
</div>
<script>
const svg = document.getElementById('stage');
const NS = 'http://www.w3.org/2000/svg';
let mode = 'delta';
let playing = false;
let rafId = null;
let startMs = null;
function el(tag, attrs = {}, parent = svg) {
const e = document.createElementNS(NS, tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
parent.appendChild(e);
return e;
}
// ===== Layout =====
const TRAINER = { x: 40, y: 70, gridX: 60, gridY: 140, cell: 8, N: 16 };
const REPLICA = { x: 640, y: 70, gridX: 660, gridY: 140, cell: 8, N: 16 };
const BUCKET = { x: 355, y: 110, w: 110, h: 200 };
const STRIP = { cell: 9, gap: 1, yTrainer: 290, yBucket: 220, yReplica: 290 };
// ===== Cell model =====
// 16x16 = 256 cells. PULSE paper (arXiv:2602.03839) reports ~99% mean sparsity per step at RL
// learning rates, so well under 1% of bf16 elements actually flip. We show ~4 cells = 1.5% so
// the dots are visible without overstating it.
let cells = [];
let changed = [];
const CHANGED_FRAC = 4 / 256;
function initCells() {
cells = [];
changed = [];
// deterministic-ish random for repeatability across replays
const N = TRAINER.N;
const flips = new Set();
while (flips.size < Math.round(N * N * CHANGED_FRAC)) {
flips.add(Math.floor(Math.random() * N * N));
}
let packIdx = 0;
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const idx = r * N + c;
const isChanged = flips.has(idx);
const cell = { idx, row: r, col: c, changed: isChanged, packedIdx: -1 };
if (isChanged) {
cell.packedIdx = packIdx++;
changed.push(cell);
}
cells.push(cell);
}
}
}
// ===== Position helpers =====
function gridPos(side, row, col) {
const g = (side === 'trainer') ? TRAINER : REPLICA;
return { x: g.gridX + col * g.cell, y: g.gridY + row * g.cell };
}
function stripPos(loc, packedIdx) {
const stripWidth = changed.length * (STRIP.cell + STRIP.gap);
let cx;
if (loc === 'trainer') cx = TRAINER.gridX + TRAINER.N * TRAINER.cell / 2;
else if (loc === 'bucket') cx = BUCKET.x + BUCKET.w / 2;
else cx = REPLICA.gridX + REPLICA.N * REPLICA.cell / 2;
const x0 = cx - stripWidth / 2;
let y;
if (loc === 'trainer') y = STRIP.yTrainer;
else if (loc === 'bucket') y = STRIP.yBucket;
else y = STRIP.yReplica;
return { x: x0 + packedIdx * (STRIP.cell + STRIP.gap), y };
}
function lerp(a, b, t) { return a + (b - a) * t; }
function ease(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t + 2, 2) / 2; }
function lerpPt(p, q, t) { const e = ease(t); return { x: lerp(p.x, q.x, e), y: lerp(p.y, q.y, e) }; }
// ===== Timeline =====
// Each phase has a name, duration, and (implicit) animations.
const DELTA_PHASES = [
{ name: 'idle', dur: 400, title: 'idle' },
{ name: 'step', dur: 700, title: '1. optimizer step · ~5% of bf16 elements flip' },
{ name: 'extract', dur: 1100, title: '2. pack changed elements into a sparse safetensors patch' },
{ name: 'upload', dur: 900, title: '3. upload patch to HF bucket (inference still serving)' },
{ name: 'settle', dur: 500, title: '4. patch sits in the bucket' },
{ name: 'pause', dur: 500, title: '5. pause inference, signal /update_weights' },
{ name: 'download', dur: 900, title: '6. replica pulls the patch from the bucket' },
{ name: 'unpack', dur: 1100, title: '7. unpack: indices route values back to their positions' },
{ name: 'resume', dur: 500, title: '8. resume inference' },
];
const NCCL_PHASES = [
{ name: 'idle', dur: 400, title: 'idle' },
{ name: 'step', dur: 700, title: '1. optimizer step' },
{ name: 'pause', dur: 400, title: '2. pause inference (every replica blocks)' },
{ name: 'clone', dur: 600, title: '3. serialize the entire model' },
{ name: 'broadcast',dur: 1800, title: '4. NCCL broadcast: 810 GB across the wire' },
{ name: 'apply', dur: 700, title: '5. apply all weights on the replica' },
{ name: 'resume', dur: 500, title: '6. resume inference' },
];
function phasesFor(mode) { return mode === 'delta' ? DELTA_PHASES : NCCL_PHASES; }
function phaseAtTime(phases, t) {
let acc = 0;
for (let i = 0; i < phases.length; i++) {
if (t < acc + phases[i].dur) {
return { phase: phases[i], local: (t - acc) / phases[i].dur, idx: i };
}
acc += phases[i].dur;
}
return { phase: phases[phases.length - 1], local: 1, idx: phases.length - 1 };
}
function totalDuration(phases) { return phases.reduce((s, p) => s + p.dur, 0); }
// ===== Rendering =====
function drawDefs() {
const defs = el('defs');
defs.innerHTML = `
<marker id="ah-up" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto">
<path d="M0,0 L8,4 L0,8 Z" fill="#16a34a"/>
</marker>
<marker id="ah-down" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto">
<path d="M0,0 L8,4 L0,8 Z" fill="#4a90d9"/>
</marker>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
`;
}
function drawNodeBoxes(opts) {
const { trainerActive, replicaPaused } = opts;
// Trainer
el('rect', {
x: TRAINER.x, y: TRAINER.y, width: 200, height: 230, rx: 8,
fill: '#fff', stroke: trainerActive ? '#16a34a' : '#94a3b8', 'stroke-width': 1.5
});
el('text', { x: TRAINER.x + 12, y: TRAINER.y + 20, 'font-size': 12, 'font-weight': 700, fill: '#1a1a2e' })
.textContent = 'trainer';
el('text', { x: TRAINER.x + 12, y: TRAINER.y + 36, 'font-size': 9, fill: '#64748b', 'font-family': 'JetBrains Mono, monospace' })
.textContent = 'Llama-3.1-405B · bf16';
el('text', { x: TRAINER.x + 12, y: TRAINER.y + 50, 'font-size': 9, fill: '#64748b', 'font-family': 'JetBrains Mono, monospace' })
.textContent = '405B params · 810 GB';
// Replica
el('rect', {
x: REPLICA.x, y: REPLICA.y, width: 160, height: 230, rx: 8,
fill: replicaPaused ? '#fef2f2' : '#fff',
stroke: replicaPaused ? '#dc2626' : '#94a3b8',
'stroke-width': 1.5
});
el('text', { x: REPLICA.x + 12, y: REPLICA.y + 20, 'font-size': 12, 'font-weight': 700, fill: '#1a1a2e' })
.textContent = 'vLLM rollout';
el('text', { x: REPLICA.x + 12, y: REPLICA.y + 36, 'font-size': 9, fill: '#64748b', 'font-family': 'JetBrains Mono, monospace' })
.textContent = replicaPaused ? 'paused' : 'serving';
}
function drawBucket(opts = {}) {
const { bx, by, bw, bh } = { bx: BUCKET.x, by: BUCKET.y, bw: BUCKET.w, bh: BUCKET.h };
// bottom ellipse first
el('ellipse', { cx: bx + bw/2, cy: by + bh, rx: bw/2, ry: 12, fill: '#dbeafe', stroke: '#4a90d9', 'stroke-width': 1.5 });
el('rect', { x: bx, y: by, width: bw, height: bh, fill: '#eff6ff', stroke: '#4a90d9', 'stroke-width': 1.5 });
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 });
el('ellipse', { cx: bx + bw/2, cy: by, rx: bw/2, ry: 12, fill: '#fff', stroke: '#4a90d9', 'stroke-width': 1.5 });
el('text', { x: bx + bw/2, y: by + 38, 'text-anchor': 'middle', 'font-size': 11, 'font-weight': 700, fill: '#1e40af' })
.textContent = 'HF bucket';
el('text', { x: bx + bw/2, y: by + 54, 'text-anchor': 'middle', 'font-size': 9, fill: '#1e40af', 'font-family': 'JetBrains Mono, monospace' })
.textContent = 'Xet';
}
function drawGridFill(side, predicate, color, opacity = 1) {
const g = (side === 'trainer') ? TRAINER : REPLICA;
for (const c of cells) {
if (!predicate(c)) continue;
el('rect', {
x: g.gridX + c.col * g.cell + 0.5,
y: g.gridY + c.row * g.cell + 0.5,
width: g.cell - 1,
height: g.cell - 1,
fill: color,
opacity
});
}
}
function drawGridFrame(side) {
const g = (side === 'trainer') ? TRAINER : REPLICA;
const W = g.cell * g.N;
el('rect', {
x: g.gridX - 1, y: g.gridY - 1, width: W + 2, height: W + 2,
fill: 'none', stroke: '#cbd5e1', 'stroke-width': 1, rx: 2
});
}
function drawFlyingCells(localT, fromLoc, toLoc, applied=false) {
// Each changed cell at lerp(from, to, localT)
for (const c of changed) {
let p0, p1;
if (fromLoc.kind === 'grid') p0 = gridPos(fromLoc.side, c.row, c.col);
else p0 = stripPos(fromLoc.loc, c.packedIdx);
if (toLoc.kind === 'grid') p1 = gridPos(toLoc.side, c.row, c.col);
else p1 = stripPos(toLoc.loc, c.packedIdx);
const p = lerpPt(p0, p1, localT);
const sz = lerp(
fromLoc.kind === 'grid' ? TRAINER.cell - 1 : STRIP.cell,
toLoc.kind === 'grid' ? REPLICA.cell - 1 : STRIP.cell,
localT
);
el('rect', {
x: p.x + 0.5, y: p.y + 0.5,
width: sz, height: sz,
fill: applied ? '#16a34a' : '#dc2626',
filter: 'url(#glow)'
});
}
}
function drawStrip(loc, label, sizeLabel) {
if (!changed.length) return;
const stripWidth = changed.length * (STRIP.cell + STRIP.gap);
const first = stripPos(loc, 0);
const last = stripPos(loc, changed.length - 1);
// Frame
el('rect', {
x: first.x - 3, y: first.y - 3,
width: (last.x - first.x) + STRIP.cell + 6, height: STRIP.cell + 6,
fill: '#fff', stroke: '#16a34a', 'stroke-width': 1.5, rx: 2
});
// Cells
for (const c of changed) {
const p = stripPos(loc, c.packedIdx);
el('rect', {
x: p.x + 0.5, y: p.y + 0.5,
width: STRIP.cell - 1, height: STRIP.cell - 1,
fill: '#dc2626'
});
}
// Label below
const cx = first.x + (last.x - first.x + STRIP.cell) / 2;
if (label) {
el('text', {
x: cx, y: first.y + STRIP.cell + 16,
'text-anchor': 'middle', 'font-size': 9, 'font-weight': 700, fill: '#15803d',
'font-family': 'JetBrains Mono, monospace'
}).textContent = label;
}
if (sizeLabel) {
el('text', {
x: cx, y: first.y - 6,
'text-anchor': 'middle', 'font-size': 9.5, 'font-weight': 700, fill: '#15803d',
'font-family': 'JetBrains Mono, monospace'
}).textContent = sizeLabel;
}
}
function drawFlyingStrip(t, locA, locB, sizeLabel) {
// Group flying with cells inside
const stripWidth = changed.length * (STRIP.cell + STRIP.gap);
const a = stripPos(locA, 0);
const b = stripPos(locB, 0);
const p = lerpPt(a, b, t);
// Frame
el('rect', {
x: p.x - 3, y: p.y - 3,
width: stripWidth + 6, height: STRIP.cell + 6,
fill: '#fff', stroke: '#16a34a', 'stroke-width': 1.5, rx: 2, filter: 'url(#glow)'
});
// Cells
for (let i = 0; i < changed.length; i++) {
el('rect', {
x: p.x + i * (STRIP.cell + STRIP.gap) + 0.5, y: p.y + 0.5,
width: STRIP.cell - 1, height: STRIP.cell - 1,
fill: '#dc2626'
});
}
// Label
if (sizeLabel) {
el('text', {
x: p.x + stripWidth / 2, y: p.y - 6,
'text-anchor': 'middle', 'font-size': 9.5, 'font-weight': 700, fill: '#15803d',
'font-family': 'JetBrains Mono, monospace'
}).textContent = sizeLabel;
}
}
function drawMiniGrid(x, y, scale, label, opacity = 1) {
const N = TRAINER.N;
const cell = TRAINER.cell * scale;
const W = N * cell;
const g = el('g', { opacity });
// Frame
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);
for (const c of cells) {
const fill = c.changed ? '#dc2626' : '#cbd5e1';
el('rect', {
x: x + c.col * cell, y: y + c.row * cell,
width: cell - (cell > 3 ? 0.5 : 0), height: cell - (cell > 3 ? 0.5 : 0),
fill
}, g);
}
if (label) {
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)
.textContent = label;
}
}
// ===== Phase renderer =====
function renderDelta(phaseInfo) {
const { phase, local } = phaseInfo;
const name = phase.name;
// Replica grid: cells appear green only in unpack/resume
const replicaApplied = (c) => {
if (name === 'unpack') return local > 0.85 && c.changed;
if (name === 'resume') return c.changed;
return false;
};
const replicaPaused = (name === 'pause' || name === 'download' || name === 'unpack');
// ---- bucket (always visible in delta mode) ----
drawBucket();
// ---- node boxes ----
drawNodeBoxes({ trainerActive: name === 'step', replicaPaused });
// ---- trainer grid frame + base cells ----
drawGridFrame('trainer');
// Always draw all gray cells (unchanged)
drawGridFill('trainer', c => !c.changed, '#cbd5e1');
// Draw changed cells at trainer grid position ONLY if they haven't left yet
const changedAtTrainer = (name === 'idle' || name === 'step');
if (changedAtTrainer) {
const op = name === 'step' ? Math.min(1, local * 1.4) : 0.0;
drawGridFill('trainer', c => c.changed, '#dc2626', op);
}
// ---- replica grid ----
drawGridFrame('replica');
drawGridFill('replica', c => !c.changed, '#cbd5e1');
drawGridFill('replica', c => replicaApplied(c), '#16a34a');
// During unpack, draw the unchanged-replica cells where changed cells "will" land
if (name === 'unpack' && local <= 0.85) {
drawGridFill('replica', c => c.changed, '#cbd5e1');
} else if (name !== 'resume' && name !== 'unpack') {
drawGridFill('replica', c => c.changed, '#cbd5e1');
}
// ---- in-flight stuff ----
if (name === 'extract') {
// Cells fly from trainer grid (row,col) to trainer-side strip (packedIdx)
drawFlyingCells(local, { kind: 'grid', side: 'trainer' }, { kind: 'strip', loc: 'trainer' }, false);
} else if (name === 'upload') {
// Strip flies from trainer to bucket
drawFlyingStrip(local, 'trainer', 'bucket', '~6 GB');
} else if (name === 'settle') {
// Strip in bucket
drawStrip('bucket', 'delta.safetensors', '~6 GB');
} else if (name === 'pause') {
// Strip in bucket, replica becomes red
drawStrip('bucket', 'delta.safetensors', '~6 GB');
} else if (name === 'download') {
// Strip flies from bucket to replica
drawStrip('bucket', '', ''); // ghost in bucket
drawFlyingStrip(local, 'bucket', 'replica', '~6 GB');
} else if (name === 'unpack') {
if (local < 0.15) {
drawStrip('replica', 'delta.safetensors', '~6 GB');
} else {
// Cells fly from replica-side strip back to (row, col) in replica grid
const cellT = Math.min(1, (local - 0.15) / 0.85);
drawFlyingCells(cellT, { kind: 'strip', loc: 'replica' }, { kind: 'grid', side: 'replica' }, cellT > 0.9);
}
} else if (name === 'resume') {
// Apply complete
}
// ---- title ----
el('text', { x: 410, y: 30, 'text-anchor': 'middle', 'font-size': 12.5, 'font-weight': 700, fill: '#1a1a2e' })
.textContent = phase.title;
}
function renderNccl(phaseInfo) {
const { phase, local } = phaseInfo;
const name = phase.name;
const replicaPaused = (name !== 'idle' && name !== 'step' && name !== 'resume');
drawNodeBoxes({ trainerActive: name === 'step', replicaPaused });
// No bucket in NCCL: draw a "direct broadcast" hint
el('rect', { x: BUCKET.x - 10, y: BUCKET.y + 70, width: BUCKET.w + 20, height: 60, fill: 'transparent', stroke: 'none' });
if (name === 'idle' || name === 'step' || name === 'pause' || name === 'resume') {
el('text', { x: BUCKET.x + BUCKET.w/2, y: BUCKET.y + 110, 'text-anchor': 'middle', 'font-size': 10, fill: '#94a3b8', 'font-style': 'italic' })
.textContent = 'direct NCCL';
el('text', { x: BUCKET.x + BUCKET.w/2, y: BUCKET.y + 125, 'text-anchor': 'middle', 'font-size': 10, fill: '#94a3b8', 'font-style': 'italic' })
.textContent = 'no shared storage';
}
// Trainer grid
drawGridFrame('trainer');
drawGridFill('trainer', c => !c.changed, '#cbd5e1');
if (name === 'step') {
drawGridFill('trainer', c => c.changed, '#dc2626', Math.min(1, local * 1.4));
} else if (name !== 'idle') {
drawGridFill('trainer', c => c.changed, '#dc2626');
}
// Replica grid
drawGridFrame('replica');
drawGridFill('replica', c => !c.changed, '#cbd5e1');
if (name === 'apply' && local > 0.7) drawGridFill('replica', c => c.changed, '#16a34a');
else if (name === 'resume') drawGridFill('replica', c => c.changed, '#16a34a');
else drawGridFill('replica', c => c.changed, '#cbd5e1');
// In-flight: clone -> broadcast
// The whole grid duplicates and slides across
if (name === 'clone') {
const scale = 0.5;
const fx = TRAINER.x + 210, fy = TRAINER.y + 80;
drawMiniGrid(fx, fy, scale, '810 GB', local);
} else if (name === 'broadcast') {
const scale = 0.5;
const W = TRAINER.cell * TRAINER.N * scale;
const startX = TRAINER.x + 210;
const endX = REPLICA.x - 20 - W;
const y = TRAINER.y + 80;
const x = lerp(startX, endX, ease(local));
// Fat pipe behind
el('line', {
x1: TRAINER.x + 200, y1: TRAINER.y + 100,
x2: REPLICA.x, y2: REPLICA.y + 100,
stroke: '#dc2626', 'stroke-width': 6, opacity: 0.18, 'stroke-linecap': 'round'
});
drawMiniGrid(x, y, scale, '810 GB', 1);
} else if (name === 'apply') {
const scale = 0.5;
const fx = REPLICA.x - 20 - TRAINER.cell * TRAINER.N * scale;
const fy = TRAINER.y + 80;
const fade = Math.max(0, 1 - local * 1.5);
drawMiniGrid(fx, fy, scale, '810 GB', fade);
}
el('text', { x: 410, y: 30, 'text-anchor': 'middle', 'font-size': 12.5, 'font-weight': 700, fill: '#1a1a2e' })
.textContent = phase.title;
}
function renderAt(t) {
svg.innerHTML = '';
drawDefs();
const phases = phasesFor(mode);
const info = phaseAtTime(phases, t);
if (mode === 'delta') renderDelta(info);
else renderNccl(info);
}
// ===== Stats =====
function updateStats() {
if (mode === 'delta') {
document.getElementById('statPayload').textContent = '~6 GB';
document.getElementById('statPause').textContent = '~2 s';
document.getElementById('statReduction').textContent = '135×';
} else {
document.getElementById('statPayload').textContent = '810 GB';
document.getElementById('statPause').textContent = '~10 s';
document.getElementById('statReduction').textContent = '1×';
}
}
// ===== Playback =====
function play() {
if (playing) return;
initCells();
playing = true;
document.getElementById('playBtn').textContent = '⏸ stop';
document.getElementById('playBtn').classList.add('playing');
const phases = phasesFor(mode);
const total = totalDuration(phases);
startMs = performance.now();
function tick(now) {
if (!playing) return;
const t = now - startMs;
if (t >= total) {
renderAt(total - 1);
playing = false;
document.getElementById('playBtn').textContent = '↻ play again';
document.getElementById('playBtn').classList.remove('playing');
return;
}
renderAt(t);
rafId = requestAnimationFrame(tick);
}
rafId = requestAnimationFrame(tick);
}
function reset() {
if (rafId) cancelAnimationFrame(rafId);
playing = false;
initCells();
renderAt(0);
document.getElementById('playBtn').textContent = '▶ play';
document.getElementById('playBtn').classList.remove('playing');
updateStats();
}
document.getElementById('playBtn').addEventListener('click', () => {
if (playing) { reset(); } else { reset(); play(); }
});
document.getElementById('modeToggle').querySelectorAll('button').forEach(b => {
b.addEventListener('click', () => {
document.getElementById('modeToggle').querySelectorAll('button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
mode = b.dataset.mode;
reset();
});
});
reset();
</script>
</body>
</html>