Repository
nvfp4-sm120
NVFP4 (4-bit float) training + custom CUTLASS FP4 GEMM on RTX PRO 6000 Blackwell (sm_120), where Transformer Engine cannot run
- config
- held-out acc
- bf16 reference
- 100%
- NVFP4 without SR/RHT (the only path TE runs on sm_120)
- 1.9% (stalls)
- quantizer
- time
Specs
- configheld-out acc
- bf16 reference100%
- NVFP4 without SR/RHT (the only path TE runs on sm_120)1.9% (stalls)
- quantizertime
- pure torch3.7 ms
- fused Triton0.45 ms
README
nvfp4-sm120
Full NVFP4 training (NVIDIA's recipe, arXiv 2509.25149
— 2D weight scaling + Random Hadamard Transform + stochastic rounding) running on
native FP4 tensor cores on sm_120 (RTX PRO 6000 Blackwell Workstation / GeForce
Blackwell), where NVIDIA's Transformer Engine cannot.
Why this exists
On sm_120, TE 2.15's fused NVFP4 path crashes: its RHT/SR mega-kernel requests dynamic
shared memory sized for the sm_100 datacenter budget (~232 KB), exceeding sm_120's
101376-byte opt-in cap (cudaFuncSetAttribute → "invalid argument"). sm_120 also lacks
the hardware stochastic-rounding cast cvt.rs.*.e2m1 (ptxas: "Feature '.rs' not
supported"). Filed upstream as NVIDIA/TransformerEngine#3062.
Approach: decompose. Own the quantization (software SR + RHT, in our own kernels) and
send the quantized operands to a native FP4 GEMM via torch._scaled_mm
(BlockWise1x16 + SWIZZLE_32_4_4, through torchao's _addmm_nvfp4_dispatch). sm_120 does
have the round-to-nearest E2M1 cast and the FP4 tensor-core GEMM — those are all we need.
Layout
nvfp4_validate.py Reference fake-quant NVFP4 sim (all 4 techniques) + addition dataset.
nvfp4_gemm.py quant_nvfp4 (two-level E2M1/E4M3 quant) + fp4_matmul (torch._scaled_mm).
nvfp4_triton_quant.py Fused Triton quantizer: scale + SR-round + pack + in-kernel RHT.
nvfp4_cuda.py CUDA quantizer using the hardware E2M1 cast (fastest); + fp4_mm_preqB.
nvfp4_train.py FP4Linear autograd (Fprop RNE / Dgrad SR / Wgrad RHT+SR) + char-LM trainer.
nvfp4_triton.py Standalone Triton software-SR quantizer (the first correctness probe).
benchmarks/ Throughput + roofline profiling scripts.
tests/ Numerical-correctness and convergence checks.
te/ Transformer Engine comparison: TE trainer, benches, sm_120 degrade patch.
scaling/ Real-text (OpenWebText) Chinchilla-style L(N) study: BF16 vs NVFP4.
results/ scaling_results.jsonl + scaling_law.png (the figure).
Backend for nvfp4_train.py is selected by env var (default: pure-torch quant):
NVFP4_CUDA=1 (hardware-cast CUDA, fastest) · NVFP4_FUSED=1 (fused Triton) ·
NVFP4_AMORTIZE=1 (with NVFP4_CUDA=1, cache per-step weight quant).
Quickstart
# sm_120 box with CUDA 13 toolkit (nvcc), torch+cu13, torchao, triton
python nvfp4_validate.py # reference sim sanity
NVFP4_CUDA=1 python nvfp4_train.py --steps 1500 # real FP4 training (JIT-builds the CUDA ext)
python tests/cuda_val.py # numerics: byte-match, SR unbiasedness, GEMM rel-err
python benchmarks/cuda_prof.py # FP4 linear vs bf16 timing
Results (RTX PRO 6000 Blackwell, sm_120; all read from green runs)
Numerics. Custom quant matches the reference bit-for-bit on round-to-nearest
(byte-match ~100%), the FP4 GEMM holds ~0.134 relative error vs fp32, and stochastic
rounding is unbiased (mean over 32 draws drops error 0.134 → ~0.038).
GEMM throughput (benchmarks/nvfp4_bench.py): native FP4 sustains ~1100 TFLOPS at
16384³ — ~3x bf16, ~55% of the dense FP4 peak (the gap is sm_120 cuBLAS maturity).
Training (3.55M-param Nemotron-style Transformer, 3-digit-addition char-LM with
held-out pairs as a generalization probe). The full recipe on real FP4 matches bf16;
without SR/RHT it stalls — the stabilizers are load-bearing:
| config | held-out acc | val loss |
|---|---|---|
| bf16 reference | 100% | 0.965 |
| NVFP4 full recipe (SR+RHT), all blocks | 100% | 0.965 |
| NVFP4 without SR/RHT (the only path TE runs on sm_120) | 1.9% (stalls) | 1.34 |
Quantizer speed @ 8192×4096 (the wrapper bottleneck was the per-tensor amax in fp32
- a
.item()host sync — 86% of quant time — not the rounding; fixed with bf16 amax +
on-device scale):
| quantizer | time | bandwidth |
|---|---|---|
| pure torch | 3.7 ms | 23 GB/s |
| fused Triton | 0.45 ms | ~195 GB/s |
| hardware-cast CUDA | 0.12 ms | ~730 GB/s |
FP4 linear vs bf16 (hardware-cast CUDA quant + in-kernel RHT, benchmarks/cuda_prof.py):
fprop 1.0–1.4x; wgrad (SR+RHT) 0.80–1.31x (≥bf16 at 8k). Both GEMMs meet-or-beat bf16 at
scale; the remaining per-call quant-wrapper overhead (amax + swizzle + alloc) is what keeps
the full layer from a universal win — fusing those is the next lever.
Weight-quant amortization (NVFP4_AMORTIZE=1): cache the weight's two quant
orientations (fprop contracts K = quant(w); dgrad contracts N = quant(w.t()); both RNE,
so deterministic) once per optimizer step, keyed on a counter bumped by mark_step() after
opt.step (mandatory — a parity check confirms outputs go stale without it). On a
microbatched FFN step (d4096, h14336) this cut 437→122 ms at G=8 and 875→241 ms at G=16;
training still converges to bf16 parity. Note the large factor is partly because the
non-amortized baseline re-quantizes the weight on every fprop+dgrad across all G
microbatches — a real microbatched win, but G-coupled, not universal.
Scaling law on real text (OpenWebText)
Beyond the synthetic addition task, NVFP4 was validated on language modeling: a family of
nanoGPT-style decoders trained on OpenWebText (GPT-2 BPE) in BF16 vs NVFP4, fit to
L(N)=E+AN^-alpha. The two scaling laws are statistically indistinguishable — BF16
L=5.53+745N^-0.500, NVFP4 L=5.52+823*N^-0.500 (same exponent, near-identical irreducible
loss; per-point val-loss gaps <=0.07, within run noise). NVFP4 does not degrade LM
generalization at the tested scales (4.7M-49M params, ~20M tokens each). See scaling/
for the study and results/scaling_law.png for the figure.
Gotchas
- Do not
LD_PRELOADthe system cuBLASLt. That is the Transformer Engine workaround
(seete/te_env.sh); it breakstorch._scaled_mmwithCUBLAS_STATUS_NOT_INITIALIZED.
This pipeline needs no TE and no special environment. - The CUDA extension (
nvfp4_cuda.py) JIT-builds viaload_inlineand needs a CUDA 13
toolkit:CUDA_HOME=/usr/local/cuda-13, gencodecompute_120a,code=sm_120a, and
#include <ATen/cuda/CUDAContext.h>. First build takes a couple minutes (cached after). - torchao's
F.linear/torch.mmon twoNVFP4Tensors silently takes a dequant path
(correct numerics, not FP4 tensor cores). Call_addmm_nvfp4_dispatchfor the real GEMM.
Transformer Engine comparison (te/)
te/train_te.py runs the same task through TE's NVFP4BlockScaling. On sm_120 TE can only
run the degraded (no SR/RHT) path; te/nvfp4_sm120_degrade.patch makes TE auto-disable
RHT/SR with a warning instead of crashing. These scripts require Transformer Engine and
must be run with te/te_env.sh sourced (which is why they are isolated here — that env
breaks the main pipeline).
Transformer Engine comparison ( te/ )
1
-
te/train_te.py runs the same task through TE's NVFP4BlockScaling . On sm_120 TE can only run the degraded (no SR/RHT) path; te/nvfp4_sm120_degrade.patch makes TE auto-disable RHT/SR with a warning instead of crashing. These scripts require Transformer Engine and must be run with te/te_env.sh sourced (which is why they are isolated here — that env breaks the main pipeline).