Optimizers

By the end you will be able to read any optimizer's update rule, say what problem each term is solving, compute Adam and Muon steps by hand, and pick sensible settings for a transformer without cargo-culting.

You have a loss function with a few billion inputs. You can afford to evaluate its gradient, on a random slice of the data, a few hundred thousand times. Each evaluation is noisy, the landscape is stretched by factors of a million in some directions and flat in others, and you only get one attempt. How do you move?

That is the whole optimizer problem. Everything in this chapter, from plain gradient descent to Muon, is a different answer to three sub-questions: which direction do we step, how far, and how do we stop the noise and the bad conditioning from wrecking us.

We will build the answers up one term at a time. Each new optimizer adds exactly one idea to the previous one, and every idea is there because something broke without it.

The problem: a bad landscape seen through a noisy keyhole

Picture the loss as a landscape over the parameters. Three things make it hard.

Bad conditioning. In some directions the loss curves sharply (a narrow canyon), in others it is almost flat (a long valley floor). The ratio of the biggest curvature to the smallest is the condition number. For a transformer it is enormous: embedding rows for rare tokens see tiny gradients, attention logits see huge ones, and the same learning rate has to serve both.

Noise. We never see the true gradient, only a minibatch estimate. Its variance depends on batch size and on which parameters we are looking at.

Scale. Billions of parameters means we cannot store anything like a Hessian, and even a second copy of the parameters costs real memory. Every optimizer is a memory budget in disguise.

SGD (pink): zig-zags across the steep axis momentum (green): averages out the zig-zag, keeps the along-valley drift flat axis steep axis
Figure 1. An ill-conditioned bowl: contour lines are stretched ellipses. Gradient descent bounces between the steep walls while barely moving along the valley floor. Momentum cancels the bounce and adds up the drift. Look at how the pink path spends almost all its motion going up and down.

Gradient descent and SGD

Start from the simplest possible rule. The gradient points uphill, so step downhill by a fixed fraction of it.

$$\theta_{t+1} = \theta_t - \eta\, g_t, \qquad g_t = \nabla_\theta L(\theta_t)$$

Here $\theta$ is the parameter vector, $\eta$ (eta) is the learning rate, and $g_t$ is the gradient at step $t$. That is all: direction is the negative gradient, distance is $\eta$ times its length.

Worked example

Loss $L(\theta) = \tfrac12 \theta^2$ in one dimension, so $g = \theta$. Start at $\theta_0 = 4$ with $\eta = 0.1$. Step 1: $\theta_1 = 4 - 0.1 \cdot 4 = 3.6$. Step 2: $\theta_2 = 3.6 - 0.36 = 3.24$. Each step multiplies the distance to the minimum by $1 - \eta$. With $\eta = 2.1$ the multiplier would be $-1.1$: we would overshoot and blow up. The safe range for a curvature $c$ is $\eta \lt 2/c$.

That last sentence is the whole conditioning problem in one line. If one direction has curvature $c_{\max}$ and another has $c_{\min}$, the learning rate must be below $2/c_{\max}$ to be stable, but the flat direction then converges at a rate of $1 - \eta\, c_{\min} \approx 1 - 2c_{\min}/c_{\max}$. With a condition number of $10^4$ you need thousands of steps to move along the flat axis. Look again at the pink path in Figure 1.

Minibatch noise

We cannot afford the full-dataset gradient, so we average over a minibatch of $B$ examples. The result is an unbiased but noisy estimate: its mean is the true gradient, and its variance shrinks like $1/B$.

This matters in two ways. First, the noise sets a floor: with a fixed learning rate, SGD does not settle at the minimum but jitters around it in a cloud whose radius scales with $\eta \sigma$, where $\sigma$ is the gradient noise. Decaying $\eta$ shrinks the cloud, which is one reason learning-rate schedules exist. Second, noise is not evenly spread. Rare tokens get a gradient in only a few batches, so their embedding rows see a signal that is mostly zero with occasional spikes. Keep that picture; it is why Adam wins on transformers.

Common confusion

"SGD" in papers almost always means minibatch SGD with momentum, not single-example stochastic gradient descent. And "the gradient" in an optimizer update is always the minibatch estimate, never the true one.

Momentum: the heavy ball

What breaks in Figure 1 is that consecutive gradients disagree along the steep axis (they flip sign) and agree along the flat axis (they keep pointing the same way). Averaging consecutive gradients would cancel the first and reinforce the second.

So keep a running velocity that accumulates gradients, and step along the velocity instead of the raw gradient.

$$v_{t} = \beta\, v_{t-1} + g_t, \qquad \theta_{t+1} = \theta_t - \eta\, v_t$$

$\beta$ (typically 0.9) is how much of the old velocity survives each step. What just happened: $v_t$ is a weighted sum of all past gradients, $g_t + \beta g_{t-1} + \beta^2 g_{t-2} + \dots$. Gradients that keep agreeing add up to at most $1/(1-\beta)$ times a single gradient (10× for $\beta = 0.9$), while gradients that alternate cancel.

Intuition

A heavy ball rolling down the bowl. Friction ($1-\beta$) bleeds off speed each step; the gradient is the force. A ball crossing a narrow canyon does not reverse the moment it hits the far wall; it keeps its along-valley speed and the up-and-down component damps out.

Worked example: two steps of momentum

One parameter, $\eta = 0.1$, $\beta = 0.9$, $v_0 = 0$, $\theta_0 = 0$, and suppose the gradient is $2$ at both steps.

Step 1: $v_1 = 0.9 \cdot 0 + 2 = 2$, so $\theta_1 = 0 - 0.1 \cdot 2 = -0.2$.

