Skip to content

[qat] feat: MXFP4 weight-only QAT for MLite MoE models - #127

Merged
ISEEKYAN merged 50 commits into
mainfrom
qat-clean-review
Jul 28, 2026
Merged

[qat] feat: MXFP4 weight-only QAT for MLite MoE models#127
ISEEKYAN merged 50 commits into
mainfrom
qat-clean-review

Conversation

@ISEEKYAN

@ISEEKYAN ISEEKYAN commented Jul 23, 2026

Copy link
Copy Markdown
Owner

MXFP4 weight-only QAT for Megatron Lite MoE models

Scope

This PR aligns MXFP4 weight-only QAT in Megatron Lite with the quantization
contract used by rollout and composes the path into Qwen3 MoE, Qwen3.5,
DeepSeek V4, GLM-5, and Kimi K2.

  • BF16 master weights remain optimizer-owned while training forwards apply
    differentiable MXFP4 fake quantization.
  • Checkpoint loading maps logical ...weight names onto the surviving
    ...parametrizations.weight.original master instead of silently dropping
    tensors.
  • Qwen3 MoE export uses the FP4-aware resynchronization path consumed by
    rollout.
  • All five model protocols apply QAT before optimizer construction and use a
    shared checkpoint-key canonicalization contract.
  • Router replay attaches only to decoder routers. MTP-only routers are
    excluded, and GLM-5 route and replay-mask packing share its contiguous CP layout.
  • Replay fails loudly if router auxiliary loss is nonzero because native
    routing-map load statistics do not describe replay dispatch.
  • A router-free replay request fails loudly instead of silently doing nothing.

The quantization primitive remains model-agnostic and contains no model-name
allowlist. The verified delivery surface described here is MXFP4.

MXFP4 numerical alignment

Before this change, training fake quantization, checkpoint serialization, and
rollout used different scale and tie-rounding rules. The old training rule
selected a scale 2x too small whenever a block maximum's mantissa exceeded
1.5, clipping the largest value in each affected block.

Measured over 99,090,432 real Qwen3-30B-A3B weight elements
(3,096,576 blocks), the old rule produced a different scale for 43.07% of
blocks and a different decoded value for 12.38% of elements.

The implementation now shares one MXFP4 scale-selection and E2M1-rounding
contract between training fake quantization and serialization, with an
independent ModelOpt transcription as the reference. On the same real-weight
sample:

  • decoded-value differences: 0
  • maximum absolute difference: 0
  • scale-exponent mismatches: 0
  • clipped elements: 0
  • packed-byte match fraction: 1.0000
  • scale-byte match fraction: 1.0000

This is weight-level parity. Training and rollout still use different GEMM
implementations, so it is not an end-to-end equality claim.

Five-model QAT and R3 wiring

Model QAT composition Checkpoint mapping Router replay
Qwen3 MoE before optimizer construction shared canonical key mapping decoder layers only; MTP excluded
Qwen3.5 before optimizer construction shared canonical key mapping decoder layers only; MTP excluded
DeepSeek V4 before optimizer construction shared canonical key mapping decoder layers only; MTP excluded
GLM-5 before optimizer construction shared canonical key mapping shared sigmoid router; contiguous route/mask CP packing; MTP excluded
Kimi K2 before optimizer construction shared canonical key mapping shared sigmoid router; MTP excluded

The R3 correction prevents training-only MTP routers from consuming rollout
route entries. With MTP disabled, replay still attaches to every decoder layer;
with MTP enabled, the attachment count remains the decoder-layer count rather
than decoder plus MTP layers.

Four-arm DAPO experiment

All arms were planned for 30 steps but were truncated by a four-hour scheduler
limit: BF16 ended at step 21, QAT-off at step 20, QAT-on at step 22, and R3 at
step 24. These are early-window results.

Arm Training Rollout Router replay
baseline BF16 BF16 off
QAT off BF16 MXFP4 off
QAT on MXFP4 fake quant MXFP4 off
R3 MXFP4 fake quant MXFP4 on

QAT-off and QAT-on used byte-identical rollout settings; their only QAT
difference was training-side enablement. The rollout path was vLLM
compressed-tensors plus verl.utils.qat.vllm_patch.

