onnxruntime
56f6fee6 - [CUDA] QMoE GEMV fast path for batch-1 decode (#29038)

Commit
36 days ago
[CUDA] QMoE GEMV fast path for batch-1 decode (#29038) ## 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_k` rows (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 https://github.com/microsoft/onnxruntime/issues/29035. ## Key Changes ### New MoE GEMV kernel | File | Change | |---|---| | `contrib_ops/cuda/llm/moe_gemm/moe_gemv.h` | Public interface: `is_moe_gemv_supported` dispatch predicate and the symmetric INT launchers `launch_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.cu` | Symmetric INT MoE GEMV (INT4 `uint4b_t` / INT8 `uint8_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_size` 64/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 | File | Change | |---|---| | `contrib_ops/cuda/llm/moe_gemm/moe_kernels.cu` / `.h` | Branch FC1 and FC2 to the GEMV fast path when supported (carrying `group_size` through `QuantParams`); fall back to grouped GEMM otherwise. `ORT_DISABLE_MOE_GEMV=1` forces the grouped-GEMM path for A/B testing. | | `contrib_ops/cuda/llm/moe_gemm/moe_gemm_profiler.cc` / `.h`, `moe_util_kernels.h` | Use M (token count) in the MoE GEMM profiler so tactic selection reflects the decode shape. | ### Backward-compatible SwiGLU fusion | File | Change | |---|---| | `contrib_ops/cuda/moe/moe.cc`, `moe_quantization.cc` (+ `.h`) | Treat `swiglu_fusion == 0` with 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 no `swiglu_fusion` attribute, so it defaults to 0; those weights are pre-fused into FC1 and must be treated as fusion mode 1. | ### Tests, profiling, and docs | File | Change | |---|---| | `test/python/transformers/profile_qmoe_gemv.py` / `.sh` | Standalone QMoE GEMV profiling harness (NVTX-ranged, GEMV-vs-grouped-GEMM kernel comparison). | | `test/python/transformers/test_qmoe_cuda.py`, `test_moe_cuda.py` | QMoE GEMV decode-latency/parity coverage across INT4/INT8, per-column/block-wise, FP16/BF16, plus import tidy-ups. | | `docs/contrib_ops/cuda/qmoe_gemv_experiments.md` | Full experiment log: kernel-level sweeps, dispatch-gate tuning, block-wise and BF16 enablement, INT8 per-column root-cause + fix, and the GenAI end-to-end throughput comparison (CUTLASS baseline / GEMV final / FT baseline). | | `docs/contrib_ops/cuda/moe_qmoe.md` | QMoE doc updates describing the GEMV fast path and its dispatch gate. | ## Results ### GPT-OSS-20B INT4 end-to-end (H200/SM90, batch 1) Token-generation throughput (tps, higher is better): | Prompt length | CUTLASS baseline (gemm) | GEMV (final) | FT (ORT 1.26) | GEMV vs cutlass | GEMV vs FT | |---|---|---|---|---|---| | 128 | 248.9 | 288.0 | 265.2 | +15.7% | +8.6% | | 1024 | 237.8 | 272.2 | 252.9 | +14.5% | +7.6% | | 2048 | 231.3 | 265.0 | 245.6 | +14.6% | +7.9% | ### Decode microbenchmark coverage (H200/SM90, batch 1) Benchmark-loop latency in milliseconds, lower is better. `Enabled` is the default GEMV build; `Fallback` sets `ORT_DISABLE_MOE_GEMV=1` (grouped GEMM). Every case reported `has_invalid_output=false`. | Case | Quant | DType | Enabled ms | Fallback ms | Speedup | |---|---|---|---|---|---| | `int8_per_column_m1_top2_1024x4096_e8` | INT8 per-column | FP16 | 0.0566 | 0.0816 | 1.44x | | `int8_per_column_m1_top2_1024x4096_e8` | INT8 per-column | BF16 | 0.0578 | 0.0862 | 1.49x | | `gpt_oss_20b_m1_top4_int8_2880x2880_e32` | INT8 per-column | FP16 | 0.0785 | 0.0947 | 1.21x | | `gpt_oss_20b_m1_top4_int8_2880x2880_e32` | INT8 per-column | BF16 | 0.0785 | 0.0989 | 1.26x | Block-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). See `qmoe_gemv_experiments.md` for the full sweep. ## Correctness - GEMV-enabled and `ORT_DISABLE_MOE_GEMV=1` (grouped GEMM) produce identical, correct output for the GPT-OSS-20B sanity prompt; Nsight traces confirm `moe_gemv_kernel` (FC2) and `moe_gemv_interleaved_swiglu_kernel` (FC1) run for the decode shapes. - INT4/INT8 SwiGLU parity cases pass for FP16 and BF16 (max absolute difference ~1e-3 against the reference). Regression: `pytest -k "TestSwigluQMoE or TestQMoEIntPrePackSmoke"` → `34 passed, 4 skipped`. - Test on GPT-OSS-20b with block_size=32, and generated results looks good. ## Testing Notes - Build: `bash .env/cuda_130.sh --build` (CUDA 13.0, `CMAKE_CUDA_ARCHITECTURES=89;90`). Use `--clean_moe` after dispatch-code edits to avoid stale `moe_kernels.cu.o`. - Kernel profiling: `onnxruntime/test/python/transformers/profile_qmoe_gemv.sh`. - Parity/latency: `pytest onnxruntime/test/python/transformers/test_qmoe_cuda.py`. - End-to-end: GenAI `benchmark_e2e.py` on GPT-OSS-20B INT4, batch 1, prompt lengths 128/1024/2048. Run on an idle GPU — shared-GPU contention corrupts the decode-latency measurement. - A/B check: compare default vs `ORT_DISABLE_MOE_GEMV=1` for both throughput and output parity.
Author
Parents
Loading