Skip to content

[CUDA] Optimize QMoE SoftmaxTopK router for small-batch decode - #28980

Merged
Tianlei Wu (tianleiwu) merged 9 commits into
mainfrom
tlwu/qmoe_router_topk_perf
Jun 12, 2026
Merged

Tianlei Wu (tianleiwu) merged 9 commits into
mainfrom
tlwu/qmoe_router_topk_perf

Conversation

@tianleiwu

@tianleiwu Tianlei Wu (tianleiwu) commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Description

Optimizes the CUDA QMoE router top-k (LaunchSoftmaxTopK) for small-batch / autoregressive decode by replacing the old one-thread-per-row hot path with parallel CUB and warp-level top-k kernels. The dispatch now uses the fastest specialized path for common MoE expert counts while preserving the existing softmax normalization and deterministic lower-index tie-breaking semantics.

This PR also factors the warp-level top-k sorting code into a reusable CUDA helper header and adds direct CUDA-internal tests so the new routing paths are covered independently of higher-level QMoE tests.

Motivation and Context

The previous router path launched a 256-thread block per row but did all top-k work in a single thread. In decode scenarios such as num_rows == 1, that made the router latency-bound on a serial scan of all expert logits and turned SoftmaxTopKKernel into a major MoE decode bottleneck.

For a Qwen3-style MoE workload with 256 experts, top-8 routing, and 40 MoE layers, the original router accounted for roughly 50% of decode GPU time. Moving the work to block/warp-parallel kernels removes that bottleneck while keeping the same output ordering and scaling behavior.

Key Changes

Area Change
QMoE router dispatch Adds DispatchSoftmaxTopK routing for k <= 64 and num_experts <= 1024, with a fallback to the original scalar kernel for larger or uncommon shapes.
Tiny expert counts Adds SoftmaxTopKWarpBitonicKernel for num_experts <= 32, using one warp per row and in-register bitonic sorting via warp shuffles.
Small expert counts Adds SoftmaxTopKWarpMergeKernel for 32 < num_experts <= 64, using a single warp and CUB warp merge sort.
Larger common MoE counts Uses SoftmaxTopKMergeKernel with CUB block merge sort for num_experts <= 128, 256, 512, and 1024.
Reusable top-k helpers Adds onnxruntime/core/providers/cuda/cu_inc/topk_warp_sort.cuh with reusable warp bitonic and warp merge sort helpers.
Stable tie-breaking Packs (score, index) into a uint64_t stable sort key for the CUB merge paths, matching onnxruntime-genai's lower-index tie-breaking and avoiding compound comparators.
Softmax cleanup Factors shared softmax scale, safe reciprocal, top-k normalization, warp reduction, and CUB block reduction helpers to keep the optimized kernels consistent.
Tests Adds CUDA-internal SoftmaxTopK_* tests covering warp bitonic, warp merge, block merge, stable ties, normalization, float, half, and bfloat16.

Performance

H200 measurements for the target QMoE decode scenario showed the router cost dropping from roughly 5.56 ms/token to 0.17 ms/token, improving end-to-end Qwen3.6-35B-A3B INT4 decode throughput from about 80 tok/s to 113 tok/s.

Additional profiling of the 32 < num_experts <= 64 warp merge path showed the packed uint64_t stable sort key is consistently faster than a {float, int} struct comparator on H200:

Experts Sort-only packed/struct Full softmax+top-k packed/struct
33 0.680x 0.704x
48 0.672x 0.695x
64 0.673x 0.696x

Testing

  • lintrunner -a
  • ninja onnxruntime_providers_cuda_ut
  • ninja onnxruntime_provider_test
  • GTEST_FILTER='CUDA_EP_Unittest.SoftmaxTopK_*' ./onnxruntime_provider_test --gtest_filter='CUDA_EP_Unittest.All'
  • onnxruntime/test/python/transformers/test_qmoe_cuda.py -k parity (44 passed)

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.

Pull request overview

