# Floating Point

> Source: <https://tensor.khalilli.ai/blog/floating-point/>
> Published: 2026-08-10 00:00:00+00:00

Run this anywhere:

```
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
```

And in real training runs, this happens:

```
step 39,998   loss 2.41
step 39,999   loss 2.40
step 40,000   loss nan
```

Both outputs have the same cause: the way computers store numbers.
By the end of this page you will be able to predict both, explain
them to someone else, and read a line like "MXFP4, E2M1, block 32,
E8M0 scale" part by part. No prior knowledge is assumed. Every measured number on this page comes
from a small script you can run, linked where the number appears,
measured on an Apple M3 Max with torch 2.11.0 [[1](https://tensor.khalilli.ai/blog/floating-point/#ref-1)].

A computer stores a number in a fixed number of bits. Each bit has 2 states, so 32 bits can form 2 x 2 x ... x 2 = 2^32 = 4,294,967,296 different patterns. A number format assigns one number to each pattern. So for a 32-bit machine only those 4,294,967,296 numbers exist; anything else you compute is moved to the nearest one of them, and the distance moved is the rounding error. A format is exactly this: a choice of which numbers get a pattern. This page is about how that choice is made.

First, see the size of the problem. Between 1 and 2 there are infinitely many numbers. Zoom in anywhere and you find more. The line is never empty, at any depth. A format is a finite list of points. So almost no computed number is on the list, and almost every operation ends with a move to the nearest point:

The simplest choice is equal spacing: 0, 1, 2, 3, and so on, the same distance 1 between every pair of neighbors. That is what integers are, and for counting it works. Now take two numbers that one real neural network produced in one training step: an activation of 10.5 (a value flowing through the network) and a gradient of 0.0000004 (a value that steers learning). Store both with equally spaced points:

Those 2 numbers are not invented. They come from 1 training step of a small network, and this page keeps returning to that step, so here it is, drawn:

3 kinds of numbers move in that step. Activations are the values flowing forward through the layers. Weights are the numbers the network is learning; multiplying by them is what a layer does. Gradients are computed backward from the loss, 1 for every weight: each one says how much that weight should move. Train the network for a few seconds and measure all 3:

Read the two computed lines in the drawing. Why divide? The ratio says how many of the small number fit into the big one. With equal spacing, that is the number of steps between them. The gradients alone run from 0.00000037 to 0.71, and 0.71 / 0.00000037 is about 1,900,000, which is 2^20.9. So if the spacing is fine enough to keep the smallest gradient, the largest one sits 1,900,000 steps away. Across kinds it is worse: 10.5 / 0.00000037 = 28,000,000 steps. A larger model makes both numbers bigger. Equal spacing cannot hold both ends. Set the spacing to 0.0000004 so the smallest gradient survives, and even 2^31 = 2,147,483,648 points in the positive direction reach only 0.0000004 x 2,147,483,648 = 859: the format ends before 1,000. Set the spacing to 1 so that large values fit, and every gradient becomes 0, as the first drawing showed. The spacing itself has to change with the size of the number: points packed close near zero, spread out far from it.

One small tool first: what a dot means in base two, because from here on every number is bits. In decimal, each place is worth a tenth of the place before it: the 7 in 204.75 means seven tenths. Binary uses the same rule with 2 in place of 10. Left of the dot the places are worth 8, 4, 2, 1; right of it they are worth a half, a quarter, an eighth:

Read 0010.1100 with those place values: the 1s sit on 2, on a half and on a quarter, and 2 + 0.5 + 0.25 = 2.75. That is all a string of bits with a dot in it can mean, and you can now read any of them.

One question hides here, and it decides half of this page: which fractions can these places write exactly? The places are halves, quarters, eighths. Any finite sum of them is a fraction whose denominator is a power of 2: 1/2, 3/4, 5/8. Those end cleanly. Now take 1/3, 1/5 or 1/10. Their denominators carry a 3 or a 5. No finite sum of halves, quarters and eighths equals them. Watch both cases run:

Decimal plays the same game with different equipment. Base 10 is 2 x 5, so it digests 2s and 5s. Base 2 digests only 2s:

Now a first attempt that fails. Take 1 byte, which is 8 bits, and fix the dot in the middle: 4 bits for the whole part, 4 for the fraction:

This is called fixed point, and the drawing shows its problem: the dot's position decides the largest value and the smallest step at the same time, and no single position makes both good. The repair is in the name of this page. Let the dot move, and store where it went.

Where should the machine write down "where the dot went"? You already know the answer, because you already do this on paper. To write a very small or a very large number, you split it into its digits and its size: 0.00000037 becomes 3.7 x 10^-7. The 3.7 says what the digits are; the 10^-7 says how big the number is. 2 separate jobs, written separately:

A float makes exactly this split, in base 2. Any positive number
is some power of 2 times a number between 1 and 2: 10.5 is
1.3125 x 2^3 (check it: 2^3 = 8, and 1.3125 x 8 = 10.5), and
0.00000037 is about 1.552 x 2^-22. The power of 2 goes into one
field, named the **exponent**. The 1.something goes into the
other field, named the **mantissa**. The 2 words are only names
for the 2 halves of the split: the exponent is the number's
size, the mantissa is its digits.

What does each field buy on the number line? Draw them [[2](https://tensor.khalilli.ai/blog/floating-point/#ref-2)]:

Where do the intervals come from? From the split itself. Fix the power at 8. The digits part runs from 1.00 to 1.99. So the values run from 8 to just under 16: every number with power 8 lives between 8 and 16. Fix the power at 16 and you fill 16 to 32. That stretch starts twice as high, so it is twice as wide. That is all the doubling is. And the powers go both ways. The power -1 gives the interval [1/2, 1). The power -2 gives [1/4, 1/2). Each of those is half as wide as the next. So the intervals climb away from 1 in both directions: growing above it, shrinking below it, until the exponent runs out of values. The leftmost box in the drawing, [0.5, 1), is the first of the shrinking ones. Choosing the exponent is choosing the interval. 8 exponent bits count 2^8 = 256 intervals, and because every interval doubles, 256 of them span an enormous distance: that is how 8 bits reach from 1e-38 to 3e38. The mantissa then cuts the chosen interval into equal steps: 3 bits make 2 x 2 x 2 = 8 steps, binary32's 23 bits make 8,388,608. And the growing step we asked for at the start falls out by itself: [4, 8) is twice as wide as [2, 4), both are cut into the same number of steps, so every step in [4, 8) is twice as long. Small numbers get fine steps, large numbers get coarse ones, with nobody managing it.

Now store one number end to end. Move 1: pick the interval. 6.1 = 1.525 x 4, and 1.525 is between 1 and 2, so the interval is [4, 8). Move 2: say where inside. 6.1 sits 2.1 past 4, and 2.1 / 4 = 0.525. That fraction, 0.525, is what the mantissa holds:

In a real 32-bit float the split gets 1 bit for the sign, 8 for the exponent, 23 for the mantissa. Here is 6.1, every field decoded:

Two details in that card deserve their own sentences, because
every format on this page inherits both. First, the decode row
says "1 + 0.525", but the mantissa field only stored the .525.
The leading 1 costs nothing: every number in a power-of-two
interval is 1.something times a power of two, so the format does
not store the 1 and gets 24 bits of position for the price of 23.
(The subnormals, coming 2 sections from now, are 0.something
and are the one exception.) Second, the **bias**. Here is the problem it solves. The
exponent field is a box of 256 slots, numbered 0 to 255. Slot
numbers cannot be negative. But the exponents we need to store
run from -126 to 127: tiny numbers need negative ones. So the
numbers we have do not fit the labels the box has. The fix is
one move: slide every exponent up by the same constant, 127,
picked because it is the middle of 0 to 255. After the slide,
-126 sits in slot 1, -22 sits in slot 105, 0 sits in slot 127,
2 sits in slot 129, and 127 sits in slot 254. Nothing is
reordered and nothing is lost; the whole line moved as one
piece:

The stored slot number is the exponent plus the bias. So the two directions you saw earlier are just the slide and its undo: storing adds 127 (the -22 above went in as -22 + 127 = 105), and reading subtracts 127 (this card's 129 means 129 - 127 = 2). (Slots 0 and 255 are kept back; they build the edge cases 2 sections ahead.)

The rule behind 127 works in every format: the bias is the middle of what the exponent field can hold. 8 bits hold 0 to 255; the middle is 127. A 4-bit exponent field holds 0 to 15; the middle is 7, and you will meet that 7 on the next drawing.

The unsigned choice has a quiet payoff. A bigger float always
carries a bigger bit pattern, so a chip can compare 2 floats
with the integer circuits it already owns [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)].

One practical question: when does the machine do all this splitting? Mostly never at run time. The 6.1 in your source is text, and the compiler turns it into these 32 bits once, before the program runs. The 2.1 / 4 in our story needs no divider either: 4 is a power of 2, so dividing by it only moves the dot 2 places. After an add or a multiply, the hardware slides the result's dot until a single 1 stands in front, counts the slide into the exponent, and keeps the digits as the mantissa. Slides and counts: the format's own bookkeeping never divides.

Notice what the last decode row admits: the machine does not
store 6.1. It cannot. 6.1 is 61/10, and the 10 carries a 5.
The fractions figure showed what a 5 does in base 2: the bits
repeat forever. The mantissa keeps 23 of them and cuts the
rest. What remains decodes to 6.0999999..., the nearest
float32. The gap is the rounding error. The sections ahead
measure that gap and follow its consequences. This layout, the
bias and the field widths, was standardized in 1985 as IEEE 754 [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)], and it is the
format your `float32`

tensors use today, unchanged.

You cannot draw all 4,294,967,296 points of float32. But the same design at 8 bits has only 2^8 = 256 patterns, and its entire positive half fits in one picture. The 8-bit format is called E4M3, and it is not a toy: it is the FP8 on your GPU's spec sheet, the format H100 tensor cores multiply in. The name is just its recipe: E4 means 4 exponent bits, M3 means 3 mantissa bits. Every format name on this page reads the same way. This is the most important drawing on this page, because every format you will ever meet is this drawing with different counts:

Read the map slowly; every later section builds on it. Start with one count. After the sign bit, E4M3 has 7 bits left. 7 bits make 2^7 = 128 patterns. One pattern is the number 0. One pattern is NaN. 128 - 2 = 126. That is the whole story of "126 positive numbers": every pattern is drawn except those 2.

Now place the points. The 4 exponent bits pick from 2^4 = 16 exponent values. The lowest one is the dashed strip at the far left; its points work differently, and the next section explains them. The other 15 are the 15 intervals you see. Which intervals are they? Read them off with the bias, exactly as in binary32, only smaller. E4M3's exponent field has 4 bits, holding 0 to 15, so its bias is the middle: 7. Stored values 1 to 15, minus 7, give the true powers -6 up to 8. So the leftmost interval is [2^-6, 2^-5), which is [0.0156, 0.0312), and the rightmost is [2^8, 2^9), which starts at 256. 6 intervals sit below 1, holding the tiny numbers; 9 sit at 1 and above, holding the big ones. The drawing writes the power under each boundary label. Inside any interval, the 3 mantissa bits place the 8 points, and they are always the same 8: the interval's start times 1, times 1.125, times 1.25, and so on up to times 1.875. Look at the zoom: for the interval [1, 2) that is exactly 1.0, 1.125, ... 1.875.

The 2 spent patterns are the 2 holes you can see. The zero pattern would have been the first point of the dashed strip, so the strip holds 7 points instead of 8. The NaN pattern is the last point of the top interval. That interval starts at 2^8 = 256, so its last point would have been 1.875 x 256 = 480; that pattern means NaN instead, and the drawing marks it red. The biggest real number is the point before it: 1.75 x 256 = 448. Check the count region by region: 7 in the strip, 8 in each of 14 intervals, 7 in the top one. 7 + 112 + 7 = 126. It matches.

Next, the idea this whole page stands on. Ask first: what should a good format promise you? Not a small miss. A small miss compared to your number. Missing by 0.5 when your number is 1.5 ruins it; missing by 0.5 when your number is 12,000 changes nothing you care about. So the fair way to judge a miss is as a share: the miss divided by the number it happened to. The next drawing judges 4 cases that way, and each share is drawn as a red fill so you can see it instead of computing it:

Read the 2 orange bars in the drawing: identical. Here is why,
in one chain. An interval runs from its start to twice its
start, so its width equals its start. 8 steps cut that width, so
1 step is the start divided by 8, and the worst miss, half a
step, is the start divided by 16. Take the share: divide by the
start, and the start cancels. 1/16, at every size. Now the 2
words this subject uses. **Absolute error** is the size of the
miss; it doubles from interval to interval. **Relative error**
is the share; a float pins it at the same value everywhere.
Every float format works this way.

The local step also has a proper name, and you will meet it in
every numerics discussion: the **ulp**, the unit in the last
place [[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)]. "The ulp at x" means the step between neighboring
points where x lives. E4M3's ulp at 1.0 is 0.125; its ulp at 300
is 32. One number is enough to know a format's whole precision: the
ulp at 1.0. Every other interval's step is that same number,
doubled or halved some number of times. So if you know the step
in [1, 2), you know the step everywhere. The field guide ahead
prints it on every card, as "step at 1.0".

The proof under the map is 15 lines of Python that decode all 256 bit patterns from first principles, and torch agrees with every single one. The largest value is 448. The smallest positive value is 0.001953125. Divide them: 448 / 0.001953125 = 229,376, about 2^17.8. Read the exponent as a count of doublings: start at the bottom, double about 18 times, and you reach the top. That count is the format's whole reach. Now compare it with the second drawing: one tensor's gradients needed 21 doublings. The whole format is narrower than one tensor's spread, and that gap runs the entire training half of this page.

Four places on the map need special handling, and every format must decide all four.

**Zero.** The position field always means "1.something", so no
pattern naturally means zero. The all-zeros pattern is simply
assigned the value zero, as a special case. The sign bit still exists, so there is
a +0 and a -0, and they compare equal.

**The ramp.** Just above zero there is trouble. Every normal
point is 1.something x a power of 2. The exponent has a lowest
power, so there is a smallest normal point. In E4M3 the lowest
normal exponent value is 1 and the bias is 7, so the lowest power is
1 - 7 = -6, and the smallest normal point is 1.0 x 2^-6 =
0.015625. Below it, a naive format has nothing until 0. A
cliff. The fix: in the lowest interval only, drop the hidden 1.
Read the mantissa as 0.something instead of 1.something. Walk
through what that gives. The pattern 000 reads 0.000 x 2^-6 =
0: zero itself. The pattern 001 reads 0.001 x 2^-6, which is
1/8 x 0.0156 = 0.00195. The pattern 010 gives 0.0039. And so on
up to 111, which gives 0.0137. 7 evenly spaced points now fill
the gap between 0 and 0.0156. These points are the
**subnormals**. They turn the cliff into a ramp:

**Infinity.** When a result overflows the ceiling, float32 returns
a dedicated pattern called `inf`

, which then behaves lawfully:
anything finite divided by `inf`

is 0, and `inf - inf`

is the next
special value. E4M3 made a harder choice: it has no infinity at
all. Those patterns were used for one more doubling of range,
and overflow stops at 448 instead. Keep that choice in mind; it
is the first sign of how little room 8 bits leave.

**NaN.** Not a Number is the pattern returned when no answer is
defensible: 0/0, inf - inf, the square root of -1. It has one
deliberately strange law: `nan == nan`

is false, so `x != x`

is
the honest test for it. When your loss prints `nan`

, this value
is what you are looking at, and it arrived through an overflow or
an undefined step somewhere upstream. We will catch it in the act
in a few sections.

The formats in this guide carry nearly all of the world's
floating-point arithmetic, and you now own every idea needed to
read them. Each gets the same treatment: its card, the bit
counts and the numbers that follow from them; its map, where
its points land; where it came from; where it runs today; and
how it fails. One promise about the cards: none of their numbers is new
information. Give the 2 bit counts, E and M, and every row
follows by rules you already have. The bias is the middle of the
E-bit range. The lowest and highest powers come from sliding the
stored range back, so the ceiling and the floors follow. The
step at 1.0 is 1 divided by 2^M. The digits count follows from
the step. The brackets on each card point each row at the field
that sets it, and the proof computes every row from E and M and
checks it against torch's own tables
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) runs the check). The guide
covers the 4 wide formats, the ones that stand alone. The 8-,
6- and 4-bit formats enter later, each at the exact place in
the training story that needs it, and a closing sheet gathers
the whole family on one drawing.

Double binary32's byte count and you get the format the 1985 standard called double precision:

The bias is 1023, so the intervals run from 2.2e-308 up to 1.8e308: 2,046 of them, each holding 4,503,599,627,370,496 points. The ulp at 1.0 is 2.22e-16, which is about 16 decimal digits of position. A map that wide can only be drawn with breaks:

Where it came from: IEEE 754 began as the arithmetic William
Kahan designed with Intel for the 8087, the floating-point
coprocessor sold beside the 8086; the standards committee turned
that chip's arithmetic into everyone's law [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)][[18](https://tensor.khalilli.ai/blog/floating-point/#ref-18)]. float64 was
its wide grade, sized so ordinary science could chain millions
of operations and still trust the leading digits.

Where it runs today: everywhere you did not choose a dtype. A Python float is a float64. NumPy builds float64 arrays unless told otherwise. The rounding section ahead prints 0.1's stored digits; they are float64 digits.

Its proof: [p16](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) derives the whole card
from E and M and checks every row against `torch.finfo`

. One
extra line in it runs a vanishing add at this format's scale:
in float64, (1e16 + 1) - 1e16 = 0. The ulp at 1e16 is 2, and
the added 1 fell under half of it, so the sum rounded straight
back (the failure section ahead names this absorption). 16
digits push the cliff out of sight; no digit count removes it.

How it fails in a training loop: float64 rarely gives training
a wrong answer. Its problem is cost. 8 bytes per number is 2
times float32 and 4 times bfloat16, spent on digits training
cannot use: the sections ahead show training running where the
step at 1.0 is 0.0078, so digits 4 through 16 add nothing. On
GPUs there is a second cost: most chips carry few float64 units,
and NVIDIA's own throughput tables list float64 operations far
below float32 on most of its hardware [[19](https://tensor.khalilli.ai/blog/floating-point/#ref-19)]. torch's default is
float32, and float64 appears only when you ask
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) prints the default).

binary32 you have already decoded by hand; the card collects its numbers:

254 intervals from 1.2e-38 to 3.4e38, 8,388,608 points in each, about 7 digits:

Where it came from: the same 1985 standard's single precision [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)].
Where it runs today: it is the default dtype of torch;
`torch.tensor(1.0)`

is a float32 unless you say otherwise
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) prints the default from this
install). Even when matmuls run narrow, the master weights of
mixed-precision training stay float32 and the tensor cores
accumulate in float32, so the training half of this page
keeps this format at its center.

