KV cache: FP8 vs NVFP4

Result as of 2026-10-22. NVFP4 KV gives 1.6x the tokens of FP8 in the same memory on both machines measured. On the RTX 5090 node (no speculative decoding) that is the whole trade. On the two DGX Spark boxes it costs the speculative drafter, and the drafter is worth more. Same-load speed benchmarks have not been run yet (see Pending).

The two formats

FP8 (e4m3)NVFP4
Value8-bit float, 4 exponent / 3 mantissa bits4-bit float E2M1
Scaleper tensor / layerone FP8 scale per 16 elements
Bytes per element10.5625 (0.5 + 1/16)
Bytes per head, per tokenhead_sizehead_size//2 + head_size//16
Tokens per GB, vs FP81.00x1.78x (theory)

Page layout: vLLM torch_utils, source-read. 1.78x = 1 ÷ 0.5625.

Weights vs KV

Measured pool sizes

MachineKVPool, tokensBytes / tokenTokens / GBvs FP8
RTX 5090, 8.5 GB pinFP8 (to 2026-10-21)235,07436,166~27,6501.00x
RTX 5090, 8.5 GB pinNVFP4 (from 19:30Z)386,19422,014~45,4001.64x
2x DGX Spark, 47.30 GBFP8 + drafter (gen 213/220)2,887,22816,384~61,0001.00x
2x DGX Spark, 20.27 GBNVFP4, no drafter (gen 218)1,979,58410,240~97,7001.60x

Both below the 1.78x theory. RTX 5090: likely paging overhead, not checked. Sparks: the NVFP4 pool also holds a one-layer FP8 dequant workspace. Compare within one machine; engines, per-rank layouts and pins differ across them.

KV tokens per GB of pool: RTX 5090 FP8 27,650, NVFP4 45,400 (1.64x); 2x DGX Spark FP8 61,000, NVFP4 97,700 (1.60x); theory 1.78x KV tokens per GB of pool, thousands (4 px per 1k) RTX 5090 · vLLM 0.31.1rc1 FP8 27.65 NVFP4 45.4 1.64x 2x DGX Spark · SGLang 708f51e FP8 + drafter 61.0 NVFP4, no drafter 97.7 1.60x Theory for both: 1.78x (1 B vs 0.5625 B per element).
Pool tokens ÷ pool size, from each engine's startup log, 2026-10-21/22.

On the RTX 5090 the same pin holds 2.39 full-length (98,304-token) requests on FP8 and 3.93 on NVFP4.

RTX 5090 node: NVFP4 in service

Qwen3.8-27B NVFP4 weights, vLLM 0.31.1rc1, no speculative decoding. vLLM 0.31.1 on sm_120 reads NVFP4 KV through FlashInfer FA2.

Check on NVFP4 KV (2026-10-22)Result
Arithmeticcorrect
Tool callcorrect
Planted code recalled at 8k / 60k / 75k depth3 of 3
Prefill, 87,119-token prompt19.2 s ≈ 4,526 tok/s (incl. queue)
Single-stream decode (one other stream running)60.3 tok/s
FP8 decode on this card, same loadnot measured

2x DGX Spark: FP4 KV vs the drafter

GB10 x2, TP=2, SGLang 708f51e, Qwen3.8-27B NVFP4 + DFlash2 speculative drafter, 32 seats, 131,072 context.

GenKVDrafterOutcome
213FP8on, FlashInferserves; all checks pass
214NVFP4on, FlashInferdies at init: FlashInfer cannot decode NVFP4
215NVFP4on, trtllm_mhadies at CUDA graph capture: “supports decode only”, the drafter’s block pass is not
216MX-FP4on, Tritonkilled by the memory guard: pool at the fraction’s max, large allocation at drafter init
217MX-FP4, 28 GB pinon, Tritondies at first batch: the DFLASH writer copies BF16 into the FP4 pool
218NVFP4, 28 GB pinoffserves; all checks pass; 1,979,584-token pool
219NVFP4on, own FP8 poolstarts (4,074,176 tokens, 1.41x the FP8 pool) but serves wrong output
220FP8onrollback; all checks pass

Gen 219 used SGLang’s --speculative-draft-kv-cache-dtype to give the drafter an FP8 pool of its own. It answered 17×23 as “22 by 5”, made no tool call and missed the planted code at 8k, 60k and 110k. Drafter acceptance looked healthy (accept length 1.6–3.9). Suspect, not proven: the target’s verify pass runs on the prefill backend and misreads the NVFP4 pool. The launcher now refuses FP4 KV together with a drafter.

Why the drafter wins on this pair

Per-stream decode on 2x DGX Spark: FP8 with drafter about 32 tok/s at 6 streams; NVFP4 without drafter about 14 at 3-4 streams and 11 at 2. Per-GPU bandwidth at decode: 198 of 273 GB/s, weights 160, KV 35 Decode per stream, tok/s (10 px per tok/s) FP8 + drafter, 6 streams ~32 NVFP4, no drafter, 3–4 streams ~14 NVFP4, no drafter, 2 streams ~11 Different stream counts; FP8 figure from one 5-s window. Not a same-load comparison. Bandwidth per GPU at decode, NVFP4 no drafter, GB/s (1.6 px per GB/s) weights 160 KV 35 other + unused 198 of 273 at 25 passes/s Weights are 81% of the bytes per pass; shrinking KV cannot make a pass much cheaper.
Gen 218 vs gen 213/220 on production traffic, 2026-10-22. Peak 273 GB/s: GB10 spec.

Decode on the pair is weight-bandwidth bound: each pass reads the weights once. With the drafter a pass yields several accepted tokens per stream; without it, one. At 2–6 streams that is ~2.3x per stream in favour of FP8 + drafter, while NVFP4 only shrinks the 35 GB/s KV share.

Who serves NVFP4 KV

EngineGPUNVFP4 KVNote
vLLM 0.31.1rc1RTX 5090 (sm_120)servesFlashInfer FA2; in service
vLLMGB10 (sm_121)refusedparser accepts; served only on CC 10.x (FlashInfer trtllm-gen)
vLLM 0.19.1, ROCmAMDnono FP4 KV at all
SGLang 708f51eGB10, no drafterservesgen 218
SGLang 708f51eGB10, DFlash2 drafternodies (214–217) or wrong output (219)

vLLM rows: source-read from the images, except the RTX 5090 row (run). SGLang rows: run on the pair.

Pending: benchmarks

Not run yet. Each cell will be the same prompt set at the same concurrency on the same machine, FP8 vs NVFP4.

MetricRTX 5090 FP8RTX 5090 NVFP4Sparks FP8 + drafterSparks NVFP4, no drafter
Decode tok/s per stream, 1 stream————
Decode tok/s per stream, 4 streams————
Decode tok/s per stream, 8 streams————
Prefill tok/s, ~9k prompt————
Prefill tok/s, ~70k prompt————
Recall of a planted code at 8k / 60k / 110k————
Answers matching FP8 on a fixed eval set————

Spot figures above (60.3 tok/s, 4,526 tok/s, ~32 / ~14 tok/s) were taken under whatever traffic was running and are not entered here.

FP8 vs NVFP4 KV, as of 2026-10-22: NVFP4 on a node without speculative decoding (1.64x pool, checks pass); FP8 on a decode-bound pair that runs a drafter, until an SGLang build serves FP4 KV with one. Background on the FP4 formats: MXFP4 vs NVFP4; on the attention backends: FlashInfer or Triton for local inference.

Questions? Let's Chat :)

ai llm engineering