Skip to content

[PyTorch] MXFP4 weight QAT recipes on MXFP8 and FP8 block-scaling hosts#3263

Closed
xiuhu17 wants to merge 9 commits into
NVIDIA:mainfrom
xiuhu17:mxfp4_qat
Closed

[PyTorch] MXFP4 weight QAT recipes on MXFP8 and FP8 block-scaling hosts#3263
xiuhu17 wants to merge 9 commits into
NVIDIA:mainfrom
xiuhu17:mxfp4_qat

Conversation

@xiuhu17

@xiuhu17 xiuhu17 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Description

Weight-only MXFP4 quantization-aware training: weights are projected onto the MXFP4 grid (E2M1 payloads, 1x32 power-of-two UE8M0 scales) before the host recipe quantizes them, so training sees exactly the deployment weight while activations and gradients keep the base recipe untouched.

The only new numerics is the fused projection bf16/fp32 -> MXFP4 grid -> bf16/fp32 (tex.mxfp4_fake_quantize: per 1x32 block, scale 2^clamp(ceil(log2(amax/6)), -126, 125) derived from the amax bit pattern, RTNE onto E2M1, straight-through-estimator gradient). All MXFP8 and 128x128 blockwise FP8 encoding/decoding of the projected weight runs through the existing quantizer pipeline unchanged. Since every projected value is a grid point p * 2^e, the existing encodings are decoded-value exact: MXFP8 rowwise unconditionally, 128x128 blockwise for tile scale spreads up to 2^14.

Recipes: MXFP4QATMXFP8BlockScaling(MXFP8BlockScaling) and MXFP4QATFloat8BlockScaling(Float8BlockScaling); QAT can also be enabled on the stock host recipes via the mxfp4_qat_weights field (NVTE_MXFP4_QAT=1). backward_override keeps its base-recipe semantics. Also fixes ptx::exp2f(e8m0) to decode UE8M0 code 0 as 2^-127 and code 255 as NaN.

Test results (B200, tests/pytorch/test_mxfp4_qat.py)

Test Compared quantities Corpus Result
CUDA kernel vs composite-torch reference raw bits of the projected weight all 65536 bf16 patterns; fp32 bit fuzz; edges: +-0, bf16 subnormal ladder (2^-127 ... 2^-133), 2^-128 tie, every RTNE midpoint, 1.52^k +-1ulp triples, saturation 62^125, non-finite block poisoning, misaligned storage-offset views bitwise equal
CUDA kernel vs independent fp64 RTNE oracle decoded values same corpora (finite blocks) bitwise equal
--use_fast_math rebuild of the same kernel source raw bits vs normal build same edge corpora incl. the 2^-127 FTZ-critical block identical
CUDA kernel vs TileKernels (independent deployment MXFP4 implementation, power-of-two scales) projected values, bitwise bf16-exhaustive (2036/2048 finite below-saturation blocks); fp32 edges + fuzz; full edge corpus bitwise equal on the shared domain; divergence only for amax > 6*2^125 and the non-finite policy
MXFP8 rowwise raw-encode losslessness payload + scale bytes decoded independently incl. the 2^-127 grid point (UE8M0 code 0) exact
UE8M0 extreme scale codes through real dequantize planted codes 0 / 255 code 0 -> 2^-127, code 255 -> NaN exact
128x128 blockwise exactness boundary per-element E4M3-lattice prediction vs actual quantizer spread d = 0..16 x full signed payload grid exact through 2^14, first loss at 2^15
End to end fwd/dgrad/wgrad with dgrad-lineage discriminators Linear + GroupedLinear, 2 recipes x backward_override {None, dequantized, high_precision} x fused/unfused wgrad; recipe-switch cache invalidation; loud rejection of unsupported surfaces (ops API, fused Userbuffers/grouped-MLP, fp16, primary FP8 weights) pass
Perf, 8192x8192 bf16 kernel vs torch reference -- 67 us (~4.0 TB/s) vs 3.1 ms

Known limitations

  • For amax > 6*2^125 the fake-dequant saturates at the cap while packed deployment moves to scale 2^126 (unmaterializable in bf16/fp32; pinned by test).
  • fp16 weights and fp16 activation/dequantize dtypes are rejected: the MXFP4 grid exceeds fp16 range.
  • Distributed (FSDP/TP) + real-optimizer master-step end-to-end verification and a fused projection+quantize kernel are future work.

