Skip to content

[CUDA] Add Tensor-Core BF16 INT2 QMoE GEMV kernel and 3-mask BF16 converter - #33786

Merged
Tianlei Wu (tianleiwu) merged 2 commits into
mainfrom
tlwu/20261031/qmoe-cuda-int2-tc
Nov 2, 2026
Merged

Tianlei Wu (tianleiwu) merged 2 commits into
mainfrom
tlwu/20261031/qmoe-cuda-int2-tc

Conversation

@tianleiwu

@tianleiwu Tianlei Wu (tianleiwu) commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds a Tensor-Core (mma.sync.m16n8k16, BF16 inputs, FP32 accumulation) grouped GEMV kernel (MoeGemvTcInt2Kernel / TryLaunchInt2Bf16) for symmetric INT2 (cutlass::uint2b_t, block_size ∈ {32, 64, 128}) com.microsoft::QMoE projections, and optimizes Int2NumericArrayConverter<bfloat16_t, N> in interleaved_numeric_conversion.h using a 3-mask BF16 mantissa trick. On NVIDIA H200 (S = 8, k = 10, 80 routes, H = 2560, I = 640, block_size = 64), Nsight Compute (ncu) profiling shows 2.02x faster FC1+SwiGLU (70.56 us -> 35.01 us) and 1.95x faster FC2 (47.01 us -> 24.13 us) compared to the scalar SIMT moe_gemv kernel.

Summary of Changes

Tensor-Core BF16 INT2 QMoE GEMV & 3-Mask BF16 Converter

File Change
onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemv_tc.cuh Implements MoeGemvTcInt2Kernel, TryLaunchInt2Bf16, DequantPairInt2Masked, ComputeWordInt2, and DiscoverExpertRouteChunk (grouping up to 8 routes per expert into an mma.sync.m16n8k16 warp tile with software-pipelined activation and streaming weight/scale loads, supporting block_size ∈ {32, 64, 128} and sorted route discovery beyond 256 routes).
onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_gemv.cu Dispatches BF16 cutlass::uint2b_t launches to fiv::moe_tc::TryLaunchInt2Bf16. The tensor_core_min_rows parameter (default 33) of the two launchers already exists on main and is reused as-is.
onnxruntime/contrib_ops/cuda/llm/cutlass_extensions/interleaved_numeric_conversion.h Optimizes Int2NumericArrayConverter<bfloat16_t, N> to extract three 2-bit fields (0x00030003, 0x000C000C, 0x00300030) inside BF16's 7-bit mantissa with only two right-shifts (>> 6 and >> 12) per 32-bit packed word instead of 8 shifts.
onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc Adds int_scale_supports_packed_execution in QMoE::PrePack so initializer scales are transposed once when enable_int2_gemv_ or enable_int2_prefill_ is active. The ep.cuda.moe_gemv_tc_min_rows / ep.cuda.disable_moe_gemv_tc controls (ORT_MOE_GEMV_TC_MIN_ROWS / ORT_DISABLE_MOE_GEMV_TC) are already parsed on main and are reused unchanged.
docs/contrib_ops/cuda/moe_qmoe.md Documents the BF16 Tensor-Core INT2 GEMV path and session/environment controls.
onnxruntime/test/contrib_ops/moe_test.cc Adds QMoETest_Bf16Int2TensorCoreGemvParity (across block_size ∈ {32, 64, 128} and 6 shapes, including the odd-step hidden_size = 640, inter_size = 512 case that exercises the single-K-warp SwiGLU kernel) and QMoETest_Bf16Int2TensorCoreGemvExtremeScales, and starts CUPTI profiling after session.Initialize() in QMoETest_Int2CudaCachedScalesDecodeThenPrefill.

Testing

  • Built onnxruntime_provider_test and ran ./build/direct_cuda/Release/onnxruntime_provider_test --gtest_filter="MoETest.*" on NVIDIA H200 (123/123 passed, including QMoETest_Bf16Int2TensorCoreGemvParity, QMoETest_Bf16Int2TensorCoreGemvExtremeScales, and QMoETest_Int2CudaCachedScalesDecodeThenPrefill).
  • Re-verified at 9ae9283 with a CUDA-EP-as-plugin build (CUDA 13.0, NVIDIA H200, sm_90): MoETest.* gives 96 passed, 17 skipped (CPU-only tests and routing-statistics tests that the CUDA plugin EP does not support), 0 failed. Nsight Systems shows the single-K-warp SwiGLU variant MoeGemvTcInt2Kernel<true, 1, 1, G> launched once per block size for the odd-step {hidden_size = 640, inter_size = 512} shape, and compute-sanitizer --tool memcheck reports 0 errors on QMoETest_Bf16Int2TensorCoreGemvParity.
  • Profiled MoeGemvTcInt2Kernel vs moe_gemv_interleaved_swiglu_kernel / moe_gemv_kernel with Nsight Compute (ncu) on NVIDIA H200 (1.99x total layer speedup at T = 8, k = 10).
  • Verified clang-format --dry-run --Werror and git diff --check.

Checklist

  • Tests added/updated
  • No breaking changes (or documented in description)
  • Documentation updated (if applicable)

Copilot AI balanced review requested due to automatic review settings October 31, 2026 18:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The undefined TryParseSignedInt64 calls block compilation, and the tests lack coverage for the new odd-step single-K-warp SwiGLU variant.

2 open issues
What changed in this PR

Adds a BF16 INT2 Tensor-Core GEMV path to CUDA QMoE, accelerating grouped expert projections while retaining the existing fallback paths.

Changes:

  • Introduces grouped Tensor-Core GEMV kernels and optimizes BF16 INT2 conversion.
  • Adds dispatch controls and expands initializer-scale prepacking.
  • Adds parity and extreme-scale tests, updates profiling setup, and documents configuration options.
File Description
onnxruntime/​test/​contrib_ops/​moe_test.cc Adds BF16 INT2 parity and extreme-scale tests; starts profiling after initialization.
onnxruntime/​contrib_ops/​cuda/​moe/​moe_quantization.h Stores the Tensor-Core dispatch threshold.
onnxruntime/​contrib_ops/​cuda/​moe/​moe_quantization.cc Parses dispatch controls, passes thresholds, and expands scale prepacking.
onnxruntime/​contrib_ops/​cuda/​llm/​moe_gemm/​moe_gemv.h Adds threshold parameters to symmetric GEMV launch interfaces.
onnxruntime/​contrib_ops/​cuda/​llm/​moe_gemm/​moe_gemv.cu Routes eligible BF16 INT2 launches to Tensor-Core kernels.
onnxruntime/​contrib_ops/​cuda/​llm/​moe_gemm/​moe_gemv_tc.cuh Implements grouped Tensor-Core GEMV and fused SwiGLU execution.
onnxruntime/​contrib_ops/​cuda/​llm/​cutlass_extensions/​interleaved_numeric_conversion.h Uses three masks to reduce BF16 INT2 conversion shifts.
docs/​contrib_ops/​cuda/​moe_qmoe.md Documents the new kernel path and configuration controls.

Review effort: Balanced

…verter

Add MoeGemvTcInt2Kernel (mma.sync.m16n8k16) and TryLaunchInt2Bf16 for
symmetric INT2 QMoE projections (block_size in {32, 64, 128}), optimize
Int2NumericArrayConverter<bfloat16_t, N> using three 2-bit masks inside
BF16's 7-bit mantissa, and cache transposed INT2 scales in QMoE::PrePack.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The INT2 test leaves second-chunk routing and output indexing untested, while the scale-caching gate adds an unusable GPU allocation and transpose when weights are not prepacked.

2 open issues
2 resolved since last review

Review effort: Max

…MV test helper

Remove redundant int_scale_supports_packed_execution predicate in QMoE::PrePack
so runtime weights with initializer scales do not allocate unused transposed
scale buffers, and reuse RunQMoEBf16TensorCoreGemvParityCase(/*bits=*/2, ...)
in the INT2 Tensor-Core GEMV tests to cover multi-chunk (>8 routes) expert routing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tianleiwu
Tianlei Wu (tianleiwu) enabled stacked auto-merge (squash) November 2, 2026 23:06
@tianleiwu
Tianlei Wu (tianleiwu) merged commit f42c839 into main Nov 2, 2026
@tianleiwu
Tianlei Wu (tianleiwu) deleted the tlwu/20261031/qmoe-cuda-int2-tc branch November 2, 2026 23:08
Tianlei Wu (tianleiwu) added a commit that referenced this pull request Nov 3, 2026
… for SM12x and 256K context (#33802)

## Summary
Stacked on #33786 (	lwu/20261031/qmoe-cuda-int2-tc).

This PR optimizes CUDA prefill and decode kernels for
**Qwen3.8-Flash-Next (RC5)** on **Blackwell SM 12.x (100 KB shared
memory/SM, RTX Spark N1X)** from short/medium contexts (2K–32K) up to
**256K (262,144) context length**:

1. **DynamicSparseAttention (dynamic_sparse_attention.cc,
dynamic_sparse_attention_impl.cu,
dynamic_sparse_attention_prefill_int8.cuh,
dynamic_sparse_attention_helper.h)**:
- Enable DynamicSparseAttentionInt8TcKernel prefill & split-K decode by
default on SM >= 8.0.
- Add a 2-stage shared-memory pipeline (kTwoBufferSmem, 38.1 KB / 40.4
KB smem) on 100 KB/SM architectures (SM 8.6/8.9/12.0/12.1) to double
occupancy from 1 to 2 CTAs/SM (**2.83x faster DSA prefill**, **7.5x
faster DSA decode**).
- Clamp otal_sequence_length to past_cache_capacity when a smaller
runtime past_key cache is bound via
untime_profiles.
2. **PackedSparseAttentionIndexer (packed_sparse_attention_indexer.cc,
packed_sparse_attention_indexer_impl.cu)**:
- Tile **8 query tokens per CTA** (4 in registers + 4 in shared memory)
in QsaRaggedPrefillScoreKernel<T, TState, true> and replace
shared-memory reduction barriers with a bit-exact 19-shuffle 16-head
butterfly reduction, cutting present_key_state DRAM reads by 2x and
speeding up 128K prefill scoring by **1.96x (11.67 s -> 5.94 s)**.
- Avoid zero-filling inactive 2048-key chunks in QsaScoreTileTopKKernel
beyond element-0 sentinels, eliminating the 256K state_capacity padding
tax on short/medium prompts.
- Aggregate radix histogram updates in QsaPartialTopKKernel with
warp-level __match_any_sync and extend single-launch
QsaFinalMergeAndEmitKernel up to 64 chunks (256K context).
- Allow runtime past_key_state capacity to be sized dynamically from
dynamic_batching /
untime_profiles.
3. **HyperConnectionLowRankMix
(hyper_connection_low_rank_mix_impl.cu)**:
- Tune kWeightSlots to 5 on __CUDA_ARCH__ >= 1000 (30.8 KB in
HcDownKernel, 23.6 KB in HcUpPreKernel), increasing 100 KB/SM occupancy
from 2 to 3–4 CTAs/SM (-34% to -43% kernel time).
4. **QMoE Decode & Prefill (moe_gemv_tc.cuh, qmoe_int3_tc.cuh,
qmoe_kernels.cu, int8_tc_gemv.cuh)**:
- Increase kRouteChunks to 3 (38.7 KB smem -> 2 CTAs/SM) for otal_routes
<= 96 and enable kM = 2 2-route FC2 tile sharing in MoeGemvTcInt2Kernel.
- Enable kRemMode = 1 FC2 remainder prefetch in Int3TcCoalescedKernel.

## Validation
- End-to-end smoke.py and 10-item MMLU-Pro smoke evaluation across all 3
Qwen3.8-Flash-Next RC5 variants (2b375, 3b3_16, all_int4).
- 2K / 8K / 32K and 8K / 128K / 255K + 1K (262,144 total tokens) TTFT &
decode benchmarks on RTX Spark N1X (SM 12.1):
- 2b375: 32K TTFT 24.85s -> 19.63s (**+26.6%**), 128K TTFT 85.96s ->
75.06s (**-10.91s**), 255K TTFT 205.73s -> 164.07s (**-41.66s,
+25.4%**), 128K decode **97.13 tok/s**.
- 3b3_16: 32K TTFT 29.96s -> 24.47s (**+22.4%**), 128K TTFT 101.07s ->
86.50s (**-14.57s**), 255K TTFT 234.31s -> 197.38s (**-36.94s,
+18.7%**).

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tianlei Wu (tianleiwu) added a commit that referenced this pull request Nov 4, 2026
…akeup (#33787)

## Description

Stacked on #33731 (and includes #33785 and #33786 so the branch builds
and passes CI standalone). Enables portable runtime CPU/GPU offload
(`session.moe.offload.*` / `int2_bank_mode_`) for SM80-prepacked `INT2`
(`expert_weight_bits = 2, weights_prepacked = 1, block_size ∈ {32, 64,
128}`) and mixed `INT4 + INT3 + INT2` `com.microsoft::QMoE` models (such
as the 2.375-bit 48-layer Qwen3.8-Flash-Next RC5 candidate
`qmoe_2b375_x02_y14_z32_dp23_b64` with `2` INT4 + `14` INT3 + `32` INT2
layers), while optimizing `SharedSpinPool` inter-step spin duration and
`SessionPromotionBudget` pinned bounce allocation.

## Summary of Changes

### 1. `INT2` Offload Admission & `int2_bank_mode_` Execution (`Commit
3`)

| File | Change |
|------|--------|
| `onnxruntime/core/framework/session_state.cc` | Admits prepacked
`INT2` (`weight_bits == 2 && prepacked == 1 && block_size ∈ {32, 64,
128}`) in `SessionState::ValidateNodeForMoeOffload`. |
| `onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc` | Admits
`int2_sm80_prepacked` `QMoE` nodes into the v4 offload planner using
`MakeInt2ExpertBlockLayout` for `cpu_slot_stride`. |
| `onnxruntime/contrib_ops/cuda/moe/qmoe_offload.h` & `.cc` | Adds
`int2_bank_mode_` and `int2_block_size_` across GPU resident slab sizing
(`1,382,408 B/slot` at `H=2560, I=640, b64`), `FileExpertStore` bounded
host cache (`1,382,912 B/slot`), scale transposition
(`CopyOrTransposeScaleBankToHost`), CPU doorbell cold-expert execution
(`Int2Fc1SwiGluRange` / `Int2Fc2Range`), GPU hot resident decode
(`launch_moe_gemv_int_symmetric*<T, cutlass::uint2b_t>`), and GPU wave
streaming prefill. |
| `onnxruntime/contrib_ops/cuda/moe/qmoe_offload.cc` | Caps per-slot
promotion bounce allocation in `SessionPromotionBudget::EnsureCapacity`
to `pinned_limit / 6` while preserving `decode_reserve = 24 MiB` so all
48 layers can promote experts under a `128 MiB` `pinned_bytes` budget
without exhausting persistent decode pinned reservations. |
| `onnxruntime/contrib_ops/cuda/moe/qmoe_offload.cc` | Extends the
default decode spin window (`ORT_QMOE_SHARED_POOL_SPIN_US` and
`DoorbellService::Loop`) from `2,000 us` to `15,000 us` so
`SharedSpinPool` workers stay awake across the `~7.6 ms` DFlash2 7-draft
generation window between target verification steps, eliminating a `~4.0
ms` Layer 0 futex wakeup stall per step. |
| `onnxruntime/test/python/test_qmoe_offload_v4.py` | Extends
`test_v4_int4_and_int3_offload_modes_and_numerical_parity` with `INT2`
(`block_size ∈ {32, 64}`) and 3-way mixed `INT4 + INT3 + INT2` offload
numerical parity tests across `adaptive` and `cpu_execution=disabled`
streaming modes. |
| `docs/moe-cpu-offload/qmoe_offload.md` | Updates offload overview for
`INT4/INT3/INT2` models. |

### 2. Prerequisite Commits Included for Standalone CI

- **Commit 1 (#33785)**: `[CPU/MLAS] Add QMoE shared layout INT2
AVX-512/AVX2 kernels and direct CPU QMoE`
- **Commit 2 (#33786)**: `[CUDA] Add Tensor-Core BF16 INT2 QMoE GEMV
kernel and 3-mask BF16 converter`

## Testing

- **Unit & Integration Tests**:
- `./build/direct_cuda/Release/onnxruntime_provider_test
--gtest_filter="QMoESharedLayoutTest.*:MoETest.*"` (`153/153` passed).
- `python3 onnxruntime/test/python/test_qmoe_offload_v4.py
TestQMoEOffloadV4.test_v4_int4_and_int3_offload_modes_and_numerical_parity
-v` (`OK`).
- **End-to-End H200 Offload Qualification
(`qmoe_2b375_x02_y14_z32_dp23_b64`, `< 30 GB` VRAM / `< 40 GB` Anon RAM,
`device_expert_bytes = 13.5 GB`, `host_expert_bytes = 34.0 GB`)**:
- Fits **`8,267–8,363` resident expert slots** on GPU (`33.6%–34.0%` of
24,576 experts) and maps the entire `26.31 GB` cold expert complement in
host memory with **`ssd_misses = 0`** and **`Peak Anon RAM = 3.70–4.47
GB`**, **`Peak VRAM = 25.61–29.28 GB`**.
- `smoke.py`: `PASS` (`0.223 s` TTFT, `57.9 tok/s` decode, `16.92
ms/step` CPU time).
- `engine_behaviours.py`: `PASS` (`26/26` checks, `11.15 ms/step` CPU
time).
- `bench_engine_mtp.py` (`128` new tokens, `S = 8`): **`87.49 tok/s`
median (`91.49 tok/s` peak)** at `ctx = 2048`, **`137.56 tok/s` median**
at `ctx = 8192` (**`+25.3%`** vs `top16_44_33`), and **`102.99 tok/s`
median (`150.60 tok/s` peak, `36.27 ms/cycle`)** at `ctx = 32768`
(**`-35.0%`** cycle latency vs `top16_44_33`).
- `bench_ttft_engine.py`: **`0.438 s`** (`L=128`), **`0.918 s`**
(`L=512`), **`1.096 s`** (`L=1024`, **`2.43x`** faster), **`2.734 s`**
(`L=2048`), **`3.336 s`** (`L=4096`), **`4.177 s`** (`L=8192`, **`1,961
tok/s`**, **`1.67x`** faster).

## Checklist

- [x] Tests added/updated
- [x] No breaking changes (or documented in description)
- [x] Documentation updated (if applicable)

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants