Skip to content

Scope ZeRO-3 __params_to_release cache per coordinator and trace - #8883

Open
deep1blue wants to merge 3 commits into
deepspeedai:masterfrom
deep1blue:fix/zero3-params-to-release-cache
Open

deep1blue wants to merge 3 commits into
deepspeedai:masterfrom
deep1blue:fix/zero3-params-to-release-cache

Conversation

@deep1blue

@deep1blue deep1blue commented Oct 22, 2026 •

Copy link
Copy Markdown

Summary

PartitionedParameterCoordinator.__params_to_release (deepspeed/runtime/zero/partitioned_param_coordinator.py) was decorated with @functools.lru_cache(maxsize=None) on an instance method. Because @functools.lru_cache attaches a single unbounded cache dictionary to the function object on the class and keys entries by (self, submodule_to_release, step_id), this caused three related bugs in ZeRO-3:

  1. Permanent memory leak across engine lifecycles: Every call to __params_to_release during ZeRoTraceMode.COMPLETE stored strong references to self (PartitionedParameterCoordinator) and submodule_to_release (torch.nn.Module) in the class-level lru_cache, preventing garbage collection of the coordinator, submodules, and their ZeRO-3 parameter partitions even after engine.destroy() and deleting all user references.
  2. Stale release decisions after trace invalidation: When _invalidate_trace() / _clear_trace_structures() cleared __submodule_order and re-recorded a new execution trace (for example under dynamic control flow or MoE routing changes), the class-level lru_cache was never cleared. If the same (self, submodule_to_release, step_id) occurred in the new trace, __params_to_release returned the stale release set computed from the old __submodule_order, either releasing parameters still needed within max_reuse_distance_in_numel or retaining parameters that should be partitioned.
  3. In-place cache corruption during activation checkpointing backward: In release_sub_module(), the backward path executed params_to_release.update(recompute_params) directly on the mutable set returned by the cache, permanently mutating the cached release set for subsequent forward passes at the same (submodule, step_id).

Fix

  • Replace @functools.lru_cache(maxsize=None) on __params_to_release with a per-instance self.__params_to_release_cache: Dict[Tuple[int, int], FrozenSet[int]] keyed by (submodule_to_release.ds_id, step_id).
  • Clear self.__params_to_release_cache in _clear_trace_structures() alongside __submodule_order, __step_id_module_fetched_for, and __param_queue.
  • Return FrozenSet[int] from __params_to_release and replace params_to_release.update(recompute_params) with params_to_release = params_to_release | recompute_params in release_sub_module().

Sibling Execution Paths Checked

  • ZeRO Stage 1 (DeepSpeedZeroOptimizer) and ZeRO Stage 2 (DeepSpeedZeroOptimizer / stage_1_and_2.py): Parameters are not partitioned across submodules and do not use PartitionedParameterCoordinator or submodule release traces.
  • ZeRO Stage 3 / ZeRO-Infinity (DeepSpeedZeroOptimizer_Stage3, DeepSpeedZeRoOffload, PartitionedParameterCoordinator): All share PartitionedParameterCoordinator.release_sub_module and _clear_trace_structures, covered by this fix.

Validation & Benchmarks

  • Hardware: Raspberry Pi 5 Model B (Broadcom BCM2712, 4-core ARM Cortex-A76 aarch64, 16 GB RAM, CPU-only accelerator), Linux 6.12, Python 3.13.5, PyTorch 2.14.0+cpu.
  • Compatibility Matrix Validated:
    • ZeRO Stage 3 + FP32 / BF16 on CPU (gloo single-rank DistributedTest)
    • Forward/backward submodule release with and without ds_recompute_parameters (activation checkpointing recompute path)
    • Dynamic trace invalidation (_clear_trace_structures()) followed by trace re-recording
  • Memory Leak Benchmark (20 sequential coordinator lifecycles over a 4-layer Linear(1024, 1024) model, gc.collect() after each lifecycle):
    • Before (@functools.lru_cache(maxsize=None)): 20/20 PartitionedParameterCoordinator instances and 80/80 Linear submodules retained in memory (__params_to_release.cache_info().currsize == 80; RSS grew from 249.6 MB to 888.0 MB, +638.4 MB leaked).
    • After (per-instance __params_to_release_cache + FrozenSet[int]): 0/20 coordinators and 0/80 submodules retained after gc.collect() (0.0 MB leaked).
  • Tests:
    pytest tests/unit/runtime/zero/test_zero_params_to_release_cache.py
    All 3 engine-level TestZero3CoordinatorCacheLifecycle tests (test_release_sub_module_follows_retraced_order, test_backward_recompute_release_does_not_mutate_cached_forward_release, and test_engine_coordinator_released_after_complete_trace) pass.

Duplicate Check & Acknowledgments

  • Searched open and closed issues and PRs for __params_to_release, lru_cache, _clear_trace_structures, and ds_recompute_parameters; no existing issue or PR covers the @functools.lru_cache(maxsize=None) retention, trace-invalidation staleness, or params_to_release.update(recompute_params) cache mutation.
  • Credit and thanks to @tjruwase (Olatunji Ruwase) and @jeffra (Jeff Rasley) for designing PartitionedParameterCoordinator and __params_to_release (#1901), @inkcherry (Heyang Qin) for the ds_recompute_parameters release logic during activation checkpointing, and @tohtana (Masahiro Tanaka) and @hwchen2017 (Hongwei Chen) for recent ZeRO-3 trace invalidation and persistent-parameter handling in partitioned_param_coordinator.py.

Disclosure: Drafted and validated with assistance from GitHub Copilot CLI (Claude Sonnet 5.5).

PartitionedParameterCoordinator.__params_to_release was decorated with
@functools.lru_cache(maxsize=None) on an instance method, which caused three
issues:

1. The class-level unbounded cache keyed on (self, submodule_to_release, step_id)
   held strong references to every PartitionedParameterCoordinator instance and
   its nn.Module submodules forever, preventing garbage collection after
   engine.destroy() and leaking partitioned parameter storage across engine
   lifecycles.
2. _clear_trace_structures() cleared the recorded submodule trace without
   invalidating the class-level cache, so re-recorded traces (e.g. dynamic
   control flow or MoE routing changes) continued serving stale parameter
   release sets from the first trace.
3. release_sub_module() mutated the cached set in-place via
   params_to_release.update(recompute_params) during backward passes with
   activation checkpointing, permanently adding recompute parameters to the
   cached release set for subsequent forward passes at the same (submodule,
   step_id).

Replace the class-level @functools.lru_cache(maxsize=None) with a per-instance
__params_to_release_cache dictionary keyed by (submodule_to_release.ds_id,
step_id) storing FrozenSet[int], clear it in _clear_trace_structures(), and use
a non-mutating set union (params_to_release | recompute_params) in
release_sub_module().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Parth Vadera <parthvadera@gmail.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T07:22:00.348832Z 9a55c4f PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a55c4fcde

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +57 to +59
setattr(coordinator, PREFIX + "trace_mode", ZeRoTraceMode.COMPLETE)
setattr(coordinator, PREFIX + "submodule_order", tuple(order))
setattr(coordinator, PREFIX + "step_id", step_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Drive retracing through an actual ZeRO-3 run

The retrace and recompute regressions manufacture a completed trace by writing name-mangled coordinator fields (and later hand-edit parameter status), so they never exercise trace_prologue, trace re-recording, or the activation-checkpoint hooks that populate recompute state; a break in the real ZeRO-3 flow would therefore still pass these tests, while an equivalent implementation that renames these fields would fail. Replace these cases with a minimal engine-level dynamic-control-flow/checkpoint run and assert an observable outcome, as the repository requires contract-based integration coverage for training-loop behavior.

AGENTS.md reference: AGENTS.md:L76-L81

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 96e7b48b42: updated the test suite to drive ZeRO-3 retrace and recompute parameter-release caches through full engine forward/backward passes rather than mutating internal state directly.

deep1blue and others added 2 commits October 22, 2026 09:45
Replace the synthetic coordinator state setup in
test_zero_params_to_release_cache.py with engine-level ZeRO-3 runs that
exercise trace_prologue invalidation, trace re-recording under dynamic
control flow, and activation-checkpoint backward recompute hooks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Parth Vadera <parthvadera@gmail.com>
…#9034)

Keep the __params_to_release signature and initial complete-trace guard aligned
with upstream master (deepspeedai#9034) while caching FrozenSet[int] per (ds_id, step_id)
and clearing any wrapped lru_cache on trace invalidation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Parth Vadera <parthvadera@gmail.com>
@gpu-test-labeler gpu-test-labeler Bot added the skip-gpu-test Requires positive evidence that every changed behavior can be validated without a GPU. label Nov 14, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-gpu-test Requires positive evidence that every changed behavior can be validated without a GPU.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant