feat: mx9: add fused MX9 quantization kernel with pack/unpack - #33
feat: mx9: add fused MX9 quantization kernel with pack/unpack#33Jiarwang77 wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds real packed MX9 quantization support via a fused Triton pack/unpack kernel, enabling a compact 3-part representation (q / max_exp / prime) intended for inference/storage compression, along with an extensive unit test suite validating bit-exact round-trips against a clamp-127 reference.
Changes:
- Introduces
convert_to_mx9/convert_from_mx9Triton implementation for real packed MX9 (int8 q + E8M0-like exponent byte + prime bitmap). - Adds comprehensive MX9 pack/unpack tests (format, intermediates, round-trip, edge cases, invalid inputs).
- Adds
alto.kernels.mxpackage initializer and documentation.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
alto/kernels/mx/mx9_quantization.py |
New Triton MX9 pack/unpack kernel + Python wrappers for producing/consuming the packed 3-tensor representation. |
tests/unittest/mx9_mx6/test_mx9_quantization.py |
New end-to-end and component-level tests validating storage format and bit-exact round-trip behavior. |
alto/kernels/mx/__init__.py |
Declares the MX “real kernel” package and documents its relationship to the fake-quant reference path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| assert out_dtype in _TORCH_TO_TL | ||
| assert block_size == 16, f"block_size only supports 16, got {block_size}" | ||
|
|
| def test_max_exp_biased_range(): | ||
| # E8M0 stores true exponent + 127; finite fp32 true exponent in [-126, 127], | ||
| # biased to [1, 254]; should never be 0 or 255. | ||
| x = _rand((16, 64), torch.float32).cuda() | ||
| _, max_exp, _ = convert_to_mx9(x, block_size=16) | ||
| assert max_exp.min().item() >= 1 | ||
| assert max_exp.max().item() <= 254 |
| # Copyright (c) 2026 Advanced Micro Devices, Inc. | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| """MX real (fused Triton) kernels. |
There was a problem hiding this comment.
This is misleading. We don't have real mx gemm now.
|
|
||
| # Q part of QDQ operates on the *original* x (NaN will produce garbage, see below). | ||
| xf = x.to(tl.float32) | ||
| q = _round_half_even(xf / scale) |
There was a problem hiding this comment.
is it possible to use triton.language.div_rn?
There was a problem hiding this comment.
Yes, I found "PyTorch eager uses the equivalent of Triton's div_rn" in torch inductor. It can be more accurate.
- patcher: format=="mx9" now dispatches to convert_to/from_mx9 (packed Triton kernel) instead of mx9_fake_quantize (pure PyTorch). Validated: LLaMA-3.2-1B wikitext loss 2.1141 (== fake-quant 2.1140). - mx9_quantization: use tl.div_rn for IEEE round-nearest division (fixes potential 2-ULP fast-div rounding at .5 boundaries); add dtype asserts on q/max_exp/prime in convert_from_mx9. - kernels/mx/__init__: update docstring.
548d1e5 to
db210c0
Compare
207fb94 to
087a876
Compare
087a876 to
a505eb5
Compare
| internally. The original shape / axis are not stored, so the caller must pass | ||
| them back to convert_from_mx6. | ||
| """ | ||
| return _convert_to_mx_host( |
There was a problem hiding this comment.
- Normally "host" refers to functions executed on CPU but here
_convert_to_mx_hostlauches a triton kernel that requires GPU. Better change this to "device" or "gpu". blocks_per_programis a tunable parameter and should not be exposed to end-user. Keep it hidden for now would be fine.
| call. They are not stored in ``packed``; a mismatch may silently reorder | ||
| values when it happens to produce the same block count. | ||
| """ | ||
| return _convert_from_mx_host( |
| """ | ||
| return _convert_to_mx_host( | ||
| data_hp, | ||
| kernel=_convert_to_mx6_kernel, |
There was a problem hiding this comment.
I don't think we will have another equivalent kernel in the near future. Just launch this kernel inside your dispatch function. No need to pass it as an argument.
| return _convert_to_mx_host( | ||
| data_hp, | ||
| kernel=_convert_to_mx6_kernel, | ||
| n_packed_bytes=_mx6_packed_bytes(block_size), |
There was a problem hiding this comment.
we have block_size passed as an argument. Why not calculating it inside the dispatch function?
There was a problem hiding this comment.
On another thought, block_size, quant_bit, prime_group are all fixed parameters for MX6 (or MX9). Just signal whether you want MX6 or MX9 ("target_dtype=mx6/mx9") would be sufficient.
| data_hp, | ||
| kernel=_convert_to_mx6_kernel, | ||
| n_packed_bytes=_mx6_packed_bytes(block_size), | ||
| fmt="mx6_quantization", |
There was a problem hiding this comment.
fmt="mx6_quantization" has no actual function (only used for error logging) here. Please read the AI-generated code before accepting it.
| PRIME_GROUP, | ||
| QUANT_BIT, | ||
| ) | ||
| out_offs = blk[:, None] * BLOCK_SIZE + col[None, :] |
There was a problem hiding this comment.
same as above. no strides for output tensors?
|
|
||
| # E8M0 bias: store the true exponent + 127. | ||
| e_store = (max_exp + 127).to(tl.uint8) | ||
| tl.store(packed_ptr + blk * N_PACKED_BYTES, e_store, mask=blk_mask) |
There was a problem hiding this comment.
same as MX6. no strides for output tensors.
| PRIME_GROUP, | ||
| QUANT_BIT, | ||
| ) | ||
| out_offs = blk[:, None] * BLOCK_SIZE + col[None, :] |
There was a problem hiding this comment.
same as MX6. no strides for output tensors.
| packed = convert_to_mx6(x, block_size=(args.group_size or BLOCK_SIZE)) | ||
| return convert_from_mx6(packed, x.dtype, x.shape, | ||
| block_size=(args.group_size or BLOCK_SIZE)) | ||
| return original_fake_quantize(x, scale, zero_point, args, g_idx, global_scale) |
There was a problem hiding this comment.
Is this path still working? original_fake_quantize
| export LOG_RANK=${LOG_RANK:-0} | ||
| TRAIN_FILE=${TRAIN_FILE:-"alto.train"} | ||
| MODULE=${MODULE:-"llama3"} | ||
| CONFIG=${CONFIG:-"llama3_1b_mx6_wa"} |
There was a problem hiding this comment.
Please help us refactor all these example scripts.
Instead of copying examples/run.sh, re-use the script like this:
NGPU=1 MODULE=llama3 CONFIG=llama3_1b_mx6_wa ./examples/run.sh --hf_assets_path xxxx --validator.steps xxx
Summary
Add a fused Triton kernel for real packed MX9 quantization (
convert_to_mx9/convert_from_mx9), complementing the existing fake-quant path inalto.modifiers.quantization.mx.Unlike the fake-quant path (which outputs dequantized bf16/fp32 for training), this kernel produces a compact three-part packed representation:
q—[n_blocks, 16]int8: per-element quantized values, symmetric clamp ±127.max_exp—[n_blocks]uint8: per-block shared exponent in E8M0 format (true exponent + 127 bias).prime—[n_blocks, 1]uint8: per-block prime bitmap, 1 bit per pair of 2 elements, byte-packed.This achieves 9 bits/element (8b value + 0.5b amortized exponent + 0.5b prime), enabling real weight compression for inference/storage.
Key design choices
_t_exponent.Test coverage (70 tests, all passing)
Test plan
pytest tests/unittest/mx9_mx6/test_mx9_quantization.py— 70 passed (ROCm/HIP, MI300X)@triton_op+register_fakefortorch.compileintegration