<?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>Day-0 Model Support Is a Fork: What 26 NVIDIA Dynamo Snapshot Builds Say About Pinning Your LLM Serving Stack</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Mon, 09 Nov 2026 14:05:46 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/day-0-model-support-is-a-fork-what-26-nvidia-dynamo-snapshot-builds-say-about-pinning-your-llm-1mb7</link>
      <guid>https://dev.to/wolfsea2357/day-0-model-support-is-a-fork-what-26-nvidia-dynamo-snapshot-builds-say-about-pinning-your-llm-1mb7</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;NVIDIA Dynamo ships two kinds of releases: stable versions (v1.6.2 on 2026-11-05) and short-lived model snapshot builds such as &lt;code&gt;v1.7.0-kimi-k3.1-pro-dev.1&lt;/code&gt; (2026-11-06). I pulled every Dynamo release from the GitHub API, the engine release dates from PyPI, and classified what each snapshot build sits on. Everything below is reproducible with the script in this post.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable pins are never current.&lt;/strong&gt; Across 12 stable releases, the pinned vLLM was a median 30 days old on release day (range 17 to 56), and every single one had at least one newer vLLM minor already on PyPI. SGLang looked the same: 11 of 12 had a newer release available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshots outnumber stables 2.6 to 1.&lt;/strong&gt; Between 2026-07-17 and 2026-11-06 there were 10 stable releases and 26 model snapshot builds. All 26 carry a notice that they are not for production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most snapshots are not on a plain engine release.&lt;/strong&gt; Only 10 of 26 build on an unmodified released vLLM, SGLang or TensorRT-LLM tag. The other 16 use a patched release (3), a nightly or raw commit (5), or a model-specific image or fork (8).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fleet-level risk is documented, not hypothetical.&lt;/strong&gt; One build states that its patched NIXL transfer protocol 14 is not compatible with unpatched protocol 13 workers. Another requires matching KV block size and speculative decoding settings on prefill and decode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What I did not do.&lt;/strong&gt; I ran no GPU workload and deployed none of these images. This is a study of release metadata, with my own classification, and the snapshot builds are explicitly labeled as early access.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What exactly did I measure, and where does the data come from?
&lt;/h2&gt;

&lt;p&gt;Three sources, all public. The Dynamo release list and notes come from the GitHub releases API for &lt;code&gt;ai-dynamo/dynamo&lt;/code&gt; (73 entries as of 2026-11-09). Engine release dates come from PyPI upload timestamps for &lt;code&gt;vllm&lt;/code&gt; and &lt;code&gt;sglang&lt;/code&gt;. The snapshot classification comes from the header lines of each build's release notes, the ones that start with &lt;code&gt;**vLLM**:&lt;/code&gt;, &lt;code&gt;**SGLang**:&lt;/code&gt; or &lt;code&gt;**TensorRT-LLM**:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two definitions matter. A stable release is any non-prerelease entry. A snapshot is any prerelease entry whose tag names a model, for example &lt;code&gt;v1.6.0-deepseek-v4.1-flash-dev.2-post.1&lt;/code&gt;. The snapshot window is the 16 weeks from 2026-07-17 to 2026-11-06, the newest release on the day I ran this. Earlier builds often omit the engine header lines, so I left them out. The stable pin analysis uses every stable release whose notes include a pin table, 12 of them since 2026-04-23.&lt;/p&gt;

&lt;p&gt;For stable releases, the notes include a "Key Dependencies" table listing the pinned SGLang, TensorRT-LLM and vLLM versions. I parse that table and compare the pinned engine's PyPI upload date with the Dynamo release timestamp.&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;json&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;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&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;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&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;headers&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;User-Agent&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;audit/1.0&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;rels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/repos/ai-dynamo/dynamo/releases?per_page=100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;UTC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;
&lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&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;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&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;pypi_dates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;):&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;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="ow"&gt;in&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="s"&gt;https://pypi.org/pypi/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/json&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;releases&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="ow"&gt;and&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;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&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;v&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;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upload_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UTC&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;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&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="n"&gt;vllm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pypi_dates&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0\.\d+\.\d+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sglang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pypi_dates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sglang&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0\.5\.\d+(\.post\d+)?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;third&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&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;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# 1. How stale is the engine a Dynamo stable release pins, on release day?
&lt;/span&gt;&lt;span class="n"&gt;ages&lt;/span&gt; &lt;span class="o"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stable  vLLM pin  age  newer | SGLang pin  age  newer&lt;/span&gt;&lt;span class="sh"&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&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;rels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&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;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prerelease&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;| :---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&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="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;row&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;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\| \*\*v([\d.]+)\*\* \| `v?([^`]+)` \| `v?([^`]+)` \| `v?([^`]+)`&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&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;row&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;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groups&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="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;vl_newer&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;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&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="mi"&gt;1&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;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vllm&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="k"&gt;if&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;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;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;vl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;sg_newer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sglang&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="k"&gt;if&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="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;third&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;third&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ages&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;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;vllm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;vl&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;days&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;v&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6&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;vl&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&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="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;vllm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;vl&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;d &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;vl_newer&lt;/span&gt;&lt;span class="si"&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;   | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sg&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;13&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="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;sglang&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sg&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;d &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sg_newer&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&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;span class="n"&gt;ages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; stable releases, vLLM pin age: min &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ages&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;d, median &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ages&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;ages&lt;/span&gt;&lt;span class="p"&gt;)&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ages&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;ages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;d, max &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ages&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. What do the model-specific snapshot builds sit on?
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;line&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;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,? ?NIXL.*&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="p"&gt;,&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;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ghcr.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model-specific&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nightly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&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;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build `[0-9a-f]{7,}`&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nightly or commit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backport&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;released + patches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&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;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(vllm-openai|sglang):v?\d|`v?0\.\d+\.\d+|`1\.\d+\.\d+rc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;released&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model-specific&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;RANK&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-specific&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;nightly or commit&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;released + patches&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;released&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;snaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rels&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;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prerelease&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;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-17&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;lines&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;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^\*\*(?:vLLM|SGLang|TensorRT-LLM)\*\*:(.*)$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&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="sh"&gt;""&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;kinds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&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;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&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;kinds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;snaps&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="n"&gt;kinds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;snapshots since 2026-07-17:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&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;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RANK&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;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;20&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;snaps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&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;Output when I ran it on 2026-11-09:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stable  vLLM pin  age  newer | SGLang pin  age  newer
v1.0.2  0.16.0     56d   3   | 0.5.9           58d   1
v1.1.0  0.19.0     31d   1   | 0.5.10.post1    25d   0
v1.2.0  0.20.1     29d   2   | 0.5.11          27d   1
v1.3.0  0.23.0     38d   2   | 0.5.14          26d   1
v1.4.0  0.26.0     21d   1   | 0.5.16          21d   1
v1.4.1  0.26.0     28d   1   | 0.5.16          28d   2
v1.4.2  0.26.0     34d   2   | 0.5.16          35d   2
v1.5.0  0.28.0     26d   1   | 0.5.18          30d   2
v1.5.1  0.28.0     41d   3   | 0.5.18          46d   3
v1.6.0  0.31.0     17d   1   | 0.5.21          21d   1
v1.6.1  0.31.0     22d   1   | 0.5.21          26d   1
v1.6.2  0.31.0     31d   2   | 0.5.21          35d   2
12 stable releases, vLLM pin age: min 17d, median 30d, max 56d

snapshots since 2026-07-17: 26
  model-specific       8
  nightly or commit    5
  released + patches   3
  released             10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "newer" column counts distinct vLLM minor versions (or SGLang &lt;code&gt;0.5.N&lt;/code&gt; releases) newer than the pin that were already on PyPI at the moment Dynamo shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  How stale is the engine inside a Dynamo stable release?
&lt;/h2&gt;

&lt;p&gt;Always somewhat, and it does not shrink. The vLLM pin was 17 days old at the freshest (v1.6.0) and 56 days old at the stalest (v1.0.2). The median across 12 releases is 30 days. The three most recent releases sit at 17, 22 and 31 days, but all three pin the same vLLM 0.31.0, so that growth is just the clock running between a minor release and its patches.&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522v1.0.2%2522%252C%2522v1.1.0%2522%252C%2522v1.2.0%2522%252C%2522v1.3.0%2522%252C%2522v1.4.0%2522%252C%2522v1.4.1%2522%252C%2522v1.4.2%2522%252C%2522v1.5.0%2522%252C%2522v1.5.1%2522%252C%2522v1.6.0%2522%252C%2522v1.6.1%2522%252C%2522v1.6.2%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522vLLM%2520pin%2520age%2520%2528days%2529%2522%252C%2522data%2522%253A%255B56%252C31%252C29%252C38%252C21%252C28%252C34%252C26%252C41%252C17%252C22%252C31%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522SGLang%2520pin%2520age%2520%2528days%2529%2522%252C%2522data%2522%253A%255B58%252C25%252C27%252C26%252C21%252C28%252C35%252C30%252C46%252C21%252C26%252C35%255D%252C%2522backgroundColor%2522%253A%2522%252364748b%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522legend%2522%253A%257B%2522display%2522%253Atrue%257D%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Age%2520of%2520the%2520pinned%2520engine%2520on%2520the%2520day%2520each%2520Dynamo%2520stable%2520shipped%2522%252C%2522fontSize%2522%253A16%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%2522days%2520between%2520engine%2520release%2520and%2520Dynamo%2520stable%2520release%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522v1.0.2%2522%252C%2522v1.1.0%2522%252C%2522v1.2.0%2522%252C%2522v1.3.0%2522%252C%2522v1.4.0%2522%252C%2522v1.4.1%2522%252C%2522v1.4.2%2522%252C%2522v1.5.0%2522%252C%2522v1.5.1%2522%252C%2522v1.6.0%2522%252C%2522v1.6.1%2522%252C%2522v1.6.2%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522vLLM%2520pin%2520age%2520%2528days%2529%2522%252C%2522data%2522%253A%255B56%252C31%252C29%252C38%252C21%252C28%252C34%252C26%252C41%252C17%252C22%252C31%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522SGLang%2520pin%2520age%2520%2528days%2529%2522%252C%2522data%2522%253A%255B58%252C25%252C27%252C26%252C21%252C28%252C35%252C30%252C46%252C21%252C26%252C35%255D%252C%2522backgroundColor%2522%253A%2522%252364748b%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522legend%2522%253A%257B%2522display%2522%253Atrue%257D%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Age%2520of%2520the%2520pinned%2520engine%2520on%2520the%2520day%2520each%2520Dynamo%2520stable%2520shipped%2522%252C%2522fontSize%2522%253A16%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%2522days%2520between%2520engine%2520release%2520and%2520Dynamo%2520stable%2520release%2522%257D%257D%255D%257D%257D%257D" alt="Age of the pinned engine on the day each Dynamo stable shipped" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Take the newest stable as a concrete case. Dynamo v1.6.2 (2026-11-05) pins vLLM v0.31.0, SGLang v0.5.21 and TensorRT-LLM v1.3.0rc29. On that day PyPI already had vLLM 0.32.0 (2026-10-18) and 0.33.0 (2026-11-02), plus SGLang 0.5.22 and 0.5.23. TensorRT-LLM had already started its 1.4.0 release candidate line on 2026-10-22. None of that is a criticism. A stable release that bundles three engines, NIXL, UCX and Kubernetes recipes has to freeze something, and v1.6.2 says plainly that it is a patch release refreshing OS and Python packages.&lt;/p&gt;

&lt;p&gt;The point is what the gap does to anyone who needs a model that shipped after the freeze.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a stable pin turn into a stream of model snapshots?
&lt;/h2&gt;

&lt;p&gt;My reading, and I flag it as interpretation rather than something the notes state: a brand new model usually needs engine changes (a kernel, a tool-call parser, a tokenizer path, a cache layout) that the pinned engine does not have yet. Waiting for the next stable means waiting out the pin lag above. So the project cuts a branch, bases it on whatever engine build already runs that model, and publishes it under a tag that carries the model name.&lt;/p&gt;

&lt;p&gt;The volume shows how often this happens. In the 16 weeks I looked at, snapshots appeared in every month, and in September and October they outnumbered stable releases by a wide margin.&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Jul%252017-31%2522%252C%2522Aug%2522%252C%2522Sep%2522%252C%2522Oct%2522%252C%2522Nov%25201-6%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Stable%2520releases%2522%252C%2522data%2522%253A%255B1%252C4%252C1%252C3%252C1%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522Model%2520snapshot%2520builds%2522%252C%2522data%2522%253A%255B3%252C2%252C8%252C10%252C3%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Dynamo%2520GitHub%2520releases%2520per%2520month%253A%2520stable%2520vs%2520model%2520snapshot%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522stepSize%2522%253A2%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522releases%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Jul%252017-31%2522%252C%2522Aug%2522%252C%2522Sep%2522%252C%2522Oct%2522%252C%2522Nov%25201-6%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Stable%2520releases%2522%252C%2522data%2522%253A%255B1%252C4%252C1%252C3%252C1%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522Model%2520snapshot%2520builds%2522%252C%2522data%2522%253A%255B3%252C2%252C8%252C10%252C3%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Dynamo%2520GitHub%2520releases%2520per%2520month%253A%2520stable%2520vs%2520model%2520snapshot%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522stepSize%2522%253A2%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522releases%2522%257D%257D%255D%257D%257D%257D" alt="Dynamo GitHub releases per month: stable vs model snapshot" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every one of the 26 snapshot builds in the window carries a notice that it is not for production, and the ones I read in full also say it is not QA-gated. So this is not a story about a vendor shipping broken things. It is a story about where the bleeding edge of model support actually lives: on branches that are labeled as early access, built on engine bases that your normal upgrade process has never seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do those snapshot builds actually sit on?
&lt;/h2&gt;

&lt;p&gt;I classified each build by the least conventional engine line in its header, using this precedence: model-specific image or fork, then nightly or raw commit, then released tag with patches, then plain released tag. The classifier in the script above is a few regular expressions, and its totals matched my hand labels exactly (10, 3, 5 and 8).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Base under the snapshot&lt;/th&gt;
&lt;th&gt;Builds&lt;/th&gt;
&lt;th&gt;Examples from the notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain released engine tag&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v1.6.0-beam-1.0-501b-dev.1&lt;/code&gt; on vLLM v0.31.0 and SGLang v0.5.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Released tag plus patches&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v1.4.1-a.x-k2-post.1&lt;/code&gt; on vLLM 0.26.0 with four patches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly or raw commit&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v1.7.0-mistral-large-4-dev.1&lt;/code&gt; on a &lt;code&gt;cu134-nightly&lt;/code&gt; vLLM image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model-specific image or fork&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v1.7.0-kimi-k3.1-pro-dev.1&lt;/code&gt; on &lt;code&gt;vllm/vllm-openai:kimi-k31&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sixteen of 26 is 62 percent. Two caveats keep this honest. First, the labels come from header lines that different release authors fill in with different levels of detail, for example "MiniMax M3.1 Flash build" with no version at all, which I counted as model-specific. Second, a "plain released tag" build can still carry Dynamo-side patches in its runtime layer, which the header does not always say. Treat 62 percent as a rough measure of how often the base is non-standard, not an exact one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the same model keep changing its base?
&lt;/h2&gt;

&lt;p&gt;Yes, and the sequence is instructive. DeepSeek-V4.1-Flash has three Dynamo builds in the window:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Engine base named in the notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v1.6.0-deepseek-v4.1-flash-dev.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-09-12&lt;/td&gt;
&lt;td&gt;SGLang &lt;code&gt;dev-dsv41&lt;/code&gt; image, NIXL v1.4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v1.6.0-deepseek-v4.1-flash-dev.2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-10-08&lt;/td&gt;
&lt;td&gt;vLLM v0.31.0 image, NIXL v1.3.2 (connector protocol 14)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v1.6.0-deepseek-v4.1-flash-dev.2-post.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-11-05&lt;/td&gt;
&lt;td&gt;vLLM nightly &lt;code&gt;ac9126e...&lt;/code&gt; dated 2026-10-01, NIXL v1.3.2 (protocol 14)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third build's notes say it returns to a pinned nightly instead of the v0.31.0 base used by dev.2. In other words, the project moved a model from a released engine to a nightly. The notes I read do not say why it moved back to a nightly. They list a fix so prefill health probes stop leaking KV leases and refreshed recipes, so I will not guess beyond that. The practical lesson is that "which engine version is this model on" can change between two builds with nearly identical names.&lt;/p&gt;

&lt;p&gt;Kimi-K3 shows the opposite direction. Its first build (2026-07-27) used the model-specific image &lt;code&gt;vllm/vllm-openai:kimi-k3&lt;/code&gt;. By 2026-09-09, 44 days later, the build used &lt;code&gt;v0.28.0-ubuntu2404&lt;/code&gt;, a normal released tag, with SGLang on &lt;code&gt;v0.5.17&lt;/code&gt;. Model-specific images look like bridges that later converge onto released engines, at least for this model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does this bite a multi-node deployment?
&lt;/h2&gt;

&lt;p&gt;At the seams between workers. Disaggregated serving moves KV cache from a prefill worker to a decode worker, so both ends must agree on the details. The release notes name several such constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The DeepSeek post.1 notes: the patched NIXL transfer protocol (14) is not compatible with unpatched protocol 13 workers, and all workers in a KV transfer must use that runtime.&lt;/li&gt;
&lt;li&gt;The Mistral Large 4 notes: disaggregated recipes require matching block size (64) and speculative decoding settings on prefill and decode, and the Vera Rubin recipes store KV in NVFP4, which changes numerics, so re-run an accuracy check after any model or image change.&lt;/li&gt;
&lt;li&gt;The Kimi-K3.1-Pro notes: this build relies on NIXL v1.5.0, which the notes say is required for Kimi strided descriptors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NIXL itself is a moving part. Of the 26 snapshot builds, 20 name a NIXL version, and they name six different ones.&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522v1.3.0%2522%252C%2522v1.3.1%2522%252C%2522v1.3.2%2522%252C%2522v1.4.0%2522%252C%2522v1.4.1%2522%252C%2522v1.5.0%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522snapshot%2520builds%2520naming%2520this%2520NIXL%2520version%2522%252C%2522data%2522%253A%255B3%252C5%252C11%252C1%252C1%252C3%255D%252C%2522backgroundColor%2522%253A%2522%25237c3aed%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522NIXL%2520versions%2520named%2520by%252020%2520snapshot%2520builds%2520%25286%2520distinct%2520versions%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522stepSize%2522%253A2%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522snapshot%2520builds%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522v1.3.0%2522%252C%2522v1.3.1%2522%252C%2522v1.3.2%2522%252C%2522v1.4.0%2522%252C%2522v1.4.1%2522%252C%2522v1.5.0%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522snapshot%2520builds%2520naming%2520this%2520NIXL%2520version%2522%252C%2522data%2522%253A%255B3%252C5%252C11%252C1%252C1%252C3%255D%252C%2522backgroundColor%2522%253A%2522%25237c3aed%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522NIXL%2520versions%2520named%2520by%252020%2520snapshot%2520builds%2520%25286%2520distinct%2520versions%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522stepSize%2522%253A2%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522snapshot%2520builds%2522%257D%257D%255D%257D%257D%257D" alt="NIXL versions named by 20 snapshot builds" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even the stable v1.6.2 mixes NIXL versions by backend: v1.5.0 for the frontend and SGLang image, v1.3.1 for TensorRT-LLM and v1.4.1 for vLLM. That is a normal packaging decision when each backend image is tested on its own. It becomes a trap only if you assume one "Dynamo version" implies one transfer stack across every worker you run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you keep a mixed fleet from silently breaking?
&lt;/h2&gt;

&lt;p&gt;Five habits, none of which depend on Dynamo specifically.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin by digest, record the digest.&lt;/strong&gt; Most snapshot notes give a branch tip and a base image, and several pin a sha256 digest. Copy that into your deployment record. A tag like &lt;code&gt;kimi-k31&lt;/code&gt; can move; a digest cannot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat prefill and decode as one unit.&lt;/strong&gt; If you upgrade one role, upgrade the group. Never leave a rollout half done across a KV transfer boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate on the fields the notes call out.&lt;/strong&gt; Image digest, KV block size, speculative decoding config and transfer protocol version are the ones named above. Compare them across every worker in a group before traffic is allowed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-run accuracy after any image change that touches numerics.&lt;/strong&gt; The Mistral notes say it directly for NVFP4 KV. Throughput numbers from a recipe do not tell you whether outputs moved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep experimental builds off the default path.&lt;/strong&gt; Route them through a separate pool or namespace so a snapshot cannot become your production baseline by accident.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a small gate for item 3. The manifest is illustrative, written for this post, not taken from any real deployment. One group has a deliberate mismatch.&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;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="n"&gt;MUST_MATCH&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;image_digest&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;kv_block_size&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;spec_decode&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;nixl_protocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;fleet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;  &lt;span class="c1"&gt;# illustrative manifest, not a real deployment
&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;p0&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;group&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;g1&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;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;prefill&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;image_digest&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;sha256:aa11&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;kv_block_size&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spec_decode&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;off&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;nixl_protocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&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;name&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;d0&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;group&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;g1&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;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;decode&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;image_digest&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;sha256:aa11&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;kv_block_size&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spec_decode&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;off&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;nixl_protocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&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;name&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;p1&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;group&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;g2&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;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;prefill&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;image_digest&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;sha256:aa11&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;kv_block_size&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spec_decode&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;off&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;nixl_protocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&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;name&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;d1&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;group&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;g2&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;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;decode&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;image_digest&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;sha256:bb22&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;kv_block_size&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spec_decode&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;off&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;nixl_protocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&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;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fleet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&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;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt; &lt;span class="ow"&gt;in&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;groups&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MUST_MATCH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&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;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&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;FAIL group=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; field=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field&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;values&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&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;bad&lt;/span&gt; &lt;span class="k"&gt;else&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;bad&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; mismatches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAIL group=g2 field=image_digest: {'p1': 'sha256:aa11', 'd1': 'sha256:bb22'}
FAIL group=g2 field=kv_block_size: {'p1': 64, 'd1': 32}
FAIL group=g2 field=nixl_protocol: {'p1': 14, 'd1': 13}
3 mismatches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits with status 1 on any mismatch, so it drops straight into a CI step or an admission check. Extend &lt;code&gt;MUST_MATCH&lt;/code&gt; with whatever your engine's notes call out for your model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the limits of this analysis?
&lt;/h2&gt;

&lt;p&gt;It reads release notes, not behavior. I did not boot any of these images, so I cannot say whether a snapshot is faster, slower or more fragile than the stable build, and nothing here compares their performance. The classification depends on how consistently release authors fill in the header lines. The age calculation uses PyPI upload times, which can differ by hours from GitHub tag times. Finally, the window is 16 weeks long, so a different window could shift the percentages.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is it safe to run a Dynamo model snapshot build in production?&lt;/strong&gt;&lt;br&gt;
Every one of the 26 builds I checked carries a notice that it is not for production, and the notes I read in full call them experimental and not QA-gated. Use them for evaluation and early feedback, and keep them off your default serving path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does Dynamo ship model-specific builds at all?&lt;/strong&gt;&lt;br&gt;
The notes do not give a general reason. The pattern in the data fits a simple explanation: new models need engine changes sooner than a stable release can absorb them, so a branch carries the needed base until the change lands in a released engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How old is the vLLM version pinned in a Dynamo stable release?&lt;/strong&gt;&lt;br&gt;
Across the 12 stable releases with a pin table, 17 to 56 days old on release day, median 30. Dynamo v1.6.2 pins vLLM 0.31.0, while 0.32.0 and 0.33.0 were already on PyPI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I mix workers from different Dynamo builds in one disaggregated deployment?&lt;/strong&gt;&lt;br&gt;
Not safely without checking. The DeepSeek-V4.1-Flash post.1 notes say workers on the patched NIXL protocol 14 are not compatible with unpatched protocol 13 workers and that all workers in a KV transfer must use the same runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I reproduce the numbers?&lt;/strong&gt;&lt;br&gt;
Run the script in this post. It needs only the Python standard library, one GitHub API call for the release list and two PyPI JSON calls. Unauthenticated GitHub access is enough for one run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should I record when I deploy a snapshot build?&lt;/strong&gt;&lt;br&gt;
The release tag, branch tip commit, base image digest per backend, NIXL version and transfer protocol, KV block size, and speculative decoding settings. Those are the fields the notes treat as compatibility-relevant.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/vllm-033-ships-an-abi-stable-wheel-but-pypi-still-pins-torch2140-what-you-can-actually-swap-1nel"&gt;vLLM 0.33 Ships an ABI-Stable Wheel, but PyPI Still Pins torch==2.14.0: What You Can Actually Swap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/your-llm-serving-engine-ships-every-13-days-reading-vllms-breaking-change-lists-as-upgrade-risk-1815"&gt;Your LLM Serving Engine Ships Every 13 Days: Reading vLLM's Breaking-Change Lists as Upgrade Risk Data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ai-dynamo/dynamo/releases" rel="noopener noreferrer"&gt;NVIDIA Dynamo releases (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ai-dynamo/dynamo/releases/tag/v1.6.2" rel="noopener noreferrer"&gt;Dynamo v1.6.2 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ai-dynamo/dynamo/releases/tag/v1.7.0-kimi-k3.1-pro-dev.1" rel="noopener noreferrer"&gt;Dynamo v1.7.0-kimi-k3.1-pro-dev.1 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ai-dynamo/dynamo/releases/tag/v1.6.0-deepseek-v4.1-flash-dev.2-post.1" rel="noopener noreferrer"&gt;Dynamo v1.6.0-deepseek-v4.1-flash-dev.2-post.1 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ai-dynamo/dynamo/releases/tag/v1.7.0-mistral-large-4-dev.1" rel="noopener noreferrer"&gt;Dynamo v1.7.0-mistral-large-4-dev.1 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/vllm/" rel="noopener noreferrer"&gt;vllm on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/sglang/" rel="noopener noreferrer"&gt;sglang on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>llm</category>
      <category>vllm</category>
    </item>
    <item>
      <title>vLLM 0.33 Ships an ABI-Stable Wheel, but PyPI Still Pins torch==2.14.0: What You Can Actually Swap</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Sun, 08 Nov 2026 14:15:19 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/vllm-033-ships-an-abi-stable-wheel-but-pypi-still-pins-torch2140-what-you-can-actually-swap-1nel</link>
      <guid>https://dev.to/wolfsea2357/vllm-033-ships-an-abi-stable-wheel-but-pypi-still-pins-torch2140-what-you-can-actually-swap-1nel</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;On November 6 the vLLM team, with the Red Hat and Meta PyTorch teams, published how vLLM's CUDA build moved to the LibTorch Stable ABI. Since v0.33.0 (PyPI upload 2026-11-02), the CUDA wheel is meant to keep working when you replace the &lt;code&gt;torch&lt;/code&gt; wheel with a newer compatible one. I checked what that means in practice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I reproduced the audit.&lt;/strong&gt; I downloaded the 336 MB x86_64 wheel from PyPI and ran &lt;code&gt;torch-abi-audit&lt;/code&gt; 0.4.0 on it. Result: &lt;code&gt;Torch ABI: STABLE&lt;/code&gt;, &lt;code&gt;Min torch: 2.13.0&lt;/code&gt;, 13 extensions, 0 unstable symbols. It matches the blog post's output line for line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One extension sets the floor.&lt;/strong&gt; Three extensions need torch 2.10 or newer, six need 2.11, and one (QuTLASS) needs 2.13. The wheel's floor is the highest of those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The package metadata did not follow.&lt;/strong&gt; vLLM 0.33.0 on PyPI still declares &lt;code&gt;torch==2.14.0&lt;/code&gt; and &lt;code&gt;torchvision==0.29.0&lt;/code&gt;. A binary that can run on newer torch is not the same as a package that lets pip install it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The old rebuild tax was real.&lt;/strong&gt; Across four torch releases, vLLM took 33, 34, 44 and 60 days to ship the first release that pins each one. torch 2.12.0 was never pinned at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What I did not do.&lt;/strong&gt; I ran no GPU test and did not swap torch under a running server. The blog's own demo is one 135M model generating 8 tokens in eager mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What did vLLM actually change in 0.33?
&lt;/h2&gt;

&lt;p&gt;vLLM's C++ and CUDA code calls into LibTorch constantly. Before this work, a change to LibTorch's binary interface could force a vLLM rebuild even when the Python-visible API was identical. The post describes the fix as three layers of PyTorch's stable ABI: a stable C shim exposing selected LibTorch functions, C++ wrappers over that shim, and header-only utilities in &lt;code&gt;torch::headeronly&lt;/code&gt; that do not link against &lt;code&gt;libtorch.so&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The scale of the migration, from the post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Over 175 &lt;code&gt;.cpp&lt;/code&gt; and &lt;code&gt;.cu&lt;/code&gt; files plus 120 or more headers in the CUDA build.&lt;/li&gt;
&lt;li&gt;Seven compiled dependencies that also use LibTorch: FlashMLA, DeepGEMM, FlashAttention-2, FlashAttention-3, DeepSelect, FlashKDA and QuTLASS. Several were migrated on vLLM's own forks first, with upstream pull requests still open for some.&lt;/li&gt;
&lt;li&gt;A timeline from an RFC on 2025-10-15 and a first migration PR on 2025-12-29, through in-tree code becoming stable in June 2026, to DeepGEMM on 2026-10-15, which was the last piece.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two mechanics are worth copying if you ship your own CUDA ops. The &lt;code&gt;TORCH_TARGET_VERSION&lt;/code&gt; build flag makes compilation fail if any unstable LibTorch header sneaks in, and ops are registered with &lt;code&gt;STABLE_TORCH_LIBRARY&lt;/code&gt; with the implementation wrapped in &lt;code&gt;TORCH_BOX&lt;/code&gt;, which boxes and unboxes arguments into &lt;code&gt;StableIValue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The post is also open about the limits. The stable surface is a subset of LibTorch: there is no stable &lt;code&gt;SymInt&lt;/code&gt;, so meta functions had to be Python, and no autograd machinery, so backward functions must be Python too. A method as ordinary as &lt;code&gt;is_pinned()&lt;/code&gt; had no stable equivalent during the migration, so vLLM calls &lt;code&gt;aten::is_pinned&lt;/code&gt; through the generic &lt;code&gt;torch_call_dispatcher&lt;/code&gt; shim. PyTorch added a stable &lt;code&gt;is_pinned()&lt;/code&gt; in 2.14.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the audit say about the wheel you can download?
&lt;/h2&gt;

&lt;p&gt;The blog shows &lt;code&gt;torch-abi-audit&lt;/code&gt; output, but the question I care about is whether the artifact on PyPI matches it. I unpacked &lt;code&gt;vllm-0.33.0-cp38-abi3-manylinux_2_28_x86_64.whl&lt;/code&gt; and audited the 13 extension modules inside. I did not audit the aarch64 wheel.&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522_C_stable_libtorch%2522%252C%2522_moe_C_stable%2522%252C%2522deep_gemm%2522%252C%2522flashmla_C%2522%252C%2522flashmla_ext%2522%252C%2522flash_attn%2520FA2%2522%252C%2522flash_attn%2520FA3%2522%252C%2522qutlass%2522%252C%2522flashkda%2522%252C%2522deepselect%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522stable%2520shim%2520symbols%2520used%2522%252C%2522data%2522%253A%255B88%252C72%252C85%252C66%252C63%252C69%252C63%252C63%252C43%252C40%255D%252C%2522backgroundColor%2522%253A%255B%2522%252316a34a%2522%252C%2522%252316a34a%2522%252C%2522%252316a34a%2522%252C%2522%252316a34a%2522%252C%2522%252316a34a%2522%252C%2522%25232563eb%2522%252C%2522%25232563eb%2522%252C%2522%2523dc2626%2522%252C%2522%252316a34a%2522%252C%2522%25232563eb%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522vLLM%25200.33.0%2520wheel%252C%2520per%2520extension.%2520Color%2520%253D%2520minimum%2520torch%253A%2520blue%25202.10%252C%2520green%25202.11%252C%2520red%25202.13%2522%252C%2522fontSize%2522%253A15%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%2522stable%2520C%2520shim%2520symbols%2520%2528torch-abi-audit%2529%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522_C_stable_libtorch%2522%252C%2522_moe_C_stable%2522%252C%2522deep_gemm%2522%252C%2522flashmla_C%2522%252C%2522flashmla_ext%2522%252C%2522flash_attn%2520FA2%2522%252C%2522flash_attn%2520FA3%2522%252C%2522qutlass%2522%252C%2522flashkda%2522%252C%2522deepselect%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522stable%2520shim%2520symbols%2520used%2522%252C%2522data%2522%253A%255B88%252C72%252C85%252C66%252C63%252C69%252C63%252C63%252C43%252C40%255D%252C%2522backgroundColor%2522%253A%255B%2522%252316a34a%2522%252C%2522%252316a34a%2522%252C%2522%252316a34a%2522%252C%2522%252316a34a%2522%252C%2522%252316a34a%2522%252C%2522%25232563eb%2522%252C%2522%25232563eb%2522%252C%2522%2523dc2626%2522%252C%2522%252316a34a%2522%252C%2522%25232563eb%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522vLLM%25200.33.0%2520wheel%252C%2520per%2520extension.%2520Color%2520%253D%2520minimum%2520torch%253A%2520blue%25202.10%252C%2520green%25202.11%252C%2520red%25202.13%2522%252C%2522fontSize%2522%253A15%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%2522stable%2520C%2520shim%2520symbols%2520%2528torch-abi-audit%2529%2522%257D%257D%255D%257D%257D%257D" alt="Per-extension stable shim symbol counts in the vLLM 0.33.0 wheel, colored by the minimum torch version each one needs" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Extension&lt;/th&gt;
&lt;th&gt;Stable shim symbols&lt;/th&gt;
&lt;th&gt;Min torch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_C_stable_libtorch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;2.11.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deep_gemm/_C_extension&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;2.11.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_moe_C_stable_libtorch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;2.11.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm_flash_attn/_vllm_fa2_C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;2.10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_flashmla_C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;2.11.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_flashmla_extension_C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;2.11.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm_flash_attn/_vllm_fa3_C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;2.10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_qutlass_C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.13.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_flashkda_C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;2.11.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_deepselect_C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;2.10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three more modules (&lt;code&gt;cumem_allocator&lt;/code&gt;, &lt;code&gt;fs_io_C&lt;/code&gt;, &lt;code&gt;spinloop&lt;/code&gt;) do not touch torch at all. Every torch-linked module reported &lt;code&gt;unstable=0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The tool reports "the minimum LibTorch version that provides all the required stable symbols", so the wheel-level answer of 2.13.0 is the maximum over the table: one extension, QuTLASS, raises the floor by two minor versions over most of the rest. If you ever wonder why a wheel built on a stable ABI still refuses an older torch, this is the place to look.&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522FlashAttention-2%2522%252C%2522FlashAttention-3%2522%252C%2522FlashMLA%2522%252C%2522FlashMLA%2520ext%2522%252C%2522QuTLASS%2522%252C%2522DeepGEMM%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Unstable%2520libtorch%2520symbols%2520before%2520dependency%2520migration%2520%2528blog%2520audit%2529%2522%252C%2522data%2522%253A%255B85%252C81%252C70%252C68%252C59%252C59%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%252C%257B%2522label%2522%253A%2522Unstable%2520symbols%2520in%2520the%2520shipped%25200.33.0%2520wheel%2520%2528my%2520audit%2529%2522%252C%2522data%2522%253A%255B0%252C0%252C0%252C0%252C0%252C0%255D%252C%2522backgroundColor%2522%253A%2522%252316a34a%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Six%2520bundled%2520kernel%2520libraries%2520went%2520from%2520422%2520unstable%2520symbols%2520to%2520zero%2522%252C%2522fontSize%2522%253A16%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%2522unstable%2520libtorch%2520symbols%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522FlashAttention-2%2522%252C%2522FlashAttention-3%2522%252C%2522FlashMLA%2522%252C%2522FlashMLA%2520ext%2522%252C%2522QuTLASS%2522%252C%2522DeepGEMM%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Unstable%2520libtorch%2520symbols%2520before%2520dependency%2520migration%2520%2528blog%2520audit%2529%2522%252C%2522data%2522%253A%255B85%252C81%252C70%252C68%252C59%252C59%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%252C%257B%2522label%2522%253A%2522Unstable%2520symbols%2520in%2520the%2520shipped%25200.33.0%2520wheel%2520%2528my%2520audit%2529%2522%252C%2522data%2522%253A%255B0%252C0%252C0%252C0%252C0%252C0%255D%252C%2522backgroundColor%2522%253A%2522%252316a34a%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Six%2520bundled%2520kernel%2520libraries%2520went%2520from%2520422%2520unstable%2520symbols%2520to%2520zero%2522%252C%2522fontSize%2522%253A16%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%2522unstable%2520libtorch%2520symbols%2522%257D%257D%255D%257D%257D%257D" alt="Unstable libtorch symbols per bundled library before and after the dependency migration" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The blog's earlier audit, taken before the seven dependencies were migrated, listed six libraries with unstable symbols: 85 (FlashAttention-2), 81 (FlashAttention-3), 70 and 68 (the two FlashMLA modules), and 59 each for QuTLASS and DeepGEMM. That is 422 unstable symbol uses in total, and the shipped wheel has none.&lt;/p&gt;

