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

226 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"""NPU grouped routed-expert module.""" 

16 

17from __future__ import annotations 

18 

19from collections.abc import Mapping 

20from functools import partial 

21from typing import Any 

22 

23import torch # pylint: disable=forbidden-backend-import 

24from torch import nn # pylint: disable=forbidden-backend-import 

25from transformers.activations import ACT2FN 

26from hyper_parallel.components.checkpoint.weight_conversion import ( 

27 Transpose, 

28 WeightConverter, 

29 WeightRenaming, 

30) 

31 

32from hyper_parallel.models.replacement import module_replacement 

33from hyper_parallel.components.functional import ( 

34 grouped_matmul, 

35 moe_token_permute, 

36 moe_token_unpermute, 

37 swiglu, 

38) 

39 

40 

41@module_replacement 

42class GroupedExperts(nn.Module): 

43 """Transformers-compatible NPU grouped routed experts. 

44 

45 Holds gate_up_proj/up_proj, down_proj (and optional bias1, bias2) as nn.Parameters. 

46 Computation logic is handled by _grouped_gemm_expert_forward. 

47 

48 Args: 

49 module: Source Transformers experts module. 

50 module_fqn: Fully qualified name supplied by the replacement framework. 

51 context: Additional replacement context. 

52 """ 

53 

54 @staticmethod 

55 def _source_parameters(module: nn.Module, source_fc1_name: str) -> tuple[nn.Parameter, nn.Parameter]: 

56 """Validate and return source expert weight parameters.""" 

57 missing = [name for name in (source_fc1_name, "down_proj") if not hasattr(module, name)] 

58 if missing: 

59 raise TypeError(f"GroupedExperts source module is missing: {missing}") 

60 source_gate_up = getattr(module, source_fc1_name) 

61 if not isinstance(source_gate_up, nn.Parameter): 

62 raise TypeError(f"GroupedExperts requires {source_fc1_name} to be an nn.Parameter") 

63 if not isinstance(module.down_proj, nn.Parameter): 

64 raise TypeError("GroupedExperts requires down_proj to be an nn.Parameter") 

65 return source_gate_up, module.down_proj 

66 

67 @staticmethod 

68 def _source_dimensions( 

69 module: nn.Module, 

70 config: Any, 

71 source_gate_up: nn.Parameter, 

72 source_down: nn.Parameter, 

73 gated_linear_unit: bool, 

74 ) -> tuple[int, int, int]: 

75 """Infer expert count and projection dimensions from the source module.""" 

76 num_experts = getattr(module, "num_local_experts", getattr(module, "num_experts", None)) 

77 if num_experts is None and config is not None: 

78 num_experts = getattr(config, "n_routed_experts", getattr(config, "num_experts", None)) 

79 hidden_size = getattr(module, "hidden_size", getattr(module, "hidden_dim", None)) 

80 if hidden_size is None and config is not None: 

81 hidden_size = getattr(config, "hidden_size", None) 

82 intermediate_size = getattr(module, "intermediate_size", getattr(module, "intermediate_dim", None)) 

83 if intermediate_size is None and config is not None: 

84 intermediate_size = getattr(config, "moe_intermediate_size", getattr(config, "intermediate_size", None)) 

85 source_is_transposed = getattr(module, "is_transposed", None) 

86 if not gated_linear_unit and source_gate_up.dim() == 3: 

87 hidden_size = GroupedExperts._infer_ungated_hidden_size( 

88 source_gate_up, source_down, source_is_transposed, intermediate_size, hidden_size 

89 ) 

90 if num_experts is None or hidden_size is None or intermediate_size is None: 

91 raise ValueError("GroupedExperts requires expert count, hidden size, and intermediate size") 

92 return num_experts, hidden_size, intermediate_size 

93 

94 @staticmethod 

95 def _infer_ungated_hidden_size( 

96 source_gate_up: nn.Parameter, 

97 source_down: nn.Parameter, 

98 source_is_transposed: bool | None, 

99 intermediate_size: int | None, 

100 default: int | None, 

101 ) -> int | None: 

102 """Infer hidden size for ungated three-dimensional source weights.""" 

103 if source_is_transposed is False: 

104 return source_gate_up.shape[-1] 

