Part D · Numerics › Fixed-point and rounding

Module 8·Part D — Numerics·17 min

Fixed-point and rounding

Q-format as a contract, why truncation bias grows linearly while random error grows as a square root, and how fixed point buys reproducibility for free.

The core mental model

Fixed-point is integer arithmetic with an agreed binary point. A Qmm.nn value is an integer whose true value is raw/2n\text{raw}/2^n, and the crucial property is that the scale is a compile-time contract, not runtime data. That is the entire efficiency argument: no exponent to extract, no normalisation, no special cases — an adder is just an adder, and a multiplier is just a multiplier. The cost is that you now own the bookkeeping the floating-point unit used to do. Multiplication composes scales, so Qaa.bb × Qcc.dd lands in Q(a+c)(a{+}c).(b+d)(b{+}d) and needs the sum of the widths; addition requires equal scales, so aligning operands means shifting, and shifting right throws bits away.

operand Q1.1516 bitsproduct Q2.3032 bits — double width, freeaccumulator40 bits — guard bits against overflowresult after shift16 bits — you choose which
Two Q-format operands multiply into a double-width product, and the accumulator is wider still. The design decision is the window — which bits survive the shift back down. Take them too high and small values flush to zero; too low and large ones saturate. Floating point makes this choice per value at runtime; fixed point makes it once, at design time, for every value that will ever pass through.

Accumulator width is where this becomes concrete and where most people guess when they could derive. Summing NN products each WW bits wide needs W+log2NW + \lceil \log_2 N \rceil bits to remain exact. For an int8 × int8 dot product the products are 16 bits, so a 512-term reduction needs 16+9=2516 + 9 = 25 bits — which is why int32 accumulators are the universal answer and why that answer is not arbitrary. The companion decision is overflow behaviour: wraparound turns a small overflow into a sign flip, silently, which is how a marginally-too-large intermediate becomes a catastrophically wrong answer. Saturation clamps instead, so the error stays bounded and monotone. In signal processing and ML you always saturate; the cost is a comparator per operation and it is never the wrong trade.

Rounding is the part that is consistently underestimated, because the issue is bias rather than magnitude. Truncation on two’s-complement values rounds toward -\infty, so it injects a mean error of 0.5-0.5 LSB on every operation, and those errors add coherently: across NN operations the offset grows like O(N)O(N). Round-to-nearest-even is unbiased, so its errors partially cancel and grow like O(N)O(\sqrt{N}). For a 512-term sum that is an expected offset of 256 LSB versus a standard deviation of about 6.5 LSB — a factor of forty, arising purely from a choice that looks like a rounding detail. The same principle drives the FP8 rule: e4m3 for weights and activations (more mantissa, less range), e5m2 for gradients (more range, less mantissa), and always accumulate wider, because e4m3 has three mantissa bits and an accumulator in that format simply stops moving after a handful of additions.

Numbers worth memorizing

FormatMantissa bitsMachine epsilonMax valueTypical use
fp32241.19e-73.4e38accumulation, reference
tf32114.9e-43.4e38tensor-core matmul input
fp16119.77e-465504storage, small range
bf1687.8e-33.4e38fp32 range, cheap
fp8 e4m346.3e-2448weights, activations
fp8 e5m231.25e-157344gradients
int81 LSB127storage; int32 accumulate

Fixed-point rules worth being able to apply without looking up:

RuleExpressionExample
Value of a Qmm.nn integerraw/2n\text{raw} / 2^nQ15: raw 16384 → 0.5
Q15 range / resolution[1,1215][-1, 1{-}2^{-15}] / 3.05×1053.05\times10^{-5}the classic audio format
MultiplyQaa.bb × Qcc.dd → Q(a+c)(a{+}c).(b+d)(b{+}d)scales add
Accumulator widthW+log2NW + \lceil \log_2 N \rceilint8², N=512 → 25 bits
Truncation bias0.5-0.5 LSB per op, grows O(N)O(N)N=512 → −256 LSB
RNE error0 mean, grows O(N)O(\sqrt{N})N=512 → σ ≈ 6.5 LSB
Multiplier areaW2\propto W^28-bit is 16× cheaper than 32-bit
Adder areaW\propto Wwidth is cheap in adders

Critical thinking

You quantize weights to int8 and accuracy collapses. Give an ordered investigation.