Now the format hiding inside it: TensorFloat-32. Since the A100,
NVIDIA's tensor cores can take an ordinary float32 matmul, round
each factor to 10 mantissa bits, multiply, and accumulate in
full float32 [[20](https://tensor.khalilli.ai/blog/floating-point/#ref-20)]. The tensors going in and out are ordinary
float32; only the multiply is narrow, reading 19 of the 32 bits
(1 + 8 + 10). TF32 is that mode, not a storage type you can give
a tensor. NVIDIA measured large speedups at matching accuracy on
deep-learning workloads [[20](https://tensor.khalilli.ai/blog/floating-point/#ref-20)]; for code that needs all seven
digits it is a silent cut, and that is why torch ships with
matmul TF32 off since version 1.12 [[21](https://tensor.khalilli.ai/blog/floating-point/#ref-21)]. On this install,
`torch.backends.cuda.matmul.allow_tf32`

is False, the cudnn
convolution flag is True, and
`torch.set_float32_matmul_precision("high")`

is the one-line
opt-in ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) prints all three). None
of this can execute on the Apple machine this page is measured
on; the flags are read here, the behavior is cited [[20](https://tensor.khalilli.ai/blog/floating-point/#ref-20)][[21](https://tensor.khalilli.ai/blog/floating-point/#ref-21)].

How it fails: you have watched it fail all page. The step of 8 at 1e8 is float32's. The 7 digits that turned a subtraction of near-equal inputs into a 19% error are float32's. In training, its failure is price: 4 bytes per number on the memory bus that a later section shows is the bottleneck.

Cut 32 bits in half and something must go. There are two ways to choose, and the choice split the 16-bit world in two:

float16 is the left fork: keep digits, pay with reach.

5 exponent bits give bias 15 and only 30 intervals, from 6.1e-5 to 65504. 10 mantissa bits give 1,024 points per interval and about 3 digits. 30 intervals fit in one drawing, so this is the one wide format whose map you can see whole:

Where it came from: film and games, not a numerics committee.
NVIDIA and Microsoft made `half`

a type in the Cg shading
language in 2002, and Industrial Light & Magic built its OpenEXR
film format on the same 1+5+10 layout, in production from 2000
and released as open source in 2003 [[22](https://tensor.khalilli.ai/blog/floating-point/#ref-22)]. IEEE 754 adopted the
layout as binary16 in its 2008 revision [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)]. The film industry
shipped the format years before the standard named it.

Where it runs today: graphics APIs and image pipelines still,
inference engines, and mixed-precision training on hardware from
before bfloat16 spread. In torch it is the half in `.half()`

.

Its proof and its failure are the same three numbers. The
ceiling: 60000 times 1.2 is inf in float16
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p7-sixteen-bits)). The digits: 1.001 stores as
1.000977. The floor: normal numbers end at 6.1e-5, and the small
half of a gradient histogram lives below that, which is why the
loss-scaling section exists. When float16 dies in a run, it dies
at one of these three numbers.

The right fork: keep reach, pay with digits. Google's brain float keeps all 8 of float32's exponent bits and 7 of mantissa:

The reach is float32's (the ceiling prints 3.39e38 rather than 3.4e38 only because the coarser last step lands the top point lower), the intervals are the same 254 as float32's, and each holds 128 points: about 2 decimal digits.

Where it came from: Google built it into the TPU's matrix
units, which multiply in bfloat16 and accumulate in float32, and
chose the layout so that range would never be the problem and
conversion from float32 would be nearly free [[23](https://tensor.khalilli.ai/blog/floating-point/#ref-23)]. The name is
Brain Floating Point, after the Google Brain team.

Nearly free is provable, and on this machine it is proven at the
bit level: bfloat16 is float32's top half. Take any of the
65,536 possible bfloat16 bit patterns, cast it to float32, and
the result is the same 16 bits with 16 zeros appended;
[proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p17-bf16-truncation) checks all 65,536, bit for
bit. The downward cast is the same move with rounding: keep the top
16 bits, round by the low 16. The proof checks
torch's cast against that hand rule on 1,000,010 values (a
million random ones plus the edge cases), and they agree on
every one. Here is 1.7014 making the trip:

```
float32:  0 01111111 1011001 1100011101111010
bfloat16: 0 01111111 1011010   (the top half, rounded up by the rest)
stored:   1.703125
```

Where it runs today: TPUs since their second generation [[23](https://tensor.khalilli.ai/blog/floating-point/#ref-23)],
NVIDIA GPUs since the A100 [[20](https://tensor.khalilli.ai/blog/floating-point/#ref-20)], and this machine: torch's CPU
autocast picks bfloat16 by default
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) prints it). In large-model
training it is the usual compute half of the mixed-precision
loop, with FP8 taking over the matmuls on the newest chips (the
FP8 section returns to this).

How it fails: 2 digits. 1.001 stores as exactly 1.0
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p7-sixteen-bits)). The step at 1.0 is 0.0078, so
a healthy update of 0.001 sits under half a step and rounds
away: the disaster that opens the training sections ahead, and
the reason master weights exist. bfloat16 did not make training precise. It
made training's failures the quiet, repairable kind, and the
machinery that repairs them is most of the rest of this page.

The guide pauses here, because at 8 bits and below no format stands alone. Each narrow format arrives later on this page, at the moment the training story needs it, and a closing sheet gathers the whole family in one drawing. Next, with your formats on the table: what exactly happens in the space between two points, because every format above handles it the same way.

Between any 2 neighboring points of the map lies everything the format cannot
say. When a result lands there, one rule decides its fate: go to
the nearer point. When it lands exactly halfway, there is a rule for the tie: go
to the point whose last mantissa bit is 0. Half of the halfway
cases end up rounding up and half down, so a long sum does not
slowly lean one way.
This pair of rules is round-to-nearest, ties-to-even, the default
of every machine you will touch [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)]:

The worst this rule can do is move a value by half the local
step: half an ulp. For float32 the ulp at 1.0 is 2 to the power -23, which is
about 1.2e-7 (read e-7 as: move the decimal point 7 places left,
so 0.00000012), and that number is called the machine epsilon
(`torch.finfo(torch.float32).eps`

prints it). So every operation lands
within about 6e-8 of the true result, measured relative to the
result's size. Both numbers read straight
off the map: find the interval that starts at 1, take its step, halve
it. The standard error analysis of floating point is this one
bound, applied once per operation [[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)].

How can hardware apply the rule without computing every bit of the exact answer first? It keeps 3 extra bits, and that is all it ever needs:

This explains the first code block at the top of the page, and you can check every step of it by hand. To write 0.1 in binary you double it, again and again, and each time the whole part of the result is the next bit. Why does doubling do that? Because in binary, doubling slides the dot 1 place, the same way multiplying by 10 slides the decimal dot. Whatever digit crosses the dot lands in front of it, and that digit is the next bit. 0.1 doubles to 0.2: the first bit is 0. Then 0.4, 0.8: two more 0s. Then 0.8 doubles to 1.6: the first 1, and the 0.6 carries on. 0.6 doubles to 1.2: another 1, and 0.2 carries on. But 0.2 is where the second step started. The process is in a loop, and the bits repeat forever:

1/3 does the same thing in decimal, and for the same reason: the
denominator has a prime factor the base does not. Ten is 2 times
5, so 0.1 is 1/(2 times 5), and base two has no 5. The format
cuts the repetition at its mantissa width and stores the nearest
representable number, which for float64 is exactly
0.1000000000000000055511151231257827... ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p0-riddles)
prints every digit). The same happens to 0.2 and to 0.3, and the
sum of the two stored numbers is not the stored number nearest
0.3.
Nothing malfunctioned. 3 numbers you typed do not exist,
and the printed digits show exactly which nearby numbers were
stored instead.

One more piece of standard equipment: the fused multiply-add, 1
instruction computing a times b plus c with a single rounding at
the end instead of 2. The result is as if the product had been
computed exactly and only the final sum were rounded, and on
modern chips it costs about as much as 1 multiply [[27](https://tensor.khalilli.ai/blog/floating-point/#ref-27)]. The
difference is not small print. Choose a = b = 1 + 2^-27 and
c = -(1 + 2^-26): the separate path rounds the product first and
returns exactly 0, while the fused path returns the true answer,
2^-54 ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p21-fma-one-rounding) runs both on this
machine). Every tensor core in the training half of this page is
a lattice of these fused units, and the sums they accumulate
stay in float32 even when the products arrive in 8 bits. Keep
that sentence; it is why low-precision training is possible at
all.

The unit itself is worth one look, because the training half of this page keeps returning to it:

Three rules of ordinary arithmetic stop holding once every result is rounded to a point, and all three cause real bugs.

**Absorption.** At 100,000,000, which is 1e8, float32's step is 8, so
adding 1 offers a move smaller than half the gap, and the sum
rounds straight back ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p4-absorb-cancel)):

**Cancellation.** Subtract two nearly equal numbers and their
shared front digits erase each other. Whatever rounding noise
the inputs carried is suddenly the front of the answer:

The proof above subtracts 1.0000001 from 1.0 in float32 and gets
1.19e-7 where the truth is 1.0e-7: a 19% error from one
subtraction of two almost-exact inputs. The strange part: the
subtraction itself commits no error at all. When 2 numbers are
within a factor of 2 of each other, their difference is always
exactly representable (Sterbenz's lemma [[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)];
[proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p22-exact-error) checks it on this very case).
Cancellation never creates error; it exposes the error the
inputs already carried.

**Order.** Because every addition rounds, (a + b) + c and
a + (b + c) are different numbers. The smallest possible
example, drawn:

Sum 50,000 values
forward and backward and the answers disagree
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p5-sum-order)); a parallel machine that splits the
same sum across cores picks yet another order, and another answer.
torch's own `.sum()`

