KVTop
Kernels · NVFP4 GEMM dispatch · RTX 5090 (SM120)

Why doesn't a 1.03–1.16× from choosing CUTLASS configurations per shape count as a speedup yet?

1.03–1.16×vs torch._scaled_mmeight shapes, one fixed-order run; our kernel always timed first
1.02–1.26×vs CUTLASS defaultfastest recorded row per shape in the 25-shape sweep (in-sample); picking by highest utilization can give 1.00×
0.99–1.22×vs b12x11 pairable shapes, ncu time; b12x 1.3% faster at 2048·14336·4096
n = 1evidencepreliminary; fixed timing order, not yet retested in balanced order
Core result · measured
0.9×0.95×1×1.2×1.4×no change+14%1K·4K·4Kcoop+6%2K·14K·4Kcoop+3%4K·4K·4Kpingpong+15%4K·4K·14Kcoop+SK+7%8K·8K·8Kcoop+14%8K·4K·14Kcoop+SK+16%8K·28K·8Kcoop+6%16K·16K·16Kcoop÷ torch._scaled_mm
How much faster did shape-based dispatch look in the historical run? Historical fixed-order candidate: top-dispatcher was measured at 1.03–1.16× torch._scaled_mm on eight shapes. Our implementation always ran first, creating a potential thermal/power-order bias in our favour whose size is unknown. CUDA-event mean over 50 back-to-back calls (20 for the two largest shapes, M·N·K > 10¹²) after 3 warm-up calls, as written in the test source committed with this output at 39f94416; the output header itself says 50 iterations for every shape. One sequence per shape, no independent-repeat spread. Ratios are recomputed from the logged TFLOPS; the run's code revision is not separately recorded.
GPU
One RTX 5090 (our workstation), 530 W board limit, clocks not locked
baseline
torch._scaled_mm NVFP4 path (torch 2.13.0+cu130; loaded cuBLAS version not recorded); not a tuned cuBLASLt search
format
A, B packed E2M1; UE4M3 scales per 16 elements; BF16 output; alpha 0.75 in the correctness pass
timing
3 warm-up calls, then 50 timed calls (20 when M·N·K > 10¹²), per kernel_kvtop_test.py at 39f94416
repeats
one fixed-order sequence per shape; no spread

Source: top-dispatcher results/kernel_kvtop_test.txt L17–25 (output) · kernel_kvtop_test.py L28–35, L53 (timing loop) · private repository · 39f94416fdadec0de1efe4af05c6a42959848661

We started with a fairly ordinary question. On an RTX 5090, a block-scaled FP4 (NVFP4) matrix multiply usually goes through torch._scaled_mm to cuBLAS and its default heuristic. Could we do better on the shapes an LLM actually runs by choosing a CUTLASS configuration for each shape?

Our first result looked encouraging. On one card, in one fixed-order run, top-dispatcher was faster on all eight shapes: 1.03× to 1.16×, at 1,146–1,338 TFLOPS.

Then we looked at the measurement order. We had always timed our kernel first. After the power and frequency results in the previous note, that was hard to overlook. We still have a candidate worth testing, but we cannot call those ratios a speedup yet.

What did we actually change?

We did not invent a new kernel. The tiles, schedules and epilogue are CUTLASS 4.5 collectives. Our contribution here is the choice of which of three configurations to launch for a given shape.

condition configuration
N = K = 4096 and 4096 ≤ M ≤ 8192 128×128×256, ping-pong schedule
K ≥ 2N and M ≥ 2048 256×128×128, cooperative + stream-K
otherwise 256×128×128, cooperative
CTA swizzle 8 when M, N ≥ 16384, else 1

We swept 25 shapes across 15 choices: five configurations, each at three swizzles. Selecting the lowest recorded ncu duration for each shape gave 1.024–1.261× relative to the 128×128×128, swizzle-1 default. Selecting the highest tensor-pipe utilization did not always select that fastest row; its range includes 1.000×. These are in-sample selections from one historical sweep, not a held-out result for the dispatcher.

