← All Posts
Deep Learning · OptimizersChapter 07 / 11

Muon, Part 1: A Matrix Has More Than Coordinates

Jump to the loss-contour trajectory demo ↓

Why treating a 2D weight matrix as a flat list of numbers starves most learning directions—and how Muon fixes it by setting every active singular value of the update to $1$.

Optimizers like SGD, RMSprop, Adam, and Lion treat a weight matrix $W\in\mathbb R^{m\times n}$ as a flat list of $mn$ independent numbers, updating each entry $W_{ij}$ on its own. Muon (MomentUm Orthogonalized by Newton–schulz) instead treats $W$ as a 2D transformation: it accumulates a momentum matrix $M_t$, then equalizes its singular values so every active direction of the layer takes a step of the exact same size.

01 · MOMENTUMSmooth the gradientAccumulate the 2D batch gradient $G_t$ into a momentum matrix $M_t$.
02 · DIRECTIONSView as $M_t = U\Sigma V^\top$Every matrix is a set of perpendicular directions stretched by singular values $\sigma_i$.
03 · ORTHOGONALIZESet every $\sigma_i \to 1$Replace $\Sigma$ with $I$ to get $Q_t = UV^\top$ (approximated fast via Newton–Schulz).
04 · UPDATEStep along $Q_t$Update the 2D weight matrix: $W_t = W_{t-1} - \eta\,\text{scale}\cdot Q_t$.

1. Wait—isn’t the gradient a vector? Why are $G_t$ and momentum $M_t$ matrices?

In Chapters 1–6 (SGD through Adam), we stacked every parameter of the network into one long 1D column vector $w\in\mathbb R^d$, so the gradient $g_t$ and momentum $m_t$ were also written as 1D vectors. That notation worked because coordinate-wise optimizers update each weight $W_{ij}$ completely on its own, so the shape of the parameter tensor never mattered.

Inside an actual neural network, however, a linear layer $y=Wx$ connecting $n$ inputs to $m$ outputs stores its weights in a 2D grid (matrix) $W\in\mathbb R^{m\times n}$. How do the layer’s gradient $G_t$ and momentum $M_t$ become $m\times n$ matrices?

Why a momentum matrix has skewed singular values

Once we view the momentum buffer $M\in\mathbb R^{m\times n}$ as a 2D matrix (of rank $r\le\min(m,n)$), we can decompose it via the Singular Value Decomposition (SVD) into a sum of perpendicular rank-$1$ directions:

$$M=U\Sigma V^\top=\sum_{i=1}^r\sigma_i\underbrace{u_i}_{m\times 1}\underbrace{v_i^\top}_{1\times n},\qquad\sigma_1\ge\sigma_2\ge\cdots\ge\sigma_r>0.$$

Why is $v_i\in\mathbb R^n$ called an input direction and $u_i\in\mathbb R^m$ called an output direction? Look at what happens when a single term $\sigma_i u_i v_i^\top$ multiplies a layer input vector $x\in\mathbb R^n$ (just as $W$ multiplies $x$ in $y=Wx$):

$$\bigl(\sigma_i u_i v_i^\top\bigr)x=\sigma_i u_i\underbrace{(v_i^\top x)}_{\substack{\text{dot product with}\\\text{input }x\in\mathbb R^n}}=\underbrace{(v_i^\top x)}_{\substack{\text{how much input }x\\\text{points along }v_i}}\cdot\underbrace{\sigma_i}_{\text{stretch}}\cdot\underbrace{u_i}_{\substack{\text{resulting direction}\\\text{in output space }\mathbb R^m}}.$$

And because the vectors $v_1,\dots,v_r$ are mutually perpendicular unit vectors ($v_i^\top v_i=1$ and $v_j^\top v_i=0$ for $j\ne i$), feeding $x=v_i$ into the full matrix $M$ zeros out every term except the $i$-th one:

