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
Steps to run PyTorch gradient accumulation:
- Create gradient_accumulation_demo.py with deterministic data and paired models.
- gradient_accumulation_demo.py
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.
- Append the accumulation-window setup and scaled backward pass to gradient_accumulation_demo.py.
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.
- Insert the optimizer boundary below backward_passes += 1 inside the microbatch loop.
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 - Append the full-batch reference updates and parameter checks after the microbatch loop.
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.
- Run the completed gradient accumulation script.
$ 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
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.