Add NVFP4 four-over-six (row-wise) quantization to the training prototype - #7
Draft
wolfcomos wants to merge 2 commits into
Draft
Add NVFP4 four-over-six (row-wise) quantization to the training prototype#7wolfcomos wants to merge 2 commits into
wolfcomos wants to merge 2 commits into
Conversation
…type Four-over-six is an adaptive NVFP4 block-scaling recipe: each 1x16 (or 16x16) block is encoded twice -- the standard map-to-6 encoding and a map-to-4 encoding whose E4M3 block scale is expanded by 1.5x -- and the candidate with the lower MAE/MSE dequantization error is stored. The global scale bound is reduced to 256 by default to leave headroom for the 1.5x expansion, and activations optionally take one FP32 global scale per row (row-wise) instead of per tensor. This adds: - four_over_six_quantize: pure-PyTorch quantizer, transcribed operation-for-operation from the reference CUDA kernels; codes and scales verified bitwise against them on GB200 (384/384 comparisons across 1x16/16x16, MAE/MSE, bound 256/448, per-tensor/row-scaled, rowwise/columnwise, bf16/fp32, 4 shapes). - four_over_six_mm / four_over_six_linear: training autograd function. Forward GEMM operands use four-over-six (activations 1x16, weights 16x16). Backward uses standard-NVFP4 RTNE gradients with the saved columnwise four-over-six operands, except in row-scaled mode where the backward runs in bf16 (a row-scaled four-over-six tensor has no columnwise form, so the quantized wgrad operand cannot be produced). - NVFP4FourOverSixLinear: drop-in nn.Linear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One SM100 kernel behind torchao::four_over_six_quantize_cutedsl, dispatched
from four_over_six_quantize when the input is eligible (CUDA bf16/fp32,
contiguous, C % 64 == 0); everything else silently falls through to the
pure-PyTorch body, which stays intact as the fallback and bitwise oracle.
The kernel is an op-for-op reimplementation of the reference arithmetic with
every rounding pinned by inline PTX: real div.rn.f32 divisions everywhere
(the (block_amax / 6) * S_enc association, S_enc/S_dec, the encode
reciprocals, and the error denominator - never reciprocal-multiply),
cvt.rn.satfinite.e4m3x2.f32 scale casts with exact e4m3->f16->f32 decode,
cvt.rn.satfinite.e2m1x2.f32 FP4 casts with exact cvt.rn.f16x2.e2m1x2 error
dequant, NaN-dropping max.f32 amaxes, strictly sequential per-group error
accumulation in element order, and the exact width-16 shuffle-down error
tree for 16x16 tiles. One (128, 64) tile per CTA, one tile row per thread
(16-lane segments = 16 consecutive rows for the 2D mode), TMA G2S in and
TMA S2G out with row clipping, u32-vectorized scale stores.
Verified bitwise against the pure-torch reference: 1920/1920 over
{mae,mse} x {256,448} x {1x16,16x16} x {per-tensor,row-scaled} x
{bf16,fp32} x 5 shapes x 16 seeds, plus zeros / Inf / subnormal /
near-448 / wide-dynamic / amax==0 special values, and 12/12 bitwise
against TransformerEngine's quantize_4over6_nvfp4 kernels. NaN inputs
follow TE semantics (NaN-dropping amax, NaN -> +6 codes), which the torch
body cannot reproduce since torch.amax propagates NaN; documented in the
op docstring and pinned by a dedicated test. 26-30x over the pure-torch
body on DSV3-671B shapes (1200 MHz SM): 6.2 ms vs 174 ms at 131072x7168.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds the NVFP4 four-over-six adaptive block-scaling recipe to
torchao/prototype/moe_training/nvfp4_training, including the row-wise(per-row global scale) activation variant.
Four-over-six encodes every FP4 block twice — the standard map-to-6 encoding
and a map-to-4 encoding whose E4M3 block scale is expanded by exactly 1.5x —
and stores the candidate (codes and scale) with the lower MAE/MSE
dequantization error; ties select map-to-6. The global scale bound is reduced
to 256 by default so the 1.5x expansion cannot clip E4M3 (256·1.5 = 384 < 448).
The recipe applies to forward-GEMM operands only (never gradients) and targets
RL / post-training flows (no RHT, no stochastic rounding).
New in
nvfp4_training/four_over_six.py:four_over_six_quantize(x, global_amax, *, block="1x16"|"16x16", err_mode="mae"|"mse", e4m3_scale_bound=256|448)— pure-PyTorch quantizer, transcribed operation-for-operation from the
reference CUDA kernels (
quantize_4over6_nvfp4.cuh). A scalarglobal_amaxselects per-tensor scaling; a
(rows,)vector selects the row-wise variant(1x16 blocks only — a row-scaled tensor has no columnwise form).
four_over_six_mm/four_over_six_linear— training autograd function:16x16 four-over-six, via
scaled_mm(BlockWise1x16 + TensorWise).columnwise four-over-six activation/weight codes.
operand cannot exist because per-row scales do not transpose.
NVFP4FourOverSixLinear— drop-innn.Linear.Two load-bearing numeric details (documented in the module docstring):
(block_amax / 6) * S_enc,which rounds differently from the standard path's
block_amax * (S_enc * (1/6))on a fraction of blocks;(double rounding); the two divisions on the selection path use tensor
denominators to get true correctly-rounded FP32 division.
Validation (GB200, SM100)
(packed FP4 codes, E4M3 scale bytes, FP32 global amaxes;
atol=rtol=0)across shapes {128x128, 256x512, 2048x1024, 304x304} × {bf16, fp32} ×
{MAE, MSE} × {bound 256, 448} × {1x16, 16x16} × {per-tensor, row-scaled} ×
{rowwise, columnwise}, plus a 64-seed stress of the tightest case.
512x1024x768; elsewhere ≥ 92 dB SQNR — GEMM reduction-order only, ~76 dB
below the recipe's own quantization error (~17 dB vs bf16).
test/prototype/moe_training/nvfp4_training/test_four_over_six.py:32 passed, 4 skipped (includes an optional bitwise-parity test that runs
when TransformerEngine is importable).
Notes
future work.
(
NVFP4FourOverSixLinearConverter).CuTe DSL kernel (SM100)
Commit 2 adds a CuTe DSL fast path: one kernel behind
torchao::four_over_six_quantize_cutedsl(custom op + fake impl, sofour_over_six_mmstays traceable), dispatched from insidefour_over_six_quantizewhen the input is eligible — CUDA bf16/fp32,contiguous,
C % 64 == 0— and silently falling through to the pure-PyTorchbody otherwise. The PyTorch body is unchanged and remains the fallback and
the bitwise oracle; the public API and the titan-facing contract are
untouched.
Design. One
(128, 64)input tile per CTA (TMA G2S in, TMA S2G packedcodes out, u32-vectorized scale stores), 128 threads with one tile row per
thread, so lanes 0-15 / 16-31 of each warp hold 16 consecutive rows — exactly
the 16-lane segments the 16x16 (2D) mode reduces over. Every load-bearing
rounding is pinned with inline PTX so no compiler lowering can change a bit:
div.rn.f32for every division — the(block_amax / 6) * S_encassociation,
S_enc/S_dec, the encode reciprocalsmin(1/(scale·S_dec), FLT_MAX), and the error denominator6·bound(never a reciprocal multiply);
cvt.rn.satfinite.e4m3x2.f32scale casts with exactcvt.rn.f16x2.e4m3x2decode-back;cvt.rn.satfinite.e2m1x2.f32FP4 casts (NaN → +6) and the exactcvt.rn.f16x2.e2m1x2error-path dequant;max.f32block amaxes (all-NaN group → amax 0),NaN-dropping
min.f32for the 448 caps;order 0..15 with scalar FP32 RN adds; 16x16 tiles reduce their 16 row
errors with the exact width-16 shuffle-down tree
(((e0+e8)+(e4+e12))+((e2+e10)+(e6+e14))) + (((e1+e9)+(e5+e13))+((e3+e11)+(e7+e15)))broadcast from the segment base; strict
err4 < err6selection (ties andNaN errors pick map-to-6).
SASS/PTX audit: 142
div.rn.f32in the 1x16 variant (the exact expectedcount), identical FFMA counts between MAE and MSE variants (all from the
div.rnexpansions — no mul+add contraction in the error path; the MSEdiff*diffis an inlinemul.rn.f32).Bitwise validation (GB200).
atol=rtol=0, uint8views of codes and scales) over {mae, mse} × {256, 448} × {1x16, 16x16} ×
{per-tensor, row-scaled (1x16)} × {bf16, fp32} ×
shapes {128x256, 256x512, 64x1024, 2048x7168, 128x320} × 16 seeds.
subnormals, near-448 block amaxes, wide dynamic range (2^-20..2^20),
amax == 0rows/tensors,amax == inf.path asserted directly against
quantize_4over6_nvfp4.cuhoutput(12/12 bitwise, TE devel container).
(NaN-dropping
fmaxfblock amax; NaN encodes to +6), which the pure-torchbody cannot reproduce because
torch.amaxpropagates NaN into the blockscales of NaN-containing groups. NaN-free groups of a NaN-containing tensor
remain bitwise. Pinned by
test_cutedsl_nan_semantics.Bench (
benchmarks/prototype/moe_training/nvfp4_training/bench_cutedsl_four_over_six_quantize.py,GB200, SM clock 1200 MHz under load — application-clocks cap, 1965 MHz max
boost — HBM 4000 MHz; bf16 input, mae/256; bitwise-checked before timing):
The v1 kernel is correctness-first (single-stage, no pipelining) and is
compute-bound on the exact
div.rnchains; a pipelined/ILP pass can raiseGB/s further if the quantizer ever shows up in profiles.
Follow-up fusions (not wired here). The columnwise pass is still
caller-side (
x.t().contiguous()); a fused row+col kernel is v2. On the GEMMside, cuDNN frontend ships prebuilt CuTe-DSL NVFP4 wrappers
(
gemm_amax_wrapper_sm100,gemm_swiglu_wrapper_sm100, and grouped-MoEvariants, e4m3 vec16 scales) verified working on GB200 — a future fusion can
fold the next layer's amax (and the MLP activation) into the GEMM and feed
this quantizer directly.
🤖 Generated with Claude Code