Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / components / modules / moe.py: 68%

221 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +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"""PyTorch MoE building blocks: router, experts and orchestrator. 

16 

17Provides :class:`FeedForward`, :class:`GroupedExperts`, 

18:class:`TokenChoiceTopKRouter`, and the top-level :class:`MoE` orchestrator. 

19Load-balancing utilities (expert bias update and auxiliary loss) are also 

20included. 

21 

22All modules compose naturally with EP / TP parallel strategies via DTensor. 

23Distributed collectives are handled by 

24:mod:`hyper_parallel.core.expert_parallel.expert_parallel`; this module 

25contains only single-device computation. 

26""" 

27from __future__ import annotations 

28 

29__all__ = [ 

30 "FeedForward", 

31 "GroupedExperts", 

32 "MoE", 

33 "MoEAuxLossAutoScaler", 

34 "TokenChoiceTopKRouter", 

35 "update_expert_bias", 

36] 

37 

38import math 

39from typing import Any 

40 

41import torch 

42import torch.nn.functional as F 

43from torch import nn 

44 

45from hyper_parallel.components.functional.npu_grouped_swiglu import npu_grouped_swiglu 

46from hyper_parallel.core.dtensor.dtensor import DTensor 

47 

48 

49def _is_npu_tensor(tensor: torch.Tensor) -> bool: 

50 """Return whether a tensor is stored on an Ascend NPU.""" 

51 return tensor.device.type == "npu" 

52 

53 

54# --------------------------------------------------------------------------- 

55# Grouped expert computation kernels 

56# --------------------------------------------------------------------------- 

57 

58def _run_experts_for_loop( 

59 w1: torch.Tensor, 

60 w2: torch.Tensor, 

61 w3: torch.Tensor, 

62 x: torch.Tensor, 

63 num_tokens_per_expert: torch.Tensor, 

64 scores: torch.Tensor | None = None, 

65) -> torch.Tensor: 

66 """Run per-expert SwiGLU via a sequential loop (reference path). 

67 

68 Args: 

69 w1: Shape ``[num_experts, hidden_dim, dim]``. 

70 w2: Shape ``[num_experts, dim, hidden_dim]``. 

71 w3: Shape ``[num_experts, hidden_dim, dim]``. 

72 x: Routed tokens in expert-major order, shape 

73 ``[total_routed_tokens, dim]``. 

74 num_tokens_per_expert: 1-D integer tensor of length ``num_experts``. 

75 scores: Optional 1-D tensor of shape ``[total_routed_tokens]``. 

76 Routing weights applied to intermediate activations (``silu(w1(x)) * w3(x)``) 

77 before the w2 projection. When ``None``, no weighting is applied. 

78 Defaults to ``None``. 

79 

80 Returns: 

81 Expert output of shape ``[total_routed_tokens, dim]``. 

82 """ 

83 # Use ``torch.cat`` instead of ``zeros_like + in-place slice assign``. 

84 # On some backends (notably torch_npu) in-place slice assignment onto a 

85 # ``requires_grad=False`` leaf tensor does not reliably upgrade it to a 

86 # non-leaf with a ``grad_fn`` — the forward result may end up with 

87 # ``grad_fn=None`` and downstream ``backward()`` fails with 

88 # "element 0 of tensors does not require grad and does not have a grad_fn". 

89 # 

90 # Drain ``num_tokens_per_expert`` to host **once** via ``.tolist()`` 

91 # rather than calling ``int(n)`` per loop iteration — a single D2H 

92 # copy instead of ``num_local_experts`` separate ones. Per-iter 

93 # ``.item()`` would stall the host between expert kernels and shrink 

94 # the dual-pipe overlap window. 

95 counts_list = num_tokens_per_expert.tolist() 

96 parts = [] 

97 offset = 0 

98 for e, n in enumerate(counts_list): 

99 if n == 0: 

100 continue 

101 x_e = x[offset:offset + n] 

102 h = F.silu(x_e @ w1[e].T) * (x_e @ w3[e].T) 

103 if scores is not None: 

104 h = h * scores[offset:offset + n].unsqueeze(-1) 

105 parts.append(h @ w2[e].T) 

106 offset += n 

107 if not parts: 

108 # No routed tokens: return a grad-connected zero (not ``zeros_like``). 

109 return x * 0.0 

110 return torch.cat(parts, dim=0) 

111 

112 

113def _run_experts_grouped_mm_gpu( 

114 w1: torch.Tensor, 

115 w2: torch.Tensor, 

116 w3: torch.Tensor, 

117 x: torch.Tensor, 

118 num_tokens_per_expert: torch.Tensor, 

119 scores: torch.Tensor | None = None, 

120) -> torch.Tensor: 

121 """Fused grouped matmul path for NVIDIA GPU using ``torch._grouped_mm``. 

122 

123 Args: 

124 w1: Shape ``[num_experts, hidden_dim, dim]``. 

125 w2: Shape ``[num_experts, dim, hidden_dim]``. 

126 w3: Shape ``[num_experts, hidden_dim, dim]``. 

127 x: Shape ``[total_routed_tokens, dim]``. 

128 num_tokens_per_expert: 1-D integer tensor of length ``num_experts``. 

129 scores: Optional 1-D tensor of shape ``[total_routed_tokens]``. 

130 Routing weights applied to intermediate activations (``silu(w1(x)) * w3(x)``) 

131 before the w2 projection. When ``None``, no weighting is applied. 

132 Defaults to ``None``. 

133 

134 Returns: 

135 Expert output of shape ``[total_routed_tokens, dim]``. 

136 """ 

