Skip to content

[megatron]feat: Deepseek v4 RL MXFP4 experts qat support - #7895

Draft
sophiayyya wants to merge 6 commits into
verl-project:mainfrom
sophiayyya:mxfp4-experts-qat
Draft

sophiayyya wants to merge 6 commits into
verl-project:mainfrom
sophiayyya:mxfp4-experts-qat

Conversation

@sophiayyya

@sophiayyya sophiayyya commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

MXFP4 QAT precision and data flow

This change adds routed-expert weight-only MXFP4 fake quantization on top of Transformer Engine (TE) FP8 training for DeepSeek-V4-Flash-0731. Checkpoint storage, trainable parameter dtype, and GEMM precision are separate:

Module / tensors Official 0731 checkpoint storage Parameters after Bridge import Added ModelOpt QAT
Routed experts: w1, w2, w3 MXFP4 E2M1, two values per I8 byte; one E8M0 scale per 32 contiguous input-channel elements BF16 MXFP4 weight fake-quant on mlp.experts.linear_fc1/linear_fc2
Shared experts; MLA wq_a, wq_b, wkv, wo_a, wo_b; indexer wq_b FP8 E4M3 with E8M0 scales over 128 × 128 weight blocks BF16 Disabled
Embeddings, prediction head, router, norms; compressor projections and indexer weights_proj BF16 BF16 Disabled
Native FP32 tensors, including mHC hc_attn_fn/hc_ffn_fn, attn_sink, and compressor ape FP32 FP32 retained Disabled

I8 is the byte container for packed FP4 values, not INT8 quantization. The checkpoint precision map establishes tensor storage and scale geometry; it does not establish runtime kernel precision. Bridge's E2M1 unpacking and the QAT configuration provide the implementation-level interpretation of the packed expert weights. Only indexer wq_b is FP8 in this table; the entire indexer is not uniformly FP8.

flowchart TD
    C["0731 checkpoint: MXFP4 experts / FP8 projections / BF16 / FP32"]
    I["Bridge import: unpack and dequantize quantized weights"]
    P["Trainable model parameters: BF16 plus native FP32 tensors"]
    Q["Routed expert weights: dynamic MXFP4 Q/DQ; BF16 temporary output"]
    T["TE FP8 quantization and GEMM: hybrid / blockwise recipe"]
    O["Adam update: FP32 main parameters and optimizer state; HDO CPU offload"]
    E["Bridge export: requantize updated parameters to MXFP4 / FP8 weight-scale pairs"]
    V["vLLM rollout / validation: quantization=fp8; KV cache=fp8"]
    C --> I --> P
    P -->|routed expert weights| Q
    Q --> T
    P -->|other TE FP8 linear paths| T
    T --> O
    O -->|update model parameters| P
    P -->|weight synchronization| E
    E --> V
Loading

QAT scope and kernel. qat.mode=mxfp4_experts disables all ModelOpt quantizers first, then enables only *mlp.experts*weight_quantizer, with num_bits=(2, 1) and block_sizes={-1: 32, "type": "dynamic", "scale_bits": (8, 0)}. Scales are computed on each call; algorithm=None requires no calibration pass. Shared experts, attention, router, and all ModelOpt input/output quantizers remain disabled. This change does not implement FP4 indexer QK computation or additional KV/output-projection activation fake-quantization.

In the validated ModelOpt 0.44 implementation, MXFP4 fake-quant uses modelopt_cuda_ext_mx.fused_amax_convert → CUDA amax_and_convert_kernel. It returns BF16 values on the MXFP4 grid without permanently packing the trainable parameters. TE then quantizes weights and activations for its FP8 GEMMs. On the validated H100 setup, this simulates MXFP4 weight error while computing the supported GEMMs in FP8; it is not native FP4 GEMM. bypass_te_fp8_assert=True allows this explicit composition. Direct FP8 parameter loading and fp8_param are not enabled.

