This site is version 0.0.1. Most of the code on this site is drafted by an agent and needs review. I apologize for the AI-speak, we are working on rewriting it!
viv-lang.org

Notebooks · posts/muon-orthogonalizes-the-update

Muon orthogonalizes the update

Muon is an optimizer that keeps the directions of a gradient matrix and gives every one of them a step of about the same size, however lopsided the gradient is. Five Newton-Schulz steps get close to that without an SVD. On a problem whose inputs differ in size a hundredfold, it ends at least 4.5 times closer to the answer than SGD, momentum or Adam at their best step sizes, and behind momentum in training loss.

The problem it solves

A weight matrix maps inputs to outputs. Training moves it against the gradient , the matrix of how fast the loss changes with each entry of . Plain gradient descent (SGD) takes the step , where , the learning rate, is a step size chosen by hand.

Every matrix can be written , its singular value decomposition. The columns of are input directions, the columns of are output directions, and the singular values on the diagonal of say how hard the gradient pushes along each pair. An SGD step moves along each pair by times that pair's singular value. When the singular values are lopsided, the few large ones get nearly all of the movement and the small ones almost none.

The problem here is built to show that. It learns an 8 by 8 linear map from inputs whose size in different directions spans a factor of a hundred, with loss . The gradient is , and has eigenvalues from 1 down to . The first gradient's singular values run from 2.04 down to , a factor of 57,000. A direction the inputs barely excite gets a gradient ten thousand times weaker than the loudest one, and SGD's step along it is ten thousand times shorter.

Muon keeps the directions and drops the singular values: it steps along , which moves by the same amount along every pair. That is what orthogonalizing the update means, since has orthonormal rows and every singular value equal to 1. Adam does something like it one entry of at a time, dividing each entry's step by that entry's recent gradient size. Muon does it one direction of the matrix at a time, and those directions need not line up with the entries.

Newton-Schulz, without an SVD

Muon does not compute and . It divides by its Frobenius norm (the square root of the sum of the squares of its entries), which puts every singular value in , and then applies a matrix polynomial five times:

Since , one step turns into , where is applied to each singular value separately. The directions never move. Two tests check this: the singular values after five steps match, to nine digits, applied five times to the ones before; and times the result is symmetric with positive eigenvalues, which holds when the two share their singular vectors.

The coefficients were tuned for speed near zero. , so a small singular value grows about 3.4445 times per step. The price is that 1 is not a fixed point: iterated, the values settle into a band between 0.68 and 1.14 and keep moving inside it (lib/polar measures the band). On the first gradient here, the singular values after dividing by the norm are

0.985  0.168  0.042  0.011  0.0025  0.00065  0.00015  0.000017

and after five steps they are

1.04   1.02   0.875  0.752  0.716   0.311    0.071    0.0083

The five largest are in the band. The three smallest have grown by as much as 484.87 times, close to , and are still below it. So Muon's step is not exactly . Its directions are right and its sizes are much more even than the gradient's, and the next steps keep working on what is left.

Momentum and the step size

Muon orthogonalizes a momentum built from the gradients. It keeps , a running sum of gradients that shrinks by the factor (0.9 here) every step, and steps , where is the five Newton-Schulz steps. The length of that step is set by and not by the gradient, so near the minimum it does not get shorter by itself. The step size has to shrink instead: every optimizer in the race starts at its own and lowers it in a straight line to zero over the run.

The race

SGD, SGD with momentum, Adam and Muon each take 100 steps from . Each gets its best starting step size from the same grid, 0.01 * 2^g for g = 0, ..., 10, judged by how far it ends from the true map in the Frobenius norm. They start 4.96 away, and they end:

SGD                 2.74    (step size 2.56)
SGD with momentum   1.64    (2.56)
Adam                2.63    (0.64)
Muon                0.26 to 0.36    (0.64)

Muon's entry is a range because its result depends on rounding. The program runs Muon and momentum again from eight more starts, each with one entry moved by , about the rounding error of one multiplication. Momentum's result moves by less than . Muon's moves by up to a tenth: 0.26 to 0.36 over those nine starts, whether the program runs natively or in a browser. Newton-Schulz enlarges a small singular value by up to about 485 times, whatever put it there, and a rounding error in the momentum is no exception.

Even at the worst of the nine, Muon ends 4.5 times closer than the best of the others. The quietest input direction shows why. Along it SGD multiplies the error by each step, and after 100 steps 98.7% of the error is still there; a test computes that product and matches the run to twelve digits. Momentum leaves 89.6%. Muon leaves between 11% and 29%.

Measured by training loss instead, Muon is behind. From 2.67, momentum ends at from every start, and Muon ends above that in eight of the nine runs, as high as ; the ninth ends just below, at against . The quiet directions are the ones the loss hardly counts, so an optimizer can leave them alone and still reach a low loss. Getting them right matters once the model sees inputs that point along them more than the training inputs did.

The optimizers are in lib/optim, over the parameter layout of lib/params, and the Newton-Schulz iteration is in lib/polar. The transformer post in this channel trains with the same muon step.

Look inside this program

  • Atlas

    The program’s map: every function and which calls which.

  • There’s no recorded run of this program yet.

Shift+Enter in a cell runs it and moves to the next. Running a cell runs every cell above it too, as one program.

[1]lines 125–471