[Elastic EP] Async preparation (#47288)

Signed-off-by: Itay Alroy <ialroy@nvidia.com>

Itay Alroy committed Jul 28, 2026 at 15:19 UTC 35efdf6b34f1ef76c23c8980f6e8a3b73cab50f1
21 files changed +701 -677
.buildkite/test_areas/expert_parallelism.yaml
+1
@@ -52,4 +52,5 @@ steps:
52 - vllm/compilation/
53 - tests/distributed/
54 commands:
55 + - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh
56 - pytest -v -s distributed/test_elastic_ep.py
tests/distributed/test_elastic_ep.py
+135 -31
@@ -3,7 +3,9 @@
3
4 import os
5 import subprocess
6 +import threading
7 import time
8 +from concurrent.futures import ThreadPoolExecutor
9
10 import pytest
11 import requests
@@ -40,6 +42,106 @@ def _send_scale_command(server: RemoteOpenAIServer, new_dp_size: int) -> bool:
42 return False
43
44
45 +def _traffic_loop(
46 + server: RemoteOpenAIServer,
47 + dp_rank: int | None,
48 + ready: threading.Barrier,
49 + stop: threading.Event,
50 + finished: threading.Event,
51 + is_probe: bool = False,
52 +) -> list[tuple[float, float, int | None]]:
53 + url = server.url_for("is_scaling_elastic_ep" if is_probe else "v1/completions")
54 + payload = {"model": MODEL_NAME, "prompt": "Hello", "max_tokens": 4}
55 + headers = None if dp_rank is None else {"X-data-parallel-rank": str(dp_rank)}
56 + request_payload = None if is_probe else payload
57 + responses = []
58 + is_ready = False
59 + while not stop.is_set():
60 + request_start = time.perf_counter()
61 + try:
62 + response = requests.post(
63 + url, json=request_payload, headers=headers, timeout=120
64 + )
65 + status_code = response.status_code
66 + except requests.exceptions.RequestException:
67 + status_code = None
68 + responses.append((request_start, time.perf_counter(), status_code))
69 + if status_code == 200:
70 + if not is_ready:
71 + ready.wait(timeout=120)
72 + is_ready = True
73 + if finished.is_set():
74 + return responses
75 + time.sleep(0.05)
76 + return responses
77 +
78 +
79 +def _downtime(responses: list[tuple[float, float, int | None]]) -> float:
80 + rejected = [end for _, end, status in responses if status == 503]
81 + if not rejected:
82 + return 0
83 + recovered = next(
84 + end for _, end, status in responses if status == 200 and end > rejected[-1]
85 + )
86 + return recovered - rejected[0]
87 +
88 +
89 +def _scale_with_traffic(
90 + server: RemoteOpenAIServer,
91 + source_dp_size: int,
92 + new_dp_size: int,
93 + traffic_mode: str,
94 +) -> None:
95 + traffic_clients: list[int | None] = []
96 + if traffic_mode == "light":
97 + traffic_clients = [0]
98 + elif traffic_mode == "heavy":
99 + traffic_clients = [None] * source_dp_size
100 + clients = [(None, True)] + [(rank, False) for rank in traffic_clients]
101 + ready = threading.Barrier(len(clients) + 1)
102 + stop = threading.Event()
103 + finished = threading.Event()
104 +
105 + with ThreadPoolExecutor(max_workers=len(clients)) as executor:
106 + futures = [
107 + executor.submit(
108 + _traffic_loop, server, rank, ready, stop, finished, is_probe
109 + )
110 + for rank, is_probe in clients
111 + ]
112 + try:
113 + ready.wait(timeout=120)
114 + start_time = time.perf_counter()
115 + assert _send_scale_command(server, new_dp_size)
116 + scale_seconds = time.perf_counter() - start_time
117 + finished.set()
118 + probe_result, *results = [future.result(timeout=120) for future in futures]
119 + finally:
120 + stop.set()
121 +
122 + bad_statuses = {
123 + status
124 + for responses in [probe_result, *results]
125 + for _, _, status in responses
126 + if status not in (200, 503)
127 + }
128 + assert not bad_statuses, f"traffic got unexpected statuses {bad_statuses}"
129 + probe_503 = [start for start, _, status in probe_result if status == 503]
130 + assert probe_503, "Scaling probe did not observe commit"
131 + assert not results or any(
132 + status == 200 and start_time <= request_start and request_end < probe_503[0]
133 + for responses in results
134 + for request_start, request_end, status in responses
135 + ), "No request completed successfully during preparation"
136 +
137 + print(
138 + f"[Elastic EP timing][{source_dp_size}->{new_dp_size}]"
139 + f"[traffic={traffic_mode}] "
140 + f"scale_seconds={scale_seconds:.3f} "
141 + f"downtime_seconds={_downtime(probe_result):.3f}"
142 + )
143 +
144 +
145 def _run_gsm8k_eval(server: RemoteOpenAIServer, stage: str) -> float:
146 assert server.port is not None
147 result = evaluate_gsm8k(
@@ -59,7 +161,7 @@ def _run_gsm8k_eval(server: RemoteOpenAIServer, stage: str) -> float:
161 return accuracy
162
163
62 -def _base_serve_args(use_async_eplb: bool = False) -> list[str]:
164 +def _base_serve_args(dp_size: int = 2, enforce_eager: bool = False) -> list[str]:
165 args = [
166 "--trust-remote-code",
167 "--tensor-parallel-size",
@@ -78,57 +180,65 @@ def _base_serve_args(use_async_eplb: bool = False) -> list[str]:
180 "--eplb-config.num_redundant_experts",
181 "0",
182 "--eplb-config.use_async",
81 - "true" if use_async_eplb else "false",
183 + "true",
184 "--eplb-config.step_interval",
83 - "10",
185 + "300",
186 "--eplb-config.window_size",
187 "5",
188 "--data-parallel-backend",
189 "ray",
190 "--data-parallel-size",
89 - "2",
191 + str(dp_size),
192 "--api-server-count",
193 "1",
194 + "--disable-access-log-for-endpoints",
195 + "/is_scaling_elastic_ep",
196 ]
197
198 leader_address = os.environ.get("LEADER_ADDRESS")
199 if leader_address:
200 args.extend(["--data-parallel-address", leader_address])
201 + if enforce_eager:
202 + args.append("--enforce-eager")
203
204 return args
205
206
207 @pytest.mark.parametrize(
102 - "use_async_eplb", [False, True], ids=["sync_eplb", "async_eplb"]
208 + ("enforce_eager", "traffic_mode"),
209 + [
210 + pytest.param(True, "none", id="enforce_eager_none"),
211 + pytest.param(True, "light", id="enforce_eager_light"),
212 + pytest.param(True, "heavy", id="enforce_eager_heavy"),
213 + pytest.param(False, "heavy", id="cuda_graphs_heavy"),
214 + ],
215 )
216 @multi_gpu_test(num_gpus=4)
105 -def test_elastic_ep_scaling(use_async_eplb: bool):
106 - if use_async_eplb:
107 - from vllm.distributed.eplb.eplb_communicator import has_nixl
217 +def test_elastic_ep_scaling(enforce_eager: bool, traffic_mode: str):
218 + from vllm.distributed.eplb.eplb_communicator import has_nixl
219
109 - if not has_nixl():
110 - pytest.skip("Async EPLB with elastic EP requires NIXL (not installed)")
220 + if not has_nixl():
221 + pytest.skip("Async EPLB with elastic EP requires NIXL (not installed)")
222
112 - vllm_serve_args = _base_serve_args(use_async_eplb)
223 + initial_dp_size = int(os.getenv("VLLM_TEST_ELASTIC_EP_INITIAL_DP", "2"))
224 + target_dp_size = int(os.getenv("VLLM_TEST_ELASTIC_EP_TARGET_DP", "4"))
225 + assert target_dp_size > initial_dp_size
226 + vllm_serve_args = _base_serve_args(initial_dp_size, enforce_eager)
227
228 with RemoteOpenAIServer(
229 MODEL_NAME, vllm_serve_args, env_dict={}, max_wait_seconds=1200
230 ) as server:
117 - initial_accuracy = _run_gsm8k_eval(server, "Initial (2 GPUs)")
118 -
119 - assert _send_scale_command(server, 4)
120 - time.sleep(10)
121 - scale_up_accuracy = _run_gsm8k_eval(server, "After scale up (4 GPUs)")
231 + initial_accuracy = _run_gsm8k_eval(server, "Initial")
232
233 + _scale_with_traffic(server, initial_dp_size, target_dp_size, traffic_mode)
234 + scale_up_accuracy = _run_gsm8k_eval(server, "After scale up")
235 assert scale_up_accuracy >= initial_accuracy - ACCURACY_TOL, (
236 f"Scale up accuracy {scale_up_accuracy:.3f} dropped more than "
237 f"{ACCURACY_TOL} below initial accuracy {initial_accuracy:.3f}"
238 )
239
128 - assert _send_scale_command(server, 2)
129 - time.sleep(5)
130 - scale_down_accuracy = _run_gsm8k_eval(server, "After scale down (2 GPUs)")
131 -
240 + _scale_with_traffic(server, target_dp_size, initial_dp_size, traffic_mode)
241 + scale_down_accuracy = _run_gsm8k_eval(server, "After scale down")
242 assert scale_down_accuracy >= initial_accuracy - ACCURACY_TOL, (
243 f"Scale down accuracy {scale_down_accuracy:.3f} dropped more than "
244 f"{ACCURACY_TOL} below initial accuracy {initial_accuracy:.3f}"
@@ -147,24 +257,20 @@ def test_elastic_ep_scaling(use_async_eplb: bool):
257 print(f" Tolerance: {ACCURACY_TOL:.3f}")
258
259
150 -@pytest.mark.parametrize(
151 - "use_async_eplb", [False, True], ids=["sync_eplb", "async_eplb"]
152 -)
260 @multi_gpu_test(num_gpus=4)
154 -def test_elastic_ep_scaling_uneven(use_async_eplb: bool):
261 +def test_elastic_ep_scaling_uneven():
262 """Test scale up with uneven worker distribution.
263
264 This tests the case where num_new_workers % old_dp_size != 0,
265 specifically 2 -> 3 where remainder = 1 % 2 = 1.
266 This exercises the remainder handling in sender-receiver pairing.
267 """
161 - if use_async_eplb:
162 - from vllm.distributed.eplb.eplb_communicator import has_nixl
268 + from vllm.distributed.eplb.eplb_communicator import has_nixl
269
164 - if not has_nixl():
165 - pytest.skip("Async EPLB with elastic EP requires NIXL (not installed)")
270 + if not has_nixl():
271 + pytest.skip("Async EPLB with elastic EP requires NIXL (not installed)")
272
167 - vllm_serve_args = _base_serve_args(use_async_eplb)
273 + vllm_serve_args = _base_serve_args()
274
275 with RemoteOpenAIServer(
276 MODEL_NAME, vllm_serve_args, env_dict={}, max_wait_seconds=1200
@@ -174,7 +280,6 @@ def test_elastic_ep_scaling_uneven(use_async_eplb: bool):
280 # Scale 2 -> 3: This has remainder = 1 % 2 = 1
281 # Tests uneven sender-receiver pairing
282 assert _send_scale_command(server, 3)
177 - time.sleep(10)
283 scale_up_accuracy = _run_gsm8k_eval(server, "After scale up (3 GPUs)")
284
285 assert scale_up_accuracy >= initial_accuracy - ACCURACY_TOL, (
@@ -184,7 +289,6 @@ def test_elastic_ep_scaling_uneven(use_async_eplb: bool):
289
290 # Scale back down to 2
291 assert _send_scale_command(server, 2)
187 - time.sleep(5)
292 scale_down_accuracy = _run_gsm8k_eval(server, "After scale down (2 GPUs)")
293
294 assert scale_down_accuracy >= initial_accuracy - ACCURACY_TOL, (
vllm/config/parallel.py
+9
@@ -686,6 +686,14 @@ class ParallelConfig:
686 and self.data_parallel_size > 1
687 )
688
689 + @property
690 + def use_all2all(self) -> bool:
691 + return (
692 + self.data_parallel_size > 1
693 + or self.use_sequence_parallel_moe
694 + or (self.enable_expert_parallel and self.prefill_context_parallel_size > 1)
695 + )
696 +
697 @property
698 def use_batched_dp_moe(self) -> bool:
699 return (
@@ -786,6 +794,7 @@ class ParallelConfig:
794 "data_parallel_master_ip",
795 "data_parallel_master_port",
796 "_data_parallel_master_port_list",
797 + "_coord_store_port",
798 "data_parallel_rpc_port",
799 "rank",
800 "master_addr",
vllm/distributed/device_communicators/base_device_communicator.py
+3 -13
@@ -175,6 +175,7 @@ class DeviceCommunicatorBase:
175 unique_name: str = "",
176 global_ranks: list[int] | None = None,
177 global_world_size: int | None = None,
178 + use_all2all: bool = False,
179 ):
180 self.device = device or torch.device("cpu")
181 self.cpu_group = cpu_group
@@ -204,26 +205,15 @@ class DeviceCommunicatorBase:
205 self.global_world_size = dist.get_world_size()
206 self.rank_in_group = dist.get_group_rank(self.cpu_group, self.global_rank)
207
207 - use_ep = False
208 all2all_backend = None
209 from vllm.config import get_current_vllm_config_or_none
210
211 config = get_current_vllm_config_or_none()
212 if config is not None:
213 - # initialize the all2all manager for DP or sequence-parallel EP.
214 - parallel_config = config.parallel_config
215 - use_ep = (
216 - parallel_config.data_parallel_size > 1
217 - or parallel_config.use_sequence_parallel_moe
218 - or (
219 - parallel_config.enable_expert_parallel
220 - and parallel_config.prefill_context_parallel_size > 1
221 - )
222 - )
223 - all2all_backend = parallel_config.all2all_backend
213 + all2all_backend = config.parallel_config.all2all_backend
214
215 self.is_ep_communicator = unique_name.split(":")[0] == "ep"
226 - self.use_all2all = self.is_ep_communicator and use_ep
216 + self.use_all2all = self.is_ep_communicator and use_all2all
217 self.all2all_backend = all2all_backend
218 self.all2all_manager: All2AllManagerBase | None = None
219
vllm/distributed/device_communicators/cpu_communicator.py
+4 -1
@@ -24,8 +24,11 @@ class CpuCommunicator(DeviceCommunicatorBase):
24 device: torch.device | None = None,
25 device_group: ProcessGroup | None = None,
26 unique_name: str = "",
27 + use_all2all: bool = False,
28 ):
28 - super().__init__(cpu_group, device, device_group, unique_name)
29 + super().__init__(
30 + cpu_group, device, device_group, unique_name, use_all2all=use_all2all
31 + )
32 self.dist_module = torch.distributed
33
34 if (
vllm/distributed/device_communicators/cuda_communicator.py
+2
@@ -36,6 +36,7 @@ class CudaCommunicator(DeviceCommunicatorBase):
36 global_ranks: list[int] | None = None,
37 global_world_size: int | None = None,
38 tcp_store_group: StatelessProcessGroup | None = None,
39 + use_all2all: bool = False,
40 ):
41 super().__init__(
42 cpu_group,
@@ -44,6 +45,7 @@ class CudaCommunicator(DeviceCommunicatorBase):
45 unique_name,
46 global_ranks,
47 global_world_size,
48 + use_all2all=use_all2all,
49 )
50 if "tp" not in unique_name:
51 # custom allreduce or torch symm mem can be used only by tp
vllm/distributed/device_communicators/xpu_communicator.py
+4 -1
@@ -20,8 +20,11 @@ class XpuCommunicator(DeviceCommunicatorBase):
20 device: torch.device | None = None,
21 device_group: ProcessGroup | None = None,
22 unique_name: str = "",
23 + use_all2all: bool = False,
24 ):
24 - super().__init__(cpu_group, device, device_group, unique_name)
25 + super().__init__(
26 + cpu_group, device, device_group, unique_name, use_all2all=use_all2all
27 + )
28 self.ca_comm: None = None
29 if self.use_all2all:
30 if self.all2all_backend in ("naive", "allgather_reducescatter"):
vllm/distributed/elastic_ep/elastic_execute.py
+95 -86
@@ -1,9 +1,9 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -import copy
3 import gc
4 import weakref
5 from collections.abc import Iterable, Sequence
6 +from concurrent.futures import Future, ThreadPoolExecutor
7 from dataclasses import replace
8 from typing import TYPE_CHECKING
9
@@ -43,6 +43,7 @@ from vllm.model_executor.layers.fused_moe.config import FusedMoEParallelConfig
43 from vllm.model_executor.layers.fused_moe.eep_reconfigure import (
44 make_eep_staged_quant_method,
45 )
46 +from vllm.model_executor.warmup.kernel_warmup import kernel_warmup
47 from vllm.utils import is_moe_layer
48 from vllm.v1.engine import ReconfigureDistributedRequest, ReconfigureRankType
49 from vllm.v1.worker.gpu_ubatch_wrapper import UBatchWrapper
@@ -145,6 +146,10 @@ class ElasticEPScalingExecutor:
146 self.worker_ref = weakref.ref(worker)
147 self.reconfig_request = None
148 self._staged_moe_quant_methods: dict[nn.Module, FusedMoEMethodBase] = {}
149 + self._async_executor = ThreadPoolExecutor(
150 + max_workers=1, thread_name_prefix="ElasticEPAsync"
151 + )
152 + self._async_future: Future[None] | None = None
153
154 @property
155 def worker(self):
@@ -159,59 +164,68 @@ class ElasticEPScalingExecutor:
164 raise ValueError(f"Unknown execute method: {execute_method}")
165 return method(*args, **kwargs)
166
162 - def _set_eplb_suppressed(self, suppressed: bool) -> None:
163 - self.worker.model_runner.eep_eplb_suppressed = suppressed
164 - ep_group = get_standby_ep_group() or get_ep_group()
165 - if ep_group.rank == 0:
166 - logger.info(
167 - "[Elastic EP] EPLB %s elastic scaling transition",
168 - "disabled during" if suppressed else "re-enabled after",
169 - )
167 + def start_async(self, execute_method: str, *args, **kwargs) -> str:
168 + if self._async_future is not None:
169 + raise RuntimeError("Another Elastic EP async method is active")
170 + if args and isinstance(args[0], ReconfigureDistributedRequest):
171 + self.reconfig_request = args[0]
172 + dp_rank = self.worker.vllm_config.parallel_config.data_parallel_rank
173 + done_key = f"eep_async/{execute_method}/{dp_rank}/{self.worker.rank}"
174 + self._async_future = self._async_executor.submit(
175 + self._run_async, execute_method, *args, **kwargs
176 + )
177 + self._async_future.add_done_callback(lambda _: self._mark_async_done(done_key))
178 + return done_key
179 +
180 + def _run_async(self, execute_method: str, *args, **kwargs) -> None:
181 + from vllm.platforms import current_platform
182 +
183 + self.worker.vllm_config.enable_trace_function_call_for_thread()
184 + assert hasattr(self.worker, "device")
185 + current_platform.set_device(self.worker.device)
186 + with set_current_vllm_config(self.worker.vllm_config):
187 + self.execute(execute_method, *args, **kwargs)
188 +
189 + def _mark_async_done(self, done_key: str) -> None:
190 + from vllm.distributed.utils import get_cached_tcp_store_client
191 +
192 + assert self.reconfig_request is not None
193 + get_cached_tcp_store_client(
194 + self.reconfig_request.new_data_parallel_master_ip,
195 + self.reconfig_request.coord_store_port,
196 + ).set(done_key, b"1")
197 +
198 + def clear_async(self) -> None:
199 + future = self._async_future
200 + if future is None:
201 + raise RuntimeError("No Elastic EP async method is active")
202 + if not future.done():
203 + raise RuntimeError("Elastic EP async method is not done")
204 + self._async_future = None
205 + future.result()
206
207 def load_model(self) -> None:
172 - (
173 - expanded_physical_to_logical,
174 - num_logical_experts,
175 - old_num_physical_experts,
176 - ) = self.receive_expert_mapping()
177 - num_physical_experts = expanded_physical_to_logical.shape[1]
178 - self.worker.parallel_config.eplb_config.num_redundant_experts = (
179 - num_physical_experts - num_logical_experts
180 - )
208 self.worker.load_model(load_dummy_weights=True)
182 - self.worker.model_runner.setup_eplb_from_mapping(
183 - expanded_physical_to_logical, old_num_physical_experts
184 - )
185 - self._set_eplb_suppressed(True)
209
210 def create_standby_groups(
188 - self, reconfig_request: ReconfigureDistributedRequest
211 + self, reconfig_request: ReconfigureDistributedRequest, use_all2all: bool
212 ) -> None:
213 self.reconfig_request = reconfig_request
214 new_dp_size = reconfig_request.new_data_parallel_size
215 old_dp_size = get_dp_group().world_size
193 - world_size = self.worker.vllm_config.parallel_config.world_size
216 + parallel_config = self.worker.vllm_config.parallel_config
217 + world_size = parallel_config.world_size
218 new_world_size_across_dp = world_size * new_dp_size
195 - updated_config = copy.copy(self.worker.vllm_config)
196 - updated_config.parallel_config = copy.deepcopy(
197 - self.worker.vllm_config.parallel_config
219 + create_standby_groups(
220 + new_dp_size=new_dp_size,
221 + new_world_size_across_dp=new_world_size_across_dp,
222 + master_ip=reconfig_request.new_data_parallel_master_ip,
223 + coord_store_port=reconfig_request.coord_store_port,
224 + use_all2all=use_all2all,
225 + enable_eplb=parallel_config.enable_eplb,
226 )
199 - updated_config.parallel_config.data_parallel_size = new_dp_size
200 - with set_current_vllm_config(updated_config):
201 - create_standby_groups(
202 - new_dp_size=new_dp_size,
203 - new_world_size_across_dp=new_world_size_across_dp,
204 - master_ip=reconfig_request.new_data_parallel_master_ip,
205 - coord_store_port=reconfig_request.coord_store_port,
206 - enable_eplb=updated_config.parallel_config.enable_eplb,
207 - )
208 - if new_dp_size > old_dp_size:
209 - self._set_eplb_suppressed(True)
210 - eplb_state = self.worker.model_runner.eplb_state
211 - if eplb_state is not None:
212 - eplb_state.drain_async()
213 - elif new_dp_size < old_dp_size:
214 - self._stage_standby_moe_quant_methods()
227 + if new_dp_size < old_dp_size:
228 + self.stage_standby_moe_quant_methods()
229
230 def transfer_weights(self, old_dp_size: int, new_dp_size: int) -> None:
231 standby_dp_group = get_standby_dp_group()
@@ -265,6 +279,7 @@ class ElasticEPScalingExecutor:
279 model_config = self.worker.model_runner.model_config
280 eplb_state = self.worker.model_runner.eplb_state
281 assert eplb_state is not None
282 + eplb_state.drain_async()
283 eplb_model_state = eplb_state.model_states[model_config.compute_hash()]
284 physical_to_logical = eplb_model_state.physical_to_logical_map
285 num_physical_experts = physical_to_logical.shape[1]
@@ -278,10 +293,6 @@ class ElasticEPScalingExecutor:
293 src_rank=0,
294 device=self.worker.device,
295 )
281 - # New workers enter load_model after receiving the expert mapping.
282 - # Stage replacement MoE kernels before returning to the state machine
283 - # so existing ranks can participate in collective EP comm creation.
284 - self._stage_standby_moe_quant_methods()
296
297 def _make_eep_moe_config(self, module, dp_group, ep_group):
298 parallel_config = self.worker.vllm_config.parallel_config
@@ -300,7 +311,7 @@ class ElasticEPScalingExecutor:
311 moe_parallel_config=moe_parallel_config,
312 )
313
303 - def _stage_standby_moe_quant_methods(self) -> None:
314 + def stage_standby_moe_quant_methods(self) -> None:
315 standby_dp_group = get_standby_dp_group()
316 standby_ep_group = get_standby_ep_group()
317 model = self.worker.model_runner.get_model()
@@ -500,26 +511,6 @@ class ElasticEPScalingExecutor:
511 compilation_counter.stock_torch_compile_count += 1
512 self.worker.model_runner.model.compile(fullgraph=True, backend=backend)
513
503 - multi_block_table = self.worker.model_runner.input_batch.block_table
504 - saved_block_tables: list[tuple[torch.Tensor, torch.Tensor]] = []
505 - for bt in multi_block_table.block_tables:
506 - saved_block_tables.append(
507 - (bt.block_table.gpu.clone(), bt.block_table.cpu.clone())
508 - )
509 - multi_block_table.clear()
510 -
511 - unlock_workspace()
512 - self.worker.compile_or_warm_up_model()
513 - lock_workspace()
514 -
515 - for bt, (saved_gpu, saved_cpu) in zip(
516 - multi_block_table.block_tables, saved_block_tables
517 - ):
518 - bt.block_table.gpu.copy_(saved_gpu)
519 - bt.block_table.cpu.copy_(saved_cpu)
520 - if new_dp_size < old_dp_size:
521 - self._set_eplb_suppressed(False)
522 -
514 def _perform_eplb_reshuffle(
515 self, rank_mapping: dict[int, int] | None = None
516 ) -> None:
@@ -553,12 +544,25 @@ class ElasticEPScalingExecutor:
544 if get_ep_group().rank == 0:
545 logger.info("[Elastic EP] Expert resharding completed")
546
556 - def perform_eplb_reshuffle(self) -> None:
547 + def commit_scale_up(self, is_existing_worker: bool) -> None:
548 + if is_existing_worker:
549 + self.broadcast_expert_mapping()
550 + self.switch_and_prepare()
551 + else:
552 + mapping, _, num_valid_experts = self.receive_expert_mapping()
553 + self.worker.model_runner.setup_eplb_from_mapping(mapping, num_valid_experts)
554 self._perform_eplb_reshuffle()
558 - self._set_eplb_suppressed(False)
555 + self.warm_and_capture()
556 +
557 + def commit_scale_down(self, new_dp_size: int, removing: bool) -> None:
558 + self.perform_scale_down_eplb_reshuffle(new_dp_size)
559 + if removing:
560 + self.switch_and_remove()
561 + else:
562 + self.switch_and_prepare()
563 + self.warm_and_capture()
564
565 def perform_scale_down_eplb_reshuffle(self, new_dp_size: int) -> None:
561 - self._set_eplb_suppressed(True)
566 eplb_state = self.worker.model_runner.eplb_state
567 if eplb_state is not None:
568 eplb_state.drain_async()
@@ -599,12 +603,17 @@ class ElasticEPScalingExecutor:
603 )
604
605 model = self.worker.model_runner.get_model()
606 + expert_weights = [
607 + module.get_expert_weights()
608 + for module in model.modules()
609 + if is_moe_layer(module)
610 + ]
611 batch_transfer_weights(
612 model=model,
613 is_sender=False,
614 peer_rank=sender_rank,
615 dp_group=dp_group,
607 - expert_weights=model.expert_weights,
616 + expert_weights=expert_weights,
617 )
618 torch.accelerator.synchronize()
619
@@ -643,14 +652,17 @@ class ElasticEPScalingExecutor:
652 with set_current_vllm_config(self.worker.vllm_config):
653 prepare_communication_buffer_for_model(self.worker.model_runner.get_model())
654
646 - def rewarm_workspace(self) -> None:
655 + def warmup_local_kernels(self) -> None:
656 + with set_current_vllm_config(self.worker.vllm_config):
657 + kernel_warmup(self.worker, process_local_only=True)
658 +
659 + def warm_and_capture(self) -> None:
660 # Must run on every DP sibling in lockstep: _dummy_run calls
661 # coordinate_batch_across_dp whenever data_parallel_size > 1
662 # (gpu_model_runner.py:3663), which deadlocks if any rank skips it.
663
651 - # Save and clear block tables so profile_run/compile_or_warm_up_model
652 - # don't write dummy slot mappings into real KV-cache blocks (mirrors
653 - # switch_and_prepare's pattern).
664 + # Save and clear block tables so the dummy MoE forward doesn't
665 + # write dummy slot mappings into real KV-cache blocks.
666 multi_block_table = self.worker.model_runner.input_batch.block_table
667 saved_block_tables: list[tuple[torch.Tensor, torch.Tensor]] = []
668 for bt in multi_block_table.block_tables:
@@ -660,19 +672,16 @@ class ElasticEPScalingExecutor:
672 multi_block_table.clear()
673
674 # _ensure_workspace_size allocates a fresh tensor on grow, leaving
663 - # captured CUDA graphs with stale data pointers; drop graphs before
664 - # re-warm so captures realign with the resized buffer.
675 + # any captured CUDA graph with a stale data pointer; drop graphs
676 + # before re-warm so captures realign with the resized buffer.
677 self._release_cuda_graphs()
678 unlock_workspace()
679
668 - # Grow the MoE workspace at max_num_tokens.
669 - # compile_or_warm_up_model alone only exercises cudagraph-capture
670 - # sizes (≤64 tokens for this test) and leaves the workspace at
671 - # ~10-14 MB; the post-all-to-all per-rank token count under real
672 - # post-reshuffle routing needs hundreds of MB. Use _dummy_run
673 - # directly (rather than profile_run) with skip_eplb=True so dummy
674 - # routing doesn't pollute the just-rebalanced EPLB stats — same
675 - # convention compile_or_warm_up_model itself uses.
680 + # Grow the MoE workspace at max_num_tokens. compile_or_warm_up_model
681 + # alone only exercises cudagraph-capture sizes and can leave the
682 + # workspace too small for post-reshuffle routing. Use _dummy_run
683 + # directly with skip_eplb=True so dummy routing doesn't pollute the
684 + # just-rebalanced EPLB stats.
685 runner = self.worker.model_runner
686 runner._dummy_run(runner.max_num_tokens, is_profile=True, skip_eplb=True)
687 self.worker.compile_or_warm_up_model()
vllm/distributed/elastic_ep/elastic_state.py
+161 -297
@@ -1,18 +1,17 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import enum
4 -import time
4 import weakref
6 -from datetime import timedelta
7 -from typing import TYPE_CHECKING, Literal, TypeAlias
5 +from concurrent.futures import Future, ThreadPoolExecutor
6 +from typing import TYPE_CHECKING, Any, Literal, TypeAlias
7
8 import torch.distributed
9
10 from vllm.config import ParallelConfig
11 from vllm.distributed import (
13 - sched_yield,
12 stateless_destroy_torch_distributed_process_group,
13 )
14 +from vllm.distributed.utils import get_cached_tcp_store_client
15 from vllm.logger import init_logger
16 from vllm.v1.engine import (
17 EEPNotificationType,
@@ -31,35 +30,29 @@ WorkerType = Literal["existing", "new", "removing"]
30
31
32 class ScaleUpExistingEngineState(enum.IntEnum):
34 - WAIT_NEW_CORE_ENGINES_INIT = 0
35 - CREATE_STANDBY_GROUPS = 1
36 - TRANSFER_EXPERT_MAPPING = 2
37 - WAIT_NEW_CORE_ENGINES_WEIGHTS_INIT = 3
38 - TRANSFER_WEIGHTS = 4
39 - SYNC_KV_CACHE_MEMORY_SIZE = 5
40 - SWITCH_AND_PREPARE = 6
41 - EPLB_RESHUFFLE = 7
42 - COMPLETE = 8
33 + CREATE_STANDBY_GROUPS = 0
34 + STAGE_QUANT_METHODS = 1
35 + TRANSFER_WEIGHTS = 2
36 + SYNC_KV_CACHE_MEMORY_SIZE = 3
37 + COMMIT_SCALE_UP = 4 # Blocks forward passes.
38 + COMPLETE = 5
39
40
41 class ScaleUpNewEngineState(enum.IntEnum):
42 PRE_KV_INIT = 0
43 PREPARE = 1
48 - EPLB_RESHUFFLE = 2
49 - COMPLETE = 3
44 + COMPLETE = 2
45
46
47 class ScaleDownRemainingEngineState(enum.IntEnum):
48 PREPARE = 0
54 - EPLB_RESHUFFLE = 1
55 - SWITCH_AND_PREPARE = 2
56 - COMPLETE = 3
49 + COMMIT_SCALE_DOWN = 1 # Blocks forward passes.
50 + COMPLETE = 2
51
52
53 class ScaleDownRemovingEngineState(enum.IntEnum):
54 PREPARE = 0
61 - EPLB_RESHUFFLE = 1
62 - COMPLETE = 2
55 + COMPLETE = 1
56
57
58 EngineState: TypeAlias = (
@@ -70,15 +63,6 @@ EngineState: TypeAlias = (
63 )
64
65
73 -class _BarrierTimeoutError(RuntimeError):
74 - """
75 - Exception raised for timeout
76 - in the first stage of our two-staged
77 - TCPStore based barrier to synchronize the
78 - execution of all engines in the DP group.
79 - """
80 -
81 -
66 class ElasticEPScalingState:
67 def __init__(
68 self,
@@ -94,20 +78,24 @@ class ElasticEPScalingState:
78 self.engine_core_ref = weakref.ref(engine_core)
79 self.vllm_config = vllm_config
80 self.old_dp_group = self.engine_core.dp_group if worker_type != "new" else None
97 - self.old_dp_store = self.engine_core.dp_store if worker_type != "new" else None
81 self.new_parallel_config: ParallelConfig = new_parallel_config
82 self.new_dp_group = self.engine_core.dp_group if worker_type == "new" else None
83 self.new_dp_store = self.engine_core.dp_store if worker_type == "new" else None
84 self.worker_type = worker_type
85 self.scale_type = scale_type
86 self.reconfig_request = reconfig_request
104 -
87 + self.commit_requested = False
88 + self._prepare_executor = ThreadPoolExecutor(
89 + max_workers=1, thread_name_prefix="ElasticEPPrepare"
90 + )
91 + self._prepare_future: Future[Any] | None = None
92 + self._new_dp_sync: tuple[object, Any] | None = None
93 self.state: EngineState
94 if scale_type == "scale_up":
95 self.state = (
96 ScaleUpNewEngineState.PRE_KV_INIT
97 if worker_type == "new"
110 - else ScaleUpExistingEngineState.WAIT_NEW_CORE_ENGINES_INIT
98 + else ScaleUpExistingEngineState.CREATE_STANDBY_GROUPS
99 )
100 else:
101 self.state = (
@@ -130,6 +118,31 @@ class ElasticEPScalingState:
118 raise RuntimeError("Engine core has been garbage collected")
119 return engine_core
120
121 + def _collective_rpc(self, *args, **kwargs):
122 + return self.model_executor.collective_rpc(*args, **kwargs)
123 +
124 + def _execute_async(self, execute_method: str, *args) -> bool:
125 + if self._prepare_future is None:
126 + done_keys = self._collective_rpc(
127 + "elastic_ep_execute",
128 + args=("start_async", execute_method, *args),
129 + )
130 + assert self.reconfig_request is not None
131 + coord_store = get_cached_tcp_store_client(
132 + self.reconfig_request.new_data_parallel_master_ip,
133 + self.reconfig_request.coord_store_port,
134 + )
135 + self._prepare_future = self._prepare_executor.submit(
136 + coord_store.wait, done_keys
137 + )
138 + if not self._prepare_future.done():
139 + return False
140 +
141 + self._prepare_future.result()
142 + self._collective_rpc("elastic_ep_execute", args=("clear_async",))
143 + self._prepare_future = None
144 + return True
145 +
146 def progress(self) -> bool:
147 if self.scale_type == "scale_up":
148 return (
@@ -149,157 +162,43 @@ class ElasticEPScalingState:
162 assert self.progress()
163 assert self.state == ScaleUpNewEngineState.PREPARE
164
152 - def _execute_tcp_store_barrier(
153 - self, dp_store, group_rank, group_size, barrier_id, timeout=None
154 - ):
155 - arrival_key = f"arrival_{barrier_id}_{group_rank}"
156 - dp_store.set(arrival_key, b"1")
157 -
158 - start_time = time.time()
159 - processes_arrived: set[int] = set()
160 -
161 - while len(processes_arrived) < group_size:
162 - if (
163 - timeout is not None
164 - and time.time() - start_time > timeout.total_seconds()
165 - ):
166 - raise _BarrierTimeoutError(
167 - f"Barrier timed out after {timeout.total_seconds()} seconds"
168 - )
169 -
170 - for i in range(group_size):
171 - if i in processes_arrived:
172 - continue
173 -
174 - key = f"arrival_{barrier_id}_{i}"
175 - present = dp_store.check([key])
176 - if present:
177 - processes_arrived.add(i)
178 -
179 - if len(processes_arrived) < group_size:
180 - sched_yield()
181 -
182 - def _staged_barrier(self, use_new_group: bool, barrier_name: str) -> bool:
183 - """
184 - Execute a two-staged barrier to synchronize all engines in the DP group.
185 -
186 - Some DP EngineCores may receive the reconfiguration notifications
187 - later than others, and already proceed to engine step (model forward)
188 - in the busy loop.
189 - In this case, EngineCores that already proceed to reconfiguration
190 - should skip reconfiguration and execute model forward for one more
191 - step, so in the next step, all EngineCores will be synchronized.
192 - We use a two-staged barrier to achieve this. The first time each
193 - EngineCore executes the barrier, if a timeout is reached before the
194 - barrier completes, that means some EngineCores have already entered
195 - engine step. The EngineCores that timed out will then proceed to
196 - engine step, and will synchronize with the other EngineCores in the
197 - next step with a barrier without timeout.
198 - """
199 - dp_group = self.new_dp_group if use_new_group else self.old_dp_group
200 - dp_store = self.new_dp_store if use_new_group else self.old_dp_store
201 - assert dp_group is not None and dp_store is not None
202 -
203 - group_rank = dp_group.rank()
204 - group_size = dp_group.size()
205 - barrier_id = f"eep_barrier_{barrier_name}"
206 - sync_key = f"{barrier_id}_sync"
207 -
208 - # TODO(yongji): figure out appropriate timeout for the barrier
209 - timeout = None if dp_store.check([sync_key]) else timedelta(seconds=5)
210 -
211 - try:
212 - self._execute_tcp_store_barrier(
213 - dp_store, group_rank, group_size, barrier_id, timeout=timeout
214 - )
215 - torch.distributed.barrier(dp_group)
216 - if group_rank == 0:
217 - dp_store.delete_key(sync_key)
218 - for i in range(group_size):
219 - dp_store.delete_key(f"arrival_{barrier_id}_{i}")
220 - return True
221 - except _BarrierTimeoutError as e:
222 - if timeout is None:
223 - raise RuntimeError("Unexpected timeout encountered") from e
224 - dp_store.compare_set(sync_key, "", b"1")
225 - return False
226 -
165 def _progress_existing_engine(self) -> bool:
166 state = self.state
229 - assert self.old_dp_group is not None and self.old_dp_store is not None
230 -
231 - if state == ScaleUpExistingEngineState.WAIT_NEW_CORE_ENGINES_INIT:
232 - return False
167 + assert self.old_dp_group is not None
168
234 - elif state == ScaleUpExistingEngineState.CREATE_STANDBY_GROUPS:
235 - # NOTE(yongji): wait for all existing workers to receive the request
236 - if (
237 - int(self.old_dp_store.get("eep_barrier_engine_count"))
238 - < self.old_dp_group.size()
239 - ):
240 - return False
241 - if not self._staged_barrier(
242 - use_new_group=False, barrier_name="create_standby_groups"
243 - ):
169 + if state == ScaleUpExistingEngineState.CREATE_STANDBY_GROUPS:
170 + if not self._create_standby_groups():
171 return False
245 - if self.old_dp_group.rank() == 0:
246 - self.old_dp_store.delete_key("eep_barrier_engine_count")
247 - self._create_standby_groups()
248 - self.state = ScaleUpExistingEngineState.TRANSFER_EXPERT_MAPPING
172 + self.state = ScaleUpExistingEngineState.STAGE_QUANT_METHODS
173 return True
174
251 - elif state == ScaleUpExistingEngineState.TRANSFER_EXPERT_MAPPING:
252 - self._transfer_expert_mapping()
253 - self.state = ScaleUpExistingEngineState.WAIT_NEW_CORE_ENGINES_WEIGHTS_INIT
175 + elif state == ScaleUpExistingEngineState.STAGE_QUANT_METHODS:
176 + if not self._execute_async("stage_standby_moe_quant_methods"):
177 + return False
178 + self.state = ScaleUpExistingEngineState.TRANSFER_WEIGHTS
179 return True
180
256 - elif state == ScaleUpExistingEngineState.WAIT_NEW_CORE_ENGINES_WEIGHTS_INIT:
257 - return False
258 -
181 elif state == ScaleUpExistingEngineState.TRANSFER_WEIGHTS:
260 - if (
261 - int(self.old_dp_store.get("eep_barrier_engine_count"))
262 - < self.old_dp_group.size()
263 - ):
182 + if not self._transfer_weights():
183 return False
265 - if not self._staged_barrier(
266 - use_new_group=False, barrier_name="transfer_weights"
267 - ):
268 - return False
269 - if self.old_dp_group.rank() == 0:
270 - self.old_dp_store.delete_key("eep_barrier_engine_count")
271 - self._transfer_weights()
184 self.state = ScaleUpExistingEngineState.SYNC_KV_CACHE_MEMORY_SIZE
185 return True
186
187 elif state == ScaleUpExistingEngineState.SYNC_KV_CACHE_MEMORY_SIZE:
276 - self._sync_kv_cache_memory_size()
277 - self.state = ScaleUpExistingEngineState.SWITCH_AND_PREPARE
278 - return True
279 -
280 - elif state == ScaleUpExistingEngineState.SWITCH_AND_PREPARE:
281 - self._switch_and_prepare()
282 - self.state = ScaleUpExistingEngineState.EPLB_RESHUFFLE
283 - assert self.new_dp_store is not None
284 - self.new_dp_store.add("eep_barrier_engine_count", 1)
188 + if not self._sync_kv_cache_memory_size():
189 + return False
190 + self.state = ScaleUpExistingEngineState.COMMIT_SCALE_UP
191 + self._mark_ready_for_switch()
192 return True
193
287 - elif state == ScaleUpExistingEngineState.EPLB_RESHUFFLE:
288 - assert self.new_dp_group is not None and self.new_dp_store is not None
289 - if (
290 - int(self.new_dp_store.get("eep_barrier_engine_count"))
291 - < self.new_dp_group.size()
292 - ):
293 - return False
294 - if not self._staged_barrier(
295 - use_new_group=True, barrier_name="eplb_reshuffle"
296 - ):
194 + elif state == ScaleUpExistingEngineState.COMMIT_SCALE_UP:
195 + if not self.commit_requested:
196 return False
298 - if self.new_dp_group.rank() == 0:
299 - self.new_dp_store.delete_key("eep_barrier_engine_count")
300 - self._eplb_reshuffle()
197 + self._commit_new_dp_group()
198 + self._collective_rpc("elastic_ep_execute", args=("commit_scale_up", True))
199 self.state = ScaleUpExistingEngineState.COMPLETE
200 self._update_parallel_config()
201 + self._send_reconfigure_finished()
202 return True
203
204 else:
@@ -311,22 +210,17 @@ class ElasticEPScalingState:
210 assert self.new_dp_group is not None and self.new_dp_store is not None
211
212 if state == ScaleUpNewEngineState.PRE_KV_INIT:
314 - self.engine_core._eep_send_engine_core_notification(
315 - EEPNotificationType.NEW_CORE_ENGINES_WEIGHTS_INIT_READY
316 - )
317 - self.model_executor.collective_rpc(
318 - "elastic_ep_execute", args=("receive_weights",)
319 - )
213 + self._collective_rpc("elastic_ep_execute", args=("receive_weights",))
214 self.engine_core.available_gpu_memory_for_kv_cache = (
215 ParallelConfig.sync_kv_cache_memory_size(self.new_dp_group, -1)
216 )
323 - self.model_executor.collective_rpc(
324 - "elastic_ep_execute", args=("prepare_new_worker",)
325 - )
217 + self._collective_rpc("elastic_ep_execute", args=("prepare_new_worker",))
218 self.state = ScaleUpNewEngineState.PREPARE
219 return True
220
221 elif state == ScaleUpNewEngineState.PREPARE:
222 + self._collective_rpc("elastic_ep_execute", args=("warmup_local_kernels",))
223 + self._mark_ready_for_switch()
224 tensor = torch.tensor([0, 0, 0], dtype=torch.int32, device="cpu")
225 torch.distributed.all_reduce(
226 tensor,
@@ -337,22 +231,7 @@ class ElasticEPScalingState:
231 self.engine_core.engines_running = bool(data[0])
232 self.engine_core.current_wave = int(data[1])
233 self.engine_core.step_counter = int(data[2])
340 - self.state = ScaleUpNewEngineState.EPLB_RESHUFFLE
341 - self.new_dp_store.add("eep_barrier_engine_count", 1)
342 - return True
343 -
344 - elif state == ScaleUpNewEngineState.EPLB_RESHUFFLE:
345 - if (
346 - int(self.new_dp_store.get("eep_barrier_engine_count"))
347 - < self.new_dp_group.size()
348 - ):
349 - return False
350 - if not self._staged_barrier(
351 - use_new_group=True, barrier_name="eplb_reshuffle"
352 - ):
353 - return False
354 - assert self.new_dp_group.rank() > 0
355 - self._eplb_reshuffle()
234 + self._collective_rpc("elastic_ep_execute", args=("commit_scale_up", False))
235 self.state = ScaleUpNewEngineState.COMPLETE
236 return True
237
@@ -362,38 +241,23 @@ class ElasticEPScalingState:
241
242 def _progress_remaining_engine(self) -> bool:
243 state = self.state
365 - assert self.old_dp_group is not None and self.old_dp_store is not None
244 + assert self.old_dp_group is not None
245
246 if state == ScaleDownRemainingEngineState.PREPARE:
368 - self.state = ScaleDownRemainingEngineState.EPLB_RESHUFFLE
369 - self.old_dp_store.add("eep_barrier_engine_count", 1)
370 - return True
247 + if self._create_standby_groups():
248 + self.state = ScaleDownRemainingEngineState.COMMIT_SCALE_DOWN
249 + self._mark_ready_for_switch()
250 + return True
251 + return False
252
372 - elif state == ScaleDownRemainingEngineState.EPLB_RESHUFFLE:
373 - if (
374 - int(self.old_dp_store.get("eep_barrier_engine_count"))
375 - < self.old_dp_group.size()
376 - ):
377 - return False
378 - if not self._staged_barrier(
379 - use_new_group=False, barrier_name="eplb_reshuffle"
380 - ):
253 + elif state == ScaleDownRemainingEngineState.COMMIT_SCALE_DOWN:
254 + if not self.commit_requested:
255 return False
382 - if self.old_dp_group.rank() == 0:
383 - self.old_dp_store.delete_key("eep_barrier_engine_count")
384 - self._eplb_reshuffle_before_scale_down()
385 - self.state = ScaleDownRemainingEngineState.SWITCH_AND_PREPARE
386 - # NOTE(yongji): currently, after EPLB reshuffle
387 - # that redistributes experts to remaining workers, workers
388 - # to be removed will immediately initiate shutdown;
389 - # existing workers can no longer execute forward steps using
390 - # the old setup. In the future, we may keep
391 - # the removing workers alive a bit longer,
392 - # e.g., to drain in-batch requests.
393 - self._create_standby_groups()
394 - self._switch_and_prepare()
256 + self._commit_scale_down(removing=False)
257 + self._commit_new_dp_group()
258 self._update_parallel_config()
259 self.state = ScaleDownRemainingEngineState.COMPLETE
260 + self._send_reconfigure_finished()
261 return True
262
263 else:
@@ -402,26 +266,11 @@ class ElasticEPScalingState:
266
267 def _progress_removing_engine(self) -> bool:
268 state = self.state
405 - assert self.old_dp_group is not None and self.old_dp_store is not None
269 + assert self.old_dp_group is not None
270
271 if state == ScaleDownRemovingEngineState.PREPARE:
408 - self.state = ScaleDownRemovingEngineState.EPLB_RESHUFFLE
409 - self.old_dp_store.add("eep_barrier_engine_count", 1)
410 - return True
411 -
412 - if state == ScaleDownRemovingEngineState.EPLB_RESHUFFLE:
413 - if (
414 - int(self.old_dp_store.get("eep_barrier_engine_count"))
415 - < self.old_dp_group.size()
416 - ):
417 - return False
418 - if not self._staged_barrier(
419 - use_new_group=False, barrier_name="eplb_reshuffle"
420 - ):
421 - return False
272 assert self.old_dp_group.rank() > 0
423 - self._eplb_reshuffle_before_scale_down()
424 - self._switch_and_remove()
273 + self._commit_scale_down(removing=True)
274 self.state = ScaleDownRemovingEngineState.COMPLETE
275 self.engine_core._eep_send_engine_core_notification(
276 EEPNotificationType.SHUTDOWN_COMPLETE
@@ -432,22 +281,22 @@ class ElasticEPScalingState:
281 assert self.state == ScaleDownRemovingEngineState.COMPLETE
282 return True
283
435 - def handle_notification(self, notification_type: EEPNotificationType):
436 - assert self.worker_type != "new"
437 - assert self.old_dp_store is not None
438 - if (
439 - notification_type == EEPNotificationType.NEW_CORE_ENGINES_INIT_READY
440 - and self.state == ScaleUpExistingEngineState.WAIT_NEW_CORE_ENGINES_INIT
441 - ):
442 - self.old_dp_store.add("eep_barrier_engine_count", 1)
443 - self.state = ScaleUpExistingEngineState.CREATE_STANDBY_GROUPS
444 - elif (
445 - notification_type == EEPNotificationType.NEW_CORE_ENGINES_WEIGHTS_INIT_READY
446 - and self.state
447 - == ScaleUpExistingEngineState.WAIT_NEW_CORE_ENGINES_WEIGHTS_INIT
448 - ):
449 - self.old_dp_store.add("eep_barrier_engine_count", 1)
450 - self.state = ScaleUpExistingEngineState.TRANSFER_WEIGHTS
284 + def is_ready_for_switch(self) -> bool:
285 + return self.worker_type == "existing" and (
286 + self.state is ScaleUpExistingEngineState.COMMIT_SCALE_UP
287 + or self.state is ScaleDownRemainingEngineState.COMMIT_SCALE_DOWN
288 + )
289 +
290 + @property
291 + def ready_key(self) -> str:
292 + return f"eep_ready/{self.engine_core.dp_rank}"
293 +
294 + def _mark_ready_for_switch(self) -> None:
295 + parallel_config = self.new_parallel_config
296 + get_cached_tcp_store_client(
297 + parallel_config.data_parallel_master_ip,
298 + parallel_config._coord_store_port,
299 + ).set(self.ready_key, b"1")
300
301 def is_complete(self) -> bool:
302 if self.scale_type == "scale_up":
@@ -462,50 +311,78 @@ class ElasticEPScalingState:
311 else self.state == ScaleDownRemainingEngineState.COMPLETE
312 )
313
465 - def _create_standby_groups(self):
314 + def _init_new_dp_group(self) -> tuple[Any, Any]:
315 + return self.new_parallel_config.stateless_init_dp_group(return_store=True)
316 +
317 + def _ensure_new_dp_group(self) -> bool:
318 + if self.new_dp_group is not None:
319 + return True
320 +
321 + if self._prepare_future is None:
322 + self._prepare_future = self._prepare_executor.submit(
323 + self._init_new_dp_group
324 + )
325 + if not self._prepare_future.done():
326 + return False
327 +
328 + self.new_dp_group, self.new_dp_store = self._prepare_future.result()
329 + self._prepare_future = None
330 + return True
331 +
332 + def _create_standby_groups(self) -> bool:
333 assert self.old_dp_group is not None
467 - self.new_dp_group, self.new_dp_store = (
468 - self.new_parallel_config.stateless_init_dp_group(return_store=True)
469 - )
470 - self.model_executor.collective_rpc(
471 - "elastic_ep_execute", args=("create_standby_groups", self.reconfig_request)
472 - )
334 + if not self._ensure_new_dp_group():
335 + return False
336 + if not self._execute_async(
337 + "create_standby_groups",
338 + self.reconfig_request,
339 + self.new_parallel_config.use_all2all,
340 + ):
341 + return False
342 if self.old_dp_group.rank() == 0:
343 logger.info("[Elastic EP] Created standby communication groups")
344 + return True
345
476 - def _transfer_weights(self):
346 + def _transfer_weights(self) -> bool:
347 assert self.reconfig_request is not None and self.old_dp_group is not None
348 old_dp_size = self.old_dp_group.size()
349 new_dp_size = self.reconfig_request.new_data_parallel_size
350
481 - self.model_executor.collective_rpc(
482 - "elastic_ep_execute", args=("transfer_weights", old_dp_size, new_dp_size)
483 - )
351 + if not self._execute_async("transfer_weights", old_dp_size, new_dp_size):
352 + return False
353 if self.old_dp_group.rank() == 0:
354 logger.info("[Elastic EP] Transferred weights to new workers")
355 + return True
356
487 - def _transfer_expert_mapping(self):
488 - assert self.old_dp_group is not None
489 - self.model_executor.collective_rpc(
490 - "elastic_ep_execute", args=("broadcast_expert_mapping",)
491 - )
492 - if self.old_dp_group.rank() == 0:
493 - logger.info("[Elastic EP] Broadcasted expert mapping to new workers")
494 -
495 - def _sync_kv_cache_memory_size(self):
357 + def _sync_kv_cache_memory_size(self) -> bool:
358 assert self.engine_core.available_gpu_memory_for_kv_cache > 0
359 assert self.new_dp_group is not None and self.old_dp_group is not None
498 - ParallelConfig.sync_kv_cache_memory_size(
499 - self.new_dp_group,
500 - self.engine_core.available_gpu_memory_for_kv_cache,
501 - )
360 +
361 + if self._new_dp_sync is None:
362 + tensor = torch.tensor(
363 + [self.engine_core.available_gpu_memory_for_kv_cache],
364 + dtype=torch.int64,
365 + device="cpu",
366 + )
367 + work = torch.distributed.all_reduce(
368 + tensor,
369 + op=torch.distributed.ReduceOp.MIN,
370 + group=self.new_dp_group,
371 + async_op=True,
372 + )
373 + self._new_dp_sync = (tensor, work)
374 + return False
375 +
376 + _, work = self._new_dp_sync
377 + if not work.is_completed():
378 + return False
379 + work.wait()
380 + self._new_dp_sync = None
381 if self.old_dp_group.rank() == 0:
382 logger.info("[Elastic EP] Synced KV cache memory size to new workers")
383 + return True
384
505 - def _switch_and_prepare(self):
506 - self.model_executor.collective_rpc(
507 - "elastic_ep_execute", args=("switch_and_prepare",)
508 - )
385 + def _commit_new_dp_group(self):
386 old_dp_group = self.old_dp_group
387 stateless_destroy_torch_distributed_process_group(old_dp_group)
388 assert self.new_dp_group is not None
@@ -529,41 +406,28 @@ class ElasticEPScalingState:
406 self.engine_core.current_wave = int(data[1])
407 self.engine_core.step_counter = int(data[2])
408 if new_dp_group.rank() == 0:
532 - self.engine_core._eep_send_engine_core_notification(
533 - EEPNotificationType.RECONFIGURE_FINISHED
534 - )
409 logger.info("[Elastic EP] Switched to new setup")
410
537 - def _eplb_reshuffle(self):
538 - self.model_executor.collective_rpc(
539 - "elastic_ep_execute", args=("perform_eplb_reshuffle",)
540 - )
541 - # Reshuffle changes per-rank token routing; the locked MoE workspace
542 - # may now be too small. Rewarm covers both new and existing engines.
543 - self.model_executor.collective_rpc(
544 - "elastic_ep_execute", args=("rewarm_workspace",)
545 - )
411 + def _send_reconfigure_finished(self):
412 assert self.new_dp_group is not None
413 if self.new_dp_group.rank() == 0:
548 - logger.info("[Elastic EP] EPLB reshuffle completed")
414 + self.engine_core._eep_send_engine_core_notification(
415 + EEPNotificationType.RECONFIGURE_FINISHED
416 + )
417
550 - def _eplb_reshuffle_before_scale_down(self):
418 + def _commit_scale_down(self, removing: bool):
419 assert self.reconfig_request is not None and self.old_dp_group is not None
552 - self.model_executor.collective_rpc(
420 + self._collective_rpc(
421 "elastic_ep_execute",
422 args=(
555 - "perform_scale_down_eplb_reshuffle",
423 + "commit_scale_down",
424 self.reconfig_request.new_data_parallel_size,
425 + removing,
426 ),
427 )
428 if self.old_dp_group.rank() == 0:
429 logger.info("[Elastic EP] EPLB reshuffle completed")
430
562 - def _switch_and_remove(self):
563 - self.model_executor.collective_rpc(
564 - "elastic_ep_execute", args=("switch_and_remove",)
565 - )
566 -
431 def _update_parallel_config(self):
432 assert self.reconfig_request is not None
433 reconfig_request = self.reconfig_request
vllm/distributed/elastic_ep/standby_state.py
+2 -1
@@ -39,6 +39,7 @@ def create_standby_groups(
39 new_world_size_across_dp: int,
40 master_ip: str,
41 coord_store_port: int,
42 + use_all2all: bool,
43 enable_eplb: bool = True,
44 backend: str | None = None,
45 ) -> None:
@@ -86,7 +87,7 @@ def create_standby_groups(
87 )
88 standby_ep_ranks = [x.tolist() for x in standby_ep_ranks]
89 _STANDBY_EP = _init_stateless_group(
89 - standby_ep_ranks, "ep", master_ip, backend, coord_store=coord_store
90 + standby_ep_ranks, "ep", master_ip, backend, coord_store, use_all2all=use_all2all
91 )
92
93 if enable_eplb:
vllm/distributed/parallel_state.py
+13 -1
@@ -414,6 +414,7 @@ class GroupCoordinator:
414 use_device_communicator: bool, # whether to use device communicator
415 use_message_queue_broadcaster: bool = False,
416 group_name: str | None = None,
417 + use_all2all: bool = False,
418 ):
419 group_name = group_name or "anonymous"
420 self.unique_name = _get_unique_name(group_name)
@@ -508,6 +509,7 @@ class GroupCoordinator:
509 device=self.device,
510 device_group=self.device_group,
511 unique_name=self.unique_name,
512 + use_all2all=use_all2all,
513 )
514
515 from vllm.distributed.device_communicators.shm_broadcast import MessageQueue
@@ -1321,6 +1323,7 @@ def init_model_parallel_group(
1323 use_message_queue_broadcaster: bool = False,
1324 group_name: str | None = None,
1325 use_device_communicator: bool = True,
1326 + use_all2all: bool = False,
1327 ) -> GroupCoordinator:
1328 return GroupCoordinator(
1329 group_ranks=group_ranks,
@@ -1329,6 +1332,7 @@ def init_model_parallel_group(
1332 use_device_communicator=use_device_communicator,
1333 use_message_queue_broadcaster=use_message_queue_broadcaster,
1334 group_name=group_name,
1335 + use_all2all=use_all2all,
1336 )
1337
1338
@@ -1339,6 +1343,7 @@ def _init_stateless_group(
1343 backend: str,
1344 coord_store: Store,
1345 use_device_communicator: bool = True,
1346 + use_all2all: bool = False,
1347 ) -> "StatelessGroupCoordinator":
1348 """Create a StatelessGroupCoordinator with the given parameters."""
1349 from vllm.distributed.stateless_coordinator import StatelessGroupCoordinator
@@ -1354,6 +1359,7 @@ def _init_stateless_group(
1359 coord_store=coord_store,
1360 global_rank=world.rank,
1361 global_world_size=world.world_size,
1362 + use_all2all=use_all2all,
1363 )
1364
1365
@@ -1924,6 +1930,7 @@ def initialize_model_parallel(
1930 .unbind(0)
1931 )
1932 group_ranks = [x.tolist() for x in group_ranks]
1933 + use_all2all = parallel_config.use_all2all
1934 if enable_elastic_ep:
1935 _EP = _init_stateless_group(
1936 group_ranks,
@@ -1931,10 +1938,15 @@ def initialize_model_parallel(
1938 parallel_config.data_parallel_master_ip,
1939 backend,
1940 coord_store=coord_store,
1941 + use_all2all=use_all2all,
1942 )
1943 else:
1944 _EP = init_model_parallel_group(
1937 - group_ranks, get_world_group().local_rank, backend, group_name="ep"
1945 + group_ranks,
1946 + get_world_group().local_rank,
1947 + backend,
1948 + group_name="ep",
1949 + use_all2all=use_all2all,
1950 )
1951
1952 # Create EPLB group with the same ranks as EP if EPLB is enabled.
vllm/distributed/stateless_coordinator.py
+2
@@ -79,6 +79,7 @@ class StatelessGroupCoordinator(GroupCoordinator):
79 host: str = "127.0.0.1",
80 global_rank: int = 0,
81 global_world_size: int = 1,
82 + use_all2all: bool = False,
83 ):
84 group_name = group_name or "anonymous"
85 self.unique_name = _get_unique_name(group_name)
@@ -191,6 +192,7 @@ class StatelessGroupCoordinator(GroupCoordinator):
192 global_ranks=self.ranks,
193 global_world_size=global_world_size,
194 tcp_store_group=self.tcp_store_group,
195 + use_all2all=use_all2all,
196 )
197
198 self.mq_broadcaster = None
vllm/entrypoints/serve/elastic_ep/api_router.py
+1 -8
@@ -12,10 +12,7 @@ from vllm.engine.protocol import EngineClient
12 from vllm.entrypoints.openai.engine.protocol import (
13 ErrorResponse,
14 )
15 -from vllm.entrypoints.serve.elastic_ep.middleware import (
16 - get_scaling_elastic_ep,
17 - set_scaling_elastic_ep,
18 -)
15 +from vllm.entrypoints.serve.elastic_ep.middleware import get_scaling_elastic_ep
16 from vllm.entrypoints.serve.utils.api_utils import validate_json_request
17 from vllm.logger import init_logger
18
@@ -64,8 +61,6 @@ async def scale_elastic_ep(raw_request: Request):
61 status_code=400, detail="drain_timeout must be a positive integer"
62 )
63
67 - # Set scaling flag to prevent new requests
68 - set_scaling_elastic_ep(True)
64 client = engine_client(raw_request)
65 try:
66 await client.scale_elastic_ep(new_data_parallel_size, drain_timeout)
@@ -83,8 +78,6 @@ async def scale_elastic_ep(raw_request: Request):
78 except Exception as e:
79 logger.error("Scale failed: %s", e)
80 raise HTTPException(status_code=500, detail="Scale failed") from e
86 - finally:
87 - set_scaling_elastic_ep(False)
81
82
83 @router.post("/is_scaling_elastic_ep")
vllm/model_executor/warmup/kernel_warmup.py
+17 -14
@@ -93,7 +93,7 @@ def _warmup_ll_bf16_router_gemm(model: torch.nn.Module) -> None:
93 )
94
95
96 -def kernel_warmup(worker: "Worker"):
96 +def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
97 from vllm.model_executor.warmup.minimax_m3_msa_warmup import (
98 minimax_m3_msa_warmup,
99 )
@@ -118,6 +118,22 @@ def kernel_warmup(worker: "Worker"):
118 )
119
120 # Run next so input-prep kernels JIT against pristine runner state.
121 + if worker.vllm_config.kernel_config.enable_jit_warmup:
122 + fa4_cutedsl_warmup(worker)
123 + sparse_mla_triton_warmup(worker)
124 +
125 + if current_platform.has_device_capability(90):
126 + _warmup_ll_bf16_router_gemm(worker.get_model())
127 +
128 + if worker.vllm_config.kernel_config.enable_cutedsl_warmup:
129 + # TODO(roberto): Remove after registered CuTeDSL warmups are migrated
130 + # to the shared JIT warmup infrastructure.
131 + # https://github.com/vllm-project/vllm/pull/47451
132 + cutedsl_warmup()
133 +
134 + if process_local_only:
135 + return
136 +
137 flashinfer_sparse_mla_decode_autotune_warmup(worker)
138 deepseek_v4_sparse_mla_attention_warmup(worker)
139
@@ -143,9 +159,6 @@ def kernel_warmup(worker: "Worker"):
159 elif has_flashinfer() and current_platform.has_device_capability(90):
160 flashinfer_autotune(worker.model_runner)
161
146 - if current_platform.has_device_capability(90):
147 - _warmup_ll_bf16_router_gemm(worker.get_model())
148 -
162 # FlashInfer attention warmup
163 # Only warmup if the model has FlashInfer attention groups
164 # and is not a pooling model
@@ -178,16 +191,6 @@ def kernel_warmup(worker: "Worker"):
191 create_mixed_batch=True,
192 )
193
181 - if worker.vllm_config.kernel_config.enable_cutedsl_warmup:
182 - # TODO(roberto): Remove after registered CuTeDSL warmups are migrated
183 - # to the shared JIT warmup infrastructure.
184 - # https://github.com/vllm-project/vllm/pull/47451
185 - cutedsl_warmup()
186 -
187 - if worker.vllm_config.kernel_config.enable_jit_warmup:
188 - fa4_cutedsl_warmup(worker)
189 - sparse_mla_triton_warmup(worker)
190 -
194
195 def _flashinfer_autotune_skip_ops(runner: "GPUModelRunner") -> set[str] | None:
196 if envs.VLLM_FLASHINFER_AUTOTUNE_SKIP_OPS is not None:
vllm/v1/engine/__init__.py
-2
@@ -35,8 +35,6 @@ FT_STATUS_CALL_ID = -2
35
36
37 class EEPNotificationType(enum.Enum):
38 - NEW_CORE_ENGINES_INIT_READY = "NEW_CORE_ENGINES_INIT_READY"
39 - NEW_CORE_ENGINES_WEIGHTS_INIT_READY = "NEW_CORE_ENGINES_WEIGHTS_INIT_READY"
38 RECONFIGURE_FINISHED = "RECONFIGURE_FINISHED"
39 SHUTDOWN_COMPLETE = "SHUTDOWN_COMPLETE"
40
vllm/v1/engine/async_llm.py
+26 -19
@@ -109,6 +109,7 @@ class AsyncLLM(EngineClient):
109 maybe_register_config_serialize_by_value()
110
111 self.vllm_config = vllm_config
112 + self._elastic_ep_lock = asyncio.Lock()
113 self.model_config = vllm_config.model_config
114 self.observability_config = vllm_config.observability_config
115
@@ -998,17 +999,27 @@ class AsyncLLM(EngineClient):
999 "waiting for requests to drain."
1000 )
1001
1002 + async def _drain_requests_for_elastic_ep(self, drain_timeout: int) -> None:
1003 + try:
1004 + logger.info(
1005 + "VLLM_ELASTIC_EP_DRAIN_REQUESTS is set, "
1006 + "waiting for requests to drain before scaling"
1007 + )
1008 + await self.wait_for_requests_to_drain(drain_timeout)
1009 + except BaseException:
1010 + set_scaling_elastic_ep(False)
1011 + raise
1012 +
1013 async def scale_elastic_ep(
1014 self, new_data_parallel_size: int, drain_timeout: int = 300
1015 ):
1004 - """
1005 - Scale up or down the data parallel size by adding or removing
1006 - engine cores.
1007 - Args:
1008 - new_data_parallel_size: The new number of data parallel workers
1009 - drain_timeout:
1010 - Maximum time to wait for requests to drain (seconds)
1011 - """
1016 + """Scale the elastic EP data parallel size."""
1017 + async with self._elastic_ep_lock:
1018 + await self._scale_elastic_ep(new_data_parallel_size, drain_timeout)
1019 +
1020 + async def _scale_elastic_ep(
1021 + self, new_data_parallel_size: int, drain_timeout: int
1022 + ) -> None:
1023 old_data_parallel_size = self.vllm_config.parallel_config.data_parallel_size
1024 if old_data_parallel_size == new_data_parallel_size:
1025 logger.info(
@@ -1017,12 +1028,7 @@ class AsyncLLM(EngineClient):
1028 )
1029 return
1030
1020 - if envs.VLLM_ELASTIC_EP_DRAIN_REQUESTS:
1021 - logger.info(
1022 - "VLLM_ELASTIC_EP_DRAIN_REQUESTS is set, "
1023 - "waiting for requests to drain before scaling"
1024 - )
1025 - await self.wait_for_requests_to_drain(drain_timeout)
1031 + await self.engine_core.prepare_elastic_ep(new_data_parallel_size)
1032
1033 # recreate stat loggers
1034 if new_data_parallel_size > old_data_parallel_size and self.log_stats:
@@ -1042,11 +1048,12 @@ class AsyncLLM(EngineClient):
1048 self.logger_manager.log_engine_initialized()
1049
1050 set_scaling_elastic_ep(True)
1045 - try:
1046 - await self.engine_core.scale_elastic_ep(new_data_parallel_size)
1047 - self.vllm_config.parallel_config.data_parallel_size = new_data_parallel_size
1048 - finally:
1049 - set_scaling_elastic_ep(False)
1051 + if envs.VLLM_ELASTIC_EP_DRAIN_REQUESTS:
1052 + await self._drain_requests_for_elastic_ep(drain_timeout)
1053 +
1054 + await self.engine_core.commit_elastic_ep()
1055 + self.vllm_config.parallel_config.data_parallel_size = new_data_parallel_size
1056 + set_scaling_elastic_ep(False)
1057
1058 async def handle_fault(
1059 self, fault_tolerance_request: FaultToleranceRequest
vllm/v1/engine/coordinator.py
+3
@@ -376,6 +376,9 @@ class DPCoordinatorProc:
376 eng_index = outputs.engine_index
377 scheduler_stats = outputs.scheduler_stats
378 if scheduler_stats:
379 + # Elastic EP stats may arrive while the engine list changes.
380 + if eng_index >= len(self.engines):
381 + continue
382 # 1. Updated request load stats - update our local
383 # state with these.
384 stats = self.engines[eng_index].request_counts
vllm/v1/engine/core.py
+36 -39
@@ -327,8 +327,9 @@ class EngineCore:
327
328 vllm_config.validate_block_size()
329
330 - # Initialize kv cache and warmup the execution
330 self.model_executor.initialize_from_config(kv_cache_configs)
331 + if not envs.VLLM_ELASTIC_EP_SCALE_UP_LAUNCH:
332 + self.model_executor.compile_or_warm_up_model()
333
334 elapsed = time.time() - start
335 compile_time = vllm_config.compilation_config.compilation_time
@@ -993,9 +994,7 @@ class EngineCore:
994 raise NotImplementedError
995
996 def _eep_send_engine_core_notification(
996 - self,
997 - notification_type: EEPNotificationType,
998 - vllm_config: VllmConfig | None = None,
997 + self, notification_type: EEPNotificationType
998 ):
999 raise NotImplementedError
1000
@@ -1070,11 +1069,6 @@ class EngineCoreProc(EngineCore):
1069
1070 self.addresses = addresses
1071 self.process_input_queue_block = True
1073 - if envs.VLLM_ELASTIC_EP_SCALE_UP_LAUNCH:
1074 - self._eep_send_engine_core_notification(
1075 - EEPNotificationType.NEW_CORE_ENGINES_INIT_READY,
1076 - vllm_config=vllm_config,
1077 - )
1072 self._init_data_parallel(vllm_config)
1073
1074 super().__init__(
@@ -2096,12 +2090,16 @@ class DPEngineCoreProc(EngineCoreProc):
2090 self._maybe_publish_request_counts()
2091
2092 if self.eep_scaling_state is not None:
2099 - _ = self.eep_scaling_state.progress()
2100 - if self.eep_scaling_state.is_complete():
2101 - if self.eep_scaling_state.worker_type == "removing":
2093 + state = self.eep_scaling_state
2094 + if state.commit_requested or not state.is_ready_for_switch():
2095 + state.progress()
2096 + if state.is_complete():
2097 + if state.worker_type == "removing":
2098 raise SystemExit
2099 self.process_input_queue_block = True
2100 self.eep_scaling_state = None
2101 + elif not state.commit_requested and state.is_ready_for_switch():
2102 + self.process_input_queue_block = True
2103
2104 executed = self._process_engine_step()
2105 self._maybe_publish_request_counts()
@@ -2171,7 +2169,7 @@ class DPEngineCoreProc(EngineCoreProc):
2169
2170 def reinitialize_distributed(
2171 self, reconfig_request: ReconfigureDistributedRequest
2174 - ) -> None:
2172 + ) -> str:
2173 from copy import deepcopy
2174
2175 from vllm.distributed.elastic_ep.elastic_state import ElasticEPScalingState
@@ -2203,7 +2201,10 @@ class DPEngineCoreProc(EngineCoreProc):
2201 == ReconfigureRankType.SHUTDOWN_CURRENT_RANK
2202 )
2203
2206 - self.eep_scaling_state = ElasticEPScalingState(
2204 + if self.eep_scaling_state is not None:
2205 + raise RuntimeError("Elastic EP reconfiguration is already active")
2206 +
2207 + state = ElasticEPScalingState(
2208 model_executor=self.model_executor,
2209 engine_core=self,
2210 vllm_config=self.vllm_config,
@@ -2212,30 +2213,34 @@ class DPEngineCoreProc(EngineCoreProc):
2213 scale_type="scale_down" if is_scale_down else "scale_up",
2214 reconfig_request=reconfig_request,
2215 )
2216 + self.eep_scaling_state = state
2217 +
2218 self.process_input_queue_block = False
2219 logger.info(
2220 "[Elastic EP] Received reconfiguration request and starting scaling up/down"
2221 )
2222 + return state.ready_key
2223 +
2224 + def commit_prepared_elastic_ep(self) -> None:
2225 + state = self.eep_scaling_state
2226 + if state is None or state.commit_requested or not state.is_ready_for_switch():
2227 + raise RuntimeError("No prepared Elastic EP reconfiguration is ready")
2228 + state.commit_requested = True
2229 + self.process_input_queue_block = False
2230 + logger.info("[Elastic EP] Committing prepared reconfiguration")
2231
2232 def _eep_send_engine_core_notification(
2221 - self,
2222 - notification_type: EEPNotificationType,
2223 - vllm_config: VllmConfig | None = None,
2233 + self, notification_type: EEPNotificationType
2234 ):
2235 """
2236 Send notifications to EngineCoreClient, which can then forward
2237 the notifications to other engine core processes. It is used for:
2228 - 1) In scale up: new core engines to notify existing core engines
2229 - that they are ready;
2230 - 2) In scale down: removing core engines to notify EngineCoreClient
2238 + 1) In scale down: removing core engines to notify EngineCoreClient
2239 so EngineCoreClient can release their ray placement groups;
2232 - 3) Both scale up/down: to notify EngineCoreClient that existing
2240 + 2) Both scale up/down: to notify EngineCoreClient that existing
2241 core engines have already switched to the new parallel setup.
2242 """
2235 - if vllm_config is None:
2236 - dp_rank = self.vllm_config.parallel_config.data_parallel_rank
2237 - else:
2238 - dp_rank = vllm_config.parallel_config.data_parallel_rank
2243 + dp_rank = self.vllm_config.parallel_config.data_parallel_rank
2244 notification_data = (notification_type.value, dp_rank)
2245 outputs = EngineCoreOutputs(
2246 utility_output=UtilityOutput(
@@ -2257,22 +2262,11 @@ class DPEngineCoreProc(EngineCoreProc):
2262 ):
2263 socket.send_multipart(encoder.encode(outputs))
2264
2260 - def eep_handle_engine_core_notification(
2261 - self, notification_type: str | EEPNotificationType
2262 - ):
2263 - """
2264 - Handle notification received from EngineCoreClient
2265 - (forwarded from new core engines).
2266 - """
2267 - assert self.eep_scaling_state is not None
2268 - if isinstance(notification_type, str):
2269 - notification_type = EEPNotificationType(notification_type)
2270 - self.eep_scaling_state.handle_notification(notification_type)
2271 -
2265 def _eep_scale_up_before_kv_init(self):
2266 from vllm.distributed.elastic_ep.elastic_state import ElasticEPScalingState
2267
2275 - self.eep_scaling_state = ElasticEPScalingState(
2268 + self.ignore_start_dp_wave = True
2269 + state = ElasticEPScalingState(
2270 model_executor=self.model_executor,
2271 engine_core=self,
2272 vllm_config=self.vllm_config,
@@ -2281,7 +2275,10 @@ class DPEngineCoreProc(EngineCoreProc):
2275 scale_type="scale_up",
2276 reconfig_request=None,
2277 )
2284 - self.eep_scaling_state.run_pre_kv_init_states()
2278 + if self.eep_scaling_state is not None:
2279 + raise RuntimeError("Elastic EP reconfiguration is already active")
2280 + self.eep_scaling_state = state
2281 + state.run_pre_kv_init_states()
2282 self.process_input_queue_block = False
2283
2284
vllm/v1/engine/core_client.py
+175 -137
@@ -225,7 +225,10 @@ class EngineCoreClient(ABC):
225 running state."""
226 raise NotImplementedError
227
228 - async def scale_elastic_ep(self, new_data_parallel_size: int) -> None:
228 + async def commit_elastic_ep(self) -> None:
229 + raise NotImplementedError
230 +
231 + async def prepare_elastic_ep(self, new_data_parallel_size: int) -> None:
232 raise NotImplementedError
233
234 async def get_output_async(self) -> EngineCoreOutputs:
@@ -1490,6 +1493,7 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1493 )
1494
1495 assert len(self.core_engines) > 1
1496 + self._prepared_elastic_ep: tuple[int, int] | None = None
1497
1498 self.eng_start_index = (
1499 len(self.core_engines) * self.client_index
@@ -1603,31 +1607,16 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1607 if len(cache.pending_notifications[notification_type]) >= abs(
1608 cache.num_new_core_engines
1609 ):
1606 - if notification_type == EEPNotificationType.SHUTDOWN_COMPLETE:
1607 - assert isinstance(self.resources.engine_manager, CoreEngineActorManager)
1608 - assert cache.num_new_core_engines < 0
1609 - old_dp_size = len(cache.existing_core_engines)
1610 - new_dp_size = old_dp_size + cache.num_new_core_engines
1611 - self.resources.engine_manager.scale_down_elastic_ep(
1612 - old_dp_size, new_dp_size
1613 - )
1614 - else:
1615 - await asyncio.gather(
1616 - *[
1617 - self._call_utility_async(
1618 - "eep_handle_engine_core_notification",
1619 - notification_type,
1620 - engine=engine,
1621 - )
1622 - for engine in cache.existing_core_engines
1623 - ]
1624 - )
1625 - cache.pending_notifications[notification_type] = set()
1626 - if notification_type in [
1627 - EEPNotificationType.SHUTDOWN_COMPLETE,
1628 - EEPNotificationType.NEW_CORE_ENGINES_WEIGHTS_INIT_READY,
1629 - ]:
1630 - self.eep_scaling_cache = None
1610 + engine_manager = self.resources.engine_manager
1611 + assert isinstance(engine_manager, CoreEngineActorManager)
1612 + assert cache.num_new_core_engines < 0
1613 + old_dp_size = len(cache.existing_core_engines)
1614 + new_dp_size = old_dp_size + cache.num_new_core_engines
1615 + engine_manager.scale_down_elastic_ep(old_dp_size, new_dp_size)
1616 + self.vllm_config.parallel_config.data_parallel_size_local = len(
1617 + engine_manager.local_engine_actors
1618 + )
1619 + self.eep_scaling_cache = None
1620
1621 async def abort_requests_async(self, request_ids: list[str]) -> None:
1622 if not request_ids or self.resources.engine_dead:
@@ -1651,31 +1640,46 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1640 ) -> None:
1641 await self._send_input(EngineCoreRequestType.ABORT, request_ids, engine)
1642
1654 - async def scale_elastic_ep(self, new_data_parallel_size: int) -> None:
1655 - """Scale elastic EP data parallel size"""
1643 + async def commit_elastic_ep(self) -> None:
1644 + """Commit prepared elastic EP scaling."""
1645 + prepared = self._prepared_elastic_ep
1646 + if prepared is None:
1647 + raise RuntimeError("Elastic EP scaling has not been prepared")
1648 + new_data_parallel_size, num_redundant_experts = prepared
1649 cur_data_parallel_size = len(self.core_engines)
1657 -
1658 - assert new_data_parallel_size != cur_data_parallel_size, (
1659 - f"new_data_parallel_size {new_data_parallel_size} must be "
1660 - f"different from cur_data_parallel_size {cur_data_parallel_size}"
1650 + if new_data_parallel_size > cur_data_parallel_size:
1651 + await self._commit_scale_up_elastic_ep(new_data_parallel_size)
1652 + else:
1653 + await self._commit_scale_down_elastic_ep(new_data_parallel_size)
1654 + self.vllm_config.parallel_config.eplb_config.num_redundant_experts = (
1655 + num_redundant_experts
1656 )
1657 + self._prepared_elastic_ep = None
1658
1659 + async def prepare_elastic_ep(self, new_data_parallel_size: int) -> None:
1660 + """Prepare elastic EP scaling without routing requests to new engines."""
1661 + if (prepared := self._prepared_elastic_ep) is not None:
1662 + if prepared[0] == new_data_parallel_size:
1663 + return
1664 + raise RuntimeError("Elastic EP scaling is already prepared")
1665 + cur_data_parallel_size = len(self.core_engines)
1666 assert self.vllm_config.parallel_config.data_parallel_backend == "ray", (
1667 "Only ray DP backend supports scaling elastic EP"
1668 )
1666 -
1667 - scale_up = new_data_parallel_size > cur_data_parallel_size
1668 -
1669 - if scale_up:
1670 - await self._scale_up_elastic_ep(
1671 - cur_data_parallel_size, new_data_parallel_size
1672 - )
1669 + parallel_config = self.vllm_config.parallel_config
1670 + num_experts = self.vllm_config.model_config.get_num_experts()
1671 + num_redundant_experts = (
1672 + num_experts + parallel_config.eplb_config.num_redundant_experts
1673 + ) * new_data_parallel_size // cur_data_parallel_size - num_experts
1674 + if new_data_parallel_size < cur_data_parallel_size:
1675 + await self._prepare_scale_down_elastic_ep(new_data_parallel_size)
1676 else:
1674 - await self._scale_down_elastic_ep(
1675 - cur_data_parallel_size, new_data_parallel_size
1677 + await self._prepare_scale_up_elastic_ep(
1678 + new_data_parallel_size, num_redundant_experts
1679 )
1680 + self._prepared_elastic_ep = new_data_parallel_size, num_redundant_experts
1681
1678 - async def _eep_wait_for_setup_switch_complete(self) -> None:
1682 + def _eep_wait_for_setup_switch_complete(self) -> asyncio.Future:
1683 """
1684 Wait for core engines to switch to the new setup.
1685
@@ -1687,9 +1691,26 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1691 future = asyncio.get_running_loop().create_future()
1692 self.utility_results[EEP_NOTIFICATION_CALL_ID] = future
1693 self._ensure_output_queue_task()
1690 - await future
1694 + return future
1695 +
1696 + def _wait_for_new_engine_ready(self, new_core_engines: list[bytes]) -> None:
1697 + new_engine_identities = set(new_core_engines)
1698 + sync_input_socket = zmq.Socket.shadow(self.input_socket)
1699 + while new_engine_identities:
1700 + if not sync_input_socket.poll(timeout=VLLM_ENGINE_READY_TIMEOUT_S * 1000):
1701 + raise TimeoutError(
1702 + f"Timed out waiting for new engine core processes to "
1703 + f"start. Waited "
1704 + f"{VLLM_ENGINE_READY_TIMEOUT_S}s (configured by "
1705 + f"VLLM_ENGINE_READY_TIMEOUT_S). To increase the "
1706 + f"timeout, set the environment variable: "
1707 + f"VLLM_ENGINE_READY_TIMEOUT_S=<seconds>"
1708 + )
1709 + identity, payload = sync_input_socket.recv_multipart()
1710 + new_engine_identities.discard(identity)
1711 + self._apply_ready_response(payload)
1712
1692 - def _setup_elastic_ep_reconfig_bootstrap(self) -> tuple[str, int]:
1713 + def _setup_elastic_ep_reconfig_bootstrap(self) -> None:
1714 from vllm.distributed.utils import create_tcp_store
1715 from vllm.utils.network_utils import get_open_ports_list
1716
@@ -1709,36 +1730,36 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1730 )
1731 parallel_config._coord_store_port = store.port
1732 self._coord_store = store
1712 - return ip, store.port
1733
1714 - async def _scale_up_elastic_ep(
1715 - self, cur_data_parallel_size: int, new_data_parallel_size: int
1716 - ) -> None:
1717 - """Scale up the data parallel size by creating new engine cores
1718 - and reconfiguring existing ones."""
1719 - cur_data_parallel_size = len(self.core_engines)
1720 -
1721 - self.eep_scaling_cache = ElasticScalingCache(
1722 - existing_core_engines=self.core_engines.copy(),
1723 - num_new_core_engines=new_data_parallel_size - cur_data_parallel_size,
1724 - pending_notifications=dict(),
1734 + def _make_reconfig_request(
1735 + self,
1736 + new_data_parallel_size: int,
1737 + rank_type: ReconfigureRankType = ReconfigureRankType.KEEP_CURRENT_RANK,
1738 + ) -> ReconfigureDistributedRequest:
1739 + parallel_config = self.vllm_config.parallel_config
1740 + return ReconfigureDistributedRequest(
1741 + new_data_parallel_size=new_data_parallel_size,
1742 + new_data_parallel_rank=rank_type,
1743 + new_data_parallel_rank_local=ReconfigureRankType.KEEP_CURRENT_RANK,
1744 + new_data_parallel_master_ip=parallel_config.data_parallel_master_ip,
1745 + new_data_parallel_master_port=parallel_config.data_parallel_master_port,
1746 + new_data_parallel_master_port_list=parallel_config._data_parallel_master_port_list,
1747 + coord_store_port=parallel_config._coord_store_port,
1748 )
1749
1727 - parallel_config = self.vllm_config.parallel_config
1728 - ip, coord_store_port = self._setup_elastic_ep_reconfig_bootstrap()
1750 + async def _prepare_scale_up_elastic_ep(
1751 + self,
1752 + new_data_parallel_size: int,
1753 + num_redundant_experts: int,
1754 + ) -> None:
1755 + """Prepare scale up by creating new engine cores and reconfiguring
1756 + existing ones."""
1757 + self._setup_elastic_ep_reconfig_bootstrap()
1758
1759 # Phase 1: Send reconfig messages to existing engines
1760 reconfig_futures = []
1761 for engine in self.core_engines:
1733 - reconfig_request = ReconfigureDistributedRequest(
1734 - new_data_parallel_size=new_data_parallel_size,
1735 - new_data_parallel_rank=ReconfigureRankType.KEEP_CURRENT_RANK,
1736 - new_data_parallel_rank_local=ReconfigureRankType.KEEP_CURRENT_RANK,
1737 - new_data_parallel_master_ip=ip,
1738 - new_data_parallel_master_port=parallel_config.data_parallel_master_port,
1739 - new_data_parallel_master_port_list=parallel_config._data_parallel_master_port_list,
1740 - coord_store_port=coord_store_port,
1741 - )
1762 + reconfig_request = self._make_reconfig_request(new_data_parallel_size)
1763 coro = self._call_utility_async(
1764 "reinitialize_distributed", reconfig_request, engine=engine
1765 )
@@ -1746,51 +1767,54 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1767
1768 # Phase 2: Create new engines
1769 assert isinstance(self.resources.engine_manager, CoreEngineActorManager)
1749 - parallel_config.eplb_config.num_redundant_experts = 0
1770 start_new_worker_future = asyncio.to_thread(
1771 self.resources.engine_manager.scale_up_elastic_ep,
1772 self.vllm_config,
1773 new_data_parallel_size,
1774 + num_redundant_experts,
1775 )
1755 - wait_future = self._eep_wait_for_setup_switch_complete()
1776
1777 # Phase 3: Wait for new engines to be created
1778 # and reconfig messages to be received
1779 await asyncio.gather(start_new_worker_future, *reconfig_futures)
1780 + ready_keys = [future.result() for future in reconfig_futures]
1781 + ready_keys.extend(
1782 + f"eep_ready/{rank}"
1783 + for rank in range(len(self.core_engines), new_data_parallel_size)
1784 + )
1785 + await asyncio.to_thread(self._coord_store.wait, ready_keys)
1786 logger.info("[Elastic EP] Successfully started new engines")
1787
1762 - # Create new CoreEngine objects for the new engines
1763 - new_engine_identities = set()
1764 - for i in range(cur_data_parallel_size, new_data_parallel_size):
1765 - new_engine = i.to_bytes(2, "little")
1766 - self.core_engines.append(new_engine)
1767 - # NOTE(yongji): we don't update lb_engines here,
1768 - # we let run_engine_stats_update_task to update it.
1769 - new_engine_identities.add(new_engine)
1770 -
1771 - # Wait for ready messages from new engines on the input socket
1772 - sync_input_socket = zmq.Socket.shadow(self.input_socket)
1773 - while new_engine_identities:
1774 - if not sync_input_socket.poll(
1775 - timeout=VLLM_ENGINE_READY_TIMEOUT_S * 1000 # convert to ms
1776 - ):
1777 - raise TimeoutError(
1778 - f"Timed out waiting for new engine core processes to "
1779 - f"start. Waited "
1780 - f"{VLLM_ENGINE_READY_TIMEOUT_S}s (configured by "
1781 - f"VLLM_ENGINE_READY_TIMEOUT_S). To increase the "
1782 - f"timeout, set the environment variable: "
1783 - f"VLLM_ENGINE_READY_TIMEOUT_S=<seconds>"
1784 - )
1785 - identity, payload = sync_input_socket.recv_multipart()
1786 - new_engine_identities.discard(identity)
1787 - self._apply_ready_response(payload)
1788 + async def _commit_scale_up_elastic_ep(self, new_data_parallel_size: int) -> None:
1789 + new_core_engines = [
1790 + rank.to_bytes(2, "little")
1791 + for rank in range(len(self.core_engines), new_data_parallel_size)
1792 + ]
1793
1789 - # NOTE(yongji): Before we schedule any requests on the new workers,
1790 - # we should wait for them to switch to the new setup.
1791 - await wait_future
1794 + await self.pause_scheduler_async(mode="keep", clear_cache=False)
1795 + wait_future = self._eep_wait_for_setup_switch_complete()
1796 + finish_futures = [
1797 + asyncio.create_task(
1798 + self._call_utility_async("commit_prepared_elastic_ep", engine=engine)
1799 + )
1800 + for engine in self.core_engines
1801 + ]
1802 + try:
1803 + await asyncio.gather(*finish_futures)
1804 + await wait_future
1805 + self._wait_for_new_engine_ready(new_core_engines)
1806 + except Exception:
1807 + wait_future.cancel()
1808 + raise
1809 +
1810 + self.core_engines.extend(new_core_engines)
1811 # Update the parallel config
1793 - self.vllm_config.parallel_config.data_parallel_size = new_data_parallel_size
1812 + parallel_config = self.vllm_config.parallel_config
1813 + parallel_config.data_parallel_size = new_data_parallel_size
1814 + if isinstance(self.resources.engine_manager, CoreEngineActorManager):
1815 + parallel_config.data_parallel_size_local = len(
1816 + self.resources.engine_manager.local_engine_actors
1817 + )
1818 # Notify coordinator about scale up through existing
1819 # stats_update_task connection
1820 self._ensure_stats_update_task()
@@ -1803,10 +1827,23 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1827 "[Elastic EP] Scale up completed, new data parallel size: %s",
1828 new_data_parallel_size,
1829 )
1830 + await self.resume_scheduler_async()
1831
1807 - async def _scale_down_elastic_ep(
1808 - self, cur_data_parallel_size: int, new_data_parallel_size: int
1809 - ) -> None:
1832 + async def _prepare_scale_down_elastic_ep(self, new_data_parallel_size: int) -> None:
1833 + self._setup_elastic_ep_reconfig_bootstrap()
1834 +
1835 + reconfig_futures = []
1836 + for engine in self.core_engines[:new_data_parallel_size]:
1837 + reconfig_request = self._make_reconfig_request(new_data_parallel_size)
1838 + coro = self._call_utility_async(
1839 + "reinitialize_distributed", reconfig_request, engine=engine
1840 + )
1841 + reconfig_futures.append(asyncio.create_task(coro))
1842 +
1843 + ready_keys = await asyncio.gather(*reconfig_futures)
1844 + await asyncio.to_thread(self._coord_store.wait, ready_keys)
1845 +
1846 + async def _commit_scale_down_elastic_ep(self, new_data_parallel_size: int) -> None:
1847 """Scale down the data parallel size by shutting down and
1848 reconfiguring existing engine cores."""
1849 cur_data_parallel_size = len(self.core_engines)
@@ -1817,50 +1854,51 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1854 pending_notifications=dict(),
1855 )
1856
1820 - parallel_config = self.vllm_config.parallel_config
1821 - ip, coord_store_port = self._setup_elastic_ep_reconfig_bootstrap()
1822 -
1857 + old_core_engines = self.core_engines
1858 + # NOTE(yongji): Immediately stop sending requests to the removing engines.
1859 + self.core_engines = old_core_engines[:new_data_parallel_size]
1860 + self.lb_engines = self.lb_engines[:new_data_parallel_size]
1861 removed_dp_size = cur_data_parallel_size - new_data_parallel_size
1862 + pause_modes = ["keep"] * new_data_parallel_size + ["abort"] * removed_dp_size
1863 + pause_futures = [
1864 + self._call_utility_async("pause_scheduler", mode, False, engine=engine)
1865 + for mode, engine in zip(pause_modes, old_core_engines)
1866 + ]
1867 + await asyncio.gather(*pause_futures)
1868 assert isinstance(self.resources.engine_manager, CoreEngineActorManager)
1869 self.resources.engine_manager.remove_run_refs_for_scale_down(removed_dp_size)
1870 + wait_future = self._eep_wait_for_setup_switch_complete()
1871 reconfig_futures = []
1827 - for cur_dp_rank, engine in enumerate(self.core_engines):
1828 - reconfig_request = ReconfigureDistributedRequest(
1829 - new_data_parallel_size=new_data_parallel_size,
1830 - new_data_parallel_rank=ReconfigureRankType.KEEP_CURRENT_RANK,
1831 - new_data_parallel_rank_local=ReconfigureRankType.KEEP_CURRENT_RANK,
1832 - new_data_parallel_master_ip=ip,
1833 - new_data_parallel_master_port=parallel_config.data_parallel_master_port,
1834 - new_data_parallel_master_port_list=parallel_config._data_parallel_master_port_list,
1835 - coord_store_port=coord_store_port,
1836 - )
1837 - if cur_dp_rank >= new_data_parallel_size:
1838 - reconfig_request.new_data_parallel_rank = (
1839 - ReconfigureRankType.SHUTDOWN_CURRENT_RANK
1872 + for cur_dp_rank, engine in enumerate(old_core_engines):
1873 + if cur_dp_rank < new_data_parallel_size:
1874 + coro = self._call_utility_async(
1875 + "commit_prepared_elastic_ep", engine=engine
1876 + )
1877 + else:
1878 + reconfig_request = self._make_reconfig_request(
1879 + new_data_parallel_size,
1880 + ReconfigureRankType.SHUTDOWN_CURRENT_RANK,
1881 + )
1882 + coro = self._call_utility_async(
1883 + "reinitialize_distributed", reconfig_request, engine=engine
1884 )
1841 - coro = self._call_utility_async(
1842 - "reinitialize_distributed", reconfig_request, engine=engine
1843 - )
1885 reconfig_futures.append(asyncio.create_task(coro))
1886
1846 - # NOTE(yongji): Immediately stop sending requests to the removing engines.
1847 - self.core_engines = self.core_engines[:new_data_parallel_size]
1848 - self.lb_engines = self.lb_engines[:new_data_parallel_size]
1849 - wait_future = self._eep_wait_for_setup_switch_complete()
1850 -
1851 - await asyncio.gather(*reconfig_futures)
1887 + try:
1888 + await asyncio.gather(*reconfig_futures)
1889
1853 - self.vllm_config.parallel_config.data_parallel_size = new_data_parallel_size
1854 - self._ensure_stats_update_task()
1855 - scale_down_marker = msgspec.msgpack.encode(
1856 - ("SCALE_ELASTIC_EP", new_data_parallel_size)
1857 - )
1858 - await self.first_req_send_socket.send(scale_down_marker)
1890 + self.vllm_config.parallel_config.data_parallel_size = new_data_parallel_size
1891 + self._ensure_stats_update_task()
1892 + scale_down_marker = msgspec.msgpack.encode(
1893 + ("SCALE_ELASTIC_EP", new_data_parallel_size)
1894 + )
1895 + await self.first_req_send_socket.send(scale_down_marker)
1896 + await wait_future
1897 + await self.resume_scheduler_async()
1898 + except Exception:
1899 + wait_future.cancel()
1900 + raise
1901
1860 - # NOTE(yongji): Unlike scaling up,
1861 - # here we don't actually need to wait for the setup switch to complete.
1862 - # We may want to remove it in the future.
1863 - await wait_future
1902 logger.info(
1903 "[Elastic EP] Scale down completed, new data parallel size: %s",
1904 new_data_parallel_size,
vllm/v1/engine/utils.py
+8 -23
@@ -821,7 +821,10 @@ class CoreEngineActorManager:
821 return placement_groups, local_dp_ranks
822
823 def scale_up_elastic_ep(
824 - self, cur_vllm_config: VllmConfig, new_data_parallel_size: int
824 + self,
825 + cur_vllm_config: VllmConfig,
826 + new_data_parallel_size: int,
827 + num_redundant_experts: int,
828 ) -> None:
829 import copy
830
@@ -864,6 +867,9 @@ class CoreEngineActorManager:
867 if new_data_parallel_size > 1:
868 _apply_dp_identity_suffix(dp_vllm_config, rank)
869 dp_vllm_config.parallel_config.data_parallel_size = new_data_parallel_size
870 + dp_vllm_config.parallel_config.eplb_config.num_redundant_experts = (
871 + num_redundant_experts
872 + )
873 dp_vllm_config.parallel_config.placement_group = pg
874
875 # Check if this placement group is on the head node
@@ -906,39 +912,18 @@ class CoreEngineActorManager:
912 self.created_placement_groups.append(pg)
913 self.placement_group_is_local.append(local_client)
914
909 - ray.get(
910 - [
911 - actor.wait_for_init.remote()
912 - for actor in (
913 - self.local_engine_actors[-new_local_engines:]
914 - if new_local_engines > 0
915 - else []
916 - )
917 - + self.remote_engine_actors[
918 - -(len(placement_groups) - new_local_engines) :
919 - ]
920 - ]
921 - )
922 -
915 actors = (
916 self.local_engine_actors[-new_local_engines:]
917 if new_local_engines > 0
918 else []
919 ) + self.remote_engine_actors[-(len(placement_groups) - new_local_engines) :]
920
921 + ray.get([actor.wait_for_init.remote() for actor in actors])
922 for actor in actors:
923 ref = actor.run.remote()
924 self.run_refs.append(ref)
925 self.actor_run_ref_dict[actor] = ref
926
934 - cur_vllm_config.parallel_config.data_parallel_size = new_data_parallel_size
935 - # Update old_vllm_config with new data_parallel_size_local if any new
936 - # local engines were added
937 - if new_local_engines > 0:
938 - cur_vllm_config.parallel_config.data_parallel_size_local += (
939 - new_local_engines
940 - )
941 -
927 def scale_down_elastic_ep(
928 self, cur_data_parallel_size: int, new_data_parallel_size: int
929 ) -> None:
vllm/v1/executor/abstract.py
+4 -4
@@ -116,11 +116,11 @@ class Executor(ABC):
116 raise NotImplementedError
117
118 def initialize_from_config(self, kv_cache_configs: list[KVCacheConfig]) -> None:
119 - """
120 - Initialize the KV caches and begin the model execution loop of the
121 - underlying workers.
122 - """
119 + """Initialize the KV caches on the underlying workers."""
120 self.collective_rpc("initialize_from_config", args=(kv_cache_configs,))
121 +
122 + def compile_or_warm_up_model(self) -> None:
123 + """Compile/warm up the model and capture cudagraphs on workers."""
124 compilation_times: list[CompilationTimes] = self.collective_rpc(
125 "compile_or_warm_up_model"
126 )