Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / expert_parallel / expert_parallel.py: 86%

357 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-13 05:07 +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. 

16 

17Provides token permutation helpers and four parallel styles that compose with 

18:class:`~hyper_parallel.core.expert_parallel.moe.GroupedExperts`: 

19 

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] 

37 

38from abc import ABC, abstractmethod 

39from dataclasses import dataclass 

40from typing import Any, List, Optional, Tuple, Union 

41 

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 

54 

55platform = get_platform() 

56Module = platform.Module 

57 

58 

59# --------------------------------------------------------------------------- 

60# Token permutation helpers 

61# --------------------------------------------------------------------------- 

62 

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. 

69 

70 After all-to-all, received tokens are laid out in rank-major order:: 

71 

72 [rank0·expert0 tokens | rank0·expert1 tokens | ... | 

73 rank1·expert0 tokens | rank1·expert1 tokens | ...] 

74 

75 Expert computation requires expert-major order:: 

76 

77 [all tokens for local expert 0 | all tokens for local expert 1 | ...] 

78 

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). 

85 

86 Returns: 

87 Tuple of: 

88 

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] 

97 

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] 

101 

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 

108 

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] 

118 

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] 

122 

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 

129 

130 permuted_indices = (block_src_starts + intra).long() 

131 return permuted_indices, num_tokens_per_expert 

132 

133 

134def _permute(x, tokens_per_expert_group, ep_degree: int, num_local_experts: int): 

135 """Apply rank-major → expert-major permutation to routed tokens. 

136 

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. 

145 

146 Returns: 

147 Tuple of: 

148 

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 

165 

166 

167def _unpermute(out, original_shape, permuted_indices): 

168 """Reverse the permutation applied by :func:`_permute`. 

