Carry Muon's optimizer state through universal checkpoints - #8609
Open
alanhuangyoo wants to merge 4 commits into
Open
alanhuangyoo wants to merge 4 commits into
alanhuangyoo wants to merge 4 commits into
Conversation
alanhuangyoo
requested review from
loadams,
tjruwase and
tohtana
as code owners
September 20, 2026 14:12
alanhuangyoo
force-pushed
the
fix/ucp-optimizer-state-keys
branch
from
September 21, 2026 17:22
75d9b71 to
cfd14b5
Compare
alanhuangyoo
force-pushed
the
fix/ucp-optimizer-state-keys
branch
from
September 23, 2026 04:41
cfd14b5 to
a9aead5
Compare
This was referenced Sep 23, 2026
alanhuangyoo
force-pushed
the
fix/ucp-optimizer-state-keys
branch
from
September 23, 2026 18:51
a9aead5 to
27ea17e
Compare
Conversion read Adam's `exp_avg` and `exp_avg_sq` by name, so a checkpoint written by any other optimizer could not be converted: Muon keeps `momentum_buffer` and nothing else, and `ds_to_universal` raised `KeyError: 'exp_avg'` before writing anything. Extraction now takes the names from the param group's own state, the merge takes them from the fragment files, and the ZeRO-3 loader takes them per sub-group - per group rather than per checkpoint, because DeepSpeed splits a Muon run into two param groups and one checkpoint therefore holds `momentum_buffer` for the matrices and Adam's pair for the rest. The ZeRO-3 loader also takes the step from whichever group saved one, since the Muon half has none. A state that is not shaped like the ZeRO partition now stops the conversion with a message naming it. ZeRO-1/2 give Muon a momentum buffer the size of the whole group, replicated on every rank, because Newton-Schulz needs the whole matrix; slicing that by partition offsets would write fragments that are the right size and hold the wrong rows. Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
ZeRO-1/2 keep Muon's momentum whole, per parameter, for every parameter a partition touches, because Newton-Schulz needs the whole matrix. A matrix that straddles two partitions is in both ranks' buffers, and the buffer is not shaped like the partition, so conversion refused it. The optimizer now records which of its states are laid out that way. The converter writes each parameter's whole state from every rank that holds it and checks the copies agree instead of concatenating them. The loader hands those states to the parameter whole, and ZeRO-1/2 rebuild each rank's buffer in the order of its own partition, which also makes a different data-parallel size work. ZeRO-3's universal load copied the first param group's hyperparameters onto every group. With Muon that ran the Adam half as a Muon group, so a resume diverged even though every state came back exactly. Each group now gets its own. A ZeRO-1/2 Muon checkpoint saved before the layout was recorded is refused rather than read by partition offsets. Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
alanhuangyoo
force-pushed
the
fix/ucp-optimizer-state-keys
branch
from
September 25, 2026 00:51
27ea17e to
9b15e2a
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #8608.
Universal checkpoints could not carry Muon's optimizer state at any ZeRO stage. With this they can at all three, including a resume on a different number of ranks.
#8633 is in now, so this is only its own four commits.
What was wrong
exp_avg/exp_avg_sqby name, so a Muon checkpoint failed withKeyError: 'exp_avg'.The change
whole_param_optimizer_statesin the optimizer checkpoint). The converter writes those per parameter from every rank that holds it and checks the copies agree. The loader rebuilds each rank's buffer in the order of its own partition, so a different dp size works.momentum_buffertakes scale power -1 for affine-mapped (TP) pieces, likeexp_avg.Testing
2 and 4 H20s, fp32, the same batch on every rank, clipping off. Train 3 steps, save, convert, resume, take 2 more steps, and compare with the run that never stopped. Bit-identical in every case:
Also checked offload now that #8464 is in. With offload, ZeRO-1/2 keep
momentum_bufferper partition like Adam's moments, so it converts the same way. All optimizer states come back bit-identical after loading, for ZeRO-1/2/3 with dp 2→4, 4→2, 2→1 and 1→2, and also with offload switched on or off between save and resume.test_universal_optimizer_states.pycovers offload now too.The new
tests/unit/checkpoint/test_universal_optimizer_states.pycovers stages 1 to 3 at 2→2 and 2→4, plus the refusal. Undoing each fix makes its test fail: the ZeRO-3 group copy, the ZeRO-1/2 rebuild, and the staging fix from #8633, where the conversion reports the disagreeing copies.Existing checkpoint tests (
test_universal_checkpoint.pyin fp32,test_zero_optimizer.py,test_muon_checkpoint.py,test_autotp_uc_checkpoint.py): 169 passed. The one failure,TestRealCheckpointUniversalConversionTPxPP, fails the same way on master here (a Gloo process-group attribute missing in this torch build). These need pytest < 8.4 as pinned inrequirements-dev.txt;DistributedFixturedoes not resolve under pytest 9.Not covered
round_robin_gradientsand loaded with it diverges, for Adam on master as well. Fixed separately in Link ZeRO-1/2 fragments at their round-robin offsets #8637.