Skip to content

[ROCm][Quantization] Fuse the block Hadamard input rotation into AITER's MXFP4 activation quant - #63272

Draft
mjkvaak-amd wants to merge 3 commits into
vllm-project:mainfrom
mjkvaak-amd:rocm/mxfp4-hadamard32-aiter-fused
Draft

mjkvaak-amd wants to merge 3 commits into
vllm-project:mainfrom
mjkvaak-amd:rocm/mxfp4-hadamard32-aiter-fused

Conversation

@mjkvaak-amd

Copy link
Copy Markdown
Contributor

Overview

Stacked on #62750: only the last commit belongs to this PR. Draft until ROCm/aiter#6734 is in an AITER release that vLLM pins.

#62750 rotates the input of mxfp4_hadamard32 layers in PyTorch (InputHadamardLinearMethod) before the wrapped linear method quantizes it. On AITER's MXFP4 path this adds a separate rotation kernel and a round trip of the activations through memory, which roughly doubles the per-layer time of small GEMMs (see Test Result).

ROCm/aiter#6734 adds hadamard_rotate=True to per_1x32_f4_quant_hip, which rotates each 32-wide block inside the activation quant. This PR uses it:

  • LinearMethodBase.fuse_input_hadamard(layer, size) -> bool, default False. InputHadamardLinearMethod calls it after the wrapped method's process_weights_after_loading; if the method returns True, the wrapper keeps rotating the weight but skips its own input rotation. This is the generic hook promised in [Quantization] Block Hadamard input rotation for linear layers: online mxfp4_hadamard32 and Quark online-rotation checkpoints #62750: any backend whose activation quant can rotate can take over, and nothing else changes.
  • Online MXFP4 (Mxfp4OnlineLinearMethod) and Quark OCP MX (QuarkOCP_MX through QuarkLinearMethod / QuarkScheme) forward the call to their MxFp4LinearKernel.
  • AiterMxfp4LinearKernel accepts when it uses the ASM GEMM and the size is 32, and passes hadamard_rotate=True to gemm_with_dynamic_quant, which forwards it to both HIP quant calls. The Triton branch quantizes with dynamic_mxfp4_quant, which has no rotation, so that path keeps the PyTorch rotation (asserted in the op).
  • hadamard_rotate is only passed to AITER when it is True, so AITER releases without [Bug]: vllm-0.5.3.post1部署Qwen2-72b-instruct-awq模型,刚开始服务正常,但是并发高的时候就报错 #6734 keep working as long as nothing fuses.
  • Fixes the fake implementation of gemm_with_dynamic_quant, whose signature had drifted from the op (missing x_scales, wrong argument order), and adds a FakeTensorMode test for it.

The server logs once per method type: Mxfp4OnlineLinearMethod rotates its input by the 32-wide Hadamard matrix itself.

When the ASM path is used

AiterMxfp4LinearKernel uses the ASM GEMM only for (N, K) present in AITER's tuned a4w4 config (is_asm_fp4_gemm_tuned, M = 1024 and 8192). On Qwen3.8-2.4T at TP8, out_proj (N = 8192, K = 2048) is tuned in AITER today, but in_proj_qkvz (N = 4608, K = 8192) is not, so it runs the Triton GEMM and can't fuse. A tuned config for that shape (26 values of M, all won by ASM kernels, error ratio 0) moves in_proj_qkvz to ASM; it is part of ROCm/aiter#6734. The numbers below include it in every arm.

Test Plan

Test Result

pytest tests/quantization/test_input_hadamard.py tests/quantization/test_online_mxfp4.py \
  tests/quantization/test_quantization_config_args.py tests/v1/determinism/test_mx_linear_batch_invariant.py
80 passed, 4 skipped

Per layer, µs per call (quant + GEMM, CUDA graph, MI355X, AITER a4w4 tuning for in_proj_qkvz applied):

M in_proj_qkvz plain PyTorch rotation Fused out_proj plain PyTorch rotation Fused
1 7.2 15.3 7.4 5.3 10.9 5.0
4 8.6 15.8 7.8 5.1 11.7 5.2
16 9.5 17.1 9.2 5.4 12.6 5.6
64 12.1 21.6 12.6 7.2 24.2 7.5
256 14.7 28.1 15.0 8.0 17.6 8.5
1024 29.3 65.1 29.6 16.9 32.5 17.0
4096 87.6 197.5 90.7 49.5 83.5 46.6
8192 170.6 459.1 175.7 83.5 155.5 88.0