Figure 1 · measured
0.95×1×1.26×1024×4096×40961.146×1.146×1024×14336×40961.132×1.124×1024×4096×143361.261×1.261×1024×8192×81921.097×1.090×1024×28672×81921.082×1.082×2048×4096×40961.024×1.024×2048×14336×40961.046×1.005×2048×4096×143361.091×1.091×2048×8192×81921.125×1.125×2048×28672×81921.115×1.115×4096×4096×40961.034×1.000×4096×14336×40961.104×1.103×4096×4096×143361.172×1.172×4096×8192×81921.078×1.078×4096×28672×81921.111×1.111×8192×4096×40961.038×1.000×8192×14336×40961.108×1.108×8192×4096×143361.121×1.118×8192×8192×81921.092×1.092×8192×28672×81921.125×1.125×16384×4096×40961.087×1.086×16384×14336×40961.117×1.117×16384×4096×143361.119×1.119×16384×8192×81921.118×1.104×16384×28672×81921.122×1.122×Minimum logged durationMaximum tensor-pipe utilization
Does the highest utilization also select the fastest configuration? All 25 shapes and all 15 choices from the supplied sweep: five cfg values × three swizzles. For each shape, baseline is cfg=0, swizzle=1 (128×128×128). One selector takes the lowest logged ncu duration, the other the highest util_pct. Ratios use durations from the same table. The minimum-time selector spans 1.024–1.261×; maximum-utilization selection includes 1.000×. These are retrospective in-sample selections, not the delivered dispatcher evaluated on unseen shapes. Code revision not recorded; values checked byte-for-byte against the original run output.
GPU
One RTX 5090 (our workstation)
driver
595.84
CUDA
13.2.86
power
530 W limit, clocks unlocked
evidence
Historical raw result files (Oct 25–26, 2026), re-read in full; no experiments rerun
timing
ncu logged time_us; not CUDA-event timing from kvtop_test.txt
selection
Selectors applied independently within each shape; no averaging of alternative configurations
repeats
One supplied sweep; no independent repeat estimates
code revision
not recorded

Source: results/run5/shape_table.csv:L1–L376 · raw output files; research directory not yet in a public repository

The table ended up fairly small. One 256×128×128 tile won most shapes. Under the minimum-duration selector, ping-pong only won the square 4K and 8K×4K×4K cases; stream-K won when K was long relative to N. Our working explanation is that, with few output tiles, splitting the K loop keeps more of the 170 SMs busy. We have not profiled that explanation yet.

We can also pair eleven shapes with the historical b12x table. For this comparison we use the maximum-utilization choice from our sweep and compare the recorded ncu durations. The ratio ranges from 0.987× to 1.218×, including one shape where b12x is faster. This retrospectively selected arm is not a measurement of the delivered dispatcher against b12x.

Figure 2 · measured
0.95×1×1.22×1024×4096×40961.218×1024×14336×40961.146×2048×14336×40960.987×4096×4096×40961.073×4096×14336×40961.113×4096×4096×143361.203×8192×8192×81921.116×8192×14336×40961.115×8192×4096×143361.170×8192×28672×81921.117×16384×8192×81921.130×
Where does the sweep-selected configuration beat b12x? Historical cross-file comparison restricted to 11 exact M,N,K matches. Our arm is the maximum-util_pct configuration from shape_table.csv, not necessarily the dispatcher rule or the minimum-time configuration. Ratio = b12x time_us / selected time_us. It ranges from 0.987 to 1.218×; b12x has the lower duration at 2048×14336×4096. The unmatched 16384³ b12x row is excluded. Selection budget is asymmetric, ordering is not supplied, and this is not a balanced independent library benchmark. Code revision not recorded; values checked byte-for-byte against the original run output.
GPU
One RTX 5090 (our workstation)
driver
595.84
CUDA
13.2.86
power
530 W limit, clocks unlocked
evidence
Historical raw result files (Oct 25–26, 2026), re-read in full; no experiments rerun
timing
ncu durations in two run5 CSVs; no mixing with CUDA-event ratios
repeats
One supplied row per b12x shape and one configuration sweep; no independent-repeat spread
comparison
Retrospectively tuned selection vs supplied b12x rows; b12x tuning/setup provenance incomplete
code revision
not recorded

Source: results/run5/b12x_table.csv:L2–L12; results/run5/shape_table.csv:L1–L376 · raw output files; research directory not yet in a public repository

Do the outputs agree?

We checked random E2M1 data, random UE4M3 scales in [0.5, 2), and alpha 0.75. Across five shapes, including M = 1000, which is not a multiple of the tile, the largest absolute difference from torch._scaled_mm, divided by the largest absolute output, was 3.65×10⁻³ to 4.20×10⁻³.

