What happens
With autotp_size > 1, Muon's Newton-Schulz runs on each rank's column-parallel shard of an attention projection. Orthogonalizing a block of rows is not the same computation as taking that block out of the orthogonalization of all the rows, so the update Muon applies under AutoTP is not the update Muon applies without it.
This is independent of #8384; it is the current behaviour of the whole-matrix path.
Measurement
Llama, hidden_size=256, 8 heads of 32, 1 layer, fp32, ZeRO-1, Muon (lr=0.05, momentum=0.95), same seed and the same batch on both sides, one step, model.layers.0.self_attn.q_proj.weight. The tp=2 shards are concatenated in rank order to compare against the tp=1 matrix.
initial weights identical: True
loss: 4.874537 both
gradient tp=1 vs tp=2 relative difference 7.0e-07 (fp32 all-reduce ordering)
update tp=1 vs tp=2 relative difference 3.9e-01 cosine 0.936
update norm tp=1 0.0145 tp=2 0.0161 (+11%)
The gradients agree to 7e-7. The updates do not.
Isolating the kernel from the training loop, on one random 256 x 256 gradient, 5 steps:
whole matrix, tp=2, rank 0 rel diff to the same rows of the unsharded result = 0.5273
whole matrix, tp=2, rank 1 0.5234
Both Newton-Schulz kernels behave the same way here; gram and standard give 0.53 and 0.53.
Why it happens
zeropower_via_newtonschulz5 and zeropower_via_gram_newtonschulz normalize by the matrix norm and then iterate on X @ X.mT. Both depend on every row of the input. A column-parallel shard is a strict subset of the rows, so the iteration converges to the orthogonalization of the shard, which is a different matrix from the corresponding rows of the orthogonalization of the whole.
What could be done
Three options, none free, listed in increasing cost:
- Document it. Say that Muon under AutoTP orthogonalizes per shard, and that the update is therefore tp-dependent. Cheapest, and at least stops it being a surprise.
- Gather before orthogonalizing. Restores the semantics exactly, at the cost of an all-gather of the projection and NS on the full matrix on every rank — the redundancy AutoTP exists to avoid.
- Split on an axis the orthogonalization respects. This is what per-head Newton-Schulz does. A column-parallel shard holds whole attention heads, and per-head NS batches over exactly that axis, so per-head on a shard is bit-identical to the corresponding blocks of per-head on the whole matrix — measured as
0.00e+00 for both kernels at tp=2 and tp=4. That only covers head-blocked projections; the MLP matrices in the same model are still split on an axis NS does not respect.
I have (3) implemented and tested in #8384 as a consequence of the per-head split, and it is what led me here. It is not a general answer, because it says nothing about gate_proj/up_proj/down_proj, which are the larger share of the Muon parameters.
Happy to take whichever of these is wanted, or to write up (1) on its own if the semantics are considered acceptable and just undocumented.
Reproduction
# same script run under `deepspeed --num_gpus 1` and `--num_gpus 2`, the second with
# "tensor_parallel": {"autotp_size": 2} added to the config
cfg = transformers.LlamaConfig(hidden_size=256, num_attention_heads=8, num_key_value_heads=8,
num_hidden_layers=1, intermediate_size=512, vocab_size=128)
model = transformers.AutoModelForCausalLM.from_config(cfg).float()
# ... deepspeed.initialize with {"optimizer": {"type": "Muon", "params": {"lr": 0.05}}}
# one step on an identical batch, then compare (w_after - w_before) across the two runs
What happens
With
autotp_size > 1, Muon's Newton-Schulz runs on each rank's column-parallel shard of an attention projection. Orthogonalizing a block of rows is not the same computation as taking that block out of the orthogonalization of all the rows, so the update Muon applies under AutoTP is not the update Muon applies without it.This is independent of #8384; it is the current behaviour of the whole-matrix path.
Measurement
Llama,
hidden_size=256, 8 heads of 32, 1 layer, fp32, ZeRO-1, Muon (lr=0.05,momentum=0.95), same seed and the same batch on both sides, one step,model.layers.0.self_attn.q_proj.weight. The tp=2 shards are concatenated in rank order to compare against the tp=1 matrix.The gradients agree to 7e-7. The updates do not.
Isolating the kernel from the training loop, on one random 256 x 256 gradient, 5 steps:
Both Newton-Schulz kernels behave the same way here;
gramandstandardgive 0.53 and 0.53.Why it happens
zeropower_via_newtonschulz5andzeropower_via_gram_newtonschulznormalize by the matrix norm and then iterate onX @ X.mT. Both depend on every row of the input. A column-parallel shard is a strict subset of the rows, so the iteration converges to the orthogonalization of the shard, which is a different matrix from the corresponding rows of the orthogonalization of the whole.What could be done
Three options, none free, listed in increasing cost:
0.00e+00for both kernels at tp=2 and tp=4. That only covers head-blocked projections; the MLP matrices in the same model are still split on an axis NS does not respect.I have (3) implemented and tested in #8384 as a consequence of the per-head split, and it is what led me here. It is not a general answer, because it says nothing about
gate_proj/up_proj/down_proj, which are the larger share of the Muon parameters.Happy to take whichever of these is wanted, or to write up (1) on its own if the semantics are considered acceptable and just undocumented.
Reproduction