Skip to content

MoE: native FP8 QAT for routed-expert GMMs (FP8 forward + FP8-weight backward dLHS) - #5969

Open
shuningjin wants to merge 3 commits into
atwigg/mlperffrom
shuningjin/rl-qat
Open

shuningjin wants to merge 3 commits into
atwigg/mlperffrom
shuningjin/rl-qat

Conversation

@shuningjin

@shuningjin shuningjin commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Description

This PR adds a native-QAT path for the routed-expert GMMs, on top of the existing fake QAT (fp8_moe_fake_quant + fp8_fake_quant_hoist=gather8).

What fake QAT does today:

  • It all-gathers the FP8 weight codes.
  • It then dequantizes them to a BF16 copy in HBM.
  • Every GMM runs in BF16.

What the new flags change:

  • fp8_moe_native_fwd=true: the wi/wo forward GMMs run FP8×FP8 on the TPU7x MXU. They read the gathered codes and per-channel scale directly, and quantize the activation inside the kernel.
  • fp8_moe_native_bwd_dlhs=true: the backward dLHS GMM also reads the codes and dequantizes each tile in VMEM. No BF16 copy of the weight is written to HBM.
  • Numerics: the backward is bit-identical to the existing dequant path. The forward changes numerics only by quantizing activations to FP8.

All new flags default to off. With them off, the MoE GMM programs are byte-identical to base.

Related: the dropless full-buffer index clamp that was part of this PR is now #6068. The end-to-end results below were measured with that clamp in the tree; land #6068 first if you run dropless replay with moe_routing_index_math=count.

Why:

  • The trainer already holds FP8 weight codes after the gather8 all-gather. Fake QAT spends HBM bandwidth and time writing BF16 copies, then runs BF16 GMMs at half the TPU7x FP8 MXU throughput.
  • Running the forward in FP8 also matches what the FP8 rollout's MoE kernel computes.
  • On the Qwen3.5-397B RL trainer bench (128 chips, same image and HBM cap per pair), step time drops 2.56–3.03% vs fake QAT (throughput +2.63–3.13%). The range depends on the routing setting (see Tests).
  • In a GMM kernel microbenchmark on one TPU7x TensorCore at the trainer's per-device shapes, the native forward kernels (FP8 GMM, including the in-kernel BF16 → FP8 activation quantization) are 1.6–1.7× faster than the BF16 GMM, which needs no activation quantization (wi 1.635×, wo 1.731×).
  • At one seed, convergence shows no regression signal.

Data flow

flowchart LR
  subgraph F["Fake QAT"]
    F1["FP8 weights<br/>+ scales"] --> F2["dequant<br/>to BF16"]
    F2 --> F3["fwd GMM<br/>BF16 x BF16"]
    F2 --> F4["bwd dLHS<br/>BF16 x BF16"]
    F2 --> F5["bwd dW<br/>BF16"]
  end
  subgraph N["Native QAT (this PR)"]
    N1["FP8 weights<br/>+ scales"] --> N3["fwd GMM<br/>FP8 x FP8"]
    N1 --> N4["bwd dLHS<br/>VMEM dequant"]
    N1 --> N2["dequant<br/>to BF16"] --> N5["bwd dW<br/>BF16"]
  end
Loading
Stage Fake QAT (base) fp8_moe_native_fwd=true + fp8_moe_native_bwd_dlhs=true
Weight gather FP8 codes all-gathered (gather8) Same Same
Forward weight operand codes → BF16 in HBM (dequantize_weight_codes_ste) codes + [g,1,n] f32 scale, passed as rhs_scale Same
Forward activation BF16 BF16 → float8_e4m3fn in the kernel: one f32 absmax scale per row over the full K, f32 accumulation Same
Backward dLHS weight BF16 dequantized weight BF16 re-dequantized from the saved codes (same expression, bit-identical) Codes + scale read by the kernel. Each tile is dequantized into a VMEM cache (16 MiB for wi, 8 MiB for wo) inside the first matmul of its block.
Backward TGMM (dW) BF16 Unchanged Unchanged
Weight gradient Straight-through into the master weight Same, via a gradient anchor (below) Same

