[DeepSeekV3.2] Enable pure TP & Partial DP Attention (#13646)
This commit is contained in:
@@ -330,6 +330,25 @@ class Indexer(CustomOp):
|
||||
topk_result = metadata.topk_transform(logits, self.index_topk)
|
||||
return topk_result
|
||||
|
||||
def _should_chunk_mqa_logits(
|
||||
self, num_q: int, num_k: int, device: torch.device
|
||||
) -> Tuple[bool, int]:
|
||||
"""
|
||||
Detect whether we need to chunk the MQA logits computation to avoid OOM
|
||||
Return: (need_chunk, free_mem)
|
||||
"""
|
||||
# Quick static check for normal batches
|
||||
if num_q * num_k < 8_000_000: # 8M elements ≈ 32MB logits
|
||||
return False, 0
|
||||
|
||||
free_mem, total_mem = torch.cuda.mem_get_info(device)
|
||||
bytes_per_elem = 4 # float32
|
||||
logits_bytes = num_q * num_k * bytes_per_elem
|
||||
|
||||
# Logits should not exceed 50% of free memory or 30% of total memory
|
||||
need_chunk = (logits_bytes * 2 > free_mem) or (logits_bytes > total_mem * 0.3)
|
||||
return need_chunk, free_mem
|
||||
|
||||
def _get_topk_ragged(
|
||||
self,
|
||||
forward_batch: ForwardBatch,
|
||||
@@ -409,24 +428,86 @@ class Indexer(CustomOp):
|
||||
# ks = [0, 0, 0, 10, 10]
|
||||
# ke = [8, 9, 10, 13, 14]
|
||||
|
||||
logits = deep_gemm.fp8_mqa_logits(
|
||||
q_fp8[:q_offset],
|
||||
kv_fp8,
|
||||
weights[:q_offset],
|
||||
ks,
|
||||
ke,
|
||||
clean_logits=False,
|
||||
)
|
||||
|
||||
token_nums, _, _ = q_fp8.shape
|
||||
assert logits.shape[0] == len(seq_lens_expanded)
|
||||
assert logits.shape[1] == k_offset
|
||||
device = q_fp8.device
|
||||
|
||||
# Check if we need to chunk to avoid OOM
|
||||
need_chunk, free_mem = self._should_chunk_mqa_logits(q_offset, k_offset, device)
|
||||
|
||||
if not need_chunk:
|
||||
logits = deep_gemm.fp8_mqa_logits(
|
||||
q_fp8[:q_offset],
|
||||
kv_fp8,
|
||||
weights[:q_offset],
|
||||
ks,
|
||||
ke,
|
||||
clean_logits=False,
|
||||
)
|
||||
assert logits.shape[0] == len(seq_lens_expanded)
|
||||
assert logits.shape[1] == k_offset
|
||||
|
||||
raw_topk_result = metadata.topk_transform(logits, self.index_topk, ks=ks)
|
||||
topk_result = torch.full(
|
||||
(token_nums, self.index_topk),
|
||||
-1,
|
||||
device=device,
|
||||
dtype=torch.int32,
|
||||
)
|
||||
topk_result[:q_offset] = raw_topk_result
|
||||
return topk_result
|
||||
|
||||
# Chunk path
|
||||
bytes_per_elem = 4 # float32
|
||||
bytes_per_row = k_offset * bytes_per_elem
|
||||
# Reserve 50% of free memory for logits
|
||||
max_rows = max(1, int((free_mem * 0.5) // max(bytes_per_row, 1)))
|
||||
max_rows = min(max_rows, q_offset)
|
||||
|
||||
global_topk_offset = metadata.attn_metadata.topk_indices_offset
|
||||
|
||||
assert (
|
||||
seq_lens_expanded.shape[0] == q_offset
|
||||
), f"seq_lens_expanded length mismatch: {seq_lens_expanded.shape[0]} != {q_offset}"
|
||||
if global_topk_offset is not None:
|
||||
assert (
|
||||
global_topk_offset.shape[0] >= q_offset
|
||||
), f"topk_indices_offset too short: {global_topk_offset.shape[0]} < {q_offset}"
|
||||
|
||||
raw_topk_result = metadata.topk_transform(logits, self.index_topk, ks=ks)
|
||||
topk_result = torch.full(
|
||||
(token_nums, self.index_topk), -1, device=q_fp8.device, dtype=torch.int32
|
||||
(token_nums, self.index_topk), -1, device=device, dtype=torch.int32
|
||||
)
|
||||
topk_result[:q_offset] = raw_topk_result
|
||||
|
||||
start = 0
|
||||
while start < q_offset:
|
||||
end = min(start + max_rows, q_offset)
|
||||
|
||||
logits_chunk = deep_gemm.fp8_mqa_logits(
|
||||
q_fp8[start:end],
|
||||
kv_fp8,
|
||||
weights[start:end],
|
||||
ks[start:end],
|
||||
ke[start:end],
|
||||
clean_logits=False,
|
||||
)
|
||||
|
||||
lengths_chunk = seq_lens_expanded[start:end]
|
||||
|
||||
topk_offset_chunk = (
|
||||
global_topk_offset[start:end]
|
||||
if global_topk_offset is not None
|
||||
else None
|
||||
)
|
||||
|
||||
raw_topk_chunk = metadata.topk_transform(
|
||||
logits_chunk,
|
||||
self.index_topk,
|
||||
ks=ks[start:end],
|
||||
ke_offset=lengths_chunk,
|
||||
topk_indices_offset_override=topk_offset_chunk,
|
||||
)
|
||||
topk_result[start:end] = raw_topk_chunk
|
||||
start = end
|
||||
|
||||
return topk_result
|
||||
|
||||
def _forward_cuda_k_only(
|
||||
|
||||
@@ -170,6 +170,7 @@ class NSAIndexerMetadata(BaseIndexerMetadata):
|
||||
cu_seqlens_q: torch.Tensor = None,
|
||||
ke_offset: torch.Tensor = None,
|
||||
batch_idx_list: List[int] = None,
|
||||
topk_indices_offset_override: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
from sgl_kernel import (
|
||||
fast_topk_transform_fused,
|
||||
@@ -177,7 +178,10 @@ class NSAIndexerMetadata(BaseIndexerMetadata):
|
||||
fast_topk_v2,
|
||||
)
|
||||
|
||||
if cu_seqlens_q is not None:
|
||||
if topk_indices_offset_override is not None:
|
||||
cu_topk_indices_offset = topk_indices_offset_override
|
||||
cu_seqlens_q_topk = None
|
||||
elif cu_seqlens_q is not None:
|
||||
cu_seqlens_q = cu_seqlens_q.to(torch.int32)
|
||||
cu_seqlens_q_topk = compute_cu_seqlens(cu_seqlens_q)
|
||||
cu_topk_indices_offset = torch.repeat_interleave(
|
||||
@@ -286,9 +290,11 @@ class NativeSparseAttnBackend(AttentionBackend):
|
||||
)
|
||||
self.speculative_step_id = speculative_step_id
|
||||
|
||||
self.device_capability = torch.cuda.get_device_capability()
|
||||
self.device_sm_major = self.device_capability[0]
|
||||
|
||||
# Allocate global workspace buffer for TRTLLm ragged attention kernel (SM100/B200)
|
||||
device_sm_major = torch.cuda.get_device_capability()[0]
|
||||
if device_sm_major >= 10:
|
||||
if self.device_sm_major >= 10:
|
||||
global global_workspace_buffer
|
||||
if global_workspace_buffer is None:
|
||||
global_workspace_buffer = torch.empty(
|
||||
@@ -921,6 +927,11 @@ class NativeSparseAttnBackend(AttentionBackend):
|
||||
q_nope = q_all[:, :, : layer.v_head_dim]
|
||||
q_rope = q_all[:, :, layer.v_head_dim :]
|
||||
|
||||
# Align topk_indices with q dimensions
|
||||
# This handles cases where q is padded (TP + partial DP attention)
|
||||
if topk_indices is not None:
|
||||
topk_indices = self._pad_topk_indices(topk_indices, q_nope.shape[0])
|
||||
|
||||
# NOTE(dark): here, we use page size = 1
|
||||
topk_transform_method = self.get_topk_transform_method()
|
||||
if NSA_FUSE_TOPK:
|
||||
@@ -1058,6 +1069,10 @@ class NativeSparseAttnBackend(AttentionBackend):
|
||||
q_nope = q_all[:, :, : layer.v_head_dim]
|
||||
q_rope = q_all[:, :, layer.v_head_dim :]
|
||||
|
||||
# Align topk_indices with q dimensions
|
||||
if topk_indices is not None:
|
||||
topk_indices = self._pad_topk_indices(topk_indices, q_nope.shape[0])
|
||||
|
||||
if NSA_FUSE_TOPK:
|
||||
page_table_1 = topk_indices
|
||||
else:
|
||||
@@ -1178,13 +1193,43 @@ class NativeSparseAttnBackend(AttentionBackend):
|
||||
) -> torch.Tensor:
|
||||
from sgl_kernel.flash_mla import flash_mla_sparse_fwd
|
||||
|
||||
# FlashMLA sparse kernel requires num_heads to be a multiple of 64 (Hopper) or 128 (Blackwell)
|
||||
# When using TP, num_heads might be smaller (e.g., 256//8=32)
|
||||
num_tokens, num_heads, head_dim = q_all.shape
|
||||
|
||||
# Determine required padding based on GPU architecture (use cached value)
|
||||
required_padding = 128 if self.device_sm_major >= 10 else 64
|
||||
|
||||
need_padding = num_heads % required_padding != 0
|
||||
|
||||
if need_padding:
|
||||
assert required_padding % num_heads == 0, (
|
||||
f"num_heads {num_heads} cannot be padded to {required_padding}. "
|
||||
f"TP size may be too large for this model."
|
||||
)
|
||||
|
||||
# Pad q to required size
|
||||
q_padded = q_all.new_zeros((num_tokens, required_padding, head_dim))
|
||||
q_padded[:, :num_heads, :] = q_all
|
||||
q_input = q_padded
|
||||
else:
|
||||
q_input = q_all
|
||||
|
||||
# indices shape must be (s_q, h_kv=1, topk), keep h_kv=1 unchanged
|
||||
indices_input = page_table_1.unsqueeze(1)
|
||||
|
||||
o, _, _ = flash_mla_sparse_fwd(
|
||||
q=q_all,
|
||||
q=q_input,
|
||||
kv=kv_cache,
|
||||
indices=page_table_1.unsqueeze(1),
|
||||
indices=indices_input,
|
||||
sm_scale=sm_scale,
|
||||
d_v=v_head_dim,
|
||||
)
|
||||
|
||||
# Trim output back to original num_heads if we padded
|
||||
if need_padding:
|
||||
o = o[:, :num_heads, :]
|
||||
|
||||
return o
|
||||
|
||||
def _forward_flashmla_kv(
|
||||
@@ -1259,8 +1304,7 @@ class NativeSparseAttnBackend(AttentionBackend):
|
||||
)
|
||||
|
||||
# Use TRTLLm ragged attention for SM100 (Blackwell/B200) to avoid FA4 accuracy issues
|
||||
device_sm_major = torch.cuda.get_device_capability()[0]
|
||||
if device_sm_major >= 10:
|
||||
if self.device_sm_major >= 10:
|
||||
import flashinfer
|
||||
|
||||
seq_lens = metadata.cache_seqlens_int32
|
||||
@@ -1357,6 +1401,27 @@ class NativeSparseAttnBackend(AttentionBackend):
|
||||
# kv_cache = kv_cache.view(-1, 1, layer.head_dim)
|
||||
return o
|
||||
|
||||
def _pad_topk_indices(
|
||||
self, topk_indices: torch.Tensor, num_tokens: int
|
||||
) -> torch.Tensor:
|
||||
current_tokens = topk_indices.shape[0]
|
||||
if current_tokens == num_tokens:
|
||||
return topk_indices
|
||||
|
||||
assert current_tokens <= num_tokens, (
|
||||
f"topk_indices rows ({current_tokens}) > num_tokens ({num_tokens}); "
|
||||
"this indicates a mismatch between indexer output and q layout."
|
||||
)
|
||||
|
||||
pad_size = num_tokens - current_tokens
|
||||
padding = torch.full(
|
||||
(pad_size, topk_indices.shape[1]),
|
||||
-1,
|
||||
dtype=topk_indices.dtype,
|
||||
device=topk_indices.device,
|
||||
)
|
||||
return torch.cat([topk_indices, padding], dim=0)
|
||||
|
||||
def get_cuda_graph_seq_len_fill_value(self):
|
||||
"""Get the fill value for sequence length in CUDA graph."""
|
||||
return 1
|
||||
|
||||
@@ -963,7 +963,12 @@ class ServerArgs:
|
||||
f"Enable Context Parallel opt for deeeseekv3.2-DSA, Setting dp_size == {self.dp_size} and moe_dense_tp_size == {self.moe_dense_tp_size}, ep_size == {self.ep_size}, tp_size == {self.tp_size}, kv_cache_dtype == {self.kv_cache_dtype}, moe_a2a_backend {self.moe_a2a_backend} "
|
||||
)
|
||||
else:
|
||||
self.dp_size = self.tp_size
|
||||
# Pure TP and partial DP Attention mode is active for NSA, logging a warning
|
||||
if self.dp_size < self.tp_size:
|
||||
logger.warning(
|
||||
f"NSA with TP mode is active, dp_size={self.dp_size}, tp_size={self.tp_size}, "
|
||||
f"attn_tp_size={self.tp_size}, attention weights will be sharded across {self.tp_size} ranks."
|
||||
)
|
||||
|
||||
self.page_size = 64
|
||||
logger.warning("Setting page size to 64 for DeepSeek NSA.")
|
||||
|
||||
Reference in New Issue
Block a user