跳到主要内容
起居室老虎
返回

初探优化器(一):从SGD到SOAP-Muon

11,211 字约 40 分钟

最近一段时间在看优化器相关的工作。一开始的目的是想跟一下 Modded NanoGPT,研究一下训练加速 oriented 的模型架构设计,发现还有个专门做加速器的 Track3。看的过程中感觉自己对优化器的理解太零散,Adam 大概知道在做什么,Muon 大概知道是把奇异值拉平,Shampoo 和 SOAP 就只记得是在梯度两边各乘一个矩阵,至于这些方法之间是什么关系、为什么有的在 Transformer 上好用有的不好用,基本说不太清楚。所以干脆把这段时间读的论文和做的实验整理成一个系列,算是给自己补课。

这一篇先把已有的优化器过一遍,从最基础的 SGD 一直讲到 Track 3 现在纪录里在用的 SOAP-Muon,整理一下逻辑的思路。

优化器时间线:逐元素、曲率、矩阵三条线

按时间排下来大概是这样(2010 年之前的部分压缩了),我把它们分成了三条线:逐元素(elementwise)的方法,从 SGD、动量一直到 Adam 和 AdamW;利用曲率(curvature)的方法,从 Newton 法到 Natural Gradient 和 K-FAC;以及把权重矩阵当成矩阵来处理的方法,Shampoo、Muon、SOAP 和 SOAP-Muon。最后这条线 2024 年之后变得非常快,Track 3 上现在排在前面的基本都是这一类。

0. Preliminary

0.1. 优化器在做什么

考虑一组参数 θ\theta 和一个训练要最小化的 loss L(θ)L(\theta),每一步从训练集里取一个 batch BtB_t,算出这个 batch 上的梯度

gt=∇θLBt(θt),g_t = \nabla_\theta L_{B_t}(\theta_t),

然后更新参数

θt+1=θt−ηtΔt.\theta_{t+1} = \theta_t - \eta_t \Delta_t .

ηt\eta_t 是学习率,Δt\Delta_t 是这一步的更新方向。这篇里讲到的所有优化器,说到底都在回答同一个问题:手上有到目前为止的所有梯度 g1,…,gtg_1, \dots, g_t,再加上自己存的一些状态(比如动量),这一步的 Δt\Delta_t 应该怎么取。最简单的取法是 Δt=gt\Delta_t = g_t,也就是 SGD。

另一个会一直出现的对象是线性层。y=Wxy = Wx,权重 W∈ℝm×nW \in \mathbb{R}^{m \times n} 把 nn 维的输入映射成 mm 维的输出,Transformer 里绝大部分参数都在这种矩阵里(Attention 的 WQ,WK,WV,WOW_Q, W_K, W_V, W_O,MLP 的两个矩阵)。一个 batch 里有 NN 个 token,第 ii 个 token 的输入记为 xix_i,loss 对它输出的梯度记为 δi=∂L/∂yi\delta_i = \partial L / \partial y_i,那么 WW 的梯度是

G=∑i=1Nδixi⊤,G = \sum_{i=1}^{N} \delta_i x_i^\top ,

也就是 NN 个秩为 1 的外积加在一起。讲逐元素方法的时候用不太到这个式子,把所有参数拉平成一个长向量就行;但到了 K-FAC、Muon、Shampoo 这些方法,基本都是在 ”梯度是一堆外积之和” 这件事上做文章。

0.2. 两个麻烦:噪声和形状

如果每一步都能拿到整个训练集上的精确梯度,最朴素的梯度下降其实已经能用了,麻烦在于实际训练里有两件事会捣乱。

一个是噪声。batch 只是训练集里很小的一部分,gtg_t 等于真实梯度 ∇L(θt)\nabla L(\theta_t) 加上一个随机误差,这个误差期望是 0,方差大致和 batch size 成反比,batch 越小,每一步的方向就越不靠谱。

另一个是形状。loss 曲面在不同方向上的弯曲程度可以差很多,一个简单的例子是:

L(x,y)=12(x2+κy2),κ=100,L(x, y) = \tfrac{1}{2}\left(x^2 + \kappa y^2\right), \qquad \kappa = 100 ,

一个沿 xx 方向很平、沿 yy 方向很陡的山谷。梯度下降的学习率受最陡的方向限制,超过 2/κ2/\kappa 就会在 yy 方向发散,于是在平坦的 xx 方向上每一步只能挪一点点。曲率最大值和最小值之比 κ\kappa 叫条件数,梯度下降需要的步数大致和它成正比。

GD、Adam 和 Newton 法在沿坐标轴与旋转后的山谷上各走 50 步

上图是让几种方法在这个山谷上各走 50 步。左边是沿坐标轴放的山谷:GD 先在陡的方向上来回震荡,之后沿着谷底一点点往前挪,50 步之后离终点还剩起点距离的四成多;Adam 给每个坐标单独定步长,刚好能抵消沿坐标轴的形状,很快就到了终点附近(因为带了动量,在终点附近还冲过头绕了一下);Newton 法一步到位。右边是把同一个山谷整体转 45°,起点也跟着转。GD 和 Newton 的轨迹和左边完全一样,只是整体转了一下,它们本来就不关心坐标轴朝哪;Adam 就不行了,逐坐标的缩放这时候和山谷的方向对不上,在起点附近来回折腾,50 步之后还剩六成多的距离,比 GD 还差。

换个坐标系,Adam 就从最好的变成了最差的。神经网络里的权重矩阵其实天然就有这个问题:对输入或者输出空间做一个旋转,网络表示的函数可以完全不变,逐坐标的优化器看到的东西却全变了。

0.3. NanoGPT speedrun Track 3

Track 3 是我最近主要在看的 benchmark,后面会经常引用上面的结果。modded-nanogpt 原本是 Keller Jordan 发起的 NanoGPT speedrun,比的是在 8 张 H100 上用最短的时间把模型训到目标 loss。年初加了一个 optimization track,也就是 Track 3:模型结构(GPT-2 small 规模,12 层,宽度 768)、数据(FineWeb)和 batch size(每步 512 条长度 1024 的序列,大约 52 万 token)都固定,每一步最多一次前向一次反向,优化器自己花多少计算都不管,比的是把 val loss 训到 3.28 需要多少步11。我比较喜欢这个设置的一点在于,所有方法都在同一个调好的 baseline 上比,很多优化器论文里 ”新方法对比一个没调好的 Adam” 的问题在这里基本不存在(当然它也有自己的问题)。到目前为止,调好的 Muon 加 AdamW baseline 要 3250 步,纪录是 2690 步。

1. SGD 与动量

1.1. SGD

随机梯度下降(Stochastic Gradient Descent)可以一直追溯到 Robbins 和 Monro 1951 年的随机逼近22:

θt+1=θt−ηtgt.\theta_{t+1} = \theta_t - \eta_t g_t .

gtg_t 是真实梯度的无偏估计,所以只要学习率按合适的方式衰减,SGD 在凸问题上能收敛到最优解。不过深度学习里基本没人按那些理论条件去衰减学习率。Track 3 的 baseline 是前 30% 恒定、后 70% 线性衰减到 0。schedule 对最终结果的影响很大,Track 3 上不少纪录都有一部分收益来自改 schedule,但它和优化器算是两件独立的事,这篇就不展开了。

1.2. 动量

Polyak 1964 年的 heavy ball 方法33在梯度下降上加了一个 ”速度”:

mt=βmt−1+gt,θt+1=θt−ηmt,m_t = \beta m_{t-1} + g_t, \qquad \theta_{t+1} = \theta_t - \eta m_t ,

β∈[0,1)\beta \in [0, 1) 一般取 0.9 或 0.95。展开之后 mt=∑s≤tβt−sgsm_t = \sum_{s \le t} \beta^{t-s} g_s,是过去所有梯度的指数加权和。改动很小,但同时干了三件事:

对二次函数,Polyak 证明了选好 η\eta 和 β\beta 之后,收敛需要的步数可以从和 κ\kappa 成正比降到和 κ\sqrt{\kappa} 成正比。实现上还有一种 EMA(指数滑动平均)的写法 mt=βmt−1+(1−β)gtm_t = \beta m_{t-1} + (1-\beta) g_t,两者只差一个常数倍,可以吸收进学习率里,Adam 用的是 EMA 形式,PyTorch 的 SGD 和 Muon 用的是前一种,同样叫 lr,两种写法在 β=0.95\beta = 0.95 时能差出 20 倍,读代码的时候还是要注意一下这个问题。

1.3. Nesterov

Nesterov 1983 年的加速方法44把算梯度的位置挪到 ”按当前速度再往前走一步” 的地方,在凸优化里有更好的理论保证。Sutskever 等人 2013 年把它带进了深度学习55,实际实现一般写成不需要额外前向的形式:

mt=βmt−1+gt,Δt=gt+βmt.m_t = \beta m_{t-1} + g_t, \qquad \Delta_t = g_t + \beta m_t .

和普通动量相比,它在更新里多给了当前梯度一点权重,反应更快一些,Muon 默认用的就是这种。

1.4. Gradient noise scale

噪声的方差和 batch size 成反比,这件事 McCandlish 等人在研究大 batch 训练的时候提出了一个很好用的量化指标66: 记单个样本梯度的协方差为 Σ\Sigma,真实梯度为 g‾\bar g,

Bnoise=tr⁡Σ‖g‾‖2.B_{\mathrm{noise}} = \frac{\operatorname{tr}\Sigma}{\|\bar g\|^2} .

batch size 为 BB 时,梯度里噪声和信号的能量之比大约是 Bnoise/BB_{\mathrm{noise}}/B。BB 远小于 BnoiseB_{\mathrm{noise}} 时,每一步的方向主要由噪声决定,batch 加倍差不多能让需要的步数减半;BB 远大于 BnoiseB_{\mathrm{noise}} 时,梯度已经很准了,再加大 batch 几乎省不了步数,BnoiseB_{\mathrm{noise}} 附近就是所谓的临界 batch size。训练中 BnoiseB_{\mathrm{noise}} 会慢慢变大,因为 loss 越低,真实梯度越小,而单个样本梯度的波动并不会同比例下降。

这个量对读 Track 3 的结果很重要。Track 3 的 batch 是固定的,所有优化器面对的噪声水平都一样;但自己做实验的时候不可能每个想法都跑一遍完整的 Track 3,经常要先在更小的模型、更小的 batch 上试,这时候噪声在梯度里占的比例会大很多,一些对付噪声的技巧在小 batch 上效果明显,换到大 batch 上就不一定了。

2. 逐坐标自适应:从 Adagrad 到 AdamW

2.1. Adagrad 与 RMSProp

SGD 和动量对所有参数用同一个学习率,但不同参数的梯度大小可以差好几个数量级,比如 embedding 里低频 token 对应的那一行,大部分步数里梯度都是 0,偶尔才更新一次。Adagrad77 的想法很直接,给每个坐标单独算一个步长:

vt=vt−1+gt⊙gt,θt+1=θt−ηgtvt+ϵ,v_t = v_{t-1} + g_t \odot g_t, \qquad \theta_{t+1} = \theta_t - \eta \frac{g_t}{\sqrt{v_t} + \epsilon},

这里的平方、开方和除法都是逐元素的。累积梯度一直很大的坐标步长变小,很少有梯度的坐标步长相对变大。

Adagrad 来自在线凸优化(online convex optimization),在那个框架下它的有一个很漂亮的 regret 界。论文里其实还给了一个 full-matrix 版本,用整个梯度外积的累积 ∑sgsgs⊤\sum_s g_s g_s^\top 而不只是它的对角线,更新为 (∑sgsgs⊤)−1/2gt(\sum_s g_s g_s^\top)^{-1/2} g_t。这个版本对大模型完全没法算,但 Shampoo 就是从这里出发的。

Adagrad 的毛病是 vtv_t 只增不减,训练久了所有坐标的步长都会趋于 0,不太适合长时间的非凸训练。Hinton 在 Coursera 课程的一页讲义里给了一个很简单的改法88,把累加换成指数滑动平均,只记住最近一段时间的梯度大小:

vt=β2vt−1+(1−β2)gt⊙gt,v_t = \beta_2 v_{t-1} + (1 - \beta_2)\, g_t \odot g_t ,

这就是 RMSProp。它一直没有正式发表,但用得非常广,引用的时候只能引一页课程 slides,也挺有意思的。

2.2. Adam 与 AdamW

Adam99 就是把动量和 RMSProp 合在了一起:

mt=β1mt−1+(1−β1)gt,vt=β2vt−1+(1−β2)gt⊙gt,mˆt=mt1−β1t,vˆt=vt1−β2t,θt+1=θt−ηmˆtvˆt+ϵ.\begin{aligned} m_t &= \beta_1 m_{t-1} + (1-\beta_1)\, g_t, \\ v_t &= \beta_2 v_{t-1} + (1-\beta_2)\, g_t \odot g_t, \\ \hat m_t &= \frac{m_t}{1-\beta_1^t}, \qquad \hat v_t = \frac{v_t}{1-\beta_2^t}, \\ \theta_{t+1} &= \theta_t - \eta \frac{\hat m_t}{\sqrt{\hat v_t} + \epsilon}. \end{aligned}

mtm_t 和 vtv_t 分别是梯度一阶矩和(未中心化的)二阶矩的滑动估计。第三行是偏差修正:m0=v0=0m_0 = v_0 = 0,训练刚开始时两个估计都偏小,除以 1−βt1-\beta^t 把它们校正回来。常用超参是 β1=0.9\beta_1 = 0.9、β2=0.999\beta_2 = 0.999,大模型训练里 β2\beta_2 一般取 0.95。每个参数要额外存 mm 和 vv 两个状态,fp32 下优化器状态是参数量的两倍。

Adam 为什么好用,我看到的解释大概有三种,它们之间并不完全一致:

  • 符号下降。β1=β2=0\beta_1 = \beta_2 = 0 时更新就是 gt/|gt|=sign(gt)g_t / |g_t| = \mathrm{sign}(g_t),只保留每个坐标的符号,所以 Adam 可以看成带平滑的符号下降(signSGD1010)。Balles 和 Hennig 把 Adam 的更新拆成符号乘上一个逐坐标的缩放因子 |mˆt|/vˆt|\hat m_t| / \sqrt{\hat v_t}1111:梯度方向稳定的坐标,这个因子接近 1;符号来回变的坐标,mm 被平均掉而 vv 不会,因子就变小,大概可以理解成每个坐标上的信噪比,也被称作 variance adaptation。Google 搜出来的 Lion1212 更直接,更新就是 sign(β1mt−1+(1−β1)gt)\mathrm{sign}\big(\beta_1 m_{t-1} + (1-\beta_1) g_t\big),只存一个动量,在不少任务上和 AdamW 差不多,算是给这个看法提供了一个旁证;
  • 对角曲率。vtv_t 估计的是 𝔼[g⊙g]\mathbb{E}[g \odot g],也就是经验 Fisher 矩阵的对角线,所以 Adam 也常被说成是一种对角的二阶方法。这个说法我感觉有点牵强,Adam 用的是平方根,Natural Gradient 用的是逆,而且经验 Fisher 本身拿来近似曲率就有不少问题1313;
  • 噪声不是主要原因。Kunstner 等人 2023 年发现,在 Transformer 上即使用 full batch(没有采样噪声),SGD 和 Adam 之间的差距依然在,而带动量的符号下降能追上大部分差距1414,说明 Adam 的优势至少有一部分来自它对形状的处理。

