[CUDA] QMoE: release raw MXFP4 weight initializers after PrePack - #31154
Merged
Conversation
tianleiwu
force-pushed
the
tlwu/20260730/fp4_qmoe_no_weight_copy
branch
from
July 30, 2026 18:29
f59da47 to
c60c50b
Compare
tianleiwu
force-pushed
the
tlwu/20260730/fp4_qmoe_no_weight_copy
branch
from
July 30, 2026 20:54
c60c50b to
22ff9d0
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR reduces persistent device-memory usage for MXFP4 QMoE on SM80–SM119 by enabling both prefill (SM80 grouped GEMM) and decode (fused GEMV) to share the same SM80 pair-interleaved prepacked weight buffers, and then allowing ORT to release the original raw FP4 weight initializers after PrePack.
Changes:
- Update FP4 GEMV decode to optionally read the SM80 pair-interleaved (prefill) weight layout by in-register un-permutation, avoiding a second decode-layout weight copy when shapes satisfy the interleaved rules.
- Enable releasing raw MXFP4 weight initializers after
PrePackin the SM80 grouped-GEMM regime, with a defensive runtime guard to fail fast if buffers are unexpectedly incomplete. - Add Python CUDA tests validating numerical parity vs. the dequant fallback and validating that raw initializer release returns device memory (when initializers bypass the arena), plus documentation for the single-copy regime.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| onnxruntime/test/python/transformers/test_qmoe_fp4_cuda.py | Adds SM80-regime tests for parity vs fallback and for device-memory reduction when raw initializers are released. |
| onnxruntime/contrib_ops/cuda/moe/moe_quantization.h | Adds state/flags tracking raw-initializer release and whether GEMV reads the SM80 layout. |
| onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc | Implements raw-initializer release via PrePack, routes decode GEMV to shared weights when possible, and adds a safety guard if fallback would need released tensors. |
| onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemv_fp4.h | Extends GEMV APIs to support an SM80 pair-interleaved weight mode and adds a layout-only shape gate helper for PrePack. |
| onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemv_fp4.cu | Adds the SM80-pair GEMV dispatch variant and the layout-only support predicate; threads the new flag through launch paths. |
| onnxruntime/contrib_ops/cuda/llm/fpA_intB_gemv/details.h | Extends FP4 decode to optionally invert the SM80 pair-interleave at compile time via a templated converter. |
| docs/contrib_ops/cuda/moe_qmoe.md | Documents the new SM80 single-copy weights behavior, shape gate, and the initializer-release interaction with the device allocator / arena. |
hariharans29
approved these changes
Jul 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
On SM80–SM119 the MXFP4 QMoE node used to keep up to three persistent copies of the expert
weights: the raw
[E, K, N/2]initializers (only ever read by the dequant fallback), the SM80pair-interleaved buffer consumed by the grouped-GEMM prefill, and a GEMV-native buffer consumed
by the fused decode GEMV. For a 20B-class MXFP4 MoE each copy is ~9 GiB, which put post-load
VRAM out of reach of 24 GB consumer cards.
This PR collapses that to a single copy in the default
ORT_FP4_SM80_GEMM=1regime:preprocessor step — the
[e0,e2,e4,e6,e1,e3,e5,e7]nibble pair-interleave applied byinterleave_int4s_inplace_kernel. Inverting it in the decoder is a compile-time index remapof the same eight
decodecalls (Fp4I2FConverter<AType, PairInterleaved>), so there are noextra branches, registers, or memory traffic.
gemv_fp4_fc{1,2}_reads_sm80_layout_recordsper-FC whether the dedicated
gemv_fp4_fc*_weights_decode_copy can be skipped.PrePackreleases the raw initializers. With both prefill and decode served frompre-packed buffers, the dequant fallback — the only consumer of the raw layout — is
unreachable, so
PrePackreportsis_packed = truefor inputs 2/5 and cachesfc*_weights_shape_somoe_helper::CheckInputscan still validate shapes. The staged e8m0block-scale copy is dropped in
TryBuildGemvFp4Scalesfor the same reason.A defensive guard in
ComputeInternalreturns a descriptive error (pointing atORT_FP4_SM80_GEMM=0) if the raw weights were released but the SM80 buffers are somehowincomplete, rather than dereferencing a null initializer.
Measured —
gpt-oss-20bMXFP4, post-load device memoryShape gate and fallback
Reusing the interleaved GEMV (
kInterleave=4,kStepK=32) inherits its rules — MXFP4group_size == 32,n % 16 == 0,k % 64 == 0— checked at pack time byis_moe_gemv_fp4_sm80_layout_supported. Shapes that miss the gate transparently keep thededicated decode-layout copy (logged at INFO). NVFP4 (block 16) always uses the plain
ColToRowlayout and is unaffected.
gpt-oss-20b(k=2880,n=5760/2880) clears the gate.Setting
ORT_FP4_SM80_GEMM=0restores the previous behavior: raw initializers retained, prefillon the dequant fallback, decode on the GEMV-native copy.
Motivation and Context
Makes 20B-class MXFP4 MoE models loadable on 24 GB consumer GPUs, and removes ~9 GiB of dead
device memory on every SM80–SM119 deployment.
Testing
New
TestQMoEFP4Sm80SingleWeightCopyinonnxruntime/test/python/transformers/test_qmoe_fp4_cuda.py(gated on a build with--use_fp4_qmoeand on80 <= SM < 120):test_sm80_parity_vs_dequant_fallback— fp16/bf16 × 1/4/64/256 tokens,hidden = inter = 512, 8 experts, top-2, SwiGLU. Every case is run twice, withORT_FP4_SM80_GEMM=1and=0,and both are compared against a PyTorch reference built from the dequantized weights. The
1/4-token cases exercise the fused decode GEMV un-permuting the pair-interleaved buffer; the
64/256-token cases exercise the SM80 grouped GEMM against the dequant fallback.
test_sm80_release_frees_raw_weight_initializers— 16 experts,hidden = 2048,inter = 1024,session.use_device_allocator_for_initializers=1. A throwaway sessionpre-warms the shared CUDA arena, then session-creation device-memory deltas are compared
between the retaining and releasing configurations.
Results on H200 (SM90), CUDA 13.0 — 8 passed:
Memory test: raw e2m1 initializers 48.0 MiB; retaining session 172.0 MiB vs releasing session
120.0 MiB → 52.0 MiB returned to the device.
Notes for reviewers
onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemv_fp4.{cu,h}also change in [CUDA] Address feedback for block-scaled MatMul and MoE GEMV #29910; expecta conflict depending on merge order.