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

65 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"""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.config import ( 

22 GROUPED_NDIM, 

23 GROUP_LIST_COUNTS, 

24 GROUP_LIST_OFFSETS, 

25 MATRIX_NDIM, 

26 MX_BLOCK_SIZE_32, 

27 validate_mx_block_size, 

28) 

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

30 W4A8NpuOps, 

31 _get_w4a8_npu_ops, 

32) 

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

34from hyper_parallel.components.quantization.tensor import ( 

35 QuantizedTensor, 

36 W4A8Tensor, 

37) 

38 

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

40 

41 

42class W4A8Quantizer(Quantizer): 

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

44 

45 def __init__( 

46 self, 

47 block_size: int = MX_BLOCK_SIZE_32, 

48 ) -> None: 

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

50 

51 self.block_size = validate_mx_block_size(block_size) 

52 

53 @property 

54 def npu_ops(self) -> W4A8NpuOps: 

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

56 

57 return _get_w4a8_npu_ops() 

58 

59 @staticmethod 

60 def _validate_group_list( 

61 group_list: Optional[torch.Tensor], 

62 group_list_type: int, 

63 ) -> None: 

64 """Validate the grouped activation metadata representation.""" 

65 if group_list_type not in (GROUP_LIST_OFFSETS, GROUP_LIST_COUNTS): 

66 raise ValueError( 

67 "W4A8 group_list_type must be 0 or 1, " 

68 f"but got {group_list_type}." 

69 ) 

70 if group_list is not None and ( 

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

72 ): 

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

74 

75 @staticmethod 

76 def _boundaries( 

77 group_list: Optional[torch.Tensor], 

78 group_list_type: int, 

79 ) -> Optional[torch.Tensor]: 

80 if group_list is None: 

81 return None 

82 return ( 

83 group_list 

84 if group_list_type == GROUP_LIST_OFFSETS 

85 else torch.cumsum(group_list, dim=0) 

86 ) 

87 

88 def _quantize_activation( 

89 self, 

90 tensor: torch.Tensor, 

91 *, 

92 rowwise: bool, 

93 colwise: bool, 

94 group_list: Optional[torch.Tensor], 

95 group_list_type: int, 

96 ) -> W4A8Tensor: 

97 """Quantize activations into the native W4A8 directions.""" 

98 row_data = row_scale = col_data = col_scale = None 

99 boundaries = self._boundaries(group_list, group_list_type) 

100 if rowwise: 

101 row_data, row_scale = self.npu_ops.dynamic_mx_quant( 

102 tensor, 

103 axis=-1, 

104 block_size=self.block_size, 

105 quant_dtype=self.npu_ops.activation_dtype, 

106 ) 

107 if colwise: 

108 if boundaries is not None: 

109 col_data, col_scale = self.npu_ops.grouped_dynamic_mx_quant( 

110 tensor, 

111 boundaries, 

112 block_size=MX_BLOCK_SIZE_32, 

113 quant_dtype=self.npu_ops.activation_dtype, 

114 ) 

115 else: 

116 col_data, col_scale = self.npu_ops.dynamic_mx_quant( 

117 tensor, 

118 axis=-2, 

119 block_size=self.block_size, 

120 quant_dtype=self.npu_ops.activation_dtype, 

121 ) 

122 return W4A8Tensor( 

123 shape=tensor.shape, 

124 dtype=tensor.dtype, 

125 quantizer=self, 

126 row_data=row_data, 

127 row_scale=row_scale, 

128 col_data=col_data, 

129 col_scale=col_scale, 

130 device=tensor.device, 

131 ) 

132 

133 def _quantize_weight( 

134 self, 

135 tensor: torch.Tensor, 

136 *, 

137 rowwise: bool, 

138 colwise: bool, 

139 ) -> W4A8Tensor: 

140 """Pack master weights into the native MXFP4 representation.""" 

141 if any(dimension % MX_BLOCK_SIZE_32 for dimension in tensor.shape[1:]): 

142 raise ValueError( 

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

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

145 ) 

146 row_data = row_scale = col_data = col_scale = None 

147 if rowwise: 

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

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

150 row_data, row_scale = self.npu_ops.pack_mxfp4_weight( 

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

152 block_size=MX_BLOCK_SIZE_32, 

153 ) 

154 if colwise: 

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

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

157 col_data, col_scale = self.npu_ops.pack_mxfp4_weight( 

158 tensor, 

159 block_size=MX_BLOCK_SIZE_32, 

160 ) 

161 return W4A8Tensor( 

162 shape=tensor.shape, 

163 dtype=tensor.dtype, 

164 quantizer=self, 

165 row_data=row_data, 

166 row_scale=row_scale, 

167 col_data=col_data, 

168 col_scale=col_scale, 

169 device=tensor.device, 

170 ) 

171 

172 def quantize( 

173 self, 

174 tensor: torch.Tensor, 

175 *, 

176 rowwise: bool = False, 

177 colwise: bool = False, 

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

179 group_list_type: int = GROUP_LIST_OFFSETS, 

180 role: str = "activation", 

181 ) -> W4A8Tensor: 

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

183 

184 Args: 

185 tensor: High-precision source tensor. 

186 rowwise: Keep the forward representation. 

187 colwise: Keep the representation required by backward. 

188 group_list: Optional cumulative expert boundaries or token counts. 

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

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

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

192 optional grouped boundaries. 

193 """ 

194 

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

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

197 if not rowwise and not colwise: 

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

199 if role not in _ROLE_NAMES: 

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

201 self._validate_group_list(group_list, group_list_type) 

202 if role == "weight": 

203 if tensor.ndim != GROUPED_NDIM: 

204 raise ValueError( 

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

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

207 ) 

208 if group_list is not None: 

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

210 return self._quantize_weight( 

211 tensor, 

212 rowwise=rowwise, 

213 colwise=colwise, 

214 ) 

215 if tensor.ndim != MATRIX_NDIM: 

216 raise ValueError( 

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

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

219 ) 

220 return self._quantize_activation( 

221 tensor, 

222 rowwise=rowwise, 

223 colwise=colwise, 

224 group_list=group_list, 

225 group_list_type=group_list_type, 

226 ) 

227 

228 def quantize_activation( 

229 self, 

230 tensor: torch.Tensor, 

231 *, 

232 rowwise: bool = False, 

233 colwise: bool = False, 

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

235 group_list_type: int = GROUP_LIST_OFFSETS, 

236 ) -> W4A8Tensor: 

237 """Quantize an MXFP8 activation representation.""" 

238 

239 return self.quantize( 

240 tensor, 

241 rowwise=rowwise, 

242 colwise=colwise, 

243 group_list=group_list, 

244 group_list_type=group_list_type, 

245 role="activation", 

246 ) 

247 

248 def quantize_weight( 

249 self, 

250 tensor: torch.Tensor, 

251 *, 

252 rowwise: bool = False, 

253 colwise: bool = False, 

254 ) -> W4A8Tensor: 

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

256 

257 return self.quantize( 

258 tensor, 

259 rowwise=rowwise, 

260 colwise=colwise, 

261 role="weight", 

262 ) 

263 

264 

265__all__ = [ 

266 "W4A8Quantizer", 

267]