@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 4f423bd5bc
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 1, 23:45 UTC

[EPLB] Nixl communicator optimization. Zero-copy transfers (#41633)

Signed-off-by: ilmarkov <markovilya197@gmail.com> Signed-off-by: Markov Ilya <markovilya19@gmail.com> Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com> Co-authored-by: Markov Ilya <markovilya19@gmail.com> Co-authored-by: Nicolò Lucchesi <nlucches@redhat.com> Co-authored-by: Tyler Michael Smith <tyler@neuralmagic.com> Co-authored-by: Tyler Michael Smith <tlrmchlsmth@gmail.com>

Ilya Markov committed Jun 4, 2026 at 05:40 UTC 4f423bd5bcf38cb2fc09db5dc9e0bc8a6bfb1638
12 files changed +287 -219
tests/distributed/test_eplb_execute.py
+21 -9
@@ -277,12 +277,15 @@ def assert_verification_synced(local_ok: bool, msg: str) -> None:
277 assert bool(ok_tensor.item()), msg
278
279
280 -def create_eplb_communicator_or_raise(*, group_coordinator, backend, expert_weights):
280 +def create_eplb_communicator_or_raise(
281 + *, group_coordinator, backend, expert_weights, expert_buffer
282 +):
283 try:
284 return create_eplb_communicator(
285 group_coordinator=group_coordinator,
286 backend=backend,
287 expert_weights=expert_weights,
288 + expert_buffer=expert_buffer,
289 )
290 except Exception as exc:
291 raise RuntimeError(
@@ -355,7 +358,8 @@ def _test_async_transfer_layer_without_mtp_worker(
358 communicator = create_eplb_communicator_or_raise(
359 group_coordinator=ep_group_coordinator,
360 backend=eplb_communicator,
358 - expert_weights=expert_weights[0],
361 + expert_weights=expert_weights,
362 + expert_buffer=expert_buffer,
363 )
364 communicator.set_stream(cuda_stream)
365
@@ -368,6 +372,7 @@ def _test_async_transfer_layer_without_mtp_worker(
372 ep_group=ep_group,
373 communicator=communicator,
374 cuda_stream=cuda_stream,
375 + layer_idx=layer_idx,
376 )
377 cuda_stream.synchronize()
378 move_from_buffer(
@@ -460,10 +465,12 @@ def _test_rearrange_expert_weights_with_redundancy(
465 num_layers, num_local_experts, hidden_sizes, ep_rank, device, old_indices
466 )
467
468 + expert_buffer = [torch.empty_like(w) for w in expert_weights[0]]
469 communicator = create_eplb_communicator_or_raise(
470 group_coordinator=ep_group_coordinator,
471 backend=eplb_communicator,
466 - expert_weights=expert_weights[0],
472 + expert_weights=expert_weights,
473 + expert_buffer=expert_buffer,
474 )
475
476 # Execute weight rearrangement
@@ -471,9 +478,9 @@ def _test_rearrange_expert_weights_with_redundancy(
478 old_indices,
479 new_indices,
480 expert_weights,
481 + expert_buffer,
482 ep_group,
475 - is_profile=False,
476 - communicator=communicator,
483 + communicator,
484 )
485
486 # Verify the rearrangement result
@@ -593,10 +600,12 @@ def _test_rearrange_expert_weights_no_change(env, world_size) -> None:
600 layer_copy.append(weight.clone())
601 original_weights.append(layer_copy)
602
603 + expert_buffer = [torch.empty_like(w) for w in expert_weights[0]]
604 communicator = create_eplb_communicator_or_raise(
605 group_coordinator=ep_group_coordinator,
606 backend="torch_nccl",
599 - expert_weights=expert_weights[0],
607 + expert_weights=expert_weights,
608 + expert_buffer=expert_buffer,
609 )
610
611 # Execute rearrangement (should be no change)
@@ -604,9 +613,9 @@ def _test_rearrange_expert_weights_no_change(env, world_size) -> None:
613 indices,
614 indices, # Same indices
615 expert_weights,
616 + expert_buffer,
617 ep_group,
618 communicator,
609 - is_profile=False,
619 )
620
621 # Verify that the weights have not changed
@@ -726,10 +735,12 @@ def _test_rearrange_expert_weights_profile_mode(env, world_size) -> None:
735 layer_copy.append(weight.clone())
736 original_weights.append(layer_copy)
737
738 + expert_buffer = [torch.empty_like(w) for w in expert_weights[0]]
739 communicator = create_eplb_communicator_or_raise(
740 group_coordinator=ep_group_coordinator,
741 backend="torch_nccl",
732 - expert_weights=expert_weights[0],
742 + expert_weights=expert_weights,
743 + expert_buffer=expert_buffer,
744 )
745
746 # Execute profile mode rearrangement
@@ -737,9 +748,10 @@ def _test_rearrange_expert_weights_profile_mode(env, world_size) -> None:
748 old_indices,
749 new_indices,
750 expert_weights,
751 + expert_buffer,
752 ep_group,
753 communicator,
742 - is_profile=True, # Profile mode
754 + is_profile=True,
755 )
756
757 # In profile mode, the weights should remain unchanged
tests/distributed/test_eplb_fused_moe_layer.py
+11 -1
@@ -9,9 +9,11 @@ import pytest
9 import torch
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 rearrange_expert_weights_inplace
14 from vllm.distributed.parallel_state import (
15 ensure_model_parallel_initialized,
16 + get_eplb_group,
17 get_tp_group,
18 )
19 from vllm.model_executor.layers.fused_moe.layer import FusedMoE
@@ -213,12 +215,20 @@ def _test_eplb_fml(env, world_size: int, test_config: TestConfig):
215 for lidx in range(test_config.num_layers):
216 shuffled_indices[lidx] = torch.randperm(test_config.num_experts)
217
218 + expert_buffer = [torch.empty_like(w) for w in rank_expert_weights[0]]
219 + communicator = create_eplb_communicator(
220 + group_coordinator=get_eplb_group(),
221 + backend="torch_nccl",
222 + expert_weights=rank_expert_weights,
223 + expert_buffer=expert_buffer,
224 + )
225 rearrange_expert_weights_inplace(
226 indices,
227 shuffled_indices,
228 rank_expert_weights,
229 + expert_buffer,
230 ep_group,
221 - is_profile=False,
231 + communicator,
232 )
233
234 num_local_experts = test_config.num_local_experts
tests/distributed/test_eplb_fused_moe_layer_dep_nvfp4.py
+11 -1
@@ -10,11 +10,13 @@ import torch
10
11 from tests.kernels.moe.utils import make_test_quant_config
12 from vllm.config import VllmConfig, set_current_vllm_config
13 +from vllm.distributed.eplb.eplb_communicator import create_eplb_communicator
14 from vllm.distributed.eplb.eplb_state import EplbLayerState
15 from vllm.distributed.eplb.rebalance_execute import rearrange_expert_weights_inplace
16 from vllm.distributed.parallel_state import (
17 ensure_model_parallel_initialized,
18 get_dp_group,
19 + get_eplb_group,
20 )
21 from vllm.forward_context import set_forward_context
22 from vllm.model_executor.layers.fused_moe.layer import FusedMoE
@@ -171,12 +173,20 @@ def _test_eplb_fml(env, world_size: int, test_config: TestConfig):
173 for lidx in range(test_config.num_layers):
174 shuffled_indices[lidx] = torch.randperm(test_config.num_experts)
175
176 + expert_buffer = [torch.empty_like(w) for w in rank_expert_weights[0]]
177 + communicator = create_eplb_communicator(
178 + group_coordinator=get_eplb_group(),
179 + backend="torch_nccl",
180 + expert_weights=rank_expert_weights,
181 + expert_buffer=expert_buffer,
182 + )
183 rearrange_expert_weights_inplace(
184 indices,
185 shuffled_indices,
186 rank_expert_weights,
187 + expert_buffer,
188 ep_group,
179 - is_profile=False,
189 + communicator,
190 )
191
192 num_global_experts = test_config.num_experts
tests/kernels/moe/test_moe_layer.py
+4 -1
@@ -1287,10 +1287,12 @@ def _test_body_eplb(
1287
1288 expert_weights = [list(eplb_moe_layer.get_expert_weights())]
1289
1290 + expert_buffer = [torch.empty_like(w) for w in expert_weights[0]]
1291 communicator = create_eplb_communicator(
1292 group_coordinator=get_eplb_group(),
1293 backend=vllm_config.parallel_config.eplb_config.communicator,
1293 - expert_weights=expert_weights[0],
1294 + expert_weights=expert_weights,
1295 + expert_buffer=expert_buffer,
1296 )
1297
1298 # Rearrange expert weights across EP ranks
@@ -1298,6 +1300,7 @@ def _test_body_eplb(
1300 old_global_expert_indices=initial_indices.unsqueeze(0),
1301 new_global_expert_indices=shuffled_indices.unsqueeze(0),
1302 expert_weights=expert_weights,
1303 + expert_buffer=expert_buffer,
1304 ep_group=cpu_group,
1305 communicator=communicator,
1306 )
vllm/distributed/device_communicators/custom_all_reduce.py
+1 -5
@@ -40,11 +40,7 @@ def _can_p2p(rank: int, world_size: int) -> bool:
40 return True
41
42
43 -def is_weak_contiguous(inp: torch.Tensor):
44 - return inp.is_contiguous() or (
45 - inp.storage().nbytes() - inp.storage_offset() * inp.element_size()
46 - == inp.numel() * inp.element_size()
47 - )
43 +from vllm.distributed.utils import is_weak_contiguous # noqa: E402
44
45
46 class CustomAllreduce:
vllm/distributed/device_communicators/quick_all_reduce.py
+1 -5
@@ -24,11 +24,7 @@ except Exception:
24 quick_ar = False
25
26
27 -def is_weak_contiguous(inp: torch.Tensor):
28 - return inp.is_contiguous() or (
29 - inp.storage().nbytes() - inp.storage_offset() * inp.element_size()
30 - == inp.numel() * inp.element_size()
31 - )
27 +from vllm.distributed.utils import is_weak_contiguous # noqa: E402, F401
28
29
30 class QuickReduceRegime(Enum):
vllm/distributed/elastic_ep/elastic_execute.py
+5 -1
@@ -470,10 +470,14 @@ class ElasticEPScalingExecutor:
470 module._replace_quant_method(module.quant_method.old_quant_method)
471 prepare_communication_buffer_for_model(self.worker.model_runner.model)
472
473 + eplb_model_state.expert_buffer = [
474 + torch.empty_like(w) for w in model.expert_weights[0]
475 + ]
476 eplb_model_state.communicator = create_eplb_communicator(
477 group_coordinator=get_eplb_group(),
478 backend=parallel_config.eplb_config.communicator,
476 - expert_weights=model.expert_weights[0],
479 + expert_weights=model.expert_weights,
480 + expert_buffer=eplb_model_state.expert_buffer,
481 )
482
483 if (
vllm/distributed/eplb/async_worker.py
+1
@@ -120,6 +120,7 @@ def transfer_run_periodically(
120 ep_group=eplb_group,
121 is_profile=is_profile,
122 cuda_stream=cuda_stream,
123 + layer_idx=layer_idx,
124 )
125
126 # Wait until all writes to expert_buffer have finished before making the
vllm/distributed/eplb/eplb_communicator.py
+199 -186
@@ -30,6 +30,7 @@ from vllm.distributed.parallel_state import (
30 is_local_first_rank,
31 )
32 from vllm.distributed.stateless_coordinator import StatelessGroupCoordinator
33 +from vllm.distributed.utils import is_weak_contiguous
34 from vllm.logger import init_logger
35 from vllm.platforms import current_platform
36
@@ -63,8 +64,22 @@ class EplbCommunicator(ABC):
64 pass
65
66 @abstractmethod
66 - def execute(self, old_indices: np.ndarray | None = None) -> None:
67 - pass
67 + def execute(self) -> None:
68 + """Complete all enqueued transfers.
69 +
70 + Some backends perform communication here; others (e.g. NIXL)
71 + issue transfers eagerly in add_recv and only wait here.
72 + On return, all data is available in the destination buffers.
73 + """
74 +
75 + def set_transfer_context( # noqa: B027
76 + self, old_indices: np.ndarray, layer_idx: int
77 + ) -> None:
78 + """Pre-set layer context before add_recv calls.
79 +
80 + Default is a no-op; overridden by backends (e.g. NIXL) that need
81 + layer-level context to issue transfers inside add_recv.
82 + """
83
84 @property
85 def needs_profile_buffer_reservation(self) -> bool:
@@ -125,7 +140,7 @@ class TorchDistNcclEplbCommunicator(EplbCommunicator):
140 )
141 )
142
128 - def execute(self, old_indices: np.ndarray | None = None) -> None:
143 + def execute(self) -> None:
144 if not self._p2p_ops:
145 return
146 try:
@@ -168,7 +183,7 @@ class TorchDistGlooStagedEplbCommunicator(EplbCommunicator):
183 for tensor in tensors:
184 self._ops.append(("recv", tensor, src_rank))
185
171 - def execute(self, old_indices: np.ndarray | None = None) -> None:
186 + def execute(self) -> None:
187 if not self._ops:
188 return
189
@@ -229,29 +244,47 @@ class NixlEplbCommunicator(EplbCommunicator):
244 def __init__(
245 self,
246 cpu_group: ProcessGroup,
232 - expert_weights: Sequence[torch.Tensor],
233 - cuda_stream: torch.cuda.Stream | None = None,
247 + all_expert_weights: Sequence[Sequence[torch.Tensor]],
248 + expert_buffer: Sequence[torch.Tensor],
249 ) -> None:
235 - assert expert_weights, "NixlEplbCommunicator requires non-empty expert_weights."
250 + assert all_expert_weights, (
251 + "NixlEplbCommunicator requires non-empty all_expert_weights."
252 + )
253 + assert expert_buffer, "NixlEplbCommunicator requires non-empty expert_buffer."
254 nixl_wrapper_cls = nixl_utils.NixlWrapper
255 if nixl_wrapper_cls is None:
256 raise RuntimeError("NIXL/ RIXL is unavailable.")
257 +
258 self._cpu_group = cpu_group
240 - self._cuda_stream = cuda_stream
259 self._world_size = cpu_group.size()
260 self._rank = cpu_group.rank()
243 - # expert_id -> weight tensors to pack into the send buffer.
244 - self._expert_send_map: dict[int, list[torch.Tensor]] = {}
245 - # src_rank -> expert_id -> weight tensors to unpack after transfer.
246 - self._recv_map: dict[int, dict[int, list[torch.Tensor]]] = {}
247 - self._num_local_experts: int = expert_weights[0].shape[0]
248 - self._device = expert_weights[0].device
249 - for tensor in expert_weights:
250 - assert tensor.device == self._device, (
251 - "All local EPLB tensors are expected to be on the same device: "
252 - f"expected={self._device}, got={tensor.device}"
261 +
262 + self._all_expert_weights = all_expert_weights
263 + self._expert_buffer = expert_buffer
264 + self._num_local_experts: int = all_expert_weights[0][0].shape[0]
265 + self._device = all_expert_weights[0][0].device
266 +
267 + for layer_tensors in all_expert_weights:
268 + for tensor in layer_tensors:
269 + assert is_weak_contiguous(tensor), (
270 + "Expert weight tensors must be contiguous in memory"
271 + )
272 + assert tensor.device == self._device, (
273 + "All local EPLB tensors are expected to be on the same "
274 + f"device: expected={self._device}, got={tensor.device}"
275 + )
276 + for tensor in expert_buffer:
277 + assert is_weak_contiguous(tensor), (
278 + "expert_buffer tensors must be contiguous in memory"
279 )
280
281 + # (local_dlist, remote_dlist, xfer_handle) for in-flight READs;
282 + # accumulated by add_recv, drained by execute.
283 + self._xfer_entries: list[tuple[int, int, int]] = []
284 + # Per-rank expert_id -> physical row; set by set_transfer_context.
285 + self._expert_to_src_row: list[dict[int, int]] | None = None
286 + self._layer_idx: int | None = None
287 +
288 nixl_agent_config = nixl_utils.nixl_agent_config
289 config = (
290 nixl_agent_config(capture_telemetry=False)
@@ -260,15 +293,16 @@ class NixlEplbCommunicator(EplbCommunicator):
293 )
294 self._nixl_wrapper = nixl_wrapper_cls(self._make_agent_name(), config)
295 self._nixl_memory_type = "VRAM"
263 - self._registered_desc: object | None = None
296 + # NIXL registration handles; deregistered in __del__.
297 + self._registered_descs: list[object] = []
298 self._remote_agents: dict[int, str] = {}
265 - self._remote_send_meta: dict[int, tuple[int, int]] = {}
266 - self._send_buffer: torch.Tensor = torch.empty(0)
267 - self._recv_buffer: torch.Tensor = torch.empty(0)
268 - self._expert_bytes: int = 0
299 + # peer -> (layer, tensor) -> (base_ptr, bytes_per_expert, dev_id).
300 + self._remote_send_meta: dict[
301 + int, dict[tuple[int, int], tuple[int, int, int]]
302 + ] = {}
303
304 self._cuda_device_id = int(self._device.index or 0)
271 - self._init_step("buffers", self._init_registered_buffers, expert_weights)
305 + self._init_step("buffers", self._init_registered_buffers)
306 self._init_step("agents", self._init_remote_agents)
307 self._init_step("send meta", self._exchange_remote_send_meta)
308 self._log_initialized()
@@ -291,19 +325,34 @@ class NixlEplbCommunicator(EplbCommunicator):
325 uid = uuid.uuid4().hex[:8]
326 return f"eplb-{self._rank}{pp_suffix}-{uid}"
327
328 + def set_stream(self, cuda_stream: torch.cuda.Stream | None) -> None:
329 + pass
330 +
331 def add_send(
332 self,
333 tensors: list[torch.Tensor],
334 dst_rank: int,
335 expert_id: int,
336 ) -> None:
300 - assert dst_rank != self._rank, (
301 - "EPLB communicator should not enqueue same-rank sends: "
302 - f"rank={self._rank}, dst_rank={dst_rank}"
337 + # No-op: NIXL READ is receiver-initiated. The sender's expert
338 + # weights are pre-registered and always readable in-place.
339 + pass
340 +
341 + def set_transfer_context(self, old_indices: np.ndarray, layer_idx: int) -> None:
342 + # Pre-compute expert_id -> src_row mapping for every rank so that
343 + # add_recv can immediately issue NIXL READs.
344 + assert not self._xfer_entries, (
345 + f"set_transfer_context() called with {len(self._xfer_entries)} "
346 + f"pending transfers from layer {self._layer_idx}; "
347 + f"execute() was not called after previous add_recv() calls"
348 )
304 - # An expert sent to multiple peers is packed only once; skip duplicates.
305 - if expert_id not in self._expert_send_map:
306 - self._expert_send_map[expert_id] = tensors
349 + self._layer_idx = layer_idx
350 + n = self._num_local_experts
351 + rank_experts = old_indices[: self._world_size * n].reshape(self._world_size, n)
352 + self._expert_to_src_row = [
353 + {int(eid): i for i, eid in enumerate(row) if eid != -1}
354 + for row in rank_experts
355 + ]
356
357 def add_recv(
358 self,
@@ -311,13 +360,44 @@ class NixlEplbCommunicator(EplbCommunicator):
360 src_rank: int,
361 expert_id: int,
362 ) -> None:
314 - assert src_rank != self._rank, (
315 - "EPLB communicator should not enqueue same-rank recvs: "
316 - f"rank={self._rank}, src_rank={src_rank}"
363 + # Build NIXL descriptors and issue the RDMA READ immediately,
364 + # overlapping the transfer with the remaining Python loop in
365 + # move_to_buffer.
366 + assert self._expert_to_src_row is not None and self._layer_idx is not None, (
367 + "set_transfer_context() must be called before add_recv()"
368 + )
369 + src_row = self._expert_to_src_row[src_rank][expert_id]
370 + layer_idx = self._layer_idx
371 +
372 + local_descs: list[tuple[int, int, int]] = []
373 + remote_descs: list[tuple[int, int, int]] = []
374 + for t_idx, t in enumerate(tensors):
375 + send_base, send_stride, remote_dev = self._remote_send_meta[src_rank][
376 + (layer_idx, t_idx)
377 + ]
378 + assert t.nbytes == send_stride, (
379 + f"tensor {t_idx} size {t.nbytes} != remote stride {send_stride}"
380 + )
381 + local_descs.append(
382 + (
383 + t.data_ptr(),
384 + t.nbytes,
385 + self._cuda_device_id,
386 + )
387 + )
388 + remote_descs.append(
389 + (
390 + send_base + src_row * send_stride,
391 + send_stride,
392 + remote_dev,
393 + )
394 + )
395 +
396 + local_h, remote_h, xfer_h = self._create_peer_xfer(
397 + src_rank, local_descs, remote_descs
398 )
318 - recv_experts = self._recv_map.setdefault(src_rank, {})
319 - if expert_id not in recv_experts:
320 - recv_experts[expert_id] = tensors
399 + self._nixl_wrapper.transfer(xfer_h)
400 + self._xfer_entries.append((local_h, remote_h, xfer_h))
401
402 def _init_remote_agents(self) -> None:
403 local_metadata = self._nixl_wrapper.get_agent_metadata()
@@ -334,73 +414,60 @@ class NixlEplbCommunicator(EplbCommunicator):
414 peer_metadata
415 )
416
337 - def _init_registered_buffers(self, expert_weights: Sequence[torch.Tensor]) -> None:
338 - total_bytes = max(sum(t.nbytes for t in expert_weights), 1)
339 - assert total_bytes % self._num_local_experts == 0, (
340 - f"Number of bytes in moe layer {total_bytes} is not divisible "
341 - f"by number of local experts {self._num_local_experts}"
342 - )
343 - self._expert_bytes = total_bytes // self._num_local_experts
417 + def _init_registered_buffers(self) -> None:
418 + all_tensors: list[torch.Tensor] = []
419 + for layer_tensors in self._all_expert_weights:
420 + all_tensors.extend(layer_tensors)
421 + all_tensors.extend(self._expert_buffer)
422
345 - self._send_buffer = torch.empty(
346 - total_bytes, device=self._device, dtype=torch.uint8
347 - )
348 - self._recv_buffer = torch.empty(
349 - total_bytes, device=self._device, dtype=torch.uint8
350 - )
351 -
352 - descs = self._nixl_wrapper.get_reg_descs([self._send_buffer, self._recv_buffer])
423 + descs = self._nixl_wrapper.get_reg_descs(all_tensors)
424 self._nixl_wrapper.register_memory(descs)
354 - self._registered_desc = descs
425 + self._registered_descs.append(descs)
426
427 def _exchange_remote_send_meta(self) -> None:
357 - """Exchange send-buffer metadata so each rank can build dynamic
358 - descriptors at execute time."""
359 - local_meta: tuple[int, int] = (
360 - self._send_buffer.data_ptr(),
361 - self._cuda_device_id,
362 - )
363 - gathered_meta: list[tuple[int, int] | None] = [None] * self._world_size
428 + """Exchange per-layer per-tensor metadata so receivers can compute
429 + remote RDMA addresses at transfer time."""
430 + local_meta: dict[tuple[int, int], tuple[int, int, int]] = {}
431 + for layer_idx, layer_tensors in enumerate(self._all_expert_weights):
432 + for t_idx, t in enumerate(layer_tensors):
433 + nbytes_per_expert = t.nbytes // self._num_local_experts
434 + local_meta[(layer_idx, t_idx)] = (
435 + t.data_ptr(),
436 + nbytes_per_expert,
437 + self._cuda_device_id,
438 + )
439 +
440 + # Per-rank map: (layer_idx, tensor_idx) -> (base_ptr, bytes_per_expert, dev_id).
441 + # add_recv uses base_ptr + src_row * bytes_per_expert to compute
442 + # the remote RDMA address for each expert.
443 + gathered_meta: list[dict[tuple[int, int], tuple[int, int, int]] | None] = [
444 + None
445 + ] * self._world_size
446 torch.distributed.all_gather_object(
447 gathered_meta, local_meta, group=self._cpu_group
448 )
449
450 + local_keys = set(local_meta.keys())
451 for peer in self._remote_agents:
452 peer_meta = gathered_meta[peer]
453 assert peer_meta is not None
454 + peer_keys = set(peer_meta.keys())
455 + if peer_keys != local_keys:
456 + raise RuntimeError(
457 + f"NIXL EPLB metadata key mismatch with rank {peer}: "
458 + f"local={sorted(local_keys)}, peer={sorted(peer_keys)}"
459 + )
460 + for key in local_keys:
461 + _, local_stride, _ = local_meta[key]
462 + _, peer_stride, _ = peer_meta[key]
463 + if local_stride != peer_stride:
464 + raise RuntimeError(
465 + f"NIXL EPLB nbytes_per_expert mismatch for {key} "
466 + f"with rank {peer}: "
467 + f"local={local_stride}, peer={peer_stride}"
468 + )
469 self._remote_send_meta[peer] = peer_meta
470
373 - @staticmethod
374 - def _pack_send_buffer(
375 - in_tensors: list[torch.Tensor],
376 - send_buffer: torch.Tensor,
377 - byte_offset: int,
378 - ) -> None:
379 - for tensor in in_tensors:
380 - raw = tensor.reshape(-1).view(torch.uint8)
381 - if raw.numel() == 0:
382 - continue
383 - send_buffer[byte_offset : byte_offset + raw.numel()].copy_(
384 - raw, non_blocking=True
385 - )
386 - byte_offset += raw.numel()
387 -
388 - @staticmethod
389 - def _unpack_recv_buffer(
390 - recv_buffer: torch.Tensor,
391 - out_tensors: list[torch.Tensor],
392 - byte_offset: int,
393 - ) -> None:
394 - for tensor in out_tensors:
395 - num_bytes = tensor.numel() * tensor.element_size()
396 - if num_bytes == 0:
397 - continue
398 - tensor.reshape(-1).view(torch.uint8).copy_(
399 - recv_buffer[byte_offset : byte_offset + num_bytes],
400 - non_blocking=True,
401 - )
402 - byte_offset += num_bytes
403 -
471 def _wait_for_all_transfers(self, handles: list[int]) -> None:
472 pending = set(handles)
473 while pending:
@@ -456,110 +523,52 @@ class NixlEplbCommunicator(EplbCommunicator):
523 )
524 return (local_handle, remote_handle, xfer_handle)
525
459 - def execute(self, old_indices: np.ndarray | None = None) -> None:
460 - assert old_indices is not None, (
461 - "NixlEplbCommunicator.execute requires old_indices"
526 + def execute(self) -> None:
527 + assert self._layer_idx is not None or not self._xfer_entries, (
528 + "set_transfer_context() must be called before execute() "
529 + "if any add_recv() calls were made"
530 )
463 -
464 - xfer_entries: list[tuple[int, int, int]] = []
531 try:
466 - n = self._num_local_experts
467 - rank_experts = old_indices[: self._world_size * n].reshape(
468 - self._world_size, n
469 - )
470 - # Build expert_id -> send slot mapping per rank.
471 - expert_to_send_slot: list[dict[int, int]] = [
472 - {int(eid): i for i, eid in enumerate(row) if eid != -1}
473 - for row in rank_experts
474 - ]
532 + self._wait_for_all_transfers([x[2] for x in self._xfer_entries])
533
476 - # Phase 1: pack each expert at its slot offset in the send buffer.
477 - with torch.cuda.stream(self._cuda_stream):
478 - for expert_id, tensors in self._expert_send_map.items():
479 - slot = expert_to_send_slot[self._rank][expert_id]
480 - byte_offset = slot * self._expert_bytes
481 - self._pack_send_buffer(tensors, self._send_buffer, byte_offset)
482 -
483 - # Ensure all packed data is visible in device memory before pulls.
484 - if self._cuda_stream is not None:
485 - self._cuda_stream.synchronize()
486 - else:
487 - torch.cuda.current_stream().synchronize()
488 - # READ is receiver-initiated; synchronize all ranks before transfer.
489 - # We use monitored_barrier so a rank that crashes or exits early
490 - # produces a diagnostic timeout instead of a silent hang.
534 + # Post-READ barrier.
535 + # Correctness fence for zero-copy: prevents overwrite-while-
536 + # remote-read race.
537 torch.distributed.monitored_barrier(
538 group=self._cpu_group,
539 timeout=timedelta(minutes=5),
540 )
495 -
496 - # Phase 2: issue one batched READ per peer.
497 - recv_offsets: dict[tuple[int, int], int] = {}
498 - recv_offset = 0
499 - recv_base = self._recv_buffer.data_ptr()
500 - for src in range(self._world_size):
501 - if src == self._rank:
502 - continue
503 - recv_experts = self._recv_map.get(src)
504 - if not recv_experts:
505 - continue
506 - expert_ids = list(recv_experts.keys())
507 - remote_base, remote_dev = self._remote_send_meta[src]
508 - local_descs: list[tuple[int, int, int]] = []
509 - remote_descs: list[tuple[int, int, int]] = []
510 - for expert_id in expert_ids:
511 - slot = expert_to_send_slot[src][expert_id]
512 - remote_off = slot * self._expert_bytes
513 - recv_offsets[(src, expert_id)] = recv_offset
514 - local_descs.append(
515 - (
516 - recv_base + recv_offset,
517 - self._expert_bytes,
518 - self._cuda_device_id,
519 - )
520 - )
521 - remote_descs.append(
522 - (remote_base + remote_off, self._expert_bytes, remote_dev)
523 - )
524 - recv_offset += self._expert_bytes
525 - assert recv_offset <= self._recv_buffer.nbytes
526 - local_h, remote_h, xfer_h = self._create_peer_xfer(
527 - src, local_descs, remote_descs
528 - )
529 - self._nixl_wrapper.transfer(xfer_h)
530 - xfer_entries.append((local_h, remote_h, xfer_h))
531 -
532 - # Phase 3: wait for all in-flight transfers, then unpack.
533 - self._wait_for_all_transfers([x[2] for x in xfer_entries])
534 -
535 - with torch.cuda.stream(self._cuda_stream):
536 - for (src, expert_id), offset in recv_offsets.items():
537 - self._unpack_recv_buffer(
538 - self._recv_buffer,
539 - self._recv_map[src][expert_id],
540 - offset,
541 - )
541 finally:
543 - for local_h, remote_h, xfer_h in xfer_entries:
542 + for local_h, remote_h, xfer_h in self._xfer_entries:
543 with contextlib.suppress(Exception):
544 self._nixl_wrapper.release_xfer_handle(xfer_h)
545 with contextlib.suppress(Exception):
546 self._nixl_wrapper.release_dlist_handle(local_h)
547 with contextlib.suppress(Exception):
548 self._nixl_wrapper.release_dlist_handle(remote_h)
550 - self._expert_send_map.clear()
551 - self._recv_map.clear()
549 + self._xfer_entries.clear()
550 + self._expert_to_src_row = None
551 + self._layer_idx = None
552
553 def __del__(self) -> None:
554 - try:
555 - if self._registered_desc is not None:
556 - self._nixl_wrapper.deregister_memory(self._registered_desc)
557 - self._registered_desc = None
554 + with contextlib.suppress(Exception):
555 + for local_h, remote_h, xfer_h in self._xfer_entries:
556 + with contextlib.suppress(Exception):
557 + self._nixl_wrapper.release_xfer_handle(xfer_h)
558 + with contextlib.suppress(Exception):
559 + self._nixl_wrapper.release_dlist_handle(local_h)
560 + with contextlib.suppress(Exception):
561 + self._nixl_wrapper.release_dlist_handle(remote_h)
562 + with contextlib.suppress(Exception):
563 + for descs in self._registered_descs:
564 + with contextlib.suppress(Exception):
565 + self._nixl_wrapper.deregister_memory(descs)
566 + self._registered_descs.clear()
567 + with contextlib.suppress(Exception):
568 for agent_name in self._remote_agents.values():
559 - self._nixl_wrapper.remove_remote_agent(agent_name)
569 + with contextlib.suppress(Exception):
570 + self._nixl_wrapper.remove_remote_agent(agent_name)
571 self._remote_agents.clear()
561 - except Exception as e:
562 - logger.warning("Error during NixlEplbCommunicator cleanup: %s", e)
572
573
574 class PyNcclEplbCommunicator(EplbCommunicator):
@@ -600,7 +609,7 @@ class PyNcclEplbCommunicator(EplbCommunicator):
609 for tensor in tensors:
610 self._pynccl_comm.recv(tensor, src_rank, stream=self._cuda_stream)
611
603 - def execute(self, old_indices: np.ndarray | None = None) -> None:
612 + def execute(self) -> None:
613 if self._group_started:
614 self._pynccl_comm.group_end()
615 self._group_started = False
@@ -609,7 +618,8 @@ class PyNcclEplbCommunicator(EplbCommunicator):
618 def create_eplb_communicator(
619 group_coordinator: GroupCoordinator,
620 backend: str | None,
612 - expert_weights: Sequence[torch.Tensor],
621 + expert_weights: Sequence[Sequence[torch.Tensor]],
622 + expert_buffer: Sequence[torch.Tensor],
623 ) -> EplbCommunicator:
624 """Create an EPLB communicator for the given backend.
625
@@ -624,16 +634,18 @@ def create_eplb_communicator(
634 ``"pynccl"`` in that case. When tensors reside on CPU,
635 ``"torch_gloo"`` or ``"torch_nccl"`` are used via the CPU
636 process group.
627 - expert_weights: Expert weight tensors from *one* MoE layer.
628 - NixlEplbCommunicator pre-allocates send/recv buffers sized
629 - to this layer, so all other MoE layers must have the same
630 - tensor count, shapes, and dtypes.
637 + expert_weights: Expert weight tensors for *all* MoE layers.
638 + Shape ``(num_layers)(num_tensors_per_layer)``.
639 + NixlEplbCommunicator registers all layers with NIXL for
640 + zero-copy RDMA reads.
641 + expert_buffer: Pre-allocated receive buffer tensors (one per
642 + weight tensor in a single layer).
643 """
632 - # Keep a safe default for callers that have not resolved communicator yet.
644 if backend is None:
645 backend = "torch_nccl"
646
636 - tensor_device_type = expert_weights[0].device.type if expert_weights else "cpu"
647 + first_layer = expert_weights[0] if expert_weights else []
648 + tensor_device_type = first_layer[0].device.type if first_layer else "cpu"
649 torch_group = (
650 group_coordinator.cpu_group
651 if tensor_device_type == "cpu"
@@ -649,7 +661,7 @@ def create_eplb_communicator(
661 unsupported_dtypes = sorted(
662 {
663 tensor.dtype
652 - for tensor in expert_weights
664 + for tensor in first_layer
665 if not ncclDataTypeEnum.supports_torch_dtype(tensor.dtype)
666 },
667 key=str,
@@ -704,7 +716,8 @@ def create_eplb_communicator(
716 try:
717 return NixlEplbCommunicator(
718 cpu_group=group_coordinator.cpu_group,
707 - expert_weights=expert_weights,
719 + all_expert_weights=expert_weights,
720 + expert_buffer=expert_buffer,
721 )
722 except Exception as exc:
723 raise RuntimeError(
vllm/distributed/eplb/eplb_state.py
+3 -1
@@ -450,7 +450,8 @@ class EplbState:
450 communicator = create_eplb_communicator(
451 group_coordinator=get_eplb_group(),
452 backend=self.parallel_config.eplb_config.communicator,
453 - expert_weights=model.expert_weights[0],
453 + expert_weights=model.expert_weights,
454 + expert_buffer=expert_buffer,
455 )
456
457 model_state = EplbModelState(
@@ -766,6 +767,7 @@ class EplbState:
767 eplb_model_state.physical_to_logical_map,
768 new_physical_to_logical_map,
769 eplb_model_state.model.expert_weights,
770 + eplb_model_state.expert_buffer,
771 ep_group,
772 eplb_model_state.communicator,
773 is_profile,
vllm/distributed/eplb/rebalance_execute.py
+16 -9
@@ -178,6 +178,7 @@ def move_to_buffer(
178 cuda_stream: torch.cuda.Stream | None,
179 ep_rank: int,
180 communicator: EplbCommunicator,
181 + layer_idx: int = 0,
182 ) -> TransferMetadata:
183 """
184 Rearranges expert weights during EPLB rebalancing.
@@ -193,6 +194,7 @@ def move_to_buffer(
194 cuda_stream: CUDA stream for async copies (can be None for sync mode).
195 ep_rank: Rank of this process in expert parallel group.
196 communicator: EplbCommunicator instance for P2P communication.
197 + layer_idx: Index of the MoE layer being transferred.
198
199 Returns:
200 TransferMetadata: Metadata needed for completing remote weight transfers.
@@ -265,6 +267,8 @@ def move_to_buffer(
267 for w, b in zip(expert_weights, expert_weights_buffers):
268 b[dst].copy_(w[src_local], non_blocking=True)
269
270 + communicator.set_transfer_context(old_indices, layer_idx)
271 +
272 # 2. Post sends
273 if send_count > 0:
274 experts = send_expert_ids[:send_count]
@@ -331,9 +335,8 @@ def move_to_buffer(
335 expert_id=int(expert),
336 )
337
334 - # 4. Execute the P2P operations. The real communication happens here.
335 - communicator.execute(old_indices=old_indices)
336 - # wait for the communication to finish
338 + # 4. Execute transfers and wait for completion.
339 + communicator.execute()
340 return TransferMetadata(
341 is_unchanged=is_unchanged,
342 is_received_locally=is_received_locally,
@@ -431,6 +434,7 @@ def transfer_layer(
434 is_profile: bool = False,
435 cuda_stream: torch.cuda.Stream | None = None,
436 rank_mapping: dict[int, int] | None = None,
437 + layer_idx: int = 0,
438 ) -> TransferMetadata:
439 """
440 Rearranges the expert weights in place according to the new expert indices.
@@ -452,6 +456,7 @@ def transfer_layer(
456 communications to reserve enough memory for the buffers.
457 cuda_stream: CUDA stream for async copies (can be None for sync mode).
458 rank_mapping: Optional rank mapping for elastic expert parallelism.
459 + layer_idx: Index of the MoE layer being transferred.
460
461 Returns:
462 TransferMetadata: Metadata needed for completing remote weight transfers,
@@ -499,6 +504,7 @@ def transfer_layer(
504 cuda_stream=cuda_stream,
505 ep_rank=ep_group.rank(),
506 communicator=communicator,
507 + layer_idx=layer_idx,
508 )
509
510
@@ -506,6 +512,7 @@ def rearrange_expert_weights_inplace(
512 old_global_expert_indices: torch.Tensor,
513 new_global_expert_indices: torch.Tensor,
514 expert_weights: Sequence[Sequence[torch.Tensor]],
515 + expert_buffer: Sequence[torch.Tensor],
516 ep_group: ProcessGroup,
517 communicator: EplbCommunicator,
518 is_profile: bool = False,
@@ -524,6 +531,8 @@ def rearrange_expert_weights_inplace(
531 of tensors of shape (num_local_physical_experts, hidden_size_i).
532 For example, a linear layer may have up and down projection,
533 so weight_count = 2. Each weight's hidden size can be different.
534 + expert_buffer: Pre-allocated receive buffer tensors (one per
535 + weight tensor in a single layer).
536 ep_group: The device process group for expert parallelism.
537 communicator: EplbCommunicator instance for P2P communication.
538 is_profile (bool): If `True`, do not perform any actual weight copy.
@@ -566,10 +575,10 @@ def rearrange_expert_weights_inplace(
575 # Reserve NCCL communication buffers via a dummy all_gather.
576 # Backends that pre-allocate their own transfer buffers
577 # skip this to avoid the extra memory spike during profiling.
569 - weights_buffer: list[torch.Tensor] = [
578 + profile_buffer: list[torch.Tensor] = [
579 torch.empty_like(w) for w in first_layer_weights
580 ]
572 - for weight, buffer in zip(expert_weights[0], weights_buffer):
581 + for weight, buffer in zip(expert_weights[0], profile_buffer):
582 dummy_recv_buffer = [buffer for _ in range(ep_size)]
583 torch.distributed.barrier()
584 all_gather(
@@ -579,10 +588,7 @@ def rearrange_expert_weights_inplace(
588 )
589 return
590
582 - # Buffers to hold the expert weights during the exchange.
583 - # NOTE: Currently we assume the same weights across different layers
584 - # have the same shape.
585 - weights_buffer = [torch.empty_like(w) for w in first_layer_weights]
591 + weights_buffer = list(expert_buffer)
592
593 old_global_expert_indices_cpu = old_global_expert_indices.cpu().numpy()
594 new_global_expert_indices_cpu = new_global_expert_indices.cpu().numpy()
@@ -597,6 +603,7 @@ def rearrange_expert_weights_inplace(
603 cuda_stream=None,
604 ep_rank=ep_rank,
605 communicator=communicator,
606 + layer_idx=layer_idx,
607 )
608
609 move_from_buffer(
vllm/distributed/utils.py
+14
@@ -64,6 +64,20 @@ def divide(numerator, denominator):
64 return numerator // denominator
65
66
67 +def is_weak_contiguous(inp: torch.Tensor) -> bool:
68 + """Check that *inp* occupies a single contiguous block of memory.
69 +
70 + Unlike ``torch.Tensor.is_contiguous()``, this also accepts tensors
71 + whose strides are not strictly C-contiguous (e.g. column-major) as
72 + long as the underlying storage from the tensor's offset onward is
73 + exactly ``numel * element_size`` bytes.
74 + """
75 + return inp.is_contiguous() or (
76 + inp.storage().nbytes() - inp.storage_offset() * inp.element_size()
77 + == inp.numel() * inp.element_size()
78 + )
79 +
80 +
81 def split_tensor_along_last_dim(
82 tensor: torch.Tensor,
83 num_partitions: int,