<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Storage on vishctl</title><link>https://vishctl.dev/tags/storage/</link><description>Recent content in Storage on vishctl</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 01 Nov 2026 01:29:00 +0800</lastBuildDate><atom:link href="https://vishctl.dev/tags/storage/index.xml" rel="self" type="application/rss+xml"/><item><title>Faster vLLM Cold Starts on Kubernetes: Cache the Model, Not the Download</title><link>https://vishctl.dev/posts/vllm-cold-start-model-cache-kubernetes/</link><pubDate>Sun, 01 Nov 2026 01:29:00 +0800</pubDate><guid>https://vishctl.dev/posts/vllm-cold-start-model-cache-kubernetes/</guid><category>kubernetes</category><category>vllm</category><category>cold-start</category><category>storage</category><category>inference</category><category>ai-infra</category><description>A simple, diagram-first guide to making a new vLLM Pod ready in seconds instead of minutes: where cold-start time goes, how to cache weights on a volume, how to pre-pull the image, and which storage to pick.</description><content:encoded><![CDATA[<p>A new vLLM Pod has a lot to do before it answers its first request. By default it pulls a large image, downloads the model from the internet, and then loads those weights into the GPU.</p>
<p>On a fresh node that can take five minutes. During a traffic spike, five minutes is the whole incident.</p>
<p>Most of that time is avoidable, because most of it is spent fetching things that have not changed since the last Pod started.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Default start = pull the image + download the model + load it, every single time
</span></span><span class="line"><span class="cl">Cached start  = image and model are already on disk, so only the load remains
</span></span></code></pre></div><figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    subgraph SLOW["Default start"]
        direction LR
        A1["Pull image"] a1@--> A2["Download<br/>16 GB model"]
        A2 a2@--> A3["Load into<br/>GPU"]
        A3 a3@--> A4["Ready in<br/>about 5 min"]
    end

    subgraph FAST["Cached start"]
        direction LR
        B1["Image already<br/>on node"] b1@--> B2["Model already<br/>on disk"]
        B2 b2@--> B3["Load into<br/>GPU"]
        B3 b3@--> B4["Ready in<br/>about 30 s"]
    end

    SLOW ~~~ FAST
    a1@{ animate: true }
    a2@{ animate: true }
    a3@{ animate: true }
    b1@{ animate: true }
    b2@{ animate: true }
    b3@{ animate: true }

    classDef warn fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-width:2px;
    classDef idle fill:#101e27,stroke:#324950,color:#8aa0a8;
    classDef tree fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    class A1,A2,A4 warn;
    class B1,B2 idle;
    class A3,B3 tree;
    class B4 gpu;</div>
  <figcaption>Top: the default path downloads everything on each start. Bottom: with the image pre-pulled and the model on a volume, the Pod only has to load weights.</figcaption>
</figure>

<p>My <a href="https://vishctl.dev/posts/autoscaling-llm-inference-kubernetes-keda/">autoscaling guide</a> explained why slow cold starts make scaling hard. This post is about making them short.</p>
<h2 id="where-the-time-goes">Where the time goes</h2>
<p>A cold start has four phases. Each is limited by something different.</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart LR
    P1["Image pull<br/>registry speed"] c1@--> P2["Model download<br/>internet speed"]
    P2 c2@--> P3["Weight load<br/>disk speed"]
    P3 c3@--> P4["Engine start<br/>fixed cost"]
    P4 c4@--> P5["Ready"]

    c1@{ animate: true }
    c2@{ animate: true }
    c3@{ animate: true }
    c4@{ animate: true }

    classDef warn fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-width:2px;
    classDef tree fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef idle fill:#101e27,stroke:#324950,color:#8aa0a8;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:3px;
    class P1,P2 warn;
    class P3 tree;
    class P4 idle;
    class P5 gpu;</div>
  <figcaption>The four phases of a cold start and what limits each one. The first two can be removed completely. The third can be made much faster.</figcaption>
</figure>

<p>Every phase is &ldquo;size divided by speed&rdquo;. Take an 8B model in 16-bit (16 GB) and an 8 GB serving image as an example:</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Phase</th>
            <th scope="col">Size</th>
            <th scope="col">Speed (example)</th>
            <th scope="col">Time</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>1. Image pull</td>
            <td>8 GB</td>
            <td>100 MB/s</td>
            <td>80 s</td>
        </tr>
        <tr>
            <td>2. Model download</td>
            <td>16 GB</td>
            <td>100 MB/s</td>
            <td>160 s</td>
        </tr>
        <tr>
            <td>3. Weight load from local SSD</td>
            <td>16 GB</td>
            <td>500 MB/s</td>
            <td>32 s</td>
        </tr>
        <tr>
            <td>4. Engine start</td>
            <td>Fixed</td>
            <td>Fixed</td>
            <td>About 20 s</td>
        </tr>
        <tr>
            <td><strong>Total</strong></td>
            <td></td>
            <td></td>
            <td><strong>About 292 s</strong></td>
        </tr>
    </tbody>
  </table>
</div>
<p>These speeds are round numbers for illustration. Yours will differ, but the shape rarely does: <strong>the two downloads are most of the wait.</strong></p>
<h2 id="four-ways-to-start-compared">Four ways to start, compared</h2>
<p>Now remove the phases one at a time, using the same example numbers:</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Setup</th>
            <th scope="col">Image pull</th>
            <th scope="col">Download</th>
            <th scope="col">Load</th>
            <th scope="col">Engine</th>
            <th scope="col">Total</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>A. Default</td>
            <td>80 s</td>
            <td>160 s</td>
            <td>32 s</td>
            <td>20 s</td>
            <td>292 s</td>
        </tr>
        <tr>
            <td>B. Image pre-pulled</td>
            <td>0</td>
            <td>160 s</td>
            <td>32 s</td>
            <td>20 s</td>
            <td>212 s</td>
        </tr>
        <tr>
            <td>C. B plus model on a shared network volume</td>
            <td>0</td>
            <td>0</td>
            <td>80 s</td>
            <td>20 s</td>
            <td>100 s</td>
        </tr>
        <tr>
            <td>D. B plus model on node-local NVMe</td>
            <td>0</td>
            <td>0</td>
            <td>8 s</td>
            <td>20 s</td>
            <td>28 s</td>
        </tr>
    </tbody>
  </table>
</div>
<p>Two things stand out:</p>
<ul>
<li><strong>Caching the model is the biggest single win.</strong> It removes the largest phase outright.</li>
<li><strong>After that, disk speed is everything.</strong> Setup C loads from network storage at 200 MB/s. Setup D loads from NVMe at 2,000 MB/s. Same files, ten times faster.</li>
</ul>
<p>From A to D is a 10x improvement, with no change to the model or the GPU.</p>
<h2 id="step-1-put-the-model-on-a-volume">Step 1: put the model on a volume</h2>
<p>By default vLLM downloads from the Hugging Face Hub into a cache directory inside the container. When the Pod dies, the cache dies with it.</p>
<p>Point that cache at a persistent volume, and the download happens once.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># model-cache-pvc.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">PersistentVolumeClaim</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">model-cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">llm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">accessModes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">ReadWriteMany</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">requests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">storage</span><span class="p">:</span><span class="w"> </span><span class="l">100Gi</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f model-cache-pvc.yaml
</span></span></code></pre></div><p><code>ReadWriteMany</code> lets several replicas mount the same volume, so they all share one copy. It needs a storage class that supports it (NFS, a managed file store, or a distributed file system). With <code>ReadWriteOnce</code>, each replica needs its own claim and its own download.</p>
<h3 id="fill-the-cache-with-a-job">Fill the cache with a Job</h3>
<p>Download the model once, ahead of time, instead of making the first serving Pod do it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># model-download-job.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">batch/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">model-download</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">llm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">backoffLimit</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">restartPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">OnFailure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">download</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">python:3.12-slim</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="l">bash</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- -<span class="l">c</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="l">pip install -q huggingface_hub &amp;&amp; hf download &#34;$MODEL&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">MODEL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&lt;org&gt;/&lt;model&gt;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">HF_HOME</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">HF_TOKEN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hf-token</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">token</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">optional</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">persistentVolumeClaim</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">claimName</span><span class="p">:</span><span class="w"> </span><span class="l">model-cache</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f model-download-job.yaml
</span></span><span class="line"><span class="cl">kubectl -n llm <span class="nb">wait</span> --for<span class="o">=</span><span class="nv">condition</span><span class="o">=</span><span class="nb">complete</span> job/model-download --timeout<span class="o">=</span>30m
</span></span></code></pre></div><p><code>HF_HOME</code> tells the Hugging Face tools where the cache lives. The token is only needed for gated models. On older versions of <code>huggingface_hub</code>, the command is <code>huggingface-cli download</code> instead of <code>hf download</code>.</p>
<h3 id="mount-it-in-the-serving-pod">Mount it in the serving Pod</h3>
<p>Add three things to the vLLM container: the same <code>HF_HOME</code>, the volume, and a switch that forbids downloads.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># excerpt from the vLLM Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">vllm/vllm-openai:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;--model&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;&lt;org&gt;/&lt;model&gt;&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">HF_HOME</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">HF_HUB_OFFLINE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">readOnly</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">persistentVolumeClaim</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">claimName</span><span class="p">:</span><span class="w"> </span><span class="l">model-cache</span><span class="w">
</span></span></span></code></pre></div><p><code>HF_HUB_OFFLINE=1</code> is the important line. Without it, a Pod that cannot find the model quietly starts a 16 GB download. With it, the Pod fails immediately and tells you the cache is missing.</p>
<p><strong>A fast, loud failure beats a slow, silent one.</strong></p>
<p>Pin the image to a specific version tag in real deployments. <code>latest</code> is here only to keep the example short.</p>
<h2 id="step-2-pre-pull-the-image">Step 2: pre-pull the image</h2>
<p>The serving image is large because it carries CUDA libraries and PyTorch. A node that has never run it must pull all of it before the container can start.</p>
<p>A small DaemonSet fixes that by pulling the image on every GPU node as soon as the node joins:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vllm-image-prepull.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">DaemonSet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-image-prepull</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">llm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-image-prepull</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-image-prepull</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">nodeSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">nvidia.com/gpu.present</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">tolerations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">nvidia.com/gpu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">operator</span><span class="p">:</span><span class="w"> </span><span class="l">Exists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">effect</span><span class="p">:</span><span class="w"> </span><span class="l">NoSchedule</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">initContainers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">pull</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">vllm/vllm-openai:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;sh&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-c&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;exit 0&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">pause</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">registry.k8s.io/pause:3.10</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f vllm-image-prepull.yaml
</span></span></code></pre></div><p>The init container runs the vLLM image just long enough to exit, which forces the node to download it. The tiny <code>pause</code> container then keeps the Pod alive without using resources.</p>
<p>Use the same image tag here as in your Deployment. Otherwise you pre-pull an image nobody runs. The <code>nvidia.com/gpu.present</code> label is set by the GPU Operator&rsquo;s feature discovery; change the selector if your GPU nodes are labelled differently.</p>
<h2 id="step-3-pick-the-right-storage">Step 3: pick the right storage</h2>
<p>Once nothing is downloaded, the load phase is all that is left, and it runs at the speed of your disk.</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Storage</th>
            <th scope="col">Typical read speed</th>
            <th scope="col">16 GB load</th>
            <th scope="col">Shared by replicas</th>
            <th scope="col">Survives node loss</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Network file storage</td>
            <td>Around 200 MB/s</td>
            <td>About 80 s</td>
            <td>Yes</td>
            <td>Yes</td>
        </tr>
        <tr>
            <td>Network block storage</td>
            <td>Around 500 MB/s</td>
            <td>About 32 s</td>
            <td>No, one Pod each</td>
            <td>Yes</td>
        </tr>
        <tr>
            <td>Node-local NVMe</td>
            <td>2,000 MB/s or more</td>
            <td>About 8 s</td>
            <td>Only Pods on that node</td>
            <td>No</td>
        </tr>
    </tbody>
  </table>
</div>
<p>There is no single winner. It is a trade between speed and convenience:</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    Q1["Do GPU nodes have<br/>local NVMe?"] s1@-->|"Yes"| Q2["Do nodes come<br/>and go often?"]
    Q1 s2@-->|"No"| N["Shared network volume<br/>simple and good enough"]
    Q2 s3@-->|"No"| L["Local NVMe cache<br/>fastest starts"]
    Q2 s4@-->|"Yes"| H["Both: shared volume<br/>plus a copy to NVMe"]

    s1@{ animate: true }
    s2@{ animate: true }
    s3@{ animate: true }
    s4@{ animate: true }

    classDef question fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef input fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:2px;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    class Q1,Q2 question;
    class N input;
    class L,H gpu;</div>
  <figcaption>A short way to choose where the model cache lives. Start with whether the nodes have fast local disks.</figcaption>
</figure>

<p>The &ldquo;both&rdquo; option is common on autoscaled clusters. An init container copies the model from the shared volume to the node&rsquo;s NVMe the first time a node is used. Later Pods on that node start at full NVMe speed.</p>
<p>One free speedup is worth knowing. Linux keeps recently read files in memory. If a Pod restarts on the same node soon after, the weights may still be in the page cache, and the load takes seconds regardless of the disk.</p>
<h2 id="what-about-baking-the-model-into-the-image">What about baking the model into the image?</h2>
<p>It is tempting: one artifact, nothing to mount.</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col"></th>
            <th scope="col">Model in the image</th>
            <th scope="col">Model on a volume</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Moving parts</td>
            <td>Fewer</td>
            <td>A volume and a download Job</td>
        </tr>
        <tr>
            <td>Image size</td>
            <td>Huge (image plus weights)</td>
            <td>Normal</td>
        </tr>
        <tr>
            <td>Changing models</td>
            <td>Rebuild and push tens of GB</td>
            <td>Run the download Job again</td>
        </tr>
        <tr>
            <td>First start on a new node</td>
            <td>Pulls everything, slowly</td>
            <td>Loads from the volume</td>
        </tr>
        <tr>
            <td>Works without shared storage</td>
            <td>Yes</td>
            <td>No</td>
        </tr>
    </tbody>
  </table>