105 if source_is_transposed is True: 

106 return source_gate_up.shape[-2] 

107 if intermediate_size is None: 

108 return default 

109 if source_gate_up.shape[-2] == intermediate_size and source_down.shape[-1] == intermediate_size: 

110 return source_gate_up.shape[-1] 

111 if source_gate_up.shape[-1] == intermediate_size and source_down.shape[-2] == intermediate_size: 

112 return source_gate_up.shape[-2] 

113 return default 

114 

115 @staticmethod 

116 def _activation(module: nn.Module, config: Any, gated_linear_unit: bool) -> Any: 

117 """Resolve the source-compatible expert activation.""" 

118 hidden_act = getattr(config, "hidden_act", None) 

119 if hidden_act is None: 

120 hidden_act = getattr(config, "hidden_activation", None) 

121 if hidden_act is None: 

122 hidden_act = getattr(config, "mlp_hidden_act", None) 

123 activation_func = getattr(module, "act_fn", None) 

124 if activation_func is None: 

125 if hidden_act is None: 

126 hidden_act = "silu" 

127 activation_func = ACT2FN[hidden_act] 

128 source_apply_gate = getattr(module, "_apply_gate", None) 

129 source_apply_gate_func = getattr(source_apply_gate, "__func__", source_apply_gate) 

130 if ( 

131 gated_linear_unit 

132 and source_apply_gate_func is not None 

133 and getattr(source_apply_gate_func, "__name__", "") != "_default_apply_gate" 

134 ): 

135 raise ValueError("GroupedExperts custom _apply_gate requires a dedicated replacement") 

136 if not gated_linear_unit: 

137 return activation_func 

138 if hidden_act == "silu" and bool(getattr(config, "use_fused_swiglu", True)): 

139 return partial(swiglu, dim=-1) 

140 

141 def glu(x: torch.Tensor) -> torch.Tensor: 

142 """Apply the configured gated linear unit.""" 

143 gate, value = torch.chunk(x, 2, dim=-1) 

144 return activation_func(gate) * value 

145 

146 return glu 

147 

148 @staticmethod 

149 def _source_layout( 

150 source_shapes: tuple[tuple[int, ...], tuple[int, ...]], 

151 source_is_transposed: bool | None, 

152 expert_shapes: tuple[tuple[int, ...], tuple[int, ...]], 

153 transformers_shapes: tuple[tuple[int, ...], tuple[int, ...]], 

154 npu_shapes: tuple[tuple[int, ...], tuple[int, ...]], 

155 ) -> tuple[bool, bool]: 

156 """Return whether experts are two-dimensional and need transposition.""" 

157 if source_shapes == expert_shapes: 

158 return True, False 

159 if source_is_transposed is False: 

160 if source_shapes != transformers_shapes: 

161 raise ValueError("GroupedExperts source weights do not match their declared layout") 

162 return False, True 

163 if source_is_transposed is True: 

164 if source_shapes != npu_shapes: 

165 raise ValueError("GroupedExperts source weights do not match their declared layout") 

166 return False, False 

167 if source_shapes == transformers_shapes: 

168 return False, True 

169 if source_shapes == npu_shapes: 

170 return False, False 

171 raise ValueError("GroupedExperts source weight shapes are incompatible with the configured dimensions") 

172 

173 def _initialize_weights( 

174 self, 

175 source_gate_up: nn.Parameter, 

176 source_down: nn.Parameter, 

177 npu_gate_up_shape: tuple[int, ...], 

178 npu_down_shape: tuple[int, ...], 

179 gated_linear_unit: bool, 

180 ) -> None: 

181 """Create or reuse expert parameters in the target NPU layout.""" 

182 if self._transpose_source_weights: 

183 target_gate_up = nn.Parameter( 

184 torch.empty(npu_gate_up_shape, device=source_gate_up.device, dtype=source_gate_up.dtype), 

185 requires_grad=source_gate_up.requires_grad, 

186 ) 

187 self.down_proj = nn.Parameter( 

188 torch.empty(npu_down_shape, device=source_down.device, dtype=source_down.dtype), 

189 requires_grad=source_down.requires_grad, 

190 ) 

191 else: 

192 target_gate_up = source_gate_up 

