AI Knowledge Tree

Compilation (torch.compile, XLA, Triton)

Trace, fuse, and lower DL graphs through torch.compile, XLA, and custom Triton kernels for speed.

⏱ 22 min

A naive PyTorch training loop runs each operation as a separate kernel launch, then reads / writes intermediate tensors to GPU memory between them. Compilation fuses sequences of operations into single kernels, removing the launch overhead and the intermediate memory traffic. The result is 1.3-2x speedups on most workloads, and sometimes much more on memory-bound operations.

Why compilation helps

Three sources of inefficiency in eager-mode execution:

  • Kernel launch overhead: each op launches a CUDA kernel with some fixed cost. Small ops are dominated by launch overhead.
  • Memory traffic: each op writes its output to HBM (slow GPU memory); the next op reads it back. Memory bandwidth, not compute, is the bottleneck for many ops.
  • No global optimization: each op is independently optimal but the pipeline isn't.

A compiler can:

  • Fuse ops: combine multiple element-wise operations into one kernel.
  • Layout optimization: choose memory layouts and tile sizes.
  • Algebraic simplification: eliminate redundant computations.
  • Operator selection: pick the best implementation (e.g., different matmul kernels for different shapes).

torch.compile

PyTorch 2.0's compiler[1].

model = MyModel()
compiled = torch.compile(model)
output = compiled(input)              # first call: trace + compile
output = compiled(input)              # subsequent calls: fast

Under the hood:

  1. TorchDynamo: a Python frame evaluator that traces the model into a graph by intercepting Python bytecode. Handles dynamic control flow (if/else, while) by recompiling for different paths.
  2. AOTAutograd: produces both the forward and backward graphs.
  3. Inductor: the default backend. Lowers the graph to Triton (for GPU) or C++ (for CPU), generates kernels.

First call has a compilation overhead (seconds to minutes); subsequent calls reuse the compiled kernels. For training, the overhead is paid once.

Compilation is opt-in in PyTorch; the eager mode is still default.

Triton

OpenAI's GPU programming language. A Python DSL for writing high-performance CUDA-like kernels with much less code than CUDA C++.

Triton kernels are the building blocks of:

  • torch.compile's Inductor backend (most generated kernels are Triton).
  • FlashAttention (next lesson).
  • Custom kernels for specific architectures (Mamba, mixture-of-experts routing, etc.).

A typical Triton kernel is 50-100 lines of Python with explicit memory tiling, block sizing, and program-id-based work distribution.

XLA

Google's compiler for ML, used by JAX, TensorFlow, and PyTorch (via torch_xla).

Two compilation modes:

  • JIT (just-in-time, in JAX via jax.jit): compile a Python function the first time it's called with a particular input shape.
  • AOT (ahead-of-time): compile to a serialized binary for deployment.

XLA's compilation passes are similar in spirit to torch.compile but have decades more maturity on the TPU side. XLA-compiled code typically achieves higher performance on TPUs and competitive performance on GPUs.

For JAX, compilation is free and ubiquitous: just put @jit on your function. The integration is much tighter than torch.compile.

CUDA Graphs

A complementary technique: CUDA Graphs record a fixed sequence of GPU operations once, then replay the recorded graph for subsequent invocations. Skips kernel launch overhead even without compilation.

Useful when the model has a fixed graph structure (e.g., a Transformer block with fixed shapes). Combined with torch.compile or used standalone.

When compilation helps and doesn't

Helps a lot:

  • Many small element-wise ops in sequence (RMSNorm + SiLU + multiply in a SwiGLU FFN).
  • Custom kernels for fused attention.
  • Repeated forward passes with the same shape.

Doesn't help much:

  • Single big matmul; the cuBLAS kernel is already optimized.
  • Eager-mode debugging (you want to see each op).
  • Highly dynamic models with constantly changing shapes (recompilation storm).

For most production training, compilation gives a 30-50% speedup with minimal code changes. The savings compound over thousands of GPU-hours.

