Floating point, cancellation, mixed precision, conditioning and backward error
Before this lesson: lesson 08 (the SVD), lesson 11 (norms) and lesson 12 (QR and least squares). Time: about 85 minutes, plus the notebook.
Why this matters #
Every number in a training run is rounded, and bf16 keeps only 8 significant bits (2–3 decimal digits). Rounding explains failures that look like bugs: a loss that becomes NaN in fp16, a running sum that stops growing, a variance that comes out negative, regression coefficients that change completely when one data point is added. Two ideas organize all of it. Conditioning says how sensitive the problem is: you lose about digits, whatever algorithm you use. Stability says whether the algorithm adds more error than that.
1. Predict first #
2. The picture #
A 3-digit decimal computer. Suppose a calculator keeps 3 significant digits: , . The gap between neighbours grows with size (0.01 near 1, 1000 near ), but the relative gap stays about the same. Rounding to the nearest number has a relative error of at most .
Binary formats work the same way, with powers of 2 instead of powers of 10. Between and the numbers are evenly spaced; at each power of 2 the gap doubles.
One fp16 number, bit by bit. fp16 uses 16 bits: 1 sign bit , 5 exponent bits and 10 fraction bits . A normal number is
The 15 is the bias. It lets the stored be an unsigned integer from 0 to 31 while the true
exponent can be negative. Decode the bits 1 10001 1010000000:
- Sign: , so the number is negative.
- Exponent: , so .
- Fraction: , so the significand is . The leading 1 is not stored (the hidden bit), so fp16 has bits of precision.
- Value: .
Two exponent values are special. gives zero and the subnormal numbers (no hidden bit). gives (if ) and NaN (if ). So the largest fp16 number has and all fraction bits 1: .
Click the bits or type a number. The sign, exponent and fraction bits are decoded step by step below them. Try largest (65504 in fp16), then ∞: all exponent bits are 1 and the fraction is 0; flip one fraction bit of and you get NaN. Type 0.1 and look at the stored value: 0.1 has no finite binary expansion. Switch to bf16: the same number gets 8 exponent bits and only 7 fraction bits.
Move or pick a preset. The top chart is the gap between neighbouring floats near : a staircase that doubles at every power of 2, for bf16, fp16 and fp32. The bottom rows draw the representable numbers of each format in the same small window around ; the dot is where is stored. Try 2049 (fp16 cannot store it), 1e−8 (fp16 underflows to 0) and 70000 (fp16 overflows).
fp16 has bits of precision. What is the gap between neighbouring fp16 numbers near ?
lies in . In that interval the gap is , so the fp16 numbers near 1000 are . The machine epsilon is the gap at 1 only; it is the relative gap, and the absolute gap grows with .
Cancellation on the 3-digit computer. The true values and are stored as and , each with a relative error below 0.4%. The subtraction is exact, but the true difference is : the result is wrong by 49%. The leading digits cancel, and the old rounding errors become the main part of what is left.
A matrix as an amplifier. maps the unit circle to an ellipse with semi-axes . Solving runs the map backwards. If lies along the long axis, is small (). A change along the short axis gives a large change . So the relative error can grow by . A flat ellipse (large ) means a nearly singular matrix.
Drag in the small disk. The two equations of are the orange and blue lines, and is where they meet. Changing shifts both lines in parallel (dashed), and the intersection moves to . The violet ellipse holds every possible for . Make small: the lines become nearly parallel, the ellipse stretches like , and the amplification approaches when you press Worst δb.
3. Definitions and main results #
Floating-point numbers #
A binary floating-point number is : a significand with bits of precision (the leading 1 is not stored) and an exponent in a fixed range.
| format | bits s/e/f | |
|---|---|---|
| fp64 (double) | 1/11/52 | |
| fp32 | 1/8/23 | |
| fp16 | 1/5/10 | |
| bf16 | 1/8/7 |
| format | largest | smallest normal |
|---|---|---|
| fp64 (double) | ||
| fp32 | ||
| fp16 | ||
| bf16 |
"s/e/f" counts the sign, exponent and fraction bits; the precision is = fraction bits + 1. Below the smallest normal number come subnormal numbers with fewer significant bits; the smallest positive fp16 number is .
- Machine epsilon is the gap between 1 and the next larger float: in double. It is not the smallest relative gap: the relative gap varies between and .
- Spacing. In the floats are evenly spaced with gap . So the gap near lies between and : the absolute gap grows with , the relative gap stays near . Example: the gap at 1024 in double is .
- Unit roundoff ( in double) bounds the relative error of rounding to nearest: with (no overflow or underflow).
- Standard model. IEEE 754 rounds the exact result of and : , . One operation is always accurate; trouble comes from how many operations combine.
Why . has no finite binary expansion, so it is rounded when stored (), and so are and . The sum of the stored and rounds to , while the stored is : two neighbouring doubles. The cause is that these decimals are not representable in binary, not the size of .
Cancellation #
Subtracting nearly equal floats is exact (Sterbenz: if , then is a float). The damage comes from the errors already in the inputs. If and with , then , so
The factor is the condition number of subtraction. When it is huge: this is catastrophic cancellation.
The fix: rewrite so that no nearly equal numbers are subtracted.
- Algebra: ; .
- Library functions for small :
log1p(x),expm1(x). - Quadratic : compute the root of larger magnitude with the sign that adds ( and have the same sign), then the other root from Vieta's formula .
- Variance: subtracts two large, nearly equal numbers when the mean is large compared with the spread. Use two passes, , or Welford's one-pass update of a running mean and a running sum of squared deviations.
Pick a formula and move the probe. Each dot is the true relative error of one evaluation in double precision (the exact values come from 100-digit arithmetic). The naive formula follows the dashed bound up to error 1; the stable rewrite stays at the level of for every input.
Conditioning against stability, in one example. The small root of is about . It is a well-conditioned function of the coefficients: a relative change of or changes the root by the same relative amount (condition number 1). Still, the textbook formula gives in double, 25% wrong. The formula is an unstable algorithm for a well-conditioned quantity: the formula itself creates the cancellation. The rewrite changes the algorithm, not the problem.
For small , which formula computes accurately in floating point?
has no subtraction, so every operation has relative error at most . The other formulas still subtract two nearly equal numbers ( and , or and ). More precision only moves the problem: for , rounds to exactly 1 even in double.
Mixed precision #
Training in bf16 or fp16 halves memory traffic and runs on fast tensor cores, but it needs three safeguards. The first one comes from a simple fact: a 16-bit running sum stops growing.
Why a 16-bit sum stops growing, step by step. fp16 has .
- In the fp16 numbers are apart.
- Below 2048 (in ) the gap is . From on, the gap is : the numbers are
- Up to 2048, each is a float, so the sum is exact.
- lies exactly halfway between 2048 and 2050. A tie goes to the neighbour whose significand ends in 0 (round to even): ends in 0, and ends in 1. So rounds to .
- From now on, every addition gives 2048 again. The sum is stuck.
bf16 has . The gap at is , so rounds back to 256: bf16 sums of ones stop at 256. In general, adding to is lost when is less than half the gap at (this loss is called swamping). fp32 has , so the same sum of ones only stops at .
Change the term and the number of additions. Each line is a running sum rounded after every addition: bf16, fp16 and fp32. On these log–log axes the exact sum is the grey diagonal, and a sum that stops growing becomes a horizontal line: fp16 at 2048 and bf16 at 256 for . The violet line keeps the inputs in bf16 but the running sum in fp32 (as tensor cores do); it stays on the diagonal.
The three safeguards:
- fp32 accumulation. Matmuls take 16-bit inputs but accumulate in fp32. Large reductions (softmax sums, normalization statistics, the loss) also run in fp32. The error bound for a sum of terms is about ; with bf16's this bound exceeds the sum itself once passes a few hundred.
- fp32 master weights. An update vanishes when is below half the gap at . In bf16 the gap above is , so rounds back to 1. The optimizer keeps an fp32 copy of the weights, applies updates there, and casts down for the forward and backward pass.
- Loss scaling (fp16). fp16 cannot represent magnitudes below about , and it
loses precision below . Small gradients underflow: becomes .
Multiply the loss by (a power of 2, so the scaling itself is exact). By the chain rule
every gradient is multiplied by and moves into range ().
Unscale in fp32 before the update. Dynamic loss scaling (PyTorch
GradScalerstarts at ) halves and skips the step when an inf or NaN appears (overflow above 65504), and doubles after 2000 steps without one.
A team switches mixed-precision training from fp16 to bf16. Which safeguard can they usually drop?
Loss scaling fixes a range problem (fp16 underflows below about ). bf16 has 8 exponent bits like fp32, so small gradients do not underflow. But bf16 has only 8 significant bits (fp16 has 11), so its precision problem is worse: running sums stop at 256 and updates below half the gap at are lost. That needs fp32 accumulation and fp32 master weights.
Conditioning #
The relative condition number of a problem is the worst amplification of a small relative input change:
For a scalar differentiable : .
For an invertible matrix , the condition number is (an induced norm). In the 2-norm, and (because ), so
for orthogonal , for , and for a singular . For a rectangular with full column rank, as well. The determinant does not measure conditioning: has and .
Theorem (error bound for ) #
Let be invertible, , and . Then
and in the 2-norm the bound is attained.
Proof. Each line says which fact it uses.
- . Subtract from .
- . is invertible.
- . An induced norm satisfies (lesson 11); use it with .
- . The same inequality with , since .
- . Divide line 4 by ; both are positive because .
- . Multiply lines 3 and 5 (both sides are nonnegative).
- Sharpness in the 2-norm. Lines 3 and 4 are equalities for the right vectors. Take : then and . Take : then and . The ratio is exactly .
A check. has , , .
- , so . Take : then . The relative change of is ; the relative change of is . The bound is attained.
- Same , but : then , a relative change of . The same size of has a 100 times smaller effect: the direction matters.
- , so , and : , a relative change of again. The worst case needs along (small ) and along .
Perturbing instead of
If , then , so and
To first order, . So also measures the sensitivity to changes in the matrix.
Theorem: #
Let have full column rank and SVD . Then . This is an SVD (and an eigendecomposition) of the symmetric positive definite , so its singular values are and .
Meaning for the normal equations. Any method that works with meets condition number . With , QR keeps about 10 digits and the normal equations about 4. For , the computed can be exactly singular (the Läuchli example of lesson 12). The digits are lost when is formed, before any solver runs.
A least-squares problem has . You solve it in double precision () with the normal equations . About how many correct digits can you expect?
Forming squares the condition number: , so about digits remain. QR works with itself and keeps about . becomes numerically singular only near .
Backward error and stability #
The picture first. A solver returns instead of the exact . Ask a different question: for which data is the exact answer? Put . Then solves exactly. If is very close to , the solver did its job well: it solved a nearby problem exactly. The relative size of that change of the data is the backward error.
Drag the computed answer in the left panel. The right panel shows the data: and , the data for which is exact. The violet ellipse holds every answer with the same backward error as . It is times longer than it is wide. Press error along v₂: a tiny change of the data explains a large error in the answer. Raise and watch the ellipse stretch.
Now the definitions. For a computed :
- the forward error is : how wrong the answer is. It is usually unknown, because is unknown.
- the backward error is the smallest relative change of the data that makes exact.
It is computable from the residual .
- If only may change: , so the backward error is .
- If only may change: . This is the usual definition for linear solvers.
- An algorithm is backward stable if its backward error is for every input. Householder QR and SVD solvers are backward stable; Gaussian elimination with partial pivoting is backward stable in practice. The normal equations are not backward stable for least squares.
Why the smallest change of has size
- Any with satisfies .
- So , that is, .
- The rank-1 matrix reaches this value: , and the 2-norm of is , so .
- Divide by .
Forward error from backward error. The computed is the exact solution for the data . Apply the error bound theorem with :
The same holds to first order for changes in . This gives the rule of thumb:
Conditioning belongs to the problem, stability to the algorithm, and the error is roughly their product. In double precision you lose about of the 16 digits. A small residual does not mean an accurate when is large.
Answers to section 1. (1) . From 2048 on, the gap between fp16 numbers is 2, so is a tie and rounds back to 2048 (ties go to the even significand). (2) About digits (forward error ). Yes, the residual is tiny, about , because the solver is backward stable.
4. Worked example #
Two nearly parallel columns.
Change in its fifth digit, . Then .
- and .
- is symmetric positive definite, so its singular values are its eigenvalues, the roots of : and . So .
- Bound: ; times gives . Actual: . The bound holds and is nearly sharp.
- Normal equations would face .
- In double,
np.linalg.solve(A, b)has relative error , inside .
In a regression the two columns are two almost identical features (say, temperature at two neighbouring grid points). The data cannot separate their effects, so the two coefficients follow the noise.
The same example, read backwards. Suppose a solver had returned for the original . Step by step:
- Residual: .
- Backward error in : . With changes in : .
- Forward error: , that is, 100% wrong.
- Check the bound: . ✓
So an answer that is completely wrong is the exact answer of a problem whose data differs by 0.0035%. If comes from a measurement with 4 digits, is as good an answer as .
For the matrix above (), a backward-stable solver in double precision returns . About how many correct digits does have?
forward error , so about digits are correct. NumPy's actual error is . The determinant says nothing about accuracy.
Cancellation and its fix. tends to as . It is very well conditioned there: , so .
| naive | rewrite | |
|---|---|---|
| (rel. error ) | ||
| (rel. error ) | ||
| (rel. error 1) |
At , rounds to exactly 1 (the gap below 1 is ), so all information is gone before the subtraction. The rewrite has no subtraction and is correct to about for every . The problem was well conditioned; only the formula was unstable.
5. In code #
NumPy shows the facts of this lesson. NumPy has no bfloat16 type, so a three-line helper rounds float32 values to bf16 by cutting the last 16 bits with round-to-nearest-even.
import numpy as np
from decimal import Decimal
def bf16(x):
"""Round to the nearest bfloat16 (ties to even); the result is a float32 that bf16 can store."""
b = np.asarray(x, dtype=np.float32).view(np.uint32).astype(np.uint64)
b = (b + 0x7FFF + ((b >> 16) & 1)) & 0xFFFF0000
return b.astype(np.uint32).view(np.float32)
for t in (np.float64, np.float32, np.float16):
fi = np.finfo(t)
print(t.__name__, fi.eps, fi.max, fi.smallest_normal) # ε, largest, smallest normal
print("bfloat16 eps", bf16(1 + 2**-7) - 1) # 0.0078125 = 2^-7
print(np.spacing(1.0), np.spacing(1024.0)) # 2.2e-16, 2.3e-13: the gap grows with |x|
print(Decimal(0.1 + 0.2)) # 0.3000000000000000444...
print(Decimal(0.3)) # 0.2999999999999999888...: a different double
bits = np.array([-6.5], dtype=np.float16).view(np.uint16)[0]
print(f"{bits:016b}") # 1100011010000000 = 1 | 10001 | 1010000000
s = np.float16(0)
for _ in range(10_000):
s = np.float16(s + np.float16(1))
print(s) # 2.048e+03 (= 2048): swamping
print(bf16(bf16(1.0) + bf16(1e-3))) # 1.0: the update is lost
print(np.float16(1e-8), np.float16(1e-8 * 2**16)) # 0.0 (underflow), 0.000655 (after scaling)
print(bf16(1e-8)) # 1.0011718e-08: bf16 has the range of fp32
The cancellation table:
import numpy as np
np.set_printoptions(precision=16)
x = np.array([1e-4, 1e-6, 1e-8])
print((1 - np.cos(x)) / x**2) # [0.4999999969612645 0.5000444502911705 0.]: wrong digits
print(2 * np.sin(x / 2)**2 / x**2) # [0.4999999995833334 0.4999999999999583 0.5]: all correct
Conditioning, and a tiny backward error with a large forward error:
import numpy as np
from scipy.linalg import hilbert
A = np.array([[1., 1.], [1., 1.0001]])
b = np.array([2., 2.0001])
print(np.linalg.cond(A), np.linalg.cond(A.T @ A)) # 4.0e4 and 1.6e9 = κ²
print(np.linalg.solve(A, b)) # [1. 1.]
print(np.linalg.solve(A, [2., 2.0002])) # [0. 2.] (up to ~1e-16)
x_hat = np.array([0., 2.]) # a wrong answer for the original b
r = b - A @ x_hat
print(f"{np.linalg.norm(r) / np.linalg.norm(b):.1e}") # 3.5e-05: backward error in b; forward error is 1
H = hilbert(10) # κ ≈ 1.6e13
x_true = np.ones(10)
b = H @ x_true
x = np.linalg.solve(H, b) # LU with partial pivoting
backward = np.linalg.norm(b - H @ x) / (np.linalg.norm(H, 2) * np.linalg.norm(x))
forward = np.linalg.norm(x - x_true) / np.linalg.norm(x_true)
print(f"cond {np.linalg.cond(H):.1e} backward {backward:.0e} forward {forward:.0e}")
# cond 1.6e+13 backward 1e-16 forward 4e-04: tiny residual, only about 3-4 correct digits
6. Where it shows up #
- Mixed-precision training.
torch.autocastruns matmuls in bf16 or fp16 and keeps numerically sensitive ops such as softmax, layer norm and the losses in fp32;GradScaleris for fp16. fp8 formats (E4M3, E5M2) have even less range and precision, so fp8 training uses per-tensor or per-block scale factors: the loss-scaling idea, applied to every tensor. - Stable formulas in ML code. Log-sum-exp subtracts the maximum before
exp(an overflow fix);log1p,expm1andsoftplusavoid cancellation in losses and log-probabilities; normalization layers and streaming statistics use two-pass or Welford variance. - ML for science. Physical fields have large means and small anomalies: temperature near 288 K with anomalies of about 1 K, surface pressure near Pa. A variance computed as in fp32 loses about 5 of its 7 digits; work with anomalies. Kernel (Gram) matrices of smooth kernels are extremely ill-conditioned, which is why Gaussian processes add a small jitter . Discretized differential operators have growing like with the grid spacing , which slows iterative solvers (lesson 14).
- Regression diagnostics. A large means some combination of features is nearly zero. Rescale columns, remove collinear features, or use ridge: has condition number .
7. Common mistakes #
- Taking for the smallest positive float. is the gap at 1; the smallest positive double is about (a subnormal). Also, books use "machine precision" for both and : say which one you mean.
- Blaming the problem for an unstable formula, or the algorithm for an ill-conditioned problem. Ask both questions: how large is , and does the algorithm add error beyond ?
- Reading a small residual as an accurate solution. The residual measures backward error; the forward error can be times larger.
- Judging near-singularity by . Use , or .
- Treating loss scaling as the bf16 fix while sums run in 16 bits. bf16's problem is precision, not range: accumulate in fp32.
8. Check yourself #
- What is the gap between doubles near ?
Answer
, so the gap is . - Rewrite for large . How bad is the naive formula at ?
Answer
. The true value is ; the naive formula has relative error (the two square roots agree in their first 13 digits), the rewrite . - What are and ?
Answer
, and : all singular values of are equal, although . - Why does bf16 training usually need no loss scaling, but still needs fp32 accumulation?
Answer
bf16 has 8 exponent bits like fp32, so small gradients do not underflow. It has only 8 bits of precision, so long sums swamp () and updates below half the gap at vanish: accumulate and update in fp32. - A solver returns with , and .
Is accurate?
Answer
The backward error is tiny (the solver is backward stable), but the forward error can be up to : only about 3 correct digits. The Hilbert matrix of size 10 shows exactly this ().
What are machine epsilon and the unit roundoff in double precision?
is the gap from 1 to the next float. bounds the relative rounding error: , .
Decode the fp16 bits
0 10000 1000000000(sign, exponent, fraction; bias 15)., so , . The value is .
What is catastrophic cancellation?
Subtracting two nearly equal rounded numbers. The leading digits cancel and the old rounding errors become most of the result; the relative error can reach .
What does bf16 trade compared with fp16?
bf16 has 8 exponent bits (the range of fp32) but only 8 bits of precision; fp16 has 11 bits of precision but 5 exponent bits (max 65504).
Why do 16-bit matmuls and reductions accumulate in fp32?
A 16-bit running sum stops growing once half the gap at exceeds the term (fp16: , bf16: ). fp32 has 24 bits of precision.
Write with singular values.
.
State the error bound for when changes by .
. In the 2-norm it is attained for along and along .
What is , and why does it matter?
. The normal equations lose about twice as many digits as QR for least squares.
What is the backward error of a computed solution of ?
The smallest relative change of the data that makes exact. With changes in it equals .
Conditioning versus stability: which belongs to the problem, which to the algorithm, and how do they combine?
Conditioning () belongs to the problem, stability to the algorithm. Forward error backward error for a backward-stable algorithm.
9. Interview angle #
- "Why does fp16 training need loss scaling and bf16 does not?" — fp16 has 5 exponent bits (smallest subnormal , max 65504), so small gradients underflow; bf16 has fp32's 8 exponent bits. Both need fp32 accumulation and master weights.
- "What is the condition number, and why should an ML engineer care?" — ; you lose about digits, , and gradient descent on a quadratic converges at a rate set by the Hessian's .
- "Your loss became NaN after switching to fp16. What do you check?" — overflow above 65504 in activations or logits, missing or badly tuned loss scaling, softmax or loss computed in 16 bits, and constants such as an that become 0 in fp16.
10. Practice #
The notebook for this lesson runs in Colab. It has five exercises, each with a check cell and a folded solution:
- Find machine epsilon of float16, float32 and float64 by halving until , and compare
with
np.finfo. - Rewrite and stably, and test them against exact
values from Python's
decimalmodule. - Write a stable quadratic solver and compare it with the textbook formula on .
- Write Welford's one-pass variance and compare it with on data with a large mean.
- Write
condition_numberandbackward_error, check on random matrices, and compare backward and forward error on the Hilbert matrix.
11. Watch and read #
- Driscoll and Braun, Fundamentals of Numerical Computation, free online: §1.1 floating-point numbers, §1.2 conditioning, §1.3 algorithms, §1.4 stability and §2.8 matrix condition numbers.
- David Goldberg, "What every computer scientist should know about floating-point arithmetic" (1991), section "Rounding Error" (relative error and ulps, cancellation).
- Nick Higham, "What is backward error?" (2020); for the full theory, Trefethen and Bau, Numerical Linear Algebra, Lectures 12–15 and 18.
- Micikevicius et al., "Mixed Precision Training" (ICLR 2018), §3.1–3.3 (fp32 master weights, loss scaling, fp32 accumulation).
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.