The PyTorch rotation adds 70–170%; the fused rotation adds 0–5%. Compiling the PyTorch rotation (torch.compile) doesn't help. Without the tuned config, in_proj_qkvz runs the Triton GEMM (10.8 µs at M = 1, 120 µs at M = 1024) and keeps the PyTorch rotation.

Fused vs PyTorch rotation: same relative error against the unquantized GEMM to five digits at every M. Up to 0.07% of output elements differ, because the fused quant rotates with an fp32 butterfly while the PyTorch path uses a bf16 matmul with a different summation order. Both round the rotated activations to bf16, and the rare values that round differently can land on the other side of an FP4 rounding boundary. Both paths are deterministic run to run.

TP8 serving (Qwen3.8-2.4T-A95B Quark MXFP4, dummy weights, ISL/OSL 1024/1024, mean of 2 reps):

GDN projections Output tok/s c=4 c=64 c=256 Mean TPOT ms c=4 c=64 c=256
RTN mxfp4 373.0 1807.1 4028.3 10.45 34.09 60.91
mxfp4_hadamard32, PyTorch rotation (#62750) 326.9 (-12.4%) 1694.4 (-6.2%) 3787.7 (-6.0%) 11.94 36.23 64.86
mxfp4_hadamard32, fused (this PR) 371.3 (-0.5%) 1796.6 (-0.6%) 4024.2 (-0.1%) 10.50 34.21 61.09

Repeats differ by up to 1.6%. The server log of the fused arm has the "rotates its input" line; the other two don't. All three arms quantize the same 322 layers.

The rotation is the same as in #62750, so the accuracy results there carry over; the fused and unfused outputs differ only as described above.

Submission Checklist

Add InputHadamardLinearMethod, which wraps any linear method and rotates
the layer input by an orthonormal block Hadamard matrix before the
wrapped apply, in the manner of CompressedTensorsLinearTransformMethod.
The rotation is independent of the weight format and backend.

Two producers use it:
- online quantization: QuantSpec.hadamard_size and the mxfp4_hadamard32
  shorthand (also per layer via --quantization-config.targets); the
  weight is rotated once before the online method quantizes it.
- Quark MXFP4 checkpoints exported with an online rotation: weights are
  stored rotated and the int8 input_rotation tensor is checked to be the
  Sylvester matrix, then dropped.

Paths that have not been validated (online methods other than MXFP4
linear, MoE, requantization, Quark formats other than MXFP4,
non-Sylvester rotations) raise NotImplementedError.

Signed-off-by: Mikko Tukiainen <Mikko.Tukiainen@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@mergify

mergify Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Documentation preview: https://vllm--63272.org.readthedocs.build/en/63272/

mjkvaak-amd and others added 2 commits October 23, 2026 05:54
…reload

- InputHadamardLinearMethod registers itself with
  register_weight_loader_v2_supported_method instead of being listed in
  WEIGHT_LOADER_V2_SUPPORTED.
- QuantSpec validates that hadamard_size is a power of two; drop the
  duplicate checks in the online config and the wrapper's constructor
  (hadamard_matrix keeps its own).
- Test layerwise weight reloading for both the online and the Quark
  checkpoint path; use set_random_seed in the tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Mikko Tukiainen <Mikko.Tukiainen@amd.com>
…R's MXFP4 quant

Add LinearMethodBase.fuse_input_hadamard(layer, size), default False.
InputHadamardLinearMethod calls it after the wrapped method's
process_weights_after_loading and skips its own input rotation when the
method takes it over. Online MXFP4 and Quark OCP MX forward the call to
their MxFp4LinearKernel; AiterMxfp4LinearKernel accepts on the ASM GEMM
path at size 32 and passes hadamard_rotate=True to the HIP activation
quant (ROCm/aiter#6734). hadamard_rotate is only passed to AITER when
set, so older AITER keeps working as long as nothing fuses.

Also fix the fake implementation of gemm_with_dynamic_quant, whose
signature had drifted from the op.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Mikko Tukiainen <Mikko.Tukiainen@amd.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation quantization rocm Related to AMD ROCm

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant