Skip to content
Closed
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 @@ -315,6 +315,7 @@ const STRINGS = {
'outside-power-model': 'these benchmark points are outside the supported power model',
'no-cost': 'no TCO for this tier',
'no-token-mix': 'no input/output token mix recorded',
'invalid-cache-observation': 'cache observation missing, partial or not validated',
} satisfies Record<ProfitEstimatorSkipReason, string>,
compareHistory: 'Compare history',
gpuConfig: 'Chip Config',
Expand Down Expand Up @@ -439,6 +440,7 @@ const STRINGS = {
'outside-power-model': '这些基准测试数据点超出功耗模型的适用范围',
'no-cost': '该层级无 TCO 数据',
'no-token-mix': '未记录输入/输出 token 比例',
'invalid-cache-observation': '缓存观测缺失、不完整或未通过有效性检查',
} satisfies Record<ProfitEstimatorSkipReason, string>,
compareHistory: '对比历史趋势',
gpuConfig: '芯片配置',
Expand Down
141 changes: 141 additions & 0 deletions packages/app/src/components/calculator/atom-cache-validity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { describe, expect, it } from 'vitest';

import { NORMALIZED_TOKEN_REVENUE_PRICING } from '@/components/inference/token-revenue';
import type { BenchmarkRow } from '@/lib/api/api';
import { Percentile, Sequence } from '@/lib/catalog/data-mappings';

import { interpolateForGPU } from './interpolation';
import { estimateSkuProfit } from './profit-estimator';
import { buildGpuGroups } from './throughput-data';

const RUN = 'https://github.com/SemiAnalysisAI/InferenceX/actions/runs/36734492117/attempts/1';
const SPECS = { powerKwPerGpu: 2, costPerGpuHour: 10 };
const ASSUMPTIONS = { utilizationPct: 60, labCutPct: 30, basis: 'chip-hour' as const };
const COMPLETE = {
server_gpu_cache_hit_rate: 0.97161,
server_cpu_cache_hit_rate: 0,
server_external_cache_hit_rate: 0,
server_cache_pricing_eligible: 1,
server_cache_validity_code: 1,
};

function atomRow(
conc: number,
intvty: number,
tput: number,
metrics: Record<string, number>,
): BenchmarkRow {
return {
id: conc,
hardware: 'mi355x',
framework: 'atom-disagg',
model: 'dsv4',
precision: 'fp4',
spec_method: 'none',
disagg: true,
is_multinode: true,
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: 'agentic_traces',
isl: null,
osl: null,
conc,
offload_mode: 'none',
image: 'rocm/atom-dev:nightly',
metrics: {
// benchmark-transform derives *_intvty as 1 / *_itl and drops it when itl is absent.
p90_itl: 1 / intvty,
median_itl: 1 / intvty,
tput_per_gpu: tput,
input_tput_per_gpu: tput * 0.99,
output_tput_per_gpu: tput * 0.01,
input_tput_tps: tput * 16 * 0.99,
output_tput_tps: tput * 16 * 0.01,
...metrics,
},
date: '2026-09-30',
run_url: RUN,
} as BenchmarkRow;
}

function price(rows: BenchmarkRow[], target?: number) {
const { grouped } = buildGpuGroups(rows, {
sequence: Sequence.AgenticTraces,
precisions: ['fp4'],
percentile: Percentile.P90,
classify: () => ({ key: 'curve', meta: { hwKey: 'curve' } }),
});
const points = grouped.curve ?? [];
const at = target ?? points[0]!.interactivity;
const result = interpolateForGPU(points, at, 'interactivity_to_throughput', 'costh')!;
return {
points,
result,
row: estimateSkuProfit(result, SPECS, NORMALIZED_TOKEN_REVENUE_PRICING, ASSUMPTIONS),
};
}

