Gradient Accumulation and Effective Batch Size
When a GPU can't fit the batch size a training recipe calls for, gradient accumulation simulates a larger batch by summing gradients from several smaller mini-batches before taking a single optimizer step — the same update, spread over more, smaller forward passes.
Prerequisites: Stochastic Gradient Descent
A training recipe might specify a batch size of 512 for good reasons — it worked in the original paper, it gives a favorable ratio of gradient noise to signal, it matches the learning rate schedule the recipe was tuned for. But a single GPU's memory might only hold 64 examples at once for that model. Simply switching to batch size 64 changes the actual optimization dynamics, not just a technical detail — smaller batches produce noisier gradient estimates and often need a different learning rate entirely. Gradient accumulation reconciles the two: it processes the 512 examples as eight passes of 64, but only updates the weights once, after summing all eight gradients — numerically equivalent to having run the full batch of 512 at once.
The analogy: paying a bill in installments that still add up to the full amount
A large purchase split across eight monthly installments is still, in total, exactly the same purchase — nothing about the final amount owed changes, only the number of separate transactions used to get there. Gradient accumulation does the same thing to a gradient computation: eight "installments" of 64 examples each, summed together, produce exactly the gradient a single pass of 512 examples would have produced — the model only "pays" (updates its weights) once, after all eight installments are in.
The mechanics
For accumulation steps, each processing a micro-batch of size , the accumulated gradient before the optimizer step is:
In words: run the forward and backward pass on each micro-batch as usual, computing its gradient , but instead of immediately calling the optimizer, add into a running total. Only after all micro-batches have contributed does the optimizer take a single step using the summed gradient — which (for a loss that averages over the batch, as is standard) is mathematically the same update a real batch of size would have produced, once the accumulated gradient is divided by to restore the average.
Worked example 1: matching a target effective batch size
Target effective batch size 512, GPU fits 64 examples at a time: accumulation steps. Each of the 8 micro-batches computes its own gradient via a normal forward and backward pass; none of them individually triggers an optimizer step. After the 8th, the accumulated (and averaged) gradient is used for one optimizer.step() call, and the accumulator is reset to zero for the next group of 8.
Worked example 2: why forgetting to zero the accumulator is a classic bug
Suppose gradients accumulate correctly for steps giving , and the optimizer step uses as intended — correct. But if optimizer.zero_grad() is called only once at the start of training instead of after every step, the next group of 4 micro-batches adds its gradients on top of the stale still in the buffer, say another , giving : an effective gradient of instead of the intended — more than double the correct update size, entirely from a bookkeeping error. Every implementation must zero the gradient buffer immediately after each optimizer step, not just once at the start.
Gradient accumulation sums gradients from smaller micro-batches before taking one optimizer step, reproducing the mathematics of a true batch of size while only ever holding one micro-batch in memory at a time — it trades wall-clock time (more forward/backward passes) for memory it doesn't have.
What this means in practice
Gradient accumulation is standard whenever a model or sequence length is too large to fit a target batch size on available hardware — common when fine-tuning large language models on a single GPU. It is not free: it increases wall-clock time roughly proportional to , since the passes are still done sequentially even though the optimizer step happens once; it also interacts with batch normalization, whose running statistics are computed per micro-batch, so very small micro-batches can leave batch norm statistics noisier than the effective batch size alone suggests.
The common confusion is believing gradient accumulation makes a small-memory GPU behave identically in every respect to a GPU with enough memory for the full batch, including speed. It reproduces the optimization update exactly, but not the wall-clock cost — sequential micro-batch passes generally take about times as long as one large batch pass would have, so accumulation buys the statistical benefits of a large effective batch size, not the throughput benefits.
Practice in interviews
Further reading
- Ott et al., Scaling Neural Machine Translation (2018)