Skip to content

Commit 50bf336

Browse files
cquil11claude
andcommitted
fix(inference): scope run picker to selected scenario
On the /inference dashboard, the "Run N/M" picker listed every workflow run for the date regardless of the selected scenario (Agentic Traces vs Single Turn): it filtered only by changelog config_keys (model + precision, no benchmark_type). A same-day single_turn sweep therefore leaked into the Agentic Traces picker, and selecting it poisoned the asOfRunId cutoff, blanking the chart. Fix, DB-side and non-circular: the runConfigs coverage in the workflow-info response (which enumerates all runs on a date independently of the asOf cutoff) now carries the raw benchmark_type / isl / osl columns. New pure helpers scenarioRunIdsForDate() (runEnumeration.ts) and restrictRunsToScenario() (utils.ts) derive the run ids with coverage for the selected model + scenario and restrict the picker list to them, with fallbacks so the selector still renders when coverage data is unavailable. GlobalFilterContext exposes runConfigs; InferenceContext filters the picker's runs by the selected scenario before deriving asOf. Filtering from loaded chart rows was rejected as circular (those rows depend on asOfRunId). Unofficial-run overlays are unaffected: dataRunsForDate() dedupes by run id, so the finer-grained DISTINCT rows collapse. 中文:修复 /inference 面板中 "Run N/M" 选择器不区分场景的问题。此前 选择器仅按 changelog 的 config_keys(模型 + 精度)过滤,不含 benchmark_type,导致同一天的 single_turn 运行泄漏进 Agentic Traces 的选择器,选中后会污染 asOfRunId 截断点、使图表变空。修复方案在数据 库侧且无循环依赖:workflow-info 响应中的 runConfigs 覆盖数据(独立于 asOf 截断点、枚举当天全部运行)新增 benchmark_type / isl / osl 原始 列;新增纯函数 scenarioRunIdsForDate()(runEnumeration.ts)和 restrictRunsToScenario()(utils.ts),按所选模型 + 场景推导有数据覆盖 的运行 id 并据此收窄选择器列表,覆盖数据缺失时回退到原列表以保证选择 器正常渲染。GlobalFilterContext 暴露 runConfigs;InferenceContext 在 推导 asOf 之前按所选场景过滤选择器的运行列表。基于已加载图表行的客户 端过滤方案因循环依赖(这些行本身依赖 asOfRunId)被否决。非官方运行 叠加层不受影响:dataRunsForDate() 按运行 id 去重,更细粒度的 DISTINCT 行会被合并。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 098f611 commit 50bf336

10 files changed

Lines changed: 261 additions & 11 deletions

File tree

