[CUDA] Add decode (M=1) GEMV fast path to MatMul - #28986
Closed
Tianlei Wu (tianleiwu) wants to merge 2 commits into
Closed
Tianlei Wu (tianleiwu) wants to merge 2 commits into
Tianlei Wu (tianleiwu) wants to merge 2 commits into
Conversation
At decode (M==1) a plain fp16/bf16 MatMul with a constant weight is a memory
bound GEMV. cuBLAS dispatches a split-K path -- a dot_kernel followed by a
separate reduce_1Block_kernel -- i.e. two launches for one tiny GEMV. On the
Qwen3.6-35B-A3B router-gate shapes ([1,2048]x[2048,256] and [2048,1]) this is
~350 us/token across the model.
Add a single-kernel GEMV path:
- PrePack: for a constant fp16/bf16 weight B used as a plain [K, N] matrix
with small N (<= 1024) and K >= 256, build a transposed [N, K] copy once at
session init. is_packed stays false so the original B remains available for
the cuBLAS path when M > 1.
- ComputeInternal: at M==1 (non-transposed A, single output offset, matching
N/K), dispatch GemvM1Kernel -- one block per output column, splitting K
across the block and reducing once. The transposed layout makes the per-step
reads coalesced. Accumulation is fp32 to match cuBLAS's fp32-compute fp16
GEMM. All other shapes fall through to cuBLAS unchanged.
A row-major (non-transposed) GEMV ties cuBLAS; the coalesced transposed layout
is what wins. Microbench [1,2048]x[2048,256] fp16: cuBLAS 7.1 us vs custom GEMV
3.65 us (1.95x). End to end on Qwen3.6-35B-A3B (H200, CUDA graph) the router-gate
dot+reduce+nvjet kernels (~350 us/tok) are replaced by GemvM1 (~190 us/tok),
improving decode 154.8 -> 158.3 tok/s (+2.3%); GPU busy 5.25 -> 5.10 ms/tok.
Adds MatMul_Float16_GemvDecode parity test (constant-B fast path and
non-constant-B cuBLAS path both validated). All 36 MatMul/FusedMatMul tests pass.
Tianlei Wu (tianleiwu)
marked this pull request as draft
June 10, 2026 17:47
Tianlei Wu (tianleiwu)
marked this pull request as ready for review
June 12, 2026 01:51
Tianlei Wu (tianleiwu)
marked this pull request as draft
June 18, 2026 06:28
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 a decode-oriented (
M == 1) GEMV fast path to the CUDAMatMulop. At decode a plainfp16/bf16
MatMulwith a constant weight is a memory-bound GEMV; cuBLAS dispatches asplit-K path (a
dot_kernelfollowed by a separatereduce_1Block_kernel) — two launchesfor one tiny GEMV. A single custom kernel that prepacks the constant weight (transposed for
coalescing) and assigns one block per output column is ~2× faster.
Motivation
Profiling decode of a hybrid linear-attention + MoE model (Qwen3-Next / Qwen3.6) showed the
MoE router-gate
MatMulnodes ([1, 2048] × [2048, 256]and a[2048, 1]projection,fp16) spending ~350 µs/token across the model. They are plain
MatMulops that ORT routesto cuBLAS, which selects a 496-way split-K (
dot_kernel) + separatereduce_1Block_kernel— two kernels for one small
M = 1GEMV.A microbench of
[1, 2048] × [2048, 256]fp16 showed:Hgemm(split-K dot + reduce)[K, N](strided)[N, K](coalesced)The row-major variant ties cuBLAS — the coalesced transposed layout is what wins, so the
weight is transposed once during
PrePack.Key Changes
core/providers/cuda/math/matmul_gemv.{h,cu}GemvM1Kernel(one block per output column, splits K across the block, single block-reduce, fp32 accumulation) + a tiledTransposeForGemvhelper. Instantiated for fp16/bf16 only.core/providers/cuda/math/matmul.{h,cc}PrePacktransposes an eligible constant B into an[N, K]buffer;ComputeInternaldispatches the GEMV atM == 1.test/providers/cpu/math/matmul_test.ccMatMul_Float16_GemvDecodeparity test.Eligibility (all required): input is the constant weight B (initializer, input 1),
dtype fp16/bf16, used as a plain 2-D
[K, N]matrix (notransB/transBatchB),N ≤ 1024,K ≥ 256.is_packedis left false so the original B tensor staysavailable — at
M > 1(and for transposed A, batched GEMM, or non-eligible shapes) thekernel falls through to the existing cuBLAS path unchanged.
Performance
End-to-end on Qwen3.6-35B-A3B (H200, INT4, single-sequence decode, CUDA graph on):
nvjet207 +dot_kernel73 +reduce_1Block72 =~352 µs/token) are replaced by a single
GemvM1Kernel(~192 µs/token).Testing
MatMul_Float16_GemvDecodeexercises both paths (constant-B fast path and non-constant-BcuBLAS path) against a reference; fp32 accumulation matches cuBLAS's fp32-compute fp16 GEMM.
All 36
MatMul/FusedMatMultests pass.Motivation and Context
Decode-throughput optimization for fp16/bf16 models with small constant-weight projections
(e.g. MoE router gates). No public API change; numerics are preserved and all non-eligible
shapes keep the existing cuBLAS behavior.