Deep Learning

PyTorch Optimizers: AdamW vs SGD vs RMSprop Explained

August 13, 2026 · 24 min read
In this article
  1. Table of Contents
  2. WTF Is An Optimizer? 
  3. Meet The Three Gurus
  4. Core Concept Dilemmas :
  5. The Exact Memory Cost Per Parameter
  6. Hyperparameter Deep Dive (The Real Magic)
  7. Technical Troubleshooting (The Painful Reality)
  8. The fused=True Compatibility Rule
  9. Gradient Accumulation: The “Poor Man’s Multi-GPU” 
  10. Real Code Examples :
  11. The Ultimate Cheat Sheet :
  12. What To Actually Remember :
  13. Final Words :

Stop getting NaN errors. The ultimate no-BS guide to PyTorch optimizers. Learn exact VRAM costs, hyperparameter tuning, and why fused=True crashes your GPU.

Table of Contents

  1. WTF Is An Optimizer? 

  2. Meet The Three Gurus

  3. Core Concept Dilemmas (The Stuff That Keeps You Up At Night)

  4. The Exact Memory Cost Per Parameter

  5. Hyperparameter Deep Dive (The Real Magic)

  6. Technical Troubleshooting (The Painful Reality)

  7. The fused=True Compatibility Rule

  8. Gradient Accumulation: The “Poor Man’s Multi-GPU”

  9. Real Code Examples 

  10. The Ultimate Cheat Sheet


WTF Is An Optimizer? 

You’ve got a neural network. It’s making predictions. They’re shitty. You calculate the loss (how shitty the predictions are). Now you need to update the weights so the next predictions are less shitty.

The optimizer = The math formula that decides HOW to update those weights.

It’s like your GPS telling you “turn left” vs “turn right” vs “go straight”. Different optimizers give different directions.


Meet The Three Gurus

1. SGD – The Disciplined Student :

Full Form: Stochastic Gradient Descent.

What it does:
Takes one step. No memory of past steps. Pure, simple, brutal.

The Math:

weight_new = weight_old - lr × gradient

Why use it:

Why hate it:


2. AdamW – The Genius Who Remembers Everything :

Full Form: Adaptive Moment Estimation with Weight Decay

What it does:
Maintains TWO diaries: one for direction (momentum) and one for magnitude (variance). Uses BOTH to take adaptive steps.

The Math:

momentum = β₁ × momentum + (1-β₁) × gradient         (Direction memory)
variance = β₂ × variance + (1-β₂) × gradient²        (Magnitude memory)
weight_new = weight_old - lr × momentum / (√variance + ε)

Why use it:

Why hate it:


3. RMSprop – The Middle Child Who Found His Niche :

Full Form: Root Mean Square Propagation.

What it does:
Maintains ONE diary: only tracks magnitude (variance). No direction memory.

The Math:

variance = α × variance + (1-α) × gradient²
weight_new = weight_old - lr × gradient / (√variance + ε)

Why use it:

Why hate it:


Core Concept Dilemmas :

Dilemma 1: “Why does Adam fail on CNNs where SGD + Momentum succeeds?”

The Community Consensus:

Imagine you’re trying to find the lowest point in a bumpy, crater-filled terrain.

The Science:

“Adaptive methods like Adam tend to converge to sharp minima, while SGD converges to flat minima. Flat minima generalize better to unseen data.” – Keskar et al. (2017)

Real World:
ResNets trained with SGD + Momentum consistently beat Adam on ImageNet. That’s why all the big vision papers use SGD.

Comparison of SGD vs Adam convergence paths on a loss landscape showing Adam trapped in sharp minima while SGD bounces to flat minima

Code Proof:

# SGD wins on CNNs
optimizer = SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
# Accuracy: 95.2% on CIFAR-10

# Adam loses on same CNN
optimizer = AdamW(model.parameters(), lr=1e-4)
# Accuracy: 93.7% on CIFAR-10 (worse!)

Dilemma 2: “Why use AdamW instead of Adam? What did Adam actually break?”

The Community Consensus:

Standard Adam does L2 regularization WRONG.

In regular Adam, weight decay is implemented by adding λ·w to the gradient BEFORE Adam applies its adaptive scaling.

The Problem:

gradient_new = gradient_old + λ × weight    # Weight decay added HERE
# Then Adam scales this gradient by 1/√variance
# Weights with SMALL historical gradients get DECAYED MORE
# Weights with LARGE historical gradients get DECAYED LESS

This is WRONG because the regularization penalty should be independent of the adaptive learning rate.

AdamW Fixes This:

gradient_new = gradient_old    # No decay in gradient
# Adam does its adaptive thing
weight_new = weight_old - lr × adaptive_step
weight_new = weight_new - lr × λ × weight_old    # Decay applied HERE

The Result:
AdamW gives better generalization and more stable training than Adam. Every modern LLM (GPT, LLaMA, BERT) uses AdamW, not Adam.

Visual explanation of Adam vs AdamW weight decay showing decoupled weight decay applied after gradient update

Code:

# DON'T DO THIS (Old Adam)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4, weight_decay=0.01)

# DO THIS (AdamW)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)

Dilemma 3: “Why is RMSprop preferred over Adam in Reinforcement Learning (RL) and LSTMs?”

The Community Consensus:

RL environments are non-stationary. The reward distribution changes constantly as the agent learns.

Real World Example:
OpenAI’s PPO, DeepMind’s DQN, Stable-Baselines3 – ALL default to RMSprop.

RMSprop vs Adam in non-stationary RL environments showing RMSprop adapting faster to changing reward distributions

Code:

# RL standard
optimizer = RMSprop(
    model.parameters(), 
    lr=1e-4, 
    alpha=0.99, 
    eps=1e-5,  # Use 1e-5 for FP16!
    momentum=0.0  # No momentum!
)

The Exact Memory Cost Per Parameter

This is the table everyone searches for. Here’s the exact breakdown.

OptimizerState TensorsBytes/Param1B Params7B Params
SGD00 bytes0 GB0 GB
SGD + Momentum18 bytes8 GB56 GB
RMSprop18 bytes8 GB56 GB
Adam216 bytes16 GB112 GB
AdamW216 bytes16 GB112 GB

Wait, but what about FP16?

The Math (Why 8 Bytes?):

Real Example:
LLaMA-7B has 7 billion parameters.

That’s why you need an A100 (80GB) or H100 (80GB).

🛑 Tired of just reading code? Want to actually SEE these math concepts? If you’re a college student or professor trying to visualize how these algorithms actually work under the hood without burning your laptop’s GPU, check out Learning Rate & Optimizer Race Visualizer — Interactive PyTorch Lab | Neural Ninjas . For more such labs – Simulation Labs – neuralninjas.in – neuralninjas.in.


Hyperparameter Deep Dive (The Real Magic)

Alright, THIS is where the magic happens. Most people just copy-paste hyperparameters from Stack Overflow and pray. But understanding what each knob does is the difference between a model that trains in 2 days vs 2 weeks.

Let me break down EVERY SINGLE HYPERPARAMETER for each optimizer. 


SGD Hyperparameters :

SGD is the simplest optimizer. That’s why it has the fewest knobs to turn.

1. Learning Rate (lr) – The Step Size 👣

What it does: Controls how big of a step you take in the direction of the gradient.

The Math:

weight_new = weight_old - lr × gradient

If lr is too HIGH:

If lr is too LOW:

Typical Values:

Real Example:

# Too high - loss explodes
optimizer = SGD(model.parameters(), lr=0.1)  # Loss: NaN after 10 steps

# Too low - takes forever
optimizer = SGD(model.parameters(), lr=0.0001)  # Loss: 0.01 decrease per epoch

# Just right
optimizer = SGD(model.parameters(), lr=0.01)  # Steady decrease

2. Momentum (momentum) – The Velocity ⚡

What it does: Accumulates past gradient directions to smooth out updates. Like a heavy ball rolling down a hill.

The Math:

velocity = momentum × velocity_old + gradient
weight_new = weight_old - lr × velocity

