Plain SGD and Nesterov-momentum steps over ℝ — the optimizer peers of AdamStep #
The ℝ reference for the cifar8_{sgd,mom}_train_step family. Coordinatewise over
Vec, mirroring the emitted StableHLO op-for-op so the faithfulness theorems in StableHLO.Basic
are structural matches (rfl), exactly as AdamStep.lean is for the AdamW triple.
Why these are separate from the *Sgd ops. The kit's *Sgd op family fuses the gradient
and the update and bakes lr as a literal, so it cannot serve a scheduled optimizer (the fused
θ − lr·g was also what the AdamW render had to replace).
The renders that need these take %lr as a runtime tensor<f32> argument so one graph serves a
whole LR schedule.
Claim ceiling. Like AdamStep, the verified target here is faithfulness — the rendered
update denotes these functions — not a loss-decrease bound. Plain SGD does have descent results in
this repo (Proofs/Training/SgdDescent/), but they are about a specific net's loss under smoothness
hypotheses; nothing below claims Nesterov descends. Say "the momentum render is certified", never
"momentum is proven to descend".
The velocity convention matches the emitter it replaces (tests/TestCifar8AdamTrain.emitMomentum):
the v slot carries the velocity and the m slot is an untouched passthrough, so the packed
[θ|m|v] signature is shared verbatim with the AdamW render.
References #
- Polyak 1964, Some methods of speeding up the convergence of iteration methods (heavy ball). https://doi.org/10.1016/0041-5553(64)90137-5
- Nesterov 1983, A method of solving a convex programming problem with convergence rate O(1/k²). https://www.mathnet.ru/eng/dan46009
- Sutskever, Martens, Dahl, Hinton 2013, On the importance of initialization and momentum in deep learning. https://proceedings.mlr.press/v28/sutskever13.html
Plain SGD parameter update, coordinatewise: θ' = θ − lr·g.
lr is an ℝ here and a tensor<f32> function argument in the render — the op carries both, the
name for emission and the value for denotation, exactly as adamWParamF does.
Equations
- Proofs.sgdParam lr θ g i = θ i - lr * g i
Instances For
Nesterov velocity update: v' = μ·v + g.
Note the gradient enters undamped (coefficient 1, not 1 − μ) — that is the convention the
emitter uses, and it is the one PyTorch's SGD(momentum=μ, dampening=0) implements.
Equations
- Proofs.momVNext μ v g i = μ * v i + g i
Instances For
Nesterov parameter update: θ' = θ − lr·(g + μ·v'), where v' is the updated velocity.
The g + μ·v' look-ahead is what makes this Nesterov rather than heavy-ball; plain momentum
would step by v' alone (see momParam_heavyBall_diff).
Equations
- Proofs.momParam μ lr θ v g i = θ i - lr * (g i + μ * Proofs.momVNext μ v g i)
Instances For
One Nesterov step: the new parameter together with the new velocity — the pair the rendered
train step returns per parameter (m rides through unchanged).
Equations
- Proofs.momStep μ lr θ v g = (Proofs.momParam μ lr θ v g, Proofs.momVNext μ v g)
Instances For
μ = 0 collapses Nesterov to plain SGD. The bridge between the two renders, and a cheap
check that the momentum formula has no stray term: at zero momentum v' = g and the look-ahead
g + 0·v' is just g.
Nesterov is heavy-ball plus one extra μ·v' of look-ahead. Stated to pin which variant is
rendered: a heavy-ball emitter would step by lr·v', and this is the exact difference.
Velocity is affine in the gradient — the property the shard/DP gates rely on when they use
a moment slot as a gradient proxy: at v = 0, v' = g exactly, so averaging velocities across
replicas is averaging gradients. (adamMNextF's analogue is m' = (1−β₁)·g.)