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

96 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +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"""Canonical low-precision adapter for DeepSeek-V3 grouped experts. 

16 

17``GroupedExperts`` is the single canonical grouped-experts shell: it only 

18orchestrates routing, activation, and token ordering. Gate/up and down 

19projections run through the bound ``GroupedLinear``, whose 

20instance is picked exclusively in ``strategy_factory``. 

21""" 

22 

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

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

25 

26from hyper_parallel.components.quantization.config import ( 

27 GROUPED_NDIM, 

28 GROUP_LIST_COUNTS, 

29 MATRIX_NDIM, 

30) 

31from hyper_parallel.components.quantization.functional.base_gmm_func import ( 

32 GroupedLinear, 

33 _GroupedLinearFunction, 

34) 

35from hyper_parallel.components.quantization.ops.npu_mxfp8 import LowPrecisionCapabilityError 

36from hyper_parallel.components.quantization.ops.npu_w4a8 import ( 

37 W4A8CapabilityError, 

38) 

39 

40_EXPERT_PARAMETER_NAMES = ("gate_up_proj", "down_proj") 

41_GATE_UP_BRANCHES = 2 

42 

43 

44class GroupedExperts(nn.Module): 

45 """Canonical low-precision shell for DeepSeek-V3 grouped experts. 

46 

47 The bound grouped-linear compute may select MXFP8, HiFloat8, native W4A8, 

48 or fake W4A8. The shell owns no quantizer; it holds only the 

49 ``grouped_linear`` instance selected by ``strategy_factory``. 

50 """ 

51 

52 def __init__( 

53 self, 

54 num_experts: int, 

55 hidden_dim: int, 

56 intermediate_dim: int, 

57 *, 

58 grouped_linear: GroupedLinear, 

59 fqn: str = "", 

60 ) -> None: 

61 """Create an unbound packed-expert module.""" 

62 

63 self._validate_grouped_linear(grouped_linear) 

64 super().__init__() 

65 self.gate_up_proj = nn.Parameter( 

66 torch.empty( 

67 num_experts, 

68 _GATE_UP_BRANCHES * intermediate_dim, 

69 hidden_dim, 

70 ) 

71 ) 

72 self.down_proj = nn.Parameter( 

73 torch.empty(num_experts, hidden_dim, intermediate_dim) 

74 ) 

75 self.act_fn = nn.SiLU() 

76 self.num_experts = num_experts 

77 self.hidden_dim = hidden_dim 

78 self.intermediate_dim = intermediate_dim 

79 self.fqn = fqn 

80 self.grouped_linear = grouped_linear 

81 self.activation_limit = None 

82 

83 @staticmethod 

84 def _validate_grouped_linear(grouped_linear: GroupedLinear) -> None: 

85 """Require a plain strategy so module registration stays unchanged.""" 

86 

87 if isinstance(grouped_linear, nn.Module) or not isinstance( 

88 grouped_linear, 

89 GroupedLinear, 

90 ): 

91 raise TypeError( 

92 "grouped_linear must be a non-Module GroupedLinear instance" 

93 ) 

94 

95 @classmethod 

96 def from_module( 

97 cls, 

98 source: nn.Module, 

99 *, 

100 fqn: str, 

101 grouped_linear: GroupedLinear, 

102 ) -> "GroupedExperts": 

103 """Create a no-allocation shell retaining the source registrations.""" 

104 

105 cls._validate_grouped_linear(grouped_linear) 

106 parameter_names = tuple(source._parameters) # pylint: disable=protected-access 

107 if parameter_names != _EXPERT_PARAMETER_NAMES: 

108 raise TypeError( 

109 f"{fqn!r} must register packed expert parameters " 

110 f"{_EXPERT_PARAMETER_NAMES}, but got {parameter_names}." 

111 ) 

112 gate_up_proj = source._parameters["gate_up_proj"] # pylint: disable=protected-access 

113 down_proj = source._parameters["down_proj"] # pylint: disable=protected-access 

114 if gate_up_proj is None or down_proj is None: 

115 raise ValueError(f"{fqn!r} packed expert parameters cannot be None.") 

116 if gate_up_proj.ndim != GROUPED_NDIM or down_proj.ndim != GROUPED_NDIM: 

117 raise ValueError( 

118 f"{fqn!r} packed expert weights must be three-dimensional." 

119 ) 

120 expected_down_shape = ( 

121 gate_up_proj.shape[0], 

122 gate_up_proj.shape[2], 

123 gate_up_proj.shape[1] // _GATE_UP_BRANCHES, 

124 ) 

125 if ( 

126 gate_up_proj.shape[1] % _GATE_UP_BRANCHES 

127 or tuple(down_proj.shape) != expected_down_shape 

128 ): 

129 raise ValueError( 

130 f"{fqn!r} has incompatible gate/up and down expert shapes: " 

131 f"gate_up={tuple(gate_up_proj.shape)}, down={tuple(down_proj.shape)}." 

132 ) 

133 

134 # Create a no-allocation shell: cls.__new__(cls) resolves to 

135 # object.__new__ (a staticmethod), so passing cls explicitly is correct; 

136 # pylint mis-infers the binding of __new__ on a classmethod's cls. 

137 converted = cls.__new__(cls) # pylint: disable=no-value-for-parameter 

138 # Deliberately invoke nn.Module.__init__ directly so the shell does not 

139 # re-run this class's allocating __init__. 

140 nn.Module.__init__(converted) # pylint: disable=unnecessary-dunder-call 

141 for name, parameter in source._parameters.items(): # pylint: disable=protected-access 

