Repository navigation
Conversation
Pb314314
left a comment
There was a problem hiding this comment.
Review of e01a8e5 (commit 1 d76fffc5e3 has the same capture plans as the earlier cg/pr-a@9b2c7c2db3; I checked 2880 CPU configs). Code is in good shape; most findings are about the description's claims, plus one real behavior delta (adaptive verification, inline).
Description: claims that need fixing
-
"Capture plans vs main on CPU (240 configurations) ... identical" is not backed.
design/diff_capture_plans.pydocumentspython -I, which ignoresPYTHONPATH, so both sides imported main. Rerun with-P -sand the current API on 2880 configs (SM100 on/off × max_num_seqs {1,8,72,128,256,512} × K {0,1,3,5,7,16} × dynamic SD {none, 3-tier} × mnbt {2048, 8192} × {default, max1024, max8192, pow2 list} × 3 modes), commit 1 vs main60a7d568f4:graphs vs main PIECEWISE / token-keyed identical, 2880/2880 target uniform decode, static K identical except explicit deprecated lists, which only gain coverage (e.g. pow2, max_num_seqs=8, K=5: main stops at 6 requests, PR adds 8) target uniform decode, dynamic SD differs in 512 configs: unreachable graphs dropped (q=4 above 8 requests), tiers denser; per tier no lost coverage; −13 graphs on average one-token draft decodes narrower in 348 (the documented delta), denser at the same max in 972 adaptive verification differs, see inline on get_varlen_decode_cudagraph_token_sizesGPT-OSS override, off SM100 PIECEWISE identical; FULL decode grid changes, max never lower -
"other GPUs, nothing set; or only the deprecated fields: same as main" holds for prefill and static-K target decode only. Add dynamic SD, explicit lists, GPT-OSS (all gains or neutral) and AV (losses, inline).
-
Test command lists
test_lilicorr.pybut nottests/v1/worker/test_gpu_ubatch_slicing.py, which this PR edits. On a GB200 node:test_cudagraph_manager.py27 passed,test_dynamic_sd_cug.py6 passed,test_lilicorr.py33 passed,test_gpu_ubatch_slicing.py29 passed,test_config.py97 passed / 4 failed (test_use_cudagraphs[*], which also fails on main on this node: the model never compiles,num_graphs_seen0). Running all files in one process makes 12 forked tests fail with "Cannot re-initialize CUDA in forked subprocess"; runtest_config.pyon its own. -
"The renaming and refactor commits change no capture plan for these configurations": verified.
42fbe5b0acvse01a8e5: target and draft plans identical in 2880/2880. AV plans are identical for the validated SM100 default configs (K=5, max_num_seqs 256/512); elsewhere the only change is one extra graph at the cap when the cap is off the grid. -
Commit 2 scope: verified. Off SM100, or with any deprecated field set, commit 2 changes no plan (2520 + 840 AV configs identical to commit 1). Only SM100 with nothing set changes.
Review summary (
|
Adaptive verification (AV): full list of capture-plan differences vs mainFollow-up to the inline thread on Method. CPU capture-plan dump (no model and no capture), comparing main (
Result. 254/480 configs are identical. 226 differ: in 76 the AV cap goes down, in 66 it goes up, and in the rest the cap is the same but the graph set differs. WhyIn this PR the AV sizes are
Separate from these: on SM100 with nothing set, the AV cap grows with the new decode default. For example, K=5 with AV plans for the validated GB200 configs (DSv4.1 DSpark + AV, K=5, Correction to my
|
|
[Claude] v3 is at
New in v3:
Commits 1 and 2 differ from |
ac7e96b to
f6013fc
Compare
ff7d528 to
4a3270c
Compare
… graph capture sizes Model runner V2 captured every CUDA graph from one token list (cudagraph_capture_sizes) and one cap (max_cudagraph_capture_size), so prefill and decode coverage moved together, and each runner rounded the token list to its own decode query length. Add two independent pairs, each in its own unit: - prefill_cudagraph_capture_token_sizes / max_prefill_cudagraph_capture_tokens (tokens): graphs for batches that are not uniform decode, and for every batch when cudagraph_mode is FULL or PIECEWISE. - decode_cudagraph_capture_request_sizes / max_decode_cudagraph_capture_requests (requests): FULL graphs for uniform decode batches. A runner captures n requests at its own query length q, up to the requests its batches hold and max_num_batched_tokens // q. With adaptive verification, decode batches take token-keyed FULL graphs on the token grid up to max_decode_cudagraph_capture_requests * (1 + K). Each batch kind's sizes come from one config entry point, so CudaGraphManager holds no grid logic. Decode dispatch uses one request-indexed table per (query length, LoRA count, ubatches), so under dynamic speculative decoding one query length no longer hides another's graph. cudagraph_capture_sizes and max_cudagraph_capture_size are deprecated on model runner V2. Set alone, they seed both kinds, with a warning. Combined with the new fields, they raise. After resolution they hold the token counts of all captured graphs, for the code that sizes buffers by them. Model runner V1 is unchanged. Defaults follow main, except for three kinds of graphs: - One-token draft decodes (EAGLE/EAGLE3/MTP draft steps) now stop at the target's decode request cap instead of main's token ceiling. With max_num_seqs >= 512, K=7 draft decode coverage goes from 512 to 64 requests off SM100, and from 1024 to 128 on SM100 or with max_cudagraph_capture_size=1024. In that range the target has no FULL decode graph on main either, and the K - 1 draft steps now run eagerly too, adding their host launch time to each step. On GB200 with Qwen3-8B EAGLE3 K=7 (TP1, 1K/1K) and the off-SM100 graph sets, this costs 15% / 10% / 2% of throughput at concurrency 128 / 256 / 512. Draft decode graphs that cover max_num_seqs come in a later commit. - DSpark draft graphs (query length K) also stop at the target's cap, so they cover about K/(K+1) of main's requests: 71 to 64 at K=7 off SM100. - Adaptive verification graphs take the token grid up to the decode cap times 1 + K. Their cap rounds down to a multiple of 1 + K, an explicit deprecated list no longer shapes them, and the extra sizes 1 + K and 2 * (1 + K) are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7mm2nfeDm8urozFY3PCda Signed-off-by: Bo Pang <bopang@inferact.ai>
With model runner V2 on SM100 GPUs, capture prefill graphs up to min(8192, max_num_batched_tokens) tokens and decode graphs up to max_num_seqs requests by default, at any number of speculative tokens. The shared default of 1024 tokens covered only 1024 / (1 + K) decode requests, so larger decode batches ran PIECEWISE or eager, and prefill and mixed steps above 1024 tokens ran eager. Other GPUs, and configs that set the deprecated fields, keep the old defaults. One-token draft decodes (the EAGLE, EAGLE3 and MTP draft steps after the first) take the request-count grid up to max_num_seqs on every GPU, however the decode sizes are set; [] still captures none. Off SM100 the target's decode cap is 512 / (1 + K) requests (64 at K=7), and above it the K - 1 draft steps would run eagerly and pay their host launch time on every step: on GB200 with Qwen3-8B EAGLE3 K=7 and the off-SM100 graph sets that cost 15% / 10% / 2% of throughput at concurrency 128 / 256 / 512. Their token counts join cudagraph_capture_sizes, so backends that size CUDA graph buffers by max_cudagraph_capture_size cover them. DFlash, DSpark and multi-module MTP keep the target's request counts: their widest draft graph cannot reuse the target's free graph-pool blocks while each manager captures on its own stream (vllm-project#59537 fixes that). The tests that pin the old platform defaults now run off SM100. The GPT-OSS override skips SM100 with model runner V2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7mm2nfeDm8urozFY3PCda Signed-off-by: Bo Pang <bopang@inferact.ai>
- Empty prefill and decode size lists, or both caps at 0, resolve to cudagraph_mode NONE. The engine core resolves the config again after its handshake, and that pass asserts max_cudagraph_capture_size >= 1. - Decode shapes are bounded by the recorded max_cudagraph_capture_size, as on main. DFlash and DSpark capture the decode request sizes at their own query length, so under dynamic speculative decoding they could exceed the size that attention backends size CUDA graph buffers by. - GPT-OSS raises the capture cap to 1024 tokens for performance. Its decode request cap divides 1024 by the narrowest target decode query length, so dynamic speculative decoding tiers keep main's coverage. - The KDA recoverSSM filter bounds mixed graphs by max_num_seqs * (1 + K) tokens, its workspace, instead of the manager's widest decode query length, which a dynamic speculative decoding schedule can keep below 1 + K. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7mm2nfeDm8urozFY3PCda Signed-off-by: Bo Pang <bopang@inferact.ai>
On SM100 the default grids reach 8192 tokens and max_num_seqs requests with 39 PIECEWISE and 64 decode sizes at the vllm serve defaults; main captures 83 PIECEWISE sizes up to 1024 tokens and 80-83 decode graphs. Each step is the largest power of two that keeps padding cheap where the step time grows with the batch: - Prefill: powers of two up to 256, then every 256 tokens, and every 512 once 7% of the size exceeds 512. PIECEWISE step time is flat below 1-4K tokens and grows linearly above. - Decode: every count up to 6, then a step of the largest power of two not above 0.65 * n^(2/3): 2 from 8, 4 from 16, 8 from 48, 16 from 128, 32 from 352 requests. On GB200 the grids are within 0.3% of engine time of 72-size grids on DeepSeek-V4-Flash and within 1.2% on MiniMax-M3 (mixed-heavy workload). Adaptive verification keeps a finer token grid (4 sizes per doubling from 8 to 1024, every 128 to 4096): it picks its verify budget from the captured sizes, and on DeepSeek-V4.1 DSpark K=5 the prefill grid's gaps cut the verify budget per request from 5.9 to 4.2 tokens at 16 requests. GPT-OSS keeps main's 1024-token PIECEWISE sizes off SM100. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7mm2nfeDm8urozFY3PCda Signed-off-by: Bo Pang <bopang@inferact.ai>
…SM100 Add max_decode_cudagraph_capture_tokens: each runner captures uniform decode graphs of n requests at query length q only while n * q fits, and wider decode batches take the prefill graphs. Adaptive verification's graphs are not bounded by it. It defaults to 4096 on SM100 and to no budget elsewhere. At the vllm serve defaults this keeps every decode graph up to max_num_seqs for K <= 3 and stops K=7 targets at 512 requests. On GB200, K=7 is 13-41% slower than K=1-3 at 512-1024 concurrent requests, while its 1024-request decode graph holds the widest buffers: on DeepSeek-V4-Flash DSpark K=7 the budget cuts CUDA graph memory from 25.4 to 15.9 GiB and grows the KV cache 29%, with throughput within launch noise at 512 requests and 4-5% lower at 1024. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7mm2nfeDm8urozFY3PCda Signed-off-by: Bo Pang <bopang@inferact.ai>
…red decode graphs The DeepSeek-V4.1 specialized decode recorded attention-metadata builds up to max_num_batched_tokens and max_num_seqs, while the FULL decode graphs stop at the recorded max within the decode token budget, and under dynamic speculative decoding at the widest query length's reach. It recorded keys no graph serves and missed the budget-truncated boundary graph, whose steps then built their metadata eagerly. Share the forward graphs' token bound and query-length reach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7mm2nfeDm8urozFY3PCda Signed-off-by: Bo Pang <bopang@inferact.ai>
…raphs Adaptive verification prices its steps by replaying the decode graphs. An empty decode_cudagraph_capture_request_sizes (e.g. on a prefill-only kv_producer) used to fail at startup in cost profiling; fail at config time instead, as main does for cudagraph_mode=PIECEWISE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7mm2nfeDm8urozFY3PCda Signed-off-by: Bo Pang <bopang@inferact.ai>
…apture sizes Cover every way the resolved decode grid can end up empty (an empty list, a zero cap, sizes all outside [1, max_num_seqs], FULL_DECODE_ONLY) and the configs that must still pass (defaults, the deprecated shared sizes, dynamic spec decode, FULL mode, no adaptive verification), on and off SM100. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7mm2nfeDm8urozFY3PCda Signed-off-by: Bo Pang <bopang@inferact.ai>
The prefill grid stepped by powers of two up to 256 and then by 256, so steps of 520-540 tokens padded to 768. On DeepSeek-V4.1 768 is the decoder-replay trim threshold, and under DP+EP those steps took the replay path every time: K3 at 512 and K7 at 256 concurrent requests ran 7-13% slower than main. Step by a quarter of the size below 1024 (8 below 64): 54 sizes at 8192 instead of 39, the same sizes above 1024. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7mm2nfeDm8urozFY3PCda Signed-off-by: Bo Pang <bopang@inferact.ai>
Overview
Model runner V2 captures CUDA graphs from one token list (
cudagraph_capture_sizes, capped bymax_cudagraph_capture_size, 1024 tokens on GB200), shared by prefill and decode. With speculative decoding a decode request holds 1 + K tokens, so decode graphs stop at 1024 / (1 + K) requests (128 at K = 7), and every prefill or mixed step above 1024 tokens runs eagerly.This PR gives each kind its own setting in its own unit: prefill graphs in tokens, decode graphs in requests, plus a token budget per decode graph. On SM100 (GB200, B200) the defaults cover prefill and mixed steps up to 8192 tokens and decode batches up to
max_num_seqs, within 4096 tokens per decode graph (1024 requests for K ≤ 3, 512 at K = 7). The default grids are coarser than main's list, so at the GB200 defaults every runner captures fewer graphs than main. On other GPUs the prefill and target decode defaults stay as on main.This is the "separate prefill/decode graph sizes" item of the Q4 roadmap (vllm-project#61024).
Claims
All on GB200 with the
vllm servedefaults (max_num_seqs1024,max_num_batched_tokens16384), against main60a7d568f4.The gain comes from steps that are eager on main: mixed steps of 1K–8K tokens (PIECEWISE here) and, at 256–512 requests, decode batches above 1024 / (1 + K) requests (FULL here).
Not for every setting: with
max_num_seqs256 and no SD, target decode is 37 graphs (main 35); off SM100, one-token draft decodes are 64 (main 51) and dynamic SD target decode 176 (main 147).Configuration
Five fields on
CompilationConfig(--compilation-config '{...}'):max_prefill_cudagraph_capture_tokensmin(8192, max_num_batched_tokens)max_cudagraph_capture_size, as on main (512 on H100/H200)prefill_cudagraph_capture_token_sizesmax_decode_cudagraph_capture_requestsq(1 + K for the target)max_num_seqsdecode_cudagraph_capture_request_sizesnrequests asn · qtokensmax_decode_cudagraph_capture_tokensn · q ≤budget, including q=1. Larger batches use PIECEWISE where supported, otherwise eager; AV is exempt. Memory safeguard and follow-upsDefault grids, and how they differ from main. On main, one token list (1, 2, 4, then every 8 up to 256, then every 16 up to 1024: 83 sizes) serves both kinds. A decode graph is a list entry divisible by 1 + K, so decode reaches 1024 / (1 + K) requests: 1024 / 512 / 336 / 256 / 168 / 128 at K = 0 / 1 / 2 / 3 / 5 / 7.
1, 2, 4, …, 256; then every 256 tokens through 7680; finally 8192. General rule: Appendix AGraph count here counts capture sizes per routine; one PIECEWISE size can contain multiple CUDA graph objects. GB200 defaults: 1024 sequences, 16384 batched tokens. AV uses
1, 2, 4, then four sizes per doubling through 1024, every 128 through 4096, and every 256 above, capped atmin(decode cap × (1 + K), max_num_batched_tokens).How to configure.
[]to disable that kind; set both lists to[]to disable CUDA graphs.Size limits, graph-mode exceptions, and legacy compatibility
max_num_batched_tokens, decode ≤max_num_seqs.FULLandPIECEWISEuse the prefill grid for all batches.max_cudagraph_capture_size / q. That recorded maximum covers all graph kinds, so changing the prefill cap can change draft FULL coverage. Example: DFlash K=7 with dynamic SD 7/3/1, prefill cap 128 → 8192: 37 → 48 draft graphs, reaching 256 → 512 requests. The other runners use their separate prefill/decode settings as described in the runner table.cudagraph_capture_sizesandmax_cudagraph_capture_sizeinitialize both kinds with a deprecation warning. After resolution, they record the captured token sizes / maximum for buffer sizing. Compatibility does not mean identical graphs for AV or DSpark: with a legacy power-of-two token list through 512 at K=5, AV gets 25 sizes through 258 tokens (main: 10 through 512), and DSpark reaches 43 requests (main: 52). Model Runner V1 is unchanged and rejects the new prefill/decode fields.Examples.
Results at the current defaults
Performance at matched SD settings. These measurements isolate the effect of this PR at the listed K; they do not establish gains over a tuned main baseline. Output tok/s, this PR / main, one untraced launch per build (
--numa-bind, after a throwaway launch). ShareGPT_V3 prompts with their own lengths,--ignore-eos, temperature 0, 1024 output tokens:Output throughput (tokens/s; higher is better).
Time per output token (TPOT, ms; lower is better).
More configurations (this head, one launch per build, ShareGPT through the chat endpoint with each model's template, 1024 output tokens; 16 / 64 / 512 prompts at concurrency 1 / 16 / 256, 1024 at 512):
max_num_seqs256)Correctness with AV enabled: this checks whether the new graph configuration reduces GSM8K accuracy. DeepSeek-V4 K=7 scored 95.00% on main → 94.77% on PR; no statistically significant drop was detected in this paired run. The table’s 96.8% is answer agreement, not accuracy. Paired results.
Run conditions. One untraced launch per build; small gains need repeated runs. Main:
60a7d568f4; PR:30c305c944. Build details and commands are folded below; engine step times are in Appendix C.Build details, commands and logs
AV paired correctness check: main 1253/1319 correct; PR 1250/1319. Of the changed outcomes, 18 went correct→wrong and 15 wrong→correct; exact two-sided McNemar p=0.73. This tests for an accuracy difference in this run, not bitwise identity or a proof of equivalence.
C64/C128 are single-launch measurements with finite request counts; throughput includes startup/drain effects. DeepSeek used an unmeasured warm-up to exclude a cold Triton compilation observed in the first attempt. These points are not independent repetitions of the other rows.
Build details. The original DeepSeek-V4 K=7 and MiniMax rows use
a4975e7fd4, whose differences from30c305c944are tests and the AV grid, unused by these runs. C64/C128 use the current head. Both builds use the same compiled binaries. Earlier repeat launches varied by up to 10%; this is observed variability, not a confidence interval for these single-launch results.Server logs, bench JSONs, step traces and the run scripts are kept on our cluster and available on request.
Memory: three causes and their fixes
The memory increase is substantial today. Follow-up prototypes have reduced it in measured runs; the current-grid combination is being validated separately.
The two completed experiments used earlier grids without the 4096-token decode limit. They demonstrate that the identified allocations can be reduced; they do not establish the current-grid after total. Current-grid validation measures memory and checks GSM8K again. The estimator fix is a separate accounting change: it can recover KV budget without shrinking graph tensors.
The three physical-memory issues are sparse-indexer logits, auxiliary hidden states retained across capture sizes, and DSpark draft logits. Each section names the affected implementation, explains the cause and fix, then gives a measured model example. Memory estimation is a separate accounting issue.
GB200, GiB per GPU (rank 0), with default grids and a 4096-token decode budget; no adaptive verification.
Added GPU memory means device usage after graph setup minus before it (the log calls this
actual). It includes graph pools and other allocations. For AV, graph setup also runs the cost-curve profiling forwards. Weights and KV already allocated before this step are excluded.Measurement details and source logs
DeepSeek-V4-Flash: TP4+EP, DSpark K=7, 1M context. MiniMax-M3: TP4, EAGLE3 K=3. Allocator breakdown: one instrumented startup per build, PR
79fb06ad41(30c305c944+ instrumentation), main9ac2d91b11(60a7d568f4+ instrumentation). Sources:p0final/out/{dsv4,m3}_memb_{main,pr}/analysis.rank0.jsonand allocator snapshots, retained on the cluster. These are current-budget measurements; the earlier no-budget optimization stack is folded below.KV counts are from the serving comparison (
a4975e7fd4, equivalent to this head for these non-AV paths); the instrumented DeepSeek PR startup had 6,208,515 tokens. Rounding can affect the last digit. The outside-pool residual includes graph/runtime/library allocations; it has not been fully separated into graph-object bytes and other owners. It is not all graph objects. Allocations already retained from startup profiling are outside this real-capture delta: outside-pool memory retained after profiling was DeepSeek 2.20 → 1.40 GiB and MiniMax 1.49 → 1.14 GiB. These lifecycle measurements are not independent charges to add blindly. A complete pure graph-object attribution remains open. Here profiling means startup memory estimation; prefill means a model execution phase.KV budgeting is separate from physical memory usage. The 29.19 GiB below is a software estimate, not another allocation: DeepSeek PR actually adds 18.66 GiB above. An inaccurate estimate changes the KV budget; the estimator follow-up addresses this separately.
Pool reservations and live-tensor peaks are different views of the same memory; do not add them together.
1. Sparse-attention indexer: chunk token-selection logits
Tensor and cause:
logits = fp8_fp4_paged_mqa_logits(...)materializes BF16 scores over candidate KV positions;as_fp32_logitscreates an FP32 copy for the selected top-k backend. Top-k selection consumes these logits before sparse attention uses the selected positions. The workspace grows with query rows × context length. This affects that implementation path, not every sparse-attention model.Measured example — DeepSeek-V4-Flash: BF16 indexer logits and their FP32 conversion trigger 10.02 GiB of the 10.53 GiB target FULL pool increment, with the 4096-token budget already enabled (512 requests × q=8, 1M context). These are KV-selection scores, distinct from the vocabulary logits in issue 3.
Shape behind the 8.016 GiB: 512 requests × (K+1=8) = 4096 query tokens, with a 1M-token context limit. The indexer scores the compressed KV positions for each row. The allocator trace attributes two 4104 MiB pool segments to FP32 conversion (8.016 GiB total); this is reserved pool growth, not one 8 GiB live tensor. Capturing this shape keeps up to 512 concurrent uniform requests on FULL graphs. It is a coverage choice, not a mandatory size: a lower decode cap saves memory but sends larger batches to PIECEWISE/eager.
TODO: compute and convert logits in row chunks, reusing smaller buffers. #62169 covers SM90/SM120; the SM100 prototype is published in this fork for inspection, but is not merged or included in this PR. This applies to the affected sparse-indexer implementation.
_alloc_attn_out_fused_qnorm_rope_kv_insertMeasured result of indexer chunking: on the earlier grid, this fix alone reduced added GPU memory from 25.35 to 15.35 GiB/GPU. The current-grid after value is being measured. Replay still needs working storage; #62860 separately budgets eager workspace and does not itself shrink the logits.
2. Auxiliary hidden states: release outputs retained across capture sizes
Scope and cause: drafters that consume target auxiliary hidden states are affected when nested output references keep those tensors alive across PIECEWISE capture sizes. The current draft round needs its hidden states; retaining separate outputs for every capture size is unnecessary.
Pending fix: use weak references for nested auxiliary outputs (#63427, open and not merged; not included in this PR).
Measured examples: MiniMax + EAGLE3 retains 4.378 GiB within its 4.619 GiB target PIECEWISE pool; DeepSeek + DSpark retains 2.918 GiB. These are two instances of the same output-lifetime issue.
The final pool saving depends on the working storage needed for replay. The 4.378 GiB is retained outputs, not MoE workspace; no MoE-chunking change is proposed from these measurements.
Memory after the aux-output fix: current-grid after-fix measurements are still TODO. Earlier-grid tests confirm the fix reduces memory, but their totals cannot be substituted for today's grids.
Earlier-grid validation: 72 PIECEWISE sizes, no 4096-token decode limit, aux-output fix only (
design/FINAL_VALIDATION.md, job 39109). Retained aux tensors fell to zero; replay working storage still remains. Do not subtract today's 4.378 / 2.918 GiB from today's reserved pool to predict the after-fix total.3. DSpark drafter: compute logits per step and reuse temporary storage
Problem: the generic DSpark sampling path computes all K steps' vocabulary logits before sequential sampling. Temporary logits grow with requests × K × vocabulary size. This affects models using that path, not just DeepSeek; custom sampling implementations can differ.
TODO: compute one step's logits, sample it, then reuse the temporary storage for the next step. A local step-chunking prototype exists; no public PR yet. The persistent logits cache needed for probabilistic verification remains separate.
Measured logits shape and memory (DeepSeek-V4, TP4, DSpark K=7): the largest draft graph covers 585 requests, or 4095 draft rows. With a 129,280-token vocabulary and BF16 (2 bytes/value), the trace shows:
4095 × 3232016380 × 32320(same element count as4095 × 129280)4095 × 129280Shapes follow the code and BF16 configuration; the allocator trace confirms their storage sizes and simultaneous lifetimes. The drafter's total tensor peak is 2.313 GiB; its reserved pool is 2.994 GiB. These are overlapping views, not costs to add together. Step-wise logits can reduce the temporary buffers; the probabilistic-verification cache is separate.
Additional reuse fix: capture target and drafter sequentially on one capture stream per GPU (#59537), reducing stream-local workspace duplication and allowing pool reuse. This is separate from step chunking. Savings with the current grids remain to be measured; the table is one configuration, not a universal DSpark cost. In the earlier combined prototype, after indexer chunking, aux-output release and step-wise logits were already applied, the shared-stream fix reduced measured capture memory from 8.43 to 6.00 GiB/GPU. This is an incremental result on that earlier stack; the current-grid after-fix total remains TODO.
Measured graph counts, capture-stage memory, and remaining workspace
Each stage reports the additional memory reserved by the graph pool, in GiB/GPU; the total is the sum of those increments. Graph count counts capture sizes; one PIECEWISE size may contain multiple CUDA graph objects. These tables cover the measured DeepSeek + DSpark and MiniMax + EAGLE3 deployments, so each includes its own drafter.
For the measured MiniMax K=3 example, target and draft step 0 use q=4: main reaches 256 × 4 tokens, PR reaches 1024 × 4. Target capture sizes total 164 → 103; EAGLE3 drafter sizes total 247 → 167. A zero step-0 FULL pool increment means reuse of already-reserved space.
MiniMax target uniform-decode graph pool (FULL mode, 0.662 GiB):
MiniMax's PIECEWISE MoE live peak is 0.678 GiB; the FULL peak is 0.290 GiB. These use the reported pools and are not additional charges. Required working storage remains after fixing retention.
Graph/runtime objects and other outside-pool allocations are included in the total-memory table, but a complete pure graph-object attribution remains open. These two configurations do not establish the same breakdown for other models.
The separate memory-budget estimator determines how much KV cache is admitted; it does not change the size of the three allocations above. Its measurements follow the graph inventory.
Graphs per runner (real config resolution and
CudaGraphManageron CPU, this head vs main;FULL_AND_PIECEWISE; GB200: 1024 sequences / 16384 batched tokens with the decode budget; off SM100: 1024 / 8192). Cells give graph count (capture sizes) and maximum coverage; parentheses explicitly label the main baseline. Target and drafter capture their own graphs. Adaptive verification changes how many draft tokens the target verifies per request; its target FULL graphs therefore support variable query lengths. Draft-generation graphs remain in the drafter rows.q = 1 + K(requests)max_decode_cudagraph_capture_tokensq = 1, draft tokens 2 … Kq = K)q = K + 1)q = K + 1)qper tier; one-token decode per K ≥ 2Closing the DeepSeek-V4 KV cache gap
The current-budget physical breakdown is above: DeepSeek-V4-Flash DSpark K=7 retains 0.81× of main's KV capacity, and MiniMax-M3 EAGLE3 K=3 retains 0.94×. The follow-ups address different owners and conditions; indexer chunking is specific to the affected sparse-indexer path, aux-output weak references apply where those outputs are retained, and the estimator needs checking across models.
Why the decode budget exists. The SM100 default of 4096 tokens is an interim memory safeguard against wide FULL captures, especially the target sparse-indexer workspace (issue 1); multi-token drafter allocations are also bounded. It is not a drafter-specific limit. Reassess the default after workspace fixes and measurements on the current grids; the configurable budget remains useful. Large K also has a compute cost (Appendix B).
Adaptive verification: measured memory and fixes
GB200, GiB/GPU. Graph setup includes target/drafter capture and AV cost-curve profiling. The original serving runs below measured the total memory increase and KV capacity; the new allocator measurements then separate what occupies that memory.
New measured breakdown — DeepSeek-V4, DSpark K=7 + AV. The similar totals hide a larger graph pool and less idle ordinary-allocator cache in the PR.
Conclusion: AV has both a real graph-pool increase and a cache/estimation problem. Clearing the ordinary idle cache (#61509) does not remove the persistent graph pool or runtime workspace requirements. The physical fixes above and the estimator fix must be evaluated separately; cache bytes cannot simply be converted into promised KV gains.
One MEMB-instrumented startup per build, frozen main
60a7d568f4/ PR30c305c944; rank 0 and rank 3 agree. Totals reproduce the original DeepSeek-V4 runs. Source:avmem/out/job_39639/{dsv4_main,dsv4_pr}anddesign/AV_MEMORY_BREAKDOWN.md. AV’s 8192-token target grid is exempt from the uniform-decode budget; its 13.654 GiB FULL pool is a different configuration from the no-AV 4096-token / 10.525 GiB example. DeepSeek-V4.1’s paired allocator breakdown is still running.TODO: release AV profiling cache (vllm-project#61509), correct the full-initialization estimate, then remeasure actual memory, KV capacity and serving performance. The fixes and their status are listed below.
Source:
design/P1_RESULTS_RAW.md, the underlyingP1_dsv4sd/P1_dsv41logs, anddesign/EST_BLOCKER.md. These totals are measured; a per-owner AV pool breakdown must not be inferred from them.Memory-estimator TODO across models
Separate issue: total memory budget error after initialization. This includes graph-memory estimation and other unbudgeted buffers. Values are actual usage minus the configured GPU memory budget: positive = budget exceeded; negative = unused budget that could hold KV. Final head, one launch per build. Diagnostic instrumentation calls
empty_cache()to separate terms, so these runs must be kept separate from the uninstrumented AV totals above.empty_cache()and are extrapolated (large only with DeepSeek's indexer warmup) and (b) the adaptive-verification profiling cache ([Bugfix][MRV2] Don't count adaptive verification profiling cache as CUDA graph memory vllm-project/vllm#61509). Under-reservation comes from (c) runner buffers allocated with the KV cache that the budget never counts (all models: 4.6 GiB DeepSeek-V4, 3.1 GiB Qwen3.8-27B, 1.6 GiB M3) and (d) runtime buffers larger than the profile (DeepSeek's eager indexer logits, [Bugfix][DSA] Reserve the decode indexer logits in the memory profile vllm-project/vllm#62860).profile_cudagraph_memory, plus [Bugfix][MRV2] Don't count adaptive verification profiling cache as CUDA graph memory vllm-project/vllm#61509 and [Bugfix][DSA] Reserve the decode indexer logits in the memory profile vllm-project/vllm#62860, and 6–59 s more startup.How much memory the follow-up fixes saved in earlier experiments
These measurements explain possible remedies, not the current 18.66 GiB breakdown. The 0.989× KV result is a combined prototype on an older head, not achieved by this PR alone. Savings overlap and cannot be subtracted independently from current totals.
On earlier grids, the budget reduced the logged capture delta from 25.4 to 15.9 GiB (KV 0.83×). With the final grids it is 18.66 GiB despite about 1 GiB less live memory: allocator snapshots show unreused target-pool space growing from 7.0 to 10.9 GiB. Capture layout and reuse matter as well as live-tensor size.
Earlier-grid prototype
4a3270c7ad, one launch per setting, rank 0. Physical fixes are applied cumulatively below; the final estimator row uses a separate accounting scope.Measured step by step:
4a3270c7ad, before the 4096-token budgetThe stack keeps this PR's throughput (×3.0 at 256 requests), completes 1024 requests where main crashes, and scores GSM8K 0.949–0.955 against main's 0.950. On Kimi-K3 (8-layer dummy, DSpark K=7), #63427 plus step-chunked draft logits take KV from 0.82× to 0.94×.
Limitations
max_num_seqs(64 graphs, main 51); DSpark reaches 64 requests at K = 7 (main 71); adaptive verification 28 graphs (main 53); dynamic SD 176 target decode graphs (main 147); GPT-OSS decode 64 (main 83) at the same reach. Computed, not measured on H100/H200.Test plan and results
Validation: on GB200, configuration tests passed (110/110) and CUDA-graph manager tests passed (28/28);
pre-commitis clean. Model correctness results are in Results. Commands, CPU-only limitations and detailed counts are folded below.Test commands and detailed results
30c305c944(this head,CUDA_VISIBLE_DEVICES=).test_config.py: 98 passed; the other 12 start avllm_runnerand need a GPU. The other three files: 53 passed, 9 skipped, 1 failed (needs a CUDA device). Main fails the same GPU-only tests on CPU.30c305c944:test_config.py110 passed,test_cudagraph_manager.py28 passed.CudaGraphManageron CPU, at30c305c944and at main.pre-commiton60a7d568f4..30c305c944: clean.Not a duplicate
Appendix A. How the grids were chosen
Data. Measured on earlier PR heads, traced. The candidate comparison used one server holding every candidate's graphs, without the decode budget.
The rule. Each grid captures a graph wherever padding to the next size would cost more than a fixed share ε of the step's time. A batch runs on the smallest graph that holds it, and padded rows are not free, so the spacing between sizes follows how fast step time grows with batch size, regime by regime.
Graph count costs little:
With an aux-state drafter, every PIECEWISE size also keeps 24–36 KiB per captured token until vllm-project#63427 lands.
(a) What a padded row costs, per operator class. Measured inside FULL decode replays with per-segment GPU timing:
seq_len0 are skippedsm100_paged_mqa_logits)(b) Decode step time, by regime in tokens
t = q · n. Marginal cost per request, as a multiple of the linear-tail slope:T = T0 + s · n, with an intercept of 650–950 tokens.n · qtokens, though, so it can still cross a step: at q = 3, 673 requests pad to 704, which is 2019 → 2112 tokens and crosses 2048.(c) Decode step rule.
nby δ costs aboute(n) · δ / nof its step time, wheree(n) = n · c′(n) / T(n).Δ(n) = 2εn / e(n).e ≈ n^(1/3) / 5, givesΔ(n) = 10 ε n^(2/3). Rounded down to a power of two, with ε = 0.065, that is the decode rule: 64 sizes at 1024.(d) PIECEWISE rule. PIECEWISE step time is flat below 1–4K tokens and linear above (11.8–14.0 ms per 1K tokens per rank). The same rule with
e_PW(t) = min(1, t / 4096)and ε = 0.035 gives the prefill grid: 39 sizes at 8192, which sum to 127,487 tokens (main: 35,335).The coefficient 0.07 is
2 × ε, with ε = 0.035, used to choose grid spacing; it is not a measured 7% slowdown or a universal padding bound. Assuming batches land roughly uniformly inside a grid interval, average padding is about half its width. Under the fitted cost model, the nominal stride ismin(t, 0.07 × max(t, 4096)), rounded down to a power of two; the next size is the next multiple of that stride, with the cap always included. For example, att=4096,0.07×4096=286.72gives stride 256; att=7680,0.07×7680=537.6gives stride 512 and the next size is 8192. At the default cap the exact sequence is the one listed in Configuration. This is an empirical cost-model choice, checked by the candidate-grid measurements below, not an end-to-end performance guarantee.(e) Measured candidates. One server held the union of the candidates' graphs and switched grids at idle, in rotated order. Each cell is the worst change in engine GPU time against the earlier 72 / 72 grids:
Appendix B. Why high concurrency + large K is not a target
Data. Earlier grids, no budget, untraced, one launch per configuration. DeepSeek-V4-Flash DSpark, tok/s on semantic prompts (chat, code, math; acceptance 3.4–4.3 at K=7):
n · (1 + K)tokens, so a large K spends compute on drafts that get rejected.Why use a 4096-token decode budget? It trades some FULL-graph coverage for lower memory. In the earlier DeepSeek-V4 + DSpark K=7 tests below, 4096 preserved throughput at 512 requests within single-run noise; lowering it to 2048 caused OOM without indexer chunking. AV is exempt because bounding it reduced throughput without saving memory. One launch per setting:
Appendix C. Where the performance comes from
Data. Earlier grids (72 / 72, PR
4a3270c7adwith tracing), no budget. Every engine step of one launch per build was traced, and steps were bucketed by kind and size. The per-bucket savings reproduce the measured engine-time difference within 1–10%. Gains come only from steps that run eagerly on main and on a graph here.Final grids. Build
147a0ced88(this PR's grids before the decode budget), tracing on, 2 launches per build; an earlier run than the Results table. Medians, main → PR:Appendix D. Per-model validation on the earlier grids (superseded by the Results tables)
Data. Earlier grids, PR
4a3270c7ad(72 / 72 graphs, the same widest graphs and coverage as this head), no budget, untraced. Main is60a7d568f4.max_num_seqs256)Kimi-Linear correctness: no regression observed on the current head. GSM8K accuracy is 89.7% on main → 90.1% on PR, one evaluation per build. The earlier-grid results in the table above are historical measurements, not a current correctness failure.
Other notes.
p0final/out/m3_g_{main,pr}{2,3,4}/gsm8k.json.Appendix E. Follow-ups
rows × max_model_len / 4, BF16 + FP32) are allocated whole: 12 GiB at 1024 × 8 rowsfix/dsa-indexer-decode-chunk-sm100; extends vllm-project#62169 (SM90/SM120); vllm-project#62860weak_ref_tensorsconverts only the outer tupleempty_cache(), the AV profiling cache, and runner buffers outside the budgetn · (1 + K)measured within ±0.25% engine time; a rule derived from the decode regimes in tokens might need fewerAppendix F. Dense models on one GPU (Qwen3.8-27B): measured trade-offs and tuning evidence
What we measured. Qwen3.8-27B with MTP K=3 on one GB200, final head against main, ShareGPT prompts with 1024 output tokens:
Why the wider graphs do not pay off here. A dense 27B model on one GPU is GPU-bound from about 1.5K tokens per step: a PIECEWISE graph shortens a mixed step by 14–21%, but the next step grows by the same amount, so the pair takes the same time (1.5–2K tokens: 147 vs 151 ms; 2–3K: 188 vs 188 ms). The host launch time that the graph removes was already hidden behind GPU work. Large MoE models with tensor or expert parallelism are the opposite: many small kernels per layer make the host the bottleneck, which is where this PR's gains come from. Padding costs this model nothing measurable (decode batches padded to 160 requests run as fast as main's 152 / 156 graphs; GEMM tile quantization).
Tuning direction for dense models on one or a few GPUs: if wider graph coverage reduces KV capacity without improving throughput, try lowering the prefill capture cap. Consider lowering decode coverage only when the workload rarely reaches those batch sizes. Validate throughput, tail latency and KV capacity on the intended workload before choosing limits; no specific preset is recommended yet.