Skip to content

Commit e999d03

Browse files
committed
feat: default the measured-power tier filter to the measured power scatter
An explicit i_metric still wins. Turning the filter on moves a non-measured metric to measured power once, on entry. 中文:将实测功耗分级筛选(i_power)的默认 Y 轴设为实测功耗散点图。链接中显式的 i_metric 仍然优先;首次开启该筛选时,非实测类指标会切换到实测功耗。
1 parent 11c9876 commit e999d03

7 files changed

Lines changed: 127 additions & 32 deletions

File tree

‎docs/data-transforms.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Returns `{ chartData: InferenceData[][], hardwareConfig: HardwareConfig }`.
6363
- `tokenRevenuePerGpuHour` uses cache-aware token sale pricing. `buildDerivedChartFields` emits a normalized placeholder; `applyTokenRevenuePricing` applies the selected normalized or OpenRouter prices to uncached input, cached input, and output. Revenue is the priced streams per GPU-second scaled to one hour. Normalized pricing uses $1/M for uncached input and output and $0.10/M for cached input. OpenRouter uses its published cache-read price when available, otherwise 10% of the prompt price. Agentic cache share uses the same measured GPU-plus-external-or-CPU rule as Fleet Lifecycle. Aggregate rows use their measured input/output split; disaggregated rows derive a like-for-like split from fixed ISL:OSL or measured agentic prompt:generation totals because their raw input/output rates use different prefill/decode GPU denominators. Historical Trends splines total throughput, token share, and cache hit independently before applying these prices; a partly measured cache frontier receives no cache discount.
6464
- `tpPerMw` — `(tputPerGpu * 1000) / hardwarePower` (GPU power in kW, result in tok/s/MW).
6565
- Cost-per-million fields — GPU hourly cost divided by tokens-per-hour (in millions): `costh` / `costr` for owning-at-large-hyperscaler-volume / 3-year-rental pricing respectively. Three token variants exist: combined (`costh`/`costr`), output-only (`costhOutput`/`costrOutput`), and input-only (`costhi`/`costri`). The former Neocloud tier (`costn`/`costni`/`costnOutput`) was removed; its share-link keys alias to the hyperscaler-volume metric.
66-
- Infrastructure purchasing-power fields divide tokens per hour by the same hourly costs. Total (`tokensPerDollarH`/`R`), output-only (`outputTokensPerDollarH`/`R`), and input-only (`inputTokensPerDollarH`/`R`) are separate USD Y-axis metrics. The former ¥-priced `tokensPerRmb*` axes and the Neocloud `*PerDollarN` axes were removed; their share-link keys alias to the matching hyperscaler-volume $ metric. The dashboard defaults to the hyperscaler-volume total-tokens-per-dollar axis (`y_tokensPerDollarH`). Links created for the removed API-price `i_metric=y_tokensPerDollar` axis resolve to the same hyperscaler-volume variant.
66+
- Infrastructure purchasing-power fields divide tokens per hour by the same hourly costs. Total (`tokensPerDollarH`/`R`), output-only (`outputTokensPerDollarH`/`R`), and input-only (`inputTokensPerDollarH`/`R`) are separate USD Y-axis metrics. The former ¥-priced `tokensPerRmb*` axes and the Neocloud `*PerDollarN` axes were removed; their share-link keys alias to the matching hyperscaler-volume $ metric. The dashboard defaults to the hyperscaler-volume total-tokens-per-dollar axis (`y_tokensPerDollarH`); with a measured-power tier filter (`i_power`) it defaults to the measured power scatter (`y_measuredAvgPower`) instead. Links created for the removed API-price `i_metric=y_tokensPerDollar` axis resolve to the same hyperscaler-volume variant.
6767
- Energy fields — `jTotal` / `jOutput` / `jInput`: `(hardwarePower * 1000) / tputPerGpu` (Joules per token, where power in kW is converted to W). These divide one GPU's all-in power by that GPU's reported throughput; for disaggregated rows `output_tput_per_gpu` is per decode GPU, so `jOutput` ignores the prefill pool. Its semantics are frozen; the whole-deployment view lives in the power boundaries below.
6868
- Power-boundary fields — see [Power boundaries](#power-boundaries).
6969

‎docs/state-ownership.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ them.
113113
Axis and presentation state:
114114

115115
- selected x-axis and y-axis metrics, percentile, and effective x-axis mode
116+
- the y-axis default depends on the power tier quick filter: `defaultYAxisMetric` in `url-state.ts` opens a link that has `i_power` but no `i_metric` on `y_measuredAvgPower`, and `InferenceContext` omits `i_metric` from share links only when the metric equals that contextual default (or the metric a route seeds, as on the compare pages), so an explicit `i_metric` always wins. Turning the tier filter on from empty moves a metric outside the measured family to `y_measuredAvgPower`; tier edits after that, clearing the filter, and restoring `i_power` from a link leave the metric alone
116117
- the Measured controls (Boundary / Per / Scope / Statistic / Display / Unit) own no state: `measured-metric-config.ts` resolves each selection to the nearest registered metric key and writes it back to `selectedYAxisMetric`, so the power boundary rides on `i_metric` (there is deliberately no `i_pbasis`; see [PowerX Permanent View](./powerx-permanent-view.md))
117118
- the Measured Power Display value `timeline` (`y_measuredPowerTimeline`) swaps the chart body for `PowerTimeline`; its axis mode, line mode, window-only display, focused trace, all-in reference switch and concurrency filter are component state that `PowerTimeline` itself reads and writes through `useUrlState` as `i_ptaxis` / `i_ptlines` / `i_ptwindow` / `i_ptfocus` / `i_ptutility` / `i_ptconc`, so they never pass through the display domain
118119
- token-revenue price source (`i_revenue`): normalized uncached/cached/output pricing or the selected model's live OpenRouter catalog prices

‎packages/app/cypress/e2e/certified-power-filter.cy.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { assertShareLinkParams } from '../support/share-link';
2+
13
// Deterministic intercepted rows exercise the validated/historical power UI:
24
// one row has power_valid=1 and one has no validation verdict.
35

@@ -95,6 +97,15 @@ function visitCertifiedPowerChart(extraParams = '', benchmarks = powerBenchmarks
9597
cy.get('[data-testid="chart-figure"]').should('have.length.at.least', 1);
9698
}
9799

100+
const MEASURED_POWER_OPTION = 'Measured Power';
101+
const MEASURED_POWER_TITLE = 'Measured Average Power per Chip';
102+
const TOKENS_PER_DOLLAR_TITLE = 'Total Tokens per $1 TCO';
103+
104+
const assertYAxisMetric = (option: string, title: string) => {
105+
cy.get('[data-testid="yaxis-metric-selector"]').should('contain.text', option);
106+
cy.get('[data-testid="chart-figure"] h2').should('contain.text', title);
107+
};
108+
98109
const visiblePowerPoints = () =>
99110
cy.get<SVGGElement>('.dot-group').filter((_, element) => element.style.opacity !== '0');
100111

@@ -173,6 +184,39 @@ describe('Validated vs historical measured power', () => {
173184
cy.get('[data-testid="quick-filters-selected-count"]').should('contain.text', '1 selected');
174185
});
175186

187+
it('opens a shared i_power link without i_metric on the measured power scatter', () => {
188+
visitCertifiedPowerChart('&i_power=certified');
189+
190+
assertYAxisMetric(MEASURED_POWER_OPTION, MEASURED_POWER_TITLE);
191+
cy.get('.dot-group[data-hw-key^="mi300x"]').should('exist');
192+
cy.get('.dot-group[data-hw-key^="b200"]').should('not.exist');
193+
assertShareLinkParams({ i_power: 'certified', i_metric: null });
194+
});
195+
196+
it('keeps an explicit tokens-per-dollar metric next to a power tier filter', () => {
197+
visitCertifiedPowerChart('&i_metric=y_tokensPerDollarH&i_power=certified');
198+
199+
assertYAxisMetric(TOKENS_PER_DOLLAR_TITLE, TOKENS_PER_DOLLAR_TITLE);
200+
assertShareLinkParams({ i_metric: 'y_tokensPerDollarH', i_power: 'certified' });
201+
});
202+
203+
it('moves the default chart to measured power when Validated turns on, not when cleared', () => {
204+
visitCertifiedPowerChart();
205+
assertYAxisMetric(TOKENS_PER_DOLLAR_TITLE, TOKENS_PER_DOLLAR_TITLE);
206+
207+
cy.get('[data-testid="scatter-quick-filters"]').click();
208+
cy.get('[data-testid="quick-filter-power-certified"]').click();
209+
cy.get('[data-testid="quick-filters-selected-count"]').should('contain.text', '1 selected');
210+
assertYAxisMetric(MEASURED_POWER_OPTION, MEASURED_POWER_TITLE);
211+
212+
cy.contains('button', 'Clear filters').click();
213+
cy.get('[data-testid="quick-filters-selected-count"]').should('not.exist');
214+
assertYAxisMetric(MEASURED_POWER_OPTION, MEASURED_POWER_TITLE);
215+
cy.get('[data-testid="quick-filters-dialog"]').contains('button', 'Done').click();
216+
217+
assertShareLinkParams({ i_metric: 'y_measuredAvgPower', i_power: null });
218+
});
219+
176220
it('uses Optimal Only for measured markers while preserving the power boundary', () => {
177221
// Each chip has two boundary vertices and one lower-power interior point.
178222
const curveBenchmarks = powerBenchmarks.map((row) => ({

‎packages/app/src/components/inference/InferenceContext.tsx‎

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import type {
4747
TokenRevenuePriceSource,
4848
} from '@/components/inference/types';
4949
import { resolveMetricConfigKey } from '@/components/inference/metric-registry';
50+
import { getMeasuredMetricConfig } from '@/components/inference/measured-metric-config';
5051
import { Button } from '@/components/ui/button';
5152
import {
5253
Dialog,
@@ -74,7 +75,7 @@ import {
7475
} from '@/components/inference/perf-ruler-store';
7576
import { useParetoHighlightToggle } from './hooks/useParetoHighlightToggle';
7677
import { useOpenRouterPricing } from '@/hooks/api/use-openrouter-pricing';
77-
import { DEFAULT_Y_AXIS_METRIC } from '@/lib/url-state';
78+
import { defaultYAxisMetric } from '@/lib/url-state';
7879
import { computeToggle } from '@/hooks/useTogglableSet';
7980
import { buildAvailabilityHwKey } from '@/lib/chart-utils';
8081
import { getHardwareConfig, getModelSortIndex, isKnownGpu } from '@/lib/constants';
@@ -125,6 +126,9 @@ import {
125126
type SpecMode,
126127
} from './utils/quickFilters';
127128

129+
const splitUrlList = (value: string | undefined): string[] =>
130+
value ? value.split(',').filter(Boolean) : [];
131+
128132
const InferenceDataContext = createContext<InferenceDataContextType | undefined>(undefined);
129133
const InferenceFiltersContext = createContext<InferenceFiltersContextType | undefined>(undefined);
130134
const InferenceDisplayContext = createContext<InferenceDisplayContextType | undefined>(undefined);
@@ -445,7 +449,11 @@ export function InferenceProvider({
445449
}
446450
}, [selectedGpuResolution]);
447451
const [selectedYAxisMetric, setSelectedYAxisMetric] = useState<string>(() =>
448-
resolveMetricConfigKey(getUrlParam('i_metric'), initialYAxisMetric ?? DEFAULT_Y_AXIS_METRIC),
452+
resolveMetricConfigKey(
453+
getUrlParam('i_metric'),
454+
initialYAxisMetric ??
455+
defaultYAxisMetric(parsePowerTiers(splitUrlList(getUrlParam('i_power')))),
456+
),
449457
);
450458
const [tokenRevenuePriceSource, setTokenRevenuePriceSource] = useState<TokenRevenuePriceSource>(
451459
() => (getUrlParam('i_revenue') === 'openrouter' ? 'openrouter' : 'normalized'),
@@ -557,10 +565,8 @@ export function InferenceProvider({
557565
const [quickFilterPower, setQuickFilterPower] = useState<PowerTier[]>([]);
558566
const [quickFilterTopologies, setQuickFilterTopologies] = useState<string[]>([]);
559567
useEffect(() => {
560-
const parse = (key: 'i_vendor' | 'i_fw' | 'i_disagg' | 'i_spec' | 'i_power' | 'i_topology') => {
561-
const v = getUrlParam(key);
562-
return v ? v.split(',').filter(Boolean) : [];
563-
};
568+
const parse = (key: 'i_vendor' | 'i_fw' | 'i_disagg' | 'i_spec' | 'i_power' | 'i_topology') =>
569+
splitUrlList(getUrlParam(key));
564570
const vendors = parse('i_vendor');
565571
const frameworks = parse('i_fw');
566572
// Preserve old shared links: `agg` used to mean every non-disaggregated
@@ -990,6 +996,22 @@ export function InferenceProvider({
990996
},
991997
[setSelectedYAxisMetric, clearPresetOnChange],
992998
);
999+
// A tier filter is only meaningful on a power chart. The move happens once, on
1000+
// entry, so a metric picked while the filter is on survives later tier edits.
1001+
// The URL restore keeps the raw setter so a link's explicit `i_metric` wins.
1002+
const setQuickFilterPowerAndMetric = useCallback(
1003+
(tiers: PowerTier[]) => {
1004+
setQuickFilterPower(tiers);
1005+
if (
1006+
quickFilterPower.length === 0 &&
1007+
tiers.length > 0 &&
1008+
!getMeasuredMetricConfig(selectedYAxisMetric)
1009+
) {
1010+
setSelectedYAxisMetricAndClear(defaultYAxisMetric(tiers));
1011+
}
1012+
},
1013+
[quickFilterPower, selectedYAxisMetric, setSelectedYAxisMetricAndClear],
1014+
);
9931015
const setSelectedGPUsAndClear = useCallback(
9941016
(next: string[]) => {
9951017
if (
@@ -1677,7 +1699,10 @@ export function InferenceProvider({
16771699

16781700
useUrlStateSync(
16791701
{
1680-
i_metric: selectedYAxisMetric,
1702+
i_metric:
1703+
selectedYAxisMetric === (initialYAxisMetric ?? defaultYAxisMetric(quickFilterPower))
1704+
? ''
1705+
: selectedYAxisMetric,
16811706
i_revenue: usesTokenSalePricing(selectedYAxisMetric) ? tokenRevenuePriceSource : 'normalized',
16821707
i_mstat: fixedSequenceStatistic,
16831708
i_gpus: selectedGPUs.join(','),
@@ -1716,6 +1741,7 @@ export function InferenceProvider({
17161741
},
17171742
[
17181743
selectedYAxisMetric,
1744+
initialYAxisMetric,
17191745
tokenRevenuePriceSource,
17201746
selectedXAxisMetric,
17211747
selectedE2eXAxisMetric,
@@ -2028,7 +2054,7 @@ export function InferenceProvider({
20282054
setQuickFilterFrameworks,
20292055
setQuickFilterDeployment,
20302056
setQuickFilterSpec,
2031-
setQuickFilterPower,
2057+
setQuickFilterPower: setQuickFilterPowerAndMetric,
20322058
setQuickFilterTopologies,
20332059
setIsLegendExpanded,
20342060
setHideNonOptimal,

‎packages/app/src/components/inference/ui/ChartControls.tsx‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import {
6767
import { XAxisModeSelector } from './XAxisModeSelector';
6868
import { showsTcoBasisSelector, Sequence, type Model } from '@/lib/data-mappings';
6969
import { useLocale } from '@/lib/use-locale';
70-
import { DEFAULT_Y_AXIS_METRIC } from '@/lib/url-state';
70+
import { defaultYAxisMetric } from '@/lib/url-state';
7171

7272
const STRINGS = {
7373
en: {
@@ -207,8 +207,14 @@ export default function ChartControls({
207207
useEffect(() => setMounted(true), []);
208208

209209
const { openDropdown, handleDropdownOpenChange } = useOpenDropdown<string>();
210-
const { selectedModel, selectedSequence, selectedPrecisions, selectedGPUs, selectedDateRange } =
211-
useInferenceFilters();
210+
const {
211+
selectedModel,
212+
selectedSequence,
213+
selectedPrecisions,
214+
selectedGPUs,
215+
selectedDateRange,
216+
quickFilters,
217+
} = useInferenceFilters();
212218
const {
213219
graphs,
214220
availableGPUs,
@@ -449,7 +455,7 @@ export default function ChartControls({
449455
};
450456

451457
const secondaryCount =
452-
(selectedYAxisMetric === DEFAULT_Y_AXIS_METRIC ? 0 : 1) +
458+
(selectedYAxisMetric === defaultYAxisMetric(quickFilters.power) ? 0 : 1) +
453459
(showXAxisMode && selectedXAxisMode !== 'interactivity' ? 1 : 0) +
454460
(selectedXAxisMetric === undefined || selectedXAxisMetric === 'p90_ttft' ? 0 : 1) +
455461
(scaleType === 'auto' ? 0 : 1) +

‎packages/app/src/lib/url-state.test.ts‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,13 @@ describe('PARAM_DEFAULTS', () => {
4646
expect(PARAM_DEFAULTS.i_seq).toBe('');
4747
});
4848

49-
it('strips i_metric against the same default the dashboard opens on', async () => {
50-
// A share link omits any value equal to PARAM_DEFAULTS. If this drifted
51-
// from DEFAULT_Y_AXIS_METRIC, a link captured on the *other* metric would
52-
// be written without `i_metric` and reopen on the dashboard default.
53-
const { PARAM_DEFAULTS, DEFAULT_Y_AXIS_METRIC } = await import('@/lib/url-state');
49+
it('opens on tokens per dollar, or on measured power under any power tier filter', async () => {
50+
const { defaultYAxisMetric } = await import('@/lib/url-state');
5451
const { DEFAULT_METRIC_CONFIG_KEY } = await import('@/components/inference/metric-registry');
55-
expect(PARAM_DEFAULTS.i_metric).toBe(DEFAULT_Y_AXIS_METRIC);
56-
expect(DEFAULT_Y_AXIS_METRIC).toBe('y_tokensPerDollarH');
57-
expect(DEFAULT_Y_AXIS_METRIC).toBe(DEFAULT_METRIC_CONFIG_KEY);
52+
expect(defaultYAxisMetric([])).toBe('y_tokensPerDollarH');
53+
expect(defaultYAxisMetric([])).toBe(DEFAULT_METRIC_CONFIG_KEY);
54+
expect(defaultYAxisMetric(['certified'])).toBe('y_measuredAvgPower');
55+
expect(defaultYAxisMetric(['legacy'])).toBe('y_measuredAvgPower');
5856
});
5957

6058
it('has expected default for r_range', async () => {
@@ -375,6 +373,19 @@ describe('writeUrlParams + buildShareUrl', () => {
375373
expect(url).toContain('g_model=test-model');
376374
});
377375

376+
it('keeps an explicit tokens-per-dollar metric, whose default status depends on i_power', async () => {
377+
setupWindow('', '/inference');
378+
const { writeUrlParams, buildShareUrl, DEFAULT_Y_AXIS_METRIC } =
379+
await import('@/lib/url-state');
380+
381+
writeUrlParams({ i_metric: DEFAULT_Y_AXIS_METRIC, i_power: 'certified' });
382+
await vi.advanceTimersByTimeAsync(200);
383+
384+
const params = new URL(buildShareUrl()).searchParams;
385+
expect(params.get('i_metric')).toBe('y_tokensPerDollarH');
386+
expect(params.get('i_power')).toBe('certified');
387+
});
388+
378389
it('removes params that match their default value', async () => {
379390
setupWindow('', '/inference');
380391
const { writeUrlParams, buildShareUrl } = await import('@/lib/url-state');

‎packages/app/src/lib/url-state.ts‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* Only non-default values are written to keep URLs short.
1414
*/
1515
import { LIFECYCLE_DEFAULTS } from '@/components/calculator/lifecycle';
16+
import { MEASURED_METRIC_DEFAULTS } from '@/components/inference/measured-metric-config';
1617
import { dashboardRouteForPathname, getDashboardRoute } from '@/lib/dashboard-routes';
1718
import { routeModelForPathname } from '@/lib/model-routes';
1819

@@ -135,19 +136,22 @@ export type UrlStateParams = Partial<Record<UrlStateKey, string>>;
135136
/** Default values for each parameter. Params matching their default are omitted from share URLs. */
136137
/**
137138
* Dashboard default y-axis: total tokens purchased per $1 of Hyperscaler
138-
* ownership TCO. It leads with infrastructure purchasing power, which depends
139-
* only on measured throughput and hardware cost, so the opening view does not
140-
* assume a token sale price. Token Revenue per GPU Hour remains one selector
141-
* click away for revenue-side questions.
142-
* `?i_metric=` still wins, so existing shared links are unaffected.
143-
*
144-
* Lives here rather than in `InferenceContext` because `PARAM_DEFAULTS` below
145-
* strips any value equal to the default from share links. If the two drifted,
146-
* a link captured on the *other* metric would be written without `i_metric`
147-
* and reopen on this one.
139+
* ownership TCO. It depends only on measured throughput and hardware cost, so
140+
* the opening view does not assume a token sale price.
148141
*/
149142
export const DEFAULT_Y_AXIS_METRIC = 'y_tokensPerDollarH';
150143

144+
/**
145+
* The y-axis a link without `i_metric` opens on. A measured-power tier filter
146+
* (`i_power`) is only meaningful on a power chart, so any tier opens the
147+
* measured power scatter. `InferenceContext` resolves links and strips
148+
* `i_metric` from share links with this one function; if those two defaults
149+
* differed, a link would reopen on a metric other than the one it captured.
150+
*/
151+
export function defaultYAxisMetric(powerTiers: readonly string[]): string {
152+
return powerTiers.length > 0 ? MEASURED_METRIC_DEFAULTS.power : DEFAULT_Y_AXIS_METRIC;
153+
}
154+
151155
/** Shared defaults for the fleet lifecycle and calculator MW controls. */
152156
export const DEFAULT_FLEET_MW = '10';
153157
export const DEFAULT_LIFECYCLE_RAMP_MONTHS = String(LIFECYCLE_DEFAULTS.rampMonths);
@@ -169,7 +173,10 @@ export const PARAM_DEFAULTS: Record<UrlStateKey, string> = {
169173
// explicitly, so an explicit FP4 selection must survive (not be stripped as a
170174
// "default") or it would silently revert to the per-model auto default on reload.
171175
i_prec: '',
172-
i_metric: DEFAULT_Y_AXIS_METRIC,
176+
// No strippable default: the no-param metric depends on `i_power` and on a
177+
// route's seeded metric, so `InferenceContext` writes '' when the metric
178+
// equals that resolved default and the explicit key otherwise.
179+
i_metric: '',
173180
i_revenue: 'normalized',
174181
i_pctl: 'p90',
175182
i_mstat: 'median',

0 commit comments

Comments
 (0)