Performance Profiling on AMD GPUs - Part 7: AI/PyTorch® Edition AMD published Part 7 of its ROCm profiling guide series, an AI/PyTorch edition that applies the same profiling loop used on HIP and Fortran applications to a PyTorch training workload on AMD GPUs. The guide instructs readers to separate one-time warm-up costs — HIP context setup, rocBLAS/MIOpen initialization and Triton just-in-time compilation — from steady-state steps, then annotate timelines with ROCTx markers via torch.cuda.nvtx, trim them with rocprof-sys --selected-regions, summarize kernels with rocprofv3 and collect hardware counters with rocprof-compute using iteration multiplexing. It evaluates three optimizations — PyTorch-level fusion, torch.compile and custom Triton kernels — and teaches readers to distinguish a real win (fusion) from a change that only pays off under the compiler (Triton) by comparing kernel counts, timelines and throughput. Performance Profiling on AMD GPUs - Part 7: AI/PyTorch® Edition performance-profiling-on-amd-gpus-part-7-ai-pytorch-edition This blog, like the previous articles in the profiling guide series Part 1 https://rocm.blogs.amd.com/software-tools-optimization/profiling-guide/intro/README.html , Part 2 https://rocm.blogs.amd.com/software-tools-optimization/profiling-guide/novice/README.html , Part 3 https://rocm.blogs.amd.com/software-tools-optimization/profiling-guide/advanced/README.html , Part 4 https://rocm.blogs.amd.com/software-tools-optimization/profiling-guide/fortran openmp/README.html , Part 5 https://rocm.blogs.amd.com/software-tools-optimization/profiling-guide/ai-assist-optimization/README.html , and Part 6 https://rocm.blogs.amd.com/software-tools-optimization/profiling-guide/att/README.html , applies a systematic profiling workflow to a GPU-accelerated application. Those posts establish the core loop on HIP and Fortran applications: baseline timing, kernel hotspots with rocprofv3 , rooflines and hardware counters with rocprof-compute , and whole-system timelines with rocprof-sys . Here we apply that loop to a PyTorch training workload, where one logical operator can lower to many GPU kernels and optimization often means changing how the framework dispatches work rather than rewriting a kernel. AI vs HPC note: The profiling loop is unchanged, but one distinction from HPC drives everything that follows. An HPC application is compiled ahead of execution, a PyTorch workload compiles during it. With torch.compile , kernels are traced and generated on the first iteration, and Triton kernels are just-in-time JIT compiled and auto-tuned the first time each tensor shape is seen; even in eager mode the first step pays one-time warm-up costs HIP context, rocBLAS/MIOpen init, Triton JIT . Separate warm-up from measurement and read timing and counters from steady-state steps only; otherwise the profile reflects the compiler, not the model. 1. What you already know : - You run GPU-accelerated PyTorch code training or stepping a model forward and can match CLI knobs such as batch size, sequence length, and step count across runs. - You understand that one logical operator for example, attention or a layer norm can map to many GPU kernels, and that fusion or custom kernels change kernel counts and kernel names. - You recognize that a training step has both a forward and a backward pass, and that the backward pass is often the larger of the two. - Optional: you have compared runtime across hardware vendors and want AMD-specific traces and counters to explain the gap. 2. What this guide will teach you : - Annotate from Python with ROCTx markers via torch.cuda.nvtx the namespace is CUDA-named, but torch.cuda is PyTorch’s device API on a ROCm™ build as well, and ROCTx mirrors the NVTX range calls, so the same annotations serve either runtime , then trim the timeline with --selected-regions so it maps onto the model and stays readable. - Analyze hardware counters with rocprof-compute , using iteration multiplexing to collect the full counter set in a single pass. - Evaluate three optimizations PyTorch-level fusion, torch.compile , and custom Triton kernels and learn to tell a real win fusion from a change that only pays off under the compiler Triton by comparing kernel counts, timelines, and throughput. Profiling Basics profiling-basics The workflow set out below is the one we apply to optimize ROCm-based PyTorch workloads. We establish a baseline, capture a whole-run system trace with rocprof-sys-run , annotate it with ROCTx and narrow it with rocprof-sys --selected-regions , summarize kernels with rocprofv3 , and analyze bottlenecks with rocprof-compute using its iteration-multiplexing option. Each optimization is then measured against that baseline using the same set of tools. The rest of the post walks through this loop on the TinyOpenFold example. The TinyOpenFold Example the-tinyopenfold-example OpenFold https://github.com/aqlaboratory/openfold is a trainable reproduction of AlphaFold 2. Its performance-critical core is the Evoformer , a stack of blocks that each refine two tensors: a multiple sequence alignment MSA representation B × N seq × N res × d msa and a pair representation B × N res × N res × d pair . Each block mixes the two, then refines the pair tensor with the triangle operations that give the pattern its name, a triangular multiplicative update and a triangular self-attention, both of which update every residue pair i, j by routing information through a third residue k. TinyOpenFold shrinks this stack fewer blocks, smaller channel widths, synthetic batches while keeping that pair/triangle pattern intact, so training runs are short. The defaults are d msa = 64, d pair = 128, and n blocks = 4, which together give approximately 2.64M parameters. For the full block-by-block breakdown, see the upstream ARCHITECTURE.md https://github.com/amd/HPCTrainingExamples/blob/main/MLExamples/TinyOpenFold/ARCHITECTURE.md . For profiling, two properties of the pair tensor matter most: it is quadratic in sequence length, so it drives O N res² activation memory, and the triangle stages that update it multiplication and self-attention drive O N res³ compute, N res² pairs each reduced over all N res choices of k. Those triangle stages are the dominant compute at moderate N res and the first place to look in any trace. In addition to that intrinsic cost, the readable baseline pays for many small kernel launches and tail elementwise kernels, the launch-bound fragmentation that fusion version 2 removes. The profiler separates the math-bound triangles from that launch-bound overhead. TinyOpenFold ships three implementations of the same model. When comparing performance between implementations, use the same training knob values. | Stage | Directory | Entry point | Role | |---|---|---|---| | Version 1 | version1 pytorch baseline/ | tiny openfold v1.py | Baseline : clean, readable PyTorch. Many kernels per step, the reference for hotspots and operator-to-kernel mapping. | | Version 2 | version2 pytorch fused/ | tiny openfold v2.py | PyTorch fusion : query, key, and value QKV and triangle projections fused; fewer dispatches, lower peak memory. | | Version 3 | version3 triton/ | tiny openfold v3.py | Triton kernels : custom fused LayerNorm plus fused scaled dot-product attention SDPA and QKV fusion, wrapped in autograd functions. | Getting the Code getting-the-code Clone the canonical repository and enter the example directory: git clone https://github.com/amd/HPCTrainingExamples.git cd HPCTrainingExamples/MLExamples/TinyOpenFold The project README https://github.com/amd/HPCTrainingExamples/blob/main/MLExamples/TinyOpenFold/README.md documents the quick start and scaling helpers, and ARCHITECTURE.md https://github.com/amd/HPCTrainingExamples/blob/main/MLExamples/TinyOpenFold/ARCHITECTURE.md walks through the parameter math block by block. Environment Setup ROCm + PyTorch environment-setup-rocm-pytorch We validated this post on ROCm 7.13 with PyTorch 2.11 and Triton 3.6 on an AMD Instinct™ MI300X. There are two ways to obtain this stack, depending on whether the ROCm runtime comes from the Python wheels or from a module the cluster already provides. Either path supports every command in the rest of the post. - Pip channel used in this post . Install PyTorch and Triton pre-packaged for ROCm 7.13 from AMD’s TheRock https://github.com/ROCm/TheRock multi-arch pip channel. The wheels bundle a self-contained ROCm runtime as a dependency of torch , so no /opt/rocm and no ROCm module are needed for training. - System module. Load a rocm/7.13 module, if available, from the cluster and install the matching PyTorch, Triton, and other Python dependencies into a virtual environment against it. We use the pip channel throughout for reproducibility on any gfx942 MI300X / gfx950 MI355X node: We used Python 3.12 for the blog. The rocpd trace tools rocpd, rocpd2pftrace, rocpd2otf2 support Python 3.10-3.13. module load python/3.12 cd HPCTrainingExamples/MLExamples/TinyOpenFold python3 -m venv venv713 source venv713/bin/activate pip3 install --upgrade pip INDEX URL=https://rocm.nightlies.amd.com/whl-multi-arch/ PyTorch 2.11 + Triton 3.6 for ROCm 7.13 torch pulls the ROCm libraries in automatically . pip3 install --index-url $INDEX URL torch torchvision torchaudio triton ROCm SDK including the hardware profilers rocprofv3, rocprof-compute, rocprof-sys . The profiler extra adds them; pick the device arch s you run on gfx942 = MI300X, gfx950 = MI355X . pip3 install --index-url $INDEX URL "rocm libraries,devel,profiler,device-gfx942 ==7.13. " otf2: Python dep for the rocpd OTF2 exporters rocpd, rocpd2otf2 ; Requires Python <=3.13. pip3 install otf2 Put the pip-installed ROCm binaries on PATH for this shell. export ROCM PATH=$ rocm-sdk path --root export PATH=$ROCM PATH/bin:$PATH export LD LIBRARY PATH=$ROCM PATH/lib:$LD LIBRARY PATH Confirm PyTorch sees the GPU: python python3 -c "import torch; print torch. version , torch.cuda.is available , torch.cuda.get device name 0 " Then validate the example itself from the baseline directory: cd version1 pytorch baseline python3 tiny openfold v1.py --validate-setup Because this post focuses exclusively on single-GPU profiling, we pin a device with ROCR VISIBLE DEVICES=0 or --device 0 so that timing and counter collection remain deterministic. Step 1 — Establish a Baseline Version 1 step-1-establish-a-baseline-version-1 All of the commands in this section are run from inside the version1 pytorch baseline/ directory. Throughout the post we use the small problem 64 residues, 16 MSA sequences, batch size 4 because it turns the loop over in a few seconds. A large problem 128 residues, 32 MSA sequences, batch size 4 is used later for comparison. Application Timing application-timing We begin by measuring throughput with no profiler attached, which establishes the number against which every later change is compared. python3 tiny openfold v1.py --batch-size 4 --seq-len 64 --num-seqs 16 --num-blocks 4 --num-steps 30 --device 0 For the small problem, the baseline reports roughly 99 samples/sec, or about 40 ms per batch \ 1\ id3 . Record the corresponding number for the target hardware and compare against runs at the same configuration. An Orthogonal Axis: torch.compile an-orthogonal-axis-torch-compile The cheapest optimization available, before restructuring the model, is to ask PyTorch to compile it. All three versions accept --enable-torch-compile , which wraps the model in torch.compile https://docs.pytorch.org/docs/stable/generated/torch.compile.html ; its inductor backend traces the model and generates fused Triton kernels automatically. Note that torch.compile is not one of the three numbered steps in this post. Because it requires no code change and applies to every version, it constitutes an orthogonal axis that stacks on the baseline Step 1 , the manual fusion Step 2 , and the custom kernels Step 3 alike. We introduce it here and report its effect alongside every version in the final comparison. python3 tiny openfold v1.py --batch-size 4 --seq-len 64 --num-seqs 16 --num-blocks 4 --num-steps 30 --device 0 \ --enable-torch-compile This is the point at which the AI-vs-HPC difference described above becomes concrete. The first step is far slower than the rest, because that is when compilation and autotuning happen; the per-step time then drops and holds at a new steady state. In the printed per-step timing, step 0 pays the compile cost, and the later steps settle to roughly 174 samples/sec ~23 ms per batch , about a 1.75× speedup over eager for a one-line change. This behavior is the reason warm-up must be separated from measurement, since averaging the compile step into the timing would understate the gain. A more aggressive setting, torch.compile mode="max-autotune" , benchmarks several kernel implementations and can enable CUDA graphs to cut launch overhead further; we leave that as an exercise for the reader see the ROCm torch.compile blog https://rocm.blogs.amd.com/artificial-intelligence/torch compile/README.html how-torch-compile-works . Because torch.compile does not reveal where time is spent, the remaining analysis runs in eager mode with the profilers. Capture a Baseline System Trace rocprof-sys capture-a-baseline-system-trace-rocprof-sys The first look at a new workload should be a whole-run system trace which is a single timeline that shows Python and host threads, the HIP runtime, and GPU activity together. Capture it with rocprof-sys-run using the workload-trace preset, the AI/ML preset for training and long-running GPU workloads. It enables the ROCm domains HIP, kernel, marker, and memory , sizes the Perfetto buffer for a long run, and routes through rocPD to capture child-thread activity. It emits a Perfetto .proto timeline alongside a rocPD .db : ROCR VISIBLE DEVICES=0 rocprof-sys-run --preset=workload-trace -o rsys v1 workload -- \ python3 tiny openfold v1.py --batch-size 4 --seq-len 64 --num-seqs 16 --num-blocks 4 --num-steps 10 --device 0 The resulting rsys v1 workload/