diff --git a/packages/app/cypress/e2e/navigation.cy.ts b/packages/app/cypress/e2e/navigation.cy.ts index 63cebe8af..10bfa30a6 100644 --- a/packages/app/cypress/e2e/navigation.cy.ts +++ b/packages/app/cypress/e2e/navigation.cy.ts @@ -75,8 +75,16 @@ describe('First-load navigation', () => { cy.location('pathname').should('eq', '/compare'); }); - it('navigates to dashboard from the landing CTA with one click', () => { - cy.contains('a', 'Open Dashboard').click(); + it('navigates to overview and the full dashboard from the landing CTAs', () => { + cy.get('[data-testid="landing-overview-link"]') + .should('have.attr', 'href', '/overview') + .click(); + cy.location('pathname').should('eq', '/overview'); + + cy.visit('/'); + cy.get('[data-testid="landing-full-dashboard-link"]') + .should('have.attr', 'href', '/inference') + .click(); cy.location('pathname').should('eq', '/inference'); }); diff --git a/packages/app/cypress/e2e/overview.cy.ts b/packages/app/cypress/e2e/overview.cy.ts index 903372f66..ee8391201 100644 --- a/packages/app/cypress/e2e/overview.cy.ts +++ b/packages/app/cypress/e2e/overview.cy.ts @@ -9,13 +9,8 @@ const MODEL_LABELS = [ 'Qwen3.5 397B', ]; -// Models whose row collapses to a coverage note instead of 5 platform cells: -// GLM5.2 (fixtures carry no 8K/1K rows) and Kimi K3 (no fixture rows at all — -// nothing ingested yet). -const ZERO_COVERAGE_MODELS = 2; - const PLATFORM_HEADERS = [ - 'Model', + 'Model · Scenario', 'B200 · Reference', 'MI355X', 'B300', @@ -96,17 +91,15 @@ describe('Overview page', () => { }); }); - desktopModel('GLM-5.2') - .find('[data-testid="overview-model-coverage-note"]') - .should('have.text', 'No 8K/1K results'); - desktopModel('GLM-5.2').find('[data-testid="overview-pair-missing"]').should('not.exist'); + desktopModel('GLM-5.2').within(() => { + cy.get('[data-testid="overview-model-scenario"]').should('have.text', 'AgentX'); + cy.get('[data-testid="overview-pair-missing"]').should('have.length', 5); + }); cy.get( '[data-testid="overview-engine-scope-switcher"] [data-overview-engine-scope="all"]', ).click(); cy.location('search').should('eq', '?engine=all'); - desktopModel('GLM-5.2') - .find('[data-testid="overview-model-coverage-note"]') - .should('have.text', 'No 8K/1K results'); + desktopModel('GLM-5.2').find('[data-testid="overview-pair-missing"]').should('have.length', 5); cy.get('[data-testid="overview-tier-switcher"]') .contains('a', '100') .should('have.attr', 'href', '/overview?tier=100&engine=all') @@ -134,42 +127,31 @@ describe('Overview page', () => { }); }); - it('shows cost per Mtok with standard-decode and FP8 labels', () => { + it('prefers speculative decode and falls back to labelled standard-decode reads', () => { cy.viewport(1280, 900); cy.visit('/overview'); desktopModel('Kimi-K2.5').within(() => { - platform('b200') - .should('contain.text', '$1.01') - .and('contain.text', 'SGLang · FP4 · Standard decode'); - platform('mi355x').within(() => { - cy.get('[data-testid="overview-pair-value"][data-hardware="mi355x"]') - .should('have.text', '$0.58') - .and('not.contain.text', '≈') - .and('not.have.attr', 'title'); - cy.get('[data-testid="overview-pair-value"][data-hardware="mi355x"] a').should('not.exist'); - cy.get('[data-testid="overview-pair-evidence-date"][data-hardware="mi355x"]') - .find('a') - .should('have.attr', 'href') - .and('include', 'i_spec=stp') - .and('include', 'i_metric=y_outputTputPerGpu'); - cy.get('[data-testid="overview-pair-evidence-date"][data-hardware="mi355x"]') - .find('a') - .should('have.attr', 'aria-label') - .and('include', 'Open raw source dashboard'); - cy.contains('SGLang · FP4 · Standard decode').should('exist'); - }); - platform('b300').within(() => { - cy.get('[data-testid="overview-pair-value"][data-hardware="b300"]').should( - 'contain.text', - '$1.07', - ); - cy.contains('SGLang · FP8 · Standard decode').should('exist'); - }); - cy.get('a[title*="NONE"], a[aria-label*="NONE"]').should('not.exist'); + cy.get('[data-testid="overview-pair-value"]').should('have.length', 3); + cy.get('[data-testid="overview-pair-missing"]') + .should('have.length', 2) + .then(($missing) => { + expect([...$missing].map((element) => element.getAttribute('title'))).to.deep.equal([ + 'no data for this scenario', + 'no data for this scenario', + ]); + }); + cy.get('[data-testid="overview-pair-missing"]') + .children('[aria-hidden="true"]') + .should('have.length', 2) + .and('have.text', '∞∞'); + platform('b200').should('contain.text', 'Standard decode'); + platform('mi355x').should('contain.text', 'Standard decode'); + platform('b300').should('contain.text', 'Standard decode'); }); desktopModel('DeepSeek-V4-Pro').within(() => { + cy.contains('MTP').should('exist'); platform('gb300').within(() => { cy.get('[data-testid="overview-pair-value"][data-hardware="gb300"]') .should('have.attr', 'title', 'Estimated from validated benchmark runs.') @@ -183,28 +165,23 @@ describe('Overview page', () => { cy.get('[data-testid="overview-pair-missing"]').should('not.exist'); }); }); + cy.contains( + 'Priority: speculative FP4 → speculative FP8 → standard FP4 → standard FP8.', + ).should('exist'); + cy.get('body').should('not.contain.text', 'P90'); }); it('color-grades the cost delta against B200 and omits it without a baseline', () => { cy.viewport(1280, 900); cy.visit('/overview'); - desktopModel('Kimi-K2.5').within(() => { + desktopModel('Qwen-3.5-397B-A17B').within(() => { platform('b200').find('[data-testid="overview-cost-delta"]').should('not.exist'); platform('mi355x') .find('[data-testid="overview-cost-delta"]') - .should('have.text', '-42%42% cheaper than B200') .and('have.attr', 'data-cost-polarity', 'cheaper') - .and('have.attr', 'title', '42% cheaper than B200') .then(($badge) => { - expect($badge.attr('style')).to.contain('rgb(16 185 129 / 0.35)'); - }); - platform('b300') - .find('[data-testid="overview-cost-delta"]') - .should('contain.text', '+7%') - .and('have.attr', 'data-cost-polarity', 'pricier') - .then(($badge) => { - expect($badge.attr('style')).to.contain('rgb(239 68 68 / 0.12)'); + expect($badge.attr('style')).to.contain('rgb(16 185 129 /'); }); }); @@ -238,11 +215,14 @@ describe('Overview page', () => { 'Every active model across MI355X, B200, B300, GB200 and GB300 at a glance.', ).should('exist'); cy.contains( - 'Cost per million output tokens from the best observed platform serving envelopes', - ).should('exist'); - cy.contains( - 'GPU rental cost / 1M output tokens · 8K→1K · @50 tok/s/user · ↓ lower is better', + 'Cost per million output tokens from each platform’s best observed serving envelope', ).should('exist'); + cy.get('[data-testid="overview-scope"]') + .should( + 'have.text', + 'GPU rental cost / 1M output tokens · @50 tok/s/user · ↓ lower is better', + ) + .and('not.contain.text', '8K→1K'); cy.contains( 'Cost = 3-yr rental $/GPU/hr ÷ output tok/s per deployed GPU. All percentages compare against B200.', ).should('exist'); @@ -269,17 +249,24 @@ describe('Overview page', () => { ); }); cy.get('[data-testid="overview-desktop-model"]').should('have.length', MODEL_LABELS.length); - // Zero-coverage rows collapse to a single note instead of 5 cells. - cy.get('[data-testid="overview-platform"]').should( - 'have.length', - (MODEL_LABELS.length - ZERO_COVERAGE_MODELS) * 5, - ); + cy.get('[data-testid="overview-platform"]').should('have.length', MODEL_LABELS.length * 5); + cy.get('[data-testid="overview-model-coverage-note"]').should('not.exist'); cy.get('details, summary, button').should('not.exist'); cy.contains(/PRIMARY|Ranked results/).should('not.exist'); }); for (const label of MODEL_LABELS) { cy.get('[data-testid="overview-desktop-matrix"]').should('contain.text', label); } + for (const model of ['Kimi-K3', 'GLM-5.2']) { + desktopModel(model) + .find('[data-testid="overview-model-scenario"]') + .should('have.text', 'AgentX'); + } + for (const model of ['DeepSeek-V4-Pro', 'Kimi-K2.5', 'MiniMax-M3', 'Qwen-3.5-397B-A17B']) { + desktopModel(model) + .find('[data-testid="overview-model-scenario"]') + .should('have.text', 'Single-turn · 8K→1K'); + } }); it('shows per-cell best reads with precision badges, dates, and evidence links', () => { @@ -344,7 +331,7 @@ describe('Overview page', () => { platform('b200') .find('[data-testid="overview-pair-missing"]') .should('contain.text', '∞') - .and('have.attr', 'title', 'no 8K/1K data'); + .and('have.attr', 'title', 'no data for this scenario'); platform('gb300').within(() => { cy.get('[data-testid="overview-pair-value"][data-hardware="gb300"]').should( 'contain.text', @@ -353,9 +340,10 @@ describe('Overview page', () => { }); }); - desktopModel('GLM-5.2') - .find('[data-testid="overview-model-coverage-note"]') - .should('have.text', 'No 8K/1K results'); + desktopModel('GLM-5.2').within(() => { + cy.get('[data-testid="overview-pair-missing"]').should('have.length', 5); + cy.get('[data-testid="overview-model-coverage-note"]').should('not.exist'); + }); cy.contains('∞ = no comparable result').should('exist'); cy.get('body') .invoke('text') @@ -374,9 +362,10 @@ describe('Overview page', () => { }); cy.location('search').should('eq', '?tier=100'); - cy.contains( - 'GPU rental cost / 1M output tokens · 8K→1K · @100 tok/s/user · ↓ lower is better', - ).should('exist'); + cy.get('[data-testid="overview-scope"]').should( + 'have.text', + 'GPU rental cost / 1M output tokens · @100 tok/s/user · ↓ lower is better', + ); cy.get('[data-testid="overview-tier-switcher"]').within(() => { cy.get('[aria-current="page"]').should('have.text', '100'); cy.contains('a', '50').should('have.attr', 'href', '/overview'); @@ -414,17 +403,16 @@ describe('Overview page', () => { .should('not.match', /∞\s*%/); cy.visit('/overview?tier=100'); - cy.contains( - 'GPU rental cost / 1M output tokens · 8K→1K · @100 tok/s/user · ↓ lower is better', - ).should('exist'); + cy.get('[data-testid="overview-scope"]').should( + 'have.text', + 'GPU rental cost / 1M output tokens · @100 tok/s/user · ↓ lower is better', + ); cy.get('[data-testid="language-toggle"]') .should('have.attr', 'href', '/zh/overview?tier=100') .click(); cy.location('pathname').should('eq', '/zh/overview'); cy.location('search').should('eq', '?tier=100'); - cy.contains('GPU 租赁成本 / 每百万输出 token · 8K→1K · @100 tok/s/用户 · ↓ 越低越好').should( - 'exist', - ); + cy.contains('GPU 租赁成本 / 每百万输出 token · @100 tok/s/用户 · ↓ 越低越好').should('exist'); }); it('uses the same cell semantics on mobile and fits both 390px and 320px widths', () => { @@ -551,7 +539,7 @@ describe('Overview page', () => { cy.viewport(390, 844); cy.visit('/overview'); - mobileModel('Kimi-K2.5').within(() => { + mobileModel('Qwen-3.5-397B-A17B').within(() => { platform('mi355x').within(() => { cy.get('[data-testid="overview-pair-value"][data-hardware="mi355x"]').then(([value]) => { cy.get('[data-testid="overview-pair-evidence-date"][data-hardware="mi355x"]').then( @@ -565,7 +553,7 @@ describe('Overview page', () => { }); cy.get('[data-testid="overview-pair-value"][data-hardware="mi355x"]').then(([value]) => { - cy.contains('div', 'SGLang · FP4 · Standard decode').then(([metadata]) => { + cy.contains('div', 'SGLang · FP8 · MTP').then(([metadata]) => { const valueRect = textRect(value); const metadataRect = textRect(metadata); @@ -618,10 +606,13 @@ describe('Overview page', () => { cy.contains('一眼对比各活跃模型在 MI355X、B200、B300、GB200 与 GB300 上的表现。').should( 'exist', ); - cy.contains('基于最佳观测平台服务包络线计算的各活跃模型每百万输出 token 成本').should('exist'); - cy.contains('GPU 租赁成本 / 每百万输出 token · 8K→1K · @50 tok/s/用户 · ↓ 越低越好').should( + cy.contains('按各模型标注的场景,基于各平台最佳观测服务包络线计算每百万输出 token 成本').should( 'exist', ); + cy.get('[data-testid="overview-scope"]').should( + 'have.text', + 'GPU 租赁成本 / 每百万输出 token · @50 tok/s/用户 · ↓ 越低越好', + ); cy.contains('成本 = 3 年期租赁 $/GPU/小时 ÷ 每张已部署 GPU 的输出 tok/s。').should('exist'); cy.contains('分离式结果的分母同时计入预填充与解码 GPU。').should('exist'); cy.contains( @@ -644,19 +635,25 @@ describe('Overview page', () => { .invoke('text') .should('not.match', /100 档由.+领先/); desktopModel('Kimi-K2.5').within(() => { - platform('mi355x').should('contain.text', 'SGLang · FP4 · 标准解码'); - platform('b300').should('contain.text', 'SGLang · FP8 · 标准解码'); + cy.get('[data-testid="overview-pair-value"]').should('have.length', 3); + cy.get('[data-testid="overview-pair-missing"]').should('have.length', 2); + platform('b200').should('contain.text', '标准解码'); platform('b200').find('[data-testid="overview-cost-delta"]').should('not.exist'); }); - desktopModel('GLM-5.2') - .find('[data-testid="overview-model-coverage-note"]') - .should('have.text', '暂无 8K/1K 结果'); + desktopModel('GLM-5.2').within(() => { + cy.get('[data-testid="overview-model-scenario"]').should('have.text', 'AgentX'); + cy.get('[data-testid="overview-pair-missing"]').should('have.length', 5); + platform('b300') + .find('[data-testid="overview-pair-missing"]') + .should('have.attr', 'title', '该场景暂无数据'); + }); + cy.contains('优先顺序:推测解码 FP4 → 推测解码 FP8 → 标准解码 FP4 → 标准解码 FP8。').should( + 'exist', + ); cy.contains('∞ = 无可比结果').should('exist'); cy.visit('/zh/overview?tier=100'); - cy.contains('GPU 租赁成本 / 每百万输出 token · 8K→1K · @100 tok/s/用户 · ↓ 越低越好').should( - 'exist', - ); + cy.contains('GPU 租赁成本 / 每百万输出 token · @100 tok/s/用户 · ↓ 越低越好').should('exist'); cy.get('[data-testid="overview-tier-switcher"]').within(() => { cy.contains('a', '50').should('have.attr', 'href', '/zh/overview'); }); diff --git a/packages/app/cypress/e2e/zh-pages.cy.ts b/packages/app/cypress/e2e/zh-pages.cy.ts index 77fc8cfc5..70c0a6a85 100644 --- a/packages/app/cypress/e2e/zh-pages.cy.ts +++ b/packages/app/cypress/e2e/zh-pages.cy.ts @@ -5,12 +5,17 @@ describe('Chinese (/zh) pages', () => { }); it('renders the Chinese landing content', () => { - cy.contains('h2', '完整仪表板').should('exist'); + cy.contains('h2', '探索 InferenceX').should('exist'); cy.contains('快速对比').should('exist'); }); - it('links into the Chinese dashboard tree', () => { - cy.get('a[href="/zh/inference"]').should('exist'); + it('links to the Chinese overview and full dashboard', () => { + cy.get('[data-testid="landing-overview-link"]') + .should('have.attr', 'href', '/zh/overview') + .and('have.text', '总览'); + cy.get('[data-testid="landing-full-dashboard-link"]') + .should('have.attr', 'href', '/zh/inference') + .and('have.text', '完整仪表板'); }); it('sets hreflang alternates to the English homepage', () => { diff --git a/packages/app/src/app/(dashboard)/overview/page.tsx b/packages/app/src/app/(dashboard)/overview/page.tsx index c324a26fe..564c3f7ce 100644 --- a/packages/app/src/app/(dashboard)/overview/page.tsx +++ b/packages/app/src/app/(dashboard)/overview/page.tsx @@ -10,7 +10,7 @@ import { getOverviewPageData } from '@/lib/overview-data.server'; export const dynamic = 'force-dynamic'; const DESCRIPTION = - 'Cost per million output tokens from the best observed platform serving envelopes across MI355X, B200, B300, GB200 and GB300 at a fixed single-turn 8K input / 1K output workload, prioritizing speculative decode and FP4.'; + 'Compare cost per million output tokens across MI355X, B200, B300, GB200 and GB300 using the scenario shown for each active model.'; export const metadata: Metadata = { title: 'Inference Cost Overview', diff --git a/packages/app/src/app/zh/(dashboard)/overview/page.tsx b/packages/app/src/app/zh/(dashboard)/overview/page.tsx index 5ddeb6fd3..e3aaf44a6 100644 --- a/packages/app/src/app/zh/(dashboard)/overview/page.tsx +++ b/packages/app/src/app/zh/(dashboard)/overview/page.tsx @@ -10,7 +10,7 @@ import { getOverviewPageData } from '@/lib/overview-data.server'; export const dynamic = 'force-dynamic'; const DESCRIPTION = - '在固定单轮 8K 输入 / 1K 输出负载下,基于最佳观测平台服务包络线对比各活跃模型在 MI355X、B200、B300、GB200 与 GB300 上的每百万输出 token 成本;优先采用推测解码与 FP4。'; + '按各活跃模型标注的场景,对比 MI355X、B200、B300、GB200 与 GB300 的每百万输出 token 成本。'; export const metadata: Metadata = { title: '推理成本总览', diff --git a/packages/app/src/components/calculator/useThroughputData.ts b/packages/app/src/components/calculator/useThroughputData.ts index 01ce8bfca..facd79d2b 100644 --- a/packages/app/src/components/calculator/useThroughputData.ts +++ b/packages/app/src/components/calculator/useThroughputData.ts @@ -4,11 +4,11 @@ import { useCallback, useMemo } from 'react'; import { DB_MODEL_TO_DISPLAY, rowToSequence } from '@semianalysisai/inferencex-constants'; -import type { AggDataEntry, HardwareConfig, InferenceData } from '@/components/inference/types'; +import type { AggDataEntry, HardwareConfig } from '@/components/inference/types'; import { useBenchmarks } from '@/hooks/api/use-benchmarks'; import type { BenchmarkRow } from '@/lib/api'; import { rowToAggDataEntry } from '@/lib/benchmark-transform'; -import { getHardwareKey, paretoFrontUpperRight } from '@/lib/chart-utils'; +import { getHardwareKey } from '@/lib/chart-utils'; import { getModelSortIndex, getHardwareConfig, getGpuSpecs } from '@/lib/constants'; import { Percentile, Sequence, type Model } from '@/lib/data-mappings'; import { overlayRunIndex } from '@/lib/overlay-run-style'; @@ -22,6 +22,7 @@ import { paretoFrontUpperLeft, sign, } from './interpolation'; +import { restrictAgenticPointsToE2eFrontier } from '@/lib/agentic-frontier'; import type { CostProvider, GPUDataPoint, InterpolatedResult } from './types'; // Re-export pure functions so existing imports from this module keep working. @@ -52,50 +53,6 @@ export interface OverlayGroupMeta extends GroupMeta { runIndex: number; } -interface AgenticFrontierPoint { - x: number; - y: number; - orig: GPUDataPoint; -} - -/** - * Match the main agentic interactivity chart's anti-benchmark-hacking seed: - * within each date, only points that also win on the (end-to-end latency, - * total throughput) upper-right Pareto frontier may feed interpolation. - * - * The chart's shared Pareto helper operates on `InferenceData.x/y` only, so - * minimal projections keep this path aligned without running the full chart - * transformation pipeline. - */ -export function restrictAgenticPointsToE2eFrontier(points: GPUDataPoint[]): GPUDataPoint[] { - const byDate = new Map(); - - for (const point of points) { - if ( - typeof point.e2eLatency !== 'number' || - !Number.isFinite(point.e2eLatency) || - !Number.isFinite(point.throughput) - ) { - continue; - } - const date = point.date ?? ''; - let bucket = byDate.get(date); - if (!bucket) { - bucket = []; - byDate.set(date, bucket); - } - bucket.push({ x: point.e2eLatency, y: point.throughput, orig: point }); - } - - const winners = new Set(); - for (const bucket of byDate.values()) { - for (const winner of paretoFrontUpperRight(bucket as unknown as InferenceData[])) { - winners.add((winner as unknown as AgenticFrontierPoint).orig); - } - } - return points.filter((point) => winners.has(point)); -} - function getAgenticMetric( entry: AggDataEntry, percentile: Percentile, diff --git a/packages/app/src/components/landing/landing-page.tsx b/packages/app/src/components/landing/landing-page.tsx index 61c4f0d76..02f10f294 100644 --- a/packages/app/src/components/landing/landing-page.tsx +++ b/packages/app/src/components/landing/landing-page.tsx @@ -11,12 +11,13 @@ import type { Locale } from '@/lib/i18n'; const STRINGS = { en: { + exploreInferenceX: 'Explore InferenceX', + exploreInferenceXLead: + 'Start with a concise cost overview across active models and key platforms, or open the full dashboard for every model, GPU, framework, and metric.', fullDashboard: 'Full Dashboard', - fullDashboardP1: - 'Every model, GPU, framework, and metric. Fully configurable inference benchmark charts with date ranges, concurrency sweeps, and raw data export.', - fullDashboardP2: + platformCoverage: 'Compare NVIDIA GB300 NVL72, GB200 NVL72, B300, B200, H200, H100, AMD MI355X, MI325X, MI300X and soon VR200 NVL72, AMD MI455X UALoE72, TPUv7 Ironwood, etc across DeepSeekv4 Pro, Qwen, Kimi, GLM, MiniMax, gpt-oss, Llama and other models.', - openDashboard: 'Open Dashboard', + overview: 'Overview', reproTitle: 'Every Result Is Transparently done through Public GitHub Actions Automation', reproP1: 'Every data point on the dashboard is produced by a public GitHub Actions workflow run. The recipe lives in the repo, the run executes on the actual target hardware, and the full logs and artifacts are publicly viewable. Click any point on a chart to jump straight to the run that produced it. All reproducible, auditable, and open source.', @@ -39,12 +40,13 @@ const STRINGS = { 'Jump straight into the most popular GPU inference benchmark comparisons, curated and ready to explore.', }, zh: { + exploreInferenceX: '探索 InferenceX', + exploreInferenceXLead: + '先查看活跃模型与关键平台的精简成本总览,或打开完整仪表板,探索所有模型、GPU、框架与指标。', fullDashboard: '完整仪表板', - fullDashboardP1: - '覆盖所有模型、GPU、框架与指标。完全可配置的推理基准测试图表,支持日期范围、并发扫描与原始数据导出。', - fullDashboardP2: + platformCoverage: '跨 DeepSeekv4 Pro、Qwen、Kimi、GLM、MiniMax、gpt-oss、Llama 等模型,对比 NVIDIA GB300 NVL72、GB200 NVL72、B300、B200、H200、H100、AMD MI355X、MI325X、MI300X,以及即将上线的 VR200 NVL72、AMD MI455X UALoE72、TPUv7 Ironwood 等硬件。', - openDashboard: '打开仪表板', + overview: '总览', reproTitle: '每一条结果都通过公开的 GitHub Actions 自动化流程透明产生', reproP1: '仪表板上的每个数据点都由公开的 GitHub Actions 工作流运行产生。配置方案(recipe)保存在公开仓库中,运行在真实目标硬件上执行,完整日志与产物公开可查。点击图表上的任意数据点即可跳转到生成它的那次运行。一切都可复现、可审计、开源。', @@ -76,25 +78,36 @@ export function LandingPage({ locale = 'en' }: { locale?: Locale } = {}) {
- {/* Split: Dashboard vs Presets */} + {/* Split: exploration entry points vs presets */}
- {/* Left - Full Dashboard */} + {/* Primary product entry points */}
-

{t.fullDashboard}

+

{t.exploreInferenceX}

-

{t.fullDashboardP1}

-

{t.fullDashboardP2}

-
+

{t.exploreInferenceXLead}

+

{t.platformCoverage}

+
+ + {t.overview} + - {t.openDashboard} - + {t.fullDashboard} +
diff --git a/packages/app/src/components/overview/overview-page.tsx b/packages/app/src/components/overview/overview-page.tsx index a346ae763..2728535bd 100644 --- a/packages/app/src/components/overview/overview-page.tsx +++ b/packages/app/src/components/overview/overview-page.tsx @@ -31,7 +31,10 @@ export function OverviewPageContent({ data, locale }: OverviewPageProps) {

{strings.title}

{strings.purpose}

-

+

{strings.scope(data.tier)}

{snapshot === null ? null : ( diff --git a/packages/app/src/components/overview/overview-scorecard.tsx b/packages/app/src/components/overview/overview-scorecard.tsx index 8c3574c83..1c5a3c1c7 100644 --- a/packages/app/src/components/overview/overview-scorecard.tsx +++ b/packages/app/src/components/overview/overview-scorecard.tsx @@ -21,7 +21,7 @@ export const OVERVIEW_STRINGS = { title: 'Inference Cost Overview', purpose: 'Every active model across MI355X, B200, B300, GB200 and GB300 at a glance.', scope: (tier: number) => - `GPU rental cost / 1M output tokens · 8K→1K · @${tier} tok/s/user · ↓ lower is better`, + `GPU rental cost / 1M output tokens · @${tier} tok/s/user · ↓ lower is better`, tierNavLabel: 'Service level', tierUnit: 'tok/s/user', engineScopeNavLabel: 'Engine scope', @@ -31,27 +31,30 @@ export const OVERVIEW_STRINGS = { }, snapshot: (through: string) => `Database snapshot through ${through}`, caption: - "Cost per million output tokens from the best observed platform serving envelopes for every active model across today's key platforms, prioritizing speculative decode and FP4.", - modelHeader: 'Model', + 'Cost per million output tokens from each platform’s best observed serving envelope for the scenario shown with each model.', + modelHeader: 'Model · Scenario', + scenarioLabels: { + single_turn_8k1k: 'Single-turn · 8K→1K', + agentx: 'AgentX', + }, detailsHeader: 'Details', detailLink: 'View details', detailAria: (modelLabel: string) => `View details: ${modelLabel}`, rawDashboardAria: (evidenceDate: string, modelLabel: string, stack: string) => `Open raw source dashboard for ${evidenceDate}: ${modelLabel} · ${stack}`, - standardDecode: 'Standard decode', estimatedTooltip: (topologies: readonly string[]) => topologies.length === 0 ? 'Estimated from validated benchmark runs.' : `Estimated from validated ${topologies.join(' and ')} runs.`, estimatedAria: (value: string, explanation: string) => `Approximately ${value}. ${explanation}`, - noWorkloadResults: 'No 8K/1K results', infinityLegend: '∞ = no comparable result', missingReasons: (tier: number): Record => ({ int4_bf16_only: 'INT4/BF16 only', - no_8k1k_data: 'no 8K/1K data', + no_scenario_data: 'no data for this scenario', cannot_reach_at_tier: `cannot reach @${tier}`, no_exact_at_tier: `no exact @${tier} result`, }), + standardDecodeLabel: 'Standard decode', methodologyNote: 'Priority: speculative FP4 → speculative FP8 → standard FP4 → standard FP8.', costNote: 'Cost = 3-yr rental $/GPU/hr ÷ output tok/s per deployed GPU. All percentages compare against B200.', @@ -64,13 +67,12 @@ export const OVERVIEW_STRINGS = { interpolationNote: 'Tier values use the best observed platform serving envelope; ≈ marks estimates between validated runs. No extrapolation.', comparabilityNote: - 'Directional platform comparison: cells pick each platform’s best observed envelope, so dates, engines, precisions and decode methods may differ.', + 'Each row compares platforms within the scenario shown with that model; dates, engines, precisions and speculative methods may differ.', }, zh: { title: '推理成本总览', purpose: '一眼对比各活跃模型在 MI355X、B200、B300、GB200 与 GB300 上的表现。', - scope: (tier: number) => - `GPU 租赁成本 / 每百万输出 token · 8K→1K · @${tier} tok/s/用户 · ↓ 越低越好`, + scope: (tier: number) => `GPU 租赁成本 / 每百万输出 token · @${tier} tok/s/用户 · ↓ 越低越好`, tierNavLabel: '服务档位', tierUnit: 'tok/s/用户', engineScopeNavLabel: '引擎范围', @@ -79,28 +81,30 @@ export const OVERVIEW_STRINGS = { community: '开源社区引擎(vLLM/SGLang)', }, snapshot: (through: string) => `数据库快照截至 ${through}`, - caption: - '基于最佳观测平台服务包络线计算的各活跃模型每百万输出 token 成本;优先采用推测解码与 FP4。', - modelHeader: '模型', + caption: '按各模型标注的场景,基于各平台最佳观测服务包络线计算每百万输出 token 成本。', + modelHeader: '模型 · 场景', + scenarioLabels: { + single_turn_8k1k: '单轮 · 8K→1K', + agentx: 'AgentX', + }, detailsHeader: '详情', detailLink: '查看详情', detailAria: (modelLabel: string) => `查看详情:${modelLabel}`, rawDashboardAria: (evidenceDate: string, modelLabel: string, stack: string) => `打开 ${evidenceDate} 原始数据仪表板:${modelLabel} · ${stack}`, - standardDecode: '标准解码', estimatedTooltip: (topologies: readonly string[]) => topologies.length === 0 ? '根据已验证的基准运行结果估算。' : `根据已验证的 ${topologies.join(' 与 ')} 运行结果估算。`, estimatedAria: (value: string, explanation: string) => `约 ${value}。${explanation}`, - noWorkloadResults: '暂无 8K/1K 结果', infinityLegend: '∞ = 无可比结果', missingReasons: (tier: number): Record => ({ int4_bf16_only: '仅 INT4/BF16', - no_8k1k_data: '无 8K/1K 数据', + no_scenario_data: '该场景暂无数据', cannot_reach_at_tier: `无法达到 @${tier}`, no_exact_at_tier: `无精确 @${tier} 结果`, }), + standardDecodeLabel: '标准解码', methodologyNote: '优先顺序:推测解码 FP4 → 推测解码 FP8 → 标准解码 FP4 → 标准解码 FP8。', costNote: '成本 = 3 年期租赁 $/GPU/小时 ÷ 每张已部署 GPU 的输出 tok/s。所有百分比均相对 B200。', costDeltaAria: (pct: string, cheaper: boolean) => `比 B200 ${cheaper ? '便宜' : '昂贵'} ${pct}`, @@ -109,8 +113,7 @@ export const OVERVIEW_STRINGS = { normalizationNote: '分离式结果的分母同时计入预填充与解码 GPU。', interpolationNote: '各档位数值采用最佳观测平台服务包络线;≈ 表示根据已验证运行结果估算。不会外推。', - comparabilityNote: - '方向性平台对比:各单元格取该平台最佳观测包络线,日期、引擎、精度与解码方式可能不同。', + comparabilityNote: '每行均在该模型标注的场景内比较各平台;日期、引擎、精度与推测方法可能不同。', }, } as const; @@ -263,14 +266,16 @@ function CellValue({ return ; } const precisionLabel = config?.precision.toUpperCase() ?? member.precision?.toUpperCase() ?? null; + const specLabel = + config === null + ? null + : config.specMethod === 'none' || config.specMethod === '' + ? strings.standardDecodeLabel + : config.specLabel; const stackBadge = - config === null || precisionLabel === null + config === null || precisionLabel === null || specLabel === null ? null - : [ - config.frameworkLabel, - precisionLabel, - member.decodeMode === 'standard' ? strings.standardDecode : null, - ] + : [config.frameworkLabel, precisionLabel, specLabel] .filter((part): part is string => part !== null) .join(' · '); const stack = @@ -280,7 +285,7 @@ function CellValue({ member.hardwareLabel, config.frameworkLabel, config.precision.toUpperCase(), - member.decodeMode === 'standard' ? strings.standardDecode : config.specLabel, + specLabel, ].join(' · '); const evidenceDateLabel = evidenceDate === null ? '' : formatEvidenceDate(formatters, evidenceDate); @@ -382,27 +387,17 @@ function PlatformCell(props: { ); } -/** A model with zero 8K/1K coverage collapses to one note instead of a row of - * identical empty states. */ -function hasNo8k1kResult(model: OverviewModelSummary): boolean { +function ModelName({ model, strings }: { model: OverviewModelSummary; strings: OverviewStrings }) { return ( - model.platforms.length > 0 && - model.platforms.every((platform) => platform.missingReason === 'no_8k1k_data') - ); -} - -function ModelName({ model }: { model: OverviewModelSummary }) { - return

{model.modelLabel}

; -} - -function CoverageNote({ strings }: { strings: OverviewStrings }) { - return ( -

- {strings.noWorkloadResults} -

+
+

{model.modelLabel}

+

+ {strings.scenarioLabels[model.scenario]} +

+
); } @@ -456,28 +451,22 @@ export function DesktopOverviewMatrix({ models, locale, formatters, strings }: S className="border-b border-border/50 align-top last:border-b-0" > - + - {hasNo8k1kResult(model) ? ( - - + {model.platforms.map((platform) => ( + + - ) : ( - model.platforms.map((platform) => ( - - - - )) - )} + ))} - - {hasNo8k1kResult(model) ? ( - - ) : ( -
- {model.platforms.map((platform) => ( -
+
+ {model.platforms.map((platform) => ( +
+ - - {platform.hardwareLabel} - - -
- ))} -
- )} + {platform.hardwareLabel} + + +
+ ))} +
{ + x: number; + y: number; + orig: T; +} + +/** + * Keep only date-scoped winners on the AgentX + * (E2E latency, selected throughput metric) Pareto frontier. + */ +export function restrictAgenticPointsToE2eFrontier( + points: readonly T[], +): T[] { + const byDate = new Map[]>(); + + for (const point of points) { + if ( + !Number.isFinite(point.e2eLatency) || + (point.e2eLatency ?? 0) <= 0 || + !Number.isFinite(point.throughput) || + point.throughput <= 0 + ) { + continue; + } + const date = point.date ?? ''; + const framed = { x: point.e2eLatency!, y: point.throughput, orig: point }; + const bucket = byDate.get(date); + if (bucket) bucket.push(framed); + else byDate.set(date, [framed]); + } + + const winners = new Set(); + for (const bucket of byDate.values()) { + for (const winner of paretoFrontUpperRight(bucket as unknown as InferenceData[])) { + winners.add((winner as unknown as FramedPoint).orig); + } + } + return points.filter((point) => winners.has(point)); +} diff --git a/packages/app/src/lib/overview-data.test.ts b/packages/app/src/lib/overview-data.test.ts index a77a3c90f..02aabf5af 100644 --- a/packages/app/src/lib/overview-data.test.ts +++ b/packages/app/src/lib/overview-data.test.ts @@ -9,6 +9,7 @@ import { DEFAULT_MODELS, Model, Precision } from './data-mappings'; import { assembleOverviewPageData, buildOverviewModelSummary, + overviewScenarioForModel, resolveOverviewEngineScope, resolveOverviewTier, type OverviewModelSummary, @@ -84,7 +85,47 @@ function headlinePairOf(summary: OverviewModelSummary, id: string) { return candidate === undefined || baseline === undefined ? undefined : { candidate, baseline }; } -describe('overview engine scope and comparable fallbacks', () => { +describe('overview engine scope and scenario selection', () => { + it('assigns each active model to its configured scenario', () => { + expect(overviewScenarioForModel(Model.Kimi_K3)).toBe('agentx'); + expect(overviewScenarioForModel(Model.GLM_5_2)).toBe('agentx'); + expect(overviewScenarioForModel(Model.DeepSeek_V4_Pro)).toBe('single_turn_8k1k'); + expect(overviewScenarioForModel(Model.Kimi_K2_5)).toBe('single_turn_8k1k'); + expect(overviewScenarioForModel(Model.MiniMax_M3)).toBe('single_turn_8k1k'); + expect(overviewScenarioForModel(Model.Qwen3_5)).toBe('single_turn_8k1k'); + }); + + it('prefers single-turn 8K/1K rows and otherwise falls back to AgentX', () => { + const singleTurn = frontier([1200, 1000, 800, 600], { + model: 'glm5.2', + hardware: 'b200', + }); + const agentx = [ + row({ + model: 'glm5.2', + hardware: 'b200', + benchmark_type: 'agentic_traces', + isl: null, + osl: null, + metrics: { + p90_itl: 1 / 50, + p90_ttlt: 25, + output_tput_per_gpu: 850, + }, + }), + ]; + + expect(buildOverviewModelSummary(Model.GLM_5_2, [...agentx, ...singleTurn]).scenario).toBe( + 'single_turn_8k1k', + ); + expect( + buildOverviewModelSummary( + Model.Qwen3_5, + agentx.map((entry) => ({ ...entry, model: 'qwen3.5' })), + ).scenario, + ).toBe('agentx'); + }); + it('resolves valid engine scopes and defaults invalid values to community', () => { expect(resolveOverviewEngineScope('community')).toBe('community'); expect(resolveOverviewEngineScope('all')).toBe('all'); @@ -410,7 +451,7 @@ describe('overview engine scope and comparable fallbacks', () => { expect(headlinePairOf(summary, 'gb300-vs-b200')?.candidate.read.value).toBeNull(); }); - it('applies the decode and precision priority ladder independently per platform', () => { + it('uses speculative FP4, speculative FP8, standard FP4, then standard FP8', () => { const summary = buildOverviewModelSummary(Model.Qwen3_5, [ ...frontier([900, 700, 500, 300], { hardware: 'b200', precision: Precision.FP4 }), ...frontier([1400, 1200, 1000, 800], { @@ -447,18 +488,17 @@ describe('overview engine scope and comparable fallbacks', () => { ]); expect( - summary.platforms.map(({ hardware, precision, decodeMode, read }) => ({ + summary.platforms.map(({ hardware, precision, read }) => ({ hardware, precision, - decodeMode, value: read.value, })), ).toEqual([ - { hardware: 'b200', precision: Precision.FP4, decodeMode: 'speculative', value: 700 }, - { hardware: 'mi355x', precision: Precision.FP8, decodeMode: 'speculative', value: 900 }, - { hardware: 'b300', precision: Precision.FP4, decodeMode: 'standard', value: 1100 }, - { hardware: 'gb200', precision: Precision.FP8, decodeMode: 'standard', value: 1000 }, - { hardware: 'gb300', precision: Precision.FP4, decodeMode: 'standard', value: 900 }, + { hardware: 'b200', precision: Precision.FP4, value: 700 }, + { hardware: 'mi355x', precision: Precision.FP8, value: 900 }, + { hardware: 'b300', precision: Precision.FP4, value: 1100 }, + { hardware: 'gb200', precision: Precision.FP8, value: 1000 }, + { hardware: 'gb300', precision: Precision.FP4, value: 900 }, ]); }); }); @@ -614,21 +654,135 @@ describe('overview platform selection', () => { ); }); - it('uses standard decode when available and still flags unsupported precision coverage', () => { + it('falls back to standard decode and still flags unsupported precision coverage', () => { const summary = buildOverviewModelSummary(Model.Qwen3_5, [ ...frontier([1200, 1000, 800, 600], { hardware: 'b200', precision: Precision.FP4 }), - row({ hardware: 'mi355x', precision: Precision.FP8, spec_method: 'none' }), + ...frontier([1100, 900, 700, 500], { + hardware: 'mi355x', + precision: Precision.FP8, + spec_method: 'none', + }), row({ hardware: 'b300', precision: Precision.INT4 }), ]); expect(headlinePairOf(summary, 'mi355x-vs-b200')?.candidate).toMatchObject({ - decodeMode: 'standard', missingReason: null, - read: { value: 1000 }, + precision: Precision.FP8, + read: { value: 900, config: { specMethod: 'none' } }, }); expect(headlinePairOf(summary, 'b300-vs-b200')?.candidate.missingReason).toBe('int4_bf16_only'); }); + it('falls back to standard decode for AgentX when no speculative result exists', () => { + const agentxRows = [40, 50, 60].map((interactivity, index) => + row({ + model: 'glm5.2', + hardware: 'b300', + benchmark_type: 'agentic_traces', + isl: null, + osl: null, + precision: Precision.FP4, + spec_method: 'none', + conc: index + 1, + metrics: { + p90_itl: 1 / interactivity, + p90_ttlt: 30 - index * 5, + output_tput_per_gpu: 1000 - index * 100, + }, + }), + ); + + const summary = buildOverviewModelSummary(Model.GLM_5_2, agentxRows); + expect(summary.scenario).toBe('agentx'); + expect(summary.platforms.find(({ hardware }) => hardware === 'b300')).toMatchObject({ + missingReason: null, + precision: Precision.FP4, + read: { value: 900, config: { specMethod: 'none' } }, + }); + }); + + it('reads AgentX at the chart-default P90 contract without exposing P90 as the scenario', () => { + const summary = buildOverviewModelSummary(Model.GLM_5_2, [ + row({ + model: 'glm5.2', + hardware: 'b200', + benchmark_type: 'agentic_traces', + isl: null, + osl: null, + precision: Precision.FP4, + spec_method: 'mtp', + conc: 8, + metrics: { + p90_itl: 1 / 40, + p90_ttlt: 30, + tput_per_gpu: 1400, + output_tput_per_gpu: 1200, + }, + }), + row({ + model: 'glm5.2', + hardware: 'b200', + benchmark_type: 'agentic_traces', + isl: null, + osl: null, + precision: Precision.FP4, + spec_method: 'mtp', + conc: 12, + metrics: { + p90_itl: 1 / 50, + p90_ttlt: 25, + tput_per_gpu: 900, + output_tput_per_gpu: 850, + }, + }), + row({ + model: 'glm5.2', + hardware: 'b200', + benchmark_type: 'agentic_traces', + isl: null, + osl: null, + precision: Precision.FP4, + spec_method: 'mtp', + conc: 16, + metrics: { + p90_itl: 1 / 60, + p90_ttlt: 20, + tput_per_gpu: 1000, + output_tput_per_gpu: 800, + }, + }), + ]); + + expect(summary.scenario).toBe('agentx'); + expect(summary.platforms.find(({ hardware }) => hardware === 'b200')?.read).toMatchObject({ + value: 850, + boundary: 'interpolated', + estimated: false, + }); + }); + + it('reports scenario-level missing coverage when AgentX rows lack usable P90 metrics', () => { + const summary = buildOverviewModelSummary(Model.GLM_5_2, [ + row({ + model: 'glm5.2', + hardware: 'b200', + benchmark_type: 'agentic_traces', + isl: null, + osl: null, + precision: Precision.FP4, + spec_method: 'mtp', + metrics: { + output_tput_per_gpu: 1200, + }, + }), + ]); + + expect(summary.platforms.find(({ hardware }) => hardware === 'b200')).toMatchObject({ + read: { value: null }, + missingReason: 'no_scenario_data', + }); + }); + it('returns all five platforms with coverage gaps for an empty model', () => { const summary = buildOverviewModelSummary(Model.Qwen3_5, []); @@ -640,9 +794,9 @@ describe('overview platform selection', () => { 'gb300', ]); expect(summary.platforms.every(({ precision }) => precision === null)).toBe(true); - expect(summary.platforms.every(({ missingReason }) => missingReason === 'no_8k1k_data')).toBe( - true, - ); + expect( + summary.platforms.every(({ missingReason }) => missingReason === 'no_scenario_data'), + ).toBe(true); }); }); @@ -774,7 +928,7 @@ describe('assembleOverviewPageData over the overview-rows fixture', () => { // MiniMax: the platform result remains visible when B200 has no 8K/1K data. const minimax = page.models.find((m) => m.model === Model.MiniMax_M3)!; const mmGb300 = headlinePairOf(minimax, 'gb300-vs-b200')!; - expect(mmGb300.baseline.missingReason).toBe('no_8k1k_data'); + expect(mmGb300.baseline.missingReason).toBe('no_scenario_data'); expect(mmGb300.candidate.read.value).toBe(700); // Qwen: MI355X independently falls back to FP8 while B200 and B300 use FP4. @@ -790,24 +944,33 @@ describe('assembleOverviewPageData over the overview-rows fixture', () => { expect(qwenB300.baseline.precision).toBe(Precision.FP4); expect(qwenB300.baseline.read.value).toBeCloseTo(733.594); - // Kimi: standard decode supplies each platform's highest-priority precision. + // Kimi: standard-only rows remain visible as explicitly labelled fallbacks. const kimi = page.models.find((m) => m.model === Model.Kimi_K2_5)!; const kimiMi = headlinePairOf(kimi, 'mi355x-vs-b200')!; expect(kimiMi.candidate.precision).toBe(Precision.FP4); - expect(kimiMi.candidate.read.value).toBe(1000); + expect(kimiMi.candidate.read).toMatchObject({ + value: 1000, + config: { specMethod: 'none' }, + }); + expect(kimiMi.candidate.missingReason).toBeNull(); expect(kimiMi.baseline.precision).toBe(Precision.FP4); - expect(kimiMi.baseline.read.value).toBe(800); + expect(kimiMi.baseline.read).toMatchObject({ + value: 800, + config: { specMethod: 'none' }, + }); const kimiB300 = headlinePairOf(kimi, 'b300-vs-b200')!; expect(kimiB300.candidate.precision).toBe(Precision.FP8); - expect(kimiB300.candidate.read.value).toBe(900); - expect(kimiB300.baseline.precision).toBe(Precision.FP4); - expect(kimiB300.baseline.read.value).toBe(800); + expect(kimiB300.candidate.read).toMatchObject({ + value: 900, + config: { specMethod: 'none' }, + }); + expect(kimiB300.candidate.missingReason).toBeNull(); - // GLM mirrors the live coverage gap: agentic data exists, but no single-turn 8K/1K line does. + // GLM's AgentX fixture lacks valid P90 metrics, so it cannot produce a tier read. const glm = page.models.find((m) => m.model === Model.GLM_5_2)!; const glmB300 = headlinePairOf(glm, 'b300-vs-b200')!; expect(glmB300.candidate.read.value).toBeNull(); - expect(glmB300.candidate.missingReason).toBe('no_8k1k_data'); + expect(glmB300.candidate.missingReason).toBe('no_scenario_data'); const communityPage = assembleOverviewPageData( overviewRowsFixture as unknown as Record, @@ -817,6 +980,6 @@ describe('assembleOverviewPageData over the overview-rows fixture', () => { const communityGlm = communityPage.models.find((m) => m.model === Model.GLM_5_2)!; const communityGlmB300 = headlinePairOf(communityGlm, 'b300-vs-b200')!; expect(communityGlmB300.candidate.read.value).toBeNull(); - expect(communityGlmB300.candidate.missingReason).toBe('no_8k1k_data'); + expect(communityGlmB300.candidate.missingReason).toBe('no_scenario_data'); }); }); diff --git a/packages/app/src/lib/overview-data.ts b/packages/app/src/lib/overview-data.ts index 2ca2a3bf3..33cc0e409 100644 --- a/packages/app/src/lib/overview-data.ts +++ b/packages/app/src/lib/overview-data.ts @@ -1,18 +1,25 @@ import { resolveFrameworkPartLabel } from '@semianalysisai/inferencex-constants'; +import { restrictAgenticPointsToE2eFrontier } from './agentic-frontier'; import type { BenchmarkRow } from './api'; +import { rowToAggDataEntry } from './benchmark-transform'; import { buildAvailabilityHwKey } from './chart-utils'; import { getGpuSpecs, getHardwareConfig } from './constants'; -import { DEFAULT_MODELS, getModelLabel, Precision, type Model } from './data-mappings'; +import { DEFAULT_MODELS, getModelLabel, Model, Precision } from './data-mappings'; import { frameworkFamily } from './framework-family'; -import { computeTcoFeed, type TcoTierBoundary } from './tco-feed'; +import { + computeTcoFeed, + computeTierReads, + type TcoTierBoundary, + type TcoTierPoint, +} from './tco-feed'; export const OVERVIEW_WORKLOAD = { isl: 8192, osl: 1024 } as const; export const OVERVIEW_TIERS = [30, 50, 75, 100] as const; export type OverviewTier = (typeof OVERVIEW_TIERS)[number]; export const OVERVIEW_PRIMARY_TIER = 50; export type OverviewEngineScope = 'all' | 'community'; -export type OverviewDecodeMode = 'speculative' | 'standard'; +export type OverviewScenario = 'single_turn_8k1k' | 'agentx'; export function resolveOverviewEngineScope( raw: string | string[] | undefined, @@ -72,7 +79,7 @@ export interface OverviewTierRead { * under-swept. */ export type OverviewMissingReason = | 'int4_bf16_only' - | 'no_8k1k_data' + | 'no_scenario_data' | 'cannot_reach_at_tier' | 'no_exact_at_tier'; @@ -82,7 +89,6 @@ export interface OverviewPlatformResult { hardware: string; hardwareLabel: string; precision: string | null; - decodeMode: OverviewDecodeMode | null; read: OverviewTierRead; missingReason: OverviewMissingReason | null; /** $ per million output tokens at the retail-rental $/GPU/hr tier. */ @@ -95,6 +101,7 @@ export interface OverviewPlatformResult { export interface OverviewModelSummary { model: Model; modelLabel: string; + scenario: OverviewScenario; platforms: OverviewPlatformResult[]; } @@ -106,14 +113,11 @@ export interface OverviewPageData { } const OVERVIEW_SLICE_PRIORITY = [ - { decodeMode: 'speculative', precision: Precision.FP4 }, - { decodeMode: 'speculative', precision: Precision.FP8 }, - { decodeMode: 'standard', precision: Precision.FP4 }, - { decodeMode: 'standard', precision: Precision.FP8 }, -] as const satisfies readonly { - decodeMode: OverviewDecodeMode; - precision: string; -}[]; + { speculative: true, precision: Precision.FP4 }, + { speculative: true, precision: Precision.FP8 }, + { speculative: false, precision: Precision.FP4 }, + { speculative: false, precision: Precision.FP8 }, +] as const; const OVERVIEW_PRECISIONS: readonly string[] = [Precision.FP4, Precision.FP8]; const OVERVIEW_HARDWARE_LABELS: Readonly> = { gb200: 'GB200', @@ -124,8 +128,25 @@ function overviewHardwareLabel(hardware: string, model: Model): string { return OVERVIEW_HARDWARE_LABELS[hardware] ?? getHardwareConfig(hardware, model).label; } -function decodeModeForSpecMethod(specMethod: string): OverviewDecodeMode { - return specMethod === 'none' || specMethod === '' ? 'standard' : 'speculative'; +const isSpeculativeDecode = (specMethod: string): boolean => + specMethod !== 'none' && specMethod !== ''; + +export function overviewScenarioForModel( + model: Model, + rows: readonly BenchmarkRow[] = [], +): OverviewScenario { + if ( + rows.some( + (row) => + row.benchmark_type === 'single_turn' && + row.isl === OVERVIEW_WORKLOAD.isl && + row.osl === OVERVIEW_WORKLOAD.osl, + ) + ) { + return 'single_turn_8k1k'; + } + if (rows.some((row) => row.benchmark_type === 'agentic_traces')) return 'agentx'; + return model === Model.Kimi_K3 || model === Model.GLM_5_2 ? 'agentx' : 'single_turn_8k1k'; } function overviewEngineRows( @@ -139,7 +160,13 @@ function overviewEngineRows( }); } -function overviewWorkloadRows(rows: readonly BenchmarkRow[]): BenchmarkRow[] { +function overviewScenarioRows( + scenario: OverviewScenario, + rows: readonly BenchmarkRow[], +): BenchmarkRow[] { + if (scenario === 'agentx') { + return rows.filter((row) => row.benchmark_type === 'agentic_traces'); + } return rows.filter( (row) => row.benchmark_type === 'single_turn' && @@ -148,13 +175,10 @@ function overviewWorkloadRows(rows: readonly BenchmarkRow[]): BenchmarkRow[] { ); } -/** Deliberately from raw rows, not retained winners: an unranked precision or - * engine still dates the dataset it was measured in. */ -export function overviewDatasetThroughDate( - rows: readonly BenchmarkRow[], - engineScope: OverviewEngineScope = 'community', -): string | null { - return overviewWorkloadRows(overviewEngineRows(rows, engineScope)).reduce( +/** Deliberately from raw scenario rows, not retained winners: an unranked + * precision or engine still dates the dataset it was measured in. */ +function latestOverviewDate(rows: readonly BenchmarkRow[]): string | null { + return rows.reduce( (latest, row) => (latest === null || row.date > latest ? row.date : latest), null, ); @@ -173,9 +197,13 @@ function overviewServingSeriesKey(row: BenchmarkRow): string { } /** Chart-equivalent serving series: topology variants are points on one curve. */ -function buildConfigs(model: Model, workloadRows: readonly BenchmarkRow[]): OverviewConfigResult[] { +function buildConfigs( + model: Model, + scenario: OverviewScenario, + scenarioRows: readonly BenchmarkRow[], +): OverviewConfigResult[] { const rowsByConfig = new Map(); - for (const row of workloadRows) { + for (const row of scenarioRows) { if (!OVERVIEW_PRECISIONS.includes(row.precision)) continue; const key = overviewServingSeriesKey(row); const configRows = rowsByConfig.get(key); @@ -190,7 +218,7 @@ function buildConfigs(model: Model, workloadRows: readonly BenchmarkRow[]): Over configRows[0].date, ); const latestRows = configRows.filter((row) => row.date === latestDate); - const config = buildConfigResult(model, latestRows[0].precision, key, latestRows); + const config = buildConfigResult(model, scenario, latestRows[0].precision, key, latestRows); if (config) configs.push(config); } return configs; @@ -255,9 +283,8 @@ function nonComparableAsMissing( function configPriorityIndex(config: OverviewConfigResult): number { return OVERVIEW_SLICE_PRIORITY.findIndex( - (priority) => - priority.precision === config.precision && - priority.decodeMode === decodeModeForSpecMethod(config.specMethod), + ({ speculative, precision }) => + speculative === isSpeculativeDecode(config.specMethod) && precision === config.precision, ); } @@ -275,18 +302,18 @@ function selectPlatformRead( .filter( (read) => read.config.precision === priority.precision && - decodeModeForSpecMethod(read.config.specMethod) === priority.decodeMode && + isSpeculativeDecode(read.config.specMethod) === priority.speculative && isInRangeTierRead(read), ) .toSorted(compareTierReads)[0]; if (exact) return exact; } - const fallback = reads.toSorted( + const bestMissingRead = reads.toSorted( (a, b) => configPriorityIndex(a.config) - configPriorityIndex(b.config) || compareTierReads(a, b), )[0]; - return fallback ? nonComparableAsMissing(fallback, tier) : nullTierRead(tier); + return bestMissingRead ? nonComparableAsMissing(bestMissingRead, tier) : nullTierRead(tier); } function missingReasonForPlatform( @@ -297,13 +324,14 @@ function missingReasonForPlatform( ): OverviewMissingReason | null { if (isInRangeTierRead(read)) return null; const hardwareRows = workloadRows.filter((row) => row.hardware === hardware); - if (hardwareRows.length === 0) return 'no_8k1k_data'; + if (hardwareRows.length === 0) return 'no_scenario_data'; const supportedRows = hardwareRows.filter((row) => OVERVIEW_PRECISIONS.includes(row.precision)); if (supportedRows.length === 0) return 'int4_bf16_only'; + if (bucketReads.length === 0) return 'no_scenario_data'; // `cannot reach` is a claim about the whole platform, so it holds only when // EVERY qualified serving series tops out below the tier — one merely // under-swept stack downgrades the gap to a missing exact read. - return bucketReads.length > 0 && bucketReads.every((r) => r.boundary === 'unreachable') + return bucketReads.every((r) => r.boundary === 'unreachable') ? 'cannot_reach_at_tier' : 'no_exact_at_tier'; } @@ -320,10 +348,11 @@ export function overviewCostPerMtok( function buildPlatformResults( model: Model, - workloadRows: readonly BenchmarkRow[], + scenario: OverviewScenario, + scenarioRows: readonly BenchmarkRow[], tier: OverviewTier, ): OverviewPlatformResult[] { - const configs = buildConfigs(model, workloadRows); + const configs = buildConfigs(model, scenario, scenarioRows); const readsForHardware = (hardware: string): OverviewTierRead[] => configs .filter((config) => config.hardware === hardware) @@ -335,10 +364,9 @@ function buildPlatformResults( hardware, hardwareLabel: overviewHardwareLabel(hardware, model), precision: read.config?.precision ?? null, - decodeMode: read.config === null ? null : decodeModeForSpecMethod(read.config.specMethod), read, missingReason: missingReasonForPlatform( - workloadRows, + scenarioRows, hardware, read, readsForHardware(hardware), @@ -357,37 +385,82 @@ function buildPlatformResults( })); } +function deployedGpuFactor(row: BenchmarkRow): number { + const totalGpus = row.num_prefill_gpu + row.num_decode_gpu; + return row.disagg && row.num_prefill_gpu > 0 && row.num_decode_gpu > 0 && totalGpus > 0 + ? row.num_decode_gpu / totalGpus + : 1; +} + +function topologyEvidence(row: BenchmarkRow): string | undefined { + return row.disagg && row.num_prefill_gpu > 0 && row.num_decode_gpu > 0 + ? `${row.num_prefill_gpu}P+${row.num_decode_gpu}D` + : undefined; +} + +interface OverviewAgenticTierPoint extends TcoTierPoint { + e2eLatency: number; + throughput: number; +} + +function buildAgenticTierReads(rows: readonly BenchmarkRow[]) { + const points = rows.flatMap((row): OverviewAgenticTierPoint[] => { + const entry = rowToAggDataEntry(row); + const factor = deployedGpuFactor(row); + const interactivity = entry.p90_intvty; + const e2eLatency = entry.p90_e2el; + const outputThroughput = entry.output_tput_per_gpu * factor; + if ( + !Number.isFinite(interactivity) || + interactivity <= 0 || + !Number.isFinite(e2eLatency) || + e2eLatency <= 0 || + !Number.isFinite(outputThroughput) || + outputThroughput <= 0 + ) { + return []; + } + return [ + { + interactivity, + e2eLatency, + throughput: outputThroughput, + outputThroughput, + date: row.date, + evidenceLabel: topologyEvidence(row), + }, + ]; + }); + return computeTierReads(restrictAgenticPointsToE2eFrontier(points), OVERVIEW_TIERS); +} + function buildConfigResult( model: Model, + scenario: OverviewScenario, precision: string, key: string, rows: BenchmarkRow[], ): OverviewConfigResult | null { - const feedRows = rows.map((row) => { - const outputTputPerGpu = row.metrics.output_tput_per_gpu; - const totalGpus = row.num_prefill_gpu + row.num_decode_gpu; - const comparableOutputTputPerGpu = - row.disagg && - row.num_prefill_gpu > 0 && - row.num_decode_gpu > 0 && - totalGpus > 0 && - Number.isFinite(outputTputPerGpu) - ? (outputTputPerGpu * row.num_decode_gpu) / totalGpus - : outputTputPerGpu; - const evidenceLabel = - row.disagg && row.num_prefill_gpu > 0 && row.num_decode_gpu > 0 - ? `${row.num_prefill_gpu}P+${row.num_decode_gpu}D` - : undefined; - return { - ...row, - metrics: { - ...row.metrics, - output_tput_per_gpu: comparableOutputTputPerGpu, - }, - evidence_label: evidenceLabel, - }; - }); - const feed = computeTcoFeed(feedRows, [OVERVIEW_WORKLOAD], OVERVIEW_TIERS); + const feed = + scenario === 'agentx' + ? buildAgenticTierReads(rows) + : computeTcoFeed( + rows.map((row) => { + const outputTputPerGpu = row.metrics.output_tput_per_gpu; + return { + ...row, + metrics: { + ...row.metrics, + output_tput_per_gpu: Number.isFinite(outputTputPerGpu) + ? outputTputPerGpu * deployedGpuFactor(row) + : outputTputPerGpu, + }, + evidence_label: topologyEvidence(row), + }; + }), + [OVERVIEW_WORKLOAD], + OVERVIEW_TIERS, + ); if (feed.length === 0) return null; const first = rows[0]; @@ -432,10 +505,13 @@ export function buildOverviewModelSummary( engineScope: OverviewEngineScope = 'community', ): OverviewModelSummary { const scopedRows = overviewEngineRows(rows, engineScope); + const scenario = overviewScenarioForModel(model, rows); + const scenarioRows = overviewScenarioRows(scenario, scopedRows); return { model, modelLabel: getModelLabel(model), - platforms: buildPlatformResults(model, overviewWorkloadRows(scopedRows), tier), + scenario, + platforms: buildPlatformResults(model, scenario, scenarioRows, tier), }; } @@ -451,9 +527,11 @@ export function assembleOverviewPageData( models: perModel.map(({ model, rows }) => buildOverviewModelSummary(model, rows, tier, engineScope), ), - datasetThroughDate: overviewDatasetThroughDate( - perModel.flatMap(({ rows }) => rows), - engineScope, + datasetThroughDate: latestOverviewDate( + perModel.flatMap(({ model, rows }) => { + const scenario = overviewScenarioForModel(model, rows); + return overviewScenarioRows(scenario, overviewEngineRows(rows, engineScope)); + }), ), tier, engineScope, diff --git a/packages/app/src/lib/overview-links.test.ts b/packages/app/src/lib/overview-links.test.ts index 3711d1554..31950c81e 100644 --- a/packages/app/src/lib/overview-links.test.ts +++ b/packages/app/src/lib/overview-links.test.ts @@ -55,6 +55,7 @@ function summary(overrides: Partial = {}): OverviewModelSu return { model: Model.Qwen3_5, modelLabel: 'Qwen 3.5', + scenario: 'single_turn_8k1k', platforms: [], ...overrides, }; @@ -93,6 +94,18 @@ describe('buildOverviewDashboardHref', () => { expect(href).toContain('i_prec=fp8'); }); + it('opens AgentX evidence in the Agentic Traces dashboard scenario', () => { + const href = buildOverviewDashboardHref( + 'en', + summary({ model: Model.GLM_5_2, scenario: 'agentx' }), + config(), + ); + + expect(href).toContain('g_model=GLM-5.2'); + expect(href).toContain('i_seq=agentic-traces'); + expect(href).not.toContain('i_seq=8k%2F1k'); + }); + it('maps specMethod to the dashboard mtp/stp filter bucket, not the raw DB value', () => { expect(buildOverviewDashboardHref('en', summary(), config({ specMethod: 'eagle' }))).toContain( 'i_spec=mtp', @@ -115,6 +128,12 @@ describe('detailHref', () => { '/inference?g_model=Qwen-3.5-397B-A17B&i_seq=8k%2F1k&i_optimal=1', ); }); + + it('opens AgentX rows in the Agentic Traces dashboard scenario', () => { + expect(detailHref('en', summary({ model: Model.GLM_5_2, scenario: 'agentx' }))).toBe( + '/inference?g_model=GLM-5.2&i_seq=agentic-traces&i_optimal=1', + ); + }); }); describe('overviewHref', () => { diff --git a/packages/app/src/lib/overview-links.ts b/packages/app/src/lib/overview-links.ts index ab8db3a3c..24e444458 100644 --- a/packages/app/src/lib/overview-links.ts +++ b/packages/app/src/lib/overview-links.ts @@ -8,8 +8,9 @@ import { } from './overview-data'; import type { UrlStateParams } from './url-state'; -/** The single-turn 8K-in/1K-out workload every overview link filters to. */ -const OVERVIEW_WORKLOAD_SEQ = '8k/1k'; +function overviewSequence(model: OverviewModelSummary): '8k/1k' | 'agentic-traces' { + return model.scenario === 'agentx' ? 'agentic-traces' : '8k/1k'; +} /** The `/inference` route base for a locale — shared by every overview link. */ function inferenceRoute(locale: 'en' | 'zh'): string { @@ -57,7 +58,7 @@ export function buildOverviewDashboardHref( g_model: model.model, g_rundate: config.latestDate, g_runid: soleSourceRun(config)?.id, - i_seq: OVERVIEW_WORKLOAD_SEQ, + i_seq: overviewSequence(model), i_prec: config.precision, i_metric: 'y_outputTputPerGpu', i_gpus: config.hwKey, @@ -81,7 +82,7 @@ export function buildOverviewDashboardHref( export function detailHref(locale: 'en' | 'zh', model: OverviewModelSummary): string { const query = new URLSearchParams({ g_model: model.model, - i_seq: OVERVIEW_WORKLOAD_SEQ, + i_seq: overviewSequence(model), i_optimal: '1', }); return `${inferenceRoute(locale)}?${query}`; diff --git a/packages/app/src/lib/tco-feed.ts b/packages/app/src/lib/tco-feed.ts index 0003fdf0e..ced6be3d1 100644 --- a/packages/app/src/lib/tco-feed.ts +++ b/packages/app/src/lib/tco-feed.ts @@ -81,6 +81,15 @@ export interface TcoFeedRow { evidence_labels?: string[]; } +export interface TcoTierPoint { + interactivity: number; + outputThroughput: number; + date: string; + evidenceLabel?: string; +} + +export type TcoTierRead = Omit; + export const DEFAULT_TIERS: readonly number[] = [30, 50, 75, 100]; export const DEFAULT_WORKLOADS: readonly TcoFeedWorkload[] = [ { isl: 1024, osl: 1024 }, @@ -207,21 +216,14 @@ export function parseAlpha(raw: string | null): number | null { return value; } -interface FrontierPoint { - interactivity: number; - throughput: number; - date: string; - evidenceLabel?: string; -} - const round3 = (v: number): number => Math.round(v * 1000) / 1000; /** Two knots' dates as an ascending {from,to} evidence range. */ -function evidenceRange(a: FrontierPoint, b: FrontierPoint): { from: string; to: string } { +function evidenceRange(a: TcoTierPoint, b: TcoTierPoint): { from: string; to: string } { return a.date <= b.date ? { from: a.date, to: b.date } : { from: b.date, to: a.date }; } -function evidenceLabels(a: FrontierPoint, b: FrontierPoint): string[] | undefined { +function evidenceLabels(a: TcoTierPoint, b: TcoTierPoint): string[] | undefined { const labels = [...new Set([a.evidenceLabel, b.evidenceLabel].filter(Boolean))] as string[]; return labels.length === 0 ? undefined : labels; } @@ -231,7 +233,7 @@ function evidenceLabels(a: FrontierPoint, b: FrontierPoint): string[] | undefine * pair xs[lo] ≤ tier ≤ xs[lo+1], collapsing to a single knot when `tier` lands * on an endpoint. `frontier` is sorted ascending by interactivity. */ -function bracketKnots(frontier: FrontierPoint[], tier: number): [FrontierPoint, FrontierPoint] { +function bracketKnots(frontier: TcoTierPoint[], tier: number): [TcoTierPoint, TcoTierPoint] { const last = frontier.length - 1; if (tier <= frontier[0].interactivity) return [frontier[0], frontier[0]]; if (tier >= frontier[last].interactivity) return [frontier[last], frontier[last]]; @@ -243,6 +245,72 @@ function bracketKnots(frontier: FrontierPoint[], tier: number): [FrontierPoint, return [frontier[lo], frontier[lo + 1]]; } +/** Read fixed service tiers from an output-throughput serving frontier. */ +export function computeTierReads( + points: readonly TcoTierPoint[], + tiers: readonly number[], +): TcoTierRead[] { + const frontier = paretoFrontUpperLeft( + [...points], + (point) => point.interactivity, + (point) => point.outputThroughput, + ).toSorted((a, b) => a.interactivity - b.interactivity); + if (frontier.length === 0) return []; + + const xs = frontier.map((point) => point.interactivity); + const ys = frontier.map((point) => point.outputThroughput); + const slopes = monotoneSlopes(xs, ys); + const minIv = xs[0]; + const maxIv = xs.at(-1)!; + const yLo = Math.min(...ys); + const yHi = Math.max(...ys); + let latest = frontier[0].date; + let oldest = frontier[0].date; + for (const point of frontier) { + if (point.date > latest) latest = point.date; + if (point.date < oldest) oldest = point.date; + } + + return tiers.map((tier) => { + let value: number; + let boundary: TcoTierBoundary; + let isInterpolated = false; + let evidenceDate: { from: string; to: string } | null; + let tierEvidenceLabels: string[] | undefined; + if (tier > maxIv) { + value = 0; + boundary = 'unreachable'; + evidenceDate = null; + } else if (tier < minIv) { + value = ys[0]; + boundary = 'clamped_low'; + evidenceDate = evidenceRange(frontier[0], frontier[0]); + tierEvidenceLabels = evidenceLabels(frontier[0], frontier[0]); + } else { + const [lo, hi] = bracketKnots(frontier, tier); + const raw = hermiteInterpolate(xs, ys, slopes, tier); + value = Math.max(yLo, Math.min(yHi, raw)); + boundary = 'interpolated'; + isInterpolated = lo.interactivity !== hi.interactivity; + evidenceDate = evidenceRange(lo, hi); + tierEvidenceLabels = evidenceLabels(lo, hi); + } + return { + tier, + output_tput_per_gpu: round3(value), + boundary, + is_interpolated: isInterpolated, + frontier_points: frontier.length, + frontier_min_interactivity: round3(minIv), + frontier_max_interactivity: round3(maxIv), + latest_date: latest, + oldest_frontier_date: oldest, + evidence_date: evidenceDate, + ...(tierEvidenceLabels === undefined ? {} : { evidence_labels: tierEvidenceLabels }), + }; + }); +} + /** * Compute the feed: for each (workload, hardware), build the * (interactivity, output tok/s/GPU) Pareto frontier across every config — @@ -256,7 +324,7 @@ export function computeTcoFeed( const out: TcoFeedRow[] = []; for (const workload of workloads) { - const byHardware = new Map(); + const byHardware = new Map(); for (const row of rows) { // Rows predating the benchmark_type column are fixed-seq by definition // (agentic rows are newer and carry isl/osl = null, which the workload @@ -281,7 +349,7 @@ export function computeTcoFeed( if (!Number.isFinite(otput) || otput <= 0) continue; const point = { interactivity, - throughput: otput, + outputThroughput: otput, date: row.date, evidenceLabel: row.evidence_label, }; @@ -293,68 +361,11 @@ export function computeTcoFeed( const workloadKey = `${workload.isl}x${workload.osl}`; const hardwareKeys = [...byHardware.keys()].toSorted((a, b) => a.localeCompare(b)); for (const hardware of hardwareKeys) { - const frontier = paretoFrontUpperLeft( - byHardware.get(hardware)!, - (p) => p.interactivity, - (p) => p.throughput, - ).toSorted((a, b) => a.interactivity - b.interactivity); - if (frontier.length === 0) continue; - - const xs = frontier.map((p) => p.interactivity); - const ys = frontier.map((p) => p.throughput); - const slopes = monotoneSlopes(xs, ys); - const minIv = xs[0]; - const maxIv = xs.at(-1)!; - // Clamp bounds against spline overshoot, mirroring interpolateForGPU. - const yLo = Math.min(...ys); - const yHi = Math.max(...ys); - // 'YYYY-MM-DD' compares chronologically as a string. - let latest = frontier[0].date; - let oldest = frontier[0].date; - for (const p of frontier) { - if (p.date > latest) latest = p.date; - if (p.date < oldest) oldest = p.date; - } - - for (const tier of tiers) { - let value: number; - let boundary: TcoTierBoundary; - let isInterpolated = false; - let evidenceDate: { from: string; to: string } | null; - let tierEvidenceLabels: string[] | undefined; - if (tier > maxIv) { - value = 0; - boundary = 'unreachable'; - evidenceDate = null; - } else if (tier < minIv) { - value = ys[0]; - boundary = 'clamped_low'; - // The clamped read is the min-interactivity knot's throughput. - evidenceDate = evidenceRange(frontier[0], frontier[0]); - tierEvidenceLabels = evidenceLabels(frontier[0], frontier[0]); - } else { - const [lo, hi] = bracketKnots(frontier, tier); - const raw = hermiteInterpolate(xs, ys, slopes, tier); - value = Math.max(yLo, Math.min(yHi, raw)); - boundary = 'interpolated'; - isInterpolated = lo.interactivity !== hi.interactivity; - evidenceDate = evidenceRange(lo, hi); - tierEvidenceLabels = evidenceLabels(lo, hi); - } + for (const read of computeTierReads(byHardware.get(hardware)!, tiers)) { out.push({ hardware, workload: workloadKey, - tier, - output_tput_per_gpu: round3(value), - boundary, - is_interpolated: isInterpolated, - frontier_points: frontier.length, - frontier_min_interactivity: round3(minIv), - frontier_max_interactivity: round3(maxIv), - latest_date: latest, - oldest_frontier_date: oldest, - evidence_date: evidenceDate, - ...(tierEvidenceLabels === undefined ? {} : { evidence_labels: tierEvidenceLabels }), + ...read, }); } }