142 converted.register_parameter(name, parameter) 

143 for name, buffer in source._buffers.items(): # pylint: disable=protected-access 

144 converted.register_buffer( 

145 name, 

146 buffer, 

147 persistent=name not in source._non_persistent_buffers_set, # pylint: disable=protected-access 

148 ) 

149 for name, module in source._modules.items(): # pylint: disable=protected-access 

150 converted.add_module(name, module) 

151 converted.act_fn = getattr(source, "act_fn", nn.SiLU()) 

152 

153 converted.num_experts = gate_up_proj.shape[0] 

154 converted.hidden_dim = gate_up_proj.shape[2] 

155 converted.intermediate_dim = gate_up_proj.shape[1] // _GATE_UP_BRANCHES 

156 converted.fqn = fqn 

157 converted.grouped_linear = grouped_linear 

158 converted.activation_limit = getattr(source, "limit", None) 

159 if hasattr(source, "config"): 

160 converted.config = source.config 

161 converted.training = source.training 

162 return converted 

163 

164 def _grouped_forward( 

165 self, 

166 sorted_inputs: torch.Tensor, 

167 tokens_per_expert: torch.Tensor, 

168 ) -> torch.Tensor: 

169 """Run already expert-major tokens through the two low-precision GMMs.""" 

170 

171 try: 

172 gate_up = _GroupedLinearFunction.apply( 

173 sorted_inputs, 

174 self.gate_up_proj, 

175 tokens_per_expert, 

176 self.grouped_linear, 

177 GROUP_LIST_COUNTS, 

178 ) 

179 gate, up = gate_up.chunk(_GATE_UP_BRANCHES, dim=-1) 

180 if self.activation_limit is None: 

181 intermediate = self.act_fn(gate) * up 

182 else: 

183 gate = gate.clamp(max=self.activation_limit) 

184 up = up.clamp(min=-self.activation_limit, max=self.activation_limit) 

185 intermediate = self.act_fn(gate) * up 

186 return _GroupedLinearFunction.apply( 

187 intermediate, 

188 self.down_proj, 

189 tokens_per_expert, 

190 self.grouped_linear, 

191 GROUP_LIST_COUNTS, 

192 ) 

193 except (LowPrecisionCapabilityError, W4A8CapabilityError) as exc: 

194 target = self.fqn or "<unknown>" 

195 raise LowPrecisionCapabilityError( 

196 f"Low-precision grouped-expert target {target!r} cannot run: {exc}" 

197 ) from exc 

198 

199 def forward( 

200 self, 

201 hidden_states: torch.Tensor, 

202 top_k_index: torch.Tensor, 

203 top_k_weights: torch.Tensor, 

204 ) -> torch.Tensor: 

205 """Sort routed tokens, execute grouped experts, and restore token order.""" 

206 

207 if hidden_states.ndim != MATRIX_NDIM: 

208 raise ValueError( 

209 "Grouped experts require two-dimensional hidden_states, " 

210 f"but got shape {tuple(hidden_states.shape)}." 

211 ) 

212 if ( 

213 top_k_index.shape != top_k_weights.shape 

214 or top_k_index.ndim != MATRIX_NDIM 

215 ): 

216 raise ValueError( 

217 "top_k_index and top_k_weights must have the same two-dimensional shape." 

218 ) 

219 if top_k_index.shape[0] != hidden_states.shape[0]: 

220 raise ValueError( 

221 "The routed token count must match hidden_states: " 

222 f"routes={top_k_index.shape[0]}, tokens={hidden_states.shape[0]}." 

223 ) 

224 

225 input_dtype = hidden_states.dtype 

226 token_count = hidden_states.shape[0] 

227 top_k = top_k_index.shape[1] 

228 source_token_indices = torch.arange( 

229 token_count, 

230 device=hidden_states.device, 

231 ).unsqueeze(1).expand(-1, top_k).reshape(-1) 

232 flattened_expert_indices = top_k_index.reshape(-1) 

233 expert_order = flattened_expert_indices.argsort() 

234 sorted_inputs = hidden_states[source_token_indices[expert_order]] 

235 tokens_per_expert = torch.bincount( 

236 flattened_expert_indices, 

237 minlength=self.num_experts, 

238 ) 

239 # bincount rejects negative indices; an extra bin identifies an upper bound. 

240 if tokens_per_expert.numel() != self.num_experts: 

241 raise ValueError( 

242 f"Expert indices must be in [0, {self.num_experts})." 

243 ) 

244 # bincount returns per-expert counts, so the incoming group-list type 

245 # is 1. Each grouped-linear strategy adapts this only if its backend 

246 # requires cumulative offsets instead. 

247 sorted_outputs = self._grouped_forward(sorted_inputs, tokens_per_expert) 

248 sorted_weights = top_k_weights.reshape(-1)[expert_order] 

249 sorted_outputs = sorted_outputs * sorted_weights.unsqueeze(-1) 

250 

251 inverse_order = torch.empty_like(expert_order) 

252 inverse_order[expert_order] = torch.arange( 

253 expert_order.shape[0], 

254 device=expert_order.device, 

255 ) 

256 # Accumulate every routed contribution before rounding once at the 

257 # module boundary. GroupedExperts preserves the input activation 

258 # dtype even when FP32 routing weights promote the weighted outputs. 

259 return sorted_outputs[inverse_order].view( 

260 token_count, 

261 top_k, 

262 self.hidden_dim, 

263 ).sum(dim=1).to(dtype=input_dtype) 

264 

265 

266__all__ = ["GroupedExperts"]