@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / bd877162eb
Fork 0

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 10, 19:00 UTC

[BugFix] Support online dense model DP without overhead (#30739)

Signed-off-by: Nick Hill <nhill@redhat.com> Signed-off-by: njhill <nickhill123@gmail.com>

Nick Hill committed Jan 2, 2026 at 07:36 UTC bd877162ebd7a2eb36961a8e49824422dd35bcdc
20 files changed +344 -145
tests/test_config.py
+48 -5
@@ -205,8 +205,8 @@ def test_default_pooling_type(model_id, default_pooling_type, pooling_type):
205 )
206 def test_moe_model_detection(model_id, expected_is_moe_model):
207 model_config = ModelConfig(model_id)
208 - # Just check that is_moe_model field exists and is a boolean
209 - assert model_config.is_model_moe() == expected_is_moe_model
208 + # Just check that is_moe field exists and is a boolean
209 + assert model_config.is_moe == expected_is_moe_model
210
211
212 @pytest.mark.parametrize(
@@ -224,7 +224,7 @@ def test_moe_model_detection(model_id, expected_is_moe_model):
224 def test_is_quantized(model_id, quantized):
225 model_config = ModelConfig(model_id)
226 # Just check that quantized field exists and is a boolean
227 - assert model_config.is_quantized() == quantized
227 + assert model_config.is_quantized == quantized
228
229
230 @pytest.mark.skipif(
@@ -925,7 +925,7 @@ def test_vllm_config_callable_defaults():
925 model_config=quantized_model, optimization_level=OptimizationLevel.O2
926 )
927 enable_if_quantized = lambda cfg: (
928 - cfg.model_config is not None and cfg.model_config.is_quantized()
928 + cfg.model_config is not None and cfg.model_config.is_quantized
929 )
930 assert enable_if_quantized(config_quantized) is True
931 assert enable_if_quantized(config_no_model) is False
@@ -936,7 +936,7 @@ def test_vllm_config_callable_defaults():
936 model_config=moe_model, optimization_level=OptimizationLevel.O2
937 )
938 enable_if_sequential = lambda cfg: (
939 - cfg.model_config is not None and not cfg.model_config.is_model_moe()
939 + cfg.model_config is not None and not cfg.model_config.is_moe
940 )
941 assert enable_if_sequential(config_moe) is False
942 assert enable_if_sequential(config_quantized) is True
@@ -1050,3 +1050,46 @@ def test_scheduler_config_init():
1050 with pytest.raises(AttributeError):
1051 # InitVar does not become an attribute
1052 print(SchedulerConfig.default_factory().max_model_len)
1053 +
1054 +
1055 +@pytest.mark.parametrize(
1056 + (
1057 + "model_id",
1058 + "data_parallel_size",
1059 + "external_lb",
1060 + "expected_needs_coordinator",
1061 + ),
1062 + [
1063 + # Non-MoE model with DP=1 should not need coordinator
1064 + ("facebook/opt-125m", 1, False, False),
1065 + # Non-MoE model with DP>1 internal LB should need coordinator
1066 + ("facebook/opt-125m", 2, False, True),
1067 + # Non-MoE model with DP>1 external LB should not need coordinator
1068 + ("facebook/opt-125m", 2, True, False),
1069 + # MoE model with DP=1 should not need coordinator
1070 + ("mistralai/Mixtral-8x7B-Instruct-v0.1", 1, False, False),
1071 + # MoE model with DP>1 internal LB should need both coordinator
1072 + # and wave coordination
1073 + ("mistralai/Mixtral-8x7B-Instruct-v0.1", 2, False, True),
1074 + # MoE model with DP>1 external LB needs coordinator for wave coordination
1075 + # (wave coordination runs in coordinator process)
1076 + ("mistralai/Mixtral-8x7B-Instruct-v0.1", 2, True, True),
1077 + ],
1078 +)
1079 +def test_needs_dp_coordination(
1080 + model_id,
1081 + data_parallel_size,
1082 + external_lb,
1083 + expected_needs_coordinator,
1084 +):
1085 + """Test that DP coordinator and wave coordination are configured correctly."""
1086 + from vllm.config import ParallelConfig
1087 +
1088 + model_config = ModelConfig(model_id)
1089 + parallel_config = ParallelConfig(
1090 + data_parallel_size=data_parallel_size,
1091 + data_parallel_external_lb=external_lb,
1092 + )
1093 + vllm_config = VllmConfig(model_config=model_config, parallel_config=parallel_config)
1094 +
1095 + assert vllm_config.needs_dp_coordinator == expected_needs_coordinator
tests/v1/engine/test_engine_core_client.py
+1
@@ -133,6 +133,7 @@ def test_mp_client_uses_env_timeout(monkeypatch: pytest.MonkeyPatch):
133 parallel_config = SimpleNamespace(
134 data_parallel_size=1,
135 data_parallel_rank=0,
136 + data_parallel_index=0,
137 data_parallel_size_local=1,
138 data_parallel_rank_local=None,
139 data_parallel_hybrid_lb=False,
vllm/compilation/backends.py
+1 -1
@@ -630,7 +630,7 @@ class VllmBackend:
630 os.makedirs(cache_dir, exist_ok=True)
631 self.compilation_config.cache_dir = cache_dir
632 rank = vllm_config.parallel_config.rank
633 - dp_rank = vllm_config.parallel_config.data_parallel_rank
633 + dp_rank = vllm_config.parallel_config.data_parallel_index
634 local_cache_dir = os.path.join(cache_dir, f"rank_{rank}_{dp_rank}", self.prefix)
635 os.makedirs(local_cache_dir, exist_ok=True)
636 self.compilation_config.local_cache_dir = local_cache_dir
vllm/compilation/decorators.py
+1 -1
@@ -403,7 +403,7 @@ def _support_torch_compile(
403 )
404
405 rank = self.vllm_config.parallel_config.rank
406 - dp_rank = self.vllm_config.parallel_config.data_parallel_rank
406 + dp_rank = self.vllm_config.parallel_config.data_parallel_index
407 cache_dir = os.path.join(cache_dir, f"rank_{rank}_{dp_rank}")
408 aot_compilation_path = os.path.join(cache_dir, "model")
409 try:
vllm/config/model.py
+6 -7
@@ -642,7 +642,7 @@ class ModelConfig:
642 cls = "Transformers"
643 # If 'hf_config != hf_text_config' it's a nested config, i.e. multimodal
644 cls += "MultiModal" if self.hf_config != self.hf_text_config else ""
645 - cls += "MoE" if self.get_num_experts() > 1 else ""
645 + cls += "MoE" if self.is_moe else ""
646 # Check if the architecture we're wrapping has defaults
647 runner = None
648 task = None
@@ -1001,8 +1001,7 @@ class ModelConfig:
1001 self.enforce_eager = True
1002
1003 def _verify_with_expert_parallelism(self) -> None:
1004 - num_experts = self.get_num_experts()
1005 - if num_experts < 1:
1004 + if not self.is_moe:
1005 raise ValueError(
1006 "Number of experts in the model must be greater than 0 "
1007 "when expert parallelism is enabled."
@@ -1797,11 +1796,11 @@ class ModelConfig:
1796 logger.debug("Generative models support prefix caching.")
1797 return True
1798
1800 - def is_model_moe(
1801 - self,
1802 - ) -> bool:
1803 - return self.get_num_experts() > 1
1799 + @property
1800 + def is_moe(self) -> bool:
1801 + return self.get_num_experts() > 0
1802
1803 + @property
1804 def is_quantized(self) -> bool:
1805 return getattr(self.hf_config, "quantization_config", None) is not None
1806
vllm/config/parallel.py
+15
@@ -119,6 +119,8 @@ class ParallelConfig:
119 between local data parallel ranks, but an external LB balances
120 between vLLM nodes/replicas. Set explicitly in conjunction with
121 --data-parallel-start-rank."""
122 + is_moe_model: bool | None = None
123 + """Whether the deployed model is MoE (if known)."""
124 enable_expert_parallel: bool = False
125 """Use expert parallelism instead of tensor parallelism for MoE layers."""
126 enable_eplb: bool = False
@@ -255,6 +257,10 @@ class ParallelConfig:
257 Block_size should be divisible by cp_kv_cache_interleave_size.
258 """
259
260 + data_parallel_index: int = Field(init=False)
261 + """Equal to the data parallel rank but not used for torch process groups
262 + and not overridden for dense models."""
263 +
264 _api_process_count: int = Field(default=1, gt=0)
265 """
266 The number of API processes initialized.
@@ -466,6 +472,7 @@ class ParallelConfig:
472 "data_parallel_rank",
473 "data_parallel_rank_local",
474 "data_parallel_size_local",
475 + "data_parallel_index",
476 "data_parallel_backend",
477 "data_parallel_external_lb",
478 "data_parallel_hybrid_lb",
@@ -546,6 +553,14 @@ class ParallelConfig:
553 self.data_parallel_master_ip = envs.VLLM_DP_MASTER_IP
554 self.data_parallel_master_port = envs.VLLM_DP_MASTER_PORT
555
556 + if self.data_parallel_size > 1 and self.is_moe_model is False:
557 + raise ValueError(
558 + "Offline data parallel mode is not supported/useful"
559 + " for dense models."
560 + )
561 +
562 + self.data_parallel_index = self.data_parallel_rank
563 +
564 if self.distributed_executor_backend == "external_launcher":
565 os.environ["VLLM_ENABLE_V1_MULTIPROCESSING"] = "0"
566 logger.info("Disabling V1 multiprocessing for external launcher.")
vllm/config/vllm.py
+33 -8
@@ -343,6 +343,29 @@ class VllmConfig:
343 # i.e., batch_size <= self.compilation_config.max_cudagraph_capture_size
344 return self.compilation_config.bs_to_padded_graph_size[batch_size]
345
346 + @property
347 + def needs_dp_coordinator(self) -> bool:
348 + """
349 + Determine if the DPCoordinator process is needed.
350 +
351 + The DPCoordinator is needed in two cases:
352 + 1. For MoE models with DP > 1: to handle wave coordination
353 + (even in external LB mode, since wave coordination runs in the coordinator)
354 + 2. For non-MoE models in internal/hybrid LB mode: to collect and publish
355 + queue stats for load balancing across DP ranks
356 +
357 + Returns:
358 + True if DPCoordinator process is needed, False otherwise.
359 + """
360 +
361 + # For non-MoE models, only need coordinator in internal/hybrid LB mode
362 + # (for stats collection).
363 + return self.parallel_config.data_parallel_size > 1 and (
364 + self.model_config is None
365 + or self.model_config.is_moe
366 + or not self.parallel_config.data_parallel_external_lb
367 + )
368 +
369 def enable_trace_function_call_for_thread(self) -> None:
370 """
371 Set up function tracing for the current thread,
@@ -522,6 +545,8 @@ class VllmConfig:
545 self.model_config.verify_with_parallel_config(self.parallel_config)
546 self.model_config.verify_dual_chunk_attention_config(self.load_config)
547
548 + self.parallel_config.is_moe_model = self.model_config.is_moe
549 +
550 self.cache_config.verify_with_parallel_config(self.parallel_config)
551
552 if self.lora_config is not None:
@@ -827,9 +852,14 @@ class VllmConfig:
852 )
853
854 # Do this after all the updates to compilation_config.mode
855 + effective_dp_size = (
856 + self.parallel_config.data_parallel_size
857 + if self.model_config is None or self.model_config.is_moe
858 + else 1
859 + )
860 self.compilation_config.set_splitting_ops_for_v1(
861 all2all_backend=self.parallel_config.all2all_backend,
832 - data_parallel_size=self.parallel_config.data_parallel_size,
862 + data_parallel_size=effective_dp_size,
863 )
864
865 if self.compilation_config.pass_config.enable_sp:
@@ -1297,13 +1327,8 @@ class VllmConfig:
1327 if self.compilation_config.debug_dump_path is None:
1328 return None
1329 tp_rank = self.parallel_config.rank
1300 - dp_rank = self.parallel_config.data_parallel_rank
1301 - data_parallel_size = self.parallel_config.data_parallel_size
1302 - append_path = (
1303 - f"rank_{tp_rank}"
1304 - if data_parallel_size == 1
1305 - else f"rank_{tp_rank}_dp_{dp_rank}"
1306 - )
1330 + dp_rank = self.parallel_config.data_parallel_index
1331 + append_path = f"rank_{tp_rank}_dp_{dp_rank}"
1332 path = self.compilation_config.debug_dump_path / append_path
1333 return path
1334
vllm/distributed/kv_transfer/kv_connector/v1/mooncake_connector.py
+1 -1
@@ -915,6 +915,6 @@ def get_mooncake_side_channel_port(vllm_config: VllmConfig) -> int:
915 # This logic is now centralized
916 return (
917 envs.VLLM_MOONCAKE_BOOTSTRAP_PORT
918 - + vllm_config.parallel_config.data_parallel_rank
918 + + vllm_config.parallel_config.data_parallel_index
919 * vllm_config.parallel_config.tensor_parallel_size
920 )
vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.py
+1 -1
@@ -471,7 +471,7 @@ class NixlConnectorScheduler:
471 self.side_channel_host = envs.VLLM_NIXL_SIDE_CHANNEL_HOST
472 self.side_channel_port = (
473 envs.VLLM_NIXL_SIDE_CHANNEL_PORT
474 - + vllm_config.parallel_config.data_parallel_rank
474 + + vllm_config.parallel_config.data_parallel_index
475 )
476 assert vllm_config.kv_transfer_config is not None
477 if current_platform.device_type == "cpu":
vllm/distributed/parallel_state.py
+22 -15
@@ -1115,7 +1115,11 @@ _EP: GroupCoordinator | None = None
1115
1116
1117 def get_ep_group() -> GroupCoordinator:
1118 - assert _EP is not None, "expert parallel group is not initialized"
1118 + assert _EP is not None, (
1119 + "expert parallel group is not initialized. "
1120 + "EP group is only created for MoE models with num_experts > 0. "
1121 + "This function should only be called for MoE models."
1122 + )
1123 return _EP
1124
1125
@@ -1400,20 +1404,23 @@ def initialize_model_parallel(
1404
1405 global _EP
1406 assert _EP is None, "expert parallel group is already initialized"
1403 - group_ranks = (
1404 - all_ranks.transpose(1, 2)
1405 - .reshape(
1406 - -1,
1407 - data_parallel_size
1408 - * prefill_context_model_parallel_size
1409 - * tensor_model_parallel_size,
1407 + # Don't create EP group for dense models.
1408 + if config is None or config.model_config is None or config.model_config.is_moe:
1409 + group_ranks = (
1410 + all_ranks.transpose(1, 2)
1411 + .reshape(
1412 + -1,
1413 + data_parallel_size
1414 + * prefill_context_model_parallel_size
1415 + * tensor_model_parallel_size,
1416 + )
1417 + .unbind(0)
1418 )
1411 - .unbind(0)
1412 - )
1413 - group_ranks = [x.tolist() for x in group_ranks]
1414 - _EP = init_model_parallel_group(
1415 - group_ranks, get_world_group().local_rank, backend, group_name="ep"
1416 - )
1419 + group_ranks = [x.tolist() for x in group_ranks]
1420 + _EP = init_model_parallel_group(
1421 + group_ranks, get_world_group().local_rank, backend, group_name="ep"
1422 + )
1423 + # If no EP group needed, _EP remains None
1424
1425 logger.info_once(
1426 "rank %s in world size %s is assigned as "
@@ -1425,7 +1432,7 @@ def initialize_model_parallel(
1432 _PP.rank_in_group,
1433 _PCP.rank_in_group,
1434 _TP.rank_in_group,
1428 - _EP.rank_in_group,
1435 + _EP.rank_in_group if _EP is not None else "N/A",
1436 )
1437
1438
vllm/engine/arg_utils.py
+1
@@ -1575,6 +1575,7 @@ class EngineArgs:
1575 data_parallel_rpc_port=data_parallel_rpc_port,
1576 data_parallel_backend=self.data_parallel_backend,
1577 data_parallel_hybrid_lb=self.data_parallel_hybrid_lb,
1578 + is_moe_model=model_config.is_moe,
1579 enable_expert_parallel=self.enable_expert_parallel,
1580 all2all_backend=self.all2all_backend,
1581 enable_dbo=self.enable_dbo,
vllm/forward_context.py
+1
@@ -102,6 +102,7 @@ class DPMetadata:
102 ) -> "DPMetadata":
103 assert num_tokens_across_dp_cpu is not None
104 assert parallel_config.data_parallel_size > 1
105 + assert parallel_config.is_moe_model is not False
106 dp_rank = parallel_config.data_parallel_rank
107 batchsize = num_tokens
108
vllm/v1/core/sched/scheduler.py
+1 -1
@@ -127,7 +127,7 @@ class Scheduler(SchedulerInterface):
127
128 self.kv_event_publisher = EventPublisherFactory.create(
129 self.kv_events_config,
130 - self.parallel_config.data_parallel_rank,
130 + self.parallel_config.data_parallel_index,
131 )
132 self.ec_connector = None
133 if self.vllm_config.ec_transfer_config is not None:
vllm/v1/engine/coordinator.py
+62 -43
@@ -55,7 +55,9 @@ class DPCoordinator:
55 request wave / running state changes.
56 """
57
58 - def __init__(self, parallel_config: ParallelConfig):
58 + def __init__(
59 + self, parallel_config: ParallelConfig, enable_wave_coordination: bool = True
60 + ):
61 dp_size = parallel_config.data_parallel_size
62 assert dp_size > 1, "Coordinator only used for data parallel"
63
@@ -83,6 +85,7 @@ class DPCoordinator:
85 "front_publish_address": front_publish_address,
86 "back_output_address": back_output_address,
87 "back_publish_address": back_publish_address,
88 + "enable_wave_coordination": enable_wave_coordination,
89 },
90 daemon=True,
91 )
@@ -110,13 +113,19 @@ class EngineState:
113
114
115 class DPCoordinatorProc:
113 - def __init__(self, engine_count: int, min_stats_update_interval_ms: int = 100):
116 + def __init__(
117 + self,
118 + engine_count: int,
119 + min_stats_update_interval_ms: int = 100,
120 + enable_wave_coordination: bool = True,
121 + ):
122 set_process_title("DPCoordinator")
123 self.ctx = zmq.Context()
124
125 self.engines = [EngineState() for _ in range(engine_count)]
126
127 self.stats_update_interval_ms = min_stats_update_interval_ms
128 + self.enable_wave_coordination = enable_wave_coordination
129
130 @staticmethod
131 def run_coordinator(
@@ -125,10 +134,12 @@ class DPCoordinatorProc:
134 back_output_address: str,
135 back_publish_address: str,
136 min_stats_update_interval_ms: int = 100,
137 + enable_wave_coordination: bool = True,
138 ):
139 coordinator = DPCoordinatorProc(
140 engine_count=engine_count,
141 min_stats_update_interval_ms=min_stats_update_interval_ms,
142 + enable_wave_coordination=enable_wave_coordination,
143 )
144 try:
145 coordinator.process_input_socket(
@@ -265,22 +276,25 @@ class DPCoordinatorProc:
276 )
277 continue # Skip normal engine notification processing
278
268 - # We received a message on the front-end XPUB socket,
269 - # from an API server sending a new request while the
270 - # engines are paused, so that we can wake the other
271 - # engines.
272 - engine_to_exclude, wave = decoded
273 - if not engines_running:
274 - if wave < current_wave:
275 - # If the wave number is stale, ensure the message
276 - # is handled by all the engines.
277 - engine_to_exclude = None
278 -
279 - engines_running = True
280 - wave_state_changed = True
281 - self._send_start_wave(
282 - publish_back, current_wave, engine_to_exclude
283 - )
279 + # Wave coordination: handle new-request messages from front-end.
280 + # Only process these when wave coordination is enabled
281 + if self.enable_wave_coordination:
282 + # We received a message on the front-end XPUB socket,
283 + # from an API server sending a new request while the
284 + # engines are paused, so that we can wake the other
285 + # engines.
286 + engine_to_exclude, wave = decoded
287 + if not engines_running:
288 + if wave < current_wave:
289 + # If the wave number is stale, ensure the message
290 + # is handled by all the engines.
291 + engine_to_exclude = None
292 +
293 + engines_running = True
294 + wave_state_changed = True
295 + self._send_start_wave(
296 + publish_back, current_wave, engine_to_exclude
297 + )
298
299 if output_back in events:
300 # We received a message from one of the engines.
@@ -325,34 +339,39 @@ class DPCoordinatorProc:
339 stats[1] = scheduler_stats.num_running_reqs
340 stats_changed = True
341
328 - if (wave := outputs.wave_complete) is not None:
329 - # 2. Notification from rank 0 engine that we've
330 - # moved into the global paused state
331 - # (engines_running==False).
332 - if current_wave <= wave:
333 - new_wave = wave + 1
342 + # Wave coordination: handle wave completion and start notifications
343 + # Only process these when wave coordination is enabled
344 + if self.enable_wave_coordination:
345 + if (wave := outputs.wave_complete) is not None:
346 + # 2. Notification from rank 0 engine that we've
347 + # moved into the global paused state
348 + # (engines_running==False).
349 + if current_wave <= wave:
350 + new_wave = wave + 1
351 + logger.debug(
352 + "Moving DP wave from %d to %d.",
353 + current_wave,
354 + new_wave,
355 + )
356 + current_wave = new_wave
357 + engines_running = False
358 + wave_state_changed = True
359 + elif (wave := outputs.start_wave) is not None and (
360 + wave > current_wave
361 + or (wave == current_wave and not engines_running)
362 + ):
363 + # 3. The engine received request for a non-current wave
364 + # so we must ensure that other engines progress to the
365 + # next wave (race condition handling).
366 logger.debug(
335 - "Moving DP wave from %d to %d.", current_wave, new_wave
367 + "Starting wave %d after notification of "
368 + "stale wave request from engine.",
369 + wave,
370 )
337 - current_wave = new_wave
338 - engines_running = False
371 + current_wave = wave
372 + engines_running = True
373 wave_state_changed = True
340 - elif (wave := outputs.start_wave) is not None and (
341 - wave > current_wave
342 - or (wave == current_wave and not engines_running)
343 - ):
344 - # 3. The engine received request for a non-current wave
345 - # so we must ensure that other engines progress to the
346 - # next wave (race condition handling).
347 - logger.debug(
348 - "Starting wave %d after notification of "
349 - "stale wave request from engine.",
350 - wave,
351 - )
352 - current_wave = wave
353 - engines_running = True
354 - wave_state_changed = True
355 - self._send_start_wave(publish_back, wave, eng_index)
374 + self._send_start_wave(publish_back, wave, eng_index)
375
376 if wave_state_changed:
377 message = (None, current_wave, engines_running)
vllm/v1/engine/core.py
+94 -23
@@ -84,6 +84,7 @@ class EngineCore:
84 executor_class: type[Executor],
85 log_stats: bool,
86 executor_fail_callback: Callable | None = None,
87 + include_finished_set: bool = False,
88 ):
89 # plugins need to be loaded at the engine/scheduler level too
90 from vllm.plugins import load_general_plugins
@@ -91,7 +92,7 @@ class EngineCore:
92 load_general_plugins()
93
94 self.vllm_config = vllm_config
94 - if vllm_config.parallel_config.data_parallel_rank == 0:
95 + if not vllm_config.parallel_config.data_parallel_rank_local:
96 logger.info(
97 "Initializing a V1 LLM engine (v%s) with config: %s",
98 VLLM_VERSION,
@@ -138,7 +139,7 @@ class EngineCore:
139 vllm_config=vllm_config,
140 kv_cache_config=kv_cache_config,
141 structured_output_manager=self.structured_output_manager,
141 - include_finished_set=vllm_config.parallel_config.data_parallel_size > 1,
142 + include_finished_set=include_finished_set,
143 log_stats=self.log_stats,
144 block_size=scheduler_block_size,
145 )
@@ -605,6 +606,7 @@ class EngineCoreProc(EngineCore):
606 executor_class: type[Executor],
607 log_stats: bool,
608 client_handshake_address: str | None = None,
609 + *,
610 engine_index: int = 0,
611 ):
612 self.input_queue = queue.Queue[tuple[EngineCoreRequestType, Any]]()
@@ -636,17 +638,22 @@ class EngineCoreProc(EngineCore):
638 self.has_coordinator,
639 self.frontend_stats_publish_address,
640 )
639 - # Only publish request queue stats to coordinator for "internal"
640 - # and "hybrid" LB modes .
641 - self.publish_dp_lb_stats = (
641 + internal_dp_balancing = (
642 self.has_coordinator
643 and not vllm_config.parallel_config.data_parallel_external_lb
644 )
645 + # Only publish request queue stats to coordinator for "internal"
646 + # and "hybrid" LB modes.
647 + self.publish_dp_lb_stats = internal_dp_balancing
648
649 self._init_data_parallel(vllm_config)
650
651 super().__init__(
649 - vllm_config, executor_class, log_stats, executor_fail_callback
652 + vllm_config,
653 + executor_class,
654 + log_stats,
655 + executor_fail_callback,
656 + internal_dp_balancing,
657 )
658
659 # Background Threads and Queues for IO. These enable us to
@@ -854,18 +861,29 @@ class EngineCoreProc(EngineCore):
861
862 engine_core: EngineCoreProc | None = None
863 try:
857 - parallel_config: ParallelConfig = kwargs["vllm_config"].parallel_config
858 - if parallel_config.data_parallel_size > 1 or dp_rank > 0:
864 + vllm_config: VllmConfig = kwargs["vllm_config"]
865 + parallel_config: ParallelConfig = vllm_config.parallel_config
866 + data_parallel = parallel_config.data_parallel_size > 1 or dp_rank > 0
867 + if data_parallel:
868 + parallel_config.data_parallel_rank_local = local_dp_rank
869 set_process_title("EngineCore", f"DP{dp_rank}")
860 - decorate_logs()
870 + else:
871 + set_process_title("EngineCore")
872 + decorate_logs()
873 +
874 + parallel_config.data_parallel_index = dp_rank
875 + if data_parallel and vllm_config.model_config.is_moe:
876 # Set data parallel rank for this engine process.
877 parallel_config.data_parallel_rank = dp_rank
863 - parallel_config.data_parallel_rank_local = local_dp_rank
878 engine_core = DPEngineCoreProc(*args, **kwargs)
879 else:
866 - set_process_title("EngineCore")
867 - decorate_logs()
868 - engine_core = EngineCoreProc(*args, **kwargs)
880 + # Non-MoE DP ranks are completely independent, so treat like DP=1.
881 + # Note that parallel_config.data_parallel_index will still reflect
882 + # the original DP rank.
883 + parallel_config.data_parallel_size = 1
884 + parallel_config.data_parallel_size_local = 1
885 + parallel_config.data_parallel_rank = 0
886 + engine_core = EngineCoreProc(*args, engine_index=dp_rank, **kwargs)
887
888 engine_core.run_busy_loop()
889
@@ -1195,6 +1213,10 @@ class DPEngineCoreProc(EngineCoreProc):
1213 log_stats: bool,
1214 client_handshake_address: str | None = None,
1215 ):
1216 + assert vllm_config.model_config.is_moe, (
1217 + "DPEngineCoreProc should only be used for MoE models"
1218 + )
1219 +
1220 # Counts forward-passes of the model so that we can synchronize
1221 # finished with DP peers every N steps.
1222 self.step_counter = 0
@@ -1210,7 +1232,7 @@ class DPEngineCoreProc(EngineCoreProc):
1232 executor_class,
1233 log_stats,
1234 client_handshake_address,
1213 - dp_rank,
1235 + engine_index=dp_rank,
1236 )
1237
1238 def _init_data_parallel(self, vllm_config: VllmConfig):
@@ -1391,7 +1413,7 @@ class DPEngineCoreProc(EngineCoreProc):
1413 )
1414
1415
1394 -class DPEngineCoreActor(DPEngineCoreProc):
1416 +class EngineCoreActorMixin:
1417 """
1418 Ray actor for running EngineCore in a data parallel context
1419 """
@@ -1399,15 +1421,12 @@ class DPEngineCoreActor(DPEngineCoreProc):
1421 def __init__(
1422 self,
1423 vllm_config: VllmConfig,
1402 - local_client: bool,
1424 addresses: EngineZmqAddresses,
1404 - executor_class: type[Executor],
1405 - log_stats: bool,
1425 dp_rank: int = 0,
1426 local_dp_rank: int = 0,
1427 ):
1428 self.addresses = addresses
1410 - vllm_config.parallel_config.data_parallel_rank = dp_rank
1429 + vllm_config.parallel_config.data_parallel_index = dp_rank
1430 vllm_config.parallel_config.data_parallel_rank_local = local_dp_rank
1431
1432 # Set CUDA_VISIBLE_DEVICES as early as possible in actor life cycle
@@ -1429,8 +1448,6 @@ class DPEngineCoreActor(DPEngineCoreProc):
1448 # of ray.
1449 self._set_visible_devices(vllm_config, local_dp_rank)
1450
1432 - super().__init__(vllm_config, local_client, "", executor_class, log_stats)
1433 -
1451 def _set_visible_devices(self, vllm_config: VllmConfig, local_dp_rank: int):
1452 from vllm.platforms import current_platform
1453
@@ -1491,7 +1508,7 @@ class DPEngineCoreActor(DPEngineCoreProc):
1508 Run the engine core busy loop.
1509 """
1510 try:
1494 - self.run_busy_loop()
1511 + self.run_busy_loop() # type: ignore[attr-defined]
1512 except SystemExit:
1513 logger.debug("EngineCore exiting.")
1514 raise
@@ -1499,4 +1516,58 @@ class DPEngineCoreActor(DPEngineCoreProc):
1516 logger.exception("EngineCore encountered a fatal error.")
1517 raise
1518 finally:
1502 - self.shutdown()
1519 + self.shutdown() # type: ignore[attr-defined]
1520 +
1521 +
1522 +class DPMoEEngineCoreActor(EngineCoreActorMixin, DPEngineCoreProc):
1523 + """Used for MoE model data parallel cases."""
1524 +
1525 + def __init__(
1526 + self,
1527 + vllm_config: VllmConfig,
1528 + local_client: bool,
1529 + addresses: EngineZmqAddresses,
1530 + executor_class: type[Executor],
1531 + log_stats: bool,
1532 + dp_rank: int = 0,
1533 + local_dp_rank: int = 0,
1534 + ):
1535 + vllm_config.parallel_config.data_parallel_rank = dp_rank
1536 +
1537 + EngineCoreActorMixin.__init__(
1538 + self, vllm_config, addresses, dp_rank, local_dp_rank
1539 + )
1540 + DPEngineCoreProc.__init__(
1541 + self, vllm_config, local_client, "", executor_class, log_stats
1542 + )
1543 +
1544 +
1545 +class EngineCoreActor(EngineCoreActorMixin, EngineCoreProc):
1546 + """Used for non-MoE and/or non-DP cases."""
1547 +
1548 + def __init__(
1549 + self,
1550 + vllm_config: VllmConfig,
1551 + local_client: bool,
1552 + addresses: EngineZmqAddresses,
1553 + executor_class: type[Executor],
1554 + log_stats: bool,
1555 + dp_rank: int = 0,
1556 + local_dp_rank: int = 0,
1557 + ):
1558 + vllm_config.parallel_config.data_parallel_size = 1
1559 + vllm_config.parallel_config.data_parallel_size_local = 1
1560 + vllm_config.parallel_config.data_parallel_rank = 0
1561 +
1562 + EngineCoreActorMixin.__init__(
1563 + self, vllm_config, addresses, dp_rank, local_dp_rank
1564 + )
1565 + EngineCoreProc.__init__(
1566 + self,
1567 + vllm_config,
1568 + local_client,
1569 + "",
1570 + executor_class,
1571 + log_stats,
1572 + engine_index=dp_rank,
1573 + )
vllm/v1/engine/core_client.py
+1 -1
@@ -502,7 +502,7 @@ class MPClient(EngineCoreClient):
502
503 parallel_config = vllm_config.parallel_config
504 dp_size = parallel_config.data_parallel_size
505 - dp_rank = parallel_config.data_parallel_rank
505 + dp_rank = parallel_config.data_parallel_index
506 dp_local_size = parallel_config.data_parallel_size_local
507 offline_mode = parallel_config.data_parallel_rank_local is not None
508 # Client manages local+remote EngineCores in pure internal LB case.
vllm/v1/engine/llm_engine.py
+2 -1
@@ -65,8 +65,9 @@ class LLMEngine:
65
66 self.log_stats = log_stats
67
68 - executor_backend = self.vllm_config.parallel_config.distributed_executor_backend
68 parallel_config = vllm_config.parallel_config
69 + executor_backend = parallel_config.distributed_executor_backend
70 +
71 self.external_launcher_dp = (
72 parallel_config.data_parallel_size > 1
73 and executor_backend == "external_launcher"
vllm/v1/engine/utils.py
+37 -19
@@ -75,7 +75,6 @@ class EngineHandshakeMetadata:
75
76 addresses: EngineZmqAddresses
77 parallel_config: dict[str, int | str | list[int]]
78 - parallel_config_hash: str | None = None
78
79
80 class CoreEngineProcManager:
@@ -249,12 +248,19 @@ class CoreEngineActorManager:
248 from ray.runtime_env import RuntimeEnv
249 from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
250
252 - from vllm.v1.engine.core import DPEngineCoreActor
251 + from vllm.v1.engine.core import DPMoEEngineCoreActor, EngineCoreActor
252 +
253 + dp_size = vllm_config.parallel_config.data_parallel_size
254 + actor_class = (
255 + DPMoEEngineCoreActor
256 + if dp_size > 1 and vllm_config.model_config.is_moe
257 + else EngineCoreActor
258 + )
259
260 self.local_engine_actors: list[ray.ActorHandle] = []
261 self.remote_engine_actors: list[ray.ActorHandle] = []
262
257 - env_vars_list = get_env_vars_to_copy(destination="DPEngineCoreActor")
263 + env_vars_list = get_env_vars_to_copy(destination=actor_class.__name__)
264 self.env_vars_dict = {
265 name: os.environ[name] for name in env_vars_list if name in os.environ
266 }
@@ -263,7 +269,6 @@ class CoreEngineActorManager:
269 self.addresses = addresses
270 self.executor_class = executor_class
271 self.log_stats = log_stats
266 - dp_size = vllm_config.parallel_config.data_parallel_size
272 local_engine_count = vllm_config.parallel_config.data_parallel_size_local
273 world_size = vllm_config.parallel_config.world_size
274
@@ -314,7 +319,7 @@ class CoreEngineActorManager:
319 runtime_env = RuntimeEnv(env_vars=actor_env_vars)
320
321 actor = (
317 - ray.remote(DPEngineCoreActor)
322 + ray.remote(actor_class)
323 .options(
324 scheduling_strategy=PlacementGroupSchedulingStrategy(
325 placement_group=pg,
@@ -624,7 +629,13 @@ class CoreEngineActorManager:
629 from ray.runtime_env import RuntimeEnv
630 from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
631
627 - from vllm.v1.engine.core import DPEngineCoreActor
632 + from vllm.v1.engine.core import DPMoEEngineCoreActor, EngineCoreActor
633 +
634 + actor_class = (
635 + DPMoEEngineCoreActor
636 + if cur_vllm_config.model_config.is_moe
637 + else EngineCoreActor
638 + )
639
640 cur_data_parallel_size = len(self.local_engine_actors) + len(
641 self.remote_engine_actors
@@ -667,7 +678,7 @@ class CoreEngineActorManager:
678 )
679
680 actor = (
670 - ray.remote(DPEngineCoreActor)
681 + ray.remote(actor_class)
682 .options(
683 scheduling_strategy=PlacementGroupSchedulingStrategy(
684 placement_group=pg,
@@ -804,12 +815,19 @@ def launch_core_engines(
815 ],
816 )
817
807 - # Run the DP Coordinator process with rank 0 when in
808 - # online DP mode.
809 - run_coordinator = dp_size > 1 and not offline_mode and dp_rank == 0
818 + # Run the DP Coordinator process with rank 0 when in online DP mode.
819 + # The coordinator is needed for:
820 + # 1. Internal/hybrid LB: collecting and publishing queue stats for load balancing
821 + # 2. MoE models: wave coordination in addition to stats
822 + run_coordinator = (
823 + vllm_config.needs_dp_coordinator and not offline_mode and dp_rank == 0
824 + )
825
826 if run_coordinator:
812 - coordinator = DPCoordinator(parallel_config)
827 + coordinator = DPCoordinator(
828 + parallel_config,
829 + enable_wave_coordination=vllm_config.model_config.is_moe,
830 + )
831
832 addresses.coordinator_input, addresses.coordinator_output = (
833 coordinator.get_engine_socket_addresses()
@@ -905,6 +923,7 @@ def launch_core_engines(
923 addresses,
924 engines_to_handshake,
925 parallel_config,
926 + dp_size > 1 and vllm_config.model_config.is_moe,
927 vllm_config.cache_config,
928 local_engine_manager,
929 coordinator.proc if coordinator else None,
@@ -916,6 +935,7 @@ def wait_for_engine_startup(
935 addresses: EngineZmqAddresses,
936 core_engines: list[CoreEngine],
937 parallel_config: ParallelConfig,
938 + coordinated_dp: bool,
939 cache_config: CacheConfig,
940 proc_manager: CoreEngineProcManager | None,
941 coord_process: Process | None,
@@ -997,8 +1017,7 @@ def wait_for_engine_startup(
1017 )
1018
1019 if status == "HELLO" and engine.state == CoreEngineState.NEW:
1000 - # Send init message with DP config info and config hash.
1001 - # The config hash ensures all DP workers have compatible configs.
1020 + # Send init message with DP config info.
1021 init_message = msgspec.msgpack.encode(
1022 EngineHandshakeMetadata(
1023 addresses=addresses,
@@ -1010,10 +1029,9 @@ def wait_for_engine_startup(
1029 "_data_parallel_master_port_list",
1030 "data_parallel_size",
1031 )
1013 - },
1014 - parallel_config_hash=parallel_config.compute_hash()
1015 - if parallel_config.data_parallel_size > 1
1016 - else None,
1032 + }
1033 + if coordinated_dp
1034 + else {},
1035 )
1036 )
1037 handshake_socket.send_multipart((eng_identity, init_message), copy=False)
@@ -1034,8 +1052,8 @@ def wait_for_engine_startup(
1052 if addresses.frontend_stats_publish_address is None:
1053 addresses.frontend_stats_publish_address = msg.get("dp_stats_address")
1054
1037 - # Validate config hash consistency across DP workers
1038 - if parallel_config.data_parallel_size > 1:
1055 + # Validate config hash consistency across DP workers for MoE models.
1056 + if coordinated_dp:
1057 worker_config_hash = msg.get("parallel_config_hash")
1058 expected_hash = parallel_config.compute_hash()
1059 if worker_config_hash != expected_hash:
vllm/v1/worker/gpu/model_runner.py
+9 -9
@@ -98,9 +98,6 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
98 self.max_num_reqs = self.scheduler_config.max_num_seqs
99 self.inputs_embeds_size = self.model_config.get_inputs_embeds_size()
100
101 - self.dp_size = self.parallel_config.data_parallel_size
102 - self.dp_rank = self.parallel_config.data_parallel_rank
103 -
101 self.use_async_scheduling = self.scheduler_config.async_scheduling
102 self.output_copy_stream = torch.cuda.Stream(self.device)
103 self.output_copy_event = torch.cuda.Event()
@@ -268,7 +265,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
265 if not skip_attn:
266 self.prepare_dummy_attn_metadata(input_batch)
267
271 - num_tokens_across_dp = make_num_tokens_across_dp(self.dp_size, num_tokens)
268 + dp_size = self.parallel_config.data_parallel_size
269 + num_tokens_across_dp = make_num_tokens_across_dp(dp_size, num_tokens)
270 num_sampled_tokens = np.ones(input_batch.num_reqs, dtype=np.int32)
271 with (
272 self.maybe_dummy_run_with_lora(
@@ -312,7 +310,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
310 self._dummy_sampler_run(sample_hidden_states)
311 if self.do_spec_decode:
312 num_tokens_across_dp = make_num_tokens_across_dp(
315 - self.dp_size, self.max_num_tokens
313 + self.parallel_config.data_parallel_size, self.max_num_tokens
314 )
315 self.speculator.run_model(
316 self.max_num_tokens,
@@ -807,7 +805,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
805 scheduler_output: SchedulerOutput,
806 ) -> tuple[CUDAGraphMode, int, torch.Tensor | None]:
807 total_num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
810 - if self.dp_size == 1:
808 + dp_size = self.parallel_config.data_parallel_size
809 + if dp_size == 1:
810 # No DP. Only consider CUDA graphs.
811 if total_num_scheduled_tokens == 0:
812 # Special case: no tokens to run.
@@ -835,11 +834,12 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
834 cudagraph_size_before_dp = -1
835
836 assert cudagraph_size_before_dp is not None
837 + dp_rank = self.parallel_config.data_parallel_rank
838 num_tokens_across_dp, cudagraph_size_across_dp = get_batch_metadata_across_dp(
839 total_num_scheduled_tokens,
840 cudagraph_size_before_dp,
841 - self.dp_size,
842 - self.dp_rank,
841 + dp_size,
842 + dp_rank,
843 )
844 if all(cudagraph_size_across_dp >= 0):
845 # If all ranks can use CUDA graph, pad to the maximum number of tokens
@@ -850,7 +850,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
850 # If any of the ranks cannot use CUDA graph, use eager mode for all ranks.
851 # No padding is needed except for ranks that have no tokens to run.
852 num_tokens_across_dp = torch.clamp(num_tokens_across_dp, min=1)
853 - num_tokens_after_padding = num_tokens_across_dp[self.dp_rank]
853 + num_tokens_after_padding = num_tokens_across_dp[dp_rank]
854 cudagraph_mode = CUDAGraphMode.NONE
855 return cudagraph_mode, num_tokens_after_padding, num_tokens_across_dp
856
vllm/v1/worker/gpu_worker.py
+7 -9
@@ -179,22 +179,20 @@ class Worker(WorkerBase):
179 self.cache_config.num_cpu_blocks = num_cpu_blocks
180
181 def init_device(self):
182 - device = self.device_config.device
183 - if isinstance(device, torch.device) and device.type == "cuda":
182 + if self.device_config.device_type == "cuda":
183 # This env var set by Ray causes exceptions with graph building.
184 os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
185 + parallel_config = self.parallel_config
186 if (
187 - self.parallel_config.data_parallel_size > 1
188 - and self.parallel_config.data_parallel_size_local > 0
189 - and self.parallel_config.distributed_executor_backend
190 - not in ["ray", "external_launcher"]
191 - and self.vllm_config.parallel_config.data_parallel_backend != "ray"
192 - and self.vllm_config.parallel_config.nnodes_within_dp == 1
187 + parallel_config.distributed_executor_backend
188 + not in ("ray", "external_launcher")
189 + and parallel_config.data_parallel_backend != "ray"
190 + and parallel_config.nnodes_within_dp == 1
191 ):
192 # Use local DP rank if available, otherwise use global DP rank.
193 dp_local_rank = self.parallel_config.data_parallel_rank_local
194 if dp_local_rank is None:
197 - dp_local_rank = self.parallel_config.data_parallel_rank
195 + dp_local_rank = self.parallel_config.data_parallel_index
196
197 tp_pp_world_size = (
198 self.parallel_config.pipeline_parallel_size