[XPU] Enable XPU eplb (#44987)

Signed-off-by: mayuyuace <qiming1.zhang@intel.com> Signed-off-by: Qiming Zhang <qiming1.zhang@intel.com>

Qiming Zhang committed Sep 16, 2026 at 22:19 UTC e6b1a5e5e3fce6b777c5f63398f87c7c911a40b3
16 files changed +267 -109
tests/conftest.py
+1 -1
@@ -935,7 +935,7 @@ def hf_runner():
935
936
937 def _default_block_size() -> int:
938 - if torch.xpu.is_available():
938 + if current_platform.is_xpu():
939 return 64
940 if current_platform.is_cpu():
941 return 128
tests/distributed/eplb_utils.py
+6 -2
@@ -13,6 +13,7 @@ from vllm.config import VllmConfig, set_current_vllm_config
13 from vllm.distributed.parallel_state import (
14 init_distributed_environment,
15 )
16 +from vllm.platforms import current_platform
17 from vllm.utils.system_utils import update_environment_variables
18
19 mp.set_start_method("spawn", force=True)
@@ -73,13 +74,16 @@ def distributed_run(fn, world_size, *args):
74 def set_env_vars_and_device(env: dict[str, str]) -> None:
75 update_environment_variables(env)
76 local_rank = os.environ["LOCAL_RANK"]
76 - device = torch.device(f"cuda:{local_rank}")
77 + accelerator_type = torch.accelerator.current_accelerator().type
78 + device = torch.device(f"{accelerator_type}:{local_rank}")
79 torch.accelerator.set_device_index(device)
80
81 + backend = "xccl" if current_platform.is_xpu() else "nccl"
82 +
83 # Create a minimal vllm config for init_distributed_environment
84 vllm_config = VllmConfig()
85 with set_current_vllm_config(vllm_config):
82 - init_distributed_environment()
86 + init_distributed_environment(backend=backend)
87 atexit.register(_destroy_process_group_if_initialized)
88 # Ensure each worker process has the same random seed
89 random.seed(42)
tests/distributed/test_elastic_ep.py
+4
@@ -10,6 +10,8 @@ from concurrent.futures import ThreadPoolExecutor
10 import pytest
11 import requests
12
13 +from vllm.platforms import current_platform
14 +
15 from ..evals.gsm8k.gsm8k_eval import evaluate_gsm8k
16 from ..utils import RemoteOpenAIServer, multi_gpu_test
17
@@ -233,6 +235,8 @@ def test_elastic_ep_scaling(
235
236 if not has_nixl():
237 pytest.skip("Async EPLB with elastic EP requires NIXL (not installed)")
238 + if current_platform.is_xpu():
239 + pytest.skip("Elastic EP with NIXL is not supported on XPU")
240 if all2all_backend == "nixl_ep" and not has_nixl_ep():
241 pytest.skip("NIXL EP is not installed")
242
tests/distributed/test_eplb_algo.py
+4 -1
@@ -10,6 +10,7 @@ from vllm.distributed.eplb.eplb_state import (
10 compute_logical_maps,
11 )
12 from vllm.distributed.eplb.policy.default import DefaultEplbPolicy
13 +from vllm.platforms import current_platform
14
15
16 def test_eplb_load_stats_reduce_across_ranks():
@@ -263,11 +264,13 @@ def test_global_load_balance_fallback():
264 assert torch.sum(logcnt) == num_replicas
265
266
266 -@pytest.mark.parametrize("device", ["cpu", "cuda"])
267 +@pytest.mark.parametrize("device", ["cpu", "cuda", "xpu"])
268 def test_device_compatibility(device):
269 """Test device compatibility."""
270 if device == "cuda" and not torch.cuda.is_available():
271 pytest.skip("CUDA not available")
272 + if device == "xpu" and not current_platform.is_xpu():
273 + pytest.skip("XPU not available")
274
275 weight = torch.tensor([[10, 20, 30, 40]], device=device)
276 num_replicas = 6
tests/distributed/test_eplb_events.py
+18 -11
@@ -4,15 +4,17 @@
4 import threading
5 import time
6
7 +import pytest
8 import torch
9
9 -from vllm.distributed.eplb.eplb_utils import CpuGpuEvent
10 +from vllm.distributed.eplb.eplb_utils import CpuGpuEvent, device_stream
11 +from vllm.platforms import current_platform
12
13
14 def test_wait_blocks_until_record():
15 event = CpuGpuEvent()
14 - record_stream = torch.cuda.Stream()
15 - wait_stream = torch.cuda.Stream()
16 + record_stream = torch.Stream()
17 + wait_stream = torch.Stream()
18 wait_returned = threading.Event()
19
20 def waiter():
@@ -33,8 +35,8 @@ def test_wait_blocks_until_record():
35
36 def test_reuse_across_multiple_cycles():
37 wrapper = CpuGpuEvent()
36 - record_stream = torch.cuda.Stream()
37 - wait_stream = torch.cuda.Stream()
38 + record_stream = torch.Stream()
39 + wait_stream = torch.Stream()
40 NUM_CYCLES = 8
41 completed_cycles = []
42 barriers = [threading.Barrier(2) for _ in range(NUM_CYCLES)]
@@ -56,13 +58,18 @@ def test_reuse_across_multiple_cycles():
58 assert len(completed_cycles) == NUM_CYCLES
59
60
59 -def test_producer_consumer():
60 - """This test uses the CpuGpuEvent to synchronize reads and writes to/from a shared
61 - GPU tensor on multiple CPU threads.
61 +@pytest.mark.parametrize("device", ["cuda", "xpu"])
62 +def test_producer_consumer(device):
63 + """This test uses the CpuGpuEvent to synchronize reads and writes to/from
64 + a shared GPU tensor on multiple CPU threads.
65 """
63 - worker_stream = torch.cuda.Stream()
66 + if device == "cuda" and not torch.cuda.is_available():
67 + pytest.skip("CUDA not available")
68 + if device == "xpu" and not current_platform.is_xpu():
69 + pytest.skip("XPU not available")
70 + worker_stream = torch.Stream()
71 # Create a single element counter that will be shared between two threads
65 - buf = torch.zeros(1, device="cuda")
72 + buf = torch.zeros(1, device=device)
73 NUM_ROUNDS = 5
74
75 ready_cpu = [threading.Event() for _ in range(NUM_ROUNDS)]
@@ -76,7 +83,7 @@ def test_producer_consumer():
83 if i > 0:
84 events[i - 1].wait(stream=worker_stream)
85
79 - with torch.cuda.stream(worker_stream):
86 + with device_stream(worker_stream):
87 buf.fill_(float(i + 1))
88
89 worker_stream.synchronize()
tests/distributed/test_eplb_execute.py
+78 -29
@@ -23,6 +23,7 @@ from vllm.distributed.parallel_state import (
23 ensure_model_parallel_initialized,
24 get_tp_group,
25 )
26 +from vllm.platforms import current_platform
27
28 from .eplb_utils import distributed_run, set_env_vars_and_device
29
@@ -208,10 +209,14 @@ def verify_redundant_experts_have_same_weights(
209 ep_rank: int,
210 world_size: int,
211 num_local_experts: int,
212 + cpu_group,
213 ) -> bool:
214 """Verify that all replicas of the same logical expert have the same weights."""
215 num_layers = len(expert_weights)
216 total_physical_experts = world_size * num_local_experts
217 + # XPU: gloo does not support XPU tensors, so gather on CPU then move back.
218 + # CUDA: gloo supports CUDA tensors directly, keep original device.
219 + use_cpu_gather = current_platform.is_xpu()
220
221 ok = True
222 for layer in range(num_layers):
@@ -219,31 +224,35 @@ def verify_redundant_experts_have_same_weights(
224 all_weights: list[torch.Tensor] = []
225
226 for weight_idx, hidden_size in enumerate(hidden_sizes):
227 + orig_device = expert_weights[layer][weight_idx].device
228 +
229 + if use_cpu_gather:
230 + local_weights = expert_weights[layer][weight_idx].cpu()
231 + gather_device = "cpu"
232 + else:
233 + local_weights = expert_weights[layer][weight_idx]
234 + gather_device = orig_device
235 +
236 # Create tensor to store all expert weights
237 # Shape: [total_physical_experts, hidden_size]
238 gathered_weights = torch.zeros(
239 total_physical_experts,
240 hidden_size,
227 - device=expert_weights[layer][weight_idx].device,
228 - dtype=expert_weights[layer][weight_idx].dtype,
241 + device=gather_device,
242 + dtype=local_weights.dtype,
243 )
244
231 - # Use all_gather to collect expert weights from current node
232 - # expert_weights[layer][weight_idx] shape:
233 - # [num_local_experts, hidden_size]
234 - local_weights = expert_weights[layer][
235 - weight_idx
236 - ] # [num_local_experts, hidden_size]
237 -
245 # Split tensor along dim 0 into a list for all_gather
246 gathered_weights_list = torch.chunk(gathered_weights, world_size, dim=0)
247
248 torch.distributed.all_gather(
242 - # Output list: each element corresponds to one rank's weights
249 list(gathered_weights_list),
244 - local_weights, # Input: current rank's local weights
250 + local_weights,
251 + group=cpu_group,
252 )
253
254 + if use_cpu_gather:
255 + gathered_weights = gathered_weights.to(orig_device)
256 all_weights.append(gathered_weights)
257
258 # Verify that all replicas of the same logical expert have the same
@@ -294,9 +303,23 @@ def verify_redundant_experts_have_same_weights(
303 return ok
304
305
297 -def assert_verification_synced(local_ok: bool, msg: str) -> None:
298 - ok_tensor = torch.tensor([1 if local_ok else 0], device="cuda", dtype=torch.int32)
299 - torch.distributed.all_reduce(ok_tensor, op=torch.distributed.ReduceOp.MIN)
306 +def assert_verification_synced(local_ok: bool, msg: str, cpu_group) -> None:
307 + if current_platform.is_xpu():
308 + # XPU: default group backend (gloo) does not support XPU tensors;
309 + # use CPU tensor with cpu_group instead.
310 + ok_tensor = torch.tensor(
311 + [1 if local_ok else 0], device="cpu", dtype=torch.int32
312 + )
313 + torch.distributed.all_reduce(
314 + ok_tensor, op=torch.distributed.ReduceOp.MIN, group=cpu_group
315 + )
316 + else:
317 + # CUDA: nccl (default group) supports device tensors directly.
318 + device = torch.accelerator.current_accelerator()
319 + ok_tensor = torch.tensor(
320 + [1 if local_ok else 0], device=device, dtype=torch.int32
321 + )
322 + torch.distributed.all_reduce(ok_tensor, op=torch.distributed.ReduceOp.MIN)
323 assert bool(ok_tensor.item()), msg
324
325
@@ -336,8 +359,10 @@ def _test_async_transfer_layer_without_mtp_worker(
359
360 ep_group_coordinator = get_tp_group()
361 ep_group = ep_group_coordinator.device_group
362 + cpu_group = ep_group_coordinator.cpu_group
363 ep_rank = torch.distributed.get_rank()
340 - device = torch.device(f"cuda:{ep_rank}")
364 + accelerator_type = torch.accelerator.current_accelerator().type
365 + device = torch.device(f"{accelerator_type}:{ep_rank}")
366
367 total_physical_experts = world_size * num_local_experts
368 hidden_sizes = [16, 32]
@@ -376,7 +401,7 @@ def _test_async_transfer_layer_without_mtp_worker(
401 new_indices_cpu = new_indices.cpu()
402
403 expert_buffer = [torch.empty_like(w) for w in expert_weights[0]]
379 - cuda_stream = torch.cuda.Stream(device=device)
404 + stream = torch.Stream(device=device)
405
406 communicator = create_eplb_communicator_or_raise(
407 group_coordinator=ep_group_coordinator,
@@ -384,7 +409,7 @@ def _test_async_transfer_layer_without_mtp_worker(
409 expert_weights=expert_weights,
410 expert_buffer=expert_buffer,
411 )
387 - communicator.set_stream(cuda_stream)
412 + communicator.set_stream(stream)
413
414 for layer_idx in range(num_layers):
415 transfer_metadata = transfer_layer(
@@ -394,10 +419,10 @@ def _test_async_transfer_layer_without_mtp_worker(
419 expert_weights_buffer=expert_buffer,
420 ep_group=ep_group,
421 communicator=communicator,
397 - cuda_stream=cuda_stream,
422 + stream=stream,
423 layer_idx=layer_idx,
424 )
400 - cuda_stream.synchronize()
425 + stream.synchronize()
426 move_from_buffer(
427 expert_weights=expert_weights[layer_idx],
428 expert_weights_buffers=expert_buffer,
@@ -421,6 +446,7 @@ def _test_async_transfer_layer_without_mtp_worker(
446 ep_rank,
447 world_size,
448 num_local_experts,
449 + cpu_group=cpu_group,
450 )
451 and local_ok
452 )
@@ -428,6 +454,7 @@ def _test_async_transfer_layer_without_mtp_worker(
454 local_ok,
455 "Async transfer verification failed on at least one rank. "
456 "See logs for details.",
457 + cpu_group=cpu_group,
458 )
459
460
@@ -452,9 +479,11 @@ def _test_rearrange_expert_weights_with_redundancy(
479 )
480
481 ep_group_coordinator = get_tp_group()
455 - ep_group = ep_group_coordinator.cpu_group
482 + ep_group = ep_group_coordinator.device_group
483 + cpu_group = ep_group_coordinator.cpu_group
484 ep_rank = torch.distributed.get_rank()
457 - device = torch.device(f"cuda:{ep_rank}")
485 + accelerator_type = torch.accelerator.current_accelerator().type
486 + device = torch.device(f"{accelerator_type}:{ep_rank}")
487
488 # Test parameters
489 total_physical_experts = world_size * num_local_experts
@@ -523,12 +552,14 @@ def _test_rearrange_expert_weights_with_redundancy(
552 ep_rank,
553 world_size,
554 num_local_experts,
555 + cpu_group=cpu_group,
556 )
557 and local_ok
558 )
559 assert_verification_synced(
560 local_ok,
561 "Rearrange verification failed on at least one rank. See logs for details.",
562 + cpu_group=cpu_group,
563 )
564
565
@@ -556,7 +587,7 @@ def _test_rearrange_expert_weights_with_redundancy(
587 ],
588 )
589 @pytest.mark.parametrize(
559 - "eplb_communicator", ["torch_nccl", "torch_gloo", "pynccl", "nixl"]
590 + "eplb_communicator", ["torch_nccl", "torch_gloo", "torch_xccl", "pynccl", "nixl"]
591 )
592 def test_rearrange_expert_weights_with_redundancy(
593 world_size,
@@ -568,6 +599,12 @@ def test_rearrange_expert_weights_with_redundancy(
599 """Test the functionality of rearranging expert weights with redundancy."""
600 if eplb_communicator == "nixl" and not has_nixl():
601 pytest.skip("NIXL is not available")
602 + if eplb_communicator == "nixl" and current_platform.is_xpu():
603 + pytest.skip("NIXL does not support XPU")
604 + if eplb_communicator in ("torch_nccl", "pynccl") and not torch.cuda.is_available():
605 + pytest.skip(f"{eplb_communicator} requires CUDA")
606 + if eplb_communicator == "torch_xccl" and not current_platform.is_xpu():
607 + pytest.skip("torch_xccl requires XPU")
608 if torch.accelerator.device_count() < world_size:
609 pytest.skip(f"Need at least {world_size} GPUs to run the test")
610 distributed_run(
@@ -592,9 +629,11 @@ def _test_rearrange_expert_weights_no_change(env, world_size) -> None:
629 )
630
631 ep_group_coordinator = get_tp_group()
595 - ep_group = ep_group_coordinator.cpu_group
632 + ep_group = ep_group_coordinator.device_group
633 + cpu_group = ep_group_coordinator.cpu_group
634 ep_rank = torch.distributed.get_rank()
597 - device = torch.device(f"cuda:{ep_rank}")
635 + accelerator_type = torch.accelerator.current_accelerator().type
636 + device = torch.device(f"{accelerator_type}:{ep_rank}")
637
638 num_layers = 2
639 num_local_experts = 2
@@ -623,9 +662,10 @@ def _test_rearrange_expert_weights_no_change(env, world_size) -> None:
662 original_weights.append(layer_copy)
663
664 expert_buffer = [torch.empty_like(w) for w in expert_weights[0]]
665 + default_backend = "torch_xccl" if current_platform.is_xpu() else "torch_nccl"
666 communicator = create_eplb_communicator_or_raise(
667 group_coordinator=ep_group_coordinator,
628 - backend="torch_nccl",
668 + backend=default_backend,
669 expert_weights=expert_weights,
670 expert_buffer=expert_buffer,
671 )
@@ -657,6 +697,7 @@ def _test_rearrange_expert_weights_no_change(env, world_size) -> None:
697 assert_verification_synced(
698 local_ok,
699 "No-change EPLB verification failed on at least one rank.",
700 + cpu_group=cpu_group,
701 )
702
703
@@ -666,7 +707,7 @@ def _test_rearrange_expert_weights_no_change(env, world_size) -> None:
707 (2, 2, 2, 3),
708 ],
709 )
669 -@pytest.mark.parametrize("eplb_communicator", ["torch_gloo", "nixl"])
710 +@pytest.mark.parametrize("eplb_communicator", ["torch_gloo", "torch_xccl", "nixl"])
711 def test_async_transfer_layer_without_mtp(
712 world_size: int,
713 num_layers: int,
@@ -677,6 +718,10 @@ def test_async_transfer_layer_without_mtp(
718 """Exercise async EPLB transfer path without MTP/spec decode."""
719 if eplb_communicator == "nixl" and not has_nixl():
720 pytest.skip("NIXL is not available")
721 + if eplb_communicator == "nixl" and current_platform.is_xpu():
722 + pytest.skip("NIXL does not support XPU")
723 + if eplb_communicator == "torch_xccl" and not current_platform.is_xpu():
724 + pytest.skip("torch_xccl requires XPU")
725 if torch.accelerator.device_count() < world_size:
726 pytest.skip(f"Need at least {world_size} GPUs to run the test")
727
@@ -715,9 +760,11 @@ def _test_rearrange_expert_weights_profile_mode(env, world_size) -> None:
760 )
761
762 ep_group_coordinator = get_tp_group()
718 - ep_group = ep_group_coordinator.cpu_group
763 + ep_group = ep_group_coordinator.device_group
764 + cpu_group = ep_group_coordinator.cpu_group
765 ep_rank = torch.distributed.get_rank()
720 - device = torch.device(f"cuda:{ep_rank}")
766 + accelerator_type = torch.accelerator.current_accelerator().type
767 + device = torch.device(f"{accelerator_type}:{ep_rank}")
768
769 num_layers = 1
770 num_local_experts = 2
@@ -753,9 +800,10 @@ def _test_rearrange_expert_weights_profile_mode(env, world_size) -> None:
800 original_weights.append(layer_copy)
801
802 expert_buffer = [torch.empty_like(w) for w in expert_weights[0]]
803 + default_backend = "torch_xccl" if current_platform.is_xpu() else "torch_nccl"
804 communicator = create_eplb_communicator_or_raise(
805 group_coordinator=ep_group_coordinator,
758 - backend="torch_nccl",
806 + backend=default_backend,
807 expert_weights=expert_weights,
808 expert_buffer=expert_buffer,
809 )
@@ -788,6 +836,7 @@ def _test_rearrange_expert_weights_profile_mode(env, world_size) -> None:
836 assert_verification_synced(
837 local_ok,
838 "Profile-mode EPLB verification failed on at least one rank.",
839 + cpu_group=cpu_group,
840 )
841
842
tests/distributed/test_eplb_fused_moe_layer.py
+6 -3
@@ -17,6 +17,7 @@ from vllm.distributed.parallel_state import (
17 get_tp_group,
18 )
19 from vllm.model_executor.layers.fused_moe import FusedMoEFactory, MoERunner
20 +from vllm.platforms import current_platform
21
22 from .eplb_utils import distributed_run, set_env_vars_and_device
23
@@ -81,7 +82,7 @@ def make_fused_moe_layer(
82 )
83 re = fml.routed_experts
84
84 - device = torch.device(f"cuda:{rank}")
85 + device = torch.device(f"{torch.accelerator.current_accelerator().type}:{rank}")
86
87 from functools import partial
88
@@ -190,13 +191,14 @@ def _test_eplb_fml(env, world_size: int, test_config: TestConfig):
191 vllm_config = VllmConfig()
192 vllm_config.parallel_config.tensor_parallel_size = world_size
193 vllm_config.parallel_config.enable_expert_parallel = True
194 + vllm_config.parallel_config.enable_eplb = True
195
196 with set_current_vllm_config(vllm_config):
197 ensure_model_parallel_initialized(
198 tensor_model_parallel_size=world_size, pipeline_model_parallel_size=1
199 )
200
199 - ep_group = get_tp_group().cpu_group
201 + ep_group = get_tp_group().device_group
202 ep_rank = torch.distributed.get_rank()
203
204 fml_layers = [
@@ -216,9 +218,10 @@ def _test_eplb_fml(env, world_size: int, test_config: TestConfig):
218 shuffled_indices[lidx] = torch.randperm(test_config.num_experts)
219
220 expert_buffer = [torch.empty_like(w) for w in rank_expert_weights[0]]
221 + default_backend = "torch_xccl" if current_platform.is_xpu() else "torch_nccl"
222 communicator = create_eplb_communicator(
223 group_coordinator=get_eplb_group(),
221 - backend="torch_nccl",
224 + backend=default_backend,
225 expert_weights=rank_expert_weights,
226 expert_buffer=expert_buffer,
227 )
tests/distributed/test_eplb_fused_moe_layer_dep_nvfp4.py
+3
@@ -24,6 +24,7 @@ from vllm.model_executor.layers.quantization.modelopt import (
24 ModelOptNvFp4Config,
25 ModelOptNvFp4FusedMoE,
26 )
27 +from vllm.platforms import current_platform
28
29 from .eplb_utils import distributed_run, set_env_vars_and_device
30
@@ -270,6 +271,8 @@ def test_eplb_fml(
271 ):
272 if torch.accelerator.device_count() < world_size:
273 pytest.skip(f"Need at least {world_size} GPUs to run the test")
274 + if current_platform.is_xpu():
275 + pytest.skip("NVFP4 quantization and flashinfer backends are CUDA-only")
276
277 num_local_experts = num_experts // world_size
278 num_topk = 4
vllm/config/parallel.py
+12 -5
@@ -38,7 +38,9 @@ DistributedExecutorBackend = Literal["ray", "mp", "uni", "external_launcher"]
38 DataParallelBackend = Literal["ray", "mp"]
39 EPLBPolicyOption = Literal["default"]
40 DCPCommBackend = Literal["ag_rs", "a2a"]
41 -EPLBCommunicatorBackend = Literal["torch_nccl", "torch_gloo", "nixl", "pynccl"]
41 +EPLBCommunicatorBackend = Literal[
42 + "torch_nccl", "torch_gloo", "torch_xccl", "nixl", "pynccl"
43 +]
44 All2AllBackend = Literal[
45 "naive",
46 "pplx",
@@ -94,9 +96,11 @@ class EPLBConfig:
96 Backend for EPLB expert weight communication:
97 - "torch_nccl": Use torch.distributed on the device process group
98 - "torch_gloo": Use torch.distributed gloo with CPU staging
99 + - "torch_xccl": Use torch.distributed XCCL device P2P on XPU
100 - "nixl": Use NIXL with staged send/recv buffers
101 - "pynccl": Use PyNccl send/recv
99 - - None: Auto-select backend (prefers "nixl", falls back to "torch_gloo")
102 + - None: Auto-select backend ("torch_xccl" on XPU, prefers "nixl"
103 + on CUDA, falls back to "torch_gloo")
104 """
105
106 @model_validator(mode="after")
@@ -525,10 +529,10 @@ class ParallelConfig:
529 )
530
531 if self.enable_eplb:
528 - if not current_platform.is_cuda_alike():
532 + if not current_platform.is_cuda_alike() and not current_platform.is_xpu():
533 raise ValueError(
534 "Expert parallelism load balancing is only supported on "
531 - "CUDA devices or ROCm devices now."
535 + "CUDA devices or ROCm devices or XPU devices now."
536 )
537 if not self.enable_expert_parallel:
538 raise ValueError("enable_expert_parallel must be True to use EPLB.")
@@ -1034,7 +1038,10 @@ class ParallelConfig:
1038 # See https://github.com/pytorch/pytorch/issues/174288
1039 from vllm.distributed.nixl_utils import is_nixl_available
1040
1037 - if is_nixl_available():
1041 + if current_platform.is_xpu():
1042 + # On XPU, use the device-native XCCL P2P backend.
1043 + self.eplb_config.communicator = "torch_xccl"
1044 + elif is_nixl_available():
1045 self.eplb_config.communicator = "nixl"
1046 elif self.enable_elastic_ep:
1047 self.eplb_config.communicator = "pynccl"
vllm/distributed/eplb/async_worker.py
+14 -14
@@ -10,7 +10,7 @@ import torch
10 from vllm.distributed.parallel_state import get_eplb_group
11 from vllm.logger import init_logger
12
13 -from .eplb_utils import CpuGpuEvent
13 +from .eplb_utils import CpuGpuEvent, device_stream
14 from .rebalance_execute import AsyncEplbLayerResult, transfer_layer
15
16 if TYPE_CHECKING:
@@ -24,17 +24,17 @@ def start_async_worker(
24 is_profile: bool = False,
25 ) -> threading.Thread:
26 rank = get_eplb_group().device_group.rank()
27 - device_index = state.cuda_device_index
27 + device_index = state.device_index
28 assert state.is_async
29
30 def thread_target() -> None:
31 assert device_index is not None
32 torch.accelerator.set_device_index(device_index)
33 - cuda_stream = torch.cuda.Stream(device=device_index)
33 + stream = torch.Stream(device=torch.accelerator.current_accelerator())
34 try:
35 transfer_run_periodically(
36 state=state,
37 - cuda_stream=cuda_stream,
37 + stream=stream,
38 is_profile=is_profile,
39 )
40 except Exception as exc: # pragma: no cover - diagnostic path
@@ -49,13 +49,13 @@ def run_rebalance_experts(
49 model_state: "EplbModelState",
50 eplb_state: "EplbState",
51 physical_to_logical_map_cpu: torch.Tensor,
52 - cuda_stream: torch.cuda.Stream,
52 + stream: torch.Stream,
53 ) -> torch.Tensor:
54 assert model_state.eplb_stats is not None
55 eplb_stats = model_state.eplb_stats
56
57 # Move the global expert load window to CPU for computation.
58 - with torch.cuda.stream(cuda_stream):
58 + with device_stream(stream):
59 global_expert_load_window = eplb_stats.global_expert_load_window.cpu()
60 # Compute new expert mappings for the model
61 new_physical_to_logical_map = eplb_state.policy.rebalance_experts(
@@ -73,11 +73,11 @@ def run_rebalance_experts(
73
74 def transfer_run_periodically(
75 state: "EplbState",
76 - cuda_stream: torch.cuda.Stream,
76 + stream: torch.Stream,
77 is_profile: bool = False,
78 ) -> None:
79 while True:
80 - state.rearrange_event.wait(stream=cuda_stream)
80 + state.rearrange_event.wait(stream=stream)
81
82 eplb_group = get_eplb_group().device_group
83 eplb_cpu_group = get_eplb_group().cpu_group
@@ -87,16 +87,16 @@ def transfer_run_periodically(
87 for model_state in state.model_states.values():
88 layer_idx = 0
89 # Set the async worker's CUDA stream on the communicator
90 - model_state.communicator.set_stream(cuda_stream)
90 + model_state.communicator.set_stream(stream)
91 num_layers = model_state.model.num_moe_layers
92
93 # Snapshot the physical_to_logical_map (synchronized with
94 # rearrange_event) and copy it to CPU
95 - with torch.cuda.stream(cuda_stream):
95 + with device_stream(stream):
96 physical_to_logical_map_cpu = model_state.physical_to_logical_map.cpu()
97
98 new_physical_to_logical_map = run_rebalance_experts(
99 - model_state, state, physical_to_logical_map_cpu, cuda_stream
99 + model_state, state, physical_to_logical_map_cpu, stream
100 )
101
102 # Execute one EPLB layer transfer per model forward pass. Each iteration
@@ -132,13 +132,13 @@ def transfer_run_periodically(
132 communicator=model_state.communicator,
133 ep_group=eplb_group,
134 is_profile=is_profile,
135 - cuda_stream=cuda_stream,
135 + stream=stream,
136 layer_idx=layer_idx,
137 )
138
139 # Wait until all writes to expert_buffer have finished before making the
140 # AsyncEplbLayerResult visible to the main thread.
141 - cuda_stream.synchronize()
141 + stream.synchronize()
142
143 # This event guarantees that expert_buffer will not be overwritten by
144 # subsequent iterations of this loop until the main thread has consumed
@@ -155,6 +155,6 @@ def transfer_run_periodically(
155 # Block this thread until the main thread and main stream
156 # finish copying model_state.expert_buffer into
157 # model_state.model.expert_weights[layer_idx]
158 - consumed_event.wait(stream=cuda_stream)
158 + consumed_event.wait(stream=stream)
159 assert model_state.pending_result is None
160 layer_idx += 1
vllm/distributed/eplb/eplb_communicator.py
+78 -17
@@ -22,6 +22,7 @@ from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator
22 from vllm.distributed.device_communicators.pynccl_wrapper import (
23 ncclDataTypeEnum,
24 )
25 +from vllm.distributed.eplb.eplb_utils import device_stream
26 from vllm.distributed.parallel_state import (
27 GroupCoordinator,
28 get_pp_group,
@@ -87,8 +88,8 @@ class EplbCommunicator(ABC):
88 communication buffers."""
89 return True
90
90 - def set_stream(self, cuda_stream: torch.cuda.Stream | None) -> None:
91 - self._cuda_stream = cuda_stream
91 + def set_stream(self, stream: torch.Stream | None) -> None:
92 + self._stream = stream
93
94 def _log_initialized(self) -> None:
95 if is_local_first_rank():
@@ -101,10 +102,10 @@ class TorchDistNcclEplbCommunicator(EplbCommunicator):
102 def __init__(
103 self,
104 ep_group: ProcessGroup,
104 - cuda_stream: torch.cuda.Stream | None = None,
105 + stream: torch.Stream | None = None,
106 ) -> None:
107 self._ep_group = ep_group
107 - self._cuda_stream = cuda_stream
108 + self._stream = stream
109 self._p2p_ops: list[P2POp] = []
110 self._log_initialized()
111
@@ -144,7 +145,7 @@ class TorchDistNcclEplbCommunicator(EplbCommunicator):
145 if not self._p2p_ops:
146 return
147 try:
147 - with torch.cuda.stream(self._cuda_stream):
148 + with device_stream(self._stream):
149 reqs = batch_isend_irecv(self._p2p_ops)
150 for req in reqs:
151 req.wait()
@@ -158,10 +159,10 @@ class TorchDistGlooStagedEplbCommunicator(EplbCommunicator):
159 def __init__(
160 self,
161 cpu_group: ProcessGroup,
161 - cuda_stream: torch.cuda.Stream | None = None,
162 + stream: torch.Stream | None = None,
163 ) -> None:
164 self._cpu_group = cpu_group
164 - self._cuda_stream = cuda_stream
165 + self._stream = stream
166 self._ops: list[tuple[str, torch.Tensor, int]] = []
167 self._log_initialized()
168
@@ -217,7 +218,7 @@ class TorchDistGlooStagedEplbCommunicator(EplbCommunicator):
218 recv_staging.append((tensor, cpu_tensor))
219
220 try:
220 - with torch.cuda.stream(self._cuda_stream):
221 + with device_stream(self._stream):
222 build_ops()
223 finally:
224 self._ops.clear()
@@ -225,10 +226,10 @@ class TorchDistGlooStagedEplbCommunicator(EplbCommunicator):
226 # Wait for all D2H copies to finish
227 # before issuing gloo batch_isend_irecv operations.
228 with gpu_sync_allowed():
228 - if self._cuda_stream is not None:
229 - self._cuda_stream.synchronize()
229 + if self._stream is not None:
230 + self._stream.synchronize()
231 else:
231 - torch.cuda.current_stream().synchronize()
232 + torch.accelerator.current_stream().synchronize()
233
234 reqs = batch_isend_irecv(p2p_ops)
235 for req in reqs:
@@ -236,11 +237,69 @@ class TorchDistGlooStagedEplbCommunicator(EplbCommunicator):
237
238 if not recv_staging:
239 return
239 - with torch.cuda.stream(self._cuda_stream):
240 + with device_stream(self._stream):
241 for dst_tensor, cpu_tensor in recv_staging:
242 dst_tensor.copy_(cpu_tensor, non_blocking=True)
243
244
245 +class TorchDistXCCLStagedEplbCommunicator(EplbCommunicator):
246 + """EPLB communicator using XCCL device-to-device P2P on XPU."""
247 +
248 + def __init__(
249 + self,
250 + ep_group: ProcessGroup,
251 + stream: torch.Stream | None = None,
252 + ) -> None:
253 + self._ep_group = ep_group
254 + self._stream = stream
255 + self._ops: list[tuple[str, torch.Tensor, int]] = []
256 + self._log_initialized()
257 +
258 + def add_send(
259 + self,
260 + tensors: list[torch.Tensor],
261 + dst_rank: int,
262 + expert_id: int, # unused by this backend
263 + ) -> None:
264 + for tensor in tensors:
265 + self._ops.append(("send", tensor, dst_rank))
266 +
267 + def add_recv(
268 + self,
269 + tensors: list[torch.Tensor],
270 + src_rank: int,
271 + expert_id: int, # unused by this backend
272 + ) -> None:
273 + for tensor in tensors:
274 + self._ops.append(("recv", tensor, src_rank))
275 +
276 + def execute(self) -> None:
277 + if not self._ops:
278 + return
279 +
280 + p2p_ops: list[P2POp] = []
281 + try:
282 + for op, tensor, peer_rank in self._ops:
283 + send_or_recv = (
284 + torch.distributed.isend if op == "send" else torch.distributed.irecv
285 + )
286 + p2p_ops.append(
287 + P2POp(
288 + send_or_recv,
289 + tensor,
290 + peer_rank,
291 + self._ep_group,
292 + )
293 + )
294 + finally:
295 + self._ops.clear()
296 +
297 + with device_stream(self._stream):
298 + reqs = batch_isend_irecv(p2p_ops)
299 + for req in reqs:
300 + req.wait()
301 +
302 +
303 class NixlEplbCommunicator(EplbCommunicator):
304 """EPLB communicator backed by NIXL READ transfers."""
305
@@ -343,7 +402,7 @@ class NixlEplbCommunicator(EplbCommunicator):
402 uid = uuid.uuid4().hex[:8]
403 return f"eplb-{self._rank}{pp_suffix}-{uid}"
404
346 - def set_stream(self, cuda_stream: torch.cuda.Stream | None) -> None:
405 + def set_stream(self, stream: torch.Stream | None) -> None:
406 pass
407
408 def add_send(
@@ -602,10 +661,10 @@ class PyNcclEplbCommunicator(EplbCommunicator):
661 def __init__(
662 self,
663 pynccl_comm: PyNcclCommunicator,
605 - cuda_stream: torch.cuda.Stream | None = None,
664 + stream: torch.Stream | None = None,
665 ) -> None:
666 self._pynccl_comm = pynccl_comm
608 - self._cuda_stream = cuda_stream
667 + self._stream = stream
668 self._group_started = False
669 self._log_initialized()
670
@@ -622,7 +681,7 @@ class PyNcclEplbCommunicator(EplbCommunicator):
681 ) -> None:
682 self._ensure_group_started()
683 for tensor in tensors:
625 - self._pynccl_comm.send(tensor, dst_rank, stream=self._cuda_stream)
684 + self._pynccl_comm.send(tensor, dst_rank, stream=self._stream)
685
686 def add_recv(
687 self,
@@ -632,7 +691,7 @@ class PyNcclEplbCommunicator(EplbCommunicator):
691 ) -> None:
692 self._ensure_group_started()
693 for tensor in tensors:
635 - self._pynccl_comm.recv(tensor, src_rank, stream=self._cuda_stream)
694 + self._pynccl_comm.recv(tensor, src_rank, stream=self._stream)
695
696 def execute(self) -> None:
697 if self._group_started:
@@ -750,6 +809,8 @@ def create_eplb_communicator(
809 return TorchDistGlooStagedEplbCommunicator(
810 cpu_group=group_coordinator.cpu_group,
811 )
812 + elif backend == "torch_xccl":
813 + return TorchDistXCCLStagedEplbCommunicator(ep_group=torch_group)
814 elif backend == "torch_nccl":
815 return TorchDistNcclEplbCommunicator(ep_group=torch_group)
816 elif backend == "pynccl":
vllm/distributed/eplb/eplb_state.py
+11 -11
@@ -203,9 +203,9 @@ class EplbModelState:
203 """
204 EPLB stats for the model.
205 """
206 - cuda_device_index: int | None
206 + device_index: int | None
207 """
208 - CUDA device index for the async EPLB worker thread.
208 + Device index for the async EPLB worker thread.
209 """
210 communicator: EplbCommunicator
211 """
@@ -286,14 +286,14 @@ class EplbState:
286 """
287 Background thread handling async transfers.
288 """
289 - self.cuda_device_index: int | None = None
289 + self.device_index: int | None = None
290 """
291 - CUDA device index for the async EPLB worker thread.
291 + Device index for the async EPLB worker thread.
292 """
293 - if self.device.type == "cuda":
294 - self.cuda_device_index = self.device.index
295 - if self.cuda_device_index is None and torch.cuda.is_available():
296 - self.cuda_device_index = torch.accelerator.current_device_index()
293 + if self.device.type in ("cuda", "xpu"):
294 + self.device_index = self.device.index
295 + if self.device_index is None and torch.accelerator.is_available():
296 + self.device_index = torch.accelerator.current_device_index()
297
298 @staticmethod
299 def build_initial_global_physical_to_logical_map(
@@ -504,7 +504,7 @@ class EplbState:
504 expert_buffer=expert_buffer,
505 rebalanced=False,
506 eplb_stats=None,
507 - cuda_device_index=self.cuda_device_index,
507 + device_index=self.device_index,
508 communicator=communicator,
509 num_unpadded_tokens_tensors=num_unpadded_tokens_tensors,
510 )
@@ -763,8 +763,8 @@ class EplbState:
763 is_main_rank = ep_rank == 0
764 if is_main_rank:
765 if not self.is_async or is_profile:
766 - start_event = torch.cuda.Event(enable_timing=True)
767 - end_event = torch.cuda.Event(enable_timing=True)
766 + start_event = torch.Event(enable_timing=True)
767 + end_event = torch.Event(enable_timing=True)
768 start_event.record()
769 logger.info(
770 "Rearranging experts %s %s...",
vllm/distributed/eplb/eplb_utils.py
+21 -4
@@ -2,6 +2,7 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Utility functions for EPLB (Expert Parallel Load Balancing)."""
4
5 +import contextlib
6 import os
7 import threading
8
@@ -13,6 +14,22 @@ from vllm.logger import init_logger
14 logger = init_logger(__name__)
15
16
17 +@contextlib.contextmanager
18 +def device_stream(stream: torch.Stream | None):
19 + """Platform-agnostic context manager that activates *stream* as the
20 + current accelerator stream for the duration of the ``with`` block.
21 + A no-op when *stream* is ``None``."""
22 + if stream is None:
23 + yield
24 + return
25 + prev = torch.accelerator.current_stream()
26 + torch.accelerator.set_stream(stream)
27 + try:
28 + yield
29 + finally:
30 + torch.accelerator.set_stream(prev)
31 +
32 +
33 class CpuGpuEvent:
34 """Combines a CUDA event with a CPU threading event to enforce record->wait
35 ordering across two threads.
@@ -30,10 +47,10 @@ class CpuGpuEvent:
47 """
48
49 def __init__(self):
33 - self._event = torch.cuda.Event()
50 + self._event = torch.Event()
51 self._recorded = threading.Event()
52
36 - def wait(self, stream: torch.cuda.Stream | None = None):
53 + def wait(self, stream: torch.Stream | None = None):
54 """Blocks the calling thread until record finishes. Used to guarantee that the
55 record kernel is called before wait.
56
@@ -43,7 +60,7 @@ class CpuGpuEvent:
60 self._event.wait(stream)
61 self._recorded.clear()
62
46 - def record(self, stream: torch.cuda.Stream | None = None):
63 + def record(self, stream: torch.Stream | None = None):
64 """Unblocks the waiting thread after calling event.record().
65
66 Should only be called by the main thread.
@@ -53,7 +70,7 @@ class CpuGpuEvent:
70 "CpuGpuEvent.record() called before the previous event was "
71 "consumed by wait()"
72 )
56 - self._event = torch.cuda.Event()
73 + self._event = torch.Event()
74 self._event.record(stream)
75 self._recorded.set()
76
vllm/distributed/eplb/rebalance_execute.py
+9 -10
@@ -13,7 +13,7 @@ import torch
13 from torch.distributed import ProcessGroup, all_gather
14
15 from vllm.distributed.eplb.eplb_communicator import EplbCommunicator
16 -from vllm.distributed.eplb.eplb_utils import CpuGpuEvent
16 +from vllm.distributed.eplb.eplb_utils import CpuGpuEvent, device_stream
17 from vllm.logger import init_logger
18 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
19
@@ -173,7 +173,7 @@ def move_to_buffer(
173 new_indices: np.ndarray,
174 expert_weights: Sequence[torch.Tensor],
175 expert_weights_buffers: Sequence[torch.Tensor],
176 - cuda_stream: torch.cuda.Stream | None,
176 + stream: torch.Stream | None,
177 ep_rank: int,
178 communicator: EplbCommunicator,
179 layer_idx: int = 0,
@@ -183,12 +183,12 @@ def move_to_buffer(
183 Args:
184 num_local_experts: Number of local experts.
185 old_indices: (num_experts_total,) ndarray of current (old)
186 - global-to-local expert assignments.
186 + global-to-locals expert assignments.
187 new_indices: (num_experts_total,) ndarray of desired (new)
188 global-to-local assignments after rebalance.
189 expert_weights: Original expert weights for the layer.
190 expert_weights_buffers: Intermediate buffers (one per tensor).
191 - cuda_stream: CUDA stream for async copies (can be None for sync mode).
191 + stream: CUDA/XPU stream for async copies (can be None for sync mode).
192 ep_rank: Rank of this process in expert parallel group.
193 communicator: EplbCommunicator instance for P2P communication.
194 layer_idx: Index of the MoE layer being transferred.
@@ -261,10 +261,9 @@ def move_to_buffer(
261 expert = new_local_expert_ids[dst]
262 src_local = expert_to_src_map.get(expert, -1)
263 if src_local != -1:
264 - with torch.cuda.stream(cuda_stream):
264 + with device_stream(stream):
265 for w, b in zip(expert_weights, expert_weights_buffers):
266 b[dst].copy_(w[src_local], non_blocking=True)
267 -
267 communicator.set_transfer_context(old_indices, layer_idx)
268
269 # 2. Post sends
@@ -430,7 +429,7 @@ def transfer_layer(
429 ep_group: ProcessGroup,
430 communicator: EplbCommunicator,
431 is_profile: bool = False,
433 - cuda_stream: torch.cuda.Stream | None = None,
432 + stream: torch.Stream | None = None,
433 rank_mapping: dict[int, int] | None = None,
434 layer_idx: int = 0,
435 ) -> TransferMetadata:
@@ -451,7 +450,7 @@ def transfer_layer(
450 is_profile (bool): If `True`, do not perform any actual weight copy.
451 This is used during profile run, where we only perform dummy
452 communications to reserve enough memory for the buffers.
454 - cuda_stream: CUDA stream for async copies (can be None for sync mode).
453 + stream: CUDA stream for async copies (can be None for sync mode).
454 rank_mapping: Optional rank mapping for elastic expert parallelism.
455 layer_idx: Index of the MoE layer being transferred.
456
@@ -499,7 +498,7 @@ def transfer_layer(
498 new_indices=new_layer_indices_np,
499 expert_weights=expert_weights,
500 expert_weights_buffers=expert_weights_buffer,
502 - cuda_stream=cuda_stream,
501 + stream=stream,
502 ep_rank=ep_group.rank(),
503 communicator=communicator,
504 layer_idx=layer_idx,
@@ -601,7 +600,7 @@ def rearrange_expert_weights_inplace(
600 new_indices=new_global_expert_indices_cpu[layer_idx],
601 expert_weights=expert_weights[layer_idx],
602 expert_weights_buffers=weights_buffer,
604 - cuda_stream=None,
603 + stream=None,
604 ep_rank=ep_rank,
605 communicator=communicator,
606 layer_idx=layer_idx,
vllm/model_executor/layers/fused_moe/experts/xpu_moe.py
+1
@@ -171,6 +171,7 @@ class XPUExperts(mk.FusedMoEExpertsModular):
171 num_experts=self.moe_config.num_local_experts,
172 ep_rank=self.moe_config.ep_rank,
173 ep_size=self.moe_config.ep_size,
174 + expert_map=expert_map,
175 gemm1_clamp_limit=self.gemm1_clamp_limit,
176 )
177 assert self.fused_moe_impl is not None
vllm/model_executor/layers/fused_moe/router/base_router.py
+1 -1
@@ -13,7 +13,7 @@ from vllm.platforms import current_platform
13 from vllm.triton_utils import tl, triton
14 from vllm.v1.worker.ubatching import dbo_current_ubatch_id
15
16 -if current_platform.is_cuda_alike():
16 +if current_platform.is_cuda_alike() or current_platform.is_xpu():
17
18 @triton.jit
19 def _eplb_map_and_record_i32_kernel(