Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const getCachedDerivedAgenticMetrics = cachedQuery(
* - p75/p90_e2e_norm_intvty: slow-tail OSL / E2E-latency in tok/s/user,
* computed as 1 / pXX(per-request E2EL/OSL) across every turn in every
* session — plain interactivity charged for the prefill wait.
* - request_length_moments: exact joint (ISL, OSL) moment sums over profiling
* requests, used client-side for closed-form attention-FLOP pricing.
*
* Ids without a trace_replay blob or with unparseable records are omitted.
*/
Expand Down
32 changes: 30 additions & 2 deletions packages/app/src/components/inference/hooks/useChartData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import {
resolveComparisonEntries,
} from '@/components/inference/utils/comparisonEntry';
import { useBenchmarks, benchmarkQueryOptions } from '@/hooks/api/use-benchmarks';
import { useDerivedAgenticMetrics } from '@/hooks/api/use-derived-agentic-metrics';
import type { BenchmarkRow } from '@/lib/api';
import type { RequestLengthMoments } from '@/lib/attention-flops';
import { isPersistedBenchmarkId } from '@/lib/benchmark-id';
import {
GPU_ALIAS_TO_CANONICAL,
getModelSortIndex,
Expand Down Expand Up @@ -384,15 +387,40 @@ export function useChartData(
overviewHistoryPair?.baselineConfigKey,
]);

// Request-length moment sums for agentic points — the attention-FLOPs input
// of the TFLOP/s-per-chip y-metric. Shares the 'derived-agentic-metrics'
// react-query cache with ChartDisplay's derived-x fetch, so switching between
// the two costs one request. The chart renders without them first; the
// TFLOP/s metric fills in when they arrive.
const momentTargetIds = useMemo(() => {
const ids = new Set<number>();
for (const r of rows) {
const numericId = typeof r.id === 'number' ? r.id : Number(r.id);
if (r.benchmark_type === 'agentic_traces' && isPersistedBenchmarkId(numericId)) {
ids.add(numericId);
}
}
return [...ids].sort((a, b) => a - b);
}, [rows]);
const derivedAgenticQuery = useDerivedAgenticMetrics(momentTargetIds, momentTargetIds.length > 0);
const requestLengthMomentsById = useMemo(() => {
if (!derivedAgenticQuery.data) return undefined;
const map: Record<number, RequestLengthMoments | null> = {};
for (const metric of Object.values(derivedAgenticQuery.data)) {
map[metric.id] = metric.request_length_moments;
}
return map;
}, [derivedAgenticQuery.data]);

// Transform filtered rows into chart data
const { chartData, hardwareConfig: rawHardwareConfig } = useMemo(() => {
if (rows.length === 0)
return {
chartData: [] as InferenceData[][],
hardwareConfig: {} as HardwareConfig,
};
return transformBenchmarkRows(rows, selectedPercentile);
}, [rows, selectedPercentile]);
return transformBenchmarkRows(rows, selectedPercentile, requestLengthMomentsById);
}, [rows, selectedPercentile, requestLengthMomentsById]);

