Repository navigation
[pipelining] Fix backward hook accumulation on recv buffer activations - #185355
karthik-0306 wants to merge 1 commit into
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/185355
Note: Links to docs will display an error until the docs builds have been completed.
|
|
The following ciflow label(s) have been added but CI has not been triggered yet because the workflows are awaiting approval:
Once a maintainer approves the workflows (scroll to the bottom of the PR page), the corresponding CI jobs will be triggered automatically. Please ping one of the reviewers if you do not have access to approve and run workflows. |
|
@pytorchbot label "topic: not user facing" |
|
@wconstab @wanchaol @howard-huang could you please take a look when you get a chance? This fixes hook accumulation on recv buffer activations (issue #185331). Happy to address any feedback! |
|
@claude can you see if there is an alternative that avoids a .clone on the recv but also prevents leaking registered hooks between microbatch executions? |
|
Claude finished @wconstab's task in 3m 48s —— View job Investigating alternatives to
|
detach() |
clone() |
|
|---|---|---|
| Fixes hook accumulation | ✓ | ✓ |
| Zero-copy | ✓ | ✗ |
| Preserves pinned/custom alloc | ✓ | ✗ (clone may use default allocator) |
| Safe if user holds ref across steps | ✗ (shared storage overwritten) | ✓ |
Since the "user holds ref across steps" scenario is unusual and was already broken before this PR, detach() seems like the better default — it preserves the performance and allocation properties of the buffer while fixing the actual bug.
5ef63af to
b22e2ef
Compare
|
thanks for the suggestion @wconstab! Switched from .clone() to .detach() — you're right that clone would lose any pinned memory or custom allocator properties on the recv buffer, which is not ideal. .detach() gives us the same fix (fresh tensor identity each step so hooks don't accumulate) but is zero-copy and fully preserves the buffer's underlying memory allocation. Updated the comments in both the fix and the test to reflect this. |
|
@karthik-0306 Can you fix the lint? |
sanketpurandare
left a comment
There was a problem hiding this comment.
Can you add your test to the existing test file test_stage.py? Also make use of the existing testing infrastructure as much as possible to reduce code redundancy
b22e2ef to
920f5d6
Compare
|
Thanks @sanketpurandare! Fixed the lint and moved the test into test_stage.py reusing MultiMLP and existing test infrastructure. |
920f5d6 to
7063e03
Compare
|
@karthik-0306 Please check the test failure: trunk / linux-jammy-rocm-py3.10-mi355 / test (distributed, 1, 4, linux.rocm.gpu.gfx950.2) (gh) Your own test is failing |
7063e03 to
c954b2d
Compare
|
@sanketpurandare Root cause found — pipeline() was called without split_spec, so it kept the model as a single stage regardless of world_size, causing the "Pipeline group size N cannot be larger than number of stages 1" error on the 4-GPU runner. Fixed by passing MultiMLP's split_spec through, same pattern test_tracer already uses. The other failures (torchtitan integration tests, win-vs2022 builds) look unrelated to this PR — pure Python diff, no build/dependency changes touched. The Windows build log showed a pip self-modification error, and torchtitan runs full training jobs that are known to be flaky independent of the change under test. Happy to dig further if you think otherwise, but wanted to flag in case these can just be re-triggered. Pushed the split_spec fix, should be green on the real failure now. |
|
Looks like this PR hasn't been updated in a while so we're going to go ahead and mark this as |
|
Removing stale — this PR is still active and ready for review. 175/214 checks passing. The 8 remaining failures are all infrastructure issues unrelated to this change (torchtitan flakyintegration tests, Windows pip self-modification errors, and an aarch64 container infra failure). The distributed pipelining tests themselves pass cleanly including on ROCm. Happy to address any further feedback from @wconstab or @sanketpurandare. |
|
@pytorchbot label "no-stale" |
Backward hooks registered on a stage-boundary activation used to accumulate across training steps (fired counts 2, 4, 6, 8, 10), because recv buffers were persistent and requires_grad_() aliased the buffer as the activation (pytorch#185331). Since pytorch#196463, recv buffers are allocated per receive and released once consumed, so the bug no longer reproduces from reading the code and no source change is needed. This adds a regression test that runs 5 steps of Schedule1F1B with 2 microbatches and checks that exactly one hook fires per microbatch per step. Test Plan: python test/distributed/pipelining/test_stage.py -k test_recv_buffer_hooks_do_not_accumulate
c954b2d to
a971006
Compare
|
@wconstab @sanketpurandare I've rebased this onto current main and updated the PR. Since #196463 (just-in-time recv buffer allocation), each receive gets a fresh buffer that is released once consumed, so hooks can no longer accumulate on a persistent buffer. That makes the .detach() change in stage.py unnecessary, so I've dropped it. The PR now only adds a regression test, test_recv_buffer_hooks_do_not_accumulate, so the behavior stays guarded. I haven't run it on GPU locally, so I'm relying on CI for that. If it fails on current main, the bug is still there and I'll look at it again. |
Fixes #185331
Root cause:
_retrieve_recv_activationspassedinfo.bufferdirectly as the stageactivation via
requires_grad_(), which is in-place and returnsself.This aliased the persistent recv buffer as the activation tensor, so
backward hooks attached by user code (e.g. via module forward pre-hooks
for gradient collection or debugging) accumulated on the buffer across
steps rather than being discarded — causing hook fired counts to grow
as 2, 4, 6, 8, 10 instead of staying constant at 2.
Fix:
Clone the recv buffer before use as the stage activation. The clone is a
fresh leaf tensor each step, giving stage-boundary activations the same
lifecycle as normal non-pipelined activations. The recv buffer itself
remains untouched and continues to serve as the
irecvtarget.Test:
test/distributed/pipelining/test_recv_buffer_hooks.py— runsSchedule1F1B with 2 processes, attaches a forward pre-hook that
registers tensor backward hooks, and asserts hook fired count stays
constant (== n_microbatches) across 5 steps.