Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Commit Β·
9fab25e
1
Parent(s): b531a96
made inference throughput visualization more elaborate
Browse files
app/src/content/chapters/5-infrastructure.mdx
CHANGED
|
@@ -447,10 +447,12 @@ With a trillion-parameter model you won't be generating billions of tokens per h
|
|
| 447 |
|
| 448 |
#### Visualizing Throughput
|
| 449 |
|
| 450 |
-
To get an intuition for what these throughput numbers feel like, <FigRef target="inference-throughput" /> lets you pick a model and scale up the number of GPUs. Each page represents roughly 500 tokens of generated text. At high enough throughput, pages roll up into books (
|
| 451 |
|
|
|
|
| 452 |
<HtmlEmbed
|
| 453 |
id="inference-throughput"
|
| 454 |
src="inference-throughput.html"
|
| 455 |
-
caption="Interactive GPU throughput simulator. Select a model and adjust the number of H100 GPUs to see how fast pages (500 tokens
|
| 456 |
/>
|
|
|
|
|
|
| 447 |
|
| 448 |
#### Visualizing Throughput
|
| 449 |
|
| 450 |
+
To get an intuition for what these throughput numbers feel like, <FigRef target="inference-throughput" /> lets you pick a model and scale up the number of GPUs. Each page represents roughly 500 tokens of generated text. At high enough throughput, pages roll up into books (250 pages each), and books into bookshelves (250 books each).
|
| 451 |
|
| 452 |
+
<Wide>
|
| 453 |
<HtmlEmbed
|
| 454 |
id="inference-throughput"
|
| 455 |
src="inference-throughput.html"
|
| 456 |
+
caption="Interactive GPU throughput simulator. Select a model and adjust the number of H100 GPUs to see how fast pages (500 tokens), books (250 pages), or bookshelves (250 books) are generated."
|
| 457 |
/>
|
| 458 |
+
</Wide>
|
app/src/content/embeds/inference-throughput.html
CHANGED
|
@@ -2,379 +2,662 @@
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
-
<title>
|
| 6 |
<style>
|
| 7 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 8 |
-
body { background: #fff; font-family: system-ui, sans-serif; display: flex; flex-direction: column; align-items: center; padding:
|
| 9 |
-
|
| 10 |
-
.
|
| 11 |
-
|
| 12 |
-
.
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
.
|
| 17 |
-
|
| 18 |
-
.
|
| 19 |
-
.
|
| 20 |
-
.
|
| 21 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
</style>
|
| 23 |
</head>
|
| 24 |
<body>
|
| 25 |
-
<div class="
|
| 26 |
-
|
| 27 |
-
<
|
| 28 |
-
|
| 29 |
-
<
|
| 30 |
-
<select id="model">
|
| 31 |
-
<option value="45540">SmolLM2-135M (45,540 tps/gpu)</option>
|
| 32 |
-
<option value="8086">Qwen3-4B (8,086 tps/gpu)</option>
|
| 33 |
-
<option value="6443">Qwen3-8B (6,443 tps/gpu)</option>
|
| 34 |
-
<option value="6117">GPT-OSS-120B (6,117 tps/gpu)</option>
|
| 35 |
-
<option value="1724">Gemma-3-27B (1,724 tps/gpu)</option>
|
| 36 |
-
</select>
|
| 37 |
</div>
|
| 38 |
-
<div class="control-group">
|
| 39 |
-
<label>GPUs</label>
|
| 40 |
-
<div style="display:flex;align-items:center;gap:8px;">
|
| 41 |
-
<input type="range" id="gpus" min="0" max="10" step="0.01" value="0">
|
| 42 |
-
<span class="gpu-count" id="gpuLabel">1</span>
|
| 43 |
-
</div>
|
| 44 |
-
</div>
|
| 45 |
-
</div>
|
| 46 |
|
| 47 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
<div class="
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
</div>
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
<div class="label">
|
|
|
|
|
|
|
|
|
|
| 57 |
</div>
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
<div class="
|
|
|
|
| 61 |
</div>
|
| 62 |
</div>
|
| 63 |
|
| 64 |
<script>
|
| 65 |
const canvas = document.getElementById('c');
|
| 66 |
const ctx = canvas.getContext('2d');
|
| 67 |
-
const W = 1000, H = 400;
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
const modelSelect = document.getElementById('model');
|
| 78 |
const gpuSlider = document.getElementById('gpus');
|
| 79 |
-
const gpuLabel = document.getElementById('gpuLabel');
|
| 80 |
const ppsEl = document.getElementById('pps');
|
| 81 |
const ppsLabelEl = document.getElementById('ppsLabel');
|
| 82 |
const tpsEl = document.getElementById('tps');
|
|
|
|
| 83 |
const totalEl = document.getElementById('total');
|
| 84 |
const totalLabelEl = document.getElementById('totalLabel');
|
| 85 |
|
| 86 |
-
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
let lastTime = performance.now();
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
|
|
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
function
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
|
| 109 |
-
gpuSlider.addEventListener('input', () => {
|
| 110 |
-
|
| 111 |
-
});
|
| 112 |
|
| 113 |
-
function
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
let cols, rows;
|
| 120 |
if (count === 1) { cols = 1; rows = 1; }
|
| 121 |
else if (count === 2) { cols = 2; rows = 1; }
|
| 122 |
else if (count <= 4) { cols = 2; rows = 2; }
|
| 123 |
-
else
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
else if (count <= 64) { cols = 8; rows = 8; }
|
| 127 |
-
else if (count <= 128) { cols = 16; rows = 8; }
|
| 128 |
-
else if (count <= 256) { cols = 16; rows = 16; }
|
| 129 |
-
else if (count <= 512) { cols = 32; rows = 16; }
|
| 130 |
-
else { cols = 32; rows = 32; }
|
| 131 |
-
|
| 132 |
-
const gap = count <= 16 ? 3 : count <= 64 ? 2 : 1;
|
| 133 |
-
const cellW = (aw - gap * (cols - 1)) / cols;
|
| 134 |
-
const cellH = (ah - gap * (rows - 1)) / rows;
|
| 135 |
-
const gw = Math.min(cellW, cellH * 0.6); // GPU aspect ratio ~0.6
|
| 136 |
const gh = gw / 0.6;
|
| 137 |
-
|
| 138 |
-
const
|
| 139 |
-
const totalH = rows * gh + (rows - 1) * gap;
|
| 140 |
-
const offX = ax + (aw - totalW) / 2;
|
| 141 |
-
const offY = ay + (ah - totalH) / 2;
|
| 142 |
-
|
| 143 |
const fanSpeed = (performance.now() / 200) * Math.min(getPps(), 100);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
for (let i = 0; i < count; i++) {
|
| 146 |
-
const
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
// GPU body
|
| 152 |
-
ctx.fillStyle = '#2a2a2a';
|
| 153 |
-
ctx.fillRect(x, y, gw, gh);
|
| 154 |
-
ctx.strokeStyle = '#000';
|
| 155 |
-
ctx.lineWidth = count <= 16 ? 1.5 : 0.5;
|
| 156 |
-
ctx.strokeRect(x, y, gw, gh);
|
| 157 |
-
|
| 158 |
-
if (gw >= 12) {
|
| 159 |
-
// Fan area
|
| 160 |
-
const fanSize = gw * 0.7;
|
| 161 |
-
const fanX = x + (gw - fanSize) / 2;
|
| 162 |
-
const fanY = y + gw * 0.08;
|
| 163 |
-
ctx.fillStyle = '#1a1a1a';
|
| 164 |
-
ctx.fillRect(fanX, fanY, fanSize, fanSize);
|
| 165 |
-
|
| 166 |
-
// Fan circle + blades
|
| 167 |
-
const fcx = fanX + fanSize / 2;
|
| 168 |
-
const fcy = fanY + fanSize / 2;
|
| 169 |
-
const fr = fanSize / 2 - 2;
|
| 170 |
-
ctx.beginPath();
|
| 171 |
-
ctx.arc(fcx, fcy, fr, 0, Math.PI * 2);
|
| 172 |
-
ctx.fillStyle = '#333';
|
| 173 |
-
ctx.fill();
|
| 174 |
-
|
| 175 |
-
if (gw >= 20) {
|
| 176 |
-
// Fan blades
|
| 177 |
-
const bladeR = fr - 2;
|
| 178 |
-
ctx.save();
|
| 179 |
-
ctx.translate(fcx, fcy);
|
| 180 |
-
ctx.rotate(fanSpeed + i * 0.5); // offset per GPU
|
| 181 |
-
const bladeCount = gw >= 40 ? 7 : 5;
|
| 182 |
-
for (let b = 0; b < bladeCount; b++) {
|
| 183 |
-
ctx.rotate(Math.PI * 2 / bladeCount);
|
| 184 |
-
ctx.beginPath();
|
| 185 |
-
ctx.moveTo(0, 0);
|
| 186 |
-
ctx.quadraticCurveTo(bladeR * 0.5, bladeR * 0.3, bladeR * 0.85, 0);
|
| 187 |
-
ctx.quadraticCurveTo(bladeR * 0.5, -bladeR * 0.3, 0, 0);
|
| 188 |
-
ctx.fillStyle = '#555';
|
| 189 |
-
ctx.fill();
|
| 190 |
-
}
|
| 191 |
-
ctx.restore();
|
| 192 |
-
}
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
}
|
| 216 |
-
} else {
|
| 217 |
-
// Too small for detail β just a green LED dot
|
| 218 |
-
ctx.fillStyle = '#8a8';
|
| 219 |
-
const dotR = Math.max(0.5, gw * 0.1);
|
| 220 |
-
ctx.fillRect(x + gw/2 - dotR, y + gh * 0.8, dotR * 2, dotR * 2);
|
| 221 |
}
|
|
|
|
|
|
|
| 222 |
}
|
| 223 |
}
|
| 224 |
|
| 225 |
-
//
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
ctx.
|
| 245 |
-
|
| 246 |
-
// Title lines on cover
|
| 247 |
-
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
| 248 |
-
ctx.fillRect(p.x + 5, p.y + 5, 8, 1.5);
|
| 249 |
-
ctx.fillRect(p.x + 5, p.y + 8, 6, 1.5);
|
| 250 |
-
// Border
|
| 251 |
-
ctx.strokeStyle = '#333';
|
| 252 |
-
ctx.lineWidth = 0.5;
|
| 253 |
-
ctx.strokeRect(p.x, p.y, bw, bh);
|
| 254 |
-
} else {
|
| 255 |
-
const pw = 14, ph = 18;
|
| 256 |
-
// Shadow
|
| 257 |
-
ctx.fillStyle = 'rgba(0,0,0,0.08)';
|
| 258 |
-
ctx.fillRect(p.x + 2, p.y + 2, pw, ph);
|
| 259 |
-
// Page body
|
| 260 |
-
ctx.fillStyle = p.color;
|
| 261 |
-
ctx.fillRect(p.x, p.y, pw, ph);
|
| 262 |
-
ctx.strokeStyle = '#aaa';
|
| 263 |
-
ctx.lineWidth = 0.5;
|
| 264 |
-
ctx.strokeRect(p.x, p.y, pw, ph);
|
| 265 |
-
// Text lines
|
| 266 |
-
ctx.fillStyle = '#ccc';
|
| 267 |
-
for (let i = 0; i < 4; i++) {
|
| 268 |
-
const lw = 6 + Math.sin(p.id + i) * 4;
|
| 269 |
-
ctx.fillRect(p.x + 2, p.y + 3 + i * 3, lw, 1);
|
| 270 |
-
}
|
| 271 |
}
|
|
|
|
| 272 |
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
}
|
| 275 |
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
}
|
| 279 |
|
| 280 |
function spawnItem() {
|
| 281 |
-
const
|
| 282 |
-
const
|
| 283 |
-
const
|
| 284 |
-
|
| 285 |
-
const
|
| 286 |
-
const
|
| 287 |
-
const
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
y,
|
| 291 |
-
|
| 292 |
-
type: bookMode ? 'book' : 'page',
|
| 293 |
-
color: bookMode
|
| 294 |
-
? bookColors[Math.floor(Math.random() * bookColors.length)]
|
| 295 |
-
: pageColors[Math.floor(Math.random() * pageColors.length)],
|
| 296 |
-
id: totalPages
|
| 297 |
});
|
| 298 |
-
totalPages += bookMode ? PAGES_PER_BOOK : 1;
|
| 299 |
}
|
| 300 |
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
}
|
| 307 |
|
| 308 |
function frame(now) {
|
| 309 |
-
const dt = Math.min((now - lastTime) / 1000, 0.1);
|
| 310 |
-
|
| 311 |
|
| 312 |
ctx.clearRect(0, 0, W, H);
|
|
|
|
| 313 |
|
| 314 |
-
|
| 315 |
-
ctx.
|
| 316 |
-
ctx.
|
| 317 |
-
|
| 318 |
-
// Arrow flow hint (subtle)
|
| 319 |
-
ctx.strokeStyle = '#e0e0e0';
|
| 320 |
-
ctx.lineWidth = 1;
|
| 321 |
-
ctx.setLineDash([4, 8]);
|
| 322 |
-
for (let y = 60; y < H - 30; y += 40) {
|
| 323 |
-
ctx.beginPath();
|
| 324 |
-
ctx.moveTo(getSpawnX(), y);
|
| 325 |
-
ctx.lineTo(W - 20, y);
|
| 326 |
-
ctx.stroke();
|
| 327 |
-
}
|
| 328 |
ctx.setLineDash([]);
|
| 329 |
|
| 330 |
-
|
| 331 |
-
const pps =
|
| 332 |
-
const bookMode = isBookMode();
|
| 333 |
-
const spawnRate = bookMode ? pps / PAGES_PER_BOOK : pps;
|
| 334 |
-
|
| 335 |
spawnAccum += spawnRate * dt;
|
| 336 |
-
while (spawnAccum >= 1) {
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
pages.splice(i, 1);
|
| 346 |
continue;
|
| 347 |
}
|
| 348 |
-
drawItem(
|
| 349 |
}
|
| 350 |
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
ppsLabelEl.textContent = '
|
| 360 |
-
|
| 361 |
-
ppsEl.textContent = formatNum(pps);
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
totalEl.textContent = formatNum(
|
| 368 |
-
totalLabelEl.textContent = 'Books generated';
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
}
|
| 373 |
|
| 374 |
requestAnimationFrame(frame);
|
| 375 |
}
|
| 376 |
|
| 377 |
-
gpuLabel.textContent = getGpuCount().toLocaleString();
|
| 378 |
requestAnimationFrame(frame);
|
| 379 |
</script>
|
| 380 |
</body>
|
|
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
+
<title>GPU Page Generator</title>
|
| 6 |
<style>
|
| 7 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 8 |
+
body { background: #fff; font-family: system-ui, sans-serif; display: flex; flex-direction: column; align-items: center; padding: 16px 0; }
|
| 9 |
+
|
| 10 |
+
.viz { width: 100%; max-width: 1200px; }
|
| 11 |
+
|
| 12 |
+
.legend { color: #888; font-size: 12px; text-align: center; margin-bottom: 10px; }
|
| 13 |
+
.legend b { color: #555; }
|
| 14 |
+
|
| 15 |
+
.control-group { display: flex; flex-direction: column; gap: 3px; }
|
| 16 |
+
.control-group label { font-size: 11px; font-weight: 600; color: #444; text-transform: uppercase; letter-spacing: 0.5px; }
|
| 17 |
+
select { font-size: 13px; font-family: inherit; padding: 5px 8px; border: 2px solid #000; background: #fff; cursor: pointer; }
|
| 18 |
+
.canvas-row { display: flex; gap: 8px; align-items: stretch; }
|
| 19 |
+
.canvas-row .side-panel { display: flex; flex-direction: column; gap: 6px; justify-content: center; }
|
| 20 |
+
.canvas-row canvas { flex: 1; min-width: 0; }
|
| 21 |
+
.slider-area { position: relative; width: 100%; margin-bottom: 4px; }
|
| 22 |
+
.slider-area input[type=range] { width: 100%; margin: 0; display: block; -webkit-appearance: none; appearance: none; height: 6px; border-radius: 3px; outline: none; cursor: pointer; }
|
| 23 |
+
.slider-area input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #fff; border: 2px solid #2e5f7e; cursor: pointer; margin-top: -5px; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
|
| 24 |
+
.slider-area input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: #fff; border: 2px solid #2e5f7e; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
|
| 25 |
+
|
| 26 |
+
.landmark-row { position: relative; width: 100%; height: 14px; }
|
| 27 |
+
.landmark { position: absolute; transform: translateX(-50%); cursor: pointer; display: flex; flex-direction: column; align-items: center; }
|
| 28 |
+
.landmark-row.a1 .landmark, .landmark-row.a2 .landmark, .landmark-row.a3 .landmark { bottom: 0; }
|
| 29 |
+
.landmark-row.b1 .landmark, .landmark-row.b2 .landmark, .landmark-row.b3 .landmark { top: 0; }
|
| 30 |
+
.landmark .tick { width: 1.5px; height: 4px; flex-shrink: 0; }
|
| 31 |
+
.landmark .name { font-size: 8px; font-weight: 600; white-space: nowrap; letter-spacing: 0.3px; line-height: 1.1; }
|
| 32 |
+
.landmark:hover .name { color: #000 !important; }
|
| 33 |
+
|
| 34 |
+
.landmark-row.a1 .tick, .landmark-row.a2 .tick, .landmark-row.a3 .tick { background: #b45309; }
|
| 35 |
+
.landmark-row.a1 .name, .landmark-row.a2 .name, .landmark-row.a3 .name { color: #b45309; }
|
| 36 |
+
.landmark-row.b1 .tick, .landmark-row.b2 .tick, .landmark-row.b3 .tick { background: #2e5f7e; }
|
| 37 |
+
.landmark-row.b1 .name, .landmark-row.b2 .name, .landmark-row.b3 .name { color: #2e5f7e; }
|
| 38 |
+
|
| 39 |
+
.landmark .tooltip { display: none; position: absolute; left: 50%; transform: translateX(-50%); background: #1a1a2e; color: #fff; font-size: 11px; padding: 8px 12px; border-radius: 5px; width: 260px; white-space: normal; z-index: 10; pointer-events: none; line-height: 1.45; text-align: left; }
|
| 40 |
+
.landmark-row.a1 .tooltip, .landmark-row.a2 .tooltip, .landmark-row.a3 .tooltip { bottom: calc(100% + 4px); }
|
| 41 |
+
.landmark-row.b1 .tooltip, .landmark-row.b2 .tooltip, .landmark-row.b3 .tooltip { top: calc(100% + 4px); }
|
| 42 |
+
.landmark .tooltip::after { content: ''; position: absolute; left: 50%; transform: translateX(-50%); border: 5px solid transparent; }
|
| 43 |
+
.landmark .tooltip.tip-right::after { left: auto; right: 16px; transform: none; }
|
| 44 |
+
.landmark .tooltip.tip-left::after { left: 16px; transform: none; }
|
| 45 |
+
.landmark-row.a1 .tooltip::after, .landmark-row.a2 .tooltip::after, .landmark-row.a3 .tooltip::after { top: 100%; border-top-color: #1a1a2e; }
|
| 46 |
+
.landmark-row.b1 .tooltip::after, .landmark-row.b2 .tooltip::after, .landmark-row.b3 .tooltip::after { bottom: 100%; border-bottom-color: #1a1a2e; }
|
| 47 |
+
.landmark:hover .tooltip { display: block; }
|
| 48 |
+
|
| 49 |
+
canvas { width: 100%; border: 2px solid #000; background: #fafafa; display: block; }
|
| 50 |
+
|
| 51 |
+
.metrics { display: flex; gap: 24px; margin-top: 10px; flex-wrap: wrap; justify-content: center; }
|
| 52 |
+
.metric { text-align: center; min-width: 100px; }
|
| 53 |
+
.metric .value { font-size: 24px; font-weight: 700; color: #2e5f7e; font-variant-numeric: tabular-nums; font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; }
|
| 54 |
+
.metric .label { font-size: 10px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
|
| 55 |
+
|
| 56 |
+
.datasets { margin-top: 12px; width: 100%; }
|
| 57 |
+
.datasets-title { font-size: 10px; font-weight: 700; color: #444; text-transform: uppercase; letter-spacing: 0.5px; text-align: center; margin-bottom: 6px; }
|
| 58 |
+
.dataset-bars { display: flex; flex-wrap: wrap; gap: 6px 16px; justify-content: center; }
|
| 59 |
+
.dataset-item { display: flex; align-items: center; gap: 5px; font-size: 11px; min-width: 140px; position: relative; cursor: pointer; }
|
| 60 |
+
.dataset-item .ds-check { font-size: 13px; width: 16px; text-align: center; }
|
| 61 |
+
.dataset-item .ds-name { font-weight: 600; color: #333; }
|
| 62 |
+
.dataset-item .ds-time { color: #2e5f7e; font-weight: 700; font-variant-numeric: tabular-nums; }
|
| 63 |
+
.dataset-item .ds-size { color: #999; font-size: 10px; }
|
| 64 |
+
.dataset-item.done .ds-name { color: #16a34a; }
|
| 65 |
+
.dataset-item.done .ds-time { color: #16a34a; }
|
| 66 |
+
.dataset-item .ds-tip { display: none; position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: #1a1a2e; color: #fff; font-size: 11px; padding: 8px 12px; border-radius: 5px; width: 240px; white-space: normal; z-index: 10; pointer-events: none; line-height: 1.45; text-align: left; }
|
| 67 |
+
.dataset-item .ds-tip::after { content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: #1a1a2e; }
|
| 68 |
+
.dataset-item:hover .ds-tip { display: block; }
|
| 69 |
</style>
|
| 70 |
</head>
|
| 71 |
<body>
|
| 72 |
+
<div class="viz">
|
| 73 |
+
<div class="legend">
|
| 74 |
+
1 π <b>Page</b> = 500 tokens ≈ 1,800 characters |
|
| 75 |
+
1 π <b>Book</b> = 250 pages = 125K tokens |
|
| 76 |
+
1 π <b>Bookshelf</b> = 250 books = 31.25M tokens
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
<div class="slider-area">
|
| 80 |
+
<div class="landmark-row a3" id="row-a3"></div>
|
| 81 |
+
<div class="landmark-row a2" id="row-a2"></div>
|
| 82 |
+
<div class="landmark-row a1" id="row-a1"></div>
|
| 83 |
+
<input type="range" id="gpus" min="0" max="1" step="0.001" value="0">
|
| 84 |
+
<div class="landmark-row b1" id="row-b1"></div>
|
| 85 |
+
<div class="landmark-row b2" id="row-b2"></div>
|
| 86 |
+
<div class="landmark-row b3" id="row-b3"></div>
|
| 87 |
+
</div>
|
| 88 |
|
| 89 |
+
<div class="canvas-row">
|
| 90 |
+
<div class="side-panel">
|
| 91 |
+
<div class="control-group">
|
| 92 |
+
<label>Model</label>
|
| 93 |
+
<select id="model">
|
| 94 |
+
<option value="45540">SmolLM2-135M (45,540 tps/gpu)</option>
|
| 95 |
+
<option value="8086">Qwen3-4B (8,086 tps/gpu)</option>
|
| 96 |
+
<option value="6443">Qwen3-8B (6,443 tps/gpu)</option>
|
| 97 |
+
<option value="6117">GPT-OSS-120B (6,117 tps/gpu)</option>
|
| 98 |
+
<option value="1724">Gemma-3-27B (1,724 tps/gpu)</option>
|
| 99 |
+
</select>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
<canvas id="c"></canvas>
|
| 103 |
</div>
|
| 104 |
+
|
| 105 |
+
<div class="metrics">
|
| 106 |
+
<div class="metric"><div class="value" id="pps">0</div><div class="label" id="ppsLabel">Pages / second</div></div>
|
| 107 |
+
<div class="metric"><div class="value" id="tps">0</div><div class="label">Tokens / second</div></div>
|
| 108 |
+
<div class="metric"><div class="value" id="totalTokens">0</div><div class="label">Tokens generated</div></div>
|
| 109 |
+
<div class="metric"><div class="value" id="total">0</div><div class="label" id="totalLabel">Pages generated</div></div>
|
| 110 |
</div>
|
| 111 |
+
|
| 112 |
+
<div class="datasets">
|
| 113 |
+
<div class="datasets-title">Time to generate dataset at current throughput</div>
|
| 114 |
+
<div class="dataset-bars" id="datasetBars"></div>
|
| 115 |
</div>
|
| 116 |
</div>
|
| 117 |
|
| 118 |
<script>
|
| 119 |
const canvas = document.getElementById('c');
|
| 120 |
const ctx = canvas.getContext('2d');
|
|
|
|
| 121 |
|
| 122 |
+
function resizeCanvas() {
|
| 123 |
+
const w = canvas.clientWidth;
|
| 124 |
+
canvas.width = w * 2;
|
| 125 |
+
canvas.height = Math.round(w * 0.28) * 2;
|
| 126 |
+
canvas.style.height = Math.round(w * 0.28) + 'px';
|
| 127 |
+
}
|
| 128 |
+
resizeCanvas();
|
| 129 |
+
window.addEventListener('resize', resizeCanvas);
|
| 130 |
|
| 131 |
+
const TOKENS_PER_PAGE = 500;
|
| 132 |
+
const TOKENS_PER_BOOK = 125_000;
|
| 133 |
+
const TOKENS_PER_BOOKSHELF = 31_250_000;
|
| 134 |
+
const GPUS_PER_NODE = 8;
|
| 135 |
+
const GPUS_PER_RACK = 32;
|
| 136 |
+
const GPUS_PER_SUPERPOD = 256;
|
| 137 |
+
|
| 138 |
+
const MIN_GPUS = 1, MAX_GPUS = 1_000_000;
|
| 139 |
+
const LOG_MIN = Math.log(MIN_GPUS), LOG_MAX = Math.log(MAX_GPUS);
|
| 140 |
+
|
| 141 |
+
const DATASETS = [
|
| 142 |
+
{ name: 'BookCorpus', tokens: 1e9,
|
| 143 |
+
desc: '<b>BookCorpus</b> (2015)<br>11K unpublished books scraped from smashwords.com. Used to train the original BERT and GPT-1.' },
|
| 144 |
+
{ name: 'Wikipedia', tokens: 4.3e9,
|
| 145 |
+
desc: '<b>English Wikipedia</b><br>All articles from en.wikipedia.org. A staple ingredient in virtually every LLM pretraining mix.' },
|
| 146 |
+
{ name: 'C4', tokens: 156e9,
|
| 147 |
+
desc: '<b>C4</b> (Colossal Clean Crawled Corpus, 2020)<br>Cleaned version of Common Crawl used to train T5. Aggressive filtering removed 99% of the raw crawl.' },
|
| 148 |
+
{ name: 'The Pile', tokens: 330e9,
|
| 149 |
+
desc: '<b>The Pile</b> (EleutherAI, 2020)<br>22 diverse sources including PubMed, ArXiv, GitHub, StackExchange. Used to train GPT-Neo and GPT-J.' },
|
| 150 |
+
{ name: 'FinePhrase', tokens: 1e12,
|
| 151 |
+
desc: '<b>FinePhrase</b> (Hugging Face, 2026)<br>1T tokens of LLM-rephrased web text. Synthetic data that teaches small models to punch above their weight.' },
|
| 152 |
+
{ name: 'FineWeb', tokens: 15e12,
|
| 153 |
+
desc: '<b>FineWeb</b> (Hugging Face, 2024)<br>15T tokens of deduplicated, quality-filtered Common Crawl. The largest open pretraining dataset.' },
|
| 154 |
+
{ name: 'RedPajama', tokens: 100e12,
|
| 155 |
+
desc: '<b>RedPajama v2</b> (Together AI, 2023)<br>100T raw tokens from 84 Common Crawl snapshots with quality signals. Covers 5 languages.' },
|
| 156 |
+
{ name: 'Common Crawl', tokens: 3e15,
|
| 157 |
+
desc: '<b>Common Crawl</b> (ongoing since 2008)<br>The raw web archive. Petabytes of HTML from billions of pages. The upstream source for most web-text datasets.' },
|
| 158 |
+
{ name: 'The Internet', tokens: 100e15,
|
| 159 |
+
desc: '<b>The entire Internet</b> (estimate)<br>Rough estimate of all text ever published online. Nobody has actually tokenized it all.' },
|
| 160 |
+
];
|
| 161 |
+
|
| 162 |
+
let dsDone = DATASETS.map(() => false);
|
| 163 |
+
|
| 164 |
+
const TRAINING_RUNS = [
|
| 165 |
+
{ gpus: 8, name: 'BERT', row: 'a3',
|
| 166 |
+
desc: '<b>BERT</b> (Google, 2018)<br>16 TPU v3 chips. 340M params.<br>Trained on BooksCorpus + Wikipedia. Introduced masked language modeling. Changed NLP forever.' },
|
| 167 |
+
{ gpus: 32, name: 'GPT-2', row: 'a1',
|
| 168 |
+
desc: '<b>GPT-2</b> (OpenAI, 2019)<br>\u224832 V100 GPUs. 1.5B params.<br>"Too dangerous to release." Trained on 40 GB of internet text (WebText). Showed scaling up autoregressive LMs produces strong zero-shot results.' },
|
| 169 |
+
{ gpus: 2_048, name: 'Llama 1', row: 'a2',
|
| 170 |
+
desc: '<b>Llama 1</b> (Meta, 2023)<br>2,048 A100 GPUs. 65B params.<br>Trained on 1.4T tokens of public data only. Llama-13B outperformed GPT-3 (175B). Open-sourced and ignited the open LLM movement.' },
|
| 171 |
+
{ gpus: 2_788, name: 'DeepSeek', row: 'a3',
|
| 172 |
+
desc: '<b>DeepSeek V3</b> (DeepSeek, 2024)<br>2,048 H800 GPUs. 671B MoE params (37B active).<br>Only 2.8M GPU-hours using FP8 mixed precision. One of the most cost-efficient frontier model training runs ever (\u2248$5.6M).' },
|
| 173 |
+
{ gpus: 10_000, name: 'GPT-3', row: 'a1',
|
| 174 |
+
desc: '<b>GPT-3</b> (OpenAI, 2020)<br>10,000 V100 GPUs. 175B params.<br>Trained on 300B tokens. Demonstrated few-shot learning. Sparked the LLM revolution. Training cost estimated at $4.6M.' },
|
| 175 |
+
{ gpus: 16_384, name: 'Llama 3', row: 'a2',
|
| 176 |
+
desc: '<b>Llama 3</b> (Meta, 2024)<br>16,384 H100 GPUs. 405B params.<br>Trained on 15T tokens. Meta\u2019s largest open model. Used two 24K-GPU clusters with custom Tectonic filesystem for checkpointing.' },
|
| 177 |
+
{ gpus: 25_000, name: 'GPT-4', row: 'a3',
|
| 178 |
+
desc: '<b>GPT-4</b> (OpenAI, 2023, estimated)<br>\u224825K A100 GPUs. \u22481.8T MoE params.<br>Trained on \u224813T tokens over \u2248100 days. Estimated cost $63M. First GPT model to use mixture-of-experts (16 experts).' },
|
| 179 |
+
{ gpus: 50_000, name: 'GPT-5', row: 'a1',
|
| 180 |
+
desc: '<b>GPT-5</b> (OpenAI, 2025, estimated)<br>\u224850K H100-equiv GPUs (est.).<br>Used less training compute than GPT-4.5 due to focus on post-training scaling. Trained on Stargate infrastructure.' },
|
| 181 |
+
];
|
| 182 |
+
|
| 183 |
+
const INFRA_LANDMARKS = [
|
| 184 |
+
{ gpus: 1, name: '1 GPU', row: 'b1',
|
| 185 |
+
desc: '<b>NVIDIA H100 SXM</b><br>80 GB HBM3, 3.96 PFLOPS FP8.<br>The workhorse of modern AI training and inference.' },
|
| 186 |
+
{ gpus: 8, name: '1 node', row: 'b2',
|
| 187 |
+
desc: '<b>DGX H100</b> \u2014 8\u00d7H100 SXM<br>640 GB HBM3, NVLink 900 GB/s, 32 PFLOPS FP8.<br>NVIDIA\u2019s flagship AI server, fits in a single 10U chassis.' },
|
| 188 |
+
{ gpus: 32, name: '1 rack', row: 'b3',
|
| 189 |
+
desc: '<b>DGX SuperPOD rack</b> \u2014 4\u00d7DGX H100<br>32 GPUs, 2.5 TB HBM3, 40+ kW per rack.<br>The building block of enterprise AI clusters.' },
|
| 190 |
+
{ gpus: 256, name: 'SuperPOD', row: 'b1',
|
| 191 |
+
desc: '<b>DGX SuperPOD (1 SU)</b> \u2014 32 nodes, 256 GPUs<br>NDR400 InfiniBand, 256 PFLOPS FP8.<br>NVIDIA\u2019s reference architecture for large-scale AI.' },
|
| 192 |
+
{ gpus: 10_752, name: 'ALPS', row: 'b2',
|
| 193 |
+
desc: '<b>ALPS</b> \u2014 CSCS, Lugano, Switzerland<br>10,752 GH200 Grace-Hopper superchips, 270 PFLOPS.<br>#7 on TOP500. Used by Swiss AI Initiative to pre-train 70B-parameter LLMs.' },
|
| 194 |
+
{ gpus: 12_288, name: 'ByteDance', row: 'b3',
|
| 195 |
+
desc: '<b>ByteDance MegaScale</b><br>12,288 GPUs (A100/H800 mix).<br>Trained a 175B model at 55.2% MFU (1.34\u00d7 Megatron-LM). Published at NSDI \u201924. Full-stack optimization for 10K+ GPU training.' },
|
| 196 |
+
{ gpus: 64_000, name: 'Stargate', row: 'b1',
|
| 197 |
+
desc: '<b>Stargate</b> \u2014 OpenAI / Oracle, Abilene, TX<br>64K GB200 GPUs (planned end 2026), 1.2 GW.<br>$500B joint venture (OpenAI, Oracle, SoftBank, MGX). 875-acre campus, 8 AI factory buildings.' },
|
| 198 |
+
{ gpus: 100_000, name: 'Tencent', row: 'b2',
|
| 199 |
+
desc: '<b>Tencent Xingmai 2.0</b><br>100K GPUs (H800/A800 mix) in a single cluster.<br>60% comms efficiency gain over v1. 3.2 TB/s inter-server bandwidth. Supports training and fine-tuning at scale.' },
|
| 200 |
+
{ gpus: 200_000, name: 'Colossus', row: 'b3',
|
| 201 |
+
desc: '<b>Colossus</b> \u2014 xAI, Memphis, TN<br>200K H100/H200 GPUs, 250 MW.<br>Built in 122 days (vs 18\u201324 months typical). Powered by 35 gas turbines + 208 Tesla Megapacks.' },
|
| 202 |
+
{ gpus: 250_000, name: 'CoreWeave', row: 'b1',
|
| 203 |
+
desc: '<b>CoreWeave</b> \u2014 250K+ GPUs across 32 data centers<br>Mix of H100, H200, GB200, GB300.<br>Largest GPU-native cloud. IPO\u2019d 2025. Clients: OpenAI, Microsoft, Meta.' },
|
| 204 |
+
{ gpus: 600_000, name: 'Meta', row: 'b2',
|
| 205 |
+
desc: '<b>Meta AI fleet</b> \u2014 600K H100-equivalent GPUs<br>\u2248350K H100 + A100s. $12B+ GPU investment.<br>Organized into 24K-GPU clusters (RoCE + InfiniBand). Trained Llama 3 405B.' },
|
| 206 |
+
{ gpus: 800_000, name: 'Rainier', row: 'b3',
|
| 207 |
+
desc: '<b>Project Rainier</b> \u2014 AWS / Anthropic<br>500K Trainium 2 chips (\u2248800K H100-equiv).<br>$8B across 30 data centers in Indiana. Scaling to 1M chips. Used to train Claude.' },
|
| 208 |
+
{ gpus: 1_000_000, name: 'Colossus 2', row: 'b1',
|
| 209 |
+
desc: '<b>Colossus 2</b> \u2014 xAI (planned)<br>1M+ H100-equivalent GPUs, 2 GW power.<br>$20B Series E from NVIDIA, Cisco, and others. Expanding across Memphis-area facilities.' },
|
| 210 |
+
];
|
| 211 |
+
|
| 212 |
+
function gpusToSlider(gpus) { return (Math.log(Math.max(gpus, 1)) - LOG_MIN) / (LOG_MAX - LOG_MIN); }
|
| 213 |
+
function sliderToGpus(val) { return Math.round(Math.exp(LOG_MIN + val * (LOG_MAX - LOG_MIN))); }
|
| 214 |
+
|
| 215 |
+
// Audio: short tick sound via Web Audio API
|
| 216 |
+
let audioCtx = null;
|
| 217 |
+
function playTick() {
|
| 218 |
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
| 219 |
+
const osc = audioCtx.createOscillator();
|
| 220 |
+
const gain = audioCtx.createGain();
|
| 221 |
+
osc.connect(gain);
|
| 222 |
+
gain.connect(audioCtx.destination);
|
| 223 |
+
osc.frequency.value = 880;
|
| 224 |
+
osc.type = 'sine';
|
| 225 |
+
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
| 226 |
+
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.06);
|
| 227 |
+
osc.start(audioCtx.currentTime);
|
| 228 |
+
osc.stop(audioCtx.currentTime + 0.06);
|
| 229 |
}
|
| 230 |
|
| 231 |
const modelSelect = document.getElementById('model');
|
| 232 |
const gpuSlider = document.getElementById('gpus');
|
|
|
|
| 233 |
const ppsEl = document.getElementById('pps');
|
| 234 |
const ppsLabelEl = document.getElementById('ppsLabel');
|
| 235 |
const tpsEl = document.getElementById('tps');
|
| 236 |
+
const totalTokensEl = document.getElementById('totalTokens');
|
| 237 |
const totalEl = document.getElementById('total');
|
| 238 |
const totalLabelEl = document.getElementById('totalLabel');
|
| 239 |
|
| 240 |
+
let floatingItems = [], totalTokens = 0, spawnAccum = 0, lastTime = performance.now(), lastMetricUpdate = 0;
|
| 241 |
|
| 242 |
+
function getGpuCount() { return sliderToGpus(parseFloat(gpuSlider.value)); }
|
| 243 |
+
function getTps() { return parseInt(modelSelect.value) * getGpuCount(); }
|
| 244 |
+
function getPps() { return getTps() / TOKENS_PER_PAGE; }
|
|
|
|
| 245 |
|
| 246 |
+
function updateSliderGradient() {
|
| 247 |
+
const val = parseFloat(gpuSlider.value) * 100;
|
| 248 |
+
gpuSlider.style.background = 'linear-gradient(to right, #2e5f7e 0%, #c0392b ' + (val * 0.5) + '%, #c4a020 ' + val + '%, #ddd ' + val + '%)';
|
| 249 |
|
| 250 |
+
}
|
| 251 |
+
updateSliderGradient();
|
| 252 |
+
gpuSlider.addEventListener('input', updateSliderGradient);
|
| 253 |
+
|
| 254 |
+
function formatGpuLabel(gpus) {
|
| 255 |
+
if (gpus < GPUS_PER_NODE) return gpus + ' GPU' + (gpus > 1 ? 's' : '');
|
| 256 |
+
const nodes = Math.round(gpus / GPUS_PER_NODE);
|
| 257 |
+
const gpuStr = gpus >= 1000 ? (gpus / 1000).toFixed(gpus >= 10000 ? 0 : 1) + 'K' : gpus.toLocaleString();
|
| 258 |
+
if (nodes === 1) return '1 node (' + gpuStr + ' GPUs)';
|
| 259 |
+
const nodeStr = nodes >= 1000 ? (nodes / 1000).toFixed(nodes >= 10000 ? 0 : 1) + 'K' : nodes.toLocaleString();
|
| 260 |
+
return nodeStr + ' nodes (' + gpuStr + ' GPUs)';
|
| 261 |
}
|
| 262 |
|
| 263 |
+
const rowEls = { a3: document.getElementById('row-a3'), a2: document.getElementById('row-a2'), a1: document.getElementById('row-a1'),
|
| 264 |
+
b1: document.getElementById('row-b1'), b2: document.getElementById('row-b2'), b3: document.getElementById('row-b3') };
|
| 265 |
+
|
| 266 |
+
function addLandmark(lm) {
|
| 267 |
+
const pct = gpusToSlider(lm.gpus) * 100;
|
| 268 |
+
const isAbove = lm.row.startsWith('a');
|
| 269 |
+
const el = document.createElement('div');
|
| 270 |
+
el.className = 'landmark';
|
| 271 |
+
el.style.left = pct + '%';
|
| 272 |
+
let tipClass = 'tooltip';
|
| 273 |
+
let tipStyle = '';
|
| 274 |
+
if (pct > 80) { tipClass += ' tip-right'; tipStyle = 'left:auto;right:0;transform:none;'; }
|
| 275 |
+
else if (pct < 20) { tipClass += ' tip-left'; tipStyle = 'left:0;transform:none;'; }
|
| 276 |
+
const tip = '<div class="' + tipClass + '" style="' + tipStyle + '">' + lm.desc + '</div>';
|
| 277 |
+
el.innerHTML = isAbove
|
| 278 |
+
? '<div class="name">' + lm.name + '</div><div class="tick"></div>' + tip
|
| 279 |
+
: '<div class="tick"></div><div class="name">' + lm.name + '</div>' + tip;
|
| 280 |
+
el.addEventListener('click', () => { gpuSlider.value = gpusToSlider(lm.gpus); updateSliderGradient(); resetCountdown(); });
|
| 281 |
+
rowEls[lm.row].appendChild(el);
|
| 282 |
}
|
| 283 |
+
TRAINING_RUNS.forEach(addLandmark);
|
| 284 |
+
INFRA_LANDMARKS.forEach(addLandmark);
|
| 285 |
+
|
| 286 |
+
const datasetBarsEl = document.getElementById('datasetBars');
|
| 287 |
+
const dsEls = DATASETS.map(ds => {
|
| 288 |
+
const el = document.createElement('div');
|
| 289 |
+
el.className = 'dataset-item';
|
| 290 |
+
el.innerHTML = '<span class="ds-check">\u23f3</span><span class="ds-name">' + ds.name + '</span><span class="ds-time">-</span><span class="ds-size">(' + formatNumInt(ds.tokens) + ' tok)</span><div class="ds-tip">' + ds.desc + '</div>';
|
| 291 |
+
datasetBarsEl.appendChild(el);
|
| 292 |
+
return el;
|
| 293 |
+
});
|
| 294 |
|
| 295 |
+
function resetCountdown() {
|
| 296 |
+
dsDone = DATASETS.map(() => false);
|
| 297 |
+
totalTokens = 0;
|
| 298 |
+
floatingItems = [];
|
| 299 |
+
spawnAccum = 0;
|
| 300 |
+
dsEls.forEach(el => { el.classList.remove('done'); el.querySelector('.ds-check').textContent = '\u23f3'; el.querySelector('.ds-time').textContent = '-'; });
|
| 301 |
}
|
| 302 |
|
| 303 |
+
gpuSlider.addEventListener('input', () => { resetCountdown(); });
|
| 304 |
+
modelSelect.addEventListener('change', resetCountdown);
|
|
|
|
| 305 |
|
| 306 |
+
function formatNum(n) {
|
| 307 |
+
if (n >= 1e15) return (n / 1e15).toFixed(1) + 'Q';
|
| 308 |
+
if (n >= 1e12) return (n / 1e12).toFixed(1) + 'T';
|
| 309 |
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
|
| 310 |
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
| 311 |
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
| 312 |
+
return Math.round(n).toString();
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
function formatNumInt(n) {
|
| 316 |
+
const fmt = (v, s) => (v % 1 === 0 ? v.toFixed(0) : v.toFixed(1)) + s;
|
| 317 |
+
if (n >= 1e15) return fmt(n / 1e15, 'Q');
|
| 318 |
+
if (n >= 1e12) return fmt(n / 1e12, 'T');
|
| 319 |
+
if (n >= 1e9) return fmt(n / 1e9, 'B');
|
| 320 |
+
if (n >= 1e6) return fmt(n / 1e6, 'M');
|
| 321 |
+
if (n >= 1e3) return fmt(n / 1e3, 'K');
|
| 322 |
+
return Math.round(n).toString();
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
function formatDuration(seconds) {
|
| 326 |
+
if (!isFinite(seconds) || seconds < 0) return '\u221e';
|
| 327 |
+
if (seconds < 1) return '<1s';
|
| 328 |
+
if (seconds < 60) return Math.round(seconds) + 's';
|
| 329 |
+
if (seconds < 3600) return Math.round(seconds / 60) + 'm';
|
| 330 |
+
if (seconds < 86400) return (seconds / 3600).toFixed(1) + 'h';
|
| 331 |
+
if (seconds < 86400 * 365) return (seconds / 86400).toFixed(1) + 'd';
|
| 332 |
+
const years = seconds / (86400 * 365);
|
| 333 |
+
if (years < 1000) return years.toFixed(1) + 'y';
|
| 334 |
+
if (years < 1e6) return (years / 1e3).toFixed(1) + 'Ky';
|
| 335 |
+
if (years < 1e9) return (years / 1e6).toFixed(1) + 'My';
|
| 336 |
+
return (years / 1e9).toFixed(1) + 'By';
|
| 337 |
+
}
|
| 338 |
|
| 339 |
+
function getW() { return canvas.width; }
|
| 340 |
+
function getH() { return canvas.height; }
|
| 341 |
+
|
| 342 |
+
// Hardware display: GPU β node β rack β superpod β cluster grid
|
| 343 |
+
function getHardwareLevel(gpus) {
|
| 344 |
+
if (gpus < GPUS_PER_NODE) return 'gpu';
|
| 345 |
+
if (gpus < GPUS_PER_RACK) return 'node';
|
| 346 |
+
if (gpus < GPUS_PER_SUPERPOD) return 'rack';
|
| 347 |
+
return 'cluster';
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
function drawHardware() {
|
| 351 |
+
const W = getW(), H = getH();
|
| 352 |
+
const gpus = getGpuCount();
|
| 353 |
+
const labelH = 40;
|
| 354 |
+
const aw = W * 0.32, ah = H - 20 - labelH, ax = 10, ay = 10 + labelH;
|
| 355 |
+
const level = getHardwareLevel(gpus);
|
| 356 |
+
|
| 357 |
+
// GPU count label above hardware
|
| 358 |
+
ctx.fillStyle = '#2e5f7e';
|
| 359 |
+
ctx.font = 'bold 32px system-ui, sans-serif';
|
| 360 |
+
ctx.textAlign = 'center';
|
| 361 |
+
ctx.textBaseline = 'top';
|
| 362 |
+
ctx.fillText(formatGpuLabel(gpus), ax + aw / 2, 8);
|
| 363 |
+
ctx.textAlign = 'start'; ctx.textBaseline = 'alphabetic';
|
| 364 |
+
|
| 365 |
+
if (level === 'gpu') {
|
| 366 |
+
drawGpuGrid(gpus, ax, ay, aw, ah);
|
| 367 |
+
} else if (level === 'node') {
|
| 368 |
+
const nodeCount = Math.max(1, Math.round(gpus / GPUS_PER_NODE));
|
| 369 |
+
drawNodeGrid(nodeCount, ax, ay, aw, ah);
|
| 370 |
+
} else if (level === 'rack') {
|
| 371 |
+
const rackCount = Math.max(1, Math.round(gpus / GPUS_PER_RACK));
|
| 372 |
+
drawRackGrid(rackCount, ax, ay, aw, ah);
|
| 373 |
+
} else {
|
| 374 |
+
const pods = Math.round(gpus / GPUS_PER_SUPERPOD);
|
| 375 |
+
drawPodGrid(pods, ax, ay, aw, ah);
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
function drawGpuGrid(count, ax, ay, aw, ah) {
|
| 380 |
let cols, rows;
|
| 381 |
if (count === 1) { cols = 1; rows = 1; }
|
| 382 |
else if (count === 2) { cols = 2; rows = 1; }
|
| 383 |
else if (count <= 4) { cols = 2; rows = 2; }
|
| 384 |
+
else { cols = 4; rows = 2; }
|
| 385 |
+
const gap = 3;
|
| 386 |
+
const gw = Math.min((aw - gap * (cols - 1)) / cols, ((ah - gap * (rows - 1)) / rows) * 0.6);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
const gh = gw / 0.6;
|
| 388 |
+
const offX = ax + (aw - (cols * gw + (cols - 1) * gap)) / 2;
|
| 389 |
+
const offY = ay + (ah - (rows * gh + (rows - 1) * gap)) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
const fanSpeed = (performance.now() / 200) * Math.min(getPps(), 100);
|
| 391 |
+
for (let i = 0; i < count; i++) {
|
| 392 |
+
const c = i % cols, r = Math.floor(i / cols);
|
| 393 |
+
drawSingleGpu(offX + c * (gw + gap), offY + r * (gh + gap), gw, gh, fanSpeed + i * 0.5);
|
| 394 |
+
}
|
| 395 |
+
}
|
| 396 |
|
| 397 |
+
function drawSingleGpu(x, y, gw, gh, fanPhase) {
|
| 398 |
+
ctx.fillStyle = '#2a2a2a'; ctx.fillRect(x, y, gw, gh);
|
| 399 |
+
ctx.strokeStyle = '#000'; ctx.lineWidth = 1.5; ctx.strokeRect(x, y, gw, gh);
|
| 400 |
+
if (gw < 12) { ctx.fillStyle = '#1a1a1a'; ctx.fillRect(x + gw * 0.15, y + gw * 0.08, gw * 0.7, gw * 0.7); return; }
|
| 401 |
+
const fs = gw * 0.7, fx = x + (gw - fs) / 2, fy = y + gw * 0.08;
|
| 402 |
+
ctx.fillStyle = '#1a1a1a'; ctx.fillRect(fx, fy, fs, fs);
|
| 403 |
+
const cx = fx + fs / 2, cy = fy + fs / 2, fr = fs / 2 - 2;
|
| 404 |
+
ctx.beginPath(); ctx.arc(cx, cy, fr, 0, Math.PI * 2); ctx.fillStyle = '#333'; ctx.fill();
|
| 405 |
+
if (gw >= 20) {
|
| 406 |
+
const br = fr - 2; ctx.save(); ctx.translate(cx, cy); ctx.rotate(fanPhase);
|
| 407 |
+
const n = gw >= 40 ? 7 : 5;
|
| 408 |
+
for (let b = 0; b < n; b++) {
|
| 409 |
+
ctx.rotate(Math.PI * 2 / n); ctx.beginPath(); ctx.moveTo(0, 0);
|
| 410 |
+
ctx.quadraticCurveTo(br * 0.5, br * 0.3, br * 0.85, 0);
|
| 411 |
+
ctx.quadraticCurveTo(br * 0.5, -br * 0.3, 0, 0);
|
| 412 |
+
ctx.fillStyle = '#555'; ctx.fill();
|
| 413 |
+
}
|
| 414 |
+
ctx.restore();
|
| 415 |
+
}
|
| 416 |
+
const hy = fy + fs + 2, hh = gh - (hy - y) - gw * 0.15;
|
| 417 |
+
if (hh > 4) { const lh = Math.max(1, Math.min(4, hh / 6)), lg = lh * 1.5;
|
| 418 |
+
for (let i = 0; i * lg < hh; i++) { ctx.fillStyle = i % 2 === 0 ? '#444' : '#383838'; ctx.fillRect(x + gw * 0.1, hy + i * lg, gw * 0.8, lh); } }
|
| 419 |
+
if (gw >= 30) { ctx.fillStyle = '#c4a020'; const pw = Math.max(1, gw * 0.06), pc = Math.floor(gw * 0.7 / (pw * 2)), ps = x + (gw - pc * pw * 2) / 2;
|
| 420 |
+
for (let p = 0; p < pc; p++) ctx.fillRect(ps + p * pw * 2, y + gh, pw, Math.max(2, gw * 0.08)); }
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
// Grid of DGX nodes
|
| 424 |
+
function drawNodeGrid(count, ax, ay, aw, ah) {
|
| 425 |
+
const gap = 6;
|
| 426 |
+
const cols = count <= 2 ? 1 : 2;
|
| 427 |
+
const rows = Math.ceil(count / cols);
|
| 428 |
+
const nw = (aw - gap * (cols - 1)) / cols;
|
| 429 |
+
const nh = Math.min((ah - gap * (rows - 1)) / rows, aw * 0.35);
|
| 430 |
+
const tw = cols * nw + (cols - 1) * gap;
|
| 431 |
+
const th = rows * nh + (rows - 1) * gap;
|
| 432 |
+
const ox = ax + (aw - tw) / 2, oy = ay + (ah - th) / 2;
|
| 433 |
for (let i = 0; i < count; i++) {
|
| 434 |
+
const c = i % cols, r = Math.floor(i / cols);
|
| 435 |
+
drawSingleNode(ox + c * (nw + gap), oy + r * (nh + gap), nw, nh);
|
| 436 |
+
}
|
| 437 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
+
function drawSingleNode(nx, ny, nw, nh) {
|
| 440 |
+
ctx.fillStyle = '#1a1a2e';
|
| 441 |
+
ctx.beginPath(); ctx.roundRect(nx, ny, nw, nh, 4); ctx.fill();
|
| 442 |
+
ctx.strokeStyle = '#555'; ctx.lineWidth = 2;
|
| 443 |
+
ctx.beginPath(); ctx.roundRect(nx, ny, nw, nh, 4); ctx.stroke();
|
| 444 |
+
const slotW = (nw - 20) / 8, slotH = nh * 0.6;
|
| 445 |
+
const slotY = ny + (nh - slotH) / 2;
|
| 446 |
+
const fanPhase = (performance.now() / 200) * Math.min(getPps(), 100);
|
| 447 |
+
for (let i = 0; i < 8; i++) {
|
| 448 |
+
const sx = nx + 10 + i * slotW;
|
| 449 |
+
drawSingleGpu(sx + 1, slotY, slotW - 2, slotH, fanPhase + i * 0.5);
|
| 450 |
+
}
|
| 451 |
+
ctx.fillStyle = '#4ade80';
|
| 452 |
+
ctx.beginPath(); ctx.arc(nx + 8, ny + 8, 3, 0, Math.PI * 2); ctx.fill();
|
| 453 |
+
ctx.fillStyle = '#60a5fa';
|
| 454 |
+
ctx.beginPath(); ctx.arc(nx + 18, ny + 8, 3, 0, Math.PI * 2); ctx.fill();
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
// Grid of racks
|
| 458 |
+
function drawRackGrid(count, ax, ay, aw, ah) {
|
| 459 |
+
const gap = 4;
|
| 460 |
+
const cols = count <= 3 ? count : Math.min(4, Math.ceil(Math.sqrt(count)));
|
| 461 |
+
const rows = Math.ceil(count / cols);
|
| 462 |
+
const rw = (aw - gap * (cols - 1)) / cols;
|
| 463 |
+
const rh = Math.min((ah - gap * (rows - 1)) / rows, ah * 0.95);
|
| 464 |
+
const tw = cols * rw + (cols - 1) * gap;
|
| 465 |
+
const th = rows * rh + (rows - 1) * gap;
|
| 466 |
+
const ox = ax + (aw - tw) / 2, oy = ay + (ah - th) / 2;
|
| 467 |
+
for (let i = 0; i < count; i++) {
|
| 468 |
+
const c = i % cols, r = Math.floor(i / cols);
|
| 469 |
+
drawSingleRack(ox + c * (rw + gap), oy + r * (rh + gap), rw, rh);
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
|
| 473 |
+
function drawSingleRack(rx, ry, rw, rh) {
|
| 474 |
+
ctx.fillStyle = '#111'; ctx.beginPath(); ctx.roundRect(rx, ry, rw, rh, 4); ctx.fill();
|
| 475 |
+
ctx.strokeStyle = '#555'; ctx.lineWidth = 2;
|
| 476 |
+
ctx.beginPath(); ctx.roundRect(rx, ry, rw, rh, 4); ctx.stroke();
|
| 477 |
+
const nodeCount = 4, pad = 4, gap = 3;
|
| 478 |
+
const nodeH = (rh - 2 * pad - (nodeCount - 1) * gap) / nodeCount;
|
| 479 |
+
const nodeW = rw - 2 * pad;
|
| 480 |
+
const fanPhase = (performance.now() / 200) * Math.min(getPps(), 100);
|
| 481 |
+
for (let n = 0; n < nodeCount; n++) {
|
| 482 |
+
const ny = ry + pad + n * (nodeH + gap), nx = rx + pad;
|
| 483 |
+
ctx.fillStyle = '#1a1a2e';
|
| 484 |
+
ctx.beginPath(); ctx.roundRect(nx, ny, nodeW, nodeH, 2); ctx.fill();
|
| 485 |
+
ctx.strokeStyle = '#444'; ctx.lineWidth = 1;
|
| 486 |
+
ctx.beginPath(); ctx.roundRect(nx, ny, nodeW, nodeH, 2); ctx.stroke();
|
| 487 |
+
const slotCount = 8, slotW = (nodeW - 6) / slotCount;
|
| 488 |
+
for (let g = 0; g < slotCount; g++) {
|
| 489 |
+
const gx = nx + 3 + g * slotW, gy = ny + 2;
|
| 490 |
+
ctx.fillStyle = '#2a2a2a'; ctx.fillRect(gx, gy, slotW - 1, nodeH - 4);
|
| 491 |
+
const fcx = gx + (slotW - 1) / 2, fcy = gy + (nodeH - 4) * 0.35;
|
| 492 |
+
const fr = Math.min((slotW - 1) * 0.35, (nodeH - 4) * 0.25);
|
| 493 |
+
if (fr > 1) {
|
| 494 |
+
ctx.beginPath(); ctx.arc(fcx, fcy, fr, 0, Math.PI * 2); ctx.fillStyle = '#444'; ctx.fill();
|
| 495 |
+
ctx.save(); ctx.translate(fcx, fcy); ctx.rotate(fanPhase + g * 0.3 + n);
|
| 496 |
+
for (let b = 0; b < 4; b++) { ctx.rotate(Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, 0);
|
| 497 |
+
ctx.lineTo(fr * 0.8, fr * 0.3); ctx.lineTo(fr * 0.8, -fr * 0.3); ctx.fillStyle = '#666'; ctx.fill(); }
|
| 498 |
+
ctx.restore();
|
| 499 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
}
|
| 501 |
+
ctx.fillStyle = '#4ade80';
|
| 502 |
+
ctx.beginPath(); ctx.arc(nx + nodeW - 6, ny + nodeH / 2, Math.min(2, nodeH * 0.15), 0, Math.PI * 2); ctx.fill();
|
| 503 |
}
|
| 504 |
}
|
| 505 |
|
| 506 |
+
// SuperPod grid: draw abstract pods
|
| 507 |
+
function drawPodGrid(pods, ax, ay, aw, ah) {
|
| 508 |
+
const vn = Math.min(pods, 1024);
|
| 509 |
+
let cols, rows;
|
| 510 |
+
if (vn <= 2) { cols = 1; rows = vn; } else if (vn <= 4) { cols = 2; rows = 2; }
|
| 511 |
+
else if (vn <= 8) { cols = 2; rows = 4; } else if (vn <= 16) { cols = 4; rows = 4; }
|
| 512 |
+
else if (vn <= 32) { cols = 4; rows = 8; } else if (vn <= 64) { cols = 8; rows = 8; }
|
| 513 |
+
else if (vn <= 128) { cols = 8; rows = 16; } else if (vn <= 256) { cols = 16; rows = 16; }
|
| 514 |
+
else if (vn <= 512) { cols = 16; rows = 32; } else { cols = 32; rows = 32; }
|
| 515 |
+
const gap = vn <= 16 ? 3 : vn <= 64 ? 2 : 1;
|
| 516 |
+
const cw = (aw - gap * (cols - 1)) / cols, ch = (ah - gap * (rows - 1)) / rows;
|
| 517 |
+
const tw = cols * cw + (cols - 1) * gap, th = rows * ch + (rows - 1) * gap;
|
| 518 |
+
const ox = ax + (aw - tw) / 2, oy = ay + (ah - th) / 2;
|
| 519 |
+
for (let i = 0; i < vn; i++) {
|
| 520 |
+
const c = i % cols, r = Math.floor(i / cols);
|
| 521 |
+
drawPodUnit(ox + c * (cw + gap), oy + r * (ch + gap), cw, ch, vn);
|
| 522 |
+
}
|
| 523 |
+
if (pods > vn) {
|
| 524 |
+
ctx.fillStyle = 'rgba(0,0,0,0.7)';
|
| 525 |
+
const lh = 24, lw = aw - 20, lx = ax + (aw - lw) / 2, ly = ay + ah - lh - 5;
|
| 526 |
+
ctx.fillRect(lx, ly, lw, lh); ctx.fillStyle = '#fff';
|
| 527 |
+
ctx.font = 'bold 12px system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
| 528 |
+
const ns = pods >= 1000 ? (pods / 1000).toFixed(pods >= 10000 ? 0 : 1) + 'K' : pods.toLocaleString();
|
| 529 |
+
ctx.fillText(ns + ' SuperPODs', lx + lw / 2, ly + lh / 2);
|
| 530 |
+
ctx.textAlign = 'start'; ctx.textBaseline = 'alphabetic';
|
| 531 |
+
}
|
| 532 |
+
}
|
| 533 |
|
| 534 |
+
function drawPodUnit(x, y, nw, nh, total) {
|
| 535 |
+
const r = Math.min(2, nw * 0.1);
|
| 536 |
+
ctx.fillStyle = '#1a1a2e'; ctx.beginPath(); ctx.roundRect(x, y, nw, nh, r); ctx.fill();
|
| 537 |
+
ctx.strokeStyle = '#444'; ctx.lineWidth = total <= 16 ? 1 : 0.5;
|
| 538 |
+
ctx.beginPath(); ctx.roundRect(x, y, nw, nh, r); ctx.stroke();
|
| 539 |
+
if (nw < 4) return;
|
| 540 |
+
const lr = Math.max(0.8, Math.min(2, nw * 0.06)), ly = y + nh * 0.2;
|
| 541 |
+
ctx.fillStyle = '#4ade80'; ctx.beginPath(); ctx.arc(x + nw * 0.2, ly, lr, 0, Math.PI * 2); ctx.fill();
|
| 542 |
+
if (nw >= 8) { ctx.fillStyle = '#60a5fa'; ctx.beginPath(); ctx.arc(x + nw * 0.4, ly, lr, 0, Math.PI * 2); ctx.fill(); }
|
| 543 |
+
if (nw >= 6) { ctx.fillStyle = '#c4a020'; ctx.beginPath(); ctx.arc(x + nw * 0.6, ly, lr, 0, Math.PI * 2); ctx.fill(); }
|
| 544 |
+
if (nw >= 10) {
|
| 545 |
+
const vy = y + nh * 0.45, vh = nh * 0.4, lc = Math.min(6, Math.floor(nw / 4));
|
| 546 |
+
ctx.strokeStyle = '#333'; ctx.lineWidth = 0.5;
|
| 547 |
+
for (let i = 0; i < lc; i++) { const lx = x + nw * 0.2 + (nw * 0.6) * i / lc; ctx.beginPath(); ctx.moveTo(lx, vy); ctx.lineTo(lx, vy + vh); ctx.stroke(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
}
|
| 549 |
+
}
|
| 550 |
|
| 551 |
+
function getVisualMode() {
|
| 552 |
+
const bps = getPps() / 250;
|
| 553 |
+
if (bps >= 250) return 'shelf';
|
| 554 |
+
if (bps >= 1) return 'book';
|
| 555 |
+
return 'page';
|
| 556 |
}
|
| 557 |
|
| 558 |
+
const EMOJI = { page: '\u{1F4C4}', book: '\u{1F4D6}', shelf: '\u{1F4DA}' };
|
| 559 |
+
const EMOJI_SIZE = { page: 28, book: 34, shelf: 40 };
|
| 560 |
+
|
| 561 |
+
function drawItem(p) {
|
| 562 |
+
const W = getW();
|
| 563 |
+
const fadeStart = W - W * 0.1;
|
| 564 |
+
const alpha = p.x > fadeStart ? 1 - (p.x - fadeStart) / (W - fadeStart) : 1;
|
| 565 |
+
ctx.globalAlpha = alpha;
|
| 566 |
+
const sz = EMOJI_SIZE[p.type];
|
| 567 |
+
ctx.font = sz + 'px system-ui, sans-serif';
|
| 568 |
+
ctx.textBaseline = 'top';
|
| 569 |
+
ctx.fillText(EMOJI[p.type], p.x, p.y);
|
| 570 |
+
ctx.globalAlpha = 1;
|
| 571 |
}
|
| 572 |
|
| 573 |
function spawnItem() {
|
| 574 |
+
const H = getH(), W = getW();
|
| 575 |
+
const spawnX = W * 0.34;
|
| 576 |
+
const yMin = 30, yMax = H - 60, y = yMin + Math.random() * (yMax - yMin);
|
| 577 |
+
const mode = getVisualMode();
|
| 578 |
+
const pps = getPps();
|
| 579 |
+
const rate = mode === 'shelf' ? pps / 62500 : mode === 'book' ? pps / 250 : pps;
|
| 580 |
+
const bs = 0.8 + Math.log10(Math.max(1, rate)) * 0.8;
|
| 581 |
+
const tokens = mode === 'shelf' ? TOKENS_PER_BOOKSHELF : mode === 'book' ? TOKENS_PER_BOOK : TOKENS_PER_PAGE;
|
| 582 |
+
floatingItems.push({
|
| 583 |
+
x: spawnX + Math.random() * 20, y, speed: bs + Math.random() * bs,
|
| 584 |
+
type: mode, tokens
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
});
|
|
|
|
| 586 |
}
|
| 587 |
|
| 588 |
+
// Throttle sound: max one tick per 50ms
|
| 589 |
+
let lastTickTime = 0;
|
| 590 |
+
function maybePlayTick() {
|
| 591 |
+
const now = performance.now();
|
| 592 |
+
if (now - lastTickTime > 50) { lastTickTime = now; playTick(); }
|
| 593 |
}
|
| 594 |
|
| 595 |
function frame(now) {
|
| 596 |
+
const dt = Math.min((now - lastTime) / 1000, 0.1); lastTime = now;
|
| 597 |
+
const W = getW(), H = getH();
|
| 598 |
|
| 599 |
ctx.clearRect(0, 0, W, H);
|
| 600 |
+
ctx.fillStyle = '#fafafa'; ctx.fillRect(0, 0, W, H);
|
| 601 |
|
| 602 |
+
const spawnX = W * 0.34;
|
| 603 |
+
ctx.strokeStyle = '#e0e0e0'; ctx.lineWidth = 1; ctx.setLineDash([4, 8]);
|
| 604 |
+
for (let y = 60; y < H - 30; y += 40) { ctx.beginPath(); ctx.moveTo(spawnX, y); ctx.lineTo(W - 20, y); ctx.stroke(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
ctx.setLineDash([]);
|
| 606 |
|
| 607 |
+
const pps = getPps(), mode = getVisualMode();
|
| 608 |
+
const spawnRate = mode === 'shelf' ? Math.min(pps / 62500, 60) : mode === 'book' ? Math.min(pps / 250, 120) : Math.min(pps, 400);
|
|
|
|
|
|
|
|
|
|
| 609 |
spawnAccum += spawnRate * dt;
|
| 610 |
+
while (spawnAccum >= 1) { spawnItem(); spawnAccum -= 1; }
|
| 611 |
+
|
| 612 |
+
for (let i = floatingItems.length - 1; i >= 0; i--) {
|
| 613 |
+
floatingItems[i].x += floatingItems[i].speed;
|
| 614 |
+
if (floatingItems[i].x > W + 40) {
|
| 615 |
+
// Item exited: count its tokens and play sound
|
| 616 |
+
totalTokens += floatingItems[i].tokens;
|
| 617 |
+
maybePlayTick();
|
| 618 |
+
floatingItems.splice(i, 1);
|
|
|
|
| 619 |
continue;
|
| 620 |
}
|
| 621 |
+
drawItem(floatingItems[i]);
|
| 622 |
}
|
| 623 |
|
| 624 |
+
drawHardware();
|
| 625 |
+
|
| 626 |
+
// Throttle DOM metric updates to ~4 Hz to prevent flickering
|
| 627 |
+
if (now - lastMetricUpdate > 100) {
|
| 628 |
+
lastMetricUpdate = now;
|
| 629 |
+
const tps = getTps();
|
| 630 |
+
const bps = pps / 250;
|
| 631 |
+
const sps = bps / 250;
|
| 632 |
+
if (sps >= 1) { ppsEl.textContent = formatNum(sps); ppsLabelEl.textContent = 'π Shelves / second'; }
|
| 633 |
+
else if (bps >= 1) { ppsEl.textContent = formatNum(bps); ppsLabelEl.textContent = 'π Books / second'; }
|
| 634 |
+
else { ppsEl.textContent = formatNum(pps); ppsLabelEl.textContent = 'π Pages / second'; }
|
| 635 |
+
tpsEl.textContent = formatNum(tps);
|
| 636 |
+
totalTokensEl.textContent = formatNum(totalTokens);
|
| 637 |
+
const totalShelves = totalTokens / TOKENS_PER_BOOKSHELF;
|
| 638 |
+
const totalBooks = totalTokens / TOKENS_PER_BOOK;
|
| 639 |
+
const totalPages = totalTokens / TOKENS_PER_PAGE;
|
| 640 |
+
if (totalShelves >= 1) { totalEl.textContent = formatNum(totalShelves); totalLabelEl.textContent = 'π Bookshelves generated'; }
|
| 641 |
+
else if (totalBooks >= 1) { totalEl.textContent = formatNum(totalBooks); totalLabelEl.textContent = 'π Books generated'; }
|
| 642 |
+
else { totalEl.textContent = formatNum(totalPages); totalLabelEl.textContent = 'π Pages generated'; }
|
| 643 |
+
|
| 644 |
+
for (let i = 0; i < DATASETS.length; i++) {
|
| 645 |
+
if (dsDone[i]) continue;
|
| 646 |
+
const remaining = DATASETS[i].tokens - totalTokens;
|
| 647 |
+
if (remaining <= 0) {
|
| 648 |
+
dsDone[i] = true;
|
| 649 |
+
dsEls[i].classList.add('done');
|
| 650 |
+
dsEls[i].querySelector('.ds-check').textContent = '\u2705';
|
| 651 |
+
dsEls[i].querySelector('.ds-time').textContent = 'done';
|
| 652 |
+
} else {
|
| 653 |
+
dsEls[i].querySelector('.ds-time').textContent = formatDuration(remaining / tps);
|
| 654 |
+
}
|
| 655 |
+
}
|
| 656 |
}
|
| 657 |
|
| 658 |
requestAnimationFrame(frame);
|
| 659 |
}
|
| 660 |
|
|
|
|
| 661 |
requestAnimationFrame(frame);
|
| 662 |
</script>
|
| 663 |
</body>
|