Step 2: $v_2 = 0.9 \cdot 2 + 2 = 3.8$, so $\theta_2 = -0.2 - 0.38 = -0.58$.

Plain SGD would be at $-0.4$. Momentum is already moving almost twice as fast, and if the gradient stayed at $2$ the velocity would approach $2/(1-0.9) = 20$: a ten-fold effective learning rate in directions where the gradient is consistent.

Because of that $1/(1-\beta)$ factor, people sometimes write the update as $v_t = \beta v_{t-1} + (1-\beta) g_t$, an exponential moving average with the same fixed point but with the ten-fold gain folded into $\eta$. PyTorch's SGD uses the first form; Adam (below) uses the second. When you compare learning rates across papers, check which one they mean.

Nesterov momentum, briefly

Nesterov's variant evaluates the gradient at the look-ahead point $\theta_t - \eta\beta v_{t-1}$ (where the velocity is about to take us) instead of at $\theta_t$. If the ball is about to overshoot, the look-ahead gradient already points back, so the correction arrives one step earlier. In practice it is a mild but consistent improvement over classical momentum, and it is the default inside Muon.

Where it came from

Polyak (1964) introduced the heavy-ball method; Nesterov (1983) the accelerated variant. Sutskever et al. (2013) showed that well-tuned momentum was a large part of what made deep networks trainable at all.

Symbols
$\theta$ = parameters
$g_t$ = minibatch gradient
$\eta$ = learning rate
$\beta$ = momentum coefficient
$v_t$ = velocity
STEP 1
Gradient
Backprop on a minibatch to get $g_t$.
STEP 2
Accumulate
$v_t = \beta v_{t-1} + g_t$: fade the old velocity, add the new push.
STEP 3
Step
$\theta \leftarrow \theta - \eta v_t$. Consistent directions build speed; alternating ones cancel.

Adaptive methods: one learning rate per parameter

Momentum fixes the zig-zag but not the scale mismatch. If parameter A sees gradients of size $10$ and parameter B sees gradients of size $0.01$, a single $\eta$ is either far too big for A or hopelessly small for B. The fix is obvious once said: give each parameter its own learning rate, chosen automatically from the size of the gradients it has been getting.

AdaGrad

AdaGrad (Duchi et al., 2011) keeps, per parameter, the sum of all squared gradients so far, and divides the step by its square root.

$$s_t = s_{t-1} + g_t^2, \qquad \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{s_t} + \epsilon}\, g_t$$

All operations are elementwise. A parameter that has seen big gradients gets a small effective learning rate; one that has seen tiny gradients gets a large one. The catch: $s_t$ only grows, so every learning rate decays to zero and training stalls. Good for convex problems, bad for a million-step transformer run.

RMSProp

The fix is to forget: replace the sum with an exponential moving average, so the normalizer tracks recent gradient size instead of the whole history.

$$s_t = \beta_2\, s_{t-1} + (1-\beta_2)\, g_t^2, \qquad \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{s_t} + \epsilon}\, g_t$$

With $\beta_2 = 0.9$, $s_t$ is roughly the mean of the last ten squared gradients. Divide by its root and the update has a size of about $\eta$ in every coordinate regardless of gradient scale. RMSProp was proposed by Tieleman and Hinton in a 2012 lecture and never got a paper; it is essentially AdaGrad with a leaky memory.

Adam: two moments and a bias correction

Adam (Kingma and Ba, 2014) combines the two ideas: a momentum-style average of the gradient (the first moment) divided by an RMSProp-style average of its square (the second moment). One more wrinkle makes it work at step one.

First, the two moving averages. Both start at zero.

$$m_t = \beta_1 m_{t-1} + (1-\beta_1)\, g_t, \qquad v_t = \beta_2 v_{t-1} + (1-\beta_2)\, g_t^2$$

Now the problem. At $t = 1$ with $\beta_1 = 0.9$, $m_1 = 0.1\, g_1$: the average is ten times too small because it was initialized at zero. Same for $v_1 = 0.001\, g_1^2$, a thousand times too small. Their ratio is badly off. So we divide each by the total weight it has accumulated so far, which is $1 - \beta^t$.

$$\hat m_t = \frac{m_t}{1-\beta_1^t}, \qquad \hat v_t = \frac{v_t}{1-\beta_2^t}$$

At $t=1$ this divides $m_1$ by $0.1$ and $v_1$ by $0.001$, exactly undoing the shrinkage. As $t$ grows the correction fades to 1. Finally the step:

$$\theta_{t+1} = \theta_t - \eta\, \frac{\hat m_t}{\sqrt{\hat v_t} + \epsilon}$$

What just happened: the direction is a smoothed gradient, the scale is normalized per parameter, and $\epsilon$ (typically $10^{-8}$) stops a parameter with zero gradient history from dividing by zero. For a gradient that is consistent in sign, $\hat m / \sqrt{\hat v} \approx \pm 1$, so every parameter moves by about $\eta$ per step. Adam is nearly a sign-of-gradient method with a smoothed sign.

Worked example: Adam at steps 1 and 2

Take $\beta_1 = 0.9$, $\beta_2 = 0.999$, $\eta = 0.1$, gradients $g_1 = 2$, $g_2 = 1$, and $\theta_0 = 0$.

Step 1. $m_1 = 0.1 \cdot 2 = 0.2$; $v_1 = 0.001 \cdot 4 = 0.004$. Bias-correct: $\hat m_1 = 0.2/0.1 = 2$, $\hat v_1 = 0.004/0.001 = 4$. Update $= 0.1 \cdot 2/\sqrt 4 = 0.1$, so $\theta_1 = -0.1$. Without correction the ratio would be $0.2/\sqrt{0.004} = 3.16$, a 3× overshoot.