还有一种看法,我觉得是理解后面 Muon 的关键。Bernstein 和 Newhouse 指出,很多优化器都可以写成某个范数下的最速下降1515:在 ”步长不超过 1” 的约束下,选一个和梯度内积最大的方向,

Δ⋆=arg⁡max‖Δ‖≤1⁡⟨g,Δ⟩.\Delta^\star = \arg\max_{\|\Delta\| \le 1} \langle g, \Delta \rangle .

范数取 ℓ2\ell_2,解是 g/‖g‖2g / \|g\|_2,就是归一化的 SGD;取 ℓ∞\ell_\infty(每个坐标的绝对值都不超过 1),解是 sign(g)\mathrm{sign}(g),就是符号下降。换句话说,选什么范数,就是在选怎么衡量 ”一步走了多远”,更新的方向也就跟着定了。

大模型里用 Adam 还有两个实践上的细节。一个是 β2\beta_2:0.999 意味着 vv 大概记得最近一千步的梯度大小,梯度的尺度变化很快的时候(比如训练刚开始,或者 loss spike 之后),vv 跟不上,mˆ/vˆ\hat m / \sqrt{\hat v} 会突然变得很大,一般认为这是大模型训练里把 β2\beta_2 调到 0.95 的主要原因。另一个是 ϵ\epsilon:对梯度本来就很小的坐标,分母里的 ϵ\epsilon 可能比 vˆ\sqrt{\hat v} 还大,这时 Adam 在这些坐标上就退化成了 SGD,所以 ϵ\epsilon 的取值是会改变优化器行为的。Track 3 baseline 里 aux AdamW 的 ϵ\epsilon 是 10−1010^{-10},比 PyTorch 默认的 10−810^{-8} 小两个数量级,One-sided Shampoo 那个提交里 grafting 用的 Adam 更是设到了 10−1510^{-15},基本就是不想让 ϵ\epsilon 起任何作用的意思。至于 10−1010^{-10} 和 10−810^{-8} 在 Track 3 上到底能差多少,我还没有专门测过。

剩下一个问题是正则化。在 SGD 里,L2 正则化(loss 里加 λ2‖θ‖2\frac{\lambda}{2}\|\theta\|^2)和权重衰减(每步把参数乘以 1−ηλ1 - \eta\lambda)是一回事,Loshchilov 和 Hutter 指出在 Adam 里这两者不再等价1616:L2 正则项的梯度 λθ\lambda\theta 会和普通梯度一起被 vˆt\sqrt{\hat v_t} 除掉,于是梯度一直很大的参数反而得到更少的正则化。AdamW 把权重衰减从梯度里拿出来,直接作用在参数上:

θt+1=θt−η(mˆtvˆt+ϵ+λθt).\theta_{t+1} = \theta_t - \eta \left( \frac{\hat m_t}{\sqrt{\hat v_t} + \epsilon} + \lambda \theta_t \right).

改动看起来很小,效果却明显更好,之后 AdamW 就成了大模型训练的默认选择,常见配置是 β=(0.9,0.95)\beta = (0.9, 0.95)、weight decay 0.1,再加上 warmup 和 cosine 衰减。后面的 Muon 和 SOAP 基本都沿用了这种解耦的权重衰减。

2.3. Adafactor

Adafactor1717 的出发点是省显存。对一个矩阵参数 W∈ℝm×nW \in \mathbb{R}^{m\times n},Adam 要存一个同样形状的二阶矩 VV,Adafactor 只存 VV 的行和 r∈ℝmr \in \mathbb{R}^m 与列和 c∈ℝnc \in \mathbb{R}^n,用它们重建一个秩为 1 的近似:

Vˆ=rc⊤𝟏⊤r.\hat V = \frac{r\, c^\top}{\mathbf{1}^\top r} .

状态从 mnmn 降到 m+nm + n。论文里还有几个配套的改动:去掉一阶动量进一步省显存、对更新做裁剪(update clipping)、按参数自身的大小决定相对步长、让 β2\beta_2 随训练步数增加。T5 和 PaLM 都用过它(PaLM 用的是不做分解的版本)。

我觉得 Adafactor 在这条线上的位置有一些特别之处:它是第一个用上了” 参数是一个矩阵,有行有列” 这个结构的主流优化器,虽然出发点只是压缩二阶矩,和矩阵的几何没什么关系。有意思的是,几年之后 SOAP 的作者回过头发现,Shampoo 的一个变体其实就是在某个旋转过的坐标系里跑 Adafactor。

3. 曲率:Newton、Natural Gradient 与 K-FAC

3.1. Newton 法

回到 0.2 节的山谷,GD 慢是因为它对所有方向一视同仁,最自然的想法就是把曲率也考虑进来。在当前点附近把 loss 展开到二阶,记位移为 dd,

L(θ+d)≈L(θ)+g⊤d+12d⊤Hd,L(\theta + d) \approx L(\theta) + g^\top d + \tfrac12 d^\top H d ,

HH 是 Hessian 矩阵。这个二次函数的最小值点在 d=−H−1gd = -H^{-1} g,按 0.1 节 θt+1=θt−ηtΔt\theta_{t+1} = \theta_t - \eta_t \Delta_t 的写法,就是取 Δ=H−1g\Delta = H^{-1} g、η=1\eta = 1,这就是 Newton 法。对山谷来说 H=diag(1,κ)H = \mathrm{diag}(1, \kappa),Newton 法一步就到最优点,而且不管山谷怎么转都一样,因为它对参数的线性变换是不变的(affine invariant)。

问题在于规模。一个 10810^8 参数的模型,Hessian 有 101610^{16} 个元素,存都存不下,更别说求逆。神经网络的 loss 也不是凸的,HH 可能有负特征值,直接用 H−1H^{-1} 会往上坡走;从一个 batch 估出来的 HH 噪声也很大。所以实际能用的二阶方法,基本都在做两件事:找一个半正定的曲率矩阵代替 HH,再找一种结构化的近似让它算得起。

3.2. Gauss-Newton、Fisher 与 Natural Gradient

loss 是负对数似然的时候(比如语言模型的交叉熵),有一个天然的半正定替代品,Fisher 信息矩阵:

F=𝔼x𝔼yˆ∼pθ(⋅|x)[∇θlog⁡pθ(yˆ|x)∇θlog⁡pθ(yˆ|x)⊤].F = \mathbb{E}_{x}\,\mathbb{E}_{\hat y \sim p_\theta(\cdot\mid x)}\left[\nabla_\theta \log p_\theta(\hat y \mid x)\, \nabla_\theta \log p_\theta(\hat y \mid x)^\top\right].

注意这里的 yˆ\hat y 是从模型自己的预测分布里采样的。对交叉熵 loss,FF 等于 Gauss-Newton 矩阵,可以理解成去掉了 Hessian 里可能为负的那部分。用 FF 代替 HH 得到的更新 Δ=F−1g\Delta = F^{-1} g 叫 Natural Gradient1818。我比较喜欢它的一个解释:普通梯度下降在参数空间里用欧氏距离衡量 ”一步走多远”,Natural Gradient 用的则是模型输出分布之间的 KL 散度,所以它不依赖参数化的方式,换一种等价的参数化,更新出来的函数变化是一样的。

把 yˆ\hat y 换成真实标签,得到的是经验 Fisher 𝔼[gg⊤]\mathbb{E}[g g^\top],算起来方便很多,Adam 的 vv 就是它的对角线。但经验 Fisher 和真正的 Fisher 在很多情况下差别很大,拿它做曲率近似要小心1313。

3.3. K-FAC

K-FAC1919 算是把 Natural Gradient 做到神经网络规模上最成功的尝试之一,它用的就是 0.1 节那个外积结构。对线性层 y=Wxy = Wx,单个样本的梯度是 δx⊤\delta x^\top,把它拉成向量之后,Fisher 中对应这一层的块是

FW=𝔼[(xx⊤)⊗(δδ⊤)]≈𝔼[xx⊤]⊗𝔼[δδ⊤]=A⊗B,F_W = \mathbb{E}\big[(x x^\top) \otimes (\delta \delta^\top)\big] \approx \mathbb{E}[x x^\top] \otimes \mathbb{E}[\delta \delta^\top] = A \otimes B ,

约等号来自假设输入 xx 和输出梯度 δ\delta 相互独立。A∈ℝn×nA \in \mathbb{R}^{n\times n} 是输入的二阶矩,B∈ℝm×mB \in \mathbb{R}^{m\times m} 是输出梯度的二阶矩。Kronecker 积的逆等于逆的 Kronecker 积,于是 Natural Gradient 的更新可以写回矩阵形式:

ΔW=B−1GA−1.\Delta W = B^{-1}\, G\, A^{-1} .

原本要对一个 mn×mnmn \times mn 的矩阵求逆,现在只需要对一个 m×mm \times m 和一个 n×nn \times n 的矩阵求逆,对 4096×40964096 \times 4096 的层,就是从一个约 1.7×1071.7\times 10^7 维的问题变成两个 4096 维的问题。实现里 AA 和 BB 用滑动平均估计,每隔若干步重新求一次逆,再加上 damping 保证数值稳定。

来自 Benzing 2022 年的工作2020对 K-FAC 有一个比较特别的解读:只看输入一侧,GA−1G A^{-1} 正好是一个最小二乘问题的解:先决定每个 token 上这一层的输出想往哪个方向变(就是 −δi-\delta_i),再找一个最能用输入 xix_i 线性实现这些变化的 ΔW\Delta W。他管这个叫在神经元上做梯度下降(gradient descent on neurons),并且发现 K-FAC 的效果主要来自输入一侧的因子 AA。

K-FAC 在研究里影响很大,但据我所知在 LLM 预训练里用得不多,求逆的开销、对 damping 的敏感,以及实现的复杂度大概都是原因。

4. 把矩阵当作矩阵:Muon

4.1. 动机

前面的逐元素方法有一个共同点:它们把一个 m×nm\times n 的权重矩阵当成 mnmn 个互不相关的数,可网络用 WW 的方式,是把它当成一个线性映射作用在输入上。0.2 节的右图其实已经把这个问题摆出来了,坐标系一转,逐坐标缩放的效果就全变了。

另一个观察来自梯度本身的形状。G=∑iδixi⊤G = \sum_i \delta_i x_i^\top 是一堆外积之和,实际训练里这种矩阵的奇异值往往衰减得很快,少数几个方向占了绝大部分能量。直接拿 GG(或者它的动量)去更新,步长几乎全花在那几个主导方向上,其他方向基本学不动。

4.2. 算法

Muon(MomentUm Orthogonalized by Newton-Schulz)是 Keller Jordan 在 2024 年底提出的2121,最早用在 NanoGPT speedrun 和 CIFAR-10 speedrun 上,做法非常简单粗暴:先算动量,然后把动量矩阵的所有奇异值都换成 1。

Mt=βMt−1+Gt,M~t=Gt+βMt,Ot=polar(M~t)=UV⊤,M~t=UΣV⊤,Wt+1=Wt−ηsOt.\begin{aligned} M_t &= \beta M_{t-1} + G_t, \qquad \tilde M_t = G_t + \beta M_t, \\ O_t &= \mathrm{polar}(\tilde M_t) = U V^\top, \qquad \tilde M_t = U \Sigma V^\top, \\ W_{t+1} &= W_t - \eta\, s\, O_t . \end{aligned}

第一行是 Nesterov 动量,UΣV⊤U \Sigma V^\top 是 M~t\tilde M_t 的瘦 SVD。UV⊤U V^\top 在线性代数里叫极分解(polar decomposition)的正交因子,后面记作 polar(⋅)\mathrm{polar}(\cdot),它保留了原矩阵的奇异向量,也就是 ”往哪些方向走”,但把每个方向上的步长都拉平成 1。ss 是一个和矩阵形状有关的缩放系数,Jordan 的实现里取 s=max⁡(1,m/n)s = \sqrt{\max(1, m/n)},mm 是输出维度,nn 是输入维度。

Muon 只管网络中间的二维权重矩阵,embedding、最后的 LM head、RMSNorm 的 gain 和各种 bias 还是交给 AdamW,Track 3 里管这部分叫 aux AdamW。embedding 的输入是 one-hot 向量,每一步只有被用到的那几行有梯度,“整个矩阵对任意输入的作用” 这种看法对它不太适用;LM head 在 Jordan 的实验里用 Muon 也不如用 AdamW;一维的 gain 和 bias 则根本没有奇异值可言。所以实际跑起来的 ”Muon” 一直是 Muon 和 AdamW 的混合体,aux AdamW 这部分的超参调起来也挺费劲的,Track 3 #36 那个调好的 baseline,改的超参里一大半都是 aux AdamW 的。

4.3. Newton-Schulz 迭代

每个矩阵每一步都做一次 SVD,在 GPU 上太慢,也不适合 bf16。Muon 用 Newton-Schulz 迭代来近似 UV⊤U V^\top:先把矩阵除以它的 Frobenius 范数,保证所有奇异值都不超过 1,然后反复做

Xk+1=aXk+bXkXk⊤Xk+cXk(Xk⊤Xk)2.X_{k+1} = a X_k + b\, X_k X_k^\top X_k + c\, X_k (X_k^\top X_k)^2 .

这个迭代只用矩阵乘法,而且不改变奇异向量,每个奇异值 σ\sigma 各自经历同一个标量映射 σ↦aσ+bσ3+cσ5\sigma \mapsto a\sigma + b\sigma^3 + c\sigma^5。所以选系数的问题就变成了:选一个奇多项式,让它迭代几次之后,把 (0,1](0, 1] 里的数都推到 1 附近。

Newton-Schulz 迭代的标量映射与奇异值谱

左图是三组系数下的标量映射。经典的三次迭代 (1.5,−0.5,0)(1.5, -0.5, 0) 在 1 附近收敛很快,但对很小的奇异值每次只能放大 1.5 倍,迭代 12 步之后,10−210^{-2} 以下的奇异值还明显没到 1。Jordan 调出来的五次多项式 (3.4445,−4.7750,2.0315)(3.4445, -4.7750, 2.0315) 在 0 附近斜率很大,5 步就能把 10−310^{-3} 量级的奇异值推到 0.5 左右,3×10−33\times 10^{-3} 以上的基本都到了 1 附近;代价是它并不收敛到 1,在 [10−2,1][10^{-2}, 1] 的输入上,5 步之后的输出在 0.68 到 1.13 之间来回摆。Jordan 的判断是这点误差对训练基本没有影响,换来的是更少的迭代次数,speedrun 比的是墙钟时间,这个取舍很合理。Track 3 比的是步数,算得慢一点无所谓,所以 baseline 换成了另一组系数 (2,−1.5,0.5)(2, -1.5, 0.5),迭代 12 步,它在 0 附近斜率为 2,在 1 处精确收敛,10−310^{-3} 以上的奇异值最后都是 1。

右图是一个合成的动量矩阵(256×1024256 \times 1024,奇异值按幂律衰减再加上噪声)经过两种迭代之后的奇异值,原始的奇异值跨了将近两个数量级,迭代之后基本都被拉到了 1 附近。按 Jordan 的估算,Newton-Schulz 带来的额外计算在典型的语言模型训练里不到 1%2121。

系数和缩放这些东西看起来只是实现细节,我在小模型上就被它坑过一次。一开始用的是一个常见的 Muon 库实现(Jordan 的五次系数、不加 weight decay、库自带的步长缩放),在这个设置下看到某个改动比 SOAP 还好 0.01 左右;后来把这些全部换成 Track 3 的约定重跑,收益只剩下五分之一,置信区间也跨过了 0。也就是说之前看到的 ”收益”,有一大半是在弥补库实现本身的问题。从那之后我的小模型实验都尽量和 Track 3 的约定对齐。

4.4. 为什么有效

Muon 为什么有效,我目前看到的主要有三种说法,彼此相容,只是角度不同。

第一种是谱范数下的最速下降1515。把 2.2 节的范数换成矩阵的谱范数(spectral norm,也就是最大奇异值 ‖Δ‖op\|\Delta\|_{\mathrm{op}}),

arg⁡max‖Δ‖op≤1⁡⟨G,Δ⟩=UV⊤,\arg\max_{\|\Delta\|_{\mathrm{op}} \le 1} \langle G, \Delta \rangle = U V^\top ,

解正好是 Muon 的更新。谱范数衡量的是 ”这个矩阵最多能把一个输入向量放大多少倍”,限制 ‖ΔW‖op\|\Delta W\|_{\mathrm{op}},就是限制这一步对任何输入能造成的最大输出变化。Bernstein 等人的一系列工作把这个思路推广到了整个网络2222 2323:每一层用和它在网络中的角色相匹配的范数,线性层用的是 ”输入和输出都按 RMS 衡量” 的算子范数,正好对应形状缩放 m/n\sqrt{m/n}。

输入 xx 的每个分量大小在 1 左右,也就是 RMS 为 1、‖x‖2=n\|x\|_2 = \sqrt{n},那么一个所有奇异值都是 σ\sigma 的更新 ΔW\Delta W 最多能让输出变化 σn\sigma\sqrt{n},换算成输出的 RMS 是 σn/m\sigma\sqrt{n/m}。想让输出的 RMS 变化和输入在同一个量级,就要取 σ=m/n\sigma = \sqrt{m/n}。Jordan 的 max⁡(1,m/n)\sqrt{\max(1, m/n)} 只在输出维度大于输入维度时放大,算是这个式子的一个变体。

第二种是把 Muon 看成没有记忆的 Shampoo,下一节讲 Shampoo 的时候再说。

第三种是 Jordan 原始博客里的直觉:SGD 和 Adam 产生的更新矩阵往往接近低秩,被少数方向主导,正交化之后,那些原本很小、但可能很重要的方向拿到了和主导方向一样的步长。这个说法很直观,但 ”可能很重要” 基本算纯猜。奇异值很小的那些方向,到底是被压制住的有用信号,还是就是噪声?正交化对两者是一视同仁地放大的。后面的不少工作,都是在追问这个问题。

4.5. 规模化与变体

Muon 在 speedrun 上火了之后,Moonshot 的 Moonlight 把它推到了大模型上2424。他们发现规模大了之后要改两点:加上和 AdamW 一样的解耦权重衰减;把形状缩放改成 0.2max⁡(m,n)0.2\sqrt{\max(m, n)},让 Muon 每个元素的更新大小和 AdamW 差不多,这样可以直接沿用 AdamW 的学习率和 weight decay(这个改法很工程但很实用,从 AdamW 迁移过来不用重新调一遍超参)。在他们的 scaling law 实验里,Muon 达到同样 loss 需要的计算量大约是 AdamW 的一半。之后的 Kimi K2 用 Muon 训了一个万亿参数的 MoE 模型,又加了一个 QK-Clip 机制防止 attention logit 爆炸,合起来叫 MuonClip2525。

分布式训练是 Muon 的一个工程难点:Newton-Schulz 需要完整的矩阵,而 ZeRO 或 FSDP 会把参数和梯度切到不同的卡上,需要额外的 gather 和调度。Dion2626 用低秩的正交化来减少通信,是这方面的一个代表。

Muon 之后的变体也很多,比较有代表性的一类是在正交化之后再加一层逐元素或逐行的自适应缩放,比如 NorMuon2727 对每个输出神经元单独做二阶矩归一化。Frans 等人的对比实验里,给 Muon 加上这类 variance adaptation 带来的提升,和从 Adam 换到 Muon 的提升差不多大2828。Track 3 上 NorMuon 是 3250 步,和调好的 Muon baseline 一样。

5. 带记忆的两侧统计量:Shampoo

5.1. 算法

Shampoo2929 其实比 Muon 早了六年,是 Gupta、Koren 和 Singer 在 2018 年提出的,出发点是 2.1 节那个算不起的 full-matrix Adagrad:对一个 m×nm \times n 的矩阵,那个预条件矩阵是 mn×mnmn \times mn 的。Shampoo 的想法是不去管这个大矩阵,只在矩阵的两侧分别做统计:

Lt=Lt−1+GtGt⊤∈ℝm×m,Rt=Rt−1+Gt⊤Gt∈ℝn×n,Wt+1=Wt−ηLt−1/4GtRt−1/4.\begin{aligned} L_t &= L_{t-1} + G_t G_t^\top \in \mathbb{R}^{m\times m}, \qquad R_t = R_{t-1} + G_t^\top G_t \in \mathbb{R}^{n\times n}, \\ W_{t+1} &= W_t - \eta\, L_t^{-1/4}\, G_t\, R_t^{-1/4} . \end{aligned}

这两个矩阵的含义挺直观的。GG⊤G G^\top 的第 (i,j)(i, j) 个元素是 GG 第 ii 行和第 jj 行的内积,也就是第 ii 个和第 jj 个输出神经元收到的梯度有多相似,累积起来,LL 记录的就是梯度在输出空间里各个方向上的能量和相关性;G⊤GG^\top G 对输入空间做同样的事。左右各乘一个负幂次,可以理解成在输出和输入两个空间里分别做白化(whitening):历史上梯度能量一直很大的方向被压下去,一直很小的方向被放大。至于为什么是 −1/4-1/4:full-matrix Adagrad 要的是那个大矩阵的 −1/2-1/2 次幂,论文证明了它可以被 Lt1/2⊗Rt1/2L_t^{1/2} \otimes R_t^{1/2}(乘上一个和梯度秩有关的常数)从上方控制住,代进去再取 −1/2-1/2 次幂,每一侧就分到了 −1/4-1/4。

和 K-FAC 放在一起看会清楚一些。两者都是 Kronecker 结构,左边一个矩阵、右边一个矩阵,但统计量和幂次都不一样:K-FAC 用的是单个样本层面的输入二阶矩 A=𝔼[xx⊤]A = \mathbb{E}[xx^\top] 和输出梯度二阶矩 B=𝔼[δδ⊤]B = \mathbb{E}[\delta\delta^\top],取 −1-1 次幂,来自 Natural Gradient;Shampoo 用的是整个 batch 梯度 GG 的左右 Gram 矩阵,取 −1/4-1/4 次幂,来自 Adagrad。后来 Morwani 等人证明,Shampoo 预条件矩阵的平方,正好是用幂迭代求 Adagrad 矩阵最优 Kronecker 近似时的第一步3030,这大概也解释了为什么取 −1/2-1/2 次幂的版本往往比原来的 −1/4-1/4 效果更好。

5.2. 和 Muon 的关系

如果去掉累积,只用当前这一步的梯度,那么 L=GG⊤L = G G^\top,R=G⊤GR = G^\top G。把 G=UΣV⊤G = U\Sigma V^\top 代进去:

L−1/4GR−1/4=(UΣ−1/2U⊤)(UΣV⊤)(VΣ−1/2V⊤)=UV⊤.L^{-1/4}\, G\, R^{-1/4} = (U \Sigma^{-1/2} U^\top)(U \Sigma V^\top)(V \Sigma^{-1/2} V^\top) = U V^\top .

这里要稍微小心一点:GG 不是方阵的时候,LL 和 RR 至少有一个是奇异的,上面的 −1/4-1/4 次幂要在伪逆、或者加上 ϵI\epsilon I 再让 ϵ→0\epsilon \to 0 的意义下理解。所以没有记忆的 Shampoo 就是 Muon 的正交化,Muon 只是额外在动量上做这件事。Track 3 的 #22 干脆把 SpectralDescent、不带动量的 Muon 和 betas 取 (0, 0) 的 Shampoo 写成了同一行,8225 步。

反过来,Shampoo 可以看成带记忆的 Muon:它用历史上累积的 LL 和 RR 来决定每个方向的步长,而 Muon 每一步只看当前的动量。最近的 Clarifying Shampoo 把这个关系类比成 Adam 和 Signum(带动量的符号下降)之间的关系3131:Signum 只看当前的符号,Adam 用历史统计量调整每个坐标的步长;Muon 只看当前的奇异向量,Shampoo 用历史统计量调整每个方向的步长。我觉得这是理解这一类方法最顺的一个类比。

5.3. 工程上的麻烦

Shampoo 一直没有像 Adam 那样普及,主要是工程上的原因:

grafting 这一条我第一次看到的时候觉得有点奇怪,等于是承认 Shampoo 自己定不好每一层的步长,方向用 Shampoo 的,大小还得借 Adam 的。Track 3 上那个 Shampoo 的提交,打开配置一看也是开着 Adam grafting 的。我自己在小模型上跑 SOAP 一类的方法,fp32 的 torch.linalg.eigh 有时(可能 5%?)会报不收敛直接退出,最后只好在失败的时候退回 fp64 重算。

Google 的 Anil 等人做了一个大规模的分布式实现3232,Meta 的 PyTorch 分布式版本3333在 2024 年的 AlgoPerf 优化器评测里拿到了 external tuning 赛道的第一3434。Track 3 上,按原始设置(−1/4-1/4 次幂、每 5 步更新)的 Shampoo 要 4100 步,只在一侧做预条件的 One-sided Shampoo 要 3375 步,都还不如 Muon。

把这几类方法的开销放在一起(对一个 m×nm \times n 的矩阵):

方法额外状态每步额外计算
SGD + 动量mnmn无
AdamW2mn2mn逐元素
Adafactor(不用动量)m+nm + n逐元素
Muonmnmn5 到 12 次 Newton-Schulz,每次几个矩阵乘法
Shampoomn+2(m2+n2)mn + 2(m^2 + n^2),含缓存的逆根每隔若干步一次 O(m3+n3)O(m^3 + n^3) 的特征分解
SOAP2mn+2(m2+n2)2mn + 2(m^2 + n^2)每步几次旋转(矩阵乘法),每隔若干步一次特征分解

6. SOAP:在 Shampoo 的特征基里跑 Adam

6.1. 出发点

SOAP(ShampoO with Adam in the Preconditioner’s eigenbasis)是 Vyas 等人 2024 年提出的3535。他们的观察在于:取 −1/2-1/2 次幂的 Shampoo,等价于先把梯度旋转到 LL 和 RR 的特征基里,在那个坐标系下跑 Adafactor,再旋转回来。

推导很短。设 L=QLΛLQL⊤L = Q_L \Lambda_L Q_L^\top,R=QRΛRQR⊤R = Q_R \Lambda_R Q_R^\top,旋转后的梯度为 G′=QL⊤GQRG' = Q_L^\top G Q_R,那么

L−1/2GR−1/2=QLΛL−1/2G′ΛR−1/2QR⊤,L^{-1/2}\, G\, R^{-1/2} = Q_L\, \Lambda_L^{-1/2}\, G'\, \Lambda_R^{-1/2}\, Q_R^\top ,

也就是在旋转后的坐标系里,第 (i,j)(i, j) 个元素被除以 λL,iλR,j\sqrt{\lambda_{L,i}\,\lambda_{R,j}}。而 LL 的第 ii 个特征值 λL,i\lambda_{L,i} 正好等于(累积意义下的)∑jGij′2\sum_j G'^2_{ij},λR,j\lambda_{R,j} 等于 ∑iGij′2\sum_i G'^2_{ij},分别是旋转后梯度平方的行和与列和,也就是 Adafactor 用来重建二阶矩的 rr 和 cc。两者只差一个标量:Adafactor 的 Vˆ\hat V 分母里还有一个 𝟏⊤r=tr⁡(L)\mathbf{1}^\top r = \operatorname{tr}(L),所以 Adafactor 的更新是这个 Shampoo 的 tr⁡(L)\sqrt{\operatorname{tr}(L)} 倍。这个因子对同一个矩阵里的所有元素都一样,但会随层和训练步数变化,相当于每一层每一步换了一个学习率,方向上两者是一致的。

那既然 Shampoo 相当于在特征基里跑 Adafactor,为什么不在这个坐标系里跑效果更好的 Adam?这就是 SOAP。

同样的思路在 2018 年的 EKFAC3636 就用过:K-FAC 在它的 Kronecker 特征基里,等价于拿两个因子特征值的乘积当每个坐标的二阶矩,EKFAC 则在这个特征基里直接重新估计每个坐标的二阶矩。SOAP 可以看成在 Shampoo 的特征基里做同样的事情,再加上 Adam 的动量和偏差修正。

6.2. 算法

记 LL 和 RR 的特征向量矩阵为 QLQ_L 和 QRQ_R,SOAP 每一步做:

  1. 用滑动平均更新 L←βL+(1−β)GG⊤L \leftarrow \beta L + (1-\beta) G G^\top,R←βR+(1−β)G⊤GR \leftarrow \beta R + (1-\beta) G^\top G;
  2. 每隔 ff 步重新算一次 QLQ_L 和 QRQ_R,原论文用特征分解,也可以用一步幂迭代加 QR 分解来近似;
  3. 把梯度和动量旋转到特征基里:G′=QL⊤GQRG' = Q_L^\top G Q_R,M′=QL⊤MQRM' = Q_L^\top M Q_R;
  4. 在旋转后的坐标系里跑 Adam:V←β2V+(1−β2)G′⊙G′V \leftarrow \beta_2 V + (1-\beta_2)\, G' \odot G',N′=M′/(V+ϵ)N' = M' / (\sqrt{V} + \epsilon);
  5. 旋转回去并更新:W←W−ηQLN′QR⊤W \leftarrow W - \eta\, Q_L N' Q_R^\top。

一阶动量 MM 在原坐标里累积,用的时候再转进特征基;二阶矩 VV 直接在特征基里累积。这样分开放是有原因的:MM 对梯度是线性的,放在原坐标里,每次用当前的特征基去转,就等于把历史上所有梯度都放进当前这组基里重新表示,特征基怎么换都对得上(如果存在旧的特征基里,eigh 返回的特征向量本来就只确定到一个正负号,重新分解时某个方向翻了号,历史动量就会和新梯度互相抵消);VV 是逐元素的平方,没法只靠 VV 本身换到新的基下,只能留在特征基里接着用,好在平方对特征向量变号不敏感,QQ 每次的变化也不大。原论文还拿这一点和 GaLore3737 做了对比,GaLore 为了省显存,把梯度投影到一个低秩子空间里跑 Adam,动量也存在投影后的空间里,子空间一换,旧的动量就对不上了。

