Skip to main content

llvm

Tune and analyse a FlyDSL kernel at the LLVM level: pick a compile hint, function attribute, or AMDGPU backend flag, then PROVE it reached codegen. Covers the `compile_hints` keys, the `llvm_options` cl::opt escape hatch, `rocdl.*` / raw LLVM function attributes, per-arch options for gfx950 and gfx1250, dumping and reading `NN_llvm_ir.ll`, replaying the LLVM half with `llc`, and the knobs in this repo that silently do nothing. Use when asked to reduce spills, raise occupancy, change the instruction schedule, set waves_per_eu or flat_work_group_size, pass an `-mllvm` or `-amdgpu-*` flag, sweep scheduler strategies, read LLVM IR or ISA the compiler produced, or explain why a tuning knob had no measurable effect. For before/after register and spill deltas use `/isa-resource-diff`; for why-is-it-slow with a profiler use `/kernel-trace-analysis`; for building LLVM use `/build-flydsl`. Usage: /llvm <question, kernel, or knob>

Source facts

Repository
ROCm/FlyDSL
Last source activity
September 22, 2026 at 11:07
Detected SKILL.md language
English
Stars
305
Forks
138

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
8 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
llvm
description
Tune and analyse a FlyDSL kernel at the LLVM level: pick a compile hint, function attribute, or AMDGPU backend flag, then PROVE it reached codegen. Covers the `compile_hints` keys, the `llvm_options` cl::opt escape hatch, `rocdl.*` / raw LLVM function attributes, per-arch options for gfx950 and gfx1250, dumping and reading `NN_llvm_ir.ll`, replaying the LLVM half with `llc`, and the knobs in this repo that silently do nothing. Use when asked to reduce spills, raise occupancy, change the instruction schedule, set waves_per_eu or flat_work_group_size, pass an `-mllvm` or `-amdgpu-*` flag, sweep scheduler strategies, read LLVM IR or ISA the compiler produced, or explain why a tuning knob had no measurable effect. For before/after register and spill deltas use `/isa-resource-diff`; for why-is-it-slow with a profiler use `/kernel-trace-analysis`; for building LLVM use `/build-flydsl`. Usage: /llvm <question, kernel, or knob>
allowed-tools
Read Write Edit Bash Grep Glob
# LLVM-Level Kernel Tuning and Analysis FlyDSL reaches LLVM through four lanes, and **only some of them are alive**. This skill routes you to the one that works for your knob, then makes you prove it landed. The repo is knob-rich and instrument-poor: four knobs were found doing nothing at all, each looking like a real recompile because an unknown hint still changes the JIT cache key. **Hard rule: never report "the knob had no effect" without a positive control.** Establish that your instrument reads *something* before you record a negative. This project has been burned by false negatives repeatedly; §7 is not optional. ## Pick the right skill first | Question | Skill | |---|---| | Which LLVM knob do I turn, and did it actually apply? | **this skill** (compile-only, no GPU) | | Did registers / spills / LDS move between two builds? | `/isa-resource-diff` (owns `*_final_isa.s` parsing) | | *Why* is this kernel slow — which instructions stall? | `/kernel-trace-analysis` (needs a GPU run + ATT trace) | | How do I collect a trace at all? | `/capture-kernel-trace` | | Which commit made it slow? | `/bisect-perf-regression` | | How do I build or rebuild LLVM? | `/build-flydsl` | | How do I add a backend op in C++/TableGen? | `/add-target-atom-op` | | GEMM tiling / pipelining strategy | `/gemm-optimization` | This skill is **compile-only**. It answers "what did the compiler emit, and why" — never "where did the cycles go". Register and spill questions get measured by `isa_resource_table.py`; do not re-parse ISA by hand. ## Step 1 — Establish your toolchain Do this before quoting any flag. Co-located LLVM builds on this box **disagree on which flags exist**, so "the flag is not recognised" is meaningless until you know which binary you asked. ```bash cat thirdparty/llvm-build-info.json # the pin that compiles kernels echo "${FLYDSL_COMPILE_LLVM_DIR:-<empty = embedded>}" # which LLVM does final codegen ``` ⚠ Answer flag-existence questions against the **checkout matching the pin** in `thirdparty/llvm-build-info.json`. A newer local LLVM will register flags the pin does not have, so "llc accepts it" proves nothing unless the versions agree. - Some local trees are empty or stale — confirm `llvm/lib/Target/AMDGPU` is populated before trusting a grep over one. - A runnable `llc` from a build of that pin covers gfx942 / gfx950 / gfx1250. If yours came from a different tree, cross-check flag existence against the pin. - ⚠ `build-fly/bin/fly-opt` **cannot run on the host** (missing `GLIBC_2.33/2.34`, `GLIBCXX_3.4.29/3.4.30`). Run it through the dev container instead. - A second pin exists in `thirdparty/custom-llvm-tools.json` (ROCm fork, `c8cf6da4`) used **only** for the external `mlir-opt` tarball. ⚠ **The host Python is 3.6; this repo targets 3.10+.** Both helper scripts below refuse to run on the host with a clear message and exit 2 — run them in the dev container, which has 3.10 and sees the same bind-mounted paths: ```bash docker exec -w $PWD <dev-container> python3 <script> <args> ``` Before using any `llvm_options` flag, check it exists in *your* LLVM: ```bash docker exec -w $PWD <dev-container> python3 \ ${CLAUDE_SKILL_DIR}/scripts/llvm_knob_check.py enable-post-misched ``` A flag appearing in a shipped kernel is **not** evidence that it exists — see §8. ## Step 2 — Dump hygiene ```bash export FLYDSL_DUMP_DIR=$(mktemp -d) FLYDSL_DUMP_IR=1 FLYDSL_RUNTIME_ENABLE_CACHE=0 python your_kernel.py ls $FLYDSL_DUMP_DIR/*/ # expect ~22 files, 00_origin.mlir .. 21_final_isa.s ``` ⚠ Traps that manufacture false negatives, all confirmed in this repo: - **`FLYDSL_RUNTIME_ENABLE_CACHE=0` is mandatory.** A cache hit skips compilation and you get **no dumps at all**. - ⚠ **`scripts/dumpir.sh` lies about it.** It prints `(cache disabled)` but never sets the variable, so a cache hit yields no dumps under a banner claiming otherwise. Set it yourself. - **Stage numbers are positional and unstable.** `FLYDSL_DEBUG_ENABLE_DEBUG_INFO=1` inserts a stage at 19 and shifts `llvm_ir`/`final_isa` to 21/22. **Always match by suffix** (`*_llvm_ir.ll`, `*_final_isa.s`), never by index. - **Dump dirs are keyed by kernel name only.** Different JIT specialisations overwrite each other. Use a fresh dir per run and compare whole trees. - **`scripts/run_tests.sh` skips the MLIR stage** when FileCheck is missing. It says so explicitly and `FLYDSL_REQUIRE_FILECHECK=1` makes it an error, but still assert you saw `PASS` lines, never merely the absence of `FAIL`. - The ISA dump is **skipped entirely** in external-LLVM mode. ## Step 3 — The four lanes into LLVM | Lane | Reaches LLVM as | Status | |---|---|---| | `compile_hints` → attribute path | `rocdl.waves_per_eu` → `"amdgpu-waves-per-eu"` | **live** | | `compile_hints` → `opts=` CLI path | `gpu-module-to-binary{opts="..."}` | ⚠ **DEAD on AMD** | | `llvm_options` | LLVM `cl::opt` set in-process (or argv in external mode) | **live** | | `kernel_attrs` / `value_attrs` | any MLIR attr on `gpu.func`, incl. raw `passthrough` | **live** | | ROCDL intrinsics | `llvm.amdgcn.sched.*`, `s_setprio`, `s_waitcnt` | **live** | ⚠ **The `opts=` lane is discarded on AMD.** `TargetOptions::tokenizeCmdOptions()` is called only by the XeVM and NVVM targets; ROCDL never calls it, and `ROCDL::assembleIsa()` takes no flags parameter, so anything placed there is dropped without a diagnostic. FlyDSL used to route `--amdgpu-waves-per-eu`, `--amdgpu-num-vgpr` and `-g` through it; `RocmBackend._pipeline_parts` now leaves `bin_cli_opts` empty, with a comment recording why. The first two were never command-line flags in any case: they are IR function attributes, which `llc` rejects outright when passed as flags. `waves_per_eu` works *only* because `RocmBackend.lower_compile_hints` also sets the `rocdl.waves_per_eu` MLIR attribute. `maxnreg` had no such second path — see §8. ## Step 4 — The compile_hints keys | Key | Reaches LLVM as | Validated? | Verify with | |---|---|---|---| | `waves_per_eu` | `rocdl.waves_per_eu` → `"amdgpu-waves-per-eu"="N"` | bool/int/range | grep `*_llvm_ir.ll` | | ~~`maxnreg`~~ | **removed** — now raises; see §8 | raises | n/a | | `fast_fp_math` | `rocdl-attach-target{fast=true}` **+** ambient tracing fastmath | truthiness | `fadd fast float` in IR | | `unsafe_fp_math` | `rocdl-attach-target{unsafe-math=true}` only | truthiness | ⚠ pipeline string only — **no** op flags | | `fastmath` | explicit fastmath flags on traced ops | normalised | `fadd fast float` in IR | | `llvm_options` | arbitrary LLVM `cl::opt`s | type dispatch | ISA diff | ⚠ **The dict is un-whitelisted.** There is no key whitelist and no unknown-key error. A typo is ignored by every consumer **but still rides the JIT cache key**, so you get a genuine recompile that behaves exactly like no hint. This is the single most likely way to conclude a knob "does not work". `fast_fp_math` and `unsafe_fp_math` are **not** a symmetric pair: the first also seeds ambient tracing fastmath, the second does not. ## Step 5 — Hint layering rules - `merge_compile_hints` **drops `None`** — you cannot clear an inherited hint. `waves_per_eu=0` is the only sentinel ("no override"), and `0` and absent produce **different cache keys**. - The merge is **shallow**: an inner `llvm_options` dict wholly **replaces** the outer one. To add one flag, merge by hand: ```python opts = {**fn.compile_hints.get("llvm_options", {}), "new-flag": True} ``` - `flyc.compile[{...}](fn)` **mutates the JitFunction permanently** — it is not a scoped copy. For scoped behaviour use `CompilationContext.compile_hints({...})`. - The autotuner reaches only `waves_per_eu`; every other key must be set around the tuned call, e.g. in a `CompilationContext.compile_hints({...})` block. ## Step 6 — Arch applicability | Knob | gfx942 | gfx950 | gfx1250 | Gate | |---|---|---|---|---| | `amdgpu-waves-per-eu` | works (max 8) | works (max 8) | works (**max 16**) | — | | `amdgpu-num-vgpr` | ⚠ **doubled** | ⚠ **doubled** | exact | `hasGFX90AInsts()` | | `amdgpu-agpr-alloc` | works | works | **inert** | `hasMAIInsts()` | | `amdgpu-flat-work-group-size` | works | works | works | — | | `-amdgpu-sched-strategy=coexec` | ⚠ warns, **runs anyway** | ⚠ warns, **runs anyway** | intended | `hasGFX1250Insts()` | | `amdgpu-expert-scheduling-mode` | ⚠ gate false (GFX12+) | ⚠ gate false | works | `>= GFX12` | | `-mattr=+wavefrontsize64` | works | works | ⚠⚠ **emits an empty object** | `supportsWave64()` | | legacy `s_waitcnt` intrinsic | works | works | ⚠⚠ **aborts llc** | — | ⚠ **The same number means different things across families.** `amdgpu-num-vgpr=64` yields VGPR 64 + AGPR 64 = **128** on gfx942/gfx950 (silently doubled, `GCNSubtarget.cpp:625-626`) but exactly **64** on gfx1250. Max occupancy is 8 waves/SIMD on CDNA and **16** on gfx1250, so `waves_per_eu=2` requests 1/4 of peak on CDNA and 1/8 on gfx1250. **Do not port these values unexamined.** > Load `references/arch-gfx950.md` when tuning for gfx950 / MI355X. > Load `references/arch-gfx1250.md` when tuning for gfx1250. ## Step 7 — Verify it took effect **Every claim is an artifact grep, never a timing measurement.** Timings on gfx950 carry ±14% clock noise; a knob that changed nothing in the IR cannot have changed the runtime. | Knob | Assert on | Expect | |---|---|---| | `waves_per_eu` | `*_llvm_ir.ll` | `"amdgpu-waves-per-eu"="N"` | | `flat_work_group_size` | `*_final_isa.s` | `.max_flat_workgroup_size: N` | | `fast_fp_math`, `fastmath` | `*_llvm_ir.ll` | `fast` / `nnan` etc. on the op, e.g. `fadd fast float` | | `unsafe_fp_math` | pipeline string | `unsafe-math=true` on `rocdl-attach-target`; it adds **no** op flags | | denormal mode | `*_final_isa.s` | `.amdhsa_float_denorm_mode_32` — ⚠ never the IR attribute | | an `llvm_options` cl::opt | two `*_final_isa.s` | instruction order differs | | any ROCDL intrinsic | `*_final_isa.s` | the **mnemonic** is present | | registers / spills / LDS | `isa_resource_table.py` | never hand-parse ISA | ```bash docker exec -w $PWD <dev-container> python3 \ ${CLAUDE_SKILL_DIR}/scripts/llvm_ir_attrs.py $FLYDSL_DUMP_DIR # compare two runs directly: docker exec -w $PWD <dev-container> python3 \ ${CLAUDE_SKILL_DIR}/scripts/llvm_ir_attrs.py <before> <after> --diff ``` ### The mandatory negative control After an assertion passes, **repeat it with a deliberately misspelled key**: ```python CompilationContext.compile_hints({"waves_per_eU": 4}) # note the typo ``` The assertion **must now fail**. If it still passes, your instrument is dead — stop and fix that before recording any result. An un-whitelisted hint dict makes this failure silent by default (§4). ### Replaying just the LLVM half ```bash llc -mtriple=amdgcn-amd-amdhsa -mcpu=gfx950 \ -pass-remarks-analysis=kernel-resource-usage NN_llvm_ir.ll -o /dev/null ``` `kernel-resource-usage` prints VGPRs, AGPRs, SGPRs, ScratchSize, Occupancy, spills and LDS in one shot — prefer it to scraping assembly. ⚠ Two blockers on this route: - The dumped `.ll` has **no `target triple`** and no `target-cpu` attribute. Always pass `-mtriple` **and** `-mcpu`, or you silently target a generic subtarget. - MLIR emits `f0x`-prefixed hex float literals. The pinned LLVM parses these fine ([measured] on gfx950 and gfx1250); if an older `llc` rejects one, check its version before rewriting the constant. ⚠ `-stats` and `-debug-only` need an assertions build. On a Release `llc`, `-stats` **parses cleanly and prints nothing** — grepping it for "spill" yields a false "no spills". ## Step 8 — Knobs in this repo that do not work Each was verified by running `llc` against the pinned LLVM. Do not reach for these. | Knob | Reality | Status | |---|---|---| | `maxnreg` | **Was silently inert.** Its only consumer emitted `--amdgpu-num-vgpr` into the dead `opts=` lane, and that is not a flag. No attribute fallback, yet it still changed the cache key — so it looked like a real recompile, and `autotune.Config` was searching an identity transform. | removed; the hint now raises `ValueError` and `Config(maxnreg=…)` raises `TypeError`. Stale cache files drop the key rather than failing to load | | `amdgpu-schedule-regions` | **Does not exist** in the pin. `llc --help-hidden` has zero hits and rejects it, so in external mode it would fail the `mlir-opt` subprocess. | removed from `flash_attn_generic.py` | | `lsr-drop-solution=<int>` | **Invalid value.** The option is `cl::opt<boolOrDefault>`; `llc` reports "invalid value for boolean argument". Kernels correctly pass `True`. | test sample switched to `amdgpu-schedule-metric-bias` |
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub