|
23 | 23 | emit_quantized_gather, |
24 | 24 | emit_stop_position, |
25 | 25 | parse_dequant_int4_node, |
| 26 | + parse_dequant_mx_node, |
26 | 27 | parse_dequant_node, |
27 | 28 | parse_dequant_nvfp4_node, |
28 | 29 | regroup_affine_scales, |
@@ -1166,6 +1167,196 @@ def __call__(self, P, n): |
1166 | 1167 | return out |
1167 | 1168 |
|
1168 | 1169 |
|
| 1170 | +def _mx_mlx_mode_bits(elem_dtype: torch.dtype): |
| 1171 | + """Map an MX element dtype to the MLX (mode, bits). Rejects unsupported formats. |
| 1172 | +
|
| 1173 | + MLX's block-scaled kernels only implement E4M3 elements (mxfp8). Other MX |
| 1174 | + element encodings (e5m2, fp4/fp6, ...) are rejected rather than silently |
| 1175 | + mis-lowered. |
| 1176 | + """ |
| 1177 | + if elem_dtype == torch.float8_e4m3fn: |
| 1178 | + return "mxfp8", 8 |
| 1179 | + raise ValueError( |
| 1180 | + f"MLX backend does not support MX element dtype {elem_dtype}; " |
| 1181 | + "only float8_e4m3fn (mxfp8) is supported." |
| 1182 | + ) |
| 1183 | + |
| 1184 | + |
| 1185 | +def _mx_pack_for_mlx(P, qdata_node, scale_node): |
| 1186 | + """Reinterpret ExportableMXTensor's native FP8/E8M0 storage to MLX layout. |
| 1187 | +
|
| 1188 | + ExportableMXTensor stores ``qdata`` as native FP8 (one value per byte) and |
| 1189 | + ``scale`` as ``float8_e8m0fnu``; MLX's kernel wants ``qdata`` as uint32 |
| 1190 | + (4 FP8 codes per word) and ``scale`` as uint8. Both conversions are pure bit |
| 1191 | + reinterpretations (``view``), registered as MLX constants. |
| 1192 | + """ |
| 1193 | + qdata_target, qdata = P.get_placeholder_target_and_tensor(qdata_node) |
| 1194 | + scale_target, scale = P.get_placeholder_target_and_tensor(scale_node) |
| 1195 | + w = qdata.contiguous().view(torch.uint8).view(torch.uint32) |
| 1196 | + sc = scale.contiguous().view(torch.uint8) |
| 1197 | + w_slot = P.make_or_get_constant(f"{qdata_target}_mx_packed", w) |
| 1198 | + scale_slot = P.make_or_get_constant(f"{scale_target}_mx_u8", sc) |
| 1199 | + return w_slot, scale_slot |
| 1200 | + |
| 1201 | + |
| 1202 | +@REGISTRY.register_pattern(name="MX_QUANTIZED_LINEAR") |
| 1203 | +class MXQuantizedLinearHandler(PatternHandler): |
| 1204 | + """Fuse dequantize_mx + linear into QuantizedMatmulNode for the MX format. |
| 1205 | +
|
| 1206 | + Matches: |
| 1207 | + linear(x, dequantize_mx(qdata, scale, elem_dtype, block_size), bias) |
| 1208 | +
|
| 1209 | + Emits: |
| 1210 | + QuantizedMatmulNode [→ AddNode(bias)] [→ AsTypeNode] |
| 1211 | +
|
| 1212 | + The element dtype is mapped to the MLX ``mode``/``bits`` (rejecting formats |
| 1213 | + MLX can't lower). MX has no per-tensor scale, so no MultiplyNode is emitted. |
| 1214 | + """ |
| 1215 | + |
| 1216 | + def __init__(self, head, body, qdata, scale, elem_dtype, block_size, output_dtype): |
| 1217 | + super().__init__(head, body) |
| 1218 | + self.qdata = qdata |
| 1219 | + self.scale = scale |
| 1220 | + self.elem_dtype = elem_dtype |
| 1221 | + self.block_size = block_size |
| 1222 | + self.output_dtype = output_dtype |
| 1223 | + |
| 1224 | + @classmethod |
| 1225 | + def maybe_create(cls, ep, head): |
| 1226 | + if not match_target(head, torch.ops.aten.linear.default): |
| 1227 | + return None |
| 1228 | + x, dequant = head.args[0:2] |
| 1229 | + if not isinstance(dequant, Node): |
| 1230 | + return None |
| 1231 | + if not has_single_user(dequant): |
| 1232 | + return None |
| 1233 | + parsed = parse_dequant_mx_node(dequant) |
| 1234 | + if parsed is None: |
| 1235 | + return None |
| 1236 | + qdata, scale, elem_dtype, block_size, output_dtype = parsed |
| 1237 | + return cls(head, [dequant], qdata, scale, elem_dtype, block_size, output_dtype) |
| 1238 | + |
| 1239 | + def __call__(self, P, n): |
| 1240 | + assert n == self.head |
| 1241 | + |
| 1242 | + x_node, w_node = n.args[0:2] |
| 1243 | + b_node = n.args[2] if len(n.args) > 2 else None |
| 1244 | + |
| 1245 | + mode, bits = _mx_mlx_mode_bits(self.elem_dtype) |
| 1246 | + needs_cast = x_node.meta["val"].dtype != self.output_dtype |
| 1247 | + has_bias = b_node is not None |
| 1248 | + |
| 1249 | + w, scales = _mx_pack_for_mlx(P, self.qdata, self.scale) |
| 1250 | + x, bias = P.slot_map([x_node, b_node]) |
| 1251 | + |
| 1252 | + out = P.make_or_get_slot(n) |
| 1253 | + P.emit( |
| 1254 | + QuantizedMatmulNode( |
| 1255 | + x=P.slot_to_tid(x), |
| 1256 | + w=P.slot_to_tid(w), |
| 1257 | + scales=P.slot_to_tid(scales), |
| 1258 | + out=P.slot_to_tid(out), |
| 1259 | + biases=None, |
| 1260 | + group_size=self.block_size, |
| 1261 | + bits=bits, |
| 1262 | + mode=mode, |
| 1263 | + transpose=True, |
| 1264 | + ) |
| 1265 | + ) |
| 1266 | + |
| 1267 | + if has_bias: |
| 1268 | + P.emit( |
| 1269 | + AddNode( |
| 1270 | + a=P.slot_to_tid(out), |
| 1271 | + b=P.slot_to_tid(bias), |
| 1272 | + out=P.slot_to_tid(out), |
| 1273 | + ) |
| 1274 | + ) |
| 1275 | + |
| 1276 | + if needs_cast: |
| 1277 | + P.emit( |
| 1278 | + AsTypeNode( |
| 1279 | + x=P.slot_to_tid(out), |
| 1280 | + out=P.slot_to_tid(out), |
| 1281 | + scalar_type=torch_dtype_to_scalar_type(self.output_dtype), |
| 1282 | + ) |
| 1283 | + ) |
| 1284 | + |
| 1285 | + return out |
| 1286 | + |
| 1287 | + |
| 1288 | +@REGISTRY.register_pattern(name="MX_QUANTIZED_EMBEDDING") |
| 1289 | +class MXQuantizedEmbeddingHandler(PatternHandler): |
| 1290 | + """Fuse dequantize_mx + embedding into gather + DequantizeNode for the MX format. |
| 1291 | +
|
| 1292 | + Matches: |
| 1293 | + embedding(dequantize_mx(qdata, scale, elem_dtype, block_size), indices) |
| 1294 | +
|
| 1295 | + Emits: |
| 1296 | + TakeNode(qdata) → TakeNode(scales) → DequantizeNode [→ AsTypeNode] |
| 1297 | + """ |
| 1298 | + |
| 1299 | + def __init__(self, head, body, qdata, scale, elem_dtype, block_size, output_dtype): |
| 1300 | + super().__init__(head, body) |
| 1301 | + self.qdata = qdata |
| 1302 | + self.scale = scale |
| 1303 | + self.elem_dtype = elem_dtype |
| 1304 | + self.block_size = block_size |
| 1305 | + self.output_dtype = output_dtype |
| 1306 | + |
| 1307 | + @classmethod |
| 1308 | + def maybe_create(cls, ep, head): |
| 1309 | + if not match_target(head, torch.ops.aten.embedding.default): |
| 1310 | + return None |
| 1311 | + |
| 1312 | + w, x = head.args[0:2] |
| 1313 | + if not isinstance(w, Node): |
| 1314 | + return None |
| 1315 | + if not has_single_user(w): |
| 1316 | + return None |
| 1317 | + parsed = parse_dequant_mx_node(w) |
| 1318 | + if parsed is None: |
| 1319 | + return None |
| 1320 | + qdata, scale, elem_dtype, block_size, output_dtype = parsed |
| 1321 | + return cls(head, [w], qdata, scale, elem_dtype, block_size, output_dtype) |
| 1322 | + |
| 1323 | + def __call__(self, P: MLXProgramBuilder, n: Node) -> Slot: |
| 1324 | + assert n == self.head |
| 1325 | + w_node, x_node = n.args[0:2] |
| 1326 | + |
| 1327 | + mode, bits = _mx_mlx_mode_bits(self.elem_dtype) |
| 1328 | + x_dtype = x_node.meta["val"].dtype |
| 1329 | + needs_cast = self.output_dtype != x_dtype |
| 1330 | + |
| 1331 | + qdata_slot, scales_slot = _mx_pack_for_mlx(P, self.qdata, self.scale) |
| 1332 | + (x,) = P.slot_map([x_node]) |
| 1333 | + |
| 1334 | + out = P.make_or_get_slot(n) |
| 1335 | + emit_quantized_gather( |
| 1336 | + P, |
| 1337 | + out, |
| 1338 | + x, |
| 1339 | + qdata_slot, |
| 1340 | + scales_slot, |
| 1341 | + None, |
| 1342 | + group_size=self.block_size, |
| 1343 | + bits=bits, |
| 1344 | + mode=mode, |
| 1345 | + out_dtype=self.output_dtype, |
| 1346 | + ) |
| 1347 | + |
| 1348 | + if needs_cast: |
| 1349 | + P.emit( |
| 1350 | + AsTypeNode( |
| 1351 | + x=P.slot_to_tid(out), |
| 1352 | + out=P.slot_to_tid(out), |
| 1353 | + scalar_type=torch_dtype_to_scalar_type(self.output_dtype), |
| 1354 | + ) |
| 1355 | + ) |
| 1356 | + |
| 1357 | + return out |
| 1358 | + |
| 1359 | + |
1169 | 1360 | @REGISTRY.register_pattern(name="INT4_QUANTIZED_LINEAR") |
1170 | 1361 | class Int4QuantizedLinearHandler(PatternHandler): |
1171 | 1362 | """Fuse dequantize_int4_tensor + linear into QuantizedMatmulNode(mode="affine"). |
|
0 commit comments