diff --git a/.vscode/settings.json b/.vscode/settings.json index b43223d..be9fa58 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -55,6 +55,7 @@ "pageleave", "postbuild", "posthog", + "powerx", "protobufjs", "qwen", "renderable", @@ -65,6 +66,7 @@ "smallint", "smallserial", "timestamptz", + "togglable", "tput", "trtllm", "ttft", diff --git a/README.md b/README.md index 22facd5..2040572 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A [Next.js](https://nextjs.org) dashboard for visualizing ML inference benchmark ## Overview -LLM inference performance is a major concern of providing AI services, but accurate performance analysis remains elusive. Fast cadence of software development and model releases makes comparing performance between setups difficult. Existing performance benchmarks quickly become obsolete due to being static, and participants game the benchmarks with unrealistic, highly specific configurations. InferenceX (formerly InferenceMAX) tackles these issues by benchmarking popular models on major hardware platforms nightly with the latest software. For each model and hardware combination, InferenceX sweeps through different tensor parallel sizes and max concurrent requests, showing the throughput vs. latency graph for the full picture. In terms of software configurations, we ensure they are generally applicable across different serving scenarios, and we open source the repo to welcome community contributions. We hope InferenceX informs the community up-to-date and realistic LLM inference performance. +LLM inference performance is a major concern of providing AI services, but accurate performance analysis remains elusive. Fast cadence of software development and model releases makes comparing performance between setups difficult. Existing performance benchmarks quickly become obsolete due to being static, and participants game the benchmarks with unrealistic, highly specific configurations. InferenceX tackles these issues by benchmarking popular models on major hardware platforms nightly with the latest software. For each model and hardware combination, InferenceX sweeps through different tensor parallel sizes and max concurrent requests, showing the throughput vs. latency graph for the full picture. In terms of software configurations, we ensure they are generally applicable across different serving scenarios, and we open source the repo to welcome community contributions. We hope InferenceX informs the community up-to-date and realistic LLM inference performance. ## Architecture diff --git a/packages/app/cypress/component/chart-legend.cy.tsx b/packages/app/cypress/component/chart-legend.cy.tsx index d1310a9..4a362c2 100644 --- a/packages/app/cypress/component/chart-legend.cy.tsx +++ b/packages/app/cypress/component/chart-legend.cy.tsx @@ -58,9 +58,11 @@ function ChartLegendWrapper({ items = MOCK_ITEMS }: { items?: CommonLegendItemPr isLegendExpanded={expanded} onExpandedChange={setExpanded} variant="sidebar" - showResetFilter={itemsWithHandler.some((i) => !i.isActive)} - allSelected={itemsWithHandler.every((i) => i.isActive)} - onResetFilter={() => setLegendItems(items)} + actions={ + itemsWithHandler.some((i) => !i.isActive) + ? [{ id: 'reset-filter', label: 'Reset filter', onClick: () => setLegendItems(items) }] + : [] + } /> ); } diff --git a/packages/app/cypress/component/favorite-presets.cy.tsx b/packages/app/cypress/component/favorite-presets.cy.tsx deleted file mode 100644 index 4745a16..0000000 --- a/packages/app/cypress/component/favorite-presets.cy.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import FavoritePresetsDropdown from '@/components/favorites/FavoritePresetsDropdown'; -import { FAVORITE_PRESETS } from '@/components/favorites/favorite-presets'; -import { mountWithProviders } from '../support/test-utils'; - -describe('FavoritePresetsDropdown', () => { - beforeEach(() => { - mountWithProviders(, { inference: {} }); - }); - - it('renders the toggle button', () => { - cy.get('[data-testid="favorites-toggle"]').should('be.visible'); - cy.get('[data-testid="favorites-toggle"]').should('contain.text', 'Favorites'); - }); - - it('panel is hidden by default', () => { - cy.get('[data-testid="favorites-panel"]').should('not.exist'); - }); - - it('clicking toggle opens the panel', () => { - cy.get('[data-testid="favorites-toggle"]').click(); - cy.get('[data-testid="favorites-panel"]').should('be.visible'); - }); - - it('clicking toggle again closes the panel', () => { - cy.get('[data-testid="favorites-toggle"]').click(); - cy.get('[data-testid="favorites-panel"]').should('be.visible'); - cy.get('[data-testid="favorites-toggle"]').click(); - cy.get('[data-testid="favorites-panel"]').should('not.exist'); - }); - - it('renders all preset cards with their titles', () => { - cy.get('[data-testid="favorites-toggle"]').click(); - - for (const preset of FAVORITE_PRESETS) { - cy.get(`[data-testid="favorite-preset-${preset.id}"]`) - .should('be.visible') - .and('contain.text', preset.title); - } - }); - - it('preset cards show descriptions and tags', () => { - cy.get('[data-testid="favorites-toggle"]').click(); - - const first = FAVORITE_PRESETS[0]; - cy.get(`[data-testid="favorite-preset-${first.id}"]`).should( - 'contain.text', - first.description.slice(0, 30), - ); - - for (const tag of first.tags) { - cy.get(`[data-testid="favorite-preset-${first.id}"]`).should('contain.text', tag); - } - }); - - it('clicking a preset calls context setters to apply it', () => { - cy.get('[data-testid="favorites-toggle"]').click(); - - const preset = FAVORITE_PRESETS[0]; // gb200-vs-b200 - cy.get(`[data-testid="favorite-preset-${preset.id}"]`).click(); - - cy.get('@setSelectedModel').should('have.been.calledWith', preset.config.model); - cy.get('@setSelectedSequence').should('have.been.calledWith', preset.config.sequence); - cy.get('@setSelectedPrecisions').should('have.been.calledWith', preset.config.precisions); - cy.get('@setSelectedYAxisMetric').should('have.been.calledWith', preset.config.yAxisMetric); - cy.get('@setActivePresetId').should('have.been.calledWith', preset.id); - }); - - it('clicking an active preset clears it', () => { - // Mount with the first preset already active - mountWithProviders(, { - inference: { activePresetId: FAVORITE_PRESETS[0].id }, - }); - - cy.get('[data-testid="favorites-toggle"]').click(); - cy.get(`[data-testid="favorite-preset-${FAVORITE_PRESETS[0].id}"]`).click(); - - // Clearing sets activePresetId to null - cy.get('@setActivePresetId').should('have.been.calledWith', null); - cy.get('@selectAllHwTypes').should('have.been.called'); - }); - - it('shows "Active" badge when a preset is active', () => { - mountWithProviders(, { - inference: { activePresetId: FAVORITE_PRESETS[0].id }, - }); - - cy.get('[data-testid="favorites-toggle"]').should('contain.text', FAVORITE_PRESETS[0].title); - cy.get('[data-testid="favorites-toggle"]').should('contain.text', 'Active'); - }); -}); diff --git a/packages/app/cypress/e2e/favorite-presets.cy.ts b/packages/app/cypress/e2e/favorite-presets.cy.ts deleted file mode 100644 index 4c1bf44..0000000 --- a/packages/app/cypress/e2e/favorite-presets.cy.ts +++ /dev/null @@ -1,83 +0,0 @@ -describe('Favorite Presets', () => { - before(() => { - cy.visit('/', { - onBeforeLoad(win) { - win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); - }, - }); - cy.get('[data-testid="model-selector"]').should('be.visible'); - }); - - it('renders the favorites dropdown toggle', () => { - cy.get('[data-testid="favorites-toggle"]').should('be.visible'); - cy.get('[data-testid="favorites-toggle"]').should('contain.text', 'Favorites'); - }); - - it('opens the preset panel on click', () => { - cy.get('[data-testid="favorites-toggle"]').click(); - cy.get('[data-testid="favorites-panel"]').should('be.visible'); - }); - - it('shows all 6 preset cards', () => { - cy.get('[data-testid^="favorite-preset-"]').should('have.length', 6); - }); - - it('each preset card has a title and description', () => { - cy.get('[data-testid^="favorite-preset-"]').each(($card) => { - cy.wrap($card).find('p').first().should('not.be.empty'); - cy.wrap($card).find('p').eq(1).should('not.be.empty'); - }); - }); - - it('each preset card has at least one tag badge', () => { - cy.get('[data-testid^="favorite-preset-"]').each(($card) => { - cy.wrap($card).find('.flex-wrap').children().should('have.length.greaterThan', 0); - }); - }); - - // Scatter preset: b200-vs-h200 - it('activating b200-vs-h200 shows Active badge and renders data', () => { - cy.get('[data-testid="favorite-preset-b200-vs-h200"]').click(); - cy.get('[data-testid="favorites-toggle"]').should('contain.text', 'B200 vs H200'); - cy.get('[data-testid="favorites-toggle"]').should('contain.text', 'Active'); - - // Chart should render with data points (not empty) - cy.get('[data-testid="scatter-graph"]').first().find('svg .dot-group').should('exist'); - cy.contains('No data available').should('not.exist'); - }); - - // Deactivate preset - it('clicking the active preset again deactivates it', () => { - cy.get('[data-testid="favorite-preset-b200-vs-h200"]').click(); - cy.get('[data-testid="favorites-toggle"]').should('contain.text', 'Favorites'); - cy.get('[data-testid="favorites-toggle"]').should('not.contain.text', 'Active'); - - // Chart should still render (not empty) - cy.get('[data-testid="scatter-graph"]').first().find('svg').should('exist'); - }); - - // Scatter preset: amd-generations - it('activating amd-generations shows AMD preset and renders data', () => { - cy.get('[data-testid="favorite-preset-amd-generations"]').click(); - cy.get('[data-testid="favorites-toggle"]').should('contain.text', 'AMD'); - cy.get('[data-testid="favorites-toggle"]').should('contain.text', 'Active'); - - cy.get('[data-testid="scatter-graph"]').first().find('svg .dot-group').should('exist'); - cy.contains('No data available').should('not.exist'); - }); - - // Switch directly to another preset - it('switching from one preset to another updates the active state', () => { - cy.get('[data-testid="favorite-preset-gb200-vs-b200"]').click(); - cy.get('[data-testid="favorites-toggle"]').should('contain.text', 'GB200'); - cy.get('[data-testid="favorites-toggle"]').should('contain.text', 'Active'); - }); - - // Clean up: deactivate and close - it('deactivate and close the panel', () => { - cy.get('[data-testid="favorite-preset-gb200-vs-b200"]').click(); - cy.get('[data-testid="favorites-toggle"]').should('not.contain.text', 'Active'); - cy.get('[data-testid="favorites-toggle"]').click(); - cy.get('[data-testid="favorites-panel"]').should('not.exist'); - }); -}); diff --git a/packages/app/cypress/support/mock-data.ts b/packages/app/cypress/support/mock-data.ts index eb0b251..e7a8c1d 100644 --- a/packages/app/cypress/support/mock-data.ts +++ b/packages/app/cypress/support/mock-data.ts @@ -34,42 +34,36 @@ export function createMockHardwareConfig(): HardwareConfig { label: 'H100', suffix: '', gpu: "NVIDIA 'Hopper' H100", - color: 'var(--h100)', }, h200: { name: 'h200', label: 'H200', suffix: '', gpu: "NVIDIA 'Hopper' H200", - color: 'var(--h200)', }, b200: { name: 'b200', label: 'B200', suffix: '', gpu: "NVIDIA 'Blackwell' B200", - color: 'var(--b200)', }, b200_trt: { name: 'b200-trt', label: 'B200', suffix: '(TRT)', gpu: "NVIDIA 'Blackwell' B200 TRT", - color: 'var(--b200-trt)', }, mi300x: { name: 'mi300x', label: 'MI300X', suffix: '', gpu: 'AMD Instinct MI300X', - color: 'var(--mi300x)', }, h100_vllm: { name: 'h100-vllm', label: 'H100', suffix: '(vLLM)', gpu: "NVIDIA 'Hopper' H100 vLLM", - color: 'var(--h100-vllm)', framework: 'vllm', }, b200_sglang: { @@ -77,7 +71,6 @@ export function createMockHardwareConfig(): HardwareConfig { label: 'B200', suffix: '(SGLang)', gpu: "NVIDIA 'Blackwell' B200 SGLang", - color: 'var(--b200-sglang)', framework: 'sglang', }, }; @@ -170,8 +163,10 @@ export function createMockInferenceContext( activeHwTypes: new Set(['h100', 'b200', 'b200_trt', 'mi300x', 'h200']), hwTypesWithData: new Set(['h100', 'b200', 'b200_trt', 'mi300x', 'h200']), toggleHwType: namedStub('toggleHwType'), + removeHwType: namedStub('removeHwType'), selectAllHwTypes: namedStub('selectAllHwTypes'), toggleActiveDate: namedStub('toggleActiveDate'), + removeActiveDate: namedStub('removeActiveDate'), selectAllActiveDates: namedStub('selectAllActiveDates'), activeDates: new Set(['2025-03-01']), hardwareConfig: hwConfig, @@ -310,6 +305,7 @@ export function createMockEvaluationContext( unfilteredChartData: [createMockEvaluationChartData()], enabledHardware: new Set(['b200_trt', 'h100', 'mi300x']), toggleHardware: namedStub('toggleHardware'), + removeHardware: namedStub('removeHardware'), highContrast: false, setHighContrast: namedStub('setHighContrast_eval'), showLabels: true, @@ -373,6 +369,7 @@ export function createMockReliabilityContext( setHighContrast: namedStub('setHighContrast_reliability'), enabledModels: new Set([Model.DeepSeek_R1]), toggleModel: namedStub('toggleModel'), + removeModel: namedStub('removeModel'), isLegendExpanded: true, setIsLegendExpanded: namedStub('setIsLegendExpanded_reliability'), modelsWithData: new Set([Model.DeepSeek_R1]), diff --git a/packages/app/package.json b/packages/app/package.json index cb46bcb..8769901 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -2,7 +2,7 @@ "name": "@semianalysisai/inferencex-app", "version": "0.1.0", "private": true, - "description": "InferenceX (formerly InferenceMAX) - Inference performance benchmarking and visualization", + "description": "InferenceX - Inference performance benchmarking and visualization", "repository": { "type": "git", "url": "https://github.com/SemiAnalysisAI/inferencemax-app" diff --git a/packages/app/src/app/(dashboard)/calculator/page.tsx b/packages/app/src/app/(dashboard)/calculator/page.tsx new file mode 100644 index 0000000..0bd2ead --- /dev/null +++ b/packages/app/src/app/(dashboard)/calculator/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from 'next'; + +import ThroughputCalculatorDisplay from '@/components/calculator/ThroughputCalculatorDisplay'; +import { tabMetadata } from '@/lib/tab-meta'; + +export const metadata: Metadata = tabMetadata('calculator'); + +export default function CalculatorPage() { + return ; +} diff --git a/packages/app/src/app/(dashboard)/evaluation/page.tsx b/packages/app/src/app/(dashboard)/evaluation/page.tsx new file mode 100644 index 0000000..f1be9b0 --- /dev/null +++ b/packages/app/src/app/(dashboard)/evaluation/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next'; + +import { EvaluationProvider } from '@/components/evaluation/EvaluationContext'; +import EvaluationChartDisplay from '@/components/evaluation/ui/ChartDisplay'; +import { tabMetadata } from '@/lib/tab-meta'; + +export const metadata: Metadata = tabMetadata('evaluation'); + +export default function EvaluationPage() { + return ( + + + + ); +} diff --git a/packages/app/src/app/(dashboard)/gpu-metrics/page.tsx b/packages/app/src/app/(dashboard)/gpu-metrics/page.tsx new file mode 100644 index 0000000..8669582 --- /dev/null +++ b/packages/app/src/app/(dashboard)/gpu-metrics/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from 'next'; + +import GpuMetricsDisplay from '@/components/gpu-power/GpuPowerDisplay'; +import { tabMetadata } from '@/lib/tab-meta'; + +export const metadata: Metadata = tabMetadata('gpu-metrics'); + +export default function GpuMetricsPage() { + return ; +} diff --git a/packages/app/src/app/(dashboard)/gpu-specs/page.tsx b/packages/app/src/app/(dashboard)/gpu-specs/page.tsx new file mode 100644 index 0000000..f93f719 --- /dev/null +++ b/packages/app/src/app/(dashboard)/gpu-specs/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from 'next'; + +import { GpuSpecsContent } from '@/components/gpu-specs/gpu-specs-content'; +import { tabMetadata } from '@/lib/tab-meta'; + +export const metadata: Metadata = tabMetadata('gpu-specs'); + +export default function GpuSpecsPage() { + return ; +} diff --git a/packages/app/src/app/(dashboard)/historical/page.tsx b/packages/app/src/app/(dashboard)/historical/page.tsx new file mode 100644 index 0000000..e790a27 --- /dev/null +++ b/packages/app/src/app/(dashboard)/historical/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next'; + +import { InferenceProvider } from '@/components/inference/InferenceContext'; +import HistoricalTrendsDisplay from '@/components/trends/HistoricalTrendsDisplay'; +import { tabMetadata } from '@/lib/tab-meta'; + +export const metadata: Metadata = tabMetadata('historical'); + +export default function HistoricalPage() { + return ( + + + + ); +} diff --git a/packages/app/src/app/(dashboard)/inference/page.tsx b/packages/app/src/app/(dashboard)/inference/page.tsx new file mode 100644 index 0000000..894ac0d --- /dev/null +++ b/packages/app/src/app/(dashboard)/inference/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next'; + +import { InferenceProvider } from '@/components/inference/InferenceContext'; +import InferenceChartDisplay from '@/components/inference/ui/ChartDisplay'; +import { tabMetadata } from '@/lib/tab-meta'; + +export const metadata: Metadata = tabMetadata('inference'); + +export default function InferencePage() { + return ( + + + + ); +} diff --git a/packages/app/src/app/(dashboard)/layout.tsx b/packages/app/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..4d38318 --- /dev/null +++ b/packages/app/src/app/(dashboard)/layout.tsx @@ -0,0 +1,5 @@ +import { DashboardShell } from '@/components/dashboard-shell'; + +export default function DashboardLayout({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/packages/app/src/app/(dashboard)/reliability/page.tsx b/packages/app/src/app/(dashboard)/reliability/page.tsx new file mode 100644 index 0000000..6c647d3 --- /dev/null +++ b/packages/app/src/app/(dashboard)/reliability/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next'; + +import { ReliabilityProvider } from '@/components/reliability/ReliabilityContext'; +import ReliabilityChartDisplay from '@/components/reliability/ui/ChartDisplay'; +import { tabMetadata } from '@/lib/tab-meta'; + +export const metadata: Metadata = tabMetadata('reliability'); + +export default function ReliabilityPage() { + return ( + + + + ); +} diff --git a/packages/app/src/app/(landing)/page.tsx b/packages/app/src/app/(landing)/page.tsx new file mode 100644 index 0000000..64097c9 --- /dev/null +++ b/packages/app/src/app/(landing)/page.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next'; + +import { LandingPage } from '@/components/landing/landing-page'; +import { LANDING_META } from '@/lib/tab-meta'; +import { SITE_URL } from '@semianalysisai/inferencex-constants'; + +export const metadata: Metadata = { + title: LANDING_META.title, + description: LANDING_META.description, + alternates: { canonical: SITE_URL }, + openGraph: { + title: `${LANDING_META.title} | InferenceX`, + description: LANDING_META.description, + url: SITE_URL, + }, + twitter: { + title: `${LANDING_META.title} | InferenceX`, + description: LANDING_META.description, + }, +}; + +export default function HomePage() { + return ; +} diff --git a/packages/app/src/app/[[...tab]]/page.tsx b/packages/app/src/app/[[...tab]]/page.tsx deleted file mode 100644 index acd78bf..0000000 --- a/packages/app/src/app/[[...tab]]/page.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type { Metadata } from 'next'; -import { notFound } from 'next/navigation'; - -import { PageContent } from '@/components/page-content'; -import { TAB_META, VALID_TABS } from '@/lib/tab-meta'; -import { SITE_URL } from '@semianalysisai/inferencex-constants'; - -export const dynamicParams = true; - -export function generateStaticParams() { - return [{ tab: [] }, ...VALID_TABS.map((t) => ({ tab: [t] }))]; -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ tab?: string[] }>; -}): Promise { - const { tab } = await params; - const activeTab = tab?.[0] ?? 'inference'; - const meta = TAB_META[activeTab as keyof typeof TAB_META]; - if (!meta) return {}; - - const url = activeTab === 'inference' ? SITE_URL : `${SITE_URL}/${activeTab}`; - - return { - title: meta.title, - description: meta.description, - alternates: { canonical: url }, - openGraph: { - title: `${meta.title} | InferenceX`, - description: meta.description, - url, - }, - twitter: { - title: `${meta.title} | InferenceX`, - description: meta.description, - }, - }; -} - -export default async function Page({ params }: { params: Promise<{ tab?: string[] }> }) { - const { tab } = await params; - const activeTab = tab?.[0] ?? 'inference'; - - if (!VALID_TABS.includes(activeTab as (typeof VALID_TABS)[number]) || (tab && tab.length > 1)) { - notFound(); - } - - return ; -} diff --git a/packages/app/src/app/about/page.tsx b/packages/app/src/app/about/page.tsx new file mode 100644 index 0000000..776e455 --- /dev/null +++ b/packages/app/src/app/about/page.tsx @@ -0,0 +1,126 @@ +import type { Metadata } from 'next'; +import Link from 'next/link'; + +import { Card } from '@/components/ui/card'; +import { FAQ_ITEMS } from '@/components/about/faq-data'; +import { SITE_URL } from '@semianalysisai/inferencex-constants'; + +const faqJsonLd = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: FAQ_ITEMS.map((item) => ({ + '@type': 'Question', + name: item.question, + acceptedAnswer: { + '@type': 'Answer', + text: [item.answer, item.link?.text, ...(item.list ?? [])].filter(Boolean).join(' '), + }, + })), +}; + +export const metadata: Metadata = { + title: 'About', + description: + 'InferenceX is an independent, vendor neutral, reproducible benchmark which continuously benchmarks inference software across a wide range of AI accelerators.', + alternates: { canonical: `${SITE_URL}/about` }, + openGraph: { + title: 'About | InferenceX', + description: + 'InferenceX is an independent, vendor neutral, reproducible benchmark which continuously benchmarks inference software across a wide range of AI accelerators.', + url: `${SITE_URL}/about`, + }, + twitter: { + title: 'About | InferenceX', + description: + 'InferenceX is an independent, vendor neutral, reproducible benchmark which continuously benchmarks inference software across a wide range of AI accelerators.', + }, +}; + +export default function AboutPage() { + return ( +
+ +
+
+ +

+ Open Source Continuous Inference Benchmark trusted by Operators of Trillion Dollar + GigaWatt Scale Token Factories +

+

+ As the world progresses exponentially towards AGI, software development and model + releases move at the speed of light. Existing benchmarks rapidly become obsolete due + to their static nature, and participants often submit software images purpose-built + for the benchmark itself which do not reflect real world performance. +

+

+ InferenceX™ (formerly InferenceMAX) is our independent, vendor + neutral, reproducible benchmark which addresses these issues by continuously + benchmarking inference software across a wide range of AI accelerators that are + actually available to the ML community. +

+

+ Our open data & insights are widely adopted by the ML community, capacity planning + strategy teams at trillion dollar token factories & AI Labs & at multiple billion + dollar NeoClouds. Learn more in our articles:{' '} + + InferenceX v1 + + ,{' '} + + InferenceX v2 + + . +

+
+
+ +
+ +

Frequently Asked Questions

+
+ {FAQ_ITEMS.map((item) => ( +
+
{item.question}
+
+ {item.answer && ( +

+ {item.answer} + {item.link && ( + <> + {' '} + + {item.link.text} + + + )} +

+ )} + {item.list && ( +
    + {item.list.map((li) => ( +
  • {li}
  • + ))} +
+ )} +
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/packages/app/src/app/api/v1/github-stars/route.test.ts b/packages/app/src/app/api/v1/github-stars/route.test.ts deleted file mode 100644 index 7abcfbf..0000000 --- a/packages/app/src/app/api/v1/github-stars/route.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; - -vi.mock('@semianalysisai/inferencex-constants', () => ({ - GITHUB_OWNER: 'TestOwner', - GITHUB_REPO: 'TestRepo', -})); - -import { GET } from './route'; - -const originalFetch = globalThis.fetch; -let origToken: string | undefined; - -beforeEach(() => { - vi.clearAllMocks(); - origToken = process.env.GITHUB_TOKEN; -}); - -afterEach(() => { - globalThis.fetch = originalFetch; - if (origToken === undefined) { - delete process.env.GITHUB_TOKEN; - } else { - process.env.GITHUB_TOKEN = origToken; - } -}); - -describe('GET /api/v1/github-stars', () => { - it('returns star count on success', async () => { - globalThis.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - json: async () => ({ stargazers_count: 42000 }), - }); - - const res = await GET(); - expect(res.status).toBe(200); - const body = await res.json(); - expect(body).toEqual({ - owner: 'TestOwner', - repo: 'TestRepo', - stars: 42000, - }); - expect(globalThis.fetch).toHaveBeenCalledWith( - 'https://api.github.com/repos/TestOwner/TestRepo', - expect.objectContaining({ - headers: expect.objectContaining({ - Accept: 'application/vnd.github.v3+json', - }), - }), - ); - }); - - it('returns 502 when GitHub API errors', async () => { - globalThis.fetch = vi.fn().mockResolvedValueOnce({ - ok: false, - status: 403, - }); - - const res = await GET(); - expect(res.status).toBe(502); - const body = await res.json(); - expect(body.error).toBe('GitHub API error: 403'); - }); - - it('returns 500 when fetch throws', async () => { - globalThis.fetch = vi.fn().mockRejectedValueOnce(new Error('Network error')); - - const res = await GET(); - expect(res.status).toBe(500); - const body = await res.json(); - expect(body.error).toBe('Internal server error'); - }); -}); diff --git a/packages/app/src/app/api/v1/github-stars/route.ts b/packages/app/src/app/api/v1/github-stars/route.ts deleted file mode 100644 index 3ca000c..0000000 --- a/packages/app/src/app/api/v1/github-stars/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextResponse } from 'next/server'; - -import { GITHUB_OWNER, GITHUB_REPO } from '@semianalysisai/inferencex-constants'; - -export async function GET() { - try { - const res = await fetch(`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`, { - headers: { - Accept: 'application/vnd.github.v3+json', - ...(process.env.GITHUB_TOKEN && { Authorization: `token ${process.env.GITHUB_TOKEN}` }), - }, - next: { revalidate: 60 * 60 }, // 1 hour - }); - - if (!res.ok) { - return NextResponse.json({ error: `GitHub API error: ${res.status}` }, { status: 502 }); - } - - const data = await res.json(); - return NextResponse.json( - { owner: GITHUB_OWNER, repo: GITHUB_REPO, stars: data.stargazers_count }, - { - headers: { - 'Cache-Control': 'public, max-age=0, s-maxage=3600, stale-while-revalidate=7200', - }, - }, - ); - } catch (error) { - console.error('Error fetching GitHub stars:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} diff --git a/packages/app/src/app/blog/[slug]/page.tsx b/packages/app/src/app/blog/[slug]/page.tsx index 4de8d8d..5d24b61 100644 --- a/packages/app/src/app/blog/[slug]/page.tsx +++ b/packages/app/src/app/blog/[slug]/page.tsx @@ -138,8 +138,8 @@ export default async function BlogPostPage({ params }: Props) { -
-
+
+
@@ -174,19 +174,23 @@ export default async function BlogPostPage({ params }: Props) {
- - {headings.length > 0 && } - -
- {content} -

- All articles and posts are © SemiAnalysis. All rights reserved. The AGPL-3.0 - license covering the application source code does not apply to article content. -

-
+ {headings.length > 0 && ( +
+ +
+ )} +
+
+ {content} +

+ All articles and posts are © SemiAnalysis. All rights reserved. The AGPL-3.0 + license covering the application source code does not apply to article content. +

+
+
-
-
+
+

Articles

@@ -66,50 +66,50 @@ export default async function BlogPage({ ))}

)} - - - {filtered.length === 0 ? ( -

- {activeTag ? `No articles tagged "${activeTag}".` : 'Coming soon.'} -

- ) : ( -
- {filtered.map((post) => ( - -
-
- - · - {post.readingTime} min read -
-

- {post.title} -

-

{post.subtitle}

- {post.tags && post.tags.length > 0 && ( -
- {post.tags.map((tag) => ( - - {tag} - - ))} +
+ {filtered.length === 0 ? ( +

+ {activeTag ? `No articles tagged "${activeTag}".` : 'Coming soon.'} +

+ ) : ( +
+ {filtered.map((post) => ( + +
+
+ + · + {post.readingTime} min read
- )} -
-
- ))} -
- )} +

+ {post.title} +

+

{post.subtitle}

+ {post.tags && post.tags.length > 0 && ( +
+ {post.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+
+ ))} +
+ )} +
diff --git a/packages/app/src/app/globals.css b/packages/app/src/app/globals.css index 26fabd8..27c29fe 100644 --- a/packages/app/src/app/globals.css +++ b/packages/app/src/app/globals.css @@ -177,64 +177,6 @@ --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.556 0 0); - - /* GPU Colors - NVIDIA (light → dark within each group, fixed hue+chroma per group) */ - --h100: oklch(82% 0.19 140); - --h100-dynamo-trt: oklch(78% 0.19 140); - --h100-dynamo-trt-mtp: oklch(74% 0.19 140); - --h100-dynamo-sglang-mtp: oklch(72% 0.19 140); - --h100-vllm: oklch(70% 0.19 140); - --h200: oklch(76% 0.18 142); - --h200-trt: oklch(73% 0.18 142); - --h200-trt-mtp: oklch(70% 0.18 142); - --h200-dynamo-trt: oklch(67% 0.18 142); - --h200-dynamo-trt-mtp: oklch(64% 0.18 142); - --h200-sglang: oklch(61% 0.18 142); - --h200-dynamo-sglang: oklch(58% 0.18 142); - --h200-dynamo-sglang-mtp: oklch(55% 0.18 142); - --h200-vllm: oklch(52% 0.18 142); - --b200: oklch(60% 0.15 155); - --b200-trt: oklch(57% 0.15 155); - --b200-trt-mtp: oklch(54% 0.15 155); - --b200-dynamo-trt: oklch(51% 0.15 155); - --b200-dynamo-trt-mtp: oklch(48% 0.15 155); - --b200-sglang: oklch(45% 0.15 155); - --b200-sglang-mtp: oklch(42% 0.15 155); - --b200-dynamo-sglang: oklch(39% 0.15 155); - --b200-dynamo-sglang-mtp: oklch(36% 0.15 155); - --b200-vllm: oklch(33% 0.15 155); - --b300: oklch(50% 0.14 150); - --b300-dynamo-trt: oklch(46% 0.14 150); - --b300-dynamo-trt-mtp: oklch(42% 0.14 150); - --gb200: oklch(65% 0.16 132); - --gb200-mtp: oklch(62% 0.16 132); - --gb200-dynamo-trt: oklch(59% 0.16 132); - --gb200-dynamo-trt-mtp: oklch(56% 0.16 132); - --gb200-dynamo-trtllm: oklch(53% 0.16 132); - --gb200-dynamo-trtllm-mtp: oklch(50% 0.16 132); - --gb200-dynamo-sglang: oklch(47% 0.16 132); - --gb300: oklch(43% 0.12 148); - --gb300-dynamo-trt: oklch(40% 0.12 148); - --gb300-dynamo-trt-mtp: oklch(37% 0.12 148); - --gb300-dynamo-trtllm: oklch(34% 0.12 148); - --gb300-dynamo-trtllm-mtp: oklch(31% 0.12 148); - --gb300-dynamo-sglang: oklch(28% 0.12 148); - - /* GPU Colors - AMD (light → dark within each group, fixed hue+chroma per group) */ - --mi300x: oklch(72% 0.23 25); - --mi300x-sglang: oklch(68% 0.23 25); - --mi300x-vllm: oklch(64% 0.23 25); - --mi325x: oklch(60% 0.21 32); - --mi325x-sglang: oklch(56% 0.21 32); - --mi325x-vllm: oklch(52% 0.21 32); - --mi355x: oklch(55% 0.17 35); - --mi355x-mori-sglang: oklch(51% 0.17 35); - --mi355x-mori-sglang-mtp: oklch(47% 0.17 35); - --mi355x-sglang: oklch(43% 0.17 35); - --mi355x-sglang-mtp: oklch(41% 0.17 35); - --mi355x-vllm: oklch(39% 0.17 35); - --mi355x-atom: oklch(35% 0.17 35); - --mi355x-atom-mtp: oklch(31% 0.17 35); } .dark { diff --git a/packages/app/src/app/layout.tsx b/packages/app/src/app/layout.tsx index 0d8023d..d7ae720 100644 --- a/packages/app/src/app/layout.tsx +++ b/packages/app/src/app/layout.tsx @@ -7,7 +7,7 @@ import { DM_Sans } from 'next/font/google'; import { Footer } from '@/components/footer/footer'; import { Header } from '@/components/header/header'; -import { cn } from '@/lib/utils'; +import { CircuitBackground } from '@/components/circuit-background'; import { ThemeProvider } from '@/components/ui/theme-provider'; import { AUTHOR_HANDLE, @@ -19,6 +19,7 @@ import { SITE_TITLE, SITE_URL, } from '@semianalysisai/inferencex-constants'; +import { fetchStarCount } from '@/lib/github-stars.server'; import { QueryProvider } from '@/providers/query-provider'; import { PostHogProvider, PostHogPageView } from '@/providers/posthog-provider'; @@ -152,11 +153,12 @@ const jsonLd = { ], }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const starCount = await fetchStarCount(); return ( @@ -164,16 +166,7 @@ export default function RootLayout({ -