Learning Rate Warmup › Code Implementation

Michael BrenndoerferMarch 24, 20266 min read
Language AI HandbookAll chapters
Part 32 of 67Training Optimization
Chapter 231Learning Rate Warmup › Code Implementation
Reading section 12 of 16 · Code Implementation · 6 minSection 12 of 16 · 6 min

Book contents

Section 12 of 16 · 6 min read

Learning Rate Warmup › Code ImplementationOne section · Sections in this chapter12 / 16 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's implement warmup schedules from scratch and visualize how they behave. We'll build a general-purpose schedule that combines linear warmup with cosine decay.

Linear Warmup Implementation

In[8]:
Code
import math


def linear_warmup_cosine_decay(
    step: int,
    warmup_steps: int,
    total_steps: int,
    peak_lr: float,
    min_lr: float = 0.0,
) -> float:
    """
    Compute learning rate at a given step using linear warmup + cosine decay.

    Args:
        step: Current training step (1-indexed)
        warmup_steps: Number of warmup steps
        total_steps: Total number of training steps
        peak_lr: Peak learning rate after warmup
        min_lr: Minimum learning rate at end of training

    Returns:
        Learning rate at this step
    """
    if step <= warmup_steps:
        # Linear warmup phase
        return peak_lr * (step / warmup_steps)
    else:
        # Cosine decay phase
        decay_steps = total_steps - warmup_steps
        completed = step - warmup_steps
        cosine_decay = 0.5 * (1 + math.cos(math.pi * completed / decay_steps))
        return min_lr + (peak_lr - min_lr) * cosine_decay
Out[9]:
Console
Step-by-step learning rate trace:
      Step            LR                 Phase
--------------------------------------------------
         1     1.500e-07                warmup
       100     1.500e-05                warmup
     1,000     1.500e-04                warmup
     2,000     3.000e-04                warmup
     5,000     2.994e-04          cosine decay
    50,000     1.693e-04          cosine decay
   100,000     3.000e-05          cosine decay

The trace confirms the warmup phase increases linearly from near-zero to the peak at step 2,000, then cosine decay smoothly reduces the learning rate toward the minimum over the remaining 98,000 steps.

PyTorch Integration

In practice, you use a learning rate scheduler to automate this. PyTorch's LambdaLR makes it straightforward to implement any schedule as a function that maps step number to a multiplier applied to the base optimizer learning rate:

In[10]:
Code
import torch
import torch.nn as nn
from torch.optim import AdamW


# A minimal model for demonstration
class TinyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(64, 64)

    def forward(self, x):
        return self.linear(x)


model = TinyModel()

# Standard optimizer configuration for transformer training
optimizer = AdamW(
    model.parameters(),
    lr=3e-4,  # This is peak_lr; the scheduler will scale it
    betas=(0.9, 0.999),
    eps=1e-8,
    weight_decay=0.01,
)


# Manually implement the warmup scheduler using LambdaLR
def warmup_cosine_lambda(step):
    warmup = 2000
    total = 100_000
    min_ratio = 0.1  # min_lr / peak_lr
    if step < warmup:
        return step / warmup
    else:
        decay_steps = total - warmup
        completed = step - warmup
        cosine = 0.5 * (1 + math.cos(math.pi * completed / decay_steps))
        return min_ratio + (1.0 - min_ratio) * cosine


scheduler = torch.optim.lr_scheduler.LambdaLR(
    optimizer, lr_lambda=warmup_cosine_lambda
)
Out[11]:
Console
Simulated learning rates from PyTorch LambdaLR scheduler:
      Step            LR
------------------------------
         1     1.500e-07
       500     7.500e-05
     1,000     1.500e-04
     2,000     3.000e-04
    10,000     2.956e-04
    50,000     1.693e-04
   100,000     3.000e-05

