3.6 — AdamW: decoupled weight decay
Goal: See why Adam-with-L2 is not the same as Adam-with-weight-decay, and derive the AdamW update used in every modern LLM.
1. Why this session exists
If you read Adam optimiser configs in real LLM code (GPT-NeoX, LLaMA, Mistral, nanoGPT), you almost never see Adam(weight_decay=...). You see AdamW(weight_decay=...). The difference is one line of the update, and it matters enough that every serious modern training run uses the W variant.
This session pins down why. The short version: in plain SGD, "add an L2 penalty to the loss" and "shrink weights toward zero each step" are the same thing. In Adam they are not, because of the per-parameter scaling . Loshchilov & Hutter (2017, Decoupled Weight Decay Regularization) noticed this and fixed it by re-coupling the shrinkage in the parameter update rather than in the gradient.[2, 3, 4]
2. L2 vs weight decay in plain SGD
Start with plain SGD on a loss with L2 penalty:
with . The SGD step is
Read it: take the usual SGD step, then multiply by the shrink factor . This is what people call weight decay: an explicit multiplicative shrinkage of the parameters at every step.
So in plain SGD, "L2 penalty on the loss" and "decay the weights by at each step" are algebraically identical. The two names refer to the same update.
3. The non-equivalence in Adam
Now do the same with Adam. Add an L2 penalty to the loss, so the gradient becomes . Adam's EMAs run on , not on :
The update is then
The shrinkage of is not a clean anymore. The L2 term gets folded into the EMAs and rescaled by . Concretely: parameters with large gradients (large ) get shrunk less, because their effective LR for the L2 term is reduced by .
That is the wrong direction. The whole point of weight decay is to keep weights small; "shrink less the parameters that already have big gradients" defeats the regulariser.
4. The AdamW update
Loshchilov & Hutter's fix: decouple the weight decay from the gradient pipeline. Compute Adam's adaptive step on the unmodified gradient , then apply weight decay as a separate multiplicative shrinkage of :
The only line that changed: the parameter update now multiplies by before subtracting the Adam step. The EMAs no longer see the L2 gradient.
The shrinkage rate is uniform across all parameters: every coordinate gets multiplied by exactly each step, independent of its gradient history. This is the L2-on-SGD behaviour, restored.
5. The two updates side by side
Adam-with-L2 (the "wrong" thing):
AdamW (the right thing):
The decay term in AdamW is not rescaled by . That single difference is responsible for AdamW's robustly better generalisation across vision, NLP, and LLMs.
A useful mental model: in AdamW, the shrinkage acts on the parameter; in Adam-with-L2, the shrinkage acts on the gradient. The first is what we mean by "decay". The second only happens to coincide with decay in the plain-SGD case where there is no adaptive scaling.
6. Practical defaults for LLMs
Modern LLM training (GPT-3, LLaMA, Mistral, Qwen) ships AdamW with:
- peak (smaller for bigger models; see scaling laws in Ch.7).
- , (note: smaller than Adam's ; LM gradients are noisier and we want to track them).
- .
- (weight decay).
- Cosine LR schedule with linear warmup over first 1% of steps (next session).
Notably, weight decay is usually applied only to weight matrices, not to biases, layer-norm parameters, or embedding bias. Why? Layer-norm are scale parameters of the activations — shrinking them toward zero would deliberately break the unit-variance property LN tries to preserve. Biases see no L2 penalty in Bayesian-prior derivations either (see Appendix 3.A for the L2-as-Gaussian-prior derivation). Most implementations expose a param_groups mechanism so you can set weight_decay=0 on those parameters.
no_decay = ["bias", "LayerNorm.weight", "ln.weight"]
optimizer_groups = [
{"params": [p for n, p in model.named_parameters()
if not any(nd in n for nd in no_decay)],
"weight_decay": 0.1},
{"params": [p for n, p in model.named_parameters()
if any(nd in n for nd in no_decay)],
"weight_decay": 0.0},
]
optimizer = torch.optim.AdamW(optimizer_groups, lr=6e-4, betas=(0.9, 0.95))
This snippet (or a near-clone of it) lives in every modern LLM training script.
7. When to use Adam vs AdamW
- AdamW is the modern default. Always start here for LLMs, large vision models, and anything with substantial regularisation.
- Plain Adam (no decay) is fine when you don't want any weight decay — e.g., RL policy optimisation, where shrinking the policy weights toward zero has no statistical motivation.
- Adam with L2 (as in old PyTorch's
weight_decay) is a historical accident. Don't use it. If you see a paper or codebase that does, treat its weight-decay coefficients as uncalibrated.
8. Takeaways
- In plain SGD, L2 penalty on the loss = multiplicative weight decay each step. Identically.
- In Adam, the two are not equivalent: L2 gets rescaled by , which weakens it for parameters with large gradients.
- AdamW decouples them: apply shrinkage to separately from the Adam step on the unmodified gradient .
- This is the optimiser used by every modern LLM. Typical .
- Exclude biases and LayerNorm parameters from weight decay.
(a) Verify the identity . Why does this same identity fail to hold once is replaced by an adaptive step ?
(b) A parameter has a consistently large gradient: . Compare the effective shrinkage rate on under Adam-with-L2 () versus AdamW (). Numerically, by how much does the L2 version under-shrink relative to AdamW for this coordinate?
(c) Argue (informally) why you'd want to exclude bias parameters from weight decay. Hint: think about what the bias parameter "means" in a typical layer and what the L2-as-Gaussian-prior interpretation (3.8) would say about its prior.
(d) Modify the Adam pseudocode from 3.5 §7 to become AdamW. How many lines change? Show the diff.
References
- [1]
Loshchilov, I., & Hutter, F. (2019). "Decoupled weight decay regularization". ICLR. arXiv:1711.05101 — AdamW (3.6).
↩ - [2]
Chen, X., Liang, C., Huang, D., et al. (2023). "Symbolic discovery of optimization algorithms" (Lion). arXiv:2302.06675 — Lion (3.6 aside).
↩ - [3]
Liu, H., Li, Z., Hall, D., et al. (2023). "Sophia: A scalable stochastic second-order optimizer for language model pre-training". arXiv:2305.14342 — Sophia (3.6 aside).
↩ - [4]
Jordan, K., et al. (2024). "Muon: An optimizer for hidden layers in neural networks". keller-jordan.github.io — Muon (3.6 aside).
↩