Gradient anchor: an FP8 primal would get an FP8 cotangent. So each weight also enters the op as its BF16 dequantized value, an extra input whose value the forward never reads; XLA drops that unread dequantization. The weight gradient is returned as that input's cotangent. It then flows through dequantize_weight_codes_ste to the FSDP shard exactly as it does today.

Implementation

  • kernels/megablox/ops.py adds four functions:

    • gmm_silu_fp8: wi with fused SwiGLU.
    • gmm_dswiglu_fp8: wo.
    • gmm_fp8_forward: forward only, for ring_ragged_unsort_down_projection.
    • dequantize_fp8_codes.

    Each custom_vjp forward calls gmm_v2(rhs=codes, rhs_scale=…, maybe_quantize_lhs=True). With fp8_moe_native_bwd_dlhs, the dLHS backward calls gmm_v2 with a codes RHS (rhs_contract_scale). Otherwise the backward dequantizes and reuses the existing BF16 backward.

  • pallas_mosaic_tpu_v2_gmm_kernel.py:

    • LHS quantization options: lhs_quant_block_size (512 or full K), lhs_f32_scale (max(amax, 1e-12)/448 in f32), lhs_hoist_quant and rhs_scale_first. fp8_moe_native_fwd always uses full K with an f32 scale and f32 accumulation; the other options are ops-level arguments for benchmarks and tests only.
    • The rhs_contract_dequant path: an FP8 double buffer with per-tile dequant into a VMEM cache, fused into the first matmul of each block by default.
    • Tiles that path can't take (tile_n % 32 != 0, tile_k % 128 != 0, non-dividing tiles) fall back to the XLA dequant. Production tiles take the native path.
    • The VMEM cache only hits with one K tile. With more, each step dequantizes inside its matmul; outputs are unchanged and it is still faster than the XLA-dequant fallback (see Tests).
  • layers/moe.py:

    • Adds an Fp8NativeWeight(anchor, codes, scale) pytree, built from the gather8 codes after the prefetch barrier. The shard-map in_specs give the scale P(e, None, n).
    • Dispatch points:
      • gmm_up_or_silu (wi);
      • _run_wo_gmm_dswiglu (wo);
      • down_projection / down_projection_bwd_dswiglu inside _ring_down_project_and_unpermute (ring-of-experts wo).
    • A RoutedMoE call with fp8_moe_native_fwd=true but no gather8 absmax now raises instead of silently running fake QAT. This happens for unscanned layers, KV-cache decode, or a Qwix-intercepted call.
  • configs/types.py, configs/base.yml: two new flags, both default off.

    Flag Values Recommended
    fp8_moe_native_fwd bool true
    fp8_moe_native_bwd_dlhs bool true
    • fp8_moe_native_fwd requires fp8_moe_fake_quant, fp8_fake_quant_hoist=gather8, sparse_matmul, use_tokamax_gmm, use_gmm_v2, moe_wi_gmm_fused_swiglu, moe_wo_gmm_fused_dswiglu, fixed tiles (use_gmm_v2_heuristic_tiling=false: the heuristic tiling budgets a bf16 accumulator when the LHS is quantized in the kernel, but the native forward accumulates in f32), and single-K-tile forward tiles (wi_tile_fwd_embed_dim = embed dim and wo_tile_fwd_mlp_dim = MoE MLP dim, plus eval_* when separate eval axis rules are set; make_gmm_configs also rejects tile_k != size_k when lhs_quant_block_size=0).
    • fp8_moe_native_bwd_dlhs additionally requires fp8_moe_native_fwd and moe_gmm_v2_dlhs_transpose_rhs.