If momentum is 0: Pure SGD. No memory. Noisy updates.

If momentum is too HIGH (0.99+):

If momentum is too LOW (0.5):

Typical Value: 0.9 (the sweet spot)

Why it matters:

Example:

# No momentum - oscillates
optimizer = SGD(model.parameters(), lr=0.01, momentum=0.0)  
# Loss: 1.2 → 0.8 → 1.1 → 0.7 (oscillates)

# With momentum - smooth
optimizer = SGD(model.parameters(), lr=0.01, momentum=0.9)
# Loss: 1.2 → 0.9 → 0.7 → 0.5 (smooth decrease)

3. Weight Decay (weight_decay) – The Regularizer 🛡️

What it does: Penalizes large weights to prevent overfitting.

The Math:

gradient_new = gradient_old + weight_decay × weight_old

If weight_decay is too HIGH:

If weight_decay is too LOW:

Typical Value: 1e-4 (0.0001)

Why it matters:

Effect of weight decay on weight magnitudes showing weights shrinking to prevent overfitting

Example:

# No weight decay - overfits
optimizer = SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=0.0)
# Train: 99% Test: 75% (overfitting)

# With weight decay - generalizes
optimizer = SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
# Train: 95% Test: 92% (good generalization)

4. Nesterov Momentum (nesterov) – The Look-Ahead 👀

What it does: Calculates gradient at the “look-ahead” position (where momentum would take you) instead of current position.

The Math:

velocity = momentum × velocity_old
look_ahead = weight_old - lr × velocity  # Where you're going to be
gradient_at_look_ahead = ∇loss(look_ahead)  # Look ahead!
velocity_new = momentum × velocity_old + gradient_at_look_ahead
weight_new = weight_old - lr × velocity_new

Effect: Slightly faster convergence and better stability.

Typical Value: True (enabled)

Why use it:

Example:

# Without Nesterov
optimizer = SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=False)

# With Nesterov (slightly better)
optimizer = SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=True)
# Typically 1-2% better accuracy

5. Dampening (dampening) – The Brakes 🛑

What it does: Reduces the momentum accumulation at each step.

The Math:

velocity = momentum × velocity_old + (1 - dampening) × gradient

If dampening = 0: Standard momentum (full velocity)

If dampening = 0.1: Velocity reduced by 10% each step

Typical Value: 0 (disabled)

Why use it: Rarely used. Only for special cases where you want to slow down momentum accumulation.


RMSprop Hyperparameters (The Middle Child) :

RMSprop has a few more knobs because it tracks variance.

1. Learning Rate (lr) – The Step Size 👣

Same concept as SGD, but RMSprop is more stable with higher LRs.

Typical Values:

Why different from SGD: RMSprop adapts step sizes automatically, so you can use higher LRs.


2. Alpha (α) – The Decay Rate 📉

What it does: Controls how fast old gradient information decays in the variance calculation.

The Math:

variance_new = α × variance_old + (1-α) × gradient²

If α is too HIGH (0.999):

If α is too LOW (0.9):

Typical Value: 0.99 (the sweet spot)

Why it matters:

Real Example:

# Too slow to adapt
optimizer = RMSprop(model.parameters(), lr=1e-4, alpha=0.999)
# Takes 1000 episodes to adjust to new reward distribution

# Just right
optimizer = RMSprop(model.parameters(), lr=1e-4, alpha=0.99)
# Adapts in 100 episodes

# Too sensitive
optimizer = RMSprop(model.parameters(), lr=1e-4, alpha=0.9)
# Oscillates because it forgets too quickly

3. Epsilon (ε) – The Stabilizer 🛡️

What it does: Prevents division by zero in the update step.

The Math:

weight_new = weight_old - lr × gradient / (√variance + ε)

If ε is too SMALL (1e-8):

If ε is too LARGE (1e-3):

Typical Value:

Why it matters:

Example:

# Default for FP32 - works
optimizer = RMSprop(model.parameters(), lr=1e-4, eps=1e-8)
# Loss: 0.5 → 0.3 → 0.1 (fine)

