<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Inferencia-Llm on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/inferencia-llm/</link><description>Recent content in Inferencia-Llm on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Fri, 22 May 2026 01:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/inferencia-llm/index.xml" rel="self" type="application/rss+xml"/><item><title>Disaggregated serving: prefill y decode en pods especializados</title><link>https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/</link><pubDate>Fri, 22 May 2026 01:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>La inferencia LLM tiene dos fases con perfiles opuestos: &lt;strong>prefill&lt;/strong> (procesar el prompt entero de golpe) es compute-bound, &lt;strong>decode&lt;/strong> (generar token a token) es memory-bandwidth-bound. Ejecutarlas en la misma GPU obliga a elegir entre dos hardware óptimos incompatibles, y deja entre el 60 % y el 80 % de la capacidad de pico sin usar. La industria ha consolidado el patrón en 2026: &lt;strong>disaggregated serving&lt;/strong> — pods separados para cada fase, conectados por un canal de transferencia de KV cache (NIXL sobre UCX, RDMA, o NCCL en su defecto). DistServe demostró 7,4× más request rate a igual SLO; NVIDIA Dynamo 1.0 (GA en GTC 2026) lleva el patrón a producción a escala datacenter. Mezclar hardware heterogéneo —H100 para prefill, GPUs commodity para decode— recorta hasta el 48 % del coste por token. Este artículo explica el porqué, el cómo, y los números que importan para una infraestructura on-premise típica.&lt;/p>
&lt;h2 id="estás-aquí-deploy">Estás aquí: Deploy&lt;/h2>
&lt;p>Disaggregated serving es una decisión arquitectónica de la etapa &lt;strong>Deploy&lt;/strong> del &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">pipeline LLMOps de seis etapas&lt;/a>. No cambia el modelo, no cambia los datos, no cambia las evals — sólo cambia &lt;strong>cómo se reparten los pods de inferencia sobre el hardware GPU&lt;/strong>. Pero ese cambio mueve el throughput agregado entre 2× y 7×.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#dsm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#dsm)}&lt;/style>
&lt;defs>&lt;marker id="dsm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DEPLOY · topología de pods prefill/decode y transferencia de KV cache&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-analogía-la-cocina-con-dos-brigadas">La analogía: la cocina con dos brigadas&lt;/h2>
&lt;p>Una cocina industrial seria —cualquiera que sirva más de 50 cubiertos por noche— funciona con dos brigadas distintas y dos espacios físicos separados.&lt;/p>
&lt;p>La &lt;strong>brigada de prep&lt;/strong> empieza al alba. Su trabajo es la &lt;em>mise en place&lt;/em>: cortar, marinar, blanquear, hervir fondos, preparar componentes complejos. Equipamiento: cuchillos buenos, fogones grandes, hornos de convección, ollas de 40 litros. Es trabajo intensivo en capacidad y se hace de golpe. Cuando termina, queda todo en bandejas etiquetadas listas para usar.&lt;/p>
&lt;p>La &lt;strong>brigada de pase&lt;/strong> entra a media tarde. Su trabajo es el servicio: tomar las bandejas de la prep, calentar porciones, emplatar, montar el pase. Equipamiento: salamandras, planchas pequeñas, espátulas finas, mucha vajilla. Es trabajo de muñeca, de ritmo, de no fallar al cliente que tiene el plato delante. La capacidad por hora importa menos que la latencia por plato.&lt;/p>
&lt;p>Si haces que &lt;strong>la misma persona&lt;/strong> haga prep y pase, las dos cosas sufren. El cocinero está parado mientras hace mise en place a media tarde. Tiene que parar a emplatar cuando entran cinco pedidos a la vez. Su equipo de trabajo está diseñado para uno o para el otro, no para ambos.&lt;/p>
&lt;p>Las cocinas serias resolvieron esto hace décadas: brigadas separadas, espacios separados, equipo separado. Lo único que cruza entre ambas son las bandejas de mise en place.&lt;/p>
&lt;p>Las &lt;strong>bandejas son el KV cache&lt;/strong>. La separación es &lt;strong>disaggregated serving&lt;/strong>. El pase de la prep al servicio es la &lt;strong>transferencia de KV cache&lt;/strong>, hoy resuelta con NIXL sobre RDMA. Y los pods especializados son las dos brigadas con sus equipos óptimos.&lt;/p>
&lt;h2 id="recap-rápido-prefill-y-decode">Recap rápido: prefill y decode&lt;/h2>
&lt;p>Una petición a un LLM atraviesa siempre dos fases:&lt;/p>
&lt;p>&lt;strong>Prefill.&lt;/strong> Coger el prompt completo (por ejemplo, 4.000 tokens) y procesarlo de una sola pasada por todas las capas del modelo. El resultado es el KV cache de esos 4.000 tokens (ver el &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">artículo previo sobre KV cache&lt;/a> si quieres recordar qué guarda exactamente). Este paso es masivamente paralelo: todos los tokens van a la vez por las matrices de atención, lo que se traduce en multiplicaciones de matrices enormes y densas. La GPU está al 90-95 % de uso de compute. &lt;strong>TTFT&lt;/strong> (time to first token) lo determina esta fase.&lt;/p>
&lt;p>&lt;strong>Decode.&lt;/strong> Una vez está el KV cache listo, el modelo genera tokens uno por uno. Cada token nuevo es una pasada por todas las capas con un solo vector de query, leyendo todo el KV cache acumulado para calcular la atención. No hay paralelismo entre tokens (cada uno depende del anterior). Lo que limita aquí no es el compute sino el ancho de banda: cada paso hay que leer los pesos completos del modelo desde HBM. La GPU está al 20-40 % de uso de compute, pero al 90 % de uso del HBM. &lt;strong>TBT&lt;/strong> (time between tokens) lo determina esta fase.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Fase&lt;/th>
&lt;th>Característica&lt;/th>
&lt;th>Cuello de botella&lt;/th>
&lt;th>Métrica clave&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Prefill&lt;/td>
&lt;td>Cómputo masivo paralelo sobre N tokens de golpe&lt;/td>
&lt;td>TFLOPS (compute)&lt;/td>
&lt;td>TTFT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Decode&lt;/td>
&lt;td>Streaming de pesos desde HBM, 1 token cada vez&lt;/td>
&lt;td>Bandwidth HBM&lt;/td>
&lt;td>TBT (inter-token latency)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 290" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Utilizacion compute vs bandwidth en prefill y decode">
&lt;style>
.bar { stroke: #333; stroke-width: 1; }
.b-compute { fill: #2a9d8f; }
.b-bandwidth { fill: #e76f51; }
.b-low { fill-opacity: 0.35; }
.ax { stroke: #333; stroke-width: 1.5; }
.grid { stroke: #ddd; stroke-width: 1; stroke-dasharray: 3,3; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
.tag { font: 600 12px sans-serif; }
&lt;/style>
&lt;text x="360" y="22" text-anchor="middle" class="lbl">Utilización de la GPU durante cada fase (orden de magnitud típico)&lt;/text>
&lt;line class="ax" x1="100" y1="240" x2="100" y2="60"/>
&lt;line class="ax" x1="100" y1="240" x2="680" y2="240"/>
&lt;line class="grid" x1="100" y1="78" x2="680" y2="78"/>
&lt;line class="grid" x1="100" y1="114" x2="680" y2="114"/>
&lt;line class="grid" x1="100" y1="150" x2="680" y2="150"/>
&lt;line class="grid" x1="100" y1="186" x2="680" y2="186"/>
&lt;text x="90" y="63" text-anchor="end" class="lbl-sm">100%&lt;/text>
&lt;text x="90" y="117" text-anchor="end" class="lbl-sm">75%&lt;/text>
&lt;text x="90" y="153" text-anchor="end" class="lbl-sm">50%&lt;/text>
&lt;text x="90" y="189" text-anchor="end" class="lbl-sm">25%&lt;/text>
&lt;text x="90" y="243" text-anchor="end" class="lbl-sm">0%&lt;/text>
&lt;text x="240" y="270" text-anchor="middle" class="lbl">PREFILL&lt;/text>
&lt;text x="240" y="284" text-anchor="middle" class="lbl-sm">compute-bound&lt;/text>
&lt;text x="540" y="270" text-anchor="middle" class="lbl">DECODE&lt;/text>
&lt;text x="540" y="284" text-anchor="middle" class="lbl-sm">memory-bound&lt;/text>
&lt;rect x="160" y="69" width="65" height="171" class="bar b-compute"/>
&lt;text x="193" y="62" text-anchor="middle" class="tag" fill="#2a9d8f">95%&lt;/text>
&lt;text x="193" y="255" text-anchor="middle" class="lbl-sm">compute&lt;/text>
&lt;rect x="245" y="132" width="65" height="108" class="bar b-bandwidth b-low"/>
&lt;text x="278" y="125" text-anchor="middle" class="tag" fill="#e76f51">60%&lt;/text>
&lt;text x="278" y="255" text-anchor="middle" class="lbl-sm">HBM&lt;/text>
&lt;rect x="460" y="177" width="65" height="63" class="bar b-compute b-low"/>
&lt;text x="493" y="170" text-anchor="middle" class="tag" fill="#2a9d8f">35%&lt;/text>
&lt;text x="493" y="255" text-anchor="middle" class="lbl-sm">compute&lt;/text>
&lt;rect x="545" y="78" width="65" height="162" class="bar b-bandwidth"/>
&lt;text x="578" y="71" text-anchor="middle" class="tag" fill="#e76f51">90%&lt;/text>
&lt;text x="578" y="255" text-anchor="middle" class="lbl-sm">HBM&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>La asimetría es estructural: prefill quema el compute y deja la memoria a media, decode hace lo contrario. &lt;strong>Una GPU diseñada para ser excelente en ambos a la vez es una GPU diseñada para estar mal aprovechada todo el tiempo.&lt;/strong>&lt;/p>
&lt;h2 id="por-qué-juntarlas-en-la-misma-gpu-es-un-mal-negocio">Por qué juntarlas en la misma GPU es un mal negocio&lt;/h2>
&lt;p>Hasta 2023, la asunción universal era ejecutar prefill y decode &lt;strong>en el mismo proceso de inferencia, sobre la misma GPU&lt;/strong>. El motor scheduler (vLLM, TGI, Triton) decidía en cada ciclo si hacer prefill de una petición nueva o decode de las que ya estaban en marcha. La intuición era que compartir hardware ahorra coste.&lt;/p>
&lt;p>La intuición es incorrecta. El problema tiene tres caras:&lt;/p>
&lt;p>&lt;strong>Interferencia en latencia.&lt;/strong> Cuando el motor decide hacer prefill de una petición nueva, &lt;strong>interrumpe&lt;/strong> todos los decodes en curso. Eso sube el TBT de las otras peticiones. El usuario que estaba viendo tokens caer fluidos en su pantalla nota un parón de varios cientos de milisegundos. Esto se conoce como &lt;em>prefill-decode interference&lt;/em> y degrada la experiencia de forma visible a medida que sube la concurrencia.&lt;/p>
&lt;p>&lt;strong>Hardware sub-óptimo para cada fase.&lt;/strong> Una H100 SXM tiene 989 TFLOPS BF16 de compute y 3,35 TB/s de HBM3. Es excelente para prefill, donde el compute es el límite. Para decode, donde lo único que importa es el bandwidth, esos 989 TFLOPS están desaprovechados al 60-70 %. Inversamente, una GPU con menos compute pero similar bandwidth relativo (RTX 4090, L40S) resolvería el decode igual de bien por una fracción del precio.&lt;/p>
&lt;p>&lt;strong>Utilización agregada baja.&lt;/strong> En workloads reales con Llama 3 70B y outputs de 512 tokens, &lt;strong>alrededor del 80 % del wall-clock se gasta en decode&lt;/strong>. Eso quiere decir que el 80 % del presupuesto de tu cluster H100 está haciendo lecturas de memoria, no cálculos. Es como pagar un Ferrari para usarlo en cola de aparcamiento.&lt;/p>
&lt;h2 id="la-idea-pods-especializados-kv-cache-como-entregable">La idea: pods especializados, KV cache como entregable&lt;/h2>
&lt;p>Disaggregated serving rompe el ciclo de inferencia en dos servicios distintos:&lt;/p>
&lt;p>&lt;strong>Pod de prefill.&lt;/strong> Recibe el prompt, ejecuta el prefill, produce el KV cache. Hardware: GPUs con alto compute (H100, H200, B200). Optimizado para batching agresivo y throughput, no para latencia individual: si llegan 32 prompts en 100 ms, los procesa juntos.&lt;/p>
&lt;p>&lt;strong>Pod de decode.&lt;/strong> Recibe el KV cache ya construido, ejecuta la generación token a token, streamea al cliente. Hardware: GPUs con buen bandwidth pero idealmente más baratas por TFLOPS (RTX 4090, L40S, A100, incluso A30 según el caso). Optimizado para latencia por token (TBT bajo).&lt;/p>
&lt;p>Entre ambos: una &lt;strong>transferencia de KV cache&lt;/strong> sobre la red, que puede ser nodo-local (shared memory, NVLink), intra-rack (RDMA con InfiniBand o RoCE) o cross-rack (NIXL sobre UCX). El coste de esta transferencia escala linealmente con la longitud del contexto, y es la clave económica del esquema.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Arquitectura monolitica vs disaggregated">
&lt;style>
.node { stroke: #333; stroke-width: 1.5; }
.n-mono { fill: #ffe9d6; }
.n-prefill { fill: #d9f5d6; }
.n-decode { fill: #d6eaff; }
.n-router { fill: #fffae6; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
.lbl-section { font: 700 14px sans-serif; fill: #222; }
.arr { stroke: #444; stroke-width: 1.6; fill: none; marker-end: url(#ah4); }
.arr-int { stroke: #c1121f; stroke-width: 1.4; fill: none; stroke-dasharray: 5,3; marker-end: url(#ah4r); }
&lt;/style>
&lt;defs>
&lt;marker id="ah4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#444"/>&lt;/marker>
&lt;marker id="ah4r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#c1121f"/>&lt;/marker>
&lt;/defs>
&lt;text x="170" y="25" text-anchor="middle" class="lbl-section">Monolítico (aggregated)&lt;/text>
&lt;rect x="40" y="50" width="260" height="220" rx="10" class="node n-mono"/>
&lt;text x="170" y="78" text-anchor="middle" class="lbl">GPU única&lt;/text>
&lt;text x="170" y="98" text-anchor="middle" class="lbl-sm">scheduler decide cada ciclo:&lt;/text>
&lt;rect x="65" y="115" width="100" height="40" rx="5" class="node n-prefill"/>
&lt;text x="115" y="140" text-anchor="middle" class="lbl-sm">prefill&lt;/text>
&lt;rect x="180" y="115" width="100" height="40" rx="5" class="node n-decode"/>
&lt;text x="230" y="140" text-anchor="middle" class="lbl-sm">decode&lt;/text>
&lt;path class="arr-int" d="M165,128 L180,128"/>
&lt;path class="arr-int" d="M180,145 L165,145"/>
&lt;text x="170" y="180" text-anchor="middle" class="lbl-sm" fill="#c1121f">interferencia en cada cambio&lt;/text>
&lt;text x="170" y="200" text-anchor="middle" class="lbl-sm">→ TBT sube cuando llega prefill&lt;/text>
&lt;text x="170" y="230" text-anchor="middle" class="lbl-sm">una HW óptima para ambos:&lt;/text>
&lt;text x="170" y="250" text-anchor="middle" class="lbl-sm">imposible&lt;/text>
&lt;text x="540" y="25" text-anchor="middle" class="lbl-section">Disaggregated&lt;/text>
&lt;rect x="370" y="50" width="150" height="100" rx="10" class="node n-prefill"/>
&lt;text x="445" y="80" text-anchor="middle" class="lbl">pod prefill&lt;/text>
&lt;text x="445" y="102" text-anchor="middle" class="lbl-sm">H100 / H200 / B200&lt;/text>
&lt;text x="445" y="120" text-anchor="middle" class="lbl-sm">compute alto, batching&lt;/text>
&lt;text x="445" y="138" text-anchor="middle" class="lbl-sm">agresivo&lt;/text>
&lt;rect x="560" y="50" width="150" height="100" rx="10" class="node n-decode"/>
&lt;text x="635" y="80" text-anchor="middle" class="lbl">pod decode&lt;/text>
&lt;text x="635" y="102" text-anchor="middle" class="lbl-sm">4090 / L40S / A100&lt;/text>
&lt;text x="635" y="120" text-anchor="middle" class="lbl-sm">bandwidth alto, TBT&lt;/text>
&lt;text x="635" y="138" text-anchor="middle" class="lbl-sm">estable&lt;/text>
&lt;path class="arr" d="M520,100 L560,100"/>
&lt;text x="540" y="92" text-anchor="middle" class="lbl-sm">KV cache&lt;/text>
&lt;text x="540" y="115" text-anchor="middle" class="lbl-sm">NIXL/RDMA&lt;/text>
&lt;rect x="450" y="180" width="180" height="50" rx="8" class="node n-router"/>
&lt;text x="540" y="200" text-anchor="middle" class="lbl">router (vLLM/Dynamo)&lt;/text>
&lt;text x="540" y="218" text-anchor="middle" class="lbl-sm">distribuye prompts y streams&lt;/text>
&lt;path class="arr" d="M445,150 L500,180"/>
&lt;path class="arr" d="M635,150 L580,180"/>
&lt;text x="540" y="260" text-anchor="middle" class="lbl-sm" fill="#2a9d8f">→ TBT estable, TTFT bajo&lt;/text>
&lt;text x="540" y="280" text-anchor="middle" class="lbl-sm">coste: transferencia KV cache&lt;/text>
&lt;text x="540" y="298" text-anchor="middle" class="lbl-sm">~5-50 ms según interconnect&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="el-protocolo-de-transferencia-la-economía-del-movimiento">El protocolo de transferencia: la economía del movimiento&lt;/h2>
&lt;p>El KV cache transferido en un Llama 3 70B con 4K de contexto pesa aproximadamente &lt;strong>2,6 GB&lt;/strong> (80 layers × 8 KV heads × 128 dim × 4 096 tokens × 2 (K y V) × 2 bytes en BF16). Mover 2,6 GB entre dos GPUs no es trivial:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Canal&lt;/th>
&lt;th style="text-align:right">Bandwidth efectivo&lt;/th>
&lt;th style="text-align:right">Tiempo para 2,6 GB&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>NVLink intra-nodo (NVSwitch)&lt;/td>
&lt;td style="text-align:right">~450 GB/s&lt;/td>
&lt;td style="text-align:right">~6 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Shared memory (mismo nodo, PCIe 5)&lt;/td>
&lt;td style="text-align:right">~60 GB/s&lt;/td>
&lt;td style="text-align:right">~45 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RDMA InfiniBand 400 Gbps&lt;/td>
&lt;td style="text-align:right">~50 GB/s&lt;/td>
&lt;td style="text-align:right">~55 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RDMA RoCE 200 Gbps&lt;/td>
&lt;td style="text-align:right">~25 GB/s&lt;/td>
&lt;td style="text-align:right">~105 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TCP/IP 10 GbE&lt;/td>
&lt;td style="text-align:right">~1 GB/s&lt;/td>
&lt;td style="text-align:right">~2,6 s&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Lectura inmediata: por encima de InfiniBand-grade, la transferencia es cómoda. Por debajo, lleva al traste el TTFT que estamos intentando mejorar. &lt;strong>Disaggregated serving es viable sólo con interconexión decente&lt;/strong> — no es un patrón para clusters montados con switches Ethernet de consumo.&lt;/p>
&lt;p>NVIDIA respondió a esto con &lt;strong>NIXL&lt;/strong> (NVIDIA Inference Transfer Library), publicada a mediados de 2025: una librería que abstrae el transporte (UCX, NCCL, RDMA verbs directos, shared memory) y elige el mejor camino disponible automáticamente. vLLM la integra desde finales de 2025 mediante el &lt;code>NixlConnector&lt;/code>. Es ahora el default de facto para nuevos despliegues.&lt;/p>
&lt;h2 id="implementaciones-reales-en-mayo-2026">Implementaciones reales en mayo 2026&lt;/h2>
&lt;p>El recorrido del patrón en dos años:&lt;/p>
&lt;pre tabindex="0">&lt;code>2024 ene · DistServe (HKU + UCSD): 7,4× requests al mismo SLO
2024 may · SplitWise (Microsoft): variante con hardware heterogéneo
2024 dic · vLLM disagg experimental (SharedStorage + PyNcclConnector)
2025 mar · NIXL release (NVIDIA): librería de transferencia unificada
2025 jul · vLLM NixlConnector estable
2025 nov · SGLang, llm-d, MoonCake adoptan el patrón
2026 mar · NVIDIA Dynamo 1.0 GA (GTC 2026): production-ready a escala datacenter
&lt;/code>&lt;/pre>&lt;p>A día de hoy, &lt;strong>el patrón es el default&lt;/strong> en cualquier framework de serving serio. Los que siguen monolíticos son los pequeños o los educativos.&lt;/p>
&lt;p>Tres opciones realistas para una infraestructura on-premise:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>vLLM disagg con NixlConnector.&lt;/strong> El camino más abierto, requiere desplegar dos sets de pods de vLLM (uno con &lt;code>--kv-transfer-config '{&amp;quot;kv_role&amp;quot;:&amp;quot;producer&amp;quot;}'&lt;/code>, otro con &lt;code>&amp;quot;kv_role&amp;quot;:&amp;quot;consumer&amp;quot;&lt;/code>) y un proxy router. Suficiente para clusters de 4-16 GPUs.&lt;/li>
&lt;li>&lt;strong>SGLang con disagg.&lt;/strong> Equivalente conceptual, mejor performance en algunos workloads MoE.&lt;/li>
&lt;li>&lt;strong>NVIDIA Dynamo 1.0.&lt;/strong> El que se está imponiendo a escala datacenter. Cubre routing, KV cache management, monitorización y scheduling en un solo plano de control. Más pesado, pero la solución de referencia si tu cluster crece por encima de 32 GPUs.&lt;/li>
&lt;/ol>
&lt;h2 id="los-números-que-importan">Los números que importan&lt;/h2>
&lt;p>Lo que la disaggregation desbloquea, en términos directos:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Aggregated (monolítico)&lt;/th>
&lt;th>Disaggregated&lt;/th>
&lt;th>Mejora&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Goodput (req/s al SLO)&lt;/td>
&lt;td>baseline&lt;/td>
&lt;td>1,4 – 2×&lt;/td>
&lt;td>hasta 2×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TTFT bajo carga alta&lt;/td>
&lt;td>sube agresivo desde QPS 4&lt;/td>
&lt;td>estable hasta QPS 7+&lt;/td>
&lt;td>~2×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Request rate al mismo SLO (DistServe paper)&lt;/td>
&lt;td>baseline&lt;/td>
&lt;td>7,4×&lt;/td>
&lt;td>7,4×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput MoE en Blackwell (Dynamo, GB300 NVL72)&lt;/td>
&lt;td>baseline (Hopper)&lt;/td>
&lt;td>hasta 50×&lt;/td>
&lt;td>depende del modelo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Coste por token (heterogéneo H100 + commodity)&lt;/td>
&lt;td>baseline (todo H100)&lt;/td>
&lt;td>-48 %&lt;/td>
&lt;td>casi mitad&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Hay que leer estos números con cuidado: los más espectaculares (7× y 50×) requieren hardware específico (Blackwell GB200/GB300 NVL72) y modelos específicos (MoE grandes). El &lt;strong>rango realista para un on-premise típico es 1,4-2× en goodput y -30 a -50 % en coste por token&lt;/strong>, dependiendo de cuán heterogénea sea la mezcla de GPUs y de cuán optimizada esté la transferencia de KV cache.&lt;/p>
&lt;h2 id="heterogeneidad-la-versión-radical">Heterogeneidad: la versión radical&lt;/h2>
&lt;p>El paso lógico siguiente, propuesto por SplitWise en 2024 y madurado en 2025-2026 (Cronus, Tessera y otros), es &lt;strong>mezclar tipos de GPU&lt;/strong>: GPUs caras de cómputo alto para prefill, GPUs commodity con buen bandwidth para decode.&lt;/p>
&lt;p>Coste indicativo (precios de mercado típicos a mediados de 2026):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>H100 SXM&lt;/strong>: ~30-40 k$ capex, ~3-4 $/h amortizado. Perfil compute-pesado.&lt;/li>
&lt;li>&lt;strong>L40S&lt;/strong>: ~8-10 k$ capex, ~1,5 $/h. Perfil intermedio, 864 GB/s de bandwidth.&lt;/li>
&lt;li>&lt;strong>RTX 4090&lt;/strong>: ~1,5 k$ capex, ~0,30 $/h. Perfil compute-modesto pero 1 TB/s de bandwidth GDDR6X — suficiente para decode de modelos hasta ~30B parámetros.&lt;/li>
&lt;/ul>
&lt;p>Un cluster mixto realista para servir un modelo 8B:&lt;/p>
&lt;pre tabindex="0">&lt;code>2× RTX 4090 (prefill batch) → ~3.000 $ capex, ~0,60 $/h
4× RTX 4090 (decode pool) → ~6.000 $ capex, ~1,20 $/h
TOTAL → ~9.000 $ capex, ~1,80 $/h
&lt;/code>&lt;/pre>&lt;p>Frente a la alternativa monolítica equivalente en throughput:&lt;/p>
&lt;pre tabindex="0">&lt;code>2× H100 SXM (todo en uno) → ~70.000 $ capex, ~7 $/h
&lt;/code>&lt;/pre>&lt;p>El mismo throughput a una fracción del capex y a la cuarta parte del coste por hora, &lt;strong>a costa de complejidad operativa&lt;/strong>: ahora tienes dos pools que coordinar, una red de transferencia que cuidar, y un scheduler que no es trivial.&lt;/p>
&lt;p>Para modelos más grandes (Llama 3 70B), el decode pool ya no cabe en una 4090 individual (el modelo no entra en 24 GB ni siquiera cuantizado a INT4 con margen). Ahí la mezcla razonable es H100 para prefill + L40S o A100 80GB para decode, con ahorro típico del 30-40 % sobre la opción todo-H100.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise-típico">Aplicado a hardware on-premise típico&lt;/h2>
&lt;h3 id="caso-1--una-o-dos-rtx-4090-monolítico-sigue-ganando">Caso 1 — Una o dos RTX 4090: monolítico sigue ganando&lt;/h3>
&lt;p>Con una sola GPU no hay disaggregation que valga: el patrón requiere mínimo dos GPUs en pods separados. Con dos 4090, técnicamente puedes intentarlo (una para prefill, otra para decode con KV cache transferido por PCIe 5 o RDMA básico), pero el overhead de transferencia se come la ganancia para modelos pequeños donde el prefill ya es rápido.&lt;/p>
&lt;p>&lt;strong>Recomendación:&lt;/strong> mantener monolítico (vLLM tradicional, &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">bien configurado con KV cache cuantizado&lt;/a>). El siguiente nivel justificable de complejidad es un cluster con interconexión rápida.&lt;/p>
&lt;h3 id="caso-2--cluster-4h100-sxm-320-gb-nvlink-el-sweet-spot">Caso 2 — Cluster 4×H100 SXM (320 GB, NVLink): el sweet spot&lt;/h3>
&lt;p>Configuración mínima realista para disaggregation seria, sirviendo un modelo 70B en producción:&lt;/p>
&lt;pre tabindex="0">&lt;code>2× H100 (TP=2) → 2 pods de prefill
2× H100 (TP=2) → pods de decode con varias instancias compartiendo TP
NIXL sobre NVLink → transferencia KV cache &amp;lt;6 ms
Router (vLLM o Dynamo) → distribución de prompts y stream
&lt;/code>&lt;/pre>&lt;p>Resultado realista esperado: goodput &lt;strong>1,6-1,9× respecto al mismo cluster en monolítico&lt;/strong>, con TTFT estable hasta cargas de QPS 7-8 (frente al QPS 4 al que empieza a degradar el monolítico).&lt;/p>
&lt;p>Si la mezcla heterogénea es posible (añadir 4-8 L40S al cluster para hacer el decode pool), el coste por token cae adicionalmente entre un 25 % y un 35 %, manteniendo el modelo 70B servido íntegro.&lt;/p>
&lt;h2 id="posición-dentro-de-la-arquitectura">Posición dentro de la arquitectura&lt;/h2>
&lt;p>Disaggregated serving es una &lt;strong>capa transversal&lt;/strong> a casi todo lo discutido en artículos previos. Toca:&lt;/p>
&lt;ul>
&lt;li>El &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a> porque es el artefacto que se transfiere entre pods. Sin entender bien cuánto pesa el cache y cómo crece con el contexto, no se puede dimensionar la transferencia.&lt;/li>
&lt;li>El &lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">fine-tuning continuo&lt;/a> porque el multi-LoRA hot-swap conserva su semántica: cada pod (prefill o decode) carga los adapters por separado, y el router decide qué adapter aplicar en cada fase.&lt;/li>
&lt;li>La topología del cluster: cambia la HW recomendada, el networking exigido y el modelo de costes.&lt;/li>
&lt;/ul>
&lt;p>Si estás diseñando una infraestructura de inferencia para 2026 desde cero, &lt;strong>disaggregation deja de ser opcional&lt;/strong> para cualquier cluster que exceda 4 GPUs de capacidad. Si estás modernizando una existente, es la actualización con mejor retorno por euro invertido — siempre que el networking entre pods sea decente (NVLink intra-nodo o RDMA intra-rack como mínimo).&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>NIXL en detalle&lt;/strong>: cómo elige el transporte óptimo, cómo se configura UCX, qué pasa cuando RDMA falla y hay que degradar a TCP.&lt;/li>
&lt;li>&lt;strong>Scheduler de routing&lt;/strong>: cómo decide el orquestador qué pod recibe qué petición, batching dinámico, manejo de prioridades.&lt;/li>
&lt;li>&lt;strong>Multi-tenant disagg&lt;/strong>: aislamiento de KV cache entre tenants, ACLs por adapter, multi-LoRA sobre pods especializados.&lt;/li>
&lt;li>&lt;strong>Disagg + prefix caching&lt;/strong>: cómo se combina con el patrón de reutilización de KV cache cuando varios prompts comparten prefijo (system prompt común).&lt;/li>
&lt;li>&lt;strong>Disagg en edge / inferencia local&lt;/strong>: viabilidad sobre hardware doméstico (4090 + Mac Studio, por ejemplo), donde la transferencia depende de Thunderbolt o Ethernet residencial.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">El pipeline LLMOps de seis etapas&lt;/a> — el mapa maestro al que pertenece la etapa Deploy. Este post entra en una decisión arquitectónica concreta dentro de esa etapa.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cluster-h100-plataforma-multi-tenant/">El cluster GPU como plataforma multi-tenant&lt;/a> — el patrón de capas Gateway/Quota/Isolation/Observability sobre el cual la disaggregation aquí descrita se sitúa: el cluster H100 que sirve a varios tenants combina ambos patrones.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators de inferencia LLM en Kubernetes&lt;/a> — los operators (vLLM Production Stack, NVIDIA Dynamo, llm-d, OME) que materializan en Kubernetes los pods especializados de prefill y decode.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a> — el artefacto exacto que se transfiere entre pods, con la fórmula completa de su tamaño.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention por dentro: bloques, tabla de páginas, evicción y el estado del arte del KV cache en 2026&lt;/a> — la mecánica del KV cache que la disaggregation explota a nivel del bloque, y el panorama de optimizaciones derivadas (vAttention, LMCache, RadixAttention).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">Fine-tuning continuo en producción&lt;/a> — cómo el multi-LoRA hot-swap convive con la disaggregation: cada pod carga adapters por separado, el router elige.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Zhong et al., &lt;em>DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving&lt;/em> (OSDI 2024).&lt;/li>
&lt;li>Patel et al., &lt;em>SplitWise: Efficient Generative LLM Inference Using Phase Splitting&lt;/em> (ISCA 2024).&lt;/li>
&lt;li>NVIDIA, &lt;em>NVIDIA Dynamo 1.0: Production-Ready Disaggregated Inference&lt;/em> (GTC 2026, marzo): &lt;a href="https://developer.nvidia.com/blog/nvidia-dynamo-1-production-ready/">https://developer.nvidia.com/blog/nvidia-dynamo-1-production-ready/&lt;/a>.&lt;/li>
&lt;li>NVIDIA, &lt;em>NIXL: NVIDIA Inference Transfer Library&lt;/em> — documentación oficial.&lt;/li>
&lt;li>vLLM, &lt;em>Disaggregated Prefilling&lt;/em>: &lt;a href="https://docs.vllm.ai/en/stable/features/disagg_prefill/">https://docs.vllm.ai/en/stable/features/disagg_prefill/&lt;/a>.&lt;/li>
&lt;li>vLLM, &lt;em>NixlConnector Usage Guide&lt;/em>: &lt;a href="https://docs.vllm.ai/en/stable/features/nixl_connector_usage/">https://docs.vllm.ai/en/stable/features/nixl_connector_usage/&lt;/a>.&lt;/li>
&lt;li>Hao AI Lab, &lt;em>Disaggregated Inference: 18 Months Later&lt;/em> (UCSD, 2025) — retrospectiva técnica del paper DistServe.&lt;/li>
&lt;/ul></description></item><item><title>El cluster GPU como plataforma: cómo convertir un cluster compartido en un servicio multi-tenant que tus equipos puedan consumir</title><link>https://blog.lo0.es/posts/cluster-h100-plataforma-multi-tenant/</link><pubDate>Thu, 21 May 2026 07:15:00 +0200</pubDate><guid>https://blog.lo0.es/posts/cluster-h100-plataforma-multi-tenant/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Tener un cluster de GPUs caro y muchas cargas distintas que lo quieren usar no es un problema de &lt;strong>infraestructura&lt;/strong>: es un problema de &lt;strong>producto interno&lt;/strong>. Lo que separa &amp;ldquo;tenemos un cluster&amp;rdquo; de &amp;ldquo;tenemos una plataforma de inferencia&amp;rdquo; son cuatro capas que el mercado ha consolidado en 2026: una &lt;strong>capa de gateway&lt;/strong> que centraliza autenticación, routing y políticas (LiteLLM, Portkey, Kong AI Gateway); un &lt;strong>modelo de aislamiento GPU&lt;/strong> apropiado al perfil de los tenants (MIG hardware-isolation para multi-tenant no confiable, MPS para procesos del mismo equipo, time-slicing solo para dev); un &lt;strong>sistema de quotas y rate limiting&lt;/strong> con presupuestos por tenant/equipo/proyecto (LiteLLM lo hace en su core a nivel team/user/api-key con 429s descriptivos); y un &lt;strong>plano de observabilidad multi-tenant&lt;/strong> que permite cost attribution real (showback como paso intermedio, chargeback como destino), tracing por tenant y dashboards diferenciados. Aplicado a un cluster GPU mid-scale típico (un nodo con 4-8 H100 SXM y NVLink, un punto habitual para empezar en producción), esto se traduce en decisiones concretas: con ~640 GB de VRAM agregada en 8 GPUs y dos modelos típicos en producción (un modelo grande de 70B+ con tensor parallel y un modelo mediano replicado), el cluster sirve entre &lt;strong>decenas y bajos centenares de sesiones simultáneas&lt;/strong> según mix; el aislamiento GPU se suele resolver con &lt;strong>MIG en cargas inferiores y dedicación per-model&lt;/strong> en cargas grandes; y la métrica de éxito de la plataforma es la &lt;strong>utilización efectiva&lt;/strong>, que en producción típica está en &lt;strong>30-40%&lt;/strong> y el objetivo razonable de optimización es subirla a 60-70% sin degradar SLA.&lt;/p>
&lt;blockquote>
&lt;p>Este es el &lt;strong>quinto post de la serie MLOps para LLMs&lt;/strong>. Es el más operacionalmente orientado y atraviesa varias etapas del pipeline (Deploy + Observe + transversales). El &amp;ldquo;estás aquí&amp;rdquo; señala las dos etapas activas porque la noción de plataforma multi-tenant no vive en una sola.&lt;/p>
&lt;/blockquote>
&lt;h2 id="estás-aquí-deploy--observe-cluster-como-producto">Estás aquí: Deploy + Observe (cluster como producto)&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Deploy + Observe">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7adb7a;stroke-width:3}.active2{fill:#c47aff;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mt1)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mt1)}&lt;/style>
&lt;defs>&lt;marker id="mt1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DEPLOY + OBSERVE · el cluster como plataforma con tenants&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box active2"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-pregunta-que-cambia-el-marco">La pregunta que cambia el marco&lt;/h2>
&lt;p>Cuando un equipo de plataforma adquiere hardware GPU caro y empieza a montar inferencia, la primera versión casi siempre es &lt;strong>mononosa&lt;/strong>: un modelo, un cliente, una latencia objetivo. Funciona. Cuando llega el segundo equipo pidiendo el mismo recurso, &lt;strong>la mononosa se vuelve política interna&lt;/strong>: ¿cuántas réplicas le damos? ¿Qué hacemos si chocan los SLA? ¿Quién paga los tokens del experimento del equipo B? Y cuando llega el tercero, lo que era un proyecto de SRE pasa a ser un proyecto de &lt;strong>producto interno&lt;/strong>.&lt;/p>
&lt;p>La distinción no es técnica, es de marco. &lt;strong>Un cluster es infra&lt;/strong>. &lt;strong>Una plataforma es un servicio con clientes, contratos y métricas de éxito&lt;/strong>. El cambio de marco implica:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Clientes identificables&lt;/strong> (tenants), no usuarios anónimos.&lt;/li>
&lt;li>&lt;strong>Contratos&lt;/strong> (latency SLA, throughput garantizado, modelos disponibles), no &amp;ldquo;lo que dé tiempo&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Métricas de éxito&lt;/strong> que no son técnicas sino de producto: adopción, satisfaction, cost per query por tenant, tiempo del primer &amp;ldquo;hello world&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;p>Este post recorre cómo se opera ese cambio de marco. Lo aterriza sobre un &lt;strong>cluster mid-scale (4-8 H100 SXM con NVLink en un solo nodo)&lt;/strong>, configuración habitual cuando se empieza con inferencia LLM seria; pero los principios se generalizan a cualquier topología, desde un nodo único con dos GPUs hasta clusters multi-nodo con InfiniBand.&lt;/p>
&lt;h2 id="las-cuatro-capas-de-una-plataforma-de-inferencia-multi-tenant">Las cuatro capas de una plataforma de inferencia multi-tenant&lt;/h2>
&lt;p>La arquitectura canónica que se ha establecido en 2026 tiene &lt;strong>cuatro capas&lt;/strong> que cualquier plataforma multi-tenant seria implementa, en orden de afuera hacia adentro:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 410" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Cuatro capas plataforma multi-tenant">
&lt;style>.title{font:700 13px sans-serif;fill:#222}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#555}.tiny{font:10px sans-serif;fill:#666}.layer{stroke:#444;stroke-width:1.5;rx:6}.gw{fill:#ffe9d6}.pol{fill:#d6eaff}.iso{fill:#d9f5d6}.obs{fill:#e9d6f5}.cluster{stroke:#666;stroke-dasharray:4 2;fill:none}.tenant{stroke:#888;stroke-width:1.4;fill:#fffce6;rx:4}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#pm1)}&lt;/style>
&lt;defs>&lt;marker id="pm1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="22" text-anchor="middle" class="title">Las cuatro capas de la plataforma multi-tenant&lt;/text>
&lt;rect x="40" y="50" width="100" height="40" class="tenant"/>&lt;text x="90" y="68" text-anchor="middle" class="sm">Tenant A&lt;/text>&lt;text x="90" y="82" text-anchor="middle" class="tiny">soporte chat&lt;/text>
&lt;rect x="160" y="50" width="100" height="40" class="tenant"/>&lt;text x="210" y="68" text-anchor="middle" class="sm">Tenant B&lt;/text>&lt;text x="210" y="82" text-anchor="middle" class="tiny">RAG legal&lt;/text>
&lt;rect x="280" y="50" width="100" height="40" class="tenant"/>&lt;text x="330" y="68" text-anchor="middle" class="sm">Tenant C&lt;/text>&lt;text x="330" y="82" text-anchor="middle" class="tiny">agente code&lt;/text>
&lt;rect x="400" y="50" width="100" height="40" class="tenant"/>&lt;text x="450" y="68" text-anchor="middle" class="sm">Tenant D&lt;/text>&lt;text x="450" y="82" text-anchor="middle" class="tiny">data extr.&lt;/text>
&lt;rect x="520" y="50" width="100" height="40" class="tenant"/>&lt;text x="570" y="68" text-anchor="middle" class="sm">Tenant E&lt;/text>&lt;text x="570" y="82" text-anchor="middle" class="tiny">batch ETL&lt;/text>
&lt;rect x="640" y="50" width="100" height="40" class="tenant"/>&lt;text x="690" y="68" text-anchor="middle" class="sm">notebooks&lt;/text>&lt;text x="690" y="82" text-anchor="middle" class="tiny">research&lt;/text>
&lt;rect x="40" y="120" width="700" height="60" class="layer gw"/>
&lt;text x="390" y="144" text-anchor="middle" class="lbl">Capa 1 · AI Gateway&lt;/text>
&lt;text x="55" y="166" class="sm">Auth (OIDC/API keys) · Routing por modelo · Failover · Caching · Logging · OTel emission · Rate limiting&lt;/text>
&lt;rect x="40" y="195" width="700" height="60" class="layer pol"/>
&lt;text x="390" y="219" text-anchor="middle" class="lbl">Capa 2 · Policy &amp;amp; Quota Plane&lt;/text>
&lt;text x="55" y="241" class="sm">Quotas RPS/TPM por tenant · Budgets mensuales · Whitelist modelos · Priority classes · Admission control&lt;/text>
&lt;rect x="40" y="270" width="700" height="60" class="layer iso"/>
&lt;text x="390" y="294" text-anchor="middle" class="lbl">Capa 3 · Isolation Plane&lt;/text>
&lt;text x="55" y="316" class="sm">MIG / MPS / time-slicing · Namespaces K8s · NetworkPolicies · ResourceQuotas · Priority + preemption&lt;/text>
&lt;rect x="40" y="345" width="700" height="55" class="layer obs"/>
&lt;text x="390" y="369" text-anchor="middle" class="lbl">Capa 4 · Observability Plane (multi-tenant)&lt;/text>
&lt;text x="55" y="391" class="sm">Traces con tenant_id · Métricas labeled · Cost attribution · Dashboards por tenant · Audit logs&lt;/text>
&lt;path class="arr" d="M90,90 L390,120"/>
&lt;path class="arr" d="M450,90 L390,120"/>
&lt;path class="arr" d="M690,90 L390,120"/>
&lt;/svg>
&lt;/div>
&lt;p>Cada capa resuelve un problema concreto. Vamos a una por una.&lt;/p>
&lt;h2 id="capa-1--ai-gateway-la-puerta-de-entrada-única">Capa 1 — AI Gateway: la puerta de entrada única&lt;/h2>
&lt;p>El &lt;strong>AI Gateway&lt;/strong> es el componente que tus tenants ven. Es una API HTTP/gRPC compatible con OpenAI (típicamente &lt;code>/v1/chat/completions&lt;/code>, &lt;code>/v1/embeddings&lt;/code>, &lt;code>/v1/models&lt;/code>) que &lt;strong>centraliza&lt;/strong> todo lo que pasa antes de tocar los backends de inferencia.&lt;/p>
&lt;h3 id="por-qué-centralizar">Por qué centralizar&lt;/h3>
&lt;p>Sin gateway, los tenants se conectan directamente a vLLM o al modelo que sea. Cada cambio (rotar un endpoint, añadir un modelo, cambiar credenciales, aplicar política) requiere notificar a todos los tenants. Cada tenant tiene su propia lógica de retry, su propio logging, su propio modelo de auth. Es inoperable a partir del tercer cliente.&lt;/p>
&lt;p>Con gateway, &lt;strong>el cambio se hace en un sitio&lt;/strong>. Los tenants tienen una URL estable y unas credenciales; el resto es problema del gateway.&lt;/p>
&lt;h3 id="las-tres-opciones-dominantes-2026">Las tres opciones dominantes 2026&lt;/h3>
&lt;p>&lt;strong>&lt;a href="https://docs.litellm.ai/">LiteLLM&lt;/a>&lt;/strong> es la opción &lt;strong>OSS más popular&lt;/strong>, Python-first, modelo de despliegue como proxy. Soporta &lt;strong>100+ proveedores&lt;/strong> (OpenAI, Anthropic, Bedrock, vLLM self-hosted, Ollama, etc.) detrás de una API OpenAI-compatible unificada. &lt;strong>Hierarchy nativa multi-tenant&lt;/strong> con Organizations → Teams → Users → API Keys, cada nivel con budget independiente. Versión Apache 2.0 cubre lo básico; &lt;strong>RBAC, SSO, audit logs y team-level enforcement requieren versión Enterprise paga&lt;/strong>. Despliegue en K8s con Helm chart oficial.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://portkey.ai/">Portkey&lt;/a>&lt;/strong> es la opción &lt;strong>comercial / SaaS&lt;/strong> más madura. Single control plane que enforces budgets, quotas, permissions, compliance. &lt;strong>Real-time spending tracking&lt;/strong> con alerting. RBAC, audit, workspaces, SSO incluidos. Trade-off: dependencia de un servicio externo y modelo de pricing por requests.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://konghq.com/blog/enterprise/llm-cost-management-ai-showback-and-chargeback">Kong AI Gateway&lt;/a>&lt;/strong> es la opción para organizaciones &lt;strong>que ya tienen Kong como API gateway&lt;/strong>. Plug-in AI sobre el gateway Kong existente, integra con su modelo de plugins, consumers y rate-limits. Si tu equipo de plataforma ya opera Kong, es la fricción más baja.&lt;/p>
&lt;h3 id="cuándo-elegir-cada-uno">Cuándo elegir cada uno&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Situación&lt;/th>
&lt;th>Gateway&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>OSS puro, self-host, equipo Python-first&lt;/td>
&lt;td>&lt;strong>LiteLLM&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Necesitas RBAC, SSO, audit log out-of-the-box, presupuesto disponible&lt;/td>
&lt;td>&lt;strong>Portkey&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ya operas Kong como API gateway corporativo&lt;/td>
&lt;td>&lt;strong>Kong AI Gateway&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Greenfield enterprise con compliance estricto&lt;/td>
&lt;td>Portkey (probablemente)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Empresa media OSS-first sin compliance regulado&lt;/td>
&lt;td>LiteLLM (típicamente)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="lo-que-el-gateway-tiene-que-hacer-mínimo">Lo que el gateway tiene que hacer mínimo&lt;/h3>
&lt;p>Independientemente de la opción, lo que cualquier deployment serio debe enforcer:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Auth y identidad&lt;/strong>: cada request lleva una API key resoluble a un tenant + usuario + equipo.&lt;/li>
&lt;li>&lt;strong>Routing por modelo&lt;/strong>: el tenant pide &lt;code>model: &amp;quot;gpt-4o&amp;quot;&lt;/code>; el gateway decide si va a OpenAI, a Azure OpenAI, a tu vLLM con Qwen3 32B (fallback más barato), según política.&lt;/li>
&lt;li>&lt;strong>Rate limiting&lt;/strong>: RPS por tenant, TPM (tokens por minuto), concurrency limits.&lt;/li>
&lt;li>&lt;strong>Caching de respuestas idénticas&lt;/strong>: 5-30% de las queries de RAG son repetidas; cachear ahorra latencia y coste.&lt;/li>
&lt;li>&lt;strong>OTel emission&lt;/strong>: cada llamada produce un span con &lt;code>gen_ai.*&lt;/code> semantic conventions y &lt;code>tenant_id&lt;/code> como atributo. Cubierto en &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">post de Evals&lt;/a> y &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Failover&lt;/strong>: si vLLM se cae, el gateway redirige a OpenAI API. Si OpenAI rate-limita, el gateway tira a Anthropic. Política configurable.&lt;/li>
&lt;/ul>
&lt;h3 id="ejemplo-de-configuración-litellm-multi-tenant">Ejemplo de configuración LiteLLM multi-tenant&lt;/h3>
&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="c"># litellm-config.yaml — ejemplo simplificado&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">model_list&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">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama-3-70b&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">litellm_params&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">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/llama-3-70b&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">api_base&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://vllm-llama3-70b.inference/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">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/VLLM_API_KEY&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">qwen3-32b&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">litellm_params&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">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/qwen3-32b&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">api_base&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://vllm-qwen3-32b.inference/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">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/VLLM_API_KEY&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o&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">litellm_params&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">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/gpt-4o&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">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/OPENAI_API_KEY&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">router_settings&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">routing_strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">usage-based-routing-v2&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">fallbacks&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">llama-3-70b&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">qwen3-32b, gpt-4o] &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># si vLLM cae, fallback al externo&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">general_settings&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">master_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/LITELLM_MASTER_KEY&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">database_url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/DATABASE_URL &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Postgres para budgets/keys&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Hierarchy: Organizations → Teams → Users → API Keys&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="c"># Se crean vía API, no en YAML estático&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Crear un team con presupuesto:&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">curl -X POST http://litellm/team/new &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">LITELLM_MASTER_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;team_alias&amp;#34;: &amp;#34;soporte-chat&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_budget&amp;#34;: 500, # 500 USD/mes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;budget_duration&amp;#34;: &amp;#34;30d&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;tpm_limit&amp;#34;: 100000, # 100K tokens/min
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;rpm_limit&amp;#34;: 1000, # 1000 requests/min
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;models&amp;#34;: [&amp;#34;llama-3-70b&amp;#34;, &amp;#34;qwen3-32b&amp;#34;] # acceso a estos
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Y la API key del team:&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">curl -X POST http://litellm/key/generate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">LITELLM_MASTER_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;team_id&amp;#34;: &amp;#34;&amp;lt;team-id&amp;gt;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;duration&amp;#34;: &amp;#34;30d&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;metadata&amp;#34;: {&amp;#34;environment&amp;#34;: &amp;#34;production&amp;#34;, &amp;#34;app&amp;#34;: &amp;#34;support-bot&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esa API key es lo que el tenant usa. Cada request que pase con ella consumirá del budget del team. Cuando se agote, LiteLLM devuelve &lt;strong>HTTP 429&lt;/strong> con descripción.&lt;/p>
&lt;h2 id="capa-2--policy--quota-plane-qué-puede-hacer-cada-tenant">Capa 2 — Policy &amp;amp; Quota Plane: qué puede hacer cada tenant&lt;/h2>
&lt;p>El gateway es donde se enforza. La política es &lt;strong>lo que se enforza&lt;/strong>. Cinco ejes de política multi-tenant:&lt;/p>
&lt;h3 id="quotas-técnicas">Quotas técnicas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>TPM&lt;/strong> (tokens por minuto): el límite duro de consumo. Para un Llama 3 70B en TP=5, ~3000 tokens/s salidos sostenidos = 180K TPM agregados. Si tienes 10 tenants, asignar 18K cada uno como techo.&lt;/li>
&lt;li>&lt;strong>RPS / RPM&lt;/strong>: control de carga, no de consumo. Una sesión de 4K tokens cuenta como una request; un batch de 100 mini-completions también. Útil contra abuso.&lt;/li>
&lt;li>&lt;strong>Concurrency&lt;/strong>: cuántas requests simultáneas activas por tenant. Importante para SLA de latencia: 100 RPS con concurrency=50 se traducen en 2 segundos por request.&lt;/li>
&lt;/ul>
&lt;h3 id="budgets-económicos">Budgets económicos&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Mensual por tenant&lt;/strong>: hard cap en USD.&lt;/li>
&lt;li>&lt;strong>Diario y por hora&lt;/strong>: soft caps para evitar runaway en un solo día.&lt;/li>
&lt;li>&lt;strong>Por proyecto / API key&lt;/strong>: granularidad fina dentro de un mismo tenant.&lt;/li>
&lt;/ul>
&lt;p>LiteLLM tiene un campo &lt;code>max_budget&lt;/code> en cada nivel de la jerarquía (organization, team, user, api key). Los presupuestos se heredan/restringen hacia abajo.&lt;/p>
&lt;h3 id="whitelist-y-blacklist-de-modelos">Whitelist y blacklist de modelos&lt;/h3>
&lt;p>Tenants con cargas críticas → solo modelos estables (&lt;code>llama-3-70b&lt;/code>, &lt;code>gpt-4o&lt;/code>). Tenants de investigación → acceso también a modelos experimentales.&lt;/p>
&lt;h3 id="priority-classes">Priority classes&lt;/h3>
&lt;p>No todos los requests son iguales. Tres clases típicas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Guaranteed&lt;/strong>: cargas con SLA, latencia respetada incluso bajo presión.&lt;/li>
&lt;li>&lt;strong>Best-effort&lt;/strong>: cargas normales sin SLA estricto.&lt;/li>
&lt;li>&lt;strong>Spot&lt;/strong>: batches que pueden esperar, evictable si llega un guaranteed.&lt;/li>
&lt;/ul>
&lt;p>El &lt;a href="https://arxiv.org/abs/2603.00356">paper Token Management in Multi-Tenant AI Inference Platforms&lt;/a> (2026) formaliza esto con un &lt;strong>modelo de token pools por priority class&lt;/strong> que se ha empezado a adoptar en producción. Mantiene &lt;strong>P99 latency garantizada&lt;/strong> para guaranteed workloads incluso bajo overload, throttling selectivo sobre spot.&lt;/p>
&lt;h3 id="admission-control">Admission control&lt;/h3>
&lt;p>Antes de aceptar una request: ¿hay capacidad? Si no, devolver 429 inmediatamente en vez de encolar y degradar a todos. Es la disciplina operacional más infravalorada — un cluster con admission control bien hecho tiene &lt;strong>latencia predecible&lt;/strong>; sin él, &lt;strong>catastrophic degradation&lt;/strong> cuando llega el pico.&lt;/p>
&lt;h3 id="el-patrón-típico-en-2026">El patrón típico en 2026&lt;/h3>
&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="c"># Política conceptual para un tenant &amp;#34;soporte-chat&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">tenant&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">soporte-chat&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">quotas&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">tpm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50000&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">rpm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">500&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">max_concurrency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&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">budget&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">monthly_usd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">800&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">alert_thresholds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="m">0.5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.8&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.95&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># avisa cuando llegues&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">models_allowed&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="l">llama-3-70b&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="l">qwen3-32b&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">priority&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">guaranteed&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">fallback_on_overload&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="l">qwen3-32b &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># si guaranteed se llena, fallback&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="l">gpt-4o-mini &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># último recurso, modelo externo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="capa-3--isolation-plane-aislar-las-cargas-físicamente">Capa 3 — Isolation Plane: aislar las cargas físicamente&lt;/h2>
&lt;p>Esta es la capa más densa técnicamente. Tienes un nodo con varias GPUs H100 SXM interconectadas por NVLink. ¿Cómo las particionas entre tenants?&lt;/p>
&lt;h3 id="tres-mecanismos-nvidia-para-compartir-gpu">Tres mecanismos NVIDIA para compartir GPU&lt;/h3>
&lt;p>&lt;strong>MIG (Multi-Instance GPU)&lt;/strong> es el aislamiento más fuerte. Particiona la GPU en hasta &lt;strong>7 instancias&lt;/strong> con &lt;strong>memoria HBM separada físicamente&lt;/strong> y &lt;strong>compute units (SMs) dedicados&lt;/strong>. Los tenants en MIG diferentes no pueden tocarse: una carga no consume memoria que otra necesita, una no degrada el throughput de otra. &lt;strong>Aislamiento hardware&lt;/strong>. Disponible en A100, H100, B100, B200.&lt;/p>
&lt;p>&lt;strong>MPS (Multi-Process Service)&lt;/strong> es soft. Múltiples procesos comparten la GPU concurrentemente, NVIDIA reparte SMs según uso. Buen rendimiento si todos los procesos son tuyos y confías en ellos. Peor para multi-tenant entre clientes que no se conocen porque un proceso ruidoso puede degradar a los otros.&lt;/p>
&lt;p>&lt;strong>Time-slicing&lt;/strong> es lo más simple: la GPU se asigna alternadamente, slot por slot, a procesos distintos. Latencia mucho peor (waits entre slots); no se recomienda para cargas de producción con SLA.&lt;/p>
&lt;h3 id="la-elección-para-multi-tenant-2026">La elección para multi-tenant 2026&lt;/h3>
&lt;p>Según el survey de adopción enterprise: &lt;strong>80% usa MIG para multi-tenant no confiable&lt;/strong> (clientes distintos que no se conocen) y &lt;strong>MPS para entornos confiados&lt;/strong> (procesos del mismo equipo) donde quieres maximizar throughput. Time-slicing solo se usa en dev/staging para que cada developer toque GPU sin coste de exclusividad.&lt;/p>
&lt;p>Limitación importante de MIG: &lt;strong>aísla compute y memoria HBM&lt;/strong>, pero &lt;strong>el camino PCIe sigue siendo compartido&lt;/strong>. Para cargas PCIe-bound (mucho tráfico host↔device), tenants en MIG distintos pueden seguir afectándose. Para inferencia LLM, el path principal es HBM, así que esto rara vez es problema. Pero conviene saberlo.&lt;/p>
&lt;h3 id="las-particiones-mig-en-h100">Las particiones MIG en H100&lt;/h3>
&lt;p>Una H100 (80GB HBM3) se puede particionar en perfiles fijos:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Perfil&lt;/th>
&lt;th>SM&lt;/th>
&lt;th>Memoria&lt;/th>
&lt;th>Instancias máx por GPU&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1g.10gb&lt;/td>
&lt;td>14&lt;/td>
&lt;td>10 GB&lt;/td>
&lt;td>7&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1g.20gb&lt;/td>
&lt;td>14&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2g.20gb&lt;/td>
&lt;td>28&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3g.40gb&lt;/td>
&lt;td>42&lt;/td>
&lt;td>40 GB&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7g.80gb&lt;/td>
&lt;td>98&lt;/td>
&lt;td>80 GB&lt;/td>
&lt;td>1 (toda la GPU)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Para un cluster mid-scale con NVLink, &lt;strong>MIG tiene un problema fundamental&lt;/strong>: cuando particionas con MIG, &lt;strong>se desactiva el NVLink entre GPUs&lt;/strong>. Una H100 en MIG &lt;strong>no&lt;/strong> participa en tensor parallel multi-GPU. Si vas a servir un modelo grande con tensor parallel (Llama 3 70B con TP=4 o TP=8, por ejemplo), esas GPUs deben estar enteras, sin MIG.&lt;/p>
&lt;p>Esto define la decisión arquitectónica. Hay dos enfoques principales:&lt;/p>
&lt;h3 id="enfoque-a--modelo-grande-compartido-con-quotas-en-gateway">Enfoque A — Modelo grande compartido con quotas en gateway&lt;/h3>
&lt;p>Todas las GPUs del nodo sirven &lt;strong>un único modelo grande con tensor parallel&lt;/strong> que abarca el nodo entero. Todos los tenants comparten esa instancia. El aislamiento se hace en la capa de gateway (quotas, rate limiting) y la capa de policy (priority classes). El kernel del cluster es una sola instancia vLLM enorme con &lt;code>--max-num-seqs=128&lt;/code> o similar; vLLM internamente reparte tiempo de GPU entre las requests activas con continuous batching.&lt;/p>
&lt;p>&lt;strong>Ventajas&lt;/strong>: aprovechas todas las GPUs al máximo, NVLink activo, mejor utilización del KV cache.
&lt;strong>Desventajas&lt;/strong>: aislamiento blando — un tenant que satura no degrada a otros directamente (vLLM bachea), pero sí compite por slots del batch. Necesitas priority classes serias.&lt;/p>
&lt;h3 id="enfoque-b--dedicar-gpus-por-modelo--tenant">Enfoque B — Dedicar GPUs por modelo / tenant&lt;/h3>
&lt;p>Divides las GPUs en pools dedicados a modelos distintos. Ejemplos en un nodo de 8 GPUs:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>4 GPUs&lt;/strong>: modelo grande de 70B con TP=4.&lt;/li>
&lt;li>&lt;strong>2 GPUs&lt;/strong>: modelo mediano de 32B replicado (2 instancias independientes) para tenants con SLA estricto.&lt;/li>
&lt;li>&lt;strong>2 GPUs&lt;/strong>: cargas misceláneas (modelos más pequeños, experimentación).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Ventajas&lt;/strong>: aislamiento físico entre modelos / tenants críticos.
&lt;strong>Desventajas&lt;/strong>: peor utilización agregada; algunas GPUs idle mientras otras saturan.&lt;/p>
&lt;h3 id="enfoque-c-avanzado--mig-en-algunas-gpus--dedicar-el-resto">Enfoque C (avanzado) — MIG en algunas GPUs + dedicar el resto&lt;/h3>
&lt;p>Si tienes cargas pequeñas (modelos de 4B, 7B), puedes hacer MIG en 1-2 GPUs para servirlas y dedicar las restantes a tensor parallel del modelo grande. Combina aislamiento fuerte para cargas chicas con aprovechamiento del NVLink para el modelo grande.&lt;/p>
&lt;h3 id="la-elección-operativa-empieza-por-a-sube-a-c-si-hace-falta">La elección operativa: empieza por A, sube a C si hace falta&lt;/h3>
&lt;p>En la mayoría de despliegues, el Enfoque A (modelo grande compartido + quotas) es el punto de partida correcto. La utilización es mejor, la operación es más simple, y los aislamientos blandos del gateway funcionan para cargas razonables.&lt;/p>
&lt;p>Cuando hay un tenant con SLA estricto que no tolera competir con otros, mueves a Enfoque B para ese tenant en particular (dedicar GPUs a una instancia del modelo solo para él), manteniendo el resto del cluster compartido.&lt;/p>
&lt;p>Enfoque C es para cuando tienes 10+ tenants con perfiles muy heterogéneos.&lt;/p>
&lt;h3 id="aislamiento-a-nivel-kubernetes">Aislamiento a nivel Kubernetes&lt;/h3>
&lt;p>Independiente del aislamiento GPU, en K8s se aplica aislamiento de pod:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Namespaces por tenant&lt;/strong>: &lt;code>tenant-soporte&lt;/code>, &lt;code>tenant-legal&lt;/code>, etc.&lt;/li>
&lt;li>&lt;strong>ResourceQuotas y LimitRanges&lt;/strong>: límites de CPU/memoria por namespace.&lt;/li>
&lt;li>&lt;strong>NetworkPolicies&lt;/strong>: tenant A no puede hablar con namespaces de tenant B.&lt;/li>
&lt;li>&lt;strong>PriorityClasses K8s&lt;/strong>: clases con valor numérico que define preemption order si llega un pod más crítico.&lt;/li>
&lt;li>&lt;strong>PodDisruptionBudgets&lt;/strong>: cuántos pods de cada deployment pueden caer simultáneamente.&lt;/li>
&lt;/ul>
&lt;h2 id="capa-4--observability-plane-ver-lo-que-pasa-por-tenant">Capa 4 — Observability Plane: ver lo que pasa por tenant&lt;/h2>
&lt;p>La cuarta capa: &lt;strong>observabilidad con dimensión tenant&lt;/strong>. Sin esto, no puedes hacer cost attribution, no puedes debugear incidentes de un solo tenant, no puedes mostrar dashboards a stakeholders.&lt;/p>
&lt;h3 id="las-cuatro-propiedades-obligatorias">Las cuatro propiedades obligatorias&lt;/h3>
&lt;p>&lt;strong>1. tenant_id en todos los spans&lt;/strong>. El AI gateway resuelve la API key y atribuye un &lt;code>tenant_id&lt;/code>. Ese ID &lt;strong>se propaga&lt;/strong> vía &lt;code>params._meta&lt;/code> o headers OTel a todos los componentes downstream (vLLM, retrieval, MCP servers, tools). Cualquier span en cualquier sistema lleva ese label. Es lo que permite reconstruir traces tenant-específicos.&lt;/p>
&lt;p>&lt;strong>2. Métricas labeled por tenant&lt;/strong>. &lt;code>gen_ai.usage.input_tokens{tenant=&amp;quot;soporte-chat&amp;quot;}&lt;/code> o equivalentes. Prometheus, Grafana, agrupable por tenant.&lt;/p>
&lt;p>&lt;strong>3. Cost attribution real&lt;/strong>. La suma de tokens × cost/token por tenant da el coste. Para vLLM self-hosted, el coste es por hora de GPU + parte proporcional de tokens (puedes calcular un cost-per-1k-tokens equivalente).&lt;/p>
&lt;p>&lt;strong>4. Audit log inmutable&lt;/strong>. Cada API key usada, cada modelo invocado, cada cambio de quota, cada budget exceeded. Para compliance.&lt;/p>
&lt;h3 id="showback-vs-chargeback">Showback vs chargeback&lt;/h3>
&lt;p>Distinción importante de FinOps que ha ganado claridad en 2026:&lt;/p>
&lt;p>&lt;strong>Showback&lt;/strong>: visibilidad sin consecuencia. &amp;ldquo;Equipo de soporte, has consumido $623 este mes en LLM&amp;rdquo;. Información, no factura. Permite detectar abusos sin penalizar antes de que el equipo entienda.&lt;/p>
&lt;p>&lt;strong>Chargeback&lt;/strong>: el coste se imputa al presupuesto del equipo. Cuando se acaba, se acaba. Cambia comportamiento.&lt;/p>
&lt;p>La práctica que funciona: &lt;strong>6-18 meses en showback&lt;/strong> mientras se calibran tags, se identifican misattributions, se forma a los equipos. &lt;strong>Después chargeback&lt;/strong> cuando los números son creíbles. Lanzar chargeback el día 1 cuando los costs aún están sucios crea pelea política inmediata; lanzar showback prepara terreno para que el chargeback aterrice ordenadamente.&lt;/p>
&lt;p>&lt;a href="https://spendark.com/blog/kubernetes-cost-allocation/">Solo 14% de organizaciones tienen chargeback activo&lt;/a> según un survey reciente, lo que indica que esto sigue siendo mayoritariamente showback en producción real.&lt;/p>
&lt;h3 id="herramientas">Herramientas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://www.kubecost.com/">Kubecost&lt;/a>&lt;/strong>: cost allocation por namespace, deployment, pod en Kubernetes. Para el coste de la GPU compartida, allocate proporcionalmente a tokens consumidos por tenant.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.finout.io/">Finout&lt;/a>&lt;/strong>: FinOps platform que combina cloud bills + LLM API costs en una vista unificada con tagging virtual.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://langfuse.com/">Langfuse&lt;/a>&lt;/strong>: ya cubierto. Cost tracking por trace, agrupable por usuario o session metadata.&lt;/li>
&lt;li>&lt;strong>LiteLLM tracking nativo&lt;/strong>: el master DB de LiteLLM mantiene running spend por team, user, API key, accesible vía API o UI.&lt;/li>
&lt;/ul>
&lt;h3 id="dashboard-mínimo-multi-tenant">Dashboard mínimo multi-tenant&lt;/h3>
&lt;p>Cualquier plataforma debería tener:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Resumen por tenant&lt;/strong>: spend mensual, RPS actual, TPM consumido, % budget gastado, sesiones activas.&lt;/li>
&lt;li>&lt;strong>Top usuarios&lt;/strong> dentro de cada tenant (para detección de abuso interno).&lt;/li>
&lt;li>&lt;strong>Latencia p95 por tenant&lt;/strong>: SLA tracking.&lt;/li>
&lt;li>&lt;strong>Errores 429 / 503&lt;/strong>: cuántas requests están siendo rate-limitadas o rechazadas por overload.&lt;/li>
&lt;li>&lt;strong>Cost trend&lt;/strong>: trayectoria mensual con proyección.&lt;/li>
&lt;li>&lt;strong>Drift por tenant&lt;/strong> (de la serie post-tracing): si un tenant empieza a tener peores resultados, alerta.&lt;/li>
&lt;/ol>
&lt;h2 id="dimensionado-en-clusters-gpu-mid-scale-decisiones-concretas">Dimensionado en clusters GPU mid-scale: decisiones concretas&lt;/h2>
&lt;p>Bajemos a hardware. Tomamos como referencia un nodo con &lt;strong>N H100 SXM (entre 4 y 8) con NVLink/NVSwitch&lt;/strong>, 80 GB HBM3 cada una. Eso da entre &lt;strong>320 GB y 640 GB de VRAM agregada&lt;/strong>. Conectividad inter-GPU 900 GB/s (NVLink 4) o 600 GB/s (NVLink 3) según generación. Ancho de banda HBM por GPU 3.35 TB/s.&lt;/p>
&lt;h3 id="decisiones-por-defecto">Decisiones por defecto&lt;/h3>
&lt;p>Empezar con &lt;strong>Enfoque A&lt;/strong>: todas las GPUs del nodo sirviendo &lt;strong>un único modelo grande de 70B en BF16 con tensor parallel = N&lt;/strong>. Capacidad real esperada (calculada para un nodo HGX estándar de 8 GPUs como ejemplo; escala aproximadamente lineal con N):&lt;/p>
&lt;ul>
&lt;li>VRAM modelo (70B BF16): ~140 GB (≈ 17.5 GB/GPU en TP=8).&lt;/li>
&lt;li>VRAM overhead vLLM + activations: ~10 GB/GPU.&lt;/li>
&lt;li>VRAM libre para KV cache: ~52 GB/GPU. En un nodo de 8 GPUs son &lt;strong>~416 GB agregados&lt;/strong>; en uno de 4 son ~210 GB.&lt;/li>
&lt;li>Con &lt;code>--kv-cache-dtype=fp8&lt;/code> y un modelo 70B GQA: ~320 KB/token.&lt;/li>
&lt;li>Capacidad agregada de cache (nodo de 8 GPUs): &lt;strong>~1.3M tokens&lt;/strong> repartibles entre sesiones simultáneas.&lt;/li>
&lt;/ul>
&lt;p>Esto se traduce en throughput y concurrencia (cifras orientativas para un nodo de 8 GPUs):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Sesiones simultáneas&lt;/th>
&lt;th style="text-align:right">Contexto medio por sesión&lt;/th>
&lt;th style="text-align:right">Throughput agregado (tokens/s)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">16K&lt;/td>
&lt;td style="text-align:right">~5000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">64&lt;/td>
&lt;td style="text-align:right">8K&lt;/td>
&lt;td style="text-align:right">~8000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">4K&lt;/td>
&lt;td style="text-align:right">~12000&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Latencias típicas: &lt;strong>TTFT ~150ms&lt;/strong> a tráfico bajo, &lt;strong>TPOT ~15-20 ms/tok&lt;/strong>. Con concurrencia alta, TTFT sube hasta ~500ms si el queue está saturado.&lt;/p>
&lt;h3 id="esquema-de-tenants-ejemplo">Esquema de tenants ejemplo&lt;/h3>
&lt;p>Cluster con 4 tenants y un pool de research:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tenant&lt;/th>
&lt;th style="text-align:right">TPM cap&lt;/th>
&lt;th style="text-align:right">RPM cap&lt;/th>
&lt;th style="text-align:right">Concurrency&lt;/th>
&lt;th style="text-align:right">Budget&lt;/th>
&lt;th>Priority&lt;/th>
&lt;th>Modelos&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Soporte chat&lt;/td>
&lt;td style="text-align:right">80K&lt;/td>
&lt;td style="text-align:right">800&lt;/td>
&lt;td style="text-align:right">50&lt;/td>
&lt;td style="text-align:right">1500 USD/mes&lt;/td>
&lt;td>Guaranteed&lt;/td>
&lt;td>llama-3-70b, qwen3-32b&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Legal RAG&lt;/td>
&lt;td style="text-align:right">30K&lt;/td>
&lt;td style="text-align:right">200&lt;/td>
&lt;td style="text-align:right">15&lt;/td>
&lt;td style="text-align:right">600 USD/mes&lt;/td>
&lt;td>Guaranteed&lt;/td>
&lt;td>llama-3-70b&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agente code&lt;/td>
&lt;td style="text-align:right">50K&lt;/td>
&lt;td style="text-align:right">300&lt;/td>
&lt;td style="text-align:right">25&lt;/td>
&lt;td style="text-align:right">1200 USD/mes&lt;/td>
&lt;td>Best-effort&lt;/td>
&lt;td>llama-3-70b, qwen-coder&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Data extr. batch&lt;/td>
&lt;td style="text-align:right">40K&lt;/td>
&lt;td style="text-align:right">1000&lt;/td>
&lt;td style="text-align:right">40&lt;/td>
&lt;td style="text-align:right">400 USD/mes&lt;/td>
&lt;td>Spot&lt;/td>
&lt;td>llama-3-70b, qwen3-32b&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Research / notebooks&lt;/td>
&lt;td style="text-align:right">10K&lt;/td>
&lt;td style="text-align:right">100&lt;/td>
&lt;td style="text-align:right">5&lt;/td>
&lt;td style="text-align:right">200 USD/mes&lt;/td>
&lt;td>Spot&lt;/td>
&lt;td>todos&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Suma TPM: 210K. Capacidad agregada del cluster: ~180K TPM sostenidos. &lt;strong>Está overcommit del ~15%&lt;/strong>, asumiendo que no todos los tenants llegan al techo simultáneamente. Es lo normal y deseable; si todos lo hacen al mismo tiempo, las priority classes degradan ordenadamente.&lt;/p>
&lt;h3 id="cuándo-añadir-hardware">Cuándo añadir hardware&lt;/h3>
&lt;p>Señales que indican que el nodo se ha quedado pequeño:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TTFT p95 sostenida &amp;gt; 500 ms&lt;/strong> durante horas de pico → el queue se está acumulando.&lt;/li>
&lt;li>&lt;strong>&lt;code>vllm:num_requests_waiting&lt;/code> constantemente &amp;gt; 20&lt;/strong> → admission control empezando a rechazar.&lt;/li>
&lt;li>&lt;strong>Utilización GPU sostenida &amp;gt; 80% en horas críticas&lt;/strong> sin caer abajo en horas valle → no hay margen.&lt;/li>
&lt;li>&lt;strong>Tasa de 429 sobre los tenants guaranteed &amp;gt; 1%&lt;/strong> → la plataforma rompe SLA en producción.&lt;/li>
&lt;/ul>
&lt;p>Cuando varios de estos se cumplan, el siguiente paso natural es añadir otro nodo HGX con NVLink interno y montar &lt;strong>una segunda instancia vLLM&lt;/strong> del mismo modelo. El gateway hace load balancing entre las dos instancias. Throughput agregado se duplica; latencia se mantiene.&lt;/p>
&lt;h2 id="trampas-operativas-comunes">Trampas operativas comunes&lt;/h2>
&lt;h3 id="gateway-sin-auth-backdoor-al-cluster">Gateway sin auth: backdoor al cluster&lt;/h3>
&lt;p>Tu vLLM está en un Service ClusterIP, la app principal habla con él. Algún tenant directo descubre el endpoint y le pega directamente sin pasar por el gateway. Quotas y costs se evaden silenciosamente. &lt;strong>NetworkPolicy estricta&lt;/strong>: solo el gateway puede hablar con los Service vLLM; el resto del cluster no.&lt;/p>
&lt;h3 id="mig-y-nvlink-incompatibles">MIG y NVLink incompatibles&lt;/h3>
&lt;p>Activas MIG en una GPU pensando que tendrás aislamiento + multi-GPU; descubres que MIG desactiva NVLink. Cualquier modelo grande con TP queda inservible. &lt;strong>Decide MIG vs NVLink globalmente por cluster&lt;/strong>, no por GPU individual.&lt;/p>
&lt;h3 id="quotas-pegadas-al-techo-del-cluster">Quotas pegadas al techo del cluster&lt;/h3>
&lt;p>Sumas los TPM de todos los tenants y dan exactamente la capacidad del cluster. Cuando dos tenants pico simultáneamente, ambos esperan o uno rechaza. &lt;strong>Overcommit 10-20%&lt;/strong> es saludable (asume que no todos pican a la vez); más es peligroso.&lt;/p>
&lt;h3 id="sin-observabilidad-multi-tenant-desde-el-día-1">Sin observabilidad multi-tenant desde el día 1&lt;/h3>
&lt;p>Lanzas con quotas y aislamiento pero sin tenant_id en spans. A los 3 meses, tu CFO pregunta &amp;ldquo;¿cuánto cuesta el agente de soporte vs el de legal?&amp;rdquo; y no puedes responder. &lt;strong>OTel con tenant_id obligatorio desde la primera versión&lt;/strong>, aunque no haya dashboards aún; tener los datos vale más que tener dashboards perfectos sin datos.&lt;/p>
&lt;h3 id="showback-que-nunca-llega-a-chargeback">Showback que nunca llega a chargeback&lt;/h3>
&lt;p>Llevas 18 meses en showback, los equipos saben los números, nadie cambia comportamiento. Sin la presión del chargeback real, el incentivo se diluye. &lt;strong>Calendario explícito&lt;/strong> para la transición a chargeback, con dueño y deadline.&lt;/p>
&lt;h3 id="modelos-no-whitelisteados-consumiendo-presupuesto">Modelos no whitelisteados consumiendo presupuesto&lt;/h3>
&lt;p>Un equipo descubre que LiteLLM tiene &lt;code>gpt-4o&lt;/code> configurado. Lo usa sin permiso. El budget se quema en API externa cuando la idea era usar el self-hosted barato. &lt;strong>Whitelist explícita por team de modelos accesibles&lt;/strong>.&lt;/p>
&lt;h3 id="priority-classes-mal-calibradas">Priority classes mal calibradas&lt;/h3>
&lt;p>Todo el mundo se declara &amp;ldquo;guaranteed&amp;rdquo;. En el primer pico, no queda nada por degradar y todo sufre. &lt;strong>Priority classes solo para casos críticos&lt;/strong> con justificación. La mayoría debería ser best-effort.&lt;/p>
&lt;h3 id="sin-failover-desde-el-gateway">Sin failover desde el gateway&lt;/h3>
&lt;p>Tu vLLM se cae. El gateway no tiene fallback configurado y devuelve 503 a todos los tenants. &lt;strong>Fallback configurado&lt;/strong> a otro modelo, idealmente externo (OpenAI) para cargas guaranteed, aunque pague más por hora — la disponibilidad vale más que el coste por hora.&lt;/p>
&lt;h2 id="roadmap-operativo-de-arranque">Roadmap operativo de arranque&lt;/h2>
&lt;p>Si parte de cero con un nodo GPU vacío, el orden mínimo es el siguiente. Cada hito es un día de trabajo con margen, no apretado:&lt;/p>
&lt;p>&lt;strong>Día 1-2 — Infra base K8s&lt;/strong>. NVIDIA GPU Operator + nvidia-device-plugin + dcgm-exporter + NetworkPolicies cluster-default. Validación: un pod básico con &lt;code>nvidia.com/gpu: 1&lt;/code> se schedulea.&lt;/p>
&lt;p>&lt;strong>Día 3 — vLLM con un modelo grande y tensor parallel del nodo entero&lt;/strong>. Helm chart de vLLM Production Stack (o vLLM bare manifests). Pesos del modelo en PVC compartido (CephFS o NFS). Validación: una petición &lt;code>curl&lt;/code> contra el Service interno responde.&lt;/p>
&lt;p>&lt;strong>Día 4 — AI Gateway: LiteLLM&lt;/strong>. Helm chart, Postgres para budgets, master key, primer model_list pointing a vLLM. Validación: una petición OpenAI-compatible vía LiteLLM responde con el mismo contenido que el vLLM directo.&lt;/p>
&lt;p>&lt;strong>Día 5 — Multi-tenancy básica&lt;/strong>. Crear teams, API keys, budget, model whitelist. Probar con dos teams. Validación: el segundo team usando el modelo que no tiene whitelisteado recibe 403.&lt;/p>
&lt;p>&lt;strong>Día 6 — Observabilidad mínima&lt;/strong>. Prometheus + Grafana scraping vLLM y LiteLLM. Dashboard con TTFT, TPOT, throughput, num_requests_waiting, budget_consumed_per_team. Validación: visible en Grafana con datos reales.&lt;/p>
&lt;p>&lt;strong>Día 7-8 — Cliente piloto&lt;/strong>. Un tenant real (idealmente uno interno controlado) empieza a usar. Mide latencias reales, descubre los primeros incidentes operativos.&lt;/p>
&lt;p>&lt;strong>Día 9-10 — Tuning&lt;/strong>. Ajustar &lt;code>--max-num-seqs&lt;/code>, &lt;code>--gpu-memory-utilization&lt;/code>, priority classes, quotas según lo aprendido del piloto.&lt;/p>
&lt;p>&lt;strong>Día 11-14 — Onboarding del segundo tenant + iteración&lt;/strong>. Repeat. Cada nuevo tenant onboarded revela nuevos casos.&lt;/p>
&lt;p>A las dos semanas tienes una plataforma operacional con dos tenants reales y datos para decidir si está lista para más. La línea de avance de aquí en adelante es &lt;strong>horizontal&lt;/strong> (más tenants) hasta saturar; a partir de ahí, &lt;strong>vertical&lt;/strong> (más hardware).&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-posts">Lo que no hemos cubierto (próximos posts)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Fine-tuning continuo en producción&lt;/strong> (post 6, decidido): LoRA/QLoRA/DPO, dataset curation, eval gates, A/B versioning con tráfico real entre versiones del modelo.&lt;/li>
&lt;li>&lt;strong>Constitutional AI y alignment runtime&lt;/strong>: opción que sigue en la mesa.&lt;/li>
&lt;li>&lt;strong>Edge LLMs&lt;/strong>: cuando un cluster H100 es demasiado caro para una carga concreta, modelos distillados corriendo en NPUs o GPUs consumer.&lt;/li>
&lt;li>&lt;strong>GPU networking deep dive&lt;/strong>: NCCL, InfiniBand, GPUDirect, RDMA. Para clusters multi-nodo con tensor parallel cross-host.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>Multi-tenancy y aislamiento GPU:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.linuxoperatingsystem.net/multitenant-gpu-infrastructure-4-powerful-design-rules/">Multitenant GPU Infrastructure: 4 Powerful Design Rules&lt;/a> — survey de patrones enterprise.&lt;/li>
&lt;li>&lt;a href="https://www.spheron.network/blog/run-multiple-llms-one-gpu-mig-time-slicing-guide/">Run Multiple LLMs on One GPU: MIG, Time-Slicing, and MPS Guide (Spheron)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://sagar-parmar.medium.com/a-practical-guide-to-gpu-partitioning-with-mig-on-on-prem-servers-and-kubernetes-797ccea7e1c7">A Practical Guide to GPU Partitioning with MIG (Medium)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.suse.com/c/kubecon-eu-2026-nvidia-mig-suse-virtualization/">GPU Partitioning for AI Workloads: NVIDIA MIG with SUSE Virtualization (KubeCon EU 2026)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2508.20274">Predictable LLM Serving on GPU Clusters (arxiv 2508.20274)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2603.00356">Token Management in Multi-Tenant AI Inference Platforms (arxiv 2603.00356)&lt;/a> — paper de priority + admission control.&lt;/li>
&lt;/ul>
&lt;p>AI Gateways:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://docs.litellm.ai/docs/proxy/multi_tenant_architecture">LiteLLM — Multi-Tenant Architecture&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://docs.litellm.ai/docs/proxy/users">LiteLLM — Budgets and Rate Limits&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://portkey.ai/">Portkey AI Gateway&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://konghq.com/blog/enterprise/llm-cost-management-ai-showback-and-chargeback">Kong AI Gateway — LLM Cost Management&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.spheron.network/blog/ai-gateway-litellm-portkey-kong-gpu-cloud/">AI Gateway Setup 2026: LiteLLM, Portkey, Kong (Spheron)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://techsy.io/en/blog/best-llm-gateway-tools">Stop Juggling LLM APIs: 8 Gateways Ranked 2026 (TECHSY)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>FinOps multi-tenant:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.digiusher.com/blog/the-death-of-cost-allocation-why-chargeback-models-are-failing-in-the-kubernetes-and-ai-era/">The Death of Chargeback in the Kubernetes and AI Era (DigiUsher)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://medium.com/@nicholasthoni/how-to-actually-track-kubernetes-costs-in-2026-a-practical-guide-to-showback-chargeback-and-the-6a4c23f9cf51">How to Actually Track Kubernetes Costs in 2026 (Medium)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://konghq.com/blog/enterprise/llm-cost-management-ai-showback-and-chargeback">LLM Cost Management: AI Showback and Chargeback (Kong)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.kubecost.com/">Kubecost — cost allocation&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.finout.io/">Finout — FinOps + AI costs&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references:&lt;/p>
&lt;ul>
&lt;li>Posts previos serie 4: &lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">Panorama MLOps LLMs&lt;/a>, &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">RAG sobre Kafka&lt;/a>, &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">Pipeline de 6 etapas&lt;/a>, &lt;a href="https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/">PostgreSQL + Qdrant&lt;/a>.&lt;/li>
&lt;li>Posts relevantes de la serie inferencia: &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes&lt;/a> — el escenario de nodo HGX multi-GPU que aquí desarrollamos. &lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators LLM K8s&lt;/a> — vLLM Production Stack y OME que el gateway puede dirigir.&lt;/li>
&lt;li>Observabilidad: &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals&lt;/a>, &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&lt;/a>, &lt;a href="https://blog.lo0.es/posts/ebpf-on-device-inference-drift/">eBPF + drift&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>Operators de inferencia LLM en Kubernetes: OME, vLLM Production Stack, NVIDIA Dynamo y llm-d</title><link>https://blog.lo0.es/posts/operators-llm-kubernetes/</link><pubDate>Mon, 18 May 2026 17:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/operators-llm-kubernetes/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Servir un LLM en producción no es ejecutar un binario: es coordinar &lt;strong>un modelo&lt;/strong> (decenas de gigabytes que tardan minutos en cargar), &lt;strong>un runtime&lt;/strong> (vLLM, SGLang, TensorRT-LLM con cien flags), &lt;strong>GPUs heterogéneas&lt;/strong> (NVLink, MIG, PCIe), &lt;strong>prefill y decode&lt;/strong> que viven mejor separados, &lt;strong>un cache de KV&lt;/strong> que quiere offloading a tiers más fríos, &lt;strong>routing inteligente&lt;/strong> que aproveche prefix caching, y &lt;strong>autoscaling&lt;/strong> que reaccione a métricas que no son CPU%. Un &lt;code>Deployment&lt;/code> plano de Kubernetes solo cubre el primer 20% de esto. El otro 80% lo cubren los &lt;strong>operators de inferencia LLM&lt;/strong>, que en 2026 son cuatro relevantes: &lt;strong>OME&lt;/strong> (LMSYS, julio 2025, multi-engine con foco en SGLang), &lt;strong>vLLM Production Stack&lt;/strong> (Helm chart curado del propio vLLM con LMCache para tiered KV), &lt;strong>NVIDIA Dynamo&lt;/strong> (sucesor oficial de Triton, multi-engine, scheduler propio Grove) y &lt;strong>llm-d&lt;/strong> (donación CNCF de marzo 2026 por Red Hat + Google + IBM + CoreWeave + NVIDIA, sobre vLLM, foco en escala distribuida). Detrás de los cuatro está &lt;strong>KServe&lt;/strong>, el operator madre del CNCF que normalizó el concepto de &lt;code>InferenceService&lt;/code> y sobre el que varios se apoyan. Este artículo recorre la jerarquía completa, da un mapa de decisión y enseña a no perderse cuando alguien suelte siete siglas en la primera reunión.&lt;/p>
&lt;blockquote>
&lt;p>Este artículo cierra la serie de inferencia LLM. Los anteriores fueron &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a>, &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes: la pieza de inferencia LLM que sí escala&lt;/a> y &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention por dentro y el estado del arte del KV cache en 2026&lt;/a>. Allí explicamos qué pasa &lt;strong>dentro de un proceso de inferencia&lt;/strong>. Aquí explicamos cómo se coordinan &lt;strong>muchos procesos de inferencia&lt;/strong> a través de Kubernetes.&lt;/p>
&lt;/blockquote>
&lt;h2 id="la-analogía-de-initd-a-systemd-a-operators">La analogía: de &lt;code>init.d&lt;/code> a systemd a operators&lt;/h2>
&lt;p>El que lleva 20 años en sysadmin reconocerá el patrón. Hace décadas, arrancar un servicio en Linux era un script shell en &lt;code>/etc/init.d/&lt;/code>: start, stop, status, recargado a mano. Cuando los servicios se hicieron más complejos —dependencias entre ellos, monitorización, restart on failure, slots por usuario— se hizo evidente que un script no bastaba. Llegó &lt;strong>systemd&lt;/strong>, que convirtió &amp;ldquo;un servicio&amp;rdquo; en una &lt;strong>unidad declarativa&lt;/strong> con dependencias, recursos, restart policy, sockets, timers. El script no desapareció; se subió un nivel de abstracción.&lt;/p>
&lt;p>Kubernetes hizo el mismo movimiento para servicios distribuidos. Un &lt;code>Deployment&lt;/code> declara &amp;ldquo;quiero N réplicas de este contenedor&amp;rdquo;; un &lt;code>Service&lt;/code> declara &amp;ldquo;estas réplicas se exponen así&amp;rdquo;; un &lt;code>Ingress&lt;/code> declara &amp;ldquo;este tráfico HTTP entra aquí&amp;rdquo;. El controller traduce la declaración en estado real y mantiene el sistema convergente.&lt;/p>
&lt;p>Servir LLMs en 2024 era el equivalente al &lt;code>/etc/init.d/&lt;/code>: cada equipo escribía sus &lt;code>Deployment&lt;/code>/&lt;code>Service&lt;/code>/&lt;code>HPA&lt;/code> con scripts customizados de carga de modelo, drenaje de sesiones, manejo de GPU. Lo cubrimos en el &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">artículo de vLLM en Kubernetes&lt;/a>: se puede hacer, y de hecho funciona, pero &lt;strong>es repetitivo, frágil y nadie está extrayendo las abstracciones correctas&lt;/strong>. Servir LLMs en 2026 ha vivido la misma transición que los servicios: ha aparecido el equivalente a systemd —los &lt;strong>operators de inferencia&lt;/strong>— que normalizan las abstracciones y dejan al ingeniero declarar lo importante: &amp;ldquo;este modelo, con este runtime, así de escalable, con esta política de routing&amp;rdquo;.&lt;/p>
&lt;p>Hay cuatro operators relevantes en 2026 y un quinto antecesor común. Vamos por orden.&lt;/p>
&lt;h2 id="por-qué-un-operator-y-no-solo-un-deployment">Por qué un operator, y no solo un Deployment&lt;/h2>
&lt;p>Listar lo que un operator de inferencia aporta sobre un Deployment plano es la mejor manera de entender qué problema resuelve:&lt;/p>
&lt;p>&lt;strong>Modelo como ciudadano de primera clase.&lt;/strong> En un Deployment, el modelo es &amp;ldquo;lo que descargas en un initContainer y montas como volumen&amp;rdquo;. En un operator, el modelo es una &lt;code>CustomResource&lt;/code> con metadatos (origen, fingerprint, licencia, GPU requirements). Pueden compartirse entre InferenceServices, versionarse, replicarse a múltiples nodos. Es la diferencia entre &amp;ldquo;un fichero&amp;rdquo; y &amp;ldquo;un artifact gestionado&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Runtime como ciudadano de primera clase.&lt;/strong> Idem para el runtime (vLLM/SGLang/TRT-LLM): no es &amp;ldquo;una imagen Docker con flags&amp;rdquo;; es una &lt;code>ServingRuntime&lt;/code> que declara qué args acepta, qué métricas exporta, qué tipos de despliegue soporta (single-node, multi-node TP, PD-disag). Cambiar de runtime es cambiar una referencia, no reescribir todos los manifests.&lt;/p>
&lt;p>&lt;strong>Composición declarativa.&lt;/strong> Una &lt;code>InferenceService&lt;/code> (CRD nuclear de KServe y descendientes) &lt;strong>referencia&lt;/strong> un modelo y un runtime, &lt;strong>declara&lt;/strong> la política de escalado, &lt;strong>enlaza&lt;/strong> observabilidad, &lt;strong>configura&lt;/strong> routing. El controller compone todas las piezas: Deployment(s), Service, HPA, eventualmente LeaderWorkerSet, ScaledObject de KEDA, HTTPRoute de Gateway API. Tú declaras intención; el operator emite los 8 recursos derivados.&lt;/p>
&lt;p>&lt;strong>Prefill–decode disaggregation operacional.&lt;/strong> Como vimos en el artículo de PagedAttention, separar prefill y decode en pools distintos puede dar 7× goodput. Modelar eso con Deployments planos es viable, pero requiere coordinar dos sets de pods, un transport para mover KV cache, routing condicional. Un operator lo modela como una sola &lt;code>InferenceService&lt;/code> con dos sub-pools.&lt;/p>
&lt;p>&lt;strong>Autoscaling con métricas LLM.&lt;/strong> El HPA estándar no entiende &lt;code>vllm:num_requests_waiting&lt;/code>. Un operator integra KEDA o Prometheus Adapter automáticamente y expone las métricas correctas como knobs del CRD.&lt;/p>
&lt;p>&lt;strong>Multi-tenancy.&lt;/strong> Múltiples modelos en el mismo cluster, con cuotas, prioridades y fairness. Un Deployment por modelo escalando independientemente está bien hasta el quinto modelo; a partir de ahí, la coordinación de GPUs entre tenants se vuelve operationally hostil.&lt;/p>
&lt;p>&lt;strong>Lifecycle del modelo.&lt;/strong> Pesos en PVC compartido, calentamiento del primer pod, rolling updates con &lt;code>maxUnavailable: 0&lt;/code>, drenaje de sesiones activas, observabilidad integrada. Cosas que en Deployment plano hay que reinventar en cada equipo.&lt;/p>
&lt;p>Si tu carga es &lt;strong>un modelo, un nodo, hasta tres réplicas&lt;/strong>, un Deployment plano basta y un operator es overkill. Si tu carga es &lt;strong>dos o más modelos, escalado serio, disaggregation o multi-tenancy&lt;/strong>, un operator deja de ser opcional.&lt;/p>
&lt;h2 id="kserve-el-antecesor-común">KServe: el antecesor común&lt;/h2>
&lt;p>Antes de los cuatro nuevos, hay que mencionar a &lt;a href="https://kserve.github.io/website/">KServe&lt;/a>, que es el operator madre del que descienden conceptualmente todos los demás. Nació como &lt;strong>KFServing&lt;/strong> dentro del proyecto Kubeflow en 2019, pasó a llamarse KServe al independizarse en 2021, y en 2025 fue &lt;a href="https://thenewstack.io/kserve-joins-cncf-to-standardize-ai-model-serving-on-kubernetes/">aceptado en la CNCF&lt;/a> como proyecto incubando hacia graduado.&lt;/p>
&lt;p>La contribución conceptual de KServe es &lt;strong>el CRD &lt;code>InferenceService&lt;/code>&lt;/strong>, que se ha convertido en el vocabulario común del campo: un objeto K8s declarativo que une un &lt;code>model&lt;/code> (origen + metadata) con un &lt;code>predictor&lt;/code> (runtime + recursos) y produce un servicio HTTP listo. Bajo el capó, el controller emite Deployments, Services, HorizontalPodAutoscalers, Knative Services si haces serverless, Istio VirtualServices si haces traffic splitting.&lt;/p>
&lt;p>KServe fue diseñado en una era pre-LLM: sus primeros casos de uso eran modelos scikit-learn, TensorFlow y PyTorch tradicionales servidos como REST APIs simples. Eso le da fortalezas (es maduro, lleva 6 años en producción en Bloomberg, JPMorgan y otros) y debilidades (no fue diseñado para gestionar tensor parallel multi-nodo, prefill–decode disaggregation, ni los patrones específicos de LLMs).&lt;/p>
&lt;p>La forma en la que el ecosistema ha reaccionado es elegante: &lt;strong>los nuevos operators de LLM heredan o se inspiran en &lt;code>InferenceService&lt;/code> pero extienden la API con primitivos específicos de LLM&lt;/strong>. OME es el ejemplo más claro: usa el nombre &lt;code>InferenceService&lt;/code> y la idea de &amp;ldquo;modelo + runtime → servicio&amp;rdquo;, pero añade &lt;code>BaseModel&lt;/code>, &lt;code>ServingRuntime&lt;/code> con flags LLM-aware, y modos de despliegue (PD-disag, multi-node) que KServe no contempla nativamente.&lt;/p>
&lt;h2 id="ome-open-model-engine">OME (Open Model Engine)&lt;/h2>
&lt;p>&lt;a href="https://github.com/ome-projects/ome">OME&lt;/a> lo publicó el equipo de LMSYS en julio 2025 (anunciado en &lt;a href="https://www.lmsys.org/blog/2025-07-08-ome/">su blog&lt;/a>). Es un operator que entiende SGLang en profundidad (es su runtime de primera clase) pero también soporta vLLM, TensorRT-LLM y Triton.&lt;/p>
&lt;h3 id="la-jerarquía-de-crds">La jerarquía de CRDs&lt;/h3>
&lt;p>OME modela el dominio con cuatro CRDs principales:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>BaseModel&lt;/code>&lt;/strong> y &lt;strong>&lt;code>ClusterBaseModel&lt;/code>&lt;/strong>: el modelo en sí. Define origen (Hugging Face, S3, URL), fingerprint, metadatos. La versión &lt;code>Cluster*&lt;/code> es global; la &lt;code>BaseModel&lt;/code> es namespaced. Permite que múltiples &lt;code>InferenceService&lt;/code> referencien el mismo modelo sin duplicar la descarga.&lt;/li>
&lt;li>&lt;strong>&lt;code>FineTunedWeight&lt;/code>&lt;/strong>: adapters LoRA o pesos finetuneados que se sirven encima de un &lt;code>BaseModel&lt;/code>. Crítico para multi-tenant donde cada cliente tiene su finetune.&lt;/li>
&lt;li>&lt;strong>&lt;code>ServingRuntime&lt;/code>&lt;/strong> y &lt;strong>&lt;code>ClusterServingRuntime&lt;/code>&lt;/strong>: el runtime (vLLM, SGLang, etc.) con su configuración. Declara qué args acepta, qué métricas exporta, qué modos de despliegue soporta.&lt;/li>
&lt;li>&lt;strong>&lt;code>InferenceService&lt;/code>&lt;/strong>: la pieza central, declarativa, que une &lt;code>BaseModel&lt;/code> + &lt;code>ServingRuntime&lt;/code> + infraestructura.&lt;/li>
&lt;/ul>
&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">ome.io/v1beta1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">InferenceService&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">llama3-70b-prod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inference&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&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">meta-llama-3-70b-instruct &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># referencia a un BaseModel&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">runtime&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">sglang-h100 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># referencia a un ServingRuntime&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">deploymentMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PrefillDecodeDisaggregated &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># standard | PD | MultiNode | Serverless&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">prefill&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">minReplicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&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">maxReplicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&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">nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4&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">decode&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">minReplicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4&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">maxReplicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">16&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&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">nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">router&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">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cache-aware &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># SGLang router con cache awareness&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">autoscaling&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">metricSource&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">keda&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">metrics&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">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">prometheus&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">metricName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm_requests_waiting&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">threshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;10&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esto es lo que el operador toma como entrada. La salida son aproximadamente &lt;strong>8 recursos derivados&lt;/strong> que serían un horror declarar a mano: dos LeaderWorkerSets (uno por pool prefill/decode), dos Services, un Deployment para el router, ScaledObjects de KEDA por cada pool, HTTPRoute de Gateway API, y un PriorityClass que conecta con Kueue para gang scheduling.&lt;/p>
&lt;h3 id="los-cuatro-modos-de-despliegue">Los cuatro modos de despliegue&lt;/h3>
&lt;p>OME materializa la &lt;code>InferenceService&lt;/code> de forma distinta según &lt;code>deploymentMode&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Standard&lt;/strong>: un Deployment con N réplicas; clásico. Para modelos pequeños o single-GPU.&lt;/li>
&lt;li>&lt;strong>PrefillDecodeDisaggregated&lt;/strong>: dos pools coordinados; el router de SGLang los enruta.&lt;/li>
&lt;li>&lt;strong>MultiNode&lt;/strong>: tensor parallel sobre múltiples nodos vía LeaderWorkerSet, con NCCL/InfiniBand. Para modelos &amp;gt;70B donde un solo nodo no llega.&lt;/li>
&lt;li>&lt;strong>Serverless&lt;/strong>: Knative-style scale-to-zero. Para cargas esporádicas donde el coste de mantener GPUs encendidas no compensa. Trade-off: el primer request paga el coste de cold start del modelo (minutos).&lt;/li>
&lt;/ul>
&lt;h3 id="integración-con-el-ecosistema-k8s">Integración con el ecosistema K8s&lt;/h3>
&lt;p>OME no inventa primitivos donde ya existen. Se apoya en:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://kueue.sigs.k8s.io/">Kueue&lt;/a>&lt;/strong> para gang scheduling: todos los pods de un tensor parallel deben arrancar a la vez o ninguno; Kueue lo garantiza.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://lws.sigs.k8s.io/">LeaderWorkerSet (LWS)&lt;/a>&lt;/strong> para multi-nodo: workers se unen al cluster Ray del leader, ciclo de vida atómico (caída de uno reinicia el grupo).&lt;/li>
&lt;li>&lt;strong>KEDA&lt;/strong> para autoscaling por métricas Prometheus específicas de LLM (queue depth, GPU cache usage, TTFT p95).&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://gateway-api.sigs.k8s.io/">Gateway API&lt;/a>&lt;/strong> y su &lt;strong>Inference Extension&lt;/strong> para routing avanzado (model-aware, prefix-aware, weighted canary).&lt;/li>
&lt;/ul>
&lt;p>La consecuencia: OME se siente &amp;ldquo;idiomáticamente Kubernetes&amp;rdquo;. No introduce conceptos nuevos donde no hace falta; usa primitivos estándar y se concentra en lo específico del dominio LLM.&lt;/p>
&lt;h3 id="cuándo-elegirlo">Cuándo elegirlo&lt;/h3>
&lt;p>OME es la opción natural si &lt;strong>SGLang es tu runtime principal&lt;/strong> y/o si vienes del ecosistema KServe y quieres una evolución idiomática. Es maduro pero relativamente joven (un año en el momento de este artículo); espera bordes ásperos en features avanzadas.&lt;/p>
&lt;h2 id="vllm-production-stack">vLLM Production Stack&lt;/h2>
&lt;p>&lt;a href="https://github.com/vllm-project/production-stack">vLLM Production Stack&lt;/a> es el proyecto &lt;strong>oficial del propio vLLM&lt;/strong> para producción en Kubernetes. Su filosofía es opuesta a la de OME: en lugar de un operator con CRDs nuevos, es &lt;strong>un Helm chart curado&lt;/strong> que despliega un conjunto coherente de piezas.&lt;/p>
&lt;h3 id="las-tres-piezas">Las tres piezas&lt;/h3>
&lt;p>El stack tiene tres componentes:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Serving engines&lt;/strong>: pods de vLLM, configurados con los flags que llevamos viendo en toda la serie (&lt;code>--enable-prefix-caching&lt;/code>, &lt;code>--kv-cache-dtype fp8&lt;/code>, etc.). El Helm chart te deja declararlos como una lista; despliega los Deployments y Services subyacentes.&lt;/li>
&lt;li>&lt;strong>Request router&lt;/strong>: un proxy delante de los engines que decide a cuál enviar cada petición. Soporta varias políticas:
&lt;ul>
&lt;li>&lt;strong>Round-robin&lt;/strong>: trivial, para baseline.&lt;/li>
&lt;li>&lt;strong>Session-based&lt;/strong>: clava cada sesión a una réplica para mantener su KV cache.&lt;/li>
&lt;li>&lt;strong>Prefix-aware&lt;/strong>: detecta prefijos compartidos entre peticiones y las enruta a la réplica que ya los tenga cacheados.&lt;/li>
&lt;li>&lt;strong>KV-aware&lt;/strong>: ve el &lt;code>gpu_cache_usage_perc&lt;/code> de cada réplica y evita las saturadas.&lt;/li>
&lt;li>&lt;strong>Disaggregated-prefill&lt;/strong> con &lt;strong>LMCache&lt;/strong> nativo: separa prefill y decode con LMCache como transport del KV cache entre ambos.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Observability stack&lt;/strong>: Prometheus + Grafana con dashboards listos. Mide TTFT, TBT (Time-Between-Tokens), throughput, queue depth, GPU memory.&lt;/li>
&lt;/ol>
&lt;h3 id="lmcache-y-el-tiered-kv">LMCache y el tiered KV&lt;/h3>
&lt;p>Una de las piezas más interesantes que mete el stack es &lt;a href="https://github.com/LMCache/LMCache">&lt;strong>LMCache&lt;/strong>&lt;/a>, que añade un caché de KV con &lt;strong>múltiples tiers&lt;/strong>: GPU HBM como L1, CPU RAM como L2, disco local como L3, y opcionalmente storage remoto como L4. Cuando un bloque de KV cache no cabe en HBM, en lugar de evictarlo y recalcularlo, LMCache lo baja a un tier inferior. Para cargas con prefijos compartidos y multi-turn, el ahorro es brutal.&lt;/p>
&lt;p>LMCache se integra como sidecar de los engines y como parte del transport en disaggregated-prefill. El Production Stack lo trae habilitado por defecto en su Helm chart.&lt;/p>
&lt;h3 id="manifest-típico-valuesyaml">Manifest típico (values.yaml)&lt;/h3>
&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">servingEngineSpec&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">modelSpec&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">llama3-8b&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">repository&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm/vllm-openai&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">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v0.6.3&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">modelURL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">meta-llama/Meta-Llama-3-8B-Instruct&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">replicaCount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&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">requestCPU&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4&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">requestMemory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">16Gi&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">requestGPU&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">vllmConfig&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">enablePrefixCaching&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&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">kvCacheDtype&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fp8&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">maxModelLen&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">32768&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">enableChunkedPrefill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">routerSpec&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">routingLogic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">prefix-aware &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># round-robin | session | prefix-aware | kv-aware&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">sessionKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-user-id &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># cuando routingLogic=session&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">cacheserverSpec&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">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># LMCache para tiered KV&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">storageBackends&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="l">cpu&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="l">disk &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># offload a disco local&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">observabilitySpec&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">prometheus&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">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&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">grafana&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">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&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">dashboards&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="l">vllm-engine-metrics&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="l">lmcache-metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esto es declarativo pero &lt;strong>no son CRDs&lt;/strong>: son valores de un Helm chart. La diferencia con OME no es semántica (ambos parten de declaración) sino operacional: con Helm, los cambios pasan por &lt;code>helm upgrade&lt;/code>; con CRDs, pasan por &lt;code>kubectl apply&lt;/code>. Para equipos que ya viven en GitOps con Argo CD o Flux, ambos enfoques se integran limpiamente, pero los flujos son distintos.&lt;/p>
&lt;h3 id="cuándo-elegirlo-1">Cuándo elegirlo&lt;/h3>
&lt;p>Si &lt;strong>tu único runtime es vLLM&lt;/strong> y quieres lo más cercano a &amp;ldquo;el camino feliz que recomienda el proyecto&amp;rdquo;, esto. Es la versión productivizada y mantenida por la misma gente que escribe el motor. Las desventajas: ata a vLLM (no es genérico) y no resuelve algunos casos avanzados como multi-tenancy con cuotas estrictas o gang scheduling, donde OME u operators full-fledged son superiores.&lt;/p>
&lt;h2 id="nvidia-dynamo">NVIDIA Dynamo&lt;/h2>
&lt;p>&lt;a href="https://developer.nvidia.com/dynamo">NVIDIA Dynamo&lt;/a> es el &lt;strong>sucesor oficial de Triton Inference Server&lt;/strong>, anunciado en GTC 2025 y fusionado con la marca como &lt;strong>Dynamo-Triton&lt;/strong> en marzo de ese año. Triton llevaba años siendo el motor de inferencia más usado en infraestructuras NVIDIA &amp;ldquo;serias&amp;rdquo;; Dynamo es lo que NVIDIA cree que la nueva generación necesita.&lt;/p>
&lt;h3 id="qué-es-exactamente">Qué es exactamente&lt;/h3>
&lt;p>Dynamo es &lt;strong>un framework de inferencia distribuida&lt;/strong>, no exactamente un operator de Kubernetes. Tiene runtime propio (puede correr engines), scheduler (Grove), routing inteligente, gestión de KV cache multi-tier y disaggregation. Soporta como engines a &lt;strong>SGLang, TensorRT-LLM y vLLM&lt;/strong>, pero los engines son ejecutados por Dynamo, no a la inversa: el modelo es &amp;ldquo;Dynamo gestiona, el engine ejecuta&amp;rdquo;.&lt;/p>
&lt;p>En Kubernetes, Dynamo se despliega vía operator + CRDs propios, normalizados con la integración K8s que NVIDIA formalizó a finales de 2025 (la cubre &lt;a href="https://www.infoq.com/news/2025/12/nvidia-dynamo-kubernetes/">esta nota de InfoQ&lt;/a>). Los CRDs son específicos del producto: definen un &lt;code>DynamoCluster&lt;/code>, una topología de prefill/decode workers, una política de routing.&lt;/p>
&lt;h3 id="las-cuatro-contribuciones">Las cuatro contribuciones&lt;/h3>
&lt;p>Dynamo se vende sobre cuatro pilares, con números reportados por NVIDIA:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Disaggregated serving&lt;/strong> built-in con scheduler propio.&lt;/li>
&lt;li>&lt;strong>Smart routing&lt;/strong> basado en estado de cache: si un worker ya tiene cacheada la mayoría de un prompt, la petición va ahí.&lt;/li>
&lt;li>&lt;strong>Multi-tier KV cache&lt;/strong>: análogo a LMCache, con HBM/RAM/SSD/NVMe.&lt;/li>
&lt;li>&lt;strong>Autoscaling&lt;/strong> integrado con el scheduler de Dynamo.&lt;/li>
&lt;/ol>
&lt;p>El número marketing: &lt;strong>hasta 30× más throughput&lt;/strong> que Triton legacy en el mismo hardware. Con todas las precauciones que merece un benchmark de vendor.&lt;/p>
&lt;h3 id="grove-scheduler-propio">Grove: scheduler propio&lt;/h3>
&lt;p>Una decisión polémica de Dynamo es no apoyarse al 100% en el scheduler de Kubernetes y, en su lugar, traer un scheduler propio llamado &lt;strong>Grove&lt;/strong> que entiende topologías de GPU. Grove decide qué worker corre en qué GPU física, qué interconexiones (NVLink/InfiniBand) son relevantes, y cómo distribuir tensor parallel entre nodos. Esto le da más control que kube-scheduler estándar.&lt;/p>
&lt;p>Operacionalmente: si tu cluster es &amp;ldquo;puro Kubernetes&amp;rdquo; con kube-scheduler y workloads heterogéneos (no solo LLMs), Grove añade un componente adicional a operar. Si tu cluster es &lt;strong>dedicado a inferencia LLM&lt;/strong> y ya hay equipo dedicado a operarlo, Grove te da más palancas.&lt;/p>
&lt;h3 id="cuándo-elegirlo-2">Cuándo elegirlo&lt;/h3>
&lt;p>Dynamo tiene sentido si:&lt;/p>
&lt;ul>
&lt;li>Tu infraestructura es &lt;strong>NVIDIA-heavy&lt;/strong> (Hopper, Blackwell, GB200) y quieres aprovechar lo más reciente de TensorRT-LLM con la integración de Triton-de-toda-la-vida pero modernizado.&lt;/li>
&lt;li>Ya eras usuario de Triton para inferencia legacy (visión, recomendación) y quieres mantener el ecosistema.&lt;/li>
&lt;li>Tienes equipo SRE dedicado a inferencia y la complejidad operacional adicional de Grove no es un problema.&lt;/li>
&lt;/ul>
&lt;p>Es la opción &lt;strong>vendor-specific&lt;/strong> del cuarteto. A cambio te da el soporte de NVIDIA y la integración de primera con su hardware. Si tu organización ya pelea con NVIDIA por GPUs, igual te llaman para ofrecer asistencia con Dynamo.&lt;/p>
&lt;h2 id="llm-d">llm-d&lt;/h2>
&lt;p>&lt;a href="https://github.com/llm-d/llm-d">llm-d&lt;/a> es el más joven y el más &amp;ldquo;político&amp;rdquo; de los cuatro. En marzo de 2026, en &lt;a href="https://siliconangle.com/2026/03/24/red-hat-bets-big-kubernetes-inference-llm-d-kubeconeu/">KubeCon Europe Amsterdam&lt;/a>, &lt;strong>Red Hat, Google Cloud, IBM Research, CoreWeave y NVIDIA&lt;/strong> anunciaron la donación conjunta del proyecto a la CNCF como Sandbox, con soporte de AMD, Cisco, Hugging Face, Intel, Lambda, Mistral AI, UC Berkeley y University of Chicago. Una coalición de vendor-neutralidad explícita.&lt;/p>
&lt;h3 id="filosofía">Filosofía&lt;/h3>
&lt;p>llm-d se posiciona como &lt;strong>el &amp;ldquo;Kubernetes blueprint&amp;rdquo; vendor-neutral para inferencia distribuida&lt;/strong>. No es un runtime; es un sistema que se monta encima de vLLM (motor por defecto) y orquesta el plano de control.&lt;/p>
&lt;p>Las primitivas que el proyecto pone sobre la mesa:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Routing inteligente&lt;/strong> con prefix-cache awareness y load-aware balancing.&lt;/li>
&lt;li>&lt;strong>Tiered KV cache&lt;/strong> con offload a CPU y disco para multi-turn.&lt;/li>
&lt;li>&lt;strong>Prefill/decode disaggregation&lt;/strong> sobre interconnects rápidos.&lt;/li>
&lt;li>&lt;strong>Wide expert-parallelism&lt;/strong> para servir Mixture-of-Experts (MoE) muy grandes —un patrón crítico que DeepSeek-V3 y Mixtral popularizaron— donde los expertos viven en distintas GPUs y hay que enrutar tokens al experto correcto.&lt;/li>
&lt;/ul>
&lt;h3 id="números">Números&lt;/h3>
&lt;p>El &lt;a href="https://github.com/llm-d/llm-d/releases">release v0.5&lt;/a> valida ~3.1k tok/s por GPU de decode B200, y hasta 50k output tok/s en una topología 16×16 B200 prefill/decode. El benchmark más interesante: &lt;strong>orden de magnitud de reducción de TTFT&lt;/strong> vs una baseline round-robin. Es decir, el routing inteligente vale lo que se dice.&lt;/p>
&lt;h3 id="cncf-y-futuro">CNCF y futuro&lt;/h3>
&lt;p>Donar a la CNCF como Sandbox significa &lt;strong>gobernanza neutral&lt;/strong>: ningún vendor manda. Para una organización que recela de quedar atado a un único proveedor, llm-d es probablemente la apuesta más segura a medio plazo. El precio: como cualquier proyecto Sandbox, todavía no es &amp;ldquo;boring&amp;rdquo; en el sentido en que vLLM lo es. Hay churn de API, features que se mueven, documentación que va por detrás del código.&lt;/p>
&lt;h3 id="cuándo-elegirlo-3">Cuándo elegirlo&lt;/h3>
&lt;p>llm-d tiene sentido si:&lt;/p>
&lt;ul>
&lt;li>Quieres &lt;strong>portabilidad multi-vendor&lt;/strong> sin ataduras a NVIDIA, Red Hat o Google.&lt;/li>
&lt;li>Tu carga incluye &lt;strong>MoE grandes&lt;/strong> (DeepSeek-V3, Mixtral 8x22B, Llama 4 Behemoth si confirma tamaño), donde wide expert parallelism es decisivo.&lt;/li>
&lt;li>Tu organización ya está cómoda con CNCF Sandbox (proyectos en evolución activa, no aún 1.0 estable).&lt;/li>
&lt;li>Quieres apostar por el proyecto que probablemente sea el estándar de facto en 2-3 años.&lt;/li>
&lt;/ul>
&lt;h2 id="el-antecesor-común-sigue-ahí-kserve">El antecesor común sigue ahí: KServe&lt;/h2>
&lt;p>Vale la pena reconectar antes de la comparativa: &lt;strong>KServe sigue vivo y muy usado&lt;/strong> en organizaciones que sirven tanto LLMs como modelos tradicionales (scikit-learn, XGBoost, PyTorch CV). Su &lt;code>InferenceService&lt;/code> es lo bastante genérico como para servir cualquier modelo, incluyendo vLLM o SGLang como &lt;code>ServingRuntime&lt;/code>. Lo que no hace bien es lo específico de LLM: disaggregation, tensor parallel multi-nodo, routing con awareness de KV cache. Si tu organización ya tiene KServe en producción para otros modelos, &lt;strong>añadir un operator específico de LLM al lado&lt;/strong> (OME, vLLM Stack o llm-d) es razonable. Pelearlo todo desde KServe puro no.&lt;/p>
&lt;h2 id="mapa-de-decisión">Mapa de decisión&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dimensión&lt;/th>
&lt;th>OME&lt;/th>
&lt;th>vLLM Prod Stack&lt;/th>
&lt;th>NVIDIA Dynamo&lt;/th>
&lt;th>llm-d&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Filosofía&lt;/strong>&lt;/td>
&lt;td>Operator clásico K8s-idiomático&lt;/td>
&lt;td>Helm chart curado&lt;/td>
&lt;td>Framework con scheduler propio&lt;/td>
&lt;td>Blueprint CNCF vendor-neutral&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CRDs propios&lt;/strong>&lt;/td>
&lt;td>Sí (BaseModel, ServingRuntime, InferenceService&amp;hellip;)&lt;/td>
&lt;td>No (Helm values)&lt;/td>
&lt;td>Sí (DynamoCluster)&lt;/td>
&lt;td>Sí (KServe-derived + extensions)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Runtime primario&lt;/strong>&lt;/td>
&lt;td>SGLang (primera clase), también vLLM/TRT-LLM/Triton&lt;/td>
&lt;td>vLLM exclusivamente&lt;/td>
&lt;td>TensorRT-LLM (primera clase), también SGLang/vLLM&lt;/td>
&lt;td>vLLM (primera clase)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>PD-disaggregation&lt;/strong>&lt;/td>
&lt;td>Sí, declarativo&lt;/td>
&lt;td>Sí, con LMCache&lt;/td>
&lt;td>Sí, scheduler propio&lt;/td>
&lt;td>Sí, nativo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Multi-nodo TP&lt;/strong>&lt;/td>
&lt;td>Sí, via LWS&lt;/td>
&lt;td>Limitado&lt;/td>
&lt;td>Sí, via Grove&lt;/td>
&lt;td>Sí, via LWS y MoE EP&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Multi-modelo en cluster&lt;/strong>&lt;/td>
&lt;td>Sí, multi-tenant maduro&lt;/td>
&lt;td>Sí (lista de modelos en values)&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Multi-LoRA&lt;/strong>&lt;/td>
&lt;td>Sí, primera clase (FineTunedWeight CRD)&lt;/td>
&lt;td>Limitado&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>En roadmap&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tiered KV cache&lt;/strong>&lt;/td>
&lt;td>Vía LMCache (integración externa)&lt;/td>
&lt;td>LMCache nativo&lt;/td>
&lt;td>Multi-tier propio&lt;/td>
&lt;td>Sí, nativo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Routing inteligente&lt;/strong>&lt;/td>
&lt;td>Cache-aware via SGLang router&lt;/td>
&lt;td>Prefix-aware / KV-aware / session-based&lt;/td>
&lt;td>Smart routing propio&lt;/td>
&lt;td>Prefix-cache + load-aware&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Scheduler GPU&lt;/strong>&lt;/td>
&lt;td>kube-scheduler + Kueue&lt;/td>
&lt;td>kube-scheduler&lt;/td>
&lt;td>Grove (propio)&lt;/td>
&lt;td>kube-scheduler + Kueue&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Hardware&lt;/strong>&lt;/td>
&lt;td>NVIDIA, AMD ROCm, Intel&lt;/td>
&lt;td>NVIDIA, AMD ROCm&lt;/td>
&lt;td>NVIDIA exclusivo (con énfasis)&lt;/td>
&lt;td>NVIDIA, AMD, Intel — neutral&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Madurez (mid-2026)&lt;/strong>&lt;/td>
&lt;td>Joven, en evolución&lt;/td>
&lt;td>Estable&lt;/td>
&lt;td>Estable, vendor-driven&lt;/td>
&lt;td>CNCF Sandbox, evolución rápida&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Gobernanza&lt;/strong>&lt;/td>
&lt;td>LMSYS (académico-industrial)&lt;/td>
&lt;td>vLLM project (académico)&lt;/td>
&lt;td>NVIDIA (vendor)&lt;/td>
&lt;td>CNCF (neutral)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Curva de aprendizaje&lt;/strong>&lt;/td>
&lt;td>Media (4 CRDs nuevos)&lt;/td>
&lt;td>Baja (Helm values familiar)&lt;/td>
&lt;td>Media-alta (Grove + CRDs propios)&lt;/td>
&lt;td>Media (similar a KServe extendido)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="cuándo-elegir-cada-uno">Cuándo elegir cada uno&lt;/h3>
&lt;p>&lt;strong>Elige OME&lt;/strong> si:&lt;/p>
&lt;ul>
&lt;li>SGLang es tu motor principal.&lt;/li>
&lt;li>Necesitas multi-LoRA serving en producción.&lt;/li>
&lt;li>Te encaja la abstracción jerárquica (BaseModel → ServingRuntime → InferenceService) y vienes de o convives con KServe.&lt;/li>
&lt;li>Tienes appetito por un proyecto joven y muy activo.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Elige vLLM Production Stack&lt;/strong> si:&lt;/p>
&lt;ul>
&lt;li>vLLM es tu único motor y quieres alinearte con lo que el proyecto recomienda.&lt;/li>
&lt;li>Tu equipo ya vive en Helm y no quiere aprender CRDs nuevos.&lt;/li>
&lt;li>LMCache + routing avanzado dentro de un solo Helm chart es exactamente lo que necesitas.&lt;/li>
&lt;li>Tu escala es media (decenas de réplicas), no extrema.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Elige NVIDIA Dynamo&lt;/strong> si:&lt;/p>
&lt;ul>
&lt;li>Tu infraestructura es NVIDIA-heavy y quieres el path más optimizado para Hopper/Blackwell.&lt;/li>
&lt;li>Ya operabas Triton para inferencia legacy y la transición es natural.&lt;/li>
&lt;li>Aceptas vendor lock-in a cambio de soporte directo NVIDIA.&lt;/li>
&lt;li>Tu organización tiene equipo SRE dedicado a inferencia.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Elige llm-d&lt;/strong> si:&lt;/p>
&lt;ul>
&lt;li>Quieres apostar por el estándar CNCF futuro, neutro entre vendors.&lt;/li>
&lt;li>Tu carga incluye MoE grandes con wide expert parallelism.&lt;/li>
&lt;li>Operas en multi-cloud o multi-hardware y la portabilidad es valiosa.&lt;/li>
&lt;li>Aceptas la inmadurez de un proyecto Sandbox a cambio de la apuesta a futuro.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Elige KServe puro&lt;/strong> si:&lt;/p>
&lt;ul>
&lt;li>Ya sirves modelos no-LLM y quieres unificar; los LLMs son una minoría de tu carga.&lt;/li>
&lt;li>Necesitas el caso de uso más conservador y maduro.&lt;/li>
&lt;li>Aceptas que features avanzadas de LLM (disaggregation, MoE EP, smart routing) te tocará añadirlas con piezas externas.&lt;/li>
&lt;/ul>
&lt;h3 id="escenarios-concretos">Escenarios concretos&lt;/h3>
&lt;p>&lt;strong>Escenario A — Startup pequeña, 1-2 modelos, 1-3 nodos GPU.&lt;/strong> Probablemente no necesitas operator. Deployment + Service + HPA con métricas de KEDA, como en el artículo de vLLM en Kubernetes. Cuando crezcas a 5+ modelos, evalúa.&lt;/p>
&lt;p>&lt;strong>Escenario B — Empresa media, 5-15 modelos, multi-tenant interno.&lt;/strong> vLLM Production Stack o OME son las opciones razonables. Production Stack si vLLM es todo lo que vas a usar; OME si quieres flexibilidad de runtime y CRDs idiomáticos.&lt;/p>
&lt;p>&lt;strong>Escenario C — Plataforma interna corporativa o servicio externo a clientes finales.&lt;/strong> llm-d o Dynamo. llm-d si valoras vendor-neutralidad; Dynamo si vives en infraestructura NVIDIA y quieres el camino que ellos recomiendan.&lt;/p>
&lt;p>&lt;strong>Escenario D — Cluster mixto LLM + modelos tradicionales.&lt;/strong> KServe como base, operator de LLM al lado (OME es lo más natural por su parentesco conceptual).&lt;/p>
&lt;h2 id="trampas-comunes">Trampas comunes&lt;/h2>
&lt;p>&lt;strong>&amp;ldquo;Voy a empezar con KServe puro porque es maduro&amp;rdquo;.&lt;/strong> Para LLMs medianos en adelante, KServe puro deja muchas optimizaciones sobre la mesa. Lo razonable es KServe como base si convives con otros modelos, pero operator LLM-específico al lado.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Voy a montar todo a mano para entenderlo&amp;rdquo;.&lt;/strong> Razonable en PoC, suicida en producción. Hay 8 recursos derivados por modelo. Multiplica por 10 modelos. Estás escribiendo 80 YAMLs y manteniéndolos. Usa un operator.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Voy a elegir el que más me gusta y luego pivoto si me equivoco&amp;rdquo;.&lt;/strong> Pivotar entre operators no es gratis: aunque la abstracción &lt;code>InferenceService&lt;/code> se está homogeneizando, los detalles (cómo se modela LoRA, cómo se configura routing, cómo se exponen métricas) varían. Migrar de OME a Dynamo es un proyecto de semanas, no de días.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Voy a poner Dynamo porque es de NVIDIA y mejor&amp;rdquo;.&lt;/strong> Solo si tu organización ya está alineada con su filosofía operacional (scheduler propio, vendor lock-in aceptable). Para muchos casos, vLLM Production Stack o llm-d dan 95% del valor con menos fricción.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Helm chart vs operator es una decisión técnica&amp;rdquo;.&lt;/strong> Es una decisión cultural/operacional. Si tu equipo entrega vía Argo CD con Helm values en Git, Production Stack encaja sin fricción. Si tu equipo vive en &lt;code>kubectl apply -f&lt;/code> directo y la idea de operators te resulta natural, OME o llm-d.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://github.com/kvcache-ai/Mooncake">Mooncake&lt;/a>&lt;/strong>: el sistema de cache de KV compartido entre instancias que Kimi/Moonshot lleva en producción a cientos de millones de queries. Es un primitivo (no un operator completo), pero se integra como tier de cache con varios de los anteriores.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://docs.ray.io/en/latest/serve/llm/serving-llms.html">Ray Serve LLM&lt;/a>&lt;/strong>: la oferta de Anyscale, en Kubernetes a través de KubeRay. Más vinculado al ecosistema Ray que a los CRDs nativos K8s. Útil si Ray ya es parte de tu infraestructura.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://fireworks.ai/">Fireworks AI&lt;/a>, &lt;a href="https://www.modular.com/">Modular MAX&lt;/a>&lt;/strong>: plataformas comerciales con primitivos similares, pero hospedadas. No son operators K8s; son competidores en otra capa.&lt;/li>
&lt;li>&lt;strong>Gateway API Inference Extension&lt;/strong>: la propuesta sigwg para extender Gateway API con primitivos LLM (model-aware routing, sticky sessions, fairness). En 2026 está en alpha; los operators de arriba ya empiezan a soportarla. Cuando madure, el routing dejará de ser problema de cada operator y será parte del estándar de Kubernetes.&lt;/li>
&lt;li>&lt;strong>Inference observability stack genérico&lt;/strong>: Prometheus + Grafana se está estandarizando en torno a las métricas &lt;code>vllm:*&lt;/code> que cubrimos en el artículo de vLLM. Hay esfuerzo de OpenTelemetry para LLMs (&lt;code>gen-ai&lt;/code> semantic conventions) que probablemente sea el siguiente eslabón.&lt;/li>
&lt;/ul>
&lt;h2 id="cerrando-la-serie">Cerrando la serie&lt;/h2>
&lt;p>Esta serie de cuatro artículos ha recorrido la inferencia LLM en producción de abajo arriba:&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a> — por qué cada token consume VRAM y cuánto.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes: la pieza de inferencia LLM que sí escala&lt;/a> — cómo se sirve un modelo en producción con un Deployment serio.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention por dentro y el estado del arte del KV cache en 2026&lt;/a> — qué pasa dentro del motor a nivel del bloque, y qué ha llegado después.&lt;/li>
&lt;li>&lt;strong>Este&lt;/strong> — cómo se orquestan muchos modelos en cluster.&lt;/li>
&lt;/ol>
&lt;p>Si has llegado aquí, tienes el vocabulario y el mapa para sentarte en una reunión donde cinco personas tiren siglas y reconocer cada una en su sitio. Y, lo más importante, para empezar a tomar decisiones razonadas sobre por dónde empezar.&lt;/p>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>Operators y proyectos cubiertos:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/ome-projects/ome">OME — Open Model Engine (GitHub)&lt;/a> — operator de LMSYS para LLM serving con SGLang/vLLM/TRT-LLM/Triton.&lt;/li>
&lt;li>&lt;a href="https://www.lmsys.org/blog/2025-07-08-ome/">Introducing OME (LMSYS Blog, jul 2025)&lt;/a> — anuncio y arquitectura.&lt;/li>
&lt;li>&lt;a href="https://github.com/vllm-project/production-stack">vLLM Production Stack (GitHub)&lt;/a> — Helm chart oficial de vLLM para K8s.&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/projects/production-stack/en/latest/deployment/">vLLM Production Stack docs&lt;/a> — instalación y configuración.&lt;/li>
&lt;li>&lt;a href="https://github.com/LMCache/LMCache">LMCache (GitHub)&lt;/a> — caché de KV con tiers.&lt;/li>
&lt;li>&lt;a href="https://developer.nvidia.com/dynamo">NVIDIA Dynamo&lt;/a> — sucesor de Triton.&lt;/li>
&lt;li>&lt;a href="https://www.infoq.com/news/2025/12/nvidia-dynamo-kubernetes/">NVIDIA Dynamo Addresses Multi-Node LLM Inference Challenges (InfoQ, dic 2025)&lt;/a> — integración K8s.&lt;/li>
&lt;li>&lt;a href="https://github.com/llm-d/llm-d">llm-d (GitHub)&lt;/a> — proyecto CNCF Sandbox.&lt;/li>
&lt;li>&lt;a href="https://thenewstack.io/llm-d-cncf-kubernetes-inference/">IBM, Red Hat, and Google donated llm-d to CNCF (The New Stack)&lt;/a> — anuncio KubeCon EU 2026.&lt;/li>
&lt;li>&lt;a href="https://siliconangle.com/2026/03/24/red-hat-bets-big-kubernetes-inference-llm-d-kubeconeu/">Red Hat bets big on Kubernetes inference with llm-d (SiliconANGLE, mar 2026)&lt;/a> — cobertura del anuncio.&lt;/li>
&lt;/ul>
&lt;p>Antecesores y primitivos:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://kserve.github.io/website/">KServe (sitio)&lt;/a> y &lt;a href="https://thenewstack.io/kserve-joins-cncf-to-standardize-ai-model-serving-on-kubernetes/">KServe joins CNCF (The New Stack)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://kueue.sigs.k8s.io/">Kueue&lt;/a> — gang scheduling.&lt;/li>
&lt;li>&lt;a href="https://lws.sigs.k8s.io/">LeaderWorkerSet&lt;/a> — workloads coordinados como tensor parallel multi-pod.&lt;/li>
&lt;li>&lt;a href="https://keda.sh/">KEDA&lt;/a> — autoscaling por métricas externas.&lt;/li>
&lt;li>&lt;a href="https://gateway-api.sigs.k8s.io/">Gateway API&lt;/a> — sucesor del Ingress.&lt;/li>
&lt;/ul>
&lt;p>Análisis y perspectivas:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://jimmysong.io/blog/cloud-native-llm-inference-stack/">Building Efficient LLM Inference with the Cloud Native Quartet: KServe, vLLM, llm-d, and WG Serving (Jimmy Song)&lt;/a> — visión integradora.&lt;/li>
&lt;li>&lt;a href="https://dev.to/x4nent/complete-guide-to-llm-d-cncf-sandbox-kubernetes-native-distributed-llm-inference-1imj">Complete Guide to llm-d CNCF Sandbox (DEV Community)&lt;/a> — walkthrough operacional.&lt;/li>
&lt;li>Artículos previos en este blog: &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a>, &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes&lt;/a>, &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention deep dive&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>PagedAttention por dentro: bloques, tabla de páginas, evicción y el estado del arte del KV cache en 2026</title><link>https://blog.lo0.es/posts/pagedattention-deep-dive/</link><pubDate>Mon, 18 May 2026 15:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/pagedattention-deep-dive/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>PagedAttention (Kwon et al., SOSP 2023) fue la idea que convirtió la gestión del KV cache de un problema de &lt;strong>malloc clásico&lt;/strong> —reservar contiguo, malgastar el 60-80%— en un problema resuelto &lt;strong>como lo resuelven los sistemas operativos desde hace medio siglo&lt;/strong>: bloques pequeños de tamaño fijo, una tabla de páginas por proceso, asignación bajo demanda. El paper midió un desperdicio menor al 4% y 2-4× más throughput agregado en el mismo hardware. Tres años después, PagedAttention sigue siendo el modelo mental dominante, pero su implementación literal ya no es la de ningún sistema de inferencia serio: la propia documentación de vLLM califica al paper original de &amp;ldquo;documento histórico&amp;rdquo;. Han llegado &lt;strong>vAttention&lt;/strong> (paginar usando la MMU de CUDA, no la indirección software), &lt;strong>EvicPress&lt;/strong> (combinar compresión y evicción), &lt;strong>KVTC&lt;/strong> (transform coding del cache), &lt;strong>LaProx&lt;/strong> (evicción como aproximación matricial), &lt;strong>disaggregated serving&lt;/strong> (prefill y decode en GPUs distintas, en producción en NVIDIA Dynamo, llm-d, Mooncake y media docena más), &lt;strong>RadixAttention&lt;/strong> de SGLang (trie de prefijos compartidos, con hit rates del 85% en cargas de agentes) y la nueva generación de &lt;strong>speculative decoding&lt;/strong> (EAGLE-3, DeepSeek MTP, Mirror Speculative). Este artículo desmonta PagedAttention al nivel del bloque, explica qué hace vLLM hoy en su lugar, y traza el mapa del estado del arte para que no te pierdas eligiendo entre quince siglas en la primera reunión.&lt;/p>
&lt;blockquote>
&lt;p>Este artículo cierra una mini-serie. El primero —&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a>— explicó por qué cada token consume VRAM. El segundo —&lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes: la pieza de inferencia LLM que sí escala&lt;/a>— mostró cómo se sirve eso en producción. Éste baja al fondo: cómo se gestiona el cache &lt;strong>dentro&lt;/strong> del motor, y qué hay después de PagedAttention.&lt;/p>
&lt;/blockquote>
&lt;h2 id="la-analogía-pasar-de-malloc-al-kernel-multiproceso">La analogía: pasar de &lt;code>malloc()&lt;/code> al kernel multiproceso&lt;/h2>
&lt;p>Un programa C ingenuo pide memoria con &lt;code>malloc(N)&lt;/code> y recibe un bloque contiguo de N bytes. Si pide muchos bloques de tamaños distintos y los libera en cualquier orden, el heap se llena de huecos: hay tres megabytes libres en total, pero ningún hueco contiguo de un megabyte, y el siguiente &lt;code>malloc(1MB)&lt;/code> falla. Fragmentación externa. Si reserva siempre el peor caso &amp;ldquo;para estar seguro&amp;rdquo; —&lt;code>malloc(MAX_POSSIBLE_SIZE)&lt;/code>— el heap se queda lleno con bloques medio vacíos. Fragmentación interna.&lt;/p>
&lt;p>Los sistemas operativos modernos no permiten que eso pase con la memoria virtual de un proceso. La memoria virtual se divide en &lt;strong>páginas&lt;/strong> (4 KB típicamente), cada una asignada a un &lt;strong>marco físico&lt;/strong> en RAM mediante una &lt;strong>tabla de páginas&lt;/strong> específica del proceso. El proceso ve un espacio contiguo enorme; el SO lo respalda con marcos físicos dispersos, asignados bajo demanda y liberados cuando dejan de usarse. El concepto tiene 50 años y funciona.&lt;/p>
&lt;p>Antes de PagedAttention, &lt;strong>los motores de inferencia LLM eran programas C ingenuos&lt;/strong>. Cada sesión reservaba un bloque contiguo de KV cache dimensionado al peor caso &lt;code>max_context_len × bytes_per_token × n_layers × 2&lt;/code>. Una conversación que usa 273 tokens reservaba sitio para 32 768. Cuando el motor servía 50 sesiones simultáneas, el 60-80% de la VRAM dedicada a KV cache estaba reservada y vacía. El paper de PagedAttention midió este desperdicio en cargas reales y propuso lo evidente: tratar el KV cache como &lt;strong>memoria virtual&lt;/strong>. Bloques físicos pequeños (16 tokens), tabla de páginas por sesión, asignación bajo demanda. El resultado: &amp;lt; 4% de desperdicio, 2-4× más throughput agregado en el mismo hardware.&lt;/p>
&lt;p>La idea no era nueva fuera del mundo LLM, era nueva &lt;strong>dentro&lt;/strong>. Y eso vale como contribución: a veces traer una técnica madura de otro campo es más impactante que inventar algo desde cero.&lt;/p>
&lt;h2 id="el-paper-original-en-cristiano">El paper original, en cristiano&lt;/h2>
&lt;p>Kwon et al. publicaron &lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em> en SOSP 2023 e implementaron simultáneamente vLLM, que en seis meses pasó de proyecto académico a &amp;ldquo;el motor de inferencia que todo el mundo usa&amp;rdquo;. Las tres aportaciones del paper, en orden de importancia:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Cuantificación del problema&lt;/strong>: medir el desperdicio en sistemas existentes y mostrar que el 60-80% de la VRAM se estaba quemando en &lt;em>peor-caso reservations&lt;/em> que no se usaban.&lt;/li>
&lt;li>&lt;strong>El algoritmo de paging&lt;/strong>: cómo dividir el KV cache, qué tamaño de bloque elegir, cómo gestionar la tabla de páginas en GPU.&lt;/li>
&lt;li>&lt;strong>El kernel CUDA&lt;/strong>: cómo implementar la operación de atención cuando los tokens de una secuencia están dispersos por la VRAM, sin destruir el rendimiento.&lt;/li>
&lt;/ol>
&lt;h3 id="el-modelo-de-bloques">El modelo de bloques&lt;/h3>
&lt;p>El KV cache se divide en bloques de tamaño fijo. La elección por defecto en vLLM es &lt;strong>16 tokens por bloque&lt;/strong>, decisión que el paper justifica con un barrido empírico: bloques más pequeños reducen la fragmentación interna pero aumentan el overhead de metadata y de indirección; bloques más grandes mejoran throughput pero pierden eficiencia. 16 es el punto razonable para los modelos y cargas medidas.&lt;/p>
&lt;p>Cada bloque almacena los &lt;strong>K y V de N tokens consecutivos&lt;/strong> de &lt;strong>una sola sesión&lt;/strong> en &lt;strong>una sola capa&lt;/strong> del modelo. Para un Llama 3 8B con 32 capas, una sesión de 128 tokens necesita aproximadamente &lt;code>128 / 16 × 32 = 256 bloques&lt;/code> (uno por capa por grupo de 16 tokens). Los bloques son lógicamente independientes entre sí: pueden vivir en cualquier dirección física de VRAM.&lt;/p>
&lt;h3 id="la-tabla-de-páginas-block-table">La tabla de páginas (block table)&lt;/h3>
&lt;p>Cada sesión tiene asociada una &lt;strong>block table&lt;/strong>: una lista ordenada de identificadores de bloques físicos. Cuando vLLM calcula la atención para el token 200 de la sesión X, mira la block table de X, encuentra que el bloque que contiene el token 200 está en la posición &lt;code>200 / 16 = 12&lt;/code> de la lista, lee qué bloque físico corresponde y va a buscarlo.&lt;/p>
&lt;p>La block table vive en VRAM, no en RAM como la tabla de páginas del SO. Si viviese en CPU, cada paso de decode tendría que hacer una indirección PCIe, lo que mataría el throughput. Está en VRAM, junto al cache, y el kernel CUDA la lee como una estructura más durante el cómputo.&lt;/p>
&lt;div class="diagram" style="max-width:720px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Block table apuntando a bloques físicos dispersos">
&lt;style>.title{font:600 13px sans-serif;fill:#222}.lbl{font:11px sans-serif;fill:#444}.bt{fill:#ffe9d6;stroke:#666}.blk{fill:#d6eaff;stroke:#666}.free{fill:#eee;stroke:#bbb;stroke-dasharray:3 2}.arr{stroke:#888;stroke-width:1.2;fill:none;marker-end:url(#ah)}&lt;/style>
&lt;defs>&lt;marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#888"/>&lt;/marker>&lt;/defs>
&lt;text x="120" y="20" text-anchor="middle" class="title">Block table (sesión X)&lt;/text>
&lt;text x="500" y="20" text-anchor="middle" class="title">VRAM (pool de bloques físicos)&lt;/text>
&lt;rect x="40" y="40" width="160" height="22" class="bt"/>&lt;text x="120" y="56" text-anchor="middle" class="lbl">posición 0 → bloque #7&lt;/text>
&lt;rect x="40" y="65" width="160" height="22" class="bt"/>&lt;text x="120" y="81" text-anchor="middle" class="lbl">posición 1 → bloque #2&lt;/text>
&lt;rect x="40" y="90" width="160" height="22" class="bt"/>&lt;text x="120" y="106" text-anchor="middle" class="lbl">posición 2 → bloque #11&lt;/text>
&lt;rect x="40" y="115" width="160" height="22" class="bt"/>&lt;text x="120" y="131" text-anchor="middle" class="lbl">posición 3 → bloque #5&lt;/text>
&lt;rect x="40" y="140" width="160" height="22" class="bt"/>&lt;text x="120" y="156" text-anchor="middle" class="lbl">posición 4 → bloque #9&lt;/text>
&lt;rect x="300" y="40" width="60" height="22" class="free"/>&lt;text x="330" y="56" text-anchor="middle" class="lbl">#0 libre&lt;/text>
&lt;rect x="365" y="40" width="60" height="22" class="free"/>&lt;text x="395" y="56" text-anchor="middle" class="lbl">#1 libre&lt;/text>
&lt;rect x="430" y="40" width="60" height="22" class="blk"/>&lt;text x="460" y="56" text-anchor="middle" class="lbl">#2 sesión X&lt;/text>
&lt;rect x="495" y="40" width="60" height="22" class="blk"/>&lt;text x="525" y="56" text-anchor="middle" class="lbl">#3 sesión Y&lt;/text>
&lt;rect x="560" y="40" width="60" height="22" class="blk"/>&lt;text x="590" y="56" text-anchor="middle" class="lbl">#4 sesión Z&lt;/text>
&lt;rect x="625" y="40" width="60" height="22" class="blk"/>&lt;text x="655" y="56" text-anchor="middle" class="lbl">#5 sesión X&lt;/text>
&lt;rect x="300" y="70" width="60" height="22" class="blk"/>&lt;text x="330" y="86" text-anchor="middle" class="lbl">#6 sesión Y&lt;/text>
&lt;rect x="365" y="70" width="60" height="22" class="blk"/>&lt;text x="395" y="86" text-anchor="middle" class="lbl">#7 sesión X&lt;/text>
&lt;rect x="430" y="70" width="60" height="22" class="free"/>&lt;text x="460" y="86" text-anchor="middle" class="lbl">#8 libre&lt;/text>
&lt;rect x="495" y="70" width="60" height="22" class="blk"/>&lt;text x="525" y="86" text-anchor="middle" class="lbl">#9 sesión X&lt;/text>
&lt;rect x="560" y="70" width="60" height="22" class="blk"/>&lt;text x="590" y="86" text-anchor="middle" class="lbl">#10 sesión Z&lt;/text>
&lt;rect x="625" y="70" width="60" height="22" class="blk"/>&lt;text x="655" y="86" text-anchor="middle" class="lbl">#11 sesión X&lt;/text>
&lt;path class="arr" d="M200,51 L365,51"/>
&lt;path class="arr" d="M200,76 L430,51"/>
&lt;path class="arr" d="M200,101 L625,76"/>
&lt;path class="arr" d="M200,126 L625,51"/>
&lt;path class="arr" d="M200,151 L495,81"/>
&lt;text x="360" y="200" text-anchor="middle" class="lbl">los bloques de una misma sesión están dispersos; la block table reconstruye su orden lógico&lt;/text>
&lt;text x="360" y="225" text-anchor="middle" class="lbl">cuando un bloque queda libre (sesión termina), vuelve al pool y otra sesión lo ocupa en el siguiente paso&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Cuando una sesión genera su token N-ésimo, vLLM mira si el último bloque de la block table aún tiene huecos (&lt;code>N mod 16 != 0&lt;/code>). Si los tiene, escribe ahí. Si no, pide un bloque nuevo del &lt;strong>pool global&lt;/strong>, lo añade al final de la block table y escribe en su primera posición. Crecer la sesión cuesta &lt;strong>una asignación O(1) en el pool global más una append O(1) a la block table&lt;/strong>. Liberar una sesión devuelve sus bloques al pool: también O(N_bloques) y rapidísimo.&lt;/p>
&lt;h3 id="el-pool-de-bloques">El pool de bloques&lt;/h3>
&lt;p>El pool global se dimensiona al arrancar el motor. Lo típico:&lt;/p>
&lt;pre tabindex="0">&lt;code>bloques_disponibles = (VRAM_total - modelo - activations - overhead) / block_size_bytes
&lt;/code>&lt;/pre>&lt;p>Para una RTX 4090 (24 GB) sirviendo Llama 3 8B BF16 con cache también en BF16:&lt;/p>
&lt;pre tabindex="0">&lt;code>modelo: ~16 GB
activations: ~1.5 GB
overhead vLLM: ~1 GB
disponible para KV cache: ~5.5 GB
block_size = 16 tokens × 32 capas × 2 (K,V) × 8 KV heads × 128 head_dim × 2 bytes = 2 MB
bloques disponibles ≈ 5.5 GB / 2 MB ≈ 2800 bloques
tokens cacheables totales (todas sesiones) ≈ 2800 × 16 = 44800 ≈ 44 K tokens
&lt;/code>&lt;/pre>&lt;p>Si una sola sesión pide 32 K tokens, ocupa 2 000 bloques (de 2 800). Si las sesiones son más cortas, caben más simultáneas. El pool es &lt;strong>un recurso compartido global&lt;/strong>, no per-sesión, y ahí está la clave del aprovechamiento.&lt;/p>
&lt;h3 id="copy-on-write-para-sampling-paralelo">Copy-on-write para sampling paralelo&lt;/h3>
&lt;p>Una sutileza elegante del paper: cuando una petición usa sampling paralelo o beam search, las N secuencias &lt;strong>comparten el prefijo&lt;/strong> (el prompt + lo que se haya generado hasta el punto de divergencia). En lugar de duplicar el KV cache de ese prefijo, vLLM hace que las N secuencias &lt;strong>compartan los bloques físicos&lt;/strong> vía la block table. Solo cuando una secuencia diverge —genera un token distinto que las otras— vLLM &lt;strong>copia el último bloque&lt;/strong> afectado (no toda la secuencia) y la rama esa pasa a tener su propia versión.&lt;/p>
&lt;p>Esto es exactamente lo que hace el kernel de Linux con &lt;code>fork()&lt;/code>: copy-on-write de las páginas. La memoria solo se duplica cuando se modifica. En beam search con N=4 y prefijos largos, el ahorro es enorme.&lt;/p>
&lt;h3 id="el-kernel-cuda">El kernel CUDA&lt;/h3>
&lt;p>El reto técnico no obvio: el cómputo de atención &lt;strong>debe seguir la indirección de la block table&lt;/strong> para cada token. En la versión naïve (cache contiguo), el kernel asume que los tokens 0..N-1 de la sesión X están en direcciones contiguas y los lee de un tirón. Con paging, los tokens 0..15 están en el bloque #7, los 16..31 en el #2, los 32..47 en el #11, etc.&lt;/p>
&lt;p>El kernel &lt;code>paged_attention&lt;/code> de vLLM resuelve esto con &lt;strong>block-aware tiling&lt;/strong>: divide el cómputo de atención en chunks alineados con el tamaño de bloque (16 tokens), y para cada chunk localiza el bloque físico vía la block table y lo procesa. Es más complejo que el kernel contiguo, pero el coste medido es solo &lt;strong>5-10% de latencia adicional&lt;/strong> frente a la operación contigua equivalente, contra una ganancia de 2-4× en throughput agregado por la mejor utilización de VRAM. Compromiso aplastante.&lt;/p>
&lt;h2 id="evicción-y-preemption-qué-hace-cuando-el-pool-se-agota">Evicción y preemption: qué hace cuando el pool se agota&lt;/h2>
&lt;p>El KV cache crece. Cada token nuevo en cualquier sesión consume bloques. En un servidor con tráfico alto, el pool global se vacía. ¿Qué hacer cuando llega una nueva petición y no hay bloques libres?&lt;/p>
&lt;p>Tres opciones: &lt;strong>rechazar&lt;/strong> la petición (mala UX), &lt;strong>bloquear&lt;/strong> hasta que algo se libere (mala latencia), o &lt;strong>expulsar&lt;/strong> alguna sesión existente para hacer sitio (preemption). vLLM elige la tercera, con dos estrategias seleccionables:&lt;/p>
&lt;h3 id="estrategia-1-recompute">Estrategia 1: recompute&lt;/h3>
&lt;p>Cuando vLLM expulsa una sesión, &lt;strong>libera todos sus bloques&lt;/strong> y la pone en cola de espera. Cuando vuelve a haber sitio (otras sesiones terminan), vLLM rehace el prefill entero de la sesión expulsada desde el prompt original. El KV cache se reconstruye desde cero.&lt;/p>
&lt;p>Ventaja: liberación instantánea, no consume bandwidth de PCIe.
Coste: la sesión rehace &lt;strong>todo el cómputo del prefill&lt;/strong>, segundos o decenas de segundos para prompts largos.&lt;/p>
&lt;h3 id="estrategia-2-swap">Estrategia 2: swap&lt;/h3>
&lt;p>vLLM mueve los bloques de la sesión expulsada &lt;strong>a RAM de CPU&lt;/strong> (vía PCIe), liberando la VRAM. Cuando la sesión vuelva a tocar, vLLM la trae de vuelta a VRAM.&lt;/p>
&lt;p>Ventaja: conserva el cache, no rehace cómputo.
Coste: tiempo de transferencia PCIe (~32 GB/s en PCIe gen4 x16). Mover 4 GB de KV cache cuesta ~125 ms ida y vuelta.&lt;/p>
&lt;p>vLLM elige entre las dos en función del tamaño del cache de la sesión y de la latencia esperada. Para sesiones cortas, recompute suele ganar; para sesiones largas con prompts grandes, swap. Es configurable con &lt;code>--swap-space&lt;/code>.&lt;/p>
&lt;h3 id="el-problema-de-la-preemption-agresiva">El problema de la preemption agresiva&lt;/h3>
&lt;p>Hay un fallo de modo: si el sistema está saturado y vLLM no para de expulsar y reincorporar las mismas sesiones, todas hacen poco progreso y el throughput se hunde. Este es &lt;strong>thrashing&lt;/strong>, exactamente el mismo problema que tiene un SO cuando la presión de paginación es muy alta.&lt;/p>
&lt;p>La solución operativa es la misma que en SO: &lt;strong>admission control&lt;/strong>. Configurar &lt;code>--max-num-seqs&lt;/code> para limitar cuántas sesiones puede atender vLLM simultáneamente. Si llegan más, esperan en la cola HTTP. Mejor tener 10 sesiones avanzando rápido que 100 thrasheando.&lt;/p>
&lt;h2 id="lo-que-vllm-hace-hoy-más-allá-del-paper-original">Lo que vLLM hace hoy: más allá del paper original&lt;/h2>
&lt;p>La documentación oficial de vLLM señala que el &lt;a href="https://docs.vllm.ai/en/latest/design/paged_attention/">paper de PagedAttention es ya un documento histórico&lt;/a> que &lt;strong>ya no describe la implementación actual&lt;/strong>. ¿Qué ha cambiado?&lt;/p>
&lt;h3 id="chunked-prefill-integrado-con-paged-kv">Chunked prefill integrado con paged KV&lt;/h3>
&lt;p>El kernel original asumía que el prefill ocupaba el batch entero un paso, y el decode ocupaba batches separados. El motor actual mezcla prefill (troceado en chunks) con decode en el mismo paso, usando el mismo paged KV cache para ambos. Esto mejora la utilización de tensor cores cuando hay pocas peticiones en prefill y muchas en decode.&lt;/p>
&lt;h3 id="prefix-caching-cross-session">Prefix caching cross-session&lt;/h3>
&lt;p>El paper original ya tenía copy-on-write para sampling paralelo en una sola petición. La extensión natural fue compartir bloques de prefijo entre &lt;strong>peticiones distintas&lt;/strong> que llegan con el mismo system prompt. En vLLM se activa con &lt;code>--enable-prefix-caching&lt;/code>. Es una versión más simple que la de SGLang (no usa radix tree explícito, hace hash de bloques) pero efectiva: 30-70% mejora de TTFT en cargas con prompts compartidos.&lt;/p>
&lt;h3 id="sliding-window-attention">Sliding window attention&lt;/h3>
&lt;p>Modelos como Mistral 7B usan atención con ventana deslizante: solo atienden a los últimos K tokens (4 096 en Mistral). El motor mantiene únicamente los bloques de la ventana activa, liberando los más viejos. Esto cambia la economía: para esos modelos, el cache no crece sin límite.&lt;/p>
&lt;h3 id="flashattention-3-paged">FlashAttention-3 paged&lt;/h3>
&lt;p>Las versiones recientes de FlashAttention (especialmente FA-3) tienen kernels paged-aware optimizados para Hopper (H100). vLLM los usa por defecto en H100 cuando están disponibles, con ganancias adicionales del 15-30% sobre el kernel paged original.&lt;/p>
&lt;h2 id="vattention-paging-sin-reescribir-el-kernel">vAttention: paging sin reescribir el kernel&lt;/h2>
&lt;p>El paper de &lt;a href="https://arxiv.org/abs/2405.04437">vAttention (Prabhu et al., arxiv 2405.04437)&lt;/a> hace una observación incómoda: el coste de PagedAttention no es solo el 5-10% del kernel. Hay dos costes ocultos:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Inadaptable a kernels nuevos&lt;/strong>: cada vez que sale una optimización de atención (FlashAttention-2, FlashAttention-3, kernel custom), hay que &lt;strong>reescribir su versión paged&lt;/strong>. Eso ha hecho que vLLM frecuentemente esté 1-2 versiones por detrás del frente de FlashAttention.&lt;/li>
&lt;li>&lt;strong>Block tables en VRAM&lt;/strong>: pequeño pero constante. Para muchas sesiones, las block tables ocupan VRAM y cuestan accesos.&lt;/li>
&lt;/ol>
&lt;p>La propuesta de vAttention: usar &lt;strong>CUDA Virtual Memory Management (VMM)&lt;/strong>, las primitivas de virtual memory que NVIDIA expone desde CUDA 11.2. Con VMM puedes &lt;strong>reservar un rango virtual contiguo enorme&lt;/strong> y &lt;strong>asignar memoria física bajo demanda&lt;/strong> en porciones, mapeándolas en posiciones del rango virtual. El kernel de atención ve un rango contiguo (no necesita ser paged-aware); el runtime mete el paging dentro de la API de CUDA.&lt;/p>
&lt;p>Resultado medido en el paper: hasta &lt;strong>1.99× decode throughput&lt;/strong> sobre vLLM con FlashAttention-2 original. Y el kernel de atención es el de FlashAttention estándar, sin modificar.&lt;/p>
&lt;p>La idea es disruptiva porque sugiere que &lt;strong>la abstracción del paper de PagedAttention era inadecuada&lt;/strong>: el problema nunca fue que el cache tenía que ser físicamente paginado, sino que la asignación tenía que ser dinámica. La forma de resolverlo es delegar el paging al hardware (MMU + VMM de CUDA), no implementarlo en software.&lt;/p>
&lt;p>vAttention no ha desplazado a PagedAttention en vLLM por inercia y por consideraciones de portabilidad (VMM no está disponible en GPUs AMD ni Intel; PagedAttention sí). Pero los runtimes nuevos —y algunos forks de vLLM— ya lo están adoptando. Es plausible que en 2027 sea el default.&lt;/p>
&lt;h2 id="compresión-y-evicción-inteligente-lo-que-ha-llegado-en-2025-2026">Compresión y evicción inteligente: lo que ha llegado en 2025-2026&lt;/h2>
&lt;p>PagedAttention y vAttention atacan &lt;strong>dónde&lt;/strong> vive el cache. Otra línea de trabajo ataca &lt;strong>qué&lt;/strong> vive en el cache: si no necesitas todo el KV de un contexto largo, ¿por qué guardarlo todo?&lt;/p>
&lt;h3 id="streamingllm-xiao-et-al-2024-los-attention-sinks">StreamingLLM (Xiao et al., 2024): los attention sinks&lt;/h3>
&lt;p>El precursor conceptual. Observación: los primeros 4 tokens de cualquier contexto reciben atención desproporcionada de los tokens posteriores, incluso cuando semánticamente no son relevantes (son &amp;ldquo;sinks&amp;rdquo; para que el softmax se normalice). Si descartas todo el cache excepto los primeros 4 tokens más una ventana deslizante de los últimos K, el modelo sigue generando con calidad razonable indefinidamente.&lt;/p>
&lt;p>Impacto: permite &lt;strong>contexto efectivamente infinito&lt;/strong> con cache acotado. Coste: olvido real del contenido medio.&lt;/p>
&lt;h3 id="h2o-snapkv-2024-eviction-por-attention-score">H2O, SnapKV (2024): eviction por attention score&lt;/h3>
&lt;p>Variantes que mantienen un score acumulado de atención por token y, cuando el cache se llena, descartan los tokens con menor score. Son métodos por sesión, no por sistema: cada sesión decide qué partes de su propio cache descartar.&lt;/p>
&lt;h3 id="evicpress-microsoft-research-2026">EvicPress (Microsoft Research, 2026)&lt;/h3>
&lt;p>El paper &lt;a href="https://arxiv.org/abs/2512.14946">EvicPress: Joint KV-Cache Compression and Eviction for Efficient LLM Serving&lt;/a> hace una observación elegante: hasta ahora, evicción y compresión se han tratado como técnicas separadas. &lt;strong>Si vas a expulsar un bloque, ¿por qué no comprimirlo y guardarlo en RAM o NVMe en lugar de tirarlo?&lt;/strong> Y si lo tienes comprimido en un tier más lento, ¿cuándo merece la pena descomprimirlo y volver a HBM?&lt;/p>
&lt;p>EvicPress modela el problema como &lt;strong>optimización conjunta&lt;/strong> sobre múltiples tiers de almacenamiento (HBM, RAM, NVMe), aplica compresión lossy a los bloques candidatos a evicción y mantiene metadata para decidir cuándo trasladar de un tier a otro. Resultados: &lt;strong>2.19× faster TTFT&lt;/strong> a igual calidad de generación.&lt;/p>
&lt;p>La idea importa porque cambia el framing: el KV cache deja de ser &amp;ldquo;está o no está&amp;rdquo; para pasar a ser &amp;ldquo;está, en qué tier, con qué fidelidad&amp;rdquo;. Es directamente análogo a la jerarquía de caches L1/L2/L3 en CPUs.&lt;/p>
&lt;h3 id="kv-cache-transform-coding-kvtc-2026">KV Cache Transform Coding (KVTC, 2026)&lt;/h3>
&lt;p>&lt;a href="https://arxiv.org/abs/2511.01815">KV Cache Transform Coding for Compact Storage in LLM Inference (arxiv 2511.01815)&lt;/a> aplica al KV cache una técnica clásica de compresión de imágenes y vídeo: &lt;strong>transform coding&lt;/strong>, similar a DCT en JPEG/MPEG. Descompone los bloques de KV en una base de transformadas, descarta los coeficientes de menor energía y guarda el resto. Testeado con Llama 3, Mistral NeMo y R1-Qwen 2.5, &lt;strong>supera a quantization (INT4) y a SVD&lt;/strong> como métodos de compresión del cache. Importante: el resultado es &lt;strong>un cache comprimido reutilizable&lt;/strong>, no comprimido on-the-fly cada vez.&lt;/p>
&lt;h3 id="laprox-2026">LaProx (2026)&lt;/h3>
&lt;p>&lt;a href="https://arxiv.org/abs/2605.07234">LaProx: Reformulating KV Cache Eviction Problem for Long-Context LLM Inference (arxiv 2605.07234)&lt;/a> reformula la evicción de KV cache. Hasta ahora la mayoría de métodos son &lt;strong>head-wise y por promedios&lt;/strong> —miran scores por cabeza de atención y los promedian para decidir qué descartar—. LaProx la convierte en un problema &lt;strong>output-aware&lt;/strong> y &lt;strong>layer-wise&lt;/strong>: aproximar la multiplicación entre los attention maps y los projected value states como una matriz que se puede comprimir minimizando el error en la salida real del modelo, no en métricas auxiliares.&lt;/p>
&lt;p>La consecuencia práctica: las decisiones de evicción mejoran porque están alineadas con lo que realmente afecta a la generación, no con un proxy.&lt;/p>
&lt;h2 id="disaggregated-serving-separar-prefill-de-decode">Disaggregated serving: separar prefill de decode&lt;/h2>
&lt;p>PagedAttention y derivados optimizan &lt;strong>un motor&lt;/strong> sirviendo peticiones mezcladas. La siguiente revolución conceptual fue darse cuenta de que &lt;strong>prefill y decode no deberían correr en la misma GPU&lt;/strong>.&lt;/p>
&lt;h3 id="el-problema-de-mezclarlos">El problema de mezclarlos&lt;/h3>
&lt;p>Prefill es &lt;em>compute-bound&lt;/em>: usa los tensor cores intensamente. Decode es &lt;em>memory-bound&lt;/em>: mueve el KV cache a través del HBM. Si los mezclas en el mismo batch, una de las dos fases siempre va a ralentizar a la otra. Si entra una petición con prompt de 32 K tokens mientras hay 50 sesiones en decode, el prefill pausa a todas durante un segundo o más. Si llega una avalancha de prefills, los decodes en curso ven su latencia de token siguiente subir.&lt;/p>
&lt;h3 id="distserve-zhong-et-al-2024">DistServe (Zhong et al., 2024)&lt;/h3>
&lt;p>&lt;a href="https://arxiv.org/abs/2401.09670">DistServe (arxiv 2401.09670)&lt;/a> propuso lo evidente: &lt;strong>dedicar GPUs distintas a prefill y a decode&lt;/strong>. Las peticiones llegan a una GPU de prefill, que procesa el prompt y produce el KV cache inicial; ese KV cache se &lt;strong>transfiere&lt;/strong> a una GPU de decode, que se encarga de generar los tokens uno a uno. Resultado: &lt;strong>7.4× más goodput&lt;/strong>, o el mismo throughput con SLO 12.6× más estrictos.&lt;/p>
&lt;p>El truco no obvio es la transferencia del KV cache entre nodos. En GPUs con NVLink/NVSwitch del mismo nodo es trivial (~300 GB/s). Entre nodos con InfiniBand, el coste es manejable pero no despreciable. DistServe asume topologías que lo soporten.&lt;/p>
&lt;h3 id="splitwise-microsoft-2024">Splitwise (Microsoft, 2024)&lt;/h3>
&lt;p>&lt;a href="https://www.microsoft.com/en-us/research/publication/splitwise-efficient-generative-llm-inference-using-phase-splitting/">Splitwise&lt;/a> llevó la idea un paso más allá: &lt;strong>GPUs heterogéneas&lt;/strong>. Los prefills, compute-bound, corren en H100 o A100 (compute-optimizadas). Los decodes, memory-bound, corren en GPUs con más memoria por dólar pero menor compute (algunas variantes datacenter). Ganancia: &lt;strong>1.4× más throughput por dólar&lt;/strong>.&lt;/p>
&lt;h3 id="2026-producción">2026: producción&lt;/h3>
&lt;p>Disaggregated serving es ya &lt;strong>producción mainstream&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>NVIDIA Dynamo&lt;/strong> (sucesor de Triton): primitivo nativo.&lt;/li>
&lt;li>&lt;strong>vLLM&lt;/strong>: soporta disaggregation con flags &lt;code>--disaggregation-prefill-instances&lt;/code> / &lt;code>--disaggregation-decode-instances&lt;/code>.&lt;/li>
&lt;li>&lt;strong>SGLang&lt;/strong>, &lt;strong>Ray Serve LLM&lt;/strong>, &lt;strong>llm-d&lt;/strong>, &lt;strong>LMCache&lt;/strong>, &lt;strong>Mooncake&lt;/strong>: idem.&lt;/li>
&lt;li>Operadores con stacks propios: Fireworks, Perplexity, Meta, Amazon, Modular, DeepInfra, Weka.&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://haoailab.com/blogs/distserve-retro/">&lt;em>Disaggregated Inference: 18 Months Later&lt;/em> (Hao AI Lab, 2026)&lt;/a> hace una retrospectiva: lo que en 2024 era investigación es, en 2026, &amp;ldquo;como tener separados webservers de bases de datos&amp;rdquo;. Asumido.&lt;/p>
&lt;h3 id="ppd-no-todos-los-prefills-son-iguales-2026">PPD: no todos los prefills son iguales (2026)&lt;/h3>
&lt;p>El refinamiento más reciente: &lt;a href="https://arxiv.org/pdf/2603.13358">Not All Prefills Are Equal: PPD Disaggregation for Multi-turn LLM Serving (arxiv 2603.13358)&lt;/a>. Observación: en cargas multi-turn (asistentes conversacionales, agentes), los &amp;ldquo;prefills&amp;rdquo; sucesivos tienen estructura distinta: el primer turno es prompt nuevo, los siguientes son extensiones del cache anterior. PPD discrimina entre tipos de prefill y los enruta a clusters distintos, mejorando aún el aprovechamiento.&lt;/p>
&lt;h2 id="radixattention-el-camino-alternativo-sglang">RadixAttention: el camino alternativo (SGLang)&lt;/h2>
&lt;p>Mientras vLLM iteraba sobre PagedAttention con prefix caching basado en hashing, &lt;a href="https://github.com/sgl-project/sglang">SGLang&lt;/a> tomó otra ruta: &lt;strong>mantener un trie (radix tree) explícito de todos los prefijos que existen actualmente en el cache&lt;/strong>.&lt;/p>
&lt;h3 id="la-idea">La idea&lt;/h3>
&lt;p>Cuando llega una petición nueva con tokens &lt;code>[t1, t2, t3, ..., tN]&lt;/code>, SGLang baja por el trie tokens-a-tokens. Si los primeros K tokens del prompt coinciden con un camino del trie, esos K tokens &lt;strong>ya tienen su KV cache calculado&lt;/strong> y se reutilizan. Solo se procesa el prefill de los tokens N-K restantes.&lt;/p>
&lt;p>Esto es prefix caching, pero con una estructura de datos que captura &lt;strong>todas las relaciones de prefijo entre todas las sesiones activas simultáneamente&lt;/strong>, no solo los matches exactos de hash. Si dos peticiones comparten 137 tokens iniciales, RadixAttention lo encuentra; si una tercera comparte 89, también.&lt;/p>
&lt;h3 id="eviction-inteligente-del-trie">Eviction inteligente del trie&lt;/h3>
&lt;p>Los nodos del trie tienen un score basado en cuántas veces se han usado recientemente y cuántos descendientes tienen. Cuando hay presión de memoria, SGLang descarta los nodos menos valiosos primero, manteniendo los caminos más &amp;ldquo;calientes&amp;rdquo;. Esto es LRU + un peso por reutilización potencial.&lt;/p>
&lt;h3 id="resultados">Resultados&lt;/h3>
&lt;p>&lt;a href="https://arxiv.org/pdf/2312.07104">El paper de SGLang&lt;/a> y benchmarks posteriores reportan &lt;strong>hasta 6.4× throughput vs sin prefix caching&lt;/strong>, y un gap consistente del &lt;strong>29%&lt;/strong> sobre el prefix caching basado en hash de vLLM en cargas mixtas. En cargas con prefijos muy compartidos (agentes ReAct, multi-tenant SaaS, repo Q&amp;amp;A con system prompt común), los hit rates llegan al &lt;strong>60-85%&lt;/strong> y el ahorro de coste por petición es de &lt;strong>5-12×&lt;/strong>.&lt;/p>
&lt;h3 id="producción">Producción&lt;/h3>
&lt;p>SGLang está en producción en xAI (sirviendo Grok 3) y Microsoft Azure (DeepSeek R1 en GPUs AMD), entre otros. No es un experimento; es un sistema de inferencia maduro.&lt;/p>
&lt;h3 id="cuándo-elegirlo-sobre-vllm">Cuándo elegirlo sobre vLLM&lt;/h3>
&lt;p>Para cargas con prefijos compartidos masivos y predecibles, &lt;strong>SGLang gana claramente&lt;/strong>. Para cargas genéricas mezcladas, &lt;strong>vLLM rinde mejor por simplicidad operativa&lt;/strong>. El criterio operativo: si tu hit rate de prefix caching estimado en vLLM pasaría del 50%, plantéate SGLang.&lt;/p>
&lt;h2 id="speculative-decoding-la-dimensión-ortogonal">Speculative decoding: la dimensión ortogonal&lt;/h2>
&lt;p>PagedAttention y sus sucesores optimizan &lt;strong>dónde y cómo&lt;/strong> vive el cache. Speculative decoding ataca &lt;strong>cómo se generan los tokens&lt;/strong>, ortogonalmente al cache. La idea genérica: usar un modelo pequeño y rápido para &lt;em>adivinar&lt;/em> varios tokens por adelantado, validarlos en paralelo con el modelo grande y aceptar los que coinciden.&lt;/p>
&lt;h3 id="eagle-3-2025">EAGLE-3 (2025)&lt;/h3>
&lt;p>&lt;a href="https://huggingface.co/papers/2401.15077">EAGLE-3 (huggingface.co/papers/2401.15077, versión 3 de 2025)&lt;/a> entrena una cabeza auto-regresiva pequeña que se condiciona en &lt;strong>tres puntos del hidden state del modelo target&lt;/strong> (early, middle, late layers) en lugar de solo en el último. Esta fusión tri-layer es la razón por la que EAGLE-3 supera a EAGLE-2 en un &lt;strong>20-40%&lt;/strong>. Latencia medida: &lt;strong>2-6× speedup&lt;/strong> según tamaño de modelo y batch.&lt;/p>
&lt;h3 id="medusa-y-deepseek-mtp">Medusa y DeepSeek MTP&lt;/h3>
&lt;p>Medusa fija N cabezas de decodificación adicionales al modelo, cada una prediciendo posición +1, +2, +3. DeepSeek-V3 ships con MTP (Multi-Token Prediction) nativo, n=4, &lt;strong>entrenado conjuntamente&lt;/strong> con el modelo principal (no es un drafter externo). En inferencia, basta un flag en SGLang o vLLM (&lt;code>--speculative-model deepseek-v3-mtp&lt;/code>) y obtienes &lt;strong>1.8× speedup out of the box&lt;/strong>, sin entrenar nada adicional, sin pesos extras que hospedar.&lt;/p>
&lt;h3 id="mirror-speculative-decoding-2025">Mirror Speculative Decoding (2025)&lt;/h3>
&lt;p>&lt;a href="https://arxiv.org/pdf/2510.13161">Mirror Speculative Decoding (arxiv 2510.13161)&lt;/a> ataca un límite que se daba por dado: la verificación de los tokens especulados sigue siendo serial dentro del modelo target. Mirror Decoding reorganiza el cómputo para &lt;strong>paralelizar también la verificación&lt;/strong>, rompiendo la barrera serial del paradigma original. Las ganancias añadidas dependen del modelo y del batch, pero el paper lo posiciona como el próximo paso de la trayectoria EAGLE → EAGLE-2 → EAGLE-3.&lt;/p>
&lt;h3 id="estado-en-2026">Estado en 2026&lt;/h3>
&lt;p>Speculative decoding &lt;strong>dejó de ser optimización experimental en 2026&lt;/strong> para convertirse en &lt;strong>capa por defecto de cualquier stack serio&lt;/strong>. Combinado con KV cache optimizado, los números reportados son &lt;strong>2.8× menos latencia&lt;/strong> y &lt;strong>47% menos coste por token&lt;/strong>.&lt;/p>
&lt;p>Caveat operativo: speculative decoding es contraproducente en cargas de baja concurrencia. Si el modelo target tiene poco batch para llenar la GPU, las cabezas especulativas no compensan su overhead. Por debajo de ~4 sesiones simultáneas, suele bajar el throughput. Por encima, lo sube. Mídelo en tu carga antes de activarlo.&lt;/p>
&lt;h2 id="implicaciones-operativas-el-config-2026-para-vllm">Implicaciones operativas: el config 2026 para vLLM&lt;/h2>
&lt;p>Si en 2026 montas vLLM en producción sin pensar mucho, los flags razonables por defecto son:&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">args&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="l">model=...&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="l">tensor-parallel-size=N&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="l">max-model-len=...&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="l">kv-cache-dtype=fp8 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># cuantización del cache&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="l">enable-prefix-caching &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># ahorro fácil en cargas con prompts compartidos&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="l">enable-chunked-prefill &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># mejor mezcla prefill/decode&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="l">gpu-memory-utilization=0.92 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># ya cubierto en el post anterior&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="l">speculative-model=... &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># SI batch sostenido &amp;gt;4&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="l">num-speculative-tokens=4 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># acompaña al anterior&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="l">max-num-seqs=128 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># admission control para evitar thrashing&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="l">preemption-mode=recompute &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># o swap si sesiones largas&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Para cargas con prefijos masivamente compartidos (agentes), considera &lt;strong>migrar a SGLang&lt;/strong>: el delta de eficiencia compensa la curva de aprendizaje. Para cargas de baja latencia con modelos estables (entrenados in-house, no cambias cada semana), &lt;strong>TensorRT-LLM&lt;/strong> sigue ganando en latencia pura. Para todo lo demás —que es la mayoría—, vLLM con los flags de arriba está dentro del 10% del óptimo en throughput.&lt;/p>
&lt;p>Para arquitecturas grandes (&amp;gt;100 sesiones concurrentes, SLO estricto), &lt;strong>disaggregated serving&lt;/strong> ya no es opcional. NVIDIA Dynamo o llm-d como orquestadores; vLLM o SGLang como motores debajo. La división típica: 1 nodo de prefill por cada 3-4 de decode, ajustando ratios según la longitud media de los prompts.&lt;/p>
&lt;h2 id="trampas-y-mitos-comunes">Trampas y mitos comunes&lt;/h2>
&lt;h3 id="pagedattention-vs-vattention-como-dilema">&amp;ldquo;PagedAttention vs vAttention&amp;rdquo; como dilema&lt;/h3>
&lt;p>No es un dilema. vAttention es una optimización de runtime; el modelo mental sigue siendo paging. La elección es entre dos implementaciones del mismo concepto. Operativamente: si tienes la versión de vLLM que lo soporta y CUDA VMM disponible, vAttention da más throughput; si no, paged va perfectamente.&lt;/p>
&lt;h3 id="cache-compression-sin-probar-calidad">&amp;ldquo;Cache compression sin probar calidad&amp;rdquo;&lt;/h3>
&lt;p>La industria de papers de compresión es prolífica y los benchmarks varían enormemente entre los del autor y los reales en producción. Compresión 8× &lt;em>parece&lt;/em> mágico hasta que mides degradación en tu corpus real. &lt;strong>Siempre evalúa con tus datos antes de activar compresión agresiva.&lt;/strong> Un FP8 cache es seguro casi siempre. Un INT4 cache requiere medir caso por caso.&lt;/p>
&lt;h3 id="prefix-caching-con-prompts-no-determinísticos">&amp;ldquo;Prefix caching con prompts no determinísticos&amp;rdquo;&lt;/h3>
&lt;p>Si tu pipeline inyecta timestamps, IDs únicos o cualquier variabilidad en el system prompt, &lt;strong>el hit rate de prefix caching se cae a cero&lt;/strong>. Es la trampa más común. Para que funcione, los prompts compartidos deben ser &lt;strong>bit-a-bit idénticos&lt;/strong>. Estructura los prompts en capas: parte estática primero, variable al final.&lt;/p>
&lt;h3 id="speculative-decoding-en-cargas-bajas">&amp;ldquo;Speculative decoding en cargas bajas&amp;rdquo;&lt;/h3>
&lt;p>Ya lo mencionamos: por debajo de ~4 sesiones simultáneas, speculative suele ser contraproducente. Si tu carga es batch puro o muy esporádica, &lt;strong>no la actives&lt;/strong>.&lt;/p>
&lt;h3 id="disaggregated-en-cluster-sin-red-rápida">&amp;ldquo;Disaggregated en cluster sin red rápida&amp;rdquo;&lt;/h3>
&lt;p>Si tu inter-nodo es Ethernet 25 GbE o peor, la transferencia del KV cache entre prefill y decode se convierte en cuello de botella. Disaggregation es para clusters con InfiniBand o RoCE 100/200/400 GbE. Sin eso, mejor colocated.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;p>Hay terreno suficiente para otra serie:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mooncake (Kimi/Moonshot, 2024+)&lt;/strong>: KV cache como &lt;strong>pool compartido entre instancias&lt;/strong>, persistente en RAM/NVMe. Producción real con cientos de millones de queries.&lt;/li>
&lt;li>&lt;strong>LMCache&lt;/strong>: cache de KV persistente en disco entre arranques de vLLM. Reduce el coste de los primeros tokens en cargas con repetición temporal.&lt;/li>
&lt;li>&lt;strong>vLLM Production Stack&lt;/strong>: distribución k8s-native de vLLM con HPA, métricas, multi-modelo, ya probada en producción a escala.&lt;/li>
&lt;li>&lt;strong>Inference scheduling teórico&lt;/strong>: hay literatura aplicando CFS-like algorithms (el scheduler de Linux) al LLM serving. Promete fairness multi-tenant medible. Aún en fase académica.&lt;/li>
&lt;li>&lt;strong>Quantization del modelo combinada con quantization del cache&lt;/strong>: AWQ/GPTQ sobre los pesos + FP8 sobre el cache + INT4 sobre cache evictado. La pirámide completa.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>Los papers fundacionales y las extensiones más leídas, en orden cronológico:&lt;/p>
&lt;ul>
&lt;li>Kwon et al., &lt;a href="https://arxiv.org/abs/2309.06180">&lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em>&lt;/a> (SOSP 2023) — paper original.&lt;/li>
&lt;li>Dao et al., &lt;a href="https://arxiv.org/abs/2307.08691">&lt;em>FlashAttention-2&lt;/em>&lt;/a> (2023) y &lt;em>FlashAttention-3&lt;/em> (2024) — kernels de atención sobre los que vLLM y vAttention apoyan.&lt;/li>
&lt;li>Xiao et al., &lt;a href="https://arxiv.org/abs/2309.17453">&lt;em>Efficient Streaming Language Models with Attention Sinks&lt;/em>&lt;/a> (StreamingLLM, 2024).&lt;/li>
&lt;li>Zhong et al., &lt;a href="https://arxiv.org/abs/2401.09670">&lt;em>DistServe: Disaggregating Prefill and Decoding for Goodput-optimized LLM Serving&lt;/em>&lt;/a> (OSDI 2024).&lt;/li>
&lt;li>Patel et al., &lt;a href="https://www.microsoft.com/en-us/research/publication/splitwise-efficient-generative-llm-inference-using-phase-splitting/">&lt;em>Splitwise: Efficient Generative LLM Inference Using Phase Splitting&lt;/em>&lt;/a> (Microsoft, 2024).&lt;/li>
&lt;li>Li et al., &lt;a href="https://huggingface.co/papers/2401.15077">&lt;em>EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty&lt;/em>&lt;/a> (2024) y EAGLE-2/3 (2024-2025).&lt;/li>
&lt;li>Prabhu et al., &lt;a href="https://arxiv.org/abs/2405.04437">&lt;em>vAttention: Dynamic Memory Management for Serving LLMs without PagedAttention&lt;/em>&lt;/a> (Microsoft, 2024-2025).&lt;/li>
&lt;li>Zheng et al., &lt;a href="https://arxiv.org/pdf/2312.07104">&lt;em>SGLang: Efficient Execution of Structured Language Model Programs&lt;/em>&lt;/a> (RadixAttention, 2024).&lt;/li>
&lt;li>DeepSeek-AI, &lt;a href="https://arxiv.org/abs/2412.19437">&lt;em>DeepSeek-V3 Technical Report&lt;/em>&lt;/a> (2024) — MTP nativo, base de speculative decoding del estado del arte.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/pdf/2510.13161">&lt;em>Mirror Speculative Decoding: Breaking the Serial Barrier in LLM Inference&lt;/em>&lt;/a> (2025).&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2511.01815">&lt;em>KV Cache Transform Coding for Compact Storage in LLM Inference&lt;/em>&lt;/a> (KVTC, 2026).&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2512.14946">&lt;em>EvicPress: Joint KV-Cache Compression and Eviction for Efficient LLM Serving&lt;/em>&lt;/a> (Microsoft Research, 2026).&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2605.07234">&lt;em>LaProx: Reformulating KV Cache Eviction Problem for Long-Context LLM Inference&lt;/em>&lt;/a> (2026).&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/pdf/2603.13358">&lt;em>Not All Prefills Are Equal: PPD Disaggregation for Multi-turn LLM Serving&lt;/em>&lt;/a> (2026).&lt;/li>
&lt;/ul>
&lt;p>Operacional:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://docs.vllm.ai/en/latest/design/paged_attention/">vLLM Paged Attention design doc&lt;/a> — la propia doc señala que el paper original es ya &amp;ldquo;historical&amp;rdquo;.&lt;/li>
&lt;li>&lt;a href="https://haoailab.com/blogs/distserve-retro/">Disaggregated Inference: 18 Months Later&lt;/a> — Hao AI Lab @ UCSD, retrospectiva de la transición a disaggregated.&lt;/li>
&lt;li>&lt;a href="https://www.marktechpost.com/2026/04/29/top-10-kv-cache-compression-techniques-for-llm-inference-reducing-memory-overhead-across-eviction-quantization-and-low-rank-methods/">Top 10 KV Cache Compression Techniques for LLM Inference&lt;/a> — survey reciente útil como mapa.&lt;/li>
&lt;li>Artículos anteriores en este blog: &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a> y &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes: la pieza de inferencia LLM que sí escala&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>vLLM en Kubernetes: la pieza de inferencia LLM que sí escala</title><link>https://blog.lo0.es/posts/vllm-kubernetes/</link><pubDate>Mon, 18 May 2026 13:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/vllm-kubernetes/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>vLLM es el motor de inferencia que convierte una GPU de propósito general en un servidor LLM productivo. Su valor no está en correr un modelo —eso lo hace cualquier &lt;code>transformers.pipeline&lt;/code> con tres líneas de Python— sino en &lt;strong>exprimir la GPU hasta el último gigabyte y el último ciclo&lt;/strong>: PagedAttention para el KV cache, &lt;em>continuous batching&lt;/em> para mezclar peticiones, scheduler propio para repartir tiempo de GPU entre sesiones. Kubernetes es su hábitat natural porque vLLM se comporta como un proceso UNIX moderno —tiene endpoint de health, métricas Prometheus, draining ordenado, recursos declarables— y K8s ya sabe cómo gestionarlos. Pero hay trampas: el HPA estándar no escala vLLM bien, el modelo tarda minutos en cargar, y los rolling updates ingenuos cortan sesiones a medio decodificar. Este artículo desmonta el motor y luego lo encaja, con manifests reales, en un cluster que sí pueda servirlo.&lt;/p>
&lt;blockquote>
&lt;p>Este artículo es la continuación natural de &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a>. Allí explicamos por qué cada token consume VRAM. Aquí vemos qué se hace con esa VRAM cuando la quieres ofrecer como servicio.&lt;/p>
&lt;/blockquote>
&lt;h2 id="la-analogía-kernel-multiproceso-para-tu-gpu">La analogía: kernel multiproceso para tu GPU&lt;/h2>
&lt;p>Imagina que tienes un único procesador y necesitas servir cien procesos concurrentes sin que ninguno bloquee a los demás. Nadie en su sano juicio escribiría un bucle &lt;code>while-true&lt;/code> que despacha procesos uno a uno: instalaría un sistema operativo. El kernel se encarga del scheduling, de la paginación de memoria, del aislamiento, de las prioridades, de la limpieza al terminar. El &amp;ldquo;proceso&amp;rdquo; se convierte en una abstracción cómoda y el kernel hace el trabajo sucio.&lt;/p>
&lt;p>vLLM es, para tu GPU, lo que el kernel es para tu CPU. Frente a la GPU, una conversación con un LLM es &lt;strong>un proceso que vive durante muchos pasos de decodificación&lt;/strong>, ocupa una porción de VRAM (su KV cache) y demanda tiempo de cómputo cada vez que toca generar un token. Tienes cien de esos procesos a la vez. Necesitas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Repartir tiempo de GPU entre ellos&lt;/strong> sin pausarlos enteros (sería desastroso si una conversación larga monopoliza la GPU).&lt;/li>
&lt;li>&lt;strong>Gestionar la memoria con paginación&lt;/strong> porque, igual que en RAM, reservar contiguo es ineficiente.&lt;/li>
&lt;li>&lt;strong>Encolar peticiones nuevas&lt;/strong> cuando la GPU está saturada y servirlas en orden razonable.&lt;/li>
&lt;li>&lt;strong>Recuperar recursos&lt;/strong> cuando una sesión termina.&lt;/li>
&lt;/ul>
&lt;p>PagedAttention es la &lt;strong>memoria virtual&lt;/strong> del KV cache. &lt;em>Continuous batching&lt;/em> es el &lt;strong>scheduler con time-slicing&lt;/strong> que reparte la GPU token a token. El servidor OpenAI-compatible es la &lt;strong>interfaz de syscalls&lt;/strong> uniforme. Llamarlo &amp;ldquo;kernel&amp;rdquo; para la GPU es marketing, pero es marketing que captura bien la idea.&lt;/p>
&lt;h2 id="qué-hace-vllm-por-dentro">Qué hace vLLM por dentro&lt;/h2>
&lt;h3 id="continuous-batching-dejar-de-esperar-al-más-lento">Continuous batching: dejar de esperar al más lento&lt;/h3>
&lt;p>El motor de inferencia naïve hace &lt;em>static batching&lt;/em>: agrupa N peticiones, las procesa hasta que &lt;strong>todas&lt;/strong> terminan, devuelve y empieza otra ronda. El problema es obvio: si una petición pide 8 tokens y otra pide 800, las otras siete esperan a la lenta. La utilización de GPU se cae a plomo.&lt;/p>
&lt;p>&lt;em>Continuous batching&lt;/em> (Yu et al., 2022, popularizado por vLLM) cambia el modelo. En cada paso de decode —que produce un token para cada sesión activa— el motor compone el batch con &lt;strong>los tokens activos de TODAS las sesiones que estén vivas en ese instante&lt;/strong>. Cuando una sesión termina su generación, libera su slot inmediatamente y otra petición de la cola lo ocupa. El batch nunca se queda esperando a la sesión más lenta porque nadie está bloqueado: todos avanzan al ritmo de un token por paso.&lt;/p>
&lt;p>El paper original midió &lt;strong>5–23× más throughput&lt;/strong> que el static batching equivalente. El número exacto depende de la variabilidad de la longitud de las respuestas, pero el orden de magnitud se mantiene en la práctica.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 240" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Static vs continuous batching">
&lt;style>.title{font:600 13px sans-serif;fill:#222}.lbl{font:11px sans-serif;fill:#444}.s1{fill:#2a9d8f}.s2{fill:#e76f51}.s3{fill:#264653}.s4{fill:#e9c46a}.empty{fill:#eee;stroke:#999;stroke-dasharray:3 2}&lt;/style>
&lt;text x="180" y="20" text-anchor="middle" class="title">Static batching&lt;/text>
&lt;text x="540" y="20" text-anchor="middle" class="title">Continuous batching&lt;/text>
&lt;text x="20" y="55" class="lbl">sesión 1&lt;/text>
&lt;text x="20" y="80" class="lbl">sesión 2&lt;/text>
&lt;text x="20" y="105" class="lbl">sesión 3&lt;/text>
&lt;text x="20" y="130" class="lbl">sesión 4&lt;/text>
&lt;rect x="70" y="40" width="40" height="20" class="s1"/>
&lt;rect x="70" y="65" width="120" height="20" class="s2"/>
&lt;rect x="70" y="90" width="60" height="20" class="s3"/>
&lt;rect x="70" y="115" width="30" height="20" class="s4"/>
&lt;rect x="110" y="40" width="80" height="20" class="empty"/>
&lt;rect x="130" y="90" width="60" height="20" class="empty"/>
&lt;rect x="100" y="115" width="90" height="20" class="empty"/>
&lt;text x="180" y="160" text-anchor="middle" class="lbl">slots vacíos esperan a la sesión 2&lt;/text>
&lt;rect x="380" y="40" width="40" height="20" class="s1"/>
&lt;rect x="420" y="40" width="80" height="20" class="s3"/>
&lt;rect x="500" y="40" width="40" height="20" class="s4"/>
&lt;rect x="540" y="40" width="40" height="20" class="s1"/>
&lt;rect x="380" y="65" width="120" height="20" class="s2"/>
&lt;rect x="500" y="65" width="40" height="20" class="s3"/>
&lt;rect x="540" y="65" width="80" height="20" class="s4"/>
&lt;rect x="380" y="90" width="60" height="20" class="s3"/>
&lt;rect x="440" y="90" width="50" height="20" class="s2"/>
&lt;rect x="490" y="90" width="40" height="20" class="s4"/>
&lt;rect x="530" y="90" width="100" height="20" class="s1"/>
&lt;rect x="380" y="115" width="30" height="20" class="s4"/>
&lt;rect x="410" y="115" width="80" height="20" class="s2"/>
&lt;rect x="490" y="115" width="60" height="20" class="s3"/>
&lt;rect x="550" y="115" width="80" height="20" class="s1"/>
&lt;text x="540" y="160" text-anchor="middle" class="lbl">slots se reasignan token a token&lt;/text>
&lt;line x1="70" y1="190" x2="630" y2="190" stroke="#666"/>
&lt;text x="350" y="210" text-anchor="middle" class="lbl">tiempo →&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>La consecuencia para el operador es contraintuitiva: &lt;strong>una sola réplica vLLM rinde como tres réplicas naïve&lt;/strong>. No tiene sentido añadir pods sin justificarlo con métricas reales.&lt;/p>
&lt;h3 id="pagedattention-la-memoria-virtual-del-kv-cache">PagedAttention: la memoria virtual del KV cache&lt;/h3>
&lt;p>Ya lo dejamos apuntado en el artículo del KV cache: el motor naïve reserva un bloque contiguo por sesión, dimensionado al &lt;em>peor caso&lt;/em> (&lt;code>max_context_len&lt;/code>), y desperdicia el 60–80% de la VRAM porque las sesiones reales no llegan ni de lejos a su techo.&lt;/p>
&lt;p>PagedAttention pide prestada la solución que los sistemas operativos llevan medio siglo usando: &lt;strong>dividir la VRAM en bloques pequeños&lt;/strong> (16 tokens en la implementación por defecto) y mantener una &lt;strong>tabla de páginas lógicas → físicas&lt;/strong> por sesión. Una sesión que tiene 273 tokens de contexto ocupa 18 bloques (no necesariamente contiguos), y crece de bloque en bloque conforme genera. El paper midió &lt;strong>&amp;lt;4% de desperdicio&lt;/strong> —un orden de magnitud mejor que la asignación contigua— y eso se traduce en &lt;strong>2–4× más throughput agregado&lt;/strong> en el mismo hardware, porque caben más sesiones a la vez.&lt;/p>
&lt;p>Hay un coste: cada operación de atención debe indirectarse por la tabla de páginas. Pero los kernels CUDA de vLLM están escritos para que esa indirección sea barata, y el resultado neto es masivamente positivo.&lt;/p>
&lt;h3 id="prefill-vs-decode-dos-fases-con-perfiles-opuestos">Prefill vs decode: dos fases con perfiles opuestos&lt;/h3>
&lt;p>Una petición LLM tiene dos fases con perfiles de GPU radicalmente distintos:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Prefill&lt;/strong>: procesa el prompt entero de golpe. Es &lt;em>compute-bound&lt;/em>: usa los tensor cores intensamente, la GPU está al 90%+, dura entre cientos de ms y unos pocos segundos según el tamaño del prompt.&lt;/li>
&lt;li>&lt;strong>Decode&lt;/strong>: genera token a token. Es &lt;em>memory-bound&lt;/em>: el cómputo es modesto pero hay que leer el KV cache entero por cada token, dura desde unas decenas de ms por token hasta minutos para respuestas largas.&lt;/li>
&lt;/ul>
&lt;p>Un servidor naïve trata cada petición como una unidad y sirve las dos fases en serie. vLLM las desacopla: mezcla peticiones en prefill con peticiones en decode en el mismo paso (técnica llamada &lt;em>chunked prefill&lt;/em> cuando además trocea prefills largos). Resultado: la GPU está siempre ocupada haciendo &lt;em>algo&lt;/em> —los tensor cores con prefills, el ancho de banda HBM con decodes— en lugar de oscilar entre fases.&lt;/p>
&lt;p>Implicación operativa: la métrica &amp;ldquo;% utilización GPU&amp;rdquo; del &lt;code>nvidia-smi&lt;/code> engaña. Una GPU al 100% haciendo prefills puede tener su HBM bandwidth ocioso. Una GPU al 40% haciendo decodes puede tener el HBM saturado. Para LLM serving, &lt;strong>la métrica útil es el ancho de banda HBM efectivo&lt;/strong>, no el porcentaje de cómputo.&lt;/p>
&lt;h3 id="tensor-parallel-cuando-el-modelo-no-cabe-en-una-gpu">Tensor parallel: cuando el modelo no cabe en una GPU&lt;/h3>
&lt;p>Llama 3 70B en BF16 son ~140 GB. No hay una sola GPU en el mercado que lo aguante. La solución es &lt;strong>tensor parallel&lt;/strong>: dividir cada capa del modelo por columnas y ejecutar las particiones en N GPUs en paralelo, sincronizando con un &lt;em>all-reduce&lt;/em> tras cada capa.&lt;/p>
&lt;p>Para N=5 GPUs y un modelo de 70B, cada GPU ve aproximadamente 28 GB de pesos. Suena bien hasta que recuerdas que el all-reduce de cada capa significa &lt;strong>leer y escribir tensores grandes entre GPUs&lt;/strong>. Si las GPUs comparten &lt;strong>NVLink/NVSwitch&lt;/strong> (300–900 GB/s), el all-reduce es barato. Si comparten solo PCIe (~32 GB/s gen4 x16), el all-reduce se come la mitad del tiempo y el throughput se hunde.&lt;/p>
&lt;p>Implicación para K8s, que viene a continuación: el scheduler tiene que &lt;strong>garantizar que las N GPUs estén físicamente cerca&lt;/strong>. Esto se traduce en NodeAffinity al producto correcto (&lt;code>NVIDIA-H100-80GB-HBM3&lt;/code>), pod único con &lt;code>nvidia.com/gpu: N&lt;/code> (no N pods compartiendo) y, si hace falta multi-nodo, InfiniBand con NCCL como transporte.&lt;/p>
&lt;h3 id="el-servidor-openai-compatible">El servidor OpenAI-compatible&lt;/h3>
&lt;p>Por encima de todo lo anterior, vLLM expone un servidor HTTP con endpoints idénticos a los de OpenAI: &lt;code>/v1/chat/completions&lt;/code>, &lt;code>/v1/completions&lt;/code>, &lt;code>/v1/embeddings&lt;/code>, &lt;code>/v1/models&lt;/code>. Soporta streaming Server-Sent Events. Soporta tool calling. Soporta logprobs.&lt;/p>
&lt;p>El valor de esto es enorme y se subestima: &lt;strong>cualquier cliente que use la SDK de OpenAI funciona sin cambios&lt;/strong>. Tu aplicación apunta a &lt;code>https://vllm.tu-cluster.local/v1&lt;/code> en vez de a &lt;code>https://api.openai.com/v1&lt;/code>, y todo lo demás —los SDKs de LangChain, LlamaIndex, OpenAI Python, OpenAI JS— funciona. Es la razón principal por la que vLLM ha ganado tracción sobre alternativas técnicamente comparables: &lt;strong>es la opción aburrida que funciona&lt;/strong>.&lt;/p>
&lt;h2 id="por-qué-kubernetes-es-el-hábitat-natural">Por qué Kubernetes es el hábitat natural&lt;/h2>
&lt;p>vLLM es un proceso bien comportado: arranca, expone métricas, atiende un endpoint de health, recibe SIGTERM con dignidad, declara los recursos que necesita. Kubernetes lleva diez años perfeccionando la gestión de procesos así. Lo único que K8s ha tardado en absorber bien es la GPU, y eso ya está resuelto.&lt;/p>
&lt;h3 id="gpu-como-recurso-primitivo">GPU como recurso primitivo&lt;/h3>
&lt;p>El plumbing es el siguiente:&lt;/p>
&lt;ol>
&lt;li>El nodo tiene driver NVIDIA instalado (o lo instala el GPU Operator).&lt;/li>
&lt;li>Un DaemonSet, &lt;strong>nvidia-device-plugin&lt;/strong>, registra las GPUs físicas como recursos &lt;code>nvidia.com/gpu&lt;/code> ante kubelet.&lt;/li>
&lt;li>El scheduler de Kubernetes ve esos recursos como ve CPU y memoria, los pone en su contabilidad y los asigna a Pods que los piden.&lt;/li>
&lt;li>El &lt;strong>nvidia-container-toolkit&lt;/strong> se asegura de que containerd inyecte los devices correctos en el contenedor al arrancar.&lt;/li>
&lt;/ol>
&lt;p>Para el pod, pedir una GPU es esto:&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">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&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">nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">limits&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">nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Sin MIG ni MPS ni time-slicing configurados, &lt;strong>una GPU no se comparte entre pods&lt;/strong>: la pides entera o no la pides. Para vLLM —que quiere toda la GPU para sí— esto es lo deseable.&lt;/p>
&lt;h3 id="el-ciclo-de-vida-del-pod-vllm">El ciclo de vida del Pod vLLM&lt;/h3>
&lt;p>Diferencias con un Pod de webapp típico:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Startup largo&lt;/strong>. Cargar 16 GB de pesos en VRAM por encima de la red tarda 30 segundos en el mejor caso y 5 minutos en el peor. Una &lt;code>readinessProbe&lt;/code> con &lt;code>initialDelaySeconds: 30&lt;/code> y &lt;code>failureThreshold: 3&lt;/code> mata el pod antes de que arranque. Solución: &lt;code>startupProbe&lt;/code> con threshold alto antes de que la &lt;code>livenessProbe&lt;/code> empiece a evaluar.&lt;/li>
&lt;li>&lt;strong>Warm-up útil&lt;/strong>. El primer prefill compila kernels CUDA específicos del shape de entrada. Las primeras 2–3 peticiones son sensiblemente más lentas. Si la latencia importa desde el segundo 1, conviene disparar un POST de warm-up tras el ready.&lt;/li>
&lt;li>&lt;strong>Draining no instantáneo&lt;/strong>. SIGTERM no debe matar las sesiones en curso. vLLM, configurado con &lt;code>--disable-graceful-shutdown false&lt;/code> (default), termina las peticiones activas antes de cerrar. Esto puede tardar 30–180 segundos. &lt;code>terminationGracePeriodSeconds&lt;/code> debe acomodarlo.&lt;/li>
&lt;li>&lt;strong>Rollouts hostiles&lt;/strong>. Un rolling update naïve (&lt;code>maxUnavailable: 1&lt;/code>) puede dejarte sin réplicas atendiendo si la nueva tarda en cargar. Pon &lt;code>maxSurge: 1, maxUnavailable: 0&lt;/code> para que el pod nuevo esté Ready antes de drenar el viejo.&lt;/li>
&lt;/ul>
&lt;h2 id="anatomía-de-un-despliegue-en-serio">Anatomía de un despliegue en serio&lt;/h2>
&lt;h3 id="antes-que-nada-gpu-operator">Antes que nada: GPU Operator&lt;/h3>
&lt;p>Sin GPU Operator (o instalación manual equivalente), un Pod con &lt;code>nvidia.com/gpu: 1&lt;/code> se queda &lt;strong>Pending&lt;/strong> para siempre. Lo que el operator instala como DaemonSets en cada nodo con GPU:&lt;/p>
&lt;ul>
&lt;li>&lt;code>nvidia-driver-daemonset&lt;/code> — el driver kernel-mode (si no lo tienes instalado al nivel del host).&lt;/li>
&lt;li>&lt;code>nvidia-device-plugin-daemonset&lt;/code> — registra las GPUs como recurso de kubelet.&lt;/li>
&lt;li>&lt;code>nvidia-container-toolkit-daemonset&lt;/code> — la integración con containerd.&lt;/li>
&lt;li>&lt;code>nvidia-dcgm-exporter&lt;/code> — métricas Prometheus de la GPU (utilización, temperatura, ECC errors, memoria).&lt;/li>
&lt;li>&lt;code>gpu-feature-discovery&lt;/code> — labels del nodo: &lt;code>nvidia.com/gpu.product&lt;/code>, &lt;code>nvidia.com/gpu.memory&lt;/code>, etc., imprescindibles para NodeAffinity.&lt;/li>
&lt;/ul>
&lt;p>La instalación recomendada es el chart Helm oficial. La parte sensible es alinear el driver con la versión del kernel del host: si los nodos llevan kernel 6.x, el operator necesita un branch de driver compatible.&lt;/p>
&lt;h3 id="deployment-vllm-completo-y-comentado">Deployment vLLM completo y comentado&lt;/h3>
&lt;p>Lo siguiente despliega Llama 3 8B con KV cache cuantizado FP8, hasta 32K de contexto, en una RTX 4090. Es el manifest de referencia; los comentarios explican las decisiones no obvias.&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">apps/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">Deployment&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">vllm-llama3-8b&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">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inference&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">strategy&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">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RollingUpdate&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">rollingUpdate&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">maxSurge&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">maxUnavailable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># nunca quedarse sin réplicas durante el rollout&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">selector&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">matchLabels&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">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama3-8b&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">template&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">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">labels&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">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama3-8b&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">annotations&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">prometheus.io/scrape&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prometheus.io/port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prometheus.io/path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/metrics&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Solo nodos con la GPU que esperamos&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">nodeSelector&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">nvidia.com/gpu.product&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NVIDIA-GeForce-RTX-4090&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">tolerations&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">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nvidia.com/gpu&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">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Exists&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="c"># Predescargar pesos si no están en el PVC compartido&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">initContainers&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">model-download&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io/huggingface/huggingface-cli:latest&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">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;-c&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&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="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"> if [ ! -f /models/llama-3-8b/config.json ]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> huggingface-cli download meta-llama/Meta-Llama-3-8B-Instruct \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --local-dir /models/llama-3-8b --local-dir-use-symlinks False
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> fi&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">env&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">HF_TOKEN&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">valueFrom&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">secretKeyRef&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">huggingface&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">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">token&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">volumeMounts&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">models&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">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/models&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">containers&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">vllm&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm/vllm-openai:v0.6.3&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">args&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="l">model=/models/llama-3-8b&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="l">served-model-name=llama-3-8b&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="l">tensor-parallel-size=1&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="l">max-model-len=32768&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="l">kv-cache-dtype=fp8&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="l">enable-chunked-prefill&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="l">enable-prefix-caching&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="l">gpu-memory-utilization=0.92&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="l">port=8000&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">ports&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">http&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">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&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">metrics&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">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># mismo puerto que http; /metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&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">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;4&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">8Gi&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">nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">limits&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">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;8&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">16Gi&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">nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">startupProbe&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">httpGet&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">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/health&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">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&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">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&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">failureThreshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">60&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 10 min de gracia para cargar el modelo&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">readinessProbe&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">httpGet&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">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/health&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">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&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">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&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">livenessProbe&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">httpGet&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">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/health&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">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&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">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&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">failureThreshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&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">volumeMounts&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">models&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">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/models&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">readOnly&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># ningún proceso debe escribir aquí en runtime&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">shm&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">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/dev/shm &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># vLLM usa shared memory para IPC entre workers&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">volumes&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">models&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">persistentVolumeClaim&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">claimName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">model-cache&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">shm&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">emptyDir&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">medium&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Memory&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">sizeLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">4Gi&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">terminationGracePeriodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">120&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># acomoda drenaje de sesiones activas&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="nn">---&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">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">Service&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">vllm-llama3-8b&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">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inference&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&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">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama3-8b&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">ports&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">http&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">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&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">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Cinco cosas que no se ven en primera lectura:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>/dev/shm&lt;/code> en memoria, 4 GB&lt;/strong>. vLLM lanza procesos worker (uno por GPU en tensor parallel, además del driver) que se comunican por shared memory. El default de Docker (64 MB) revienta en cuanto el modelo es mediano. Sin esto, el pod arranca pero falla en cuanto sirve la primera petición compleja.&lt;/li>
&lt;li>&lt;strong>&lt;code>--enable-prefix-caching&lt;/code>&lt;/strong>. Si los prompts de tu carga comparten estructura (system prompt común, few-shot examples), vLLM reutiliza el KV cache de la parte común. Ganancia gratis del 30–60% en TTFT.&lt;/li>
&lt;li>&lt;strong>&lt;code>--gpu-memory-utilization=0.92&lt;/code>&lt;/strong>. vLLM reserva el % indicado de la VRAM para sí. El 8% restante deja margen para activations, kernels CUDA, y el overhead que no se cuenta. Bajarlo da seguridad; subirlo más de 0.95 invita al OOM.&lt;/li>
&lt;li>&lt;strong>PVC &lt;code>ReadOnlyMany&lt;/code>&lt;/strong> ideal. El modelo no cambia en runtime. Varios pods pueden montar el mismo PVC sin contención.&lt;/li>
&lt;li>&lt;strong>Ningún &lt;code>livenessProbe&lt;/code> que tarde menos que el &lt;code>terminationGracePeriodSeconds&lt;/code>&lt;/strong>. Si un drain tarda 90s y la liveness mata a los 60s, los rollouts pierden sesiones.&lt;/li>
&lt;/ol>
&lt;h3 id="tensor-parallel-multi-pod-leaderworkerset">Tensor parallel multi-pod: LeaderWorkerSet&lt;/h3>
&lt;p>Cuando el modelo necesita más GPUs de las que tiene un solo nodo, el patrón es &lt;strong>un grupo de pods coordinados, uno por GPU, que se comportan como una única réplica&lt;/strong>. Esto se modeló durante años con StatefulSet más init scripts; desde Kubernetes 1.32, el primitivo idiomático es &lt;strong>LeaderWorkerSet&lt;/strong> (LWS):&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">leaderworkerset.x-k8s.io/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">LeaderWorkerSet&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">vllm-llama3-70b&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">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inference&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">leaderWorkerTemplate&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">size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 1 leader + 4 workers = 5 pods, 5 GPUs&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">restartPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RecreateGroupOnPodRestart&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">leaderTemplate&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">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelector&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">nvidia.com/gpu.product&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NVIDIA-H100-80GB-HBM3&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">containers&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">vllm-leader&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm/vllm-openai:v0.6.3&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">args&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="l">model=/models/llama-3-70b&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="l">tensor-parallel-size=5&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="l">distributed-executor-backend=ray&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="c"># ...&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">workerTemplate&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">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelector&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">nvidia.com/gpu.product&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NVIDIA-H100-80GB-HBM3&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">containers&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">vllm-worker&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm/vllm-openai:v0.6.3&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="c"># los workers se unen al cluster Ray del leader&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>LWS garantiza el orden de arranque (workers primero, leader después) y el ciclo de vida atómico (si un worker cae, se reinicia el grupo entero, no un solo pod). Sin esto, la coordinación es manualmente frágil.&lt;/p>
&lt;p>Una alternativa más sencilla, si todas las GPUs del tensor parallel caben en &lt;strong>un solo nodo&lt;/strong> (caso de los HGX H100 con 8 GPUs y NVSwitch interno): un único Pod con &lt;code>nvidia.com/gpu: 5&lt;/code>, &lt;code>--tensor-parallel-size=5&lt;/code>, y vLLM se encarga de todo internamente. Sin Ray, sin LWS, mucho más simple. Es el camino recomendado cuando se puede.&lt;/p>
&lt;h3 id="autoscaling-hpa-estándar-no-sirve">Autoscaling: HPA estándar no sirve&lt;/h3>
&lt;p>El HPA por CPU% es inútil para vLLM. La GPU hace el trabajo; la CPU del pod está al 5–10% incluso al máximo de carga. Tampoco sirve el porcentaje de utilización de la GPU del &lt;code>dcgm-exporter&lt;/code>: un pod al 100% de GPU% con &lt;code>gpu_cache_usage_perc=15%&lt;/code> está atendiendo una sesión larga sin saturar, mientras que un pod al 60% de GPU% con &lt;code>gpu_cache_usage_perc=95%&lt;/code> está al borde de la expulsión de sesiones.&lt;/p>
&lt;p>Las métricas correctas las exporta el propio vLLM en &lt;code>/metrics&lt;/code> (formato Prometheus):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Qué dice&lt;/th>
&lt;th>Cuándo escalar&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>vllm:num_requests_waiting&lt;/code>&lt;/td>
&lt;td>Peticiones encoladas sin entrar al batch.&lt;/td>
&lt;td>Si pasa de 5–10 sostenidos.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vllm:num_requests_running&lt;/code>&lt;/td>
&lt;td>Peticiones activas en el batch.&lt;/td>
&lt;td>Para capacity planning, no para escalar.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vllm:gpu_cache_usage_perc&lt;/code>&lt;/td>
&lt;td>% del KV cache ocupado.&lt;/td>
&lt;td>Si &amp;gt;80% sostenido, hay riesgo de preemption.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vllm:time_to_first_token_seconds&lt;/code>&lt;/td>
&lt;td>Latencia del prefill (histograma).&lt;/td>
&lt;td>Si p95 supera tu SLA.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vllm:e2e_request_latency_seconds&lt;/code>&lt;/td>
&lt;td>Latencia total por petición.&lt;/td>
&lt;td>Métrica de salida.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Para que el HPA las consuma, dos caminos: &lt;strong>Prometheus Adapter&lt;/strong> (expone métricas custom al API de K8s) o &lt;strong>KEDA&lt;/strong> (escala por queries Prometheus directamente, mucho más cómodo). Con KEDA:&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">keda.sh/v1alpha1&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">ScaledObject&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">vllm-scaler&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">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inference&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scaleTargetRef&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">vllm-llama3-8b&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">minReplicaCount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">maxReplicaCount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8&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">pollingInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&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">cooldownPeriod&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">120&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 2 min antes de scale-down (sesiones largas)&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">triggers&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">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">prometheus&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">serverAddress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://prometheus.monitoring:9090&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">threshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;5&amp;#39;&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">query&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"> sum(vllm:num_requests_waiting{app=&amp;#34;vllm-llama3-8b&amp;#34;})&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El &lt;code>cooldownPeriod&lt;/code> largo es importante: si bajas réplicas mientras hay sesiones decodificando, las matas. Mejor 2 minutos de holgura.&lt;/p>
&lt;h3 id="observabilidad-las-cuatro-métricas-que-importan">Observabilidad: las cuatro métricas que importan&lt;/h3>
&lt;p>De todo lo que &lt;code>/metrics&lt;/code> exporta, un dashboard mínimo necesita estas cuatro:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>TTFT p50/p95&lt;/strong> (time to first token) — lo que percibe el usuario al pulsar enviar.&lt;/li>
&lt;li>&lt;strong>TPOT p50/p95&lt;/strong> (time per output token) — la &amp;ldquo;velocidad&amp;rdquo; del streaming.&lt;/li>
&lt;li>&lt;strong>Throughput agregado&lt;/strong> (tokens generados/segundo del cluster) — para capacity planning.&lt;/li>
&lt;li>&lt;strong>Queue depth&lt;/strong> (&lt;code>vllm:num_requests_waiting&lt;/code>) — el indicador adelantado: si crece, todo se va a degradar.&lt;/li>
&lt;/ol>
&lt;p>A esto se le suma utilización HBM y memoria libre por GPU (de &lt;code>dcgm-exporter&lt;/code>) para detectar saturación de bandwidth y problemas de fragmentación. Un dashboard Grafana decente con esas 6 gráficas adelanta el 90% de los incidentes.&lt;/p>
&lt;h2 id="dos-escenarios-concretos">Dos escenarios concretos&lt;/h2>
&lt;p>Reutilizamos los mismos hardwares del artículo anterior para tener continuidad. Mismas matemáticas de cache, ahora con el motor montado.&lt;/p>
&lt;h3 id="escenario-a--1rtx-4090-workstation-o-nodo-k8s-pequeño">Escenario A — 1×RTX 4090 (workstation o nodo K8s pequeño)&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Topología&lt;/strong>: 1 Pod, &lt;code>--tensor-parallel-size=1&lt;/code>, 1 GPU, 1 nodo.&lt;/li>
&lt;li>&lt;strong>Modelo&lt;/strong>: hasta 8B BF16 (Llama 3 8B, Qwen3 8B, Mistral 7B) o hasta 14B en FP8/AWQ.&lt;/li>
&lt;li>&lt;strong>PVC&lt;/strong>: SSD local del nodo. La 4090 lee 1 TB/s de HBM; un SSD NVMe a 5 GB/s tarda 5 segundos en alimentar 25 GB de pesos a VRAM, despreciable frente a la inicialización.&lt;/li>
&lt;li>&lt;strong>HPA&lt;/strong>: irrelevante dentro de la 4090 (siempre 1 réplica de vLLM por GPU), pero útil entre nodos: 3 réplicas en 3 nodos con 4090 cada uno, el Service de K8s reparte round-robin.&lt;/li>
&lt;li>&lt;strong>Concurrencia útil&lt;/strong>: 4–8 sesiones simultáneas con 8K de contexto, 1–2 con 32K.&lt;/li>
&lt;li>&lt;strong>Caso de uso natural&lt;/strong>: PoC, equipos pequeños, ambientes departamentales, edge.&lt;/li>
&lt;/ul>
&lt;p>El manifest de arriba está dimensionado para este escenario. Cambiando solo el modelo y los args, el mismo Deployment sirve Qwen, Mistral o el que toque.&lt;/p>
&lt;h3 id="escenario-b--5h100-sxm-cluster-con-nvlinknvswitch">Escenario B — 5×H100 SXM (cluster con NVLink/NVSwitch)&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Topología&lt;/strong>: 1 Pod con &lt;code>nvidia.com/gpu: 5&lt;/code> en un nodo HGX, &lt;code>--tensor-parallel-size=5&lt;/code>. Si la plataforma no permite agrupar 5 GPUs en un solo Pod, &lt;strong>LeaderWorkerSet&lt;/strong> con 5 pods coordinados por Ray.&lt;/li>
&lt;li>&lt;strong>Modelo&lt;/strong>: hasta 70B BF16 (Llama 3 70B) o hasta 200B+ en FP8 con cuantización del cache.&lt;/li>
&lt;li>&lt;strong>PVC&lt;/strong>: NVMe directamente atado al nodo, o storage en red &lt;strong>rápido&lt;/strong> (Ceph con red 25/100 GbE, Lustre, GPFS). Cargar 140 GB de pesos por una red lenta tarda 5 minutos por arranque.&lt;/li>
&lt;li>&lt;strong>HPA&lt;/strong>: irrelevante dentro del cluster de 5 GPUs (las 5 son una unidad indivisible), pero útil añadiendo más nodos HGX completos cuando la carga pasa de cierto umbral. Esto se combina con Cluster Autoscaler si la infraestructura subyacente lo permite.&lt;/li>
&lt;li>&lt;strong>Concurrencia útil&lt;/strong>: 32–128 sesiones simultáneas con contextos medianos, 4–16 con contextos enormes.&lt;/li>
&lt;li>&lt;strong>Caso de uso natural&lt;/strong>: servicio interno corporativo, exposición pública con SLA, multi-tenant.&lt;/li>
&lt;/ul>
&lt;h3 id="a-y-b-lado-a-lado">A y B, lado a lado&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Aspecto&lt;/th>
&lt;th>A (1×4090)&lt;/th>
&lt;th>B (5×H100 SXM)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Topología Pod&lt;/td>
&lt;td>1 pod, 1 GPU&lt;/td>
&lt;td>1 pod con 5 GPUs (o LWS de 5)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Modelo máximo BF16&lt;/td>
&lt;td>8 B&lt;/td>
&lt;td>70 B&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TTFT @ 8K contexto, idle&lt;/td>
&lt;td>~250 ms&lt;/td>
&lt;td>~80 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TPOT, idle&lt;/td>
&lt;td>~30 ms/tok&lt;/td>
&lt;td>~15 ms/tok&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput @ concurrencia 16&lt;/td>
&lt;td>~50 tok/s/sesión&lt;/td>
&lt;td>~200 tok/s/sesión&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Drain de sesiones&lt;/td>
&lt;td>30–60 s&lt;/td>
&lt;td>60–180 s&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Autoscaling útil&lt;/td>
&lt;td>Réplicas en nodos pares&lt;/td>
&lt;td>Nodos completos vía Cluster Autoscaler&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-tenancy razonable&lt;/td>
&lt;td>Limitada: 4–8 sesiones&lt;/td>
&lt;td>Holgada: 32–128 sesiones&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Coste indicativo (hardware)&lt;/td>
&lt;td>~2 K €&lt;/td>
&lt;td>~250 K € (≈ 125×)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La asimetría sigue siendo la del artículo anterior: 125× más caro, sólo ~4× más throughput por sesión y ~10× más concurrencia. Lo que el cluster compra no es proporcional; compra &lt;strong>acceso a modelos un orden de magnitud más grandes&lt;/strong> y &lt;strong>latencias suficientemente bajas para uso interactivo a escala&lt;/strong>. Si tu carga es batch o agentes asincrónicos donde la latencia no es crítica, varias 4090s rinden sorprendentemente cerca.&lt;/p>
&lt;h2 id="vllm-frente-a-tensorrt-llm-y-sglang">vLLM frente a TensorRT-LLM y SGLang&lt;/h2>
&lt;p>Honestamente, los tres son buenos motores. La elección depende de criterios prácticos, no técnicos. Mapa de decisión, no benchmark:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Criterio&lt;/th>
&lt;th>vLLM&lt;/th>
&lt;th>TensorRT-LLM&lt;/th>
&lt;th>SGLang&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Hardware soportado&lt;/td>
&lt;td>NVIDIA, AMD ROCm, Intel Gaudi&lt;/td>
&lt;td>NVIDIA exclusivamente&lt;/td>
&lt;td>NVIDIA, AMD ROCm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Latencia pura (TTFT)&lt;/td>
&lt;td>Buena&lt;/td>
&lt;td>&lt;strong>Mejor&lt;/strong>: kernels compilados al hardware exacto&lt;/td>
&lt;td>Buena&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput agregado&lt;/td>
&lt;td>&lt;strong>Excelente&lt;/strong>&lt;/td>
&lt;td>Excelente&lt;/td>
&lt;td>Excelente (RadixAttention)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Despliegue&lt;/td>
&lt;td>&lt;strong>Trivial&lt;/strong>: imagen Docker + args&lt;/td>
&lt;td>Complejo: build engine por modelo + por GPU&lt;/td>
&lt;td>Moderado&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>API OpenAI-compatible&lt;/td>
&lt;td>&lt;strong>Nativa, completa&lt;/strong>&lt;/td>
&lt;td>Sí, a través de Triton Inference Server&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Soporte de modelos nuevos&lt;/td>
&lt;td>&lt;strong>Días tras release&lt;/strong>&lt;/td>
&lt;td>Semanas (recompilar engine)&lt;/td>
&lt;td>Días&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quantization&lt;/td>
&lt;td>AWQ, GPTQ, FP8 cache&lt;/td>
&lt;td>INT4/INT8/FP8 muy maduros&lt;/td>
&lt;td>AWQ, FP8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-modal&lt;/td>
&lt;td>Sí (Llava, Pixtral, Qwen-VL)&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>&lt;strong>Excelente&lt;/strong>, prioritario&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Function calling / tool use&lt;/td>
&lt;td>Bueno&lt;/td>
&lt;td>Limitado&lt;/td>
&lt;td>&lt;strong>Primera clase&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Comunidad / cadencia release&lt;/td>
&lt;td>&lt;strong>Muy activa, semanal&lt;/strong>&lt;/td>
&lt;td>Activa, NVIDIA-driven&lt;/td>
&lt;td>Muy activa, académica&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licencia&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Cuándo elegir cada uno&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>vLLM&lt;/strong>: el &amp;ldquo;boring choice&amp;rdquo; que funciona. Camino con menos fricción para llegar a producción. Si tu equipo no tiene un especialista dedicado al inference serving, esto. Soporta hardware variado, modelos al día, API estable, comunidad enorme.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>TensorRT-LLM&lt;/strong>: cuando la latencia por petición es la métrica única que importa y tu modelo es estable (entrenado in-house, no cambias cada quincena). El precio del rendimiento es que cada modelo + cada GPU + cada versión de TRT requiere rebuild del engine, y eso bloquea iteración rápida.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>SGLang&lt;/strong>: para cargas dominadas por agentes (tool calling intensivo) o multi-modal complejo. Su RadixAttention —caching estructural de prompts con prefijos compartidos— brilla en patrones tipo ReAct donde el mismo system prompt se repite miles de veces.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>Para la mayoría de equipos que están empezando con LLM serving on-prem, &lt;strong>vLLM es la respuesta correcta hasta que tengas datos en producción que te empujen a otra cosa&lt;/strong>.&lt;/p>
&lt;h2 id="trampas-operativas-frecuentes">Trampas operativas frecuentes&lt;/h2>
&lt;p>Una lista de gotchas que se ven una y otra vez:&lt;/p>
&lt;h3 id="el-modelo-se-descarga-en-cada-rolling-update">El modelo se descarga en cada rolling update&lt;/h3>
&lt;p>&lt;strong>Síntoma&lt;/strong>: cada deploy tarda 5+ minutos en estar disponible.
&lt;strong>Causa&lt;/strong>: no hay PVC compartido. Cada pod nuevo descarga el modelo desde Hugging Face de cero.
&lt;strong>Remedio&lt;/strong>: PVC ReadOnlyMany sobre un storage rápido, o un mirror local del registry (un Pod con &lt;code>huggingface-cli&lt;/code> que sirve un directorio por HTTP). En CI/CD, hidratar el PVC antes del rollout es 1 línea de bash.&lt;/p>
&lt;h3 id="readiness-con-timeout-corto-que-mata-pods-cargando">readiness con timeout corto que mata pods cargando&lt;/h3>
&lt;p>&lt;strong>Síntoma&lt;/strong>: pods nuevos entran en &lt;code>CrashLoopBackOff&lt;/code> durante la primera carga del modelo.
&lt;strong>Causa&lt;/strong>: &lt;code>readinessProbe&lt;/code> con timeout demasiado bajo dispara antes de que vLLM termine de cargar; &lt;code>livenessProbe&lt;/code> lo remata.
&lt;strong>Remedio&lt;/strong>: &lt;code>startupProbe&lt;/code> con &lt;code>failureThreshold: 60&lt;/code> o más (10 minutos de gracia) antes de que la liveness empiece a evaluar.&lt;/p>
&lt;h3 id="kv-cache-sin-cuantizar-y-luego-oom">KV cache sin cuantizar y luego OOM&lt;/h3>
&lt;p>&lt;strong>Síntoma&lt;/strong>: el pod arranca bien, atiende cinco minutos, &lt;strong>OOMKilled&lt;/strong> cuando llega la sesión número cinco con contexto largo.
&lt;strong>Causa&lt;/strong>: KV cache en BF16 (default) consume el doble que en FP8.
&lt;strong>Remedio&lt;/strong>: &lt;code>--kv-cache-dtype=fp8&lt;/code>. Pérdida de calidad despreciable en la inmensa mayoría de casos, capacidad duplicada.&lt;/p>
&lt;h3 id="confundir-réplicas-con-concurrencia">Confundir réplicas con concurrencia&lt;/h3>
&lt;p>&lt;strong>Síntoma&lt;/strong>: el HPA escala a 8 réplicas con poca carga real y la factura cloud sube. La latencia no mejora.
&lt;strong>Causa&lt;/strong>: alguien configuró &lt;code>targetAverageUtilization: 50%&lt;/code> sobre CPU, pensando que es &amp;ldquo;carga&amp;rdquo;. Realidad: una sola réplica vLLM atiende decenas de sesiones simultáneas.
&lt;strong>Remedio&lt;/strong>: HPA sobre &lt;code>vllm:num_requests_waiting&lt;/code>. Si la cola está vacía, una réplica basta aunque la GPU esté al 90%.&lt;/p>
&lt;h3 id="tensor-parallel-en-gpus-sin-nvlink">Tensor parallel en GPUs sin NVLink&lt;/h3>
&lt;p>&lt;strong>Síntoma&lt;/strong>: throughput 3× peor del esperado, GPUs al 30%, mucho tráfico PCIe.
&lt;strong>Causa&lt;/strong>: &lt;code>tensor_parallel=4&lt;/code> en 4 GPUs conectadas solo por PCIe; el all-reduce satura el bus en cada capa.
&lt;strong>Remedio&lt;/strong>: o las GPUs comparten NVLink/NVSwitch (modelos SXM/HGX), o &lt;strong>pipeline parallel&lt;/strong> (peor latencia pero menos all-reduce), o reduces TP y aceptas que no cabe el modelo entero.&lt;/p>
&lt;h3 id="sesiones-cortadas-en-rolling-update">Sesiones cortadas en rolling update&lt;/h3>
&lt;p>&lt;strong>Síntoma&lt;/strong>: usuarios ven respuestas truncadas durante el deploy.
&lt;strong>Causa&lt;/strong>: &lt;code>terminationGracePeriodSeconds: 30&lt;/code> (default) no llega para drenar generaciones largas.
&lt;strong>Remedio&lt;/strong>: &lt;code>terminationGracePeriodSeconds: 120–180&lt;/code>. Combinado con &lt;code>maxUnavailable: 0&lt;/code>, los rollouts son invisibles para los usuarios activos.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>vLLM con LoRA adapters en caliente&lt;/strong>: servir un base model + N adapters específicos por tenant sin recargar pesos.&lt;/li>
&lt;li>&lt;strong>Disaggregated serving&lt;/strong>: separar prefill y decode en pods especializados, cada uno optimizado para su perfil de GPU.&lt;/li>
&lt;li>&lt;strong>Quantization deep-dive&lt;/strong>: AWQ vs GPTQ vs FP8 dinámico vs FP4, trade-offs reales, cuándo cada uno.&lt;/li>
&lt;li>&lt;strong>Gateway API + AI Inference Extensions&lt;/strong>: la propuesta sigwg para que los LLMs sean ciudadanos de primera en K8s (routing por modelo, sticky session por conversación, fairness multi-tenant).&lt;/li>
&lt;li>&lt;strong>Multi-modal serving&lt;/strong>: el mismo runtime, otro tipo de peticiones —imágenes, audio, embeddings—.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Kwon et al., &lt;a href="https://arxiv.org/abs/2309.06180">&lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em>&lt;/a> (SOSP 2023) — paper original de vLLM.&lt;/li>
&lt;li>Yu et al., &lt;a href="https://www.usenix.org/conference/osdi22/presentation/yu">&lt;em>Orca: A Distributed Serving System for Transformer-Based Generative Models&lt;/em>&lt;/a> (OSDI 2022) — paper que popularizó &lt;em>continuous batching&lt;/em>.&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/">Documentación oficial de vLLM&lt;/a> — operacional y bien mantenida.&lt;/li>
&lt;li>&lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/">NVIDIA GPU Operator&lt;/a> — instalación y troubleshooting de la capa GPU en Kubernetes.&lt;/li>
&lt;li>&lt;a href="https://kubernetes.io/blog/2024/04/16/introducing-leaderworkerset/">LeaderWorkerSet&lt;/a> — primitivo para workloads coordinados como tensor parallel multi-pod.&lt;/li>
&lt;li>&lt;a href="https://keda.sh/">KEDA&lt;/a> — autoscaling event-driven, idóneo para escalar por métricas de cola.&lt;/li>
&lt;li>&lt;a href="https://github.com/NVIDIA/TensorRT-LLM">TensorRT-LLM&lt;/a> y &lt;a href="https://github.com/sgl-project/sglang">SGLang&lt;/a> — los dos comparables más serios.&lt;/li>
&lt;li>&lt;a href="https://lmsys.org/">LMSYS Chatbot Arena&lt;/a> — benchmarks periódicos comparando los tres motores.&lt;/li>
&lt;li>Artículo previo en este blog: &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>KV cache: la memoria de trabajo que sostiene la inferencia LLM</title><link>https://blog.lo0.es/posts/kv-cache-fundamentos/</link><pubDate>Mon, 18 May 2026 10:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/kv-cache-fundamentos/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El KV cache es la &lt;strong>memoria de trabajo&lt;/strong> que un modelo de lenguaje mantiene durante una conversación. Sin él, cada token nuevo obligaría a recalcular toda la conversación desde el principio, con un coste &lt;strong>cuadrático&lt;/strong> en la longitud del texto. Con él, el coste es lineal pero a cambio el cache &lt;strong>vive en VRAM y crece con cada token&lt;/strong>. En la práctica, no es el modelo lo que limita cuánto contexto puedes servir: es el KV cache. Para una RTX 4090 con Llama 3 8B, cabe el modelo en 16 GB y queda apenas espacio para ~64 K tokens de cache totales (sumando todas las sesiones simultáneas). Entender este número es la diferencia entre prometerle a un cliente &amp;ldquo;contexto de 128 K&amp;rdquo; y entregárselo.&lt;/p>
&lt;h2 id="estás-aquí-deploy">Estás aquí: Deploy&lt;/h2>
&lt;p>Este post abre la serie de fundamentos de inferencia LLM. Dentro del &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">pipeline LLMOps de seis etapas&lt;/a> que articula todo el sistema, el KV cache vive en la etapa &lt;strong>Deploy&lt;/strong>: es la pieza que dicta cuánto tráfico cabe en tu motor de inferencia y, por tanto, cuánta plataforma puedes ofrecer encima.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#kvm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#kvm)}&lt;/style>
&lt;defs>&lt;marker id="kvm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DEPLOY · KV cache como cuello de botella de VRAM&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-analogía-el-orador-con-amnesia">La analogía: el orador con amnesia&lt;/h2>
&lt;p>Imagina que asistes a una conferencia técnica de dos horas. El ponente, cada vez que va a decir una frase nueva, &lt;strong>rebobina mentalmente toda la charla desde el inicio&lt;/strong>, recompone el hilo, y solo entonces continúa. Su próxima frase requiere rememorar la anterior; la siguiente, las dos anteriores; al cabo de una hora, cada palabra nueva le cuesta una hora de recapitulación. Una conferencia así sería materialmente imposible.&lt;/p>
&lt;p>Ahora imagina al mismo ponente con un cuaderno donde apunta, mientras habla, las dos o tres ideas clave de cada frase: sujeto, objeto, vínculo con lo anterior. Antes de cada frase nueva, ojea el cuaderno y sigue. Su próxima palabra sólo cuesta una ojeada al cuaderno, no rebobinar la charla entera.&lt;/p>
&lt;p>Ese cuaderno, en un transformer, se llama &lt;strong>KV cache&lt;/strong>. Sin él, los modelos de lenguaje conversacionales serían inviables. Con él, son productos comerciales. Pero el cuaderno &lt;strong>pesa&lt;/strong>: y entender cuánto, dónde y por qué, es lo que separa una infraestructura de inferencia que funciona de una que se cae al tercer cliente concurrente.&lt;/p>
&lt;h2 id="el-mecanismo-en-sí-en-cristiano">El mecanismo en sí (en cristiano)&lt;/h2>
&lt;p>Un transformer genera texto &lt;strong>un token cada vez&lt;/strong>. Para decidir el siguiente token, el modelo aplica un mecanismo llamado &lt;strong>atención&lt;/strong> sobre todos los tokens previos: pregunta &amp;ldquo;¿qué partes del contexto anterior son relevantes para predecir lo que viene ahora?&amp;rdquo;.&lt;/p>
&lt;p>Internamente, cada token de entrada se proyecta a tres vectores:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Q&lt;/strong> (Query): &amp;ldquo;qué estoy buscando&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>K&lt;/strong> (Key): &amp;ldquo;qué oferta este token&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>V&lt;/strong> (Value): &amp;ldquo;qué información lleva este token&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>La atención del token actual contra el contexto se calcula multiplicando su &lt;strong>Q&lt;/strong> contra las &lt;strong>K&lt;/strong> de todos los tokens previos, normalizando con softmax, y ponderando las &lt;strong>V&lt;/strong> correspondientes. Resultado: una representación contextualizada del token actual.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 260" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Diagrama del cálculo de atención con Q, K, V">
&lt;style>
.box { fill: #f4f4f4; stroke: #333; stroke-width: 1.5; }
.box-q { fill: #ffe9d6; }
.box-k { fill: #d6eaff; }
.box-v { fill: #d9f5d6; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
.arr { stroke: #444; stroke-width: 1.4; fill: none; marker-end: url(#ah); }
&lt;/style>
&lt;defs>
&lt;marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
&lt;path d="M0,0 L10,5 L0,10 z" fill="#444"/>
&lt;/marker>
&lt;/defs>
&lt;text x="360" y="22" text-anchor="middle" class="lbl">Cálculo de atención para el token N&lt;/text>
&lt;rect x="40" y="60" width="120" height="40" rx="6" class="box box-q"/>
&lt;text x="100" y="85" text-anchor="middle" class="lbl">Q (token N)&lt;/text>
&lt;text x="100" y="115" text-anchor="middle" class="lbl-sm">"qué busco"&lt;/text>
&lt;rect x="280" y="60" width="160" height="40" rx="6" class="box box-k"/>
&lt;text x="360" y="85" text-anchor="middle" class="lbl">K (tokens 1..N)&lt;/text>
&lt;text x="360" y="115" text-anchor="middle" class="lbl-sm">del cache&lt;/text>
&lt;rect x="560" y="60" width="120" height="40" rx="6" class="box box-v"/>
&lt;text x="620" y="85" text-anchor="middle" class="lbl">V (tokens 1..N)&lt;/text>
&lt;text x="620" y="115" text-anchor="middle" class="lbl-sm">del cache&lt;/text>
&lt;path class="arr" d="M160,80 L280,80"/>
&lt;path class="arr" d="M440,80 L560,80"/>
&lt;p>&lt;text x="220" y="74" text-anchor="middle" class="lbl-sm">Q·Kᵀ → softmax&lt;/text>
&lt;text x="500" y="74" text-anchor="middle" class="lbl-sm">× V&lt;/text>&lt;/p>
&lt;rect x="240" y="170" width="240" height="44" rx="6" class="box"/>
&lt;text x="360" y="197" text-anchor="middle" class="lbl">representación del token N&lt;/text>
&lt;path class="arr" d="M620,100 C620,150 480,150 480,170"/>
&lt;path class="arr" d="M100,100 C100,150 240,150 240,170"/>
&lt;/svg>
&lt;/div>
&lt;p>Aquí está la clave: para predecir el token N, sólo necesito &lt;strong>Q nuevo&lt;/strong> (el del token N) y &lt;strong>K, V de todos los tokens anteriores&lt;/strong>. Las K y V de los tokens 1..N-1 no han cambiado desde la iteración anterior. Recalcularlas sería tirar trabajo.&lt;/p>
&lt;p>&lt;strong>El KV cache es exactamente eso: la memoria que guarda K y V de cada token ya procesado, en cada capa del modelo, para no recalcularlos.&lt;/strong>&lt;/p>
&lt;h2 id="por-qué-existe-el-coste-cuadrático-sin-él">Por qué existe: el coste cuadrático sin él&lt;/h2>
&lt;p>Generar un texto de N tokens implica N pasos. En el paso &lt;code>i&lt;/code>, se calcula la atención sobre &lt;code>i&lt;/code> tokens anteriores. Sin cache, en cada paso recomputas las K, V de los &lt;code>i-1&lt;/code> tokens anteriores &lt;strong>más&lt;/strong> las del nuevo. La cuenta total de cómputos de atención crece como:&lt;/p>
&lt;p>$$\sum_{i=1}^{N} i = \frac{N(N+1)}{2} \approx \frac{N^2}{2}$$&lt;/p>
&lt;p>Con KV cache, sólo procesas el token nuevo en cada paso: coste &lt;strong>lineal en N&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparativa de coste lineal vs cuadrático">
&lt;style>
.ax { stroke: #333; stroke-width: 1.5; }
.grid { stroke: #ddd; stroke-width: 1; stroke-dasharray: 3,3; }
.lin { stroke: #2a9d8f; stroke-width: 2.5; fill: none; }
.quad { stroke: #e76f51; stroke-width: 2.5; fill: none; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #555; }
.tag-lin { fill: #2a9d8f; font: 600 12px sans-serif; }
.tag-quad { fill: #e76f51; font: 600 12px sans-serif; }
&lt;/style>
&lt;text x="360" y="20" text-anchor="middle" class="lbl">Cómputo acumulado para generar N tokens&lt;/text>
&lt;line class="ax" x1="80" y1="270" x2="680" y2="270"/>
&lt;line class="ax" x1="80" y1="40" x2="80" y2="270"/>
&lt;text x="380" y="300" text-anchor="middle" class="lbl-sm">tokens generados (N)&lt;/text>
&lt;text x="30" y="155" text-anchor="middle" class="lbl-sm" transform="rotate(-90 30 155)">cómputo relativo&lt;/text>
&lt;line class="grid" x1="80" y1="220" x2="680" y2="220"/>
&lt;line class="grid" x1="80" y1="170" x2="680" y2="170"/>
&lt;line class="grid" x1="80" y1="120" x2="680" y2="120"/>
&lt;line class="grid" x1="80" y1="70" x2="680" y2="70"/>
&lt;p>&lt;text x="75" y="274" text-anchor="end" class="lbl-sm">0&lt;/text>
&lt;text x="75" y="224" text-anchor="end" class="lbl-sm">25%&lt;/text>
&lt;text x="75" y="174" text-anchor="end" class="lbl-sm">50%&lt;/text>
&lt;text x="75" y="124" text-anchor="end" class="lbl-sm">75%&lt;/text>
&lt;text x="75" y="74" text-anchor="end" class="lbl-sm">100%&lt;/text>&lt;/p>
&lt;p>&lt;text x="80" y="285" text-anchor="middle" class="lbl-sm">0&lt;/text>
&lt;text x="230" y="285" text-anchor="middle" class="lbl-sm">1K&lt;/text>
&lt;text x="380" y="285" text-anchor="middle" class="lbl-sm">2K&lt;/text>
&lt;text x="530" y="285" text-anchor="middle" class="lbl-sm">3K&lt;/text>
&lt;text x="680" y="285" text-anchor="middle" class="lbl-sm">4K&lt;/text>&lt;/p>
&lt;!-- Lineal: pendiente suave -->
&lt;path class="lin" d="M80,270 L680,265"/>
&lt;!-- Cuadrática: parábola -->
&lt;path class="quad" d="M80,270 Q380,270 680,70"/>
&lt;p>&lt;text x="640" y="258" class="tag-lin">con KV cache (lineal)&lt;/text>
&lt;text x="500" y="100" class="tag-quad">sin KV cache (cuadrático)&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;p>Los números concretos son demoledores:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Tokens generados&lt;/th>
&lt;th style="text-align:right">Sin KV cache (operaciones)&lt;/th>
&lt;th style="text-align:right">Con KV cache&lt;/th>
&lt;th style="text-align:right">Ratio&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">8 256&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">64×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">1 024&lt;/td>
&lt;td style="text-align:right">524 800&lt;/td>
&lt;td style="text-align:right">1 024&lt;/td>
&lt;td style="text-align:right">512×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">4 096&lt;/td>
&lt;td style="text-align:right">8 390 656&lt;/td>
&lt;td style="text-align:right">4 096&lt;/td>
&lt;td style="text-align:right">2 048×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">32 768&lt;/td>
&lt;td style="text-align:right">536 887 296&lt;/td>
&lt;td style="text-align:right">32 768&lt;/td>
&lt;td style="text-align:right">16 384×&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A los 32 K tokens, &lt;strong>el cache te ahorra cuatro órdenes de magnitud&lt;/strong> de cómputo. No es una optimización: es lo que hace que la inferencia conversacional sea posible.&lt;/p>
&lt;h2 id="el-precio-cuánto-pesa-la-mochila">El precio: cuánto pesa la mochila&lt;/h2>
&lt;p>El KV cache se paga en VRAM. La fórmula, por &lt;strong>secuencia&lt;/strong>, es:&lt;/p>
&lt;pre tabindex="0">&lt;code>KV_size = 2 · n_layers · n_kv_heads · head_dim · context_len · bytes_per_param
↑
K y V
&lt;/code>&lt;/pre>&lt;p>Por &lt;strong>token&lt;/strong> (sin el &lt;code>context_len&lt;/code>), es una constante propia del modelo. Veamos números reales:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Modelo&lt;/th>
&lt;th style="text-align:right">n_layers&lt;/th>
&lt;th style="text-align:right">n_kv_heads&lt;/th>
&lt;th style="text-align:right">head_dim&lt;/th>
&lt;th style="text-align:right">Bytes/token (BF16)&lt;/th>
&lt;th style="text-align:right">GB a 8 K ctx&lt;/th>
&lt;th style="text-align:right">GB a 32 K&lt;/th>
&lt;th style="text-align:right">GB a 128 K&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Llama 3 8B (MHA hipotético)&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">524 288&lt;/td>
&lt;td style="text-align:right">4.00&lt;/td>
&lt;td style="text-align:right">16.00&lt;/td>
&lt;td style="text-align:right">64.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Llama 3 8B (GQA real)&lt;/strong>&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">131 072&lt;/td>
&lt;td style="text-align:right">&lt;strong>1.00&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>4.00&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>16.00&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 3 70B (GQA)&lt;/td>
&lt;td style="text-align:right">80&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">327 680&lt;/td>
&lt;td style="text-align:right">2.50&lt;/td>
&lt;td style="text-align:right">10.00&lt;/td>
&lt;td style="text-align:right">40.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Qwen3 8B (GQA)&lt;/td>
&lt;td style="text-align:right">36&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">147 456&lt;/td>
&lt;td style="text-align:right">1.12&lt;/td>
&lt;td style="text-align:right">4.50&lt;/td>
&lt;td style="text-align:right">18.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Mistral 7B (GQA)&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">131 072&lt;/td>
&lt;td style="text-align:right">1.00&lt;/td>
&lt;td style="text-align:right">4.00&lt;/td>
&lt;td style="text-align:right">16.00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Dos lecturas inmediatas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Sin GQA, no hay 128 K que valga.&lt;/strong> Un Llama 3 8B con atención multi-head clásica necesitaría 64 GB sólo de KV cache para una única secuencia con 128 K tokens. Es decir, &lt;strong>no cabe en ninguna GPU consumer&lt;/strong>. Por eso Meta, Mistral y compañía adoptaron Grouped Query Attention.&lt;/li>
&lt;li>&lt;strong>El KV cache puede ser mayor que el modelo.&lt;/strong> Llama 3 8B BF16 ocupa ~16 GB. Con 128 K de contexto, su cache son otros 16 GB. Una sola sesión empata al modelo en VRAM.&lt;/li>
&lt;/ol>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Crecimiento del KV cache con la longitud de contexto">
&lt;style>
.ax { stroke: #333; stroke-width: 1.5; }
.grid { stroke: #ddd; stroke-width: 1; stroke-dasharray: 3,3; }
.l8b { stroke: #2a9d8f; stroke-width: 2.5; fill: none; }
.l70b { stroke: #e76f51; stroke-width: 2.5; fill: none; }
.lq8 { stroke: #6a4c93; stroke-width: 2.5; fill: none; stroke-dasharray: 5,3; }
.lim { stroke: #c1121f; stroke-width: 1.5; stroke-dasharray: 4,4; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #555; }
.tag { font: 600 11px sans-serif; }
&lt;/style>
&lt;text x="360" y="20" text-anchor="middle" class="lbl">KV cache (GB) vs longitud de contexto (1 secuencia, BF16)&lt;/text>
&lt;line class="ax" x1="80" y1="240" x2="680" y2="240"/>
&lt;line class="ax" x1="80" y1="40" x2="80" y2="240"/>
&lt;line class="grid" x1="80" y1="190" x2="680" y2="190"/>
&lt;line class="grid" x1="80" y1="140" x2="680" y2="140"/>
&lt;line class="grid" x1="80" y1="90" x2="680" y2="90"/>
&lt;p>&lt;text x="75" y="244" text-anchor="end" class="lbl-sm">0&lt;/text>
&lt;text x="75" y="194" text-anchor="end" class="lbl-sm">10&lt;/text>
&lt;text x="75" y="144" text-anchor="end" class="lbl-sm">20&lt;/text>
&lt;text x="75" y="94" text-anchor="end" class="lbl-sm">30&lt;/text>
&lt;text x="75" y="44" text-anchor="end" class="lbl-sm">40 GB&lt;/text>&lt;/p>
&lt;p>&lt;text x="80" y="258" text-anchor="middle" class="lbl-sm">0&lt;/text>
&lt;text x="180" y="258" text-anchor="middle" class="lbl-sm">8K&lt;/text>
&lt;text x="305" y="258" text-anchor="middle" class="lbl-sm">32K&lt;/text>
&lt;text x="430" y="258" text-anchor="middle" class="lbl-sm">64K&lt;/text>
&lt;text x="680" y="258" text-anchor="middle" class="lbl-sm">128K&lt;/text>&lt;/p>
&lt;!-- Limite VRAM disponible RTX 4090 (~8 GB libres tras modelo) -->
&lt;line class="lim" x1="80" y1="200" x2="680" y2="200"/>
&lt;text x="680" y="196" text-anchor="end" class="tag" fill="#c1121f">≈ VRAM libre tras cargar 8B en una 4090&lt;/text>
&lt;!-- Llama 3 8B GQA: lineal, 1 GB @8K, 16 GB @128K -->
&lt;path class="l8b" d="M80,240 L180,235 L305,220 L430,200 L680,160"/>
&lt;!-- Qwen3 8B GQA -->
&lt;path class="lq8" d="M80,240 L180,234 L305,217 L430,194 L680,150"/>
&lt;!-- Llama 3 70B GQA -->
&lt;path class="l70b" d="M80,240 L180,228 L305,190 L430,140 L680,40"/>
&lt;p>&lt;text x="690" y="160" class="tag" fill="#2a9d8f">Llama 3 8B&lt;/text>
&lt;text x="690" y="148" class="tag" fill="#6a4c93">Qwen3 8B&lt;/text>
&lt;text x="690" y="42" class="tag" fill="#e76f51">Llama 3 70B&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;p>La línea roja punteada marca la VRAM realista disponible en una RTX 4090 después de cargar el modelo. &lt;strong>Cualquier modelo cuya curva cruza esa línea no podrá servir ese contexto&lt;/strong> sin estrategias adicionales (cuantización del cache, offload, particionado).&lt;/p>
&lt;h2 id="la-inferencia-es-memory-bound-no-compute-bound">La inferencia es memory-bound, no compute-bound&lt;/h2>
&lt;p>Hay un equívoco común: pensar que &amp;ldquo;GPU rápida = inferencia rápida&amp;rdquo;. En el régimen donde realmente operan los servicios de inferencia con KV cache, &lt;strong>lo que se mide es el ancho de banda de memoria&lt;/strong>. Cada token nuevo exige leer las K y V de todos los tokens anteriores desde HBM. El cómputo es modesto; el movimiento de datos, masivo.&lt;/p>
&lt;p>Por eso, una H100 SXM (3.35 TB/s de HBM3) puede ser 2–3× más rápida que una A100 (1.55–2 TB/s) &lt;strong>sin que la frecuencia ni el número de cores expliquen del todo la diferencia&lt;/strong>. Lo explica el ancho de banda.&lt;/p>
&lt;p>Y por eso, también, las ofertas de &amp;ldquo;GPU baratas con mucha VRAM pero HBM lenta&amp;rdquo; (algunas variantes con GDDR6 o LPDDR5) decepcionan en inferencia con contextos largos: tienen sitio para guardar el cache pero les cuesta una eternidad releerlo.&lt;/p>
&lt;h2 id="trucos-para-que-el-cuaderno-sea-más-fino">Trucos para que el cuaderno sea más fino&lt;/h2>
&lt;p>Tres técnicas, en orden cronológico, han ido aplanando el tamaño del KV cache:&lt;/p>
&lt;p>&lt;strong>Multi-Head Attention (MHA).&lt;/strong> El planteamiento original del transformer (Vaswani et al., 2017). Cada cabeza de atención tiene su propia K y V. Caro en cache pero teóricamente máximo en expresividad. Es lo que tenían los modelos hasta ~2023.&lt;/p>
&lt;p>&lt;strong>Multi-Query Attention (MQA).&lt;/strong> Una sola K y V compartida por todas las cabezas. Reduce el cache &lt;code>n_heads&lt;/code> veces. Funciona razonablemente pero degrada calidad de generación en algunos benchmarks.&lt;/p>
&lt;p>&lt;strong>Grouped Query Attention (GQA).&lt;/strong> El término medio que ha ganado. Las cabezas se agrupan: en Llama 3 8B, 32 cabezas de query comparten K, V en grupos de 4 → 8 grupos de KV. Reduce el cache 4× respecto a MHA con casi idéntica calidad. Es el estándar de facto desde 2024.&lt;/p>
&lt;p>&lt;strong>Multi-Head Latent Attention (MLA).&lt;/strong> La innovación de DeepSeek-V2/V3: en vez de almacenar K, V por cabeza, comprime el estado en un vector latente más pequeño y proyecta a K, V en el momento. El cache puede llegar a 70 bytes/token, dos órdenes de magnitud menos que GQA. Es la razón principal por la que DeepSeek-V3 (671 B parámetros, 37 B activos) es servible en infraestructura abordable.&lt;/p>
&lt;div class="diagram" style="max-width: 640px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 640 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Reducción del KV cache por técnica">
&lt;style>
.bar { stroke: #333; stroke-width: 1; }
.b-mha { fill: #e76f51; }
.b-gqa { fill: #f4a261; }
.b-mqa { fill: #e9c46a; }
.b-mla { fill: #2a9d8f; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm{ font: 11px sans-serif; fill: #444; }
&lt;/style>
&lt;text x="320" y="20" text-anchor="middle" class="lbl">KB de cache por token (Llama 3 8B equivalente, BF16)&lt;/text>
&lt;rect x="200" y="40" width="380" height="22" class="bar b-mha"/>
&lt;text x="170" y="56" text-anchor="end" class="lbl-sm">MHA (32 KV heads)&lt;/text>
&lt;text x="595" y="56" class="lbl-sm">512 KB&lt;/text>
&lt;rect x="200" y="76" width="95" height="22" class="bar b-gqa"/>
&lt;text x="170" y="92" text-anchor="end" class="lbl-sm">GQA (8 KV heads)&lt;/text>
&lt;text x="310" y="92" class="lbl-sm">128 KB&lt;/text>
&lt;rect x="200" y="112" width="12" height="22" class="bar b-mqa"/>
&lt;text x="170" y="128" text-anchor="end" class="lbl-sm">MQA (1 KV head)&lt;/text>
&lt;text x="225" y="128" class="lbl-sm">16 KB&lt;/text>
&lt;rect x="200" y="148" width="3" height="22" class="bar b-mla"/>
&lt;text x="170" y="164" text-anchor="end" class="lbl-sm">MLA (DeepSeek-V3)&lt;/text>
&lt;text x="215" y="164" class="lbl-sm">~0.5 KB (real V3)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;blockquote>
&lt;p>&lt;strong>Nota:&lt;/strong> la barra de MLA es ilustrativa con valores típicos publicados por DeepSeek; la implementación exacta depende del tamaño latente. Lo importante es el orden de magnitud.&lt;/p>
&lt;/blockquote>
&lt;p>A esto se suma una cuarta técnica ortogonal: &lt;strong>cuantizar el cache&lt;/strong> a FP8, INT8 o incluso INT4. vLLM y TensorRT-LLM ya lo soportan en producción. Pasar de BF16 (2 bytes) a FP8 (1 byte) &lt;strong>divide el cache por dos&lt;/strong> con coste pequeño en calidad. Pasar a INT4, por cuatro, con coste algo mayor.&lt;/p>
&lt;h2 id="el-siguiente-dragón-la-fragmentación">El siguiente dragón: la fragmentación&lt;/h2>
&lt;p>Hasta aquí hemos hablado del cache como si fuera un bloque contiguo. En la práctica, un servidor de inferencia atiende &lt;strong>decenas de sesiones simultáneas&lt;/strong>, cada una con su propio cache que crece a un ritmo distinto. La asignación naïve —reservar el máximo posible por sesión— &lt;strong>desperdicia entre el 60 % y el 80 % de la VRAM&lt;/strong> según el paper original de PagedAttention.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 240" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Fragmentación del KV cache: naïve vs PagedAttention">
&lt;style>
.used { fill: #2a9d8f; stroke: #1a6e63; stroke-width: 1; }
.free { fill: #f0e7d8; stroke: #aaa; stroke-width: 1; }
.blk { stroke: #555; stroke-width: 0.5; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
&lt;/style>
&lt;text x="180" y="22" text-anchor="middle" class="lbl">Asignación naïve (contigua)&lt;/text>
&lt;text x="540" y="22" text-anchor="middle" class="lbl">PagedAttention (bloques)&lt;/text>
&lt;!-- Naive: 4 sesiones reservan el máximo, usan poco -->
&lt;p>&lt;text x="30" y="60" class="lbl-sm">sesión A&lt;/text>
&lt;rect x="90" y="48" width="50" height="18" class="used"/>
&lt;rect x="140" y="48" width="180" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="30" y="92" class="lbl-sm">sesión B&lt;/text>
&lt;rect x="90" y="80" width="25" height="18" class="used"/>
&lt;rect x="115" y="80" width="205" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="30" y="124" class="lbl-sm">sesión C&lt;/text>
&lt;rect x="90" y="112" width="100" height="18" class="used"/>
&lt;rect x="190" y="112" width="130" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="30" y="156" class="lbl-sm">sesión D&lt;/text>
&lt;rect x="90" y="144" width="35" height="18" class="used"/>
&lt;rect x="125" y="144" width="195" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="180" y="190" text-anchor="middle" class="lbl-sm">→ ~70 % de VRAM reservada y vacía&lt;/text>&lt;/p>
&lt;!-- PagedAttention: bloques pequeños, ocupación densa -->
&lt;g transform="translate(400,40)">
&lt;!-- 8 bloques x 5 filas -->
&lt;g>
&lt;!-- fila 1 -->
&lt;rect x="0" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="30" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="60" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="90" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="120" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="150" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="180" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="210" y="0" width="30" height="20" class="used blk"/>
&lt;pre>&lt;code> &amp;lt;rect x=&amp;quot;0&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;30&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;60&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;90&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;120&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;150&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;180&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;210&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;0&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;30&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;60&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;90&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;120&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;150&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;180&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;210&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;/g&amp;gt;
&lt;/code>&lt;/pre>
&lt;/g>
&lt;text x="540" y="190" text-anchor="middle" class="lbl-sm">→ &amp;lt; 4 % desperdicio (paper vLLM)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>&lt;strong>PagedAttention&lt;/strong> —la idea de Kwon et al. (2023) que dio origen a vLLM— resuelve esto pidiendo prestada una técnica de los sistemas operativos: dividir la VRAM en &lt;strong>bloques&lt;/strong> pequeños (típicamente de 16 tokens) y mantener una &lt;strong>tabla de páginas&lt;/strong> lógicas → físicas por sesión. Una sesión ya no reserva un bloque contiguo enorme: crece un bloque cada vez, y los bloques pueden estar dispersos por la VRAM. Resultado: ocupación efectiva del 90 % en lugar del 30 %, y por tanto &lt;strong>2–4× más throughput agregado&lt;/strong> en el mismo hardware.&lt;/p>
&lt;p>PagedAttention merece artículo propio. Lo dejo apuntado para el siguiente.&lt;/p>
&lt;h2 id="aplicado-a-la-infraestructura-fibercli">Aplicado a la infraestructura Fibercli&lt;/h2>
&lt;p>Bajemos a casos concretos.&lt;/p>
&lt;h3 id="caso-1--rtx-4090-24-gb-ada-lovelace">Caso 1 — RTX 4090 (24 GB, Ada Lovelace)&lt;/h3>
&lt;p>Configuración típica con Qwen3-8B BF16:&lt;/p>
&lt;pre tabindex="0">&lt;code>Modelo BF16: ~16 GB
Activations + overhead: ~2 GB
VRAM disponible para KV cache: ~6 GB (con margen)
&lt;/code>&lt;/pre>&lt;p>Con 144 KB/token (Qwen3-8B GQA), eso son &lt;strong>~43 K tokens totales de cache&lt;/strong> distribuidos entre todas las sesiones simultáneas. En la práctica:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Concurrencia&lt;/th>
&lt;th style="text-align:right">Contexto máximo por sesión&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">32 768&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">4&lt;/td>
&lt;td style="text-align:right">8 192&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">4 096&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">2 048&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Si necesitas anunciar &amp;ldquo;soportamos 32 K de contexto&amp;rdquo; con concurrencia 4+, hay que &lt;strong>cuantizar el cache&lt;/strong> (FP8 baja a 72 KB/token, duplica capacidad) o &lt;strong>subir el modelo de gama&lt;/strong> (un 4B con GQA y cache cuantizado holgaría).&lt;/p>
&lt;h3 id="caso-2--cluster-5h100-sxm-400-gb-total-nvlink">Caso 2 — Cluster 5×H100 SXM (400 GB total, NVLink)&lt;/h3>
&lt;p>Con tensor parallel = 5 y Llama 3 70B BF16:&lt;/p>
&lt;pre tabindex="0">&lt;code>Modelo BF16: ~140 GB (28 GB/GPU)
Overhead vLLM por GPU: ~2 GB
VRAM libre para KV por GPU: ~50 GB → ~250 GB agregados
&lt;/code>&lt;/pre>&lt;p>Con 320 KB/token (Llama 3 70B GQA), eso son &lt;strong>~800 K tokens totales de cache&lt;/strong>. Mucho margen para servir contextos largos con concurrencia alta:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Concurrencia&lt;/th>
&lt;th style="text-align:right">Contexto máximo por sesión&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">4&lt;/td>
&lt;td style="text-align:right">200 000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">50 000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">64&lt;/td>
&lt;td style="text-align:right">12 500&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Para DeepSeek-V3 671 B con MLA: la economía cambia radicalmente porque el cache es ~100× más fino. Lo que limita ya no es el cache sino la VRAM del propio modelo (cuantizado FP8 son ~671 GB → no cabe en 5×H100, hace falta cluster mayor o FP4).&lt;/p>
&lt;h3 id="implicaciones-operativas">Implicaciones operativas&lt;/h3>
&lt;p>Tres observaciones que repetimos en cada consultoría:&lt;/p>
&lt;p>Primero, &lt;strong>el contexto máximo anunciado por un modelo no es el que puedes servir en tu hardware&lt;/strong>. Llama 3 8B &amp;ldquo;soporta&amp;rdquo; 128 K, pero en una 4090 con 4 sesiones simultáneas tu contexto efectivo son ~8 K. Es trivial comprobarlo antes de prometérselo al cliente.&lt;/p>
&lt;p>Segundo, &lt;strong>cuantizar el KV cache es de las optimizaciones con mejor relación coste/beneficio en el contexto ENS&lt;/strong>. No toca los pesos, no afecta a la reproducibilidad de auditoría, y duplica capacidad. vLLM lo soporta vía &lt;code>--kv-cache-dtype fp8&lt;/code>.&lt;/p>
&lt;p>Tercero, &lt;strong>si los SLA dictan contextos largos con muchos usuarios concurrentes, GQA es necesario pero no suficiente&lt;/strong>. A medio plazo, hay que mirar modelos con MLA o variantes de attention con compresión.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>PagedAttention&lt;/strong> y su implementación en vLLM: bloques, tabla de páginas, evicción.&lt;/li>
&lt;li>&lt;strong>Prefix caching&lt;/strong>: cuando varias peticiones comparten el system prompt, no hace falta recomputar las K, V de la parte común.&lt;/li>
&lt;li>&lt;strong>Speculative decoding&lt;/strong> y su interacción con el cache.&lt;/li>
&lt;li>&lt;strong>Cache offloading&lt;/strong>: mover bloques fríos a RAM o a NVMe, técnica clave para contextos &amp;gt; 1 M.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">El pipeline LLMOps de seis etapas&lt;/a> — el mapa maestro del sistema en producción del que la etapa Deploy es una caja entre seis. Este post entra en una de las decisiones críticas dentro de Deploy.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention por dentro: bloques, tabla de páginas, evicción y el estado del arte del KV cache en 2026&lt;/a> — deep-dive teórico al nivel del bloque y panorama de optimizaciones derivadas (vAttention, EvicPress, RadixAttention, speculative decoding). Continúa este post desde la teoría académica.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">Fine-tuning continuo en producción: del tráfico real al adapter desplegado&lt;/a> — cómo se cierra el ciclo entre inferencia y entrenamiento incremental sobre el mismo stack (vLLM + Postgres), con presupuestos de VRAM que incluyen explícitamente el KV cache durante eval.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving: prefill y decode en pods especializados&lt;/a> — el KV cache deja de ser un buffer privado de la GPU para convertirse en el artefacto que se transfiere entre pods. Aquí la fórmula del tamaño del cache determina la economía de la transferencia.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cluster-h100-plataforma-multi-tenant/">El cluster GPU como plataforma multi-tenant&lt;/a> — cómo se convierte el cluster en un servicio con tenants, gateway, quotas y aislamiento. Es donde el KV cache deja de ser sólo un recurso de rendimiento y pasa a ser un asunto de plataforma.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes: la pieza de inferencia LLM que sí escala&lt;/a> — el motor que materializa todo lo que aquí se discute, desplegado en K8s con tensor parallel y autoscaling.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Vaswani et al., &lt;em>Attention Is All You Need&lt;/em> (NeurIPS 2017) — paper fundacional del transformer.&lt;/li>
&lt;li>Ainslie et al., &lt;em>GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints&lt;/em> (EMNLP 2023).&lt;/li>
&lt;li>Kwon et al., &lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em> (SOSP 2023) — paper original de vLLM.&lt;/li>
&lt;li>DeepSeek-AI, &lt;em>DeepSeek-V2 Technical Report&lt;/em> (2024) — introducción de Multi-Head Latent Attention.&lt;/li>
&lt;li>Documentación oficial de vLLM: &lt;a href="https://docs.vllm.ai/">https://docs.vllm.ai/&lt;/a>.&lt;/li>
&lt;li>Llama 3 model card (Meta): especificaciones GQA, n_layers, n_kv_heads.&lt;/li>
&lt;/ul></description></item></channel></rss>