Skip to content

Commit 5402406

Browse files
authored
fix(inference): block cross-engine AgentX STP comparisons / 阻止 AgentX STP 跨引擎对比 (#549)
* fix(inference): block cross-engine AgentX STP comparisons * fix(inference): persist overlay scope selection * fix(inference): honor scoped legend selection * fix(inference): surface URL engine conflicts
1 parent 9ec8cc1 commit 5402406

15 files changed

Lines changed: 1300 additions & 303 deletions

packages/app/cypress/component/scatter-graph.cy.tsx

Lines changed: 304 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
import { useState } from 'react';
2+
import { GlobalFilterContext } from '@/components/GlobalFilterContext';
3+
import { InferenceContext } from '@/components/inference/InferenceContext';
4+
import { UnofficialRunContext } from '@/components/unofficial-run-provider';
15
import ScatterGraph from '@/components/inference/ui/ScatterGraph';
6+
import ChartDisplay from '@/components/inference/ui/ChartDisplay';
27
import { mountWithProviders } from '../support/test-utils';
38
import {
49
createMockInferenceData,
510
createMockChartDefinition,
611
createMockHardwareConfig,
12+
createMockGlobalFilterContext,
13+
createMockInferenceContext,
14+
createMockUnofficialRunContext,
715
} from '../support/mock-data';
8-
import { Precision } from '@/lib/data-mappings';
16+
import { Model, Precision, Sequence } from '@/lib/data-mappings';
17+
import { buildExclusion, resolveExclusionGroups } from '@/lib/exclusion';
918

1019
const defaultChartDef = createMockChartDefinition();
1120
const hwConfig = createMockHardwareConfig();
@@ -331,4 +340,298 @@ describe('ScatterGraph', () => {
331340
// No label should show the generic MTP token for M3.
332341
cy.get('#test-scatter-m3-eagle svg .line-label text').should('not.contain.text', 'MTP');
333342
});
343+
344+
it('removes a cross-engine AgentX STP overlay before rendering', () => {
345+
const chartDefinition = createMockChartDefinition({
346+
chartType: 'interactivity',
347+
y_tpPerGpu_roofline: 'upper_left',
348+
});
349+
const officialData = [8, 16, 32].map((x, index) =>
350+
createMockInferenceData({
351+
hwKey: 'b200_sglang',
352+
x,
353+
y: 320 - index * 40,
354+
precision: Precision.FP4,
355+
}),
356+
);
357+
const runUrl = 'https://github.com/x/y/actions/runs/agentx-vllm';
358+
const overlayData = {
359+
data: [8, 16, 32].map((x, index) =>
360+
createMockInferenceData({
361+
hwKey: 'h100_vllm',
362+
x,
363+
y: 260 - index * 40,
364+
precision: Precision.FP4,
365+
run_url: runUrl,
366+
}),
367+
),
368+
hardwareConfig: hwConfig,
369+
label: 'agentx-vllm',
370+
runUrl,
371+
};
372+
const exclusion = buildExclusion([
373+
{ suffix: null, stripPrefixes: ['dynamo-', 'mori-', 'llmd-', 'mooncake-'] },
374+
]);
375+
const namespacedExclusion = {
376+
familyOf: (key: string) =>
377+
exclusion.familyOf(key.startsWith('overlay:') ? key.slice('overlay:'.length) : key),
378+
groupOf: (key: string) =>
379+
exclusion.groupOf(key.startsWith('overlay:') ? key.slice('overlay:'.length) : key),
380+
};
381+
382+
mountWithProviders(
383+
<div style={{ width: 800, height: 600 }}>
384+
<ScatterGraph
385+
chartId="test-scatter-agentx-engine-guard"
386+
modelLabel="DeepSeek V4 Pro"
387+
data={officialData}
388+
xLabel="Concurrency"
389+
yLabel="Throughput / GPU (tok/s)"
390+
chartDefinition={chartDefinition}
391+
overlayData={overlayData}
392+
/>
393+
</div>,
394+
{
395+
inference: {
396+
hardwareConfig: hwConfig,
397+
activeHwTypes: new Set(['b200_sglang']),
398+
hwTypesWithData: new Set(['b200_sglang']),
399+
selectedModel: Model.DeepSeek_V4_Pro,
400+
selectedSequence: Sequence.AgenticTraces,
401+
selectedPrecisions: [Precision.FP4],
402+
showLineLabels: true,
403+
resolveComparisonSelection: (proposed, prev = new Set()) =>
404+
resolveExclusionGroups(proposed, prev, namespacedExclusion, 'keep-sticky'),
405+
},
406+
unofficial: {
407+
activeOverlayHwTypes: new Set(['h100_vllm']),
408+
allOverlayHwTypes: new Set(['h100_vllm']),
409+
},
410+
},
411+
);
412+
413+
cy.get('#test-scatter-agentx-engine-guard svg .roofline-path').should('exist');
414+
cy.get('#test-scatter-agentx-engine-guard svg .overlay-roofline-path').should('not.exist');
415+
cy.get('@setActiveOverlayHwTypes').should('have.been.called');
416+
});
417+
});
418+
419+
describe('ChartDisplay engine comparison guard', () => {
420+
it('keeps cross-engine AgentX STP rows out of table mode', () => {
421+
const chartDefinition = createMockChartDefinition({ chartType: 'interactivity' });
422+
const sglangRow = createMockInferenceData({
423+
hwKey: 'b200_sglang',
424+
hw: 'Official SGLang',
425+
model: Model.DeepSeek_V4_Pro,
426+
precision: Precision.FP4,
427+
});
428+
const vllmRow = createMockInferenceData({
429+
hwKey: 'h100_vllm',
430+
hw: 'Official vLLM',
431+
model: Model.DeepSeek_V4_Pro,
432+
precision: Precision.FP4,
433+
});
434+
const exclusion = buildExclusion([
435+
{ suffix: null, stripPrefixes: ['dynamo-', 'mori-', 'llmd-', 'mooncake-'] },
436+
]);
437+
const resolveSelection = (proposed: Set<string>, prev = new Set<string>()) =>
438+
resolveExclusionGroups(proposed, prev, exclusion, 'keep-sticky');
439+
440+
mountWithProviders(<ChartDisplay />, {
441+
inference: {
442+
graphs: [
443+
{
444+
model: Model.DeepSeek_V4_Pro,
445+
sequence: Sequence.AgenticTraces,
446+
chartDefinition,
447+
data: [sglangRow, vllmRow],
448+
},
449+
],
450+
selectedModel: Model.DeepSeek_V4_Pro,
451+
selectedSequence: Sequence.AgenticTraces,
452+
selectedXAxisMode: 'interactivity',
453+
activeHwTypes: new Set(['b200_sglang']),
454+
hwTypesWithData: new Set(['b200_sglang', 'h100_vllm']),
455+
resolveComparisonSelection: resolveSelection,
456+
},
457+
globalFilters: {
458+
selectedModel: Model.DeepSeek_V4_Pro,
459+
selectedSequence: Sequence.AgenticTraces,
460+
effectiveSequence: Sequence.AgenticTraces,
461+
},
462+
unofficial: {},
463+
});
464+
465+
cy.get('[data-testid="inference-table-view-btn"]').click();
466+
cy.get('[data-testid="inference-results-table"] tbody tr').should('have.length', 1);
467+
});
468+
469+
it('keeps an explicitly empty official legend out of table mode', () => {
470+
const chartDefinition = createMockChartDefinition({ chartType: 'interactivity' });
471+
const row = createMockInferenceData({
472+
hwKey: 'b200_sglang',
473+
model: Model.DeepSeek_V4_Pro,
474+
precision: Precision.FP4,
475+
});
476+
477+
mountWithProviders(<ChartDisplay />, {
478+
inference: {
479+
graphs: [
480+
{
481+
model: Model.DeepSeek_V4_Pro,
482+
sequence: Sequence.AgenticTraces,
483+
chartDefinition,
484+
data: [row],
485+
},
486+
],
487+
selectedModel: Model.DeepSeek_V4_Pro,
488+
selectedSequence: Sequence.AgenticTraces,
489+
selectedXAxisMode: 'interactivity',
490+
activeHwTypes: new Set(['b200_sglang']),
491+
hwTypesWithData: new Set(['b200_sglang']),
492+
},
493+
globalFilters: {
494+
selectedModel: Model.DeepSeek_V4_Pro,
495+
selectedSequence: Sequence.AgenticTraces,
496+
effectiveSequence: Sequence.AgenticTraces,
497+
},
498+
unofficial: { localOfficialOverride: new Set() },
499+
});
500+
501+
cy.get('[data-testid="inference-table-view-btn"]').click();
502+
cy.contains('No data available for the current filters.').should('be.visible');
503+
cy.get('[data-testid="inference-results-table"]').should('not.exist');
504+
});
505+
506+
it('commits a new table overlay scope and preserves an explicit empty selection', () => {
507+
const chartDefinition = createMockChartDefinition({ chartType: 'interactivity' });
508+
const exclusion = buildExclusion([
509+
{ suffix: null, stripPrefixes: ['dynamo-', 'mori-', 'llmd-', 'mooncake-'] },
510+
]);
511+
const namespacedExclusion = {
512+
familyOf: (key: string) =>
513+
exclusion.familyOf(key.startsWith('overlay:') ? key.slice('overlay:'.length) : key),
514+
groupOf: (key: string) =>
515+
exclusion.groupOf(key.startsWith('overlay:') ? key.slice('overlay:'.length) : key),
516+
};
517+
const resolveSelection = (proposed: Set<string>, prev = new Set<string>()) =>
518+
resolveExclusionGroups(proposed, prev, namespacedExclusion, 'keep-sticky');
519+
const runInfo = {
520+
id: 123,
521+
name: 'agentx-scope-test',
522+
branch: 'agentx-scope-test',
523+
sha: 'abc123',
524+
createdAt: '2026-07-10T00:00:00Z',
525+
url: 'https://github.com/x/y/actions/runs/123',
526+
conclusion: 'success',
527+
status: 'completed',
528+
isNonMainBranch: true,
529+
};
530+
const baseInference = createMockInferenceContext();
531+
const baseGlobalFilters = createMockGlobalFilterContext();
532+
const baseUnofficial = createMockUnofficialRunContext();
533+
534+
function OverlayScopeHarness() {
535+
const [secondScope, setSecondScope] = useState(false);
536+
const [activeOverlayKeys, setActiveOverlayKeys] = useState(new Set(['h100_sglang']));
537+
const [, setRenderVersion] = useState(0);
538+
const model = secondScope ? Model.DeepSeek_R1 : Model.DeepSeek_V4_Pro;
539+
const officialKey = secondScope ? 'h100_vllm' : 'b200_sglang';
540+
const overlayKeys = secondScope
541+
? ['b200_vllm', 'h200_sglang']
542+
: ['h100_sglang', 'h200_sglang'];
543+
const officialRows = [
544+
createMockInferenceData({
545+
hwKey: officialKey,
546+
model,
547+
precision: Precision.FP4,
548+
}),
549+
];
550+
const overlayRows = overlayKeys.map((hwKey, index) =>
551+
createMockInferenceData({
552+
hwKey,
553+
model,
554+
precision: Precision.FP4,
555+
x: 8 + index * 8,
556+
run_url: runInfo.url,
557+
}),
558+
);
559+
const inference = {
560+
...baseInference,
561+
graphs: [
562+
{
563+
model,
564+
sequence: Sequence.AgenticTraces,
565+
chartDefinition,
566+
data: officialRows,
567+
},
568+
],
569+
selectedModel: model,
570+
selectedSequence: Sequence.AgenticTraces,
571+
selectedXAxisMode: 'interactivity' as const,
572+
selectedXAxisMetric: 'p90_ttft',
573+
activeHwTypes: new Set([officialKey]),
574+
hwTypesWithData: new Set([officialKey]),
575+
resolveComparisonSelection: resolveSelection,
576+
};
577+
const globalFilters = {
578+
...baseGlobalFilters,
579+
selectedModel: model,
580+
selectedSequence: Sequence.AgenticTraces,
581+
effectiveSequence: Sequence.AgenticTraces,
582+
};
583+
const unofficial = {
584+
...baseUnofficial,
585+
isUnofficialRun: true,
586+
unofficialRunInfo: runInfo,
587+
unofficialRunInfos: [runInfo],
588+
runIndexByUrl: { [runInfo.url]: 0, [String(runInfo.id)]: 0 },
589+
getOverlayData: () => ({ data: overlayRows, hardwareConfig: hwConfig }),
590+
activeOverlayHwTypes: activeOverlayKeys,
591+
setActiveOverlayHwTypes: setActiveOverlayKeys,
592+
allOverlayHwTypes: new Set(['h100_sglang', 'h200_sglang', 'b200_vllm']),
593+
};
594+
595+
return (
596+
<GlobalFilterContext.Provider value={globalFilters}>
597+
<UnofficialRunContext.Provider value={unofficial}>
598+
<InferenceContext.Provider value={inference}>
599+
<button data-testid="change-overlay-scope" onClick={() => setSecondScope(true)}>
600+
Change scope
601+
</button>
602+
<button
603+
data-testid="clear-overlay-scope"
604+
onClick={() => setActiveOverlayKeys(new Set())}
605+
>
606+
Clear overlays
607+
</button>
608+
<button
609+
data-testid="rerender-overlay-scope"
610+
onClick={() => setRenderVersion((version) => version + 1)}
611+
>
612+
Rerender
613+
</button>
614+
<ChartDisplay />
615+
</InferenceContext.Provider>
616+
</UnofficialRunContext.Provider>
617+
</GlobalFilterContext.Provider>
618+
);
619+
}
620+
621+
mountWithProviders(<OverlayScopeHarness />);
622+
cy.get('[data-testid="inference-table-view-btn"]').click();
623+
cy.get('[data-testid="inference-results-table"] tbody tr').should('have.length', 2);
624+
625+
cy.get('[data-testid="change-overlay-scope"]').click();
626+
cy.get('[data-testid="inference-results-table"] tbody tr').should('have.length', 2);
627+
cy.get('[data-testid="rerender-overlay-scope"]').click();
628+
cy.get('[data-testid="inference-results-table"] tbody tr').should('have.length', 2);
629+
630+
cy.get('[data-testid="clear-overlay-scope"]').click();
631+
cy.get('[data-testid="inference-results-table"] tbody tr').should('have.length', 1);
632+
cy.get('[data-testid="rerender-overlay-scope"]').click();
633+
cy.get('[data-testid="inference-results-table"] tbody tr').should('have.length', 1);
634+
cy.get('[data-testid="inference-chart-view-btn"]').click();
635+
cy.get('#chart-0 svg .unofficial-overlay-pt').should('not.exist');
636+
});
334637
});

packages/app/cypress/e2e/gpu-compare-agentic-detail.cy.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import { unlockAgenticGate } from '../support/e2e';
1313
const DEFAULT_MODEL_DB_KEY = 'dsv4'; // DeepSeek-V4-Pro
1414
const AGENTIC_DATE = '2026-06-12';
1515

16-
// Two GPUs with agentic + single_turn entries so the scenario selector resolves
16+
// Three configs with agentic + single_turn entries so the scenario selector resolves
1717
// to agentic (agentic preferred when both types exist for the same model).
1818
const AGENTIC_HARDWARE = [
1919
{ hardware: 'b200', framework: 'vllm', disagg: false },
20+
{ hardware: 'b200', framework: 'sglang', disagg: false },
2021
{ hardware: 'b300', framework: 'vllm', disagg: false },
2122
];
2223

@@ -185,4 +186,29 @@ describe('GPU comparison agentic point detail', () => {
185186
});
186187
cy.location('pathname').should('eq', '/inference');
187188
});
189+
190+
it('surfaces automatic resolution of conflicting GPU URL state', () => {
191+
cy.intercept('GET', '/api/v1/availability', { body: agenticAvailability }).as(
192+
'agenticAvailability',
193+
);
194+
cy.intercept('GET', '/api/v1/benchmarks*', { body: agenticBenchmarks }).as('agenticBenchmarks');
195+
196+
cy.visit(
197+
'/inference?g_model=DeepSeek-V4-Pro&i_seq=agentic-traces&i_prec=fp4&i_gpus=b200_sglang,b200_vllm&i_dates=2026-06-12&i_dstart=2026-06-12&i_dend=2026-06-12',
198+
{
199+
onBeforeLoad(win) {
200+
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
201+
unlockAgenticGate(win);
202+
},
203+
},
204+
);
205+
206+
cy.get('[data-testid="engine-comparison-conflict-toast"]')
207+
.should('be.visible')
208+
.and('contain.text', 'Kept SGLang and removed vLLM configs');
209+
cy.get('[data-testid="gpu-multiselect"] [data-slot="select-trigger"]')
210+
.should('contain.text', 'SGLang')
211+
.and('not.contain.text', 'vLLM');
212+
cy.contains('button', 'Jun 12, 2026').should('be.visible');
213+
});
188214
});

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
} from '@/components/reliability/types';
1616
import type { GlobalFilterContextType } from '@/components/GlobalFilterContext';
1717
import type { UnofficialRunContextType } from '@/components/unofficial-run-provider';
18+
import { computeToggle } from '@/hooks/useTogglableSet';
1819
import { Model, Sequence, Precision } from '@/lib/data-mappings';
1920
import React from 'react';
2021

@@ -165,6 +166,12 @@ export function createMockInferenceContext(
165166
toggleHwType: namedStub('toggleHwType'),
166167
removeHwType: namedStub('removeHwType'),
167168
selectAllHwTypes: namedStub('selectAllHwTypes'),
169+
resolveComparisonSelection: (proposed) => ({
170+
result: proposed,
171+
keptGroup: null,
172+
droppedGroups: [],
173+
}),
174+
toggleComparisonSelection: (prev, item, allItems) => computeToggle(prev, item, allItems),
168175
toggleActiveDate: namedStub('toggleActiveDate'),
169176
removeActiveDate: namedStub('removeActiveDate'),
170177
selectAllActiveDates: namedStub('selectAllActiveDates'),

0 commit comments

Comments
 (0)