&lt;p&gt;Two gotchas from running the tool myself. It shells out to &lt;code&gt;nm&lt;/code&gt;, so on Windows a single &lt;code&gt;.so&lt;/code&gt; fails with &lt;code&gt;'nm' not found&lt;/code&gt;. Pointed at the unpacked directory, it printed &lt;code&gt;Torch ABI: NO-TORCH&lt;/code&gt; and &lt;code&gt;Extensions: 0&lt;/code&gt;, presumably because Windows Python lists only &lt;code&gt;.pyd&lt;/code&gt; as extension-module suffixes, so &lt;code&gt;.so&lt;/code&gt; files are skipped. That is a false clean bill of health for a perfectly good wheel. I got the real result by running it in WSL Ubuntu, where &lt;code&gt;nm&lt;/code&gt; exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  If the binary is stable, why can't I just pip install a newer torch?
&lt;/h2&gt;

&lt;p&gt;Because the stable ABI fixes binary compatibility and says nothing about what the package declares. The metadata I read from PyPI on 2026-11-08:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;vllm==0.33.0&lt;/code&gt; requires &lt;code&gt;torch==2.14.0&lt;/code&gt;, &lt;code&gt;torchvision==0.29.0&lt;/code&gt; and &lt;code&gt;flashinfer-python==0.7.2.post1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;torchvision==0.29.0&lt;/code&gt; itself requires &lt;code&gt;torch==2.14.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;torchvision&lt;/code&gt; 0.29.1 (2026-09-30) and 0.29.2 (2026-10-28) relaxed that to &lt;code&gt;torch&amp;gt;=2.14.0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the PyTorch side has already moved from exact pins to lower bounds for torchvision, while vLLM's own metadata still carries exact ones. The 0.33.0 pin of 2.14.0 is also one minor version above the audited floor of 2.13.0, and the post does not say why.&lt;/p&gt;

&lt;p&gt;The post's demo sidesteps this. It builds vLLM from source (the printed version is &lt;code&gt;0.1.dev24716+gcb95d994f.d20261105&lt;/code&gt;, not a release), runs it on torch 2.14.1+cu132, swaps in &lt;code&gt;torch==2.15.0&lt;/code&gt; and reruns without rebuilding. Both runs print the same text for the prompt "The capital of France is". That is a useful existence proof, with limits worth stating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model is SmolLM2-135M-Instruct, &lt;code&gt;max_model_len=128&lt;/code&gt;, 8 generated tokens at temperature 0.&lt;/li&gt;
&lt;li&gt;It runs with &lt;code&gt;enforce_eager=True&lt;/code&gt;, which turns CUDA Graphs off. That is the mode where the post says the shim's overhead can matter, and no timing is reported.&lt;/li&gt;
&lt;li&gt;It sets &lt;code&gt;VLLM_USE_FLASHINFER_SAMPLER=0&lt;/code&gt; without explaining why. FlashInfer is a separate PyPI package that vLLM pins, and it is not one of the seven dependencies in the post's migration table, so the audit says nothing about it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How long did the old model make you wait?
&lt;/h2&gt;

&lt;p&gt;The rebuild tax shows up as a delay between a torch release and the first vLLM release that pins it. I measured it from PyPI upload timestamps, using the &lt;code&gt;torch&lt;/code&gt; requirement in each release's metadata.&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522torch%25202.10.0%2522%252C%2522torch%25202.11.0%2522%252C%2522torch%25202.13.0%2522%252C%2522torch%25202.14.0%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Days%2520from%2520torch%2520release%2520to%2520first%2520vLLM%2520release%2520pinning%2520it%2522%252C%2522data%2522%253A%255B44%252C34%252C33%252C60%255D%252C%2522backgroundColor%2522%253A%255B%2522%252364748b%2522%252C%2522%252364748b%2522%252C%2522%252364748b%2522%252C%2522%2523dc2626%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Pin%2520lag%253A%2520torch%2520release%2520to%2520first%2520vLLM%2520release%2520that%2520pins%2520it%2522%252C%2522fontSize%2522%253A16%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%2522days%2520%2528PyPI%2520upload%2520timestamps%2529%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522torch%25202.10.0%2522%252C%2522torch%25202.11.0%2522%252C%2522torch%25202.13.0%2522%252C%2522torch%25202.14.0%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Days%2520from%2520torch%2520release%2520to%2520first%2520vLLM%2520release%2520pinning%2520it%2522%252C%2522data%2522%253A%255B44%252C34%252C33%252C60%255D%252C%2522backgroundColor%2522%253A%255B%2522%252364748b%2522%252C%2522%252364748b%2522%252C%2522%252364748b%2522%252C%2522%2523dc2626%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Pin%2520lag%253A%2520torch%2520release%2520to%2520first%2520vLLM%2520release%2520that%2520pins%2520it%2522%252C%2522fontSize%2522%253A16%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%2522days%2520%2528PyPI%2520upload%2520timestamps%2529%2522%257D%257D%255D%257D%257D%257D" alt="Days between each torch release and the first vLLM release that pins it" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script that produced the numbers, so you can rerun it later and see whether the 0.33 line changes the 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="kn"&gt;import&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;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pypi&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="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&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;url&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;https://pypi.org/pypi/&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;version&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&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;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&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;def&lt;/span&gt; &lt;span class="nf"&gt;first_upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&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;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&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="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upload_time&lt;/span&gt;&lt;span class="sh"&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;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&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;pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkg&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;req&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;info&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requires_dist&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="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;match&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\s*(==|&amp;gt;=)\s*([\w.]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&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="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&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="o"&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;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;torch_dates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;first_upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;pypi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;torch&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;releases&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;vllm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pypi&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&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;releases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;seen&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;v&lt;/span&gt; &lt;span class="ow"&gt;in&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;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vllm&lt;/span&gt; &lt;span class="k"&gt;if&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;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0\.\d+\.\d+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;vllm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&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;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&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="o"&gt;&amp;lt;&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="mi"&gt;17&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;continue&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pypi&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;info&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;torch&lt;/span&gt;&lt;span class="sh"&gt;"&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;p&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;first_upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vllm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&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;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&amp;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;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;vllm &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; first pins torch&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | torch released &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch_dates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="o"&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;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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; | vllm shipped &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="o"&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;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | lag &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;torch_dates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pypi&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&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;0.33.0&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;info&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="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;vllm 0.33.0 metadata:&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="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&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;k&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;torch&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;torchvision&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;flashinfer-python&lt;/span&gt;&lt;span class="sh"&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;tv&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;0.29.0&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;0.29.1&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;0.29.2&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;torchvision &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tv&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; requires torch&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pypi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;torchvision&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tv&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;info&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;torch&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="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;Output from running it today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vllm 0.17.0  first pins torch==2.10.0  | torch released 2026-01-21 | vllm shipped 2026-03-07 | lag 44 days
vllm 0.20.0  first pins torch==2.11.0  | torch released 2026-03-23 | vllm shipped 2026-04-27 | lag 34 days
vllm 0.27.0  first pins torch==2.13.0  | torch released 2026-07-08 | vllm shipped 2026-08-10 | lag 33 days
vllm 0.33.0  first pins torch==2.14.0  | torch released 2026-09-02 | vllm shipped 2026-11-02 | lag 60 days

vllm 0.33.0 metadata: {'torch': '==2.14.0', 'torchvision': '==0.29.0', 'flashinfer-python': '==0.7.2.post1'}
torchvision 0.29.0 requires torch==2.14.0
torchvision 0.29.1 requires torch&amp;gt;=2.14.0
torchvision 0.29.2 requires torch&amp;gt;=2.14.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things stand out in that data.&lt;/p&gt;

&lt;p&gt;First, the lag was 33 to 60 days, with the longest one being the most recent: torch 2.14.0 came out on 2026-09-02 and vLLM first pinned it on 2026-11-02. The median of the four is 39 days.&lt;/p&gt;

&lt;p&gt;Second, torch 2.12.0 (2026-05-13) was never pinned by any release from 0.17.0 onward. vLLM went straight from 2.11.0 to 2.13.0 at 0.27.0.&lt;/p&gt;

&lt;p&gt;Third, a pin sticks. The 2.11.0 pin covered 11 releases over 105 days, and the 2.13.0 pin covered 7 releases over 84 days. For an operator, "which torch does the current vLLM release want" has had a stable answer for months at a time, and that is the thing the stable ABI is meant to loosen.&lt;/p&gt;

&lt;p&gt;A caveat on interpretation: PyPI pins are a proxy for what the default wheel supported. The main branch and nightlies may have worked with a new torch earlier, and I did not check. torch 2.15.0 went up on 2026-10-28, so it is 11 days old as of this writing, and vLLM 0.33.0 is still on 2.14.0. If the median lag held, a pin would land around December 6. That is a simple extrapolation from four points, not a prediction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should an operator do with this today?
&lt;/h2&gt;

&lt;p&gt;My recommendation is to treat the pin as a tested combination and the ABI as headroom, not as a promise. A concrete approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stay on the pinned torch for production&lt;/strong&gt; until you have a reason not to. The audit tells you symbols resolve. It does not tell you that kernels behave the same on a new CUDA or driver combination, and the post says plainly that Python APIs, the minimum CUDA version and third-party dependencies still constrain compatibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you need a newer torch, do it in staging and expect pip to complain.&lt;/strong&gt; Installing a newer torch on top of vLLM 0.33.0 should make pip print its incompatible-requirements warning about the exact pins, and a fresh &lt;code&gt;pip install vllm torch==2.15.0&lt;/code&gt; should fail resolution. I have not run this swap. If you use uv, its &lt;code&gt;--override&lt;/code&gt; option replaces a dependency's declared requirement, which is the cleaner way to say "I know".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit what you actually deploy.&lt;/strong&gt; Run the check against the installed package, not against the blog post's version of events:
&lt;/li&gt;
&lt;/ol&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;torch-abi-audit            &lt;span class="c"&gt;# needs nm (binutils) on PATH&lt;/span&gt;
torch-abi-audit &lt;span class="nt"&gt;--check&lt;/span&gt; both vllm      &lt;span class="c"&gt;# exit 1 on a violation, 2 on an operational error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per the tool's &lt;code&gt;--help&lt;/code&gt;, &lt;code&gt;--check torch&lt;/code&gt; fails on libtorch use without the stable ABI, &lt;code&gt;--check abi3&lt;/code&gt; fails when a torch-using package is not CPython abi3 compliant, and &lt;code&gt;both&lt;/code&gt; requires both. Add it to the image build for your own custom ops too.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Smoke test with CUDA Graphs on.&lt;/strong&gt; The one overhead the post flags (type conversion and dispatcher boxing in the shim) is host-side and is claimed to vanish after warmup under CUDA Graphs. Verify that with your own eager and graph-mode latency, because the post publishes no numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know which builds are covered.&lt;/strong&gt; The post says vLLM publishes ABI stable CUDA wheels as of 0.33.0 and lists CPU and ROCm as future work. ROCm shares most sources with CUDA through hipify, with only a few ROCm-specific files left.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;Can I upgrade PyTorch under an installed vLLM 0.33.0 without rebuilding?&lt;/strong&gt;&lt;br&gt;
At the binary level the audit says the wheel needs symbols that exist from torch 2.13.0, and the post demonstrates 2.14.1 to 2.15.0 on a source build. At the package level, PyPI metadata pins &lt;code&gt;torch==2.14.0&lt;/code&gt;, so you would be overriding the pin. I have not tested the swap on a GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is the wheel's minimum torch 2.13.0 when most extensions need 2.10 or 2.11?&lt;/strong&gt;&lt;br&gt;
The reported floor is the highest requirement of any extension, and &lt;code&gt;_qutlass_C&lt;/code&gt; requires 2.13.0. The audit output shows this directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the stable ABI make vLLM slower?&lt;/strong&gt;&lt;br&gt;
The post says the shim adds type-conversion overhead, plus operator lookup and boxing for wrappers that go through &lt;code&gt;torch_call_dispatcher&lt;/code&gt;. It calls this usually negligible and gone after warmup under CUDA Graphs, but possibly visible in eager mode with very fast kernels, and reports no noticeable regression. No measurements are published.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it cover CPU and ROCm builds?&lt;/strong&gt;&lt;br&gt;
Not yet. The announcement covers the CUDA wheels from 0.33.0. CPU and ROCm are listed as next steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a stable ABI remove CUDA version constraints?&lt;/strong&gt;&lt;br&gt;
No. The post states that Python APIs, the minimum supported CUDA version and third-party dependencies still affect compatibility. The ABI only addresses binary compatibility with LibTorch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I check any other wheel for ABI stability?&lt;/strong&gt;&lt;br&gt;
Install &lt;code&gt;torch-abi-audit&lt;/code&gt;, make sure &lt;code&gt;nm&lt;/code&gt; is available, and run it on the package or a &lt;code&gt;.so&lt;/code&gt; path. The &lt;code&gt;--json&lt;/code&gt; flag gives machine-readable output for CI.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/your-llm-serving-engine-ships-every-13-days-reading-vllms-breaking-change-lists-as-upgrade-risk-1815"&gt;Your LLM Serving Engine Ships Every 13 Days: Reading vLLM's Breaking-Change Lists as Upgrade Risk Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/vllm-033-adds-a-lossy-speculative-verifier-why-single-run-accuracy-tables-cannot-tell-you-it-is-50p3"&gt;vLLM 0.33 Adds a Lossy Speculative Verifier: Why Single-Run Accuracy Tables Cannot Tell You It Is Safe&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vllm.ai/blog/2026-11-06-stable-abi-cuda" rel="noopener noreferrer"&gt;Migrating vLLM to the LibTorch Stable ABI (vLLM Blog, 2026-11-06)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/vllm/0.33.0/" rel="noopener noreferrer"&gt;vllm 0.33.0 on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Quansight/torch-abi-audit" rel="noopener noreferrer"&gt;torch-abi-audit (Quansight)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/torchvision/" rel="noopener noreferrer"&gt;torchvision on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/torch/" rel="noopener noreferrer"&gt;torch on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>vllm</category>
    </item>
    <item>
      <title>Running an MoE Model Larger Than Your VRAM: SGLang Paged Experts, the PCIe Math, and the 4-Request Cliff</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Sat, 07 Nov 2026 00:01:10 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/running-an-moe-model-larger-than-your-vram-sglang-paged-experts-the-pcie-math-and-the-4-request-1g9o</link>
      <guid>https://dev.to/wolfsea2357/running-an-moe-model-larger-than-your-vram-sglang-paged-experts-the-pcie-math-and-the-4-request-1g9o</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;SGLang 0.5.23 (released 2026-11-02) ships an opt-in feature called Paged Experts. It keeps K of the E experts of every MoE layer in GPU memory, holds all E in pinned host RAM, and copies in only the experts a step routes to that are not resident. The expert math still runs on the GPU.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What the PR measured:&lt;/strong&gt; on Qwen3-30B-A3B, single-stream decode of 205.6, 87.7 and 32.6 tok/s (int4 on a 16 GB card, FP8 on a 16 GB card, int4 on an 8 GB laptop), against 16.6, 2.9 and 1.2 for &lt;code&gt;--cpu-offload-gb&lt;/code&gt;. That is 12x, 30x and 27x.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Against llama.cpp tuned to its best &lt;code&gt;--n-cpu-moe&lt;/code&gt;:&lt;/strong&gt; a tie when most of the model fits or the link is slow (206 vs 194 and 33 vs 33 tok/s), and nearly 2x faster in the FP8 case (87.7 vs 45.2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What I computed from the PR's numbers:&lt;/strong&gt; the measured speeds are only reachable if consecutive tokens reuse experts. Uniform random routing would cap the FP8 case near 40 tok/s on a 50 GB/s link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where it breaks:&lt;/strong&gt; with 4 concurrent requests the laptop run fell to 11 tok/s against 41 for llama.cpp. The dividing line is K // top_k, the largest decode batch that fits one wave.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I did not run any of this.&lt;/strong&gt; The measurements come from one PR author's two consumer machines. The arithmetic on top of them is mine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What does Paged Experts actually do?
&lt;/h2&gt;

&lt;p&gt;A mixture-of-experts layer routes each token to &lt;code&gt;top_k&lt;/code&gt; of &lt;code&gt;E&lt;/code&gt; experts. For Qwen3-30B-A3B that is 8 of 128, across 48 layers. I checked the model's &lt;code&gt;config.json&lt;/code&gt; on Hugging Face: 48 layers, 128 experts, 8 per token, hidden size 2048, MoE intermediate size 768. The experts are most of the 30B parameters, so a model that is cheap to compute can still be impossible to hold on a small GPU.&lt;/p&gt;

&lt;p&gt;There are three ways to serve it when the experts do not fit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--cpu-offload-gb&lt;/code&gt; in SGLang.&lt;/strong&gt; Offloaded weights are streamed to the GPU on every forward pass, whatever the router picked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;llama.cpp &lt;code&gt;--n-cpu-moe N&lt;/code&gt;.&lt;/strong&gt; The experts of N layers stay in system RAM and are computed on the CPU. No weight traffic, but CPU-speed math.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paged Experts.&lt;/strong&gt; A K-slot table per layer lives on the GPU. Each step, the experts it needs that are missing get copied from pinned host memory into slots (least recently used out), then the model's own fused-MoE kernel runs on the GPU as usual.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third option is the HiCache idea applied to expert weights: the GPU holds the working set, host RAM holds the rest. PR #42557 (merged 2026-10-19) added bf16, block-FP8 and GPTQ experts, and PR #45787 (merged 2026-10-25) added AWQ, compressed-tensors, MXFP4, NVFP4, W8A8 and block-INT8.&lt;/p&gt;

&lt;p&gt;Turning it on takes two flags, quoted from the docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; sglang.launch_server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model-path&lt;/span&gt; Qwen/Qwen3-30B-A3B-GPTQ-Int4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-paged-experts&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--paged-experts-num-resident&lt;/span&gt; 92
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constraints matter as much as the flags. The docs list single GPU only (tensor, expert, pipeline and data parallelism are rejected at startup), speculative decoding rejected at startup, and a page-locked host store that has to hold every expert of every layer. Under WSL2, pinned memory is capped at about half of system RAM.&lt;/p&gt;

&lt;h2&gt;
  
  
  How fast is it, and against what?
&lt;/h2&gt;

&lt;p&gt;The PR benchmarks on two machines: an RTX 4070 Laptop (8 GB, PCIe 4.0 x8, 54 GB RAM, WSL2) and an RTX 5070 Ti desktop (16 GB, PCIe 5.0 x16, 62 GB RAM, native Linux). The workload is ShareGPT prompts with 256 output tokens, prefix caching off in both engines, same KV capacity (10,240 tokens), byte-matched quantization (GPTQ int4 against &lt;code&gt;IQ4_XS&lt;/code&gt;, FP8 against &lt;code&gt;Q8_0&lt;/code&gt;). llama.cpp's &lt;code&gt;--n-cpu-moe&lt;/code&gt; was searched in steps of 1 and only counted when its own memory projection fit physical VRAM. The PR's predecessor compared against &lt;code&gt;--cpu-moe&lt;/code&gt; only, which in the author's own words overstated the advantage.&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%252216%2520GB%2520card%252C%2520int4%2520%2528K%253D92%2529%2522%252C%252216%2520GB%2520card%252C%2520FP8%2520%2528K%253D40%2529%2522%252C%25228%2520GB%2520laptop%252C%2520int4%2520%2528K%253D26%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522SGLang%2520Paged%2520Experts%2522%252C%2522data%2522%253A%255B205.6%252C87.7%252C32.6%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522llama.cpp%252C%2520best%2520--n-cpu-moe%2522%252C%2522data%2522%253A%255B194.4%252C45.2%252C32.8%255D%252C%2522backgroundColor%2522%253A%2522%252364748b%2522%257D%252C%257B%2522label%2522%253A%2522SGLang%2520--cpu-offload-gb%2522%252C%2522data%2522%253A%255B16.6%252C2.9%252C1.2%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Single-stream%2520decode%252C%2520Qwen3-30B-A3B%252C%2520three%2520offload%2520strategies%2520%2528PR%2520%252342557%2520data%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522type%2522%253A%2522logarithmic%2522%252C%2522ticks%2522%253A%257B%2522min%2522%253A1%252C%2522max%2522%253A400%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522single-stream%2520decode%2520tok%2Fs%2520%2528log%2520scale%2529%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%252216%2520GB%2520card%252C%2520int4%2520%2528K%253D92%2529%2522%252C%252216%2520GB%2520card%252C%2520FP8%2520%2528K%253D40%2529%2522%252C%25228%2520GB%2520laptop%252C%2520int4%2520%2528K%253D26%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522SGLang%2520Paged%2520Experts%2522%252C%2522data%2522%253A%255B205.6%252C87.7%252C32.6%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522llama.cpp%252C%2520best%2520--n-cpu-moe%2522%252C%2522data%2522%253A%255B194.4%252C45.2%252C32.8%255D%252C%2522backgroundColor%2522%253A%2522%252364748b%2522%257D%252C%257B%2522label%2522%253A%2522SGLang%2520--cpu-offload-gb%2522%252C%2522data%2522%253A%255B16.6%252C2.9%252C1.2%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Single-stream%2520decode%252C%2520Qwen3-30B-A3B%252C%2520three%2520offload%2520strategies%2520%2528PR%2520%252342557%2520data%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522type%2522%253A%2522logarithmic%2522%252C%2522ticks%2522%253A%257B%2522min%2522%253A1%252C%2522max%2522%253A400%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522single-stream%2520decode%2520tok%2Fs%2520%2528log%2520scale%2529%2522%257D%257D%255D%257D%257D%257D" alt="Bar chart of single-stream decode tokens per second for Paged Experts, tuned llama.cpp and SGLang CPU offload on three GPU and quantization setups" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup (Qwen3-30B-A3B)&lt;/th&gt;
&lt;th&gt;Paged Experts&lt;/th&gt;
&lt;th&gt;llama.cpp, best&lt;/th&gt;
&lt;th&gt;&lt;code&gt;--cpu-offload-gb&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;vs offload&lt;/th&gt;
&lt;th&gt;vs llama.cpp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16 GB, int4, K=92&lt;/td&gt;
&lt;td&gt;205.6&lt;/td&gt;
&lt;td&gt;194.4&lt;/td&gt;
&lt;td&gt;16.6&lt;/td&gt;
&lt;td&gt;12.4x&lt;/td&gt;
&lt;td&gt;1.06x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 GB, FP8, K=40&lt;/td&gt;
&lt;td&gt;87.7&lt;/td&gt;
&lt;td&gt;45.2&lt;/td&gt;
&lt;td&gt;2.9&lt;/td&gt;
&lt;td&gt;30.2x&lt;/td&gt;
&lt;td&gt;1.94x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 GB laptop, int4, K=26&lt;/td&gt;
&lt;td&gt;32.6&lt;/td&gt;
&lt;td&gt;32.8&lt;/td&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;td&gt;27.2x&lt;/td&gt;
&lt;td&gt;0.99x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Decode tok/s, single stream, from the PR tables. The two ratio columns are my division. Each headline cell was measured at least twice.&lt;/p&gt;

&lt;p&gt;The 12x to 30x headline is against the weakest baseline. Against a tuned llama.cpp the result splits in two: when the model nearly fits, or the link is slow, paging and CPU compute land in the same place. When many experts must live in RAM and the link is fast, GPU math wins by a wide margin because llama.cpp's CPU-side layers become the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is it fast when experts keep crossing PCIe?
&lt;/h2&gt;

&lt;p&gt;A token at batch 1 activates 48 x 8 = 384 experts. Per the docs an int4 expert here is 2.43 MB; FP8 is about 4.72 MB (3 x 2048 x 768 weights at one byte, scales ignored). If every activation missed, one token would need 0.93 GB (int4) or 1.81 GB (FP8) over the link.&lt;/p&gt;

&lt;p&gt;Now compare with the measured decode time. The FP8 run does 87.7 tok/s, so 11.4 ms per token. Copying 1.81 GB in 11.4 ms needs 159 GB/s, more than a PCIe 5.0 x16 link can deliver (about 63 GB/s theoretical). So most activations cannot have been misses.&lt;/p&gt;

&lt;p&gt;For a number I need an effective bandwidth. The PR implies one: a prefill pages in about 4 GB in about 80 ms at K=92, and (128 - 92) x 117 MB per slot is 4.2 GB, so roughly 50 GB/s. With that, the highest miss rate each measured speed allows is:&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522int4%252016%2520GB%2520%2528K%253D92%2529%2522%252C%2522FP8%252016%2520GB%2520%2528K%253D40%2529%2522%252C%2522int4%25208%2520GB%2520%2528K%253D26%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Miss%2520rate%2520if%2520routing%2520were%2520uniform%2520random%2520%25281%2520-%2520K%2FE%2529%2522%252C%2522data%2522%253A%255B28.1%252C68.8%252C79.7%255D%252C%2522backgroundColor%2522%253A%2522%2523f59e0b%2522%257D%252C%257B%2522label%2522%253A%2522Highest%2520miss%2520rate%2520the%2520measured%2520speed%2520allows%2522%252C%2522data%2522%253A%255B26.1%252C31.5%252C51.8%255D%252C%2522backgroundColor%2522%253A%2522%252316a34a%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Measured%2520speed%2520implies%2520expert%2520reuse%2520across%2520tokens%2520%2528my%2520arithmetic%2520on%2520PR%2520data%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A100%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522share%2520of%2520expert%2520activations%2520that%2520need%2520a%2520copy%2520%2528%2525%2529%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522int4%252016%2520GB%2520%2528K%253D92%2529%2522%252C%2522FP8%252016%2520GB%2520%2528K%253D40%2529%2522%252C%2522int4%25208%2520GB%2520%2528K%253D26%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Miss%2520rate%2520if%2520routing%2520were%2520uniform%2520random%2520%25281%2520-%2520K%2FE%2529%2522%252C%2522data%2522%253A%255B28.1%252C68.8%252C79.7%255D%252C%2522backgroundColor%2522%253A%2522%2523f59e0b%2522%257D%252C%257B%2522label%2522%253A%2522Highest%2520miss%2520rate%2520the%2520measured%2520speed%2520allows%2522%252C%2522data%2522%253A%255B26.1%252C31.5%252C51.8%255D%252C%2522backgroundColor%2522%253A%2522%252316a34a%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Measured%2520speed%2520implies%2520expert%2520reuse%2520across%2520tokens%2520%2528my%2520arithmetic%2520on%2520PR%2520data%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A100%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522share%2520of%2520expert%2520activations%2520that%2520need%2520a%2520copy%2520%2528%2525%2529%2522%257D%257D%255D%257D%257D%257D" alt="Bar chart comparing the miss rate under uniform random routing with the highest miss rate allowed by the measured decode speed" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Link assumed&lt;/th&gt;
&lt;th&gt;Miss rate if routing were uniform random (1 - K/E)&lt;/th&gt;
&lt;th&gt;Highest miss rate the speed allows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16 GB, FP8, K=40&lt;/td&gt;
&lt;td&gt;~50 GB/s&lt;/td&gt;
&lt;td&gt;69%&lt;/td&gt;
&lt;td&gt;31%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 GB laptop, int4, K=26&lt;/td&gt;
&lt;td&gt;15.75 GB/s (peak)&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 GB, int4, K=92&lt;/td&gt;
&lt;td&gt;~50 GB/s&lt;/td&gt;
&lt;td&gt;28%&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Under uniform random routing the FP8 run would be capped near 40 tok/s, and it measured 87.7. The laptop would be capped near 21 tok/s on a link running at its theoretical peak, and it measured 32.6. So the router must be reusing experts from one token to the next, the premise the docs state. The int4 16 GB row is inconclusive: at 63 GB/s instead of 50 the bound moves to 33%, above the random baseline.&lt;/p&gt;

&lt;p&gt;Two caveats. These are upper bounds on the miss rate, not measured miss rates; only a counter inside the engine gives the real value. And the reuse was seen on natural-language prompts. The PR warns that synthetic prompts mis-measure paging, so if your traffic is unusual, test on your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens with more than one request?
&lt;/h2&gt;

&lt;p&gt;The PR is candid here. At 4 concurrent requests the laptop run produced 11 to 12 tok/s against 41 for llama.cpp, and FP8 on the 16 GB card gave 84 tok/s at K=40 but 31 at K=24 (llama.cpp: 55).&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522int4%252016%2520GB%2520K%253D92%2520%2528cap%252011%2529%2522%252C%2522FP8%252016%2520GB%2520K%253D40%2520%2528cap%25205%2529%2522%252C%2522FP8%252016%2520GB%2520K%253D24%2520%2528cap%25203%2529%2522%252C%2522int4%25208%2520GB%2520K%253D26%2520%2528cap%25203%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Paged%2520Experts%252C%25204%2520requests%2522%252C%2522data%2522%253A%255B387%252C84%252C31%252C11%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522llama.cpp%2520best%252C%25204%2520requests%2522%252C%2522data%2522%253A%255B312%252C55%252C55%252C41%255D%252C%2522backgroundColor%2522%253A%2522%252364748b%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%25224%2520concurrent%2520requests%253A%2520paging%2520falls%2520behind%2520once%2520K%2520%2F%2F%2520top_k%2520drops%2520below%25204%2522%252C%2522fontSize%2522%253A16%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%2522output%2520tok%2Fs%2520with%25204%2520requests%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522int4%252016%2520GB%2520K%253D92%2520%2528cap%252011%2529%2522%252C%2522FP8%252016%2520GB%2520K%253D40%2520%2528cap%25205%2529%2522%252C%2522FP8%252016%2520GB%2520K%253D24%2520%2528cap%25203%2529%2522%252C%2522int4%25208%2520GB%2520K%253D26%2520%2528cap%25203%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Paged%2520Experts%252C%25204%2520requests%2522%252C%2522data%2522%253A%255B387%252C84%252C31%252C11%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522llama.cpp%2520best%252C%25204%2520requests%2522%252C%2522data%2522%253A%255B312%252C55%252C55%252C41%255D%252C%2522backgroundColor%2522%253A%2522%252364748b%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%25224%2520concurrent%2520requests%253A%2520paging%2520falls%2520behind%2520once%2520K%2520%2F%2F%2520top_k%2520drops%2520below%25204%2522%252C%2522fontSize%2522%253A16%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%2522output%2520tok%2Fs%2520with%25204%2520requests%2522%257D%257D%255D%257D%257D%257D" alt="Bar chart of output tokens per second with 4 concurrent requests, Paged Experts against llama.cpp, for four K settings" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The docs explain the mechanism. A decode step is served by a single wave only if &lt;code&gt;tokens x top_k &amp;lt;= K&lt;/code&gt;, which is decode up to &lt;code&gt;K // top_k&lt;/code&gt; requests, and that single wave is the case a CUDA graph can capture. Larger batches run without a graph and need several waves, each with its own page-in. Four requests with top-8 routing can touch up to 32 distinct experts per layer, and the table of K values shows the pattern:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;K&lt;/th&gt;
&lt;th&gt;K // 8&lt;/th&gt;
&lt;th&gt;4-request result vs llama.cpp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;int4, 16 GB&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;387 vs 312 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8, 16 GB&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;84 vs 55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8, 16 GB&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;31 vs 55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int4, 8 GB&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;11 vs 41&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both runs where &lt;code&gt;K // 8&lt;/code&gt; is below 4 lose; both where it is 5 or more win. Four points do not make a law, and the PR attributes the loss to steps that route to more experts than fit, the same condition seen from the other side. It does give a usable sizing rule: choose K at least &lt;code&gt;batch x top_k&lt;/code&gt; for the batch size you plan to serve, or accept that you are outside the captured fast path. For one user this is irrelevant. For anything shared it decides whether the feature fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does a resident slot cost in context length?
&lt;/h2&gt;