Curves

Reward Entropy
Four-arm reward curve Four-arm entropy curve
Train/rollout probability deviation Validation
Four-arm train/rollout probability-deviation curve Four-arm validation curve

The images live on a non-merge asset branch; this PR contains no PNG files.

Measured results

Arm Last-step probs_diff mean Pearson Entropy
BF16 0.00598 0.99923 0.4920
QAT on 0.01657 0.99527 0.5527
R3 0.02443 0.98060 0.3917
QAT off 0.02672 0.99204 1.7349

QAT-on reduced the last-step train/rollout probability discrepancy by about
38% versus QAT-off (0.0166 versus 0.0267), but remained about 2.8x
BF16 (0.00598). The result is therefore that MXFP4 QAT reduces the discrepancy,
not that it eliminates it.

MXFP4 rollout cost about 30% less per generated token than BF16 in this
window. QAT-off and QAT-on differed by only 0.4% after token normalization
(0.04072 versus 0.04090 seconds per token).

Validation was measured only at global steps 4, 9, 14, and 19. The observed
within-run noise floor was about ±0.1, so these points do not support ranking
the arms by validation score.

Two observations remain unexplained and no mechanism is claimed:

  1. QAT-off entropy was 1.7349, 3.1-4.4x the other three arms, and its
    validation series trended downward.
  2. R3 had worse train/rollout consistency than QAT-on, with the synchronized
    probs_diff gap growing from about 11% to 44% over steps 13-22.

Known limitations and follow-ups

  • The R3 layer axis covers decoder routers only. MTP routers remain on native
    routing; replaying MTP or speculative routes has not been validated.
  • Route rows omitted for final input tokens are zero-filled placeholders.
    Correctness relies on the corresponding r3_replay_mask rows being false;
    zero is not a sentinel expert ID.
  • The 99,090,432-element MXFP4 result is offline weight-level evidence reported
    here, not an end-to-end training/rollout parity test or a committed full-model
    test artifact.
  • Replay evidence is printed for the first observed microbatch only.
    changed=0 emits a warning rather than failing, and replay counters are not
    exported as training metrics.
  • QAT/R3 operational guidance is documented, but a dedicated entry in the
    MLite skill registry is still missing.

Usage

experimental/lite/examples/verl/QAT.md
documents the MXFP4 training contract, apply-before-optimizer ordering,
checkpoint mapping, safe exclusions, packed snapshots, and the exporter
boundary.

experimental/lite/examples/verl/scripts/run_qwen3moe_mxfp4_qat.sh
provides the four experiment modes and keeps the QAT delta separate from
ordinary training parameters.

Author: Yan Bai

Coverage matrix

Model Training-side QAT MXFP4 export R3
Qwen3 MoE yes yes yes
Qwen3.5 yes yes yes
DeepSeek V4 yes yes yes
GLM-5 yes yes yes
Kimi K2 yes yes yes

MXFP4 export now covers all five models. For Qwen3.5, GLM-5, and Kimi K2, the exporter validates the resync target, preserves embeddings, lm_head, and router gates, and emits primitive-identical packed weights plus UE8M0 scales.

ISEEKYAN and others added 8 commits July 27, 2026 09:23
Extend weight-only QAT to 3D GroupedLinear expert weights, add ImplConfig.qat
+ apply_qat_to_chunks in qwen3_moe protocol, and map HF loads onto parametrized
masters via _canonical_state_key. CPU tests cover expert STE, skip lists, and
DAPO three-arm impl_cfg resolution.
…m-weight train)

apply_qat_to_chunks parametrizes linear weights in build_model, renaming
mod.weight -> mod.parametrizations.weight.original, which runs before the
runtime's load_hf_weights. _copy_loaded_state matched loaded '..weight' keys
against state_dict via substring, which no longer hits the parametrized name,
so quantized-layer masters were silently never loaded (random-weight training).

