diff --git a/packages/app/cypress/e2e/agentic-point-time-series.cy.ts b/packages/app/cypress/e2e/agentic-point-time-series.cy.ts index ebb7bba3c..7a2600e82 100644 --- a/packages/app/cypress/e2e/agentic-point-time-series.cy.ts +++ b/packages/app/cypress/e2e/agentic-point-time-series.cy.ts @@ -23,11 +23,54 @@ const timelineRequest = ( ...overrides, }); +const benchmarkSiblings = { + sku: { + hardware: 'b300', + framework: 'vllm', + model: 'kimik3', + precision: 'fp4', + spec_method: 'mtp', + benchmark_type: 'agentic_traces', + github_run_id: 31893747354, + date: '2026-08-15', + dataset_slug: null, + }, + siblings: [ + { + id: 206885, + conc: 8, + offload_mode: 'off', + decode_tp: 8, + decode_ep: 1, + decode_pp: 1, + decode_dcp_size: 8, + decode_pcp_size: 1, + decode_dp_attention: false, + decode_num_workers: 0, + prefill_tp: 8, + prefill_ep: 1, + prefill_pp: 1, + prefill_dcp_size: 8, + prefill_pcp_size: 1, + prefill_dp_attention: false, + prefill_num_workers: 0, + num_prefill_gpu: 8, + num_decode_gpu: 8, + disagg: false, + is_multinode: false, + tput_per_gpu: 1.2, + total_requests: 395, + is_current: true, + has_trace: true, + }, + ], +}; + describe('Agentic point request metric time series', () => { before(() => { cy.intercept('GET', '/api/v1/trace-histograms*', { body: {} }); cy.intercept('GET', '/api/v1/trace-server-metrics*', { body: null }); - cy.intercept('GET', '/api/v1/benchmark-siblings*', { statusCode: 404 }); + cy.intercept('GET', '/api/v1/benchmark-siblings*', { body: benchmarkSiblings }); cy.intercept('GET', '/api/v1/request-timeline*', { body: { version: 3, @@ -68,6 +111,10 @@ describe('Agentic point request metric time series', () => { cy.visit('/inference/agentic/206885', { onBeforeLoad: unlockAgenticGate }); }); + it('uses the shared topology label for the active agentic point', () => { + cy.contains('button', 'TP8/DCP8 • c=8').should('be.visible'); + }); + it('renders rolling P90 interactivity and TTFT by default using profiling requests only', () => { cy.get('[data-testid="interactivity-over-time-chart"]').within(() => { cy.contains('h2', 'Interactivity over time').should('be.visible'); diff --git a/packages/app/cypress/e2e/unofficial-watermark.cy.ts b/packages/app/cypress/e2e/unofficial-watermark.cy.ts index 043dc516f..b4d82ed37 100644 --- a/packages/app/cypress/e2e/unofficial-watermark.cy.ts +++ b/packages/app/cypress/e2e/unofficial-watermark.cy.ts @@ -20,6 +20,10 @@ describe('Unofficial-run watermark', () => { benchmarks: benchmarks.map((row: Record) => ({ ...row, is_multinode: true, + prefill_tp: 8, + prefill_ep: 1, + decode_tp: 8, + decode_ep: 1, metrics: { ...(row.metrics as Record | undefined), kv_offloading: 'dram', @@ -27,6 +31,8 @@ describe('Unofficial-run watermark', () => { kv_p2p_transfer: 'nixl', router_name: 'sglang-router', router_version: '0.3.2', + dcp_size: 8, + pcp_size: 4, server_gpu_cache_hit_rate: 0.875, }, run_url: runUrl, @@ -79,9 +85,17 @@ describe('Unofficial-run watermark', () => { expect(tooltip).to.contain.text('KV Offload Engine: HiCache'); expect(tooltip).to.contain.text('KV Transfer Engine: NIXL'); expect(tooltip).to.contain.text('Router: SGLang Router 0.3.2'); + expect(tooltip).to.contain.text('Decode Context Parallelism (DCP): 8'); + expect(tooltip).to.contain.text('Prefill Context Parallelism (PCP): 4'); expect(tooltip).to.contain.text('Chip Cache Hit Rate: 87.5%'); }); + cy.get('#scatter-parallelism-labels').click(); + cy.get('[data-testid="scatter-graph"] .unofficial-overlay-pt') + .first() + .find('.overlay-label') + .should('contain.text', 'TP8/DCP8/PCP4'); + cy.get('[data-testid="scatter-graph"]').first().scrollIntoView(); cy.screenshot('unofficial-watermark', { capture: 'viewport' }); diff --git a/packages/app/src/app/api/v1/benchmark-siblings/route.ts b/packages/app/src/app/api/v1/benchmark-siblings/route.ts index 1e7482278..a5a5ce5ec 100644 --- a/packages/app/src/app/api/v1/benchmark-siblings/route.ts +++ b/packages/app/src/app/api/v1/benchmark-siblings/route.ts @@ -13,9 +13,9 @@ export const dynamic = 'force-dynamic'; const getCachedSiblings = cachedQuery( (id: number): Promise => getBenchmarkSiblings(getDb(), id), - // v2 adds PP and makes the aggregate topology explicit. Roll the blob - // namespace so old TP0 sibling payloads cannot survive the schema change. - 'benchmark-siblings-v2', + // v3 adds DCP/PCP. Roll the blob namespace so cached sibling payloads gain + // the complete parallelism mapping used by chart point labels. + 'benchmark-siblings-v3', ); /** diff --git a/packages/app/src/components/inference/agentic-point/sibling-nav.test.ts b/packages/app/src/components/inference/agentic-point/sibling-nav.test.ts index 2c1b562df..c69dc5b35 100644 --- a/packages/app/src/components/inference/agentic-point/sibling-nav.test.ts +++ b/packages/app/src/components/inference/agentic-point/sibling-nav.test.ts @@ -12,11 +12,15 @@ function sibling(overrides: Partial = {}): BenchmarkSibling { decode_tp: 0, decode_ep: 0, decode_pp: 1, + decode_dcp_size: null, + decode_pcp_size: null, decode_dp_attention: false, decode_num_workers: 0, prefill_tp: 8, prefill_ep: 1, prefill_pp: 2, + prefill_dcp_size: null, + prefill_pcp_size: null, prefill_dp_attention: false, prefill_num_workers: 1, num_prefill_gpu: 16, @@ -40,6 +44,19 @@ describe('chipLabel', () => { expect(chipLabel(sibling({ decode_tp: 8, decode_ep: 1 }))).toBe('TP8PP2 • c=2'); }); + it('uses the shared point-label format for aggregate DCP and PCP', () => { + expect( + chipLabel( + sibling({ + prefill_dcp_size: 8, + decode_dcp_size: 8, + prefill_pcp_size: 1, + decode_pcp_size: 1, + }), + ), + ).toBe('TP8PP2/DCP8 • c=2'); + }); + it('keeps prefill and decode roles separate only for disaggregated serving', () => { expect( chipLabel( @@ -48,9 +65,13 @@ describe('chipLabel', () => { decode_tp: 8, decode_ep: 1, decode_pp: 1, + prefill_dcp_size: 2, + prefill_pcp_size: 4, + decode_dcp_size: 8, + decode_pcp_size: 1, decode_num_workers: 1, }), ), - ).toBe('1xTP8PP2+1xTP8 • c=2'); + ).toBe('1xTP8PP2/DCP2/PCP4+1xTP8/DCP8 • c=2'); }); }); diff --git a/packages/app/src/components/inference/agentic-point/sibling-nav.tsx b/packages/app/src/components/inference/agentic-point/sibling-nav.tsx index 4dca8fceb..e417790aa 100644 --- a/packages/app/src/components/inference/agentic-point/sibling-nav.tsx +++ b/packages/app/src/components/inference/agentic-point/sibling-nav.tsx @@ -5,7 +5,10 @@ import { usePathname, useRouter } from 'next/navigation'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import type { BenchmarkSibling, BenchmarkSku } from '@/hooks/api/use-benchmark-siblings'; -import { parallelismLabel } from '@/components/inference/utils/parallelism-label'; +import { + meaningfulParallelismSize, + parallelismLabel, +} from '@/components/inference/utils/parallelism-label'; import { Select, SelectContent, @@ -71,6 +74,12 @@ export function chipLabel(s: BenchmarkSibling): string { !s.disagg && (s.prefill_pp !== null || s.decode_pp !== null) ? Math.max(s.prefill_pp ?? 1, s.decode_pp ?? 1) : undefined; + const aggregateDcp = s.disagg + ? undefined + : meaningfulParallelismSize(s.prefill_dcp_size, s.decode_dcp_size); + const aggregatePcp = s.disagg + ? undefined + : meaningfulParallelismSize(s.prefill_pcp_size, s.decode_pcp_size); // Same parallelism labeler the chart points use (TP/EP/TEP/DEP/DPA…). const parallel = parallelismLabel({ tp: usePrefill ? s.prefill_tp : s.decode_tp, @@ -80,17 +89,31 @@ export function chipLabel(s: BenchmarkSibling): string { ? (s.prefill_pp ?? undefined) : (s.decode_pp ?? undefined) : aggregatePp, + dcp: s.disagg + ? usePrefill + ? (s.prefill_dcp_size ?? undefined) + : (s.decode_dcp_size ?? undefined) + : aggregateDcp, + pcp: s.disagg + ? usePrefill + ? (s.prefill_pcp_size ?? undefined) + : (s.decode_pcp_size ?? undefined) + : aggregatePcp, dpAttention: usePrefill ? s.prefill_dp_attention : s.decode_dp_attention, disagg: s.disagg, isMultinode: s.is_multinode, prefillTp: s.prefill_tp, prefillEp: s.prefill_ep, prefillPp: s.prefill_pp ?? undefined, + prefillDcp: s.prefill_dcp_size ?? undefined, + prefillPcp: s.prefill_pcp_size ?? undefined, prefillDpAttention: s.prefill_dp_attention, prefillNumWorkers: s.prefill_num_workers, decodeTp: s.decode_tp, decodeEp: s.decode_ep, decodePp: s.decode_pp ?? undefined, + decodeDcp: s.decode_dcp_size ?? undefined, + decodePcp: s.decode_pcp_size ?? undefined, decodeDpAttention: s.decode_dp_attention, decodeNumWorkers: s.decode_num_workers, }); @@ -111,12 +134,16 @@ const SORT_OPTIONS: { value: SortMode; label: string }[] = [ // Group key for the "parallelism" sort: ep first (so TP/EP1 sorts ahead of // EP/TEP/DEP groups), then tp, then dp-attention, then disagg — every config // of one parallelism lands together, ordered by concurrency within. -const parallelRank = (s: BenchmarkSibling): [number, number, number, number, number] => { +const parallelRank = ( + s: BenchmarkSibling, +): [number, number, number, number, number, number, number] => { const usePrefill = !s.disagg && s.decode_tp <= 0 && s.prefill_tp > 0; return [ usePrefill ? s.prefill_ep : s.decode_ep, usePrefill ? s.prefill_tp : s.decode_tp, (usePrefill ? s.prefill_pp : s.decode_pp) ?? 1, + (usePrefill ? s.prefill_dcp_size : s.decode_dcp_size) ?? 1, + (usePrefill ? s.prefill_pcp_size : s.decode_pcp_size) ?? 1, (usePrefill ? s.prefill_dp_attention : s.decode_dp_attention) ? 1 : 0, s.disagg ? 1 : 0, ]; diff --git a/packages/app/src/components/inference/types.ts b/packages/app/src/components/inference/types.ts index af2f4e5b1..b5554a684 100644 --- a/packages/app/src/components/inference/types.ts +++ b/packages/app/src/components/inference/types.ts @@ -191,6 +191,14 @@ export interface AggDataEntry { decode_ep?: number; /** Decode-side pipeline parallelism — see {@link AggDataEntry.pp}. */ decode_pp?: number; + /** Prefill worker's decode-context parallel width, when emitted by the runtime. */ + prefill_dcp_size?: number; + /** Decode worker's decode-context parallel width, when emitted by the runtime. */ + decode_dcp_size?: number; + /** Prefill worker's prefill-context parallel width, when emitted by the runtime. */ + prefill_pcp_size?: number; + /** Decode worker's prefill-context parallel width, when emitted by the runtime. */ + decode_pcp_size?: number; decode_dp_attention?: boolean | string; decode_num_workers?: number; image?: string; diff --git a/packages/app/src/components/inference/utils/parallelism-label.test.ts b/packages/app/src/components/inference/utils/parallelism-label.test.ts index 643e911a6..a05be01c3 100644 --- a/packages/app/src/components/inference/utils/parallelism-label.test.ts +++ b/packages/app/src/components/inference/utils/parallelism-label.test.ts @@ -31,6 +31,17 @@ describe('configSegmentLabel', () => { expect(configSegmentLabel(8, 1, false, 0)).toBe('TP8'); expect(configSegmentLabel(8, 1, false, undefined)).toBe('TP8'); }); + + it('appends non-default DCP and PCP as slash-delimited segments', () => { + expect(configSegmentLabel(8, 1, false, 1, 8, 1)).toBe('TP8/DCP8'); + expect(configSegmentLabel(8, 1, false, 1, 8, 4)).toBe('TP8/DCP8/PCP4'); + expect(configSegmentLabel(8, 8, false, 2, 8, 4)).toBe('TEP8PP2/DCP8/PCP4'); + }); + + it('omits default DCP and PCP widths', () => { + expect(configSegmentLabel(8, 1, false, undefined, 1, 1)).toBe('TP8'); + expect(configSegmentLabel(8, 1, false, undefined, 0, undefined)).toBe('TP8'); + }); }); describe('parallelismLabel', () => { @@ -94,6 +105,27 @@ describe('parallelismLabel', () => { ).toBe('2xTP8PP2+1xTP8'); }); + it('applies per-role DCP and PCP to multinode-disagg segments', () => { + expect( + parallelismLabel({ + tp: 8, + ep: 1, + disagg: true, + isMultinode: true, + prefillTp: 8, + prefillEp: 1, + prefillDcp: 2, + prefillPcp: 4, + prefillNumWorkers: 1, + decodeTp: 8, + decodeEp: 1, + decodeDcp: 8, + decodePcp: 1, + decodeNumWorkers: 1, + }), + ).toBe('1xTP8/DCP2/PCP4+1xTP8/DCP8'); + }); + it('drops the decode segment when the decode pool is absent (0 workers, tp 0)', () => { // Prefill-only agentic multinode runs (e.g. Kimi-K3 TP8 PP2 on b200-dgxc) // emit decode_num_workers=0 / decode_tp=0 — the label is just the active diff --git a/packages/app/src/components/inference/utils/parallelism-label.ts b/packages/app/src/components/inference/utils/parallelism-label.ts index c14a90c37..a4e95efd7 100644 --- a/packages/app/src/components/inference/utils/parallelism-label.ts +++ b/packages/app/src/components/inference/utils/parallelism-label.ts @@ -1,6 +1,6 @@ /** * Shared parallelism-config labeling — the single source of truth for the - * short "TP8 / EP8 / TEP8 / DEP8 / DPAEP8 / 2xEP4+1xDPAEP32" labels. + * short "TP8 / TP8/DCP8 / EP8 / TEP8 / DEP8 / 2xEP4+1xDPAEP32" labels. * * Used by the scatter/GPU chart point labels (via getPointLabel) and the * agentic detail page's sibling navigator chips, so both surfaces describe a @@ -13,22 +13,38 @@ * - tp == ep and dp-attn true: "DEP{N}" * - ep > 1 (tp != ep): "EP{ep}" or "DPAEP{ep}" * - ep <= 1 (or no EP): "TP{tp}" or "DPATP{tp}" - * - pp > 1 appends a "PP{pp}" suffix (e.g. "TP8PP2"); pp <= 1 or absent - * renders nothing so pre-PP labels stay byte-identical. + * - pp > 1 appends a "PP{pp}" suffix (e.g. "TP8PP2"). + * - dcp/pcp > 1 append slash-delimited context-parallel segments + * (e.g. "TP8/DCP8" or "TP8/DCP8/PCP4"). + * - Values <= 1 or absent render nothing, preserving legacy labels. */ +export const meaningfulParallelismSize = ( + ...values: (number | null | undefined)[] +): number | undefined => { + const sizes = values.filter( + (value): value is number => value !== null && value !== undefined && value > 1, + ); + return sizes.length > 0 ? Math.max(...sizes) : undefined; +}; + export const configSegmentLabel = ( tp: number, ep: number | undefined, dpAttention: boolean | undefined, pp?: number, + dcp?: number, + pcp?: number, ): string => { const ppSuffix = pp !== null && pp !== undefined && pp > 1 ? `PP${pp}` : ''; + const dcpSuffix = dcp !== null && dcp !== undefined && dcp > 1 ? `/DCP${dcp}` : ''; + const pcpSuffix = pcp !== null && pcp !== undefined && pcp > 1 ? `/PCP${pcp}` : ''; + const suffix = `${ppSuffix}${dcpSuffix}${pcpSuffix}`; if (ep !== null && ep !== undefined && ep > 1 && tp === ep) { - return `${dpAttention ? 'DEP' : 'TEP'}${tp}${ppSuffix}`; + return `${dpAttention ? 'DEP' : 'TEP'}${tp}${suffix}`; } const dpaPrefix = dpAttention ? 'DPA' : ''; - if (ep === null || ep === undefined || ep <= 1) return `${dpaPrefix}TP${tp}${ppSuffix}`; - return `${dpaPrefix}EP${ep}${ppSuffix}`; + if (ep === null || ep === undefined || ep <= 1) return `${dpaPrefix}TP${tp}${suffix}`; + return `${dpaPrefix}EP${ep}${suffix}`; }; /** Parallelism params for one benchmark config, framework-agnostic. */ @@ -37,17 +53,25 @@ export interface ParallelismFields { ep?: number; /** Pipeline parallelism. Only rendered when > 1. */ pp?: number; + /** Decode-context parallelism. Only rendered when > 1. */ + dcp?: number; + /** Prefill-context parallelism. Only rendered when > 1. */ + pcp?: number; dpAttention?: boolean; disagg?: boolean; isMultinode?: boolean; prefillTp?: number; prefillEp?: number; prefillPp?: number; + prefillDcp?: number; + prefillPcp?: number; prefillDpAttention?: boolean; prefillNumWorkers?: number; decodeTp?: number; decodeEp?: number; decodePp?: number; + decodeDcp?: number; + decodePcp?: number; decodeDpAttention?: boolean; decodeNumWorkers?: number; } @@ -76,12 +100,16 @@ export const parallelismLabel = (f: ParallelismFields): string => { f.prefillEp ?? f.ep, f.prefillDpAttention ?? f.dpAttention, f.prefillPp ?? f.pp, + f.prefillDcp ?? f.dcp, + f.prefillPcp ?? f.pcp, ); const decodeLabel = configSegmentLabel( f.decodeTp ?? f.tp, f.decodeEp ?? f.ep, f.decodeDpAttention ?? f.dpAttention, f.decodePp ?? f.pp, + f.decodeDcp ?? f.dcp, + f.decodePcp ?? f.pcp, ); const pw = f.prefillNumWorkers ?? 1; const dw = f.decodeNumWorkers ?? 1; @@ -92,5 +120,5 @@ export const parallelismLabel = (f: ParallelismFields): string => { return `${pw}x${prefillLabel}+${dw}x${decodeLabel}`; } - return configSegmentLabel(f.tp, f.ep, f.dpAttention, f.pp); + return configSegmentLabel(f.tp, f.ep, f.dpAttention, f.pp, f.dcp, f.pcp); }; diff --git a/packages/app/src/components/inference/utils/tooltip-parallelism.test.ts b/packages/app/src/components/inference/utils/tooltip-parallelism.test.ts index 1d0ae0b5a..1e7b671dd 100644 --- a/packages/app/src/components/inference/utils/tooltip-parallelism.test.ts +++ b/packages/app/src/components/inference/utils/tooltip-parallelism.test.ts @@ -142,6 +142,41 @@ describe('tooltip parallelism — standard (ep field present)', () => { } }); + it('shows aggregate DCP and PCP widths when they are greater than one', () => { + const html = generateTooltipContent( + tooltipConfig({ + data: pt({ + tp: 8, + ep: 1, + prefill_dcp_size: 1, + decode_dcp_size: 8, + prefill_pcp_size: 4, + decode_pcp_size: 1, + }), + }), + ); + + expect(html).toContain('Decode Context Parallelism (DCP): 8'); + expect(html).toContain('Prefill Context Parallelism (PCP): 4'); + }); + + it('hides default DCP and PCP widths', () => { + const html = generateTooltipContent( + tooltipConfig({ + data: pt({ + tp: 8, + ep: 1, + prefill_dcp_size: 1, + decode_dcp_size: 1, + prefill_pcp_size: 1, + decode_pcp_size: 1, + }), + }), + ); + + expect(html).not.toContain('Context Parallelism'); + }); + it('omits Expert Parallelism line when ep is null', () => { const d = pt({ tp: 4 }); (d as any).ep = null; @@ -236,6 +271,33 @@ describe('tooltip parallelism — multinode disagg', () => { expect(html).not.toContain('PP: 1'); }); + it('shows per-role DCP and PCP in disaggregated tooltips', () => { + const html = generateTooltipContent( + tooltipConfig({ + data: pt({ + tp: 16, + ep: 1, + is_multinode: true, + disagg: true, + prefill_tp: 8, + prefill_ep: 1, + prefill_dcp_size: 2, + prefill_pcp_size: 4, + decode_tp: 8, + decode_ep: 1, + decode_dcp_size: 8, + decode_pcp_size: 1, + num_prefill_gpu: 8, + num_decode_gpu: 8, + }), + }), + ); + + expect(html).toMatch(/Prefill:<\/strong>[^<]*DCP: 2, PCP: 4,/u); + expect(html).toMatch(/Decode:<\/strong>[^<]*DCP: 8,/u); + expect(html).not.toContain('PCP: 1'); + }); + it('shows "?" for missing GPU count fields', () => { const html = generateTooltipContent( tooltipConfig({ @@ -291,6 +353,18 @@ describe('tooltip parallelism — zh locale', () => { expect(html).not.toContain('Tensor Parallelism'); }); + it('localizes DCP and PCP labels', () => { + const html = generateTooltipContent( + tooltipConfig({ + data: pt({ tp: 8, ep: 1, decode_dcp_size: 8, prefill_pcp_size: 4 }), + locale: 'zh', + }), + ); + + expect(html).toContain('解码上下文并行 (DCP)'); + expect(html).toContain('预填充上下文并行 (PCP)'); + }); + it('localizes the old-data parallelism strategy line', () => { const html = generateTooltipContent(tooltipConfig({ data: pt({ tp: 4 }), locale: 'zh' })); expect(html).toContain('并行策略'); diff --git a/packages/app/src/components/inference/utils/tooltip-utils.test.ts b/packages/app/src/components/inference/utils/tooltip-utils.test.ts index 7edc1268f..daa48b26a 100644 --- a/packages/app/src/components/inference/utils/tooltip-utils.test.ts +++ b/packages/app/src/components/inference/utils/tooltip-utils.test.ts @@ -99,6 +99,25 @@ describe('getPointLabel', () => { expect(getPointLabel(pt({ tp: 4, ep: 1 }))).toBe('TP4'); }); + it('includes DCP and PCP in the point label when non-default', () => { + expect( + getPointLabel( + pt({ + tp: 8, + decode_tp: 8, + ep: 1, + prefill_dcp_size: 8, + decode_dcp_size: 8, + prefill_pcp_size: 1, + decode_pcp_size: 1, + }), + ), + ).toBe('TP8/DCP8'); + expect( + getPointLabel(pt({ tp: 8, decode_tp: 8, ep: 1, decode_dcp_size: 8, prefill_pcp_size: 4 })), + ).toBe('TP8/DCP8/PCP4'); + }); + it('returns "DPATP4" when ep is 1 and dp_attention is true', () => { expect(getPointLabel(pt({ tp: 4, ep: 1, dp_attention: true }))).toBe('DPATP4'); }); @@ -164,7 +183,11 @@ describe('generateTooltipContent', () => { // link to /inference/agentic/0, a doomed lookup. for (const badId of [0, Number.NaN]) { const html = generateTooltipContent( - tooltipConfig({ data: pt({ id: badId }), isPinned: true, hasTrace: true }), + tooltipConfig({ + data: pt({ id: badId }), + isPinned: true, + hasTrace: true, + }), ); expect(html).not.toContain('data-action="view-charts"'); } @@ -473,6 +496,17 @@ describe('generateOverlayTooltipContent', () => { expect(html).toContain('CPU Cache Hit Rate: 42.0%'); }); + it('shows DCP and PCP for unofficial-run points', () => { + const html = generateOverlayTooltipContent( + overlayConfig({ + data: pt({ ep: 1, decode_dcp_size: 8, prefill_pcp_size: 4 }), + }), + ); + + expect(html).toContain('Decode Context Parallelism (DCP): 8'); + expect(html).toContain('Prefill Context Parallelism (PCP): 4'); + }); + it('shows point-level speculative decoding for mixed agentic overlays', () => { const mtp = generateOverlayTooltipContent( overlayConfig({ @@ -556,6 +590,17 @@ describe('generateGPUGraphTooltipContent', () => { expect(html).toContain('BF16'); }); + it('shows DCP and PCP in comparison point tooltips', () => { + const html = generateGPUGraphTooltipContent( + tooltipConfig({ + data: pt({ ep: 1, decode_dcp_size: 8, prefill_pcp_size: 4 }), + }), + ); + + expect(html).toContain('Decode Context Parallelism (DCP): 8'); + expect(html).toContain('Prefill Context Parallelism (PCP): 4'); + }); + it('splits image and SHA onto separate lines', () => { const html = generateGPUGraphTooltipContent( tooltipConfig({ data: pt({ image: 'vllm-v0.6.0 abc123' }) }), diff --git a/packages/app/src/components/inference/utils/tooltipUtils.ts b/packages/app/src/components/inference/utils/tooltipUtils.ts index eff4f0e29..3cbb14892 100644 --- a/packages/app/src/components/inference/utils/tooltipUtils.ts +++ b/packages/app/src/components/inference/utils/tooltipUtils.ts @@ -5,7 +5,10 @@ import type { Locale } from '@/lib/i18n'; import { isKvOffloadEnabled } from '@/lib/kv-offload'; import type { HardwareConfig, InferenceData, OverlayData } from '@/components/inference/types'; -import { parallelismLabel } from '@/components/inference/utils/parallelism-label'; +import { + meaningfulParallelismSize, + parallelismLabel, +} from '@/components/inference/utils/parallelism-label'; import { cacheImplementationLabel, offloadTypeLabel, @@ -59,28 +62,37 @@ const asBool = (v: boolean | string | undefined): boolean | undefined => * Delegates to the shared {@link parallelismLabel} so the chart points and the * agentic sibling navigator describe a config identically. */ -export const getPointLabel = (d: InferenceData): string => - parallelismLabel({ +export const getPointLabel = (d: InferenceData): string => { + const aggregateDcp = meaningfulParallelismSize(d.prefill_dcp_size, d.decode_dcp_size); + const aggregatePcp = meaningfulParallelismSize(d.prefill_pcp_size, d.decode_pcp_size); + return parallelismLabel({ // InferenceData.tp is the TOTAL GPU count (createChartDataPoint folds pp // into it for aggregated rows) — the label wants the actual TP width, so // prefer the raw decode_tp and keep d.tp only as a legacy fallback. tp: d.decode_tp ?? d.tp, ep: d.ep, pp: d.pp, + dcp: d.disagg ? (d.decode_dcp_size ?? d.prefill_dcp_size) : aggregateDcp, + pcp: d.disagg ? (d.decode_pcp_size ?? d.prefill_pcp_size) : aggregatePcp, dpAttention: asBool(d.dp_attention), disagg: d.disagg, isMultinode: d.is_multinode, prefillTp: d.prefill_tp, prefillEp: d.prefill_ep, prefillPp: d.prefill_pp, + prefillDcp: d.prefill_dcp_size, + prefillPcp: d.prefill_pcp_size, prefillDpAttention: asBool(d.prefill_dp_attention), prefillNumWorkers: d.prefill_num_workers, decodeTp: d.decode_tp, decodeEp: d.decode_ep, decodePp: d.decode_pp, + decodeDcp: d.decode_dcp_size, + decodePcp: d.decode_pcp_size, decodeDpAttention: asBool(d.decode_dp_attention), decodeNumWorkers: d.decode_num_workers, }); +}; const runLinkHTML = (runUrl?: string) => runUrl @@ -255,6 +267,8 @@ const PARALLELISM_STRINGS = { tensorParallelism: 'Tensor Parallelism', expertParallelism: 'Expert Parallelism', pipelineParallelism: 'Pipeline Parallelism', + decodeContextParallelism: 'Decode Context Parallelism (DCP)', + prefillContextParallelism: 'Prefill Context Parallelism (PCP)', dpAttention: 'DP Attention', }, zh: { @@ -270,10 +284,19 @@ const PARALLELISM_STRINGS = { tensorParallelism: '张量并行 (TP)', expertParallelism: '专家并行 (EP)', pipelineParallelism: '流水线并行 (PP)', + decodeContextParallelism: '解码上下文并行 (DCP)', + prefillContextParallelism: '预填充上下文并行 (PCP)', dpAttention: 'DP Attention', }, } as const; +const contextParallelismParts = (dcp?: number, pcp?: number): string => { + const parts: string[] = []; + if (dcp !== undefined && dcp > 1) parts.push(`DCP: ${dcp}`); + if (pcp !== undefined && pcp > 1) parts.push(`PCP: ${pcp}`); + return parts.length > 0 ? `${parts.join(', ')}, ` : ''; +}; + /** * Generates HTML for the parallelism configuration section of a tooltip. * Falls back to GPU count for old data without parallelism fields. @@ -283,11 +306,18 @@ const PARALLELISM_STRINGS = { const generateParallelismHTML = (d: InferenceData, locale: Locale = 'en'): string => { const t = PARALLELISM_STRINGS[locale]; const deployment = d.disagg ? t.disaggregated : d.is_multinode ? t.multiNode : t.singleNode; + const aggregateDcp = meaningfulParallelismSize(d.prefill_dcp_size, d.decode_dcp_size); + const aggregatePcp = meaningfulParallelismSize(d.prefill_pcp_size, d.decode_pcp_size); if ( (d.ep === null || d.ep === undefined) && (d.prefill_ep === null || d.prefill_ep === undefined) ) { - return tooltipLine(t.deployment, deployment) + tooltipLine(t.strategy, t.gpuCount(d.tp)); + return ( + tooltipLine(t.deployment, deployment) + + tooltipLine(t.strategy, t.gpuCount(d.tp)) + + (aggregateDcp ? tooltipLine(t.decodeContextParallelism, aggregateDcp) : '') + + (aggregatePcp ? tooltipLine(t.prefillContextParallelism, aggregatePcp) : '') + ); } if (d.is_multinode && d.disagg) { @@ -301,13 +331,15 @@ const generateParallelismHTML = (d: InferenceData, locale: Locale = 'en'): strin const ddpa = d.decode_dp_attention ?? d.dp_attention ?? false; const pw = d.prefill_num_workers ?? 1; const dw = d.decode_num_workers ?? 1; + const prefillContext = contextParallelismParts(d.prefill_dcp_size, d.prefill_pcp_size); + const decodeContext = contextParallelismParts(d.decode_dcp_size, d.decode_pcp_size); return ` ${tooltipLine(t.deployment, deployment)}
- ${t.prefill}: ${d.num_prefill_gpu ?? '?'} ${t.gpusUnit}, TP: ${ptp}, ${ppp > 1 ? `PP: ${ppp}, ` : ''}EP: ${pep}, DPA: ${pdpa ? 'True' : 'False'}, Workers: ${pw} + ${t.prefill}: ${d.num_prefill_gpu ?? '?'} ${t.gpusUnit}, TP: ${ptp}, ${ppp > 1 ? `PP: ${ppp}, ` : ''}${prefillContext}EP: ${pep}, DPA: ${pdpa ? 'True' : 'False'}, Workers: ${pw}
- ${t.decode}: ${d.num_decode_gpu ?? '?'} ${t.gpusUnit}, TP: ${dtp}, ${dpp > 1 ? `PP: ${dpp}, ` : ''}EP: ${dep}, DPA: ${ddpa ? 'True' : 'False'}, Workers: ${dw} + ${t.decode}: ${d.num_decode_gpu ?? '?'} ${t.gpusUnit}, TP: ${dtp}, ${dpp > 1 ? `PP: ${dpp}, ` : ''}${decodeContext}EP: ${dep}, DPA: ${ddpa ? 'True' : 'False'}, Workers: ${dw}
`; } @@ -315,6 +347,8 @@ const generateParallelismHTML = (d: InferenceData, locale: Locale = 'en'): strin ${tooltipLine(t.deployment, deployment)} ${tooltipLine(t.tensorParallelism, d.decode_tp ?? d.tp)} ${d.pp !== null && d.pp !== undefined && d.pp > 1 ? tooltipLine(t.pipelineParallelism, d.pp) : ''} + ${aggregateDcp ? tooltipLine(t.decodeContextParallelism, aggregateDcp) : ''} + ${aggregatePcp ? tooltipLine(t.prefillContextParallelism, aggregatePcp) : ''} ${d.ep !== null && d.ep !== undefined ? tooltipLine(t.expertParallelism, d.ep) : ''} ${tooltipLine(t.dpAttention, d.dp_attention ? 'True' : 'False')}`; }; diff --git a/packages/app/src/hooks/api/use-benchmark-siblings.ts b/packages/app/src/hooks/api/use-benchmark-siblings.ts index 7cdb990f0..7471dd45b 100644 --- a/packages/app/src/hooks/api/use-benchmark-siblings.ts +++ b/packages/app/src/hooks/api/use-benchmark-siblings.ts @@ -7,11 +7,15 @@ export interface BenchmarkSibling { decode_tp: number; decode_ep: number; decode_pp: number | null; + decode_dcp_size: number | null; + decode_pcp_size: number | null; decode_dp_attention: boolean; decode_num_workers: number; prefill_tp: number; prefill_ep: number; prefill_pp: number | null; + prefill_dcp_size: number | null; + prefill_pcp_size: number | null; prefill_dp_attention: boolean; prefill_num_workers: number; num_prefill_gpu: number; diff --git a/packages/app/src/lib/api-documentation.ts b/packages/app/src/lib/api-documentation.ts index c84e5d00a..1cb016eaf 100644 --- a/packages/app/src/lib/api-documentation.ts +++ b/packages/app/src/lib/api-documentation.ts @@ -1742,11 +1742,15 @@ export const apiOperations: readonly ApiOperation[] = [ decode_tp: 8, decode_ep: 1, decode_pp: null, + decode_dcp_size: 8, + decode_pcp_size: 1, decode_dp_attention: false, decode_num_workers: 1, prefill_tp: 8, prefill_ep: 1, prefill_pp: null, + prefill_dcp_size: 8, + prefill_pcp_size: 1, prefill_dp_attention: false, prefill_num_workers: 1, num_prefill_gpu: 0, diff --git a/packages/app/src/lib/api-route-catalog.ts b/packages/app/src/lib/api-route-catalog.ts index 7e23a541e..124e1c125 100644 --- a/packages/app/src/lib/api-route-catalog.ts +++ b/packages/app/src/lib/api-route-catalog.ts @@ -100,7 +100,7 @@ export const apiRouteCatalog = [ method: 'GET', classification: 'published-read', operationId: 'get-benchmark-siblings', - sourceSha256: '36b00e5d1c761e6a76e2c0454d9be4a15b7ccb7f3af5bdccd8ac61b6e5ff5312', + sourceSha256: '2b20de8b2b67ed53027eba478068f8f22ae7e3843ac38423e8ce6b27c1f74fd6', }, { source: 'src/app/api/v1/benchmarks/route.ts', @@ -439,7 +439,7 @@ export const apiContractSourceDigests = [ }, { source: '../db/src/queries/benchmark-siblings.ts', - sourceSha256: '5a2b7e902c3003a0b3dbf3f402cc4b6b40648a1e014a218ab39dcbfd35c80f42', + sourceSha256: '07d3d1bf93820091d1014b14bf954555edcec422c05e575ad87142f9845e4156', reviewArea: { en: 'Benchmark sibling SKU metadata and sibling navigation row shape.', zh: '基准同组 SKU 元数据和同组导航行结构。', diff --git a/packages/app/src/lib/benchmark-transform.test.ts b/packages/app/src/lib/benchmark-transform.test.ts index e6387402c..dac691b56 100644 --- a/packages/app/src/lib/benchmark-transform.test.ts +++ b/packages/app/src/lib/benchmark-transform.test.ts @@ -68,6 +68,40 @@ function makeRow(overrides: Partial = {}): BenchmarkRow { } describe('rowToAggDataEntry', () => { + it('preserves DCP and PCP metrics for point tooltips', () => { + const entry = rowToAggDataEntry( + makeRow({ + metrics: { + prefill_dcp_size: 2, + decode_dcp_size: 8, + prefill_pcp_size: 4, + decode_pcp_size: 1, + }, + }), + ); + + expect(entry.prefill_dcp_size).toBe(2); + expect(entry.decode_dcp_size).toBe(8); + expect(entry.prefill_pcp_size).toBe(4); + expect(entry.decode_pcp_size).toBe(1); + }); + + it('mirrors aggregate artifact DCP and PCP widths into both topology roles', () => { + const entry = rowToAggDataEntry( + makeRow({ + metrics: { + dcp_size: 8, + pcp_size: 1, + }, + }), + ); + + expect(entry.prefill_dcp_size).toBe(8); + expect(entry.decode_dcp_size).toBe(8); + expect(entry.prefill_pcp_size).toBe(1); + expect(entry.decode_pcp_size).toBe(1); + }); + it('maps hardware and framework fields', () => { const entry = rowToAggDataEntry(makeRow()); expect(entry.hw).toBe('h200'); diff --git a/packages/app/src/lib/benchmark-transform.ts b/packages/app/src/lib/benchmark-transform.ts index 154e57b04..d4d1bfae9 100644 --- a/packages/app/src/lib/benchmark-transform.ts +++ b/packages/app/src/lib/benchmark-transform.ts @@ -209,6 +209,14 @@ export function rowToAggDataEntry(row: BenchmarkRow): AggDataEntry { decode_tp: row.disagg ? row.decode_tp : aggregateTp, decode_ep: row.disagg ? row.decode_ep : aggregateEp, decode_pp: row.disagg ? m.decode_pp : aggregatePp, + // Context-parallel widths are emitted by the runtime into metrics JSONB. + // Preserve both role-shaped values even for aggregate deployments: the + // tooltip collapses the transport-only role names for aggregate serving, + // while disaggregated serving displays the values per role. + prefill_dcp_size: m.prefill_dcp_size ?? m.dcp_size, + decode_dcp_size: m.decode_dcp_size ?? m.dcp_size, + prefill_pcp_size: m.prefill_pcp_size ?? m.pcp_size, + decode_pcp_size: m.decode_pcp_size ?? m.pcp_size, decode_dp_attention: row.disagg ? row.decode_dp_attention : aggregateDpAttention, decode_num_workers: row.decode_num_workers, image: row.image ?? undefined, diff --git a/packages/constants/src/metric-keys.ts b/packages/constants/src/metric-keys.ts index 94c5b6134..13b7ff408 100644 --- a/packages/constants/src/metric-keys.ts +++ b/packages/constants/src/metric-keys.ts @@ -171,6 +171,10 @@ export const METRIC_KEYS = new Set([ // (rowToAggDataEntry in benchmark-transform.ts). 'prefill_pp', 'decode_pp', + // Aggregate artifacts emit one context-parallel width, while disaggregated + // artifacts may emit a separate value for each role. + 'dcp_size', + 'pcp_size', 'prefill_dcp_size', 'decode_dcp_size', 'prefill_pcp_size', diff --git a/packages/db/src/queries/benchmark-siblings.ts b/packages/db/src/queries/benchmark-siblings.ts index d1b7f890a..a6a6c41f8 100644 --- a/packages/db/src/queries/benchmark-siblings.ts +++ b/packages/db/src/queries/benchmark-siblings.ts @@ -15,11 +15,15 @@ export interface BenchmarkSibling { decode_tp: number; decode_ep: number; decode_pp: number | null; + decode_dcp_size: number | null; + decode_pcp_size: number | null; decode_dp_attention: boolean; decode_num_workers: number; prefill_tp: number; prefill_ep: number; prefill_pp: number | null; + prefill_dcp_size: number | null; + prefill_pcp_size: number | null; prefill_dp_attention: boolean; prefill_num_workers: number; num_prefill_gpu: number; @@ -93,8 +97,24 @@ export async function getBenchmarkSiblings( select br.id, br.conc, br.offload_mode, c.decode_tp, c.decode_ep, (br.metrics->>'decode_pp')::int as decode_pp, + coalesce( + (br.metrics->>'decode_dcp_size')::int, + (br.metrics->>'dcp_size')::int + ) as decode_dcp_size, + coalesce( + (br.metrics->>'decode_pcp_size')::int, + (br.metrics->>'pcp_size')::int + ) as decode_pcp_size, c.decode_dp_attention, c.decode_num_workers, c.prefill_tp, c.prefill_ep, (br.metrics->>'prefill_pp')::int as prefill_pp, + coalesce( + (br.metrics->>'prefill_dcp_size')::int, + (br.metrics->>'dcp_size')::int + ) as prefill_dcp_size, + coalesce( + (br.metrics->>'prefill_pcp_size')::int, + (br.metrics->>'pcp_size')::int + ) as prefill_pcp_size, c.prefill_dp_attention, c.prefill_num_workers, c.num_prefill_gpu, c.num_decode_gpu, c.disagg, c.is_multinode, (br.metrics->>'tput_per_gpu')::float8 as tput_per_gpu, @@ -120,11 +140,15 @@ export async function getBenchmarkSiblings( decode_tp: number; decode_ep: number; decode_pp: number | null; + decode_dcp_size: number | null; + decode_pcp_size: number | null; decode_dp_attention: boolean; decode_num_workers: number; prefill_tp: number; prefill_ep: number; prefill_pp: number | null; + prefill_dcp_size: number | null; + prefill_pcp_size: number | null; prefill_dp_attention: boolean; prefill_num_workers: number; num_prefill_gpu: number; @@ -143,11 +167,15 @@ export async function getBenchmarkSiblings( decode_tp: r.decode_tp, decode_ep: r.decode_ep, decode_pp: r.decode_pp === null ? null : Number(r.decode_pp), + decode_dcp_size: r.decode_dcp_size === null ? null : Number(r.decode_dcp_size), + decode_pcp_size: r.decode_pcp_size === null ? null : Number(r.decode_pcp_size), decode_dp_attention: r.decode_dp_attention, decode_num_workers: r.decode_num_workers, prefill_tp: r.prefill_tp, prefill_ep: r.prefill_ep, prefill_pp: r.prefill_pp === null ? null : Number(r.prefill_pp), + prefill_dcp_size: r.prefill_dcp_size === null ? null : Number(r.prefill_dcp_size), + prefill_pcp_size: r.prefill_pcp_size === null ? null : Number(r.prefill_pcp_size), prefill_dp_attention: r.prefill_dp_attention, prefill_num_workers: r.prefill_num_workers, num_prefill_gpu: r.num_prefill_gpu,