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
| 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.
There was a problem hiding this comment.
Good question. I traced it statically too (no GPU here), and I read None as unreachable on that path, so the assert holds.
copy_grads_in_partition is only called from the ipg-bucket drain at stage_1_and_2.py:1761 and :1764, so a param only gets there after it produced a gradient this backward. _fill_param_grad_accum_attribute (:1133) then sets grad_accum for every param whose param.grad is not None.
The stronger signal is inside the same block: async_accumulate_grad_in_cpu_via_gpu already dereferences grad_accum with no guard at :1547 and :1559, and it runs before the copy in every case except micro_step_id == 0 at the accumulation boundary. If None were reachable there, that would raise first.
So I read :1589s fallback and :1509s guard as defensive leftovers rather than live cases, which matches what you found. Leaving the diff as is.
|
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 |
75c3d3c to
42edcf9
Compare
`ZenFlowZeroOptimizerParallel.async_inplace_copy_grad_to_fp32_buffer_from_gpu` diverges from the base ZeRO-1/2 method it overrides in two ways. It branches on `grad_accum is None` and then calls `grad_accum.view(-1)` in both arms, so the None case raises `AttributeError: 'NoneType' object has no attribute 'view'` from inside the copy rather than the assertion the base 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. It also ends with `param.grad = None` where the base calls `self.clear_grad_attribute(param)`. The copy reads through `get_param_gradient_attribute`, so with `use_grad_accum_attribute` it consumes `param.grad_accum` and then clears `param.grad`, which the base has already set to None. The consumed gradient stays live and the next micro step's `_fill_param_grad_accum_attribute` adds onto it. Apply the base's resolution to both. Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
42edcf9 to
97c521b
Compare
|
Rebased on master and widened: the same override also clears the wrong gradient attribute. The base ends with Driving the two methods out of the source files with a stub, since ZenFlow itself needs an accelerator:
The copied buffer is
I have not been able to run a full ZenFlow ZeRO-1 job with a separate |
ZenFlowZeroOptimizerParallel.async_inplace_copy_grad_to_fp32_buffer_from_gpuoverrides the ZeRO-1/2 copy and diverges from it in two places.1. The dead
grad_accum is NonebranchBoth arms are the same expression, so the arm meant to handle a 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 declares.DeepSpeedZeroOptimizer.async_inplace_copy_grad_to_fp32_buffer_from_gpucarried the identical four lines until Coverity flagged them, and 1a8ad24 (#7431, 2025-08-02) replaced them withassert grad_accum is not None.zenflow_stage_1_and_2.pylanded thirteen days later in #7391, written against the pre-#7431 base, so it reintroduced the branch.2. Clearing the wrong gradient attribute
The base ends with
self.clear_grad_attribute(param); the override ends withparam.grad = None. Both read throughget_param_gradient_attribute, so underuse_grad_accum_attribute(ZeRO-1 with agrad_accum_dtypethat differs from the parameter dtype) the copy consumesparam.grad_accumand then clearsparam.grad, which_fill_param_grad_accum_attributehas already set to None. The consumed gradient stays live, and the next micro step'sparam.grad_accum.add_(...)accumulates onto it.Verification
ZenFlow needs an accelerator, so the two methods were driven out of the source files with a stub:
AttributeError: 'NoneType' object has no attribute 'view'AssertionError, as the base declaresparam.grad_accumafter the copytensor([1., 1., 1., 1.])NoneThe copied buffer is
[1.0, 1.0, 1.0, 1.0]either way, so the copy itself is unchanged.Test
Two cases in
tests/unit/runtime/zenflow/test_zf.py, plain functions next to the existingtest_num_selected_columns_has_nonzero_floor, neither needing an accelerator: one asserts the missing-gradient contract, one asserts the consumed attribute is cleared. Both use the realget_param_gradient_attributeandclear_grad_attributeoffDeepSpeedZeroOptimizerso the override is checked against the base behaviour rather than a copy of it.yapf --diffandflake8with the repo's own.style.yapfand.flake8are clean on both files.Scanning the rest of
deepspeed/runtime/zenflow/turns up no other branch of either shape.I have not run a full ZenFlow ZeRO-1 job with a separate
grad_accum_dtypeend to end, so part 2 rests on the base-class contract rather than on a measured loss curve. Happy to split it out if you would rather review the two separately.