<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: AI Tech News</title>
    <description>The latest articles on DEV Community by AI Tech News (@wolfsea2357).</description>
    <link>https://dev.to/wolfsea2357</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1212033%2Fb9c824aa-4bd1-4cee-92e3-b5bbcf0404d1.jpg</url>
      <title>DEV Community: AI Tech News</title>
      <link>https://dev.to/wolfsea2357</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wolfsea2357"/>
    <language>en</language>
    <item>
      <title>Prefill/Decode Disaggregation in vLLM: The KV Transfer Math That Decides If It Pays Off</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Mon, 19 Oct 2026 14:03:51 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/prefilldecode-disaggregation-in-vllm-the-kv-transfer-math-that-decides-if-it-pays-off-13j8</link>
      <guid>https://dev.to/wolfsea2357/prefilldecode-disaggregation-in-vllm-the-kv-transfer-math-that-decides-if-it-pays-off-13j8</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prefill/decode (P/D) disaggregation puts the two phases of LLM inference on separate engine instances. vLLM's own docs are blunt about the trade: it lets you tune time to first token (TTFT) and inter-token latency (ITL) independently and controls tail ITL, but "disaggregated prefill does not improve throughput."&lt;/li&gt;
&lt;li&gt;The price you pay is moving the KV cache across a network for every request. That cost is a ratio you can compute before you buy a single NIC.&lt;/li&gt;
&lt;li&gt;For a 70B-class model with 8 KV heads, the KV cache is 320 KiB per token in BF16. Against an illustrative prefill rate, shipping it over a 400 Gb link costs about 7% of prefill time, over 100 GbE about 30%. FP8 KV halves both.&lt;/li&gt;
&lt;li&gt;Because both transfer and prefill scale linearly with prompt length, the ratio does not change with prompt length. What changes is how badly colocated prefill stalls your decode batch, and that grows with every long prompt.&lt;/li&gt;
&lt;li&gt;Decision rule at the end, plus a script to measure the thing that actually matters: p99 ITL under mixed load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All throughput and latency figures below come from arithmetic on public model configs and stated assumptions. They are marked &lt;strong&gt;illustrative&lt;/strong&gt;. Measure on your own hardware before you act.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem does disaggregated prefill actually solve?
&lt;/h2&gt;

&lt;p&gt;LLM inference has two phases with opposite hardware personalities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefill&lt;/strong&gt; processes the whole prompt in one forward pass. It is compute bound: large matrix multiplies, high arithmetic intensity, GPU tensor cores busy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decode&lt;/strong&gt; generates one token per step per sequence. It is memory bandwidth bound: every step reads the full weights and the growing KV cache to produce a single token per request.&lt;/p&gt;

&lt;p&gt;When both run on the same engine, they interfere. A long prompt arriving mid-flight injects a compute-heavy step into a batch of sequences that are all waiting for their next token. Every one of those sequences sees an ITL spike equal to roughly the duration of the prefill step. Users experience this as a stream that stutters whenever someone else submits a big document.&lt;/p&gt;

&lt;p&gt;Chunked prefill is the usual first mitigation: split the prompt into chunks and interleave them with decode steps, so each stall is bounded by the chunk size. P/D disaggregation is the stronger one: prefill runs on its own instances, decode instances never execute a prefill step, and the decode step cadence becomes steady. The vLLM documentation states that this reduces tail ITL more reliably than chunked prefilling.&lt;/p&gt;

&lt;p&gt;Two other benefits follow from separation. You can give each phase a different parallelism strategy (for example, more tensor parallelism on the prefill side to cut TTFT), and you can scale the two pools independently when your traffic mix shifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it cost? The KV transfer arithmetic
&lt;/h2&gt;

&lt;p&gt;Once prefill happens elsewhere, the resulting KV cache has to reach the decode instance before the first token can be produced. The bytes to move per token are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kv_bytes_per_token = 2 * layers * kv_heads * head_dim * dtype_bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The factor 2 is for keys and values. I pulled the shape values from the public &lt;code&gt;config.json&lt;/code&gt; files of two open models:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Layers&lt;/th&gt;
&lt;th&gt;KV heads&lt;/th&gt;
&lt;th&gt;Head dim&lt;/th&gt;
&lt;th&gt;BF16 per token&lt;/th&gt;
&lt;th&gt;FP8 per token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Llama-3.1-8B&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;128 KiB&lt;/td&gt;
&lt;td&gt;64 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama-3.1-70B&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;320 KiB&lt;/td&gt;
&lt;td&gt;160 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Llama-3.1-8B%2522%252C%2522Llama-3.1-70B%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522BF16%2520%2528KiB%2Ftoken%2529%2522%252C%2522data%2522%253A%255B128%252C320%255D%252C%2522backgroundColor%2522%253A%2522%25233b6fd4%2522%257D%252C%257B%2522label%2522%253A%2522FP8%2520%2528KiB%2Ftoken%2529%2522%252C%2522data%2522%253A%255B64%252C160%255D%252C%2522backgroundColor%2522%253A%2522%25232fa37a%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520cache%2520bytes%2520per%2520token%2520%2528from%2520public%2520config.json%2529%2522%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522KiB%2520per%2520token%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522top%2522%257D%257D%257D%257D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Llama-3.1-8B%2522%252C%2522Llama-3.1-70B%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522BF16%2520%2528KiB%2Ftoken%2529%2522%252C%2522data%2522%253A%255B128%252C320%255D%252C%2522backgroundColor%2522%253A%2522%25233b6fd4%2522%257D%252C%257B%2522label%2522%253A%2522FP8%2520%2528KiB%2Ftoken%2529%2522%252C%2522data%2522%253A%255B64%252C160%255D%252C%2522backgroundColor%2522%253A%2522%25232fa37a%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520cache%2520bytes%2520per%2520token%2520%2528from%2520public%2520config.json%2529%2522%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522KiB%2520per%2520token%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522top%2522%257D%257D%257D%257D" alt="KV cache bytes per token for two open models in BF16 and FP8" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Grouped-query attention is why these numbers are tolerable. A 70B model with full multi-head attention (64 KV heads) would carry 8 times more.&lt;/p&gt;

&lt;p&gt;An 8,192-token prompt on the 70B model therefore produces 2.68 GB of KV cache in BF16 (8,192 x 327,680 bytes). That is the payload for one request.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long does the transfer take compared with prefill?
&lt;/h2&gt;

&lt;p&gt;Here is the calculator I used. It is deliberately small so you can swap in your own model shape, link speed, and measured prefill throughput.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kv_bytes_per_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv_heads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head_dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype_bytes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;kv_heads&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;head_dim&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dtype_bytes&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prefill_seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params_b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eff_tflops&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# dense-model approximation: 2 * params FLOPs per token, attention term ignored
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;params_b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpus&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;eff_tflops&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1e12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;LINKS_GBPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100GbE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;12.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200Gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;25.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;400Gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;50.0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# GB/s at line rate
&lt;/span&gt;
&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;
&lt;span class="n"&gt;pf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prefill_seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eff_tflops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# assumption, replace with a measurement
&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;kv_bytes_per_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;# BF16 KV
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gbs&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;LINKS_GBPS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gbs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pf&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;% of prefill (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pf&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assumptions are the weak point, so I will state them plainly. &lt;strong&gt;Illustrative&lt;/strong&gt; inputs: a 70B dense model, four GPUs sharing the prefill, 400 TFLOPS of effective throughput per GPU, link speeds at line rate with no protocol overhead. Real prefill throughput depends on your kernels, tensor parallel efficiency, and sequence length. Replace &lt;code&gt;eff_tflops&lt;/code&gt; with your own measurement.&lt;/p&gt;

&lt;p&gt;With those inputs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt tokens&lt;/th&gt;
&lt;th&gt;KV (BF16)&lt;/th&gt;
&lt;th&gt;Prefill&lt;/th&gt;
&lt;th&gt;100GbE&lt;/th&gt;
&lt;th&gt;200Gb&lt;/th&gt;
&lt;th&gt;400Gb&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2,048&lt;/td&gt;
&lt;td&gt;0.67 GB&lt;/td&gt;
&lt;td&gt;179 ms&lt;/td&gt;
&lt;td&gt;54 ms (30%)&lt;/td&gt;
&lt;td&gt;27 ms (15%)&lt;/td&gt;
&lt;td&gt;13 ms (7%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;2.68 GB&lt;/td&gt;
&lt;td&gt;717 ms&lt;/td&gt;
&lt;td&gt;215 ms (30%)&lt;/td&gt;
&lt;td&gt;107 ms (15%)&lt;/td&gt;
&lt;td&gt;54 ms (7%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;10.74 GB&lt;/td&gt;
&lt;td&gt;2,867 ms&lt;/td&gt;
&lt;td&gt;859 ms (30%)&lt;/td&gt;
&lt;td&gt;429 ms (15%)&lt;/td&gt;
&lt;td&gt;215 ms (7%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522100GbE%2522%252C%2522200Gb%2522%252C%2522400Gb%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522BF16%2520KV%2522%252C%2522data%2522%253A%255B30%252C15%252C7%255D%252C%2522backgroundColor%2522%253A%2522%25233b6fd4%2522%257D%252C%257B%2522label%2522%253A%2522FP8%2520KV%2522%252C%2522data%2522%253A%255B15%252C7%252C4%255D%252C%2522backgroundColor%2522%253A%2522%25232fa37a%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520transfer%2520time%2520as%2520%2525%2520of%2520prefill%2520time%2520%2528Llama-3.1-70B%252C%25204%2520GPUs%252C%2520illustrative%2529%2522%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522%2525%2520of%2520prefill%2520time%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522top%2522%257D%257D%257D%257D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522100GbE%2522%252C%2522200Gb%2522%252C%2522400Gb%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522BF16%2520KV%2522%252C%2522data%2522%253A%255B30%252C15%252C7%255D%252C%2522backgroundColor%2522%253A%2522%25233b6fd4%2522%257D%252C%257B%2522label%2522%253A%2522FP8%2520KV%2522%252C%2522data%2522%253A%255B15%252C7%252C4%255D%252C%2522backgroundColor%2522%253A%2522%25232fa37a%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520transfer%2520time%2520as%2520%2525%2520of%2520prefill%2520time%2520%2528Llama-3.1-70B%252C%25204%2520GPUs%252C%2520illustrative%2529%2522%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522%2525%2520of%2520prefill%2520time%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522top%2522%257D%257D%257D%257D" alt="KV transfer time as a percentage of prefill time by link speed, BF16 and FP8" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The percentage column is identical in every row. That is not a coincidence. Both quantities are linear in prompt length, so the ratio collapses to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transfer / prefill = (kv_bytes_per_token / link_bandwidth) / (2 * params / (gpus * flops))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prompt length cancels. The practical consequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You can decide on hardware alone.&lt;/strong&gt; Model shape, link bandwidth, and prefill throughput set the ratio. Traffic mix does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long prompts are, if anything, better for disaggregation than this table suggests.&lt;/strong&gt; Real prefill includes an attention term that grows faster than linearly, so long prompts take relatively longer to compute than to ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tensor parallelism helps the transfer side too.&lt;/strong&gt; KV heads are sharded across the prefill GPUs, so if each GPU has its own NIC path, the aggregate bandwidth scales with the group. The 50 GB/s figure above is a single 400 Gb link. Per-GPU links change the picture a lot, and it is worth checking your topology rather than assuming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FP8 KV cache pays twice.&lt;/strong&gt; It halves the memory for concurrent sequences and halves the bytes on the wire. If you already quantize the KV cache, the transfer ratio gets cheaper for free. I covered how to measure that quantization fairly in an earlier post (linked below).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A caveat on overlap: transfer is not necessarily serialized after prefill. Implementations can start moving layers or blocks while later ones are still being computed, which hides part of the cost. The numbers here are the worst case where everything waits. The upside from overlap is real but depends on the connector and your network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does colocated prefill hurt decode so much?
&lt;/h2&gt;

&lt;p&gt;The other side of the ledger is what you are paying to avoid. A decode step on a loaded engine takes tens of milliseconds. A prefill step of 8,192 tokens, under the same illustrative assumptions, takes 717 ms. During that time no sequence in the decode batch advances.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25228192%2520%2528unchunked%2529%2522%252C%25222048%2520chunk%2522%252C%2522512%2520chunk%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522ms%2522%252C%2522data%2522%253A%255B717%252C179%252C45%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523d4533b%2522%252C%2522%2523e0a030%2522%252C%2522%25232fa37a%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Decode%2520stall%2520caused%2520by%2520one%2520prefill%2520step%2520%252870B%252C%25204%2520GPUs%252C%2520illustrative%2529%2522%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522ms%2520of%2520blocked%2520decode%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522top%2522%257D%257D%257D%257D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25228192%2520%2528unchunked%2529%2522%252C%25222048%2520chunk%2522%252C%2522512%2520chunk%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522ms%2522%252C%2522data%2522%253A%255B717%252C179%252C45%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523d4533b%2522%252C%2522%2523e0a030%2522%252C%2522%25232fa37a%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Decode%2520stall%2520caused%2520by%2520one%2520prefill%2520step%2520%252870B%252C%25204%2520GPUs%252C%2520illustrative%2529%2522%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522ms%2520of%2520blocked%2520decode%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522top%2522%257D%257D%257D%257D" alt="Decode stall caused by one prefill step for unchunked, 2048-token chunk and 512-token chunk cases" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chunking shrinks the stall (179 ms at 2,048 tokens, 45 ms at 512) but never removes it, and small chunks have their own cost: more steps per prompt, lower prefill efficiency, and a longer TTFT for the prompt being chunked. Disaggregation removes the interference entirely on the decode side. What it adds is the transfer step, which in the table above is 4% to 30% of a prefill that no longer blocks anyone.&lt;/p&gt;

&lt;p&gt;So the real comparison is not "transfer cost versus nothing." It is &lt;strong&gt;transfer cost versus the p99 ITL damage of colocated prefill at your actual request mix.&lt;/strong&gt; If your workload is short chat turns, the damage is small and the transfer is pure overhead. If it is long documents, long RAG contexts, or agent traces with big tool outputs, the damage can dominate your tail latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you set it up in vLLM?
&lt;/h2&gt;

&lt;p&gt;The feature is configured with &lt;code&gt;--kv-transfer-config&lt;/code&gt;, a JSON string that selects a connector. The vLLM documentation lists several connectors, including NixlConnector, MooncakeConnector, LMCacheConnectorV1, and OffloadingConnector, and marks the feature as experimental and subject to change. A documented NixlConnector example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve &amp;lt;model&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kv-transfer-config&lt;/span&gt; &lt;span class="s1"&gt;'{"kv_connector":"NixlConnector","kv_role":"kv_both","kv_buffer_device":"cuda","kv_connector_extra_config":{"backends":["UCX","GDS"]}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the docs for your installed version for the exact role values per instance and for the proxy or router that sends the prompt to a prefill instance and then the decode instance. The interface has been moving.&lt;/p&gt;

&lt;p&gt;It is moving in an interesting direction. In a recent post on the vLLM blog (October 14, 2026), the project describes a push-based NIXL connector. In the traditional pull model the decode instance requests KV from the prefill instance. In the push model the prefill instance writes KV directly into decode memory over RDMA. The stated goal is to remove three steps from the TTFT critical path: the router relay, decode-side allocation, and transfer setup. That matters for this article's math because those fixed costs are not in the bandwidth ratio above. At small prompt sizes, per-request setup can dominate the pure byte-transfer time, so the ratio table is a floor on overhead, not a ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you measure whether it helped?
&lt;/h2&gt;

&lt;p&gt;Do not decide from average latency or from throughput. Disaggregation is a tail-latency tool, and the vLLM docs say it does not improve throughput, so a throughput benchmark will mislead you in both directions.&lt;/p&gt;

&lt;p&gt;Measure p50 and p99 ITL while a background stream of long prompts is arriving. This client uses the OpenAI-compatible streaming endpoint and records the gap between consecutive chunks of each response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream_itl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aiter_lines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DONE]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gaps&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;xs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;)))]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with a fixed pool of "victim" short requests while a separate task injects long prompts at a constant rate, once against the colocated deployment and once against the disaggregated one. Report p50 and p99 ITL for the victims, plus TTFT for the long prompts.&lt;/p&gt;