adds pairwise in a tree, which is both
faster and about 130 times closer to the true sum than a
one-by-one loop (measured: 0.24 error against 0.0018), and compensated summation (Kahan's trick of carrying
the rounding error in a second variable) closes most of the rest
[[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)].

One more fact turns this whole section from a list of failures into a toolbox: the rounding error is catchable, exactly. When s = a + b rounds, the missing sliver is itself a number the format can hold, and 3 operations hand it back whole:

Run it on this page's opening example and the 3 operations
hand back the exact rounding error of 0.1 + 0.2
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p22-exact-error) checks it against exact rational
arithmetic). A fused multiply-add does the same for a product in
1 instruction. Kahan's compensated sum is this trick run in a
loop, and the double-word arithmetic that stretches precision in
software is this trick kept instead of thrown away [[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)].

Keep the order fact in mind. It returns at the end of this page as the reason two identical training runs on two GPUs never match to the last bit.

Open any model's source code and small constants appear: an
`eps=1e-5`

in the layer norm, an `eps=1e-8`

in Adam, a
`logits - logits.max()`

in every attention implementation. None of them is superstition; each one is a patch over a place
where a formula's intermediate value leaves the map.

The largest number float32 can hold is about 3.4e38, and exp
reaches it at 88.73 (measured below). Softmax, the function that
turns a model's raw output scores, its logits, into
probabilities, applies exp to every logit. Logits above 88.73
exist in every large model, so a naive softmax returns inf, then
inf divided by inf, and your loss prints nan. The repair
costs nothing: softmax only sees differences, so subtract the
maximum first and the largest input becomes 0
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p6-stability)):

