A zero-element parameter loses its shape when it is bound to a flat buffer - #8467
A zero-element parameter loses its shape when it is bound to a flat buffer#8467alanhuangyoo wants to merge 2 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
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,)RuntimeErrorbf16+ ZeRO-1DeepSpeedZeroOptimizer(0,)RuntimeErrorbf16+ ZeRO-2DeepSpeedZeroOptimizer(0,)RuntimeErrorThat
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.
_update_model_bit16_weights:The shapes handed to
unflattenare right —torch.zeros_like(param.data, device="meta")—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 thefour binding sites call it:
zero/stage_1_and_2.py—_update_model_bit16_weightsfp16/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:
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 — a model matching
#8279's shape still fails before reduction is ever attempted.
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.