[Elastic EP] Reuse CUDA graphs across reconfiguration (#54985)
Itay Alroy committed
Sep 12, 2026 at 17:26 UTC
658c8131c73194f812cd80d493e0409475b94235
22 files changed
+372
-270
requirements/kv_connectors.txt
+1
-1
@@ -2,7 +2,7 @@ lmcache >= 0.3.9
2
# CuPy 14.1.0 imports pytest from cupy.testing._random, which breaks runtime
3
# images. 14.1.1 fixes the root cause, so only 14.1.0 is excluded.
4
cupy-cuda13x != 14.1.0
5
-nixl == 1.3.2
5
+nixl == 1.4.1
6
# CUDA 12 build. On the CUDA 13 image install-kv-connectors.sh swaps this for
7
# the mooncake-transfer-engine-cuda13 variant of the same version.
8
mooncake-transfer-engine >= 0.3.12
tests/distributed/test_elastic_ep.py
+33
-65
@@ -27,7 +27,7 @@ MODEL_NAME = "deepseek-ai/DeepSeek-V2-Lite-Chat"
27
NUM_GSM8K_QUESTIONS = 256
28
EXPECTED_ACCURACY = 0.58
29
ACCURACY_TOL = 0.08
30
-MAX_NUM_SEQS = 32
30
+DEFAULT_ALL2ALL_BACKEND = "allgather_reducescatter"
31
32
33
def _send_scale_command(server: RemoteOpenAIServer, new_dp_size: int) -> bool:
@@ -161,7 +161,12 @@ def _run_gsm8k_eval(server: RemoteOpenAIServer, stage: str) -> float:
161
return accuracy
162
163
164
-def _base_serve_args(dp_size: int = 2, enforce_eager: bool = False) -> list[str]:
164
+def _base_serve_args(
165
+ dp_size: int = 2,
166
+ max_dp_size: int = 4,
167
+ enforce_eager: bool = False,
168
+ all2all_backend: str = DEFAULT_ALL2ALL_BACKEND,
169
+) -> list[str]:
170
args = [
171
"--trust-remote-code",
172
"--tensor-parallel-size",
@@ -170,12 +175,12 @@ def _base_serve_args(dp_size: int = 2, enforce_eager: bool = False) -> list[str]
175
"0.8",
176
"--max-model-len",
177
"4096",
173
- "--max-num-seqs",
174
- str(MAX_NUM_SEQS),
178
"--enable-expert-parallel",
179
"--all2all-backend",
177
- "allgather_reducescatter",
180
+ all2all_backend,
181
"--enable-elastic-ep",
182
+ "--elastic-ep-max-dp-size",
183
+ str(max_dp_size),
184
"--enable-eplb",
185
"--eplb-config.num_redundant_experts",
186
"0",
@@ -205,28 +210,43 @@ def _base_serve_args(dp_size: int = 2, enforce_eager: bool = False) -> list[str]
210
211
212
@pytest.mark.parametrize(
208
- ("enforce_eager", "traffic_mode"),
213
+ ("enforce_eager", "traffic_mode", "all2all_backend"),
214
[
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"),
215
+ pytest.param(True, "none", DEFAULT_ALL2ALL_BACKEND, id="enforce_eager_none"),
216
+ pytest.param(True, "light", DEFAULT_ALL2ALL_BACKEND, id="enforce_eager_light"),
217
+ pytest.param(True, "heavy", DEFAULT_ALL2ALL_BACKEND, id="enforce_eager_heavy"),
218
+ pytest.param(
219
+ False,
220
+ "heavy",
221
+ DEFAULT_ALL2ALL_BACKEND,
222
+ id="cuda_graphs_heavy",
223
+ ),
224
+ pytest.param(False, "heavy", "nixl_ep", id="cuda_graphs_heavy_nixl_ep"),
225
],
226
)
227
@multi_gpu_test(num_gpus=4)
217
-def test_elastic_ep_scaling(enforce_eager: bool, traffic_mode: str):
228
+def test_elastic_ep_scaling(
229
+ enforce_eager: bool, traffic_mode: str, all2all_backend: str
230
+):
231
from vllm.distributed.eplb.eplb_communicator import has_nixl
232
+ from vllm.utils.import_utils import has_nixl_ep
233
234
if not has_nixl():
235
pytest.skip("Async EPLB with elastic EP requires NIXL (not installed)")
236
+ if all2all_backend == "nixl_ep" and not has_nixl_ep():
237
+ pytest.skip("NIXL EP is not installed")
238
239
initial_dp_size = int(os.getenv("VLLM_TEST_ELASTIC_EP_INITIAL_DP", "2"))
240
target_dp_size = int(os.getenv("VLLM_TEST_ELASTIC_EP_TARGET_DP", "4"))
241
assert target_dp_size > initial_dp_size
226
- vllm_serve_args = _base_serve_args(initial_dp_size, enforce_eager)
242
+ vllm_serve_args = _base_serve_args(
243
+ initial_dp_size, target_dp_size, enforce_eager, all2all_backend
244
+ )
245
246
with RemoteOpenAIServer(
229
- MODEL_NAME, vllm_serve_args, env_dict={}, max_wait_seconds=1200
247
+ MODEL_NAME,
248
+ vllm_serve_args,
249
+ max_wait_seconds=1200,
250
) as server:
251
initial_accuracy = _run_gsm8k_eval(server, "Initial")
252
@@ -255,55 +275,3 @@ def test_elastic_ep_scaling(enforce_eager: bool, traffic_mode: str):
275
f"(diff: {scale_down_accuracy - initial_accuracy:+.3f})"
276
)
277
print(f" Tolerance: {ACCURACY_TOL:.3f}")
258
-
259
-
260
-@multi_gpu_test(num_gpus=4)
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
- """
268
- from vllm.distributed.eplb.eplb_communicator import has_nixl
269
-
270
- if not has_nixl():
271
- pytest.skip("Async EPLB with elastic EP requires NIXL (not installed)")
272
-
273
- vllm_serve_args = _base_serve_args()
274
-
275
- with RemoteOpenAIServer(
276
- MODEL_NAME, vllm_serve_args, env_dict={}, max_wait_seconds=1200
277
- ) as server:
278
- initial_accuracy = _run_gsm8k_eval(server, "Initial (2 GPUs)")
279
-
280
- # Scale 2 -> 3: This has remainder = 1 % 2 = 1
281
- # Tests uneven sender-receiver pairing
282
- assert _send_scale_command(server, 3)
283
- scale_up_accuracy = _run_gsm8k_eval(server, "After scale up (3 GPUs)")
284
-
285
- assert scale_up_accuracy >= initial_accuracy - ACCURACY_TOL, (
286
- f"Scale up accuracy {scale_up_accuracy:.3f} dropped more than "
287
- f"{ACCURACY_TOL} below initial accuracy {initial_accuracy:.3f}"
288
- )
289
-
290
- # Scale back down to 2
291
- assert _send_scale_command(server, 2)
292
- scale_down_accuracy = _run_gsm8k_eval(server, "After scale down (2 GPUs)")
293
-
294
- assert scale_down_accuracy >= initial_accuracy - ACCURACY_TOL, (
295
- f"Scale down accuracy {scale_down_accuracy:.3f} dropped more than "
296
- f"{ACCURACY_TOL} below initial accuracy {initial_accuracy:.3f}"
297
- )
298
-
299
- print("\nAccuracy Summary (Uneven Scaling):")
300
- print(f" Initial: {initial_accuracy:.3f}")
301
- print(
302
- f" Scale up: {scale_up_accuracy:.3f} "
303
- f"(diff: {scale_up_accuracy - initial_accuracy:+.3f})"
304
- )
305
- print(
306
- f" Scale down: {scale_down_accuracy:.3f} "
307
- f"(diff: {scale_down_accuracy - initial_accuracy:+.3f})"
308
- )
309
- print(f" Tolerance: {ACCURACY_TOL:.3f}")
tests/distributed/test_eplb_utils.py
+11
-4
@@ -16,10 +16,14 @@ def _make_model_state(
16
phy2log: torch.Tensor,
17
log2phy: torch.Tensor,
18
logcnt: torch.Tensor,
19
+ phy2log_storage: torch.Tensor | None = None,
20
) -> MagicMock:
20
- """Build a minimal EplbModelState mock with only the three map tensors."""
21
+ """Build a minimal EplbModelState mock with its map tensors."""
22
state = MagicMock()
23
state.physical_to_logical_map = phy2log
24
+ state.physical_to_logical_map_buffer = (
25
+ phy2log if phy2log_storage is None else phy2log_storage
26
+ )
27
state.logical_to_physical_map = log2phy
28
state.logical_replica_count = logcnt
29
return state
@@ -27,15 +31,17 @@ def _make_model_state(
31
32
def test_commit_eplb_maps_shape_change():
33
"""
30
- The normal path copies the physical_to_logical map in-place. When the number of
31
- physical experts changes, the old map should be replaced entirely.
34
+ When the number of physical experts changes, resize the active map within its
35
+ preallocated storage.
36
"""
37
num_layers, num_logical, num_physical = 2, 4, 6
38
max_replicas = 3
39
40
# Build current state tensors
41
+ storage = torch.full((num_layers, num_physical + 2), -1, dtype=torch.long)
42
model_state = _make_model_state(
38
- phy2log=torch.zeros(num_layers, num_physical, dtype=torch.long),
43
+ phy2log=storage[:, :num_physical],
44
+ phy2log_storage=storage,
45
log2phy=torch.full(
46
(num_layers, num_logical, max_replicas), -1, dtype=torch.long
47
),
@@ -54,6 +60,7 @@ def test_commit_eplb_maps_shape_change():
60
# Check that the number of physical experts has been updated and that the values
61
# match
62
assert model_state.physical_to_logical_map.shape[1] == num_physical + 2
63
+ assert model_state.physical_to_logical_map.data_ptr() == storage.data_ptr()
64
assert torch.equal(model_state.physical_to_logical_map, new_phy2log_larger)
65
66
vllm/config/parallel.py
+17
-1
@@ -216,6 +216,8 @@ class ParallelConfig:
216
217
enable_elastic_ep: bool = False
218
"""Enable elastic expert parallelism with stateless NCCL groups for DP/EP."""
219
+ elastic_ep_max_dp_size: int = Field(default=None, ge=1) # type: ignore[assignment]
220
+ """Maximum data parallel size supported by elastic expert parallelism."""
221
222
enable_dbo: bool = False
223
"""Enable dual batch overlap for the model executor."""
@@ -435,7 +437,9 @@ class ParallelConfig:
437
)
438
"""The configurations for fault tolerance."""
439
438
- @field_validator("disable_nccl_for_dp_synchronization", mode="wrap")
440
+ @field_validator(
441
+ "disable_nccl_for_dp_synchronization", "elastic_ep_max_dp_size", mode="wrap"
442
+ )
443
@classmethod
444
def _skip_none_validation(cls, value: Any, handler: Callable) -> Any:
445
"""Skip validation if the value is `None` when initialisation is delayed."""
@@ -941,6 +945,18 @@ class ParallelConfig:
945
" for dense models."
946
)
947
948
+ max_dp_size = self.elastic_ep_max_dp_size
949
+ self.elastic_ep_max_dp_size = (
950
+ max_dp_size
951
+ if self.enable_elastic_ep and max_dp_size is not None
952
+ else self.data_parallel_size
953
+ )
954
+ if self.elastic_ep_max_dp_size < self.data_parallel_size:
955
+ raise ValueError(
956
+ "--elastic-ep-max-dp-size must be greater than or equal to "
957
+ f"the initial data_parallel_size ({self.data_parallel_size})."
958
+ )
959
+
960
self.data_parallel_index = self.data_parallel_rank
961
962
if self.distributed_executor_backend == "external_launcher":
vllm/distributed/device_communicators/all2all.py
+42
-22
@@ -1,6 +1,8 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import threading
4
+from collections.abc import Iterator
5
+from contextlib import contextmanager
6
from dataclasses import dataclass
7
from typing import Any
8
@@ -377,6 +379,7 @@ class DeepEPLLAll2AllManager(DeepEPAll2AllManagerBase):
379
@dataclass
380
class _NixlEPBufferState:
381
buffer: Any
382
+ max_num_ep_ranks: int
383
connected_ep_size: int
384
active_ep_size: int
385
@@ -402,7 +405,10 @@ class NixlEPAll2AllManager(All2AllManagerBase):
405
super().__init__(cpu_group, tcp_store_group)
406
self.support_fault_tolerance = True
407
405
- self.max_num_ep_ranks = envs.VLLM_NIXL_EP_MAX_NUM_RANKS
408
+ @property
409
+ def max_num_ep_ranks(self) -> int:
410
+ assert NixlEPAll2AllManager._buffer is not None
411
+ return NixlEPAll2AllManager._buffer.max_num_ep_ranks
412
413
def _init_buffer(
414
self,
@@ -412,11 +418,19 @@ class NixlEPAll2AllManager(All2AllManagerBase):
418
) -> None:
419
from nixl_ep import Buffer # type: ignore[import-not-found]
420
415
- max_num_global_experts = self.max_num_ep_ranks * num_experts_per_rank
421
+ parallel_config = get_current_vllm_config().parallel_config
422
+ max_num_ep_ranks = (
423
+ self.world_size
424
+ * parallel_config.elastic_ep_max_dp_size
425
+ // parallel_config.data_parallel_size
426
+ if parallel_config.enable_elastic_ep
427
+ else self.world_size
428
+ )
429
+ max_num_global_experts = max_num_ep_ranks * num_experts_per_rank
430
num_rdma_bytes = Buffer.get_rdma_size_hint(
431
num_max_dispatch_tokens_per_rank=max_num_tokens_per_dp_rank,
432
hidden=token_hidden_size,
419
- num_ranks=self.max_num_ep_ranks,
433
+ num_ranks=max_num_ep_ranks,
434
num_experts=max_num_global_experts,
435
)
436
assert NixlEPAll2AllManager._buffer is None, (
@@ -427,7 +441,7 @@ class NixlEPAll2AllManager(All2AllManagerBase):
441
tcp_store_group=self.tcp_store_group.store,
442
)
443
buffer.update_memory_buffers(
430
- num_ranks=self.max_num_ep_ranks,
444
+ num_ranks=max_num_ep_ranks,
445
num_experts_per_rank=num_experts_per_rank,
446
num_rdma_bytes=num_rdma_bytes,
447
)
@@ -435,6 +449,7 @@ class NixlEPAll2AllManager(All2AllManagerBase):
449
buffer.connect_ranks(ranks_to_connect)
450
NixlEPAll2AllManager._buffer = _NixlEPBufferState(
451
buffer=buffer,
452
+ max_num_ep_ranks=max_num_ep_ranks,
453
connected_ep_size=self.world_size,
454
active_ep_size=self.world_size,
455
)
@@ -474,6 +489,7 @@ class NixlEPAll2AllManager(All2AllManagerBase):
489
490
for rank in range(state.active_ep_size, target_ep_size):
491
state.buffer.update_mask_buffer(rank, mask=False)
492
+ torch.accelerator.synchronize()
493
state.active_ep_size = target_ep_size
494
495
def _stage_ep_size(self) -> None:
@@ -486,8 +502,12 @@ class NixlEPAll2AllManager(All2AllManagerBase):
502
if target_ep_size > state.connected_ep_size:
503
self._connect_to_ep_size(target_ep_size, make_active=False)
504
489
- def commit_staged_state(self) -> None:
490
- """Commit staged NIXL EP state to the active communication set."""
505
+ def stage_ep_size(self) -> None:
506
+ with NixlEPAll2AllManager._lock:
507
+ self._stage_ep_size()
508
+
509
+ def commit_ep_size(self) -> None:
510
+ """Commit the staged EP size to the active communication set."""
511
with NixlEPAll2AllManager._lock:
512
assert NixlEPAll2AllManager._buffer is not None
513
state = NixlEPAll2AllManager._buffer
@@ -500,31 +520,31 @@ class NixlEPAll2AllManager(All2AllManagerBase):
520
521
self._unmask_connected_ranks(target_ep_size)
522
503
- def _ensure_ep_size(self, *, stage: bool) -> None:
504
- if stage:
505
- self._stage_ep_size()
506
- else:
507
- self.commit_staged_state()
523
+ @contextmanager
524
+ def mask_remote_ranks(self) -> Iterator[None]:
525
+ peers = [rank for rank in range(self.world_size) if rank != self.rank]
526
+ assert NixlEPAll2AllManager._buffer is not None
527
+ buffer = NixlEPAll2AllManager._buffer.buffer
528
+ for rank in peers:
529
+ buffer.update_mask_buffer(rank, mask=True)
530
+ torch.accelerator.synchronize()
531
+ try:
532
+ yield
533
+ finally:
534
+ for rank in peers:
535
+ buffer.update_mask_buffer(rank, mask=False)
536
+ torch.accelerator.synchronize()
537
538
def get_handle(self, kwargs):
539
with NixlEPAll2AllManager._lock:
511
- stage = bool(kwargs.get("stage", False))
540
state = NixlEPAll2AllManager._buffer
541
if state is None:
514
- assert not stage, (
515
- "NIXL EP staged initialization requires an existing buffer"
516
- )
542
max_num_tokens_per_dp_rank = kwargs["max_num_tokens_per_dp_rank"]
518
- num_experts_per_rank = (
519
- kwargs["num_global_experts"] // kwargs["num_ep_ranks"]
520
- )
543
self._init_buffer(
544
max_num_tokens_per_dp_rank=max_num_tokens_per_dp_rank,
545
token_hidden_size=kwargs["token_hidden_size"],
524
- num_experts_per_rank=num_experts_per_rank,
546
+ num_experts_per_rank=kwargs["num_local_experts"],
547
)
526
- else:
527
- self._ensure_ep_size(stage=stage)
548
549
assert NixlEPAll2AllManager._buffer is not None
550
handle = NixlEPAll2AllManager._buffer.buffer
@@ -564,7 +584,7 @@ class NixlEPAll2AllManager(All2AllManagerBase):
584
assert state is not None
585
if NixlEPAll2AllManager._mask is None:
586
NixlEPAll2AllManager._mask = torch.zeros(
567
- self.max_num_ep_ranks, device="cuda", dtype=torch.int32
587
+ state.max_num_ep_ranks, device="cuda", dtype=torch.int32
588
)
589
state.buffer.query_mask_buffer(NixlEPAll2AllManager._mask)
590
return NixlEPAll2AllManager._mask[: state.active_ep_size]
vllm/distributed/device_communicators/base_device_communicator.py
+12
@@ -1,6 +1,8 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import threading
4
+from collections.abc import Iterator
5
+from contextlib import contextmanager
6
from weakref import WeakValueDictionary
7
8
import torch
@@ -157,6 +159,16 @@ class All2AllManagerBase:
159
def destroy(self):
160
pass
161
162
+ def stage_ep_size(self) -> None:
163
+ pass
164
+
165
+ def commit_ep_size(self) -> None:
166
+ pass
167
+
168
+ @contextmanager
169
+ def mask_remote_ranks(self) -> Iterator[None]:
170
+ yield
171
+
172
173
class DeviceCommunicatorBase:
174
"""
vllm/distributed/elastic_ep/elastic_execute.py
+77
-116
@@ -2,34 +2,32 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import gc
4
import weakref
5
-from collections.abc import Iterable, Sequence
5
+from collections.abc import Iterable, Iterator, Sequence
6
from concurrent.futures import Future, ThreadPoolExecutor
7
+from contextlib import contextmanager, nullcontext
8
from dataclasses import replace
9
from typing import TYPE_CHECKING
10
11
import torch
12
import torch.nn as nn
12
-import torch.nn.functional as F
13
from torch.distributed import P2POp
14
15
-from vllm.compilation.counter import compilation_counter
15
from vllm.compilation.cuda_graph import CUDAGraphWrapper
16
from vllm.compilation.wrapper import reset_compile_wrapper
18
-from vllm.config import (
19
- CompilationMode,
20
- set_current_vllm_config,
21
-)
17
+from vllm.config import ParallelConfig, set_current_vllm_config
18
from vllm.distributed import (
19
get_dp_group,
20
get_ep_group,
21
get_pcp_group,
22
get_tp_group,
23
+ get_world_group,
24
)
25
from vllm.distributed.elastic_ep.standby_state import (
26
create_standby_groups,
27
get_standby_dp_group,
28
get_standby_ep_group,
29
get_standby_eplb_group,
30
+ get_standby_world_group,
31
pop_standby_groups,
32
)
33
from vllm.distributed.eplb.eplb_communicator import (
@@ -41,13 +39,20 @@ from vllm.distributed.parallel_state import (
39
)
40
from vllm.distributed.stateless_coordinator import StatelessGroupCoordinator
41
from vllm.logger import init_logger
42
+from vllm.model_executor.layers.fused_moe.all2all_utils import (
43
+ get_ep_all2all_manager,
44
+)
45
from vllm.model_executor.layers.fused_moe.config import FusedMoEParallelConfig
46
from vllm.model_executor.layers.fused_moe.eep_reconfigure import (
47
make_eep_staged_quant_method,
48
)
49
+from vllm.model_executor.warmup.flashinfer_autotune_cache import (
50
+ sync_flashinfer_autotune_cache,
51
+)
52
from vllm.model_executor.warmup.kernel_warmup import kernel_warmup
53
from vllm.utils import is_moe_layer
54
from vllm.v1.engine import ReconfigureDistributedRequest, ReconfigureRankType
55
+from vllm.v1.worker.dp_utils import skip_dp_coordination
56
from vllm.v1.worker.gpu_ubatch_wrapper import UBatchWrapper
57
from vllm.v1.worker.workspace import lock_workspace, unlock_workspace
58
@@ -59,6 +64,15 @@ if TYPE_CHECKING:
64
)
65
66
67
+def can_reuse_fused_moe_kernel(parallel_config: ParallelConfig) -> bool:
68
+ """Whether fused MoE kernels can be reused across EP rank changes.
69
+
70
+ The EP backend must preserve its CUDA graph entries when ranks are connected,
71
+ disconnected, masked, or unmasked.
72
+ """
73
+ return parallel_config.all2all_backend == "nixl_ep"
74
+
75
+
76
def batch_transfer_weights(
77
model: nn.Module,
78
is_sender: bool,
@@ -105,31 +119,20 @@ def batch_transfer_weights(
119
120
def broadcast_expert_mapping(
121
physical_to_logical: torch.Tensor | None,
108
- num_local_physical_experts: int | None,
109
- num_logical_experts: int | None,
122
dp_group: StatelessGroupCoordinator,
123
device: torch.device,
124
src_rank: int = 0,
113
-) -> tuple[torch.Tensor, int, int]:
125
+) -> torch.Tensor:
126
if dp_group.rank_in_group == src_rank:
127
assert physical_to_logical is not None
116
- assert num_local_physical_experts is not None
117
- assert num_logical_experts is not None
128
assert physical_to_logical.dtype == torch.int64
129
shape_tensor = torch.tensor(
130
list(physical_to_logical.shape), dtype=torch.int64, device="cpu"
131
)
122
- metadata_tensor = torch.tensor(
123
- [num_local_physical_experts, num_logical_experts],
124
- dtype=torch.int64,
125
- device="cpu",
126
- )
132
else:
133
shape_tensor = torch.empty(2, dtype=torch.int64, device="cpu")
129
- metadata_tensor = torch.empty(2, dtype=torch.int64, device="cpu")
134
135
shape_tensor = dp_group.tcp_store_group.broadcast(shape_tensor, src_rank)
132
- metadata_tensor = dp_group.tcp_store_group.broadcast(metadata_tensor, src_rank)
136
137
if dp_group.rank_in_group != src_rank:
138
assert device is not None
@@ -140,11 +143,7 @@ def broadcast_expert_mapping(
143
)
144
145
assert physical_to_logical is not None
143
- physical_to_logical = dp_group.broadcast(physical_to_logical, src_rank)
144
- num_local_physical_experts = int(metadata_tensor[0].item())
145
- num_logical_experts = int(metadata_tensor[1].item())
146
-
147
- return physical_to_logical, num_local_physical_experts, num_logical_experts
146
+ return dp_group.broadcast(physical_to_logical, src_rank)
147
148
149
class ElasticEPScalingExecutor:
@@ -166,6 +165,21 @@ class ElasticEPScalingExecutor:
165
raise RuntimeError("Worker has been garbage collected")
166
return worker
167
168
+ def _can_reuse_fused_moe_kernel(self) -> bool:
169
+ return can_reuse_fused_moe_kernel(
170
+ self.worker.vllm_config.parallel_config,
171
+ )
172
+
173
+ @contextmanager
174
+ def _disable_flashinfer_autotune(self) -> Iterator[None]:
175
+ kernel_config = self.worker.vllm_config.kernel_config
176
+ enabled = kernel_config.enable_flashinfer_autotune
177
+ kernel_config.enable_flashinfer_autotune = False
178
+ try:
179
+ yield
180
+ finally:
181
+ kernel_config.enable_flashinfer_autotune = enabled
182
+
183
def execute(self, execute_method: str, *args, **kwargs):
184
method = getattr(self, execute_method, None)
185
if method is None:
@@ -260,11 +274,20 @@ class ElasticEPScalingExecutor:
274
use_all2all=use_all2all,
275
enable_eplb=parallel_config.enable_eplb,
276
)
263
- self.stage_standby_moe_quant_methods()
277
+ standby_ep_group = get_standby_ep_group()
278
+ assert standby_ep_group is not None
279
+ all2all_manager = get_ep_all2all_manager(standby_ep_group)
280
+ all2all_manager.stage_ep_size()
281
+ if not self._can_reuse_fused_moe_kernel():
282
+ self.stage_standby_moe_quant_methods(all2all_manager)
283
self._prepare_eplb_communicator(get_standby_eplb_group())
284
if new_dp_size > old_dp_size:
285
self.transfer_weights(old_dp_size, new_dp_size)
267
- self._warm_target_groups(get_standby_dp_group(), get_standby_ep_group())
286
+ self._warm_target_groups(get_standby_dp_group(), standby_ep_group)
287
+ if new_dp_size > old_dp_size and self._can_reuse_fused_moe_kernel():
288
+ target_world_group = get_standby_world_group()
289
+ assert target_world_group is not None
290
+ sync_flashinfer_autotune_cache(self.worker.model_runner, target_world_group)
291
292
def _prepare_eplb_communicator(self, eplb_group) -> None:
293
assert eplb_group is not None
@@ -338,14 +361,9 @@ class ElasticEPScalingExecutor:
361
assert eplb_state is not None
362
eplb_state.drain_async()
363
eplb_model_state = eplb_state.model_states[model_config.compute_hash()]
341
- physical_to_logical = eplb_model_state.physical_to_logical_map
342
- num_physical_experts = physical_to_logical.shape[1]
343
- num_local_physical_experts = num_physical_experts // get_ep_group().world_size
344
- num_logical_experts = eplb_model_state.logical_replica_count.shape[1]
364
+ physical_to_logical = eplb_model_state.physical_to_logical_map_buffer
365
broadcast_expert_mapping(
366
physical_to_logical=physical_to_logical,
347
- num_local_physical_experts=num_local_physical_experts,
348
- num_logical_experts=num_logical_experts,
367
dp_group=standby_dp_group,
368
src_rank=0,
369
device=self.worker.device,
@@ -368,7 +386,7 @@ class ElasticEPScalingExecutor:
386
moe_parallel_config=moe_parallel_config,
387
)
388
371
- def stage_standby_moe_quant_methods(self) -> None:
389
+ def stage_standby_moe_quant_methods(self, all2all_manager) -> None:
390
standby_dp_group = get_standby_dp_group()
391
standby_ep_group = get_standby_ep_group()
392
model = self.worker.model_runner.get_model()
@@ -383,6 +401,7 @@ class ElasticEPScalingExecutor:
401
standby_dp_group,
402
standby_ep_group,
403
),
404
+ all2all_manager=all2all_manager,
405
)
406
if staged_quant_method is not None:
407
self._staged_moe_quant_methods[module] = staged_quant_method
@@ -396,7 +415,6 @@ class ElasticEPScalingExecutor:
415
continue
416
assert staged_quant_method.moe_kernel is not None
417
module._replace_quant_method(staged_quant_method)
399
- staged_quant_method.moe_kernel.prepare_finalize.on_commit()
418
self._staged_moe_quant_methods.clear()
419
420
def _release_cuda_graphs(self) -> None:
@@ -405,7 +423,7 @@ class ElasticEPScalingExecutor:
423
wrapper.concrete_cudagraph_entries = {}
424
425
elif isinstance(self.worker.model_runner.model, UBatchWrapper):
408
- raise RuntimeError("DBO is not yet supported in elastic EP")
426
+ self.worker.model_runner.model.clear_graphs()
427
428
torch.compiler.reset()
429
with set_current_vllm_config(self.worker.vllm_config):
@@ -427,10 +445,8 @@ class ElasticEPScalingExecutor:
445
self._wait_for_group_cleanup()
446
447
def switch_and_prepare(self) -> tuple[GroupCoordinator | None, ...]:
430
- old_dp_size = get_dp_group().world_size
431
- old_ep_size = get_ep_group().world_size
432
-
433
- self._release_cuda_graphs()
448
+ if not self._can_reuse_fused_moe_kernel():
449
+ self._release_cuda_graphs()
450
retired_groups = _replace_active_groups(**pop_standby_groups())
451
452
parallel_config = self.worker.vllm_config.parallel_config
@@ -487,47 +503,13 @@ class ElasticEPScalingExecutor:
503
parallel_config.eplb_config.num_redundant_experts = (
504
num_physical_experts - num_logical_experts
505
)
490
- old_physical_to_logical = eplb_model_state.physical_to_logical_map
491
- num_moe_layers = old_physical_to_logical.shape[0]
492
- num_local_experts = eplb_model_state.expert_load_pass.shape[1] // old_ep_size
493
- if new_dp_size > old_dp_size:
494
- expanded_physical_to_logical = torch.full(
495
- (num_moe_layers, num_local_experts * new_ep_size),
496
- -1,
497
- dtype=old_physical_to_logical.dtype,
498
- device=old_physical_to_logical.device,
499
- )
500
- expanded_physical_to_logical[:, : num_local_experts * old_ep_size] = (
501
- old_physical_to_logical
502
- )
503
- eplb_model_state.physical_to_logical_map = expanded_physical_to_logical
504
-
505
- old_num_physical_experts = eplb_model_state.expert_load_pass.shape[1]
506
- pad_size = num_physical_experts - old_num_physical_experts
507
- if new_dp_size > old_dp_size:
508
- assert pad_size > 0
509
- expanded_expert_load_pass = F.pad(
510
- eplb_model_state.expert_load_pass, (0, pad_size), value=0
511
- )
512
- expanded_expert_load_window = F.pad(
513
- eplb_model_state.expert_load_window, (0, pad_size), value=0
514
- )
515
- eplb_model_state.expert_load_pass = expanded_expert_load_pass
516
- eplb_model_state.expert_load_window = expanded_expert_load_window
517
- else:
518
- assert pad_size < 0
519
- eplb_model_state.expert_load_pass = eplb_model_state.expert_load_pass[
520
- :, :num_physical_experts
521
- ]
522
- eplb_model_state.expert_load_window = eplb_model_state.expert_load_window[
523
- :, :, :num_physical_experts
524
- ]
506
+ eplb_state.reconfigure_physical_expert_slots(model_config, num_physical_experts)
507
508
model = self.worker.model_runner.get_model()
509
model.expert_weights = []
510
with set_current_vllm_config(self.worker.vllm_config):
511
model.set_eplb_state(
530
- eplb_model_state.expert_load_pass,
512
+ eplb_model_state.expert_load_pass_buffer,
513
eplb_model_state.logical_to_physical_map,
514
eplb_model_state.logical_replica_count,
515
)
@@ -539,6 +521,7 @@ class ElasticEPScalingExecutor:
521
num_local_physical_experts=num_local_experts,
522
)
523
self._commit_staged_moe_quant_methods()
524
+ get_ep_all2all_manager().commit_ep_size()
525
# Legacy modular methods need to be recreated for the new EP size.
526
for module in moe_modules:
527
if getattr(module._quant_method, "wraps_legacy_quant_method", False):
@@ -548,21 +531,6 @@ class ElasticEPScalingExecutor:
531
eplb_state.update_communicator(model_config, self._prepared_eplb_communicator)
532
self._prepared_eplb_communicator = None
533
551
- if (
552
- self.worker.vllm_config.compilation_config.mode
553
- == CompilationMode.STOCK_TORCH_COMPILE
554
- ):
555
- # NOTE(yongji): when using stock torch.compile,
556
- # torch.compile is triggered during GPUModelRunner's load_model()
557
- # TODO(yongji):check do we need to re-trigger torch.compile here?
558
- # any changes to the tensor shapes in execution should already
559
- # be handled internally by torch.compile.
560
- backend = self.worker.vllm_config.compilation_config.init_backend(
561
- self.worker.vllm_config
562
- )
563
- compilation_counter.stock_torch_compile_count += 1
564
- self.worker.model_runner.model.compile(fullgraph=True, backend=backend)
565
-
534
return retired_groups
535
536
def _perform_eplb_reshuffle(
@@ -608,7 +576,8 @@ class ElasticEPScalingExecutor:
576
else:
577
mapping = self.receive_expert_mapping()
578
self.worker.model_runner.setup_eplb_from_mapping(mapping)
611
- self.warm_and_capture()
579
+ if not self._can_reuse_fused_moe_kernel():
580
+ self.warm_and_capture()
581
self._perform_eplb_reshuffle(async_op=True)
582
if is_existing_worker:
583
self._start_group_cleanup(retired_groups)
@@ -619,7 +588,8 @@ class ElasticEPScalingExecutor:
588
self.switch_and_remove()
589
else:
590
retired_groups = self.switch_and_prepare()
622
- self.warm_and_capture()
591
+ if not self._can_reuse_fused_moe_kernel():
592
+ self.warm_and_capture()
593
self._start_group_cleanup(retired_groups)
594
595
def perform_scale_down_eplb_reshuffle(self, new_dp_size: int) -> None:
@@ -677,42 +647,26 @@ class ElasticEPScalingExecutor:
647
)
648
torch.accelerator.synchronize()
649
self._warm_target_groups(get_dp_group(), get_ep_group())
650
+ if self._can_reuse_fused_moe_kernel():
651
+ sync_flashinfer_autotune_cache(self.worker.model_runner, get_world_group())
652
653
def receive_expert_mapping(self) -> torch.Tensor:
654
dp_group = get_dp_group()
655
assert isinstance(dp_group, StatelessGroupCoordinator)
684
- physical_to_logical, num_local_physical_experts, _ = broadcast_expert_mapping(
656
+ return broadcast_expert_mapping(
657
physical_to_logical=None,
686
- num_local_physical_experts=None,
687
- num_logical_experts=None,
658
dp_group=dp_group,
659
src_rank=0,
660
device=self.worker.device,
661
)
692
- num_moe_layers = physical_to_logical.shape[0]
693
- new_dp_size = get_dp_group().world_size
694
- tp_size = self.worker.vllm_config.parallel_config.tensor_parallel_size
695
- new_ep_size = new_dp_size * tp_size
696
- expanded_physical_to_logical = torch.full(
697
- (num_moe_layers, num_local_physical_experts * new_ep_size),
698
- -1,
699
- dtype=physical_to_logical.dtype,
700
- device=physical_to_logical.device,
701
- )
702
- expanded_physical_to_logical[:, : physical_to_logical.shape[1]] = (
703
- physical_to_logical
704
- )
705
- return expanded_physical_to_logical
662
707
- def warmup_local_kernels(self) -> None:
663
+ def warmup_new_worker(self) -> None:
664
with set_current_vllm_config(self.worker.vllm_config):
665
kernel_warmup(self.worker, process_local_only=True)
666
+ if self._can_reuse_fused_moe_kernel():
667
+ self.warm_and_capture()
668
669
def warm_and_capture(self) -> None:
712
- # Must run on every DP sibling in lockstep: _dummy_run calls
713
- # coordinate_batch_across_dp whenever data_parallel_size > 1
714
- # (gpu_model_runner.py:3663), which deadlocks if any rank skips it.
715
-
670
# Save and clear block tables so the dummy MoE forward doesn't
671
# write dummy slot mappings into real KV-cache blocks.
672
multi_block_table = self.worker.model_runner.input_batch.block_table
@@ -735,8 +689,15 @@ class ElasticEPScalingExecutor:
689
# directly with skip_eplb=True so dummy routing doesn't pollute the
690
# just-rebalanced EPLB stats.
691
runner = self.worker.model_runner
738
- runner._dummy_run(runner.max_num_tokens, is_profile=True, skip_eplb=True)
739
- self.worker.compile_or_warm_up_model()
692
+ all2all_manager = get_ep_all2all_manager()
693
+ reuse_kernel = self._can_reuse_fused_moe_kernel()
694
+ with (
695
+ skip_dp_coordination() if reuse_kernel else nullcontext(),
696
+ all2all_manager.mask_remote_ranks() if reuse_kernel else nullcontext(),
697
+ self._disable_flashinfer_autotune() if reuse_kernel else nullcontext(),
698
+ ):
699
+ runner._dummy_run(runner.max_num_tokens, is_profile=True, skip_eplb=True)
700
+ self.worker.compile_or_warm_up_model()
701
702
lock_workspace()
703
vllm/distributed/elastic_ep/elastic_state.py
+1
-1
@@ -204,7 +204,7 @@ class ElasticEPScalingState:
204
return True
205
206
elif state == ScaleUpNewEngineState.PREPARE:
207
- self._collective_rpc("elastic_ep_execute", args=("warmup_local_kernels",))
207
+ self._collective_rpc("elastic_ep_execute", args=("warmup_new_worker",))
208
self._mark_ready_for_switch()
209
tensor = torch.tensor([0, 0, 0], dtype=torch.int32, device="cpu")
210
torch.distributed.all_reduce(
vllm/distributed/eplb/eplb_state.py
+66
-12
@@ -121,6 +121,8 @@ class EplbModelState:
121
[0, 2, 0, 1, 0, 3]]
122
```
123
"""
124
+ physical_to_logical_map_buffer: torch.Tensor
125
+ """Maximum-capacity buffer backing ``physical_to_logical_map``."""
126
logical_to_physical_map: torch.Tensor
127
"""
128
Mapping from logical experts to physical experts.
@@ -168,6 +170,8 @@ class EplbModelState:
170
171
Shape: (num_moe_layers, num_physical_experts)
172
"""
173
+ expert_load_pass_buffer: torch.Tensor
174
+ """Maximum-capacity buffer backing ``expert_load_pass``."""
175
expert_load_window: torch.Tensor
176
"""
177
A sliding window of expert load.
@@ -373,6 +377,19 @@ class EplbState:
377
physical_to_logical_map_list,
378
device=self.device,
379
)
380
+ physical_expert_capacity = model.num_physical_experts
381
+ if self.parallel_config.enable_elastic_ep:
382
+ physical_expert_capacity = (
383
+ model.num_local_physical_experts
384
+ * self.parallel_config.elastic_ep_max_dp_size
385
+ * get_ep_group().world_size
386
+ // self.parallel_config.data_parallel_size
387
+ )
388
+ physical_to_logical_map = torch.nn.functional.pad(
389
+ physical_to_logical_map,
390
+ (0, physical_expert_capacity - model.num_physical_experts),
391
+ value=-1,
392
+ )
393
# Assuming 8 GPUs per node, this supports up to
394
# (1023 + 1) / 8 = 128 nodes for now.
395
# TODO(rui): make this configurable
@@ -407,6 +424,10 @@ class EplbState:
424
)
425
.contiguous()
426
)
427
+ physical_to_logical_map_buffer = physical_to_logical_map
428
+ physical_to_logical_map = physical_to_logical_map_buffer[
429
+ :, : model.num_physical_experts
430
+ ]
431
logical_to_physical_map = (
432
logical_to_physical_map.unsqueeze(0)
433
.expand(
@@ -425,11 +446,12 @@ class EplbState:
446
.contiguous()
447
)
448
428
- expert_load_pass = torch.zeros(
429
- (model.num_moe_layers, model.num_physical_experts),
449
+ expert_load_pass_buffer = torch.zeros(
450
+ (model.num_moe_layers, physical_expert_capacity),
451
dtype=torch.int32,
452
device=self.device,
453
)
454
+ expert_load_pass = expert_load_pass_buffer[:, : model.num_physical_experts]
455
self.expert_load_window_size = self.parallel_config.eplb_config.window_size
456
expert_load_window = torch.zeros(
457
(
@@ -460,7 +482,7 @@ class EplbState:
482
]
483
484
model.set_eplb_state(
463
- expert_load_pass,
485
+ expert_load_pass_buffer,
486
logical_to_physical_map,
487
logical_replica_count,
488
)
@@ -479,9 +501,11 @@ class EplbState:
501
502
model_state = EplbModelState(
503
physical_to_logical_map=physical_to_logical_map,
504
+ physical_to_logical_map_buffer=physical_to_logical_map_buffer,
505
logical_to_physical_map=logical_to_physical_map,
506
logical_replica_count=logical_replica_count,
507
expert_load_pass=expert_load_pass,
508
+ expert_load_pass_buffer=expert_load_pass_buffer,
509
expert_load_window=expert_load_window,
510
model_name=model_config.model,
511
model=model,
@@ -1075,10 +1099,14 @@ class EplbState:
1099
expanded_physical_to_logical: torch.Tensor,
1100
) -> None:
1101
eplb_model_state = self.model_states[model_config.compute_hash()]
1078
- eplb_model_state.physical_to_logical_map.copy_(expanded_physical_to_logical)
1102
+ eplb_model_state.physical_to_logical_map_buffer.copy_(
1103
+ expanded_physical_to_logical
1104
+ )
1105
+ eplb_model_state.expert_load_pass.zero_()
1106
+ physical_to_logical_map = eplb_model_state.physical_to_logical_map
1107
1108
(logical_to_physical_map_cpu, logical_replica_count_cpu) = compute_logical_maps(
1081
- expanded_physical_to_logical.cpu(),
1109
+ physical_to_logical_map.cpu(),
1110
eplb_model_state.model.num_logical_experts,
1111
)
1112
@@ -1097,6 +1125,33 @@ class EplbState:
1125
eplb_model_state.logical_to_physical_map.copy_(logical_to_physical_map)
1126
eplb_model_state.logical_replica_count.copy_(logical_replica_count)
1127
1128
+ def reconfigure_physical_expert_slots(
1129
+ self,
1130
+ model_config: ModelConfig,
1131
+ num_physical_experts: int,
1132
+ ) -> None:
1133
+ """Replace physical_to_logical_map and expert_load_pass with views
1134
+ covering the new active size.
1135
+ """
1136
+ model_state = self.model_states[model_config.compute_hash()]
1137
+ old_num_physical_experts = model_state.model.num_physical_experts
1138
+ first_slot, last_slot = sorted((old_num_physical_experts, num_physical_experts))
1139
+ physical_to_logical_map_buffer = model_state.physical_to_logical_map_buffer
1140
+ expert_load_pass_buffer = model_state.expert_load_pass_buffer
1141
+ assert last_slot <= physical_to_logical_map_buffer.shape[1]
1142
+ expert_slots = slice(first_slot, last_slot)
1143
+ physical_to_logical_map_buffer[:, expert_slots].fill_(-1)
1144
+ expert_load_pass_buffer[:, expert_slots].zero_()
1145
+ model_state.physical_to_logical_map = physical_to_logical_map_buffer[
1146
+ :, :num_physical_experts
1147
+ ]
1148
+ model_state.expert_load_pass = expert_load_pass_buffer[:, :num_physical_experts]
1149
+
1150
+ pad_size = num_physical_experts - model_state.expert_load_window.shape[-1]
1151
+ model_state.expert_load_window = torch.nn.functional.pad(
1152
+ model_state.expert_load_window, (0, pad_size)
1153
+ )
1154
+
1155
def create_communicator(
1156
self, model_config: ModelConfig, group_coordinator: GroupCoordinator
1157
) -> EplbCommunicator:
@@ -1333,14 +1388,13 @@ def _commit_eplb_maps(
1388
1389
if PIN_MEMORY and src.is_cpu:
1390
src = src.new_empty(src.shape, pin_memory=True).copy_(src)
1336
- # Rare Case: When the number of physical experts has changed, discard the old
1337
- # physical to logical expert map and use the new one. This only happens when the
1338
- # number of GPUs available to vLLM changes while vLLM is running. Otherwise copy the
1339
- # new map into the old one.
1391
+ # When the number of physical experts changes, refresh the active map view to
1392
+ # cover the new size before copying it.
1393
if src.shape[1] != dst.shape[1]:
1341
- model_state.physical_to_logical_map = src.to(dst.device, non_blocking=True)
1342
- else:
1343
- dst.copy_(src, non_blocking=True)
1394
+ dst = model_state.physical_to_logical_map_buffer[:, : src.shape[1]]
1395
+ assert dst.shape == src.shape
1396
+ model_state.physical_to_logical_map = dst
1397
+ dst.copy_(src, non_blocking=True)
1398
1399
num_logical_experts = model_state.logical_to_physical_map.shape[1]
1400
new_logical, new_replica_count = compute_logical_maps(src, num_logical_experts)
vllm/engine/arg_utils.py
+6
@@ -507,6 +507,7 @@ class EngineArgs:
507
linear_backend: LinearBackend = KernelConfig.linear_backend
508
all2all_backend: All2AllBackend = ParallelConfig.all2all_backend
509
enable_elastic_ep: bool = ParallelConfig.enable_elastic_ep
510
+ elastic_ep_max_dp_size: int = ParallelConfig.elastic_ep_max_dp_size
511
enable_dbo: bool = ParallelConfig.enable_dbo
512
ubatch_size: int = ParallelConfig.ubatch_size
513
dbo_decode_token_threshold: int = ParallelConfig.dbo_decode_token_threshold
@@ -1201,6 +1202,10 @@ class EngineArgs:
1202
parallel_group.add_argument(
1203
"--enable-elastic-ep", **parallel_kwargs["enable_elastic_ep"]
1204
)
1205
+ parallel_group.add_argument(
1206
+ "--elastic-ep-max-dp-size",
1207
+ **parallel_kwargs["elastic_ep_max_dp_size"],
1208
+ )
1209
parallel_group.add_argument(
1210
"--dbo-decode-token-threshold",
1211
**parallel_kwargs["dbo_decode_token_threshold"],
@@ -2345,6 +2350,7 @@ class EngineArgs:
2350
enable_ep_weight_filter=self.enable_ep_weight_filter,
2351
all2all_backend=self.all2all_backend,
2352
enable_elastic_ep=self.enable_elastic_ep,
2353
+ elastic_ep_max_dp_size=self.elastic_ep_max_dp_size,
2354
enable_dbo=self.enable_dbo,
2355
ubatch_size=self.ubatch_size,
2356
dbo_decode_token_threshold=self.dbo_decode_token_threshold,
vllm/envs.py
-5
@@ -316,7 +316,6 @@ if TYPE_CHECKING:
316
VLLM_ELASTIC_EP_SCALE_UP_LAUNCH: bool = False
317
VLLM_ELASTIC_EP_DRAIN_REQUESTS: bool = False
318
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS: bool = True
319
- VLLM_NIXL_EP_MAX_NUM_RANKS: int = 32
319
VLLM_XPU_ENABLE_XPU_GRAPH: bool = False
320
VLLM_XPU_FORCE_N_CONTIG_WEIGHT: bool = False
321
VLLM_XPU_USE_SAMPLER_KERNEL: bool = True
@@ -2129,10 +2128,6 @@ environment_variables: dict[str, Callable[[], Any]] = {
2128
"VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS": lambda: bool(
2129
int(os.getenv("VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS", "1"))
2130
),
2132
- # NIXL EP environment variables
2133
- "VLLM_NIXL_EP_MAX_NUM_RANKS": lambda: int(
2134
- os.getenv("VLLM_NIXL_EP_MAX_NUM_RANKS", "32")
2135
- ),
2131
# Whether enable XPU graph on Intel GPU
2132
"VLLM_XPU_ENABLE_XPU_GRAPH": lambda: bool(
2133
int(os.getenv("VLLM_XPU_ENABLE_XPU_GRAPH", "0"))
vllm/model_executor/layers/fused_moe/all2all_utils.py
+15
-18
@@ -8,6 +8,7 @@ import torch
8
9
from vllm.config import get_current_vllm_config
10
from vllm.distributed import (
11
+ GroupCoordinator,
12
get_ep_group,
13
)
14
from vllm.logger import init_logger
@@ -104,16 +105,12 @@ if current_platform.is_cuda_alike():
105
)
106
107
107
-def get_ep_all2all_manager(eep_stage: bool = False) -> Any:
108
- if eep_stage:
109
- from vllm.distributed.elastic_ep.standby_state import get_standby_ep_group
110
-
111
- ep_group = get_standby_ep_group()
112
- assert ep_group is not None
113
- device_communicator = ep_group.device_communicator
114
- else:
115
- device_communicator = get_ep_group().device_communicator
116
-
108
+def get_ep_all2all_manager(
109
+ ep_group: GroupCoordinator | None = None,
110
+) -> Any:
111
+ if ep_group is None:
112
+ ep_group = get_ep_group()
113
+ device_communicator = ep_group.device_communicator
114
assert device_communicator is not None
115
all2all_manager = device_communicator.all2all_manager
116
assert all2all_manager is not None
@@ -168,7 +165,7 @@ def maybe_make_prepare_finalize(
165
routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
166
allow_new_interface: bool = False,
167
use_monolithic: bool = False,
171
- eep_stage: bool = False,
168
+ all2all_manager: Any | None = None,
169
) -> FusedMoEPrepareAndFinalize | None:
170
if not moe.moe_parallel_config.use_all2all_kernels:
171
if not allow_new_interface:
@@ -190,7 +187,8 @@ def maybe_make_prepare_finalize(
187
"Detected DP deployment with no --enable-expert-parallel. "
188
"Falling back to AllGather+ReduceScatter dispatch/combine."
189
)
193
- all2all_manager = get_ep_all2all_manager(eep_stage)
190
+ if all2all_manager is None:
191
+ all2all_manager = get_ep_all2all_manager()
192
return make_moe_prepare_and_finalize_naive_dp_ep(
193
is_sequence_parallel=moe.moe_parallel_config.is_sequence_parallel,
194
num_dispatchers=all2all_manager.world_size,
@@ -199,7 +197,8 @@ def maybe_make_prepare_finalize(
197
else:
198
return make_moe_prepare_and_finalize_no_dp_ep(use_monolithic)
199
202
- all2all_manager = get_ep_all2all_manager(eep_stage)
200
+ if all2all_manager is None:
201
+ all2all_manager = get_ep_all2all_manager()
202
203
prepare_finalize: FusedMoEPrepareAndFinalize | None = None
204
@@ -361,10 +360,7 @@ def maybe_make_prepare_finalize(
360
all_to_all_args = dict(
361
max_num_tokens_per_dp_rank=moe.max_num_tokens,
362
token_hidden_size=moe.hidden_dim,
364
- num_ep_ranks=all2all_manager.world_size,
365
- num_global_experts=moe.num_experts,
366
- num_local_experts=moe.num_experts // all2all_manager.world_size,
367
- stage=eep_stage,
363
+ num_local_experts=moe.num_local_experts,
364
)
365
handle = all2all_manager.get_handle(all_to_all_args)
366
@@ -378,7 +374,8 @@ def maybe_make_prepare_finalize(
374
prepare_finalize = NixlEPPrepareAndFinalize(
375
handle,
376
max_tokens_per_rank=moe.max_num_tokens,
381
- num_dispatchers=all2all_manager.world_size,
377
+ num_dispatchers=all2all_manager.max_num_ep_ranks,
378
+ expert_capacity=(moe.num_local_experts * all2all_manager.max_num_ep_ranks),
379
use_fp8_dispatch=use_fp8_dispatch,
380
global_to_physical=global_to_physical,
381
physical_to_global=physical_to_global,
vllm/model_executor/layers/fused_moe/config.py
+1
@@ -1306,6 +1306,7 @@ class FusedMoEConfig:
1306
1307
moe_backend: MoEBackend = "auto"
1308
max_num_tokens: int = SchedulerConfig.DEFAULT_MAX_NUM_BATCHED_TOKENS_FOR_BATCHED_DP
1309
+ elastic_ep_max_dp_size: int | None = None
1310
has_bias: bool = False
1311
is_lora_enabled: bool = False
1312
vllm/model_executor/layers/fused_moe/eep_reconfigure.py
+3
-1
@@ -60,6 +60,8 @@ def _make_eep_experts(
60
def make_eep_staged_quant_method(
61
module: "MoERunner",
62
moe_config: FusedMoEConfig,
63
+ *,
64
+ all2all_manager: Any,
65
) -> FusedMoEMethodBase | None:
66
quant_method = module._quant_method
67
if not quant_method.supports_internal_mk:
@@ -93,7 +95,7 @@ def make_eep_staged_quant_method(
95
routing_tables=None,
96
allow_new_interface=True,
97
use_monolithic=quant_method.is_monolithic,
96
- eep_stage=True,
98
+ all2all_manager=all2all_manager,
99
)
100
assert prepare_finalize is not None
101
assert isinstance(prepare_finalize, FusedMoEPrepareAndFinalizeModular)
vllm/model_executor/layers/fused_moe/layer.py
+1
@@ -348,6 +348,7 @@ def FusedMoEFactory(
348
moe_backend=vllm_config.kernel_config.moe_backend,
349
router_logits_dtype=router_logits_dtype,
350
max_num_tokens=max_num_batched_tokens,
351
+ elastic_ep_max_dp_size=vllm_config.parallel_config.elastic_ep_max_dp_size,
352
has_bias=has_bias,
353
is_lora_enabled=vllm_config.lora_config is not None,
354
activation=moe_activation,
vllm/model_executor/layers/fused_moe/modular_kernel.py
-7
@@ -252,13 +252,6 @@ class FusedMoEPrepareAndFinalize(ABC):
252
"""
253
return False
254
255
- def on_commit(self) -> None:
256
- """
257
- Runs after this prepare/finalize has been committed to the active
258
- MoE kernel.
259
- """
260
- return
261
-
255
256
# TODO: pass FusedMoEParallelConfig in as ctor parameter?
257
class FusedMoEPrepareAndFinalizeModular(FusedMoEPrepareAndFinalize):
vllm/model_executor/layers/fused_moe/prepare_finalize/nixl_ep.py
+3
-14
@@ -6,8 +6,6 @@ import nixl_ep
6
import torch
7
8
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
9
-from vllm.distributed import get_ep_group
10
-from vllm.distributed.device_communicators.all2all import NixlEPAll2AllManager
9
from vllm.logger import init_logger
10
from vllm.model_executor.layers.fused_moe.config import FusedMoEQuantConfig
11
from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
@@ -79,6 +77,7 @@ class NixlEPPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
77
buffer: nixl_ep.Buffer,
78
max_tokens_per_rank: int,
79
num_dispatchers: int,
80
+ expert_capacity: int,
81
use_fp8_dispatch: bool = False,
82
global_to_physical: torch.Tensor | None = None,
83
physical_to_global: torch.Tensor | None = None,
@@ -94,6 +93,7 @@ class NixlEPPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
93
# combine function.
94
self.handles: list[tuple | None] = [None, None]
95
self.num_dispatchers_ = num_dispatchers
96
+ self.expert_capacity = expert_capacity
97
98
topk_indices_dtype = self.topk_indices_dtype()
99
@@ -141,17 +141,6 @@ class NixlEPPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
141
def max_num_tokens_per_rank(self) -> int | None:
142
return self.max_tokens_per_rank
143
144
- def on_commit(self) -> None:
145
- device_communicator = get_ep_group().device_communicator
146
- assert device_communicator is not None
147
- all2all_manager = device_communicator.all2all_manager
148
- assert isinstance(all2all_manager, NixlEPAll2AllManager)
149
- # maybe_make_prepare_finalize(..., eep_stage=True) initializes self.buffer
150
- # with get_handle(..., stage=True), which stages global NIXL state for the
151
- # new config but leaves it inactive while the old config remains active.
152
- # When EEP commit switches to this P/F, this P/F needs to commit that state.
153
- all2all_manager.commit_staged_state()
154
-
144
def topk_indices_dtype(self) -> torch.dtype | None:
145
return NIXL_EP_TOPK_INDICES_DTYPE
146
@@ -267,7 +256,7 @@ class NixlEPPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
256
a1,
257
dispatch_topk_ids,
258
self.max_tokens_per_rank,
270
- num_experts,
259
+ self.expert_capacity,
260
use_fp8=self.use_fp8_dispatch,
261
# round_scale needs to be set to dispatch in ue8m0
262
round_scale=self.use_ue8m0_dispatch,
vllm/model_executor/layers/fused_moe/utils.py
+2
-1
@@ -500,7 +500,8 @@ def fi_moe_largest_bucket(moe_config: "FusedMoEConfig") -> int:
500
501
For a detailed explanation, see: `docs/serving/data_parallel_deployment.md`
502
"""
503
- return max(moe_config.max_num_tokens * moe_config.dp_size, 8192)
503
+ dp_size = moe_config.elastic_ep_max_dp_size or moe_config.dp_size
504
+ return max(moe_config.max_num_tokens * dp_size, 8192)
505
506
507
@torch.compile(dynamic=True, backend=current_platform.simple_compile_backend)
vllm/model_executor/warmup/flashinfer_autotune_cache.py
+39
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
12
import vllm.envs as envs
13
14
if TYPE_CHECKING:
15
+ from vllm.distributed.parallel_state import GroupCoordinator
16
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
17
18
@@ -40,6 +41,44 @@ def resolve_flashinfer_autotune_file(runner: "GPUModelRunner") -> Path:
41
return output_dir / "autotune_configs.json"
42
43
44
+def sync_flashinfer_autotune_cache(
45
+ runner: "GPUModelRunner",
46
+ group: "GroupCoordinator",
47
+) -> None:
48
+ cache: bytes | str | None = None
49
+ if (
50
+ group.rank_in_group == 0
51
+ and runner.vllm_config.kernel_config.enable_flashinfer_autotune
52
+ ):
53
+ try:
54
+ from vllm.platforms import current_platform
55
+ from vllm.utils.flashinfer import has_flashinfer
56
+
57
+ if has_flashinfer() and current_platform.has_device_capability(90):
58
+ from flashinfer.autotuner import AutoTuner
59
+
60
+ with tempfile.TemporaryDirectory() as temp_dir:
61
+ path = Path(temp_dir) / "autotune_configs.json"
62
+ AutoTuner.get().save_configs(str(path))
63
+ cache = path.read_bytes()
64
+ except Exception as exc:
65
+ cache = f"{type(exc).__name__}: {exc}"
66
+
67
+ cache = group.broadcast_object(cache)
68
+ if isinstance(cache, str):
69
+ raise RuntimeError(f"Failed to serialize FlashInfer autotune state: {cache}")
70
+ if cache is None or group.rank_in_group == 0:
71
+ return
72
+
73
+ from flashinfer.autotuner import AutoTuner
74
+
75
+ with tempfile.NamedTemporaryFile() as f:
76
+ f.write(cache)
77
+ f.flush()
78
+ if not AutoTuner.get().load_configs(f.name):
79
+ raise RuntimeError("FlashInfer autotune cache is incompatible")
80
+
81
+
82
def write_flashinfer_autotune_cache(cache_path: Path, contents: bytes) -> None:
83
cache_path.parent.mkdir(parents=True, exist_ok=True)
84
fd, tmp_path = tempfile.mkstemp(
vllm/v1/engine/core_client.py
+6
@@ -1711,6 +1711,12 @@ class DPLBAsyncMPClient(DPAsyncMPClient):
1711
"Only ray DP backend supports scaling elastic EP"
1712
)
1713
parallel_config = self.vllm_config.parallel_config
1714
+ if new_data_parallel_size > parallel_config.elastic_ep_max_dp_size:
1715
+ raise ValueError(
1716
+ f"Cannot scale to data_parallel_size {new_data_parallel_size}; "
1717
+ "--elastic-ep-max-dp-size is "
1718
+ f"{parallel_config.elastic_ep_max_dp_size}."
1719
+ )
1720
num_experts = self.vllm_config.model_config.get_num_experts()
1721
num_physical_experts = (
1722
num_experts + parallel_config.eplb_config.num_redundant_experts
vllm/v1/worker/dp_utils.py
+31
-1
@@ -1,7 +1,8 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-from contextlib import nullcontext
4
+from contextlib import contextmanager, nullcontext
5
+from contextvars import ContextVar
6
7
import torch
8
import torch.distributed as dist
@@ -18,6 +19,22 @@ from vllm.v1.worker.ubatch_utils import (
19
20
logger = init_logger(__name__)
21
22
+_skip_dp_coordination = ContextVar("skip_dp_coordination", default=False)
23
+
24
+
25
+@contextmanager
26
+def skip_dp_coordination():
27
+ """Run without coordinating DP metadata with other DP ranks."""
28
+ token = _skip_dp_coordination.set(True)
29
+ try:
30
+ yield
31
+ finally:
32
+ _skip_dp_coordination.reset(token)
33
+
34
+
35
+def should_skip_dp_coordination() -> bool:
36
+ return _skip_dp_coordination.get()
37
+
38
39
def _get_device_and_group(parallel_config: ParallelConfig):
40
# Use the actual device assigned to the DP group, not just the device type
@@ -223,6 +240,19 @@ def coordinate_batch_across_dp(
240
if num_tokens_padded is None:
241
num_tokens_padded = num_tokens_unpadded
242
243
+ if should_skip_dp_coordination():
244
+ should_ubatch = should_attempt_ubatching and not is_last_ubatch_empty(
245
+ num_tokens_unpadded,
246
+ num_tokens_padded,
247
+ parallel_config.num_ubatches,
248
+ )
249
+ num_tokens_after_padding = torch.full(
250
+ (parallel_config.data_parallel_size,),
251
+ num_tokens_padded,
252
+ dtype=torch.int32,
253
+ )
254
+ return should_ubatch, num_tokens_after_padding, cudagraph_mode
255
+
256
(should_ubatch, num_tokens_after_padding, synced_cudagraph_mode) = (
257
_synchronize_dp_ranks(
258
num_tokens_unpadded,
vllm/v1/worker/gpu/dp_utils.py
+5
-1
@@ -10,6 +10,7 @@ import torch.distributed as dist
10
from vllm.config import ParallelConfig
11
from vllm.config.compilation import CUDAGraphMode
12
from vllm.distributed.parallel_state import get_dp_group
13
+from vllm.v1.worker.dp_utils import should_skip_dp_coordination
14
from vllm.v1.worker.gpu.cudagraph_utils import (
15
BatchExecutionDescriptor,
16
CudaGraphManager,
@@ -71,7 +72,10 @@ def sync_cudagraph_and_dp_padding(
72
tensor[3][dp_rank] = max_query_len or -1 # (-1 means None)
73
tensor[4][dp_rank] = int(allow_ubatching)
74
tensor[5][dp_rank] = num_reqs
74
- dist.all_reduce(tensor, group=group)
75
+ if should_skip_dp_coordination():
76
+ tensor[:] = tensor[:, dp_rank, None].clone()
77
+ else:
78
+ dist.all_reduce(tensor, group=group)
79
80
num_tokens_across_dp = tensor[0]
81
cg_mode_across_dp = tensor[1]