[DSV4] Implement Sequence Parallelism (#46789)
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
Woosuk Kwon committed
Aug 1, 2026 at 10:22 UTC
38a466e7b6e087d67c35e7f924c04c245423c99f
7 files changed
+138
-29
tests/models/kimi_k3/test_sequence_parallel.py
+19
-1
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+import math
5
from types import MethodType, SimpleNamespace
6
from unittest.mock import Mock
7
@@ -9,9 +10,9 @@ import torch
10
from torch import nn
11
12
from vllm.config import ParallelConfig
13
+from vllm.models.common.ops import sequence_parallel as sp_ops
14
from vllm.models.kimi_k3.nvidia import model as kimi_model
15
from vllm.models.kimi_k3.nvidia import mtp as kimi_mtp
14
-from vllm.models.kimi_k3.nvidia.ops import sequence_parallel as sp_ops
16
from vllm.platforms import current_platform
17
18
@@ -316,6 +317,23 @@ def test_sp_reduce_scatter_uses_custom_kernel_after_padding(monkeypatch):
317
fallback.assert_not_called()
318
319
320
+@pytest.mark.parametrize("shape", [(3,), (3, 2, 2)])
321
+def test_sp_shard_pads_only_the_token_axis(monkeypatch, shape):
322
+ hidden_states = torch.arange(math.prod(shape), dtype=torch.float32).view(shape)
323
+ monkeypatch.setattr(
324
+ sp_ops,
325
+ "get_tensor_model_parallel_world_size",
326
+ lambda: 2,
327
+ )
328
+ monkeypatch.setattr(sp_ops, "get_tensor_model_parallel_rank", lambda: 1)
329
+
330
+ output = sp_ops.sp_shard(hidden_states)
331
+
332
+ padding = hidden_states.new_zeros((1, *shape[1:]))
333
+ expected = torch.cat([hidden_states, padding])[2:]
334
+ torch.testing.assert_close(output, expected)
335
+
336
+
337
def test_sp_collectives_fall_back_without_custom_kernel(monkeypatch):
338
hidden_states = torch.arange(4, dtype=torch.float32).view(2, 2)
339
monkeypatch.setattr(
vllm/models/common/ops/sequence_parallel.py
renamed
+4
-9
@@ -12,17 +12,12 @@ from vllm.distributed import (
12
)
13
14
15
-def _custom_collective(
16
- name: str,
17
- x: torch.Tensor,
18
-) -> torch.Tensor | None:
15
+def _custom_collective(name: str, x: torch.Tensor) -> torch.Tensor | None:
16
device_communicator = get_tp_group().device_communicator
17
if device_communicator is None:
18
return None
19
collective = getattr(device_communicator, name, None)
23
- if collective is None:
24
- return None
25
- return collective(x)
20
+ return None if collective is None else collective(x)
21
22
23
def sp_all_gather(x: torch.Tensor) -> torch.Tensor:
@@ -45,12 +40,12 @@ def sp_reduce_scatter(x: torch.Tensor) -> torch.Tensor:
40
41
42
def sp_shard(x: torch.Tensor) -> torch.Tensor:
48
- assert x.ndim == 2
43
tp_size = get_tensor_model_parallel_world_size()
44
tp_rank = get_tensor_model_parallel_rank()
45
sp_pad = (-x.shape[0]) % tp_size
46
if sp_pad > 0:
53
- x = torch.nn.functional.pad(x, (0, 0, 0, sp_pad))
47
+ pad = (0, 0) * (x.ndim - 1) + (0, sp_pad)
48
+ x = torch.nn.functional.pad(x, pad)
49
chunk = x.shape[0] // tp_size
50
return x[tp_rank * chunk : (tp_rank + 1) * chunk]
51
vllm/models/deepseek_v4/nvidia/dspark.py
+30
-4
@@ -15,11 +15,13 @@ import regex as re
15
import torch
16
import torch.nn as nn
17
18
+import vllm.envs as envs
19
from vllm.config import VllmConfig, get_current_vllm_config
20
from vllm.distributed import (
21
get_tensor_model_parallel_rank,
22
get_tensor_model_parallel_world_size,
23
)
24
+from vllm.forward_context import get_forward_context, is_forward_context_available
25
from vllm.logger import init_logger
26
from vllm.model_executor.kernels.mhc.tilelang import (
27
hc_head_fused_kernel_tilelang,
@@ -40,10 +42,16 @@ from vllm.model_executor.models.qwen3_dspark import (
42
DSparkMarkovHead,
43
)
44
from vllm.model_executor.models.utils import maybe_prefix
45
+from vllm.models.common.ops.sequence_parallel import (
46
+ sp_all_gather,
47
+ sp_padding_mask,
48
+ sp_shard,
49
+)
50
51
from .model import (
52
DeepseekV4DecoderLayer,
53
DeepseekV4Model,
54
+ _use_sequence_parallel,
55
make_deepseek_v4_expert_params_mapping,
56
)
57
@@ -66,6 +74,7 @@ class DSparkDeepseekV4Model(nn.Module):
74
self.rms_norm_eps = config.rms_norm_eps
75
self.num_hidden_layers = config.num_hidden_layers
76
self.target_layer_ids = tuple(config.dspark_target_layer_ids)
77
+ self.use_sequence_parallel = _use_sequence_parallel(vllm_config)
78
79
self.num_dspark_layers = getattr(config, "n_mtp_layers", None) or 3
80
@@ -86,12 +95,19 @@ class DSparkDeepseekV4Model(nn.Module):
95
)
96
self.main_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
97
98
+ self.topk_indices_buffer = torch.empty(
99
+ vllm_config.scheduler_config.max_num_batched_tokens,
100
+ config.index_topk,
101
+ dtype=torch.int32,
102
+ )
103
+
104
current_vllm_config = get_current_vllm_config()
105
self.layers = nn.ModuleList(
106
[
107
DeepseekV4DecoderLayer(
108
current_vllm_config,
109
prefix=maybe_prefix(prefix, f"layers.{self.num_hidden_layers + i}"),
110
+ topk_indices_buffer=self.topk_indices_buffer,
111
)
112
for i in range(self.num_dspark_layers)
113
]
@@ -172,6 +188,15 @@ class DSparkDeepseekV4Model(nn.Module):
188
) -> torch.Tensor:
189
if inputs_embeds is None:
190
inputs_embeds = self.embed_input_ids(input_ids)
191
+ full_num_tokens = positions.shape[0]
192
+ if self.use_sequence_parallel:
193
+ if envs.VLLM_MOE_SKIP_PADDING and is_forward_context_available():
194
+ forward_context = get_forward_context()
195
+ forward_context.is_padding = sp_padding_mask(
196
+ forward_context.is_padding, inputs_embeds
197
+ )
198
+ inputs_embeds = sp_shard(inputs_embeds)
199
+ input_ids = sp_shard(input_ids)
200
# Expand to hc_mult copies for hyper-connections ([T, H] -> [T, hc, H]).
201
hidden_states = inputs_embeds.unsqueeze(-2).repeat(1, self.hc_mult, 1)
202
@@ -186,6 +211,8 @@ class DSparkDeepseekV4Model(nn.Module):
211
residual,
212
)
213
hidden_states = mhc_post_tilelang(hidden_states, residual, post_mix, res_mix)
214
+ if self.use_sequence_parallel:
215
+ hidden_states = sp_all_gather(hidden_states)[:full_num_tokens]
216
# hc_head reduces the hc copies; return the PRE-norm head hidden
217
hidden_states = hc_head_fused_kernel_tilelang(
218
hidden_states,
@@ -279,10 +306,9 @@ class DSparkDeepseekV4ForCausalLM(nn.Module):
306
self.draft_model_config = vllm_config.speculative_config.draft_model_config
307
self.config = self.draft_model_config.hf_config
308
self.quant_config = vllm_config.quant_config
282
- self.pad_shared_expert = (
283
- getattr(self.quant_config, "weight_block_size", None) is not None
284
- and not vllm_config.parallel_config.use_sequence_parallel_moe
285
- )
309
+ self.pad_shared_expert = getattr(
310
+ self.quant_config, "weight_block_size", None
311
+ ) is not None and not _use_sequence_parallel(vllm_config)
312
self.model = DSparkDeepseekV4Model(
313
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
314
)
vllm/models/deepseek_v4/nvidia/model.py
+53
-4
@@ -65,6 +65,12 @@ from vllm.model_executor.models.utils import (
65
maybe_prefix,
66
)
67
from vllm.model_executor.utils import set_weight_attrs
68
+from vllm.models.common.ops.sequence_parallel import (
69
+ sp_all_gather,
70
+ sp_padding_mask,
71
+ sp_reduce_scatter,
72
+ sp_shard,
73
+)
74
from vllm.models.deepseek_v4.attention import DeepseekV4Attention
75
from vllm.models.deepseek_v4.eager_scratch import DeepseekV4EagerScratchPool
76
from vllm.models.deepseek_v4.nvidia.flashinfer_sparse import (
@@ -515,6 +521,7 @@ class DeepseekV4MoE(nn.Module):
521
self,
522
vllm_config: VllmConfig,
523
prefix: str = "",
524
+ use_sequence_parallel: bool = False,
525
):
526
super().__init__()
527
@@ -522,6 +529,7 @@ class DeepseekV4MoE(nn.Module):
529
config = vllm_config.model_config.hf_config
530
quant_config = vllm_config.quant_config
531
self.prefix = prefix
532
+ self.use_sequence_parallel = use_sequence_parallel
533
self.use_mega_moe = (
534
vllm_config.kernel_config.moe_backend == "deep_gemm_mega_moe"
535
)
@@ -595,6 +603,7 @@ class DeepseekV4MoE(nn.Module):
603
swiglu_limit=self.swiglu_limit,
604
quant_config=quant_config,
605
reduce_results=self.use_mega_moe,
606
+ is_sequence_parallel=use_sequence_parallel,
607
prefix=f"{prefix}.shared_experts",
608
)
609
@@ -689,6 +698,7 @@ class DeepseekV4MoE(nn.Module):
698
router_logits_dtype=torch.float32,
699
enable_eplb=parallel_config.enable_eplb,
700
num_redundant_experts=eplb_config.num_redundant_experts,
701
+ is_sequence_parallel=self.use_sequence_parallel,
702
)
703
704
def forward(
@@ -792,6 +802,17 @@ def _select_dsv4_attn_cls(vllm_config: VllmConfig) -> type[DeepseekV4Attention]:
802
return DeepseekV4FlashMLAAttention
803
804
805
+def _use_sequence_parallel(vllm_config: VllmConfig) -> bool:
806
+ parallel_config = vllm_config.parallel_config
807
+ use_mega_moe = vllm_config.kernel_config.moe_backend == "deep_gemm_mega_moe"
808
+ return (
809
+ parallel_config.pipeline_parallel_size == 1
810
+ and parallel_config.enable_expert_parallel
811
+ and parallel_config.tensor_parallel_size > 1
812
+ and (use_mega_moe or parallel_config.data_parallel_size > 1)
813
+ )
814
+
815
+
816
class DeepseekV4DecoderLayer(nn.Module):
817
def __init__(
818
self,
@@ -805,6 +826,7 @@ class DeepseekV4DecoderLayer(nn.Module):
826
827
config = vllm_config.model_config.hf_config
828
self.hidden_size = config.hidden_size
829
+ self.use_sequence_parallel = _use_sequence_parallel(vllm_config)
830
831
self.rms_norm_eps = config.rms_norm_eps
832
self.attn = _select_dsv4_attn_cls(vllm_config)(
@@ -814,7 +836,13 @@ class DeepseekV4DecoderLayer(nn.Module):
836
aux_stream_list=aux_stream_list,
837
eager_scratch_pool=eager_scratch_pool,
838
)
817
- self.ffn = DeepseekV4MoE(vllm_config, prefix=f"{prefix}.ffn")
839
+ if self.use_sequence_parallel:
840
+ self.attn.wo_b.reduce_results = False
841
+ self.ffn = DeepseekV4MoE(
842
+ vllm_config,
843
+ prefix=f"{prefix}.ffn",
844
+ use_sequence_parallel=self.use_sequence_parallel,
845
+ )
846
847
self.attn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
848
self.ffn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
@@ -932,8 +960,12 @@ class DeepseekV4DecoderLayer(nn.Module):
960
norm_eps=attn_norm_eps,
961
)
962
935
- # attn_norm is fused into mhc_pre_tilelang / mhc_fused_post_pre above.
963
+ if self.use_sequence_parallel:
964
+ x = sp_all_gather(x)[: positions.shape[0]]
965
+
966
x = self.attn(positions, x, None)
967
+ if self.use_sequence_parallel:
968
+ x = sp_reduce_scatter(x)
969
970
ffn_norm_weight = self.ffn_norm.weight.data
971
ffn_norm_eps = self.ffn_norm.variance_epsilon
@@ -972,6 +1004,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
1004
self.use_mega_moe = (
1005
vllm_config.kernel_config.moe_backend == "deep_gemm_mega_moe"
1006
)
1007
+ self.use_sequence_parallel = _use_sequence_parallel(vllm_config)
1008
if self.use_mega_moe and not vllm_config.parallel_config.enable_expert_parallel:
1009
raise NotImplementedError(
1010
"DeepSeek V4 MegaMoE currently requires expert parallel. "
@@ -1114,6 +1147,16 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
1147
if self.use_mega_moe:
1148
input_ids = input_ids.to(torch.int64)
1149
1150
+ full_num_tokens = positions.shape[0]
1151
+ if self.use_sequence_parallel:
1152
+ if envs.VLLM_MOE_SKIP_PADDING and is_forward_context_available():
1153
+ forward_context = get_forward_context()
1154
+ forward_context.is_padding = sp_padding_mask(
1155
+ forward_context.is_padding, hidden_states
1156
+ )
1157
+ hidden_states = sp_shard(hidden_states)
1158
+ input_ids = sp_shard(input_ids)
1159
+
1160
residual, post_mix, res_mix = None, None, None
1161
aux_hidden_states: list[torch.Tensor] = []
1162
final_aux_recon: torch.Tensor | None = None # avoid duplicate mhc_post call
@@ -1134,7 +1177,10 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
1177
aux_recon = mhc_post_tilelang(
1178
hidden_states, residual, post_mix, res_mix
1179
)
1137
- aux_hidden_states.append(aux_recon.mean(dim=1))
1180
+ aux_hidden_state = aux_recon.mean(dim=1)
1181
+ if self.use_sequence_parallel:
1182
+ aux_hidden_state = sp_all_gather(aux_hidden_state)[:full_num_tokens]
1183
+ aux_hidden_states.append(aux_hidden_state)
1184
final_aux_recon = aux_recon
1185
if layer is not None:
1186
# Reuse if the last layer was captured as an aux hidden state
@@ -1148,6 +1194,9 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
1194
if not get_pp_group().is_last_rank:
1195
return IntermediateTensors({"hidden_states": hidden_states})
1196
1197
+ if self.use_sequence_parallel:
1198
+ hidden_states = sp_all_gather(hidden_states)[:full_num_tokens]
1199
+
1200
if self._mtp_hidden_buffer is not None:
1201
num_tokens = hidden_states.shape[0]
1202
self._mtp_hidden_buffer[:num_tokens].copy_(hidden_states.flatten(1))
@@ -1194,7 +1243,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
1243
# ranks land on the zero pad). SP / unquantized ones need no padding.
1244
pad_shared_expert = (
1245
getattr(self.quant_config, "weight_block_size", None) is not None
1197
- and not self.parallel_config.use_sequence_parallel_moe
1246
+ and not self.use_sequence_parallel
1247
)
1248
1249
for name, loaded_weight in weights:
vllm/models/deepseek_v4/nvidia/mtp.py
+21
-4
@@ -18,11 +18,13 @@ import regex as re
18
import torch
19
import torch.nn as nn
20
21
+import vllm.envs as envs
22
from vllm.config import VllmConfig
23
from vllm.distributed import (
24
get_tensor_model_parallel_rank,
25
get_tensor_model_parallel_world_size,
26
)
27
+from vllm.forward_context import get_forward_context, is_forward_context_available
28
from vllm.logger import init_logger
29
from vllm.model_executor.kernels.mhc.tilelang import (
30
hc_head_fused_kernel_tilelang,
@@ -44,6 +46,11 @@ from vllm.model_executor.model_loader.weight_utils import default_weight_loader
46
from vllm.model_executor.models.deepseek_mtp import SharedHead
47
from vllm.model_executor.models.deepseek_v2 import get_spec_layer_idx_from_weight_name
48
from vllm.model_executor.models.utils import maybe_prefix
49
+from vllm.models.common.ops.sequence_parallel import (
50
+ sp_all_gather,
51
+ sp_padding_mask,
52
+ sp_shard,
53
+)
54
from vllm.models.deepseek_v4.common.ops import (
55
fused_mtp_input_rmsnorm,
56
mtp_shared_head_rmsnorm,
@@ -53,6 +60,7 @@ from vllm.sequence import IntermediateTensors
60
from .model import (
61
DeepseekV4DecoderLayer,
62
DeepseekV4Model,
63
+ _use_sequence_parallel,
64
make_deepseek_v4_expert_params_mapping,
65
)
66
@@ -158,6 +166,14 @@ class DeepSeekV4MultiTokenPredictorLayer(nn.Module):
166
self.enorm.variance_epsilon,
167
self.hc_mult,
168
)
169
+ if self.mtp_block.use_sequence_parallel:
170
+ if envs.VLLM_MOE_SKIP_PADDING and is_forward_context_available():
171
+ forward_context = get_forward_context()
172
+ forward_context.is_padding = sp_padding_mask(
173
+ forward_context.is_padding, inputs_embeds
174
+ )
175
+ inputs_embeds = sp_shard(inputs_embeds)
176
+ previous_hidden_states = sp_shard(previous_hidden_states)
177
hidden_states = self.h_proj(previous_hidden_states) + self.e_proj(
178
inputs_embeds
179
).unsqueeze(-2)
@@ -165,6 +181,8 @@ class DeepSeekV4MultiTokenPredictorLayer(nn.Module):
181
positions=positions, x=hidden_states, input_ids=None
182
)
183
hidden_states = mhc_post_tilelang(hidden_states, residual, post_mix, res_mix)
184
+ if self.mtp_block.use_sequence_parallel:
185
+ hidden_states = sp_all_gather(hidden_states)[: positions.shape[0]]
186
# Return the flat pre-hc_head residual so it can be re-fed as the
187
# next spec step's `previous_hidden_states` when
188
# num_speculative_tokens > 1. hc_head is deferred to compute_logits.
@@ -266,10 +284,9 @@ class DeepSeekV4MTP(nn.Module):
284
super().__init__()
285
self.config = vllm_config.model_config.hf_config
286
self.quant_config = vllm_config.quant_config
269
- self.pad_shared_expert = (
270
- getattr(self.quant_config, "weight_block_size", None) is not None
271
- and not vllm_config.parallel_config.use_sequence_parallel_moe
272
- )
287
+ self.pad_shared_expert = getattr(
288
+ self.quant_config, "weight_block_size", None
289
+ ) is not None and not _use_sequence_parallel(vllm_config)
290
self.model = DeepSeekV4MultiTokenPredictor(
291
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
292
)
vllm/models/kimi_k3/nvidia/model.py
+6
-6
@@ -88,6 +88,12 @@ from vllm.model_executor.models.utils import (
88
maybe_prefix,
89
)
90
from vllm.model_executor.models.vision import is_vit_use_data_parallel
91
+from vllm.models.common.ops.sequence_parallel import (
92
+ sp_all_gather,
93
+ sp_padding_mask,
94
+ sp_reduce_scatter,
95
+ sp_shard,
96
+)
97
from vllm.models.deepseek_v4.nvidia.model import DeepseekV4MegaMoEExperts
98
from vllm.models.deepseek_v4.nvidia.ops.prepare_megamoe import prepare_megamoe_inputs
99
from vllm.models.kimi_k3.nvidia.kda import KimiK3DeltaAttention
@@ -96,12 +102,6 @@ from vllm.models.kimi_k3.nvidia.low_latency_gemm import (
102
)
103
from vllm.models.kimi_k3.nvidia.mla import MultiHeadLatentAttention
104
from vllm.models.kimi_k3.nvidia.ops import attn_res
99
-from vllm.models.kimi_k3.nvidia.ops.sequence_parallel import (
100
- sp_all_gather,
101
- sp_padding_mask,
102
- sp_reduce_scatter,
103
- sp_shard,
104
-)
105
from vllm.multimodal import MULTIMODAL_REGISTRY
106
from vllm.multimodal.inputs import NestedTensors
107
from vllm.platforms import current_platform
vllm/models/kimi_k3/nvidia/mtp.py
+5
-1
@@ -27,6 +27,11 @@ from vllm.model_executor.model_loader.weight_utils import (
27
maybe_remap_kv_scale_name,
28
)
29
from vllm.model_executor.models.utils import get_pp_missing_layer_names, maybe_prefix
30
+from vllm.models.common.ops.sequence_parallel import (
31
+ sp_all_gather,
32
+ sp_padding_mask,
33
+ sp_shard,
34
+)
35
from vllm.sequence import IntermediateTensors
36
from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
37
@@ -38,7 +43,6 @@ from .model import (
43
get_spec_layer_idx_from_weight_name,
44
make_kimi_k3_mega_moe_expert_params_mapping,
45
)
41
-from .ops.sequence_parallel import sp_all_gather, sp_padding_mask, sp_shard
46
47
logger = init_logger(__name__)
48