A zero-element parameter loses its shape when it is bound to a flat buffer - #8467
alanhuangyoo wants to merge 3 commits into
Conversation
|
Scope widened after opening this: ZeRO-1/2 was not the only place. Measured on an H20 against unmodified master (
Test is now parametrized over those five configurations: 15 passed here, 15 failed on master. Regression on the same box with |
`_update_model_bit16_weights` repoints every parameter at its slice of the
flattened group:
updated_params = self.unflatten(self.bit16_groups_flat[i], self.round_robin_bit16_meta[i])
for p, q in zip(self.round_robin_bit16_groups[i], updated_params):
p.data = q.data
torch's `unflatten_dense_tensors` special-cases a zero-element tensor and returns
a freshly allocated 1-D `zeros({0})` instead of a view of the requested shape:
>>> _unflatten_dense_tensors(_flatten_dense_tensors([a, b]),
... [torch.zeros_like(a, device="meta"), # (8, 8)
... torch.zeros_like(b, device="meta")]) # (0, 8)
[(8, 8), (0,)]
So a `nn.Linear(8, 0, bias=False)` weight came out of `deepspeed.initialize` with
shape `(0,)` instead of `(0, 8)`, and the module's own forward then dispatched
`F.linear` to `addmv`:
RuntimeError: size mismatch, got input (1), mat (1x8), vec (0)
The parameters are rebuilt from the flat buffer after every `step()` as well as at
init, so restoring the shape once would not have held either.
Skip the assignment for a zero-element parameter. There is no slice of the flat
buffer for it to point at, and the tensor torch hands back is a fresh allocation
rather than a view, so nothing is being kept in sync by the assignment.
Stages 1 and 2 only. ZeRO-3 keeps the real shape in `ds_shape` and partitions to a
1-D local shard by design; its own zero-element failure is a different one.
Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
ZeRO-1/2 was not the only one. The same `p.data = q.data` over
`unflatten_dense_tensors` output appears in FP16_Optimizer and BF16_Optimizer,
and all three drop a zero-element parameter's shape:
fp16 + stage 0 FP16_Optimizer (0,) step: RuntimeError
bf16 + stage 0 FP16_Optimizer (0,) step: RuntimeError
bf16 + stage 1 + fp32 accum BF16_Optimizer (0,) step: RuntimeError
So the failure does not need ZeRO at all — any run with fp16 or bf16 enabled and a
zero-element parameter breaks on the first forward after `initialize`.
Move the guard into `bind_flat_views` in runtime/utils.py and call it from all
four binding sites, so the reason is written once. FP16_Optimizer's third site
copies rather than rebinds; without the guard that copy would start raising on
the shape mismatch once the earlier sites stop corrupting the shape, so it takes
the same skip inline.
Test parametrized over the five configurations, one per wrapper.
Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
09d6f71 to
f23ef1d
Compare
deepspeedai#8277 rewrote ZeRO-1/2's _update_model_bit16_weights to bind each parameter with narrow(...).view(meta.shape), which keeps a zero-element parameter's shape, so stage_1_and_2.py takes master's version and no longer needs bind_flat_views. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
ebarkhordar
left a comment
There was a problem hiding this comment.
I ran your tests at 990c49c and at the merge base b5e000c in a clean container (torch 2.8.0+cpu, deepspeed.__file__ printed each run). CPU, so this is the four non-fp16 cases.
Base b5e000c: 6 failed, 6 passed. bf16_stage0 and bf16_stage1_fp32_accum fail all three, with the signature from your description:
RuntimeError: size mismatch, got input (1), mat (1x8), vec (0)
zero1 and zero2 pass at the base, so the #8277 attribution holds up. Head 990c49c: 12 passed. The parameter bind_flat_views skips also matches the bound one, empty.weight (0, 8) bfloat16 cpu beside dense.weight (8, 8) bfloat16 cpu, so skipping does not strand it on another dtype or device.
On "every wrapper that flattens the parameters": I parsed the package for unflatten call sites rather than grepping. 16, of which the ones that bind the result back are your three, stage_1_and_2.py:857 (already narrow().view() from #8277), and four in stage3.py. Those four bind fp16_partitioned_groups, which is [param.ds_tensor ...] at stage3.py:899 and 1-D, so there is no shape there to lose. The set looks closed.
One question. Stage 3 is the wrapper missing from your table, and EmptyTailModel under it fails at BOTH refs, identically:
AssertionError: {'id': 1, 'status': 'NOT_AVAILABLE', 'numel': 0,
'ds_numel': 0, 'shape': (0,), 'ds_shape': (0, 8), 'persist': True}
ds_shape is intact, so that is a different bug and your diff does not touch stage3.py. Worth a line saying stage 3 is out of scope?
No GPU here, so fp16 and anything multi-rank is untested by me.
|
Answering my own question, since I should have checked before asking it: #8375 is already open for the stage 3 case, with the same assertion text, and its description says the ZeRO-1/2 shape loss is fixed separately. So the scope split here is deliberate and nothing needs adding to this body. The measured half of my comment stands. |
A zero-element trainable parameter loses its shape when it is bound to a flattened
parameter group, and the model's own forward then fails. This does not need ZeRO — every
wrapper that flattens the parameters does it.
nn.Linear(8, 0, bias=False)in a model,deepspeed.initialize, one step:empty.weightafter initfp16+ stage 0FP16_Optimizer(0,)RuntimeErrorbf16+ stage 0FP16_Optimizer(0,)RuntimeErrorbf16+ stage 1 + fp32 accumBF16_Optimizer(0,)RuntimeErrorZeRO-1/2 had the same failure until #8277, which binds with
narrow().view(meta.shape)and so keeps the shape. The tests still cover them.That
addmvsignature is the giveaway —F.linearonly dispatches there when the weightis 1-D.
dense.weightnext to it is still(8, 8).Root cause
Each wrapper repoints its parameters at their slices of the flat buffer with the same two
lines, e.g.
FP16_Optimizer.__init__:The shapes handed to
unflattenare right (here they are the parameters themselves),but
torch.unflatten_dense_tensorsspecial-cases a zero-element tensor and returns afreshly allocated 1-D
zeros({0})rather than a view of the requested shape:(ATen's
unflatten_dense_tensorsreturnsat::zeros({0}, flat.options())fornumel == 0instead of narrowing and viewing.)
Assigning that to
p.datareplaces the parameter. The binding runs after everystep()aswell as at init, so restoring the shape once would not have held: the second iteration's
forward would break instead of the first.
The change
bind_flat_views(tensors, views)inruntime/utils.pyskips a zero-element tensor, and thethree binding sites call it:
fp16/fused_optimizer.py— init, and afterstep_fused_adambf16_optimizer.py—_update_storage_to_flattened_tensorThere is no slice of the flat buffer for such a tensor to point at, and what torch returns
is a fresh allocation rather than a view, so the assignment was not keeping anything in
sync either.
FP16_Optimizer.stephas a fifth site that copies rather than rebinds(
p.data.copy_(q.data)). Once the earlier sites stop corrupting the shape, that copy wouldstart raising on the
(0, 8)vs(0,)mismatch, so it takes the same skip inline.Nothing changes for a parameter with elements: the
ziporder and the narrow/view path areuntouched.
Testing
tests/unit/runtime/zero/test_zero_numel_param_shape.py, parametrized over the fiveconfigurations above — the shape after
initialize, the shape after a step, and a secondstep running at all (the case that would survive a fix applied only at init).
On an H20, torch 2.9.1+cu128:
Re-run after merging master (b5e000c):
Regression, same box,
DS_SKIP_CUDA_CHECK=1so the CPU-Adam builds —test_stage2_flatten_on_gpu.py,test_zero_tensor_fragment.py,test_zero_coalesce_grad_reduction.py: 174 passed, 75 skipped, 0 failed.Related
#8280 and #8298 (issues #8279, #8297) fixed the ZeRO-1/2 reduction path for zero-element
parameters. This is the parameter-binding path, which those did not reach.
ZeRO-3 is untouched: it partitions to a 1-D local shard and keeps the real shape in
ds_shapeby design, so(0,)there is correct. Its own zero-element failure is theall-gather gate in #8375, a different bug in a different file.