Skip to content

examples: Fast Hadamard + MXFP4 Quant fused kernel - #237

Draft
Mocchibird wants to merge 2 commits into
huawei-csl:mainfrom
Mocchibird:fused-hadamard-quant-a5
Draft

examples: Fast Hadamard + MXFP4 Quant fused kernel#237
Mocchibird wants to merge 2 commits into
huawei-csl:mainfrom
Mocchibird:fused-hadamard-quant-a5

Conversation

@Mocchibird

@Mocchibird Mocchibird commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

fused_hadamard_quant_a5, fused_hadamard_quant_b32_a5: a Hadamard and MXFP4 quantization in one launch

Adds fused bf16 Hadamard + MXFP4 quantization kernels for A5:

  • fused_hadamard_quant_a5: full-row rotation, power-of-two K from 32 to 16384.
  • fused_hadamard_quant_b32_a5: independent 32-element rotations, including non-power-of-two widths such as 11008.

One launch, packed FP4 output with E8M0 scales per 32 elements. Hadamard is unnormalized.

Measured on Ascend950PR_9589, M=16384. Times in µs.

Kernel K Unfused Fused Speedup
Full-row 4096 297.7 121.7 2.45x
Full-row 16384 1209.9 493.8 2.45x
Block-32 4096 293.4 119.7 2.45x
Block-32 16384 1206.1 474.6 2.54x

At 64Mi elements, both reach ~1.4 TB/s. Input d2d copy takes 1.55–1.65x as long.

Benchmarks

Tests: 43 passed for full-row, 42 for block-32.

Requires CANN with MXFP4 support: 9.1.0 / 9.2.0; 9.0.0 is unsupported.

Two examples, each fusing a Hadamard rotation with MXFP4 quantization into a
single dav-c310-vec launch. Unfused, the butterfly writes the rotated tile out
and the quantizer reads it straight back; fused, the tile never leaves UB and
only the nibbles and scales are written, which is 6.53 B/element against 2.53.

fused_hadamard_quant_a5 rotates the whole row. Sylvester factors as
H_K = H_(K/256) (x) H_256, so the transform runs in two phases: an order-256
transform inside each 256-element window, then log2(K/256) stages pairing
windows elementwise. Neither phase holds more than one window in registers, so
the row width does not enter the register budget -- ten powers of two from 32 to
16384. A single-phase butterfly keeping a whole row in registers stops at 4096.
2.45x over the same work as two launches, and 1.55-1.58x a device-to-device
copy at 1382-1398 GB/s, flat within 1.4 us across a 16x range of K.

fused_hadamard_quant_b32_a5 rotates independent 32-element blocks, so the
rotation is 32 wide however long the row is. K therefore carries neither a
power-of-two constraint nor a register-budget limit: 28 widths from 32 to 16384,
covering 4096 and the 11008-style widths together. 2.45-2.54x over two launches
at 1448-1450 GB/s. Its 32-wide rotation also matches MXFP4's scale granularity,
and on heavy-tailed data it measured 4-5% lower quantization error than the
full-row rotation at K=4096.

K is a template parameter in both, so one .so per example holds an instantiation
per width and the launcher dispatches on it. Rows per tile is closed-form rather
than a countdown template recursion, which at K=32 would instantiate 768 deep.
The butterfly is the unnormalised Sylvester matrix in both, sqrt(K) and sqrt(32)
respectively, left to the caller because MXFP4's E8M0 scale is a power of two
and cannot always absorb it.

43 and 48 tests. The full-row reference is an independent strided transform,
itself pinned against an explicitly constructed Sylvester matrix, since that
matrix is a gigabyte at K=16384.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qbbp2Kjzv1AhUkU6w1mi4
@Mocchibird Mocchibird changed the title examples: add fused_hadamard_quant_a5 and fused_hadamard_quant_b32_a5 (Hadamard + MXFP4 in one launch) examples: Fast Hadamard + MXFP4 Quant fused kernel Sep 9, 2026
Prospector runs pylint, and a coded suppression only reaches the tool
that owns the code: `# noqa: F401` silences pyflakes but leaves pylint's
unused-import, and `# noqa: E402` silences pycodestyle but leaves
wrong-import-position. A bare `# noqa` covers every tool prospector runs,
which is what the kernels already in the repo use
(fast_hadamard/standard/bench_hadamard.py).

  torch_npu / sibling imports  coded noqa -> bare noqa
  benchmark.py record builders dict(...) -> a dict literal
  the `seeded` fixture         autouse, so it is not an unused argument
  row_quantum()                deleted; it had no caller in either tree

The seeded fixture now applies to every test in its module rather than
the eight that named it, so torch.npu.set_device(0) runs for all of them.

Verified: prospector reports 0 messages on the whole tree, and on an
Ascend950PR_9589 with CANN 9.1.0 both suites still pass, 43 and 48 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qbbp2Kjzv1AhUkU6w1mi4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant