Accumulation changes communication, not the intended batch objective
Derive a window-level update and scope DDP no_sync, mixed-precision scaling, clipping, and optimizer boundaries precisely.
On this page
The question for today
What you’ll understand
- Derive equal-size and unequal-token accumulation scaling.
- Place DDP no_sync around the required forward and backward work.
- Separate one accumulated update from multiple optimizer updates.
Gradient accumulation lets several microbatches contribute to one optimizer update without holding all their activations at once. It does not automatically choose the correct loss denominator, and it does not make several optimizer steps equivalent to one larger-batch step. We assume PyTorch 2.9 DDP with fixed participation, default averaging, and one intended optimizer boundary per accumulation window. Mixed-precision details are scoped to the documented scaling workflow; sharded wrappers and offload modes may have additional memory or support constraints.
Define the window and its single update
With equal token counts in every rank and microbatch, dividing each local mean loss by the number of microbatches gives the intended window mean after default DDP averaging. With unequal valid counts, that shortcut fails: use the global eligible-token denominator across the whole window and the local summed-loss scaling derived earlier. Either way, parameters remain at the same pre-update values while those microbatch gradients accumulate. Clear old gradients at the intended window boundary, not between contributing microbatches, and perform the optimizer update only after the complete synchronized contribution is ready.
Calling the optimizer between microbatches evaluates later gradients at different parameters and advances optimizer state more often. Even stateless SGD on a curved loss need not match the accumulated update; momentum, adaptive moments, weight decay, and step-based schedules create additional differences. A smaller final window also needs its actual denominator. Do not blindly divide by a nominal accumulation count when an epoch tail contains fewer contributing tokens. Make that tail policy explicit and compatible on all ranks instead of letting one rank silently stop early.
Suppress intermediate synchronization without losing participation
DDP no_sync suppresses gradient synchronization within its context while allowing local gradient accumulation. The forward pass must be inside the context as well as the corresponding backward work; wrapping only backward is not the documented protocol. A final normal forward/backward outside the context synchronizes accumulated gradients. Every rank must reach compatible synchronization boundaries. Skipping a final synchronized microbatch or clearing the accumulated buffer first defeats the intended update even when earlier local backward calls succeeded.
Communication suppression does not make other collectives disappear. Count reductions, synchronized layers, or application collectives still need matching participation and order. Also distinguish DDP from FSDP: the PyTorch 2.9 FSDP wrapper documents that no_sync can retain full rather than sharded gradients and increase memory. CPU offload introduces further supported-accumulation restrictions. Verify the chosen wrapper and strategy rather than copy a DDP accumulation recipe into a sharded deployment and assume the same memory footprint and synchronization behavior.
Keep scaling and nonlinear transforms at the right boundary
For mixed precision, use one consistent loss scale while accumulating the window so that contributions are commensurate. Unscale only after all contributions are present, and apply gradient clipping to the intended synchronized unscaled global gradient before the single optimizer step. Updating the scaler or clipping each microbatch separately can change the algorithm. Ensure all replicas make compatible overflow and skipped-step decisions; otherwise optimizer state, schedules, and parameters can diverge despite a shared nominal step counter. Include those edge cases in a small reference test.
The optimization lab demonstrates why step boundaries matter in a simpler setting. Reset, explicitly set step size 0.1 and initial position two, predict the first two iterates, then Step twice. Compare a fresh Reset with step size 0.2 and initial position two before one Step. The quadratic gives 1.8 then 1.62 versus 1.6. Two sequential updates evaluate a changing gradient. This is not an accumulation implementation, has no microbatches or no_sync, and models no momentum or loss scaler; it only makes the update-boundary distinction concrete.
Work it through
Four equal microbatch contributions form one mean update
Two ranks each process two equal-sized microbatches at unchanged parameters. Rank A’s scalar mean gradients are one and three; rank B’s are five and seven. The starting parameter is ten, learning rate 0.1, and SGD is stateless.
The desired mean over four equal-sized contributions is (1+3+5+7)/4=4. Each local mean loss is divided by the accumulation count two under the stated equal-size assumptions.
Rank A accumulates 1/2+3/2=2, and rank B accumulates 5/2+7/2=6. The final default DDP average is (2+6)/2=4. Intermediate no_sync avoids redundant reductions but does not replace this normalization.
After completed final synchronization, one optimizer step produces 10−0.1×4=9.6. Clearing gradients between the microbatches or stepping before the final reduction would implement a different computation.
The accumulated window produces gradient four and one next parameter of 9.6 under the equal-size and fixed-parameter assumptions.
The living notebook
Explore the model
Interactive lab · optimization
Two small steps are a different schedule from one larger step
Run Reset → explicitly set BOTH η=.1 and x₀=2 → predict → Step twice to get 1.8, then 1.62. In a separate Reset run set BOTH η=.2 and x₀=2 before predicting and taking one Step, giving 1.6. This is fixed quadratic descent, not microbatch accumulation.
- Position x0
- 2
- Objective x²/2
- 2
- Iteration behavior
- Stable step
Gradient descent on f(x) = x²/2: xₙ = (1 − η)ⁿx₀. Playback advances two discrete iterations per second. Values outside |x| ≤ 4 are explicitly reported. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forAn optimizer boundary changes where the next gradient is evaluated; suppressing communication does not define that boundary for you.
Pause & reason
Check your understanding
Your turn
Try it yourself
Two ranks each process three equal-sized microbatches at unchanged parameters. Their local mean gradients are [2,4,6] and [8,10,12]. Calculate each local accumulated contribution after dividing losses by three and the final averaged gradient. How should an unequal-token window differ?
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Use the equal-size formula only for the first calculation, then return to one global valid-token denominator.
Compare with the solution
The first rank accumulates 12/3=4 and the second 30/3=10. Default DDP averages them to seven, matching the mean of all six contributions. For unequal token counts, accumulate local summed losses scaled by world size divided by the global window’s eligible count, with no redundant division by three. Keep parameters unchanged, do not clear intermediate gradients, and synchronize before the single optimizer step. The actual final window size, zero-count handling, clipping, and loss scale must follow the same coordinated policy.
Keep the idea
Take it with you
- Normalize the entire intended update window.
- Use no_sync with forward/backward scope and a final compatible synchronization.
- Keep clipping, loss-scale changes, and optimizer updates at their defined boundaries.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.