21. Mixed-Precision-Safe Manual Loss Scaling
ai-labs-pytorch.) General pattern (with an inference note) — no source in this research names loss scaling, torch.cuda.amp, or GradScaler specifically as a reported interview question at Anthropic, OpenAI, DeepMind, or Mistral. Given that all four labs train large models where mixed-precision is standard practice, it is a reasonable inference that engineers there need to understand what GradScaler automates under the hood, but this is not a confirmed reported example.Problem
In fp16 mixed-precision training, gradients that are small in magnitude (and fp16 has far less dynamic range near zero than fp32) can silently underflow to zero during backward, stalling learning for whatever those gradients were supposed to update. The standard fix, loss scaling, is what torch.cuda.amp.GradScaler automates: multiply the loss by a large scale factor before calling .backward() (which scales every gradient by the same factor via the chain rule, pushing small gradients back into fp16's representable range), then divide the gradients back down by that same factor before the optimizer step.
Scaling up can also push some gradients to overflow into inf/nan, so a dynamic scaler checks for that after every step: skip the optimizer step and shrink the scale if it finds inf/nan, otherwise grow the scale so training spends as much time as possible at a scale large enough to protect small gradients.
Implement grad_scaler_step: given already-backward()'d gradients that were computed from a loss scaled by scale, unscale them, check whether any are non-finite, and return either the correctly unscaled gradients (and a grown scale) or None (and a shrunk scale) if the step must be skipped.
Source: src/21_manual_loss_scaling.py
def grad_scaler_step(
grads: list[torch.Tensor], scale: float, growth_factor: float = 2.0,
backoff_factor: float = 0.5,
) -> tuple[list[torch.Tensor] | None, float]: ...
>>> grads = [torch.tensor([2048.0, -4096.0])]
>>> unscaled, new_scale = grad_scaler_step(grads, scale=1024.0)
>>> unscaled[0]
tensor([ 2., -4.])
>>> new_scale
2048.0
Step-by-Step Approach
- Unscale every gradient first:
g / scalefor each tensor ingrads. - Check finiteness after unscaling, not before — a real overflow can still look like a large-but-finite number at the scaled magnitude, and only becomes visibly inf/nan once divided back down (or was already inf/nan going in, which unscaling preserves).
- If any unscaled gradient contains inf or nan, return
(None, scale * backoff_factor)—Nonesignals the caller to skip the optimizer step entirely for this iteration. - If every gradient is finite, return
(unscaled_grads, scale * growth_factor). - Note the asymmetry: a skipped step still discards the wasted forward/backward's gradients but shrinks scale for next time; a successful step both uses its gradients and grows scale, so well-behaved training spends most iterations at (or climbing toward) the largest safe scale.
The key insight is the order of operations: checking for inf/nan on the scaled gradients would miss real overflows that only manifest once the artificially-large scale factor is divided back out, so unscaling always has to happen before the finiteness check, not after.
Reference solution
import torch
def grad_scaler_step(
grads: list[torch.Tensor], scale: float, growth_factor: float = 2.0,
backoff_factor: float = 0.5,
) -> tuple[list[torch.Tensor] | None, float]:
# unscale first: inf/nan must be judged on the *true* gradient
# magnitude, not the scaled one
unscaled = [g / scale for g in grads]
found_inf = any(not torch.isfinite(g).all() for g in unscaled)
if found_inf:
# skip this optimizer step entirely and shrink scale so the next
# attempt is less likely to overflow again
return None, scale * backoff_factor
# every step that doesn't overflow grows the scale, so training spends
# as much time as possible at a scale large enough to protect small
# fp16 gradients from flushing to zero
return unscaled, scale * growth_factor
Key Functions & Tricks
torch.isfinite(g).all()— the standard finiteness check; catches bothinf(overflow) andnan(typicallyinf - infor0/0arising downstream of an earlier overflow) in one call.- Unscale-then-check ordering — the one-line detail that makes the difference between a scaler that actually catches overflow and one that misses it.
Noneas a skip signal — matchesGradScaler.step(optimizer)'s real behavior, which silently no-ops the wrappedoptimizer.step()call when it detects inf/nan, rather than raising.- Growth/backoff as plain multiplicative factors — this problem implements the single-step core of
GradScaler's algorithm; the real implementation additionally only grows the scale everygrowth_intervalconsecutive successful steps rather than every single one.
How to Recognize This Pattern
Recognize this whenever a problem mentions fp16 training, gradient underflow, or asks what GradScaler or autocast actually does under the hood rather than just how to call them. The pattern is: scale up before backward, unscale after, check finiteness on the unscaled values, and adjust the scale based on that check — a small state machine, not a complex numerical algorithm. Common variations add a growth_interval counter (only grow every K consecutive successful steps, not every single one, which is what real GradScaler does) or scope the check per-parameter-group instead of globally. The most common pitfall is checking for inf/nan on the scaled gradients instead of the unscaled ones, or forgetting that a skipped step must still discard that iteration's gradients entirely rather than applying a partially-corrupted update.