Figure 3 · measured
256×256×2561000×4096×40964096×4096×40968192×4096×143362048×14336×4096Cooperative0.0040.0040.0040.0040.004Cooperative + stream-K0.0040.0040.0040.0040.004Ping-pong0.0040.0040.0040.0040.004
How closely did the three configurations match the reference? Five supplied shapes, three configurations each, random E2M1 data and random UE4M3 scales, alpha 0.75, reference torch._scaled_mm as described in the article. The log calls the scalar max rel err; the supplied methodology defines it as max absolute difference divided by the matrix-wide maximum absolute reference output. Values are 3.65e-3 to 4.20e-3, identical across the three configurations within each shape. This is not elementwise relative error, an error distribution, proof of BF16 rounding as the cause, or task-level quality. Code revision not recorded; values checked byte-for-byte against the original run output.
GPU
One RTX 5090 (our workstation)
driver
595.84
CUDA
13.2.86
power
530 W limit, clocks unlocked
evidence
Historical raw result files (Oct 25–26, 2026), re-read in full; no experiments rerun
metric
max(abs(out − ref)) / max(abs(ref)); metric implementation not supplied here; the log labels the reference "cuBLAS" (torch._scaled_mm)
scales
Random UE4M3 in [0.5,2), as described in the supplied article
repeats
One supplied correctness pass; random seeds and independent reruns not supplied
code revision
not recorded

Source: results/run5/kvtop_test.txt:L1–L16 · raw output files; research directory not yet in a public repository

That result was identical for all three configurations and is consistent with BF16 output rounding. It is also a limited check: a matrix-wide maximum can hide relative error near zero. We still need an error distribution against an independent dequantized reference.

Where would this help in serving?

We have not tuned decode. The dispatcher works below M = 256, but nothing there is tuned, and decode is where a 5090 serving an LLM spends most of its steps.

We also have a reason to be cautious about isolated GEMM wins. A different MoE kernel of ours was 1.20× faster per layer in a microbenchmark, then only 1.04–1.11× faster on the real prefill path. We stopped that line of work. This dispatcher has not established an end-to-end speedup either.

What would make us trust the 1.03–1.16×?

The original test timed top-dispatcher first and torch._scaled_mm immediately after. The baseline always ran later in each pair, creating a potential thermal and power-order bias in our favour; its size is unknown.

Each number comes from one sequence (n = 1) of 50 calls, or 20 for the two largest shapes, after 3 warm-up calls. Inputs were random and clocks were unlocked. The dispatch table was also tuned on overlapping shapes.

Before publishing a speedup claim, we will rerun the eight shapes in balanced ABBA/BAAB pairs, logging clock and power per block. We will add a tuned cuBLASLt arm, time pre-packed GEMM and the full call separately, and test shapes the table never saw.

Figure 4 · illustration, not data
BENCHMARK PLAN · order only, not measured time or a completed retestA = top-dispatcherB = torch._scaled_mmHistorical orderABABPlanned ABBAABBAPlanned BAABBAABHistorical row: repeated A→B pairs. Planned blocks balance which arm runs earlier.Repeat blocks; log clock and power; report paired spread. Ordering alone is not a full control.
How will we test whether the lead survives a different order? Protocol illustration, not new performance data. The historical row depicts the repeated ours-first pattern, not an exact recovered run log. The planned ABBA and BAAB blocks balance order exposure; they do not guarantee equal temperature or eliminate carryover. Repeat blocks under a defined warm-up and window, with clock and power telemetry, and report paired variation.
A
top-dispatcher
B
torch._scaled_mm
scope
Order within repeated timing blocks; box width is not elapsed time
additional_controls
Separately add tuned cuBLASLt, pre-packed/full-call timing and unseen shapes

Source: Explanatory illustration for the section: What would make us trust the 1.03–1.16×?, lines 301–309

For now, these are candidate results awaiting a retest. If the 3% points fall inside the run-to-run spread, we will say so.

Method

GPUOne RTX 5090 (our workstation), 530 W board limit unchanged
Clocksnot locked
KernelCUTLASS 4.5 collectives; per-shape choice of 3 configurations (top-dispatcher @ 39f94416)
Baselinetorch._scaled_mm NVFP4 → cuBLASLt
Baseline versions (torch / CUDA / cuBLAS)torch 2.13.0+cu130, CUDA 13.2.86, driver 595.84; loaded cuBLAS library version not recorded
Data formatA, B packed E2M1; UE4M3 scale per 16 elements, 128×4 blocked layout; BF16 output
Shapes8 LLM GEMM shapes, M 1,024–16,384 (speed); 5 shapes incl. M = 1,000 (correctness)
TimingCUDA events, 3 warm-up calls, mean of 50 back-to-back calls (20 for the two largest shapes), per the test source committed with the output; top-dispatcher always first
Dispatch rules from25 shapes × 15 configurations, ranked by ncu tensor-pipe utilization
Repeats1 run; spread not recorded

Evidence and references

Code and data for this note will be released as open source; more is coming.

What this does not show

Fixed order biases toward our kernel by an unknown amount. Dispatch rules were tuned on overlapping shapes. The error metric is a matrix-wide maximum. The baseline is the default torch._scaled_mm path, not a tuned cuBLASLt search, and the loaded cuBLAS version was not recorded. No decode (small-M) path. A GEMM microbenchmark gain is not an end-to-end serving gain.