&lt;p&gt;Rules that keep the comparison fair, in the spirit of the benchmark traps I wrote about before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same total GPUs on both sides.&lt;/strong&gt; Splitting 8 GPUs into 4 prefill and 4 decode must be compared with 8 colocated, not with 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same arrival process and same prompt set&lt;/strong&gt;, replayed from a file, not regenerated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm the prefix cache identically, or disable it, before every run.&lt;/strong&gt; A reader pointed this out under my KV cache quantization post, and it applies here with more force. A warm shared cache flatters whichever configuration runs second, and in a P/D setup it also changes how much KV actually needs to be transferred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dump the resolved engine config&lt;/strong&gt; for both arms and diff them. Identical launch flags are not identical conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run long enough to hit steady state&lt;/strong&gt;, and report the offered load, not just the latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When should you use it, and when should you skip it?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strict p99 ITL target, long or bursty prompts&lt;/td&gt;
&lt;td&gt;Strong candidate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different best parallelism for prefill and decode&lt;/td&gt;
&lt;td&gt;Strong candidate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast interconnect between pools (RDMA, 200 Gb or more per path)&lt;/td&gt;
&lt;td&gt;Favorable math&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short prompts, low concurrency&lt;/td&gt;
&lt;td&gt;Skip, chunked prefill is enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline batch jobs where only total throughput matters&lt;/td&gt;
&lt;td&gt;Skip, no throughput gain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100GbE or slower between pools and no FP8 KV&lt;/td&gt;
&lt;td&gt;Check the ratio first, 30% overhead is hard to hide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small cluster where splitting pools leaves each too small to batch well&lt;/td&gt;
&lt;td&gt;Skip, utilization loss can exceed the gain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row deserves emphasis. A decode pool needs enough concurrent sequences to fill its batches, and a prefill pool needs enough arrivals to stay busy. On a small cluster, splitting can leave both under-utilized. That cost does not show up in the transfer ratio at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does prefill/decode disaggregation improve throughput?&lt;/strong&gt;&lt;br&gt;
Not by itself. The vLLM documentation states that disaggregated prefill does not improve throughput. Its value is independent tuning of TTFT and ITL and tighter tail ITL control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How big is the KV cache I have to transfer?&lt;/strong&gt;&lt;br&gt;
Use &lt;code&gt;2 * layers * kv_heads * head_dim * dtype_bytes&lt;/code&gt; per token. For Llama-3.1-70B in BF16 that is 320 KiB per token, about 2.68 GB for an 8,192-token prompt. FP8 KV halves it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is chunked prefill enough instead?&lt;/strong&gt;&lt;br&gt;
Often, for short prompts and moderate load. Chunking bounds the decode stall by chunk size but does not eliminate it. The vLLM docs say disaggregation controls tail ITL more reliably than chunked prefilling, at the cost of the KV transfer and a more complex deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which vLLM connector should I use?&lt;/strong&gt;&lt;br&gt;
The docs list several, including NixlConnector, MooncakeConnector, LMCacheConnectorV1, and OffloadingConnector. Choice depends on your interconnect and stack, and the feature is marked experimental, so pin your version and re-test on upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between pull-based and push-based KV transfer?&lt;/strong&gt;&lt;br&gt;
In pull-based transfer the decode instance requests KV from the prefill instance. In push-based transfer the prefill instance writes KV directly into decode memory over RDMA, which the vLLM blog says removes the router relay, decode-side allocation, and transfer setup from the TTFT critical path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/five-ways-your-llm-serving-benchmark-is-lying-to-you-and-how-to-catch-each-one-2bpg"&gt;Five Ways Your LLM Serving Benchmark Is Lying to You (and How to Catch Each One)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/kv-cache-quantization-in-llm-serving-fp8-and-int8-tradeoffs-the-silent-configjson-trap-and-how-5acc"&gt;KV Cache Quantization in LLM Serving: FP8 and INT8 Tradeoffs, the Silent config.json Trap, and How to Measure It Fairly&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sources: &lt;a href="https://docs.vllm.ai/en/latest/features/disagg_prefill/" rel="noopener noreferrer"&gt;vLLM disaggregated prefilling docs&lt;/a&gt;, &lt;a href="https://vllm.ai/blog/2026-10-14-nixl-push-connector" rel="noopener noreferrer"&gt;vLLM blog, push-based KV transfer (2026-10-14)&lt;/a&gt;, &lt;a href="https://huggingface.co/NousResearch/Meta-Llama-3.1-70B/blob/main/config.json" rel="noopener noreferrer"&gt;Llama-3.1-70B config.json&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>vllm</category>
    </item>
    <item>
      <title>On-Device LLM Inference: The Tradeoffs Nobody Puts on the Spec Sheet</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Mon, 12 Oct 2026 10:36:47 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/on-device-llm-inference-the-tradeoffs-nobody-puts-on-the-spec-sheet-1l9f</link>
      <guid>https://dev.to/wolfsea2357/on-device-llm-inference-the-tradeoffs-nobody-puts-on-the-spec-sheet-1l9f</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Running a language model on a device, a tablet, a handset, an edge box, is not just the cloud without the network. It is a different budget. Memory is fixed, precision is a lever, latency includes a cold start, and there is no autoscaler to hide your mistakes. This post walks through the tradeoffs that decide whether on-device inference is a demo or a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why on-device at all
&lt;/h2&gt;

&lt;p&gt;Three reasons push inference onto the device: data that cannot leave, environments with no reliable network, and latency that must not depend on a round trip. A factory floor, a secure facility, or a field device often has all three at once. When any of them holds, the cloud is not an option, and the question becomes how much capability you can fit locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory is the hard wall
&lt;/h2&gt;

&lt;p&gt;In the cloud you rent more memory. On a device you do not. The model weights, the key-value cache for context, and the runtime all have to fit in what the device has, at the same time.&lt;/p&gt;

&lt;p&gt;This makes context length a budget decision, not a free parameter. A longer prompt grows the cache, which competes with the weights for the same fixed memory. The practical move is to size context to the device, not to the model's theoretical maximum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Precision is the main lever
&lt;/h2&gt;

&lt;p&gt;Lower numeric precision shrinks the model and speeds it up, at some cost to quality. The art is choosing a precision that fits memory and still holds the behavior you need.&lt;/p&gt;

&lt;p&gt;Two cautions. First, the same model at a lower precision can behave differently, so benchmark the precision you will actually ship, not a higher one you tested in the lab. Second, quality loss from quantization is not uniform across tasks, so test on your task, not a generic one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Latency includes a cold start
&lt;/h2&gt;

&lt;p&gt;Cloud latency is usually measured warm. On a device, the model often has to load into memory first, and that cold start can dominate the first user experience. A request that arrives before the model is ready will fail or wait.&lt;/p&gt;

&lt;p&gt;The fix is to treat loading as a visible state. Show that the model is warming up, retry the first request with backoff, and do not report the warm-up error as a crash. A 503 during load is a normal signal, not a fault.&lt;/p&gt;

&lt;h2&gt;
  
  
  No autoscaler, no hiding
&lt;/h2&gt;

&lt;p&gt;In the cloud a bad capacity estimate gets papered over by scaling. On a device there is nothing to scale into. That raises the bar on three things: measure real memory headroom before you ship, estimate throughput with the mean rather than the median so one slow case does not get lost, and watch which resource is actually saturating under load rather than trusting a ratio.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Does the model, cache, and runtime fit in device memory at your target context length?&lt;/li&gt;
&lt;li&gt;Did you benchmark the exact precision you will ship, on your task?&lt;/li&gt;
&lt;li&gt;Is cold start a visible, retried state rather than an error?&lt;/li&gt;
&lt;li&gt;Did you size context to the device, not the model maximum?&lt;/li&gt;
&lt;li&gt;Did you measure headroom and throughput under real load, not at idle?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is on-device always worth it?&lt;/strong&gt; No. It is worth it when data cannot leave, the network is unreliable, or latency cannot depend on a round trip. Otherwise the cloud is simpler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How low can precision go?&lt;/strong&gt; As low as your task tolerates. That is an empirical question you answer by testing on your task, not a fixed number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does cold start matter so much?&lt;/strong&gt; Because it often dominates the first request, which is the one that shapes the user's impression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks most often?&lt;/strong&gt; Memory. Context length quietly grows the cache until it collides with the weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;On-device inference rewards humility about fixed resources. Fit memory first, ship the precision you tested, make cold start a visible state, and measure under real load. The device will punish every shortcut.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>edgeai</category>
    </item>
    <item>
      <title>KV Cache Quantization in LLM Serving: FP8 and INT8 Tradeoffs, the Silent config.json Trap, and How to Measure It Fairly</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Tue, 06 Oct 2026 19:10:48 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/kv-cache-quantization-in-llm-serving-fp8-and-int8-tradeoffs-the-silent-configjson-trap-and-how-5acc</link>
      <guid>https://dev.to/wolfsea2357/kv-cache-quantization-in-llm-serving-fp8-and-int8-tradeoffs-the-silent-configjson-trap-and-how-5acc</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The KV cache is often the real memory ceiling in LLM serving, not the weights. Quantizing it to FP8 or INT8 roughly halves KV memory, which lets you serve longer contexts or more concurrent requests on the same GPU. The catch: the quality impact is real but small when measured correctly, and it is easy to measure it wrong. A single &lt;code&gt;kv_cache_dtype&lt;/code&gt; flag, or a scaling factor baked into a model's &lt;code&gt;config.json&lt;/code&gt;, can quietly change your outputs while every dashboard still shows green. This post covers where the savings come from, what actually degrades, the silent-config trap, and a fair measurement protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is the KV cache the thing you should quantize?
&lt;/h2&gt;

&lt;p&gt;When a transformer generates tokens autoregressively, it stores the key and value tensors for every past token so it does not recompute them. That store is the KV cache, and it grows linearly with sequence length and batch size.&lt;/p&gt;

&lt;p&gt;A rough per-token KV size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bytes_per_token = 2 (K and V)
               * num_layers
               * num_kv_heads
               * head_dim
               * dtype_bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a mid-size model with 32 layers, 8 KV heads (grouped-query attention), and head_dim 128, in FP16 that is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 * 32 * 8 * 128 * 2 = 131,072 bytes  (128 KB per token)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 8K context that is 1 GB per sequence, before you batch anything. Weights are a fixed cost you pay once; the KV cache is a per-request cost that scales with your traffic. That is why it is the highest-leverage place to spend a quantization budget. Drop the dtype from 2 bytes to 1 byte and you halve that 128 KB to 64 KB per token, which directly buys you longer contexts or a bigger batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  FP8 vs INT8 for the KV cache: what is the real tradeoff?
&lt;/h2&gt;

&lt;p&gt;Both formats store each cached value in 8 bits, so the memory savings are effectively identical, about 2x versus FP16/BF16. The difference is in how they represent numbers and what that does to accuracy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FP8&lt;/strong&gt; (typically &lt;code&gt;e4m3&lt;/code&gt; or &lt;code&gt;e5m2&lt;/code&gt;) keeps an exponent field, so it covers a wide dynamic range with graceful precision falloff. The vLLM docs note that FP8 KV cache is supported on CUDA 11.8 and later and on ROCm, and that &lt;code&gt;e4m3&lt;/code&gt; is the common default on NVIDIA hardware. Because attention keys and values can have occasional large-magnitude outliers, the exponent bits help: an outlier does not blow out the whole tensor's scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;INT8&lt;/strong&gt; uses a uniform grid with a per-tensor or per-channel scale. When the distribution is well behaved, INT8 can actually be more precise than FP8 inside its range because it does not spend bits on an exponent. The risk is outliers: one large value forces a coarse scale on everything else. INT8 KV usually wants calibration to pick good scales.&lt;/p&gt;

&lt;p&gt;Practical reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FP8 is the lower-friction default. On recent NVIDIA and AMD GPUs it often needs no calibration and tends to be robust to outliers.&lt;/li&gt;
&lt;li&gt;INT8 can match or beat FP8 quality with good per-channel scales, and it is useful where FP8 hardware paths are missing, but it asks more of you up front.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How much quality do you actually lose?
&lt;/h2&gt;

&lt;p&gt;Less than people fear, if you measure the right thing. KV quantization only touches the cache, not the weights or the activations on the compute path, so the error is confined to attention's read of past tokens. In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-context, factual tasks: differences are usually within benchmark noise.&lt;/li&gt;
&lt;li&gt;Long-context retrieval and exact-copy tasks: this is where FP8/INT8 KV shows up first, because tiny per-token errors accumulate across thousands of cached positions.&lt;/li&gt;
&lt;li&gt;Reasoning chains: a small per-step perturbation can flip a final answer even when average token accuracy barely moves, so pass@1 on reasoning sets is more sensitive than perplexity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The headline number ("FP8 KV costs X percent") is meaningless without naming the task, the context length, and whether you measured a distribution or a single greedy run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The silent config.json trap
&lt;/h2&gt;

&lt;p&gt;Here is the failure that burns teams. You benchmark model A and model B with what looks like the identical serving command, same flags, same prompts, and you get different accuracy. The flags were identical. The conditions were not.&lt;/p&gt;

&lt;p&gt;Two things commonly hide inside a model directory rather than on your command line:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A checkpoint-level KV cache dtype.&lt;/strong&gt; Some published checkpoints ship a quantization config in &lt;code&gt;config.json&lt;/code&gt; (for example a &lt;code&gt;quantization_config&lt;/code&gt; or a KV-specific field) that sets the cache to FP8 even when your launch command says nothing about it. Your serving engine reads it and honors it. Your benchmark harness never sees it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Baked-in KV scaling factors.&lt;/strong&gt; FP8 KV can use static per-layer scales stored in the checkpoint. If one checkpoint has calibrated scales and another falls back to default scales, their attention precision differs even though both report "fp8 KV" in the logs. vLLM exposes &lt;code&gt;--calculate-kv-scales&lt;/code&gt; to compute scales at runtime for the &lt;code&gt;e4m3&lt;/code&gt; path precisely so you are not silently depending on whatever shipped in the file.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson that keeps recurring: identical flags are not identical conditions. The authoritative record of what ran is the resolved config inside the model folder plus the engine's startup log, not the command you typed.&lt;/p&gt;

&lt;p&gt;A one-line guard before trusting any KV benchmark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dump the resolved KV settings the engine actually used&lt;/span&gt;
python - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import json, glob
for f in glob.glob("models/**/config.json", recursive=True):
    c = json.load(open(f))
    qc = c.get("quantization_config", {})
    print(f, "| kv:", c.get("kv_cache_dtype"), "| qc_kv:", qc.get("kv_cache_dtype"))
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then confirm against the server's own report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve &amp;lt;model&amp;gt; &lt;span class="nt"&gt;--kv-cache-dtype&lt;/span&gt; fp8_e4m3 &lt;span class="nt"&gt;--calculate-kv-scales&lt;/span&gt; 2&amp;gt;&amp;amp;1 &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"kv.cache|kv_scale|quantiz"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the two disagree, the file wins, and your benchmark was comparing two different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you measure KV quantization fairly?
&lt;/h2&gt;

&lt;p&gt;Treat it as an A/B where the only thing allowed to change is the KV dtype. Everything else gets pinned.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Fair KV-quant comparison: pin everything except kv_cache_dtype
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vllm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SamplingParams&lt;/span&gt;

&lt;span class="n"&gt;COMMON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your/model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_model_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gpu_memory_utilization&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# pin the seed
&lt;/span&gt;    &lt;span class="n"&gt;enforce_eager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# avoid graph-capture variance while measuring
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SamplingParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# greedy, fixed budget
&lt;/span&gt;
&lt;span class="n"&gt;baseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;COMMON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv_cache_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# FP16/BF16 KV
&lt;/span&gt;&lt;span class="n"&gt;fp8&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;COMMON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv_cache_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fp8_e4m3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calculate_kv_scales&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules that make the result trustworthy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin the seed and the decoding params.&lt;/strong&gt; Compare greedy to greedy, or run N samples per prompt and compare distributions. Never compare one greedy run to one sampled run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give reasoning models room.&lt;/strong&gt; A &lt;code&gt;max_tokens&lt;/code&gt; that is too small truncates the chain and manufactures a fake quality gap that has nothing to do with the KV cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable prefix caching, or warm both runs identically.&lt;/strong&gt; A shared prefix cache can make the second configuration look faster or different for reasons unrelated to quantization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure where it bites.&lt;/strong&gt; Include a long-context retrieval task and a reasoning set, not just short QA. If your eval cannot tell FP16 from FP8, your eval is too easy, not your quantization too good.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report two standard errors, not a point.&lt;/strong&gt; KV-quant effects are small. A 0.4 point difference with an SE of 0.6 is noise, not a regression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate memory from quality.&lt;/strong&gt; Show the KV memory you saved and the accuracy you paid in the same table, because the whole point is the tradeoff.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal results table makes the decision obvious:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;KV bytes/token&lt;/th&gt;
&lt;th&gt;Max batch at 8K&lt;/th&gt;
&lt;th&gt;Long-ctx retrieval&lt;/th&gt;
&lt;th&gt;Reasoning pass@1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FP16 KV&lt;/td&gt;
&lt;td&gt;128 KB&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;td&gt;94.1 +/- 0.5&lt;/td&gt;
&lt;td&gt;71.2 +/- 0.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8 e4m3 KV&lt;/td&gt;
&lt;td&gt;64 KB&lt;/td&gt;
&lt;td&gt;~2x&lt;/td&gt;
&lt;td&gt;93.8 +/- 0.5&lt;/td&gt;
&lt;td&gt;70.9 +/- 0.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Numbers above are illustrative placeholders; fill them from your own run.)&lt;/p&gt;

&lt;p&gt;If the accuracy columns overlap within two standard errors and the batch column doubles, FP8 KV is a free win for that workload. If the long-context column drops outside the error bars, you have found the ceiling for that task and should keep FP16 KV there.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does KV cache quantization quantize the model weights too?&lt;/strong&gt;&lt;br&gt;
No. It only changes how cached keys and values are stored. Weights and the active compute path are untouched unless you separately apply weight quantization. That is why the quality impact is usually smaller than full weight quantization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FP8 or INT8, which should I pick first?&lt;/strong&gt;&lt;br&gt;
Start with FP8 &lt;code&gt;e4m3&lt;/code&gt; on recent NVIDIA or AMD GPUs. It is robust to attention outliers and often needs no calibration. Reach for INT8 when FP8 hardware paths are unavailable or when you have invested in good per-channel calibration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did two checkpoints give different accuracy with the same command?&lt;/strong&gt;&lt;br&gt;
Almost always a setting inside the model folder. A &lt;code&gt;config.json&lt;/code&gt; KV dtype or baked-in KV scales override your intent silently. Dump the resolved config and the engine startup log before trusting the comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does &lt;code&gt;--calculate-kv-scales&lt;/code&gt; do?&lt;/strong&gt;&lt;br&gt;
For the FP8 &lt;code&gt;e4m3&lt;/code&gt; path it computes the KV scaling factors at runtime instead of relying on whatever scales shipped in the checkpoint, which keeps your comparison honest and avoids depending on an unknown baked-in value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will KV quantization make serving faster, or just smaller?&lt;/strong&gt;&lt;br&gt;
Primarily smaller, which indirectly raises throughput: a smaller KV cache means a larger batch or longer context fits, and higher batch occupancy is where serving throughput actually comes from. Do not expect a large per-token latency drop by itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know my eval is sensitive enough to detect a regression?&lt;/strong&gt;&lt;br&gt;
Include a task you know FP16 passes and a weak baseline fails. If FP8 KV and FP16 KV score identically on everything, verify the eval can discriminate at all before concluding there is no loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;vLLM documentation: FP8 KV cache and &lt;code&gt;kv_cache_dtype&lt;/code&gt; options (docs.vllm.ai)&lt;/li&gt;
&lt;li&gt;vLLM documentation: &lt;code&gt;--calculate-kv-scales&lt;/code&gt; and quantization support matrix (docs.vllm.ai)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>vllm</category>
      <category>performance</category>
      <category>devops</category>
    </item>
    <item>
      <title>What Does It Actually Cost to Self-Host an LLM? The Batching Math Nobody Shows You</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Tue, 06 Oct 2026 00:43:30 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/what-does-it-actually-cost-to-self-host-an-llm-the-batching-math-nobody-shows-you-4pmp</link>
      <guid>https://dev.to/wolfsea2357/what-does-it-actually-cost-to-self-host-an-llm-the-batching-math-nobody-shows-you-4pmp</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Self-hosting an open-weight LLM is rarely expensive because the GPU is expensive. It is expensive because most people run the GPU at single-digit utilization. The headline rental price of an accelerator is fixed per hour, so your true cost per million tokens is set almost entirely by how many tokens you push through that hour. Continuous batching, a realistic input/output token mix, and honest utilization numbers move the price by more than 10x. This post gives you a back-of-the-envelope model you can plug your own numbers into, plus the three server settings that actually change the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is the per-hour GPU price the wrong number to quote?