Follow-ups:

  • FP8 backward for the weight-gradient TGMM (dW), which is still BF16.
  • Make fp8_moe_native_fwd imply fp8_fake_quant_hoist=gather8 and the fake-QAT flags; today it requires them, because it reuses gather8's FP8 weights.

Tests

Unit and correctness

Check Result
tests/unit/fp8_fake_quant_hoist_test.py (CPU) 38/38 pass
Outside-scan guard (CPU, shrunk Qwen3.5, 8 devices) scan_layers=false + native raises the new error. scan_layers=true + native passes the guard (and stops only at the TPU-only gmm_v2 kernel on CPU). gather8 without native still lowers.
Flags off vs base 22/22 output leaves sha256-equal at expert fills 0.40 / 1.00. The lowered TPU program of the MoE GMM ops (StableHLO plus every Mosaic module) is identical in 15/15 pairs (wi_vjp, wo_vjp, wo_ring at production, multi-tile and fallback tiles).
New tests/unit/fp8_moe_native_test.py (CPU) The flags are defined and off by default; the config validator accepts the native config and rejects each missing requirement (including heuristic tiling and multi-tile / oversized forward K tiles across train and separate-rule eval_*); make_gmm_configs rejects lhs_quant_block_size=0 with tile_k != size_k (also verified unmocked on tpu7x-8); fp8_moe_native_fwd passes full-K f32 activation quantization and f32 accumulation; a model with fp8_moe_native_fwd outside the layer scan raises instead of running fake QAT. 19/19 pass.
fp8_moe_native_bwd_dlhs vs dequant backward Bit-identical: 175 compares over 1-chip benches (small and production shapes, fills 0.29 / 0.40 / 1.00), 0 mismatches. At fallback tiles, flag on equals flag off.
dLHS with 2–4 K tiles (1 TPU7x core, production shapes) In-kernel dequant vs XLA-dequant fallback: wi 0.84× / 0.91× and wo 0.86× / 0.90× the time (2 / 4 K tiles), 0 mismatches.
Review follow-up (removed knobs, fixed-tile requirement) Lowered programs identical to the previous head: 12 kernel-level and 3 model-level programs, including native forward + dLHS with no knobs vs the old explicit fullk_f32 + float32 config.
Native forward numerics (1 chip; Gaussian, 20× outlier and Student-t₃ inputs) All outputs and gradients finite. fullk_f32 + f32 accumulation vs the rollout's MoE kernel: relative error 0.0131–0.0133 (per-512 BF16-scale variant, the old default: 0.0637; fake QAT: 0.0468).
Weight quantization (397B / 35B shapes) 0 scale differences (1.18M scales) and 0 code differences (1.61B codes) between eager and jit
AOT compile for tpu7x Forward kernels take f8e4m3fn weights with no BF16 weight upstream; backward kernels are the same 4 as base

Performance: Qwen3.5-397B-A17B, DeepSWE RL, TPU7x. Fake = base (caafa5bc6). Native = base + fp8_moe_native_fwd=true fp8_moe_native_bwd_dlhs=true, with full-K f32 activation scales and f32 accumulation (set then by two flags that are now the fixed behavior). Both arms use the same image and --xla_tpu_max_hbm_size_mib=90112.

