Repository navigation
[Bugfix][DSA] Reserve the decode indexer logits in the memory profile - #62860
Conversation
The DSA indexer's decode path allocates its paged MQA logits per call as [decode rows, max_model_len] (BF16 on SM100, FP32 elsewhere) and, on SM100, upcasts them to FP32 for every top-k backend but DeepSelect. The profiling run reserved only VLLM_SPARSE_INDEXER_MAX_LOGITS_MB for logits, the prefill chunks' budget, so the KV cache took the memory the decode logits need. CUDA graphs keep their own copy in the graph pool; eager and piecewise steps allocate from the remainder and, with many decodes at a long max_model_len, run out of memory. Reserve the larger of the prefill budget and the decode worst case: min(max_num_seqs, max_num_batched_tokens) * (1 + speculative tokens) rows at the padded max_model_len width, counting the FP32 upcast where it runs. The layer computes the row bound at construction and passes it to the op; the runtime path and the reservation share the upcast decision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Canlin <canlinguosdu@gmail.com>
|
I think you can drop the test and make the logic a bit more concise i.e. inline functions appropriately |
Thanks! I will do some clean up for it. |
Per review: the profiling-path reservation is covered by the memory numbers in the PR description; the unit test mostly re-stated the formula. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Canlin <canlinguosdu@gmail.com>
Per review: compute the reserved bytes in the profiling branch and keep the FP32-copy predicate in one local shared with the runtime upcast, instead of two module-level helpers. Same reservation as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Canlin <canlinguosdu@gmail.com>
On SM100 DeepGEMM pads each paged MQA logits row to whole 384-column KV splits and 1024-byte strides, lcm 1536 BF16 columns (1,049,088 for a 1M max_model_len, as in the OOM report), and the FP32 copy keeps that stride. Rounding to 1024 columns reserved up to 1535 columns per row too little. Other platforms keep the 1024-column bound, above their 256-column padding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Canlin <canlinguosdu@gmail.com>
…yout Spell the reservation as DeepGEMM's layout instead of bare byte counts and alignments: the logits dtype (BF16 on SM100, FP32 elsewhere) and its KV split width (384 / 256 columns), rows aligned to lcm(split, 1024-byte stride), plus the FP32 copy on SM100 when the top-k reads FP32. Other platforms now pad to DeepGEMM's 256 columns instead of an upper bound of 1024. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Canlin <canlinguosdu@gmail.com>
|
@gau-nernst READY now. Could you take a look again? |
…rofiling run Per review: the bound only sizes the profiling reservation, so compute it there from get_current_vllm_config_or_none() instead of passing it through the custom op. The profiling run sets the config; other dummy runs (e.g. DP idle-rank batches) may not, and keep the prefill budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Canlin <canlinguosdu@gmail.com>
It looks like it has been added in pr-checklist skill. |
|
/ci run |
|
✅ Triggered Buildkite CI #97788 for commit |
|
✅ @gcanlin, CI is now available for this PR.
|
|
@gcanlin help me fix the CI. This is another reason I hate mocking via Namespace object 😂 |
The profiling branch now reads max_num_seqs, max_num_batched_tokens and num_speculative_tokens from the current vLLM config, so the test's config stub needs them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Canlin <canlinguosdu@gmail.com>
|
/ci retry |
|
✅ Triggered Buildkite CI #97875 for commit |
Fixed. I can open a new PR to refactor the UT. |
|
/ci retry |
|
✅ Triggered Buildkite CI #97897 for commit |
probably not necessary. it's funny to have a PR just to fix it. we just have to live with it for now. |
|
@gau-nernst CI pass now. ready to merge |
…vllm-project#62860) Signed-off-by: Canlin <canlinguosdu@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> (cherry picked from commit 317081f)
Conflict in vllm/model_executor/layers/sparse_attn_indexer.py: the dense prefill path keeps the fused_mqa_topk row-window loop and takes upstream's DeepSelect BF16 ranking (vllm-project#62240) per window; the profiling branch takes upstream's logits reservation (vllm-project#62860, vllm-project#63047) next to the fused scratch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: LopezCastroRoberto <rocastro@redhat.com>
Overview
The DSA indexer's decode logits are not counted by the memory profiler, so eager and piecewise steps can OOM at runtime. This PR reserves them in the profiling run, the way DeepInfer's profile run does.
OOM
DeepSeek-V4.1-Flash, 4×B200 DEP4 (InferenceX recipe,
--max-num-seqs 256,--max-model-len 1048576, 5 spec tokens,--gpu-memory-utilization 0.97), AgentX at concurrency 512. Rank 0 died 5 minutes into the load:Why
[decode rows, max_model_len], in BF16 on SM100.deep_selectthen reads an FP32 copy made byas_fp32_logits. Here that copy alone is 786 rows (131 decodes × 6) × 1,049,088 columns × 4 B = 3.07 GiB.VLLM_SPARSE_INDEXER_MAX_LOGITS_MB(512 MiB, the prefill chunk budget), so the KV cache takes this memory.How DeepInfer handles it
DeepInfer@774ce24 sizes the decode logits the same way:
max_kv_len = runtime_max_model_len // kv_compress_ratio(index_attn.py#L95, #L434-L441). It keeps them in BF16 (#L87).On decode engines, its
profile_runrunsmax_num_seqssequences of1 + nextnquery tokens (model_runner.py#L444-L511). So the[max_num_seqs × (1 + nextn), max_kv_len]logits are part of the measured peak before the KV cache is sized.Fix
vLLM's shared indexer now does the same accounting. Its profiling (dummy) path reserves the larger of the prefill budget and the decode worst case:
min(max_num_seqs, max_num_batched_tokens) × (1 + num_speculative_tokens). The profiling branch reads it from the current vLLM config.max_model_len, padded as DeepGEMM pads the rows: whole KV splits (384 columns on SM100, 256 elsewhere) at a 1024-byte stride, i.e. 1536 BF16 columns on SM100 (1,049,088 for 1M, as in the OOM above) and 256 FP32 columns elsewhere.deep_selectand no DCP, 4 on other platforms (FP32).The runtime upcast and the reservation use the same predicate, so they cannot drift apart.
Memory
Same configuration, KV cache per rank at startup:
autoautodeep_selectdeep_selectMeasured before the width was aligned to 1536 columns, which adds 4.5 MiB (
auto) or 1.5 MiB (deep_select) to the reservation.The KV cache shrinks by less than the reservation, because the reservation stacks on top of what is live inside the indexer, not on top of the run's global peak. With
deep_selectthe reservation costs 0.2% of the KV cache. The decode graphs also drop the FP32 copy, sodeep_selectleaves 17.5% more KV cache thanautoon main.