Sam Babichenko

Reading notes / NanoGPT speedrun

bubbleforce: the edge of stability for Muon

Cheng and Zheng, 2026

Optimizer
Wt,W_t, ttweights after update tt; update index
η\etalearning rate
L,L, ∇L,\nabla L, HHtraining loss; its gradient; its Hessian ∇2L\nabla^2L
MtM_tmomentum ∑i=0Kci∇L(Wt−i)\sum_{i=0}^Kc_i\nabla L(W_{t-i})
ci,c_i, KKmomentum kernel weights, ∑ici=1\sum_ic_i=1; last lag, so K+1K+1 weights
β,\beta, ν\numomentum decay; Nesterov mix
A,A, msign\mathrm{msign}map from momentum to update; Muon's polar factor UV⊤UV^\top
γ\gammadecoupled weight decay coefficient
Stability
λ,\lambda, λ(W)\lambda(W)sharpness, the top Hessian eigenvalue; its value at WW
κ\kappalearning rate times curvature, ηλ\eta\lambda or ηζ\eta\zeta
Pκ,P_\kappa, zzcharacteristic polynomial of the kernel; one of its roots
ρ(P)\rho(P)root radius, the largest root modulus
κ⋆\kappa_\starbubbleforce, the first κ\kappa with a root on the unit circle
Wˉ,\bar W, u,u, xtx_tcenter of the oscillation; top Hessian eigenvector; displacement along it
Muon
T,\mathcal T, JTJ_{\mathcal T}update as a function of current weights, past gradients frozen; its Jacobian
Jmsign(M)J_{\mathrm{msign}}(M)derivative of the polar factor at MM
ζt\zeta_tpathspring, largest positive real eigenvalue of JmsignHJ_{\mathrm{msign}}H
Constants
η=0.025,\eta=0.025, γ=0.05\gamma=0.05baseline Muon learning rate and weight decay
β=ν=0.95\beta=\nu=0.95baseline Nesterov momentum
κ⋆≈26.9\kappa_\star\approx26.9bubbleforce of the baseline kernel (β=ν=0.95\beta=\nu=0.95)
K=255K=255last lag of the 256-weight kernel in the experiments
N=524,288N=524{,}288tokens per update; the figures plot ζ/N\zeta/N

Context. Run full-batch gradient descent with a constant learning rate the step size: each update subtracts η\eta times the update direction from the weights on a neural network, and the λ\lambda, the largest eigenvalue of the Hessian ∇2L\nabla^2L of the training loss LL at the current weights: the curvature along the steepest direction λ\lambda rises until ηλ≈2\eta\lambda\approx2 and then stays there while the loss keeps falling, although on a fixed quadratic any ηλ>2\eta\lambda>2 diverges. This is the the regime, found by Cohen et al. (2021), in which training holds the sharpness at the largest value the learning rate can tolerate, λ≈2/η\lambda\approx2/\eta for gradient descent (Cohen et al., 2021). No such threshold was known for an optimizer for hidden weight matrices that replaces the momentum matrix by its polar factor UV⊤UV^\top, so every singular direction moves by the same amount, the optimizer behind the recent records of the a public race to train a small GPT-2 model to a fixed validation loss in the least wall-clock time, in Keller Jordan's modded-nanogpt repository. Jeffrey Siedar Cheng and Jack Neo Zheng's post bubbleforce: Predicting the Edge-of-Stability for Muon (2026) derives such a threshold for Muon and tests it on the speedrun's Muon baseline, with code on GitHub.

Any momentum rule that averages past gradients with fixed weights has a threshold for learning rate times curvature, read off the roots of one polynomial: the post calls it the smallest value of ηλ\eta\lambda at which a fixed quadratic stops converging under a given momentum rule, and it equals 2 for plain gradient descent. For Muon the curvature to use is the the largest positive real eigenvalue of the Jacobian of Muon's update with respect to the weights, divided by the weight c0c_0 of the current gradient in the momentum. The post's claim, a conjecture supported by runs, is that Muon training settles at ηζ=κ⋆\eta\zeta=\kappa_\star, fast enough to track a decaying learning rate and to be steered along a chosen path by learning rate and momentum alone.

Setting. Write the momentum at update tt as a weighted sum of past gradients, Mt=∑i=0Kci∇L(Wt−i)M_t=\sum_{i=0}^{K}c_i\nabla L(W_{t-i}), where the weights after tt updates are the weights and the the weights c0,c1,…,cKc_0,c_1,\ldots,c_K that the momentum puts on the current gradient and the KK before it, normalized to ∑ici=1\sum_ic_i=1 cic_i sums to 1; the experiments keep 256 weights, KK=255{}=255. Gradient descent has c0=1c_0=1. momentum that steps along an exponential moving average of past gradients instead of the current gradient plus momentum that steps along a mix of the current gradient and the moving average, giving the newest gradient extra weight momentum with decay the momentum decay: the moving average multiplies its old value by β\beta and adds (1−β)(1-\beta) times the new gradient and Nesterov mix the weight on the moving average in the step; 1−ν1-\nu goes to the current gradient alone, ν=1\nu=1 is heavy-ball, ν=β\nu=\beta is standard Nesterov has ci=(1−ν)1{i=0}+ν(1−β)βic_i=(1-\nu)\mathbb 1\{i=0\}+\nu(1-\beta)\beta^i; the speedrun's Muon uses ν=β=0.95\nu=\beta=0.95. A one whose update is a fixed map AA of the momentum, Wt+1=Wt−ηA(Mt)W_{t+1}=W_t-\eta A(M_t), with no other running state such as Adam's second moments updates Wt+1=Wt−ηA(Mt)W_{t+1}=W_t-\eta A(M_t): AA is the identity for SGD with momentum, and for Muon it is msign(M)\mathrm{msign}(M)=UV⊤{}=UV^\top. The test bed is Konstantin Mishchenko's tuned Muon baseline for the speedrun: a 12-layer GPT-2 trained with 524,288 tokens per update for 3,250 updates, Muon with η=0.025\eta=0.025 and shrinking the weights by the factor 1−ηγ1-\eta\gamma at each update, separately from the gradient step; γ=0.05\gamma=0.05 here on the hidden matrices, Adam with decoupled weight decay, the optimizer that scales each coordinate's step by running estimates of its gradient's mean and size on the rest, and a learning rate constant for 975 updates, then decayed linearly to zero.

Main findings. On a fixed quadratic loss the threshold is a theorem: if why: each eigendirection of HH is its own recurrence with κ=ηλj≤ηλmax⁡\kappa=\eta\lambda_j\le\eta\lambda_{\max}; below κ⋆\kappa_\star all of its roots lie inside the unit circle, so every mode decays (Step 1), the iterates converge. Everything about real training is measured, one run per setting: after shocks to learning rate or momentum, through the learning-rate decay, and along a drawn path, the measured ζ\zeta follows κ⋆/η\kappa_\star/\eta, with the exceptions in Steps 4 to 6.

For someone training with Muon, the law turns two hyperparameters into one curvature setting: the settled pathspring is κ⋆/η\kappa_\star/\eta, and momentum enters only through κ⋆\kappa_\star, which for ν=β\nu=\beta is 13.4 at β≈0.90\beta\approx0.90, 26.9 at 0.95 and 53.8 at β≈0.975\beta\approx0.975 (my check). A momentum change can therefore do what a learning-rate change does. Whether a lower or higher ζ\zeta trains to a better loss is not tested.

Step 1

On a fixed quadratic, each momentum kernel has a threshold set by the roots of a polynomial. Take L(W)=12λ∥W∥2L(W)=\tfrac12\lambda\|W\|^2 and write κ=ηλ\kappa=\eta\lambda. The update Wt+1=Wt−κ∑iciWt−iW_{t+1}=W_t-\kappa\sum_ic_iW_{t-i} is a linear recurrence whose solutions are sums of modes Wt∝ztW_t\propto z^t, with zz a root of the the polynomial whose roots are the per-update growth factors of the recurrence's modes; a complex root zz gives a mode that scales by ∣z∣|z| and rotates by the angle of zz each update,Pκ(z)=zK+1−zK+κ∑i=0KcizK−i.P_\kappa(z)=z^{K+1}-z^K+\kappa\sum_{i=0}^{K}c_iz^{K-i}.A root with ∣z∣>1|z|>1 is a mode that grows at every update, so the recurrence is stable when the the largest modulus of a root, ρ(P)=max⁡{∣z∣:P(z)=0}\rho(P)=\max\{|z|:P(z)=0\} is below 1. Small κ>0\kappa>0 is stable, and bubbleforce is the first κ\kappa at which a root reaches the unit circle,κ⋆(c)=min⁡{κ>0:ρ(Pκ)=1}.\kappa_\star(c)=\min\{\kappa>0:\rho(P_\kappa)=1\}.For gradient descent Pκ(z)=z−1+κP_\kappa(z)=z-1+\kappa, whose root 1−κ1-\kappa leaves the circle at −1-1 when κ=2\kappa=2. For untruncated heavy-ball plus Nesterov momentum the moving average is one extra state, and Pκ(z)=z2−(1+β−(1−νβ)κ)z+β(1−(1−ν)κ)P_\kappa(z)=z^2-(1+\beta-(1-\nu\beta)\kappa)z+\beta(1-(1-\nu)\kappa). A nonreal pair of roots has squared modulus equal to the constant term, at most β<1\beta<1, so it never reaches the circle; Pκ(1)=κ(1−β)>0P_\kappa(1)=\kappa(1-\beta)>0 rules out +1+1; the exit is at z=−1z=-1, a period-two oscillation, and Pκ(−1)=0P_\kappa(-1)=0 gives Gitman et al.'s (2019) thresholdκ⋆=2(1+β)1+β−2νβ.\kappa_\star=\frac{2(1+\beta)}{1+\beta-2\nu\beta}.For a Hessian the Hessian ∇2L\nabla^2L of the loss, constant on a quadratic, with eigenvalues λ1,…,λd\lambda_1,\ldots,\lambda_d the eigendirections decouple with κj=ηλj\kappa_j=\eta\lambda_j, which gives the theorem above. At β=ν=0.95\beta=\nu=0.95, κ⋆≈26.9\kappa_\star\approx26.9 (my check, unchanged by the 256-weight truncation), about 13 times gradient descent's 2. Lee and Lee (2026) give this general construction concurrently; the post's own contribution is the extension to Muon.

Step 2

For gradient descent, oscillation pushes the sharpness down, so training settles at the threshold. The argument is Damian et al.'s (2023). Write Wt=Wˉ+xtuW_t=\bar W+x_tu, with the local time average of the iterates, the center the run oscillates around the center of the oscillation, the unit top eigenvector of the Hessian at Wˉ\bar W the top Hessian eigenvector and the signed displacement of WtW_t from Wˉ\bar W along uu the displacement along it. Averaging the gradient over a symmetric oscillation removes the term linear in xtx_t, and for a simple top eigenvalue the quadratic term, which comes from the loss's third derivative, is the gradient of the sharpness:E[∇L(Wt)]≈∇L(Wˉ)+12 E[xt2] ∇λ(Wˉ)⏟sharpness penalty.\begin{aligned}\mathbb E[\nabla L(W_t)]&\approx\nabla L(\bar W)\\&\quad+\underbrace{\tfrac12\,\mathbb E[x_t^2]\,\nabla\lambda(\bar W)}_{\text{sharpness penalty}}.\end{aligned}The oscillation thus performs gradient descent on the sharpness until ηλ<2\eta\lambda<2 and it dies, and the steady rise of the sharpness during training while it is below the threshold; it is observed, and has no accepted explanation pushes λ\lambda back up. The post's Conjecture 1 extends this to any kernel, ηλt→κ⋆\eta\lambda_t\to\kappa_\star, under why: something must keep raising the curvature; without it the run can sit below the threshold and nothing brings it to the edge and a why: the threshold of Step 1 is computed for a fixed HH; it applies to a real run only if HH changes little over the updates the kernel remembers; the second fails in practice, since top Hessian eigenvectors turn by about 50° per update.

Step 3

For Muon the pathspring takes the place of the sharpness, and the same polynomial applies. Freeze the past gradients at update tt; the update is then the the update as a function of the current weights with past gradients frozen T(W)=A(c0∇L(W)+∑i>0ci∇L(Wt−i))\mathcal T(W)=A\big(c_0\nabla L(W)+\sum_{i>0}c_i\nabla L(W_{t-i})\big), with Wt+1=Wt−ηT(Wt)W_{t+1}=W_t-\eta\mathcal T(W_t). By the chain rule its Jacobian at WtW_t isJT(Wt)=c0 Jmsign(Mt) H(Wt),J_{\mathcal T}(W_t)=c_0\,J_{\mathrm{msign}}(M_t)\,H(W_t),with the derivative of the polar factor msign\mathrm{msign} at the momentum MtM_t, a linear map on matrices and H(Wt)H(W_t) the Hessian. The pathspring is ζt=λmax⁡+(JT(Wt)/c0)\zeta_t=\lambda^{+}_{\max}\big(J_{\mathcal T}(W_t)/c_0\big), the largest positive real eigenvalue of Jmsign(Mt)H(Wt)J_{\mathrm{msign}}(M_t)H(W_t), defined for why: the definition divides by c0c_0; a kernel with no weight on the current gradient makes T\mathcal T independent of the current weights. A displacement δW\delta W of the weights changes the momentum by δMt≈∑iciH δWt−i\delta M_t\approx\sum_ic_iH\,\delta W_{t-i} and the update by Jmsign(Mt)[δMt]J_{\mathrm{msign}}(M_t)[\delta M_t]. Holding HH and JmsignJ_{\mathrm{msign}} fixed over the kernel's window, a displacement along an eigenvector of JmsignHJ_{\mathrm{msign}}H with eigenvalue ζt\zeta_t obeysδWt+1=δWt−ηζt∑ici δWt−i,\delta W_{t+1}=\delta W_t-\eta\zeta_t\sum_ic_i\,\delta W_{t-i},the recurrence of Step 1 with κ=ηζt\kappa=\eta\zeta_t. So Muon's threshold is ηζ=κ⋆\eta\zeta=\htmlData{gr=gn8}{\kappa_\star}, with the same κ⋆\kappa_\star as SGD with that kernel, and the post's Conjecture 2 is that non-adaptive optimizers settle there; for SGD, ζ\zeta is the sharpness. The post measures ζ\zeta by a Krylov method that finds a few extreme eigenvalues of a large, possibly nonsymmetric matrix using only matrix-vector products on JT/c0J_{\mathcal T}/c_0, each product a HvHv, computed by differentiating ∇L⋅v\nabla L\cdot v once more, without forming HH followed by a Jmsign(M)[V]J_{\mathrm{msign}}(M)[V], the first-order change in Muon's update when the momentum moves by VV, computed by differentiating through the Newton–Schulz iterations that Muon uses to approximate the polar factor: 6 to 13 minutes per value on 8 H100 GPUs.

Step 4

After a change of learning rate or momentum, the pathspring moves to the new predicted level. The baseline was run at constant η=0.025\eta=0.025 through update 2,009 and then changed once: all learning rates halved or doubled (the AdamW ones too, per the code's configs), or β=ν\beta=\nu lowered to 0.8991 or raised to 0.9751 to halve or double κ⋆\kappa_\star. The measurements spike above the prediction, at times several-fold, and what the law matches is their floor, which the post estimates with a a curve through the lowest level around which the measurements repeatedly cluster within a window of 400 updates on each side. In every case the floor moves from the shared prediction to the new one: the post says within a few updates; by my reading of the figures, about two updates after the momentum decrease and a few hundred, approaching from above, after the doubled learning rate. After the momentum increase the floor stays about 10% below the prediction for the 4,000 updates measured, and the mean sits on it; the authors suspect the kernel's doubled memory (mean gradient age 40 updates instead of 20).

Step 5

The prediction tracks the pathspring through a decaying learning rate and a changing kernel. The post has no theory for a moving target. In the unmodified baseline the prediction rises as η\eta decays, and the measurements follow it, about fivefold, to near update 2,700; after that the prediction grows without bound and the measurements level off, because late in a linear decay η\eta falls by a large fraction per update, faster than ζ\zeta can settle (the authors' explanation). A harder test is the authors' the authors' momentum kernel: the maximum-entropy weights over 256 lags under linear constraints on c0c_0, c1c_1, total weight, mean lag and spread, and odd against even lags kernel, maximum-entropy weights under linear constraints that include equal total weight on odd and even lags, ∑i(−1)ici=0\sum_i(-1)^ic_i=0; from update 1,000 its weights move linearly between two such solutions (the code's config). That constraint makes Pκ(−1)=±2P_\kappa(-1)=\pm2 for every κ\kappa (my check), which closes the period-two exit, so the roots that reach the circle first are a nonreal pair. The measured ζ\zeta again follows the prediction to about update 2,700, somewhat above it early on (my reading of the figure). MaxEntSlop's speedrun submission, which also changes the weight-decay schedule, takes 3,060 updates instead of 3,250 (190 fewer; the post also says 200); the authors do not recommend it.

Step 6

Learning rate and momentum together can steer the pathspring along a chosen path. Choosing ηt\eta_t and βt\beta_t so that κ⋆(βt)/ηt\kappa_\star(\beta_t)/\eta_t equals a target should make ζ\zeta follow it. With η\eta fixed at 0.0125 and the momentum scheduled, the target traced the upper outline of a Bulbasaur drawing over roughly updates 3,500 to 11,500 (read from the figure); most measurements stayed within 15% of it, and the validation loss kept falling. The lower outline, near a quarter of that level, failed: after about 3,000 updates ζ\zeta ran to 2 to 10 times the target, the losses stopped falling at about the same time, and a simplified outline and other learning rates also failed. The authors guess that progressive sharpening makes low targets unstable.

Limits and open questions. All runs use one model and data set. What holds Muon at the threshold is unknown: Damian et al.'s argument needs oscillation along a fixed top eigenvector, but only about 25% of Muon's oscillation lies in the span of the top 64 eigenvectors of JTJ_{\mathcal T}, and those turn by about 75° per update. With no theory of progressive sharpening, nothing predicts how fast ζ\zeta returns or which targets can be reached. The extension to Adam failed: as Bai et al. (2026) found, nearly dead neurons make top eigenvalues a poor measure of its curvature.

Terms

Edge of stability
training holds the sharpness at the largest value the learning rate tolerates: ηλ≈2\eta\lambda\approx2 for gradient descent
Momentum kernel
the weights cic_i the momentum puts on past gradients, Mt=∑ici∇L(Wt−i)M_t=\sum_ic_i\nabla L(W_{t-i}), with ∑ici=1\sum_ic_i=1
Characteristic polynomial PκP_\kappa
zK+1−zK+κ∑icizK−iz^{K+1}-z^K+\kappa\sum_ic_iz^{K-i}; its roots are the growth factors of the modes on a quadratic with κ=ηλ\kappa=\eta\lambda
Bubbleforce κ⋆\kappa_\star
the first κ>0\kappa>0 at which a root of PκP_\kappa reaches the unit circle; 2 for gradient descent, 2(1+β)/(1+β−2νβ)2(1+\beta)/(1+\beta-2\nu\beta) for heavy-ball plus Nesterov
Self-stabilization
oscillation along the top eigenvector adds the step −η2E[x2]∇λ-\tfrac\eta2\mathbb E[x^2]\nabla\lambda, which lowers the sharpness until the oscillation dies (Damian et al.)
Progressive sharpening
the observed rise of the sharpness during training below the threshold; no theory yet
Non-adaptive optimizer
Wt+1=Wt−ηA(Mt)W_{t+1}=W_t-\eta A(M_t) for a fixed map AA: SGD, Muon; not Adam
Pathspring ζ\zeta
largest positive real eigenvalue of JT/c0=Jmsign(M)HJ_{\mathcal T}/c_0=J_{\mathrm{msign}}(M)H; the sharpness for SGD
Bubbleforce law
ηζ=κ⋆\eta\zeta=\kappa_\star: conjectured equilibrium, matched by the floor of the measurements in nanoGPT runs
Lower envelope
the level the measurements repeatedly return to from above; what the law is compared with
MaxEntSlop
maximum-entropy kernel with ∑i(−1)ici=0\sum_i(-1)^ic_i=0, which removes the period-two exit at z=−1z=-1
Sharpness scheduling
choosing ηt\eta_t and βt\beta_t so that κ⋆(βt)/ηt\kappa_\star(\beta_t)/\eta_t follows a target path for ζ\zeta

Quiz

  1. MaxEntSlop forces ∑i(−1)ici=0\sum_i(-1)^ic_i=0. What does this do to the threshold calculation?
  2. Which part of the gradient-descent story does the post not carry over to Muon?

Sources