[Bugfix] Restore overridden HF config fields and support index_skip_topk_offset for DSA topk sharing (#27114)

This commit is contained in:
Yuxuan Zhang
2026-06-06 13:26:04 +08:00
committed by GitHub
parent aa55657e9e
commit 393d0e169e
8 changed files with 100 additions and 5 deletions
@@ -701,6 +701,7 @@ class TboForwardBatchPreparer:
"split_index", # for split prefill
"orig_seq_lens", # only used by qwen-1m, thus not care
"return_pooled_hidden_states",
"reuse_mtp_topk_indices", # forward-level flag, inherited by both child batches
]:
output_dict[key] = getattr(batch, key)
@@ -385,6 +385,10 @@ class ForwardBatch(ForwardBatchDeepSeekMHAMixin):
# For hidden states before normal
return_hidden_states_before_norm: bool = False
# For NSA/DSA topk_indices reuse across forward calls (e.g., EAGLE draft)
topk_indices: Optional[torch.Tensor] = None
reuse_mtp_topk_indices: Optional[bool] = False
# === Forward-derived (built in init_new on the forward stream; FB-owned) ===
# Position information
positions: torch.Tensor = None
@@ -245,7 +245,15 @@ class DeepseekMLAForwardMixin:
q = self.q_b_proj(q)[0].view(
-1, self.num_local_heads, self.qk_head_dim
)
if not self.skip_topk or prev_topk_indices is None:
# skip_topk (shared) layers carry no indexer weights in the
# checkpoint, so they must reuse the carried topk and never run
# the indexer. Do NOT widen this to `or prev_topk_indices is
# None` (the upstream gate): that recomputes with an
# uninitialized indexer whenever cross-layer propagation is
# unavailable (e.g. the TBO op path drops topk_indices),
# reintroducing the >index_topk garbling. The is_nextn clause is
# the sole intentional fallback (layer 78 has its own weights).
if not self.skip_topk or (self.is_nextn and prev_topk_indices is None):
topk_indices = self.indexer(
x=hidden_states,
q_lora=q_lora,
@@ -264,7 +272,12 @@ class DeepseekMLAForwardMixin:
k_nope = k_nope.unsqueeze(1)
q = self.q_b_proj(q)[0].view(-1, self.num_local_heads, self.qk_head_dim)
if q_lora is not None:
if not self.skip_topk or prev_topk_indices is None:
# See the skip_topk note above: shared layers have no
# indexer weights, so this gate must not fall back to
# computing when prev_topk_indices is None.
if not self.skip_topk or (
self.is_nextn and prev_topk_indices is None
):
topk_indices = self.indexer(
x=hidden_states,
q_lora=q_lora,
@@ -230,7 +230,14 @@ class DeepseekModelNextN(nn.Module):
forward_batch,
residual,
zero_allocator,
prev_topk_indices=(
forward_batch.topk_indices
if forward_batch.reuse_mtp_topk_indices
else None
),
)
if forward_batch.reuse_mtp_topk_indices:
forward_batch.topk_indices = topk_indices
if not forward_batch.forward_mode.is_idle():
if residual is not None:
+25 -3
View File
@@ -1469,6 +1469,7 @@ class DeepseekV2AttentionMLA(
self.q_lora_rank = q_lora_rank
self.kv_lora_rank = kv_lora_rank
self.quant_config = quant_config
self.is_nextn = is_nextn
attn_tp_rank = get_attention_tp_rank()
attn_tp_size = get_attention_tp_size()
self.use_dsa = is_deepseek_dsa(config)
@@ -1556,12 +1557,33 @@ class DeepseekV2AttentionMLA(
# skip_topk: when True, this layer will skip computation and reuse previous layer's topk indices.
# next_skip_topk: when True, the next layer will skip computation and reuse this layer's topk indices.
if is_nextn:
self.skip_topk = False
self.next_skip_topk = False
self.skip_topk = True
self.next_skip_topk = True
else:
self.index_topk_freq = getattr(config, "index_topk_freq", 1)
self.index_topk_pattern = getattr(config, "index_topk_pattern", None)
if self.index_topk_pattern is None:
self.index_skip_topk_offset = getattr(
config, "index_skip_topk_offset", None
)
if (
self.index_topk_pattern is None
and self.index_skip_topk_offset is not None
):
assert self.index_skip_topk_offset > 0, (
"index_skip_topk_offset must be positive; offset <= 0 "
"marks layer 0 as skip_topk with no prior topk to reuse"
)
self.skip_topk = (
max(layer_id - self.index_skip_topk_offset + 1, 0)
% self.index_topk_freq
!= 0
)
self.next_skip_topk = (
max(layer_id - self.index_skip_topk_offset + 2, 0)
% self.index_topk_freq
!= 0
)
elif self.index_topk_pattern is None:
self.skip_topk = max(layer_id - 1, 0) % self.index_topk_freq != 0
self.next_skip_topk = layer_id % self.index_topk_freq != 0
else:
+14
View File
@@ -1851,6 +1851,20 @@ class ServerArgs:
self.attention_backend = "dsa"
logger.info("Use dsa attention backend for DeepSeek with DSA.")
index_topk_freq = getattr(hf_config, "index_topk_freq", 1)
index_topk_pattern = getattr(hf_config, "index_topk_pattern", None)
if self.enable_two_batch_overlap and (
index_topk_freq > 1
or (index_topk_pattern is not None and "S" in index_topk_pattern)
):
raise ValueError(
"--enable-two-batch-overlap is not supported with DSA "
"index-topk sharing (index_topk_freq > 1 or an "
"index_topk_pattern containing shared layers): the TBO op "
"path does not propagate topk indices across layers, so "
"shared layers would run sparse attention without indices."
)
if not is_npu() and not is_xpu(): # CUDA or ROCm GPU
if self.enable_dsa_prefill_context_parallel:
logger.warning(
@@ -897,6 +897,17 @@ class EAGLEWorker(TpModelWorker):
# Forward multiple steps
scores = None
# Reuse NSA/DSA topk_indices from the first draft forward step for
# subsequent steps, analogous to skip_topk in deepseek_v2.py layers.
# Only safe with topk == 1: select_top_k_tokens reorders candidate rows
# each step, which would desync the cached indices from their rows.
index_share_for_mtp_iteration = (
getattr(self.model_config.hf_config, "index_share_for_mtp_iteration", False)
and self.topk == 1
)
if index_share_for_mtp_iteration:
forward_batch.reuse_mtp_topk_indices = True
forward_batch.topk_indices = None
for i in range(self.speculative_num_steps):
input_ids, hidden_states, scores, tree_info = select_top_k_tokens(
i, topk_p, topk_index, hidden_states, scores, self.topk
@@ -949,6 +960,9 @@ class EAGLEWorker(TpModelWorker):
maybe_detect_inf(hidden_states, f"draft_forward step {i}: hidden_states")
forward_batch.positions.add_(1)
if index_share_for_mtp_iteration:
forward_batch.topk_indices = None
forward_batch.reuse_mtp_topk_indices = False
parent_list, top_scores_index, draft_tokens = organize_draft_results(
score_list, token_list, parents_list, self.speculative_num_draft_tokens
)
@@ -67,6 +67,26 @@ class HfModelConfigParser(ModelConfigParserBase):
**kwargs,
)
if (
config.architectures is not None
and config.architectures[0] == "GlmMoeDsaForCausalLM"
):
# GlmMoeDsaConfig drops/clobbers raw checkpoint fields the DSA path
# needs, so re-read them from config.json and restore. Fixed upstream
# by https://github.com/huggingface/transformers/pull/46338; remove
# this block once SGLang requires transformers >= 5.10.
from transformers import PretrainedConfig
raw_config, _ = PretrainedConfig.get_config_dict(model, revision=revision)
for key in (
"qk_rope_head_dim",
"index_topk_freq",
):
if key in raw_config:
setattr(config, key, raw_config[key])
if hasattr(config, "qk_head_dim") and hasattr(config, "qk_nope_head_dim"):
config.qk_head_dim = config.qk_nope_head_dim + config.qk_rope_head_dim
if (
config.architectures is not None
and config.architectures[0] == "Phi4MMForCausalLM"