# Same for FP16 - breaks!
optimizer = RMSprop(model.parameters(), lr=1e-4, eps=1e-8)
# Loss: 0.5 → NaN! (after 100 steps)

# Fixed for FP16
optimizer = RMSprop(model.parameters(), lr=1e-4, eps=1e-5)
# Loss: 0.5 → 0.3 → 0.1 (works!)

4. Momentum (momentum) – The Optional Velocity ⚡

What it does: RMSprop typically doesn’t use momentum, but you CAN add it.

The Math:

variance = α × variance_old + (1-α) × gradient²
momentum = β × momentum_old + gradient / √variance
weight_new = weight_old - lr × momentum

If momentum = 0: Pure RMSprop (standard)

If momentum > 0: RMSprop + momentum (like Adam without bias correction)

Typical Value: 0 (disabled)

Why use it: Rarely used in practice. Standard RMSprop works fine.


5. Centered (centered) – The Mean Subtraction 🎯

What it does: Subtracts the mean of squared gradients before normalization.

The Math:

mean_gradient = α × mean_gradient_old + (1-α) × gradient
variance = α × variance_old + (1-α) × gradient²
weight_new = weight_old - lr × gradient / √(variance - mean_gradient² + ε)

Effect: Makes updates less biased.

Typical Value: False (disabled)

Warning: centered=True + sparse=True + fused=True = CRASH!

Example:

# Without centering (faster)
optimizer = RMSprop(model.parameters(), lr=1e-4, centered=False)

# With centering (slightly better, but slower)
optimizer = RMSprop(model.parameters(), lr=1e-4, centered=True)

# Don't do this - CRASH!
optimizer = RMSprop(model.parameters(), lr=1e-4, centered=True, sparse=True, fused=True)
# RuntimeError: Fused RMSprop does not support centered=True with sparse gradients

AdamW Hyperparameters (The Complex Ones) :

AdamW has the most knobs. Each one matters.

1. Learning Rate (lr) – The Step Size 👣

Typical Values:

Why so low: AdamW scales gradients automatically, so LRs are typically 10-100x lower than SGD.

The Rule: Start with 1e-4. If it explodes, go to 1e-5. If too slow, go to 3e-4.

Example:

# Standard for GPT/BERT
optimizer = AdamW(model.parameters(), lr=1e-4)  # Works 90% of time

# Too high - explodes
optimizer = AdamW(model.parameters(), lr=1e-3)  # Loss: NaN after 10 steps

# Fine-tuning pretrained
optimizer = AdamW(model.parameters(), lr=1e-5)  # Safe for fine-tuning

2. Beta1 (β₁) – The Momentum Decay 📉

What it does: Controls how much past gradients influence current momentum.

The Math:

momentum = β₁ × momentum_old + (1-β₁) × gradient

If β₁ is too HIGH (0.999):

If β₁ is too LOW (0.5):

Typical Value: 0.9 (the standard)

Why it matters:

Example:

# Too slow to adapt
optimizer = AdamW(model.parameters(), lr=1e-4, betas=(0.999, 0.999))
# Takes 1000 steps to change direction

# Just right
optimizer = AdamW(model.parameters(), lr=1e-4, betas=(0.9, 0.999))
# Adapts within 100 steps

# Too noisy
optimizer = AdamW(model.parameters(), lr=1e-4, betas=(0.5, 0.999))
# Loss oscillates

3. Beta2 (β₂) – The Variance Decay 

What it does: Controls how much past gradient variances influence current variance estimate.

The Math:

variance = β₂ × variance_old + (1-β₂) × gradient²

If β₂ is too HIGH (0.9999):

If β₂ is too LOW (0.9):

Typical Value: 0.999 (standard)

Why it matters:

AdamW beta hyperparameters visualization showing beta1 (momentum decay) and beta2 (variance decay) effects on optimization

Example:

# Too stable
optimizer = AdamW(model.parameters(), lr=1e-4, betas=(0.9, 0.9999))
# Slow to adapt to gradient changes

# Just right
optimizer = AdamW(model.parameters(), lr=1e-4, betas=(0.9, 0.999))
# Balanced stability and adaptivity

# Too adaptive
optimizer = AdamW(model.parameters(), lr=1e-4, betas=(0.9, 0.9))
# Too sensitive, oscillates

4. Weight Decay (weight_decay) – The Regularizer (DECOUPLED!) 🛡️

What it does: Penalizes large weights. In AdamW, it’s decoupled from gradients.

The Math (Important Difference!):

# Standard Adam (WRONG):
gradient_new = gradient_old + weight_decay × weight_old  # Mixed with gradient
# Then adaptive update...

# AdamW (CORRECT):
# Adam does its adaptive thing first
weight_new = weight_old - lr × adaptive_step
# THEN apply decay separately
weight_new = weight_new - lr × weight_decay × weight_old  # Decoupled!

If weight_decay is too HIGH (0.1+):

If weight_decay is too LOW (0.0):

Typical Values:

Why AdamW’s decoupling matters:

Example:

# No weight decay - overfits
optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=0.0)
# Train: 98% Test: 82% (massive overfit)

# Standard weight decay
optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)
# Train: 95% Test: 93% (good generalization)

# Too much - underfits
optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=0.5)
# Train: 85% Test: 84% (underfit)

5. Epsilon (ε) – The Stabilizer 🛡️

What it does: Prevents division by zero in the adaptive step.

The Math:

adaptive_step = momentum / (√variance + ε)
weight_new = weight_old - lr × adaptive_step

If ε is too SMALL (1e-8):

If ε is too LARGE (1e-3):

Typical Value:

Why it matters:

Example:

# Default for FP32
optimizer = AdamW(model.parameters(), lr=1e-4, eps=1e-8)
# Loss: 1.0 → 0.5 → 0.1 (fine in FP32)

# But in FP16 - breaks!
optimizer = AdamW(model.parameters(), lr=1e-4, eps=1e-8)
# Loss: 1.0 → NaN! (after 100 steps in FP16)

# Fixed for FP16
optimizer = AdamW(model.parameters(), lr=1e-4, eps=1e-6)
# Loss: 1.0 → 0.5 → 0.1 (works in FP16)

6. AmSgrad (amsgrad) – The Max Variance Tracker 

What it does: Tracks the maximum variance seen so far, preventing variance from decreasing too quickly.

The Math:

variance = β₂ × variance_old + (1-β₂) × gradient²
variance_max = max(variance_max, variance)
weight_new = weight_old - lr × momentum / (√variance_max + ε)

Effect: Slows down learning rate decay. Sometimes helps with convergence.

Typical Value: False (disabled)

Why use it: Rarely needed. Can help with convergence in some cases.

Example:

# Standard (fast)
optimizer = AdamW(model.parameters(), lr=1e-4, amsgrad=False)

# With amsgrad (slightly slower but more stable)
optimizer = AdamW(model.parameters(), lr=1e-4, amsgrad=True)
# Sometimes helps with convergence

Hyperparameter Quick Reference Table :

HyperparameterSGDRMSpropAdamWWhat It Does
lr0.01 (0.001-0.1)1e-4 (1e-5-1e-3)1e-4 (1e-5-1e-2)Step size – Bigger = faster but risky
momentum0.90 (disabled)β₁=0.9 (in beta1)Velocity – smooths updates
weight_decay1e-400.01 (0.01-0.1)Prevents overfitting
beta1/alphaα=0.99β₁=0.9Decay rate for momentum/variance
beta2β₂=0.999Decay rate for variance
eps1e-8 (1e-5 for FP16)1e-8 (1e-6 for FP16)Stabilizer – prevents division by zero
nesterovFalseLook-ahead momentum
centeredFalseSubtract mean from variance
amsgradFalseTrack max variance

Technical Troubleshooting (The Painful Reality)

Bug 1: “NaN loss after switching from SGD to AdamW”

The Cause:

AdamW divides the learning rate by √variance + ε.

The default ε = 1e-8.

In FP16 (mixed precision), numbers smaller than ~6e-8 get rounded to zero.

So √variance becomes 0, then 1e-4 / (0 + 1e-8) = 1e-4 / 1e-8 = 1e-4 × 1e8 = 10,000.

Your learning rate just became 10,000. Your weights explode. You get NaN.

The Fix:

# Increase epsilon for FP16 training
optimizer = AdamW(
    model.parameters(),
    lr=1e-4,
    eps=1e-5  # <-- CHANGE THIS from 1e-8 to 1e-5
)

# Also add gradient clipping
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

Bug 2: “fused=True crashes with custom learning rate schedules”

The Error:

RuntimeError: Tensor learning rate is not supported with fused=True

The Cause:

fused=True launches a single GPU kernel that expects scalar values (Python floats) for learning rate.

If your scheduler outputs a PyTorch Tensor (even a 0-dimensional one), the CUDA kernel can’t interpret it.

The Fix:

# DON'T DO THIS
lr = torch.tensor(0.001)  # This is a Tensor
optimizer = AdamW(model.parameters(), lr=lr, fused=True)  # ERROR!

# DO THIS
lr = 0.001  # Python float
optimizer = AdamW(model.parameters(), lr=lr, fused=True)  # WORKS!

# For schedulers that return tensors, convert them
def get_lr():
    return float(scheduler.get_last_lr()[0])  # Convert to float

Bug 3: “fused=True is SLOWER than foreach=True on my RTX 4090”

The Cause:

For small models or small batch sizes, the fused kernel underutilizes the GPU’s streaming multiprocessors (SMs).

Wait, 6.5ms is FASTER than 27.4ms. So why is it slower?

Because the 137 kernels run IN PARALLEL on different SMs.

Meanwhile, the single fused kernel takes 6.5ms to run sequentially on the GPU.

Result: foreach=True finishes in 0.2ms, fused=True takes 6.5ms. Fused is slower!

The Fix:

# On RTX 4090/H100 for small models:
optimizer = AdamW(model.parameters(), lr=1e-4, fused=False)  # Use foreach

When to use fused=True:


Bug 4: “RMSprop fused=True crashes with sparse gradients”

The Error:

RuntimeError: Fused RMSprop does not support centered=True with sparse gradients

The Cause:

Embedding layers generate sparse gradients (only some indices get updated).

fused=True with centered=True requires dense gradient tensors for the variance normalization step.

The Fix:

# Option 1: Turn off centered
optimizer = RMSprop(
    model.parameters(),
    lr=1e-4,
    centered=False,  # <-- CHANGE THIS
    fused=True
)

# Option 2: Don't use fused for sparse layers
optimizer = RMSprop(
    model.parameters(),
    lr=1e-4,
    centered=True,
    fused=False  # <-- CHANGE THIS
)

The fused=True Compatibility Rule

Memorize this table:

Optimizerfused=True CompatibilityKnown Issues
SGD✅ StableNone
SGD + Momentum✅ StableNone
RMSprop⚠️ PartialBreaks with sparse=True or centered=True
AdamW✅ StableTensor LR not supported
Adam✅ StableSame as AdamW

Rule of Thumb:


Gradient Accumulation: The “Poor Man’s Multi-GPU” 

The Problem:
You read the table above. AdamW needs 16 bytes/parameter. You have a 1B parameter model. That’s 16GB just for optimizer states. You have an RTX 3070 with 8GB VRAM. You’re screwed. Right?

Wrong.

The Solution:
Gradient Accumulation. It’s the cheat code for memory-constrained training.

How it Works:

Instead of updating the weights after every batch:

  1. Do a forward pass.

  2. Compute loss.

  3. Do a backward pass (gradients accumulate).

  4. DON’T call optimizer.step().

  5. Repeat steps 1-4 for accumulation_steps batches.

  6. NOW call optimizer.step().

This way, you’re simulating a large batch size without the VRAM cost.

The Math:

Batch_Size_Effective = Micro_Batch_Size × Accumulation_Steps

