Applied Computing · Section 1

Floating Point

From bits to silicon — how a number lives inside the machine, and why deep learning invented a zoo of new ones.

Here's a thing worth sitting with for a moment. A computer has only a finite number of bits, but the real numbers go on forever — between any two of them there's another, and another, without end. So any scheme at all for writing a real number inside a machine has to throw most of them away. It's a lossy compression, every time, no exceptions. The only question — the whole game — is which numbers you keep exactly, and how you smear the error across the rest. Floating point is the answer almost the entire computing world settled on, and it's a beauty: it's just binary scientific notation, packed into three little fields and co-designed, hand in glove, with the hardware that runs it. Get this one idea and a dozen mysteries fall open at once — why 0.1 + 0.2 isn't 0.3, why your neural net trains in something called bfloat16, why a 1999 video game computed square roots with a magic number nobody could explain.

Chapter One — Theory

1. The Problem Floats Solve

Computers store finite bits; the reals are infinite and continuous. There's no way around it — you must choose a finite grid of numbers to keep, and everything else gets rounded to the nearest point on that grid. Two classic ways to lay down the grid have been fighting it out since the beginning, and the difference between them is the whole story.

Fixed point: lock the decimal place

Fixed point nails an imaginary binary point at one fixed spot. Take 32 bits and say: sixteen of you are the integer part, sixteen of you are the fraction, and that's that. It's simple, it's blisteringly fast — it's just integer arithmetic wearing a hat. But look at what you've locked yourself into: you can count up to about $65{,}535$, and you can resolve down to steps of $1/65{,}536$, and neither of those can move. The range and the precision are welded together. An atom's radius and the distance to a star simply cannot live in the same fixed-point format — one of them rounds to zero, the other to infinity.

Floating point: let the point float

Now here's the move. Floating point says: why pin the binary point down at all? Let it float. Instead of fixing its position, store the position itself — call that the exponent — right alongside the digits, which we call the mantissa. And the instant you do that, you've reinvented something you already know from school: scientific notation, just in base 2.

$ \underbrace{6.022}_{\text{mantissa}} \times 10^{\overbrace{23}^{\text{exponent}}} \qquad\longleftrightarrow\qquad \underbrace{1.01101}_{\text{mantissa}} \times 2^{\overbrace{-4}^{\text{exponent}}} $

And the payoff is enormous. You get a colossal dynamic range, and — this is the subtle, gorgeous part — you get relative precision instead of absolute. A float hands you roughly the same number of significant digits whether you're describing $0.0000003$ or $300{,}000{,}000$. The error grows in proportion to the size of the number, which, nine times out of ten, is exactly what you wanted. Measuring a galaxy? You don't care about the millimetre. Measuring a cell? You don't care about the kilometre. Floating point bakes that common sense right into the bits.

The one tradeoff, stated once. Every decision from here to the end of the chapter is the same dial: spend a bit on the exponent and you buy range; spend it on the mantissa and you buy precision. That's the whole landscape. Hold onto it.

Chapter Two — Theory

2. The IEEE 754 Anatomy

Almost every float you will ever touch obeys one standard, IEEE 754, and that's a small miracle of engineering diplomacy. It pins down the bit layout, the rounding, and every nasty edge case so tightly that the same code gives the same answer on your laptop, your phone, and a supercomputer. Every format carves its bits into three fields, always in this order:

sign1 bit
exponentE bits
mantissa (fraction)M bits

And the value of an ordinary — we say normal — number is read off like this:

The decoding rule
$ \text{value} = (-1)^{\text{sign}} \times 1.\text{fraction} \times 2^{\,\text{exponent} - \text{bias}} $

Three small tricks make this thing sing, and each one is worth turning over in your hand on its own.

Trick one — the implicit leading 1

In binary scientific notation, the leading digit of a normalized mantissa is always a 1. That's what "normalized" even means — you shift the point until the first 1 sits just to the left of it. But if it's always 1, why on earth would you spend a bit storing it? So IEEE 754 doesn't. It leaves the leading 1 implicit. Which means a 23-bit mantissa field is secretly giving you 24 bits of precision — you get one whole bit for free, just by noticing it had to be there.

Trick two — the exponent bias

The exponent has to reach both ways: big positive powers for huge numbers, big negative powers for tiny ones. You might reach for two's complement, the usual way computers do signed integers — but IEEE 754 does something cleverer. It stores the exponent as a plain unsigned integer with a fixed bias subtracted off:

$ \text{bias} = 2^{\,E-1} - 1 $

For FP32's 8-bit exponent the bias is $127$. So a stored field of 10000000 (that's $128$) means a true exponent of $128 - 127 = 1$; a stored 01111111 ($127$) means exponent $0$. Fine — but why bias instead of two's complement? Here's the lovely reason: so that floats sort correctly as integers. Reinterpret the raw bits of two positive floats as ordinary unsigned integers and compare them — the bigger float has the bigger integer. The processor can compare floats with the exact same circuitry it uses for integers. That is a deliberate hardware/software handshake baked into the number itself, and in a moment (§6) we'll reach across it and pull off a famous trick.

Trick three — the reserved exponents

Two exponent patterns are held back for special duty: all-zeros and all-ones. They mean something other than a normal number (that's §4). And that's the small print behind a number you'll otherwise find mysterious: a normal FP32 exponent runs from $-126$ to $+127$, not the full $-127$ to $+128$ you'd expect from 8 bits — because the two ends were spoken for.

You don't have to take any of this on faith. Jump to the Interactive Lab, click individual bits on and off, and watch the decoded value move. The implicit 1, the bias, the reserved patterns — they all stop being rules to memorize and become things you can see.

Chapter Three — Theory

3. Decoding a Float by Hand

Let's make it concrete and decode the FP32 pattern for $0.15625$, by hand, no computer. First, get it into binary scientific notation:

$ 0.15625_{10} = 0.00101_2 = 1.01_2 \times 2^{-3} $

Now assemble the three fields:

  • sign $= 0$ — it's positive.
  • exponent $= -3 + 127 = 124 = $ 01111100 — the true exponent plus the bias.
  • mantissa $=$ the part after the implicit leading 1, that's 01, padded out to 23 bits: 01000000000000000000000.
0
01111100
01000000000000000000000

And to run it backwards: $1.01_2 = 1.25$, the exponent is $124 - 127 = -3$, so $1.25 \times 2^{-3} = 1.25 / 8 = 0.15625$. ✓ It closes up perfectly, because we chose a number that fits. Most don't.

Why 0.1 is a lie

Try to encode plain old $0.1$. Write it in binary and watch what happens: $0.1 = 0.0001100110011001100\ldots_2$ — the block 0011 repeats forever, exactly the way $1/3 = 0.333\ldots$ never terminates in decimal. A finite mantissa simply cannot hold it. The nearest FP32 value isn't $0.1$ at all; it's

$ 0.1 \;\longrightarrow\; 0.100000001490116119384765625 $

and that little lie is the root of the most famous head-scratcher in all of programming:

>>> 0.1 + 0.2
0.30000000000000004

Nothing is broken here — and that's the point worth making loudly. Neither $0.1$ nor $0.2$ was ever exactly representable; their rounded stand-ins add up to a hair above $0.3$; and $0.3$ itself rounds to yet another nearby value. The hardware did everything right. It's the lossy compression doing precisely, faithfully, what the bits allow — no more, no less. Go to the lab and you can watch the exact stored value for any decimal you like, and see exactly how far the lie goes.

Chapter Four — Theory

4. The Special Values

Those two reserved exponent patterns buy floats four kinds of special behavior. And these aren't bolted-on afterthoughts — they're part of the contract, the thing that lets careful numerical code sail through edge cases without sprouting an if-statement at every turn.

Signed zero (±0)

Exponent all zeros, mantissa all zeros. Yes — there are two zeros, a $+0$ and a $-0$. They compare as equal ($+0 = -0$, as they should), but they quietly remember which side you underflowed from, and that matters: $1/(+0) = +\infty$ while $1/(-0) = -\infty$. The sign survives even when the magnitude doesn't.

Subnormals — the gentle landing

Exponent all zeros, mantissa non-zero. Here IEEE 754 changes the rules on purpose: the implicit leading bit flips from 1 to 0, and the exponent freezes at $1 - \text{bias}$.

$ \text{value} = (-1)^{\text{sign}} \times 0.\text{fraction} \times 2^{\,1 - \text{bias}} $

What are these for? They fill in the gap between the smallest normal number and zero, so you get gradual underflow — a soft, graceful slide down to zero instead of a cliff. Without them there'd be a suspiciously wide dead zone hugging zero where $a - b$ comes out as exactly $0$ even though $a \neq b$, which is the kind of thing that quietly wrecks an algorithm. (We'll see in §6 that this grace has a price in speed.)