The same pattern explains the rest of the family, one line
each. `logsumexp`

slides by the max, for the same reason softmax
does. `log1p(x)`

and `expm1(x)`

exist because near zero, 1 + x
absorbs the x: the absorption figure again, happening at 1.0
instead of 1e8. The layer-norm epsilon keeps a near-zero
variance from turning `rsqrt`

into inf. Adam's epsilon guards
its denominator the same way. And an attention mask uses a large
negative number instead of -inf, so that after the slide it
becomes a clean 0 instead of a nan. One picture, many patches:
keep every in-between value where the points are.

The guide paused at 16 bits. Here is why the family keeps going down anyway.

Think about what training actually spends its time on. Mostly it moves numbers. Weights travel from memory to the arithmetic. Gradients travel back. Billions of numbers, every step. And the wire that carries them is slower than the multipliers that use them. Now halve the bits per number. The same wire carries twice the numbers. The same memory holds twice the model. The tensor cores, built for the narrower type, double their arithmetic too. One halving, three payoffs. Here is the wire, drawn to scale:

The only question is whether the surviving points still cover the numbers the network produces with small enough error. Networks tolerate rounding noise unusually well: they are trained on noisy batches and judged over many outputs, so the answer stays yes far below 32 bits if the format is chosen carefully. The fork figure already showed the first halving's two options, and training mostly picked bfloat16: reach first. The cost is a step of 0.0078 at 1.0, and the next section shows what that step does to learning.

