Repository navigation
Conversation
Under ZeRO CPU offload, Muon copies every momentum to the accelerator and back each step, but the momentum was the one buffer crossing PCIe that ignored offload_optimizer.pin_memory. Allocate it pinned in ZeRO-1/2 and ZeRO-3, and re-pin it after a checkpoint load replaces it with pageable tensors. The update and momentum of each batch (ZeRO-1/2) or parameter (ZeRO-3) are now copied back to the host on a side stream while the next one is gathered and orthogonalized. Results are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
Open
4 of 9 tasks
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.
Speeds up Muon under ZeRO CPU optimizer offload (ZeRO-1/2/3) without changing results: the momentum now honors
offload_optimizer.pin_memory, and the host writeback of each update overlaps the next orthogonalization. Part of the roadmap in #8463.Motivation
With CPU offload, Newton–Schulz runs on the accelerator, so every step copies each Muon gradient and momentum to the device and the update and momentum back. Every other buffer that crosses PCIe on this path is pinned when
pin_memoryis set; the Muon momentum was not:torch.zeros_like, ZeRO-3 withtorch.zeros(unpinned_fp32_buffer_momentum).This is the same per-step data flow as #7939, which added Muon CPU offload to ZeRO-1/2 and was closed because the offload path was too slow (cc @delock). That design is now on master; this PR targets its two costs, the pageable momentum copies and the serialized writeback.
Pageable copies ran at ~10 GB/s here versus ~43 GB/s H2D / ~57 GB/s D2H pinned. The writeback was also strictly serialized with the next batch's gather and Newton–Schulz.
Changes
stage_1_and_2.py,stage3.py): allocate it withpin_empty(_like)whenoffload_optimizer.pin_memoryis set, and re-pin it at the point of use if it is not pinned (after a checkpoint load). ZeRO-3 momentum that the NVMe swapper manages is left to the swapper; thesave_muon_momentum_buffer_in_memorycopy is pinned.zero/utils.py: OverlappedHostWriteback): the update and momentum of batch i (ZeRO-1/2) or parameter i (ZeRO-3) are copied to the host on a side stream after batch i+1 has been queued, so the D2H overlaps the next gather and Newton–Schulz. Dependencies are enforced with host-sideEvent.synchronize()rather than a cross-stream wait, since on XPU a stream waiting on another stream's event serialized the two queues (0 overlap in a trace). Sources stay referenced until their copy completes (at most one batch computed and one in flight), and the copies are drained before the host reads them (end of the update, and before an NVMe swap-out). Synchronized devices (CPU accelerator) copy inline as before.The update math is untouched; results are bitwise identical to master.
Tradeoffs
pin_memory.Validation
Hardware: 8× Intel Arc Pro B70 (XPU), 2× Xeon Platinum 8457C, 1 TB RAM. Software: torch 2.14.0.dev20260804+xpu, Level Zero / compute runtime 26.27.39122.11.
Tests
New
TestMuonOffloadPinnedMomentumintests/unit/v1/ops/muon/test_muon_checkpoint.py(ZeRO 1/2/3 ×pin_memoryTrue/False, world size 2, bf16): after training, the momentum is pinned exactly whenpin_memoryis set; aftersave_checkpoint→load_checkpoint→ training it is pinned again, and the resumed run matches the uninterrupted one.pin_memory=True, stages 1/2/3), 3 passedFull Muon suite (
cd tests && pytest unit/v1/ops/muon/): 250 passed, 10 skipped, 4 failed. The same 4 fail on master on this machine and are unrelated to offload:test_muon_overflow.py::TestMuonMatchesZeroStage0::test_every_matrix_keeps_its_momentum[1,2](9.7e-4 vs the 1e-5 bound) andtest_per_head_muon_accelerator.py::test_per_head_on_a_shard_is_the_shard_of_per_head[2-gram,2-standard](bitwise equality of sharded vs whole Newton–Schulz on XPU).Performance
Qwen3-4B shapes (randomly initialized, 36 layers, 4.02B parameters of which 3.63B Muon), seq 2048, micro-batch 1, bf16, gradient checkpointing, gradient clipping 1.0,
offload_optimizer: {device: cpu, pin_memory: true}, 1 GPU, one socket (48 cores,numactl -C 48-95 -m 1). Per run: mean of steps 2–6; 3 runs per cell. "Muon apply" is_apply_muon_updates_cpu_offload(gather, Newton–Schulz, writeback), timed with device synchronization.Breakdown of the two changes (same setup, this PR's code with one change disabled; 3 runs per cell). Pin only forces the writeback synchronous; overlap only leaves the Muon momentum pageable while every other offload buffer stays pinned. The momentum pin status was checked in every run.
Pinning is the larger effect. The overlap adds a further 0.47 s (ZeRO-2) and 0.35 s (ZeRO-3) of Muon apply on top of pinning. Under ZeRO-2 that shows up in the iteration time (5.94 → 5.68 s). Under ZeRO-3 the iteration mean does not move (6.28 s in both); the gain is within this setup's iteration noise. The +0.23 GiB ZeRO-3 peak comes from the overlap (the in-flight parameter), not from pinning.
Why ZeRO-3 peaks higher than ZeRO-2 here (pre-existing, same on master). Peak device memory per phase, this PR, single GPU:
sub_group_size: 2.5e8With one rank the ZeRO-3 partition is the whole model, so forward and backward match ZeRO-2. The difference is in the step: ZeRO-3's Muon offload apply copies the gradients and momentum of a whole sub-group to the device and gathers them at once (default
sub_group_size1e9 elements), while ZeRO-1/2 processes Muon parameters in batches bounded by a gather budget. Withsub_group_size: 2.5e8the step peak drops to 11.86 GiB, and the iteration time is unchanged (6.10 s). Batching the ZeRO-3 apply the same way as ZeRO-1/2 is left out of this PR; with more ranks the per-rank partitions shrink but the gathered full gradients and momentum of a sub-group do not.Correctness of the overlap: in the same process, the Muon apply was run twice on identical host state, once with the writeback forced synchronous and once overlapped, and the resulting gradients and momentum were compared with
torch.equal(ZeRO-2 and ZeRO-3, 4 layers). They are bitwise equal on every step after the first. On the first step two synchronous runs already differ from each other by the same amount (max 4.9e-4 in one gradient tensor), so that is first-call nondeterminism of the existing path, not the overlap. Losses are not bitwise reproducible across separate processes on master either (they differ from step 1 on), so cross-run loss equality is not used as evidence; the trajectories of master and this PR stay within master's own run-to-run spread.Reproduction
The benchmark builds
Qwen3ForCausalLMfromtransformerswith the shapes above, wraps_apply_muon_updates_cpu_offloadwith a synchronized timer, and runs 7 steps:Not validated: NVMe optimizer offload with Muon, and multi-node runs.