137 # grouped_mm needs an end offset for every expert, including the last one. 

138 offs = torch.cumsum(num_tokens_per_expert, dim=0).to(torch.int32) 

139 # Column-major views let grouped_mm consume the existing parameter storage 

140 # without retaining another full set of expert weights for backward. 

141 w1_t = w1.transpose(1, 2) 

142 w3_t = w3.transpose(1, 2) 

143 w2_t = w2.transpose(1, 2) 

144 h1 = torch._grouped_mm(x, w1_t, offs=offs) # pylint: disable=protected-access 

145 h3 = torch._grouped_mm(x, w3_t, offs=offs) # pylint: disable=protected-access 

146 h = F.silu(h1) * h3 

147 if scores is not None: 

148 h = h * scores.unsqueeze(-1) 

149 return torch._grouped_mm(h, w2_t, offs=offs) # pylint: disable=protected-access 

150 

151 

152def _run_experts_grouped_mm_npu( 

153 w1: torch.Tensor, 

154 w2: torch.Tensor, 

155 w3: torch.Tensor, 

156 x: torch.Tensor, 

157 num_tokens_per_expert: torch.Tensor, 

158 scores: torch.Tensor | None = None, 

159) -> torch.Tensor: 

160 """Fused grouped matmul path for Ascend NPU using ``torch_npu.npu_grouped_matmul``. 

161 

162 Requires ``torch_npu`` to be installed. 

163 

164 Args: 

165 w1: Shape ``[num_experts, hidden_dim, dim]``. 

166 w2: Shape ``[num_experts, dim, hidden_dim]``. 

167 w3: Shape ``[num_experts, hidden_dim, dim]``. 

168 x: Shape ``[total_routed_tokens, dim]``. 

169 num_tokens_per_expert: 1-D integer tensor of length ``num_experts``. 

170 scores: Optional 1-D tensor of shape ``[total_routed_tokens]``. 

171 Routing weights applied after the bias-free w2 projection. 

172 When ``None``, no weighting is applied. Defaults to ``None``. 

173 

174 Returns: 

175 Expert output of shape ``[total_routed_tokens, dim]``. 

176 """ 

177 if x.shape[0] == 0: 

178 parameter_zero = (w1.sum() + w2.sum() + w3.sum()).to(x.dtype) * 0.0 

179 return x + parameter_zero 

180 

181 gate_up_weight = torch.cat((w1, w3), dim=1) 

182 output = npu_grouped_swiglu( 

183 x, 

184 gate_up_weight, 

185 w2, 

186 num_tokens_per_expert, 

187 ) 

188 if scores is not None: 

189 output = output * scores.to(output.dtype).unsqueeze(-1) 

190 return output 

191 

192 

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

194# FeedForward — shared expert / standard SwiGLU FFN 

195# --------------------------------------------------------------------------- 

196 

197class FeedForward(nn.Module): 

198 """SwiGLU feed-forward network, used as a shared (always-active) expert. 

199 

200 Implements: ``output = w2(silu(w1(x)) * w3(x))`` 

201 

202 Args: 

203 dim: Input embedding dimension. 

204 hidden_dim: Intermediate hidden dimension. 

205 bias: Whether to add a learnable bias. Defaults to ``False``. 

206 

207 Example:: 

208 >>> ff = FeedForward(dim=256, hidden_dim=512) 

209 >>> out = ff(torch.randn(4, 16, 256)) 

210 >>> out.shape 

211 torch.Size([4, 16, 256]) 

212 """ 

213 

214 def __init__(self, dim: int, hidden_dim: int, bias: bool = False) -> None: 

215 """Initialize FeedForward with three linear layers. 

216 

217 Args: 

218 dim: Input embedding dimension. 

219 hidden_dim: Intermediate hidden dimension. 

220 bias: Whether to add a learnable bias. Defaults to ``False``. 

221 """ 

222 super().__init__() 

223 self.w1 = nn.Linear(dim, hidden_dim, bias=bias) 

224 self.w2 = nn.Linear(hidden_dim, dim, bias=bias) 

225 self.w3 = nn.Linear(dim, hidden_dim, bias=bias) 

226 

227 def forward(self, x: torch.Tensor) -> torch.Tensor: 

228 """Compute SwiGLU feed-forward output. 

229 

230 Args: 

231 x: Input tensor of shape ``(..., dim)``. 

232 

233 Returns: 

234 Output tensor with the same leading shape and last dimension ``dim``. 

235 """ 

236 return self.w2(F.silu(self.w1(x)) * self.w3(x)) 

237 

238 

239# --------------------------------------------------------------------------- 

240# GroupedExperts 

241# --------------------------------------------------------------------------- 

242 

243class GroupedExperts(nn.Module): 

