Repository navigation
[CUDA] Add decode GEMV kernel for K-packed NVFP4 QMoE - #33354
Open
Tianlei Wu (tianleiwu) wants to merge 3 commits into
Open
Tianlei Wu (tianleiwu) wants to merge 3 commits into
Tianlei Wu (tianleiwu) wants to merge 3 commits into
Conversation
Base automatically changed from
kvaishnavi/qwen38-nvfp4-offline-packing
to
main
October 16, 2026 04:51
Add MoeGemvFp4DecodeKernel for the offline K-packed (weights_prepacked=1) NVFP4 GEMV at decode row counts. It reproduces MoeGemvFp4RawKPackedKernel's fp32 summation order bit-exactly with 128-bit weight loads, several K tiles in flight, register FP4 decode and an FP32 activation row in shared memory. ORT_FP4_GEMV_DECODE_KERNEL=0 restores the previous kernel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tianlei Wu (tianleiwu)
force-pushed
the
tlwu/20261013/nvfp4-qmoe-decode-gemv
branch
from
October 16, 2026 05:32
4b4e16e to
60cd1fb
Compare
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Dispatch assertions reject supported PTX builds, and the generic-scale decoder needs committed regression coverage.
2 open findings
What changed in this PR
Adds optimized CUDA decode for offline K-packed NVFP4 QMoE, building on #33094, and preserves FP32 intermediates for ungated block-FP8 SiLU.
Changes:
- Adds decode GEMV dispatch with fallback and an opt-out.
- Retains FP32 FC1 accumulators through ungated FP8 bias and activation.
- Adds parity and precision tests, plus documentation.
| File | Description |
|---|---|
onnxruntime/test/python/transformers/test_qmoe_nvfp4_cuda.py |
Adds decode parity and dispatch tests. |
onnxruntime/test/python/transformers/test_qmoe_fp8_cuda.py |
Updates reference precision and adds regression tests. |
onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.h |
Supports separate projection and activation types. |
onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu |
Implements FP32 projection intermediates. |
onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc |
Allocates and routes FP32 SiLU projections. |
onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemv_fp4.cu |
Adds optimized decode kernel and guarded dispatch. |
docs/contrib_ops/cuda/moe_qmoe.md |
Documents decode eligibility and FP8 precision. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tianlei Wu (tianleiwu)
enabled auto-merge (squash)
October 17, 2026 06:20
This branch has not been deployed
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
Adds
MoeGemvFp4DecodeKernel, a faster decode kernel for the offline K-packed (weights_prepacked=1, row-major[E, N, K/2]) NVFP4 QMoE GEMV that #33094 introduced. It reproducesMoeGemvFp4RawKPackedKernel's fp32 summation order bit-exactly, so it is a drop-in replacement: QMoE outputs are bitwise identical with or without it. On H200 it makes the Qwen3.8 FC1+FC2 decode launches 1.47x faster (BF16, 10 expanded rows) and the QMoE node 11% (1 token) to 17% (2 tokens) faster end to end.Summary of Changes
Kernel
onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemv_fp4.cuMoeGemvFp4DecodeKerneland its launcher dispatch insideLaunchMoeGemvFp4RawNPacked(weights_row_majorbranch); the launcher now receivessmdocs/contrib_ops/cuda/moe_qmoe.mdORT_FP4_GEMV_DECODE_KERNELtable rowonnxruntime/test/python/transformers/test_qmoe_nvfp4_cuda.pyHow it matches
MoeGemvFp4RawKPackedKernel:KLanes = 4forK <= 1024, else8, copied from [CUDA] Support offline NVFP4 QMoE weights and fused block-FP8 execution #33094's launcher), same lane assignment (lane = block(16) % KLanes), same separate even/odd fp32 accumulators per lane, sameacc_even + acc_oddfollowed by the xor treeKLanes/2 .. 1, sameStoreMoeGemvFp4Pairepilogue (bias, fused SwiGLU).(2j, 2j+1): one 128-bit load covers two blocks, the xor-tree steps at lane distanceKLanes/2 .. 2are shuffles between theKLanes/2threads of a column, and the last step (distance 1) is an in-thread add.fp4 * 2^-126bf16 /fp4 * 2^-14fp16 bit patterns times a rescaled block scale, a single rounding identical toT(fp4 * scale)), E4M3 scale conversion withcvt.rn.f16x2.e4m3x2, and the activation row staged once per CTA as fp32 in shared memory.Dispatch (everything else falls back to
MoeGemvFp4RawKPackedKernelunchanged):weights_row_major,sm >= 90, FP16 or BF16, at most 80 expanded rows,K % 32 == 0,K <= 8192, evenN, 16/2/16-byte aligned weights/scales/activations.ORT_NVFP4_FC1_EXPERT_REUSE=1selects the two-row reuse variant.ORT_FP4_GEMV_DECODE_KERNEL=0(latched once per process, same convention asORT_FP4_GEMV_DEFAULT_TILING).Testing
Standalone harness (links
moe_gemv_fp4.cudirectly, calls the public launchers vs.MoeGemvFp4RawKPackedKernellaunched exactly like #33094's launcher), H200, CUDA 13.0:-1,E), offsets-only expert map, rows 1..80 (every row count), K = 64 .. 8192 including K not a multiple of 1024 (partial last tile: 98 and 34 blocks), big/tiny/zero global scales (generic decode path), and shapes the kernel declines (fallback).Kernel timing (us/launch, CUDA-graph average over 300 launches, 512-expert L2-cold layout, real FC1
N=1280 K=2560fused SwiGLU and FC2N=2560 K=640weights, H200):ORT-level QMoE node (Qwen3.8 shape
hidden=2560 inter=640 top_k=10, 512 experts, BF16,weights_prepacked=1, routing rotated over 64 inputs; GEMV kernel time per node from CUPTI, wall time perrun_with_iobinding):The node output is bitwise identical with
ORT_FP4_GEMV_DECODE_KERNEL=0and=1in this benchmark too.Python tests, CUDA plugin build of this branch (sm_90 only, H200,
CUDA_VISIBLE_DEVICES=2,ORT_TEST_CUDA_PLUGIN_EP=1):test_nvfp4_offline_row_major_decode_gemv_bit_exact(FP16 and BF16). Runs fiveweights_prepacked=1QMoE cases (Qwen3.8 shape at 1/2/6 tokens, i.e. 10/20/60 expanded rows, with and without bias; a partial-K-tile shape; aK % 32 != 0shape that the decode kernel declines), each checked against the PyTorch reference, and requires the output to be bitwise equal to a fresh process withORT_FP4_GEMV_DECODE_KERNEL=0. It also usestorch.profilerto assert that the decode kernel runs in the default process andMoeGemvFp4RawKPackedKernelruns with the opt-out.python -m pytest onnxruntime/test/python/transformers/test_qmoe_nvfp4_cuda.py: 178 passed, 8 skipped (opt-in large-memory tests), 0 failed (including alloffline_row_majorandkpackedtests from [CUDA] Support offline NVFP4 QMoE weights and fused block-FP8 execution #33094).lintrunner(CLANGFORMAT, RUFF, RUFF-FORMAT) on the touched files: no issues.Known limitations:
|global_scale| * 448 < 3.98; Qwen3.8 has at most 0.09). A per-expert fallback to a genericFp4I2FConverterdecode keeps results bit-identical for larger scales, but it is about 30% slower thanMoeGemvFp4RawKPackedKernel(BF16 top-10: 24.0/13.3 us vs 18.6/10.3 us). Such checkpoints should setORT_FP4_GEMV_DECODE_KERNEL=0.Motivation and Context
#33094 added the K-packed row-major layout and its native
MoeGemvFp4RawKPackedKernel. At Qwen3.8 decode shapes that kernel issues 8-byte weight loads and re-reads/decodes the activation row per 16-element block. This PR keeps its numerics and replaces only the memory/issue pattern.Related: #33323 / #33324 touch the same launcher (opt-in native E4M3 scale decoding) and are based on an earlier #33094 head, so they need a rebase onto #33094 regardless of this PR. The two are semantically compatible: the native-scale option only changes how E4M3 scales are converted (exactly), and the decode kernel already uses the hardware E4M3 conversion. The only overlap is a textual conflict next to the launcher dispatch (#33323 adds a parameter to the
launch_k_packedlambda right below the new decode dispatch).Checklist
ORT_FP4_GEMV_DECODE_KERNEL=0restores the previous kernel)docs/contrib_ops/cuda/moe_qmoe.md)