$$Mv_i=\sum_{j=1}^r\sigma_j u_j(v_j^\top v_i)=\sigma_i u_i.$$

So each rank-$1$ term $\sigma_i u_i v_i^\top$ acts as a one-channel pipe from the layer’s input space to its output space (matching the shape of $\delta_b x_b^\top$ above):

In real networks, batch activations $x_b$ and output gradients $\delta_b$ concentrate heavily along a few dominant directions. As a result, the top singular value $\sigma_1$ of $M$ is huge while the remaining singular values are tiny ($\sigma_1\gg\sigma_2\gg\cdots\gg\sigma_r$). If you update with raw momentum $\Delta W=-\eta M$, the optimizer takes a giant step along the single loudest direction $\sigma_1 u_1 v_1^\top$ while barely moving the quieter directions that still carry useful learning signal.

2. Muon’s fix: set every active singular value to $1$

Muon keeps the input and output directions $(U,V)$ of the momentum matrix intact, but replaces every nonzero singular value $\sigma_i$ with $1$. In linear algebra, this matrix $Q=UV^\top$ is called the orthogonal polar factor of $M$:

$$\begin{aligned}\text{Raw momentum:}\quad M&=U\Sigma V^\top=\sigma_1 u_1 v_1^\top+\sigma_2 u_2 v_2^\top+\cdots+\sigma_r u_r v_r^\top,\\\text{Muon update:}\quad Q&=\operatorname{polar}(M)=UV^\top=1\cdot u_1 v_1^\top+1\cdot u_2 v_2^\top+\cdots+1\cdot u_r v_r^\top.\end{aligned}$$

Think of $Q=UV^\top$ as the matrix version of the $\operatorname{sign}(\cdot)$ function: just as $\operatorname{sign}(g)$ normalizes every nonzero scalar to $\pm 1$, Muon normalizes every nonzero singular value $\sigma_i$ to $1$. When $M$ has full rank ($r=\min(m,n)$), the resulting update matrix $Q$ has perpendicular unit columns or rows:

$$Q^\top Q=I_n\quad(\text{tall matrix, }m\ge n),\qquad QQ^\top=I_m\quad(\text{wide matrix, }m\le n).$$
Muon orthogonalizes the update $\Delta W$, not the weight matrix $W$

Each step updates $W_t=W_{t-1}-\eta Q_t$. Only the step $Q_t$ is orthogonalized; the weight matrix $W_t$ itself is a sum of many steps and stays completely unconstrained.

3. Why $UV^\top$? Steepest descent with a cap on layer stretch

Why is $Q=UV^\top$ the best update direction for a linear layer? We can derive it in three simple steps without any matrix-trace tricks:

Step 1: How much does a weight update $D=\Delta W$ change the loss?
For a 1D vector, nudging parameters by $\Delta w$ changes the loss by the dot product $g^\top\Delta w=\sum_k g_k\Delta w_k$ (slope $\times$ step). For a 2D weight matrix $W\in\mathbb R^{m\times n}$, nudging every entry $W_{pq}$ by $D_{pq}$ changes the loss by the exact same entry-by-entry sum of (slope $\times$ step), denoted $\langle M,D\rangle_F$:

$$\Delta\ell\approx\sum_{p=1}^m\sum_{q=1}^n M_{pq}D_{pq}=\langle M,D\rangle_F=\operatorname{tr}(M^\top D).$$

To decrease the loss as much as possible, we want to make this sum $\langle M,D\rangle_F$ as negative as possible.

Step 2: Cap how much the layer’s output can jump ($\|D\|_2\le\eta$).
When we change the weights by $D$, a unit input vector $\|x\|_2=1$ sees its layer output change by $\Delta y=Dx$. To keep the layer’s activations stable, we cap the worst-case output jump across all unit inputs using the spectral norm:

$$\|D\|_2=\max_{\|x\|_2=1}\|Dx\|_2=\sigma_{\max}(D)\le\eta.$$

