Skip to main content
Memory-Efficient Training

Memory-Efficient Training

Understanding GPU Memory

GPU memory during training is like a hotel with a fixed number of rooms: your model parameters check in, then their gradients need rooms, then the optimizer’s bookkeeping (momentum, variance) needs even more rooms, and finally every intermediate computation (activation) holds a room open until the backward pass checks it out. When the hotel is full, training crashes with the dreaded CUDA out of memory error. Understanding who occupies which rooms — and for how long — is the key to training larger models on the hardware you actually have. Where does memory go during training? For a 7B parameter model:
  • Parameters: 28 GB
  • Gradients: 28 GB
  • Adam states: 56 GB
  • Total: ~112 GB (without activations!)
The single most impactful optimization for most teams is mixed precision training (FP16/BF16). It nearly halves memory for parameters and activations with minimal accuracy impact, and it is a one-line change in most frameworks. Start there before reaching for more complex techniques like gradient checkpointing or model parallelism.

Memory Profiling


Gradient Checkpointing

During the forward pass, PyTorch saves every intermediate activation because it needs them during the backward pass to compute gradients. For a 24-layer transformer, this means storing 24 layers worth of intermediate tensors simultaneously. Gradient checkpointing (also called activation checkpointing or rematerialization) offers a simple trade: do not save intermediate activations — instead, recompute them on-the-fly during the backward pass. You save memory at the cost of running parts of the forward pass twice. The math is elegant: for nn layers, standard training stores O(n)O(n) activations. With checkpointing every n\sqrt{n} layers, you store only O(n)O(\sqrt{n}) activations and recompute the rest, at the cost of about 33% extra compute. For a 24-layer model, that is storing 5 checkpoints instead of 24 activation sets — a nearly 5x memory reduction for activations.

Mixed Precision Training

Mixed precision training is the single highest-impact memory optimization for most practitioners. The idea: use 16-bit floating point (FP16 or BF16) for the computationally intensive operations (matrix multiplies, convolutions) while keeping 32-bit precision where it matters (loss computation, gradient accumulation, optimizer states). Modern GPUs have dedicated hardware (Tensor Cores) that run FP16 matrix multiplies at 2-8x the speed of FP32, so you get both memory and speed benefits. The tricky part is avoiding numerical issues. FP16 has a very narrow dynamic range — gradients can underflow to zero (too small to represent) or overflow to infinity. The solution is gradient scaling: multiply the loss by a large number before backward, then divide the gradients by the same number before the optimizer step. This shifts the gradient values into FP16’s representable range. BFloat16 (BF16) avoids this entirely because it has the same exponent range as FP32, at the cost of less precision in the mantissa.
Do not use FP16 mixed precision for fine-tuning large language models. BF16 is strongly preferred because FP16’s limited range causes gradient underflow on very small learning rates (1e-5 range) that are typical for LLM fine-tuning. If your GPU supports BF16 (Ampere or newer), always prefer it.

CPU Offloading

CPU offloading exploits the fact that your machine typically has 10-50x more CPU RAM than GPU memory. The idea is simple: keep only what the GPU needs right now on the GPU, and store everything else on the CPU (or even NVMe). This is the same principle behind ZeRO-Offload in DeepSpeed and FSDP’s CPU offloading in PyTorch. The trade-off is straightforward: you trade training speed (because of PCIe data transfers) for the ability to train models that otherwise would not fit at all.
CPU offloading is most effective for optimizer states (which are only needed during the parameter update step, not during forward/backward). Offloading activations is less effective because they are accessed frequently during the backward pass, making the PCIe bandwidth a bottleneck. If you are using DeepSpeed, start with ZeRO Stage 2 + CPU offloading of optimizer states before trying full activation offloading.

Activation Recomputation Strategies


Memory-Efficient Attention

Attention is often the single largest memory consumer in transformer models. Standard self-attention materializes an N×NN \times N attention matrix, where NN is the sequence length. For a 4096-token sequence with 32 attention heads in float16, that single matrix consumes 32×4096×4096×232 \times 4096 \times 4096 \times 2 bytes = 1 GB. Double the sequence length and the memory quadruples. The methods below tackle this bottleneck from different angles.
If you are using PyTorch 2.0+ or later, use torch.nn.functional.scaled_dot_product_attention with is_causal=True or attn_mask. PyTorch will automatically select the most efficient backend (FlashAttention, memory-efficient attention, or math fallback) based on your hardware, input shapes, and whether you need dropout. This is almost always the right choice — only implement custom attention when you need something the fused kernel does not support.

Efficient Batch Processing


Memory-Efficient Data Loading


Memory Debugging Tools


Exercises

Use the MemoryProfiler to analyze a model’s memory usage at each layer. Identify the biggest memory consumers and optimize them.
Create a system that automatically decides which layers to checkpoint based on their memory usage vs. compute cost.
Implement training that stays within a fixed memory budget by dynamically adjusting batch size and checkpointing.

What’s Next?

Quantization Deep Dive

Post-training and quantization-aware training

Knowledge Distillation

Transfer knowledge to smaller models