169 

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`. 

175 

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 

189 

190 

191# --------------------------------------------------------------------------- 

192# DispatchContext — state shared between token dispatch and combine 

193# --------------------------------------------------------------------------- 

194 

195@dataclass 

196class DispatchContext: 

197 """Intermediate state between token dispatch and combine. 

198 

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: 

202 

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 

207 

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) 

212 

213 Solution: Store context per-module, not per-style-instance. 

214 

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 """ 

220 

221 input_splits: List[int] 

222 output_splits: List[int] 

223 input_shape: Tuple[int, ...] 

224 permuted_indices: Any 

225 probs_input_shape: Optional[Tuple[int, ...]] = None 

226 

227 

228@dataclass 

229class DeredundencyDispatchContext(DispatchContext): 

230 """State shared by deredundency dispatch and combine. 

231 

232 The inherited split and permutation fields describe the inner-EP 

233 all-to-all. The extra fields describe the OEP shared token view and the 

234 whiteboard scatter used before the final reduce-scatter combine. 

235 """ 

236 

237 dispatch_indices: Optional[object] = None 

238 router_coeff: Optional[object] = None 

239 gathered_shape: Optional[tuple] = None 

240 oep_size: int = 1 

241 

242 

243 

244@dataclass(frozen=True) 

245class _DeredundencyMeshInfo: 

246 """Resolved mesh metadata for two-stage deredundency token exchange.""" 

247 

248 oep_group: object 

249 iep_group: object 

250 oep_size: int 

251 iep_size: int 

252 outer_rank: int 

253 inner_rank: int 

254 

255 

256def _get_deredundency_mesh_info(device_mesh: DeviceMesh) -> _DeredundencyMeshInfo: 

257 """Resolve ``oep`` / ``iep`` groups from a 1-D or 2-D EP mesh.""" 

258 ndim = getattr(device_mesh, "ndim", 1) 

259 if not isinstance(ndim, int): 

260 ndim = 1 

261 if ndim == 1: 

262 return _DeredundencyMeshInfo( 

263 oep_group=None, 

264 iep_group=device_mesh.get_group(), 

265 oep_size=1, 

266 iep_size=device_mesh.size(), 

267 outer_rank=0, 

268 inner_rank=device_mesh.get_local_rank(), 

269 ) 

270 if ndim != 2: 

271 raise ValueError( 

272 "DeredundencyTokenDispatcher expects a 1-D EP mesh or a 2-D " 

273 f"[oep, iep] EP mesh, but got ndim={ndim}." 

274 ) 

275 

276 mesh_dim_names = getattr(device_mesh, "mesh_dim_names", None) or () 

277 oep_dim = mesh_dim_names.index("oep") if "oep" in mesh_dim_names else 0 

278 iep_dim = mesh_dim_names.index("iep") if "iep" in mesh_dim_names else 1 

279 if oep_dim == iep_dim: 

280 raise ValueError("DeredundencyTokenDispatcher requires distinct oep and iep mesh dimensions.") 

281 

282 return _DeredundencyMeshInfo( 

283 oep_group=device_mesh.get_group(oep_dim), 

284 iep_group=device_mesh.get_group(iep_dim), 

285 oep_size=device_mesh.size(oep_dim), 

286 iep_size=device_mesh.size(iep_dim), 

287 outer_rank=device_mesh.get_local_rank(oep_dim), 

288 inner_rank=device_mesh.get_local_rank(iep_dim), 

289 ) 

290 

291 

292def _generate_deredundency_dispatch_indices( 

293 tokens_per_expert_by_source, 

294 expert_start: int, 

295 iep_size: int, 

296 num_local_experts: int, 

297): 

298 """Generate gather-view indices ordered by IEP destination rank. 

299 

300 ``tokens_per_expert_by_source`` is shaped ``[oep_size, num_experts]`` and 

301 describes each source rank's expert-major routed buffer after the OEP 

302 all-gather. The returned indices select the current outer expert range 

303 and order it as ``[iep_dst, local_expert, oep_source]`` so each IEP 

304 destination chunk keeps local-expert blocks contiguous for the later 

305 rank-major → expert-major permutation. 

306 """ 

307 oep_size = tokens_per_expert_by_source.shape[0] 

308 experts_per_outer = iep_size * num_local_experts 

309 expert_end = expert_start + experts_per_outer 

310 

311 source_totals = tokens_per_expert_by_source.sum(dim=1) 

312 source_offsets = source_totals.cumsum(0) - source_totals 

313 expert_offsets = ( 

314 tokens_per_expert_by_source.cumsum(dim=1) 

315 - tokens_per_expert_by_source 

316 + source_offsets.view(oep_size, 1) 

317 ) 

318 

319 selected_counts = tokens_per_expert_by_source[:, expert_start:expert_end].view( 

320 oep_size, iep_size, num_local_experts, 

321 ) 

322 selected_offsets = expert_offsets[:, expert_start:expert_end].view( 

323 oep_size, iep_size, num_local_experts, 

324 ) 

325 counts_by_destination = selected_counts.permute(1, 2, 0).contiguous() 

326 offsets_by_destination = selected_offsets.permute(1, 2, 0).contiguous() 

327 

328 block_counts = counts_by_destination.view(-1) 

329 token_counts_by_destination_expert = selected_counts.sum(dim=0).contiguous().view(-1) 

330 total = int(block_counts.sum()) 

331 if total == 0: 

332 return block_counts.new_zeros(0, dtype=block_counts.dtype).long(), token_counts_by_destination_expert 

333 

334 block_starts = offsets_by_destination.view(-1).repeat_interleave(block_counts) 

335 block_offsets = block_counts.cumsum(0) - block_counts 

336 block_offsets_per_token = block_offsets.repeat_interleave(block_counts) 

337 intra = platform.arange(0, total, device=tokens_per_expert_by_source.device) - block_offsets_per_token 

338 

339 return (block_starts + intra).long(), token_counts_by_destination_expert 

340 

341 

342def _scale_by_router_coeff(tokens, router_coeff): 

343 """Scale routed expert outputs by optional router coefficients.""" 

344 if router_coeff is None: 

345 return tokens 

346 if router_coeff.shape[0] != tokens.shape[0]: 

347 raise ValueError( 

348 "router_coeff length must match routed token count, got " 

349 f"{router_coeff.shape[0]} and {tokens.shape[0]}." 

350 ) 

351 coeff = router_coeff 

352 if len(coeff.shape) == 1 and len(tokens.shape) > 1: 

353 coeff = coeff.reshape((-1,) + (1,) * (len(tokens.shape) - 1)) 

354 return tokens * coeff 

355 

356 

357def _scatter_add_first_dim(src, indices, output_shape): 

358 """Scatter-add rows of ``src`` into a zero tensor along dim 0.""" 

359 result = src.new_zeros(*output_shape) 

360 if len(src.shape) == 1: 

361 scatter_indices = indices 

362 else: 

363 scatter_indices = indices.reshape((-1,) + (1,) * (len(src.shape) - 1)).expand( 

364 -1, *src.shape[1:], 

365 ) 

366 if hasattr(result, "scatter_add"): 

367 return result.scatter_add(0, scatter_indices, src) 

368 if hasattr(result, "index_add"): 

369 return result.index_add(0, indices, src) 

370 raise RuntimeError( 

371 "DeredundencyTokenDispatcher.combine requires tensor scatter_add or " 

372 "index_add support for exdispatch_idx accumulation." 

373 ) 

374 

375 

376class _DeredundencyCombineHandle(AsyncHandle): 

377 """Async handle that finishes deredundency combine post-processing.""" 

378 

379 def __init__( 

380 self, 

381 async_tensor: object, 

382 mesh_info: _DeredundencyMeshInfo, 

383 ctx: DeredundencyDispatchContext, 

384 ) -> None: 

385 super().__init__(async_tensor) 

386 self._mesh_info = mesh_info 

387 self._ctx = ctx 

388 self._combined: Optional[object] = None 

389 

390 def wait(self) -> object: 

391 """Wait for IEP a2a, then finish OEP scatter/reduce combine once.""" 

392 if self._combined is None: 

393 outer_output = super().wait() 

394 weighted_output = _scale_by_router_coeff(outer_output, self._ctx.router_coeff) 

395 combine_whiteboard = _scatter_add_first_dim( 

396 weighted_output, 

397 self._ctx.dispatch_indices, 

398 self._ctx.gathered_shape, 

399 ) 

400 if self._ctx.oep_size == 1: 

401 self._combined = combine_whiteboard 

402 else: 

403 self._combined = platform.differentiable_reduce_scatter( 

404 combine_whiteboard, 

405 self._ctx.oep_size, 

406 0, 

407 "sum", 

408 self._mesh_info.oep_group, 

409 ) 

410 return self._combined 

411 

412 

413# --------------------------------------------------------------------------- 

414# BaseExpertParallel — abstract base for all-to-all EP strategies 

415# --------------------------------------------------------------------------- 

416 

417class BaseExpertParallel(ParallelStyle, ABC): 

418 """Abstract base class for Expert Parallel strategies with token dispatch. 

419 

420 Subclasses implement :meth:`_partition_fn`, :meth:`_token_dispatch`, and 

421 :meth:`_token_combine`; this class wires them into :func:`distribute_module`. 

422 """ 

423 

424 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module: 

425 """Apply EP sharding and dispatch/combine hooks to *module*. 

426 

427 Args: 

428 module: A :class:`~hyper_parallel.core.expert_parallel.moe.GroupedExperts` 

429 instance to shard. 

430 device_mesh: Device mesh for this EP strategy. 

431 

432 Returns: 

433 The module with distributed parameters and dispatch/combine hooks. 

434 """ 

435 return distribute_module( 

436 module, 

437 device_mesh, 

438 self._partition_fn, 

439 self._token_dispatch, 

440 self._token_combine, 

441 ) 

442 

443 @abstractmethod 

444 def _partition_fn( 

445 self, name: str, module: Module, device_mesh: DeviceMesh 

446 ) -> None: 

447 """Shard module parameters according to this strategy. 

448 

449 Args: 

450 name: Submodule name. 

451 module: The module whose parameters are being sharded. 

452 device_mesh: Device mesh for this EP strategy. 

453 """ 

454 

455 @abstractmethod 

456 def _token_dispatch(self, module: Module, inputs, device_mesh: DeviceMesh): 

457 """Pre-hook: route input tokens to their assigned ranks. 

458 

459 Args: 

460 module: The ``GroupedExperts`` module. 

461 inputs: Forward inputs tuple. 

462 device_mesh: Device mesh for this EP strategy. 

463 

464 Returns: 

465 Transformed inputs for local expert computation. 

466 """ 

467 

468 @abstractmethod 

469 def _token_combine(self, module: Module, routed_output, device_mesh: DeviceMesh): 

470 """Post-hook: gather expert outputs back to the originating ranks. 

471 

472 Args: 

473 module: The ``GroupedExperts`` module. 

474 routed_output: Expert output tensor in expert-major order. 

475 device_mesh: Device mesh for this EP strategy. 

476 

477 Returns: 

478 Token tensor in the original token-major layout. 

479 """ 

480 

481 

482# --------------------------------------------------------------------------- 

483# AllToAllTokenDispatcher — token dispatch/combine via all-to-all 

484# --------------------------------------------------------------------------- 

485 

486class AllToAllTokenDispatcher: 

487 """Token dispatch and combine via all-to-all for expert parallelism. 

488 

489 Provides :meth:`dispatch` and :meth:`combine` as static methods that 

490 receive and return a :class:`DispatchContext` object. This decouples 

491 the all-to-all token routing logic from the parallel style class so 

492 that it can be reused or tested independently. 

493 

494 Callers (e.g. :class:`ExpertParallel`) are responsible for storing the 

495 context between the paired dispatch/combine calls. 

496 """ 

497 

498 @staticmethod 

499 def dispatch(module: Module, inputs: tuple, device_mesh: DeviceMesh) -> tuple: 

500 """Dispatch tokens to their assigned ranks via all-to-all. 

501 

502 Called as an ``input_fn`` hook by :func:`distribute_module`. Receives 

503 the module's forward inputs and returns transformed inputs. 

504 

505 Args: 

506 module: The ``GroupedExperts`` module. 

507 inputs: Tuple ``(routed_input, num_tokens_per_expert)`` or 

508 ``(routed_input, num_tokens_per_expert, permuted_probs)`` where 

509 ``routed_input`` has shape ``[total_tokens, dim]``, and 

510 ``num_tokens_per_expert`` has shape ``[num_experts]``, and 

511 ``permuted_probs`` has shape ``[total_tokens]``. 

512 device_mesh: EP device mesh (1-D). 

513 

514 Returns: 

515 Tuple ``(permuted_local_input, local_token_counts, ctx)`` or  

516 ``(permuted_local_input, local_token_counts, permuted_probs, ctx)`` 

517 depending on whether *permuted_probs* was provided -  

518 the first two elements are the transformed inputs for local 

519 expert computation; *ctx* is a :class:`DispatchContext` 

520 carrying the updated state to be stored by the caller. 

521 """ 

522 del module # module unused, kept for API consistency 

523 routed_input, num_tokens_per_expert = inputs[0], inputs[1] 

524 permuted_probs = inputs[2] if len(inputs) > 2 else None 

525 ep_group = device_mesh.get_group() 

526 ep_size = device_mesh.size() 

527 num_local_experts = num_tokens_per_expert.shape[0] // ep_size 

528 

529 # --- Step 1: exchange token counts (no gradient needed) --- 

530 # Each rank needs to know how many tokens it will receive from every 

531 # other rank (for each local expert). Uses ``async_op=True`` + an 

532 # explicit ``handle.wait()`` rather than ``async_op=False`` because 

533 # the implicit cross-stream sync is NCCL-only; on HCCL the compute 

534 # stream may read ``counts_out`` before the collective write is 

535 # visible, producing garbage values that blow up the downstream 

536 # ``torch.empty(sum(output_splits), ...)`` allocation. 

537 counts_out, handle = platform.all_to_all_single( 

538 num_tokens_per_expert, 

539 output_shape=[num_tokens_per_expert.shape[0]], 

540 group=ep_group, 

541 async_op=True, 

542 ) 

543 if handle is not None: 

544 handle.wait() 

545 # counts_out shape: [ep_size * num_local_experts] 

546 # counts_out[r * num_local_experts + e] = tokens from rank r for expert e 

547 

548 # --- Step 2: compute input / output splits --- 

549 # input_splits[r] = tokens this rank sends to rank r 

550 # output_splits[r] = tokens this rank receives from rank r 

551 # Reshape to [ep_size, num_local_experts] and sum per rank on device; 

552 # a single ``tolist()`` drains the rank-sum vector to host, replacing 

553 # ``2 * ep_size`` scalar ``int()`` D2H syncs with 2. 

554 input_splits = num_tokens_per_expert.view(ep_size, num_local_experts).sum(dim=1).tolist() 

555 output_splits = counts_out.view(ep_size, num_local_experts).sum(dim=1).tolist() 

556 

557 # --- Step 3a: exchange actual tokens (differentiable) --- 

558 dispatched = platform.differentiable_all_to_all_single( 

559 routed_input, input_splits, output_splits, group=ep_group, 

560 ) 

561 

562 # --- Step 4a: rank-major → expert-major permutation --- 

563 input_shape, permuted, permuted_indices, local_counts = _permute( 

564 dispatched, counts_out, ep_size, num_local_experts 

565 ) 

566 

567 # Steps 3b + 4b: exchange and reorder permuted_probs (when provided) 

568 # alongside the tokens, keeping weights aligned with the expert-major 

569 # token order. Extracted so dispatch stays under the Lizard NLOC cap. 

570 probs_input_shape, permuted_probs_reordered = ( 

571 AllToAllTokenDispatcher._exchange_and_reorder_probs( 

572 permuted_probs, input_splits, output_splits, 

573 counts_out, ep_size, num_local_experts, ep_group, 

574 ) 

575 ) 

576 

577 # Build dispatch context for combine step. 

578 # Caller (e.g., ExpertParallel._token_dispatch) is responsible for storing 

579 # this context and passing it to combine(). This decouples dispatch/combine 

580 # from module state and solves the instance sharing problem. 

581 ctx = DispatchContext( 

582 input_splits=input_splits, 

583 output_splits=output_splits, 

584 input_shape=input_shape, 

585 permuted_indices=permuted_indices, 

586 probs_input_shape=probs_input_shape, 

587 ) 

588 

589 if permuted_probs is not None: 

590 return permuted, local_counts, permuted_probs_reordered, ctx 

591 return permuted, local_counts, ctx 

592 

593 @staticmethod 

594 def _exchange_and_reorder_probs(permuted_probs, input_splits, output_splits, 

595 counts_out, ep_size, num_local_experts, ep_group): 

596 """Exchange ``permuted_probs`` via a2a and reorder to expert-major. 

597 

598 Returns ``(probs_input_shape, permuted_probs_reordered)``; both are 

599 ``None`` when *permuted_probs* is ``None`` (no-op, no second a2a). 

600 """ 

601 if permuted_probs is None: 

602 return None, None 

603 # --- Step 3b: exchange permuted_probs via all-to-all (differentiable) --- 

604 dispatched_probs = platform.differentiable_all_to_all_single( 

605 permuted_probs, input_splits, output_splits, group=ep_group, 

606 ) 

607 # --- Step 4b: rank-major → expert-major permutation for probs --- 

608 probs_input_shape, permuted_probs_reordered, _, _ = _permute( 

609 dispatched_probs, counts_out, ep_size, num_local_experts 

610 ) 

611 return probs_input_shape, permuted_probs_reordered 

612 

613 @staticmethod 

614 def combine(module: Module, routed_output: object, device_mesh: DeviceMesh, ctx: DispatchContext) -> object: 

615 """Gather expert outputs back to the originating ranks via all-to-all. 

616 

617 Called as an ``output_fn`` hook by :func:`distribute_module`. 

618 Receives dispatch context from the caller (previously returned by dispatch). 

619 

620 Args: 

621 module: The ``GroupedExperts`` module (unused, for API consistency). 

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`. 

