<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Fp8 on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/fp8/</link><description>Recent content in Fp8 on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 05 Jun 2026 04:00:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/fp8/index.xml" rel="self" type="application/rss+xml"/><item><title>FP8 end to end: enable it, measure quality and decide with data</title><link>https://blog.lo0.es/en/posts/fp8-end-to-end-enable-measure-quality-decide/</link><pubDate>Fri, 05 Jun 2026 04:00:00 +0000</pubDate><guid>https://blog.lo0.es/en/posts/fp8-end-to-end-enable-measure-quality-decide/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>FP8 is the configuration change with the highest impact per unit of effort available on H100 and Ada Lovelace hardware. On an H100 it enables native FP8 tensor cores: +40-60% decode throughput and ×2 the VRAM available for the KV cache. On an RTX 4090 and an L40 the compute benefit is smaller, but the ×2 VRAM is real and translates directly into twice the concurrency. The risk is quality degradation, which on well-calibrated modern models is &amp;lt;0.5% on standard benchmarks but can be larger on formal reasoning. The correct workflow is not to switch it on and pray: it is to switch it on in staging, run the eval suite, correlate quality with throughput in OTel, and decide with data.&lt;/p>
&lt;hr>
&lt;h2 id="the-analogy">The analogy&lt;/h2>
&lt;p>A photographer who works with 35 mm negatives and moves to digital. Digital photos take up less space and are processed faster. But a low-resolution photo of a landscape can be indistinguishable from the high-resolution one to the human eye, while a low-resolution photo of text loses letters. Exactly the same trade-off applies to FP8: for tasks where numerical imprecision is averaged out over thousands of activations (conversation, summarisation, RAG), it is practically invisible. For tasks where a single wrong multiplication propagates into an incorrect answer (formal mathematics, critical code), it can be decisive.&lt;/p>
&lt;hr>
&lt;h2 id="the-three-layers-of-fp8-in-vllm">The three layers of FP8 in vLLM&lt;/h2>
&lt;p>FP8 is not a single flag: it is three independent layers that are enabled separately and have different benefits.&lt;/p>
&lt;p>&lt;strong>Layer 1 — Model weights (&lt;code>--quantization fp8&lt;/code>):&lt;/strong>
The model weights are stored and computed in FP8 E4M3. The models have to be pre-quantised (available on HuggingFace with a &lt;code>-FP8&lt;/code> or &lt;code>-fp8&lt;/code> suffix) or quantised at load time with calibration. The benefit: the model takes up half the VRAM and the weight matmuls are 2× faster on an H100.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Pre-quantised model (recommended for production)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --quantization fp8
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Or on-the-fly quantisation (no extra files, somewhat slower on the first tokens)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve meta-llama/Meta-Llama-3.1-70B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --quantization fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype auto
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Layer 2 — KV cache (&lt;code>--kv-cache-dtype fp8&lt;/code>):&lt;/strong>
The K and V tensors of the KV cache are stored in FP8 instead of BF16. It halves the size of the KV cache, doubling the number of tokens that fit in VRAM. It does not affect the model weights.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve my-model &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales &lt;span class="c1"># dynamic calibration, mandatory to minimise degradation&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Layer 3 — Activations (automatic on an H100):&lt;/strong>
On Hopper GPUs, vLLM automatically enables FP8 for the intermediate activations when both previous layers are active. No extra flag is required.&lt;/p>
&lt;p>&lt;strong>Full production configuration:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --quantization fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.92 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-model-len &lt;span class="m">16384&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="the-measurable-impact-by-hardware">The measurable impact by hardware&lt;/h2>
&lt;h3 id="h100-sxm-hopper-native-fp8-tensor-cores">H100 SXM (Hopper, native FP8 tensor cores)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">Metric&lt;/th>
&lt;th style="text-align:right">BF16 baseline&lt;/th>
&lt;th style="text-align:right">FP8 enabled&lt;/th>
&lt;th style="text-align:right">Delta&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">Decode throughput (tok/s, 70B, batch 32)&lt;/td>
&lt;td style="text-align:right">~1,800&lt;/td>
&lt;td style="text-align:right">~2,700&lt;/td>
&lt;td style="text-align:right">+50%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Model VRAM (70B)&lt;/td>
&lt;td style="text-align:right">140 GB&lt;/td>
&lt;td style="text-align:right">70 GB&lt;/td>
&lt;td style="text-align:right">−50%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">KV cache VRAM available (on 4×H100)&lt;/td>
&lt;td style="text-align:right">180 GB&lt;/td>
&lt;td style="text-align:right">250 GB&lt;/td>
&lt;td style="text-align:right">+39%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Maximum concurrency (ctx 8K)&lt;/td>
&lt;td style="text-align:right">~22,500 tok&lt;/td>
&lt;td style="text-align:right">~31,250 tok&lt;/td>
&lt;td style="text-align:right">+39%&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>This is equivalent to an extra replica for free in terms of KV cache capacity.&lt;/p>
&lt;h3 id="rtx-4090-ada-lovelace-fp8-in-cuda-but-without-dedicated-tensor-cores">RTX 4090 (Ada Lovelace, FP8 in CUDA but without dedicated tensor cores)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">Metric&lt;/th>
&lt;th style="text-align:right">BF16/Q4 baseline&lt;/th>
&lt;th style="text-align:right">FP8 KV cache added&lt;/th>
&lt;th style="text-align:right">Delta&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">Decode throughput (tok/s, 14B Q4)&lt;/td>
&lt;td style="text-align:right">~45&lt;/td>
&lt;td style="text-align:right">~47&lt;/td>
&lt;td style="text-align:right">+4%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">KV cache VRAM available&lt;/td>
&lt;td style="text-align:right">15 GB&lt;/td>
&lt;td style="text-align:right">15 GB (same model)&lt;/td>
&lt;td style="text-align:right">—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Total cache tokens (ctx 8K)&lt;/td>
&lt;td style="text-align:right">~46,000&lt;/td>
&lt;td style="text-align:right">~92,000&lt;/td>
&lt;td style="text-align:right">+100%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Maximum concurrency (ctx 8K)&lt;/td>
&lt;td style="text-align:right">~5 users&lt;/td>
&lt;td style="text-align:right">~11 users&lt;/td>
&lt;td style="text-align:right">+120%&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>On Ada, the compute benefit is smaller (the FP8 tensor cores do not have the same width as on Hopper), but the ×2 in KV cache capacity is entirely real and translates into twice the possible concurrent users.&lt;/p>
&lt;hr>
&lt;h2 id="the-correct-workflow-enable-measure-decide">The correct workflow: enable, measure, decide&lt;/h2>
&lt;p>Enabling FP8 directly in production without validating quality is inadequate. The correct workflow has four steps.&lt;/p>
&lt;h3 id="step-1-baseline-in-staging">Step 1: baseline in staging&lt;/h3>
&lt;p>Before enabling FP8, record the quality metrics of the current BF16 model. The most reproducible way is to run an eval suite over a fixed dataset and save the results:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install lm-evaluation-harness&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install lm-eval
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># BF16 baseline&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lm_eval --model vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model_args &lt;span class="nv">pretrained&lt;/span>&lt;span class="o">=&lt;/span>meta-llama/Meta-Llama-3.1-70B-Instruct,dtype&lt;span class="o">=&lt;/span>bfloat16 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --tasks mmlu,hellaswag,gsm8k &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num_fewshot &lt;span class="m">5&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --output_path ./results/baseline_bf16.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-2-enable-fp8-and-run-the-same-eval-suite">Step 2: enable FP8 and run the same eval suite&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># FP8&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lm_eval --model vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model_args &lt;span class="nv">pretrained&lt;/span>&lt;span class="o">=&lt;/span>neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8,quantization&lt;span class="o">=&lt;/span>fp8,kv_cache_dtype&lt;span class="o">=&lt;/span>fp8,calculate_kv_scales&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --tasks mmlu,hellaswag,gsm8k &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num_fewshot &lt;span class="m">5&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --output_path ./results/fp8_full.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-3-compute-the-degradation">Step 3: compute the degradation&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># compare_eval.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;results/baseline_bf16.json&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">baseline&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;results/fp8_full.json&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fp8&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">tasks&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;mmlu&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;hellaswag&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;gsm8k&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="s1">&amp;#39;Task&amp;#39;&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">&amp;lt;15&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="s1">&amp;#39;BF16&amp;#39;&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">&amp;gt;8&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="s1">&amp;#39;FP8&amp;#39;&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">&amp;gt;8&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="s1">&amp;#39;Delta&amp;#39;&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">&amp;gt;8&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="s1">&amp;#39;OK?&amp;#39;&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">&amp;gt;6&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;-&amp;#34;&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">task&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">tasks&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">b&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">baseline&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;results&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="n">task&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;acc,none&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">f&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">fp8&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;results&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="n">task&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;acc,none&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">delta&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">100&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ok&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;✓&amp;#34;&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nb">abs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">delta&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="mf">1.0&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="s2">&amp;#34;✗ REVIEW&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">task&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">&amp;lt;15&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">b&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">&amp;gt;8.3f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">f&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">&amp;gt;8.3f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">delta&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">&amp;gt;+7.1f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">% &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">ok&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">&amp;gt;6&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Decision thresholds documented in MLPerf Inference 2025:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&amp;lt; 0.5% degradation&lt;/strong>: enable in production without restrictions.&lt;/li>
&lt;li>&lt;strong>0.5% – 1.5%&lt;/strong>: enable with active quality monitoring via LLM-as-judge.&lt;/li>
&lt;li>&lt;strong>&amp;gt; 1.5%&lt;/strong>: investigate before enabling — a possible calibration problem or an incompatible model.&lt;/li>
&lt;/ul>
&lt;h3 id="step-4-domain-eval-with-llm-as-judge">Step 4: domain eval with LLM-as-judge&lt;/h3>
&lt;p>Academic benchmarks measure what they measure. Your use case may be different. Adding 200 representative samples from your domain, evaluated by an LLM judge, closes the gap:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># domain_eval.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langfuse&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Langfuse&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">openai&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">OpenAI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Langfuse&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">judge&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">OpenAI&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">base_url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://judge-llm:8000/v1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;token&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Load the 200 curated production samples (prompt + expected answer)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">samples&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">load_domain_samples&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;eval_dataset_200.json&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">scores_bf16&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">scores_fp8&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[],&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">sample&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">samples&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">model_type&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">endpoint&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="p">[(&lt;/span>&lt;span class="s2">&amp;#34;bf16&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;http://staging-bf16:8000&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fp8&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;http://staging-fp8:8000&amp;#34;&lt;/span>&lt;span class="p">)]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">response&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">call_model&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">endpoint&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sample&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;prompt&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">score&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">judge&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chat&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">completions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Qwen/Qwen2.5-72B-Instruct&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">messages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Rate this answer from 1 to 5 on accuracy and completeness.&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">Question: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">sample&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;prompt&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">Expected answer: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">sample&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;expected&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">Model answer: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">response&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Reply with a number from 1 to 5 only.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">choices&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">model_type&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;bf16&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">scores_bf16&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">score&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">scores_fp8&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">score&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">numpy&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">np&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Mean BF16 score: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">scores_bf16&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.2f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Mean FP8 score: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">scores_fp8&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.2f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Degradation: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">scores_fp8&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">scores_bf16&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">scores_bf16&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.1f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">%&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="otel--langfuse-correlation-the-dashboard-that-decides">OTel + Langfuse correlation: the dashboard that decides&lt;/h2>
&lt;p>The moment of decision rests on a single dashboard with two signals on the same time axis:&lt;/p>
&lt;p>&lt;strong>Signal 1 — Throughput (Prometheus):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">generation_tokens_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Signal 2 — Mean quality (Langfuse → Prometheus via exporter):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># If you have configured Langfuse with scores exported via OTel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">langfuse_score_value&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">llm_judge_domain&lt;/span>&lt;span class="p">&amp;#34;}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The expected pattern after enabling FP8: throughput rises by 40-60% and quality stays within ±0.1 points. If quality drops by more than 0.3 points and stays low, there is a real problem.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Alert: quality drops more than 0.2 points, sustained, after the change&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">ALERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">FP8CalidadDegradada&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">IF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kr">avg_over_time&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">langfuse_score_value&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">llm_judge_domain&lt;/span>&lt;span class="p">&amp;#34;}[&lt;/span>&lt;span class="s">30m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">avg_over_time&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">langfuse_score_value&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">llm_judge_domain&lt;/span>&lt;span class="p">&amp;#34;}[&lt;/span>&lt;span class="s">1d&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">offset&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">2h&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mf">0.2&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">15m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">LABELS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nl">severity&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">warning&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">ANNOTATIONS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nl">summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">Possible quality degradation after the FP8 configuration change&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="when-not-to-enable-fp8">When NOT to enable FP8&lt;/h2>
&lt;p>FP8 is not always the right answer. The cases where degradation exceeds the acceptable threshold:&lt;/p>
&lt;p>&lt;strong>Formal mathematical reasoning:&lt;/strong> GSM8K and MATH are the benchmarks most sensitive to FP8. If your use case is solving mathematical problems or precise financial calculation, measure specifically on these benchmarks before enabling it.&lt;/p>
&lt;p>&lt;strong>Critical code with tests:&lt;/strong> numerical precision affects the probability of tokens at key positions in a function. The risk is not that the code &amp;ldquo;looks&amp;rdquo; bad, but that it passes superficial tests while having subtle bugs.&lt;/p>
&lt;p>&lt;strong>Very long contexts without &lt;code>--calculate-kv-scales&lt;/code>:&lt;/strong> without dynamic scale calibration, the accumulated numerical error in the KV cache grows with the context. With &lt;code>--calculate-kv-scales&lt;/code> active, the impact is minimal up to 32K tokens.&lt;/p>
&lt;p>&lt;strong>Small models (&amp;lt;7B):&lt;/strong> the FP8 conversion overhead can exceed the throughput benefit. The break-even point is around 7B parameters.&lt;/p>
&lt;hr>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/multimodal-on-premise-serving-vlm-vllm/">Multimodal on-premise: serving a VLM with vLLM (vision + language)&lt;/a> — FP8 so that a VLM fits with its heavy budget of visual tokens.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">the maths of FP8 E4M3: what the 4-bit exponent and the 3-bit mantissa are, and why this specific format was chosen over INT8&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">the KV cache size formula: why moving to FP8 divides it exactly by two&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/optimising-decode-vllm-squeezing-every-token-small-hardware/">&lt;code>--kv-cache-dtype fp8&lt;/code> and &lt;code>--calculate-kv-scales&lt;/code> in the context of the full decode tuning&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/instrumenting-vllm-otel-measuring-optimisations/">how to set up the Langfuse + Prometheus correlation on a single dashboard for the FP8 before/after&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">the full eval suite: how to build the 200-sample domain dataset and the LLM judge that verifies quality&lt;/a>&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="in-this-same-series">In this same series&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/vllm-batch-sizing-two-hour-grid-search/">the max-num-seqs × max-num-batched-tokens grid search: the free optimisation with the biggest impact before touching quantisation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/prefix-cache-hit-rate-engineering/">prefix cache hit rate engineering: going from 15% to 75% without adding hardware&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/one-big-replica-or-many-small-ones-platform-decision/">TP=4×1 vs TP=2×2: the architectural decision that determines how to scale what FP8 frees up&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://vllm.ai/blog/2026-04-22-fp8-kvcache">The State of FP8 KV-Cache and Attention Quantization in vLLM — vLLM Blog (abril 2026)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/en/v0.8.5/features/quantization/fp8.html">FP8 W8A8 — vLLM Documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://mlcommons.org/benchmarks/inference-datacenter/">MLPerf Inference v5.1 — resultados de calidad FP8 (sep 2025)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://ifactoryapp.com/sap-integration/on-prem-ai/fp4-vs-fp8-vs-fp16-llm-inference">FP4 vs FP8 vs FP16 LLM Inference: Quality and Speed Tradeoffs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.spheron.network/blog/vllm-production-deployment-2026/">vLLM Production Deployment 2026: FP8 Docker Setup on H100&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>FlashAttention v1/v2/v3/v4: the librarian who never clears the desk — IO-awareness, async and Blackwell's asymmetry</title><link>https://blog.lo0.es/en/posts/flashattention-librarian-io-awareness-async-blackwell-asymmetry/</link><pubDate>Fri, 29 May 2026 17:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/flashattention-librarian-io-awareness-async-blackwell-asymmetry/</guid><description>&lt;blockquote>
&lt;p>This post complements &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention deep dive&lt;/a>. The KV cache post explains what is stored; PagedAttention, how it is managed in memory; FlashAttention, how the computation is executed. They are three different layers of the same problem and they stack multiplicatively.&lt;/p>
&lt;/blockquote>
&lt;h2 id="you-are-here-deploy">You are here: DEPLOY&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7ad88f;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#fam)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#fam)}&lt;/style>
&lt;defs>&lt;marker id="fam" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: DEPLOY · attention kernel, one layer below PagedAttention and KV cache&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The standard attention of a transformer has a structural problem on modern GPUs: when you look at it with a profiler, it is not compute-bound, it is &lt;strong>memory&lt;/strong>-bound. The &lt;code>S = QK^T&lt;/code> matrix of size &lt;code>N × N&lt;/code>, with N the sequence length, does not fit in the fast on-chip SRAM and forces round trips to HBM that dominate the total time. &lt;strong>FlashAttention&lt;/strong> is the family of kernels that avoids materialising that matrix by &lt;em>tiling&lt;/em> over Q, K and V, computing the softmax block by block with the &lt;em>online&lt;/em> version and keeping everything inside SRAM. Each version raises the utilisation ceiling: &lt;strong>FA1&lt;/strong> (Dao et al. 2022) abolished the N×N matrix and brought memory complexity down to O(N); &lt;strong>FA2&lt;/strong> (Dao 2023) parallelised along the sequence dimension and cut non-matmul FLOPs to get close to 70 % utilisation on A100; &lt;strong>FA3&lt;/strong> (Shah, Bikshandi, Zhang, Thakkar, Ramani, Dao 2024) exploited three Hopper-specific mechanisms, async WGMMA, TMA and FP8, to reach 740 TFLOPS BF16 (75 % of peak) and 1.2 PFLOPS FP8 on H100; &lt;strong>FA4&lt;/strong> (March 2026) rewrote the kernel from scratch for Blackwell, where the tensor core scales 2.25× but the SFU (where the softmax &lt;code>exp&lt;/code> runs) and the SMEM bandwidth do not scale at all. The answer is a software-emulated exponential that runs on tensor cores. The result on B200: 1,605 TFLOPS BF16, 1.3× faster than cuDNN 9.13 and 2× faster than FA3 on the same GPU. This post takes apart the why (memory roofline, IO complexity), the master analogy of the librarian, the minimum mathematics and the real numbers on H100 and B200.&lt;/p>
&lt;h2 id="the-analogy-the-librarian-who-never-clears-the-desk">The analogy: the librarian who never clears the desk&lt;/h2>
&lt;p>A large library with two zones: a very fast but small work desk (the librarian&amp;rsquo;s desk, 200 books fit on top of it), and a giant shelving system spanning three floors where everything lives (50 million books). The librarian has to cross-reference all the books in one thematic room and produce a summary.&lt;/p>
&lt;p>A &lt;strong>naive librarian&lt;/strong> does it the direct way: fetches every relevant book from the shelves, piles them on the work desk, and since they do not fit, leaves half of them on the floor, on the chair, on top of boxes. The day is spent running between the floor and the desk, opening and closing books, never able to concentrate. The work desk is extremely fast but underused, because most of the time the librarian is moving books between the floor and the desk. That is what &lt;strong>standard attention&lt;/strong> does: it materialises the &lt;code>S = QK^T&lt;/code> matrix in HBM and keeps coming back for it piece by piece.&lt;/p>
&lt;p>A &lt;strong>FlashAttention v1 librarian&lt;/strong> changes strategy: asks for one shelf at a time, brings it to the desk, reads what is needed, jots notes in a compact notebook (&amp;ldquo;from this shelf I care about this, this and this, with these relative weights&amp;rdquo;), returns the shelf to its place and brings the next one. The notebook is the only thing that carries over between shelves. Nothing is ever piled higher than the desk can hold. The trick that makes this possible is the &lt;em>online softmax&lt;/em>: instead of needing all the content at once in order to normalise, it keeps a running max and a running sum that are updated shelf by shelf.&lt;/p>
&lt;p>A &lt;strong>FlashAttention v2 librarian&lt;/strong> realises that several topics can be worked in parallel, because the desk is big and the shelves are independent along some axes. Three assistants are put to work, each with a notebook, each covering a different block of the room, and the results are combined at the end.&lt;/p>
&lt;p>A &lt;strong>FlashAttention v3 librarian&lt;/strong> manages to mechanise the flow: a conveyor belt with two stations is installed. While station A reads the current shelf and takes notes, station B already has the next shelf in transit from the shelving system. When A finishes, B hands over the new shelf with no waiting. This is the &lt;strong>ping-pong producer/consumer&lt;/strong> pattern: the TMA offloads the data loading and the consumer warps do the work in parallel with the loads. On top of that, the notes are written in lower-precision shorthand (FP8), because the pages that matter have already had an orthogonal pre-treatment that stops them losing precision where it hurts.&lt;/p>
&lt;p>A &lt;strong>FlashAttention v4 librarian&lt;/strong> discovers something new about the remodelled library (Blackwell): two much faster conveyor belts have been installed (tensor cores 2.25×), but the shorthand typewriter has not been upgraded (the SFU is unchanged). Now the bottleneck is writing the notes, not fetching the shelves. The fix is elegant: instead of using the shorthand machine, the notes are written with polynomial formulas that the tensor core itself can evaluate (software-emulated exponential). The belt no longer sits idle waiting for the machine.&lt;/p>
&lt;p>The analogy holds up with four mappings:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Work desk&lt;/strong> = SRAM per SM (228 KB on H100, 256 KB on B200).&lt;/li>
&lt;li>&lt;strong>Giant shelving system&lt;/strong> = HBM (3.35 TB/s on H100, 8 TB/s on B200).&lt;/li>
&lt;li>&lt;strong>Notebook with running max and sum&lt;/strong> = online softmax stats (&lt;code>m&lt;/code>, &lt;code>ℓ&lt;/code>).&lt;/li>
&lt;li>&lt;strong>Conveyor belt with two stations&lt;/strong> = TMA + WGMMA producer/consumer pipeline.&lt;/li>
&lt;/ul>
&lt;h2 id="why-standard-attention-was-the-bottleneck">Why standard attention was the bottleneck&lt;/h2>
&lt;p>The naive intuition, &amp;ldquo;attention is matmuls, and GPUs are good at matmuls&amp;rdquo;, is correct but incomplete. There are two matmuls (&lt;code>QK^T&lt;/code> and then &lt;code>softmax(S) V&lt;/code>), and in between a non-matmul operation (softmax) that requires materialising the intermediate matrix &lt;code>S&lt;/code> of size &lt;code>N × N&lt;/code>.&lt;/p>
&lt;p>Modern GPUs have a very concrete roofline. For H100 SXM5:&lt;/p>
&lt;ul>
&lt;li>Compute peak: 989 TFLOPS BF16 (tensor core, dense, without sparsity).&lt;/li>
&lt;li>Memory bandwidth: 3.35 TB/s HBM3.&lt;/li>
&lt;li>Break-even point (arithmetic intensity break-even): &lt;code>989 × 10¹² / (3.35 × 10¹²) ≈ 295 FLOP/byte&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>Any operation that fails to perform 295 operations for every byte it moves from HBM is memory-bound: the tensor core sits waiting for data. On B200 the ratio is similar (~281 FLOP/byte) because both compute and bandwidth went up.&lt;/p>
&lt;p>Standard attention materialising &lt;code>S&lt;/code> reads the matrix twice (once for the softmax, once to multiply by V) and writes it once. For Llama 3 70B with head dim &lt;code>d = 128&lt;/code> and context &lt;code>N = 128K&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>Matrix &lt;code>S&lt;/code> per head per layer: &lt;code>N × N × 2 bytes = 128K × 128K × 2 = 34.36 GB&lt;/code>.&lt;/li>
&lt;li>80 layers × 64 Q-heads → aggregate HBM traffic (if it were materialised serially) on the order of TBs, prohibitive in transient.&lt;/li>
&lt;li>Even if it is not all materialised at once, the round trips dominate the time: effective arithmetic intensity well below 295 → memory-bound operation → tensor core underused at around 25 %.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>FlashAttention does not change the mathematics of attention, it changes the order of the operations so that S is never materialised in HBM&lt;/strong>. Its IO complexity is &lt;code>Θ(N²·d²/M)&lt;/code> with M = SRAM size per SM, against &lt;code>Θ(N·d + N²)&lt;/code> for standard attention. With d = 128 and M = 228 KB: an approximate reduction factor of M/d² ≈ 14× less HBM traffic. That is what moves the operation from memory-bound to nearly compute-bound.&lt;/p>
&lt;h2 id="the-trick-that-made-it-all-possible-online-softmax">The trick that made it all possible: online softmax&lt;/h2>
&lt;p>Without online softmax there is no FlashAttention. The idea comes from Milakov and Gimelshein, 2018 (the paper &amp;ldquo;Online normalizer calculation for softmax&amp;rdquo;, arXiv:1805.02867), and it allows &lt;code>softmax([x_1, ..., x_N])&lt;/code> to be computed in one incremental pass without needing to know the global maximum before starting.&lt;/p>
&lt;p>The standard softmax is:&lt;/p>
$$\text{softmax}(x_i) = \frac{e^{x_i - m}}{\sum_{j} e^{x_j - m}}, \quad m = \max_j x_j$$
&lt;p>The &lt;em>online&lt;/em> trick: keep a running maximum &lt;code>m^{(t)}&lt;/code> and a running sum &lt;code>ℓ^{(t)}&lt;/code>. When a new block of values arrives with local maximum &lt;code>m_{\text{new}}&lt;/code>:&lt;/p>
$$m^{(t+1)} = \max(m^{(t)}, m_{\text{new}})$$
$$\ell^{(t+1)} = e^{m^{(t)} - m^{(t+1)}} \cdot \ell^{(t)} + \sum_{j \in \text{new}} e^{x_j - m^{(t+1)}}$$
&lt;p>And the accumulated partial outputs are rescaled by the same factor &lt;code>e^{m^{(t)} - m^{(t+1)}}&lt;/code>. At the end, dividing by the final &lt;code>ℓ&lt;/code> gives exactly the same result as the standard softmax. It is mathematically exact, not an approximation.&lt;/p>
&lt;p>This is what allows K to be traversed block by block without materialising the whole &lt;code>S&lt;/code> matrix. Each block updates the stats and the accumulated outputs, and is then discarded. The desk never fills up.&lt;/p>
&lt;h2 id="the-four-versions-may-2026">The four versions (May 2026)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;strong>FA1&lt;/strong> (2022)&lt;/th>
&lt;th>&lt;strong>FA2&lt;/strong> (2023)&lt;/th>
&lt;th>&lt;strong>FA3&lt;/strong> (2024)&lt;/th>
&lt;th>&lt;strong>FA4&lt;/strong> (2026)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Target GPU&lt;/td>
&lt;td>A100 / Ampere&lt;/td>
&lt;td>A100 / H100&lt;/td>
&lt;td>H100 / Hopper&lt;/td>
&lt;td>B200 / Blackwell&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Core idea&lt;/td>
&lt;td>Tiling + online softmax&lt;/td>
&lt;td>Sequence parallelism + work partitioning&lt;/td>
&lt;td>Async WGMMA + TMA + FP8&lt;/td>
&lt;td>Polynomial exp + 2-CTA tensor cores&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Memory&lt;/td>
&lt;td>O(N) (vs O(N²))&lt;/td>
&lt;td>same&lt;/td>
&lt;td>same&lt;/td>
&lt;td>same&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Typical peak util&lt;/td>
&lt;td>~25 % A100&lt;/td>
&lt;td>~70 % A100, ~35 % H100&lt;/td>
&lt;td>&lt;strong>75 % H100 BF16, 60 % H100 FP8&lt;/strong>&lt;/td>
&lt;td>&lt;strong>71 % B200 BF16&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Effective TFLOPS&lt;/td>
&lt;td>—&lt;/td>
&lt;td>225 TFLOPS A100 BF16&lt;/td>
&lt;td>740 H100 BF16, 1,200 H100 FP8&lt;/td>
&lt;td>&lt;strong>1,605 B200 BF16&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Speedup vs previous&lt;/td>
&lt;td>2-4× standard&lt;/td>
&lt;td>2× FA1&lt;/td>
&lt;td>1.5-2× FA2 (BF16), 2.6× (FP8)&lt;/td>
&lt;td>2× FA3 on B200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Paper&lt;/td>
&lt;td>arXiv:2205.14135&lt;/td>
&lt;td>arXiv:2307.08691&lt;/td>
&lt;td>arXiv:2407.08608&lt;/td>
&lt;td>arXiv:2603.05451&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="fa1--changing-the-order-of-the-operations">FA1 — changing the order of the operations&lt;/h3>
&lt;p>Three ideas combined: tiling of Q/K/V into blocks that fit in SRAM, online softmax over those blocks, and recomputation in the backward pass. The N×N matrix S is not stored, only the stats &lt;code>(m, ℓ)&lt;/code>, and in the backward pass S is recomputed block by block from Q, K and the stats. Result: 7.6× speedup on GPT-2 versus PyTorch standard attention, memory O(N).&lt;/p>
&lt;h3 id="fa2--parallelising-along-the-sequence">FA2 — parallelising along the sequence&lt;/h3>
&lt;p>FA1 parallelised only over batch × heads. With a small batch (1-4) and models with few heads or aggressive GQA, the GPU was left with idle SMs. FA2 also parallelises along the sequence dimension: different SMs process different stretches of Q at the same time. It also rewrites the algorithm to minimise the non-matmul operations (softmax rescaling), because those do not go through tensor cores. And it improves work partitioning between warps (split-Q instead of split-K reduces shared memory traffic). Result: around 2× over FA1 on H100 and A100, 225 TFLOPS on A100 (72 % MFU). On H100 it stays around 30-35 % of BF16 peak because it does not exploit async WGMMA.&lt;/p>
&lt;h3 id="fa3--the-hopper-moment">FA3 — the Hopper moment&lt;/h3>
&lt;p>This is where FlashAttention stops being an algorithm and becomes a Hopper-specific artefact. Three pillars:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Async WGMMA&lt;/strong>: Hopper&amp;rsquo;s new tensor core instructions let a warpgroup fire a GEMM and have the rest of the warpgroup do something else (the softmax, for instance) while the tensor core keeps working. This is the trick that unlocks matmul/softmax overlap.&lt;/li>
&lt;li>&lt;strong>TMA (Tensor Memory Accelerator)&lt;/strong>: dedicated hardware for copying tiles between HBM and SRAM. It frees the SM from computing indices and predicating out-of-bounds accesses, which used to consume SM cycles. It is the equivalent of hiring warehouse staff: the librarian no longer has to carry the books personally.&lt;/li>
&lt;li>&lt;strong>FP8 with block quantisation + incoherent processing&lt;/strong>: quantising Q and K to FP8 doubles tensor core throughput. The precision loss is mitigated with two tricks: a per-tile scale (64×d) instead of a whole-tensor scale, and a pre-multiplication by a random orthogonal Hadamard-based matrix that &amp;ldquo;spreads&amp;rdquo; the outliers before quantising. Documented result: numerical error 2.6× lower than the FP8 baseline.&lt;/li>
&lt;/ol>
&lt;p>These three pillars combine with producer/consumer warp specialisation (producer warps do TMA loads; consumer warps do WGMMA + softmax) and ping-pong scheduling with two warpgroups taking turns so that there are never pipeline bubbles. When WG1 does softmax, WG2 does GEMM; then they swap.&lt;/p>
&lt;p>Numbers: 740 TFLOPS BF16 on H100 (75 % of the 989 peak), 1.2 PFLOPS FP8 (60 % of the 1978 FP8 dense peak). For sequences ≥ 1K it beats cuDNN. Speedup over FA2: 1.5-2× BF16, 2.6× FP8.&lt;/p>
&lt;h3 id="fa4--blackwells-asymmetry">FA4 — Blackwell&amp;rsquo;s asymmetry&lt;/h3>
&lt;p>Blackwell scaled everything unevenly:&lt;/p>
&lt;ul>
&lt;li>Tensor core BF16 throughput: 1 PFLOP H100 → 2.25 PFLOPS B200 (2.25×).&lt;/li>
&lt;li>SFU count (where the softmax &lt;code>exp&lt;/code> runs): unchanged.&lt;/li>
&lt;li>Shared memory bandwidth: unchanged.&lt;/li>
&lt;/ul>
&lt;p>In other words, if FA3 runs as-is on a B200 with no changes, the matmul goes twice as fast but the softmax stays exactly the same, and that stalls the pipeline. It was only a matter of time before someone fixed the imbalance.&lt;/p>
&lt;p>FA4 (March 2026, same team of Dao + Princeton + Together AI + Meta + NVIDIA + Colfax) is a ground-up rewrite with three ideas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Software-emulated exponential&lt;/strong>: a polynomial approximation of &lt;code>exp&lt;/code> that runs on the tensor core instead of on the SFU. It loses a little precision (carefully bounded and compensated by the rest of the kernel) but keeps the conveyor belt moving.&lt;/li>
&lt;li>&lt;strong>Conditional softmax rescaling&lt;/strong>: avoids rescaling accumulators when the running max does not change significantly. A &amp;ldquo;lazy&amp;rdquo; style optimisation: it only pays the cost when the cost is needed.&lt;/li>
&lt;li>&lt;strong>2-CTA tensor core&lt;/strong>: two CTAs (Cooperative Thread Arrays) cooperate to feed the tensor cores with larger tiles. It gets more out of Blackwell&amp;rsquo;s new capabilities.&lt;/li>
&lt;/ol>
&lt;p>Written in CuTeDSL (NVIDIA&amp;rsquo;s CUTLASS Python DSL, not CUDA C++ directly). Result on B200 BF16: 1,605 TFLOPS (71 % of the 2250 peak). 1.3× over cuDNN 9.13. 2.7× over Triton. 2× over FA3 run as-is on B200 (which was the previous baseline). It is the first attention kernel to pass 1 PFLOPS.&lt;/p>
&lt;blockquote>
&lt;p>Note: there is recurring confusion about &amp;ldquo;FP4 attention&amp;rdquo;. Blackwell&amp;rsquo;s NVFP4/MXFP4 extensions apply to weights, not to attention. FA4 can be combined with NVFP4 weights, but the attention computation itself is still BF16 or FP8 depending on the configuration. FP4 quantisation of QK^T does exist in some proprietary kernels (Fireworks AI&amp;rsquo;s FireAttention V4 combines it) but it is not standard practice.&lt;/p>
&lt;/blockquote>
&lt;h2 id="implementations-and-libraries-in-2026">Implementations and libraries in 2026&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Dao-AILab/flash-attention&lt;/strong> (the canonical repo): supports SM 8.0 (Ampere) with FA2, SM 9.0 (Hopper) with FA3, SM 10.0 (Blackwell datacenter B100/B200/B300) with FA4. The consumer Blackwell version (5090, SM 12.0) has partial support as this post goes out.&lt;/li>
&lt;li>&lt;strong>FlashInfer&lt;/strong> (flashinfer-ai/flashinfer, arXiv:2501.01005): an attention engine aimed at &lt;em>serving&lt;/em> (not training). Its conceptual contribution is the &lt;strong>Block-Sparse Row (BSR)&lt;/strong>, a unified abstraction covering paged KV cache, the radix tree of prefix caching and the tree masks of speculative decoding. Internally it can call FA2/FA3, cuDNN, CUTLASS or trtllm-gen FMHA kernels depending on the case. It JIT-compiles specific variants at runtime. Integrated into vLLM, SGLang and TensorRT-LLM.&lt;/li>
&lt;li>&lt;strong>vLLM (May 2026)&lt;/strong>: automatic backend selection by GPU. Default &lt;strong>FA4 on SM 10.0+, FA3 on SM 9.0, FA2 on the rest&lt;/strong>. Fallbacks on Blackwell: TRT-LLM Ragged → FlashInfer → TokenSpeed MLA. For FP8 KV cache on B200, FlashInfer is competitive.&lt;/li>
&lt;li>&lt;strong>SGLang&lt;/strong>: uses FlashInfer as its attention backend; RadixAttention is the prefix caching layer on top (a radix tree over the KV cache).&lt;/li>
&lt;li>&lt;strong>TensorRT-LLM&lt;/strong>: its own fused kernels (trtllm-gen FMHA). XQA is NVIDIA&amp;rsquo;s own optimisation for GQA in decode.&lt;/li>
&lt;li>&lt;strong>PyTorch SDPA and FlexAttention&lt;/strong>: &lt;code>torch.nn.functional.scaled_dot_product_attention&lt;/code> selects the backend automatically. &lt;strong>FlexAttention&lt;/strong> (new) lets you define custom masks declaratively and compiles to kernels that can use FA4 as a backend.&lt;/li>
&lt;li>&lt;strong>xFormers&lt;/strong>: still alive but residual. The built-in PyTorch SDPA covers most cases.&lt;/li>
&lt;/ul>
&lt;h2 id="cases-where-flashattention-does-not-help">Cases where FlashAttention does not help&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Very short contexts (N &amp;lt; 512)&lt;/strong>: the tiling and kernel launch overhead does not pay off; cuDNN can win.&lt;/li>
&lt;li>&lt;strong>Non-standard custom masks&lt;/strong>: FA ships causal, sliding window and ALiBi only. Arbitrary masks need FlexAttention or the JIT variants of FlashInfer.&lt;/li>
&lt;li>&lt;strong>Non-standard head dim&lt;/strong>: FA optimises for d = 64, 128, 256. Odd dimensions (d = 96, d = 192) fall into slow paths.&lt;/li>
&lt;li>&lt;strong>GQA/MQA with extreme ratios&lt;/strong>: natively supported, but the speedup versus pure MHA depends on the Q-heads : KV-heads ratio.&lt;/li>
&lt;li>&lt;strong>Cross-attention&lt;/strong>: supported but less optimised; self-attention is where the gain is largest.&lt;/li>
&lt;li>&lt;strong>FP8 without block quantisation or incoherent processing&lt;/strong>: it loses several points in benchmarks. If your serving framework does not implement the two tricks from the FA3 paper, FP8 attention can be a bad idea.&lt;/li>
&lt;/ul>
&lt;h2 id="implications-on-on-premise-hardware">Implications on on-premise hardware&lt;/h2>
&lt;h3 id="on-an-rtx-4090-24-gb-ada-lovelace-sm-89">On an RTX 4090 (24 GB, Ada Lovelace, SM 8.9)&lt;/h3>
&lt;p>The 4090 is Ada Lovelace, not Hopper. &lt;strong>It runs neither FA3 nor FA4&lt;/strong>; it runs FA2. That means around 70 % utilisation in BF16 attention (~250 effective TFLOPS against the 4090&amp;rsquo;s 330 TFLOPS BF16 peak). It is not a disaster, FA2 is already very good compared with standard attention, but the ceiling is clearly below that of an H100. For consumer deploys on a 4090 with Llama 3 8B BF16 or any 14B-32B INT4 AWQ, FA2 is what you will be using, and it is perfectly reasonable.&lt;/p>
&lt;h3 id="on-a-generic-4h100-sxm-cluster-320-gb-nvlink-native-fp8">On a generic 4×H100 SXM cluster (320 GB, NVLink, native FP8)&lt;/h3>
&lt;p>This is where FA3 shines and it is what vLLM/SGLang/TRT-LLM will select by default. Two common configurations:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3 70B FP8 with FA3 FP8 attention&lt;/strong>: 1.2 PFLOPS peak on the GPU, aggregate cluster throughput on the order of 8,000-12,000 tokens/s at medium batch depending on TP and context. For FP8 attention to deliver its full performance it is crucial to use the block quantisation + incoherent processing techniques from the FA3 paper (they are enabled by default in vLLM).&lt;/li>
&lt;li>&lt;strong>DeepSeek-V3 671B FP8 + MLA with FlashInfer&lt;/strong>: DeepSeek uses &lt;strong>Multi-head Latent Attention&lt;/strong> (MLA), a different variant of standard attention. FlashInfer has specific kernels (FlashMLA). The typical stack is vLLM/SGLang + FlashInfer + FlashMLA + an FA3 fallback for the non-MLA layers.&lt;/li>
&lt;/ul>
&lt;p>If the infrastructure is Blackwell (B200/B300, which some clusters start receiving in 2026), &lt;strong>FA4 is the right option&lt;/strong> and it should be enabled by default in vLLM 0.16+ and SGLang 0.5.11+.&lt;/p>
&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>MLA (Multi-head Latent Attention)&lt;/strong> from DeepSeek and the specific FlashMLA kernels: they optimise KV cache compression but require different kernels.&lt;/li>
&lt;li>&lt;strong>Flexible masking and the FlexAttention use cases&lt;/strong> (PyTorch 2.5+): how to declare arbitrary masks without paying the cost of a custom kernel.&lt;/li>
&lt;li>&lt;strong>Hardware assistance for sparse attention&lt;/strong> (NVIDIA 2:4 sparse tensor cores) and why sparse attention has not established itself as a higher ceiling than dense FA.&lt;/li>
&lt;li>&lt;strong>FA in the backward pass of fine-tuning&lt;/strong>: this post focuses on inference, but FA3/FA4 also go through the backward pass and they are what makes training models with long contexts viable on H100/B200.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/vllm-attention-backend-flashattention-flashinfer-prefill-decode-asymmetry/">The attention backend in vLLM (FlashAttention vs FlashInfer)&lt;/a> — the level above this post: how the engine chooses between FA2/FA3/FA4 and FlashInfer according to the architecture, and why prefill (compute-bound) and decode (memory-bound) call for different kernels.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory that holds up LLM inference&lt;/a> — the KV cache is what FlashAttention traverses and multiplies against Q on every iteration; understanding one requires understanding the other.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention deep dive&lt;/a> — PagedAttention organises the KV cache into non-contiguous physical blocks; FlashAttention is the kernel that iterates over those blocks. Different layers of the same problem.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">Quantisation for LLM inference&lt;/a> — FA3&amp;rsquo;s FP8 attention with block quantisation and Blackwell&amp;rsquo;s FP4 weights stack up; this post gives the general quantisation framework.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">Disaggregated serving: prefill and decode in specialised pods&lt;/a> — prefill is compute-bound and benefits a great deal from FA3/FA4 FP8; decode is memory-bound and benefits less but still benefits. Separating them allows the kernel to be optimised per phase.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">Speculative decoding: the secretary who anticipates what the boss is going to say&lt;/a> — speculative decoding produces more tokens per forward pass; FlashAttention makes each forward pass cheaper. Multiplicative levers.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/moe-inference-fundamentals-expert-parallel-deepseek-economics/">MoE inference: the call centre with 256 specialists&lt;/a> — attention is still dense in every MoE of 2026; FlashAttention is reused as-is. FlashMLA is the specific variant for DeepSeek&amp;rsquo;s Multi-head Latent Attention (latent compression of the KV cache) that extends the FA pattern with one extra step.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the master map where Deploy is stage 4.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Dao, T., Fu, D., Ermon, S., Rudra, A., Ré, C. &lt;em>FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness&lt;/em>. NeurIPS 2022. &lt;a href="https://arxiv.org/abs/2205.14135">https://arxiv.org/abs/2205.14135&lt;/a>&lt;/li>
&lt;li>Dao, T. &lt;em>FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning&lt;/em>. 2023. &lt;a href="https://arxiv.org/abs/2307.08691">https://arxiv.org/abs/2307.08691&lt;/a>&lt;/li>
&lt;li>Shah, J., Bikshandi, G., Zhang, Y., Thakkar, V., Ramani, P., Dao, T. &lt;em>FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision&lt;/em>. NeurIPS 2024. &lt;a href="https://arxiv.org/abs/2407.08608">https://arxiv.org/abs/2407.08608&lt;/a>&lt;/li>
&lt;li>Dao, T. et al. &lt;em>FlashAttention-4: Algorithm and Kernel Pipelining Co-Design for Asymmetric Hardware Scaling&lt;/em>. 2026. &lt;a href="https://arxiv.org/abs/2603.05451">https://arxiv.org/abs/2603.05451&lt;/a>&lt;/li>
&lt;li>Milakov, M., Gimelshein, N. &lt;em>Online normalizer calculation for softmax&lt;/em>. 2018. &lt;a href="https://arxiv.org/abs/1805.02867">https://arxiv.org/abs/1805.02867&lt;/a>&lt;/li>
&lt;li>Ye, Z. et al. &lt;em>FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving&lt;/em>. MLSys 2025. &lt;a href="https://arxiv.org/abs/2501.01005">https://arxiv.org/abs/2501.01005&lt;/a>&lt;/li>
&lt;li>Tri Dao FA3 blog: &lt;a href="https://tridao.me/blog/2024/flash3/">https://tridao.me/blog/2024/flash3/&lt;/a>&lt;/li>
&lt;li>Tri Dao FA4 blog: &lt;a href="https://tridao.me/blog/2026/flash4/">https://tridao.me/blog/2026/flash4/&lt;/a>&lt;/li>
&lt;li>PyTorch FlashAttention-3 announcement: &lt;a href="https://pytorch.org/blog/flashattention-3/">https://pytorch.org/blog/flashattention-3/&lt;/a>&lt;/li>
&lt;li>PyTorch FlexAttention + FA4: &lt;a href="https://pytorch.org/blog/flexattention-flashattention-4-fast-and-flexible/">https://pytorch.org/blog/flexattention-flashattention-4-fast-and-flexible/&lt;/a>&lt;/li>
&lt;li>Together AI FA4 blog: &lt;a href="https://www.together.ai/blog/flashattention-4">https://www.together.ai/blog/flashattention-4&lt;/a>&lt;/li>
&lt;li>Colfax Research FA3: &lt;a href="https://research.colfax-intl.com/flashattention-3-fast-and-accurate-attention-with-asynchrony-and-low-precision/">https://research.colfax-intl.com/flashattention-3-fast-and-accurate-attention-with-asynchrony-and-low-precision/&lt;/a>&lt;/li>
&lt;li>Colfax Research FA4: &lt;a href="https://research.colfax-intl.com/flashattention-4-algorithm-and-kernel-pipelining-co-design-for-asymmetric-hardware-scaling/">https://research.colfax-intl.com/flashattention-4-algorithm-and-kernel-pipelining-co-design-for-asymmetric-hardware-scaling/&lt;/a>&lt;/li>
&lt;li>Repo Dao-AILab/flash-attention: &lt;a href="https://github.com/Dao-AILab/flash-attention">https://github.com/Dao-AILab/flash-attention&lt;/a>&lt;/li>
&lt;li>Repo flashinfer-ai/flashinfer: &lt;a href="https://github.com/flashinfer-ai/flashinfer">https://github.com/flashinfer-ai/flashinfer&lt;/a>&lt;/li>
&lt;li>vLLM attention backends: &lt;a href="https://docs.vllm.ai/en/latest/design/attention_backends/">https://docs.vllm.ai/en/latest/design/attention_backends/&lt;/a>&lt;/li>
&lt;li>NVIDIA Hopper Architecture in Depth: &lt;a href="https://developer.nvidia.com/blog/nvidia-hopper-architecture-in-depth/">https://developer.nvidia.com/blog/nvidia-hopper-architecture-in-depth/&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Quantization for LLM inference: FP8, INT4 (GPTQ, AWQ) and GGUF — the model's accounting zoom</title><link>https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/</link><pubDate>Wed, 27 May 2026 11:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/</guid><description>&lt;blockquote>
&lt;p>This post complements &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory of LLM inference&lt;/a>, where cache quantisation is mentioned as a savings lever; here we go into the whole method, model weights and cache, and into why each format does what it does.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Quantising is a change of representation: instead of storing every model weight as a &lt;code>float16&lt;/code> or &lt;code>bfloat16&lt;/code> (2 bytes), it is stored as a short integer (1 byte INT8, half a byte INT4) with a scale factor that reconstructs a value close to the original. The price is loss of numerical precision; the reward is 2-4× less VRAM, 2-3× more throughput and, on Hopper and Blackwell, a radically lower compute cost because FP8/FP4 units execute in fewer cycles than BF16 ones. The four dominant formats in May 2026 are &lt;strong>FP8&lt;/strong> (E4M3/E5M2, datacenter), &lt;strong>INT4 GPTQ&lt;/strong> (Hessian-aware reconstruction), &lt;strong>INT4 AWQ&lt;/strong> (activation-aware) and &lt;strong>GGUF&lt;/strong> (the llama.cpp family). Each has a sweet spot: FP8 when the datacenter is Hopper/Blackwell and quality matters; GPTQ and AWQ when serving runs on Ampere/Ada and 4 bits are mandatory; GGUF when the target is edge or a consumer GPU. This post explains the minimum maths, the algorithms behind each format, what each one loses measured in perplexity and MMLU, and how it all applies on a 4090 versus an H100 cluster.&lt;/p>
&lt;h2 id="you-are-here-deploy">You are here: DEPLOY&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7ad88f;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#qm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#qm)}&lt;/style>
&lt;defs>&lt;marker id="qm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: DEPLOY · weight and KV cache quantization&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-analogy-jpeg-with-an-edge-detector">The analogy: JPEG with an edge detector&lt;/h2>
&lt;p>A JPEG compresses an image by reducing the precision with which pixels are stored, but it does not reduce it uniformly. Where there is flat sky, thousands of very similar pixels, it throws away detail without anyone noticing. Where there is a sharp edge, the outline of a face, it keeps the fidelity. The trick is to detect which parts are sensitive before compressing.&lt;/p>
&lt;p>Quantization of an LLM works the same way. You do not take every model weight and say &amp;ldquo;all of them in 4 bits&amp;rdquo;. Some weights are very important, projection weights that move the output a lot when they change, and others are less so. Modern techniques (GPTQ, AWQ) are basically &lt;strong>edge detectors&lt;/strong>: they identify which weights can be quantised aggressively and which need more bits or special treatment, and they apply the quantisation with that information.&lt;/p>
&lt;p>The analogy holds up in three details:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Calibration with a small dataset&lt;/strong> = the phase in which the JPEG encoder analyses the image before choosing blocks.&lt;/li>
&lt;li>&lt;strong>A block of 128 weights with a shared scale&lt;/strong> = the JPEG 8×8 block with its DCT.&lt;/li>
&lt;li>&lt;strong>Outliers preserved with more precision&lt;/strong> = the high frequencies of an edge are preserved more than the flat areas.&lt;/li>
&lt;/ul>
&lt;p>From there on, what follows is the maths and the operational detail.&lt;/p>
&lt;h2 id="the-minimum-maths-scale-and-zero-point">The minimum maths: scale and zero-point&lt;/h2>
&lt;p>Quantising a weight vector &lt;code>w ∈ ℝ^n&lt;/code> (in BF16) to INT4 means finding two things:&lt;/p>
&lt;ul>
&lt;li>A &lt;strong>scale&lt;/strong> &lt;code>s ∈ ℝ&lt;/code> (in BF16 or FP16).&lt;/li>
&lt;li>For each weight, an integer &lt;strong>code&lt;/strong> &lt;code>q ∈ {0, 1, ..., 15}&lt;/code> that fits in 4 bits.&lt;/li>
&lt;/ul>
&lt;p>And an approximate reconstruction formula:&lt;/p>
$$\hat{w}_i \approx s \cdot (q_i - z)$$
&lt;p>where &lt;code>z&lt;/code> is the &lt;strong>zero-point&lt;/strong> (the integer that defines which code represents the original zero). The zero-point exists in asymmetric INT4/INT8 so that half the range is not wasted on negative values when the weight distribution is not symmetric.&lt;/p>
&lt;p>The choice of &lt;code>s&lt;/code> and &lt;code>z&lt;/code> for a block of weights &lt;code>w_block&lt;/code>:&lt;/p>
$$s = \frac{\max(w_\text{block}) - \min(w_\text{block})}{2^{b} - 1}, \quad z = -\frac{\min(w_\text{block})}{s},$$
&lt;p>with &lt;code>b&lt;/code> = number of bits (4 in INT4). Encoding:&lt;/p>
$$q_i = \text{clip}\!\left(\text{round}\!\left(\frac{w_i}{s} + z\right),\, 0,\, 2^b - 1\right).$$
&lt;p>And decoding at inference time:&lt;/p>
$$\hat{w}_i = s \cdot (q_i - z).$$
&lt;h3 id="numerical-example">Numerical example&lt;/h3>
&lt;p>Take 8 real weights from a linear layer: &lt;code>w = [0.31, -0.12, 0.78, -0.05, 1.42, -0.91, 0.23, 0.66]&lt;/code>. We want to quantise to INT4 (16 levels).&lt;/p>
&lt;p>&lt;code>max = 1.42&lt;/code>, &lt;code>min = -0.91&lt;/code>. Range = 2.33.&lt;/p>
$$s = \frac{2.33}{15} \approx 0.1553, \quad z = -\frac{-0.91}{0.1553} \approx 5.86 \to 6.$$
&lt;p>Encoding of each weight:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;code>w_i&lt;/code>&lt;/th>
&lt;th>&lt;code>w_i/s + z&lt;/code>&lt;/th>
&lt;th>&lt;code>round&lt;/code>&lt;/th>
&lt;th>&lt;code>q_i&lt;/code>&lt;/th>
&lt;th>&lt;code>ŵ_i = s·(q-z)&lt;/code>&lt;/th>
&lt;th>error&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0.31&lt;/td>
&lt;td>8.00&lt;/td>
&lt;td>8&lt;/td>
&lt;td>8&lt;/td>
&lt;td>0.311&lt;/td>
&lt;td>+0.001&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-0.12&lt;/td>
&lt;td>5.23&lt;/td>
&lt;td>5&lt;/td>
&lt;td>5&lt;/td>
&lt;td>-0.155&lt;/td>
&lt;td>-0.035&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.78&lt;/td>
&lt;td>11.02&lt;/td>
&lt;td>11&lt;/td>
&lt;td>11&lt;/td>
&lt;td>0.776&lt;/td>
&lt;td>-0.004&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-0.05&lt;/td>
&lt;td>5.68&lt;/td>
&lt;td>6&lt;/td>
&lt;td>6&lt;/td>
&lt;td>0.000&lt;/td>
&lt;td>+0.050&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1.42&lt;/td>
&lt;td>15.14&lt;/td>
&lt;td>15&lt;/td>
&lt;td>15&lt;/td>
&lt;td>1.398&lt;/td>
&lt;td>-0.022&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-0.91&lt;/td>
&lt;td>-0.06&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;td>-0.932&lt;/td>
&lt;td>-0.022&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.23&lt;/td>
&lt;td>7.48&lt;/td>
&lt;td>7&lt;/td>
&lt;td>7&lt;/td>
&lt;td>0.155&lt;/td>
&lt;td>-0.075&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.66&lt;/td>
&lt;td>10.25&lt;/td>
&lt;td>10&lt;/td>
&lt;td>10&lt;/td>
&lt;td>0.621&lt;/td>
&lt;td>-0.039&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Mean squared error: &lt;code>MSE ≈ 0.0015&lt;/code>. For a single layer with millions of weights, the aggregate effect is what calibration tries to minimise.&lt;/p>
&lt;p>Storage: instead of 8 values × 2 bytes = 16 bytes (BF16), we have 8 × 4 bits = 4 bytes + 2 bytes (BF16 scale) + 0.5 byte (INT4 zero-point) ≈ 6.5 bytes. 2.5× less, yet the useful data is still recoverable with a small error.&lt;/p>
&lt;h2 id="ptq-vs-qat-when-quantisation-happens">PTQ vs QAT: when quantisation happens&lt;/h2>
&lt;p>Two different operational regimes.&lt;/p>
&lt;p>&lt;strong>Post-Training Quantization (PTQ)&lt;/strong> is applied after training, on a model already trained in BF16/FP16. It reads a small dataset (typically 128-512 examples) to calibrate the scales, runs the quantisation algorithm (GPTQ, AWQ, and so on) and produces the quantised weights. &lt;strong>Cost&lt;/strong>: minutes to a few hours. &lt;strong>Typical loss&lt;/strong>: 0.05-0.3 PPL in perplexity (~0.5-2 % in MMLU) for INT4 with modern methods.&lt;/p>
&lt;p>&lt;strong>Quantization-Aware Training (QAT)&lt;/strong> introduces the quantisation operations inside the training loop. During training the model &amp;ldquo;sees&amp;rdquo; that its weights get quantised and learns to be robust to it. &lt;strong>Cost&lt;/strong>: retraining the model (expensive), but little is needed, a short fine-tune on top of the already quantised PTQ model. &lt;strong>Typical loss&lt;/strong>: ~0 (quantisation becomes indistinguishable from the original model).&lt;/p>
&lt;p>&lt;strong>When to use which:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>PTQ&lt;/strong> = default. 90 % of production cases. The model arrives in BF16, you quantise it in 1-2 hours on one GPU, you deploy it.&lt;/li>
&lt;li>&lt;strong>QAT&lt;/strong> = when PTQ loses too much and the difference matters (typical case: INT2/INT3, or sensitive models such as specific reasoning ones).&lt;/li>
&lt;/ul>
&lt;h2 id="the-dominant-formats-in-2026">The dominant formats in 2026&lt;/h2>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Dominant quantization formats">
&lt;style>
.bx{fill:#f8f8f8;stroke:#444;stroke-width:1.4;rx:8}
.b1{fill:#ffe6d6}
.b2{fill:#d6eaff}
.b3{fill:#d9f5d6}
.b4{fill:#fff5b0}
.t{font:700 13px sans-serif;fill:#222}
.s{font:400 11px sans-serif;fill:#555}
.h{font:700 14px sans-serif;fill:#222}
&lt;/style>
&lt;text x="380" y="22" text-anchor="middle" class="h">Map of weight quantization formats (May 2026)&lt;/text>
&lt;rect x="20" y="40" width="170" height="300" class="bx b1"/>
&lt;text x="105" y="62" text-anchor="middle" class="t">FP8 (E4M3/E5M2)&lt;/text>
&lt;text x="30" y="84" class="s">Datacenter / Hopper-Blackwell&lt;/text>
&lt;text x="30" y="102" class="s">— H100, H200, B200&lt;/text>
&lt;text x="30" y="120" class="s">— native in vLLM&lt;/text>
&lt;text x="30" y="138" class="s">— FP8 tensor core hardware&lt;/text>
&lt;text x="30" y="160" class="t">Loss:&lt;/text>
&lt;text x="30" y="176" class="s">— PPL: +0.02-0.05&lt;/text>
&lt;text x="30" y="192" class="s">— MMLU: -0.3-0.8 pp&lt;/text>
&lt;text x="30" y="214" class="t">Sweet spot:&lt;/text>
&lt;text x="30" y="230" class="s">Model serving in a modern&lt;/text>
&lt;text x="30" y="246" class="s">datacenter. Quality almost identical&lt;/text>
&lt;text x="30" y="262" class="s">to BF16, ~2× less VRAM.&lt;/text>
&lt;text x="30" y="286" class="t">vLLM command:&lt;/text>
&lt;text x="30" y="304" class="s" font-family="monospace">--quantization=fp8&lt;/text>
&lt;text x="30" y="320" class="s" font-family="monospace">--kv-cache-dtype=fp8&lt;/text>
&lt;rect x="200" y="40" width="170" height="300" class="bx b2"/>
&lt;text x="285" y="62" text-anchor="middle" class="t">INT4 GPTQ&lt;/text>
&lt;text x="210" y="84" class="s">Hessian-aware reconstruction&lt;/text>
&lt;text x="210" y="102" class="s">— Ampere/Ada/Hopper&lt;/text>
&lt;text x="210" y="120" class="s">— vLLM, TensorRT-LLM, ExLlama&lt;/text>
&lt;text x="210" y="138" class="s">— calibration: 128 samples&lt;/text>
&lt;text x="210" y="160" class="t">Loss:&lt;/text>
&lt;text x="210" y="176" class="s">— PPL: +0.15-0.30&lt;/text>
&lt;text x="210" y="192" class="s">— MMLU: -1.5-3 pp&lt;/text>
&lt;text x="210" y="214" class="t">Sweet spot:&lt;/text>
&lt;text x="210" y="230" class="s">GPU serving without FP8 (Ampere/Ada),&lt;/text>
&lt;text x="210" y="246" class="s">mid-size models (8-70B).&lt;/text>
&lt;text x="210" y="262" class="s">~4× less VRAM.&lt;/text>
&lt;text x="210" y="286" class="t">vLLM command:&lt;/text>
&lt;text x="210" y="304" class="s" font-family="monospace">--quantization=gptq&lt;/text>
&lt;text x="210" y="320" class="s" font-family="monospace">(model *-GPTQ-Int4)&lt;/text>
&lt;rect x="380" y="40" width="170" height="300" class="bx b3"/>
&lt;text x="465" y="62" text-anchor="middle" class="t">INT4 AWQ&lt;/text>
&lt;text x="390" y="84" class="s">Activation-aware salient weights&lt;/text>
&lt;text x="390" y="102" class="s">— Ampere/Ada/Hopper&lt;/text>
&lt;text x="390" y="120" class="s">— vLLM, TensorRT-LLM&lt;/text>
&lt;text x="390" y="138" class="s">— keeps 1 % outlier channels&lt;/text>
&lt;text x="390" y="160" class="t">Loss:&lt;/text>
&lt;text x="390" y="176" class="s">— PPL: +0.10-0.25&lt;/text>
&lt;text x="390" y="192" class="s">— MMLU: -1-2 pp&lt;/text>
&lt;text x="390" y="214" class="t">Sweet spot:&lt;/text>
&lt;text x="390" y="230" class="s">Preferred alternative to GPTQ&lt;/text>
&lt;text x="390" y="246" class="s">in 2026. Better quality&lt;/text>
&lt;text x="390" y="262" class="s">preservation at similar cost.&lt;/text>
&lt;text x="390" y="286" class="t">vLLM command:&lt;/text>
&lt;text x="390" y="304" class="s" font-family="monospace">--quantization=awq_marlin&lt;/text>
&lt;text x="390" y="320" class="s" font-family="monospace">(model *-AWQ-INT4)&lt;/text>
&lt;rect x="560" y="40" width="180" height="300" class="bx b4"/>
&lt;text x="650" y="62" text-anchor="middle" class="t">GGUF (llama.cpp)&lt;/text>
&lt;text x="570" y="84" class="s">Edge / consumer / CPU-friendly&lt;/text>
&lt;text x="570" y="102" class="s">— CPU, Apple Silicon,&lt;/text>
&lt;text x="570" y="120" class="s"> consumer GPU (4090, AMD)&lt;/text>
&lt;text x="570" y="138" class="s">— sub-formats: Q4_K_M, Q5_K_M…&lt;/text>
&lt;text x="570" y="160" class="t">Loss (Q4_K_M):&lt;/text>
&lt;text x="570" y="176" class="s">— PPL: +0.20-0.40&lt;/text>
&lt;text x="570" y="192" class="s">— MMLU: -2-4 pp&lt;/text>
&lt;text x="570" y="214" class="t">Sweet spot:&lt;/text>
&lt;text x="570" y="230" class="s">Any non-CUDA deploy or one&lt;/text>
&lt;text x="570" y="246" class="s">with limited VRAM. Ollama, LMStudio.&lt;/text>
&lt;text x="570" y="262" class="s">~4× less VRAM/RAM.&lt;/text>
&lt;text x="570" y="286" class="t">Command:&lt;/text>
&lt;text x="570" y="304" class="s" font-family="monospace">ollama run llama3:8b-q4_K_M&lt;/text>
&lt;text x="570" y="320" class="s" font-family="monospace">llama.cpp --model *.gguf&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="fp8-the-hopperblackwell-datacenter-format">FP8: the Hopper/Blackwell datacenter format&lt;/h2>
&lt;p>FP8 is not &amp;ldquo;INT8 + sign&amp;rdquo;: these are two 8-bit floating point formats.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>E4M3&lt;/strong> (4 exponent bits, 3 mantissa bits): range ±448, reasonable precision around ±1.0. Typically used for &lt;strong>weights&lt;/strong> and for &lt;strong>activations in most layers&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>E5M2&lt;/strong> (5 exponent, 2 mantissa): range ±57,344, lower precision. Used for &lt;strong>gradients&lt;/strong> during training or for activations with large outliers at inference time.&lt;/li>
&lt;/ul>
&lt;p>Why FP8 left INT8 behind in the datacenter: the tensor cores on H100/H200/B200 execute FP8 operations natively with 2× the throughput of BF16 and 4× that of FP16. And because FP8 preserves logarithmic dynamics (just like FP16), matrices with values spread out in magnitude, typical of transformers, quantise with less error than with INT8.&lt;/p>
&lt;p>The loss measured in production is minimal: for a Llama 3.1 70B FP8 versus BF16, perplexity rises by ~0.03 and MMLU drops by ~0.5 points. It is the default option in any modern deployment on H100/B200.&lt;/p>
&lt;h3 id="microscaling-nvfp4-and-mxfp4">Microscaling: NVFP4 and MXFP4&lt;/h3>
&lt;p>Blackwell (B100/B200, 2025) introduces &lt;strong>NVFP4&lt;/strong> and &lt;strong>MXFP4&lt;/strong>, 4-bit formats with small-block scaling (typically 16 or 32 elements per scale, against 128 in INT4 GPTQ). The scale is FP8 instead of FP16/BF16, which reduces storage further.&lt;/p>
&lt;p>The result: 4 bits with quality close to FP8. In 2026 NVFP4 is becoming the default option for very large models (200B+) on Blackwell clusters. For 4×H100 SXM, Hopper rather than Blackwell, FP8 remains the sweet spot.&lt;/p>
&lt;h2 id="int4-gptq-vs-awq">INT4: GPTQ vs AWQ&lt;/h2>
&lt;p>The two algorithms that dominate 4-bit quantisation solve the same problem with different strategies.&lt;/p>
&lt;h3 id="gptq-frantar-et-al-2022">GPTQ (Frantar et al. 2022)&lt;/h3>
&lt;p>The idea: layer-by-layer quantisation, explicitly minimising the error at the output of each linear layer using information from the Hessian matrix (the second derivative of the loss). For each layer:&lt;/p>
&lt;ol>
&lt;li>Estimate the Hessian &lt;code>H = X^T X&lt;/code> where &lt;code>X&lt;/code> are the calibration activations.&lt;/li>
&lt;li>Quantise one weight at a time in order (typically the most sensitive one first).&lt;/li>
&lt;li>&lt;strong>Update the remaining weights&lt;/strong> to compensate for the error of the weight just quantised.&lt;/li>
&lt;/ol>
&lt;p>Step 3 is what makes GPTQ better than naive round-to-nearest: weights compensate for their neighbours&amp;rsquo; errors. The official implementation quantises a Llama 3 70B in ~3-4 hours on one H100 with 128 calibration samples.&lt;/p>
&lt;h3 id="awq-lin-et-al-2023">AWQ (Lin et al. 2023)&lt;/h3>
&lt;p>The AWQ observation: within a layer, not all channels (weight columns) are equally important. Roughly &lt;strong>1 % of the channels&lt;/strong> account for most of the impact on the activations. AWQ identifies them by measuring the mean magnitude of the activations that multiply them, and scales them before quantising so they are better preserved.&lt;/p>
&lt;p>Concretely: if a channel &lt;code>c&lt;/code> has large mean activations, AWQ multiplies that channel&amp;rsquo;s weights by a factor &lt;code>s_c&lt;/code> before quantising, and when quantising the input to the next layer it divides by &lt;code>s_c&lt;/code>. The maths cancels out, but the important weights end up with more resolution inside the INT4 range. No retraining, no Hessian, faster than GPTQ (~1-2 h for 70B on an H100).&lt;/p>
&lt;h3 id="which-one-to-choose">Which one to choose&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Criterion&lt;/th>
&lt;th>GPTQ&lt;/th>
&lt;th>AWQ&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Quantisation speed&lt;/td>
&lt;td>Slower&lt;/td>
&lt;td>Faster&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quality preserved in INT4&lt;/td>
&lt;td>Good&lt;/td>
&lt;td>Slightly better (~0.05-0.1 PPL)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported hardware&lt;/td>
&lt;td>Broad (Ampere+)&lt;/td>
&lt;td>Broad (Ampere+)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ecosystem&lt;/td>
&lt;td>Mature, widely integrated&lt;/td>
&lt;td>More recent, gaining ground&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Default in 2026&lt;/td>
&lt;td>When GPTQ artefacts already exist&lt;/td>
&lt;td>Default for new quantisations&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The practical rule in May 2026: &lt;strong>AWQ by default&lt;/strong> for new INT4. &lt;strong>GPTQ&lt;/strong> when a GPTQ-Int4 artefact published by the community already meets your requirements.&lt;/p>
&lt;h2 id="gguf-the-llamacpp-ecosystem">GGUF: the llama.cpp ecosystem&lt;/h2>
&lt;p>GGUF is not a quantisation algorithm, it is a file format, and a whole tooling ecosystem, around the llama.cpp runtime.&lt;/p>
&lt;p>Its value: universal compatibility. The same GGUF file runs on:&lt;/p>
&lt;ul>
&lt;li>Pure CPU (Intel/AMD x86, ARM).&lt;/li>
&lt;li>Apple Silicon (M1/M2/M3/M4) with Metal acceleration.&lt;/li>
&lt;li>Consumer GPUs (RTX, AMD Radeon) with layer offload to VRAM.&lt;/li>
&lt;li>Edge devices (Jetson, ARM phones).&lt;/li>
&lt;/ul>
&lt;p>That is what llama.cpp allows and vLLM/TensorRT-LLM do not. The trade-off: lower peak throughput on datacenter GPUs than vLLM.&lt;/p>
&lt;p>The most used GGUF sub-formats in 2026:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Sub-format&lt;/th>
&lt;th>Effective bits&lt;/th>
&lt;th>Relative quality&lt;/th>
&lt;th>Typical use&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Q8_0&lt;/code>&lt;/td>
&lt;td>8.5&lt;/td>
&lt;td>Almost lossless&lt;/td>
&lt;td>Baseline validation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q6_K&lt;/code>&lt;/td>
&lt;td>6.6&lt;/td>
&lt;td>Very small loss&lt;/td>
&lt;td>High quality + savings&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q5_K_M&lt;/code>&lt;/td>
&lt;td>5.7&lt;/td>
&lt;td>Small loss&lt;/td>
&lt;td>Quality/size sweet spot&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q4_K_M&lt;/code>&lt;/td>
&lt;td>4.8&lt;/td>
&lt;td>Moderate loss&lt;/td>
&lt;td>&lt;strong>Consumer default&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q4_K_S&lt;/code>&lt;/td>
&lt;td>4.5&lt;/td>
&lt;td>Moderate-to-high loss&lt;/td>
&lt;td>When Q4_K_M does not fit&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q3_K_M&lt;/code>&lt;/td>
&lt;td>3.9&lt;/td>
&lt;td>Noticeable loss&lt;/td>
&lt;td>Very constrained hardware&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q2_K&lt;/code>&lt;/td>
&lt;td>3.3&lt;/td>
&lt;td>Large loss&lt;/td>
&lt;td>Last resort&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;code>_K_M&lt;/code> suffix indicates the degree of mixing: inside the file, certain layers (typically &lt;code>attention.wv&lt;/code>, &lt;code>feed_forward.w2&lt;/code>) are stored with more bits than others. It is the equivalent of the JPEG &amp;ldquo;edge detector&amp;rdquo; applied layer by layer with a pre-established heuristic.&lt;/p>
&lt;h2 id="kv-cache-quantization">KV cache quantization&lt;/h2>
&lt;p>Quantising the model weights is half the problem. The &lt;strong>KV cache&lt;/strong>, covered in detail in &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory&lt;/a>, typically consumes 20-50 % of VRAM in production with concurrency. Quantising the cache is a lever too:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>--kv-cache-dtype=auto&lt;/code>&lt;/strong> (BF16/FP16, the default). 2 bytes per dimension × num_heads × head_dim × 2 (K and V).&lt;/li>
&lt;li>&lt;strong>&lt;code>--kv-cache-dtype=fp8&lt;/code>&lt;/strong> (E4M3 or E5M2 depending on hardware). 1 byte. Halves the cache with a typical loss of &amp;lt; 0.5 % on quality benchmarks.&lt;/li>
&lt;li>&lt;strong>&lt;code>--kv-cache-dtype=int4&lt;/code>&lt;/strong> (with blocks of 128). 0.5 bytes plus scale overhead. Divides the cache by ~3.5. Measurable loss (1-2 %) but acceptable with long contexts.&lt;/li>
&lt;/ul>
&lt;p>KV cache quantisation is &lt;strong>orthogonal&lt;/strong> to weight quantisation: you can have BF16 weights and an FP8 cache, or INT4 weights and an FP8 cache, and so on. The dominant combination in 2026 on H100: &lt;strong>FP8 weights + FP8 cache&lt;/strong>, which is almost indistinguishable from BF16 in quality and doubles concurrency capacity.&lt;/p>
&lt;h2 id="measured-quality-loss-llama-31-70b-instruct-reference">Measured quality loss (Llama 3.1 70B Instruct, reference)&lt;/h2>
&lt;p>A representative table for Llama 3.1 70B Instruct with a WikiText-2 calibration dataset (128 samples). Figures aggregated from public sources; they may vary by ±0.05 PPL and ±0.5 MMLU depending on implementation and seed.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Format&lt;/th>
&lt;th>Model VRAM&lt;/th>
&lt;th>Perplexity (WikiText-2)&lt;/th>
&lt;th>MMLU (5-shot)&lt;/th>
&lt;th>Relative speed (H100)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>BF16 (baseline)&lt;/td>
&lt;td>140 GB&lt;/td>
&lt;td>4.85&lt;/td>
&lt;td>82.1&lt;/td>
&lt;td>1.00×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>FP8 (E4M3)&lt;/td>
&lt;td>70 GB&lt;/td>
&lt;td>4.87 (+0.02)&lt;/td>
&lt;td>81.6 (-0.5)&lt;/td>
&lt;td>1.85×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT8 SmoothQuant&lt;/td>
&lt;td>70 GB&lt;/td>
&lt;td>4.92 (+0.07)&lt;/td>
&lt;td>81.0 (-1.1)&lt;/td>
&lt;td>1.65×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4 AWQ&lt;/td>
&lt;td>35 GB&lt;/td>
&lt;td>4.99 (+0.14)&lt;/td>
&lt;td>80.4 (-1.7)&lt;/td>
&lt;td>2.50×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4 GPTQ&lt;/td>
&lt;td>35 GB&lt;/td>
&lt;td>5.05 (+0.20)&lt;/td>
&lt;td>80.0 (-2.1)&lt;/td>
&lt;td>2.40×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GGUF Q5_K_M&lt;/td>
&lt;td>49 GB&lt;/td>
&lt;td>4.94 (+0.09)&lt;/td>
&lt;td>81.1 (-1.0)&lt;/td>
&lt;td>n/a (llama.cpp)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GGUF Q4_K_M&lt;/td>
&lt;td>42 GB&lt;/td>
&lt;td>5.08 (+0.23)&lt;/td>
&lt;td>79.8 (-2.3)&lt;/td>
&lt;td>n/a (llama.cpp)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GGUF Q3_K_M&lt;/td>
&lt;td>33 GB&lt;/td>
&lt;td>5.45 (+0.60)&lt;/td>
&lt;td>77.5 (-4.6)&lt;/td>
&lt;td>n/a (llama.cpp)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Three lessons worth keeping:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>FP8 is almost free in quality terms&lt;/strong>. If your hardware supports it, there is no debate.&lt;/li>
&lt;li>&lt;strong>INT4 AWQ is noticeably better than INT4 GPTQ&lt;/strong> in preserved quality, at comparable speed.&lt;/li>
&lt;li>&lt;strong>Q3 is already in measurable-loss territory&lt;/strong>; Q2 should no longer be used except for experiments or extreme demos.&lt;/li>
&lt;/ol>
&lt;h2 id="implications-for-on-premise-hardware">Implications for on-premise hardware&lt;/h2>
&lt;h3 id="on-an-rtx-4090-24-gb-ada-lovelace-no-native-fp8">On an RTX 4090 (24 GB, Ada Lovelace, no native FP8)&lt;/h3>
&lt;p>Llama 3.1 8B Instruct fits comfortably in BF16 (16 GB), but that leaves little headroom for a KV cache under concurrency. The usual sweet spot:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3.1 8B AWQ-INT4&lt;/strong>: ~5 GB of weights, 19 GB free for the KV cache → 4-8 concurrent sessions with a moderate context.&lt;/li>
&lt;li>&lt;strong>Llama 3 70B GGUF Q4_K_M&lt;/strong>: ~42 GB. &lt;strong>It does not fit on a whole 4090&lt;/strong>; it requires CPU offload with llama.cpp (slow decode but workable for a single user).&lt;/li>
&lt;li>&lt;strong>Llama 3 70B AWQ-INT4 with TP=2 (two 4090s)&lt;/strong>: ~17 GB per GPU → it fits and leaves headroom.&lt;/li>
&lt;/ul>
&lt;p>The 4090 &lt;strong>does not support native FP8&lt;/strong> (Ada Lovelace has the instruction but not Hopper&amp;rsquo;s accelerated throughput). In practice, FP8 on a 4090 works but without the speed gain: the sensible choice is INT4 AWQ.&lt;/p>
&lt;h3 id="on-a-generic-4h100-sxm-cluster-320-gb-nvlink-native-fp8">On a generic 4×H100 SXM cluster (320 GB, NVLink, native FP8)&lt;/h3>
&lt;p>Here FP8 shines:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3.1 70B FP8&lt;/strong> with TP=2: ~35 GB/GPU. Comfortable, leaving a huge amount of room for the KV cache → dozens of concurrent sessions.&lt;/li>
&lt;li>&lt;strong>Llama 3.1 405B FP8&lt;/strong> with TP=4: ~200 GB/GPU. It just fits, with prefill+decode in the same pool.&lt;/li>
&lt;li>&lt;strong>Llama 3.1 405B INT4 AWQ&lt;/strong> with TP=2: ~100 GB/GPU. It allows serving the large model without saturating the cluster; there is headroom left for the cache and for serving another model at the same time.&lt;/li>
&lt;/ul>
&lt;p>The rule of thumb on an H100 cluster in 2026: &lt;strong>FP8 if quality matters and the model fits; INT4 AWQ if the model does not fit in FP8 or if you want more concurrency at the cost of 1-2 MMLU points&lt;/strong>.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-articles">What we have not covered (upcoming articles)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Speculative decoding&lt;/strong>: the other big acceleration lever in inference. Orthogonal to quantization, it multiplies the speedup.&lt;/li>
&lt;li>&lt;strong>MoE quantization&lt;/strong>: Mixture-of-Experts models (Mixtral, DeepSeek V3, Qwen3-235B-A22B) have different quantisation patterns, experts are not quantised uniformly and there is dynamic routing.&lt;/li>
&lt;li>&lt;strong>Calibration dataset matters&lt;/strong>: how to choose the 128-512 calibration samples. The common mistake of grabbing a random dataset off the internet, and how to avoid it.&lt;/li>
&lt;li>&lt;strong>Multimodal quantization&lt;/strong>: vision-language models have heterogeneous layers (a CNN vision encoder, a transformer language model) that need separate treatment.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory that holds up LLM inference&lt;/a> — the cache is also quantised with the same formats (FP8, INT4); this post goes into the detail of how, and with what loss.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">Disaggregated serving: prefill and decode in specialised pods&lt;/a> — the two pools can be quantised &lt;strong>asymmetrically&lt;/strong>: prefill with less compression (quality), decode with more compression (throughput).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production&lt;/a> — QLoRA uses NF4, a specific INT4 variant for the base model during training. It is the cousin of the inference formats described here.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/modern-alignment-dpo-kto-orpo-simpo/">Modern alignment: DPO, KTO, ORPO, SimPO&lt;/a> — the reference model (&lt;code>π_ref&lt;/code>) in DPO can be quantised to FP8 to free up VRAM. The quality of the reference matters less than that of the trained model.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the master map where Deploy is stage 4.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">Speculative decoding: the secretary who anticipates what the boss is going to say&lt;/a> — a lever orthogonal to and multiplicative with quantization. The draft is usually left in BF16 even when the target is in FP8/INT4, because quantising it degrades α and kills the speedup.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/flashattention-librarian-io-awareness-async-blackwell-asymmetry/">FlashAttention v1/v2/v3/v4: the librarian who never clears the desk&lt;/a> — FA3 does FP8 attention with block quantization and incoherent processing to preserve precision; FA4 combines that with Blackwell NVFP4 weights. The two quantisation layers (weights and attention) are applied at different moments in the kernel.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/moe-inference-fundamentals-expert-parallel-deepseek-economics/">MoE inference: the call centre with 256 specialists&lt;/a> — DeepSeek-V3 671B FP8 (~685 GB) only fits on modest clusters thanks to aggressive quantisation of the routed experts; NVFP4 on expert weights + FP8 attention is the peak setup on NVL72.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/multi-lora-serving-shared-base-concurrent-adapters-sgmv-kernel/">Multi-LoRA serving: one translator with a thousand glossaries&lt;/a> — the canonical stack is an FP8/INT4 base plus BF16 adapters. Quantising the base does not just free memory, it multiplies the economics: the same cluster goes from serving dozens to hundreds of concurrent adapters.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-decode-vllm-squeezing-every-token-small-hardware/">Optimising decode in vLLM&lt;/a> — the combination of AWQ (INT4 weights) + FP8 KV cache + speculative decoding is the reference config for squeezing an RTX 4090; the concrete numbers are laid out there.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-prefill-vllm-knobs-ttft-will-not-forgive/">Optimising prefill in vLLM&lt;/a> — &lt;code>--kv-cache-dtype fp8&lt;/code> and &lt;code>--calculate-kv-scales&lt;/code> are the parameters that enable KV cache quantisation in vLLM, with the precision trade-off measured on long contexts.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-pruning-cutting-without-amputating/">Pruning LLM models&lt;/a> — an orthogonal lever: quantising reduces the precision of the weights pruning has kept; combined, 50% Wanda sparsity + AWQ INT4 shrinks the model to ~1/8 of its original size.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/knowledge-distillation-teaching-small-model-think-like-big-one/">Knowledge distillation&lt;/a> — the previous step in the pipeline: you distil first (a new, smaller model), you quantise afterwards (the same model, more efficient); the two techniques attack different dimensions.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning for on-premise LLM inference&lt;/a> — quantization is the first sizing lever when the BF16 arithmetic does not work out; the sensitivity table there quantifies the savings in VRAM and in TPOT.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/test-time-quantization-without-calibration-dataset/">Test-time quantization: quantising on the fly&lt;/a> — the alternative with no calibration dataset: it derives activation-aware scales at inference time, at the cost of runtime overhead that weighs more heavily on small models.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/aggressive-quantisation-4-bit-to-ternary/">Aggressive quantisation: from 4-bit to ternary&lt;/a> — what happens below 4-bit, where naive PTQ collapses and co-design is needed: codebooks (AQLM, QuIP#, QTIP), QAT and native ternary (BitNet b1.58).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-serving-engines-pareto-frontier-comparison/">Serving engines compared (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — how each engine supports the quantization formats described here (FP8, AWQ, GPTQ, GGUF) and what difference in goodput it makes to pick the one that implements them best.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Frantar, E., Ashkboos, S., Hoefler, T., Alistarh, D. &lt;em>GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers&lt;/em> (ICLR 2023).&lt;/li>
&lt;li>Lin, J., Tang, J., Tang, H., Yang, S., Dang, X., Han, S. &lt;em>AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration&lt;/em> (MLSys 2024).&lt;/li>
&lt;li>Dettmers, T., Pagnoni, A., Holtzman, A., Zettlemoyer, L. &lt;em>QLoRA: Efficient Finetuning of Quantized LLMs&lt;/em> (NeurIPS 2023). Introduces NF4 and double quantization.&lt;/li>
&lt;li>Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J., Han, S. &lt;em>SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models&lt;/em> (ICML 2023).&lt;/li>
&lt;li>NVIDIA. &lt;em>FP8 Formats for Deep Learning&lt;/em> — E4M3/E5M2 white paper: &lt;a href="https://arxiv.org/abs/2209.05433">https://arxiv.org/abs/2209.05433&lt;/a>.&lt;/li>
&lt;li>Rouhani, B. et al. &lt;em>Microscaling Data Formats for Deep Learning&lt;/em> — MXFP4/MXFP8: &lt;a href="https://arxiv.org/abs/2310.10537">https://arxiv.org/abs/2310.10537&lt;/a>.&lt;/li>
&lt;li>llama.cpp GGUF spec: &lt;a href="https://github.com/ggerganov/llama.cpp/blob/master/docs/gguf.md">https://github.com/ggerganov/llama.cpp/blob/master/docs/gguf.md&lt;/a>.&lt;/li>
&lt;li>vLLM quantization docs: &lt;a href="https://docs.vllm.ai/en/latest/quantization/">https://docs.vllm.ai/en/latest/quantization/&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>