Finetuning Your Program#
Note
This chapter is part of the agent-targeted Agent BLAS Performance Guide and is also published as Markdown for
agent consumption, in the markdown/ folder of the HTML documentation tree.
Scope. For a port, walk the Low-Hanging Fruit table once as a checklist (it needs no measurement to
apply), then return to the porting workflow: check launch geometry, measure
against the parity band, and stop there. Profile only if a gap above the band remains after the geometry
check, and read profiler output through grep, never as whole report pages.
The remaining gap is usually one of: occupancy, shared memory traffic, instruction mix, or the surrounding code. Do not guess which — at this stage intuition and measurement disagree often; every section starts from something observed (a counter, a SASS listing, an occupancy number).
The same rule applies to explanations: a hypothesis naming the cause of a gap (a technique the reference has that you lack, a feature the library cannot express) is worthless until it survives a falsification run — disable the technique on the reference and re-measure.
If the reference keeps its margin without it, the hypothesis was wrong, and acting on it — or stopping because of it — would have ended the search one step before the real cause.
Low-Hanging Fruit Pass#
Run this checklist immediately after Fitting Into cuBLASDx Idioms, before the coarse parameter search. It is mechanical, it needs no measurement to apply, and it catches the cases where a kernel is slow simply because it is not using the API the library expects.
This ordering is mandatory: searching parameters around a hand-written copy loop, a shared-memory round trip, or a generic-architecture descriptor only finds the least bad version of the wrong implementation.
The “Achieving High Performance” page of the HTML documentation lists the same advice in checklist form, plus a few items on layouts and precision selection that are worth a second pass after tuning.
Check |
What to look for |
|---|---|
Is this a release build? |
Compile the measured artifact with both |
Is the accelerated cubin actually present? |
For Blackwell architecture-specific descriptors, inspect the binary rather than trusting an architecture
shorthand. If |
Is the right API used? |
The timed mathematical contractions must call tensor-based |
Is |
Hand-written copy loops rarely vectorize as well. Remember |
If not copying, could a virtual tensor be used? |
An operand generated from indices (optionally combined with a small side vector) can be evaluated straight into shared memory, so a full matrix never has to exist in global memory. |
Are suggested layouts used? |
Setting the |
Is the register API used? |
The accumulator-returning |
Does each accumulator-owning thread produce multiple outputs? |
Make this a shape-screening requirement for performance candidates. A tile that gives active compute
threads only one output element rarely amortizes pipeline, callback, and epilogue overhead. Scan Especially important for trivial |
Did the tile search start from the configurator, then adapt its CTA model? |
Optional, and only for a kernel whose tile is free: run Translate its combined tile to a per-CTA For a new kernel, measure both and This is a starting point, not a fixed recommendation. Fused transforms, reductions, callbacks, and long-lived values change register and shared-memory pressure, so the best user tile can differ. Run micro-parameter scans both before tile search and around every promising tile. |
On SM100-family, was a cluster arm measured? |
|
Are bulk postprocessing functions used? |
Especially important on Blackwell, where these route through the staged |
How is gating employed? |
Check every |
Internal or reusable accumulator? |
Internal accumulation is the more portable speed-of-light path; a reusable accumulator costs an extra synchronization point and register pressure on architectures without TMEM. |
Are the general performance operators applied? |
Treat input streaming as a measured fast path, not an unconditional tag. It forbids A/B load operations; also validate epilogues that scatter an accumulator to shared memory, persistent/multiple-accumulator forms, and cooperative scratch placed next to the pipeline buffer. Keep a non-streaming descriptor as a control. On Blackwell UTCMMA the streaming descriptor needs exactly 128 threads. |
Is the pipeline thread count instruction-compatible? |
Treat this as a mandatory eligibility check before the general block-size scan. On Blackwell, an eligible UTCMMA input-streaming pipeline requires exactly 128 threads; using 256 can silently select a much slower implementation family. Do not extend that rule to load-operation, reusable-accumulator, or auxiliary-shared-memory pipelines: keep 256 as their control and validate 128 separately. On Hopper, test 128 and 256 threads, the two supported high-performance pipeline choices. Re-scan this axis after changing the tile because fragment ownership and register pressure change
with |
Can a leading dimension be employed? |
Where the data genuinely has one. Weigh it against losing the suggested layouts. |
Are the expected instructions generated? |
Check the SASS. Confirm the intended MMA and If the architecture-specific instructions are missing entirely, check that the |
Profile First#
Run Nsight Compute on the kernel before changing anything — but after the launch-geometry check in the porting workflow, which explains most uniform gaps without a profiler. The first question is only which of the four following sections applies, and the roofline plus the warp-stall breakdown answer it:
Stalled on ``MIO Throttle`` or ``Short Scoreboard`` — shared memory is the bottleneck. Go to Shared Memory Traffic.
Stalled on ``Long Scoreboard`` — waiting on global memory. Go to Overlap And Latency Hiding.
Low achieved occupancy with no memory stall — go to Occupancy And Register Pressure.
None of the above, and well below the roofline — the wrong instructions are probably being emitted. Go to Instruction Mix.
If the GEMM turns out to be a small fraction of the kernel’s time, stop and go to Tuning The Surrounding Code instead.
Occupancy And Register Pressure#
Register use grows with M * N, so the register accumulator is what usually costs occupancy. In order:
shrink the tile;
drop the block dimension (each thread owns more work, fewer blocks resident);
move a large epilogue out of registers into shared memory.
A low occupancy number is not by itself a finding — GEMMs hide latency with async instructions, not warp swaps. Act on it only when a stall reason points here.
Instruction Mix#
Dump the SASS and confirm the intended MMA appears, along with LDSM / STSM where you expect them.
Expect them only where they can occur at all: LDSM is generated only for supermma GEMMs — the
wgmma and tcgen05 paths feed the MMA from shared memory through descriptors, not through register
fragments — and STSM only when an accumulator is stored to shared memory. Their absence elsewhere is
not a finding. When they are missing where they should appear, the cause is almost never a tuning parameter:
No architecture-specific MMA (
wgmma,tcgen05) — theSMoperator is missing itsarch_specificmodifier, or another silentwgmmagate listed in the Hopper card is:WithPipeline(),EnableInputStreaming(),SM == 900exactly, 16-byteA/B/Calignment, aBlockDimof 128, 256, 512 or 1024 threads, a supportedPrecision, aSizedivisible by the warpgroup tiling, TN for tf32 / int8 / uint8 / fp8, and global tensors of exactly the descriptor’s value types — miss any one and the library quietly leaves thewgmmapath.No
LDSM/STSM— a suggested layout was paired with a default accumulator, or a non-identity load op changed the instruction selection on thewgmmaortcgen05path.supermmasupports load functors with both APIs, subject to theEnableInputStreaming()restriction.Plain layouts are not this cause: they keep
LDSMand cost bank conflicts instead, so absence ofLDSMis not evidence of plain layouts (see Shared Memory Traffic).No
TMA— check that the kernel uses the pipeline API and that its operand layouts and alignments support TMA. A pipeline can use cooperative copies instead, including when its computation usessupermma.
Arrangement And The MMA Instruction#
Arrangement is not only a statement about how your data sits in memory — it decides which MMA the library
can select, and each instruction class wants something different. In the N / T shorthand used below the
two letters are op(A) and op(B) against column-major storage, so T is row-major and N is
column-major: TN is a row-major A with a column-major B.
Instruction class |
Wants |
Why |
|---|---|---|
|
|
The default preference. Any other arrangement has to be transposed somewhere, and that cost lands either in a load op or in an extra shared-memory round trip. |
|
anything |
|
|
|
There is no |
FMA, fp32 |
|
|
|
anything for 16-bit operands; |
For 16-bit operands the |
|
anything for 16-bit, |
The |
Which class you actually get is resolved silently, so read this table for the class your descriptor
resolves to, not the one your architecture advertises: gmma requires every gate listed in
the Hopper card, utcmma every gate in the Blackwell card (the pipeline path on SM
1000/1030/1070/1100 with arch_specific and exactly 128 threads);
everything else resolves to supermma, or to FMA for float inputs. The practical
consequence: tf32 wants TN on every path (gmma, utcmma and
supermma’s ldmatrix all need K-major 32-bit operands), and float off the
pipeline path wants NT.
Two things follow. If your operands are 16-bit on supermma, gmma or utcmma, arrangement is a free
variable and should be chosen to make the surrounding code coalesce. If not, arrangement is a real constraint —
transposing in global memory beforehand is sometimes cheaper than fighting it, though that is a full extra pass
and therefore something to measure rather than assume.
Overlap And Latency Hiding#
Increase pipeline depth until the Long Scoreboard stalls stop falling, then stop. Each stage costs shared
memory linearly, and past the knee it buys occupancy loss and nothing else. A depth above the number of K
tiles is legal: the pipeline prefills only the tiles that exist, so extra stages cost shared memory without
adding overlap.
Tune depth per workload mode: a long single GEMM can profit from more stages than a batched GEMM with only
a few K tiles, even with the same descriptor. After a material win, re-scan only the axes the
change can couple to; an axis whose first scan showed under two percent sensitivity stays where it was.
If you are not on the pipeline path at all and the kernel is global-memory bound, that is the finding: go back to Finding Best Coarse Solution and change paths rather than tuning this one.
Tuning The Surrounding Code#
Once the GEMM is tuned, most of the kernel’s time is often elsewhere. Check: the global access pattern feeding the GEMM; grid shape and wave quantization; whether a neighbouring kernel could be fused in (the idiom catalogue, Fitting Into cuBLASDx Idioms, is the list of ways). Re-ask Axes That Can Be Relaxed — an axis fixed at the start may not be fixed any more.
Checklist#
Before calling the kernel finished:
Low-Hanging Fruit Pass has been walked, top to bottom.
The expected MMA and
LDSM/STSMinstructions appear in the SASS.The measurement follows the protocol in Agent BLAS Performance Guide — warm-up runs discarded, enough measured runs to be stable, clocks fixed if the number needs to be reproducible.
The result has been compared against a reference point: a roofline bound, cuBLASLt on the same shape (best of its heuristic candidates), or the naive implementation you started from.
Any relaxed axis has its accuracy cost written down. A precision relaxation whose error was never measured is not a result.