Scale and rollout handling. Checkpoint E8M0 scales are distinct from runtime compute scales: the recipe sets NVTE_FP8_BLOCK_SCALING_FP32_SCALES=1 for TE and VLLM_USE_DEEP_GEMM_E8M0=0 for the vLLM DeepGEMM path. Bridge exports the updated model parameters and independently recreates packed MXFP4 expert weights and FP8 projection weights with their scales. It does not export a previous forward pass's temporary fake-quant result. The generic NVFP4 QATWeightExporter is skipped for this mode, and rollout.qat.enable=False still retains Bridge's quantized weight synchronization. The diagram describes export formats and rollout configuration, not the compute precision of every vLLM kernel.

Checkpoint compatibility. Dynamic MXFP4 quantizers have no persistent calibration tensors. preserve_mxfp4_checkpoint_methods therefore preserves native TE/Megatron checkpoint methods while retaining the QAT forward path, with a guard rejecting persistent quantizer state. HDO resume restores optimizer main shards to the GPU before loading optimizer state. Neither fix changes the quantization recipe. Native FP32 parameters with HDO additionally require the detach fix in NVIDIA/Megatron-LM#6982, as documented in the example script.

Implementation: quantization configuration, checkpoint compatibility, and 40-step QAT example.

Quantization experiment evidence

The completed H100 experiment ran 40 steps with a 4096-token response limit, validation at steps 10/20/30/40, checkpoints at steps 20/40, and optimizer-state resume between the two 20-step segments. Step-40 AIME validation accuracy was 64.21% with MXFP4 QAT, versus 64.59% for the TE FP8 control without ModelOpt QAT. These are single-run measurements, not evidence of a statistically significant difference. See the QAT run and control run.

This experiment predates the final rebase. It used the implementation before publication cleanup and Megatron 1ff25ca7e with the equivalent local FP32-shard detach patch; the runtime patch helper is not included in this PR. These results validate that experiment, not a fresh 40-step run of the rebased head.

image

Checklist Before Starting

  • Search for similar PRs. Paste at least one query link here: ...
  • Format the PR title as [{modules}] {type}: {description} (This will be checked by the CI)
    • {modules} include fsdp, megatron, veomni, sglang, vllm, rollout, trainer, ci, training_utils, recipe, hardware, deployment, ray, worker, single_controller, misc, perf, model, algo, env, tool, ckpt, doc, data, cfg, reward, fully_async, one_step_off
    • If this PR involves multiple modules, separate them with , like [megatron, fsdp, doc]
    • {type} is in feat, fix, refactor, chore, test
    • If this PR breaks any API (CLI arguments, config, function signature, etc.), add [BREAKING] to the beginning of the title.
    • Example: [BREAKING][fsdp, megatron] feat: dynamic batching

Test

For changes that can not be tested by CI (e.g., algorithm implementation, new model support), validate by experiment(s) and show results like training curve plots, evaluation results, etc.

API and Usage Example

Demonstrate how the API changes if any, and provide usage example(s) if possible.

# Add code snippet or script demonstrating how to use this

Design & Code Changes

Demonstrate the high-level design if this PR is complex, and list the specific changes.

Checklist Before Submitting

Important

Please check all the following items before requesting a review, otherwise the reviewer might deprioritize this PR for review.

sopyang and others added 6 commits September 16, 2026 07:53
The DeepSeek-V4 report describes FP4 quantization-aware training applied to MoE expert weights
during post-training. This adds `qat.mode=mxfp4_experts`: a weight-only ModelOpt fake-quant onto
the MXFP4 grid (E2M1 values, one E8M0 power-of-two scale per 32 contiguous K elements) scoped to
routed experts via `*mlp.experts*weight_quantizer`, with `algorithm=None` because dynamic block
quantization derives its scale per call and ModelOpt asserts it is never calibrated.

