Fix the dead grad_accum branch in the ZenFlow gradient copy - #8360
Fix the dead grad_accum branch in the ZenFlow gradient copy#8360vineethsaivs wants to merge 1 commit into
Conversation
`ZenFlowZeroOptimizerParallel.async_inplace_copy_grad_to_fp32_buffer_from_gpu` branched on `grad_accum is None` and then called `grad_accum.view(-1)` in both arms, so the None case raised `AttributeError: 'NoneType' object has no attribute 'view'` from inside the copy rather than the assertion the base ZeRO-1/2 method declares. This is the same defect Coverity flagged in the base copy, fixed there by 1a8ad24 (deepspeedai#7431) two weeks before this override was added in deepspeedai#7391, which was written against the older base and so carried it forward. Apply the same resolution here. Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
| src_tensor = grad_accum.view(-1).narrow(0, source_offset, num_elements) | ||
| else: | ||
| src_tensor = grad_accum.view(-1).narrow(0, source_offset, num_elements) | ||
| assert grad_accum is not None |
There was a problem hiding this comment.
Static review, no GPU here, so I parsed rather than ran it.
One question about assert on the copy_grads_in_partition path. ZenFlowZeroOptimizerParallel overrides neither set_norm_for_param_grad_in_gpu nor update_offload_overflow_tracker_for_param_grad, so stage_1_and_2.py:1672-1676 runs three calls back to back on the same param and all three read get_param_gradient_attribute(param):
:1589treatsNoneas reachable and falls back toparam.grad:1509guards withif grad is not None- the method you patch now asserts it is not
None
At the other call site the assert is clearly safe: _finalize_cpu_offload_gradient_accumulation calls _restore_cpu_offload_grad_to_gpu first, which assigns the attribute at :1024, and only then runs the same three at :1007-1009. I could not reach that conclusion for copy_grads_in_partition, where nothing between the reduction and :1676 sets it.
So are the fallback at :1589 and the guard at :1509 dead on that path, or can grad_accum still be None there for the stage 1 plus separate accumulation dtype case :363-366 enables? The disagreement predates your PR, so this is not a request to change the diff.
On your closing line: parsing all 658 files under deepspeed/ for if/else nodes whose arms unparse identically leaves only writer_factory.py:22, stage_1_and_2.py:1731 and pipe/module.py:418, none of which dereference what they test.
|
Good question, and the answer turns out to be that the three do not disagree about reachability. Two of them assume The In mode B the fallback is a tautology: So On whether it can be if self.use_grad_accum_attribute:
self._fill_param_grad_accum_attribute(param)before the reduction that eventually reaches Your reading of the The For this diff specifically, none of it changes the risk. The On the scan: that matches mine exactly. |
|
That answers it, thanks. The fill in One thing to add, still static, no run on my side.
Both are still safe, but by a different mechanism than the one you describe. Each guards with On |
|
Checked your call-site analysis against current
So three of the six reach The part worth pinning down is why those guards are a precondition for the assert rather than a coincidence. The two accessors bottom out on the same attribute under the same flag: def get_gradient_for_reduction(self, param): # what the caller guards read
if self.use_grad_accum_attribute:
return param.grad_accum.to(self.dtype) if param.grad_accum is not None else None
else:
return param.grad
def get_param_gradient_attribute(self, param): # what the assert checks
return param.grad_accum if self.use_grad_accum_attribute else param.grad
Worth noting So: three routes into Agreed on |
|
Filed as #8371, with the mode-by-mode reproduction and the |
ZenFlowZeroOptimizerParallel.async_inplace_copy_grad_to_fp32_buffer_from_gputestsgrad_accum is Noneand then dereferences theNoneit just tested for:Both arms are the same expression, so the branch does nothing, and the arm that is supposed to handle the missing gradient is the one that cannot run: it raises
AttributeError: 'NoneType' object has no attribute 'view'from inside the copy instead of the assertion the base ZeRO-1/2 method declares.Where it came from
This is not a new defect, it is a copy of one that was already fixed.
DeepSpeedZeroOptimizer.async_inplace_copy_grad_to_fp32_buffer_from_gpucarried the identical four lines until Coverity flagged them, and 1a8ad24 ("fix issues raised by Coverity scans", #7431, 2025-08-02) replaced them with:deepspeed/runtime/zenflow/zenflow_stage_1_and_2.pywas added on 2025-08-15 by #7391, thirteen days later, and its override was written against the pre-#7431 base, so it reintroduced the branch. It has been there since. This PR applies #7431's own resolution to the override, so both copies of the method now state the same contract.Scanning the rest of
deepspeed/runtime/zenflow/turns up no other branch of this shape.Test
test_async_inplace_copy_grad_requires_a_gradientintests/unit/runtime/zenflow/test_zf.py, a plain function alongside the existingtest_num_selected_columns_has_nonzero_floor. It drives the override with a stub whose gradient attribute is unset, which is the only way to reach the branch, and needs no accelerator.yapf --diffandflake8are clean on both files.