Why can the same NVFP4 GEMM report 1,200 TFLOPS on one input and 1,700 on another?
- GPU
- One RTX 5090 (our workstation), 170 SMs
- driver
- 595.84
- power
- 530 W limit (default 575 W), clocks unlocked
- run
- 300 back-to-back calls per condition, one sequence (n = 1); at the logged rates that is about 1.5–2.2 s of GEMM work per condition (computed, not timed)
- telemetry
- nvidia-smi samples averaged per condition; sampling interval and sample filter for this run not recorded
- not recorded
- which operand(s) and scales each distribution changed beyond the label; temperature start state; throttle reasons
- code revision
- not recorded
Source: results/run10/power_2_16384.txt:L1–L9 (drawn), L11 and L17 (second sequence) · raw output files; research directory not yet in a public repository
We thought we had a kernel-efficiency problem. On our RTX 5090, top-dispatcher—a shape-based selection of CUTLASS configurations—reported about 1,146–1,338 TFLOPS across eight matrix shapes. Our instruction microbenchmark was near 2,000 TFLOPS. We looked at the gap and thought: where did a third of the hardware go?
That was an uncomfortable conclusion to revisit. We had compared two measurements without checking whether the GPU was running at the same clock.
Was the instruction itself being held back?
We started with a register-only microbenchmark, normalized by clock64. The NVFP4 block-scaled MMA (mma.sync … kind::mxf4nvf4 … m16n8k64) delivered 4,057 FLOP per SM per cycle—about 99% of the 4,096 we expected—at roughly 2.9 GHz. We did not observe an extra throughput limit on this instruction path.
The same test showed the familiar GeForce pattern on older paths: FP16 and FP8 with FP32 accumulation run at half the rate of their FP16-accumulate versions.
| instruction (all 170 SMs) | FLOP / clk / SM |
|---|---|
NVFP4 mxf4nvf4, UE4M3 scales, m16n8k64 |
4,057 |
FP4 through f8f6f4 (no block scale), m16n8k32 |
1,024 |
| FP8 E4M3, FP16 accumulate | 2,041 |
| FP8 E4M3, FP32 accumulate | 1,024 |
| BF16, FP32 accumulate | 512 |
A separate instruction probe also included structured-sparse paths. We keep it separate here: its dense NVFP4 observation was 4,093.1 FLOP per SM per cycle, rather than the earlier 4,057. The sparse rows count dense-equivalent arithmetic, so their higher rates do not mean the same dense model automatically runs twice as fast.
Source: results/run1/sparse.txt:L1–L5 · raw output files; research directory not yet in a public repository
We also inspected the listed tensors from a Qwen 27B NVFP4 weight set. The file reports that only 2.77% of the inspected groups already contain at least two zeros. Its reported extra-zeroing requirement is a structural check, not evidence that pruning would preserve model quality.
Source: results/run12/qwen27b_nvfp4_24.txt:L1–L31 · raw output files; research directory not yet in a public repository
What clock was the GEMM actually running at?
This is where our original comparison fell apart. During repeated large GEMMs, the clock fell far below 2.9 GHz. The same 16,384³ kernel reported 1,203 TFLOPS at about 1.83 GHz with random inputs, and 1,734 TFLOPS at about 2.67 GHz with all-zero inputs. As far as the run records show, nothing else changed; how operands and scales were generated, and the starting state, are still to verify.
We left the power limit at 530 W and did not lock clocks. These runs show a large data-dependent power and frequency effect. They do not establish that every NVFP4 GEMM is power-limited: the Gaussian and zero conditions drew 481 W and 391 W.
Zero-filled and constant-filled matrices are diagnostic inputs, not model workloads or evidence for a performance claim. We include them because they expose something a single throughput number leaves out. Without stating the input values, a benchmark can report almost any number in this range.
Run length gave us another reason to be careful. A 4,096³ ping-pong kernel reported 1,264 TFLOPS over 40 calls and 1,144 over 4,000. Even 4,000 calls lasted under half a second. Neither measurement was a thermal steady state.
Source: results/run6/1_4096_clk.txt · raw output files; research directory not yet in a public repository
What did we get wrong about “57–66% of peak”?
We treated the gap as if it measured time lost to operand loads, synchronization or the epilogue. It did not. Dividing application throughput by a peak measured at a different frequency cannot tell us that.
Profiling helps us locate where warps are waiting, but those samples have their own denominator. The following two captures use different shapes and schedules; their sample shares cannot be read as fractions of elapsed kernel time.
Source: results/run6/3_stall_summary.txt:L1–L4; results/run6/3_stall_summary.txt:L22–L25 · raw output files; research directory not yet in a public repository
For frequency-normalized efficiency, we need effective FLOPs divided by SM count × per-cycle peak × the clock integrated over the same measurement window. That requires clock telemetry from the same run; a separate profiler replay will not supply the missing denominator.
Source: Explanatory illustration for the section: What did we get wrong about “57–66% of peak”?, lines 250–254
We also have to apply that scrutiny to our own dispatcher. Its 1.03–1.16× over torch._scaled_mm came from a benchmark that always timed our kernel first. We need a balanced-order retest before calling it a speedup. That is the next note.
How much can we conclude from these runs?
Each condition is one historical sequence on one card (n = 1), with low-rate nvidia-smi sampling and no locked clock. The results were enough to make us withdraw our original explanation. Strengthening the causal claim still needs repeats, time-based windows, logged throttle reasons, and a fixed-clock run at a frequency every input can hold.
Method
| GPU | One RTX 5090 (our workstation), 170 SMs, driver 595.84, power limit 530 W (default 575 W) |
|---|---|
| Clocks | not locked |
| Software | CUDA 13.2.86, torch 2.13.0+cu130; CUTLASS collectives (version at build time not recorded) |
| Instruction test | mma.sync per PTX variant, clock64-normalized, warps × ILP × SM sweeps |
| GEMM | 16384³, 256×128×128 cooperative; 4096³, 128×128×256 ping-pong |
| Inputs | random E2M1, Gaussian-shaped, fixed zero fractions, constant, zero |
| Run length | 300 calls per condition (16384³), about 1.5–2.2 s of GEMM work computed from the logged rates, not timed; 40 vs 4,000 calls (4096³) |
| Telemetry | nvidia-smi samples averaged per condition; sampling interval and sample filter for this run not recorded |
| Throttle reasons per window | not recorded |
| Temperature start state | not recorded |
| Repeats | 1 sequence per condition |
Evidence and references
Code and data for this note will be released as open source; more is coming.
What this does not show
One sequence per condition, low-rate telemetry, no fixed-clock control, so the power and frequency explanation is supported but not isolated. Not every condition sat at the power limit. Zero and constant inputs are diagnostics, not workloads. A tensor-pipe activity counter is not the fraction of useful arithmetic at peak rate. 300 calls of 16384³ amount to about 1.5–2.2 s of GEMM work (computed from the logged rates), which is not a thermal steady state.