Repository navigation
ZeRO-3: copy offloaded master weights back to the device through pinned buffer - #8840
Draft
pengdurice wants to merge 1 commit into
Draft
pengdurice wants to merge 1 commit into
pengdurice wants to merge 1 commit into
Conversation
…ed buffers Signed-off-by: pengdurice <pengduhit@gmail.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ZeRO-3: copy offloaded master weights back to the device through pinned buffers
Part of: #8672 (Q4 2026 roadmap), "Enable training of large scale models".
Summary
With
offload_optimizer.device: cpu, ZeRO-3 copies each sub-group's updated float32 master partition back to thedevice from host memory that is never pinned, even with
pin_memory: true(the default since #8207). This PR copiesit through two fixed-size pinned buffers instead, with the same values.
Duplicate check
Searched issues and PRs on 2026-10-13 ("offload pinned fp32 partition", "zero3 offload pin_memory copy",
"fp32_partitioned_groups_flat", "offload ratio pinned", "pageable offload"). Related, not duplicates: #8207 (merged:
pinned the ZeRO-3 gradient offload buffers, default
pin_memory: true; master partitions unchanged), #8256 (merged:routes pin sites through
get_accelerator().pin_memory(); this PR uses the same accelerator calls), #8618 (closed:a CPU-offload benchmark).
Sibling path: ZeRO-1/2 does not have the defect. It pins the float32 partition and casts into a pinned bf16 buffer
of the partition's size before a
non_blockingcopy (stage_1_and_2.pylines 546-558 and 2841-2844).Root cause
_create_fp32_partitions(stage3.py) builds each CPU master partition with.to('cpu', dtype=...)and.clone().detach(), which is pageable memory whateverpin_memorysays. After the CPU optimizer step,_reassign_or_swap_out_partitioned_parametersrunsfp16_flat.data.copy_(fp32_flat.data): a blocking copy from CPUfloat32 to device bf16. For a blocking copy with a dtype change, PyTorch converts on the CPU into a new pageable bf16
tensor of the whole partition, copies it from pageable memory and synchronizes.
Found in a profile (MiniMax-M3, first 8 layers, 8 H200, 75% of the optimizer on the CPU, rank 0): 13 bare
aten::copy_host-to-device copies of 576 MiB per step from pageable memory, at 3-7 GB/s, 1,648 ms per step.The change (
deepspeed/runtime/zero/stage3.py, 49 added, 2 removed lines)_reassign_or_swap_out_partitioned_parametersoffload_optimizer.pin_memoryis true, the source is on the CPU and not pinned (get_accelerator().is_pinned), and the destination is on the device; every other case keepsdst.copy_(src)_copy_through_pinned_staging(dst, src)dst's dtype (get_accelerator().pin_empty, created on first use). Per piece: wait for the previous copy out of this buffer (an event), cast the piece into the buffer on the CPU,copy_(..., non_blocking=True)to the device, record an event. Before returning, wait for the last piece__init__,_unpin_offload_buffersDS_PIN_MEMORY_BACKEND=native)Design notes:
non_blockingcopy from pinned float32 into a bf16tensor converts on the device and needs a float32 device temporary of the sub-group's size. Avoiding that needs a
pinned bf16 buffer of each partition's size, as ZeRO-1/2 keeps. Either way pinned host memory would grow with the
model, and PyTorch's pinned allocator rounds each allocation up to a power of two. The two buffers here have a fixed
size; 2**26 is a power of two, so nothing is rounded up.
partition is bitwise the same.
wait keeps what callers had before: the partition is on the device when the function returns. The next forward's
parameter all-gathers run on their own stream, and it does not wait for the main stream.
for the whole copy.
Measured
Qwen2.5-32B (all 64 layers, real weights), 1 node of 8 H200 (143,771 MiB, driver 580.126.09), torch 2.8.0+cu126.
Settings:
offload_optimizer: {device: cpu, pin_memory: true};dcd8595c2wall_clock_breakdownstep/ of which the sub-group loop (optimizer_step)The losses differ from step 2 by run-to-run variation. Two master runs differ by as much: step 2 is 0.976376 with
wall_clock_breakdownand 0.976567 without it. The flash-attention backward adds with atomics. The bitwise check isthe unit test below.
MiniMax-M3 runs (DeepSpeed master + unmerged MiniMax-M3 model code: #8658, #8652)
Settings for both: ZeRO-3 + AutoEP (expert parallel 8), real weights, each 65,536-token sequence split over 4 GPUs,
65% of the optimizer on the CPU (
offload_optimizer.ratio0.65), float32 master + Adam.(mean of steps 3-10); peak device memory 132.4 GB in both.
This change, and the fused CPU Adam change stacked on it, were in the DeepSpeed tree of our MiniMax-M3 and GLM-5.2
runs against Megatron-LM (real weights, 65,536-token sequences, 80-95% of the optimizer on the CPU):
On one node (first 4 layers of each model), master plus this PR, the fused CPU Adam PR and the two model
PRs (#8658, #8659) gives the same loss at every step as that tree.
Tests
New:
TestZero3OffloadPinnedCopyBackintests/unit/v1/zero/test_zero.py(2 ranks,ratio1.0 and 0.5,sub_group_size20,000 so partial offload has sub-groups on both sides).pin_memory: false(direct copy) and withpin_memory: true, and requires the gatheredparameters to be bitwise equal.
copy with itself.
The test is in
tests/unit/v1so that PR CI selects it. With this diff, themodal-torch-latestselector(
ci/tests_fetcher.py) picks 85 test files, includingtests/unit/v1/zero/test_zero.py. I simulated the selector'sdiff with the working-tree files, since nothing was committed.
Commands, from
tests/, on 1 node of 8 H200 (143,771 MiB, driver 580.126.09), torch 2.8.0+cu126, pytest 9.1.1:The last two ran on the first version of this patch, whose
stage3.pydiffers from this one only in two comments;the timings above also used the first version.
Compatibility
pin_memorytrue, whole optimizer on the CPUratio< 1)pin_memoryfalseDS_PIN_MEMORY_BACKEND=nativeget_accelerator()calls are used (pin_empty,is_pinned,Event,unpin_memory)No config, checkpoint or public API change.
AI assistance
This change, its tests and this description were prepared with AI assistance (Claude).