<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Quantization on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/quantization/</link><description>Recent content in Quantization on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 16 Jun 2026 09:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/quantization/index.xml" rel="self" type="application/rss+xml"/><item><title>Levers for spending fewer watts per token: a quantified catalogue for LLM inference</title><link>https://blog.lo0.es/en/posts/levers-fewer-watts-per-token-llm-inference/</link><pubDate>Tue, 16 Jun 2026 09:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/levers-fewer-watts-per-token-llm-inference/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. The dollar symbol is not used
(on this site it is a formula delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Seven independent levers act on the numerator or the denominator of the identity
\(\text{J/token} = \text{power (W)} / \text{throughput (tok/s)}\). Applied in order of
cost-benefit: &lt;strong>quantisation&lt;/strong> (FP16→FP8) cuts J/token by 30–50% without changing hardware;
&lt;strong>power capping&lt;/strong> to 70% of TDP reduces power by 30% with less than a 10% drop in
throughput on memory-bound loads; &lt;strong>DVFS&lt;/strong> in the decode phase saves a further 22–45%;
&lt;strong>continuous batching&lt;/strong> lowers J/token by 25–40% against unoptimised stacks; &lt;strong>speculative
decoding&lt;/strong> trims up to 29% off J/token at low batch; &lt;strong>prefix caching&lt;/strong> eliminates the
prefill recompute for repeated prompts; and &lt;strong>scheduling to clean hours&lt;/strong> shifts the
carbon footprint without changing physical consumption. Starting reference: a vLLM FP16 stack on an H100
SXM (700 W TDP) with Llama-3 70B produces ~0.39 J/token under realistic batch loads; an
unoptimised stack (PyTorch baseline) starts 2–6× higher. The levers stack; the combined effect
can reach a 73% reduction against the unoptimised baseline
(&lt;a href="https://arxiv.org/pdf/2504.17674">arXiv 2504.17674&lt;/a>).&lt;/p>
&lt;hr>
&lt;h2 id="the-reference-identity">The reference identity&lt;/h2>
$$\text{J/token} = \frac{\overline{P}\ [\text{W}]}{\text{throughput}\ [\text{tok/s}]}$$
&lt;p>where \(\overline{P}\) is the average power over the measurement window (see the methodology in
&lt;a href="https://blog.lo0.es/en/posts/energy-per-token-spain-methodology-electricity-market-speculation/">energy per token&lt;/a>). The levers act on one
of the two factors:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>They reduce \(\overline{P}\)&lt;/strong>: power capping, DVFS, scheduling to low-intensity hours.&lt;/li>
&lt;li>&lt;strong>They raise throughput&lt;/strong> (the denominator): quantisation, continuous batching, speculative decoding,
prefix caching, chunked prefill, model selection.&lt;/li>
&lt;li>&lt;strong>Both at once&lt;/strong>: quantisation (smaller weight footprint → a larger batch fits in VRAM → higher
throughput at similar or lower power).&lt;/li>
&lt;/ul>
&lt;p>The expanded formula including infrastructure overhead:&lt;/p>
$$\text{J/token}_{\text{effective}} = \frac{\overline{P}_{\text{GPU}} + \overline{P}_{\text{rest of node}}}{\text{throughput}_{\text{tok/s}}} \times \text{PUE}$$
&lt;p>For the lever calculations the work is done on \(\overline{P}_{\text{GPU}}\) for clarity; the
PUE factor (see &lt;a href="https://blog.lo0.es/en/posts/energy-per-token-spain-methodology-electricity-market-speculation/">energy per token&lt;/a>) amplifies or
attenuates the effect of each lever proportionally.&lt;/p>
&lt;hr>
&lt;h2 id="master-table-levers--impact--cost--tool">Master table: levers × impact × cost × tool&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Lever&lt;/th>
&lt;th>J/token reduction (%)&lt;/th>
&lt;th>Effect on latency&lt;/th>
&lt;th>Effect on quality&lt;/th>
&lt;th>Main OSS tool&lt;/th>
&lt;th>Source&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Quantisation FP16→FP8&lt;/td>
&lt;td>&lt;strong>30–50%&lt;/strong>&lt;/td>
&lt;td>−8–33% TPOT (improvement)&lt;/td>
&lt;td>&amp;lt;1% degradation on benchmarks&lt;/td>
&lt;td>vLLM, TRT-LLM, llm-compressor&lt;/td>
&lt;td>arXiv 2504.17674, baseten.co&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quantisation FP16→INT4 (AWQ/GPTQ)&lt;/td>
&lt;td>&lt;strong>45–60%&lt;/strong>&lt;/td>
&lt;td>latency improves (lower BW)&lt;/td>
&lt;td>1–3% degradation depending on model&lt;/td>
&lt;td>AutoAWQ, llm-compressor, vLLM&lt;/td>
&lt;td>arXiv 2504.17674&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Continuous vs static batching&lt;/td>
&lt;td>&lt;strong>25–40%&lt;/strong> vs PyTorch&lt;/td>
&lt;td>+5× throughput (improvement)&lt;/td>
&lt;td>no impact&lt;/td>
&lt;td>vLLM, TGI, TRT-LLM&lt;/td>
&lt;td>arXiv 2504.17674, TokenPowerBench&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Power capping 70% TDP&lt;/td>
&lt;td>&lt;strong>20–30%&lt;/strong> power&lt;/td>
&lt;td>&amp;lt;10% throughput (decode)&lt;/td>
&lt;td>no impact&lt;/td>
&lt;td>nvidia-smi, DCGM, Zeus&lt;/td>
&lt;td>arXiv 2604.11391, NERSC docs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DVFS decode-phase&lt;/td>
&lt;td>&lt;strong>22–45%&lt;/strong> in decode&lt;/td>
&lt;td>1–6% total latency&lt;/td>
&lt;td>no impact&lt;/td>
&lt;td>nvidia-smi, GreenLLM, DVFS-GPT&lt;/td>
&lt;td>arXiv 2501.08219, HAL 05190051&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Speculative decoding&lt;/td>
&lt;td>&lt;strong>up to 29%&lt;/strong> (batch≤16)&lt;/td>
&lt;td>TTFT improves&lt;/td>
&lt;td>no demonstrable impact&lt;/td>
&lt;td>vLLM, TRT-LLM&lt;/td>
&lt;td>arXiv 2602.09113&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prefix caching&lt;/td>
&lt;td>&lt;strong>50–90%&lt;/strong> on reusable prefill&lt;/td>
&lt;td>TTFT −50–80% (repeated prompts)&lt;/td>
&lt;td>no impact&lt;/td>
&lt;td>vLLM, SGLang&lt;/td>
&lt;td>vLLM docs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chunked prefill&lt;/td>
&lt;td>~10–15% on mixed loads&lt;/td>
&lt;td>more stable TTFT&lt;/td>
&lt;td>no impact&lt;/td>
&lt;td>vLLM (SARATHI)&lt;/td>
&lt;td>arXiv 2308.16369&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SLM vs dense LLM selection&lt;/td>
&lt;td>&lt;strong>60–80%&lt;/strong> (7B vs 70B model)&lt;/td>
&lt;td>low latency&lt;/td>
&lt;td>degradation varies by task&lt;/td>
&lt;td>Ollama, vLLM, llama.cpp&lt;/td>
&lt;td>arXiv 2504.17674&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Scheduling to clean hours&lt;/td>
&lt;td>0% J/token, up to −70% CO₂&lt;/td>
&lt;td>no impact on the load&lt;/td>
&lt;td>no impact&lt;/td>
&lt;td>Kueue, Volcano, cron&lt;/td>
&lt;td>ACM SLIT, arXiv 2507.09942&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>Note: the percentages are against the effective J/token of the load, not against idle board
power. The effects are always measured under conditions of equal output quality (same model,
same serving stack, same prompt distribution).&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="lever-1-quantisation">Lever 1: quantisation&lt;/h2>
&lt;h3 id="mechanics">Mechanics&lt;/h3>
&lt;p>Quantisation reduces the numerical precision of the weights (and optionally the activations),
which shortens the time spent reading weights from HBM per generated token. Decode is
memory-bandwidth-bound: the bottleneck is reading the weights, not computing. Reducing bit width
frees bandwidth for higher throughput or allows a larger batch in VRAM.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Format&lt;/th>
&lt;th>Bits/weight&lt;/th>
&lt;th>BW reduction vs FP32&lt;/th>
&lt;th>J/token vs FP16&lt;/th>
&lt;th>Tool&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>FP32&lt;/td>
&lt;td>32&lt;/td>
&lt;td>1× (reference)&lt;/td>
&lt;td>+100% (worse)&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>BF16/FP16&lt;/td>
&lt;td>16&lt;/td>
&lt;td>2×&lt;/td>
&lt;td>reference&lt;/td>
&lt;td>vLLM native&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>FP8 (W8A8)&lt;/td>
&lt;td>8&lt;/td>
&lt;td>~4×&lt;/td>
&lt;td>&lt;strong>−30–50%&lt;/strong>&lt;/td>
&lt;td>vLLM, TRT-LLM, llm-compressor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4 AWQ (W4A16)&lt;/td>
&lt;td>4&lt;/td>
&lt;td>~8×&lt;/td>
&lt;td>&lt;strong>−45–60%&lt;/strong>&lt;/td>
&lt;td>AutoAWQ, llm-compressor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4 GPTQ (W4A16)&lt;/td>
&lt;td>4&lt;/td>
&lt;td>~8×&lt;/td>
&lt;td>&lt;strong>−40–55%&lt;/strong>&lt;/td>
&lt;td>AutoGPTQ, vLLM&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Concrete reference figures:&lt;/p>
&lt;ul>
&lt;li>Llama-3 405B on H100, FP16→FP8: −30% J/token measured; TPOT improves by 33% (Baseten,
&lt;a href="https://www.baseten.co/blog/33-faster-llm-inference-with-fp8-quantization/">baseten.co/blog/33-faster-llm-inference-with-fp8-quantization&lt;/a>).&lt;/li>
&lt;li>The vLLM+CUDA graphs stack vs the PyTorch baseline: 2–6× more efficient in J/token
(&lt;a href="https://arxiv.org/pdf/2504.17674">arXiv 2504.17674&lt;/a>).&lt;/li>
&lt;li>Llama-65B on V100 FP16: 3–4 J/token. Llama-3 70B on H100 FP8: 0.39 J/token. Combined
hardware+quantisation reduction: ~90%
(&lt;a href="https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/">MLCommons, arXiv 2504.17674&lt;/a>).&lt;/li>
&lt;/ul>
&lt;h3 id="quality-cost">Quality cost&lt;/h3>
&lt;p>FP8 quantisation with calibration produces &amp;lt;1% degradation on standard benchmarks (MMLU,
HumanEval). INT4 AWQ can produce 1–3% degradation depending on the model and the task;
INT4 GPTQ without fine calibration, up to 2–5%. Validate with &lt;a href="https://blog.lo0.es/en/posts/llm-energy-leaderboards-wh-per-token/">LLM energy leaderboards&lt;/a> and your own quality benchmarks.&lt;/p>
&lt;h3 id="oss-tooling">OSS tooling&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"># vLLM with FP8 (load-time quantisation, H100/A100)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve meta-llama/Llama-3-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> --dtype auto
&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"># llm-compressor: offline quantisation to W4A16 (AWQ)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python -m llmcompressor.transformers.compression.helpers &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 meta-llama/Llama-3-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> --recipe w4a16_awq.yaml &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-dir llama-70b-w4a16/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="lever-2-continuous-batching">Lever 2: continuous batching&lt;/h2>
&lt;h3 id="mechanics-1">Mechanics&lt;/h3>
&lt;p>Static batching keeps the batch fixed: if one request finishes early, the slot stays
empty until the end of the batch. &lt;strong>Continuous batching&lt;/strong> inserts new requests as
soon as a slot frees up, keeping the GPU always at the highest possible utilisation.&lt;/p>
&lt;p>The energy effect: GPU power is almost constant (the GPU is always active); the
throughput grows. The result is a larger denominator at similar power:&lt;/p>
$$\text{J/token}_{\text{continuous batch}} = \frac{\overline{P}}{\text{throughput}_{\uparrow}}$$
&lt;p>Measured figures (&lt;a href="https://arxiv.org/pdf/2504.17674">arXiv 2504.17674&lt;/a>,
&lt;a href="https://arxiv.org/pdf/2512.03024">TokenPowerBench arXiv 2512.03024&lt;/a>):&lt;/p>
&lt;ul>
&lt;li>vLLM with PagedAttention vs unoptimised Transformers: −25–40% J/token on high
batch loads.&lt;/li>
&lt;li>Online continuous batching vs static offline: a further −5% of J/token (small but
consistent).&lt;/li>
&lt;li>Batch 32→256 tokens: −25% J/token on Llama-3 70B (kernel economies of scale).&lt;/li>
&lt;li>Throughput: ×5 against static batching stacks at equivalent load.&lt;/li>
&lt;/ul>
&lt;p>The efficiency knee on the H100 sits at around 256–512 tokens of batch; beyond that,
J/token flattens or rises slightly.&lt;/p>
&lt;h3 id="oss-tooling-1">OSS tooling&lt;/h3>
&lt;p>vLLM, TGI (HuggingFace Text Generation Inference), TRT-LLM. The relevant flag in vLLM:
&lt;code>--max-num-seqs&lt;/code> (maximum concurrent sequences); tuning it to the available VRAM
maximises the effective batch.&lt;/p>
&lt;hr>
&lt;h2 id="lever-3-power-capping">Lever 3: power capping&lt;/h2>
&lt;h3 id="mechanics-2">Mechanics&lt;/h3>
&lt;p>The power cap sets a ceiling on instantaneous power (W). The GPU respects it by cutting the
clock (internal DVFS). The efficiency trick: LLM decode is &lt;strong>memory-bandwidth-bound&lt;/strong>; the
GPU is already not at usefully full clock, and lowering the TDP reduces power without degrading
throughput proportionally.&lt;/p>
&lt;h3 id="the-power-cap-vs-throughput-curve-h100-sxm">The power cap vs throughput curve (H100 SXM)&lt;/h3>
&lt;p>Figures from (&lt;a href="https://arxiv.org/html/2604.11391v1">arXiv 2604.11391&lt;/a>) and NERSC
(&lt;a href="https://docs.nersc.gov/jobs/power-capping/">docs.nersc.gov/jobs/power-capping&lt;/a>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Power cap (W)&lt;/th>
&lt;th>% of TDP (700 W)&lt;/th>
&lt;th>Relative throughput (memory-bound)&lt;/th>
&lt;th>Efficiency (tok/W)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>700 W&lt;/td>
&lt;td>100%&lt;/td>
&lt;td>100%&lt;/td>
&lt;td>1.00×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>560 W&lt;/td>
&lt;td>80%&lt;/td>
&lt;td>~95%&lt;/td>
&lt;td>~1.19×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>490 W&lt;/strong>&lt;/td>
&lt;td>&lt;strong>70%&lt;/strong>&lt;/td>
&lt;td>&lt;strong>~90–95%&lt;/strong>&lt;/td>
&lt;td>&lt;strong>~1.27×&lt;/strong> (sweet spot)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>420 W&lt;/td>
&lt;td>60%&lt;/td>
&lt;td>~80–85%&lt;/td>
&lt;td>~1.33× (latency rises)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>350 W&lt;/td>
&lt;td>50%&lt;/td>
&lt;td>~75% (the H100 reaches maximum BW at 350 W memory-bound)&lt;/td>
&lt;td>~1.50×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>200 W&lt;/td>
&lt;td>29%&lt;/td>
&lt;td>~50%&lt;/td>
&lt;td>frequency saturation&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Key finding from &lt;a href="https://arxiv.org/html/2604.11391v1">arXiv 2604.11391&lt;/a>: &lt;strong>the H100 reaches
its maximum effective memory bandwidth with only ~350 W&lt;/strong> on memory-bound loads
(STriad benchmark), against a TDP of 700 W. Going from 700 W to 490 W (−30% power) produces
less than a 10% drop in decode throughput: J/token improves by ~20–27%.&lt;/p>
&lt;p>For compute-bound loads (heavy prefill), the sweet spot shifts to 400–500 W, where
performance scales almost linearly with the cap; from 500 W to 700 W, the gain is only
~10% extra throughput for each additional 100 W.&lt;/p>
&lt;h3 id="jtoken-formula-with-a-power-cap">J/token formula with a power cap&lt;/h3>
$$\text{J/token}(P_{\text{cap}}) = \frac{P_{\text{cap}} \times \eta(P_{\text{cap}})}{\text{throughput}_{\text{base}} \times f(P_{\text{cap}})}$$
&lt;p>where \(\eta(P_{\text{cap}})\) is the fraction of power actually consumed under the
cap (on memory-bound loads, the GPU no longer consumes the whole cap), and \(f(P_{\text{cap}})\) is
the resulting fraction of throughput. In the H100&amp;rsquo;s 490–560 W zone,
\(\eta \approx 0{.}9\) and \(f \approx 0{.}92\text{–}0{.}95\): the numerator falls more than the
denominator, so J/token improves.&lt;/p>
&lt;h3 id="commands">Commands&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"># Set the power cap to 490 W on GPU 0 (H100 SXM, 700 W TDP)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo nvidia-smi -i &lt;span class="m">0&lt;/span> -pl &lt;span class="m">490&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"># Check the applied limit and the power draw in real time&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">nvidia-smi --query-gpu&lt;span class="o">=&lt;/span>power.limit,power.draw --format&lt;span class="o">=&lt;/span>csv -l &lt;span class="m">1&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"># With DCGM: read DCGM_FI_DEV_POWER_USAGE in Prometheus&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># and DCGM_FI_DEV_POWER_LIMIT to confirm the cap is active&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&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"># Apply the cap to every GPU on the node (systemd/cron script)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> i in &lt;span class="k">$(&lt;/span>seq &lt;span class="m">0&lt;/span> 3&lt;span class="k">)&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> sudo nvidia-smi -i &lt;span class="nv">$i&lt;/span> -pl &lt;span class="m">490&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With DCGM in production, the cap can be set via
&lt;code>dcgmi policy --set powercap:490&lt;/code> or through the NVIDIA GPU Operator (the
&lt;code>powerLimit&lt;/code> field in the DevicePlugin). Zeus (&lt;a href="https://ml.energy/zeus/">ml.energy/zeus&lt;/a>)
can sweep caps automatically and measure the resulting J/token per configuration.&lt;/p>
&lt;hr>
&lt;h2 id="lever-4-dvfs-dynamic-voltage-and-frequency-scaling">Lever 4: DVFS (Dynamic Voltage and Frequency Scaling)&lt;/h2>
&lt;h3 id="mechanics-3">Mechanics&lt;/h3>
&lt;p>DVFS is finer-grained than a power cap: it allows the SM frequency to be set directly
(&lt;code>nvidia-smi -lgc&lt;/code> to lock the GPU clock) or left for the driver to adjust dynamically
according to available power and load. In LLM inference, the asymmetry between phases
is the key fact:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Prefill&lt;/strong>: compute-bound. Benefits from maximum clock. Sensitive to frequency reduction.&lt;/li>
&lt;li>&lt;strong>Decode&lt;/strong>: memory-bandwidth-bound. The SM clock barely matters; what rules is
HBM bandwidth.&lt;/li>
&lt;/ul>
&lt;p>This asymmetry is what LLM-specific DVFS systems exploit:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>System&lt;/th>
&lt;th>Energy saving&lt;/th>
&lt;th>Latency penalty&lt;/th>
&lt;th>Hardware&lt;/th>
&lt;th>Source&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>DVFS-GPT (HAL)&lt;/td>
&lt;td>up to &lt;strong>32%&lt;/strong> at iso-latency&lt;/td>
&lt;td>~1–2%&lt;/td>
&lt;td>A100, RTX 4090&lt;/td>
&lt;td>HAL 05190051&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>EcoInfer&lt;/td>
&lt;td>up to &lt;strong>25.4%&lt;/strong> (average 21.5%)&lt;/td>
&lt;td>&amp;lt;5%&lt;/td>
&lt;td>A100&lt;/td>
&lt;td>arXiv MDPI Electronics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DualScale (disaggregated prefill/decode)&lt;/td>
&lt;td>&lt;strong>22–45%&lt;/strong> with SLO&lt;/td>
&lt;td>1–6%&lt;/td>
&lt;td>H100&lt;/td>
&lt;td>arXiv 2602.18755&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GreenLLM (SLO-aware DVFS)&lt;/td>
&lt;td>&lt;strong>18–30%&lt;/strong>&lt;/td>
&lt;td>within the SLO&lt;/td>
&lt;td>—&lt;/td>
&lt;td>arXiv 2508.16449&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The paper &lt;a href="https://arxiv.org/abs/2501.08219">arXiv 2501.08219&lt;/a> measures that reducing the
GPU frequency from 2,842 MHz to 180 MHz achieves an average saving of 42% with only a 1–6%
latency increase in the decode phase, because decode is memory-bound and the SM
frequency is not the bottleneck.&lt;/p>
&lt;h3 id="commands-manual-dvfs">Commands (manual DVFS)&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"># Lock the SM clock at 1,200 MHz (H100 boost: 1,980 MHz) for decode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo nvidia-smi -i &lt;span class="m">0&lt;/span> -lgc 1200,1200
&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"># Restore the dynamic clock&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo nvidia-smi -i &lt;span class="m">0&lt;/span> -rgc
&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"># Persistent mode (needed for the lock to survive between jobs)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo nvidia-smi -pm &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For automatic DVFS tuned to the SLO (target TPOT), systems such as GreenLLM or
DVFS-GPT measure the time of each iteration and adjust the frequency in real time, without
overrunning the latency budget.&lt;/p>
&lt;hr>
&lt;h2 id="lever-5-model-selection-slm-moe-vs-dense">Lever 5: model selection (SLM, MoE vs dense)&lt;/h2>
&lt;h3 id="the-effect-of-scale">The effect of scale&lt;/h3>
&lt;p>J/token scales with model size, but sublinearly. For Llama-3
(&lt;a href="https://arxiv.org/pdf/2504.17674">arXiv 2504.17674&lt;/a>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Model&lt;/th>
&lt;th>Parameters&lt;/th>
&lt;th>Relative J/token&lt;/th>
&lt;th>Factor vs 1B&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Llama-3 1B&lt;/td>
&lt;td>1B&lt;/td>
&lt;td>1.0×&lt;/td>
&lt;td>reference&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama-3 8B&lt;/td>
&lt;td>8B&lt;/td>
&lt;td>~2.3×&lt;/td>
&lt;td>×2.3 (not ×8)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama-3 70B&lt;/td>
&lt;td>70B&lt;/td>
&lt;td>~7.3×&lt;/td>
&lt;td>×7.3 (not ×70)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama-3 405B&lt;/td>
&lt;td>405B&lt;/td>
&lt;td>~40×&lt;/td>
&lt;td>×40 (not ×405)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>J/token grows ~7.3× from 1B to 70B, while parameters grow ×70: hardware
efficiency (caches, tensor parallelism, kernels) cushions the scaling.
Even so, moving from 70B to 8B cuts J/token by ~68% if the quality is sufficient for
the task.&lt;/p>
&lt;h3 id="moe-vs-dense">MoE vs dense&lt;/h3>
&lt;p>MoE models activate only a subset of experts per token (typically 2 of 8–64), which
reduces active compute. But there is an implementation trap:&lt;/p>
&lt;ul>
&lt;li>OLMoE-1B-7B (MoE, 1B active) vs OLMo-1B (dense): the MoE consumes &lt;strong>54.24% more&lt;/strong>
energy per token than the dense model of the same active size
(&lt;a href="https://arxiv.org/pdf/2504.17674">arXiv 2504.17674&lt;/a>), because the fused expert
kernels are ~19–63% slower than an equivalent dense GEMM.&lt;/li>
&lt;li>OLMoE-1B-7B vs OLMo-7B (dense): here the MoE is indeed more efficient (7B active → only 1B
active, a ~7× reduction in active FLOPs per token).&lt;/li>
&lt;/ul>
&lt;p>The rule: MoE is efficient when the comparison is &lt;strong>equal total parameters, fewer
active&lt;/strong> (for example Mixtral 8×7B vs dense Llama-3 70B). It is not efficient when the comparison
is &lt;strong>equal active parameters&lt;/strong> per token.&lt;/p>
&lt;h3 id="model-selection-table-by-use-case">Model selection table by use case&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Use case&lt;/th>
&lt;th>Recommended model&lt;/th>
&lt;th>Efficiency rationale&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Simple chat, entity extraction&lt;/td>
&lt;td>SLM 7–8B FP8&lt;/td>
&lt;td>−68% J/token vs 70B&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RAG with long contexts&lt;/td>
&lt;td>70B FP8 or MoE 8×7B&lt;/td>
&lt;td>optimal quality/J at K&amp;gt;2K tokens&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Complex code, reasoning&lt;/td>
&lt;td>70B FP8 or 405B INT4&lt;/td>
&lt;td>quality is non-negotiable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Classification, embeddings&lt;/td>
&lt;td>dedicated model (e.g. E5, BGE)&lt;/td>
&lt;td>orders of magnitude less J&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Deferrable offline batch&lt;/td>
&lt;td>large MoE (DeepSeek, Mixtral)&lt;/td>
&lt;td>high quality, batch throughput&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="lever-6-speculative-decoding">Lever 6: speculative decoding&lt;/h2>
&lt;h3 id="mechanics-4">Mechanics&lt;/h3>
&lt;p>Speculative decoding uses a small draft model to propose several
tokens, which the target model verifies in parallel in a single forward pass. If the tokens
are accepted, K tokens are generated per step instead of 1: potentially K× throughput.&lt;/p>
&lt;h3 id="the-energy-efficiency-condition">The energy efficiency condition&lt;/h3>
&lt;p>The J/token benefit &lt;strong>depends on batch size&lt;/strong> (&lt;a href="https://arxiv.org/pdf/2602.09113">arXiv 2602.09113&lt;/a>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Batch size&lt;/th>
&lt;th>Effect on J/token&lt;/th>
&lt;th>Reason&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1–16 (low batch)&lt;/td>
&lt;td>&lt;strong>−up to 29%&lt;/strong>&lt;/td>
&lt;td>the draft model fills the waiting time; verification is cheap&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>32–64 (medium batch)&lt;/td>
&lt;td>~0% (neutral)&lt;/td>
&lt;td>verification overhead cancels out the gain&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>128+ (high batch)&lt;/td>
&lt;td>&lt;strong>+25%&lt;/strong> (worse)&lt;/td>
&lt;td>GPU saturated; the draft model adds overhead without saving&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Conclusion: speculative decoding is a J/token lever only in a &lt;strong>low-latency regime
(batch≤16)&lt;/strong>; in high-throughput serving it is counterproductive in J/token terms
(although it reduces TPOT latency).&lt;/p>
&lt;h3 id="oss-tooling-2">OSS tooling&lt;/h3>
&lt;p>vLLM supports speculative decoding with &lt;code>--speculative-model&lt;/code> and &lt;code>--num-speculative-tokens&lt;/code>.
TRT-LLM does too. The draft model must come from the same family (for example Llama-3 8B as
draft for Llama-3 70B).&lt;/p>
&lt;hr>
&lt;h2 id="lever-7-prefix-caching-and-chunked-prefill">Lever 7: prefix caching and chunked prefill&lt;/h2>
&lt;h3 id="prefix-caching">Prefix caching&lt;/h3>
&lt;p>The prefill KV-cache of a shared prompt (system prompt, few-shot examples, document
context) is identical across requests. Storing and reusing it eliminates the prefill
recompute for that fraction of the prompt.&lt;/p>
&lt;p>Energy impact: prefill is compute-bound and draws power spikes. Removing the
repeated prefill removes those spikes. In scenarios with a fixed 1,000-token system prompt
and 200-token user prompts, the saving is ~1,000/(1,000+200) = &lt;strong>~83% of prefill
energy&lt;/strong>. In total J/token, the effect depends on the length of the shared prompt against
the generation length.&lt;/p>
&lt;p>Enabling it in vLLM:&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 meta-llama/Llama-3-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> --enable-prefix-caching &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-num-seqs &lt;span class="m">256&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="chunked-prefill-sarathi">Chunked prefill (SARATHI)&lt;/h3>
&lt;p>Chunked prefill splits the prefill into fixed-size chunks and processes each chunk together with
decode tokens from other requests. The benefit: the prefill of a long prompt no longer
blocks the decode of other requests (reducing TPOT spikes), which allows higher
GPU utilisation over time.&lt;/p>
&lt;p>In terms of total J/token, chunked prefill reduces the node&amp;rsquo;s peak power
(fewer prefill spikes) and improves average utilisation, which translates into ~10–15% less
J/token on mixed loads with long prompts (&lt;a href="https://arxiv.org/pdf/2308.16369">arXiv 2308.16369&lt;/a>).&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"># Enable chunked prefill in vLLM (chunk size: 512 tokens)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve meta-llama/Llama-3-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> --enable-chunked-prefill &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-num-batched-tokens &lt;span class="m">512&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="lever-8-scheduling-to-clean-hours">Lever 8: scheduling to clean hours&lt;/h2>
&lt;p>This lever does not reduce physical J/token, but it does reduce the &lt;strong>gCO₂/token&lt;/strong> of deferrable load.
The mechanics: moving training, document ingestion, batch re-ranking and fine-tuning
to the hours of lowest grid carbon intensity.&lt;/p>
&lt;p>Impact figures for temporal shifting (&lt;a href="https://arxiv.org/pdf/2507.09942">ACM SLIT, arXiv 2507.09942&lt;/a>):&lt;/p>
&lt;ul>
&lt;li>CO₂ reduction from temporal shifting: &lt;strong>up to 34.7%&lt;/strong> for flexible loads.&lt;/li>
&lt;li>With carbon-aware reinforcement learning (Eco-Orchestrator): up to a &lt;strong>70% offset&lt;/strong> in
integrated deployments.&lt;/li>
&lt;li>In Spain, the hourly variation in intensity is ~80–250 gCO₂/kWh: a factor of ~3× within the
same day (see &lt;a href="https://blog.lo0.es/en/posts/watt-to-carbon-pue-grid-intensity-gco2-per-token/">from watt to carbon&lt;/a>).&lt;/li>
&lt;/ul>
&lt;p>The practical implementation uses Kueue or Volcano on Kubernetes, with a grid intensity
metric (the ElectricityMaps API or esios) as the scheduling signal. Loads are marked with a
&lt;code>carbon-deadline&lt;/code> annotation (the maximum tolerable wait) and the scheduler places them
in the minimum-intensity hours within that window.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Kueue annotation for carbon-aware scheduling&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="nt">metadata&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="nt">annotations&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="nt">kueue.x-k8s.io/carbon-aware&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&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="nt">kueue.x-k8s.io/carbon-deadline&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;8h&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="the-power-cap-vs-throughput-curve-the-sweet-spot">The power cap vs throughput curve: the sweet spot&lt;/h2>
&lt;div class="diagram" style="max-width:720px;margin:1rem auto;">
&lt;svg viewBox="0 0 720 320" role="img" aria-label="Power cap vs throughput curve on H100 SXM: the efficiency sweet spot (minimum J/token) sits between 490 and 560 W, where the power reduction outpaces the throughput drop" xmlns="http://www.w3.org/2000/svg">
&lt;style>.ax{fill:none;stroke:currentColor;stroke-width:1}.ln{fill:none;stroke:currentColor;stroke-width:1.8}.ln2{fill:none;stroke:currentColor;stroke-width:1.8;stroke-dasharray:6 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.dot{fill:currentColor}&lt;/style>
&lt;defs>&lt;marker id="arr" 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="currentColor"/>&lt;/marker>&lt;/defs>
&lt;line class="ax" x1="60" y1="30" x2="60" y2="240" marker-end="url(#arr)"/>
&lt;line class="ax" x1="60" y1="240" x2="660" y2="240" marker-end="url(#arr)"/>
&lt;text x="62" y="22" class="ts">%&lt;/text>
&lt;text x="330" y="268" class="ts">Power cap (W) →&lt;/text>
&lt;text x="10" y="80" class="ts" transform="rotate(-90 10 140)">%&lt;/text>
&lt;text x="62" y="245" class="ts">200&lt;/text>
&lt;text x="152" y="245" class="ts">280&lt;/text>
&lt;text x="242" y="245" class="ts">350&lt;/text>
&lt;text x="332" y="245" class="ts">420&lt;/text>
&lt;text x="412" y="245" class="ts">490&lt;/text>
&lt;text x="492" y="245" class="ts">560&lt;/text>
&lt;text x="572" y="245" class="ts">630&lt;/text>
&lt;text x="642" y="245" class="ts">700&lt;/text>
&lt;text x="64" y="235" class="ts">0&lt;/text>
&lt;text x="54" y="195" class="ts">50&lt;/text>
&lt;text x="54" y="155" class="ts">70&lt;/text>
&lt;text x="54" y="115" class="ts">80&lt;/text>
&lt;text x="54" y="75" class="ts">90&lt;/text>
&lt;text x="44" y="45" class="ts">100&lt;/text>
&lt;line class="ax" x1="58" y1="40" x2="62" y2="40"/>
&lt;line class="ax" x1="58" y1="75" x2="62" y2="75"/>
&lt;line class="ax" x1="58" y1="115" x2="62" y2="115"/>
&lt;line class="ax" x1="58" y1="155" x2="62" y2="155"/>
&lt;line class="ax" x1="58" y1="195" x2="62" y2="195"/>
&lt;path class="ln" d="M90,195 L180,160 L240,130 L330,115 L420,95 L500,75 L590,55 L650,40"/>
&lt;path class="ln2" d="M90,160 L180,140 L240,128 L330,118 L420,108 L500,98 L590,92 L650,90"/>
&lt;text x="580" y="48" class="ts">power (% TDP)&lt;/text>
&lt;text x="560" y="104" class="ts">throughput (%)&lt;/text>
&lt;circle class="dot" cx="420" cy="95" r="5"/>
&lt;circle class="dot" cx="500" cy="75" r="5"/>
&lt;line class="ax" x1="420" y1="95" x2="420" y2="240" stroke-dasharray="3 3"/>
&lt;line class="ax" x1="500" y1="75" x2="500" y2="240" stroke-dasharray="3 3"/>
&lt;text x="380" y="88" class="tl">sweet spot&lt;/text>
&lt;text x="362" y="280" class="ts">490–560 W: −30% power, &amp;lt;10% throughput. Minimum J/token.&lt;/text>
&lt;text x="62" y="305" class="ts">H100 SXM (700 W TDP). Memory-bound load (LLM decode). Source: arXiv 2604.11391, NERSC.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Reading the chart: the power curve (solid line) falls faster than the throughput
curve (dashed line) in the 490–560 W zone. Above 560 W, each additional 70 W
gives only ~5% extra throughput; below 420 W, throughput starts
falling faster than power. The &lt;strong>efficiency sweet spot&lt;/strong> (minimum J/token) sits
at 490–560 W for decode-bound loads.&lt;/p>
&lt;p>For compute-bound loads (pure prefill), the sweet spot shifts to 500–600 W: prefill
saturates SM compute and needs more power to sustain throughput.&lt;/p>
&lt;hr>
&lt;h2 id="how-to-measure-the-effect-and-validate-quality">How to measure the effect and validate quality&lt;/h2>
&lt;h3 id="measurement-stack">Measurement stack&lt;/h3>
&lt;p>The full stack for measuring the effect of each lever:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>What to measure&lt;/th>
&lt;th>Tool&lt;/th>
&lt;th>Metric&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Real-time GPU power&lt;/td>
&lt;td>DCGM (&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>)&lt;/td>
&lt;td>W per GPU, 100ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>J/token in production&lt;/td>
&lt;td>Kepler + vLLM tokens&lt;/td>
&lt;td>&lt;code>rate(kepler_joules) / rate(vllm_tokens)&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>J/token on the bench&lt;/td>
&lt;td>Zeus (&lt;code>ZeusMonitor&lt;/code>)&lt;/td>
&lt;td>J measured during the benchmark&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput&lt;/td>
&lt;td>vLLM metrics (&lt;code>vllm:generation_tokens_total&lt;/code>)&lt;/td>
&lt;td>tok/s&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TPOT latency&lt;/td>
&lt;td>vLLM (&lt;code>vllm:e2e_request_latency&lt;/code>)&lt;/td>
&lt;td>ms/tok&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Output quality&lt;/td>
&lt;td>lm-evaluation-harness&lt;/td>
&lt;td>MMLU, HumanEval, etc.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>See the full stack in &lt;a href="https://blog.lo0.es/en/posts/measuring-energy-in-production-kepler-dcgm-stack/">energy tooling&lt;/a>.&lt;/p>
&lt;h3 id="measurement-protocol-per-lever">Measurement protocol per lever&lt;/h3>
&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"># Real-time J/token (Kepler + vLLM in Prometheus)&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="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">kepler_container_joules_total&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">container&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">vllm&lt;/span>&lt;span class="p">&amp;#34;}[&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;span class="line">&lt;span class="cl">&lt;span class="w"> &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="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm_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;span class="line">&lt;span class="cl">&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="c1"># Average power per GPU (DCGM)&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="k">avg&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">gpu&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_POWER_USAGE&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Check that the power cap is active&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="k">avg&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">gpu&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_POWER_LIMIT&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>With Zeus, the configuration sweep is automatic:&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="kn">from&lt;/span> &lt;span class="nn">zeus.monitor&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">ZeusMonitor&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">monitor&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ZeusMonitor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">gpu_indices&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&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">with&lt;/span> &lt;span class="n">monitor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">begin_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fp8_inference&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="c1"># run the benchmark with vLLM FP8&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">run_benchmark&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;llama-3-70b-fp8&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">num_tokens&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">10_000&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">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">monitor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">end_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fp8_inference&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;J/token: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">total_energy&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">10_000&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.4f&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;h3 id="quality-validation">Quality validation&lt;/h3>
&lt;p>Validating that the lever does not degrade quality is done with lm-evaluation-harness
(&lt;a href="https://github.com/EleutherAI/lm-evaluation-harness">github.com/EleutherAI/lm-evaluation-harness&lt;/a>):&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"># Compare MMLU between FP16 and 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>meta-llama/Llama-3-70B-Instruct,dtype&lt;span class="o">=&lt;/span>float16 &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 --num_fewshot &lt;span class="m">5&lt;/span> --output_path ./results/fp16/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&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/Llama-3-70B-Instruct,quantization&lt;span class="o">=&lt;/span>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> --tasks mmlu --num_fewshot &lt;span class="m">5&lt;/span> --output_path ./results/fp8/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The MMLU score difference between FP16 and FP8 with calibration is typically
&amp;lt;1%. Without calibration it can be 2–4%.&lt;/p>
&lt;hr>
&lt;h2 id="pareto-decision-table-energy--latency--quality">Pareto decision table (energy / latency / quality)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Lever&lt;/th>
&lt;th>J/token&lt;/th>
&lt;th>TPOT (latency)&lt;/th>
&lt;th>Quality&lt;/th>
&lt;th>Implementation cost&lt;/th>
&lt;th>Reversibility&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>FP8 quantisation&lt;/td>
&lt;td>↓↓↓&lt;/td>
&lt;td>↓ (improves)&lt;/td>
&lt;td>~ (−&amp;lt;1%)&lt;/td>
&lt;td>Low (1 flag)&lt;/td>
&lt;td>High&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4 AWQ quantisation&lt;/td>
&lt;td>↓↓↓↓&lt;/td>
&lt;td>↓ (improves)&lt;/td>
&lt;td>↓ (−1–3%)&lt;/td>
&lt;td>Medium (recalibrate)&lt;/td>
&lt;td>High&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Continuous batching&lt;/td>
&lt;td>↓↓↓&lt;/td>
&lt;td>~ (neutral on TPOT)&lt;/td>
&lt;td>~&lt;/td>
&lt;td>Low (new engine)&lt;/td>
&lt;td>High&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Power cap 70% TDP&lt;/td>
&lt;td>↓↓&lt;/td>
&lt;td>↑ &amp;lt;10%&lt;/td>
&lt;td>~&lt;/td>
&lt;td>Very low (1 command)&lt;/td>
&lt;td>Immediate&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DVFS decode&lt;/td>
&lt;td>↓↓↓&lt;/td>
&lt;td>↑ &amp;lt;6%&lt;/td>
&lt;td>~&lt;/td>
&lt;td>Medium (script + tuning)&lt;/td>
&lt;td>High&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Speculative decoding&lt;/td>
&lt;td>↓ (low batch)&lt;/td>
&lt;td>↓ (improves TTFT)&lt;/td>
&lt;td>~&lt;/td>
&lt;td>Medium (draft model)&lt;/td>
&lt;td>High&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prefix caching&lt;/td>
&lt;td>↓↓ (prefill)&lt;/td>
&lt;td>↓↓ (TTFT improves)&lt;/td>
&lt;td>~&lt;/td>
&lt;td>Low (1 flag)&lt;/td>
&lt;td>High&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chunked prefill&lt;/td>
&lt;td>↓&lt;/td>
&lt;td>↑ stable TPOT&lt;/td>
&lt;td>~&lt;/td>
&lt;td>Low (1 flag)&lt;/td>
&lt;td>High&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SLM (7B vs 70B)&lt;/td>
&lt;td>↓↓↓↓&lt;/td>
&lt;td>↓↓ (improves a lot)&lt;/td>
&lt;td>↓↓ (−variable)&lt;/td>
&lt;td>High (quality re-eval)&lt;/td>
&lt;td>Medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>MoE (vs dense, same active size)&lt;/td>
&lt;td>↑ (+54%)&lt;/td>
&lt;td>↑&lt;/td>
&lt;td>~&lt;/td>
&lt;td>—&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Scheduling to clean hours&lt;/td>
&lt;td>~ J/token, ↓↓ CO₂&lt;/td>
&lt;td>~ (for deferrable)&lt;/td>
&lt;td>~&lt;/td>
&lt;td>Medium (scheduler)&lt;/td>
&lt;td>High&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Key: ↓↓↓↓ strong improvement · ↓↓↓ significant improvement · ↓↓ moderate improvement · ↓ slight improvement ·
~ neutral · ↑ worsens · ↑↑ worsens significantly.&lt;/p>
&lt;hr>
&lt;h2 id="stacking-levers-the-combined-effect">Stacking levers: the combined effect&lt;/h2>
&lt;p>The levers are mostly &lt;strong>orthogonal&lt;/strong> and they stack. The maximum combined impact
documented in (&lt;a href="https://arxiv.org/pdf/2504.17674">arXiv 2504.17674&lt;/a>):&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Correct application of the relevant inference optimisations can reduce
total energy consumption by up to 73% against unoptimised baselines.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>Recommended sequence by cost-benefit:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Optimised engine&lt;/strong> (vLLM, TRT-LLM) with continuous batching: −25–40% immediately.&lt;/li>
&lt;li>&lt;strong>FP8 quantisation&lt;/strong> (if the hardware supports it: H100, A100 with limited support): a further −30–50%
on the remaining J/token.&lt;/li>
&lt;li>&lt;strong>Power cap at 70% TDP&lt;/strong>: −20–30% power with &amp;lt;10% of throughput. Low cost.&lt;/li>
&lt;li>&lt;strong>Prefix caching&lt;/strong>: turn it on if there are system prompts or repeated contexts. No cost.&lt;/li>
&lt;li>&lt;strong>Chunked prefill&lt;/strong>: if the load has long variable prompts.&lt;/li>
&lt;li>&lt;strong>DVFS in decode&lt;/strong>: a further −22–45% if the SLO allows it.&lt;/li>
&lt;li>&lt;strong>Speculative decoding&lt;/strong>: only if the batch is low (&amp;lt;16) and TTFT latency matters.&lt;/li>
&lt;li>&lt;strong>Model selection&lt;/strong>: move to an SLM if the quality is sufficient (evaluate with
lm-evaluation-harness on your specific task).&lt;/li>
&lt;/ol>
&lt;p>Quantified example on 4×H100 SXM (2,800 W node TDP), Llama-3 70B, chat load:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Configuration&lt;/th>
&lt;th>Power (W)&lt;/th>
&lt;th>Throughput (tok/s)&lt;/th>
&lt;th>J/token&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>PyTorch FP16, static batching&lt;/td>
&lt;td>~2,100&lt;/td>
&lt;td>~400&lt;/td>
&lt;td>&lt;strong>5.25&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>vLLM FP16, continuous batching&lt;/td>
&lt;td>~2,200&lt;/td>
&lt;td>~1,800&lt;/td>
&lt;td>&lt;strong>1.22&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>vLLM FP8, continuous batching&lt;/td>
&lt;td>~2,100&lt;/td>
&lt;td>~2,400&lt;/td>
&lt;td>&lt;strong>0.875&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>+ power cap 490 W/GPU (1,960 W node)&lt;/td>
&lt;td>~1,960&lt;/td>
&lt;td>~2,160&lt;/td>
&lt;td>&lt;strong>0.907&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>+ prefix caching (40% hit rate)&lt;/td>
&lt;td>~1,960&lt;/td>
&lt;td>~2,600&lt;/td>
&lt;td>&lt;strong>0.754&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>+ DVFS decode 1,200 MHz&lt;/td>
&lt;td>~1,500&lt;/td>
&lt;td>~2,400&lt;/td>
&lt;td>&lt;strong>0.625&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>The values are illustrative estimates based on the cited papers applied to
a 4×H100 node. Real J/token depends on the prompt distribution, the effective batch
and the temperature of the models. Measuring it with Zeus or DCGM+Kepler is mandatory.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="reference-hardware-tdp-and-power-cap-ranges">Reference hardware: TDP and power cap ranges&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>GPU&lt;/th>
&lt;th>TDP (W)&lt;/th>
&lt;th>Supported cap range&lt;/th>
&lt;th>Decode sweet spot&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>H100 SXM 80GB&lt;/td>
&lt;td>700&lt;/td>
&lt;td>200–700 W&lt;/td>
&lt;td>~490–560 W&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>H100 PCIe 80GB&lt;/td>
&lt;td>350&lt;/td>
&lt;td>100–350 W&lt;/td>
&lt;td>~245–280 W&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A100 SXM 80GB&lt;/td>
&lt;td>400&lt;/td>
&lt;td>100–400 W&lt;/td>
&lt;td>~280–320 W&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A100 PCIe 80GB&lt;/td>
&lt;td>300&lt;/td>
&lt;td>100–300 W&lt;/td>
&lt;td>~210–240 W&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>L40S 48GB&lt;/td>
&lt;td>350&lt;/td>
&lt;td>100–350 W&lt;/td>
&lt;td>~245–280 W&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RTX 5090 (reference, PCIe)&lt;/td>
&lt;td>575&lt;/td>
&lt;td>~100–575 W&lt;/td>
&lt;td>~400–460 W&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For the exact ranges: &lt;code>nvidia-smi -q -d POWER | grep -E &amp;quot;Min|Max|Current&amp;quot;&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="executive-summary-by-role">Executive summary by role&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Role&lt;/th>
&lt;th>First action&lt;/th>
&lt;th>Second action&lt;/th>
&lt;th>Key tool&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cluster operator&lt;/td>
&lt;td>&lt;code>nvidia-smi -pl [70% TDP]&lt;/code> on every node&lt;/td>
&lt;td>Enable persistent mode&lt;/td>
&lt;td>nvidia-smi, DCGM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Serving engineer&lt;/td>
&lt;td>Migrate to vLLM FP8 with continuous batching&lt;/td>
&lt;td>Enable prefix caching&lt;/td>
&lt;td>vLLM, llm-compressor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Platform architect&lt;/td>
&lt;td>Select the smallest model that meets the quality SLO&lt;/td>
&lt;td>Design a carbon-based scheduling policy&lt;/td>
&lt;td>lm-evaluation-harness, Kueue&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>MLOps team&lt;/td>
&lt;td>Set up a power cap sweep with Zeus&lt;/td>
&lt;td>Instrument J/token in Prometheus&lt;/td>
&lt;td>Zeus, DCGM exporter&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>See the full measurement stack in &lt;a href="https://blog.lo0.es/en/posts/measuring-energy-in-production-kepler-dcgm-stack/">energy tooling&lt;/a>
and the J/token methodology in &lt;a href="https://blog.lo0.es/en/posts/energy-per-token-spain-methodology-electricity-market-speculation/">energy per token&lt;/a>.
For the context of reference energy leaderboards and benchmarks, see &lt;a href="https://blog.lo0.es/en/posts/llm-energy-leaderboards-wh-per-token/">LLM energy leaderboards&lt;/a>.
The connection with full TCO is in &lt;a href="https://blog.lo0.es/en/posts/gpu-idle-cost-not-on-any-invoice/">GPU utilisation as FinOps&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>arXiv 2504.17674 · Energy Considerations of LLM Inference and Efficiency Optimizations (CMU/Hugging Face, 2025) — &lt;a href="https://arxiv.org/pdf/2504.17674">https://arxiv.org/pdf/2504.17674&lt;/a>&lt;/li>
&lt;li>arXiv 2604.11391 · Architectural Trade-offs: power-capping NVIDIA H100 and H200 (FAU Erlangen, 2026) — &lt;a href="https://arxiv.org/html/2604.11391v1">https://arxiv.org/html/2604.11391v1&lt;/a>&lt;/li>
&lt;li>arXiv 2512.03024 · TokenPowerBench: Benchmarking the Power Consumption of LLM Inference — &lt;a href="https://arxiv.org/pdf/2512.03024">https://arxiv.org/pdf/2512.03024&lt;/a>&lt;/li>
&lt;li>arXiv 2602.09113 · Benchmarking the Energy Savings with Speculative Decoding Strategies — &lt;a href="https://arxiv.org/pdf/2602.09113">https://arxiv.org/pdf/2602.09113&lt;/a>&lt;/li>
&lt;li>arXiv 2501.08219 · Characterizing LLM Inference Energy-Performance Tradeoffs across Workloads and GPU Scaling — &lt;a href="https://arxiv.org/abs/2501.08219">https://arxiv.org/abs/2501.08219&lt;/a>&lt;/li>
&lt;li>arXiv 2602.18755 · DualScale: Energy-Efficient Disaggregated LLM Serving via Phase-Aware Placement and DVFS — &lt;a href="https://arxiv.org/pdf/2602.18755">https://arxiv.org/pdf/2602.18755&lt;/a>&lt;/li>
&lt;li>arXiv 2508.16449 · GreenLLM: SLO-Aware Dynamic Frequency Scaling for Energy-Efficient LLM Serving — &lt;a href="https://arxiv.org/pdf/2508.16449">https://arxiv.org/pdf/2508.16449&lt;/a>&lt;/li>
&lt;li>HAL 05190051 · DVFS-GPT: Dynamic Voltage and Frequency Scaling for Energy-Efficient LLMs — &lt;a href="https://hal.science/hal-05190051">https://hal.science/hal-05190051&lt;/a>&lt;/li>
&lt;li>arXiv 2308.16369 · SARATHI: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills — &lt;a href="https://arxiv.org/pdf/2308.16369">https://arxiv.org/pdf/2308.16369&lt;/a>&lt;/li>
&lt;li>arXiv 2507.09942 · Green-LLM: Optimal Workload Allocation for Environmentally-Aware Distributed Inference — &lt;a href="https://arxiv.org/pdf/2507.09942">https://arxiv.org/pdf/2507.09942&lt;/a>&lt;/li>
&lt;li>arXiv 2505.06371 · The ML.ENERGY Benchmark: Toward Automated Inference Energy Measurement and Optimization — &lt;a href="https://arxiv.org/html/2505.06371v1">https://arxiv.org/html/2505.06371v1&lt;/a>&lt;/li>
&lt;li>Zeus Project (ml.energy) — &lt;a href="https://ml.energy/zeus/">https://ml.energy/zeus/&lt;/a>&lt;/li>
&lt;li>Zeus · PyTorch blog: Deep Learning Energy Measurement and Optimization — &lt;a href="https://pytorch.org/blog/zeus/">https://pytorch.org/blog/zeus/&lt;/a>&lt;/li>
&lt;li>Baseten · 33% faster LLM inference with FP8 quantization — &lt;a href="https://www.baseten.co/blog/33-faster-llm-inference-with-fp8-quantization/">https://www.baseten.co/blog/33-faster-llm-inference-with-fp8-quantization/&lt;/a>&lt;/li>
&lt;li>NVIDIA · Managing Power Capping (DGX H100/H200 User Guide) — &lt;a href="https://docs.nvidia.com/dgx/dgxh100-user-guide/power-capping.html">https://docs.nvidia.com/dgx/dgxh100-user-guide/power-capping.html&lt;/a>&lt;/li>
&lt;li>NERSC · GPU Power Capping documentation — &lt;a href="https://docs.nersc.gov/jobs/power-capping/">https://docs.nersc.gov/jobs/power-capping/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference v5.1 results — &lt;a href="https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/">https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/&lt;/a>&lt;/li>
&lt;li>vLLM · Disaggregated Prefill documentation — &lt;a href="https://docs.vllm.ai/en/latest/features/disagg_prefill/">https://docs.vllm.ai/en/latest/features/disagg_prefill/&lt;/a>&lt;/li>
&lt;li>Microsoft Research · Characterizing Power Management Opportunities for LLMs in the Cloud (ASPLOS 2024) — &lt;a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2024/03/GPU_Power_ASPLOS_24.pdf">https://www.microsoft.com/en-us/research/wp-content/uploads/2024/03/GPU_Power_ASPLOS_24.pdf&lt;/a>&lt;/li>
&lt;li>EcoInfer (MDPI Electronics) · Optimizing Energy Efficiency with Latency Guarantees Through Iteration-Level GPU Frequency Control — &lt;a href="https://doi.org/10.3390/electronics15102139">https://doi.org/10.3390/electronics15102139&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Aggressive quantisation (state of the art): from 4-bit to ternary</title><link>https://blog.lo0.es/en/posts/aggressive-quantisation-4-bit-to-ternary/</link><pubDate>Tue, 09 Jun 2026 02:10:00 +0000</pubDate><guid>https://blog.lo0.es/en/posts/aggressive-quantisation-4-bit-to-ternary/</guid><description>&lt;blockquote>
&lt;p>This post is the direct continuation of &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">Quantization for LLM inference&lt;/a>, which covers the &amp;ldquo;solved&amp;rdquo; regime (FP8, INT4 with GPTQ/AWQ). Read it first: here I assume the scale+zero-point maths, what GPTQ and AWQ do, and the PTQ/QAT distinction. What we add is the &lt;strong>sub-4-bit frontier&lt;/strong>, where scalar post-hoc quantisation stops working and you have to change tools.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>There is a sharp dividing line around 4 bits. Above it, quantising is a solved problem: INT8 is indistinguishable from BF16, and INT4 with a good method (AWQ, GPTQ) loses 1-2 MMLU points and little else. The method is still the usual one, take each weight, scale it, round it to a short integer, and it works. Below 4 bits, that method collapses: at 2 bits, naive scalar quantisation can double perplexity. The reason is geometric, each weight has only 4 possible values and the rounding error stops being negligible, and the way out is not &amp;ldquo;round better&amp;rdquo;, it is to &lt;strong>change the representation&lt;/strong>. State-of-the-art 2-bit methods (AQLM, QuIP#, QTIP) stop quantising individual weights and quantise &lt;strong>vectors&lt;/strong> of weights against dictionaries (codebooks), and they &amp;ldquo;whiten&amp;rdquo; the weight matrix to spread its energy and flatten outliers (incoherence processing). Ternary is something else again: BitNet b1.58, with weights in {-1, 0, +1} (~1.58 bits), is not PTQ, it is a model trained natively under that constraint, and it changes the arithmetic of the matmul from multiplications to additions and subtractions, hitting the compute ceiling and the memory ceiling at once. The mental rule: at ≥4-bit you compress the photo; below 4-bit you have to repaint it.&lt;/p>
&lt;h2 id="the-analogy-the-jpeg-that-cannot-be-compressed-any-further">The analogy: the JPEG that cannot be compressed any further&lt;/h2>
&lt;p>In &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">the quantization post&lt;/a> we used the JPEG with an edge detector to explain INT4. The analogy still holds here, but it has to be pushed to its limit.&lt;/p>
&lt;p>A JPEG at quality factor 90 is indistinguishable from the original. At quality 60 you notice it a little, but it is still &amp;ldquo;the same photo&amp;rdquo;. At quality 30 the 8×8 blocks appear, the haloes around edges, the banding in gradients. At quality 10 the image is destroyed: you can tell there &lt;strong>was&lt;/strong> a face, but the details have disappeared under the artefacts. And here is the key: &lt;strong>no JPEG encoder exists that can compress to quality 10 without those artefacts&lt;/strong>, because the JPEG algorithm (blockwise DCT + coefficient quantisation) has an information floor below which its own mechanism introduces the noise.&lt;/p>
&lt;p>What do you do if you need the photo at that file size and you need it to still look good? You do not compress the original harder. You &lt;strong>repaint the photo knowing in advance that it is going to live compressed&lt;/strong>: an illustrator redraws it with clean lines, a reduced palette, no subtle gradients, an image designed to survive brutal compression. The result at &amp;ldquo;10 KB&amp;rdquo; looks infinitely better than the original JPEG crushed to 10 KB, because it is not the same process: one destroys existing information, the other generates new information already adapted to the constraint.&lt;/p>
&lt;p>That is exactly the frontier of this post:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Scalar PTQ (≥4-bit)&lt;/strong> = compressing the JPEG. Up to a certain ratio, it is still the same photo.&lt;/li>
&lt;li>&lt;strong>State-of-the-art vector PTQ (2-bit: AQLM, QuIP#, QTIP)&lt;/strong> = a far more sophisticated image codec (dictionaries, decorrelating transforms) that stretches the compressible ratio a bit further before the collapse.&lt;/li>
&lt;li>&lt;strong>Native ternary (BitNet b1.58)&lt;/strong> = repainting the photo. You are not compressing an existing BF16 model; you are training a new one that is born ternary.&lt;/li>
&lt;/ul>
&lt;h2 id="the-map-of-the-frontier-bit-by-bit">The map of the frontier, bit by bit&lt;/h2>
&lt;p>Quantising a model is deciding how many distinct values each weight can take. With &lt;code>b&lt;/code> bits per weight there are &lt;code>2^b&lt;/code> possible values. The central question is: past what &lt;code>b&lt;/code> is the number of values so small that rounding destroys the model?&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Bits&lt;/th>
&lt;th>Values/weight&lt;/th>
&lt;th>State of the art&lt;/th>
&lt;th>Method required&lt;/th>
&lt;th>Typical loss vs BF16&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>256&lt;/td>
&lt;td>&lt;strong>Solved&lt;/strong>&lt;/td>
&lt;td>RTN, SmoothQuant, FP8&lt;/td>
&lt;td>~0 (indistinguishable)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>16&lt;/td>
&lt;td>&lt;strong>Solved&lt;/strong>&lt;/td>
&lt;td>AWQ, GPTQ&lt;/td>
&lt;td>1-2 pp MMLU, +0.1-0.3 PPL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>8&lt;/td>
&lt;td>Small degradation&lt;/td>
&lt;td>tuned GPTQ/AWQ, GGUF Q3_K&lt;/td>
&lt;td>3-5 pp MMLU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>4&lt;/td>
&lt;td>Serious unless SOTA&lt;/td>
&lt;td>&lt;strong>AQLM, QuIP#, QTIP&lt;/strong> (non-scalar)&lt;/td>
&lt;td>scalar: collapse; SOTA: 4-8 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1.58&lt;/td>
&lt;td>3 (ternary)&lt;/td>
&lt;td>Native only&lt;/td>
&lt;td>&lt;strong>BitNet b1.58&lt;/strong> (QAT/native training)&lt;/td>
&lt;td>n/a (not PTQ)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2 (binary)&lt;/td>
&lt;td>Research&lt;/td>
&lt;td>native, dubious claims&lt;/td>
&lt;td>large / no clear methodology&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The three transitions that matter:&lt;/p>
&lt;p>&lt;strong>8 → 4 bits: nothing breaks.&lt;/strong> With 16 levels per weight and one scale per block of 128, the rounding error is small relative to the dynamics of the weights. GPTQ compensates for the error by propagating it to the neighbouring weights; AWQ protects the ~1 % of salient channels. The model barely notices. This is in the previous post.&lt;/p>
&lt;p>&lt;strong>4 → 2 bits: the elbow.&lt;/strong> Two things happen at once here. First, with only 4 levels, the scalar quantiser can no longer represent the weight distribution, which is roughly Gaussian with long tails, without a proportionally enormous rounding error. Second, and more subtly: the quantisation error stops being &amp;ldquo;small noise that the model absorbs&amp;rdquo; and becomes &lt;strong>structured&lt;/strong>, systematically biasing the activations. Naive scalar PTQ at 2 bits on a Llama 8B typically doubles perplexity or worse. That is the elbow of the curve.&lt;/p>
&lt;p>&lt;strong>2 → 1.58 bits: a change of nature.&lt;/strong> You do not cross this with a better compression method. You cross it by training the model with the constraint from the start. It is a discontinuity: to the left you are doing PTQ, to the right you are doing training.&lt;/p>
&lt;h2 id="why-scalar-ptq-collapses-below-4-bits">Why scalar PTQ collapses below 4 bits&lt;/h2>
&lt;p>The scalar quantiser has a fundamental limitation: it quantises each weight separately, ignoring the fact that the weights of a row or column are correlated and that the error of one could be compensated by another. At 4 bits this matters little; at 2 bits it is lethal. There are three possible attacks, and the state-of-the-art methods use all three.&lt;/p>
&lt;h3 id="1-vector-quantisation-dictionaries-instead-of-scales">1. Vector quantisation: dictionaries instead of scales&lt;/h3>
&lt;p>Instead of mapping each weight to one of 4 values, group the weights into &lt;strong>vectors&lt;/strong> (say, of 8 weights) and map each vector to the nearest entry of a learned dictionary (codebook). If the dictionary has 256 entries, encoding a vector of 8 weights costs 8 bits (the index) → 1 bit/weight, but every &amp;ldquo;reconstructed value&amp;rdquo; is a point in an 8-dimensional space chosen to minimise the error over the real distribution of the weights.&lt;/p>
&lt;p>The advantage is information-theoretic: a vector dictionary can place its reconstruction points where the weights actually are (in clusters), whereas the scalar quantiser is forced to put its 4 levels on a regular grid, wasting resolution on empty regions. It is the difference between a road map with a uniform grid and one that puts more detail where the cities are.&lt;/p>
&lt;p>&lt;strong>AQLM&lt;/strong> (Additive Quantization of Language Models, arXiv:2401.06118) takes this to the extreme with additive quantisation: each weight vector is reconstructed as the &lt;strong>sum of several codes&lt;/strong> from several dictionaries (multi-codebook). It is more expressive than a single dictionary because the number of combinations is the product of the sizes, not the sum. AQLM was one of the first methods to make 2-bit &amp;ldquo;usable&amp;rdquo; (not collapsed) on large models, at the cost of an expensive calibration process and specialised inference kernels.&lt;/p>
&lt;h3 id="2-incoherence-processing-whitening-the-matrix">2. Incoherence processing: whitening the matrix&lt;/h3>
&lt;p>The second attack targets the outliers. The weight matrices of a transformer have a few entries (and a few channels) with a magnitude much larger than the rest. Those outliers dominate the quantiser&amp;rsquo;s range: if you have to represent a weight of magnitude 8 while the rest are of magnitude 0.5, your scale stretches to cover the 8 and you waste almost all the resolution.&lt;/p>
&lt;p>&lt;strong>Incoherence processing&lt;/strong> (the core idea of QuIP and QuIP#) attacks this by multiplying the weight matrix &lt;code>W&lt;/code> by random orthogonal matrices on the left and on the right: &lt;code>W' = U W V^T&lt;/code>. Because &lt;code>U&lt;/code> and &lt;code>V&lt;/code> are orthogonal, the operation is invertible and the maths of the product can be undone at inference time by absorbing it into the neighbouring layers (just as AWQ absorbs its scales). But the rotation spreads the energy: an &amp;ldquo;incoherent&amp;rdquo; matrix has its values distributed almost uniformly, with no concentrated outliers, because mixing coordinates with a random rotation flattens the distribution (it is, in essence, the central limit theorem acting on linear combinations). A matrix without outliers quantises far better at 2 bits. It is the equivalent of &amp;ldquo;whitening&amp;rdquo; a signal before digitising it.&lt;/p>
&lt;p>&lt;strong>QuIP#&lt;/strong> (arXiv:2402.04396) combines incoherence processing with E8 lattice codes: rather than an arbitrary dictionary, it uses the E8 lattice (an optimal sphere packing in 8 dimensions, the best known). Quantising vectors of 8 weights against the E8 lattice gives the smallest reconstruction error possible for a given bit density, because E8 is literally the most efficient way to place points in 8D. It is classical coding theory applied to LLM weights.&lt;/p>
&lt;h3 id="3-coding-with-memory-trellis">3. Coding with memory: trellis&lt;/h3>
&lt;p>&lt;strong>QTIP&lt;/strong> (arXiv:2406.11235) adds the third attack: trellis-coded quantisation. Instead of quantising each vector independently, it models the sequence of weights as a path through a trellis (the same structure as the convolutional codes of telecommunications) and picks the optimal code sequence with the Viterbi algorithm. The intuition: introducing &lt;strong>memory&lt;/strong> between successive quantisations allows correlated errors that cancel out, instead of independent errors that accumulate. QTIP, on top of incoherence processing, improves on QuIP# in quality at 2-3 bits while keeping fast inference kernels.&lt;/p>
&lt;p>The idea common to all three: &lt;strong>stop quantising scalars and start quantising vectors with dictionaries, and decorrelate the matrix before doing so&lt;/strong>. None of them is &amp;ldquo;rounding better&amp;rdquo;; all three change the representation at the root. That is why, below 4 bits, a flag in vLLM is no longer enough: you need co-design of the quantisation method and the inference kernel.&lt;/p>
&lt;h2 id="native-ternary-bitnet-b158">Native ternary: BitNet b1.58&lt;/h2>
&lt;p>Here we change continents. Everything above is &lt;strong>PTQ&lt;/strong>: it starts from a trained BF16 model and compresses it. BitNet&amp;rsquo;s ternary compresses nothing.&lt;/p>
&lt;p>&lt;strong>BitNet b1.58&lt;/strong> (arXiv:2402.17764) trains a transformer from scratch where &lt;strong>every weight is constrained to {-1, 0, +1}&lt;/strong> throughout training. Three values ⇒ log₂(3) ≈ &lt;strong>1.58 bits/weight&lt;/strong>. Quantisation is not a later step: the linear layers (&lt;code>BitLinear&lt;/code>) quantise their weights to ternary in the forward pass of every training step, and gradients flow through a straight-through estimator. The model learns to work with ternary weights. This is QAT taken to the extreme: not a short robustness fine-tune, but the constraint present from the first token of training.&lt;/p>
&lt;p>That difference is what dodges the elbow of the curve. PTQ at 2 bits tries to find the best ternary or quaternary approximation of a model that was trained expecting full precision, and that model has &amp;ldquo;fragile&amp;rdquo; weights that depend on nuances 2 bits cannot capture. BitNet, by contrast, never had those nuances: its weights were born ternary, so the network distributed its representational capacity in a way compatible with the constraint. It is repainting the photo instead of compressing it.&lt;/p>
&lt;h3 id="what-changes-is-not-just-the-memory-it-is-the-arithmetic">What changes is not just the memory, it is the arithmetic&lt;/h3>
&lt;p>The most underrated point about BitNet: with weights in {-1, 0, +1}, multiplication disappears from the matmul. Multiplying an activation &lt;code>x&lt;/code> by a ternary weight &lt;code>w&lt;/code> is trivial: if &lt;code>w = +1&lt;/code> you add &lt;code>x&lt;/code>, if &lt;code>w = -1&lt;/code> you subtract &lt;code>x&lt;/code>, if &lt;code>w = 0&lt;/code> you do nothing. The dominant operation of a transformer, the matrix-vector product, goes from a sea of floating point multiply-accumulate (MAC) to integer additions and subtractions.&lt;/p>
&lt;p>This matters because it connects to the roofline. As explained in &lt;a href="https://blog.lo0.es/en/posts/roofline-flips-optimising-small-models/">The inverted roofline of small models&lt;/a>, LLM inference has two ceilings: &lt;strong>memory&lt;/strong> (HBM bandwidth for loading weights) and compute (tensor core FLOPs). Normal quantisation (INT4, FP8) attacks only the memory ceiling: the weight takes up less space, but to multiply it you dequantise to FP16 and do the same multiplication as always. Ternary attacks both ceilings at once: the weight occupies 1.58 bits (memory) &lt;strong>and&lt;/strong> the operation is an addition instead of a multiplication (compute). That is why BitNet needs its own kernels, bitnet.cpp, that execute the ternary matmul without ever going through FP16; a kernel that dequantised to FP16 in order to multiply would throw away half the advantage.&lt;/p>
&lt;p>The honest trade-off: BitNet b1.58 is training from scratch. You cannot &amp;ldquo;convert your Llama 8B to BitNet&amp;rdquo;. If you want ternary, you train (or use) a natively ternary model, with everything that implies in pre-training cost and weight availability. Today it is a research line with published models at modest scales, not a drop-in replacement for your current serving.&lt;/p>
&lt;h2 id="qat-as-the-bridge-between-ptq-and-native">QAT as the bridge between PTQ and native&lt;/h2>
&lt;p>Between &amp;ldquo;compressing post-hoc&amp;rdquo; (PTQ) and &amp;ldquo;training natively ternary&amp;rdquo; (BitNet) there is a middle point: &lt;strong>QAT&lt;/strong> (Quantization-Aware Training). You take an already trained model and do a short fine-tune with the quantisation operations inside the loop, so that it learns to be robust to low bit counts without paying for a full pre-training.&lt;/p>
&lt;p>&lt;strong>Gemma 3&lt;/strong> publishes official &lt;strong>QAT&lt;/strong> variants for exactly this: models that, after the QAT fine-tune, hold up at INT4 with far less quality loss than pure PTQ on the same model. The cost is a training cost (GPU hours to days on an existing model), not an inference cost. For INT4 with QAT you recover almost all the quality; for 2-bit, QAT helps but the terrain is still difficult; for ternary, QAT stops being a &amp;ldquo;short fine-tune&amp;rdquo; and becomes native training (BitNet).&lt;/p>
&lt;p>The decision hierarchy:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>PTQ&lt;/strong> = default at ≥4 bits. Minutes to hours, without touching training weights. It covers 90 % of production.&lt;/li>
&lt;li>&lt;strong>QAT&lt;/strong> = when PTQ loses too much and the difference matters. Low bits (2-3), or sensitive models. You pay for a fine-tune.&lt;/li>
&lt;li>&lt;strong>Native (ternary)&lt;/strong> = when you want to go below 2 bits &lt;strong>and&lt;/strong> change the arithmetic. You pay for pre-training. It only makes sense if you control the model from its creation.&lt;/li>
&lt;/ul>
&lt;h2 id="the-maths-that-matter-footprint-and-how-many-fit">The maths that matter: footprint and how many fit&lt;/h2>
&lt;p>The weight footprint is direct: &lt;code>bytes = (bits/param / 8) × N&lt;/code>, with &lt;code>N&lt;/code> the number of parameters. For an 8B model:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Level&lt;/th>
&lt;th>bits/param&lt;/th>
&lt;th>8B footprint&lt;/th>
&lt;th>Ratio vs BF16&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>BF16&lt;/td>
&lt;td>16&lt;/td>
&lt;td>16.0 GB&lt;/td>
&lt;td>1.0×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT8&lt;/td>
&lt;td>8&lt;/td>
&lt;td>8.0 GB&lt;/td>
&lt;td>2.0×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4&lt;/td>
&lt;td>4&lt;/td>
&lt;td>4.0 GB&lt;/td>
&lt;td>4.0×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3-bit&lt;/td>
&lt;td>3&lt;/td>
&lt;td>3.0 GB&lt;/td>
&lt;td>5.3×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2-bit&lt;/td>
&lt;td>2&lt;/td>
&lt;td>2.0 GB&lt;/td>
&lt;td>8.0×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1.58-bit (ternary)&lt;/td>
&lt;td>~1.58&lt;/td>
&lt;td>~1.6 GB&lt;/td>
&lt;td>~10×&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>(Real ternary takes up slightly more than 1.58 bits/param because you have to pack 5 ternary values into 8 bits, 5 × log₂(3) ≈ 7.92 bits, and because norms and embeddings usually stay at higher precision. The ~1.6 GB figure for 8B is the right order of magnitude.)&lt;/p>
&lt;h3 id="how-many-8b-models-fit-on-an-rtx-4090">How many 8B models fit on an RTX 4090?&lt;/h3>
&lt;p>An &lt;strong>RTX 4090 (24 GB, Ada Lovelace)&lt;/strong> has 24 GB. Reserving ~4 GB for KV cache and activations leaves &lt;strong>20 GB&lt;/strong> for weights. How many distinct 8B models fit loaded at the same time:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Level&lt;/th>
&lt;th>8B footprint&lt;/th>
&lt;th>Models in 20 GB&lt;/th>
&lt;th>Comment&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>BF16&lt;/td>
&lt;td>16.0 GB&lt;/td>
&lt;td>&lt;strong>1&lt;/strong>&lt;/td>
&lt;td>one, with scant headroom left&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT8&lt;/td>
&lt;td>8.0 GB&lt;/td>
&lt;td>&lt;strong>2&lt;/strong>&lt;/td>
&lt;td>two different models&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4&lt;/td>
&lt;td>4.0 GB&lt;/td>
&lt;td>&lt;strong>5&lt;/strong>&lt;/td>
&lt;td>the solved regime; quality ~lossless with AWQ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3-bit&lt;/td>
&lt;td>3.0 GB&lt;/td>
&lt;td>&lt;strong>6&lt;/strong>&lt;/td>
&lt;td>small degradation already visible&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2-bit&lt;/td>
&lt;td>2.0 GB&lt;/td>
&lt;td>&lt;strong>10&lt;/strong>&lt;/td>
&lt;td>only viable with AQLM/QuIP#/QTIP&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1.58-bit&lt;/td>
&lt;td>~1.6 GB&lt;/td>
&lt;td>&lt;strong>~12&lt;/strong>&lt;/td>
&lt;td>natively ternary models only&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The arithmetic is seductive, from 1 to 12 models on the same card, but it should be read sceptically. Jumping from INT4 (5 models, almost lossless) to 2-bit (10 models) doubles the capacity, but only if you use a state-of-the-art method and accept 4-8 MMLU points. And the jump from 2-bit to ternary (10 → 12) is marginal in memory terms: ternary is not justified by footprint against a state-of-the-art 2-bit, it is justified by the arithmetic (the compute ceiling) and because being native it avoids the quality elbow. If your only metric is &amp;ldquo;how many GB it takes&amp;rdquo;, state-of-the-art 2-bit already gives you nearly everything. Ternary is for when you also want the compute saving.&lt;/p>
&lt;h3 id="the-conceptual-curve-perplexity-vs-bits">The conceptual curve: perplexity vs bits&lt;/h3>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 380" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Conceptual curve of perplexity against bits per weight">
&lt;text x="390" y="26" text-anchor="middle" fill="currentColor" font-size="15" font-weight="700">Perplexity vs bits per weight (conceptual): the elbow and the native branch&lt;/text>
&lt;line x1="90" y1="320" x2="730" y2="320" stroke="currentColor" stroke-width="1.5"/>
&lt;line x1="90" y1="60" x2="90" y2="320" stroke="currentColor" stroke-width="1.5"/>
&lt;text x="410" y="362" text-anchor="middle" fill="currentColor" font-size="13">bits per weight (inverted axis: more compressed to the right)&lt;/text>
&lt;text x="30" y="190" text-anchor="middle" fill="currentColor" font-size="13" transform="rotate(-90 30 190)">perplexity (worse at the top)&lt;/text>
&lt;text x="120" y="338" text-anchor="middle" fill="currentColor" font-size="12">16&lt;/text>
&lt;text x="250" y="338" text-anchor="middle" fill="currentColor" font-size="12">8&lt;/text>
&lt;text x="380" y="338" text-anchor="middle" fill="currentColor" font-size="12">4&lt;/text>
&lt;text x="470" y="338" text-anchor="middle" fill="currentColor" font-size="12">3&lt;/text>
&lt;text x="560" y="338" text-anchor="middle" fill="currentColor" font-size="12">2&lt;/text>
&lt;text x="650" y="338" text-anchor="middle" fill="currentColor" font-size="12">1.58&lt;/text>
&lt;line x1="380" y1="60" x2="380" y2="320" stroke="currentColor" stroke-width="0.8" stroke-dasharray="4 3"/>
&lt;text x="384" y="74" fill="currentColor" font-size="11">4-bit frontier&lt;/text>
&lt;polyline points="120,300 250,298 380,292 470,278 560,170 620,95" fill="none" stroke="#c0392b" stroke-width="2.6"/>
&lt;circle cx="120" cy="300" r="4" fill="#c0392b"/>
&lt;circle cx="250" cy="298" r="4" fill="#c0392b"/>
&lt;circle cx="380" cy="292" r="4" fill="#c0392b"/>
&lt;circle cx="470" cy="278" r="4" fill="#c0392b"/>
&lt;circle cx="560" cy="170" r="4" fill="#c0392b"/>
&lt;text x="600" y="92" fill="#c0392b" font-size="12" font-weight="700">naive scalar PTQ&lt;/text>
&lt;text x="600" y="108" fill="#c0392b" font-size="11">collapses &amp;lt;3 bits&lt;/text>
&lt;polyline points="380,292 470,284 560,250 650,232" fill="none" stroke="#2471a3" stroke-width="2.6" stroke-dasharray="6 3"/>
&lt;circle cx="560" cy="250" r="4" fill="#2471a3"/>
&lt;circle cx="650" cy="232" r="4" fill="#2471a3"/>
&lt;text x="560" y="282" fill="#2471a3" font-size="12" font-weight="700">SOTA vector PTQ&lt;/text>
&lt;text x="560" y="298" fill="#2471a3" font-size="11">AQLM / QuIP# / QTIP&lt;/text>
&lt;circle cx="650" cy="225" r="6" fill="#27ae60"/>
&lt;text x="600" y="208" fill="#27ae60" font-size="12" font-weight="700">native ternary&lt;/text>
&lt;text x="600" y="222" fill="#27ae60" font-size="11">BitNet b1.58 (not PTQ)&lt;/text>
&lt;text x="120" y="290" fill="currentColor" font-size="11">≈ flat ≥ 4 bits with a good method&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Three readings of the curve. &lt;strong>One&lt;/strong>: to the right of 4 bits, the three branches are almost stuck together and almost flat, the solved regime. &lt;strong>Two&lt;/strong>: the red branch (naive scalar PTQ) has a brutal elbow between 3 and 2 bits; that is where it doubles perplexity. The blue branch (state-of-the-art vector PTQ) flattens that elbow, it does not remove it, but it makes it tolerable down to 2 bits. &lt;strong>Three&lt;/strong>: the green point of native ternary &lt;strong>is on neither of the two PTQ curves&lt;/strong>, because it is not obtained by compressing: it is obtained by training, and that is why it can fall below the elbow without paying the quality price that any PTQ pays at that bit density. It is the difference between the crushed JPEG and the repainted photo.&lt;/p>
&lt;h2 id="mandatory-scepticism-lossless-1-bit-and-benchmarks-without-methodology">Mandatory scepticism: &amp;ldquo;lossless&amp;rdquo; 1-bit and benchmarks without methodology&lt;/h2>
&lt;p>Three warnings for reading the aggressive-quantisation literature:&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;1-bit lossless&amp;rdquo; almost always comes with small print.&lt;/strong> Pure binary {-1, +1} (1 bit) loses the ability to represent zero, which matters in transformers (many weights are effectively null). That is why the true state of the art at low density is &lt;strong>ternary&lt;/strong> (1.58 bits), not binary: the zero is worth its extra 0.58 of a bit. When a paper announces &amp;ldquo;1-bit&amp;rdquo;, it pays to check whether (a) it really is 1 bit or 1.58 rounded down in the headline, (b) &amp;ldquo;lossless&amp;rdquo; is measured in WikiText perplexity (easy) or on reasoning benchmarks (where the collapse shows up), and (c) it compares against a baseline of the same effective size or against a much larger model in order to inflate the advantage.&lt;/p>
&lt;p>&lt;strong>Flat perplexity ≠ preserved quality.&lt;/strong> Perplexity on a generic corpus is the metric most forgiving of aggressive quantisation. A 2-bit model can have a perplexity almost identical to BF16 and at the same time drop 10 points on GSM8K or on a code benchmark, because multi-step reasoning amplifies errors that average perplexity does not see. Distrust any sub-4-bit claim that only reports perplexity. As we already said in &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">the quantization post&lt;/a>, the loss has to be measured on the target task.&lt;/p>
&lt;p>&lt;strong>Hardware comparability.&lt;/strong> The &amp;ldquo;X times faster&amp;rdquo; numbers for ternary only apply &lt;strong>with the specialised kernels&lt;/strong> (bitnet.cpp) and on hardware where addition/subtraction arithmetic genuinely wins. On a GPU with tensor cores designed for FP16/FP8, a naive ternary kernel can be &lt;strong>slower&lt;/strong> than a well-optimised INT4, because it wastes the silicon. The ternary advantage is real, but it is a &lt;strong>co-design&lt;/strong> advantage (model + kernel + sometimes hardware), not a flag you switch on over your current stack. Any benchmark that does not specify the exact kernel and hardware is noise.&lt;/p>
&lt;h2 id="implications-for-on-premise-inference">Implications for on-premise inference&lt;/h2>
&lt;p>On the &lt;strong>RTX 4090 (24 GB, Ada Lovelace)&lt;/strong>: the practical regime today is still INT4 AWQ for 7-14B models, solved, almost lossless, natively supported. State-of-the-art 2-bit (AQLM/QuIP#/QTIP) is viable and lets you load larger models or more models at once, but it demands the specific kernels of each method and an expensive calibration, and it pays in quality. It makes sense when VRAM is the bottleneck and you accept the trade-off; not as a default. Ternary on a 4090 is experimental: without tensor cores designed for ternary addition and subtraction, the compute advantage is diluted, although the memory saving remains.&lt;/p>
&lt;p>On a &lt;strong>generic 4×H100 SXM cluster (320 GB, NVLink, native FP8)&lt;/strong>: here the default is FP8 (almost indistinguishable quality, native throughput) or INT4 AWQ for models that do not fit in FP8. State-of-the-art sub-4-bit is for serving enormous models (200B+) when neither FP8 nor INT4 fits with the KV cache headroom you want, at the cost of quality and kernel complexity. Native ternary, today, is a research object rather than a production one: its promise, hitting both roofline ceilings, is greater on CPU/edge (where there are no FP8 tensor cores to exploit) than on an H100 cluster, which already has dedicated FP8 hardware.&lt;/p>
&lt;p>The rule of thumb, June 2026: &lt;strong>≥4-bit is settled engineering; state-of-the-art 2-bit is a real lever but with a cost in method and in quality; ternary is an architectural bet, not a deployment tweak&lt;/strong>.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">Quantization for LLM inference: FP8, INT4, GGUF&lt;/a> — the indispensable base: the scale+zero-point maths, GPTQ/AWQ and PTQ vs QAT, all taken for granted here; this post is its continuation towards the sub-4-bit frontier.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/fp8-end-to-end-enable-measure-quality-decide/">FP8 end to end: weights, KV and quality&lt;/a> — the other end of the spectrum, the solved datacenter regime where quantisation costs almost no quality.&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 complementary lever: distilling reduces parameters, quantising reduces bits per parameter; at 2-bit they are usually combined to reach the target footprint.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-pruning-cutting-without-amputating/">Pruning LLM models&lt;/a> — sparsity and aggressive quantisation are orthogonal and they compound: 50 % sparse + 2-bit is another route to the same footprint as ternary.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory&lt;/a> — the ~4 GB we reserve for KV in the 4090 arithmetic come from here; quantising the cache is the other half of the memory budget.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/roofline-flips-optimising-small-models/">The inverted roofline of small models&lt;/a> — why ternary is special: it attacks the memory ceiling and the compute ceiling at once, while INT4/FP8 only touch the memory one.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/test-time-quantization-without-calibration-dataset/">Test-time quantization on the fly&lt;/a> — quantising dynamically at inference time against the static, calibrated quantisation that AQLM/QuIP#/QTIP describe.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/device-native-architectures-fine-grained-moe-pre-attention-router/">Device-native architectures + fine-grained MoE&lt;/a> — on-device Q4 as the starting point of which sub-4-bit and ternary are the next frontier for edge.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/qlora-multi-lora-limit-small-models/">QLoRA and aggressive multi-LoRA on SLMs&lt;/a> — fine-tuning on an already quantised base; the limit of how far you can compress the base before the adapter can no longer recover the quality.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Ma, S. et al. &lt;em>The Era of 1-bit LLMs: All Large Language Models are in 1.58 Bits&lt;/em> (BitNet b1.58). &lt;a href="https://arxiv.org/abs/2402.17764">https://arxiv.org/abs/2402.17764&lt;/a>&lt;/li>
&lt;li>Egiazarian, V., Panferov, A., Kuznedelev, D. et al. &lt;em>Extreme Compression of Large Language Models via Additive Quantization&lt;/em> (AQLM). &lt;a href="https://arxiv.org/abs/2401.06118">https://arxiv.org/abs/2401.06118&lt;/a>&lt;/li>
&lt;li>Tseng, A., Chee, J., Sun, Q., Kuleshov, V., De Sa, C. &lt;em>QuIP#: Even Better LLM Quantization with Hadamard Incoherence and Lattice Codebooks&lt;/em>. &lt;a href="https://arxiv.org/abs/2402.04396">https://arxiv.org/abs/2402.04396&lt;/a>&lt;/li>
&lt;li>Tseng, A., Sun, Q., Hou, D., De Sa, C. &lt;em>QTIP: Quantization with Trellises and Incoherence Processing&lt;/em>. &lt;a href="https://arxiv.org/abs/2406.11235">https://arxiv.org/abs/2406.11235&lt;/a>&lt;/li>
&lt;li>Frantar, E., Ashkboos, S., Hoefler, T., Alistarh, D. &lt;em>GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers&lt;/em>. &lt;a href="https://arxiv.org/abs/2210.17323">https://arxiv.org/abs/2210.17323&lt;/a>&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>. &lt;a href="https://arxiv.org/abs/2306.00978">https://arxiv.org/abs/2306.00978&lt;/a>&lt;/li>
&lt;li>Google DeepMind. &lt;em>Gemma 3 QAT (Quantization-Aware Training) models&lt;/em> — official blog: &lt;a href="https://developers.googleblog.com/en/gemma-3-quantized-aware-trained-state-of-the-art-ai-to-consumer-gpus/">https://developers.googleblog.com/en/gemma-3-quantized-aware-trained-state-of-the-art-ai-to-consumer-gpus/&lt;/a>&lt;/li>
&lt;li>Microsoft. &lt;em>bitnet.cpp&lt;/em> — 1-bit ternary inference kernels: &lt;a href="https://github.com/microsoft/BitNet">https://github.com/microsoft/BitNet&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Test-time quantization: quantising on the fly with no calibration dataset</title><link>https://blog.lo0.es/en/posts/test-time-quantization-without-calibration-dataset/</link><pubDate>Tue, 09 Jun 2026 02:00:00 +0000</pubDate><guid>https://blog.lo0.es/en/posts/test-time-quantization-without-calibration-dataset/</guid><description>&lt;blockquote>
&lt;p>This post is the natural continuation of &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">Quantization for LLM inference&lt;/a>, which is worth reading first: that is where GPTQ, AWQ, scale + zero-point and why activation outliers are the central problem all live. Here we are not discussing &lt;em>how many bits&lt;/em> to use, but &lt;strong>when and with what information the scales are computed&lt;/strong>: offline against a corpus (PTQ) or on the fly against real traffic (TTQ).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Activation-aware quantisation (AWQ, SmoothQuant) decides which channels to protect by measuring the magnitude of the activations over a &lt;strong>calibration dataset&lt;/strong> in an &lt;strong>offline pass&lt;/strong>, before deployment. The implicit assumption is that this corpus represents future traffic. But activation outliers, the channels with 10-100× the median magnitude that dominate the quantisation error, depend on the input: they change with the domain, the language and the client&amp;rsquo;s distribution. When real traffic drifts away from the calibration, the fixed scales stop being optimal and quality drops. &lt;strong>Test-time quantization (TTQ)&lt;/strong> removes the corpus and the offline pass: it derives the activation-aware scales &lt;strong>at inference time&lt;/strong>, from the activations that are actually observed, per token or per batch. The trade-off is honest and not minor: it introduces &lt;strong>runtime overhead&lt;/strong>, computing statistics, detecting outliers, recomputing scales at every step, which competes directly with the saving from quantising. On small models that overhead weighs proportionally more, because the forward is short and the fixed per-step costs dominate (the framing is in &lt;a href="https://blog.lo0.es/en/posts/roofline-flips-optimising-small-models/">the inverted roofline for SLMs&lt;/a>). TTQ is &lt;strong>orthogonal&lt;/strong> to the format: it is not a competitor to INT4 or FP8, it is a different way of deriving &lt;em>s&lt;/em>. It pays off when there is no calibration pipeline, when the traffic distribution is shifting or unknown, and in multitenant settings where no representative corpus exists.&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;defs>&lt;marker id="ttqm" 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" fill="currentColor" font-size="12" font-weight="600">You are here: DEPLOY · deriving quantization scales on the fly&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" rx="6" fill="#f4f4f4" stroke="#444" stroke-width="1.4"/>&lt;text x="85" y="58" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" rx="6" fill="#f4f4f4" stroke="#444" stroke-width="1.4"/>&lt;text x="210" y="58" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" rx="6" fill="#f4f4f4" stroke="#444" stroke-width="1.4"/>&lt;text x="335" y="58" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" rx="6" fill="#7ad88f" stroke="#444" stroke-width="3"/>&lt;text x="460" y="58" text-anchor="middle" fill="#111" font-size="12" font-weight="600">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" rx="6" fill="#f4f4f4" stroke="#444" stroke-width="1.4"/>&lt;text x="585" y="58" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" rx="6" fill="#f4f4f4" stroke="#444" stroke-width="1.4"/>&lt;text x="710" y="58" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">6 · Retrain&lt;/text>
&lt;path d="M140,52 L155,52" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttqm)"/>
&lt;path d="M265,52 L280,52" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttqm)"/>
&lt;path d="M390,52 L405,52" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttqm)"/>
&lt;path d="M515,52 L530,52" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttqm)"/>
&lt;path d="M640,52 L655,52" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttqm)"/>
&lt;path d="M710,72 L710,82 L85,82 L85,72" stroke="#888" stroke-width="1.2" fill="none" stroke-dasharray="4 2" marker-end="url(#ttqm)"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-analogy-the-tailor-who-takes-measurements-versus-off-the-peg-sizes">The analogy: the tailor who takes measurements versus off-the-peg sizes&lt;/h2>
&lt;p>A clothes shop has two ways of dressing a customer.&lt;/p>
&lt;p>The first is to &lt;strong>sell off-the-peg sizes&lt;/strong>. At some point the factory measured an &amp;ldquo;average customer&amp;rdquo;, a mean mannequin built from a population sample, and cut the garments to those measurements. When a customer walks in, you hand them the size that comes closest. It is extremely fast: the garment is already sewn, it just gets handed over. The problem appears when the customer does not look like the average mannequin: if their shoulders are much broader than average, their particular outlier, the standard size pulls or leaves spare fabric, because it was cut to protect &lt;em>other&lt;/em> areas. This is &lt;strong>offline calibrated PTQ&lt;/strong>: AWQ measured the importance of each channel over a corpus and fixed the scales once and for all; fast at inference time, but blind to the specific customer.&lt;/p>
&lt;p>The second is &lt;strong>the tailor who takes measurements on the spot&lt;/strong>. When the customer walks in, the tailor gets out the tape, measures &lt;em>that customer&lt;/em>, spots where their particular bulk is and adjusts the cut to their real anatomy. The result fits better, especially for customers who fall outside the mould. But every customer costs time: measuring, marking, deciding. This is &lt;strong>TTQ&lt;/strong>: the scales are derived on the fly from the activations that the input actually generates.&lt;/p>
&lt;p>The analogy holds up in three details:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The average mannequin = the calibration dataset.&lt;/strong> If the population walking into the shop resembles the mannequin, the sizes work; if not, they fail at the extremes.&lt;/li>
&lt;li>&lt;strong>Measuring every customer = computing activation statistics per token/batch.&lt;/strong> A better fit, but a fixed cost paid on &lt;em>every&lt;/em> garment.&lt;/li>
&lt;li>&lt;strong>The broad shoulders = the outlier activation channels.&lt;/strong> They are precisely the areas where the fit matters and where the generic size gets it most wrong.&lt;/li>
&lt;/ul>
&lt;p>The tailor wins when the customers are varied or unknown. The tailor loses when you have a homogeneous population and a mannequin that represents it well: there, paying for measurement on every customer is a waste of time.&lt;/p>
&lt;h2 id="the-problem-ttq-solves-fixed-calibration-ages-with-the-traffic">The problem TTQ solves: fixed calibration ages with the traffic&lt;/h2>
&lt;p>Let us recall from the &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">quantization post&lt;/a> what AWQ and SmoothQuant do exactly. They do not quantise all channels equally: they identify the ~1 % of channels whose activations have a large magnitude, the &lt;em>salient channels&lt;/em>, and protect them by scaling them before quantising. To measure that importance they need to see activations, and they see them over a &lt;strong>calibration dataset&lt;/strong> (128-512 samples, typically WikiText or a slice of the domain) in an &lt;strong>offline pass&lt;/strong> prior to deployment.&lt;/p>
&lt;p>The assumption is strong: that the activation distribution of the calibration corpus &lt;strong>represents that of production traffic&lt;/strong>. Two reasons why that assumption breaks:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Activation outliers depend on the input.&lt;/strong> They are not a fixed property of the model the way weights are. The channel that is an outlier when processing C++ code may not be one when processing conversational Arabic or log JSON. The magnitude and position of the peaks change with the domain, the language and the input format.&lt;/li>
&lt;li>&lt;strong>Real traffic is rarely the corpus.&lt;/strong> You calibrate with English WikiText and the client sends you support tickets in Spanish with tables pasted in. The calibration protected the channels that &lt;em>WikiText&lt;/em> activated, not the ones that real traffic activates. The scales are suboptimal exactly where the client lives.&lt;/li>
&lt;/ol>
&lt;p>The result is distribution-dependent degradation: the quantised model keeps its quality while the input resembles the calibration and loses it as the input drifts away. The most awkward case is &lt;strong>multitenant&lt;/strong>: if you serve clients with different domains from the same quantised model, no single representative corpus exists; any fixed calibration favours some tenants and penalises others.&lt;/p>
&lt;h2 id="the-ttq-mechanism-measure-the-real-activations-and-scale-on-the-fly">The TTQ mechanism: measure the real activations and scale on the fly&lt;/h2>
&lt;p>TTQ (arXiv:2603.19296, March 2026) proposes deriving &lt;strong>activation-aware quantisation at inference time&lt;/strong>, with no offline pass and no calibration dataset. The idea, in its bare conceptual form:&lt;/p>
&lt;p>&lt;strong>Step 1 — Observe.&lt;/strong> When the activation tensor &lt;code>X&lt;/code> arrives at a linear layer (per token or per batch), cheap statistics are computed over the channels: a measure of central tendency (median or mean magnitude) and one of dispersion per channel. This is the equivalent of AWQ looking at its corpus, but done over the activations that &lt;em>really&lt;/em> are coming in right now.&lt;/p>
&lt;p>&lt;strong>Step 2 — Detect outliers on the fly.&lt;/strong> With those statistics, the channels whose magnitude shoots up relative to the tensor median are identified; the typical criterion is a threshold of the form &amp;ldquo;magnitude &amp;gt; k × median&amp;rdquo;. These are the channels that, if quantised with the same scale as the rest, blow up the error.&lt;/p>
&lt;p>&lt;strong>Step 3 — Derive scales and segregate.&lt;/strong> For normal channels a scale is computed that exploits the range; for the outliers a different treatment is applied, a scale of their own, or keeping them at higher precision, in the style of &lt;em>on-the-fly mixed precision&lt;/em>. It is the same philosophy as LLM.int8() (segregating outliers to FP16) or AWQ (scaling salient channels), but with the threshold and the scales &lt;strong>recomputed over the current input&lt;/strong>, not frozen since calibration.&lt;/p>
&lt;p>&lt;strong>Step 4 — Quantise and multiply.&lt;/strong> With the fresh scales, the tensor is quantised and the GEMM runs. The activations entering the next layer compensate for the rescaling, just as in AWQ, so that the maths cancels out.&lt;/p>
&lt;p>The key difference with AWQ is not in &lt;em>what&lt;/em> is done (protecting activation outliers) but in &lt;em>when&lt;/em> and &lt;em>against what&lt;/em>: AWQ decides once, offline, against a corpus; TTQ decides at every step, on the fly, against real traffic. It is the transfer to inference of the &amp;ldquo;test-time&amp;rdquo; idea: adapting the computation to the specific sample in front of you instead of to a precomputed average.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Offline calibrated PTQ versus on-the-fly TTQ">
&lt;defs>&lt;marker id="ttq2" 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="195" y="22" text-anchor="middle" fill="currentColor" font-size="13" font-weight="700">Offline-calibrated PTQ (AWQ / GPTQ)&lt;/text>
&lt;text x="585" y="22" text-anchor="middle" fill="currentColor" font-size="13" font-weight="700">On-the-fly TTQ&lt;/text>
&lt;line x1="390" y1="35" x2="390" y2="285" stroke="#bbb" stroke-width="1" stroke-dasharray="4 3"/>
&lt;rect x="30" y="45" width="150" height="38" rx="6" fill="#ffe6d6" stroke="#a05a2c" stroke-width="1.4"/>&lt;text x="105" y="68" text-anchor="middle" fill="#111" font-size="11" font-weight="600">calibration dataset&lt;/text>
&lt;rect x="210" y="45" width="150" height="38" rx="6" fill="#ffe6d6" stroke="#a05a2c" stroke-width="1.4"/>&lt;text x="285" y="64" text-anchor="middle" fill="#111" font-size="11" font-weight="600">OFFLINE pass&lt;/text>&lt;text x="285" y="78" text-anchor="middle" fill="#444" font-size="10">fixes scales s, outliers&lt;/text>
&lt;path d="M180,64 L210,64" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;rect x="120" y="105" width="150" height="34" rx="6" fill="#f0f0f0" stroke="#444" stroke-width="1.4"/>&lt;text x="195" y="126" text-anchor="middle" fill="#111" font-size="11" font-weight="600">FROZEN scales&lt;/text>
&lt;path d="M285,83 L285,98 L195,98 L195,105" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;rect x="30" y="165" width="150" height="34" rx="6" fill="#d6eaff" stroke="#1f5fa8" stroke-width="1.4"/>&lt;text x="105" y="186" text-anchor="middle" fill="#111" font-size="11" font-weight="600">similar input → OK&lt;/text>
&lt;rect x="210" y="165" width="150" height="34" rx="6" fill="#f6caca" stroke="#a52a2a" stroke-width="1.4"/>&lt;text x="285" y="182" text-anchor="middle" fill="#111" font-size="11" font-weight="600">distant input →&lt;/text>&lt;text x="285" y="195" text-anchor="middle" fill="#a52a2a" font-size="10" font-weight="600">degradation&lt;/text>
&lt;path d="M150,139 L105,165" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;path d="M240,139 L285,165" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;text x="195" y="225" text-anchor="middle" fill="#444" font-size="10">inference overhead ≈ 0 · quality depends on calibration&lt;/text>
&lt;rect x="510" y="45" width="150" height="38" rx="6" fill="#d9f5d6" stroke="#2a7a40" stroke-width="1.4"/>&lt;text x="585" y="62" text-anchor="middle" fill="#111" font-size="11" font-weight="600">REAL activations&lt;/text>&lt;text x="585" y="77" text-anchor="middle" fill="#444" font-size="10">from current traffic&lt;/text>
&lt;rect x="510" y="100" width="150" height="34" rx="6" fill="#d9f5d6" stroke="#2a7a40" stroke-width="1.4"/>&lt;text x="585" y="115" text-anchor="middle" fill="#111" font-size="11" font-weight="600">measure + detect&lt;/text>&lt;text x="585" y="128" text-anchor="middle" fill="#444" font-size="10">outliers ON THE FLY&lt;/text>
&lt;path d="M585,83 L585,100" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;rect x="510" y="151" width="150" height="34" rx="6" fill="#d9f5d6" stroke="#2a7a40" stroke-width="1.4"/>&lt;text x="585" y="166" text-anchor="middle" fill="#111" font-size="11" font-weight="600">FRESH scales&lt;/text>&lt;text x="585" y="179" text-anchor="middle" fill="#444" font-size="10">per token / batch&lt;/text>
&lt;path d="M585,134 L585,151" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;rect x="510" y="202" width="150" height="34" rx="6" fill="#fff5b0" stroke="#9a8400" stroke-width="1.4"/>&lt;text x="585" y="217" text-anchor="middle" fill="#111" font-size="11" font-weight="600">quantize + GEMM&lt;/text>&lt;text x="585" y="230" text-anchor="middle" fill="#9a6b00" font-size="10" font-weight="600">+ per-step overhead&lt;/text>
&lt;path d="M585,185 L585,202" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;text x="585" y="258" text-anchor="middle" fill="#444" font-size="10">no corpus · quality robust to distribution · overhead ≠ 0&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-maths-that-matter">The maths that matter&lt;/h2>
&lt;h3 id="the-error-of-quantising-an-outlier-with-the-wrong-scale">The error of quantising an outlier with the wrong scale&lt;/h3>
&lt;p>Let us recall affine uniform quantisation from the &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">base post&lt;/a>: an integer code &lt;code>q = round(x/s) - z&lt;/code> with scale &lt;code>s&lt;/code> and zero-point &lt;code>z&lt;/code>, and reconstruction &lt;code>x̂ = s·(q + z)&lt;/code>. For a &lt;code>b&lt;/code>-bit quantiser with a symmetric range, the scale that covers a tensor of maximum magnitude &lt;code>M&lt;/code> is approximately &lt;code>s = M / (2^{b-1} - 1)&lt;/code>. The rounding error of each element is bounded by half a scale: &lt;code>|x - x̂| ≤ s/2&lt;/code>.&lt;/p>
&lt;p>Here is the outlier problem. The scale &lt;code>s&lt;/code> is chosen to cover the &lt;strong>largest&lt;/strong> value in the group. If one channel has 30× the median magnitude and you share a single scale with the rest of the tensor, that magnitude rules: &lt;code>M&lt;/code> is the outlier, so &lt;code>s&lt;/code> is inflated 30× relative to what the majority would need. The absolute rounding error of the normal values rises proportionally.&lt;/p>
&lt;p>A concrete calculation. Take a group where the median magnitude is 1.0 and an outlier channel is 30.0, quantised to INT4 (&lt;code>b = 4&lt;/code>, levels ±7):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>With a shared scale&lt;/strong>, &lt;code>s = 30 / 7 ≈ 4.29&lt;/code>. The rounding error of a typical value (magnitude ~1) is up to &lt;code>s/2 ≈ 2.14&lt;/code>. In other words, &lt;strong>the error on the normal values is of the order of their own value&lt;/strong>: the outlier has destroyed the resolution of everything else. Relative error of a value of magnitude 1: up to ~214 %.&lt;/li>
&lt;li>&lt;strong>Segregating the outlier&lt;/strong> (pulling it out to FP16 or giving it its own scale) and quantising the rest with &lt;code>M = 1&lt;/code>, &lt;code>s = 1/7 ≈ 0.143&lt;/code>. The error of a typical value drops to &lt;code>s/2 ≈ 0.071&lt;/code>, ~7 % relative. &lt;strong>Thirty times less error&lt;/strong> on the majority of the group&amp;rsquo;s weights.&lt;/li>
&lt;/ul>
&lt;p>That is the entire reason for the existence of activation-aware quantisation: &lt;strong>to detect and treat separately the ~1 % of channels that, if not segregated, hijack the scale&lt;/strong>. AWQ does it against the corpus; TTQ does it against the real input. And if the channel that is an outlier &lt;em>in production&lt;/em> was not an outlier &lt;em>during calibration&lt;/em>, AWQ did not protect it: it quantised real traffic with the inflated scale from the case above. That is where TTQ gains precision.&lt;/p>
&lt;h3 id="the-overhead-the-cost-of-measuring-at-every-step">The overhead: the cost of measuring at every step&lt;/h3>
&lt;p>The price is symmetric. Computing the per-token statistics, per-channel magnitudes, median or percentile, outlier threshold, scales, means reductions over the activation tensor that did not exist in the forward pass with frozen scales. Let us call:&lt;/p>
&lt;ul>
&lt;li>&lt;code>T&lt;/code> = forward time per token with fixed scales (static PTQ), in µs.&lt;/li>
&lt;li>&lt;code>Δ&lt;/code> = extra per-token cost of deriving the statistics and scales on the fly, in µs.&lt;/li>
&lt;/ul>
&lt;p>The relative overhead is simply:&lt;/p>
$$\text{overhead} = \frac{\Delta}{T}$$
&lt;p>The key point is that &lt;code>Δ&lt;/code> is relatively fixed per step (it depends on the number of channels and layers, not on how much &amp;ldquo;useful&amp;rdquo; work the model does), while &lt;code>T&lt;/code> scales with the size of the model. That is why the ratio behaves very differently depending on the model:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Large model&lt;/strong> (say, 70B): &lt;code>T&lt;/code> is large, every forward moves tens of GB of weights from HBM. If &lt;code>Δ ≈ 8 µs&lt;/code> and &lt;code>T ≈ 800 µs&lt;/code>, the overhead is &lt;code>8/800 = 1 %&lt;/code>. Negligible against the saving from quantising.&lt;/li>
&lt;li>&lt;strong>SLM&lt;/strong> (say, 1B): &lt;code>T&lt;/code> is small, the forward per token is short. With the same &lt;code>Δ ≈ 8 µs&lt;/code> and &lt;code>T ≈ 60 µs&lt;/code>, the overhead is &lt;code>8/60 ≈ 13 %&lt;/code>. No longer negligible: it eats a good part of what you gained by quantising.&lt;/li>
&lt;/ul>
&lt;p>This connects directly to the &lt;a href="https://blog.lo0.es/en/posts/roofline-flips-optimising-small-models/">inverted roofline for small models&lt;/a>: on an SLM the &lt;strong>fixed per-step costs&lt;/strong> (kernel launches, synchronisations, overheads that do not scale with the model) weigh proportionally more, because there is less useful work to spread them over. TTQ&amp;rsquo;s &lt;code>Δ&lt;/code> is exactly one of those fixed costs. Going per-batch instead of per-token amortises &lt;code>Δ&lt;/code> across all the tokens of the batch and lowers the relative overhead, at the cost of coarser scales; it is the first parameter to touch.&lt;/p>
&lt;p>The uncomfortable conclusion: TTQ gives away robustness to distribution but spends part of the acceleration budget on measuring, and in the regime where acceleration is scarcest, the SLMs, the ones most often deployed at the edge, is where that spending hurts most. It is not free; it is a change of currency.&lt;/p>
&lt;blockquote>
&lt;p>A note of methodological scepticism: arXiv:2603.19296 is from &lt;strong>March 2026&lt;/strong>, very recent, and as of the date of this post there are no broad independent reproductions. Whatever speedup and quality figures circulate should be taken with the same caution as any number without published methodology: what hardware, what batch size, what real measured &lt;code>Δ&lt;/code>, against what baseline (well-calibrated or badly calibrated PTQ), in what domain? The &lt;em>conceptual&lt;/em> argument, robustness to distribution in exchange for per-step overhead, is solid; the concrete multipliers are pending validation.&lt;/p>
&lt;/blockquote>
&lt;h2 id="what-ttq-is-not-marking-it-off-from-the-rest-of-the-zoo">What TTQ is NOT: marking it off from the rest of the zoo&lt;/h2>
&lt;p>TTQ is easily confused with neighbouring techniques. The distinction that matters is that &lt;strong>TTQ is the &lt;em>how&lt;/em> of deriving the scales, not the format nor the moment in training&lt;/strong>.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Technique&lt;/th>
&lt;th>When the scales are fixed&lt;/th>
&lt;th>Needs a calibration corpus&lt;/th>
&lt;th>Touches training&lt;/th>
&lt;th>Is it a format&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Static PTQ&lt;/strong> (GPTQ, AWQ)&lt;/td>
&lt;td>Offline, before deployment&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>No&lt;/td>
&lt;td>No (it uses INT4/INT8)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>QAT&lt;/strong>&lt;/td>
&lt;td>During training&lt;/td>
&lt;td>No (training data)&lt;/td>
&lt;td>Yes (retrains)&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>FP8 end to end&lt;/strong>&lt;/td>
&lt;td>At runtime, but simple per-tensor scales&lt;/td>
&lt;td>Minimal / none&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong> (E4M3/E5M2)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TTQ&lt;/strong>&lt;/td>
&lt;td>At runtime, activation-aware per token/batch&lt;/td>
&lt;td>&lt;strong>No&lt;/strong>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>No (orthogonal to the format)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The four distinctions, one by one:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Against static PTQ (GPTQ/AWQ).&lt;/strong> Same goal (protecting outliers), the same possible format (INT4), but PTQ freezes the decisions offline against a corpus and TTQ recomputes them on the fly. TTQ is, in a sense, &amp;ldquo;AWQ without the calibration phase, paid for at runtime&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Against QAT.&lt;/strong> QAT puts quantisation inside the training loop so that the model learns to be robust to it; it costs a retrain. TTQ does not touch training: it operates on an already trained model, at inference. They are attacks at opposite moments of the pipeline.&lt;/li>
&lt;li>&lt;strong>Against FP8 end to end.&lt;/strong> FP8 is a &lt;strong>format&lt;/strong> with its own logarithmic range; its &amp;ldquo;dynamic scaling&amp;rdquo; computes a simple per-tensor scalar at runtime, but it does not do activation-aware per-channel outlier detection. TTQ could, conceptually, derive scales on the fly &lt;em>for&lt;/em> an FP8 or INT4 quantiser: it is orthogonal to the format.&lt;/li>
&lt;li>&lt;strong>TTQ is orthogonal to the format.&lt;/strong> It decides &lt;em>how&lt;/em> to obtain &lt;code>s&lt;/code>, not how many bits you store &lt;code>q&lt;/code> in. You can imagine &amp;ldquo;TTQ over INT4&amp;rdquo; or &amp;ldquo;TTQ over FP8&amp;rdquo;. What defines TTQ is the source of the scale, real activations on the fly, not the width of the code.&lt;/li>
&lt;/ul>
&lt;h2 id="when-it-pays-off-and-when-it-does-not">When it pays off (and when it does not)&lt;/h2>
&lt;p>TTQ is not a universal replacement for AWQ. It is a tool for a specific deployment profile. &lt;strong>It pays off when:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>You have no calibration pipeline.&lt;/strong> You want to deploy a quantised model &lt;em>now&lt;/em>, without assembling the calibration dataset, running the offline pass or validating that the corpus represents the traffic. TTQ cuts out that whole phase: you load the model and serve.&lt;/li>
&lt;li>&lt;strong>The traffic distribution is shifting or unknown.&lt;/strong> An assistant that one day receives code and the next legal contracts in another language. No fixed calibration covers both well; on-the-fly adaptation follows the distribution without recalibrating.&lt;/li>
&lt;li>&lt;strong>Multitenant with no representative corpus.&lt;/strong> You serve the same model to clients with disparate domains. No single corpus represents all of them; any fixed calibration creates winners and losers among tenants. TTQ adjusts to each input, whichever tenant it comes from.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>It does not pay off when:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>You have a stable domain and a good calibration corpus.&lt;/strong> If your traffic is homogeneous and representative, offline AWQ gives you the same quality with zero runtime overhead. Paying &lt;code>Δ&lt;/code> on every token to relearn what a corpus already captured is waste.&lt;/li>
&lt;li>&lt;strong>You serve SLMs with a tight latency SLA.&lt;/strong> This is exactly the case where &lt;code>Δ/T&lt;/code> is high. If the model is small and TPOT matters, the overhead of measuring can wipe out the gain from quantising. Measure your real &lt;code>Δ&lt;/code> before assuming it adds up.&lt;/li>
&lt;li>&lt;strong>The batch is large and compute-bound.&lt;/strong> With high concurrency the forward is no longer memory-bound and the cost of the extra reductions competes worse; at the very least, amortise &lt;code>Δ&lt;/code> per batch.&lt;/li>
&lt;/ul>
&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">On an RTX 4090 (24 GB, Ada Lovelace)&lt;/h3>
&lt;p>The natural case for the 4090 is the SLM, Qwen 3 1.5B, Llama 3 8B AWQ-INT4, serving at low concurrency. That is precisely the regime where TTQ is riskiest: &lt;code>T&lt;/code> per token is small and the 4090 has no accelerated native FP8 (we discussed this in the &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">quantization post&lt;/a>), so TTQ&amp;rsquo;s extra reductions run on CUDA cores competing for the same time. Here the question is not &amp;ldquo;does it improve quality?&amp;rdquo; but &amp;ldquo;does the overhead leave me an acceptable TPOT?&amp;rdquo;. If the traffic is homogeneous, offline AWQ wins on simplicity and latency. TTQ only justifies its &lt;code>Δ&lt;/code> if the input distribution is genuinely unpredictable and the degradation from fixed calibration is measurable.&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 the calculation partly flips. With large models &lt;code>T&lt;/code> is high and &lt;code>Δ/T&lt;/code> drops to the low single-digit percentage range, so TTQ&amp;rsquo;s overhead is more digestible. The strong use case is &lt;strong>multitenant&lt;/strong>: a cluster serving a large model to clients with heterogeneous domains, where no calibration corpus satisfies everyone. There, TTQ&amp;rsquo;s robustness to distribution has real value and the overhead is diluted in a big forward. Even so, on an H100 with native FP8, the baseline to beat is demanding: static FP8 barely loses quality (see the table in the quantization post) and costs nothing at runtime. TTQ has to show that its robustness gain on the outlier tenants outweighs what it gives away in overhead. With a paper from March 2026 and no reproductions, that demonstration is pending.&lt;/p>
&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>The memory cost of the on-the-fly statistics&lt;/strong>: per-channel buffers, their impact on the footprint and on cache pressure.&lt;/li>
&lt;li>&lt;strong>Interaction with continuous batching&lt;/strong>: how scales are derived when a batch mixes requests from different domains in the same step.&lt;/li>
&lt;li>&lt;strong>TTQ + speculative decoding&lt;/strong>: whether the draft and the target derive scales on the fly separately, and how that affects the acceptance rate.&lt;/li>
&lt;li>&lt;strong>Numerical stability&lt;/strong>: what happens when a batch contains a one-off extreme outlier that inflates the scale for every token of that step.&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/quantization-llm-inference-fp8-int4-gguf/">Quantization for LLM inference&lt;/a> — the indispensable base: scale + zero-point, GPTQ, AWQ and why activation outliers are the problem; TTQ is AWQ with the scales derived on the fly instead of offline.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/roofline-flips-optimising-small-models/">Inverted roofline for small models&lt;/a> — why fixed per-step costs weigh more on an SLM; it explains directly why TTQ&amp;rsquo;s &lt;code>Δ&lt;/code> overhead hurts more on small models.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/aggressive-quantisation-4-bit-to-ternary/">Aggressive sub-4-bit and ternary quantisation&lt;/a> — the static frontier below 4 bits; it complements TTQ, which attacks the &lt;em>how&lt;/em> of the scale instead of the &lt;em>how many bits&lt;/em>.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/qlora-multi-lora-limit-small-models/">QLoRA and aggressive multi-LoRA on SLMs&lt;/a> — adapters on a quantised base; the base could derive scales on the fly while the adapters stay in BF16.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/fp8-end-to-end-enable-measure-quality-decide/">FP8 end to end: weights, KV and quality&lt;/a> — the Hopper/Blackwell datacenter format; TTQ is orthogonal and could derive scales for an FP8 quantiser.&lt;/li>
&lt;li>&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> — the KV cache is quantised too; its scales are another candidate to be derived on the fly by the same logic.&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 other route to serving robust small models; distilling shrinks the model, TTQ tunes its quantisation to the traffic.&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> — where the runtime quantisation levers for squeezing a 4090 turn into concrete parameters.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;em>TTQ: Activation-Aware Test-Time Quantization to Accelerate LLM Inference On The Fly&lt;/em> (March 2026). &lt;a href="https://arxiv.org/abs/2603.19296">https://arxiv.org/abs/2603.19296&lt;/a>&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;a href="https://arxiv.org/abs/2306.00978">https://arxiv.org/abs/2306.00978&lt;/a>&lt;/li>
&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;a href="https://arxiv.org/abs/2210.17323">https://arxiv.org/abs/2210.17323&lt;/a>&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;a href="https://arxiv.org/abs/2211.10438">https://arxiv.org/abs/2211.10438&lt;/a>&lt;/li>
&lt;li>Dettmers, T., Lewis, M., Belkada, Y., Zettlemoyer, L. &lt;em>LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale&lt;/em> (NeurIPS 2022). &lt;a href="https://arxiv.org/abs/2208.07339">https://arxiv.org/abs/2208.07339&lt;/a>&lt;/li>
&lt;/ul></description></item><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>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>