Conversation
Since vllm-project#47327 the dense-MHA prefill split routes only the leading decode tokens to forward_mqa, but the FlashMLA sparse forward paths still passed the full-batch req_id_per_token to triton_convert_req_index_to_global_index. The kernel grid is sized by req_id while the output is allocated like token_indices, so the extra rows were written past the end of the output buffer. Slice req_id_per_token to the MQA tokens at all three call sites, the same way the FlashInfer and FlashAttn sparse backends already do, and assert the length invariant in the shared conversion wrapper so a mismatch fails loudly instead of corrupting memory. Co-authored-by: Claude Signed-off-by: Mikhail Kostryukov <mike@triptrack.net>
Since vllm-project#47327 sparse-MLA prefills short enough for the dense-MHA path can carry cached context, and forward_mha then gathers that context from the KV cache. The gather kernels do not understand the fp8_ds_mla layout (656B entries: fp8 NoPE + inline tile scales + bf16 RoPE): - with DCP, the dequant branch excludes fp8_ds_mla, so raw cp_gather_cache hits its src/dst same-dtype check (uint8 cache vs bf16 workspace) and raises; - without DCP, gather_and_maybe_dequant_cache dispatches fp8_ds_mla as plain fp8 and dequantizes the first 576 bytes of each entry elementwise with the global scale, silently corrupting K/V (the inline scales and part of the bf16 RoPE bytes are read as fp8 values). Block use_mha only when the kv cache is fp8_ds_mla and the prefill actually has chunked context to read; no-context short prefills keep the dense-MHA fast path. Co-authored-by: Claude Signed-off-by: Mikhail Kostryukov <mike@triptrack.net>
drakosha
requested review from
LucasWilkinson,
MatthewBonanni and
pavanimajety
as code owners
July 14, 2026 13:15
Member
|
Thanks for catching these! I'd like to supersede this PR with #48642, though, which enables dense MHA in these circumstances instead of routing away from it. |
This was referenced Jul 14, 2026
Contributor
Author
|
Closing in favour of #48642 — enabling dense MHA is the better fix than routing those prefills back to MQA, and it also covers Two notes:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FIX #48611
Purpose
Two regressions from #47327 (dense-MHA path for sparse-MLA short sequences). Full root-cause analysis with
file:lineis in the linked issue; the short version:1. OOB write in the FlashMLA sparse index conversion. After #47327,
forward_mqareceives only the MQA subset (the leading decode tokens), but all three FlashMLA forward paths still passed the full-batchreq_id_per_tokentotriton_convert_req_index_to_global_index, whose grid is sized byreq_idwhile the output is allocated liketoken_indices— the extra rows were written past the end of the output buffer. Fixed by slicingreq_id_per_token[: topk_indices.shape[0]]at the three call sites, exactly as the FlashInfer and FlashAttn sparse backends already do, plus a length-invariant assert in the shared converter so a future mismatch fails loudly instead of corrupting memory.2.
fp8_ds_mlacontext gather on the dense-MHA path. When a short prefill carries cached context,forward_mhagathers it with kernels that do not understand the 656Bfp8_ds_mlaentry layout: rawcp_gather_cacherejects the uint8/bf16 dtype mismatch under DCP, andgather_and_maybe_dequant_cacheotherwise dequantizes the raw bytes as flat fp8 with a single global scale, silently corrupting the context K/V. Fixed by keeping such prefills on the top-k MQA path, gated onkv_cache_dtype == "fp8_ds_mla"andprefill.chunked_context is not None.Short no-context prefills — the case #47327 optimizes and benchmarks (DeepSeek-V3.2 short prompts) — never read the cache, so they keep the new dense-MHA fast path. kv-cache-dtype aliases cannot bypass the gate:
fp8/fp8_e4m3are canonicalized tofp8_ds_mlabefore the layer storeskv_cache_dtype.Not a duplicate: no open PR addresses either regression (searched
req_id_per_token, sparse-MLA /fp8_ds_mladense-MHA). #34744 is the masked-MHA follow-up to #47327; #45537 is an unrelatedcp_gather_cachebounds fix.Test plan
New regression tests in
tests/v1/attention/test_sparse_mla_backends.py:test_triton_convert_rejects_req_id_longer_than_token_indices— the converter rejects the full-batch/subset length mismatch instead of writing OOB, and the sliced call matches the reference.test_flashmla_forward_bf16_kv_slices_req_id_to_mqa_tokens— call-site regression:_forward_bf16_kvwith full-batch metadata and a subsetq/topk_indicesconverts exactly the MQA rows (fails before the fix).test_fp8_ds_mla_context_prefill_stays_on_mqa_path— the dense-MHA path is blocked precisely forfp8_ds_mla+ chunked context and stays available with no context; covers dtype-alias canonicalization.pytest tests/v1/attention/test_sparse_mla_backends.py -v \ -k "triton_convert or forward_bf16_kv_slices or fp8_ds_mla_context"Test result
ruff check/ruff formaton all touched files: clean.pytest tests/v1/attention/test_sparse_mla_backends.py→ 111 passed, 422 skipped, 0 failed (8 min). The three new regression tests pass by name:test_triton_convert_rejects_req_id_longer_than_token_indicesPASSEDtest_flashmla_forward_bf16_kv_slices_req_id_to_mqa_tokensPASSEDtest_fp8_ds_mla_context_prefill_stays_on_mqa_pathPASSEDfp8_ds_mla, MTP, prefix caching + CPU KV offload). Multi-turn chat with prefix-cache hits — the exact case that hits the brokenfp8_ds_mlacontext gather — returns correct output; a 4×551k-token concurrent stress run retrieved all 4 needles with no crash.AI assistance
AI assistance (Claude) was used for this change. The human submitter reviewed every changed line and ran the tests above.