Skip to content
Draft
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
12 changes: 12 additions & 0 deletions packages/app/cypress/component/scatter-graph.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,12 @@ describe('ScatterGraph', () => {
y: 260 - index * 40,
precision: Precision.FP4,
run_url: runUrl,
convergenceXMin: x * 0.96,
convergenceXMax: x * 1.04,
convergenceTimeSeconds: 1200,
convergenceRequests: 407,
convergenceMaxRelativeDeviation: 0.04,
convergence_tolerance_ratio: 0.05,
}),
),
hardwareConfig: hwConfig,
Expand Down Expand Up @@ -777,10 +783,16 @@ describe('ScatterGraph', () => {

cy.get('#test-scatter-dismiss-preview svg .unofficial-overlay-pt').should('have.length', 3);
cy.get('#test-scatter-dismiss-preview svg .overlay-roofline-path').should('exist');
cy.get('#test-scatter-dismiss-preview svg .convergence-range[data-source="overlay"]').should(
'exist',
);

cy.get('[data-testid="dismiss-preview"]').click();
cy.get('#test-scatter-dismiss-preview svg .unofficial-overlay-pt').should('not.exist');
cy.get('#test-scatter-dismiss-preview svg .overlay-roofline-path').should('not.exist');
cy.get('#test-scatter-dismiss-preview svg .convergence-range[data-source="overlay"]').should(
'not.exist',
);
});
});

Expand Down
165 changes: 163 additions & 2 deletions packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,15 @@ const percentileLadder = (prefix: string, base: number): Record<string, number>
[`std_${prefix}`]: base * 0.3,
});

