<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Sleep-Mode on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/sleep-mode/</link><description>Recent content in Sleep-Mode 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/sleep-mode/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></channel></rss>