// Sort hardware config — stabilize reference when keys haven't changed.
// Different sequences for the same model often have the same GPU configs,
Expand Down
45 changes: 45 additions & 0 deletions packages/app/src/components/inference/metric-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,48 @@ export const METRIC_REGISTRY = {
titleZh: '每芯片输出 token 吞吐量',
polarity: 'higher',
},
// New-input-suffix throughput pair: total / input throughput minus the
// infinite-cache theoretical prefix share (trace-derived, agentic only).
// Deliberately based on the theoretical rate rather than server-observed
// cache hits so systems with good cache storage aren't penalized — like
// MFU vs HFU, actual (uncached + output) throughput is always at least the
// theoretical (uncachable suffix + output) throughput.
newInputSuffixOutputTputPerGpu: {
field: 'newInputSuffixOutputTputPerGpu.y',
label: 'New Input Suffix + Output Token Throughput per Chip (tok/s/chip)',
labelZh: '每芯片新输入 suffix + 输出 token 吞吐量(tok/s/chip)',
title: 'New Input Suffix + Output Token Throughput per Chip',
titleZh: '每芯片新输入 suffix + 输出 token 吞吐量',
polarity: 'higher',
},
newInputSuffixTputPerGpu: {
field: 'newInputSuffixTputPerGpu.y',
label: 'New Input Suffix Token Throughput per Chip (tok/s/chip)',
labelZh: '每芯片新输入 suffix token 吞吐量(tok/s/chip)',
title: 'New Input Suffix Token Throughput per Chip',
titleZh: '每芯片新输入 suffix token 吞吐量',
polarity: 'higher',
x: 'p90_ttft',
xLabel: 'P90 Time To First Token (s)',
heading: 'vs. P90 Time To First Token',
},
// Achieved model TFLOP/s on the theoretically necessary tokens only:
// (2 × active params + per-model attention FLOPs per computed token) times
// the new-input-suffix + output token throughput above. The attention term
// integrates each architecture's mechanism (MLA, CSA/HCA + indexers, MSA,
// KDA + gated MLA, sliding/full GQA — specs in model-architectures.ts,
// math in attention-flops.ts) over the run's exact per-request (ISL, OSL)
// sums at the theoretical cache hit rate, so different request-length
// distributions are priced individually and summed. Points without stored
// request-length moments or an attention spec omit the metric.
newInputSuffixOutputTflopsPerGpu: {
field: 'newInputSuffixOutputTflopsPerGpu.y',
label: 'New Input Suffix + Output TFLOP/s per Chip (TFLOP/s/chip)',
labelZh: '每芯片新输入 suffix + 输出 TFLOP/s(TFLOP/s/chip)',
title: 'New Input Suffix + Output TFLOP/s per Chip',
titleZh: '每芯片新输入 suffix + 输出 TFLOP/s',
polarity: 'higher',
},
tpPerMw: {
field: 'tpPerMw.y',
label: 'Token Throughput per All in Utility MW (tok/s/MW)',
Expand Down Expand Up @@ -473,6 +515,9 @@ export const METRIC_CONTROL_GROUPS: readonly MetricControlGroup[] = [
'y_tpPerGpu',
'y_inputTputPerGpu',
'y_outputTputPerGpu',
'y_newInputSuffixOutputTputPerGpu',
'y_newInputSuffixTputPerGpu',
'y_newInputSuffixOutputTflopsPerGpu',
'y_tpPerMw',
'y_inputTputPerMw',
'y_outputTputPerMw',
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/components/inference/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type React from 'react';
import type { WorkerPower } from '@semianalysisai/inferencex-db/queries/benchmarks';

import type { RequestLengthMoments } from '@/lib/attention-flops';
import type { HardwareEntry } from '@/lib/constants';
import type { Model, Sequence } from '@/lib/data-mappings';
import type { MetricKey } from './metric-registry';
Expand Down Expand Up @@ -219,6 +220,14 @@ export interface AggDataEntry {
total_prompt_tokens?: number;
/** Total generated (output) tokens. */
total_generation_tokens?: number;
/**
* Exact joint (ISL, OSL) sums over the run's request population, fetched
* from the derived-agentic-metrics endpoint and merged in by
* `transformBenchmarkRows`. Feeds the attention-FLOPs term of the
* TFLOP/s-per-chip y-metric; absent on fixed-seq points, unofficial-run
* overlays, and rows whose stats haven't been fetched yet.
*/
request_length_moments?: RequestLengthMoments | null;
}

/**
Expand Down Expand Up @@ -274,6 +283,12 @@ export interface InferenceData extends Partial<Omit<AggDataEntry, AggDataConflic
tpPerGpu: { y: number; roof: boolean };
outputTputPerGpu?: { y: number; roof: boolean };
inputTputPerGpu?: { y: number; roof: boolean };
// New-input-suffix throughput (total / input minus the infinite-cache
// theoretical prefix share). Agentic-trace points only.
newInputSuffixOutputTputPerGpu?: { y: number; roof: boolean };
newInputSuffixTputPerGpu?: { y: number; roof: boolean };
/** Achieved model TFLOP/s per chip: 2 × active params × suffix+output tok/s. */
newInputSuffixOutputTflopsPerGpu?: { y: number; roof: boolean };
tpPerMw: { y: number; roof: boolean };
inputTputPerMw?: { y: number; roof: boolean };
outputTputPerMw?: { y: number; roof: boolean };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ describe('canonicalNormalizedFrontierIds', () => {
it('computes the true normalized-interactivity frontier', () => {
const points = [point(1, 100), point(2, 150), point(3, 120)];
const metrics = {
1: { id: 1, p75_e2e_norm_intvty: 10, p90_e2e_norm_intvty: 10 },
2: { id: 2, p75_e2e_norm_intvty: 20, p90_e2e_norm_intvty: 20 },
3: { id: 3, p75_e2e_norm_intvty: 30, p90_e2e_norm_intvty: 30 },
1: { id: 1, p75_e2e_norm_intvty: 10, p90_e2e_norm_intvty: 10, request_length_moments: null },
2: { id: 2, p75_e2e_norm_intvty: 20, p90_e2e_norm_intvty: 20, request_length_moments: null },
3: { id: 3, p75_e2e_norm_intvty: 30, p90_e2e_norm_intvty: 30, request_length_moments: null },
};

expect(
Expand All @@ -38,8 +38,8 @@ describe('canonicalNormalizedFrontierIds', () => {
it('keeps frontiers independent across dates', () => {
const points = [point(1, 500, { date: '2026-08-01' }), point(2, 100, { date: '2026-08-02' })];
const metrics = {
1: { id: 1, p75_e2e_norm_intvty: 50, p90_e2e_norm_intvty: 50 },
2: { id: 2, p75_e2e_norm_intvty: 10, p90_e2e_norm_intvty: 10 },
1: { id: 1, p75_e2e_norm_intvty: 50, p90_e2e_norm_intvty: 50, request_length_moments: null },
2: { id: 2, p75_e2e_norm_intvty: 10, p90_e2e_norm_intvty: 10, request_length_moments: null },
};
expect(
[...canonicalNormalizedFrontierIds(points, metrics, 'p90', 'upper_left')!].toSorted(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
generateTooltipContent,
generateOverlayTooltipContent,
generateGPUGraphTooltipContent,
theoreticalPrefixTokens,
uncachedInputTokens,
type TooltipConfig,
type OverlayTooltipConfig,
} from '@/components/inference/utils/tooltipUtils';
Expand Down Expand Up @@ -332,6 +334,98 @@ describe('generateTooltipContent', () => {
expect(html).toContain('FP8');
});

it('shows theoretical prefix and uncached input token rows for agentic points', () => {
const html = generateTooltipContent(
tooltipConfig({
data: pt({
benchmark_type: 'agentic_traces',
total_prompt_tokens: 1_000_000,
theoretical_cache_hit_rate: 0.92,
}),
}),
);
expect(html).toContain('<strong>Theoretical Prefix Tokens:</strong> 920,000');
expect(html).toContain('<strong>Input Tokens w/o Prefix Caching:</strong> 80,000');
});

it('omits the prefix token rows when the theoretical hit rate is missing', () => {
const html = generateTooltipContent(
tooltipConfig({
data: pt({
benchmark_type: 'agentic_traces',
total_prompt_tokens: 1_000_000,
theoretical_cache_hit_rate: undefined,
}),
}),
);
expect(html).toContain('<strong>Prompt Tokens:</strong> 1,000,000');
expect(html).not.toContain('Theoretical Prefix Tokens');
expect(html).not.toContain('Input Tokens w/o Prefix Caching');
});

it('never shows prefix token rows on fixed-sequence points', () => {
const html = generateTooltipContent(
tooltipConfig({
data: pt({
benchmark_type: 'single_turn',
total_prompt_tokens: 1_000_000,
theoretical_cache_hit_rate: 0.92,
}),
}),
);
expect(html).not.toContain('Theoretical Prefix Tokens');
expect(html).not.toContain('Input Tokens w/o Prefix Caching');
});

it('uses Chinese labels for the prefix token rows on /zh surfaces', () => {
const html = generateTooltipContent(
tooltipConfig({
locale: 'zh',
data: pt({
benchmark_type: 'agentic_traces',
total_prompt_tokens: 500_000,
theoretical_cache_hit_rate: 0.9,
}),
}),
);
expect(html).toContain('<strong>理论 prefix token 数:</strong> 450,000');
expect(html).toContain('<strong>无 prefix cache 的输入 token 数:</strong> 50,000');
});
});

describe('theoreticalPrefixTokens / uncachedInputTokens', () => {
it('recovers the prefix token sum from the trace hit rate', () => {
const d = pt({ total_prompt_tokens: 1_000_000, theoretical_cache_hit_rate: 0.925 });
expect(theoreticalPrefixTokens(d)).toBe(925_000);
expect(uncachedInputTokens(d)).toBe(75_000);
});

it('returns undefined when either input is missing', () => {
expect(theoreticalPrefixTokens(pt({ total_prompt_tokens: 100 }))).toBeUndefined();
expect(theoreticalPrefixTokens(pt({ theoretical_cache_hit_rate: 0.5 }))).toBeUndefined();
expect(uncachedInputTokens(pt({ total_prompt_tokens: 100 }))).toBeUndefined();
});

it('rejects out-of-range or NaN hit rates', () => {
expect(
theoreticalPrefixTokens(pt({ total_prompt_tokens: 100, theoretical_cache_hit_rate: 1.2 })),
).toBeUndefined();
expect(
theoreticalPrefixTokens(pt({ total_prompt_tokens: 100, theoretical_cache_hit_rate: -0.1 })),
).toBeUndefined();
expect(
theoreticalPrefixTokens(pt({ total_prompt_tokens: 100, theoretical_cache_hit_rate: NaN })),
).toBeUndefined();
});

it('clamps rounding so uncached input never goes negative', () => {
const d = pt({ total_prompt_tokens: 3, theoretical_cache_hit_rate: 1 });
expect(theoreticalPrefixTokens(d)).toBe(3);
expect(uncachedInputTokens(d)).toBe(0);
});
});

describe('generateTooltipContent cache metadata', () => {
it('shows offload type, backend, and version instead of the binary offload mode', () => {
const html = generateTooltipContent(
tooltipConfig({
Expand Down
63 changes: 61 additions & 2 deletions packages/app/src/components/inference/utils/tooltipUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,59 @@ const generateCacheMetadataHTML = (d: InferenceData, locale: Locale): string =>
* separately because fixed-sequence rows can carry it too.
*/
const AGENTIC_STRINGS = {
en: { speculativeDecoding: 'Speculative Decoding', off: 'Off' },
zh: { speculativeDecoding: '投机解码', off: '关闭' },
en: {
speculativeDecoding: 'Speculative Decoding',
off: 'Off',
theoreticalPrefixTokens: 'Theoretical Prefix Tokens',
uncachedInputTokens: 'Input Tokens w/o Prefix Caching',
},
zh: {
speculativeDecoding: '投机解码',
off: '关闭',
theoreticalPrefixTokens: '理论 prefix token 数',
uncachedInputTokens: '无 prefix cache 的输入 token 数',
},
} as const;

/**
* Theoretical prefix tokens for a point: the sum of every prompt prefix the
* workload has already seen, under an infinite cache. The harness reports that
* sum as a rate over served prompt tokens (`theoretical_cache_hit_rate`, the
* infinite-cache hit rate computed from the trace), so multiplying it back
* with `total_prompt_tokens` recovers the token sum. This is deliberately the
* THEORETICAL prefix — what could have been cached given the trace — not the
* server-observed cache hits, so systems with better real caching are not
* penalized on derived uncached-input metrics.
*/
export const theoreticalPrefixTokens = (d: InferenceData): number | undefined => {
const rate = d.theoretical_cache_hit_rate;
const prompt = d.total_prompt_tokens;
if (
prompt === undefined ||
prompt === null ||
rate === undefined ||
rate === null ||
Number.isNaN(rate) ||
rate < 0 ||
rate > 1
) {
return undefined;
}
return Math.round(prompt * rate);
};

/**
* Input tokens without prefix caching: served prompt tokens minus the
* theoretical prefix ({@link theoreticalPrefixTokens}). Approximates the
* prompt tokens that must be prefilled even by a system with an infinite
* prefix cache.
*/
export const uncachedInputTokens = (d: InferenceData): number | undefined => {
const prefix = theoreticalPrefixTokens(d);
if (prefix === undefined || d.total_prompt_tokens === undefined) return undefined;
return Math.max(0, d.total_prompt_tokens - prefix);
};

const generateAgenticHTML = (d: InferenceData, locale: Locale): string => {
if (d.benchmark_type !== 'agentic_traces') return '';

Expand Down Expand Up @@ -255,6 +304,16 @@ const generateAgenticHTML = (d: InferenceData, locale: Locale): string => {
if (d.total_prompt_tokens !== undefined) {
parts.push(tooltipLine('Prompt Tokens', formatNumber(d.total_prompt_tokens)));
}

const theoreticalPrefix = theoreticalPrefixTokens(d);
if (theoreticalPrefix !== undefined) {
parts.push(tooltipLine(t.theoreticalPrefixTokens, formatNumber(theoreticalPrefix)));
}
const uncachedInput = uncachedInputTokens(d);
if (uncachedInput !== undefined) {
parts.push(tooltipLine(t.uncachedInputTokens, formatNumber(uncachedInput)));
}

if (d.total_generation_tokens !== undefined) {
parts.push(tooltipLine('Generated Tokens', formatNumber(d.total_generation_tokens)));
}
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/hooks/api/use-derived-agentic-metrics.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { RequestLengthMoments } from '@/lib/attention-flops';

import { bulkIdsFetcher, useBulkIdsQuery } from './benchmark-id-query';

export interface DerivedAgenticMetric {
Expand All @@ -7,6 +9,8 @@ export interface DerivedAgenticMetric {
p75_e2e_norm_intvty: number | null;
/** Slow-tail P90 E2E Normalized Interactivity in tok/s/user — 1 / p90(per-request E2EL/OSL). */
p90_e2e_norm_intvty: number | null;
/** Exact joint (ISL, OSL) sums over the run's requests — attention-FLOPs input. */
request_length_moments: RequestLengthMoments | null;
}

export type DerivedAgenticMetricMap = Record<number, DerivedAgenticMetric>;
Expand Down
Loading