Step 3: Plug in the SVD $M=\sum_{i=1}^r\sigma_i u_i v_i^\top$.
For a single rank-$1$ term $u_i v_i^\top$, its entry-by-entry dot product with $D$ is simply $u_i^\top Dv_i$—the scalar measuring how much $D$ maps input direction $v_i$ into output direction $u_i$. Substituting the SVD of $M$ splits the loss change into $r$ independent channels:

$$\Delta\ell\approx\langle M,D\rangle_F=\sum_{i=1}^r\sigma_i\underbrace{(u_i^\top Dv_i)}_{\substack{\text{step size along}\\\text{direction }(u_i,v_i)}}.$$

Because $u_i$ and $v_i$ are unit vectors, the stretch cap $\|D\|_2\le\eta$ bounds each channel’s step between $-\eta$ and $+\eta$:

$$-\eta\le u_i^\top Dv_i\le+\eta.$$

Since every singular value is positive ($\sigma_i>0$), how do we make $\sum_{i=1}^r\sigma_i(u_i^\top Dv_i)$ as negative as possible? Push every single active channel to its full negative limit $u_i^\top Dv_i=-\eta$:

$$D^\star=\sum_{i=1}^r(-\eta)\,u_i v_i^\top=-\eta\sum_{i=1}^r u_i v_i^\top=-\eta UV^\top.$$

In short, the three main optimizer families just differ in what they equalize:

OptimizerWhat It EqualizesUpdate RuleExample on $M=\begin{bmatrix}4&0\\0&1\end{bmatrix}$
SGD / MomentumNothing (rescales the whole matrix by one number)$-\eta M$$-\eta\begin{bmatrix}4&0\\0&1\end{bmatrix}$ — direction 1 still moves $4\times$ faster than direction 2.
SignSGD / LionEvery individual entry $M_{ij}$ gets step $\pm\eta$$-\eta\operatorname{sign}(M)$Treats each cell $M_{ij}$ separately; ignores rotated matrix directions $(U,V)$.
MuonEvery active direction $(u_i,v_i)$ gets step $\eta$$-\eta UV^\top$$-\eta\begin{bmatrix}1&0\\0&1\end{bmatrix}$ — both directions now take the exact same step $\eta$.

4. Lab: matrix normalization versus entry-wise signs

Build a $2\times 2$ matrix $M=U\operatorname{diag}(s_1,s_2)V^\top$ below to compare how the raw matrix $M$, Muon’s polar factor $Q=UV^\top$, and the entry-wise sign matrix $\operatorname{sign}(M)$ transform a unit circle.

What does the matrix do to a circle?

Each panel transforms the same unit circle. All three panels share the same scale; their matrix entries and singular values are shown below each plot.

5. Lab: watch Muon descend through the loss contours

This lab runs Muon on the same eight-example quadratic bowl as the SGD demo, embedded in a $2\times 2$ weight matrix $W=\left[\begin{smallmatrix}w_1&w_2\\0&0\end{smallmatrix}\right]$. Each step updates the momentum matrix, normalizes by the Frobenius norm, and runs $5$ Newton–Schulz iterations to orthogonalize the update before stepping $W$.

Muon: parameter path and loss by iteration

Start with 5 Newton–Schulz iterations per update. Use Step, Run 10 steps, or Play, and compare learning rates 0.08 and 0.02.

6. Complete Muon algorithm, shape scaling, and memory

Computing an exact SVD on every weight matrix is slow on GPUs, so Muon approximates $UV^\top$ using $5$ fast matrix-multiply steps of a Newton–Schulz polynomial (derived in Part 2). Putting momentum $B_t$, optional Nesterov lookahead $\widetilde B_t$, Newton–Schulz orthogonalization, and decoupled weight decay $\lambda$ together gives the complete update for a 2D weight matrix $W_t\in\mathbb R^{m\times n}$:

