Profiling and benchmarking

PyTorch · Advanced · 12 min

Optimising without measuring wastes effort; most slow training loops are bottlenecked somewhere unexpected.

Before optimising, find the bottleneck. Common culprits are the data pipeline, CPU-GPU synchronisations and many small kernels, rather than the big matmuls.

`torch.profiler.profile` records operator-level CPU and CUDA activity:

  • activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA] chooses what to trace.
  • record_shapes=True and profile_memory=True add shape and allocation information.
  • Wrap regions with record_function("data") to label them in the trace.
  • prof.key_averages().table(sort_by=...) prints the top operators; prof.export_chrome_trace("trace.json") writes a timeline you can open in Perfetto.

For training loops, pass schedule=torch.profiler.schedule(wait=1, warmup=1, active=3) and call prof.step() each iteration, so you skip start-up noise and keep traces small.

What to look for in a timeline: gaps where the GPU is idle (data loading or Python overhead), many tiny kernels (candidates for torch.compile), and cudaMemcpy or synchronize calls in the middle of steps.

For micro-benchmarks, use `torch.utils.benchmark.Timer`. It handles warm-up and CUDA synchronisation and reports statistics, which naive time.time() measurements get wrong.

Example

import torch
from torch import nn
from torch.profiler import profile, record_function, ProfilerActivity
import torch.utils.benchmark as benchmark

model = nn.Sequential(nn.Linear(1024, 1024), nn.ReLU(), nn.Linear(1024, 10))
x = torch.randn(256, 1024)

activities = [ProfilerActivity.CPU]
if torch.cuda.is_available():
    activities.append(ProfilerActivity.CUDA)

with profile(activities=activities, record_shapes=True) as prof:
    with record_function("forward"):
        model(x)

print(prof.key_averages().table(sort_by="cpu_time_total", row_limit=5))
prof.export_chrome_trace("trace.json")       # open in Perfetto

t = benchmark.Timer(stmt="model(x)", globals={"model": model, "x": x})
print(t.timeit(100))                         # mean time per run, warm-up handled

Remember

  • Profile first: data loading and syncs are frequent bottlenecks
  • Label regions with record_function; view traces in Perfetto
  • Use torch.utils.benchmark for micro-benchmarks

Watch out for

  • Profiling the first iterations, which include warm-up, cuDNN autotuning and compilation.

Check yourself

  1. A trace shows the GPU idle between steps while the CPU is busy in DataLoader code. What is the most likely fix?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pytorch.org/docs/stable/profiler.html