Type of change

  • New feature (non-breaking change which adds functionality)

Checklist:

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

xiuhu17 added 4 commits July 25, 2026 03:20
MXFP4QATMXFP8BlockScaling / MXFP4QATFloat8BlockScaling project weights onto
the MXFP4 (E2M1, 1x32 power-of-two scale) grid before the host recipe
quantizes them; activations and gradients are untouched. The projection is
a fused fake-quantization with three bit-identical implementations (CUDA
kernel, CuTe DSL, PyTorch reference) dispatched via NVTE_MXFP4_QAT_IMPL,
with an identity-STE gradient.

- scale contract matches the TileKernels deployment path (floor 2^-126,
  cap 2^125); integer-bit amax/scale-exponent derivation and non-FTZ PTX
  keep results bit-exact under --use_fast_math builds
- the rowwise MXFP8 and 128x128 blockwise weight encodings of the projected
  weight are bitwise lossless (raw payload/scale verified); the columnwise
  32x1 encoding is bounded
- fix ptx.cuh exp2f expansion of UE8M0 code 0 (2^-127) and code 255 (NaN)
- reject MXFP4 QAT loudly on surfaces that bypass the weight-quantization
  hook (te.ops, Userbuffers, fused grouped MLP, quantized primary weights);
  project the weight in FSDP2/GTP backward rematerialization; invalidate
  cached weight workspaces on base<->QAT recipe switches
- tests: per-step bitwise losslessness, bf16-exhaustive and fp32 bit-fuzz
  oracles, RTNE midpoint/threshold vectors, fast-math immunity build, STE,
  misaligned/non-contiguous inputs, and a TileKernels/CuTe-DSL/CUDA/torch
  four-way bitwise matrix plus a 24-config e2e backward-override matrix
The MXFP4 grid reaches 6*2^125, far beyond fp16 range, so an fp16 pipeline
cannot represent the projected weight or its dequantized form. quantize_weight
now raises when a QAT recipe is active and the workspace/activation dtype is
fp16 (fp16 weights were already rejected by the projection itself); documented
in both recipe docstrings.
- exp2f JIT test now checks all 256 codes bitwise including the code-255
  NaN payload (0x7FFFFFFF)
- add an end-to-end nvte MXFP8 software-dequantize test with planted
  extreme scale codes through the real kernel: demands code 0 -> 2^-127
  and code 255 -> NaN on a fixed build, and pins the pre-fix wheel
  behavior (flush to zero / Inf) until then
- the bf16-dequant losslessness test auto-detects a fixed build instead
  of hard-asserting the pre-fix flush
@github-actions github-actions Bot added the community-contribution PRs from external contributor outside the core maintainers, representing community-driven work. label Jul 25, 2026
@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds MXFP4 weight-only quantization-aware training on MXFP8 and block-scaled FP8 hosts.

  • Adds the fused BF16/FP32-to-MXFP4 fake-quantization kernel, C API, PyTorch binding, and straight-through autograd wrapper.
  • Adds MXFP4-QAT recipe classes and recipe-field configuration.
  • Integrates projected-weight handling into fused PyTorch modules while rejecting unsupported operator and fused paths.
  • Corrects UE8M0 extreme-code decoding and adds extensive numerical and end-to-end tests.

Confidence Score: 3/5

The PR is not yet safe to merge because Linear can reconstruct its backward weight under a different recipe than the one used for forward.

Linear does not retain the forward MXFP4-QAT state and instead queries mutable global recipe state when rebuilding FSDP2 or distributed weight workspaces during backward, so leaving the forward autocast context can cause dgrad to use an incorrectly projected weight.

Files Needing Attention: transformer_engine/pytorch/module/linear.py

Important Files Changed

