Learn the algorithm · code-adjacent
Muon
MomentUm Orthogonalized by Newton–Schulz · Keller Jordan, Dec 2024
Muon optimizes 2D hidden-layer parameters: it takes an SGD-momentum update and replaces it with a nearby semi-orthogonal matrix via Newton–Schulz iteration. Primary source: kellerjordan.github.io/posts/muon. This page puts that blog definition next to our kit copy in the NoCap research tree.
From Keller Jordan’s blog (reference)
Quotes, formulas, and the newtonschulz5 kernel below.
Our implementation
Code from nocap-repo/kit/muon_opt.py.
Our commentary Educational notes, mapping table, and diagrams on this page. Graphs and animations here are ours (campaign educational figures) — not from the blog.
Blog (source of truth)
github.com/KellerJordan/Muon
Our kit/muon_opt.py#L17–L44
reviews/MUON_SOURCE_MAP.md (local-only)
Definition
Muon optimizes 2D hidden-layer weights. Each step builds a momentum matrix G, then replaces it with a nearby semi-orthogonal matrix before applying the update. That replacement is the Ortho step; the blog implements it with five Newton–Schulz (NS) iterations instead of an SVD.
From the post: public code defaults to Nesterov-style momentum on the gradient, then orthogonalizes that momentum update, then steps the weights.
From Keller Jordan’s blog (reference) Muon update (conceptual)
G ← momentum(∇W)
O ← NewtonSchulz5(G) // ≈ Ortho(G) = UVᵀ
W ← W − η · O
Ortho(G) is the nearest semi-orthogonal matrix to G in Frobenius norm (OᵀO = I or OOᵀ = I). If G = USVᵀ, orthogonalization drops the singular values: Ortho(G) = UVᵀ. Newton–Schulz approximates that map with repeated matrix multiplies — no SVD call.
Who gets Muon vs AdamW?
Muon is not a drop-in for every tensor. Scalars, vectors, and input / output layers stay on AdamW (or similar). Hidden 2D weights go to Muon. Conv filters: flatten the last three dims.
Hidden 2D weights use Muon; embeddings, heads, and 1D parameters stay on AdamW.
Ill-conditioned matrix updates
Orthogonalization, in plain terms: keep the directions of a momentum update but rebalance their scale so no single singular direction dominates. Rare directions that still matter for learning get scaled up; the step becomes closer to full rank.
Manual inspection in the post: SGD-momentum and Adam updates on transformer 2D weights are almost low-rank — a few singular directions carry most of the energy until Ortho spreads it.
Theory flavours: steepest descent under spectral norm / accumulation-free Shampoo (Bernstein & Newhouse). Empirics: raise small singular values.
Gradient → momentum → Newton–Schulz → apply
Momentum runs before orthogonalization (opposite order from Orthogonal-SGDM). That ordering wins in Jordan’s tests.
Source: Muon design section — momentum update, then NS post-process, then parameter step.
Why NS orthogonalizes
Newton–Schulz works on singular values one at a time. Write the momentum update as G = USVᵀ. Each NS step applies a small polynomial φ to every singular value; after scaling and repeating about five times, they land near 1 — which is what orthogonalization needs.
Formally, one NS step with coefficients (a,b,c) maps singular values by φ(x) = ax + bx³ + cx⁵. After normalize + N compositions: U φᴺ(S) Vᵀ → UVᵀ when φᴺ(x) → 1 for x ∈ (0,1].
baseline that already works: (2, −1.5, 0.5)
goal: large a (speed for small σ); limit of φᴺ in ≈ [0.7, 1.3] is enough empirically
Reading beat 1: Frobenius normalize so singular values land in [0, 1] before φ runs.
Normalize
X ← G / (‖G‖_F + ε). Puts singular values in [0, 1]. Scale-invariant: Ortho(cG)=Ortho(G).
transpose if tall, so the cheaper matmul side is used
Scale only so far — φ has not run yet.
One matmul polynomial step
No SVD. Apply φ via A = X @ X.T then X ← aX + (bA + cA@A) @ X.
φ(x) = ax + bx³ + cx⁵ · coeffs (3.4445, −4.7750, 2.0315)
Large a speeds small singular values; one step = one composition of φ.
Repeat ~5 times
Compose φ. Small σ grow; large σ stay near 1. Result ≈ UVᵀ. Third-/seventh-order polys did not beat wallclock further in the post’s tests.
U · φᴺ(S) · Vᵀ → UVᵀ
Limit of φᴺ in ≈ [0.7, 1.3] is enough empirically.
Pick NS over SVD / coupled Newton
SVD: correct, too slow. Coupled Newton (Shampoo-style roots): needs float32 here. NS: stable in bf16 on modern GPUs.
wallclock + precision drive the pick; Ortho itself is unchanged
Same Ortho goal; NS is the practical kernel in the blog and our kit.
Blog reference vs our implementation
Left: Keller’s blog kernel (as cited on the post).
Right: the function we actually train with — same coeffs, same φ matmul loop;
batched .mT / last-two-dim norms, plus our muon_update wrapper.
def newtonschulz5(G, steps=5, eps=1e-7):
assert G.ndim == 2
a, b, c = (3.4445, -4.7750, 2.0315)
X = G.bfloat16()
X /= (X.norm() + eps)
if G.size(0) > G.size(1):
X = X.T
for _ in range(steps):
A = X @ X.T
B = b * A + c * A @ A
X = a * X + B @ X
if G.size(0) > G.size(1):
X = X.T
return X
# zeropower ≈ Ortho(G) = UVᵀ (drop singular values; Keller blog: newtonschulz5)
def zeropower_via_newtonschulz5(G: torch.Tensor, steps: int = 5) -> torch.Tensor:
assert G.ndim >= 2
a, b, c = (3.4445, -4.7750, 2.0315) # tuned NS polynomial coeffs (same as blog)
X = G.bfloat16()
# Tall matrix: transpose so the cheap matmul is X @ X.mT (min side squared).
if G.size(-2) > G.size(-1):
X = X.mT
# Frobenius normalize → singular values land in [0, 1] (batched over last two dims).
X = X / (X.norm(dim=(-2, -1), keepdim=True) + 1e-7)
for _ in range(steps):
A = X @ X.mT
B = b * A + c * A @ A # degree-5 polynomial in A via matmuls
X = a * X + B @ X # φ(X) = aX + (bA + cA²)X
if G.size(-2) > G.size(-1):
X = X.mT # undo tall-matrix transpose
return X
# Momentum first, then Newton–Schulz (Muon order). Default: Nesterov lookahead.
def muon_update(grad, momentum, beta=0.95, ns_steps=5, nesterov=True):
momentum.lerp_(grad, 1 - beta)
update = grad.lerp_(momentum, beta) if nesterov else momentum # Nesterov vs plain momentum
if update.ndim == 4:
update = update.view(len(update), -1) # 4D conv weights: flatten spatial dims
update = zeropower_via_newtonschulz5(update, steps=ns_steps)
update *= max(1, update.size(-2) / update.size(-1)) ** 0.5 # aspect-ratio scale max(1, n/m)^0.5
return update
One-to-one correspondence
Our commentary Blog step → our symbol. This mapping defines no sealed ΔT%.
| Blog (Keller) | Our code | File:lines |
|---|---|---|
G · momentum update matrix |
grad / update after momentum in muon_update |
kit/muon_opt.py L37–L41 |
newtonschulz5(G) · Ortho via NS |
zeropower_via_newtonschulz5(G) |
kit/muon_opt.py L17–L33 |
Normalize X /= ‖G‖_F + ε |
X / (X.norm(dim=(-2,-1), keepdim=True) + 1e-7) |
L26 |
Transpose if tall (size(0) > size(1)) |
G.size(-2) > G.size(-1) then X.mT |
L23–L24, L31–L32 |
Coeffs (3.4445, −4.7750, 2.0315) |
Identical a, b, c |
L20 |
NS loop: [email protected]; X ← aX+(bA+cA²)X |
Same with .mT |
L27–L30 |
| Default 5 NS steps | steps=5 / ns_steps=5 |
L18, L37, L65, L134 |
| Momentum before Ortho (Muon order) | muon_update: lerp momentum, then NS |
L37–L42 |
W ← W − η · O |
p.add_(update, alpha=-lr) after WD mul |
L94–L102 (SingleDeviceMuonWithAuxAdam.step) |
| AdamW on embed / head / 1D | build_muon_adam_optimizer splits groups |
L126–L194 |
Full map:
reviews/MUON_SOURCE_MAP.md (local-only).
Split-QKV variants live in
kit/muon_opt_split_qkv_cheap_v1.py
(sealed) and related kit/muon_opt_split_qkv_*.py files; they call the same
zeropower_via_newtonschulz5 — that is a separate campaign mechanism, not the NS kernel.
Our commentary
These five Newton–Schulz steps are Keller Jordan’s Ortho kernel (blog newtonschulz5 / our zeropower_via_newtonschulz5). This page defines no sealed ΔT%. Package sealed ΔT% is for the full candidate stack (Muon + timing-cheap split-QKV) vs F1_seal (3.866 h) on the report. Under our sealed F1 baseline. Public % vs F1_seal only. Matched cand-vs-stock ΔT% = N/A.
FLOP overhead
Memory matches SGD-momentum. Extra work is NS matmuls. For an n×m weight (m ≤ n), overhead vs a linear-layer train step is at most T·m/B (post).
| Setting (from blog) | m | B (tokens) | T | Overhead |
|---|---|---|---|---|
| NanoGPT speedrun | 768 | 524,288 | 5 | 0.7% |
| Llama 405B (illustrative) | 16,384 | 16,000,000 | 5 | 0.5% |
Relation to Shampoo
Drop Shampoo’s preconditioner accumulation → update collapses to UVᵀ (Bernstein & Newhouse). Add momentum before that ortho → Muon shape, with NS instead of inverse-fourth-roots (cheaper / bf16).
Muon ≈ momentum(G) then NS≈Ortho · (Nesterov default in public code)
| Method | Ortho how | Momentum | Note (blog) |
|---|---|---|---|
| SVD Ortho | exact UVᵀ | — | too slow |
| Coupled Newton | roots | — | needs f32 here |
| Orthogonal-SGDM | SVD | after Ortho | Muon: momentum first |
| Muon | NS ×5 · bf16 | before Ortho | chosen design |
Evidence bar (from the post)
Jordan argues competitive tasks beat undertuned AdamW papers. Blog results (not NoCap seals): CIFAR-10 94% speed record 3.3→2.6 A100-s; NanoGPT FineWeb 3.28 val · 1.35×; 1.5B HellaSwag GPT-2-XL level in 10 vs 13.3 8×H100-h. Open: 20B+ scale, distributed NS, finetune/RL.
NoCap package note
From Keller Jordan’s blog (reference) Algorithm credit stays with Keller Jordan (Muon).
Our implementation
Hybrid Muon+Adam lives in
kit/muon_opt.py.
Sealed timing-cheap split wrapper:
kit/muon_opt_split_qkv_cheap_v1.py#L17–L44.
Matrix groups → Muon; embed/head/1D → AdamW.
Our commentary Diagrams, scrubber, and mapping on this page are campaign educational material — not blog assets. This commentary defines no sealed ΔT%.
zeropower_via_newtonschulz5.
Pedagogy: split-vs-ortho-explained.html (two-column Ortho vs Split)
· split-qkv-explained.html (joint vs cheap vs dual-polar; flags).
Package sealed ΔT% is for the full candidate stack (Muon B32 + timing-cheap split-QKV) vs F1_seal (3.866 h); see the claim boundary, report, and NUMBERS_CANON. NS scrubber steps, blog FLOP percentages, and blog GPU-hour figures are educational; they do not define package sealed ΔT%.
Sources
- Keller Jordan et al. — Muon: An optimizer for hidden layers in neural networks (2024)
- github.com/KellerJordan/Muon
- Kit source: rainbowpuffpuff/NoCap-Test · kit/muon_opt.py#L17-L44 · muon_opt_split_qkv_cheap_v1.py#L17-L44
-
Source map:
reviews/MUON_SOURCE_MAP.md(local-only) - Bernstein & Newhouse, Old Optimizer, New Norm (2024) · Gupta et al., Shampoo (2018)