&lt;p&gt;K and the KV cache come out of the same static budget (&lt;code&gt;--mem-fraction-static&lt;/code&gt;), and one slot costs &lt;code&gt;per_expert_bytes x num_moe_layers&lt;/code&gt;. Here the bf16 KV cache is 2 x 48 layers x 4 KV heads x 128 head dim x 2 bytes = 98,304 bytes (96 KiB) per token.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One int4 slot is 117 MB, about 1,190 tokens of bf16 KV cache (about 2,370 with an FP8 KV cache).&lt;/li&gt;
&lt;li&gt;One FP8 slot is about 227 MB, about 2,300 tokens of bf16 KV cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dropping K by 10 on the int4 model buys roughly 11,900 tokens of context at the price of more paging. The PR's whole 10,240-token cache is only about 1.0 GB, so the experts dominate the budget. If the KV cache does not fit at startup, the docs suggest lowering K or using &lt;code&gt;--kv-cache-dtype fp8&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do short prompts get slower first tokens?
&lt;/h2&gt;

&lt;p&gt;A prefill touches nearly every expert, so it pages in everything that is not resident, once. For int4 at K=92 that is about 4.2 GB. For FP8 at K=40 it is 88 experts x 227 MB, about 19.9 GB, and on the laptop (K=26) about 11.9 GB over a link that peaks at 15.75 GB/s, at least 750 ms.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TTFT p50&lt;/th&gt;
&lt;th&gt;Paged&lt;/th&gt;
&lt;th&gt;llama.cpp best&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;int4 16 GB, short / 2,048 tokens&lt;/td&gt;
&lt;td&gt;89 / 267 ms&lt;/td&gt;
&lt;td&gt;68 / 344 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8 16 GB, short / 2,048 tokens&lt;/td&gt;
&lt;td&gt;284 / 443 ms&lt;/td&gt;
&lt;td&gt;226 / 984 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;laptop int4, short / 2,048 tokens&lt;/td&gt;
&lt;td&gt;928 / 1,441 ms&lt;/td&gt;
&lt;td&gt;620 / 2,396 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The page-in is a fixed cost, while llama.cpp's CPU-side layers scale with prompt length, so paging loses on short prompts and wins on long ones. The FP8 2,048-token TTFT of 443 ms is within about 45 ms of the 400 ms needed to move 19.9 GB at 50 GB/s, which suggests nearly all of it is page-in (arithmetic, not a profile). For short chat turns, TTFT is the number to watch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can paging change the model's outputs?
&lt;/h2&gt;

&lt;p&gt;By design no. Routing is untouched and the paged layer returns the same result as the unpaged one. The docs report bit-identical logprobs for unquantized, FP8 and INT8 experts under deterministic inference, and agreement within Marlin's own batch-to-batch noise for the other formats. On GSM8K (200 questions), gpt-oss-20b MXFP4 scored 0.96 to 0.97 paged and 0.975 unpaged.&lt;/p&gt;

&lt;p&gt;The more instructive part is how it can go wrong silently, as described in PR #45787:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An expert tensor that was not listed as pageable stays unpaged, so a paged-in expert runs with another expert's values. In an AWQ control with the zero points unpaged, the median logprob error was 1.67 nats, against 0.003 when paged correctly.&lt;/li&gt;
&lt;li&gt;gpt-oss-20b's MXFP4 experts are padded by the Marlin kernel (2880 to 2944 or 3072). Before the fix, paged gpt-oss-20b scored GSM8K 0.000 with every answer truncated. After it, 0.960.&lt;/li&gt;
&lt;li&gt;The fix for the first class is structural: the pageable tensors are now derived from the layer, and at startup an independence check re-runs one more chunk of experts and fails if the post-load repack mixed experts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these crash. They produce fluent wrong text, so run an accuracy smoke test against the unpaged model on your own checkpoint format.&lt;/p&gt;

&lt;h2&gt;
  
  
  A preflight script before you turn it on
&lt;/h2&gt;

&lt;p&gt;This is the arithmetic above in a few lines: slot cost, batch cap, and whether a measured speed implies expert reuse.&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;preflight&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="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;experts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expert_mb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;link_gbps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tok_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv_bytes_per_token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;slot_mb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expert_mb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt;                    &lt;span class="c1"&gt;# GPU memory per slot, all layers
&lt;/span&gt;    &lt;span class="n"&gt;batch_cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;                          &lt;span class="c1"&gt;# largest single-wave decode batch
&lt;/span&gt;    &lt;span class="n"&gt;all_miss_gb&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;top_k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;expert_mb&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e3&lt;/span&gt;  &lt;span class="c1"&gt;# GB per token if every activation missed
&lt;/span&gt;    &lt;span class="n"&gt;max_miss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link_gbps&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tok_s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;all_miss_gb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# highest miss rate the speed allows
&lt;/span&gt;    &lt;span class="n"&gt;random_miss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;experts&lt;/span&gt;                   &lt;span class="c1"&gt;# miss rate with no reuse
&lt;/span&gt;    &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reuse implied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;max_miss&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;random_miss&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inconclusive&lt;/span&gt;&lt;span class="sh"&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;: slot=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slot_mb&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; MB (~&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slot_mb&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mf"&gt;1e6&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;kv_bytes_per_token&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; KV tokens), &lt;/span&gt;&lt;span class="sh"&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;batch cap=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;batch_cap&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, random-routing miss=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;random_miss&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="o"&gt;%&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max miss allowed by &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tok_s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tok/s=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_miss&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="o"&gt;%&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;verdict&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;span class="n"&gt;KV&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="mi"&gt;48&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;   &lt;span class="c1"&gt;# Qwen3-30B-A3B bf16 KV bytes per token
&lt;/span&gt;&lt;span class="nf"&gt;preflight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;int4 16GB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&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;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;92&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="mf"&gt;205.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KV&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;preflight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FP8  16GB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&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;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;4.72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&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="mf"&gt;87.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KV&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;preflight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;int4  8GB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&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;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;15.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;32.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KV&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output from running it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;int4 16GB: slot=117 MB (~1,187 KV tokens), batch cap=11, random-routing miss=28%, max miss allowed by 205.6 tok/s=26%, inconclusive
FP8  16GB: slot=227 MB (~2,305 KV tokens), batch cap=5, random-routing miss=69%, max miss allowed by 87.7 tok/s=31%, reuse implied
int4  8GB: slot=117 MB (~1,187 KV tokens), batch cap=3, random-routing miss=80%, max miss allowed by 32.6 tok/s=52%, reuse implied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a link speed measured with a pinned-memory copy test, not the spec sheet. The 50 GB/s here is inferred from the PR.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Confirm your format is in the supported table and the MoE runner is Triton or Marlin. Anything else is rejected at startup, which is the good failure.&lt;/li&gt;
&lt;li&gt;Check that the host can pin the whole expert store (half of RAM under WSL2).&lt;/li&gt;
&lt;li&gt;Choose K from the docs' formula, then check &lt;code&gt;K // top_k&lt;/code&gt; against your real concurrency.&lt;/li&gt;
&lt;li&gt;Measure host-to-device bandwidth on your machine, and benchmark your own prompts, because the PR warns that synthetic ones mis-measure paging.&lt;/li&gt;
&lt;li&gt;Run an accuracy smoke test against the unpaged model, and record TTFT for short and long prompts separately.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;Does it work across several GPUs or with speculative decoding?&lt;/strong&gt;&lt;br&gt;
No to both in 0.5.23. The docs say single GPU only, with tensor, expert, pipeline and data parallelism rejected at startup, and speculative decoding is rejected too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it change accuracy compared with an unpaged server?&lt;/strong&gt;&lt;br&gt;
The docs report bit-identical logprobs for unquantized, FP8 and INT8 experts under deterministic inference, and agreement within Marlin's noise for the other formats. Past bugs in this area failed silently, so run a smoke test on your format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I choose K?&lt;/strong&gt;&lt;br&gt;
Use the docs' budget formula, then keep K at least &lt;code&gt;batch x top_k&lt;/code&gt; for your target concurrency. Each extra slot costs &lt;code&gt;per_expert_bytes x layers&lt;/code&gt; of memory that would otherwise be KV cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it faster than llama.cpp's &lt;code&gt;--n-cpu-moe&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
It depends. In the PR, single-stream decode tied in two of three setups and was 1.94x faster in the third, while llama.cpp had lower TTFT on short prompts and better 4-request throughput on the laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much host RAM do I need?&lt;/strong&gt;&lt;br&gt;
Enough to pin every expert: about 14.9 GB at int4 and 29 GB at FP8 for Qwen3-30B-A3B, by my arithmetic on the docs' per-expert size.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/kv-cache-offloading-to-nvme-the-break-even-bandwidth-math-and-where-it-does-not-pay-off-1mo6"&gt;KV Cache Offloading to NVMe: The Break-Even Bandwidth Math and Where It Does Not Pay Off&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/what-does-it-actually-cost-to-self-host-an-llm-the-batching-math-nobody-shows-you-4pmp"&gt;What Does It Actually Cost to Self-Host an LLM? The Batching Math Nobody Shows You&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sgl-project/sglang/releases/tag/v0.5.23" rel="noopener noreferrer"&gt;SGLang v0.5.23 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sgl-project/sglang/pull/42557" rel="noopener noreferrer"&gt;PR #42557: Paged Experts, serve MoE models whose weights do not fit VRAM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sgl-project/sglang/pull/45787" rel="noopener noreferrer"&gt;PR #45787: Paged Experts for AWQ, compressed-tensors, MXFP4, NVFP4, W8A8 and block-INT8 experts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sgl-project/sglang/blob/v0.5.23/docs/docs/advanced_features/paged_experts.mdx" rel="noopener noreferrer"&gt;Paged Experts documentation at v0.5.23&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/Qwen/Qwen3-30B-A3B/blob/main/config.json" rel="noopener noreferrer"&gt;Qwen3-30B-A3B config.json&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>sglang</category>
    </item>
    <item>
      <title>vLLM 0.33 Adds a Lossy Speculative Verifier: Why Single-Run Accuracy Tables Cannot Tell You It Is Safe</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Thu, 05 Nov 2026 14:05:42 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/vllm-033-adds-a-lossy-speculative-verifier-why-single-run-accuracy-tables-cannot-tell-you-it-is-50p3</link>
      <guid>https://dev.to/wolfsea2357/vllm-033-adds-a-lossy-speculative-verifier-why-single-run-accuracy-tables-cannot-tell-you-it-is-50p3</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What shipped:&lt;/strong&gt; vLLM 0.33.0 (PyPI, 2026-11-02) includes FLy, an opt-in speculative decoding verifier that deliberately accepts some draft tokens the target model would have rejected. The PR states it "does not preserve the target distribution and may reduce output quality." Standard verification stays the default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the PR reports:&lt;/strong&gt; decode throughput 1.041x to 1.310x over standard speculative decoding at all 48 model, platform and batch points, with minimum accuracy retention of 96.3% against target-only decoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What I computed from the PR's own tables (not a new benchmark):&lt;/strong&gt; across the 32 accuracy cells, standard speculative decoding, which is lossless by design, lands a mean 1.68 points away from target-only. FLy lands 1.80 points away. The tables measure run-to-run noise at least as much as they measure FLy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What a real quality gate needs:&lt;/strong&gt; a paired per-question comparison. With 6% of questions flipping either way, detecting a 2-point net loss at 80% power takes about 1,175 questions. The PR's evaluation had 428 per model and platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bigger win in the same release is an accounting fix:&lt;/strong&gt; a scheduler change for DSpark and DFlash drafters took one reported run from 1,373 to 3,390 output tokens/s, and no kernel changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What does FLy change in speculative verification?
&lt;/h2&gt;

&lt;p&gt;In standard speculative decoding, a small drafter proposes K tokens and the target model verifies them in one forward pass. Verification stops at the first rejected token, and the accept/reject rule is built so the output distribution matches what the target would have produced alone.&lt;/p&gt;

&lt;p&gt;FLy, from the paper "Training-Free Loosely Speculative Decoding: Accepting Semantically Correct Drafts Beyond Exact Match" (arXiv 2511.22972), relaxes the stop. Per PR #53987 (merged 2026-10-19), a rejected position is forgiven when three conditions hold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The rejected draft token is not masked out by target constraints.&lt;/li&gt;
&lt;li&gt;The target's entropy at that position meets a threshold, meaning the target itself was unsure.&lt;/li&gt;
&lt;li&gt;The next &lt;code&gt;W&lt;/code&gt; draft tokens all pass the normal verification rule.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any of the next &lt;code&gt;W&lt;/code&gt; tokens is rejected, the original rejection stands. A short trailing window is never deferred. The PR notes the entropy is computed over only the top-k target probabilities (default 3) without renormalizing, which differs from the paper's full-vocabulary version. It needs no extra checkpoint and no extra target forward pass.&lt;/p&gt;

&lt;p&gt;Enabling it is one block in the speculative config:&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;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="n"&gt;llm&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="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;Qwen/Qwen3-8B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;speculative_config&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;method&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;draft_model&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;model&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;Qwen/Qwen3-0.6B&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;num_speculative_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejection_sample_method&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;fly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# default is standard verification
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fly_window_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fly_entropy_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fly_entropy_top_k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&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;Two details matter for reproducing the published numbers. &lt;code&gt;fly_window_size&lt;/code&gt; defaults to &lt;code&gt;min(6, num_speculative_tokens - 1)&lt;/code&gt;, and FLy needs at least two speculative tokens. The PR's ModelRunner V2 measurements, though, were run with &lt;code&gt;window=2&lt;/code&gt;. A DeepSeek setup with K=7 would default to a window of 6, so the default is not the configuration that was benchmarked. The engine also logs a one-time warning that the method is lossy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do the published speed numbers show?
&lt;/h2&gt;

&lt;p&gt;The PR's multi-batch table covers four models: DeepSeek-V4-Flash with DSpark (K=7), Qwen3.8-27B with MTP (K=3), GLM-5.2-FP8 with MTP (K=5) and Qwen3.5-122B-A10B-FP8 with MTP (K=8). They ran on AMD MI355X and NVIDIA B300 at batch sizes 1, 4, 8, 16, 32 and 64, with 1,024 generated tokens per request and a fixed pool of 64 prompts.&lt;/p&gt;

&lt;p&gt;I parsed all 48 rows. The speedup range matches the PR text (1.041x to 1.310x), and the median is 1.147x. Averaged over the six batch sizes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model (method, K)&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Mean speedup&lt;/th&gt;
&lt;th&gt;Mean draft acceptance gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.8-27B (MTP, 3)&lt;/td&gt;
&lt;td&gt;B300&lt;/td&gt;
&lt;td&gt;1.065x&lt;/td&gt;
&lt;td&gt;+7.6 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.8-27B (MTP, 3)&lt;/td&gt;
&lt;td&gt;MI355X&lt;/td&gt;
&lt;td&gt;1.083x&lt;/td&gt;
&lt;td&gt;+10.3 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-V4-Flash (DSpark, 7)&lt;/td&gt;
&lt;td&gt;MI355X&lt;/td&gt;
&lt;td&gt;1.137x&lt;/td&gt;
&lt;td&gt;+9.4 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-V4-Flash (DSpark, 7)&lt;/td&gt;
&lt;td&gt;B300&lt;/td&gt;
&lt;td&gt;1.151x&lt;/td&gt;
&lt;td&gt;+8.6 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5.2-FP8 (MTP, 5)&lt;/td&gt;
&lt;td&gt;B300&lt;/td&gt;
&lt;td&gt;1.156x&lt;/td&gt;
&lt;td&gt;+11.2 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5.2-FP8 (MTP, 5)&lt;/td&gt;
&lt;td&gt;MI355X&lt;/td&gt;
&lt;td&gt;1.170x&lt;/td&gt;
&lt;td&gt;+12.6 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-122B-A10B (MTP, 8)&lt;/td&gt;
&lt;td&gt;B300&lt;/td&gt;
&lt;td&gt;1.242x&lt;/td&gt;
&lt;td&gt;+16.4 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-122B-A10B (MTP, 8)&lt;/td&gt;
&lt;td&gt;MI355X&lt;/td&gt;
&lt;td&gt;1.281x&lt;/td&gt;
&lt;td&gt;+16.9 pp&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522scatter%2522%252C%2522data%2522%253A%257B%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522AMD%2520MI355X%2522%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%252C%2522borderColor%2522%253A%2522%2523dc2626%2522%252C%2522data%2522%253A%255B%257B%2522x%2522%253A7.0%252C%2522y%2522%253A1.103%257D%252C%257B%2522x%2522%253A11.1%252C%2522y%2522%253A1.161%257D%252C%257B%2522x%2522%253A9.0%252C%2522y%2522%253A1.11%257D%252C%257B%2522x%2522%253A8.1%252C%2522y%2522%253A1.168%257D%252C%257B%2522x%2522%253A9.5%252C%2522y%2522%253A1.144%257D%252C%257B%2522x%2522%253A11.6%252C%2522y%2522%253A1.139%257D%252C%257B%2522x%2522%253A9.5%252C%2522y%2522%253A1.082%257D%252C%257B%2522x%2522%253A10.4%252C%2522y%2522%253A1.096%257D%252C%257B%2522x%2522%253A9.7%252C%2522y%2522%253A1.083%257D%252C%257B%2522x%2522%253A11.3%252C%2522y%2522%253A1.116%257D%252C%257B%2522x%2522%253A10.8%252C%2522y%2522%253A1.068%257D%252C%257B%2522x%2522%253A10.0%252C%2522y%2522%253A1.055%257D%252C%257B%2522x%2522%253A15.1%252C%2522y%2522%253A1.172%257D%252C%257B%2522x%2522%253A14.2%252C%2522y%2522%253A1.216%257D%252C%257B%2522x%2522%253A11.2%252C%2522y%2522%253A1.172%257D%252C%257B%2522x%2522%253A11.7%252C%2522y%2522%253A1.124%257D%252C%257B%2522x%2522%253A11.3%252C%2522y%2522%253A1.229%257D%252C%257B%2522x%2522%253A11.9%252C%2522y%2522%253A1.11%257D%252C%257B%2522x%2522%253A16.2%252C%2522y%2522%253A1.251%257D%252C%257B%2522x%2522%253A17.6%252C%2522y%2522%253A1.28%257D%252C%257B%2522x%2522%253A18.1%252C%2522y%2522%253A1.295%257D%252C%257B%2522x%2522%253A16.1%252C%2522y%2522%253A1.264%257D%252C%257B%2522x%2522%253A17.1%252C%2522y%2522%253A1.289%257D%252C%257B%2522x%2522%253A16.0%252C%2522y%2522%253A1.31%257D%255D%257D%252C%257B%2522label%2522%253A%2522NVIDIA%2520B300%2522%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%252C%2522borderColor%2522%253A%2522%25232563eb%2522%252C%2522data%2522%253A%255B%257B%2522x%2522%253A8.98%252C%2522y%2522%253A1.133%257D%252C%257B%2522x%2522%253A7.9%252C%2522y%2522%253A1.132%257D%252C%257B%2522x%2522%253A7.63%252C%2522y%2522%253A1.253%257D%252C%257B%2522x%2522%253A9.18%252C%2522y%2522%253A1.17%257D%252C%257B%2522x%2522%253A7.86%252C%2522y%2522%253A1.117%257D%252C%257B%2522x%2522%253A10.26%252C%2522y%2522%253A1.099%257D%252C%257B%2522x%2522%253A6.19%252C%2522y%2522%253A1.053%257D%252C%257B%2522x%2522%253A8.21%252C%2522y%2522%253A1.095%257D%252C%257B%2522x%2522%253A8.72%252C%2522y%2522%253A1.087%257D%252C%257B%2522x%2522%253A7.22%252C%2522y%2522%253A1.065%257D%252C%257B%2522x%2522%253A7.6%252C%2522y%2522%253A1.045%257D%252C%257B%2522x%2522%253A7.93%252C%2522y%2522%253A1.041%257D%252C%257B%2522x%2522%253A10.77%252C%2522y%2522%253A1.127%257D%252C%257B%2522x%2522%253A12.78%252C%2522y%2522%253A1.207%257D%252C%257B%2522x%2522%253A12.01%252C%2522y%2522%253A1.172%257D%252C%257B%2522x%2522%253A10.65%252C%2522y%2522%253A1.149%257D%252C%257B%2522x%2522%253A11.14%252C%2522y%2522%253A1.12%257D%252C%257B%2522x%2522%253A9.97%252C%2522y%2522%253A1.162%257D%252C%257B%2522x%2522%253A16.96%252C%2522y%2522%253A1.216%257D%252C%257B%2522x%2522%253A14.39%252C%2522y%2522%253A1.202%257D%252C%257B%2522x%2522%253A17.21%252C%2522y%2522%253A1.273%257D%252C%257B%2522x%2522%253A15.32%252C%2522y%2522%253A1.241%257D%252C%257B%2522x%2522%253A16.87%252C%2522y%2522%253A1.259%257D%252C%257B%2522x%2522%253A17.68%252C%2522y%2522%253A1.258%257D%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522FLy%2520speedup%2520tracks%2520the%2520draft%2520acceptance%2520gain%2520%252848%2520points%252C%2520r%2520%253D%25200.82%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522draft%2520acceptance%2520gain%252C%2520FLy%2520minus%2520standard%2520%2528percentage%2520points%2529%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522min%2522%253A1.0%252C%2522max%2522%253A1.35%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522decode%2520tok%2Fs%252C%2520FLy%2520%2F%2520standard%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522scatter%2522%252C%2522data%2522%253A%257B%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522AMD%2520MI355X%2522%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%252C%2522borderColor%2522%253A%2522%2523dc2626%2522%252C%2522data%2522%253A%255B%257B%2522x%2522%253A7.0%252C%2522y%2522%253A1.103%257D%252C%257B%2522x%2522%253A11.1%252C%2522y%2522%253A1.161%257D%252C%257B%2522x%2522%253A9.0%252C%2522y%2522%253A1.11%257D%252C%257B%2522x%2522%253A8.1%252C%2522y%2522%253A1.168%257D%252C%257B%2522x%2522%253A9.5%252C%2522y%2522%253A1.144%257D%252C%257B%2522x%2522%253A11.6%252C%2522y%2522%253A1.139%257D%252C%257B%2522x%2522%253A9.5%252C%2522y%2522%253A1.082%257D%252C%257B%2522x%2522%253A10.4%252C%2522y%2522%253A1.096%257D%252C%257B%2522x%2522%253A9.7%252C%2522y%2522%253A1.083%257D%252C%257B%2522x%2522%253A11.3%252C%2522y%2522%253A1.116%257D%252C%257B%2522x%2522%253A10.8%252C%2522y%2522%253A1.068%257D%252C%257B%2522x%2522%253A10.0%252C%2522y%2522%253A1.055%257D%252C%257B%2522x%2522%253A15.1%252C%2522y%2522%253A1.172%257D%252C%257B%2522x%2522%253A14.2%252C%2522y%2522%253A1.216%257D%252C%257B%2522x%2522%253A11.2%252C%2522y%2522%253A1.172%257D%252C%257B%2522x%2522%253A11.7%252C%2522y%2522%253A1.124%257D%252C%257B%2522x%2522%253A11.3%252C%2522y%2522%253A1.229%257D%252C%257B%2522x%2522%253A11.9%252C%2522y%2522%253A1.11%257D%252C%257B%2522x%2522%253A16.2%252C%2522y%2522%253A1.251%257D%252C%257B%2522x%2522%253A17.6%252C%2522y%2522%253A1.28%257D%252C%257B%2522x%2522%253A18.1%252C%2522y%2522%253A1.295%257D%252C%257B%2522x%2522%253A16.1%252C%2522y%2522%253A1.264%257D%252C%257B%2522x%2522%253A17.1%252C%2522y%2522%253A1.289%257D%252C%257B%2522x%2522%253A16.0%252C%2522y%2522%253A1.31%257D%255D%257D%252C%257B%2522label%2522%253A%2522NVIDIA%2520B300%2522%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%252C%2522borderColor%2522%253A%2522%25232563eb%2522%252C%2522data%2522%253A%255B%257B%2522x%2522%253A8.98%252C%2522y%2522%253A1.133%257D%252C%257B%2522x%2522%253A7.9%252C%2522y%2522%253A1.132%257D%252C%257B%2522x%2522%253A7.63%252C%2522y%2522%253A1.253%257D%252C%257B%2522x%2522%253A9.18%252C%2522y%2522%253A1.17%257D%252C%257B%2522x%2522%253A7.86%252C%2522y%2522%253A1.117%257D%252C%257B%2522x%2522%253A10.26%252C%2522y%2522%253A1.099%257D%252C%257B%2522x%2522%253A6.19%252C%2522y%2522%253A1.053%257D%252C%257B%2522x%2522%253A8.21%252C%2522y%2522%253A1.095%257D%252C%257B%2522x%2522%253A8.72%252C%2522y%2522%253A1.087%257D%252C%257B%2522x%2522%253A7.22%252C%2522y%2522%253A1.065%257D%252C%257B%2522x%2522%253A7.6%252C%2522y%2522%253A1.045%257D%252C%257B%2522x%2522%253A7.93%252C%2522y%2522%253A1.041%257D%252C%257B%2522x%2522%253A10.77%252C%2522y%2522%253A1.127%257D%252C%257B%2522x%2522%253A12.78%252C%2522y%2522%253A1.207%257D%252C%257B%2522x%2522%253A12.01%252C%2522y%2522%253A1.172%257D%252C%257B%2522x%2522%253A10.65%252C%2522y%2522%253A1.149%257D%252C%257B%2522x%2522%253A11.14%252C%2522y%2522%253A1.12%257D%252C%257B%2522x%2522%253A9.97%252C%2522y%2522%253A1.162%257D%252C%257B%2522x%2522%253A16.96%252C%2522y%2522%253A1.216%257D%252C%257B%2522x%2522%253A14.39%252C%2522y%2522%253A1.202%257D%252C%257B%2522x%2522%253A17.21%252C%2522y%2522%253A1.273%257D%252C%257B%2522x%2522%253A15.32%252C%2522y%2522%253A1.241%257D%252C%257B%2522x%2522%253A16.87%252C%2522y%2522%253A1.259%257D%252C%257B%2522x%2522%253A17.68%252C%2522y%2522%253A1.258%257D%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522FLy%2520speedup%2520tracks%2520the%2520draft%2520acceptance%2520gain%2520%252848%2520points%252C%2520r%2520%253D%25200.82%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522draft%2520acceptance%2520gain%252C%2520FLy%2520minus%2520standard%2520%2528percentage%2520points%2529%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522min%2522%253A1.0%252C%2522max%2522%253A1.35%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522decode%2520tok%2Fs%252C%2520FLy%2520%2F%2520standard%2522%257D%257D%255D%257D%257D%257D" alt="Scatter of FLy speedup against draft acceptance gain across 48 benchmark points" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The speedup tracks the acceptance gain closely: the correlation across the 48 points is r = 0.82. That is what you would expect if FLy converts "extra accepted tokens" into "fewer target passes" with little other overhead. It also tells you where the method pays. A model whose drafter already agrees with it (Qwen3.8 at K=3, only +7.6 to +10 points) gains 6 to 8%. A model with a weaker drafter (Qwen3.5-122B, +16 to +17 points) gains 24 to 28%. A flat 1.2x is not a safe planning number for your own pair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can the accuracy tables show that FLy is safe?
&lt;/h2&gt;

&lt;p&gt;Not by themselves, and the PR does not claim they can. It says the retention figures "are single-pass measurements, not a quality guarantee." The question is how much they can say, so I rebuilt the accuracy table.&lt;/p&gt;

&lt;p&gt;Each model and platform was scored on GSM8K (first 100 questions), MATH-500 (first 100), AIME25 (30) and GPQA-Diamond (198), which is 428 questions, in three decoding modes: target-only, Standard SD and FLy. That is 2 platforms x 4 models x 4 datasets = 32 cells. Standard SD is distribution-preserving by construction, so any gap between it and target-only is noise from the run itself (batched floating-point differences, sampling, a single pass). Here is the mean absolute gap to target-only in each cell, grouped by dataset:&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522GSM8K%2522%252C%2522MATH-500%2522%252C%2522AIME25%2522%252C%2522GPQA-Diamond%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Standard%2520SD%2520vs%2520target-only%2520%2528lossless%2520method%2529%2522%252C%2522data%2522%253A%255B1.75%252C1.0%252C2.08%252C1.89%255D%252C%2522backgroundColor%2522%253A%2522%252364748b%2522%257D%252C%257B%2522label%2522%253A%2522FLy%2520vs%2520target-only%2520%2528lossy%2520method%2529%2522%252C%2522data%2522%253A%255B2.38%252C0.88%252C2.5%252C1.45%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Mean%2520%257Caccuracy%2520gap%257C%2520to%2520target-only%252C%252032%2520model%2Fplatform%2Fdataset%2520cells%2520%2528PR%2520data%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A3%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522mean%2520absolute%2520accuracy%2520gap%2520%2528percentage%2520points%2529%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522GSM8K%2522%252C%2522MATH-500%2522%252C%2522AIME25%2522%252C%2522GPQA-Diamond%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Standard%2520SD%2520vs%2520target-only%2520%2528lossless%2520method%2529%2522%252C%2522data%2522%253A%255B1.75%252C1.0%252C2.08%252C1.89%255D%252C%2522backgroundColor%2522%253A%2522%252364748b%2522%257D%252C%257B%2522label%2522%253A%2522FLy%2520vs%2520target-only%2520%2528lossy%2520method%2529%2522%252C%2522data%2522%253A%255B2.38%252C0.88%252C2.5%252C1.45%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Mean%2520%257Caccuracy%2520gap%257C%2520to%2520target-only%252C%252032%2520model%2Fplatform%2Fdataset%2520cells%2520%2528PR%2520data%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A3%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522mean%2520absolute%2520accuracy%2520gap%2520%2528percentage%2520points%2529%2522%257D%257D%255D%257D%257D%257D" alt="Mean absolute accuracy gap to target-only, by dataset, for standard and FLy decoding" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&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;Standard SD (lossless)&lt;/th&gt;
&lt;th&gt;FLy (lossy)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mean absolute gap to target-only, 32 cells&lt;/td&gt;
&lt;td&gt;1.68 pp&lt;/td&gt;
&lt;td&gt;1.80 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Largest single-cell gap&lt;/td&gt;
&lt;td&gt;6.67 pp&lt;/td&gt;
&lt;td&gt;10.00 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cells where its gap is larger than the other method's&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;12 (9 tied)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cells below / above / equal to target&lt;/td&gt;
&lt;td&gt;8 / 12 / 12&lt;/td&gt;
&lt;td&gt;13 / 11 / 8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The largest lossless gap is DeepSeek-V4-Flash on B300 for AIME25: target 26/30, Standard SD 28/30. The largest FLy gap is Qwen3.5-122B on MI355X for GSM8K: target 82, FLy 92 out of 100, where the lossy method scored 10 points better. Neither number is evidence about the method. They show that with 30 to 198 questions per cell, one cell can move by several points for reasons unrelated to the verifier.&lt;/p&gt;

&lt;p&gt;Pooling helps a little. Summed over all 8 model and platform pairs (3,424 scored answers per decoding mode):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Target-only&lt;/td&gt;
&lt;td&gt;3,040&lt;/td&gt;
&lt;td&gt;88.79%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard SD&lt;/td&gt;
&lt;td&gt;3,067&lt;/td&gt;
&lt;td&gt;89.57%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FLy&lt;/td&gt;
&lt;td&gt;3,059&lt;/td&gt;
&lt;td&gt;89.34%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;FLy is 19 answers above target-only, and the lossless method is 27 above it. A lossless method cannot genuinely beat the target on average, so the 0.8-point spread is the noise scale of this setup. A rough 95% interval for the difference between two unpaired accuracies near 90% is about plus or minus 4.0 points at 428 questions, 5.9 at 198, 8.3 at 100 and 15 at 30 (normal approximation, poor at n=30 near the ceiling). A real 1 to 2 point quality loss would sit inside all of those bands.&lt;/p&gt;

&lt;p&gt;None of this says FLy hurts quality. The evidence is consistent with a small loss and with none, and the PR's minimum retention (96.43% on AMD, 96.30% on NVIDIA) is a one-problem difference on AIME25, where each question is 3.3 points.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many questions does a real quality gate need?
&lt;/h2&gt;

&lt;p&gt;The fix is a paired design. Run both configurations on the same questions with the same seeds, record per-question correctness, and count only the discordant pairs: questions FLy lost and questions it gained. McNemar's exact test then asks whether the net movement is bigger than chance. Pairing removes the between-question variance that makes the unpaired interval so wide.&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;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;paired_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;base, cand: per-question 0/1 correctness for the SAME questions and seeds.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;lost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&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;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cand&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;x&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;gained&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&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;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cand&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;y&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&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;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lost&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;gained&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="n"&gt;lost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gained&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;p&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="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;comb&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="n"&gt;i&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;i&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;k&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="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;n&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;n&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gained&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gained&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gained&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exact_p&lt;/span&gt;&lt;span class="sh"&gt;"&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;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&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;questions_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net_loss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discordant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z_a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z_b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.8416&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# two-sided alpha 0.05, power 0.80; discordant = fraction of questions that flip either way
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;z_a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discordant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                      &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;z_b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discordant&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;net_loss&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&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;net_loss&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran this on synthetic data (illustrative: 428 questions at 89% base accuracy, with 5% of answers randomly flipped). It reported 25 lost, 2 gained, exact p below 0.0001. A deliberately damaged candidate is caught easily. The harder question is the subtle loss, and the sample size you need depends on how often the two runs disagree at all:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Net loss to detect&lt;/th&gt;
&lt;th&gt;Discordant 4%&lt;/th&gt;
&lt;th&gt;Discordant 8%&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3 points&lt;/td&gt;
&lt;td&gt;347 questions&lt;/td&gt;
&lt;td&gt;696 questions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 points&lt;/td&gt;
&lt;td&gt;783 questions&lt;/td&gt;
&lt;td&gt;1,568 questions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 point&lt;/td&gt;
&lt;td&gt;3,138 questions&lt;/td&gt;
&lt;td&gt;6,277 questions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Normal-approximation sample sizes from the formula above; the discordant fraction is an assumption you measure by running Standard SD twice on a pilot set.) At a 6% flip rate, a 2-point loss needs about 1,175 questions and a 1-point loss about 4,700. A 428-question suite per cell cannot resolve either, which is why a short eval is a smoke test and not a gate. Build the gate from your own task mix, because the entropy rule fires more often on open-ended generation than on short arithmetic answers, and your traffic is neither GSM8K nor GPQA.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the bigger win in the same release?
&lt;/h2&gt;