const agenticMetrics = (conc: number): Record<string, number> => {
const agenticMetrics = (conc: number, stabilized = true): Record<string, number> => {
const scale = conc / 16;
const itl = 0.011 * scale;
const p75Ttft = 0.4 * scale * 1.2;
const p90Ttft = 0.4 * scale * 1.5;
const p75E2e = 8 * scale * 1.2;
const p90E2e = 8 * scale * 1.5;
const p75Interactivity = 1 / (itl * 1.2);
const p90Interactivity = 1 / (itl * 1.5);
return {
...percentileLadder('ttft', 0.4 * scale),
...percentileLadder('tpot', 0.012 * scale),
Expand All @@ -58,6 +64,63 @@ const agenticMetrics = (conc: number): Record<string, number> => {
output_tput_per_gpu: 210,
input_tput_per_gpu: 740,
total_tput_tps: 7600 * conc * 0.05,
observed_window_seconds: 600,
observed_window_expected_count: 6,
observed_window_count: 6,
observed_window_min_requests: Math.max(12, Math.round(conc * 1.25)),
root_trajectory_count: Math.max(3, Math.round(conc / 4)),
root_trajectory_kish_effective_count: Math.max(1.8, conc / 8),
root_trajectory_largest_share: Math.min(0.56, 8 / conc),
observed_window_p75_ttft_min: p75Ttft * 0.55,
observed_window_p75_ttft_max: p75Ttft * 1.45,
observed_window_p90_ttft_min: p90Ttft * 0.5,
observed_window_p90_ttft_max: p90Ttft * 1.5,
observed_window_p75_e2el_min: p75E2e * 0.75,
observed_window_p75_e2el_max: p75E2e * 1.25,
observed_window_p90_e2el_min: p90E2e * 0.7,
observed_window_p90_e2el_max: p90E2e * 1.3,
observed_window_p75_intvty_min: p75Interactivity * 0.72,
observed_window_p75_intvty_max: p75Interactivity * 1.28,
observed_window_p90_intvty_min: p90Interactivity * 0.68,
observed_window_p90_intvty_max: p90Interactivity * 1.32,
convergence_checkpoint_seconds: 300,
convergence_tolerance_ratio: 0.05,
convergence_min_confirmation_seconds: 1200,
convergence_horizon_seconds: 3600,
...(stabilized
? {
convergence_p75_ttft_time_seconds: 900,
convergence_p75_ttft_requests: Math.round(conc * 25),
convergence_p75_ttft_min: p75Ttft * 0.97,
convergence_p75_ttft_max: p75Ttft * 1.03,
convergence_p75_ttft_max_relative_deviation: 0.03,
convergence_p90_ttft_time_seconds: 1200,
convergence_p90_ttft_requests: Math.round(conc * 25.4),
convergence_p90_ttft_min: p90Ttft * 0.96,
convergence_p90_ttft_max: p90Ttft * 1.04,
convergence_p90_ttft_max_relative_deviation: 0.04,
convergence_p75_e2el_time_seconds: 900,
convergence_p75_e2el_requests: Math.round(conc * 25),
convergence_p75_e2el_min: p75E2e * 0.97,
convergence_p75_e2el_max: p75E2e * 1.03,
convergence_p75_e2el_max_relative_deviation: 0.03,
convergence_p90_e2el_time_seconds: 1200,
convergence_p90_e2el_requests: Math.round(conc * 25.4),
convergence_p90_e2el_min: p90E2e * 0.96,
convergence_p90_e2el_max: p90E2e * 1.04,
convergence_p90_e2el_max_relative_deviation: 0.04,
convergence_p75_intvty_time_seconds: 900,
convergence_p75_intvty_requests: Math.round(conc * 25),
convergence_p75_intvty_min: p75Interactivity * 0.97,
convergence_p75_intvty_max: p75Interactivity * 1.03,
convergence_p75_intvty_max_relative_deviation: 0.03,
convergence_p90_intvty_time_seconds: 1200,
convergence_p90_intvty_requests: Math.round(conc * 25.4),
convergence_p90_intvty_min: p90Interactivity * 0.96,
convergence_p90_intvty_max: p90Interactivity * 1.04,
convergence_p90_intvty_max_relative_deviation: 0.04,
}
: {}),
};
};

Expand Down Expand Up @@ -116,7 +179,7 @@ const agenticBenchmarks = agenticGpus.flatMap((g) =>
offload_mode: 'off',
benchmark_type: 'agentic_traces',
image: 'vllm/vllm-openai:v0.9.0',
metrics: agenticMetrics(conc),
metrics: agenticMetrics(conc, !(g.hardware === 'b200' && conc === 16)),
workers: null,
date: AGENTIC_DATE,
run_url: null,
Expand All @@ -131,12 +194,21 @@ const fixedSequenceBenchmarks = agenticBenchmarks.map((row, index) => ({
benchmark_type: 'single_turn',
}));

const interceptDashboardMetadata = () => {
cy.intercept('GET', '/api/v1/workflow-info*', {
body: { runs: [], changelogs: [], configs: [], runConfigs: [] },
}).as('workflowInfo');
cy.intercept('GET', '/api/v1/trace-availability*', { body: {} }).as('traceAvailability');
};

const interceptAgenticData = () => {
interceptDashboardMetadata();
cy.intercept('GET', '/api/v1/availability', { body: agenticAvailability }).as('availability');
cy.intercept('GET', '/api/v1/benchmarks*', { body: agenticBenchmarks }).as('benchmarks');
};

const interceptFixedSequenceData = () => {
interceptDashboardMetadata();
cy.intercept('GET', '/api/v1/availability', { body: agenticAvailability }).as('availability');
cy.intercept('GET', '/api/v1/benchmarks*', { body: fixedSequenceBenchmarks }).as('benchmarks');
};
Expand Down Expand Up @@ -191,6 +263,90 @@ describe('X-Axis Mode Toggle (inference chart)', () => {
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'Time To First Token');
});

it('renders post-stabilization spans with an explicit non-inferential caveat', () => {
cy.get(
'[data-testid="inference-chart-display"] [data-testid="convergence-range"][data-source="official"]',
).should('have.length.greaterThan', 0);
cy.get('[data-testid="convergence-range-key"]')
.should('contain.text', 'Cumulative span after ±5% stabilization')
.and('contain.text', 'retrospective')
.and('contain.text', 'not a confidence interval');

cy.get('[data-testid="scatter-graph"] svg .dot-group').then(($dots) => {
const stabilizedDot = [...$dots].find(
(dot) =>
(dot as unknown as SVGGElement & { __data__: { id?: number } }).__data__.id === 900001,
);
expect(stabilizedDot, 'stabilized fixture point').not.to.equal(undefined);
cy.wrap(stabilizedDot).find('.visible-shape').click({ force: true });
});
cy.get('[data-chart-tooltip]:visible')
.should('contain.text', 'Stabilized by 20 minutes at ±5%')
.and('contain.text', 'Post-stabilization span')
.and('contain.text', 'Maximum later deviation')
.and('contain.text', 'Retrospective within-run diagnostic')
.and('contain.text', 'not a confidence interval or rerun prediction')
.and('contain.text', 'Kish-effective root coverage');
});

it('renders no whisker when the cumulative estimate did not stabilize', () => {
cy.get(
'[data-testid="inference-chart-display"] [data-testid="convergence-range"][data-source="official"]',
).then(($ranges) => {
const hasUnstablePoint = [...$ranges].some(
(range) =>
(range as unknown as SVGGElement & { __data__: { point: { id?: number } } }).__data__
.point.id === 900000,
);
expect(hasUnstablePoint).to.equal(false);
});

cy.get('[data-testid="scatter-graph"] svg .dot-group').then(($dots) => {
const unstableDot = [...$dots].find(
(dot) =>
(dot as unknown as SVGGElement & { __data__: { id?: number } }).__data__.id === 900000,
);
expect(unstableDot, 'non-stabilized fixture point').not.to.equal(undefined);
cy.wrap(unstableDot).find('.visible-shape').click({ force: true });
});
cy.get('[data-chart-tooltip]:visible')
.should('contain.text', 'Not stabilized within the 60-minute run at ±5%')
.and('not.contain.text', 'Post-stabilization span');
});

it('keeps convergence ranges anchored through chart zoom', () => {
cy.get(
'[data-testid="inference-chart-display"] [data-testid="convergence-range"][data-source="official"] .convergence-range-stem',
)
.first()
.then(($stem) => {
const before = [$stem.attr('x1'), $stem.attr('x2')];
cy.get('[data-testid="scatter-graph"] svg')
.first()
.then(($svg) => {
const svg = $svg[0];
const bounds = svg.getBoundingClientRect();
svg.dispatchEvent(
new WheelEvent('wheel', {
deltaY: -240,
clientX: bounds.x + bounds.width / 2,
clientY: bounds.y + bounds.height / 2,
shiftKey: true,
bubbles: true,
cancelable: true,
}),
);
});
cy.wait(300);
cy.wrap($stem).should(($zoomedStem) => {
expect(
[$zoomedStem.attr('x1'), $zoomedStem.attr('x2')],
'range geometry after zoom',
).not.to.deep.equal(before);
});
});
});

it('switches the x-axis to E2E Latency and updates the heading', () => {
cy.get('[data-testid="x-axis-mode-e2e"]').click();
cy.get('[data-testid="x-axis-mode-e2e"]').should('have.attr', 'aria-selected', 'true');
Expand Down Expand Up @@ -371,6 +527,11 @@ describe('X-Axis Mode Toggle — overlay path (finding #8 regression guard)', ()
});
expect(total).to.be.greaterThan(0);
});
cy.get(
'[data-testid="inference-chart-display"] [data-testid="convergence-range"][data-source="overlay"]',
)
.should('have.length.greaterThan', 0)
.and('have.attr', 'data-run-index', '0');
});

