Dive into Deep Learning · §25.5
Why the math is right but the loss is NaN
floating point · stable softmax · cancellation · conditioning.
Motivation
Every proof in this chapter was over \mathbb{R}. Your GPU computes over a finite, gappy imitation. When an answer is wrong, two questions split the blame:
The fixes are reformulations: max-subtraction, log-space, Welford, ridge.
01
Floating point
a number system with gaps
Floating point
A float is base-2 scientific notation with a fixed digit budget:
x = (-1)^s\,(1.m_1\ldots m_p)_2\; 2^{e}.
The exponent buys range; the mantissa fixes relative precision. Between powers of two the spacing is constant, so the absolute gap doubles at every power, and each format ends at an overflow threshold.
Machine epsilon \varepsilon_{\text{mach}} = 2^{-p} is the gap from 1 to its successor: \mathrm{fl}(x) = x(1+\delta), |\delta| \le \tfrac12 \varepsilon_{\text{mach}}.
Floating point
Each row makes a different trade: fp32 spends bits on precision; fp16 keeps precision but its tiny exponent range overflows and underflows early; bfloat16 keeps fp32’s range and sacrifices the mantissa.
dtype eps smallest normal max
float16 9.766e-04 6.104e-05 6.550e+04
float32 1.192e-07 1.175e-38 3.403e+38
bfloat16 7.812e-03 (exponent range = float32)
bfloat16 eps equals 2^-7: True and 1 + 2^-8 rounds back to 1: True
bfloat16’s epsilon is 2^{-7}, not 2^{-8}: the eighth “bit” is the implicit leading 1, which fills no gap.
Floating point
Since 2022 hardware also offers fp8, in two flavors with a division of labor:
E4M3 keeps digits: \varepsilon = 0.125 (about one decimal), max = 448: for weights and activations. E5M2 trades a mantissa bit for fp16’s full range: max = 57344, smallest normal 6.1\times10^{-5}, at \varepsilon = 0.25: for gradients, which need range.
At \varepsilon_{\text{mach}} = 0.125 there is no slack left: fp8 training must pair every tensor with a scale factor. (The ml_dtypes package provides both formats.)
Floating point
Because e^x turns additive scale into multiplicative scale, a modest logit overflows: fp32 dies at x \approx 88.7, fp16 at x \approx 11.1.
1 + eps != 1 : True
1 + eps/2 == 1 : True
gap between adjacent float32 values near 1 : 1.1920929e-07
gap between adjacent float32 values near 2^20 : 0.125
float16: exp(x) overflows for x > 11.09
float32: exp(x) overflows for x > 88.72
fp16 gradients below 6\times10^{-5} vanish; updates of relative size below \varepsilon_{\text{mach}}/2 round to no update at all. Both limits bite mixed-precision training (next slide).
Floating point
The two failure modes, and their two escapes, in mixed-precision training:
Loss scaling is underflow management; master weights are rounding management: this is what the library’s mixed-precision utilities automate, and what bfloat16’s fp32-sized exponent makes unnecessary.
02
Softmax & cross-entropy
the one-line bug, and the shift that fixes it
Stable softmax
The most common stability bug is one line: \mathrm{softmax} exponentiates logits, so any logit past 88.7 makes the numerator inf and the ratio NaN. But softmax is shift-invariant:
\mathrm{softmax}(\mathbf{z} - c\mathbf{1}) = \mathrm{softmax}(\mathbf{z}),
so, for finite logits, shift by c = \max_i z_i: every exponent is \le 0, the denominator sits in [1,n], and exponential overflow is avoided.
naive, logits z : [0.09003058 0.24472846 0.66524094]
naive, logits z + 100: [nan nan nan]
stable, logits z + 100: [0.09003057 0.24472846 0.66524094]
naive and stable agree where both work: False
Stable softmax
The softmax normalizer earns its own operator. The same shift rewrites it exactly:
\mathrm{lse}(\mathbf{z}) = \log\textstyle\sum_j e^{z_j} = c + \log\textstyle\sum_j e^{z_j - c}, \qquad \max_j z_j \le \mathrm{lse}(\mathbf{z}) \le \max_j z_j + \log n.
Logits near 1000 overflow even float64; in log space they are effortless:
naive log(sum(exp(z))) : inf
stable log_sum_exp(z) : 1002.4076
log softmax : [-2.4075928 -1.4075928 -0.40759277]
probabilities sum to 1 : 1.000013
A soft maximum, within \log n of the true max, and the reason naive Bayes sums logs instead of multiplying probabilities.
Stable softmax
Cross-entropy is computable straight from logits with one stable lse:
-\log\mathrm{softmax}(\mathbf{z})_y = \mathrm{lse}(\mathbf{z}) - z_y.
The via-probabilities route forces the loss through the representable range of probabilities and fails in one of three ways, depending on the library: subnormal noise before inf, inf outright, or a silent clip:
gap CE from logits CE via probabilities
20 20.0000 20.0000
60 60.0000 60.0000
103 103.0000 inf
104 104.0000 inf
Stable softmax
The label is the unlikely class, so the true loss is the logit gap. From logits it is exact at every gap; via probabilities it fails:
MXNet. The softmax does not linger in the subnormal range: e^{-t} underflows to exactly 0 already at gap 103, and the loss reads inf at gaps 103 and 104.
The lesson generalizes: losses and likelihoods should live in log space from birth; convert to probabilities last.
03
Catastrophic cancellation
subtracting near-equal numbers
Cancellation
Subtracting nearly equal numbers is exact, yet it strips the leading digits they agreed on, exposing the trailing noise. Relative error is amplified by \tfrac{|a| + |b|}{|a - b|}\,u, which blows up precisely when a \approx b. In float32, 1 + 10^{-8} rounds to 1, so \log(1+x) returns 0, but log1p is exact:
float32 rounds 1 + x to : 1.0
log(1 + x) = 0.0 log1p(x) = 1e-08
a - b in float32 : 2.3841858e-07 (true value 3.0e-07)
amplification (|a|+|b|)/|a-b| ~ 8.4e+06
Standard victims: \log(1{+}x), e^x{-}1 near 0 (log1p, expm1); 1{-}\cos x; the quadratic formula near a double root. Reformulate; don’t add bits.
Cancellation
The one-pass variance formula \mathbb{E}[x^2] - \mathbb{E}[x]^2 subtracts two numbers near \mu^2 to get \sigma^2, amplification \mu^2/\sigma^2. Welford keeps a running mean and centered sum of squares, so nothing large is ever subtracted:
m_k = m_{k-1} + \frac{x_k - m_{k-1}}{k}, \qquad M_k = M_{k-1} + (x_k - m_{k-1})(x_k - m_k).
Mean 10^9, true variance 1, 10^5 samples, all in float64:
naive E[x^2] - E[x]^2 : -256.000000
Welford, one pass : 1.000257
two-pass reference : 1.000257
The naive answer is pure amplified noise; here it even comes out negative (-256), a variance below zero, its sign hostage to the summation order. This is what BatchNorm avoids with running moments.
Cancellation
The naive variance’s noise changed sign between NumPy builds. That is summation error at work. Adding n floats left to right commits one rounding per addition, worst case \approx n\,u; the repairs reorganize the additions:
left-to-right O(n\,u) · pairwise (sum halves recursively) O(u \log n) (what NumPy’s sum does, blocking and all) · Kahan (carry each rounding in a second accumulator) O(u), independent of n
Welford composes with either: the pairwise merge rule is exactly how running moments are combined across devices.
04
Conditioning
backward error, forward error, and κ
Conditioning
Forward error is what you want: \|\hat{\mathbf{x}} - \mathbf{x}\|. Backward error judges the algorithm: the smallest input perturbation for which \hat{\mathbf{x}} is exactly right. The condition number converts one into the other:
\frac{\|\hat{\mathbf{x}} - \mathbf{x}\|}{\|\hat{\mathbf{x}}\|} \le \kappa(\mathbf{A})\,\varepsilon.
correct digits \approx format digits -\,\log_{10}\kappa(\mathbf{A}). A backward-stable float64 solve carries \approx 16; \kappa = 10^k costs you k.
Conditioning
\kappa of the Hilbert matrix H_{ij} = 1/(i{+}j{-}1) grows exponentially. Solving \mathbf{H}\mathbf{x} = \mathbf{b} with \mathbf{x} = \mathbf{1}, the surviving digits track the rule of thumb row by row:
n kappa log10 kappa forward error correct digits backward error
4 1.6e+04 4.2 4.1e-14 13.4 0.0e+00
6 1.5e+07 7.2 1.4e-10 9.8 1.3e-16
8 1.5e+10 10.2 6.1e-08 7.2 5.7e-17
10 1.6e+13 13.2 8.7e-05 4.1 1.0e-16
12 1.8e+16 16.2 3.2e-01 0.5 9.8e-17
The backward error never leaves the 10^{-16} floor; the solver is blameless at every row. The matrix, not the algorithm, amplifies the error.
Conditioning
Solving least squares via \mathbf{A}^\top\mathbf{A}\,\mathbf{w} = \mathbf{A}^\top\mathbf{b} replaces \kappa(\mathbf{A}) with its square:
\kappa(\mathbf{A}^\top\mathbf{A}) = \kappa(\mathbf{A})^2.
With \kappa(\mathbf{A}) = 10^5, that is five extra digits lost versus an SVD/QR solve on \mathbf{A} directly:
kappa(A) = 1.0e+05 kappa(A^T A) = 1.0e+10
normal equations: relative error 7.7e-08 (7.1 correct digits)
SVD (lstsq) : relative error 5.5e-14 (13.3 correct digits)
This is why lstsq exists and numerical libraries solve least squares by QR or SVD, never by forming \mathbf{A}^\top\mathbf{A}.
Conditioning
Adding \lambda\|\mathbf{w}\|^2 lifts every eigenvalue of \mathbf{A}^\top\mathbf{A} by \lambda, so \kappa = \tfrac{\sigma_1^2 + \lambda}{\sigma_n^2 + \lambda}\downarrow 1: the valley rounds into a bowl, and the same \lambda pays twice, accurate solves and fast gradient descent. Unlike a true preconditioner, ridge changes the minimizer: it shrinks \mathbf{w}_\lambda toward \mathbf{0}.
iterations / kappa: [5.89 5.9 5.94 5.99 6.06 6.15 6.19 6.25 6. ]
Wrap-up
log1p, Welford).\kappa sets both solve accuracy and gradient-descent speed, and ridge is the one knob that helps both. The fixes throughout are reformulations rather than extra bits.