Large models and high-resolution inputs can exhaust accelerator memory before a preferred batch size fits. PyTorch can retain gradients across smaller microbatches so the optimizer still updates from the combined effective batch.
Each call to loss.backward() adds into parameter .grad buffers. Dividing each mean loss by accumulation_steps keeps the accumulated gradient on the same scale as one loss over the effective batch when every accumulation window contains equally sized microbatches.
A deterministic CPU run uses four microbatches and performs optimizer.step() after every pair. A second model trains on two full batches of four samples, so matching parameters, changed weights, and cleared gradients expose both the update and its accumulation boundary.
Related: How to run a training loop in PyTorch
Related: How to zero gradients in PyTorch
import copy import torch from torch import nn from torch.utils.data import DataLoader, TensorDataset torch.manual_seed(7) features = torch.linspace(-1.5, 1.5, steps=24, dtype=torch.float32).reshape(8, 3) targets = features @ torch.tensor([[0.7], [-0.2], [0.4]]) + 0.15 microbatch_size = 2 accumulation_steps = 2 effective_batch_size = microbatch_size * accumulation_steps train_loader = DataLoader( TensorDataset(features, targets), batch_size=microbatch_size, shuffle=False, ) model = nn.Linear(3, 1) reference_model = copy.deepcopy(model) initial_parameters = [parameter.detach().clone() for parameter in model.parameters()] loss_fn = nn.MSELoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.05) reference_optimizer = torch.optim.SGD(reference_model.parameters(), lr=0.05)
microbatch_size controls the samples processed by one forward pass. effective_batch_size counts the samples represented by one optimizer update.
if len(train_loader) % accumulation_steps != 0: raise ValueError("train_loader must contain complete accumulation windows") optimizer.zero_grad(set_to_none=True) backward_passes = 0 optimizer_steps = 0 for batch_index, (batch_features, batch_targets) in enumerate(train_loader, start=1): predictions = model(batch_features) loss = loss_fn(predictions, batch_targets) / accumulation_steps loss.backward() backward_passes += 1
The loss division averages the gradients across two equal microbatches. A shorter final window needs scaling by its actual sample count rather than the configured full-window count.
if batch_index % accumulation_steps == 0: optimizer.step() optimizer_steps += 1 optimizer.zero_grad(set_to_none=True) gradients_cleared = all( parameter.grad is None for parameter in model.parameters() ) print( f"optimizer_step={optimizer_steps} " f"completed_after_microbatch={batch_index} " f"gradients_cleared={gradients_cleared}" )
optimizer.step() and optimizer.zero_grad() occur only after a complete accumulation window. Gradient clipping or mixed-precision scaler updates belong at this same boundary.
Related: How to clip gradients in PyTorch
Related: How to enable mixed precision in PyTorch
reference_loader = DataLoader( TensorDataset(features, targets), batch_size=effective_batch_size, shuffle=False, ) for batch_features, batch_targets in reference_loader: reference_optimizer.zero_grad(set_to_none=True) reference_loss = loss_fn(reference_model(batch_features), batch_targets) reference_loss.backward() reference_optimizer.step() parameters_changed = any( not torch.equal(before, after) for before, after in zip(initial_parameters, model.parameters()) ) max_parameter_difference = max( (parameter - reference_parameter).abs().max().item() for parameter, reference_parameter in zip( model.parameters(), reference_model.parameters() ) ) matches_full_batch = max_parameter_difference < 1e-7 print(f"effective_batch_size={effective_batch_size}") print(f"backward_passes={backward_passes}") print(f"optimizer_steps={optimizer_steps}") print(f"parameters_changed={parameters_changed}") print(f"max_parameter_difference={max_parameter_difference:.8f}") print(f"matches_full_batch={matches_full_batch}") if not parameters_changed or not matches_full_batch: raise SystemExit("gradient accumulation verification failed")
The reference optimizer receives the same four samples per update in one batch. A nonzero parameter difference or unchanged accumulated model makes the script exit with an error.
$ python3 gradient_accumulation_demo.py optimizer_step=1 completed_after_microbatch=2 gradients_cleared=True optimizer_step=2 completed_after_microbatch=4 gradients_cleared=True effective_batch_size=4 backward_passes=4 optimizer_steps=2 parameters_changed=True max_parameter_difference=0.00000000 matches_full_batch=True