it('normalized-e2e mode shows suppression banner for unofficial-run overlays', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import {
dedupeRowsToLatestPerConfig,
filterByGPU,
flipRooflineDirection,
remapChartPoint,
} from './useChartData';

import type { InferenceData } from '@/components/inference/types';

interface DedupeInput {
id: number;
hardware: string;
Expand Down Expand Up @@ -151,3 +154,25 @@ describe('flipRooflineDirection', () => {
}
});
});

describe('remapChartPoint', () => {
it('preserves the selected metric roof flag while flattening its y value', () => {
const point = {
x: 1,
y: 2,
hwKey: 'h100',
date: '2026-06-01',
tp: 100,
conc: 1,
precision: 'fp8',
p90_ttft: 3,
tpPerGpu: { y: 42, roof: true },
} as InferenceData;

expect(remapChartPoint(point, 'tpPerGpu', 'p90_ttft')).toMatchObject({
x: 3,
y: 42,
roof: true,
});
});
});
43 changes: 28 additions & 15 deletions packages/app/src/components/inference/hooks/useChartData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { isPersistedBenchmarkId } from '@/lib/benchmark-id';
import { calculateCostsForGpus, calculatePowerForGpus } from '@/lib/utils';
import { e2eFrontierWinners } from '@/components/inference/utils/e2eFrontier';
import { resolveXAxisField } from '@/components/inference/utils/resolveXAxisField';
import { withConvergenceRange } from '@/components/inference/utils/convergence-range';
import {
applyQuickFilters,
computeAvailableQuickFilters,
Expand Down Expand Up @@ -139,6 +140,32 @@ export function flipRooflineDirection(dir: RooflineDirection): RooflineDirection
return FLIP_MAP[dir];
}

