joelniklaus HF Staff commited on
Commit
de4e224
Β·
1 Parent(s): 7db0003

combined throughput visualization into one file and fixed counting bug

Browse files
app/src/content/chapters/1-introduction.mdx CHANGED
@@ -72,7 +72,8 @@ Want to learn how to make GPUs go brrr and generate synthetic tokens at scale li
72
  <Wide>
73
  <HtmlEmbed
74
  id="intro-throughput"
75
- src="inference-throughput.html"
 
76
  caption="Drag the slider to scale up GPUs and watch the tokens fly. By the end of this post, you'll know exactly how to set this up."
77
  />
78
  </Wide>
 
72
  <Wide>
73
  <HtmlEmbed
74
  id="intro-throughput"
75
+ src="inference-throughput-compare.html"
76
+ config={{ modelCount: 1 }}
77
  caption="Drag the slider to scale up GPUs and watch the tokens fly. By the end of this post, you'll know exactly how to set this up."
78
  />
79
  </Wide>
app/src/content/chapters/5-infrastructure.mdx CHANGED
@@ -437,6 +437,7 @@ To get a feel for what these throughput numbers actually mean, pick two models a
437
  <HtmlEmbed
438
  id="inference-throughput-compare"
439
  src="inference-throughput-compare.html"
 
440
  caption="Side-by-side throughput comparison. Pick two models and adjust the GPU count to see the relative speedup. Scale mapping: πŸ“„ 1 page = 500 toks, πŸ“– 1 book = 500 pages, πŸ“š 1 shelf = 500 books."
441
  />
442
  </Wide>
 
437
  <HtmlEmbed
438
  id="inference-throughput-compare"
439
  src="inference-throughput-compare.html"
440
+ config={{ modelCount: 2 }}
441
  caption="Side-by-side throughput comparison. Pick two models and adjust the GPU count to see the relative speedup. Scale mapping: πŸ“„ 1 page = 500 toks, πŸ“– 1 book = 500 pages, πŸ“š 1 shelf = 500 books."
442
  />
443
  </Wide>
app/src/content/embeds/inference-throughput-compare.html CHANGED
@@ -227,6 +227,17 @@
227
  .instance-b .output-unit { color: var(--text-muted); }
228
  .instance-b select { border-color: var(--brand-b); }
229
 
 
 
 
 
 
 
 
 
 
 
 
230
  .instance-label {
231
  font-size: 11px;
232
  font-weight: 700;
@@ -302,10 +313,11 @@
302
  <div class="side-panel">
303
  <div class="instance-label">Model A</div>
304
  <div class="control-group">
 
305
  <select data-role="modelA">
306
  <option value="45540">SmolLM2-135M</option>
307
  <option value="8086">Qwen3-4B</option>
308
- <option value="6443" selected>Qwen3-8B</option>
309
  <option value="6117">GPT-OSS-120B</option>
310
  <option value="1724">Gemma-3-27B</option>
311
  </select>
@@ -317,7 +329,7 @@
317
  </div>
318
  <div class="bottom-strip">
319
  <div class="throughput-strip">
320
- <span data-role="booksRateA">0 pages/sec</span>
321
  <span class="tps" data-role="tpsInlineA">(0 TPS)</span>
322
  </div>
323
  <div class="output-panel">
@@ -330,7 +342,7 @@
330
  </div>
331
  </div>
332
  <div class="datasets">
333
- <div class="datasets-title">Time to generate dataset</div>
334
  <div class="dataset-bars" data-role="datasetBarsA"></div>
335
  </div>
336
  </div>
@@ -345,12 +357,13 @@
345
  <div class="side-panel">
346
  <div class="instance-label">Model B</div>
347
  <div class="control-group">
 
348
  <select data-role="modelB">
349
  <option value="45540">SmolLM2-135M</option>
350
  <option value="8086">Qwen3-4B</option>
351
  <option value="6443">Qwen3-8B</option>
352
  <option value="6117">GPT-OSS-120B</option>
353
- <option value="1724" selected>Gemma-3-27B</option>
354
  </select>
355
  </div>
356
  </div>
@@ -373,7 +386,7 @@
373
  </div>
374
  </div>
375
  <div class="datasets">
376
- <div class="datasets-title">Time to generate dataset</div>
377
  <div class="dataset-bars" data-role="datasetBarsB"></div>
378
  </div>
379
  </div>
@@ -388,6 +401,27 @@
388
  root.setAttribute('data-init', '1');
389
  const $ = (role) => root.querySelector('[data-role="' + role + '"]');
390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  const TOKENS_PER_PAGE = 500;
392
  const PAGES_PER_BOOK = 500;
393
  const BOOKS_PER_SHELF = 500;
@@ -400,7 +434,28 @@
400
  const MIN_GPUS = 1, MAX_GPUS = 1_000_000;
401
  const LOG_MIN = Math.log(MIN_GPUS), LOG_MAX = Math.log(MAX_GPUS);
402
 
403
- const TRAINING_RUNS = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  { gpus: 8, name: 'BERT', row: 'a1',
405
  desc: '<b>BERT</b> (Google, 2018)<br>16 TPU v3 chips. 340M params.<br>Trained on BooksCorpus + Wikipedia.' },
406
  { gpus: 32, name: 'GPT-2', row: 'a1',
@@ -421,7 +476,34 @@
421
  desc: '<b>GPT-5</b> (OpenAI, 2025, estimated)<br>\u224850K H100-equiv GPUs.' },
422
  ];
423
 
424
- const INFRA_LANDMARKS = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  { gpus: 1, name: '1 GPU', row: 'b1',
426
  desc: '<b>NVIDIA H100 SXM</b><br>80 GB HBM3, 3.96 PFLOPS FP8.' },
427
  { gpus: 8, name: '1 node', row: 'b1',
@@ -454,6 +536,32 @@
454
  const gpuSlider = $('gpus');
455
  const gpuLabelEl = $('gpuLabel');
456
  const speedupRatioEl = $('speedupRatio');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
 
458
  const themeTokens = {};
459
  function refreshThemeTokens() {
@@ -512,10 +620,25 @@
512
  el.addEventListener('click', () => { gpuSlider.value = gpusToSlider(lm.gpus); updateSliderGradient(); updateGpuLabel(); instances.forEach(inst => inst.reset()); });
513
  rowEls[lm.row].appendChild(el);
514
  }
515
- TRAINING_RUNS.forEach(addLandmark);
516
- INFRA_LANDMARKS.forEach(addLandmark);
517
 
518
- const DATASETS = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
  { name: 'BookCorpus', tokens: 1e9,
520
  desc: '<b>BookCorpus</b> (2015)<br>11K unpublished books scraped from smashwords.com.' },
521
  { name: 'Wikipedia', tokens: 6e9,
@@ -530,6 +653,8 @@
530
  desc: '<b>The entire Internet</b> (estimate)<br>Rough estimate of all text ever published online.' },
531
  ];
532
 
 
 
533
  // --- Utility functions ---
