Skip to content

fix(zero3): Add focused lifecycle coverage for the ZeRO retained-backward fix. - #28

Merged
nathon-lee merged 4 commits into
feature/zero-multi-loss-separate-backwardfrom
feature/zero-multi-loss-separate-backward-test
Jul 15, 2026
Merged

fix(zero3): Add focused lifecycle coverage for the ZeRO retained-backward fix.#28
nathon-lee merged 4 commits into
feature/zero-multi-loss-separate-backwardfrom
feature/zero-multi-loss-separate-backward-test

Conversation

@nathon-lee

@nathon-lee nathon-lee commented Jul 13, 2026

Copy link
Copy Markdown
Owner

deepspeedai#8135

Summary

This PR stabilizes retained-graph backward behavior and makes the related regression tests more robust across ZeRO stages.

Background

The retained-graph test subset exposed two instability patterns:

  1. Cross-step state leakage in a specific retained-backward sequence, causing duplicate gradient reduction assertions in ZeRO-2.
  2. Overly strict ZeRO-3 status expectations that could fail on valid non-INFLIGHT states.

What Changed

  1. Runtime hook lifecycle hardening
    Updated output-backward preprocess lifecycle handling to avoid stale preprocess state across retained-graph backward executions.

  2. ZeRO-3 assertion semantics in tests
    Relaxed retained-graph status checks from “must be NOT_AVAILABLE” to “must not be INFLIGHT” to better match valid runtime behavior.

  3. Cross-step contamination test path stabilization
    In the contamination-across-step scenario, switched the retained pair to engine.backward(...) so the test focuses on step-isolation semantics rather than manual backward lifecycle internals.

Why

  • The contamination test is intended to validate isolation between a retained sequence and a fresh next step.
  • Manual retained-backward correctness is still covered by dedicated manual-path regression tests.
  • This separation improves signal quality and reduces false negatives in CI.

Validation

Executed retained-graph regression subset:

  • two_losses_separate_backward_gas1
  • two_losses_separate_manual_backward_gas1
  • retain_graph_state_clears_after_followup_backward
  • retained_backward_does_not_contaminate_next_step

Result:

  • 12 passed, 55 deselected
root@cf036bb6df07:/workspace/DeepSpeed_woo/tests# DS_UNITTEST_TIMEOUT=900 CUDA_VISIBLE_DEVICES=0,1 NCCL_IB_DISABLE=1 NCCL_P2P_DISABLE=1 TORCH_NCCL_ASYNC_ERROR_HANDLING=1 TORCH_DISTRIBUTED_DEBUG=DETAIL PYTHONFAULTHANDLER=1 python -m pytest -q unit/v1/zero/test_zero_user_backward.py -k "(two_losses_separate_backward_gas1 or two_losses_separate_manual_backward_gas1 or retain_graph_state_clears_after_followup_backward or retained_backward_does_not_contaminate_next_step)" -s
================================================================== test session starts ===================================================================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0 -- /usr/local/bin/python
cachedir: .pytest_cache
rootdir: /workspace/DeepSpeed_woo/tests
configfile: pytest.ini
plugins: anyio-4.12.0
collected 67 items / 55 deselected / 12 selected                                                                                                         

unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_backward_gas1[1] [Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:08:59.338478988 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[W715 07:09:00.389552529 ProcessGroupNCCL.cpp:1524] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_backward_gas1[2] [Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:09:11.777395101 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank [Gloo] Rank 01 is connected to  is connected to 11 peer ranks.  peer ranks. Expected number of connected peer ranks is : Expected number of connected peer ranks is : 11

[W715 07:09:12.722439545 ProcessGroupNCCL.cpp:1524] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_backward_gas1[3] [Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:09:23.766982097 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_manual_backward_gas1[1] [Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:09:36.874653442 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank [Gloo] Rank 1 is connected to 01 is connected to  peer ranks. 1Expected number of connected peer ranks is :  peer ranks. 1Expected number of connected peer ranks is : 
1
[W715 07:09:37.869096801 ProcessGroupNCCL.cpp:1524] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_manual_backward_gas1[2] [Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:09:48.969250738 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[W715 07:09:49.932992027 ProcessGroupNCCL.cpp:1524] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_manual_backward_gas1[3] [Gloo] Rank 0 is connected to 1 peer ranks. [Gloo] Rank Expected number of connected peer ranks is : 1
1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:10:00.184245959 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retain_graph_state_clears_after_followup_backward[1] [Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:10:12.929956792 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[W715 07:10:13.834234132 ProcessGroupNCCL.cpp:1524] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retain_graph_state_clears_after_followup_backward[2] [Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:10:23.633543989 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[W715 07:10:24.555803347 ProcessGroupNCCL.cpp:1524] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retain_graph_state_clears_after_followup_backward[3] [Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:10:35.452876703 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retained_backward_does_not_contaminate_next_step[1] [Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:10:47.384722308 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[W715 07:10:48.530872973 ProcessGroupNCCL.cpp:1524] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retained_backward_does_not_contaminate_next_step[2] [Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:11:00.883208899 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank [Gloo] Rank 1 is connected to 01 is connected to  peer ranks. 1Expected number of connected peer ranks is :  peer ranks. 1Expected number of connected peer ranks is : 
1
[W715 07:11:01.981734967 ProcessGroupNCCL.cpp:1524] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
PASSED
unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retained_backward_does_not_contaminate_next_step[3] [Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[rank0]:[W715 07:11:12.084394842 ProcessGroupNCCL.cpp:5072] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group()
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
PASSED

==================================================================== warnings summary ====================================================================
<string>:8
  <string>:8: PytestDeprecationWarning: A private pytest class or function was used.

unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_backward_gas1[1]
  /workspace/DeepSpeed_woo/tests/conftest.py:47: UserWarning: Running test without verifying torch version, please provide an expected torch version with --torch_ver
    warnings.warn(

unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_backward_gas1[1]
  /workspace/DeepSpeed_woo/tests/conftest.py:54: UserWarning: Running test without verifying cuda version, please provide an expected cuda version with --cuda_ver
    warnings.warn(

unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_backward_gas1[1]
  /usr/local/lib/python3.12/dist-packages/_pytest/fixtures.py:1313: PytestRemovedIn10Warning: Class-scoped fixture defined as instance method is deprecated.
  Instance attributes set in this fixture will NOT be visible to test methods,
  as each test gets a new instance while the fixture runs only once per class.
  Use @classmethod decorator and set attributes on cls instead.
  See https://docs.pytest.org/en/stable/deprecations.html#class-scoped-fixture-as-instance-method
    fixturefunc = resolve_fixture_function(fixturedef, request)

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=================================================================== slowest durations ====================================================================
12.49s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retained_backward_does_not_contaminate_next_step[1]
12.34s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_backward_gas1[1]
12.29s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_backward_gas1[2]
12.22s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retained_backward_does_not_contaminate_next_step[3]
12.21s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_manual_backward_gas1[3]
12.12s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_manual_backward_gas1[1]
12.08s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_manual_backward_gas1[2]
12.05s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retained_backward_does_not_contaminate_next_step[2]
12.00s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_two_losses_separate_backward_gas1[3]
11.89s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retain_graph_state_clears_after_followup_backward[2]
11.74s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retain_graph_state_clears_after_followup_backward[1]
11.46s call     unit/v1/zero/test_zero_user_backward.py::TestZeroUserBackwardSeparateLoss::test_retain_graph_state_clears_after_followup_backward[3]

(24 durations < 1s hidden.)
=============================================== 12 passed, 55 deselected, 4 warnings in 153.56s (0:02:33) ================================================

Risk / Follow-up

  • Low risk: changes are limited to lifecycle robustness and test expectation alignment.
  • Follow-up option: add a dedicated manual-backward cross-step isolation test to further tighten coverage boundaries.

@nathon-lee nathon-lee changed the title test(zero3): verify retain_graph state cleanup and no step leakage fix(zero3): Add focused lifecycle coverage for the ZeRO retained-backward fix. Jul 15, 2026
@nathon-lee
nathon-lee force-pushed the feature/zero-multi-loss-separate-backward-test branch 2 times, most recently from 99beea8 to f647a07 Compare July 15, 2026 04:13
Signed-off-by: nathon-lee <leejianwoo@gmail.com>

fix(zero3): defer retained param release and flush after follow-up backward

Signed-off-by: nathon-lee <leejianwoo@gmail.com>

fix: add retain_graph_state_clears_after_followup_backward and retained_backward_does_not_contaminate_next_step

Signed-off-by: nathon-lee <leejianwoo@gmail.com>

fix: fix some format errs by tool

Signed-off-by: nathon-lee <leejianwoo@gmail.com>
@nathon-lee
nathon-lee force-pushed the feature/zero-multi-loss-separate-backward-test branch from f647a07 to e4c7eca Compare July 15, 2026 04:17
…status assertions

Signed-off-by: nathon-lee <leejianwoo@gmail.com>
…kward

- Reset output backward preprocess state after each autograd graph completes.

- Ensure preprocess_once runs once per backward graph task, including repeated backward on retained graphs.

- Keep backward lifecycle consistent for manual scale(...).backward() flows.

- Relax ZeRO-3 retained-graph status assertions from strict NOT_AVAILABLE to no INFLIGHT.

Signed-off-by: nathon-lee <leejianwoo@gmail.com>
…st flow

- Reset output-backward preprocess state at graph completion so retained-graph backward can re-enter preprocess/prologue correctly.

- Keep preprocess_once execution scoped to each autograd graph task.

- Relax ZeRO-3 retained-graph status checks to require no INFLIGHT params instead of forcing all NOT_AVAILABLE.

- In cross-step contamination test, use engine.backward path for the retained pair to validate step isolation without manual-path lifecycle interference.

Signed-off-by: nathon-lee <leejianwoo@gmail.com>
@nathon-lee
nathon-lee merged commit 467d869 into feature/zero-multi-loss-separate-backward Jul 15, 2026
9 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant