| <!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 & 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; |
| } |
| |
| |
| 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 }; |
| |
| |
| |
| |
| |
| let cells = []; |
| let changed = []; |
| const CHANGED_FRAC = 4 / 256; |
| |
| function initCells() { |
| cells = []; |
| changed = []; |
| |
| 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); |
| } |
| } |
| } |
| |
| |
| 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) }; } |
| |
| |
| |
| 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); } |
| |
| |
| 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; |
| |
| 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'; |
| |
| |
| 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 }; |
| |
| 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) { |
| |
| 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); |
| |
| 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 |
| }); |
| |
| 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' |
| }); |
| } |
| |
| 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) { |
| |
| const stripWidth = changed.length * (STRIP.cell + STRIP.gap); |
| const a = stripPos(locA, 0); |
| const b = stripPos(locB, 0); |
| const p = lerpPt(a, b, t); |
| |
| 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)' |
| }); |
| |
| 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' |
| }); |
| } |
| |
| 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 }); |
| |
| 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; |
| } |
| } |
| |
| |
| function renderDelta(phaseInfo) { |
| const { phase, local } = phaseInfo; |
| const name = phase.name; |
| |
| |
| 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'); |
| |
| |
| drawBucket(); |
| |
| |
| drawNodeBoxes({ trainerActive: name === 'step', replicaPaused }); |
| |
| |
| drawGridFrame('trainer'); |
| |
| drawGridFill('trainer', c => !c.changed, '#cbd5e1'); |
| |
| 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); |
| } |
| |
| |
| drawGridFrame('replica'); |
| drawGridFill('replica', c => !c.changed, '#cbd5e1'); |
| drawGridFill('replica', c => replicaApplied(c), '#16a34a'); |
| |
| if (name === 'unpack' && local <= 0.85) { |
| drawGridFill('replica', c => c.changed, '#cbd5e1'); |
| } else if (name !== 'resume' && name !== 'unpack') { |
| drawGridFill('replica', c => c.changed, '#cbd5e1'); |
| } |
| |
| |
| if (name === 'extract') { |
| |
| drawFlyingCells(local, { kind: 'grid', side: 'trainer' }, { kind: 'strip', loc: 'trainer' }, false); |
| } else if (name === 'upload') { |
| |
| drawFlyingStrip(local, 'trainer', 'bucket', '~6 GB'); |
| } else if (name === 'settle') { |
| |
| drawStrip('bucket', 'delta.safetensors', '~6 GB'); |
| } else if (name === 'pause') { |
| |
| drawStrip('bucket', 'delta.safetensors', '~6 GB'); |
| } else if (name === 'download') { |
| |
| drawStrip('bucket', '', ''); |
| drawFlyingStrip(local, 'bucket', 'replica', '~6 GB'); |
| } else if (name === 'unpack') { |
| if (local < 0.15) { |
| drawStrip('replica', 'delta.safetensors', '~6 GB'); |
| } else { |
| |
| 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') { |
| |
| } |
| |
| |
| 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 }); |
| |
| |
| 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'; |
| } |
| |
| |
| 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'); |
| } |
| |
| |
| 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'); |
| |
| |
| |
| 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)); |
| |
| 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); |
| } |
| |
| |
| 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×'; |
| } |
| } |
| |
| |
| 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> |
|
|