Repository navigation
Conversation
With offload_optimizer, ZeRO-1/2 checks every partition gradient for inf/NaN in update_offload_overflow_tracker(). It tested the result of has_inf_or_nan() with a Python `if`, so each gradient cost a host synchronization, although has_inf_or_nan() returns a device tensor so that callers can accumulate without one. The tracker also ran with check_grad_overflow off (bf16 with the check disabled, and fp32), where nothing reads the flag: only has_overflow() does, and it is called only when check_grad_overflow is set. The tracker now returns early when check_grad_overflow is off and otherwise ORs the result into a device flag. has_overflow() all-reduces that flag as before, so the host reads it once, after the all-reduce. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
🟡 Changes recommended
The six regression cases are outside the configured CI test scope and cannot run in the selected GPU or multirank CPU jobs, leaving the overflow and disabled-check behavior unverified by those jobs.
1 open issue
What changed in this PR
This PR reduces host synchronization in ZeRO-1/2 optimizer offload by keeping overflow tracking on the accelerator until the final collective result is read.
Changes:
- Skip gradient overflow tracking when
check_grad_overflowis disabled. - Accumulate overflow results in a device tensor and convert it for the existing all-reduce.
- Add regression tests for flag accumulation, overflow step skipping, and disabled checks; update the existing optimizer fixture.
| File | Description |
|---|---|
tests/unit/v1/zero/test_zero2_gradient_safety.py |
Initialize check_grad_overflow in the optimizer fixture. |
tests/unit/runtime/zero/test_offload_overflow_flag.py |
Add two-rank regression tests for offload overflow tracking. |
deepspeed/runtime/zero/stage_1_and_2.py |
Avoid per-gradient host truth tests and bypass disabled overflow checks. |
Review effort: Balanced
| @pytest.mark.parametrize("zero_stage", [1, 2]) | ||
| class TestOffloadOverflowFlag(DistributedTest): | ||
| world_size = 2 |
There was a problem hiding this comment.
Moved to tests/unit/v1/zero in 5257815; the selector now picks it in both workflows.
The PR CI selects tests only from tests/unit/v1 (ci/tests_fetcher.py), so the six cases under tests/unit/runtime/zero never ran there. Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>

With
offload_optimizer, ZeRO-1/2 checks each partition gradient for inf/NaN inupdate_offload_overflow_tracker(). The check had two problems.has_inf_or_nan()straight into a Pythonif.has_inf_or_nan()returns a device tensor so that callers can accumulate it without synchronizing (its docstring says so), and theifforces a sync for every gradient anyway.check_grad_overflowdisabled: bf16 with the check turned off, and fp32 training, where the engine always passesFalse. Onlyhas_overflow()reads the flag, andstep()calls it only whencheck_grad_overflowis set.Measured
Setup: one GH200, Qwen3-4B (398 parameter tensors), ZeRO-1 with CPU offload, bf16, fp32 gradient accumulation.
The backward of the accumulation-boundary micro-step showed 399
aten::itemand 404cudaStreamSynchronize, about one per parameter tensor. The absolute cost is small here, a few tenths of a second in a ~62 s training step. It grows with the number of parameter tensors, and it serializes the host with the GPU right where the boundary reduction and offload copies run.Change
update_offload_overflow_tracker()returns early whencheck_grad_overflowis off.has_inf_or_nan()into a device flag.local_overflowstarts asFalseand becomes a device bool tensor.has_overflow()turns that flag into the byte tensor it all-reduces. The host reads it once, in the existing.item()after the all-reduce.safe_set_full_gradpath (tensor_fragment.py) needs no change.Tests
New file
tests/unit/runtime/zero/test_offload_overflow_flag.py: world size 2, ZeRO stages 1 and 2 with offload, torch Adam, runs on the CPU accelerator. Three tests:__bool__); it stays a tensor, catches an inf and resets;check_grad_overflow=False, no gradient is checked.On the unchanged code, 4 of the 6 cases fail: the first and third tests, at both stages.
test_zero2_gradient_safety.pybuilds the optimizer with__new__and sets the__init__attributes by hand. It now setscheck_grad_overflowas well.The following pass on the CPU accelerator (
LOCAL_SIZE=2), 84 passed and 2 skipped:test_zero_optim_overflow.pytest_zero1_optimizer_fastpath.pytest_zero2_gradient_safety.pyyapf, flake8, codespell and the license check pass.