Order matters here because the cheap checks eliminate most cases.

  1. Per-tensor versus per-channel scale. One channel with a 100× larger range forces a scale that crushes every other channel to a handful of levels. Per-output-channel scaling for weights is nearly free and usually the whole problem.
  2. Are you saturating or wrapping? If wrapping, a single outlier flips sign and the layer outputs garbage. Check the overflow behaviour before anything statistical.
  3. Accumulator width. W+log2NW + \lceil \log_2 N \rceil. If someone accumulated int8 products in int16, a 512-term dot product overflows routinely.
  4. Symmetric versus asymmetric. Post-ReLU activations are one-sided; a symmetric (zero-point free) scheme wastes half its range on values that never occur, costing a full bit.
  5. Calibration data. An unrepresentative calibration set gives ranges that clip real inputs.
  6. Activation outliers. In transformers a few channels run 100× larger than the rest, which destroys per-tensor activation scales specifically. The fixes are per-channel or per-group scaling, or keeping the outlier channels in higher precision.

Worth noting the counter-intuitive one: clipping outliers often improves accuracy. Clipping at a percentile instead of the max sacrifices a few saturated values and buys resolution everywhere else, and the trade is usually strongly favourable.

Why does truncation hurt so much more than its magnitude suggests?

Because it is biased, and bias accumulates coherently while random error accumulates in quadrature.

Truncation of a two’s-complement value rounds toward -\infty, so its error is uniform on [1,0][-1, 0] LSB with mean 0.5-0.5. Over NN operations the expected total is 0.5N-0.5N: for N=512N = 512, an offset of 256 LSB. Round-to-nearest-even has zero mean and error uniform on [0.5,+0.5][-0.5, +0.5], standard deviation 0.29\approx 0.29 LSB, so over NN operations the deviation is 0.29N0.29\sqrt{N} \approx 6.5 LSB.

That is a factor of forty at N=512N = 512, and the gap widens with NN because one grows linearly and the other as a square root. Then it compounds through layers: a systematic offset entering a nonlinearity shifts its operating point, and the next layer amplifies a bias it treats as signal. Random error largely cancels in the same situation.

The practical statement: the choice between truncate and round-to-nearest-even is not a rounding detail, it is the difference between error that grows like NN and error that grows like N\sqrt{N}. Round-half-up is also biased (toward ++\infty on ties) which is why the standard is specifically round-to-nearest-even — ties go both ways, so even the tie-breaking is unbiased. Stochastic rounding takes this further and is what allows small updates to survive in low-precision training rather than being rounded away every step.

What does bit-exact reproducibility across two machines actually require?

In floating point, considerably more than people expect:

  • Fix the reduction order. Floating-point addition is not associative, so any change in how a sum is decomposed changes the result. That means no atomics, no run-to-run-varying block or tile sizes, no library kernel that picks a different split by heuristic, and identical thread counts.
  • Control FMA contraction. a*b + c as a fused multiply-add rounds once; as separate operations it rounds twice. The compiler is free to choose, so it must be pinned.
  • Disable fast math. Any reassociation permission voids the guarantee.
  • Pin library versions. IEEE-754 mandates correct rounding for +,,×,÷,+, -, \times, \div, \sqrt{}, but not for transcendentals. Two libm implementations may both be excellent and differ in the last bit of exp, so reproducibility across machines requires the same math library, not merely a compliant one.

In fixed point it is free. Integer addition and multiplication are exact and associative, so any reduction order gives the identical result, no compiler flag can change it, and no library is involved. The only thing that must match is where saturation clamps.

That asymmetry is a substantive architectural argument, not a footnote. If you need an auditable, replayable, provably identical computation — a risk system, a settlement path, anything you may have to reconstruct months later — integer arithmetic gives it by construction, and floating point gives it only through discipline that one careless library upgrade destroys.

Why must FP8 be accumulated in a wider format? Show the failure.

Because e4m3 has three mantissa bits, so its machine epsilon is about 24=0.06252^{-4} = 0.0625.

Consider accumulating values around 1.0. Once the running sum reaches roughly 16, the smallest representable increment near that magnitude is about 1. Adding a value of 0.5 rounds to no change at all: the addition is a no-op, and it stays a no-op for every subsequent term. The accumulator stops moving while the true sum keeps growing. This is swamping, and in fp8 it happens after a handful of additions rather than the millions it takes in fp32.

So the rule is not a safety margin, it is what makes the reduction converge at all. Store and transport in fp8; accumulate in fp16 or fp32; requantise once at the end. Every tensor-core FP8 path does exactly this in hardware, and the FP8 “throughput” figures quoted for accelerators are always fp8 inputs with fp32 accumulation.

