joelniklaus HF Staff commited on
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 (200 pages each).
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 each) or books (200 pages each) are generated."
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>Demo 6: 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: 30px; }
9
- .subtitle { color: #666; margin-bottom: 20px; font-size: 14px; }
10
- .controls { display: flex; gap: 24px; align-items: flex-end; margin-bottom: 16px; flex-wrap: wrap; justify-content: center; }
11
- .control-group { display: flex; flex-direction: column; gap: 4px; }
12
- .control-group label { font-size: 12px; font-weight: 600; color: #444; text-transform: uppercase; letter-spacing: 0.5px; }
13
- select, input[type=range] { font-size: 14px; font-family: inherit; }
14
- select { padding: 6px 10px; border: 2px solid #000; background: #fff; cursor: pointer; }
15
- input[type=range] { width: 200px; accent-color: #2e5f7e; }
16
- .gpu-count { font-size: 14px; font-weight: 700; color: #2e5f7e; min-width: 60px; text-align: right; font-variant-numeric: tabular-nums; }
17
- canvas { border: 2px solid #000; background: #fafafa; }
18
- .metrics { display: flex; gap: 32px; margin-top: 14px; flex-wrap: wrap; justify-content: center; }
19
- .metric { text-align: center; }
20
- .metric .value { font-size: 28px; font-weight: 700; color: #2e5f7e; font-variant-numeric: tabular-nums; }
21
- .metric .label { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  </style>
23
  </head>
24
  <body>
25
- <div class="subtitle">1 page β‰ˆ 500 tokens β‰ˆ 1,800 characters</div>
26
-
27
- <div class="controls">
28
- <div class="control-group">
29
- <label>Model</label>
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
- <canvas id="c" width="1000" height="400"></canvas>
 
 
 
 
 
 
 
 
48
 
49
- <div class="metrics">
50
- <div class="metric">
51
- <div class="value" id="pps">0</div>
52
- <div class="label" id="ppsLabel">Pages / second</div>
 
 
 
 
 
 
 
 
 
 
53
  </div>
54
- <div class="metric">
55
- <div class="value" id="tps">0</div>
56
- <div class="label">Tokens / second</div>
 
 
 
57
  </div>
58
- <div class="metric">
59
- <div class="value" id="total">0</div>
60
- <div class="label" id="totalLabel">Pages generated</div>
 
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
- const TOKENS_PER_PAGE = 500;
70
- const GPU_AREA_X = 10, GPU_AREA_Y = 10, GPU_AREA_W = 200, GPU_AREA_H = H - 20;
71
- const FADE_START = W - 120;
 
 
 
 
 
72
 
73
- function getSpawnX() {
74
- return GPU_AREA_X + GPU_AREA_W + 15;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- const PAGES_PER_BOOK = 200; // ~100K tokens per book
87
 
88
- let pages = [];
89
- let totalPages = 0;
90
- let spawnAccum = 0;
91
- let lastTime = performance.now();
92
 
93
- // Page colors β€” slight variations
94
- const pageColors = ['#f5f0e8', '#f0ece4', '#ebe7df', '#f2ede5', '#e8e4dc'];
 
95
 
96
- function getGpuCount() {
97
- // Logarithmic slider: 0-10 maps to 1-1024
98
- return Math.round(Math.pow(2, gpuSlider.value));
 
 
 
 
 
 
 
 
99
  }
100
 
101
- function getTps() {
102
- return parseInt(modelSelect.value) * getGpuCount();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- function getPps() {
106
- return getTps() / TOKENS_PER_PAGE;
 
 
 
 
107
  }
108
 
109
- gpuSlider.addEventListener('input', () => {
110
- gpuLabel.textContent = getGpuCount().toLocaleString();
111
- });
112
 
113
- function drawGPUs() {
114
- const count = getGpuCount();
115
- const ax = GPU_AREA_X, ay = GPU_AREA_Y, aw = GPU_AREA_W, ah = GPU_AREA_H;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- // Compute grid layout: find cols/rows that fit the area
118
- // Aim for roughly square-ish cells that fill the area
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 if (count <= 8) { cols = 4; rows = 2; }
124
- else if (count <= 16) { cols = 4; rows = 4; }
125
- else if (count <= 32) { cols = 8; rows = 4; }
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
- // Center the grid
138
- const totalW = cols * gw + (cols - 1) * gap;
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 col = i % cols;
147
- const row = Math.floor(i / cols);
148
- const x = offX + col * (gw + gap);
149
- const y = offY + row * (gh + gap);
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
- // Heatsink lines below fan
195
- const heatY = fanY + fanSize + 2;
196
- const heatH = gh - (heatY - y) - gw * 0.15;
197
- if (heatH > 4) {
198
- const lineH = Math.max(1, Math.min(4, heatH / 6));
199
- const lineGap = lineH * 1.5;
200
- for (let li = 0; li * lineGap < heatH; li++) {
201
- ctx.fillStyle = li % 2 === 0 ? '#444' : '#383838';
202
- ctx.fillRect(x + gw * 0.1, heatY + li * lineGap, gw * 0.8, lineH);
203
- }
204
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
- // Gold pins at bottom
207
- if (gw >= 30) {
208
- ctx.fillStyle = '#c4a020';
209
- const pinW = Math.max(1, gw * 0.06);
210
- const pinCount = Math.floor(gw * 0.7 / (pinW * 2));
211
- const pinStart = x + (gw - pinCount * pinW * 2) / 2;
212
- for (let p = 0; p < pinCount; p++) {
213
- ctx.fillRect(pinStart + p * pinW * 2, y + gh, pinW, Math.max(2, gw * 0.08));
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
- // Book cover colors
226
- const bookColors = ['#8b4513','#2e4057','#6b3a3a','#3a5a3a','#4a3a6b','#5a3a2e','#2e3a5a','#6b5a3a'];
227
-
228
- function drawItem(p) {
229
- const alpha = p.x > FADE_START ? 1 - (p.x - FADE_START) / (W - FADE_START) : 1;
230
- ctx.globalAlpha = alpha;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
- if (p.type === 'book') {
233
- const bw = 16, bh = 20;
234
- // Shadow
235
- ctx.fillStyle = 'rgba(0,0,0,0.12)';
236
- ctx.fillRect(p.x + 2, p.y + 2, bw, bh);
237
- // Cover
238
- ctx.fillStyle = p.color;
239
- ctx.fillRect(p.x, p.y, bw, bh);
240
- // Spine
241
- ctx.fillStyle = 'rgba(0,0,0,0.25)';
242
- ctx.fillRect(p.x, p.y, 3, bh);
243
- // Page edges (right side lighter stripe)
244
- ctx.fillStyle = '#f0ece4';
245
- ctx.fillRect(p.x + bw - 2, p.y + 1, 2, bh - 2);
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
- ctx.globalAlpha = 1;
 
 
 
 
274
  }
275
 
276
- function isBookMode() {
277
- return getPps() / PAGES_PER_BOOK >= 1;
 
 
 
 
 
 
 
 
 
 
 
278
  }
279
 
280
  function spawnItem() {
281
- const yMin = 30, yMax = H - 60;
282
- const y = yMin + Math.random() * (yMax - yMin);
283
- const bookMode = isBookMode();
284
- // Use effective rate for speed calculation
285
- const rate = bookMode ? getPps() / PAGES_PER_BOOK : getPps();
286
- const baseSpeed = 2 + Math.log10(Math.max(1, rate)) * 2;
287
- const speed = baseSpeed + Math.random() * baseSpeed;
288
- pages.push({
289
- x: getSpawnX() + Math.random() * 20,
290
- y,
291
- speed,
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
- function formatNum(n) {
302
- if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(1) + 'B';
303
- if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
304
- if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
305
- return n.toFixed(0);
306
  }
307
 
308
  function frame(now) {
309
- const dt = Math.min((now - lastTime) / 1000, 0.1); // cap at 100ms
310
- lastTime = now;
311
 
312
  ctx.clearRect(0, 0, W, H);
 
313
 
314
- // Background gradient hint
315
- ctx.fillStyle = '#fafafa';
316
- ctx.fillRect(0, 0, W, H);
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
- // Spawn items β€” books when fast enough, pages otherwise
331
- const pps = getPps();
332
- const bookMode = isBookMode();
333
- const spawnRate = bookMode ? pps / PAGES_PER_BOOK : pps;
334
-
335
  spawnAccum += spawnRate * dt;
336
- while (spawnAccum >= 1) {
337
- spawnItem();
338
- spawnAccum -= 1;
339
- }
340
-
341
- // Update and draw items
342
- for (let i = pages.length - 1; i >= 0; i--) {
343
- pages[i].x += pages[i].speed;
344
- if (pages[i].x > W + 40) {
345
- pages.splice(i, 1);
346
  continue;
347
  }
348
- drawItem(pages[i]);
349
  }
350
 
351
- // Draw GPUs on top
352
- drawGPUs();
353
-
354
- // Update metrics β€” switch to books when throughput is high enough
355
- const totalTps = getTps();
356
- const bps = pps / PAGES_PER_BOOK;
357
- if (bps >= 1) {
358
- ppsEl.textContent = formatNum(bps);
359
- ppsLabelEl.textContent = 'Books / second';
360
- } else {
361
- ppsEl.textContent = formatNum(pps);
362
- ppsLabelEl.textContent = 'Pages / second';
363
- }
364
- tpsEl.textContent = formatNum(totalTps);
365
- const totalBooks = totalPages / PAGES_PER_BOOK;
366
- if (totalBooks >= 1) {
367
- totalEl.textContent = formatNum(totalBooks);
368
- totalLabelEl.textContent = 'Books generated';
369
- } else {
370
- totalEl.textContent = formatNum(totalPages);
371
- totalLabelEl.textContent = 'Pages generated';
 
 
 
 
 
 
 
 
 
 
 
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 &asymp; 1,800 characters &nbsp;|&nbsp;
75
+ 1 πŸ“– <b>Book</b> = 250 pages = 125K tokens &nbsp;|&nbsp;
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>