$$\begin{aligned}B_t&=\beta B_{t-1}+G_t\qquad&\text{(1. Momentum buffer)}\\ \widetilde B_t&=\begin{cases}G_t+\beta B_t&\text{(Nesterov = True)}\\B_t&\text{(Nesterov = False)}\end{cases}\qquad&\text{(2. Lookahead direction)}\\ Q_t&=\operatorname{NewtonSchulz}_5\!\left(\frac{\widetilde B_t}{\|\widetilde B_t\|_F+\epsilon}\right)\approx UV^\top\qquad&\text{(3. Orthogonalize }\Sigma\to I\text{)}\\ W_t&=(1-\eta\lambda)W_{t-1}-\eta\sqrt{\max\!\left(1,\frac{m}{n}\right)}\,Q_t\qquad&\text{(4. Weight decay + step)}\end{aligned}$$

Two practical details explain how Step 3 and Step 4 work:

# One Muon step for a 2D hidden weight matrix W of shape (m, n)
B = beta * B + grad
B_tilde = grad + beta * B if nesterov else B
X0 = B_tilde / (B_tilde.norm() + 1e-7)     # Shrink all singular values into (0, 1]
Q = newton_schulz_5(X0)                    # Push all active singular values -> 1 (Q ≈ U @ V.T)
scale = max(1.0, rows / cols) ** 0.5       # Aspect-ratio scaling for tall matrices
W *= 1 - lr * weight_decay                 # Decoupled weight decay
W -= lr * scale * Q

Optimizer state memory: Unlike AdamW (which stores two buffers $m_t$ and $v_t$, costing $+8\text{ bytes/parameter}$ in FP32), Muon stores only one buffer per parameter—the momentum matrix $B_t$ ($+4\text{ bytes/parameter}$ in FP32, matching SGD with Momentum)—because $Q_t$ is computed on the fly and immediately discarded.

7. Parameter routing: which layers use Muon vs. AdamW?

Muon is built specifically for dense 2D hidden-layer weight matrices. In a Transformer or MLP, non-matrix and vocabulary parameters should still be optimized with AdamW:

Parameter GroupTypical ShapeOptimizerWhy
Hidden linear layers (MLP up/down/gate, Attention $W_Q,W_K,W_V,W_O$)2D $(m\times n)$MuonDense linear maps between hidden states where equalizing singular directions speeds up learning.
Norm gains & biases (RMSNorm, LayerNorm, biases)1D $(d,)$AdamW1D vectors have no 2D row-column matrix geometry to orthogonalize.
Token embeddings2D $(V\times d)$AdamWEach token lookup updates only a single row $e_k$; rows are separate vocabulary words, not a dense basis.
Output / LM head (unembedding / classifier)2D $(V\times d)$AdamWMaps hidden states directly to rare and frequent class logits, which benefit from AdamW’s per-coordinate second moment $v_t$.

8. Questions worth answering

How is Muon different from simply dividing the momentum matrix by its Frobenius norm $\|M\|_F$?

Dividing $M$ by the single number $\|M\|_F=\sqrt{\sum_i\sigma_i^2}$ shrinks all singular values by the same factor, leaving their lopsided ratios $\sigma_1/\sigma_i$ completely unchanged. Muon sets every nonzero singular value to $1$, giving quiet directions the same step size as dominant ones.

Why doesn’t Muon call torch.linalg.svd(M) at every step?

Exact SVD on large matrices is slow on GPUs and cannot fully utilize low-precision Tensor Cores. Part 2 shows how $5$ steps of a degree-$5$ Newton–Schulz matrix polynomial push all singular values toward $1$ using only fast bfloat16 matrix multiplications.

Sources

Keller Jordan, Muon: An optimizer for hidden layers in neural networks; Muon reference implementation; Bernstein and Newhouse, Old Optimizer, New Norm: An Anthology.