Skip to content

Commit d6d1b48

Browse files
hyuknlfr-0531
andauthored
[None][fix] DSv4: bound indexer MQA-logits transient via query tiling (#15569)
Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com> Co-authored-by: Fanrong Li <23290157+lfr-0531@users.noreply.github.com>
1 parent af42658 commit d6d1b48

1 file changed

Lines changed: 68 additions & 38 deletions

File tree

  • tensorrt_llm/_torch/attention_backend/sparse

tensorrt_llm/_torch/attention_backend/sparse/dsa.py

Lines changed: 68 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# SPDX-License-Identifier: Apache-2.0
33
"""Dense Sparse Attention (DSA) backend for TRT-LLM with indexer-based TopK selection."""
44
import math
5+
import os
56
import threading
67
from contextlib import contextmanager
78
from dataclasses import dataclass
@@ -43,6 +44,18 @@
4344

4445
ModelConfig = tensorrt_llm.bindings.ModelConfig
4546

47+
# Cap the per-call indexer MQA-logits transient (in elements). fp8_mqa_logits
48+
# allocates its [q x kv] logits output via torch.empty; the KV dimension is the
49+
# full (compressed) context and is unbounded, so for a large query chunk on a
50+
# long-context prefill this single allocation can reach tens of GB. Under
51+
# PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True such an allocation can stall
52+
# indefinitely in cuMemCreate on the longest-context (attention_dp laggard) rank
53+
# -> GPU idle -> peers block at the next MoE all-to-all -> watchdog hang. Tiling
54+
# the query dimension caps the transient to q_tile x kv with identical results
55+
# (each query row's logits/top-k are independent). Override via env if needed.
56+
_INDEXER_MQA_LOGITS_ELEM_BUDGET = int(
57+
os.environ.get("TLLM_INDEXER_MQA_LOGITS_ELEM_BUDGET", 1 << 31))
58+
4659
if TYPE_CHECKING:
4760
from tensorrt_llm._torch.speculative.interface import SpecMetadata
4861
from tensorrt_llm._torch.speculative.spec_tree_manager import \
@@ -2218,44 +2231,61 @@ def sparse_attn_indexer(
22182231
global_q_start = chunk.token_start + chunk_q_start
22192232
global_q_end = chunk.token_start + chunk_q_end
22202233

2221-
chunk_q_scale = q_scale[global_q_start:global_q_end,
2222-
...] if self.use_fp4 else None
2223-
logits = self._call_mqa_logits(
2224-
q_fp8[global_q_start:global_q_end, ...],
2225-
chunk_k_fp8,
2226-
chunk_k_scale,
2227-
weights[global_q_start:global_q_end, ...],
2228-
chunk.cu_seqlen_ks[chunk_q_start:chunk_q_end],
2229-
chunk.cu_seqlen_ke[chunk_q_start:chunk_q_end],
2230-
chunk_q_scale,
2231-
)
2232-
if use_custom_topk:
2233-
torch.ops.trtllm.indexer_topk_prefill(
2234-
logits,
2235-
chunk.cu_seqlen_ks[chunk_q_start:chunk_q_end],
2236-
chunk.cu_seqlen_ke[chunk_q_start:chunk_q_end],
2237-
topk_indices_buffer[global_q_start:global_q_end, :],
2238-
self.index_topk)
2239-
else:
2240-
topk_indices = logits.topk(min(self.index_topk,
2241-
logits.shape[-1]),
2242-
dim=-1)[1]
2243-
topk_indices -= chunk.cu_seqlen_ks[
2244-
chunk_q_start:chunk_q_end][:, None]
2245-
2246-
mask_lo = topk_indices >= 0
2247-
mask_hi = topk_indices - (
2248-
chunk.cu_seqlen_ke[chunk_q_start:chunk_q_end] -
2249-
chunk.cu_seqlen_ks[chunk_q_start:chunk_q_end]
2250-
)[:, None] < 0
2251-
mask = mask_lo & mask_hi
2252-
2253-
# local indices per sequence
2254-
topk_indices = topk_indices.masked_fill(~mask, -1)
2255-
2256-
topk_indices_buffer[
2257-
global_q_start:global_q_end, :topk_indices.
2258-
shape[-1]] = topk_indices.to(dtype=torch.int32)
2234+
# Tile the query dimension so each fp8_mqa_logits call
2235+
# allocates at most [q_tile x num_k_tokens] instead of the
2236+
# full [local_q x num_k_tokens] (which can reach tens of GB
2237+
# on a long context and stall cuMemCreate under
2238+
# expandable_segments -> engine hang; see
2239+
# _INDEXER_MQA_LOGITS_ELEM_BUDGET). Results are identical:
2240+
# each query row's logits/top-k are independent and the KV
2241+
# (chunk_k_fp8) is unchanged across tiles, so the per-call
2242+
# allocation is the same size and the caching allocator
2243+
# reuses one block (peak ~= one tile, no extra sync).
2244+
local_q_len = chunk_q_end - chunk_q_start
2245+
q_tile = max(
2246+
1,
2247+
min(
2248+
local_q_len, _INDEXER_MQA_LOGITS_ELEM_BUDGET //
2249+
max(1, num_k_tokens)))
2250+
for tile_off in range(0, local_q_len, q_tile):
2251+
c0 = chunk_q_start + tile_off
2252+
c1 = min(c0 + q_tile, chunk_q_end)
2253+
g0 = chunk.token_start + c0
2254+
g1 = chunk.token_start + c1
2255+
tile_q_scale = q_scale[g0:g1,
2256+
...] if self.use_fp4 else None
2257+
logits = self._call_mqa_logits(
2258+
q_fp8[g0:g1, ...],
2259+
chunk_k_fp8,
2260+
chunk_k_scale,
2261+
weights[g0:g1, ...],
2262+
chunk.cu_seqlen_ks[c0:c1],
2263+
chunk.cu_seqlen_ke[c0:c1],
2264+
tile_q_scale,
2265+
)
2266+
if use_custom_topk:
2267+
torch.ops.trtllm.indexer_topk_prefill(
2268+
logits, chunk.cu_seqlen_ks[c0:c1],
2269+
chunk.cu_seqlen_ke[c0:c1],
2270+
topk_indices_buffer[g0:g1, :], self.index_topk)
2271+
else:
2272+
topk_indices = logits.topk(min(
2273+
self.index_topk, logits.shape[-1]),
2274+
dim=-1)[1]
2275+
topk_indices -= chunk.cu_seqlen_ks[c0:c1][:, None]
2276+
2277+
mask_lo = topk_indices >= 0
2278+
mask_hi = topk_indices - (
2279+
chunk.cu_seqlen_ke[c0:c1] -
2280+
chunk.cu_seqlen_ks[c0:c1])[:, None] < 0
2281+
mask = mask_lo & mask_hi
2282+
2283+
# local indices per sequence
2284+
topk_indices = topk_indices.masked_fill(~mask, -1)
2285+
2286+
topk_indices_buffer[
2287+
g0:g1, :topk_indices.shape[-1]] = \
2288+
topk_indices.to(dtype=torch.int32)
22592289

22602290
if apply_q_split:
22612291
q_sizes = [(r + 1) * chunk_num_token // tp_size -

0 commit comments

Comments
 (0)