feat(backward): explain the freed autograd graph in terms of invokes - #694
Closed
Hotragn wants to merge 1 commit into
Closed
feat(backward): explain the freed autograd graph in terms of invokes#694Hotragn wants to merge 1 commit into
Hotragn wants to merge 1 commit into
Conversation
Calling `.backward()` in more than one invoke of a trace fails deep inside
`torch.autograd`:
RuntimeError: Trying to backward through the graph a second time (or
directly access saved tensors after they have already been freed). ...
The message is accurate but points away from the cause. Each invoke calls
`.backward()` exactly once, so "a second time" reads as describing someone
else's code -- and the real reason is a fact about nnsight, not about torch:
invokes are not separate runs. Every invoke's input is combined into one
batch and the model is called once, so the whole trace has a single autograd
graph, and the first `.backward()` frees it for every invoke after it.
Translate that specific error. `_explain_freed_graph` matches autograd's
message (a bare `RuntimeError`, so there is nothing else to match on),
states the shared-forward-pass cause, and shows the fix -- `retain_graph=True`
on every `.backward()` but the last -- while keeping torch's original text
appended. When the failing call already passed `retain_graph=True` it says so
instead, since the missing flag is then on an earlier call. Any other
`RuntimeError` is re-raised untouched.
Behaviour is unchanged: `retain_graph=True` already worked across invokes,
and each invoke's gradient is its own rows. Tests assert that against
per-prompt single-invoke references (relative error ~1e-5 for the matching
invoke, ~1.2 against the others) for two and three invokes.
Not fixed here: nnsight could retain the graph implicitly for all but the
last invoke. That trades peak memory for convenience on every batched trace,
so it seemed better left to a maintainer's call than folded into an error
message.
Docs: adds the cross-invoke case to docs/gotchas/backward.md. The existing
`retain_graph` section only covered two backwards in one invoke, which does
not read as the same problem, and points at it from the cross-invoke gotcha
TL;DR.
Author
|
Superseded by #699, which is the same change based on I checked the problem still reproduces there before porting — #699 also folds in the batched-invoke gradient coverage from #693, since |
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
Calling
.backward()in more than one invoke of a trace fails deep insidetorch.autograd:The message is accurate but points away from the cause. Each invoke calls
.backward()exactly once, so "a second time" reads as describing someone else's code. And the real reason is a fact about nnsight, not about torch: invokes are not separate runs. Every invoke's input is combined into one batch and the model is called once, so the whole trace has a single autograd graph — and the first.backward()frees it for every invoke after it.docs/gotchas/backward.mddid coverretain_graph, but framed as "if you call.backward()more than once on overlapping graphs". One backward per invoke does not read as that, which is exactly why this is worth a dedicated diagnostic.Changes
_explain_freed_graphtranslates that one error. It matches autograd's message — a bareRuntimeErrorwith no exception type or code to key off, so string matching is the only option, and it is narrow — then states the cause in nnsight's terms and shows the fix, keeping torch's original text appended:When the failing call already passed
retain_graph=True, the message says the earlier one did not, rather than suggesting a flag that is already set. Any otherRuntimeErroris re-raised untouched.Docs: adds the cross-invoke case as its own section in
docs/gotchas/backward.md(symptom / cause / wrong code / right code / mitigation, matching the file's existing shape) and points at it from thecross-invoke.mdTL;DR.Behaviour is unchanged
retain_graph=Trueacross invokes already worked; it just wasn't discoverable. The tests pin that it produces each invoke's own gradient, against per-prompt single-invoke references:for both two and three invokes.
Not fixed here
nnsight could retain the graph implicitly for all but the last invoke and make the whole thing disappear. That trades peak memory for convenience on every batched trace and changes a default, so it felt like a maintainer's call rather than something to fold into an error message. Happy to follow up if you'd prefer that.
Verification
tests/test_backward_errors.py, 5 tests — 2 fail ondev, all 5 pass here. Covers: the cause and fix appear in the message; the already-retaining variant; an unrelatedRuntimeErroris not reworded;retain_graphacross two and three invokes yields the correct per-invoke gradients.Full CI test list on CPU:
1 failed, 248 passed. The one failure istest_lm.py::TestGradients::test_backward_with_multiple_invokers, already red ondevandmainand unrelated to this change (addressed in #693).Run with
transformers5.15.1 /torch2.9.1 on CPU.