193 self.down_proj = source_down 

194 if gated_linear_unit: 

195 self.gate_up_proj = target_gate_up 

196 else: 

197 self.up_proj = target_gate_up 

198 

199 def _initialize_biases(self, module: nn.Module) -> None: 

200 """Validate and materialize optional expert bias parameters.""" 

201 self._source_bias_names = ( 

202 "bias1" if hasattr(module, "bias1") else f"{self._source_fc1_name}_bias", 

203 "bias2" if hasattr(module, "bias2") else "down_proj_bias", 

204 ) 

205 source_bias1 = getattr(module, self._source_bias_names[0], None) 

206 source_bias2 = getattr(module, self._source_bias_names[1], None) 

207 if (source_bias1 is None) != (source_bias2 is None): 

208 raise ValueError("GroupedExperts source projections must use the same bias policy") 

209 self.add_bias = source_bias1 is not None 

210 if not self.add_bias: 

211 return 

212 if not isinstance(source_bias1, nn.Parameter) or not isinstance(source_bias2, nn.Parameter): 

213 raise TypeError("GroupedExperts source biases must be nn.Parameter instances") 

214 target_biases = [] 

215 for source_name, target_name, source_bias in zip( 

216 self._source_bias_names, ("bias1", "bias2"), (source_bias1, source_bias2) 

217 ): 

218 if source_name == target_name: 

219 target_biases.append(source_bias) 

220 else: 

221 target_biases.append( 

222 nn.Parameter(torch.empty_like(source_bias), requires_grad=source_bias.requires_grad) 

223 ) 

224 # astroid cannot infer the length of a list built by appends in a 

225 # loop; the zip above always yields exactly two entries. 

226 self.bias1, self.bias2 = target_biases # pylint: disable=W0632 

227 

228 def __init__( 

229 self, 

230 *, 

231 module: nn.Module, 

232 module_fqn: str = "", 

233 context: Mapping[str, Any] | None = None, 

234 ) -> None: 

235 """Build the high-performance module from a Transformers experts module.""" 

236 super().__init__() 

237 del module_fqn, context 

238 config = getattr(module, "config", None) 

239 self.config = config 

240 self.initializer_range = getattr(config, "initializer_range", None) 

241 if bool(getattr(module, "has_bias", False)): 

242 raise ValueError("GroupedExperts does not support source modules with has_bias=True") 

243 gated_linear_unit = bool( 

244 getattr( 

245 module, 

246 "has_gate", 

247 getattr(config, "gated_linear_unit", True), 

248 ) 

249 ) 

250 self._source_fc1_name = "gate_up_proj" if gated_linear_unit else "up_proj" 

251 source_gate_up, source_down = self._source_parameters(module, self._source_fc1_name) 

252 num_experts, hidden_size, intermediate_size = self._source_dimensions( 

253 module, config, source_gate_up, source_down, gated_linear_unit 

254 ) 

255 source_down = module.down_proj 

256 source_is_transposed = getattr(module, "is_transposed", None) 

257 

258 self.num_local_experts = num_experts 

259 self.num_experts = num_experts 

260 self.hidden_size = hidden_size 

261 self.intermediate_size = intermediate_size 

262 self.router_gating_in_fp32 = bool( 

263 getattr(module, "router_gating_in_fp32", True) 

264 ) 

265 self.activation_func = self._activation(module, config, gated_linear_unit) 

266 

267 # GroupGemm state (NPU-only) 

268 self._group_list = None 

269 self._tokens_per_expert_gmm = None 

270 

271 self._initialize_projection_layout( 

272 source_gate_up, source_down, source_is_transposed, gated_linear_unit 

273 ) 

274 self._initialize_biases(module) 

275 

276 self.has_gate = gated_linear_unit 

277 self.has_bias = self.add_bias 

278 self.is_concatenated = bool(getattr(module, "is_concatenated", True)) 

279 if self.has_gate and not self.is_concatenated: 

280 raise ValueError("GroupedExperts requires concatenated gate/up expert weights") 

281 self.is_transposed = True 

282 # Keep HF native initialization order stable; this replacement module 

283 # resets its packed expert parameters after materialization instead. 

284 self._hp_reset_after_materialization = True 

285 self.train(module.training) 

286 

287 def _initialize_projection_layout( 

288 self, source_gate_up, source_down, source_is_transposed, gated_linear_unit, 

289 ) -> None: 

290 """Resolve expert weight layouts and initialize their target parameters.""" 

291 hidden_size = self.hidden_size 

292 intermediate_size = self.intermediate_size 

293 fc1_output_size = intermediate_size 

294 if gated_linear_unit: 

295 fc1_output_size *= 2 

296 

297 npu_gate_up_shape = ( 

298 self.num_local_experts, 

299 hidden_size, 

300 fc1_output_size, 

301 ) 

302 npu_down_shape = ( 

303 self.num_local_experts, 

304 intermediate_size, 

305 hidden_size, 

306 ) 

307 transformers_gate_up_shape = ( 

308 self.num_local_experts, 

309 fc1_output_size, 

310 hidden_size, 

311 ) 

312 transformers_down_shape = ( 

313 self.num_local_experts, 

314 hidden_size, 

315 intermediate_size, 

316 ) 

317 experts_2d_gate_up_shape = ( 

318 self.num_local_experts * hidden_size, 

319 fc1_output_size, 

320 ) 

321 experts_2d_down_shape = ( 

322 self.num_local_experts * intermediate_size, 

323 hidden_size, 

324 ) 

325 source_shapes = (tuple(source_gate_up.shape), tuple(source_down.shape)) 

326 self.use_2d_experts, self._transpose_source_weights = self._source_layout( 

327 source_shapes, 

328 source_is_transposed, 

329 (experts_2d_gate_up_shape, experts_2d_down_shape), 

330 (transformers_gate_up_shape, transformers_down_shape), 

331 (npu_gate_up_shape, npu_down_shape), 

332 ) 

333 self._initialize_weights( 

334 source_gate_up, source_down, npu_gate_up_shape, npu_down_shape, gated_linear_unit 

335 ) 

336 

337 def reset_parameters(self) -> None: 

338 """Initialize grouped weights with the source model's configured standard deviation.""" 

339 if self.initializer_range is None: 

340 raise ValueError( 

341 "GroupedExperts random initialization requires config.initializer_range" 

342 ) 

343 target_fc1 = self.gate_up_proj if self.has_gate else self.up_proj 

344 nn.init.normal_(target_fc1, mean=0.0, std=self.initializer_range) 

345 nn.init.normal_(self.down_proj, mean=0.0, std=self.initializer_range) 

346 if self.add_bias: 

347 nn.init.zeros_(self.bias1) 

348 nn.init.zeros_(self.bias2) 

349 

350 def make_transforms(self) -> list[WeightRenaming | WeightConverter]: 

351 """Describe reversible source-checkpoint to high-performance conversion.""" 

352 transforms: list[WeightRenaming | WeightConverter] = [] 

353 if self._transpose_source_weights: 

354 transforms.extend( 

355 [ 

356 WeightConverter( 

357 source_patterns=self._source_fc1_name, 

358 target_patterns=self._source_fc1_name, 

359 operations=[Transpose(dim0=-2, dim1=-1)], 

360 ), 

361 WeightConverter( 

362 source_patterns="down_proj", 

363 target_patterns="down_proj", 

364 operations=[Transpose(dim0=-2, dim1=-1)], 

365 ), 

366 ] 

367 ) 

368 if self.add_bias: 

369 for source_name, target_name in zip( 

370 self._source_bias_names, 

371 ("bias1", "bias2"), 

372 ): 

373 if source_name == target_name: 

374 continue 

375 transforms.append( 

376 WeightRenaming( 

377 source_patterns=source_name, 

378 target_patterns=target_name, 

379 ) 

380 ) 

381 return transforms 

382 

383 def _grouped_gemm_expert_forward( 

384 self, gate_up_proj, down_proj, permuted, tokens_per_expert, permuted_probs 

385 ): 

386 """GroupGemm expert computation (NPU-only).""" 

387 self._tokens_per_expert_gmm = tokens_per_expert.to(device=permuted.device) 

388 self._group_list = self._tokens_per_expert_gmm.cumsum(dim=0) 

389 

390 if self.use_2d_experts: 

