NoCap-Test evidence · think2earn algorithm explainer · Keller Jordan reference
Source code ↗

Learn the algorithm · code-adjacent

Muon

MomentUm Orthogonalized by Newton–Schulz · Keller Jordan, Dec 2024

Muon is an optimizer for 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.

2D matrices Newton–Schulz ×5 bf16-stable AdamW for embed/head

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.

Problem 01 · hybrid optimizer shape → method

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.

Visual · Parameter shape → optimizer Our commentary
LAYER STACK embed / lm_head hidden W ∈ ℝⁿˣᵐ 2D · Muon bias · LayerNorm 1D → AdamW AdamW elementwise moments embed · head · vectors Muon ortho(momentum update) hidden matrices only FROM THE POST Keep AdamW on embedding + classifier. Modular-norm intuition for embed; head = empirics.

Hidden 2D weights use Muon; embeddings, heads, and 1D parameters stay on AdamW.

Problem 02 · condition number why Ortho

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.

Visual · G → Ortho(G) ≈ UVᵀ Our commentary
G · momentum update σ(G) high κ almost low-rank NS≈Ortho Ortho(G) ≈ UVᵀ σ after NS σ → ~1 rare dirs scaled up

Theory flavours: steepest descent under spectral norm / accumulation-free Shampoo (Bernstein & Newhouse). Empirics: raise small singular values.

Problem 03 · update pipeline one step

Gradient → momentum → Newton–Schulz → apply

Momentum runs before orthogonalization (opposite order from Orthogonal-SGDM). That ordering wins in Jordan’s tests.

Visual · One Muon step Our commentary
1 Gradient ∇W 2 Momentum G · Nesterov 3 · core Newton–Schulz 5 iters · bf16 ≈ Ortho(G) 4 Apply W − η · O Memory = SGD-momentum. Extra FLOPs ≤ T·m/B for typical LM setups (<1%). T = NS steps · m = width · B = tokens / batch

Source: Muon design section — momentum update, then NS post-process, then parameter step.

Problem 04 · Newton–Schulz φ on σ

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].

tuned coeffs (post): a,b,c = (3.4445, −4.7750, 2.0315)
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
Teach beats · Newton–Schulz Our commentary
Campaign educational figure — not from Keller Jordan’s blog. Scroll to advance · range / numbered buttons / ← → as fallback.
beat 1 · normalize
‖X‖_F (schematic) ‖G‖_F → 1
σ range (schematic) σ ∈ [0, 1]
φ stage pre-φ · scale only
Singular-value schematic · unit ball after normalize x ≈ 0.35
Pedagogical φ(x) curve with schematic σ marker — not measured from a run x=0 x=1 ≈1
Scroll-synced placeholder · not a sealed metric · drag while pinned
Newton–Schulz beat diagram Four beats: Frobenius normalize, one φ matmul step, compose ≈5 times toward UVᵀ, then why NS over SVD or coupled Newton. normalize φ matmul compose ×N pick NS G ÷‖G‖_F X σ(X) ∈ [0, 1] Ortho(cG)=Ortho(G) · transpose if tall φ(x)=ax+bx³+cx⁵ · matmuls, no SVD ≈1 x=0 x=1 A = X @ X.T X ← aX + (bA + cA@A) @ X tuned a=3.4445 · steeper near 0 φ ∘ φ ∘ … ∘ φ (N ≈ 5) small σ grow; large σ stay near 1 U · φᴺ(S) · Vᵀ → UVᵀ limit of φᴺ in ≈ [0.7, 1.3] is enough empirically 3rd-/7th-order polys did not beat wallclock further in the post SVD correct, too slow Coupled Newton needs float32 here Newton–Schulz bf16-stable · wallclock engineering pick, not novelty same Ortho goal · UVᵀ

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.

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.

From Keller Jordan’s blog (reference) newtonschulz5(G, steps=5)
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
Our implementation kit/muon_opt.py · L17–L44
# 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
Ours Inline comments are educational annotations

One-to-one correspondence

Our commentary Blog step → our symbol. No sealed NoCap ΔT% here.

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: A=X@X.T; 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). Any sealed wall-clock claim in this package is for the full candidate stack (Muon + timing-cheap split-QKV) vs F1_seal on the report — not a Muon-alone speed percentage invented on this page.

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)mB (tokens)TOverhead
NanoGPT speedrun768524,28850.7%
Llama 405B (illustrative)16,38416,000,00050.5%
Efficiency note (from the blog). NS FLOPs are small for large token batches. Per-step wallclock can still exceed AdamW. Muon’s case in the post rests on sample efficiency on competitive tasks (NanoGPT / CIFAR-10 speedrunning), rather than zero extra compute.

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).

Shampoo (no accum) → W − η UVᵀ
Muon ≈ momentum(G) then NS≈Ortho · (Nesterov default in public code)
MethodOrtho howMomentumNote (blog)
SVD Orthoexact UVᵀtoo slow
Coupled Newtonrootsneeds f32 here
Orthogonal-SGDMSVDafter OrthoMuon: momentum first
MuonNS ×5 · bf16before Orthochosen 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. No sealed ΔT% invented here.

Split-QKV is a separate mechanism from Ortho/NS. Keller’s blog notes that separate Q/K/V matrices tend to beat fused QKV under Muon — an architecture/layout preference, separate from the Newton–Schulz kernel. This campaign’s timing-cheap split-QKV is a second, banked mechanism that still calls the same 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). Sealed wall-clock claims are for the full candidate stack (Muon B32 + timing-cheap split-QKV) vs F1_seal — see the report / NUMBERS_CANON. NS scrubber beats, blog FLOP percentages, and inventory GPU-hours on this page are educational; they are not that sealed ΔT%.