Skip to content

fix(zero-offload): mitigate CPU RSS waste on ZeRO-2 optimizer offload (#7693)#8132

Open
liuyun7345 wants to merge 1 commit into
deepspeedai:masterfrom
liuyun7345:fix/cpu-offload-memory-leak-7693
Open

fix(zero-offload): mitigate CPU RSS waste on ZeRO-2 optimizer offload (#7693)#8132
liuyun7345 wants to merge 1 commit into
deepspeedai:masterfrom
liuyun7345:fix/cpu-offload-memory-leak-7693

Conversation

@liuyun7345

@liuyun7345 liuyun7345 commented Jul 11, 2026

Copy link
Copy Markdown

Summary

Best-effort mitigations for CPU RSS waste in ZeRO-2 + CPU optimizer offload tracked by #7693.

This PR does not claim a complete resolution of all overhead in that issue (H2D cast/copy buffer tiling and other unaccounted bytes remain open). Another commenter also reported being unable to reproduce the two ~4 bytes/param leaks on a newer PyTorch build.

Changes

1. unscale_and_clip_grads — float conversion only on CPU offload (stage_1_and_2.py)

Convert combined_scale to a Python float only when cpu_offload is enabled, so mul_ receives a scalar on the CPU path.

Per Codex review: an unconditional float() would read a CUDA scalar back to Python when ZeRO-1/2 runs without CPU offload and clip_grad > 0, forcing a device sync every step. The GPU clipping path stays on-device.

2. DeepSpeedCPUAdam — per-group malloc_trim(0) (cpu_adam.py)

Call malloc_trim(0) once per param group / subgroup on that group's first step.

Per Codex review: ZeRO-2 CPU offload invokes step() once per bit16_groups entry. An optimizer-wide “first step” flag would skip trim for later groups after group 0. Per-group tracking fixes that.

This is a best-effort glibc RSS mitigation (Linux-only no-op elsewhere), not a correctness fix.

3. Tests (test_cpu_offload_memory.py)

  • per-group trim call count
  • CPU-offload float unscale path keeps storage
  • non-offload path still scales correctly
  • LF line endings

Removed after review

  • Dropped the ds_adam_step is_contiguous() guard in cpu_adam_impl.cpp. As @delock pointed out, Tensor.contiguous() already returns self when the tensor is contiguous, so the explicit check was a no-op.

Test plan

  • test_malloc_trim_helper_exists
  • test_malloc_trim_called_per_param_group
  • test_cpu_adam_first_step_initializes_optimizer_states
  • test_unscale_and_clip_grads_cpu_offload_uses_float_scale
  • test_unscale_and_clip_grads_no_clip
  • test_unscale_and_clip_grads_gpu_path_skips_float_conversion

Notes

Addresses #7693

@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: 9ef04cf853

ℹ️ 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 thread deepspeed/runtime/zero/stage_1_and_2.py Outdated
# result buffer even for 0-dim tensors, pinning ~4 bytes/param in the
# CPU allocator cache that is never reused by later allocations.
# mul_(float) is guaranteed in-place. See issue #7693.
combined_scale = float(combined_scale)

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 Keep CUDA clipping scale on device

When ZeRO-1/2 runs without CPU offload and clip_grad > 0, scaled_global_norm() produces total_norm on the accelerator and this unconditional float() reads the CUDA scalar back to Python, forcing a device synchronization for every optimizer step/param group before unscaling gradients. The allocator leak described here is CPU-specific, so limiting the conversion to CPU gradients/scales avoids regressing the normal GPU ZeRO gradient-clipping path.

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.

Thanks — agreed. Updated so float(combined_scale) only runs when self.cpu_offload is true, keeping the GPU ZeRO clipping path on-device and avoiding the sync regression.

Comment thread deepspeed/ops/adam/cpu_adam.py Outdated
Comment on lines +189 to +191
if not getattr(self, '_first_step_done', False):
self._first_step_done = True
_malloc_trim()

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 Trim after every ZeRO group’s first CPU step

In ZeRO-2 CPU offload, stage_1_and_2._optimizer_step() narrows self.optimizer.param_groups to a single group and calls DeepSpeedCPUAdam.step() once per bit16_groups entry. With multiple optimizer param groups, this optimizer-wide flag is set after group 0, so later groups still lazily allocate their optimizer state and temporary buffers but skip _malloc_trim(), leaving the RSS waste this patch is meant to reclaim for all parameters outside the first group.

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.

Thanks — agreed. Replaced the optimizer-wide _first_step_done flag with per-group / per-subgroup _trimmed_groups tracking so each ZeRO param group still gets malloc_trim after its first lazy state init.

@liuyun7345 liuyun7345 force-pushed the fix/cpu-offload-memory-leak-7693 branch from 9ef04cf to 8e4b0d3 Compare July 11, 2026 16:20
@liuyun7345 liuyun7345 changed the title fix(zero-offload): reduce CPU memory leaks in ZeRO-2 optimizer offload (#7693) fix(zero-offload): mitigate CPU RSS waste on ZeRO-2 optimizer offload (#7693) Jul 11, 2026
Comment thread csrc/adam/cpu_adam_impl.cpp Outdated
auto params_c = params.is_contiguous() ? params : params.contiguous();
auto grads_c = grads.is_contiguous() ? grads : grads.contiguous();
auto exp_avg_c = exp_avg.is_contiguous() ? exp_avg : exp_avg.contiguous();
auto exp_avg_sq_c = exp_avg_sq.is_contiguous() ? exp_avg_sq : exp_avg_sq.contiguous();

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.

From this document, this change should not be necessary, PyTorch ensures return the same tensor if the tensor is already contiguous.
https://docs.pytorch.org/docs/2.13/generated/torch.Tensor.contiguous.html

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.

Thanks @delock — you're right. Tensor.contiguous() already returns self when the tensor is contiguous, so the explicit is_contiguous() check is a no-op. I'll remove this change from the PR.

Addresses deepspeedai#7693 with targeted mitigations for CPU-offload paths:

1. unscale_and_clip_grads: convert combined_scale to float only when
   cpu_offload is enabled, avoiding a GPU device-sync regression on the
   normal ZeRO-1/2 clipping path (Codex review).
2. DeepSpeedCPUAdam: call malloc_trim(0) once per param group/subgroup on
   its first step so later ZeRO groups also release RSS after lazy state
   init (Codex review).

Also adds regression tests (LF line endings).

Dropped the ds_adam_step .contiguous() guard: Tensor.contiguous() already
returns self when contiguous (review feedback from @delock).

This is a best-effort mitigation, not a complete resolution of all CPU
memory overhead tracked in deepspeedai#7693 (e.g. H2D tiling remains open).

Signed-off-by: liuyun7345 <liuyun7345@sina.com>
@liuyun7345 liuyun7345 force-pushed the fix/cpu-offload-memory-leak-7693 branch from 8e4b0d3 to b3ed969 Compare July 12, 2026 05:15
# float() would force a device sync every step. The allocator concern in
# #7693 is CPU-specific, so keep the GPU path on-device. See #7693 / #8132.
if self.cpu_offload:
combined_scale = float(combined_scale)

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.

I didn't observed the 4byte/element memory usage from combined_scale. The pytorch version I use is torch 2.11. Did you observe this memory bloat on your environment? Thanks!

@delock

delock commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Hi @liuyun7345 thanks for tracking this issue and do the best effort fix. Yet I found some claims in the original issue may not be the real reason. More over, on a single 5090 system with CPU offload, we didn't observe significant RSS change before and after the PR. Did you observe RSS change after applying the patch?

I would suggest to digg deeper to find the real reason for RSS usage. If the cause claimed in #7693 is not true, then we should avoid adding these complexity for no return.

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