391 gate_up_proj = gate_up_proj.view(self.num_local_experts, self.hidden_size, -1) 

392 

393 # up 

394 if permuted.nelement() != 0: 

395 fc1_output = grouped_matmul( # pylint: disable=not-callable 

396 permuted, gate_up_proj, bias=None, group_list=self._group_list, group_type=0, group_list_type=0, 

397 ) 

398 if self.add_bias: 

399 b1 = self.bias1.view(self.num_local_experts, -1) 

400 fc1_output = fc1_output + torch.repeat_interleave(b1, self._tokens_per_expert_gmm, dim=0) 

401 else: 

402 gate_up_proj_2d = gate_up_proj.view(self.hidden_size, -1) 

403 fc1_output = torch.matmul(permuted, gate_up_proj_2d) 

404 

405 # down 

406 if not self.router_gating_in_fp32 and permuted_probs is not None: 

407 fc1_output = ( 

408 self.activation_func(fc1_output) 

409 * permuted_probs.reshape(*fc1_output.shape[:-1], 1) 

410 ) 

411 else: 

412 fc1_output = self.activation_func(fc1_output) 

413 

414 if self.use_2d_experts: 

415 down_proj = down_proj.view(self.num_local_experts, -1, self.hidden_size) 

416 

417 if fc1_output.nelement() != 0: 

418 fc2_output = grouped_matmul( # pylint: disable=not-callable 

419 fc1_output, down_proj, bias=None, group_list=self._group_list, group_type=0, group_list_type=0, 

420 ) 

421 if self.add_bias: 

422 b2 = self.bias2.view(self.num_local_experts, -1) 

423 fc2_output = fc2_output + torch.repeat_interleave( 

424 b2, self._tokens_per_expert_gmm, dim=0, 

425 ) 

426 else: 

427 down_proj_2d = down_proj.view(-1, self.hidden_size) 

428 fc2_output = torch.matmul(fc1_output, down_proj_2d) 

429 

430 return fc2_output 

431 

432 def forward_expert_major( 

433 self, 

434 x: torch.Tensor, 

435 num_tokens_per_expert: torch.Tensor, 

436 scores: torch.Tensor | None = None, 

437 ) -> torch.Tensor: 

438 """Run grouped experts on expert-major routed tokens. 

439 

440 Args: 

441 x: Expert-major input with shape ``[tokens, hidden_size]``. 

442 num_tokens_per_expert: Token count for every local expert. 

443 scores: Optional routing weight for every routed token. 

444 

445 Returns: 

446 Expert-major output with shape ``[tokens, hidden_size]``. 

447 """ 

448 gate_up_proj = self.gate_up_proj if self.has_gate else self.up_proj 

449 down_proj = self.down_proj 

450 self.num_local_experts = num_tokens_per_expert.shape[0] 

451 

452 return self._grouped_gemm_expert_forward( 

453 gate_up_proj, down_proj, x, num_tokens_per_expert, scores 

454 ) 

455 

456 def forward( 

457 self, 

458 hidden_states: torch.Tensor, 

459 top_k_index: torch.Tensor, 

460 top_k_weights: torch.Tensor, 

461 ) -> torch.Tensor: 

462 """Run grouped experts with the Transformers Experts interface.""" 

463 hidden_shape = hidden_states.shape 

464 hidden_states_flat = hidden_states.view(-1, hidden_states.shape[-1]) 

465 permuted_tokens, sorted_indices = moe_token_permute( # pylint: disable=not-callable 

466 hidden_states_flat, 

467 top_k_index, 

468 ) 

469 flatten_probs = top_k_weights.view(-1) 

470 permuted_probs = flatten_probs.index_select(0, sorted_indices) 

471 tokens_per_expert = torch.histc( 

472 top_k_index.view(-1).float(), 

473 bins=self.num_experts, 

474 min=0, 

475 max=self.num_experts, 

476 ).long() 

477 expert_outputs = self.forward_expert_major( 

478 permuted_tokens, 

479 tokens_per_expert, 

480 permuted_probs, 

481 ) 

482 output = moe_token_unpermute( # pylint: disable=not-callable 

483 expert_outputs, 

484 sorted_indices, 

485 top_k_weights, 

486 ) 

487 return output.view(hidden_shape)