Repository navigation
perf(optim): overlap the offloaded AdamW step with the next forward (builds on #4008) - #4427
Draft
faresobeid wants to merge 15 commits into
Draft
faresobeid wants to merge 15 commits into
faresobeid wants to merge 15 commits into
Conversation
…of the chunked-step gate
# Conflicts: # tests/unit/train/test_state_offload.py
The chunked CPUOffloadOptimizer step returns before its chunks run. They update on a side stream while the next forward runs, enqueued as the forward reaches them (8 up front, then 4 ahead of the forward), so the CPU never fills the CUDA launch queue. Each FSDP parameter group waits for the chunk holding its parameters before its all-gather. The step's hyperparameters and gradients are captured at step time, so the math is unchanged. model.offload_overlap_forward (default true) turns it on for AdamW. Anything else that reads the parameters calls wait_for_optimizer_step first: weight broadcasts in the SFT and RL loops; checkpoint saves already wait through prepare_checkpoint_save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
model.offload_overlap_forward now defaults to false. The overlapped step and the pre-broadcast wait have not been exercised in an RL run yet, nor with sparse weight deltas; enable it explicitly until they have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erlap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se the DTensor local helpers in _move_states Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.
Builds on #4008 (this branch is #4008's head plus two commits, merged with main). With
model.offload = "optimizer", AdamW andmodel.offload_overlap_forward = true, the chunked state copies of the optimizer step run during the next forward, so the step no longer adds its full copy time to every training step.Opt-in for now:
model.offload_overlap_forwarddefaults tofalse, which keeps #4008's synchronous chunked step. It stays off until an RL run has exercised the wait before the weight broadcast and it has been tested with sparse weight deltas (#4085).Why the step was still slow
On 8xH200, the AdamW states are 28.4 GiB per rank for Qwen3-30B-A3B. Each step moves them to the GPU and back, 56.9 GiB per rank. With all 8 GPUs copying at once, each GPU gets 26 GiB/s in one direction or 38.8 GiB/s with both directions at once, and only if its pinned buffers sit on its own socket (the NUMA fix is #4426). Each pair of GPUs shares one PCIe Gen5 x16 uplink (52 GiB/s per direction per pair), and each socket tops out near 155 GiB/s with both directions running. That puts a floor of 1.47 s on the copies, while the GPU math takes about 75 ms. #4008 reaches that floor (1.48 s with #4426), but the GPU still does nothing else for those 1.5 s of every ~3.9 s step.
Change
optimizer.step()returns as soon as it has queued the first 8 chunks. The rest are queued as the next forward reaches them, staying 4 chunks ahead of it. Queuing all ~33 chunks at once filled the CUDA launch queue and blocked the CPU for about 1 s, which stopped any overlap.unshardand on the current stream inwait_for_unshard. This wrapsFSDPParamGroup.unshard/wait_for_unshardper group (FSDP2 internals) and covers each mesh's group, e.g. EP experts. If a parameter is not managed by FSDP, the step logs a warning and runs synchronously as in perf(optim): overlap optimizer state offload copies with the AdamW step #4008.zero_grad. Gradients arerecord_streamed onto the update stream so their memory is not reused early.model.offload_overlap_forward(defaultfalse, opt-in; AdamW only).wait_for_optimizer_step(optimizer)drains a step in flight. It runs before the weight broadcasts in the SFT and RL loops. Checkpoint save/load andstate_dictdrain through_move_states.Numbers
Rows labelled "this PR" are with
offload_overlap_forward = true; with the default (false) the step runs as in #4008.Qwen3-30B-A3B, 1 node 8xH200, FSDP 8 / EP 8, 16k seq, micro batch 1, global batch 8, full AC, compile, fake data. tok/s is the mean of the last 10 of 20 steps; step time is the mean of steps 6-20. "NUMA" means bound to the GPU's socket as in #4426.
offload = "optimizer", no activation offloading:offload_overlap_forward = false(default)offload = "optimizer"plus activation offloading (the default config otherwise):Without NUMA binding, results depend on which socket each rank's buffers happened to land on (3 to 5 of 8 ranks were remote in these runs). Host RSS is 41 GiB per rank (45 GiB with activation offload) in every arm. Peak GPU memory rises by 0.4 GiB, because gradients live into the next forward until their chunk has updated. The #4154 row shows that #4154's bounded first step lowers the peak recorded in steady state. That is independent of this change.
Trace of the NUMA run (rank 0, one step): the copies start as soon as
step()returns. They run at about 95% of the link while the trainer finishes the step's bookkeeping, then at 70-80% under the forward. The forward stretches from 0.64 s to 1.56 s because it waits for its chunks. Backward is unchanged at 1.54 s.Validation
pytest -m "not gpu" tests/unit/train tests/unit/test_configs.pypasses, excepttest_cpu_adam.py, which needsninjaon the machine I used.ruff checkandruff format --checkpass. The RL loop's new wait before its weight broadcast was not exercised at runtime.🤖 Generated with Claude Code