Repository navigation
perf(train): release consumed grads at the optimizer-step boundary - #460
Merged
Merged
Conversation
optimizer_step() zeroed grads only on the skip path; committed updates left them alive across offload()/onload(), which move "params + grads + optimizer" as one train state, and across the next batch's anchor prep. Release them after the commit so dead tensors stop riding the rollout boundary. Accumulation windows are unaffected: non-stepping parts run zero_grad=False and the stepping update still zeroes at entry.
Yuanjia-Lee
requested review from
CjhHa1,
celve,
haonan3 and
zzhuoxin1508
as code owners
September 14, 2026 10:29
Collaborator
|
@Yuanjia-Lee I've shorten the docstrings as the method itself is worth adding |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
optimizer_step()zeroed grads only on the skip path; committed updates leftthem alive. The colocate loops then carry them through the whole cycle:
offload()/onload()move params, grads, and optimizer state together, andthe next batch's anchor prep runs with the previous update's grads on GPU.
Nothing reads
.gradbetween updates: the only two callers (stack/base.py,unified_model_stack.py) go straight to metric aggregation, and accumulationnever spans an optimizer step.
A/B over a full FSDP2 train/offload/rollout/onload/anchor cycle, arms
differing only in the release line: parameters, AdamW state, and both
updates' losses are bit-identical. The baseline arm alone still moves the
previous update's grads through offload and onload; with the release,
onload's allocated delta drops from params+grads to params only. On the 4B
full-FT AR recipe the sharded fp32 grads reach 2 GB/rank, crossing PCIe
twice per rollout.
The release is the skip path's own
zero_grad(set_to_none=True), placedafter the EMA/count consumers. Accumulation windows are unaffected:
non-stepping parts run
zero_grad=False, the stepping update zeroes atentry. Recorded in
unirl/train/readme.mdGotchas.Related Issue
Related to #429: move count there, payload size here.
Test Plan
SKIP=no-commit-to-branch pre-commit run --files unirl/train/backend/base_backend.py unirl/train/readme.md --show-diff-on-failure→ all hooks pass.
optimizer_step→ realfsdp_offload→ realfsdp_onload→ no-gradanchor forward → next update. Param shards and AdamW state match
bit-for-bit (max diff 0.0), losses equal.
Compatibility / Risk
shard. Clipping and deferred sync are unchanged: grads are fully
reduce-scattered before
optimizer.step().Reviewer Notes
an opt-in parking method for external-engine rollouts; refactor(diffusion): make role residency one choice per role, not per phase #428 reorders
trainer offload events).
Checklist