244 """Batch expert computation with optional grouped matrix-multiply. 

245 

246 All expert weights are stored in a single 3-D parameter so that EP / TP 

247 sharding strategies can distribute the expert dimension via DTensor. 

248 

249 Args: 

250 dim: Token embedding dimension. 

251 hidden_dim: Expert hidden dimension (SwiGLU intermediate size). 

252 num_experts: Total number of experts. 

253 use_grouped_mm: If ``True``, uses a hardware-accelerated grouped 

254 matmul kernel (``torch._grouped_mm`` on GPU, 

255 ``torch_npu.npu_grouped_matmul`` on NPU). Falls back to the 

256 for-loop path when neither is available. Defaults to ``False``. 

257 

258 Note: 

259 When weights are DTensors (e.g. after TP sharding via 

260 :class:`ExpertTensorParallel`), ``forward`` calls ``.to_local()`` 

261 before computation. 

262 

263 Example:: 

264 >>> experts = GroupedExperts(dim=8, hidden_dim=16, num_experts=4) 

265 >>> x = torch.randn(10, 8) 

266 >>> counts = torch.tensor([3, 2, 4, 1]) 

267 >>> out = experts(x, counts) 

268 >>> out.shape 

269 torch.Size([10, 8]) 

270 """ 

271 

272 def __init__( 

273 self, 

274 dim: int, 

275 hidden_dim: int, 

276 num_experts: int, 

277 use_grouped_mm: bool = False, 

278 ) -> None: 

279 """Initialize GroupedExperts with stacked expert weight matrices. 

280 

281 Args: 

282 dim: Input embedding dimension. 

283 hidden_dim: Intermediate hidden dimension. 

284 num_experts: Number of experts. 

285 use_grouped_mm: Whether to use grouped matrix multiplication. 

286 """ 

287 super().__init__() 

288 # Weight layout: [num_experts, out_dim, in_dim] so that the standard 

289 # linear operation is x @ w[e].T. 

290 self.w1 = nn.Parameter(torch.empty(num_experts, hidden_dim, dim)) 

291 self.w2 = nn.Parameter(torch.empty(num_experts, dim, hidden_dim)) 

292 self.w3 = nn.Parameter(torch.empty(num_experts, hidden_dim, dim)) 

293 self.num_experts = num_experts 

294 self.use_grouped_mm = use_grouped_mm 

295 self._reset_parameters() 

296 

297 def _reset_parameters(self) -> None: 

298 """Kaiming-uniform initialisation for all expert weight tensors.""" 

299 for weight in (self.w1, self.w2, self.w3): 

300 nn.init.kaiming_uniform_(weight.view(weight.shape[0], -1), a=math.sqrt(5)) 

301 

302 def forward( 

303 self, 

304 x: torch.Tensor, 

305 num_tokens_per_expert: torch.Tensor, 

306 scores: torch.Tensor | None = None, 

307 ) -> torch.Tensor: 

308 """Run all experts on their assigned tokens. 

309 

310 Args: 

311 x: Routed tokens in expert-major order, 

312 shape ``[total_routed_tokens, dim]``. 

313 num_tokens_per_expert: 1-D integer tensor of length 

314 ``num_local_experts`` with the token count per expert. 

315 scores: Optional 1-D tensor of shape ``[total_routed_tokens]``. 

316 Routing weights for each token's expert output. 

317 When ``None``, no weighting is applied. Defaults to ``None``. 

318 

319 Returns: 

320 Expert output of shape ``[total_routed_tokens, dim]``. 

321 """ 

322 # Extract local shard when parameters are DTensors (TP path). 

323 w1 = self.w1.to_local() if isinstance(self.w1, DTensor) else self.w1 

324 w2 = self.w2.to_local() if isinstance(self.w2, DTensor) else self.w2 

325 w3 = self.w3.to_local() if isinstance(self.w3, DTensor) else self.w3 

326 

327 if not self.use_grouped_mm: 

328 return _run_experts_for_loop(w1, w2, w3, x, num_tokens_per_expert, scores) 

329 if _is_npu_tensor(x): 

330 return _run_experts_grouped_mm_npu(w1, w2, w3, x, num_tokens_per_expert, scores) 

331 if x.device.type == "cuda": 

332 return _run_experts_grouped_mm_gpu(w1, w2, w3, x, num_tokens_per_expert, scores) 

333 return _run_experts_for_loop(w1, w2, w3, x, num_tokens_per_expert, scores) 

334 

335 

336# --------------------------------------------------------------------------- 

337# TokenChoiceTopKRouter 

338# --------------------------------------------------------------------------- 

339 

340class TokenChoiceTopKRouter(nn.Module): 

341 """Top-K router: each token independently selects its top-K experts. 

342 

343 Args: 

344 dim: Token embedding dimension (input to the gate). 

345 num_experts: Total number of experts. 

346 top_k: Experts selected per token. Defaults to ``1``. 

347 score_func: Activation on gate logits before topk. 

348 One of ``"sigmoid"`` (default) or ``"softmax"``. 

349 num_expert_groups: For node-limited routing, number of expert 

350 groups. ``None`` disables node-limited routing. 

351 num_limited_groups: Groups to keep in node-limited routing. Required 

352 when ``num_expert_groups`` is not ``None``. 

353 route_scale: Scalar multiplier applied to routing scores. 

354 Defaults to ``1.0``. 

355 

356 Example:: 

357 >>> router = TokenChoiceTopKRouter(dim=64, num_experts=8, top_k=2) 

358 >>> scores, indices, counts = router(torch.randn(32, 64)) 

359 >>> scores.shape, indices.shape, counts.shape 

360 (torch.Size([32, 2]), torch.Size([32, 2]), torch.Size([8])) 

361 """ 

