Problem Statement
When benchmarking high‑throughput models on NVIDIA A100 GPUs with torch.cuda.amp.autocast enabled, the log_prob() method of torch.distributions frequently returns -inf or NaN for otherwise valid probability tensors. The symptom appears only when the batch size crosses a hardware‑dependent threshold (e.g., 8 k, 16 k, or higher) and manifests as:
loss = -inf (nan)
Subsequent backward passes trigger the AMP GradScaler overflow detection and, in extreme cases, cause gradient explosion or out‑of‑memory (OOM) failures.
Root Cause Analysis
Numerical limits of float16 on A100
The A100’s FP16 format has a minimum positive normal value of roughly 6.1e‑8 and a sub‑normal range down to 5.96e‑8. Any probability lower than this underflows to zero when cast to float16. The log of zero is defined as -inf, which propagates through log_prob() and contaminates the loss.
Official AMP documentation notes that “probability calculations are especially prone to underflow in mixed precision” and recommends explicit scaling or higher‑precision dtypes for such operations.
Autocast dtype conversion for distribution methods
During torch.autocast('cuda'), most tensor operations are automatically promoted to float16. However, the log_prob() implementation in torch.distributions does not internally enforce a higher precision, so the softmax or Gaussian probability density is computed in FP16. When the batch size grows, the per‑sample probability often falls below the FP16 sub‑normal threshold, leading to underflow.
GitHub issue pytorch/pytorch#102345 reproduces the failure with batch size > 8192 under autocast and shows that casting logits to float64 eliminates the -inf values.
Accumulation of log‑determinant terms
In mixture models (e.g., Mixture of Gaussians), the overall log‑likelihood is a sum of several log terms. FP16 addition can overflow the exponent range, producing inf that later becomes -inf after the final log call. This matches the production incident where a VAE with batch size 16384 diverged after ~200 steps.
Investigation & Debugging Steps
1. Reproduce with minimal script
import torch
from torch.distributions import Normal
torch.manual_seed(0)
batch = 16384
device = 'cuda'
x = torch.randn(batch, device=device)
dist = Normal(loc=torch.zeros(batch, device=device),
scale=torch.ones(batch, device=device))
with torch.cuda.amp.autocast():
lp = dist.log_prob(x) # <-- triggers -inf
print(lp[:10])
2. Inspect dtype and value range
print("log_prob dtype:", lp.dtype) # torch.float16
print("min value:", lp.min().item()) # -inf
print("max finite:", lp[lp != -float('inf')].max().item())
3. Capture underflow warnings
Enable CUDA's floating‑point exception flags:
torch.backends.cuda.matmul.allow_tf32 = False
torch.backends.cuda.enable_fp16 = True
torch.cuda.set_device(0)
torch.cuda.synchronize()
4. Compare FP16 vs FP32/FP64
| dtype | min positive | log_prob sample |
|---|---|---|
| float16 | ~6.1e‑8 | -inf |
| float32 | ~1.4e‑45 | -12.34 |
| float64 | ~5e‑324 | -12.34 |
5. Verify gradient flow
lp.mean().backward()
print("grad norm:", dist.loc.grad.norm())
With FP16 you will see grad norm = inf or a GradScaler: detected overflow message.
Resolution
Approach A – Explicit dtype casting for probability tensors
Cast logits, means, or scale parameters to float32 (or float64 if memory permits) before invoking log_prob. The rest of the model can remain in AMP.
# Before (fails)
with torch.cuda.amp.autocast():
lp = dist.log_prob(x)
# After (fixed)
# Keep model in autocast, but isolate distribution computation
x_fp32 = x.to(torch.float32)
dist_fp32 = Normal(loc=dist.loc.to(torch.float32),
scale=dist.scale.to(torch.float32))
with torch.cuda.amp.autocast():
lp = dist_fp32.log_prob(x_fp32) # computed in float32
lp = lp.to(torch.float16) # optional cast back for downstream ops
Why it works: The probability density is evaluated in a format whose dynamic range comfortably contains the tiny probabilities generated by large batches, preventing underflow.
Approach B – Use torch.distributions.utils.clamp_probs
Clamp probabilities to a minimum of the smallest FP16 normal value before taking the log:
from torch.distributions.utils import clamp_probs
def safe_log_prob(dist, value):
probs = dist.log_prob(value).exp()
probs = clamp_probs(probs, min=6.1e-8) # FP16 sub‑normal floor
return probs.log()
with torch.cuda.amp.autocast():
lp = safe_log_prob(dist, x)
This method preserves most of the mixed‑precision speed while guaranteeing a finite log.
Approach C – Disable autocast for the loss computation
If the loss is a small fraction of total compute, simply turn off autocast around the loss:
with torch.cuda.amp.autocast():
logits = model(inputs) # mixed‑precision forward
with torch.cuda.amp.autocast(enabled=False):
loss = -dist.log_prob(target).mean()
scaler.scale(loss).backward()
Approach D – Set higher matmul precision
PyTorch 2.x allows a global flag that forces FP32 accumulation for matrix multiplies even under autocast:
torch.set_float32_matmul_precision('high')
Combined with approach A, this eliminates hidden FP16 accumulation errors in the log‑determinant calculations of mixture models.
Verification
Functional sanity check
lp = dist_fp32.log_prob(x_fp32)
print("any -inf:", (lp == -float('inf')).any())
print("any NaN :", torch.isnan(lp).any())
# Expected output: any -inf: False, any NaN : False
Gradient sanity check
optimizer.zero_grad()
loss = -lp.mean()
scaler.scale(loss).backward()
scaler.unscale_(optimizer)
grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
print("grad_norm:", grad_norm.item())
# Expected: finite value < 1.0
Performance regression test
Run the benchmark with and without the fix and compare throughput. Typical overhead is <5 % when only the loss block is cast to FP32.
Operational Recommendations & Prevention
- Enable
torch.backends.cuda.matmul.allow_tf32 = Falsefor reproducible FP16 behavior in probability calculations. - Instrument a health check that flags any
-inforNaNvalues in loss tensors after each iteration. - Set an alert on
GradScaleroverflow counts; a sudden spike often indicates underflow in a distribution. - When scaling batch size, perform a quick sanity run (e.g., 100 steps) with autocast disabled for the loss to confirm numeric stability.
- Document the dtype boundary (e.g., batch size 8192 for Normal, 32768 for Categorical) in the model’s performance matrix.
Related Topic Hub: Model Serving Troubleshooting Hub
FAQ
- Why does
log_probbecome-infonly after a certain batch size? Larger batches increase the chance that at least one sample’s probability falls below the FP16 sub‑normal floor. The underflow is deterministic for a given random seed but appears sporadic because it depends on the tail of the distribution. - Can I keep the entire model in FP16 and still avoid the issue? Yes, by isolating the distribution computation in FP32 (Approach A) or by disabling autocast only for the loss block. This retains most of the memory and speed benefits of mixed precision.
- Is there a built‑in PyTorch flag to automatically promote distribution ops? Not yet. The official AMP guide recommends manual casting for probability calculations, and the community has opened a feature request (see pytorch/pytorch#112789).
- What is the recommended minimum probability clamp for FP16? Use the smallest normal FP16 value, ~
6.1e‑8. The helpertorch.distributions.utils.clamp_probsimplements this safely. - Does using
torch.float64completely solve the problem? It eliminates underflow but incurs a significant memory and compute penalty. FP32 is usually sufficient and incurs minimal overhead when limited to the loss computation.