534
  function formatNum(n) {
535
  if (n >= 1e15) return (n / 1e15).toFixed(1) + 'Q';
@@ -891,14 +1016,12 @@
891
  const pagesPerShelf = PAGES_PER_BOOK * BOOKS_PER_SHELF;
892
  const rate = mode === 'shelf' ? pps / pagesPerShelf : mode === 'book' ? pps / PAGES_PER_BOOK : pps;
893
  const bs = 0.8 + Math.log10(Math.max(1, rate)) * 0.8;
894
- const tokens = mode === 'shelf' ? TOKENS_PER_SHELF : mode === 'book' ? TOKENS_PER_BOOK : TOKENS_PER_PAGE;
895
  const scale = 0.85 + Math.random() * 0.3;
896
  const bobAmp = 3 + Math.random() * 5;
897
  const bobPhase = Math.random() * Math.PI * 2;
898
  const wobbleAmp = mode === 'page' ? 0.15 + Math.random() * 0.25 : 0.08 + Math.random() * 0.15;
899
  const wobblePhase = Math.random() * Math.PI * 2;
900
  const depthSpeed = (bs + Math.random() * bs) * (0.7 + scale * 0.6);
901
- totalTokens += tokens;
902
  floatingItems.push({
903
  x: spawnX + Math.random() * 20, y, speed: depthSpeed,
904
  type: mode, scale, bobAmp, bobPhase, wobbleAmp, wobblePhase
@@ -908,10 +1031,14 @@
908
  function frame(now, dt) {
909
  const W = getW(), H = getH();
910
  _fNow = now;
911
- _fPps = getPps();
 
912
  _fFanPhase = (now / 200) * Math.min(_fPps, 100);
913
  _fBlinkSpeed = Math.min(_fPps, 200);
914
 
 
 
 
915
  ctx.clearRect(0, 0, W, H);
916
  ctx.fillStyle = themeTokens.canvasBg; ctx.fillRect(0, 0, W, H);
917
 
@@ -1007,15 +1134,18 @@
1007
  totalItemUnit: $('totalItemUnitA'),
1008
  });
1009
 
1010
- const instB = createInstance('cB', 'modelB', 'datasetBarsB', {
1011
- booksRate: $('booksRateB'),
1012
- tpsInline: $('tpsInlineB'),
1013
- totalTokensNum: $('totalTokensNumB'),
1014
- totalItemNum: $('totalItemNumB'),
1015
- totalItemUnit: $('totalItemUnitB'),
1016
- });
1017
-
1018
- const instances = [instA, instB];
 
 
 
1019
 
1020
  let lastTime = performance.now(), lastMetricUpdate = 0;
1021
 
@@ -1023,27 +1153,31 @@
1023
  const dt = Math.min((now - lastTime) / 1000, 0.1);
1024
  lastTime = now;
1025
 
1026
- instA.frame(now, dt);
1027
- instB.frame(now, dt);
 
1028
 
1029
  if (now - lastMetricUpdate > 100) {
1030
  lastMetricUpdate = now;
1031
  instA.updateMetrics(now);
1032
- instB.updateMetrics(now);
1033
-
1034
- const tpsA = instA.getTps();
1035
- const tpsB = instB.getTps();
1036
- if (tpsA === tpsB) {
1037
- speedupRatioEl.textContent = 'Same throughput';
1038
- speedupRatioEl.className = 'ratio';
1039
- } else if (tpsA > tpsB) {
1040
- const ratio = tpsA / tpsB;
1041
- speedupRatioEl.textContent = 'Model A is ' + ratio.toFixed(1) + '\u00d7 faster';
1042
- speedupRatioEl.className = 'ratio a-faster';
1043
- } else {
1044
- const ratio = tpsB / tpsA;
1045
- speedupRatioEl.textContent = 'Model B is ' + ratio.toFixed(1) + '\u00d7 faster';
1046
- speedupRatioEl.className = 'ratio b-faster';
 
 
 
1047
  }
1048
  }
1049
 
 
227
  .instance-b .output-unit { color: var(--text-muted); }
228
  .instance-b select { border-color: var(--brand-b); }
229
 
230
+ .mode-single .instance-b,
231
+ .mode-single .speedup-badge { display: none; }
232
+
233
+ .mode-single .instance-a .instance-label { display: none; }
234
+ .mode-single .instance-a .control-group label { display: block; }
235
+ .mode-single .instance-a .canvas-wrap { border-color: var(--border); }
236
+ .mode-single .instance-a select { border-color: var(--border); }
237
+
238
+ .mode-compare .instance-a .control-group label,
239
+ .mode-compare .instance-b .control-group label { display: none; }
240
+
241
  .instance-label {
242
  font-size: 11px;
243
  font-weight: 700;
 
313
  <div class="side-panel">
314
  <div class="instance-label">Model A</div>
315
  <div class="control-group">
316
+ <label data-role="modelLabelA">Model</label>
317
  <select data-role="modelA">
318
  <option value="45540">SmolLM2-135M</option>
319
  <option value="8086">Qwen3-4B</option>
320
+ <option value="6443">Qwen3-8B</option>
321
  <option value="6117">GPT-OSS-120B</option>
322
  <option value="1724">Gemma-3-27B</option>
323
  </select>
 
329
  </div>
330
  <div class="bottom-strip">
331
  <div class="throughput-strip">
332
+ <span data-role="booksRateA">0 books/sec</span>
333
  <span class="tps" data-role="tpsInlineA">(0 TPS)</span>
334
  </div>
335
  <div class="output-panel">
 
342
  </div>
343
  </div>
344
  <div class="datasets">
345
+ <div class="datasets-title" data-role="datasetsTitleA">Time to generate dataset</div>
346
  <div class="dataset-bars" data-role="datasetBarsA"></div>
347
  </div>
348
  </div>
 
357
  <div class="side-panel">
358
  <div class="instance-label">Model B</div>
359
  <div class="control-group">
360
+ <label data-role="modelLabelB">Model</label>
361
  <select data-role="modelB">
362
  <option value="45540">SmolLM2-135M</option>
363
  <option value="8086">Qwen3-4B</option>
364
  <option value="6443">Qwen3-8B</option>
365
  <option value="6117">GPT-OSS-120B</option>
366
+ <option value="1724">Gemma-3-27B</option>
367
  </select>
368
  </div>
369
  </div>
 
386
  </div>
387
  </div>
388
  <div class="datasets">
389
+ <div class="datasets-title" data-role="datasetsTitleB">Time to generate dataset</div>
390
  <div class="dataset-bars" data-role="datasetBarsB"></div>
391
  </div>
392
  </div>
 
401
  root.setAttribute('data-init', '1');
402
  const $ = (role) => root.querySelector('[data-role="' + role + '"]');
403
 
404
+ function readEmbedConfig() {
405
+ let mountEl = root;
406
+ while (mountEl && !mountEl.getAttribute?.('data-config')) {
407
+ mountEl = mountEl.parentElement;
408
+ }
409
+ try {
410
+ const rawConfig = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
411
+ return rawConfig ? JSON.parse(rawConfig) : {};
412
+ } catch (error) {
413
+ console.error('Error parsing embed config:', error);
414
+ return {};
415
+ }
416
+ }
417
+
418
+ const embedConfig = readEmbedConfig();
419
+ const modelCount = Number(embedConfig.modelCount) === 2 ? 2 : 1;
420
+ const isCompareMode = modelCount === 2;
421
+ const defaultModelA = String(embedConfig.modelA || (isCompareMode ? 6443 : 45540));
422
+ const defaultModelB = String(embedConfig.modelB || 1724);
423
+ root.classList.add(isCompareMode ? 'mode-compare' : 'mode-single');
424
+
425
  const TOKENS_PER_PAGE = 500;
426
  const PAGES_PER_BOOK = 500;
427
  const BOOKS_PER_SHELF = 500;
 
434
  const MIN_GPUS = 1, MAX_GPUS = 1_000_000;
435
  const LOG_MIN = Math.log(MIN_GPUS), LOG_MAX = Math.log(MAX_GPUS);
436
 
437
+ const TRAINING_RUNS_SINGLE = [
438
+ { gpus: 8, name: 'BERT', row: 'a1',
439
+ 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.' },
440
+ { gpus: 32, name: 'GPT-2', row: 'a1',
441
+ 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.' },
442
+ { gpus: 384, name: 'BLOOM', row: 'a1',
443
+ desc: '<b>BLOOM</b> (BigScience, 2022)<br>384 A100 80GB GPUs on Jean Zay (48 nodes). 176B params.<br>One of the first major open large-model training runs.' },
444
+ { gpus: 2_048, name: 'Llama 1', row: 'a2',
445
+ 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.' },
446
+ { gpus: 2_788, name: 'DeepSeek', row: 'a1',
447
+ 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).' },
448
+ { gpus: 10_000, name: 'GPT-3', row: 'a1',
449
+ 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.' },
450
+ { gpus: 16_384, name: 'Llama 3', row: 'a2',
451
+ 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.' },
452
+ { gpus: 25_000, name: 'GPT-4', row: 'a1',
453
+ 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).' },
454
+ { gpus: 50_000, name: 'GPT-5', row: 'a1',
455
+ 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.' },
456
+ ];
457
+
458
+ const TRAINING_RUNS_COMPARE = [
459
  { gpus: 8, name: 'BERT', row: 'a1',
460
  desc: '<b>BERT</b> (Google, 2018)<br>16 TPU v3 chips. 340M params.<br>Trained on BooksCorpus + Wikipedia.' },
461
  { gpus: 32, name: 'GPT-2', row: 'a1',
 
476
  desc: '<b>GPT-5</b> (OpenAI, 2025, estimated)<br>\u224850K H100-equiv GPUs.' },
477
  ];
478
 
479
+ const INFRA_LANDMARKS_SINGLE = [
480
+ { gpus: 1, name: '1 GPU', row: 'b1',
481
+ desc: '<b>NVIDIA H100 SXM</b><br>80 GB HBM3, 3.96 PFLOPS FP8.<br>The workhorse of modern AI training and inference.' },
482
+ { gpus: 8, name: '1 node', row: 'b1',
483
+ 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.' },
484
+ { gpus: 32, name: '1 rack', row: 'b1',
485
+ 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.' },
486
+ { gpus: 256, name: 'SuperPOD', row: 'b1',
487
+ 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.' },
488
+ { gpus: 10_752, name: 'ALPS', row: 'b1',
489
+ 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.' },
490
+ { gpus: 12_288, name: 'ByteDance', row: 'b2',
491
+ 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.' },
492
+ { gpus: 64_000, name: 'Stargate', row: 'b1',
493
+ 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.' },
494
+ { gpus: 100_000, name: 'Tencent', row: 'b2',
495
+ 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.' },
496
+ { gpus: 200_000, name: 'Colossus', row: 'b1',
497
+ 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.' },
498
+ { gpus: 250_000, name: 'CoreWeave', row: 'b2',
499
+ 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.' },
500
+ { gpus: 600_000, name: 'Meta', row: 'b2',
501
+ 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.' },
502
+ { gpus: 1_000_000, name: 'Colossus 2', row: 'b1',
503
+ 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.' },
504
+ ];
505
+
506
+ const INFRA_LANDMARKS_COMPARE = [
507
  { gpus: 1, name: '1 GPU', row: 'b1',
508
  desc: '<b>NVIDIA H100 SXM</b><br>80 GB HBM3, 3.96 PFLOPS FP8.' },
509
  { gpus: 8, name: '1 node', row: 'b1',
 
536
  const gpuSlider = $('gpus');
537
  const gpuLabelEl = $('gpuLabel');
538
  const speedupRatioEl = $('speedupRatio');
539
+ const modelASelect = $('modelA');
540
+ const modelBSelect = $('modelB');
541
+ const datasetsTitleAEl = $('datasetsTitleA');
542
+ const datasetsTitleBEl = $('datasetsTitleB');
543
+ const booksRateAEl = $('booksRateA');
544
+ const booksRateBEl = $('booksRateB');
545
+
546
+ function applyModelDefault(selectEl, modelValue) {
547
+ const hasOption = Array.from(selectEl.options).some((option) => option.value === modelValue);
548
+ if (hasOption) {
549
+ selectEl.value = modelValue;
550
+ }
551
+ }
552
+
553
+ applyModelDefault(modelASelect, defaultModelA);
554
+ applyModelDefault(modelBSelect, defaultModelB);
555
+
556
+ if (isCompareMode) {
557
+ datasetsTitleAEl.textContent = 'Time to generate dataset';
558
+ datasetsTitleBEl.textContent = 'Time to generate dataset';
559
+ booksRateAEl.textContent = '0 pages/sec';
560
+ booksRateBEl.textContent = '0 pages/sec';
561
+ } else {
562
+ datasetsTitleAEl.innerHTML = 'Time to generate dataset<br>at current throughput';
563
+ booksRateAEl.textContent = '0 books/sec';
564
+ }
565
 
566
  const themeTokens = {};
567
  function refreshThemeTokens() {
 
620
  el.addEventListener('click', () => { gpuSlider.value = gpusToSlider(lm.gpus); updateSliderGradient(); updateGpuLabel(); instances.forEach(inst => inst.reset()); });
621
  rowEls[lm.row].appendChild(el);
622
  }
623
+ (isCompareMode ? TRAINING_RUNS_COMPARE : TRAINING_RUNS_SINGLE).forEach(addLandmark);
624
+ (isCompareMode ? INFRA_LANDMARKS_COMPARE : INFRA_LANDMARKS_SINGLE).forEach(addLandmark);
625
 
626
+ const DATASETS_SINGLE = [
627
+ { name: 'BookCorpus', tokens: 1e9,
628
+ desc: '<b>BookCorpus</b> (2015)<br>11K unpublished books scraped from smashwords.com. Used to train the original BERT and GPT-1.' },
629
+ { name: 'Wikipedia', tokens: 6e9,
630
+ desc: '<b>Multilingual Wikipedia</b><br>All articles across all 300+ language editions. ~4.7B words, ~6B tokens. A staple ingredient in virtually every LLM pretraining mix.' },
631
+ { name: 'FinePhrase', tokens: 1e12,
632
+ 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.' },
633
+ { name: 'RedPajama', tokens: 100e12,
634
+ desc: '<b>RedPajama v2</b> (Together AI, 2023)<br>100T raw tokens from 84 Common Crawl snapshots with quality signals. Covers 5 languages.' },
635
+ { name: 'Common Crawl', tokens: 3e15,
636
+ 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.' },
637
+ { name: 'The Internet', tokens: 100e15,
638
+ desc: '<b>The entire Internet</b> (estimate)<br>Rough estimate of all text ever published online. Nobody has actually tokenized it all.' },
639
+ ];
640
+
641
+ const DATASETS_COMPARE = [
642
  { name: 'BookCorpus', tokens: 1e9,
643
  desc: '<b>BookCorpus</b> (2015)<br>11K unpublished books scraped from smashwords.com.' },
644
  { name: 'Wikipedia', tokens: 6e9,
 
653
  desc: '<b>The entire Internet</b> (estimate)<br>Rough estimate of all text ever published online.' },
654
  ];
655
 
656
+ const DATASETS = isCompareMode ? DATASETS_COMPARE : DATASETS_SINGLE;
657
+
658
  // --- Utility functions ---
659
  function formatNum(n) {
660
  if (n >= 1e15) return (n / 1e15).toFixed(1) + 'Q';
 
1016
  const pagesPerShelf = PAGES_PER_BOOK * BOOKS_PER_SHELF;
1017
  const rate = mode === 'shelf' ? pps / pagesPerShelf : mode === 'book' ? pps / PAGES_PER_BOOK : pps;
1018
  const bs = 0.8 + Math.log10(Math.max(1, rate)) * 0.8;
 
1019
  const scale = 0.85 + Math.random() * 0.3;
1020
  const bobAmp = 3 + Math.random() * 5;
1021
  const bobPhase = Math.random() * Math.PI * 2;
1022
  const wobbleAmp = mode === 'page' ? 0.15 + Math.random() * 0.25 : 0.08 + Math.random() * 0.15;
1023
  const wobblePhase = Math.random() * Math.PI * 2;
1024
  const depthSpeed = (bs + Math.random() * bs) * (0.7 + scale * 0.6);
 
1025
  floatingItems.push({
1026
  x: spawnX + Math.random() * 20, y, speed: depthSpeed,
1027
  type: mode, scale, bobAmp, bobPhase, wobbleAmp, wobblePhase
 
1031
  function frame(now, dt) {
1032
  const W = getW(), H = getH();
1033
  _fNow = now;
1034
+ const tps = getTps();
1035
+ _fPps = tps / TOKENS_PER_PAGE;
1036
  _fFanPhase = (now / 200) * Math.min(_fPps, 100);
1037
  _fBlinkSpeed = Math.min(_fPps, 200);
1038
 
1039
+ // Keep dataset timers accurate at any throughput, independent of capped visual spawn rate.
1040
+ totalTokens += tps * dt;
1041
+
1042
  ctx.clearRect(0, 0, W, H);
1043
  ctx.fillStyle = themeTokens.canvasBg; ctx.fillRect(0, 0, W, H);
1044
 
 
1134
  totalItemUnit: $('totalItemUnitA'),
1135
  });
1136
 
1137
+ const instances = [instA];
1138
+ let instB = null;
1139
+ if (isCompareMode) {
1140
+ instB = createInstance('cB', 'modelB', 'datasetBarsB', {
1141
+ booksRate: $('booksRateB'),
1142
+ tpsInline: $('tpsInlineB'),
1143
+ totalTokensNum: $('totalTokensNumB'),
1144
+ totalItemNum: $('totalItemNumB'),
1145
+ totalItemUnit: $('totalItemUnitB'),
1146
+ });
1147
+ instances.push(instB);
1148
+ }
1149
 
1150
  let lastTime = performance.now(), lastMetricUpdate = 0;
1151
 
 
1153
  const dt = Math.min((now - lastTime) / 1000, 0.1);
1154
  lastTime = now;
1155
 
1156
+ for (let i = 0; i < instances.length; i++) {
1157
+ instances[i].frame(now, dt);
1158
+ }
1159
 
1160
  if (now - lastMetricUpdate > 100) {
1161
  lastMetricUpdate = now;
1162
  instA.updateMetrics(now);
1163
+
1164
+ if (instB) {
1165
+ instB.updateMetrics(now);
1166
+
1167
+ const tpsA = instA.getTps();
1168
+ const tpsB = instB.getTps();
1169
+ if (tpsA === tpsB) {
1170
+ speedupRatioEl.textContent = 'Same throughput';
1171
+ speedupRatioEl.className = 'ratio';
1172
+ } else if (tpsA > tpsB) {
1173
+ const ratio = tpsA / tpsB;
1174
+ speedupRatioEl.textContent = 'Model A is ' + ratio.toFixed(1) + '\u00d7 faster';
1175
+ speedupRatioEl.className = 'ratio a-faster';
1176
+ } else {
1177
+ const ratio = tpsB / tpsA;
1178
+ speedupRatioEl.textContent = 'Model B is ' + ratio.toFixed(1) + '\u00d7 faster';
1179
+ speedupRatioEl.className = 'ratio b-faster';
1180
+ }
1181
  }
1182
  }
1183
 
app/src/content/embeds/inference-throughput.html DELETED
@@ -1,945 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>GPU Page Generator</title>
6
- <style>
7
- :root {
8
- color-scheme: light dark;
9
- --bg: #ffffff;
10
- --surface: #f9fafb;
11
- --canvas-bg: #fafafa;
12
- --canvas-grid: #d8dbe0;
13
- --text: #1f2937;
14
- --text-strong: #111827;
15
- --text-muted: #4b5563;
16
- --text-faint: #6b7280;
17
- --border: #111827;
18
- --brand: #2e5f7e;
19
- --training: #b45309;
20
- --success: #16a34a;
21
- --tooltip-bg: #111827;
22
- --tooltip-text: #f8fafc;
23
- --slider-start: #2e5f7e;
24
- --slider-mid: #c0392b;
25
- --slider-end: #c4a020;
26
- --slider-rest: #d1d5db;
27
- }
28
-
29
- @media (prefers-color-scheme: dark) {
30
- :root {
31
- --bg: #020617;
32
- --surface: #0b1324;
33
- --canvas-bg: #f8fafc;
34
- --canvas-grid: #d4d7dd;
35
- --text: #e2e8f0;
36
- --text-strong: #f8fafc;
37
- --text-muted: #cbd5e1;
38
- --text-faint: #94a3b8;
39
- --border: #475569;
40
- --brand: #7dd3fc;
41
- --training: #f59e0b;
42
- --success: #22c55e;
43
- --tooltip-bg: #020617;
44
- --tooltip-text: #f8fafc;
45
- --slider-start: #38bdf8;
46
- --slider-mid: #fb7185;
47
- --slider-end: #facc15;
48
- --slider-rest: #334155;
49
- }
50
- .bottom-strip { color: #e2e8f0; }
51
- .bottom-strip .tps { color: #cbd5e1; }
52
- }
53
-
54
- * { margin: 0; padding: 0; box-sizing: border-box; }
55
- body { background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; display: flex; flex-direction: column; align-items: center; padding: 16px 0; }
56
-
57
- .viz { width: 100%; max-width: 1200px; }
58
-
59
- .control-group { display: flex; flex-direction: column; gap: 4px; }
60
- .control-group label { font-size: 12px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.6px; }
61
- select {
62
- -webkit-appearance: none;
63
- appearance: none;
64
- color: var(--text-strong);
65
- font-size: 15px;
66
- font-family: inherit;
67
- padding: 7px 28px 7px 10px;
68
- border: 2px solid var(--border);
69
- border-radius: 8px;
70
- background-color: var(--surface);
71
- background-image:
72
- linear-gradient(45deg, transparent 50%, var(--text-muted) 50%),
73
- linear-gradient(135deg, var(--text-muted) 50%, transparent 50%);
74
- background-position:
75
- calc(100% - 14px) calc(50% - 2px),
76
- calc(100% - 9px) calc(50% - 2px);
77
- background-size: 5px 5px, 5px 5px;
78
- background-repeat: no-repeat;
79
- cursor: pointer;
80
- }
81
- .canvas-row { display: flex; gap: 8px; align-items: stretch; position: relative; z-index: 1; }
82
- .canvas-row .side-panel { display: flex; flex-direction: column; gap: 6px; justify-content: center; }
83
- .canvas-wrap {
84
- position: relative;
85
- flex: 1;
86
- min-width: 0;
87
- border: 2px solid var(--border);
88
- background: var(--canvas-bg);
89
- }
90
- .canvas-stage { position: relative; }
91
- .canvas-wrap canvas {
92
- width: 100%;
93
- height: auto;
94
- display: block;
95
- border: 0;
96
- background: transparent;
97
- }
98
- .slider-area { position: relative; width: 100%; margin-bottom: 8px; z-index: 9999; }
99
- .gpu-label {
100
- text-align: center;
101
- font-size: 14px;
102
- font-weight: 700;
103
- color: var(--brand);
104
- margin-bottom: 2px;
105
- font-variant-numeric: tabular-nums;
106
- }
107
- .slider-area input[type=range] { position: relative; z-index: 3; width: 100%; margin: 0; display: block; -webkit-appearance: none; appearance: none; height: 8px; border-radius: 4px; outline: none; cursor: pointer; }
108
- .slider-area input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--bg); border: 2px solid var(--brand); cursor: pointer; margin-top: -5px; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
109
- .slider-area input[type=range]::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--bg); border: 2px solid var(--brand); cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
110
-
111
- .landmark-row { position: relative; width: 100%; height: 16px; }
112
- .landmark { position: absolute; transform: translateX(-50%); cursor: pointer; display: flex; flex-direction: column; align-items: center; }
113
- .landmark-row.top .landmark { bottom: 0; }
114
- .landmark-row.bottom .landmark { top: 0; }
115
- .landmark .tick { width: 2px; height: 5px; flex-shrink: 0; }
116
- .landmark .name { font-size: 10px; font-weight: 700; white-space: nowrap; letter-spacing: 0.2px; line-height: 1.15; }
117
- .landmark:hover .name { color: var(--text-strong) !important; }
118
-
119
- .landmark-row.top .tick { background: var(--training); }
120
- .landmark-row.top .name { color: var(--training); }
121
- .landmark-row.bottom .tick { background: var(--brand); }
122
- .landmark-row.bottom .name { color: var(--brand); }
123
- #row-a1 .landmark, #row-b1 .landmark { z-index: 2; }
124
- #row-a2 .landmark, #row-b2 .landmark { z-index: 1; }
125
- #row-a2 .landmark .tick,
126
- #row-b2 .landmark .tick { position: relative; }
127
- #row-a2 .landmark .tick::after {
128
- content: '';
129
- position: absolute;
130
- left: 50%;
131
- transform: translateX(-50%);
132
- top: 100%;
133
- width: 1px;
134
- height: 18px;
135
- background: var(--training);
136
- opacity: 0.35;
137
- z-index: -1;
138
- }
139
- #row-b2 .landmark .tick::before {
140
- content: '';
141
- position: absolute;
142
- left: 50%;
143
- transform: translateX(-50%);
144
- bottom: 100%;
145
- width: 1px;
146
- height: 18px;
147
- background: var(--brand);
148
- opacity: 0.35;
149
- z-index: -1;
150
- }
151
-
152
- .landmark .tooltip { display: none; position: absolute; left: 50%; transform: translateX(-50%); background: var(--tooltip-bg); color: var(--tooltip-text); font-size: 12px; padding: 10px 14px; border-radius: 8px; width: 300px; white-space: normal; z-index: 2147483647; pointer-events: none; line-height: 1.45; text-align: left; box-shadow: 0 12px 30px rgba(0,0,0,0.35); }
153
- .landmark-row.top .tooltip { top: calc(100% + 4px); }
154
- .landmark-row.bottom .tooltip { top: calc(100% + 4px); }
155
- .landmark .tooltip::after { content: ''; position: absolute; left: 50%; transform: translateX(-50%); border: 5px solid transparent; }
156
- .landmark .tooltip.tip-right::after { left: auto; right: 16px; transform: none; }
157
- .landmark .tooltip.tip-left::after { left: 16px; transform: none; }
158
- .landmark-row.top .tooltip::after { bottom: 100%; border-bottom-color: var(--tooltip-bg); }
159
- .landmark-row.bottom .tooltip::after { bottom: 100%; border-bottom-color: var(--tooltip-bg); }
160
- .landmark-row:has(.landmark:hover) { z-index: 99999; position: relative; }
161
- .landmark:hover .tooltip { display: block; }
162
-
163
- .bottom-strip {
164
- position: absolute;
165
- bottom: 4px;
166
- left: 10px;
167
- right: 10px;
168
- display: flex;
169
- align-items: center;
170
- justify-content: space-between;
171
- font-size: 11px;
172
- font-weight: 700;
173
- font-variant-numeric: tabular-nums;
174
- line-height: 1.2;
175
- color: #1f2937;
176
- pointer-events: none;
177
- z-index: 4;
178
- }
179
- .throughput-strip {
180
- padding: 0;
181
- text-align: center;
182
- }
183
- .throughput-strip .tps {
184
- color: var(--text-muted);
185
- font-weight: 600;
186
- }
187
-
188
- .output-panel {
189
- display: flex;
190
- align-items: center;
191
- gap: 6px;
192
- text-align: center;
193
- }
194
- .output-heading {
195
- color: var(--text-muted);
196
- text-transform: uppercase;
197
- letter-spacing: 0.4px;
198
- font-weight: 700;
199
- font-size: 10px;
200
- }
201
- .output-num {
202
- font-weight: 700;
203
- color: var(--brand);
204
- font-variant-numeric: tabular-nums;
205
- font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
206
- font-size: 11px;
207
- }
208
- .output-unit {
209
- font-size: 11px;
210
- color: var(--text-muted);
211
- font-weight: 700;
212
- }
213
- .output-unit-emoji {
214
- font-size: 13px;
215
- line-height: 1;
216
- }
217
-
218
- .datasets { min-width: 180px; display: flex; flex-direction: column; justify-content: center; }
219
- .datasets-title { font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 6px; }
220
- .dataset-bars { display: flex; flex-direction: column; gap: 4px; }
221
- .dataset-item { display: flex; align-items: center; gap: 5px; font-size: 12px; position: relative; cursor: pointer; }
222
- .dataset-item .ds-check { font-size: 15px; width: 18px; text-align: center; }
223
- .dataset-item .ds-name { font-weight: 600; color: var(--text-strong); }
224
- .dataset-item .ds-time { color: var(--brand); font-weight: 700; font-variant-numeric: tabular-nums; }
225
- .dataset-item .ds-size { color: var(--text-faint); font-size: 11px; }
226
- .dataset-item.done .ds-name { color: var(--success); }
227
- .dataset-item.done .ds-time { color: var(--success); }
228
- .dataset-item .ds-tip { display: none; position: absolute; top: calc(100% + 6px); left: 0; background: var(--tooltip-bg); color: var(--tooltip-text); font-size: 12px; padding: 10px 14px; border-radius: 8px; width: 260px; white-space: normal; z-index: 2147483647; pointer-events: none; line-height: 1.45; text-align: left; box-shadow: 0 12px 30px rgba(0,0,0,0.35); }
229
- .dataset-item .ds-tip::after { content: ''; position: absolute; bottom: 100%; left: 20px; border: 5px solid transparent; border-bottom-color: var(--tooltip-bg); }
230
- .dataset-item:hover { z-index: 99999; }
231
- .dataset-item:hover .ds-tip { display: block; }
232
-
233
- @media (max-width: 980px) {
234
- .canvas-row { flex-direction: column; }
235
- .canvas-row .side-panel { align-items: flex-start; }
236
- .landmark .name { font-size: 9px; }
237
- .bottom-strip {
238
- left: 6px;
239
- right: 6px;
240
- font-size: 10px;
241
- }
242
- .output-num { font-size: 10px; }
243
- .output-unit { font-size: 10px; }
244
- .output-unit-emoji { font-size: 12px; }
245
- .output-heading { font-size: 9px; }
246
- .datasets { max-width: 100%; min-width: 0; }
247
- .dataset-bars { flex-direction: row; flex-wrap: wrap; gap: 4px 12px; }
248
- .dataset-item { font-size: 12px; }
249
- }
250
- </style>
251
- </head>
252
- <body>
253
- <div class="viz">
254
- <div class="slider-area">
255
- <div class="gpu-label" data-role="gpuLabel">1 GPU</div>
256
- <div class="landmark-row top" data-role="row-a2"></div>
257
- <div class="landmark-row top" data-role="row-a1"></div>
258
- <input type="range" data-role="gpus" min="0" max="1" step="0.001" value="0">
259
- <div class="landmark-row bottom" data-role="row-b1"></div>
260
- <div class="landmark-row bottom" data-role="row-b2"></div>
261
- </div>
262
-
263
- <div class="canvas-row">
264
- <div class="side-panel">
265
- <div class="control-group">
266
- <label>Model</label>
267
- <select data-role="model">
268
- <option value="45540">SmolLM2-135M</option>
269
- <option value="8086">Qwen3-4B</option>
270
- <option value="6443">Qwen3-8B</option>
271
- <option value="6117">GPT-OSS-120B</option>
272
- <option value="1724">Gemma-3-27B</option>
273
- </select>
274
- </div>
275
- </div>
276
- <div class="canvas-wrap">
277
- <div class="canvas-stage">
278
- <canvas data-role="c"></canvas>
279
- </div>
280
- <div class="bottom-strip">
281
- <div class="throughput-strip">
282
- <span data-role="booksRate">0 books/sec</span>
283
- <span class="tps" data-role="tpsInline">(0 TPS)</span>
284
- </div>
285
- <div class="output-panel">
286
- <span class="output-heading">Generated</span>
287
- <span class="output-num" data-role="totalTokensNum">0</span>
288
- <span class="output-unit output-unit-text">toks</span>
289
- <span class="output-num" data-role="totalItemNum">0</span>
290
- <span class="output-unit output-unit-emoji" data-role="totalItemUnit">πŸ“„</span>
291
- </div>
292
- </div>
293
- </div>
294
- <div class="datasets">
295
- <div class="datasets-title">Time to generate dataset<br>at current throughput</div>
296
- <div class="dataset-bars" data-role="datasetBars"></div>
297
- </div>
298
- </div>
299
- </div>
300
-
301
- <script>
302
- (function() {
303
- const allViz = document.querySelectorAll('.viz:not([data-init])');
304
- const root = allViz[allViz.length - 1];
305
- if (!root) return;
306
- root.setAttribute('data-init', '1');
307
- const $ = (role) => root.querySelector('[data-role="' + role + '"]');
308
- const canvas = $('c');
309
- let ctx = canvas.getContext('2d');
310
-
311
- function resizeCanvas() {
312
- const w = canvas.clientWidth;
313
- canvas.width = w * 2;
314
- canvas.height = Math.round(w * 0.28) * 2;
315
- canvas.style.height = Math.round(w * 0.28) + 'px';
316
- }
317
- resizeCanvas();
318
- window.addEventListener('resize', resizeCanvas);
319
-
320
- const TOKENS_PER_PAGE = 500;
321
- const PAGES_PER_BOOK = 500;
322
- const BOOKS_PER_SHELF = 500;
323
- const TOKENS_PER_BOOK = TOKENS_PER_PAGE * PAGES_PER_BOOK;
324
- const TOKENS_PER_SHELF = TOKENS_PER_BOOK * BOOKS_PER_SHELF;
325
- const GPUS_PER_NODE = 8;
326
- const GPUS_PER_RACK = 32;
327
- const GPUS_PER_SUPERPOD = 256;
328
-
329
- const MIN_GPUS = 1, MAX_GPUS = 1_000_000;
330
- const LOG_MIN = Math.log(MIN_GPUS), LOG_MAX = Math.log(MAX_GPUS);
331
-
332
- const DATASETS = [
333
- { name: 'BookCorpus', tokens: 1e9,
334
- desc: '<b>BookCorpus</b> (2015)<br>11K unpublished books scraped from smashwords.com. Used to train the original BERT and GPT-1.' },
335
- { name: 'Wikipedia', tokens: 6e9,
336
- desc: '<b>Multilingual Wikipedia</b><br>All articles across all 300+ language editions. ~4.7B words, ~6B tokens. A staple ingredient in virtually every LLM pretraining mix.' },
337
- { name: 'FinePhrase', tokens: 1e12,
338
- 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.' },
339
- { name: 'RedPajama', tokens: 100e12,
340
- desc: '<b>RedPajama v2</b> (Together AI, 2023)<br>100T raw tokens from 84 Common Crawl snapshots with quality signals. Covers 5 languages.' },
341
- { name: 'Common Crawl', tokens: 3e15,
342
- 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.' },
343
- { name: 'The Internet', tokens: 100e15,
344
- desc: '<b>The entire Internet</b> (estimate)<br>Rough estimate of all text ever published online. Nobody has actually tokenized it all.' },
345
- ];
346
-
347
- let dsDone = DATASETS.map(() => false);
348
-
349
- const TRAINING_RUNS = [
350
- { gpus: 8, name: 'BERT', row: 'a1',
351
- 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.' },
352
- { gpus: 32, name: 'GPT-2', row: 'a1',
353
- 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.' },
354
- { gpus: 384, name: 'BLOOM', row: 'a1',
355
- desc: '<b>BLOOM</b> (BigScience, 2022)<br>384 A100 80GB GPUs on Jean Zay (48 nodes). 176B params.<br>One of the first major open large-model training runs.' },
356
- { gpus: 2_048, name: 'Llama 1', row: 'a2',
357
- 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.' },
358
- { gpus: 2_788, name: 'DeepSeek', row: 'a1',
359
- 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).' },
360
- { gpus: 10_000, name: 'GPT-3', row: 'a1',
361
- 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.' },
362
- { gpus: 16_384, name: 'Llama 3', row: 'a2',
363
- 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.' },
364
- { gpus: 25_000, name: 'GPT-4', row: 'a1',
365
- 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).' },
366
- { gpus: 50_000, name: 'GPT-5', row: 'a1',
367
- 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.' },
368
- ];
369
-
370
- const INFRA_LANDMARKS = [
371
- { gpus: 1, name: '1 GPU', row: 'b1',
372
- desc: '<b>NVIDIA H100 SXM</b><br>80 GB HBM3, 3.96 PFLOPS FP8.<br>The workhorse of modern AI training and inference.' },
373
- { gpus: 8, name: '1 node', row: 'b1',
374
- 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.' },
375
- { gpus: 32, name: '1 rack', row: 'b1',
376
- 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.' },
377
- { gpus: 256, name: 'SuperPOD', row: 'b1',
378
- 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.' },
379
- { gpus: 10_752, name: 'ALPS', row: 'b1',
380
- 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.' },
381
- { gpus: 12_288, name: 'ByteDance', row: 'b2',
382
- 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.' },
383
- { gpus: 64_000, name: 'Stargate', row: 'b1',
384
- 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.' },
385
- { gpus: 100_000, name: 'Tencent', row: 'b2',
386
- 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.' },
387
- { gpus: 200_000, name: 'Colossus', row: 'b1',
388
- 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.' },
389
- { gpus: 250_000, name: 'CoreWeave', row: 'b2',
390
- 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.' },
391
- { gpus: 600_000, name: 'Meta', row: 'b2',
392
- 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.' },
393
- { gpus: 1_000_000, name: 'Colossus 2', row: 'b1',
394
- 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.' },
395
- ];
396
-
397
- function gpusToSlider(gpus) { return (Math.log(Math.max(gpus, 1)) - LOG_MIN) / (LOG_MAX - LOG_MIN); }
398
- function sliderToGpus(val) { return Math.round(Math.exp(LOG_MIN + val * (LOG_MAX - LOG_MIN))); }
399
-
400
- const modelSelect = $('model');
401
- const gpuSlider = $('gpus');
402
- const gpuLabelEl = $('gpuLabel');
403
- const booksRateEl = $('booksRate');
404
- const tpsInlineEl = $('tpsInline');
405
- const totalTokensNumEl = $('totalTokensNum');
406
- const totalItemNumEl = $('totalItemNum');
407
- const totalItemUnitEl = $('totalItemUnit');
408
- const themeTokens = {};
409
-
410
- function refreshThemeTokens() {
411
- const styles = getComputedStyle(document.documentElement);
412
- themeTokens.sliderStart = styles.getPropertyValue('--slider-start').trim();
413
- themeTokens.sliderMid = styles.getPropertyValue('--slider-mid').trim();
414
- themeTokens.sliderEnd = styles.getPropertyValue('--slider-end').trim();
415
- themeTokens.sliderRest = styles.getPropertyValue('--slider-rest').trim();
416
- themeTokens.brand = styles.getPropertyValue('--brand').trim();
417
- themeTokens.canvasBg = styles.getPropertyValue('--canvas-bg').trim();
418
- themeTokens.canvasGrid = styles.getPropertyValue('--canvas-grid').trim();
419
- }
420
-
421
- refreshThemeTokens();
422
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
423
- refreshThemeTokens();
424
- updateSliderGradient();
425
- });
426
-
427
- let floatingItems = [], totalTokens = 0, spawnAccum = 0, lastTime = performance.now(), lastMetricUpdate = 0;
428
-
429
- function getGpuCount() { return sliderToGpus(parseFloat(gpuSlider.value)); }
430
- function getTps() { return parseInt(modelSelect.value) * getGpuCount(); }
431
- function getPps() { return getTps() / TOKENS_PER_PAGE; }
432
-
433
- function updateSliderGradient() {
434
- const val = parseFloat(gpuSlider.value) * 100;
435
- gpuSlider.style.background = 'linear-gradient(to right, '
436
- + themeTokens.sliderStart + ' 0%, '
437
- + themeTokens.sliderMid + ' ' + (val * 0.5) + '%, '
438
- + themeTokens.sliderEnd + ' ' + val + '%, '
439
- + themeTokens.sliderRest + ' ' + val + '%)';
440
- }
441
- updateSliderGradient();
442
- function updateGpuLabel() { gpuLabelEl.textContent = formatGpuLabel(getGpuCount()); }
443
- gpuSlider.addEventListener('input', () => { updateSliderGradient(); updateGpuLabel(); });
444
-
445
- function formatGpuLabel(gpus) {
446
- if (gpus >= 1_000_000) return (gpus / 1_000_000).toFixed(gpus >= 10_000_000 ? 0 : 1) + 'M GPUs';
447
- if (gpus >= 1000) return (gpus / 1000).toFixed(gpus >= 10000 ? 0 : 1) + 'K GPUs';
448
- return gpus + ' GPU' + (gpus > 1 ? 's' : '');
449
- }
450
-
451
- const rowEls = { a2: $('row-a2'), a1: $('row-a1'), b1: $('row-b1'), b2: $('row-b2') };
452
-
453
- function addLandmark(lm) {
454
- const pct = gpusToSlider(lm.gpus) * 100;
455
- const isAbove = lm.row.startsWith('a');
456
- const el = document.createElement('div');
457
- el.className = 'landmark';
458
- el.style.left = pct + '%';
459
- let tipClass = 'tooltip';
460
- let tipStyle = '';
461
- if (pct > 80) { tipClass += ' tip-right'; tipStyle = 'left:auto;right:0;transform:none;'; }
462
- else if (pct < 20) { tipClass += ' tip-left'; tipStyle = 'left:0;transform:none;'; }
463
- const tip = '<div class="' + tipClass + '" style="' + tipStyle + '">' + lm.desc + '</div>';
464
- el.innerHTML = isAbove
465
- ? '<div class="name">' + lm.name + '</div><div class="tick"></div>' + tip
466
- : '<div class="tick"></div><div class="name">' + lm.name + '</div>' + tip;
467
- el.addEventListener('click', () => { gpuSlider.value = gpusToSlider(lm.gpus); updateSliderGradient(); updateGpuLabel(); resetCountdown(); });
468
- rowEls[lm.row].appendChild(el);
469
- }
470
- TRAINING_RUNS.forEach(addLandmark);
471
- INFRA_LANDMARKS.forEach(addLandmark);
472
-
473
- const datasetBarsEl = $('datasetBars');
474
- const dsEls = DATASETS.map(ds => {
475
- const el = document.createElement('div');
476
- el.className = 'dataset-item';
477
-
478
- 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) + ' toks)</span><div class="ds-tip">' + ds.desc + '</div>';
479
- datasetBarsEl.appendChild(el);
480
- return el;
481
- });
482
-
483
- function resetCountdown() {
484
- dsDone = DATASETS.map(() => false);
485
- totalTokens = 0;
486
- floatingItems = [];
487
- spawnAccum = 0;
488
- dsEls.forEach(el => { el.classList.remove('done'); el.querySelector('.ds-check').textContent = '\u23f3'; el.querySelector('.ds-time').textContent = '-'; });
489
- }
490
-
491
- gpuSlider.addEventListener('input', () => { resetCountdown(); });
492
- modelSelect.addEventListener('change', resetCountdown);
493
-
494
- function formatNum(n) {
495
- if (n >= 1e15) return (n / 1e15).toFixed(1) + 'Q';
496
- if (n >= 1e12) return (n / 1e12).toFixed(1) + 'T';
497
- if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
498
- if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
499
- if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
500
- return Math.round(n).toString();
501
- }
502
-
503
- function formatNumInt(n) {
504
- const fmt = (v, s) => (v % 1 === 0 ? v.toFixed(0) : v.toFixed(1)) + s;
505
- if (n >= 1e15) return fmt(n / 1e15, 'Q');
506
- if (n >= 1e12) return fmt(n / 1e12, 'T');
507
- if (n >= 1e9) return fmt(n / 1e9, 'B');
508
- if (n >= 1e6) return fmt(n / 1e6, 'M');
509
- if (n >= 1e3) return fmt(n / 1e3, 'K');
510
- return Math.round(n).toString();
511
- }
512
-
513
- function formatDuration(seconds) {
514
- if (!isFinite(seconds) || seconds < 0) return '\u221e';
515
- if (seconds < 1) return '<1s';
516
- if (seconds < 60) return Math.round(seconds) + 's';
517
- if (seconds < 3600) return Math.round(seconds / 60) + 'm';
518
- if (seconds < 86400) return (seconds / 3600).toFixed(1) + 'h';
519
- if (seconds < 86400 * 365) return (seconds / 86400).toFixed(1) + 'd';
520
- const years = seconds / (86400 * 365);
521
- if (years < 1000) return years.toFixed(1) + 'y';
522
- if (years < 1e6) return (years / 1e3).toFixed(1) + 'Ky';
523
- if (years < 1e9) return (years / 1e6).toFixed(1) + 'My';
524
- return (years / 1e9).toFixed(1) + 'By';
525
- }
526
-
527
- function getW() { return canvas.width; }
528
- function getH() { return canvas.height; }
529
-
530
- // Per-frame cached state to avoid recomputing in nested draw calls
531
- let _fNow = 0, _fPps = 0, _fFanPhase = 0, _fBlinkSpeed = 0;
532
-
533
- function getHardwareLevel(gpus) {
534
- if (gpus < GPUS_PER_NODE) return 'gpu';
535
- if (gpus < GPUS_PER_RACK) return 'node';
536
- if (gpus < GPUS_PER_SUPERPOD) return 'rack';
537
- return 'cluster';
538
- }
539
-
540
- // Offscreen buffer for hardware rendering
541
- const hwCanvas = document.createElement('canvas');
542
- const hwCtx = hwCanvas.getContext('2d');
543
- let hwDirty = true, hwLastGpus = -1, hwLastW = 0, hwLastH = 0;
544
- gpuSlider.addEventListener('input', () => { hwDirty = true; });
545
- modelSelect.addEventListener('change', () => { hwDirty = true; });
546
-
547
- function drawHardware() {
548
- const W = getW(), H = getH();
549
- const gpus = getGpuCount();
550
-
551
- // Only re-render hardware if config or size changed, or animation needs update
552
- const needsAnim = _fPps > 0;
553
- if (hwCanvas.width !== W * 0.34 || hwCanvas.height !== H) {
554
- hwCanvas.width = Math.ceil(W * 0.34);
555
- hwCanvas.height = H;
556
- hwDirty = true;
557
- }
558
- // Animated hardware (fans/LEDs) redraws every frame when running
559
- if (gpus !== hwLastGpus || hwDirty || needsAnim) {
560
- hwLastGpus = gpus; hwDirty = false;
561
- const prevCtx = ctx;
562
- ctx = hwCtx;
563
- ctx.clearRect(0, 0, hwCanvas.width, hwCanvas.height);
564
-
565
- const pad = 10;
566
- const aw = hwCanvas.width - 2 * pad, ax = pad;
567
- const level = getHardwareLevel(gpus);
568
-
569
- const ah = hwCanvas.height * 0.75;
570
- const ay = (hwCanvas.height - ah) / 2;
571
- ctx.textAlign = 'start'; ctx.textBaseline = 'alphabetic';
572
-
573
- if (level === 'gpu') drawGpuGrid(gpus, ax, ay, aw, ah);
574
- else if (level === 'node') drawNodeGrid(Math.max(1, Math.round(gpus / GPUS_PER_NODE)), ax, ay, aw, ah);
575
- else if (level === 'rack') drawRackGrid(Math.max(1, Math.round(gpus / GPUS_PER_RACK)), ax, ay, aw, ah);
576
- else drawPodGrid(Math.round(gpus / GPUS_PER_SUPERPOD), ax, ay, aw, ah);
577
-
578
- ctx = prevCtx;
579
- }
580
- ctx.drawImage(hwCanvas, 0, 0);
581
- }
582
-
583
- function drawGpuGrid(count, ax, ay, aw, ah) {
584
- let cols, rows;
585
- if (count === 1) { cols = 1; rows = 1; }
586
- else if (count === 2) { cols = 2; rows = 1; }
587
- else if (count <= 4) { cols = 2; rows = 2; }
588
- else { cols = 4; rows = 2; }
589
- const gap = 3;
590
- const gw = Math.min((aw - gap * (cols - 1)) / cols, ((ah - gap * (rows - 1)) / rows) * 0.6);
591
- const gh = gw / 0.6;
592
- const offX = ax + (aw - (cols * gw + (cols - 1) * gap)) / 2;
593
- const offY = ay + (ah - (rows * gh + (rows - 1) * gap)) / 2;
594
- const fanSpeed = _fFanPhase;
595
- for (let i = 0; i < count; i++) {
596
- const c = i % cols, r = Math.floor(i / cols);
597
- drawSingleGpu(offX + c * (gw + gap), offY + r * (gh + gap), gw, gh, fanSpeed + i * 0.5);
598
- }
599
- }
600
-
601
- function drawSingleGpu(x, y, gw, gh, fanPhase) {
602
- ctx.fillStyle = '#2a2a2a'; ctx.fillRect(x, y, gw, gh);
603
- ctx.strokeStyle = '#000'; ctx.lineWidth = 1.5; ctx.strokeRect(x, y, gw, gh);
604
- if (gw < 12) { ctx.fillStyle = '#1a1a1a'; ctx.fillRect(x + gw * 0.15, y + gw * 0.08, gw * 0.7, gw * 0.7); return; }
605
- const fs = gw * 0.7, fx = x + (gw - fs) / 2, fy = y + gw * 0.08;
606
- ctx.fillStyle = '#1a1a1a'; ctx.fillRect(fx, fy, fs, fs);
607
- const cx = fx + fs / 2, cy = fy + fs / 2, fr = fs / 2 - 2;
608
- ctx.beginPath(); ctx.arc(cx, cy, fr, 0, Math.PI * 2); ctx.fillStyle = '#333'; ctx.fill();
609
- if (gw >= 20) {
610
- const br = fr - 2; ctx.save(); ctx.translate(cx, cy); ctx.rotate(fanPhase);
611
- const n = gw >= 40 ? 7 : 5;
612
- for (let b = 0; b < n; b++) {
613
- ctx.rotate(Math.PI * 2 / n); ctx.beginPath(); ctx.moveTo(0, 0);
614
- ctx.quadraticCurveTo(br * 0.5, br * 0.3, br * 0.85, 0);
615
- ctx.quadraticCurveTo(br * 0.5, -br * 0.3, 0, 0);
616
- ctx.fillStyle = '#555'; ctx.fill();
617
- }
618
- ctx.restore();
619
- }
620
- const hy = fy + fs + 2, hh = gh - (hy - y) - gw * 0.15;
621
- if (hh > 4) { const lh = Math.max(1, Math.min(4, hh / 6)), lg = lh * 1.5;
622
- 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); } }
623
- 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;
624
- for (let p = 0; p < pc; p++) ctx.fillRect(ps + p * pw * 2, y + gh, pw, Math.max(2, gw * 0.08)); }
625
- }
626
-
627
- // Grid of DGX nodes
628
- function drawNodeGrid(count, ax, ay, aw, ah) {
629
- const gap = 6;
630
- const cols = count <= 2 ? 1 : 2;
631
- const rows = Math.ceil(count / cols);
632
- const nw = (aw - gap * (cols - 1)) / cols;
633
- const nh = Math.min((ah - gap * (rows - 1)) / rows, aw * 0.35);
634
- const tw = cols * nw + (cols - 1) * gap;
635
- const th = rows * nh + (rows - 1) * gap;
636
- const ox = ax + (aw - tw) / 2, oy = ay + (ah - th) / 2;
637
- for (let i = 0; i < count; i++) {
638
- const c = i % cols, r = Math.floor(i / cols);
639
- drawSingleNode(ox + c * (nw + gap), oy + r * (nh + gap), nw, nh);
640
- }
641
- }
642
-
643
- function drawSingleNode(nx, ny, nw, nh) {
644
- ctx.fillStyle = '#1a1a2e';
645
- ctx.beginPath(); ctx.roundRect(nx, ny, nw, nh, 4); ctx.fill();
646
- ctx.strokeStyle = '#555'; ctx.lineWidth = 2;
647
- ctx.beginPath(); ctx.roundRect(nx, ny, nw, nh, 4); ctx.stroke();
648
- const slotW = (nw - 20) / 8, slotH = nh * 0.6;
649
- const slotY = ny + (nh - slotH) / 2;
650
- for (let i = 0; i < 8; i++) {
651
- const sx = nx + 10 + i * slotW;
652
- drawSingleGpu(sx + 1, slotY, slotW - 2, slotH, _fFanPhase + i * 0.5);
653
- }
654
- const ledAlpha = 0.4 + 0.6 * (0.5 + 0.5 * Math.sin(_fNow / (300 - _fBlinkSpeed * 1.2)));
655
- ctx.globalAlpha = ledAlpha;
656
- ctx.fillStyle = '#4ade80';
657
- ctx.beginPath(); ctx.arc(nx + 8, ny + 8, 3, 0, Math.PI * 2); ctx.fill();
658
- ctx.globalAlpha = 0.4 + 0.6 * (0.5 + 0.5 * Math.sin(_fNow / (400 - _fBlinkSpeed) + 1.5));
659
- ctx.fillStyle = '#60a5fa';
660
- ctx.beginPath(); ctx.arc(nx + 18, ny + 8, 3, 0, Math.PI * 2); ctx.fill();
661
- ctx.globalAlpha = 1;
662
- }
663
-
664
- // Grid of racks
665
- function drawRackGrid(count, ax, ay, aw, ah) {
666
- const gap = 4;
667
- const cols = count <= 3 ? count : Math.min(4, Math.ceil(Math.sqrt(count)));
668
- const rows = Math.ceil(count / cols);
669
- const rw = (aw - gap * (cols - 1)) / cols;
670
- const rh = Math.min((ah - gap * (rows - 1)) / rows, ah * 0.95);
671
- const tw = cols * rw + (cols - 1) * gap;
672
- const th = rows * rh + (rows - 1) * gap;
673
- const ox = ax + (aw - tw) / 2, oy = ay + (ah - th) / 2;
674
- for (let i = 0; i < count; i++) {
675
- const c = i % cols, r = Math.floor(i / cols);
676
- drawSingleRack(ox + c * (rw + gap), oy + r * (rh + gap), rw, rh);
677
- }
678
- }
679
-
680
- function drawSingleRack(rx, ry, rw, rh) {
681
- ctx.fillStyle = '#111'; ctx.beginPath(); ctx.roundRect(rx, ry, rw, rh, 4); ctx.fill();
682
- ctx.strokeStyle = '#555'; ctx.lineWidth = 2;
683
- ctx.beginPath(); ctx.roundRect(rx, ry, rw, rh, 4); ctx.stroke();
684
- const nodeCount = 4, pad = 4, gap = 3;
685
- const nodeH = (rh - 2 * pad - (nodeCount - 1) * gap) / nodeCount;
686
- const nodeW = rw - 2 * pad;
687
- for (let n = 0; n < nodeCount; n++) {
688
- const ny = ry + pad + n * (nodeH + gap), nx = rx + pad;
689
- ctx.fillStyle = '#1a1a2e';
690
- ctx.beginPath(); ctx.roundRect(nx, ny, nodeW, nodeH, 2); ctx.fill();
691
- ctx.strokeStyle = '#444'; ctx.lineWidth = 1;
692
- ctx.beginPath(); ctx.roundRect(nx, ny, nodeW, nodeH, 2); ctx.stroke();
693
- const slotCount = 8, slotW = (nodeW - 6) / slotCount;
694
- for (let g = 0; g < slotCount; g++) {
695
- const gx = nx + 3 + g * slotW, gy = ny + 2;
696
- ctx.fillStyle = '#2a2a2a'; ctx.fillRect(gx, gy, slotW - 1, nodeH - 4);
697
- const fcx = gx + (slotW - 1) / 2, fcy = gy + (nodeH - 4) * 0.35;
698
- const fr = Math.min((slotW - 1) * 0.35, (nodeH - 4) * 0.25);
699
- if (fr > 1) {
700
- ctx.beginPath(); ctx.arc(fcx, fcy, fr, 0, Math.PI * 2); ctx.fillStyle = '#444'; ctx.fill();
701
- ctx.save(); ctx.translate(fcx, fcy); ctx.rotate(_fFanPhase + g * 0.3 + n);
702
- for (let b = 0; b < 4; b++) { ctx.rotate(Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, 0);
703
- ctx.lineTo(fr * 0.8, fr * 0.3); ctx.lineTo(fr * 0.8, -fr * 0.3); ctx.fillStyle = '#666'; ctx.fill(); }
704
- ctx.restore();
705
- }
706
- }
707
- const rackLedAlpha = 0.3 + 0.7 * (0.5 + 0.5 * Math.sin(_fNow / (350 - _fBlinkSpeed) + n * 1.2));
708
- ctx.globalAlpha = rackLedAlpha;
709
- ctx.fillStyle = '#4ade80';
710
- ctx.beginPath(); ctx.arc(nx + nodeW - 6, ny + nodeH / 2, Math.min(2, nodeH * 0.15), 0, Math.PI * 2); ctx.fill();
711
- ctx.globalAlpha = 1;
712
- }
713
- }
714
-
715
- // SuperPod grid: draw abstract pods
716
- function drawPodGrid(pods, ax, ay, aw, ah) {
717
- const vn = Math.min(pods, 1024);
718
- let cols, rows;
719
- if (vn <= 2) { cols = 1; rows = vn; } else if (vn <= 4) { cols = 2; rows = 2; }
720
- else if (vn <= 8) { cols = 2; rows = 4; } else if (vn <= 16) { cols = 4; rows = 4; }
721
- else if (vn <= 32) { cols = 4; rows = 8; } else if (vn <= 64) { cols = 8; rows = 8; }
722
- else if (vn <= 128) { cols = 8; rows = 16; } else if (vn <= 256) { cols = 16; rows = 16; }
723
- else if (vn <= 512) { cols = 16; rows = 32; } else { cols = 32; rows = 32; }
724
- const gap = vn <= 16 ? 3 : vn <= 64 ? 2 : 1;
725
- const cw = (aw - gap * (cols - 1)) / cols, ch = (ah - gap * (rows - 1)) / rows;
726
- const tw = cols * cw + (cols - 1) * gap, th = rows * ch + (rows - 1) * gap;
727
- const ox = ax + (aw - tw) / 2, oy = ay + (ah - th) / 2;
728
- for (let i = 0; i < vn; i++) {
729
- const c = i % cols, r = Math.floor(i / cols);
730
- drawPodUnit(ox + c * (cw + gap), oy + r * (ch + gap), cw, ch, vn);
731
- }
732
- if (pods > vn) {
733
- ctx.fillStyle = 'rgba(0,0,0,0.7)';
734
- const lh = 24, lw = aw - 20, lx = ax + (aw - lw) / 2, ly = ay + ah - lh - 5;
735
- ctx.fillRect(lx, ly, lw, lh); ctx.fillStyle = '#fff';
736
- ctx.font = 'bold 12px system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
737
- const ns = pods >= 1000 ? (pods / 1000).toFixed(pods >= 10000 ? 0 : 1) + 'K' : pods.toLocaleString();
738
- ctx.fillText(ns + ' SuperPODs', lx + lw / 2, ly + lh / 2);
739
- ctx.textAlign = 'start'; ctx.textBaseline = 'alphabetic';
740
- }
741
- }
742
-
743
- function drawPodUnit(x, y, nw, nh, total) {
744
- const r = Math.min(2, nw * 0.1);
745
- ctx.fillStyle = '#1a1a2e'; ctx.beginPath(); ctx.roundRect(x, y, nw, nh, r); ctx.fill();
746
- ctx.strokeStyle = '#444'; ctx.lineWidth = total <= 16 ? 1 : 0.5;
747
- ctx.beginPath(); ctx.roundRect(x, y, nw, nh, r); ctx.stroke();
748
- if (nw < 4) return;
749
- const lr = Math.max(0.8, Math.min(2, nw * 0.06)), ly = y + nh * 0.2;
750
- // Skip per-unit LED animation for large grids (> 64 pods) to save draw calls
751
- if (total > 64) {
752
- ctx.fillStyle = '#4ade80'; ctx.beginPath(); ctx.arc(x + nw * 0.2, ly, lr, 0, Math.PI * 2); ctx.fill();
753
- } else {
754
- const podHash = (x * 31 + y * 17) & 0xffff;
755
- ctx.globalAlpha = 0.3 + 0.7 * (0.5 + 0.5 * Math.sin(_fNow / (400 - _fBlinkSpeed) + podHash));
756
- ctx.fillStyle = '#4ade80'; ctx.beginPath(); ctx.arc(x + nw * 0.2, ly, lr, 0, Math.PI * 2); ctx.fill();
757
- if (nw >= 8) {
758
- ctx.globalAlpha = 0.3 + 0.7 * (0.5 + 0.5 * Math.sin(_fNow / (350 - _fBlinkSpeed) + podHash + 2));
759
- ctx.fillStyle = '#60a5fa'; ctx.beginPath(); ctx.arc(x + nw * 0.4, ly, lr, 0, Math.PI * 2); ctx.fill();
760
- }
761
- if (nw >= 6) {
762
- ctx.globalAlpha = 0.3 + 0.7 * (0.5 + 0.5 * Math.sin(_fNow / (300 - _fBlinkSpeed) + podHash + 4));
763
- ctx.fillStyle = '#c4a020'; ctx.beginPath(); ctx.arc(x + nw * 0.6, ly, lr, 0, Math.PI * 2); ctx.fill();
764
- }
765
- ctx.globalAlpha = 1;
766
- }
767
- if (nw >= 10) {
768
- const vy = y + nh * 0.45, vh = nh * 0.4, lc = Math.min(6, Math.floor(nw / 4));
769
- ctx.strokeStyle = '#333'; ctx.lineWidth = 0.5;
770
- 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(); }
771
- }
772
- }
773
-
774
- function getVisualMode() {
775
- const bps = getPps() / PAGES_PER_BOOK;
776
- if (bps >= BOOKS_PER_SHELF) return 'shelf';
777
- if (bps >= 1) return 'book';
778
- return 'page';
779
- }
780
-
781
- const EMOJI = { page: '\u{1F4C4}', book: '\u{1F4D6}', shelf: '\u{1F4DA}' };
782
- const EMOJI_SIZE = { page: 28, book: 34, shelf: 40 };
783
-
784
- // Pre-render emojis at multiple scales to offscreen canvases
785
- const emojiCache = {};
786
- function getEmojiCanvas(type, scale) {
787
- const sz = Math.round(EMOJI_SIZE[type] * scale);
788
- const key = type + '_' + sz;
789
- if (emojiCache[key]) return emojiCache[key];
790
- const off = document.createElement('canvas');
791
- off.width = sz + 4; off.height = sz + 4;
792
- const octx = off.getContext('2d');
793
- octx.font = sz + 'px system-ui, sans-serif';
794
- octx.textBaseline = 'top';
795
- octx.fillText(EMOJI[type], 2, 2);
796
- emojiCache[key] = off;
797
- return off;
798
- }
799
-
800
- function drawItem(p, now, fadeStart, fadeRange) {
801
- const off = getEmojiCanvas(p.type, p.scale);
802
- const bobY = p.y + Math.sin(now / 600 + p.bobPhase) * p.bobAmp;
803
- const angle = Math.sin(now / 400 + p.wobblePhase) * p.wobbleAmp;
804
- const needsAlpha = p.x > fadeStart;
805
- if (needsAlpha) ctx.globalAlpha = 1 - (p.x - fadeStart) / fadeRange;
806
- if (Math.abs(angle) < 0.01) {
807
- ctx.drawImage(off, p.x, bobY);
808
- } else {
809
- ctx.save();
810
- ctx.translate(p.x + off.width / 2, bobY + off.height / 2);
811
- ctx.rotate(angle);
812
- ctx.drawImage(off, -off.width / 2, -off.height / 2);
813
- ctx.restore();
814
- }
815
- if (needsAlpha) ctx.globalAlpha = 1;
816
- }
817
-
818
- function spawnItem() {
819
- const H = getH(), W = getW();
820
- const spawnX = W * 0.34;
821
- const margin = H * 0.12;
822
- const yMin = margin + H * 0.04, yMax = H - margin + H * 0.04, y = yMin + Math.random() * (yMax - yMin);
823
- const mode = getVisualMode();
824
- const pps = getPps();
825
- const pagesPerShelf = PAGES_PER_BOOK * BOOKS_PER_SHELF;
826
- const rate = mode === 'shelf' ? pps / pagesPerShelf : mode === 'book' ? pps / PAGES_PER_BOOK : pps;
827
- const bs = 0.8 + Math.log10(Math.max(1, rate)) * 0.8;
828
- const tokens = mode === 'shelf' ? TOKENS_PER_SHELF : mode === 'book' ? TOKENS_PER_BOOK : TOKENS_PER_PAGE;
829
- const scale = 0.85 + Math.random() * 0.3;
830
- const bobAmp = 3 + Math.random() * 5;
831
- const bobPhase = Math.random() * Math.PI * 2;
832
- const wobbleAmp = mode === 'page' ? 0.15 + Math.random() * 0.25 : 0.08 + Math.random() * 0.15;
833
- const wobblePhase = Math.random() * Math.PI * 2;
834
- const depthSpeed = (bs + Math.random() * bs) * (0.7 + scale * 0.6);
835
- totalTokens += tokens;
836
- floatingItems.push({
837
- x: spawnX + Math.random() * 20, y, speed: depthSpeed,
838
- type: mode, scale, bobAmp, bobPhase, wobbleAmp, wobblePhase
839
- });
840
- }
841
-
842
- function frame(now) {
843
- const dt = Math.min((now - lastTime) / 1000, 0.1); lastTime = now;
844
- const W = getW(), H = getH();
845
-
846
- // Cache per-frame values for nested draw calls
847
- _fNow = now;
848
- _fPps = getPps();
849
- _fFanPhase = (now / 200) * Math.min(_fPps, 100);
850
- _fBlinkSpeed = Math.min(_fPps, 200);
851
-
852
- ctx.clearRect(0, 0, W, H);
853
- ctx.fillStyle = themeTokens.canvasBg; ctx.fillRect(0, 0, W, H);
854
-
855
- const spawnX = W * 0.34;
856
- ctx.strokeStyle = themeTokens.canvasGrid; ctx.lineWidth = 1; ctx.setLineDash([4, 8]);
857
- const gridMargin = H * 0.12;
858
- const gridOffset = H * 0.04;
859
- for (let y = gridMargin + gridOffset; y < H - gridMargin + gridOffset; y += 40) { ctx.beginPath(); ctx.moveTo(spawnX, y); ctx.lineTo(W - 20, y); ctx.stroke(); }
860
- ctx.setLineDash([]);
861
-
862
- const pps = _fPps, mode = getVisualMode();
863
- const pagesPerShelf = PAGES_PER_BOOK * BOOKS_PER_SHELF;
864
- const spawnRate = mode === 'shelf' ? Math.min(pps / pagesPerShelf, 60) : mode === 'book' ? Math.min(pps / PAGES_PER_BOOK, 120) : Math.min(pps, 400);
865
- spawnAccum += spawnRate * dt;
866
- while (spawnAccum >= 1) { spawnItem(); spawnAccum -= 1; }
867
-
868
- const fadeStart = W * 0.9;
869
- const fadeRange = W - fadeStart;
870
- const cullX = W + 40;
871
-
872
- // Update positions and cull off-screen items
873
- let writeIdx = 0;
874
- for (let i = 0; i < floatingItems.length; i++) {
875
- const p = floatingItems[i];
876
- p.x += p.speed;
877
- if (p.x > cullX) continue;
878
- floatingItems[writeIdx++] = p;
879
- }
880
- floatingItems.length = writeIdx;
881
-
882
- // Sort by cache key so repeated drawImage calls hit the same source texture
883
- floatingItems.sort((a, b) => {
884
- const ka = EMOJI_SIZE[a.type] * 100 + Math.round(a.scale * 30);
885
- const kb = EMOJI_SIZE[b.type] * 100 + Math.round(b.scale * 30);
886
- return ka - kb;
887
- });
888
-
889
- for (let i = 0; i < floatingItems.length; i++) {
890
- drawItem(floatingItems[i], now, fadeStart, fadeRange);
891
- }
892
-
893
- drawHardware();
894
-
895
- // Throttle DOM metric updates to ~4 Hz to prevent flickering
896
- if (now - lastMetricUpdate > 100) {
897
- lastMetricUpdate = now;
898
- const tps = getTps();
899
- const booksPerSecond = pps / PAGES_PER_BOOK;
900
- const shelvesPerSecond = booksPerSecond / BOOKS_PER_SHELF;
901
- if (shelvesPerSecond >= 1) {
902
- booksRateEl.textContent = formatNum(shelvesPerSecond) + ' shelves/sec';
903
- } else if (booksPerSecond >= 1) {
904
- booksRateEl.textContent = formatNum(booksPerSecond) + ' books/sec';
905
- } else {
906
- booksRateEl.textContent = formatNum(pps) + ' pages/sec';
907
- }
908
- tpsInlineEl.textContent = '(' + formatNum(tps) + ' TPS)';
909
- totalTokensNumEl.textContent = formatNum(totalTokens);
910
- const totalShelves = totalTokens / TOKENS_PER_SHELF;
911
- const totalBooks = totalTokens / TOKENS_PER_BOOK;
912
- const totalPages = totalTokens / TOKENS_PER_PAGE;
913
- if (totalShelves >= 1) {
914
- totalItemNumEl.textContent = formatNum(totalShelves);
915
- totalItemUnitEl.textContent = 'πŸ“š';
916
- } else if (totalBooks >= 1) {
917
- totalItemNumEl.textContent = formatNum(totalBooks);
918
- totalItemUnitEl.textContent = 'πŸ“–';
919
- } else {
920
- totalItemNumEl.textContent = formatNum(totalPages);
921
- totalItemUnitEl.textContent = 'πŸ“„';
922
- }
923
-
924
- for (let i = 0; i < DATASETS.length; i++) {
925
- if (dsDone[i]) continue;
926
- const remaining = DATASETS[i].tokens - totalTokens;
927
- if (remaining <= 0) {
928
- dsDone[i] = true;
929
- dsEls[i].classList.add('done');
930
- dsEls[i].querySelector('.ds-check').textContent = '\u2705';
931
- dsEls[i].querySelector('.ds-time').textContent = 'done';
932
- } else {
933
- dsEls[i].querySelector('.ds-time').textContent = formatDuration(remaining / tps);
934
- }
935
- }
936
- }
937
-
938
- requestAnimationFrame(frame);
939
- }
940
-
941
- requestAnimationFrame(frame);
942
- })();
943
- </script>
944
- </body>
945
- </html>