362 

363 def __init__( 

364 self, 

365 dim: int, 

366 num_experts: int, 

367 top_k: int = 1, 

368 score_func: str = "sigmoid", 

369 num_expert_groups: int | None = None, 

370 num_limited_groups: int | None = None, 

371 route_scale: float = 1.0, 

372 ) -> None: 

373 """Initialize the top-K router. 

374 

375 Args: 

376 dim: Input dimension of token representations. 

377 num_experts: Total number of experts. 

378 top_k: Number of experts to select per token. 

379 score_func: Scoring function, ``"sigmoid"`` or ``"softmax"``. 

380 num_expert_groups: Number of expert groups for node-limited routing. 

381 num_limited_groups: Number of groups each token can route to. 

382 route_scale: Scalar multiplier applied to routing scores. 

383 """ 

384 super().__init__() 

385 if score_func not in ("sigmoid", "softmax"): 

386 raise ValueError( 

387 f"score_func must be 'sigmoid' or 'softmax', got '{score_func}'." 

388 ) 

389 if num_expert_groups is not None and num_limited_groups is None: 

390 raise ValueError( 

391 "num_limited_groups must be set when num_expert_groups is not None." 

392 ) 

393 self.gate = nn.Linear(dim, num_experts, bias=False) 

394 self.num_experts = num_experts 

395 self.top_k = top_k 

396 self.score_func = score_func 

397 self.num_expert_groups = num_expert_groups 

398 self.num_limited_groups = num_limited_groups 

399 self.route_scale = route_scale 

400 

401 def forward( 

402 self, 

403 x: torch.Tensor, 

404 expert_bias: torch.Tensor | None = None, 

405 ) -> tuple: 

406 """Compute routing scores and top-K expert assignments. 

407 

408 Args: 

409 x: Token tensor of shape ``[num_tokens, dim]``. 

410 expert_bias: Optional 1-D tensor of shape ``[num_experts]`` added 

411 to gate logits for topk selection only (auxiliary-loss-free 

412 load balancing). Does not affect the returned ``top_scores``. 

413 

414 Returns: 

415 Tuple of: 

416 

417 - ``top_scores``: shape ``[num_tokens, top_k]`` — routing weights. 

418 - ``selected_experts``: shape ``[num_tokens, top_k]`` — expert IDs. 

419 - ``num_tokens_per_expert``: shape ``[num_experts]`` — load counts. 

420 """ 

421 # Gate in float32 for numerical stability. 

422 scores = self.gate(x).float() 

423 

424 if self.score_func == "sigmoid": 

425 scores = torch.sigmoid(scores) 

426 else: 

427 scores = F.softmax(scores, dim=-1) 

428 

429 if self.route_scale != 1.0: 

430 scores = scores * self.route_scale 

431 

432 # Node-limited routing — mask out low-scoring expert groups. 

433 scores_for_topk = scores 

434 if self.num_expert_groups is not None: 

435 scores_for_topk = self._get_node_limited_routing_scores(scores) 

436 

437 # Add expert bias only for selection; returned scores remain unbiased. 

438 scores_with_bias = scores_for_topk 

439 if expert_bias is not None: 

440 scores_with_bias = scores_for_topk + expert_bias.float() 

441 

442 top_scores, selected_experts = scores_with_bias.topk(self.top_k, dim=-1) 

443 # Gather unbiased scores as the actual routing weights. 

444 top_scores = scores.gather(1, selected_experts) 

445 

446 num_tokens_per_expert = torch.bincount( 

447 selected_experts.flatten(), minlength=self.num_experts 

448 ) 

449 return top_scores, selected_experts, num_tokens_per_expert 

450 

451 def _get_node_limited_routing_scores(self, scores: torch.Tensor) -> torch.Tensor: 

452 """Mask out low-scoring expert groups (node-limited routing). 

453 

454 Args: 

455 scores: Routing scores of shape ``[num_tokens, num_experts]``. 

456 

457 Returns: 

458 Scores with non-selected groups masked to ``-inf``. 

459 """ 

460 num_tokens, num_experts = scores.shape 

461 experts_per_group = num_experts // self.num_expert_groups 

462 group_scores = scores.view( 

463 num_tokens, self.num_expert_groups, experts_per_group 

464 ).max(dim=-1).values # [num_tokens, num_groups] 

465 

466 _, selected_groups = group_scores.topk(self.num_limited_groups, dim=-1) 

467 

468 mask = scores.new_zeros(num_tokens, self.num_expert_groups) 

469 mask.scatter_(1, selected_groups, 1.0) 

470 mask = ( 

471 mask.unsqueeze(-1) 

472 .expand(num_tokens, self.num_expert_groups, experts_per_group) 

473 .reshape(num_tokens, num_experts) 

474 ) 

475 return scores.masked_fill(mask == 0, float("-inf")) 

476 

477 

478# --------------------------------------------------------------------------- 

479# Load-balance auxiliary loss 

480# --------------------------------------------------------------------------- 

481 

