Repository navigation
[Perf][DSv4.1] Extend the latency_specialized decode on NVIDIA - #63921
Conversation
Extends decode_backend=latency_specialized, all under vllm/models/deepseek_v41/nvidia and common, with no new environment switches; the stock DSv4.1 path is unchanged, and the DSpark draft keeps its stock paths. - Routed experts: the low-batch path covers up to 192 rows; the fused front selects on an 8-CTA cluster and applies the router's padding mask; GEMM2 starts per token tile. - All-reduce + mHC: a two-shot (BT) protocol from 64 rows for the fused all-reduce and the MoE finalize; the next mHC coefficients always on the side stream. - Attention: small batches decode sparse MLA with a CuTe DSL kernel; the MXFP8 projections use pinned CuTe DSL tactics above 8 rows and the low-latency GEMMs up to 48 rows; up to 48 rows the decode Q/KV norm and the fused wo_a trigger their PDL dependents early. These early triggers are options that only the specialized decode sets. - Indexers: row-sharded from two requests (the rows all-gathered over NCCL), planned while the index key is pending, top-k selected from BF16 logits with a CuTe DSL kernel, metadata built on separate branches. - Embedding and Engram: a full copy of the embedding table on every rank (1.32 GB per GPU) written straight into the hc streams; the Engram WKV and lookback gather beside other work. - The new CuTe DSL kernels compile during warm-up. - Tests for the new kernels; AllReduceMHC LL and BT protocol tests, which CI now runs with the Engram tests. Signed-off-by: Yaoyao Ding <yaoyaod@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… all-gather The specialized decode gathered its vocab-parallel logits with the custom all-gather, whose MNNVL Lamport kernel (mnnvl_lamport_all_gather) never waits for a rank's own multicast copy: a copy that lands after the next call has cleaned its stage stays there, and a later gather of another size can read it as a peer's payload. The row-sharded indexer's gathers had moved to NCCL for that reason. LamportAllGather is a CuTe DSL all-gather over NVLink multicast with a mailbox of its own (three generations x ranks x a fixed capacity). Each thread publishes one 16-byte pack, polls every rank's slot of it, its own included, writes them out and resets the slots it read, so no call leaves a word for a later one; the last CTA to arrive advances the generation. The executor builds one, the attention steps gather the indexer's row slices with it, and the specialized logits processor gathers the logits with it; both fall back to NCCL above its capacity (4 MiB a rank, the custom all-gather's TP4 limit). test_all_gather.py checks it against NCCL at TP4 for growing and shrinking sizes with a late rank, the sentinel word, and CUDA graphs of mixed sizes replayed in a random order; run as a module with --replays it is a stress test. test_numerics.py gathers its logits, its row-sharded indexer and its graph replays with it. Signed-off-by: Yaoyao Ding <yaoyaod@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ther stress mode The replicated embedding table is copied from the vocabulary shards once, at load, so the specialized decode now also requires no runtime weight transfer (weight_transfer_config), as Kimi-K3's does. test_all_gather.py run as a module takes --custom-all-gather: it replays the same gather graphs on vLLM's custom all-gather and counts the mismatched words, so the stale words of mnnvl_lamport_all_gather can be reproduced beside the Lamport all-gather's zero. AGENTS.md: the indexer rows and logits go through the Lamport all-gather, the FlashInfer internals list names the wide low-latency GEMM and the cute-dsl runner, and the package table lists embedding/. all_gather.py notes that its PTX helpers follow FlashInfer's inline-asm wrappers. Signed-off-by: Yaoyao Ding <yaoyaod@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Infer The specialized decode enabled four features only on flashinfer-python 0.7.2 exactly, so the bump to 0.7.2.post1 turned all four off and skipped the numerics test. Gate each on what it needs instead: - the publishing wo_b GEMM and the MXFP8 layer input, which use FlashInfer internals, on 0.7.2 or newer; test_numerics.py compares both with FlashInfer's GEMM and quantize bitwise on the pinned release; - the pinned MXFP8 tactics on the tuner's autotune_tactics listing them (already checked), with no version gate; - the sparse-MLA decode's KV counters on the decode taking multi_ctas_kv_counter_buffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yaoyao Ding <yaoyaod@nvidia.com>
…tension Brings in vLLM main (ed1a730, FlashInfer 0.7.2.post1) and the fix that gates the specialized decode's FlashInfer paths on what each needs. This branch's two further paths on FlashInfer internals, the pinned cute-dsl MXFP8 tactics and the low-latency MXFP8 GEMM above 8 rows, run on FlashInfer 0.7.2 or newer like the others. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yaoyao Ding <yaoyaod@nvidia.com>
FlashInfer 0.7.2.post1 finalizes small batches with the top-k products split into two groups, so its sum can differ from the FMA chain by more than one BF16 ulp where the products cancel. Bound the difference by one ulp plus FP32's reordering error instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yaoyao Ding <yaoyaod@nvidia.com>
The Lamport all-gathers on the run-ahead side stream hang multi-request decode: test_specialized_decode_matches_the_model (12 requests decoding together) timed out in sample_tokens in 2 of 3 runs on GB200, and vllm-project#63921 alone passed 3 of 3. With the chain's gathers back on NCCL it passed 5 of 5, with host-offloaded and GPU-resident tables. The run-ahead for GPU-resident tables stays; the Lamport gathers saved only about 17 us a step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
|
This pull request has merge conflicts that must be resolved before it can be |
Brings in vLLM main (8321d11), which now has the FlashInfer gate fix (vllm-project#63914). The conflicts are in the lines this branch extends: the gate documentation, the wide low-latency MXFP8 GEMM and the CuTe DSL sparse MLA decode; they keep this branch's side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yaoyao Ding <yaoyaod@nvidia.com>
…B300 Opt-in VLLM_SD_* knobs for the latency_specialized decode, stacked on vllm-project#63921 and vllm-project#61869: earlier PDL triggers at the Lamport AR+mHC boundaries, main-stream-first capture and an MXFP8 hop A in the MoE half, routed GEMMs that leave SMs to the shared expert, Q RoPE and the KV write moved off the MLA critical path, an earlier MLA prologue with L2 prefetch, table-free MLA on indexer layers, SGLang's top-k kernels, batched L20 block selection, and fewer bubbles in the MRV2 step and the post-forward and draft graphs. Every knob is off by default. The tree matches the measured build. Signed-off-by: Bowen Wang <bowen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ci run |
|
✅ Triggered Buildkite CI #99406 for commit ℹ️ This PR is 21 commits behind upstream |
|
This pull request has merge conflicts that must be resolved before it can be |
Brings in vLLM main (b14e2f2). Main renamed the FlashInfer DSv4 workspace getter to get_flashinfer_dsv4_workspace, so the specialized sparse MLA decode calls it by that name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yaoyao Ding <yaoyaod@nvidia.com>
|
/ci run |
|
✅ Triggered Buildkite CI #99611 for commit ℹ️ This PR is 5 commits behind upstream |
…ison The routed-expert test compares the reference FMA top-k reduction with FlashInfer's finalized output and then the draft's finalize kernel with the reference. Check the finalize kernel first, so its bitwise result is still checked when the FlashInfer comparison fails: with FlashInfer 0.7.2.post1 it does, because FlashInfer now adds the top-k products in another order (vllm-project#63921 relaxes that comparison). Signed-off-by: Yaoyao Ding <yaoyaod@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This pull request has merge conflicts that must be resolved before it can be |
Brings in vLLM main (e940c1f). Main's vllm-project#64580 gives the DeepGEMM MQA logits an explicit logits_dtype (FP32 by default) and the indexer layer a logits_dtype attribute (BF16 for DeepSeek on SM100). The specialized decode's paged logits call passes op.logits_dtype, so its BF16 logits still reach decode_topk without an upcast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yaoyao Ding <yaoyaod@nvidia.com>
Head branch was pushed to by a user without write access
|
/ci run |
|
✅ Triggered Buildkite CI #99839 for commit ℹ️ This PR is 5 commits behind upstream |
…ecode) Merges vllm-project#61869 head 12a509d, whose merge base with main predates vllm-project#63921's landing, into main 7663a8c. Textual conflicts, resolved keeping both sides: - specialized_decode/runtime/decode.py: DecodeStep keeps vllm-project#63921's fields with vllm-project#61869's executor union; the executor builds vllm-project#63921's LamportAllGather and vllm-project#61869's Boundaries(all_reduce, mhc_stream). - boundary/boundary.py: vllm-project#63921's step-free Boundaries API on vllm-project#61869's shared mHC stream; first() keeps vllm-project#61869's draft branch. - attention/step.py: vllm-project#63921's early-trigger and pinned wo_b paths, with vllm-project#61869's TP-partial wo_b as every fallback. - AGENTS.md: both descriptions. - tests: both sides kept; test_kernels keeps vllm-project#63921's FP32-order slack after vllm-project#61869's finalize-kernel check. Fixes without a textual conflict: - runtime/draft.py: Boundaries calls without the step argument; the draft AttentionStep gets the target's all_gather; DraftDecode.run takes streams. - runtime/draft.py: check_low_batch_graph reads the decode request sizes, which main's vllm-project#63406 made the drafter capture, instead of rounding cudagraph_capture_sizes up to whole requests. - nvidia/dspark.py: the draft's logits processor gets the target executor's all_gather (vllm-project#63921 replaced the custom all-gather). - nvidia/model.py: cast the target step's executor to SpecializedDecode for .embedding. - test_layers.py: the probe wraps the step-free Boundaries.sublayer. Signed-off-by: Bowen Wang <bowen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up the main fixes for the fusion E2E mixed-precision test (vllm-project#65495), the DeepSeek-V4.1 _Mxfp8Linear test (vllm-project#63921) and UMA KV cache sizing on DGX Spark (vllm-project#65413). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: khluu <khluu000@gmail.com>
Overview
This PR makes the
latency_specializeddecode from #61868 faster for DeepSeek-V4.1 on NVIDIA GPUs. Most of the work is in the decode step's kernels: larger batches now stay on the low-latency paths, more of each step overlaps, and the indexers split their work across the TP ranks. "What changed" below goes through each part.On one GB300 tray with DSpark, the decode step takes 5.9–17.0% less time than on
mainat batch 1 to 32, and AgentX P90 interactivity is 4–26% higher at concurrency 1 to 32, with slightly higher throughput.Left: three ABBA sets at
0cdfc566, 12 measurements per arm and batch. Right: AgentX at0cdfc566, both arms of each concurrency on one tray.Validation
This branch includes #63914 (merged into this branch), which keeps the specialized decode's FlashInfer paths on
main's FlashInfer 0.7.2.post1; this PR's two further paths on FlashInfer internals follow the same rule. The runs below are from before this branch mergedmain:mainat76518259on that commit's wheel with FlashInfer 0.7.2, against this PR at0cdfc566, except compute-sanitizer, which ran at5ab5ef9e, the commit before it (0cdfc566adds only a start-up requirement, no runtime weight updates, a test option, a comment and documentation).After the merge, on
main's wheel ated1a7307with FlashInfer 0.7.2.post1, four GB300: these GPU tests pass atb7bb822b(test_kernels.py,test_all_gather.py,test_decode_topk.py,test_sparse_mla_cute.py,test_numerics.py,test_layers.py,tests/kernels/test_engram.py,tests/distributed/test_custom_all_reduce.py; all buttest_kernels.pyran atd7fcb755, which differs only in that file), and a TP4 server withdecode_backend: latency_specializedstarts with the specialized decode on and no fallback warning, and answers a completion and a chat request correctly. FlashInfer 0.7.2.post1's MoE finalize adds the top-k products in a different order at small batches, sob7bb822blets one test's finalize comparison allow FP32's reordering error.Correctness
mainsingle_2,multikey_2,multikey_3; 32K, 128K, 256K; 40 samples each), concurrency 8VLLM_GPU_SYNC_CHECK=errordecode_backendunset (stock decode)test_all_gather.pyas a module;--custom-all-gatherreplays the same graphs on vLLM's custom all-gather)Tests
tests/kernels/test_engram.py), 1 skippedtorch.topk, k 512 and 2048, 1–64 rows: sampled and direct paths, crowded, constant and rising values, NaN and infinities, a sample that misses the top values, the wide 16-CTA rows, CUDA graphs; block mode against the stockselect_candidate_blocksAllReduceMHCtest_kernels.py; at5ab5ef9e)CI. The new test files (the all-gather's among them), the
AllReduceMHCtests (test_custom_all_reduce.py -k all_reduce_mhc) andtest_engram.pyjoin the nightly 4×B200 DeepSeek-V4.1 step. Three tests need hardware that step does not have, and skip there:test_pinned_mxfp8_mmandtest_wide_low_latency_mxfp8_mm(the tactics are pinned for GB300, SM103) and the layers test (four GB300).Performance
All runs: DeepSeek-V4.1-Flash on one GB300 tray (TP4), DSpark (5 draft tokens), Model Runner V2, FULL CUDA graphs for decode. The decode step and AgentX use DSpark's synthetic acceptance (length 3.51, as InferenceX runs AgentX); the accuracy runs above verify for real.
Server command and clients
Both arms ran the same command, on
main's wheel with eithermain's or this PR's source:"rejection_sample_method":"synthetic","synthetic_acceptance_length":3.51.niah_single_2,niah_multikey_2andniah_multikey_3at 32K, 128K and 256K tokens, 40 seeded samples per task and length (upstream lm-eval draws 500), 128 output tokens, thinking off, one run per cell.AgentX. The InferenceX AgentX trace corpus (393 traces), 3,600 s per concurrency, closed loop, DSpark at the synthetic acceptance length 3.51 as InferenceX runs it, at
0cdfc566. Both arms of a concurrency ran on the same tray, one after the other (the first arm alternates with the concurrency). Each cell readsmain→ this PR.Some trace turns ask for one output token. When that token ends the reasoning, the response has no content, and the client records the request as an error: 0–1 such requests a run for
mainand 0–1 for this PR.TTFT: API server and engine
AgentX TTFT is mostly the API server's single-threaded work before the engine sees the request: chat-template rendering and tokenization of the whole conversation. aiperf's per-request
request_ack_nsseparates that work from the engine's (acknowledgement to first token). This PR changes only worker-side code, and the engine part stays within 18 ms ofmainat every concurrency; the TTFT differences in the table are in the API server:Decode top-k.
decode_topkagainst vLLM's opt-in DeepSelect and the stock path (FP32 upcast +cooperative_topk), k = 512, µs per call:Random BF16 logits; "valid" is the share of each row's columns before its end. One GB300, a CUDA graph of 20 back-to-back calls, median of 20 replays. Each cell:
decode_topk/ DeepSelect / stock.With row sharding, a rank selects 12 rows at batch 8 and 48 rows at batch 32 (6 rows a request, over 4 ranks).
decode_topkis faster than DeepSelect at every shape but two: 65,536 columns with 1/8 valid (7.3–7.7 against 4.2–4.3 µs), and 1,048,576 columns with 1/8 valid at 12 rows (17.1 against 16.5).decode_topkstays faster with all 262,144 columns valid (11.8–22.5 against 26.1–26.2 µs).decode_topkdoes not take 1,048,576-column rows, so they use the stock path (81.7–289.3 µs against DeepSelect's 16.6–76.6).decode_topkis faster at every size (5.9–12.7 against 9.3–80.0 µs).decode_topkis faster at every shape it takes, except 65,536 columns with 1/8 valid (7.3–17.4 against 6.7–12.9 µs; equal at 24 rows).All shapes
Block mode (the candidate source: top 2,048 blocks of 8 columns; DeepSelect over the block maxima), 1/8 valid,
decode_topk/ DeepSelect:Memory and start-up. The replicated embedding table takes 1.32 GB per GPU and the all-gather's mailbox 48 MiB, which the KV cache gives up (106,281,697 → 105,286,588 KV-cache tokens on the tray, −0.9%). Engine start: the engine's init (profile, KV cache, warm-up) took 98.8–103.0 s (
main) and 98.8–108.9 s (this PR) with warm compile caches, three starts each; a tray's first start, with cold caches, took 680–748 s in either arm.Details
Duplicate-work check. No open PR extends the DeepSeek-V4.1 specialized decode. Related open PRs:
FLASHINFER_MLA_SPARSE_DSV41backend, which the specialized decode requires.cooperative_topkread BF16 logits, and [Perf][DSA] Add FlashInfer gvr_2 as a sparse-indexer top-k backend #57109 adds FlashInfer's sampledgvr_2as an opt-in indexer top-k.decode_topkserves only this model's decode indexers. It also selects the candidate source's blocks, and it covers the 1M-wide rows on 16 CTAs. The comparison with DeepSelect is above.What changed, and what each part saved when it was added
Each row was measured against the tree before it, mostly as decode-step ABBA blocks on one tray (batch B = B requests × 6 rows). They were measured one at a time, on a moving base that also held DSpark-draft changes not in this PR, so they do not add up exactly.
wo_btactic (15.3 against 5.9 µs at 48 rows), about 310 µs a step at batch 8, depending on the server startcooperative_topkplus the FP32 upcast took 182 µs a step on the slowest rank at batch 8maingathers them with the custom all-gather, whose Lamport kernel never waits for the rank's own multicast copy, so a late copy can corrupt a later gather of another size (test_all_gather.py --custom-all-gatherreproduces it)wo_atrigger their dependents early (≤ 48 rows)wq_bandwo_bload their weights before their own wait, but were launched latePull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
BEFORE SUBMITTING, PLEASE READ https://docs.vllm.ai/en/latest/contributing (anything written below this line will be removed by GitHub Actions)