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

92 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"""Fake-QAT W4A8 quantizer. 

16 

17This quantizer is deliberately separate from ``W4A8Quantizer``. Native W4A8 

18stores packed MXFP4/WeightNz data; fake W4A8 stores only the MXFP8 operands 

19that the all-MXFP8 GMM consumes after an on-the-fly MXFP4 quantize/dequantize 

20step. 

21""" 

22 

23from typing import Optional 

24 

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

26 

27from hyper_parallel.components.quantization.ops.npu_fake_w4a8 import ( 

28 convert_mx_scale_to_axis2, 

29 expand_scale_to_per_row, 

30 fake_mxfp4_to_mxfp8, 

31 transform_grouped_scale, 

32 _get_fake_w4a8_npu_ops, 

33 FakeW4A8NpuOps, 

34) 

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

36from hyper_parallel.components.quantization.tensor import MXFP8Tensor, QuantizedTensor 

37 

38 

39_SUPPORTED_BLOCK_SIZES = (32, 128) 

40 

41 

42class FakeW4A8Quantizer(Quantizer): 

43 """Build fake MXFP4 weights and MXFP8 activation/backward operands.""" 

44 

45 def __init__(self, block_size: int = 32) -> None: 

46 if block_size not in _SUPPORTED_BLOCK_SIZES: 

47 raise ValueError( 

48 f"Fake W4A8 block_size must be 32 or 128, got {block_size}." 

49 ) 

50 self.block_size = block_size 

51 

52 @property 

53 def npu_ops(self) -> FakeW4A8NpuOps: 

54 return _get_fake_w4a8_npu_ops() 

55 

56 @staticmethod 

57 def _boundaries( 

58 group_list: Optional[torch.Tensor], 

59 group_list_type: int, 

60 ) -> Optional[torch.Tensor]: 

61 if group_list is None: 

62 return None 

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

64 

65 @staticmethod 

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

67 return torch.pow(2.0, scale.to(torch.float32) - 127.0) 

68 

69 def _quantize_activation( 

70 self, 

71 tensor: torch.Tensor, 

72 *, 

73 rowwise: bool, 

74 colwise: bool, 

75 group_list: Optional[torch.Tensor], 

76 group_list_type: int, 

77 ) -> MXFP8Tensor: 

78 row_data = row_scale = col_data = col_scale = None 

79 boundaries = self._boundaries(group_list, group_list_type) 

80 if rowwise: 

81 row_data, row_scale = self.npu_ops.dynamic_mx_quant( 

82 tensor, 

83 axis=-1, 

84 block_size=self.block_size, 

85 quant_dtype=self.npu_ops.activation_dtype, 

86 ) 

87 if self.block_size == 128: 

88 row_blocks = (tensor.shape[-1] + self.block_size - 1) // self.block_size 

89 row_scale = self._scale_to_fp32(row_scale) 

90 row_scale = row_scale.reshape(tensor.shape[0], -1)[..., :row_blocks] 

91 if colwise: 

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

93 # This is the G-B grouped backward quantizer. The operator 

94 # requires int32 group indices and has no scale_alg argument. 

95 col_data, col_scale = self.npu_ops.grouped_dynamic_mx_quant( 

96 tensor, 

97 boundaries, 

98 block_size=32, 

99 quant_dtype=self.npu_ops.activation_dtype, 

100 ) 

101 else: 

102 # Block-128 has no grouped_dynamic_mx_quant contract. Quantize 

103 # along the token axis and insert the expert-boundary rows in 

104 # the scale tensor consumed by group_type=2 GMM. 

105 col_data, col_scale = self.npu_ops.dynamic_mx_quant( 

106 tensor, 

107 axis=-2, 

108 block_size=self.block_size, 

109 quant_dtype=self.npu_ops.activation_dtype, 

110 ) 

111 if boundaries is not None: 

112 col_scale = transform_grouped_scale( 

113 tensor, 

114 col_scale, 

115 boundaries, 

116 self.block_size, 

117 ) 

118 return MXFP8Tensor( 

119 shape=tensor.shape, 

120 dtype=tensor.dtype, 

121 quantizer=self, 

122 row_data=row_data, 

123 row_scale=row_scale, 

124 col_data=col_data, 

125 col_scale=col_scale, 

126 device=tensor.device, 

127 ) 

128 

129 def _quantize_weight( 

130 self, 

131 tensor: torch.Tensor, 

132 *, 

133 rowwise: bool, 

134 colwise: bool, 

135 ) -> MXFP8Tensor: 

136 if tensor.ndim != 3: 

137 raise ValueError( 

138 "Fake W4A8 GMM weights must be [experts,K,N], " 

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

140 ) 

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

142 raise ValueError( 

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

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

145 ) 

146 if any(dimension % self.block_size for dimension in tensor.shape[1:]): 

147 raise ValueError( 

148 "Fake MXFP8 GMM dimensions must be divisible by block_size, " 

149 f"got shape {tuple(tensor.shape)}, block_size={self.block_size}." 

150 ) 