482def _compute_load_balance_loss( 

483 top_scores: torch.Tensor, 

484 selected_experts: torch.Tensor, 

485 num_experts: int, 

486 sequence_partition_group: Any | None = None, 

487) -> torch.Tensor: 

488 """Compute load-balance auxiliary loss. 

489 

490 Standard formulation: ``loss = num_experts * Σ (expert_fraction_i * expert_prob_i)`` 

491 

492 Where: 

493 - expert_fraction_i = tokens routed to expert i / total routed tokens 

494 - expert_prob_i = sum of routing scores for expert i / total tokens 

495 

496 When routing is perfectly balanced, loss ≈ 1.0. 

497 When routing is imbalanced, loss > 1.0. 

498 

499 When ``sequence_partition_group`` is provided (e.g. the TP×CP or CP 

500 process group), ``expert_fraction`` (token counts) is all-reduced across 

501 the group so that the loss reflects the *global* token distribution over 

502 the full sequence. ``expert_prob`` is kept local to preserve gradient 

503 flow back to the router weights, and ``num_tokens`` is scaled by 

504 ``num_sub_sequence`` (the group world size) to account for the sharded 

505 sequence dimension. This follows the Megatron-LM 

506 ``full_sequence_aux_loss`` pattern. 

507 

508 Args: 

509 top_scores: Routing weights, shape ``[num_tokens, top_k]``. 

510 selected_experts: Expert IDs, shape ``[num_tokens, top_k]``. 

511 num_experts: Total number of experts. 

512 sequence_partition_group: Optional process group spanning the 

513 sequence-partition dimension (TP+SP or CP). When not ``None``, 

514 ``expert_fraction`` is all-reduced across this group. 

515 

516 Returns: 

517 Scalar loss tensor. Returns 0.0 for empty input. 

518 """ 

519 import torch.distributed as dist # pylint: disable=C0415 

520 

521 num_tokens, top_k = top_scores.shape 

522 

523 if num_tokens == 0: 

524 return torch.tensor(0.0, dtype=top_scores.dtype, device=top_scores.device) 

525 

526 flat_experts = selected_experts.flatten() 

527 flat_scores = top_scores.flatten() 

528 

529 expert_fraction = torch.zeros( 

530 num_experts, dtype=top_scores.dtype, device=top_scores.device 

531 ) 

532 expert_fraction.scatter_add_(0, flat_experts, torch.ones_like(flat_scores)) 

533 

534 num_sub_sequence = 1 

535 if sequence_partition_group is not None: 

536 num_sub_sequence = dist.get_world_size(sequence_partition_group) 

537 dist.all_reduce(expert_fraction, group=sequence_partition_group) 

538 

539 expert_fraction = expert_fraction / (num_tokens * num_sub_sequence * top_k) 

540 

541 expert_prob = torch.zeros( 

542 num_experts, dtype=top_scores.dtype, device=top_scores.device 

543 ) 

544 expert_prob.scatter_add_(0, flat_experts, flat_scores) 

545 expert_prob = expert_prob / (num_tokens * num_sub_sequence) 

546 

547 loss = num_experts * (expert_fraction * expert_prob).sum() 

548 

549 return loss 

550 

551 

552# --------------------------------------------------------------------------- 

553# MoEAuxLossAutoScaler — gradient injection for auxiliary loss 

554# --------------------------------------------------------------------------- 

555 

556 

557class MoEAuxLossAutoScaler(torch.autograd.Function): 

558 """Autograd function that injects auxiliary-loss gradient into the backward chain. 

559 

560 Forward transparently passes through ``top_scores`` (values unchanged). 

561 Backward injects a scaled gradient for ``aux_loss``, causing its gradients 

562 to flow through the router weights without adding aux_loss to the main 

563 loss explicitly. 

564 

565 The loss scale is managed via the class-level :meth:`set_loss_scale` method, 

566 which should be called before ``loss.backward()`` in the training loop 

567 (typically by the pipeline schedule or training framework). 

568 

569 Reference: Megatron-LM ``megatron/core/transformer/moe/moe_utils.py`` 

570 """ 

571 

572 main_loss_backward_scale: torch.Tensor | None = None 

573 

574 @staticmethod 

575 def forward(ctx: Any, output: torch.Tensor, aux_loss: torch.Tensor) -> torch.Tensor: 

576 """Pass through ``output`` unchanged; save ``aux_loss`` for backward. 

577 

578 Args: 

579 output: Router output (e.g. ``top_scores``), passed through unchanged. 

580 aux_loss: Scalar auxiliary loss tensor to inject gradient for. 

581 

582 Returns: 

583 The ``output`` tensor, identical in value but with an added 

584 autograd edge to ``aux_loss``. 

585 """ 

586 ctx.save_for_backward(aux_loss) 

587 return output 

588 

589 @staticmethod 

590 def backward(ctx: Any, grad_output: torch.Tensor) -> tuple: 

591 """Inject scaled aux_loss gradient into the backward chain. 

592 

593 Args: 

594 grad_output: Gradient flowing back through ``output``. 

595 

596 Returns: 

597 Tuple of (grad_output unchanged, scaled aux_loss gradient). 

598 """ 

599 (aux_loss,) = ctx.saved_tensors 

