<?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 — Blog Técnico</title><link>https://blog.lo0.es/tags/vram/</link><description>Recent content in Vram on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 11 Jun 2026 02:40:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/tags/vram/index.xml" rel="self" type="application/rss+xml"/><item><title>Servir varios modelos en una sola GPU: co-residencia, model-swapping y sleep mode</title><link>https://blog.lo0.es/posts/servir-varios-modelos-una-gpu-swap-sleep/</link><pubDate>Thu, 11 Jun 2026 02:40:00 +0000</pubDate><guid>https://blog.lo0.es/posts/servir-varios-modelos-una-gpu-swap-sleep/</guid><description>&lt;blockquote>
&lt;p>Esta es la &lt;strong>segunda pieza&lt;/strong> de una serie operativa sobre exprimir un cluster LLM on-premise genérico de &lt;strong>4×H100 SXM 80 GB con NVLink&lt;/strong>. La hermana &lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">Compartir una GPU: time-slicing, MPS y MIG&lt;/a> parte la GPU para que varios procesos la usen a la vez; esta parte el problema complementario: tienes &lt;strong>más modelos que VRAM&lt;/strong> y necesitas que convivan en el tiempo, no solo en el espacio. La tercera, &lt;a href="https://blog.lo0.es/posts/rag-en-cpu-plano-datos-generacion/">El plano de datos del RAG en CPU&lt;/a>, saca de la GPU todo lo que no necesita estar; y el asistente soberano end-to-end (cuarta entrega, en preparación) ensambla todo esto detrás de LibreChat y LiteLLM. Aquí asumimos que &lt;strong>la VRAM es el recurso escaso&lt;/strong> y que tener todo cargado a la vez no es opción.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Tienes un LLM de agentes que sirve casi todo el tráfico, uno o dos &lt;strong>rerankers&lt;/strong> para el RAG, un &lt;strong>modelo alterno&lt;/strong> (otra familia, otro idioma, un fine-tune) y, de vez en cuando, un &lt;strong>modelo grande&lt;/strong> para tareas difíciles. Sumados no caben en los 80 GB de una H100. Hay tres maneras de que convivan, y la clave para no confundirlas es entender que &lt;strong>son tres estados distintos de dónde viven los pesos&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Co-residencia&lt;/strong> — varios modelos cargados &lt;strong>a la vez&lt;/strong> en la HBM, si la suma de sus presupuestos de VRAM cabe en la física. Es lo ideal cuando entran: cero latencia de cambio. La trampa: cada motor debe &lt;strong>autolimitar su VRAM&lt;/strong>; si dos motores creen que tienen los 80 GB enteros, el segundo en pedir memoria revienta con OOM. En una GPU compartida por &lt;strong>time-slicing&lt;/strong>, el reparto de tiempo &lt;strong>no protege la memoria&lt;/strong> —los procesos siguen compitiendo por la misma HBM— y este problema empeora (lo vemos en la &lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">pieza hermana&lt;/a>).&lt;/li>
&lt;li>&lt;strong>Model-swapping&lt;/strong> (con &lt;strong>llama-swap&lt;/strong>) — solo &lt;strong>uno&lt;/strong> (o muy pocos) residente a la vez. El proxy mira el campo &lt;code>model&lt;/code> de la request y, si toca otro, &lt;strong>descarga&lt;/strong> el actual y &lt;strong>carga&lt;/strong> el pedido. Como solo hay uno dentro, cada modelo puede usar &lt;strong>casi toda&lt;/strong> la GPU. El coste es un cold start completo: segundos desde NVMe local, minutos desde almacenamiento de red. Mezcla motores: expone endpoints OpenAI-compatible incluido &lt;code>/rerank&lt;/code> con &lt;code>llama-server&lt;/code> sobre GGUF.&lt;/li>
&lt;li>&lt;strong>Sleep mode&lt;/strong> de vLLM (&lt;code>--enable-sleep-mode&lt;/code>, endpoints &lt;code>/sleep&lt;/code> y &lt;code>/wake_up&lt;/code>) — el modelo &lt;strong>no se descarga, se duerme&lt;/strong>. Los pesos se aparcan en &lt;strong>RAM del host&lt;/strong> (nivel 1) o se descartan dejando el proceso vivo (nivel 2). El wake es &lt;strong>18–200× más rápido&lt;/strong> que un cold start porque el proceso sigue vivo y conserva el allocator de CUDA, los CUDA graphs y los kernels JIT compilados —lo único que se reconstruye es el KV-cache, que &lt;strong>se descarta al dormir&lt;/strong>. Requiere RAM suficiente para los pesos dormidos.&lt;/li>
&lt;/ul>
&lt;p>El árbol de decisión es corto: &lt;strong>¿caben todos?&lt;/strong> → co-residencia. &lt;strong>¿No caben, el cambio es poco frecuente y mezclas motores?&lt;/strong> → llama-swap. &lt;strong>¿Todo es vLLM, hay RAM de sobra y la latencia de wake importa?&lt;/strong> → sleep mode. &lt;strong>¿Carga sostenida de todos a la vez?&lt;/strong> → no es swap, es &lt;strong>más GPUs o réplicas&lt;/strong> (ver &lt;a href="https://blog.lo0.es/posts/tp-replicas-una-grande-vs-n-pequenas/">una grande vs N pequeñas&lt;/a>). Este post pone los números a cada uno sobre una H100 de 80 GB.&lt;/p>
&lt;h2 id="la-analogía-un-solo-escenario-un-solo-foco">La analogía: un solo escenario, un solo foco&lt;/h2>
&lt;p>Imagina un teatro con &lt;strong>un único escenario&lt;/strong> y &lt;strong>un solo foco&lt;/strong> —la GPU. Tienes más actores (modelos) en la compañía que los que caben iluminados a la vez. Hay tres formas de montar la función:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Co-residencia: varios actores pequeños en escena.&lt;/strong> Si el reparto de esta escena son tres actores menudos —el LLM principal, un reranker, otro reranker—, caben todos bajo el foco al mismo tiempo. El cambio de réplica entre ellos es instantáneo: ya están en escena. Pero el escenario tiene un tamaño fijo; si metes a un cuarto actor corpulento (el modelo grande), no caben y alguien se cae del borde —eso es el &lt;strong>OOM&lt;/strong>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Model-swapping: el actor se va a casa.&lt;/strong> Cuando un actor termina su papel, &lt;strong>se va a su casa&lt;/strong> (el disco). Si la escena siguiente lo requiere otra vez, tiene que &lt;strong>volver desde casa&lt;/strong>: vestirse, maquillarse, llegar al teatro. Eso son minutos. A cambio, mientras está en escena tiene &lt;strong>todo el escenario para él&lt;/strong>. Es el modelo de llama-swap: máxima VRAM por actor, pero la reentrada cuesta un viaje completo.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Sleep mode: el actor espera entre bambalinas.&lt;/strong> En vez de irse a casa, el actor sale de la luz del foco pero &lt;strong>se queda entre bastidores&lt;/strong> (la RAM del host), ya vestido y maquillado. Cuando le toca, &lt;strong>entra en dos pasos&lt;/strong>. No hay viaje, no hay vestuario: solo cruzar la cortina. Ese es el wake de vLLM: los pesos no vuelven del disco sino de la RAM, y todo lo que se tarda en &amp;ldquo;preparar al actor&amp;rdquo; —el allocator, los CUDA graphs, los kernels— ya está hecho porque nunca se fue del edificio.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>La moraleja operativa es la misma que en el teatro: &lt;strong>el coste de un actor no es solo su talento, es cuánto tarda en entrar a escena cuando lo necesitas.&lt;/strong> Co-residencia paga el sitio permanente; swap paga el viaje; sleep paga la RAM de tenerlo esperando cerca. El resto del post es elegir cuál, con presupuesto de VRAM y reloj en mano.&lt;/p>
&lt;h2 id="los-tres-estados-dónde-viven-los-pesos">Los tres estados: dónde viven los pesos&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="Tres estados de servir varios modelos: co-residencia, swap por request y sleep-wake">
&lt;text x="410" y="24" text-anchor="middle" font-size="15" font-weight="700" fill="currentColor">Dónde viven los pesos en cada estrategia&lt;/text>
&lt;!-- Co-residencia -->
&lt;text x="140" y="56" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">Co-residencia&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">LLM agentes&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">headroom KV + activaciones&lt;/text>
&lt;text x="140" y="206" text-anchor="middle" font-size="10.5" fill="currentColor">cambio: 0 ms · suma &amp;lt; 80 GB o 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">un modelo residente&lt;/text>
&lt;text x="410" y="151" text-anchor="middle" font-size="10" fill="currentColor" opacity="0.8">usa casi toda la VRAM&lt;/text>
&lt;text x="410" y="206" text-anchor="middle" font-size="10.5" fill="currentColor">los demás en DISCO (NVMe / red)&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">cambio: descargar + cargar (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">modelo despierto&lt;/text>
&lt;text x="680" y="140" text-anchor="middle" font-size="10" fill="currentColor" opacity="0.8">proceso vivo&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 (se descarta al dormir)&lt;/text>
&lt;text x="680" y="206" text-anchor="middle" font-size="10.5" fill="currentColor">los demás dormidos en RAM del host&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 (cientos de ms–s)&lt;/text>
&lt;text x="410" y="290" text-anchor="middle" font-size="12.5" font-weight="700" fill="currentColor">El eje es dónde están los pesos de los modelos inactivos&lt;/text>
&lt;text x="410" y="312" text-anchor="middle" font-size="11" fill="currentColor">co-residencia: en HBM (cuestan sitio) · swap: en disco (cuestan un viaje) · sleep: en RAM (cuestan RAM)&lt;/text>
&lt;text x="410" y="332" text-anchor="middle" font-size="11" fill="currentColor" opacity="0.8">y eso decide la latencia de tenerlos disponibles otra vez&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="co-residencia-el-presupuesto-de-vram">Co-residencia: el presupuesto de VRAM&lt;/h2>
&lt;p>Co-residir es la opción por defecto &lt;strong>cuando caben&lt;/strong>: cero latencia de cambio porque todos los modelos están ya en la HBM. La pregunta entera es &lt;strong>¿caben?&lt;/strong>, y se responde con un presupuesto de VRAM. La VRAM de un motor de inferencia se reparte, a grandes rasgos, en tres partidas:&lt;/p>
&lt;p>$$\text{VRAM}&lt;em>{\text{modelo}} = \underbrace{P \cdot b}&lt;/em>{\text{pesos}} + \underbrace{\text{KV}}&lt;em>{\text{caché}} + \underbrace{A}&lt;/em>{\text{activaciones + overhead}}$$&lt;/p>
&lt;p>donde $P$ es el número de parámetros, $b$ los bytes por parámetro según la cuantización, KV el presupuesto de KV-cache (que escala con concurrencia y longitud de contexto) y $A$ el overhead de activaciones, buffers de CUDA y fragmentación. La regla dura de la co-residencia es:&lt;/p>
&lt;p>$$\sum_i \text{VRAM}&lt;em>{\text{modelo } i} + \text{margen} ;&amp;lt;; \text{VRAM}&lt;/em>{\text{física}}$$&lt;/p>
&lt;p>Si la suma se pasa, &lt;strong>no hay reparto que valga&lt;/strong>: el primer motor que pida memoria por encima del hueco libre muere con OOM. Y aquí está el matiz crítico que conecta con la pieza hermana: &lt;strong>partir la GPU por tiempo no parte la memoria&lt;/strong>. En time-slicing, dos procesos se turnan el cómputo pero &lt;strong>comparten la HBM entera sin aislamiento&lt;/strong>; si ambos asumen que tienen los 80 GB, chocan. Solo &lt;strong>MIG&lt;/strong> da particiones de memoria con frontera real (lo vemos en &lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">Compartir una GPU&lt;/a>). Para co-residir bien, cada motor debe &lt;strong>autolimitarse&lt;/strong>: en vLLM, &lt;code>--gpu-memory-utilization 0.45&lt;/code> le dice &amp;ldquo;no uses más del 45 % de la GPU&amp;rdquo;; en &lt;code>llama-server&lt;/code>, controlas las capas en GPU y el tamaño de KV. Si no pones esos límites, vLLM reclama por defecto el &lt;strong>90 %&lt;/strong> de la GPU para sí solo, y no queda sitio para nadie más.&lt;/p>
&lt;h3 id="ejemplo-numérico-qué-cabe-en-80-gb">Ejemplo numérico: qué cabe en 80 GB&lt;/h3>
&lt;p>Pongamos el escenario realista. Un &lt;strong>LLM de agentes de 32B en FP8&lt;/strong> ($b = 1$ byte/parám):&lt;/p>
&lt;p>$$P \cdot b = 32 \times 10^9 \cdot 1 \text{ B} = 32 \text{ GB de pesos}$$&lt;/p>
&lt;p>A eso hay que sumarle KV-cache. Para servir con concurrencia decente y contextos de agente (que son largos: historial, herramientas, documentos), un presupuesto de KV de &lt;strong>~12 GB&lt;/strong> es razonable, más unos &lt;strong>~2 GB&lt;/strong> de activaciones y overhead. Total del LLM principal: &lt;strong>~46 GB&lt;/strong>. Quedan &lt;strong>~34 GB&lt;/strong> de los 80. Veamos qué entra ahí:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Servicio&lt;/th>
&lt;th>Tamaño (FP8/INT8)&lt;/th>
&lt;th>KV + overhead&lt;/th>
&lt;th>VRAM total&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>LLM agentes 32B&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>Modelo alterno 8B&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>Suma&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>Margen libre&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>Con esto, &lt;strong>co-residen perfectamente&lt;/strong>: el LLM de 32B, los dos rerankers (que son ligerísimos —cientos de MB a 1 GB cada uno) y hasta un alterno de 8B, dejando 20 GB de colchón. Los rerankers son el caso de manual de co-residencia: tan pequeños que &lt;strong>siempre&lt;/strong> caben junto al LLM principal, y rotarlos sería absurdo. El cambio de unos a otros es instantáneo porque están todos cargados.&lt;/p>
&lt;p>¿Cuándo se rompe? Cuando entra el &lt;strong>modelo grande ocasional&lt;/strong>, un 70B en FP8:&lt;/p>
&lt;p>$$70 \times 10^9 \cdot 1 \text{ B} = 70 \text{ GB de pesos}$$&lt;/p>
&lt;p>Solo los pesos del 70B ya consumen 70 de los 80 GB. No hay forma de co-residirlo con el 32B —ni de lejos—. &lt;strong>Ahí termina la co-residencia y empieza la rotación&lt;/strong>: el 70B solo puede entrar si &lt;strong>echamos&lt;/strong> al 32B de la GPU. Esa es la frontera exacta: co-residir mientras la suma de presupuestos quepa con margen; rotar en cuanto un modelo necesite, él solo, más VRAM de la que sobra.&lt;/p>
&lt;h2 id="model-swapping-con-llama-swap-el-actor-se-va-a-casa">Model-swapping con llama-swap: el actor se va a casa&lt;/h2>
&lt;p>Cuando no caben a la vez, la primera respuesta es &lt;strong>rotar&lt;/strong>: tener un solo modelo residente y cambiarlo bajo demanda. La herramienta canónica para esto en el mundo on-premise es &lt;a href="https://github.com/mostlygeek/llama-swap">&lt;strong>llama-swap&lt;/strong>&lt;/a>: un proxy en Go que se pone &lt;strong>delante&lt;/strong> de tus servidores de inferencia (llama.cpp, vLLM, TabbyAPI…) y los arranca y para según haga falta.&lt;/p>
&lt;p>El mecanismo es elegante por lo simple. Cada request OpenAI-compatible lleva un campo &lt;code>model&lt;/code>. llama-swap &lt;strong>lee ese campo&lt;/strong>, mira qué servidor upstream tiene configurado para ese modelo, y:&lt;/p>
&lt;ol>
&lt;li>Si el modelo pedido &lt;strong>ya está cargado&lt;/strong>, enruta la request directamente.&lt;/li>
&lt;li>Si está cargado &lt;strong>otro&lt;/strong>, lo &lt;strong>para&lt;/strong> (libera su VRAM) y &lt;strong>arranca&lt;/strong> el correcto.&lt;/li>
&lt;li>Cuando el nuevo servidor responde &amp;ldquo;listo&amp;rdquo;, reenvía la request.&lt;/li>
&lt;/ol>
&lt;p>El coste de un swap es exactamente un &lt;strong>cold start completo&lt;/strong> del modelo nuevo: descargar el actual (rápido, liberar memoria) más cargar el nuevo (lento, mover los pesos del disco a la HBM). Ese segundo término es el que duele, y su magnitud la decide &lt;strong>dónde están los pesos&lt;/strong>: desde &lt;strong>NVMe local&lt;/strong> son segundos; desde &lt;strong>almacenamiento de red&lt;/strong> (Ceph RGW, NFS) pueden ser minutos. Todo el análisis del camino de carga —y por qué el loader por defecto lo hace lento— está en &lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">Del disco a la HBM&lt;/a>; aquí basta la consecuencia: &lt;strong>un swap solo es viable si es infrecuente&lt;/strong>, porque cada cambio paga ese peaje entero.&lt;/p>
&lt;p>La ventaja a cambio: como solo hay &lt;strong>uno residente&lt;/strong>, ese modelo puede usar &lt;strong>casi toda&lt;/strong> la GPU. El 70B que no co-reside con nadie sí cabe holgado si es el único dentro. Y llama-swap &lt;strong>mezcla motores&lt;/strong>: puedes tener configurado un vLLM para el LLM grande, un &lt;code>llama-server&lt;/code> con GGUF para el alterno, y dos &lt;code>llama-server&lt;/code> para los rerankers —que exponen &lt;code>/rerank&lt;/code>, &lt;code>/v1/rerank&lt;/code> y &lt;code>/v1/reranking&lt;/code> de forma nativa—. Esa heterogeneidad (GGUF + rerank + vLLM bajo un solo endpoint OpenAI) es justo lo que vLLM solo no te da, y la razón principal para elegir llama-swap.&lt;/p>
&lt;h3 id="configmap-de-ejemplo-dos-rerankers-que-rotan-en-el-mismo-puerto">ConfigMap de ejemplo: dos rerankers que rotan en el mismo puerto&lt;/h3>
&lt;p>Un caso concreto y útil: tienes &lt;strong>dos rerankers&lt;/strong> —uno multilingüe y uno especializado en código— que &lt;strong>no necesitas a la vez&lt;/strong> y prefieres no tener ambos residentes. Con llama-swap rotan en el mismo endpoint, disparados por el campo &lt;code>model&lt;/code>. El &lt;code>ConfigMap&lt;/code> (montado como &lt;code>config.yaml&lt;/code> del proxy en un despliegue de Kubernetes) sería:&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"> # Tiempo que un modelo sigue cargado tras la última request
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # antes de que llama-swap lo descargue para liberar 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"> # Reranker multilingüe (GGUF, vía 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: tras 300 s ocioso, se descarga y libera la GPU
&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"> # Reranker de código (GGUF, vía 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>Una request a &lt;code>/v1/rerank&lt;/code> con &lt;code>&amp;quot;model&amp;quot;: &amp;quot;reranker-multilang&amp;quot;&lt;/code> arranca ese servidor; la siguiente con &lt;code>&amp;quot;model&amp;quot;: &amp;quot;reranker-code&amp;quot;&lt;/code> &lt;strong>para el multilingüe y arranca el de código&lt;/strong> en el mismo &lt;code>${PORT}&lt;/code> que llama-swap gestiona. Como ambos son pequeños, el swap entre ellos es de &lt;strong>uno o dos segundos&lt;/strong> —los GGUF cuantizados pesan pocos cientos de MB y vienen de NVMe local—. El &lt;code>ttl&lt;/code> controla cuánto sigue cargado un modelo tras su última petición: subirlo evita swaps si las peticiones llegan a ráfagas; bajarlo libera la GPU antes para otros usos.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Matiz honesto:&lt;/strong> este patrón de &lt;em>dos rerankers que rotan&lt;/em> tiene sentido cuando &lt;strong>no&lt;/strong> te caben junto al resto, o cuando quieres reservar la VRAM para otra cosa. Si te caben (y un par de rerankers de 0,5 GB caben casi siempre, como vimos arriba), &lt;strong>co-residirlos es estrictamente mejor&lt;/strong>: cero latencia de swap. llama-swap brilla cuando rotas modelos &lt;strong>grandes&lt;/strong> o cuando mezclas motores que no coexisten bien, no para hacer malabares con modelos diminutos que cabrían juntos.&lt;/p>
&lt;/blockquote>
&lt;h2 id="vllm-sleep-mode-el-actor-entre-bambalinas">vLLM sleep mode: el actor entre bambalinas&lt;/h2>
&lt;p>El swap tiene un problema: cada cambio paga el cold start &lt;strong>entero&lt;/strong>, y un cold start no es solo mover pesos. Como vimos en &lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">Del disco a la HBM&lt;/a>, arrancar un motor de inferencia incluye inicializar el proceso de Python y el contexto CUDA, montar el allocator de memoria, &lt;strong>capturar los CUDA graphs&lt;/strong> y &lt;strong>compilar los kernels JIT&lt;/strong> (DeepGEMM, FlashInfer, TorchInductor). Mover los pesos es solo &lt;strong>una de cinco&lt;/strong> partidas, y a menudo no la mayor.&lt;/p>
&lt;p>El &lt;strong>sleep mode&lt;/strong> de vLLM (&lt;code>--enable-sleep-mode&lt;/code>) ataca exactamente eso: en vez de &lt;strong>matar&lt;/strong> el proceso cuando un modelo deja de usarse, lo &lt;strong>duerme&lt;/strong> dejando el proceso vivo. Hay dos niveles, y la diferencia es dónde van los pesos:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Nivel 1:&lt;/strong> descarga los pesos a la &lt;strong>RAM del host&lt;/strong> (CPU) y descarta el KV-cache. El proceso sigue vivo. El wake copia los pesos &lt;strong>de RAM a VRAM&lt;/strong> —no del disco—. Wake típico: &lt;strong>~0,1–0,8 s&lt;/strong> para modelos pequeños, &lt;strong>~3–6 s&lt;/strong> para grandes. Necesita RAM suficiente para los pesos dormidos (del orden de los GB que pesa el modelo).&lt;/li>
&lt;li>&lt;strong>Nivel 2:&lt;/strong> &lt;strong>descarta los pesos&lt;/strong> por completo, conservando solo buffers pequeños (tensores de rope scaling, etc.). El wake &lt;strong>sí recarga los pesos del disco&lt;/strong>, pero &lt;strong>todo lo demás —proceso, allocator, CUDA graphs, kernels JIT— ya está hecho&lt;/strong>. Wake típico: &lt;strong>~0,8–2,6 s&lt;/strong> para modelos pequeños. RAM casi nula (megabytes).&lt;/li>
&lt;/ul>
&lt;p>La clave que explica los números: en ambos niveles, mantener el proceso vivo &lt;strong>preserva la infraestructura cara&lt;/strong>. Por eso el benchmark del &lt;a href="https://blog.vllm.ai/2025/10/26/sleep-mode.html">blog de vLLM (oct 2025)&lt;/a> reporta que un wake es &lt;strong>18–200×&lt;/strong> más rápido que un full reload —y, lo más contraintuitivo, &lt;strong>el nivel 2 sigue siendo 23–45× más rápido&lt;/strong> que un cold start a pesar de recargar los pesos del mismo disco, porque se salta las otras cuatro partidas—. En sus medidas sobre A100, un ciclo completo de 5 cambios de modelo pasa de &lt;strong>357 s sin sleep&lt;/strong> (≈48 s por cambio) a &lt;strong>112 s con nivel 1&lt;/strong> (wake de 0,26 s / 0,82 s) o &lt;strong>125 s con nivel 2&lt;/strong> (0,85 s / 2,58 s).&lt;/p>
&lt;p>El &lt;strong>KV-cache se descarta siempre al dormir.&lt;/strong> No es un detalle menor: significa que la &lt;strong>primera respuesta tras el wake reconstruye el KV&lt;/strong> desde cero —paga un prefill completo—. Por eso el wake no es &amp;ldquo;gratis del todo&amp;rdquo;: el modelo está disponible en sub-segundo, pero la primera petición es un poco más lenta hasta que el KV se repuebla. Un warm-up de una petición tras el wake oculta ese coste.&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"># Arrancar vLLM con sleep mode (endpoints de admin, solo red de confianza)&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;modelo&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"># Dormir (nivel 1: pesos a RAM del host)&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"># Despertar&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>Aviso de seguridad&lt;/strong> (del propio blog de vLLM): los endpoints &lt;code>/sleep&lt;/code>, &lt;code>/wake_up&lt;/code>, &lt;code>/collective_rpc&lt;/code> y &lt;code>/reset_prefix_cache&lt;/code> requieren &lt;code>VLLM_SERVER_DEV_MODE=1&lt;/code> y &lt;strong>solo deben exponerse en redes de confianza&lt;/strong> —pueden tumbar el servicio—. Son para orquestación interna (un controlador que duerme y despierta modelos según la cola), no para el plano público.&lt;/p>
&lt;/blockquote>
&lt;h2 id="las-matemáticas-de-la-latencia-por-qué-el-wake-gana">Las matemáticas de la latencia: por qué el wake gana&lt;/h2>
&lt;p>Pongamos números al &amp;ldquo;el wake llega de RAM, no de disco&amp;rdquo;. El coste de &lt;strong>tener un modelo disponible&lt;/strong> es, en cada estrategia, el tiempo de mover sus pesos desde donde estén hasta la HBM (más, en swap, los otros cuatro costes del cold start). Tomemos los &lt;strong>34 GB de pesos&lt;/strong> del LLM de 32B en FP8 y comparemos los tres caminos.&lt;/p>
&lt;p>&lt;strong>Cold start desde NVMe (swap).&lt;/strong> Un NVMe Gen4/Gen5 razonable da del orden de &lt;strong>~5 GB/s efectivos por flujo&lt;/strong> con el loader por defecto (el suelo teórico del disco es mayor, pero el deserializado monohilo no lo satura —ver &lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">Del disco a la HBM&lt;/a>). Para 34 GB:&lt;/p>
&lt;p>$$t_{\text{NVMe→HBM}} \approx \frac{34 \text{ GB}}{5 \text{ GB/s}} \approx 6,8 \text{ s solo de mover bytes}$$&lt;/p>
&lt;p>Y eso es &lt;strong>antes&lt;/strong> de sumar la captura de CUDA graphs y la compilación de kernels, que añaden varios segundos más. El cold start real de un 32B ronda los &lt;strong>15–40 s&lt;/strong> según loader y storage. Desde &lt;strong>red&lt;/strong> (Ceph RGW), multiplica.&lt;/p>
&lt;p>&lt;strong>Wake nivel 1 desde RAM.&lt;/strong> Los pesos no vienen del disco sino de la &lt;strong>RAM del host&lt;/strong>, y viajan por &lt;strong>PCIe Gen5 x16&lt;/strong>, cuyo ancho de banda práctico host→GPU es de &lt;strong>~50–64 GB/s&lt;/strong>. Para los mismos 34 GB:&lt;/p>
&lt;p>$$t_{\text{RAM→HBM}} \approx \frac{34 \text{ GB}}{55 \text{ GB/s}} \approx 0,62 \text{ s}$$&lt;/p>
&lt;p>Y &lt;strong>no hay nada más que pagar&lt;/strong>: el allocator, los graphs y los kernels ya están. El wake real de un modelo de este tamaño cae en el rango &lt;strong>sub-segundo a pocos segundos&lt;/strong> que reporta vLLM. La aceleración frente al cold start de NVMe es del orden de:&lt;/p>
&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>
&lt;p>consistente con el &lt;strong>18–200×&lt;/strong> del blog (que en sus medidas incluye modelos más pequeños, donde el peso relativo de los CUDA graphs preservados es aún mayor y el factor sube).&lt;/p>
&lt;p>&lt;strong>Por qué la diferencia de ancho de banda lo explica casi todo.&lt;/strong> El salto clave no es 5 vs 55 GB/s (un ~11× en el transporte). Es que el cold start paga &lt;strong>además&lt;/strong> la reconstrucción de infraestructura, que el wake &lt;strong>se ahorra entera&lt;/strong>. La tabla:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Camino&lt;/th>
&lt;th>Origen pesos&lt;/th>
&lt;th>Ancho de banda&lt;/th>
&lt;th>34 GB solo bytes&lt;/th>
&lt;th>+ CUDA graphs / JIT&lt;/th>
&lt;th>Total realista&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cold start (swap) NVMe&lt;/td>
&lt;td>disco&lt;/td>
&lt;td>~5 GB/s&lt;/td>
&lt;td>~6,8 s&lt;/td>
&lt;td>sí (varios s)&lt;/td>
&lt;td>&lt;strong>15–40 s&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cold start (swap) red&lt;/td>
&lt;td>red&lt;/td>
&lt;td>~1–2 GB/s&lt;/td>
&lt;td>17–34 s&lt;/td>
&lt;td>sí&lt;/td>
&lt;td>&lt;strong>30 s – min&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Wake nivel 1&lt;/td>
&lt;td>RAM host&lt;/td>
&lt;td>~50–64 GB/s&lt;/td>
&lt;td>~0,6 s&lt;/td>
&lt;td>&lt;strong>no&lt;/strong> (preservado)&lt;/td>
&lt;td>&lt;strong>0,6–3 s&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Wake nivel 2&lt;/td>
&lt;td>disco&lt;/td>
&lt;td>~5 GB/s&lt;/td>
&lt;td>~6,8 s&lt;/td>
&lt;td>&lt;strong>no&lt;/strong> (preservado)&lt;/td>
&lt;td>&lt;strong>7–10 s&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Fíjate en la fila del &lt;strong>nivel 2&lt;/strong>: recarga los pesos del &lt;strong>mismo&lt;/strong> disco que el swap (~6,8 s de bytes), pero como &lt;strong>no&lt;/strong> reconstruye graphs ni kernels, su total (~7–10 s) sigue batiendo al cold start completo (15–40 s). Es la prueba de que &lt;strong>mover bytes es solo una parte del coste&lt;/strong>, y la que el sleep mode explota.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Supuestos, honestamente:&lt;/strong> los anchos de banda son orientativos. NVMe &amp;ldquo;5 GB/s efectivos&amp;rdquo; asume el loader por defecto; con un streamer concurrente sube. PCIe &amp;ldquo;55 GB/s&amp;rdquo; asume Gen5 x16 con buffer &lt;em>pinned&lt;/em> y NUMA-local; si el buffer cae en el socket equivocado, baja. Y el rango &amp;ldquo;15–40 s&amp;rdquo; de cold start depende del modelo, la cuantización y si los ficheros están o no en page cache (la trampa del &lt;em>&amp;ldquo;la segunda vez cargó rápido&amp;rdquo;&lt;/em>). Los números son para razonar órdenes de magnitud, &lt;strong>no para dimensionar sin medir en tu hardware&lt;/strong>.&lt;/p>
&lt;/blockquote>
&lt;h2 id="el-árbol-de-decisión">El árbol de decisión&lt;/h2>
&lt;p>Las tres estrategias no compiten: &lt;strong>cada una gana en un régimen distinto&lt;/strong>. El árbol, en orden:&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="Árbol de decisión: co-residencia, llama-swap, sleep mode o más 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">¿Caben TODOS a la vez&lt;/text>
&lt;text x="380" y="56" text-anchor="middle" font-size="11" fill="currentColor">(suma de presupuestos &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">sí&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-residencia&lt;/text>
&lt;text x="140" y="130" text-anchor="middle" font-size="10" fill="currentColor">cada motor autolimita su 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">¿Carga sostenida de&lt;/text>
&lt;text x="380" y="127" text-anchor="middle" font-size="11" fill="currentColor">todos a la vez?&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">sí&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">Más GPUs / réplicas&lt;/text>
&lt;text x="655" y="130" text-anchor="middle" font-size="10" fill="currentColor">no es swap: es capacidad&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">¿Todo es vLLM y hay&lt;/text>
&lt;text x="380" y="207" text-anchor="middle" font-size="11" fill="currentColor">RAM de sobra?&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 (mezclas motores)&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">rota por campo `model`&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">sí (latencia importa)&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">nivel 1: pesos a 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">Y se combinan: co-residir los ligeros + rotar/dormir los grandes&lt;/text>
&lt;text x="380" y="378" text-anchor="middle" font-size="10.5" fill="currentColor" opacity="0.85">rerankers siempre co-residen · el 70B ocasional duerme o entra por swap&lt;/text>
&lt;text x="380" y="398" text-anchor="middle" font-size="10.5" fill="currentColor" opacity="0.85">si todo se usa a tope a la vez, ninguna estrategia te salva: necesitas más silicio&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>El nodo que más se ignora es el de la derecha arriba: &lt;strong>&amp;quot;¿carga sostenida de todos a la vez?&amp;quot;&lt;/strong>. Si tus cuatro modelos reciben tráfico &lt;strong>constante y simultáneo&lt;/strong>, ni el swap ni el sleep ayudan —ambos asumen que los modelos se &lt;strong>turnan&lt;/strong> en el tiempo—. Rotar bajo carga sostenida solo añade latencia de cambio sin resolver el problema de fondo: &lt;strong>no hay suficiente cómputo&lt;/strong>. La respuesta entonces es &lt;strong>escalar horizontal&lt;/strong> (más réplicas) o repartir en &lt;strong>más GPUs&lt;/strong>, una decisión de capacidad que se analiza en &lt;a href="https://blog.lo0.es/posts/tp-replicas-una-grande-vs-n-pequenas/">Una grande vs N pequeñas&lt;/a>. El swap y el sleep son herramientas para cargas &lt;strong>temporalmente desbalanceadas&lt;/strong>: muchos modelos, pero rara vez activos a la vez.&lt;/p>
&lt;h2 id="aplicado-al-cluster-genérico-4h100">Aplicado al cluster genérico 4×H100&lt;/h2>
&lt;p>Bajemos esto a las &lt;strong>4 H100 SXM de 80 GB con NVLink&lt;/strong>. La estrategia ganadora no es elegir &lt;strong>una&lt;/strong> de las tres, sino &lt;strong>repartir los modelos por GPU según su patrón de uso&lt;/strong> y aplicar a cada GPU la estrategia que le toca. Un reparto razonable:&lt;/p>
&lt;p>&lt;strong>H100 #0 — El caballo de batalla (co-residencia).&lt;/strong> El LLM de agentes de 32B (servicio principal, tráfico constante) co-reside con &lt;strong>los dos rerankers&lt;/strong> y, si caben, el alterno de 8B. Es la GPU que nunca rota: todo lo que vive aquí se usa de continuo y entra holgado en 80 GB (los ~60 GB del ejemplo de arriba). Cero latencia de cambio entre el LLM y sus rerankers, que es justo lo que el RAG necesita —un reranker rápido es inútil si hay que esperar un swap cada vez—.&lt;/p>
&lt;p>&lt;strong>H100 #1 — Servicios ligeros con MIG.&lt;/strong> Si tienes muchos servicios pequeños heterogéneos —un modelo de embeddings, un clasificador, un guardrail, un STT/TTS—, partir esta GPU con &lt;strong>MIG&lt;/strong> en instancias aisladas (cada una con su trozo de HBM &lt;strong>con frontera real&lt;/strong>) da co-residencia &lt;strong>con aislamiento de memoria&lt;/strong>, evitando que un servicio que infla su KV tumbe a los demás. El detalle de cuándo MIG bate a time-slicing está en &lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">Compartir una GPU&lt;/a>; la regla aquí: co-residir servicios ligeros en una GPU tiene sentido cuando &lt;strong>caben&lt;/strong> y &lt;strong>conviene aislarlos&lt;/strong>, y MIG es la herramienta para lo segundo.&lt;/p>
&lt;p>&lt;strong>H100 #2 — El modelo grande ocasional (sleep mode o swap).&lt;/strong> El 70B que solo se invoca para tareas difíciles &lt;strong>no merece una GPU dedicada despierta&lt;/strong> —estaría ociosa la mayor parte del tiempo, quemando 700 W para nada—. Dos opciones:&lt;/p>
&lt;ul>
&lt;li>Si esta GPU &lt;strong>también sirve&lt;/strong> un modelo mediano de forma habitual y solo eventualmente necesitas el 70B, usa &lt;strong>sleep mode&lt;/strong>: duerme el mediano (nivel 1, pesos a RAM), despierta el 70B&amp;hellip; salvo que el 70B no quepa ni solo despierto junto al mediano dormido en VRAM —recuerda que dormir nivel 1 &lt;strong>libera la VRAM&lt;/strong>, los pesos van a RAM, así que sí cabe—. El wake del mediano al volver es sub-segundo.&lt;/li>
&lt;li>Si el 70B viene en GGUF o mezclas motores, &lt;strong>llama-swap&lt;/strong> rota entre el mediano y el 70B por el campo &lt;code>model&lt;/code>. Cada invocación del 70B paga su cold start (segundos desde NVMe local), aceptable si es &lt;strong>ocasional&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>H100 #3 — Réplica / desbordamiento.&lt;/strong> La cuarta GPU absorbe picos: una &lt;strong>réplica&lt;/strong> del LLM principal para cuando la cola del #0 crece, o capacidad de reserva. Aquí &lt;strong>no hay swap&lt;/strong>: es capacidad pura, la respuesta al nodo &amp;ldquo;carga sostenida&amp;rdquo; del árbol.&lt;/p>
&lt;p>El principio transversal: &lt;strong>co-residir lo que se usa junto y de continuo (LLM + rerankers); aislar con MIG lo ligero y heterogéneo; dormir o rotar lo grande y ocasional; replicar lo que satura.&lt;/strong> Las cuatro GPUs no hacen lo mismo —cada una ejecuta la estrategia que su patrón de carga pide—. Y el NVLink entre ellas importa para otra cosa (tensor parallel del 70B si no cupiera ni en una; ver &lt;a href="https://blog.lo0.es/posts/tp-replicas-una-grande-vs-n-pequenas/">Una grande vs N pequeñas&lt;/a>), pero para el problema de este post —muchos modelos, una GPU— la palanca es &lt;strong>cuándo cada modelo necesita estar despierto&lt;/strong>.&lt;/p>
&lt;h2 id="trampas-y-cosas-que-no-son-lo-que-parecen">Trampas y cosas que no son lo que parecen&lt;/h2>
&lt;p>&lt;strong>&amp;ldquo;Co-residir es siempre mejor si caben.&amp;rdquo;&lt;/strong> Casi, pero ojo al &lt;strong>KV-cache&lt;/strong>: co-residir dos modelos significa partir el presupuesto de KV entre ambos. Si el LLM principal necesita un KV grande para concurrencia alta y contextos largos, meterle un compañero le recorta ese KV y baja su throughput. A veces es mejor darle la GPU entera al principal y rotar el secundario. Co-residir no es gratis: &lt;strong>el inquilino le quita sitio a la caché del que importa&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;El sleep mode es como el swap pero más rápido.&amp;rdquo;&lt;/strong> No exactamente. El swap &lt;strong>libera el proceso&lt;/strong>; puedes tener N modelos configurados y solo pagas RAM/disco por el residente. El sleep mode mantiene &lt;strong>un proceso vivo por modelo dormido&lt;/strong> —cada vLLM dormido sigue ocupando su slot de proceso, su RAM (nivel 1) y su hueco de gestión—. Sleep escala bien a &lt;strong>unos pocos&lt;/strong> modelos que rotan; para &lt;strong>muchos&lt;/strong> (10+), nivel 2 (RAM mínima) o directamente swap encajan mejor. No metas 15 modelos en sleep nivel 1 esperando que la RAM aguante.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;El wake es instantáneo, no pierdo nada.&amp;rdquo;&lt;/strong> El wake del &lt;strong>modelo&lt;/strong> es sub-segundo, pero el &lt;strong>KV-cache se descartó al dormir&lt;/strong>. La primera petición tras el wake paga un &lt;strong>prefill completo&lt;/strong> para repoblar el KV —más lenta de lo normal—. Si tu SLA es estricto en la primera respuesta tras un período ocioso, mete un warm-up automático tras el wake. El &lt;a href="https://blog.lo0.es/posts/prefix-cache-hit-rate-engineering/">prefix caching&lt;/a> ayuda a que ese reprefill sea más barato si hay prefijos estables.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;llama-swap con &lt;code>ttl&lt;/code> bajo me ahorra VRAM gratis.&amp;rdquo;&lt;/strong> Te ahorra VRAM &lt;strong>mientras nadie use ese modelo&lt;/strong>, pero cada vez que vuelve paga el cold start. Un &lt;code>ttl&lt;/code> agresivo en un modelo con tráfico a ráfagas convierte cada ráfaga en una espera de carga. El &lt;code>ttl&lt;/code> correcto depende del patrón temporal de las peticiones, no de cuánta VRAM quieres liberar. Mídelo.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Time-slicing me deja co-residir más modelos.&amp;rdquo;&lt;/strong> Falso y peligroso. El time-slicing reparte &lt;strong>tiempo de cómputo&lt;/strong>, no memoria —todos los procesos siguen compitiendo por la &lt;strong>misma HBM sin aislamiento&lt;/strong>—. Co-residir vía time-slicing no te da más VRAM efectiva; te da más procesos peleándose por la misma, y un OOM cuando la suma se pasa. Para &lt;strong>partición de memoria&lt;/strong> real, MIG. El detalle, en la &lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">pieza hermana&lt;/a>.&lt;/p>
&lt;h2 id="conclusión">Conclusión&lt;/h2>
&lt;p>Tener más modelos que VRAM no es un problema de hardware insuficiente: es un problema de &lt;strong>gestión temporal de un recurso escaso&lt;/strong>. La intuición de &amp;ldquo;necesito una GPU por modelo&amp;rdquo; es cara y casi siempre falsa, porque los modelos rara vez se usan &lt;strong>todos a la vez&lt;/strong>. Las tres estrategias son tres respuestas a la misma pregunta —&lt;strong>dónde viven los pesos de los modelos que ahora no estás usando&lt;/strong>—: en la HBM si caben (co-residencia, cero latencia pero cuestan sitio), en el disco si el cambio es raro (swap, máxima VRAM por modelo pero un viaje de vuelta de segundos a minutos), o en la RAM si la latencia de cambio importa (sleep mode, wake de sub-segundo a costa de tener la RAM ocupada). El sleep mode es la incorporación más interesante de 2025 porque rompe el falso dilema &amp;ldquo;todo cargado vs recargar cada vez&amp;rdquo;: al mantener el proceso vivo y preservar el allocator, los CUDA graphs y los kernels, convierte un cold start de 30–100 s en un wake de menos de un segundo —y lo hace incluso cuando recarga los pesos del mismo disco (nivel 2), porque mover bytes nunca fue el coste entero—. En el cluster de cuatro H100, la jugada no es elegir una estrategia sino &lt;strong>repartir&lt;/strong>: co-residir lo que va junto, aislar lo ligero con MIG, dormir o rotar lo grande y ocasional, replicar lo que satura. La GPU es el escenario con un solo foco; el arte está en saber qué actor entra a escena, cuál se va a casa y cuál espera entre bambalinas.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/acelerar-cold-start-carga-modelos-tensorizer/">Acelerar el cold start de modelos: de minutos a segundos&lt;/a> — cómo bajar el coste del cold start que paga cada swap.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">Compartir una GPU: time-slicing, MPS y MIG&lt;/a> — la pieza hermana: cómo repartir &lt;strong>una&lt;/strong> GPU entre varios procesos en el espacio (no en el tiempo). Clave aquí para entender por qué el time-slicing no protege la memoria y por qué MIG es lo que da co-residencia con aislamiento real de HBM.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">Del disco a la HBM: el cold start y la carga del modelo&lt;/a> — el camino de carga que el swap paga entero en cada cambio y que el sleep mode esquiva; por qué mover bytes es solo una de cinco partidas del arranque.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving: fundamentos&lt;/a> — la alternativa cuando los &amp;ldquo;varios modelos&amp;rdquo; son adaptadores del &lt;strong>mismo&lt;/strong> base: en vez de rotar modelos enteros, sirves muchos LoRA sobre un base co-residente sin coste de swap.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/prefix-cache-hit-rate-engineering/">Ingeniería del prefix cache hit rate&lt;/a> — el KV-cache se descarta al dormir; un buen hit rate de prefijos estables abarata el reprefill de la primera petición tras el wake.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/tp-replicas-una-grande-vs-n-pequenas/">Una grande vs N pequeñas: tensor parallel y réplicas&lt;/a> — la rama &amp;ldquo;carga sostenida de todos a la vez&amp;rdquo; del árbol: cuando rotar no basta y hay que repartir en más GPUs o réplicas.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/entornos-mixtos-nvidia-intel-servidores-nucs/">Entornos mixtos NVIDIA / Intel: servidores y NUCs&lt;/a> — dónde colocar los servicios ligeros (rerankers, embeddings) que no necesitan una H100: a veces co-residir no es en la GPU grande sino en hardware más modesto.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&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> (proxy de model-swapping OpenAI/Anthropic-compatible): &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> (especificaciones HBM3, 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>Compartir una GPU entre varias cargas: time-slicing, MPS y MIG</title><link>https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/</link><pubDate>Thu, 11 Jun 2026 02:00:00 +0000</pubDate><guid>https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/</guid><description>&lt;blockquote>
&lt;p>Este post &lt;strong>abre una serie operativa&lt;/strong> sobre cómo exprimir un cluster LLM on-premise genérico de 4×H100 SXM. Las piezas hermanas: &lt;a href="https://blog.lo0.es/posts/servir-varios-modelos-una-gpu-swap-sleep/">Servir varios modelos en una GPU: swap y sleep&lt;/a> (qué hacer cuando los modelos no caben a la vez y hay que turnarlos en memoria), &lt;a href="https://blog.lo0.es/posts/rag-en-cpu-plano-datos-generacion/">RAG en CPU: separar plano de datos y generación&lt;/a> (mover el retrieval fuera de la GPU para liberarla) y Asistente soberano end-to-end con LibreChat, LiteLLM y RAG —el ensamblaje final, cuarta entrega en preparación—. Aquí empezamos por lo más básico: tienes una GPU y quieres meterle varias cargas encima. ¿Cómo se reparte?&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Tienes una GPU —o pocas— y varias cargas que quieren correr encima: un modelo de chat, un servicio de embeddings, un reranker, una cola de jobs de dev. La GPU está infrautilizada si solo corre una cosa, pero meter varias a lo bruto provoca contención, OOM o caídas en cascada. Hay &lt;strong>tres mecanismos&lt;/strong> y reparten cosas distintas. El &lt;strong>time-slicing&lt;/strong> (réplicas del NVIDIA k8s device-plugin) multiplexa en el &lt;strong>tiempo&lt;/strong>: anuncia que la GPU es &amp;ldquo;N GPUs&amp;rdquo; y los procesos se turnan el cómputo, pero &lt;strong>comparten la VRAM física completa&lt;/strong>, sin aislamiento de memoria ni de fallos ni QoS. Su trampa es un OOM que no aparece en el scheduler de Kubernetes sino en tiempo de ejecución, cuando la suma de asignaciones de VRAM supera la memoria real. El &lt;strong>MPS&lt;/strong> (Multi-Process Service) multiplexa en el &lt;strong>espacio&lt;/strong>: reparte los SMs entre procesos que ejecutan kernels &lt;strong>concurrentemente&lt;/strong>, reduce el overhead de context-switch y permite limitar SMs y memoria por proceso —sube el throughput cuando hay muchos kernels pequeños, pero el aislamiento de fallos sigue siendo débil. El &lt;strong>MIG&lt;/strong> (Multi-Instance GPU) particiona en &lt;strong>hardware&lt;/strong>: corta la GPU Hopper en hasta siete instancias con SMs, L2, memoria y ancho de banda &lt;strong>dedicados&lt;/strong>, con aislamiento real de memoria, fallo y rendimiento; solo en datacenter (A100/H100/H200/B200), nunca en una RTX 5090. La regla: aislamiento real / multi-tenant / compliance → MIG (si es Hopper); muchos kernels pequeños concurrentes y confianza entre cargas → MPS; dev, ráfagas, GPU de consumo o sin necesidad de aislar → time-slicing. Este post lo trabaja con números: el presupuesto de VRAM de cuatro vLLM sobre una H100 anunciada como cuatro réplicas, y qué cabe en una instancia MIG de 10 GB.&lt;/p>
&lt;h2 id="la-analogía-un-fogón-compartido-una-cocina-con-varios-cocineros-varias-cocinas">La analogía: un fogón compartido, una cocina con varios cocineros, varias cocinas&lt;/h2>
&lt;p>Imagina que tienes &lt;strong>un único fogón profesional&lt;/strong> y tres pedidos que cocinar a la vez. Hay tres maneras de organizarlo, y son exactamente los tres mecanismos.&lt;/p>
&lt;p>&lt;strong>Time-slicing es un solo fogón por turnos, sin despensa propia.&lt;/strong> Cada cocinero entra, cocina su plato, sale, entra el siguiente. El reparto es &lt;strong>temporal&lt;/strong>: nadie cocina a la vez, se turnan. El problema no es el fogón —se va turnando bien— sino la &lt;strong>despensa común&lt;/strong>: los ingredientes están en una sola alacena compartida y nadie tiene la suya. Si los tres cocineros reservan más harina de la que hay en total, no es que esperen turno: es que &lt;strong>no hay harina&lt;/strong>. El servicio se cae para todos. Y si un cocinero deja una sartén ardiendo y provoca un incendio, quema la cocina entera, no su rincón.&lt;/p>
&lt;p>&lt;strong>MPS son varios cocineros coordinados en la misma encimera.&lt;/strong> Ahora sí cocinan &lt;strong>a la vez&lt;/strong>, repartiéndose el espacio de la encimera (los SMs). Un jefe de cocina (el daemon MPS) coordina para que no choquen y para que la encimera no se quede vacía mientras uno espera a que hierva el agua. Puedes asignarle a cada cocinero un porcentaje de la encimera y un límite de despensa. Trabajan más rápido en conjunto porque la encimera no queda ociosa entre tareas pequeñas. Pero &lt;strong>siguen compartiendo la cocina&lt;/strong>: si uno provoca un incendio grave, los demás lo notan.&lt;/p>
&lt;p>&lt;strong>MIG son varias cocinas independientes en el mismo edificio.&lt;/strong> Una pared de hormigón separa cada cocina: su propio fogón, su propia despensa, su propia puerta y su propio cuadro eléctrico. Lo que pasa en la cocina 3 —un incendio, una despensa vacía, un cocinero lento— &lt;strong>no toca&lt;/strong> a la cocina 1. Es el único reparto con aislamiento de verdad. El precio: tienes que decidir de antemano cuántas cocinas y de qué tamaño, las paredes son fijas, y solo los edificios caros (datacenter) vienen preparados para levantarlas.&lt;/p>
&lt;p>El resto del post es, esencialmente, cuándo quieres turnos baratos, cuándo quieres cocineros coordinados y cuándo necesitas paredes de hormigón.&lt;/p>
&lt;h2 id="por-qué-compartir-el-problema-operativo">Por qué compartir: el problema operativo&lt;/h2>
&lt;p>Una H100 SXM 80 GB no se llena con cualquier carga. Un reranker &lt;code>bge-reranker-v2-m3&lt;/code> ocupa unos cientos de MB y satura unos pocos SMs; un servicio de embeddings &lt;code>bge-m3&lt;/code> es igual de pequeño; un modelo guardrail de 1B en INT4 cabe en un par de GB. Dedicar 80 GB de HBM3 y 132 SMs a servir embeddings es usar una prensa hidráulica para clavar una chincheta —el mismo argumento de &lt;a href="https://blog.lo0.es/posts/entornos-mixtos-nvidia-intel-servidores-nucs/">los entornos mixtos&lt;/a>, pero ahora &lt;em>dentro&lt;/em> de la GPU en lugar de moviendo la carga a otro silicio.&lt;/p>
&lt;p>El objetivo de compartir es subir la &lt;strong>utilización útil&lt;/strong> del capital fijo. Pero compartir mal introduce tres patologías:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Contención de cómputo&lt;/strong>: dos cargas pelean por los mismos SMs y ambas van lentas, con jitter de latencia impredecible.&lt;/li>
&lt;li>&lt;strong>Contención de memoria&lt;/strong>: la suma de VRAM solicitada supera la física y algo muere con un &lt;code>CUDA out of memory&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Fallo en cascada&lt;/strong>: una carga que peta (un kernel ilegal, un OOM) puede arrastrar a las vecinas si comparten contexto.&lt;/li>
&lt;/ul>
&lt;p>Los tres mecanismos atacan estas patologías con distinta profundidad. Ninguno las resuelve todas salvo MIG, y MIG cuesta hardware concreto. Veámoslos uno a uno.&lt;/p>
&lt;h2 id="time-slicing-turnos-de-cómputo-despensa-compartida">Time-slicing: turnos de cómputo, despensa compartida&lt;/h2>
&lt;p>El &lt;strong>time-slicing&lt;/strong> es multiplexación &lt;strong>temporal por software&lt;/strong>. En Kubernetes, el &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html">NVIDIA GPU Operator&lt;/a> configura el device-plugin para &lt;strong>anunciar N réplicas&lt;/strong> de cada GPU física. Una H100 declarada con &lt;code>replicas: 4&lt;/code> aparece ante el scheduler como &lt;strong>cuatro recursos&lt;/strong> &lt;code>nvidia.com/gpu&lt;/code>, y Kubernetes puede colocar cuatro pods sobre ella. Internamente, el planificador de la GPU va dando &lt;strong>turnos de cómputo&lt;/strong> a cada proceso: ejecuta un poco del proceso A, cambia al B, al C, al D, vuelve al A. Es el mismo &lt;em>time-sharing&lt;/em> que un sistema operativo hace con la CPU.&lt;/p>
&lt;p>La idea clave, y la que más confusión genera, es esta: &lt;strong>una réplica NO es una fracción de la GPU&lt;/strong>. Es un turno de cómputo. La documentación de NVIDIA es explícita: a diferencia de MIG, &lt;strong>no hay aislamiento de memoria ni de fallos&lt;/strong> entre réplicas. Las cuatro réplicas de la H100 ven los &lt;strong>80 GB completos&lt;/strong> de VRAM, sin partición. No hay 20 GB por réplica. Hay 80 GB para los cuatro, repartidos por orden de llegada de &lt;code>cudaMalloc&lt;/code>.&lt;/p>
&lt;p>Esto tiene tres consecuencias que hay que interiorizar:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>No aísla memoria.&lt;/strong> Si la suma de lo que reservan los cuatro procesos supera 80 GB, el cuarto &lt;code>cudaMalloc&lt;/code> falla con OOM. El scheduler de Kubernetes &lt;strong>no lo ve venir&lt;/strong>: él contó cuatro recursos &lt;code>nvidia.com/gpu&lt;/code> disponibles y colocó cuatro pods felizmente. El OOM aparece en &lt;strong>tiempo de ejecución&lt;/strong>, no en &lt;em>scheduling&lt;/em>. Esta es la trampa número uno del time-slicing.&lt;/li>
&lt;li>&lt;strong>No aísla fallos.&lt;/strong> Un proceso que dispara un error de CUDA irrecuperable puede dejar el contexto de la GPU en un estado que afecta a los vecinos. Comparten el mismo dispositivo sin barreras.&lt;/li>
&lt;li>&lt;strong>No da QoS de cómputo.&lt;/strong> Bajo contención, el reparto de turnos no garantiza una fracción mínima a nadie. La latencia de cada carga sufre &lt;strong>jitter&lt;/strong> proporcional a cuántas réplicas activas peleen por la GPU en ese instante. Una carga sensible a latencia (un chat interactivo) puede ver su TTFT bailar según lo que hagan las vecinas.&lt;/li>
&lt;/ol>
&lt;p>¿Para qué sirve entonces? Para &lt;strong>dev, ráfagas y baja utilización&lt;/strong>. Si tienes cuatro desarrolladores que tocan la GPU esporádicamente, anunciar cuatro réplicas deja que los cuatro tengan acceso sin pelearse casi nunca (rara vez coinciden activos). Para cargas batch tolerantes a jitter. Y, ventaja decisiva, &lt;strong>funciona en GPUs de consumo&lt;/strong>: una RTX 5090 32 GB no soporta MIG, pero sí time-slicing. Es la única forma &amp;ldquo;Kubernetes-native&amp;rdquo; de compartir una 5090 entre varios 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="Las tres estrategias de reparto: temporal, espacial y particionado hardware">
&lt;text x="410" y="24" text-anchor="middle" font-size="15" font-weight="700" fill="currentColor">Cómo reparte cada mecanismo&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">reparto en el TIEMPO&lt;/text>
&lt;rect x="30" y="84" width="220" height="120" fill="none" stroke="currentColor" stroke-width="1.4"/>
&lt;!-- turnos -->
&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">turno 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">turno 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">turno 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">turno D&lt;/text>
&lt;text x="140" y="224" text-anchor="middle" font-size="11" fill="currentColor">VRAM compartida: 80 GB para los 4&lt;/text>
&lt;text x="140" y="240" text-anchor="middle" font-size="11" fill="#ef4444">sin aislamiento · OOM si suma &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">reparto en el ESPACIO (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">kernels concurrentes en SMs distintos&lt;/text>
&lt;text x="410" y="240" text-anchor="middle" font-size="11" fill="#f59e0b">límite SM y mem por proceso · fallo débil&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">partición HARDWARE&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">SMs, L2, VRAM y BW dedicados&lt;/text>
&lt;text x="680" y="240" text-anchor="middle" font-size="11" fill="#22c55e">aislamiento real · paredes de hormigón&lt;/text>
&lt;!-- footer -->
&lt;text x="410" y="290" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">Un fogón por turnos · varios cocineros en la encimera · varias cocinas con su pared&lt;/text>
&lt;text x="410" y="312" text-anchor="middle" font-size="11" fill="currentColor">consumo + datacenter · datacenter (CUDA) · solo datacenter Hopper/Ampere/Blackwell&lt;/text>
&lt;text x="410" y="332" text-anchor="middle" font-size="11" fill="currentColor">software · software (contexto CUDA) · hardware (fusibles físicos)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="el-presupuesto-de-vram-en-time-slicing-el-cálculo-que-evita-el-oom">El presupuesto de VRAM en time-slicing (el cálculo que evita el OOM)&lt;/h3>
&lt;p>Aquí está la matemática que hay que hacer &lt;strong>antes&lt;/strong> de desplegar, porque Kubernetes no la hará por ti. Supongamos una H100 80 GB anunciada como &lt;strong>4 réplicas&lt;/strong> y queremos correr &lt;strong>cuatro instancias de vLLM&lt;/strong> encima, una por réplica.&lt;/p>
&lt;p>vLLM reserva memoria con el parámetro &lt;code>--gpu-memory-utilization&lt;/code>, que es la &lt;strong>fracción de la VRAM física total&lt;/strong> que cada instancia se queda (para pesos del modelo más KV-cache). El detalle que mata: esa fracción se calcula sobre los &lt;strong>80 GB físicos&lt;/strong>, &lt;strong>no&lt;/strong> sobre un supuesto &amp;ldquo;20 GB de mi réplica&amp;rdquo; —porque la réplica no tiene 20 GB, recordemos que no hay partición de memoria. Cada vLLM ve los 80 GB y reserva su fracción de ellos.&lt;/p>
&lt;p>La restricción de no-OOM es entonces que la &lt;strong>suma&lt;/strong> de fracciones sea menor que 1:&lt;/p>
&lt;p>$$\sum_{i=1}^{N} g_i &amp;lt; 1 \quad\Longleftrightarrow\quad \sum_{i=1}^{N} g_i \cdot V_{\text{HBM}} &amp;lt; V_{\text{HBM}}$$&lt;/p>
&lt;p>donde $g_i$ es el &lt;code>--gpu-memory-utilization&lt;/code> de la instancia $i$ y $V_{\text{HBM}} = 80$ GB. Conviene dejar margen (overhead del runtime, fragmentación, contexto CUDA), así que en la práctica se busca que la suma quede holgadamente por debajo de 1, digamos $\le 0.9$.&lt;/p>
&lt;p>&lt;strong>Caso que funciona.&lt;/strong> Cuatro vLLM a $g_i = 0.20$:&lt;/p>
&lt;p>$$\sum_{i=1}^{4} 0.20 = 0.80 \quad\Rightarrow\quad 0.80 \times 80\ \text{GB} = 64\ \text{GB} &amp;lt; 80\ \text{GB} \quad\checkmark$$&lt;/p>
&lt;p>Cada instancia reserva $0.20 \times 80 = 16$ GB. Cuatro instancias suman 64 GB, dejando 16 GB de colchón. No hay OOM. Cada vLLM tiene 16 GB para pesos más KV-cache: suficiente para un modelo de 7B–8B en FP8/INT4 con un KV-cache modesto.&lt;/p>
&lt;p>&lt;strong>Caso que revienta.&lt;/strong> Las mismas cuatro instancias, pero alguien sube $g_i = 0.30$ pensando &amp;ldquo;tengo cuatro réplicas, puedo darle más a cada una&amp;rdquo;:&lt;/p>
&lt;p>$$\sum_{i=1}^{4} 0.30 = 1.20 \quad\Rightarrow\quad 1.20 \times 80\ \text{GB} = 96\ \text{GB} &amp;gt; 80\ \text{GB} \quad\times$$&lt;/p>
&lt;p>Las primeras instancias arrancan y reservan $0.30 \times 80 = 24$ GB cada una. Tres instancias ya van por $72$ GB. La cuarta intenta reservar otros 24 GB, no quedan, y &lt;strong>muere con &lt;code>CUDA out of memory&lt;/code>&lt;/strong>. Y lo peor: Kubernetes la reprogramará sobre la misma GPU (sigue viendo cuatro réplicas), donde volverá a morir, en un &lt;code>CrashLoopBackOff&lt;/code> que no se explica mirando solo el manifiesto del pod.&lt;/p>
&lt;p>La regla operativa es brutalmente simple: &lt;strong>en time-slicing, el presupuesto de VRAM lo gestionas tú a mano, sumando los &lt;code>--gpu-memory-utilization&lt;/code>&lt;/strong>. El número de réplicas controla cuántos pods &lt;em>caben por turnos de cómputo&lt;/em>, pero &lt;strong>no reserva ni un byte de memoria&lt;/strong>. Confundir las dos cosas es el error recurrente.&lt;/p>
&lt;h2 id="mps-cocineros-coordinados-en-la-misma-encimera">MPS: cocineros coordinados en la misma encimera&lt;/h2>
&lt;p>El &lt;strong>Multi-Process Service&lt;/strong> (&lt;a href="https://docs.nvidia.com/deploy/mps/index.html">MPS&lt;/a>) ataca un problema distinto. Por defecto, cuando varios procesos usan la misma GPU sin MPS, cada uno tiene su propio &lt;strong>contexto CUDA&lt;/strong>, y la GPU &lt;strong>alterna&lt;/strong> entre contextos (time-slicing a nivel de driver): no ejecutan kernels a la vez, se turnan, con overhead de cambio de contexto. Si tus kernels son pequeños y no llenan la GPU ellos solos, esto deja SMs ociosos: el proceso A usa el 30 % de los SMs durante su turno y el otro 70 % se desperdicia.&lt;/p>
&lt;p>MPS introduce un &lt;strong>daemon&lt;/strong> que comparte un único contexto CUDA entre procesos, de modo que sus kernels pueden ejecutarse &lt;strong>concurrentemente&lt;/strong> ocupando SMs distintos &lt;strong>a la vez&lt;/strong>. Es reparto &lt;strong>espacial&lt;/strong> de cómputo: en lugar de turnarse la encimera entera, cada cocinero ocupa una parte y trabajan en paralelo. Esto reduce el overhead de context-switch y &lt;strong>sube el throughput cuando hay muchos kernels pequeños concurrentes&lt;/strong> que individualmente no saturan la GPU.&lt;/p>
&lt;p>Y, a diferencia del time-slicing puro, MPS permite &lt;strong>poner límites por proceso&lt;/strong>, lo que da una forma de QoS:&lt;/p>
&lt;ul>
&lt;li>&lt;code>CUDA_MPS_ACTIVE_THREAD_PERCENTAGE&lt;/code> limita el &lt;strong>porcentaje de SMs&lt;/strong> que un cliente MPS puede usar. Por defecto cada cliente recibe $100 / \text{MaxSharedClients}$. Fijarlo a, digamos, 40 % cierra el techo de cómputo de ese proceso (&lt;a href="https://docs.nvidia.com/deploy/mps/appendix-tools-and-interface-reference.html">docs MPS&lt;/a>).&lt;/li>
&lt;li>&lt;code>CUDA_MPS_PINNED_DEVICE_MEM_LIMIT&lt;/code> impone un &lt;strong>tope de memoria&lt;/strong> por cliente (válido desde CUDA 11.5). Esto es lo que el time-slicing &lt;strong>no&lt;/strong> tiene: un límite de VRAM por proceso que el runtime hace cumplir.&lt;/li>
&lt;/ul>
&lt;p>Estos dos límites convierten a MPS en un mecanismo de &lt;strong>provisioning de recursos&lt;/strong> que mitiga el &lt;em>noisy neighbor&lt;/em>: puedes garantizar que un proceso no se coma más del X % de SMs ni más de Y GB. La combinación da una QoS razonable —no perfecta, pero real.&lt;/p>
&lt;p>La limitación que MPS &lt;strong>no&lt;/strong> resuelve: &lt;strong>el aislamiento de fallos es débil&lt;/strong>. Como los clientes comparten el contexto CUDA del daemon, un error fatal en un cliente puede afectar al daemon y, por tanto, a los demás clientes (históricamente, un cliente que muere de forma sucia podía requerir reiniciar el daemon). Es mejor que el time-slicing en este aspecto, pero está lejos del aislamiento por hardware. Por eso MPS encaja cuando hay &lt;strong>confianza entre las cargas&lt;/strong> —procesos de tu propio equipo, no tenants ajenos.&lt;/p>
&lt;p>El caso de uso canónico: &lt;strong>muchas peticiones de inferencia pequeñas y concurrentes&lt;/strong> que individualmente dejan la GPU medio vacía. MPS las solapa y sube el throughput agregado. Servir varios modelos pequeños o varias réplicas ligeras del mismo modelo en una GPU de datacenter, donde confías en todas las cargas, es territorio MPS.&lt;/p>
&lt;h2 id="mig-paredes-de-hormigón">MIG: paredes de hormigón&lt;/h2>
&lt;p>El &lt;strong>Multi-Instance GPU&lt;/strong> (&lt;a href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/">MIG&lt;/a>) es el único de los tres que da &lt;strong>aislamiento de verdad&lt;/strong>, porque corta la GPU &lt;strong>en hardware&lt;/strong>. Disponible en las GPU de datacenter modernas —A100 (Ampere), H100/H200 (Hopper), B200 (Blackwell)— y &lt;strong>nunca&lt;/strong> en las de consumo: una RTX 5090 (Blackwell de consumo) &lt;strong>no soporta MIG&lt;/strong>, igual que las GeForce en general.&lt;/p>
&lt;p>MIG divide la GPU en hasta &lt;strong>siete instancias&lt;/strong> (&lt;em>GPU Instances&lt;/em>), y cada instancia recibe una porción &lt;strong>dedicada&lt;/strong> de:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>SMs&lt;/strong> (compute slices): el cómputo se reparte en 7 &lt;em>slices&lt;/em>, cada uno ~1/7 de los SMs.&lt;/li>
&lt;li>&lt;strong>L2 cache y memoria&lt;/strong>: cada instancia tiene su trozo de HBM y su porción de caché L2.&lt;/li>
&lt;li>&lt;strong>Ancho de banda de memoria&lt;/strong>: dedicado, no compartido.&lt;/li>
&lt;li>&lt;strong>Caminos de datos y motores&lt;/strong>: con barreras de fallo entre instancias.&lt;/li>
&lt;/ul>
&lt;p>El resultado es que una instancia MIG se comporta como &lt;strong>una GPU más pequeña e independiente&lt;/strong>: lo que pase en una —un OOM, un kernel que peta, una carga que satura su cómputo— &lt;strong>no afecta&lt;/strong> a las vecinas. Aislamiento de memoria, de fallo y de rendimiento (QoS), las tres cosas que el time-slicing no da y que MPS solo da a medias.&lt;/p>
&lt;h3 id="los-perfiles-de-la-h100-80gb">Los perfiles de la H100 80GB&lt;/h3>
&lt;p>MIG no permite tamaños arbitrarios: tiene &lt;strong>perfiles fijos&lt;/strong>. En la H100 80GB, el &lt;a href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-mig-profiles.html">catálogo de perfiles&lt;/a> (notación &lt;code>&amp;lt;compute&amp;gt;g.&amp;lt;memoria&amp;gt;gb&lt;/code>) es:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Perfil&lt;/th>
&lt;th>Compute (slices)&lt;/th>
&lt;th>Memoria&lt;/th>
&lt;th>Instancias máx.&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 (GPU entera)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>(Existe además &lt;code>1g.10gb+me&lt;/code>, una variante con &lt;em>media engines&lt;/em> para codificación de vídeo.) La unidad base de memoria en la H100 80GB es de &lt;strong>10 GB por slice&lt;/strong> (80 GB / 8, con un slice reservado), y la de cómputo es 1/7 de los SMs. Los perfiles combinan estas unidades. Fíjate en &lt;code>1g.20gb&lt;/code>: misma fracción de cómputo que &lt;code>1g.10gb&lt;/code> (1/7 de SMs) pero &lt;strong>el doble de memoria&lt;/strong> —útil cuando una carga necesita más VRAM que cómputo.&lt;/p>
&lt;p>Un detalle importante: las particiones MIG no se mezclan libremente. La GPU se divide siguiendo una geometría válida (los perfiles encajan como piezas en una rejilla), y los perfiles &lt;strong>se fijan al configurar&lt;/strong> la GPU; cambiarlos requiere drenar y reparticionar. Son paredes de hormigón: sólidas, pero no se mueven en caliente.&lt;/p>
&lt;h3 id="el-cálculo-71g10gb-frente-a-17g80gb">El cálculo: 7×1g.10gb frente a 1×7g.80gb&lt;/h3>
&lt;p>Comparemos los dos extremos. A la izquierda, &lt;strong>siete instancias &lt;code>1g.10gb&lt;/code>&lt;/strong>: siete GPU aisladas de 10 GB cada una. A la derecha, &lt;strong>una sola &lt;code>7g.80gb&lt;/code>&lt;/strong>: la H100 entera sin particionar.&lt;/p>
&lt;p>La pregunta operativa es &lt;strong>qué cabe en 10 GB&lt;/strong>. El presupuesto de VRAM de una instancia se reparte entre pesos del modelo y KV-cache:&lt;/p>
&lt;p>$$V_{\text{inst}} = V_{\text{pesos}} + V_{\text{KV}} + V_{\text{overhead}}$$&lt;/p>
&lt;p>Tomemos un modelo de &lt;strong>7B parámetros en FP8&lt;/strong> (1 byte/parámetro):&lt;/p>
&lt;p>$$V_{\text{pesos}} \approx 7 \times 10^9 \times 1\ \text{byte} = 7\ \text{GB}$$&lt;/p>
&lt;p>En una instancia &lt;code>1g.10gb&lt;/code> (10 GB), tras los 7 GB de pesos y restando ~0.5–1 GB de overhead del runtime, quedan &lt;strong>~2 GB para KV-cache&lt;/strong>. Eso da para una ventana de contexto modesta y poca concurrencia —correcto para un servicio guardrail, un clasificador o un modelo de extracción que procesa prompts cortos uno a uno. Un 7B en INT4 (~3.5 GB de pesos) deja ~5.5 GB de KV-cache, mucho más holgado. Pero un modelo de 13B en FP8 (~13 GB de pesos) &lt;strong>no cabe&lt;/strong> en una instancia de 10 GB: ni siquiera entran los pesos. Para él necesitas &lt;code>1g.20gb&lt;/code>, &lt;code>2g.20gb&lt;/code> o mayor.&lt;/p>
&lt;p>Frente a esto, la &lt;code>7g.80gb&lt;/code> (GPU entera) te da los 80 GB para &lt;strong>un modelo grande&lt;/strong>: un 70B en FP8 (~70 GB de pesos) cabe con KV-cache justo, o un 70B con más holgura repartido en &lt;em>tensor-parallel&lt;/em> sobre varias GPU enteras (ver &lt;a href="https://blog.lo0.es/posts/tp-replicas-una-grande-vs-n-pequenas/">TP frente a réplicas: una grande contra N pequeñas&lt;/a>).&lt;/p>
&lt;p>La lectura es clara: &lt;strong>particionar fino (7×1g.10gb) maximiza el número de cargas aisladas pequeñas; no particionar (1×7g.80gb) maximiza el tamaño del modelo que cabe.&lt;/strong> El KV-cache disponible por instancia se reduce proporcionalmente al particionar, así que MIG fino sirve para &lt;strong>muchos servicios ligeros aislados&lt;/strong>, no para un modelo grande troceado. Si tu carga es un solo modelo grande, MIG no es para ti —usa la GPU entera o varias en TP.&lt;/p>
&lt;h2 id="el-árbol-de-decisión">El árbol de decisión&lt;/h2>
&lt;p>Las tres preguntas, en orden:&lt;/p>
&lt;pre tabindex="0">&lt;code>¿Necesitas aislamiento REAL?
(multi-tenant, compliance, fallo de una carga no debe tocar otra)
│
┌────┴────┐
SÍ NO
│ │
¿Es Hopper/ ¿Muchos kernels PEQUEÑOS concurrentes
Ampere/ Y confías en todas las cargas?
Blackwell? │
│ ┌────┴────┐
┌──┴──┐ SÍ NO
SÍ NO │ │
│ │ MPS ¿Dev / ráfagas / GPU de consumo
MIG │ (espacial, / sin necesidad de aislar?
│ │ QoS por │
│ no hay proceso) SÍ
│ aislam. │
│ real: TIME-SLICING
│ replantea (temporal, barato,
│ (mueve a funciona en 5090)
│ CPU, otra GPU,
│ o asume riesgo
│ con time-slicing)
&lt;/code>&lt;/pre>&lt;p>Y en una frase cada rama:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MIG&lt;/strong> cuando el aislamiento es un requisito (compliance, multi-tenant, SLA duro) &lt;strong>y&lt;/strong> tienes hardware de datacenter que lo soporta. Las paredes de hormigón cuestan, pero si las necesitas no hay sustituto.&lt;/li>
&lt;li>&lt;strong>MPS&lt;/strong> cuando tienes muchas cargas pequeñas concurrentes que dejan la GPU medio vacía &lt;strong>y&lt;/strong> confías en todas (mismo equipo, no tenants ajenos). Subes throughput con QoS razonable, asumiendo aislamiento de fallos imperfecto.&lt;/li>
&lt;li>&lt;strong>Time-slicing&lt;/strong> cuando es dev, ráfagas, baja utilización, GPU de consumo, o simplemente no necesitas aislar nada. Barato y universal, pero gestiona el presupuesto de VRAM a mano.&lt;/li>
&lt;/ul>
&lt;p>Un matiz que la documentación reciente recoge: &lt;strong>se pueden combinar&lt;/strong>. Puedes hacer time-slicing &lt;em>sobre&lt;/em> una instancia MIG (aislamiento de hardware en la frontera de la instancia, turnos de software dentro), o usar MPS dentro de una instancia MIG. Las capas no son excluyentes; el árbol elige la &lt;strong>estrategia primaria&lt;/strong>.&lt;/p>
&lt;h2 id="aplicado-al-cluster-genérico-4h100">Aplicado al cluster genérico 4×H100&lt;/h2>
&lt;p>Bajemos a números con un cluster on-premise genérico de &lt;strong>4×H100 SXM 80 GB con NVLink&lt;/strong>. Es habitual tener un menú de cargas heterogéneo: un modelo grande de chat, servicios ligeros (embeddings, reranker, guardrails) y una cola de dev/experimentación. Cada tipo pide un mecanismo distinto. Un reparto razonado:&lt;/p>
&lt;p>&lt;strong>GPU 0 y GPU 1 — modelo grande en tensor-parallel (sin compartir).&lt;/strong> Un modelo de 70B en FP8 ocupa ~70 GB de pesos; servido cómodo con KV-cache generoso necesita más de una H100. Lo repartimos en &lt;em>tensor-parallel&lt;/em> sobre &lt;strong>dos H100 enteras&lt;/strong> unidas por NVLink (el ancho de banda intra-nodo es lo que hace viable el TP; el detalle está en &lt;a href="https://blog.lo0.es/posts/tp-replicas-una-grande-vs-n-pequenas/">TP frente a réplicas&lt;/a>). Aquí &lt;strong>no compartimos&lt;/strong>: estas dos GPU son del modelo grande y punto. Aislamiento total por dedicación.&lt;/p>
&lt;p>$$V_{\text{disponible}} = 2 \times 80 = 160\ \text{GB};\quad V_{\text{pesos}} \approx 70\ \text{GB};\quad V_{\text{KV}} \approx 80\ \text{GB de KV-cache}$$&lt;/p>
&lt;p>Sobra memoria para una cola de peticiones larga y mucha concurrencia.&lt;/p>
&lt;p>&lt;strong>GPU 2 — partida con MIG en instancias pequeñas para servicios ligeros.&lt;/strong> Embeddings (&lt;code>bge-m3&lt;/code>), reranker (&lt;code>bge-reranker-v2-m3&lt;/code>) y un par de modelos guardrail (1B–3B) son cargas distintas, de equipos potencialmente distintos, y quieres que un fallo o un pico en una &lt;strong>no toque&lt;/strong> a las demás. Multi-tenant ligero con aislamiento → &lt;strong>MIG&lt;/strong>. Una partición razonable de la H100:&lt;/p>
&lt;p>$$\underbrace{3 \times \texttt{1g.10gb}}&lt;em>{\text{30 GB, 3/7 SMs}} ;+; \underbrace{1 \times \texttt{4g.40gb}}&lt;/em>{\text{40 GB, 4/7 SMs}}$$&lt;/p>
&lt;p>Las tres &lt;code>1g.10gb&lt;/code> (10 GB, 1/7 SMs cada una) alojan embeddings, reranker y un guardrail 1B INT4 —cada uno aislado, sin &lt;em>noisy neighbor&lt;/em>. La &lt;code>4g.40gb&lt;/code> (40 GB, 4/7 SMs) aloja un modelo intermedio de 7B–13B con KV-cache decente para un servicio de apoyo. Cada servicio tiene su despensa y su pared; si el reranker peta, el chat no se entera.&lt;/p>
&lt;p>&lt;strong>GPU 3 — time-slicing para dev y ráfagas.&lt;/strong> Los desarrolladores tocan la GPU esporádicamente: experimentos, fine-tunes cortos, pruebas de modelos. No necesitan aislamiento (es el mismo equipo) y rara vez coinciden activos. La anunciamos como &lt;strong>4 réplicas&lt;/strong> vía device-plugin. Cuatro pods de dev caben por turnos. Presupuesto de VRAM con la fórmula de arriba: si cada dev levanta un vLLM a &lt;code>--gpu-memory-utilization 0.20&lt;/code>, suman $4 \times 16 = 64$ GB &amp;lt; 80 GB, sin OOM. Si alguien necesita más, baja el número de réplicas o coordina con el equipo —el coste de la flexibilidad es la disciplina manual.&lt;/p>
&lt;p>&lt;strong>Resumen del reparto:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Recurso&lt;/th>
&lt;th>Mecanismo&lt;/th>
&lt;th>Carga&lt;/th>
&lt;th>Aislamiento&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>GPU 0 + GPU 1&lt;/td>
&lt;td>Dedicación (TP)&lt;/td>
&lt;td>70B chat en tensor-parallel&lt;/td>
&lt;td>total (dedicado)&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>hardware real&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPU 3&lt;/td>
&lt;td>Time-slicing (4 réplicas)&lt;/td>
&lt;td>dev, ráfagas, experimentos&lt;/td>
&lt;td>ninguno (confianza)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La lógica es siempre la misma: &lt;strong>gasta aislamiento (MIG) donde lo necesitas, gasta concurrencia barata (time-slicing) donde no, y reserva las GPU enteras para lo que de verdad las llena.&lt;/strong> Una H100 sirviendo embeddings con &lt;code>7g.80gb&lt;/code> sería tan absurdo como una RTX 5090 intentando MIG: la herramienta no encaja con la carga.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Qué pasa cuando ni siquiera caben a la vez&lt;/strong>: si tienes más modelos que VRAM y hay que turnarlos &lt;em>en memoria&lt;/em> (cargar/descargar pesos, no solo turnar cómputo), entras en territorio de &lt;em>swap&lt;/em> y &lt;em>sleep&lt;/em> —la pieza hermana &lt;a href="https://blog.lo0.es/posts/servir-varios-modelos-una-gpu-swap-sleep/">Servir varios modelos en una GPU&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Scheduling NUMA-aware&lt;/strong>: en nodos multi-socket, qué GPU toca a qué CPU/memoria importa para la latencia; ver &lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">Kubelet resource managers en RKE2&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Autoscaling de las réplicas&lt;/strong>: cuántas instancias levantar según carga real, con KEDA y métricas de cola; ver &lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling de LLM en Kubernetes con KEDA&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Benchmark de jitter bajo contención&lt;/strong>: cuánto baila realmente el TTFT en time-slicing con 4 réplicas activas frente a MIG —material que merece medición propia, no estimación.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/finops-multi-tenancy-gpu-litellm/">FinOps y multi-tenancy del cluster GPU: quién paga qué&lt;/a> — MIG como base del aislamiento y la atribución de coste entre tenants.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/gitops-stack-inferencia-llm-flux/">GitOps del stack de inferencia con Flux: operar el asistente como código&lt;/a> — cómo se declara el reparto de GPU (MIG, gpu-memory-utilization) como código en GitOps.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/servir-varios-modelos-una-gpu-swap-sleep/">Servir varios modelos en una GPU: swap y sleep&lt;/a> — pieza hermana de la serie: cuando los modelos no caben a la vez en VRAM y hay que turnarlos en memoria, no solo en cómputo.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">Kubelet resource managers en RKE2: NUMA y topología&lt;/a> — el reparto de GPU se complica con afinidad NUMA; qué GPU asignar a qué socket para no pagar latencia de interconexión.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/tp-replicas-una-grande-vs-n-pequenas/">TP frente a réplicas: una grande contra N pequeñas&lt;/a> — la decisión de dedicar 2 H100 enteras en tensor-parallel para el modelo grande es justo lo que aquí asumimos en el reparto del cluster.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">Capacity planning para inferencia LLM on-premise&lt;/a> — el presupuesto de VRAM (pesos + KV-cache) que aquí trabajamos por instancia es el núcleo del sizing del cluster entero.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling de LLM en Kubernetes con KEDA&lt;/a> — cuántas réplicas (time-sliced o no) levantar según la carga real, en lugar de fijarlas a mano.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/">Cinco niveles de madurez de una plataforma LLM on-premise&lt;/a> — pasar de &amp;ldquo;una GPU, una carga&amp;rdquo; a compartir con aislamiento es uno de los saltos de madurez que marca el modelo.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&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> (catálogo H100 80GB). &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> (réplicas de time-slicing). &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>