AI Grimoire
Sheet
statusstandard
difficultyintroductory
timeO(1)
memoryO(P)
described2017
revisedtoday

Gradient Accumulation

Activation memory scales with batch size and the optimiser only sees the summed gradient. Split the batch, accumulate, step once — mathematically identical, and only if you are careful about the denominator.

[memory][training][core]Current standard

Standing

Current standardPart of the default recipe. Present in most frontier models trained today, and the thing a new design departs from rather than argues for.

In every training stack. Also the source of a normalisation bug that went unnoticed in several major libraries until late 2024.

judged as of 2026-09 · what the labels mean

Theory

Large batches are wanted for stable gradients and good hardware utilisation. They are limited by activation memory, which scales linearly with batch size.

The gradient of a sum is the sum of gradients, so the batch can be split.

θ1BiBi=1BkiBkθi\nabla_\theta \frac{1}{|\mathcal{B}|}\sum_{i \in \mathcal{B}} \ell_i = \frac{1}{|\mathcal{B}|} \sum_{k} \sum_{i \in \mathcal{B}_k} \nabla_\theta \ell_i
eq. 1 — the identity the technique rests on

Run each micro-batch forward and backward, let the gradients accumulate in .grad, and step after the last one. Activations for one micro-batch at a time, optimiser behaviour of the full batch.

The normalisation bug

Eq. 1 divides by B|\mathcal{B}| — the total number of examples, or for a language model the total number of tokens. The natural implementation divides each micro-batch’s loss by the number of accumulation steps instead.

Those agree only when every micro-batch has the same token count.

1knkkiicorrect1Kk1nkiicommon\underbrace{\frac{1}{\sum_k n_k}\sum_k \sum_i \ell_i}_{\text{correct}} \qquad\ne\qquad \underbrace{\frac{1}{K}\sum_k \frac{1}{n_k}\sum_i \ell_i}_{\text{common}}
eq. 2 — the two denominators, and when they differ

The second is a mean of means. A micro-batch of 100 tokens and one of 1000 contribute equally, so short sequences are weighted ten times too heavily.

This was found in late 2024 to affect several widely used fine-tuning libraries. The symptom was that runs with accumulation reached measurably worse loss than the equivalent large batch — attributed for some time to accumulation being “approximate”, which it is not. It is exact, and the implementations were wrong.

Implementation

python · torch
import torch
from torch import nn


def train_step(model: nn.Module, opt, batches: list, clip: float = 1.0) -> float:
    """One optimiser step over several micro-batches."""
    opt.zero_grad(set_to_none=True)

    # Count the tokens first: the denominator is the total, not the batch count.
    total_tokens = sum(int(b["labels"].ne(-100).sum()) for b in batches)
    running = 0.0

    for i, b in enumerate(batches):
        # Sum reduction, scaled once by the global denominator.
        out = model(**b, reduction="sum")
        loss = out.loss / total_tokens

        # Skip the all-reduce on every micro-batch but the last.
        sync = i == len(batches) - 1
        with model.no_sync() if not sync and hasattr(model, "no_sync") else nullcontext():
            loss.backward()

        running += loss.item()

    # Clip after accumulation, on the complete gradient.
    torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
    opt.step()
    return running

Two details beyond the denominator.

no_sync matters under data parallelism. Without it, DDP all-reduces the gradient after every micro-batch — sixteen collectives per step where one is needed, which on a slow interconnect is most of the step time.

And clipping belongs after the loop. Clipping each micro-batch bounds a partial gradient and changes the direction of the accumulated one, which is the property clipping exists to preserve.

What it cannot fix

Batch normalisation, if any survives in the model, computes statistics per forward pass — so a micro-batch of 4 gives statistics of 4, not of 64. Accumulation is exact for everything else and not for that, which is one more entry on BatchNorm’s list of incompatibilities.

The hardware limit also remains. Micro-batches below about 8 leave the GPU underutilised, so accumulation trades memory for wall-clock beyond that point rather than getting it free.

Micro-batch
4
Accumulation steps
16
Activation memory
1/16
Effective batch, unchanged

Related

References

[1]Goyal et al. — Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour (2017)arXiv:1706.02677
[2]Ott et al. — Scaling Neural Machine Translation (2018)arXiv:1806.00187
[3]Unsloth — Bugs in LLM Training: Gradient Accumulation Fix (2024)unsloth.ai