&lt;p&gt;PR #59468 (merged 2026-10-15) fixes how the scheduler charges token budget for block-diffusion drafters. DFlash and DSpark were reserving draft slots in each request's share of &lt;code&gt;max_num_batched_tokens&lt;/code&gt; even though they run their draft pass separately, not inside the target batch. For DSpark with K=5, a decode request was charged 10 tokens instead of 6. The PR sets the reserved slots to 0 and instead caps running requests at &lt;code&gt;max_num_batched_tokens // num_lookahead_tokens&lt;/code&gt;, so the separate draft buffers still fit.&lt;/p&gt;

&lt;p&gt;The reported end-to-end result (DeepSeek-V4.1-Flash with DSpark, TP4 on one GB300, &lt;code&gt;--max-num-batched-tokens 512&lt;/code&gt;, one run per side):&lt;/p&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;main&lt;/th&gt;
&lt;th&gt;PR #59468&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Max running requests&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tok/s&lt;/td&gt;
&lt;td&gt;1,373&lt;/td&gt;
&lt;td&gt;3,390&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPOT p50 / p99 (ms)&lt;/td&gt;
&lt;td&gt;29.2 / 63.5&lt;/td&gt;
&lt;td&gt;15.7 / 36.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT p50 (s)&lt;/td&gt;
&lt;td&gt;93.6&lt;/td&gt;
&lt;td&gt;30.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acceptance length&lt;/td&gt;
&lt;td&gt;4.72&lt;/td&gt;
&lt;td&gt;4.73&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Running%2520requests%2522%252C%2522Output%2520tok%2Fs%2522%252C%2522TPOT%2520p50%2520%2528lower%2520is%2520better%252C%2520inverted%2529%2522%252C%2522TTFT%2520p50%2520%2528lower%2520is%2520better%252C%2520inverted%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522PR%2520%252359468%2520%2F%2520main%2520%25281.0%2520%253D%2520no%2520change%2529%2522%252C%2522data%2522%253A%255B1.23%252C2.47%252C1.86%252C3.12%255D%252C%2522backgroundColor%2522%253A%255B%2522%252364748b%2522%252C%2522%252316a34a%2522%252C%2522%25232563eb%2522%252C%2522%2523f59e0b%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522DSpark%2520K%253D5%252C%2520512-token%2520budget%253A%2520a%252023%2525%2520admission%2520gain%2520became%25202.5x%2520throughput%2522%252C%2522fontSize%2522%253A16%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%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%2522ratio%2520vs%2520main%2520%2528one%2520run%2520per%2520side%2529%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Running%2520requests%2522%252C%2522Output%2520tok%2Fs%2522%252C%2522TPOT%2520p50%2520%2528lower%2520is%2520better%252C%2520inverted%2529%2522%252C%2522TTFT%2520p50%2520%2528lower%2520is%2520better%252C%2520inverted%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522PR%2520%252359468%2520%2F%2520main%2520%25281.0%2520%253D%2520no%2520change%2529%2522%252C%2522data%2522%253A%255B1.23%252C2.47%252C1.86%252C3.12%255D%252C%2522backgroundColor%2522%253A%255B%2522%252364748b%2522%252C%2522%252316a34a%2522%252C%2522%25232563eb%2522%252C%2522%2523f59e0b%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522DSpark%2520K%253D5%252C%2520512-token%2520budget%253A%2520a%252023%2525%2520admission%2520gain%2520became%25202.5x%2520throughput%2522%252C%2522fontSize%2522%253A16%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%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%2522ratio%2520vs%2520main%2520%2528one%2520run%2520per%2520side%2529%2522%257D%257D%255D%257D%257D%257D" alt="Ratios of PR to main for running requests, throughput, TPOT and TTFT" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Admission rose 1.23x and throughput 2.47x. The PR's explanation for the gap is scheduling, not drafting: on main, the 2 to 32 leftover tokens split each new prefill into small chunks, so most steps mixed prefill into the decode batch and missed the full decode CUDA graph (ITL p50 169 ms on main against 30 ms with the fix). Acceptance length is unchanged at 4.72 versus 4.73, and greedy outputs for 20 fixed prompts were byte-identical (3,197 completion tokens). The author reports that an earlier revision, measured with two interleaved runs per side, was within 2% of these numbers. DFlash was not run end to end.&lt;/p&gt;