627 

628 Returns: 

629 Token tensor in the original token-major layout, 

630 shape ``[sum(input_splits), dim]``. 

631 """ 

632 del module # module not used, kept for API consistency 

633 ep_group = device_mesh.get_group() 

634 

635 # expert-major → rank-major 

636 unpermuted = _unpermute(routed_output, ctx.input_shape, ctx.permuted_indices) 

637 

638 # reverse all-to-all (output/input splits are swapped) 

639 combined = platform.differentiable_all_to_all_single( 

640 unpermuted, 

641 ctx.output_splits, # was output, now becomes input 

642 ctx.input_splits, # was input, now becomes output 

643 group=ep_group, 

644 ) 

645 return combined 

646 

647 @staticmethod 

648 def combine_start(routed_output, device_mesh, ctx): 

649 """Launch async combine all-to-all without waiting for completion. 

650 

651 Splits the combine into two phases so that the caller can overlap 

652 the a2a communication with independent computation (e.g. a shared 

653 expert forward pass). The caller must later call 

654 :meth:`combine_wait` or ``handle.wait()`` to obtain the final 

655 result. 

656 

657 Step 1 (synchronous, local): expert-major → rank-major unpermute. 

658 Step 2 (asynchronous, cross-rank): reverse all-to-all. 

659 

660 Args: 

661 routed_output: Expert output tensor in expert-major order, 

662 shape ``[sum(local_counts), dim]``. 

663 device_mesh: EP device mesh (1-D). 

664 ctx: :class:`DispatchContext` previously returned by 

665 :meth:`dispatch`. 

666 

667 Returns: 