600 if MoEAuxLossAutoScaler.main_loss_backward_scale is None: 

601 MoEAuxLossAutoScaler.main_loss_backward_scale = torch.tensor( 

602 1.0, device=aux_loss.device, 

603 ) 

604 aux_loss_backward_scale = MoEAuxLossAutoScaler.main_loss_backward_scale 

605 scaled_aux_loss_grad = torch.ones_like(aux_loss) * aux_loss_backward_scale 

606 return grad_output, scaled_aux_loss_grad 

607 

608 @staticmethod 

609 def set_loss_scale(scale: torch.Tensor) -> None: 

610 """Set the gradient scale for auxiliary loss. 

611 

612 Should be called before ``loss.backward()`` in the training loop. 

613 Uses in-place copy on subsequent calls to avoid tensor reallocation. 

614 

615 Args: 

616 scale: Tensor containing the loss scale value, typically 

617 ``1 / (num_microbatches * dp_size)`` or similar. 

618 """ 

619 if MoEAuxLossAutoScaler.main_loss_backward_scale is None: 

620 MoEAuxLossAutoScaler.main_loss_backward_scale = scale 

621 else: 

622 MoEAuxLossAutoScaler.main_loss_backward_scale.copy_(scale) 

623 

624 

625# --------------------------------------------------------------------------- 

626# MoE orchestrator 

627# --------------------------------------------------------------------------- 

628 

629class MoE(nn.Module): 

630 """Mixture-of-Experts layer. 

631 

632 Orchestrates routing, token permutation, expert computation, and output 

633 scatter-add. Supports shared experts, auxiliary-loss-free load balancing 

634 via expert bias, node-limited routing, and auxiliary load-balance loss. 

635 

636 Args: 

637 dim: Token embedding dimension. 

638 hidden_dim: Expert hidden dimension. 

639 num_experts: Total number of experts. 

640 top_k: Experts selected per token. Defaults to ``1``. 

641 score_before_experts: If ``True``, multiply routed tokens by routing 

642 weights *before* expert computation; otherwise multiply expert 

643 outputs *after*. Defaults to ``True``. 

644 load_balance_coeff: When not ``None``, enables auxiliary load-balance 

645 loss. The loss gradient is automatically injected into router 

646 weights via :class:`MoEAuxLossAutoScaler` (no manual loss 

647 addition needed). The scalar loss value is attached as 

648 ``output._load_balance_loss`` for logging purposes. 

649 sequence_partition_group: Optional process group spanning the 

650 sequence-partition dimension (TP+SP or CP). When provided, 

651 ``expert_fraction`` is all-reduced across this group so the 

652 load-balance loss reflects global token distribution. Defaults 

653 to ``None`` (no cross-rank synchronization). 

654 shared_expert: Optional :class:`FeedForward` running on every token 

655 in parallel; output added to routed-expert output. 

656 router_kwargs: Extra keyword arguments forwarded to 

657 :class:`TokenChoiceTopKRouter`. 

658 use_grouped_mm: If ``True``, uses a hardware-accelerated grouped 

659 matmul kernel (e.g. ``npu_grouped_matmul``) inside 

660 :class:`GroupedExperts`. Defaults to ``False``. 

661 

662 Note: 

663 *Auxiliary-loss-free load balancing*: call :func:`update_expert_bias` 

664 once per optimiser step to adjust ``expert_bias`` from the accumulated 

665 ``tokens_per_expert`` histogram. 

666 

667 Example:: 

668 >>> moe = MoE(dim=64, hidden_dim=128, num_experts=8, top_k=2) 

669 >>> out = moe(torch.randn(2, 16, 64)) 

670 >>> out.shape 

671 torch.Size([2, 16, 64]) 

672 """ 

673 

674 def __init__( 

675 self, 

676 dim: int, 

677 hidden_dim: int, 

678 num_experts: int, 

679 top_k: int = 1, 

680 score_before_experts: bool = True, 

681 load_balance_coeff: float | None = None, 

682 sequence_partition_group: Any | None = None, 

683 shared_expert: FeedForward | None = None, 

684 router_kwargs: dict | None = None, 

685 use_grouped_mm: bool = False, 

686 ) -> None: 

687 """Initialize MoE block with experts, router and optional shared expert. 

688 

689 Args: 

690 dim: Input embedding dimension. 

691 hidden_dim: Intermediate hidden dimension of each expert. 

692 num_experts: Number of experts. 

693 top_k: Number of experts to select per token. 

694 score_before_experts: If ``True``, multiply routed hidden states by 

695 the routing score. 

696 load_balance_coeff: Load-balance loss coefficient. 

697 shared_expert: Optional shared expert applied to all tokens. 

698 router_kwargs: Additional keyword arguments for the router. 

699 use_grouped_mm: Whether to use grouped matrix multiplication. 

700 """ 

701 super().__init__() 

702 router_kw = router_kwargs or {} 

703 self.experts = GroupedExperts( 

704 dim=dim, hidden_dim=hidden_dim, num_experts=num_experts, 

705 use_grouped_mm=use_grouped_mm, 

706 ) 

707 self.router = TokenChoiceTopKRouter( 

708 dim=dim, num_experts=num_experts, top_k=top_k, **router_kw, 

709 ) 

710 self.shared_expert = shared_expert 

