End-to-End MXFP8 Reinforcement Learning on Blackwell: What Matching Buys, When It Pays Off, and How It Fails
Abstract
Low-precision arithmetic can make reinforcement learning (RL) for large language models cheaper, but RL requires the sampler and the learner to agree. Several frameworks now run FP8 on both sides. We implement end-to-end MXFP8 RL in verl (Megatron/Transformer Engine training; vLLM and SGLang rollout; dense and MoE models) and measure on B200 GPUs, with Qwen3-8B and Qwen3-30B-A3B, what this design buys, when it is faster, and how it fails. (1) Matching. For an MXFP8 learner, a sampler on the learner's grid is more consistent than a higher-precision BF16 sampler: in one engine at fixed weights, train-rollout KL is 11% lower with half of the sampler's linear layers on the grid and 16% lower with all of them, and the synchronized weights we checked equal the learner's FP8 forward weights byte for byte. On the MoE model, an FP8 pipeline whose sampler uses another grid has twice the shared-grid pipeline's KL from the first step. Without a quantized KV cache, BF16 end to end remains 2.5-10x more consistent. (2) Speed. MXFP8 training adds a fixed cost per micro-batch and saves a per-token cost, so in our dense setting it pays off only above about 4.5k tokens per micro-batch; a micro-batch sweep on identical data confirms the per-micro-batch cost, and a trace shows the GPU waiting for the host for most of the added time. End to end, MXFP8 cuts MoE step time by 10.7% and generation time by 24%, and slows a short-sequence dense workload by 5%. (3) Correctness. We state seven conditions every weight synchronization must preserve and checks that test them against the live engine. Each of the seven failures we met on hardware broke one condition, five silently; the checks caught two at their first opportunity, each missed by the other checks, and CPU fault-injection tests reproduce five. All runs are single-seed and at most 60 steps long; we make no claim about final model quality.
Figures & tables
| Learner GEMMs | Sampler GEMMs | Grid relation | Reported in | Here |
|---|---|---|---|---|
| BF16 | BF16 | same values, different kernels | standard RL; [ 23 , 5 ] | |
| BF16 | FP8 / INT8 | post-training quantization of the learner | [ 14 , 24 , 31 ] | |
| FP8 tensorwise | BF16 | sampler not quantized | this work | |
| MXFP8 | BF16 | sampler not quantized | this work (ablation) | |
| MXFP8 | FP8 blockwise | different formats | [ 1 ] (reward collapse) | |
| FP8 blockwise | FP8 blockwise | different scales (SM100: power-of-two vs. FP32) | this work |
| A : dense, gsm8k | B : dense, DAPO | C : dense, gsm8k, SGLang | D : MoE, DAPO | |
|---|---|---|---|---|
| Model | Qwen3-8B | Qwen3-8B | Qwen3-8B | Qwen3-30B-A3B (48 layers, 128 experts, top-8) |
| GPUs, parallelism | 2 B200, TP 2 | 2 B200, TP 2 | 2 B200, TP 2 | 4 B200, TP 2 EP 2 |
| Prompt / response cap | 1024 / 2048 | 1024 / 8192 | 1024 / 2048 | 1024 / 8192 |
| Tokens per step | 0.29–0.68 M | 2.9–3.7 M | 0.37–0.67 M | 3.4 M |
| Steps | 60 | 32 | 30 | 10 |
| Micro-batching | 4 sequences (update), 8 (log-prob) | dynamic, 12 288 tokens/GPU | as A | dynamic, 20 480 tokens/GPU |
| Setting | Pairing | kl , step 1 | kl | log_ppl_abs_diff | Pearson | chi2_token |
| A: dense, gsm8k (vLLM) | BF16 / BF16 | 0.00066 | 0.00069 (0.00071) | 0.00143 (0.00129) | 0.9954 (0.9965) | 0.0015 (0.0014) |
| FP8-TW / BF16 | 0.00859 | 0.01217 (0.01225) | 0.01317 (0.01325) | 0.9380 (0.9464) | 0.0404 (0.0408) | |
| MXFP8 / BF16 | 0.00828 | 0.00864 (0.00865) | 0.00953 (0.00940) | 0.9678 (0.9724) | 0.0220 (0.0230) | |
| MXFP8 / MXFP8, gate_up BF16 | 0.00723 | 0.00693 (0.00784) | 0.00784 (0.00862) | 0.9791 (0.9758) | 0.0170 (0.0200) | |
| C: dense, gsm8k (SGLang) | MXFP8 / MXFP8, all matched | 0.00681 | 0.00670 (0.00703) | 0.00730 (0.00759) | 0.9806 (0.9792) | 0.0158 (0.0183) |
| B: dense, DAPO (vLLM) | BF16 / BF16 | 0.00610 | 0.00882 (0.00764) | 0.00864 (0.00749) | 0.9828 (0.9870) | 0.0224 (0.0184) |
| KL reduction (95% interval) against | |||||||
|---|---|---|---|---|---|---|---|
| Pairing | kl , step 1 | kl | log_ppl_abs_diff | Pearson | chi2_token | BF16 sampler | gate_up BF16 |
| BF16 / BF16 | 0.00067 | 0.00072 | 0.00108 | 0.9979 | 0.00129 | – | – |
| MXFP8 / BF16 | 0.00854 | 0.00812 | 0.00868 | 0.9778 | 0.02279 | – | – |
| MXFP8 / MXFP8, gate_up BF16 | 0.00719 | 0.00726 | 0.00787 | 0.9794 | 0.02013 | 10.6% (8.6–12.7) | – |
| MXFP8 / MXFP8, all matched | 0.00697 | 0.00682 | 0.00739 | 0.9809 | 0.01841 | 16.1% (14.1–18.1) | 6.1% (3.9–8.3) |
| Micro-batch | Predicted MXFP8 BF16 (s) | |||||
| sequences (tokens) | per step | BF16 (s) | MXFP8 (s) | MXFP8 BF16 (s) | per micro-batch | per step |
| 1 (1.3k) | 512 | 69.12 | 84.49 | |||
| 2 (2.6k) | 256 | 42.90 | 48.71 | |||
| 4 (5.2k) | 128 | 32.72 | 33.36 | |||
| 8 (10.3k) | 64 | 32.47 | 24.86 | |||
| 4, half batch (5.2k) | 64 | 17.43 | 16.55 | |||
| Per micro-batch | BF16 | MXFP8 | Difference |
| Wall time under the profiler (ms) | 159.3 | 202.6 | |
| GPU kernel time, summed (ms) | 72.0 | 75.5 | |
| GEMM | 20.4 | 13.3 | |
| MXFP8 quantization | – | 2.2 | |
| scale rearrangement | – | 2.6 | |
| NCCL | 13.3 | 18.3 |
| Stage | Arm | (s) | ( s/token) | (ms per micro-batch) | (tokens per micro-batch) | |
|---|---|---|---|---|---|---|
| update_actor (fwd+bwd) | BF16 | 0.964 | – | – | ||
| FP8-TW | 0.899 | 45.3 | ||||
| MXFP8 | 0.932 | 51.5 | ||||
| old_log_prob (fwd) | BF16 | 0.872 | – | – | ||
| FP8-TW | 0.942 | 10.6 | ||||
| MXFP8 | 0.809 | 12.0 |
| BF16 / BF16 | FP8-TW / BF16 | FP8-BW / FP8-BW | MXFP8 / MXFP8 | |
| step (s) | 506.0 | 487.2 ( ) | 488.9 ( ) | 451.8 ( ) |
| generation | 133.6 | 133.1 ( ) | 115.5 ( ) | 101.7 ( ) |
| old log-prob | 63.9 | 64.8 ( ) | 66.1 ( ) | 60.5 ( ) |
| reference (BF16) | 57.3 | 57.3 ( ) | 56.5 ( ) | 57.1 ( ) |
| update_actor | 244.4 | 225.4 ( ) | 240.8 ( ) | 221.3 ( ) |
| weight sync | 3.5 | 3.4 | 6.3 | 7.5 |
| Setting | BF16 / BF16 | FP8-TW / BF16 | MXFP8 / BF16 | MXFP8 / MXFP8 | |
| A | gsm8k accuracy, step 0 60 | 0.3169 0.9363 | 0.3169 0.9249 | 0.3154 0.9356 | 0.3359 0.9393 |
| A | entropy change | ||||
| C | gsm8k accuracy, step 0 30 | 0.3184 0.8870 | 0.3230 0.8946 | – | 0.3738 0.8795 |
| C | entropy change | – | |||
| B | score, last 10 steps | 0.150 | 0.240 | – | 0.135 |
| B | entropy change | – |
| Failure | Breaks | Symptom | Root cause | Caught now by; fix | |
|---|---|---|---|---|---|
| F1 | lm_head quantized in the sampler (silent) | C1 | reward 0.000 for all 20 steps; responses at the length cap; NaN probability difference; untrained model scores 0.000 (healthy: 0.316) | not isolated: excluding lm_head removes the NaN logits; value range, scale handling in the vocabulary-parallel head and the kernel path remain candidates; with NaN logits EOS is never sampled | audit, sentinel; lm_head and embedding kept in BF16 |
| F2 | router exclusion silently removes dense MLP layers (silent) | C1 | none in the metrics; 56 of 311 parameters flagged on Qwen3-1.7B | vLLM 0.24’s ModelOpt matcher falls back to substring matching, so mlp.gate also excludes mlp.gate_up_proj | audit (engine re-check); exact matching on module paths |
| F3 | stale swizzled scales after sync, SGLang FlashInfer CUTLASS (silent) | C3 | held-out accuracy 0.000 from step 0; KL 7.2; entropy 10.4 ( uniform over the vocabulary) | the backend derives an interleaved scale copy at load that the sync never rebuilds | self-check, sentinel; refit loader re-runs post-processing |
| F4 | stale MoE expert layout after sync, vLLM TRT-LLM MXFP8 MoE (silent) | C3 | expert probe relative error 1.74 at the first sync (healthy 0.05); KL 0.047 vs. 0.0022 on a small randomly initialized MoE | an in-place repack that keeps shape and dtype escaped shape-based detection; a “done” flag also blocked re-processing | self-check (MoE probe); record same-shape repacks, merged as verl #7986 |
| F5 | BF16 MoE sync crashes on SM100 | C3 | crash at the first sync | vLLM picks TRT-LLM’s unquantized MoE kernel, whose 4-D layout the BF16 per-expert sync cannot write | crash; in-place staging, merged as verl #7987 |
| F6 | FP32 block scales on SM100 | C2 | assertion at the first FP8 quantization | TE emulates block scaling with MXFP8 GEMMs, whose E8M0 scales keep only the exponent | assertion; FP32 scales documented as Hopper-only, verl #8062 |
| Check | Tests | Runs | Catches | Cannot see |
|---|---|---|---|---|
| quantized-layer audit | C1 | once, at the first sync after a training step | any parameter whose precision differs between learner and engine, in either direction, read from the engine’s live state (vLLM) | values (C2–C6); the engine query counts only linear and expert layers, so a quantized lm_head shows only in the rule half; where no engine query exists (SGLang, FP8-blockwise path) it compares with the configured rule and inherits the rule’s errors |
| self-check | C3 (C6 for the probed layers) | every sync | stale derived state in every layer of a kind, whatever its shape and dtype, because it reads the kernel’s output | C2 (kernel and reference use the same synchronized weights); faults confined to layers or experts it does not probe; C4 (it calls the kernel eagerly, not through the CUDA graph) |
| step sentinel | C6 | every step | non-finite probabilities, KL above 1, nearly every response truncated on two steps, whatever the cause | divergence below its thresholds (F2, F4) |
| refit-cycle assertions | C4, C5 | every sync | an update that cannot be folded into the captured storage; staged buffers not folded back; scales a streamed update never wrote (SGLang) | storage the cycle does not track |
| byte comparison (offline) | C2, C3 | at two synchronizations of the controlled runs | a sampler weight, scale or kernel layout that differs from TE’s quantization of the synchronized weights, or a learner forward weight that does (Section 9.3 ) | layers it does not sample (3 of 36); it is not run during training |
| Fault | audit, rule half | audit, engine re-check | self-check | sentinel |
|---|---|---|---|---|
| F1 lm_head quantized | caught (t) | missed: the engine query counts only linear and expert layers (c) | missed: lm_head is not probed (c) | fires: NaN probability difference (m) |
| F2 gate_up_proj left in BF16 | missed: 28 of 28 layers agree (m) | caught: 56 of 311 parameters (m) | missed: probed layers are healthy (c) | silent: KL 0.007–0.010, far below 1 (m) |
| F3 stale swizzled scales (SGLang) | missed: layer set correct (c) | no engine query (c) | caught (t) | fires: KL 7.2 (m) |
| F4 stale expert layout (vLLM) | missed: 2 of 2 layers agree (m) | missed: 69 of 69 parameters agree (m) | caught: 1.739 vs. 0.25 at the first sync (m) | silent: KL 0.047 for 4 steps (m) |
| Injected violation | Breaks | Expected to catch it | Evidence now |
|---|---|---|---|
| a class of layers left in BF16 by the sampler | C1 | audit, engine re-check | CPU tests; hardware (F2) |
| new weights with stale derived scales | C3 | self-check | CPU tests; hardware (F4) |
| content permuted, shape and dtype kept | C3 | self-check, which reads the output | CPU tests; hardware (F4) |
| corruption confined to one expert or a late layer | C3 | none, by construction | open: size of the blind spot |
| update written outside the CUDA-graph storage | C4 | the refit cycle’s fold; the eager self-check cannot see it | CPU tests of the fold; open: graph replay |
| update incomplete across buckets | C5 | refit-cycle assertions | CPU tests (SGLang loader, BF16 MoE staging); open on hardware |
Appendix figures & tables1 asset
Supplementary material from the paper’s appendix.
Appendix
RL Fine-Tuning
Reinforcement learning fine-tuning adapts pretrained models and policies through task rewards, preference feedback, or environment interaction.
See all 232 papers