668 :class:`AsyncHandle` carrying the state needed by 

669 :meth:`combine_wait`. 

670 """ 

671 ep_group = device_mesh.get_group() 

672 

673 # expert-major → rank-major (local, no communication) 

674 unpermuted = _unpermute(routed_output, ctx.input_shape, ctx.permuted_indices) 

675 

676 # async reverse all-to-all (output/input splits are swapped) 

677 combined_async = platform.differentiable_all_to_all_single_async( 

678 unpermuted, 

679 ctx.output_splits, 

680 ctx.input_splits, 

681 group=ep_group, 

682 ) 

683 

684 return AsyncHandle(combined_async) 

685 

686 @staticmethod 

687 def combine_wait(handle): 

688 """Wait for the async combine all-to-all to complete. 

689 

690 Args: 

691 handle: :class:`AsyncHandle` returned by :meth:`combine_start`. 

692 

693 Returns: 

694 Combined tensor in the original token-major layout. 

695 """ 

696 return handle.wait() 

697 

698 

699# --------------------------------------------------------------------------- 

700# DeredundencyTokenDispatcher — token dispatch via OEP all-gather + IEP all-to-all 

701# --------------------------------------------------------------------------- 

702 

703class DeredundencyTokenDispatcher: 

704 """Token dispatch/combine via OEP all-gather plus IEP all-to-all. 

705 

706 This dispatcher keeps the same public contract as 

707 :class:`AllToAllTokenDispatcher`, but decomposes the global EP all-to-all 

708 into the deredundency flow described in 

709 ``docs/moe_alltoall_deredundency_token_permutation.md``: 

710 

711 1. Form a shared token/count view across the OEP group. 

712 2. Select only the current outer expert range. 

713 3. Send selected tokens to concrete local-expert ranks inside the IEP 

714 group. 

715 4. Sort received tokens into local expert-major order. 

716 

717 For a 2-D mesh, dimension ``"oep"`` / ``0`` is the outer group and 

718 ``"iep"`` / ``1`` is the inner group. A 1-D mesh is treated as 

719 ``oep_size == 1`` and degenerates to the standard all-to-all data flow. 

720 """ 

721 

722 @staticmethod 

723 def _oep_gather_for_dispatch( 

724 num_tokens_per_expert, 

725 routed_input, 

726 router_coeff, 

727 mesh_info: _DeredundencyMeshInfo, 

728 ) -> tuple: 

729 """All-gather token counts and routed input across the OEP group. 

730 

731 Args: 

732 num_tokens_per_expert: Token count per expert ``[num_experts]``. 

733 routed_input: Routed token tensor ``[total_tokens, dim]``. 

734 router_coeff: Optional router coefficients ``[total_tokens]``. 

735 mesh_info: Resolved OEP/IEP mesh descriptor. 

736 

737 Returns: 

738 Tuple ``(gathered_counts, gathered_routed, gathered_router_coeff)`` 

739 where ``gathered_counts`` has shape ``[oep_size, num_experts]``, 

740 ``gathered_routed`` has shape ``[oep_size * total_tokens, dim]``, 

741 and ``gathered_router_coeff`` is the gathered coefficients or None. 

742 

743 Raises: 

744 ValueError: If routed token counts differ across OEP ranks. 

745 """ 

746 if mesh_info.oep_size == 1: 

747 gathered_counts = num_tokens_per_expert.view(1, num_tokens_per_expert.shape[0]) 

748 return gathered_counts, routed_input, router_coeff 

749 

750 gathered_counts, handle = platform.all_gather_single( 

751 num_tokens_per_expert, 

752 output_shape=[mesh_info.oep_size * num_tokens_per_expert.shape[0]], 

753 group=mesh_info.oep_group, 

754 async_op=True, 

755 ) 

756 if handle is not None: 

757 handle.wait() 

758 gathered_counts = gathered_counts.view(mesh_info.oep_size, num_tokens_per_expert.shape[0]) 

759 source_token_totals = gathered_counts.sum(dim=1).tolist() 

760 if any(total != routed_input.shape[0] for total in source_token_totals): 

761 raise ValueError( 

762 "DeredundencyTokenDispatcher requires equal routed token " 

763 "counts within each OEP group because the shared token view " 

764 f"uses all-gather, got totals {source_token_totals}." 

765 ) 

766 gathered_routed = platform.differentiable_all_gather_concat( 

767 routed_input, mesh_info.oep_group, mesh_info.oep_size, 0, 

768 ) 

769 if router_coeff is None: 

770 gathered_router_coeff = None 

771 else: 

772 gathered_router_coeff = platform.differentiable_all_gather_concat( 

773 router_coeff, mesh_info.oep_group, mesh_info.oep_size, 0, 

774 ) 

775 return gathered_counts, gathered_routed, gathered_router_coeff 

776 

777 @staticmethod 

778 def dispatch(module: Module, inputs: tuple, device_mesh: DeviceMesh) -> tuple: 

779 """Dispatch tokens using OEP all-gather and IEP all-to-all. 

780 

781 Args: 

782 module: The ``GroupedExperts`` module (unused here). 

783 inputs: Tuple ``(routed_input, num_tokens_per_expert)`` or 

784 ``(routed_input, num_tokens_per_expert, router_coeff)`` where 

785 ``routed_input`` has shape ``[total_tokens, dim]``, 

786 ``num_tokens_per_expert`` has shape ``[num_experts]``, and 

787 ``router_coeff`` is an optional 1-D tensor of shape 

788 ``[total_tokens]``. 

789 device_mesh: 1-D EP mesh or 2-D ``[oep, iep]`` EP mesh. 

790 

791 Returns: 

792 Tuple ``(permuted_local_input, local_token_counts, ctx)`` with the 

793 same meaning as :meth:`AllToAllTokenDispatcher.dispatch`. Unlike 

794 :meth:`AllToAllTokenDispatcher.dispatch`, this always returns a 

795 3-tuple — ``router_coeff`` (when provided) is consumed by the 

796 combine step, not returned for in-expert weighting. 

797 

798 Raises: 

799 ValueError: If the expert count is not divisible by the full EP 

800 size represented by the deredundency mesh. 

801 

802 Note: 

803 The third input is **router_coeff**, *not* ``permuted_probs``. 

804 Both occupy ``inputs[2]`` with shape ``[total_tokens]`` but differ: 

805 ``permuted_probs`` (:class:`AllToAllTokenDispatcher`) is exchanged 

806 with tokens and applied **inside** experts (pre-w2 activation, 

807 i.e. ``score_before_experts=False``); ``router_coeff`` (here) is 

808 OEP-gathered, scattered with tokens, and applied in 

809 :meth:`combine` to the expert **final output**. The w2/SiLU 

810 nonlinearity makes the two mathematically distinct, so 

811 deredundency does **not** support ``score_before_experts=False`` 

812 — passing scores as ``inputs[2]`` would be silently treated as 

813 ``router_coeff``. Use ``score_before_experts=True`` until 

814 deredundency's weighting is redesigned. 

