Skip to content
mlmentorship

Gradient accumulation

Run several forward-backward passes before each optimizer step to simulate a larger effective batch size without the memory cost.

Published · 5 min read ·Role-specific ·Advanced

Visual quick review

Visual first · depth when needed

Distinguish the state that changes after every micro-batch (the gradient buffer) from the parameters that change only once after K micro-batches.

Preparing the visual…

Summary

Gradient accumulation runs forward-backward passes on micro-batches, summing (or averaging) the gradients across those passes, and then performs a single optimizer step. The effective batch size is the per-pass batch size, with no extra activation memory.

Many training recipes prescribe a specific effective batch size (e.g., 1024 sequences) for stable convergence. If your GPU can only hold 32 sequences at a time, you have two choices: spread the batch across 32 GPUs, or accumulate gradients over 32 steps on one GPU.

Gradient accumulation is the cheap option. It is the standard way to (a) match published training recipes on smaller hardware and (b) increase effective batch size in fine-tuning loops where multi-GPU is not available.

The mechanism

Replace this loop:

for batch in dataloader:
    loss = model(batch)
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

With:

for step, batch in enumerate(dataloader):
    loss = model(batch) / accumulation_steps   # average over K micro-batches
    loss.backward()                             # gradients accumulate in .grad
    if (step + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

Key points:

  • loss.backward() adds to existing .grad (does not overwrite). Calling it times accumulates.
  • Divide the loss by so the accumulated gradient equals the average over the effective batch (matches a single big batch’s gradient).
  • Optimizer step happens once per micro-batches.

One accumulation window · K = 4

The gradient buffer changes four times; the parameters change once.

Gradient and parameter state across four accumulated micro-batches Four numbered micro-batches run from left to right using the same parameter value theta t. Each backward pass adds one quarter of its gradient to the gradient buffer, which progresses from g1 over 4 to the sum of g1 through g4 over 4. A vertical boundary follows the fourth pass. Only then does optimizer step change the parameters from theta t to theta t plus 1, after which zero grad clears the buffer. FORWARD + BACKWARD ON FOUR EQUAL-SIZED MICRO-BATCHES 1 · micro-batch B1loss / 4, then backward 2 · micro-batch B2loss / 4, then backward 3 · micro-batch B3loss / 4, then backward 4 · micro-batch B4loss / 4, then backward .grad after backwardg1 / 4 .grad after backward(g1 + g2) / 4 .grad after backward(g1 + g2 + g3) / 4 complete average gradient(g1 + g2 + g3 + g4) / 4 BOUNDARY optimizer.step()consume average once PARAMETER STATE SEEN BY EACH FORWARD PASS parameters θt parameters θt parameters θt parameters θt parameters θt+1 no optimizer step · parameters stay fixed zero_grad() starts the window zero_grad() clears .grad for the next window effective batch = 4 micro-batches · parameter updates = 1
Read it this way: scan the two rows together. Each numbered backward call adds one normalized contribution to .grad, but all four forward passes still use θt. Only the completed average crosses the dashed boundary into optimizer.step(), producing θt+1 once; zero_grad() then starts a new window. This original schematic assumes four equal-sized micro-batches.

Cost model

  • Activation memory: same as one micro-batch (each is forward-backward’d independently).
  • Optimizer memory: unchanged (no extra optimizer state).
  • Wall clock: an optimizer step runs forward-backward micro-batches. Per-sample throughput may stay similar, improve through less frequent communication, or fall because smaller matrix shapes use the accelerator poorly. Measure it.
  • Convergence: nearly equivalent to a true large-batch step, modulo BatchNorm (see pitfalls).

Combined with other techniques

  • DDP / FSDP: gradient accumulation reduces the frequency of inter-GPU communication. With GPUs and accumulation steps, only one all-reduce per micro-batches → faster throughput. PyTorch’s model.no_sync() skips the all-reduce on intermediate steps.
  • Mixed precision: works identically; the loss scaler handles accumulated gradients.
  • Activation checkpointing: orthogonal; combine for maximum effective batch size on minimum memory.

Common pitfalls

  • Forgetting to divide by . Without normalization, the gradient magnitude is × larger than expected → effectively a × larger LR.
  • Calling optimizer.step() every micro-batch. Defeats the purpose.
  • Mixing with BatchNorm. BN computes statistics within a single forward pass. With accumulation, BN sees only the micro-batch. Statistics are noisier than at the effective batch size. Use LayerNorm or GroupNorm instead, or sync BN across micro-batches.
  • Using accumulation as a replacement for distributed training when bandwidth is available. Multi-GPU with proper sharding is faster and more memory-efficient than serial accumulation.