</div>
<p>For a small model in an air-gapped cluster, baking it in can be the simplest thing that works. For anything large or frequently updated, the volume wins.</p>
<h2 id="do-not-let-kubernetes-kill-a-slow-starter">Do not let Kubernetes kill a slow starter</h2>
<p>Even a fast start takes longer than a typical web app. Tell Kubernetes to wait, or it will restart the Pod mid-load and make things worse:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">          </span><span class="nt">startupProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/health</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">periodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">failureThreshold</span><span class="p">:</span><span class="w"> </span><span class="m">120</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">readinessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/health</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">periodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span></code></pre></div><p>That allows up to 10 minutes (120 checks, 5 seconds apart) before the Pod is considered failed. Once startup passes, the readiness probe takes over. A short <code>periodSeconds</code> matters here: it is how quickly a ready Pod starts receiving traffic.</p>
<h2 id="measure-your-own-cold-start">Measure your own cold start</h2>
<p>Do not trust the tables above. Time the real thing:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl -n llm delete pod -l <span class="nv">app</span><span class="o">=</span>vllm --wait<span class="o">=</span><span class="nb">false</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl -n llm get pods -l <span class="nv">app</span><span class="o">=</span>vllm -w
</span></span></code></pre></div><p>Watch how long the new Pod spends in each state:</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Pod status</th>
            <th scope="col">Phase it corresponds to</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><code>Pending</code></td>
            <td>Waiting for a node or a GPU</td>
        </tr>
        <tr>
            <td><code>ContainerCreating</code></td>
            <td>Image pull and volume mount</td>
        </tr>
        <tr>
            <td><code>Running</code>, not ready</td>
            <td>Model download (if any) and weight load</td>
        </tr>
        <tr>
            <td><code>Running</code>, ready</td>
            <td>Serving</td>
        </tr>
    </tbody>
  </table>
</div>
<p>For the exact split inside the container, read the startup log. vLLM prints how long loading the weights took:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl -n llm logs -l <span class="nv">app</span><span class="o">=</span>vllm <span class="p">|</span> grep -i -E <span class="s2">&#34;load|took|seconds&#34;</span>
</span></span></code></pre></div><p>Whichever phase is longest is the one to fix next.</p>
<h2 id="cheat-sheet">Cheat sheet</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Cold start        = time from Pod created to first request served
</span></span><span class="line"><span class="cl">Image pull        = downloading the serving image; removed by pre-pulling
</span></span><span class="line"><span class="cl">Model download    = fetching weights from the Hub; removed by a cached volume
</span></span><span class="line"><span class="cl">Weight load       = reading weights from disk into the GPU; limited by disk speed
</span></span><span class="line"><span class="cl">HF_HOME           = where Hugging Face tools keep their cache; point it at a volume
</span></span><span class="line"><span class="cl">HF_HUB_OFFLINE=1  = forbid downloads, so a missing cache fails fast instead of slowly
</span></span><span class="line"><span class="cl">Download Job      = fills the cache once, before any serving Pod needs it
</span></span><span class="line"><span class="cl">ReadWriteMany     = a volume several replicas can mount at once and share
</span></span><span class="line"><span class="cl">Pre-pull DaemonSet = pulls the image on every GPU node ahead of time
</span></span><span class="line"><span class="cl">Node-local NVMe   = the fastest place to load from, but lost with the node
</span></span><span class="line"><span class="cl">Page cache        = files recently read are still in memory, so restarts are quick
</span></span><span class="line"><span class="cl">startupProbe      = tells Kubernetes a Pod is allowed to take minutes to start
</span></span><span class="line"><span class="cl">Rule of thumb     = never download at start time, then load from the fastest disk you have
</span></span></code></pre></div><p>Fast starts make <a href="https://vishctl.dev/posts/autoscaling-llm-inference-kubernetes-keda/">autoscaling</a> practical: a replica that is ready in 30 seconds can react to a spike that a 5-minute replica would miss. For how big the weights are in the first place, see <a href="https://vishctl.dev/posts/llm-gpu-memory-sizing-weights-kv-cache/">How Much GPU Memory Does an LLM Need?</a> and <a href="https://vishctl.dev/posts/llm-quantization-fp16-int8-4bit-gguf/">LLM Quantization, Simply</a>.</p>
]]></content:encoded></item></channel></rss>