DEV Community

Seth Wheeler
Seth Wheeler

Posted on Originally published at sethwheeler.dev

Comparing INT4 and NVFP4 Palettes on Real Gradient Tensors

Four-bit training quantizes every number to one of 16 values. NVFP4's menu is {0, ±0.5, ±1, ±1.5, ±2, ±3, ±4, ±6}, with one scale factor per block of 16 elements. Those levels are spaced like a float: fine near zero, coarse at the top. The standard pipeline also applies a random Hadamard rotation before quantizing, which spreads outlier energy across a block and pulls the per-coordinate distribution toward a bell curve.

Both of those exist to handle outliers, which made me suspect they were paying for the same thing twice. So I treated the 16-value menu as a design variable and searched for better ones. Plain evenly-spaced INT4 beat NVFP4 on Gaussian blocks at both rounding modes. On my heavy-tailed stand-in for unrotated data it lost by a factor of 2.3. That looked like a clean conditional, so I wrote it as one: the uniform grid wins after the rotation, because the rotation has already removed the outliers the float spacing exists for.

Then I ran the comparison on 45 real gradient tensors instead of synthetic ones. Uniform INT4 wins on 44 of 45 rotated, and on 41 of 45 unrotated. The condition I attached to the claim was an artifact of how I generated fake data, and the reason is a single number I hadn't looked at.

(This is the second thing I've written about the same study. The noise-floor post covers the scale rule and why two rented GPUs couldn't see it; that argument isn't repeated here. The code is not public, so this post carries the numbers instead.)

The two corners, and what the palette does to them

Every measurement below scores a palette at two extremes of the same trade-off. The unbiased corner scales each block by its absmax, so nothing clips, and rounds stochastically. Its error is computed exactly as E[(y-l)(u-y)] over the interval each value lands in. The biased corner sweeps 19 candidate scales per block, rounds to nearest, keeps the best, and applies the per-block reconstruction scale that minimizes error. Values above the top level clip, so it is biased, but its error is much lower. Gradient training wants the unbiased corner and the forward pass wants the low-error one, which is why both matter.

On synthetic blocks (12,000 for fitting, 30,000 held out, normalized MSE):

Gaussian blocks unbiased biased ratio
NVFP4 0.01789 0.00685 2.61x
uniform INT4 0.01445 0.00621 2.33x
absmax quantile grid 0.01845 0.00706 2.62x
coordinate descent, unbiased objective 0.01281 0.00524 2.45x
coordinate descent, biased objective 0.01307 0.00523 2.50x

Uniform INT4 is 19.2% lower error at the unbiased corner and 9.3% lower at the biased one, with no format machinery at all. Optimizing the palette properly gets 28.4% and 23.6%, and the two optimizations converge on nearly the same grid (0, 0.098, 0.206, 0.319, 0.438, 0.588, 0.774, 1.000 for the unbiased objective), which is a useful thing to know: a future format could pick one palette without committing to a rounding philosophy.

Nothing here escapes the underlying trade-off, though. The unbiased-to-biased ratio sits between 2.18x and 2.80x for every grid I tested, including the ones optimized specifically for one corner. Palette design moves the level of the error; it does not buy an unbiased estimator at biased-corner cost. That trade is a property of having 16 values, not of NVIDIA's choice of which 16.

And then the row that produced the wrong claim. Repeating the measurement on blocks where 5% of values are contaminated with noise at ten times the scale, my stand-in for what unrotated gradients look like:

heavy-tailed blocks unbiased biased
NVFP4 0.01651 0.00756
uniform INT4 0.03731 0.01709

Uniform INT4 loses by 2.26x at both corners. The story wrote itself: the float spacing is dynamic range, dynamic range is what you need when a block holds an outlier, and the rotation is what removes outliers. Hence "after the rotation".

45 real tensors disagree

The real test is real gradients. I trained a small transformer (3M parameters, byte-level, three layers) and captured every 2-D weight-gradient tensor at steps 20, 60 and 120, giving 45 tensors at a training loss of 2.79. Then I scored both palettes on each tensor twice: once raw, once through the study's own 16x16 random Hadamard.

real gradients NVFP4 unbiased INT4 unbiased INT4 wins
rotation-free 0.01812 0.01411 (+22.1%) 41 of 45
Hadamard-rotated 0.01781 0.01469 (+17.5%) 44 of 45
real gradients NVFP4 biased INT4 biased INT4 wins
rotation-free 0.00694 0.00605 (+12.8%) 38 of 45
Hadamard-rotated 0.00684 0.00631 (+7.8%) 39 of 45

Uniform INT4 wins in every cell. Worse for my framing, it wins more without the rotation than with it (22.1% against 17.5% at the unbiased corner), so the rotation is not what enables the result; it very slightly shaves it. The rotation moves every number in these tables by under 4% in either direction, which for a transform I had cast as the precondition is close to a no-op.

The number I should have looked at first

Excess kurtosis is the standard measure of how outlier-heavy a distribution is (0 is Gaussian). Measured on the same 45 tensors, two ways:

raw, whole tensor after per-block absmax normalization
real gradients, rotation-free +19.40 (median +4.07, max +255.07) -0.19
real gradients, rotated +22.12 (median +4.68, max +260.99) -0.15
my Gaussian stand-in -0.01 -0.50
my heavy-tailed stand-in +40.27 +0.83

Real gradients are extremely outlier-heavy, exactly as the literature says: one tensor has excess kurtosis of 255. But a quantizer with a per-16-element scale never sees the whole tensor. It sees one block at a time, divided by that block's own absmax, and by that measure real gradients are slightly flatter than a Gaussian at -0.19, whether you rotate them or not.

The outliers in a real gradient live between blocks, not within them. A block containing a huge value gets a huge scale, and the per-block scale has already absorbed the problem before any level spacing is consulted. My heavy-tailed stand-in contaminated 5% of values independently, which scatters roughly one outlier into every block of 16 and leaves nothing for the scale to absorb. That is a within-block problem, and within-block spread is exactly what exponential level spacing is for. So the stand-in did not model unrotated gradients. It modelled a distribution whose outliers sit at the one granularity where NVFP4's palette earns its keep. Getting the tail weight roughly right was not enough, because what mattered was the tail's position relative to the scale granularity.

Once blocks are that well behaved, NVFP4's spacing is straightforwardly a handicap, and it is easy to see where the error is. Decomposing the unbiased corner's error on Gaussian blocks by which interval each value fell into:

NVFP4 interval width share of values share of total error
0.3333 19.3% 49.4%
0.1667 30.8% 35.0%
0.0833 49.9% 15.6%

Stochastic rounding error inside an interval scales with the square of its width, so NVFP4's widest interval turns 19.3% of the values into 49.4% of the error. Its widest gap is 4.00 times its narrowest. Uniform INT4's is 1.00 by construction, and even the coordinate-descent palettes only stretch to 2.31x; nothing the search found wanted spacing as aggressive as a float's.

What I can't claim from this

Every number above is quantization error, not training loss, and the earlier post is the reason I am careful about that: the same study showed that differences of this size do not surface in loss until batches reach one to five million tokens, well past anything I have run. Uniform INT4 has a 17 to 22% error advantage on real gradient blocks and an entirely unmeasured effect on final loss. Nobody should switch formats on the strength of a proxy.

The model is also tiny (3M parameters on a byte-level corpus, 45 tensors, one rotation seed), and 1 to 7 of those 45 tensors go the other way depending on the corner, so this is a distribution-level result rather than a universal one. My harness also gives both palettes an exact floating-point block scale, where real NVFP4 stores that scale in FP8 E4M3 alongside a per-tensor FP32 scale. That compares palettes under identical conditions, not two shipped formats. And NVFP4 has Blackwell tensor cores behind it while an INT4 gradient path does not, so a 19% error reduction is not a 19% anything else. I measured no wall clock.

What generalizes

The finding I would keep is that scale granularity mattered more than level placement, and I have it twice now from unrelated work. A separate experiment in the same repository, quantizing a token embedding table, found that int4 with one scale per tensor costs 2.5 points of top-1 accuracy while int4 with one scale per row costs 0.3, for 16 KB of extra scales (1.7% more bytes). Same lesson from the other direction: given a per-block scale fine enough, the levels can be evenly spaced, and given one that is not, no palette saves you.

The methodological half is the part I will actually carry forward. Replacing real data with a synthetic distribution means choosing a tail weight, and I knew that; what I did not think about is that it also silently chooses where the tails act relative to the scale granularity. My stand-in matched real gradients on the statistic I checked and inverted the result anyway. The check that caught it was one line of kurtosis on the blocks my quantizer actually consumes, and I could have run it before the synthetic study rather than after.

Top comments (2)

Collapse
 
max_quimby profile image
Max Quimby

This is a genuinely careful post — the part I appreciate most is that you caught your own conditional being an artifact of synthetic data instead of shipping it. That failure mode (the fake distribution quietly encoding the conclusion) burns a lot of quantization work.

My guess at "the single number you hadn't looked at": kurtosis of the per-block distribution after the Hadamard rotation. The whole justification for NVFP4's float-like spacing is fat tails, but a random Hadamard rotation drives each block toward Gaussian (CLT doing its job across 16 elements), so post-rotation the excess kurtosis is small and the outlier budget the float menu pays for simply isn't there — uniform INT4 then wins because it spends all 16 levels on the bulk where the mass actually is. If the number is instead the fraction of block energy above the top level, that's the same story from the clipping side.

Two questions: does the INT4 win hold as you shrink the block below 16, where the rotation has fewer elements to Gaussianize? And did you look at whether the one unrotated tensor INT4 lost on had visibly higher kurtosis than the other 44 — i.e. is the exception the rule confirming itself?

Collapse
 
megapixel99 profile image
Seth Wheeler

Regarding the guess: the number is per-block kurtosis, as you said, but the disagreement is over what flattens it. After the rotation the excess kurtosis of the blocks my quantizer actually consumes is -0.15; rotation-free it is already -0.19, so the rotation moves it slightly the wrong way rather than gaussianizing anything, and whole-tensor kurtosis actually rises under it (+19.40 to +22.12). What flattens the blocks is the per-16-element absmax scale, not the CLT: dividing a block by its own max is most of the outlier handling already, and it happens whether or not you rotate. The clipping-side version of the same story cannot be the mechanism at the unbiased corner specifically, since that corner sets the scale to absmax by construction and nothing can land above the top level.

Does the win hold below 16? It holds and it grows (rotation-free, unbiased corner):

block NVFP4 uniform INT4 INT4 wins
4 0.00971 0.00622 (+35.9%) 44 of 45
8 0.01440 0.01008 (+30.0%) 44 of 45
16 0.01812 0.01411 (+22.1%) 41 of 45
32 0.02090 0.01809 (+13.5%) 37 of 45

That is the opposite of what the CLT reading predicts; the rotation has fewer elements to work with at every step down and INT4's margin widens anyway, because the finer scale drags normalized block kurtosis further below Gaussian (-0.19 at 16, -0.52 at 8, -0.93 at 4). Going the other way costs INT4 for the same reason. Two caveats on that table: NVFP4's block is 16 by definition, so 4 and 8 are palettes under a hypothetical rather than shipped formats, and I am holding the palette fixed while ignoring what the scales cost, which at one E4M3 scale per block is 0.5 extra bits per value at 16 and 2.0 at 4.

The tensors INT4 lost on: four, not one. 44 of 45 is the rotated cell; rotation-free is 41 of 45. And yes, the exception confirms the rule, though the raw statistic points the wrong way. The four losers average +0.201 normalized block kurtosis against the winners' -0.233, and the worst of them (the layer-2 MLP down-projection at step 20) sits at +0.774, which is very nearly my heavy-tailed stand-in's +0.83; INT4 loses on exactly the tensors that look like the fake data. However, on raw whole-tensor kurtosis the losers average +5.52 and the winners +20.75, and the fattest-tailed tensor in the set, the token embedding at step 120 at +255.07, is one INT4 wins on. Screening on whole-tensor kurtosis would have selected precisely the wrong four. All four rotation-free losers are fc2, which I do not have an explanation for yet.