Step 2. $m_2 = 0.9 \cdot 0.2 + 0.1 \cdot 1 = 0.28$; $v_2 = 0.999 \cdot 0.004 + 0.001 \cdot 1 = 0.004996$. Corrections: $1 - 0.9^2 = 0.19$ and $1 - 0.999^2 = 0.001999$. So $\hat m_2 = 0.28/0.19 = 1.474$, $\hat v_2 = 0.004996/0.001999 = 2.499$, $\sqrt{\hat v_2} = 1.581$. Update $= 0.1 \cdot 1.474/1.581 = 0.093$, so $\theta_2 = -0.193$.

Check the sizes: the corrected ratio $0.93$ is a sensible "average gradient over its RMS". The uncorrected ratio $0.28/\sqrt{0.004996} = 3.96$ would be four times too large. Bias correction is not cosmetic; it is what stops the first hundred steps from being wildly oversized.

InteractiveAdam bias-correction stepperstep through t = 1 … 6

A fixed gradient sequence; watch $m$, $v$, their bias-corrected versions, and how much the correction changes the step early on.

gradientg_t EMA of gm_t, β₁ = 0.9 EMA of g²v_t, β₂ = 0.999 ÷ (1 − β₁ᵗ)m̂_t ÷ (1 − β₂ᵗ)v̂_t m̂ / (√v̂ + ε)× η → step
Figure 2. Adam as a pipeline. The top path is momentum (direction), the bottom path is RMSProp (scale). Both are corrected for their zero start before the divide. Note that everything is elementwise: each parameter has its own $m$ and $v$, which is where Adam's memory cost comes from.

What ε actually does

$\epsilon$ looks like numerical hygiene, but it also sets a floor on the normalizer. If $\sqrt{\hat v}$ is comparable to $\epsilon$, the update degrades toward plain SGD with learning rate $\eta/\epsilon$. For tiny-gradient parameters in mixed precision this can matter: some large-model runs use $\epsilon = 10^{-6}$ or larger to damp noise, others keep $10^{-8}$. It is a real hyperparameter, just a boring one.

Symbols
$m_t$ = first moment (mean of $g$)
$v_t$ = second moment (mean of $g^2$)
$\beta_1, \beta_2$ = decay rates
$\hat m, \hat v$ = bias-corrected
$\epsilon$ = floor on the normalizer
STEP 1
Smooth
$m_t \leftarrow \beta_1 m + (1-\beta_1) g$ and $v_t \leftarrow \beta_2 v + (1-\beta_2) g^2$, elementwise.
STEP 2
Correct
Divide by $1-\beta_1^t$ and $1-\beta_2^t$ to undo the zero start.
STEP 3
Normalize & step
$\theta \leftarrow \theta - \eta\, \hat m / (\sqrt{\hat v} + \epsilon)$. Each parameter moves by about $\eta$ per step.

Why Adam beats SGD on transformers

On image classifiers, SGD with momentum matches or beats Adam. On transformers, SGD is not even close. Why?

The short answer is scale mismatch plus heavy-tailed noise. A transformer's parameters live at very different gradient scales: an embedding row for a token that appears once per ten thousand batches sees a gradient that is zero almost always and large when it fires; a LayerNorm gain sees a dense, smooth gradient every step; attention logit weights sit somewhere in between. SGD's single learning rate cannot serve all of them. Adam's per-parameter normalizer makes the rare embedding row move as far per step as the busy LayerNorm gain.

Zhang et al. (2020) measured the noise in transformer gradients and found it heavy-tailed: occasional enormous gradient samples dominate the average. Under such noise, methods that normalize (and, they argue, gradient clipping, which does something similar) converge where plain SGD does not. Kunstner et al. (2023) went a step further: they showed that the sign-descent behaviour of Adam (recall that $\hat m / \sqrt{\hat v} \approx \pm 1$) explains most of the gap, and that even sign descent with momentum closes much of it, particularly at large batch sizes where the noise is tamed.

A cleaner way to say it

SGD steps are proportional to the gradient: big where the loss is steep, tiny where it is flat. Adam steps are roughly the same size everywhere. When "flat" directions (rare tokens) are exactly the ones you need to learn, "same size everywhere" wins.

InteractiveOptimizer race on a 2-D losspick a surface, drag lr and β

Four optimizers, same start, 100 steps each. On the stretched bowl watch SGD zig-zag; on the valley watch who finds the floor and who overshoots.

AdamW: decoupled weight decay

Weight decay is the oldest regularizer there is: pull every parameter gently toward zero each step, so weights stay small and the model does not overfit to whatever quirks the data has. With SGD there are two equivalent ways to write it. You can add an $L_2$ penalty $\frac{\lambda}{2}\|\theta\|^2$ to the loss, which adds $\lambda\theta$ to the gradient, or you can shrink the weights directly after the gradient step. With SGD these give identical updates: $\theta \leftarrow \theta - \eta(g + \lambda\theta) = (1-\eta\lambda)\theta - \eta g$.

Under Adam they are not the same, and this is the entire content of the AdamW paper (Loshchilov and Hutter, 2019). If you add $\lambda\theta$ to the gradient, it goes through the $1/\sqrt{\hat v}$ normalizer like everything else. A weight with large historical gradients gets its decay term divided by a large number, so it is barely regularized at all; a weight with tiny gradients gets a huge effective decay. The regularization strength depends on the gradient history, which is not what anyone wanted.

AdamW's fix: keep the decay out of the gradient and apply it separately, after the normalized step.

$$\theta_{t+1} = \theta_t - \eta\left(\frac{\hat m_t}{\sqrt{\hat v_t} + \epsilon} + \lambda\, \theta_t\right)$$