Fix: canonicalize parametrized state keys (strip .parametrizations.<attr>.original
back to .<attr>) when resolving loaded tensors, so '..linear.weight' lands on
'..linear.parametrizations.weight.original'. Quantizer buffers (.amax) untouched;
amax is recomputed from the real weight on first forward, and dist_opt resyncs
its fp32 master post-load via reload_model_params, so apply-before-load is safe.

CPU regression: 7 new tests (all four formats int8/int4/fp8_e4m3/mxfp4) prove
the master receives real weights, plus a guard that the naive substring path
misses the parametrized key. Full qat suite 46 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend PR#127 with QuantizerB prepare/export_hf_checkpoint for INT4_W4A16_G128
and NVFP4_W4A16 (ModelOpt 0.43 preset), qwen3_moe protocol/checkpoint QAT
(MoE 3D expert fake-quant), and HF master-weight load canonicalization.
CPU: 55 unit tests (qat_unit + qat_recipe_unit + qat_qwen3_moe_unit).
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Training-time MXFP4 fake-quant, the checkpoint serializer and the quantizer
the rollout actually runs (ModelOpt MXFP4QTensor.quantize, reached from verl
utils/modelopt/qat_weight_exporter.py) were three different encodings:

  implementation       E8M0 scale exponent rule                 E2M1 ties
  training fake-quant  floor(log2(amax)) - 2   (OCP Alg. 1)      to-even
  ckpt serializer      ceil(log2(amax / 6))                      to-even
  rollout (ModelOpt)   ceil(max(log2(amax / 6), -127))           down

The OCP Alg. 1 form picks a scale 2x too small whenever the block amax
mantissa exceeds 1.5 -- 29% of blocks of a standard gaussian tensor, 42% at
weight-like scales -- and then saturates the block maximum to 6.0, clipping
~2.3% of all elements. ModelOpt never saturates. On randn[512, 4096] that was
8.07% of elements different, sum|d|/sum|w| = 2.19%. QAT was therefore learning
to compensate a distortion deployment does not produce.

Consolidate the two rules into primitive/quantization/mxfp4.py as the single
source of truth (mx_shared_scale_exponent + e2m1_round_index) and have qat.py
import them instead of keeping a second "equivalent" copy. Both are now
bit-identical to ModelOpt on gaussian / weight-scale / heavy-tail / extreme
(1e4, 1e-4, all-zero, bf16 max, exact E2M1 midpoints) tensors.

Add test_mxfp4_modelopt_parity_unit.py, which locks both paths against a
verbatim transcription of the ModelOpt kernel (vendored so the test runs
without nvidia-modelopt, cross-checked against the real package when it is
importable).

Two tests in test_qat_unit.py asserted the old rules and were what let the
mismatch ship; they now assert the ModelOpt contract. In particular the block
maximum is no longer clipped, so its gradient is no longer zeroed by the STE.
ISEEKYAN added 2 commits July 27, 2026 09:38
…ted routing

Receiving routed_experts proves that rollout routes arrived, but it does not
prove that replay overrode a routing decision. A silently inactive replay hook
could otherwise emit the same configuration, metric keys, and shapes.

Count the behavior directly. RouterReplay accumulates replayed forward calls,
routing rows, and changed rows; RouterReplayDriver emits one
R3_REPLAY_EVIDENCE line with calls/rows/changed/changed_frac.

The counters distinguish replay not running, replay running without changing a
route while actor and rollout still agree, and replay substituting routing. The
first is a loud R3_REPLAY_VOID error, the second emits R3_REPLAY_WARN, and the
third emits direct evidence.

Tests cover the counters, mask-limited partial replay, record mode, and both
void paths.
@ISEEKYAN ISEEKYAN changed the title [qat] feat: weight-only QAT primitive (int8/int4/fp8_e4m3/mxfp4) — review boundary [qat] feat: MXFP4 weight-only QAT for MLite MoE models Jul 28, 2026
@ISEEKYAN
ISEEKYAN merged commit 648f0b8 into main Jul 28, 2026
1 check passed
flameagainst pushed a commit to verl-project/Megatron-LM that referenced this pull request Jul 28, 2026
Mirror of ISEEKYAN#127 at d60ec48, rebased onto verl-project lite.
Content is byte-identical to that PR: 41 files, +4502/-384.
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