FP8 E4M3 vs E5M2

Same byte, different split. E4M3 gives one exponent bit to the mantissa: twice as fine (8 steps per power of two vs 4), over 18 binades of range instead of 32. For a KV cache take E4M3. Memory is identical, so precision decides.

At a glance

E4M3E5M2
Bits: sign · exponent · mantissa1 · 4 · 31 · 5 · 2
Exponent bias715
Largest finite±448±57,344
Smallest normal2−6 = 0.0156252−14 ≈ 6.1 × 10−5
Smallest subnormal2−9 ≈ 0.001952−16 ≈ 1.5 × 10−5
Range, subnormals included18 binades32 binades
Steps per power of two84
Worst rounding error, half a step6.25 %12.5 %
Infinitynone±inf
NaN codes2 (S.1111.111)6
Finite codes of 256254248
PyTorch dtypetorch.float8_e4m3fntorch.float8_e5m2
Training use (FP8 paper, Transformer Engine HYBRID)weights, activations: forward passgradients: backward pass
KV cachevLLM fp8 = fp8_e4m3; SGLang: “recommended”supported, for “larger dynamic range”

Limits: Micikevicius et al. 2022, Table 1 (the encoding OCP OFP8 standardised). Code counts: all 256 codes enumerated with ml_dtypes 0.6.0. Rounding error as a share of the power of two below the value.

Bit layout: E4M3 is 1 sign, 4 exponent and 3 mantissa bits, bias 7, max 448; E5M2 is 1 sign, 5 exponent and 2 mantissa bits, bias 15, max 57,344. Below, the representable values between 1 and 4: E4M3 has 16, E5M2 has 8. The value 3.3 rounds to 3.25 in E4M3, an error of minus 1.5 percent, and to 3.5 in E5M2, plus 6.1 percent. One byte, two splits E4M3 SEEEEMMM bias 7 · max 448 · 8 steps / binade E5M2 SEEEEEMM bias 15 · max 57,344 · 4 steps / binade S sign · E exponent = range · M mantissa = precision Every value between 1 and 4, and where 3.3 lands 3.3 E4M3 → 3.25 · −1.5 % 1234 E5M2 → 3.5 · +6.1 % 1234 The step doubles at every power of two: E4M3 0.125 then 0.25 · E5M2 0.25 then 0.5.
Bit split from the FP8 paper's Table 1. Grid points: all 256 codes decoded with ml_dtypes 0.6.0; rounding is to nearest.

Exponent bits buy range, mantissa bits buy precision

A normal FP8 value is (−1)S × 1.M × 2E − bias; an all-zero exponent gives subnormals, 0.M × 21 − bias. After the sign there are 7 bits to share.

Worked example

ValueE4M3ErrorE5M2Error
3.33.25−1.5 %3.5+6.1 %
0.10.1015625+1.6 %0.09375−6.3 %
0.0010.001953125+95 %0.0009765625−2.3 %
1000out of range (NaN)—1024+2.4 %

ml_dtypes 0.6.0 in python:3.12-slim, round to nearest even, no scale. Its cast does not saturate, so 1000 becomes NaN rather than 448.

3.3 = 1.65 × 21. E4M3 has mantissas 1.625 and 1.75 either side, and 1.625 is nearer: 3.25. E5M2 has 1.5 and 1.75, and 1.75 is nearer: 3.5.

0.001 is below E4M3’s smallest normal (2−6) and nearest its smallest subnormal (2−9), so it nearly doubles. For comparison, BF16 stores 3.3 as 3.296875 (−0.09 %) and 0.1 as 0.10009765625 (+0.10 %).

The scale moves the window, it does not widen it

E4M3 gets its range back with a scale s: store x ÷ s, read back q × s. One FP32 scale per tensor, or per head, is a few bytes per layer.

Stored as10003.30.10.001
E4M3, s = 1NaN3.250.10156250.001953125
E4M3, s = 410243.250.10156250
E5M2, s = 110243.50.093750.0009765625

Same ml_dtypes run. 0.001 ÷ 4 = 0.00025, below half of E4M3's smallest subnormal: flushed to zero.

0.001 to 1000 is 19.9 binades. E4M3 covers 17.8 (2−9 to 448), so one scale keeps one end. E5M2 covers 31.8 and keeps both, at twice the rounding error everywhere.

How the engines set it:

At scale 1.0, any K or V value past ±448 is out of E4M3’s range.

Which one for a KV cache

Measured here: E4M3 KV on 2x DGX Spark

What we saw on our hardware, not a general law. Qwen3.8 Flash-Next on vLLM, tensor parallel over two GB10 boxes. Its KV default moved from BF16 to fp8_e4m3 on 2026-10-26. fp8_e5m2 has never run here.

Running requestsBF16 KV, gen tok/sFP8 E4M3 KV, gen tok/sChange
146–54≈31≈ −40 %
5–12, window A122–13593≈ −25 %
5–12, window B143–154111≈ −25 %
MTP drafter acceptance0.55–0.630.48

The gateway's per-minute perf log, 2026-10-26, production traffic at ~44–79K tokens of context per request. Same concurrency, not a controlled benchmark.

Likely reason, not proven: this model’s attention backend (vLLM’s QWEN4_EXP_QSA_TRITON) dequantises FP8 to BF16 in registers on every read. That adds work instead of saving bandwidth. On this kernel FP8 KV bought ~2x context capacity, not speed.

The 4-bit side of the same question: KV cache: FP8 vs NVFP4 and FP4 KV cache on the local fleet. Every kv_cache_dtype and who refuses it: KV type. E4M3 is also NVFP4’s block-scale format: MXFP4 vs NVFP4. The 16-bit baseline both are weighed against: BF16 vs FP16.

Sources

Questions? Let's Chat :)

ai llm engineering