diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..3c258190 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Deliberately compact test fixture (one benchmark row per line, <=60 lines); +# oxfmt/prettier would pretty-print it to ~530 lines. See overview-data.test.ts drift guard. +packages/app/cypress/fixtures/api/overview-rows.json diff --git a/packages/app/cypress/component/header.cy.tsx b/packages/app/cypress/component/header.cy.tsx index 747396ea..308b31d4 100644 --- a/packages/app/cypress/component/header.cy.tsx +++ b/packages/app/cypress/component/header.cy.tsx @@ -5,10 +5,24 @@ import { PathnameContext } from 'next/dist/shared/lib/hooks-client-context.share import { Header } from '@/components/header/header'; import { ThemeProvider } from '@/components/ui/theme-provider'; +// Component specs mount outside the Next app shell, so the global stylesheet is +// never injected. The layout assertions below need real Tailwind output, and +// next-style-loader inserts before this anchor node, so both must exist before +// the stylesheet module evaluates. +const cssAnchor = document.createElement('noscript'); +cssAnchor.id = '__next_css__DO_NOT_USE__'; +document.head.append(cssAnchor); +require('@/app/globals.css'); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); +/** Minimum touch target, per WCAG 2.5.8 and the `size-11` / `min-h-11` utilities. */ +const MIN_TOUCH_PX = 44; +/** Tolerance for sub-pixel layout rounding. */ +const EPSILON = 0.5; + function createMockRouter() { return { push: cy.stub(), @@ -20,6 +34,10 @@ function createMockRouter() { }; } +function rectOf(selector: string) { + return cy.get(selector).then(($el) => $el[0].getBoundingClientRect()); +} + describe('Header', () => { beforeEach(() => { const mockRouter = createMockRouter(); @@ -84,4 +102,87 @@ describe('Header', () => { cy.contains('a', 'Supporters').should('be.visible').and('have.attr', 'href', '/quotes'); }); }); + + describe('at 320x700', () => { + beforeEach(() => { + cy.viewport(320, 700); + }); + + it('hides the GitHub star control', () => { + cy.get('[data-testid="header-star-button"]').should('not.be.visible'); + }); + + it('keeps the remaining controls inside the header bounds', () => { + cy.get('[data-testid="header"]').then(($header) => { + const bounds = $header[0].getBoundingClientRect(); + const selectors = [ + '[data-testid="header-brand"]', + '[data-testid="language-toggle"]', + '[data-testid="theme-toggle"]', + '[data-testid="mobile-menu-toggle"]', + ]; + selectors.forEach((selector) => { + rectOf(selector).then((rect) => { + expect(rect.left, `${selector} left edge`).to.be.at.least(bounds.left - EPSILON); + expect(rect.right, `${selector} right edge`).to.be.at.most(bounds.right + EPSILON); + }); + }); + }); + }); + + it('gives the brand and language controls a 44px touch height', () => { + ['[data-testid="header-brand"]', '[data-testid="language-toggle"]'].forEach((selector) => { + rectOf(selector).then((rect) => { + expect(rect.height, `${selector} height`).to.be.at.least(MIN_TOUCH_PX - EPSILON); + }); + }); + }); + + it('gives the icon controls a 44px touch target in both dimensions', () => { + ['[data-testid="theme-toggle"]', '[data-testid="mobile-menu-toggle"]'].forEach((selector) => { + rectOf(selector).then((rect) => { + expect(rect.width, `${selector} width`).to.be.at.least(MIN_TOUCH_PX - EPSILON); + expect(rect.height, `${selector} height`).to.be.at.least(MIN_TOUCH_PX - EPSILON); + }); + }); + }); + + it('does not overflow horizontally', () => { + cy.get('[data-testid="header"]').then(($header) => { + const header = $header[0]; + expect(header.scrollWidth, 'header scrollWidth').to.be.at.most(header.clientWidth); + }); + }); + + it('still opens the menu and exposes its links', () => { + cy.get('[data-testid="mobile-menu-toggle"]').click(); + cy.get('[data-testid="mobile-menu"]').should('be.visible'); + cy.get('[data-testid="mobile-menu"]').within(() => { + ['Home', 'Dashboard', 'Comparisons', 'Supporters', 'Datasets', 'Articles', 'About'].forEach( + (label) => { + cy.contains('a', label).should('be.visible'); + }, + ); + }); + cy.get('[data-testid="mobile-menu"] a').each(($link) => { + const rect = $link[0].getBoundingClientRect(); + expect(rect.height, `${$link.text()} link height`).to.be.at.least(MIN_TOUCH_PX - EPSILON); + }); + }); + + it('exposes the minecraft audio toggles in the mobile menu without overflowing', () => { + cy.get('[data-testid="theme-toggle"]').click(); + cy.get('[data-testid="theme-toggle"]').click(); + cy.get('html').should('have.class', 'minecraft'); + cy.get('[data-testid="mobile-menu-toggle"]').click(); + cy.get('[data-testid="mobile-menu"]').within(() => { + cy.get('button[aria-label="Mute music"]').should('be.visible'); + cy.get('button[aria-label="Mute click sounds"]').should('be.visible'); + }); + cy.get('[data-testid="header"]').then(($header) => { + const header = $header[0]; + expect(header.scrollWidth, 'header scrollWidth').to.be.at.most(header.clientWidth); + }); + }); + }); }); diff --git a/packages/app/cypress/e2e/overview.cy.ts b/packages/app/cypress/e2e/overview.cy.ts new file mode 100644 index 00000000..fc63f3be --- /dev/null +++ b/packages/app/cypress/e2e/overview.cy.ts @@ -0,0 +1,321 @@ +const MODEL_LABELS = [ + 'DeepSeek V4 Pro 1.6T', + 'Kimi K2.5/2.6/2.7-Code 1T', + 'MiniMax M3 428B', + 'GLM5.2', + 'Qwen3.5 397B', +]; + +const PLATFORM_HEADERS = [ + 'Model', + 'B200', + 'MI355X', + 'B300', + 'GB200 NVL72', + 'GB300 NVL72', + 'Details', +]; + +function expectNoHorizontalOverflow() { + cy.document().then((doc) => { + expect(doc.documentElement.scrollWidth).to.be.lte(doc.documentElement.clientWidth); + }); +} + +function expectNoHorizontalScroller(testId: string) { + cy.get(`[data-testid="${testId}"]`).then(([surface]) => { + const scrollers = [surface, ...surface.querySelectorAll('*')] + .filter( + (el) => + !el.classList.contains('sr-only') && + getComputedStyle(el).display !== 'inline' && + el.scrollWidth > el.clientWidth + 1, + ) + .map((el) => `${el.tagName} ${el.scrollWidth}>${el.clientWidth}`); + expect(scrollers, `horizontally scrollable inside ${testId}`).to.deep.equal([]); + }); +} + +function desktopModel(model: string) { + return cy.get(`[data-testid="overview-desktop-model"][data-model="${model}"]`); +} + +function mobileModel(model: string) { + return cy.get(`[data-testid="overview-mobile-model"][data-model="${model}"]`); +} + +function pair(pairId: string) { + return cy.get(`[data-testid="overview-pair"][data-pair="${pairId}"]`); +} + +describe('Overview page', () => { + it('renders the full platform matrix for every active model', () => { + cy.viewport(1280, 900); + cy.visit('/overview'); + + cy.contains('h1', 'AI Inference Overview').should('exist'); + cy.contains( + 'Every active model across MI355X, B200, B300, GB200 and GB300 at a glance.', + ).should('exist'); + cy.contains('Best validated stack per platform').should('exist'); + cy.contains('Database snapshot through Jul 18').should('exist'); + cy.get('[data-testid="overview-desktop-matrix"]') + .should('be.visible') + .within(() => { + cy.get('thead th').then(($headers) => { + expect([...$headers].map((header) => header.textContent?.trim())).to.deep.equal( + PLATFORM_HEADERS, + ); + }); + cy.get('[data-testid="overview-desktop-model"]').should('have.length', MODEL_LABELS.length); + cy.get('[data-testid="overview-baseline"]').should('have.length', MODEL_LABELS.length); + cy.get('[data-testid="overview-pair"]').should('have.length', MODEL_LABELS.length * 4); + 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); + } + }); + + it('shows per-cell best reads with precision badges, dates, links, and matched deltas', () => { + cy.viewport(1280, 900); + cy.visit('/overview'); + + desktopModel('Qwen-3.5-397B-A17B').within(() => { + cy.get('[data-testid="overview-baseline"][data-hardware="b200"]') + .should('contain.text', '900') + .and('contain.text', 'FP8'); + pair('mi355x-vs-b200').within(() => { + cy.contains('SGLang · FP8').should('exist'); + cy.get('[data-testid="overview-pair-value"][data-hardware="mi355x"]') + .should('contain.text', '760') + .find('a') + .should('have.attr', 'title', 'MI355X · SGLang · FP8 · MTP'); + cy.get('[data-testid="overview-pair-value"][data-hardware="mi355x"] a') + .should('have.attr', 'href') + .and('include', 'g_model=Qwen-3.5-397B-A17B') + .and('include', 'i_prec=fp8') + .and('include', 'i_gpus=mi355x_sglang_mtp'); + cy.get('[data-testid="overview-pair-evidence-date"][data-hardware="mi355x"]').should( + 'have.text', + 'Jul 18', + ); + cy.get('[data-testid="overview-pair-delta"]').should('have.text', '−16% vs B200'); + cy.contains('At 100, MI355X leads').should('exist'); + }); + }); + + desktopModel('DeepSeek-V4-Pro').within(() => { + cy.get('[data-testid="overview-baseline"][data-hardware="b200"]') + .should('contain.text', '900') + .and('contain.text', 'FP4'); + pair('b300-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-value"][data-hardware="b300"]').should( + 'contain.text', + '1,122', + ); + cy.get('[data-testid="overview-pair-evidence-date"][data-hardware="b300"]').should( + 'have.text', + 'Jun 24–Jul 4', + ); + cy.get('[data-testid="overview-pair-delta"]').should('have.text', '+25% vs B200'); + }); + }); + }); + + it('never mixes precisions or releases in a delta and says so instead', () => { + cy.viewport(1280, 900); + cy.visit('/overview'); + + desktopModel('DeepSeek-V4-Pro').within(() => { + pair('gb200-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-value"][data-hardware="gb200"]') + .should('contain.text', '600') + .parents('[data-testid="overview-pair"]') + .should('contain.text', 'FP8'); + cy.get('[data-testid="overview-pair-mismatch"]').should( + 'have.text', + 'FP8 vs FP4 · no comparable delta', + ); + cy.get('[data-testid="overview-pair-delta"]').should('not.exist'); + }); + }); + + desktopModel('Qwen-3.5-397B-A17B').within(() => { + pair('b300-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-value"][data-hardware="b300"]').should( + 'contain.text', + '1,151', + ); + cy.get('[data-testid="overview-pair-mismatch"]').should( + 'have.text', + 'FP4 vs FP8 · no comparable delta', + ); + cy.get('[data-testid="overview-pair-delta"]').should('not.exist'); + }); + }); + }); + + it('distinguishes candidate, baseline, and whole-row missing results without a percent', () => { + cy.viewport(1280, 900); + cy.visit('/overview'); + + desktopModel('DeepSeek-V4-Pro').within(() => { + pair('mi355x-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-missing"][data-hardware="mi355x"]') + .should('contain.text', '∞') + .and('have.attr', 'title', 'no exact @50 result'); + }); + pair('gb300-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-missing"][data-hardware="gb300"]') + .should('contain.text', '∞') + .and('have.attr', 'title', 'cannot reach @50'); + }); + }); + + desktopModel('MiniMax-M3').within(() => { + cy.get('[data-testid="overview-baseline"] [data-testid="overview-pair-missing"]') + .should('contain.text', '∞') + .and('have.attr', 'title', 'no 8K/1K data'); + pair('gb300-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-value"][data-hardware="gb300"]').should( + 'contain.text', + '700', + ); + cy.get('[data-testid="overview-pair-delta"]').should('not.exist'); + cy.get('[data-testid="overview-pair-mismatch"]').should('not.exist'); + }); + }); + + desktopModel('Kimi-K2.5').within(() => { + cy.get('[data-testid="overview-pair-missing"]').should('have.length', 5); + cy.get('[data-testid="overview-pair-value"]').should('not.exist'); + }); + cy.contains('∞ = no comparable result').should('exist'); + cy.get('body') + .invoke('text') + .should('not.match', /∞\s*%/); + }); + + it('re-renders the whole matrix at the service level the URL names, via plain links', () => { + cy.viewport(1280, 900); + cy.visit('/overview'); + + cy.get('[data-testid="overview-tier-switcher"]').within(() => { + cy.get('[aria-current="page"]').should('have.text', '50'); + cy.get('a').should('have.length', 3); + cy.contains('a', '30').should('have.attr', 'href', '/overview?tier=30'); + cy.contains('a', '100').should('have.attr', 'href', '/overview?tier=100').click(); + }); + + cy.location('search').should('eq', '?tier=100'); + cy.contains('Output tok/s/GPU @100 tok/s/user').should('exist'); + 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'); + }); + + desktopModel('Qwen-3.5-397B-A17B').within(() => { + cy.get('[data-testid="overview-baseline"][data-hardware="b200"]') + .should('contain.text', '432') + .and('contain.text', 'FP8'); + pair('mi355x-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-value"][data-hardware="mi355x"]').should( + 'contain.text', + '635', + ); + cy.get('[data-testid="overview-pair-delta"]').should('have.text', '+47% vs B200'); + cy.contains('At 100, MI355X leads').should('not.exist'); + }); + pair('b300-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-missing"][data-hardware="b300"]') + .should('contain.text', '∞') + .and('have.attr', 'title', 'cannot reach @100'); + }); + }); + + cy.visit('/overview?tier=30'); + desktopModel('DeepSeek-V4-Pro').within(() => { + pair('b300-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-value"][data-hardware="b300"]').should( + 'contain.text', + '1,300', + ); + cy.get('[data-testid="overview-pair-delta"]').should('not.exist'); + }); + cy.get('[data-testid="overview-baseline"] [data-testid="overview-pair-missing"]') + .should('contain.text', '∞') + .and('have.attr', 'title', 'no exact @30 result'); + }); + cy.get('body') + .invoke('text') + .should('not.match', /∞\s*%/); + + cy.visit('/overview?tier=100'); + cy.contains('Output tok/s/GPU @100 tok/s/user').should('exist'); + cy.get('[data-testid="language-toggle"]').click(); + cy.location('pathname').should('eq', '/zh/overview'); + cy.location('search').should('eq', '?tier=100'); + cy.contains('每 GPU 输出 tok/s @100 tok/s/用户').should('exist'); + }); + + it('uses the same cell semantics on mobile and fits both 390px and 320px widths', () => { + for (const width of [390, 320]) { + cy.viewport(width, 844); + cy.visit('/overview'); + + cy.get('[data-testid="overview-mobile-list"]').should('be.visible'); + cy.get('[data-testid="overview-tier-switcher"]').should('be.visible'); + cy.get('[data-testid="overview-desktop-matrix"]').should('not.be.visible'); + mobileModel('Qwen-3.5-397B-A17B').within(() => { + cy.get('[data-testid="overview-pair"]').should('have.length', 4); + pair('mi355x-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-value"][data-hardware="mi355x"]').should( + 'contain.text', + '760', + ); + cy.get('[data-testid="overview-pair-delta"]').should('have.text', '−16% vs B200'); + }); + }); + expectNoHorizontalOverflow(); + expectNoHorizontalScroller('overview-mobile-list'); + } + }); + + it('renders the Chinese sibling with equivalent matrix copy and semantics', () => { + cy.viewport(1280, 900); + cy.visit('/zh/overview'); + + cy.contains('h1', 'AI 推理总览').should('exist'); + cy.contains('一眼对比各活跃模型在 MI355X、B200、B300、GB200 与 GB300 上的表现。').should( + 'exist', + ); + cy.contains('各平台最佳验证配置').should('exist'); + desktopModel('Qwen-3.5-397B-A17B').within(() => { + pair('mi355x-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-delta"]').should('have.text', '相对 B200 −16%'); + cy.contains('100 档由 MI355X 领先').should('exist'); + }); + pair('b300-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-mismatch"]').should( + 'have.text', + 'FP4 与 FP8 · 无可比差值', + ); + }); + }); + cy.contains('∞ = 无可比结果').should('exist'); + + cy.visit('/zh/overview?tier=100'); + cy.contains('每 GPU 输出 tok/s @100 tok/s/用户').should('exist'); + desktopModel('Qwen-3.5-397B-A17B').within(() => { + pair('mi355x-vs-b200').within(() => { + cy.get('[data-testid="overview-pair-delta"]').should('have.text', '相对 B200 +47%'); + cy.contains('100 档由 MI355X 领先').should('not.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/fixtures/api/overview-rows.json b/packages/app/cypress/fixtures/api/overview-rows.json new file mode 100644 index 00000000..96a441b0 --- /dev/null +++ b/packages/app/cypress/fixtures/api/overview-rows.json @@ -0,0 +1,29 @@ +{ + "DeepSeek-V4-Pro": [ + {"id": 1, "hardware": "b300", "framework": "sglang", "model": "dsv4", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 8, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 30, "output_tput_per_gpu": 1300}, "date": "2026-06-24", "run_url": null}, + {"id": 2, "hardware": "b300", "framework": "sglang", "model": "dsv4", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 16, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 70, "output_tput_per_gpu": 1000}, "date": "2026-07-04", "run_url": null}, + {"id": 3, "hardware": "b200", "framework": "sglang", "model": "dsv4", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 8, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 40, "output_tput_per_gpu": 1050}, "date": "2026-07-18", "run_url": null}, + {"id": 4, "hardware": "b200", "framework": "sglang", "model": "dsv4", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 24, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 110, "output_tput_per_gpu": 300}, "date": "2026-07-18", "run_url": null}, + {"id": 5, "hardware": "mi355x", "framework": "sglang", "model": "dsv4", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 16, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 60, "output_tput_per_gpu": 760}, "date": "2026-07-18", "run_url": null}, + {"id": 6, "hardware": "gb300", "framework": "sglang", "model": "dsv4", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 8, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 45, "output_tput_per_gpu": 1050}, "date": "2026-07-18", "run_url": null}, + {"id": 7, "hardware": "h100", "framework": "sglang", "model": "dsv4", "precision": "fp4", "spec_method": "none", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 8, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 30, "output_tput_per_gpu": 800}, "date": "2026-07-18", "run_url": null}, + {"id": 8, "hardware": "gb200", "framework": "sglang", "model": "dsv4", "precision": "fp8", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 8, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 50, "output_tput_per_gpu": 600}, "date": "2026-07-18", "run_url": null} + ], + "Qwen-3.5-397B-A17B": [ + {"id": 9, "hardware": "b300", "framework": "sglang", "model": "qwen3.5", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 8, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 30, "output_tput_per_gpu": 1400}, "date": "2026-07-18", "run_url": null}, + {"id": 10, "hardware": "b300", "framework": "sglang", "model": "qwen3.5", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 16, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 70, "output_tput_per_gpu": 980}, "date": "2026-07-18", "run_url": null}, + {"id": 11, "hardware": "b200", "framework": "sglang", "model": "qwen3.5", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 8, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 40, "output_tput_per_gpu": 800}, "date": "2026-07-18", "run_url": null}, + {"id": 12, "hardware": "b200", "framework": "sglang", "model": "qwen3.5", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 24, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 80, "output_tput_per_gpu": 600}, "date": "2026-07-18", "run_url": null}, + {"id": 13, "hardware": "b200", "framework": "sglang", "model": "qwen3.5", "precision": "fp8", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 12, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 50, "output_tput_per_gpu": 900}, "date": "2026-07-18", "run_url": null}, + {"id": 14, "hardware": "mi355x", "framework": "sglang", "model": "qwen3.5", "precision": "fp8", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 12, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 50, "output_tput_per_gpu": 760}, "date": "2026-07-18", "run_url": null}, + {"id": 19, "hardware": "b200", "framework": "sglang", "model": "qwen3.5", "precision": "fp8", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 24, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 120, "output_tput_per_gpu": 300}, "date": "2026-07-18", "run_url": null}, + {"id": 20, "hardware": "mi355x", "framework": "sglang", "model": "qwen3.5", "precision": "fp8", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 24, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 120, "output_tput_per_gpu": 600}, "date": "2026-07-18", "run_url": null} + ], + "MiniMax-M3": [ + {"id": 15, "hardware": "h200", "framework": "sglang", "model": "minimaxm3", "precision": "fp8", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 8, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 30, "output_tput_per_gpu": 820}, "date": "2026-07-18", "run_url": null}, + {"id": 16, "hardware": "h200", "framework": "sglang", "model": "minimaxm3", "precision": "fp8", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 16, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 70, "output_tput_per_gpu": 560}, "date": "2026-07-18", "run_url": null}, + {"id": 17, "hardware": "mi325x", "framework": "sglang", "model": "minimaxm3", "precision": "fp8", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 12, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 50, "output_tput_per_gpu": 650}, "date": "2026-07-18", "run_url": null}, + {"id": 18, "hardware": "h200", "framework": "sglang", "model": "minimaxm3", "precision": "fp4", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 12, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 50, "output_tput_per_gpu": 600}, "date": "2026-07-18", "run_url": null}, + {"id": 21, "hardware": "gb300", "framework": "sglang", "model": "minimaxm3", "precision": "fp8", "spec_method": "mtp", "disagg": false, "is_multinode": false, "prefill_tp": 8, "prefill_ep": 1, "prefill_dp_attention": false, "prefill_num_workers": 1, "decode_tp": 8, "decode_ep": 1, "decode_dp_attention": false, "decode_num_workers": 1, "num_prefill_gpu": 8, "num_decode_gpu": 8, "benchmark_type": "single_turn", "isl": 8192, "osl": 1024, "conc": 12, "offload_mode": "off", "image": null, "metrics": {"median_intvty": 50, "output_tput_per_gpu": 700}, "date": "2026-07-18", "run_url": null} + ] +} diff --git a/packages/app/src/app/overview/page.tsx b/packages/app/src/app/overview/page.tsx new file mode 100644 index 00000000..8e1bde73 --- /dev/null +++ b/packages/app/src/app/overview/page.tsx @@ -0,0 +1,46 @@ +import type { Metadata } from 'next'; + +import { SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; + +import { OverviewPageContent } from '@/components/overview/overview-page'; +import { enAlternates } from '@/lib/i18n'; +import { resolveOverviewTier } from '@/lib/overview-data'; +import { getOverviewPageData } from '@/lib/overview-data.server'; + +export const dynamic = 'force-dynamic'; + +const DESCRIPTION = + 'Every active model across MI355X, B200, B300, GB200 and GB300 at a fixed single-turn 8K input / 1K output workload — each platform’s best validated speculative-decode serving result at 50 tok/s/user, with same-precision deltas against B200.'; + +export const metadata: Metadata = { + title: 'AI Inference Overview', + description: DESCRIPTION, + alternates: enAlternates('/overview'), + openGraph: { + title: `AI Inference Overview | ${SITE_NAME}`, + description: DESCRIPTION, + url: `${SITE_URL}/overview`, + type: 'website', + }, + twitter: { + card: 'summary_large_image', + title: `AI Inference Overview | ${SITE_NAME}`, + description: DESCRIPTION, + }, +}; + +interface Props { + searchParams: Promise>; +} + +export default async function OverviewPage({ searchParams }: Props) { + const sp = await searchParams; + const data = await getOverviewPageData(resolveOverviewTier(sp.tier)); + return ( +
+
+ +
+
+ ); +} diff --git a/packages/app/src/app/sitemap.ts b/packages/app/src/app/sitemap.ts index 32bc84d4..2dd3e934 100644 --- a/packages/app/src/app/sitemap.ts +++ b/packages/app/src/app/sitemap.ts @@ -58,6 +58,11 @@ export default async function sitemap(): Promise { return [ ...localizedPair('/', { lastModified: now, changeFrequency: 'daily', priority: 1 }), + ...localizedPair('/overview', { + lastModified: now, + changeFrequency: 'daily', + priority: 0.9, + }), ...TABS.flatMap((tab) => localizedPair(`/${tab}`, { lastModified: now, diff --git a/packages/app/src/app/zh/overview/page.tsx b/packages/app/src/app/zh/overview/page.tsx new file mode 100644 index 00000000..dd5d6cc9 --- /dev/null +++ b/packages/app/src/app/zh/overview/page.tsx @@ -0,0 +1,47 @@ +import type { Metadata } from 'next'; + +import { SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; + +import { OverviewPageContent } from '@/components/overview/overview-page'; +import { ZH_OG_LOCALE, zhAlternates } from '@/lib/i18n'; +import { resolveOverviewTier } from '@/lib/overview-data'; +import { getOverviewPageData } from '@/lib/overview-data.server'; + +export const dynamic = 'force-dynamic'; + +const DESCRIPTION = + '在固定单轮 8K 输入 / 1K 输出负载下,总览各活跃模型在 MI355X、B200、B300、GB200 与 GB300 上的表现;每格为该平台最佳验证推测解码结果(50 tok/s/user 档位),相对 B200 的差值仅在同精度结果之间计算。'; + +export const metadata: Metadata = { + title: 'AI 推理总览', + description: DESCRIPTION, + alternates: zhAlternates('/overview'), + openGraph: { + title: `AI 推理总览 | ${SITE_NAME}`, + description: DESCRIPTION, + url: `${SITE_URL}/zh/overview`, + type: 'website', + locale: ZH_OG_LOCALE, + }, + twitter: { + card: 'summary_large_image', + title: `AI 推理总览 | ${SITE_NAME}`, + description: DESCRIPTION, + }, +}; + +interface Props { + searchParams: Promise>; +} + +export default async function ZhOverviewPage({ searchParams }: Props) { + const sp = await searchParams; + const data = await getOverviewPageData(resolveOverviewTier(sp.tier)); + return ( +
+
+ +
+
+ ); +} diff --git a/packages/app/src/components/header/header.tsx b/packages/app/src/components/header/header.tsx index b5fbbf71..e029e445 100644 --- a/packages/app/src/components/header/header.tsx +++ b/packages/app/src/components/header/header.tsx @@ -75,7 +75,13 @@ function isActive(pathname: string, href: string): boolean { } /** EN ↔ 中文 switcher; maps the current page to its sibling in the other language. */ -function LanguageToggle({ pathname }: { pathname: string }) { +function LanguageToggle({ + pathname, + router, +}: { + pathname: string; + router: ReturnType; +}) { const isZh = isZhPathname(pathname); const target = switchLocalePath(pathname); return ( @@ -83,8 +89,12 @@ function LanguageToggle({ pathname }: { pathname: string }) { href={target} data-testid="language-toggle" hrefLang={isZh ? 'en' : 'zh-CN'} - className="px-2 py-1.5 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors whitespace-nowrap" - onClick={() => track('header_language_toggled', { to: isZh ? 'en' : 'zh' })} + className="inline-flex items-center min-h-11 px-2 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors whitespace-nowrap" + onClick={(event) => { + track('header_language_toggled', { to: isZh ? 'en' : 'zh' }); + // The sibling keeps shareable query state (e.g. the overview's ?tier=). + navigateInApp(event, router, target + window.location.search); + }} > {isZh ? 'EN' : '中文'} @@ -145,7 +155,11 @@ export const Header = ({ starCount }: { starCount?: number | null }) => {
{/* Brand */} - + InferenceX by @@ -184,9 +198,18 @@ export const Header = ({ starCount }: { starCount?: number | null }) => { {/* Right side */}
- - - + + + + + {/* Minecraft-only audio toggles ride alongside GitHub stars below the + same `sm` cutoff: in minecraft mode the pixel font widens the bar, + and these two extra buttons are what push a 320px header past its + bounds. Below `sm` they move into the mobile menu instead (see the + MinecraftToggles instance in the hamburger dropdown below). */} + + + {/* Mobile hamburger */} @@ -195,7 +218,7 @@ export const Header = ({ starCount }: { starCount?: number | null }) => { type="button" data-testid="mobile-menu-toggle" onClick={toggleMenu} - className="flex items-center justify-center size-9 rounded-md transition-colors hover:bg-muted cursor-pointer" + className="flex items-center justify-center size-11 rounded-md transition-colors hover:bg-muted cursor-pointer" aria-expanded={mobileMenuOpen} aria-label="Navigation menu" > @@ -224,7 +247,7 @@ export const Header = ({ starCount }: { starCount?: number | null }) => { key={href} href={displayHref} className={cn( - 'px-3 py-2 rounded-md text-sm font-medium transition-colors', + 'flex items-center min-h-11 px-3 rounded-md text-sm font-medium transition-colors', isActive(pathname, href) ? 'text-brand bg-brand/10' : 'text-muted-foreground hover:text-foreground hover:bg-muted', @@ -237,6 +260,9 @@ export const Header = ({ starCount }: { starCount?: number | null }) => { {label} ))} + + +
)}
diff --git a/packages/app/src/components/minecraft/minecraft-toggles.tsx b/packages/app/src/components/minecraft/minecraft-toggles.tsx index ccea82b6..073a0bff 100644 --- a/packages/app/src/components/minecraft/minecraft-toggles.tsx +++ b/packages/app/src/components/minecraft/minecraft-toggles.tsx @@ -6,7 +6,7 @@ import { track } from '@/lib/analytics'; import { cn } from '@/lib/utils'; const toggleClasses = cn( - 'inline-flex items-center justify-center rounded-md p-2', + 'inline-flex items-center justify-center rounded-md size-11', 'text-muted-foreground hover:text-foreground hover:bg-accent', 'transition-colors duration-200', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', diff --git a/packages/app/src/components/overview/overview-detail-link.tsx b/packages/app/src/components/overview/overview-detail-link.tsx new file mode 100644 index 00000000..2e1e44cb --- /dev/null +++ b/packages/app/src/components/overview/overview-detail-link.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { ArrowRight } from 'lucide-react'; +import type { ReactNode } from 'react'; + +import { track } from '@/lib/analytics'; +import { cn } from '@/lib/utils'; + +/** + * Model-level dashboard link. A plain anchor, not a ``: the dashboard + * reads its filters from a snapshot `url-state.ts` takes at module evaluation, + * so a client-side navigation would land on an unfiltered dashboard while the + * label promises the model's own view. + */ +export function OverviewDetailLink({ + href, + model, + ariaLabel, + className, + children, +}: { + href: string; + model: string; + ariaLabel: string; + className?: string; + children: ReactNode; +}) { + return ( + track('overview_model_detail_clicked', { model })} + > + {children} + + ); +} diff --git a/packages/app/src/components/overview/overview-page.tsx b/packages/app/src/components/overview/overview-page.tsx new file mode 100644 index 00000000..5ac8c864 --- /dev/null +++ b/packages/app/src/components/overview/overview-page.tsx @@ -0,0 +1,61 @@ +import { Card } from '@/components/ui/card'; +import type { OverviewPageData } from '@/lib/overview-data'; + +import { + DesktopOverviewMatrix, + MobileOverviewList, + OverviewMethodology, + OverviewTierSwitcher, + overviewFormatters, + OVERVIEW_STRINGS, + type OverviewLocale, +} from './overview-scorecard'; + +interface OverviewPageProps { + data: OverviewPageData; + locale: OverviewLocale; +} + +export function OverviewPageContent({ data, locale }: OverviewPageProps) { + const strings = OVERVIEW_STRINGS[locale]; + const formatters = overviewFormatters(locale); + const snapshot = + data.datasetThroughDate === null + ? null + : strings.snapshot(formatters.shortDate(data.datasetThroughDate)); + + return ( +
+
+

{strings.title}

+

+ {strings.purpose} +

+

+ {strings.scope(data.tier)} +

+ {snapshot === null ? null : ( +

{snapshot}

+ )} + +
+ + {/* Official-only summary; uploaded runs remain in the linked dashboard. */} + + + + + +
+ ); +} diff --git a/packages/app/src/components/overview/overview-scorecard.tsx b/packages/app/src/components/overview/overview-scorecard.tsx new file mode 100644 index 00000000..a6adb504 --- /dev/null +++ b/packages/app/src/components/overview/overview-scorecard.tsx @@ -0,0 +1,533 @@ +/** + * @file overview-scorecard.tsx + * @description The comparison surface itself: a portfolio matrix of every + * active model (rows) across today's key serving platforms (columns — B200 as + * the reference, then MI355X, B300, GB200 NVL72, GB300 NVL72). The desktop + * table at `xl` and the mobile cards below `xl` render every cell through the + * SAME `BaselineCell`/`CandidateCell` components, so the two surfaces cannot + * drift semantically. + * + * Each cell shows that platform's own best validated speculative read at the + * displayed tier (50 tok/s/user by default; `?tier=` picks another) with its + * precision badge, evidence date and filtered-dashboard link. The signed + * delta against B200 appears only when both displayed reads share a precision + * and a release — mismatches say so instead of comparing FP4 with FP8. A side + * with no exact read shows `∞` with its reason, never a number or percent. + */ + +import { + OVERVIEW_HIGH_TIER, + OVERVIEW_TIERS, + type OverviewHeadlinePairComparison, + type OverviewHeadlinePairMember, + type OverviewModelSummary, + type OverviewTier, +} from '@/lib/overview-data'; +import { buildOverviewDashboardHref, detailHref, overviewTierHref } from '@/lib/overview-links'; + +import { OverviewDetailLink } from './overview-detail-link'; + +export type OverviewLocale = 'en' | 'zh'; + +export const OVERVIEW_STRINGS = { + en: { + title: 'AI Inference Overview', + purpose: 'Every active model across MI355X, B200, B300, GB200 and GB300 at a glance.', + scope: (tier: number) => + `8K→1K · Single-turn · Output tok/s/GPU @${tier} tok/s/user · Speculative decode only · Best validated stack per platform`, + tierNavLabel: 'Service level', + tierUnit: 'tok/s/user', + snapshot: (through: string) => `Database snapshot through ${through}`, + caption: + "Best validated serving results for every active model across today's key platforms, each compared with B200.", + modelHeader: 'Model', + detailsHeader: 'Details', + detailLink: 'View details', + detailAria: (modelLabel: string) => `View details: ${modelLabel}`, + dashboardAria: (modelLabel: string, stack: string) => + `Open filtered dashboard: ${modelLabel} · ${stack}`, + delta: (signedPct: string) => `${signedPct} vs B200`, + precisionMismatch: (candidatePrecision: string, baselinePrecision: string) => + `${candidatePrecision.toUpperCase()} vs ${baselinePrecision.toUpperCase()} · no comparable delta`, + versionMismatch: 'Different releases · no comparable delta', + highLeadChanged: (leaderLabel: string) => `At ${OVERVIEW_HIGH_TIER}, ${leaderLabel} leads`, + infinityLegend: '∞ = no comparable result', + missingReasons: (tier: number): Record => ({ + standard_decode_only: 'standard decode only', + int4_bf16_only: 'INT4/BF16 only', + no_8k1k_data: 'no 8K/1K data', + cannot_reach_at_tier: `cannot reach @${tier}`, + no_exact_at_tier: `no exact @${tier} result`, + }), + methodologyNote: + "Each cell shows the platform's best validated speculative-decode serving configuration for that model, labeled with its precision. Deltas against B200 compare only same-precision, same-release results — FP4 is never measured against FP8. Results compare complete serving stacks rather than isolated silicon.", + interpolationNote: + 'Tier values interpolate each configuration’s official Pareto frontier — no extrapolation.', + }, + zh: { + title: 'AI 推理总览', + purpose: '一眼对比各活跃模型在 MI355X、B200、B300、GB200 与 GB300 上的表现。', + scope: (tier: number) => + `8K→1K · 单轮 · 每 GPU 输出 tok/s @${tier} tok/s/用户 · 仅推测解码 · 各平台最佳验证配置`, + tierNavLabel: '服务档位', + tierUnit: 'tok/s/用户', + snapshot: (through: string) => `数据库快照截至 ${through}`, + caption: '各活跃模型在关键平台上的最佳验证服务结果,均与 B200 对比。', + modelHeader: '模型', + detailsHeader: '详情', + detailLink: '查看详情', + detailAria: (modelLabel: string) => `查看详情:${modelLabel}`, + dashboardAria: (modelLabel: string, stack: string) => + `打开筛选后的仪表板:${modelLabel} · ${stack}`, + delta: (signedPct: string) => `相对 B200 ${signedPct}`, + precisionMismatch: (candidatePrecision: string, baselinePrecision: string) => + `${candidatePrecision.toUpperCase()} 与 ${baselinePrecision.toUpperCase()} · 无可比差值`, + versionMismatch: '版本不同 · 无可比差值', + highLeadChanged: (leaderLabel: string) => `${OVERVIEW_HIGH_TIER} 档由 ${leaderLabel} 领先`, + infinityLegend: '∞ = 无可比结果', + missingReasons: (tier: number): Record => ({ + standard_decode_only: '仅标准解码', + int4_bf16_only: '仅 INT4/BF16', + no_8k1k_data: '无 8K/1K 数据', + cannot_reach_at_tier: `无法达到 @${tier}`, + no_exact_at_tier: `无精确 @${tier} 结果`, + }), + methodologyNote: + '每格展示该平台在该模型上表现最好的已验证推测解码服务配置,并标注精度。相对 B200 的差值只在同精度、同版本结果之间计算——绝不将 FP4 与 FP8 互比。对比对象是完整服务栈,而非单独的芯片。', + interpolationNote: '各档位数据基于各配置官方 Pareto 前沿插值;不进行外推。', + }, +} as const; + +export type OverviewStrings = (typeof OVERVIEW_STRINGS)[OverviewLocale]; + +interface Formatters { + number: Intl.NumberFormat; + shortDate: (date: string) => string; +} + +export function overviewFormatters(locale: OverviewLocale): Formatters { + const tag = locale === 'zh' ? 'zh-CN' : 'en-US'; + const shortDateFormat = new Intl.DateTimeFormat(tag, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }); + return { + number: new Intl.NumberFormat(tag, { maximumFractionDigits: 0 }), + shortDate: (date) => shortDateFormat.format(new Date(`${date}T00:00:00Z`)), + }; +} + +/** + * A result's evidence dates, localized: a single day (`Jul 6` / `7月6日`) when + * both backing frontier points share a date, else an en-dash range + * (`Jun 24–Jul 4`). The caller renders nothing when there is no backing date. + */ +function formatEvidenceDate( + formatters: Formatters, + evidenceDate: { from: string; to: string }, +): string { + const from = formatters.shortDate(evidenceDate.from); + return evidenceDate.from === evidenceDate.to + ? from + : `${from}–${formatters.shortDate(evidenceDate.to)}`; +} + +/** The reason copy for a missing side, keyed to the displayed tier. */ +function missingReasonCopy(member: OverviewHeadlinePairMember, strings: OverviewStrings): string { + const reason = member.missingReason; + return reason === null ? '' : strings.missingReasons(member.read.tier)[reason]; +} + +/** `candidate/B200 − 1` as a rounded percent with a U+2212 or `+` sign. */ +function signedPercent(formatters: Formatters, percent: number): string { + const rounded = Math.round(percent); + const sign = rounded < 0 ? '−' : '+'; + return `${sign}${formatters.number.format(Math.abs(rounded))}%`; +} + +/** + * The @100 line, rendered only when the @100 leader is a different hardware + * than the displayed tier's leader — the leader being the side with the + * greater exact @100 read. Null on the 100 view itself (data layer). + */ +function highLeadLine( + pair: OverviewHeadlinePairComparison, + strings: OverviewStrings, +): string | null { + if (pair.highLeaderTransition !== 'changed_hardware') return null; + const candidateHigh = pair.candidate.highRead.value; + const baselineHigh = pair.baseline.highRead.value; + if (candidateHigh === null || baselineHigh === null) return null; + const leaderLabel = + candidateHigh > baselineHigh ? pair.candidate.hardwareLabel : pair.baseline.hardwareLabel; + return strings.highLeadChanged(leaderLabel); +} + +const PAIR_VALUE_LINK_CLASS = + 'inline-flex min-h-11 items-center rounded-sm underline decoration-dotted underline-offset-4 hover:decoration-solid focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50'; + +/** A side with no exact read at the displayed tier: `∞` plus its reason, no link/date/percent. */ +function CellMissing({ hardware, reason }: { hardware: string; reason: string }) { + return ( + + + {reason} + + ); +} + +/** + * One platform's read: its exact displayed-tier value linked to the platform's + * own filtered dashboard, with a precision badge and the evidence date — or + * `∞` with the reason it has no exact read. + */ +function CellValue({ + locale, + model, + member, + formatters, + strings, +}: { + locale: OverviewLocale; + model: OverviewModelSummary; + member: OverviewHeadlinePairMember; + formatters: Formatters; + strings: OverviewStrings; +}) { + const { value, config, evidenceDate } = member.read; + if (member.missingReason !== null || value === null) { + return ; + } + // The full deployment stack behind this read; the framework and precision + // stay visible, the spec method rides the link title and aria only. + const stack = + config === null + ? null + : `${member.hardwareLabel} · ${config.frameworkLabel} · ${config.precision.toUpperCase()} · ${config.specLabel}`; + return ( +
+ + {config === null || stack === null ? ( + formatters.number.format(value) + ) : ( + + {formatters.number.format(value)} + + )} + + {member.precision === null ? null : ( + + {config === null + ? member.precision.toUpperCase() + : `${config.frameworkLabel} · ${config.precision.toUpperCase()}`} + + )} + {evidenceDate === null ? null : ( + + {formatEvidenceDate(formatters, evidenceDate)} + + )} +
+ ); +} + +/** The B200 reference cell: the read alone, no delta against itself. */ +function BaselineCell(props: { + locale: OverviewLocale; + model: OverviewModelSummary; + member: OverviewHeadlinePairMember; + formatters: Formatters; + strings: OverviewStrings; +}) { + return ( +
+ +
+ ); +} + +/** + * One candidate platform's cell: its own read, then the same-precision delta + * against B200, a mismatch note when both sides exist but cannot compare, and + * the @100 leader line only when the leader flips. + */ +function CandidateCell({ + locale, + model, + pair, + formatters, + strings, +}: { + locale: OverviewLocale; + model: OverviewModelSummary; + pair: OverviewHeadlinePairComparison; + formatters: Formatters; + strings: OverviewStrings; +}) { + const highLine = highLeadLine(pair, strings); + return ( +
+ + {pair.directDeltaPercent === null ? null : ( +

+ {strings.delta(signedPercent(formatters, pair.directDeltaPercent))} +

+ )} + {pair.deltaUnavailableReason === null ? null : ( +

+ {pair.deltaUnavailableReason === 'precision_mismatch' + ? strings.precisionMismatch( + pair.candidate.precision ?? '', + pair.baseline.precision ?? '', + ) + : strings.versionMismatch} +

+ )} + {highLine === null ? null :

{highLine}

} +
+ ); +} + +/** The model's display name, shared by both surfaces. */ +function ModelName({ model }: { model: OverviewModelSummary }) { + return

{model.modelLabel}

; +} + +interface SurfaceProps { + models: OverviewModelSummary[]; + locale: OverviewLocale; + formatters: Formatters; + strings: OverviewStrings; +} + +/** Model column, the B200 reference, one column per candidate platform, details. */ +export function DesktopOverviewMatrix({ models, locale, formatters, strings }: SurfaceProps) { + const headlinePairs = models[0]?.headlinePairs ?? []; + const baselineHeader = headlinePairs[0]?.baseline.hardwareLabel ?? null; + return ( +
+ + + + + {baselineHeader === null ? null : } + {headlinePairs.map((pair) => ( + + ))} + + + + + + {baselineHeader === null ? null : ( + + )} + {headlinePairs.map((pair) => ( + + ))} + + + + + {models.map((model) => ( + + + {model.headlinePairs[0] === undefined ? null : ( + + )} + {model.headlinePairs.map((pair) => ( + + ))} + + + ))} + +
{strings.caption}
+ {strings.modelHeader} + + {baselineHeader} + + {pair.candidate.hardwareLabel} + + {strings.detailsHeader} +
+ + + + + + + + {strings.detailLink} + +
+
+ ); +} + +/** Below `xl`: the same cells stacked as one card per model, always fully visible. */ +export function MobileOverviewList({ models, locale, formatters, strings }: SurfaceProps) { + return ( +
    + {models.map((model) => ( +
  • +
    + +
    + {model.headlinePairs[0] === undefined ? null : ( + <> + + {model.headlinePairs[0].baseline.hardwareLabel} + + + + )} + {model.headlinePairs.map((pair) => ( +
    + + {pair.candidate.hardwareLabel} + + +
    + ))} +
    + + {strings.detailLink} + +
    +
  • + ))} +
+ ); +} + +/** + * The service-level selector: one plain link per tier, so every view is a + * server-rendered, copyable URL — no client state. The displayed tier is + * inert text marked `aria-current`, never a self-link. + */ +export function OverviewTierSwitcher({ + tier, + locale, + strings, +}: { + tier: OverviewTier; + locale: OverviewLocale; + strings: OverviewStrings; +}) { + const optionClass = 'px-3 py-1.5 tabular-nums'; + return ( + + ); +} + +export function OverviewMethodology({ strings }: { strings: OverviewStrings }) { + return ( +
+

{strings.methodologyNote}

+

{strings.infinityLegend}

+

{strings.interpolationNote}

+
+ ); +} diff --git a/packages/app/src/components/ui/mode-toggle.tsx b/packages/app/src/components/ui/mode-toggle.tsx index f216624f..7dcdd354 100644 --- a/packages/app/src/components/ui/mode-toggle.tsx +++ b/packages/app/src/components/ui/mode-toggle.tsx @@ -27,7 +27,7 @@ export function ModeToggle() { }; const buttonClasses = cn( - 'inline-flex items-center justify-center rounded-md p-2', + 'inline-flex items-center justify-center rounded-md size-11', 'text-muted-foreground hover:text-foreground hover:bg-accent', 'transition-colors duration-200', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', diff --git a/packages/app/src/hooks/useTogglableSet.ts b/packages/app/src/hooks/useTogglableSet.ts index bf84694c..59bbac71 100644 --- a/packages/app/src/hooks/useTogglableSet.ts +++ b/packages/app/src/hooks/useTogglableSet.ts @@ -1,33 +1,8 @@ import { useCallback, useState } from 'react'; -/** - * Pure toggle state transition. - * - * - If all items are active and one is toggled, only that item remains active (solo). - * - If only one item is active and it's toggled, all items become active (restore all). - * - Otherwise, the item is simply added or removed from the active set. - */ -export function computeToggle(prev: Set, item: string, allItems: Set): Set { - const allAreActive = prev.size === allItems.size; - const isActive = prev.has(item); +import { computeToggle } from '@/lib/toggle-set'; - if (isActive) { - if (allAreActive) { - // Solo: only keep the clicked item - return new Set([item]); - } else if (prev.size === 1) { - // Restore all: re-enable everything - return allItems; - } - // Remove the clicked item - const next = new Set(prev); - next.delete(item); - return next; - } - // Add the clicked item - const next = new Set([...prev, item]); - return next; -} +export { computeToggle }; /** * Hook for managing a togglable set with "click to solo, click again to restore all" behavior. diff --git a/packages/app/src/lib/benchmark-data.server.ts b/packages/app/src/lib/benchmark-data.server.ts new file mode 100644 index 00000000..ccc85317 --- /dev/null +++ b/packages/app/src/lib/benchmark-data.server.ts @@ -0,0 +1,21 @@ +import { FIXTURES_MODE, getDb } from '@semianalysisai/inferencex-db/connection'; +import { + type BenchmarkRow, + getLatestBenchmarks, +} from '@semianalysisai/inferencex-db/queries/benchmarks'; + +import { cachedQuery } from '@/lib/api-cache'; +import { loadFixture } from '@/lib/test-fixtures'; + +/** Cache slot is keyed on the dbKeys array. Both `/compare/` and + * `/compare-per-dollar/` for the same model hit the same blob entry — + * the per-dollar route doesn't duplicate the fetch or the cache. */ +export const getCachedBenchmarks = cachedQuery( + (dbModelKeys: string[]) => { + if (FIXTURES_MODE) return Promise.resolve(loadFixture('benchmarks')); + + return getLatestBenchmarks(getDb(), dbModelKeys); + }, + 'benchmarks', + { blobOnly: true }, +); diff --git a/packages/app/src/lib/compare-ssr.ts b/packages/app/src/lib/compare-ssr.ts index 592a8238..5a3ec2c6 100644 --- a/packages/app/src/lib/compare-ssr.ts +++ b/packages/app/src/lib/compare-ssr.ts @@ -18,16 +18,11 @@ import { SITE_URL, sequenceToIslOsl, } from '@semianalysisai/inferencex-constants'; -import { FIXTURES_MODE, getDb } from '@semianalysisai/inferencex-db/connection'; - -import { - type BenchmarkRow, - getLatestBenchmarks, -} from '@semianalysisai/inferencex-db/queries/benchmarks'; +import type { BenchmarkRow } from '@semianalysisai/inferencex-db/queries/benchmarks'; import { interpolateForGPU } from '@/components/calculator/interpolation'; import type { GPUDataPoint, InterpolatedResult } from '@/components/calculator/types'; -import { cachedQuery } from '@/lib/api-cache'; +import { getCachedBenchmarks } from '@/lib/benchmark-data.server'; import { rowToAggDataEntry } from '@/lib/benchmark-transform'; import { getHardwareKey } from '@/lib/chart-utils'; import { @@ -39,24 +34,8 @@ import { compareModelSeoName, } from '@/lib/compare-slug'; import { getHardwareConfig, getGpuSpecs } from '@/lib/constants'; -import { loadFixture } from '@/lib/test-fixtures'; - -// --------------------------------------------------------------------------- -// Cached benchmark fetch -// --------------------------------------------------------------------------- -/** Cache slot is keyed on the dbKeys array. Both `/compare/` and - * `/compare-per-dollar/` for the same model hit the same blob entry — - * the per-dollar route doesn't duplicate the fetch or the cache. */ -export const getCachedBenchmarks = cachedQuery( - (dbModelKeys: string[]) => { - if (FIXTURES_MODE) return Promise.resolve(loadFixture('benchmarks')); - - return getLatestBenchmarks(getDb(), dbModelKeys); - }, - 'benchmarks', - { blobOnly: true }, -); +export { getCachedBenchmarks }; // --------------------------------------------------------------------------- // URL-param validators (shared by both routes' overrides) diff --git a/packages/app/src/lib/exclusion.ts b/packages/app/src/lib/exclusion.ts index 21f4d1bd..941202f1 100644 --- a/packages/app/src/lib/exclusion.ts +++ b/packages/app/src/lib/exclusion.ts @@ -1,6 +1,6 @@ import { SPEC_METHOD_KEYS } from '@semianalysisai/inferencex-constants'; -import { computeToggle } from '@/hooks/useTogglableSet'; +import { computeToggle } from '@/lib/toggle-set'; /** * Data-driven config exclusion. diff --git a/packages/app/src/lib/i18n.test.ts b/packages/app/src/lib/i18n.test.ts index 492d2742..36a9fde3 100644 --- a/packages/app/src/lib/i18n.test.ts +++ b/packages/app/src/lib/i18n.test.ts @@ -40,6 +40,7 @@ describe('hasZhSibling', () => { it('matches mirrored exact routes', () => { expect(hasZhSibling('/')).toBe(true); expect(hasZhSibling('/inference')).toBe(true); + expect(hasZhSibling('/overview')).toBe(true); expect(hasZhSibling('/about')).toBe(true); }); @@ -72,12 +73,14 @@ describe('switchLocalePath', () => { it('switches English pages to their zh sibling', () => { expect(switchLocalePath('/')).toBe('/zh'); expect(switchLocalePath('/inference')).toBe('/zh/inference'); + expect(switchLocalePath('/overview')).toBe('/zh/overview'); expect(switchLocalePath('/blog/some-post')).toBe('/zh/blog/some-post'); }); it('switches zh pages back to English', () => { expect(switchLocalePath('/zh')).toBe('/'); expect(switchLocalePath('/zh/quotes')).toBe('/quotes'); + expect(switchLocalePath('/zh/overview')).toBe('/overview'); expect(switchLocalePath('/zh/blog/some-post')).toBe('/blog/some-post'); }); diff --git a/packages/app/src/lib/i18n.ts b/packages/app/src/lib/i18n.ts index b094b456..7577353b 100644 --- a/packages/app/src/lib/i18n.ts +++ b/packages/app/src/lib/i18n.ts @@ -36,6 +36,7 @@ export function isZhPathname(pathname: string): boolean { */ export const ZH_MIRRORED_ROUTES: readonly { path: string; exact?: boolean }[] = [ { path: '/', exact: true }, + { path: '/overview', exact: true }, { path: '/inference', exact: true }, { path: '/inference/agentic' }, { path: '/evaluation', exact: true }, diff --git a/packages/app/src/lib/overview-config-identity.test.ts b/packages/app/src/lib/overview-config-identity.test.ts new file mode 100644 index 00000000..02fab087 --- /dev/null +++ b/packages/app/src/lib/overview-config-identity.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest'; + +import type { BenchmarkRow } from './api'; +import { overviewConfigIdentityKey } from './overview-config-identity'; + +let nextId = 1; + +function row(overrides: Partial = {}): BenchmarkRow { + return { + id: nextId++, + hardware: 'b200', + framework: 'sglang', + model: 'qwen3.5', + precision: 'fp8', + spec_method: 'mtp', + disagg: false, + is_multinode: false, + prefill_tp: 8, + prefill_ep: 1, + prefill_dp_attention: false, + prefill_num_workers: 1, + decode_tp: 8, + decode_ep: 1, + decode_dp_attention: false, + decode_num_workers: 1, + num_prefill_gpu: 8, + // Not 8: the num_decode_gpu case below flips this to 8 and must change the key. + num_decode_gpu: 4, + benchmark_type: 'single_turn', + isl: 8192, + osl: 1024, + conc: 16, + offload_mode: 'off', + image: null, + metrics: { median_intvty: 50, output_tput_per_gpu: 1000 }, + date: '2026-07-20', + run_url: null, + ...overrides, + }; +} + +/** The pre-v2.1 coarse key: hardware/framework/spec/disagg/offload, no GPU counts. */ +const oldCoarseKey = (r: BenchmarkRow): string => + `${r.hardware}|${r.framework}|${r.spec_method}|${r.disagg}|${r.offload_mode}`; + +describe('overviewConfigIdentityKey', () => { + it.each([ + ['model', 'kimik2.7-code'], + ['hardware', 'gb300'], + ['framework', 'dynamo-trt'], + ['precision', 'fp4'], + ['spec_method', 'eagle'], + ['disagg', true], + ['is_multinode', true], + ['prefill_tp', 4], + ['prefill_ep', 4], + ['prefill_dp_attention', true], + ['prefill_num_workers', 7], + ['decode_tp', 16], + ['decode_ep', 16], + ['decode_dp_attention', true], + ['decode_num_workers', 2], + ['num_prefill_gpu', 28], + ['num_decode_gpu', 8], + ['offload_mode', 'cpu'], + ] satisfies [keyof BenchmarkRow, BenchmarkRow[keyof BenchmarkRow]][])( + 'changes when %s changes', + (field, value) => { + expect(overviewConfigIdentityKey(row({ [field]: value } as Partial))).not.toBe( + overviewConfigIdentityKey(row()), + ); + }, + ); + + it('keeps concurrency and evidence outside configuration identity', () => { + const base = row(); + expect( + overviewConfigIdentityKey({ + ...base, + conc: 64, + date: '2026-07-21', + image: 'new-image', + run_url: 'https://github.com/SemiAnalysisAI/InferenceX/actions/runs/2', + }), + ).toBe(overviewConfigIdentityKey(base)); + }); + + it('separates disagg topologies the old coarse key merged (28P+8D vs 40P+16D)', () => { + // The old coarse key omitted GPU counts, so two different P/D splits on the + // same hardware/framework/spec/disagg/offload collided into one result. + const split28p8d = row({ disagg: true, num_prefill_gpu: 28, num_decode_gpu: 8 }); + const split40p16d = row({ disagg: true, num_prefill_gpu: 40, num_decode_gpu: 16 }); + + expect(oldCoarseKey(split28p8d)).toBe(oldCoarseKey(split40p16d)); + expect(overviewConfigIdentityKey(split28p8d)).not.toBe(overviewConfigIdentityKey(split40p16d)); + }); +}); diff --git a/packages/app/src/lib/overview-config-identity.ts b/packages/app/src/lib/overview-config-identity.ts new file mode 100644 index 00000000..18e4e9f4 --- /dev/null +++ b/packages/app/src/lib/overview-config-identity.ts @@ -0,0 +1,33 @@ +import type { BenchmarkRow } from './api'; + +/** + * Exact per-deployment identity for the overview grouping. Every dimension that + * makes a serving configuration a distinct deployable topology is part of the + * key — model, hardware, framework, precision, spec method, disagg, multinode, + * per-role parallelism and worker counts, GPU counts, and offload mode. Rows + * that differ only in concurrency, date, image, or run URL share one identity. + * + * Serialized as a JSON tuple so no field delimiter can collide with a value. + */ +export function overviewConfigIdentityKey(row: BenchmarkRow): string { + return JSON.stringify([ + row.model, + row.hardware, + row.framework, + row.precision, + row.spec_method, + row.disagg, + row.is_multinode, + row.prefill_tp, + row.prefill_ep, + row.prefill_dp_attention, + row.prefill_num_workers, + row.decode_tp, + row.decode_ep, + row.decode_dp_attention, + row.decode_num_workers, + row.num_prefill_gpu, + row.num_decode_gpu, + row.offload_mode ?? 'off', + ]); +} diff --git a/packages/app/src/lib/overview-data.server.ts b/packages/app/src/lib/overview-data.server.ts new file mode 100644 index 00000000..0bfa5fe6 --- /dev/null +++ b/packages/app/src/lib/overview-data.server.ts @@ -0,0 +1,39 @@ +import { DISPLAY_MODEL_TO_DB } from '@semianalysisai/inferencex-constants'; +import { FIXTURES_MODE } from '@semianalysisai/inferencex-db/connection'; + +import type { BenchmarkRow } from '@/lib/api'; +import { getCachedBenchmarks } from '@/lib/benchmark-data.server'; +import { DEFAULT_MODELS } from '@/lib/data-mappings'; +import { + assembleOverviewPageData, + OVERVIEW_PRIMARY_TIER, + type OverviewPageData, + type OverviewTier, +} from '@/lib/overview-data'; +import { loadFixture } from '@/lib/test-fixtures'; + +export async function getOverviewPageData( + tier: OverviewTier = OVERVIEW_PRIMARY_TIER, +): Promise { + // E2E fixtures mode serves a small synthetic rows-by-display-model fixture + // through the same assembler the live path uses, so a contract drift breaks + // the fixture tests instead of silently stranding the page's data. + if (FIXTURES_MODE) { + return assembleOverviewPageData( + loadFixture>('overview-rows'), + tier, + ); + } + + // Fetch rows per db model, concatenated per display model (one display model + // can span several db buckets), then hand the same shape to the assembler. + const entries = await Promise.all( + [...DEFAULT_MODELS].map(async (model) => { + const keys = DISPLAY_MODEL_TO_DB[model] ?? []; + const rows = keys.length > 0 ? await getCachedBenchmarks(keys) : []; + return [model, rows] as const; + }), + ); + + return assembleOverviewPageData(Object.fromEntries(entries), tier); +} diff --git a/packages/app/src/lib/overview-data.test.ts b/packages/app/src/lib/overview-data.test.ts new file mode 100644 index 00000000..61ba6c7f --- /dev/null +++ b/packages/app/src/lib/overview-data.test.ts @@ -0,0 +1,552 @@ +import { describe, expect, it } from 'vitest'; + +import overviewRowsFixture from '../../cypress/fixtures/api/overview-rows.json'; + +import type { BenchmarkRow } from './api'; +import { DEFAULT_MODELS, Model, Precision } from './data-mappings'; +import { + assembleOverviewPageData, + buildOverviewModelSummary, + resolveOverviewTier, + type OverviewModelSummary, +} from './overview-data'; + +let nextId = 1; + +function row(overrides: Partial = {}): BenchmarkRow { + return { + id: nextId++, + hardware: 'b200', + framework: 'sglang', + model: 'qwen3.5', + precision: 'fp8', + spec_method: 'mtp', + disagg: false, + is_multinode: false, + prefill_tp: 8, + prefill_ep: 1, + prefill_dp_attention: false, + prefill_num_workers: 1, + decode_tp: 8, + decode_ep: 1, + decode_dp_attention: false, + decode_num_workers: 1, + num_prefill_gpu: 8, + num_decode_gpu: 8, + benchmark_type: 'single_turn', + isl: 8192, + osl: 1024, + conc: 16, + offload_mode: 'off', + image: null, + metrics: { median_intvty: 50, output_tput_per_gpu: 1000 }, + date: '2026-07-20', + run_url: null, + ...overrides, + }; +} + +/** One frontier point per tier for a single configuration. */ +function frontier( + throughputs: [number, number, number, number], + overrides: Partial = {}, +): BenchmarkRow[] { + return [30, 50, 75, 100].map((tier, index) => + row({ + conc: index + 1, + metrics: { median_intvty: tier, output_tput_per_gpu: throughputs[index] }, + ...overrides, + }), + ); +} + +/** Frontier at explicit [interactivity, throughput] knots — for clamped/unreachable tiers. */ +function frontierAt( + points: [number, number][], + overrides: Partial = {}, +): BenchmarkRow[] { + return points.map(([intvty, tput], index) => + row({ + conc: index + 1, + metrics: { median_intvty: intvty, output_tput_per_gpu: tput }, + ...overrides, + }), + ); +} + +function headlinePairOf(summary: OverviewModelSummary, id: string) { + return summary.headlinePairs.find((pair) => pair.id === id); +} + +describe('overview headline pairs', () => { + it('keeps each side on its own best precision and withholds the delta on a mismatch', () => { + const summary = buildOverviewModelSummary(Model.Qwen3_5, [ + ...frontier([1200, 1000, 800, 600], { + hardware: 'mi355x', + precision: Precision.FP4, + }), + ...frontier([1100, 900, 700, 500], { + hardware: 'mi355x', + precision: Precision.FP8, + }), + ...frontier([1000, 800, 600, 400], { + hardware: 'b200', + precision: Precision.FP8, + }), + ]); + + const pair = headlinePairOf(summary, 'mi355x-vs-b200'); + expect(pair?.candidate.read.value).toBe(1000); + expect(pair?.candidate.precision).toBe(Precision.FP4); + expect(pair?.baseline.read.value).toBe(800); + expect(pair?.baseline.precision).toBe(Precision.FP8); + expect(pair?.precision).toBeNull(); + expect(pair?.directDeltaPercent).toBeNull(); + expect(pair?.deltaUnavailableReason).toBe('precision_mismatch'); + expect(pair?.highLeaderTransition).toBeNull(); + }); + + it('breaks equal exact-read coverage toward FP4', () => { + const summary = buildOverviewModelSummary(Model.Qwen3_5, [ + ...frontier([1200, 1000, 800, 600], { + hardware: 'mi355x', + precision: Precision.FP4, + }), + ...frontier([1100, 900, 700, 500], { + hardware: 'b200', + precision: Precision.FP4, + }), + ...frontier([1000, 800, 600, 400], { + hardware: 'mi355x', + precision: Precision.FP8, + }), + ...frontier([900, 700, 500, 300], { + hardware: 'b200', + precision: Precision.FP8, + }), + ]); + + expect(headlinePairOf(summary, 'mi355x-vs-b200')?.precision).toBe(Precision.FP4); + }); + + it('keeps an FP4 bucket and member boundaries when neither side has an exact read', () => { + const summary = buildOverviewModelSummary(Model.Qwen3_5, [ + ...frontierAt( + [ + [60, 900], + [70, 800], + [80, 700], + [90, 600], + ], + { hardware: 'mi355x', precision: Precision.FP4 }, + ), + ...frontierAt( + [ + [20, 500], + [30, 450], + [40, 400], + [45, 350], + ], + { hardware: 'b200', precision: Precision.FP4 }, + ), + ]); + + const pair = headlinePairOf(summary, 'mi355x-vs-b200'); + expect(pair?.precision).toBeNull(); + expect(pair?.candidate.precision).toBe(Precision.FP4); + expect(pair?.candidate.dbModel).toBe('qwen3.5'); + expect(pair?.baseline.precision).toBe(Precision.FP4); + expect(pair?.candidate.read).toMatchObject({ + value: null, + boundary: 'clamped_low', + config: { hardware: 'mi355x' }, + }); + expect(pair?.baseline.read).toMatchObject({ + value: null, + boundary: 'unreachable', + config: { hardware: 'b200' }, + }); + expect(pair?.candidate.missingReason).toBe('no_exact_at_tier'); + expect(pair?.baseline.missingReason).toBe('cannot_reach_at_tier'); + }); + + it('keeps an exact side and marks an unreachable side missing without a delta', () => { + const summary = buildOverviewModelSummary(Model.Qwen3_5, [ + ...frontier([1200, 1000, 800, 600], { + hardware: 'mi355x', + precision: Precision.FP4, + }), + ...frontierAt( + [ + [20, 500], + [30, 450], + [40, 400], + [45, 350], + ], + { hardware: 'b200', precision: Precision.FP4 }, + ), + ]); + + const pair = headlinePairOf(summary, 'mi355x-vs-b200'); + expect(pair?.candidate.read).toMatchObject({ value: 1000, boundary: 'interpolated' }); + expect(pair?.baseline.read).toMatchObject({ value: null, boundary: 'unreachable' }); + expect(pair?.baseline.missingReason).toBe('cannot_reach_at_tier'); + expect(pair?.directDeltaPercent).toBeNull(); + }); + + it('selects member precisions independently and shares a pair precision only when they match', () => { + const summary = buildOverviewModelSummary(Model.Qwen3_5, [ + ...frontier([1200, 1000, 800, 600], { + hardware: 'mi355x', + precision: Precision.FP8, + }), + ...frontier([1100, 900, 700, 500], { + hardware: 'b200', + precision: Precision.FP8, + }), + ...frontier([1300, 1100, 900, 700], { + hardware: 'gb300', + precision: Precision.FP4, + }), + ...frontier([1000, 800, 600, 400], { + hardware: 'b200', + precision: Precision.FP4, + }), + ]); + + const matched = headlinePairOf(summary, 'mi355x-vs-b200'); + expect(matched?.precision).toBe(Precision.FP8); + expect(matched?.directDeltaPercent).not.toBeNull(); + + const mismatched = headlinePairOf(summary, 'gb300-vs-b200'); + expect(mismatched?.candidate.precision).toBe(Precision.FP4); + expect(mismatched?.baseline.precision).toBe(Precision.FP8); + expect(mismatched?.precision).toBeNull(); + expect(mismatched?.deltaUnavailableReason).toBe('precision_mismatch'); + }); + + it('shows each release’s own read but never deltas across releases', () => { + const summary = buildOverviewModelSummary(Model.Kimi_K2_5, [ + ...frontier([1200, 1000, 800, 600], { + model: 'kimik2.5', + hardware: 'b200', + precision: Precision.FP4, + date: '2026-07-10', + }), + ...frontier([1100, 900, 700, 500], { + model: 'kimik2.7-code', + hardware: 'mi355x', + precision: Precision.FP4, + date: '2026-07-20', + }), + ]); + + const pair = headlinePairOf(summary, 'mi355x-vs-b200'); + expect(pair?.candidate.read.config?.dbModel).toBe('kimik2.7-code'); + expect(pair?.baseline.read.config?.dbModel).toBe('kimik2.5'); + expect(pair?.dbModel).toBeNull(); + expect(pair?.directDeltaPercent).toBeNull(); + expect(pair?.deltaUnavailableReason).toBe('version_mismatch'); + expect(pair?.highLeaderTransition).toBeNull(); + }); + + it('computes signed candidate-relative-to-B200 deltas', () => { + const summary = buildOverviewModelSummary(Model.Qwen3_5, [ + ...frontier([1000, 800, 600, 400], { + hardware: 'mi355x', + precision: Precision.FP4, + }), + ...frontier([1200, 1000, 800, 600], { + hardware: 'b200', + precision: Precision.FP4, + }), + ...frontier([1400, 1200, 1000, 800], { + hardware: 'gb300', + precision: Precision.FP4, + }), + ]); + + expect(headlinePairOf(summary, 'mi355x-vs-b200')?.directDeltaPercent).toBeCloseTo(-20); + expect(headlinePairOf(summary, 'gb300-vs-b200')?.directDeltaPercent).toBeCloseTo(20); + }); + + it('reports a hardware leader flip at @100 using independently best configs', () => { + const summary = buildOverviewModelSummary(Model.Qwen3_5, [ + ...frontier([1400, 1200, 800, 400], { + hardware: 'mi355x', + precision: Precision.FP4, + framework: 'sglang', + }), + ...frontier([1000, 700, 650, 600], { + hardware: 'mi355x', + precision: Precision.FP4, + framework: 'dynamo-trt', + }), + ...frontier([1200, 1000, 800, 650], { + hardware: 'b200', + precision: Precision.FP4, + }), + ]); + + const pair = headlinePairOf(summary, 'mi355x-vs-b200'); + expect(pair?.candidate.read.config?.framework).toBe('sglang'); + expect(pair?.candidate.highRead.config?.framework).toBe('dynamo-trt'); + expect(pair?.highLeaderTransition).toBe('changed_hardware'); + }); + + it('claims cannot-reach only when every speculative bucket is unreachable', () => { + const unreachable: [number, number][] = [ + [20, 500], + [30, 450], + [40, 400], + [45, 350], + ]; + const underSwept: [number, number][] = [ + [60, 900], + [70, 800], + [80, 700], + [90, 600], + ]; + const baseline = frontier([1200, 1000, 800, 600], { + hardware: 'b200', + precision: Precision.FP4, + }); + + const mixed = buildOverviewModelSummary(Model.Qwen3_5, [ + ...baseline, + ...frontierAt(unreachable, { hardware: 'mi355x', precision: Precision.FP4 }), + ...frontierAt(underSwept, { hardware: 'mi355x', precision: Precision.FP8 }), + ]); + expect(headlinePairOf(mixed, 'mi355x-vs-b200')?.candidate.missingReason).toBe( + 'no_exact_at_tier', + ); + + const allUnreachable = buildOverviewModelSummary(Model.Qwen3_5, [ + ...baseline, + ...frontierAt(unreachable, { hardware: 'mi355x', precision: Precision.FP4 }), + ...frontierAt(unreachable, { hardware: 'mi355x', precision: Precision.FP8 }), + ]); + expect(headlinePairOf(allUnreachable, 'mi355x-vs-b200')?.candidate.missingReason).toBe( + 'cannot_reach_at_tier', + ); + }); + + it('distinguishes standard-decode-only and unsupported-precision coverage per member', () => { + 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' }), + row({ hardware: 'b300', precision: Precision.INT4 }), + ]); + + expect(headlinePairOf(summary, 'mi355x-vs-b200')?.candidate.missingReason).toBe( + 'standard_decode_only', + ); + expect(headlinePairOf(summary, 'b300-vs-b200')?.candidate.missingReason).toBe('int4_bf16_only'); + }); + + it('always returns all four fixed pairs for an empty model', () => { + const summary = buildOverviewModelSummary(Model.Qwen3_5, []); + + expect(summary.headlinePairs.map(({ id }) => id)).toEqual([ + 'mi355x-vs-b200', + 'b300-vs-b200', + 'gb200-vs-b200', + 'gb300-vs-b200', + ]); + expect(summary.headlinePairs.every(({ precision }) => precision === null)).toBe(true); + expect( + summary.headlinePairs.every( + ({ candidate, baseline }) => + candidate.missingReason === 'no_8k1k_data' && baseline.missingReason === 'no_8k1k_data', + ), + ).toBe(true); + }); +}); + +describe('tier-parameterized overview', () => { + it('resolves the tier query value and falls back to 50', () => { + expect(resolveOverviewTier('100')).toBe(100); + expect(resolveOverviewTier(['75', '30'])).toBe(75); + expect(resolveOverviewTier('40')).toBe(50); + expect(resolveOverviewTier('')).toBe(50); + expect(resolveOverviewTier(undefined)).toBe(50); + }); + + it('stamps the displayed tier on the page and defaults to 50, down to empty models', () => { + expect(assembleOverviewPageData({}).tier).toBe(50); + const page = assembleOverviewPageData({}, 75); + expect(page.tier).toBe(75); + expect(page.models[0]?.headlinePairs[0]?.candidate.read.tier).toBe(75); + }); + + it('reads every pair member and its delta at the requested tier', () => { + const page = assembleOverviewPageData( + { + [Model.Qwen3_5]: [ + ...frontier([1000, 800, 600, 400], { hardware: 'mi355x', precision: Precision.FP4 }), + ...frontier([1200, 1000, 800, 600], { hardware: 'b200', precision: Precision.FP4 }), + ], + }, + 100, + ); + + const pair = headlinePairOf( + page.models.find((m) => m.model === Model.Qwen3_5)!, + 'mi355x-vs-b200', + ); + expect(pair?.candidate.read).toMatchObject({ tier: 100, value: 400 }); + expect(pair?.baseline.read).toMatchObject({ tier: 100, value: 600 }); + expect(pair?.directDeltaPercent).toBeCloseTo((400 / 600 - 1) * 100); + }); + + it('turns an unreachable @50 side into an exact read on the 30 view', () => { + const rows = [ + ...frontier([1200, 1000, 800, 600], { hardware: 'mi355x', precision: Precision.FP4 }), + ...frontierAt( + [ + [20, 500], + [30, 450], + [40, 400], + [45, 350], + ], + { hardware: 'b200', precision: Precision.FP4 }, + ), + ]; + + const at50 = headlinePairOf( + assembleOverviewPageData({ [Model.Qwen3_5]: rows }).models.find( + (m) => m.model === Model.Qwen3_5, + )!, + 'mi355x-vs-b200', + ); + expect(at50?.baseline.missingReason).toBe('cannot_reach_at_tier'); + + const at30 = headlinePairOf( + assembleOverviewPageData({ [Model.Qwen3_5]: rows }, 30).models.find( + (m) => m.model === Model.Qwen3_5, + )!, + 'mi355x-vs-b200', + ); + expect(at30?.baseline.read).toMatchObject({ tier: 30, value: 450 }); + expect(at30?.baseline.missingReason).toBeNull(); + expect(at30?.directDeltaPercent).toBeCloseTo((1200 / 450 - 1) * 100); + }); + + it('re-selects each platform’s best bucket at the displayed tier', () => { + // FP4 wins @50 (1000 > 900) but tops out low; FP8 wins @100 (700 > 400). + const page = (tier?: 30 | 50 | 75 | 100) => + assembleOverviewPageData( + { + [Model.Qwen3_5]: [ + ...frontier([1200, 1000, 800, 400], { hardware: 'mi355x', precision: Precision.FP4 }), + ...frontier([1100, 900, 850, 700], { hardware: 'mi355x', precision: Precision.FP8 }), + ...frontier([1200, 1000, 800, 600], { hardware: 'b200', precision: Precision.FP8 }), + ], + }, + tier, + ).models.find((m) => m.model === Model.Qwen3_5)!; + + const at50 = headlinePairOf(page(), 'mi355x-vs-b200'); + expect(at50?.candidate.precision).toBe(Precision.FP4); + expect(at50?.deltaUnavailableReason).toBe('precision_mismatch'); + + const at100 = headlinePairOf(page(100), 'mi355x-vs-b200'); + expect(at100?.candidate.precision).toBe(Precision.FP8); + expect(at100?.candidate.read.value).toBe(700); + expect(at100?.directDeltaPercent).toBeCloseTo((700 / 600 - 1) * 100); + }); + + it('never reports a leader transition on the 100 view', () => { + const rows = [ + ...frontier([1400, 1200, 800, 400], { + hardware: 'mi355x', + precision: Precision.FP4, + framework: 'sglang', + }), + ...frontier([1000, 700, 650, 600], { + hardware: 'mi355x', + precision: Precision.FP4, + framework: 'dynamo-trt', + }), + ...frontier([1200, 1000, 800, 650], { hardware: 'b200', precision: Precision.FP4 }), + ]; + + const summaryAt = (tier?: 30 | 50 | 75 | 100) => + assembleOverviewPageData({ [Model.Qwen3_5]: rows }, tier).models.find( + (m) => m.model === Model.Qwen3_5, + )!; + expect(headlinePairOf(summaryAt(), 'mi355x-vs-b200')?.highLeaderTransition).toBe( + 'changed_hardware', + ); + expect(headlinePairOf(summaryAt(100), 'mi355x-vs-b200')?.highLeaderTransition).toBeNull(); + }); +}); + +// Drift guard for the synthetic overview e2e fixture: if the assembler contract +// changes, the hand-built rows in overview-rows.json must be regenerated, and +// this fails loudly instead of the e2e stranding empty data. Every expectation +// is derived by running this same assembler over the fixture — never eyeballed — +// so it locks the exact matrix cell states overview.cy.ts renders against. +describe('assembleOverviewPageData over the overview-rows fixture', () => { + it('serves every matrix cell state through the real builder', () => { + const page = assembleOverviewPageData( + overviewRowsFixture as unknown as Record, + ); + + expect(page.models).toHaveLength(DEFAULT_MODELS.size); + expect(page.datasetThroughDate).toBe('2026-07-18'); + expect(page.tier).toBe(50); + + // DeepSeek: FP4 exacts on B200/B300 carry the only same-precision delta with + // a cross-day evidence range; GB200's own best is FP8, so its value shows + // but the delta is withheld; MI355X and GB300 miss in opposite clamp + // directions. + const deepseek = page.models.find((m) => m.model === Model.DeepSeek_V4_Pro)!; + const dsB300 = headlinePairOf(deepseek, 'b300-vs-b200')!; + expect(dsB300.precision).toBe(Precision.FP4); + expect(dsB300.baseline.read.value).toBeCloseTo(900.219); + expect(dsB300.candidate.read.value).toBeCloseTo(1121.875); + expect(dsB300.candidate.read.evidenceDate).toEqual({ from: '2026-06-24', to: '2026-07-04' }); + expect(dsB300.directDeltaPercent).toBeCloseTo(24.62, 1); + const dsGb200 = headlinePairOf(deepseek, 'gb200-vs-b200')!; + expect(dsGb200.candidate.precision).toBe(Precision.FP8); + expect(dsGb200.candidate.read.value).toBe(600); + expect(dsGb200.deltaUnavailableReason).toBe('precision_mismatch'); + expect(headlinePairOf(deepseek, 'mi355x-vs-b200')?.candidate.missingReason).toBe( + 'no_exact_at_tier', + ); + expect(headlinePairOf(deepseek, 'gb300-vs-b200')?.candidate.missingReason).toBe( + 'cannot_reach_at_tier', + ); + + // MiniMax: only GB300 has workload rows, so its value stands alone — a + // missing baseline never yields a delta OR a mismatch note. + 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.candidate.read.value).toBe(700); + expect(mmGb300.directDeltaPercent).toBeNull(); + expect(mmGb300.deltaUnavailableReason).toBeNull(); + + // Qwen: FP8 exacts on both sides yield the −16% delta and a leader flip at + // 100, while B300's FP4 best withholds its delta against the FP8 baseline. + const qwen = page.models.find((m) => m.model === Model.Qwen3_5)!; + const qwenMi = headlinePairOf(qwen, 'mi355x-vs-b200')!; + expect(qwenMi.precision).toBe(Precision.FP8); + expect(qwenMi.directDeltaPercent).toBeCloseTo(-15.56, 1); + expect(qwenMi.highLeaderTransition).toBe('changed_hardware'); + const qwenB300 = headlinePairOf(qwen, 'b300-vs-b200')!; + expect(qwenB300.candidate.precision).toBe(Precision.FP4); + expect(qwenB300.candidate.read.value).toBeCloseTo(1150.625); + expect(qwenB300.deltaUnavailableReason).toBe('precision_mismatch'); + + // Kimi: no workload rows at all → every member of every pair says so. + const kimi = page.models.find((m) => m.model === Model.Kimi_K2_5)!; + expect( + kimi.headlinePairs.every( + ({ candidate, baseline }) => + candidate.missingReason === 'no_8k1k_data' && baseline.missingReason === 'no_8k1k_data', + ), + ).toBe(true); + }); +}); diff --git a/packages/app/src/lib/overview-data.ts b/packages/app/src/lib/overview-data.ts new file mode 100644 index 00000000..82398216 --- /dev/null +++ b/packages/app/src/lib/overview-data.ts @@ -0,0 +1,529 @@ +import { resolveFrameworkPartLabel } from '@semianalysisai/inferencex-constants'; + +import type { BenchmarkRow } from './api'; +import { buildAvailabilityHwKey } from './chart-utils'; +import { getHardwareConfig, getModelSortIndex } from './constants'; +import { DEFAULT_MODELS, getModelLabel, Precision, type Model } from './data-mappings'; +import { overviewConfigIdentityKey } from './overview-config-identity'; +import { computeTcoFeed, type TcoTierBoundary } 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]; +/** Default service point; ?tier= re-renders the matrix at any OVERVIEW_TIERS value. */ +export const OVERVIEW_PRIMARY_TIER = 50; +/** High-interactivity capability read; also drives the high-tier leader transition. */ +export const OVERVIEW_HIGH_TIER = 100; + +/** The `?tier=` query value as a displayable tier; anything else falls back to 50. */ +export function resolveOverviewTier(raw: string | string[] | undefined): OverviewTier { + const candidate = Number(Array.isArray(raw) ? raw[0] : raw); + return OVERVIEW_TIERS.find((tier) => tier === candidate) ?? OVERVIEW_PRIMARY_TIER; +} + +export interface OverviewTierValue { + tier: number; + value: number | null; + boundary: TcoTierBoundary; + /** + * Run dates of the frontier point(s) backing this value — the two bracketing + * points when interpolated (from = earlier, to = later; equal on the same + * day), the single point twice when clamped, null when `value` is null. Comes + * only from this config's own frontier at this tier, never a sibling or cohort. + */ + evidenceDate: { from: string; to: string } | null; +} + +/** + * One real deployable serving configuration, identified by its exact + * deployment topology (model × hardware × framework × precision × spec_method × + * disagg × multinode × per-role parallelism × GPU counts × offload). Tier + * values come from this configuration's own Pareto frontier only — never + * blended across configurations. + */ +export interface OverviewConfigResult { + key: string; + dbModel: string; + hardware: string; + hwKey: string; + framework: string; + frameworkLabel: string; + specMethod: string; + specLabel: string; + disagg: boolean; + precision: string; + sourceRunUrls: string[]; + tierValues: OverviewTierValue[]; + latestDate: string; +} + +/** One hardware's frontier read at a single tier, with its backing config. */ +export interface OverviewTierRead { + tier: number; + value: number | null; + boundary: TcoTierBoundary | null; + /** Copied from the backing config's tier value; null when there is no read. */ + evidenceDate: { from: string; to: string } | null; + config: OverviewConfigResult | null; +} + +/** + * Why a pair member shows `∞` instead of a value, from most to least + * fundamental gap: + * + * - `no_8k1k_data` — no 8K/1K single-turn row for this hardware at all; + * - `int4_bf16_only` — measured only outside FP4/FP8; + * - `standard_decode_only` — FP4/FP8 measured, but standard decode only; + * - `cannot_reach_at_tier` — speculative frontier tops out below the + * displayed tier; + * - `no_exact_at_tier` — no exact read at the displayed tier for any other + * reason (an under-swept frontier that starts above the tier). + */ +export type OverviewMissingReason = + | 'standard_decode_only' + | 'int4_bf16_only' + | 'no_8k1k_data' + | 'cannot_reach_at_tier' + | 'no_exact_at_tier'; + +export const OVERVIEW_HEADLINE_PAIR_DEFINITIONS = [ + { id: 'mi355x-vs-b200', candidateHardware: 'mi355x', baselineHardware: 'b200' }, + { id: 'b300-vs-b200', candidateHardware: 'b300', baselineHardware: 'b200' }, + { id: 'gb200-vs-b200', candidateHardware: 'gb200', baselineHardware: 'b200' }, + { id: 'gb300-vs-b200', candidateHardware: 'gb300', baselineHardware: 'b200' }, +] as const; + +export type OverviewHeadlinePairId = (typeof OVERVIEW_HEADLINE_PAIR_DEFINITIONS)[number]['id']; + +export interface OverviewHeadlinePairMember { + hardware: string; + hardwareLabel: string; + /** + * The precision of this platform's own best qualified speculative read at + * the displayed tier, selected independently per platform (exact first, then + * value, tie → FP4). Null when the platform has no FP4/FP8 speculative + * bucket at all. + */ + precision: string | null; + /** The dbModel (point release) backing this platform's selected read. */ + dbModel: string | null; + /** Exact read when available; non-exact reads retain their boundary but never a numeric value. */ + read: OverviewTierRead; + /** Best @100 read from this platform's own selected dbModel and precision. */ + highRead: OverviewTierRead; + missingReason: OverviewMissingReason | null; +} + +export interface OverviewHeadlinePairComparison { + id: OverviewHeadlinePairId; + label: string; + /** The precision shared by both displayed reads when the delta is comparable; null otherwise. */ + precision: string | null; + /** The dbModel shared by both displayed reads when the delta is comparable; null otherwise. */ + dbModel: string | null; + candidate: OverviewHeadlinePairMember; + baseline: OverviewHeadlinePairMember; + /** + * Candidate relative to B200. Non-null only when both displayed-tier reads + * are exact, share one precision AND one dbModel, and B200 > 0 — a delta + * never mixes FP4 with FP8 or two point releases. + */ + directDeltaPercent: number | null; + /** Why two displayed exact reads still carry no delta; null when a delta exists or a side is missing. */ + deltaUnavailableReason: 'precision_mismatch' | 'version_mismatch' | null; + highLeaderTransition: 'same_hardware' | 'changed_hardware' | null; +} + +export interface OverviewModelSummary { + model: Model; + modelLabel: string; + /** Fixed candidate-vs-B200 serving-stack comparisons, one per matrix column. */ + headlinePairs: OverviewHeadlinePairComparison[]; +} + +export interface OverviewPageData { + models: OverviewModelSummary[]; + datasetThroughDate: string | null; + /** The service point every headline pair was read at. */ + tier: OverviewTier; +} + +/** Precisions the overview may rank, in preference order. */ +const OVERVIEW_PRECISIONS: readonly string[] = [Precision.FP4, Precision.FP8]; + +/** Rows measuring the fixed overview workload, before any other filter. */ +function overviewWorkloadRows(rows: readonly BenchmarkRow[]): BenchmarkRow[] { + return rows.filter( + (row) => + row.benchmark_type === 'single_turn' && + row.isl === OVERVIEW_WORKLOAD.isl && + row.osl === OVERVIEW_WORKLOAD.osl, + ); +} + +/** + * Newest raw workload row across the page. Deliberately not derived from the + * retained winners: a precision or engine the page never ranks still dates the + * dataset it was measured in. + */ +export function overviewDatasetThroughDate(rows: readonly BenchmarkRow[]): string | null { + return overviewWorkloadRows(rows).reduce( + (latest, row) => (latest === null || row.date > latest ? row.date : latest), + null, + ); +} + +/** + * Speculative configs for one precision, grouped by exact deployment identity. + * Standard-decode rows never enter the pool, so a precision is ranked on its + * speculative frontier alone. + */ +function buildPrecisionConfigs( + model: Model, + workloadRows: readonly BenchmarkRow[], + precision: string, +): OverviewConfigResult[] { + const rowsByConfig = new Map(); + for (const row of workloadRows) { + if (row.precision !== precision || row.spec_method === 'none') continue; + const key = overviewConfigIdentityKey(row); + const configRows = rowsByConfig.get(key); + if (configRows) configRows.push(row); + else rowsByConfig.set(key, [row]); + } + + const configs: OverviewConfigResult[] = []; + for (const [key, configRows] of rowsByConfig) { + const config = buildConfigResult(model, precision, key, configRows); + if (config) configs.push(config); + } + return configs; +} + +function readConfigAtTier(config: OverviewConfigResult, tier: number): OverviewTierRead { + const tierValue = config.tierValues.find((value) => value.tier === tier); + return { + tier, + value: tierValue?.value ?? null, + boundary: tierValue?.boundary ?? null, + evidenceDate: tierValue?.evidenceDate ?? null, + config, + }; +} + +/** A tier read known to be backed by a configuration. */ +interface ConfigTierRead extends OverviewTierRead { + config: OverviewConfigResult; +} + +/** + * A tier read counts only when it lands inside the configuration's own measured + * frontier. A clamped or unreachable read is a coverage gap, so it can never + * lead a tier or anchor a percentage gap. + */ +const isExactTierRead = (read: T): read is T & { value: number } => + read.value !== null && read.boundary === 'interpolated'; + +function compareTierReads(a: ConfigTierRead, b: ConfigTierRead): number { + return ( + (b.value ?? -1) - (a.value ?? -1) || + getModelSortIndex(a.config.hardware) - getModelSortIndex(b.config.hardware) || + a.config.key.localeCompare(b.config.key) + ); +} + +/** + * One read per hardware at `tier`: its best exact in-range read, or — only when + * the hardware has no exact read at all — its best out-of-range read, kept so + * the hardware still surfaces as a coverage gap rather than disappearing. + */ +function readsByHardwareAtTier( + configs: readonly OverviewConfigResult[], + tier: number, +): Map { + const reads = configs + .map((config): ConfigTierRead => ({ ...readConfigAtTier(config, tier), config })) + .toSorted(compareTierReads); + + const byHardware = new Map(); + for (const read of reads.filter(isExactTierRead)) { + if (!byHardware.has(read.config.hardware)) byHardware.set(read.config.hardware, read); + } + for (const read of reads) { + if (!byHardware.has(read.config.hardware)) byHardware.set(read.config.hardware, read); + } + return byHardware; +} + +function nullTierRead(tier: number): OverviewTierRead { + return { tier, value: null, boundary: null, evidenceDate: null, config: null }; +} + +function nonComparableAsMissing( + read: OverviewTierRead | undefined, + tier: number, +): OverviewTierRead { + if (read === undefined) return nullTierRead(tier); + return isExactTierRead(read) ? read : { ...read, value: null, evidenceDate: null }; +} + +interface OverviewHeadlinePairBucket { + precision: string; + dbModel: string; + configs: OverviewConfigResult[]; + tierReads: Map; + newestEvidence: string; +} + +function buildHeadlinePairBuckets( + configsByPrecision: ReadonlyMap, + hardware: ReadonlySet, + tier: OverviewTier, +): OverviewHeadlinePairBucket[] { + const buckets: OverviewHeadlinePairBucket[] = []; + for (const precision of OVERVIEW_PRECISIONS) { + const byDbModel = new Map(); + for (const config of configsByPrecision.get(precision) ?? []) { + if (!hardware.has(config.hardware)) continue; + const configs = byDbModel.get(config.dbModel); + if (configs) configs.push(config); + else byDbModel.set(config.dbModel, [config]); + } + + for (const [dbModel, configs] of byDbModel) { + const tierReads = readsByHardwareAtTier(configs, tier); + const exactReads = [...tierReads.values()].filter(isExactTierRead); + buckets.push({ + precision, + dbModel, + configs, + tierReads, + newestEvidence: exactReads.reduce( + (latest, read) => + read.evidenceDate !== null && read.evidenceDate.to > latest + ? read.evidenceDate.to + : latest, + configs.reduce( + (latest, config) => (config.latestDate > latest ? config.latestDate : latest), + '', + ), + ), + }); + } + } + return buckets; +} + +function missingReasonForHeadlineMember( + workloadRows: readonly BenchmarkRow[], + hardware: string, + read: OverviewTierRead, + bucketReads: readonly OverviewTierRead[], +): OverviewMissingReason | null { + if (isExactTierRead(read)) return null; + const hardwareRows = workloadRows.filter((row) => row.hardware === hardware); + if (hardwareRows.length === 0) return 'no_8k1k_data'; + const supportedRows = hardwareRows.filter((row) => OVERVIEW_PRECISIONS.includes(row.precision)); + if (supportedRows.length === 0) return 'int4_bf16_only'; + if (!supportedRows.some((row) => row.spec_method !== 'none')) return 'standard_decode_only'; + // `cannot reach` is a claim about the whole platform, so it holds only when + // EVERY qualified speculative stack 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') + ? 'cannot_reach_at_tier' + : 'no_exact_at_tier'; +} + +function headlineLeaderTransition( + candidatePrimary: OverviewTierRead, + baselinePrimary: OverviewTierRead, + candidateHigh: OverviewTierRead, + baselineHigh: OverviewTierRead, +): OverviewHeadlinePairComparison['highLeaderTransition'] { + if ( + !isExactTierRead(candidatePrimary) || + !isExactTierRead(baselinePrimary) || + !isExactTierRead(candidateHigh) || + !isExactTierRead(baselineHigh) || + candidatePrimary.value === baselinePrimary.value || + candidateHigh.value === baselineHigh.value + ) { + return null; + } + const primaryLeaderIsCandidate = candidatePrimary.value > baselinePrimary.value; + const highLeaderIsCandidate = candidateHigh.value > baselineHigh.value; + return primaryLeaderIsCandidate === highLeaderIsCandidate ? 'same_hardware' : 'changed_hardware'; +} + +function buildHeadlinePairs( + model: Model, + workloadRows: readonly BenchmarkRow[], + tier: OverviewTier, +): OverviewHeadlinePairComparison[] { + // FP4 and FP8 configs are built independently and never blend within a read. + const configsByPrecision = new Map( + OVERVIEW_PRECISIONS.map( + (precision) => [precision, buildPrecisionConfigs(model, workloadRows, precision)] as const, + ), + ); + // Each platform selects its own best qualified bucket (dbModel × precision): + // exact at the displayed tier first, then value, tie → FP4, newest evidence, + // lexical dbModel. One bucket per member keeps point releases from blending + // within a read. + const buildMember = (memberHardware: string): OverviewHeadlinePairMember => { + const candidates = buildHeadlinePairBuckets( + configsByPrecision, + new Set([memberHardware]), + tier, + ).map((memberBucket) => ({ + bucket: memberBucket, + read: nonComparableAsMissing(memberBucket.tierReads.get(memberHardware), tier), + })); + const best = candidates.toSorted( + (a, b) => + Number(isExactTierRead(b.read)) - Number(isExactTierRead(a.read)) || + (b.read.value ?? -1) - (a.read.value ?? -1) || + OVERVIEW_PRECISIONS.indexOf(a.bucket.precision) - + OVERVIEW_PRECISIONS.indexOf(b.bucket.precision) || + b.bucket.newestEvidence.localeCompare(a.bucket.newestEvidence) || + a.bucket.dbModel.localeCompare(b.bucket.dbModel), + )[0]; + const primary = best?.read ?? nullTierRead(tier); + const high = nonComparableAsMissing( + best === undefined + ? undefined + : readsByHardwareAtTier(best.bucket.configs, OVERVIEW_HIGH_TIER).get(memberHardware), + OVERVIEW_HIGH_TIER, + ); + return { + hardware: memberHardware, + hardwareLabel: getHardwareConfig(memberHardware, model).label, + precision: best?.bucket.precision ?? null, + dbModel: best?.bucket.dbModel ?? null, + read: primary, + highRead: high, + missingReason: missingReasonForHeadlineMember( + workloadRows, + memberHardware, + primary, + candidates.map(({ read }) => read), + ), + }; + }; + + return OVERVIEW_HEADLINE_PAIR_DEFINITIONS.map((definition) => { + const candidate = buildMember(definition.candidateHardware); + const baseline = buildMember(definition.baselineHardware); + const bothExact = isExactTierRead(candidate.read) && isExactTierRead(baseline.read); + const comparable = + bothExact && + candidate.precision === baseline.precision && + candidate.dbModel === baseline.dbModel; + return { + id: definition.id, + label: `${candidate.hardwareLabel} vs ${baseline.hardwareLabel}`, + precision: comparable ? candidate.precision : null, + dbModel: comparable ? candidate.dbModel : null, + candidate, + baseline, + directDeltaPercent: + comparable && + candidate.read.value !== null && + baseline.read.value !== null && + baseline.read.value > 0 + ? (candidate.read.value / baseline.read.value - 1) * 100 + : null, + deltaUnavailableReason: bothExact + ? candidate.precision === baseline.precision + ? candidate.dbModel === baseline.dbModel + ? null + : 'version_mismatch' + : 'precision_mismatch' + : null, + // On the 100 view the displayed reads ARE the high reads, so a + // transition line would restate the cell values. + highLeaderTransition: + comparable && tier !== OVERVIEW_HIGH_TIER + ? headlineLeaderTransition( + candidate.read, + baseline.read, + candidate.highRead, + baseline.highRead, + ) + : null, + }; + }); +} + +function buildConfigResult( + model: Model, + precision: string, + key: string, + rows: BenchmarkRow[], +): OverviewConfigResult | null { + const feed = computeTcoFeed(rows, [OVERVIEW_WORKLOAD], OVERVIEW_TIERS); + if (feed.length === 0) return null; + + const first = rows[0]; + const { hardware, framework, spec_method: specMethod, disagg } = first; + const sourceRunUrls = [ + ...new Set(rows.flatMap((row) => (row.run_url === null ? [] : [row.run_url]))), + ].toSorted(); + return { + key, + dbModel: first.model, + hardware, + hwKey: buildAvailabilityHwKey(hardware, framework, specMethod, disagg), + framework, + frameworkLabel: resolveFrameworkPartLabel(model, framework), + specMethod, + specLabel: resolveFrameworkPartLabel(model, specMethod), + disagg, + precision, + sourceRunUrls, + tierValues: feed.map((row) => { + const value = + row.boundary === 'unreachable' && row.output_tput_per_gpu === 0 + ? null + : row.output_tput_per_gpu; + return { + tier: row.tier, + value, + boundary: row.boundary, + evidenceDate: value === null ? null : row.evidence_date, + }; + }), + latestDate: feed[0].latest_date, + }; +} + +export function buildOverviewModelSummary( + model: Model, + rows: BenchmarkRow[], + tier: OverviewTier = OVERVIEW_PRIMARY_TIER, +): OverviewModelSummary { + return { + model, + modelLabel: getModelLabel(model), + headlinePairs: buildHeadlinePairs(model, overviewWorkloadRows(rows), tier), + }; +} + +/** + * Assemble the whole page from rows already grouped by DISPLAY model. Iterating + * DEFAULT_MODELS fixes the output order and renders every active model — a model + * with no rows still gets its four pairs, every member carrying a missing + * reason. The live server path and the e2e fixture path feed this same + * function; only the row source differs. + */ +export function assembleOverviewPageData( + rowsByModel: Record, + tier: OverviewTier = OVERVIEW_PRIMARY_TIER, +): OverviewPageData { + const perModel = [...DEFAULT_MODELS].map((model) => ({ model, rows: rowsByModel[model] ?? [] })); + return { + models: perModel.map(({ model, rows }) => buildOverviewModelSummary(model, rows, tier)), + datasetThroughDate: overviewDatasetThroughDate(perModel.flatMap(({ rows }) => rows)), + tier, + }; +} diff --git a/packages/app/src/lib/overview-links.test.ts b/packages/app/src/lib/overview-links.test.ts new file mode 100644 index 00000000..71406dd6 --- /dev/null +++ b/packages/app/src/lib/overview-links.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from 'vitest'; + +import { Model, Precision } from './data-mappings'; +import type { OverviewConfigResult, OverviewModelSummary } from './overview-data'; +import { buildOverviewDashboardHref, detailHref } from './overview-links'; + +const RUN_URL = 'https://github.com/SemiAnalysisAI/InferenceX/actions/runs/26714221123'; + +/** Query the default fixture produces: one source run, so the run is pinned. */ +const PINNED_QUERY = + 'g_model=Qwen-3.5-397B-A17B&g_rundate=2026-07-18&g_runid=26714221123&i_seq=8k%2F1k' + + '&i_prec=fp4&i_gpus=b200_sglang_mtp&i_spec=mtp&i_disagg=agg&i_optimal=1&i_advlabel=1'; + +function config(overrides: Partial = {}): OverviewConfigResult { + return { + key: 'qwen3.5|b200|sglang|mtp|agg|fp4', + dbModel: 'qwen3.5', + hardware: 'b200', + hwKey: 'b200_sglang_mtp', + framework: 'sglang', + frameworkLabel: 'SGLang', + specMethod: 'mtp', + specLabel: 'MTP', + disagg: false, + precision: Precision.FP4, + sourceRunUrls: [RUN_URL], + tierValues: [{ tier: 50, value: 1000, boundary: 'interpolated', evidenceDate: null }], + latestDate: '2026-07-18', + ...overrides, + }; +} + +function summary(overrides: Partial = {}): OverviewModelSummary { + return { + model: Model.Qwen3_5, + modelLabel: 'Qwen 3.5', + headlinePairs: [], + ...overrides, + }; +} + +describe('buildOverviewDashboardHref', () => { + it('pins model, run, workload and exact configuration on the English route', () => { + expect(buildOverviewDashboardHref('en', summary(), config())).toBe( + `/inference?${PINNED_QUERY}`, + ); + }); + + it('selects the disaggregated deployment mode for a disaggregated configuration', () => { + const href = buildOverviewDashboardHref( + 'en', + summary(), + config({ disagg: true, hwKey: 'gb200_dynamo-trt-disagg_mtp' }), + ); + + expect(href).toBe( + '/inference?g_model=Qwen-3.5-397B-A17B&g_rundate=2026-07-18&g_runid=26714221123' + + '&i_seq=8k%2F1k&i_prec=fp4&i_gpus=gb200_dynamo-trt-disagg_mtp&i_spec=mtp' + + '&i_disagg=disagg&i_optimal=1&i_advlabel=1', + ); + }); + + it('writes g_model even when it equals the dashboard default model', () => { + const href = buildOverviewDashboardHref( + 'en', + summary({ model: Model.DeepSeek_V4_Pro }), + config({ precision: Precision.FP8 }), + ); + + expect(href).toContain('g_model=DeepSeek-V4-Pro'); + expect(href).toContain('i_prec=fp8'); + }); + + 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', + ); + expect(buildOverviewDashboardHref('en', summary(), config({ specMethod: 'none' }))).toContain( + 'i_spec=stp', + ); + expect(buildOverviewDashboardHref('en', summary(), config({ specMethod: 'mtp' }))).toContain( + 'i_spec=mtp', + ); + }); +}); + +describe('detailHref', () => { + it('keeps the model drilldown precision-neutral because headline pairs may differ', () => { + expect(detailHref('en', summary())).toBe( + '/inference?g_model=Qwen-3.5-397B-A17B&i_seq=8k%2F1k&i_optimal=1', + ); + }); +}); diff --git a/packages/app/src/lib/overview-links.ts b/packages/app/src/lib/overview-links.ts new file mode 100644 index 00000000..132def11 --- /dev/null +++ b/packages/app/src/lib/overview-links.ts @@ -0,0 +1,103 @@ +/** + * @file overview-links.ts + * @description Evidence links for the overview scorecard: a pre-filtered + * /inference view and the CI run a configuration came from. Pure and + * React-free so the server-rendered overview page can build them. + */ + +import { runIdFromRunUrl } from './known-issues'; +import { + OVERVIEW_PRIMARY_TIER, + type OverviewConfigResult, + type OverviewModelSummary, + type OverviewTier, +} 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'; + +/** The `/inference` route base for a locale — shared by every overview link. */ +function inferenceRoute(locale: 'en' | 'zh'): string { + return locale === 'zh' ? '/zh/inference' : '/inference'; +} + +/** + * Maps a raw DB `spec_method` to the dashboard's `SpecMode` filter bucket + * (mirrors `pointSpecMode` in quickFilters.ts, minus its hwKey suffix check — + * overview `specMethod` comes straight from `spec_method`). + */ +function dashboardSpecMode(specMethod: string): 'mtp' | 'stp' { + return specMethod === 'none' || specMethod === '' ? 'stp' : 'mtp'; +} + +/** + * The one run backing a configuration, or null when it has none, has several, + * or its single source URL names no run (a run list rather than a run). Both + * helpers below read this one predicate, so the `g_runid` pin and the source-run + * link can never disagree about whether a single run backs the configuration. + */ +function soleSourceRun(config: OverviewConfigResult): { url: string; id: string } | null { + if (config.sourceRunUrls.length !== 1) return null; + const url = config.sourceRunUrls[0]; + const id = runIdFromRunUrl(url); + return id === null ? null : { url, id }; +} + +/** + * Inference-dashboard link narrowed to the configuration the overview ranked: + * its model, run date, workload, precision, hardware/framework/spec key and + * deployment mode. The run is pinned only when a single run produced the + * configuration — pinning one of several would hide the rest of its frontier. + * + * This is a filtered view, not a proof of topology: `i_gpus` selects a + * hardware/framework/spec key, which can still hold more than one parallelism + * or GPU-count topology. + */ +export function buildOverviewDashboardHref( + locale: 'en' | 'zh', + model: OverviewModelSummary, + config: OverviewConfigResult, +): string { + const params: UrlStateParams = { + g_model: model.model, + g_rundate: config.latestDate, + g_runid: soleSourceRun(config)?.id, + i_seq: OVERVIEW_WORKLOAD_SEQ, + i_prec: config.precision, + i_gpus: config.hwKey, + i_spec: dashboardSpecMode(config.specMethod), + i_disagg: config.disagg ? 'disagg' : 'agg', + i_optimal: '1', + i_advlabel: '1', + }; + + const query = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) query.set(key, value); + } + return `${inferenceRoute(locale)}?${query}`; +} + +/** + * Model-level dashboard view: precision-neutral, because the two headline pairs + * may select different precisions. Result-level evidence links narrow further. + */ +export function detailHref(locale: 'en' | 'zh', model: OverviewModelSummary): string { + const query = new URLSearchParams({ + g_model: model.model, + i_seq: OVERVIEW_WORKLOAD_SEQ, + i_optimal: '1', + }); + return `${inferenceRoute(locale)}?${query}`; +} + +/** + * The overview itself at another service point. The default tier keeps the bare + * canonical URL; other tiers ride a plain `?tier=` query the server re-renders + * from, so any view is a copyable link. + */ +export function overviewTierHref(locale: 'en' | 'zh', tier: OverviewTier): string { + const base = locale === 'zh' ? '/zh/overview' : '/overview'; + return tier === OVERVIEW_PRIMARY_TIER ? base : `${base}?tier=${tier}`; +} diff --git a/packages/app/src/lib/tco-feed.test.ts b/packages/app/src/lib/tco-feed.test.ts index 4775c4d9..d6cf6203 100644 --- a/packages/app/src/lib/tco-feed.test.ts +++ b/packages/app/src/lib/tco-feed.test.ts @@ -180,6 +180,34 @@ describe('computeTcoFeed — frontier reads', () => { expect(rows[0].oldest_frontier_date).toBe('2026-04-25'); }); + it("uses only an exact interior frontier knot's date as evidence", () => { + const rows = computeTcoFeed( + [ + makeRow({ itl: 1 / 20, otput: 1000, date: '2026-04-25' }), + makeRow({ itl: 1 / 50, otput: 400, date: '2026-05-20' }), + makeRow({ itl: 1 / 100, otput: 100, date: '2026-07-12' }), + ], + WORKLOAD_8K1K, + [50], + ); + + expect(rows[0].evidence_date).toEqual({ from: '2026-05-20', to: '2026-05-20' }); + }); + + it('uses both bracketing frontier-knot dates as evidence between knots', () => { + const rows = computeTcoFeed( + [ + makeRow({ itl: 1 / 20, otput: 1000, date: '2026-04-25' }), + makeRow({ itl: 1 / 50, otput: 400, date: '2026-05-20' }), + makeRow({ itl: 1 / 100, otput: 100, date: '2026-07-12' }), + ], + WORKLOAD_8K1K, + [75], + ); + + expect(rows[0].evidence_date).toEqual({ from: '2026-05-20', to: '2026-07-12' }); + }); + it('groups by workload and hardware, sorted by hardware key', () => { const rows = computeTcoFeed( [ diff --git a/packages/app/src/lib/tco-feed.ts b/packages/app/src/lib/tco-feed.ts index 12630d4e..083c37d6 100644 --- a/packages/app/src/lib/tco-feed.ts +++ b/packages/app/src/lib/tco-feed.ts @@ -61,6 +61,14 @@ export interface TcoFeedRow { latest_date: string; /** Oldest benchmark date among the frontier knots (staleness flag). */ oldest_frontier_date: string; + /** + * Run dates of the frontier knot(s) actually backing THIS tier read: the + * bracketing pair when interpolated, the single min-interactivity knot when + * clamped_low, null when unreachable. Unlike latest/oldest_frontier_date + * (whole-frontier freshness), it never blends knots outside the tier, so a + * downstream result can carry its own evidence date. Ordered ascending. + */ + evidence_date: { from: string; to: string } | null; } export const DEFAULT_TIERS: readonly number[] = [30, 50, 75, 100]; @@ -197,6 +205,28 @@ interface FrontierPoint { 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 } { + return a.date <= b.date ? { from: a.date, to: b.date } : { from: b.date, to: a.date }; +} + +/** + * The frontier knots backing an interpolated read at `tier`: the bracketing + * 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] { + 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]]; + let lo = 0; + for (let i = 0; i < last; i++) { + if (frontier[i].interactivity === tier) return [frontier[i], frontier[i]]; + if (frontier[i].interactivity <= tier) lo = i; + } + return [frontier[lo], frontier[lo + 1]]; +} + /** * Compute the feed: for each (workload, hardware), build the * (interactivity, output tok/s/GPU) Pareto frontier across every config — @@ -268,16 +298,22 @@ export function computeTcoFeed( for (const tier of tiers) { let value: number; let boundary: TcoTierBoundary; + let evidenceDate: { from: string; to: string } | null; 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]); } else { const raw = hermiteInterpolate(xs, ys, slopes, tier); value = Math.max(yLo, Math.min(yHi, raw)); boundary = 'interpolated'; + const [lo, hi] = bracketKnots(frontier, tier); + evidenceDate = evidenceRange(lo, hi); } out.push({ hardware, @@ -290,6 +326,7 @@ export function computeTcoFeed( frontier_max_interactivity: round3(maxIv), latest_date: latest, oldest_frontier_date: oldest, + evidence_date: evidenceDate, }); } } diff --git a/packages/app/src/lib/toggle-set.ts b/packages/app/src/lib/toggle-set.ts new file mode 100644 index 00000000..963d3ea4 --- /dev/null +++ b/packages/app/src/lib/toggle-set.ts @@ -0,0 +1,32 @@ +/** + * Pure toggle state transition. + * + * - If all items are active and one is toggled, only that item remains active (solo). + * - If only one item is active and it's toggled, all items become active (restore all). + * - Otherwise, the item is simply added or removed from the active set. + * + * Kept free of React so server-only modules (`exclusion.ts`, and through it the + * overview data layer) can reuse it without pulling client hooks into a Server + * Component graph. + */ +export function computeToggle(prev: Set, item: string, allItems: Set): Set { + const allAreActive = prev.size === allItems.size; + const isActive = prev.has(item); + + if (isActive) { + if (allAreActive) { + // Solo: only keep the clicked item + return new Set([item]); + } else if (prev.size === 1) { + // Restore all: re-enable everything + return allItems; + } + // Remove the clicked item + const next = new Set(prev); + next.delete(item); + return next; + } + // Add the clicked item + const next = new Set([...prev, item]); + return next; +}