&lt;/h2&gt;

&lt;p&gt;When someone says "an H100 costs about 2 to 3 dollars per hour," they have quoted an input to the problem, not the answer. What you sell or consume is tokens, not GPU-seconds. The conversion factor between them is throughput, and throughput is not a property of the GPU alone. It is a property of the GPU plus the model plus the request pattern plus your server configuration.&lt;/p&gt;

&lt;p&gt;The single formula that matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cost_per_1M_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpu_hourly_cost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tokens_per_hour&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;
&lt;span class="n"&gt;tokens_per_hour&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;throughput_tokens_per_sec&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;utilization&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two deployments renting the identical GPU at the identical hourly price can differ by an order of magnitude in &lt;code&gt;cost_per_1M_tokens&lt;/code&gt;, purely because one achieves 800 tokens per second at 70 percent utilization and the other achieves 90 tokens per second at 15 percent utilization. The hardware invoice is the same. The unit economics are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much does continuous batching really change the math?
&lt;/h2&gt;

&lt;p&gt;A naive server processes one request, waits for it to finish generating, then starts the next. The GPU spends most of its time idle between the compute-heavy prefill and the memory-bound decode steps. Continuous batching, popularized by the vLLM project, instead keeps a running set of in-flight sequences and injects new requests into the batch as soon as slots free up, token by token. The vLLM docs describe this as iteration-level scheduling, and it is the main reason a well-tuned open server reaches throughput that feels implausible if you have only ever benchmarked batch size 1.&lt;/p&gt;

&lt;p&gt;Here is the intuition in a tiny simulator. It is not a GPU model, it just shows why aggregate throughput climbs with concurrency until something saturates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tokens_per_hour&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;single_stream_tps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_concurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scaling_efficiency&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# scaling_efficiency &amp;lt; 1 captures memory-bandwidth and KV-cache limits
&lt;/span&gt;    &lt;span class="n"&gt;effective&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;single_stream_tps&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;max_concurrency&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;scaling_efficiency&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;effective&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tokens_per_hour&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scaling_efficiency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tph&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;   &lt;span class="c1"&gt;# $2.5/hr GPU, illustrative
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;concurrency=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  tok/hr=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tph&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;1e6&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;5.2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;M  $/1M=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbers above are illustrative, not measured, but the shape is real: moving from one in-flight request to a few dozen can turn a 20-dollar price per million tokens into something close to 1 dollar. The cover chart shows the same relationship. The curve flattens once you hit a bottleneck, which is usually KV-cache memory rather than raw compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What caps the batch size before compute does?
&lt;/h2&gt;

&lt;p&gt;The quiet ceiling is the KV cache. Every active sequence stores keys and values for all previous tokens, and that memory grows with context length and batch size. When the cache fills, the scheduler either queues new requests or preempts running ones, and your effective concurrency stops climbing.&lt;/p&gt;

&lt;p&gt;A rough KV-cache size estimate for a transformer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;kv_bytes_per_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;num_layers&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;num_kv_heads&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;head_dim&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bytes_per_elem&lt;/span&gt;
&lt;span class="c1"&gt;# factor of 2 is for keys AND values
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two levers shrink this and therefore raise the concurrency ceiling:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Grouped-query attention (fewer KV heads than query heads) directly reduces &lt;code&gt;num_kv_heads&lt;/code&gt;, which is why many recent open models adopt it.&lt;/li&gt;
&lt;li&gt;KV-cache quantization (storing the cache in FP8 instead of FP16) halves &lt;code&gt;bytes_per_elem&lt;/code&gt;. vLLM exposes this through &lt;code&gt;kv_cache_dtype&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your p99 latency is fine but throughput plateaus early, you are almost certainly cache-bound, not compute-bound. Measure &lt;code&gt;gpu_cache_usage&lt;/code&gt; in the server metrics before you reach for a bigger GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does quantizing the weights lower cost, or just fit a bigger model?
&lt;/h2&gt;

&lt;p&gt;Weight quantization (for example 4-bit GGUF for llama.cpp, or AWQ and GPTQ for GPU serving) does two separate things, and people conflate them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It reduces the memory the weights occupy, which frees room for a larger KV cache and therefore higher concurrency. This lowers cost per token.&lt;/li&gt;
&lt;li&gt;It reduces memory-bandwidth pressure during the decode phase, which is bandwidth-bound. This can raise single-stream throughput.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does not reliably do is cut cost on a GPU you were already underutilizing. If you serve one user at a time, a 4-bit model on an idle GPU still produces an embarrassing price per million tokens, because the denominator (tokens per hour) is still tiny. Quantization pays off when you combine it with batching so the freed memory becomes extra concurrency.&lt;/p&gt;

&lt;p&gt;GGUF on CPU is a related but different trade. llama.cpp can serve quantized models with no GPU at all, which is attractive for low-traffic internal tools where a GPU would sit idle. The crossover point is traffic volume: below some requests-per-minute threshold, a CPU box you already own beats a rented GPU you barely use. Above it, the GPU wins decisively because its tokens-per-hour ceiling is so much higher.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I estimate my own number in ten minutes?
&lt;/h2&gt;

&lt;p&gt;You need four inputs, three of which you can measure and one you must assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;gpu_hourly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;          &lt;span class="c1"&gt;# your actual rental or amortized price
&lt;/span&gt;&lt;span class="n"&gt;measured_tps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;650&lt;/span&gt;        &lt;span class="c1"&gt;# steady-state tokens/sec under realistic load
&lt;/span&gt;&lt;span class="n"&gt;utilization&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.60&lt;/span&gt;        &lt;span class="c1"&gt;# fraction of the hour the GPU is actually busy
&lt;/span&gt;&lt;span class="n"&gt;overhead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.15&lt;/span&gt;           &lt;span class="c1"&gt;# networking, retries, idle head start, replicas
&lt;/span&gt;
&lt;span class="n"&gt;tph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;measured_tps&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;utilization&lt;/span&gt;
&lt;span class="n"&gt;cost_per_1M&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpu_hourly&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tph&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;overhead&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost_per_1M&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USD per 1M tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one input people fake is &lt;code&gt;measured_tps&lt;/code&gt;. Do not copy a vendor benchmark run at batch 256 with 128-token outputs if your workload is batch 4 with 1,000-token outputs. Output-heavy workloads spend far more time in the bandwidth-bound decode phase, so their tokens-per-hour is lower and their cost is higher. Benchmark with your own input/output length distribution or the number is fiction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which three settings move the result the most?
&lt;/h2&gt;

&lt;p&gt;After tuning many open-model deployments, the settings with the largest effect on cost per token are consistently these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;max_num_seqs&lt;/code&gt; (or the equivalent max concurrency). Set it too low and you leave throughput on the table. Set it too high and you thrash the KV cache and trigger preemption. Sweep it against your real traffic.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gpu_memory_utilization&lt;/code&gt;. vLLM pre-allocates the KV cache from this fraction. Nudging it from 0.80 to 0.90 can meaningfully raise the concurrency ceiling, as long as you keep headroom for activation spikes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kv_cache_dtype&lt;/code&gt; set to fp8. This is often the single cheapest way to raise concurrency on memory-bound models, with a quality impact that is small for many workloads but must be validated per model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else (speculative decoding, chunked prefill, tensor parallel degree) matters, but these three are where the first large wins live.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is self-hosting cheaper than a hosted API?&lt;/strong&gt;&lt;br&gt;
It depends entirely on utilization. A hosted API amortizes one GPU across thousands of tenants, so it runs at high utilization you would struggle to match. Self-hosting wins on cost only when your sustained traffic keeps your own GPU busy, or when data residency and latency, not price, are the reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is my cost per token so much higher than the blog posts I read?&lt;/strong&gt;&lt;br&gt;
Almost always because those posts report peak throughput at large batch sizes with short outputs, while your workload has long outputs and modest concurrency. Re-run the benchmark with your own token distribution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a bigger GPU always lower cost per token?&lt;/strong&gt;&lt;br&gt;
No. A bigger GPU only helps if you can fill it. If you are already at low utilization, a larger accelerator raises your hourly cost while the tokens-per-hour barely moves, making the unit cost worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do continuous batching and quantization overlap?&lt;/strong&gt;&lt;br&gt;
Quantization frees memory, continuous batching turns that freed memory into extra concurrent sequences. Used together they compound. Used alone on an idle GPU, neither fixes the underlying utilization problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know if I am compute-bound or memory-bound?&lt;/strong&gt;&lt;br&gt;
Watch KV-cache usage and GPU compute utilization together. If cache usage hits the ceiling while compute sits below 100 percent, you are memory-bound and should attack the cache (GQA models, FP8 cache, shorter contexts) before buying compute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a realistic utilization target?&lt;/strong&gt;&lt;br&gt;
For bursty interactive traffic, sustained 50 to 70 percent is a reasonable and honest target. Claiming 95 percent usually means you are either batching offline jobs or quoting a benchmark, not a production SLA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;vLLM documentation on continuous batching and paged attention: &lt;a href="https://docs.vllm.ai/" rel="noopener noreferrer"&gt;https://docs.vllm.ai/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;llama.cpp and the GGUF format for CPU and quantized serving: &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;https://github.com/ggml-org/llama.cpp&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All throughput and cost figures in this post are labeled illustrative and are meant to show the shape of the relationships. Replace them with your own measured values before making a budget decision.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>vllm</category>
      <category>performance</category>
      <category>devops</category>
    </item>
    <item>
      <title>Five Ways Your LLM Serving Benchmark Is Lying to You (and How to Catch Each One)</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Mon, 05 Oct 2026 08:55:22 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/five-ways-your-llm-serving-benchmark-is-lying-to-you-and-how-to-catch-each-one-2bpg</link>
      <guid>https://dev.to/wolfsea2357/five-ways-your-llm-serving-benchmark-is-lying-to-you-and-how-to-catch-each-one-2bpg</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In my benchmarking work on vLLM serving stacks, every one of the following produced a confident, wrong conclusion before anyone noticed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prefix caching inflated an A/B test unequally.&lt;/strong&gt; The harness reused the same prompt on every call. Cache hit rate was 67% on one arm and 45% on the other, and paired throughput came out 2.0 to 2.8x higher than a sweep run on the identical server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The harness counted SSE chunks, not tokens.&lt;/strong&gt; Under speculative decoding one chunk carries several tokens, so measured output was cut nearly in half (698 vs 362). On a 1:1 chat workload that error is about 20% of total throughput: enough to report a +49% win as a loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identical CLI flags, different runtime.&lt;/strong&gt; A &lt;code&gt;config.json&lt;/code&gt; inside one model folder declared an 8-bit KV cache scheme, and vLLM silently enabled FP8 KV. KV capacity was 1.775x larger on that arm (940,736 vs 529,856 tokens).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five knobs changed at once.&lt;/strong&gt; Result: -16% to -22% on long-context reads, with no way to tell which knob did it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "stability-first" auto-selection gate picked the slow config.&lt;/strong&gt; It was the only candidate with run-to-run spread under 3% because it was 20% slower everywhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bonus: &lt;code&gt;Restart=always&lt;/code&gt; turned a configuration bug into a silent crash loop that &lt;code&gt;systemctl is-active&lt;/code&gt; reported as healthy.&lt;/p&gt;

