Repository navigation
feat(optim): own Muon implementation, drop the dion dependency - #4133
Closed
faresobeid wants to merge 4 commits into
Closed
faresobeid wants to merge 4 commits into
faresobeid wants to merge 4 commits into
Conversation
faresobeid
marked this pull request as ready for review
October 17, 2026 16:30
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c6717a3. Configure here.
faresobeid
force-pushed
the
feat/own-muon
branch
from
October 18, 2026 00:33
41ff517 to
a7c914e
Compare
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.

Replaces the
diondependency (samsja/dion@0c801b0) with a Muon optimizer in prime-rl:src/prime_rl/trainer/optim/muon.py.Changes vs main
Muon(prime-rl) replacesdion.Muon. Its defaults give the same update as main:M = mu*M + G, with Nesterov (mu*M + G) whenoptim.nesterovis set;baddbmm);0.2*sqrt(max(fan_out, fan_in))per matrix (dion'srms_norm), with decoupled weight decay;matrix_partitionsentry (fused qkv / gate_up) is orthogonalized and scaled on its own. Equal partitions run as one batched Newton-Schulz;Shard(0), orShard(1)) is batched with same-layout parameters, in batches of its shard-group size. An all-to-all over that group gives each rank one whole matrix. The rank orthogonalizes it, and a second all-to-all sends the result back. Shards may be uneven or empty.optimizer.chunks()/optimizer.step_chunk(chunk):step()walks a fixed list of chunks. A chunk is one batch of Muon matrices or one AdamW group.step_chunkupdates only that chunk's params, on the current stream.optimizer.orthogonalize_chunk(params, U, first_owner): Newton-Schulz plus the exchange, without the momentum update or the parameter update.optim.newton_schulz_coefficients, a list of (a, b, c) triples, one per Newton-Schulz iteration. The default is dion's 5-step schedule, written out.optim.update_rmsdefaults to 0.2 (Kimi/Moonlight). DeepSeek-V4's setting isnewton_schulz_coefficients = 8 x (3.4445, -4.7750, 2.0315) + 2 x (2, -1.5, 0.5)withupdate_rms = 0.18: about 2x the Newton-Schulz time, singular values 0.998-1.003.dp_shard_cp/dp_shard_mod_ep.dionis removed frompyproject.tomlanduv.lock.momentum,variance) and param-group keys and layout are the same as dion's. A dion checkpoint therefore resumes directly (checked below).Validation
Optimizer-step benchmark: the real
setup_model+setup_optimizer, random init, random grads, Nesterov, wd 0.1. Times are the median over steps 2+, max over ranks. H200.Newton-Schulz per call (H200), compiled ->
baddbmm: (32,768,2048) 2.12 -> 2.10 ms, (16,2048,768) 1.67 -> 1.21 ms, (16,1408,4096) 6.25 -> 5.80 ms, (8,2048,6144) 9.54 -> 8.57 ms, (6144,16384) 24.1 -> 23.1 ms. Relative error against fp64 Newton-Schulz with the same coefficients is 0.54-0.58%, down from 0.77-0.98% with the compiled version, which roundedA @ AandB @ Xto bf16 before the adds. Dropping the compile also shortens the first step (Qwen3-30B-A3B 8.0 s vs 22.7 s, GLM-4.5-Air 15.0 s vs 44.3 s).Against the compiled-NS version of this PR (same weights and grads, 3 steps, Qwen3-30B-A3B EP8, 4 layers),
||Δ - Δ_compiled|| / ||Δ_compiled||is 0.36 / 0.47 / 0.54%, below the 0.9% bf16 reference below. Owner rotation alone gives bitwise-identical updates for every matrix that goes through the all-to-all (Qwen3-30B-A3B, GLM-4.5-Air, GLM-5.3).Update A/B: same weights and grads in one process, 4 layers, 5 steps. The metric is
||Δ_this - Δ_dion|| / ||Δ_dion||over all Muon matrices. As a bf16-noise reference, dion is run against itself with grads scaled by 1.001. Muon's output does not depend on that scale except through rounding.The max per parameter is 0.94% vs 1.0%. With
offload = "optimizer"the results are the same.SFT, Qwen3-0.6B Reverse-Text-SFT, Muon lr 5e-5, bs 32, 150 steps. Mean loss over each step range:
After the resume, the step-51 loss equals dion's own continuation (0.0737). The SFT runs came before the commit that compiles the momentum and update ops. That commit is numerically equivalent: unpartitioned matrices are bitwise equal on CPU, and the update A/B above was rerun after it.
Tests:
tests/unit/train/test_muon.py. It checks that Newton-Schulz singular values stay in range for the default and the DeepSeek-V4 coefficients, and that a packed parameter steps like its separate matrices.pytest -m "not gpu" tests/unit/test_configs.py tests/unit/train tests/unit/orchestrator: 556 passed. The default coefficients give bit-identical params and optimizer state to the previous preset (CPU, 3 steps).ruffis clean.Related open PRs
optimizer.chunks(), stepped withoptimizer.step_chunk. The dion imports and the per-chunk group-step restore can go, because AdamW's step count is only bumped by its own chunk.dtype=inMuon._state.matrix_partitionsand routing, whichMuontakes the same way as dion did. Equal head blocks now run as one batched Newton-Schulz instead of a Python loop per partition.dion.muon_update_batch_asyncbecomesoptimizer.orthogonalize_chunk(params, bf16 momentum)plusoptimizer._update_scale.Note
Medium Risk
Changes the distributed Muon optimizer step and NCCL all-to-all behavior on training-critical paths; checkpoint layout is preserved but numerical updates may differ slightly from dion (~1% in reported A/B tests).
Overview
Replaces the external dion Muon optimizer with an in-tree implementation in
src/prime_rl/trainer/optim/muon.pyand removes the dion git dependency frompyproject.toml/uv.lock.The new Muon keeps dion-compatible param groups and optimizer state keys so existing checkpoints resume. It runs bf16 Newton–Schulz (configurable via
optim.newton_schulz_coefficientsandoptim.update_rms), AdamW on non-matrix params, andmatrix_partitionsfor fused weights. FSDP2 matrix shards are reassembled with paired all-to-alls derived from each parameter’s DTensor placement (shard_group), with up to three chunks in flight and NCCL warm-up on every shard process group.setup_optimizerwires the new class, drops explicit mesh wiring for Muon, and exposeschunks(),step_chunk(), andorthogonalize_chunk()for chunked/offload paths. Unit tests cover Newton–Schulz singular values and packed-parameter equivalence.Reviewed by Cursor Bugbot for commit a7c914e. Bugbot is set up for automated code reviews on this repo. Configure here.