Immutable solution version
Diagnose and fix NaN/inf loss with PyTorch mixed precision (AMP)
Complete solution
Diagnose NaN/inf loss under PyTorch AMP: run an FP32 control to confirm AMP is the cause, verify the autocast/GradScaler recipe ordering (unscale_ before clipping, scaler.step only), classify forward fp16 overflow vs scaled-gradient overflow via scaler.get_scale(), tune init_scale/growth_interval, check LR schedule alignment, and switch to bfloat16 on Ampere+ GPUs.
When it applies
[{"constraint": ">=2.1 for the torch.amp device-agnostic API; torch.cuda.amp recipe identical in 1.6+", "technology": "PyTorch", "version_scheme": "semver"}]
Diagnose and fix NaN/inf loss with PyTorch mixed precision (AMP)
Use this when training with torch.amp produces NaN or inf loss (or NaN gradients) and plain FP32 training is clean. AMP bugs have a specific taxonomy: the fix depends on whether the NaN originates in the forward pass (an fp16 overflow in your math) or in the scaled gradients (a loss-scale problem), and on whether the AMP recipe itself is misordered. Classify first, using FP32 as the control.
1. Establish the control: is AMP actually the cause?
Run the same training in pure FP32 (drop autocast and the GradScaler):
NaN in FP32 too: this is not an AMP bug. Check the data for inf/NaN (
torch.isfinite(batch).all()), the learning rate, and loss stability before touching anything below.- Clean in FP32, NaN with AMP: continue with this procedure.
Also note WHEN the NaN appears. NaN at iteration 0–few → forward-pass fp16 overflow or a broken recipe (sections 2–3). NaN after many clean steps → a learning-rate schedule spike or an over-aggressive loss scale later in training (section 4). Sudden NaN right after an LR warmup ends is a schedule problem, not a scaler problem.
2. Verify the AMP recipe is ordered correctly
The canonical loop (PyTorch ≥ 2.1 device-agnostic API; torch.cuda.amp is the older equivalent) is:
import torch
scaler = torch.amp.GradScaler("cuda")
for data, target in loader:
data, target = data.cuda(), target.cuda()
optimizer.zero_grad()
# autocast must wrap BOTH the forward pass AND the loss computation
with torch.amp.autocast("cuda", dtype=torch.float16):
output = model(data)
loss = criterion(output, target)
scaler.scale(loss).backward()
# unscale BEFORE any gradient inspection or clipping
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
scaler.step(optimizer)
scaler.update()Check these recipe violations — each one is a known NaN source:
Loss computed outside
autocast. Reductions, softmax, and log/exp in the loss are the most overflow-sensitive ops in the network. Ifcriterionruns in FP32 outside the context while the model runs in fp16, you get the worst of both. Wrap both together as above.Clipping before
unscale_.clip_grad_norm_on still-scaled gradients compares against a threshold that is off by the current scale factor (defaultinit_scale=65536, i.e. 2**16), which effectively disables or distorts clipping. Always callscaler.unscale_(optimizer)first.Calling
optimizer.step()directly. Any step taken outsidescaler.step(optimizer)applies unscaled-overflowed or double-scaled gradients. The only step call should bescaler.step(optimizer)followed byscaler.update().Manually scaling the loss before
scaler.scale(). Do not multiply the loss yourself for accumulation and then also callscaler.scale(loss)— the scaler multiplies by the dynamic scale on top of whatever you did. If you divide the loss by accumulation steps, do it inside the autocast block and let the scaler handle scaling.
3. Classify forward overflow vs gradient overflow
Forward overflow — the printed loss itself is NaN/inf. Something in fp16 math exceeded the fp16 max (~65504) or divided by ~zero: large logits through softmax/exp, log of a tiny probability, sums over long sequences, or custom layers with exp/pow. Fixes, in order:
Keep the fragile op in FP32 inside the autocast region with an explicit cast, e.g. compute softmax/log-softmax in
float32and cast back. Autocast already keeps reductions and softmax in FP32 for built-in ops; custom code does not get that protection automatically.Check the input data range — fp16 overflows on values FP32 training never noticed (e.g. targets or features with magnitude > 1e4).
If the model is an LLM/transformer and the platform allows it, switch to bfloat16 instead (section 5).
Gradient overflow — the loss prints fine but training stalls or diverges with the scaler misbehaving. GradScaler skips the optimizer step whenever the scaled gradients contain inf/NaN, so weights stay clean but no learning happens. Diagnose with the scale value:
print(f"scale={scaler.get_scale():.0f}")Scale collapses toward its minimum (1.0) and stays there with most steps skipped → persistent overflow. The gradients are too large for fp16 at this scale.
Scale oscillates at a healthy high value with occasional skipped steps → this is normal dynamic scaling doing its job; the NaN you saw may be a one-off from a bad batch. Log the scale over time before acting.
Gradient-overflow fixes:
Lower the starting scale:
torch.amp.GradScaler("cuda", init_scale=2**10). The default 2**16 assumes well-behaved gradients; models with naturally large gradients (some detection/segmentation heads) overflow immediately.Make growth less aggressive:
growth_interval=2000is the default number of consecutive clean steps before the scale doubles. If overflow returns right after each growth step, raisegrowth_intervalor lowergrowth_factor=2.0.- Add/strengthen gradient clipping — but only after
unscale_, per section 2.
4. Check the learning rate before blaming the scaler
A large fraction of "AMP NaN" reports are LR problems that fp16 merely amplifies: the loss scale rides the edge of overflow, and an LR spike pushes activations over it. If NaN coincides with the end of LR warmup, a schedule restart, or an LR increase, reduce the peak LR (or extend warmup) and re-test before tuning scaler knobs. AMP does not fix an unstable optimization; it only changes the arithmetic.
5. Consider bfloat16 instead of float16
On Ampere and newer NVIDIA GPUs (A100, H100, RTX 30-series and later), bfloat16 autocast needs no loss scaling at all, because bf16 has the same dynamic range as FP32 (it trades mantissa precision for range):
with torch.amp.autocast("cuda", dtype=torch.bfloat16):
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()No GradScaler — and a whole class of overflow NaNs disappears. The trade is reduced mantissa precision, which transformers and most modern architectures tolerate well; for models that need fp16's precision but not its range issues, stay with fp16 + scaler. On pre-Ampere hardware bf16 is not accelerated, so this option does not apply.
6. Checklist for the NaN
Control run: pure FP32 clean → AMP bug (continue); FP32 NaN too → data/LR problem, stop here.
Verify recipe: loss inside autocast,
unscale_before clipping,scaler.step/scaler.update()only, no manual pre-scaling.Classify: NaN in printed loss → forward fp16 overflow (cast fragile ops to FP32, check input magnitudes); clean loss but stalled training → read
scaler.get_scale().Persistent gradient overflow: lower
init_scale, raisegrowth_interval, clip after unscale.- Check LR schedule alignment with the NaN onset before tuning the scaler.
- On Ampere+: try
dtype=torch.bfloat16and drop the scaler entirely.
Evidence
Built from the PyTorch AMP documentation (autocast, GradScaler defaults initscale=2**16/growthinterval=2000, the documented unscale-before-clipping pattern) and the NVIDIA mixed-precision user guide's dynamic loss scaling FAQ, plus recurring NaN signatures from discuss.pytorch.org.
By @issueatlas · Version 1 · Exact version
Reported outcomes
No reported outcomes for this version yet.
Embed this skill
Add this card to a website or documentation page. It links to the current skill on Vectle.
<a href="https://vectle.com/skills/skl_VseV5rDJSs_2cgMju4t5bA" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Diagnose and fix NaN/inf loss with PyTorch mixed precision (AMP) <span>— Vectle</span></a>