+
No matched cost data available.
)}
@@ -371,8 +502,8 @@ export async function GET(
style={{
display: 'flex',
justifyContent: 'space-between',
- paddingTop: 9,
- fontSize: 15,
+ paddingTop: 9 * R,
+ fontSize: 15 * R,
color: COLORS.muted,
}}
>
diff --git a/packages/app/src/app/compare/[slug]/page.tsx b/packages/app/src/app/compare/[slug]/page.tsx
index 5158d6c9..d6c660ec 100644
--- a/packages/app/src/app/compare/[slug]/page.tsx
+++ b/packages/app/src/app/compare/[slug]/page.tsx
@@ -13,9 +13,11 @@ import {
} from '@/lib/compare-slug';
import { getAllComparableCompareSlugs } from '@/lib/compare-availability';
import {
+ buildBreadcrumbJsonLd,
buildJsonLd,
compareTableNarrative,
computeCompareTableData,
+ dateRangeForPair,
getCachedBenchmarks,
KNOWN_MODELS,
KNOWN_PRECISIONS,
@@ -134,6 +136,7 @@ export default async function ComparePage({ params, searchParams }: Props) {
);
const url = `${SITE_URL}/compare/${canonical}`;
+ const { oldest, newest } = dateRangeForPair(rows, parsed.a, parsed.b);
const jsonLd = buildJsonLd(
'full',
parsed.model,
@@ -143,6 +146,15 @@ export default async function ComparePage({ params, searchParams }: Props) {
summaryA,
summaryB,
ssrRows,
+ undefined,
+ oldest,
+ newest,
+ parsed.model.displayName,
+ );
+ const breadcrumbJsonLd = buildBreadcrumbJsonLd(
+ 'full',
+ compareModelDisplayLabel(parsed.model, parsed.a, parsed.b),
+ url,
);
const label = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b);
const aMeta = HW_REGISTRY[parsed.a];
@@ -161,6 +173,7 @@ export default async function ComparePage({ params, searchParams }: Props) {
return (
<>
+
= {}): BenchmarkRow {
+ return {
+ hardware: 'h200',
+ framework: 'sglang',
+ model: 'dsr1',
+ precision: 'fp8',
+ spec_method: 'none',
+ disagg: false,
+ is_multinode: false,
+ prefill_tp: 8,
+ prefill_ep: 1,
+ prefill_dp_attention: false,
+ prefill_num_workers: 0,
+ decode_tp: 8,
+ decode_ep: 1,
+ decode_dp_attention: false,
+ decode_num_workers: 0,
+ num_prefill_gpu: 8,
+ num_decode_gpu: 8,
+ isl: 1024,
+ osl: 1024,
+ conc: 128,
+ image: null,
+ metrics: { tput_per_gpu: 100, median_intvty: 30 },
+ date: '2026-03-01',
+ run_url: null,
+ ...overrides,
+ };
+}
+
+function pairRows(): BenchmarkRow[] {
+ return [
+ stubRow({ hardware: 'h200', conc: 16, metrics: { tput_per_gpu: 800, median_intvty: 10 } }),
+ stubRow({ hardware: 'h200', conc: 32, metrics: { tput_per_gpu: 600, median_intvty: 20 } }),
+ stubRow({ hardware: 'h200', conc: 64, metrics: { tput_per_gpu: 400, median_intvty: 30 } }),
+ stubRow({ hardware: 'h200', conc: 128, metrics: { tput_per_gpu: 200, median_intvty: 40 } }),
+ stubRow({ hardware: 'b200', conc: 16, metrics: { tput_per_gpu: 900, median_intvty: 10 } }),
+ stubRow({ hardware: 'b200', conc: 32, metrics: { tput_per_gpu: 700, median_intvty: 20 } }),
+ stubRow({ hardware: 'b200', conc: 64, metrics: { tput_per_gpu: 500, median_intvty: 30 } }),
+ stubRow({ hardware: 'b200', conc: 128, metrics: { tput_per_gpu: 250, median_intvty: 40 } }),
+ ];
+}
+
+describe('computeCompareImageRows', () => {
+ const range = { min: 10, max: 40 };
+
+ it('returns 17 evenly-spaced samples when no includeTargets are passed', () => {
+ const rows = computeCompareImageRows(pairRows(), 'h200', 'b200', '1k/1k', 'fp8', range);
+ expect(rows.length).toBe(17);
+ expect(rows.at(0)?.target).toBe(10);
+ expect(rows.at(-1)?.target).toBe(40);
+ });
+
+ it('inserts includeTargets as exact samples without dropping the even grid', () => {
+ const rows = computeCompareImageRows(
+ pairRows(),
+ 'h200',
+ 'b200',
+ '1k/1k',
+ 'fp8',
+ range,
+ [17, 25, 33],
+ );
+ const targets = rows.map((r) => r.target);
+ expect(targets).toContain(17);
+ expect(targets).toContain(25);
+ expect(targets).toContain(33);
+ // Endpoints from the even grid still present.
+ expect(targets).toContain(10);
+ expect(targets).toContain(40);
+ // Strictly increasing — required so curve-partition by target works.
+ for (let i = 1; i < targets.length; i++) {
+ expect(targets[i]).toBeGreaterThan(targets[i - 1]);
+ }
+ });
+
+ it('drops includeTargets that fall outside the interactivity range', () => {
+ const rows = computeCompareImageRows(
+ pairRows(),
+ 'h200',
+ 'b200',
+ '1k/1k',
+ 'fp8',
+ range,
+ [-5, 9, 41, 1000],
+ );
+ const targets = rows.map((r) => r.target);
+ expect(targets).not.toContain(-5);
+ expect(targets).not.toContain(9);
+ expect(targets).not.toContain(41);
+ expect(targets).not.toContain(1000);
+ // The even grid is unaffected when every includeTarget is rejected.
+ expect(rows.length).toBe(17);
+ });
+
+ it('dedupes includeTargets that already coincide with an even-grid sample', () => {
+ const rows = computeCompareImageRows(
+ pairRows(),
+ 'h200',
+ 'b200',
+ '1k/1k',
+ 'fp8',
+ range,
+ [10, 40],
+ );
+ expect(rows.length).toBe(17);
+ expect(rows.filter((r) => r.target === 10).length).toBe(1);
+ expect(rows.filter((r) => r.target === 40).length).toBe(1);
+ });
+
+ it('returns an empty array when the interactivity range is degenerate', () => {
+ expect(
+ computeCompareImageRows(
+ pairRows(),
+ 'h200',
+ 'b200',
+ '1k/1k',
+ 'fp8',
+ { min: 20, max: 20 },
+ [20],
+ ),
+ ).toEqual([]);
+ });
+});
diff --git a/packages/app/src/lib/compare-ssr.ts b/packages/app/src/lib/compare-ssr.ts
index 829a2985..e6e87dd7 100644
--- a/packages/app/src/lib/compare-ssr.ts
+++ b/packages/app/src/lib/compare-ssr.ts
@@ -10,7 +10,13 @@
* and JSON-LD shape — with a `variant` knob that swaps the headline
* framing between the latency+throughput view and the per-dollar view.
*/
-import { HW_REGISTRY, sequenceToIslOsl } from '@semianalysisai/inferencex-constants';
+import {
+ AUTHOR_NAME,
+ AUTHOR_URL,
+ HW_REGISTRY,
+ SITE_URL,
+ sequenceToIslOsl,
+} from '@semianalysisai/inferencex-constants';
import { FIXTURES_MODE, JSON_MODE, getDb } from '@semianalysisai/inferencex-db/connection';
import * as jsonProvider from '@semianalysisai/inferencex-db/json-provider';
import {
@@ -75,7 +81,7 @@ export function pickString(value: string | string[] | undefined): string | undef
}
// ---------------------------------------------------------------------------
-// Pair summary (JSON-LD Product additionalProperty)
+// Pair summary (JSON-LD additionalProperty)
// ---------------------------------------------------------------------------
export interface PairSummary {
@@ -260,7 +266,12 @@ export function computeCompareTableData(
/** Sample the same interpolated cost curve used for the comparison table for
* server-rendered image assets. More samples make the static PNG read like the
- * interactive roofline without requiring browser-based chart capture. */
+ * interactive roofline without requiring browser-based chart capture.
+ *
+ * `includeTargets` are merged into the even sampling grid so callers can
+ * guarantee the curve has exact samples at specific targets (e.g. the plotted
+ * comparison dots), making it safe to partition the curve into solid /
+ * dashed segments without interpolation gaps at the boundary. */
export function computeCompareImageRows(
rows: BenchmarkRow[],
a: string,
@@ -268,6 +279,7 @@ export function computeCompareImageRows(
sequence: string | null,
precision: string | null,
interactivityRange: { min: number; max: number },
+ includeTargets: number[] = [],
): SsrInterpolatedRow[] {
if (!sequence || !precision || interactivityRange.max <= interactivityRange.min) return [];
@@ -280,20 +292,26 @@ export function computeCompareImageRows(
const sampleCount = 17;
const span = interactivityRange.max - interactivityRange.min;
- return Array.from({ length: sampleCount }, (_, index) => {
- const target = interactivityRange.min + (span * index) / (sampleCount - 1);
- return {
- target,
- a:
- pointsA.length > 0
- ? interpolateForGPU(pointsA, target, 'interactivity_to_throughput', 'costh')
- : null,
- b:
- pointsB.length > 0
- ? interpolateForGPU(pointsB, target, 'interactivity_to_throughput', 'costh')
- : null,
- };
- });
+ const evenTargets = Array.from(
+ { length: sampleCount },
+ (_, index) => interactivityRange.min + (span * index) / (sampleCount - 1),
+ );
+ const clamped = includeTargets.filter(
+ (t) => t >= interactivityRange.min && t <= interactivityRange.max,
+ );
+ const targets = [...new Set([...evenTargets, ...clamped])].toSorted((x, y) => x - y);
+
+ return targets.map((target) => ({
+ target,
+ a:
+ pointsA.length > 0
+ ? interpolateForGPU(pointsA, target, 'interactivity_to_throughput', 'costh')
+ : null,
+ b:
+ pointsB.length > 0
+ ? interpolateForGPU(pointsB, target, 'interactivity_to_throughput', 'costh')
+ : null,
+ }));
}
// ---------------------------------------------------------------------------
@@ -303,7 +321,7 @@ export function computeCompareImageRows(
function jsonLdEntryFor(key: string, summary: PairSummary, position: number) {
const meta = HW_REGISTRY[key];
const label = meta?.label ?? key.toUpperCase();
- const props: { name: string; value: string | number }[] = [];
+ const props: { name: string; value: string | number }[] = [{ name: 'Category', value: 'GPU' }];
if (meta) {
props.push({ name: 'Vendor', value: meta.vendor });
props.push({ name: 'Architecture', value: meta.arch });
@@ -332,10 +350,8 @@ function jsonLdEntryFor(key: string, summary: PairSummary, position: number) {
'@type': 'ListItem',
position,
item: {
- '@type': 'Product',
+ '@type': 'Thing',
name: label,
- brand: { '@type': 'Brand', name: meta?.vendor ?? 'Unknown' },
- category: 'GPU',
...(props.length > 0 && {
additionalProperty: props.map((p) => ({
'@type': 'PropertyValue',
@@ -758,6 +774,46 @@ export function bucketComparePairsByVendor(modelSlug: string, pairs: ComparePair
return { cross, nvidia, amd };
}
+/** Breadcrumb trail for a compare slug page. Emitted alongside the main
+ * Dataset/ItemList JSON-LD so Google can render the Home → Compare → A vs B
+ * trail in search results. Variant chooses /compare vs /compare-per-dollar. */
+export function buildBreadcrumbJsonLd(
+ variant: CompareJsonLdVariant,
+ pairLabel: string,
+ url: string,
+) {
+ const indexUrl =
+ variant === 'per-dollar' ? `${SITE_URL}/compare-per-dollar` : `${SITE_URL}/compare`;
+ const indexName = variant === 'per-dollar' ? 'GPU Performance per Dollar' : 'GPU Comparisons';
+ return {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ itemListElement: [
+ { '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL },
+ { '@type': 'ListItem', position: 2, name: indexName, item: indexUrl },
+ { '@type': 'ListItem', position: 3, name: pairLabel, item: url },
+ ],
+ };
+}
+
+/** Pick the oldest and newest benchmark dates among rows whose hardware matches
+ * the compared pair — used to populate Dataset.datePublished / dateModified. */
+export function dateRangeForPair(
+ rows: BenchmarkRow[],
+ a: string,
+ b: string,
+): { oldest?: string; newest?: string } {
+ let oldest: string | undefined;
+ let newest: string | undefined;
+ for (const row of rows) {
+ if (row.hardware !== a && row.hardware !== b) continue;
+ if (!row.date) continue;
+ if (oldest === undefined || row.date < oldest) oldest = row.date;
+ if (newest === undefined || row.date > newest) newest = row.date;
+ }
+ return { oldest, newest };
+}
+
export function buildJsonLd(
variant: CompareJsonLdVariant,
model: CompareModelSlug,
@@ -768,6 +824,13 @@ export function buildJsonLd(
summaryB: PairSummary,
ssrRows: SsrInterpolatedRow[],
imageUrl?: string,
+ /** ISO date of oldest benchmark row contributing to this dataset. */
+ datePublished?: string,
+ /** ISO date of newest benchmark row — drives Google Dataset Search freshness. */
+ dateModified?: string,
+ /** Display model name accepted by /api/v1/benchmarks?model=…, used to wire the
+ * Dataset's `distribution: DataDownload` to a real machine-readable export. */
+ modelApiKey?: string,
) {
const aLabel = HW_REGISTRY[a]?.label ?? a.toUpperCase();
const bLabel = HW_REGISTRY[b]?.label ?? b.toUpperCase();
@@ -814,7 +877,7 @@ export function buildJsonLd(
);
}
return {
- '@type': 'Observation',
+ '@type': 'Dataset',
name: `${model.label} comparison at ${row.target} tok/s/user interactivity`,
variableMeasured: metrics.map((m) => ({
'@type': 'PropertyValue',
@@ -844,6 +907,40 @@ export function buildJsonLd(
name: datasetName,
description: datasetDescription,
url,
+ license: 'https://www.apache.org/licenses/LICENSE-2.0',
+ isAccessibleForFree: true,
+ measurementTechnique:
+ 'Open-source automated GPU CI/CD inference benchmark (github.com/SemiAnalysisAI/InferenceX)',
+ keywords: [
+ ...new Set(
+ [
+ 'AI inference benchmark',
+ 'GPU comparison',
+ variant === 'per-dollar' ? 'cost per million tokens' : 'inference latency',
+ variant === 'per-dollar' ? 'performance per dollar' : 'tokens per second',
+ model.label,
+ aLabel,
+ bLabel,
+ HW_REGISTRY[a]?.vendor,
+ HW_REGISTRY[b]?.vendor,
+ ].filter(Boolean),
+ ),
+ ].join(', '),
+ ...(datePublished && { datePublished }),
+ ...(dateModified && { dateModified }),
+ creator: {
+ '@type': 'Organization',
+ name: AUTHOR_NAME,
+ url: AUTHOR_URL,
+ },
+ ...(modelApiKey && {
+ distribution: {
+ '@type': 'DataDownload',
+ encodingFormat: 'application/json',
+ contentUrl: `${SITE_URL}/api/v1/benchmarks?model=${encodeURIComponent(modelApiKey)}`,
+ name: `${model.label} latest benchmark rows (JSON)`,
+ },
+ }),
...(imageUrl && {
image: {
'@type': 'ImageObject',