packages/app/cypress/support/mock-data.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ export function createMockGlobalFilterContext(
447447
availabilityRows: undefined,
448448
workflowInfo: null,
449449
availableRuns: {},
450+
runConfigs: [],
450451
workflowLoading: false,
451452
workflowError: null,
452453
...overrides,

packages/app/src/components/GlobalFilterContext.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
import { computeAutoSwitchDecision } from '@/lib/unofficial-run-auto-switch';
3737
import { countCurvesByPrecision, resolveEffectivePrecisions } from '@/lib/default-precisions';
3838
import { resolveEffectiveSequence } from '@/lib/default-sequence';
39-
import type { AvailabilityRow, WorkflowInfoResponse } from '@/lib/api';
39+
import type { AvailabilityRow, RunConfigRow, WorkflowInfoResponse } from '@/lib/api';
4040

4141
const RUNDATE_RE = /^\d{4}-\d{2}-\d{2}$/u;
4242
const RUNID_RE = /^[A-Za-z0-9_-]{1,64}$/u;
@@ -107,6 +107,14 @@ export interface GlobalFilterContextType {
107107
// Workflow info
108108
workflowInfo: { runInfoBySequence: Record<string, RunInfo> }[] | null;
109109
availableRuns: Record<string, RunInfo>;
110+
/**
111+
* Per-(run, config) coverage for the current date — which workflow runs
112+
* produced benchmark data for which model / scenario / precision. Data-driven
113+
* (from the benchmark rows), so it enumerates every run on the date including
114+
* ones without a changelog entry. The run picker uses this to scope its list
115+
* to the selected model + scenario (see `scenarioRunIdsForDate`).
116+
*/
117+
runConfigs: RunConfigRow[];
110118
workflowLoading: boolean;
111119
workflowError: string | null;
112120
}
@@ -442,6 +450,8 @@ export function GlobalFilterProvider({
442450
[workflowData],
443451
);
444452

453+
const runConfigs = useMemo(() => workflowData?.runConfigs ?? [], [workflowData]);
454+
445455
const workflowInfo = useMemo(
446456
() => (Object.keys(availableRuns).length > 0 ? [{ runInfoBySequence: availableRuns }] : null),
447457
[availableRuns],
@@ -530,6 +540,7 @@ export function GlobalFilterProvider({
530540
availabilityRows,
531541
workflowInfo,
532542
availableRuns,
543+
runConfigs,
533544
workflowLoading,
534545
workflowError,
535546
}),
@@ -551,6 +562,7 @@ export function GlobalFilterProvider({
551562
availabilityRows,
552563
workflowInfo,
553564
availableRuns,
565+
runConfigs,
554566
workflowLoading,
555567
workflowError,
556568
],

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

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ import {
6363
resolveExclusionToggle,
6464
type ExclusionConflictPolicy,
6565
} from '@/lib/exclusion';
66-
import { filterRunsByModel, getDisplayLabel } from '@/lib/utils';
66+
import { filterRunsByModel, getDisplayLabel, restrictRunsToScenario } from '@/lib/utils';
6767

6868
import {
6969
isAgenticOnlyXAxisMode,
@@ -72,6 +72,7 @@ import {
7272
type XAxisMode,
7373
} from './hooks/useChartData';
7474
import { resolveComparisonEntries } from './utils/comparisonEntry';
75+
import { scenarioRunIdsForDate } from './utils/runEnumeration';
7576
import { resolveLabelState, serializeLabelState } from './utils/label-defaults';
7677
import {
7778
EMPTY_QUICK_FILTERS,
@@ -135,6 +136,7 @@ export function InferenceProvider({
135136
availabilityRows,
136137
workflowInfo,
137138
availableRuns,
139+
runConfigs,
138140
workflowError,
139141
} = useGlobalFilters();
140142

@@ -368,11 +370,39 @@ export function InferenceProvider({
368370
[selectedModel],
369371
);
370372

371-
const filteredAvailableRuns = useMemo(
373+
// DB model keys for the selected display model (e.g. ['dsv4']). Used both by
374+
// the scenario run scoping below and the GPU comparison date picker further down.
375+
const dbModelKeys = useMemo<string[]>(
376+
() => DISPLAY_MODEL_TO_DB[selectedModel] ?? [selectedModel],
377+
[selectedModel],
378+
);
379+
380+
const changelogFilteredRuns = useMemo(
372381
() => filterRunsByModel(availableRuns, modelPrefixes, [...effectivePrecisions]),
373382
[availableRuns, modelPrefixes, effectivePrecisions],
374383
);
375384

385+
// Run ids that actually produced data for the selected model + scenario on the
386+
// date, derived from per-(run, config) coverage (independent of the chart's
387+
// "as of run" query, so no circular dependency). `/api/v1/workflow-info`
388+
// returns every run on a date across all models/scenarios, so without this a
389+
// same-day run for a different scenario (e.g. a single_turn sweep while viewing
390+
// Agentic Traces) would leak into the picker and poison the "as of run" cutoff.
391+
const scenarioRunIds = useMemo(
392+
() =>
393+
scenarioRunIdsForDate(runConfigs, dbModelKeys, effectiveSequence, [...effectivePrecisions]),
394+
[runConfigs, dbModelKeys, effectiveSequence, effectivePrecisions],
395+
);
396+
397+
// The picker list: the changelog/precision-scoped runs, further restricted to
398+
// the ones that shipped data for the selected scenario. Falls back to the
399+
// changelog-scoped list when coverage data is unavailable (old dates / JSON
400+
// mode) so the selector always renders.
401+
const filteredAvailableRuns = useMemo(
402+
() => restrictRunsToScenario(changelogFilteredRuns, availableRuns, scenarioRunIds),
403+
[changelogFilteredRuns, availableRuns, scenarioRunIds],
404+
);
405+
376406
const effectiveSelectedRunId = useMemo(() => {
377407
if (!filteredAvailableRuns) return selectedRunId;
378408
const filteredRunIds = Object.keys(filteredAvailableRuns);
@@ -484,11 +514,6 @@ export function InferenceProvider({
484514
);
485515

486516
// For GPU comparison date picker — use shared availability data from global filters
487-
const dbModelKeys = useMemo<string[]>(
488-
() => DISPLAY_MODEL_TO_DB[selectedModel] ?? [selectedModel],
489-
[selectedModel],
490-
);
491-
492517
const dateRangeAvailableDates = useMemo(() => {
493518
if (selectedGPUs.length === 0) return availableDates;
494519
if (!availabilityRows) return availableDates;

packages/app/src/components/inference/utils/runEnumeration.test.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
22

33
import type { RunConfigRow } from '@/lib/api';
44

5-
import { dataRunsForDate } from './runEnumeration';
5+
import { dataRunsForDate, scenarioRunIdsForDate } from './runEnumeration';
66

77
function rc(over: Partial<RunConfigRow>): RunConfigRow {
88
return {
@@ -16,10 +16,24 @@ function rc(over: Partial<RunConfigRow>): RunConfigRow {
1616
framework: 'vllm',
1717
spec_method: 'none',
1818
disagg: false,
19+
// Defaults to a single_turn 8k/1k row; agentic tests override these.
20+
benchmark_type: 'single_turn',
21+
isl: 8192,
22+
osl: 1024,
1923
...over,
2024
};
2125
}
2226

27+
/** A single_turn (fixed-seq) run config for the given isl/osl. */
28+
function single(over: Partial<RunConfigRow>): RunConfigRow {
29+
return rc({ benchmark_type: 'single_turn', isl: 8192, osl: 1024, ...over });
30+
}
31+
32+
/** An agentic_traces run config (null isl/osl, as ingested for agentic rows). */
33+
function agentic(over: Partial<RunConfigRow>): RunConfigRow {
34+
return rc({ benchmark_type: 'agentic_traces', isl: null, osl: null, ...over });
35+
}
36+
2337
const SCOPE = {
2438
modelDbKeys: ['minimaxm3'],
2539
selectedGPUs: ['mi300x_vllm'],
@@ -107,3 +121,66 @@ describe('dataRunsForDate', () => {
107121
expect(dataRunsForDate([rc({ model: 'dsr1' })], SCOPE)).toEqual([]);
108122
});
109123
});
124+
125+
// Mirrors the repro: on one date the same model has a single_turn run and an
126+
// agentic run; each scenario must list ONLY its own run.
127+
const ids = (rows: RunConfigRow[], model: string[], seq: string, prec: string[] = []) =>
128+
[...scenarioRunIdsForDate(rows, model, seq, prec)].toSorted();
129+
130+
describe('scenarioRunIdsForDate', () => {
131+
it('agentic scenario excludes runs that only produced single_turn data', () => {
132+
const rows = [
133+
agentic({ github_run_id: 28955639528, model: 'dsv4' }), // dsv4 agentic
134+
single({ github_run_id: 28900000001, model: 'dsv4' }), // dsv4 single_turn (leaks today)
135+
single({ github_run_id: 28900000002, model: 'glm5' }), // other model
136+
];
137+
expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['28955639528']);
138+
});
139+
140+
it('single_turn scenario excludes agentic-only runs', () => {
141+
const rows = [
142+
agentic({ github_run_id: 1, model: 'dsv4' }),
143+
single({ github_run_id: 2, model: 'dsv4' }),
144+
];
145+
expect(ids(rows, ['dsv4'], '8k/1k')).toEqual(['2']);
146+
});
147+
148+
it('lists a run that produced data for both scenarios under each scenario', () => {
149+
const rows = [
150+
agentic({ github_run_id: 42, model: 'dsv4' }),
151+
single({ github_run_id: 42, model: 'dsv4' }), // same run, both types
152+
];
153+
expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['42']);
154+
expect(ids(rows, ['dsv4'], '8k/1k')).toEqual(['42']);
155+
});
156+
157+
it('scopes to the selected model DB keys', () => {
158+
const rows = [
159+
agentic({ github_run_id: 1, model: 'dsv4' }),
160+
agentic({ github_run_id: 2, model: 'glm5' }),
161+
];
162+
expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['1']);
163+
});
164+
165+
it('respects precision scoping when precisions are provided', () => {
166+
const rows = [
167+
agentic({ github_run_id: 1, model: 'dsv4', precision: 'fp4' }),
168+
agentic({ github_run_id: 2, model: 'dsv4', precision: 'fp8' }),
169+
];
170+
expect(ids(rows, ['dsv4'], 'agentic-traces', ['fp4'])).toEqual(['1']);
171+
// Empty precisions = no precision constraint.
172+
expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['1', '2']);
173+
});
174+
175+
it('dedupes a run appearing across multiple matching configs', () => {
176+
const rows = [
177+
agentic({ github_run_id: 7, model: 'dsv4', hardware: 'b200' }),
178+
agentic({ github_run_id: 7, model: 'dsv4', hardware: 'gb200' }),
179+
];
180+
expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['7']);
181+
});
182+
183+
it('returns an empty set when there is no coverage data', () => {
184+
expect(scenarioRunIdsForDate([], ['dsv4'], 'agentic-traces').size).toBe(0);
185+
});
186+
});

packages/app/src/components/inference/utils/runEnumeration.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* chart keys them.
1616
*/
1717

18+
import { rowToSequence } from '@semianalysisai/inferencex-constants';
19+
1820
import type { AggDataEntry } from '@/components/inference/types';
1921
import type { RunConfigRow } from '@/lib/api';
2022
import { getHardwareKey } from '@/lib/chart-utils';
@@ -78,3 +80,38 @@ export function dataRunsForDate(runConfigs: RunConfigRow[], scope: RunScope): Da
7880

7981
return [...byRun.values()].toSorted((a, b) => a.runStartedAt.localeCompare(b.runStartedAt));
8082
}
83+
84+
/**
85+
* GitHub run ids (as strings) that produced benchmark data for the given model
86+
* DB keys + scenario (sequence) — and, when `precisions` is non-empty, only for
87+
* those precisions — on a date. Derived from per-(run, config) coverage
88+
* ({@link RunConfigRow}, i.e. the benchmark rows themselves), so a run that
89+
* shipped data without a changelog entry still counts.
90+
*
91+
* The run picker uses this to list ONLY the runs that actually produced data for
92+
* the currently selected model + scenario. `/api/v1/workflow-info` returns every
93+
* run on a date across all models and scenarios, so without this scoping a
94+
* same-day run for a different scenario (e.g. a `single_turn` sweep while the
95+
* user is viewing Agentic Traces) leaks into the picker — and selecting it
96+
* poisons the "as of run" cutoff, blanking the chart.
97+
*
98+
* Scenario matching mirrors the chart's own row filter (`rowToSequence(row) ===
99+
* selectedSequence` in useChartData), so the picker and the plotted data always
100+
* agree on which runs are relevant.
101+
*/
102+
export function scenarioRunIdsForDate(
103+
runConfigs: RunConfigRow[],
104+
modelDbKeys: string[],
105+
sequence: string,
106+
precisions: string[] = [],
107+
): Set<string> {
108+
const precSet = precisions.length > 0 ? new Set(precisions) : null;
109+
const ids = new Set<string>();
110+
for (const rc of runConfigs) {
111+
if (!modelDbKeys.includes(rc.model)) continue;
112+
if (precSet && !precSet.has(rc.precision)) continue;
113+
if (rowToSequence(rc) !== sequence) continue;
114+
ids.add(String(rc.github_run_id));
115+
}
116+
return ids;
117+
}

packages/app/src/lib/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ export interface RunConfigRow {
9696
framework: string;
9797
spec_method: string;
9898
disagg: boolean;
99+
/** Benchmark scenario: `single_turn` (fixed-seq isl/osl) or `agentic_traces`. */
100+
benchmark_type: string;
101+
/** ISL in tokens — null for agentic_traces. With osl + benchmark_type this maps to a sequence. */
102+
isl: number | null;
103+
/** OSL in tokens — null for agentic_traces. */
104+
osl: number | null;
99105
}
100106

101107
export interface WorkflowInfoResponse {

packages/app/src/lib/utils.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
computeOutputCostFields,
1111
computeInputCostFields,
1212
filterRunsByModel,
13+
restrictRunsToScenario,
1314
getFrameworkLabel,
1415
getHardwareLabel,
1516
getDisplayLabel,
@@ -539,6 +540,51 @@ describe('filterRunsByModel', () => {
539540
});
540541
});
541542

543+
// ---------------------------------------------------------------------------
544+
// restrictRunsToScenario
545+
// ---------------------------------------------------------------------------
546+
describe('restrictRunsToScenario', () => {
547+
const runs = {
548+
'10': makeRun([['dsv4-fp4-b200-sglang']], { runId: '10' }), // agentic run
549+
'20': makeRun([['dsv4-fp4-b200-trt']], { runId: '20' }), // single_turn run
550+
};
551+
552+
it('keeps only runs present in the scenario coverage set', () => {
553+
const result = restrictRunsToScenario(runs, runs, new Set(['10']));
554+
expect(Object.keys(result!)).toEqual(['10']);
555+
});
556+
557+
it('falls back to the changelog-scoped list when coverage is empty', () => {
558+
const result = restrictRunsToScenario(runs, runs, new Set());
559+
expect(result).toBe(runs);
560+
});
561+
562+
it('pulls a scenario run from the raw map when the changelog list omits it', () => {
563+
// A run that shipped data without a changelog entry: filterRunsByModel drops
564+
// it, but it is still a valid scenario run the picker must show.
565+
const changelogFiltered = { '10': runs['10'] };
566+
const allRuns = runs;
567+
const result = restrictRunsToScenario(changelogFiltered, allRuns, new Set(['10', '20']));
568+
expect(Object.keys(result!).toSorted()).toEqual(['10', '20']);
569+
expect(result!['20']).toBe(runs['20']);
570+
});
571+
572+
it('prefers the changelog-scoped entry over the raw one', () => {
573+
const scoped = makeRun([['dsv4-fp4-b200-sglang']], { runId: '10', conclusion: 'scoped' });
574+
const result = restrictRunsToScenario({ '10': scoped }, runs, new Set(['10']));
575+
expect(result!['10']).toBe(scoped);
576+
});
577+
578+
it('falls back to the changelog list when no scenario run survives', () => {
579+
const result = restrictRunsToScenario(runs, runs, new Set(['999']));
580+
expect(result).toBe(runs);
581+
});
582+
583+
it('returns null changelog list unchanged', () => {
584+
expect(restrictRunsToScenario(null, null, new Set(['10']))).toBeNull();
585+
});
586+
});
587+
542588
// ===========================================================================
543589
// getFrameworkLabel
544590
// ===========================================================================

packages/app/src/lib/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,39 @@ export function filterRunsByModel(
288288
return Object.keys(fallback).length > 0 ? fallback : null;
289289
}
290290

291+
/**
292+
* Restrict the run-picker list to the runs that actually produced data for the
293+
* currently selected scenario, given `scenarioRunIds` (the run ids with coverage
294+
* for the selected model + scenario, from `scenarioRunIdsForDate`).
295+
*
296+
* `changelogFiltered` is the model/precision-scoped list (`filterRunsByModel`);
297+
* `allRuns` is the raw, date-wide map. For each scenario run we prefer the
298+
* changelog-scoped entry (its changelog is already precision-filtered) and fall
299+
* back to the raw entry — a run can ship benchmark data without a changelog
300+
* entry, so `filterRunsByModel` may omit it while it is still a legitimate
301+
* scenario run the user must be able to select.
302+
*
303+
* Fallbacks that keep the selector rendering:
304+
* - `scenarioRunIds` empty → no coverage data (dates predating per-run
305+
* coverage, or JSON/fixtures mode): return the changelog-scoped list
306+
* unchanged rather than blanking the picker.
307+
* - Non-empty coverage but zero surviving runs (shouldn't happen in practice)
308+
* → also fall back to the changelog-scoped list.
309+
*/
310+
export function restrictRunsToScenario(
311+
changelogFiltered: Record<string, RunInfo> | null,
312+
allRuns: Record<string, RunInfo> | null,
313+
scenarioRunIds: Set<string>,
314+
): Record<string, RunInfo> | null {
315+
if (scenarioRunIds.size === 0) return changelogFiltered;
316+
const restricted: Record<string, RunInfo> = {};
317+
for (const runId of scenarioRunIds) {
318+
const info = changelogFiltered?.[runId] ?? allRuns?.[runId];
319+
if (info) restricted[runId] = info;
320+
}
321+
return Object.keys(restricted).length > 0 ? restricted : changelogFiltered;
322+
}
323+
291324
/**
292325
* Computes missing energy fields (jTotal, jOutput, jInput) at runtime.
293326
* This handles backwards compatibility with historical data that doesn't have these fields.

0 commit comments

Comments
 (0)