Now every weight shrinks by the same fraction $\eta\lambda$ per step, regardless of its gradients. Note that $\lambda$ multiplies $\eta$, so when the learning-rate schedule decays, so does the decay; some implementations keep them separate. PyTorch's AdamW uses this coupled-to-$\eta$ form.

Worked example: the same λ, two different effects

Take $\eta = 10^{-3}$, $\lambda = 0.1$, and a weight $\theta = 1$. Under AdamW the decay term is $\eta\lambda\theta = 10^{-4}$: the weight shrinks by $0.01\%$ per step no matter what.

Under Adam with $L_2$ in the gradient, the decay contributes $\lambda\theta = 0.1$ to $g$. If this weight's gradients have RMS $\sqrt{\hat v} = 10$, the decay's share of the normalized update is $0.1/10 = 0.01$, so the shrink is $\eta \cdot 0.01 = 10^{-5}$, ten times weaker than AdamW. If instead the gradient RMS is $0.01$, the decay's share is $0.1/0.01 = 10$, and it swamps the actual gradient. Same $\lambda$, effects a thousand-fold apart.

Adam + L₂ (decay goes through the normalizer) g + λθ m̂ / √v̂ × η θ −= … decay ∝ λ/√v̂ AdamW (decay bypasses the normalizer) g m̂ / √v̂ + λθ × η θ −= … decay = ηλ, always
Figure 3. Where the decay term enters is the whole difference. In the top row it is normalized by each parameter's gradient RMS, so regularization strength varies wildly across parameters. In the bottom row it is added after normalization and is the same fraction for every weight.
InteractiveL₂ in the gradient vs decoupled decay, under Adamdrag the gradient scale

One parameter pulled toward $\theta^\star = 4$ by a noisy quadratic loss, with decay $\lambda$ pulling it toward 0. Scaling the whole loss (and its gradient) by $s$ should not change how strongly we regularize, yet under Adam + L₂ it does.

Common confusion

"Weight decay 0.1" in an LLM paper means AdamW's $\lambda = 0.1$, applied to matrices but usually not to biases, LayerNorm gains, or (often) embeddings. Applying it everywhere is a classic silent bug: decaying a LayerNorm gain toward zero fights the normalization it is supposed to provide.

Learning rate: warmup and schedules

Adam's per-parameter scaling assumes $\hat v$ is a decent estimate of the gradient's typical size. In the first few hundred steps it is not: it has seen a handful of samples from a network whose gradients are about to change wildly as the loss drops. The remedy is warmup: ramp $\eta$ linearly from zero over the first 1–2% of training, so the early, poorly-normalized steps are small.

After warmup, decay. Cosine decay to about 10% of the peak is the common LLM default; some runs use linear decay to zero, and "warmup-stable-decay" (a long flat plateau followed by a short sharp decay) makes it easier to extend a run. The reason to decay at all is the noise cloud from earlier: a smaller $\eta$ shrinks the jitter around the minimum, and a large fraction of the final loss improvement arrives during the decay phase.

step η warmup peak η (e.g. 3e-4) ≈ 10% of peak cosine decay warmup-stable-decay
Figure 4. Two common LLM learning-rate schedules. Both share the linear warmup. Cosine decays continuously; warmup-stable-decay holds the peak and drops late, which lets you decide the run length after it started.

Gradient clipping

Occasionally a minibatch produces a gradient hundreds of times larger than usual: a bad batch, an attention logit spike, a numerical hiccup. Adam's $v$ takes ~1,000 steps to absorb such an outlier, so the normalizer is briefly far too small and the model takes a huge step. That is a loss spike, and it can take thousands of steps to recover from.

The standard defence is global-norm clipping (Pascanu et al., 2013): if the norm of the whole gradient vector exceeds a threshold $c$, scale the whole vector down to have norm $c$.

$$g \leftarrow g \cdot \min\!\left(1, \frac{c}{\|g\|}\right)$$

Direction is preserved, only the length is capped. Almost every LLM run uses $c = 1.0$. It is cheap insurance, and Zhang et al. (2020) argue it is also part of why adaptive-style methods handle heavy-tailed noise. Watch the fraction of steps that get clipped: if it climbs from a few percent to most steps, something upstream is wrong (usually the learning rate).

Large-batch training: LARS and LAMB

When you train on thousands of GPUs you want batches of millions of tokens, and past a certain critical batch size (McCandlish et al., 2018) bigger batches stop buying proportionally faster convergence. The problem: to use a huge batch efficiently you must raise the learning rate, and at high learning rates some layers become unstable before others.

LARS (You et al., 2017) and LAMB (You et al., 2019) address this by normalizing the update per layer: the step for a weight matrix is scaled so that its norm is a fixed fraction of the weight's own norm, $\|\Delta W\| \propto \|W\|$, in what is called a trust ratio. LAMB applies this on top of Adam. It made batch sizes of 32k–64k viable for BERT pretraining, cutting wall-clock time from days to about an hour on a large TPU pod. For typical LLM pretraining today, batches are chosen below the critical size and plain AdamW is used; LAMB shows up mostly in extreme scale-out settings.

Memory: Adafactor, 8-bit Adam, GaLore

Here is a number people forget: for a model with $N$ parameters, Adam stores $2N$ extra floats ($m$ and $v$). In fp32 that is 8 bytes per parameter, on top of the 2-byte bf16 weight and its fp32 master copy. For a 70B model the optimizer state alone is 560 GB. Optimizer memory, not weights, is often what forces you onto more GPUs.

Adafactor: factor the second moment