Example:
Micro_Batch_Size = 8  (Fits in your 8GB VRAM)
Accumulation_Steps = 8
Effective_Batch_Size = 64  (Simulates a 64-batch on an A100)

The Code:

accumulation_steps = 8
optimizer = AdamW(model.parameters(), lr=1e-4)

for epoch in range(epochs):
    for i, batch in enumerate(dataloader):
        # Forward pass
        output = model(batch)
        loss = criterion(output, target)
        
        # Divide loss by accumulation steps
        # This ensures gradients don't explode when accumulated
        loss = loss / accumulation_steps
        
        # Backward pass (gradients accumulate)
        loss.backward()
        
        # Only update weights every `accumulation_steps` batches
        if (i + 1) % accumulation_steps == 0:
            # Gradient clipping (optional but recommended)
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            
            # Take one step with accumulated gradients
            optimizer.step()
            
            # Clear gradients for next cycle
            optimizer.zero_grad()

Example:

You’re training GPT-2 (1.5B parameters) on an RTX 3090 (24GB).

Caveats:


Real Code Examples :

Example 1: Vision Transformer Training (AdamW + Fused)

import torch
import torch.nn as nn
from torch.optim import AdamW
from torch.cuda.amp import GradScaler, autocast

# Model (say ViT-Base)
model = vit_base_patch16_224()

# Move to GPU
model = model.cuda()

# Optimizer with fused=True
optimizer = AdamW(
    model.parameters(),
    lr=1e-4,
    betas=(0.9, 0.999),
    weight_decay=0.05,
    eps=1e-6,  # Higher epsilon for FP16
    fused=True  # Single GPU kernel
)

# Mixed precision
scaler = GradScaler()

# Training loop
for epoch in range(100):
    for batch in dataloader:
        images, labels = batch
        images = images.cuda()
        labels = labels.cuda()
        
        # Forward with mixed precision
        with autocast():
            outputs = model(images)
            loss = criterion(outputs, labels)
        
        # Backward with scaler
        scaler.scale(loss).backward()
        
        # Gradient clipping
        scaler.unscale_(optimizer)
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        
        # Step
        scaler.step(optimizer)
        scaler.update()
        optimizer.zero_grad()

Example 2: ResNet Training (SGD + Momentum)

import torch
from torch.optim import SGD

# Model
model = torchvision.models.resnet50(pretrained=False)

# SGD with momentum
optimizer = SGD(
    model.parameters(),
    lr=0.1,  # High LR for SGD
    momentum=0.9,
    weight_decay=1e-4,
    nesterov=True,  # Nesterov momentum for extra boost
    fused=True  # Stable on all GPUs
)

# StepLR scheduler
scheduler = torch.optim.lr_scheduler.StepLR(
    optimizer, 
    step_size=30, 
    gamma=0.1
)

# Training loop (standard)
for epoch in range(90):
    for batch in dataloader:
        optimizer.zero_grad()
        outputs = model(batch)
        loss = criterion(outputs, targets)
        loss.backward()
        optimizer.step()
    
    scheduler.step()  # Reduce LR every 30 epochs

Example 3: PPO in RL (RMSprop)

import torch
from torch.optim import RMSprop

# Policy network
class PolicyNetwork(nn.Module):
    def __init__(self, obs_dim, action_dim):
        super().__init__()
        self.fc1 = nn.Linear(obs_dim, 256)
        self.fc2 = nn.Linear(256, action_dim)
    
    def forward(self, x):
        x = torch.relu(self.fc1(x))
        return self.fc2(x)

model = PolicyNetwork(obs_dim=128, action_dim=10)

# RMSprop - RL standard
optimizer = RMSprop(
    model.parameters(),
    lr=3e-4,  # Standard for PPO
    alpha=0.99,
    eps=1e-5,  # Increased for FP16
    momentum=0.0,  # No momentum
    weight_decay=0.0
)

# PPO training loop (simplified)
for episode in range(1000):
    # Collect trajectories
    states, actions, rewards = collect_trajectories(model)
    
    # Compute advantages
    advantages = compute_gae(rewards)
    
    # Policy update
    optimizer.zero_grad()
    loss = compute_ppo_loss(model, states, actions, advantages)
    loss.backward()
    
    # Gradient clipping for RL
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=0.5)
    
    optimizer.step()

