Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / expert_parallel / expert_parallel.py: 85%
346 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 05:41 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 05:41 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Expert Parallelism distributed strategies.
17Provides token permutation helpers and four parallel styles that compose with
18:class:`~hyper_parallel.core.expert_parallel.moe.GroupedExperts`:
20- :class:`BaseExpertParallel` — abstract base for EP strategies with
21 all-to-all token dispatch/combine.
22- :class:`ExpertParallel` — standard EP: each rank owns a shard of experts;
23 tokens are routed via differentiable all-to-all.
24- :class:`TensorParallel` — TP-only weight sharding for experts with no token
25 dispatch; for use when EP degree = 1.
26- :class:`ExpertTensorParallel` — combined EP + TP on a 2-D mesh ``[ep, tp]``;
27 weights are doubly sharded, dispatch uses the EP sub-mesh.
28"""
29__all__ = [
30 "AllToAllTokenDispatcher",
31 "DeredundencyTokenDispatcher",
32 "BaseExpertParallel",
33 "ExpertParallel",
34 "TensorParallel",
35 "ExpertTensorParallel",
36]
38from abc import ABC, abstractmethod
39from dataclasses import dataclass
40from typing import Any, List, Optional, Tuple, Union
42from hyper_parallel.core.dtensor.device_mesh import DeviceMesh
43from hyper_parallel.core.dtensor.dtensor import (
44 distribute_module,
45 distribute_tensor,
46 _distribute_module_iter_params,
47 _distribute_module_new_parameter,
48 _distribute_module_param_source,
49 _distribute_module_set_param,
50)
51from hyper_parallel.core.dtensor.placement_types import Shard
52from hyper_parallel.core.tensor_parallel.style import ParallelStyle
53from hyper_parallel.platform import AsyncHandle, get_platform
55platform = get_platform()
56Module = platform.Module
59# ---------------------------------------------------------------------------
60# Token permutation helpers
61# ---------------------------------------------------------------------------
63def _generate_permute_indices(
64 tokens_per_expert_group,
65 experts_per_rank: int,
66 num_ranks: int,
67):
68 """Generate permutation indices for rank-major → expert-major reordering.
70 After all-to-all, received tokens are laid out in rank-major order::
72 [rank0·expert0 tokens | rank0·expert1 tokens | ... |
73 rank1·expert0 tokens | rank1·expert1 tokens | ...]
75 Expert computation requires expert-major order::
77 [all tokens for local expert 0 | all tokens for local expert 1 | ...]
79 Args:
80 tokens_per_expert_group: 1-D integer tensor of shape
81 ``[num_ranks * experts_per_rank]``. Entry ``[r * E + e]`` is the
82 number of tokens received from rank ``r`` for local expert ``e``.
83 experts_per_rank: Number of experts owned by each rank.
84 num_ranks: EP degree (total number of ranks in the EP group).
86 Returns:
87 Tuple of:
89 - ``permuted_indices``: 1-D long tensor of length
90 ``total_received_tokens``. ``permuted_indices[i]`` is the source
91 position in the rank-major buffer for destination position ``i`` in
92 the expert-major buffer.
93 - ``num_tokens_per_expert``: 1-D integer tensor of length
94 ``experts_per_rank`` with the token count per local expert.
95 """
96 counts = tokens_per_expert_group # [num_ranks * experts_per_rank]
98 # num_tokens_per_expert[e] = Σ_r counts[r * E + e]
99 counts_2d = counts.view(num_ranks, experts_per_rank) # [R, E]
100 num_tokens_per_expert = counts_2d.sum(dim=0) # [E]
102 # ``total`` must be a host int because ``arange`` needs a scalar size.
103 # That single D2H drain is unavoidable. Everything else stays on
104 # device — no per-block ``.item()`` in a loop.
105 total = int(num_tokens_per_expert.sum())
106 if total == 0:
107 return counts.new_zeros(0, dtype=counts.dtype), num_tokens_per_expert
109 # ---- Vectorized expert-major permutation, no host stalls -----------
110 # Source offsets in the rank-major receive buffer for each (r, e) block.
111 src_offsets_rm = counts.cumsum(0) - counts # [R*E], starts of each block
112 # Reorder src offsets to expert-major iteration order: block (e, r).
113 src_offsets_em = (
114 src_offsets_rm.view(num_ranks, experts_per_rank).T.contiguous().view(-1)
115 ) # [E*R]
116 # Counts in expert-major iteration order.
117 counts_em = counts_2d.T.contiguous().view(-1) # [E*R]
119 # ``repeat_interleave`` expands each block's src start to one entry per
120 # token in that block — gives the source position of each output token.
121 block_src_starts = src_offsets_em.repeat_interleave(counts_em) # [total]
123 # Destination block starts in expert-major order, then expanded. The
124 # ``arange(total) - dst_block_starts_per_token`` produces 0..n-1 within
125 # each block, i.e. the intra-block offset.
126 dst_block_starts = counts_em.cumsum(0) - counts_em # [E*R]
127 dst_block_starts_per_token = dst_block_starts.repeat_interleave(counts_em)
128 intra = platform.arange(0, total, device=counts.device) - dst_block_starts_per_token
130 permuted_indices = (block_src_starts + intra).long()
131 return permuted_indices, num_tokens_per_expert
134def _permute(x, tokens_per_expert_group, ep_degree: int, num_local_experts: int):
135 """Apply rank-major → expert-major permutation to routed tokens.
137 Args:
138 x: Received token tensor of shape
139 ``[sum(tokens_per_expert_group), *feature_dims]``.
140 tokens_per_expert_group: 1-D integer tensor of shape
141 ``[ep_degree * num_local_experts]`` (output of the first
142 all-to-all that exchanges token counts).
143 ep_degree: EP group size (number of ranks).
144 num_local_experts: Number of experts owned by this rank.
146 Returns:
147 Tuple of:
149 - ``original_shape``: shape of *x* before permutation.
150 - ``permuted_x``: tokens reordered to expert-major layout.
151 - ``permuted_indices``: permutation indices (needed for
152 :func:`_unpermute`).
153 - ``num_tokens_per_expert``: token count per local expert.
154 """
155 original_shape = x.shape
156 permuted_indices, num_tokens_per_expert = _generate_permute_indices(
157 tokens_per_expert_group, num_local_experts, ep_degree
158 )
159 # ``x[permuted_indices]`` works for empty indices too (returns a
160 # shape-0 tensor with a real grad_fn). Avoid the early-return with
161 # ``new_zeros`` which would produce a leaf tensor without grad_fn and
162 # silently break autograd for ranks that happen to receive zero tokens.
163 permuted_x = x[permuted_indices]
164 return original_shape, permuted_x, permuted_indices, num_tokens_per_expert
167def _unpermute(out, original_shape, permuted_indices):
168 """Reverse the permutation applied by :func:`_permute`.
170 Args:
171 out: Expert-major output tensor of shape
172 ``[sum(num_tokens_per_expert), *feature_dims]``.
173 original_shape: Shape before permutation (from :func:`_permute`).
174 permuted_indices: Permutation indices from :func:`_permute`.
176 Returns:
177 Token tensor restored to the rank-major layout received after
178 all-to-all, with shape ``original_shape``.
179 """
180 # ``result[permuted_indices] = out`` is a differentiable scatter that
181 # also handles the empty-index case (no-op assignment, but autograd
182 # still connects ``result`` back to ``out``). Do NOT short-circuit
183 # with a bare ``new_zeros`` — that returns a leaf tensor without
184 # grad_fn and the downstream combine a2a loses its backward path,
185 # which manifests as "element 0 of tensors does not require grad".
186 result = out.new_zeros(*original_shape)
187 result[permuted_indices] = out
188 return result
191# ---------------------------------------------------------------------------
192# DispatchContext — state shared between token dispatch and combine
193# ---------------------------------------------------------------------------
195@dataclass
196class DispatchContext:
197 """Intermediate state between token dispatch and combine.
199 Stored in ``module._ep_dispatch_ctx`` for a single forward pass.
200 This solves the instance sharing problem when the same ExpertParallel
201 style object is applied to multiple layers:
203 Example problem (before this fix):
204 ep_style = ExpertParallel()
205 ep_style.apply(layer1.experts, mesh) # registers hooks
206 ep_style.apply(layer2.experts, mesh) # reuses same ep_style
208 # During forward:
209 # layer1.dispatch writes to ep_style._state_stack
210 # layer2.dispatch pushes to same stack ← INTERLEAVING
211 # layer1.combine pops wrong state (LIFO violation)
213 Solution: Store context per-module, not per-style-instance.
215 Built by :meth:`AllToAllTokenDispatcher.dispatch` and consumed by
216 :meth:`AllToAllTokenDispatcher.combine`. The caller
217 (e.g. :class:`ExpertParallel`) stores this on the module between the
218 paired dispatch/combine calls.
219 """
221 input_splits: List[int]
222 output_splits: List[int]
223 input_shape: Tuple[int, ...]
224 permuted_indices: Any
227@dataclass
228class DeredundencyDispatchContext(DispatchContext):
229 """State shared by deredundency dispatch and combine.
231 The inherited split and permutation fields describe the inner-EP
232 all-to-all. The extra fields describe the OEP shared token view and the
233 whiteboard scatter used before the final reduce-scatter combine.
234 """
236 dispatch_indices: Optional[object] = None
237 router_coeff: Optional[object] = None
238 gathered_shape: Optional[tuple] = None
239 oep_size: int = 1
243@dataclass(frozen=True)
244class _DeredundencyMeshInfo:
245 """Resolved mesh metadata for two-stage deredundency token exchange."""
247 oep_group: object
248 iep_group: object
249 oep_size: int
250 iep_size: int
251 outer_rank: int
252 inner_rank: int
255def _get_deredundency_mesh_info(device_mesh: DeviceMesh) -> _DeredundencyMeshInfo:
256 """Resolve ``oep`` / ``iep`` groups from a 1-D or 2-D EP mesh."""
257 ndim = getattr(device_mesh, "ndim", 1)
258 if not isinstance(ndim, int):
259 ndim = 1
260 if ndim == 1:
261 return _DeredundencyMeshInfo(
262 oep_group=None,
263 iep_group=device_mesh.get_group(),
264 oep_size=1,
265 iep_size=device_mesh.size(),
266 outer_rank=0,
267 inner_rank=device_mesh.get_local_rank(),
268 )
269 if ndim != 2:
270 raise ValueError(
271 "DeredundencyTokenDispatcher expects a 1-D EP mesh or a 2-D "
272 f"[oep, iep] EP mesh, but got ndim={ndim}."
273 )
275 mesh_dim_names = getattr(device_mesh, "mesh_dim_names", None) or ()
276 oep_dim = mesh_dim_names.index("oep") if "oep" in mesh_dim_names else 0
277 iep_dim = mesh_dim_names.index("iep") if "iep" in mesh_dim_names else 1
278 if oep_dim == iep_dim:
279 raise ValueError("DeredundencyTokenDispatcher requires distinct oep and iep mesh dimensions.")
281 return _DeredundencyMeshInfo(
282 oep_group=device_mesh.get_group(oep_dim),
283 iep_group=device_mesh.get_group(iep_dim),
284 oep_size=device_mesh.size(oep_dim),
285 iep_size=device_mesh.size(iep_dim),
286 outer_rank=device_mesh.get_local_rank(oep_dim),
287 inner_rank=device_mesh.get_local_rank(iep_dim),
288 )
291def _generate_deredundency_dispatch_indices(
292 tokens_per_expert_by_source,
293 expert_start: int,
294 iep_size: int,
295 num_local_experts: int,
296):
297 """Generate gather-view indices ordered by IEP destination rank.
299 ``tokens_per_expert_by_source`` is shaped ``[oep_size, num_experts]`` and
300 describes each source rank's expert-major routed buffer after the OEP
301 all-gather. The returned indices select the current outer expert range
302 and order it as ``[iep_dst, local_expert, oep_source]`` so each IEP
303 destination chunk keeps local-expert blocks contiguous for the later
304 rank-major → expert-major permutation.
305 """
306 oep_size = tokens_per_expert_by_source.shape[0]
307 experts_per_outer = iep_size * num_local_experts
308 expert_end = expert_start + experts_per_outer
310 source_totals = tokens_per_expert_by_source.sum(dim=1)
311 source_offsets = source_totals.cumsum(0) - source_totals
312 expert_offsets = (
313 tokens_per_expert_by_source.cumsum(dim=1)
314 - tokens_per_expert_by_source
315 + source_offsets.view(oep_size, 1)
316 )
318 selected_counts = tokens_per_expert_by_source[:, expert_start:expert_end].view(
319 oep_size, iep_size, num_local_experts,
320 )
321 selected_offsets = expert_offsets[:, expert_start:expert_end].view(
322 oep_size, iep_size, num_local_experts,
323 )
324 counts_by_destination = selected_counts.permute(1, 2, 0).contiguous()
325 offsets_by_destination = selected_offsets.permute(1, 2, 0).contiguous()
327 block_counts = counts_by_destination.view(-1)
328 token_counts_by_destination_expert = selected_counts.sum(dim=0).contiguous().view(-1)
329 total = int(block_counts.sum())
330 if total == 0:
331 return block_counts.new_zeros(0, dtype=block_counts.dtype).long(), token_counts_by_destination_expert
333 block_starts = offsets_by_destination.view(-1).repeat_interleave(block_counts)
334 block_offsets = block_counts.cumsum(0) - block_counts
335 block_offsets_per_token = block_offsets.repeat_interleave(block_counts)
336 intra = platform.arange(0, total, device=tokens_per_expert_by_source.device) - block_offsets_per_token
338 return (block_starts + intra).long(), token_counts_by_destination_expert
341def _scale_by_router_coeff(tokens, router_coeff):
342 """Scale routed expert outputs by optional router coefficients."""
343 if router_coeff is None:
344 return tokens
345 if router_coeff.shape[0] != tokens.shape[0]:
346 raise ValueError(
347 "router_coeff length must match routed token count, got "
348 f"{router_coeff.shape[0]} and {tokens.shape[0]}."
349 )
350 coeff = router_coeff
351 if len(coeff.shape) == 1 and len(tokens.shape) > 1:
352 coeff = coeff.reshape((-1,) + (1,) * (len(tokens.shape) - 1))
353 return tokens * coeff
356def _scatter_add_first_dim(src, indices, output_shape):
357 """Scatter-add rows of ``src`` into a zero tensor along dim 0."""
358 result = src.new_zeros(*output_shape)
359 if len(src.shape) == 1:
360 scatter_indices = indices
361 else:
362 scatter_indices = indices.reshape((-1,) + (1,) * (len(src.shape) - 1)).expand(
363 -1, *src.shape[1:],
364 )
365 if hasattr(result, "scatter_add"):
366 return result.scatter_add(0, scatter_indices, src)
367 if hasattr(result, "index_add"):
368 return result.index_add(0, indices, src)
369 raise RuntimeError(
370 "DeredundencyTokenDispatcher.combine requires tensor scatter_add or "
371 "index_add support for exdispatch_idx accumulation."
372 )
375class _DeredundencyCombineHandle(AsyncHandle):
376 """Async handle that finishes deredundency combine post-processing."""
378 def __init__(
379 self,
380 async_tensor: object,
381 mesh_info: _DeredundencyMeshInfo,
382 ctx: DeredundencyDispatchContext,
383 ) -> None:
384 super().__init__(async_tensor)
385 self._mesh_info = mesh_info
386 self._ctx = ctx
387 self._combined: Optional[object] = None
389 def wait(self) -> object:
390 """Wait for IEP a2a, then finish OEP scatter/reduce combine once."""
391 if self._combined is None:
392 outer_output = super().wait()
393 weighted_output = _scale_by_router_coeff(outer_output, self._ctx.router_coeff)
394 combine_whiteboard = _scatter_add_first_dim(
395 weighted_output,
396 self._ctx.dispatch_indices,
397 self._ctx.gathered_shape,
398 )
399 if self._ctx.oep_size == 1:
400 self._combined = combine_whiteboard
401 else:
402 self._combined = platform.differentiable_reduce_scatter(
403 combine_whiteboard,
404 self._ctx.oep_size,
405 0,
406 "sum",
407 self._mesh_info.oep_group,
408 )
409 return self._combined
412# ---------------------------------------------------------------------------
413# BaseExpertParallel — abstract base for all-to-all EP strategies
414# ---------------------------------------------------------------------------
416class BaseExpertParallel(ParallelStyle, ABC):
417 """Abstract base class for Expert Parallel strategies with token dispatch.
419 Subclasses implement :meth:`_partition_fn`, :meth:`_token_dispatch`, and
420 :meth:`_token_combine`; this class wires them into :func:`distribute_module`.
421 """
423 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module:
424 """Apply EP sharding and dispatch/combine hooks to *module*.
426 Args:
427 module: A :class:`~hyper_parallel.core.expert_parallel.moe.GroupedExperts`
428 instance to shard.
429 device_mesh: Device mesh for this EP strategy.
431 Returns:
432 The module with distributed parameters and dispatch/combine hooks.
433 """
434 return distribute_module(
435 module,
436 device_mesh,
437 self._partition_fn,
438 self._token_dispatch,
439 self._token_combine,
440 )
442 @abstractmethod
443 def _partition_fn(
444 self, name: str, module: Module, device_mesh: DeviceMesh
445 ) -> None:
446 """Shard module parameters according to this strategy.
448 Args:
449 name: Submodule name.
450 module: The module whose parameters are being sharded.
451 device_mesh: Device mesh for this EP strategy.
452 """
454 @abstractmethod
455 def _token_dispatch(self, module: Module, inputs, device_mesh: DeviceMesh):
456 """Pre-hook: route input tokens to their assigned ranks.
458 Args:
459 module: The ``GroupedExperts`` module.
460 inputs: Forward inputs tuple.
461 device_mesh: Device mesh for this EP strategy.
463 Returns:
464 Transformed inputs for local expert computation.
465 """
467 @abstractmethod
468 def _token_combine(self, module: Module, routed_output, device_mesh: DeviceMesh):
469 """Post-hook: gather expert outputs back to the originating ranks.
471 Args:
472 module: The ``GroupedExperts`` module.
473 routed_output: Expert output tensor in expert-major order.
474 device_mesh: Device mesh for this EP strategy.
476 Returns:
477 Token tensor in the original token-major layout.
478 """
481# ---------------------------------------------------------------------------
482# AllToAllTokenDispatcher — token dispatch/combine via all-to-all
483# ---------------------------------------------------------------------------
485class AllToAllTokenDispatcher:
486 """Token dispatch and combine via all-to-all for expert parallelism.
488 Provides :meth:`dispatch` and :meth:`combine` as static methods that
489 receive and return a :class:`DispatchContext` object. This decouples
490 the all-to-all token routing logic from the parallel style class so
491 that it can be reused or tested independently.
493 Callers (e.g. :class:`ExpertParallel`) are responsible for storing the
494 context between the paired dispatch/combine calls.
495 """
497 @staticmethod
498 def dispatch(module: Module, inputs: tuple, device_mesh: DeviceMesh) -> tuple:
499 """Dispatch tokens to their assigned ranks via all-to-all.
501 Called as an ``input_fn`` hook by :func:`distribute_module`. Receives
502 the module's forward inputs and returns transformed inputs.
504 Args:
505 module: The ``GroupedExperts`` module.
506 inputs: Tuple ``(routed_input, num_tokens_per_expert)`` where
507 ``routed_input`` has shape ``[total_tokens, dim]`` and
508 ``num_tokens_per_expert`` has shape ``[num_experts]``.
509 device_mesh: EP device mesh (1-D).
511 Returns:
512 Tuple ``(permuted_local_input, local_token_counts, ctx)`` —
513 the first two elements are the transformed inputs for local
514 expert computation; *ctx* is a :class:`DispatchContext`
515 carrying the updated state to be stored by the caller.
516 """
517 del module # module unused, kept for API consistency
518 routed_input, num_tokens_per_expert = inputs[0], inputs[1]
519 ep_group = device_mesh.get_group()
520 ep_size = device_mesh.size()
521 num_local_experts = num_tokens_per_expert.shape[0] // ep_size
523 # --- Step 1: exchange token counts (no gradient needed) ---
524 # Each rank needs to know how many tokens it will receive from every
525 # other rank (for each local expert). Uses ``async_op=True`` + an
526 # explicit ``handle.wait()`` rather than ``async_op=False`` because
527 # the implicit cross-stream sync is NCCL-only; on HCCL the compute
528 # stream may read ``counts_out`` before the collective write is
529 # visible, producing garbage values that blow up the downstream
530 # ``torch.empty(sum(output_splits), ...)`` allocation.
531 counts_out, handle = platform.all_to_all_single(
532 num_tokens_per_expert,
533 output_shape=[num_tokens_per_expert.shape[0]],
534 group=ep_group,
535 async_op=True,
536 )
537 if handle is not None:
538 handle.wait()
539 # counts_out shape: [ep_size * num_local_experts]
540 # counts_out[r * num_local_experts + e] = tokens from rank r for expert e
542 # --- Step 2: compute input / output splits ---
543 # input_splits[r] = tokens this rank sends to rank r
544 # output_splits[r] = tokens this rank receives from rank r
545 # Reshape to [ep_size, num_local_experts] and sum per rank on device;
546 # a single ``tolist()`` drains the rank-sum vector to host, replacing
547 # ``2 * ep_size`` scalar ``int()`` D2H syncs with 2.
548 input_splits = num_tokens_per_expert.view(ep_size, num_local_experts).sum(dim=1).tolist()
549 output_splits = counts_out.view(ep_size, num_local_experts).sum(dim=1).tolist()
551 # --- Step 3: exchange actual tokens (differentiable) ---
552 dispatched = platform.differentiable_all_to_all_single(
553 routed_input, input_splits, output_splits, group=ep_group,
554 )
556 # --- Step 4: rank-major → expert-major permutation ---
557 input_shape, permuted, permuted_indices, local_counts = _permute(
558 dispatched, counts_out, ep_size, num_local_experts
559 )
561 # Build dispatch context for combine step.
562 # Caller (e.g., ExpertParallel._token_dispatch) is responsible for storing
563 # this context and passing it to combine(). This decouples dispatch/combine
564 # from module state and solves the instance sharing problem.
565 ctx = DispatchContext(
566 input_splits=input_splits,
567 output_splits=output_splits,
568 input_shape=input_shape,
569 permuted_indices=permuted_indices,
570 )
572 return permuted, local_counts, ctx
574 @staticmethod
575 def combine(module: Module, routed_output: object, device_mesh: DeviceMesh, ctx: DispatchContext) -> object:
576 """Gather expert outputs back to the originating ranks via all-to-all.
578 Called as an ``output_fn`` hook by :func:`distribute_module`.
579 Receives dispatch context from the caller (previously returned by dispatch).
581 Args:
582 module: The ``GroupedExperts`` module (unused, for API consistency).
583 routed_output: Expert output tensor in expert-major order,
584 shape ``[sum(local_counts), dim]``.
585 device_mesh: EP device mesh (1-D).
586 ctx: :class:`DispatchContext` previously returned by
587 :meth:`dispatch`.
589 Returns:
590 Token tensor in the original token-major layout,
591 shape ``[sum(input_splits), dim]``.
592 """
593 del module # module not used, kept for API consistency
594 ep_group = device_mesh.get_group()
596 # expert-major → rank-major
597 unpermuted = _unpermute(routed_output, ctx.input_shape, ctx.permuted_indices)
599 # reverse all-to-all (output/input splits are swapped)
600 combined = platform.differentiable_all_to_all_single(
601 unpermuted,
602 ctx.output_splits, # was output, now becomes input
603 ctx.input_splits, # was input, now becomes output
604 group=ep_group,
605 )
606 return combined
608 @staticmethod
609 def combine_start(routed_output, device_mesh, ctx):
610 """Launch async combine all-to-all without waiting for completion.
612 Splits the combine into two phases so that the caller can overlap
613 the a2a communication with independent computation (e.g. a shared
614 expert forward pass). The caller must later call
615 :meth:`combine_wait` or ``handle.wait()`` to obtain the final
616 result.
618 Step 1 (synchronous, local): expert-major → rank-major unpermute.
619 Step 2 (asynchronous, cross-rank): reverse all-to-all.
621 Args:
622 routed_output: Expert output tensor in expert-major order,
623 shape ``[sum(local_counts), dim]``.
624 device_mesh: EP device mesh (1-D).
625 ctx: :class:`DispatchContext` previously returned by
626 :meth:`dispatch`.
628 Returns:
629 :class:`AsyncHandle` carrying the state needed by
630 :meth:`combine_wait`.
631 """
632 ep_group = device_mesh.get_group()
634 # expert-major → rank-major (local, no communication)
635 unpermuted = _unpermute(routed_output, ctx.input_shape, ctx.permuted_indices)
637 # async reverse all-to-all (output/input splits are swapped)
638 combined_async = platform.differentiable_all_to_all_single_async(
639 unpermuted,
640 ctx.output_splits,
641 ctx.input_splits,
642 group=ep_group,
643 )
645 return AsyncHandle(combined_async)
647 @staticmethod
648 def combine_wait(handle):
649 """Wait for the async combine all-to-all to complete.
651 Args:
652 handle: :class:`AsyncHandle` returned by :meth:`combine_start`.
654 Returns:
655 Combined tensor in the original token-major layout.
656 """
657 return handle.wait()
660# ---------------------------------------------------------------------------
661# DeredundencyTokenDispatcher — token dispatch via OEP all-gather + IEP all-to-all
662# ---------------------------------------------------------------------------
664class DeredundencyTokenDispatcher:
665 """Token dispatch/combine via OEP all-gather plus IEP all-to-all.
667 This dispatcher keeps the same public contract as
668 :class:`AllToAllTokenDispatcher`, but decomposes the global EP all-to-all
669 into the deredundency flow described in
670 ``docs/moe_alltoall_deredundency_token_permutation.md``:
672 1. Form a shared token/count view across the OEP group.
673 2. Select only the current outer expert range.
674 3. Send selected tokens to concrete local-expert ranks inside the IEP
675 group.
676 4. Sort received tokens into local expert-major order.
678 For a 2-D mesh, dimension ``"oep"`` / ``0`` is the outer group and
679 ``"iep"`` / ``1`` is the inner group. A 1-D mesh is treated as
680 ``oep_size == 1`` and degenerates to the standard all-to-all data flow.
681 """
683 @staticmethod
684 def _oep_gather_for_dispatch(
685 num_tokens_per_expert,
686 routed_input,
687 router_coeff,
688 mesh_info: _DeredundencyMeshInfo,
689 ) -> tuple:
690 """All-gather token counts and routed input across the OEP group.
692 Args:
693 num_tokens_per_expert: Token count per expert ``[num_experts]``.
694 routed_input: Routed token tensor ``[total_tokens, dim]``.
695 router_coeff: Optional router coefficients ``[total_tokens]``.
696 mesh_info: Resolved OEP/IEP mesh descriptor.
698 Returns:
699 Tuple ``(gathered_counts, gathered_routed, gathered_router_coeff)``
700 where ``gathered_counts`` has shape ``[oep_size, num_experts]``,
701 ``gathered_routed`` has shape ``[oep_size * total_tokens, dim]``,
702 and ``gathered_router_coeff`` is the gathered coefficients or None.
704 Raises:
705 ValueError: If routed token counts differ across OEP ranks.
706 """
707 if mesh_info.oep_size == 1:
708 gathered_counts = num_tokens_per_expert.view(1, num_tokens_per_expert.shape[0])
709 return gathered_counts, routed_input, router_coeff
711 gathered_counts, handle = platform.all_gather_single(
712 num_tokens_per_expert,
713 output_shape=[mesh_info.oep_size * num_tokens_per_expert.shape[0]],
714 group=mesh_info.oep_group,
715 async_op=True,
716 )
717 if handle is not None:
718 handle.wait()
719 gathered_counts = gathered_counts.view(mesh_info.oep_size, num_tokens_per_expert.shape[0])
720 source_token_totals = gathered_counts.sum(dim=1).tolist()
721 if any(total != routed_input.shape[0] for total in source_token_totals):
722 raise ValueError(
723 "DeredundencyTokenDispatcher requires equal routed token "
724 "counts within each OEP group because the shared token view "
725 f"uses all-gather, got totals {source_token_totals}."
726 )
727 gathered_routed = platform.differentiable_all_gather_concat(
728 routed_input, mesh_info.oep_group, mesh_info.oep_size, 0,
729 )
730 if router_coeff is None:
731 gathered_router_coeff = None
732 else:
733 gathered_router_coeff = platform.differentiable_all_gather_concat(
734 router_coeff, mesh_info.oep_group, mesh_info.oep_size, 0,
735 )
736 return gathered_counts, gathered_routed, gathered_router_coeff
738 @staticmethod
739 def dispatch(module: Module, inputs: tuple, device_mesh: DeviceMesh) -> tuple:
740 """Dispatch tokens using OEP all-gather and IEP all-to-all.
742 Args:
743 module: The ``GroupedExperts`` module (unused here).
744 inputs: Tuple ``(routed_input, num_tokens_per_expert)`` where
745 ``routed_input`` has shape ``[total_tokens, dim]`` and
746 ``num_tokens_per_expert`` has shape ``[num_experts]``.
747 device_mesh: 1-D EP mesh or 2-D ``[oep, iep]`` EP mesh.
749 Returns:
750 Tuple ``(permuted_local_input, local_token_counts, ctx)`` with the
751 same meaning as :meth:`AllToAllTokenDispatcher.dispatch`.
753 Raises:
754 ValueError: If the expert count is not divisible by the full EP
755 size represented by the deredundency mesh.
756 """
757 del module
758 routed_input, num_tokens_per_expert = inputs[0], inputs[1]
759 router_coeff = inputs[2] if len(inputs) > 2 else None
760 if router_coeff is not None and router_coeff.shape[0] != routed_input.shape[0]:
761 raise ValueError(
762 "router_coeff length must match routed_input token count, got "
763 f"{router_coeff.shape[0]} and {routed_input.shape[0]}."
764 )
765 mesh_info = _get_deredundency_mesh_info(device_mesh)
766 ep_size = mesh_info.oep_size * mesh_info.iep_size
767 if num_tokens_per_expert.shape[0] % ep_size != 0:
768 raise ValueError(
769 "num_tokens_per_expert length must be divisible by the full "
770 f"EP size {ep_size}, got {num_tokens_per_expert.shape[0]}."
771 )
772 num_local_experts = num_tokens_per_expert.shape[0] // ep_size
773 experts_per_outer = mesh_info.iep_size * num_local_experts
774 expert_start = mesh_info.outer_rank * experts_per_outer
776 gathered_counts, gathered_routed, gathered_router_coeff = (
777 DeredundencyTokenDispatcher._oep_gather_for_dispatch(
778 num_tokens_per_expert, routed_input, router_coeff, mesh_info,
779 )
780 )
782 dispatch_indices, node_counts_per_expert = _generate_deredundency_dispatch_indices(
783 gathered_counts,
784 expert_start,
785 mesh_info.iep_size,
786 num_local_experts,
787 )
788 iep_input_splits = node_counts_per_expert.view(mesh_info.iep_size, num_local_experts).sum(dim=1).tolist()
790 iep_counts_out, handle = platform.all_to_all_single(
791 node_counts_per_expert,
792 output_shape=[node_counts_per_expert.shape[0]],
793 group=mesh_info.iep_group,
794 async_op=True,
795 )
796 if handle is not None:
797 handle.wait()
798 iep_output_splits = iep_counts_out.view(mesh_info.iep_size, num_local_experts).sum(dim=1).tolist()
800 outer_routed_input = gathered_routed[dispatch_indices]
801 outer_router_coeff = (
802 None if gathered_router_coeff is None else gathered_router_coeff[dispatch_indices]
803 )
804 dispatched = platform.differentiable_all_to_all_single(
805 outer_routed_input,
806 iep_input_splits,
807 iep_output_splits,
808 group=mesh_info.iep_group,
809 )
811 input_shape, permuted, permuted_indices, local_counts = _permute(
812 dispatched, iep_counts_out, mesh_info.iep_size, num_local_experts,
813 )
814 ctx = DeredundencyDispatchContext(
815 input_splits=iep_input_splits,
816 output_splits=iep_output_splits,
817 input_shape=input_shape,
818 permuted_indices=permuted_indices,
819 dispatch_indices=dispatch_indices,
820 router_coeff=outer_router_coeff,
821 gathered_shape=gathered_routed.shape,
822 oep_size=mesh_info.oep_size,
823 )
824 return permuted, local_counts, ctx
826 @staticmethod
827 def combine(module: Module, routed_output: object, device_mesh: DeviceMesh,
828 ctx: DeredundencyDispatchContext) -> object:
829 """Gather expert outputs back to the originating ranks.
831 Args:
832 module: The ``GroupedExperts`` module (unused).
833 routed_output: Expert output tensor in expert-major order.
834 device_mesh: 1-D EP mesh or 2-D ``[oep, iep]`` EP mesh.
835 ctx: Context returned by :meth:`dispatch`.
837 Returns:
838 Token tensor in the original source-rank routed order.
839 """
840 del module
841 mesh_info = _get_deredundency_mesh_info(device_mesh)
842 DeredundencyTokenDispatcher._validate_combine_mesh(mesh_info, ctx)
844 unpermuted = _unpermute(routed_output, ctx.input_shape, ctx.permuted_indices)
845 outer_output = platform.differentiable_all_to_all_single(
846 unpermuted,
847 ctx.output_splits,
848 ctx.input_splits,
849 group=mesh_info.iep_group,
850 )
852 weighted_output = _scale_by_router_coeff(outer_output, ctx.router_coeff)
853 combine_whiteboard = _scatter_add_first_dim(
854 weighted_output, ctx.dispatch_indices, ctx.gathered_shape,
855 )
856 if ctx.oep_size == 1:
857 return combine_whiteboard
859 return platform.differentiable_reduce_scatter(
860 combine_whiteboard,
861 ctx.oep_size,
862 0,
863 "sum",
864 mesh_info.oep_group,
865 )
867 @staticmethod
868 def _validate_combine_mesh(
869 mesh_info: _DeredundencyMeshInfo,
870 ctx: DeredundencyDispatchContext,
871 ) -> None:
872 """Validate that dispatch context and combine mesh are compatible."""
873 if mesh_info.oep_size != ctx.oep_size:
874 raise ValueError(
875 "DeredundencyTokenDispatcher.combine received a context for "
876 f"oep_size={ctx.oep_size}, but the mesh resolves to oep_size={mesh_info.oep_size}."
877 )
879 @staticmethod
880 def combine_start(
881 routed_output: object,
882 device_mesh: DeviceMesh,
883 ctx: DeredundencyDispatchContext,
884 ) -> AsyncHandle:
885 """Launch async IEP combine all-to-all and defer deredundency post-processing.
887 The local expert-major → rank-major unpermute is performed
888 synchronously. The reverse IEP all-to-all is launched asynchronously,
889 and :meth:`combine_wait` finishes router weighting, whiteboard
890 scatter-add, and optional OEP reduce-scatter after the async output is
891 materialised.
893 Args:
894 routed_output: Expert output tensor in expert-major order.
895 device_mesh: 1-D EP mesh or 2-D ``[oep, iep]`` EP mesh.
896 ctx: Context returned by :meth:`dispatch`.
898 Returns:
899 :class:`AsyncHandle` carrying the pending IEP a2a and deredundency
900 combine state.
901 """
902 mesh_info = _get_deredundency_mesh_info(device_mesh)
903 DeredundencyTokenDispatcher._validate_combine_mesh(mesh_info, ctx)
905 unpermuted = _unpermute(routed_output, ctx.input_shape, ctx.permuted_indices)
906 outer_output_async = platform.differentiable_all_to_all_single_async(
907 unpermuted,
908 ctx.output_splits,
909 ctx.input_splits,
910 group=mesh_info.iep_group,
911 )
912 return _DeredundencyCombineHandle(outer_output_async, mesh_info, ctx)
914 @staticmethod
915 def combine_wait(handle: AsyncHandle) -> object:
916 """Wait for async deredundency combine and return the final tensor.
918 Args:
919 handle: :class:`AsyncHandle` returned by :meth:`combine_start`.
921 Returns:
922 Token tensor in the original source-rank routed order.
923 """
924 return handle.wait()
927_TOKEN_DISPATCHERS = {
928 "all_to_all": AllToAllTokenDispatcher,
929 "deredundency": DeredundencyTokenDispatcher,
930}
933def _resolve_token_dispatcher(token_dispatcher: str):
934 """Resolve a token dispatcher name to its implementation class."""
935 try:
936 return _TOKEN_DISPATCHERS[token_dispatcher]
937 except KeyError as exc:
938 supported = "', '".join(sorted(_TOKEN_DISPATCHERS))
939 raise ValueError(
940 f"token_dispatcher must be one of '{supported}', got {token_dispatcher!r}."
941 ) from exc
944def _get_flattened_ep_mesh(device_mesh: DeviceMesh) -> DeviceMesh:
945 """Return a 1-D EP mesh, flattening a 2-D deredundency mesh if needed."""
946 if getattr(device_mesh, "ndim", 1) == 1:
947 return device_mesh
948 mesh_dim_names = getattr(device_mesh, "mesh_dim_names", None) or ()
949 if "ep" in mesh_dim_names or "ep" in device_mesh.get_flatten_mapping():
950 return device_mesh["ep"]
951 if set(mesh_dim_names) == {"oep", "iep"}:
952 return device_mesh.flatten("ep")
953 raise ValueError(
954 "Deredundency ExpertParallel expects a 1-D EP mesh or a 2-D "
955 "[oep, iep] mesh when partitioning expert weights."
956 )
959# ---------------------------------------------------------------------------
960# ExpertParallel — standard all-to-all EP
961# ---------------------------------------------------------------------------
963class ExpertParallel(BaseExpertParallel):
964 """Expert Parallel: shard experts across ranks via all-to-all token routing.
966 Applies :meth:`apply` to a :class:`GroupedExperts` module:
968 1. **Partition** — distributes expert weights on dim 0 (``Shard(0)``) so
969 each rank holds ``num_experts // ep_degree`` local experts.
970 2. **Token dispatch** (forward pre-hook) — two-step all-to-all:
971 a. Exchange token counts (non-differentiable).
972 b. Exchange actual tokens (differentiable, gradient flows back).
973 Followed by rank-major → expert-major permutation.
974 3. **Token combine** (forward post-hook) — expert-major → rank-major
975 unpermute, then reverse all-to-all (differentiable).
977 All collectives use ``platform.differentiable_all_to_all_single`` /
978 ``platform.all_to_all_single`` — no direct ``torch.distributed`` calls.
980 The token dispatcher is selectable. ``"all_to_all"`` uses
981 :class:`AllToAllTokenDispatcher`; ``"deredundency"`` uses
982 :class:`DeredundencyTokenDispatcher`.
984 Args:
985 token_dispatcher: Token dispatch strategy. Supported values are
986 ``"all_to_all"`` and ``"deredundency"``.
987 async_combine: When ``True``, the combine all-to-all is launched
988 asynchronously so that the caller (e.g. :class:`MoE`) can
989 overlap it with shared-expert computation. When ``False``
990 (default), combine is fully synchronous — no overlap, identical
991 to the baseline.
993 Example::
994 >>> ep_style = ExpertParallel()
995 >>> sharded_experts = ep_style.apply(experts_module, ep_device_mesh)
996 >>> # With async combine for shared-expert overlap:
997 >>> ep_style = ExpertParallel(async_combine=True)
998 >>> sharded_experts = ep_style.apply(experts_module, ep_device_mesh)
999 """
1001 def __init__(self, token_dispatcher: Union[str, bool] = "all_to_all", async_combine: bool = False) -> None:
1002 """Initialize ExpertParallel.
1004 Args:
1005 token_dispatcher: Token dispatch strategy. Supported values are
1006 ``"all_to_all"`` and ``"deredundency"``.
1007 async_combine: If ``True``, use asynchronous combine all-to-all
1008 to overlap communication with shared-expert computation.
1009 """
1010 if isinstance(token_dispatcher, bool):
1011 async_combine = token_dispatcher
1012 token_dispatcher = "all_to_all"
1013 self._dispatch_ctx: Optional[DispatchContext] = None
1014 self.async_combine = async_combine
1015 self._token_dispatcher_name = token_dispatcher
1016 self._token_dispatcher = _resolve_token_dispatcher(token_dispatcher)
1018 def _token_dispatch(self, module: Module, inputs, device_mesh: DeviceMesh):
1019 """Dispatch tokens to their assigned ranks via all-to-all.
1021 Delegates to the configured token dispatcher and stores the
1022 returned :class:`DispatchContext` on the instance for the matching
1023 :meth:`_token_combine` call.
1025 Args:
1026 module: The ``GroupedExperts`` module.
1027 inputs: Tuple ``(routed_input, num_tokens_per_expert)`` or
1028 ``(routed_input, num_tokens_per_expert, scores)``.
1029 device_mesh: EP device mesh (1-D).
1031 Returns:
1032 Tuple ``(permuted_local_input, local_token_counts)``.
1034 Raises:
1035 ValueError: If ``score_before_experts=False`` (scores passed as
1036 a positional argument) and EP degree > 1. After dispatch,
1037 the token order changes but scores remain in the pre-dispatch
1038 order, causing a silent correctness bug.
1039 """
1040 ep_size = device_mesh.size()
1041 # When EP reorders tokens across ranks, scores (if provided) would
1042 # no longer align with the dispatched token order. The caller must
1043 # use score_before_experts=True so that scores are multiplied in
1044 # before dispatch.
1045 if ep_size > 1 and len(inputs) > 2 and inputs[2] is not None:
1046 raise ValueError(
1047 "ExpertParallel does not support score_before_experts=False "
1048 "when ep_size > 1. After all-to-all dispatch the token order "
1049 "changes but scores remain in the pre-dispatch order, causing "
1050 "incorrect routing weights. Set score_before_experts=True in "
1051 "MoE so that scores are multiplied before dispatch."
1052 )
1054 permuted, local_counts, ctx = (
1055 self._token_dispatcher.dispatch(module, inputs, device_mesh)
1056 )
1057 # Store context in module attribute for _token_combine to read.
1058 # Using module attribute ensures each module has its own context,
1059 # solving the instance sharing problem when the same ExpertParallel
1060 # style object is applied to multiple GroupedExperts modules.
1061 # pylint: disable=W0212
1062 module._ep_dispatch_ctx = ctx
1063 return permuted, local_counts
1065 def _token_combine(self, module: Module, routed_output, device_mesh: DeviceMesh):
1066 """Gather expert outputs back to the originating ranks via all-to-all.
1068 When ``async_combine=True``, launches the combine all-to-all
1069 asynchronously and returns an :class:`AsyncCollectiveTensor`. The
1070 actual device-side wait is deferred until the downstream consumer
1071 (e.g. MoE unpermutation) first reads the tensor, enabling overlap
1072 with shared-expert computation.
1074 When ``async_combine=False`` (default), uses the synchronous
1075 :meth:`AllToAllTokenDispatcher.combine` — identical to the baseline.
1077 Args:
1078 module: The ``GroupedExperts`` module.
1079 routed_output: Expert output tensor in expert-major order.
1080 device_mesh: EP device mesh (1-D).
1082 Returns:
1083 Token tensor in the original token-major layout. When
1084 ``async_combine=True``, this may be an async collective tensor
1085 whose values are not yet materialised.
1087 Raises:
1088 RuntimeError: If dispatch context is not found (dispatch was not called).
1089 """
1090 # Read dispatch context from module attribute set by _token_dispatch.
1091 # pylint: disable=W0212
1092 ctx = getattr(module, "_ep_dispatch_ctx", None)
1093 if ctx is None:
1094 raise RuntimeError(
1095 "_token_combine called but no dispatch context found in module. "
1096 "This indicates _token_dispatch was not called before _token_combine, "
1097 "or the context was already consumed by a previous combine call."
1098 )
1100 # Note: Do NOT delete the context here. In PyTorch, the tensors in ctx
1101 # are captured by autograd graph and don't need the attribute. But in
1102 # MindSpore PyNative mode, deleting the attribute may break backward.
1103 # The context will be overwritten on the next forward call.
1105 if self.async_combine:
1106 handle = self._token_dispatcher.combine_start(
1107 routed_output, device_mesh, ctx
1108 )
1109 # Store on module for external inspection / advanced use cases.
1110 # pylint: disable=W0212
1111 module._ep_combine_handle = handle
1112 # Return the async tensor. The first non-view access by the
1113 # downstream consumer (e.g. MoE unpermutation) will trigger the
1114 # implicit wait, overlapping with shared_expert computation.
1115 return handle.wait()
1117 return self._token_dispatcher.combine(
1118 module, routed_output, device_mesh, ctx,
1119 )
1121 def _partition_mesh(self, device_mesh: DeviceMesh) -> DeviceMesh:
1122 """Return the mesh used to shard expert weights."""
1123 if self._token_dispatcher_name == "deredundency":
1124 return _get_flattened_ep_mesh(device_mesh)
1125 return device_mesh
1127 def _partition_fn(
1128 self, name: str, module: Module, device_mesh: DeviceMesh
1129 ) -> None:
1130 """Shard all expert parameters along dim 0 (expert dimension).
1132 Args:
1133 name: Submodule name (unused).
1134 module: The module whose parameters are being sharded.
1135 device_mesh: EP device mesh.
1136 """
1137 del name
1138 partition_mesh = self._partition_mesh(device_mesh)
1139 for key, param in _distribute_module_iter_params(module):
1140 if param is None:
1141 continue
1142 src = _distribute_module_param_source(param)
1143 requires_grad = bool(getattr(param, "requires_grad", True))
1144 dt = distribute_tensor(src, partition_mesh, [Shard(0)])
1145 new_param = _distribute_module_new_parameter(key, dt, requires_grad)
1146 _distribute_module_set_param(module, key, new_param)
1149# ---------------------------------------------------------------------------
1150# TensorParallel — TP-only weight sharding for experts (no token dispatch)
1151# ---------------------------------------------------------------------------
1152class TensorParallel(ParallelStyle):
1153 """Tensor Parallel for expert weights (no token dispatch).
1155 Shards the ``GroupedExperts`` weight tensors in the column/row-wise
1156 pattern used by standard TP:
1158 - ``w1`` / ``w3``: ``Shard(1)`` — column-wise (hidden_dim dimension).
1159 - ``w2``: ``Shard(2)`` — row-wise (output dim dimension).
1161 Use this when EP degree is 1 and you want TP across experts without
1162 any all-to-all token dispatch. Typically combined with the standard
1163 :class:`~hyper_parallel.core.tensor_parallel.style.ColwiseParallel` /
1164 :class:`~hyper_parallel.core.tensor_parallel.style.RowwiseParallel`
1165 pattern for attention layers.
1167 Example::
1168 >>> tp_style = TensorParallel()
1169 >>> sharded_experts = tp_style.apply(experts_module, tp_device_mesh)
1170 """
1172 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module:
1173 """Apply TP weight sharding to *module*.
1175 Args:
1176 module: A :class:`GroupedExperts` instance.
1177 device_mesh: 1-D TP device mesh (``mesh_dim_names=("tp",)``).
1179 Returns:
1180 The module with TP-sharded expert parameters.
1181 """
1182 return distribute_module(
1183 module,
1184 device_mesh,
1185 self._partition_fn,
1186 )
1188 @staticmethod
1189 def _partition_fn(name: str, module: Module, device_mesh: DeviceMesh) -> None:
1190 """Shard expert weights column-wise (w1/w3) or row-wise (w2).
1192 ``GroupedExperts`` weight layout is ``[num_experts, out_dim, in_dim]``
1193 so:
1195 - ``w1``/``w3``: shard ``Shard(1)`` → split ``hidden_dim``
1196 (column-wise analogue).
1197 - ``w2``: shard ``Shard(2)`` → split ``in_dim = hidden_dim``
1198 (row-wise analogue).
1200 Args:
1201 name: Submodule name (unused).
1202 module: The module whose parameters are being sharded.
1203 device_mesh: TP device mesh.
1204 """
1205 del name
1206 for key, param in _distribute_module_iter_params(module):
1207 if param is None:
1208 continue
1209 src = _distribute_module_param_source(param)
1210 requires_grad = bool(getattr(param, "requires_grad", True))
1211 # w1, w3: column-wise → Shard(1); w2: row-wise → Shard(2).
1212 shard_dim = 2 if key == "w2" else 1
1213 dt = distribute_tensor(src, device_mesh, [Shard(shard_dim)])
1214 new_param = _distribute_module_new_parameter(key, dt, requires_grad)
1215 _distribute_module_set_param(module, key, new_param)
1218# ---------------------------------------------------------------------------
1219# ExpertTensorParallel — combined EP + TP on a 2-D [ep, tp] mesh
1220# ---------------------------------------------------------------------------
1222class ExpertTensorParallel(ExpertParallel):
1223 """Combined Expert + Tensor Parallel on a 2-D ``[ep, tp]`` device mesh.
1225 Extends :class:`ExpertParallel` to operate on a 2-D mesh with named
1226 dimensions ``"ep"`` and ``"tp"``:
1228 - **Partition**: each expert weight ``[num_experts, out, in]`` is doubly
1229 sharded — ``Shard(0)`` along the EP dim (expert ownership) and
1230 ``Shard(1)``/``Shard(2)`` along the TP dim (column-wise / row-wise).
1231 - **Dispatch / Combine**: use only the 1-D ``device_mesh["ep"]`` sub-mesh
1232 so that token routing uses EP-group collectives, not the full 2-D mesh.
1234 Args:
1235 token_dispatcher: Token dispatch strategy. Supported values are
1236 ``"all_to_all"`` and ``"deredundency"``.
1237 async_combine: Forwarded to :class:`ExpertParallel`. When ``True``,
1238 the combine all-to-all is launched asynchronously for
1239 shared-expert overlap.
1241 Example::
1242 >>> etp_style = ExpertTensorParallel()
1243 >>> sharded = etp_style.apply(experts_module, ep_tp_2d_mesh)
1244 """
1246 def __init__(self, token_dispatcher: Union[str, bool] = "all_to_all", async_combine: bool = False) -> None:
1247 """Initialize ExpertTensorParallel.
1249 Args:
1250 async_combine: If ``True``, use asynchronous combine all-to-all.
1251 """
1252 super().__init__(token_dispatcher=token_dispatcher, async_combine=async_combine)
1254 def _dispatch_mesh(self, device_mesh: DeviceMesh) -> DeviceMesh:
1255 """Return the mesh used for token dispatch in ETP."""
1256 if self._token_dispatcher_name == "deredundency":
1257 raise NotImplementedError(
1258 "ExpertTensorParallel does not yet support "
1259 "token_dispatcher='deredundency'. Use ExpertParallel with a "
1260 "[oep, iep] mesh, or add [oep, iep, tp] mesh handling first."
1261 )
1262 return device_mesh["ep"]
1264 def _token_dispatch(self, module: Module, inputs, device_mesh: DeviceMesh):
1265 """Dispatch tokens using only the EP sub-mesh.
1267 Args:
1268 module: The ``GroupedExperts`` module.
1269 inputs: Forward inputs tuple.
1270 device_mesh: 2-D device mesh with dims ``("ep", "tp")``.
1272 Returns:
1273 Transformed inputs for local expert computation.\
1275 Raises:
1276 ValueError: If ``score_before_experts=False`` and EP degree > 1.
1277 """
1278 ep_mesh = device_mesh["ep"]
1279 # Same score_before_experts check as ExpertParallel, but using
1280 # the EP sub-mesh size.
1281 ep_size = ep_mesh.size()
1282 if ep_size > 1 and len(inputs) > 2 and inputs[2] is not None:
1283 raise ValueError(
1284 "ExpertTensorParallel does not support score_before_experts=False "
1285 "when ep_size > 1. After all-to-all dispatch the token order "
1286 "changes but scores remain in the pre-dispatch order, causing "
1287 "incorrect routing weights. Set score_before_experts=True in "
1288 "MoE so that scores are multiplied before dispatch."
1289 )
1291 dispatch_mesh = self._dispatch_mesh(device_mesh)
1292 permuted, local_counts, ctx = (
1293 self._token_dispatcher.dispatch(module, inputs, dispatch_mesh)
1294 )
1295 # pylint: disable=W0212
1296 # Store context in module attribute for _token_combine to read.
1297 module._ep_dispatch_ctx = ctx
1298 return permuted, local_counts
1300 def _token_combine(self, module: Module, routed_output, device_mesh: DeviceMesh):
1301 """Combine tokens using only the EP sub-mesh.
1303 When ``async_combine=True``, launches the combine all-to-all
1304 asynchronously via :meth:`self._token_dispatcher.combine_start`.
1306 Args:
1307 module: The ``GroupedExperts`` module.
1308 routed_output: Expert output tensor in expert-major order.
1309 device_mesh: 2-D device mesh with dims ``("ep", "tp")``.
1311 Returns:
1312 Token tensor in the original token-major layout.
1314 Raises:
1315 RuntimeError: If dispatch context is not found.
1316 """
1317 # pylint: disable=W0212
1318 # Read dispatch context from module attribute set by _token_dispatch.
1319 ctx = getattr(module, "_ep_dispatch_ctx", None)
1320 if ctx is None:
1321 raise RuntimeError(
1322 "_token_combine called but no dispatch context found in module. "
1323 "This indicates _token_dispatch was not called before _token_combine, "
1324 "or the context was already consumed by a previous combine call."
1325 )
1327 # Note: Do NOT delete the context here. In PyTorch, the tensors in ctx
1328 # are captured by autograd graph and don't need the attribute. But in
1329 # MindSpore PyNative mode, deleting the attribute may break backward.
1330 # The context will be overwritten on the next forward call.
1332 dispatch_mesh = self._dispatch_mesh(device_mesh)
1334 if self.async_combine:
1335 handle = self._token_dispatcher.combine_start(
1336 routed_output, dispatch_mesh, ctx
1337 )
1338 # pylint: disable=W0212
1339 module._ep_combine_handle = handle
1340 return handle.wait()
1342 return self._token_dispatcher.combine(
1343 module, routed_output, dispatch_mesh, ctx,
1344 )
1346 def _partition_fn(
1347 self, name: str, module: Module, device_mesh: DeviceMesh
1348 ) -> None:
1349 """Shard expert weights along both EP (dim 0) and TP (dim 1 or 2).
1351 Weight layout ``[num_experts, out_dim, in_dim]``:
1353 - ``w1``/``w3``: ``[Shard(0), Shard(1)]`` — EP shards experts,
1354 TP splits hidden_dim (column-wise).
1355 - ``w2``: ``[Shard(0), Shard(2)]`` — EP shards experts, TP splits
1356 the input dimension (row-wise).
1358 Args:
1359 name: Submodule name (unused).
1360 module: The module whose parameters are being sharded.
1361 device_mesh: 2-D device mesh with dims ``("ep", "tp")``.
1362 """
1363 del name
1364 for key, param in _distribute_module_iter_params(module):
1365 if param is None:
1366 continue
1367 src = _distribute_module_param_source(param)
1368 requires_grad = bool(getattr(param, "requires_grad", True))
1369 # EP shards expert ownership (dim 0); TP shards weight dim.
1370 tp_dim = 2 if key == "w2" else 1
1371 dt = distribute_tensor(src, device_mesh, [Shard(0), Shard(tp_dim)])
1372 new_param = _distribute_module_new_parameter(key, dt, requires_grad)
1373 _distribute_module_set_param(module, key, new_param)