Shazeer and Stern (2018) noticed that $v$ for a weight matrix $W \in \mathbb{R}^{m \times n}$ is itself an $m \times n$ matrix, and that it can be approximated by a rank-1 outer product of its row sums and column sums.

$$\hat V_{ij} \approx \frac{R_i\, C_j}{\sum_k R_k}, \qquad R = \text{row sums of } v,\ C = \text{column sums of } v$$

Storing $R$ and $C$ costs $m + n$ numbers instead of $mn$. For a $4096 \times 4096$ matrix that is 8k numbers instead of 16M, a 2,000× saving on the second moment. Adafactor also drops the first moment by default (or keeps it in low precision) and uses a relative step size ($\eta$ scaled by the RMS of the weights). It was the optimizer for T5 and PaLM.

8-bit Adam: quantize the state

Dettmers et al. (2022) keep the full $m$ and $v$ but store each in 8 bits using block-wise quantization: every block of 2,048 values gets its own scale, and values are mapped to a non-uniform (dynamic) 8-bit grid. States are dequantized to fp32 for the update and re-quantized after. Optimizer memory drops 4× with, in their experiments, no loss in quality. This is what bitsandbytes gives you.

GaLore: project the gradient

GaLore (Zhao et al., 2024) takes a different angle. The gradient of a weight matrix tends to be low-rank, so project it onto a rank-$r$ subspace (via an SVD refreshed every few hundred steps), run Adam in that small space, and project the update back. The optimizer state becomes $O((m+n)r)$ instead of $O(mn)$. The authors report pretraining a 7B model on a single 24 GB consumer GPU. Unlike LoRA, the full weights are still trained, only the optimizer state is compressed.

Lion: keep only the sign

Chen et al. (2023) used an automated search over update rules and found a strikingly simple winner: interpolate the gradient with momentum, take the sign, step.

$$u_t = \text{sign}\big(\beta_1 m_{t-1} + (1-\beta_1)\, g_t\big), \qquad m_t = \beta_2 m_{t-1} + (1-\beta_2)\, g_t, \qquad \theta_{t+1} = \theta_t - \eta\,(u_t + \lambda \theta_t)$$

Only one state tensor ($m$), so half of Adam's memory. Since every coordinate moves by exactly $\pm\eta$, Lion needs a learning rate 3–10× smaller than Adam and a correspondingly larger $\lambda$ to get the same effective decay. The sign makes explicit what Adam does approximately (recall $\hat m/\sqrt{\hat v} \approx \pm 1$), which is a neat confirmation of the Kunstner et al. (2023) story.

Shampoo and SOAP: preconditioning with Kronecker factors

Adam's normalizer is diagonal: each parameter is scaled independently. But the parameters of a weight matrix are not independent; the rows interact through the input and the columns through the output. A full second-order preconditioner for an $m \times n$ matrix would be an $mn \times mn$ matrix, which is hopeless. Shampoo (Gupta et al., 2018) approximates it as a Kronecker product of two small matrices.

$$L_t = L_{t-1} + G_t G_t^\top \ (m \times m), \qquad R_t = R_{t-1} + G_t^\top G_t \ (n \times n), \qquad \Delta W = -\eta\, L_t^{-1/4}\, G_t\, R_t^{-1/4}$$

What just happened: $L$ captures how gradient rows correlate, $R$ how columns correlate, and the inverse fourth roots on both sides whiten the gradient in both directions at once. For a $4096 \times 4096$ layer you store two $4096 \times 4096$ matrices rather than one $16\text{M} \times 16\text{M}$ one, and you need a matrix root, which is computed every few hundred steps and amortized.

A distributed Shampoo implementation won the external-tuning track of the AlgoPerf benchmark in 2024, and SOAP (Vyas et al., 2024) sharpened the idea: run Adam in the eigenbasis of Shampoo's preconditioners, refreshing that basis only occasionally. SOAP reports fewer steps and less wall-clock than AdamW on 360M–660M models, at the cost of more memory and a couple of extra hyperparameters.

Muon: orthogonalize the momentum

Muon (Jordan et al., 2024) is the optimizer that made people re-examine whether Adam is the final word for transformers. It is simple to state, cheap, and, as reported in a 2025 paper by Liu et al. and in the Kimi K2 technical report, has been used at trillion-parameter scale with a modified clipping scheme.

The problem Muon solves

Look at the gradient (or momentum) of a single weight matrix in a transformer. Take its SVD: $M = U \Sigma V^\top$. Empirically its singular values are wildly uneven: a few directions carry almost all the energy, and the rest are tiny. An SGD step along $M$ moves the weight almost entirely along those few dominant directions. Adam's elementwise scaling does not fix this, because the dominant directions are spread across all elements, not concentrated in a few coordinates.

The rare directions are exactly the ones we want to learn from: they are the "rare features" of the matrix update, analogous to rare tokens for embeddings. Muon's idea is to give every direction the same step: replace $M$ by the closest orthogonal matrix, $UV^\top$, which has every singular value equal to 1.

Intuition

Adam equalizes step sizes across coordinates. Muon equalizes step sizes across directions of a matrix. For a 2-D weight, directions (singular vectors) are the natural unit, not coordinates, because the layer's function depends on $W$ through matrix products.

Why not just compute the SVD?

An SVD of a $4096 \times 4096$ matrix every step for every layer is too slow and does not run well in bf16 on GPUs. The trick is that we do not need the SVD itself, only $UV^\top$, and there is a classical iteration for that: Newton–Schulz. Applying an odd polynomial $p(X) = aX + b(XX^\top)X + c(XX^\top)^2 X$ to a matrix acts on its singular values alone: each $\sigma_i$ becomes $p(\sigma_i) = a\sigma_i + b\sigma_i^3 + c\sigma_i^5$ while $U$ and $V$ stay fixed. Choose the coefficients so that repeated application pushes every $\sigma$ in $(0, 1]$ toward 1, and you get $UV^\top$ from matrix multiplications only.

The Muon recipe, with the coefficients from the reference implementation:

$$X_0 = \frac{M}{\|M\|_F}, \qquad X_{k+1} = a X_k + b\,(X_k X_k^\top) X_k + c\,(X_k X_k^\top)^2 X_k, \qquad a = 3.4445,\ b = -4.7750,\ c = 2.0315$$

The Frobenius normalization first guarantees every singular value is at most 1, which is the range where the polynomial is contractive toward 1. Five iterations are enough. These particular coefficients were tuned for speed rather than exact convergence: they push tiny singular values up very fast (the slope at zero is $a = 3.44$), at the price of the values settling into a band around 1 rather than at exactly 1. For an optimizer that is fine; what matters is that no direction is starved.

Worked example: one singular value through Newton–Schulz

Suppose after normalization a singular value is $\sigma = 0.3$. One iteration: $3.4445 \cdot 0.3 - 4.7750 \cdot 0.027 + 2.0315 \cdot 0.00243 = 1.033 - 0.129 + 0.005 = 0.909$. A value that was 30% of the largest is now 91% of it, after a single pass. A value of $0.05$ becomes $0.172 - 0.0006 + 0 \approx 0.17$, then $0.57$, then $1.10$: three iterations lift it 20-fold. Meanwhile a value already at $0.9$ goes to $3.100 - 3.481 + 1.200 = 0.819$ and then oscillates in the $0.7$–$1.2$ band.

InteractiveNewton–Schulz orthogonalizationstep through iterations; reroll the matrix

A seeded random matrix, normalized by its Frobenius norm, then pushed through the quintic iteration. Watch the singular values (bars) converge toward 1 while the matrix itself keeps its singular vectors.

The Muon algorithm

Put it together. For each 2-D weight matrix $W$ with gradient $G$:

$$M_t = \mu M_{t-1} + G_t, \qquad O_t = \text{NewtonSchulz}_5(M_t), \qquad W_{t+1} = W_t - \eta\, O_t$$

with Nesterov-style momentum ($\mu = 0.95$) by default, and, in most implementations, a scale factor on $O_t$ such as $\sqrt{\max(1, m/n)}$ so that the update's RMS does not depend on the matrix's aspect ratio. Weight decay is applied decoupled, as in AdamW.

Symbols
$G_t$ = gradient of one weight matrix
$M_t$ = momentum buffer
$\mu$ = momentum (0.95)
$O_t$ = orthogonalized update ($\approx UV^\top$)
$a,b,c$ = NS coefficients
STEP 1
Momentum
$M \leftarrow \mu M + G$, per matrix, Nesterov by default.
STEP 2
Normalize
$X_0 = M/\|M\|_F$ so all singular values are $\le 1$.
STEP 3
Newton–Schulz ×5
$X \leftarrow aX + b(XX^\top)X + c(XX^\top)^2X$, five times, in bf16.
STEP 4
Step
$W \leftarrow W - \eta\, X_5$, scaled for aspect ratio, with decoupled decay.

What Muon applies to, and what it does not

Orthogonalization is a matrix operation, so Muon only makes sense for parameters that are matrices acting as linear maps: the attention projections and MLP weights. Embeddings, the output head, biases, and LayerNorm/RMSNorm gains are handled by AdamW in every practical setup. The reasons are partly principled (an embedding table is a lookup, not a linear map on activations; its rows should be scaled per token, which is what Adam does) and partly empirical (the output head trained with Muon was reported to be worse).

Convolution kernels can be reshaped to matrices, and attention's fused QKV weight is usually split into its three matrices before orthogonalizing, so that each gets its own equalized update.

Reported results

In the original speedrun setting (Jordan et al., 2024) Muon reached the GPT-2-small target loss with about 1.35× fewer tokens than a tuned AdamW baseline. Liu et al. (2025) report scaling Muon to a 16B-total MoE model with roughly 2× the compute efficiency of AdamW in their setup, using decoupled weight decay and a per-parameter RMS-matching scale so AdamW hyperparameters could be reused. The Kimi K2 technical report (2025) describes training a 1T-parameter MoE with a variant called MuonClip, which adds a "QK-clip" that rescales attention query/key weights when their logits grow too large, to prevent the loss spikes they observed with plain Muon. Treat all of these as reported by their authors rather than as settled, independently replicated findings.

The math, slowly: why orthogonalization is "steepest descent"

Bernstein and Newhouse (2024) point out that Muon's update is the steepest-descent step under the spectral norm: among all matrices $\Delta$ with $\|\Delta\|_2 \le 1$, the one that most decreases the linearized loss $\langle G, \Delta \rangle$ is exactly $-UV^\top$. Adam-with-sign is the analogous statement for the max-abs (elementwise) norm, and SGD for the Frobenius norm. So the three optimizers differ by which norm they measure "a unit step" in, and the spectral norm is the natural one for a layer because it bounds how much the layer's output can change.

momentum M = U Σ Vᵀ U Σ: 1 big, rest tiny Vᵀ NS ×5 update O = U Vᵀ U Σ → I: all equal Vᵀ Same singular vectors (the directions), equalized singular values (the step in each direction). Rare directions in the gradient now get a full-size step instead of being drowned by the dominant one.
Figure 5. What Newton–Schulz does to a momentum matrix. The pink squares are singular values drawn to scale. Left: one direction dominates. Right: after orthogonalization every direction gets the same step. $U$ and $V$ are untouched.
import torch

