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:
co-authored by
Julien Lin
YAMY1234
Yangmin Li
parent
8139a1740e
commit
3a64faa1f2
@@ -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],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user