Skip to content

Model Averaging — EMA, SWA

Overview

Averaging models over training is a free ensemble: it smooths the loss landscape, de-noises the last-epoch chatter, and routinely adds 0.5-2% accuracy or BLEU/ROUGE for nothing at inference time.

  • EMA (Exponential Moving Average): ema = decay * ema + (1-decay) * current, updated every step. Best for final deployment weights.
  • SWA (Stochastic Weight Averaging): uniform mean of checkpoints over the last few epochs. Better for training-end quality and flat minima.
  • EMA decay example: 0.999-0.9999 for long jobs; 0.99 for short bursts.

💡 Rule: use the EMA weights at eval/inference; keep the live weights for training (they adapt faster). Never train the EMA copy itself.


EMA — the Standard Implementation

import copy, torch, torch.nn as nn

class EMA:
    def __init__(self, model, decay=0.999):
        self.decay = decay
        self.ema = copy.deepcopy(model)   # shadow copy
        for p in self.ema.parameters():
            p.requires_grad_(False)

    @torch.no_grad()
    def update(self, model):
        ema_p = dict(self.ema.named_parameters())
        for name, p in model.named_parameters():
            if name in ema_p:
                ema_p[name].mul_(self.decay).add_(p.detach(), alpha=1 - self.decay)

    def state_dict(self):
        return self.ema.state_dict()

ema = EMA(model, decay=0.999)
for step in range(100):
    loss = (model(torch.randn(8, 4))).abs().mean()
    loss.backward(); opt.step()
    ema.update(model)                    # shadow follows live model

# switch to EMA at eval:
ema_model = ema.ema.eval()
with torch.no_grad():
    print(ema_model(torch.randn(1, 4)).shape)

⚠️ deepcopy needs the model on CPU or the same device — either deepcopy before .cuda() or move after copy.


Buffers & EMA (Batchnorm gotcha)

Copying only parameters leaves BatchNorm running stats stale. Copy buffers too:

@torch.no_grad()
def ema_update(self, model):
    for name, p in model.named_parameters():
        if name in self.ema_names:
            self.ema_params[name].mul_(self.decay).add_(p.detach(), alpha=1 - self.decay)
    for name, b in model.named_buffers():
        if name in self.ema_buffers:
            self.ema_buffers[name].copy_(b)   # buffers follow live copy

💡 Many opt for: after training, re-run a few eval forwards on EMA model so BatchNorm recalibrates (model.train(False) + forward pass on real data).


SWA — Stochastic Weight Averaging

from torch.optim.swa_utils import AveragedModel, SWALR

swa_model = AveragedModel(model)          # averages params over time
swa_sched = SWALR(opt, swa_lr=0.05)       # low, constant LR for the SWA phase

for epoch in range(10):
    train_one_epoch()
    if epoch >= 7:                        # start averaging near the end
        swa_model.update_parameters(model)
        swa_sched.step()

torch.optim.swa_utils.update_bn(swa_model, val_loader)   # recalibrate BN stats

EMA vs SWA — When to Use What

EMA SWA
Update frequency every step per epoch/checkpoint
Effective ensemble infinite memory last-N checkpoints only
Final weights long-run smoothed flattened minima
Best for chat/LLM deployment, RL value nets vision/classical-generalization tasks
Decay/lr 0.99-0.9999 SWA phase LR ~ peak/10

✅ EMA is the modern default for LLM/RL deployment; SWA shines on classification deadlines with fewer training steps.


Pitfalls

  1. Use EMA at inference, not in training metrics — train metrics reflect fast weights.
  2. torch.compile + EMA: compile the EMA copy separately or keep EMA un-compiled (shadows don't benefit).
  3. Don't EMA-then-distill: EMA-of-EMA compounds; one EMA layer is enough.
  4. Checkpoint both: save model and ema.state_dict() — resume keeps both consistent.
ckpt = {"model": model.state_dict(), "ema": ema.state_dict(), "step": step}
torch.save(ckpt, "ckpt.pt")

Key Takeaways

  • EMA/SWA are free ensembles: no extra forward passes, small memory (one shadow copy).
  • EMA: per-step decay, best for deployment; SWA: end-phase averaging, best for final checkpoints.
  • Copy buffers (BN stats) in EMA; recalibrate BN after SWA.
  • Serve EMA weights at inference; keep the fast live weights during training.
  • Save both in checkpoints for clean resumability.