Infinity (±∞)

Exponent all ones, mantissa all zeros. This is what overflow gives you, and what $1/0$ gives you, and it propagates like a sensible adult: $\infty + 1 = \infty$, $1/\infty = 0$. Your computation can sail past the edge of the representable and still carry meaning.

NaN — Not a Number

Exponent all ones, mantissa non-zero. This is the answer to questions that have no answer: $0/0$, $\sqrt{-1}$, $\infty - \infty$. NaN is contagious — touch it with any arithmetic and the result is NaN — and it has one truly spooky property: it is the only value not equal to itself. The expression NaN != NaN is true, and that self-inequality is exactly the idiom programmers use to sniff one out. (One more detail: the top mantissa bit splits quiet NaNs, which slip along silently, from signaling NaNs, which can trip a hardware alarm.)

All four live at the edges of the number line, and you can drive right up to them in the lab: push a value past a format's maximum and watch it tip into ∞; crank it below the smallest normal and watch the subnormals catch the fall.

Chapter Five — The Zoo

5. The Format Zoo

Now it gets interesting, because this is where that one tradeoff dial — range against precision — explodes into a whole menagerie of formats. And here's the unifying thing to hold in your head: every single one of these is the exact same idea, just with the bits split differently between exponent and mantissa. Once you see that, the zoo stops being a list to memorize and becomes one picture.

FP64
FP32
bfloat16
FP16
FP8 E5M2
FP8 E4M3
sign exponent (range) mantissa (precision)
The floating-point zoo
FormatBitsExp / MantBiasMax finiteSmallest normalDec. digitsStandard
FP64 (double)6411 / 521023~1.8 × 10³⁰⁸~2.2 × 10⁻³⁰⁸~15–17IEEE 754
FP32 (single)328 / 23127~3.4 × 10³⁸~1.2 × 10⁻³⁸~7IEEE 754
FP16 (half)165 / 101565504~6.1 × 10⁻⁵~3–4IEEE 754
bfloat16168 / 7127~3.4 × 10³⁸~1.2 × 10⁻³⁸~2–3industry (Google)
FP8 E5M285 / 21557344~6.1 × 10⁻⁵~1–2OCP
FP8 E4M384 / 37448~1.6 × 10⁻²~2OCP

Three things in that table repay a closer look.

bfloat16 is just FP32 with its tail chopped off. Look at the exponents: both have 8 bits and a bias of 127, so they cover the exact same range. bfloat16 simply keeps 7 mantissa bits where FP32 keeps 23. Which means converting FP32 → bfloat16 is literally throwing away the bottom 16 bits, and going back is padding with zeros. The conversion is nearly free in hardware — and that was the entire point of designing it.

