Repository navigation
[CUDA] Tune NVFP4 GEMV tiling for Qwen MTP - #32140
Conversation
There was a problem hiding this comment.
Pull request overview
Tunes CUDA NVFP4 tensor-core GEMV dispatch for Qwen MTP workloads.
Changes:
- Adds wave-based column tiling and K-split selection.
- Adds shape-scoped benchmark overrides.
- Adds boundary tests and dispatch documentation.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
matmul_block_scaled_fp4.cu |
Updates GEMV tiling policy and overrides. |
matmul_block_scaled_fp4_test.cc |
Adds Qwen tiling cases. |
matmul_block_scaled_fp4.md |
Documents policy and environment variables. |
π‘ Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Review β PR #32140: [CUDA] Tune NVFP4 GEMV tiling for Qwen MTP Small, focused perf-tuning PR (+131/-22 across 5 files) with strong empirical justification. Extends the NVFP4 tensor-core GEMV dispatch policy from a two-arm decision tree (wide-column-if-any-wave, else column-starved) to a three-arm tree that adds a middle "moderate column-tile" branch, plus adds shape-scoped benchmark overrides and boundary tests. Author is a trusted CUDA EP contributor with a clean numeric argument in the doc. Bug diagnosis β the pre-PR dispatch collapse Walking the pre-PR policy in matmul_block_scaled_fp4.cu: if ((col_tiles + 3) / 4 >= sm_count) { // 1 wave of wide-column blocks
return {std::min(2, windows), 4}; // wide-column: ColTiles=4, small KSplit
}
// Column-starved: max-out KSplit up to 16-warp block
int k_split = 1;
while (k_split < 16 && k_split * 2 <= windows) k_split <<= 1;
return {k_split, 1};For Qwen MTP Similarly for The fix β three-arm policy const int wide_col_blocks = (col_tiles + 3) / 4;
if (wide_col_blocks >= kTargetGridWaves * sm_count && // 4 waves for wide-col
(windows < kLongReductionWindows || wide_col_blocks >= kLongReductionGridWaves * sm_count)) { // 8 waves for long-K wide-col
return {std::min(2, windows), 4};
}
if (col_tiles >= kTargetGridWaves * sm_count) { // 4 waves for single-col
const int k_split = windows >= kLongReductionWindows ? 8 : std::min(2, windows);
return {k_split, 1};
}
// Column-starved fallback, unchangedConstants:
Verifying the algorithm hits the intended Qwen shapes:
Two rows change (the two Qwen shapes), three rows don't. Measured speedups: 50.7 β 46.6 us (1.09Γ) and 61.4 β 55.0 us (1.12Γ). Regression-free on the other four documented shapes. β The wave constants are load-bearing but empirical
// Wave-count thresholds tuned on H100/H200 (SM 90, 132 SMs); H200 measurements in the doc.
constexpr int kTargetGridWaves = 4;
constexpr int kLongReductionGridWaves = 8;
constexpr int kLongReductionWindows = 64; // K = 8192, the threshold where doubling K-split beats doubling column tiles.Ideally these would be per-architecture (SM 80 vs 90 vs 100 may have different sweet spots), but the current dispatch only enables the tensor-core path on API split β public The refactor extracts the tiling logic to a new header matmul_block_scaled_fp4_tiling.h: struct Fp4MmaConfig {
int k_split;
int col_tiles;
};
Fp4MmaConfig PickFp4MmaConfig(int n, int k, int sm_count);Clean, no CUDA includes, and importantly lets the host-side test matmul_block_scaled_fp4_test.cc call the tiling algorithm without compiling CUDA. β The launcher calls the internal const Fp4MmaConfig cfg = PickFp4MmaConfigImpl(n, k, device_prop.multiProcessorCount);Rationale is presumably to avoid the CUDA-version-check overhead in the outer wrapper, but the wrapper's overhead is a single Environment-variable overrides β well-scoped, but static int const k_split = ParseEnvironmentVariableWithDefault<int>("ORT_FP4_GEMV_KSPLIT", 0);
...
ORT_ENFORCE(k_split == 0 || k_split == 1 || k_split == 2 || k_split == 4 ||
k_split == 8 || k_split == 16,
"ORT_FP4_GEMV_KSPLIT must be 0, 1, 2, 4, 8, or 16.");The env vars are read once ( static const int k_split = []() {
int v = ParseEnvironmentVariableWithDefault<int>("ORT_FP4_GEMV_KSPLIT", 0);
ORT_ENFORCE(v == 0 || v == 1 || v == 2 || v == 4 || v == 8 || v == 16,
"ORT_FP4_GEMV_KSPLIT must be 0, 1, 2, 4, 8, or 16.");
return v;
}();That way validation happens exactly once, on first entry. Micro-nit, non-blocking. Kernel-shape constraint enforcement β good defensive check ORT_ENFORCE(config.col_tiles == 1 || config.k_split <= 2,
"ORT_FP4_GEMV_COL_TILES=4 supports only KSplit 1 or 2.");Placed after applying overrides so that a
The if ((match_n != 0 && n != match_n) || (match_k != 0 && k != match_k)) {
return config; // shape doesn't match; use default
}This is genuinely useful for benchmarking campaigns where you want to override one shape without affecting others in the same model run. Rarely seen in kernel dispatch code; good addition. Test coverage β hits the three policy branches The new {64 * sm_count, 5120, 2, 1}, // moderate-col, ordinary K
{64 * sm_count, 8192, 8, 1}, // moderate-col, long K
{512 * sm_count, 512, 2, 4}, // wide-col, 8 waves
{64 * sm_count - 16, 2048, 16, 1}, // column-starvedI re-walked each against the new algorithm:
Boundary gap: no test at exactly {16 * kTargetGridWaves * sm_count, 8192, 8, 1}, // exactly at 4-wave wide-col threshold, long K -> falls through to moderatewould sharpen the wide-col-long-K interaction. Optional nit. Documentation update β clean policy table The added table in matmul_block_scaled_fp4.md is exactly the right level of abstraction:
Correctly captures the dispatch behaviour and lets a future maintainer look up "why did this shape get this config?" without reading the C++. Docs also cover the four new env vars. β One minor doc claim to verify
This describes the wide β moderate transition, but "before using one-column blocks" reads slightly ambiguously β it could be parsed as "prefers wide-col until wave-count drops below four", which is what it means. Minor wordsmithing suggestion:
Non-blocking. Test file wiring The tiling header is included inside a CI status 87 checks total on tip The CI trend across commits is:
The test commit dropped the count more than the tiling-logic commit did. Suggests the test itself may have added CI cost or has an issue. Stacking note Author notes this PR is a follow-up to #32128 and "can be rebased/stacked on #32128 after that PR merges." I don't see #32128 as merged yet β worth checking the base branch situation before merge. If #32128 is still open, this needs to rebase and its Summary of items Blocking:
Non-blocking but suggested:
Recommendation Approve β pending confirmation of the CI slip and the #32128 stacking situation. The tuning is well-diagnosed, the algorithm change is precisely bounded (only two shape classes shift; four documented shapes are unchanged), the new selector API extraction enables clean host-side testing, and the doc + env-var overrides make future tuning campaigns straightforward. The magic constants deserve one line of comment but are legitimate empirical tuning knobs, not accidental. Good example of how to add a shape-dispatch performance fix without regressing sibling shapes. |
Summary
Motivation
On H200, the previous dispatch could select a configuration with too few blocks for Qwen MTP shapes such as N=17408, K=5120. The updated policy keeps KSplit=8 for the longer K=8192 reduction while using KSplit=2 for K=5120, and requires sufficient grid waves before selecting wide column tiles.
This PR is the tiling follow-up to #32128 and contains no duplicate vectorized NVFP4 dequantization changes. It can be rebased/stacked on #32128 after that PR merges.
Validation
git diff --checkpassed.