amd-agi/geak/tuning-triton
Claude Code and agent skill by amd-agi/geak
- community
Checking that the SKILL.md file is still in the repository.
Tune Triton and Gluon GPU kernels on AMD Instinct — build a config space including the AMD-only knobs (waves_per_eu, matrix_instr_nonkdim, kpack), prune it before racing, set the autotune key correctly, and confirm the win exceeds the noise floor. Use when the kernel you are tuning is written in Triton or Gluon.
Install
npx vskill@latest install amd-agi/geak/tuning-tritonSKILL.md
Tuning Triton kernels
Read ../tuning-core/SKILL.md first — this skill is the Triton specialization of that loop.
Triton is the one backend where you tune by authoring the search space, not by invoking a tuner. @triton.autotune will faithfully race whatever list you hand it, so the quality of your result is capped by the quality of that list. Three things decide the outcome:
- whether the AMD-specific knobs are in the space at all (they are easy to omit silently);
- whether the
key=is right (getting it wrong costs multiples, silently); - whether you pruned before racing (an unpruned space wastes most of the compile budget).
Measurements below are gfx942 / MI300X, Triton 3.6.0, bf16.
1. Put the AMD knobs in the space
This is the trap that costs the most performance, because it fails as a TypeError you can "fix" by deleting the knob.
triton.Config.__init__ accepts only num_warps, num_stages, num_ctas, maxnreg, pre_hook, ir_override. Passing an AMD knob as a keyword raises:
The knobs are real, but they travel in the first positional dict, alongside your tl.constexpr block sizes:
Verify rather than trust: cfg.all_kwargs() should list them. They are consumed by the HIP backend, whose full option set you can read on any box with
The three worth searching, and what they trade:
| knob | default | what it controls | when it matters | |
|---|---|---|---|---|
matrix_instr_nonkdim | 0 (auto) | MFMA instruction shape (e.g. 16 → 16×16, 32 → 32×32) | most impactful; small tiles and skinny shapes often want 16 | |
kpack | 1 | K-elements packed per load into the MFMA operand | second-order; interacts with BLOCK_K | |
waves_per_eu | 0 (auto) | occupancy hint — caps registers to fit more waves per EU | latency-bound kernels; can hurt register-heavy ones |
Measured on 4096³ bf16, tile 128×128×64, num_warps=8, num_stages=2, on both parts (tuning_benchmark/tools/triton_tables.py reproduces this):
| variant | gfx942 TFLOPS | spread | vs base | gfx950 TFLOPS | spread | vs base | |
|---|---|---|---|---|---|---|---|
| baseline (all auto) | 356.8 | 5.4% | — | 760.5 | 5.1% | — | |
matrix_instr_nonkdim=16 | 384.3 | 0.8% | +7.7% | 791.1 | 1.6% | +4.0% | |
matrix_instr_nonkdim=32 | 372.5 | 7.7% | +4.4% | 757.9 | 2.8% | −0.3% |
Read that table the way the next section says to, not the way it first looks. The conclusion survives the move — nonkdim=16 is the real win on both parts, nonkdim=32 is not distinguishable from nothing on either — but the magnitude halves and nonkdim=32 crosses from marginally positive to marginally negative. That is the general pattern for this whole skill on gfx950: the knob that matters still matters, and by less.
The gfx950 column was itself only measurable after interleaving the three variants. Timed back-to-back on that part they report 13.8–20.7% spreads, which swallows the entire result and would have made this table read as three indistinguishable numbers. See ../tuning-core/measurement.md Rule 6b before reproducing any of this on MI355.
2. Compare the win to the noise floor, not to zero
Both variants above are "faster than baseline". Only one of them is a result.
nonkdim=16: +7.7%, and its own spread is 0.8%. The effect is an order of magnitude
larger than the measurement's own variability. Real.
nonkdim=32: +4.4%, spread 7.7%. The claimed gain is smaller than the noise in the
measurement that produced it. Not distinguishable from nothing.
A single do_bench call reports neither of these facts. It returns one number for each and they both look like wins. The discipline — take independent repeats, report the median and the spread, and refuse to believe a delta smaller than the spread — is what separates them. Full rules in ../tuning-core/measurement.md.
Practical consequence for autotuning: @triton.autotune picks the single fastest config from one timing pass. When several configs sit inside each other's noise band, which one "wins" is partly chance. That is acceptable — you get one of the good configs — but it means you must not report the autotuner's margin as a speedup without re-measuring the winner properly.
3. Set key= to every variable that changes the answer
key is the list of kernel arguments whose values invalidate the cached choice. Autotuning runs once per distinct key tuple; every later call with a matching tuple reuses that config.
Omit a dimension and you silently serve a config tuned for a different shape. Same kernel, same config list, only key differs:
| shape | key=['M','N','K'] | key=['N','K'] | |
|---|---|---|---|
| M=4096 N=4096 K=4096 | 348.9 µs — picks BM=256,BN=128 | 350.2 µs — picks BM=256,BN=128 | |
| M=1 N=4096 K=4096 | 43.4 µs — picks BM=16,BN=64 | 162.9 µs — reuses BM=256,BN=128 |
3.75× slower on the decode shape, no error, no warning. The big-M config was cached first; M is not in the key, so the M=1 call is treated as the same problem. A BM=256 tile on a 1-row GEMM computes 255 rows of padding.
On gfx950 the same experiment gives 2.45× (M=1: 54.5 µs with the decode tile, 22.2 µs with the right one — and 180.6 µs vs 479.7 µs at M=4096 in the other direction). Smaller, still multiples, still silent. The penalty shrinks because a 256-row tile wastes proportionally less on a part with more bandwidth per CU, not because the mistake became safe.
Rules that follow:
- include every dimension the kernel branches or tiles on — for a GEMM that is
M,N,K; - include dtype-selecting or feature-flag arguments if they change which code path runs;
- if a value is genuinely constant, make it a
tl.constexprrather than trusting yourself to
remember it is constant.
Cost of being right: one autotune pass per distinct shape. In serving, bound that by bucketing M (see ../tuning-core/search_strategy.md) rather than by shortening the key.
4. Prune before you race
Every config in the list costs a compile plus a timing pass, and the space is multiplicative: block sizes × warps × stages × three AMD knobs reaches thousands of entries. Most are invalid — they exceed LDS, or give threads no work — and Triton finds that out by compiling them.
Filter analytically first. The pruning predicate and its measured reduction (1600 → 912 configs, 57%) are in ../tuning-core/search_strategy.md; example_autotune.py here shows it wired into a real kernel.
Then narrow by regime — this is worth more than it sounds. Same kernel, same autotuner, same M=1 N=4096 K=4096 shape, only the space differs:
| space raced | winner | gfx942 | gfx950 | |
|---|---|---|---|---|
general (BM ∈ 64,128,256) | BM=64, BN=64, BK=64, nonkdim=16 | 0.041 ms | 0.041 ms | |
decode (BM ∈ 16,32, deep BK) | BM=16, BN=32, BK=256, nonkdim=0, warps=2 | 0.027 ms | 0.023 ms |
34% faster from changing the candidate list alone on gfx942, and 44% on gfx950. The general space simply did not contain the right answer, and an autotuner cannot select what you did not offer it. Note the winner also chose nonkdim=0 — the auto MFMA shape — where the square case wanted 16; another reason a single fused list serves neither regime well.
This is the one measurement in this skill that got larger on the newer part. Splitting the space by regime is the highest-value item here on gfx950, above any individual knob.
One caveat the example makes explicit: @triton.autotune is evaluated at import time, so the config list is fixed before any argument is seen. You cannot choose the space from the shape inside the decorator. Either accept one larger union list, or define separate kernels per regime and dispatch between them yourself — a real cost of regime-splitting worth pricing in.
You can also use Triton's own hook, prune_configs_by=, which additionally lets you drop configs using the runtime argument values (early_config_prune, perf_model, top_k).
5. Control the cache while you tune
Autotune results and compiled binaries are cached under /root/.triton/cache (knobs.cache.dir). During a tuning session this is a hazard: you change the config list, rerun, and get the previous answer.
TRITON_PRINT_AUTOTUNING=1 is the cheapest engagement signal Triton offers: it tells you autotuning actually ran and which config won. If it prints nothing, you are on a cached result and your edits did not take effect. Also readable in-process as kernel.best_config after the first call — that is how the key= table above was produced.
6. Gluon
Gluon (triton.experimental.gluon) is the lower-level dialect: you place data layouts by hand instead of letting the compiler infer them. Everything above still applies — same Config, same key, same noise discipline — but the search space gains layout choices (BlockedLayout, SwizzledSharedLayout, PaddedSharedLayout, DotOperandLayout, AMDMFMALayout).
The arch split is explicit in the API, which makes it a good place to reason about gfx942 vs gfx950:
cdna3(gfx942):mfma,buffer_load/buffer_store,buffer_atomic_*cdna4(gfx950): all of the above plusmfma_scaled,get_mfma_scale_layout,
async_copy
So a Gluon kernel written against cdna3 runs on both; one written against cdna4 does not run on gfx942. Tune on the arch you will deploy on — and note that mfma_scaled is the entry point for the microscaled dtypes that only gfx950 has, which is why those shapes have no gfx942 equivalent to carry a tuned config over from.
Reach for Gluon only after the Triton-level space is exhausted: the extra layout freedom is also extra ways to be slow, and the compiler's inferred layouts are usually good.
Tuning someone else's Gluon kernel: the space is smaller than the signature
A hand-placed layout can encode the tile shape, and then the tile shape stops being tunable. aiter's gluon.gemm_afp4wfp4 accepts the usual ten-key GEMM config and writes its layouts as literal basis vectors:
A basis of 128 needs a dimension above 128, and three warp bases mean eight warps. The tile is pinned at 256×256×256 on num_warps=8 and nothing in the signature says so. Measured on gfx950, one knob moved at a time:
| knob | result | |
|---|---|---|
BLOCK_SIZE_M/N/K, num_warps | LLVM ERROR ... abort() — the process dies | |
num_stages, matrix_instr_nonkdim | accepted, declared, never read; median moves <2% | |
GROUP_SIZE_M, waves_per_eu, NUM_KSPLIT, cache_modifier | real |
Three consequences worth carrying to any hand-written-layout kernel:
Probe the space in subprocesses. The tile-shape failure is abort(), not an exception — no try will catch it, and an in-process sweep loses every result it had gathered. One subprocess per candidate, judged by exit code, is the only safe way to map the boundary. Do it once and turn the answer into a constraint.
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingperf_knowledge/expert_skills/tuning/tuning-triton/SKILL.md
Popularity
- Repository stars
- 182
- Repository forks
- 56
- 7-day trend score
- 26
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/amd-agi/geak/tuning-triton)