Skip to content

Commit 290fdfd

Browse files
committed
[nvbugs/6276981][fix] DSA chunked-prefill indexer: always q-split when eligible
When the indexer chunked-prefill is gated by q_split_eligible (TP > 1, no attention DP) but apply_q_split is False (chunk smaller than q_split_threshold), every TP rank computes the full chunk's topk indices independently via fp8_mqa_logits / fp8_fp4_mqa_logits. Those DeepGEMM kernels are not bit-exact across launches, so per-rank topk indices diverge for the same tokens. The downstream MLA attention then attends to different KV positions on different ranks, corrupting KV-cache writes. Short generations (MMLU's 2-token answers) hide it; long ones (GSM8K's 256 tokens) compound it into garbage and 0% accuracy. Force the q-split + allgather path whenever eligible: small chunks pay a microscopic allgather instead of redundant per-rank logits compute, and the per-token canonical owner from the slice + allgather erases any rank-local nondeterminism before downstream layers read the indices. q_split_threshold < 0 still fully disables eligibility. Signed-off-by: tensorrt-cicd <90828364+tensorrt-cicd@users.noreply.github.com>
1 parent eb2bddd commit 290fdfd

3 files changed

Lines changed: 85 additions & 105 deletions

File tree

tensorrt_llm/_torch/attention_backend/sparse/dsa.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,8 +2016,19 @@ def sparse_attn_indexer(
20162016
num_k_tokens, gather_head_dim)
20172017

20182018
chunk_num_token = chunk.token_end - chunk.token_start
2019-
apply_q_split = q_split_eligible and chunk_num_token >= q_split_threshold
2020-
if apply_q_split:
2019+
# Always q-split when eligible: redundant per-rank
2020+
# `fp8_mqa_logits` / `fp8_fp4_mqa_logits` are not bit-exact
2021+
# across launches, so different TP ranks produce different
2022+
# topk indices for the same tokens. The downstream MLA
2023+
# attention then attends to divergent KV positions on
2024+
# different ranks, corrupting KV-cache writes -- invisible
2025+
# for short generations (e.g. MMLU's 2-token answers) but
2026+
# accumulates into garbage over long ones (GSM8K's 256
2027+
# tokens) -> 0% accuracy. The q-split + allgather path
2028+
# canonicalizes ownership per-token, so any rank-local
2029+
# nondeterminism is erased before downstream layers read
2030+
# the indices. q_split_threshold < 0 still fully disables.
2031+
if q_split_eligible:
20212032
chunk_q_start = chunk_num_token * tp_rank // tp_size
20222033
chunk_q_end = chunk_num_token * (tp_rank + 1) // tp_size
20232034
else:
@@ -2065,7 +2076,7 @@ def sparse_attn_indexer(
20652076
global_q_start:global_q_end, :topk_indices.
20662077
shape[-1]] = topk_indices.to(dtype=torch.int32)
20672078

2068-
if apply_q_split:
2079+
if q_split_eligible:
20692080
q_sizes = [(r + 1) * chunk_num_token // tp_size -
20702081
r * chunk_num_token // tp_size
20712082
for r in range(tp_size)]

0 commit comments

Comments
 (0)