Repository navigation
[CUDA] Pin FP8 GEMV residency for grids just past two blocks per SM - #32433
Merged
Tianlei Wu (tianleiwu) merged 8 commits intoSep 9, 2026
Merged
Tianlei Wu (tianleiwu) merged 8 commits into
Tianlei Wu (tianleiwu) merged 8 commits into
Conversation
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Hardware-specific CUDA residency tuning and its unmerged stacked dependency require final human validation.
Pull request overview
Pins FP8 tensor-core GEMV residency for grids between two and three blocks per SM. No actionable issues found.
Changes:
- Adds a launch-bounded kernel entry point and dispatch predicate.
- Adds a K-split override for benchmarking.
- Adds boundary and device-dependent correctness tests.
File summaries
| File | Description |
|---|---|
matmul_block_scaled_fp8.cu |
Adds pinned-kernel dispatch and K-split override. |
matmul_block_scaled_fp8_tiling.h |
Defines tiling and residency-selection predicates. |
matmul_block_scaled_fp8_test.cc |
Tests selection boundaries and pinned FP16 execution. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Tianlei Wu (tianleiwu)
marked this pull request as draft
September 4, 2026 00:38
The tensor-core FP8 GEMV launches ceil(N / 16) blocks. A 16-warp block only fits
twice per SM, so an N just above 32 * sm_count spills into a second, nearly empty
wave: on H200 N = 5120 launches 1.21 waves and ncu measures 66% active cycles.
Add a second entry point over the same kernel body carrying
__launch_bounds__(32 * KSplit, 3), and route the shapes in that window to it. The
hint caps registers at 40, which makes three blocks resident and collapses those
shapes to a single wave.
Kernel time on H200 (sm_90), FP16 activations, block_size 128:
N = 5120, K = 6144 13.25 -> 11.04 us
N = 6144, K = 5120 13.18 -> 9.63 us
N = 5120, K = 17408 52.00 -> 36.51 us
On a Qwen3 27B NVFP4 + FP8 decode workload the GEMV total drops from 12150 to
11712 us per forward, and end-to-end decode throughput is 1.03-1.04x at batch 1.
The window is narrow and the hint is a pessimization outside it, so the selector
in matmul_block_scaled_fp8_tiling.h gates on all four conditions that measurement
showed matter:
* grids at or below 2 blocks per SM are already one wave (N = 1024 -> 0.96x);
* grids above 3 blocks per SM stay multi-wave either way;
* 8-warp blocks (KSplit 8, taken from N >= 8192) lose 1.05-1.08x from any
explicit __launch_bounds__, and 32-warp blocks cannot host 3 blocks per SM;
* only one row tile meets the 40-register cap. Two tiles measure 0.74x and four
0.24x, both from spills.
Also adds ORT_FP8_GEMV_KSPLIT to override the K-split heuristic for A/B sweeps,
matching the override already present in the FP4 GEMV.
Tianlei Wu (tianleiwu)
force-pushed
the
tlwu/fp8_gemv_residency_hint
branch
from
September 9, 2026 05:06
6e6386c to
f9b7b74
Compare
Tianlei Wu (tianleiwu)
marked this pull request as ready for review
September 9, 2026 05:10
Isolate pinned-kernel tests from cached environment overrides and cover both activation dtypes with optional QDQ and bias. Remove the unused duplicate K-split override reader. Validation: two host selector tests passed with UBSan; test formatting and git diff checks passed. CUDA build and GPU execution were unavailable locally.
…nleiwu/onnxruntime into tlwu/fp8_gemv_residency_hint
Baiju Meswani (baijumeswani)
approved these changes
Sep 9, 2026
Baiju Meswani (baijumeswani)
left a comment
Contributor
There was a problem hiding this comment.
The residency-pinned KSplit16 kernel is measurably positive on a 48-SM SM121 GB10 when a matrix falls inside the PR's dispatch window. The strongest and most stable batch-1 decode results show approximately 8-10% lower FP8 GEMV operator latency at M=1 for Qwen-like K dimensions.
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
Stacked on #32409. That PR's commit is the first commit here; only the second commit (
Pin FP8 GEMV residency ...) belongs to this PR. Please merge #32409 first ??? GitHub will then collapse this diff to the residency change alone.The tensor-core FP8 GEMV launches
ceil(N / 16)blocks. A 16-warp block only fits twice per SM, so anNjust above32 * sm_countspills into a second, nearly empty wave: on H200N = 5120launches 1.21 waves andncumeasures 66% active cycles.This adds a second
__global__entry point over the same kernel body carrying__launch_bounds__(32 * KSplit, 3), and routes the shapes in that window to it. The hint caps registers at 40, which makes three blocks resident and collapses those shapes to a single wave.cuobjdump -res-usageconfirms the new entry point atREG:40 SHARED:9216, so40 * 512 * 3 = 61440registers and27 KiBof shared memory per SM ??? three blocks fit.The window is narrow and the hint is a pessimization outside it, so
Fp8MmaGemvPinsResidencyinmatmul_block_scaled_fp8_tiling.hgates on the conditions that measurement showed matter:N = 1024, 64 blocks, measured 0.96x);KSplit8, taken fromN >= 8192) lose 1.05-1.08x from any explicit__launch_bounds__??? declaring it replaces nvcc's implicit bounds even when the register cap is unchanged. 32-warp blocks (theKSplit32 arm from Add FP8 GEMV KSplit32 scheduling for the 48-SM SM121 GPU #32409) cannot host 3 blocks per SM at all;M = 16(two tiles) measures 0.74x andM = 32(four tiles) 0.24x, both from spills.The plain kernel is unchanged and stays the default for everything outside the window, so no currently-selected shape changes code path.
Also adds
ORT_FP8_GEMV_KSPLITto override the K-split heuristic for A/B sweeps, matching the override already present in the FP4 GEMV.Measurements (H200, sm_90, 132 SMs, CUDA 13.0)
Kernel time, FP16 activations,
block_size128:On a Qwen3 27B NVFP4 weights + FP8 KV decode workload the FP8 GEMV total drops from 12150 to 11712 us per forward. End-to-end decode throughput (median of repeats, both arms built as real binaries):
M = 32, gated out)Acceptance rate is identical to four decimals in every cell.
Measurements (GeForce RTX 5060 Ti, sm_120, 36 SMs, CUDA 13.0)
Kernel medians from Nsight Systems CUDA activity traces, FP16 activations,
M = 8,block_size128. Each trace contains 100 measured launches after warmup;beforeandafterselect the plain and residency-pinned entry points from the same binary.The predicate boundaries behave as intended:
N = 1152(exactly 2 blocks/SM),N = 1744(just above 3 blocks/SM), andM = 16all stay on the plain kernel and measure within 1% between A/B arms. A ragged in-windowN = 1169produces bitwise-identical output (max_abs_diff = 0).Measurements (GeForce RTX 3060, sm_86, 28 SMs, CUDA 13.0)
The same methodology shows that applying the hint on SM86 is a small but repeatable pessimization, which motivates the SM89 minimum:
Three independent
N = 1024,K = 5120traces with alternating arm order reproduce a 0.988-0.991x speedup.cuobjdump -res-usageshows that SM86 moves fromREG:56 STACK:0for the plain FP16 kernel toREG:40 STACK:8for the pinned kernel. Boundary andM = 16controls remain neutral, and a ragged in-window correctness case is bitwise identical. The final dispatch therefore leaves pre-SM89 devices on the plain kernel.Measurements (GeForce RTX 4090, sm_89, 128 SMs, CUDA 13.3)
Kernel time measured with Nsight Systems CUDA traces, FP16 activations,
block_size128. The baseline and pinned arms are separate provider binaries built from the same merged PR head; the baseline changes only the residency dispatch to select the plain entry point.Controls outside the pinned dispatch remain neutral within run-to-run variance:
M = 8, N = 1024, K = 5120(grid below 2 blocks/SM)M = 16, N = 5120, K = 6144(MTiles = 2)Using the occurrence counts in the Qwen3.8 workload microbenchmark, the three affected shapes save about 407 us per forward. A reverse-order repeat reproduced the main gains (1.24x, 1.22x, and 1.04x respectively).
Tests
onnxruntime_provider_test --gtest_filter='*MatMulBlockQuantizedFp8Weight*'??? 14/14 pass.Two new tests:
GemvTensorCorePinnedResidencyBoundaries??? pure predicate test at a fixedsm_countof 132, asserting the(264, 396]column-block window, SM86 exclusion, SM89 inclusion, and theKSplit/MTilesexclusions, so the expectations do not move with the test machine.GemvTensorCorePinnedResidency??? runs the hinted kernel. WhichNselects it depends on the device's SM count, so the shape is derived fromcudaGetDeviceProperties(N = 16 * (2 * sm_count + 1), plus a ragged+ 5) and cross-checked againstPickFp8MmaKSplit. Skips on pre-SM89 devices and devices whose window lands aboveN = 8192, where the launcher drops to 8 warps and stops hinting.Note for #32409
cuobjdump -res-usageshowsMatMulBlockScaledFp8MmaGemvKernel<32, 4, ...>atSHARED:66560(65 KiB).PickFp8MmaKSplitonly returns 32 whenm <= 8, soMTilesis always 1 there and that instantiation can never be launched ??? but it is still compiled and emitted. Callinglaunch_mma.template operator()<32, 1>()directly instead of going throughlaunch_for_ksplit<32>()would drop it. Not changed here since it belongs to #32409.