FP16 and bfloat16 make opposite bets with the same 16 bits. FP16 spends more on the mantissa (finer precision) and pays with a cramped range that tops out at $65504$. bfloat16 spends on the exponent (FP32's full range) and pays with a coarse mantissa. That one difference decides which one you reach for — as §8 will show.

The two FP8 formats are a matched pair. E5M2 has the wider range (it borrows FP16's exponent); E4M3 has the finer precision. The names just spell it out: E<exponent bits>M<mantissa bits>. One wrinkle: the OCP E4M3 bends the IEEE rules — it throws out infinity altogether and reclaims those bit patterns to stretch its top finite value out to $448$. (PyTorch calls it float8_e4m3fn, where fn means "finite.") E5M2 keeps the usual infinities and NaNs.

Want to feel the dial turn? The lab lets you slide the exponent/mantissa split yourself and watch the range and the decimal digits trade off in real time — and watch the same number land cleanly in one format and round hard in another.

Interactive — Practice

▸ Interactive Lab

Now it's your turn. Everything below is live. Grab a slider, flip a bit, switch a format — and watch the number rearrange itself. Two benches here: the first is about a single number and the bits that hold it; the second is about the machine underneath, where precision turns into silicon. Keyboard works too — focus any slider and the arrow keys nudge it, Home and End jump to the extremes.

Controls
1
Figure 1
The bit explorer — click any bit to flip it
value = (−1)^sign × 1.fraction × 2^(exp − bias)
Drag the value slider to encode a target number; click individual bits to flip them and watch the value jump. The error line shows how far the stored value drifts from what you asked for.
The same number, encoded into the format you pick. Sign in red, exponent in blue, mantissa in amber. When a value can't be represented exactly, it rounds to the nearest grid point — and the relative error tells you how much that cost.
Figure 2
Why 0.1 is a lie
the nearest representable float is almost never the decimal you typed
Zoom far enough onto the real line and the representable floats (the ticks) are discrete. Your decimal (the marker) lands between two of them and snaps to the closer one. The readout shows the exact stored value — and the famous 0.1 + 0.2.
Figure 3
Relative precision: the gap grows with the number
ULP (gap between neighbors) ∝ magnitude — so the digits stay roughly constant
On a log–log plot the spacing between neighboring floats climbs as a staircase: each time the exponent ticks up, the gap doubles. That's relative precision made visible — big numbers are coarse, small numbers are fine, and the ratio stays put. Your chosen format (blue) versus FP32 (faint).

Chapter Six — The Machine

6. The Hardware / Software Interface

This is the heart of the whole thing. The format spec is a contract: software agrees on what the bits mean and how every operation must round, and hardware promises to make those operations come true in silicon, correctly, every time. Let's walk both sides of that handshake.

The FPU, then and now

In the 1980s the floating-point unit was a separate physical chip. The Intel 8087 sat in its own socket beside the 8086, and if you didn't have one, floating-point math was emulated in software and crawled — orders of magnitude slower. That was the original hardware/software interface in the most literal sense: a coprocessor with its own instructions, fed work by the main CPU.

The old x87 unit had a quirk worth remembering, because it's a parable. It computed everything internally at 80-bit extended precision on a little register stack. Sounds generous — but it caused maddening bugs. An intermediate result sitting in a register carried more precision than the very same value written out to memory as a 64-bit double, so a calculation could hand you different answers depending on whether the optimizer happened to keep a number in a register or spill it to memory. "Excess precision," they called it, and it's a cautionary tale about what happens when an abstraction leaks at the hardware boundary.

Modern x86 threw out the stack. SSE brought flat XMM registers (128-bit) that work on 32- and 64-bit floats directly, with no hidden extra precision; AVX widened them to YMM (256-bit) and AVX-512 to ZMM (512-bit). ARM has the same in NEON and SVE. Width matters because these are SIMD units — Single Instruction, Multiple Data — so one 512-bit AVX-512 instruction can add sixteen FP32 numbers, or thirty-two bfloat16 numbers, in a single shot.

The instruction set, the rounding, the flags

Floating-point operations are real machine instructions. A scalar single-precision add on x86 is ADDSS; the vectorized version is VADDPS. That set of opcodes is the software side of the contract — the ISA promises that ADDSS hands back the correctly-rounded IEEE 754 sum.

And what's "correctly rounded"? IEEE 754 defines several rounding modes, with round-to-nearest, ties-to-even as the default. (Ties-to-even means $2.5$ rounds to $2$, not $3$ — it dodges the slight statistical bias that "always round half up" would sneak into a long calculation.) The other modes — toward zero, toward $+\infty$, toward $-\infty$ — you can switch on at runtime through a control register: MXCSR on x86, FPCR on ARM. That same register also holds status flags, sticky bits that quietly record whether any operation was inexact, overflowed, underflowed, divided by zero, or did something invalid. Software can read them to catch numerical trouble, or ask the hardware to trap — raise an exception — the moment one trips.

Fused multiply-add

The single most important modern float instruction is FMA: it computes $a \times b + c$ with only one rounding, at the very end, instead of rounding after the multiply and then again after the add. So it's both faster (one instruction, one trip down the pipeline) and more accurate (no intermediate rounding error to accumulate). Nearly every numerical kernel you can name — matrix multiply, dot products, evaluating a polynomial — is built out of FMAs. One catch worth filing away: this means fma(a,b,c) and a*b+c can return different bits, which now and then ambushes someone chasing perfect reproducibility.

Subnormals are slow

Remember those graceful subnormals from §4? On a lot of CPUs, the moment an operation touches a subnormal value it falls off the fast hardware path into slow microcode — a $100\times$ slowdown is not a myth. So performance-critical code often flips on flush-to-zero (FTZ) and denormals-are-zero (DAZ) in the control register, trading a sliver of correctness right near zero for predictable speed. It's the software side of the contract deliberately relaxing the hardware side — for the sake of the clock.

Why low precision = cheap silicon (the punchline)

Here's the deep reason the whole zoo exists. The area and power of a floating-point multiplier scale roughly with the square of the mantissa width — because a hardware multiplier is, at bottom, a grid of partial-product adders. Halve the mantissa and you quarter the multiplier.

So an FP8 multiply-accumulate unit is tiny next to an FP32 one — and on a fixed slab of silicon you can pack many more of them. That's exactly what a GPU "tensor core" does: it performs a little matrix multiply (say a $4\times4$ block of multiply-accumulates) in a single operation, and by dropping to FP8 it crams thousands of those units onto the die. The precision you sacrifice buys raw throughput, and for workloads that can stomach the noise (§8), that's a spectacular bargain. The entire low-precision ML format explosion is downstream of this one fact about multiplier area — and you can feel it on the dial in Figure 5.

The bits are just bits: a famous hack

Because IEEE 754 was built so floats sort like integers (§2), you can reinterpret a float's bits as an integer and mess with them directly. The legendary example is the fast inverse square root from Quake III Arena:

float Q_rsqrt(float number) {
  long i; float x2, y;
  x2 = number * 0.5F;
  y  = number;
  i  = *(long*)&y;           // reinterpret float bits as an integer
  i  = 0x5f3759df - (i >> 1); // the magic
  y  = *(float*)&i;          // reinterpret back as a float
  y  = y * (threehalfs - (x2*y*y)); // one Newton step
  return y;
}

It works because a float's bit pattern is approximately proportional to the logarithm of the number it stands for — the exponent field literally is the base-2 log of the magnitude. Shift the integer right by one and you halve the exponent, which approximates a square root; negating flips that into an inverse; and the magic constant 0x5f3759df corrects what the mantissa contributes. A single Newton iteration polishes the guess to game-quality accuracy. It's the perfect demonstration that the hardware/software boundary is a convention you can reach across — though on today's hardware you'd just call the dedicated RSQRTSS instruction and be done. Play with it in Figure 6.

When the silicon breaks its word: a famous bug

That contract at the top of the chapter — hardware promises every operation comes true in silicon, correctly, every time — has one spectacular asterisk, and it cost nearly half a billion dollars. In 1994 Intel shipped the original Pentium, and for a small set of operand pairs its floating-point divide instruction, FDIV, quietly handed back the wrong answer. The hardware lied.

The classic demonstration is a single division you could check by hand:

4195835 / 3145727
  correct : 1.333820449…
  Pentium : 1.333739068…  ← wrong from the 5th digit on

About six parts in a hundred thousand off — invisible in a video game, ruinous in a ledger or a stress calculation. And the root cause is a lovely little lesson in itself. The Pentium divided using the SRT algorithm, which earns its speed by looking the next chunk of quotient bits up in a hard-wired table of 1,066 entries. Five of those cells had been left empty — a slip in the script that burned the table into the chip — and any division whose digits happened to steer into one of the five holes came back short. Five blank cells out of a thousand, etched identically into millions of processors.

A mathematician, Thomas Nicely, tripped over it in late 1994 when sums he was computing over the primes started to drift. Intel's first instinct was to wave it away — you'd hit it maybe once in nine billion random divides; we'll swap your chip only if you can prove your work needs it — and the public detonated. By December, Intel had caved to a no-questions-asked recall and taken a 475-million-dollar charge against earnings. But the deeper legacy outlived the money: the FDIV bug is a big part of why chipmakers now formally verify their arithmetic units — proving a divider correct with mechanized logic rather than merely testing it on a pile of examples — so that the promise on the first line of this chapter is today something much closer to a theorem than a hope.

Chapter Seven — The Machine

7. The Gotchas Every Programmer Hits

Every one of these falls straight out of the bit-level reality we've built up. None of them is a bug in your language; all of them are the compression showing through.

Floats are not associative

$(a + b) + c$ can differ from $a + (b + c)$, because each + rounds. Add a tiny number to a huge one and the tiny one simply vanishes — its bits fall off the bottom of the mantissa — so the order you add things in decides what survives. This has a sharp practical edge: a parallel sum, which reduces the numbers in a different order than a serial one, produces different bits. Bit-for-bit reproducibility across different thread counts or different GPUs is genuinely hard for exactly this reason.

Never compare floats with ==

Because of rounding, two computations that "ought" to land on the same number almost never do. Compare with a tolerance instead — but choose it with your eyes open. An absolute epsilon like $|a-b| < 10^{-9}$ falls apart for large numbers, where the gap between neighboring floats is already wider than your epsilon (you saw that staircase in Figure 3). A relative epsilon handles scale far better.

Catastrophic cancellation

Subtract two nearly-equal floats and you annihilate all their leading significant digits, leaving only the noisy trailing bits — and the relative error explodes. The classic cure is to rearrange the algebra so the dangerous subtraction never happens (rationalizing $\sqrt{x+1} - \sqrt{x}$ into $\frac{1}{\sqrt{x+1}+\sqrt{x}}$ is the textbook move).

Accumulate wide, store narrow

Sum a million FP32 numbers into an FP32 running total and you bleed precision as the total grows and the small new addends stop registering at all. Sum them into an FP64 accumulator instead — or use Kahan summation, which tracks the lost low-order bits and feeds them back in — and the error stays bounded. "Compute wide, store narrow" is a refrain you'll hear over and over, in numerical code and right at the center of how modern neural networks are trained.

Chapter Eight — Applications

8. Real-World Uses

Now the formats land on real jobs — and notice that every single choice comes back to the same dial: range against precision, matched to what the workload can tolerate.

FP64

FP64 — the precision workhorse

Scientific and engineering simulation lives here: fluid dynamics, climate and weather, molecular dynamics, orbital mechanics, finite-element analysis. Anywhere errors compound over billions of timesteps, or where catastrophic cancellation lurks, those extra digits earn their keep. It's also the default float in Python, R, MATLAB, and JavaScript — which is the quiet reason those languages "just work" for everyday math, at the cost of speed. (Note that consumer GPUs deliberately hobble FP64 throughput; it's the territory of expensive datacenter cards, because graphics never needed it.)

FP32

FP32 — the general-purpose default

This is the format real-time graphics was built on, so it's the native currency of GPUs, game engines, and shaders. It rules digital signal processing, audio, most embedded and scientific work that doesn't demand FP64 — and it was the standard for machine-learning training for years. Seven significant digits is plenty for the vast majority of programs, at half the memory bandwidth of FP64.

FP16 & bfloat16

The 16-bit pair, pulling opposite ways

FP16 is precision-first. Its home turf is graphics — HDR images and textures, where ten mantissa bits keep gradients smooth in a tidy 16 bits — and on the ML side, memory-bound inference on phones and edge devices. Its weakness is that cramped range: gradients in deep training routinely slip below FP16's $6\times10^{-5}$ floor and silently flush to zero. Training in FP16 therefore needs loss scaling — multiply the loss by a big constant to shove the gradients up into representable territory, then divide it back out. The sheer annoyance of loss scaling is what motivated the next format.

bfloat16 was built by Google for their TPUs on one insight: in neural-network training, range matters more than precision. Gradients sprawl across many orders of magnitude and must not underflow, but they're noisy anyway, so a coarse mantissa is fine. By keeping FP32's full 8-bit exponent, bfloat16 lets you train without loss scaling, and its trivial conversion to and from FP32 keeps mixed-precision pipelines clean (FP32 master weights, bfloat16 for the heavy matrix math). It's now the de-facto training format for large language models. If you train a modern network, you are almost certainly using it.

FP8 & beyond

FP8 — the frontier — and the road to FP4

NVIDIA's Hopper (H100) and Blackwell GPUs ship FP8 tensor cores, and they're the reason training trillion-parameter models is economically thinkable at all. The two variants run as a complementary pair: E4M3 (more precision) for the forward pass — weights and activations — and E5M2 (more range) for the backward pass — gradients, which need the wider dynamic range. The result is roughly double the throughput and half the memory of bfloat16. FP8 is also a leading format for inference quantization, shrinking deployed models so they fit in less memory and run faster. The catch is real: FP8 needs careful per-tensor scaling factors to keep values centered in its tiny window — but at frontier scale the numerical-engineering effort pays for itself.

And the dial keeps turning. FP4 formats (NVIDIA's NVFP4, the OCP MXFP4 "microscaling" format) push down to four bits, pairing tiny elements with a shared block-level scale factor to claw back enough effective range. Every step down trades more numerical care for more raw throughput — the very same bargain that has driven this whole march from FP64 all the way to FP4.

Chapter Nine — Practice

9. Sharpen Your Instinct

Now look — you've watched every bit move in the lab. Here's where you find out whether it's really yours. Each of these is a spot a working programmer actually walks into, and not one of them says the words "mantissa" or "ULP" or whispers which trick to reach for. Nobody does that for you in the wild. So that's the job: see the bit-level reality showing through the symptom, and name what's really going on. Keep the lab open in another tab if you want to flip the bits and check yourself.

Problem 1 · Embedded

A sensor driver hands you a raw 32-bit word, and you need to read it without a debugger: 0 10000001 01000000000000000000000 (sign · exponent · mantissa).

(a) What FP32 value is this? (b) What is the gap to the very next representable float above it — and would that same gap hold if the reading were a thousand times larger?

Domain note. Decode it the way §3 did — peel off the bias, restore the implicit leading 1. The "gap to the next float" has a name (the ULP); think about which field of the number sets it.
Problem 2 · Finance

A billing service keeps prices in FP32 dollars and rings up 10,000 line items of $0.10 each. The test total == 1000.00f fails, and a ticket gets filed against the payments team.

(a) Why is the total not exactly $1000.00? (b) What should they have stored instead?

Domain note. Reach back to "why 0.1 is a lie" in §3. Then ask the real question: what grid does money actually live on — and is it the same grid floats lay down?
Problem 3 · ML Training

You're training a network in FP16. Partway through, a gradient comes out to $3\times10^{-6}$. FP16's smallest normal number is about $6.1\times10^{-5}$.

(a) What happens to that gradient — and why might the weight it feeds simply never move? (b) Give two different fixes, and say which field of the format each one is really buying.

Domain note. The number is below the smallest normal value, not below the smallest representable one — so think about subnormals, and about flush-to-zero. One fix rescales the data; the other changes the format. (§4, §8 both feed this.)
Problem 4 · Scientific Computing

You solve $x^2 + bx + c = 0$ with the textbook formula $x = \frac{-b \pm \sqrt{b^2 - 4c}}{2}$. For $b = 10^{8}$ and $c = 1$, the large root comes out fine, but the small root prints as $0$ — clearly wrong, since the true value is near $-10^{-8}$.

(a) Which step destroys the small root, and why? (b) Rewrite the computation so it survives.

Domain note. Look hard at $-b + \sqrt{b^2-4c}$ when $b$ is huge: you're subtracting two numbers that are almost equal. There's a name for the wreckage in §7 — and the classic escape is to never let that subtraction happen (the two roots multiply to $c$).
Problem 5 · High-Performance Computing

A simulation sums a billion FP32 values. The single-threaded run and the 64-thread run disagree in the last few digits, every time, and QA insists it's a race condition.

(a) Is it a bug? (b) What would actually make the two runs agree bit-for-bit?

Domain note. A parallel reduction adds the numbers in a different order than a serial loop. Recall the property from §7 that order shouldn't matter for real numbers — but does for floats.
Problem 6 · ML Systems

You must pack a tensor whose values run from about $10^{-30}$ all the way up to $10^{30}$ into a 16-bit format.

(a) Can FP16 hold that span? Can bfloat16? (b) Which do you choose, and what exactly are you giving up?

Domain note. Don't think about precision first — think about whether the smallest and largest values even survive. That's a question about the exponent field, which is the one thing the two 16-bit formats split differently (§5).

Solutions

Problem 1 — Decode. Peel it apart. The exponent field 10000001 is $129$, so the true exponent is $129 - 127 = 2$. The mantissa $1.01_2 = 1.25$ once the implicit 1 is restored. So the value is $1.25 \times 2^{2} = $ 5.0. (b) The gap to the next float — one ULP — is $2^{\text{exp}-23} = 2^{2-23} = 2^{-21} \approx 4.77\times10^{-7}$. A thousand times larger (near $5000$, exponent $12$) the gap balloons to $2^{12-23} = 2^{-11} \approx 4.9\times10^{-4}$ — about a thousand times wider.

What breaks this? The spacing between floats is not constant — it scales with the magnitude of the number, because the exponent sets the step. That's the relative-precision bargain from §1, and it's exactly why an absolute epsilon for comparing floats falls apart at large magnitudes (§7).

Problem 2 — Money. (a) $0.10$ is a repeating fraction in binary, so it's never stored exactly; each of the 10,000 addends is a hair off, and the rounding of every + piles on top of that, so the total lands a touch beside $1000.00$ and the == fails. (b) Store money as an integer number of cents (fixed point) — or a purpose-built decimal type. Then $0.10$ becomes the integer $10$, exact, and the sum is exactly $100{,}000$ cents.

What breaks this? This was never a precision problem you could fix with more bits — it's a category error. Money is exact and decimal; floats are approximate and binary. Using a float for currency is reaching for the wrong number system entirely, the way reaching for a sum when the truth is a product is the wrong tool.

Problem 3 — Gradient underflow. (a) $3\times10^{-6}$ sits below FP16's smallest normal ($6.1\times10^{-5}$), so it lands in the subnormal range with badly degraded precision — and if the hardware runs in flush-to-zero mode (common on GPUs for speed, §6), it becomes exactly $0$, the update vanishes, and the weight never moves. (b) Either loss scaling — multiply the loss by, say, $1024$ to shove every gradient up into normal range, then divide it back out before the update (buying range by shifting the data) — or switch to bfloat16, whose 8-bit exponent reaches down to $\sim10^{-38}$, so $3\times10^{-6}$ is an ordinary number needing no tricks (buying range by spending exponent bits).

What breaks this? It's FP16's five-bit exponent — its range — that bites here, not its mantissa. Beginners reach for "more precision"; the actual fix is more range. That single realization is the whole reason Google built bfloat16 (§8).

Problem 4 — Cancellation. (a) With $b=10^8$, $\sqrt{b^2-4c}\approx b$, so the small root's numerator $-b+\sqrt{b^2-4c}$ subtracts two nearly-equal huge numbers — catastrophic cancellation — and every leading significant digit annihilates, leaving noise. (b) Compute the well-conditioned root first, $x_1 = \frac{-b-\sqrt{b^2-4c}}{2}\approx-10^8$ (an addition of like signs, no cancellation), then get the other from the product of roots $x_1 x_2 = c$: $\;x_2 = \dfrac{c}{x_1} = \dfrac{1}{-10^8} = -10^{-8}$. Exact-feeling, no subtraction of equals.

What breaks this? Subtracting two nearly-equal numbers throws away all the digits that agreed and keeps only the noisy ones. The bits were already lost when the values were rounded; the subtraction merely reveals the loss. The cure is algebraic — rearrange so the dangerous subtraction never occurs (§7).

Problem 5 — Reproducibility. (a) No bug. Float addition is not associative — $(a+b)+c \neq a+(b+c)$ in general, because each + rounds — so a parallel reduction, which combines the values in a different order than the serial loop, legitimately produces different low-order bits. (b) Pin down a deterministic reduction order, or accumulate in a wider/compensated form (FP64 accumulator, or Kahan summation) so the rounding error stays well below the digits you actually report and print, making the two runs agree.

What breaks this? Every + rounds, so the order you add in decides which bits survive — add a tiny number to a huge running total and it falls off the bottom of the mantissa entirely. "Compute wide, store narrow" is the standing answer (§7).

Problem 6 — Format choice. (a) FP16 tops out near $65{,}504$ and bottoms out (normal) near $6\times10^{-5}$ — so $10^{30}$ overflows straight to $\infty$ and $10^{-30}$ flushes to $0$; it cannot hold the span at all. bfloat16 keeps FP32's full 8-bit exponent, reaching from $\sim10^{-38}$ to $\sim3\times10^{38}$, so the whole range fits with room to spare. (b) Choose bfloat16 — and what you give up is mantissa: only 7 fraction bits, roughly two to three significant digits.

What breaks this? Sixteen bits can buy range or precision, never both at once. This data is screaming for range, so you spend the bits on the exponent and accept a coarse mantissa — the very same dial, §1 to §8, turned one more notch.

Chapter Ten — Close

10. Quick Reference

One card to keep beside you.

Choosing a format
If you're doing…Reach for
General-purpose code, unsure what you needFP32 (FP64 if precision is critical)
Scientific simulation, long error-accumulating runsFP64
Training neural networksbfloat16 (with FP32 master weights)
Frontier-scale LLM training / inferenceFP8 (E4M3 forward, E5M2 backward)
Graphics textures / HDR storage, edge inferenceFP16
The one tradeoff to remember

More exponent bits buy range; more mantissa bits buy precision. Every format is just a different point on that one line, and the right pick is whichever matches your workload's tolerance for noise against its appetite for throughput.

And if you keep only one picture from this whole chapter, keep this one: a float is binary scientific notation, packed into a sign-exponent-mantissa layout that was deliberately co-designed with the hardware — so the bits sort like integers, the operations round predictably in silicon, and the whole thing degrades gracefully at its edges. Everything else — the zoo, the gotchas, the magic constants — is just that one idea, followed honestly all the way down to the metal.