MLSYS ENGINEERING

10.5. Activation checkpointing

As we saw in autodiff, the backward pass needs the saved activations from the forward pass. These intermediate activations must stay in GPU memory, taking up substantial space and frequently causing out-of-memory errors for deep models. Storing and reloading them from GPU memory also generates heavy memory traffic, especially for memory-bound elementwise operations.

To reduce memory consumption, we can trade compute for memory using activation checkpointing. Instead of saving intermediate activations at every op, we only keep them at selected points along the chain, as shown below.

Activation Forward op x0 x1 x2 x3 op1 op2 op3 Saved activations x0 x2
Figure 51. Checkpointing selected activations during the forward pass.

This reduces memory consumption during the forward pass and decreases memory traffic when loading saved activations during the backward pass.

However, when the backward pass reaches an op whose input activation was not saved, such as op2 needing x1, the framework must recompute it on the fly. It does this by running op1 forward again starting from the nearest saved checkpoint x0, as shown below.

Activation Gradient Forward op Backward op Saved activations x0 x2 op1 x1 x0 x1 x2 x3 op1 op2 op3
Figure 52. Recomputing activations during the backward pass.

Activation checkpointing is essentially trading compute for more memory space: rerunning forward ops consumes additional compute cycles, but in return, it frees up substantial GPU memory.

So it is not suitable for compute-bound operations, since adding extra compute directly worsens the bottleneck and slows down training. Fortunately, modern frameworks handle this selection properly, choosing which operations to save and which to recompute to minimize memory usage without significantly hurting performance.1

Throughout this chapter, we have seen how frameworks translate high-level Python code into efficient execution on hardware, chaining forward and backward passes together and managing activation memory through checkpointing. So far, however, we have assumed that our models and their computation graphs fit comfortably on a single GPU. As modern models scale to tens or hundreds of billions of parameters, their weights, activations, and optimizer states rapidly outgrow what any individual GPU can hold. In the next chapter, we scale our systems outward, exploring how clusters of GPUs connect and how to distribute both computation and memory across them.


1. Modern compilers and frameworks can automate this selection across arbitrary computation graphs using graph partitioning algorithms such as min-cut, weighting the cut to avoid recomputing compute-heavy operations while minimizing saved memory.