Repository navigation
Conversation
|
|
|
@tianyu-l could you approve the pending Lint, CPU Unit Test, and GPU Unit Tests workflows for this revision? The two-H100 deterministic comparison and deferred-reduction, CUDA Graph, and recovery results are in the description. |
|
@claude explain the issue to me in simpler ways -- use example if it's helpful also review the PR |
|
Claude finished @tianyu-l's task in 2m 59s —— View job Part 1 -- The problem, explained simplyBackground: what deferred accumulation is trying to do. Say one optimizer step uses 3 microbatches (mb0, mb1, mb2). With
So each param group should keep its unsharded gradients locally through mb0 and mb1, then do one reduce-scatter per group during mb2's backward. With HSDP, What That gives N+1 backward passes per microbatch, and FSDP treats each one as a full backward. Bug A: the wrapper overwrites the caller's settings. On main, the wrapper does this: lm_head.set_requires_gradient_sync(False) # chunks 0..N-2
...
lm_head.set_requires_gradient_sync(True) # final chunk, unconditionally
...
lm_head.set_reshard_after_backward(True) # after the loop, unconditionallyExample: on mb0 the engine says "don't sync", but the wrapper turns sync on for Bug B: an inner chunk backward reduces the decoder too early. When an autograd pass touches any FSDP module, FSDP's root post-backward callback runs over every group in the root context, not just Each decoder group gets reduced twice instead of once, plus an early Bug C: a grouped The PR's fix:
Part 2 -- ReviewThe diagnosis looks right to me, and I agree with the goal. My concerns are mostly about how the fix gets there. 1. Heavy dependence on FSDP2 private internals. This is the main concern.
That is a lot of private state for a core component (Core Principle 1: this kind of complexity belongs upstream). Any FSDP2 refactor of these names or semantics would break it silently. The CPU tests won't catch that, because
2. Default HSDP behavior changes, but was only checked on CPU. In the HSDP default (non-deferred) mode, 3. Reshard before reduction on the last microbatch with deferral ( 4. The frozen-decoder grouped-head workaround (
5. Test coverage doesn't cover the new behavior. The test diff only adapts the existing mock. There's no checked-in test for:
The description lists a lot of good ad-hoc verification (Gloo 2/4-rank, 2xH100), but none of it is in the repo. CLAUDE.md asks for CPU unit tests plus a GPU/multi-rank test for parallelism changes. The 6. Audit sibling callsites. 7. Nits.
Before merge, in addition to the author's request: run Overall: the bugs are real and the numerical evidence is strong. My hesitation is the amount of private FSDP2 state being manipulated in core I only read the code; I didn't run any tests (no torch or GPUs in this environment). |
|
The wrapper now preserves the caller's policies across the inner chunk backwards, so a three-microbatch step can keep its gradients together until decoder backward. Added real four-rank FSDP/HSDP CPU and GPU regressions for grouped and frozen heads, failures with pending gradients, and reset/retry; all 16 CPU combinations pass. The public sync setter also changes HSDP's separate all-reduce flag, so the policy snapshot remains in one private-state boundary. Kept early reshard and documented its ownership: the autograd leaf retains gradients after full weight storage is released; current native coverage and the two model-CI failures are recorded in the description. |
|
@claude take another look
|
|
Claude finished @tianyu-l's task in 7m 14s —— View job Re-review: chunked-loss FSDP policy preservation
I only read the code and traced FSDP2's root post-backward callback, 1. CorrectnessCorrect as written
Needs attention before merge (a) PP: what the wrapper preserves is the previous microbatch's policy. Under PP, Mathematically it should still be correct, because PP's finalization reduces whatever is pending. But it changes how (b) Production uses only the grouped layout; the separate layout is test-only. (c) A pre-existing perf issue that the defer path fixes only some of the time. With the default (d) Test robustness. The test counts reductions by patching 2. Readability and maintainabilityThere is room here. In rough order of impact: 2.1 Drop pending-gradient detection by always isolating the head pass (when # decoder_requires_grad: decoder backward follows in this microbatch and owns
# the root finalization. Inner head-only passes must not reduce, reshard or
# finalize any non-head group.
for group in non_head_groups:
group.reduce_grads = False
group.reshard_after_backward = False
state_ctx.is_last_backward = False
# head groups: reduce_grads=False until the final chunk, then the caller's policy (unchanged)
2.2 Put all private FSDP access behind one object, and move it to # torchtitan/distributed/fsdp.py
def finish_grouped_forward(module: FSDPModule, output: Any) -> Any: ... # shared with post_validate_fsdp_for_cuda_graph
@contextmanager
def isolated_head_backward(head, *, run_backward, decoder_requires_grad) -> Iterator[HeadBackwardHooks]:
...
class HeadBackwardHooks: # no-op implementation when head is not an FSDPModule
def after_forward(self, output): ... # frozen grouped head -> finish_grouped_forward
def before_last_chunk(self): ... # restore caller sync on head groupsThen 2.3 Smaller items
3. What belongs in the upstream FSDP APIListed by how much private state each removes from torchtitan:
Each private access should get a Summary
|
discard metadata-inference gradients before the first training microbatch and retain caller replica-reduction policy until manual finalization.
| # Metadata inference runs a real backward. Discard its unreduced FSDP | ||
| # gradients as well as the sharded gradients cleared by the stage. | ||
| for stage in stages: | ||
| if isinstance(stage.submod, FSDPModule): | ||
| schedule.register_post_metadata_inference_cleanup( | ||
| stage.submod.reset_iter_state | ||
| ) |
There was a problem hiding this comment.
should this be in upstream pytorch pipelining code or should it be here?
There was a problem hiding this comment.
this belongs in upstream PipelineStage cleanup.I kept the registration here cuz preserving deferred reduction leaves metadata-probe state outside param.grad, and the first real step needs that state cleared.
main forces head sync on in the last chunk, which clears that state early, so the cleanup needs to stay with this fix
There was a problem hiding this comment.
tracked the cleanup in pytorch/pytorch#203178 with a standalone reproduction. the local registration keeps discarded metadata-probe gradients out of the first training step until PipelineStage owns that reset.
There was a problem hiding this comment.
should this be a separate file, or should it be included in fsdp.py, or is it worth creating a folder for fsdp/
There was a problem hiding this comment.
I think should keep it separate since this file owns runtime state and fsdp.py owns sharding setup
both loss and validation use it, and keeping the private accesses here gives the upstream replacement one place to remove
added the module docstring and moved the grouped-forward TODO to the helper that owns it
|
@claude reivew my latest comments |
|
Claude finished @tianyu-l's task in 1m 59s —— View job Replies to your three inline comments
I couldn't import torch in this runner (permission prompt), so I haven't checked the upstream pipelining and FSDP source on the pinned nightly. The points about upstream internals below come from reading the code and from memory. 1.
|
| current | suggested |
|---|---|
chunked_loss_fsdp |
fsdp_partial_backwards (or isolated_fsdp_partial_backwards) |
lm_head param |
module |
decoder_requires_grad |
outer_backward_follows. This is the real meaning: another backward through the root follows and owns finalization. |
_ChunkedLossHooks |
PartialBackwardHooks |
before_last_chunk |
before_last_backward |
defer_head_reduction |
defer_to_outer_backward |
If you'd rather keep it chunked-loss-specific, that's defensible too, since the before_last_chunk contract is tailored to it. In that case the current name is honest, but the docstring should say so explicitly: "Assumes module is the only FSDP module run in the inner passes and that all inner passes precede a single outer backward."
A related readability nit from the earlier review that's still open: the context manager yields None when the head isn't FSDP, so loss.py checks if fsdp is not None twice inside the chunk loop (loss.py:736, loss.py:756). Yielding a no-op PartialBackwardHooks instead removes both branches, and the loop body then reads the same with or without FSDP.
Summary:
- The PP cleanup should eventually live in upstream
PipelineStage; keep it here with a TODO, and confirm whether it's actually a separate fix that's reachable onmain. - Keep
fsdp_lifecycle.pyas a separate file (no package), with a module docstring and upstream TODOs. - The logic is generic, so I'd rename it after the "scoped partial backward" concept rather than chunked loss.
I only posted review comments; I made no code changes.
|
@ZenAlexa what claude says makes sense to me. Could you help address the final nits? |
|
@claude review again also check if the CI failure is introduced by this PR |
There was a problem hiding this comment.
Verdict: Request changes -- This PR causes the CI failure: the two PP model goldens fail on this head and pass on main. That numerics change is still unexplained, and the golden files haven't been regenerated.
What changed since the last review (64646a2). The commit is mostly mechanical:
- renames (
fsdp_partial_backwards,outer_backward_follows,_PartialBackwardHooks,before_last_backward); - a no-op hooks object for non-FSDP heads, which removes the two
if fsdp is not Nonebranches inloss.py; - a module docstring, and the grouped-forward TODO moved onto
complete_fsdp_grouped_forward; - a reworded PP cleanup comment.
None of these change behavior.
CI failure: caused by this PR. The failing job is 8 GPU Integration (Real PG, models), run on head 64646a2. Two tests fail the exact-match golden check:
| test | first mismatch | example |
|---|---|---|
deepseek_v3_fsdp+cp+pp+ep |
grad_norm at step 6, loss at step 7 | loss 4.0435534 vs golden 4.0435486 |
gpt_oss_pp+fsdp+cp+ep+sacop |
grad_norm at step 3, loss at step 4 |
Every other step is bitwise equal, and llama3_fsdp+tp+pp passes in the same job. The evidence that this PR is the cause:
- On #5664, the latest commit on main, the same job passes both tests with the same golden files ("All losses are equal ... Assertion passed!"). That job fails only on an unrelated
deepseek_v4_dist_muon_fsdp+epconfig error ('TrainingConfig' object has no attribute 'disable_cuda_graphs'). - The same two tests failed the same way on this PR's earlier
142f59599. - Both failing configs use
Interleaved1F1Bwith 8 microbatches and useChunkedLossWrapper(it is the debug model default). That is the path where this PR changes howlm_headgradients are grouped into reductions; see the inline comment onfsdp_lifecycle.py:86-108.
The size and onset of the drift (~1e-6 relative, appearing after a few steps) are consistent with a change in summation order, not a gradient bug. Unverified: that it is purely an ordering effect. A same-environment main vs. head run on these two configs would confirm it.
Must fix
fsdp_lifecycle.py:86-108-- Under PP, the policy snapshot captures a value left over from the previous microbatch, and that leftover changes how head gradients are grouped into reductions, so the PP goldens fail. Either declare it an intended numerics change and regeneratedeepseek_v3_cp_pp.txt/gpt_oss_pp.txt(and every affected backend), or keep main's per-microbatch head reduction under PP (see inline).fsdp_lifecycle.py:65-66,pipeline_parallel.py:519-520-- These TODOs link to comments on this PR instead of upstream issues. File the three PyTorch issues and link them (see inline).
Still open from earlier reviews
- §2.1 from the 10-27 review: always isolate non-head groups when an outer backward follows, instead of reading
_post_backward_pending/_partial_reduce_output. It wasn't adopted and there's no reply. That would remove the two private reads, and it is also the natural place to make the PP behavior deliberate (must-fix 1). test_loss.py's_RecordingFSDPLinearstill mocks private FSDP fields withSimpleNamespace. Now that the real-FSDP lifecycle test exists, drop the policy assertions from the mock.
Resolved
- Coverage for the weight-tied 3-module group: added to both the lifecycle and pipeline matrices.
- The multi-GPU lifecycle test runs at head: Multi-GPU Unit Tests is green.
- Naming, the no-op hooks object and the module docstring, as requested in the 10-28 review.
- Scope of the PP cleanup: the author confirmed it is needed only because of this PR, and the comment now says so.
Should consider
fsdp_lifecycle.py:103--before_last_backwardis invoked before the last forward (inline nit).
Not verified: that the PP drift is purely a summation-order effect. A maintainer still needs to review, since this PR touches core loss and PP code.
| defer_reduction = ( | ||
| ( | ||
| state_ctx.manual_backward_finalization | ||
| and any( | ||
| not policy.all_reduce_grads | ||
| for policy in policies | ||
| if policy.group in module_groups | ||
| ) | ||
| ) | ||
| or outer_backward_follows | ||
| and any( | ||
| policy.group._post_backward_pending | ||
| or policy.group._partial_reduce_output is not None | ||
| for policy in policies | ||
| ) | ||
| ) | ||
|
|
||
| def before_last_backward() -> None: | ||
| if not defer_reduction: | ||
| for policy in policies: | ||
| if policy.group in module_groups: | ||
| policy.group.reduce_grads = policy.reduce_grads | ||
| policy.group.all_reduce_grads = policy.all_reduce_grads |
There was a problem hiding this comment.
This is where the PP golden failures come from. Under PP, the loss runs inside the last stage's forward step, before the schedule sets the backward policy for that microbatch. So the "caller policy" snapshotted at line 75 is whatever the previous stage backward, or the previous step's finalization, left behind. That value then decides whether defer_reduction holds and whether the head groups reduce at the final chunk.
On main, the head reduce-scatters on every microbatch. With this PR, the number of head reductions per step depends on the schedule, which matches your own CPU 1F1B measurement of 5 -> 3. The result is still mathematically correct, but the fp summation order changes, and Interleaved1F1B with 8 microbatches makes that visible from step 6 onward.
Please choose one of the following:
- treat it as an intended numerics change: say so in the description, and regenerate
deepseek_v3_cp_pp.txtandgpt_oss_pp.txt(plus any other affected backend goldens); - or keep main's per-microbatch head reduction when
manual_backward_finalization/ a PP schedule owns the step. That leaves PP numerics unchanged and confines the change to the non-PP accumulation path.
Either way, under PP the "preserve caller policy" here is preserving a leftover value rather than a policy anyone chose for this microbatch. The docstring should say that.
There was a problem hiding this comment.
manual finalization now owns head reduction from the first microbatch for every schedule, and parameter residency follows that same owner. the accumulation-order change is explicit in the description. backend-specific golden regeneration and the current memory, throughput and model checks remain open for the project runners.
Preserve partial-backward policy restoration and HSDP pipeline finalization across current loss, vocabulary-parallel, and accelerator changes. Signed-off-by: ZenAlexa <zimingwang945@gmail.com>
|
@claude review again |
There was a problem hiding this comment.
Verdict: Request changes -- The only new commit since the 10-30 review is a merge of main. It leaves the lifecycle code untouched, so both must-fix items from that review are still open. The PP numerics change is still undecided, and the golden files are unchanged.
What changed since the last review (64646a2 -> e002910). There is one commit, e002910, an ordinary merge of main@d86c77d (#5661, TP cross-entropy).
fsdp_lifecycle.py,pipeline_parallel.py,fsdp.pyand the tests are byte-identical to64646a2.loss.pyagainst main is the same hunk as before (chunk loop wrapped infsdp_partial_backwards), and the merge resolved cleanly.- I found no new problems in the merge.
CI. Lint, CPU Unit Test and GPU Unit Tests at e002910 are waiting for workflow approval (action_required). The 8-GPU model job hasn't run on this head. The latest model result is still the one from 64646a2: deepseek_v3_fsdp+cp+pp+ep and gpt_oss_pp+fsdp+cp+ep+sacop fail their goldens, and both pass on main.
Must fix (still open)
-
fsdp_lifecycle.py:86-108-- PP golden failures. The description now confirms the mechanism on CPU: 1F1B goes from 5 to 3 reduce-scatters per step, with a gradient difference of up to2.98e-8. That makes this an expected numerics change under PP, not a hypothesis. The same-environment GPU rerun would quantify it but won't remove the need to choose one of these:- (a) declare it intended and regenerate
deepseek_v3_cp_pp.txt/gpt_oss_pp.txt, plus every other affected backend; - (b) keep main's per-microbatch head reduction when a PP schedule owns finalization.
Under PP, the snapshotted "caller policy" is whatever the previous microbatch left behind, so (b) loses nothing intended. @tianyu-l, this one is your call.
- (a) declare it intended and regenerate
-
fsdp_lifecycle.py:65-66,pipeline_parallel.py:519-520-- The TODOs still link to comments on this PR. The description's "upstream FSDP API boundaries" section lists three proposals but no filed issues. Please file them in pytorch/pytorch and link them:- scoped partial backward;
- value-based policy snapshot/restore, with
set_requires_gradient_syncdecoupled fromall_reduce_grads; PipelineStageresettingFSDPModuleiteration state after metadata inference.
Still open, no author reply
- §2.1 from the 10-27 review: mask non-head groups unconditionally when an outer backward follows, instead of reading
_post_backward_pending/_partial_reduce_output. Option (b) above fits naturally there. test_loss.py:974-1007--_RecordingFSDPLinearstill rebuilds FSDP's private layout withSimpleNamespaceand assertsreduce_gradson it. The real-FSDP test intest_chunked_loss_fsdp.pyalready covers policy restoration, so trim the mock back to the unshard and forward counts.fsdp_lifecycle.py:103--before_last_backwardruns before the last forward, so the name is a nit.
A maintainer still needs to review, since this touches core loss and PP code.
Not verified: the GPU size of the PP drift at e002910. That needs the model job, which is waiting for approval.
|
@claude explain the PP numerics change with easy-to-understand examples before vs. after
sounds that we should do it? |
|
Claude finished @tianyu-l's task in 3m 38s —— View job Short answer: reducing lm_head once per step under PP is worth doing, but this PR only gets there by accident. The "5 -> 3" comes from whatever FSDP policy the previous microbatch left behind, so the number of head reductions depends on the schedule (GPipe: no change). I'd keep main's behavior under PP in this PR and make "head reduces once per step under PP" its own small PR, with memory numbers and regenerated goldens. Why PP is differentUnder PP, the loss runs inside the last stage's forward action. That covers Example. One stage holds
Why the bits change. The math is the same, but the order of additions differs. Let Float addition isn't associative. In fp32, Should we do it?The goal, yes. On main, lm_head is the only parameter in a PP stage that reduces every microbatch. Everything else waits for PP finalization, and lm_head is usually the largest single weight (Llama3-8B: 128256 x 4096, about 525M params). But the PR doesn't implement that goal deliberately:
Recommendation
If you'd rather land it in one PR, the minimum is option 2 here, deterministic rather than leftover-dependent, plus the memory numbers and regenerated goldens. Either way it's your call. The PR's current form is the only one I'd rule out. Assumptions behind the example
|
|
What still puzzles me is "reduce-scatter mb0" on F0. My impression has been that, without chunked loss wrapper, every FSDP module will only incur one reduce-scatter across all the PP microbatches, after the last backward happens. Correct me if I'm wrong. It seems it's not the case with chunked loss wrapper, even with changes in this PR. Regardless of whether or not we fix in this PR (I think we should), is there a way that we defer this mb0 reduce-scatter on lm_head as well? |
|
Claude finished @tianyu-l's task in 2m 38s —— View job You're right that without the wrapper each FSDP group reduce-scatters once per PP step. The mb0 reduce-scatter on lm_head happens because the wrapper runs lm_head's backward inside the last stage's forward action, and at F0 the stage hasn't switched FSDP to no-sync yet. Yes, it can be deferred: when PP owns finalization, always defer the head (a one-clause change in Why there's normally one reduce-scatter per stepPyTorch's Why lm_head breaks that pattern under chunked lossThe loss runs inside
At F0, the PR's
So How to defer itThe stage already sets defer_reduction = state_ctx.manual_backward_finalization or (
outer_backward_follows
and any(
policy.group._post_backward_pending
or policy.group._partial_reduce_output is not None
for policy in policies
)
)Details: what this changes, and what to update with it
Not verified: I couldn't import torch here, so I haven't checked the upstream |
|
@ZenAlexa could you fix the remaining gap in PP, so that whether chunked loss do grad sync aligns with the PP+FSDP setup? |
let the pipeline schedule own every head microbatch from its first forward, retain its parameter residency, and preserve non-pipeline accumulation policy. replace private-state mock checks with real grouped multi-output coverage and link the lifecycle workarounds to their upstream trackers. validation: four-rank Gloo lifecycle scenarios passed 56 combinations and the multi-output case, 89 selected regressions passed with one LoRA case deselected, and changed-file pre-commit checks passed. the old head reproduces five GPipe reduce-scatters where the schedule-owned contract requires two. GPU model, backend golden, memory, throughput and convergence checks remain pending.
integrate upstream 196b4a5 and keep the eight partial-backward patch files unchanged. the four-rank lifecycle scenario and selected loss and pipeline regressions pass on the combined tree.
|
yep, the first head pass now follows the schedule's manual-finalization mode too, so every head microbatch waits for the schedule's reduction and parameter residency stays with the schedule. this removes the dependence on the previous backward's flags for both GPipe and 1F1B. the resulting summation-order change is intentional, and the remaining model checks and backend-specific golden regeneration are listed in the description. non-head groups are already masked for every inner pass when decoder backward follows, and that keeps their gradients and prefetch state with the outer backward. the pending-gradient check now applies only outside manual finalization, where it keeps the shared norm/head group's accumulated gradients together for one reduction. restoring head sync on every last chunk would split that group's reduction again, so the check stays within this single lifecycle owner. |
|
@claude review again |
There was a problem hiding this comment.
Verdict: Request changes -- The lifecycle code now does what was asked: under PP, the head's reduction always waits for the schedule's finalization. What still blocks merge is evidence, not code. The PP goldens this intentionally breaks haven't been regenerated, and there are no last-stage memory or throughput numbers yet. Both need a GPU run that a maintainer has to trigger.
What changed since the last review (e002910 -> 636e16d). The real changes are in 049301b, which is a merge commit that also carries edits. 636e16d is a plain merge of main and leaves the PR files untouched.
fsdp_lifecycle.py:88-97: when the schedule finalizes backward manually (manual_backward_finalization),defer_reductionis now unconditionally True. The HSDP-only clause is gone.fsdp_lifecycle.py:136-143: under manual finalization the wrapper no longer reshards the head after a backward. Residency follows the schedule's no-sync policy.- The
before_last_backwardhook is renamed tobefore_last_forward. TODOs now link pytorch/pytorch#203176, #203177 and #203178. - The
SimpleNamespacemock tests intest_loss.pyare deleted and replaced by a real-FSDP multi-output scenario. The PP matrix adds a plain-FSDP axis and asserts reduce-scatter counts at finalization.
Example. One stage holds decoder plus the grouped (norm, lm_head), runs 1F1B, and takes 3 microbatches (counts are reduce-scatters per step):
F0 F1 F2 finalize total
main head RS head RS head RS head+decoder 5
e002910 head RS accum accum head+decoder 3 (GPipe: same as main)
636e16d accum accum accum head+decoder 2 (every schedule)
The expected_reductions check in test_chunked_loss_fsdp.py:191-199 asserts exactly this count (2, or 1 with a frozen decoder) for both GPipe and 1F1B.
Resolved since the last review
- PP head deferral now depends on the schedule, not on the policy the previous action left behind (must-fix 1, code side).
- The TODOs link upstream issues instead of comments on this PR (must-fix 2).
- The
_RecordingFSDPLinearprivate-state mock is removed. - The
before_last_backwardnaming nit is fixed. - §2.1 (pending-gradient reads): the author explained that outside PP, the check keeps a shared
[norm, lm_head]group's accumulated gradients in one reduction, and there was no pushback. Accepted.
Must fix
- Regenerate the PP goldens in this PR. The summation-order change now covers every PP + FSDP config that uses
ChunkedLossWrapper, not just the two that failed before.llama3_fsdp+tp+pppassed one002910only because its schedule happened to reduce per microbatch. Expect it to drift now too (unverified). Per CLAUDE.md principle 6, every affected backend (A10G and MI350X) needs regenerated files, with the explanation already in the description. A maintainer needs to trigger the 8-GPU model job at636e16d. - Report last-stage peak memory and throughput on an 8-GPU PP config before and after.
- Cost: the head's unsharded weight and accumulated gradient now stay alive from F0 until finalization. With
finalize_gradients=False, they stay alive across schedule steps. Main resharded and reduced the head every microbatch. - Gain: the per-microbatch head all-gather and reduce-scatter go away.
- This is the intended trade, and it matches what PP's no-sync already does to every other group on the stage. But it is a memory change on the main PP path, so it needs numbers.
- Cost: the head's unsharded weight and accumulated gradient now stay alive from F0 until finalization. With
Should consider
test_chunked_loss_fsdp.pystill reports failures withraise RuntimeError(...)(for example at lines 195-199).self.assertEqualortorch.testing.assert_closewould show diffs. This was raised earlier and is still open.- The PR description has grown into a revision-by-revision log, with evidence from
e002910,81b3eec,142f595,65b1173and1bbb64b. Trimming it to the current head's behavior, the before/after table above and the pending GPU gates would make it easier to verify.
A maintainer still needs to review, since this touches core loss and PP code.
Not verified:
- that the upstream
PipelineStagesetsreshard_after_backward=Falseon non-final backwards (the memory claim in must-fix 2 rests on this); - whether
llama3_fsdp+tp+ppnow drifts; - the CPU, GPU and Lint runs at
636e16d, which were still queued or running at review time.
why
ChunkedLossWrapperruns several head-only backwards before decoder backward. main restores head sync unconditionally on the final chunk, overwriting the caller's FSDP and HSDP accumulation policies. an inner root callback can also reduce earlier decoder gradients before the current microbatch arrives.pipeline schedules choose their backward policy after the loss runs. the wrapper now follows the schedule's manual-finalization owner from the first head pass, so GPipe and 1F1B both defer every head microbatch until schedule finalization.
what changed
torchtitan/distributed/fsdp_lifecycle.py, shared by loss and validation. the loss loop usesbefore_last_forwardandafter_forwardwithout private FSDP reads.is_last_backward, including when a chunk raises.GraphTrainer's production path uses SimpleFSDP and keeps its separate reduction owner.
current integration and validation
implemented the lifecycle correction in
049301bfb83cand integratedmain@196b4a5fcfb3through ordinary merge636e16da4e78, with complete tree7482898122bd. the latest main integration leaves all eight partial-backward patch files unchanged. pipeline manual finalization now unconditionally owns head reduction, including the first microbatch, and the wrapper leaves parameter residency with the schedule. the last-forward hook is namedbefore_last_forward.on macOS arm64, Python
3.13.18, PyTorch2.16.0.dev20261026and four Gloo ranks, the current lifecycle scenario passed 56 combinations plus one real grouped, frozen-decoder, two-output scenario. the matrix covers FSDP and HSDP, GPipe and 1F1B, deferred finalization, frozen decoders, tied weights, caller-policy restoration, reset/retry and losses, gradients and updated parameters against a full-logits reference. each finalized pipeline accumulation performs two reduce-scatters for a trainable decoder or one for a frozen decoder. an accumulation without finalization performs zero. metadata inference is counted separately because its gradients are discarded.the two-output scenario runs two chunks of each output through one real FSDP group, observes one explicit unshard and one actual reshard, and compares its weighted loss and gradients against full logits. the former private-state mock and its duplicate checks are removed.
the same reduction-count scenario fails on the previous
e0029100edbbrevision, where GPipe performs five reductions in the checked accumulation. the final combined run passed the lifecycle entry and 89 selected loss and pipeline regressions, reporting 90 passed with one LoRA CPU FlexAttention case deselected. changed-file pre-commit checks, including Pyrefly, passed.GLOO_SOCKET_IFNAME=lo0 pytest tests/unit_tests/cpu/components/test_chunked_loss_fsdp.py -q pytest tests/unit_tests/cpu/components/test_loss.py tests/unit_tests/cpu/distributed/test_pipeline_parallel.py -k 'not lora' -qnumerical and hardware merge gate
pipeline head gradients now use the same reduction owner as the rest of their stage. this deliberately changes floating-point accumulation order from schedule-dependent partial head reductions to the schedule's final reduction. the existing tolerances and golden files remain unchanged.
the current eight-H100 attempt reached no workload and provides no GPU result. please run the current GPU lifecycle and model workflows, including
llama3_fsdp+tp+pp,deepseek_v3_fsdp+cp+pp+epandgpt_oss_pp+fsdp+cp+ep+sacop, then regenerate the affected A10G and MI350X goldens from those exact backends. same-environment loss and grad-norm comparisons, last-stage peak memory and throughput, and representative convergence remain required before merge. historical device results in this description retain their original revisions.upstream lifecycle trackers
all three include self-contained two-rank CPU reproductions on PyTorch
2.16.0.dev20261026and references to the current upstream implementation. the related pipeline policy RFC owns broader configurable schedule reduction and residency policy. this PR implements the existing schedule's finalization contract.project GPU results on
e0029100edbbthe November 2 multi-GPU job executed and passed the prior 40-combination lifecycle scenario. the full job reported 46 passed, 11 skipped, 358 deselected and one xfailed on the eight-A10G runner with PyTorch
2.16.0.dev20261101+cu132. checkoutdcc041amergede0029100edbbintod86c77d609b4. this is evidence for the earlier revision, before unconditional pipeline-head deferral and the added multi-output scenario. the model job on that revision again failed the strict imported goldens for DeepSeek-V3 and GPT-OSS, beginning at grad-norm steps six and three respectively.earlier CPU lifecycle evidence
at
81b3eec872d2, the HSDP/pipeline correction passed complete loss, gradient and updated-parameter checks against the full-logits reference, preserving caller policy. a separate four-rank metadata-inference failure scenario passed GPipe and 1F1B with tied weights, second-chunk failure, cleanup, retry, delayed accumulation and train → eval → train. cleanup occurred at each metadata-inference boundary and remained absent between normal accumulation groups. those results retain that revision.project GPU results on
142f59599142f59599, before the latest merge and HSDP pipeline correction.project GPU results on
65b11734reduce_scatter_singlecounts. runtime: eight-A10G runner, Python3.12.15, PyTorch2.16.0.dev20261026+cu132, NCCL2.32.3, driver580.178.04. the checked-out merge tree matches65b11734.fsdp_deferred_gradient_reductionon two ranks andfsdp_pp_deferred_gradient_reductionon four ranks, both for ten steps with recorded CUDA graphs. these are execution checks, without a baseline/head bitwise comparison.fsdp_per_group_cuda_graphandshared_eval_cuda_graphpassed in the single-GPU fake-PG job.earlier native evidence
the two-H100 comparison on
1bbb64b44used driver580.95.05, PyTorch2.16.0.dev20261018+cu130, CUDA13.0, NCCL2.32.3, seed 42 and deterministic algorithms. sixteen reduced-model configurations across two revisions completed ten steps. ordinary FSDP loss, gradients, grad norm and parameters were bitwise equal; deferred eager reduce-scatters dropped from seven to three separate owners or two grouped owners. both revisions peaked at676.144 MiBon the large-vocabulary default workload. native lifecycle and reset/retry checks also passed.those scripts explicitly set
is_last_backward(final)in both modes. their numbers apply to that setting and revision. the checked-in scenario follows the actual defaultis_last_backward=True. the earlier separate four-GPU allocation expired in its ten-minute queue budget without a test process or result and was removed.