The scheduler's output at each step matches our manual calculations, confirming the implementation is correct. The LambdaLR pattern is preferred over hard-coded schedulers because it gives you full control over the schedule shape and makes it easy to experiment with different warmup durations or decay strategies.

Using the Hugging Face Scheduler

For practitioners working with the Hugging Face transformers library, built-in schedulers provide these schedules without needing to implement them from scratch:

In[12]:
Code
# Hugging Face transformers provides these schedules out of the box.
# The snippet below shows how you would use it in practice.
# We use eval=false here because transformers may not be installed in all environments.
# The API is shown for reference.

# from transformers import get_cosine_schedule_with_warmup
#
# scheduler_hf = get_cosine_schedule_with_warmup(
#     optimizer=optimizer,
#     num_warmup_steps=2000,
#     num_training_steps=100_000
# )
#
# # In your training loop:
# for batch in dataloader:
#     optimizer.zero_grad()
#     loss = model(batch)
#     loss.backward()
#     optimizer.step()
#     scheduler_hf.step()
Out[13]:
Console
HuggingFace-equivalent cosine schedule with warmup:
      Step    Multiplier   Effective LR (peak=3e-4)
-------------------------------------------------------
         1        0.0005                  1.500e-07
       500        0.2500                  7.500e-05
     1,000        0.5000                  1.500e-04
     2,000        1.0000                  3.000e-04
    10,000        0.9836                  2.951e-04
    50,000        0.5160                  1.548e-04
   100,000        0.0000                  0.000e+00

Note that the Hugging Face version decays to exactly zero (minimum multiplier of 0.0) rather than a nonzero minimum. For language model pretraining, you typically prefer a nonzero minimum (such as 10% of peak), which requires the custom implementation shown earlier.

Large Batch Scaling

Let's implement the linear scaling rule with proportional warmup scaling:

In[14]:
Code
def scaled_lr_and_warmup(
    base_lr: float,
    base_batch_size: int,
    base_warmup_steps: int,
    new_batch_size: int,
) -> tuple:
    """
    Apply linear scaling rule for batch size changes.

    Returns:
        (new_lr, new_warmup_steps) tuple
    """
    k = new_batch_size / base_batch_size
    new_lr = base_lr * k
    new_warmup = int(base_warmup_steps * k)
    return new_lr, new_warmup


# Baseline configuration
base_lr_scale = 1e-4
base_batch_scale = 256
base_warmup_scale = 1000

# Scaling scenarios
batch_sizes_scale = [256, 512, 1024, 2048, 4096, 8192]
Out[15]:
Console
Linear scaling rule: batch size vs. learning rate and warmup
Base: LR=1.0e-04, batch=256, warmup=1000 steps

  Batch Size   Scale k          LR    Warmup Steps
-------------------------------------------------------
         256       1.0    1.00e-04           1,000
         512       2.0    2.00e-04           2,000
       1,024       4.0    4.00e-04           4,000
       2,048       8.0    8.00e-04           8,000
       4,096      16.0    1.60e-03          16,000
       8,192      32.0    3.20e-03          32,000

The table shows that a 32x increase in batch size (from 256 to 8,192) requires both a 32x larger learning rate and 32x more warmup steps. Without the proportional warmup increase, the high learning rate would cause instability in early training. This is why the linear scaling rule is always described together with gradual warmup: they are two sides of the same coin.

Key Parameters

The important parameters for configuring learning rate warmup are:

  • warmup_steps: Number of steps to ramp from 0 to peak learning rate. Typical values: 1,000-5,000 for most transformer training runs.
  • peak_lr: The target learning rate reached after warmup. Must be tuned based on model size, batch size, and architecture.
  • min_lr: The floor for cosine decay at end of training. Usually 10% of peak_lr (min_lr = 0.1 * peak_lr).
  • beta2: Adam's second-moment decay rate. Affects how quickly statistics stabilize; lower values (0.95-0.99) allow shorter warmup.
  • batch_size: Larger batches require proportionally higher learning rates and longer warmup durations.