def newton_schulz5(G: torch.Tensor, steps: int = 5) -> torch.Tensor:
    """Approximate U V^T for G = U S V^T using the quintic Newton-Schulz iteration."""
    a, b, c = 3.4445, -4.7750, 2.0315
    X = G.to(torch.bfloat16) if G.is_cuda else G.float()
    transposed = X.size(0) > X.size(1)
    if transposed:                      # work on the "wide" orientation: X X^T is the smaller product
        X = X.T
    X = X / (X.norm() + 1e-7)           # Frobenius-normalize so all singular values are <= 1
    for _ in range(steps):
        A = X @ X.T
        B = b * A + c * (A @ A)
        X = a * X + B @ X               # a X + b (X X^T) X + c (X X^T)^2 X
    return (X.T if transposed else X).to(G.dtype)


class Muon(torch.optim.Optimizer):
    """Momentum + orthogonalized update. Use only for 2-D weights; give the rest to AdamW."""
    def __init__(self, params, lr=0.02, momentum=0.95, nesterov=True, weight_decay=0.0):
        super().__init__(params, dict(lr=lr, momentum=momentum, nesterov=nesterov, weight_decay=weight_decay))

    @torch.no_grad()
    def step(self):
        for group in self.param_groups:
            for p in group["params"]:
                if p.grad is None or p.ndim != 2:
                    continue
                st = self.state[p]
                if "buf" not in st:
                    st["buf"] = torch.zeros_like(p.grad)
                buf = st["buf"]
                buf.mul_(group["momentum"]).add_(p.grad)
                g = p.grad.add(buf, alpha=group["momentum"]) if group["nesterov"] else buf
                upd = newton_schulz5(g)
                upd *= max(1.0, p.size(0) / p.size(1)) ** 0.5   # keep update RMS independent of shape
                if group["weight_decay"]:
                    p.mul_(1 - group["lr"] * group["weight_decay"])
                p.add_(upd, alpha=-group["lr"])

Note the three practical choices in the code: work on the orientation where $XX^\top$ is the smaller matrix, run the iteration in bf16 (the coefficients were chosen to tolerate it), and apply the aspect-ratio scale. The companion code/lumen/optimizers.py has this alongside from-scratch SGD, momentum, Adam and AdamW, all with the same interface as torch.optim so you can swap them into code/lumen/train.py.

Comparison table

Memory is counted in extra numbers per parameter (the weight itself and its gradient not included). "Hyperparameters" lists the ones you realistically tune.

OptimizerState per paramHyperparametersWhen to use
SGD0$\eta$Convex toy problems; a baseline.
SGD + momentum1$\eta$, $\beta$Convnets, image classification; rarely transformers.
Adam / AdamW2$\eta$, $\beta_1$, $\beta_2$, $\epsilon$, $\lambda$Default for transformers of every size.
Adafactor≈ 0 (factored $v$; optional $m$)relative $\eta$, $\beta_2$ schedule, clippingMemory-bound pretraining (T5, PaLM era); fine-tuning on small GPUs.
8-bit Adam2 × 1 bytesame as AdamSame behaviour as Adam at a quarter of the state memory.
GaLore$O((m+n)r/mn)$ per matrixrank $r$, refresh interval, $\eta$Full-parameter training when optimizer state does not fit.
Lion1$\eta$ (3–10× smaller), $\beta_1$, $\beta_2$, $\lambda$Memory-constrained; vision-language models; fewer knobs.
Shampoo / SOAP2 + $(m^2+n^2)$ per matrix$\eta$, preconditioner refresh, $\epsilon$, plus Adam'sWhen you can pay memory and compute for fewer steps.
Muon (+ AdamW for the rest)1 (matrices)$\eta$, $\mu$, $\lambda$; AdamW settings for 1-D paramsTransformer pretraining; reported gains at scale, active research area.

Practical LLM settings

These are typical values from published pretraining recipes (GPT-3, Brown et al. 2020; LLaMA, Touvron et al. 2023) and open reproductions, not laws. The one robust rule is that peak learning rate falls as the model grows.

SettingTypical valueWhy
OptimizerAdamWPer-parameter scaling for heavy-tailed, multi-scale gradients.
$\beta_1$, $\beta_2$0.9, 0.95$\beta_2 = 0.95$ (not 0.999) forgets faster, which reduces loss spikes on large models.
$\epsilon$$10^{-8}$Some runs raise it to $10^{-6}$ in bf16 to damp tiny-gradient parameters.
Weight decay $\lambda$0.1Decoupled; skip biases and norm gains.
Peak $\eta$$6 \times 10^{-4}$ (125M) → $3 \times 10^{-4}$ (1B) → $\approx 1.5 \times 10^{-4}$ (70B)Bigger models are less stable at the same $\eta$.
Warmup1–2% of steps (often 2,000 steps)Let $\hat v$ become a good estimate before taking full steps.
DecayCosine to 10% of peak, or WSDShrinks the noise cloud; most late-run gains come here.
Gradient clippingglobal norm 1.0Cheap insurance against bad batches.
Batch size0.5M–4M tokens, sometimes rampedBelow the critical batch size; larger for larger models.
Muon variant$\eta \approx 0.02$ for matrices, $\mu = 0.95$, AdamW for the restAs in the reference implementation; recipes are still moving.
Common confusion

Learning rates are not comparable across optimizers. Adam's $\eta$ is a per-step displacement in "gradient-normalized" units; SGD's is a multiplier on a raw gradient; Muon's sets the spectral norm of the update. An Adam $\eta$ of $3\times10^{-4}$ and a Muon $\eta$ of $0.02$ can be equally aggressive.