&lt;p&gt;The admission arithmetic is easy to check:&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;max_running&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_seqs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_decode_request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&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;max_seqs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;tokens_per_decode_request&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;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;charge&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;before, K=5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&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;after, K=5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&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="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;max_running&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&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;charge&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;max_running&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&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="n"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# before, K=5 51 51
# after, K=5 64 85
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple division gives 51 for the old charge where the PR observed 52, a remainder effect I did not trace. For the new charge with &lt;code&gt;--max-num-seqs 128&lt;/code&gt; it gives 85, which matches the PR exactly, and the new request cap of 102 (512 // 5) does not bind. You can see the cap in effect at startup: the log line reads like "The dspark draft pass needs 5 query tokens per request within max_num_batched_tokens=512. Overriding max_num_active_seqs from 128 to 102."&lt;/p&gt;

&lt;p&gt;The operational lesson generalizes beyond drafters. If you run speculative decoding and your running-request count plateaus well below &lt;code&gt;--max-num-seqs&lt;/code&gt; under saturating load, token-budget accounting is binding and the number you set is not the number you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8000/metrics | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^vllm:num_requests_running'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What else in 0.33 touches a speculative serving stack?
&lt;/h2&gt;

&lt;p&gt;Medusa and the MLP speculator were removed (#61701), lookahead block hashes for EAGLE and MTP prefix-cache hits are now on by default (#61589), CUDA 12.9 images are no longer published, and the NIXL connector needs NIXL 1.5 or newer. Read those before upgrading a fleet that uses drafters.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rollout checklist for FLy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Leave standard verification on&lt;/strong&gt; wherever outputs feed an unreviewed downstream decision. That is my recommendation, not the PR's, and the reason is the lack of a distribution guarantee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce the configuration that was measured&lt;/strong&gt;, including &lt;code&gt;fly_window_size&lt;/code&gt;, or measure your own window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a paired eval from your traffic&lt;/strong&gt;, with at least 1,000 questions to resolve a 2-point loss, fixed seeds, and per-question results stored so you can run the exact test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track acceptance, not only tokens/s.&lt;/strong&gt; The speedup follows the acceptance gain, so a workload where the draft already agrees will show little benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on the one-time lossy warning&lt;/strong&gt; in logs so a config drift does not turn the method on silently.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I did not run FLy myself. Every number above comes from the PR authors' tables or from arithmetic on them, and the throughput results are their measurements on their hardware.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is FLy on by default in vLLM 0.33?&lt;/strong&gt;&lt;br&gt;
No. Standard verification is the default, and FLy requires &lt;code&gt;rejection_sample_method&lt;/code&gt; set to &lt;code&gt;"fly"&lt;/code&gt; in the speculative config, with at least two speculative tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does FLy preserve the target model's output distribution?&lt;/strong&gt;&lt;br&gt;
No. The PR says it does not and that it may reduce output quality. Standard speculative decoding does preserve the distribution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much faster is it?&lt;/strong&gt;&lt;br&gt;
The PR reports 1.041x to 1.310x over standard speculative decoding across 48 model, platform and batch combinations, with a median of 1.147x in my parse of its table. The gain follows the draft acceptance gain, so it varies by model pair.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do the published accuracy results show no quality loss?&lt;/strong&gt;&lt;br&gt;
They show no loss that a single run on 30 to 198 questions per dataset could detect. In the same tables, the lossless method differs from target-only by a mean 1.68 points, close to FLy's 1.80, so the noise is of similar size to any plausible effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many questions do I need to detect a 2-point accuracy loss?&lt;/strong&gt;&lt;br&gt;
About 1,175 paired questions at 80% power and 6% of questions flipping either way, rising to about 1,570 at 8%. Use a paired exact test on per-question results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the scheduler fix need a config change?&lt;/strong&gt;&lt;br&gt;
No. For DFlash and DSpark the scheduler lowers the active-request cap at startup when needed and logs it, and it does not alter &lt;code&gt;--max-num-seqs&lt;/code&gt; itself.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/speculative-decoding-at-high-batch-size-where-the-3x-speedup-turns-into-a-tax-3da5"&gt;Speculative Decoding at High Batch Size: Where the 3x Speedup Turns Into a Tax&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/your-llm-serving-engine-ships-every-13-days-reading-vllms-breaking-change-lists-as-upgrade-risk-1815"&gt;Your LLM Serving Engine Ships Every 13 Days: Reading vLLM's Breaking-Change Lists as Upgrade Risk Data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;vLLM v0.33.0 release notes: &lt;a href="https://github.com/vllm-project/vllm/releases" rel="noopener noreferrer"&gt;https://github.com/vllm-project/vllm/releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FLy pull request #53987: &lt;a href="https://github.com/vllm-project/vllm/pull/53987" rel="noopener noreferrer"&gt;https://github.com/vllm-project/vllm/pull/53987&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Scheduler budget pull request #59468: &lt;a href="https://github.com/vllm-project/vllm/pull/59468" rel="noopener noreferrer"&gt;https://github.com/vllm-project/vllm/pull/59468&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Paper, "Training-Free Loosely Speculative Decoding: Accepting Semantically Correct Drafts Beyond Exact Match": &lt;a href="https://arxiv.org/abs/2511.22972" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2511.22972&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>vllm</category>
    </item>
    <item>
      <title>vLLM Fast Start Explained: What Preload and Snapshots Save, What They Cannot, and the Fingerprint Gap</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Tue, 03 Nov 2026 14:12:04 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/vllm-fast-start-explained-what-preload-and-snapshots-save-what-they-cannot-and-the-fingerprint-33bb</link>
      <guid>https://dev.to/wolfsea2357/vllm-fast-start-explained-what-preload-and-snapshots-save-what-they-cannot-and-the-fingerprint-33bb</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What shipped:&lt;/strong&gt; vLLM 0.31.0 (2026-10-05) added &lt;code&gt;vllm preload&lt;/code&gt;, a per-GPU daemon that keeps post-quantized weights resident so a restarting engine maps them over CUDA IPC instead of reloading from disk, plus experimental CRIU-based initialized-engine snapshots. The design goal, in the original PR's words, is that "engine restarts are dominated by weight loading from disk."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it does not do:&lt;/strong&gt; the daemon is node-local and must itself load from disk once, so it speeds up restarts on a GPU that already holds the weights. It does not speed up scaling out to a fresh node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What a faster start is worth (illustrative simulation):&lt;/strong&gt; with a 1.5x traffic burst and a 10 s queue-wait SLO, cutting replica startup from 300 s to 45 s raises the safe baseline utilization from 69% to 83%. For 100 GPUs of baseline load that is 145 GPUs versus 120. At 2x bursts it is 196 versus 159.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A correctness gap to know about:&lt;/strong&gt; as of 2026-11-03, issue #59647 is still open. The weight-cache fingerprint hashes safetensors headers, not weight values, so a daemon holding checkpoint A can serve an engine started on checkpoint B without any warning. The proposed fix samples only 1 KiB per tensor, which on Qwen3-8B is 0.002% of the file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What did vLLM actually ship for fast restarts?
&lt;/h2&gt;

&lt;p&gt;The feature landed in two pieces. The first is the weight cache daemon, merged as PR #54921 on 2026-09-04 and exposed as &lt;code&gt;vllm preload&lt;/code&gt; in v0.31.0. One daemon process per GPU loads its tensor-parallel shard, runs quantization post-processing, and exports every parameter as a CUDA IPC handle. An engine started with &lt;code&gt;--load-format ipc_cache&lt;/code&gt; builds the model on the meta device and asks the daemon for the tensors. The second piece is &lt;code&gt;vllm snapshot create/restore&lt;/code&gt;, which uses CRIU to freeze a fully initialized engine and bring it back before the HTTP server binds.&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;# one daemon per GPU, loads from disk once&lt;/span&gt;
vllm preload &lt;span class="nt"&gt;--model&lt;/span&gt; meta-llama/Llama-3.1-8B-Instruct &lt;span class="nt"&gt;--tensor-parallel-size&lt;/span&gt; 4

&lt;span class="c"&gt;# engine restarts map the resident weights instead of reading disk&lt;/span&gt;
vllm serve meta-llama/Llama-3.1-8B-Instruct &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tensor-parallel-size&lt;/span&gt; 4 &lt;span class="nt"&gt;--load-format&lt;/span&gt; ipc_cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two mechanisms keep very different amounts of state, and their constraints differ accordingly. Everything in this table comes from the vLLM docs pages for the two features as of today.&lt;/p&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;Weight preload&lt;/th&gt;
&lt;th&gt;Initialized snapshot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What persists&lt;/td&gt;
&lt;td&gt;Post-quantized weights in GPU memory (and FlashInfer autotune results on disk)&lt;/td&gt;
&lt;td&gt;The whole engine process tree and CUDA state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Same node, same user, Unix socket&lt;/td&gt;
&lt;td&gt;Same host, GPU, driver, kernel, vLLM version, engine args&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallelism&lt;/td&gt;
&lt;td&gt;Tensor, expert and data parallel, including multi-node (one launcher per node)&lt;/td&gt;
&lt;td&gt;TP1, one NVIDIA GPU only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validated&lt;/td&gt;
&lt;td&gt;PR test results include TP2 and MTP draft models&lt;/td&gt;
&lt;td&gt;Only dense float16 TP1; acceptance run was Qwen3-0.6B on an H200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost while idle&lt;/td&gt;
&lt;td&gt;The daemon pins the weights in GPU memory&lt;/td&gt;
&lt;td&gt;Disk: artifact size can approach process plus GPU memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fallback&lt;/td&gt;
&lt;td&gt;Disk load if daemon missing or fingerprint mismatched&lt;/td&gt;
&lt;td&gt;None: restore fails closed before CRIU runs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One number from outside vLLM gives a sense of scale for the weight-load part alone. The README of a separate PyPI plugin built on the same idea, &lt;code&gt;vllm-ipc-cache&lt;/code&gt; 0.2.0, reports a 27B model at tensor parallel 4 on a MetaX N300-A dropping from 35.80 s to 0.47 s. It is one vendor-reported run on non-NVIDIA hardware and covers the weight-attach step only, not an end-to-end restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which startup events does a faster restart actually help?
&lt;/h2&gt;

&lt;p&gt;Preload covers only some of the reasons you start an engine.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rolling upgrades and config changes.&lt;/strong&gt; The daemon outlives the engine, so you restart the engine and keep the weights. This is the cleanest win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine crashes where the GPU is healthy.&lt;/strong&gt; The daemon is a separate process, so a crashed engine can remap the same weights. If the fault is at the GPU level, the daemon probably goes down with it and you are back to a disk load. I did not test that case. The docs say that if any daemon rank exits, you should restart the whole preload group before attaching engines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale-out to a new node.&lt;/strong&gt; Not helped. CUDA IPC handles are node-local, the docs state that each node serves only its own GPUs, and the daemon itself loads from disk. The one scale-out feature is warmup-cache seeding: a new daemon can fetch rank-matched FlashInfer autotune tables from a source replica over an authenticated, unencrypted TCP channel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-sharing a GPU between models.&lt;/strong&gt; This is what snapshots are for: "repeatedly activating the same model and engine configuration on the same machine."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The autotune detail matters more than it looks. The docs say that once weights come from the daemon, the FlashInfer autotune pass "dominates what is left of the startup time" on a large MoE model, which is why &lt;code&gt;vllm preload&lt;/code&gt; now runs it at preload time. v0.31.0 also parallelized Triton warmup compilation (#58582). So the honest accounting of a restart is weights, then autotune, then warmup and CUDA graph work, and each has its own fix.&lt;/p&gt;

&lt;p&gt;Here is the arithmetic that makes rolling upgrades the main prize. A rolling restart of 16 replicas, one at a time, runs the fleet at 15/16 capacity (93.75%) for 16 times the per-replica restart time.&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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522300%2520s%2522%252C%2522120%2520s%2522%252C%252245%2520s%2522%252C%252210%2520s%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522minutes%2522%252C%2522data%2522%253A%255B80.0%252C32.0%252C12.0%252C2.7%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523dc2626%2522%252C%2522%2523f59e0b%2522%252C%2522%25232563eb%2522%252C%2522%252316a34a%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Arithmetic%253A%2520rolling%2520restart%2520of%252016%2520replicas%252C%2520one%2520at%2520a%2520time%2520%2528minutes%2520at%252015%2F16%2520capacity%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522per-replica%2520restart%2520time%2522%257D%257D%255D%252C%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%2522minutes%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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522300%2520s%2522%252C%2522120%2520s%2522%252C%252245%2520s%2522%252C%252210%2520s%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522minutes%2522%252C%2522data%2522%253A%255B80.0%252C32.0%252C12.0%252C2.7%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523dc2626%2522%252C%2522%2523f59e0b%2522%252C%2522%25232563eb%2522%252C%2522%252316a34a%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Arithmetic%253A%2520rolling%2520restart%2520of%252016%2520replicas%252C%2520one%2520at%2520a%2520time%2520%2528minutes%2520at%252015%2F16%2520capacity%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522per-replica%2520restart%2520time%2522%257D%257D%255D%252C%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%2522minutes%2522%257D%257D%255D%257D%257D%257D" alt="Rolling restart of 16 replicas, minutes at 15/16 capacity, by per-replica restart time" width="1800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At 300 s per replica that is 80 minutes of reduced capacity; at 45 s it is 12 minutes; at 10 s it is 2.7 minutes. If you ship engine upgrades every couple of weeks, that is the number to compare against the cost of keeping a daemon pinned.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much utilization does a faster start buy you?
&lt;/h2&gt;

&lt;p&gt;Startup time matters to capacity planning through one mechanism: during a burst, demand exceeds capacity for roughly the detection lag plus the startup time, and the backlog built in that window has to drain inside your queue-wait SLO. If the burst multiplies load by B, baseline utilization is u, the window is W seconds and the SLO is S seconds of queue wait, the backlog stays inside the SLO when:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(B * u - 1) * W &amp;lt;= S      so      u_max ~= (1 + S / W) / B,   W ~= startup + detection lag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked this with a fluid-queue simulation (illustrative, not a measurement of any real fleet): 16 replicas, a 30-minute run, demand ramping by a factor B over 30 s, a scaler that averages demand over 30 s, checks every 10 s and restores the baseline utilization, and a violation counted for every second the queue wait exceeds 10 s. The core loop:&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;for&lt;/span&gt; &lt;span class="n"&gt;t&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;T_END&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;demand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lam0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;burst&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="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# replicas that finished starting
&lt;/span&gt;    &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;r&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&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;t&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ceil&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;hist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;MU&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;        &lt;span class="c1"&gt;# restore baseline utilization
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;pending&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;t&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;startup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;want&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;max&lt;/span&gt;&lt;span class="p"&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;q&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;lam&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;MU&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;# fluid backlog
&lt;/span&gt;    &lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="o"&gt;+=&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="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="n"&gt;MU&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each startup time I searched for the highest baseline utilization with zero violation seconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Replica startup&lt;/th&gt;
&lt;th&gt;Max safe baseline utilization, burst x1.5&lt;/th&gt;
&lt;th&gt;GPUs for 100 GPUs of load&lt;/th&gt;
&lt;th&gt;Max safe utilization, burst x2.0&lt;/th&gt;
&lt;th&gt;GPUs for 100 GPUs of load&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;300 s&lt;/td&gt;
&lt;td&gt;0.69&lt;/td&gt;
&lt;td&gt;145&lt;/td&gt;
&lt;td&gt;0.51&lt;/td&gt;
&lt;td&gt;196&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120 s&lt;/td&gt;
&lt;td&gt;0.73&lt;/td&gt;
&lt;td&gt;137&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;185&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;45 s&lt;/td&gt;
&lt;td&gt;0.83&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;0.63&lt;/td&gt;
&lt;td&gt;159&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 s&lt;/td&gt;
&lt;td&gt;0.99 (search cap)&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;118&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The closed form with W set to startup plus 45 s predicts 0.686 for the 300 s row against 0.69 in the simulation. It is conservative for fast starts, because the ramp is finite and the scaler orders in steps; at 45 s it predicts 0.74 against 0.83. The 10 s row is the idealized limit, since it ignores KV cache warmup, load balancer registration and anything else that happens after the process is up.&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%3D1000%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522300%2520s%2522%252C%2522120%2520s%2522%252C%252245%2520s%2522%252C%252210%2520s%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Burst%2520x1.5%2522%252C%2522data%2522%253A%255B145%252C137%252C120%252C101%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522Burst%2520x2.0%2522%252C%2522data%2522%253A%255B196%252C185%252C159%252C118%255D%252C%2522backgroundColor%2522%253A%2522%2523f59e0b%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%253A%2520GPUs%2520needed%2520to%2520carry%2520100%2520GPUs%2520of%2520baseline%2520load%252C%2520by%2520replica%2520startup%2520time%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522replica%2520startup%2520time%2520%2528seconds%2529%2522%257D%257D%255D%252C%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%2522GPUs%2520for%2520a%252010%2520s%2520queue-wait%2520SLO%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%3D1000%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522300%2520s%2522%252C%2522120%2520s%2522%252C%252245%2520s%2522%252C%252210%2520s%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Burst%2520x1.5%2522%252C%2522data%2522%253A%255B145%252C137%252C120%252C101%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522Burst%2520x2.0%2522%252C%2522data%2522%253A%255B196%252C185%252C159%252C118%255D%252C%2522backgroundColor%2522%253A%2522%2523f59e0b%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%253A%2520GPUs%2520needed%2520to%2520carry%2520100%2520GPUs%2520of%2520baseline%2520load%252C%2520by%2520replica%2520startup%2520time%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522replica%2520startup%2520time%2520%2528seconds%2529%2522%257D%257D%255D%252C%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%2522GPUs%2520for%2520a%252010%2520s%2520queue-wait%2520SLO%2522%257D%257D%255D%257D%257D%257D" alt="GPUs needed to carry 100 GPUs of baseline load by replica startup time" width="2000" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same data as violation seconds shows how sharply the cliff moves. At 80% baseline utilization and a 1.5x burst, a 300 s startup spends 413 s over the SLO with a peak queue wait of 57 s. At 120 s it is 113 s over, and at 45 s it is zero violations with a peak wait of 7.6 s.&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25220.6%2522%252C%25220.65%2522%252C%25220.7%2522%252C%25220.75%2522%252C%25220.8%2522%252C%25220.85%2522%252C%25220.9%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522300%2520s%2520startup%2522%252C%2522data%2522%253A%255B0%252C0%252C102%252C288%252C413%252C587%252C913%255D%252C%2522borderColor%2522%253A%2522%2523dc2626%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522120%2520s%2520startup%2522%252C%2522data%2522%253A%255B0%252C0%252C0%252C47%252C113%252C187%252C313%255D%252C%2522borderColor%2522%253A%2522%2523f59e0b%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%252245%2520s%2520startup%2522%252C%2522data%2522%253A%255B0%252C0%252C0%252C0%252C0%252C15%252C63%255D%252C%2522borderColor%2522%253A%2522%252316a34a%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%252210%2520s%2520startup%2522%252C%2522data%2522%253A%255B0%252C0%252C0%252C0%252C0%252C0%252C0%255D%252C%2522borderColor%2522%253A%2522%25232563eb%2522%252C%2522fill%2522%253Afalse%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%253A%2520seconds%2520over%2520a%252010%2520s%2520queue-wait%2520SLO%2520during%2520a%2520x1.5%2520burst%2522%252C%2522fontSize%2522%253A17%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522baseline%2520fleet%2520utilization%2520before%2520the%2520burst%2522%257D%257D%255D%252C%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%2522violation%2520seconds%2520in%252030%2520minutes%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25220.6%2522%252C%25220.65%2522%252C%25220.7%2522%252C%25220.75%2522%252C%25220.8%2522%252C%25220.85%2522%252C%25220.9%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522300%2520s%2520startup%2522%252C%2522data%2522%253A%255B0%252C0%252C102%252C288%252C413%252C587%252C913%255D%252C%2522borderColor%2522%253A%2522%2523dc2626%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522120%2520s%2520startup%2522%252C%2522data%2522%253A%255B0%252C0%252C0%252C47%252C113%252C187%252C313%255D%252C%2522borderColor%2522%253A%2522%2523f59e0b%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%252245%2520s%2520startup%2522%252C%2522data%2522%253A%255B0%252C0%252C0%252C0%252C0%252C15%252C63%255D%252C%2522borderColor%2522%253A%2522%252316a34a%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%252210%2520s%2520startup%2522%252C%2522data%2522%253A%255B0%252C0%252C0%252C0%252C0%252C0%252C0%255D%252C%2522borderColor%2522%253A%2522%25232563eb%2522%252C%2522fill%2522%253Afalse%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%253A%2520seconds%2520over%2520a%252010%2520s%2520queue-wait%2520SLO%2520during%2520a%2520x1.5%2520burst%2522%252C%2522fontSize%2522%253A17%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522baseline%2520fleet%2520utilization%2520before%2520the%2520burst%2522%257D%257D%255D%252C%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%2522violation%2520seconds%2520in%252030%2520minutes%2522%257D%257D%255D%257D%257D%257D" alt="Seconds over a 10 s queue-wait SLO during a x1.5 burst, by baseline utilization and startup time" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two readings follow. First, going from 300 s to 45 s buys about 17% to 19% fewer GPUs in these scenarios, which is real money but not a revolution. Second, those gains apply to scale-out, and preload does not accelerate scale-out to fresh nodes. Where this model really applies is a parked warm replica on the same host (a daemon on an idle GPU, or a snapshot on disk), and that parked GPU costs about the same as the headroom replica you were trying to avoid. For most fleets the dependable return is the rolling-restart arithmetic above, not the autoscaling story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can the weight cache serve the wrong checkpoint?
&lt;/h2&gt;

&lt;p&gt;It can today, in a specific case, and the project knows. The fingerprint that decides whether a daemon's weights match the engine covers architecture, TP and DP layout, dtype, quantization, RoPE parameters, KV cache dtype and vLLM version. For the checkpoint itself it hashes each safetensors shard's header: tensor names, dtypes, shapes and byte offsets. A header contains no weight values. Issue #59647, filed 2026-10-01 and still open on 2026-11-03, reports that a base model and its fine-tune therefore get the same key. If a daemon preloaded from A is still running and an engine starts on B with &lt;code&gt;ipc_cache&lt;/code&gt;, the daemon serves A's tensors, the engine never reads B from disk, and nothing is logged. A write-up on dev.to by a separate author reproduced it on 0.30.0 by zeroing one norm tensor while leaving the header intact. The docs promise a safe fallback on mismatch, which is why this surprises people.&lt;/p&gt;

&lt;p&gt;The open fix, PR #59648, adds file size plus the first 1 KiB of every tensor to the hash. I re-implemented both schemes (the logic is small) and ran them on eight 1024x1024 float16 tensors, then changed the values in different ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change to the checkpoint&lt;/th&gt;
&lt;th&gt;Header-only hash detects it&lt;/th&gt;
&lt;th&gt;Sampled 1 KiB hash detects it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Noise added to every tensor (a dense fine-tune)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One whole tensor shifted by a constant&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First row of one tensor zeroed&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row 500 of one tensor zeroed&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One element at row 900 changed&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Byte-identical copy in another directory&lt;/td&gt;
&lt;td&gt;same key (wanted)&lt;/td&gt;
&lt;td&gt;same key (wanted)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The proposal catches realistic fine-tunes, because training changes nearly every value, including each tensor's first kilobyte. It cannot catch a surgical edit that leaves the head of each tensor alone. The reason is coverage: on the real headers I fetched, Qwen3-0.6B has 311 tensors and 1.50 GB, so 1 KiB per tensor is 0.018% of the file; Qwen3-8B is 0.0022%; Qwen3-32B has 707 tensors over 65.5 GB and the sample is 0.00095%.&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Qwen3-0.6B%2522%252C%2522Qwen3-8B%2522%252C%2522Qwen3-32B%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Checkpoint%2520bytes%2522%252C%2522data%2522%253A%255B1503264768%252C16381470720%252C65524246528%255D%252C%2522backgroundColor%2522%253A%2522%252364748b%2522%257D%252C%257B%2522label%2522%253A%2522Bytes%2520sampled%2520by%2520the%2520proposed%2520fingerprint%2520%25281%2520KiB%2520per%2520tensor%2529%2522%252C%2522data%2522%253A%255B275456%252C353280%252C625664%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Real%2520safetensors%2520headers%253A%2520bytes%2520in%2520the%2520file%2520vs%2520bytes%2520the%2520fingerprint%2520reads%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522type%2522%253A%2522logarithmic%2522%252C%2522ticks%2522%253A%257B%2522min%2522%253A100000%252C%2522max%2522%253A100000000000%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522bytes%2520%2528log%2520scale%2529%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Qwen3-0.6B%2522%252C%2522Qwen3-8B%2522%252C%2522Qwen3-32B%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Checkpoint%2520bytes%2522%252C%2522data%2522%253A%255B1503264768%252C16381470720%252C65524246528%255D%252C%2522backgroundColor%2522%253A%2522%252364748b%2522%257D%252C%257B%2522label%2522%253A%2522Bytes%2520sampled%2520by%2520the%2520proposed%2520fingerprint%2520%25281%2520KiB%2520per%2520tensor%2529%2522%252C%2522data%2522%253A%255B275456%252C353280%252C625664%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Real%2520safetensors%2520headers%253A%2520bytes%2520in%2520the%2520file%2520vs%2520bytes%2520the%2520fingerprint%2520reads%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522type%2522%253A%2522logarithmic%2522%252C%2522ticks%2522%253A%257B%2522min%2522%253A100000%252C%2522max%2522%253A100000000000%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522bytes%2520%2528log%2520scale%2529%2522%257D%257D%255D%257D%257D%257D" alt="Bytes in the checkpoint versus bytes sampled by the proposed fingerprint, on Qwen3-0.6B, 8B and 32B" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is a fine tradeoff for a startup-path check, since reading everything would defeat the purpose. The practical conclusion is not to rely on the fingerprint for identity. Tie the daemon's lifetime to a digest you computed once, at promotion time, with the full file bytes. SHA-256 ran at about 1.8 GB/s on my PC (median of five runs on an in-memory buffer), so a 65.5 GB checkpoint is roughly 36 s of CPU time if the disk can feed it, paid once in CI and not on every restart.&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;# at promotion time, once per checkpoint&lt;/span&gt;
&lt;span class="nv"&gt;DIGEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sha256sum&lt;/span&gt; /models/qwen3-8b/&lt;span class="k"&gt;*&lt;/span&gt;.safetensors | &lt;span class="nb"&gt;sha256sum&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-c1-16&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;SOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/run/user/&lt;span class="nv"&gt;$UID&lt;/span&gt;/vllm-wc/&lt;span class="nv"&gt;$DIGEST&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 700 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

vllm preload &lt;span class="nt"&gt;--model&lt;/span&gt; /models/qwen3-8b &lt;span class="nt"&gt;--weight-cache-socket-dir&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
vllm serve /models/qwen3-8b &lt;span class="nt"&gt;--load-format&lt;/span&gt; ipc_cache &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--model-loader-extra-config&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;socket_dir&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An engine pointed at a different checkpoint computes a different digest, looks in a different directory, finds no socket, and falls back to disk, which is the documented behavior for an unavailable daemon. I used only flags from the preload docs and did not run this against a GPU engine for this post. Add a temperature-0 canary request per checkpoint after every start; the snapshot feature does exactly that internally, comparing a recorded token and log probability before it returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pre-flight checklist for adopting fast start
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Count your restart events by type for a month: upgrades, crashes, scale-outs. Preload only pays on the first two.&lt;/li&gt;
&lt;li&gt;Measure your own restart phases separately: weight load, autotune, warmup and CUDA graphs. Do not assume weights are the whole story.&lt;/li&gt;
&lt;li&gt;Key the daemon to a full-file digest, and run a canary after each start.&lt;/li&gt;
&lt;li&gt;Keep the disk fallback enabled in production and alert on it. Budget the GPU memory the daemon pins, and do not combine the loader with sleep mode.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;What does &lt;code&gt;vllm preload&lt;/code&gt; do?&lt;/strong&gt;&lt;br&gt;
It starts one daemon per GPU that loads and post-processes the weights once and keeps them in GPU memory. Engines started with &lt;code&gt;--load-format ipc_cache&lt;/code&gt; map those weights over CUDA IPC instead of reading the checkpoint again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does preload speed up autoscaling to a new GPU node?&lt;/strong&gt;&lt;br&gt;
No. The handles are node-local and the daemon must load from disk the first time. It helps restarts on a node that already holds the weights. Only the FlashInfer autotune tables can be seeded to a new daemon from another replica.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the weight cache load the wrong model?&lt;/strong&gt;&lt;br&gt;
Under the current fingerprint, yes, if two checkpoints share names, shapes, dtypes and offsets and a daemon for the other one is still running (issue #59647, open on 2026-11-03). A digest-keyed socket directory and a canary request avoid it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much GPU capacity does faster startup save?&lt;/strong&gt;&lt;br&gt;
In my illustrative simulation with 1.5x bursts and a 10 s queue-wait SLO, 45 s startup needs 120 GPUs where 300 s needs 145, for 100 GPUs of baseline load. Your own burst size, detection lag and SLO will move that.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/your-llm-serving-engine-ships-every-13-days-reading-vllms-breaking-change-lists-as-upgrade-risk-1815"&gt;Your LLM Serving Engine Ships Every 13 Days: Reading vLLM's Breaking-Change Lists as Upgrade Risk Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/what-does-it-actually-cost-to-self-host-an-llm-the-batching-math-nobody-shows-you-4pmp"&gt;What Does It Actually Cost to Self-Host an LLM? The Batching Math Nobody Shows You&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sources: &lt;a href="https://docs.vllm.ai/en/latest/features/preload/" rel="noopener noreferrer"&gt;vLLM docs, Preload&lt;/a&gt;; &lt;a href="https://github.com/vllm-project/vllm/blob/main/docs/features/initialized_snapshots.md" rel="noopener noreferrer"&gt;vLLM docs, Initialized engine snapshots&lt;/a&gt;; &lt;a href="https://github.com/vllm-project/vllm/releases/tag/v0.31.0" rel="noopener noreferrer"&gt;vLLM v0.31.0 release notes&lt;/a&gt;; &lt;a href="https://github.com/vllm-project/vllm/pull/54921" rel="noopener noreferrer"&gt;PR #54921 Fast Start&lt;/a&gt;; &lt;a href="https://github.com/vllm-project/vllm/pull/51360" rel="noopener noreferrer"&gt;PR #51360 snapshots&lt;/a&gt;; &lt;a href="https://github.com/vllm-project/vllm/issues/59647" rel="noopener noreferrer"&gt;Issue #59647&lt;/a&gt;; &lt;a href="https://github.com/vllm-project/vllm/pull/59648" rel="noopener noreferrer"&gt;PR #59648&lt;/a&gt;; &lt;a href="https://pypi.org/project/vllm-ipc-cache/" rel="noopener noreferrer"&gt;vllm-ipc-cache on PyPI&lt;/a&gt;; &lt;a href="https://huggingface.co/Qwen/Qwen3-8B" rel="noopener noreferrer"&gt;Qwen3-8B&lt;/a&gt; and &lt;a href="https://huggingface.co/Qwen/Qwen3-32B" rel="noopener noreferrer"&gt;Qwen3-32B&lt;/a&gt; safetensors headers.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>vllm</category>
    </item>
    <item>
      <title>When Should an LLM Router Stop Being Sticky? llm-d 1.0 Cache-Affinity Routing, Break-Even Math, and a Simulation</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Mon, 02 Nov 2026 14:05:55 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/when-should-an-llm-router-stop-being-sticky-llm-d-10-cache-affinity-routing-break-even-math-and-1lpe</link>
      <guid>https://dev.to/wolfsea2357/when-should-an-llm-router-stop-being-sticky-llm-d-10-cache-affinity-routing-break-even-math-and-1lpe</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What shipped:&lt;/strong&gt; llm-d 1.0 was announced on 2026-10-20 under CNCF governance, with vLLM and SGLang as supported model servers. The feature that most affects your GPU bill is not the headline list. It is the default routing rule described in llm-d's August post "Sticky Until Saturated": send a request to the endpoint that already holds its prefix, until that endpoint's queued token load passes a limit, then route by load alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The limit is arithmetic, not taste:&lt;/strong&gt; tau = R_peak x T_max. With the published Qwen3-32B numbers (8,192 tokens in 0.40 s, so 20,480 tokens/s, and a 14 s tolerance) that gives 286,720 tokens, exactly 35 scheduler chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A break-even you can compute:&lt;/strong&gt; for one request, sticking beats spilling while the warm endpoint's queue exceeds the cold one's by no more than the tokens the cache saves. For a 55,000-token prefix that is 2.69 s of queue. llm-d's limit is 5.2 times larger on purpose, and my simulation shows what happens if you set it at break-even instead: the fleet collapses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reported results (vendor benchmark):&lt;/strong&gt; 2.9x and 2.0x the throughput of Kubernetes Service round-robin on two prefill-bound workloads, parity on a decode-bound one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What did llm-d 1.0 actually ship?
&lt;/h2&gt;

&lt;p&gt;The announcement lists a lot: prefix-cache and load-aware routing, KV cache offloading, prefill/decode disaggregation, wide expert parallelism, batch and async inference, multi-tenant quality of service, router high availability with hot standby, and session-aware routing for agents. The founding contributors named are Red Hat, IBM, Google Cloud, CoreWeave and NVIDIA, with more than 500 contributors from over 25 organizations. The GitHub release notes add a few operational details: a &lt;code&gt;DisaggregatedSet&lt;/code&gt; that manages prefill and decode replicas as one logical unit, a router configuration API that graduates to v1, and deprecation of the &lt;code&gt;llm-d-rocm&lt;/code&gt; container image builds in favor of upstream vLLM.&lt;/p&gt;

&lt;p&gt;Most of that is plumbing you will only appreciate during an incident. The routing default is different, because it decides how often your GPUs recompute a prompt prefix they have already seen. If you serve long shared system prompts (coding agents, RAG with a fixed preamble, multi-turn chat), prefix reuse is the largest single lever on cost per token, and the router is the component that either preserves it or throws it away.&lt;/p&gt;

&lt;p&gt;IBM's announcement also quotes figures such as 11.5 times lower inference cost than an all-proprietary configuration at roughly a 98% prefix-cache hit rate. Those are the project backers' numbers with workload details I could not check, so I treat them as a statement of how much headroom a high hit rate can give, not as something to plan capacity around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does cache-affinity routing hot-spot?
&lt;/h2&gt;

&lt;p&gt;Round-robin ignores caches. A fleet of 10 endpoints, each able to hold only a few long prefixes, ends up with every endpoint seeing every prefix, evicting constantly, and recomputing most of what it receives. Affinity routing fixes that by sending a request to an endpoint that already has its prefix.&lt;/p&gt;

&lt;p&gt;The failure mode is the other direction. In llm-d's own words, affinity concentrates traffic on cache-warm endpoints, and without an explicit saturation release the warm endpoint keeps absorbing load past the point where a cold endpoint would serve the request faster. Real traffic is skewed. A few popular prefixes carry a large share of requests, so the endpoint that owns the most popular prefix becomes a queue.&lt;/p&gt;

&lt;p&gt;The previous llm-d default tried to handle this with a four-signal weighted blend: a prefix-cache scorer at weight 3, plus queue, KV-cache-utilization and no-hit-LRU scorers at weight 2 each. The authors describe the behavior as hard to predict and harder to tune. That is a fair description of any weighted sum where one signal is in tokens, one is a count and one is a ratio: the weights encode an exchange rate between units nobody measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does llm-d decide when to stop being sticky?
&lt;/h2&gt;

&lt;p&gt;The replacement splits the decision in two. A filter keeps only endpoints whose cached prefix match is at or above an &lt;code&gt;affinityThreshold&lt;/code&gt; of 0.8. Then one load scorer, matched to the workload's bottleneck, picks among them: &lt;code&gt;token-load-scorer&lt;/code&gt; (uncached prefill tokens in flight) for prefill-bound traffic, &lt;code&gt;active-request-scorer&lt;/code&gt; (active request count) for decode-bound traffic. When the best warm endpoint's load passes the saturation limit tau, the filter stops restricting and the request goes wherever load is lowest.&lt;/p&gt;

&lt;p&gt;The limit comes from two things you can measure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tau = R_peak x T_max
R_peak = peak prefill throughput per endpoint (tokens/s)
T_max  = how much TTFT degradation you will tolerate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the benchmark setup (Qwen3-32B, tensor parallel 2 across two H100s per server, vLLM with &lt;code&gt;max-num-batched-tokens=8192&lt;/code&gt;), the post reports T(B = 8192) = 0.40 s. I checked the rest of the chain: 8,192 / 0.40 = 20,480 tokens/s; 20,480 x 14 s = 286,720; and 286,720 / 8,192 = 35.0 chunks. The numbers are internally consistent.&lt;/p&gt;

&lt;p&gt;Here is the same calculation as code, with the break-even and the KV sizing I use below. The KV geometry comes from the model's &lt;code&gt;config.json&lt;/code&gt; on Hugging Face (64 layers, 8 KV heads, head dimension 128, bf16):&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;saturation_tau&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttft_tolerance_s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r_peak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;batch_seconds&lt;/span&gt;          &lt;span class="c1"&gt;# prefill tokens/s per endpoint
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r_peak&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;r_peak&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ttft_tolerance_s&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;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="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&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="c1"&gt;# K and V
&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tau&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;saturation_tau&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&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="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tau&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tau&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="c1"&gt;# 20480.0 286720 35.0
&lt;/span&gt;&lt;span class="n"&gt;per_tok&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;64&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_tok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;55000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;per_tok&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="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# 262144 bytes, 13.43 GiB per 55k prefix
&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;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&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="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;55000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;per_tok&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;     &lt;span class="c1"&gt;# 4 prefixes fit in a 64 GiB budget
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line matters. At 256 KiB per token, one 55,000-token prefix is 13.4 GiB of KV. A server with a 64 GiB KV budget (my assumption, not a figure from the post) holds four of them. A coding-agent fleet with dozens of distinct long prefixes cannot cache them all on every endpoint, which is exactly why routing decides the hit rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What throughput did the benchmark report?
&lt;/h2&gt;

&lt;p&gt;The post compares matched configurations against Kubernetes Service round-robin on 10 Qwen3-32B servers (8 for the third workload). All figures below are from llm-d's own write-up, not an independent test.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Bottleneck&lt;/th&gt;
&lt;th&gt;Matched config&lt;/th&gt;
&lt;th&gt;Throughput vs round-robin&lt;/th&gt;
&lt;th&gt;Knee, matched vs round-robin&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code generation (55k-token mean system prompt, 1,500 new tokens/turn)&lt;/td&gt;
&lt;td&gt;prefill&lt;/td&gt;
&lt;td&gt;prefix + token load&lt;/td&gt;
&lt;td&gt;46k vs 16k input tokens/s (2.9x)&lt;/td&gt;
&lt;td&gt;concurrency 60 vs 30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning (250-token shared prompt, 8,000-token mean output)&lt;/td&gt;
&lt;td&gt;decode&lt;/td&gt;
&lt;td&gt;prefix + active requests&lt;/td&gt;
&lt;td&gt;7.0k vs 6.8k output tokens/s (parity)&lt;/td&gt;
&lt;td&gt;concurrency 350 vs 250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2B SaaS (many 6,000-token prompts, fixed repeating corpus)&lt;/td&gt;
&lt;td&gt;prefill&lt;/td&gt;
&lt;td&gt;prefix + token load&lt;/td&gt;
&lt;td&gt;90k vs 45k input tokens/s (2.0x)&lt;/td&gt;
&lt;td&gt;38 vs 18 QPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is the useful part. Where prefill dominates and prefixes are long, affinity roughly doubles to triples capacity. Where decode dominates, there is little prefill to save, so throughput is at parity and the only gain is a later knee. The same routing rule is not a universal win, and the post's choice to pair the load signal with the bottleneck is the honest way to say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the break-even point for stickiness?
&lt;/h2&gt;

&lt;p&gt;Take one request whose prefix of P tokens is cached on endpoint A and absent from endpoint B. It carries U new tokens. On A it costs U tokens of prefill after waiting q_A tokens in queue. On B it costs P + U tokens after waiting q_B. Sticking is faster for that request while&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;q_A + U &amp;lt;= q_B + P + U   =&amp;gt;   q_A - q_B &amp;lt;= P
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The queue gap you should accept is exactly the number of tokens the cache saves. For P = 55,000 at 20,480 tokens/s that is 2.69 s. A cached request costs 73 ms of prefill; a cold one costs 2.76 s, a ratio of 37.7.&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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Cache%2520hit%2520%25281%252C500%2520new%2520tokens%2529%2522%252C%2522Cold%2520start%2520%252856%252C500%2520tokens%2529%2522%252C%2522Break-even%2520queue%2520gap%2522%252C%2522Queue%2520wait%2520at%2520tau%2520%253D%2520286%252C720%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522seconds%2520of%2520prefill%2520at%252020%252C480%2520tokens%2Fs%2522%252C%2522data%2522%253A%255B0.073%252C2.759%252C2.686%252C14.0%255D%252C%2522backgroundColor%2522%253A%255B%2522%252316a34a%2522%252C%2522%2523dc2626%2522%252C%2522%2523f59e0b%2522%252C%2522%25232563eb%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Arithmetic%2520from%2520the%2520published%2520per-endpoint%2520prefill%2520rate%2520%252855k-token%2520shared%2520prefix%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%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%2522seconds%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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Cache%2520hit%2520%25281%252C500%2520new%2520tokens%2529%2522%252C%2522Cold%2520start%2520%252856%252C500%2520tokens%2529%2522%252C%2522Break-even%2520queue%2520gap%2522%252C%2522Queue%2520wait%2520at%2520tau%2520%253D%2520286%252C720%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522seconds%2520of%2520prefill%2520at%252020%252C480%2520tokens%2Fs%2522%252C%2522data%2522%253A%255B0.073%252C2.759%252C2.686%252C14.0%255D%252C%2522backgroundColor%2522%253A%255B%2522%252316a34a%2522%252C%2522%2523dc2626%2522%252C%2522%2523f59e0b%2522%252C%2522%25232563eb%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Arithmetic%2520from%2520the%2520published%2520per-endpoint%2520prefill%2520rate%2520%252855k-token%2520shared%2520prefix%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%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%2522seconds%2522%257D%257D%255D%257D%257D%257D" alt="Prefill cost of one request, cache hit versus cold start, next to the break-even gap and the queue wait at llm-d's limit" width="1800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So llm-d's 14 s tolerance is not the per-request break-even. It is 5.2 times larger. That looks wrong until you count what a spill costs beyond the one request. A cold fill burns 2.76 s of the receiving endpoint's capacity, and it inserts a 13.4 GiB prefix into a cache that holds four, evicting something another request was about to hit. The single-request formula ignores both. The next section tests whether that externality is large enough to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens if you set the limit at break-even?
&lt;/h2&gt;

&lt;p&gt;I built a small discrete-event simulation. It is illustrative, loosely modeled on the code-generation workload, and it is not a reproduction of llm-d's benchmark. The assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10 endpoints, each a FIFO queue serving prefill at 20,480 tokens/s. Decode and continuous batching are ignored.&lt;/li&gt;
&lt;li&gt;40 prefix groups of 55,000 tokens, requested with Zipf skew (exponent 1.1, so the most popular group takes 27.2% of traffic and the top five take 58.5%). Each request adds 1,500 new tokens.&lt;/li&gt;
&lt;li&gt;Each endpoint's cache is an LRU holding 4 prefixes, from the 64 GiB budget above.&lt;/li&gt;
&lt;li&gt;Poisson arrivals, 600 s per run, 5 seeds averaged. The router sees exact queue length and cache contents, which a real router only approximates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three policies: round-robin; always sticky (the warm endpoint with the least queue, spill only when no endpoint is warm); and sticky until saturated (spill when the best warm endpoint's queue passes tau).&lt;/p&gt;

&lt;p&gt;The core of the third policy is short:&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;warm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&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&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;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;least&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="nf"&gt;range&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="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pend&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;        &lt;span class="c1"&gt;# pend = queued tokens
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;bw&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;warm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pend&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bw&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pend&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;tau&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;least&lt;/span&gt;            &lt;span class="c1"&gt;# stick until saturated
&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;least&lt;/span&gt;
&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%2Fquickchart.io%2Fchart%3Fw%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25222%2522%252C%25224%2522%252C%25226%2522%252C%25228%2522%252C%252210%2522%252C%252220%2522%252C%252240%2522%252C%252260%2522%252C%252280%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Round-robin%2522%252C%2522data%2522%253A%255B2.76%252C4.97%252C61.78%252C247.13%252C437.52%252C1406.27%252C3351.64%252C5293.76%252C7253.11%255D%252C%2522borderColor%2522%253A%2522%2523dc2626%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522Always%2520sticky%2522%252C%2522data%2522%253A%255B2.76%252C2.76%252C2.63%252C2.34%252C1.97%252C1.47%252C0.73%252C121.77%252C292.1%255D%252C%2522borderColor%2522%253A%2522%2523f59e0b%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522Sticky%2520until%2520saturated%2520%2528tau%2520%253D%2520286%252C720%2529%2522%252C%2522data%2522%253A%255B2.76%252C2.76%252C2.63%252C2.34%252C1.97%252C1.47%252C0.73%252C3.21%252C1000.25%255D%252C%2522borderColor%2522%253A%2522%25232563eb%2522%252C%2522fill%2522%253Afalse%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%253A%2520p90%2520TTFT%2520vs%2520arrival%2520rate%252C%252010%2520endpoints%2522%252C%2522fontSize%2522%253A17%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522requests%2520per%2520second%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522type%2522%253A%2522logarithmic%2522%252C%2522ticks%2522%253A%257B%2522min%2522%253A0.1%252C%2522max%2522%253A10000%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522p90%2520TTFT%252C%2520seconds%2520%2528log%2520scale%2529%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25222%2522%252C%25224%2522%252C%25226%2522%252C%25228%2522%252C%252210%2522%252C%252220%2522%252C%252240%2522%252C%252260%2522%252C%252280%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Round-robin%2522%252C%2522data%2522%253A%255B2.76%252C4.97%252C61.78%252C247.13%252C437.52%252C1406.27%252C3351.64%252C5293.76%252C7253.11%255D%252C%2522borderColor%2522%253A%2522%2523dc2626%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522Always%2520sticky%2522%252C%2522data%2522%253A%255B2.76%252C2.76%252C2.63%252C2.34%252C1.97%252C1.47%252C0.73%252C121.77%252C292.1%255D%252C%2522borderColor%2522%253A%2522%2523f59e0b%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522Sticky%2520until%2520saturated%2520%2528tau%2520%253D%2520286%252C720%2529%2522%252C%2522data%2522%253A%255B2.76%252C2.76%252C2.63%252C2.34%252C1.97%252C1.47%252C0.73%252C3.21%252C1000.25%255D%252C%2522borderColor%2522%253A%2522%25232563eb%2522%252C%2522fill%2522%253Afalse%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%253A%2520p90%2520TTFT%2520vs%2520arrival%2520rate%252C%252010%2520endpoints%2522%252C%2522fontSize%2522%253A17%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522requests%2520per%2520second%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522type%2522%253A%2522logarithmic%2522%252C%2522ticks%2522%253A%257B%2522min%2522%253A0.1%252C%2522max%2522%253A10000%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522p90%2520TTFT%252C%2520seconds%2520%2528log%2520scale%2529%2522%257D%257D%255D%257D%257D%257D" alt="p90 time to first token versus arrival rate for round-robin, always sticky and sticky-until-saturated, log scale" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arrival rate&lt;/th&gt;
&lt;th&gt;Round-robin p90 TTFT&lt;/th&gt;
&lt;th&gt;Always sticky p90&lt;/th&gt;
&lt;th&gt;Sticky until saturated p90&lt;/th&gt;
&lt;th&gt;Hit rate (RR / sticky / saturated)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 req/s&lt;/td&gt;
&lt;td&gt;4.97 s&lt;/td&gt;
&lt;td&gt;2.76 s&lt;/td&gt;
&lt;td&gt;2.76 s&lt;/td&gt;
&lt;td&gt;35% / 88% / 88%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 req/s&lt;/td&gt;
&lt;td&gt;437.5 s&lt;/td&gt;
&lt;td&gt;1.97 s&lt;/td&gt;
&lt;td&gt;1.97 s&lt;/td&gt;
&lt;td&gt;35% / 95% / 95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40 req/s&lt;/td&gt;
&lt;td&gt;3,352 s&lt;/td&gt;
&lt;td&gt;0.73 s&lt;/td&gt;
&lt;td&gt;0.73 s&lt;/td&gt;
&lt;td&gt;35% / 100% / 100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60 req/s&lt;/td&gt;
&lt;td&gt;5,294 s&lt;/td&gt;
&lt;td&gt;121.8 s&lt;/td&gt;
&lt;td&gt;3.21 s&lt;/td&gt;
&lt;td&gt;35% / 99% / 99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80 req/s&lt;/td&gt;
&lt;td&gt;7,253 s&lt;/td&gt;
&lt;td&gt;292.1 s&lt;/td&gt;
&lt;td&gt;1,000 s&lt;/td&gt;
&lt;td&gt;36% / 99% / 90%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things in that table can be checked by hand, which is how I convinced myself the simulation is not producing noise.&lt;/p&gt;

&lt;p&gt;Round-robin's hit rate is 35%, so its average request costs 0.35 x 1,500 + 0.65 x 56,500 = 37,250 tokens. Ten endpoints deliver 204,800 tokens/s, which caps the fleet at 5.5 requests/s. The simulation falls over between 4 and 6. Its huge numbers at 10 req/s and above are the backlog after a 10 minute window, not a steady-state latency.&lt;/p&gt;

&lt;p&gt;Always-sticky ties each prefix to one endpoint. The hottest prefix takes 27.2% of traffic at 73 ms per request, so its endpoint saturates at 1 / (0.272 x 0.0732) = 50 req/s. The simulation is healthy at 40 and in trouble at 60. That is hot-spotting, and a saturation release fixes it: at 60 req/s p90 drops from 121.8 s to 3.21 s.&lt;/p&gt;

&lt;p&gt;At low rates p90 sits near 2.76 s for both sticky policies. That is one cold fill, and it comes from first-time prefixes and LRU evictions. No router removes it.&lt;/p&gt;

&lt;p&gt;The last row is the part I did not expect to write. At 80 req/s the saturating policy does worse than always-sticky (1,000 s against 292 s) because the fleet is simply short of capacity, and spilling turns scarce capacity into cold fills. A saturation release helps a skewed fleet that has total headroom. It does not create headroom.&lt;/p&gt;

&lt;p&gt;Now the sweep that answers the break-even question. Same simulation at 60 req/s, varying tau only:&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%3D1000%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%252227k%2522%252C%252255k%2522%252C%2522110k%2522%252C%2522220k%2522%252C%2522287k%2520%2528llm-d%2529%2522%252C%2522573k%2522%252C%25221146k%2522%252C%2522no%2520limit%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522p90%2520TTFT%2520%2528s%2529%2522%252C%2522data%2522%253A%255B5302.57%252C5289.39%252C5289.05%252C2.42%252C3.21%252C5.66%252C15.53%252C121.77%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523dc2626%2522%252C%2522%2523dc2626%2522%252C%2522%2523dc2626%2522%252C%2522%2523f59e0b%2522%252C%2522%25232563eb%2522%252C%2522%2523f59e0b%2522%252C%2522%2523f59e0b%2522%252C%2522%2523f59e0b%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%2520simulation%253A%2520p90%2520TTFT%2520at%252060%2520req%2Fs%2520vs%2520the%2520stickiness%2520limit%2520%2528tokens%2529%2522%252C%2522fontSize%2522%253A19%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522type%2522%253A%2522logarithmic%2522%252C%2522ticks%2522%253A%257B%2522min%2522%253A1%252C%2522max%2522%253A10000%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522p90%2520TTFT%252C%2520seconds%2520%2528log%2520scale%2529%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%3D1000%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%252227k%2522%252C%252255k%2522%252C%2522110k%2522%252C%2522220k%2522%252C%2522287k%2520%2528llm-d%2529%2522%252C%2522573k%2522%252C%25221146k%2522%252C%2522no%2520limit%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522p90%2520TTFT%2520%2528s%2529%2522%252C%2522data%2522%253A%255B5302.57%252C5289.39%252C5289.05%252C2.42%252C3.21%252C5.66%252C15.53%252C121.77%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523dc2626%2522%252C%2522%2523dc2626%2522%252C%2522%2523dc2626%2522%252C%2522%2523f59e0b%2522%252C%2522%25232563eb%2522%252C%2522%2523f59e0b%2522%252C%2522%2523f59e0b%2522%252C%2522%2523f59e0b%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%2520simulation%253A%2520p90%2520TTFT%2520at%252060%2520req%2Fs%2520vs%2520the%2520stickiness%2520limit%2520%2528tokens%2529%2522%252C%2522fontSize%2522%253A19%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522type%2522%253A%2522logarithmic%2522%252C%2522ticks%2522%253A%257B%2522min%2522%253A1%252C%2522max%2522%253A10000%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522p90%2520TTFT%252C%2520seconds%2520%2528log%2520scale%2529%2522%257D%257D%255D%257D%257D%257D" alt="p90 TTFT at 60 requests per second as the stickiness limit varies from 27,500 tokens to no limit, log scale" width="2000" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;tau (tokens)&lt;/th&gt;
&lt;th&gt;p90 TTFT&lt;/th&gt;
&lt;th&gt;Hit rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;27,500&lt;/td&gt;
&lt;td&gt;5,303 s&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;55,000 (break-even)&lt;/td&gt;
&lt;td&gt;5,289 s&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;110,000&lt;/td&gt;
&lt;td&gt;5,289 s&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;220,000&lt;/td&gt;
&lt;td&gt;2.42 s&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;286,720 (llm-d)&lt;/td&gt;
&lt;td&gt;3.21 s&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;573,440&lt;/td&gt;
&lt;td&gt;5.66 s&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,146,880&lt;/td&gt;
&lt;td&gt;15.5 s&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no limit&lt;/td&gt;
&lt;td&gt;121.8 s&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Below roughly 110,000 tokens the system falls into the same state as round-robin. A spill creates a cold fill, the cold fill evicts a warm prefix, the next request for that prefix misses, spills again, and the hit rate sinks to 35% and stays there. Above the cliff everything is fine, and then latency climbs slowly as stickiness turns back into hot-spotting. The shape is a cliff on the low side and a slope on the high side, which argues for erring high.&lt;/p&gt;

&lt;p&gt;Be careful with the exact position of the cliff. It comes from my parameters (4 slots, 40 groups, 60 req/s) and a router that sees perfect state. A real fleet has chunked prefill, imprecise cache indexing and a different mix. What transfers is the mechanism and the asymmetry, not the number 110,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you calibrate this on your own fleet?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure R_peak where you will run it.&lt;/strong&gt; Time a prefill of one full scheduler batch on the real model, tensor-parallel layout and engine flags. R_peak is a property of that exact setup, and it will move if you change &lt;code&gt;max-num-batched-tokens&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick T_max from your TTFT objective&lt;/strong&gt;, not from the post. Fourteen seconds is the benchmark's tolerance, and an interactive product will want less. Compute tau and express it in scheduler chunks so it is easy to sanity check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute the break-even for your median prefix&lt;/strong&gt; (P tokens, so P / R_peak seconds) and confirm your tau is well above it. If it is not, you are in the region my simulation collapses in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count how many prefixes fit&lt;/strong&gt; per endpoint with the KV-bytes formula. If everything fits everywhere, routing barely matters and round-robin is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay a day of real prefix-hash traffic&lt;/strong&gt; through a simulator like the one above before touching production, and watch hit rate, not only latency. A falling hit rate under load is the early warning for the cliff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the load signal to the bottleneck.&lt;/strong&gt; Token load for prefill-bound traffic, request count for decode-bound.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What should you not conclude from this?
&lt;/h2&gt;

&lt;p&gt;The 2.9x and 2.0x figures come from the llm-d authors on their own 10-server cluster and workloads. They are a strong reason to test, not a forecast for your traffic. The decode-bound result at parity says the same thing from the other side.&lt;/p&gt;

&lt;p&gt;My simulation is a one-queue-per-endpoint toy. It omits decode, continuous batching, cache-indexing error and multi-tenant priorities, all of which the production router handles. It supports one claim only: a stickiness limit set at the single-request break-even can be catastrophic, because the cost of a spill lands on other requests.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What is prefix-cache-aware routing?&lt;/strong&gt;&lt;br&gt;
Sending a request to an inference endpoint that already holds the KV cache for the start of its prompt, so the endpoint can skip recomputing those tokens and only prefill the new ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does "sticky until saturated" mean in llm-d?&lt;/strong&gt;&lt;br&gt;
Requests stay on the cache-warm endpoint until its queued token load passes a calibrated limit (tau = R_peak x T_max). After that, routing falls back to load alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How is llm-d's 286,720-token limit derived?&lt;/strong&gt;&lt;br&gt;
In the published setup, 8,192 tokens take 0.40 s, so one endpoint prefills 20,480 tokens/s. With a 14 s TTFT degradation tolerance, 20,480 x 14 = 286,720 tokens, which equals 35 batches of 8,192.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is round-robin ever the right choice?&lt;/strong&gt;&lt;br&gt;
Yes, when all your prefixes fit on every endpoint, or when requests share little prefix. Then there is no cache to protect, and the decode-bound benchmark at parity shows the gain can vanish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not set the limit at the break-even queue gap?&lt;/strong&gt;&lt;br&gt;
Because a spill costs more than one request: a cold fill uses 2.76 s of the receiving endpoint's capacity and can evict a warm prefix. In my simulation, limits at or below about twice the break-even collapsed the hit rate to 35%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this apply outside Kubernetes?&lt;/strong&gt;&lt;br&gt;
The logic does. It needs only per-endpoint queue depth in tokens and some record of which endpoint holds which prefix, which you can approximate by hashing prompt prefixes.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/prefilldecode-disaggregation-in-vllm-the-kv-transfer-math-that-decides-if-it-pays-off-13j8"&gt;Prefill/Decode Disaggregation in vLLM: The KV Transfer Math That Decides If It Pays Off&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/kv-cache-offloading-to-nvme-the-break-even-bandwidth-math-and-where-it-does-not-pay-off-1mo6"&gt;KV Cache Offloading to NVMe: The Break-Even Bandwidth Math and Where It Does Not Pay Off&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sources: &lt;a href="https://llm-d.ai/blog/sticky-until-saturated-token-aware-routing" rel="noopener noreferrer"&gt;llm-d, "Sticky Until Saturated: Token-Aware Routing in llm-d"&lt;/a&gt;; &lt;a href="https://llm-d.ai/blog/llm-d-v1.0-state-of-the-art-inference-ready-for-day-2" rel="noopener noreferrer"&gt;llm-d, "llm-d v1.0: State-of-the-art inference, ready for Day 2"&lt;/a&gt;; &lt;a href="https://newsroom.ibm.com/2026-10-20-llm-d-1-0-advances-open,-cost-efficient-ai-inference-for-production-scale" rel="noopener noreferrer"&gt;IBM Newsroom, 2026-10-20&lt;/a&gt;; &lt;a href="https://huggingface.co/Qwen/Qwen3-32B/resolve/main/config.json" rel="noopener noreferrer"&gt;Qwen3-32B config.json&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>When Is a KV Cache Slot Actually Free? Lessons From the SGLang and Cloudflare Cross-Request Exposure</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:07:13 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/when-is-a-kv-cache-slot-actually-free-lessons-from-the-sglang-and-cloudflare-cross-request-exposure-1058</link>
      <guid>https://dev.to/wolfsea2357/when-is-a-kv-cache-slot-actually-free-lessons-from-the-sglang-and-cloudflare-cross-request-exposure-1058</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What was reported:&lt;/strong&gt; On 2026-10-26, Cloudflare and the SGLang team disclosed that Cloudflare's GLM-5.3 deployment on Workers AI returned &lt;strong&gt;155 responses&lt;/strong&gt; containing data from other customers' requests between 2026-08-27 and 2026-09-09. Cloudflare says all 155 were answers to prompts longer than &lt;strong&gt;681,280 tokens&lt;/strong&gt;, and that no other Workers AI model was affected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two bugs, one question:&lt;/strong&gt; A configuration gap (prefill KV pool of 681,280 tokens against a 1,048,576-token context window) made an error path reachable. Separately, SGLang's disaggregated serving could free a decode-side KV slot while a launched RDMA write was still on its way to it. Both come down to: &lt;em&gt;when is a slot really free?&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The invariant:&lt;/strong&gt; A slot is safe to reuse only when no remote writer can still touch it. The SGLang write-up calls the abort race "a distributed use-after-free."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix has a formula:&lt;/strong&gt; hold time must exceed abort propagation + remaining chunks + the call already in flight. If the acknowledgement path dies, every abort waits out the full timeout, and held capacity is roughly abort rate x hold time x pages per request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What to do today:&lt;/strong&gt; Check pool size against context length after every config change, run SGLang v0.5.21 or later for PD disaggregation, pair aborts at the router, and watch the release outcome metric. Scripts below.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What actually happened in the Cloudflare and SGLang disclosure?
&lt;/h2&gt;

&lt;p&gt;Cloudflare's report describes a model that launched on 2026-08-27 with a configured context window of 1,048,576 tokens. The prefill KV cache pool could hold only 681,280 tokens. That is 65.0% of the window, a gap of 367,296 tokens or 35.0%. A request could therefore fit the context window and still exceed the memory available to process it. In Cloudflare's words, nothing forced the two numbers to agree.&lt;/p&gt;

&lt;p&gt;On the engine side, the SGLang write-up says that in disaggregated prefill/decode serving, "a request marked for rejection could still proceed through the handoff. Decode could then read cache pages that had not been populated for that request." Cloudflare describes the same thing from the outside: the rejected request was replaced by a one-token placeholder, decode got a "transfer complete" message, and it assumed prefill had overwritten the old cache contents. Prefill had not, so stale pages from earlier requests survived.&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%3D1000%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Prefill%2520KV%2520pool%2522%252C%2522Configured%2520context%2520window%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522tokens%2522%252C%2522data%2522%253A%255B681280%252C1048576%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523dc2626%2522%252C%2522%25232563eb%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Reported%2520GLM-5.3%2520deployment%253A%2520pool%2520681%252C280%2520tokens%2520vs%2520window%25201%252C048%252C576%2520%252835.0%2525%2520gap%2529%2522%252C%2522fontSize%2522%253A19%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%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%2522tokens%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%3D1000%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Prefill%2520KV%2520pool%2522%252C%2522Configured%2520context%2520window%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522tokens%2522%252C%2522data%2522%253A%255B681280%252C1048576%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523dc2626%2522%252C%2522%25232563eb%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Reported%2520GLM-5.3%2520deployment%253A%2520pool%2520681%252C280%2520tokens%2520vs%2520window%25201%252C048%252C576%2520%252835.0%2525%2520gap%2529%2522%252C%2522fontSize%2522%253A19%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%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%2522tokens%2522%257D%257D%255D%257D%257D%257D" alt="Chart: prefill KV pool of 681,280 tokens against a 1,048,576-token context window" width="2000" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The timeline, from the two primary documents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date (2026)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Aug 27&lt;/td&gt;
&lt;td&gt;Exposure window opens (GLM-5.3 launch on Workers AI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 9&lt;/td&gt;
&lt;td&gt;Cloudflare disables public access to the model, notifies SGLang maintainers, restores service that evening after a larger pool and an engine patch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 16&lt;/td&gt;
&lt;td&gt;SGLang merges the intake admission fix (#38935) and optional KV transfer checksums (#39500)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oct 2&lt;/td&gt;
&lt;td&gt;v0.5.21: deferred KV release on by default for Mooncake and NIXL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oct 3&lt;/td&gt;
&lt;td&gt;Generation-tagged acks and MORI support (#37077), shipped in v0.5.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oct 26&lt;/td&gt;
&lt;td&gt;Coordinated disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One scope note matters for your risk assessment: the SGLang post says default PD configurations were affected from the v0.4.x line until v0.5.21 (v0.5.22 for MORI), and that the fix line was &lt;strong&gt;not backported&lt;/strong&gt; to earlier release branches. A single-instance deployment without the cross-engine transfer path is not affected by the abort race.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can a KV slot be "free" and still get written to?
&lt;/h2&gt;

&lt;p&gt;The decode engine pre-reserves destination slots for each incoming request, and its allocator marks them allocated or free. The disclosure's key observation is that "free" has a second, hidden condition. The allocator's bit says nothing about writes that were already posted to a NIC queue.&lt;/p&gt;

&lt;p&gt;The failure takes four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A client disconnect, admission rejection, or transfer fault aborts request A while at least one launched write has not landed.&lt;/li&gt;
&lt;li&gt;Decode frees A's slots. The allocator marks them free.&lt;/li&gt;
&lt;li&gt;The allocator hands those slots to request B.&lt;/li&gt;
&lt;li&gt;A's late write lands inside B's KV cache. B decodes on foreign keys and values, with no error anywhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The write-up gives four facts that make step 2 to step 4 possible, and each is worth knowing if you run any disaggregated stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The allocator can mark a slot free while writes are in flight.&lt;/strong&gt; It and the NIC do not share state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launched RDMA work cannot be cancelled.&lt;/strong&gt; There is no call to cancel one work request already posted to the queue pair, and Python cannot interrupt a blocking C++ transfer call mid-flight. A pull design does not escape this, because a posted READ is as hard to take back as a posted WRITE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Router cancellation helps but cannot finish the job.&lt;/strong&gt; An abort is only safe when both halves of a prefill/decode pair learn about it, yet whatever the transfer engine already launched continues regardless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The window scales with context length.&lt;/strong&gt; A short prompt is in flight for milliseconds. A 128K to 1M-token context keeps writes in flight for seconds to minutes, so the same abort rate produces far more exposure on long-context traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The corruption is silent by design. The transfer reports success and no component logs an error. A single contaminated slot derails a whole generation because attention reads every position. In the team's 2xH200 reproduction, the victim counted correctly up to 80 and then its output turned into the donor's prompt text.&lt;/p&gt;

&lt;p&gt;The race is probabilistic in production, but a client can raise the odds by sending long-context requests and disconnecting mid-transfer. The client cannot choose the victim and learns nothing back, so the impact is one-way leakage plus an integrity problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  How likely is corruption if you free slots at abort time?
&lt;/h2&gt;

&lt;p&gt;I wrote a small Monte Carlo to see how the three ingredients interact. &lt;strong&gt;This is an illustrative model with stated assumptions, not a measurement of SGLang.&lt;/strong&gt; The core of it is below; the assumptions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;KV size of 37.5 KB per token, derived from the one data point in the post: a single call for an 800K-token request carried 30 GB (taking GB as 10^9 bytes).&lt;/li&gt;
&lt;li&gt;8,192-token chunks at an effective 25 GB/s, so one chunk write takes about 12.3 ms.&lt;/li&gt;
&lt;li&gt;Prefill at 20,000 tokens/s, so 128K tokens take 6.6 s and 1M tokens take 52.4 s.&lt;/li&gt;
&lt;li&gt;The allocator reuses freed slots after an exponentially distributed delay. The x-axis is the mean of that delay, standing in for memory pressure.&lt;/li&gt;
&lt;li&gt;Every abort lands mid-transfer, as the disclosure's conditions require.
&lt;/li&gt;
&lt;/ul&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;p_contam_free_at_abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tail_fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mean_reuse&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;200_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Slots are freed at abort and reused after Exp(mean_reuse).
    Corruption if a launched write lands after the reuse.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;reuse&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="nf"&gt;expovariate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;mean_reuse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;tail_fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reuse&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;

&lt;span class="n"&gt;chunk_t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;37_500&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;25e9&lt;/span&gt;                      &lt;span class="c1"&gt;# one chunk write, about 12.3 ms
&lt;/span&gt;&lt;span class="n"&gt;tail_notify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chunk_t&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="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;# abort reached prefill
&lt;/span&gt;&lt;span class="n"&gt;tail_silent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;ctx&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;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;20_000&lt;/span&gt;   &lt;span class="c1"&gt;# prefill keeps pushing
&lt;/span&gt;&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%2Fquickchart.io%2Fchart%3Fw%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25221%2520ms%2522%252C%252210%2520ms%2522%252C%2522100%2520ms%2522%252C%25221%2520s%2522%252C%252210%2520s%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Abort%2520notifies%2520prefill%2520%2528one%2520chunk%2520in%2520flight%2529%2522%252C%2522data%2522%253A%255B92.6%252C42.8%252C5.9%252C0.6%252C0.1%255D%252C%2522borderColor%2522%253A%2522%252316a34a%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522No%2520notification%252C%2520128K%2520context%2522%252C%2522data%2522%253A%255B100.0%252C99.8%252C98.5%252C84.8%252C26.3%255D%252C%2522borderColor%2522%253A%2522%2523f59e0b%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522No%2520notification%252C%25201M%2520context%2522%252C%2522data%2522%253A%255B100.0%252C100.0%252C99.8%252C98.1%252C81.0%255D%252C%2522borderColor%2522%253A%2522%2523dc2626%2522%252C%2522fill%2522%253Afalse%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%253A%2520share%2520of%2520mid-transfer%2520aborts%2520that%2520corrupt%2520a%2520neighbor%2520if%2520slots%2520are%2520freed%2520at%2520abort%2522%252C%2522fontSize%2522%253A15%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522mean%2520time%2520until%2520the%2520allocator%2520reuses%2520the%2520freed%2520slots%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A100%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522%2525%2520of%2520aborts%2520with%2520a%2520late%2520write%2520landing%2520after%2520reuse%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25221%2520ms%2522%252C%252210%2520ms%2522%252C%2522100%2520ms%2522%252C%25221%2520s%2522%252C%252210%2520s%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Abort%2520notifies%2520prefill%2520%2528one%2520chunk%2520in%2520flight%2529%2522%252C%2522data%2522%253A%255B92.6%252C42.8%252C5.9%252C0.6%252C0.1%255D%252C%2522borderColor%2522%253A%2522%252316a34a%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522No%2520notification%252C%2520128K%2520context%2522%252C%2522data%2522%253A%255B100.0%252C99.8%252C98.5%252C84.8%252C26.3%255D%252C%2522borderColor%2522%253A%2522%2523f59e0b%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522No%2520notification%252C%25201M%2520context%2522%252C%2522data%2522%253A%255B100.0%252C100.0%252C99.8%252C98.1%252C81.0%255D%252C%2522borderColor%2522%253A%2522%2523dc2626%2522%252C%2522fill%2522%253Afalse%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%253A%2520share%2520of%2520mid-transfer%2520aborts%2520that%2520corrupt%2520a%2520neighbor%2520if%2520slots%2520are%2520freed%2520at%2520abort%2522%252C%2522fontSize%2522%253A15%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522mean%2520time%2520until%2520the%2520allocator%2520reuses%2520the%2520freed%2520slots%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A100%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522%2525%2520of%2520aborts%2520with%2520a%2520late%2520write%2520landing%2520after%2520reuse%2522%257D%257D%255D%257D%257D%257D" alt="Chart: share of mid-transfer aborts that corrupt a neighbor when slots are freed at abort" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two readings follow. When the abort does &lt;strong&gt;not&lt;/strong&gt; reach prefill, the tail is every chunk prefill still has to push, so corruption is near certain unless reuse takes seconds. Notifying prefill (SGLang's #27372) shrinks the tail to one in-flight chunk and collapses the curve, but at a 10 ms mean reuse delay my model still corrupts 43% of mid-transfer aborts. Notification narrows the window. Only holding the slots closes it. The disclosure frames it the same way: the router decides who dies, and the engine decides when memory is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the fix hold, and for how long?
&lt;/h2&gt;

&lt;p&gt;Decode cannot cancel prefill's launched writes, so it keeps the aborted request's slots reserved for longer than those writes can keep landing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T_hold &amp;gt; t_abort_propagation + t_remaining_chunks + t_in_flight_call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design makes every abort path hold the slots, then bounds each term on the right:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Notify prefill on abort (#27372).&lt;/strong&gt; Decode sends ABORT to every prefill rank, and each rank drops chunks still waiting in its software queue. That drives &lt;code&gt;t_remaining_chunks&lt;/code&gt; toward zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hold, do not free (#35049, NIXL port #35360).&lt;/strong&gt; On abort, decode holds the request's KV slots, its row in the request pool, and its metadata buffer entry. Each prefill rank counts outstanding work per room and replies &lt;code&gt;ABORT_ACK&lt;/code&gt; only when that count drains to zero. Decode releases when every notified rank has acknowledged, or when a timeout fires. The default timeout in that PR is 30 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate by capability, then default on (#37454, #41023).&lt;/strong&gt; The hold only helps on backends that acknowledge, so it shipped opt-in and became the default in v0.5.21.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The timeout is described as "a practical backstop, not a proof", because a call that times out can leave writes the engine is still retrying. That is why #43719 (merged 2026-10-12) makes prefill withhold the drain ack for any room whose send failed. Before it, a failed Mooncake send still acked as drained.&lt;/p&gt;

&lt;p&gt;The design rule in the conclusion is worth copying: &lt;em&gt;make every degradation lengthen the hold.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How long should the timeout be?
&lt;/h3&gt;

&lt;p&gt;The post recommends 60 to 120 seconds for &lt;code&gt;SGLANG_DISAGGREGATION_DEFERRED_DECODE_KV_RELEASE_TIMEOUT&lt;/code&gt;, and says a larger value "adds little safety and multiplies held KVCache during abort storms." I checked the shape of that claim with a second illustrative model: 5% of aborts take a degraded path where writes keep retrying past the 30 s transfer timeout (30 s plus an exponential with a 20 s mean), and the release is timeout-only.&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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25225%2520s%2522%252C%252210%2520s%2522%252C%252230%2520s%2522%252C%252260%2520s%2522%252C%2522120%2520s%2522%252C%2522240%2520s%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Corrupted%2520per%252010%252C000%2520aborts%2520%25285%2525%2520of%2520aborts%2520on%2520a%2520degraded%2520path%2529%2522%252C%2522data%2522%253A%255B506.1%252C492.0%252C499.5%252C112.9%252C6.0%252C0.0%255D%252C%2522backgroundColor%2522%253A%2522%25237c3aed%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%253A%2520timeout-only%2520release%252C%2520by%2520hold%2520time%2520T_hold%2522%252C%2522fontSize%2522%253A16%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Atrue%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522T_hold%2522%257D%257D%255D%252C%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%2522corrupted%2520per%252010%252C000%2520aborts%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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25225%2520s%2522%252C%252210%2520s%2522%252C%252230%2520s%2522%252C%252260%2520s%2522%252C%2522120%2520s%2522%252C%2522240%2520s%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Corrupted%2520per%252010%252C000%2520aborts%2520%25285%2525%2520of%2520aborts%2520on%2520a%2520degraded%2520path%2529%2522%252C%2522data%2522%253A%255B506.1%252C492.0%252C499.5%252C112.9%252C6.0%252C0.0%255D%252C%2522backgroundColor%2522%253A%2522%25237c3aed%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%253A%2520timeout-only%2520release%252C%2520by%2520hold%2520time%2520T_hold%2522%252C%2522fontSize%2522%253A16%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Atrue%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522T_hold%2522%257D%257D%255D%252C%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%2522corrupted%2520per%252010%252C000%2520aborts%2522%257D%257D%255D%257D%257D%257D" alt="Chart: corrupted aborts per 10,000 as a function of hold time" width="1800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below the transfer timeout the hold is useless on the degraded path (about 500 per 10,000 corrupted, which is just the 5% degraded share). At 60 s it falls to 113, at 120 s to 6, and at 240 s to 0 in 200,000 trials. The tail decays exponentially under my assumption, so the second doubling buys almost nothing while it doubles the cost below. Measure your own retry distribution before copying a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does holding cost in capacity?
&lt;/h2&gt;

&lt;p&gt;The post gives the sizing rule: if releases fall back to the timeout, held capacity is about abort rate x &lt;code&gt;T_hold&lt;/code&gt; x pages per request. With the healthy ack path, a hold is released milliseconds after the drain and costs nothing noticeable. With a dead ack path, every abort pays the full timeout.&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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522T_hold%252030%2520s%2522%252C%2522T_hold%252060%2520s%2522%252C%2522T_hold%2520120%2520s%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%25221%2520abort%2Fmin%2522%252C%2522data%2522%253A%255B2.4%252C4.8%252C9.6%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%25226%2520aborts%2Fmin%2522%252C%2522data%2522%253A%255B14.4%252C28.8%252C57.6%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520held%2520when%2520every%2520release%2520waits%2520out%2520the%2520timeout%2520%2528128K-token%2520reservations%252C%252037.5%2520KB%2Ftoken%2529%2522%252C%2522fontSize%2522%253A15%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%2522GB%2520held%2520on%2520decode%2520%2528steady%2520state%2529%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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522T_hold%252030%2520s%2522%252C%2522T_hold%252060%2520s%2522%252C%2522T_hold%2520120%2520s%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%25221%2520abort%2Fmin%2522%252C%2522data%2522%253A%255B2.4%252C4.8%252C9.6%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%25226%2520aborts%2Fmin%2522%252C%2522data%2522%253A%255B14.4%252C28.8%252C57.6%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520held%2520when%2520every%2520release%2520waits%2520out%2520the%2520timeout%2520%2528128K-token%2520reservations%252C%252037.5%2520KB%2Ftoken%2529%2522%252C%2522fontSize%2522%253A15%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%2522GB%2520held%2520on%2520decode%2520%2528steady%2520state%2529%2522%257D%257D%255D%257D%257D%257D" alt="Chart: KV held on decode when every release waits out the timeout" width="1800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abort rate&lt;/th&gt;
&lt;th&gt;T_hold&lt;/th&gt;
&lt;th&gt;Held KV at 128K-token reservations (4.8 GB each)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 per minute&lt;/td&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;td&gt;2.4 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 per minute&lt;/td&gt;
&lt;td&gt;120 s&lt;/td&gt;
&lt;td&gt;9.6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 per minute&lt;/td&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;td&gt;14.4 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 per minute&lt;/td&gt;
&lt;td&gt;120 s&lt;/td&gt;
&lt;td&gt;57.6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At the 37.5 KB/token figure, one 800K-token reservation is 30 GB, so a handful of long-context aborts waiting out a 120 s hold can pin a large share of a decode pool. This is why the metric below matters as much as the fix itself: a dead ack path turns a safety feature into an admission-latency problem.&lt;/p&gt;

&lt;p&gt;The SGLang team measured the steady-state price as small: across 21 runs on 2xH100 (1P1D Mooncake, 1024 requests at concurrency 256), throughput was statistically indistinguishable with the feature on or off, all deltas within plus or minus 2%. The hold only applies on the abort path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which pre-flight check catches the configuration half?
&lt;/h2&gt;

&lt;p&gt;Cloudflare's first recommendation is to measure the KV pool against context length after every configuration change. That check needs no GPU. It needs two numbers from your engine: the token capacity of the prefill KV pool and the configured context length. Field names differ across versions, so read them from your engine's startup log or info endpoint and verify on your build.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pool_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool_tokens&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;context_len&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;max_ctx_per_req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fail the deploy if a request that fits the context window cannot fit the prefill KV pool.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_ctx_per_req&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;context_len&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pool_tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;short&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pool_tokens&lt;/span&gt;
        &lt;span class="k"&gt;return&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;FAIL pool=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pool_tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt; context=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (short by &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;short&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;short&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;); lower context_len or grow the pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&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;OK pool=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pool_tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; covers context=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;pool_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;681_280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1_048_576&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# FAIL pool=681280 &amp;lt; context=1048576 (short by 367296, 35.0%); lower context_len or grow the pool
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make &lt;code&gt;FAIL&lt;/code&gt; block the rollout. It would have flagged the reported configuration on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you know the protection is actually on?
&lt;/h2&gt;

&lt;p&gt;SGLang v0.5.22 ships a counter, &lt;code&gt;sglang:decode_deferred_kv_release_total{outcome}&lt;/code&gt;. The post says a healthy deployment releases almost all holds through &lt;code&gt;drained&lt;/code&gt;, while a timeout-heavy ratio means the ack path needs attention. Here is a parser and a verdict function; I ran it against the synthetic sample shown, whose numbers are made up.&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;PAT&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;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sglang:decode_deferred_kv_release_total\{([^}]*)\}\s+([0-9.eE+-]+)&lt;/span&gt;&lt;span class="sh"&gt;'&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;release_outcomes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metrics_text&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;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;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PAT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metrics_text&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;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;outcome=&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="s"&gt;]+)&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&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="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;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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&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="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="mf"&gt;0.0&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;val&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outcomes&lt;/span&gt;&lt;span class="p"&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;min_drained&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&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="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outcomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&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;total&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="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NO DATA: no holds yet, or the metric is missing on this build&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;drained&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outcomes&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;drained&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HEALTHY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;drained&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;min_drained&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ACK PATH SUSPECT&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; drained=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;drained&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="si"&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;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; holds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;sglang:decode_deferred_kv_release_total{outcome=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;drained&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,tp_rank=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;} 9712
sglang:decode_deferred_kv_release_total{outcome=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,tp_rank=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;} 41
sglang:decode_deferred_kv_release_total{outcome=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,tp_rank=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;} 3&lt;/span&gt;&lt;span class="sh"&gt;'''&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;verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;release_outcomes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="c1"&gt;# HEALTHY drained=99.5% of 9756 holds
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 0.95 threshold is my choice, not SGLang's. Set yours from your own baseline after rollout.&lt;/p&gt;

&lt;p&gt;Beyond the counter, the engine offers an opt-in integrity check: &lt;code&gt;--disaggregation-enable-kv-checksum&lt;/code&gt; on both prefill and decode (their KV layouts must match). It computes an Adler-32 checksum on the GPU and verifies the received cache before the request joins a running batch. It is off by default because each transfer pays an extra GPU read and a synchronization point, and it catches only late writes that land before the commit point. The hold covers the rest.&lt;/p&gt;

&lt;p&gt;Operators still on v0.5.19 or v0.5.20 are told to set &lt;code&gt;SGLANG_DISAGGREGATION_DEFERRED_DECODE_KV_RELEASE=1&lt;/code&gt; on both servers, plus &lt;code&gt;SGLANG_MOONCAKE_MAX_TRANSFER_BATCH_INDICES&lt;/code&gt; on v0.5.20. The post is explicit that this does not replace upgrading, because failure kinds other than decode-initiated aborts stay unprotected before v0.5.21.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does this apply to engines other than SGLang?
&lt;/h2&gt;

&lt;p&gt;The write-up says the four steps apply to "any PD implementation that moves KVCache between engines with asynchronous DMA, push or pull." It does not say other engines are affected, and I have not tested any. Ask three questions of your own stack, whichever engine it runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a request dies mid-transfer, what delays the allocator's reuse of its destination slots?&lt;/li&gt;
&lt;li&gt;Is that delay tied to a positive signal that the writer finished, or only to a timer?&lt;/li&gt;
&lt;li&gt;If the signal path breaks, does the system get safer (longer hold) or less safe (earlier release)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot answer these from the code or docs, you have found your next audit item.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What exactly leaked in the Cloudflare incident?&lt;/strong&gt;&lt;br&gt;
Cloudflare reports 155 responses that contained data from other customers' requests, all to prompts longer than 681,280 tokens, between 2026-08-27 and 2026-09-09. Only GLM-5.3 on Workers AI was affected, per its investigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is my single-node SGLang deployment affected?&lt;/strong&gt;&lt;br&gt;
The SGLang post says a single-instance deployment without the cross-engine KV transfer path is not affected by the abort race. The exposed path is PD disaggregation with Mooncake, NIXL, MORI, or a custom transfer backend on affected versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which version should I run?&lt;/strong&gt;&lt;br&gt;
Per the post, v0.5.21 protects Mooncake and NIXL by default, v0.5.22 covers MORI, and the fix was not backported. Some press coverage says the fix landed earlier; I follow the engine team's own release mapping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I tell whether I was exposed before upgrading?&lt;/strong&gt;&lt;br&gt;
Not reliably. The corruption leaves no error or log line, and the opt-in checksum only helps going forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a bigger timeout make it safer?&lt;/strong&gt;&lt;br&gt;
Only until it exceeds the longest realistic write tail. Past that it mostly increases held capacity during abort storms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are the charts measurements?&lt;/strong&gt;&lt;br&gt;
The cover uses the reported pool and window sizes. The other three come from my own simulation, so read them as shapes, not SGLang numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.lmsys.org/blog/2026-10-26-cf-security/" rel="noopener noreferrer"&gt;SGLang security update: improving request isolation (LMSYS, 2026-10-26)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.cloudflare.com/kv-cache-exposure-remediation" rel="noopener noreferrer"&gt;Cross-request KV cache exposure in the GLM-5.3 Workers AI model, and our remediation with SGLang (Cloudflare)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SGLang pull requests cited: &lt;a href="https://github.com/sgl-project/sglang/pull/38935" rel="noopener noreferrer"&gt;#38935&lt;/a&gt;, &lt;a href="https://github.com/sgl-project/sglang/pull/39500" rel="noopener noreferrer"&gt;#39500&lt;/a&gt;, &lt;a href="https://github.com/sgl-project/sglang/pull/32758" rel="noopener noreferrer"&gt;#32758&lt;/a&gt;, &lt;a href="https://github.com/sgl-project/sglang/pull/37077" rel="noopener noreferrer"&gt;#37077&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SGLang releases: &lt;a href="https://github.com/sgl-project/sglang/releases/tag/v0.5.21" rel="noopener noreferrer"&gt;v0.5.21&lt;/a&gt;, &lt;a href="https://github.com/sgl-project/sglang/releases/tag/v0.5.22" rel="noopener noreferrer"&gt;v0.5.22&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Simulation assumptions: KV 37.5 KB/token (30 GB per 800K-token call, from #32758's tests as quoted in the post), 8,192-token chunks at 25 GB/s, prefill 20,000 tokens/s, exponential reuse delay, 5% degraded aborts with a 30 s timeout plus a 20 s-mean retry tail, 200,000 trials per point, seed 7.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/prefilldecode-disaggregation-in-vllm-the-kv-transfer-math-that-decides-if-it-pays-off-13j8"&gt;Prefill/Decode Disaggregation in vLLM: The KV Transfer Math That Decides If It Pays Off&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/your-llm-serving-engine-ships-every-13-days-reading-vllms-breaking-change-lists-as-upgrade-risk-1815"&gt;Your LLM Serving Engine Ships Every 13 Days: Reading vLLM's Breaking-Change Lists as Upgrade Risk Data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>Your LLM Serving Engine Ships Every 13 Days: Reading vLLM's Breaking-Change Lists as Upgrade Risk Data</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:04:46 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/your-llm-serving-engine-ships-every-13-days-reading-vllms-breaking-change-lists-as-upgrade-risk-1815</link>
      <guid>https://dev.to/wolfsea2357/your-llm-serving-engine-ships-every-13-days-reading-vllms-breaking-change-lists-as-upgrade-risk-1815</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; In the 365 days to 2026-10-31, vLLM published 33 stable releases on PyPI, including 21 new minor series (0.12 through 0.32). The median gap between minor series was 13.5 days. SGLang published 19 releases, all inside 0.5.x (median gap 14.6 days). LMDeploy published 12 (median 26.5 days).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breaking-change volume:&lt;/strong&gt; The vLLM release notes for 0.28 through 0.32 list 30 items under "Breaking changes". The per-release count rose every time: 4, 5, 6, 7, 8.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The risky class:&lt;/strong&gt; By my own classification, 8 of the 30 items (27%) change a default or a behavior while the exact same launch command still starts and passes a health check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lag has a price too:&lt;/strong&gt; Staying on 0.28 and jumping to 0.32 means reviewing 26 items at once, and 0.32.0 is the first of the five releases whose notes cite security advisories (three of them).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What to do:&lt;/strong&gt; Put an upgrade gate in front of every version bump: extract the breaking items automatically, diff the resolved server config, replay a golden request set. Working code for the first step is below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; I did not run an inference engine for this post. Every number comes from the PyPI JSON API and the GitHub release notes, and I say where my judgment enters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How often do LLM serving engines actually ship?
&lt;/h2&gt;

&lt;p&gt;Most teams I read about treat a serving engine like a database: pick a version, pin it, upgrade "when needed". The release data says the engine moves much faster than that mental model.&lt;/p&gt;

&lt;p&gt;Method: I pulled &lt;code&gt;https://pypi.org/pypi/&amp;lt;package&amp;gt;/json&lt;/code&gt;, took the earliest upload time of any file for each version, and dropped release candidates, dev builds and post releases. The window is 2025-10-31 to 2026-10-31.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Stable releases&lt;/th&gt;
&lt;th&gt;Median gap (days)&lt;/th&gt;
&lt;th&gt;Longest gap (days)&lt;/th&gt;
&lt;th&gt;Latest in window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;32.3&lt;/td&gt;
&lt;td&gt;0.32.0 (2026-10-18)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SGLang&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;14.6&lt;/td&gt;
&lt;td&gt;41.7&lt;/td&gt;
&lt;td&gt;0.5.23 (2026-10-30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LMDeploy&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;26.5&lt;/td&gt;
&lt;td&gt;43.1&lt;/td&gt;
&lt;td&gt;0.18.0 (2026-09-28)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I left TensorRT-LLM out on purpose. Its PyPI listing shows 24 releases and a last upload of 1.2.1 on 2026-04-20, and I cannot tell whether PyPI is its primary channel. Guessing from a possibly incomplete source would put a number in this table that I cannot defend.&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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25222025Q4%2520%2528Nov-Dec%2529%2522%252C%25222026Q1%2522%252C%25222026Q2%2522%252C%25222026Q3%2522%252C%25222026Q4%2520%2528Oct%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522vLLM%2522%252C%2522data%2522%253A%255B4%252C9%252C10%252C8%252C2%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522SGLang%2522%252C%2522data%2522%253A%255B2%252C3%252C5%252C6%252C3%255D%252C%2522backgroundColor%2522%253A%2522%252316a34a%2522%257D%252C%257B%2522label%2522%253A%2522LMDeploy%2522%252C%2522data%2522%253A%255B2%252C3%252C3%252C4%252C0%255D%252C%2522backgroundColor%2522%253A%2522%2523f59e0b%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Stable%2520PyPI%2520releases%2520per%2520quarter%2520%2528trailing%2520365%2520days%2520to%25202026-10-31%2529%2522%252C%2522fontSize%2522%253A16%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%2522releases%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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25222025Q4%2520%2528Nov-Dec%2529%2522%252C%25222026Q1%2522%252C%25222026Q2%2522%252C%25222026Q3%2522%252C%25222026Q4%2520%2528Oct%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522vLLM%2522%252C%2522data%2522%253A%255B4%252C9%252C10%252C8%252C2%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522SGLang%2522%252C%2522data%2522%253A%255B2%252C3%252C5%252C6%252C3%255D%252C%2522backgroundColor%2522%253A%2522%252316a34a%2522%257D%252C%257B%2522label%2522%253A%2522LMDeploy%2522%252C%2522data%2522%253A%255B2%252C3%252C3%252C4%252C0%255D%252C%2522backgroundColor%2522%253A%2522%2523f59e0b%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Stable%2520PyPI%2520releases%2520per%2520quarter%2520%2528trailing%2520365%2520days%2520to%25202026-10-31%2529%2522%252C%2522fontSize%2522%253A16%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%2522releases%2522%257D%257D%255D%257D%257D%257D" alt="Stable PyPI releases per quarter for vLLM, SGLang and LMDeploy" width="1800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of vLLM's 33 releases, 12 are patch releases. If you count only the first release of each minor series, you get 21 series with gaps of 15, 32, 9, 27, 9, 13, 13, 24, 17, 14, 14, 16, 11, 13, 16, 15, 13, 12, 13 and 13 days. The median is 13.5, the shortest is 9 and the longest is 32. The last four gaps (0.28 to 0.29 to 0.30 to 0.31 to 0.32) are 13, 12, 13 and 13 days, which is 52 days from 0.28 to 0.32.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the version number tell you how risky an upgrade is?
&lt;/h3&gt;

&lt;p&gt;No, and the two big engines show why. vLLM is on 0.y.z, and the middle number moves with every scheduled release, so "0.31 to 0.32" is the normal unit of change. SGLang stayed in 0.5 for all 19 releases, so its normal unit is the patch number. A "patch bump" in one project is the same size of event as a "minor bump" in the other. Neither number carries a compatibility promise you can read off. The release notes are the only signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did vLLM list as breaking changes from 0.28 to 0.32?
&lt;/h2&gt;

&lt;p&gt;The GitHub notes for vLLM 0.28 through 0.32 each contain a bullet named "Breaking changes" with semicolon-separated entries. I counted entries. Two caveats: older releases label this differently (the 0.27.0 notes, for example, call the PyTorch upgrade "a breaking environment change" inside a highlight and have no dedicated bullet), so I used only the five releases with the dedicated bullet. And one entry can bundle several changes, such as "two environment variables removed", so the counts are a floor on what changed, not an exact tally.&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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25220.28%2522%252C%25220.29%2522%252C%25220.30%2522%252C%25220.31%2522%252C%25220.32%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Dependency%2520or%2520Python%2520pin%2522%252C%2522data%2522%253A%255B1%252C0%252C0%252C0%252C2%255D%252C%2522backgroundColor%2522%253A%2522%25237c3aed%2522%257D%252C%257B%2522label%2522%253A%2522Default%2520or%2520behavior%2520changed%2520%2528same%2520command%2520still%2520runs%2529%2522%252C%2522data%2522%253A%255B0%252C1%252C2%252C3%252C2%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%252C%257B%2522label%2522%253A%2522Removed%2520or%2520renamed%2520flag%252C%2520env%2520var%252C%2520feature%252C%2520backend%2522%252C%2522data%2522%253A%255B3%252C3%252C2%252C4%252C3%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522Deprecation%2520or%2520API%2520rename%2522%252C%2522data%2522%253A%255B0%252C1%252C2%252C0%252C1%255D%252C%2522backgroundColor%2522%253A%2522%25239ca3af%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Items%2520in%2520the%2520vLLM%2520%255C%2522Breaking%2520changes%255C%2522%2520bullet%252C%2520by%2520class%2520%2528my%2520classification%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522stacked%2522%253Atrue%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522stacked%2522%253Atrue%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522items%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%3D900%26h%3D440%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25220.28%2522%252C%25220.29%2522%252C%25220.30%2522%252C%25220.31%2522%252C%25220.32%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Dependency%2520or%2520Python%2520pin%2522%252C%2522data%2522%253A%255B1%252C0%252C0%252C0%252C2%255D%252C%2522backgroundColor%2522%253A%2522%25237c3aed%2522%257D%252C%257B%2522label%2522%253A%2522Default%2520or%2520behavior%2520changed%2520%2528same%2520command%2520still%2520runs%2529%2522%252C%2522data%2522%253A%255B0%252C1%252C2%252C3%252C2%255D%252C%2522backgroundColor%2522%253A%2522%2523dc2626%2522%257D%252C%257B%2522label%2522%253A%2522Removed%2520or%2520renamed%2520flag%252C%2520env%2520var%252C%2520feature%252C%2520backend%2522%252C%2522data%2522%253A%255B3%252C3%252C2%252C4%252C3%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522Deprecation%2520or%2520API%2520rename%2522%252C%2522data%2522%253A%255B0%252C1%252C2%252C0%252C1%255D%252C%2522backgroundColor%2522%253A%2522%25239ca3af%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Items%2520in%2520the%2520vLLM%2520%255C%2522Breaking%2520changes%255C%2522%2520bullet%252C%2520by%2520class%2520%2528my%2520classification%2529%2522%252C%2522fontSize%2522%253A16%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522stacked%2522%253Atrue%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522stacked%2522%253Atrue%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522items%2522%257D%257D%255D%257D%257D%257D" alt="Breaking-change items per vLLM release, by class" width="1800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Items&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.28.0&lt;/td&gt;
&lt;td&gt;2026-08-26&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;bitsandbytes moved to an out-of-tree plugin; &lt;code&gt;calculate_kv_scales&lt;/code&gt; removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.29.0&lt;/td&gt;
&lt;td&gt;2026-09-09&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;ten deprecated model architectures removed; PyAV video decoder backend removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.30.0&lt;/td&gt;
&lt;td&gt;2026-09-22&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;scale-out endpoints now opt-in via &lt;code&gt;--enable-scale-out&lt;/code&gt;; GPTQ &lt;code&gt;g_idx&lt;/code&gt; removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.31.0&lt;/td&gt;
&lt;td&gt;2026-10-05&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tokenizer_mode="slow"&lt;/code&gt; removed; &lt;code&gt;--enforce-eager&lt;/code&gt; now also disables JIT kernel warmup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.32.0&lt;/td&gt;
&lt;td&gt;2026-10-18&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Python 3.10 dropped; default CORS origins restricted; &lt;code&gt;max_tokens&lt;/code&gt; no longer reserved from the context window&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I then sorted all 30 entries into four classes. This is my judgment, and a few entries could go either way.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Examples from the notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dependency or Python pin&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Python 3.10 dropped (0.32); &lt;code&gt;transformers&amp;gt;=5.16.1,&amp;lt;5.20.0&lt;/code&gt; (0.32); Transformers bumped to 5.15.0 (0.28)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default or behavior changed&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;default CORS origins restricted (0.32); &lt;code&gt;max_tokens&lt;/code&gt; no longer reserved from the context window (0.32); &lt;code&gt;--enforce-eager&lt;/code&gt; also disables JIT warmup (0.31); YaRN aligned with Transformers so vendor aliases no longer re-scale &lt;code&gt;max_model_len&lt;/code&gt; (0.30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Removed or renamed flag, env var, feature, backend&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tokenizer_mode="slow"&lt;/code&gt; removed (0.31); &lt;code&gt;--enable-mamba-fine-grained-prefix-cache&lt;/code&gt; renamed (0.31); &lt;code&gt;calculate_kv_scales&lt;/code&gt; removed (0.28)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deprecation or API rename&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;python -m vllm.entrypoints.openai.api_server&lt;/code&gt; deprecated for &lt;code&gt;vllm serve&lt;/code&gt; (0.29); Responses API reasoning stream events renamed (0.32)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The four classes add up to 3 + 8 + 15 + 4 = 30.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are the behavior-change items the dangerous ones?
&lt;/h2&gt;

&lt;p&gt;A removed flag usually announces itself: the deploy fails at launch, or your staging smoke test does. A removed environment variable is murkier, because plenty of programs ignore variables they do not recognize. Whether yours errors out is something to test on your exact version, not something to assume.&lt;/p&gt;

&lt;p&gt;The 8 behavior items are different in kind. The command is identical. The process starts. The health endpoint returns 200. What changed is what the server does with the traffic. Four of them, read as an operator:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max_tokens&lt;/code&gt; no longer reserved from the context window (0.32).&lt;/strong&gt; Capacity plans that assumed a long &lt;code&gt;max_tokens&lt;/code&gt; reserves context for every request may now behave differently. The notes do not say whether admitted concurrency goes up or down for your workload. I would replay traffic with realistic &lt;code&gt;max_tokens&lt;/code&gt; values and compare, rather than guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--enforce-eager&lt;/code&gt; now also disables JIT kernel warmup (0.31).&lt;/strong&gt; If you run eager mode to save memory or to debug, I would expect first-request behavior to change. I have not measured it. Cold-start p99 is the number to compare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YaRN aligned with Transformers (0.30).&lt;/strong&gt; If you serve a long-context checkpoint that relies on YaRN scaling, the effective &lt;code&gt;max_model_len&lt;/code&gt; can differ from before. Compare the context length the server reports at startup, old version against new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default CORS origins restricted (0.32).&lt;/strong&gt; This is a security improvement and the direction is right. It is still a behavior change that can break a browser client that talks to the server directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same family of failure as the baked-in config trap I covered in an earlier post on KV cache dtype: identical flags, different conditions. The fix there was to compare the resolved configuration, not the command line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does falling behind cost?
&lt;/h2&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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25220.28%2520%2528start%2529%2522%252C%2522to%25200.29%2522%252C%2522to%25200.30%2522%252C%2522to%25200.31%2522%252C%2522to%25200.32%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Items%2520to%2520review%2520if%2520you%2520stay%2520on%25200.28%2520and%2520jump%2522%252C%2522data%2522%253A%255B0%252C5%252C11%252C18%252C26%255D%252C%2522borderColor%2522%253A%2522%2523dc2626%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522Items%2520per%2520single%2520step%2520%2528upgrade%2520every%2520release%2529%2522%252C%2522data%2522%253A%255B0%252C5%252C6%252C7%252C8%255D%252C%2522borderColor%2522%253A%2522%25232563eb%2522%252C%2522fill%2522%253Afalse%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Review%2520burden%253A%2520one%2520big%2520jump%2520vs%2520stepwise%2520%2528counts%2520from%2520the%25205%2520release%2520notes%2529%2522%252C%2522fontSize%2522%253A16%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%2522breaking-change%2520items%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%3D900%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25220.28%2520%2528start%2529%2522%252C%2522to%25200.29%2522%252C%2522to%25200.30%2522%252C%2522to%25200.31%2522%252C%2522to%25200.32%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Items%2520to%2520review%2520if%2520you%2520stay%2520on%25200.28%2520and%2520jump%2522%252C%2522data%2522%253A%255B0%252C5%252C11%252C18%252C26%255D%252C%2522borderColor%2522%253A%2522%2523dc2626%2522%252C%2522fill%2522%253Afalse%257D%252C%257B%2522label%2522%253A%2522Items%2520per%2520single%2520step%2520%2528upgrade%2520every%2520release%2529%2522%252C%2522data%2522%253A%255B0%252C5%252C6%252C7%252C8%255D%252C%2522borderColor%2522%253A%2522%25232563eb%2522%252C%2522fill%2522%253Afalse%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Review%2520burden%253A%2520one%2520big%2520jump%2520vs%2520stepwise%2520%2528counts%2520from%2520the%25205%2520release%2520notes%2529%2522%252C%2522fontSize%2522%253A16%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%2522breaking-change%2520items%2522%257D%257D%255D%257D%257D%257D" alt="Items to review: one jump from 0.28 versus stepwise upgrades" width="1800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Arithmetic from the same five release notes. Upgrading one release at a time, you review 5, 6, 7 and 8 items on the way from 0.28 to 0.32. Staying on 0.28 and jumping straight to 0.32, you review 5 + 6 + 7 + 8 = 26 items in a single change window, and you cannot bisect which one broke you because they all landed together.&lt;/p&gt;

&lt;p&gt;The pinned option also has a cost on the security side. The 0.32.0 notes cite three GitHub security advisories as fixed: GHSA-4hhp-h66f-j5j7 (chat template resource exhaustion), GHSA-q97c-p368-75c4 (tokenizer &lt;code&gt;max_token_id&lt;/code&gt;) and GHSA-frvg-qw88-hww9 (P2P ZMQ socket exhaustion). A text search for "GHSA" in the 0.28 to 0.31 bodies finds none. Maintainers can describe a fix without citing an ID, and the notes do not say whether anything is backported to older lines, so read this as "the three advisories I can see are in 0.32.0", not as proof that earlier versions were unsafe.&lt;/p&gt;

&lt;p&gt;At a 13.5-day median gap, a "we upgrade quarterly" policy skips roughly 6 to 7 minor series per jump. Small, frequent, gated upgrades give you a smaller diff each time and let you pick up fixes sooner. That is an opinion, but it follows directly from the numbers above.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you build an upgrade gate?
&lt;/h2&gt;

&lt;p&gt;Four layers, each aimed at a class from the table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;What catches it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dependency or Python pin&lt;/td&gt;
&lt;td&gt;Build the new image in CI, run &lt;code&gt;pip check&lt;/code&gt;, boot it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Removed or renamed&lt;/td&gt;
&lt;td&gt;Startup smoke test with the exact production arguments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default or behavior changed&lt;/td&gt;
&lt;td&gt;Resolved-config diff plus golden request replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deprecation or API rename&lt;/td&gt;
&lt;td&gt;Grep startup logs for deprecation warnings, schedule the removal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 1: extract the breaking items automatically
&lt;/h3&gt;

&lt;p&gt;This script pulls the "Breaking changes" bullet for any release tags you give it. I ran it against v0.29.0 through v0.32.0 and it returned 5, 6, 7 and 8 items, 26 in total, which matches the counts above.&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;json&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;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;breaking_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vllm-project/vllm&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="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;https://api.github.com/repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/releases/tags/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&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="sh"&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;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\* \*\*Breaking changes?\*\*:(.*?)\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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;s&lt;/span&gt; &lt;span class="ow"&gt;in&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&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="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="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;breaking_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;total&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;items&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;## &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tag&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; items&lt;/span&gt;&lt;span class="sh"&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;it&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&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="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="n"&gt;it&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;total&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 as &lt;code&gt;python breaking.py v0.30.0 v0.31.0 v0.32.0&lt;/code&gt;. It depends on the notes keeping this bullet format, which held for 0.28 through 0.32 and did not hold for earlier releases, so treat an empty result as "go read the notes", not as "nothing broke".&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: diff the resolved config, not the command line
&lt;/h3&gt;

&lt;p&gt;Start the candidate version in staging with the exact production arguments. Save the startup log and the model-listing response from the old and the new server, then diff them. You are looking for any value you did not set that moved: context length, default dtypes, enabled endpoints, warmup behavior. This is what catches the YaRN and &lt;code&gt;--enforce-eager&lt;/code&gt; style changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: replay a golden request set
&lt;/h3&gt;

&lt;p&gt;The sketch below is illustrative. I did not run it here because it needs two live endpoints. It sends a fixed set of prompts at temperature 0 to the old and the new server and compares text, finish reason, token count and latency.&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;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&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;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&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;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="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;messages&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;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="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;t0&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="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;120&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&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="nf"&gt;return &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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&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="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finish_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&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;completion_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;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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&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;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&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;prompts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;same&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat_old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat_new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fin&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="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompts&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&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;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&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;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;same&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;a&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="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&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;fin&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;a&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="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;b&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="n"&gt;lat_old&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;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="n"&gt;lat_new&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;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identical_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;same&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finish_reason_changed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fin&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p50_old&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat_old&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p50_new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat_new&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Temperature 0 does not guarantee identical text across engine versions, or even across batch compositions on the same version, because kernels and reduction orders change. So do not demand 100% identical text. Track the identical fraction as a trend, treat a jump in &lt;code&gt;finish_reason_changed&lt;/code&gt; as a red flag, and score a task-level metric (exact match, pass rate) on the same prompts for the decision that matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: canary with a rollback condition
&lt;/h3&gt;

&lt;p&gt;Send a small share of live traffic to the new version and write the rollback condition down before you start: error rate, p99 latency, and one quality proxy. If you cannot name the number that triggers rollback, you are not ready to canary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the limits of this analysis?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Release times are PyPI upload times, not GitHub tag times. The two can differ by hours.&lt;/li&gt;
&lt;li&gt;The breaking-change counts depend on how maintainers wrote one bullet. A longer bullet is not necessarily a bigger break.&lt;/li&gt;
&lt;li&gt;The four-way classification is mine. Reasonable people would move a few entries.&lt;/li&gt;
&lt;li&gt;I covered one engine's notes in depth. SGLang and LMDeploy appear only in the cadence table.&lt;/li&gt;
&lt;li&gt;Nothing here measures performance or accuracy. It measures how much changes, and how loudly.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;How often does vLLM release a new version?&lt;/strong&gt;&lt;br&gt;
Over the 365 days to 2026-10-31, PyPI shows 33 stable releases, including 21 new minor series. The median gap between minor series was 13.5 days, and the last four gaps were 13, 12, 13 and 13 days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the version number tell me whether an upgrade is breaking?&lt;/strong&gt;&lt;br&gt;
Not in vLLM or SGLang. vLLM bumps the middle number every scheduled release, and SGLang stayed within 0.5.x for 19 releases. Read the release notes instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which breaking changes are most dangerous for production?&lt;/strong&gt;&lt;br&gt;
The ones where your launch command still works but behavior differs: changed defaults, changed accounting such as how &lt;code&gt;max_tokens&lt;/code&gt; is treated, changed context-length handling. In the five releases I classified, that was 8 of 30 items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I pin an old version for stability?&lt;/strong&gt;&lt;br&gt;
Pinning is fine as a short-term choice, but it grows the eventual jump (26 items from 0.28 to 0.32) and delays fixes. The 0.32.0 notes list three security advisory fixes. A small upgrade every few weeks, behind a gate, is easier to debug than one big jump.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I find breaking changes without reading every note?&lt;/strong&gt;&lt;br&gt;
Parse the "Breaking changes" bullet with the script above, then read only the entries it returns. Empty output means the format changed, so read the full notes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does temperature 0 give identical output across engine versions?&lt;/strong&gt;&lt;br&gt;
No guarantee. Kernel and scheduling changes can alter floating-point reduction order. Compare identical-text rate as a trend and use a task metric for pass/fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/vllm/" rel="noopener noreferrer"&gt;vllm on PyPI&lt;/a&gt;, &lt;a href="https://pypi.org/project/sglang/" rel="noopener noreferrer"&gt;sglang on PyPI&lt;/a&gt;, &lt;a href="https://pypi.org/project/lmdeploy/" rel="noopener noreferrer"&gt;lmdeploy on PyPI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM release notes: &lt;a href="https://github.com/vllm-project/vllm/releases/tag/v0.28.0" rel="noopener noreferrer"&gt;v0.28.0&lt;/a&gt;, &lt;a href="https://github.com/vllm-project/vllm/releases/tag/v0.29.0" rel="noopener noreferrer"&gt;v0.29.0&lt;/a&gt;, &lt;a href="https://github.com/vllm-project/vllm/releases/tag/v0.30.0" rel="noopener noreferrer"&gt;v0.30.0&lt;/a&gt;, &lt;a href="https://github.com/vllm-project/vllm/releases/tag/v0.31.0" rel="noopener noreferrer"&gt;v0.31.0&lt;/a&gt;, &lt;a href="https://github.com/vllm-project/vllm/releases/tag/v0.32.0" rel="noopener noreferrer"&gt;v0.32.0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&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&lt;/a&gt;&lt;/li&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;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>vllm</category>
      <category>llm</category>
    </item>
    <item>
      <title>One Fleet for Serving and Training? What a 325M Pretraining Run on an Inference NPU Means for AI Infrastructure</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Mon, 26 Oct 2026 06:12:23 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/one-fleet-for-serving-and-training-what-a-325m-pretraining-run-on-an-inference-npu-means-for-ai-22jn</link>
      <guid>https://dev.to/wolfsea2357/one-fleet-for-serving-and-training-what-a-325m-pretraining-run-on-an-inference-npu-means-for-ai-22jn</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: this account is run by members of the VIDRAFT team, and the work discussed below is VIDRAFT's own result. It is not an independent review.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Most AI infrastructure is planned as two fleets: GPUs for training, cheaper accelerators for serving. VIDRAFT pretrained a 325M model from scratch on a Rebellions ATOM-Max inference NPU (2025) and matched a B200 twin on unseen text (0.7993 vs 0.7994). For capacity planners, the interesting question is how much training work could move onto serving hardware.&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%2FFINAL-Bench%2FVKORE-325M-NPU-Base%2Fresolve%2Fmain%2Farticle%2Ffig1_hero.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%2FFINAL-Bench%2FVKORE-325M-NPU-Base%2Fresolve%2Fmain%2Farticle%2Ffig1_hero.png" alt="At a glance" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do teams keep two fleets today?
&lt;/h2&gt;

&lt;p&gt;Serving accelerators are cheaper per query and more power-efficient, but their software runs forward passes only. Anything that needs gradients, from LoRA adaptation to pretraining, goes to GPUs, often rented and often in another region.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes if serving hardware can train?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Utilization.&lt;/strong&gt; Serving fleets idle off-peak; training jobs could fill those hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data locality.&lt;/strong&gt; Adaptation can run where the data and the deployed model already live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Procurement.&lt;/strong&gt; One hardware class covers more of the model lifecycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What was measured?
&lt;/h2&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%2FFINAL-Bench%2FVKORE-325M-NPU-Base%2Fresolve%2Fmain%2Farticle%2Ffig2_training_loss.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%2FFINAL-Bench%2FVKORE-325M-NPU-Base%2Fresolve%2Fmain%2Farticle%2Ffig2_training_loss.png" alt="Training curves" width="799" height="407"&gt;&lt;/a&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%2FFINAL-Bench%2FVKORE-325M-NPU-Base%2Fresolve%2Fmain%2Farticle%2Ffig3_evaluation.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%2FFINAL-Bench%2FVKORE-325M-NPU-Base%2Fresolve%2Fmain%2Farticle%2Ffig3_evaluation.png" alt="Evaluation" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The run covered 5.0B tokens in 76,292 steps over about four days without divergence. On 1,024 unseen windows the paired difference to the GPU twin is -0.0001 (standard error 0.0007).&lt;/p&gt;

&lt;h2&gt;
  
  
  What should planners not assume yet?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Throughput and cost per run&lt;/strong&gt; are not reported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The chip alone does not train.&lt;/strong&gt; The ATOM-Max software is built for inference; training relies on VIDRAFT's own stack and a stability technique under patent application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale&lt;/strong&gt; beyond 325M is unproven.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Which NPU?&lt;/strong&gt; Rebellions ATOM-Max, released in 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the results be checked?&lt;/strong&gt; Yes: weights and evaluate_unseen.py are in &lt;a href="https://huggingface.co/FINAL-Bench/VKORE-325M-NPU-Base" rel="noopener noreferrer"&gt;the model repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Technical report: &lt;a href="https://huggingface.co/blog/FINAL-Bench/pretraining-a-325m-language-model-from-zero-on-an" rel="noopener noreferrer"&gt;https://huggingface.co/blog/FINAL-Bench/pretraining-a-325m-language-model-from-zero-on-an&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>infrastructure</category>
      <category>hardware</category>
    </item>
    <item>
      <title>AI-automated attacks are forcing application security to automate defense</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Fri, 23 Oct 2026 09:23:13 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/ai-automated-attacks-are-forcing-application-security-to-automate-defense-5fpe</link>
      <guid>https://dev.to/wolfsea2357/ai-automated-attacks-are-forcing-application-security-to-automate-defense-5fpe</guid>
      <description>&lt;p&gt;Through 2026, the story in application security has been automation on the offensive side. Open-source AI penetration tools now chain reconnaissance, injection, authentication bypass and bulk data access with little human direction. The techniques are old; the speed and autonomy are new.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why speed changes the math
&lt;/h2&gt;

&lt;p&gt;When an attacker's loop runs in minutes without supervision, a defense that depends on a human scheduling scans and triaging results cannot keep pace. The response across the industry has been to automate the defensive loop in three ways.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Autonomous auditing: tooling that forms its own hypotheses and widens coverage without a human specifying every step.&lt;/li&gt;
&lt;li&gt;Verification layers: a second pass, increasingly an LLM, that filters false positives so teams act on evidence rather than noise.&lt;/li&gt;
&lt;li&gt;Daily fingerprint refresh: keeping a vulnerability database current so newly disclosed flaws are caught the same day.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The non-destructive constraint
&lt;/h2&gt;

&lt;p&gt;There is a catch. Active, attack-style scanning can take production down. Mature teams draw a hard line between offensive testing in isolated environments and non-destructive auditing against live systems. The useful defensive tools stay non-destructive by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data-sovereignty angle
&lt;/h2&gt;

&lt;p&gt;For regulated sectors, the other constraint is that code and findings cannot leave the network. That is pushing interest toward on-prem models for the verification layer, so the whole loop can run inside a boundary. Whether on-prem matches cloud accuracy is now a live benchmark question, and an interesting one to watch.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>appsec</category>
      <category>devops</category>
    </item>
    <item>
      <title>KV Cache Offloading to NVMe: The Break-Even Bandwidth Math and Where It Does Not Pay Off</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:04:58 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/kv-cache-offloading-to-nvme-the-break-even-bandwidth-math-and-where-it-does-not-pay-off-1mo6</link>
      <guid>https://dev.to/wolfsea2357/kv-cache-offloading-to-nvme-the-break-even-bandwidth-math-and-where-it-does-not-pay-off-1mo6</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loading a cached KV prefix from NVMe only beats recomputing it when the drive can deliver more bytes per second than the break-even bandwidth for that prompt length, model, and GPU.&lt;/li&gt;
&lt;li&gt;In a recent measurement study (arXiv 2609.11744, Llama 3.2 3B on an H100 node), the break-even bandwidth fell from 23.2 GB/s at 1k tokens to 10.4 GB/s at 8k and 3.5 GB/s at 80k tokens. A drive that sustains 13.5 GB/s clears the bar from 8k up and misses it at 1k.&lt;/li&gt;
&lt;li&gt;By a first-order model, bigger models are easier to offload: KV bytes per token grow slowly with model size, while prefill FLOPs per token grow with parameter count. A 70B-class model needs about 1 GB/s in the illustrative calculation below, and a 3B model needs several times that.&lt;/li&gt;
&lt;li&gt;The same study found a trace replay on an H100 where the average request sat below the SSD break-even point (6,203 tokens for Qwen3 4B) and external caching gave no TTFT gain over GPU prefix caching alone.&lt;/li&gt;
&lt;li&gt;Treat external KV caching as an admission decision made per prefix, not a switch you turn on for a cluster.&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%3D1000%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25221k%2520tokens%2522%252C%25228k%2520tokens%2522%252C%252280k%2520tokens%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522type%2522%253A%2522line%2522%252C%2522label%2522%253A%2522bandwidth%2520needed%2520to%2520match%2520recompute%2520%2528measured%2529%2522%252C%2522data%2522%253A%255B23.2%252C10.4%252C3.5%255D%252C%2522borderColor%2522%253A%2522%2523d62728%2522%252C%2522backgroundColor%2522%253A%2522%2523d62728%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A5%257D%252C%257B%2522type%2522%253A%2522line%2522%252C%2522label%2522%253A%2522PCIe%25205.0%2520NVMe%252C%2520sustained%2520read%2520%252813.5%2520GB%2Fs%2529%2522%252C%2522data%2522%253A%255B13.5%252C13.5%252C13.5%255D%252C%2522borderColor%2522%253A%2522%25231f77b4%2522%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A0%252C%2522borderDash%2522%253A%255B6%252C6%255D%257D%252C%257B%2522type%2522%253A%2522line%2522%252C%2522label%2522%253A%2522PCIe%25204.0%2520NVMe%252C%2520sustained%2520read%2520%25286%2520GB%2Fs%2529%2522%252C%2522data%2522%253A%255B6.0%252C6.0%252C6.0%255D%252C%2522borderColor%2522%253A%2522%25237f7f7f%2522%252C%2522backgroundColor%2522%253A%2522%25237f7f7f%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A0%252C%2522borderDash%2522%253A%255B6%252C6%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520offload%2520break-even%253A%2520longer%2520prompts%2520need%2520less%2520bandwidth%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522prompt%2520length%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522GB%2Fs%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%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%3D1000%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25221k%2520tokens%2522%252C%25228k%2520tokens%2522%252C%252280k%2520tokens%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522type%2522%253A%2522line%2522%252C%2522label%2522%253A%2522bandwidth%2520needed%2520to%2520match%2520recompute%2520%2528measured%2529%2522%252C%2522data%2522%253A%255B23.2%252C10.4%252C3.5%255D%252C%2522borderColor%2522%253A%2522%2523d62728%2522%252C%2522backgroundColor%2522%253A%2522%2523d62728%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A5%257D%252C%257B%2522type%2522%253A%2522line%2522%252C%2522label%2522%253A%2522PCIe%25205.0%2520NVMe%252C%2520sustained%2520read%2520%252813.5%2520GB%2Fs%2529%2522%252C%2522data%2522%253A%255B13.5%252C13.5%252C13.5%255D%252C%2522borderColor%2522%253A%2522%25231f77b4%2522%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A0%252C%2522borderDash%2522%253A%255B6%252C6%255D%257D%252C%257B%2522type%2522%253A%2522line%2522%252C%2522label%2522%253A%2522PCIe%25204.0%2520NVMe%252C%2520sustained%2520read%2520%25286%2520GB%2Fs%2529%2522%252C%2522data%2522%253A%255B6.0%252C6.0%252C6.0%255D%252C%2522borderColor%2522%253A%2522%25237f7f7f%2522%252C%2522backgroundColor%2522%253A%2522%25237f7f7f%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A0%252C%2522borderDash%2522%253A%255B6%252C6%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520offload%2520break-even%253A%2520longer%2520prompts%2520need%2520less%2520bandwidth%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522prompt%2520length%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522GB%2Fs%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%257D%257D" alt="KV offload break-even bandwidth versus prompt length" width="2000" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What question does a KV offload tier actually answer?
&lt;/h2&gt;

&lt;p&gt;Prefix caching stores the key and value tensors computed for a prompt prefix so a later request with the same prefix can skip that part of the prefill. When the GPU cache is too small to hold every prefix you care about, an offload tier (CPU DRAM, then NVMe) keeps more of them. The question is never "is a bigger cache better". It is whether, for one specific hit, this inequality holds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t_fixed + (prefix_tokens * kv_bytes_per_token) / effective_bandwidth  &amp;lt;  t_prefill(prefix_tokens)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The left side is the cost of a hit: a fixed scheduling and copy overhead plus the transfer. The right side is what the GPU would spend recomputing the same prefix. If the left side is larger, the cache made the request slower. The py-kvcache study states it directly: for short prefixes or fast GPUs, recomputation can be faster than loading from an external cache.&lt;/p&gt;

&lt;p&gt;Two terms in that inequality scale differently. KV bytes grow linearly with prefix length. Prefill time grows faster than linearly, because attention cost adds a term quadratic in length on top of the linear matrix multiplications. So longer prefixes are better candidates for offload, and the bandwidth you need goes down as prompts get longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much bandwidth does break-even need?
&lt;/h2&gt;

&lt;p&gt;The py-kvcache authors computed the bandwidth a cache path must sustain for a fully cached prompt to tie with recomputation. For Llama 3.2 3B, which stores 114,688 bytes of KV per token in BF16, on their H100 node (arXiv 2609.11744, section 5):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt length&lt;/th&gt;
&lt;th&gt;Break-even bandwidth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1k tokens&lt;/td&gt;
&lt;td&gt;23.2 GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k tokens&lt;/td&gt;
&lt;td&gt;10.4 GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80k tokens&lt;/td&gt;
&lt;td&gt;3.5 GB/s&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%25221k%2520tokens%2522%252C%25228k%2520tokens%2522%252C%252280k%2520tokens%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522bandwidth%2520needed%2520%2528GB%2Fs%2529%252C%2520measured%2522%252C%2522data%2522%253A%255B23.2%252C10.4%252C3.5%255D%252C%2522backgroundColor%2522%253A%2522%2523d62728%2522%257D%252C%257B%2522type%2522%253A%2522line%2522%252C%2522label%2522%253A%2522PCIe%25205.0%2520NVMe%2520sustained%2520read%2520%252813.5%2529%2522%252C%2522data%2522%253A%255B13.5%252C13.5%252C13.5%255D%252C%2522borderColor%2522%253A%2522%25231f77b4%2522%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A0%252C%2522borderDash%2522%253A%255B6%252C6%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Measured%2520break-even%2520bandwidth%252C%2520Llama%25203.2%25203B%2520on%2520H100%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522prompt%2520length%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522GB%2Fs%2520needed%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%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%25221k%2520tokens%2522%252C%25228k%2520tokens%2522%252C%252280k%2520tokens%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522bandwidth%2520needed%2520%2528GB%2Fs%2529%252C%2520measured%2522%252C%2522data%2522%253A%255B23.2%252C10.4%252C3.5%255D%252C%2522backgroundColor%2522%253A%2522%2523d62728%2522%257D%252C%257B%2522type%2522%253A%2522line%2522%252C%2522label%2522%253A%2522PCIe%25205.0%2520NVMe%2520sustained%2520read%2520%252813.5%2529%2522%252C%2522data%2522%253A%255B13.5%252C13.5%252C13.5%255D%252C%2522borderColor%2522%253A%2522%25231f77b4%2522%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A0%252C%2522borderDash%2522%253A%255B6%252C6%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Measured%2520break-even%2520bandwidth%252C%2520Llama%25203.2%25203B%2520on%2520H100%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522prompt%2520length%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522GB%2Fs%2520needed%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%257D%257D" alt="Measured break-even bandwidth at 1k, 8k and 80k tokens" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Compare that with what the hardware delivered in the same study. A GPU to CPU copy sustained 54 to 56 GB/s on the PCIe 5.0 node, so a DRAM tier clears the requirement at every length tested. The llm-d disk path moved 9.0 GB/s at 1k tokens and 11.2 to 12.0 GB/s at larger sizes, which leaves it below the requirement at 1k and 2k tokens and above it from 8k onward. Their &lt;code&gt;fio&lt;/code&gt; reference was 13.5 GB/s for the PCIe 5.0 drive and 6 GB/s for a PCIe 4.0 drive.&lt;/p&gt;

&lt;p&gt;Two consequences follow. Storage bandwidth sets a minimum useful prompt length: on that setup, an 8k prompt needed 77.8% of its tokens to be a cached prefix to break even, while at 80k tokens a prefix of only 7.8% was enough. And a PCIe 4.0 drive at 6 GB/s fails the bar for this model until prompts are well past 8k tokens, so the "NVMe offload" label hides a factor of two between drive generations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do larger models make offload easier?
&lt;/h2&gt;

&lt;p&gt;Take the prefill cost per token for a dense model as about &lt;code&gt;2 * P&lt;/code&gt; FLOPs, where P is the parameter count. KV bytes per token are &lt;code&gt;layers * 2 * kv_heads * head_dim * dtype_bytes&lt;/code&gt;. Dividing the two gives the bandwidth needed to keep pace with the GPU at long context, ignoring attention and overhead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;break_even_bandwidth  ~=  kv_bytes_per_token * effective_FLOPs / (2 * P)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grouped-query attention keeps &lt;code&gt;kv_heads&lt;/code&gt; at 8 for many current open models, so KV bytes per token barely move between a 3B and a 70B model, while P grows more than twentyfold. I read the configs for three open models from their Hugging Face repositories and took the Llama 3.2 3B value from the paper:&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%2522Llama%25203.2%25203B%2522%252C%2522Qwen3%25204B%2522%252C%2522Qwen3%252032B%2522%252C%2522Llama%25203.3%252070B%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522KV%2520cache%2520KiB%2520per%2520token%2520%2528BF16%2529%2522%252C%2522data%2522%253A%255B112.0%252C144.0%252C256.0%252C320.0%255D%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520cache%2520size%2520per%2520token%2520by%2520model%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522model%2522%257D%257D%255D%252C%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%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%25203.2%25203B%2522%252C%2522Qwen3%25204B%2522%252C%2522Qwen3%252032B%2522%252C%2522Llama%25203.3%252070B%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522KV%2520cache%2520KiB%2520per%2520token%2520%2528BF16%2529%2522%252C%2522data%2522%253A%255B112.0%252C144.0%252C256.0%252C320.0%255D%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522KV%2520cache%2520size%2520per%2520token%2520by%2520model%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522model%2522%257D%257D%255D%252C%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%257D%257D" alt="KV cache KiB per token for four models" width="1600" height="840"&gt;&lt;/a&gt;&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;KV bytes per token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 3B&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;114,688 (112 KiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 4B Instruct 2507&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;147,456 (144 KiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 32B&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;262,144 (256 KiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.3 70B Instruct&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;327,680 (320 KiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is the calculation I used, which you can adapt to your own GPU and model:&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;FL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;989e12&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt;   &lt;span class="c1"&gt;# H100 SXM BF16 dense peak x 0.50 achievable (illustrative)
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;t_prefill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P&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;q_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;n_tokens&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;matmul&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;P&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n_tokens&lt;/span&gt;
    &lt;span class="n"&gt;attn&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="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n_tokens&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;q_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="c1"&gt;# causal QK^T and AV
&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;matmul&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;attn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;FL&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;required_bw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kv_bytes_per_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;P&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;q_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;n_tokens&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;kv_bytes_per_token&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;t_prefill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P&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;q_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;n_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Llama 3.3 70B at 32,768 tokens
&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;required_bw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;327_680&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;70.55e9&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;64&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;32_768&lt;/span&gt;&lt;span class="p"&gt;)&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="c1"&gt;# about 0.88 GB/s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results of that model, for prefixes of 8k and 32k tokens:&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;8k tokens&lt;/th&gt;
&lt;th&gt;32k tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 3B&lt;/td&gt;
&lt;td&gt;7.24 GB/s&lt;/td&gt;
&lt;td&gt;4.70 GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 4B&lt;/td&gt;
&lt;td&gt;6.97 GB/s&lt;/td&gt;
&lt;td&gt;4.12 GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 32B&lt;/td&gt;
&lt;td&gt;1.75 GB/s&lt;/td&gt;
&lt;td&gt;1.30 GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.3 70B&lt;/td&gt;
&lt;td&gt;1.07 GB/s&lt;/td&gt;
&lt;td&gt;0.88 GB/s&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%25203.2%25203B%2522%252C%2522Qwen3%25204B%2522%252C%2522Qwen3%252032B%2522%252C%2522Llama%25203.3%252070B%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%25228k-token%2520prefix%2522%252C%2522data%2522%253A%255B7.24%252C6.97%252C1.75%252C1.07%255D%252C%2522backgroundColor%2522%253A%2522%2523ff7f0e%2522%257D%252C%257B%2522label%2522%253A%252232k-token%2520prefix%2522%252C%2522data%2522%253A%255B4.7%252C4.12%252C1.3%252C0.88%255D%252C%2522backgroundColor%2522%253A%2522%2523d62728%2522%257D%252C%257B%2522type%2522%253A%2522line%2522%252C%2522label%2522%253A%2522PCIe%25204.0%2520NVMe%2520%25286%2520GB%2Fs%2529%2522%252C%2522data%2522%253A%255B6.0%252C6.0%252C6.0%252C6.0%255D%252C%2522borderColor%2522%253A%2522%25237f7f7f%2522%252C%2522backgroundColor%2522%253A%2522%25237f7f7f%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A0%252C%2522borderDash%2522%253A%255B6%252C6%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%2520lower%2520bound%2520on%2520break-even%2520bandwidth%2520%2528H100-class%2520compute%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522model%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522GB%2Fs%2520needed%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%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%2522Llama%25203.2%25203B%2522%252C%2522Qwen3%25204B%2522%252C%2522Qwen3%252032B%2522%252C%2522Llama%25203.3%252070B%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%25228k-token%2520prefix%2522%252C%2522data%2522%253A%255B7.24%252C6.97%252C1.75%252C1.07%255D%252C%2522backgroundColor%2522%253A%2522%2523ff7f0e%2522%257D%252C%257B%2522label%2522%253A%252232k-token%2520prefix%2522%252C%2522data%2522%253A%255B4.7%252C4.12%252C1.3%252C0.88%255D%252C%2522backgroundColor%2522%253A%2522%2523d62728%2522%257D%252C%257B%2522type%2522%253A%2522line%2522%252C%2522label%2522%253A%2522PCIe%25204.0%2520NVMe%2520%25286%2520GB%2Fs%2529%2522%252C%2522data%2522%253A%255B6.0%252C6.0%252C6.0%252C6.0%255D%252C%2522borderColor%2522%253A%2522%25237f7f7f%2522%252C%2522backgroundColor%2522%253A%2522%25237f7f7f%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A0%252C%2522borderDash%2522%253A%255B6%252C6%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Illustrative%2520lower%2520bound%2520on%2520break-even%2520bandwidth%2520%2528H100-class%2520compute%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522model%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522GB%2Fs%2520needed%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%257D%257D" alt="Illustrative lower bound on break-even bandwidth by model" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How to read this honestly: these are illustrative numbers from the stated assumptions (H100 SXM class compute at 50% utilization, no fixed overhead), not measurements. They are a lower bound. For Llama 3.2 3B the model gives 7.24 GB/s at 8k and 2.76 GB/s at 80k, against the measured 10.4 and 3.5 GB/s. The gap is the fixed per-hit overhead that the formula ignores, and it matters most for short prompts. The paper also ran on a different H100 variant than the one my constants describe, so do not compare absolute values too closely. What carries over is the shape: the requirement falls with prompt length and falls sharply with model size.&lt;/p&gt;

&lt;p&gt;Two practical readings. A 70B-class model has plenty of headroom even on a PCIe 4.0 drive (about 1 GB/s needed against 6 GB/s available), so offload economics look much better for large dense models. And anything that shrinks KV bytes per token lowers the requirement in proportion: an FP8 KV cache halves it relative to BF16, which ties into the earlier post on KV cache quantization linked below. With tensor parallelism, KV bytes and FLOPs both shard across GPUs, so to first order the per-GPU requirement is unchanged, provided each GPU has its own path to storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the spec sheet bandwidth match what you actually get?
&lt;/h2&gt;

&lt;p&gt;Not by default. The py-kvcache study found several places where the delivered number fell away from the device number:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data amplification.&lt;/strong&gt; In the llm-d connector, a request holding 4.4 GB of useful KV data caused roughly 10 GB to be copied and read or written, because of a minimum staging allocation. The authors reported it upstream and it was fixed in llm-d pull request 589. After removing the minimum allocation, TTFT fell about 50% below LMCache in that test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asymmetric reads and writes.&lt;/strong&gt; The connector's reads reached more than 12 GB/s while writes stayed around 2 GB/s. A workload that writes many new prefixes will see a different picture than one that mostly reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request size.&lt;/strong&gt; Block tracing showed the filesystem issuing mostly 512 KiB requests. In a standalone benchmark, I/O engines differed widely at 4 KiB, but at 1 MiB with one thread every engine clustered near the 6 GB/s that &lt;code&gt;fio&lt;/code&gt; measured on that drive. Swapping the I/O engine did not improve TTFT. KV blocks are far larger than 1 MiB (one 256-token block of Llama 3.2 3B is about 28 MiB), so the workload is limited by bandwidth and transfer placement, not by small-I/O rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling.&lt;/strong&gt; Where a transfer enters the request schedule matters. The authors' py-kvcache connector starts disk reads while a request is still queued, and preloading contributed a 1.34x improvement at 80k tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For capacity planning, measure end-to-end TTFT with your connector on your drives and treat the &lt;code&gt;fio&lt;/code&gt; number as an upper bound.&lt;/p&gt;

&lt;h2&gt;
  
  
  When does external KV caching not pay off?
&lt;/h2&gt;

&lt;p&gt;The most useful result in the study is a negative one. The authors replayed three production-style traces from Alibaba Cloud Bailian (a coder trace, an interactive trace, and an API-driven trace) on two machines.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On the smaller GPU (an RTX 4000 Ada with 20 GiB), py-kvcache was 1.12x to 1.79x faster than GPU prefix caching alone and 1.10x to 1.25x faster than LMCache.&lt;/li&gt;
&lt;li&gt;On the H100 node, the average request length was below the measured 6,203-token SSD break-even point for Qwen3 4B, and the larger GPU memory kept a large share of the working set resident. TTFT with GPU prefix caching alone was identical to py-kvcache, and vLLM's native offload implementation was 0.2 s slower.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same traces flipped from "offload helps" to "offload costs you" depending on the GPU. The authors conclude that external KV caching is a setup-specific admission decision, and that recomputation is a valid fallback rather than a cache miss.&lt;/p&gt;

&lt;p&gt;Where it does pay, the numbers are large. At 80k tokens, loading from disk with py-kvcache was 2.0x faster than LMCache. With GPU, CPU, and disk tiers all enabled it was 1.23x faster than LMCache and within about 4% of vLLM's native KV offload implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you configure KV offload in vLLM, and how do you gate it?
&lt;/h2&gt;

&lt;p&gt;In the vLLM v0.32.0 source (released October 18, 2026), &lt;code&gt;CacheConfig&lt;/code&gt; exposes two offload options. &lt;code&gt;kv_offloading_size&lt;/code&gt; is the buffer size in GiB (summed across tensor-parallel ranks), and offloading is only active when it is set. &lt;code&gt;kv_offloading_backend&lt;/code&gt; accepts &lt;code&gt;native&lt;/code&gt; or &lt;code&gt;lmcache&lt;/code&gt;, and the docstring describes the offload target as CPU. If you want an NVMe tier, the study's comparisons use LMCache's disk support and an external connector, so check what your chosen connector supports.&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;# CPU DRAM tier with the native backend (GiB, summed across TP ranks)&lt;/span&gt;
vllm serve &amp;lt;model&amp;gt; &lt;span class="nt"&gt;--kv-offloading-size&lt;/span&gt; 64 &lt;span class="nt"&gt;--kv-offloading-backend&lt;/span&gt; native
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The admission rule is separate from the tier. The py-kvcache design runs an offline calibration once per node and model, writes a small file with the break-even prefix length per source tier, and at lookup time declines a load that is predicted to cost more than recomputation. You can approximate the same policy:&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;should_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv_bytes_per_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;measured_bw_Bps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;t_recompute_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_fixed_s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;t_load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t_fixed_s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;prefix_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;kv_bytes_per_token&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;measured_bw_Bps&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;t_load&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;t_recompute_s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fill the inputs from your own sweep, not from a data sheet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send prompts at several lengths (for example 1k, 4k, 8k, 16k, 32k, 64k) with all caches cold and record TTFT. That is &lt;code&gt;t_recompute_s&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Send the same prompts again with the prefix resident only in the offload tier, and record TTFT. Subtract the non-transfer part to get the transfer time and the effective bandwidth.&lt;/li&gt;
&lt;li&gt;Repeat with prefix fractions of 10%, 50%, and 90% of the prompt, since the break-even frontier depends on both length and fraction.&lt;/li&gt;
&lt;li&gt;Store the break-even prefix length per tier and re-run the sweep whenever the GPU, model, KV dtype, or drive changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One caveat from the same paper: a static threshold on prefix length and bandwidth cannot tell a demand read from a preload that queueing may hide, so a stronger policy would also track staging capacity and per-tier transfer rates.&lt;/p&gt;

&lt;p&gt;A separate benchmark, PrefixBench-H100 (arXiv 2609.19657), reports that cache effectiveness is largely insensitive to concurrency and output length on a single H100. That supports calibrating the break-even point offline.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is NVMe KV cache offloading faster than recomputing the prefill?&lt;/strong&gt;&lt;br&gt;
Only above a break-even bandwidth that depends on prompt length, model, and GPU. In the measured study, a 13.5 GB/s drive beat recomputation for Llama 3.2 3B from 8k tokens up, but a 1k-token prompt needed 23.2 GB/s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I calculate the break-even bandwidth for my model?&lt;/strong&gt;&lt;br&gt;
Divide the bytes of KV for the cached prefix by the time budget left after subtracting non-transfer overhead from the recompute time. A first-order estimate is &lt;code&gt;kv_bytes_per_token * effective_FLOPs / (2 * parameters)&lt;/code&gt;. Measure the fixed overhead on your own stack, because it dominates short prompts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do larger models need less offload bandwidth?&lt;/strong&gt;&lt;br&gt;
With grouped-query attention, KV bytes per token change slowly with model size, while prefill FLOPs per token scale with parameter count. More compute per byte of KV means recomputation is slower, so a slower drive is enough to beat it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I enable KV offload on an H100?&lt;/strong&gt;&lt;br&gt;
It depends on your prefix length distribution. In the study's trace replay on an H100 node, the average request was below the SSD break-even point and GPU prefix caching alone matched the offload connector. Measure your own trace before committing storage hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does vLLM offload KV cache to disk by default?&lt;/strong&gt;&lt;br&gt;
No. In v0.32.0 offloading is off unless &lt;code&gt;kv_offloading_size&lt;/code&gt; is set, and the native backend targets CPU memory per the config docstring. Disk tiers come from connectors such as LMCache or external ones.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/prefilldecode-disaggregation-in-vllm-the-kv-transfer-math-that-decides-if-it-pays-off-13j8"&gt;Prefill/Decode Disaggregation in vLLM: The KV Transfer Math That Decides If It Pays Off&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 Catch It&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Building py-kvcache: A Performance Characterization of External KV Caching for vLLM with NVMe SSDs. &lt;a href="https://arxiv.org/abs/2609.11744" rel="noopener noreferrer"&gt;arXiv 2609.11744&lt;/a&gt; (September 2026). All measured figures attributed to "the study" come from this paper.&lt;/li&gt;
&lt;li&gt;PrefixBench-H100: Characterizing Prefix Reuse and Time-to-First-Token in H100 LLM Serving. &lt;a href="https://arxiv.org/abs/2609.19657" rel="noopener noreferrer"&gt;arXiv 2609.19657&lt;/a&gt; (September 2026).&lt;/li&gt;
&lt;li&gt;vLLM. &lt;a href="https://github.com/vllm-project/vllm/releases/tag/v0.32.0" rel="noopener noreferrer"&gt;v0.32.0 release&lt;/a&gt; and &lt;a href="https://github.com/vllm-project/vllm/blob/v0.32.0/vllm/config/cache.py" rel="noopener noreferrer"&gt;vllm/config/cache.py at v0.32.0&lt;/a&gt; (KV offloading options).&lt;/li&gt;
&lt;li&gt;Model configs read from Hugging Face: &lt;a href="https://huggingface.co/Qwen/Qwen3-4B-Instruct-2507" rel="noopener noreferrer"&gt;Qwen/Qwen3-4B-Instruct-2507&lt;/a&gt;, &lt;a href="https://huggingface.co/Qwen/Qwen3-32B" rel="noopener noreferrer"&gt;Qwen/Qwen3-32B&lt;/a&gt;, &lt;a href="https://huggingface.co/unsloth/Llama-3.3-70B-Instruct" rel="noopener noreferrer"&gt;unsloth/Llama-3.3-70B-Instruct&lt;/a&gt; (mirror of the Llama 3.3 70B config).&lt;/li&gt;
&lt;li&gt;NVIDIA. &lt;a href="https://www.nvidia.com/en-us/data-center/h100/" rel="noopener noreferrer"&gt;H100 Tensor Core GPU&lt;/a&gt; (peak compute figure used in the illustrative model).&lt;/li&gt;
&lt;li&gt;Model outputs in the tables for the four models above are illustrative calculations from the stated assumptions, not measurements.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>vllm</category>
      <category>llm</category>
    </item>
    <item>
      <title>Speculative Decoding at High Batch Size: Where the 3x Speedup Turns Into a Tax</title>
      <dc:creator>AI Tech News</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:05:46 +0000</pubDate>
      <link>https://dev.to/wolfsea2357/speculative-decoding-at-high-batch-size-where-the-3x-speedup-turns-into-a-tax-3da5</link>
      <guid>https://dev.to/wolfsea2357/speculative-decoding-at-high-batch-size-where-the-3x-speedup-turns-into-a-tax-3da5</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Published speculative decoding speedups (2x to 3x) are almost always measured at low concurrency. The two vLLM posts I read this week report no concurrency sweep.&lt;/li&gt;
&lt;li&gt;Speculation spends spare compute to save memory traffic. When the verify pass stops being memory-bound, the spare compute is gone and the extra draft tokens become pure overhead.&lt;/li&gt;
&lt;li&gt;In an illustrative roofline model (27B dense verifier, H100-class GPU, 2k context), verifying 8 tokens per sequence turns compute-bound at about 30 concurrent sequences. Verifying 4 tokens per sequence holds out to about 85.&lt;/li&gt;
&lt;li&gt;The throughput-optimal speculation length falls from 8 at batch 1 to 3 at batch 128. A fixed &lt;code&gt;num_speculative_tokens&lt;/code&gt; is the wrong setting for a server whose load changes during the day.&lt;/li&gt;
&lt;li&gt;The model overstates absolute speedups. Use it for the shape of the curve, then measure your own server with the sweep at the end.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why do the headline speedups all come from batch 1?
&lt;/h2&gt;

&lt;p&gt;Decode is memory-bound at small batch. Every step streams the full set of weights from HBM to produce one token per sequence, and the arithmetic units sit mostly idle. Speculative decoding exploits that idle compute: a cheap drafter proposes several tokens, and the large model verifies all of them in a single forward pass. Rejection sampling keeps the output distribution identical to ordinary decoding, so quality is unchanged (&lt;a href="https://arxiv.org/abs/2211.17192" rel="noopener noreferrer"&gt;Leviathan et al., 2022&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The newest drafters make the drafting side cheaper. The vLLM Speculators team describes three parallel drafting methods: P-EAGLE, DFlash and DSpark. Instead of generating draft tokens one forward pass at a time like EAGLE-3, they predict a whole block of candidates in one pass, which decouples drafting latency from speculation length (&lt;a href="https://vllm.ai/blog/2026-07-28-speculators-parallel-drafting" rel="noopener noreferrer"&gt;vLLM blog, July 2026&lt;/a&gt;). The example launch command in that post 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 Qwen/Qwen3-30B-A3B &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tensor-parallel-size&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--reasoning-parser&lt;/span&gt; qwen3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-config&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "RedHatAI/Qwen3-30B-A3B-speculator.dflash",
    "num_speculative_tokens": 7,
    "method": "dflash"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A follow-up post on AMD GPUs reports measured speedups on MI300X and MI355X: 2.87x for DFlash on Gemma-4-26B-A4B (MATH500) and 2.34x on Gemma-4-31B. The spread across tasks is wide. DSpark on Qwen3-8B ranged from 1.15x on MATH500 to 1.63x on GSM8K (&lt;a href="https://vllm.ai/blog/2026-08-23-speculative-decoding-amd-gpus" rel="noopener noreferrer"&gt;vLLM blog, August 2026&lt;/a&gt;). That post also notes that a broader evaluation across concurrency levels and batch sizes would help, which is the gap this article tries to fill with arithmetic rather than a new benchmark.&lt;/p&gt;

&lt;p&gt;None of this means the technique is overrated. It means the batch size at which a number was measured belongs next to the number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does one speculative iteration actually cost?
&lt;/h2&gt;

&lt;p&gt;The original analysis gives the expected number of tokens produced per verify pass when each draft token is accepted independently with probability alpha and the drafter proposes gamma tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;E(alpha, gamma) = (1 - alpha^(gamma + 1)) / (1 - alpha)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With alpha = 0.8 and gamma = 7 that is 4.16 tokens per iteration. The iteration is not free. It costs one verify pass over (gamma + 1) tokens per sequence plus one drafter pass. Throughput speedup is therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;speedup(B) = E * t_base(B) / ( t_verify(B, gamma + 1) + t_draft(B, gamma) )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything interesting hides in how &lt;code&gt;t_verify&lt;/code&gt; grows with batch size B. At small B it barely grows, because the step is bound by weight and KV-cache reads that do not depend on how many tokens you verify. At large B it grows almost linearly with (gamma + 1), because the step is bound by compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does the roofline model work?
&lt;/h2&gt;

&lt;p&gt;I built the smallest model that captures that transition. Treat every number below as illustrative, not as a measurement of any specific deployment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assumption&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Verifier&lt;/td&gt;
&lt;td&gt;27B dense, BF16 (54 GB of weights)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;H100 SXM class: 3.35 TB/s HBM3, 989 TFLOPS BF16 dense (&lt;a href="https://www.nvidia.com/en-us/data-center/h100/" rel="noopener noreferrer"&gt;NVIDIA H100&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Achievable fraction&lt;/td&gt;
&lt;td&gt;80% of peak bandwidth, 50% of peak compute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV cache&lt;/td&gt;
&lt;td&gt;64 layers, 8 KV heads, head dim 128, BF16: 262,144 bytes per token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;2,048 tokens per sequence (0.54 GB of KV read per step per sequence)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drafter&lt;/td&gt;
&lt;td&gt;Parallel drafter, 4% of verifier parameters, one pass per iteration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step time&lt;/td&gt;
&lt;td&gt;max(memory time, compute time)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The core of the script is short:&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;t_step&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;tokens_per_seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frac&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;frac&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;KV_SEQ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;BW&lt;/span&gt;          &lt;span class="c1"&gt;# weights + KV reads
&lt;/span&gt;    &lt;span class="n"&gt;cmp_&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;P&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;frac&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tokens_per_seq&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;FL&lt;/span&gt;
    &lt;span class="k"&gt;return&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;mem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmp_&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;speedup&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;gamma&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_tokens&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;t_step&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;t_step&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;gamma&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="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;t_step&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;gamma&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.04&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;expected_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One closed form falls out of the model. With weight-read time &lt;code&gt;t_w&lt;/code&gt; (20.1 ms here), KV-read time per sequence &lt;code&gt;k&lt;/code&gt; (0.20 ms), and compute time per token &lt;code&gt;c&lt;/code&gt; (0.109 ms), the verify pass becomes compute-bound at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;B* = t_w / ((gamma + 1) * c - k)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gamma&lt;/th&gt;
&lt;th&gt;verify turns compute-bound at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;about 1,100 sequences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;about 85 sequences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;about 44 sequences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;about 30 sequences&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Past B*, every extra draft token costs real GPU time. Below it, extra draft tokens are almost free. That single threshold explains the rest of the article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why should you not trust an independent-acceptance model for long drafts?
&lt;/h2&gt;

&lt;p&gt;The formula above assumes every position is accepted with the same probability. Real drafters degrade with distance. The AMD post reports, for Gemma-4-26B with Gemma 4 MTP at N=5, a 94% first-position acceptance, 66% at the fifth position, and 80.0% overall. Under independent acceptance at 80%, gamma = 7 gives 4.16 tokens per iteration. If acceptance declines linearly from 94% by 7 points per position (my interpolation between the two reported endpoints, treating each as a conditional acceptance), gamma = 7 gives 4.49 tokens, but the marginal gain from each extra position collapses:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gamma&lt;/th&gt;
&lt;th&gt;expected tokens per iteration (decaying acceptance)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2.76&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3.41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;4.49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;4.53&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Going from gamma = 5 to gamma = 8 buys 0.33 tokens per iteration while adding 3 more verified tokens per sequence. At low batch that trade is free. At high batch it is not.&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%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25221%2522%252C%25222%2522%252C%25223%2522%252C%25224%2522%252C%25225%2522%252C%25226%2522%252C%25227%2522%252C%25228%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522alpha%253D0.5%2522%252C%2522data%2522%253A%255B1.5%252C1.75%252C1.88%252C1.94%252C1.97%252C1.98%252C1.99%252C2.0%255D%252C%2522borderColor%2522%253A%2522%25237f7f7f%2522%252C%2522backgroundColor%2522%253A%2522%25237f7f7f%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%252C%257B%2522label%2522%253A%2522alpha%253D0.7%2522%252C%2522data%2522%253A%255B1.7%252C2.19%252C2.53%252C2.77%252C2.94%252C3.06%252C3.14%252C3.2%255D%252C%2522borderColor%2522%253A%2522%25231f77b4%2522%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%252C%257B%2522label%2522%253A%2522alpha%253D0.9%2522%252C%2522data%2522%253A%255B1.9%252C2.71%252C3.44%252C4.1%252C4.69%252C5.22%252C5.7%252C6.13%255D%252C%2522borderColor%2522%253A%2522%2523d62728%2522%252C%2522backgroundColor%2522%253A%2522%2523d62728%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Expected%2520tokens%2520per%2520verify%2520pass%2520saturates%2520with%2520speculation%2520length%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522speculation%2520length%2520gamma%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522expected%2520tokens%2520per%2520iteration%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%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%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25221%2522%252C%25222%2522%252C%25223%2522%252C%25224%2522%252C%25225%2522%252C%25226%2522%252C%25227%2522%252C%25228%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522alpha%253D0.5%2522%252C%2522data%2522%253A%255B1.5%252C1.75%252C1.88%252C1.94%252C1.97%252C1.98%252C1.99%252C2.0%255D%252C%2522borderColor%2522%253A%2522%25237f7f7f%2522%252C%2522backgroundColor%2522%253A%2522%25237f7f7f%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%252C%257B%2522label%2522%253A%2522alpha%253D0.7%2522%252C%2522data%2522%253A%255B1.7%252C2.19%252C2.53%252C2.77%252C2.94%252C3.06%252C3.14%252C3.2%255D%252C%2522borderColor%2522%253A%2522%25231f77b4%2522%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%252C%257B%2522label%2522%253A%2522alpha%253D0.9%2522%252C%2522data%2522%253A%255B1.9%252C2.71%252C3.44%252C4.1%252C4.69%252C5.22%252C5.7%252C6.13%255D%252C%2522borderColor%2522%253A%2522%2523d62728%2522%252C%2522backgroundColor%2522%253A%2522%2523d62728%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Expected%2520tokens%2520per%2520verify%2520pass%2520saturates%2520with%2520speculation%2520length%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522speculation%2520length%2520gamma%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522expected%2520tokens%2520per%2520iteration%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%257D%257D" alt="Expected tokens per verify pass saturate with speculation length" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the speedup curve look like as concurrency grows?
&lt;/h2&gt;

&lt;p&gt;I ran the model with the decaying-acceptance assumption. Values are throughput relative to plain decoding at the same batch size.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrent sequences&lt;/th&gt;
&lt;th&gt;gamma = 2&lt;/th&gt;
&lt;th&gt;gamma = 4&lt;/th&gt;
&lt;th&gt;gamma = 7&lt;/th&gt;
&lt;th&gt;Best gamma (speedup)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2.63x&lt;/td&gt;
&lt;td&gt;3.71x&lt;/td&gt;
&lt;td&gt;4.28x&lt;/td&gt;
&lt;td&gt;8 (4.32x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2.48x&lt;/td&gt;
&lt;td&gt;3.50x&lt;/td&gt;
&lt;td&gt;4.04x&lt;/td&gt;
&lt;td&gt;8 (4.08x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;2.17x&lt;/td&gt;
&lt;td&gt;3.06x&lt;/td&gt;
&lt;td&gt;3.39x&lt;/td&gt;
&lt;td&gt;6 (3.45x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;1.95x&lt;/td&gt;
&lt;td&gt;2.64x&lt;/td&gt;
&lt;td&gt;2.13x&lt;/td&gt;
&lt;td&gt;4 (2.64x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;1.75x&lt;/td&gt;
&lt;td&gt;1.85x&lt;/td&gt;
&lt;td&gt;1.49x&lt;/td&gt;
&lt;td&gt;3 (1.90x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;1.45x&lt;/td&gt;
&lt;td&gt;1.45x&lt;/td&gt;
&lt;td&gt;1.16x&lt;/td&gt;
&lt;td&gt;3 (1.49x)&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%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25221%2522%252C%25228%2522%252C%252216%2522%252C%252232%2522%252C%252264%2522%252C%2522128%2522%252C%2522256%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522gamma%253D2%2522%252C%2522data%2522%253A%255B2.63%252C2.48%252C2.35%252C2.17%252C1.95%252C1.75%252C1.45%255D%252C%2522borderColor%2522%253A%2522%25231f77b4%2522%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%252C%257B%2522label%2522%253A%2522gamma%253D4%2522%252C%2522data%2522%253A%255B3.71%252C3.5%252C3.32%252C3.06%252C2.64%252C1.85%252C1.45%255D%252C%2522borderColor%2522%253A%2522%25232ca02c%2522%252C%2522backgroundColor%2522%253A%2522%25232ca02c%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%252C%257B%2522label%2522%253A%2522gamma%253D7%2522%252C%2522data%2522%253A%255B4.28%252C4.04%252C3.83%252C3.39%252C2.13%252C1.49%252C1.16%255D%252C%2522borderColor%2522%253A%2522%2523d62728%2522%252C%2522backgroundColor%2522%253A%2522%2523d62728%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%252C%257B%2522label%2522%253A%2522break-even%2520%25281.0x%2529%2522%252C%2522data%2522%253A%255B1.0%252C1.0%252C1.0%252C1.0%252C1.0%252C1.0%252C1.0%255D%252C%2522borderColor%2522%253A%2522%25237f7f7f%2522%252C%2522backgroundColor%2522%253A%2522%25237f7f7f%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A0%252C%2522borderDash%2522%253A%255B6%252C6%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Speculative%2520decoding%2520speedup%2520vs%2520concurrent%2520sequences%2520%2528illustrative%2520roofline%2520model%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522concurrent%2520sequences%2520%2528batch%2520size%2529%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522throughput%2520vs%2520no%2520speculation%2520%2528x%2529%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%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%2522line%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%25221%2522%252C%25228%2522%252C%252216%2522%252C%252232%2522%252C%252264%2522%252C%2522128%2522%252C%2522256%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522gamma%253D2%2522%252C%2522data%2522%253A%255B2.63%252C2.48%252C2.35%252C2.17%252C1.95%252C1.75%252C1.45%255D%252C%2522borderColor%2522%253A%2522%25231f77b4%2522%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%252C%257B%2522label%2522%253A%2522gamma%253D4%2522%252C%2522data%2522%253A%255B3.71%252C3.5%252C3.32%252C3.06%252C2.64%252C1.85%252C1.45%255D%252C%2522borderColor%2522%253A%2522%25232ca02c%2522%252C%2522backgroundColor%2522%253A%2522%25232ca02c%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%252C%257B%2522label%2522%253A%2522gamma%253D7%2522%252C%2522data%2522%253A%255B4.28%252C4.04%252C3.83%252C3.39%252C2.13%252C1.49%252C1.16%255D%252C%2522borderColor%2522%253A%2522%2523d62728%2522%252C%2522backgroundColor%2522%253A%2522%2523d62728%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A4%257D%252C%257B%2522label%2522%253A%2522break-even%2520%25281.0x%2529%2522%252C%2522data%2522%253A%255B1.0%252C1.0%252C1.0%252C1.0%252C1.0%252C1.0%252C1.0%255D%252C%2522borderColor%2522%253A%2522%25237f7f7f%2522%252C%2522backgroundColor%2522%253A%2522%25237f7f7f%2522%252C%2522fill%2522%253Afalse%252C%2522borderWidth%2522%253A3%252C%2522pointRadius%2522%253A0%252C%2522borderDash%2522%253A%255B6%252C6%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Speculative%2520decoding%2520speedup%2520vs%2520concurrent%2520sequences%2520%2528illustrative%2520roofline%2520model%2529%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522concurrent%2520sequences%2520%2528batch%2520size%2529%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522throughput%2520vs%2520no%2520speculation%2520%2528x%2529%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%257D%257D" alt="Speculative decoding speedup versus concurrent sequences, illustrative roofline model" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things stand out.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The curves cross.&lt;/strong&gt; The long draft (gamma = 7) wins until about 32 sequences, then falls behind the short one by 64. That matches the closed form: gamma = 7 turns compute-bound near 30 sequences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The optimum moves.&lt;/strong&gt; The best speculation length is 8 up to 16 sequences, 6 at 32, 4 at 64, and 3 from 128 up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speculation rarely turns negative in this model, but it can.&lt;/strong&gt; With the decaying acceptance curve, gamma = 7 first drops below 1.0x at about 513 sequences and gamma = 8 at about 329. With independent acceptance at alpha = 0.6, gamma = 7 breaks even at only 84 sequences. A weaker drafter or a harder task moves the break-even left fast.&lt;/li&gt;
&lt;/ol&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%25221%2522%252C%25228%2522%252C%252216%2522%252C%252232%2522%252C%252264%2522%252C%2522128%2522%252C%2522256%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522throughput-optimal%2520speculation%2520length%2522%252C%2522data%2522%253A%255B8%252C8%252C8%252C6%252C4%252C3%252C3%255D%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Best%2520speculation%2520length%2520shrinks%2520as%2520batch%2520grows%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522concurrent%2520sequences%2520%2528batch%2520size%2529%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522best%2520num_speculative_tokens%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%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%25221%2522%252C%25228%2522%252C%252216%2522%252C%252232%2522%252C%252264%2522%252C%2522128%2522%252C%2522256%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522throughput-optimal%2520speculation%2520length%2522%252C%2522data%2522%253A%255B8%252C8%252C8%252C6%252C4%252C3%252C3%255D%252C%2522backgroundColor%2522%253A%2522%25231f77b4%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522responsive%2522%253Afalse%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Best%2520speculation%2520length%2520shrinks%2520as%2520batch%2520grows%2522%252C%2522fontSize%2522%253A18%257D%252C%2522scales%2522%253A%257B%2522xAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522concurrent%2520sequences%2520%2528batch%2520size%2529%2522%257D%257D%255D%252C%2522yAxes%2522%253A%255B%257B%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522best%2520num_speculative_tokens%2522%257D%252C%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%257D%255D%257D%257D%257D" alt="Throughput-optimal speculation length versus batch size" width="1600" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What does this model leave out?
&lt;/h2&gt;

&lt;p&gt;Be suspicious of the absolute numbers. A 4.3x speedup at batch 1 is higher than the 2x to 3x measured in the posts above, because the model ignores scheduler overhead, drafter launch latency, sampling, and the real shape of acceptance across tasks. It also has these specific blind spots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mixture-of-experts verifiers.&lt;/strong&gt; A dense model touches all weights at every step. An MoE touches only the experts routed to. Verifying more tokens per sequence activates more distinct experts, so the verify pass reads more weight bytes as gamma grows. That shifts the break-even left compared with a dense model of the same total size. I did not model it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Longer context.&lt;/strong&gt; The KV term &lt;code&gt;k&lt;/code&gt; grows with context length. A larger &lt;code&gt;k&lt;/code&gt; makes the base step more memory-bound for longer, which pushes B* to the right (the denominator in the closed form shrinks). Long-context workloads can tolerate longer drafts at a given batch size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefix caching and chunked prefill.&lt;/strong&gt; Prefill chunks share each step with decode tokens and consume compute that the verify pass would otherwise use. The model treats decode in isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drafter quality varies by traffic.&lt;/strong&gt; Acceptance on code and math is usually higher than on open-ended chat. A single alpha across your traffic mix is a fiction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How do you measure this on your own server?
&lt;/h2&gt;

&lt;p&gt;Sweep concurrency with speculation on and off, and log acceptance per position. Use identical prompts and output lengths for both arms, and warm any prefix cache the same way before each run so one arm does not inherit a hotter cache. Flag names below match recent vLLM; check &lt;code&gt;vllm bench serve --help&lt;/code&gt; on your build.&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;# Arm A: speculation off.  Arm B: same command plus --speculative-config.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;C &lt;span class="k"&gt;in &lt;/span&gt;1 8 32 64 128 256&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;vllm bench serve &lt;span class="nt"&gt;--model&lt;/span&gt; &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="nt"&gt;--dataset-name&lt;/span&gt; random &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--random-input-len&lt;/span&gt; 1024 &lt;span class="nt"&gt;--random-output-len&lt;/span&gt; 256 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--num-prompts&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;C &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; &lt;span class="nv"&gt;$C&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--save-result&lt;/span&gt; &lt;span class="nt"&gt;--result-filename&lt;/span&gt; &lt;span class="s2"&gt;"specoff_c&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;C&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then read the acceptance counters from the server's Prometheus endpoint. Metric names starting with &lt;code&gt;vllm:spec_decode&lt;/code&gt; carry draft and accepted token counts, and recent builds expose accepted tokens per position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8000/metrics | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^vllm:spec_decode'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compute accepted tokens per position as a conditional rate (position k accepted given k-1 accepted) before fitting any model. If conditional acceptance at position 6 is below 50%, a gamma of 7 is probably costing you at any load above light.&lt;/p&gt;

&lt;p&gt;Two more rules from earlier posts in this series apply here. Record the effective configuration the engine reports rather than the one you intended, and count failed or timed-out requests as outcomes instead of dropping them from the denominator.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a practical tuning rule?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Compute B* for your hardware and context length with the closed form, using your measured weight-read time per step and measured per-token compute time.&lt;/li&gt;
&lt;li&gt;Pick gamma so that B* for that gamma sits above your p95 concurrency per replica, then verify with the sweep.&lt;/li&gt;
&lt;li&gt;If load swings widely, either route low-concurrency traffic to a speculative replica and high-concurrency traffic to a plain one, or check whether your engine can shorten or skip speculation dynamically under load. Do not assume it does; look for it in the release notes of your version.&lt;/li&gt;
&lt;li&gt;Re-measure whenever the drafter, the model or the traffic mix changes. Acceptance is a property of the traffic, not of the model card.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;Does speculative decoding change the output of the model?&lt;/strong&gt;&lt;br&gt;
No. With rejection sampling, the output distribution matches ordinary decoding exactly, so quality is mathematically identical (&lt;a href="https://arxiv.org/abs/2211.17192" rel="noopener noreferrer"&gt;Leviathan et al.&lt;/a&gt;; the vLLM Speculators post makes the same statement). What changes is latency and throughput.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does speculative decoding help less at high batch size?&lt;/strong&gt;&lt;br&gt;
At high batch size the GPU is already busy producing tokens for many sequences, so there is little idle compute to spend on verification. Each extra draft token then costs real compute time while the number of accepted tokens per iteration saturates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What speculation length should I use in vLLM?&lt;/strong&gt;&lt;br&gt;
Start from the value in the drafter's model card (the vLLM example above uses 7 for DFlash), then sweep lower values under your real concurrency. In the illustrative model the best value fell from 8 at batch 1 to 3 at batch 128.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are parallel drafters like DFlash and P-EAGLE better than EAGLE-3 under load?&lt;/strong&gt;&lt;br&gt;
They make drafting cheaper because one drafter pass covers the whole block, which helps when drafting latency matters. They do not change the verify pass, so the compute-bound threshold for the verifier is unchanged. Compare them with your own concurrency sweep instead of relying on a single-batch number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can speculative decoding make throughput worse?&lt;/strong&gt;&lt;br&gt;
Yes, when the verify pass is compute-bound and acceptance is low. In this model, a weak drafter (alpha of 0.6) with gamma = 7 falls below plain decoding at about 84 concurrent sequences under independent acceptance.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfsea2357/prefilldecode-disaggregation-in-vllm-the-kv-transfer-math-that-decides-if-it-pays-off-13j8"&gt;Prefill/Decode Disaggregation in vLLM: The KV Transfer Math That Decides If It Pays Off&lt;/a&gt;&lt;/li&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;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Leviathan, Kalman, Matias. &lt;a href="https://arxiv.org/abs/2211.17192" rel="noopener noreferrer"&gt;Fast Inference from Transformers via Speculative Decoding&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM blog. &lt;a href="https://vllm.ai/blog/2026-07-28-speculators-parallel-drafting" rel="noopener noreferrer"&gt;Parallel All the Way Down: Beyond Single-Token Generation with Speculative Decoding&lt;/a&gt; (July 28, 2026)&lt;/li&gt;
&lt;li&gt;vLLM blog. &lt;a href="https://vllm.ai/blog/2026-08-23-speculative-decoding-amd-gpus" rel="noopener noreferrer"&gt;Exploring Speculative Decoding in vLLM on AMD GPUs&lt;/a&gt; (August 23, 2026)&lt;/li&gt;
&lt;li&gt;NVIDIA. &lt;a href="https://www.nvidia.com/en-us/data-center/h100/" rel="noopener noreferrer"&gt;H100 Tensor Core GPU&lt;/a&gt; (peak bandwidth and compute figures)&lt;/li&gt;
&lt;li&gt;All model outputs in this article are illustrative calculations from the stated assumptions, not measurements.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>vllm</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