直观上,旋转到特征基之后,梯度的各个坐标之间近似不相关,Adam 那种逐坐标处理的假设就更接近成立了,相当于先把 0.2 节右边那个转过的山谷转回来,再交给 Adam。另一个好处是稳定性:Shampoo 的预条件矩阵在两次更新之间是冻结的,而 SOAP 在两次更新特征基之间,Adam 的二阶矩还在一直适应,所以特征分解可以做得更稀疏,代价更低。当然在 Track 3 这种只看步数的设置下,这个好处就无所谓了,现在的纪录都是每一步更新特征基。

原论文在 3.6 亿和 6.6 亿参数的语言模型上报告,SOAP 比 AdamW 少用 40% 以上的迭代步数、35% 以上的 WallTime,比 Shampoo 在两项上各好 20% 左右3535。

6.3. 后续的分析

SOAP 之后,对这一类 ”矩阵白化” 优化器的分析多了起来。KL-Shampoo 和 KL-SOAP 从最小化 KL 散度的角度重新推导了两侧的统计量3838。Frans 等人的对比发现,这类方法里 variance adaptation 和谱的归一化差不多同样重要,而且只在输入一侧做 SOAP 就能恢复完整 SOAP 的大部分效果2828。Track 3 上,带 hyperball 约束的 KL-SOAP 和 SOAP 都做到了 3125 步。

7. SOAP-Muon:先预处理,再正交化

7.1. 想法

SOAP 和 Muon 都好用,那把它们串起来呢?先用 SOAP 对动量做预处理,再把结果送进 Muon 的正交化:

Ot=polar(T(Mt)),Wt+1=Wt−ηsOt,O_t = \mathrm{polar}\big(T(M_t)\big), \qquad W_{t+1} = W_t - \eta\, s\, O_t ,

T(⋅)T(\cdot) 表示 SOAP 的旋转、逐元素归一化再转回去的那一整套操作。Vyas 等人在一份简短的技术说明里讨论了这个组合3939,他们的解释是:当矩阵接近方阵、并且只需要在一侧做条件化时,Muon 本身就相当于在白化梯度,矩阵很扁或者需要两侧条件化时就不够了;先在一侧做 SOAP 再做 Muon,可以用不高的代价近似两侧的白化,他们把这类做法叫 iterative whitening。说明里的主要版本只在较小的那一侧做 SOAP,他们也提到两侧都做 SOAP 再接 Muon 效果更好,作者自己也说这些实验还比较初步。

7.2. 在 Track 3 上的演化

SOAP-Muon 是 Track 3 上推动纪录最多的一类方法,大致经历了这几步11:

#44 和 #46 里 SOAP-Muon 的实现有几点值得注意:特征基每步只用一次幂迭代加 QR 来更新;Gram 矩阵用的是原始梯度,滑动平均系数 0.9;被预处理的是动量,并且用的是上一步建好的特征基;预处理的输出还会按某种矩阵范数重新缩放,再送进 Newton-Schulz。

#44 算是比较扎实的 SOTA,但 #44 之后这两步我是有点怀疑的。#45 和 #46 各自比上一个纪录少 30 步,按 Track 3 自己给的换算大约是 0.0014 的 loss,而按提交的 log 算,seed 之间的标准差在 0.001 左右。拿 README 里两两对比的显著性公式算一下(两个提交的平均 loss 之差,加上步数之差按每 100 步 0.0045 折算出的 loss,再除以 1/n1+1/n2\sqrt{1/n_1 + 1/n_2},同样以 0.004 为线),#45 相对 #44 是 0.0043,刚刚过线;#46 相对 #45 是 0.0035,其实没过(它作为纪录是有效的,因为它自己在 2690 步上显著低于 3.28,只是这些 log 还不足以说明它比 #45 更好)。README 对 #46 的说明也很坦白,好几个细节还没有被单独证明有用,其中最好笑的一个是 Muon 动量的 cooldown 被安排在 2700 到 2900 步,而纪录在 2690 步就验收了,也就是说这段 cooldown 根本没跑到。

自己做实验的时候也有比较多这种奇怪的 setting 导致的问题,反正体感上 #44 之后的几十步,基本都是靠这样一个个开关叠出来的 buff,单看每一个都很难说清楚有没有用。

7.3. 一些有代表性的 Track 3 结果

编号方法步数
#22不带动量的 Muon(spectral descent)8225
#2Adam(单个 seed,提交者注明大概率没调好)5625
#4AdamH(Adam + hyperball 约束)4875
#21Shampoo(−1/4-1/4 次幂,每 5 步更新)4100
#33PSGD(Kronecker 预条件)3375
#35One-sided Shampoo3375
#12Muon + AdamW3325
#15Newton-Muon(输入侧激活协方差预条件 + Muon)3275
#36调好超参的 Muon + AdamW(当前基线)3250
#10NorMuon3250
#14SOAP-Muon(只用在 MLP 上)3150
#19KL-SOAP + hyperball3125
#27SOAP + hyperball3125
#44SOAP-Muon(所有隐藏层,每步更新特征基)2750
#46#44 + Tail-EMA + RowFloor + CWD(当前纪录)2690

需要注意的是,不同提交的调参力度差别很大,很多结果还叠加了学习率 schedule、初始化、hyperball 这类和更新方向无关的改动,所以它算不上一个纯算法的排名。另外在 3300 步附近,少 100 步大约相当于 val loss 低 0.004511,表里几十步的差距对应的 loss 差其实很小。即便如此,大的趋势还是很清楚,加粗的三行就是三个 stage:逐元素的 Adam 明显落后;Muon 这类把矩阵当矩阵的方法是一个大台阶;在 Muon 之上再加 SOAP 的统计量,是第二个 stage。

8. 留给下一篇的问题

把这些方法按时间顺序读下来,一个感受是它们之间的关系比我原来想的紧得多。full-matrix Adagrad 做 Kronecker 近似得到 Shampoo,Shampoo 去掉记忆就是 Muon 的正交化;Adam 的二阶矩按行列分解得到 Adafactor,把 Adafactor 搬到 Shampoo 的特征基里再换回 Adam 就是 SOAP;最后 SOAP 和 Muon 又在 SOAP-Muon 里接到了一起。逐元素和矩阵这两条线,一直在互相借用对方的东西。

读的过程中形成了一些问题:

  1. Adam 的 vv 到底在估计什么,是噪声的大小,还是某种曲率?full batch 下 Adam 依然更好,似乎说明不只是噪声。
  2. Muon 把所有奇异值拉平成 1,奇异值很小的那些方向,是值得放大的信号,还是噪声?
  3. K-FAC 用的是单个样本层面的输入和输出二阶矩,Shampoo 和 SOAP 用的是整个 batch 梯度的 Gram 矩阵,这两种统计量包含的信息一样吗?
  4. MLP 的权重是 4d×d4d \times d 这样很扁的矩阵,左右两侧统计量的维度差了四倍,哪一侧更重要?
  5. Muon 最后一步的 polar(⋅)\mathrm{polar}(\cdot) 会把所有奇异值强行拉回 1,那 SOAP-Muon 里 SOAP 费了半天劲算出来的逐元素缩放,岂不是全被抹掉了?可实际上先做 SOAP 再正交化,比单独用 Muon 少了好几百步(虽然中间也混了别的改动)。polar 抹掉的只是奇异值,奇异向量还在,而 SOAP 的逐元素缩放恰恰会改变奇异向量,也就是说 polar(T(M))≠polar(M)\mathrm{polar}(T(M)) \neq \mathrm{polar}(M)。那 SOAP 到底把奇异向量往哪边掰了,为什么掰过之后会更好?