815 """ 

816 del module 

817 routed_input, num_tokens_per_expert = inputs[0], inputs[1] 

818 router_coeff = inputs[2] if len(inputs) > 2 else None 

819 if router_coeff is not None and router_coeff.shape[0] != routed_input.shape[0]: 

820 raise ValueError( 

821 "router_coeff length must match routed_input token count, got " 

822 f"{router_coeff.shape[0]} and {routed_input.shape[0]}." 

823 ) 

824 mesh_info = _get_deredundency_mesh_info(device_mesh) 

825 ep_size = mesh_info.oep_size * mesh_info.iep_size 

826 if num_tokens_per_expert.shape[0] % ep_size != 0: 

827 raise ValueError( 

828 "num_tokens_per_expert length must be divisible by the full " 

829 f"EP size {ep_size}, got {num_tokens_per_expert.shape[0]}." 

830 ) 

831 num_local_experts = num_tokens_per_expert.shape[0] // ep_size 

832 experts_per_outer = mesh_info.iep_size * num_local_experts 

833 expert_start = mesh_info.outer_rank * experts_per_outer 

834 

835 gathered_counts, gathered_routed, gathered_router_coeff = ( 

836 DeredundencyTokenDispatcher._oep_gather_for_dispatch( 

837 num_tokens_per_expert, routed_input, router_coeff, mesh_info, 

838 ) 

839 ) 

840 

841 dispatch_indices, node_counts_per_expert = _generate_deredundency_dispatch_indices( 

842 gathered_counts, 

843 expert_start, 

844 mesh_info.iep_size, 

845 num_local_experts, 

846 ) 

847 iep_input_splits = node_counts_per_expert.view( 

848 mesh_info.iep_size, num_local_experts).sum(dim=1).tolist() 

849 iep_counts_out, iep_output_splits = ( 

850 DeredundencyTokenDispatcher._iep_exchange_counts( 

851 node_counts_per_expert, mesh_info, num_local_experts, 

852 ) 

853 ) 

854 outer_routed_input = gathered_routed[dispatch_indices] 

855 outer_router_coeff = ( 

856 None if gathered_router_coeff is None else gathered_router_coeff[dispatch_indices] 

857 ) 

858 dispatched = platform.differentiable_all_to_all_single( 

859 outer_routed_input, iep_input_splits, iep_output_splits, 

860 group=mesh_info.iep_group, 

861 ) 

862 input_shape, permuted, permuted_indices, local_counts = _permute( 

863 dispatched, iep_counts_out, mesh_info.iep_size, num_local_experts, 

864 ) 

865 ctx = DeredundencyDispatchContext( 

866 input_splits=iep_input_splits, 

867 output_splits=iep_output_splits, 

868 input_shape=input_shape, 

869 permuted_indices=permuted_indices, 

870 dispatch_indices=dispatch_indices, 

871 router_coeff=outer_router_coeff, 

872 gathered_shape=gathered_routed.shape, 

873 oep_size=mesh_info.oep_size, 

874 ) 

875 return permuted, local_counts, ctx 

876 

877 @staticmethod 

878 def _iep_exchange_counts(node_counts_per_expert, mesh_info, num_local_experts): 

879 """IEP all-to-all of per-expert counts; return (counts_out, output_splits).""" 

880 iep_counts_out, handle = platform.all_to_all_single( 

881 node_counts_per_expert, 

882 output_shape=[node_counts_per_expert.shape[0]], 

883 group=mesh_info.iep_group, async_op=True, 

884 ) 

885 if handle is not None: 

886 handle.wait() 

887 iep_output_splits = iep_counts_out.view( 

888 mesh_info.iep_size, num_local_experts).sum(dim=1).tolist() 

889 return iep_counts_out, iep_output_splits 

890 

891 @staticmethod 

892 def combine(module: Module, routed_output: object, device_mesh: DeviceMesh, 

893 ctx: DeredundencyDispatchContext) -> object: 

894 """Gather expert outputs back to the originating ranks. 

895 

896 Args: 

897 module: The ``GroupedExperts`` module (unused). 

898 routed_output: Expert output tensor in expert-major order. 

899 device_mesh: 1-D EP mesh or 2-D ``[oep, iep]`` EP mesh. 

900 ctx: Context returned by :meth:`dispatch`. 

901 

902 Returns: 

903 Token tensor in the original source-rank routed order. 

904 """ 

905 del module 

906 mesh_info = _get_deredundency_mesh_info(device_mesh) 

907 DeredundencyTokenDispatcher._validate_combine_mesh(mesh_info, ctx) 

908 

909 unpermuted = _unpermute(routed_output, ctx.input_shape, ctx.permuted_indices) 

910 outer_output = platform.differentiable_all_to_all_single( 

911 unpermuted, 

912 ctx.output_splits, 

913 ctx.input_splits, 

914 group=mesh_info.iep_group, 

915 ) 

916 

917 weighted_output = _scale_by_router_coeff(outer_output, ctx.router_coeff) 

918 combine_whiteboard = _scatter_add_first_dim( 

919 weighted_output, ctx.dispatch_indices, ctx.gathered_shape, 

920 ) 

921 if ctx.oep_size == 1: 

922 return combine_whiteboard 

923 

924 return platform.differentiable_reduce_scatter( 

925 combine_whiteboard, 

926 ctx.oep_size, 

927 0, 

928 "sum", 

929 mesh_info.oep_group, 

930 ) 

931 

932 @staticmethod 

933 def _validate_combine_mesh( 

934 mesh_info: _DeredundencyMeshInfo, 

935 ctx: DeredundencyDispatchContext, 

936 ) -> None: 

937 """Validate that dispatch context and combine mesh are compatible.""" 

938 if mesh_info.oep_size != ctx.oep_size: 

939 raise ValueError( 

940 "DeredundencyTokenDispatcher.combine received a context for " 

941 f"oep_size={ctx.oep_size}, but the mesh resolves to oep_size={mesh_info.oep_size}." 

942 ) 

943 

944 @staticmethod 

945 def combine_start( 

946 routed_output: object, 

947 device_mesh: DeviceMesh, 

948 ctx: DeredundencyDispatchContext, 

949 ) -> AsyncHandle: 

950 """Launch async IEP combine all-to-all and defer deredundency post-processing. 

951 

952 The local expert-major → rank-major unpermute is performed 

953 synchronously. The reverse IEP all-to-all is launched asynchronously, 

954 and :meth:`combine_wait` finishes router weighting, whiteboard 

955 scatter-add, and optional OEP reduce-scatter after the async output is 

956 materialised. 

957 

958 Args: 

959 routed_output: Expert output tensor in expert-major order. 

960 device_mesh: 1-D EP mesh or 2-D ``[oep, iep]`` EP mesh. 

961 ctx: Context returned by :meth:`dispatch`. 

962 

963 Returns: 

964 :class:`AsyncHandle` carrying the pending IEP a2a and deredundency 

965 combine state. 

966 """ 

967 mesh_info = _get_deredundency_mesh_info(device_mesh) 

968 DeredundencyTokenDispatcher._validate_combine_mesh(mesh_info, ctx) 

969 

970 unpermuted = _unpermute(routed_output, ctx.input_shape, ctx.permuted_indices) 

971 outer_output_async = platform.differentiable_all_to_all_single_async( 

972 unpermuted, 

973 ctx.output_splits, 

974 ctx.input_splits, 

975 group=mesh_info.iep_group, 

976 ) 

977 return _DeredundencyCombineHandle(outer_output_async, mesh_info, ctx) 

978 

979 @staticmethod 

980 def combine_wait(handle: AsyncHandle) -> object: 

981 """Wait for async deredundency combine and return the final tensor. 

