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

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 5, 18:30 UTC

[EPLB] Add alternative communication for EPLB weight exchange (#33176)

Signed-off-by: ilmarkov <markovilya197@gmail.com> Signed-off-by: Markov Ilya <markovilya19@gmail.com> Co-authored-by: Markov Ilya <markovilya19@gmail.com>

Ilya Markov committed Mar 31, 2026 at 14:17 UTC abdbb683861a2586817ae6836bad308c09919d5c
12 files changed +635 -183
.buildkite/test_areas/expert_parallelism.yaml
+2 -2
@@ -13,8 +13,8 @@ steps:
13 - pytest -v -s distributed/test_eplb_algo.py
14 - pytest -v -s distributed/test_eplb_utils.py
15
16 -- label: EPLB Execution
17 - timeout_in_minutes: 20
16 +- label: EPLB Execution # 17min
17 + timeout_in_minutes: 27
18 working_dir: "/vllm-workspace/tests"
19 num_devices: 4
20 source_file_dependencies:
tests/distributed/eplb_utils.py
+39 -2
@@ -1,9 +1,11 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 +import atexit
5 import os
6 import random
7
8 +import pytest
9 import torch
10 import torch.multiprocessing as mp
11
@@ -16,9 +18,20 @@ from vllm.utils.system_utils import update_environment_variables
18 mp.set_start_method("spawn", force=True)
19
20
21 +def _distributed_worker_wrapper(fn, env, world_size, args, rank, skip_queue):
22 + try:
23 + fn(env, world_size, *args)
24 + except BaseException as exc:
25 + if isinstance(exc, pytest.skip.Exception):
26 + skip_queue.put((rank, str(exc)))
27 + return
28 + raise
29 +
30 +
31 def distributed_run(fn, world_size, *args):
32 number_of_processes = world_size
33 processes: list[mp.Process] = []
34 + skip_queue: mp.SimpleQueue = mp.SimpleQueue()
35 for i in range(number_of_processes):
36 env: dict[str, str] = {}
37 env["RANK"] = str(i)
@@ -27,13 +40,32 @@ def distributed_run(fn, world_size, *args):
40 env["LOCAL_WORLD_SIZE"] = str(number_of_processes)
41 env["MASTER_ADDR"] = "localhost"
42 env["MASTER_PORT"] = "12345"
30 - p = mp.Process(target=fn, args=(env, world_size, *args))
43 + p = mp.Process(
44 + target=_distributed_worker_wrapper,
45 + args=(fn, env, world_size, args, i, skip_queue),
46 + )
47 processes.append(p)
48 p.start()
49
50 for p in processes:
51 p.join()
52
53 + skipped: list[tuple[int, str]] = []
54 + while not skip_queue.empty():
55 + rank, reason = skip_queue.get()
56 + skipped.append((rank, reason))
57 +
58 + if len(skipped) == number_of_processes:
59 + reason = skipped[0][1]
60 + pytest.skip(reason)
61 + if 0 < len(skipped) < number_of_processes:
62 + skipped_ranks = sorted(rank for rank, _ in skipped)
63 + raise AssertionError(
64 + "Distributed test had partial skips; expected either all ranks "
65 + f"to skip or none. Skipped ranks: {skipped_ranks}, "
66 + f"total ranks: {number_of_processes}"
67 + )
68 +
69 for p in processes:
70 assert p.exitcode == 0
71
@@ -48,7 +80,12 @@ def set_env_vars_and_device(env: dict[str, str]) -> None:
80 vllm_config = VllmConfig()
81 with set_current_vllm_config(vllm_config):
82 init_distributed_environment()
51 -
83 + atexit.register(_destroy_process_group_if_initialized)
84 # Ensure each worker process has the same random seed
85 random.seed(42)
86 torch.manual_seed(42)
87 +
88 +
89 +def _destroy_process_group_if_initialized() -> None:
90 + if torch.distributed.is_available() and torch.distributed.is_initialized():
91 + torch.distributed.destroy_process_group()
tests/distributed/test_eplb_execute.py
+195 -55
@@ -9,6 +9,7 @@ import torch
9 import torch.distributed
10
11 from vllm.config import VllmConfig, set_current_vllm_config
12 +from vllm.distributed.eplb.eplb_communicator import create_eplb_communicator
13 from vllm.distributed.eplb.rebalance_execute import (
14 move_from_buffer,
15 rearrange_expert_weights_inplace,
@@ -130,9 +131,10 @@ def verify_expert_weights_after_shuffle(
131 hidden_sizes: list[int],
132 ep_rank: int,
133 num_local_experts: int,
133 -):
134 +) -> bool:
135 """Verify the weights after shuffling are correct."""
136 num_layers = len(expert_weights)
137 + ok = True
138
139 for layer in range(num_layers):
140 for weight_idx, hidden_size in enumerate(hidden_sizes):
@@ -155,29 +157,38 @@ def verify_expert_weights_after_shuffle(
157 dtype=actual_weights.dtype,
158 )
159
158 - torch.testing.assert_close(
159 - actual_weights,
160 - expected_weights,
161 - msg=f"Layer {layer}, weight {weight_idx},"
162 - f"local expert {local_expert}: "
163 - f"weights do not match. "
164 - f"Expected logical expert {expected_logical_expert}",
165 - )
160 + if not torch.equal(actual_weights, expected_weights):
161 + ok = False
162 + actual_head = actual_weights[:8].detach().cpu().tolist()
163 + expected_head = expected_weights[:8].detach().cpu().tolist()
164 + print(
165 + "verify_expert_weights_after_shuffle failed: "
166 + f"rank={ep_rank}, "
167 + f"layer={layer}, weight_idx={weight_idx}, "
168 + f"local_expert={local_expert}, "
169 + f"expected_logical_expert={expected_logical_expert}, "
170 + f"actual_head={actual_head}, expected_head={expected_head}",
171 + flush=True,
172 + )
173 +
174 + return ok
175
176
177 def verify_redundant_experts_have_same_weights(
178 expert_weights: list[list[torch.Tensor]],
179 indices: torch.Tensor,
180 hidden_sizes: list[int],
181 + ep_rank: int,
182 world_size: int,
183 num_local_experts: int,
174 -):
184 +) -> bool:
185 """
186 Verify that all replicas of the same logical expert have the same weights.
187 """
188 num_layers = len(expert_weights)
189 total_physical_experts = world_size * num_local_experts
190
191 + ok = True
192 for layer in range(num_layers):
193 # Collect weights for all physical experts for each weight matrix
194 all_weights: list[torch.Tensor] = []
@@ -227,14 +238,54 @@ def verify_redundant_experts_have_same_weights(
238 # Verify that current physical expert's weights match the
239 # previously saved logical expert weights
240 for weight_idx in range(len(hidden_sizes)):
230 - torch.testing.assert_close(
241 + if not torch.equal(
242 all_weights[weight_idx][physical_pos],
243 logical_expert_weights[logical_expert_id][weight_idx],
233 - msg=f"Layer {layer}, weight {weight_idx},"
234 - f"logical expert {logical_expert_id}: "
235 - f"Physical expert {physical_pos} has different weights"
236 - f"than expected",
237 - )
244 + ):
245 + ok = False
246 + actual_head = (
247 + all_weights[weight_idx][physical_pos][:8]
248 + .detach()
249 + .cpu()
250 + .tolist()
251 + )
252 + reference_head = (
253 + logical_expert_weights[logical_expert_id][weight_idx][:8]
254 + .detach()
255 + .cpu()
256 + .tolist()
257 + )
258 + print(
259 + "verify_redundant_experts_have_same_weights failed: "
260 + f"rank={ep_rank}, "
261 + f"layer={layer}, weight_idx={weight_idx}, "
262 + f"logical_expert={logical_expert_id}, "
263 + f"physical_pos={physical_pos}, "
264 + f"actual_head={actual_head}, "
265 + f"reference_head={reference_head}",
266 + flush=True,
267 + )
268 +
269 + return ok
270 +
271 +
272 +def assert_verification_synced(local_ok: bool, msg: str) -> None:
273 + ok_tensor = torch.tensor([1 if local_ok else 0], device="cuda", dtype=torch.int32)
274 + torch.distributed.all_reduce(ok_tensor, op=torch.distributed.ReduceOp.MIN)
275 + assert bool(ok_tensor.item()), msg
276 +
277 +
278 +def create_eplb_communicator_or_raise(*, group_coordinator, backend, expert_weights):
279 + try:
280 + return create_eplb_communicator(
281 + group_coordinator=group_coordinator,
282 + backend=backend,
283 + expert_weights=expert_weights,
284 + )
285 + except Exception as exc:
286 + raise RuntimeError(
287 + f"Failed to create EPLB communicator for backend={backend}: {exc}"
288 + ) from exc
289
290
291 def _test_async_transfer_layer_without_mtp_worker(
@@ -243,6 +294,7 @@ def _test_async_transfer_layer_without_mtp_worker(
294 num_layers: int,
295 num_local_experts: int,
296 num_logical_experts: int,
297 + eplb_communicator: str,
298 ) -> None:
299 set_env_vars_and_device(env)
300
@@ -254,8 +306,8 @@ def _test_async_transfer_layer_without_mtp_worker(
306 tensor_model_parallel_size=world_size, pipeline_model_parallel_size=1
307 )
308
257 - tp_group = get_tp_group()
258 - ep_group = tp_group.device_group
309 + ep_group_coordinator = get_tp_group()
310 + ep_group = ep_group_coordinator.device_group
311 ep_rank = torch.distributed.get_rank()
312 device = torch.device(f"cuda:{ep_rank}")
313
@@ -298,6 +350,13 @@ def _test_async_transfer_layer_without_mtp_worker(
350 expert_buffer = [torch.empty_like(w) for w in expert_weights[0]]
351 cuda_stream = torch.cuda.Stream(device=device)
352
353 + communicator = create_eplb_communicator_or_raise(
354 + group_coordinator=ep_group_coordinator,
355 + backend=eplb_communicator,
356 + expert_weights=expert_weights[0],
357 + )
358 + communicator.set_stream(cuda_stream)
359 +
360 for layer_idx in range(num_layers):
361 is_unchanged, is_received_locally, recv_metadata = asyncio.run(
362 transfer_layer(
@@ -306,6 +365,7 @@ def _test_async_transfer_layer_without_mtp_worker(
365 expert_weights=expert_weights[layer_idx],
366 expert_weights_buffer=expert_buffer,
367 ep_group=ep_group,
368 + communicator=communicator,
369 cuda_stream=cuda_stream,
370 )
371 )
@@ -320,24 +380,38 @@ def _test_async_transfer_layer_without_mtp_worker(
380 ep_rank=ep_rank,
381 )
382
323 - verify_expert_weights_after_shuffle(
324 - expert_weights,
325 - new_indices,
326 - hidden_sizes,
327 - ep_rank,
328 - num_local_experts,
329 - )
383 + local_ok = verify_expert_weights_after_shuffle(
384 + expert_weights,
385 + new_indices,
386 + hidden_sizes,
387 + ep_rank,
388 + num_local_experts,
389 + )
390 + local_ok = (
391 verify_redundant_experts_have_same_weights(
392 expert_weights,
393 new_indices,
394 hidden_sizes,
395 + ep_rank,
396 world_size,
397 num_local_experts,
398 )
399 + and local_ok
400 + )
401 + assert_verification_synced(
402 + local_ok,
403 + "Async transfer verification failed on at least one rank. "
404 + "See logs for details.",
405 + )
406
407
408 def _test_rearrange_expert_weights_with_redundancy(
340 - env, world_size, num_layers, num_local_experts, num_logical_experts
409 + env,
410 + world_size,
411 + num_layers,
412 + num_local_experts,
413 + num_logical_experts,
414 + eplb_communicator: str,
415 ) -> None:
416 # Initialize model parallel (using tensor parallel as an entrypoint
417 # to expert parallel)
@@ -351,7 +425,8 @@ def _test_rearrange_expert_weights_with_redundancy(
425 tensor_model_parallel_size=world_size, pipeline_model_parallel_size=1
426 )
427
354 - ep_group = get_tp_group().cpu_group
428 + ep_group_coordinator = get_tp_group()
429 + ep_group = ep_group_coordinator.cpu_group
430 ep_rank = torch.distributed.get_rank()
431 device = torch.device(f"cuda:{ep_rank}")
432
@@ -387,6 +462,12 @@ def _test_rearrange_expert_weights_with_redundancy(
462 num_layers, num_local_experts, hidden_sizes, ep_rank, device, old_indices
463 )
464
465 + communicator = create_eplb_communicator_or_raise(
466 + group_coordinator=ep_group_coordinator,
467 + backend=eplb_communicator,
468 + expert_weights=expert_weights[0],
469 + )
470 +
471 # Execute weight rearrangement
472 rearrange_expert_weights_inplace(
473 old_indices,
@@ -394,24 +475,33 @@ def _test_rearrange_expert_weights_with_redundancy(
475 expert_weights,
476 ep_group,
477 is_profile=False,
478 + communicator=communicator,
479 )
480
399 - # Verify the rearrangement result
400 - verify_expert_weights_after_shuffle(
401 - expert_weights,
402 - new_indices,
403 - hidden_sizes,
404 - ep_rank,
405 - num_local_experts,
406 - )
481 + # Verify the rearrangement result
482 + local_ok = verify_expert_weights_after_shuffle(
483 + expert_weights,
484 + new_indices,
485 + hidden_sizes,
486 + ep_rank,
487 + num_local_experts,
488 + )
489
490 + local_ok = (
491 verify_redundant_experts_have_same_weights(
492 expert_weights,
493 new_indices,
494 hidden_sizes,
495 + ep_rank,
496 world_size,
497 num_local_experts,
498 )
499 + and local_ok
500 + )
501 + assert_verification_synced(
502 + local_ok,
503 + "Rearrange verification failed on at least one rank. See logs for details.",
504 + )
505
506
507 @pytest.mark.parametrize(
@@ -437,8 +527,13 @@ def _test_rearrange_expert_weights_with_redundancy(
527 (4, 8, 8, 16),
528 ],
529 )
530 +@pytest.mark.parametrize("eplb_communicator", ["torch_nccl", "torch_gloo", "pynccl"])
531 def test_rearrange_expert_weights_with_redundancy(
441 - world_size, num_layers, num_local_experts, num_logical_experts
532 + world_size,
533 + num_layers,
534 + num_local_experts,
535 + num_logical_experts,
536 + eplb_communicator,
537 ):
538 """Test the functionality of rearranging expert weights with redundancy."""
539
@@ -450,6 +545,7 @@ def test_rearrange_expert_weights_with_redundancy(
545 num_layers,
546 num_local_experts,
547 num_logical_experts,
548 + eplb_communicator,
549 )
550
551
@@ -464,7 +560,8 @@ def _test_rearrange_expert_weights_no_change(env, world_size) -> None:
560 tensor_model_parallel_size=world_size, pipeline_model_parallel_size=1
561 )
562
467 - ep_group = get_tp_group().cpu_group
563 + ep_group_coordinator = get_tp_group()
564 + ep_group = ep_group_coordinator.cpu_group
565 ep_rank = torch.distributed.get_rank()
566 device = torch.device(f"cuda:{ep_rank}")
567
@@ -494,24 +591,40 @@ def _test_rearrange_expert_weights_no_change(env, world_size) -> None:
591 layer_copy.append(weight.clone())
592 original_weights.append(layer_copy)
593
594 + communicator = create_eplb_communicator_or_raise(
595 + group_coordinator=ep_group_coordinator,
596 + backend="torch_nccl",
597 + expert_weights=expert_weights[0],
598 + )
599 +
600 # Execute rearrangement (should be no change)
601 rearrange_expert_weights_inplace(
602 indices,
603 indices, # Same indices
604 expert_weights,
605 ep_group,
606 + communicator,
607 is_profile=False,
608 )
609
506 - # Verify that the weights have not changed
507 - for layer in range(num_layers):
508 - for weight_idx in range(len(hidden_sizes)):
509 - torch.testing.assert_close(
510 - expert_weights[layer][weight_idx],
511 - original_weights[layer][weight_idx],
512 - msg=f"""Layer {layer}, weight {weight_idx}
513 - should remain unchanged""",
610 + # Verify that the weights have not changed
611 + local_ok = True
612 + for layer in range(num_layers):
613 + for weight_idx in range(len(hidden_sizes)):
614 + if not torch.equal(
615 + expert_weights[layer][weight_idx],
616 + original_weights[layer][weight_idx],
617 + ):
618 + local_ok = False
619 + print(
620 + "test_rearrange_expert_weights_no_change failed: "
621 + f"layer={layer}, weight_idx={weight_idx}",
622 + flush=True,
623 )
624 + assert_verification_synced(
625 + local_ok,
626 + "No-change EPLB verification failed on at least one rank.",
627 + )
628
629
630 @pytest.mark.parametrize(
@@ -520,11 +633,13 @@ def _test_rearrange_expert_weights_no_change(env, world_size) -> None:
633 (2, 2, 2, 3),
634 ],
635 )
636 +@pytest.mark.parametrize("eplb_communicator", ["torch_nccl", "torch_gloo", "pynccl"])
637 def test_async_transfer_layer_without_mtp(
638 world_size: int,
639 num_layers: int,
640 num_local_experts: int,
641 num_logical_experts: int,
642 + eplb_communicator: str,
643 ):
644 """Exercise async EPLB transfer path without MTP/spec decode."""
645
@@ -537,6 +652,7 @@ def test_async_transfer_layer_without_mtp(
652 num_layers,
653 num_local_experts,
654 num_logical_experts,
655 + eplb_communicator,
656 )
657
658
@@ -549,7 +665,10 @@ def test_rearrange_expert_weights_no_change(world_size):
665
666 if torch.accelerator.device_count() < world_size:
667 pytest.skip(f"Need at least {world_size} GPUs to run the test")
552 - distributed_run(_test_rearrange_expert_weights_no_change, world_size)
668 + distributed_run(
669 + _test_rearrange_expert_weights_no_change,
670 + world_size,
671 + )
672
673
674 def _test_rearrange_expert_weights_profile_mode(env, world_size) -> None:
@@ -563,7 +682,8 @@ def _test_rearrange_expert_weights_profile_mode(env, world_size) -> None:
682 tensor_model_parallel_size=world_size, pipeline_model_parallel_size=1
683 )
684
566 - ep_group = get_tp_group().cpu_group
685 + ep_group_coordinator = get_tp_group()
686 + ep_group = ep_group_coordinator.cpu_group
687 ep_rank = torch.distributed.get_rank()
688 device = torch.device(f"cuda:{ep_rank}")
689
@@ -600,23 +720,40 @@ def _test_rearrange_expert_weights_profile_mode(env, world_size) -> None:
720 layer_copy.append(weight.clone())
721 original_weights.append(layer_copy)
722
723 + communicator = create_eplb_communicator_or_raise(
724 + group_coordinator=ep_group_coordinator,
725 + backend="torch_nccl",
726 + expert_weights=expert_weights[0],
727 + )
728 +
729 # Execute profile mode rearrangement
730 rearrange_expert_weights_inplace(
731 old_indices,
732 new_indices,
733 expert_weights,
734 ep_group,
735 + communicator,
736 is_profile=True, # Profile mode
737 )
738
612 - # In profile mode, the weights should remain unchanged
613 - for layer in range(num_layers):
614 - for weight_idx in range(len(hidden_sizes)):
615 - torch.testing.assert_close(
616 - expert_weights[layer][weight_idx],
617 - original_weights[layer][weight_idx],
618 - msg="In profile mode, the weights should remain unchanged",
739 + # In profile mode, the weights should remain unchanged
740 + local_ok = True
741 + for layer in range(num_layers):
742 + for weight_idx in range(len(hidden_sizes)):
743 + if not torch.equal(
744 + expert_weights[layer][weight_idx],
745 + original_weights[layer][weight_idx],
746 + ):
747 + local_ok = False
748 + print(
749 + "test_rearrange_expert_weights_profile_mode failed: "
750 + f"layer={layer}, weight_idx={weight_idx}",
751 + flush=True,
752 )
753 + assert_verification_synced(
754 + local_ok,
755 + "Profile-mode EPLB verification failed on at least one rank.",
756 + )
757
758
759 @pytest.mark.parametrize("world_size", [2, 4])
@@ -625,4 +762,7 @@ def test_rearrange_expert_weights_profile_mode(world_size):
762
763 if torch.accelerator.device_count() < world_size:
764 pytest.skip(f"Need at least {world_size} GPUs to run the test")
628 - distributed_run(_test_rearrange_expert_weights_profile_mode, world_size)
765 + distributed_run(
766 + _test_rearrange_expert_weights_profile_mode,
767 + world_size,
768 + )
vllm/config/parallel.py
+22 -10
@@ -35,6 +35,7 @@ DistributedExecutorBackend = Literal["ray", "mp", "uni", "external_launcher"]
35 DataParallelBackend = Literal["ray", "mp"]
36 EPLBPolicyOption = Literal["default"]
37 DCPCommBackend = Literal["ag_rs", "a2a"]
38 +EPLBCommunicatorBackend = Literal["torch_nccl", "torch_gloo", "pynccl"]
39 All2AllBackend = Literal[
40 "naive",
41 "pplx",
@@ -83,6 +84,15 @@ class EPLBConfig:
84 policy: EPLBPolicyOption = "default"
85 """The policy type for expert parallel load balancing (EPLB)."""
86
87 + communicator: EPLBCommunicatorBackend | None = None
88 + """
89 + Backend for EPLB expert weight communication:
90 + - "torch_nccl": Use torch.distributed on the device process group
91 + - "torch_gloo": Use torch.distributed gloo with CPU staging
92 + - "pynccl": Use PyNccl send/recv
93 + - None: Auto-select backend ("torch_gloo" for async, "torch_nccl" for sync)
94 + """
95 +
96 @model_validator(mode="after")
97 def _validate_eplb_config(self) -> Self:
98 if self.use_async and self.policy != "default":
@@ -764,16 +774,18 @@ class ParallelConfig:
774 "backend is mp, uni or external_launcher."
775 )
776
767 - if (
768 - self.all2all_backend in ("allgather_reducescatter")
769 - and self.eplb_config.use_async
770 - ):
771 - logger.warning(
772 - "Async EPLB causes hangs with the '%s' all2all backend. "
773 - "Forcing synchronous EPLB.",
774 - self.all2all_backend,
775 - )
776 - self.eplb_config.use_async = False
777 + if self.enable_eplb and self.eplb_config.communicator is None:
778 + if self.enable_elastic_ep:
779 + # Elastic EP requires stateless mode
780 + # (torch.distributed.batch_isend_irecv doesn't
781 + # support stateless mode), so we use PyNCCL backend
782 + self.eplb_config.communicator = "pynccl"
783 + elif self.eplb_config.use_async:
784 + # Torch Gloo is a backend that allows avoiding hangs
785 + # due to NCCL multi-thread conflicts in async EPLB
786 + self.eplb_config.communicator = "torch_gloo"
787 + else:
788 + self.eplb_config.communicator = "torch_nccl"
789
790 @property
791 def use_ray(self) -> bool:
vllm/distributed/device_communicators/pynccl_wrapper.py
+27 -18
@@ -23,6 +23,7 @@
23 # variable in the code.
24
25 import ctypes
26 +import functools
27 import platform
28 from dataclasses import dataclass
29 from typing import Any
@@ -75,26 +76,34 @@ class ncclDataTypeEnum:
76 ncclFloat8e4m3 = 10
77 ncclNumTypes = 11
78
79 + @classmethod
80 + @functools.lru_cache(maxsize=1)
81 + def _torch_to_nccl_map(cls) -> dict[torch.dtype, int]:
82 + return {
83 + torch.int8: cls.ncclInt8,
84 + torch.uint8: cls.ncclUint8,
85 + torch.int32: cls.ncclInt32,
86 + torch.int64: cls.ncclInt64,
87 + torch.float16: cls.ncclFloat16,
88 + torch.float32: cls.ncclFloat32,
89 + torch.float64: cls.ncclFloat64,
90 + torch.bfloat16: cls.ncclBfloat16,
91 + current_platform.fp8_dtype(): cls.ncclFloat8e4m3,
92 + }
93 +
94 + @classmethod
95 + def supports_torch_dtype(cls, dtype: torch.dtype) -> bool:
96 + return dtype in cls._torch_to_nccl_map()
97 +
98 + @classmethod
99 + def try_from_torch(cls, dtype: torch.dtype) -> int | None:
100 + return cls._torch_to_nccl_map().get(dtype)
101 +
102 @classmethod
103 def from_torch(cls, dtype: torch.dtype) -> int:
80 - if dtype == torch.int8:
81 - return cls.ncclInt8
82 - if dtype == torch.uint8:
83 - return cls.ncclUint8
84 - if dtype == torch.int32:
85 - return cls.ncclInt32
86 - if dtype == torch.int64:
87 - return cls.ncclInt64
88 - if dtype == torch.float16:
89 - return cls.ncclFloat16
90 - if dtype == torch.float32:
91 - return cls.ncclFloat32
92 - if dtype == torch.float64:
93 - return cls.ncclFloat64
94 - if dtype == torch.bfloat16:
95 - return cls.ncclBfloat16
96 - if dtype == current_platform.fp8_dtype():
97 - return cls.ncclFloat8e4m3
104 + nccl_dtype = cls.try_from_torch(dtype)
105 + if nccl_dtype is not None:
106 + return nccl_dtype
107 raise ValueError(
108 f"Unsupported dtype {dtype}: should be one of "
109 f"int8, uint8, int32, int64, float16, float32, float64, bfloat16,"
vllm/distributed/elastic_ep/elastic_execute.py
+9
@@ -29,8 +29,10 @@ from vllm.distributed.elastic_ep.standby_state import (
29 get_standby_ep_group,
30 pop_standby_groups,
31 )
32 +from vllm.distributed.eplb.eplb_communicator import create_eplb_communicator
33 from vllm.distributed.parallel_state import (
34 _replace_active_groups,
35 + get_eplb_group,
36 prepare_communication_buffer_for_model,
37 )
38 from vllm.distributed.stateless_coordinator import StatelessGroupCoordinator
@@ -411,6 +413,13 @@ class ElasticEPScalingExecutor:
413 module.quant_method = module.quant_method.old_quant_method
414 module.runner = module._init_runner()
415 prepare_communication_buffer_for_model(self.worker.model_runner.model)
416 +
417 + eplb_model_state.communicator = create_eplb_communicator(
418 + group_coordinator=get_eplb_group(),
419 + backend=parallel_config.eplb_config.communicator,
420 + expert_weights=model.expert_weights[0],
421 + )
422 +
423 if (
424 self.worker.vllm_config.compilation_config.mode
425 == CompilationMode.STOCK_TORCH_COMPILE
vllm/distributed/eplb/async_worker.py
+3
@@ -98,6 +98,8 @@ async def transfer_run_periodically(
98
99 assert state.is_async
100 for model_state in state.model_states.values():
101 + # Set the async worker's CUDA stream on the communicator
102 + model_state.communicator.set_stream(cuda_stream)
103 rebalancing_algorithm_executed = False
104 physical_to_logical_map_cpu = None
105 current_num_layers = model_state.model.num_moe_layers
@@ -157,6 +159,7 @@ async def transfer_run_periodically(
159 expert_weights=model_state.model.expert_weights[layer_idx],
160 expert_weights_buffer=model_state.expert_buffer,
161 ep_group=eplb_group,
162 + communicator=model_state.communicator,
163 is_profile=is_profile,
164 cuda_stream=cuda_stream,
165 )
vllm/distributed/eplb/eplb_communicator.py new
+277
@@ -0,0 +1,277 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""
4 +EPLB communicator implementations and factory.
5 +"""
6 +
7 +from abc import ABC, abstractmethod
8 +from collections.abc import Sequence
9 +
10 +import torch
11 +from torch.distributed import (
12 + P2POp,
13 + ProcessGroup,
14 + batch_isend_irecv,
15 +)
16 +
17 +from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator
18 +from vllm.distributed.device_communicators.pynccl_wrapper import (
19 + ncclDataTypeEnum,
20 +)
21 +from vllm.distributed.parallel_state import GroupCoordinator, is_local_first_rank
22 +from vllm.distributed.stateless_coordinator import StatelessGroupCoordinator
23 +from vllm.logger import init_logger
24 +
25 +logger = init_logger(__name__)
26 +
27 +
28 +class EplbCommunicator(ABC):
29 + """Abstract EPLB communicator for expert weight transfers."""
30 +
31 + @abstractmethod
32 + def add_send(self, tensor: torch.Tensor, dst_rank: int) -> None:
33 + pass
34 +
35 + @abstractmethod
36 + def add_recv(self, tensor: torch.Tensor, src_rank: int) -> None:
37 + pass
38 +
39 + @abstractmethod
40 + def execute(self) -> None:
41 + pass
42 +
43 + def set_stream(self, cuda_stream: torch.cuda.Stream | None) -> None:
44 + self._cuda_stream = cuda_stream
45 +
46 + def _log_initialized(self) -> None:
47 + if is_local_first_rank():
48 + logger.info("Initialized EPLB communicator: %s.", self.__class__.__name__)
49 +
50 +
51 +class TorchDistNcclEplbCommunicator(EplbCommunicator):
52 + """EPLB communicator backed by torch.distributed isend/irecv."""
53 +
54 + def __init__(
55 + self,
56 + ep_group: ProcessGroup,
57 + cuda_stream: torch.cuda.Stream | None = None,
58 + ) -> None:
59 + self._ep_group = ep_group
60 + self._cuda_stream = cuda_stream
61 + self._p2p_ops: list[P2POp] = []
62 + self._log_initialized()
63 +
64 + def add_send(self, tensor: torch.Tensor, dst_rank: int) -> None:
65 + self._p2p_ops.append(
66 + P2POp(
67 + torch.distributed.isend,
68 + tensor,
69 + dst_rank,
70 + self._ep_group,
71 + )
72 + )
73 +
74 + def add_recv(self, tensor: torch.Tensor, src_rank: int) -> None:
75 + self._p2p_ops.append(
76 + P2POp(
77 + torch.distributed.irecv,
78 + tensor,
79 + src_rank,
80 + self._ep_group,
81 + )
82 + )
83 +
84 + def execute(self) -> None:
85 + if not self._p2p_ops:
86 + return
87 + try:
88 + with torch.cuda.stream(self._cuda_stream):
89 + reqs = batch_isend_irecv(self._p2p_ops)
90 + for req in reqs:
91 + req.wait()
92 + finally:
93 + self._p2p_ops.clear()
94 +
95 +
96 +class TorchDistGlooStagedEplbCommunicator(EplbCommunicator):
97 + """EPLB communicator using gloo P2P with CPU staging."""
98 +
99 + def __init__(
100 + self,
101 + cpu_group: ProcessGroup,
102 + cuda_stream: torch.cuda.Stream | None = None,
103 + ) -> None:
104 + self._cpu_group = cpu_group
105 + self._cuda_stream = cuda_stream
106 + self._ops: list[tuple[str, torch.Tensor, int]] = []
107 + self._log_initialized()
108 +
109 + def add_send(self, tensor: torch.Tensor, dst_rank: int) -> None:
110 + self._ops.append(("send", tensor, dst_rank))
111 +
112 + def add_recv(self, tensor: torch.Tensor, src_rank: int) -> None:
113 + self._ops.append(("recv", tensor, src_rank))
114 +
115 + def execute(self) -> None:
116 + if not self._ops:
117 + return
118 +
119 + p2p_ops: list[P2POp] = []
120 + recv_staging: list[tuple[torch.Tensor, torch.Tensor]] = []
121 +
122 + def build_ops() -> None:
123 + for op, tensor, peer_rank in self._ops:
124 + if op == "send":
125 + cpu_tensor = tensor.to(device="cpu", non_blocking=True)
126 + p2p_ops.append(
127 + P2POp(
128 + torch.distributed.isend,
129 + cpu_tensor,
130 + peer_rank,
131 + self._cpu_group,
132 + )
133 + )
134 + continue
135 + cpu_tensor = torch.empty_like(tensor, device="cpu")
136 + p2p_ops.append(
137 + P2POp(
138 + torch.distributed.irecv,
139 + cpu_tensor,
140 + peer_rank,
141 + self._cpu_group,
142 + )
143 + )
144 + recv_staging.append((tensor, cpu_tensor))
145 +
146 + try:
147 + with torch.cuda.stream(self._cuda_stream):
148 + build_ops()
149 + finally:
150 + self._ops.clear()
151 +
152 + # Wait for all D2H copies to finish
153 + # before issuing gloo batch_isend_irecv operations.
154 + if self._cuda_stream is not None:
155 + self._cuda_stream.synchronize()
156 + else:
157 + torch.cuda.current_stream().synchronize()
158 +
159 + reqs = batch_isend_irecv(p2p_ops)
160 + for req in reqs:
161 + req.wait()
162 +
163 + if not recv_staging:
164 + return
165 + with torch.cuda.stream(self._cuda_stream):
166 + for dst_tensor, cpu_tensor in recv_staging:
167 + dst_tensor.copy_(cpu_tensor, non_blocking=True)
168 +
169 +
170 +class PyNcclEplbCommunicator(EplbCommunicator):
171 + """EPLB communicator backed by PyNcclCommunicator using ncclSend/ncclRecv."""
172 +
173 + def __init__(
174 + self,
175 + pynccl_comm: PyNcclCommunicator,
176 + cuda_stream: torch.cuda.Stream | None = None,
177 + ) -> None:
178 + self._pynccl_comm = pynccl_comm
179 + self._cuda_stream = cuda_stream
180 + self._group_started = False
181 + self._log_initialized()
182 +
183 + def _ensure_group_started(self) -> None:
184 + if not self._group_started:
185 + self._pynccl_comm.group_start()
186 + self._group_started = True
187 +
188 + def add_send(self, tensor: torch.Tensor, dst_rank: int) -> None:
189 + self._ensure_group_started()
190 + self._pynccl_comm.send(tensor, dst_rank, stream=self._cuda_stream)
191 +
192 + def add_recv(self, tensor: torch.Tensor, src_rank: int) -> None:
193 + self._ensure_group_started()
194 + self._pynccl_comm.recv(tensor, src_rank, stream=self._cuda_stream)
195 +
196 + def execute(self) -> None:
197 + if self._group_started:
198 + self._pynccl_comm.group_end()
199 + self._group_started = False
200 +
201 +
202 +def create_eplb_communicator(
203 + group_coordinator: GroupCoordinator,
204 + backend: str | None,
205 + expert_weights: Sequence[torch.Tensor],
206 +) -> EplbCommunicator:
207 + # Keep a safe default for callers that have not resolved communicator yet.
208 + if backend is None:
209 + backend = "torch_nccl"
210 +
211 + tensor_device_type = expert_weights[0].device.type if expert_weights else "cpu"
212 + torch_group = (
213 + group_coordinator.cpu_group
214 + if tensor_device_type == "cpu"
215 + else group_coordinator.device_group
216 + )
217 +
218 + def _create_pynccl() -> EplbCommunicator:
219 + if tensor_device_type == "cpu":
220 + raise RuntimeError(
221 + "EPLB communicator 'pynccl' supports only cuda-like devices "
222 + f"(got {tensor_device_type})."
223 + )
224 + unsupported_dtypes = sorted(
225 + {
226 + tensor.dtype
227 + for tensor in expert_weights
228 + if not ncclDataTypeEnum.supports_torch_dtype(tensor.dtype)
229 + },
230 + key=str,
231 + )
232 + if unsupported_dtypes:
233 + raise RuntimeError(
234 + "EPLB communicator 'pynccl' requested but expert weights contain "
235 + "unsupported dtypes: "
236 + f"({', '.join(str(dtype) for dtype in unsupported_dtypes)})."
237 + )
238 +
239 + device_comm = group_coordinator.device_communicator
240 + pynccl_comm = (
241 + getattr(device_comm, "pynccl_comm", None)
242 + if device_comm is not None
243 + else None
244 + )
245 + if pynccl_comm is None or pynccl_comm.disabled or not pynccl_comm.available:
246 + raise RuntimeError("EPLB communicator 'pynccl' requested but unavailable.")
247 + try:
248 + return PyNcclEplbCommunicator(pynccl_comm=pynccl_comm)
249 + except Exception as exc:
250 + raise RuntimeError(
251 + f"Failed to initialize PyNcclEplbCommunicator ({exc})."
252 + ) from exc
253 +
254 + is_stateless = isinstance(group_coordinator, StatelessGroupCoordinator)
255 + if is_stateless:
256 + if backend not in ("torch_nccl", "pynccl"):
257 + raise ValueError(
258 + f"Elastic EP requires 'torch_nccl' or 'pynccl' EPLB communicator "
259 + f"(got '{backend}'). torch_gloo is not supported with stateless groups."
260 + )
261 + if backend == "torch_nccl":
262 + logger.warning(
263 + "Stateless elastic EP requires PyNCCL backend. "
264 + "Forcing EPLB communicator to 'pynccl'."
265 + )
266 + backend = "pynccl"
267 + return _create_pynccl()
268 +
269 + if backend == "torch_gloo":
270 + return TorchDistGlooStagedEplbCommunicator(
271 + cpu_group=group_coordinator.cpu_group,
272 + )
273 + elif backend == "torch_nccl":
274 + return TorchDistNcclEplbCommunicator(ep_group=torch_group)
275 + elif backend == "pynccl":
276 + return _create_pynccl()
277 + raise ValueError(f"Unknown EPLB communicator backend: {backend}")
vllm/distributed/eplb/eplb_state.py
+23 -5
@@ -37,6 +37,7 @@ from torch.distributed import ProcessGroup, all_reduce
37 from vllm.config import ModelConfig, ParallelConfig
38 from vllm.distributed.parallel_state import (
39 get_ep_group,
40 + get_eplb_group,
41 get_node_count,
42 in_the_same_node_as,
43 )
@@ -46,6 +47,7 @@ from vllm.logger import init_logger
47 from vllm.model_executor.models.interfaces import MixtureOfExperts
48
49 from .async_worker import start_async_worker
50 +from .eplb_communicator import EplbCommunicator, create_eplb_communicator
51 from .policy import EPLB_POLICIES, AbstractEplbPolicy, DefaultEplbPolicy
52 from .rebalance_execute import (
53 RecvMetadata,
@@ -225,6 +227,10 @@ class EplbModelState:
227 """
228 CUDA device index for the async EPLB worker thread.
229 """
230 + communicator: EplbCommunicator
231 + """
232 + The communicator for expert weight transfers.
233 + """
234 new_physical_to_logical_map: torch.Tensor | None = None
235 """
236 intermediate variable between `move_to_buffer` and `move_to_workspace`.
@@ -472,6 +478,12 @@ class EplbState:
478 self._init_should_record_tensor(model)
479 expert_buffer = [torch.empty_like(w) for w in model.expert_weights[0]]
480
481 + communicator = create_eplb_communicator(
482 + group_coordinator=get_eplb_group(),
483 + backend=self.parallel_config.eplb_config.communicator,
484 + expert_weights=model.expert_weights[0],
485 + )
486 +
487 model_state = EplbModelState(
488 physical_to_logical_map=physical_to_logical_map,
489 logical_to_physical_map=logical_to_physical_map,
@@ -498,6 +510,7 @@ class EplbState:
510 recv_dst_rows=np.array([]),
511 ),
512 cuda_device_index=self.cuda_device_index,
513 + communicator=communicator,
514 new_physical_to_logical_map=None,
515 )
516 self.model_states[model_config.compute_hash()] = model_state
@@ -800,6 +813,7 @@ class EplbState:
813 new_physical_to_logical_map,
814 eplb_model_state.model.expert_weights,
815 ep_group,
816 + eplb_model_state.communicator,
817 is_profile,
818 rank_mapping,
819 )
@@ -923,11 +937,8 @@ class EplbState:
937 new_indices=new_indices,
938 ep_rank=ep_group.rank(),
939 )
926 - # Record event after consuming buffer to signal async thread
927 - # that it's safe to overwrite the intermediate buffer
928 - consumed_event = torch.cuda.Event()
929 - consumed_event.record()
930 - model_state.buffer_consumed_event = consumed_event
940 +
941 + transferred_layer = model_state.layer_to_transfer
942
943 transferred_layer = model_state.layer_to_transfer
944 assert model_state.new_physical_to_logical_map is not None
@@ -936,6 +947,13 @@ class EplbState:
947 new_physical_to_logical_map=model_state.new_physical_to_logical_map,
948 layer=transferred_layer,
949 )
950 +
951 + # Record event after consuming buffer to signal async thread
952 + # that it's safe to overwrite the intermediate buffer
953 + consumed_event = torch.cuda.Event()
954 + consumed_event.record()
955 + model_state.buffer_consumed_event = consumed_event
956 +
957 # After the main thread consumes, advance layer_to_transfer
958 model_state.layer_to_transfer += 1
959 model_state.ep_buffer_ready = 0
vllm/distributed/eplb/eplb_utils.py
+13 -2
@@ -21,6 +21,10 @@ def override_envs_for_eplb(parallel_config: ParallelConfig) -> None:
21 is_eplb_enabled = parallel_config.enable_eplb
22 async_eplb = parallel_config.eplb_config.use_async
23 is_deepep_ll = parallel_config.all2all_backend == "deepep_low_latency"
24 + is_nccl_based_eplb_communicator = parallel_config.eplb_config.communicator in (
25 + "torch_nccl",
26 + "pynccl",
27 + )
28
29 # Override NCCL_MAX_CTAS to avoid hangs when using async EPLB with the
30 # DeepEP low-latency backend.
@@ -39,7 +43,13 @@ def override_envs_for_eplb(parallel_config: ParallelConfig) -> None:
43 # Limiting NCCL occupancy via NCCL_MAX_CTAS leaves space for the DeepEP
44 # cooperative kernel to launch and complete, breaking the deadlock.
45 # See: https://github.com/deepseek-ai/DeepEP/issues/496
42 - if is_data_parallel and is_eplb_enabled and is_deepep_ll and async_eplb:
46 + if (
47 + is_data_parallel
48 + and is_eplb_enabled
49 + and is_deepep_ll
50 + and async_eplb
51 + and is_nccl_based_eplb_communicator
52 + ):
53 current_value_str = os.getenv("NCCL_MAX_CTAS")
54
55 if current_value_str and current_value_str.isdigit():
@@ -49,6 +59,7 @@ def override_envs_for_eplb(parallel_config: ParallelConfig) -> None:
59 os.environ["NCCL_MAX_CTAS"] = str(override_value)
60 logger.info_once(
61 f"EPLB: Setting NCCL_MAX_CTAS={override_value} "
52 - "for expert parallel with EPLB and deepep_low_latency backend",
62 + "for expert parallel with NCCL-based EPLB communicator and "
63 + "deepep_low_latency backend",
64 scope="global",
65 )
vllm/distributed/eplb/rebalance_execute.py
+24 -84
@@ -11,19 +11,9 @@ from dataclasses import dataclass
11
12 import numpy as np
13 import torch
14 -from torch.distributed import (
15 - P2POp,
16 - ProcessGroup,
17 - all_gather,
18 - batch_isend_irecv,
19 - get_global_rank,
20 -)
14 +from torch.distributed import ProcessGroup, all_gather
15
22 -from vllm.distributed.parallel_state import get_ep_group
23 -from vllm.distributed.stateless_coordinator import StatelessGroupCoordinator
24 -from vllm.logger import init_logger
25 -
26 -logger = init_logger(__name__)
16 +from .eplb_communicator import EplbCommunicator
17
18
19 @dataclass
@@ -158,7 +148,8 @@ def move_to_buffer(
148 expert_weights: Sequence[torch.Tensor],
149 expert_weights_buffers: Sequence[torch.Tensor],
150 cuda_stream: torch.cuda.Stream | None,
161 - ep_group: ProcessGroup,
151 + ep_rank: int,
152 + communicator: EplbCommunicator,
153 ) -> MoveToBufferResult:
154 """
155 Rearranges expert weights during EPLB rebalancing.
@@ -172,7 +163,8 @@ def move_to_buffer(
163 expert_weights: Original expert weights for the layer.
164 expert_weights_buffers: Intermediate buffers (one per tensor).
165 cuda_stream: CUDA stream for async copies (can be None for sync mode).
175 - ep_group: Distributed process group for expert parallel comms.
166 + ep_rank: Rank of this process in expert parallel group.
167 + communicator: EplbCommunicator instance for P2P communication.
168
169 Returns:
170 is_unchanged (np.ndarray): (num_local_experts,), True where an expert row
@@ -182,8 +174,6 @@ def move_to_buffer(
174 RecvMetadata: Metadata needed for completing remote weight transfers.
175 """
176 assert old_indices.shape == new_indices.shape
185 - ep_rank = ep_group.rank()
186 -
177 recv_primary_mask = np.zeros((num_local_experts,), dtype=np.bool_)
178 send_expert_ids = np.full((num_local_experts,), -1, dtype=np.int64)
179 send_src_rows = np.full((num_local_experts,), -1, dtype=np.int32)
@@ -247,22 +237,9 @@ def move_to_buffer(
237 expert = new_local_expert_ids[dst]
238 src_local = expert_to_src_map.get(expert, -1)
239 if src_local != -1:
250 - for w, b in zip(expert_weights, expert_weights_buffers):
251 - b[dst].copy_(w[src_local], non_blocking=True)
252 -
253 - p2p_ops: list[P2POp] = []
254 - if isinstance(get_ep_group(), StatelessGroupCoordinator):
255 - ep_group = get_ep_group()
256 - is_stateless = True
257 - else:
258 - is_stateless = False
259 -
260 - # Pre-compute global ranks mapping (only needed for non-stateless groups)
261 - ep_size = ep_group.size()
262 - if not is_stateless:
263 - rank_to_global = {
264 - rank: get_global_rank(ep_group, rank) for rank in range(ep_size)
265 - }
240 + with torch.cuda.stream(cuda_stream):
241 + for w, b in zip(expert_weights, expert_weights_buffers):
242 + b[dst].copy_(w[src_local], non_blocking=True)
243
244 # 2. Post sends
245 if send_count > 0:
@@ -294,23 +271,8 @@ def move_to_buffer(
271 if recver_pos < len(ranks_to_recv):
272 recv_ranks.append(ranks_to_recv[recver_pos])
273 for dst in recv_ranks:
297 - if is_stateless:
298 - for w in expert_weights:
299 - op = object.__new__(P2POp)
300 - op.op = torch.distributed.isend
301 - op.tensor = w[src]
302 - op.group_peer = dst
303 - p2p_ops.append(op)
304 - else:
305 - dst_global = rank_to_global[dst]
306 - p2p_ops += [
307 - P2POp(
308 - torch.distributed.isend,
309 - w[src],
310 - dst_global,
311 - )
312 - for w in expert_weights
313 - ]
274 + for w in expert_weights:
275 + communicator.add_send(w[src], dst)
276
277 # 3. Post recvs
278 if recv_count > 0:
@@ -339,40 +301,11 @@ def move_to_buffer(
301 src = ranks_to_send[recver_pos // num_dst_per_sender]
302 else:
303 src = ranks_to_send[recver_pos - remainder_start]
342 - if is_stateless:
343 - for b in expert_weights_buffers:
344 - op = object.__new__(P2POp)
345 - op.op = torch.distributed.irecv
346 - op.tensor = b[dst]
347 - op.group_peer = src
348 - p2p_ops.append(op)
349 - else:
350 - src_global = rank_to_global[src]
351 - p2p_ops += [
352 - P2POp(
353 - torch.distributed.irecv,
354 - b[dst],
355 - src_global,
356 - )
357 - for b in expert_weights_buffers
358 - ]
304 + for b in expert_weights_buffers:
305 + communicator.add_recv(b[dst], src)
306
307 # 4. Execute the P2P operations. The real communication happens here.
361 - if p2p_ops and cuda_stream is not None:
362 - with torch.cuda.stream(cuda_stream):
363 - if is_stateless:
364 - ep_group.device_communicator.batch_isend_irecv(p2p_ops)
365 - else:
366 - reqs = batch_isend_irecv(p2p_ops)
367 - for req in reqs:
368 - req.wait()
369 - elif p2p_ops:
370 - if is_stateless:
371 - ep_group.device_communicator.batch_isend_irecv(p2p_ops)
372 - else:
373 - reqs = batch_isend_irecv(p2p_ops)
374 - for req in reqs:
375 - req.wait()
308 + communicator.execute()
309 # wait for the communication to finish
310 return (
311 is_unchanged,
@@ -471,6 +404,7 @@ async def transfer_layer(
404 expert_weights: Sequence[torch.Tensor],
405 expert_weights_buffer: Sequence[torch.Tensor],
406 ep_group: ProcessGroup,
407 + communicator: EplbCommunicator,
408 is_profile: bool = False,
409 cuda_stream: torch.cuda.Stream | None = None,
410 rank_mapping: dict[int, int] | None = None,
@@ -489,6 +423,7 @@ async def transfer_layer(
423 For example, a linear layer may have up and down projection.
424 expert_weights_buffer: Intermediate buffers (one per weight tensor).
425 ep_group: The device process group for expert parallelism.
426 + communicator: EplbCommunicator instance for P2P communication.
427 is_profile (bool): If `True`, do not perform any actual weight copy.
428 This is used during profile run, where we only perform dummy
429 communications to reserve enough memory for the buffers.
@@ -542,7 +477,8 @@ async def transfer_layer(
477 expert_weights=expert_weights,
478 expert_weights_buffers=expert_weights_buffer,
479 cuda_stream=cuda_stream,
545 - ep_group=ep_group,
480 + ep_rank=ep_group.rank(),
481 + communicator=communicator,
482 )
483 return is_unchanged, is_received_locally, recv_metadata
484
@@ -552,6 +488,7 @@ def rearrange_expert_weights_inplace(
488 new_global_expert_indices: torch.Tensor,
489 expert_weights: Sequence[Sequence[torch.Tensor]],
490 ep_group: ProcessGroup,
491 + communicator: EplbCommunicator,
492 is_profile: bool = False,
493 rank_mapping: dict[int, int] | None = None,
494 ) -> None:
@@ -569,6 +506,7 @@ def rearrange_expert_weights_inplace(
506 For example, a linear layer may have up and down projection,
507 so weight_count = 2. Each weight's hidden size can be different.
508 ep_group: The device process group for expert parallelism.
509 + communicator: EplbCommunicator instance for P2P communication.
510 is_profile (bool): If `True`, do not perform any actual weight copy.
511 This is used during profile run, where we only perform dummy
512 communications to reserve enough memory for the buffers.
@@ -599,6 +537,7 @@ def rearrange_expert_weights_inplace(
537 assert new_global_expert_indices.shape == (num_moe_layers, num_physical_experts)
538
539 ep_size = ep_group.size()
540 + ep_rank = ep_group.rank()
541 assert num_physical_experts == ep_size * num_local_physical_experts
542
543 first_layer_weights = list(expert_weights[0])
@@ -635,7 +574,8 @@ def rearrange_expert_weights_inplace(
574 expert_weights=expert_weights[layer_idx],
575 expert_weights_buffers=weights_buffer,
576 cuda_stream=None,
638 - ep_group=ep_group,
577 + ep_rank=ep_rank,
578 + communicator=communicator,
579 )
580
581 move_from_buffer(
@@ -645,7 +585,7 @@ def rearrange_expert_weights_inplace(
585 is_received_locally=is_received_locally,
586 recv_metadata=recv_metadata,
587 new_indices=new_global_expert_indices_cpu[layer_idx],
648 - ep_rank=ep_group.rank(),
588 + ep_rank=ep_rank,
589 )
590
591
vllm/distributed/parallel_state.py
+1 -5
@@ -1690,11 +1690,7 @@ def initialize_model_parallel(
1690 # using torch.distributed in execution with torch.distributed in EPLB.
1691 global _EPLB
1692 assert _EPLB is None, "EPLB group is already initialized"
1693 - if (
1694 - config is not None
1695 - and config.parallel_config is not None
1696 - and config.parallel_config.enable_eplb
1697 - ):
1693 + if config.parallel_config.enable_eplb:
1694 if enable_elastic_ep:
1695 _EPLB = _init_stateless_group(
1696 group_ranks,