describe('ATOM cache validity through buildGpuGroups, interpolateForGPU and estimateSkuProfit', () => {
it('prices an eligible complete observation', () => {
const { points, row } = price([atomRow(1, 190, 840, COMPLETE)]);
expect(points[0]!.cacheHitRate).toBeCloseTo(0.97161, 12);
expect(row).toHaveProperty('revenue');
});

it('keeps performance but refuses actual pricing for a partial observation', () => {
const partial = {
...COMPLETE,
server_cpu_cache_hit_rate: undefined,
server_external_cache_hit_rate: undefined,
};
const { points, row } = price([atomRow(1, 190, 840, partial as Record<string, number>)]);
expect(points[0]!.throughput).toBe(840);
expect(points[0]!.cacheHitRateUnavailable).toBe('PARTIAL_COMPONENT');
expect(row).toMatchObject({ reason: 'invalid-cache-observation' });
});

it('quarantines the known-bad legacy row and restores a re-derived one', () => {
const legacy = {
server_gpu_cache_hit_rate: 0.48352,
server_cpu_cache_hit_rate: 0,
server_external_cache_hit_rate: 0,
};
expect(price([atomRow(1, 190, 840, legacy)]).row).toMatchObject({
reason: 'invalid-cache-observation',
});
expect(price([atomRow(1, 190, 840, COMPLETE)]).row).toHaveProperty('revenue');
});

it('does not restore a mixed curve whose frontier holds an unresolved point', () => {
const unresolved = {
...COMPLETE,
server_cache_pricing_eligible: 0,
server_cache_validity_code: 14,
};
const valid = [atomRow(1, 190, 840, COMPLETE), atomRow(16, 120, 4_000, COMPLETE)];
expect(price(valid, 150).row).toHaveProperty('revenue');
const mixed = [...valid, atomRow(256, 40, 48_000, unresolved)];
const { result, row } = price(mixed, 150);
expect(result.cacheHitRateUnavailable).toBe('REQUEST_SCOPE_UNRESOLVED');
expect(row).toMatchObject({ reason: 'invalid-cache-observation' });
});

it('takes the ATOM P/D token mix from client rates, not the doubled prompt_total', () => {
const inconsistent = {
...COMPLETE,
input_tput_per_gpu: 10,
total_prompt_tokens: 2,
total_generation_tokens: 1,
};
const { points } = price([atomRow(1, 190, 840, inconsistent)]);
expect(points[0]!.inputTokenShare).toBeCloseTo(0.99, 12);
});
});
6 changes: 6 additions & 0 deletions packages/app/src/components/calculator/interpolation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ export function interpolateForGPU(
inputTpPerMw: sorted[0].inputTpPerMw,
outputTpPerMw: sorted[0].outputTpPerMw,
cacheHitRate: sorted[0].cacheHitRate,
cacheHitRateUnavailable: sorted[0].cacheHitRateUnavailable,
theoreticalCacheHitRate: sorted[0].theoreticalCacheHitRate,
inputTokenShare: sorted[0].inputTokenShare,
concurrency: sorted[0].concurrency,
Expand Down Expand Up @@ -453,6 +454,10 @@ export function interpolateForGPU(
// would invent a dip in the cached fraction, so the whole sweep opts out and
// its input tokens bill at full price. That avoids inventing a cache curve,
// but overstates revenue and margin if the missing point actually had hits.
// An invalid actual cache rate on a frontier point leaves the curve's rate unknown: the
// all-or-nothing spline below cannot exclude it, so it must not be priced as zero reuse.
const unavailableAnchor = sorted.find((p) => p.cacheHitRateUnavailable);
const cacheHitRateUnavailable = unavailableAnchor?.cacheHitRateUnavailable;
const cacheHitRate = sorted.every((p) => typeof p.cacheHitRate === 'number')
? buildMetric((p) => p.cacheHitRate!)
: undefined;
Expand Down Expand Up @@ -495,6 +500,7 @@ export function interpolateForGPU(
inputTpPerMw,
outputTpPerMw,
cacheHitRate,
cacheHitRateUnavailable,
theoreticalCacheHitRate,
inputTokenShare,
concurrency,
Expand Down
42 changes: 42 additions & 0 deletions packages/app/src/components/calculator/profit-estimator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,45 @@ describe('applyCacheHitMode', () => {
expect(theoreticalRow.revenue).toBeLessThan(actualRow.revenue);
});
});

describe('explicit theoretical pricing of an invalid actual observation', () => {
const pricing = { inputPerMillion: 1, cachedInputPerMillion: 0.1, outputPerMillion: 1 };
const specs = { powerKwPerGpu: 2, costPerGpuHour: 10 };
const assumptions = { utilizationPct: 60, labCutPct: 30, basis: 'chip-hour' as const };
const invalid = {
hwKey: 'mi355x_atom',
resultKey: 'mi355x_atom',
value: 10_000,
inputTokenShare: 0.75,
clamped: false,
cacheHitRateUnavailable: 'REQUEST_SCOPE_UNRESOLVED',
theoreticalCacheHitRate: 0.96797,
};

it('is skipped in actual mode even when a number is present', () => {
const estimate = estimateSkuProfit(
{ ...invalid, cacheHitRate: 0.95 },
specs,
pricing,
assumptions,
);
expect(estimate).toMatchObject({ reason: 'invalid-cache-observation' });
});

it('is priced only after the explicit theoretical mode swaps in a valid rate', () => {
const [theoretical] = applyCacheHitMode([invalid], 'theoretical');
expect(theoretical).toMatchObject({ cacheHitRate: 0.96797, cacheHitRateSource: 'theoretical' });
expect(estimateSkuProfit(theoretical, specs, pricing, assumptions)).toHaveProperty('revenue');
});

it('is not authorised by a non-finite theoretical rate', () => {
const [swapped] = applyCacheHitMode(
[{ ...invalid, theoreticalCacheHitRate: Number.NaN }],
'theoretical',
);
expect(swapped).not.toHaveProperty('cacheHitRateSource');
expect(estimateSkuProfit(swapped, specs, pricing, assumptions)).toMatchObject({
reason: 'invalid-cache-observation',
});
});
});
27 changes: 24 additions & 3 deletions packages/app/src/components/calculator/profit-estimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ export type ProfitEstimatorSkipReason =
| 'outside-measured-range'
| 'no-power'
| 'no-cost'
| 'no-token-mix';
| 'no-token-mix'
| 'invalid-cache-observation';

export interface ProfitEstimatorSkipped {
hwKey: string;
Expand Down Expand Up @@ -291,7 +292,15 @@ export interface ProfitEstimatorSpecs {
export function estimateSkuProfit(
result: Pick<
InterpolatedResult,
'hwKey' | 'resultKey' | 'precision' | 'value' | 'inputTokenShare' | 'cacheHitRate' | 'clamped'
| 'hwKey'
| 'resultKey'
| 'precision'
| 'value'
| 'inputTokenShare'
| 'cacheHitRate'
| 'cacheHitRateUnavailable'
| 'cacheHitRateSource'
| 'clamped'
> & { date?: string },
specs: ProfitEstimatorSpecs,
pricing: TokenRevenuePricing,
Expand All @@ -308,6 +317,10 @@ export function estimateSkuProfit(
const gpuHours = assumptions.basis === 'chip-hour' ? 1 : gpuHoursPerGwYear(specs.powerKwPerGpu);
if (gpuHours === null) return { ...base, reason: 'no-power' };
if (!(specs.costPerGpuHour > 0)) return { ...base, reason: 'no-cost' };
// Only a rate swapped in by an explicit theoretical mode may price an invalid actual observation.
if (result.cacheHitRateUnavailable && result.cacheHitRateSource !== 'theoretical') {
return { ...base, reason: 'invalid-cache-observation' };
}

const revenuePerGpuHour = tokenRevenueFromRatesPerGpuHour(
result.value,
Expand Down Expand Up @@ -428,7 +441,15 @@ export function applyCacheHitMode<
if (mode === 'actual') return results as T[];
return results.map((r) =>
typeof r.theoreticalCacheHitRate === 'number'
? { ...r, cacheHitRate: r.theoreticalCacheHitRate }
? {
...r,
cacheHitRate: r.theoreticalCacheHitRate,
...(Number.isFinite(r.theoreticalCacheHitRate) &&
r.theoreticalCacheHitRate >= 0 &&
r.theoreticalCacheHitRate <= 1
? { cacheHitRateSource: 'theoretical' as const }
: {}),
}
: r,
);
}
Expand Down
23 changes: 21 additions & 2 deletions packages/app/src/components/calculator/throughput-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { rowToSequence } from '@semianalysisai/inferencex-constants';
import type { AggDataEntry, HardwareConfig } from '@/components/inference/types';
import type { BenchmarkRow } from '@/lib/api/api';
import { rowToAggDataEntry } from '@/lib/benchmarks/benchmark-transform';
import { pricingCacheHitRate } from '@/lib/calculator/cache-pricing';
import { actualCacheRateUnavailable, pricingCacheHitRate } from '@/lib/calculator/cache-pricing';
import { getHardwareKey } from '@/lib/charts/chart-utils';
import {
DEFAULT_TCO_BASIS,
Expand Down Expand Up @@ -152,6 +152,16 @@ function inputTokenShare(row: BenchmarkRow, inputTput: number, outputTput: numbe
if (typeof isl === 'number' && typeof osl === 'number' && isl + osl > 0) {
return isl / (isl + osl);
}
// ATOM P/D prompt_total sums the prefill and decode prompt counters (decode re-counts each
// prompt), so it is not a token mix; use the client cohort's own input/output rates.
if (row.framework?.toLowerCase().startsWith('atom') && row.disagg) {
const clientIn = row.metrics.input_tput_tps;
const clientOut = row.metrics.output_tput_tps;
if (typeof clientIn === 'number' && typeof clientOut === 'number' && clientIn + clientOut > 0) {
return clientIn / (clientIn + clientOut);
}
return null;
}
const prompt = row.metrics.total_prompt_tokens;
const generated = row.metrics.total_generation_tokens;
if (typeof prompt === 'number' && typeof generated === 'number' && prompt + generated > 0) {
Expand Down Expand Up @@ -221,6 +231,11 @@ export function buildGpuGroups<M extends GroupMeta>(
const outputTput = m.output_tput_per_gpu ?? tput;
const inputTput = m.input_tput_per_gpu ?? 0;
const cacheHitRate = pricingCacheHitRate({ ...m, hw: row.hardware });
const cacheUnavailable = actualCacheRateUnavailable({
...m,
framework: row.framework,
run_url: row.run_url,
});
const theoreticalRate =
typeof m.theoretical_cache_hit_rate === 'number' &&
Number.isFinite(m.theoretical_cache_hit_rate)
Expand Down Expand Up @@ -252,7 +267,11 @@ export function buildGpuGroups<M extends GroupMeta>(
throughput: tput,
outputThroughput: outputTput,
inputThroughput: inputTput,
...(cacheHitRate === null ? {} : { cacheHitRate }),
...(cacheUnavailable === null
? cacheHitRate === null
? {}
: { cacheHitRate }
: { cacheHitRateUnavailable: cacheUnavailable }),
...(theoreticalRate === null ? {} : { theoreticalCacheHitRate: theoreticalRate }),
...(tokenShare === null ? {} : { inputTokenShare: tokenShare }),
concurrency: row.conc,
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/components/calculator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export interface GPUDataPoint {
* absence is what makes cached-input billing a no-op outside agentic traces.
*/
cacheHitRate?: number;
/** Reason code when the actual cache rate must not be priced. */
cacheHitRateUnavailable?: string;
/**
* Infinite-cache theoretical hit rate (0..1) computed from the trace itself,
* representing the maximum reuse potential of the workload with unbounded
Expand Down Expand Up @@ -100,6 +102,10 @@ export interface InterpolatedResult {
* fixed-sequence frontier). See {@link GPUDataPoint.cacheHitRate}.
*/
cacheHitRate?: number;
/** Reason code when the actual cache rate must not be priced. */
cacheHitRateUnavailable?: string;
/** Set by an explicit theoretical cache-hit mode that swapped in a valid rate. */
cacheHitRateSource?: 'theoretical';
/**
* Infinite-cache theoretical hit rate at that operating point, or undefined
* when the frontier did not carry a theoretical rate on every point.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
dedupeAgenticHistoryRuns,
} from '@/lib/benchmarks/benchmark-run-selection';
import { rowToAggDataEntry } from '@/lib/benchmarks/benchmark-transform';
import { pricingCacheHitRate } from '@/lib/calculator/cache-pricing';
import { actualCacheRateUnavailable, pricingCacheHitRate } from '@/lib/calculator/cache-pricing';
import {
buildDerivedChartFields,
getHardwareKey,
Expand Down Expand Up @@ -85,6 +85,11 @@ export function rowToLightweightPoint(
server_external_cache_hit_rate: entry.server_external_cache_hit_rate,
server_cpu_cache_hit_rate: entry.server_cpu_cache_hit_rate,
theoretical_cache_hit_rate: entry.theoretical_cache_hit_rate,
framework: entry.framework,
disagg: entry.disagg,
run_url: entry.run_url,
server_cache_pricing_eligible: entry.server_cache_pricing_eligible,
server_cache_validity_code: entry.server_cache_validity_code,
...buildDerivedChartFields(derivedEntry, hwKey, requestedMetrics, tcoBasis),
} as InferenceData;

Expand Down Expand Up @@ -238,6 +243,7 @@ export function interpolateMetricAtInteractivity(
};
const throughputYs = sorted.map((p) => extractMetric(p, 'tpPerGpu')!);
const inputShares = sorted.map(inputTokenShareForRevenue);
if (sorted.some((p) => actualCacheRateUnavailable(p) !== null)) return null;
const cacheHitRates = sorted.map(pricingCacheHitRate);
const inputShare = inputShares.every((share): share is number => share !== null)
? interpolateBounded(inputShares)
Expand Down
Loading