<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Vram on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/vram/</link><description>Recent content in Vram on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 11 Jun 2026 02:40:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/vram/index.xml" rel="self" type="application/rss+xml"/><item><title>Serving several models on a single GPU: co-residency, model-swapping and sleep mode</title><link>https://blog.lo0.es/en/posts/serving-several-models-single-gpu-co-residency-swapping-sleep-mode/</link><pubDate>Thu, 11 Jun 2026 02:40:00 +0000</pubDate><guid>https://blog.lo0.es/en/posts/serving-several-models-single-gpu-co-residency-swapping-sleep-mode/</guid><description>&lt;blockquote>
&lt;p>This is the second piece of an operational series on squeezing a generic on-premise LLM cluster of 4×H100 SXM 80 GB with NVLink. Its sibling, &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">Sharing a GPU: time-slicing, MPS and MIG&lt;/a>, splits the GPU so that several processes can use it at once; this one splits the complementary problem: you have &lt;strong>more models than VRAM&lt;/strong> and you need them to coexist in time, not just in space. The third, &lt;a href="https://blog.lo0.es/en/posts/rag-cpu-data-plane-generation-plane/">The RAG data plane on CPU&lt;/a>, moves off the GPU everything that does not need to be there; and the end-to-end sovereign assistant (fourth instalment, in preparation) assembles all of this behind LibreChat and LiteLLM. Here we assume that VRAM is the scarce resource and that keeping everything loaded at once is not an option.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>You have an agent LLM serving almost all the traffic, one or two rerankers for RAG, an alternate model (another family, another language, a fine-tune) and, now and then, a large model for hard tasks. Added up, they do not fit in the 80 GB of an H100. There are three ways to make them coexist, and the key to not confusing them is to understand that &lt;strong>they are three different states of where the weights live&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Co-residency&lt;/strong>: several models loaded at the same time in HBM, if the sum of their VRAM budgets fits in the physical memory. It is the ideal when they fit: zero switching latency. The catch: each engine must cap its own VRAM; if two engines both believe they own the whole 80 GB, the second one to ask for memory dies with OOM. On a GPU shared by time-slicing, sharing time does not protect memory (the processes still compete for the same HBM) and this problem gets worse, as we see in the &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">sibling piece&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Model-swapping&lt;/strong> (with llama-swap): only one (or very few) resident at a time. The proxy looks at the request&amp;rsquo;s &lt;code>model&lt;/code> field and, if another one is needed, unloads the current model and loads the requested one. Since there is only one inside, each model can use almost the whole GPU. The cost is a full cold start: seconds from local NVMe, minutes from network storage. It mixes engines: it exposes OpenAI-compatible endpoints including &lt;code>/rerank&lt;/code> with &lt;code>llama-server&lt;/code> on GGUF.&lt;/li>
&lt;li>&lt;strong>vLLM sleep mode&lt;/strong> (&lt;code>--enable-sleep-mode&lt;/code>, endpoints &lt;code>/sleep&lt;/code> and &lt;code>/wake_up&lt;/code>): the model is not unloaded, it is put to sleep. The weights are parked in host RAM (level 1) or discarded while the process stays alive (level 2). The wake is &lt;strong>18–200× faster&lt;/strong> than a cold start because the process is still alive and keeps the CUDA allocator, the CUDA graphs and the compiled JIT kernels. The only thing rebuilt is the KV cache, which is discarded on sleep. It needs enough RAM for the sleeping weights.&lt;/li>
&lt;/ul>
&lt;p>The decision tree is short. &lt;strong>Do they all fit?&lt;/strong> Co-residency. &lt;strong>They do not fit, switching is infrequent and you mix engines?&lt;/strong> llama-swap. &lt;strong>Everything is vLLM, there is RAM to spare and wake latency matters?&lt;/strong> Sleep mode. &lt;strong>Sustained load on all of them at once?&lt;/strong> That is not swapping, it is more GPUs or replicas (see &lt;a href="https://blog.lo0.es/en/posts/one-big-replica-or-many-small-ones-platform-decision/">one big vs N small&lt;/a>). This post puts numbers on each of them on an 80 GB H100.&lt;/p>
&lt;h2 id="the-analogy-one-stage-one-spotlight">The analogy: one stage, one spotlight&lt;/h2>
&lt;p>Picture a theatre with a single stage and a single spotlight, the GPU. You have more actors (models) in the company than fit under the light at once. There are three ways to stage the show:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Co-residency: several small actors on stage.&lt;/strong> If the cast for this scene is three slight actors, the main LLM, one reranker, another reranker, they all fit under the spotlight at the same time. Switching lines between them is instantaneous: they are already on stage. But the stage has a fixed size; add a fourth, burly actor (the large model) and they do not fit, so someone falls off the edge. That is the &lt;strong>OOM&lt;/strong>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Model-swapping: the actor goes home.&lt;/strong> When an actor finishes their part, they go home (the disk). If the next scene needs them again, they have to come back from home: get dressed, put the make-up on, travel to the theatre. That takes minutes. In exchange, while they are on stage they have the whole stage to themselves. That is the llama-swap model: maximum VRAM per actor, but re-entry costs a full round trip.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Sleep mode: the actor waits in the wings.&lt;/strong> Instead of going home, the actor steps out of the spotlight but stays backstage (host RAM), already dressed and made up. When their turn comes, they step in in two strides. No journey, no wardrobe: just crossing the curtain. That is the vLLM wake: the weights come back not from disk but from RAM, and everything that goes into &amp;ldquo;preparing the actor&amp;rdquo;, the allocator, the CUDA graphs, the kernels, is already done because they never left the building.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>The operational moral is the same as in the theatre: &lt;strong>the cost of an actor is not only their talent, it is how long they take to walk on when you need them.&lt;/strong> Co-residency pays for the permanent spot; swapping pays for the journey; sleep pays for the RAM of keeping them waiting nearby. The rest of this post is about choosing which, with a VRAM budget and a stopwatch in hand.&lt;/p>
&lt;h2 id="the-three-states-where-the-weights-live">The three states: where the weights live&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Three states for serving several models: co-residency, swap per request and sleep-wake">
&lt;text x="410" y="24" text-anchor="middle" font-size="15" font-weight="700" fill="currentColor">Where the weights live in each strategy&lt;/text>
&lt;!-- Co-residency -->
&lt;text x="140" y="56" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">Co-residency&lt;/text>
&lt;rect x="30" y="68" width="220" height="120" fill="none" stroke="#3b82f6" stroke-width="1.6"/>
&lt;text x="140" y="86" text-anchor="middle" font-size="11" font-weight="700" fill="#3b82f6">HBM 80 GB&lt;/text>
&lt;rect x="44" y="96" width="192" height="26" fill="#3b82f6" fill-opacity="0.18" stroke="#3b82f6" stroke-width="1"/>
&lt;text x="140" y="113" text-anchor="middle" font-size="10.5" fill="currentColor">agent LLM&lt;/text>
&lt;rect x="44" y="126" width="92" height="22" fill="#22c55e" fill-opacity="0.18" stroke="#22c55e" stroke-width="1"/>
&lt;text x="90" y="141" text-anchor="middle" font-size="10" fill="currentColor">reranker A&lt;/text>
&lt;rect x="144" y="126" width="92" height="22" fill="#22c55e" fill-opacity="0.18" stroke="#22c55e" stroke-width="1"/>
&lt;text x="190" y="141" text-anchor="middle" font-size="10" fill="currentColor">reranker B&lt;/text>
&lt;rect x="44" y="152" width="192" height="28" fill="none" stroke="#888" stroke-width="1" stroke-dasharray="3 3"/>
&lt;text x="140" y="170" text-anchor="middle" font-size="10" fill="currentColor" opacity="0.75">KV + activations headroom&lt;/text>
&lt;text x="140" y="206" text-anchor="middle" font-size="10.5" fill="currentColor">switch: 0 ms · sum &amp;lt; 80 GB or OOM&lt;/text>
&lt;!-- Swap -->
&lt;text x="410" y="56" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">Model-swapping (llama-swap)&lt;/text>
&lt;rect x="300" y="68" width="220" height="120" fill="none" stroke="#3b82f6" stroke-width="1.6"/>
&lt;text x="410" y="86" text-anchor="middle" font-size="11" font-weight="700" fill="#3b82f6">HBM 80 GB&lt;/text>
&lt;rect x="314" y="96" width="192" height="84" fill="#f59e0b" fill-opacity="0.18" stroke="#f59e0b" stroke-width="1.2"/>
&lt;text x="410" y="135" text-anchor="middle" font-size="11" fill="currentColor">one resident model&lt;/text>
&lt;text x="410" y="151" text-anchor="middle" font-size="10" fill="currentColor" opacity="0.8">uses almost all the VRAM&lt;/text>
&lt;text x="410" y="206" text-anchor="middle" font-size="10.5" fill="currentColor">the rest on DISK (NVMe / network)&lt;/text>
&lt;rect x="330" y="222" width="160" height="22" fill="#ef4444" fill-opacity="0.12" stroke="#ef4444" stroke-width="1"/>
&lt;text x="410" y="237" text-anchor="middle" font-size="10" fill="#ef4444">switch: unload + load (s–min)&lt;/text>
&lt;!-- Sleep -->
&lt;text x="680" y="56" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">Sleep mode (vLLM)&lt;/text>
&lt;rect x="570" y="68" width="220" height="120" fill="none" stroke="#3b82f6" stroke-width="1.6"/>
&lt;text x="680" y="86" text-anchor="middle" font-size="11" font-weight="700" fill="#3b82f6">HBM 80 GB&lt;/text>
&lt;rect x="584" y="96" width="192" height="60" fill="#f59e0b" fill-opacity="0.18" stroke="#f59e0b" stroke-width="1.2"/>
&lt;text x="680" y="124" text-anchor="middle" font-size="11" fill="currentColor">awake model&lt;/text>
&lt;text x="680" y="140" text-anchor="middle" font-size="10" fill="currentColor" opacity="0.8">process alive&lt;/text>
&lt;rect x="584" y="160" width="192" height="20" fill="none" stroke="#888" stroke-width="1" stroke-dasharray="3 3"/>
&lt;text x="680" y="174" text-anchor="middle" font-size="9.5" fill="currentColor" opacity="0.7">KV cache (discarded on sleep)&lt;/text>
&lt;text x="680" y="206" text-anchor="middle" font-size="10.5" fill="currentColor">the rest asleep in host RAM&lt;/text>
&lt;rect x="600" y="222" width="160" height="22" fill="#22c55e" fill-opacity="0.14" stroke="#22c55e" stroke-width="1"/>
&lt;text x="680" y="237" text-anchor="middle" font-size="10" fill="#22c55e">wake: RAM→VRAM (hundreds of ms–s)&lt;/text>
&lt;text x="410" y="290" text-anchor="middle" font-size="12.5" font-weight="700" fill="currentColor">The axis is where the weights of the idle models sit&lt;/text>
&lt;text x="410" y="312" text-anchor="middle" font-size="11" fill="currentColor">co-residency: in HBM (cost space) · swap: on disk (cost a trip) · sleep: in RAM (cost RAM)&lt;/text>
&lt;text x="410" y="332" text-anchor="middle" font-size="11" fill="currentColor" opacity="0.8">and that decides the latency of having them available again&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="co-residency-the-vram-budget">Co-residency: the VRAM budget&lt;/h2>
&lt;p>Co-residing is the default option when they fit: zero switching latency because every model is already in HBM. The whole question is do they fit?, and it is answered with a VRAM budget. The VRAM of an inference engine splits, roughly, into three items:&lt;/p>
$$\text{VRAM}_{\text{model}} = \underbrace{P \cdot b}_{\text{weights}} + \underbrace{\text{KV}}_{\text{cache}} + \underbrace{A}_{\text{activations + overhead}}$$
&lt;p>where $P$ is the number of parameters, $b$ the bytes per parameter given the quantisation, KV the KV-cache budget (which scales with concurrency and context length) and $A$ the overhead of activations, CUDA buffers and fragmentation. The hard rule of co-residency is:&lt;/p>
$$\sum_i \text{VRAM}_{\text{model } i} + \text{headroom} \;&lt;\; \text{VRAM}_{\text{physical}}$$
&lt;p>If the sum overflows, no sharing scheme saves you: the first engine to ask for memory beyond the free gap dies with OOM. And here is the critical nuance that connects to the sibling piece: &lt;strong>splitting the GPU by time does not split the memory&lt;/strong>. Under time-slicing, two processes take turns on compute but share the whole HBM with no isolation; if both assume they own the 80 GB, they collide. Only MIG gives memory partitions with a real boundary, as we see in &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">Sharing a GPU&lt;/a>. To co-reside properly, each engine must cap itself: in vLLM, &lt;code>--gpu-memory-utilization 0.45&lt;/code> tells it &amp;ldquo;do not use more than 45 % of the GPU&amp;rdquo;; in &lt;code>llama-server&lt;/code>, you control the layers on GPU and the KV size. If you do not set those limits, vLLM claims 90 % of the GPU for itself by default, and there is no room left for anyone else.&lt;/p>
&lt;h3 id="a-worked-example-what-fits-in-80-gb">A worked example: what fits in 80 GB&lt;/h3>
&lt;p>Take the realistic scenario. A 32B agent LLM in FP8 ($b = 1$ byte/param):&lt;/p>
$$P \cdot b = 32 \times 10^9 \cdot 1 \text{ B} = 32 \text{ GB of weights}$$
&lt;p>On top of that comes the KV cache. To serve with decent concurrency and agent contexts (which are long: history, tools, documents), a KV budget of ~12 GB is reasonable, plus some ~2 GB of activations and overhead. Total for the main LLM: ~46 GB. That leaves ~34 GB of the 80. Let us see what fits there:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Service&lt;/th>
&lt;th>Size (FP8/INT8)&lt;/th>
&lt;th>KV + overhead&lt;/th>
&lt;th>Total VRAM&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>32B agent LLM&lt;/td>
&lt;td>32 GB&lt;/td>
&lt;td>~14 GB&lt;/td>
&lt;td>&lt;strong>~46 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reranker A (cross-encoder ~0.5B)&lt;/td>
&lt;td>~0.5 GB&lt;/td>
&lt;td>~0.5 GB&lt;/td>
&lt;td>&lt;strong>~1 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reranker B (cross-encoder ~0.5B)&lt;/td>
&lt;td>~0.5 GB&lt;/td>
&lt;td>~0.5 GB&lt;/td>
&lt;td>&lt;strong>~1 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8B alternate model&lt;/td>
&lt;td>~8 GB&lt;/td>
&lt;td>~4 GB&lt;/td>
&lt;td>&lt;strong>~12 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Sum&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;strong>~60 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Free headroom&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;strong>~20 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>With this they co-reside comfortably: the 32B LLM, the two rerankers (which are featherweights, a few hundred MB to 1 GB each) and even an 8B alternate, leaving 20 GB of cushion. Rerankers are the textbook case for co-residency: so small that they always fit alongside the main LLM, and rotating them would be absurd. Switching between them is instantaneous because they are all loaded.&lt;/p>
&lt;p>When does it break? When the occasional large model arrives, a 70B in FP8:&lt;/p>
$$70 \times 10^9 \cdot 1 \text{ B} = 70 \text{ GB of weights}$$
&lt;p>The 70B&amp;rsquo;s weights alone already eat 70 of the 80 GB. There is no way to co-reside it with the 32B, not remotely. That is where co-residency ends and rotation begins: the 70B can only come in if we evict the 32B from the GPU. That is the exact boundary: co-reside while the sum of budgets fits with headroom; rotate as soon as one model needs, on its own, more VRAM than is left over.&lt;/p>
&lt;h2 id="model-swapping-with-llama-swap-the-actor-goes-home">Model-swapping with llama-swap: the actor goes home&lt;/h2>
&lt;p>When they do not fit at once, the first answer is to rotate: keep one resident model and change it on demand. The canonical tool for this in the on-premise world is &lt;a href="https://github.com/mostlygeek/llama-swap">llama-swap&lt;/a>, a Go proxy that sits in front of your inference servers (llama.cpp, vLLM, TabbyAPI and so on) and starts and stops them as needed.&lt;/p>
&lt;p>The mechanism is elegant in its simplicity. Every OpenAI-compatible request carries a &lt;code>model&lt;/code> field. llama-swap reads that field, looks at which upstream server it has configured for that model, and:&lt;/p>
&lt;ol>
&lt;li>If the requested model is already loaded, it routes the request straight through.&lt;/li>
&lt;li>If another one is loaded, it stops it (freeing its VRAM) and starts the right one.&lt;/li>
&lt;li>When the new server reports ready, it forwards the request.&lt;/li>
&lt;/ol>
&lt;p>The cost of a swap is exactly a full cold start of the new model: unloading the current one (fast, just freeing memory) plus loading the new one (slow, moving the weights from disk to HBM). That second term is the painful one, and its magnitude is decided by where the weights are: from local NVMe it is seconds; from network storage (Ceph RGW, NFS) it can be minutes. The whole analysis of the load path, and why the default loader makes it slow, is in &lt;a href="https://blog.lo0.es/en/posts/pantry-lift-from-disk-to-hbm-why-the-kitchen-opens-late/">From disk to HBM&lt;/a>; here the consequence is enough: &lt;strong>a swap is only viable if it is infrequent&lt;/strong>, because every switch pays that toll in full.&lt;/p>
&lt;p>The advantage in exchange: since there is only one resident, that model can use almost the whole GPU. The 70B that co-resides with nobody fits comfortably if it is the only one inside. And llama-swap mixes engines: you can have a vLLM configured for the big LLM, a &lt;code>llama-server&lt;/code> with GGUF for the alternate, and two &lt;code>llama-server&lt;/code> instances for the rerankers, which expose &lt;code>/rerank&lt;/code>, &lt;code>/v1/rerank&lt;/code> and &lt;code>/v1/reranking&lt;/code> natively. That heterogeneity (GGUF + rerank + vLLM behind a single OpenAI endpoint) is exactly what vLLM alone does not give you, and the main reason to choose llama-swap.&lt;/p>
&lt;h3 id="example-configmap-two-rerankers-rotating-on-the-same-port">Example ConfigMap: two rerankers rotating on the same port&lt;/h3>
&lt;p>A concrete and useful case: you have two rerankers, one multilingual and one specialised in code, that you do not need at the same time and would rather not keep both resident. With llama-swap they rotate on the same endpoint, triggered by the &lt;code>model&lt;/code> field. The &lt;code>ConfigMap&lt;/code> (mounted as the proxy&amp;rsquo;s &lt;code>config.yaml&lt;/code> in a Kubernetes deployment) would be:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama-swap-rerankers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config.yaml&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # How long a model stays loaded after the last request
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # before llama-swap unloads it to free VRAM
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> healthCheckTimeout: 60
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> models:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # Multilingual reranker (GGUF, via llama-server)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;reranker-multilang&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cmd: &amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> /usr/bin/llama-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --model /models/bge-reranker-v2-m3.Q8_0.gguf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --reranking
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --port ${PORT}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --n-gpu-layers 99
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --ctx-size 8192
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # ttl: after 300 s idle, it is unloaded and the GPU is freed
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ttl: 300
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # Code reranker (GGUF, via llama-server)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;reranker-code&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cmd: &amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> /usr/bin/llama-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --model /models/codereranker.Q8_0.gguf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --reranking
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --port ${PORT}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --n-gpu-layers 99
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --ctx-size 8192
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ttl: 300&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A request to &lt;code>/v1/rerank&lt;/code> with &lt;code>&amp;quot;model&amp;quot;: &amp;quot;reranker-multilang&amp;quot;&lt;/code> starts that server; the next one with &lt;code>&amp;quot;model&amp;quot;: &amp;quot;reranker-code&amp;quot;&lt;/code> stops the multilingual one and starts the code one on the same &lt;code>${PORT}&lt;/code> that llama-swap manages. Since both are small, the swap between them takes one or two seconds: quantised GGUFs weigh a few hundred MB and come from local NVMe. The &lt;code>ttl&lt;/code> controls how long a model stays loaded after its last request: raising it avoids swaps if requests arrive in bursts; lowering it frees the GPU sooner for other uses.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>An honest caveat:&lt;/strong> this pattern of &lt;em>two rerankers that rotate&lt;/em> makes sense when they do not fit alongside the rest, or when you want to reserve the VRAM for something else. If they do fit (and a couple of 0.5 GB rerankers almost always fit, as we saw above), co-residing them is strictly better: zero swap latency. llama-swap shines when you rotate large models or mix engines that do not coexist well, not for juggling tiny models that would fit together.&lt;/p>
&lt;/blockquote>
&lt;h2 id="vllm-sleep-mode-the-actor-in-the-wings">vLLM sleep mode: the actor in the wings&lt;/h2>
&lt;p>Swapping has a problem: every switch pays the entire cold start, and a cold start is not just moving weights. As we saw in &lt;a href="https://blog.lo0.es/en/posts/pantry-lift-from-disk-to-hbm-why-the-kitchen-opens-late/">From disk to HBM&lt;/a>, starting an inference engine includes initialising the Python process and the CUDA context, setting up the memory allocator, capturing the CUDA graphs and compiling the JIT kernels (DeepGEMM, FlashInfer, TorchInductor). Moving the weights is only one of five items, and often not the largest.&lt;/p>
&lt;p>vLLM&amp;rsquo;s sleep mode (&lt;code>--enable-sleep-mode&lt;/code>) attacks exactly that: instead of killing the process when a model stops being used, it puts it to sleep, leaving the process alive. There are two levels, and the difference is where the weights go:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Level 1:&lt;/strong> offloads the weights to host RAM (CPU) and discards the KV cache. The process stays alive. The wake copies the weights from RAM to VRAM, not from disk. Typical wake: ~0.1–0.8 s for small models, ~3–6 s for large ones. It needs enough RAM for the sleeping weights (on the order of the model&amp;rsquo;s size in GB).&lt;/li>
&lt;li>&lt;strong>Level 2:&lt;/strong> discards the weights entirely, keeping only small buffers (rope scaling tensors and the like). The wake does reload the weights from disk, but &lt;strong>everything else, process, allocator, CUDA graphs, JIT kernels, is already done&lt;/strong>. Typical wake: ~0.8–2.6 s for small models. RAM use is almost nil (megabytes).&lt;/li>
&lt;/ul>
&lt;p>The key that explains the numbers: at both levels, keeping the process alive preserves the expensive infrastructure. That is why the benchmark in the &lt;a href="https://blog.vllm.ai/2025/10/26/sleep-mode.html">vLLM blog (Oct 2025)&lt;/a> reports that a wake is 18–200× faster than a full reload, and, most counter-intuitively, &lt;strong>level 2 is still 23–45× faster&lt;/strong> than a cold start despite reloading the weights from the same disk, because it skips the other four items. In their measurements on an A100, a full cycle of 5 model switches goes from 357 s without sleep (≈48 s per switch) to 112 s with level 1 (wake of 0.26 s / 0.82 s) or 125 s with level 2 (0.85 s / 2.58 s).&lt;/p>
&lt;p>The &lt;strong>KV cache is always discarded on sleep.&lt;/strong> That is not a minor detail: it means the first response after the wake rebuilds the KV from scratch, paying a full prefill. That is why the wake is not &amp;ldquo;entirely free&amp;rdquo;: the model is available in sub-second time, but the first request is a little slower until the KV is repopulated. A one-request warm-up after the wake hides that cost.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Start vLLM with sleep mode (admin endpoints, trusted networks only)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">VLLM_SERVER_DEV_MODE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve &amp;lt;model&amp;gt; --enable-sleep-mode --port &lt;span class="m">8001&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Sleep (level 1: weights to host RAM)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -X POST &lt;span class="s1">&amp;#39;localhost:8001/sleep?level=1&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Wake up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -X POST &lt;span class="s1">&amp;#39;localhost:8001/wake_up&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>Security warning&lt;/strong> (from vLLM&amp;rsquo;s own blog): the &lt;code>/sleep&lt;/code>, &lt;code>/wake_up&lt;/code>, &lt;code>/collective_rpc&lt;/code> and &lt;code>/reset_prefix_cache&lt;/code> endpoints require &lt;code>VLLM_SERVER_DEV_MODE=1&lt;/code> and should only be exposed on trusted networks, since they can take the service down. They are for internal orchestration (a controller that sleeps and wakes models according to the queue), not for the public plane.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-latency-maths-why-the-wake-wins">The latency maths: why the wake wins&lt;/h2>
&lt;p>Let us put numbers on &amp;ldquo;the wake comes from RAM, not from disk&amp;rdquo;. The cost of having a model available is, in each strategy, the time to move its weights from wherever they are to HBM, plus, in the swap case, the other four cold-start costs. Take the 34 GB of weights of the 32B LLM in FP8 and compare the three paths.&lt;/p>
&lt;p>&lt;strong>Cold start from NVMe (swap).&lt;/strong> A reasonable Gen4/Gen5 NVMe gives on the order of ~5 GB/s effective per stream with the default loader (the disk&amp;rsquo;s theoretical floor is higher, but single-threaded deserialisation does not saturate it, see &lt;a href="https://blog.lo0.es/en/posts/pantry-lift-from-disk-to-hbm-why-the-kitchen-opens-late/">From disk to HBM&lt;/a>). For 34 GB:&lt;/p>
$$t_{\text{NVMe→HBM}} \approx \frac{34 \text{ GB}}{5 \text{ GB/s}} \approx 6.8 \text{ s just moving bytes}$$
&lt;p>And that is before adding CUDA graph capture and kernel compilation, which add several more seconds. The real cold start of a 32B is around 15–40 s depending on loader and storage. From the network (Ceph RGW), multiply.&lt;/p>
&lt;p>&lt;strong>Level 1 wake from RAM.&lt;/strong> The weights come not from disk but from host RAM, and travel over PCIe Gen5 x16, whose practical host→GPU bandwidth is ~50–64 GB/s. For the same 34 GB:&lt;/p>
$$t_{\text{RAM→HBM}} \approx \frac{34 \text{ GB}}{55 \text{ GB/s}} \approx 0.62 \text{ s}$$
&lt;p>And there is nothing else to pay: the allocator, the graphs and the kernels are already there. The real wake of a model this size lands in the sub-second to a few seconds range that vLLM reports. The speed-up against an NVMe cold start is on the order of:&lt;/p>
$$\frac{t_{\text{cold start}}}{t_{\text{wake}}} \approx \frac{15\text{–}40 \text{ s}}{0.6\text{–}3 \text{ s}} \approx 10\text{–}60\times$$
&lt;p>consistent with the 18–200× from the blog, whose measurements include smaller models, where the relative weight of the preserved CUDA graphs is even larger and the factor goes up.&lt;/p>
&lt;p>&lt;strong>Why the bandwidth difference explains almost everything.&lt;/strong> The key jump is not 5 vs 55 GB/s (a ~11× in transport). It is that the cold start also pays for rebuilding infrastructure, which the wake skips entirely. The table:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Path&lt;/th>
&lt;th>Weight source&lt;/th>
&lt;th>Bandwidth&lt;/th>
&lt;th>34 GB bytes only&lt;/th>
&lt;th>+ CUDA graphs / JIT&lt;/th>
&lt;th>Realistic total&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cold start (swap) NVMe&lt;/td>
&lt;td>disk&lt;/td>
&lt;td>~5 GB/s&lt;/td>
&lt;td>~6.8 s&lt;/td>
&lt;td>yes (several s)&lt;/td>
&lt;td>&lt;strong>15–40 s&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cold start (swap) network&lt;/td>
&lt;td>network&lt;/td>
&lt;td>~1–2 GB/s&lt;/td>
&lt;td>17–34 s&lt;/td>
&lt;td>yes&lt;/td>
&lt;td>&lt;strong>30 s – min&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Level 1 wake&lt;/td>
&lt;td>host RAM&lt;/td>
&lt;td>~50–64 GB/s&lt;/td>
&lt;td>~0.6 s&lt;/td>
&lt;td>&lt;strong>no&lt;/strong> (preserved)&lt;/td>
&lt;td>&lt;strong>0.6–3 s&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Level 2 wake&lt;/td>
&lt;td>disk&lt;/td>
&lt;td>~5 GB/s&lt;/td>
&lt;td>~6.8 s&lt;/td>
&lt;td>&lt;strong>no&lt;/strong> (preserved)&lt;/td>
&lt;td>&lt;strong>7–10 s&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Look at the level 2 row: it reloads the weights from the same disk as the swap (~6.8 s of bytes), but since it does not rebuild graphs or kernels, its total (~7–10 s) still beats a full cold start (15–40 s). It is the proof that &lt;strong>moving bytes is only part of the cost&lt;/strong>, and the part sleep mode exploits.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Assumptions, honestly:&lt;/strong> the bandwidths are indicative. NVMe &amp;ldquo;5 GB/s effective&amp;rdquo; assumes the default loader; with a concurrent streamer it goes up. PCIe &amp;ldquo;55 GB/s&amp;rdquo; assumes Gen5 x16 with a &lt;em>pinned&lt;/em> buffer and NUMA locality; if the buffer lands on the wrong socket, it drops. And the &amp;ldquo;15–40 s&amp;rdquo; cold-start range depends on the model, the quantisation and whether the files are in page cache (the &lt;em>&amp;ldquo;the second time it loaded fast&amp;rdquo;&lt;/em> trap). The numbers are there to reason about orders of magnitude, not to size anything without measuring on your hardware.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-decision-tree">The decision tree&lt;/h2>
&lt;p>The three strategies do not compete: each wins in a different regime. The tree, in order:&lt;/p>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 420" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Decision tree: co-residency, llama-swap, sleep mode or more GPUs">
&lt;defs>&lt;marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect x="270" y="20" width="220" height="44" fill="currentColor" fill-opacity="0.05" stroke="#888" stroke-width="1.4"/>
&lt;text x="380" y="40" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">Do they ALL fit at once&lt;/text>
&lt;text x="380" y="56" text-anchor="middle" font-size="11" fill="currentColor">(sum of budgets &amp;lt; VRAM)?&lt;/text>
&lt;path d="M380,64 L380,90" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
&lt;text x="250" y="84" text-anchor="middle" font-size="11" font-weight="700" fill="#22c55e">yes&lt;/text>
&lt;rect x="40" y="92" width="200" height="50" fill="#22c55e" fill-opacity="0.14" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="140" y="113" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">Co-residency&lt;/text>
&lt;text x="140" y="130" text-anchor="middle" font-size="10" fill="currentColor">each engine caps its own VRAM&lt;/text>
&lt;path d="M320,86 L240,110" stroke="#22c55e" stroke-width="1.4" marker-end="url(#ar)"/>
&lt;text x="430" y="84" text-anchor="middle" font-size="11" font-weight="700" fill="#ef4444">no&lt;/text>
&lt;rect x="270" y="92" width="220" height="44" fill="currentColor" fill-opacity="0.05" stroke="#888" stroke-width="1.4"/>
&lt;text x="380" y="111" text-anchor="middle" font-size="11.5" font-weight="700" fill="currentColor">Sustained load on&lt;/text>
&lt;text x="380" y="127" text-anchor="middle" font-size="11" fill="currentColor">all of them at once?&lt;/text>
&lt;path d="M490,114 L560,114" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
&lt;text x="525" y="106" text-anchor="middle" font-size="11" font-weight="700" fill="#ef4444">yes&lt;/text>
&lt;rect x="565" y="92" width="180" height="50" fill="#ef4444" fill-opacity="0.12" stroke="#ef4444" stroke-width="1.6"/>
&lt;text x="655" y="113" text-anchor="middle" font-size="11.5" font-weight="700" fill="currentColor">More GPUs / replicas&lt;/text>
&lt;text x="655" y="130" text-anchor="middle" font-size="10" fill="currentColor">not swapping: capacity&lt;/text>
&lt;path d="M380,136 L380,170" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
&lt;text x="408" y="156" text-anchor="middle" font-size="11" font-weight="700" fill="#22c55e">no&lt;/text>
&lt;rect x="270" y="172" width="220" height="44" fill="currentColor" fill-opacity="0.05" stroke="#888" stroke-width="1.4"/>
&lt;text x="380" y="191" text-anchor="middle" font-size="11.5" font-weight="700" fill="currentColor">All vLLM and is there&lt;/text>
&lt;text x="380" y="207" text-anchor="middle" font-size="11" fill="currentColor">RAM to spare?&lt;/text>
&lt;path d="M270,194 L120,194 L120,250" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
&lt;text x="180" y="186" text-anchor="middle" font-size="11" font-weight="700" fill="#ef4444">no (mixed engines)&lt;/text>
&lt;rect x="30" y="252" width="200" height="64" fill="#f59e0b" fill-opacity="0.14" stroke="#f59e0b" stroke-width="1.6"/>
&lt;text x="130" y="275" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">llama-swap&lt;/text>
&lt;text x="130" y="292" text-anchor="middle" font-size="10" fill="currentColor">rotates by `model` field&lt;/text>
&lt;text x="130" y="307" text-anchor="middle" font-size="10" fill="currentColor">GGUF + /rerank + vLLM&lt;/text>
&lt;path d="M490,194 L640,194 L640,250" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
&lt;text x="575" y="186" text-anchor="middle" font-size="11" font-weight="700" fill="#3b82f6">yes (latency matters)&lt;/text>
&lt;rect x="540" y="252" width="200" height="64" fill="#3b82f6" fill-opacity="0.14" stroke="#3b82f6" stroke-width="1.6"/>
&lt;text x="640" y="275" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">Sleep mode&lt;/text>
&lt;text x="640" y="292" text-anchor="middle" font-size="10" fill="currentColor">level 1: weights to RAM&lt;/text>
&lt;text x="640" y="307" text-anchor="middle" font-size="10" fill="currentColor">wake 18–200× vs cold&lt;/text>
&lt;text x="380" y="356" text-anchor="middle" font-size="11.5" font-weight="700" fill="currentColor">And they combine: co-reside the light ones + rotate/sleep the big ones&lt;/text>
&lt;text x="380" y="378" text-anchor="middle" font-size="10.5" fill="currentColor" opacity="0.85">rerankers always co-reside · the occasional 70B sleeps or comes in by swap&lt;/text>
&lt;text x="380" y="398" text-anchor="middle" font-size="10.5" fill="currentColor" opacity="0.85">if everything runs flat out at once, no strategy saves you: you need more silicon&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The node most often ignored is the one at the top right: &amp;ldquo;sustained load on all of them at once?&amp;rdquo;. If your four models get constant and simultaneous traffic, neither swapping nor sleep helps, since both assume the models take turns in time. Rotating under sustained load only adds switching latency without solving the underlying problem: &lt;strong>there is not enough compute&lt;/strong>. The answer then is to scale horizontally (more replicas) or spread across more GPUs, a capacity decision analysed in &lt;a href="https://blog.lo0.es/en/posts/one-big-replica-or-many-small-ones-platform-decision/">One big vs N small&lt;/a>. Swapping and sleep are tools for temporally unbalanced workloads: many models, but rarely active at the same time.&lt;/p>
&lt;h2 id="applied-to-the-generic-4h100-cluster">Applied to the generic 4×H100 cluster&lt;/h2>
&lt;p>Let us bring this down to the 4 H100 SXM 80 GB with NVLink. The winning strategy is not to pick one of the three, but to &lt;strong>spread the models across GPUs according to their usage pattern&lt;/strong> and apply to each GPU the strategy it calls for. A reasonable split:&lt;/p>
&lt;p>&lt;strong>H100 #0, the workhorse (co-residency).&lt;/strong> The 32B agent LLM (main service, constant traffic) co-resides with the two rerankers and, if they fit, the 8B alternate. It is the GPU that never rotates: everything living here is used continuously and fits comfortably in 80 GB (the ~60 GB of the example above). Zero switching latency between the LLM and its rerankers, which is exactly what RAG needs, since a fast reranker is useless if you have to wait for a swap every time.&lt;/p>
&lt;p>&lt;strong>H100 #1, light services with MIG.&lt;/strong> If you have many small heterogeneous services, an embedding model, a classifier, a guardrail, an STT/TTS, splitting this GPU with MIG into isolated instances (each with its slice of HBM with a real boundary) gives co-residency with memory isolation, preventing a service that inflates its KV from taking the others down. The detail of when MIG beats time-slicing is in &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">Sharing a GPU&lt;/a>; the rule here: co-residing light services on one GPU makes sense when they fit and when isolating them is worthwhile, and MIG is the tool for the second part.&lt;/p>
&lt;p>&lt;strong>H100 #2, the occasional large model (sleep mode or swap).&lt;/strong> The 70B that is only invoked for hard tasks does not deserve a dedicated GPU kept awake, since it would sit idle most of the time, burning 700 W for nothing. Two options:&lt;/p>
&lt;ul>
&lt;li>If this GPU also serves a medium model routinely and you only occasionally need the 70B, use sleep mode: put the medium one to sleep (level 1, weights to RAM), wake the 70B. Note that level 1 sleep frees the VRAM, the weights go to RAM, so yes, it fits even though the 70B would not fit alongside an awake medium model. Waking the medium one again is sub-second.&lt;/li>
&lt;li>If the 70B comes as GGUF or you mix engines, llama-swap rotates between the medium model and the 70B by the &lt;code>model&lt;/code> field. Each 70B invocation pays its cold start (seconds from local NVMe), acceptable if it is occasional.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>H100 #3, replica / overflow.&lt;/strong> The fourth GPU absorbs peaks: a replica of the main LLM for when #0&amp;rsquo;s queue grows, or reserve capacity. There is no swapping here: it is pure capacity, the answer to the tree&amp;rsquo;s &amp;ldquo;sustained load&amp;rdquo; node.&lt;/p>
&lt;p>The cross-cutting principle: &lt;strong>co-reside what is used together and continuously (LLM + rerankers); isolate the light and heterogeneous with MIG; sleep or rotate the large and occasional; replicate what saturates.&lt;/strong> The four GPUs do not all do the same thing, each runs the strategy its load pattern calls for. And the NVLink between them matters for something else (tensor parallelism for the 70B if it would not fit even on one; see &lt;a href="https://blog.lo0.es/en/posts/one-big-replica-or-many-small-ones-platform-decision/">One big vs N small&lt;/a>), but for this post&amp;rsquo;s problem, many models, one GPU, the lever is &lt;strong>when each model needs to be awake&lt;/strong>.&lt;/p>
&lt;h2 id="traps-and-things-that-are-not-what-they-seem">Traps and things that are not what they seem&lt;/h2>
&lt;p>&lt;strong>&amp;ldquo;Co-residing is always better if they fit.&amp;rdquo;&lt;/strong> Almost, but watch the KV cache: co-residing two models means splitting the KV budget between them. If the main LLM needs a large KV for high concurrency and long contexts, adding a flatmate cuts that KV and lowers its throughput. Sometimes it is better to give the whole GPU to the main model and rotate the secondary one. Co-residency is not free: the tenant takes room away from the cache of the one that matters.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Sleep mode is like swapping but faster.&amp;rdquo;&lt;/strong> Not exactly. Swapping releases the process; you can have N models configured and only pay RAM/disk for the resident one. Sleep mode keeps one live process per sleeping model, so each sleeping vLLM still occupies its process slot, its RAM (level 1) and its management footprint. Sleep scales well to a few rotating models; for many (10+), level 2 (minimal RAM) or plain swapping fit better. Do not put 15 models into level 1 sleep and expect the RAM to hold.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;The wake is instantaneous, I lose nothing.&amp;rdquo;&lt;/strong> The model&amp;rsquo;s wake is sub-second, but the KV cache was discarded on sleep. The first request after the wake pays a full prefill to repopulate the KV, slower than normal. If your SLA is strict on the first response after an idle period, add an automatic warm-up after the wake. &lt;a href="https://blog.lo0.es/en/posts/prefix-cache-hit-rate-engineering/">Prefix caching&lt;/a> helps make that reprefill cheaper if there are stable prefixes.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;llama-swap with a low &lt;code>ttl&lt;/code> saves me VRAM for free.&amp;rdquo;&lt;/strong> It saves you VRAM while nobody uses that model, but every time it comes back it pays the cold start. An aggressive &lt;code>ttl&lt;/code> on a model with bursty traffic turns every burst into a load wait. The right &lt;code>ttl&lt;/code> depends on the temporal pattern of the requests, not on how much VRAM you want to free. Measure it.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Time-slicing lets me co-reside more models.&amp;rdquo;&lt;/strong> False and dangerous. Time-slicing shares out compute time, not memory, and all the processes still compete for the same HBM with no isolation. Co-residing via time-slicing does not give you more effective VRAM; it gives you more processes fighting over the same VRAM, and an OOM when the sum overflows. For real memory partitioning, MIG. The detail is in the &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">sibling piece&lt;/a>.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Having more models than VRAM is not a problem of insufficient hardware: it is a problem of &lt;strong>managing a scarce resource over time&lt;/strong>. The intuition of &amp;ldquo;I need one GPU per model&amp;rdquo; is expensive and almost always wrong, because models are rarely used all at once. The three strategies are three answers to the same question, &lt;strong>where do the weights of the models you are not currently using live&lt;/strong>: in HBM if they fit (co-residency, zero latency but they cost space), on disk if switching is rare (swapping, maximum VRAM per model but a return trip of seconds to minutes), or in RAM if switching latency matters (sleep mode, sub-second wake at the cost of tied-up RAM). Sleep mode is the most interesting addition of 2025 because it breaks the false dilemma of &amp;ldquo;everything loaded vs reload every time&amp;rdquo;: by keeping the process alive and preserving the allocator, the CUDA graphs and the kernels, it turns a 30–100 s cold start into a sub-second wake, and it does so even when it reloads the weights from the same disk (level 2), because moving bytes was never the whole cost. On the four-H100 cluster, the play is not to pick one strategy but to spread: co-reside what goes together, isolate the light stuff with MIG, sleep or rotate the large and occasional, replicate what saturates. The GPU is the stage with a single spotlight; the art is knowing which actor walks on, which goes home and which waits in the wings.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/speeding-up-model-cold-start-minutes-to-seconds/">Speeding up model cold start: from minutes to seconds&lt;/a> — how to lower the cold-start cost that every swap pays.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">Sharing a GPU: time-slicing, MPS and MIG&lt;/a> — the sibling piece: how to split one GPU among several processes in space (not in time). Key here to understand why time-slicing does not protect memory and why MIG is what gives co-residency with real HBM isolation.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/pantry-lift-from-disk-to-hbm-why-the-kitchen-opens-late/">From disk to HBM: cold start and model loading&lt;/a> — the load path that swapping pays in full on every switch and that sleep mode sidesteps; why moving bytes is only one of five start-up items.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/multi-lora-serving-shared-base-concurrent-adapters-sgmv-kernel/">Multi-LoRA serving: fundamentals&lt;/a> — the alternative when the &amp;ldquo;several models&amp;rdquo; are adapters of the same base: instead of rotating whole models, you serve many LoRAs on a co-resident base with no swap cost.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/prefix-cache-hit-rate-engineering/">Engineering the prefix cache hit rate&lt;/a> — the KV cache is discarded on sleep; a good hit rate on stable prefixes makes the reprefill of the first request after the wake cheaper.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/one-big-replica-or-many-small-ones-platform-decision/">One big vs N small: tensor parallelism and replicas&lt;/a> — the &amp;ldquo;sustained load on all of them at once&amp;rdquo; branch of the tree: when rotating is not enough and you have to spread across more GPUs or replicas.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/mixed-nvidia-intel-environments-llm-inference/">Mixed NVIDIA / Intel environments: servers and NUCs&lt;/a> — where to place the light services (rerankers, embeddings) that do not need an H100: sometimes co-residing is not on the big GPU but on more modest hardware.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>vLLM Blog (Embedded LLM), &lt;em>Zero-Reload Model Switching with vLLM Sleep Mode&lt;/em>, 26 oct 2025: &lt;a href="https://blog.vllm.ai/2025/10/26/sleep-mode.html">https://blog.vllm.ai/2025/10/26/sleep-mode.html&lt;/a>&lt;/li>
&lt;li>vLLM Docs, &lt;em>Sleep Mode&lt;/em>: &lt;a href="https://docs.vllm.ai/en/latest/features/sleep_mode/">https://docs.vllm.ai/en/latest/features/sleep_mode/&lt;/a>&lt;/li>
&lt;li>mostlygeek, &lt;em>llama-swap&lt;/em> (OpenAI/Anthropic-compatible model-swapping proxy): &lt;a href="https://github.com/mostlygeek/llama-swap">https://github.com/mostlygeek/llama-swap&lt;/a>&lt;/li>
&lt;li>llama-swap, &lt;em>Configuration&lt;/em>: &lt;a href="https://github.com/mostlygeek/llama-swap/blob/main/docs/configuration.md">https://github.com/mostlygeek/llama-swap/blob/main/docs/configuration.md&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>H100 Tensor Core GPU&lt;/em> (HBM3 specs, 80 GB, ~3.35 TB/s): &lt;a href="https://www.nvidia.com/en-us/data-center/h100/">https://www.nvidia.com/en-us/data-center/h100/&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>Reducing Cold Start Latency for LLM Inference with NVIDIA Run:ai Model Streamer&lt;/em>: &lt;a href="https://developer.nvidia.com/blog/reducing-cold-start-latency-for-llm-inference-with-nvidia-runai-model-streamer/">https://developer.nvidia.com/blog/reducing-cold-start-latency-for-llm-inference-with-nvidia-runai-model-streamer/&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Sharing one GPU between several workloads: time-slicing, MPS and MIG</title><link>https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/</link><pubDate>Thu, 11 Jun 2026 02:00:00 +0000</pubDate><guid>https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/</guid><description>&lt;blockquote>
&lt;p>This post &lt;strong>opens an operational series&lt;/strong> on how to squeeze a generic on-premise 4×H100 SXM LLM cluster. The sibling pieces: &lt;a href="https://blog.lo0.es/en/posts/serving-several-models-single-gpu-co-residency-swapping-sleep-mode/">Serving several models on one GPU: swap and sleep&lt;/a> (what to do when the models do not fit at the same time and have to take turns in memory), &lt;a href="https://blog.lo0.es/en/posts/rag-cpu-data-plane-generation-plane/">RAG on CPU: separating the data plane from generation&lt;/a> (moving retrieval off the GPU to free it up) and Sovereign end-to-end assistant with LibreChat, LiteLLM and RAG, the final assembly, a fourth instalment in preparation. Here we start with the most basic question: you have one GPU and you want to put several workloads on it. How do you split it?&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>You have one GPU, or a few, and several workloads that want to run on it: a chat model, an embeddings service, a reranker, a queue of dev jobs. The GPU is underused if it only runs one thing, but piling several on carelessly causes contention, OOM or cascading failures. There are &lt;strong>three mechanisms&lt;/strong> and they share out different things. &lt;strong>Time-slicing&lt;/strong> (replicas from the NVIDIA k8s device-plugin) multiplexes in time: it advertises the GPU as &amp;ldquo;N GPUs&amp;rdquo; and the processes take turns at the compute, but they share the full physical VRAM, with no memory isolation, no fault isolation and no QoS. Its trap is an OOM that does not show up in the Kubernetes scheduler but at run time, when the sum of VRAM allocations exceeds the real memory. &lt;strong>MPS&lt;/strong> (Multi-Process Service) multiplexes in space: it shares out the SMs between processes that execute kernels concurrently, reduces context-switch overhead and allows SM and memory limits per process. It raises throughput when there are many small kernels, but fault isolation is still weak. &lt;strong>MIG&lt;/strong> (Multi-Instance GPU) partitions in hardware: it cuts the Hopper GPU into up to seven instances with dedicated SMs, L2, memory and bandwidth, with real memory, fault and performance isolation; datacenter only (A100/H100/H200/B200), never on an RTX 5090. The rule: real isolation / multi-tenant / compliance → MIG (if it is Hopper); many small concurrent kernels and trust between workloads → MPS; dev, bursts, consumer GPU or no need to isolate → time-slicing. This post works it through with numbers: the VRAM budget of four vLLM instances on an H100 advertised as four replicas, and what fits into a 10 GB MIG instance.&lt;/p>
&lt;h2 id="the-analogy-a-shared-hob-one-kitchen-with-several-cooks-several-kitchens">The analogy: a shared hob, one kitchen with several cooks, several kitchens&lt;/h2>
&lt;p>Imagine you have a single professional hob and three orders to cook at the same time. There are three ways to organise it, and they are exactly the three mechanisms.&lt;/p>
&lt;p>&lt;strong>Time-slicing is one hob taken in turns, with no larder of your own.&lt;/strong> Each cook comes in, cooks their dish, leaves, and the next one comes in. The split is temporal: nobody cooks at the same time, they take turns. The problem is not the hob, which gets shared out fine, but the common larder: the ingredients sit in a single shared cupboard and nobody has their own. If the three cooks reserve more flour than there is in total, it is not that they wait their turn: it is that there is no flour. The service goes down for everyone. And if one cook leaves a pan burning and starts a fire, it burns the whole kitchen, not just their corner.&lt;/p>
&lt;p>&lt;strong>MPS is several cooks coordinated on the same worktop.&lt;/strong> Now they do cook at the same time, sharing out the space on the worktop (the SMs). A head chef (the MPS daemon) coordinates so they do not collide and so the worktop does not sit empty while one of them waits for water to boil. You can assign each cook a percentage of the worktop and a larder limit. They work faster as a group because the worktop does not sit idle between small tasks. But they still share the kitchen: if one starts a serious fire, the others notice.&lt;/p>
&lt;p>&lt;strong>MIG is several independent kitchens in the same building.&lt;/strong> A concrete wall separates each kitchen: its own hob, its own larder, its own door and its own electrical panel. What happens in kitchen 3, a fire, an empty larder, a slow cook, does not touch kitchen 1. It is the only split with genuine isolation. The price: you have to decide in advance how many kitchens and of what size, the walls are fixed, and only expensive buildings (datacenter) come ready to put them up.&lt;/p>
&lt;p>The rest of the post is, essentially, when you want cheap turns, when you want coordinated cooks and when you need concrete walls.&lt;/p>
&lt;h2 id="why-share-the-operational-problem">Why share: the operational problem&lt;/h2>
&lt;p>An H100 SXM 80 GB does not fill up with just any workload. A &lt;code>bge-reranker-v2-m3&lt;/code> reranker takes a few hundred MB and saturates a handful of SMs; a &lt;code>bge-m3&lt;/code> embeddings service is just as small; a 1B guardrail model in INT4 fits in a couple of GB. Dedicating 80 GB of HBM3 and 132 SMs to serving embeddings is using a hydraulic press to hammer in a drawing pin, the same argument as in &lt;a href="https://blog.lo0.es/en/posts/mixed-nvidia-intel-environments-llm-inference/">mixed environments&lt;/a>, but now &lt;em>inside&lt;/em> the GPU instead of moving the workload to different silicon.&lt;/p>
&lt;p>The aim of sharing is to raise the useful utilisation of fixed capital. But sharing badly introduces three pathologies:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Compute contention&lt;/strong>: two workloads fight over the same SMs and both run slowly, with unpredictable latency jitter.&lt;/li>
&lt;li>&lt;strong>Memory contention&lt;/strong>: the sum of requested VRAM exceeds the physical amount and something dies with a &lt;code>CUDA out of memory&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Cascading failure&lt;/strong>: a workload that blows up (an illegal kernel, an OOM) can drag its neighbours down with it if they share a context.&lt;/li>
&lt;/ul>
&lt;p>The three mechanisms attack these pathologies to different depths. None of them solves all three except MIG, and MIG costs specific hardware. Let us look at them one by one.&lt;/p>
&lt;h2 id="time-slicing-compute-turns-shared-larder">Time-slicing: compute turns, shared larder&lt;/h2>
&lt;p>&lt;strong>Time-slicing&lt;/strong> is temporal multiplexing in software. On Kubernetes, the &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html">NVIDIA GPU Operator&lt;/a> configures the device-plugin to &lt;strong>advertise N replicas&lt;/strong> of each physical GPU. An H100 declared with &lt;code>replicas: 4&lt;/code> appears to the scheduler as four &lt;code>nvidia.com/gpu&lt;/code> resources, and Kubernetes can place four pods on it. Internally, the GPU scheduler hands out compute turns to each process: it runs a bit of process A, switches to B, to C, to D, then back to A. It is the same &lt;em>time-sharing&lt;/em> an operating system does with the CPU.&lt;/p>
&lt;p>The key idea, and the one that causes most confusion, is this: &lt;strong>a replica is NOT a fraction of the GPU&lt;/strong>. It is a compute turn. NVIDIA&amp;rsquo;s documentation is explicit: unlike MIG, there is no memory or fault isolation between replicas. The four replicas of the H100 see the full 80 GB of VRAM, unpartitioned. There is no 20 GB per replica. There are 80 GB for all four, handed out in &lt;code>cudaMalloc&lt;/code> arrival order.&lt;/p>
&lt;p>This has three consequences you need to internalise:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>It does not isolate memory.&lt;/strong> If the sum of what the four processes reserve exceeds 80 GB, the fourth &lt;code>cudaMalloc&lt;/code> fails with OOM. The Kubernetes scheduler does not see it coming: it counted four available &lt;code>nvidia.com/gpu&lt;/code> resources and happily placed four pods. The OOM appears at run time, not at &lt;em>scheduling&lt;/em> time. This is trap number one of time-slicing.&lt;/li>
&lt;li>&lt;strong>It does not isolate faults.&lt;/strong> A process that triggers an unrecoverable CUDA error can leave the GPU context in a state that affects its neighbours. They share the same device with no barriers.&lt;/li>
&lt;li>&lt;strong>It gives no compute QoS.&lt;/strong> Under contention, the turn-taking does not guarantee a minimum fraction to anyone. Each workload&amp;rsquo;s latency suffers jitter proportional to how many active replicas are fighting over the GPU at that instant. A latency-sensitive workload (an interactive chat) can see its TTFT dance around depending on what its neighbours are doing.&lt;/li>
&lt;/ol>
&lt;p>What is it good for, then? For dev, bursts and low utilisation. If you have four developers who touch the GPU sporadically, advertising four replicas lets all four have access without fighting almost ever (they rarely overlap while active). For batch workloads that tolerate jitter. And, a decisive advantage, &lt;strong>it works on consumer GPUs&lt;/strong>: an RTX 5090 32 GB does not support MIG, but it does support time-slicing. It is the only &amp;ldquo;Kubernetes-native&amp;rdquo; way to share a 5090 between several pods.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The three sharing strategies: temporal, spatial and hardware partitioning">
&lt;text x="410" y="24" text-anchor="middle" font-size="15" font-weight="700" fill="currentColor">How each mechanism shares out&lt;/text>
&lt;!-- TIME-SLICING -->
&lt;text x="140" y="56" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">Time-slicing&lt;/text>
&lt;text x="140" y="72" text-anchor="middle" font-size="11" fill="currentColor">split in TIME&lt;/text>
&lt;rect x="30" y="84" width="220" height="120" fill="none" stroke="currentColor" stroke-width="1.4"/>
&lt;!-- turns -->
&lt;rect x="40" y="96" width="200" height="20" fill="#3b82f6"/>
&lt;text x="140" y="111" text-anchor="middle" font-size="11" fill="#ffffff">turn A&lt;/text>
&lt;rect x="40" y="120" width="200" height="20" fill="#22c55e"/>
&lt;text x="140" y="135" text-anchor="middle" font-size="11" fill="#ffffff">turn B&lt;/text>
&lt;rect x="40" y="144" width="200" height="20" fill="#f59e0b"/>
&lt;text x="140" y="159" text-anchor="middle" font-size="11" fill="#ffffff">turn C&lt;/text>
&lt;rect x="40" y="168" width="200" height="20" fill="#ef4444"/>
&lt;text x="140" y="183" text-anchor="middle" font-size="11" fill="#ffffff">turn D&lt;/text>
&lt;text x="140" y="224" text-anchor="middle" font-size="11" fill="currentColor">shared VRAM: 80 GB for all 4&lt;/text>
&lt;text x="140" y="240" text-anchor="middle" font-size="11" fill="#ef4444">no isolation · OOM if sum &amp;gt; VRAM&lt;/text>
&lt;!-- MPS -->
&lt;text x="410" y="56" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">MPS&lt;/text>
&lt;text x="410" y="72" text-anchor="middle" font-size="11" fill="currentColor">split in SPACE (SMs)&lt;/text>
&lt;rect x="300" y="84" width="220" height="120" fill="none" stroke="currentColor" stroke-width="1.4"/>
&lt;rect x="310" y="96" width="60" height="92" fill="#3b82f6"/>
&lt;text x="340" y="146" text-anchor="middle" font-size="11" fill="#ffffff">A 40%&lt;/text>
&lt;rect x="372" y="96" width="45" height="92" fill="#22c55e"/>
&lt;text x="394" y="146" text-anchor="middle" font-size="11" fill="#ffffff">B 30%&lt;/text>
&lt;rect x="419" y="96" width="45" height="92" fill="#f59e0b"/>
&lt;text x="441" y="146" text-anchor="middle" font-size="10" fill="#ffffff">C 20%&lt;/text>
&lt;rect x="466" y="96" width="44" height="92" fill="#ef4444"/>
&lt;text x="488" y="146" text-anchor="middle" font-size="10" fill="#ffffff">D 10%&lt;/text>
&lt;text x="410" y="224" text-anchor="middle" font-size="11" fill="currentColor">concurrent kernels on different SMs&lt;/text>
&lt;text x="410" y="240" text-anchor="middle" font-size="11" fill="#f59e0b">SM and mem limit per process · weak fault&lt;/text>
&lt;!-- MIG -->
&lt;text x="680" y="56" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">MIG&lt;/text>
&lt;text x="680" y="72" text-anchor="middle" font-size="11" fill="currentColor">HARDWARE partitioning&lt;/text>
&lt;rect x="570" y="84" width="220" height="120" fill="none" stroke="currentColor" stroke-width="1.4"/>
&lt;rect x="580" y="96" width="98" height="44" fill="#3b82f6"/>
&lt;text x="629" y="122" text-anchor="middle" font-size="10" fill="#ffffff">1g.10gb&lt;/text>
&lt;rect x="684" y="96" width="98" height="44" fill="#22c55e"/>
&lt;text x="733" y="122" text-anchor="middle" font-size="10" fill="#ffffff">1g.10gb&lt;/text>
&lt;rect x="580" y="144" width="98" height="44" fill="#f59e0b"/>
&lt;text x="629" y="170" text-anchor="middle" font-size="10" fill="#ffffff">3g.40gb&lt;/text>
&lt;rect x="684" y="144" width="98" height="44" fill="#ef4444"/>
&lt;text x="733" y="170" text-anchor="middle" font-size="10" fill="#ffffff">…&lt;/text>
&lt;text x="680" y="224" text-anchor="middle" font-size="11" fill="currentColor">dedicated SMs, L2, VRAM and BW&lt;/text>
&lt;text x="680" y="240" text-anchor="middle" font-size="11" fill="#22c55e">real isolation · concrete walls&lt;/text>
&lt;!-- footer -->
&lt;text x="410" y="290" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">One hob in turns · several cooks on the worktop · several kitchens with their wall&lt;/text>
&lt;text x="410" y="312" text-anchor="middle" font-size="11" fill="currentColor">consumer + datacenter · datacenter (CUDA) · datacenter Hopper/Ampere/Blackwell only&lt;/text>
&lt;text x="410" y="332" text-anchor="middle" font-size="11" fill="currentColor">software · software (CUDA context) · hardware (physical fuses)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="the-vram-budget-in-time-slicing-the-calculation-that-avoids-the-oom">The VRAM budget in time-slicing (the calculation that avoids the OOM)&lt;/h3>
&lt;p>Here is the maths you have to do before deploying, because Kubernetes will not do it for you. Suppose an H100 80 GB advertised as 4 replicas and we want to run four vLLM instances on it, one per replica.&lt;/p>
&lt;p>vLLM reserves memory with the &lt;code>--gpu-memory-utilization&lt;/code> parameter, which is the fraction of the total physical VRAM that each instance keeps (for model weights plus KV-cache). The detail that kills: that fraction is computed over the 80 physical GB, &lt;strong>not&lt;/strong> over a supposed &amp;ldquo;20 GB of my replica&amp;rdquo;, because the replica does not have 20 GB. Remember, there is no memory partitioning. Each vLLM sees the 80 GB and reserves its fraction of them.&lt;/p>
&lt;p>The no-OOM constraint is therefore that the sum of fractions be less than 1:&lt;/p>
$$\sum_{i=1}^{N} g_i &lt; 1 \quad\Longleftrightarrow\quad \sum_{i=1}^{N} g_i \cdot V_{\text{HBM}} &lt; V_{\text{HBM}}$$
&lt;p>where $g_i$ is the &lt;code>--gpu-memory-utilization&lt;/code> of instance $i$ and $V_{\text{HBM}} = 80$ GB. It is wise to leave headroom (runtime overhead, fragmentation, CUDA context), so in practice you aim for the sum to stay comfortably below 1, say $\le 0.9$.&lt;/p>
&lt;p>&lt;strong>A case that works.&lt;/strong> Four vLLM instances at $g_i = 0.20$:&lt;/p>
$$\sum_{i=1}^{4} 0.20 = 0.80 \quad\Rightarrow\quad 0.80 \times 80\ \text{GB} = 64\ \text{GB} &lt; 80\ \text{GB} \quad\checkmark$$
&lt;p>Each instance reserves $0.20 \times 80 = 16$ GB. Four instances add up to 64 GB, leaving a 16 GB cushion. There is no OOM. Each vLLM has 16 GB for weights plus KV-cache: enough for a 7B–8B model in FP8/INT4 with a modest KV-cache.&lt;/p>
&lt;p>&lt;strong>A case that blows up.&lt;/strong> The same four instances, but somebody raises $g_i = 0.30$ thinking &amp;ldquo;I have four replicas, I can give each one more&amp;rdquo;:&lt;/p>
$$\sum_{i=1}^{4} 0.30 = 1.20 \quad\Rightarrow\quad 1.20 \times 80\ \text{GB} = 96\ \text{GB} > 80\ \text{GB} \quad\times$$
&lt;p>The first instances start up and reserve $0.30 \times 80 = 24$ GB each. Three instances are already at $72$ GB. The fourth tries to reserve another 24 GB, there is nothing left, and it dies with &lt;code>CUDA out of memory&lt;/code>. And worse: Kubernetes will reschedule it on the same GPU (it still sees four replicas), where it will die again, in a &lt;code>CrashLoopBackOff&lt;/code> that makes no sense if you only look at the pod manifest.&lt;/p>
&lt;p>The operational rule is brutally simple: &lt;strong>in time-slicing, you manage the VRAM budget by hand, adding up the &lt;code>--gpu-memory-utilization&lt;/code> values&lt;/strong>. The replica count controls how many pods &lt;em>fit in compute turns&lt;/em>, but it does not reserve a single byte of memory. Confusing the two is the recurring mistake.&lt;/p>
&lt;h2 id="mps-coordinated-cooks-on-the-same-worktop">MPS: coordinated cooks on the same worktop&lt;/h2>
&lt;p>The &lt;strong>Multi-Process Service&lt;/strong> (&lt;a href="https://docs.nvidia.com/deploy/mps/index.html">MPS&lt;/a>) attacks a different problem. By default, when several processes use the same GPU without MPS, each one has its own CUDA context, and the GPU alternates between contexts (time-slicing at driver level): they do not execute kernels at the same time, they take turns, with context-switch overhead. If your kernels are small and do not fill the GPU on their own, this leaves SMs idle: process A uses 30 % of the SMs during its turn and the other 70 % is wasted.&lt;/p>
&lt;p>MPS introduces a daemon that shares a single CUDA context between processes, so that their kernels can execute concurrently occupying different SMs at the same time. It is spatial sharing of compute: instead of taking turns at the whole worktop, each cook occupies a part and they work in parallel. This reduces context-switch overhead and &lt;strong>raises throughput when there are many small concurrent kernels&lt;/strong> that individually do not saturate the GPU.&lt;/p>
&lt;p>And, unlike pure time-slicing, MPS allows per-process limits, which gives you a form of QoS:&lt;/p>
&lt;ul>
&lt;li>&lt;code>CUDA_MPS_ACTIVE_THREAD_PERCENTAGE&lt;/code> limits the percentage of SMs an MPS client can use. By default each client gets $100 / \text{MaxSharedClients}$. Pinning it to, say, 40 % caps that process&amp;rsquo;s compute ceiling (&lt;a href="https://docs.nvidia.com/deploy/mps/appendix-tools-and-interface-reference.html">MPS docs&lt;/a>).&lt;/li>
&lt;li>&lt;code>CUDA_MPS_PINNED_DEVICE_MEM_LIMIT&lt;/code> imposes a memory cap per client (valid from CUDA 11.5). This is what time-slicing does &lt;strong>not&lt;/strong> have: a per-process VRAM limit that the runtime enforces.&lt;/li>
&lt;/ul>
&lt;p>These two limits turn MPS into a resource provisioning mechanism that mitigates the &lt;em>noisy neighbour&lt;/em>: you can guarantee that a process does not eat more than X % of the SMs or more than Y GB. The combination gives reasonable QoS, not perfect but real.&lt;/p>
&lt;p>The limitation MPS does &lt;strong>not&lt;/strong> solve: &lt;strong>fault isolation is weak&lt;/strong>. Since clients share the daemon&amp;rsquo;s CUDA context, a fatal error in one client can affect the daemon and therefore the other clients (historically, a client dying dirtily could require restarting the daemon). It is better than time-slicing in this respect, but a long way from hardware isolation. That is why MPS fits when there is trust between the workloads, processes from your own team, not third-party tenants.&lt;/p>
&lt;p>The canonical use case: many small concurrent inference requests that individually leave the GPU half empty. MPS overlaps them and raises aggregate throughput. Serving several small models, or several light replicas of the same model, on a datacenter GPU where you trust all the workloads, is MPS territory.&lt;/p>
&lt;h2 id="mig-concrete-walls">MIG: concrete walls&lt;/h2>
&lt;p>The &lt;strong>Multi-Instance GPU&lt;/strong> (&lt;a href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/">MIG&lt;/a>) is the only one of the three that gives genuine isolation, because it cuts the GPU in hardware. Available on modern datacenter GPUs, A100 (Ampere), H100/H200 (Hopper), B200 (Blackwell), and never on consumer ones: an RTX 5090 (consumer Blackwell) &lt;strong>does not support MIG&lt;/strong>, nor do GeForce cards in general.&lt;/p>
&lt;p>MIG divides the GPU into up to seven instances (&lt;em>GPU Instances&lt;/em>), and each instance gets a dedicated portion of:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>SMs&lt;/strong> (compute slices): compute is split into 7 &lt;em>slices&lt;/em>, each one roughly 1/7 of the SMs.&lt;/li>
&lt;li>&lt;strong>L2 cache and memory&lt;/strong>: each instance has its slab of HBM and its portion of L2 cache.&lt;/li>
&lt;li>&lt;strong>Memory bandwidth&lt;/strong>: dedicated, not shared.&lt;/li>
&lt;li>&lt;strong>Data paths and engines&lt;/strong>: with fault barriers between instances.&lt;/li>
&lt;/ul>
&lt;p>The result is that a MIG instance behaves like a smaller, independent GPU: whatever happens in one, an OOM, a kernel blowing up, a workload saturating its compute, &lt;strong>does not affect&lt;/strong> its neighbours. Memory, fault and performance (QoS) isolation, the three things time-slicing does not give and MPS only half gives.&lt;/p>
&lt;h3 id="the-h100-80gb-profiles">The H100 80GB profiles&lt;/h3>
&lt;p>MIG does not allow arbitrary sizes: it has &lt;strong>fixed profiles&lt;/strong>. On the H100 80GB, the &lt;a href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-mig-profiles.html">profile catalogue&lt;/a> (notation &lt;code>&amp;lt;compute&amp;gt;g.&amp;lt;memory&amp;gt;gb&lt;/code>) is:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Profile&lt;/th>
&lt;th>Compute (slices)&lt;/th>
&lt;th>Memory&lt;/th>
&lt;th>Max. instances&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>1g.10gb&lt;/code>&lt;/td>
&lt;td>1/7&lt;/td>
&lt;td>10 GB&lt;/td>
&lt;td>7&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>1g.20gb&lt;/code>&lt;/td>
&lt;td>1/7&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>2g.20gb&lt;/code>&lt;/td>
&lt;td>2/7&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>3g.40gb&lt;/code>&lt;/td>
&lt;td>3/7&lt;/td>
&lt;td>40 GB&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>4g.40gb&lt;/code>&lt;/td>
&lt;td>4/7&lt;/td>
&lt;td>40 GB&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>7g.80gb&lt;/code>&lt;/td>
&lt;td>7/7&lt;/td>
&lt;td>80 GB&lt;/td>
&lt;td>1 (whole GPU)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>(There is also &lt;code>1g.10gb+me&lt;/code>, a variant with &lt;em>media engines&lt;/em> for video encoding.) The base memory unit on the H100 80GB is 10 GB per slice (80 GB / 8, with one slice reserved), and the compute unit is 1/7 of the SMs. The profiles combine these units. Note &lt;code>1g.20gb&lt;/code>: the same compute fraction as &lt;code>1g.10gb&lt;/code> (1/7 of the SMs) but twice the memory, useful when a workload needs more VRAM than compute.&lt;/p>
&lt;p>One important detail: MIG partitions do not mix freely. The GPU is divided following a valid geometry (the profiles fit together like pieces in a grid), and the profiles are fixed when you configure the GPU; changing them requires draining and repartitioning. They are concrete walls: solid, but they do not move while hot.&lt;/p>
&lt;h3 id="the-calculation-71g10gb-against-17g80gb">The calculation: 7×1g.10gb against 1×7g.80gb&lt;/h3>
&lt;p>Let us compare the two extremes. On the left, seven &lt;code>1g.10gb&lt;/code> instances: seven isolated 10 GB GPUs. On the right, a single &lt;code>7g.80gb&lt;/code>: the whole H100, unpartitioned.&lt;/p>
&lt;p>The operational question is what fits in 10 GB. An instance&amp;rsquo;s VRAM budget is split between model weights and KV-cache:&lt;/p>
$$V_{\text{inst}} = V_{\text{weights}} + V_{\text{KV}} + V_{\text{overhead}}$$
&lt;p>Take a model of 7B parameters in FP8 (1 byte/parameter):&lt;/p>
$$V_{\text{weights}} \approx 7 \times 10^9 \times 1\ \text{byte} = 7\ \text{GB}$$
&lt;p>On a &lt;code>1g.10gb&lt;/code> instance (10 GB), after the 7 GB of weights and subtracting roughly 0.5–1 GB of runtime overhead, about 2 GB are left for KV-cache. That is enough for a modest context window and low concurrency, fine for a guardrail service, a classifier or an extraction model that handles short prompts one at a time. A 7B in INT4 (~3.5 GB of weights) leaves ~5.5 GB of KV-cache, much roomier. But a 13B model in FP8 (~13 GB of weights) &lt;strong>does not fit&lt;/strong> in a 10 GB instance: not even the weights get in. For that you need &lt;code>1g.20gb&lt;/code>, &lt;code>2g.20gb&lt;/code> or larger.&lt;/p>
&lt;p>Against this, the &lt;code>7g.80gb&lt;/code> (whole GPU) gives you the 80 GB for one large model: a 70B in FP8 (~70 GB of weights) fits with a tight KV-cache, or a 70B with more room spread across several whole GPUs in &lt;em>tensor-parallel&lt;/em> (see &lt;a href="https://blog.lo0.es/en/posts/one-big-replica-or-many-small-ones-platform-decision/">TP against replicas: one big one versus N small ones&lt;/a>).&lt;/p>
&lt;p>The reading is clear: &lt;strong>fine partitioning (7×1g.10gb) maximises the number of small isolated workloads; not partitioning (1×7g.80gb) maximises the model size that fits.&lt;/strong> The KV-cache available per instance shrinks proportionally as you partition, so fine-grained MIG serves many light isolated services, not one large model chopped up. If your workload is a single large model, MIG is not for you: use the whole GPU or several in TP.&lt;/p>
&lt;h2 id="the-decision-tree">The decision tree&lt;/h2>
&lt;p>The three questions, in order:&lt;/p>
&lt;pre tabindex="0">&lt;code>Do you need REAL isolation?
(multi-tenant, compliance, one workload failing must not touch another)
│
┌────┴────┐
YES NO
│ │
Is it Hopper/ Many SMALL concurrent kernels
Ampere/ AND you trust every workload?
Blackwell? │
│ ┌────┴────┐
┌──┴──┐ YES NO
YES NO │ │
│ │ MPS Dev / bursts / consumer GPU
MIG │ (spatial, / no need to isolate?
│ │ per-process │
│ no real QoS) YES
│ isolation: │
│ rethink TIME-SLICING
│ (move to (temporal, cheap,
│ CPU, another works on a 5090)
│ GPU, or accept
│ the risk with
│ time-slicing)
&lt;/code>&lt;/pre>&lt;p>And each branch in one sentence:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MIG&lt;/strong> when isolation is a requirement (compliance, multi-tenant, hard SLA) and you have datacenter hardware that supports it. Concrete walls cost money, but if you need them there is no substitute.&lt;/li>
&lt;li>&lt;strong>MPS&lt;/strong> when you have many small concurrent workloads that leave the GPU half empty and you trust all of them (same team, not third-party tenants). You raise throughput with reasonable QoS, accepting imperfect fault isolation.&lt;/li>
&lt;li>&lt;strong>Time-slicing&lt;/strong> when it is dev, bursts, low utilisation, a consumer GPU, or you simply do not need to isolate anything. Cheap and universal, but manage the VRAM budget by hand.&lt;/li>
&lt;/ul>
&lt;p>One nuance the recent documentation records: &lt;strong>they can be combined&lt;/strong>. You can do time-slicing &lt;em>on top of&lt;/em> a MIG instance (hardware isolation at the instance boundary, software turns inside), or use MPS inside a MIG instance. The layers are not mutually exclusive; the tree picks the primary strategy.&lt;/p>
&lt;h2 id="applied-to-the-generic-4h100-cluster">Applied to the generic 4×H100 cluster&lt;/h2>
&lt;p>Let us get down to numbers with a generic on-premise cluster of 4×H100 SXM 80 GB with NVLink. It is common to have a heterogeneous menu of workloads: a large chat model, light services (embeddings, reranker, guardrails) and a dev/experimentation queue. Each type calls for a different mechanism. A reasoned split:&lt;/p>
&lt;p>&lt;strong>GPU 0 and GPU 1, large model in tensor-parallel (not shared).&lt;/strong> A 70B model in FP8 takes ~70 GB of weights; served comfortably with a generous KV-cache it needs more than one H100. We spread it in &lt;em>tensor-parallel&lt;/em> over two whole H100s joined by NVLink (intra-node bandwidth is what makes TP viable; the detail is in &lt;a href="https://blog.lo0.es/en/posts/one-big-replica-or-many-small-ones-platform-decision/">TP against replicas&lt;/a>). Here we do not share: these two GPUs belong to the large model, full stop. Total isolation by dedication.&lt;/p>
$$V_{\text{available}} = 2 \times 80 = 160\ \text{GB};\quad V_{\text{weights}} \approx 70\ \text{GB};\quad V_{\text{KV}} \approx 80\ \text{GB of KV-cache}$$
&lt;p>There is memory to spare for a long request queue and high concurrency.&lt;/p>
&lt;p>&lt;strong>GPU 2, split with MIG into small instances for light services.&lt;/strong> Embeddings (&lt;code>bge-m3&lt;/code>), reranker (&lt;code>bge-reranker-v2-m3&lt;/code>) and a couple of guardrail models (1B–3B) are different workloads, potentially from different teams, and you want a failure or a spike in one not to touch the others. Light multi-tenancy with isolation → &lt;strong>MIG&lt;/strong>. A reasonable partitioning of the H100:&lt;/p>
$$\underbrace{3 \times \texttt{1g.10gb}}_{\text{30 GB, 3/7 SMs}} \;+\; \underbrace{1 \times \texttt{4g.40gb}}_{\text{40 GB, 4/7 SMs}}$$
&lt;p>The three &lt;code>1g.10gb&lt;/code> instances (10 GB, 1/7 of the SMs each) host embeddings, reranker and a 1B INT4 guardrail, each isolated, with no &lt;em>noisy neighbour&lt;/em>. The &lt;code>4g.40gb&lt;/code> (40 GB, 4/7 of the SMs) hosts an intermediate 7B–13B model with a decent KV-cache for a support service. Each service has its larder and its wall; if the reranker blows up, the chat never finds out.&lt;/p>
&lt;p>&lt;strong>GPU 3, time-slicing for dev and bursts.&lt;/strong> The developers touch the GPU sporadically: experiments, short fine-tunes, model trials. They do not need isolation (it is the same team) and they rarely overlap while active. We advertise it as 4 replicas via the device-plugin. Four dev pods fit in turns. VRAM budget with the formula above: if each dev brings up a vLLM at &lt;code>--gpu-memory-utilization 0.20&lt;/code>, they add up to $4 \times 16 = 64$ GB &amp;lt; 80 GB, with no OOM. If somebody needs more, lower the replica count or coordinate with the team. The cost of flexibility is manual discipline.&lt;/p>
&lt;p>&lt;strong>Summary of the split:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Resource&lt;/th>
&lt;th>Mechanism&lt;/th>
&lt;th>Workload&lt;/th>
&lt;th>Isolation&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>GPU 0 + GPU 1&lt;/td>
&lt;td>Dedication (TP)&lt;/td>
&lt;td>70B chat in tensor-parallel&lt;/td>
&lt;td>total (dedicated)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPU 2&lt;/td>
&lt;td>MIG (3×1g.10gb + 1×4g.40gb)&lt;/td>
&lt;td>embeddings, reranker, guardrails, 7B–13B&lt;/td>
&lt;td>real hardware&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPU 3&lt;/td>
&lt;td>Time-slicing (4 replicas)&lt;/td>
&lt;td>dev, bursts, experiments&lt;/td>
&lt;td>none (trust)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The logic is always the same: &lt;strong>spend isolation (MIG) where you need it, spend cheap concurrency (time-slicing) where you do not, and reserve whole GPUs for what genuinely fills them.&lt;/strong> An H100 serving embeddings on a &lt;code>7g.80gb&lt;/code> would be as absurd as an RTX 5090 trying MIG: the tool does not match the workload.&lt;/p>
&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>What happens when they do not even fit at the same time&lt;/strong>: if you have more models than VRAM and they have to take turns &lt;em>in memory&lt;/em> (loading/unloading weights, not just taking turns at compute), you enter &lt;em>swap&lt;/em> and &lt;em>sleep&lt;/em> territory, the sibling piece &lt;a href="https://blog.lo0.es/en/posts/serving-several-models-single-gpu-co-residency-swapping-sleep-mode/">Serving several models on one GPU&lt;/a>.&lt;/li>
&lt;li>&lt;strong>NUMA-aware scheduling&lt;/strong>: on multi-socket nodes, which GPU goes with which CPU/memory matters for latency; see &lt;a href="https://blog.lo0.es/en/posts/cpu-memory-topology-manager-rke2/">Kubelet resource managers on RKE2&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Autoscaling the replicas&lt;/strong>: how many instances to bring up according to real load, with KEDA and queue metrics; see &lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">Autoscaling LLMs on Kubernetes with KEDA&lt;/a>.&lt;/li>
&lt;li>&lt;strong>A jitter benchmark under contention&lt;/strong>: how much TTFT really wobbles in time-slicing with 4 active replicas compared with MIG, material that deserves its own measurement, not an estimate.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/finops-multi-tenancy-gpu-cluster-who-pays/">FinOps and multi-tenancy of the GPU cluster: who pays for what&lt;/a> — MIG as the basis for isolation and cost attribution between tenants.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">GitOps for the inference stack with Flux: operating the assistant as code&lt;/a> — how the GPU split (MIG, gpu-memory-utilization) is declared as code in GitOps.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/serving-several-models-single-gpu-co-residency-swapping-sleep-mode/">Serving several models on one GPU: swap and sleep&lt;/a> — the sibling piece in the series: when the models do not fit in VRAM at the same time and have to take turns in memory, not just in compute.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/cpu-memory-topology-manager-rke2/">Kubelet resource managers on RKE2: NUMA and topology&lt;/a> — GPU sharing gets more complicated with NUMA affinity; which GPU to assign to which socket so you do not pay interconnect latency.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/one-big-replica-or-many-small-ones-platform-decision/">TP against replicas: one big one versus N small ones&lt;/a> — the decision to dedicate 2 whole H100s in tensor-parallel to the large model is exactly what we assume here in the cluster split.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning for on-premise LLM inference&lt;/a> — the VRAM budget (weights + KV-cache) we work through here per instance is the core of sizing the whole cluster.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">Autoscaling LLMs on Kubernetes with KEDA&lt;/a> — how many replicas (time-sliced or not) to bring up according to real load, instead of fixing them by hand.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">Five maturity levels of an on-premise LLM platform&lt;/a> — going from &amp;ldquo;one GPU, one workload&amp;rdquo; to sharing with isolation is one of the maturity jumps the model marks out.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gpu-idle-cost-not-on-any-invoice/">GPU utilisation as a FinOps lever&lt;/a> — MIG, MPS and time-slicing as levers to raise occupancy and lower cost per token.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>NVIDIA — &lt;em>Time-Slicing GPUs in Kubernetes&lt;/em> (GPU Operator). &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html">https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>Multi-Process Service (MPS) Overview&lt;/em>. &lt;a href="https://docs.nvidia.com/deploy/mps/index.html">https://docs.nvidia.com/deploy/mps/index.html&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>MPS: Tools and Interface Reference&lt;/em> (&lt;code>CUDA_MPS_ACTIVE_THREAD_PERCENTAGE&lt;/code>, &lt;code>CUDA_MPS_PINNED_DEVICE_MEM_LIMIT&lt;/code>). &lt;a href="https://docs.nvidia.com/deploy/mps/appendix-tools-and-interface-reference.html">https://docs.nvidia.com/deploy/mps/appendix-tools-and-interface-reference.html&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>Multi-Instance GPU (MIG) User Guide&lt;/em>. &lt;a href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/">https://docs.nvidia.com/datacenter/tesla/mig-user-guide/&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>Supported MIG Profiles&lt;/em> (H100 80GB catalogue). &lt;a href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-mig-profiles.html">https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-mig-profiles.html&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>k8s-device-plugin&lt;/em> (time-slicing replicas). &lt;a href="https://github.com/NVIDIA/k8s-device-plugin">https://github.com/NVIDIA/k8s-device-plugin&lt;/a>&lt;/li>
&lt;li>vLLM — &lt;em>Engine Arguments&lt;/em> (&lt;code>--gpu-memory-utilization&lt;/code>). &lt;a href="https://docs.vllm.ai/en/latest/serving/engine_args.html">https://docs.vllm.ai/en/latest/serving/engine_args.html&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>