main
py 706 lines 27.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """The actual execution of the rearrangement.
4
5 This involves the exchange of expert weights between GPUs.
6 """
7
8 from collections.abc import Sequence
9 from dataclasses import dataclass
10
11 import numpy as np
12 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, device_stream
17 from vllm.logger import init_logger
18 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
19
20 logger = init_logger(__name__)
21
22
23 @dataclass
24 class TransferMetadata:
25 """Metadata describing a completed EPLB buffer transfer."""
26
27 is_unchanged: np.ndarray
28 """Mask of (num_local_experts,) indicating experts unchanged after rebalance."""
29 is_received_locally: np.ndarray
30 """Mask of (num_local_experts,) indicating experts received from local data."""
31 recv_primary_mask: np.ndarray
32 """Mask of (num_local_experts,) indicating primary experts received."""
33 recv_count: int
34 """Number of received experts for the layer."""
35 recv_expert_ids: np.ndarray
36 """Expert ids (num_local_experts,) of remote primary experts."""
37 recv_dst_rows: np.ndarray
38 """Target expert indices (num_local_experts,) in local tensors to send."""
39
40
41 @dataclass
42 class AsyncEplbLayerResult:
43 """The result of one completed async EPLB layer transfer."""
44
45 layer_idx: int
46 """Index of the MoE layer that was transferred."""
47 new_physical_to_logical_map: torch.Tensor
48 """
49 New physical→logical mapping for layers_idx, on CPU.
50 Shape: (num_physical_experts)
51 """
52 transfer_metadata: TransferMetadata
53 """Metadata describing what was received during transfer_layer."""
54 consumed_event: CpuGpuEvent
55 """
56 Event used to synchronize access to the intermediate buffer. The async worker calls
57 wait() after it finishes transferring weights to the intermediate buffer. The main
58 thread calls record() after it finishes transferring weights out of the intermediate
59 buffer in _move_to_workspace()
60 """
61
62
63 def get_ep_ranks_with_experts_batch(
64 expert_ids: np.ndarray,
65 num_local_experts: int,
66 old_indices: np.ndarray,
67 new_indices: np.ndarray,
68 ) -> tuple[dict[int, list[int]], dict[int, list[int]]]:
69 """Get the ranks of the experts that need to be exchanged.
70
71 Args:
72 expert_ids: 1D array of expert indices to query.
73 num_local_experts: The number of local experts.
74 old_indices: The old indices of the experts.
75 new_indices: The new indices of the experts.
76
77 Returns:
78 A tuple of two dictionaries mapping expert_id to:
79 - ranks_to_send: The ranks that have this expert and need to send.
80 - ranks_to_recv: The ranks that need to receive this expert.
81
82 """
83 ranks_to_send_map: dict[int, list[int]] = {}
84 ranks_to_recv_map: dict[int, list[int]] = {}
85
86 # Fast path: if no experts, return empty dicts
87 if expert_ids.size == 0:
88 return ranks_to_send_map, ranks_to_recv_map
89
90 unique_experts = np.unique(expert_ids)
91 num_positions = len(old_indices)
92 position_indices = np.arange(num_positions, dtype=np.int32)
93
94 # Vectorized approach: find all positions matching any query expert in one pass
95 # Use np.isin to get boolean masks for all relevant positions at once
96 old_relevant_mask = np.isin(old_indices, unique_experts)
97 new_relevant_mask = np.isin(new_indices, unique_experts)
98
99 # Process old_indices (send ranks)
100 if np.any(old_relevant_mask):
101 old_relevant_positions = position_indices[old_relevant_mask]
102 old_relevant_experts = old_indices[old_relevant_mask]
103 old_relevant_ranks = old_relevant_positions // num_local_experts
104
105 # Sort by expert first, then by position (to maintain first-appearance order)
106 sort_order = np.lexsort((old_relevant_positions, old_relevant_experts))
107 sorted_experts = old_relevant_experts[sort_order]
108 sorted_ranks = old_relevant_ranks[sort_order]
109
110 # Find boundaries where expert changes
111 expert_boundaries = np.concatenate(
112 [[0], np.where(np.diff(sorted_experts) != 0)[0] + 1, [len(sorted_experts)]]
113 )
114
115 # For each expert, extract unique ranks in order of first appearance
116 for i in range(len(expert_boundaries) - 1):
117 start, end = expert_boundaries[i], expert_boundaries[i + 1]
118 expert = int(sorted_experts[start])
119 expert_ranks = sorted_ranks[start:end]
120
121 # Get unique ranks preserving order
122 _, unique_idx = np.unique(expert_ranks, return_index=True)
123 unique_ranks = expert_ranks[np.sort(unique_idx)]
124 ranks_to_send_map[expert] = unique_ranks.tolist()
125
126 # Process new_indices (recv ranks)
127 if np.any(new_relevant_mask):
128 new_relevant_positions = position_indices[new_relevant_mask]
129 new_relevant_experts = new_indices[new_relevant_mask]
130 new_relevant_ranks = new_relevant_positions // num_local_experts
131
132 # Sort by expert first, then by position
133 sort_order = np.lexsort((new_relevant_positions, new_relevant_experts))
134 sorted_experts = new_relevant_experts[sort_order]
135 sorted_ranks = new_relevant_ranks[sort_order]
136
137 # Find boundaries where expert changes
138 expert_boundaries = np.concatenate(
139 [[0], np.where(np.diff(sorted_experts) != 0)[0] + 1, [len(sorted_experts)]]
140 )
141
142 # For each expert, extract unique ranks and exclude local copies
143 for i in range(len(expert_boundaries) - 1):
144 start, end = expert_boundaries[i], expert_boundaries[i + 1]
145 expert = int(sorted_experts[start])
146 expert_ranks = sorted_ranks[start:end]
147
148 # Get unique ranks preserving order
149 _, unique_idx = np.unique(expert_ranks, return_index=True)
150 unique_ranks = expert_ranks[np.sort(unique_idx)]
151
152 # Remove ranks that have local copies (in send map)
153 send_ranks_set = set(ranks_to_send_map.get(expert, []))
154 recv_ranks_actual = [
155 int(r) for r in unique_ranks if r not in send_ranks_set
156 ]
157 ranks_to_recv_map[expert] = recv_ranks_actual
158
159 # Handle experts that only appear in old (send only) or new (recv only)
160 for expert in unique_experts:
161 expert = int(expert)
162 if expert not in ranks_to_send_map:
163 ranks_to_send_map[expert] = []
164 if expert not in ranks_to_recv_map:
165 ranks_to_recv_map[expert] = []
166
167 return ranks_to_send_map, ranks_to_recv_map
168
169
170 def move_to_buffer(
171 num_local_experts: int,
172 old_indices: np.ndarray,
173 new_indices: np.ndarray,
174 expert_weights: Sequence[torch.Tensor],
175 expert_weights_buffers: Sequence[torch.Tensor],
176 stream: torch.Stream | None,
177 ep_rank: int,
178 communicator: EplbCommunicator,
179 layer_idx: int = 0,
180 ) -> TransferMetadata:
181 """Rearranges expert weights during EPLB rebalancing.
182
183 Args:
184 num_local_experts: Number of local experts.
185 old_indices: (num_experts_total,) ndarray of current (old)
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 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.
195
196 Returns:
197 TransferMetadata: Metadata needed for completing remote weight transfers.
198
199 """
200 assert old_indices.shape == new_indices.shape
201 recv_primary_mask = np.zeros((num_local_experts,), dtype=np.bool_)
202 send_expert_ids = np.full((num_local_experts,), -1, dtype=np.int64)
203 send_src_rows = np.full((num_local_experts,), -1, dtype=np.int32)
204 recv_expert_ids = np.full((num_local_experts,), -1, dtype=np.int64)
205 recv_dst_rows = np.full((num_local_experts,), -1, dtype=np.int32)
206
207 base = ep_rank * num_local_experts
208 local_rows = np.arange(num_local_experts, dtype=np.int32)
209 local_global = base + local_rows
210
211 old_local_expert_ids = old_indices[local_global]
212 new_local_expert_ids = new_indices[local_global]
213
214 # Unchanged mask
215 is_unchanged = old_local_expert_ids == new_local_expert_ids
216
217 # Local receive eligibility
218 new_valid = new_local_expert_ids != -1
219 can_recv_local = np.isin(
220 new_local_expert_ids, old_local_expert_ids, assume_unique=False
221 )
222 is_received_locally = np.logical_or(
223 is_unchanged, np.logical_and(new_valid, can_recv_local)
224 )
225
226 # Send map: first src row per unique expert present locally in old mapping
227 send_count = 0
228 valid_old = old_local_expert_ids != -1
229 if np.any(valid_old):
230 uniq_experts, first_idx = np.unique(
231 old_local_expert_ids[valid_old], return_index=True
232 )
233 filtered_rows = local_rows[valid_old]
234 src_rows = filtered_rows[first_idx]
235 send_count = int(uniq_experts.shape[0])
236 send_expert_ids[:send_count] = uniq_experts
237 send_src_rows[:send_count] = src_rows
238
239 # Recv map: primary dst per unique expert needed remotely
240 recv_count = 0
241 need_recv_mask = np.logical_and(~is_received_locally, new_valid)
242 if np.any(need_recv_mask):
243 desired_experts = new_local_expert_ids[need_recv_mask]
244 desired_dsts = local_rows[need_recv_mask]
245 uniq_recv_experts, uniq_indices = np.unique(desired_experts, return_index=True)
246 dst_rows = desired_dsts[uniq_indices]
247 recv_count = int(uniq_recv_experts.shape[0])
248 recv_expert_ids[:recv_count] = uniq_recv_experts
249 recv_dst_rows[:recv_count] = dst_rows
250 recv_primary_mask[dst_rows] = True
251
252 eligible_local_buffer_mask = np.logical_and(~is_unchanged, is_received_locally)
253
254 # 1. Local moves into tmp buffers
255 if bool(eligible_local_buffer_mask.any()) and send_count > 0:
256 dest_indices = np.nonzero(eligible_local_buffer_mask)[0].tolist()
257 expert_to_src_map = dict(
258 zip(send_expert_ids[:send_count], send_src_rows[:send_count])
259 )
260 for dst in dest_indices:
261 expert = new_local_expert_ids[dst]
262 src_local = expert_to_src_map.get(expert, -1)
263 if src_local != -1:
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 communicator.set_transfer_context(old_indices, layer_idx)
268
269 # 2. Post sends
270 if send_count > 0:
271 experts = send_expert_ids[:send_count]
272 srcs = send_src_rows[:send_count]
273 order = np.argsort(experts, kind="stable")
274 experts = experts[order]
275 srcs = srcs[order]
276
277 send_map, recv_map = get_ep_ranks_with_experts_batch(
278 experts,
279 num_local_experts,
280 old_indices,
281 new_indices,
282 )
283
284 for expert, src in zip(experts.tolist(), srcs.tolist()):
285 ranks_to_send = send_map[expert]
286 ranks_to_recv = recv_map[expert]
287 if not ranks_to_send or not ranks_to_recv:
288 continue
289 num_dst_per_sender = len(ranks_to_recv) // len(ranks_to_send)
290 sender_pos = ranks_to_send.index(ep_rank)
291 recv_begin = sender_pos * num_dst_per_sender
292 recv_end = recv_begin + num_dst_per_sender
293 recv_ranks = ranks_to_recv[recv_begin:recv_end]
294 remainder_start = len(ranks_to_send) * num_dst_per_sender
295 recver_pos = remainder_start + sender_pos
296 if recver_pos < len(ranks_to_recv):
297 recv_ranks.append(ranks_to_recv[recver_pos])
298 expert_tensors = [w[src] for w in expert_weights]
299 for dst in recv_ranks:
300 communicator.add_send(expert_tensors, dst, expert_id=int(expert))
301
302 # 3. Post recvs
303 if recv_count > 0:
304 experts = recv_expert_ids[:recv_count]
305 dsts = recv_dst_rows[:recv_count]
306 order = np.argsort(experts, kind="stable")
307 experts = experts[order]
308 dsts = dsts[order]
309
310 send_map, recv_map = get_ep_ranks_with_experts_batch(
311 experts,
312 num_local_experts,
313 old_indices,
314 new_indices,
315 )
316
317 for expert, dst in zip(experts.tolist(), dsts.tolist()):
318 ranks_to_send = send_map[expert]
319 ranks_to_recv = recv_map[expert]
320 if not ranks_to_send or not ranks_to_recv:
321 continue
322 num_dst_per_sender = len(ranks_to_recv) // len(ranks_to_send)
323 recver_pos = ranks_to_recv.index(ep_rank)
324 remainder_start = len(ranks_to_send) * num_dst_per_sender
325 if recver_pos < remainder_start:
326 src = ranks_to_send[recver_pos // num_dst_per_sender]
327 else:
328 src = ranks_to_send[recver_pos - remainder_start]
329 communicator.add_recv(
330 [b[dst] for b in expert_weights_buffers],
331 src,
332 expert_id=int(expert),
333 )
334
335 # 4. Execute transfers and wait for completion.
336 communicator.execute()
337 return TransferMetadata(
338 is_unchanged=is_unchanged,
339 is_received_locally=is_received_locally,
340 recv_primary_mask=recv_primary_mask,
341 recv_count=recv_count,
342 recv_expert_ids=recv_expert_ids,
343 recv_dst_rows=recv_dst_rows,
344 )
345
346
347 def move_from_buffer(
348 expert_weights: Sequence[torch.Tensor],
349 expert_weights_buffers: list[torch.Tensor],
350 transfer_metadata: TransferMetadata,
351 new_indices: np.ndarray,
352 ep_rank: int,
353 ) -> None:
354 """Copies expert weights from communication buffers back to the target weight
355 tensors after EPLB rebalancing.
356
357 Args:
358 expert_weights: List of the actual MoE layer weights used in the execution.
359 expert_weights_buffers: Intermediate buffers containing the experts weights
360 after the transfer is completed.
361 transfer_metadata: TransferMetadata containing transfer metadata.
362 new_indices: (num_experts_total,) mapping from local rows to desired
363 (possibly global) expert id, after rebalance.
364 ep_rank: Rank of the process in the expert parallel group.
365
366 """
367 is_unchanged = transfer_metadata.is_unchanged
368 is_received_locally = transfer_metadata.is_received_locally
369 recv_primary_mask = transfer_metadata.recv_primary_mask
370 recv_count = transfer_metadata.recv_count
371 recv_expert_ids = transfer_metadata.recv_expert_ids
372 recv_dst_rows = transfer_metadata.recv_dst_rows
373 num_local_experts = is_unchanged.shape[0]
374
375 # Mask for rows to copy back from buffers:
376 # copy if locally received OR remote primary recv
377 copy_mask = np.logical_or(is_received_locally, recv_primary_mask)
378 dest_mask_np = np.logical_and(~is_unchanged, copy_mask)
379 if bool(dest_mask_np.any()):
380 dest_indices = np.nonzero(dest_mask_np)[0].tolist()
381 for dst in dest_indices:
382 for w, b in zip(expert_weights, expert_weights_buffers):
383 w[dst].copy_(b[dst], non_blocking=True)
384
385 if recv_count == 0:
386 return
387
388 # Duplicate remote received rows to non-primary duplicate dsts
389 base = ep_rank * num_local_experts
390 local_experts = new_indices[base + np.arange(num_local_experts, dtype=np.int32)]
391 duplicate_mask = np.logical_and(
392 np.logical_and(~is_unchanged, ~is_received_locally),
393 np.logical_and(~recv_primary_mask, local_experts != -1),
394 )
395 # All received experts are unique in the destination, so no need to copy duplicates
396 if not bool(duplicate_mask.any()):
397 return
398
399 dup_dst_rows = np.nonzero(duplicate_mask)[0]
400 dup_experts = local_experts[dup_dst_rows]
401
402 prim_experts = recv_expert_ids[:recv_count]
403 prim_dsts = recv_dst_rows[:recv_count]
404 order = np.argsort(prim_experts, kind="stable")
405 prim_experts_sorted = prim_experts[order]
406 prim_dsts_sorted = prim_dsts[order]
407 pos = np.searchsorted(prim_experts_sorted, dup_experts)
408 valid = np.logical_and(
409 pos < prim_experts_sorted.shape[0],
410 prim_experts_sorted[np.minimum(pos, prim_experts_sorted.shape[0] - 1)]
411 == dup_experts,
412 )
413 if not bool(valid.any()):
414 return
415
416 matched_dst_rows = dup_dst_rows[valid]
417 matched_src_rows = prim_dsts_sorted[pos[valid]]
418
419 for dst, src in zip(matched_dst_rows.tolist(), matched_src_rows.tolist()):
420 for w in expert_weights:
421 w[dst].copy_(w[src], non_blocking=True)
422
423
424 def transfer_layer(
425 old_layer_indices: torch.Tensor,
426 new_layer_indices: torch.Tensor,
427 expert_weights: Sequence[torch.Tensor],
428 expert_weights_buffer: Sequence[torch.Tensor],
429 ep_group: ProcessGroup,
430 communicator: EplbCommunicator,
431 is_profile: bool = False,
432 stream: torch.Stream | None = None,
433 rank_mapping: dict[int, int] | None = None,
434 layer_idx: int = 0,
435 ) -> TransferMetadata:
436 """Rearranges the expert weights in place according to the new expert indices.
437
438 The value of the indices arguments are logical indices of the experts,
439 while keys are physical.
440
441 Args:
442 old_layer_indices: Shape (num_physical_experts,).
443 new_layer_indices: Shape (num_physical_experts,).
444 expert_weights: Iterable of weight tensors for this layer, each with shape
445 (num_local_physical_experts, hidden_size_i).
446 For example, a linear layer may have up and down projection.
447 expert_weights_buffer: Intermediate buffers (one per weight tensor).
448 ep_group: The device process group for expert parallelism.
449 communicator: EplbCommunicator instance for P2P communication.
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.
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
457 Returns:
458 TransferMetadata: Metadata needed for completing remote weight transfers,
459 including is_unchanged and is_received_locally masks.
460
461 """
462 ep_size = ep_group.size()
463 if rank_mapping is not None:
464 # Add a layer dimension for compatibility with mapping functions
465 old_layer_indices_2d = old_layer_indices.unsqueeze(0)
466 new_layer_indices_2d = new_layer_indices.unsqueeze(0)
467
468 if len(rank_mapping) == ep_group.size():
469 # scale down
470 new_layer_indices_2d = _map_new_expert_indices_with_rank_mapping(
471 new_layer_indices_2d,
472 rank_mapping,
473 )
474 else:
475 # scale up
476 old_layer_indices_2d = _map_old_expert_indices_with_rank_mapping(
477 old_layer_indices_2d,
478 rank_mapping,
479 ep_group.size(),
480 )
481
482 # Remove the layer dimension
483 old_layer_indices = old_layer_indices_2d.squeeze(0)
484 new_layer_indices = new_layer_indices_2d.squeeze(0)
485
486 assert old_layer_indices.shape == new_layer_indices.shape
487 num_physical_experts = old_layer_indices.shape[0]
488 assert len(expert_weights[0]) >= 1
489 num_local_physical_experts = expert_weights[0].shape[0]
490 assert num_physical_experts == ep_size * num_local_physical_experts
491
492 old_layer_indices_np = old_layer_indices.cpu().numpy()
493 new_layer_indices_np = new_layer_indices.cpu().numpy()
494
495 return move_to_buffer(
496 num_local_experts=num_local_physical_experts,
497 old_indices=old_layer_indices_np,
498 new_indices=new_layer_indices_np,
499 expert_weights=expert_weights,
500 expert_weights_buffers=expert_weights_buffer,
501 stream=stream,
502 ep_rank=ep_group.rank(),
503 communicator=communicator,
504 layer_idx=layer_idx,
505 )
506
507
508 def rearrange_expert_weights_inplace(
509 old_global_expert_indices: torch.Tensor,
510 new_global_expert_indices: torch.Tensor,
511 expert_weights: Sequence[Sequence[torch.Tensor]],
512 expert_buffer: Sequence[torch.Tensor],
513 ep_group: ProcessGroup,
514 communicator: EplbCommunicator,
515 is_profile: bool = False,
516 rank_mapping: dict[int, int] | None = None,
517 ) -> None:
518 """Rearranges the expert weights in place according to the new expert indices.
519
520 The value of the indices arguments are logical indices of the experts,
521 while keys are physical.
522
523 Args:
524 old_global_expert_indices: Shape (num_moe_layers, num_physical_experts).
525 new_global_expert_indices: Shape (num_moe_layers, num_physical_experts).
526 expert_weights: A sequence of shape (num_moe_layers)(weight_count)
527 of tensors of shape (num_local_physical_experts, hidden_size_i).
528 For example, a linear layer may have up and down projection,
529 so weight_count = 2. Each weight's hidden size can be different.
530 expert_buffer: Pre-allocated receive buffer tensors (one per
531 weight tensor in a single layer).
532 ep_group: The device process group for expert parallelism.
533 communicator: EplbCommunicator instance for P2P communication.
534 is_profile (bool): If `True`, do not perform any actual weight copy.
535 This is used during profile run, where we only perform dummy
536 communications to reserve enough memory for the buffers.
537 rank_mapping: A dictionary mapping old rank to new rank.
538
539 """
540 if rank_mapping is not None:
541 if len(rank_mapping) == ep_group.size():
542 # scale down
543 new_global_expert_indices = _map_new_expert_indices_with_rank_mapping(
544 new_global_expert_indices,
545 rank_mapping,
546 )
547 else:
548 # scale up
549 old_global_expert_indices = _map_old_expert_indices_with_rank_mapping(
550 old_global_expert_indices,
551 rank_mapping,
552 ep_group.size(),
553 )
554
555 assert old_global_expert_indices.shape[1] == new_global_expert_indices.shape[1]
556
557 num_moe_layers, num_physical_experts = old_global_expert_indices.shape
558 assert len(expert_weights) == num_moe_layers
559 assert len(expert_weights[0]) >= 1
560
561 num_local_physical_experts = expert_weights[0][0].shape[0]
562 assert new_global_expert_indices.shape == (num_moe_layers, num_physical_experts)
563
564 ep_size = ep_group.size()
565 ep_rank = ep_group.rank()
566 assert num_physical_experts == ep_size * num_local_physical_experts
567
568 first_layer_weights = list(expert_weights[0])
569
570 if is_profile:
571 if communicator.needs_profile_buffer_reservation:
572 # Reserve NCCL communication buffers via a dummy all_gather.
573 # Backends that pre-allocate their own transfer buffers
574 # skip this to avoid the extra memory spike during profiling.
575 profile_buffer: list[torch.Tensor] = [
576 torch.empty_like(w) for w in first_layer_weights
577 ]
578 for weight, buffer in zip(expert_weights[0], profile_buffer):
579 dummy_recv_buffer = [buffer for _ in range(ep_size)]
580 torch.distributed.barrier()
581 all_gather(
582 dummy_recv_buffer,
583 weight,
584 group=ep_group,
585 )
586 return
587
588 weights_buffer = list(expert_buffer)
589
590 # The per-layer transfer plan is built in Python, so both maps have to
591 # come back to the host. Once per rearrangement.
592 with gpu_sync_allowed():
593 old_global_expert_indices_cpu = old_global_expert_indices.cpu().numpy()
594 new_global_expert_indices_cpu = new_global_expert_indices.cpu().numpy()
595
596 for layer_idx in range(num_moe_layers):
597 transfer_metadata = move_to_buffer(
598 num_local_experts=num_local_physical_experts,
599 old_indices=old_global_expert_indices_cpu[layer_idx],
600 new_indices=new_global_expert_indices_cpu[layer_idx],
601 expert_weights=expert_weights[layer_idx],
602 expert_weights_buffers=weights_buffer,
603 stream=None,
604 ep_rank=ep_rank,
605 communicator=communicator,
606 layer_idx=layer_idx,
607 )
608
609 move_from_buffer(
610 expert_weights=expert_weights[layer_idx],
611 expert_weights_buffers=weights_buffer,
612 transfer_metadata=transfer_metadata,
613 new_indices=new_global_expert_indices_cpu[layer_idx],
614 ep_rank=ep_rank,
615 )
616
617
618 def _map_old_expert_indices_with_rank_mapping(
619 old_global_expert_indices: torch.Tensor,
620 rank_mapping: dict[int, int],
621 new_ep_size: int,
622 ) -> torch.Tensor:
623 """Map the old global expert indices to the new global expert indices.
624
625 Args:
626 old_global_expert_indices:
627 Shape (num_layers, old_ep_size * num_local_physical_experts).
628 rank_mapping: Mapping from old rank to new rank.
629 new_ep_size: New expert parallelism size.
630
631 Returns:
632 Mapped expert indices with shape
633 (num_layers, new_ep_size * num_local_physical_experts).
634
635 """
636 num_layers, old_num_physical_experts = old_global_expert_indices.shape
637 assert rank_mapping, "Rank mapping is required"
638
639 # Get sizes from parameters and rank_mapping
640 old_ep_size = len(rank_mapping)
641 num_local_physical_experts = old_num_physical_experts // old_ep_size
642 new_num_physical_experts = new_ep_size * num_local_physical_experts
643
644 # Create mapped tensor with new shape, initialized to -1
645 mapped_expert_indices = torch.full(
646 (num_layers, new_num_physical_experts),
647 fill_value=-1,
648 dtype=old_global_expert_indices.dtype,
649 device=old_global_expert_indices.device,
650 )
651
652 # Handle rank mapping (scale up/down with rank changes)
653 for old_rank in range(old_ep_size):
654 new_rank = rank_mapping.get(old_rank)
655 if new_rank is not None and new_rank >= 0 and new_rank < new_ep_size:
656 # This old rank exists in the new configuration
657 old_start_idx = old_rank * num_local_physical_experts
658 old_end_idx = (old_rank + 1) * num_local_physical_experts
659 new_start_idx = new_rank * num_local_physical_experts
660 new_end_idx = (new_rank + 1) * num_local_physical_experts
661
662 mapped_expert_indices[:, new_start_idx:new_end_idx] = (
663 old_global_expert_indices[:, old_start_idx:old_end_idx]
664 )
665 # If new_rank is None or >= new_ep_size, the experts remain -1
666 # (scale down case)
667
668 return mapped_expert_indices
669
670
671 def _map_new_expert_indices_with_rank_mapping(
672 new_global_expert_indices: torch.Tensor,
673 rank_mapping: dict[int, int],
674 ) -> torch.Tensor:
675 num_layers, new_num_physical_experts = new_global_expert_indices.shape
676 assert rank_mapping, "Rank mapping is required"
677
678 # Get sizes from parameters and rank_mapping
679 old_ep_size = len(rank_mapping)
680 new_ep_size = sum(new_rank != -1 for new_rank in rank_mapping.values())
681 num_local_physical_experts = new_num_physical_experts // new_ep_size
682 old_num_physical_experts = old_ep_size * num_local_physical_experts
683
684 mapped_expert_indices = torch.full(
685 (num_layers, old_num_physical_experts),
686 fill_value=-1,
687 dtype=new_global_expert_indices.dtype,
688 device=new_global_expert_indices.device,
689 )
690
691 for old_rank in range(old_ep_size):
692 new_rank = rank_mapping[old_rank]
693 if new_rank >= 0 and new_rank < new_ep_size:
694 old_start_idx = old_rank * num_local_physical_experts
695 old_end_idx = (old_rank + 1) * num_local_physical_experts
696 new_start_idx = new_rank * num_local_physical_experts
697 new_end_idx = (new_rank + 1) * num_local_physical_experts
698
699 mapped_expert_indices[:, old_start_idx:old_end_idx] = (
700 new_global_expert_indices[:, new_start_idx:new_end_idx]
701 )
702
703 return mapped_expert_indices
704
705
706 __all__ = ["transfer_layer", "move_from_buffer", "TransferMetadata"]