[DSA] Re-enable fused top-k v2 for MTP: clamp padded-row seq_lens to >= 0 (#30378)

Co-authored-by: ziyi.xu <ziyi.xu@radixark.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
DarkSharpness
2026-07-07 13:44:01 -07:00
committed by GitHub
co-authored by ziyi.xu Claude Fable 5
parent 6875df3378
commit bbc537035a
6 changed files with 57 additions and 28 deletions
+20
View File
@@ -67,6 +67,15 @@ _PLAN_METADATA_INTS_PER_BATCH = 2
def plan_topk_v2(seq_lens: torch.Tensor, static_threshold: int = 0) -> torch.Tensor:
"""Preprocess the per-batch routing plan for :func:`topk_transform_512_v2`.
IMPORTANT: every entry of ``seq_lens`` must be NON-NEGATIVE. The device
kernel reads the int32 buffer as ``uint32_t``, so a negative length (e.g.
-4 from a DP-padded / idle-companion row) reinterprets as ~4e9, poisons
the plan, and drives the transform kernel into an illegal memory access.
Producers of padded rows must clamp their lengths to 0 (0 selects the
trivial all-(-1) output path, which is safe).
"""
module = _jit_topk_v2_module()
bs = seq_lens.shape[0]
metadata = seq_lens.new_empty(bs + 1, _PLAN_METADATA_INTS_PER_BATCH)
@@ -83,6 +92,17 @@ def topk_transform_512_v2(
metadata: torch.Tensor,
out_raw_indices: Optional[torch.Tensor] = None,
) -> None:
"""Fused top-k + page-table transform (DeepSeek-V4 top-k v2 kernel).
IMPORTANT: every entry of ``seq_lens`` must be NON-NEGATIVE, and
``metadata`` must come from :func:`plan_topk_v2` over the same ``seq_lens``
values. The kernel reads lengths as ``uint32_t``: a negative entry
reinterprets as a ~4e9-token sequence, sending the row down the cluster
path over garbage scores and crashing with an illegal memory access
(GLM 5.2 MTP DP-idle companion rows hit exactly this). A length of 0 is
the valid way to express "no tokens": the row takes the trivial path and
the output is all -1.
"""
module = _jit_topk_v2_module()
module.topk_transform(
scores,
@@ -84,22 +84,22 @@ class DSATopKBackend(Enum):
row_starts: Optional[torch.Tensor] = None,
batch_idx_list: Optional[List[int]] = None,
force_unfused_topk: bool = False,
allow_topk_v2: bool = True,
) -> torch.Tensor:
if not envs.SGLANG_DSA_FUSE_TOPK.get() or force_unfused_topk:
return self.topk_func(logits, lengths, topk, row_starts=row_starts)
# Decode-shaped PAGED top-k routes to the DeepSeek-V4 top-k v2 JIT kernel,
# which fuses top-k selection and the page-table transform in one launch and
# consumes the indexer's own page_size>=1 table directly, so no page_size=1
# table is materialized. Shared by DeepSeek-V3.2 and GLM DSA. This is a
# deterministic dispatch on the work shape, not a best-effort attempt: the
# fused-decode CUDA graph drops the page_size=1 table for exactly this case
# (see dsa_drop_wide_page_table), so once the shape matches we commit to v2
# and never silently fall back to the legacy page_size=1 path from here.
# Decode-shaped PAGED top-k (plain decode AND spec verify / draft-extend,
# whose expanded rows match the same shape) routes to the DeepSeek-V4 top-k
# v2 JIT kernel, which fuses top-k selection and the page-table transform in
# one launch and consumes the indexer's own page_size>=1 table directly, so
# no page_size=1 table is materialized. Shared by DeepSeek-V3.2 and GLM DSA.
# This is a deterministic dispatch on the work shape, not a best-effort
# attempt: the fused-decode CUDA graph drops the page_size=1 table for
# exactly this case (see dsa_drop_wide_page_table), so once the shape
# matches we commit to v2 and never silently fall back to the legacy
# page_size=1 path from here.
if (
allow_topk_v2
and envs.SGLANG_OPT_USE_TOPK_V2.get()
envs.SGLANG_OPT_USE_TOPK_V2.get()
and topk_transform_method == TopkTransformMethod.PAGED
and row_starts is None
and batch_idx_list is None
@@ -260,6 +260,12 @@ def _topk_transform_v2_paged(
than fall back to the slow legacy path (which may not even have a page_size=1
table to fall back to) or silently paper over bad input (padding, recomputing
the plan) at the cost of the performance this path exists to deliver.
``lengths`` entries must be NON-NEGATIVE: the kernel reads them as
``uint32_t``, so a negative row length (DP-padded / idle-companion rows)
reinterprets as ~4e9 tokens and illegal-addresses. Metadata producers clamp
padded rows to 0 (see ``fused_dsa_draft_extend_metadata`` /
``seqlens_expand_kernel``); 0 takes the trivial all-(-1) output path.
"""
from sglang.jit_kernel.dsv4.topk import topk_transform_512_v2
from sglang.srt.model_executor.forward_context import get_token_to_kv_pool
@@ -251,12 +251,6 @@ class DSAIndexerMetadata(BaseIndexerMetadata):
paged_mqa_schedule_metadata: Optional[torch.Tensor] = None
paged_mqa_ctx_lens_2d: Optional[torch.Tensor] = None
force_unfused_topk: bool = False
# Whether the fused top-k v2 kernel may be used for this forward. Disabled for
# spec verify / draft-extend: the v2 small-batch path illegal-addresses under
# those multi-query-per-request CUDA graphs (GLM 5.2 MTP), and it is only
# e2e-validated for single-query decode. TODO(dsa-topk-v2): re-enable once the
# small-batch kernel is fixed; see the crash analysis in the PR.
allow_topk_v2: bool = True
def get_seqlens_int32(self) -> torch.Tensor:
return self.attn_metadata.cache_seqlens_int32
@@ -326,7 +320,6 @@ class DSAIndexerMetadata(BaseIndexerMetadata):
row_starts=ks,
batch_idx_list=batch_idx_list,
force_unfused_topk=self.force_unfused_topk,
allow_topk_v2=self.allow_topk_v2,
)
@@ -2849,14 +2842,6 @@ class DeepseekSparseAttnBackend(
self.hisparse_coordinator is not None
and forward_batch.forward_mode.is_decode_or_idle()
)
# TEMP(dsa-topk-v2): the fused v2 small-batch path illegal-addresses under
# spec verify / draft-extend CUDA graphs (GLM 5.2 MTP). Restrict v2 to the
# single-query decode shape it is e2e-validated on; spec falls back to the
# legacy transform (page_table_1 is present for spec, not dropped).
allow_topk_v2 = not (
forward_batch.forward_mode.is_target_verify()
or forward_batch.forward_mode.is_draft_extend_v2()
)
return DSAIndexerMetadata(
attn_metadata=self.forward_metadata,
topk_transform_method=self.get_topk_transform_method(
@@ -2866,7 +2851,6 @@ class DeepseekSparseAttnBackend(
paged_mqa_schedule_metadata=self.forward_metadata.paged_mqa_schedule_metadata,
paged_mqa_ctx_lens_2d=self.forward_metadata.paged_mqa_ctx_lens_2d,
force_unfused_topk=force_unfused,
allow_topk_v2=allow_topk_v2,
)
def _compute_flashmla_metadata(self, cache_seqlens: torch.Tensor, seq_len_q: int):
@@ -495,7 +495,14 @@ def _fused_dsa_draft_extend_metadata_kernel(
mask=mask_e,
other=0,
).to(tl.int32)
# Clamp to >= 0: DP-padded / idle-companion rows carry the CUDA-graph
# seq_len fill value (1), which is smaller than qo_len, so the raw
# per-row visible kv length goes negative. Consumers treat these
# lengths as unsigned (the top-k v2 kernel reads them as uint32), so a
# negative row becomes a ~4e9-token length and an illegal memory
# access. 0 keeps padded rows on the trivial all-(-1) output path.
expanded_seq = base_seq - qo_len_for_row + local_off + 1
expanded_seq = tl.maximum(expanded_seq, 0)
expanded_seq = tl.where(mask_e, expanded_seq, 0)
dsa_seq = tl.minimum(expanded_seq, dsa_index_topk)
dsa_cu = tl.cumsum(dsa_seq, 0)
@@ -347,7 +347,12 @@ def seqlens_expand_kernel(
offs = tl.arange(0, BLOCK)
mask = offs < qo_len
values = start + offs
# Clamp to >= 0: rows with kv_len < qo_len (DP-padded / idle-companion
# rows whose kv is the CUDA-graph fill value) would otherwise produce
# negative lengths, which unsigned consumers (e.g. the top-k v2 kernel,
# which reads lengths as uint32) turn into ~4e9-token lengths and an
# illegal memory access.
values = tl.maximum(start + offs, 0)
tl.store(output_ptr + out_offset + offs, values, mask=mask)