711 self.num_experts = num_experts 

712 self.top_k = top_k 

713 self.score_before_experts = score_before_experts 

714 self.load_balance_coeff = load_balance_coeff 

715 self.sequence_partition_group = sequence_partition_group 

716 self.last_aux_loss: torch.Tensor | None = None 

717 self.enable_expert_bias = True 

718 

719 # Auxiliary-loss-free load-balance buffers (no gradient). 

720 self.register_buffer("expert_bias", torch.zeros(num_experts)) 

721 self.register_buffer("tokens_per_expert", torch.zeros(num_experts)) 

722 

723 def permutation( 

724 self, 

725 selected_experts: torch.Tensor, 

726 top_scores: torch.Tensor, 

727 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: 

728 """Compute token-major → expert-major permutation indices. 

729 

730 Args: 

731 selected_experts: Expert IDs of shape ``[num_tokens, top_k]``. 

732 top_scores: Routing weights of shape ``[num_tokens, top_k]``. 

733 

734 Returns: 

735 Tuple of ``(token_indices, top_scores_sorted, 

736 num_tokens_per_expert)`` where *token_indices* maps each 

737 expert-major slot back to its source token row, 

738 *top_scores_sorted* are the scores in expert-major order, and 

739 *num_tokens_per_expert* has shape ``[num_experts]``. 

740 """ 

741 # flat_experts[i] is the expert ID for the i-th (token, top_k) slot. 

742 flat_experts = selected_experts.flatten() # [num_tokens * top_k] 

743 flat_indices = flat_experts.argsort(stable=True) # expert-major permutation 

744 top_scores_sorted = top_scores.flatten()[flat_indices] # [num_tokens * top_k] 

745 # Each entry in flat_indices maps to a position in [0, num_tokens * top_k); 

746 # divide by top_k to recover the original token row index. 

747 token_indices = flat_indices // self.top_k # [num_tokens * top_k] 

748 num_tokens_per_expert = torch.bincount( 

749 flat_experts, minlength=self.num_experts 

750 ) 

751 return token_indices, top_scores_sorted, num_tokens_per_expert 

752 

753 def unpermutation( 

754 self, 

755 expert_out: torch.Tensor, 

756 token_indices: torch.Tensor, 

757 num_tokens: int, 

758 dim: int, 

759 ) -> torch.Tensor: 

760 """Scatter expert outputs back to token-major order. 

761 

762 Args: 

763 expert_out: Expert output tensor in expert-major order, 

764 shape ``[total_routed_tokens, dim]``. 

765 token_indices: Permutation indices from :meth:`permutation`, 

766 shape ``[num_tokens * top_k]``. 

767 num_tokens: Total number of tokens (unflattened). 

768 dim: Feature dimension. 

769 

770 Returns: 

771 Token-major output tensor of shape ``[num_tokens, dim]``. 

772 """ 

773 # Use out-of-place ``scatter_add`` so autograd correctly records 

774 # ``ScatterAddBackward``; ``new_zeros + scatter_add_`` on some 

775 # backends (torch_npu) leaves the leaf un-upgraded and the result 

776 # without a ``grad_fn``. 

777 return torch.zeros( 

778 num_tokens, dim, dtype=expert_out.dtype, device=expert_out.device, 

779 ).scatter_add( 

780 0, 

781 token_indices.unsqueeze(1).expand(-1, dim), 

782 expert_out, 

783 ) 

784 

785 def forward(self, x: torch.Tensor) -> torch.Tensor: 

786 """Run the MoE layer. 

787 

788 Args: 

789 x: Input tensor of shape ``[batch, seq_len, dim]``. 

790 

791 Returns: 

792 Output tensor of shape ``[batch, seq_len, dim]``. When 

793 ``load_balance_coeff`` is set, carries a ``_load_balance_loss`` 

794 attribute with the auxiliary loss scalar (for logging only; 

795 gradient injection is handled automatically by 

796 :class:`MoEAuxLossAutoScaler`). 

797 """ 

798 bs, seq_len, dim = x.shape 

799 num_tokens = bs * seq_len 

800 x_flat = x.view(num_tokens, dim) # [num_tokens, dim] 

801 

802 # --- Routing --- 

803 top_scores, selected_experts, token_counts = self.router( 

804 x_flat, self.expert_bias 

805 ) 

806 

807 # Accumulate token histogram without creating gradient nodes. 

808 with torch.no_grad(): 

809 self.tokens_per_expert.add_(token_counts.float()) 

810 

811 # --- Auxiliary load-balance loss --- 

812 # Compute aux_loss early and attach it to top_scores via 

813 # MoEAuxLossAutoScaler so that backward through top_scores 

814 # also triggers aux_loss gradient (injected into router weights). 

815 if self.load_balance_coeff is not None: 

816 lb_loss = self.load_balance_coeff * _compute_load_balance_loss( 

817 top_scores, selected_experts, self.num_experts, 

818 sequence_partition_group=self.sequence_partition_group, 

819 ) 

820 # Apply AutoScaler *before* top_scores is used in the forward path 

821 # so that the main backward through top_scores triggers aux_loss 

822 # gradient injection. Forward values are unchanged. 

823 top_scores = MoEAuxLossAutoScaler.apply(top_scores, lb_loss) 

824 self.last_aux_loss = lb_loss.detach() 

825 else: 

826 lb_loss = None 

827 self.last_aux_loss = None 

828 

829 # --- Token permutation: token-major → expert-major --- 

830 token_indices, top_scores_sorted, num_tokens_per_expert = self.permutation( 

831 selected_experts, top_scores 

832 ) 

833 

834 # Gather routed tokens in expert-major order. 

835 routed_x = x_flat[token_indices] # [num_tokens * top_k, dim] 

836 

837 # --- Expert computation --- 

838 if self.score_before_experts: 

839 routed_x = routed_x * top_scores_sorted.unsqueeze(1) 

840 expert_out = self.experts(routed_x, num_tokens_per_expert, scores=None) 

841 else: 

842 expert_out = self.experts(routed_x, num_tokens_per_expert, scores=top_scores_sorted) 

843 

844 # --- Shared expert (parallel with routed experts) --- 

845 shared_out = None 

846 if self.shared_expert is not None: 

847 shared_out = self.shared_expert(x_flat) 

848 

849 

850 # --- Scatter expert outputs back to token order --- 

851 out = self.unpermutation(expert_out, token_indices, num_tokens, dim) 

852 

853 if shared_out is not None: 

854 out = out + shared_out 

855 

856 result = out.view(bs, seq_len, dim) 

857 

858 # Attach auxiliary loss to the returned tensor for logging. 

859 if lb_loss is not None: 

860 result._load_balance_loss = lb_loss # pylint: disable=protected-access 

861 

862 return result 

863 

864 def update_expert_bias( 

865 self, 

866 lr: float = 1e-3, 

867 num_recomputations: int = 1, 

868 ) -> None: 

869 """Update expert bias for auxiliary-loss-free load balancing. 

870 

871 Should be called once per training step after the optimizer step. 

872 Adjusts ``expert_bias`` to push token load towards the mean, then 

873 resets the ``tokens_per_expert`` accumulator. 

874 

875 The update delta is centered to have zero mean, preventing systematic 

876 drift of all bias values over time. 

877 

878 When activation checkpoint is enabled, forward is re-executed during 

879 backward, causing ``tokens_per_expert`` to accumulate twice (or more). 

880 Use ``num_recomputations`` to correct for this double-counting. 

881 

882 Args: 

883 lr: Step size for the bias update. Defaults to ``1e-3``. 

884 num_recomputations: Number of times forward is executed per optimizer 

885 step. Default ``1`` (normal training). Set to ``2`` when activation 

886 checkpoint is enabled (forward + recompute during backward). For 

887 nested checkpoint strategies, set to the actual execution count. 

888 

889 Example:: 

890 >>> # Single-card scenario without activation checkpoint: 

891 >>> moe_layer.update_expert_bias(lr=1e-3) 

892 >>> 

893 >>> # With activation checkpoint (forward executed twice): 

894 >>> moe_layer.update_expert_bias(lr=1e-3, num_recomputations=2) 

895 """ 

896 with torch.no_grad(): 

897 if num_recomputations > 1: 

898 self.tokens_per_expert.div_(num_recomputations) 

899 avg = self.tokens_per_expert.float().mean() 

900 delta = lr * (avg - self.tokens_per_expert.float()).sign() 

901 delta = delta - delta.mean() 

902 self.expert_bias.data += delta 

903 self.tokens_per_expert.zero_() 

904 

905 

906# --------------------------------------------------------------------------- 

907# Expert bias update for auxiliary-loss-free load balancing 

908# --------------------------------------------------------------------------- 

909 

910def update_expert_bias( 

911 moe: MoE, 

912 lr: float = 1e-3, 

913 num_recomputations: int = 1, 

914) -> None: 

915 """Update expert bias for auxiliary-loss-free load balancing. 

916 

917 This is a module-level wrapper that calls ``moe.update_expert_bias()``. 

918 Prefer calling the instance method directly. 

919 

920 Should be called once per training step after the optimizer step. 

921 Adjusts ``moe.expert_bias`` to push token load towards the mean, then 

922 resets the ``tokens_per_expert`` accumulator. 

923 

924 The update delta is centered to have zero mean, preventing systematic 

925 drift of all bias values over time. 

926 

927 When activation checkpoint is enabled, forward is re-executed during 

928 backward, causing ``tokens_per_expert`` to accumulate twice (or more). 

929 Use ``num_recomputations`` to correct for this double-counting. 

930 

931 Args: 

932 moe: The :class:`MoE` module whose bias should be updated. 

933 lr: Step size for the bias update. Defaults to ``1e-3``. 

934 num_recomputations: Number of times forward is executed per optimizer 

935 step. Default ``1`` (normal training). Set to ``2`` when activation 

936 checkpoint is enabled (forward + recompute during backward). For 

937 nested checkpoint strategies, set to the actual execution count. 

938 

939 Example:: 

940 >>> # Single-card scenario without activation checkpoint: 

941 >>> update_expert_bias(moe_layer, lr=1e-3) 

942 >>> 

943 >>> # With activation checkpoint (forward executed twice): 

944 >>> update_expert_bias(moe_layer, lr=1e-3, num_recomputations=2) 

945 """ 

946 moe.update_expert_bias(lr=lr, num_recomputations=num_recomputations)