Filename Overview
transformer_engine/pytorch/module/linear.py Integrates QAT projection into Linear weight handling, but backward reconstruction still derives QAT state from the ambient recipe rather than forward state.
transformer_engine/pytorch/module/layernorm_linear.py Adds MXFP4 projection to LayerNormLinear weight preparation and backward workspace reconstruction.
transformer_engine/pytorch/module/layernorm_mlp.py Adds projected-weight handling for both MLP weights while preserving the existing fused backward structure.
transformer_engine/pytorch/module/base.py Extends shared weight-workspace validation and quantization to account for MXFP4-QAT recipe state.
transformer_engine/pytorch/mxfp4_qat.py Introduces the validated PyTorch fake-quantization wrapper and identity-gradient estimator.
transformer_engine/common/cast/mxfp4/fake_quantize_mxfp4.cuh Implements the fused blockwise MXFP4 projection CUDA kernel.
transformer_engine/common/recipe/init.py Adds MXFP4-QAT configuration and host-recipe subclasses.
tests/pytorch/test_mxfp4_qat.py Adds broad numerical, recipe, integration, and unsupported-path coverage.

Sequence Diagram

sequenceDiagram
  participant User
  participant Module as PyTorch fused module
  participant QAT as MXFP4 fake quantizer
  participant Host as MXFP8 / FP8 block quantizer
  participant GEMM
  User->>Module: Forward under MXFP4-QAT recipe
  Module->>QAT: Project master weight to MXFP4 grid
  QAT-->>Module: BF16/FP32 projected weight
  Module->>Host: Encode using host recipe
  Host-->>GEMM: Quantized weight workspace
  GEMM-->>User: Forward output
  User->>Module: Backward
  Module->>Host: Reconstruct weight workspace if needed
  Host->>GEMM: Dgrad weight operand
Loading

Reviews (7): Last reviewed commit: "Drop the TileKernels cross-check tests" | Re-trigger Greptile

Comment thread transformer_engine/pytorch/module/linear.py
xiuhu17 added 4 commits July 25, 2026 11:41
NVFP4-style control: MXFP4 weight QAT is now a field on the MXFP8 and
Float8 block-scaling host recipes, defaulting from NVTE_MXFP4_QAT, so a
stock --fp8-recipe mxfp8/blockwise launch enables QAT without framework
changes. The QAT subclasses remain as the explicit API (field pinned
True); blockwise validation moves to the host, guarded on the field.
…t raw-tuple identity

The host encoder re-canonicalizes (payload, scale): TE-native normalizes the
block amax into (224, 448] while a fixed-shift direct converter keeps the
original factorization, so the raw E4M3/UE8M0 bytes generally differ between
the two even though every decoded value is identical. 'Losslessly' in the
module docstring was readable as raw-bit identity; say exactly what holds.
- Move the composite-torch-ops reference implementation to
  tests/pytorch/references/mxfp4_qat_reference.py (the numerical oracle,
  matching the existing blockwise reference layout); the main module no
  longer carries a runtime fallback path.
- transformer_engine/pytorch/mxfp4_qat.py now only validates inputs,
  applies the straight-through-estimator wrapper, and calls the
  tex.mxfp4_fake_quantize binding; a missing binding is a hard error and
  the NVTE_MXFP4_QAT_IMPL/REQUIRE_KERNEL/DISABLE_CUDA_KERNEL knobs are
  removed.
- Drop the CuTe DSL implementation: the cast layer has no DSL precedent
  and TE carries no cutlass-DSL runtime dependency.
- Tests compare the kernel against the reference directly and demand the
  fixed UE8M0 code-0/255 dequantize semantics unconditionally.
They imported an external TileKernels checkout with a developer-machine
default path and can therefore never run in CI; the cross-backend
comparison lives on as a standalone harness outside the tree. The
in-tree suite keeps the kernel-vs-reference parity, exhaustive bf16 and
fp32 fuzz coverage, and the raw-byte encode oracles.
@xiuhu17 xiuhu17 closed this Jul 27, 2026
@xiuhu17
xiuhu17 deleted the mxfp4_qat branch July 27, 2026 08:29
@xiuhu17
xiuhu17 restored the mxfp4_qat branch July 27, 2026 10:03
@xiuhu17 xiuhu17 reopened this Jul 27, 2026
@xiuhu17 xiuhu17 closed this Jul 27, 2026
@xiuhu17
xiuhu17 deleted the mxfp4_qat branch July 27, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution PRs from external contributor outside the core maintainers, representing community-driven work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant