Skip to content

Wiring up offload_opt_states - #8186

Merged
tohtana merged 10 commits into
deepspeedai:masterfrom
pengdurice:peng-better-deepcompile-v2
Aug 3, 2026
Merged

Wiring up offload_opt_states#8186
tohtana merged 10 commits into
deepspeedai:masterfrom
pengdurice:peng-better-deepcompile-v2

Conversation

@pengdurice

Copy link
Copy Markdown
Contributor

Make DeepCompile's optimizer-state offloading work under inductor and reachable from config

Summary of Changes

  • Registered ops instead of closures — four ops in the existing dc namespace. The graph
    carries a tensor anchor and an integer index; live tensors stay in module state.
  • ORDERED effect registration so stock inductor keeps the ops and preserves their order.
    Reload-before-sync is correctness-critical. A test fails without it.
  • Config wiring, capacity-first schedule [(0,[z3]), (1,[for_init, z3, move_opt_states])]:
    states are emptied to host before profiling, so the plan is made against the floor and a job
    that only fits with offloading never runs a step with everything resident. (The pass author's
    own ordering from their test harness; the budget formula is unchanged.)
  • Three stream races fixed, all silent if wrong: copies wait for the optimizer's writes;
    record_stream protects reload buffers from early reuse; and the copy stream waits for the
    compute stream before writing a reload buffer — without it a mid-backward reload overwrites a
    live activation, which showed up as NaN losses. All are stream dependencies, no host waits.
  • Reload buffers allocate from the pool with room (compute stream, just freed by backward).
    The wrong pool costs an allocator retry plus a device-wide sync per step.
  • Multi-graph gating so graph breaks do not duplicate copies; empty_cache once per compile
    phase rather than per step (per-step measured +28%); mutual exclusion with offload_parameters.

Results

Qwen3-14B, 8×H200 (141 GB), ZeRO-3, micro-batch 4, fp32 states (22.2 GB/rank),
expandable_segments:True. Medians over each run's final phase, single campaign.

configuration seq 2048 seq 2400
plain eager ZeRO-3, no compile OOM OOM
ZeRO-Offload (CPU Adam) 10.11 OOM
DeepCompile, no offload (z3 only) 1.89 dies at the recompile
DeepCompile, no offload (default schedule) 1.82 dies at the recompile
this pass, blocking variant (eager-only) 4.06 4.42
this pass (async), eager 2.43 4.32
this pass (async), inductor 2.01 3.70
this pass at micro-batch 1 0.76 — planner offloads nothing 0.85

Limitations

  • Runs in place of prefetch and selective gather (worth 3–27%); combining them is future work.
  • Designed for gradient_accumulation_steps=1: the graph runs per micro-batch, so accumulation
    repeats the whole cycle. Documented in the config docstring.
  • Mutually exclusive with offload_parameters.

Tests

tests/unit/v1/compile/test_offload_opt_states.py — op and ORDERED-effect registration; a
mechanism test compiling side-effect ops through stock inductor and asserting program order;
budget planning and node placement; re-run and multi-graph gating; once-per-phase empty_cache;
and a 2-GPU end-to-end loss-parity test whose op counters prove the ops ran in the compiled graph
(reloads are skipped while profiling, so a nonzero reload count is the proof).

Replace the Python closures the move_opt_states pass inserted as FX graph
nodes with registered custom ops (torch.library, dc namespace), making the
pass compatible with the inductor backend and its compile cache. Wire the
pass to the user config (compile.offload_opt_states) using the capacity-first
schedule: offload everything, profile on the emptied GPU, then keep resident
only what the memory budget allows. Frees are completion-driven
(record_stream), empty_cache runs once per compile phase, and unit plus
2-GPU end-to-end tests cover op registration, budget planning, schedule
placement, and loss parity.

Signed-off-by: pengdurice <pengduhit@gmail.com>
Signed-off-by: pengdurice <pengduhit@gmail.com>
Signed-off-by: pengdurice <pengduhit@gmail.com>
Signed-off-by: pengdurice <pengduhit@gmail.com>
@pengdurice
pengdurice marked this pull request as ready for review July 31, 2026 21:49

@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: 3ca19178b2

ℹ️ 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 +98 to +100
def test_offload_ops_registered_with_ordered_effects():
_ensure_dc_ops()
from torch._higher_order_ops.effects import SIDE_EFFECTS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip effect-registry tests on older PyTorch

When these tests run under PyTorch versions allowed by the module-level min_version=2.1 marker but before _register_effectful_op exists, this unconditional import/assertion fails even though the production code explicitly treats that registry as optional. This affects CPU/unit test runs on older supported torch versions; guard these tests with the same availability check or raise the pytest minimum for this file.

Useful? React with 👍 / 👎.

Signed-off-by: pengdurice <pengduhit@gmail.com>
Signed-off-by: pengdurice <pengduhit@gmail.com>
Signed-off-by: pengdurice <pengduhit@gmail.com>

@tohtana tohtana left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @pengdurice,
Thank you for submitting this PR! The adaptive offloading feature was described in the paper, but the code had remained unorganized for a long time. I’m very glad that you cleaned it up and enabled the feature.

I left a few comments about some minor issues. Can you please address them? I don’t see any issues with the core implementation of this PR.

schedule = []
if (compile_config.offload_parameters):
schedule.append((0, [zero3_compile.add_z3_gather_release, offload_parameters.offload_parameter_fwd]))
elif compile_config.offload_opt_states:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This pass can be enabled with ZeRO optimizer's offload, but they won't work together. Can we reject the combination?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

sure, just added before this line:

    optimizer = engine.optimizer
    use_opt = not isinstance(optimizer, DeepSpeedZeRoOffload)

Thank you!

from unit.util import bf16_required_version_check, skip_on_arch
from unit.v1.compile.util import compare_loss

pytestmark = pytest.mark.skipif(not required_torch_version(min_version=2.1),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

DeepCompile already limits the version to 2.6+. I think we should make this consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

sure, updated to 2.6. thank you!

Signed-off-by: pengdurice <pengduhit@gmail.com>
Signed-off-by: pengdurice <pengduhit@gmail.com>
@tohtana

tohtana commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Hi @pengdurice,
Thank you for the update!

I found the condition engine.zero_offload_optimizer() is not None also rejects an explicitly disabled ZeRO offload config (offload_optimizer: {} or device: none). Can you fix it? engine.zero_use_cpu_optimizer() might be useful. The expected behavior would be:

  • device: cpu: Reject with ValueError
  • device: nvme: Reject with ValueError
  • device: none or {}: Accept

Signed-off-by: pengdurice <pengduhit@gmail.com>
@pengdurice

Copy link
Copy Markdown
Contributor Author

Hi @pengdurice, Thank you for the update!

I found the condition engine.zero_offload_optimizer() is not None also rejects an explicitly disabled ZeRO offload config (offload_optimizer: {} or device: none). Can you fix it? engine.zero_use_cpu_optimizer() might be useful. The expected behavior would be:

  • device: cpu: Reject with ValueError
  • device: nvme: Reject with ValueError
  • device: none or {}: Accept

thank you for the comment, just fixed;-)

@tohtana tohtana left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for the update! It looks good to me now.

@tohtana
tohtana added this pull request to the merge queue Aug 3, 2026
Merged via the queue into deepspeedai:master with commit 76928ff Aug 3, 2026
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.

2 participants