Example 4: Gradient Accumulation (Poor Man’s A100)

# The full gradient accumulation pattern

accumulation_steps = 8
effective_batch_size = 64
micro_batch_size = effective_batch_size // accumulation_steps  # = 8

optimizer = AdamW(model.parameters(), lr=1e-4)

for epoch in range(epochs):
    # Reset gradient accumulator
    optimizer.zero_grad()
    
    for i, batch in enumerate(dataloader):
        # Forward pass
        outputs = model(batch)
        loss = criterion(outputs, targets)
        
        # Scale loss for accumulation
        loss = loss / accumulation_steps
        
        # Backward pass (accumulates gradients)
        loss.backward()
        
        # Check if we've accumulated enough
        if (i + 1) % accumulation_steps == 0:
            # Clip gradients before step
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            
            # Take optimizer step
            optimizer.step()
            
            # Reset gradients
            optimizer.zero_grad()

The Ultimate Cheat Sheet :

Decision Tree

Is your task Computer Vision (CNN)?
    ✅ YES → Use SGD + Momentum (lr=0.01, momentum=0.9, weight_decay=1e-4)
    ❌ NO → Continue

Is your task NLP/Transformers/LLM?
    ✅ YES → Use AdamW (lr=1e-4, betas=(0.9, 0.999), weight_decay=0.01)
    ❌ NO → Continue

Is your task Reinforcement Learning?
    ✅ YES → Use RMSprop (lr=1e-4, alpha=0.99, eps=1e-5)
    ❌ NO → Continue

Is your task fine-tuning a pretrained model?
    ✅ YES → Use AdamW (lr=1e-5, lower than default!)
    ❌ NO → Use AdamW (it works for 90% of cases)

Hyperparameters Cheat Sheet :

Optimizerlrmomentum/β1β2/alphaweight_decayepsOther
SGD0.010.91e-4nesterov=True
SGD + Mom0.010.91e-4nesterov=False
RMSprop1e-40.00.990.01e-5centered=False
AdamW1e-40.90.9990.011e-8amsgrad=False

PRO TIP: For AdamW, use lr=1e-4 for Transformers and lr=1e-5 for fine-tuning.


What To Actually Remember :

  1. SGD = Memory efficient but slow. Use for CNNs when you have VRAM constraints.

  2. AdamW = Fast but memory hungry. Use for EVERYTHING else if you have VRAM.

  3. RMSprop = RL’s best friend. Use for PPO, DQN, and LSTMs.

  4. AdamW > Adam. Always use AdamW. Adam has a bug in weight decay.

  5. fused=True is NOT always faster. Test on your hardware.

  6. Gradient accumulation = Fake big batch size. Use when VRAM is low.

  7. Increase epsilon to 1e-5 for FP16 training. Default 1e-8 will NaN your loss.

  8. CNNs like SGD. Transformers like AdamW. RL likes RMSprop. Don’t fight it.

  9. Hyperparameters matter more than you think. Tune them properly.

  10. There’s no magic bullet. Experiment with YOUR model and data.


Final Words :

I’ve been training models for some years now. I’ve seen NaN losses at 3 AM. I’ve run out of VRAM 5 minutes before a deadline. I’ve copy-pasted optimizer code from Stack Overflow and prayed it would work.

Here’s what I’ve learned:


That’s it. The complete, no-bullshit guide to PyTorch optimizers in 2026.  And if you’re reading this at 3 AM because your model won’t converge… I’ve been there. It gets better. Or at least, you learn to cope. 

Never miss what we build next.

New articles and interactive labs, straight to your inbox the moment they ship — no fixed schedule, no fluff.

Logic Lama
Neural Ninjas
// Continuing from this article

Your Neural Path

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
// STILL BROWSING?
Build along, don't just read.
Get labs & articles matched to what you're into — free, takes 30 seconds.
Start building free