Everything more than one experiment needs. Nothing here trains anything; all five experiment packages import from it, which is what stops them describing different algorithms under the same name.
| File | What it holds |
|---|---|
optimizers.py |
The eight methods as torch.optim.Optimizer subclasses, plus muon_lmo and the Newton–Schulz iteration |
lr_scaling.py |
Per-layer learning-rate rules, derived rather than tuned |
models.py |
CNN2, ResNet9, ResNet18 |
utils.py |
Seeding, run directories, the metrics schema, parameter routing, the cosine schedule |
plotting.py |
The one figure style: palette, page geometry, rcParams |
hardware.py |
GPU, driver, CUDA, Python, torch and commit, stamped into every run |
paths.py |
Where results/ is, and how absolute paths are kept out of a bundle |
One base class with a _direction hook; each method is the ~5 lines that differ.
M is the effective momentum direction and polar(Y) = U Vᵀ is the Muon LMO.
| Class | d_t (the step is X ← X − η·λ·d_t) |
Family |
|---|---|---|
Muon |
polar(M) |
lmo |
SignSGD |
sign(M) |
sign |
SignMuon |
sign(polar(M)) |
sign |
MuonUSign |
polar(sign(M)) |
lmo |
MuonSign |
sign(polar(sign(M))) |
sign |
EF21SignMuon |
EF21 estimator of polar(M) |
lmo |
EF21MuonUSign |
polar(g_est), g_est → M |
lmo |
EF21MuonSign |
as above, plus downlink EF21-P on X |
lmo |
Three things to know before reading a step:
- Momentum is the EMA form
M = μM + (1−μ)Gof the algorithm boxes. It is trajectory-identical to the main text's heavy-ball form: the buffers differ by the constant(1−μ), andsign,polarand the EF21 recursion are all positively homogeneous, so the factor cancels. Pinned bytest_gradient_scale_invariance. - Weight decay is decoupled (
p *= 1 − lr·wd), not as a preference. Every_directionis positively homogeneous of degree zero, so foldingwd·pinto the gradient cannot shorten the step at all; it only rotates it, by an amount set by the drifting ratiowd‖p‖/‖g‖.decoupled_weight_decay=Falsereproduces the coupled convention for the ablation. EF21MuonSignkeeps two models.p.datais the broadcast modelW, sop.gradis the gradient atW; the server modelX, the iterate the theory bounds, lives in optimizer state.using_exact()evaluates on it,restore_exact()installs it.
zeropower_via_newtonschulz5 approximates the polar factor's direction well but
its norm only to a few percent, and oscillates rather than converging;
test_newton_schulz_gets_the_direction_but_not_the_scale measures that band
instead of asserting a tolerance someone picked. Which is why magnitude is handled
separately, by lr_scaling.
The two families produce step matrices whose Frobenius norms scale differently with
shape (√min(m,n) against √(mn)), so one global rate cannot be right for both,
nor across layers. The rule is derived from one criterion: an update's RMS gain
γ(A) = ‖A‖_F/√fan_out should be a fixed fraction of the initialization's.
λ = √fan_out / ‖s‖_F ⟹ lmo: η₀·√max(1, m/n) sign: η₀/√fan_in
The strongest evidence for the criterion is that it re-derives the
√max(1, m/n) factor already shipped in reference Muon, which is also why Muon's
learning rate transfers across widths. The sign family has never been given its
counterpart.
python3 -m common.lr_scaling # the rules, and what each assumes
python3 -m common.lr_scaling --compare # per-layer multiplier profiles
python3 -m common.lr_scaling --measure # is a single sign step incoherent or aligned?--measure settles the one modelling question the derivation leaves open. mup
applies the criterion to the accumulated update assuming successive sign steps
align with the activations; unit-gain assumes they do not. The measurement gives
‖sign(polar(M))‖_op ≈ 0.93(√m + √n), flat to 9% across every ResNet-18 shape:
spectrally incoherent, not rank-one aligned. The complementary check is
centralized.main --log-gain --constant-lr, which records the accumulated update's
growth; centralized.export_article fits its exponent. Both flags are needed —
under a decaying schedule the accumulation saturates and the fit reports the
schedule.
Every plotting script starts with use_paper_style(). Two properties follow:
- A figure matches
aaai2027.sty. Times text and STIX math (the template loadsnewtxtext), andpdf.fonttype = 42; matplotlib's default emits Type 3 fonts, which AAAI forbids. - A method keeps one colour across a figure family.
color_ofholds the per-method map used by the counterexample, synthetic and nanoGPT figures. The centralized panels are the deliberate exception: each holds three methods and reads as its own small comparison, so it colours by panel slot and draws Muon in reference gray throughout.
Sizes are printed points, so author at TEXT_WIDTH (7.0 in, a figure*) or
COLUMN_WIDTH (3.3125 in, a figure), include at width=\textwidth or
\columnwidth, and skip the tight bounding box. Trimming margins makes LaTeX
scale the figure back up, taking every point size with it. A script that must draw
oversize (nanogpt/plot_article.py, whose insets need the room) passes
use_paper_style(scale=k).
History is the piece to know. It records the x-axis explicitly and stores
only evaluated points:
{"steps": [0, 100, 200], "test_acc": [10.0, 61.2, 74.8], "test_loss": [...]}The pre-refactor format wrote one entry per round and forward-filled whenever
eval_freq > 1, which turns unmeasured rounds into flat plateaus and makes a
cross-seed mean meaningless. split_param_names is the other load-bearing one:
both drivers call it, so the centralized and federated settings cannot drift apart
in which parameters count as matrices.
results_root() is the single definition of where runs land, honouring
SIGNMUON_RESULTS. repo_relative() strips the writing machine's prefix from the
paths a run records, so an export bundle carries no home directory; a bundle
ships with an anonymous submission. No torch import, so the exporters and plotters
can use both without one.