&lt;p&gt;Each section below gives the symptom, the mechanism, and a code-level fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does prefix caching inflate vLLM benchmarks, and why unequally?
&lt;/h2&gt;

&lt;p&gt;vLLM enables Automatic Prefix Caching by default in recent V1 releases (&lt;a href="https://docs.vllm.ai/en/latest/features/automatic_prefix_caching.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt;). If two requests share a prefix, the second one skips prefill for the cached blocks. That is great in production and poison in a synthetic benchmark.&lt;/p&gt;

&lt;p&gt;Here is what happened. A team I worked with ran a paired A/B of two serving configs and wanted one shot per configuration, so they used &lt;code&gt;--reps 1 --warm 0&lt;/code&gt;. The harness derived the prompt seed from the batch index &lt;code&gt;b&lt;/code&gt;. With one rep and no warmup, &lt;code&gt;b&lt;/code&gt; was always 0. &lt;strong&gt;Every call sent the exact same prompt.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Measured prefix cache hit rate:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Arm%2520A%2522%252C%2522Arm%2520B%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522hit%2520rate%2520%2525%2522%252C%2522data%2522%253A%255B67%252C45%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523d9534f%2522%252C%2522%2523f0ad4e%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Prefix%2520cache%2520hit%2520rate%2520when%2520every%2520call%2520reused%2520one%2520prompt%2520%2528measured%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522font%2522%253A%257B%2522weight%2522%253A%2522bold%2522%257D%257D%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522hit%2520rate%2520%2525%2522%257D%257D%255D%257D%257D%257D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Arm%2520A%2522%252C%2522Arm%2520B%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522hit%2520rate%2520%2525%2522%252C%2522data%2522%253A%255B67%252C45%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523d9534f%2522%252C%2522%2523f0ad4e%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Prefix%2520cache%2520hit%2520rate%2520when%2520every%2520call%2520reused%2520one%2520prompt%2520%2528measured%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522font%2522%253A%257B%2522weight%2522%253A%2522bold%2522%257D%257D%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522hit%2520rate%2520%2525%2522%257D%257D%255D%257D%257D%257D" alt="Prefix cache hit rate per arm" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The clue was a ratio that made no sense: on the same server with the same settings, paired-comparison throughput was &lt;strong&gt;2.0 to 2.8x&lt;/strong&gt; the sweep throughput. At concurrency 32 on a long-context workload, the paired run reported 61,347 tok/s against 22,159 tok/s in the sweep.&lt;/p&gt;

&lt;p&gt;The part most people miss is that the contamination is &lt;strong&gt;not symmetric&lt;/strong&gt;. Cache capacity is whatever KV memory is left over, so the arm with more free KV blocks keeps cached prefixes alive longer and gets a larger bonus. You stop measuring "which config is faster" and start measuring "which config retains cache better". If the two arms have different memory headroom, that alone biases the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: seed every call independently and log the hit rate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="n"&gt;LARGE_PRIME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1_000_003&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Separate axes for run seed, batch and request index, so
&lt;/span&gt;    &lt;span class="c1"&gt;# no two calls in any phase or concurrency level share a prefix.
&lt;/span&gt;    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;LARGE_PRIME&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Put the randomness at the START of the prompt; a shared system
&lt;/span&gt;    &lt;span class="c1"&gt;# prompt in front would still produce prefix hits.
&lt;/span&gt;    &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[req &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getrandbits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;016&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VOCAB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_words&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then prove it. Scrape the server's Prometheus endpoint before and after each run and record the delta as part of the result artifact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prefix_cache_counters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vllm:prefix_cache_queries_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vllm:prefix_cache_hits_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;rf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;(?:{{[^}}]*}})?\s+([0-9.e+]+)$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;span class="c1"&gt;# hit_rate = d(hits) / d(queries) over the run window
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Metric names vary across vLLM versions; check your &lt;code&gt;/metrics&lt;/code&gt; output.) If you want a cache-free number, start the server with &lt;code&gt;--no-enable-prefix-caching&lt;/code&gt;, but still log the counters: "we disabled it" is a claim, the counter is evidence.&lt;/p&gt;

&lt;p&gt;One more lesson from that week: before you discount a competitor's published numbers because "they use prefix caching and we don't", print your own hit rate. In this case the team had made exactly that argument while running at 67%.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does my tokens-per-second drop when I enable speculative decoding?
&lt;/h2&gt;

&lt;p&gt;Sometimes it doesn't drop. Your counter does.&lt;/p&gt;

&lt;p&gt;A common streaming harness pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# OpenAI-compatible SSE stream
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ntok&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;                   &lt;span class="c1"&gt;# BUG: counts chunks, not tokens
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With normal decoding, one SSE event roughly equals one token, so this works by accident. Speculative decoding (EAGLE, MTP, n-gram drafting; see the &lt;a href="https://docs.vllm.ai/en/latest/features/spec_decode.html" rel="noopener noreferrer"&gt;vLLM speculative decoding docs&lt;/a&gt;) accepts several tokens per step, and the server emits them in a single chunk. The harness undercounts &lt;strong&gt;only&lt;/strong&gt; the accelerated arm.&lt;/p&gt;

&lt;p&gt;In the case I saw, measured output dropped from 698 to 362 tok/s for the speculative arm while total throughput moved only 6%. Those two numbers cannot both be true: you cannot halve output and keep total flat. That contradiction was the entrance to the bug.&lt;/p&gt;

&lt;p&gt;Why didn't it show up sooner? The test was long-context with a 32:1 input:output ratio, so output tokens were a tiny share of the total and the error on total was about 1%. On a 1:1 chat-shaped workload, the same bug costs about 20% of total throughput. The real speculative result there was &lt;strong&gt;+49%&lt;/strong&gt;; a 20% haircut on one arm is big enough to flip the headline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Paired%2520A%2FB%2520run%2522%252C%2522Sweep%2520run%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522tok%2Fs%2520at%2520concurrency%252032%2522%252C%2522data%2522%253A%255B61347%252C22159%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523d9534f%2522%252C%2522%25234a7bd0%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Same%2520server%252C%2520same%2520settings%253A%2520paired%2520run%2520vs%2520sweep%2520%2528measured%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522font%2522%253A%257B%2522weight%2522%253A%2522bold%2522%257D%257D%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522tok%2Fs%2520at%2520concurrency%252032%2522%257D%257D%255D%257D%257D%257D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Paired%2520A%2FB%2520run%2522%252C%2522Sweep%2520run%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522tok%2Fs%2520at%2520concurrency%252032%2522%252C%2522data%2522%253A%255B61347%252C22159%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523d9534f%2522%252C%2522%25234a7bd0%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Same%2520server%252C%2520same%2520settings%253A%2520paired%2520run%2520vs%2520sweep%2520%2528measured%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522font%2522%253A%257B%2522weight%2522%253A%2522bold%2522%257D%257D%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522tok%2Fs%2520at%2520concurrency%252032%2522%257D%257D%255D%257D%257D%257D" alt="Long-context throughput: the cache artifact" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: count tokens from the source of truth
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OUT_LEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore_eos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;# vLLM extension: fixes output length
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# The final SSE event carries usage.completion_tokens. Use it.
# Cross-check: with ignore_eos, completion_tokens must equal OUT_LEN.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;ignore_eos=True&lt;/code&gt; plus &lt;code&gt;max_tokens=N&lt;/code&gt; pins the output length, you can often recompute historical results without rerunning: &lt;code&gt;true_total = in_tok_per_s * (in_len + out_len) / in_len&lt;/code&gt;, provided input token counts were exact. Apply the correction to &lt;strong&gt;every&lt;/strong&gt; arm with the same formula. Correcting only the arm you suspect is a new contamination.&lt;/p&gt;

&lt;p&gt;General rule: any counter that answers "how much" by counting "how many times" (chunks, callbacks, events) breaks under acceleration techniques, because those techniques work precisely by packing more into each event. Batching, speculative decoding and stream coalescing all do this, and each one breaks a different counter in a different direction. Log at least two quantities that must agree with each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  Can identical vLLM command-line flags produce different runtime configs?
&lt;/h2&gt;

&lt;p&gt;Yes, and command-line diffs will never show it.&lt;/p&gt;

&lt;p&gt;Two quantized checkpoints of the same base model were served with scripts identical except for model path, port and GPU index. The comparison was presented as a clean weights-only A/B. Then someone diffed the startup logs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arm&lt;/th&gt;
&lt;th&gt;KV cache dtype (from log)&lt;/th&gt;
&lt;th&gt;KV cache capacity (tokens)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kv_cache_dtype=auto&lt;/code&gt; (16-bit)&lt;/td&gt;
&lt;td&gt;529,856&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Using standard fp8 KV cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;940,736 (1.775x)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cause: arm B's &lt;code&gt;config.json&lt;/code&gt; carried a quantization block with a &lt;code&gt;kv_cache_scheme&lt;/code&gt; of 8 bits. vLLM reads quantization metadata from the checkpoint and enabled FP8 KV on its own (&lt;a href="https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache.html" rel="noopener noreferrer"&gt;quantized KV cache docs&lt;/a&gt;). Arm A had no such entry. Same flags, different conditions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522A%253A%2520auto%2520%252816-bit%2520KV%2529%2522%252C%2522B%253A%2520FP8%2520KV%2520from%2520config.json%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522KV%2520cache%2520tokens%2522%252C%2522data%2522%253A%255B529856%252C940736%255D%252C%2522backgroundColor%2522%253A%255B%2522%25234a7bd0%2522%252C%2522%2523d9534f%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520cache%2520capacity%2520with%2520identical%2520CLI%2520flags%2520%2528measured%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522font%2522%253A%257B%2522weight%2522%253A%2522bold%2522%257D%257D%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522KV%2520cache%2520tokens%2522%257D%257D%255D%257D%257D%257D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522A%253A%2520auto%2520%252816-bit%2520KV%2529%2522%252C%2522B%253A%2520FP8%2520KV%2520from%2520config.json%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522KV%2520cache%2520tokens%2522%252C%2522data%2522%253A%255B529856%252C940736%255D%252C%2522backgroundColor%2522%253A%255B%2522%25234a7bd0%2522%252C%2522%2523d9534f%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520cache%2520capacity%2520with%2520identical%2520CLI%2520flags%2520%2528measured%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522font%2522%253A%257B%2522weight%2522%253A%2522bold%2522%257D%257D%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522KV%2520cache%2520tokens%2522%257D%257D%255D%257D%257D%257D" alt="KV cache capacity with identical CLI flags" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same report had a second confound: one row was "weights B + default settings", another was "weights A + tuned settings", and the conclusion said "tuning lost". Two axes moved at once; that sentence was never proven.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: diff the realized config, not the intended one
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run after both servers are up. Compare what the engine actually did.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;serverA.log serverB.log&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ei&lt;/span&gt; &lt;span class="s2"&gt;"kv_cache_dtype|fp8 KV|GPU KV cache size|Maximum concurrency|attention backend|quantization=|tensor_parallel_size|max_num_seqs|enable_prefix_caching"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# And check the checkpoint itself before you start:&lt;/span&gt;
jq &lt;span class="s1"&gt;'.quantization_config | {quant_method, kv_cache_scheme}'&lt;/span&gt; modelB/config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to force parity, pass &lt;code&gt;--kv-cache-dtype&lt;/code&gt; explicitly on both arms rather than relying on &lt;code&gt;auto&lt;/code&gt;. And label every row of a results table with both (weights, config), never just one.&lt;/p&gt;

&lt;p&gt;A useful habit: when a difference appears, check whether it is a number you already know. That 1.775x matched an earlier experiment where increasing KV capacity by about 78% did not raise the concurrency ceiling at all. So "B wins because it has more KV" was ruled out in minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why should you tune vLLM settings one at a time?
&lt;/h2&gt;

&lt;p&gt;An "apply the recommended profile" change modified five settings together: FP8 KV cache, &lt;code&gt;--gpu-memory-utilization 0.95&lt;/code&gt;, lower &lt;code&gt;--max-model-len&lt;/code&gt;, higher &lt;code&gt;--max-num-seqs&lt;/code&gt;, and async scheduling. Result: &lt;strong&gt;-16% to -22% across every long-context point&lt;/strong&gt;. Nobody could say which knob caused it, and each server restart cost about 10 minutes, so bisecting was expensive.&lt;/p&gt;

&lt;p&gt;Serving knobs trade against each other (memory you gain, compute you may lose), so their effects have mixed signs and do not add up neatly.&lt;/p&gt;

&lt;p&gt;Two things made the failure useful anyway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The failure named the bottleneck.&lt;/strong&gt; Growing KV capacity by 78% (523,264 to 933,120 tokens) left the concurrency-64 throughput ceiling unchanged. Therefore the bottleneck was compute, not memory. A success would never have told us that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same setting flips sign by workload.&lt;/strong&gt; The identical profile was -22% on prefill-dominated long-context reads and +6.8% on a balanced read/write workload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Long-context%2520reads%2520%2528prefill-heavy%2529%2522%252C%2522Balanced%2520read%2Fwrite%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522change%2520vs%2520baseline%2520%2525%2522%252C%2522data%2522%253A%255B-22%252C6.8%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523d9534f%2522%252C%2522%25235cb85c%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Same%25205-knob%2520profile%252C%2520opposite%2520sign%2520by%2520workload%2520%2528measured%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522font%2522%253A%257B%2522weight%2522%253A%2522bold%2522%257D%257D%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522change%2520vs%2520baseline%2520%2525%2522%257D%257D%255D%257D%257D%257D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Long-context%2520reads%2520%2528prefill-heavy%2529%2522%252C%2522Balanced%2520read%2Fwrite%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522change%2520vs%2520baseline%2520%2525%2522%252C%2522data%2522%253A%255B-22%252C6.8%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523d9534f%2522%252C%2522%25235cb85c%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Same%25205-knob%2520profile%252C%2520opposite%2520sign%2520by%2520workload%2520%2528measured%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522font%2522%253A%257B%2522weight%2522%253A%2522bold%2522%257D%257D%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522change%2520vs%2520baseline%2520%2525%2522%257D%257D%255D%257D%257D%257D" alt="Same tuning, opposite sign by workload" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: one axis per run, and ask the code before restarting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Baseline, then vary exactly one flag per run.&lt;/span&gt;
&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--max-model-len 32768 --max-num-seqs 128 --gpu-memory-utilization 0.90"&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;VARIANT &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="s2"&gt;"--kv-cache-dtype fp8"&lt;/span&gt; &lt;span class="s2"&gt;"--max-num-seqs 256"&lt;/span&gt; &lt;span class="s2"&gt;"--async-scheduling"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;vllm serve &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MODEL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$BASE&lt;/span&gt; &lt;span class="nv"&gt;$VARIANT&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"log_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VARIANT&lt;/span&gt;&lt;span class="p"&gt;// /_&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;amp;
  wait_for_ready &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; run_bench &lt;span class="nt"&gt;--shape&lt;/span&gt; longctx &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; run_bench &lt;span class="nt"&gt;--shape&lt;/span&gt; chat
  &lt;span class="nb"&gt;kill&lt;/span&gt; %1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;wait
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flag names drift across releases, so check &lt;code&gt;vllm serve --help&lt;/code&gt; for your version. For expensive restarts (for example, choosing between attention backends) try to answer feasibility in-process before launching a server: in one case, calling the backend's support-check function directly classified four candidates in about a minute, after two full restarts had been wasted trying them one by one.&lt;/p&gt;

&lt;p&gt;Always benchmark at least two workload shapes (prefill-heavy and decode-heavy). A single-shape result is half a result.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why do "stability-first" auto-tuning gates pick the slowest config?
&lt;/h2&gt;

&lt;p&gt;An auto-selection script chose among three batch-limit candidates with this rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Among candidates whose run-to-run spread is under 3% of peak, pick the one with the highest peak.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Only one candidate passed (spread 0.1%). The other two had spreads around 20%. The winner was stable &lt;strong&gt;because it was 20% slower at every point&lt;/strong&gt;. Throttling a system removes variance along with throughput. With no performance floor, "do nothing" is always the most stable option.&lt;/p&gt;

&lt;p&gt;The resulting combined profile lost to the best single change on four of five measured points: -13.0%, +2.0%, -3.1%, -3.3%, -13.9%.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522P1%2522%252C%2522P2%2522%252C%2522P3%2522%252C%2522P4%2522%252C%2522P5%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522combo%2520vs%2520single%2520%2525%2522%252C%2522data%2522%253A%255B-13.0%252C2.0%252C-3.1%252C-3.3%252C-13.9%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523d9534f%2522%252C%2522%25235cb85c%2522%252C%2522%2523d9534f%2522%252C%2522%2523d9534f%2522%252C%2522%2523d9534f%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Auto-selected%2520combo%2520vs%2520best%2520single%2520change%252C%25205%2520points%2520%2528measured%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522font%2522%253A%257B%2522weight%2522%253A%2522bold%2522%257D%257D%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522combo%2520vs%2520single%2520%2525%2522%257D%257D%255D%257D%257D%257D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522P1%2522%252C%2522P2%2522%252C%2522P3%2522%252C%2522P4%2522%252C%2522P5%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522combo%2520vs%2520single%2520%2525%2522%252C%2522data%2522%253A%255B-13.0%252C2.0%252C-3.1%252C-3.3%252C-13.9%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523d9534f%2522%252C%2522%25235cb85c%2522%252C%2522%2523d9534f%2522%252C%2522%2523d9534f%2522%252C%2522%2523d9534f%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Auto-selected%2520combo%2520vs%2520best%2520single%2520change%252C%25205%2520points%2520%2528measured%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522font%2522%253A%257B%2522weight%2522%253A%2522bold%2522%257D%257D%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522combo%2520vs%2520single%2520%2525%2522%257D%257D%255D%257D%257D%257D" alt="Auto-selected combo vs best single change" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same gate had a second hole: "adopt if it wins on both shapes". Actual margins were +0.0% to +0.5%, all inside the noise, and they were adopted because the sign was positive.&lt;/p&gt;

&lt;p&gt;This is the same mistake at a smaller scale that I have seen in model evaluation: a probe at 89.5% vs a baseline at 89.1% was marked "pass" because &lt;code&gt;A &amp;gt; B&lt;/code&gt;. On 276 questions the standard error was about 1.9 points, the bootstrap 95% interval for the difference was [-4.3, +5.1] points, and McNemar z was 0.00. Not a win.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: gates need a floor and an interval, written in code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bootstrap_ci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Paired bootstrap of mean(a - b); a, b are per-run (or per-item) values.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;means&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;means&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;97.5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline_runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline_peak&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;peak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;spread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_runs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;peak&lt;/span&gt;
    &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bootstrap_ci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline_runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;spread&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;peak&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;baseline_peak&lt;/span&gt;   &lt;span class="c1"&gt;# performance floor
&lt;/span&gt;        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;                          &lt;span class="c1"&gt;# interval, not sign
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two more rules: always measure the auto-selected combination next to the best single change (combos can be worse than their parts), and do not assume more knobs means more speed. Of the five knobs tested that day, one helped; the rest were neutral or harmful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Can systemd Restart=always hide a broken benchmark worker?
&lt;/h2&gt;

&lt;p&gt;Yes. &lt;code&gt;Restart=always&lt;/code&gt; is medicine for transient failures. A configuration error is permanent, and the restart policy disguises it as a retry.&lt;/p&gt;

&lt;p&gt;In one fleet, an automatic rebalancer assigned zero work slots to three workers. The generated unit line ended up with an empty argument, which the shell dropped entirely. The worker script ran with &lt;code&gt;set -u&lt;/code&gt;, hit &lt;code&gt;$4: unbound variable&lt;/code&gt;, and exited immediately. systemd plus a watchdog revived it every 15 seconds, forever. Load average went to 67 with every GPU at 0%, and &lt;code&gt;systemctl is-active&lt;/code&gt; said &lt;strong&gt;active&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A related trap: a watchdog that "restores the service when the queue is idle" cannot tell idle from "I deliberately stopped things to run a benchmark". In one case it revived a 50 GB service onto the GPU under test, and the control server got roughly one tenth of its normal KV capacity (89,536 vs 919,232 tokens). Left alone, that would have produced a headline claiming speculative decoding was 10x faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;StartLimitIntervalSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;300&lt;/span&gt;
&lt;span class="py"&gt;StartLimitBurst&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5          # after 5 failures in 5 min, stay failed and alert&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;15&lt;/span&gt;
&lt;span class="py"&gt;ExecStartPre&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/validate-args.sh %i   # fail loudly on empty args&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate generated argument lists at generation time (raise on empty values), disable watchdogs and auto-restorers before a measurement window, and record &lt;code&gt;nvidia-smi&lt;/code&gt; memory per GPU in the benchmark artifact so a stray tenant shows up in the data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre-flight checklist for LLM serving benchmarks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every request has a unique prompt prefix; seeds use separate axes for run, batch and request index.&lt;/li&gt;
&lt;li&gt;[ ] Prefix cache hit rate is scraped from &lt;code&gt;/metrics&lt;/code&gt; and stored with the results.&lt;/li&gt;
&lt;li&gt;[ ] Output tokens come from &lt;code&gt;usage.completion_tokens&lt;/code&gt;, not chunk counts; &lt;code&gt;ignore_eos&lt;/code&gt; plus &lt;code&gt;max_tokens&lt;/code&gt; pins length.&lt;/li&gt;
&lt;li&gt;[ ] At least two quantities in each run must agree (output tok/s, total tok/s, completion_tokens x requests / wall time).&lt;/li&gt;
&lt;li&gt;[ ] Startup logs of both arms are diffed for KV dtype, KV capacity, attention backend, parallelism and max sequences.&lt;/li&gt;
&lt;li&gt;[ ] Checkpoint &lt;code&gt;config.json&lt;/code&gt; is inspected for &lt;code&gt;quantization_config&lt;/code&gt; and KV cache schemes.&lt;/li&gt;
&lt;li&gt;[ ] Results table rows state both weights and config.&lt;/li&gt;
&lt;li&gt;[ ] One knob per run; at least one prefill-heavy and one decode-heavy shape.&lt;/li&gt;
&lt;li&gt;[ ] Acceptance gates use a confidence interval lower bound above zero, plus a performance floor relative to baseline.&lt;/li&gt;
&lt;li&gt;[ ] Auto-selected combos are benchmarked against the best single change.&lt;/li&gt;
&lt;li&gt;[ ] Watchdogs off during measurement; per-GPU memory snapshot recorded; restart policies have burst limits.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I disable prefix caching for every vLLM benchmark?
&lt;/h3&gt;

&lt;p&gt;Not necessarily. If production traffic shares system prompts, a cache-on benchmark is more realistic. What matters is that both arms see the same cache conditions and that you report the measured hit rate. Unique prompts with caching left on, plus a logged hit rate near zero, is a clean way to measure raw engine speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I count tokens correctly with streaming responses?
&lt;/h3&gt;

&lt;p&gt;Request &lt;code&gt;stream_options: {"include_usage": true}&lt;/code&gt; and read &lt;code&gt;usage.completion_tokens&lt;/code&gt; from the final event. If you need per-token timing, tokenize the accumulated text with the model's tokenizer instead of counting events, and validate against the usage field.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can I tell if vLLM enabled FP8 KV cache on its own?
&lt;/h3&gt;

&lt;p&gt;Grep the startup log for the KV cache dtype and the reported KV cache size in tokens, and inspect &lt;code&gt;quantization_config&lt;/code&gt; in the checkpoint's &lt;code&gt;config.json&lt;/code&gt;. Passing &lt;code&gt;--kv-cache-dtype&lt;/code&gt; explicitly on every arm removes the ambiguity.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many repetitions do I need before declaring a winner?
&lt;/h3&gt;

&lt;p&gt;Enough that the confidence interval of the difference excludes zero. Start with at least five runs per arm, compute a paired bootstrap interval, and treat any gap smaller than the observed run-to-run spread as a tie.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a lower-variance config always better for production?
&lt;/h3&gt;

&lt;p&gt;No. Low variance is easy to buy by throttling throughput. Require a performance floor (for example, at least 95% of the best peak) before stability becomes a tiebreaker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did my tuning help chat traffic but hurt long documents?
&lt;/h3&gt;

&lt;p&gt;Prefill-dominated and decode-dominated workloads hit different bottlenecks. In the case above the same profile measured -22% on long-context reads and +6.8% on a balanced workload. Benchmark both shapes and report them separately.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>vllm</category>
      <category>performance</category>
      <category>devops</category>
    </item>
    <item>
      <title>A 4B Model Just Beat 8B — We Tested 18 Small LLMs and the Results Are Wild</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Tue, 10 Mar 2026 14:59:27 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/a-4b-model-just-beat-8b-we-tested-18-small-llms-and-the-results-are-wild-npb</link>
      <guid>https://dev.to/wolfsea2357/a-4b-model-just-beat-8b-we-tested-18-small-llms-and-the-results-are-wild-npb</guid>
      <description>&lt;h2&gt;
  
  
  The "bigger is better" assumption is wrong.
&lt;/h2&gt;

&lt;p&gt;We spent weeks evaluating &lt;strong&gt;18 small language models&lt;/strong&gt; from &lt;strong&gt;12 different makers&lt;/strong&gt; on &lt;strong&gt;125 questions across 7 languages&lt;/strong&gt; — and the results seriously challenge conventional wisdom about model scaling.&lt;/p&gt;

&lt;p&gt;Here's what the data actually shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;4B model&lt;/strong&gt; outperforms an &lt;strong&gt;8B model&lt;/strong&gt; — using &lt;strong&gt;36% of the RAM&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;1.5GB MoE model&lt;/strong&gt; matches dense models that need &lt;strong&gt;8.5GB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;1.7B model&lt;/strong&gt; beats three separate &lt;strong&gt;7B–14B models&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;1.3B model&lt;/strong&gt; fabricates fake content &lt;strong&gt;80% of the time&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't theoretical predictions. These are measured results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F1.png" alt="Smol AI WorldCup Leaderboard" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Why we built yet another benchmark
&lt;/h2&gt;

&lt;p&gt;Here's the thing — &lt;strong&gt;MMLU, GPQA, and HumanEval weren't built for edge AI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They give the same test to a 0.5B model and a 500B model. That's fine if you only care about "how smart is it?" But if you're deploying on a &lt;strong&gt;phone&lt;/strong&gt;, a &lt;strong&gt;Raspberry Pi&lt;/strong&gt;, or an &lt;strong&gt;8GB laptop&lt;/strong&gt;, you need to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does it fit?&lt;/strong&gt; → How much RAM does it actually need?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it lie?&lt;/strong&gt; → How often does it fabricate fake information?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it fast enough?&lt;/strong&gt; → How many tokens per second?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it worth the cost?&lt;/strong&gt; → What's the performance per GB of RAM?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Existing benchmarks answer none of these. So we built one that answers all of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏟️ Introducing SHIFT — 5 axes, not 1
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;S&lt;/strong&gt;ize · &lt;strong&gt;H&lt;/strong&gt;onesty · &lt;strong&gt;I&lt;/strong&gt;ntelligence · &lt;strong&gt;F&lt;/strong&gt;ast · &lt;strong&gt;T&lt;/strong&gt;hrift&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How big is the model?&lt;/td&gt;
&lt;td&gt;Parameter count, active params for MoE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;H&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Does it resist hallucination?&lt;/td&gt;
&lt;td&gt;40 questions — traps, calibration, refusal, self-correction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;I&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How smart is it?&lt;/td&gt;
&lt;td&gt;85 questions — reasoning, math, coding, 7 languages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;F&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How fast does it run?&lt;/td&gt;
&lt;td&gt;tok/s measured via HF Inference API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How much resource does it need?&lt;/td&gt;
&lt;td&gt;Peak RAM at Q4 quantization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All 125 questions require &lt;strong&gt;JSON-structured output&lt;/strong&gt; with verifiable fields. No keyword matching. 75 questions are fully automatic — zero human grading needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F2.png" alt="SHIFT Framework" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 The ranking formula: WCS
&lt;/h2&gt;

&lt;p&gt;The tricky part — how do you rank models when you're measuring both &lt;em&gt;quality&lt;/em&gt; and &lt;em&gt;efficiency&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SHIFT alone?&lt;/strong&gt; Then 14B always beats 1.7B. Boring. Expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PIR (efficiency) alone?&lt;/strong&gt; Then a terrible 1.3B model becomes #1 because it's tiny. Misleading.&lt;/p&gt;

&lt;p&gt;Our solution: &lt;strong&gt;WorldCup Score (WCS)&lt;/strong&gt; — the geometric mean of both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WCS = √( SHIFT × PIR_norm )

Where:
  SHIFT    = H × 0.4 + I × 0.6       → quality
  PIR      = (I × H × F) ÷ (S × T)   → efficiency
  PIR_norm = log₁₀(PIR) / log₁₀(max) × 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Why geometric mean?&lt;/strong&gt; Because &lt;code&gt;√(A × B)&lt;/code&gt; requires &lt;em&gt;both&lt;/em&gt; to be high. Smart but huge? Low WCS. Tiny but dumb? Also low WCS. You need &lt;strong&gt;both quality and efficiency&lt;/strong&gt; to rank well.&lt;/p&gt;


&lt;h2&gt;
  
  
  🏆 The results
&lt;/h2&gt;

&lt;p&gt;Here are the top 5 — and they're not what you'd expect:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#  Model              WCS    SHIFT  RAM     League
🏆 GPT-OSS-20B       82.6   76.9   1.5GB   🥅 Raspberry Pi tier
🥈 Gemma-3n-E4B      81.8   77.3   2.0GB   ⚽ Smartphone tier
🥉 Llama-4-Scout     79.3   74.2   10GB    🏆 Desktop (but 240 tok/s!)
4  Qwen3-4B          76.6   76.8   2.8GB   ⚽ Smartphone tier
5  Qwen3-1.7B        76.1   66.8   1.2GB   🥅 IoT tier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;The WCS champion runs on a Raspberry Pi.&lt;/strong&gt; Let that sink in.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔬 5 findings that surprised us
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Finding 1: 4B = 8B (at 36% of the RAM)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gemma-3n-E4B  → SHIFT 77.3  (4B,  2.0GB)  ← #1 quality!
Qwen3-8B      → SHIFT 76.9  (8B,  5.5GB)
                              Gap: 0.4 points
                              RAM: 2.75× more
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Google's PLE architecture and Qwen3's training pipeline have made &lt;strong&gt;4B models functionally equivalent to 8B&lt;/strong&gt; on structured evaluation tasks. The extra 3.5GB of RAM buys you almost nothing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F3.png" alt="4B vs 8B comparison" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Finding 2: MoE is the cheat code for edge AI
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPT-OSS-20B   → 21B total, 3.6B active, 1.5GB RAM → SHIFT 76.9
Gemma-3-12B   → 12B total, 12B active,  8.5GB RAM → SHIFT 75.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same quality. &lt;strong&gt;5.7× less RAM.&lt;/strong&gt; MoE models activate only a fraction of their parameters at inference time, giving you big-model knowledge with small-model resources.&lt;/p&gt;
&lt;h3&gt;
  
  
  Finding 3: Thinking models have a dark side
&lt;/h3&gt;

&lt;p&gt;Models with &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; reasoning tokens (DeepSeek-R1, Nemotron-Nano) face a double penalty:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quality hit&lt;/strong&gt; — &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; tags break JSON structured output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Qwen3-8B (non-thinking)    → SHIFT 76.9
DeepSeek-R1-7B (thinking)  → SHIFT 68.2  (−8.7 points!)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Speed hit&lt;/strong&gt; — internal reasoning = 2–6× more tokens generated:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Qwen3-8B        → 186.8 tok/s
DeepSeek-R1-7B  →  69.2 tok/s  (2.7× slower)
Nemotron-Nano   →  29.8 tok/s  (6.3× slower)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Thinking helps for complex math (DeepSeek-R1-14B's reasoning score is the highest we measured), but for &lt;strong&gt;real-time structured tasks&lt;/strong&gt;, non-thinking models win.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F6.png" alt="Thinking model penalties" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Finding 4: The hallucination gap is enormous
&lt;/h3&gt;

&lt;p&gt;Our H1 test presents fake people, papers, and products. Models must refuse to fabricate.&lt;/p&gt;

&lt;p&gt;The score range? &lt;strong&gt;20 to 100.&lt;/strong&gt; That's an 80-point spread — the widest of any metric.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;H1 = 100: Qwen3-4B, Qwen3-8B, GPT-OSS-20B, GLM-4.7-Flash
H1 = 90:  Gemma-3n-E4B, Llama-4-Scout
H1 = 60:  Qwen3-1.7B, DeepSeek-R1-14B
H1 = 20:  Llama-3.2-1B  ← fabricates 80% of the time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The Qwen3 family is remarkably consistent at hallucination resistance across all sizes. Meanwhile, the smallest model (1.3B) will confidently tell you about a nonexistent professor's nonexistent research paper, complete with fake citations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F4.png" alt="Hallucination scores" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Finding 5: 1.7B beats 14B
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Qwen3-1.7B    (1.2GB)  → SHIFT 66.8
Mistral-7B    (5.0GB)  → SHIFT 60.6  ← 4.2× bigger, 6.2 points worse
Llama-3.1-8B  (5.5GB)  → SHIFT 61.0  ← 4.7× bigger, 5.8 points worse
DeepSeek-R1-14B (9.5GB) → SHIFT 59.8  ← 8.7× bigger, 7.0 points worse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Architecture generation matters more than parameter count. A 2025 model at 1.7B outperforms three 2024 models at 7–14B.&lt;/p&gt;


&lt;h2&gt;
  
  
  🏅 vs SOTA: How do small models compare to Claude and GPT-5?
&lt;/h2&gt;

&lt;p&gt;We give the same 19 questions to both our small models and frontier giants:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Sonnet 4.6  → 69.9  (ceiling)
Claude Opus 4.6    → 69.3
GPT-5.4            → 62.4
Qwen3.5-397B       → 57.1
────────────────────────────
Gemma-3-12B        → 57.1  (82% of Claude!)
GPT-OSS-20B        → 54.2  (78% of Claude)
Gemma-3n-E4B       → 47.4  (68% of Claude)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A 12B model matches a 397B model on identical questions. The gap between small and large is &lt;strong&gt;narrower than most people think&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚡ Speed: Provider matters more than model size
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Llama-4-Scout (Groq)       → 240.5 tok/s
Llama-3.1-8B (Cerebras)    → 187.7 tok/s
Qwen3-8B (Fireworks)       → 186.8 tok/s
...
Gemma-3-12B (Featherless)  →  18.7 tok/s
Mistral-7B (Featherless)   →  17.8 tok/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The fastest model is &lt;strong&gt;13× faster&lt;/strong&gt; than the slowest — and it's a bigger model. The difference? Groq's inference chip vs. generic GPU hosting. &lt;strong&gt;Infrastructure choice dominates model size in determining real-world speed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F5.png" alt="Speed rankings" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🗓️ Anti-contamination: Season system
&lt;/h2&gt;

&lt;p&gt;One concern with any public benchmark: models will eventually train on the questions.&lt;/p&gt;

&lt;p&gt;Our defense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;30 anchor questions&lt;/strong&gt; stay fixed across seasons (for IRT calibration)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;95 questions rotate&lt;/strong&gt; (70%+ replaced each season)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Union Eval questions are never published&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Season 2 planned for 2026 Q3&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🤝 Built with the community
&lt;/h2&gt;

&lt;p&gt;This benchmark was developed in collaboration with the &lt;strong&gt;&lt;a href="https://huggingface.co/FINAL-Bench" rel="noopener noreferrer"&gt;FINAL Bench&lt;/a&gt;&lt;/strong&gt; research team. The Union Eval cross-benchmark design draws on their evaluation methodology.&lt;/p&gt;

&lt;p&gt;It also integrates with the &lt;strong&gt;&lt;a href="https://huggingface.co/spaces/FINAL-Bench/all-bench-leaderboard" rel="noopener noreferrer"&gt;ALL Bench Leaderboard&lt;/a&gt;&lt;/strong&gt; — so you can see where your small model ranks among small models (Smol WorldCup) &lt;em&gt;and&lt;/em&gt; against the full landscape including GPT-5 and Claude (ALL Bench).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fginigen-ai%2Fsmol-worldcup%2Fresolve%2Fmain%2F7.png" alt="Recommendations" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The dataset is open under &lt;strong&gt;Apache 2.0&lt;/strong&gt;. We welcome new model submissions.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dataset&lt;/span&gt;

&lt;span class="n"&gt;ds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ginigen-ai/smol-worldcup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;train&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; questions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Filter by axis
&lt;/span&gt;&lt;span class="n"&gt;honesty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;train&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shift_axis&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;H&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Filter by language
&lt;/span&gt;&lt;span class="n"&gt;korean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;train&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;multilingual_ko&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;🏟️ &lt;a href="https://huggingface.co/spaces/ginigen-ai/smol-worldcup" rel="noopener noreferrer"&gt;Live Leaderboard&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;📊 &lt;a href="https://huggingface.co/datasets/ginigen-ai/smol-worldcup" rel="noopener noreferrer"&gt;Dataset on HuggingFace&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;🏅 &lt;a href="https://huggingface.co/spaces/FINAL-Bench/all-bench-leaderboard" rel="noopener noreferrer"&gt;ALL Bench Leaderboard&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;



&lt;p&gt;&lt;em&gt;Developed by &lt;a href="https://ginigen.ai" rel="noopener noreferrer"&gt;Ginigen.ai&lt;/a&gt; · Small but Mighty AI&lt;/em&gt;&lt;/p&gt;


&lt;div class="ltag__huggingface"&gt;
  &lt;iframe src="https://ginigen-ai-smol-worldcup.hf.space" title="Hugging Face Space" width="100%" height="600"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>opensource</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>MARL: Runtime Middleware That Reduces LLM Hallucination Without Fine-Tuning</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Mon, 09 Mar 2026 08:45:56 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/marl-runtime-middleware-that-reduces-llm-hallucination-without-fine-tuning-5fca</link>
      <guid>https://dev.to/wolfsea2357/marl-runtime-middleware-that-reduces-llm-hallucination-without-fine-tuning-5fca</guid>
      <description>&lt;p&gt;Your LLM is confidently wrong, and it can't stop itself.&lt;/p&gt;

&lt;p&gt;Ask GPT about a historical date, and it answers with full confidence — right or wrong. Ask Claude to analyze a contract, and it commits to its first interpretation without ever reconsidering. This is &lt;strong&gt;hallucination&lt;/strong&gt;, and in 2026, it remains the #1 blocker for production AI.&lt;/p&gt;

&lt;p&gt;The root cause is structural. LLMs are autoregressive: each token is conditioned on previous tokens. Once generation starts, the model cannot stop mid-stream and say &lt;em&gt;"wait, I was wrong."&lt;/em&gt; If the initial framing is flawed, it rides that trajectory to the end.&lt;/p&gt;

&lt;p&gt;We built &lt;strong&gt;MARL&lt;/strong&gt; to fix this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;marl-middleware
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0mao3nfm036rmtax66c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0mao3nfm036rmtax66c.png" alt=" " width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Data Says
&lt;/h2&gt;

&lt;p&gt;We released &lt;a href="https://huggingface.co/spaces/FINAL-Bench/Leaderboard" rel="noopener noreferrer"&gt;FINAL Bench&lt;/a&gt; — the world's first benchmark measuring AI &lt;strong&gt;metacognition&lt;/strong&gt; (the ability to know what you know and what you don't). We tested 9 SOTA models including GPT-5.2, Claude Opus 4.6, and Gemini 3 Pro across 1,800 assessments:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What It Measures&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;MA&lt;/strong&gt; (Metacognitive Accuracy)&lt;/td&gt;
&lt;td&gt;Can it say "I might be wrong"?&lt;/td&gt;
&lt;td&gt;0.694&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;ER&lt;/strong&gt; (Error Recovery)&lt;/td&gt;
&lt;td&gt;Can it actually find and fix errors?&lt;/td&gt;
&lt;td&gt;0.302&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The chasm between knowing and doing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.392&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AI models &lt;em&gt;sense&lt;/em&gt; they could be wrong. But they can't &lt;em&gt;fix&lt;/em&gt; what's broken. A 39.2 percentage-point gap between awareness and action.&lt;/p&gt;

&lt;h2&gt;
  
  
  How MARL Works
&lt;/h2&gt;

&lt;p&gt;MARL (Model-Agnostic Runtime Middleware for LLMs) decomposes a single LLM call into a 5-stage expert pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Query
    │
    ▼
S1: Hypothesis  → Designs the optimal approach
    │
    ▼
S2: Solver      → Performs deep reasoning
    │
    ▼
S3: Auditor     → Audits for gaps and contradictions
    │
    ▼
S4: Verifier    → Adversarial cross-validation
    │
    ▼
S5: Synthesizer → Integrates ALL feedback,
                   generates entirely new final response
    │
    ▼
Clean Answer (user sees only the refined result)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two mechanisms run simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cooperative Reinforcement&lt;/strong&gt; — knowledge compounds across S1→S2→S3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversarial Cross-Validation&lt;/strong&gt; — S4 deliberately attacks S2's conclusions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Synthesizer (S5) doesn't patch the original. It writes a &lt;strong&gt;completely new response&lt;/strong&gt; informed by every correction. This transforms "answer in one shot" into &lt;strong&gt;"think, doubt, correct, and rewrite."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In our FINAL Bench tests, this metacognitive scaffolding improved performance on the hardest tasks by &lt;strong&gt;over 70%&lt;/strong&gt;, with &lt;strong&gt;94.8% of the gain coming from error recovery&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not Fine-Tuning. Not RAG. A Third Way.
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Fine-Tuning&lt;/th&gt;
&lt;th&gt;RAG&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;MARL&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Changes&lt;/td&gt;
&lt;td&gt;Model weights&lt;/td&gt;
&lt;td&gt;External knowledge&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Reasoning structure&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$10K+ GPU&lt;/td&gt;
&lt;td&gt;Vector DB setup&lt;/td&gt;
&lt;td&gt;1 line of code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;Weeks&lt;/td&gt;
&lt;td&gt;Days&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lock-in&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixes&lt;/td&gt;
&lt;td&gt;Domain gaps&lt;/td&gt;
&lt;td&gt;Knowledge gaps&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Reasoning errors&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MARL never touches weights. Switch from GPT-5.4 to Claude to Llama — the MARL layer stays. No vendor lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration: One Line
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="c1"&gt;# Just change base_url. That's it.
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# ← MARL server
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Everything else stays exactly the same
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your prompt here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every call now flows through the 5-stage pipeline automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk5k1g5a1ucafe2keqxyp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk5k1g5a1ucafe2keqxyp.png" alt=" " width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9 Domain-Specific Emergence Engines
&lt;/h2&gt;

&lt;p&gt;Beyond default reasoning enhancement, MARL ships with 9 specialized engines — activated by appending &lt;code&gt;::mode&lt;/code&gt; to the model name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4::pharma&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# 💊 Drug discovery (172 items)
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4::invent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# 🔬 Invention &amp;amp; patents (4,275 items)
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4::genomics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# 🧬 Genomics &amp;amp; bio (104 items)
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4::chemistry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# 🧪 Chemistry &amp;amp; materials (135 items)
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4::ecology&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# 🌍 Ecology &amp;amp; environment (105 items)
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4::law&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;        &lt;span class="c1"&gt;# ⚖️ Legal &amp;amp; regulatory (59 items)
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4::create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# 🎨 General creative (493 seeds)
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4::doc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;        &lt;span class="c1"&gt;# 📝 Document generation
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4::recipe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# 🍳 Culinary fusion
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5,538 expert data items cross-combined across multiple layers. Each engine has 5 emergence rules and 10 cross-layer bonus pairs. Works with &lt;strong&gt;any LLM model name&lt;/strong&gt; — not just OpenAI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Core: Protected Engine, Transparent Reasoning
&lt;/h2&gt;

&lt;p&gt;The core engine (pipeline logic, attention matrix, agent prompts) ships as a &lt;strong&gt;compiled binary&lt;/strong&gt; — proprietary tech stays protected.&lt;/p&gt;

&lt;p&gt;Everything else is open: installation, API integration, A/B test demos, and most importantly — &lt;strong&gt;the full reasoning trace&lt;/strong&gt;. Every stage is logged transparently. You can see exactly where an error was caught and how it was corrected.&lt;/p&gt;

&lt;p&gt;If LLMs are black boxes, MARL is a &lt;strong&gt;glass box&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Available Everywhere
&lt;/h2&gt;

&lt;p&gt;We shipped MARL simultaneously across four platforms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# PyPI&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;marl-middleware

&lt;span class="c"&gt;# Docker&lt;/span&gt;
docker pull vidraft/marl:latest

&lt;span class="c"&gt;# ClawHub (OpenClaw — 260K+ developers, 3,200+ AI skills)&lt;/span&gt;
clawhub &lt;span class="nb"&gt;install &lt;/span&gt;marl-middleware

&lt;span class="c"&gt;# GitHub&lt;/span&gt;
git clone https://github.com/Vidraft/MARL.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On &lt;strong&gt;ClawHub&lt;/strong&gt;, MARL is the first middleware in the &lt;strong&gt;Reasoning Enhancement&lt;/strong&gt; category. One command gives your AI agent a metacognition upgrade — it thinks before it acts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2e7c2dr4ik5ljbtqg71x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2e7c2dr4ik5ljbtqg71x.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;📝 Technical deep dive&lt;/strong&gt;: &lt;a href="https://huggingface.co/blog/FINAL-Bench/marl-middleware" rel="noopener noreferrer"&gt;huggingface.co/blog/FINAL-Bench/marl-middleware&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🤗 Live A/B test&lt;/strong&gt; (Raw LLM vs MARL): &lt;a href="https://huggingface.co/spaces/VIDraft/MARL" rel="noopener noreferrer"&gt;huggingface.co/spaces/VIDraft/MARL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📦 PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/marl-middleware/" rel="noopener noreferrer"&gt;pypi.org/project/marl-middleware&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🐙 GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Vidraft/MARL" rel="noopener noreferrer"&gt;github.com/Vidraft/MARL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🦀 ClawHub&lt;/strong&gt;: &lt;a href="https://clawhub.ai/Cutechicken99/marl-middleware" rel="noopener noreferrer"&gt;clawhub.ai/Cutechicken99/marl-middleware&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://vidraft.net" rel="noopener noreferrer"&gt;VIDRAFT&lt;/a&gt; — the team behind FINAL Bench (HF Dataset Global #5), FACTS Grounding Medical AI World #2 (CNRS-verified), and HuggingFace STAR AI TOP 12 (2024). 2M monthly active users, 1,500+ public AI models.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