This PR optimizes the CUDA QMoE router softmax+top-k path by introducing block-per-row kernels that parallelize the work across threads (targeting the small-batch / autoregressive decode case) and adding a dispatcher to select the best kernel based on k and num_experts.

Changes:

  • Added SoftmaxTopKMergeKernel (CUB block sort) for k <= 64 and num_experts <= 1024.
  • Added SoftmaxTopKKernelBlock (block-parallel reductions + k rounds of argmax) as fallback for k <= 64 and num_experts > 1024.
  • Replaced direct SoftmaxTopKKernel<<<...>>>() launches with DispatchSoftmaxTopK(...) in all LaunchSoftmaxTopK overloads.

Comment thread onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu Outdated
Comment thread onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu Outdated
Comment thread onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu Outdated
Comment thread onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu Outdated
Comment thread onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu Outdated
Comment thread onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu Outdated
The QMoE router top-k kernel (SoftmaxTopKKernel) used one thread per row.
In autoregressive decode (num_rows == 1) this leaves all but one thread of
the block idle while it serially scans every expert's logit with
uncoalesced, latency-bound global reads. For Qwen3 MoE (256 experts,
top-8, 40 MoE layers) this kernel alone accounted for ~50% of decode GPU
time (~139 us/call).

Add two block-per-row kernels and dispatch to them for the common case:
- SoftmaxTopKMergeKernel: each block sorts one row's logits descending with
  cub::BlockMergeSort and reads the first k. A full block sort of <=1024
  logits is the fastest option on these sizes (benchmarked), matching the
  CUB block-merge recommendation already used elsewhere for top-k.
- SoftmaxTopKKernelBlock: block-parallel max/sum reduction plus k rounds of
  block argmax; used as the fallback for num_experts > 1024.

DispatchSoftmaxTopK selects the merge kernel by num_experts (capacity =
block_size * items_per_thread) for k <= 64, falls back to the block-argmax
kernel for larger expert counts, and keeps the original one-thread-per-row
kernel only for k > 64. Tie-breaking (lower expert index wins on equal
logits) and the softmax normalization semantics are preserved.

On an H200 this cuts the router from ~5.56 ms/token to ~0.17 ms/token and
raises end-to-end Qwen3.6-35B-A3B INT4 decode from ~80 to ~113 tok/s. All
QMoE CUDA parity tests pass.
- Sort a composite (logit, index) key so equal-logit ties deterministically
  prefer the lower expert index instead of relying on cub::BlockMergeSort
  stability (it is not a stable sort).
- Use non-templated ::cuda::maximum() / ::cuda::std::plus() to match the
  other CUDA softmax kernels in the repo.
- Drop redundant __syncthreads() barriers (the single barrier after each
  shared write already publishes the value and separates the shared-storage
  reuse; Sort() leaves results in registers and temp.merge is not reused).
@tianleiwu
Tianlei Wu (tianleiwu) force-pushed the tlwu/qmoe_router_topk_perf branch from 939da79 to f36a84e Compare June 11, 2026 23:04
@tianleiwu
Tianlei Wu (tianleiwu) marked this pull request as ready for review June 11, 2026 23:04
Comment thread onnxruntime/core/providers/cuda/cu_inc/topk_warp_sort.cuh Fixed

@github-actions github-actions Bot 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.

You can commit the suggested changes from lintrunner.

Comment thread onnxruntime/core/providers/cuda/cu_inc/topk_warp_sort.cuh Outdated

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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

Comment thread onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu Outdated
Comment thread onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu Outdated
Comment thread onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu Outdated
Comment thread onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu
Comment thread onnxruntime/core/providers/cuda/cu_inc/topk_warp_sort.cuh
Comment thread onnxruntime/core/providers/cuda/cu_inc/topk_warp_sort.cuh
@tianleiwu
Tianlei Wu (tianleiwu) merged commit dbf95cf into main Jun 12, 2026
86 checks passed
@tianleiwu
Tianlei Wu (tianleiwu) deleted the tlwu/qmoe_router_topk_perf branch June 12, 2026 07:11
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