Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / components / quantization / quantizers / w4a8.py: 34%

74 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"""Native W4A8 quantizer for grouped expert training.""" 

16 

17from typing import Optional 

18 

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

20 

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

22 transform_grouped_scale, 

23 W4A8NpuOps, 

24 _get_w4a8_npu_ops, 

25) 

26from hyper_parallel.components.quantization.quantizers.base import Quantizer 

27from hyper_parallel.components.quantization.tensor import ( 

28 QuantizedTensor, 

29 W4A8Tensor, 

30) 

31 

32_SUPPORTED_BLOCK_SIZES = (32, 128) 

33_ROLE_NAMES = ("activation", "weight") 

34 

35 

36class W4A8Quantizer(Quantizer): 

37 """Quantize MXFP4 weights and MXFP8 activations for native GMM.""" 

38 

39 def __init__( 

40 self, 

41 block_size: int = 32, 

42 ) -> None: 

43 """Create a block-size-specific native W4A8 quantizer.""" 

44 

45 if block_size not in _SUPPORTED_BLOCK_SIZES: 

46 raise ValueError( 

47 f"Native W4A8 block_size must be 32 or 128, got {block_size}." 

48 ) 

49 self.block_size = block_size 

50 

51 @property 

52 def npu_ops(self) -> W4A8NpuOps: 

53 """Return the process-local native W4A8 ops adapter.""" 

54 

55 return _get_w4a8_npu_ops() 

56 

57 @staticmethod 

58 def _validate_group_list( 

59 group_list: Optional[torch.Tensor], 

60 group_list_type: int, 

61 ) -> None: 

62 if group_list_type not in (0, 1): 

63 raise ValueError( 

64 "W4A8 group_list_type must be 0 or 1, " 

65 f"but got {group_list_type}." 

66 ) 

67 if group_list is not None and ( 

68 not isinstance(group_list, torch.Tensor) or group_list.ndim != 1 

69 ): 

70 raise ValueError("W4A8 group_list must be a one-dimensional Tensor.") 

71 

72 @staticmethod 

73 def _boundaries( 

74 group_list: Optional[torch.Tensor], 

75 group_list_type: int, 

76 ) -> Optional[torch.Tensor]: 

77 if group_list is None: 

78 return None 

79 return group_list if group_list_type == 0 else torch.cumsum(group_list, dim=0) 

80 

81 def _quantize_activation( 

82 self, 

83 tensor: torch.Tensor, 

84 *, 

85 rowwise: bool, 

86 colwise: bool, 

87 group_list: Optional[torch.Tensor], 

88 group_list_type: int, 

89 ) -> W4A8Tensor: 

90 row_data = row_scale = col_data = col_scale = None 

91 boundaries = self._boundaries(group_list, group_list_type) 

92 if rowwise: 

93 row_data, row_scale = self.npu_ops.dynamic_mx_quant( 

94 tensor, 

95 axis=-1, 

96 block_size=self.block_size, 

97 quant_dtype=self.npu_ops.activation_dtype, 

98 ) 

99 if self.block_size == 128: 

100 row_scale = self._scale_to_fp32(row_scale).reshape( 

101 tensor.shape[0], 

102 -1, 

103 ) 

104 if colwise: 

105 if self.block_size == 32 and boundaries is not None: 

106 col_data, col_scale = self.npu_ops.grouped_dynamic_mx_quant( 

107 tensor, 

108 boundaries, 

109 block_size=32, 

110 quant_dtype=self.npu_ops.activation_dtype, 

111 ) 

112 else: 

113 col_data, col_scale = self.npu_ops.dynamic_mx_quant( 

114 tensor, 

115 axis=-2, 

116 block_size=self.block_size, 

117 quant_dtype=self.npu_ops.activation_dtype, 

118 ) 

119 if boundaries is not None: 

120 col_scale = transform_grouped_scale( 

121 tensor, 

122 col_scale, 

123 boundaries, 

124 self.block_size, 

125 ) 

126 return W4A8Tensor( 

127 shape=tensor.shape, 

128 dtype=tensor.dtype, 

129 quantizer=self, 

130 row_data=row_data, 

131 row_scale=row_scale, 

132 col_data=col_data, 

133 col_scale=col_scale, 

134 device=tensor.device, 

135 ) 

136 

137 @staticmethod 

138 def _scale_to_fp32(scale: torch.Tensor) -> torch.Tensor: 

139 """Convert an E8M0 exponent tensor to the block-128 FP32 contract.""" 

140 

141 return torch.pow(2.0, scale.to(torch.float32) - 127) 

142 

143 def _quantize_weight( 

144 self, 

145 tensor: torch.Tensor, 

146 *, 

147 rowwise: bool, 

148 colwise: bool, 

149 ) -> W4A8Tensor: 

150 if any(dimension % 32 for dimension in tensor.shape[1:]): 

151 raise ValueError( 

152 "Native MXFP4 weight dimensions must be divisible by 32, " 

153 f"got shape {tuple(tensor.shape)}." 

154 ) 

155 row_data = row_scale = col_data = col_scale = None 

156 if rowwise: 

157 # Forward GMM consumes [E, K/2, N] packed NZ data. Quantizing the 

158 # live [E, N, K] source along K gives that native contract. 

159 row_data, row_scale = self.npu_ops.pack_mxfp4_weight( 

160 tensor.transpose(-2, -1).contiguous(), 

161 block_size=32, 

162 ) 

163 if colwise: 

164 # Dgrad uses the other matrix direction, so its FP4 scales must be 

165 # generated independently from the [E, K, N] source. 

166 col_data, col_scale = self.npu_ops.pack_mxfp4_weight( 

167 tensor, 

168 block_size=32, 

169 ) 

170 return W4A8Tensor( 

171 shape=tensor.shape, 

172 dtype=tensor.dtype, 

173 quantizer=self, 

174 row_data=row_data, 

175 row_scale=row_scale, 

176 col_data=col_data, 

177 col_scale=col_scale, 

178 device=tensor.device, 

179 ) 

180 

181 def quantize( 

182 self, 

183 tensor: torch.Tensor, 

184 *, 

185 rowwise: bool = False, 

186 colwise: bool = False, 

187 group_list: Optional[torch.Tensor] = None, 

188 group_list_type: int = 0, 

189 role: str = "activation", 

190 ) -> W4A8Tensor: 

191 """Quantize an activation or target-layout expert weight. 

192 

193 Args: 

194 tensor: High-precision source tensor. 

195 rowwise: Keep the forward representation. 

196 colwise: Keep the representation required by backward. 

197 group_list: Optional cumulative expert boundaries or token counts. 

198 group_list_type: ``0`` for boundaries and ``1`` for token counts. 

199 role: ``"weight"`` quantizes a 3D MXFP4 expert weight with no 

200 grouping; ``"activation"`` quantizes a 2D activation with 

201 optional grouped boundaries. 

202 """ 

203 

204 if not isinstance(tensor, torch.Tensor) or isinstance(tensor, QuantizedTensor): 

205 raise TypeError("W4A8Quantizer expects a high-precision torch.Tensor.") 

206 if not rowwise and not colwise: 

207 raise ValueError("W4A8Quantizer requires rowwise=True or colwise=True.") 

208 if role not in _ROLE_NAMES: 

209 raise ValueError(f"Unsupported W4A8 quantization role {role!r}.") 

210 self._validate_group_list(group_list, group_list_type) 

211 if role == "weight": 

212 if tensor.ndim != 3: 

213 raise ValueError( 

214 "Native W4A8 weights must be three-dimensional, " 

215 f"got shape {tuple(tensor.shape)}." 

216 ) 

217 if group_list is not None: 

218 raise ValueError("W4A8 weight quantization does not accept group_list.") 

219 return self._quantize_weight( 

220 tensor, 

221 rowwise=rowwise, 

222 colwise=colwise, 

223 ) 

224 if tensor.ndim != 2: 

225 raise ValueError( 

226 "Native W4A8 activations must be two-dimensional, " 

227 f"got shape {tuple(tensor.shape)}." 

228 ) 

229 return self._quantize_activation( 

230 tensor, 

231 rowwise=rowwise, 

232 colwise=colwise, 

233 group_list=group_list, 

234 group_list_type=group_list_type, 

235 ) 

236 

237 def quantize_activation( 

238 self, 

239 tensor: torch.Tensor, 

240 *, 

241 rowwise: bool = False, 

242 colwise: bool = False, 

243 group_list: Optional[torch.Tensor] = None, 

244 group_list_type: int = 0, 

245 ) -> W4A8Tensor: 

246 """Quantize an MXFP8 activation representation.""" 

247 

248 return self.quantize( 

249 tensor, 

250 rowwise=rowwise, 

251 colwise=colwise, 

252 group_list=group_list, 

253 group_list_type=group_list_type, 

254 role="activation", 

255 ) 

256 

257 def quantize_weight( 

258 self, 

259 tensor: torch.Tensor, 

260 *, 

261 rowwise: bool = False, 

262 colwise: bool = False, 

263 ) -> W4A8Tensor: 

264 """Quantize an expert weight into native Packed MXFP4 views.""" 

265 

266 return self.quantize( 

267 tensor, 

268 rowwise=rowwise, 

269 colwise=colwise, 

270 role="weight", 

271 ) 

272 

273 

274__all__ = [ 

275 "W4A8Quantizer", 

276]