{"slug": "floating-point", "title": "Floating Point", "summary": "A technical explainer from tensor.khalilli.ai demonstrates that the floating-point arithmetic error causing 0.1 + 0.2 to equal 0.30000000000000004 and training loss to become NaN at step 40,000 stems from the finite number of bits used to store numbers, with measurements taken on an Apple M3 Max using torch 2.11.0. The article explains how equal spacing fails for neural network values spanning from 0.00000037 to 10.5, requiring a format where spacing changes with magnitude, and introduces binary place values as a foundation for understanding formats like MXFP4, E2M1, block 32, and E8M0 scale.", "body_md": "Run this anywhere:\n\n```\n>>> 0.1 + 0.2\n0.30000000000000004\n>>> 0.1 + 0.2 == 0.3\nFalse\n```\n\nAnd in real training runs, this happens:\n\n```\nstep 39,998   loss 2.41\nstep 39,999   loss 2.40\nstep 40,000   loss nan\n```\n\nBoth outputs have the same cause: the way computers store numbers.\nBy the end of this page you will be able to predict both, explain\nthem to someone else, and read a line like \"MXFP4, E2M1, block 32,\nE8M0 scale\" part by part. No prior knowledge is assumed. Every measured number on this page comes\nfrom a small script you can run, linked where the number appears,\nmeasured on an Apple M3 Max with torch 2.11.0 [[1](https://tensor.khalilli.ai/blog/floating-point/#ref-1)].\n\nA 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.\n\nFirst, 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:\n\nThe 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:\n\nThose 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:\n\n3 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:\n\nRead 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.\n\nOne 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:\n\nRead 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.\n\nOne 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:\n\nDecimal plays the same game with different equipment. Base 10 is 2 x 5, so it digests 2s and 5s. Base 2 digests only 2s:\n\nNow 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:\n\nThis 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.\n\nWhere 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:\n\nA float makes exactly this split, in base 2. Any positive number\nis some power of 2 times a number between 1 and 2: 10.5 is\n1.3125 x 2^3 (check it: 2^3 = 8, and 1.3125 x 8 = 10.5), and\n0.00000037 is about 1.552 x 2^-22. The power of 2 goes into one\nfield, named the **exponent**. The 1.something goes into the\nother field, named the **mantissa**. The 2 words are only names\nfor the 2 halves of the split: the exponent is the number's\nsize, the mantissa is its digits.\n\nWhat does each field buy on the number line? Draw them [[2](https://tensor.khalilli.ai/blog/floating-point/#ref-2)]:\n\nWhere 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.\n\nNow 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:\n\nIn 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:\n\nTwo details in that card deserve their own sentences, because\nevery format on this page inherits both. First, the decode row\nsays \"1 + 0.525\", but the mantissa field only stored the .525.\nThe leading 1 costs nothing: every number in a power-of-two\ninterval is 1.something times a power of two, so the format does\nnot store the 1 and gets 24 bits of position for the price of 23.\n(The subnormals, coming 2 sections from now, are 0.something\nand are the one exception.) Second, the **bias**. Here is the problem it solves. The\nexponent field is a box of 256 slots, numbered 0 to 255. Slot\nnumbers cannot be negative. But the exponents we need to store\nrun from -126 to 127: tiny numbers need negative ones. So the\nnumbers we have do not fit the labels the box has. The fix is\none move: slide every exponent up by the same constant, 127,\npicked because it is the middle of 0 to 255. After the slide,\n-126 sits in slot 1, -22 sits in slot 105, 0 sits in slot 127,\n2 sits in slot 129, and 127 sits in slot 254. Nothing is\nreordered and nothing is lost; the whole line moved as one\npiece:\n\nThe 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.)\n\nThe 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.\n\nThe unsigned choice has a quiet payoff. A bigger float always\ncarries a bigger bit pattern, so a chip can compare 2 floats\nwith the integer circuits it already owns [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)].\n\nOne 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.\n\nNotice what the last decode row admits: the machine does not\nstore 6.1. It cannot. 6.1 is 61/10, and the 10 carries a 5.\nThe fractions figure showed what a 5 does in base 2: the bits\nrepeat forever. The mantissa keeps 23 of them and cuts the\nrest. What remains decodes to 6.0999999..., the nearest\nfloat32. The gap is the rounding error. The sections ahead\nmeasure that gap and follow its consequences. This layout, the\nbias 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\nformat your `float32`\n\ntensors use today, unchanged.\n\nYou 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:\n\nRead 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.\n\nNow 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.\n\nThe 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.\n\nNext, 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:\n\nRead the 2 orange bars in the drawing: identical. Here is why,\nin one chain. An interval runs from its start to twice its\nstart, so its width equals its start. 8 steps cut that width, so\n1 step is the start divided by 8, and the worst miss, half a\nstep, is the start divided by 16. Take the share: divide by the\nstart, and the start cancels. 1/16, at every size. Now the 2\nwords this subject uses. **Absolute error** is the size of the\nmiss; it doubles from interval to interval. **Relative error**\nis the share; a float pins it at the same value everywhere.\nEvery float format works this way.\n\nThe local step also has a proper name, and you will meet it in\nevery numerics discussion: the **ulp**, the unit in the last\nplace [[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)]. \"The ulp at x\" means the step between neighboring\npoints where x lives. E4M3's ulp at 1.0 is 0.125; its ulp at 300\nis 32. One number is enough to know a format's whole precision: the\nulp at 1.0. Every other interval's step is that same number,\ndoubled or halved some number of times. So if you know the step\nin [1, 2), you know the step everywhere. The field guide ahead\nprints it on every card, as \"step at 1.0\".\n\nThe 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.\n\nFour places on the map need special handling, and every format must decide all four.\n\n**Zero.** The position field always means \"1.something\", so no\npattern naturally means zero. The all-zeros pattern is simply\nassigned the value zero, as a special case. The sign bit still exists, so there is\na +0 and a -0, and they compare equal.\n\n**The ramp.** Just above zero there is trouble. Every normal\npoint is 1.something x a power of 2. The exponent has a lowest\npower, so there is a smallest normal point. In E4M3 the lowest\nnormal exponent value is 1 and the bias is 7, so the lowest power is\n1 - 7 = -6, and the smallest normal point is 1.0 x 2^-6 =\n0.015625. Below it, a naive format has nothing until 0. A\ncliff. The fix: in the lowest interval only, drop the hidden 1.\nRead the mantissa as 0.something instead of 1.something. Walk\nthrough what that gives. The pattern 000 reads 0.000 x 2^-6 =\n0: zero itself. The pattern 001 reads 0.001 x 2^-6, which is\n1/8 x 0.0156 = 0.00195. The pattern 010 gives 0.0039. And so on\nup to 111, which gives 0.0137. 7 evenly spaced points now fill\nthe gap between 0 and 0.0156. These points are the\n**subnormals**. They turn the cliff into a ramp:\n\n**Infinity.** When a result overflows the ceiling, float32 returns\na dedicated pattern called `inf`\n\n, which then behaves lawfully:\nanything finite divided by `inf`\n\nis 0, and `inf - inf`\n\nis the next\nspecial value. E4M3 made a harder choice: it has no infinity at\nall. Those patterns were used for one more doubling of range,\nand overflow stops at 448 instead. Keep that choice in mind; it\nis the first sign of how little room 8 bits leave.\n\n**NaN.** Not a Number is the pattern returned when no answer is\ndefensible: 0/0, inf - inf, the square root of -1. It has one\ndeliberately strange law: `nan == nan`\n\nis false, so `x != x`\n\nis\nthe honest test for it. When your loss prints `nan`\n\n, this value\nis what you are looking at, and it arrived through an overflow or\nan undefined step somewhere upstream. We will catch it in the act\nin a few sections.\n\nThe formats in this guide carry nearly all of the world's\nfloating-point arithmetic, and you now own every idea needed to\nread them. Each gets the same treatment: its card, the bit\ncounts and the numbers that follow from them; its map, where\nits points land; where it came from; where it runs today; and\nhow it fails. One promise about the cards: none of their numbers is new\ninformation. Give the 2 bit counts, E and M, and every row\nfollows by rules you already have. The bias is the middle of the\nE-bit range. The lowest and highest powers come from sliding the\nstored range back, so the ceiling and the floors follow. The\nstep at 1.0 is 1 divided by 2^M. The digits count follows from\nthe step. The brackets on each card point each row at the field\nthat sets it, and the proof computes every row from E and M and\nchecks it against torch's own tables\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) runs the check). The guide\ncovers the 4 wide formats, the ones that stand alone. The 8-,\n6- and 4-bit formats enter later, each at the exact place in\nthe training story that needs it, and a closing sheet gathers\nthe whole family on one drawing.\n\nDouble binary32's byte count and you get the format the 1985 standard called double precision:\n\nThe 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:\n\nWhere it came from: IEEE 754 began as the arithmetic William\nKahan designed with Intel for the 8087, the floating-point\ncoprocessor sold beside the 8086; the standards committee turned\nthat 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\nits wide grade, sized so ordinary science could chain millions\nof operations and still trust the leading digits.\n\nWhere 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.\n\nIts proof: [p16](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) derives the whole card\nfrom E and M and checks every row against `torch.finfo`\n\n. One\nextra line in it runs a vanishing add at this format's scale:\nin float64, (1e16 + 1) - 1e16 = 0. The ulp at 1e16 is 2, and\nthe added 1 fell under half of it, so the sum rounded straight\nback (the failure section ahead names this absorption). 16\ndigits push the cliff out of sight; no digit count removes it.\n\nHow it fails in a training loop: float64 rarely gives training\na wrong answer. Its problem is cost. 8 bytes per number is 2\ntimes float32 and 4 times bfloat16, spent on digits training\ncannot use: the sections ahead show training running where the\nstep at 1.0 is 0.0078, so digits 4 through 16 add nothing. On\nGPUs there is a second cost: most chips carry few float64 units,\nand NVIDIA's own throughput tables list float64 operations far\nbelow float32 on most of its hardware [[19](https://tensor.khalilli.ai/blog/floating-point/#ref-19)]. torch's default is\nfloat32, and float64 appears only when you ask\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) prints the default).\n\nbinary32 you have already decoded by hand; the card collects its numbers:\n\n254 intervals from 1.2e-38 to 3.4e38, 8,388,608 points in each, about 7 digits:\n\nWhere it came from: the same 1985 standard's single precision [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)].\nWhere it runs today: it is the default dtype of torch;\n`torch.tensor(1.0)`\n\nis a float32 unless you say otherwise\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) prints the default from this\ninstall). Even when matmuls run narrow, the master weights of\nmixed-precision training stay float32 and the tensor cores\naccumulate in float32, so the training half of this page\nkeeps this format at its center.\n\nNow the format hiding inside it: TensorFloat-32. Since the A100,\nNVIDIA's tensor cores can take an ordinary float32 matmul, round\neach factor to 10 mantissa bits, multiply, and accumulate in\nfull float32 [[20](https://tensor.khalilli.ai/blog/floating-point/#ref-20)]. The tensors going in and out are ordinary\nfloat32; only the multiply is narrow, reading 19 of the 32 bits\n(1 + 8 + 10). TF32 is that mode, not a storage type you can give\na tensor. NVIDIA measured large speedups at matching accuracy on\ndeep-learning workloads [[20](https://tensor.khalilli.ai/blog/floating-point/#ref-20)]; for code that needs all seven\ndigits it is a silent cut, and that is why torch ships with\nmatmul TF32 off since version 1.12 [[21](https://tensor.khalilli.ai/blog/floating-point/#ref-21)]. On this install,\n`torch.backends.cuda.matmul.allow_tf32`\n\nis False, the cudnn\nconvolution flag is True, and\n`torch.set_float32_matmul_precision(\"high\")`\n\nis the one-line\nopt-in ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) prints all three). None\nof this can execute on the Apple machine this page is measured\non; 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)].\n\nHow 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.\n\nCut 32 bits in half and something must go. There are two ways to choose, and the choice split the 16-bit world in two:\n\nfloat16 is the left fork: keep digits, pay with reach.\n\n5 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:\n\nWhere it came from: film and games, not a numerics committee.\nNVIDIA and Microsoft made `half`\n\na type in the Cg shading\nlanguage in 2002, and Industrial Light & Magic built its OpenEXR\nfilm format on the same 1+5+10 layout, in production from 2000\nand released as open source in 2003 [[22](https://tensor.khalilli.ai/blog/floating-point/#ref-22)]. IEEE 754 adopted the\nlayout as binary16 in its 2008 revision [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)]. The film industry\nshipped the format years before the standard named it.\n\nWhere it runs today: graphics APIs and image pipelines still,\ninference engines, and mixed-precision training on hardware from\nbefore bfloat16 spread. In torch it is the half in `.half()`\n\n.\n\nIts proof and its failure are the same three numbers. The\nceiling: 60000 times 1.2 is inf in float16\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p7-sixteen-bits)). The digits: 1.001 stores as\n1.000977. The floor: normal numbers end at 6.1e-5, and the small\nhalf of a gradient histogram lives below that, which is why the\nloss-scaling section exists. When float16 dies in a run, it dies\nat one of these three numbers.\n\nThe right fork: keep reach, pay with digits. Google's brain float keeps all 8 of float32's exponent bits and 7 of mantissa:\n\nThe 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.\n\nWhere it came from: Google built it into the TPU's matrix\nunits, which multiply in bfloat16 and accumulate in float32, and\nchose the layout so that range would never be the problem and\nconversion from float32 would be nearly free [[23](https://tensor.khalilli.ai/blog/floating-point/#ref-23)]. The name is\nBrain Floating Point, after the Google Brain team.\n\nNearly free is provable, and on this machine it is proven at the\nbit level: bfloat16 is float32's top half. Take any of the\n65,536 possible bfloat16 bit patterns, cast it to float32, and\nthe result is the same 16 bits with 16 zeros appended;\n[proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p17-bf16-truncation) checks all 65,536, bit for\nbit. The downward cast is the same move with rounding: keep the top\n16 bits, round by the low 16. The proof checks\ntorch's cast against that hand rule on 1,000,010 values (a\nmillion random ones plus the edge cases), and they agree on\nevery one. Here is 1.7014 making the trip:\n\n```\nfloat32:  0 01111111 1011001 1100011101111010\nbfloat16: 0 01111111 1011010   (the top half, rounded up by the rest)\nstored:   1.703125\n```\n\nWhere it runs today: TPUs since their second generation [[23](https://tensor.khalilli.ai/blog/floating-point/#ref-23)],\nNVIDIA GPUs since the A100 [[20](https://tensor.khalilli.ai/blog/floating-point/#ref-20)], and this machine: torch's CPU\nautocast picks bfloat16 by default\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) prints it). In large-model\ntraining it is the usual compute half of the mixed-precision\nloop, with FP8 taking over the matmuls on the newest chips (the\nFP8 section returns to this).\n\nHow it fails: 2 digits. 1.001 stores as exactly 1.0\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p7-sixteen-bits)). The step at 1.0 is 0.0078, so\na healthy update of 0.001 sits under half a step and rounds\naway: the disaster that opens the training sections ahead, and\nthe reason master weights exist. bfloat16 did not make training precise. It\nmade training's failures the quiet, repairable kind, and the\nmachinery that repairs them is most of the rest of this page.\n\nThe 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.\n\nBetween any 2 neighboring points of the map lies everything the format cannot\nsay. When a result lands there, one rule decides its fate: go to\nthe nearer point. When it lands exactly halfway, there is a rule for the tie: go\nto the point whose last mantissa bit is 0. Half of the halfway\ncases end up rounding up and half down, so a long sum does not\nslowly lean one way.\nThis pair of rules is round-to-nearest, ties-to-even, the default\nof every machine you will touch [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)]:\n\nThe worst this rule can do is move a value by half the local\nstep: half an ulp. For float32 the ulp at 1.0 is 2 to the power -23, which is\nabout 1.2e-7 (read e-7 as: move the decimal point 7 places left,\nso 0.00000012), and that number is called the machine epsilon\n(`torch.finfo(torch.float32).eps`\n\nprints it). So every operation lands\nwithin about 6e-8 of the true result, measured relative to the\nresult's size. Both numbers read straight\noff the map: find the interval that starts at 1, take its step, halve\nit. The standard error analysis of floating point is this one\nbound, applied once per operation [[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)].\n\nHow 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:\n\nThis 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:\n\n1/3 does the same thing in decimal, and for the same reason: the\ndenominator has a prime factor the base does not. Ten is 2 times\n5, so 0.1 is 1/(2 times 5), and base two has no 5. The format\ncuts the repetition at its mantissa width and stores the nearest\nrepresentable number, which for float64 is exactly\n0.1000000000000000055511151231257827... ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p0-riddles)\nprints every digit). The same happens to 0.2 and to 0.3, and the\nsum of the two stored numbers is not the stored number nearest\n0.3.\nNothing malfunctioned. 3 numbers you typed do not exist,\nand the printed digits show exactly which nearby numbers were\nstored instead.\n\nOne more piece of standard equipment: the fused multiply-add, 1\ninstruction computing a times b plus c with a single rounding at\nthe end instead of 2. The result is as if the product had been\ncomputed exactly and only the final sum were rounded, and on\nmodern chips it costs about as much as 1 multiply [[27](https://tensor.khalilli.ai/blog/floating-point/#ref-27)]. The\ndifference is not small print. Choose a = b = 1 + 2^-27 and\nc = -(1 + 2^-26): the separate path rounds the product first and\nreturns exactly 0, while the fused path returns the true answer,\n2^-54 ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p21-fma-one-rounding) runs both on this\nmachine). Every tensor core in the training half of this page is\na lattice of these fused units, and the sums they accumulate\nstay in float32 even when the products arrive in 8 bits. Keep\nthat sentence; it is why low-precision training is possible at\nall.\n\nThe unit itself is worth one look, because the training half of this page keeps returning to it:\n\nThree rules of ordinary arithmetic stop holding once every result is rounded to a point, and all three cause real bugs.\n\n**Absorption.** At 100,000,000, which is 1e8, float32's step is 8, so\nadding 1 offers a move smaller than half the gap, and the sum\nrounds straight back ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p4-absorb-cancel)):\n\n**Cancellation.** Subtract two nearly equal numbers and their\nshared front digits erase each other. Whatever rounding noise\nthe inputs carried is suddenly the front of the answer:\n\nThe proof above subtracts 1.0000001 from 1.0 in float32 and gets\n1.19e-7 where the truth is 1.0e-7: a 19% error from one\nsubtraction of two almost-exact inputs. The strange part: the\nsubtraction itself commits no error at all. When 2 numbers are\nwithin a factor of 2 of each other, their difference is always\nexactly representable (Sterbenz's lemma [[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)];\n[proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p22-exact-error) checks it on this very case).\nCancellation never creates error; it exposes the error the\ninputs already carried.\n\n**Order.** Because every addition rounds, (a + b) + c and\na + (b + c) are different numbers. The smallest possible\nexample, drawn:\n\nSum 50,000 values\nforward and backward and the answers disagree\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p5-sum-order)); a parallel machine that splits the\nsame sum across cores picks yet another order, and another answer.\ntorch's own `.sum()`\n\nadds pairwise in a tree, which is both\nfaster and about 130 times closer to the true sum than a\none-by-one loop (measured: 0.24 error against 0.0018), and compensated summation (Kahan's trick of carrying\nthe rounding error in a second variable) closes most of the rest\n[[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)].\n\nOne 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:\n\nRun it on this page's opening example and the 3 operations\nhand back the exact rounding error of 0.1 + 0.2\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p22-exact-error) checks it against exact rational\narithmetic). A fused multiply-add does the same for a product in\n1 instruction. Kahan's compensated sum is this trick run in a\nloop, and the double-word arithmetic that stretches precision in\nsoftware is this trick kept instead of thrown away [[4](https://tensor.khalilli.ai/blog/floating-point/#ref-4)].\n\nKeep 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.\n\nOpen any model's source code and small constants appear: an\n`eps=1e-5`\n\nin the layer norm, an `eps=1e-8`\n\nin Adam, a\n`logits - logits.max()`\n\nin every attention implementation. None of them is superstition; each one is a patch over a place\nwhere a formula's intermediate value leaves the map.\n\nThe largest number float32 can hold is about 3.4e38, and exp\nreaches it at 88.73 (measured below). Softmax, the function that\nturns a model's raw output scores, its logits, into\nprobabilities, applies exp to every logit. Logits above 88.73\nexist in every large model, so a naive softmax returns inf, then\ninf divided by inf, and your loss prints nan. The repair\ncosts nothing: softmax only sees differences, so subtract the\nmaximum first and the largest input becomes 0\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p6-stability)):\n\nThe same pattern explains the rest of the family, one line\neach. `logsumexp`\n\nslides by the max, for the same reason softmax\ndoes. `log1p(x)`\n\nand `expm1(x)`\n\nexist because near zero, 1 + x\nabsorbs the x: the absorption figure again, happening at 1.0\ninstead of 1e8. The layer-norm epsilon keeps a near-zero\nvariance from turning `rsqrt`\n\ninto inf. Adam's epsilon guards\nits denominator the same way. And an attention mask uses a large\nnegative number instead of -inf, so that after the slide it\nbecomes a clean 0 instead of a nan. One picture, many patches:\nkeep every in-between value where the points are.\n\nThe guide paused at 16 bits. Here is why the family keeps going down anyway.\n\nThink 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:\n\nThe 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.\n\nAt 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:\n\nThe proof runs it: 1,000 consecutive updates of 0.001 leave a\nbfloat16 weight at exactly 1.0 ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p8-lost-update)).\nNot slowed. Stopped, silently, for that weight. This is why\nevery mixed-precision recipe since 2017 keeps a float32 master\ncopy of the weights [[5](https://tensor.khalilli.ai/blog/floating-point/#ref-5)]. The forward and backward passes run\nnarrow. But the update lands in the float32 copy, where the\nlocal step is 65,536 times finer and 0.001 fits with room to\nspare. The other repair is\nstochastic rounding, which we will need again at 4 bits: round\nup with probability proportional to how far you got. In the same\nproof, the stochastically rounded weight reaches 2.0 alongside the\nmaster copy.\n\nSo the loop that trains every model you use runs like this:\n\nThe 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.\n\nOne 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:\n\nThe rescue is one multiplication. Multiply the loss by a\nnumber S before the backward pass. Gradients are built from the\nloss by multiplications and additions, so every gradient comes\nout S times bigger, and the whole histogram slides right, into\nthe band where float16 has points. Divide by S afterward.\nNothing about the mathematics changed; the only difference is\nwhich gradients survived the trip. In practice S adjusts\nitself: see an overflow, halve S and skip that step; survive a\nstretch of clean steps, double it. That is the whole of `GradScaler`\n\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p9-loss-scaling) measures the cast; on our toy\nnet only 0.1% of gradients die, and the fraction grows with depth\nand training time, which is why the recipe exists\n[[5](https://tensor.khalilli.ai/blog/floating-point/#ref-5)]). bfloat16 mostly\nretired this machinery: it keeps float32's reach, so its normal\nfloor sits at 1.2e-38 against float16's 6.1e-5, about 34 powers\nof 10 lower, and the scaler became optional.\n\nHalve again, to 8 bits, and something new happens: no single\nsplit of the bits serves both halves of training anymore. The\nforward pass wants digits. The backward pass wants reach. So at\n8 bits the fork ships both of its arms, as a pair [[6](https://tensor.khalilli.ai/blog/floating-point/#ref-6)]: E4M3\nfor digits, E5M2 for reach.\n\nE4M3 you have known since the map section: its 15 intervals are\nthis page's teaching map, and its 256 patterns are decoded in\n[proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p2-e4m3-map). The card records its one non-IEEE\nchoice: no infinity. Those patterns were used for one more\ndoubling of range, overflow stops at 448, and a single pattern\nmeans NaN.\n\nE5M2 is float16 with the bottom byte cut, and that is a\nbit-level fact, provable the same way bfloat16 was: all 256\nE5M2 patterns cast to float16 as the same 8 bits with 8 zeros\nappended ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p20-e5m2-top-byte) checks every one).\n\nNVIDIA, Arm and Intel proposed the pair for deep learning in\n2022 [[6](https://tensor.khalilli.ai/blog/floating-point/#ref-6)], and hardware arrived with the Hopper chips. In use,\nE4M3 carries the forward pass. E5M2 carries the gradients,\nwhose thin far tails need reach more than digits. And one more\npiece rides along: every FP8 tensor gets one float32 scale of\nits own. The scale is picked from the tensor's recent largest\nvalue, so that the tensor's histogram slides into the band.\nThis is exactly the loss-scaling move, now made automatic and\napplied per tensor. Recipes differ only in when they measure\nthat largest value: from recent history (delayed scaling) or on\nthe spot (current scaling):\n\nAnd not everything is quantized. (To quantize a tensor: move\nits numbers onto a narrower format's points.) Attention's and\nthe MLP's big matmuls run in FP8. Softmax, the normalizations,\nthe first embedding and the final projection stay wide [[11](https://tensor.khalilli.ai/blog/floating-point/#ref-11)].\n\nWired into a transformer through NVIDIA's Transformer Engine,\nthe published result is training 30 to 40 percent faster at\nmatching loss curves [[6](https://tensor.khalilli.ai/blog/floating-point/#ref-6)]. DeepSeek-V3 pushed the same idea one\nstep finer: it trained a 671-billion-parameter model with FP8\nmatmuls by giving a scale to every 128-element tile instead of\nevery tensor [[7](https://tensor.khalilli.ai/blog/floating-point/#ref-7)]. Hold that thought; it is halfway to the next\nsection. And one honest caution before the next halving. E4M3\ncarries about 1.2 decimal digits. E5M2 carries about 0.9. At 8\nbits, no single stored number is trustworthy. Only averages\nover many are.\n\nBefore the next halving, meet the format waiting at the bottom, because its 2 numbers explain everything that follows. 4 bits:\n\n16 patterns. 15 values (0 appears twice, once with each sign).\nNo inf, no NaN. And one number to hold on to: the whole\npositive range is 6 / 0.5 = 12x, about 3.6 doublings. The 2023\nOCP standard defined E2M1 as the 4-bit element of the block\nfamily these sections are building [[9](https://tensor.khalilli.ai/blog/floating-point/#ref-9)], and the newest NVIDIA\nchips run it at twice the FP8 rate [[24](https://tensor.khalilli.ai/blog/floating-point/#ref-24)]. But alone it is\nunusable. Even the per-tensor scale that carried FP8 cannot\nsave it, because per-tensor scaling has one enemy. Transformer\nactivations grow a few channels whose values run hundreds of\ntimes larger than the rest, systematically, past about 6\nbillion parameters [[8](https://tensor.khalilli.ai/blog/floating-point/#ref-8)]. One scale must now serve the spike and\nthe bell at once:\n\nRead the measured numbers. FP8 with a single scale loses 1% of\nthe ordinary values next to a 500x outlier, because a float grid\nkeeps its relative error at any scale until values fall below\nits smallest positive number, and FP8's smallest is 229,376x\nunder its largest (448 / 0.00195, from the map section). FP4's\nsmallest is only 12x under its largest (6 / 0.5, computed\nabove). One spike takes the whole 12x, and\n100% of the ordinary values quantize to zero\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p10-fp4-outlier)). At 4 bits, blocks are not an\nimprovement; nothing works without them.\n\nSo the scale moved into the data type. The Open Compute Project's\nMX formats, standardized in 2023 by AMD, Arm, Intel, Meta,\nMicrosoft, NVIDIA and Qualcomm [[9](https://tensor.khalilli.ai/blog/floating-point/#ref-9)], cut every tensor\ninto blocks of 32 and give each block one 8-bit scale:\n\nEvery idea on this page meets here, so take it slowly. The\nscale byte is an E8M0: a float with no mantissa at all. It is\nthe interval-picker from the floating-dot figure with the\nposition deleted, so applying it is just exponent addition,\nnearly free in silicon. The block of 32 is the histogram idea\ncut fine: an outlier can now only hurt its own 31 neighbors.\nThe price is small. 32 x 4 + 8 = 136 bits for 32 numbers, 4.25\nbits each; 136 / 128 = 1.06, so 6% over raw 4-bit storage. Even the block size is a\nmeasured compromise: smaller blocks fit better but spend more\nbits on scales, and our own sweep across sizes 8, 16, 32, 64\nand 128 shows both effects moving\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p11-mxblock)). The same block layout carries\n8-, 6- and 4-bit elements, named MXFP8, MXFP6, MXFP4.\n\nWhat the tensor core does with 2 of these blocks is 1 more drawing:\n\nThe block layout also changed who does what inside FP8. With a\nscale for every 32 values, one block rarely spans more than\nE4M3's 18 doublings. So E4M3, which has 8 points per doubling\nwhere E5M2 has 4, measured better for the gradients too. The\nlargest published MXFP8 pretraining run, 8 billion parameters\non 15 trillion tokens, quantizes every tensor as E4M3 [[11](https://tensor.khalilli.ai/blog/floating-point/#ref-11)].\n\nThe 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.\n\nTheir maps are small enough to draw complete: 3 intervals of 8\npoints, 7 intervals of 4. Blackwell-class chips execute them at\nfull 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\nthem yet. A format survives by having a job nobody else does.\nFP8 is safer, FP4 is faster on paper, and FP6 is still looking\nfor its job.\n\nNVIDIA's NVFP4 variant tightens it further: 16-element blocks, a\nfractional E4M3 scale instead of a power-of-two one, plus one\nfloat32 scale per tensor, buying a finer fit for a quarter bit\nmore [[10](https://tensor.khalilli.ai/blog/floating-point/#ref-10)]. The bytes carry the quarter bit, and the deeper\ndifference sits in the scale byte itself:\n\nHere is one NVFP4 block quantized by hand, every scale shown:\n\nThe largest of the 16 values, 15.011, sets the scales and comes\nback exactly; 0.25 and 0.5 fall below half of the smallest step\nat that scale and come back as 0; 3.2002 comes back as 3.7528,\nabout 17% off ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p19-nvfp4-worked) runs every\nnumber). A single 4-bit value is coarse. The bet of 4-bit\narithmetic is that billions of such errors, kept unbiased,\ncancel on average.\n\nOne 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:\n\nNVIDIA measured the difference as the gap between divergence and\nparity 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)\nshows the mechanism in 2 lines.\n\nThe whole scaling story now fits on one ladder, and every recipe of the last 5 sections is 1 rung of it:\n\nThe 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:\n\nThe first is stochastic rounding, the lost-update repair now\ndoing the main work. At 4 bits nearly every update is below half the\nlocal step, so deterministic rounding freezes everything; rounding\nup with probability proportional to progress is unbiased in\nexpectation. Our proof runs 10,000 micro-updates: nearest\nrounds to a frozen 1.0, stochastic reaches a mean of 3.0113\nagainst a true 3.0, noisy per run and honest on average\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p12-sr-drift)).\n\nThe 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:\n\nOur 64-value proof takes the max-to-mean ratio from 11.8 to 2.6\n([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p13-hadamard)). The third is selective precision:\nthe first and last layers, where quantization error is measured\nto hurt most, stay in higher precision. With all three plus\nNVFP4, a 12-billion-parameter model has been pretrained on 10\ntrillion tokens at nearly the same loss as its FP8 twin\n[[10](https://tensor.khalilli.ai/blog/floating-point/#ref-10)]; MXFP4 needs more tokens for the same loss\n[[12](https://tensor.khalilli.ai/blog/floating-point/#ref-12)]. Where each rescue is applied is itself measured engineering\n[[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\nthe forward pass it would add noise for nothing, so weights and\nactivations round to nearest. The Hadamard rotation runs only\non the inputs of the weight-gradient matmul. And weights are\nscaled in 16 x 16 squares rather than 1 x 16 rows, for a reason\nworth drawing:\n\nA row-scaled weight matrix and its column-scaled transpose are 2\ndifferent quantized matrices, and the forward and backward\npasses must see the same weights. The last roughly 15% of\nlayers stay in higher precision, and if a gap to the wide\nbaseline remains at the end, switching the forward pass to\nhigher precision for the final phase of training closes most\nof 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\nrecipes as versioned, not settled.\n\nInference 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:\n\nINT4 spaces its points equally, so it wastes points where\nweights are rare. FP4 doubles its steps and crowds zero. NF4,\nfrom the QLoRA work, places its 16 values at the quantiles of a\ngaussian, which is approximately what trained weights are, and\nit 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\nthese grids grew the post-training toolkit, one idea per tool.\nGPTQ rounds each weight so as to compensate the error left by\nthe weights before it [[14](https://tensor.khalilli.ai/blog/floating-point/#ref-14)]. AWQ rescales channels so the few\nweights that matter most land on finer points [[15](https://tensor.khalilli.ai/blog/floating-point/#ref-15)]. And the\nsame how-many-bits question now runs through the KV cache, and\neven the optimizer states: an 8-bit Adam keeps its moments in\nblocks with per-block scales [[16](https://tensor.khalilli.ai/blog/floating-point/#ref-16)], the MX idea arriving from\nthe other direction.\n\nEvery format on this page has now been met. Gather them:\n\nRead 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.\n\nAll of it, on one shelf:\n\nThese panels are the honest reason this whole family of\nformats exists. A\n7-billion-parameter model is 28 GB of float32 or 3.7 GB of MXFP4\nwith its scales; a training run holds 8x its bf16 weights; and\nin serving, the KV cache decides how many conversations one GPU\ncarries. gpt-oss ships its 120-billion-parameter\nmixture-of-experts in MXFP4 so it fits a single 80 GB GPU\n[[9](https://tensor.khalilli.ai/blog/floating-point/#ref-9)].\n\nServing adds a loop of its own. A chat model generates 1 token\nat a time, and every token's step reads all the weights and the\nwhole KV cache, the stored attention keys and values of every\ntoken so far; serving engines keep that cache in fixed-size\npages so many conversations can grow side by side without\nfragmenting memory [[29](https://tensor.khalilli.ai/blog/floating-point/#ref-29)]. That loop, end to end:\n\nThroughput follows the halvings only where the hardware speaks\nthe format natively. On a GPU that must unpack a narrow format\nin software, the format saves memory but not time. That warning\nis measurable. One published comparison ran the same\nimage-generation model on a GPU with no MXFP8 hardware. The\nMXFP8 file was smaller. Generation was slower than bfloat16:\n112.8 seconds against 96.0, because every block had to be\nunpacked on the way in [[25](https://tensor.khalilli.ai/blog/floating-point/#ref-25)]. The same tests also ranked output\nquality: FP32 first, then FP16 and BF16 about equal, then\nMXFP8, then per-tensor-scaled FP8, then NVFP4, then plain FP8\nlast [[25](https://tensor.khalilli.ai/blog/floating-point/#ref-25)]. Size and quality move together, and a scale at any\ngranularity beats no scale. Who\nspeaks what natively today: NVIDIA's Blackwell chips run\nMXFP8/6/4 and NVFP4 in their tensor cores [[24](https://tensor.khalilli.ai/blog/floating-point/#ref-24)]; AMD's newest\nInstinct chips run the MX family [[26](https://tensor.khalilli.ai/blog/floating-point/#ref-26)]; Hopper GPUs and\nTrainium2 can store MX blocks but unpack them for the math [[26](https://tensor.khalilli.ai/blog/floating-point/#ref-26)].\nThe same facts, drawn so a spec sheet can be checked against\nthem:\n\nAnd what a native format buys, next to the measured price of one the hardware cannot speak:\n\nRead a spec sheet's format row before believing its speedup column.\n\nTwo runs of the same training script, same seeds, same data,\non two different GPUs, will not match bit for bit. You now own\nevery reason. Sums land in different orders on different core\ncounts. Tensor cores round differently than the unfused loop.\nTF32 trims mantissas silently on one machine and not another. And there is a quieter actor: the compiler. IEEE\n754 fixes each instruction's bits, but no language promises\nwhich instructions your line becomes. A compiler may legally\nfuse a * b + c into 1 fused multiply-add (1 rounding) or keep it\nas 2 instructions (2 roundings), and the choice can flip with an\noptimization flag or with something as small as inlining a\nfunction [[27](https://tensor.khalilli.ai/blog/floating-point/#ref-27)]; the rounding section's proof showed the 2 paths\ndisagreeing. torch.compile is a compiler too: its fused kernels\nare allowed to round differently than eager mode [[28](https://tensor.khalilli.ai/blog/floating-point/#ref-28)]. And\nrounding in 2 hops is not rounding once: take an exact value to\nfloat64 first and then to float32, and you can land on a\ndifferent number than going straight to float32, because the\nfirst rounding can create a tie the second one breaks the other\nway ([proof](https://tensor.khalilli.ai/blog/floating-point/#proof-p23-double-rounding) builds one). This\ndouble rounding is why the old x87 unit, which computed in\n80-bit registers and rounded again on every store to memory,\nmade 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.\n\nThe same physics follows a model into serving. At temperature 0\nan LLM should answer one prompt one way, and public endpoints do\nnot: one measured run asked a model the same question 1,000\ntimes and got 80 different answers [[31](https://tensor.khalilli.ai/blog/floating-point/#ref-31)]. The cause is not which\nGPU core finishes first; each kernel by itself returns the same\nbits every run. The cause is the batch. A server computes your\nrequest together with whoever else arrived, its kernels choose\ntheir splits, and so their addition order, by batch size, and a\ndifferent order means different last bits. Other people's\ntraffic changes your logits, and at some token the changed bits\nflip the chosen word. The repair is the one this page keeps\nteaching: pin the addition order. Kernels rewritten to reduce in\none fixed pattern at every batch size made all 1,000 answers\nidentical, at about a 2x cost in an unoptimized server [[31](https://tensor.khalilli.ai/blog/floating-point/#ref-31)].\n\nThe working rules: compare models with tolerances\n(`torch.testing.assert_close`\n\n, never `==`\n\n); flip\n`torch.use_deterministic_algorithms(True)`\n\nwhen you need\nrepeatability inside one machine and accept the speed cost; and\nwhen a loss goes nan, hunt the overflow upstream (an unmasked\nsoftmax, an unscaled fp16 gradient, a variance of zero) rather\nthan rerolling the seed. The nan at the top of this page has the same kind of cause as\nthe 0.1 + 0.2: some number moved past the edge of its map.\n\nKonrad Zuse's Z3 computed in 22-bit floating point in 1941,\nspecial values included. 44 years later IEEE 754, largely\nWilliam Kahan's design, ended an era in which every vendor's\narithmetic disagreed [[3](https://tensor.khalilli.ai/blog/floating-point/#ref-3)]. The formats since bfloat16\nare the same standard's ideas at new bit counts, and the full table\nof every named format that ever shipped, from the Z3 to the\nG.711 telephone float of 1972 (an 8-bit float in every phone call\nfor 50 years) to NVFP4, is longer than this page and belongs\nto a follow-up table of its own. The roads not taken, posits and\nlogarithmic number systems, are worth reading about\n[[17](https://tensor.khalilli.ai/blog/floating-point/#ref-17)].\n\nYou already did, in the proofs. The 15-line decoder in\n[p2_e4m3_map.py](https://tensor.khalilli.ai/blog/floating-point/#proof-p2-e4m3-map) is a complete FP8\nimplementation that torch agrees with on all 256 patterns; the\nblock quantizer in [p11_mxblock.py](https://tensor.khalilli.ai/blog/floating-point/#proof-p11-mxblock) is MXFP4\nminus the packing; the stochastic rounder lives in\n[p12_sr_drift.py](https://tensor.khalilli.ai/blog/floating-point/#proof-p12-sr-drift). Change E4M3's constants\nto E5M2's and check yourself against torch again: that is the\nexercise that makes every format in this family yours.\n\nPredict first, then run, then explain the difference:\n[p0](https://tensor.khalilli.ai/blog/floating-point/#proof-p0-riddles) the two opening questions;\n[p1](https://tensor.khalilli.ai/blog/floating-point/#proof-p1-where-numbers-live) your own histogram;\n[p2](https://tensor.khalilli.ai/blog/floating-point/#proof-p2-e4m3-map) all 256 of E4M3;\n[p4](https://tensor.khalilli.ai/blog/floating-point/#proof-p4-absorb-cancel) absorption and cancellation;\n[p5](https://tensor.khalilli.ai/blog/floating-point/#proof-p5-sum-order) order;\n[p6](https://tensor.khalilli.ai/blog/floating-point/#proof-p6-stability) the cliff;\n[p7](https://tensor.khalilli.ai/blog/floating-point/#proof-p7-sixteen-bits) the fork;\n[p8](https://tensor.khalilli.ai/blog/floating-point/#proof-p8-lost-update) the vanished update;\n[p9](https://tensor.khalilli.ai/blog/floating-point/#proof-p9-loss-scaling) the slide;\n[p10](https://tensor.khalilli.ai/blog/floating-point/#proof-p10-fp4-outlier) the spike;\n[p11](https://tensor.khalilli.ai/blog/floating-point/#proof-p11-mxblock) blocks;\n[p12](https://tensor.khalilli.ai/blog/floating-point/#proof-p12-sr-drift) stochastic rounding;\n[p13](https://tensor.khalilli.ai/blog/floating-point/#proof-p13-hadamard) the rotation;\n[p14](https://tensor.khalilli.ai/blog/floating-point/#proof-p14-grids) the three grids;\n[p16](https://tensor.khalilli.ai/blog/floating-point/#proof-p16-wide-formats) the four cards from two numbers;\n[p17](https://tensor.khalilli.ai/blog/floating-point/#proof-p17-bf16-truncation) all 65,536 bfloat16 patterns;\n[p18](https://tensor.khalilli.ai/blog/floating-point/#proof-p18-family-sheet) the family sheet;\n[p19](https://tensor.khalilli.ai/blog/floating-point/#proof-p19-nvfp4-worked) one NVFP4 block by hand;\n[p20](https://tensor.khalilli.ai/blog/floating-point/#proof-p20-e5m2-top-byte) all 256 E5M2 patterns;\n[p21](https://tensor.khalilli.ai/blog/floating-point/#proof-p21-fma-one-rounding) 1 rounding against 2;\n[p22](https://tensor.khalilli.ai/blog/floating-point/#proof-p22-exact-error) the error, caught exactly;\n[p23](https://tensor.khalilli.ai/blog/floating-point/#proof-p23-double-rounding) rounding twice.\nOn paper: how many doublings separate E5M2's floor from its\nceiling, and would the p1 gradients fit without a scale? And\nbefore running p16: derive float16's ceiling from E=5, M=10\nyourself, the way the binary32 section taught you.\n\n[1] Khalilli, *proof scripts for this page, measured on an Apple\nM3 Max, torch 2.11.0, CPU*, 2026. Linked in place above; rerun them\nto check me.\n\n[2] Sanglard, *Floating Point Visually Explained*, 2017. The\ninterval-and-position way of seeing the fields comes from here.\n[https://fabiensanglard.net/floating_point_visually_explained/](https://fabiensanglard.net/floating_point_visually_explained/)\n\n[3] IEEE, *754-2019: Standard for Floating-Point Arithmetic*\n(first edition 1985).\n\n[4] Goldberg, *What Every Computer Scientist Should Know About\nFloating-Point Arithmetic*, ACM Computing Surveys, 1991; and Muller\net al., *Handbook of Floating-Point Arithmetic*, 2nd ed., 2018.\n\n[5] Micikevicius et al., *Mixed Precision Training*, ICLR 2018.\n[https://arxiv.org/abs/1710.03740](https://arxiv.org/abs/1710.03740)\n\n[6] Micikevicius et al., *FP8 Formats for Deep Learning*, 2022.\n[https://arxiv.org/abs/2209.05433](https://arxiv.org/abs/2209.05433)\n\n[7] DeepSeek-AI, *DeepSeek-V3 Technical Report*, 2024.\n[https://arxiv.org/abs/2412.19437](https://arxiv.org/abs/2412.19437)\n\n[8] Dettmers et al., *LLM.int8(): 8-bit Matrix Multiplication\nfor Transformers at Scale*, 2022 (the emergent-outlier measurement).\n[https://arxiv.org/abs/2208.07339](https://arxiv.org/abs/2208.07339)\n\n[9] Open Compute Project, *Microscaling Formats (MX)\nSpecification v1.0*, 2023; and Rouhani et al., *Microscaling Data\nFormats for Deep Learning*, 2023.\n[https://arxiv.org/abs/2310.10537](https://arxiv.org/abs/2310.10537)\n\n[10] NVIDIA, *Pretraining Large Language Models with NVFP4*,\n2025. [https://arxiv.org/abs/2509.25149](https://arxiv.org/abs/2509.25149)\n\n[11] Mishra et al., *Recipes for Pre-training LLMs with MXFP8*,\n2025 (the scale-rounding result).\n[https://arxiv.org/abs/2506.08027](https://arxiv.org/abs/2506.08027)\n\n[12] Tseng et al., *Training LLMs with MXFP4*, 2025.\n[https://arxiv.org/abs/2502.20586](https://arxiv.org/abs/2502.20586)\n\n[13] Dettmers et al., *QLoRA: Efficient Finetuning of Quantized\nLLMs*, 2023 (NF4). [https://arxiv.org/abs/2305.14314](https://arxiv.org/abs/2305.14314)\n\n[14] Frantar et al., *GPTQ*, 2022.\n[https://arxiv.org/abs/2210.17323](https://arxiv.org/abs/2210.17323)\n\n[15] Lin et al., *AWQ: Activation-aware Weight Quantization*,\n2023. [https://arxiv.org/abs/2306.00978](https://arxiv.org/abs/2306.00978)\n\n[16] Dettmers et al., *8-bit Optimizers via Block-wise\nQuantization*, 2021.\n[https://arxiv.org/abs/2110.02861](https://arxiv.org/abs/2110.02861)\n\n[17] Gustafson and Yonemoto, *Beating Floating Point at its Own\nGame: Posit Arithmetic*, 2017; logarithmic number systems survey in\nMuller et al. [4].\n\n[18] Severance, *An Interview with the Old Man of\nFloating-Point* (William Kahan on IEEE 754 and the Intel 8087), 1998.\n[https://people.eecs.berkeley.edu/~wkahan/ieee754status/754story.html](https://people.eecs.berkeley.edu/~wkahan/ieee754status/754story.html)\n\n[19] NVIDIA, *CUDA C++ Programming Guide*, the arithmetic\ninstructions throughput table (per-architecture float64 rates).\n[https://docs.nvidia.com/cuda/cuda-c-programming-guide/](https://docs.nvidia.com/cuda/cuda-c-programming-guide/)\n\n[20] Kharya, *TensorFloat-32 in the A100 GPU Accelerates AI\nTraining, HPC up to 20x*, NVIDIA blog, 2020.\n[https://blogs.nvidia.com/blog/tensorfloat-32-precision-format/](https://blogs.nvidia.com/blog/tensorfloat-32-precision-format/)\n\n[21] PyTorch documentation, *CUDA semantics* (the TF32 flags and\ntheir defaults since 1.12).\n[https://docs.pytorch.org/docs/stable/notes/cuda.html](https://docs.pytorch.org/docs/stable/notes/cuda.html)\n\n[22] Industrial Light & Magic, *About OpenEXR* (the half type,\n2000-2003, and its Cg compatibility); and Bogart, Kainz, Hess, *The\nOpenEXR Image File Format*, GPU Gems, 2004.\n[https://openexr.com/en/latest/about.html](https://openexr.com/en/latest/about.html)\n\n[23] Wang and Kanwar, *BFloat16: The secret to high performance\non Cloud TPUs*, Google Cloud blog, 2019.\n[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)\n\n[24] Ku, Poli et al. (Radical Numerics), *NVFP4 pretraining:\nfrom theory to implementation, Part 1*, 2026. The recipe\nwalk-through this page's NVFP4 worked block follows.\n[https://www.radicalnumerics.ai/blog/nvfp4-part1](https://www.radicalnumerics.ai/blog/nvfp4-part1)\n\n[25] Easygoing, *Which is Better: FP8_scaled or MXFP8? A\nThorough Comparison of Image Generation AI Model Accuracy and\nSpeed*, AI Image Journey, 2026. The measured quality ranking and\nthe measured slowdown of MXFP8 on hardware without MX support.\n[https://note.com/ai_image_journey/n/n99d0ed2f1c1d](https://note.com/ai_image_journey/n/n99d0ed2f1c1d)\n\n[26] ZeroEntropy, *MXFP4* (concepts), 2026. The per-vendor\nnative-support summary.\n[https://zeroentropy.dev/concepts/mxfp4/](https://zeroentropy.dev/concepts/mxfp4/)\n\n[27] Boehm, *Can Function Inlining Affect Floating Point\nOutputs? Exploring FMA and Other Consistency Issues*, 2023.\n[https://siboehm.com/articles/23/Inlining-FMA-FP-consistency](https://siboehm.com/articles/23/Inlining-FMA-FP-consistency)\n\n[28] PyTorch documentation, *Numerical accuracy*.\n[https://docs.pytorch.org/docs/stable/notes/numerical_accuracy.html](https://docs.pytorch.org/docs/stable/notes/numerical_accuracy.html)\n\n[29] Kwon et al., *Efficient Memory Management for Large Language\nModel Serving with PagedAttention* (the vLLM paper), 2023.\n[https://arxiv.org/abs/2309.06180](https://arxiv.org/abs/2309.06180)\n\n[30] NVIDIA, *H100 Tensor Core GPU* specifications (HBM3 bandwidth\nand PCIe generation 5 rates).\n[https://www.nvidia.com/en-us/data-center/h100/](https://www.nvidia.com/en-us/data-center/h100/)\n\n[31] He and Thinking Machines Lab, *Defeating Nondeterminism in\nLLM Inference*, Connectionism, 2025. The 1,000-completions\nmeasurement and the batch-invariant kernels.\n[https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/](https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/)\n\nThree 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.", "url": "https://wpnews.pro/news/floating-point", "canonical_source": "https://tensor.khalilli.ai/blog/floating-point/", "published_at": "2026-08-10 00:00:00+00:00", "updated_at": "2026-08-11 19:44:49.326694+00:00", "lang": "en", "topics": ["machine-learning", "ai-research"], "entities": ["Apple M3 Max", "torch 2.11.0", "tensor.khalilli.ai"], "alternates": {"html": "https://wpnews.pro/news/floating-point", "markdown": "https://wpnews.pro/news/floating-point.md", "text": "https://wpnews.pro/news/floating-point.txt", "jsonld": "https://wpnews.pro/news/floating-point.jsonld"}}