At 16 bits the map's steps are wide enough to stop training itself. A bfloat16 weight sitting at 1.0 has neighbors 0.0078 away. A healthy update of 0.001 is an arrow one eighth of that step:

The proof runs it: 1,000 consecutive updates of 0.001 leave a
bfloat16 weight at exactly 1.0 ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p8-lost-update)).
Not slowed. Stopped, silently, for that weight. This is why
every mixed-precision recipe since 2017 keeps a float32 master
copy of the weights [[5](https://tensor.khalilli.ai/blog/floating-point/#ref-5)]. The forward and backward passes run
narrow. But the update lands in the float32 copy, where the
local step is 65,536 times finer and 0.001 fits with room to
spare. The other repair is
stochastic rounding, which we will need again at 4 bits: round
up with probability proportional to how far you got. In the same
proof, the stochastically rounded weight reaches 2.0 alongside the
master copy.

So the loop that trains every model you use runs like this:

The products accumulate in float32 inside the tensor cores: that is the fused multiply-add from the rounding section doing its job. Precision goes exactly where the numbers need it and nowhere else. Stations 6 and 8, the S dial and the inf check, are the subject of the next section.

One group of numbers still fails inside this loop. Gradients are the smallest numbers in training, and float16's floor is high: its subnormals end near 6e-8 and its normal range starts at 6e-5. The left tail of the gradient distribution simply falls below every point of the format:

The rescue is one multiplication. Multiply the loss by a
number S before the backward pass. Gradients are built from the
loss by multiplications and additions, so every gradient comes
out S times bigger, and the whole histogram slides right, into
the band where float16 has points. Divide by S afterward.
Nothing about the mathematics changed; the only difference is
which gradients survived the trip. In practice S adjusts
itself: see an overflow, halve S and skip that step; survive a
stretch of clean steps, double it. That is the whole of `GradScaler`

([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p9-loss-scaling) measures the cast; on our toy
net only 0.1% of gradients die, and the fraction grows with depth
and training time, which is why the recipe exists
[[5](https://tensor.khalilli.ai/blog/floating-point/#ref-5)]). bfloat16 mostly
retired this machinery: it keeps float32's reach, so its normal
floor sits at 1.2e-38 against float16's 6.1e-5, about 34 powers
of 10 lower, and the scaler became optional.

Halve again, to 8 bits, and something new happens: no single
split of the bits serves both halves of training anymore. The
forward pass wants digits. The backward pass wants reach. So at
8 bits the fork ships both of its arms, as a pair [[6](https://tensor.khalilli.ai/blog/floating-point/#ref-6)]: E4M3
for digits, E5M2 for reach.

E4M3 you have known since the map section: its 15 intervals are
this page's teaching map, and its 256 patterns are decoded in
[proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p2-e4m3-map). The card records its one non-IEEE
choice: no infinity. Those patterns were used for one more
doubling of range, overflow stops at 448, and a single pattern
means NaN.

E5M2 is float16 with the bottom byte cut, and that is a
bit-level fact, provable the same way bfloat16 was: all 256
E5M2 patterns cast to float16 as the same 8 bits with 8 zeros
appended ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p20-e5m2-top-byte) checks every one).

NVIDIA, Arm and Intel proposed the pair for deep learning in
2022 [[6](https://tensor.khalilli.ai/blog/floating-point/#ref-6)], and hardware arrived with the Hopper chips. In use,
E4M3 carries the forward pass. E5M2 carries the gradients,
whose thin far tails need reach more than digits. And one more
piece rides along: every FP8 tensor gets one float32 scale of
its own. The scale is picked from the tensor's recent largest
value, so that the tensor's histogram slides into the band.
This is exactly the loss-scaling move, now made automatic and
applied per tensor. Recipes differ only in when they measure
that largest value: from recent history (delayed scaling) or on
the spot (current scaling):

And not everything is quantized. (To quantize a tensor: move
its numbers onto a narrower format's points.) Attention's and
the MLP's big matmuls run in FP8. Softmax, the normalizations,
the first embedding and the final projection stay wide [[11](https://tensor.khalilli.ai/blog/floating-point/#ref-11)].

Wired into a transformer through NVIDIA's Transformer Engine,
the published result is training 30 to 40 percent faster at
matching loss curves [[6](https://tensor.khalilli.ai/blog/floating-point/#ref-6)]. DeepSeek-V3 pushed the same idea one
step finer: it trained a 671-billion-parameter model with FP8
matmuls by giving a scale to every 128-element tile instead of
every tensor [[7](https://tensor.khalilli.ai/blog/floating-point/#ref-7)]. Hold that thought; it is halfway to the next
section. And one honest caution before the next halving. E4M3
carries about 1.2 decimal digits. E5M2 carries about 0.9. At 8
bits, no single stored number is trustworthy. Only averages
over many are.

Before the next halving, meet the format waiting at the bottom, because its 2 numbers explain everything that follows. 4 bits:

16 patterns. 15 values (0 appears twice, once with each sign).
No inf, no NaN. And one number to hold on to: the whole
positive range is 6 / 0.5 = 12x, about 3.6 doublings. The 2023
OCP standard defined E2M1 as the 4-bit element of the block
family these sections are building [[9](https://tensor.khalilli.ai/blog/floating-point/#ref-9)], and the newest NVIDIA
chips run it at twice the FP8 rate [[24](https://tensor.khalilli.ai/blog/floating-point/#ref-24)]. But alone it is
unusable. Even the per-tensor scale that carried FP8 cannot
save it, because per-tensor scaling has one enemy. Transformer
activations grow a few channels whose values run hundreds of
times larger than the rest, systematically, past about 6
billion parameters [[8](https://tensor.khalilli.ai/blog/floating-point/#ref-8)]. One scale must now serve the spike and
the bell at once:

Read the measured numbers. FP8 with a single scale loses 1% of
the ordinary values next to a 500x outlier, because a float grid
keeps its relative error at any scale until values fall below
its smallest positive number, and FP8's smallest is 229,376x
under its largest (448 / 0.00195, from the map section). FP4's
smallest is only 12x under its largest (6 / 0.5, computed
above). One spike takes the whole 12x, and
100% of the ordinary values quantize to zero
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p10-fp4-outlier)). At 4 bits, blocks are not an
improvement; nothing works without them.

So the scale moved into the data type. The Open Compute Project's
MX formats, standardized in 2023 by AMD, Arm, Intel, Meta,
Microsoft, NVIDIA and Qualcomm [[9](https://tensor.khalilli.ai/blog/floating-point/#ref-9)], cut every tensor
into blocks of 32 and give each block one 8-bit scale:

Every idea on this page meets here, so take it slowly. The
scale byte is an E8M0: a float with no mantissa at all. It is
the interval-picker from the floating-dot figure with the
position deleted, so applying it is just exponent addition,
nearly free in silicon. The block of 32 is the histogram idea
cut fine: an outlier can now only hurt its own 31 neighbors.
The price is small. 32 x 4 + 8 = 136 bits for 32 numbers, 4.25
bits each; 136 / 128 = 1.06, so 6% over raw 4-bit storage. Even the block size is a
measured compromise: smaller blocks fit better but spend more
bits on scales, and our own sweep across sizes 8, 16, 32, 64
and 128 shows both effects moving
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p11-mxblock)). The same block layout carries
8-, 6- and 4-bit elements, named MXFP8, MXFP6, MXFP4.

What the tensor core does with 2 of these blocks is 1 more drawing:

The block layout also changed who does what inside FP8. With a
scale for every 32 values, one block rarely spans more than
E4M3's 18 doublings. So E4M3, which has 8 points per doubling
where E5M2 has 4, measured better for the gradients too. The
largest published MXFP8 pretraining run, 8 billion parameters
on 15 trillion tokens, quantizes every tensor as E4M3 [[11](https://tensor.khalilli.ai/blog/floating-point/#ref-11)].

The 6-bit elements deserve their own cards, because they are the family's open seats: E2M3 keeps digits, E3M2 keeps reach, one more run of the fork.

Their maps are small enough to draw complete: 3 intervals of 8
points, 7 intervals of 4. Blackwell-class chips execute them at
full speed as MXFP6 [[9](https://tensor.khalilli.ai/blog/floating-point/#ref-9)][[24](https://tensor.khalilli.ai/blog/floating-point/#ref-24)]; almost no published recipe uses
them yet. A format survives by having a job nobody else does.
FP8 is safer, FP4 is faster on paper, and FP6 is still looking
for its job.

NVIDIA's NVFP4 variant tightens it further: 16-element blocks, a
fractional E4M3 scale instead of a power-of-two one, plus one
float32 scale per tensor, buying a finer fit for a quarter bit
more [[10](https://tensor.khalilli.ai/blog/floating-point/#ref-10)]. The bytes carry the quarter bit, and the deeper
difference sits in the scale byte itself:

Here is one NVFP4 block quantized by hand, every scale shown:

The largest of the 16 values, 15.011, sets the scales and comes
back exactly; 0.25 and 0.5 fall below half of the smallest step
at that scale and come back as 0; 3.2002 comes back as 3.7528,
about 17% off ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p19-nvfp4-worked) runs every
number). A single 4-bit value is coarse. The bet of 4-bit
arithmetic is that billions of such errors, kept unbiased,
cancel on average.

One subtlety earned a hardware rule. The E8M0 scale can only be a power of 2, so a block whose largest value is 5.9 must round its scale one way or the other, and the 2 ways are not close:

NVIDIA measured the difference as the gap between divergence and
parity at trillion-token scale [[11](https://tensor.khalilli.ai/blog/floating-point/#ref-11)]; [proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p11-mxblock)
shows the mechanism in 2 lines.

The whole scaling story now fits on one ladder, and every recipe of the last 5 sections is 1 rung of it:

The destination format, E2M1, was drawn whole 2 sections back: 15 values, 12x wide. Training inside those 16 patterns needs 3 rescues beyond blocks, and each is an idea from earlier on this page pushed one step:

The first is stochastic rounding, the lost-update repair now
doing the main work. At 4 bits nearly every update is below half the
local step, so deterministic rounding freezes everything; rounding
up with probability proportional to progress is unbiased in
expectation. Our proof runs 10,000 micro-updates: nearest
rounds to a frozen 1.0, stochastic reaches a mean of 3.0113
against a true 3.0, noisy per run and honest on average
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p12-sr-drift)).

The second is the random Hadamard transform: rotate the tensor by an orthogonal matrix before quantizing and undo it after, which changes no matmul (the rotation cancels) but spreads an outlier's energy across every coordinate:

Our 64-value proof takes the max-to-mean ratio from 11.8 to 2.6
([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p13-hadamard)). The third is selective precision:
the first and last layers, where quantization error is measured
to hurt most, stay in higher precision. With all three plus
NVFP4, a 12-billion-parameter model has been pretrained on 10
trillion tokens at nearly the same loss as its FP8 twin
[[10](https://tensor.khalilli.ai/blog/floating-point/#ref-10)]; MXFP4 needs more tokens for the same loss
[[12](https://tensor.khalilli.ai/blog/floating-point/#ref-12)]. Where each rescue is applied is itself measured engineering
[[10](https://tensor.khalilli.ai/blog/floating-point/#ref-10)][[24](https://tensor.khalilli.ai/blog/floating-point/#ref-24)]. Stochastic rounding runs only on the gradients; on
the forward pass it would add noise for nothing, so weights and
activations round to nearest. The Hadamard rotation runs only
on the inputs of the weight-gradient matmul. And weights are
scaled in 16 x 16 squares rather than 1 x 16 rows, for a reason
worth drawing:

A row-scaled weight matrix and its column-scaled transpose are 2
different quantized matrices, and the forward and backward
passes must see the same weights. The last roughly 15% of
layers stay in higher precision, and if a gap to the wide
baseline remains at the end, switching the forward pass to
higher precision for the final phase of training closes most
of it [[10](https://tensor.khalilli.ai/blog/floating-point/#ref-10)][[24](https://tensor.khalilli.ai/blog/floating-point/#ref-24)]. 4-bit training is real and it is young; treat
recipes as versioned, not settled.

Inference relaxed one constraint: if only the weights are quantized and arithmetic runs wider, the 16 points need not be a float at all. 3 designs compete on the same bell-shaped distribution of trained weights:

INT4 spaces its points equally, so it wastes points where
weights are rare. FP4 doubles its steps and crowds zero. NF4,
from the QLoRA work, places its 16 values at the quantiles of a
gaussian, which is approximately what trained weights are, and
it wins the tail ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p14-grids), [[13](https://tensor.khalilli.ai/blog/floating-point/#ref-13)]). Around
these grids grew the post-training toolkit, one idea per tool.
GPTQ rounds each weight so as to compensate the error left by
the weights before it [[14](https://tensor.khalilli.ai/blog/floating-point/#ref-14)]. AWQ rescales channels so the few
weights that matter most land on finer points [[15](https://tensor.khalilli.ai/blog/floating-point/#ref-15)]. And the
same how-many-bits question now runs through the KV cache, and
even the optimizer states: an 8-bit Adam keeps its moments in
blocks with per-block scales [[16](https://tensor.khalilli.ai/blog/floating-point/#ref-16)], the MX idea arriving from
the other direction.

Every format on this page has now been met. Gather them:

Read the sheet by columns. The bit bars share one scale: you can see float64 dwarf everything and the narrow rows shrink to almost nothing. The reach bars collapse from float64's 2,098 doublings to E2M1's 3.6; every format from float16 down fits its whole reach on the drawing. The digits column falls from 16 to 0.6. The block rows at the bottom are the repair: the same narrow elements, plus a shared scale whose dashed arrows say what it does, sliding each block's small reach to wherever its values live. And the sheet is the page in one drawing: everything before the field guide taught you what the columns mean, and everything after it was the story of how the narrow rows are made usable.

All of it, on one shelf:

These panels are the honest reason this whole family of
formats exists. A
7-billion-parameter model is 28 GB of float32 or 3.7 GB of MXFP4
with its scales; a training run holds 8x its bf16 weights; and
in serving, the KV cache decides how many conversations one GPU
carries. gpt-oss ships its 120-billion-parameter
mixture-of-experts in MXFP4 so it fits a single 80 GB GPU
[[9](https://tensor.khalilli.ai/blog/floating-point/#ref-9)].

Serving adds a loop of its own. A chat model generates 1 token
at a time, and every token's step reads all the weights and the
whole KV cache, the stored attention keys and values of every
token so far; serving engines keep that cache in fixed-size
pages so many conversations can grow side by side without
fragmenting memory [[29](https://tensor.khalilli.ai/blog/floating-point/#ref-29)]. That loop, end to end:

Throughput follows the halvings only where the hardware speaks
the format natively. On a GPU that must unpack a narrow format
in software, the format saves memory but not time. That warning
is measurable. One published comparison ran the same
image-generation model on a GPU with no MXFP8 hardware. The
MXFP8 file was smaller. Generation was slower than bfloat16:
112.8 seconds against 96.0, because every block had to be
unpacked on the way in [[25](https://tensor.khalilli.ai/blog/floating-point/#ref-25)]. The same tests also ranked output
quality: FP32 first, then FP16 and BF16 about equal, then
MXFP8, then per-tensor-scaled FP8, then NVFP4, then plain FP8
last [[25](https://tensor.khalilli.ai/blog/floating-point/#ref-25)]. Size and quality move together, and a scale at any
granularity beats no scale. Who
speaks what natively today: NVIDIA's Blackwell chips run
MXFP8/6/4 and NVFP4 in their tensor cores [[24](https://tensor.khalilli.ai/blog/floating-point/#ref-24)]; AMD's newest
Instinct chips run the MX family [[26](https://tensor.khalilli.ai/blog/floating-point/#ref-26)]; Hopper GPUs and
Trainium2 can store MX blocks but unpack them for the math [[26](https://tensor.khalilli.ai/blog/floating-point/#ref-26)].
The same facts, drawn so a spec sheet can be checked against
them:

And what a native format buys, next to the measured price of one the hardware cannot speak:

Read a spec sheet's format row before believing its speedup column.

Two runs of the same training script, same seeds, same data,
on two different GPUs, will not match bit for bit. You now own
every reason. Sums land in different orders on different core
counts. Tensor cores round differently than the unfused loop.
TF32 trims mantissas silently on one machine and not another. And there is a quieter actor: the compiler. IEEE
754 fixes each instruction's bits, but no language promises
which instructions your line becomes. A compiler may legally
fuse a * b + c into 1 fused multiply-add (1 rounding) or keep it
as 2 instructions (2 roundings), and the choice can flip with an
optimization flag or with something as small as inlining a
function [[27](https://tensor.khalilli.ai/blog/floating-point/#ref-27)]; the rounding section's proof showed the 2 paths
disagreeing. torch.compile is a compiler too: its fused kernels
are allowed to round differently than eager mode [[28](https://tensor.khalilli.ai/blog/floating-point/#ref-28)]. And
rounding in 2 hops is not rounding once: take an exact value to
float64 first and then to float32, and you can land on a
different number than going straight to float32, because the
first rounding can create a tie the second one breaks the other
way ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p23-double-rounding) builds one). This
double rounding is why the old x87 unit, which computed in
80-bit registers and rounded again on every store to memory,
made a program's bits depend on register allocation [[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)]. None of it is a bug; all of it is the map.

The same physics follows a model into serving. At temperature 0
an LLM should answer one prompt one way, and public endpoints do
not: one measured run asked a model the same question 1,000
times and got 80 different answers [[31](https://tensor.khalilli.ai/blog/floating-point/#ref-31)]. The cause is not which
GPU core finishes first; each kernel by itself returns the same
bits every run. The cause is the batch. A server computes your
request together with whoever else arrived, its kernels choose
their splits, and so their addition order, by batch size, and a
different order means different last bits. Other people's
traffic changes your logits, and at some token the changed bits
flip the chosen word. The repair is the one this page keeps
teaching: pin the addition order. Kernels rewritten to reduce in
one fixed pattern at every batch size made all 1,000 answers
identical, at about a 2x cost in an unoptimized server [[31](https://tensor.khalilli.ai/blog/floating-point/#ref-31)].

The working rules: compare models with tolerances
(`torch.testing.assert_close`

, never `==`

); flip
`torch.use_deterministic_algorithms(True)`

when you need
repeatability inside one machine and accept the speed cost; and
when a loss goes nan, hunt the overflow upstream (an unmasked
softmax, an unscaled fp16 gradient, a variance of zero) rather
than rerolling the seed. The nan at the top of this page has the same kind of cause as
the 0.1 + 0.2: some number moved past the edge of its map.

Konrad Zuse's Z3 computed in 22-bit floating point in 1941,
special values included. 44 years later IEEE 754, largely
William Kahan's design, ended an era in which every vendor's
arithmetic disagreed [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)]. The formats since bfloat16
are the same standard's ideas at new bit counts, and the full table
of every named format that ever shipped, from the Z3 to the
G.711 telephone float of 1972 (an 8-bit float in every phone call
for 50 years) to NVFP4, is longer than this page and belongs
to a follow-up table of its own. The roads not taken, posits and
logarithmic number systems, are worth reading about
[[17](https://tensor.khalilli.ai/blog/floating-point/#ref-17)].

You already did, in the proofs. The 15-line decoder in
[p2_e4m3_map.py](https://tensor.khalilli.ai/blog/floating-point/#proof-p2-e4m3-map) is a complete FP8
implementation that torch agrees with on all 256 patterns; the
block quantizer in [p11_mxblock.py](https://tensor.khalilli.ai/blog/floating-point/#proof-p11-mxblock) is MXFP4
minus the packing; the stochastic rounder lives in
[p12_sr_drift.py](https://tensor.khalilli.ai/blog/floating-point/#proof-p12-sr-drift). Change E4M3's constants
to E5M2's and check yourself against torch again: that is the
exercise that makes every format in this family yours.

Predict first, then run, then explain the difference:
[p0](https://tensor.khalilli.ai/blog/floating-point/#proof-p0-riddles) the two opening questions;
[p1](https://tensor.khalilli.ai/blog/floating-point/#proof-p1-where-numbers-live) your own histogram;
[p2](https://tensor.khalilli.ai/blog/floating-point/#proof-p2-e4m3-map) all 256 of E4M3;
[p4](https://tensor.khalilli.ai/blog/floating-point/#proof-p4-absorb-cancel) absorption and cancellation;
[p5](https://tensor.khalilli.ai/blog/floating-point/#proof-p5-sum-order) order;
[p6](https://tensor.khalilli.ai/blog/floating-point/#proof-p6-stability) the cliff;
[p7](https://tensor.khalilli.ai/blog/floating-point/#proof-p7-sixteen-bits) the fork;
[p8](https://tensor.khalilli.ai/blog/floating-point/#proof-p8-lost-update) the vanished update;
[p9](https://tensor.khalilli.ai/blog/floating-point/#proof-p9-loss-scaling) the slide;
[p10](https://tensor.khalilli.ai/blog/floating-point/#proof-p10-fp4-outlier) the spike;
[p11](https://tensor.khalilli.ai/blog/floating-point/#proof-p11-mxblock) blocks;
[p12](https://tensor.khalilli.ai/blog/floating-point/#proof-p12-sr-drift) stochastic rounding;
[p13](https://tensor.khalilli.ai/blog/floating-point/#proof-p13-hadamard) the rotation;
[p14](https://tensor.khalilli.ai/blog/floating-point/#proof-p14-grids) the three grids;
[p16](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) the four cards from two numbers;
[p17](https://tensor.khalilli.ai/blog/floating-point/#proof-p17-bf16-truncation) all 65,536 bfloat16 patterns;
[p18](https://tensor.khalilli.ai/blog/floating-point/#proof-p18-family-sheet) the family sheet;
[p19](https://tensor.khalilli.ai/blog/floating-point/#proof-p19-nvfp4-worked) one NVFP4 block by hand;
[p20](https://tensor.khalilli.ai/blog/floating-point/#proof-p20-e5m2-top-byte) all 256 E5M2 patterns;
[p21](https://tensor.khalilli.ai/blog/floating-point/#proof-p21-fma-one-rounding) 1 rounding against 2;
[p22](https://tensor.khalilli.ai/blog/floating-point/#proof-p22-exact-error) the error, caught exactly;
[p23](https://tensor.khalilli.ai/blog/floating-point/#proof-p23-double-rounding) rounding twice.
On paper: how many doublings separate E5M2's floor from its
ceiling, and would the p1 gradients fit without a scale? And
before running p16: derive float16's ceiling from E=5, M=10
yourself, the way the binary32 section taught you.

[1] Khalilli, *proof scripts for this page, measured on an Apple
M3 Max, torch 2.11.0, CPU*, 2026. Linked in place above; rerun them
to check me.

[2] Sanglard, *Floating Point Visually Explained*, 2017. The
interval-and-position way of seeing the fields comes from here.
[https://fabiensanglard.net/floating_point_visually_explained/](https://fabiensanglard.net/floating_point_visually_explained/)

[3] IEEE, *754-2019: Standard for Floating-Point Arithmetic*
(first edition 1985).

[4] Goldberg, *What Every Computer Scientist Should Know About
Floating-Point Arithmetic*, ACM Computing Surveys, 1991; and Muller
et al., *Handbook of Floating-Point Arithmetic*, 2nd ed., 2018.

[5] Micikevicius et al., *Mixed Precision Training*, ICLR 2018.
[https://arxiv.org/abs/1710.03740](https://arxiv.org/abs/1710.03740)

[6] Micikevicius et al., *FP8 Formats for Deep Learning*, 2022.
[https://arxiv.org/abs/2209.05433](https://arxiv.org/abs/2209.05433)

[7] DeepSeek-AI, *DeepSeek-V3 Technical Report*, 2024.
[https://arxiv.org/abs/2412.19437](https://arxiv.org/abs/2412.19437)

[8] Dettmers et al., *LLM.int8(): 8-bit Matrix Multiplication
for Transformers at Scale*, 2022 (the emergent-outlier measurement).
[https://arxiv.org/abs/2208.07339](https://arxiv.org/abs/2208.07339)

[9] Open Compute Project, *Microscaling Formats (MX)
Specification v1.0*, 2023; and Rouhani et al., *Microscaling Data
Formats for Deep Learning*, 2023.
[https://arxiv.org/abs/2310.10537](https://arxiv.org/abs/2310.10537)

[10] NVIDIA, *Pretraining Large Language Models with NVFP4*,
2025. [https://arxiv.org/abs/2509.25149](https://arxiv.org/abs/2509.25149)

[11] Mishra et al., *Recipes for Pre-training LLMs with MXFP8*,
2025 (the scale-rounding result).
[https://arxiv.org/abs/2506.08027](https://arxiv.org/abs/2506.08027)

[12] Tseng et al., *Training LLMs with MXFP4*, 2025.
[https://arxiv.org/abs/2502.20586](https://arxiv.org/abs/2502.20586)

[13] Dettmers et al., *QLoRA: Efficient Finetuning of Quantized
LLMs*, 2023 (NF4). [https://arxiv.org/abs/2305.14314](https://arxiv.org/abs/2305.14314)

[14] Frantar et al., *GPTQ*, 2022.
[https://arxiv.org/abs/2210.17323](https://arxiv.org/abs/2210.17323)

[15] Lin et al., *AWQ: Activation-aware Weight Quantization*,
2023. [https://arxiv.org/abs/2306.00978](https://arxiv.org/abs/2306.00978)

[16] Dettmers et al., *8-bit Optimizers via Block-wise
Quantization*, 2021.
[https://arxiv.org/abs/2110.02861](https://arxiv.org/abs/2110.02861)

[17] Gustafson and Yonemoto, *Beating Floating Point at its Own
Game: Posit Arithmetic*, 2017; logarithmic number systems survey in
Muller et al. [4].

[18] Severance, *An Interview with the Old Man of
Floating-Point* (William Kahan on IEEE 754 and the Intel 8087), 1998.
[https://people.eecs.berkeley.edu/~wkahan/ieee754status/754story.html](https://people.eecs.berkeley.edu/~wkahan/ieee754status/754story.html)

[19] NVIDIA, *CUDA C++ Programming Guide*, the arithmetic
instructions throughput table (per-architecture float64 rates).
[https://docs.nvidia.com/cuda/cuda-c-programming-guide/](https://docs.nvidia.com/cuda/cuda-c-programming-guide/)

[20] Kharya, *TensorFloat-32 in the A100 GPU Accelerates AI
Training, HPC up to 20x*, NVIDIA blog, 2020.
[https://blogs.nvidia.com/blog/tensorfloat-32-precision-format/](https://blogs.nvidia.com/blog/tensorfloat-32-precision-format/)

[21] PyTorch documentation, *CUDA semantics* (the TF32 flags and
their defaults since 1.12).
[https://docs.pytorch.org/docs/stable/notes/cuda.html](https://docs.pytorch.org/docs/stable/notes/cuda.html)

[22] Industrial Light & Magic, *About OpenEXR* (the half type,
2000-2003, and its Cg compatibility); and Bogart, Kainz, Hess, *The
OpenEXR Image File Format*, GPU Gems, 2004.
[https://openexr.com/en/latest/about.html](https://openexr.com/en/latest/about.html)

[23] Wang and Kanwar, *BFloat16: The secret to high performance
on Cloud TPUs*, Google Cloud blog, 2019.
[https://cloud.google.com/blog/products/ai-machine-learning/bfloat16-the-secret-to-high-performance-on-cloud-tpus](https://cloud.google.com/blog/products/ai-machine-learning/bfloat16-the-secret-to-high-performance-on-cloud-tpus)

[24] Ku, Poli et al. (Radical Numerics), *NVFP4 pretraining:
from theory to implementation, Part 1*, 2026. The recipe
walk-through this page's NVFP4 worked block follows.
[https://www.radicalnumerics.ai/blog/nvfp4-part1](https://www.radicalnumerics.ai/blog/nvfp4-part1)

[25] Easygoing, *Which is Better: FP8_scaled or MXFP8? A
Thorough Comparison of Image Generation AI Model Accuracy and
Speed*, AI Image Journey, 2026. The measured quality ranking and
the measured slowdown of MXFP8 on hardware without MX support.
[https://note.com/ai_image_journey/n/n99d0ed2f1c1d](https://note.com/ai_image_journey/n/n99d0ed2f1c1d)

[26] ZeroEntropy, *MXFP4* (concepts), 2026. The per-vendor
native-support summary.
[https://zeroentropy.dev/concepts/mxfp4/](https://zeroentropy.dev/concepts/mxfp4/)

[27] Boehm, *Can Function Inlining Affect Floating Point
Outputs? Exploring FMA and Other Consistency Issues*, 2023.
[https://siboehm.com/articles/23/Inlining-FMA-FP-consistency](https://siboehm.com/articles/23/Inlining-FMA-FP-consistency)

[28] PyTorch documentation, *Numerical accuracy*.
[https://docs.pytorch.org/docs/stable/notes/numerical_accuracy.html](https://docs.pytorch.org/docs/stable/notes/numerical_accuracy.html)

[29] Kwon et al., *Efficient Memory Management for Large Language
Model Serving with PagedAttention* (the vLLM paper), 2023.
[https://arxiv.org/abs/2309.06180](https://arxiv.org/abs/2309.06180)

[30] NVIDIA, *H100 Tensor Core GPU* specifications (HBM3 bandwidth
and PCIe generation 5 rates).
[https://www.nvidia.com/en-us/data-center/h100/](https://www.nvidia.com/en-us/data-center/h100/)

[31] He and Thinking Machines Lab, *Defeating Nondeterminism in
LLM Inference*, Connectionism, 2025. The 1,000-completions
measurement and the batch-invariant kernels.
[https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/](https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/)

Three good things to read after this page: Goldberg's paper [4] slowly, with the map in hand; the OCP MX specification [9], which is short and readable; and Kahan's interview on the making of IEEE 754 [18], where the standard's designer tells the story himself.