982 

983 Args: 

984 handle: :class:`AsyncHandle` returned by :meth:`combine_start`. 

985 

986 Returns: 

987 Token tensor in the original source-rank routed order. 

988 """ 

989 return handle.wait() 

990 

991 

992_TOKEN_DISPATCHERS = { 

993 "all_to_all": AllToAllTokenDispatcher, 

994 "deredundency": DeredundencyTokenDispatcher, 

995} 

996 

997 

998def _resolve_token_dispatcher(token_dispatcher: str): 

999 """Resolve a token dispatcher name to its implementation class.""" 

1000 try: 

1001 return _TOKEN_DISPATCHERS[token_dispatcher] 

1002 except KeyError as exc: 

1003 supported = "', '".join(sorted(_TOKEN_DISPATCHERS)) 

1004 raise ValueError( 

1005 f"token_dispatcher must be one of '{supported}', got {token_dispatcher!r}." 

1006 ) from exc 

1007 

1008 

1009def _get_flattened_ep_mesh(device_mesh: DeviceMesh) -> DeviceMesh: 

1010 """Return a 1-D EP mesh, flattening a 2-D deredundency mesh if needed.""" 

1011 if getattr(device_mesh, "ndim", 1) == 1: 

1012 return device_mesh 

1013 mesh_dim_names = getattr(device_mesh, "mesh_dim_names", None) or () 

1014 if "ep" in mesh_dim_names or "ep" in device_mesh.get_flatten_mapping(): 

1015 return device_mesh["ep"] 

1016 if set(mesh_dim_names) == {"oep", "iep"}: 

1017 return device_mesh.flatten("ep") 

1018 raise ValueError( 

1019 "Deredundency ExpertParallel expects a 1-D EP mesh or a 2-D " 

1020 "[oep, iep] mesh when partitioning expert weights." 

1021 ) 

1022 

1023 

1024# --------------------------------------------------------------------------- 

1025# ExpertParallel — standard all-to-all EP 

1026# --------------------------------------------------------------------------- 

1027 

1028class ExpertParallel(BaseExpertParallel): 

1029 """Expert Parallel: shard experts across ranks via all-to-all token routing. 

1030 

1031 Applies :meth:`apply` to a :class:`GroupedExperts` module: 

1032 

1033 1. **Partition** — distributes expert weights on dim 0 (``Shard(0)``) so 

1034 each rank holds ``num_experts // ep_degree`` local experts. 

1035 2. **Token dispatch** (forward pre-hook) — two-step all-to-all: 

1036 a. Exchange token counts (non-differentiable). 

1037 b. Exchange actual tokens (differentiable, gradient flows back). 

1038 Followed by rank-major → expert-major permutation. 

1039 3. **Token combine** (forward post-hook) — expert-major → rank-major 

1040 unpermute, then reverse all-to-all (differentiable). 

1041 

1042 All collectives use ``platform.differentiable_all_to_all_single`` / 

1043 ``platform.all_to_all_single`` — no direct ``torch.distributed`` calls. 

1044 

1045 The token dispatcher is selectable. ``"all_to_all"`` uses 

1046 :class:`AllToAllTokenDispatcher`; ``"deredundency"`` uses 

1047 :class:`DeredundencyTokenDispatcher`. 

1048 

1049 Args: 

1050 token_dispatcher: Token dispatch strategy. Supported values are 

1051 ``"all_to_all"`` and ``"deredundency"``. 

1052 async_combine: When ``True``, the combine all-to-all is launched 

1053 asynchronously so that the caller (e.g. :class:`MoE`) can 

1054 overlap it with shared-expert computation. When ``False`` 

1055 (default), combine is fully synchronous — no overlap, identical 

1056 to the baseline. 

1057 

1058 Example:: 

1059 >>> ep_style = ExpertParallel() 

1060 >>> sharded_experts = ep_style.apply(experts_module, ep_device_mesh) 

1061 >>> # With async combine for shared-expert overlap: 

1062 >>> ep_style = ExpertParallel(async_combine=True) 

1063 >>> sharded_experts = ep_style.apply(experts_module, ep_device_mesh) 

1064 """ 

1065 

1066 def __init__(self, token_dispatcher: Union[str, bool] = "all_to_all", async_combine: bool = False) -> None: 

1067 """Initialize ExpertParallel. 

1068 

1069 Args: 

1070 token_dispatcher: Token dispatch strategy. Supported values are 

1071 ``"all_to_all"`` and ``"deredundency"``. 

1072 async_combine: If ``True``, use asynchronous combine all-to-all 

1073 to overlap communication with shared-expert computation. 

1074 """ 

1075 if isinstance(token_dispatcher, bool): 

1076 async_combine = token_dispatcher 

1077 token_dispatcher = "all_to_all" 

1078 self._dispatch_ctx: Optional[DispatchContext] = None 

1079 self.async_combine = async_combine 

1080 self._token_dispatcher_name = token_dispatcher 

1081 self._token_dispatcher = _resolve_token_dispatcher(token_dispatcher) 

1082 

1083 def _token_dispatch(self, module: Module, inputs, device_mesh: DeviceMesh): 

1084 """Dispatch tokens to their assigned ranks via all-to-all. 

1085 

1086 Delegates to the configured token dispatcher and stores the 

1087 returned :class:`DispatchContext` on the instance for the matching 

1088 :meth:`_token_combine` call. 

1089 

1090 Args: 

1091 module: The ``GroupedExperts`` module. 

1092 inputs: Tuple ``(routed_input, num_tokens_per_expert)`` or  

1093 ``(routed_input, num_tokens_per_expert, routed_probs)``. 

1094 device_mesh: EP device mesh (1-D). 

1095 

1096 Returns: 

1097 Tuple ``(permuted_local_input, local_token_counts)`` or  

1098 ``(permuted_local_input, local_token_counts, permuted_probs)`` 

1099 depending on whether *routed_probs* was provided. 

1100 """ 

1101 # Delegate to the configured dispatcher (all_to_all or deredundency). 

1102 # Hard-coding AllToAllTokenDispatcher here would mismatch _token_combine 

1103 # (which uses self._token_dispatcher) and break deredundency. 

1104 dispatch_result = self._token_dispatcher.dispatch(module, inputs, device_mesh) 

1105 ctx = dispatch_result[-1] 

1106 # Store context in module attribute for _token_combine to read. 

1107 # Using module attribute ensures each module has its own context, 

1108 # solving the instance sharing problem when the same ExpertParallel 

1109 # style object is applied to multiple GroupedExperts modules. 

1110 # pylint: disable=W0212 

1111 module._ep_dispatch_ctx = ctx 

1112 # dispatch_result is either (permuted, local_counts, ctx) or 

1113 # (permuted, local_counts, permuted_probs, ctx); return all but ctx. 

1114 return dispatch_result[:-1] 

1115 

1116 def _token_combine(self, module: Module, routed_output, device_mesh: DeviceMesh): 

1117 """Gather expert outputs back to the originating ranks via all-to-all. 

1118 

1119 When ``async_combine=True``, launches the combine all-to-all 