Grouped matmul (GMM) kernel microbenchmark (one TPU7x TensorCore, the trainer's per-device shapes and production tiles):

GMM, tiles (tm, tk, tn) BF16 → FP8 speedup
wi (512, 4096, 2048) 1.635×
wo (512, 1024, 4096) 1.731×
  • Setting: kernel only, one call at full fill (64 experts, 225,280 rows).
    • The BF16 arm gets a weight that was dequantized to BF16 beforehand, so fake QAT's XLA weight dequant is not timed.
    • The FP8 arm takes the FP8 weight and scale directly, and its timing includes the in-kernel activation quantization.
  • Conclusion: the FP8 kernels are 1.6–1.7× faster even after paying for activation quantization. Against the full fake-QAT path, which also dequantizes the weight in XLA, the forward gain is larger.

Trainer bench (128 chips, 4x4x8, mean of steps 1–4; tok/s/chip = 37,748,736 tokens per step ÷ (128 × s/step)):

Setting Runs (fake / native) Fake s/step (tok/s/chip) Native s/step (tok/s/chip) Step time Throughput
lewu-a1-pr repro (+ HBM cap 90112): uniform routing, ragged_buffer_factor=2.75 sj-a4s-f-10251647 / sj-a4s-n-10251647 63.610 (4,636) 61.981 (4,758) −2.56% +2.63%
e2e-like MoE setting (as in pb-r2c9b2-s2's trainer): ragged_buffer_factor=2.0, synthetic production-like routing sj-a4edl-f-10251718 / sj-a4edl-n-10251718 83.288 (3,541) 80.761 (3,652) −3.03% +3.13%

Row 2 overflows the expert buffer on one micro-batch per step, and the dropless path reruns it (+16 s/step in both arms). That rerun needs the index clamp from #6068, which these runs had.

640-chip e2e (seed 677013886):

Run Role HBM cap Native flags
sj-c2-f-p1 Baseline: our repro of Param's pb-r2c9b2-s2 (fake QAT) 84992 (base) none
sj-c3-n-s2q Native QAT (this PR) 90112 fp8_moe_native_fwd (fullk_f32, f32 acc) + fp8_moe_native_bwd_dlhs

XProf (one trainer micro-batch, TPU:0):

Baseline Native QAT
wi + wo forward GMM (ms) 542.1 436.7
XLA weight-dequant fusions 240 (119.9 ms) 0
dLHS kernels with in-VMEM dequant 0 240 / 240
HBM peak (GiB, of 94.7) 85.2 86.6

Total device busy time isn't compared here: the two runs sampled different micro-batches (attention time alone differs by 247 ms). The step-time gain is measured in the trainer bench above.

Convergence:

Run pass@4 at step 6 (target > 0.69)
at-1015b-s1 0.777 ± 0.026
Fake QAT, Param's run (pb-r2c9b2-s2) 0.761 ± 0.027 (merged)
Fake QAT, our repro (sj-c2-f-p1) 0.757 ± 0.027 (merged)
Native QAT (sj-c3-n-s2q) 0.777 ± 0.026 (merged)

Checklist

Before submitting this PR, please make sure (put X in square brackets):

  • I have performed a self-review of my code. For an optional AI review, add the gemini-review label.
  • I have necessary comments in my code, particularly in hard-to-understand areas.
  • I have run end-to-end tests tests and provided workload links above if applicable.
  • I have made or will make corresponding changes to the doc if needed, including adding new documentation pages to the relevant Table of Contents (toctree directive) as explained in our documentation.

@codecov

codecov Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 23.80952% with 240 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ernels/megablox/pallas_mosaic_tpu_v2_gmm_kernel.py 25.35% 77 Missing and 29 partials ⚠️
src/maxtext/kernels/megablox/ops.py 23.36% 82 Missing ⚠️
src/maxtext/layers/moe.py 21.21% 51 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

…backward dLHS), and fix the dropless full-buffer gather index

Adds a native-QAT path on top of fake QAT (fp8_moe_fake_quant + fp8_fake_quant_hoist=gather8):

- fp8_moe_native_fwd: the wi/wo forward GMMs run FP8 x FP8 on the MXU from the gathered FP8 weights
  and per-channel scales; gmm_v2 quantizes the activation in the kernel
  (fp8_moe_native_fwd_lhs_quant, fp8_moe_native_fwd_acc_dtype).
- fp8_moe_native_bwd_dlhs: the backward dLHS GMMs read the FP8 weights and dequantize each tile in VMEM,
  fused into the first matmul of each block (rhs_contract_dequant); bit-identical to the XLA dequant
  backward. Unsupported tiles fall back to the XLA dequant and log a warning.
- Fp8NativeWeight(anchor, codes, scale) carries the weight through shard_map; the weight gradient is
  returned as the anchor's cotangent (straight-through into the master weight).
- A RoutedMoE call with fp8_moe_native_fwd but no gather8 absmax (outside the layer scan) raises instead
  of silently running fake QAT.
- ragged_sort: clamp the masked (-1) indices of the full-buffer ring gather-reduces; the unclamped rows
  halted the SparseCore on dropless replay. The clamp applies on every full-buffer ring gather-reduce,
  not only with moe_routing_index_math=count; it leaves outputs unchanged.
- gmm_v2: new lhs quantization kwargs (lhs_quant_block_size, lhs_f32_scale, lhs_hoist_quant,
  rhs_scale_first) and GmmConfigs fields; rejects a negative lhs_quant_block_size.
- Tests: fp8_moe_native_test (flags, config validator, outside-scan guard, dequantize_fp8_codes bit-identical
  to dequantize_weight_codes_ste, lhs_quant config values match ops._LHS_QUANT_MODES);
  ragged_sort_full_buffer_indices_test (cpu_only: it passes unclamped indices on purpose).

All new flags default to off; with them off, the lowered program is unchanged (lowered TPU StableHLO + Mosaic
kernels compared: 12 GMM-op programs and 3 model fwd+grad programs identical).
@shuningjin shuningjin changed the title MoE: native FP8 QAT for routed-expert GMMs (FP8 forward + FP8-code backward dLHS), and fix the dropless full-buffer gather index MoE: native FP8 QAT for routed-expert GMMs (FP8 forward + FP8-weight backward dLHS), and fix the dropless full-buffer gather index Oct 29, 2026
@shuningjin
shuningjin marked this pull request as ready for review October 29, 2026 18:52
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request implements a native FP8 Quantization-Aware Training (QAT) path for Mixture-of-Experts (MoE) GMMs, significantly enhancing training throughput by leveraging TPU7x hardware capabilities. By processing FP8 weights directly and performing in-kernel activation quantization, the changes reduce HBM bandwidth usage and increase computational efficiency. Additionally, the PR addresses a pre-existing bug in the dropless ring gather-reduce logic, ensuring index safety and preventing SparseCore crashes during micro-batch replays.

Highlights

  • Native FP8 QAT Support: Introduced a native FP8 QAT path for routed-expert GMMs, enabling FP8 forward and backward dLHS computations on TPU7x MXU hardware.
  • Performance Optimization: Achieved 1.6-1.7x speedup in GMM kernels by performing in-kernel activation quantization and avoiding redundant BF16 weight copies in HBM.
  • Bug Fix: Resolved a critical out-of-bounds index error in the dropless ring gather-reduce operation that previously caused SparseCore runtime halts.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

Comment thread src/maxtext/configs/base.yml Outdated
# gmm_v2 accumulator across K blocks for fp8_moe_native_fwd: "" (kernel default, bfloat16), "bfloat16" or "float32".
fp8_moe_native_fwd_acc_dtype: ""
# In-kernel lhs quantization scheme for fp8_moe_native_fwd ("" = default block512_bf16; see types.py).
fp8_moe_native_fwd_lhs_quant: ""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the recommended settings be the defaults here, fullk_f32 and float32?

setting only fp8_moe_native_fwd: true gives a bf16 scale per 512-column block and bf16 accumulation. Your numerics table shows that combination is further from the FP8 rollout than fake QAT (0.0637 vs. 0.0468), while fullk_f32 with f32 accumulation gets 0.0131. So the simplest config is the least accurate one, and you only get the good numerics if you know about two extra flags.

Both flags are only read when fp8_moe_native_fwd is on, so the flags-off path doesn't change. The types.py defaults and test_flags_are_defined_and_off_by_default would need the same update.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good point. In 10fdc4f I removed both knobs instead: fp8_moe_native_fwd: true now always uses per-row full-K f32 scales and f32 accumulation, which is both the closest to the rollout kernel (0.0131 vs 0.0637) and the fastest. The other modes remain ops-level arguments for benchmarks.

The native forward now also requires fixed tiles, since the heuristic tiling budgets a bf16 accumulator and could overflow VMEM with f32. Lowered programs are unchanged vs the old explicit config; tests updated.

else:
tile_k, tile_n = tiling[4], tiling[5]
size_n, size_k = rhs_shape[1], rhs_shape[2]
supported = tile_n % 32 == 0 and tile_k % 128 == 0 and size_n % tile_n == 0 and size_k % tile_k == 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this check also needs "one K tile", i.e. tile_k == size_k.

The kernel's VMEM cache holds one weight block, keyed by (expert, n tile, k tile), and the grid loops over K innermost (n → m tile → k). With two or more K tiles, each step evicts the block the next step needs. The cache then never hits, and every step dequantizes its weights again. The _rhs_cache_miss docstring already assumes a single K tile, but nothing enforces it.

The MLPerf recipe isn't affected, because its tuned tiles give one K tile for both wi and wo. But a config with wi_tile_dlhs_mlp_dim=1024 (the recipe's value before the tuned override) would silently take this path with 2 K tiles, since the fused wi has K = 2 × 1024. If multi-K-tile support matters, the alternative is to cache the whole [tile_n, size_k] block, keyed by (expert, n).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks! Agreed the cache never hits with more than one K tile (n tiles are fine; the grid is (n, gm, k)). I measured it on TPU7x at our shapes before gating, and the miss path is still 9-16% faster than the XLA-dequant fallback, bit-identical, since it reads FP8 instead of a bf16 copy:

