Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
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"
|
| 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
|
| 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"
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 516 |
-
|
| 517 |
|
| 518 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1027 |
-
|
|
|
|
| 1028 |
|
| 1029 |
if (now - lastMetricUpdate > 100) {
|
| 1030 |
lastMetricUpdate = now;
|
| 1031 |
instA.updateMetrics(now);
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
|
|
|
|
|
|
|
|
|
| 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|