1120 asynchronously and returns an :class:`AsyncCollectiveTensor`. The 

1121 actual device-side wait is deferred until the downstream consumer 

1122 (e.g. MoE unpermutation) first reads the tensor, enabling overlap 

1123 with shared-expert computation. 

1124 

1125 When ``async_combine=False`` (default), uses the synchronous 

1126 :meth:`AllToAllTokenDispatcher.combine` — identical to the baseline. 

1127 

1128 Args: 

1129 module: The ``GroupedExperts`` module. 

1130 routed_output: Expert output tensor in expert-major order. 

1131 device_mesh: EP device mesh (1-D). 

1132 

1133 Returns: 

1134 Token tensor in the original token-major layout. When 

1135 ``async_combine=True``, this may be an async collective tensor 

1136 whose values are not yet materialised. 

1137 

1138 Raises: 

1139 RuntimeError: If dispatch context is not found (dispatch was not called). 

1140 """ 

1141 # Read dispatch context from module attribute set by _token_dispatch. 

1142 # pylint: disable=W0212 

1143 ctx = getattr(module, "_ep_dispatch_ctx", None) 

1144 if ctx is None: 

1145 raise RuntimeError( 

1146 "_token_combine called but no dispatch context found in module. " 

1147 "This indicates _token_dispatch was not called before _token_combine, " 

1148 "or the context was already consumed by a previous combine call." 

1149 ) 

1150 

1151 # Note: Do NOT delete the context here. In PyTorch, the tensors in ctx 

1152 # are captured by autograd graph and don't need the attribute. But in 

1153 # MindSpore PyNative mode, deleting the attribute may break backward. 

1154 # The context will be overwritten on the next forward call. 

1155 

1156 if self.async_combine: 

1157 handle = self._token_dispatcher.combine_start( 

1158 routed_output, device_mesh, ctx 

1159 ) 

1160 # Store on module for external inspection / advanced use cases. 

1161 # pylint: disable=W0212 

1162 module._ep_combine_handle = handle 

1163 # Return the async tensor. The first non-view access by the 

1164 # downstream consumer (e.g. MoE unpermutation) will trigger the 

1165 # implicit wait, overlapping with shared_expert computation. 

1166 return handle.wait() 

1167 

1168 return self._token_dispatcher.combine( 

1169 module, routed_output, device_mesh, ctx, 

1170 ) 

1171 

1172 def _partition_mesh(self, device_mesh: DeviceMesh) -> DeviceMesh: 

1173 """Return the mesh used to shard expert weights.""" 

1174 if self._token_dispatcher_name == "deredundency": 

1175 return _get_flattened_ep_mesh(device_mesh) 

1176 return device_mesh 

1177 

1178 def _partition_fn( 

1179 self, name: str, module: Module, device_mesh: DeviceMesh 

1180 ) -> None: 

1181 """Shard all expert parameters along dim 0 (expert dimension). 

1182 

1183 Args: 

1184 name: Submodule name (unused). 

1185 module: The module whose parameters are being sharded. 

1186 device_mesh: EP device mesh. 

1187 """ 

1188 del name 

1189 partition_mesh = self._partition_mesh(device_mesh) 

1190 for key, param in _distribute_module_iter_params(module): 

1191 if param is None: 

1192 continue 

1193 src = _distribute_module_param_source(param) 

1194 requires_grad = bool(getattr(param, "requires_grad", True)) 

1195 dt = distribute_tensor(src, partition_mesh, [Shard(0)]) 

1196 new_param = _distribute_module_new_parameter(key, dt, requires_grad) 

1197 _distribute_module_set_param(module, key, new_param) 

1198 

1199 

1200# --------------------------------------------------------------------------- 

1201# TensorParallel — TP-only weight sharding for experts (no token dispatch) 

1202# --------------------------------------------------------------------------- 

1203class TensorParallel(ParallelStyle): 

1204 """Tensor Parallel for expert weights (no token dispatch). 

1205 

1206 Shards the ``GroupedExperts`` weight tensors in the column/row-wise 

1207 pattern used by standard TP: 

1208 

1209 - ``w1`` / ``w3``: ``Shard(1)`` — column-wise (hidden_dim dimension). 

1210 - ``w2``: ``Shard(2)`` — row-wise (output dim dimension). 

1211 

1212 Use this when EP degree is 1 and you want TP across experts without 

1213 any all-to-all token dispatch. Typically combined with the standard 

1214 :class:`~hyper_parallel.core.tensor_parallel.style.ColwiseParallel` / 

1215 :class:`~hyper_parallel.core.tensor_parallel.style.RowwiseParallel` 

1216 pattern for attention layers. 

1217 

1218 Example:: 

1219 >>> tp_style = TensorParallel() 

1220 >>> sharded_experts = tp_style.apply(experts_module, tp_device_mesh) 

1221 """ 

1222 

1223 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module: 

1224 """Apply TP weight sharding to *module*. 

1225 

1226 Args: 

1227 module: A :class:`GroupedExperts` instance. 

1228 device_mesh: 1-D TP device mesh (``mesh_dim_names=("tp",)``). 

1229 

1230 Returns: 

1231 The module with TP-sharded expert parameters. 

1232 """ 

1233 return distribute_module( 

1234 module, 

1235 device_mesh, 

1236 self._partition_fn, 

1237 ) 

1238 

1239 @staticmethod 

1240 def _partition_fn(name: str, module: Module, device_mesh: DeviceMesh) -> None: 

1241 """Shard expert weights column-wise (w1/w3) or row-wise (w2). 

1242 

1243 ``GroupedExperts`` weight layout is ``[num_experts, out_dim, in_dim]`` 

1244 so: 

1245 

1246 - ``w1``/``w3``: shard ``Shard(1)`` → split ``hidden_dim`` 

1247 (column-wise analogue). 

1248 - ``w2``: shard ``Shard(2)`` → split ``in_dim = hidden_dim`` 

1249 (row-wise analogue). 

1250 

1251 Args: 

1252 name: Submodule name (unused). 

1253 module: The module whose parameters are being sharded. 

1254 device_mesh: TP device mesh. 

1255 """ 

1256 del name 

1257 for key, param in _distribute_module_iter_params(module): 

1258 if param is None: 

1259 continue 

1260 src = _distribute_module_param_source(param) 

1261 requires_grad = bool(getattr(param, "requires_grad", True)) 

1262 # w1, w3: column-wise → Shard(1); w2: row-wise → Shard(2). 

1263 shard_dim = 2 if key == "w2" else 1 

1264 dt = distribute_tensor(src, device_mesh, [Shard(shard_dim)]) 

1265 new_param = _distribute_module_new_parameter(key, dt, requires_grad) 

1266 _distribute_module_set_param(module, key, new_param) 

1267 

1268 

1269# --------------------------------------------------------------------------- 

1270# ExpertTensorParallel — combined EP + TP on a 2-D [ep, tp] mesh 

1271# --------------------------------------------------------------------------- 

1272 

1273class ExpertTensorParallel(ExpertParallel): 

1274 """Combined Expert + Tensor Parallel on a 2-D ``[ep, tp]`` device mesh. 

1275 

1276 Extends :class:`ExpertParallel` to operate on a 2-D mesh with named 

