Repository navigation
Conversation
After harvesting the recomputation-leaf gradients into the returned grads tuple, clear the leaves .grad attributes. When the detached leaves outlive the call (e.g. kept alive by saved-variable references from the recomputation subgraph), the stale .grad otherwise retains a full copy of the segment input gradients across iterations. Signed-off-by: iiap <1471127927@qq.com>
|
/claude strict-review |
| for inp in detached_inputs: | ||
| if isinstance(inp, torch.Tensor): | ||
| inp.grad = None |
There was a problem hiding this comment.
[IMPORTANT Correctness] Please confirm this is safe when backward runs under CUDA graph capture.
What: Before this change, the detached leaf's .grad was a second, independent strong owner of the input-gradient buffer. After it, the only owner is the grads tuple, which is handed to the autograd engine, consumed by the upstream node, and then freed.
Why it matters: backward() here explicitly tolerates running during capture — line 603 gates the _is_checkpoint_valid() error on not is_graph_capturing(), and cuda_graphs.py:1063 documents the [interaction with recompute] case where the recompute forward is re-run inside the backward pass. If torch.autograd.backward(outputs, args) on line 632 executes while a graph is being captured, the .grad tensors that AccumulateGrad produces are allocated from the capture mempool (self.mempool). Dropping the last Python reference to such a block returns it to that mempool, where a subsequent capture in the same pool can hand the same address to a different tensor — the two captured graphs then alias on replay and produce silently wrong gradients. This is precisely the hazard the create_strong_ref / fwd_cudagraph_buffer / bwd_cudagraph_buffer machinery in cuda_graphs.py exists to prevent, and why gc.collect() there is deliberately restricted to the last layer.
I believe the standard Megatron cudagraph path is unaffected, because checkpoint() (line 658) bypasses CheckpointFunction entirely under is_graph_warmup() or is_graph_capturing(), so no CheckpointFunction node ends up inside the captured forward graph and create_bwd_graph never captures its backward. But line 603 only makes sense if some configuration does reach this code during capture, so the invariant is not obvious from the file alone.
Suggested fix: either confirm via a run of the cudagraph + recompute_granularity=full functional tests (label the PR "Run functional tests" per .claude/skills/mcore-testing/SKILL.md) that this path is never entered during capture, or guard the release so it is skipped while capturing:
from megatron.core.transformer.cuda_graphs import is_graph_capturing
# (already imported at the top of this method)
# Release the recomputation-leaf gradients ... (comment)
# Skipped during graph capture: the returned `grads` are the only remaining
# owner, and freeing a capture-mempool block lets a later capture alias it.
if not is_graph_capturing():
for inp in detached_inputs:
if isinstance(inp, torch.Tensor):
inp.grad = NoneA one-line note in the PR description recording which configuration you validated would also be useful, since the reported measurement is from a non-cudagraph run.
| # Release the recomputation-leaf gradients: they were harvested into | ||
| # `grads` above, but the detached leaves can outlive this call (e.g. | ||
| # kept alive by saved-variable references from the recomputation | ||
| # subgraph), retaining a stale copy of the segment's input gradients | ||
| # across iterations. |
There was a problem hiding this comment.
[SUGGESTION Other] The comment says the stale .grad is retained "across iterations", but the mechanism it describes — saved-variable references from the recomputation subgraph keeping the detached leaves alive — does not by itself explain an iteration-length lifetime.
The recomputed subgraph built at line 623 is dropped as soon as torch.autograd.backward on line 632 finishes (it is not retained), and ctx is torn down when the engine releases the node. What actually extends the leaves' lifetime past that point is whatever external reference the measurement found, e.g. an outer autograd node or a Python-side container still holding the recomputed tensors. Without naming it, a future reader cannot tell whether the release is still necessary or has become dead code, and cannot reproduce the finding.
The PR description says weakref instrumentation identified the retaining path. Naming that path here — the specific holder, not just "saved-variable references" — would make the comment verifiable:
# Release the recomputation-leaf gradients. They have already been harvested
# into `grads` above, so the returned gradients are unaffected. The detached
# leaves can outlive this call when <name the concrete holder>, in which case
# their `.grad` keeps a full copy of the segment's input gradients alive until
# the holder is dropped. Nested checkpointing is unaffected: each call touches
# only its own `detached_inputs`.Also worth noting inline: this loop is safe only because no caller reads detached_inputs[i].grad after backward returns. I checked and none does — CheckpointWithoutOutputFunction.backward (line 766) reads inp.grad from its own ctx.inputs, and ScheduleNode.get_grad (pipeline_parallel/utils.py:290) reads from its own detached self.inputs, both distinct tensor sets. Stating that invariant in the comment protects it against a future change.
Strict Review SummaryFindings: CRITICAL: 0 · IMPORTANT: 1 · SUGGESTION: 1 ScopeSingle hunk, 9 added lines, 0 deleted, in What I verified
Most impactful findingThe one substantive concern is the CUDA graph capture interaction. This change removes the second strong reference to the input-gradient buffers, leaving My reading is that the standard path is safe, because AssessmentRisk: low. The change is minimal, local, numerically inert on the gradient values, and the memory-lifetime reasoning is sound for the non-cudagraph path. The measured 0.27 GB saving is small and honestly reported, with the 2.4 GB figure correctly labelled an extrapolation rather than a measurement. Two things before merge:
Neither blocks the approach — this is a reasonable, well-scoped memory fix. |
What does this PR do?
Re-submission of #6117 on current
dev(c76ff61). The change is byte-identical to the original —megatron/core/tensor_parallel/random.pyis unchanged since that PR was opened, so it applies cleanly.Why the original PR disappeared: #6117 was not withdrawn by me. My fork left the upstream fork network on Aug 5 (the repository was turned private for unrelated internal use), and GitHub auto-closes any open PR coming from a detached fork, attributing the close to the repo owner — which is why the timeline reads "htesd closed this". It took #6017 down in the same second; that one is re-submitted as #6338. This PR comes from a fresh, properly attached fork (
htesd/Megatron-LM-oss).#6117 never got past
copy-pr-bot, so it has no prior review or CI history. @yaox12 — sorry to ping you directly, you handled the sibling PR; would you mind vetting this one with/ok to testso CI can run?The bug
CheckpointFunction.backwardrecomputes the forward pass using detached segment inputs, runs backward on the recomputed graph, and collects the input gradients from the detached leaves:The detached leaves can remain reachable after
backwardreturns when the recomputed graph retains references to them. In that case, their.gradfields also keep the input-gradient storage alive longer than required.This change clears each detached tensor's
.gradafter collecting the returned gradients. Thegradstuple retains the gradient tensors, so the gradients returned byCheckpointFunction.backwardare unchanged. Nested checkpointing is unaffected: each call only touches its owndetached_inputs.Measured impact
Numbers below are from #6117 (measured on that PR's base commit); the affected code path is unchanged on current
dev.On a single H200, a 2-layer GPT configuration with
num_residual_streams=4, bf16, micro-batch size 1, mock data, andrecompute_granularity='full'produced the following allocator floor, in otherwise identical runs differing only by this change:This is a reduction of 0.27 GB for the measured configuration.
Weakref instrumentation confirmed that the detached recomputation inputs and their
.gradstorage remained reachable during the between-iteration interval before this change, and that clearing those.gradfields manually released the same allocator blocks. With this change, the gradient references are cleared immediately after collection.Assuming linear scaling with checkpointed segment count and sequence length, the measured result corresponds to an estimated reduction of approximately 2.4 GB for a 9-layer pipeline stage at sequence length 16384. This figure is an extrapolation, not a direct measurement.