Skip to main content

thread-trace

Diagnoses why a compute kernel on an AMD GPU is slow, instruction by instruction, with Advanced Thread Trace (rocprofv3 --att) and the rocprof-trace-decoder Python API. Use when a kernel runs below its expected throughput and you need the instructions and source lines that hold the time, such as waves stalled on busy vector or matrix (MFMA) instructions, waiting on memory (s_waitcnt) or at barriers (__syncthreads), or idle; or when capturing or analyzing .att traces. Not for hardware counters, roofline, PC sampling, host-side timelines, or NVIDIA GPUs.

Source facts

Repository
ROCm/rocm-systems
Last source activity
October 14, 2026 at 04:33
Detected SKILL.md language
English
Stars
536
Forks
453

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
19 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
thread-trace
description
Diagnoses why a compute kernel on an AMD GPU is slow, instruction by instruction, with Advanced Thread Trace (rocprofv3 --att) and the rocprof-trace-decoder Python API. Use when a kernel runs below its expected throughput and you need the instructions and source lines that hold the time, such as waves stalled on busy vector or matrix (MFMA) instructions, waiting on memory (s_waitcnt) or at barriers (__syncthreads), or idle; or when capturing or analyzing .att traces. Not for hardware counters, roofline, PC sampling, host-side timelines, or NVIDIA GPUs.
# Advanced Thread Trace (ATT) ATT records the instructions that the traced waves of a kernel executed, in order (fewer when trace data is lost or the buffer fills): when the wave first tried to issue it, how many cycles it stalled before it issued, and its duration, from which the idle time before the next instruction follows. By default it covers the waves of one compute unit per traced shader engine (on RDNA GPUs, one SIMD of a WGP), for the dispatches you select. Timing says how slow a kernel is; the trace says which instructions the time went to. ## Prerequisites - rocprofv3 from ROCm 10.0 or later, with thread trace support for your GPU. - The rocprof-trace-decoder library and its Python package both ship with ROCm 10.0 and later. [capture.md](resources/capture.md#setup) covers each one. Build the kernel as you run it (keep its optimization flags, such as `-O3`; a debug build, `-O0`, runs different code and timing). Debug information is optional: ATT works at the instruction level, and `-g` or `-gline-tables-only` adds the source line of each instruction ([capture.md](resources/capture.md#source-lines-optional)). ## How to use ATT ```bash rocprofv3 --att --kernel-include-regex '<kernel name regex>' -d capture -- ./app <args> ``` - Only the first dispatch of each matching kernel is traced. To skip warm-up, run the kernel in a loop and add `--kernel-iteration-range N` to trace the Nth dispatch, or `--kernel-iteration-range N-M` for dispatches N to M (the decoder-based reports pool their waves; `stats` lists each dispatch's CSV rows separately). - Use a new directory for each capture, with one kernel in it: `att_mine.py` decodes every `.att` file under a directory together, so several kernels from one run are mixed, and a second run in the same directory stops it, because each run numbers its code objects from 1. Unknown kernel names and the other options (which compute unit, SIMDs, and shader engines to trace, the buffer size, consecutive kernels) are in [capture.md](resources/capture.md#capture). ## What the trace shows, and where to read Each row is something `att_mine.py` reports, its possible causes, and the page that covers them. A capture can match several rows; read each page that applies. | What you see | Possible causes | Read | | --- | --- | --- | | Wait instructions (`s_waitcnt`, or `s_wait_*` on gfx12 and later) hold the time: the wait share of `summary`'s `wave_time`, and the `wait` column of `hotspots` and `lines` | Latency of memory or LDS instructions not covered by other work, or bandwidth (the trace cannot tell which) | [latency.md](resources/latency.md) | | `s_barrier` holds the time (the barrier share of `wave_time`) | Waves reaching a barrier at different times, or many barriers | [synchronization.md](resources/synchronization.md) | | Stall holds the time (`wave_time`, with the class in `stall by class`): VALU, matrix, LDS, or memory instructions, atomics included, or VALU instructions (matrix included) are issuing most of the time | Instructions their pipe did not accept, usually because the unit was busy with earlier instructions | [compute.md](resources/compute.md) | | The idle column dominates the top instructions, or idle between instructions is large in `wave_time` | A source or destination register dependency (waiting for an earlier result), arbiter loss, or instruction cache misses | [stalls.md](resources/stalls.md#idle-cycles) | | Few waves are resident for most of the kernel (`summary`'s `waves_per_simd` against its `max`) | Registers per wave, LDS per workgroup (which limits how many workgroups fit, so few waves in each leaves few resident), or too few workgroups | [latency.md](resources/latency.md#few-waves) | | Issue holds the most time in `wave_time` | The waves spend more time issuing than waiting, stalling, or idle: more instructions than the work needs, or vector issue near its limit (`pipes`) | [compute.md](resources/compute.md#ceiling-check) | | The time before the waves' first instruction holds the most time in `wave_time` (`instructions_cover` well below 100%) | Short or few waves; no instruction accounts for it | [stalls.md](resources/stalls.md#before-the-first-instruction) | The trace shows where the time went in the code that ran, not whether a change will help: a large stall or wait share does not show that the time cannot be reduced or that a unit is at its limit, and a low `busy_share` (the share of time a class was issuing) says nothing about moving the work to other instructions or another algorithm. When the trace leaves a change open, build it and time it rather than ruling it out. ## What ATT shows - For each instruction, summed over the traced waves: how often it ran, its latency, stall, and idle cycles ([stalls.md](resources/stalls.md#per-instruction)), and its source line. - For each wave: when it started and ended, and its instructions in order with their category (VALU, SALU, SMEM, VMEM, FLAT, LDS, and others), from which `summary` splits the waves' time into wait, barrier, other immediate, stall, issue, and idle, plus the time outside any instruction ([stalls.md](resources/stalls.md#wave-time)). - How many waves were resident over time, and the dispatch's VGPR allocation per wave and LDS per workgroup. - An estimate of how many of an instruction's cycles overlapped issue on related pipes (hidden latency; [stalls.md](resources/stalls.md#hidden-cost)). ATT does not show which threads of a wave were active, the addresses instructions accessed, cache or HBM traffic, host-side work such as API calls and launch overhead, or interconnect traffic, and its per-instruction and per-wave detail covers only the traced compute units (the occupancy records can span more). With `--att-consecutive-kernels` it records several dispatches, including the gaps between them on the GPU. Say so when a question needs what the trace lacks. ## Reading a capture A loop that tends to work goes from broad to narrow: the kind of time from the trace, then the lines that hold it, read against the source. Adapt it to the task. 1. **Read the kernel.** Find the loop that does the work and what each iteration loads, computes, and synchronizes on, so that the trace's lines and instructions can be read against it. Leave what limits the kernel to the trace. 2. **Capture and check the capture.** `python3 <skill dir>/scripts/att_mine.py capture summary` (rather than reading the stats CSV, which sums over waves). Decoder warnings (data lost, incomplete waves, incomplete stitching) and unresolved instructions mark parts of the trace whose totals are incomplete; zero waves means nothing ran on the traced compute unit (on RDNA, on the traced SIMD; on gfx9 with `--att-simd-select`, waves of the other SIMDs are still counted, without instructions) ([capture.md](resources/capture.md#troubleshooting)). 3. **Broad: what kind of time it is.** Each command decodes the capture again, which takes longer as the trace grows (time the first one to see what each will cost): - `summary`: `wave_time`, the share of the waves' time in wait instructions, barriers, stall (by class), issue, idle between instructions, and before the first instruction, summed from the instruction records; - `pipes`: issue and stall cycles by instruction class (VALU, matrix, LDS, the memory classes VMEM, FLAT, and SMEM, waits, barriers), the opcodes that stalled most, and the ceiling check ([compute.md](resources/compute.md#ceiling-check)); - `lifetime` and `occupancy`: what grows with wave lifetime, and how many waves were resident. 4. **Narrow: where in the source.** `lines` and `hotspots` rank source lines and instructions by the part of their cost that issue on related pipes on the SIMD (and, on gfx11 and later, memory issue on the other SIMD) did not overlap ([stalls.md](resources/stalls.md#hidden-cost)); `stats` ranks the stats CSV; `barriers` shows which wave, by launch order, waited least at `s_barrier` (often, not always, the one the others wait for), and in what share of the workgroups (estimated). When no line stands out, the cost is spread over many instructions: go back to `pipes`, which shows which classes hold it. When a question is narrower than these reports (one instruction across waves, one loop iteration, the time between a load and its wait, one barrier), query the decoded records with the Python API ([python-api.md](resources/python-api.md)). 5. **Read the page for what you found before changing the kernel** ([What the trace shows, and where to read](#what-the-trace-shows-and-where-to-read)). Each page says what each cause looks like in the trace, changes that address it, and how to confirm them. `stats`, `summary`, `pipes`, `hotspots`, and `lines` end with a `next:` line (a capture with no waves gets none); read each page it names. 6. **Change, verify, and repeat.** Build the change, check its output, and time it without the profiler. Take figures from the tools rather than from memory: the trace has measured where the time goes, `summary` prints the GPU's properties as rocprofv3 recorded them, and timing a change settles what estimates cannot. Capture the new version and run `att_mine.py <old capture> compare <new capture>` (`delta` is new minus old); it compares cost per wave, so when a change alters the work each wave does, compare the timings. The kind of time often changes after a fix, so start the next round at step 3, and time again the changes the earlier trace set aside. 7. **Report.** Keep it short; state each finding once, in the item it belongs to. Leave out the commands you ran and raw cycle and instruction counts. Give the user, in this order: 1. the bottleneck: the kind of time (`wave_time` and the instruction class), with the two or three figures that show it; 2. where: file:line and share, for the lines that hold most of the cost; read each line in the source before describing it; 3. the mechanism: why those lines cost what they do, without repeating item 1, marking what you inferred from the code and the trace did not measure; 4. the change, if you made one, and the kernel's time before and after, measured without the profiler; 5. what the trace could not show and you did not check, and the changes you would try next, marked untested (the trace does not show whether a change will help); setup problems only if they change how to read the result. ## Tips - A high cost on an instruction does not say why: `hotspots` shows how much of it is `stall`, `wait` (wait, barrier, and other immediate instructions), and `idle` cycles, the rest being issue cycles (execution on gfx10 and later), and `summary`'s `wave_time` shows the same split for the whole kernel. - One source line can compile to many instructions, especially a call inlined from a header, such as a math function: `insts` in `lines` counts the distinct instructions the line ran, and the disassembly shows them. Check it before treating a line as one instruction. - Read the disassembly of the captured code object before concluding what an instruction belongs to; `hipcc --offload-device-only -S` also writes it to a `.s` file. - Inlined code carries its call chain: `att_mine.py` prints `kernel.hip:23 (inlined f.h:12 > g.h:5)`, from the call site in the kernel through each inlined call to the line that ran. Name the lines that matter: the cycles are spent on the last one, and the change may belong at any of them. ## Reference | File | Contents | | --- | --- | | [capture.md](resources/capture.md) | Decoder setup, capture options, output files, troubleshooting | | [reading-the-trace.md](resources/reading-the-trace.md) | The stats CSV and every `att_mine.py` command | | [python-api.md](resources/python-api.md) | Using the decoder's Python API: decoding a capture, the calls and what they return, the records' fields, and worked queries | | [stalls.md](resources/stalls.md) | What latency, stall, idle, hidden cycles, and the split of wave time mean | | [latency.md](resources/latency.md) | Waves waiting on earlier memory instructions, and too few waves | | [compute.md](resources/compute.md) | Waves stalled: instructions their pipe did not accept | | [synchronization.md](resources/synchronization.md) | Waves waiting at barriers |
View on GitHub