[CUDA] QMoE GEMV fast path for batch-1 decode#29038
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a CUDA INT4 per-channel MoE GEMV fast path intended for batch-1 decode (small expanded row counts), wiring it into the existing CUTLASS MoE runner with an environment-variable kill switch, plus profiling/tactic-selection improvements and test+doc updates.
Changes:
- Add an
fpA_intB_gemv-based batched MoE GEMV kernel (including an interleaved SwiGLU-fused FC1 variant) and route FC1/FC2 to GEMV when supported, otherwise fall back to grouped GEMM (ORT_DISABLE_MOE_GEMV=1forces fallback). - Update MoE GEMM profiling/tactic caching to be M-bucket aware so decode vs prefill shapes can select different best tiles.
- Improve backward compatibility for legacy SwiGLU models (treat
swiglu_fusion==0+ no separate FC3 weights as interleaved fusion) and add benchmark harness/docs.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| onnxruntime/test/python/transformers/test_qmoe_cuda.py | Adds opt-in benchmark plumbing (NVTX ranges/env knobs), decode-sized parity coverage, and a helper benchmark runner. |
| onnxruntime/test/python/transformers/test_moe_cuda.py | Updates INT weight packing comments and tweaks test-case commentary to reflect GEMV routing. |
| onnxruntime/test/python/transformers/profile_qmoe_gemv.sh | New Nsight Systems profiling wrapper to compare GEMV vs grouped-GEMM fallback. |
| onnxruntime/test/python/transformers/profile_qmoe_gemv.py | New Python entrypoint to run the opt-in benchmark cases and emit structured results. |
| onnxruntime/contrib_ops/cuda/moe/moe.h | Removes per-instance cached GemmId members (now handled via profiler cache). |
| onnxruntime/contrib_ops/cuda/moe/moe.cc | Adds legacy SwiGLU fusion compatibility; updates profiler API usage and per-forward profiling. |
| onnxruntime/contrib_ops/cuda/moe/moe_quantization.h | Removes per-instance cached GemmId members (now handled via profiler cache). |
| onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc | Adds legacy SwiGLU fusion compatibility; updates profiler API usage and per-forward profiling. |
| onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_util_kernels.h | Extends fused prologue API to materialize a permuted-row→expert map. |
| onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_kernels.h | Plumbs the new permuted-row→expert map through GEMM entrypoints (for GEMV fast path). |
| onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_kernels.cu | Implements GEMV dispatch + env kill switch, writes the row→expert map in prologue, adds one-row finalize specialization, and wires GEMV into FC1/FC2. |
| onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemv.h | New public GEMV support predicate + launchers (incl. interleaved SwiGLU fused FC1). |
| onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemv.cu | New batched MoE GEMV CUDA kernels and launch logic based on fpA_intB_gemv building blocks. |
| onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemm_profiler.h | Updates profiler API and documents M-bucket-based caching. |
| onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemm_profiler.cc | Implements M bucketing, per-bucket caching, and nearest-bucket fallback. |
| docs/contrib_ops/cuda/qmoe_gemv_experiments.md | Adds a detailed experiment/profiling log to support future tuning/regression checks. |
| docs/contrib_ops/cuda/moe_qmoe.md | Documents the GEMV path, dispatch gates, and improvement plan; adds profiling workflow notes. |
sanaa-hamel-microsoft
approved these changes
Jun 15, 2026
tianleiwu
added a commit
that referenced
this pull request
Jul 9, 2026
This cherry-picks the following commits for the release: | Commit ID | PR Number | Commit Title | |-----------|-----------|-------------| | 56f6fee | #29038 | [CUDA] QMoE GEMV fast path for batch-1 decode | | a2c7c3b | #29081 | Fix QMoE CPU livelock by eliminating nested intra-op parallelism | | bd0cb9a | #28571 | [MLAS] KleidiAI fix igemm regression | | 5eb4aee | #29574 | Fix CustomOp forward compatibility: cap version instead of rejecting | | 5f49a37 | #29274 | fix(ci): incorrect identity for azcopy | | bb9ba7e | #29468 | Upgrade to Xcode 26 | | 36c6b7e | #29450 | Fix brew install applesimutils failure by trusting wix/brew tap | | a491809 | #29575 | Don't echo command when setting VSO variable in mac-cpu-packing-jobs.yml. | | a06675e | #29609 | Fix web e2e (npm/vite) and Python DML CI pipelines | Also fixed version missed by version update script. --------- Signed-off-by: Qxiang Xu <Qixiang.Xu@arm.com> Signed-off-by: Jonathan Clohessy <Jonathan.Clohessy@arm.com> Signed-off-by: Martin Klacer <martin.klacer@arm.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: tlwu <tlwu@example.com> Co-authored-by: Martin Klacer <martin.klacer@arm.com> Co-authored-by: Jonathan Clohessy <Jonathan.Clohessy@arm.com> Co-authored-by: Damien Dooley <damien.dooley@arm.com> Co-authored-by: Chi Lo <54722500+chilo-ms@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sanaa Hamel <sanaahamel@microsoft.com> Co-authored-by: Edward Chen <18449977+edgchen1@users.noreply.github.com>
This was referenced Jul 11, 2026
This was referenced Jul 20, 2026
Open
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.
Summary
Adds a symmetric weight-only MoE GEMV fast path for single-token (batch-1) decode of quantized MoE models such as GPT-OSS-20B, replacing the CUTLASS grouped-GEMM path when the expanded row count is small. The fast path now covers INT4 and INT8 weights, per-column and block-wise (group size 32/64/128) scales, and FP16 and BF16 activations. On an H200 (SM90) this improves end-to-end GPT-OSS-20B INT4 token-generation throughput by roughly 15% over the CUTLASS grouped-GEMM baseline and about 8% over the FasterTransformer kernel used in ORT 1.26 across prompt lengths 128/1024/2048, while producing bit-faithful output.
Motivation
At batch-1 decode each token expands to
top_krows (4 for GPT-OSS-20B), so the MoE FC1/FC2 GEMMs are extremely skinny. The CUTLASS grouped GEMM is built for throughput at larger M and leaves the decode path memory-bound and underutilized. A dedicated weight-only INT GEMV with per-expert dispatch is a better fit for this regime and closes the gap to (and surpasses) ORT 1.26 for GPT-OSS-20B.This also adds block_size=32 support as requested in #29035.
Key Changes
New MoE GEMV kernel
contrib_ops/cuda/llm/moe_gemm/moe_gemv.his_moe_gemv_supporteddispatch predicate and the symmetric INT launcherslaunch_moe_gemv_int_symmetric<T, WeightType>/launch_moe_gemv_int_symmetric_interleaved_swiglu<T, WeightType>(plus the original INT4 per-channel launchers).contrib_ops/cuda/llm/moe_gemm/moe_gemv.cuuint4b_t/ INT8uint8_t, FP16/BF16). One CTA per expanded row × N-tile; per-expert weight/scale/bias offsets via a direct row-to-expert map (prefix-offset scan fallback). Supports per-column (group_size <= 0) and block-wise (group_size64/128) scales. FC1 has an interleaved SwiGLU-fused epilogue; a static top-k one-row finalize specialization is used for FC2 routing.Runner wiring and profiler
contrib_ops/cuda/llm/moe_gemm/moe_kernels.cu/.hgroup_sizethroughQuantParams); fall back to grouped GEMM otherwise.ORT_DISABLE_MOE_GEMV=1forces the grouped-GEMM path for A/B testing.contrib_ops/cuda/llm/moe_gemm/moe_gemm_profiler.cc/.h,moe_util_kernels.hBackward-compatible SwiGLU fusion
contrib_ops/cuda/moe/moe.cc,moe_quantization.cc(+.h)swiglu_fusion == 0with no separate FC3 as interleaved fusion (== 1). The published GPT-OSS-20B model (and any model exported by ORT < 1.27) hard-coded the interleaved layout and emits noswiglu_fusionattribute, so it defaults to 0; those weights are pre-fused into FC1 and must be treated as fusion mode 1.Tests, profiling, and docs
test/python/transformers/profile_qmoe_gemv.py/.shtest/python/transformers/test_qmoe_cuda.py,test_moe_cuda.pydocs/contrib_ops/cuda/qmoe_gemv_experiments.mddocs/contrib_ops/cuda/moe_qmoe.mdResults
GPT-OSS-20B INT4 end-to-end (H200/SM90, batch 1)
Token-generation throughput (tps, higher is better):
Decode microbenchmark coverage (H200/SM90, batch 1)
Benchmark-loop latency in milliseconds, lower is better.
Enabledis the default GEMV build;FallbacksetsORT_DISABLE_MOE_GEMV=1(grouped GEMM). Every case reportedhas_invalid_output=false.int8_per_column_m1_top2_1024x4096_e8int8_per_column_m1_top2_1024x4096_e8gpt_oss_20b_m1_top4_int8_2880x2880_e32gpt_oss_20b_m1_top4_int8_2880x2880_e32Block-wise INT4 (
block_size=64,1024x4096, e8) routes to GEMV for both FP16 and BF16 with FC1/FC2 kernel times within noise across dtypes (FP16 4.53/6.95 us, BF16 4.62/7.01 us). Seeqmoe_gemv_experiments.mdfor the full sweep.Correctness
ORT_DISABLE_MOE_GEMV=1(grouped GEMM) produce identical, correct output for the GPT-OSS-20B sanity prompt; Nsight traces confirmmoe_gemv_kernel(FC2) andmoe_gemv_interleaved_swiglu_kernel(FC1) run for the decode shapes.pytest -k "TestSwigluQMoE or TestQMoEIntPrePackSmoke"→34 passed, 4 skipped.Testing Notes
bash .env/cuda_130.sh --build(CUDA 13.0,CMAKE_CUDA_ARCHITECTURES=89;90). Use--clean_moeafter dispatch-code edits to avoid stalemoe_kernels.cu.o.onnxruntime/test/python/transformers/profile_qmoe_gemv.sh.pytest onnxruntime/test/python/transformers/test_qmoe_cuda.py.benchmark_e2e.pyon GPT-OSS-20B INT4, batch 1, prompt lengths 128/1024/2048. Run on an idle GPU — shared-GPU contention corrupts the decode-latency measurement.ORT_DISABLE_MOE_GEMV=1for both throughput and output parity.