DLHS tiles (m, k, n) K tiles fallback in-kernel
wi 256, 1024, 4096 2 5.01 ms 4.20 ms
wi 256, 512, 4096 4 5.29 ms 4.84 ms
wo 512, 2048, 1024 2 2.11 ms 1.82 ms
wo 512, 1024, 1024 4 2.38 ms 2.14 ms

So I kept the path and clarified the _rhs_cache_miss docstring in 10fdc4f. Caching the full [tile_n, size_k] block would need 16 MiB of VMEM for wi, so I left that out.

return pos.reshape(-1)


def _clamp_masked_indices(indices, valid_rows_mask):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: suggest taking this index fix into its own PR first? It could land right away instead of waiting on the kernel review, and if the native path ever needs a revert, the crash fix stays in.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, done: moved to #6068 and removed here in 10fdc4f.

…, multi-K DLHS doc; split out the ragged_sort fix

- Remove fp8_moe_native_fwd_lhs_quant and fp8_moe_native_fwd_acc_dtype: fp8_moe_native_fwd always quantizes the
  activation with one float32 absmax scale per row over the full K dim and accumulates in float32 (the setting
  that measured closest to the rollout's FP8 MoE kernel and fastest); the other modes stay ops-level arguments for
  benchmarks and tests. Lowered programs are unchanged vs the old explicit fullk_f32 + float32 config.
- fp8_moe_native_fwd requires use_gmm_v2_heuristic_tiling=False: the heuristic tiling budgets a bf16 accumulator
  when the lhs is quantized in the kernel, so with the float32 accumulator its tiles could exceed VMEM.
- _rhs_cache_miss: document that the DLHS dequant cache only hits with a single K tile; with more, each step
  dequantizes inside its matmul, unchanged outputs, still faster than the XLA dequant fallback.
- Move the ragged_sort full-buffer index clamp and its test to a separate PR.
@shuningjin shuningjin changed the title MoE: native FP8 QAT for routed-expert GMMs (FP8 forward + FP8-weight backward dLHS), and fix the dropless full-buffer gather index MoE: native FP8 QAT for routed-expert GMMs (FP8 forward + FP8-weight backward dLHS) Oct 31, 2026
# enough to minimize compute overhead of quantization.
quant_block_size=512,
quant_block_size=(
dims.size_k if lhs_quant_block_size == 0 else (lhs_quant_block_size or _DEFAULT_LHS_QUANT_BLOCK_SIZE)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here lhs_quant_block_size == 0 sets quant_block_size = dims.size_k, but k_ranges in inner_kernel (line 926) only iterates within a single tile_k (range(0, cfgs.tiles.tile_k, q_block_size)).
if someone enables fp8_moe_native_fwd=True on Qwen3.5-397B (emb_dim=4096) without overriding base.yml's default wi_tile_fwd_embed_dim: 1024 (or under eval rules with eval_wi_tile_fwd_embed_dim: 1024), wi will silently compute one scale per 1024-column tile instead of one scale over the full K dim?

@shuningjin shuningjin Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — confirmed (tile_k > size_k also fails on rhs_scale bounds). Our Qwen3.5-397B RL runs here were unaffected (the recipe explicitly sets wi_tile_fwd_embed_dim=4096 and wo_tile_fwd_mlp_dim=1024, with no separate eval_* rules in RL), though runs leaving base.yml's 1024 default would hit it.

Guarded at both layers in 22ebd03 (covered in fp8_moe_native_test.py and verified on tpu7x-8):

  1. Kernel (make_gmm_configs): raises NotImplementedError when dynamic in-kernel LHS quant uses lhs_quant_block_size == 0 with tiles.tile_k != dims.size_k.
  2. Config (_fp8_moe_native_fwd_tile_requirements): checks upfront (when K is unsharded) that wi/wo (and eval_* when enabled) forward tiles equal emb_dim / moe_mlp_dim.

@uwlei uwlei left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HBM cap 84992 vs 90112: sj-c3-n-s2q bumped the HBM cap from 84992 to 90112 and peak HBM went from 85.2 to 86.6 GiB. Does fp8_moe_native_fwd + fp8_moe_native_bwd_dlhs fit within the recipe's default 84992 cap (including dropless replay), or will enabling this in the recipe need the 90112 cap too?

@shuningjin shuningjin left a comment •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HBM cap 84992 vs 90112: sj-c3-n-s2q bumped the HBM cap from 84992 to 90112 and peak HBM went from 85.2 to 86.6 GiB. Does fp8_moe_native_fwd + fp8_moe_native_bwd_dlhs fit within the recipe's default 84992 cap (including dropless replay), or will enabling this in the recipe need the 90112 cap too?

TL;DR: It fits under the default 84992 cap without OOM (including dropless replay), so enabling it in the recipe does not require 90112. Bumping to 90112 is optional (8.1 GiB free out of 94.7 GiB in sj-c3-n-s2q) and only avoids extra XLA remat on larger-buffer steps (rbf=2.75 or dropless replay).

  • Why sj-c3-n-s2q had 90112: We didn't bump it for e2e (rbf=2.0) — we added 90112 during our 128-chip speed screen on lewu-a1-pr (rbf=2.75), where larger ragged buffers pushed the module over 84992 and triggered ~120 ms/micro of extra XLA remat (0 remat at 90112, both arms run at 90112). We kept that flag unchanged when launching sj-c3-n-s2q.
  • Normal recipe steps (rbf=2.0): 84992 MiB (83.0 GiB) caps one XLA module (excluding the ~6 GiB persistent grad accumulators shown in XProf peak). Our earlier 640-chip run at default 84992 (sj-c2-n-p1, fp8_moe_native_fwd only) peaked higher (88.4 GiB) with 0 XLA remat ops, and adding fp8_moe_native_bwd_dlhs drops peak to 86.6 GiB.
  • Dropless replay: Lowering the XLA cap triggers remat rather than OOM. Our earlier 84992 run compiled and launched dropless replay without OOM (before hitting the pre-#6068 index fault); full replay timing was verified at 90112 (sj-a4edl-n, 86.5 GiB peak), so under 84992 an overflowed micro-batch may pay ~0.1–0.2 s of extra remat.

This branch has not been deployed

No deployments
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