Compare commits

...
Author SHA1 Message Date
Woosuk Kwon e0bbe8d667 Skip compile SP pass under enforce_eager (V2 runner)
sp_threshold force-enabled compilation_config.pass_config.enable_sp, which the
V2 model runner rejects ("does not yet support: sequence parallelism"). The
eager in-forward SP path reads sp_threshold directly and needs no compile pass;
under enforce_eager there is no compilation to attach it to. Guard the mapping.

Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-30 15:27:09 +00:00
Woosuk Kwon 44c1a8eb78 Simplify SP token padding condition and comments
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-30 15:26:34 +00:00
Woosuk Kwon d58e45771c Require sp_threshold > max cudagraph size instead of aligning sizes
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-30 15:26:34 +00:00
Woosuk Kwon 6bcd9b3d03 revert mrv1 & do tp check
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-30 15:26:34 +00:00
Woosuk Kwon 2e8704317e minor
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-30 15:26:34 +00:00
Woosuk Kwon ab64990834 minor
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-30 15:26:34 +00:00
Woosuk Kwon 1513ab9940 fix
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-30 15:26:34 +00:00
Woosuk Kwon 5072ad2f65 MTP runs without mHC SP (full-token draft)
Draft is a single decoder block; sharding its per-token mHC saves negligible compute and would require tp-aligning the whole spec-decode dispatch. Draft consumes the target's full pre-hc_head residual.

Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-30 15:26:34 +00:00
Woosuk Kwon 1f42f683c4 sp with no ep
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-30 15:26:34 +00:00
Woosuk Kwon 44c5396c74 wip
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-30 15:26:33 +00:00
6 changed files with 169 additions and 5 deletions
+26
View File
@@ -207,6 +207,24 @@ class ParallelConfig:
enable_dbo: bool = False
"""Enable dual batch overlap for the model executor."""
sp_threshold: int | None = None
"""Sequence-parallelism engage threshold, in tokens. A single umbrella knob
covering both mechanisms:
- the compile-based `SequenceParallelismPass` (drives
`compilation_config.pass_config.enable_sp` / `sp_min_token_num`), used by
torch.compile models, and
- the eager, in-forward SP applied by models that run without
torch.compile (e.g. DeepSeek V4's per-token mHC residual stream).
Values:
- `None` (default): sequence parallelism is not used. For the compile pass
this defers to the optimization-level default.
- `N > 0`: use SP, engaging only on forwards with at least `N` tokens (so
smaller forwards such as decode run plain TP).
Only takes effect when `tensor_parallel_size > 1`."""
ubatch_size: int = Field(default=0, ge=0)
"""Number of ubatch size."""
@@ -630,6 +648,14 @@ class ParallelConfig:
assert last_exc is not None
raise last_exc
@property
def enable_sp(self) -> bool:
return self.tensor_parallel_size > 1 and self.sp_threshold is not None
@property
def sequence_parallel_size(self) -> int:
return self.tensor_parallel_size if self.enable_sp else 1
# The all_reduce at the end of attention (during o_proj) means that
# inputs are replicated across each rank of the tensor parallel group.
# If using expert-parallelism with DeepEP All2All ops, replicated
+13
View File
@@ -1201,6 +1201,19 @@ class VllmConfig:
# async tp is built on top of sequence parallelism and requires it.
pass_config = self.compilation_config.pass_config
# Map the umbrella `parallel_config.sp_threshold` onto the compile-based
# SP pass (the eager in-forward SP path reads sp_threshold directly):
# None -> defer to the optimization-level default
# N > 0 -> force the pass on with N as its min-token threshold
sp_threshold = self.parallel_config.sp_threshold
# Only enable the compile-based SP pass when compilation actually runs.
# The eager in-forward SP path reads sp_threshold directly and runs the
# V2 model runner, which rejects the compile SP pass; under enforce_eager
# there is no compilation to attach it to anyway.
if sp_threshold is not None and not self.model_config.enforce_eager:
pass_config.enable_sp = True
if pass_config.sp_min_token_num is None:
pass_config.sp_min_token_num = sp_threshold
if pass_config.fuse_gemm_comms:
pass_config.enable_sp = True
if pass_config.enable_sp:
+6
View File
@@ -490,6 +490,7 @@ class EngineArgs:
all2all_backend: All2AllBackend = ParallelConfig.all2all_backend
enable_elastic_ep: bool = ParallelConfig.enable_elastic_ep
enable_dbo: bool = ParallelConfig.enable_dbo
sp_threshold: int | None = ParallelConfig.sp_threshold
ubatch_size: int = ParallelConfig.ubatch_size
dbo_decode_token_threshold: int = ParallelConfig.dbo_decode_token_threshold
dbo_prefill_token_threshold: int = ParallelConfig.dbo_prefill_token_threshold
@@ -1097,6 +1098,10 @@ class EngineArgs:
"--all2all-backend", **parallel_kwargs["all2all_backend"]
)
parallel_group.add_argument("--enable-dbo", **parallel_kwargs["enable_dbo"])
parallel_group.add_argument(
"--sp-threshold",
**parallel_kwargs["sp_threshold"],
)
parallel_group.add_argument(
"--ubatch-size",
**parallel_kwargs["ubatch_size"],
@@ -2092,6 +2097,7 @@ class EngineArgs:
all2all_backend=self.all2all_backend,
enable_elastic_ep=self.enable_elastic_ep,
enable_dbo=self.enable_dbo,
sp_threshold=self.sp_threshold,
ubatch_size=self.ubatch_size,
dbo_decode_token_threshold=self.dbo_decode_token_threshold,
dbo_prefill_token_threshold=self.dbo_prefill_token_threshold,
+78 -2
View File
@@ -15,6 +15,9 @@ from vllm.distributed import (
get_pp_group,
get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size,
tensor_model_parallel_all_gather,
tensor_model_parallel_all_reduce,
tensor_model_parallel_reduce_scatter,
)
from vllm.distributed.eplb.eplb_state import EplbLayerState
from vllm.forward_context import get_forward_context, is_forward_context_available
@@ -57,6 +60,7 @@ from vllm.model_executor.models.utils import (
is_pp_missing_parameter,
make_layers,
maybe_prefix,
sequence_parallel_chunk,
)
from vllm.model_executor.utils import set_weight_attrs
from vllm.models.deepseek_v4.attention import DeepseekV4Attention
@@ -524,6 +528,12 @@ class DeepseekV4MoE(nn.Module):
"Enable it with --enable-expert-parallel, or pick a different "
"moe backend."
)
self.enable_sp = vllm_config.parallel_config.enable_sp
if self.enable_sp and self.use_mega_moe:
raise NotImplementedError(
"mHC sequence parallelism requires the FusedMoE path; "
"MegaMoE + sequence parallelism is not supported."
)
self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
self.hidden_size = config.hidden_size
@@ -682,6 +692,7 @@ class DeepseekV4MoE(nn.Module):
router_logits_dtype=torch.float32,
enable_eplb=parallel_config.enable_eplb,
num_redundant_experts=eplb_config.num_redundant_experts,
is_sequence_parallel=self.enable_sp,
)
def forward(
@@ -797,6 +808,7 @@ class DeepseekV4DecoderLayer(nn.Module):
config = vllm_config.model_config.hf_config
self.hidden_size = config.hidden_size
self.enable_sp = vllm_config.parallel_config.enable_sp
self.rms_norm_eps = config.rms_norm_eps
self.attn = _select_dsv4_attn_cls(vllm_config)(
@@ -805,6 +817,8 @@ class DeepseekV4DecoderLayer(nn.Module):
topk_indices_buffer=topk_indices_buffer,
aux_stream_list=aux_stream_list,
)
if self.enable_sp:
self.attn.wo_b.reduce_results = False
self.ffn = DeepseekV4MoE(vllm_config, prefix=f"{prefix}.ffn")
self.attn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
@@ -866,6 +880,7 @@ class DeepseekV4DecoderLayer(nn.Module):
post_mix: torch.Tensor | None = None,
res_mix: torch.Tensor | None = None,
residual: torch.Tensor | None = None,
is_sp_sharded: bool = False,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
attn_norm_weight = self.attn_norm.weight.data
attn_norm_eps = self.attn_norm.variance_epsilon
@@ -904,9 +919,18 @@ class DeepseekV4DecoderLayer(nn.Module):
norm_weight=attn_norm_weight,
norm_eps=attn_norm_eps,
)
if is_sp_sharded:
x = tensor_model_parallel_all_gather(x, dim=0)
# attn_norm is fused into mhc_pre_tilelang / mhc_fused_post_pre above.
x = self.attn(positions, x, None)
if self.enable_sp:
# When SP is enabled, attn o-proj returns an un-reduced per-rank partial.
if is_sp_sharded:
# `num_tokens` meets the sp_threshold, so we do reduce-scatter.
x = tensor_model_parallel_reduce_scatter(x, dim=0)
else:
# `num_tokens` does not meet the sp_threshold, so we do all-reduce.
x = tensor_model_parallel_all_reduce(x)
ffn_norm_weight = self.ffn_norm.weight.data
ffn_norm_eps = self.ffn_norm.variance_epsilon
@@ -928,8 +952,18 @@ class DeepseekV4DecoderLayer(nn.Module):
norm_weight=ffn_norm_weight,
norm_eps=ffn_norm_eps,
)
if is_sp_sharded:
x = tensor_model_parallel_all_gather(x, dim=0)
x = self.ffn(x, input_ids)
if self.enable_sp:
# When SP is enabled, MLP/MoE returns an un-reduced per-rank partial.
if is_sp_sharded:
# `num_tokens` meets the sp_threshold, so we do reduce-scatter.
x = tensor_model_parallel_reduce_scatter(x, dim=0)
else:
# `num_tokens` does not meet the sp_threshold, so we do all-reduce.
x = tensor_model_parallel_all_reduce(x)
return x, residual, post_mix, res_mix
@@ -1033,6 +1067,19 @@ class DeepseekV4Model(nn.Module):
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
return self.embed_tokens(input_ids)
def _is_sp_sharded(self, num_tokens: int) -> bool:
# Engage SP once the (tp-padded) token count clears the threshold. The
# runner pads eligible batches to a multiple of tp_size, so the chunk is
# exact. enable_sp guarantees sp_threshold is not None. Never shard
# fewer tokens than tp ranks (can't give each rank a token) -> plain TP.
sp_threshold = self.parallel_config.sp_threshold
return (
self.parallel_config.enable_sp
and sp_threshold is not None
and num_tokens >= sp_threshold
and num_tokens >= self.parallel_config.tensor_parallel_size
)
def make_empty_intermediate_tensors(
self,
batch_size: int,
@@ -1043,10 +1090,15 @@ class DeepseekV4Model(nn.Module):
# of shape (num_tokens, hc_mult, hidden_size) — V4 expands the
# token embedding to hc_mult streams before the first decoder
# layer and keeps that shape until hc_head() collapses it.
# Under mHC sequence parallelism the stream is token-sharded across the
# TP group, so the per-rank PP buffer holds num_tokens / tp_size rows.
num_tokens = batch_size
if self._is_sp_sharded(batch_size):
num_tokens = cdiv(batch_size, get_tensor_model_parallel_world_size())
return IntermediateTensors(
{
"hidden_states": torch.zeros(
(batch_size, self.hc_mult, self.config.hidden_size),
(num_tokens, self.hc_mult, self.config.hidden_size),
dtype=dtype,
device=device,
),
@@ -1060,15 +1112,32 @@ class DeepseekV4Model(nn.Module):
intermediate_tensors: IntermediateTensors | None,
inputs_embeds: torch.Tensor | None = None,
) -> torch.Tensor | IntermediateTensors:
# Whether to token-shard the mHC stream this forward (eager SP). A pure
# function of the full (padded) token count + threshold, so it is stable
# between cudagraph capture and replay for a given bucket. (The runner
# pads eligible batches to a multiple of tp_size, so an engaged forward's
# count is tp-aligned and the chunk is exact.)
if get_pp_group().is_first_rank:
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.embed_input_ids(input_ids)
hidden_states = hidden_states.unsqueeze(-2).repeat(1, self.hc_mult, 1)
# Full token count is hidden_states.shape[0] here (before the chunk).
is_sp_sharded = self._is_sp_sharded(hidden_states.shape[0])
# Shard the mHC residual stream along the token dim; it stays
# sharded through every layer. Non-first PP ranks already receive a
# sharded stream from the previous rank.
if is_sp_sharded:
hidden_states = sequence_parallel_chunk(hidden_states)
else:
assert intermediate_tensors is not None
hidden_states = intermediate_tensors["hidden_states"]
# Match the producing rank from the full (padded) token count on the
# forward context (rank-independent).
batch_descriptor = get_forward_context().batch_descriptor
assert batch_descriptor is not None
is_sp_sharded = self._is_sp_sharded(batch_descriptor.num_tokens)
if self.use_mega_moe:
input_ids = input_ids.to(torch.int64)
@@ -1082,6 +1151,7 @@ class DeepseekV4Model(nn.Module):
post_mix,
res_mix,
residual,
is_sp_sharded,
)
if layer is not None:
hidden_states = mhc_post_tilelang(
@@ -1091,6 +1161,12 @@ class DeepseekV4Model(nn.Module):
if not get_pp_group().is_last_rank:
return IntermediateTensors({"hidden_states": hidden_states})
# Reassemble the full token stream before the MTP buffer copy and the
# hc_head collapse (both need all tokens on this rank). Only when this
# forward actually sharded; otherwise the stream is already full.
if is_sp_sharded:
hidden_states = tensor_model_parallel_all_gather(hidden_states, dim=0)
# Stash pre-hc_head residual for the MTP draft (captured copy_).
num_tokens = hidden_states.shape[0]
self._mtp_hidden_buffer[:num_tokens].copy_(hidden_states.flatten(1))
+22 -2
View File
@@ -22,6 +22,7 @@ from vllm.config import VllmConfig
from vllm.distributed import (
get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size,
tensor_model_parallel_all_gather,
)
from vllm.logger import init_logger
from vllm.model_executor.kernels.mhc.tilelang import (
@@ -40,7 +41,7 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.deepseek_mtp import SharedHead
from vllm.model_executor.models.deepseek_v2 import get_spec_layer_idx_from_weight_name
from vllm.model_executor.models.utils import maybe_prefix
from vllm.model_executor.models.utils import maybe_prefix, sequence_parallel_chunk
from vllm.models.deepseek_v4.common.ops import (
fused_mtp_input_rmsnorm,
mtp_shared_head_rmsnorm,
@@ -80,6 +81,7 @@ class DeepSeekV4MultiTokenPredictorLayer(nn.Module):
self.config = config
quant_config = vllm_config.quant_config
self.rms_norm_eps = config.rms_norm_eps
self.parallel_config = vllm_config.parallel_config
self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
@@ -122,6 +124,8 @@ class DeepSeekV4MultiTokenPredictorLayer(nn.Module):
self.shared_head = SharedHead(
config=config, prefix=prefix, quant_config=quant_config
)
# The draft decoder block participates in mHC SP like the target's
# layers (sharded-in / sharded-out when the forward engages SP).
self.mtp_block = DeepseekV4DecoderLayer(
vllm_config,
prefix,
@@ -157,10 +161,26 @@ class DeepSeekV4MultiTokenPredictorLayer(nn.Module):
hidden_states = self.h_proj(previous_hidden_states) + self.e_proj(
inputs_embeds
).unsqueeze(-2)
sp_threshold = self.parallel_config.sp_threshold
tp_size = self.parallel_config.tensor_parallel_size
# Never shard fewer tokens than tp ranks -> plain TP.
is_sp_sharded = (
self.parallel_config.enable_sp
and hidden_states.shape[0] >= sp_threshold
and hidden_states.shape[0] >= tp_size
)
if is_sp_sharded:
hidden_states = sequence_parallel_chunk(hidden_states)
hidden_states, residual, post_mix, res_mix = self.mtp_block(
positions=positions, x=hidden_states, input_ids=None
positions=positions,
x=hidden_states,
input_ids=None,
is_sp_sharded=is_sp_sharded,
)
hidden_states = mhc_post_tilelang(hidden_states, residual, post_mix, res_mix)
if is_sp_sharded:
hidden_states = tensor_model_parallel_all_gather(hidden_states, dim=0)
# Return the flat pre-hc_head residual so it can be re-fed as the
# next spec step's `previous_hidden_states` when
# num_speculative_tokens > 1. hc_head is deferred to compute_logits.
+24 -1
View File
@@ -45,7 +45,7 @@ from vllm.model_executor.model_loader import get_model_loader
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.sequence import IntermediateTensors
from vllm.tasks import SupportedTask
from vllm.utils.math_utils import cdiv
from vllm.utils.math_utils import cdiv, round_up
from vllm.utils.mem_utils import DeviceMemoryProfiler, format_gib
from vllm.utils.torch_utils import PIN_MEMORY, STR_DTYPE_TO_TORCH_DTYPE
from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
@@ -464,6 +464,21 @@ class GPUModelRunner(LoRAModelRunnerMixin):
self.kv_cache_config,
self.max_num_reqs,
)
sp_threshold = self.parallel_config.sp_threshold
max_cg_size = self.compilation_config.max_cudagraph_capture_size
if (
sp_threshold is not None
and self.parallel_config.tensor_parallel_size > 1
and max_cg_size is not None
and sp_threshold <= max_cg_size
):
raise ValueError(
f"sp_threshold ({sp_threshold}) must be larger than the max "
f"cudagraph capture size ({max_cg_size}) so that sequence "
"parallelism never engages on a captured cudagraph size."
)
self.cudagraph_manager = ModelCudaGraphManager(
self.vllm_config,
self.device,
@@ -1143,6 +1158,14 @@ class GPUModelRunner(LoRAModelRunnerMixin):
max_query_len = max(scheduler_output.num_scheduled_tokens.values())
uniform_tok_count = get_uniform_token_count(num_reqs, num_toks, max_query_len)
# mHC sequence parallelism shards the token dim across the TP group.
# so a forward that engages SP (>= sp_threshold tokens) must run
# on a multiple of tp_size tokens.
tp_size = self.parallel_config.tensor_parallel_size
sp_threshold = self.parallel_config.sp_threshold
if sp_threshold is not None and num_toks >= sp_threshold:
num_toks = round_up(num_toks, tp_size)
num_active_loras = 0
if self.lora_config:
req_ids = list(scheduler_output.num_scheduled_tokens.keys())