A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
[Bugfix] Fix sequence parallelism bug when enable pipeline parallelism (#24021)
Signed-off-by: cascade812 <cascade812@outlook.com>
cascade committed
Sep 15, 2025 at 21:32 UTC
17871983a2efde49d93ee8692f17da490da47e95
6 files changed
+135
-42
tests/distributed/test_sequence_parallel.py
+2
-1
@@ -235,7 +235,6 @@ def _compare_sp(
235
'level': 3,
236
'custom_ops': ["+rms_norm"],
237
'compile_sizes': [4, 8],
238
- 'splitting_ops': [],
238
'pass_config': {
239
'enable_sequence_parallelism': True,
240
'enable_fusion': enable_fusion,
@@ -251,6 +250,8 @@ def _compare_sp(
250
*common_args,
251
"--tensor-parallel-size",
252
str(tp_size),
253
+ "--pipeline-parallel-size",
254
+ str(pp_size),
255
"--distributed-executor-backend",
256
distributed_backend,
257
"--compilation_config",
vllm/distributed/parallel_state.py
+46
-5
@@ -663,14 +663,29 @@ class GroupCoordinator:
663
tensor_dict: dict[str, Union[torch.Tensor, Any]],
664
dst: Optional[int] = None,
665
all_gather_group: Optional["GroupCoordinator"] = None,
666
+ all_gather_tensors: Optional[dict[str, bool]] = None,
667
) -> Optional[dict[str, Union[torch.Tensor, Any]]]:
668
"""Send the input tensor dictionary.
669
NOTE: `dst` is the local rank of the source rank.
670
+
671
+ all_gather_group: The group for the all-gather operation. If provided,
672
+ an optimization is enabled where each rank in the group sends a
673
+ slice of a tensor and the receiver reconstructs it using an
674
+ all-gather, which can improve performance. This is typically the
675
+ tensor-parallel group.
676
+ all_gather_tensors: A dictionary to specify which tensors should use
677
+ the all-gather optimization, which is only effective when
678
+ `all_gather_group` is provided. By default, this optimization is
679
+ on for any tensor whose size is divisible by the
680
+ `all_gather_group`'s world size. However, it should be disabled
681
+ for tensors that are not fully replicated across the group (e.g.,
682
+ the residual tensor when sequence parallelism is enabled). This
683
+ dictionary allows overriding the default behavior on a per-tensor
684
+ basis.
685
"""
686
# Bypass the function if we are using only 1 GPU.
687
if not torch.distributed.is_initialized() or self.world_size == 1:
688
return tensor_dict
673
-
689
all_gather_size = (1 if all_gather_group is None else
690
all_gather_group.world_size)
691
all_gather_rank = (0 if all_gather_group is None else
@@ -699,14 +714,23 @@ class GroupCoordinator:
714
# `send_object_list` has serialization & deserialization,
715
# all happening on CPU. Therefore, we can use the CPU group.
716
self.send_object(metadata_list, dst=dst)
702
- for tensor in tensor_list:
717
+
718
+ tensor_keys = [
719
+ k for k, v in tensor_dict.items() if isinstance(v, torch.Tensor)
720
+ ]
721
+ assert len(tensor_keys) == len(tensor_list)
722
+
723
+ for key, tensor in zip(tensor_keys, tensor_list):
724
if tensor.numel() == 0:
725
# Skip sending empty tensors.
726
continue
727
728
# send-allgather: send only a slice, then do allgather.
708
- if (all_gather_group is not None
709
- and tensor.numel() % all_gather_size == 0):
729
+ use_all_gather = (all_gather_group is not None
730
+ and tensor.numel() % all_gather_size == 0)
731
+ use_all_gather = all_gather_tensors.get(key, use_all_gather) \
732
+ if all_gather_tensors else use_all_gather
733
+ if use_all_gather:
734
tensor = tensor.reshape(all_gather_size, -1)[all_gather_rank]
735
736
if tensor.is_cpu:
@@ -725,14 +749,29 @@ class GroupCoordinator:
749
self,
750
src: Optional[int] = None,
751
all_gather_group: Optional["GroupCoordinator"] = None,
752
+ all_gather_tensors: Optional[dict[str, bool]] = None,
753
) -> Optional[dict[str, Union[torch.Tensor, Any]]]:
754
"""Recv the input tensor dictionary.
755
NOTE: `src` is the local rank of the source rank.
756
+
757
+ all_gather_group: The group for the all-gather operation. If provided,
758
+ an optimization is enabled where each rank in the group sends a
759
+ slice of a tensor and the receiver reconstructs it using an
760
+ all-gather, which can improve performance. This is typically the
761
+ tensor-parallel group.
762
+ all_gather_tensors: A dictionary to specify which tensors should use
763
+ the all-gather optimization, which is only effective when
764
+ `all_gather_group` is provided. By default, this optimization is
765
+ on for any tensor whose size is divisible by the
766
+ `all_gather_group`'s world size. However, it should be disabled
767
+ for tensors that are not fully replicated across the group (e.g.,
768
+ the residual tensor when sequence parallelism is enabled). This
769
+ dictionary allows overriding the default behavior on a per-tensor
770
+ basis.
771
"""
772
# Bypass the function if we are using only 1 GPU.
773
if not torch.distributed.is_initialized() or self.world_size == 1:
774
return None
735
-
775
all_gather_size = (1 if all_gather_group is None else
776
all_gather_group.world_size)
777
all_gather_rank = (0 if all_gather_group is None else
@@ -766,6 +805,8 @@ class GroupCoordinator:
805
# send-allgather: send only a slice, then do allgather.
806
use_all_gather = (all_gather_group is not None
807
and tensor.numel() % all_gather_size == 0)
808
+ use_all_gather = all_gather_tensors.get(key, use_all_gather) \
809
+ if all_gather_tensors else use_all_gather
810
811
if use_all_gather:
812
orig_shape = tensor.shape
vllm/v1/worker/cpu_worker.py
+15
-3
@@ -19,6 +19,7 @@ from vllm.v1.outputs import ModelRunnerOutput
19
from vllm.v1.worker.cpu_model_runner import CPUModelRunner
20
from vllm.v1.worker.gpu_worker import (Worker,
21
init_worker_distributed_environment)
22
+from vllm.v1.worker.utils import is_residual_scattered_for_sp
23
24
logger = init_logger(__name__)
25
@@ -107,18 +108,29 @@ class CPUWorker(Worker):
108
scheduler_output: "SchedulerOutput",
109
) -> Optional[ModelRunnerOutput]:
110
intermediate_tensors = None
111
+ num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
112
+ num_input_tokens = self.model_runner._get_num_input_tokens(
113
+ num_scheduled_tokens)
114
+ all_gather_tensors = {
115
+ "residual":
116
+ not is_residual_scattered_for_sp(self.vllm_config,
117
+ num_input_tokens)
118
+ }
119
if not get_pp_group().is_first_rank:
120
intermediate_tensors = IntermediateTensors(
121
get_pp_group().recv_tensor_dict(
113
- all_gather_group=get_tp_group()))
122
+ all_gather_group=get_tp_group(),
123
+ all_gather_tensors=all_gather_tensors))
124
125
output = self.model_runner.execute_model(scheduler_output,
126
intermediate_tensors)
127
128
if not get_pp_group().is_last_rank:
129
assert isinstance(output, IntermediateTensors)
120
- get_pp_group().send_tensor_dict(output.tensors,
121
- all_gather_group=get_tp_group())
130
+ get_pp_group().send_tensor_dict(
131
+ output.tensors,
132
+ all_gather_group=get_tp_group(),
133
+ all_gather_tensors=all_gather_tensors)
134
return None
135
136
assert isinstance(output, ModelRunnerOutput)
vllm/v1/worker/gpu_model_runner.py
+33
-30
@@ -88,6 +88,7 @@ from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
88
from vllm.v1.worker.kv_connector_model_runner_mixin import (
89
KVConnectorModelRunnerMixin)
90
from vllm.v1.worker.lora_model_runner_mixin import LoRAModelRunnerMixin
91
+from vllm.v1.worker.utils import is_residual_scattered_for_sp
92
93
from .utils import (AttentionGroup, MultiModalBudget,
94
add_kv_sharing_layers_to_kv_cache_groups, bind_kv_cache,
@@ -1633,21 +1634,14 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1634
assert self.intermediate_tensors is not None
1635
1636
tp = self.vllm_config.parallel_config.tensor_parallel_size
1636
- enabled_sp = self.compilation_config.pass_config. \
1637
- enable_sequence_parallelism
1638
- if enabled_sp:
1639
- # When sequence parallelism is enabled, we always pad num_tokens
1640
- # to be a multiple of tensor_parallel_size (tp) earlier
1641
- assert num_tokens % tp == 0
1642
- is_residual_scattered = tp > 1 and enabled_sp \
1643
- and num_tokens % tp == 0
1637
+ is_rs = is_residual_scattered_for_sp(self.vllm_config, num_tokens)
1638
1639
# When sequence parallelism is enabled, the "residual" tensor is sharded
1640
# across tensor parallel ranks, so each rank only needs its own slice.
1641
if sync_self:
1642
assert intermediate_tensors is not None
1643
for k, v in intermediate_tensors.items():
1650
- is_scattered = k == "residual" and is_residual_scattered
1644
+ is_scattered = k == "residual" and is_rs
1645
copy_len = num_tokens // tp if is_scattered else \
1646
num_tokens
1647
self.intermediate_tensors[k][:copy_len].copy_(
@@ -1655,8 +1649,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1649
1650
return IntermediateTensors({
1651
k:
1658
- v[:num_tokens // tp]
1659
- if k == "residual" and is_residual_scattered else v[:num_tokens]
1652
+ v[:num_tokens //
1653
+ tp] if k == "residual" and is_rs else v[:num_tokens]
1654
for k, v in self.intermediate_tensors.items()
1655
})
1656
@@ -1741,6 +1735,25 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1735
pooler_output=pooler_output,
1736
)
1737
1738
+ def _get_num_input_tokens(self, num_scheduled_tokens: int) -> int:
1739
+ if (self.compilation_config.cudagraph_mode != CUDAGraphMode.NONE
1740
+ and not envs.VLLM_DISABLE_PAD_FOR_CUDAGRAPH
1741
+ and hasattr(self, "cudagraph_batch_sizes")
1742
+ and self.cudagraph_batch_sizes
1743
+ and num_scheduled_tokens <= self.cudagraph_batch_sizes[-1]):
1744
+ # Use CUDA graphs.
1745
+ # Add padding to the batch size.
1746
+ return self.vllm_config.pad_for_cudagraph(num_scheduled_tokens)
1747
+
1748
+ # Eager mode.
1749
+ # Pad tokens to multiple of tensor_parallel_size when
1750
+ # enabled collective fusion for SP
1751
+ tp_size = self.vllm_config.parallel_config.tensor_parallel_size
1752
+ if (self.compilation_config.pass_config.enable_sequence_parallelism
1753
+ and tp_size > 1):
1754
+ return round_up(num_scheduled_tokens, tp_size)
1755
+ return num_scheduled_tokens
1756
+
1757
def _preprocess(
1758
self,
1759
scheduler_output: "SchedulerOutput",
@@ -1750,24 +1763,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1763
Optional[IntermediateTensors], dict[str, Any]]:
1764
1765
num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
1753
- if (self.compilation_config.cudagraph_mode != CUDAGraphMode.NONE
1754
- and not envs.VLLM_DISABLE_PAD_FOR_CUDAGRAPH
1755
- and num_scheduled_tokens <= self.cudagraph_batch_sizes[-1]):
1756
- # Use CUDA graphs.
1757
- # Add padding to the batch size.
1758
- num_input_tokens = self.vllm_config.pad_for_cudagraph(
1759
- num_scheduled_tokens)
1760
- else:
1761
- # Eager mode.
1762
- # Pad tokens to multiple of tensor_parallel_size when
1763
- # enabled collective fusion for SP
1764
- tp_size = self.vllm_config.parallel_config.tensor_parallel_size
1765
- if self.compilation_config.pass_config. \
1766
- enable_sequence_parallelism and tp_size > 1:
1767
- num_input_tokens = round_up(num_scheduled_tokens, tp_size)
1768
- else:
1769
- num_input_tokens = num_scheduled_tokens
1770
-
1766
+ num_input_tokens = self._get_num_input_tokens(num_scheduled_tokens)
1767
# Padding for DP
1768
num_pad, num_tokens_across_dp = self.get_dp_padding(num_input_tokens)
1769
num_input_tokens += num_pad
@@ -2108,8 +2104,15 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2104
assert not self.is_pooling_model
2105
2106
if not get_pp_group().is_last_rank:
2107
+ all_gather_tensors = {
2108
+ "residual":
2109
+ not is_residual_scattered_for_sp(
2110
+ self.vllm_config, num_input_tokens)
2111
+ }
2112
get_pp_group().send_tensor_dict(
2112
- hidden_states.tensors, all_gather_group=get_tp_group())
2113
+ hidden_states.tensors,
2114
+ all_gather_group=get_tp_group(),
2115
+ all_gather_tensors=all_gather_tensors)
2116
logits = None
2117
else:
2118
sample_hidden_states = hidden_states[logits_indices]
vllm/v1/worker/gpu_worker.py
+13
-2
@@ -32,6 +32,7 @@ from vllm.v1.outputs import (EMPTY_MODEL_RUNNER_OUTPUT, AsyncModelRunnerOutput,
32
DraftTokenIds, ModelRunnerOutput)
33
from vllm.v1.utils import report_usage_stats
34
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
35
+from vllm.v1.worker.utils import is_residual_scattered_for_sp
36
from vllm.v1.worker.worker_base import WorkerBase
37
38
logger = init_logger(__name__)
@@ -428,10 +429,19 @@ class Worker(WorkerBase):
429
) -> Optional[Union[ModelRunnerOutput, AsyncModelRunnerOutput]]:
430
intermediate_tensors = None
431
forward_pass = scheduler_output.total_num_scheduled_tokens > 0
432
+ num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
433
+ num_input_tokens = self.model_runner._get_num_input_tokens(
434
+ num_scheduled_tokens)
435
+ all_gather_tensors = {
436
+ "residual":
437
+ not is_residual_scattered_for_sp(self.vllm_config,
438
+ num_input_tokens)
439
+ }
440
if forward_pass and not get_pp_group().is_first_rank:
441
intermediate_tensors = IntermediateTensors(
442
get_pp_group().recv_tensor_dict(
434
- all_gather_group=get_tp_group()))
443
+ all_gather_group=get_tp_group(),
444
+ all_gather_tensors=all_gather_tensors))
445
446
output = self.model_runner.execute_model(scheduler_output,
447
intermediate_tensors)
@@ -444,7 +454,8 @@ class Worker(WorkerBase):
454
"external_launcher") and not get_pp_group().is_last_rank
455
456
get_pp_group().send_tensor_dict(output.tensors,
447
- all_gather_group=get_tp_group())
457
+ all_gather_group=get_tp_group(),
458
+ all_gather_tensors=all_gather_tensors)
459
460
kv_connector_output = output.kv_connector_output
461
if not kv_connector_output:
vllm/v1/worker/utils.py
+26
-1
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Optional
7
import torch
8
9
from vllm.attention.backends.abstract import AttentionBackend
10
-from vllm.config import ModelConfig, SchedulerConfig
10
+from vllm.config import ModelConfig, SchedulerConfig, VllmConfig
11
from vllm.model_executor.models.interfaces import MultiModalEmbeddings
12
from vllm.model_executor.models.utils import extract_layer_index
13
from vllm.multimodal.cache import processor_only_cache_from_config
@@ -288,3 +288,28 @@ def bind_kv_cache(
288
for layer_name, kv_cache in kv_caches.items():
289
# NOTE: Use list because of v0 PP virtual engine.
290
forward_context[layer_name].kv_cache = [kv_cache]
291
+
292
+
293
+def is_residual_scattered_for_sp(vllm_config: VllmConfig,
294
+ num_input_tokens: int) -> bool:
295
+ """Check if the residual tensor is scattered for sequence parallelism.
296
+
297
+ The residual tensor is scattered across tensor parallel ranks when sequence
298
+ parallelism and tensor parallelism is enabled, and the number of
299
+ input tokens is one of the compilation sizes.
300
+ """
301
+ if not vllm_config.compilation_config.pass_config.\
302
+ enable_sequence_parallelism:
303
+ return False
304
+
305
+ tp = vllm_config.parallel_config.tensor_parallel_size
306
+
307
+ if tp == 1:
308
+ return False
309
+
310
+ # When sequence parallelism is enabled, we always pad num_input_tokens
311
+ # to be a multiple of tensor_parallel_size (tp) earlier.
312
+ assert num_input_tokens % tp == 0
313
+
314
+ # Currently, SP is only enabled for static size fx graphs.
315
+ return (num_input_tokens in vllm_config.compilation_config.compile_sizes)