Home › Neural network training & inference › Optimizers & schedules
algorithm

AdamW

Problem Someone training a transformer or similar large model needs an optimizer that adapts step sizes per parameter and still applies regularization correctly, so training is stable and generalises well.

Adam's per-parameter adaptive steps with weight decay applied directly to the weights; the default optimizer for transformers and LLM pretraining.

O(P) per step; 2 fp32 states (8 bytes/param); 16 bytes/param of model state with mixed precision
optimizeradamweight-decayadaptivellm
Overview

Adam divides a momentum estimate m by the root of a running mean of squared gradients v, so every parameter moves about lr per step whatever its gradient scale. Adam with L2 regularization gets decay wrong: the decay term is also divided by √v, so weights with large gradients are barely regularized. AdamW applies decay to the weights directly, outside the adaptive step.

Key facts

Use when

  • Transformers, diffusion models, most fine-tuning.
  • Language models (GPT-3 style recipes, LLaMA), vision transformers (Swin Transformer: AdamW, weight decay 0.05, batch 1024) and speech recognition (Whisper: AdamW with gradient-norm clipping).

Avoid when

  • Optimizer memory is the limit: 8-bit Adam, Adafactor, or ZeRO.
  • Convnet recipes tuned for SGD with momentum.

Deep dive

How it works

In: weights w, gradient g, learning rate lr, step count t · Out: updated weights · State: m and v per parameter (fp32) plus t · Cost: O(P) per step, 28 B/param of memory traffic in fp32.

  1. Update the first moment m (momentum) and the second moment v (per-coordinate gradient scale).
  2. Bias-correct both, since they start at 0.
  3. Shrink the weights by lr·λ (decoupled decay).
  4. Subtract lr·m̂/(√v̂+ε).

Per step t, with gradient g:

m ← β1·m + (1−β1)·g                 first moment (momentum)
v ← β2·v + (1−β2)·g²                second moment (per-coordinate scale)
m̂ = m / (1−β1ᵗ),  v̂ = v / (1−β2ᵗ)    bias correction: m and v start at 0
w ← w − lr·λ·w                      decoupled decay (PyTorch multiplies λ by lr)
w ← w − lr·m̂ / (√v̂ + ε)
  • The update size is bounded by about lr per coordinate, since |m̂/√v̂| ≲ 1 for stationary gradients. That makes lr a step size in weight units.
  • β2 sets the averaging window, about 1/(1−β2): 1000 steps at 0.999 and 20 at 0.95. A short window reacts quickly to gradient-scale jumps, which suppresses LLM loss spikes.
  • The equilibrium weight norm is set by lr·λ. Changing lr also changes the effective regularization.

Code

pub struct AdamW {
    pub beta1: f32, pub beta2: f32, pub eps: f32, pub weight_decay: f32,
    t: i32, m: Vec<f32>, v: Vec<f32>,
}

impl AdamW {
    pub fn new(n: usize) -> Self {
        Self { beta1: 0.9, beta2: 0.95, eps: 1e-8, weight_decay: 0.1, t: 0, m: vec![0.0; n], v: vec![0.0; n] }
    }

    /// Fused update matching torch.optim.AdamW. `decay = false` for biases, norm scales, (often) embeddings.
    pub fn step(&mut self, w: &mut [f32], g: &[f32], lr: f32, decay: bool) {
        self.t += 1;
        let (b1, b2) = (self.beta1, self.beta2);
        let step = lr / (1.0 - b1.powi(self.t));                 // fold bias correction 1 into lr
        let bc2_sqrt = (1.0 - b2.powi(self.t)).sqrt();
        let shrink = if decay { 1.0 - lr * self.weight_decay } else { 1.0 };
        for i in 0..w.len() {
            let gi = g[i];
            self.m[i] = b1 * self.m[i] + (1.0 - b1) * gi;
            self.v[i] = b2 * self.v[i] + (1.0 - b2) * gi * gi;
            let denom = self.v[i].sqrt() / bc2_sqrt + self.eps;  // eps outside the sqrt, as in PyTorch
            w[i] = w[i] * shrink - step * self.m[i] / denom;
        }
    }
}