151 row_data = row_scale = col_data = col_scale = None 

152 

153 def _quantize_source(source: torch.Tensor): 

154 packed, packed_scale = self.npu_ops.dynamic_mx_quant( 

155 source, 

156 axis=-1, 

157 block_size=32, 

158 quant_dtype=self.npu_ops.weight_dtype, 

159 round_mode="rint", 

160 scale_alg=0, 

161 ) 

162 return fake_mxfp4_to_mxfp8( 

163 packed, 

164 packed_scale, 

165 mx8_block_h=self.block_size, 

166 mx8_block_w=self.block_size, 

167 ) 

168 

169 # Block-128 uses the A5 G-B contract directly: the forward view is 

170 # generated from the [E,K,N] source and dgrad consumes its transposed 

171 # view. Its FP32 square scale grid is exactly what split_item=2 

172 # grouped matmul expects. 

173 if self.block_size == 128: 

174 fp8_data, fp8_grid = _quantize_source(tensor) 

175 if rowwise: 

176 row_data = fp8_data 

177 row_scale = fp8_grid.to(torch.float32) 

178 if colwise: 

179 col_data = fp8_data.transpose(-2, -1).contiguous() 

180 col_scale = fp8_grid.transpose(1, 2).contiguous().to(torch.float32) 

181 return MXFP8Tensor( 

182 shape=tensor.shape, 

183 dtype=tensor.dtype, 

184 quantizer=self, 

185 row_data=row_data, 

186 row_scale=row_scale, 

187 col_data=col_data, 

188 col_scale=col_scale, 

189 device=tensor.device, 

190 ) 

191 

192 # The ordinary MXFP8 GMM contract has two directional views: 

193 # row-wise data/scales for the transposed-right dgrad and column-wise 

194 # data/scales for the forward NN matmul. Quantize each view along its 

195 # actual contracting axis instead of reusing a transposed scale grid; 

196 # the latter makes the A5 kernel see mismatched weight/scale 

197 # transposition flags for NT. 

198 if rowwise: 

199 row_data, row_grid = _quantize_source(tensor) 

200 if self.block_size == 32: 

201 row_scale = expand_scale_to_per_row(row_grid) 

202 else: 

203 row_scale = row_grid.to(torch.float32) 

204 if colwise: 

205 col_fp8, col_grid = _quantize_source(tensor.transpose(-2, -1).contiguous()) 

206 col_data = col_fp8.transpose(-2, -1).contiguous() 

207 if self.block_size == 32: 

208 col_grid = col_grid.permute(0, 2, 1).contiguous() 

209 col_scale = convert_mx_scale_to_axis2( 

210 col_grid, 

211 tuple(tensor.shape), 

212 ) 

213 else: 

214 col_scale = col_grid.transpose(1, 2).contiguous().to(torch.float32) 

215 return MXFP8Tensor( 

216 shape=tensor.shape, 

217 dtype=tensor.dtype, 

218 quantizer=self, 

219 row_data=row_data, 

220 row_scale=row_scale, 

221 col_data=col_data, 

222 col_scale=col_scale, 

223 device=tensor.device, 

224 ) 

225 

226 def quantize( 

227 self, 

228 tensor: torch.Tensor, 

229 *, 

230 rowwise: bool = False, 

231 colwise: bool = False, 

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

233 group_list_type: int = 0, 

234 role: str = "activation", 

235 ) -> MXFP8Tensor: 

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

237 raise TypeError("FakeW4A8Quantizer expects a high-precision Tensor.") 

238 if not rowwise and not colwise: 

239 raise ValueError("FakeW4A8Quantizer requires rowwise or colwise=True.") 

240 if group_list_type not in (0, 1): 

241 raise ValueError("Fake W4A8 group_list_type must be 0 or 1.") 

242 if group_list is not None and ( 

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

244 ): 

245 raise ValueError("Fake W4A8 group_list must be one-dimensional.") 

246 if role == "weight": 

247 if group_list is not None: 

248 raise ValueError("Fake W4A8 weight quantization does not accept groups.") 

249 return self._quantize_weight(tensor, rowwise=rowwise, colwise=colwise) 

250 if role != "activation" or tensor.ndim != 2: 

251 raise ValueError("Fake W4A8 activation quantization expects a 2D Tensor.") 

252 return self._quantize_activation( 

253 tensor, 

254 rowwise=rowwise, 

255 colwise=colwise, 

256 group_list=group_list, 

257 group_list_type=group_list_type, 

258 ) 

259 

260 def quantize_activation(self, tensor: torch.Tensor, **kwargs) -> MXFP8Tensor: 

261 return self.quantize(tensor, role="activation", **kwargs) 

262 

263 def quantize_weight(self, tensor: torch.Tensor, **kwargs) -> MXFP8Tensor: 

264 return self.quantize(tensor, role="weight", **kwargs) 

265 

266 

267__all__ = ["FakeW4A8Quantizer"]