Practice

Exercise 1 — Adam from scratch

Implement Adam as a subclass of torch.optim.Optimizer in code/lumen/optimizers.py, with bias correction and decoupled weight decay (i.e. AdamW). Check it against torch.optim.AdamW on a random 2-layer MLP: after 10 steps on the same data the parameters should agree to $10^{-6}$. Then set correct_bias=False and plot the norm of the first 20 updates for both variants.

Solution sketch

Keep exp_avg, exp_avg_sq and a step counter in self.state[p]. Per step: m.mul_(b1).add_(g, alpha=1-b1), v.mul_(b2).addcmul_(g, g, value=1-b2), then denom = (v / (1-b2**t)).sqrt().add_(eps) and p.addcdiv_(m, denom, value=-lr/(1-b1**t)). Apply p.mul_(1 - lr*wd) before the gradient step. Without bias correction the first update is about $\sqrt{1-\beta_2}/(1-\beta_1) \approx 0.32$ of the corrected one for $\beta_2 = 0.999$ in this ordering; with $\beta_2 = 0.95$ the early updates are instead too large. Either way the trajectory changes in the first ~100 steps and then converges.

Exercise 2 — Muon vs AdamW on the tiny GPT

Train the small model from code/lumen/gpt2.py on the corpus in code/lumen/data.py for 500 steps, once with AdamW ($\eta = 3\times10^{-4}$) and once with Muon on the 2-D weights ($\eta = 0.02$, $\mu = 0.95$) plus AdamW on embeddings and norms. Log the validation loss every 50 steps. Which reaches a loss of 3.0 first? Then swap the coefficients to $(a,b,c) = (1.5, -0.5, 0)$, the classical cubic Newton–Schulz, and check whether five iterations are still enough.

Solution sketch

Build two parameter groups by p.ndim == 2 and "embed" not in name and "lm_head" not in name. Expect Muon to pull ahead within the first hundred steps on this scale, though the gap is small on a toy corpus. With the cubic coefficients the smallest singular values grow far more slowly (the slope at zero is 1.5 instead of 3.44), so after five iterations a singular value that started at 0.05 is still only about 0.4. You need 10–15 iterations for the same effect, which is the speed the quintic coefficients buy.

Exercise 3 — measure the gradient scale mismatch

On the tiny GPT after 200 steps, compute the RMS of the gradient separately for the token embedding, each attention projection, each MLP matrix, and each RMSNorm gain. Also compute, per row of the embedding, how many of the last 100 batches gave that row a non-zero gradient. Plot both. How many orders of magnitude separate the largest and smallest RMS, and what fraction of embedding rows fired fewer than 10 times?

Solution sketch

Use hooks or just read p.grad after backward(). Expect three or more orders of magnitude between the norm gains and the rare embedding rows, with a long tail of rows that received a handful of updates. This is the picture behind "why Adam beats SGD": a single $\eta$ cannot serve both ends of that plot, and the rare rows are exactly the ones an elementwise normalizer rescues.

Check yourself
Why does Adam divide $m_t$ by $1 - \beta_1^t$?
$m_t$ starts at zero, so after $t$ steps it has only accumulated total weight $1 - \beta_1^t$. Dividing by that restores the correct scale; at $t=1$ it turns $0.1\,g_1$ back into $g_1$. The correction fades to 1 as $t$ grows.
Under Adam, what goes wrong if weight decay is implemented as an $L_2$ term added to the gradient?
The $\lambda\theta$ term passes through $1/\sqrt{\hat v}$ like any other gradient component, so its strength depends on gradient history. AdamW moves the decay after the normalizer so every weight shrinks by the same fraction $\eta\lambda$.
A 2-D loss has curvature 1 along $\theta_1$ and 20 along $\theta_2$. What is the largest stable learning rate for plain gradient descent?
Stability requires $\eta \lt 2/c_{\max} = 2/20 = 0.1$. At that rate the flat direction shrinks by only $1 - 0.1 = 0.9$ per step, which is the conditioning problem in one number.
What does one Newton–Schulz iteration do to a matrix $X = U\Sigma V^\top$?
$aX + b(XX^\top)X + c(XX^\top)^2X = U\,p(\Sigma)\,V^\top$ with $p(\sigma) = a\sigma + b\sigma^3 + c\sigma^5$. Repeating pushes each singular value toward 1 (into a band around 1 for Muon's speed-tuned coefficients) using only matrix multiplications.
Which parameters does Muon not apply to in practice?
Orthogonalization is meaningful for matrices acting as linear maps on activations. Embeddings are lookups, and 1-D parameters have no singular values to equalize; those use AdamW alongside Muon.

Key takeaways

  • Every optimizer answers three questions: direction, step size, and how to survive noise and bad conditioning. Each new term exists because something broke without it.
  • Momentum averages consecutive gradients: consistent directions accelerate up to $1/(1-\beta)$×, alternating ones cancel.
  • Adam is momentum divided by an RMS normalizer, per parameter, with bias correction so the first steps are not oversized. It behaves like a smoothed sign descent, which is why it beats SGD on the multi-scale, heavy-tailed gradients of transformers.
  • AdamW applies weight decay after normalization so every weight is regularized by the same fraction; $L_2$ in the gradient under Adam is a different, worse algorithm.
  • Optimizer state is a memory budget: Adam costs two extra numbers per parameter; Adafactor, 8-bit Adam and GaLore each shrink it a different way; Lion halves it.
  • Muon orthogonalizes the momentum of each weight matrix with five Newton–Schulz iterations, giving every direction an equal step. It applies to 2-D weights only, with AdamW for the rest, and has reported wins at large scale.

Further reading