What compilation isn't

  • Not a way to fit a larger model in memory (use mixed precision + FSDP + checkpointing).
  • Not a way to make a bad architecture good (it can't change the algorithm).
  • Not free debugging (compiled code is harder to step through).

Practical recipe

For a typical PyTorch training pipeline:

  1. Get correct eager-mode training first.
  2. Wrap the forward call in torch.compile(model)[1].
  3. Profile and decide if specific layers need custom Triton kernels.

For JAX, compilation is the default; eager-mode is rare.

Exercises

Exercise 1. A SwiGLU feed-forward block runs five small element-wise ops in sequence (RMSNorm, two projections' activations, a SiLU, and a multiply). Each op in eager mode costs 5 microseconds of kernel-launch overhead and reads plus writes a 40 MB activation tensor to HBM at 3 TB/s. Estimate the per-op time eager mode spends on launch plus memory traffic, then state what fusing all five into one kernel removes.

Solution

Step 1. Memory traffic per op: read 40 MB and write 40 MB is 80 MB. At 3 TB/s, 80×106/3×1012≈26.780 \times 10^6 / 3 \times 10^{12} \approx 26.7 microseconds.

Step 2. Per-op total is launch plus traffic: 5+26.7≈31.75 + 26.7 \approx 31.7 microseconds. Across five ops, eager mode spends 5×31.7≈1585 \times 31.7 \approx 158 microseconds, of which 5×5=255 \times 5 = 25 microseconds is launch overhead and 5×26.7≈1335 \times 26.7 \approx 133 microseconds is HBM round-trips of intermediates.

Step 3. Fusing the five ops into one kernel pays one launch (55 microseconds) and reads the input plus writes the final output once (8080 MB, ≈26.7\approx 26.7 microseconds), keeping the four intermediate tensors in registers/SRAM. It removes four launches and eight intermediate HBM transfers, turning ≈158\approx 158 microseconds into ≈32\approx 32 microseconds, the kind of memory-bound speedup compilation targets.

Exercise 2. torch.compile pays a one-time tracing/compilation cost the first time a shape is seen, then reuses the compiled kernel. A training run does 50,000 steps at a fixed shape. Compilation costs 90 seconds once; each compiled step saves 8 ms over eager. Find the break-even step count and the net time saved over the run.

Solution

Step 1. Break-even is when accumulated per-step savings equal the compilation cost: 90 s/0.008 s=11,25090 \text{ s} / 0.008 \text{ s} = 11{,}250 steps. After that, compilation is net positive.

Step 2. Over 50,000 steps the savings are 50,000×0.008=40050{,}000 \times 0.008 = 400 seconds.

Step 3. Net time saved is 400−90=310400 - 90 = 310 seconds, about 5.2 minutes. The fixed compile cost is amortized roughly five times over, which is why compilation pays off for long fixed-shape training but not for a handful of calls.

Exercise 3. A model has highly dynamic shapes: the sequence length changes nearly every step, and torch.compile recompiles whenever it sees a new shape (a "recompilation storm"). The same model also has one fixed-shape sub-block (a per-token MLP head). Argue where compilation helps and where it hurts, and propose a layout that captures the win without the storm.

Solution

Step 1. The dynamic part is the problem: each new sequence length triggers a fresh trace plus compile, and the compile cost is never amortized because the shape rarely repeats. Net effect is slower than eager, plus compile-cache churn.

Step 2. The fixed-shape MLP head is the opposite: its shape is stable, so one compilation is reused across all steps and the fused element-wise ops give a steady speedup.

Step 3. Compile selectively: wrap only the fixed-shape sub-block in torch.compile and leave the variable-length attention path in eager mode (or use dynamic-shape compilation with bucketed shapes so a small set of shapes is compiled once each). This captures the per-token MLP fusion win while avoiding recompilation for every new sequence length.

The next lesson covers the canonical example of a custom kernel that beat the default implementation by an order of magnitude: FlashAttention.


References

  1. [1]Paszke, A., et al. (2019). "PyTorch: an imperative style, high-performance deep learning library". NeurIPS. arXiv:1912.01703. The eager-mode framework that torch.compile traces, fuses, and lowers.↩