Fix disagg PP MTP for GLM-5.2 (#39378)

Co-authored-by: Julien Lin <jullin@nvidia.com>
Co-authored-by: YAMY1234 <74099316+YAMY1234@users.noreply.github.com>
Co-authored-by: Yangmin Li <yangminl@nvidia.com>
This commit is contained in:
Po-Han Huang (NVIDIA)
2026-09-19 13:55:07 -07:00
committed by GitHub
co-authored by Julien Lin YAMY1234 Yangmin Li
parent 8139a1740e
commit 3a64faa1f2
12 changed files with 627 additions and 105 deletions
+71 -57
View File
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional
from sglang.srt.arg_groups.overrides import (
_hisparse_validation,
model_config_of,
resolved_view,
resolving_view,
run_post_process_pass,
@@ -24,6 +25,20 @@ from sglang.srt.utils.runai_utils import is_runai_obj_uri
logger = logging.getLogger(__name__)
_PP_EAGLE_SUPPORTED_ARCHITECTURES = frozenset(
{
"DeepseekV2ForCausalLM",
"DeepseekV3ForCausalLM",
"DeepseekV32ForCausalLM",
"GlmMoeDsaForCausalLM",
# Qwen3.5 (dense / MoE, text and multimodal); folded in from #39602.
"Qwen3_5ForCausalLM",
"Qwen3_5MoeForCausalLM",
"Qwen3_5ForConditionalGeneration",
"Qwen3_5MoeForConditionalGeneration",
}
)
def validate_response_store(server_args: Any) -> None:
cfg = resolving_view(server_args)
@@ -35,6 +50,58 @@ def validate_response_store(server_args: Any) -> None:
)
def check_pipeline_parallel_compat(
cfg: Any, *, model_architecture: Optional[str] = None
) -> None:
"""Validate features used with pipeline parallelism."""
assert cfg.disable_overlap_schedule, (
"Pipeline parallelism is not compatible with overlap schedule"
)
if cfg.speculative_algorithm is not None:
assert (
cfg.speculative_algorithm.upper() == "EAGLE"
and not cfg.enable_multi_layer_eagle
), (
"Pipeline parallelism currently only supports EAGLE "
"(non-multi-layer) speculative decoding"
)
if envs.SGLANG_ENABLE_PP_SPEC.get():
# The aggregate relay carries an EAGLE-shaped tree and only
# EAGLEWorkerV2 tail-drafts. PD prefill relays topk_p /
# topk_index / hidden states through RelayPayload; the gated
# flow replaces that relay with its own and does not carry
# those fields.
assert cfg.disaggregation_mode == "null", (
"SGLANG_ENABLE_PP_SPEC is not compatible with --disaggregation-mode"
)
# The PP relay slices spec results with the configured
# num_draft_tokens; adaptive spec changes it at runtime.
assert not cfg.speculative_adaptive, (
"SGLANG_ENABLE_PP_SPEC is not compatible with --speculative-adaptive"
)
# Every stage rebuilds the same verify input from the relayed
# per-request state, so all stages must see the same batch.
# DP attention partitions it per DP rank.
assert not cfg.enable_dp_attention, (
"SGLANG_ENABLE_PP_SPEC is not compatible with --enable-dp-attention"
)
else:
assert cfg.disaggregation_mode == "prefill", (
"PP + speculative decoding (MTP) is only supported on prefill nodes "
"(disaggregation-mode=prefill)"
)
assert model_architecture in _PP_EAGLE_SUPPORTED_ARCHITECTURES, (
"PP + speculative decoding is only supported for DeepSeek/GLM/Qwen3.5 "
"models whose last pipeline stage supplies the EAGLE draft "
f"embedding; got architecture={model_architecture}"
)
assert cfg.min_free_slots_delay is None, (
"--min-free-slots-delay is not supported with pipeline "
"parallelism: allocatable slots per microbatch are bounded by "
"pp-max-micro-batch-size, so the threshold may never be reached"
)
def check_server_args(server_args: Any):
from sglang.srt.arg_groups.lora_hook import check_lora_server_args
@@ -60,63 +127,10 @@ def check_server_args(server_args: Any):
)
if cfg.pp_size > 1:
if get_platform().is_npu:
# NPU: allow PP + EAGLE speculative decoding
assert cfg.disable_overlap_schedule, (
"Pipeline parallelism is not compatible with overlap schedule"
)
if cfg.speculative_algorithm is not None:
assert (
cfg.speculative_algorithm.upper() == "EAGLE"
and not cfg.enable_multi_layer_eagle
), (
"Pipeline parallelism currently only supports EAGLE "
"(non-multi-layer) speculative decoding"
)
assert cfg.disaggregation_mode == "prefill", (
"NPU PP + speculative decoding (MTP) is only supported "
"on prefill nodes (disaggregation-mode=prefill)"
)
elif envs.SGLANG_ENABLE_PP_SPEC.get():
assert cfg.disable_overlap_schedule, (
"SGLANG_ENABLE_PP_SPEC requires --disable-overlap-schedule"
)
# The relay carries an EAGLE-shaped tree and only EAGLEWorkerV2
# tail-drafts; every other algorithm would be mis-rebuilt.
assert (
cfg.speculative_algorithm == "EAGLE"
and not cfg.enable_multi_layer_eagle
), (
"SGLANG_ENABLE_PP_SPEC supports single-layer EAGLE/MTP only, "
f"got {cfg.speculative_algorithm}"
)
# PD prefill relays topk_p / topk_index / hidden states through
# RelayPayload; the gated flow replaces that relay with its own
# and does not carry those fields.
assert cfg.disaggregation_mode == "null", (
"SGLANG_ENABLE_PP_SPEC is not compatible with --disaggregation-mode"
)
# The PP relay slices spec results with the configured
# num_draft_tokens; adaptive spec changes it at runtime.
assert not cfg.speculative_adaptive, (
"SGLANG_ENABLE_PP_SPEC is not compatible with --speculative-adaptive"
)
# Every stage rebuilds the same verify input from the relayed
# per-request state, so all stages must see the same batch.
# DP attention partitions it per DP rank.
assert not cfg.enable_dp_attention, (
"SGLANG_ENABLE_PP_SPEC is not compatible with --enable-dp-attention"
)
else:
# Non-NPU: PP + speculative decoding is not supported
assert cfg.disable_overlap_schedule and cfg.speculative_algorithm is None, (
"Pipeline parallelism is not compatible with overlap schedule, speculative decoding"
)
assert cfg.min_free_slots_delay is None, (
"--min-free-slots-delay is not supported with pipeline "
"parallelism: allocatable slots per microbatch are bounded by "
"pp-max-micro-batch-size, so the threshold may never be reached"
)
model_architecture = None
if cfg.speculative_algorithm is not None:
model_architecture = model_config_of(server_args).hf_config.architectures[0]
check_pipeline_parallel_compat(cfg, model_architecture=model_architecture)
assert not (cfg.dp_size > 1 and cfg.nnodes != 1 and not cfg.enable_dp_attention), (
"multi-node data parallel is not supported unless dp attention!"
@@ -1265,12 +1265,18 @@ class CommonKVManager(BaseKVManager):
)
# Regular MLA PP slicing
start_layer = self.kv_args.prefill_start_layer
end_layer = start_layer + len(src_kv_ptrs)
# Decode pp size should be equal to prefill pp size or 1
start_layer, end_layer = self._mla_kv_entry_span_with_pp(len(src_kv_ptrs))
sliced_dst_kv_ptrs = dst_kv_ptrs[start_layer:end_layer]
return src_kv_ptrs, sliced_dst_kv_ptrs, len(src_kv_ptrs)
def _mla_kv_entry_span_with_pp(self, n_src: int) -> Tuple[int, int]:
# A plain MLA pool registers one region per layer ascending, addressed as
# layer_id - start_layer, so this stage occupies [start, start + n_src) of a
# peer that registered the whole model. Pointer view: get_mla_kv_ptrs_with_pp.
start_layer = self.kv_args.prefill_start_layer
return start_layer, start_layer + n_src
def _mla_slice_ptrs_for_pp(
self,
src_kv_ptrs: List[int],
+55 -7
View File
@@ -1026,6 +1026,59 @@ class NixlKVManager(StagingManagerMixin, CommonKVManager):
)
peer_info.kv_xfer_segments = prepared_segments
def _build_transfer_dst_indices(
self, *, peer_info: KVArgsRegisterInfo, n_src: int, n_dst: int
) -> List[int]:
"""Map source entries to destination entries for this transfer.
Heterogeneous PP over a plain MLA pool uses the source stage's layer span.
All other layouts use explicit layer IDs, or positional pairing for non-PP.
"""
use_pp_mla_offsets = (
self.pp_size > 1
and n_src != n_dst
and not self.kv_args.kv_layer_ids
and not peer_info.dst_kv_layer_ids
and self.is_mla_backend
and not self.is_hybrid_mla_backend
and not self.kv_args.mla_compression_ratios
)
if not use_pp_mla_offsets:
pairs = build_transfer_entry_pairs(
self.kv_args.kv_layer_ids,
peer_info.dst_kv_layer_ids,
n_src,
n_dst,
allow_positional_fallback=self.pp_size == 1,
)
return [j for _, j in pairs]
start, end = self._mla_kv_entry_span_with_pp(n_src)
# Bootstrap admits a peer running our pp or 1, so a peer that does not cover the
# span is a matched-pp stage above 0, whose entries start at its own index 0.
if end > n_dst:
pairs = build_transfer_entry_pairs(
self.kv_args.kv_layer_ids,
peer_info.dst_kv_layer_ids,
n_src,
n_dst,
allow_positional_fallback=False,
)
return [j for _, j in pairs]
indices = list(range(start, end))
src_item_lens = list(self.kv_args.kv_item_lens)
dst_item_lens = [peer_info.dst_kv_item_lens[j] for j in indices]
if src_item_lens != dst_item_lens:
# Disagreeing cell sizes mean the peers did not build the same KV geometry;
# writing anyway would silently corrupt the peer's pool.
raise RuntimeError(
"PP-heterogeneous MLA transfer: decode KV cell geometry differs from "
f"prefill over layers [{start}, {end}): prefill item_lens="
f"{src_item_lens}, decode item_lens={dst_item_lens}"
)
return indices
def _prepare_payload_xfer(self, peer_info: KVArgsRegisterInfo):
# If prefill does not run speculative decoding (the usual case),
# decode with speculative decoding will have more kv items.
@@ -1110,14 +1163,9 @@ class NixlKVManager(StagingManagerMixin, CommonKVManager):
else self._num_slots_src
)
pairs = build_transfer_entry_pairs(
self.kv_args.kv_layer_ids,
peer_info.dst_kv_layer_ids,
n_src,
n_dst,
allow_positional_fallback=self.pp_size == 1,
dst_indices = self._build_transfer_dst_indices(
peer_info=peer_info, n_src=n_src, n_dst=n_dst
)
dst_indices = [j for _, j in pairs]
dst_kv_ptrs = [peer_info.dst_kv_ptrs[j] for j in dst_indices]
dst_kv_item_lens = [peer_info.dst_kv_item_lens[j] for j in dst_indices]
dst_kv_data_lens = [
@@ -890,6 +890,11 @@ class SchedulerPPMixin:
tensor_dict["draft_topk_p"] = draft_input.topk_p.contiguous()
tensor_dict["draft_topk_index"] = draft_input.topk_index.contiguous()
tensor_dict["draft_hidden_states"] = draft_input.hidden_states.contiguous()
# Preserve the DSA IndexShare seed when rebuilding spec_info on each rank.
if draft_input.dsa_topk_indices is not None:
tensor_dict["draft_dsa_topk_indices"] = (
draft_input.dsa_topk_indices.contiguous()
)
has_sampling_mask_output = (
result.logits_output is not None
@@ -1130,6 +1135,7 @@ class SchedulerPPMixin:
bonus_tokens=next_token_ids,
num_tokens_per_req=1,
num_tokens_for_logprob_per_req=1,
dsa_topk_indices=pp_outputs.tensors.get("draft_dsa_topk_indices"),
)
batch.spec_info = next_draft_input
@@ -683,7 +683,6 @@ class ModelRunner:
model=self.model,
model_config=self.model_config,
is_draft_worker=self.is_draft_worker,
spec_algorithm=self.spec_algorithm,
)
adjust_hybrid_swa_layer_ids(
model_config=self.model_config,
@@ -5,12 +5,8 @@ from typing import TYPE_CHECKING, Any, NamedTuple
import msgspec
from torch import nn
from sglang.srt.environ import envs
from sglang.srt.utils import is_npu
if TYPE_CHECKING:
from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
class AttentionAndMoeLayers(NamedTuple):
@@ -141,7 +137,6 @@ def resolve_layer_indices(
model: Any,
model_config: ModelConfig,
is_draft_worker: bool,
spec_algorithm: SpeculativeAlgorithm,
) -> ModelLayerInfo:
# For MTP models like DeepSeek-V3 or GLM-4.5, the MTP layer(s) are used separately as draft
# models for speculative decoding. In those cases, `num_nextn_predict_layers` is used to
@@ -149,8 +144,6 @@ def resolve_layer_indices(
model_num_layers = _compute_model_num_layers(
model=model, model_config=model_config, is_draft_worker=is_draft_worker
)
_nnpl = model_config.num_nextn_predict_layers
model_has_mtp_layers = _nnpl is not None and _nnpl > 0
pp_range = _resolve_pp_layer_range(model=model, model_num_layers=model_num_layers)
num_effective_layers = pp_range.end_layer - pp_range.start_layer
@@ -159,14 +152,6 @@ def resolve_layer_indices(
if loop_num > 1:
num_effective_layers = num_effective_layers * loop_num
if not is_npu():
_assert_pp_mtp_compat(
model_has_mtp_layers=model_has_mtp_layers,
spec_algorithm=spec_algorithm,
num_effective_layers=num_effective_layers,
model_num_layers=model_num_layers,
)
return ModelLayerInfo(
start_layer=pp_range.start_layer,
end_layer=pp_range.end_layer,
@@ -213,25 +198,6 @@ def _resolve_pp_layer_range(*, model: Any, model_num_layers: int) -> _PPLayerRan
)
def _assert_pp_mtp_compat(
*,
model_has_mtp_layers: bool,
spec_algorithm: SpeculativeAlgorithm,
num_effective_layers: int,
model_num_layers: int,
) -> None:
if envs.SGLANG_ENABLE_PP_SPEC.get():
return
assert (
(not model_has_mtp_layers)
or (spec_algorithm.is_none())
or (
(not spec_algorithm.is_none())
and (num_effective_layers == model_num_layers)
)
), "PP is not compatible with MTP models."
def adjust_hybrid_swa_layer_ids(
*,
model_config: ModelConfig,
@@ -403,8 +403,8 @@ class DeepseekV2WeightLoaderMixin:
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
# Skip loading embed_tokens if not first rank in pipeline parallelism
if ".embed_tokens." in name and not self.pp_group.is_first_rank:
# The last PP stage also owns an embedding for its NextN draft.
if ".embed_tokens." in name and name not in params_dict:
continue
# Skip loading norm if not last rank in pipeline parallelism
if ".norm." in name and not self.pp_group.is_last_rank:
+8 -1
View File
@@ -2793,6 +2793,13 @@ class DeepseekV2DecoderLayer(nn.Module):
return output
def pp_stage_needs_embedding(pp_group, speculative_algorithm) -> bool:
"""The first stage embeds inputs; the last supplies the EAGLE draft embedding."""
return pp_group.is_first_rank or (
pp_group.is_last_rank and speculative_algorithm is not None
)
class DeepseekV2Model(nn.Module):
fall_back_to_pt_during_load = False
@@ -2810,7 +2817,7 @@ class DeepseekV2Model(nn.Module):
self.first_k_dense_replace = config.first_k_dense_replace
self.pp_group = get_parallel().pp_group
if self.pp_group.is_first_rank or (_is_npu and self.pp_group.is_last_rank):
if pp_stage_needs_embedding(self.pp_group, get_spec().speculative_algorithm):
self.embed_tokens = VocabParallelEmbedding(
config.vocab_size,
config.hidden_size,