/**
* Remap a transformed benchmark point onto the currently selected chart axes.
* Keep the selected y-metric's roofline marker on the flattened point: chart
* consumers read this marker after the metric object has been reduced to `y`.
*/
export function remapChartPoint(
point: InferenceData,
metricKey: YAxisMetricKey,
xAxisField: keyof AggDataEntry,
isOnE2eFrontier?: boolean,
): InferenceData {
const metric = point[metricKey] as { y: number; roof: boolean } | undefined;
const xCandidate = (point as Partial<AggDataEntry>)[xAxisField];

return withConvergenceRange(
{
...point,
x: typeof xCandidate === 'number' ? xCandidate : point.x,
y: metric?.y ?? point.y,
roof: metric?.roof ?? false,
isOnE2eFrontier,
},
String(xAxisField),
);
}

/** The dedup key fields a chart series is identified by. */
interface DedupeRow {
hardware: string;
Expand Down Expand Up @@ -526,25 +553,11 @@ export function useChartData(
? filteredData
.filter((d) => metricKey in d)
.map((d: InferenceData) => {
const yValue = (d[metricKey] as { y: number })?.y ?? d.y;
const roof = (d[metricKey] as { roof: boolean })?.roof ?? false;
// xAxisField is `keyof AggDataEntry`; InferenceData embeds those
// fields via `Partial<Omit<AggDataEntry, ...>>`, so a typed
// accessor catches a future field rename (silent fallthrough to
// d.x would otherwise mask the regression).
const xCandidate = (d as Partial<AggDataEntry>)[xAxisField];
const xValue = typeof xCandidate === 'number' ? xCandidate : d.x;
const isOnE2eFrontier =
e2eParetoSet === null
? undefined
: isPersistedBenchmarkId(d.id) && e2eParetoSet.has(d.id);
return {
...d,
x: xValue,
y: yValue,
roof,
isOnE2eFrontier,
};
return remapChartPoint(d, metricKey, xAxisField, isOnE2eFrontier);
})
// When TTFT is on the x-axis, apply the latency limit to filter
// overload outliers (fixed-seq conc=2048 rows with TTFT > 60s that
Expand Down
Loading