main
py 516 lines 18.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 """Expert Map Manager for MoE layers.
5
6 This module contains the ExpertMapManager class which manages expert ID
7 mappings and placement strategies for Expert Parallelism in MoE models.
8 """
9
10 import torch
11
12 from vllm.config.parallel import ExpertPlacementStrategy
13 from vllm.logger import init_logger
14 from vllm.model_executor.layers.fused_moe.config import FusedMoEParallelConfig
15 from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import (
16 init_aiter_topK_meta_data,
17 )
18
19 logger = init_logger(__name__)
20
21
22 def determine_expert_map(
23 ep_size: int,
24 ep_rank: int,
25 global_num_experts: int,
26 expert_placement_strategy: ExpertPlacementStrategy = "linear",
27 num_fused_shared_experts: int = 0,
28 return_expert_mask: bool = False,
29 ) -> tuple[int, torch.Tensor | None, torch.Tensor | None]:
30 """Calculates how many experts should be assigned to each rank for EP and
31 creates a mapping from global to local expert index. Experts are
32 distributed evenly across ranks. Any remaining are assigned to the
33 last rank.
34
35 Args:
36 ep_size: The size of the expert parallel group
37 ep_rank: The rank of the current process in the expert parallel
38 group
39 global_num_experts: The total number of experts in the model.
40 expert_placement_strategy: The expert placement strategy.
41 num_fused_shared_experts: Number of fused shared experts (for AITER)
42 return_expert_mask: Whether to return expert mask for AITER
43
44 Returns:
45 tuple[int, Optional[torch.Tensor], Optional[torch.Tensor]]: A tuple containing:
46 - local_num_experts (int): The number of experts assigned
47 to the current rank.
48 - expert_map (Optional[torch.Tensor]): A tensor of shape
49 (global_num_experts,) mapping from global to local index.
50 Contains -1 for experts not assigned to the current rank.
51 Returns None if ep_size is 1.
52 - expert_mask (Optional[torch.Tensor]): A tensor of shape
53 (global_num_experts + num_fused_shared_experts + 1,)
54 containing 1 for experts assigned to the current rank
55 and 0 for sentinel.
56 Returns None if ep_size is 1.
57 Used only when AITER MOE is enabled.
58
59 """
60 from typing import get_args
61
62 assert ep_size > 0
63 if ep_size == 1:
64 return (global_num_experts, None, None)
65
66 # Distribute experts as evenly as possible to each rank.
67 base_experts = global_num_experts // ep_size
68 remainder = global_num_experts % ep_size
69 local_num_experts = base_experts + 1 if ep_rank < remainder else base_experts
70
71 # Create a tensor of size num_experts filled with -1
72 expert_map = torch.full((global_num_experts,), -1, dtype=torch.int32)
73
74 # Create an expert map for the local experts
75 if expert_placement_strategy == "linear":
76 start_idx = ep_rank * base_experts + min(ep_rank, remainder)
77 expert_map[start_idx : start_idx + local_num_experts] = torch.arange(
78 0, local_num_experts, dtype=torch.int32
79 )
80 elif expert_placement_strategy == "round_robin":
81 local_log_experts = torch.arange(
82 ep_rank, global_num_experts, ep_size, dtype=torch.int32
83 )
84
85 expert_map[local_log_experts] = torch.arange(
86 0, local_num_experts, dtype=torch.int32
87 )
88 else:
89 raise ValueError(
90 "Unsupported expert placement strategy "
91 f"'{expert_placement_strategy}', expected one of "
92 f"{get_args(ExpertPlacementStrategy)}"
93 )
94
95 expert_mask = None
96 if return_expert_mask:
97 expert_mask = torch.ones(
98 (global_num_experts + num_fused_shared_experts + 1,), dtype=torch.int32
99 )
100 expert_mask[-1] = 0
101 expert_mask[:global_num_experts] = expert_map > -1
102 expert_map = torch.cat(
103 (
104 expert_map,
105 torch.tensor(
106 [local_num_experts + i for i in range(num_fused_shared_experts)],
107 dtype=torch.int32,
108 ),
109 ),
110 dim=0,
111 )
112
113 return (local_num_experts, expert_map, expert_mask)
114
115
116 def determine_expert_placement_strategy(
117 expert_placement_strategy: ExpertPlacementStrategy,
118 moe_parallel_config: FusedMoEParallelConfig,
119 num_expert_group: int | None,
120 num_redundant_experts: int,
121 enable_eplb: bool,
122 ) -> ExpertPlacementStrategy:
123 if expert_placement_strategy == "round_robin":
124 round_robin_supported = (
125 (num_expert_group is not None and num_expert_group > 1)
126 and num_redundant_experts == 0
127 and not enable_eplb
128 )
129
130 if not round_robin_supported:
131 logger.warning(
132 "Round-robin expert placement is only supported for "
133 "models with multiple expert groups and no redundant "
134 "experts. Falling back to linear expert placement."
135 )
136 return "linear"
137 if (
138 moe_parallel_config.use_all2all_kernels
139 and not moe_parallel_config.needs_round_robin_routing_tables
140 ):
141 logger.warning(
142 "Round-robin expert placement currently only supports "
143 "the DeepEP low-latency or NIXL EP backend, but '%s' was configured. "
144 "Falling back to linear expert placement.",
145 moe_parallel_config.all2all_backend,
146 )
147 return "linear"
148
149 return expert_placement_strategy
150
151
152 class ExpertMapManager:
153 """Manages expert ID mappings and placement for Expert Parallelism.
154
155 Responsibilities:
156 - Calculate local vs global expert counts
157 - Map between global, local, and physical expert IDs
158 - Manage placement strategies (linear, round_robin)
159 - Maintain routing tables for round-robin placement
160 - Support dynamic reconfiguration of EP topology
161
162 When expert_map is required:
163 - Expert Parallelism (EP) is enabled, i.e., when ep_size > 1
164 - EP disabled (ep_size == 1): expert_map is None
165 * All experts are local to the current rank
166 * No mapping is needed
167 - EP enabled (ep_size > 1): expert_map is created
168 * Maps global expert IDs to local expert IDs
169 * Shape: (global_num_experts,)
170 * Contains the local expert index for experts on this rank, -1 for experts
171 on other ranks
172 * Used by kernels to handle distributed expert execution
173 - Kernel support varies:
174 * Supports expert_map: fused_moe, fused_marlin_moe, fused_humming_moe,
175 rocm_aiter_fused_moe, deep_gemm_moe, xpu_moe, gpt_oss_triton_kernels_moe
176 * Does not support: flashinfer_cutlass_moe, fused_batched_moe, most cutlass_moe
177 variants, trtllm_* kernels
178 * When kernel doesn't support expert_map: The modular kernel method sets
179 expert_map=None even if EP is enabled
180 """
181
182 def __init__(
183 self,
184 max_num_batched_tokens: int,
185 top_k: int,
186 global_num_experts: int,
187 num_redundant_experts: int,
188 num_expert_group: int | None,
189 moe_parallel_config: FusedMoEParallelConfig,
190 placement_strategy: ExpertPlacementStrategy,
191 enable_eplb: bool,
192 num_fused_shared_experts: int = 0,
193 rocm_aiter_enabled: bool = False,
194 ):
195 """Initialize expert map manager.
196
197 Args:
198 max_num_batched_tokens: Maximum number of batched tokens per step
199 top_k: Number of experts each token is routed to
200 global_num_experts: Total number of experts across all ranks
201 num_redundant_experts: Number of redundant expert replicas (EPLB)
202 num_expert_group: Number of expert groups, when grouped routing is
203 used
204 moe_parallel_config: MoE parallel configuration (contains ep_size,
205 ep_rank, backend flags)
206 placement_strategy: Strategy for placing experts ('linear' or 'round_robin')
207 num_fused_shared_experts: Number of fused shared experts (for AITER)
208 rocm_aiter_enabled: Whether ROCm AITER fusion is enabled
209 enable_eplb: Whether expert-parallel load balancing is enabled
210
211 """
212 self.global_num_experts = global_num_experts
213 self.moe_parallel_config = moe_parallel_config
214 self.num_fused_shared_experts = num_fused_shared_experts
215 self.rocm_aiter_enabled = rocm_aiter_enabled
216 self.top_k = top_k
217 self.max_num_batched_tokens = max_num_batched_tokens
218
219 if moe_parallel_config.use_ep:
220 # Determine expert placement strategy before creating manager
221 placement_strategy = determine_expert_placement_strategy(
222 expert_placement_strategy=placement_strategy,
223 moe_parallel_config=moe_parallel_config,
224 num_expert_group=num_expert_group,
225 num_redundant_experts=num_redundant_experts,
226 enable_eplb=enable_eplb,
227 )
228
229 # Determine effective placement strategy
230 self._placement_strategy = self._determine_placement_strategy(
231 placement_strategy
232 )
233
234 # Calculate expert mappings
235 self._calculate_expert_maps()
236
237 # Initialize routing tables if needed
238 self._routing_tables = self._init_routing_tables()
239
240 self._init_aiter_shared_experts_topK_buffer()
241
242 if self.use_ep and self.rocm_aiter_enabled:
243 expert_mask = self.expert_mask
244 assert expert_mask is None or torch.all(
245 (expert_mask == 0) | (expert_mask == 1)
246 ), "Aiter Fused MoE kernel only supports expert_map with 0 and 1s."
247
248 # Log EP configuration
249 if self.use_ep:
250 logger.info_once(
251 "[EP Rank %s/%s] Expert parallelism is enabled. Expert "
252 "placement strategy: %s. Local/global"
253 " number of experts: %s/%s. Experts local to global index map:"
254 " %s.",
255 self.ep_rank,
256 self.ep_size,
257 self.placement_strategy,
258 self.local_num_experts,
259 self.global_num_experts,
260 self.get_compressed_map_string(),
261 )
262
263 def _init_aiter_shared_experts_topK_buffer(self):
264 if self.num_fused_shared_experts > 0:
265 dp_size = self.moe_parallel_config.dp_size
266 init_aiter_topK_meta_data(
267 n_routed_experts=self.global_num_experts,
268 n_shared_experts=self.num_fused_shared_experts,
269 top_k=self.top_k,
270 tp_rank=self.ep_rank if self.use_ep else self.tp_rank,
271 tp_size=self.ep_size if self.use_ep else self.tp_size,
272 shared_experts_score=1.0,
273 max_num_tokens=self.max_num_batched_tokens * dp_size,
274 is_EP=self.use_ep,
275 )
276
277 @property
278 def use_ep(self) -> int:
279 return self.moe_parallel_config.use_ep
280
281 @property
282 def ep_size(self) -> int:
283 return self.moe_parallel_config.ep_size
284
285 @property
286 def ep_rank(self) -> int:
287 return self.moe_parallel_config.ep_rank
288
289 @property
290 def tp_size(self) -> int:
291 return self.moe_parallel_config.tp_size
292
293 @property
294 def tp_rank(self) -> int:
295 return self.moe_parallel_config.tp_rank
296
297 @property
298 def local_num_experts(self) -> int:
299 return self._local_num_experts
300
301 @property
302 def expert_map(self) -> torch.Tensor | None:
303 """Mapping from global expert ID to local expert ID.
304
305 Returns tensor of shape (global_num_experts,) where:
306 - expert_map[global_id] = local_id if expert is on this rank
307 - expert_map[global_id] = -1 if expert is not on this rank
308
309 Returns None if EP is not enabled (ep_size == 1).
310 """
311 return self._expert_map
312
313 @property
314 def expert_mask(self) -> torch.Tensor | None:
315 """Expert mask for AITER fusion (ROCm-specific).
316
317 Returns tensor of shape (global_num_experts + num_fused_shared + 1,)
318 where 1 indicates expert is on this rank, 0 otherwise.
319 """
320 return self._expert_mask
321
322 @property
323 def placement_strategy(self) -> ExpertPlacementStrategy:
324 """Expert placement strategy ('linear' or 'round_robin')."""
325 return self._placement_strategy
326
327 @property
328 def routing_tables(
329 self,
330 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None:
331 """Routing tables for round-robin placement.
332
333 Returns (global_to_physical, physical_to_global, local_to_global)
334 or None if not using round-robin or tables not needed.
335 """
336 return self._routing_tables
337
338 def map_global_to_local(self, global_id: int) -> int:
339 """Map global expert ID to local expert ID.
340
341 Args:
342 global_id: Global expert ID (0 to global_num_experts - 1)
343
344 Returns:
345 Local expert ID (0 to local_num_experts - 1)
346
347 Raises:
348 ValueError: If expert is not on this rank
349
350 """
351 if self._expert_map is None:
352 return global_id
353
354 return self._expert_map[global_id].item()
355
356 def is_local_expert(self, global_id: int) -> bool:
357 """Check if expert is assigned to this rank."""
358 if self._expert_map is None:
359 return True
360 return self._expert_map[global_id] != -1
361
362 def get_local_expert_ids(self) -> list[int]:
363 """Get list of global IDs for experts on this rank."""
364 if self._expert_map is None:
365 return list(range(self.global_num_experts))
366
367 return torch.where(self._expert_map != -1)[0].tolist()
368
369 def update(
370 self,
371 moe_parallel_config: FusedMoEParallelConfig,
372 global_num_experts: int,
373 ) -> None:
374 """Update expert mappings for new EP configuration.
375
376 Used during dynamic reconfiguration (e.g., elastic scaling).
377
378 Args:
379 global_num_experts: New total number of experts across all ranks
380 moe_parallel_config: New MoE parallel configuration (contains ep_size,
381 ep_rank, backend flags)
382
383 """
384 self.moe_parallel_config = moe_parallel_config
385 self.global_num_experts = global_num_experts
386
387 if self._expert_map is not None:
388 device = self._expert_map.device
389 elif self._expert_mask is not None:
390 device = self._expert_mask.device
391 else:
392 raise AssertionError("_expert_map or _expert_mask must be present.")
393
394 with device:
395 self._calculate_expert_maps()
396 self._routing_tables = self._init_routing_tables()
397
398 # Reinitialize AITER buffer if needed and parameters provided
399 self._init_aiter_shared_experts_topK_buffer()
400
401 def get_compressed_map_string(self) -> str:
402 """Get compressed string representation of expert map for logging.
403
404 Returns string mapping local to global expert IDs.
405 """
406 if self._expert_map is None:
407 return f"[0..{self.global_num_experts - 1}]"
408
409 global_indices = torch.where(self._expert_map != -1)[0]
410 local_indices = self._expert_map[global_indices]
411 return ", ".join(
412 f"{local_index.item()}->{global_index.item()}"
413 for local_index, global_index in zip(local_indices, global_indices)
414 )
415
416 # Private methods
417
418 def _determine_placement_strategy(
419 self, requested_strategy: ExpertPlacementStrategy
420 ) -> ExpertPlacementStrategy:
421 """Determine effective placement strategy based on config."""
422 if requested_strategy != "round_robin":
423 return requested_strategy
424
425 # Round-robin requires specific conditions
426 if self.ep_size == 1:
427 return "linear"
428
429 if (
430 self.moe_parallel_config.use_all2all_kernels
431 and not self.moe_parallel_config.needs_round_robin_routing_tables
432 ):
433 logger.warning(
434 "Round-robin placement requires DeepEP-ll or NIXL backend. "
435 "Falling back to linear."
436 )
437 return "linear"
438
439 return "round_robin"
440
441 def _calculate_expert_maps(self) -> None:
442 """Calculate expert mappings based on placement strategy."""
443 (
444 self._local_num_experts,
445 self._expert_map,
446 self._expert_mask,
447 ) = determine_expert_map(
448 ep_size=self.ep_size,
449 ep_rank=self.ep_rank,
450 global_num_experts=self.global_num_experts,
451 expert_placement_strategy=self._placement_strategy,
452 num_fused_shared_experts=self.num_fused_shared_experts,
453 return_expert_mask=self.rocm_aiter_enabled,
454 )
455
456 self._local_num_experts += self.num_fused_shared_experts
457
458 def _init_routing_tables(
459 self,
460 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None:
461 """Ensure routing tables are initialized if needed for round-robin.
462
463 This is a public method that can be called to explicitly initialize
464 routing tables. It's safe to call multiple times (idempotent).
465 """
466 if self._placement_strategy != "round_robin":
467 return None
468
469 if not self.moe_parallel_config.needs_round_robin_routing_tables:
470 return None
471
472 if self._expert_map is None:
473 return None
474
475 return self._init_round_robin_expert_routing_tables()
476
477 def _init_round_robin_expert_routing_tables(
478 self,
479 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
480 """Build routing tables for round-robin placement."""
481 assert self.num_fused_shared_experts == 0, (
482 "Round robin not supported for AITER."
483 )
484
485 global_indices = torch.arange(
486 self.global_num_experts,
487 dtype=torch.long,
488 )
489 owner = torch.remainder(global_indices, self.ep_size)
490 local_index = torch.div(global_indices, self.ep_size, rounding_mode="floor")
491
492 base = self.global_num_experts // self.ep_size
493 remainder = self.global_num_experts % self.ep_size
494 physical_offset = owner * base
495
496 if remainder > 0:
497 remainder_tensor = torch.tensor(
498 remainder,
499 dtype=torch.long,
500 )
501 physical_offset = physical_offset + torch.minimum(owner, remainder_tensor)
502
503 global_to_physical = physical_offset + local_index
504 physical_to_global = torch.empty_like(global_to_physical)
505 physical_to_global[global_to_physical] = global_indices
506
507 local_global = torch.arange(
508 self.ep_rank,
509 self.global_num_experts,
510 self.ep_size,
511 dtype=torch.long,
512 )
513 if local_global.numel() != self._local_num_experts:
514 local_global = local_global[: self._local_num_experts]
515
516 return (global_to_physical, physical_to_global, local_global)