The GEMMs keep running in real FP8 through TE's blockwise autocast rather than falling back to a
simulated BF16 matmul. ModelOpt refuses to run any quantizer under `fp8_autocast` -- a guard that
keeps pure simulation faithful, but here the stacking is the point (FP4 weights feeding a real FP8
GEMM, which is what the checkpoint's own mixed MXFP4/FP8 layout implies). `qat.bypass_te_fp8_assert`
lifts it at runtime without patching ModelOpt. Activations are quantized once, by TE; ModelOpt's
input_quantizer stays disabled so they are not quantized twice.

Two adjacent fixes were required to make QAT usable on such a model:

- The ModelOpt layer spec is no longer forced on providers that declare an
  `experimental_attention_variant` (DeepSeek-V4 "dsv4_hybrid", GLM5 "dsa", Qwen3-Next
  "gated_delta_net"). Those install their own heterogeneous spec in `provider_bridge()`, and the
  generic ModelOpt GPT spec would silently replace it and build a different model. ModelOpt swaps
  QuantModules by class, so the spec change is not needed for quantization to apply.
- `export_qat_weights` is gated to the NVFP4 modes it actually implements. For `mxfp4_experts` the
  bridge already re-quantizes routed experts to MXFP4 on every weight sync, so the training forward
  and the rollout weights land on one grid without a second exporter.

`apply_qat_to_modules` now fails fast if the experts-only config did not land where intended: it
requires at least one enabled routed-expert weight quantizer and no other enabled quantizer (a
mis-ordered cfg would leave every remaining linear on ModelOpt's default per-tensor INT8), and logs
whether the fake-quant is the identity on the loaded weights -- it is, exactly, when the checkpoint
already stores those experts on the MXFP4 grid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Co-authored-by: OpenAI Codex <noreply@openai.com>
Mirrors run_deepseek_v4_flash_megatron.sh with TE fp8 blockwise training, fp8 vLLM rollout and the
optional `mxfp4_experts` QAT (off by default). Documents why the FP4 step is simulated on Hopper
while the FP8 GEMM is not, and why filter_overlong_prompts must stay off for DeepSeek-V4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preserve native Megatron checkpoint methods around stateless expert MXFP4
quantization, and restore offloaded optimizer shards before loading HDO state.

Add independent length-only and MXFP4 expert QAT recipes with a 4096-token
response budget, 40 total steps and validation every 10 steps. Require model
and dataset locations from the environment and use portable output defaults.

Validation: checkpoint regressions passed (6 and 7 tests); the original
length-only and QAT runs each completed 40 steps including save/resume.
Published launch arguments match both completed experiments at 20/40 steps.
Shell syntax, required-input checks and credential/path scans passed.
Two pre-existing repository checks are excluded: stale generated reference
configuration and naming of an older example. New example names pass.

Co-authored-by: OpenAI Codex <noreply@openai.com>
Remove the two checkpoint regression test files from this branch as requested.

Co-authored-by: OpenAI Codex <noreply@openai.com>
Record the HDO FP32-shard detach prerequisite, upstream fix and validation
provenance in the standalone MXFP4 QAT script. Remove the length-only recipe
and its stale reference from the QAT header.

Validation: bash -n and git diff --check pass; the executable QAT script body
is byte-for-byte unchanged.

Co-authored-by: OpenAI Codex <noreply@openai.com>
Rename the original DeepSeek-V4-Flash QAT example to use the required
Megatron backend suffix, preserving its contents. Refresh the generated
Megatron reference config with the existing bypass_te_fp8_assert defaults.

Co-authored-by: Codex
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

wuxibin89 pushed a commit that referenced this pull request Sep 29, 2026
…8062)

### What does this PR do?

The FP8 end-to-end section of `docs/low_precision/fp8.md` lists
`NVTE_FP8_BLOCK_SCALING_FP32_SCALES=1` as a general requirement. It only
applies on Hopper.

On Blackwell (SM100+), Transformer Engine has no native block-scaled
GEMM. It runs the block-wise recipe through MXFP8 GEMMs, whose E8M0
scales hold only an exponent. So both of its block-quantize kernels
assert power-of-two scales on SM100+, and a run with the variable set
stops at the first FP8 quantization:

```
transformer_engine/common/transpose/quantize_transpose_vector_blockwise.cu:534 in function quantize_transpose_vector_blockwise:
Assertion failed: pow2_scale. On Blackwell and newer, the FP8 block scaling recipe is emulated with MXFP8, which requires using power of two scaling factors.
```

This is the failure reported in #6172 (8×B200) when following this doc
and the verl-recipe FP8 E2E script. This PR marks the variable as
Hopper-only and says that on Blackwell it must stay unset; power-of-two
scales are the Transformer Engine default. Docs only.

Refs #6172. This does not fix #6172: that issue also reports garbled FP8
rollouts on Blackwell once the variable is removed, and this PR does not
address that.

### Checklist Before Starting

- [x] Search for similar PRs:
-
[NVTE_FP8_BLOCK_SCALING_FP32_SCALES](https://github.com/verl-project/verl/pulls?q=NVTE_FP8_BLOCK_SCALING_FP32_SCALES)
- [blockwise
Blackwell](https://github.com/verl-project/verl/pulls?q=blockwise+Blackwell)
- No PR changes this requirement. Related: #6172 (the issue above);
#7895 (sets the variable in its DeepSeek-V4 example scripts, does not
touch this doc); #7519 (MXFP8 training, does not change this line).
- [x] Format the PR title as `[{modules}] {type}: {description}`

### Test

Documentation-only change.

- `pre-commit run --files docs/low_precision/fp8.md`: all hooks pass,
including "Check docs Last updated info".
- Reproduced on 1×B200 (SM 10.0, torch 2.11.0+cu130, CUDA 13.0,
Transformer Engine 2.16.1). `te.Linear` (2048→2048) and
`te.GroupedLinear` (4 × 2048→768) ran forward and backward under
`Float8BlockScaling(fp8_format=Format.E4M3)` and were compared with a
bf16 run of the same module:

| `NVTE_FP8_BLOCK_SCALING_FP32_SCALES` | `te.Linear` out / dgrad / wgrad
rel. err | `te.GroupedLinear` out / dgrad / wgrad rel. err |
|---|---|---|
| unset (power-of-two scales) | 0.0377 / 0.0423 / 0.0462 | 0.0376 /
0.0475 / 0.0510 |
| `1` | `RuntimeError`: the `pow2_scale` assertion above | same |

The same modules under `MXFP8BlockScaling` give the same errors as the
unset row, with or without the variable.

Source references (Transformer Engine 2.16.1, commit c9877beb):
-
`transformer_engine/common/transpose/quantize_transpose_vector_blockwise.cu:533-536`
and
`transformer_engine/common/transpose/quantize_transpose_square_blockwise.cu:490-493`:
`if (sm_arch() >= 100) NVTE_CHECK(pow2_scale, ...)`.
- `transformer_engine/pytorch/csrc/extensions/gemm.cpp:289-300`:
"Emulate the FP8 block scaling recipe with MXFP8 on Blackwell and newer
as it is not natively supported by cublasLt".
- `transformer_engine/common/swizzle/swizzle_block_scaling.cu:116-118,
205-206`: each FP32 block scale becomes an E8M0 scale by keeping only
its exponent byte, `(sf >> 23) & 0xFF`. This is why the scales must be
powers of two.

### API and Usage Example

None.

### Design & Code Changes

`docs/low_precision/fp8.md`:
- The requirement line now says that the variable is Hopper-only, why it
fails on Blackwell, and what to do there.
- "Last updated" is bumped.

The verl-recipe script
`low_precision/run_dapo_qwen3_moe_30b_megatron_fp8e2e.sh` also exports
the variable unconditionally. That lives in the recipe repository and
can get a matching note separately.

### Checklist Before Submitting

- [ ] Read the [Contribute
Guide](https://github.com/verl-project/verl/blob/main/CONTRIBUTING.md).
- [x] Apply [pre-commit
checks](https://github.com/verl-project/verl/blob/main/CONTRIBUTING.md#code-linting-and-formatting).
- [x] Add / Update [the
documentation](https://github.com/verl-project/verl/tree/main/docs).
- [ ] Add unit or end-to-end test(s) to [the CI
workflow](https://github.com/verl-project/verl/tree/main/.github/workflows)
to cover all the code. If not feasible, explain why: documentation-only
change; the behaviour it documents was checked on B200 as described
under Test.
- [ ] Once your PR is ready for CI, send a message in [the `ci-request`
channel](https://verl-project.slack.com/archives/C091TCESWB1).
- [ ] If your PR is related to the `recipe` submodule, please also
update the reference to the submodule commit: not applicable.

AI assistance: this PR was prepared with AI assistance (Claude Code). I
reviewed the change and the evidence above, and the checks listed under
Test were run for this PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
wuxibin89 added a commit that referenced this pull request Nov 2, 2026
…h live refit (#7971)

### What does this PR do?

Add end-to-end native NVFP4 W4A4 training and rollout on NVIDIA
Blackwell GPUs, demonstrated with Qwen3-30B-A3B-Base DAPO using
Megatron/Transformer Engine and vLLM.

This contributes to Sophia's [low-precision roadmap
#5972](#5972), specifically
**NVFP4 real quant** and **NVFP4 training in Megatron**. In brief:

- **Training:** Transformer Engine runs selected routed-expert MLPs in
NVFP4 with per-token activation scaling and dequantized backward.
Persistent policy weights remain BF16, with full optimizer state.
- **Weight synchronization:** following the trainer-side quantization
boundary of #8287, the trainer exports the routed experts as NVFP4 with
`moe: NVFP4` and `moe_activation: nvfp4_per_token` after each policy
update.
- **Rollout:** vLLM loads the NVFP4 expert weights and quantizes
activations per token, matching training. Refit preserves the storage
referenced by CUDA graphs.

Attention and the configured boundary-layer MLPs remain BF16. The Qwen3
example keeps the first two and last four MLP layers in BF16 and applies
NVFP4 to the remaining 42 routed-expert MLP layers.

### Checklist Before Starting

- [x] Search for similar PRs:
[NVFP4](https://github.com/verl-project/verl/pulls?q=is%3Apr+is%3Aopen+NVFP4),
[roadmap
#5972](https://github.com/verl-project/verl/pulls?q=is%3Apr+is%3Aopen+5972+in%3Abody).
- [x] Format the PR title as `[{modules}] {type}: {description}`.

Related work: #6455 targets ModelOpt/TRT-LLM synchronization; #6992
provides FSDP W4A8 simulation; #7895 targets DeepSeek MXFP4 on TE FP8;
#7519 adds MXFP8 training and rollout. #8287 adds ModelOpt NVFP4 QAT
with static activation scales; this PR adds native TE NVFP4 training
with per-token activation scaling, on the same export path.

### Test

Unit tests (CPU and GB200) pass; scoped Ruff, formatting and lockfile
checks pass. The trainer-side export is bit-identical to vLLM's online
`nvfp4_per_token` quantization, and 6-step runs on Qwen3-30B-A3B (8 × 4
GB200) and a small DeepSeek-V3-layout model pass.

#### Long run

BF16 and W4A4 runs of the example script below (Qwen3-30B-A3B-Base,
DAPO, R3, token-level TIS) on 8 nodes × 4 GB200 GPUs, 700 steps with
checkpoint resume across jobs. **W4A4 vs BF16:**

**Training score (`critic/score/mean`)**

<img width="1813" height="957" alt="image"
src="https://github.com/user-attachments/assets/d0693c0b-6789-4285-8658-44a885d72a72"
/>

**Entropy (`actor/entropy`)**

<img width="1814" height="952" alt="image"
src="https://github.com/user-attachments/assets/e63b552c-fe9b-4b23-9751-62c38976732c"
/>

**Train–rollout mismatch (`rollout_corr/kl`)**

<img width="1811" height="952" alt="image"
src="https://github.com/user-attachments/assets/13131fe5-c670-41ea-91e1-68e5fff123ee"
/>


**Gradient norm (`actor/grad_norm`)**

<img width="1813" height="953" alt="image"
src="https://github.com/user-attachments/assets/b121c562-b6f7-4432-8a7e-ed5768d78e69"
/>

**Performance:** at matched response length, NVFP4 rollout generation is
about 1.3× faster than BF16 once the mean response exceeds ~2.5k tokens,
giving roughly 1.2× shorter end-to-end steps; NVFP4 training itself is
still slower than BF16 in Transformer Engine.

### API and Usage Example

With a Ray cluster running on eight nodes, provide model and dataset
paths shared by all workers:

```bash
MODEL_PATH=/shared/models/Qwen3-30B-A3B-Base \
TRAIN_FILE=/shared/data/train.parquet \
TEST_FILE=/shared/data/validation.parquet \
EXP_NAME=qwen3-native-nvfp4 \
PRECISION_MODE=real_nvfp4 \
bash examples/real_nvfp4/run_qwen3_30b_megatron.sh
```

Use `PRECISION_MODE=bf16` for the baseline. NVFP4 training is enabled
like FP8 training, through
`actor_rollout_ref.actor.megatron.override_transformer_config` (`fp4:
e2m1`, `fp4_recipe: nvfp4`, and `first_last_layers_bf16` with
`num_layers_at_start_in_bf16` / `num_layers_at_end_in_bf16`), and
rollout through `actor_rollout_ref.rollout.quantization.moe=NVFP4` with
`moe_activation=nvfp4_per_token`, whose `ignore` list keeps the same
layers in BF16; the trainer checks that the two agree. The example uses
the standard verl PPO entry point, parquet datasets and built-in reward
manager, with the DAPO recipe (clip-higher, token-level loss, dynamic
sampling and the overlong penalty), R3 routing replay and token-level
TIS. See [the example
guide](https://github.com/zhangyimi/verl/blob/feat/native-nvfp4/examples/real_nvfp4/README.md)
for configuration and runtime setup.

### Design & Code Changes

- **Training precision:** the Megatron per-module recipe (attention
BF16, routed-expert MLPs NVFP4, carved-out layers BF16) is generated
from the configuration rather than maintained as a file. After model
construction, the effective precision of every attention and expert
linear layer is checked against it. Packed THD sequences use the TE
alignment for FP4 as for FP8.
- **Rollout config and refit:** `moe_activation=nvfp4_per_token` selects
a compressed-tensors scheme (NVFP4 weights, fully dynamic NVFP4 inputs).
The trainer quantizes the experts with its own Triton kernel, sharing
one global scale across `gate_proj`/`up_proj`, bit-identical to vLLM's
online `nvfp4_per_token` quantization. In vLLM the scheme selects
FlashInfer TRT-LLM with per-token activations; refits load each bucket
in place into the expert storage captured by CUDA graphs and
post-process each MoE layer, so the extra memory is one MoE layer.
- **Routing replay:** use vLLM’s native fused-MoE routing capture and
align replay data with FP4 training padding.
- **Runtime compatibility:** Transformer Engine 2.20.2 from the verl
wheelhouse. No vLLM source patch is needed.

The validated Megatron + vLLM stack uses:

| Component | Version |
|---|---|
| PyTorch | 2.13 / CUDA 13 |
| vLLM | 0.32.0 |
| Transformers | 5.16.1 |
| Megatron-Core | 0.19.2 |
| Megatron-Bridge | 0.6.2 |
| Transformer Engine | 2.20.2 |
| FlashInfer | 0.7.0.post1 |
| CUTLASS DSL / QuACK | 4.8.0 / 0.6.5 |

SGLang end-to-end validation remains follow-up work.

### Checklist Before Submitting

- [x] Read the [Contribute
Guide](https://github.com/verl-project/verl/blob/main/CONTRIBUTING.md).
- [ ] Run the complete all-files pre-commit suite. Focused lint,
formatting, shell and lockfile checks have passed.
- [x] Add usage, design and runtime compatibility documentation.
- [x] Add focused unit and end-to-end coverage. Project CI integration
remains to be completed.
- [x] Request project CI and review.
- [x] Check recipe coordination: the recipe submodule reference is
unchanged; no companion recipe PR is required.

AI assistance: OpenAI Codex and Anthropic Claude assisted with
implementation, testing and this PR description.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: wuxibin <wuxibin@bytedance.com>
wuxibin89 added a commit that referenced this pull request Nov 2, 2026
### What does this PR do?

Add shared TileLang quantization kernels for FP8_BLOCK, MXFP8, and MXFP4
QAT and parameter export. QAT uses fused quantize-dequantize with
straight-through gradients, while export uses the same kernels without
dequantization so values and scales follow the same numerical recipe.
NVFP4 QAT uses triton kernel in
#8494.
This overlaps with
[#7895](#7895), which adds
DeepSeek-specific routed-expert MXFP4 weight-only QAT using ModelOpt's
CUDA extension. This PR instead provides reusable TileLang kernels for
FP8_BLOCK, MXFP8, and MXFP4, applies them to both weight/activation fake
quantization and checkpoint export, and keeps the existing QAT mode
interface.

AI assistance was used for implementation, numerical comparison,
testing, debugging, and preparation of this PR description. The human
submitter directed the numerical design and must review every changed
line and validation result before requesting maintainer review.

### Checklist Before Starting

- [x] Search for similar PRs: [QAT quant kernel
search](https://github.com/verl-project/verl/pulls?q=is%3Apr+is%3Aopen+QAT+quant+kernel),
[MXFP4 QAT
search](https://github.com/verl-project/verl/pulls?q=is%3Apr+is%3Aopen+MXFP4+QAT).
- [x] Format the PR title as `[{modules}] {type}: {description}`.

### Test

```bash
# FP8, MXFP8, MXFP4 kernels, export integration, and ModelOpt integration
pytest -q tests/utils/test_qat_fp8_on_gpu.py tests/utils/test_qat_fp4_on_gpu.py tests/utils/test_qat_export_kernels_on_gpu.py tests/utils/test_modelopt_mxfp8_on_gpu.py
# 51 passed

# CPU CI-equivalent environment
uv run --frozen --all-packages --extra cpu pytest -s -x --asyncio-mode=auto tests/utils/real_nvfp4
# 24 passed, 1 skipped

uvx --with hydra-core pre-commit run --all-files --show-diff-on-failure --color=always
# all hooks passed

git diff --check origin/main...HEAD
# passed
```

FP8 and MXFP4 quantized values, scales, and fused QDQ outputs were also
compared bitwise against the DeepSeek-V4.1 inference reference kernels.

### API and Usage Example

```python
from verl.utils.qat.fp4 import fp4_quant
from verl.utils.qat.fp8 import fp8_quant

fp8_weight, fp8_scale = fp8_quant(weight, (128, 128))
mxfp8_weight, mxfp8_scale = fp8_quant(weight, (1, 32), scale_fmt="e8m0")
mxfp4_weight, mxfp4_scale = fp4_quant(weight)

# Fused fake quantization used by QAT:
dequantized_activation = fp8_quant(activation, (1, 32), scale_fmt="e8m0", dequant=True)
```

### Design & Code Changes

- Add 2D FP8_BLOCK and 1D MXFP8 TileLang kernels with optional fused
dequantization and FP32/E8M0 scale output.
- Add a packed 1x32 MXFP4 TileLang kernel with optional fused
dequantization.
- Route ModelOpt FP8_BLOCK, MXFP8, and MXFP4 fake quantizers through the
new kernels while retaining straight-through gradients.
- Route compressed-tensors FP8_BLOCK, MXFP8, and MXFP4 CUDA weight
export through the same kernels; retain CPU and NVFP4 fallbacks.
- Add numerical and export integration tests and document the shared
QAT/export behavior.
- Upgrade ModelOpt to 0.47.0 and update the lockfile.
- Isolate Megatron stubs in the Muon CPU tests so they do not pollute
later tests through `sys.modules`.

### Checklist Before Submitting

- [x] Read the [Contribute
Guide](https://github.com/verl-project/verl/blob/main/CONTRIBUTING.md).
- [x] Apply [pre-commit
checks](https://github.com/verl-project/verl/blob/main/CONTRIBUTING.md#code-linting-and-formatting).
- [x] Add / Update [the
documentation](https://github.com/verl-project/verl/tree/main/docs).
- [x] Add unit tests to cover the kernels, QAT integration, and export
integration.
- [ ] Once the PR is ready for CI, send a message in the `ci-request`
channel.
- [x] This PR is not related to the `recipe` submodule.
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.

2 participants