feat: chunked per-layer optimizer step with stream-overlapped CPU offload#3109
Open
samsja wants to merge 6 commits into
Open
feat: chunked per-layer optimizer step with stream-overlapped CPU offload#3109samsja wants to merge 6 commits into
samsja wants to merge 6 commits into
Conversation
…load The CPUOffloadOptimizer now performs the optimizer.step() per-transformer-layer instead of all-at-once. Each layer's optimizer states are moved to GPU, the step runs for that layer only, and states are moved back to CPU before the next layer. This bounds peak GPU optimizer-state memory to ~one layer's worth (plus one prefetched layer for overlap) instead of the full model's, preventing OOM when weight + grad + all_opt_states exceeds available VRAM. H2D and D2H transfers are pipelined on dedicated CUDA streams so the next layer's states are prefetched while the current layer computes, and the previous layer's states are evicted while the next layer computes. Pinned-memory D2H is fixed to pre-allocate the pinned destination and async-copy into it, avoiding a race where pin_memory() reads a tensor whose async copy hasn't completed. Muon's per-group step counter is synced after all chunks complete, since the param_groups are temporarily swapped during chunked stepping.
Add and config options to independently toggle per-layer chunking and stream-overlapped H2D/D2H. When stream is disabled, the chunked step uses a simple sequential move→step→move loop with no CUDA stream logic, making the code path clearer and easier to debug.
samsja
marked this pull request as ready for review
July 22, 2026 20:07
1. Async D2H checkpoint corruption (HIGH): Add torch.cuda.synchronize()
after _move_states('cpu') in state_dict() and in ckpt.py's
AppState.state_dict() to ensure async pinned D2H copies complete
before CPU tensors are read.
2. Incomplete stream dependency chain (MEDIUM): Replace wait_stream-only
finish in _step_chunked_streamed with torch.cuda.synchronize() to
block CPU. Add h2d_stream.wait_stream(d2h_stream) before prefetching
chunk i+1 to prevent racing with the previous chunk's D2H into the
same pinned CPU buffers.
3. First step OOM (HIGH): The init step now uses the same chunked path
instead of a full optimizer.step() that materializes all states on
GPU at once. States are created per-chunk and immediately evicted.
4. Defaults disagree (MEDIUM): Align setup_optimizer defaults for
cpu_offload_chunked and cpu_offload_stream with config defaults
(both False).
Also add torch.cuda.synchronize() at the end of _step_chunked (no-stream
path) to ensure async D2H completes before returning.
Remove the 'if step in group' guard so _sync_step_counters always writes group['step'] = orig_step + 1. This handles optimizers that lazily create the step key on per-chunk copies (e.g. Muon) where the original groups might not have had the key set yet.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 053beda. Configure here.
Pass closure only to the first chunk's optimizer.step() and None to the rest, so loss/grad recomputation happens once per training step rather than once per layer.
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.

Summary
The
CPUOffloadOptimizernow optionally performsoptimizer.step()per-transformer-layer ("chunked") instead of all-at-once, with optional stream-overlapped H2D/D2H transfers. Each layer's optimizer states are moved to GPU, the step runs for that layer only, and states move back to CPU before the next layer.Problem
With the old all-at-once offload, the entire model's optimizer states land on GPU simultaneously during
step(). Whenweight + grad + all_opt_states > VRAM— even without activations — this causes OOM on large models.Config
Two new config options (both default
false— opt-in):optim_cpu_offload_chunked = false: all-at-once offloading (loads full model states to GPU in one shot)optim_cpu_offload_stream = false: chunked step without CUDA stream overlap — simple sequential loop, no stream objects in the code pathImplementation
layers.{N}.). Non-layer params (embeddings, lm_head, norm) go into a trailing "misc" chunk._step_chunked_streamed): H2D/D2H on dedicated CUDA streams; next layer prefetched while current computes; previous layer evicted while next computes. Ends withtorch.cuda.synchronize()to block CPU._step_chunked): Simple sequentialmove_to_gpu -> step -> move_to_cpuper chunk. No CUDA stream objects.state_dict()and ckpt paths sync after D2H to prevent reading incomplete copies.stepinteger synced after all chunks sinceparam_groupsare temporarily swapped.VRAM impact (synthetic benchmark, float32, AdamW)
Validation
base_optimizer,state,param_groups,zero_grad,state_dict,load_state_dictAPIs unchangedBugbot issues addressed
torch.cuda.synchronize()after_move_states("cpu")instate_dict()andckpt.pytorch.cuda.synchronize()at end of streamed step;h2d_stream.wait_stream(d2h_stream)before prefetch to prevent pin buffer racesoptimizer.step()setup_optimizerdefaults with config (false)