Minibatch as Parallelism#
A minibatch of size provides a gradient estimator:
where is a random subset of of size . The variance of this estimator is:
for , where is the per-example gradient variance. The variance falls as , but the computational cost rises as — a classic trade-off.
In a data-parallel setup with workers, each worker processes a local minibatch of size and computes a local gradient. The global batch size is , and the gradient is the average of the local gradients. Workers operate independently on disjoint data shards — no model-level synchronisation during the forward/backward passes.
Data Parallel Loop#
Each iteration of data-parallel SGD:
- Broadcast: The current model parameters are sent from the parameter server (or replicated via all-gather) to all workers.
- Local forward/backward: Each worker samples a minibatch of size , computes the local gradient .
- All-reduce: Workers collectively sum (or average) their gradients. The all-reduce primitive computes and makes it available to every worker.
- Update: Each worker computes the global gradient and applies the optimiser step (or communicates the averaged gradient to a parameter server).
The all-reduce operation (e.g., ring all-reduce) is the communication bottleneck. For a gradient vector of size (number of parameters), the communication time per iteration is roughly worth of data transfer per worker in a bandwidth-optimal ring reduce. With workers and (40 MB per gradient), this is about 80 MB per worker per step — a few milliseconds on high-bandwidth interconnects (NVLink, InfiniBand), tens of milliseconds on Ethernet.
Learning Rate Scaling Folklore#
Linear scaling rule: If you multiply the batch size by (by adding workers), multiply the learning rate by to maintain the same expected parameter update per step:
Why it sometimes works. The expected gradient is the same regardless of : . The total displacement after one step with batch and learning rate is . If you use and , the expected displacement per step scales as , but the variance scales as — variance grows linearly with .
When it breaks:
- Large causes instability: The variance increase can cause the iterates to diverge early in training before the gradient shrinks.
- The noise is beneficial: SGDStochastic Gradient Descent noise helps escape sharp minima (Week 12). Reducing noise by increasing batch size can lead to worse generalisation.
- Warmup is essential: Starting with the full scaled learning rate can cause immediate divergence. A linear warmup (gradually increasing from a small value) is standard practice.
The linear scaling rule is a heuristic, not a theorem. It is a starting point; hyperparameter tuning (warmup duration, momentum correction, gradient clipping) is nearly always needed.
Generalization Gap Stories#
A consistent empirical finding: large-batch training often produces models with worse test accuracy than small-batch training, even when the training loss is matched. This "generalisation gap" has several proposed explanations:
-
Sharp minima hypothesis: Large batches produce lower-variance gradients, which converge to sharper minima — points where the loss increases rapidly in some directions. Small-batch SGD noise helps escape these sharp regions and land in flatter minima that generalise better (Week 12).
-
Effective learning rate: For a fixed total number of epochs, larger batches mean fewer parameter updates (fewer iterations). The model sees fewer noisy gradient evaluations, effectively exploring less of the loss landscape.
-
Regularisation effect of noise: SGDStochastic Gradient Descent noise acts as implicit regularisation, similar to dropout or weight decay. Reducing noise by scaling the batch removes this regularisation.
Counterpoint. Subsequent work has shown that with careful training recipes (longer training, adjusted regularisation, SAMSharpness-Aware Minimization), large-batch models can match small-batch generalisation. The "generalisation gap" is not a law of nature but a failure of naive scaling — it signals that the optimisation procedure (not just the batch size) must be adapted.
What This Course Does Not Cover#
Distributed training beyond data parallelism is a rich field that this course only gestures at:
- Model parallelism: Different layers of the network reside on different workers (pipeline parallelism). Used when a single model is too large for one GPU.
- Tensor parallelism: Individual matrix multiplications are split across workers.
- ZeRO / FSDP: Sharding optimiser states, gradients, and parameters to reduce per-GPU memory.
- Gradient compression: Quantisation and sparsification to reduce all-reduce communication volume.
These are implementation concerns, not optimisation theory — they belong to systems for ML, not an optimisation curriculum. The conceptual takeaway: data-parallel scaling is mathematically clean (averaging independent gradient estimates) until communication cost or generalisation intervenes.
If you double the number of workers while keeping each worker's local batch size unchanged, the global batch size ____.
What is the purpose of an all-reduce operation in data-parallel training?
Browser lab#
Simulate workers each producing a noisy gradient estimate. Demonstrate that the variance of the averaged gradient falls as , and that the expected gradient is unbiased regardless of .
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(123)
d = 100 # dimensional
true_grad = np.random.randn(d)
# Generate K workers, each with noisy gradient: true + noise
def simulate_workers(K, noise_std=1.0, n_trials=2000):
estimates = np.zeros((n_trials, d))
for t in range(n_trials):
grads = np.array([true_grad + noise_std * np.random.randn(d) for _ in range(K)])
estimates[t] = grads.mean(axis=0)
# Variance of averaged gradient (averaged over dimensions and trials)
var = np.mean(np.var(estimates, axis=0))
bias = np.linalg.norm(estimates.mean(axis=0) - true_grad)
return var, bias
K_values = [1, 2, 4, 8, 16, 32, 64]
variances = []
biases = []
for K in K_values:
var, bias = simulate_workers(K, noise_std=1.0)
variances.append(var)
biases.append(bias)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
ax1.loglog(K_values, variances, "bo-", markersize=6, label="Empirical variance")
ax1.loglog(K_values, [variances[0]/k for k in K_values], "r--", linewidth=1, label=r"$\propto 1/K$ (theory)")
ax1.set_xlabel("Number of workers $K$")
ax1.set_ylabel("Variance of averaged gradient")
ax1.set_title("Gradient variance vs $K$")
ax1.legend()
ax1.grid(True, alpha=0.3)
ax2.plot(K_values, biases, "go-", markersize=6)
ax2.axhline(0, color="gray", linestyle="--", alpha=0.5)
ax2.set_xlabel("Number of workers $K$")
ax2.set_ylabel("$\|\|\mathbb{E}[\hat{g}] - \\nabla f\|\|$")
ax2.set_title("Bias of averaged gradient vs $K$ (unbiased)")
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("K | Variance | Bias")
print("-" * 35)
for K, var, bias in zip(K_values, variances, biases):
print(f"{K:3d} | {var:.6f} | {bias:.6f}")