The general principle, which transfers to the fixed-point case directly: the precision needed for storage and the precision needed for accumulation are different problems with different answers. Narrow the first aggressively, because it buys bandwidth and multiplier area. Never narrow the second, because it buys almost nothing — accumulator width costs adder area, which grows linearly, while the multipliers you already shrank cost quadratically.

Choose a Q-format for a layer whose activations calibrate to a maximum of 6.2, feeding a 256-term dot product with int8 weights.

Work it in three steps, and state the accumulator explicitly because that is the step people skip.

Range. A maximum of 6.2 needs 3 integer bits plus a sign to represent up to 8. So Q4.11 in 16 bits gives range [8,8)[-8, 8) and resolution 2114.9×1042^{-11} \approx 4.9\times10^{-4}. If the distribution is heavy-tailed, clipping at the 99.9th percentile — say 3.1 — buys a whole extra fraction bit (Q3.12), and saturating the rare outliers is usually a net accuracy gain.

Product scale. Q4.11 activations times Q1.6 int8 weights lands in Q5.17, needing 23 bits.

Accumulator. 23+log2256=23+8=23 + \lceil \log_2 256 \rceil = 23 + 8 = 31 bits. A 32-bit accumulator fits with one bit to spare; a 24-bit accumulator overflows and, if it wraps rather than saturates, does so silently.

Requantise. Shift right by the difference in fractional bits, round to nearest even, saturate to the next layer’s format.

The habit worth building: every fixed-point layer is specified by four numbers — input format, weight format, accumulator width, output format — and the accumulator is derived rather than chosen. If someone cannot tell you their accumulator width, they do not yet know whether their pipeline is correct.

Self-check

Give the accumulator width for a 1024-term int8 × int8 dot product, and the general rule.

Products of two 8-bit values are 16 bits; the rule is W+log2NW + \lceil \log_2 N \rceil, so 16+10=16 + 10 = 26 bits, and int32 is the natural container. The rule states the width at which the sum remains exact — it is derived from the reduction length, never chosen by preference, and it is the number to ask for first when someone reports mysterious quantisation loss.

Why does truncation error grow as O(N) while round-to-nearest-even grows as O(√N)?

Truncation is biased: its error is uniform on [1,0][-1, 0] LSB with mean 0.5-0.5, so errors add coherently and the total grows linearly. RNE is unbiased with mean zero and σ ≈ 0.29 LSB, so errors add in quadrature and the total grows as the square root. At N=512N = 512 that is an offset of 256 LSB versus a deviation of about 6.5 LSB — roughly 40×, widening with NN. Ties go to even specifically so that the tie-breaking rule is itself unbiased.

What breaks bit-exact reproducibility in float, and why is fixed point immune?

Non-associativity: any change in reduction order changes the result, so atomics, varying tile sizes, differing thread counts, FMA contraction choices, fast-math reassociation and differing libm implementations all break it — IEEE-754 mandates correct rounding for the basic operations but not for transcendentals. Fixed-point integer arithmetic is exact and associative, so reproducibility is automatic and only the saturation points need to match.

Explain the FP8 swamping failure concretely.

e4m3 has three mantissa bits, giving ε ≈ 0.0625. Accumulating values near 1.0, once the running sum reaches about 16 the smallest representable step near that magnitude is about 1, so adding 0.5 rounds to no change — and every subsequent term does the same. The accumulator freezes while the true sum grows. Hence: store in fp8, accumulate in fp16/fp32, requantise once. Accumulator width costs linear adder area, while the multiplier savings you already banked were quadratic.

Why does multiplier area drive quantization strategy?

Multiplier area scales as W2W^2 while adder area scales as WW, so halving the multiplier width is a 4× area win and an int8 multiplier is about 16× cheaper than an int32 one. That asymmetry is why quantisation always targets the multiplied operands, why int8-in/int32-accumulate is the canonical shape, and why widening an accumulator for safety is far cheaper than the intuition from storage costs suggests.

Why can clipping activations at a percentile rather than the maximum improve accuracy?

Because the scale is set by the largest value, a single heavy-tailed outlier crushes the resolution available to the bulk of the distribution. Clipping at, say, the 99.9th percentile saturates a handful of values — bounded, monotone error, provided you saturate rather than wrap — and buys a fraction bit or more of resolution for the other 99.9%. The trade is usually strongly favourable, which is why calibration searches over clipping thresholds rather than simply taking the max.