/// Global-norm clipping across ALL parameters (and all shards) before the step.
pub fn clip_global_norm(grads: &mut [&mut [f32]], max_norm: f32) -> f32 {
    let sq: f64 = grads.iter().flat_map(|g| g.iter()).map(|&x| (x as f64) * (x as f64)).sum();
    let norm = sq.sqrt() as f32;
    if norm > max_norm {
        let s = max_norm / (norm + 1e-6);
        grads.iter_mut().for_each(|g| g.iter_mut().for_each(|x| *x *= s));
    }
    norm
}

fn main() {
    // Two coordinates with gradients 1e4x apart still move at about the same speed.
    let mut w = vec![1.0f32, 1.0];
    let mut opt = AdamW::new(2);
    for _ in 0..100 {
        let mut g = vec![1e-3 * w[0], 10.0 * w[1]];
        clip_global_norm(&mut [&mut g[..]], 1e3);
        opt.step(&mut w, &g, 1e-2, true);
    }
    println!("w after 100 steps: {:?}", w); // both about 0.09: same speed despite 1e4x gradient ratio
    assert!(w[0] < 0.5 && w[1].abs() < 0.5);
}

Complexity and constants

  • Traffic per param in fp32: read w, g, m, v and write w, m, v = 28 B. For 7B params that is 196 GB, about 60 ms at 3.35 TB/s if unsharded. Under ZeRO with N ranks it divides by N.
  • Model-state memory with mixed precision: 2 (bf16 w) + 2 (bf16 g) + 4 (fp32 master) + 4 (m) + 4 (v) = 16 B/param (ZeRO paper accounting).
  • 8-bit block-wise states (Dettmers et al.) cut m and v to about 2 B/param total with matching quality on the reported LLM and vision benchmarks.

Efficiency

  • Cost: per parameter the fp32 step reads w, g, m, v and writes w, m, v: 28 B, so 196 GB for 7B parameters, about 60 ms at 3.35 TB/s (H100 SXM HBM3 bandwidth, NVIDIA spec). Model state is 16 B/param with mixed precision (ZeRO paper accounting). Bound: memory bandwidth, not compute.
  • Levers:
    • fused multi-tensor kernel — one pass over all tensors instead of about 10 kernel launches per tensor (this page's count) — GPU training with many small tensors
    • shard states with ZeRO/FSDP — per-rank state divided by N ranks (ZeRO paper) — multi-GPU training
    • 8-bit block-wise m and v — states from 8 B to about 2 B/param (Dettmers et al.) — optimizer memory is the limit
    • factored second moment (Adafactor) — O(n+m) state for an n×m matrix (Shazeer and Stern) — very large matrices
    • global-norm clipping fused with the step — one fewer pass over gradients — every run with clipping
  • Naive → standard → state of the art:
    • per-tensor Python loop · about 10 launches per tensor, launch-bound
    • fused or foreach multi-tensor kernel · one memory-bound pass, 28 B/param
    • fused kernel with sharded or 8-bit states · state divided by N ranks or reduced to about 2 B/param

Implementation notes

  • Use the multi-tensor fused kernel (torch.optim.AdamW(fused=True), apex/DeepSpeed FusedAdam). A per-tensor Python loop launches about 10 kernels per parameter tensor and can cost more than the update itself.
  • Keep m, v and the master weights in fp32. In bf16, v (which holds g² ≈ 1e-10) loses its meaning and ε dominates.
  • Two parameter groups: decay for matmul weights, no decay for biases and norm γ/β. Embedding decay varies by recipe.
  • Clip on the global norm across all shards and pipeline stages. Under FSDP use the module's clip_grad_norm_, which all-reduces the squared norms.
  • Checkpoint the step count t along with m and v. Bias correction depends on it.

Pitfalls

  • torch.optim.Adam(weight_decay=…) is L2 regularization, not AdamW.
  • λ semantics differ. PyTorch and Optax shrink by lr·λ per step, so λ = 0.1 with lr = 3e-4 means a factor of 1 − 3e-5. The paper scales decay by the schedule multiplier only. Recipes do not port 1:1 between frameworks.
  • β2 = 0.999 on large LLMs: rare large gradients after a long quiet stretch blow up m/√v and cause loss spikes. 0.95 is the common fix.
  • ε too small with fp16 states underflows. ε too large (1e-4+) turns Adam into momentum SGD with lr/ε scaling.
  • Changing batch size: the Adam learning rate scales roughly with √(batch ratio), not linearly as for SGD.

Variants and alternatives

Adam (L2)the original. Fine with λ = 0.
Adafactorfactored second moment (row + column statistics), O(n+m) state for an n×m matrix. Used for T5 and PaLM on TPU.
8-bit Adam (bitsandbytes)block-wise quantized m and v (blocks of 2048) with dynamic quantization.
LAMBAdam plus a per-layer trust ratio for very large batches (BERT in 76 minutes).
Lionsign update, one state buffer, about 3-10x smaller lr.
Muon / Shampoo / SOAPmatrix-aware preconditioners. They sometimes beat AdamW at equal compute but add complexity.

In production

torch.optim.AdamW (foreach and fused CUDA kernels), DeepSpeed FusedAdam and CPUAdam (ZeRO-Offload), bitsandbytes AdamW8bit, Optax adamw. Trained GPT-3 (β2 0.95, λ 0.1), LLaMA 1-3, Mistral and most open LLMs.

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
burnRusttracel-ai/burncargo add burn————AdamW optimizer in burn-optim
candleRusthuggingface/candlecargo add candle-nn————candle_nn::AdamW
PyTorchPython / C++pytorch/pytorchpip install torch————torch.optim.AdamW, foreach and fused=True CUDA kernels
DeepSpeedPython / C++deepspeedai/DeepSpeedpip install deepspeed————FusedAdam, CPUAdam (ZeRO-Offload)
bitsandbytesPython / CUDAbitsandbytes-foundation/bitsandbytespip install bitsandbytes————8-bit AdamW

Sources

  • Kingma and Ba, Adam: A Method for Stochastic Optimization, ICLR 2015. paper
  • Loshchilov and Hutter, Decoupled Weight Decay Regularization, ICLR 2019. paper
  • Touvron et al., LLaMA: Open and Efficient Foundation Language Models, 2023. paper
  • Dettmers, Lewis, Shleifer, Zettlemoyer, 8-bit Optimizers via Block-wise Quantization, ICLR 2022. paper · code
  • Shazeer and Stern, Adafactor: Adaptive Learning Rates with Sublinear Memory Cost, ICML 2018. paper
  • You et al., Large Batch Optimization for Deep Learning: Training BERT in 76 minutes (LAMB), ICLR 2020. paper
  • Liu et al., Swin Transformer, ICCV 2021 (AdamW training recipe). paper
  • Radford et al., Robust Speech Recognition via Large-Scale Weak Supervision (Whisper), 2022 (AdamW with gradient-norm clipping). paper

Used by

direct preference optimization · plannedDPO and variants: optimize a policy directly on preference pairs with a reference-model log-ratio loss
EmbeddingsA neural network has to take categorical inputs, such as words, users, items or board squares, and represent each as a useful numeric vector, without paying for a huge one-hot input.Image model trainingHow do you set up the training of an image classifier or a diffusion model so it works well? This guide helps pick optimizer, augmentation, resolution schedules and other settings that are known to work.LLM fine-tuningHow do you adapt an existing pretrained language model to a specific task, style or preference, cheaply and without damaging what it already knows? This guide covers when to fine-tune and how to check it worked.LLM pretrainingHow is a large language model trained from scratch in practice? This guide covers deciding data mix, scale, schedules and parallel layout so a months-long run on thousands of GPUs stays stable and efficient, and whether to pretrain at all.Mixed-precision trainingNeural network training is slow and memory-hungry in full 32-bit arithmetic. Engineers want to use faster, smaller number formats on modern GPUs without losing accuracy or hitting numerical failures.Neural network trainingA team has a model, a dataset and a cluster, and needs the model to reach its best accuracy without diverging, wasting GPU hours or losing a long run to a crash. This is the loop that does the learning.Policy gradient and PPOAn engineer training an agent by trial and error in a simulator, or fine-tuning a language model against a reward signal, needs a robust way to improve its behaviour without one large update wrecking it.Q-learning and DQNAn agent in an environment with a limited set of actions must learn which action is best from reward alone, and reuse past experience instead of discarding it after each update.
variational inference · plannedADVI and mean-field VI: optimize the ELBO with reparameterization gradients
ZeRO and FSDP shardingTraining a model whose weights, gradients and optimizer state do not fit in one GPU's memory, without rewriting the model to split its layers across devices, is the problem for data-parallel training.

Systems (7):

Alternatives and related