1277 dimensions ``"ep"`` and ``"tp"``: 

1278 

1279 - **Partition**: each expert weight ``[num_experts, out, in]`` is doubly 

1280 sharded — ``Shard(0)`` along the EP dim (expert ownership) and 

1281 ``Shard(1)``/``Shard(2)`` along the TP dim (column-wise / row-wise). 

1282 - **Dispatch / Combine**: use only the 1-D ``device_mesh["ep"]`` sub-mesh 

1283 so that token routing uses EP-group collectives, not the full 2-D mesh. 

1284 

1285 Args: 

1286 token_dispatcher: Token dispatch strategy. Supported values are 

1287 ``"all_to_all"`` and ``"deredundency"``. 

1288 async_combine: Forwarded to :class:`ExpertParallel`. When ``True``, 

1289 the combine all-to-all is launched asynchronously for 

1290 shared-expert overlap. 

1291 

1292 Example:: 

1293 >>> etp_style = ExpertTensorParallel() 

1294 >>> sharded = etp_style.apply(experts_module, ep_tp_2d_mesh) 

1295 """ 

1296 

1297 def __init__(self, token_dispatcher: Union[str, bool] = "all_to_all", async_combine: bool = False) -> None: 

1298 """Initialize ExpertTensorParallel. 

1299 

1300 Args: 

1301 async_combine: If ``True``, use asynchronous combine all-to-all. 

1302 """ 

1303 super().__init__(token_dispatcher=token_dispatcher, async_combine=async_combine) 

1304 

1305 def _dispatch_mesh(self, device_mesh: DeviceMesh) -> DeviceMesh: 

1306 """Return the mesh used for token dispatch in ETP.""" 

1307 if self._token_dispatcher_name == "deredundency": 

1308 raise NotImplementedError( 

1309 "ExpertTensorParallel does not yet support " 

1310 "token_dispatcher='deredundency'. Use ExpertParallel with a " 

1311 "[oep, iep] mesh, or add [oep, iep, tp] mesh handling first." 

1312 ) 

1313 return device_mesh["ep"] 

1314 

1315 def _token_dispatch(self, module: Module, inputs, device_mesh: DeviceMesh): 

1316 """Dispatch tokens using only the EP sub-mesh. 

1317 

1318 Args: 

1319 module: The ``GroupedExperts`` module. 

1320 inputs: Tuple ``(routed_input, num_tokens_per_expert)`` or 

1321 ``(routed_input, num_tokens_per_expert, routed_probs)``. 

1322 device_mesh: 2-D device mesh with dims ``("ep", "tp")``. 

1323 

1324 Returns: 

1325 Tuple ``(permuted_local_input, local_token_counts)`` or 

1326 ``(permuted_local_input, local_token_counts, permuted_probs)`` 

1327 depending on whether *routed_probs* was provided. 

1328 """ 

1329 dispatch_mesh = self._dispatch_mesh(device_mesh) 

1330 # Gate: _dispatch_mesh raises NotImplementedError for deredundency, 

1331 # keeping dispatch/combine symmetric (both reject deredundency). 

1332 dispatch_result = self._token_dispatcher.dispatch(module, inputs, dispatch_mesh) 

1333 ctx = dispatch_result[-1] 

1334 # Store context in module attribute for _token_combine to read. 

1335 # Using module attribute ensures each module has its own context, 

1336 # solving the instance sharing problem when the same ExpertParallel 

1337 # style object is applied to multiple GroupedExperts modules. 

1338 # pylint: disable=W0212 

1339 # Store context in module attribute for _token_combine to read. 

1340 module._ep_dispatch_ctx = ctx 

1341 # dispatch_result is either (permuted, local_counts, ctx) or 

1342 # (permuted, local_counts, permuted_probs, ctx); return all but ctx. 

1343 return dispatch_result[:-1] 

1344 

1345 def _token_combine(self, module: Module, routed_output, device_mesh: DeviceMesh): 

1346 """Combine tokens using only the EP sub-mesh. 

1347 

1348 When ``async_combine=True``, launches the combine all-to-all 

1349 asynchronously via :meth:`self._token_dispatcher.combine_start`. 

1350 

1351 Args: 

1352 module: The ``GroupedExperts`` module. 

1353 routed_output: Expert output tensor in expert-major order. 

1354 device_mesh: 2-D device mesh with dims ``("ep", "tp")``. 

1355 

1356 Returns: 

1357 Token tensor in the original token-major layout. 

1358 

1359 Raises: 

1360 RuntimeError: If dispatch context is not found. 

1361 """ 

1362 # pylint: disable=W0212 

1363 # Read dispatch context from module attribute set by _token_dispatch. 

1364 ctx = getattr(module, "_ep_dispatch_ctx", None) 

1365 if ctx is None: 

1366 raise RuntimeError( 

1367 "_token_combine called but no dispatch context found in module. " 

1368 "This indicates _token_dispatch was not called before _token_combine, " 

1369 "or the context was already consumed by a previous combine call." 

1370 ) 

1371 

1372 # Note: Do NOT delete the context here. In PyTorch, the tensors in ctx 

1373 # are captured by autograd graph and don't need the attribute. But in 

1374 # MindSpore PyNative mode, deleting the attribute may break backward. 

1375 # The context will be overwritten on the next forward call. 

1376 

1377 dispatch_mesh = self._dispatch_mesh(device_mesh) 

1378 

1379 if self.async_combine: 

1380 handle = self._token_dispatcher.combine_start( 

1381 routed_output, dispatch_mesh, ctx 

1382 ) 

1383 # pylint: disable=W0212 

1384 module._ep_combine_handle = handle 

1385 return handle.wait() 

1386 

1387 return self._token_dispatcher.combine( 

1388 module, routed_output, dispatch_mesh, ctx, 

1389 ) 

1390 

1391 def _partition_fn( 

1392 self, name: str, module: Module, device_mesh: DeviceMesh 

1393 ) -> None: 

1394 """Shard expert weights along both EP (dim 0) and TP (dim 1 or 2). 

1395 

1396 Weight layout ``[num_experts, out_dim, in_dim]``: 

1397 

1398 - ``w1``/``w3``: ``[Shard(0), Shard(1)]`` — EP shards experts, 

1399 TP splits hidden_dim (column-wise). 

1400 - ``w2``: ``[Shard(0), Shard(2)]`` — EP shards experts, TP splits 

1401 the input dimension (row-wise). 

1402 

1403 Args: 

1404 name: Submodule name (unused). 

1405 module: The module whose parameters are being sharded. 

1406 device_mesh: 2-D device mesh with dims ``("ep", "tp")``. 

1407 """ 

1408 del name 

1409 for key, param in _distribute_module_iter_params(module): 

1410 if param is None: 

1411 continue 

1412 src = _distribute_module_param_source(param) 

1413 requires_grad = bool(getattr(param, "requires_grad", True)) 

1414 # EP shards expert ownership (dim 0); TP shards weight dim. 

1415 tp_dim = 2 if key == "w2" else 1 

1416 dt = distribute_tensor(src, device_mesh, [Shard(0), Shard(tp_dim)]) 

1417 new_param = _distribute_module_new_parameter(key, dt, requires_grad) 

1418 _distribute_module_set_param(module, key, new_param)