<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Mig on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/mig/</link><description>Recent content in Mig on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 15 Jun 2026 05:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/mig/index.xml" rel="self" type="application/rss+xml"/><item><title>GPU idle: the cost that shows up on no invoice but is paid by the whole TCO</title><link>https://blog.lo0.es/en/posts/gpu-idle-cost-not-on-any-invoice/</link><pubDate>Mon, 15 Jun 2026 05:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/gpu-idle-cost-not-on-any-invoice/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. The dollar sign is not used
(on this site it is a formula delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The cost per token of on-premise inference is \( \text{€/GPU-hour} \div \text{throughput} \). Throughput is a direct function of the GPU&amp;rsquo;s &lt;strong>useful occupancy&lt;/strong>. On a generic node of 4×H100 SXM with a reference cost of ~11 € per GPU-hour (depreciation plus energy plus infrastructure), the cost curve over occupancy looks like this:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Useful occupancy&lt;/th>
&lt;th>Effective throughput (tok/s)&lt;/th>
&lt;th>Cost per 1M tokens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>20 %&lt;/td>
&lt;td>~700&lt;/td>
&lt;td>~43 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>40 %&lt;/td>
&lt;td>~1,400&lt;/td>
&lt;td>~21 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>70 %&lt;/td>
&lt;td>~2,500&lt;/td>
&lt;td>~12 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>85 % (practical ceiling)&lt;/td>
&lt;td>~3,000&lt;/td>
&lt;td>~10 €&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Doubling occupancy from 20 % to 40 % halves the cost per token, without buying more iron.&lt;/strong> At 70 % the cost competes with European cloud providers (~2.2 €/GPU-hour on-demand at Scaleway). At 20 % on-prem is four times dearer than renting. The lever is not the model, nor the precision: it is how many paid-for GPU-hours produce serving tokens.&lt;/p>
&lt;hr>
&lt;h2 id="1--the-fundamental-identity">1 · The fundamental identity&lt;/h2>
&lt;p>The cost per million tokens (CPM) in your own inference is not a list price. It is:&lt;/p>
$$
\text{CPM} = \frac{C_{\text{GPU}} \cdot N_{\text{GPU}}}{T_{\text{ef}} \times 3600 / 10^6}
$$
&lt;p>where \( C_{\text{GPU}} \) is the cost per GPU-hour (€/h), \( N_{\text{GPU}} \) the number of GPUs assigned to the service and \( T_{\text{ef}} \) the effective throughput (tok/s). Making the dependence on occupancy explicit:&lt;/p>
$$
T_{\text{ef}} = T_{\text{pico}} \times \rho
$$
&lt;p>where \( \rho \in [0,1] \) is the &lt;strong>useful occupancy rate&lt;/strong> (the fraction of time in which the GPU is processing real serving tokens). The resulting identity:&lt;/p>
$$
\text{CPM} = \frac{C_{\text{GPU}} \cdot N_{\text{GPU}}}{T_{\text{pico}} \times \rho \times 3600 / 10^6}
$$
&lt;p>The direct consequence: CPM is &lt;strong>inversely proportional to&lt;/strong> \( \rho \). Doubling \( \rho \) halves CPM. The numerator (cost of the iron) does not change.&lt;/p>
&lt;p>The posts &lt;a href="https://blog.lo0.es/en/posts/gpu-hour-to-cost-per-token-on-prem-vs-cloud/">coste-por-token-y-por-request&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity-planning-inferencia-llm-on-premise&lt;/a> cover how to calculate \( C_{\text{GPU}} \) and the reference peak throughput. This article deals with \( \rho \): how to measure it, what limits it and how to raise it.&lt;/p>
&lt;hr>
&lt;h2 id="2--why-the-standard-metric-lies-dcgm_fi_dev_gpu_util">2 · Why the standard metric lies: &lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code>&lt;/h2>
&lt;p>The field &lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code> (field ID 203) appears in &lt;code>nvidia-smi&lt;/code> as &amp;ldquo;GPU-Util&amp;rdquo;. Its official definition in the DCGM documentation:&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;GPU Utilization&amp;rdquo; — percentage of time during which one or more kernels were executing on the GPU.&lt;/p>
&lt;/blockquote>
&lt;p>The problem for LLM inference: &lt;strong>the decode phase is memory-bound&lt;/strong>. The GPU runs a kernel reading weights from HBM token by token; therefore &lt;code>GPU_UTIL&lt;/code> registers close to 100 % even though the tensor cores are at 15 % of their capacity. The field measures &lt;em>activity&lt;/em>, not &lt;em>useful work&lt;/em>.&lt;/p>
&lt;p>The distinction is critical for FinOps: an operator who sees &lt;code>GPU_UTIL 98 %&lt;/code> assumes &amp;ldquo;saturated GPU, no room for more load&amp;rdquo;. The reality may be &amp;ldquo;the tensor cores are at 20 % and the bottleneck is HBM&amp;rdquo;, which leaves room for continuous batching or additional bin-packing.&lt;/p>
&lt;p>The metrics that measure real occupancy are those of the &lt;code>_FI_PROF_*&lt;/code> subsystem, available in DCGM 3.x with the profiling module enabled:&lt;/p>
&lt;h3 id="table-of-relevant-dcgm-fields">Table of relevant DCGM fields&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>DCGM field&lt;/th>
&lt;th>Field ID&lt;/th>
&lt;th>What it measures&lt;/th>
&lt;th>Unit&lt;/th>
&lt;th>Operational note&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code>&lt;/td>
&lt;td>203&lt;/td>
&lt;td>% time with ≥1 active kernel&lt;/td>
&lt;td>%&lt;/td>
&lt;td>&lt;strong>Misleading&lt;/strong> in LLM decode&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_SM_ACTIVE&lt;/code>&lt;/td>
&lt;td>1002&lt;/td>
&lt;td>Ratio of cycles with ≥1 active warp per SM&lt;/td>
&lt;td>0–1&lt;/td>
&lt;td>Compute activity, not occupancy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>&lt;/td>
&lt;td>1003&lt;/td>
&lt;td>Resident warps / theoretical maximum per SM&lt;/td>
&lt;td>0–1&lt;/td>
&lt;td>Real intra-SM parallelism&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>&lt;/td>
&lt;td>1004&lt;/td>
&lt;td>% cycles with tensor cores (HMMA) active&lt;/td>
&lt;td>0–1&lt;/td>
&lt;td>The real compute efficiency metric&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>&lt;/td>
&lt;td>1005&lt;/td>
&lt;td>% cycles with HBM transferring&lt;/td>
&lt;td>0–1&lt;/td>
&lt;td>Memory saturation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_PCIE_TX_BYTES&lt;/code>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>TX bytes over PCIe&lt;/td>
&lt;td>bytes/s&lt;/td>
&lt;td>Useful in PCIe inference&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_FB_USED&lt;/code>&lt;/td>
&lt;td>252&lt;/td>
&lt;td>HBM used&lt;/td>
&lt;td>MiB&lt;/td>
&lt;td>VRAM budget&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Real consumption&lt;/td>
&lt;td>W&lt;/td>
&lt;td>For real energy cost&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_CLOCK_THROTTLE_REASONS&lt;/code>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Bitmap of throttle causes&lt;/td>
&lt;td>bitmap&lt;/td>
&lt;td>Detects silent degradation&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;code>_PROF_*&lt;/code> fields require the DCGM profiling module and the right driver permissions. They are documented exhaustively in the NVIDIA DCGM field ID reference and in the sibling post &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">observabilidad-gpu-dcgm-llm&lt;/a>.&lt;/p>
&lt;h3 id="characteristic-reading-in-llm-decode-llama-70b-fp8-on-h100">Characteristic reading in LLM decode (Llama 70B FP8 on H100)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Typical decode value&lt;/th>
&lt;th>Interpretation&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code>&lt;/td>
&lt;td>95–99 %&lt;/td>
&lt;td>&lt;strong>A lie&lt;/strong>: there is an active kernel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_SM_ACTIVE&lt;/code>&lt;/td>
&lt;td>0.45–0.65&lt;/td>
&lt;td>SMs with warps 45–65 % of the time&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>&lt;/td>
&lt;td>0.30–0.55&lt;/td>
&lt;td>Resident warps at 30–55 % of the maximum&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>&lt;/td>
&lt;td>0.10–0.25&lt;/td>
&lt;td>Tensor cores active only 10–25 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>&lt;/td>
&lt;td>0.75–0.90&lt;/td>
&lt;td>HBM busy 75–90 % of the time&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Reading: decode is &lt;strong>memory-bound&lt;/strong>. The model weights are loaded from HBM for each token; HBM is saturated but the tensor cores wait. All of the &amp;ldquo;utilisation&amp;rdquo; in &lt;code>nvidia-smi&lt;/code> comes from memory reads, not from compute.&lt;/p>
&lt;hr>
&lt;h2 id="3--mfu-and-hfu-occupancy-expressed-in-flops">3 · MFU and HFU: occupancy expressed in FLOPs&lt;/h2>
&lt;p>The canonical compute efficiency metric is &lt;strong>MFU (Model FLOPs Utilization)&lt;/strong>, defined in the PaLM paper (Chowdhery et al., arXiv 2204.02311, section 4):&lt;/p>
$$
\text{MFU} = \frac{T_{\text{obs}} \times C_{\text{modelo}}}{P_{\text{pico}}}
$$
&lt;p>where \( T_{\text{obs}} \) is the observed throughput (tok/s) and \( P_{\text{pico}} \) is the theoretical peak performance of the hardware (FLOP/s).&lt;/p>
&lt;p>where \( C_{\text{modelo}} \) is the number of FLOPs per token in a complete forward pass. For a dense transformer with \( P \) parameters, the usual approximation (forward plus backward = 6P FLOPs per token; forward only = 2P):&lt;/p>
$$
C_{\text{modelo}} \approx 2P \quad \text{(inference)}
$$
$$
C_{\text{modelo}} \approx 6P \quad \text{(training, forward + backward)}
$$
&lt;p>&lt;strong>HFU (Hardware FLOPs Utilization)&lt;/strong> measures the FLOPs &lt;strong>actually executed&lt;/strong> in hardware, including activation recomputation (gradient checkpointing). In training with recompute:&lt;/p>
$$
C_{\text{hardware}} \approx 8P \quad \text{(forward × 2 + backward × 4)}
$$
&lt;p>therefore HFU &amp;gt; MFU when there is recompute; they are identical without it.&lt;/p>
&lt;h3 id="typical-mfu-values">Typical MFU values&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Regime&lt;/th>
&lt;th>Hardware&lt;/th>
&lt;th>Typical MFU&lt;/th>
&lt;th>Limiting regime&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Training (large batch, BF16)&lt;/td>
&lt;td>H100 SXM&lt;/td>
&lt;td>35–50 %&lt;/td>
&lt;td>compute-bound&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Training PaLM 540B&lt;/td>
&lt;td>TPU v4&lt;/td>
&lt;td>46.2 %&lt;/td>
&lt;td>compute-bound&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prefill inference (large batch)&lt;/td>
&lt;td>H100 SXM&lt;/td>
&lt;td>25–45 %&lt;/td>
&lt;td>compute-bound&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Decode inference (bs=1)&lt;/td>
&lt;td>H100 SXM&lt;/td>
&lt;td>3–8 %&lt;/td>
&lt;td>memory-bound&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Decode inference (continuous batching, bs=32–64)&lt;/td>
&lt;td>H100 SXM&lt;/td>
&lt;td>15–30 %&lt;/td>
&lt;td>memory-bound, attenuated&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Decode with batch size 1 has a single-digit MFU because the hardware spends most of its time waiting for HBM to deliver weights. Raising the batch size (continuous batching) amortises the weight read across more simultaneous tokens and raises MFU.&lt;/p>
&lt;h3 id="the-roofline-model">The Roofline model&lt;/h3>
&lt;p>The roofline places each operation in the space (arithmetic intensity, throughput):&lt;/p>
&lt;div class="diagram" style="max-width:680px;margin:1rem auto;">
&lt;svg viewBox="0 0 680 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Roofline model for LLM inference: compute-bound prefill vs memory-bound decode">
&lt;style>.ax{stroke:currentColor;stroke-width:1.4;fill:none}.lbl{font:11px sans-serif;fill:currentColor}.ttl{font:700 13px sans-serif;fill:currentColor}.roof{stroke:currentColor;stroke-width:2;fill:none}.pt{fill:currentColor}&lt;/style>
&lt;text x="340" y="22" text-anchor="middle" class="ttl">Roofline: prefill vs decode on H100 SXM&lt;/text>
&lt;line x1="60" y1="270" x2="640" y2="270" class="ax"/>
&lt;line x1="60" y1="270" x2="60" y2="30" class="ax"/>
&lt;text x="350" y="298" text-anchor="middle" class="lbl">Arithmetic intensity (FLOP/byte)&lt;/text>
&lt;text x="14" y="155" text-anchor="middle" class="lbl" transform="rotate(-90 14 155)">Throughput (TFLOP/s)&lt;/text>
&lt;line x1="60" y1="270" x2="220" y2="60" class="roof"/>
&lt;line x1="220" y1="60" x2="640" y2="60" class="roof"/>
&lt;text x="230" y="52" class="lbl">~990 TFLOP/s (H100 BF16)&lt;/text>
&lt;text x="65" y="200" class="lbl" fill="currentColor">mem. bandwidth&lt;/text>
&lt;text x="65" y="213" class="lbl" fill="currentColor">~3.35 TB/s&lt;/text>
&lt;line x1="215" y1="50" x2="215" y2="270" stroke="currentColor" stroke-width="1" stroke-dasharray="4 3"/>
&lt;text x="200" y="284" class="lbl">~295&lt;/text>
&lt;text x="148" y="284" class="lbl">FLOP/byte ridge&lt;/text>
&lt;circle cx="130" cy="175" r="7" class="pt"/>
&lt;text x="138" y="171" class="lbl">decode bs=1&lt;/text>
&lt;text x="138" y="184" class="lbl">MFU ~5 %&lt;/text>
&lt;circle cx="185" cy="120" r="7" class="pt"/>
&lt;text x="193" y="116" class="lbl">decode bs=32&lt;/text>
&lt;text x="193" y="129" class="lbl">MFU ~20 %&lt;/text>
&lt;circle cx="300" cy="65" r="7" class="pt"/>
&lt;text x="308" y="61" class="lbl">prefill bs=128&lt;/text>
&lt;text x="308" y="74" class="lbl">MFU ~38 %&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Decode with batch size 1 falls in the memory-bound zone to the left of the ridge point. Raising the batch (continuous batching) moves the point to the right and up, closer to the roofline.&lt;/p>
&lt;hr>
&lt;h2 id="4--tco-sensitivity-to-occupancy">4 · TCO sensitivity to occupancy&lt;/h2>
&lt;p>Assumptions for the example: a generic node with &lt;strong>4×H100 SXM 80 GB&lt;/strong>, total node cost ~44 €/hour (5-year depreciation of ~220,000 €, energy ~4×700 W at ~0.12 €/kWh, plus rack/colocation infrastructure; see &lt;a href="https://blog.lo0.es/en/posts/gpu-hour-to-cost-per-token-on-prem-vs-cloud/">coste-por-token-y-por-request&lt;/a> for the detail of the identity). Reference peak throughput with Llama 70B FP8 plus continuous batching: ~3,500 tok/s aggregate.&lt;/p>
$$
\text{CPM}(\rho) = \frac{44\ \text{€/h}}{3500 \times \rho \times 3600 / 10^6}
= \frac{44 \times 10^6}{3500 \times \rho \times 3600}
= \frac{3.49}{\rho}\ \text{€/1M tok}
$$
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Occupancy \( \rho \)&lt;/th>
&lt;th>Effective throughput (tok/s)&lt;/th>
&lt;th>On-prem CPM&lt;/th>
&lt;th>On-demand cloud CPM (~2.2 €/GPU-h)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>20 %&lt;/td>
&lt;td>700&lt;/td>
&lt;td>&lt;strong>~17.5 €&lt;/strong>&lt;/td>
&lt;td>~6.3 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>40 %&lt;/td>
&lt;td>1,400&lt;/td>
&lt;td>&lt;strong>~8.7 €&lt;/strong>&lt;/td>
&lt;td>~6.3 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>60 %&lt;/td>
&lt;td>2,100&lt;/td>
&lt;td>&lt;strong>~5.8 €&lt;/strong>&lt;/td>
&lt;td>~6.3 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>70 %&lt;/td>
&lt;td>2,450&lt;/td>
&lt;td>&lt;strong>~5.0 €&lt;/strong>&lt;/td>
&lt;td>~6.3 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>85 %&lt;/td>
&lt;td>2,975&lt;/td>
&lt;td>&lt;strong>~4.1 €&lt;/strong>&lt;/td>
&lt;td>~6.3 €&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>On-prem / cloud crossover point&lt;/strong>: with this example hardware, the on-prem cost advantage over a comparable European cloud starts at \( \rho \approx 55\text{-}60\,\% \). Below that, idle makes on-prem dearer than renting. The cloud column is flat because cloud bills per hour used, not per the throughput you extract from it.&lt;/p>
&lt;blockquote>
&lt;p>Example figures with generic hardware. The real numbers depend on the depreciation price, the local cost of energy, the model and the precision. The structure of the curve, CPM inversely proportional to \( \rho \), is universal.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="5--idle-metrics-where-occupancy-is-lost">5 · Idle metrics: where occupancy is lost&lt;/h2>
&lt;p>Before applying levers, you need to know which kind of idle dominates. Three categories:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Type of idle&lt;/th>
&lt;th>Symptom in metrics&lt;/th>
&lt;th>Usual cause&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Scheduling idle&lt;/strong>&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> low, &lt;code>SM_ACTIVE&lt;/code> &amp;lt; 0.05&lt;/td>
&lt;td>No requests in the queue; GPU waiting for work&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Batching idle&lt;/strong>&lt;/td>
&lt;td>&lt;code>SM_ACTIVE&lt;/code> high, &lt;code>PIPE_TENSOR_ACTIVE&lt;/code> low, &lt;code>DRAM_ACTIVE&lt;/code> low&lt;/td>
&lt;td>Batch too small; prefill stall between requests&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Memory-bound idle&lt;/strong>&lt;/td>
&lt;td>&lt;code>DRAM_ACTIVE&lt;/code> high, &lt;code>PIPE_TENSOR_ACTIVE&lt;/code> low&lt;/td>
&lt;td>Normal decode; HBM is the bottleneck; insufficient batch size&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Scheduling idle is the most expensive and the most directly attacked with bin-packing. Batching idle is attacked with continuous batching. Memory-bound idle in decode does not disappear entirely (it is the physics of the transformer), but it is attenuated with a larger batch size.&lt;/p>
&lt;p>The inference engine metrics that complement the diagnosis from the service side are documented in &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">observabilidad-gpu-dcgm-llm&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/anatomy-dcgm-vllm-metrics-analogies-documented-anomalies/">anatomia-metricas-dcgm-vllm-anomalias&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="6--levers-for-raising-occupancy">6 · Levers for raising occupancy&lt;/h2>
&lt;h3 id="table-of-lever--effect--when-it-applies">Table of lever × effect × when it applies&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Lever&lt;/th>
&lt;th>Effect on \( \rho \)&lt;/th>
&lt;th>When it applies&lt;/th>
&lt;th>Operational complexity&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Continuous batching&lt;/strong>&lt;/td>
&lt;td>High: removes idle between requests; raises decode MFU from ~5 % to ~20 %&lt;/td>
&lt;td>Always in inference; enabled by default in vLLM&lt;/td>
&lt;td>Low (engine parameter)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Scheduler bin-packing&lt;/strong> (kube-scheduler / Kueue)&lt;/td>
&lt;td>High: concentrates loads on fewer nodes; frees whole nodes for shutdown or rebalancing&lt;/td>
&lt;td>Clusters with load that varies over the day&lt;/td>
&lt;td>Medium (scheduler policy)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MIG&lt;/strong> (Multi-Instance GPU)&lt;/td>
&lt;td>Medium: fills GPUs with light loads that previously lived alone on a whole GPU&lt;/td>
&lt;td>Heterogeneous loads: embeddings + reranker + guardrail + large model&lt;/td>
&lt;td>High (hot repartitioning not available)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Time-slicing&lt;/strong>&lt;/td>
&lt;td>Low to medium: raises occupancy in dev/bursts; no isolation&lt;/td>
&lt;td>Consumer GPUs (RTX 5090/4090); low-risk multi-tenant dev&lt;/td>
&lt;td>Low&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MPS&lt;/strong>&lt;/td>
&lt;td>Medium: concurrent execution of multiple small processes; reduces context-switch overhead&lt;/td>
&lt;td>Many small concurrent kernels on a datacenter GPU; trust between loads&lt;/td>
&lt;td>Medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Chunked prefill&lt;/strong>&lt;/td>
&lt;td>Medium: interleaves prefill and decode; reduces the TTFT spike and raises throughput&lt;/td>
&lt;td>Loads with a mix of short and long prompts&lt;/td>
&lt;td>Low (vLLM flag)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Quantisation&lt;/strong> (FP16→FP8→INT4)&lt;/td>
&lt;td>Indirect: raises peak throughput, which lowers CPM for the same \( \rho \)&lt;/td>
&lt;td>Models with quantised kernel support (native Hopper FP8)&lt;/td>
&lt;td>Medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Autoscaling&lt;/strong> (KEDA)&lt;/td>
&lt;td>Keeps \( \rho \) high by scaling replicas according to the queue&lt;/td>
&lt;td>Variable and predictable load; cluster with spare capacity&lt;/td>
&lt;td>Medium&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The post &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">compartir-gpu-time-slicing-mps-mig&lt;/a> details the three sharing mechanisms (time-slicing, MPS, MIG) with VRAM budgets.&lt;/p>
&lt;h3 id="bin-packing-with-kueue">Bin-packing with Kueue&lt;/h3>
&lt;p>Kueue (sigs.k8s.io/kueue) is the Kubernetes-native queue manager for GPU jobs. Its model of &lt;strong>cohorts&lt;/strong> and &lt;strong>nominal quotas&lt;/strong> allows active bin-packing: jobs accumulate in a queue and are launched only when there is a node that can take them whole, instead of fragmenting the load across partially occupied nodes.&lt;/p>
&lt;p>&lt;code>BestFit&lt;/code> packing in the &lt;code>ClusterQueue&lt;/code> is configured with:&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">kueue.x-k8s.io/v1beta1&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">ClusterQueue&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">gpu-prod&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">spec&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">preemption&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">reclaimWithinCohort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Any&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">withinClusterQueue&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LowerPriority&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">resourceGroups&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">coveredResources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;nvidia.com/gpu&amp;#34;&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">flavors&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">h100-sxm&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">resources&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="s2">&amp;#34;nvidia.com/gpu&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nominalQuota&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">16&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The priority-based preemption policy ensures that production jobs displace experimentation ones when there is scarcity, keeping occupancy on the production nodes high.&lt;/p>
&lt;h3 id="mig-as-a-bin-packing-lever-inside-the-gpu">MIG as a bin-packing lever inside the GPU&lt;/h3>
&lt;p>MIG makes it possible to fill an H100 with light loads that would otherwise live alone on a whole GPU. A profile of &lt;code>3×2g.20gb + 1×1g.10gb&lt;/code> on an H100 can host simultaneously a 7B FP8 model (~14 GB of weights), two embedding services and an INT4 guardrail, all with hardware isolation. Without MIG, each of those loads would take a whole GPU with an individual \( \rho \) below 10 %.&lt;/p>
&lt;p>The profiles available on the H100 80 GB (SXM5) according to NVIDIA&amp;rsquo;s MIG User Guide:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>MIG 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 SMs&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 SMs&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 SMs&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 SMs&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 SMs&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 SMs&lt;/td>
&lt;td>80 GB&lt;/td>
&lt;td>1 (whole GPU)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>MIG is not available on consumer GPUs (RTX 5090, RTX 4090). Time-slicing is the only sharing option on that hardware.&lt;/p>
&lt;hr>
&lt;h2 id="7--continuous-batching-the-effect-on-mfu">7 · Continuous batching: the effect on MFU&lt;/h2>
&lt;p>Continuous batching (also called iteration-level scheduling or in-flight batching) is the mechanism with the greatest impact on occupancy in inference. The idea: instead of waiting for a complete batch to finish before launching the next, the engine evaluates the pipeline after each token and replaces completed sequences with new requests from the waiting queue.&lt;/p>
&lt;p>Quantified effect in vLLM:&lt;/p>
&lt;ul>
&lt;li>Without batching (bs=1, static): decode MFU ~3–8 %; GPU idle between requests.&lt;/li>
&lt;li>With continuous batching (dynamic bs 16–64): decode MFU ~15–30 %; the GPU almost never waits.&lt;/li>
&lt;li>On pure prefill loads with a large batch (bs=128+): prefill MFU 25–45 %; it approaches the compute roofline.&lt;/li>
&lt;/ul>
&lt;p>The vLLM parameter &lt;code>--max-num-seqs&lt;/code> controls the maximum number of sequences in the concurrent batch. Raising it increases occupancy until HBM becomes the bottleneck (watch for &lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code> &amp;gt; 90 % sustained).&lt;/p>
&lt;hr>
&lt;h2 id="8--hardware-and-scale-what-applies-to-what">8 · Hardware and scale: what applies to what&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Hardware&lt;/th>
&lt;th>MIG&lt;/th>
&lt;th>MPS&lt;/th>
&lt;th>Time-slicing&lt;/th>
&lt;th>Continuous batching&lt;/th>
&lt;th>Note&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>H100 SXM / H200&lt;/td>
&lt;td>Yes (7 inst.)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>On-prem datacenter reference&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A100 SXM/PCIe&lt;/td>
&lt;td>Yes (7 inst.)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Previous generation; HBM2e&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>L40S / L40&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Ada Lovelace; no MIG; good price/VRAM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RTX 5090&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes (limited)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Consumer; no MIG; does not scale in production&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RTX 4090&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes (limited)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Consumer; 24 GB VRAM; no MIG&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The RTX 5090 and RTX 4090 illustrate the case of &lt;strong>hardware that does not scale&lt;/strong> for multi-tenant with isolation: they do not support MIG, VRAM is scarce for models above 7B with a wide KV-cache, and TDP (600 W / 450 W) is high relative to throughput. For production inference at scale, the 4×H100 SXM is the reference node of this series.&lt;/p>
&lt;hr>
&lt;h2 id="9--finops-diagnostic-flow-from-high-cpm-to-the-cause">9 · FinOps diagnostic flow: from high CPM to the cause&lt;/h2>
&lt;div class="diagram" style="max-width:720px;margin:1rem auto;">
&lt;svg viewBox="0 0 720 380" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="FinOps diagnostic flow from high CPM to the right lever">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.dec{fill:none;stroke:currentColor;stroke-width:1.3}.tl{font:600 11px sans-serif;fill:currentColor}.ts{font:10px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.2;marker-end:url(#fm)}&lt;/style>
&lt;defs>&lt;marker id="fm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="270" y="10" width="180" height="42" rx="6"/>
&lt;text x="360" y="28" text-anchor="middle" class="tl">High CPM detected&lt;/text>
&lt;text x="360" y="44" text-anchor="middle" class="ts">OpenCost + LiteLLM&lt;/text>
&lt;path class="ar" d="M360,52 L360,80"/>
&lt;polygon class="dec" points="360,80 440,110 360,140 280,110"/>
&lt;text x="360" y="105" text-anchor="middle" class="tl">POWER_USAGE&lt;/text>
&lt;text x="360" y="120" text-anchor="middle" class="ts">low?&lt;/text>
&lt;path class="ar" d="M440,110 L530,110"/>
&lt;text x="537" y="114" class="ts">Yes → scheduling idle&lt;/text>
&lt;rect class="bx" x="530" y="128" width="170" height="34" rx="5"/>
&lt;text x="615" y="142" text-anchor="middle" class="ts">Bin-packing / Kueue&lt;/text>
&lt;text x="615" y="155" text-anchor="middle" class="ts">Autoscaling down&lt;/text>
&lt;path class="ar" d="M360,140 L360,168"/>
&lt;text x="270" y="114" class="ts">No&lt;/text>
&lt;polygon class="dec" points="360,168 450,198 360,228 270,198"/>
&lt;text x="360" y="193" text-anchor="middle" class="tl">PIPE_TENSOR_ACTIVE&lt;/text>
&lt;text x="360" y="208" text-anchor="middle" class="ts">low (&amp;lt;0.10)?&lt;/text>
&lt;path class="ar" d="M450,198 L530,198"/>
&lt;text x="537" y="202" class="ts">Yes → batching idle&lt;/text>
&lt;rect class="bx" x="530" y="216" width="170" height="34" rx="5"/>
&lt;text x="615" y="230" text-anchor="middle" class="ts">Continuous batching&lt;/text>
&lt;text x="615" y="243" text-anchor="middle" class="ts">Raise --max-num-seqs&lt;/text>
&lt;path class="ar" d="M360,228 L360,256"/>
&lt;text x="270" y="202" class="ts">No&lt;/text>
&lt;polygon class="dec" points="360,256 450,286 360,316 270,286"/>
&lt;text x="360" y="280" text-anchor="middle" class="tl">DRAM_ACTIVE high&lt;/text>
&lt;text x="360" y="296" text-anchor="middle" class="ts">and batch already large?&lt;/text>
&lt;path class="ar" d="M450,286 L530,286"/>
&lt;text x="537" y="290" class="ts">Yes → memory-bound&lt;/text>
&lt;rect class="bx" x="530" y="304" width="170" height="34" rx="5"/>
&lt;text x="615" y="318" text-anchor="middle" class="ts">FP8/INT4 quantisation&lt;/text>
&lt;text x="615" y="331" text-anchor="middle" class="ts">Smaller model / TP&lt;/text>
&lt;path class="ar" d="M360,316 L360,344"/>
&lt;text x="270" y="290" class="ts">No&lt;/text>
&lt;rect class="bx" x="270" y="344" width="180" height="30" rx="5"/>
&lt;text x="360" y="364" text-anchor="middle" class="ts">MIG / MPS / multi-model bin-pack&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="10--worked-example-impact-of-continuous-batching-on-cpm">10 · Worked example: impact of continuous batching on CPM&lt;/h2>
&lt;p>Starting point: Llama 70B FP8 on 4×H100 SXM, a load of 8 requests/s with 512 output tokens on average, without continuous batching (static bs = 8):&lt;/p>
&lt;ul>
&lt;li>Observed throughput: ~900 tok/s (decode dominant).&lt;/li>
&lt;li>\( \rho_{\text{efectiva}} \approx 900 / 3500 \approx 0.26 \).&lt;/li>
&lt;li>CPM: \( 44 / (900 \times 3600 / 10^6) \approx 13.6\ \text{€} \).&lt;/li>
&lt;/ul>
&lt;p>Same load, enabling continuous batching with &lt;code>--max-num-seqs 64&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>Observed throughput: ~2,300 tok/s (the dynamic batch fills the gaps).&lt;/li>
&lt;li>\( \rho_{\text{efectiva}} \approx 2300 / 3500 \approx 0.66 \).&lt;/li>
&lt;li>CPM: \( 44 / (2300 \times 3600 / 10^6) \approx 5.3\ \text{€} \).&lt;/li>
&lt;/ul>
&lt;p>CPM reduction: from 13.6 € to 5.3 €, &lt;strong>–61 %&lt;/strong>, with no hardware change and without touching the model.&lt;/p>
&lt;p>The vLLM parameters relevant for raising effective throughput:&lt;/p>
&lt;pre tabindex="0">&lt;code>--max-num-seqs 64 # maximum concurrent batch
--max-num-batched-tokens 16384 # total tokens per iteration
--enable-chunked-prefill # interleaves prefill and decode
&lt;/code>&lt;/pre>&lt;hr>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-hour-to-cost-per-token-on-prem-vs-cloud/">coste-por-token-y-por-request&lt;/a> — the identity CPM = €/GPU-hour ÷ throughput with data from LiteLLM and OpenCost.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">observabilidad-gpu-dcgm-llm&lt;/a> — the twelve DCGM metrics and five vLLM ones that make up the cockpit.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/anatomy-dcgm-vllm-metrics-analogies-documented-anomalies/">anatomia-metricas-dcgm-vllm-anomalias&lt;/a> — a deep dive into the DCGM metrics with anomalies documented in production.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">compartir-gpu-time-slicing-mps-mig&lt;/a> — time-slicing, MPS and MIG with worked VRAM budgets.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity-planning-inferencia-llm-on-premise&lt;/a> — sizing the cluster from peak throughput and target occupancy.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/">opencost-cost-allocation-kubernetes&lt;/a> — how OpenCost calculates the \( C_{\text{GPU}} \) that goes into the CPM identity.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/cloud-gpu-on-demand-reserved-spot-neoclouds-2026-prices/">Cloud GPU: a comparison of prices, commitment and sovereign neoclouds&lt;/a> — the alternative €/GPU-hour when on-premise occupancy does not justify the CAPEX: spot and reserved prices from European neoclouds.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/full-tco-on-premise-gpu-cluster-capex-gpu-hour-break-even/">TCO of an on-premise GPU cluster: depreciation, energy and infrastructure&lt;/a> — where the \( C_{\text{GPU}} \) of the identity comes from when the iron is your own: CAPEX, depreciation, energy and operations.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>NVIDIA — &lt;em>DCGM Field Identifiers reference (v3.1)&lt;/em>, lista completa de &lt;code>DCGM_FI_*&lt;/code> con field IDs y definiciones. &lt;a href="https://docs.nvidia.com/datacenter/dcgm/3.1/dcgm-api/dcgm-api-field-ids.html">https://docs.nvidia.com/datacenter/dcgm/3.1/dcgm-api/dcgm-api-field-ids.html&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>GPU Profiling Metrics (Run:ai / DCGM)&lt;/em>, definiciones de &lt;code>DCGM_FI_PROF_SM_ACTIVE&lt;/code> (1002), &lt;code>SM_OCCUPANCY&lt;/code> (1003), &lt;code>PIPE_TENSOR_ACTIVE&lt;/code> (1004). &lt;a href="https://run-ai-docs.nvidia.com/self-hosted/platform-management/monitor-performance/gpu-profiling-metrics">https://run-ai-docs.nvidia.com/self-hosted/platform-management/monitor-performance/gpu-profiling-metrics&lt;/a>&lt;/li>
&lt;li>Chowdhery et al. — &lt;em>PaLM: Scaling Language Modeling with Pathways&lt;/em> (MFU definition, sección 4). arXiv 2204.02311. &lt;a href="https://arxiv.org/abs/2204.02311">https://arxiv.org/abs/2204.02311&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>Multi-Instance GPU (MIG) User Guide&lt;/em> (perfiles H100 SXM5, particionado, aislamiento). &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> (catálogo completo H100 80 GB). &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>Time-Slicing GPUs in Kubernetes&lt;/em> (GPU Operator 24.9.0). &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/24.9.0/gpu-sharing.html">https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/24.9.0/gpu-sharing.html&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>Multi-Process Service (MPS) Overview&lt;/em> (kernel concurrency, SM allocation). &lt;a href="https://docs.nvidia.com/deploy/mps/latest/index.html">https://docs.nvidia.com/deploy/mps/latest/index.html&lt;/a>&lt;/li>
&lt;li>Kueue — &lt;em>Overview&lt;/em> (sigs.k8s.io/kueue, bin-packing, cohorts, quotas). &lt;a href="https://kueue.sigs.k8s.io/docs/overview/">https://kueue.sigs.k8s.io/docs/overview/&lt;/a>&lt;/li>
&lt;li>vLLM Blog — &lt;em>vLLM v0.6.0: 2.7x Throughput Improvement and 5x Latency Reduction&lt;/em> (continuous batching, chunked prefill). &lt;a href="https://blog.vllm.ai/2024/09/05/perf-update.html">https://blog.vllm.ai/2024/09/05/perf-update.html&lt;/a>&lt;/li>
&lt;li>GMI Cloud — &lt;em>NVIDIA H100 GPU Pricing: 2026 Rent vs. Buy Cost Analysis&lt;/em>. &lt;a href="https://www.gmicloud.ai/en/blog/nvidia-h100-gpu-pricing-2026-rent-vs-buy-cost-analysis">https://www.gmicloud.ai/en/blog/nvidia-h100-gpu-pricing-2026-rent-vs-buy-cost-analysis&lt;/a>&lt;/li>
&lt;li>Saxena et al. — &lt;em>LLM Inference Unveiled: Survey and Roofline Model Insights&lt;/em>. arXiv 2402.16363. &lt;a href="https://arxiv.org/abs/2402.16363">https://arxiv.org/abs/2402.16363&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>