|
| 1 | +--- |
| 2 | +name: isa-resource-diff |
| 3 | +description: > |
| 4 | + Detect per-kernel GPU resource regressions (VGPR, SGPR, register spills, scratch, |
| 5 | + static LDS) by diffing the final ISA before and after a change, using |
| 6 | + scripts/isa_resource_table.py. Compile-only: needs no GPU and no profiler run, so |
| 7 | + it works on any target the compiler supports and runs in seconds. Use when asked |
| 8 | + whether a change increased register pressure, caused spilling, or hurt occupancy, |
| 9 | + when reviewing a kernel change for resource impact, or as a fast pre-check before |
| 10 | + spending a profiling run. |
| 11 | + Usage: /isa-resource-diff [<test-or-command>] [--arch <gfx>] |
| 12 | +allowed-tools: Read Write Bash Grep Glob |
| 13 | +--- |
| 14 | + |
| 15 | +# ISA Resource Diff |
| 16 | + |
| 17 | +Compare per-kernel register, spill, scratch, and LDS usage between two builds to |
| 18 | +catch resource regressions that functional tests do not surface. |
| 19 | + |
| 20 | +## Pick the right skill first |
| 21 | + |
| 22 | +| Question | Skill | |
| 23 | +|---|---| |
| 24 | +| Did my change increase registers / cause spills / grow LDS? | **this skill** (compile-only, seconds, no GPU) | |
| 25 | +| *Why* is this kernel slow — which instructions stall, and on what? | `/kernel-trace-analysis` (needs a GPU run + rocprofv3 ATT trace) | |
| 26 | +| Which commit made it slow? | `/bisect-perf-regression` (needs a runnable benchmark) | |
| 27 | +| How do I collect a trace at all? | `/capture-kernel-trace` | |
| 28 | + |
| 29 | +This skill measures **resources, not time**. A clean result here does not mean |
| 30 | +performance is unchanged — it means register/LDS/spill pressure is unchanged. |
| 31 | +A regression here is a strong, cheap signal that is usually worth acting on |
| 32 | +before profiling, because spilling and occupancy cliffs dominate most kernel |
| 33 | +slowdowns. See §7 of `docs/kernel_tuning_guide.md` for what to do about one. |
| 34 | + |
| 35 | +## Arguments |
| 36 | + |
| 37 | +| Argument | Required | Description | |
| 38 | +|---|---|---| |
| 39 | +| `<TEST-OR-COMMAND>` | No | What to run to produce dumps. Defaults to asking the user. Example: `pytest tests/kernels/test_softmax.py -q` | |
| 40 | +| `--arch <gfx>` | No | Target for compile-only runs, e.g. `gfx950`. Omit to use local hardware | |
| 41 | + |
| 42 | +If the user already has two dump directories or two JSON snapshots, skip to Step 3. |
| 43 | + |
| 44 | +## Workflow |
| 45 | + |
| 46 | +### Step 1 — Capture the "before" side |
| 47 | + |
| 48 | +Check out or stash to the baseline state first, then: |
| 49 | + |
| 50 | +```bash |
| 51 | +FLYDSL_DUMP_IR=1 FLYDSL_DUMP_DIR=/tmp/isa-before FLYDSL_RUNTIME_ENABLE_CACHE=0 \ |
| 52 | + python3 -m pytest tests/kernels/test_softmax.py -q |
| 53 | +``` |
| 54 | + |
| 55 | +`FLYDSL_RUNTIME_ENABLE_CACHE=0` is **required, not optional** — see Pitfalls. |
| 56 | + |
| 57 | +For a target without local hardware, add `ARCH=<gfx> COMPILE_ONLY=1`: |
| 58 | + |
| 59 | +```bash |
| 60 | +ARCH=gfx950 COMPILE_ONLY=1 \ |
| 61 | +FLYDSL_DUMP_IR=1 FLYDSL_DUMP_DIR=/tmp/isa-before FLYDSL_RUNTIME_ENABLE_CACHE=0 \ |
| 62 | + python3 -m pytest tests/kernels/test_softmax.py -q |
| 63 | +``` |
| 64 | + |
| 65 | +### Step 2 — Capture the "after" side |
| 66 | + |
| 67 | +Apply the change, then rerun **the identical command** into a *fresh* directory |
| 68 | +(`/tmp/isa-after`). Same test, same parameters, same arch, same cache setting. |
| 69 | + |
| 70 | +### Step 3 — Diff |
| 71 | + |
| 72 | +```bash |
| 73 | +python3 scripts/isa_resource_table.py diff /tmp/isa-before /tmp/isa-after |
| 74 | +``` |
| 75 | + |
| 76 | +The tool requires **Python 3.10+**. If `python3` is older it exits 2 with a clear |
| 77 | +message; use `python3.10 scripts/isa_resource_table.py …` instead. |
| 78 | + |
| 79 | +Both sides may independently be a dump directory or a `.json` snapshot, so a |
| 80 | +baseline can be captured once and reused: |
| 81 | + |
| 82 | +```bash |
| 83 | +python3 scripts/isa_resource_table.py summarize /tmp/isa-before --json baseline.json |
| 84 | +python3 scripts/isa_resource_table.py diff baseline.json /tmp/isa-after |
| 85 | +``` |
| 86 | + |
| 87 | +## Reading the output |
| 88 | + |
| 89 | +``` |
| 90 | +* = regression trigger; other columns are informational. |
| 91 | + vgpr = total (arch+acc, LLVM's occupancy number); arch_vgpr/agpr are its split -- do not add them. |
| 92 | +kernel *vgpr arch_vgpr agpr ... *lds_static_bytes lds_read ... |
| 93 | +------------------------------------------------------------------------------------------------- |
| 94 | +gemm::d128_fmha_fwd_kernel_0 942->960(+18) 942->960(+18) 0 ... 212992->229376(+16384) 12 |
| 95 | +
|
| 96 | +compared 1 of 1 kernels; 0 unchanged; 1 changed; worsened: 2; improved: 0 |
| 97 | +RESULT: REGRESSION |
| 98 | +``` |
| 99 | + |
| 100 | +Only changed and problematic kernels are printed. The last two stdout lines are |
| 101 | +always a count line and a `RESULT:` verdict that matches the exit code exactly. |
| 102 | + |
| 103 | +**Columns marked `*` are regression triggers**; the rest are context. |
| 104 | + |
| 105 | +| Column | Trigger | Read from | What it is | |
| 106 | +|---|---|---|---| |
| 107 | +| `vgpr` | yes | `.vgpr_count` metadata | Total VGPRs, arch + accumulator — LLVM's own occupancy number | |
| 108 | +| `arch_vgpr` | no | `.set` symbol `num_vgpr` | The arch half of that total | |
| 109 | +| `agpr` | no | `.set` symbol `num_agpr` | The accumulator half of that total | |
| 110 | +| `sgpr` | yes | `.sgpr_count` metadata | SGPRs including the fixed extras (VCC, XNACK, FLAT_SCRATCH) | |
| 111 | +| `numbered_sgpr` | no | `.set` symbol `numbered_sgpr` | SGPRs without those extras; tells a real increase from VCC becoming live | |
| 112 | +| `vgpr_spill` | yes | `.vgpr_spill_count` metadata | VGPRs the allocator spilled | |
| 113 | +| `sgpr_spill` | yes | `.sgpr_spill_count` metadata | SGPRs the allocator spilled | |
| 114 | +| `scratch_bytes` | yes | `.private_segment_fixed_size` metadata | Private segment per work-item; exact on every target | |
| 115 | +| `lds_static_bytes` | yes | `.group_segment_fixed_size` metadata | Statically allocated LDS per work-group | |
| 116 | +| `lds_read` / `lds_write` | no | instruction count | `ds_read`/`ds_load` and `ds_write`/`ds_store` sites | |
| 117 | +| `scratch_store` / `scratch_load` | no | instruction count | `scratch_*` sites; `n/a` where spilling goes through `buffer_*` | |
| 118 | +| `matrix_ops` | no | instruction count | MFMA / WMMA sites | |
| 119 | + |
| 120 | +Three things are easy to misread: |
| 121 | + |
| 122 | +- **Do not add `arch_vgpr` and `agpr` to `vgpr`.** `vgpr` is already the total |
| 123 | + (arch + accumulator) and is the only VGPR-family trigger. The other two are its |
| 124 | + split, shown so you can tell *which half* moved. Moving accumulators into AGPRs |
| 125 | + — which the tuning guide recommends — deliberately does not count as a regression. |
| 126 | +- **`n/a` is not `0`.** It means the quantity does not exist on this target, e.g. |
| 127 | + `scratch_store`/`scratch_load` on a target that spills through `buffer_*`. Use |
| 128 | + `scratch_bytes`, which is exact everywhere. |
| 129 | +- **`?` means unparsed** — the tool could not read something it reports. Any `?` |
| 130 | + forces exit 2. Never treat it as unchanged. |
| 131 | + |
| 132 | +### Exit codes |
| 133 | + |
| 134 | +| Code | stdout | Meaning | What an agent should do | |
| 135 | +|---|---|---|---| |
| 136 | +| `0` | `RESULT: OK` | Everything comparable, no trigger increased | Proceed | |
| 137 | +| `1` | `RESULT: REGRESSION` | Everything comparable, a trigger increased | Investigate — this is a real finding | |
| 138 | +| `2` | `RESULT: NOT TRUSTWORTHY` | The tool cannot answer | **Fix the inputs and rerun. Do not report "no regression"** | |
| 139 | + |
| 140 | +Exit `1` is a claim about the code under test; exit `2` is a claim about the |
| 141 | +tool's own confidence. Everything that would leave the answer partial reports `2` |
| 142 | +and never `1`: a crash, an empty dump directory, a dump file that does not parse |
| 143 | +or does not decode, a metadata entry with no kernel identity or a duplicated one, |
| 144 | +a negative resource count, a kernel present on only one side, and a target that |
| 145 | +differs — or that the tool cannot name — on either side. |
| 146 | + |
| 147 | +For scripting, `-q/--quiet` prints only the verdict line, and `--json PATH` |
| 148 | +writes the full comparison as machine-readable JSON: |
| 149 | + |
| 150 | +```bash |
| 151 | +python3 scripts/isa_resource_table.py diff /tmp/isa-before /tmp/isa-after -q --json report.json |
| 152 | +case $? in |
| 153 | + 0) echo "no resource regression" ;; |
| 154 | + 1) echo "REGRESSION — see report.json" ;; |
| 155 | + *) echo "inconclusive — inputs are bad, do not claim a clean result" ;; |
| 156 | +esac |
| 157 | +``` |
| 158 | + |
| 159 | +## Acting on a regression |
| 160 | + |
| 161 | +Map the column that moved to a cause, then follow `docs/kernel_tuning_guide.md`: |
| 162 | + |
| 163 | +| Column increased | Usual cause | |
| 164 | +|---|---| |
| 165 | +| `vgpr` | More live values; larger tiles or deeper pipelining/unrolling | |
| 166 | +| `vgpr_spill` / `sgpr_spill` / `scratch_bytes` | Register pressure crossed the budget — normally the most damaging of these signals | |
| 167 | +| `lds_static_bytes` | Bigger shared tiles or added double buffering; may cross an occupancy step | |
| 168 | +| `sgpr` alone, by 2, with `numbered_sgpr` flat | Usually just VCC becoming live — rarely meaningful | |
| 169 | + |
| 170 | +If a resource regression is confirmed but the kernel is not actually slower, |
| 171 | +say so rather than "fixing" it: these are proxies for occupancy, not timings. |
| 172 | +Confirm with `/kernel-trace-analysis` before reworking a kernel. |
| 173 | + |
| 174 | +## Pitfalls |
| 175 | + |
| 176 | +These silently produce a *confident wrong answer* if ignored: |
| 177 | + |
| 178 | +- **Dump directories are keyed by kernel name only, with no specialization key.** |
| 179 | + Every JIT specialization of one kernel writes to the same directory and |
| 180 | + overwrites the previous one, so a parametrized test leaves only its last |
| 181 | + variant. Diff one shape at a time when the answer must be exact. |
| 182 | +- **A cache hit produces no dump at all.** Hence `FLYDSL_RUNTIME_ENABLE_CACHE=0` |
| 183 | + on both sides. Without it a kernel can silently vanish from one side, which |
| 184 | + the tool reports as `ONLY IN BEFORE` and exit 2. |
| 185 | +- **`lds_static_bytes` is static LDS only.** A kernel using |
| 186 | + `SharedAllocator(static=False)` reports `0` no matter how much LDS it takes at |
| 187 | + dispatch; the tool prints a `dynamic LDS in use` note for it. LDS regressions |
| 188 | + in those kernels are invisible here. |
| 189 | +- **The stage number in `NN_final_isa.s` varies between runs.** Nothing cleans the |
| 190 | + dump directory, so reusing one can leave two files side by side. The tool uses |
| 191 | + the highest-numbered one and warns; prefer a fresh directory per run. |
| 192 | +- **Diffs across targets are refused** (exit 2). Register files and LDS banking |
| 193 | + differ between architectures, and `xnack`/`sramecc` change code generation |
| 194 | + within one, so both sides must report the same processor *and* the same target |
| 195 | + features. A target the tool cannot name is refused for the same reason. The |
| 196 | + triple's environment field is normalized, so `amdgcn-amd-amdhsa--gfx942` and |
| 197 | + `amdgcn-amd-amdhsa-unknown-gfx942` are the same target. |
| 198 | +- **Warnings on stderr never change the exit code.** They describe the input tree |
| 199 | + and are worth reading before trusting a clean result. |
| 200 | + |
| 201 | +## Verifying the tool itself |
| 202 | + |
| 203 | +`tests/unit/test_isa_resource_table.py` is backend-agnostic and needs no build: |
| 204 | + |
| 205 | +```bash |
| 206 | +python3 -m pytest tests/unit/test_isa_resource_table.py -q |
| 207 | +``` |
| 208 | + |
| 209 | +It generates its ISA input in `make_isa()` rather than checking dumps in, so the |
| 210 | +test file states exactly which parts of LLVM's output the parser relies on. The |
| 211 | +shapes cover both sides of each axis that has already broken it once: whether the |
| 212 | +target emits `.agpr_count`, which LDS mnemonic spelling it uses, and whether the |
| 213 | +target ID spells the triple's environment as empty or as `unknown`. The rest of |
| 214 | +the file pins the fail-closed verdicts: a mismatched processor, a mismatched |
| 215 | +feature set, an unnameable target, an unreadable or undecodable dump file, a |
| 216 | +kernel entry with no identity or a duplicated one, and a negative count all have |
| 217 | +to reach exit 2. |
| 218 | + |
| 219 | +If the tool reports `?` on a dump that looks healthy, LLVM's assembly format has |
| 220 | +probably drifted — update `make_isa()` to match the new shape rather than |
| 221 | +loosening the parser, since the failure is deliberately loud. |
0 commit comments