第二篇会尝试把这一篇里的方法放进一个统一的描述里,回答其中一部分,剩下的变成可以用实验检验的假说。另外,这一篇引用了不少 Track 3 社区的结果,有的有严格的多 seed 统计,有的只是单次运行,可信程度参差不齐,怎么在小模型上做可靠的对比,也许之后还可以再写一篇单独的文章。

脚注

  1. Keller Jordan 等,Modded-NanoGPT Optimization Benchmark(Track 3),https://github.com/KellerJordan/modded-nanogpt/tree/master/records/track_3_optimization。文中的步数、规则和对各个纪录的描述均来自该页面 2026 年 9 月的版本。 ↩ ↩2 ↩3

  2. Herbert Robbins, Sutton Monro. A Stochastic Approximation Method. Annals of Mathematical Statistics, 1951. ↩

  3. Boris Polyak. Some methods of speeding up the convergence of iteration methods. USSR Computational Mathematics and Mathematical Physics, 1964. ↩

  4. Yurii Nesterov. A method for solving the convex programming problem with convergence rate O(1/k²). Soviet Mathematics Doklady, 1983. ↩

  5. Ilya Sutskever, James Martens, George Dahl, Geoffrey Hinton. On the importance of initialization and momentum in deep learning. ICML 2013. ↩

  6. Sam McCandlish, Jared Kaplan, Dario Amodei, OpenAI Dota Team. An Empirical Model of Large-Batch Training. https://arxiv.org/abs/1812.06162 ↩

  7. John Duchi, Elad Hazan, Yoram Singer. Adaptive Subgradient Methods for Online Learning and Stochastic Optimization. JMLR 2011. ↩

  8. Geoffrey Hinton, Nitish Srivastava, Kevin Swersky. Neural Networks for Machine Learning, Lecture 6e, Coursera, 2012. ↩

  9. Diederik Kingma, Jimmy Ba. Adam: A Method for Stochastic Optimization. https://arxiv.org/abs/1412.6980 ↩

  10. Jeremy Bernstein, Yu-Xiang Wang, Kamyar Azizzadenesheli, Anima Anandkumar. signSGD: Compressed Optimisation for Non-Convex Problems. https://arxiv.org/abs/1802.04434 ↩

  11. Lukas Balles, Philipp Hennig. Dissecting Adam: The Sign, Magnitude and Variance of Stochastic Gradients. https://arxiv.org/abs/1705.07774 ↩

  12. Xiangning Chen 等. Symbolic Discovery of Optimization Algorithms. https://arxiv.org/abs/2302.06675 ↩

  13. Frederik Kunstner, Lukas Balles, Philipp Hennig. Limitations of the Empirical Fisher Approximation for Natural Gradient Descent. https://arxiv.org/abs/1905.12558 ↩ ↩2

  14. Frederik Kunstner, Jacques Chen, Jonathan Lavington, Mark Schmidt. Noise Is Not the Main Factor Behind the Gap Between SGD and Adam on Transformers, but Sign Descent Might Be. https://arxiv.org/abs/2304.13960 ↩

  15. Jeremy Bernstein, Laker Newhouse. Old Optimizer, New Norm: An Anthology. https://arxiv.org/abs/2409.20325 ↩ ↩2

  16. Ilya Loshchilov, Frank Hutter. Decoupled Weight Decay Regularization. https://arxiv.org/abs/1711.05101 ↩

  17. Noam Shazeer, Mitchell Stern. Adafactor: Adaptive Learning Rates with Sublinear Memory Cost. https://arxiv.org/abs/1804.04235 ↩

  18. Shun-ichi Amari. Natural Gradient Works Efficiently in Learning. Neural Computation, 1998. ↩

  19. James Martens, Roger Grosse. Optimizing Neural Networks with Kronecker-factored Approximate Curvature. https://arxiv.org/abs/1503.05671 ↩

  20. Frederik Benzing. Gradient Descent on Neurons and its Link to Approximate Second-Order Optimization. https://arxiv.org/abs/2201.12250 ↩

  21. Keller Jordan 等. Muon: An optimizer for hidden layers in neural networks. https://kellerjordan.github.io/posts/muon/ ↩ ↩2

  22. Tim Large, Yang Liu, Minyoung Huh, Hyojin Bahng, Phillip Isola, Jeremy Bernstein. Scalable Optimization in the Modular Norm. https://arxiv.org/abs/2405.14813 ↩

  23. Greg Yang, James Simon, Jeremy Bernstein. A Spectral Condition for Feature Learning. https://arxiv.org/abs/2310.17813 ↩

  24. Jingyuan Liu 等. Muon is Scalable for LLM Training. https://arxiv.org/abs/2502.16982 ↩

  25. Kimi Team. Kimi K2: Open Agentic Intelligence. https://arxiv.org/abs/2507.20534 ↩

  26. Kwangjun Ahn 等. Dion: Distributed Orthonormalized Updates. https://arxiv.org/abs/2504.05295 ↩

  27. Zichong Li, Liming Liu, Chen Liang, Weizhu Chen, Tuo Zhao. NorMuon: Making Muon more efficient and scalable. https://arxiv.org/abs/2510.05491 ↩

  28. Kevin Frans, Pieter Abbeel, Sergey Levine. What Really Matters in Matrix-Whitening Optimizers? https://arxiv.org/abs/2510.25000 ↩ ↩2

  29. Vineet Gupta, Tomer Koren, Yoram Singer. Shampoo: Preconditioned Stochastic Tensor Optimization. https://arxiv.org/abs/1802.09568 ↩

  30. Depen Morwani 等. A New Perspective on Shampoo’s Preconditioner. https://arxiv.org/abs/2406.17748 ↩

  31. Runa Eschenhagen, Anna Cai, Tsung-Hsien Lee, Hao-Jun Michael Shi. Clarifying Shampoo: Adapting Spectral Descent to Stochasticity and the Parameter Trajectory. https://arxiv.org/abs/2602.09314 ↩

  32. Rohan Anil 等. Scalable Second Order Optimization for Deep Learning. https://arxiv.org/abs/2002.09018 ↩

  33. Hao-Jun Michael Shi 等. A Distributed Data-Parallel PyTorch Implementation of the Distributed Shampoo Optimizer for Training Neural Networks At-Scale. https://arxiv.org/abs/2309.06497 ↩

  34. Priya Kasimbeg 等. Accelerating Neural Network Training: An Analysis of the AlgoPerf Competition. https://arxiv.org/abs/2502.15015 ↩

  35. Nikhil Vyas 等. SOAP: Improving and Stabilizing Shampoo using Adam. https://arxiv.org/abs/2409.11321 ↩ ↩2

  36. Thomas George, César Laurent, Xavier Bouthillier, Nicolas Ballas, Pascal Vincent. Fast Approximate Natural Gradient Descent in a Kronecker-factored Eigenbasis. https://arxiv.org/abs/1806.03884 ↩

  37. Jiawei Zhao 等. GaLore: Memory-Efficient LLM Training by Gradient Low-Rank Projection. https://arxiv.org/abs/2403.03507 ↩

  38. Wu Lin 等. Understanding and Improving Shampoo and SOAP via Kullback-Leibler Minimization. https://arxiv.org/abs/2509.03378 ↩

  39. Nikhil Vyas 等. Improving SOAP Using Iterative Whitening and Muon. https://nikhilvyas.github.io/SOAP_Muon.pdf ↩