<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Vllm on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/vllm/</link><description>Recent content in Vllm on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Wed, 20 May 2026 09:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/vllm/index.xml" rel="self" type="application/rss+xml"/><item><title>eBPF en inferencia local y detección estadística de drift: el cierre del ciclo de observabilidad LLM en 2026</title><link>https://blog.lo0.es/posts/ebpf-on-device-inference-drift/</link><pubDate>Wed, 20 May 2026 09:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/ebpf-on-device-inference-drift/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Tracing, evals, guardrails, MCP observability: las capas que ya hemos cubierto ven &lt;strong>qué está pasando ahora mismo&lt;/strong>. Lo que no ven es lo que cambia &lt;strong>silenciosamente&lt;/strong>: el agente que la semana pasada respondía bien y esta semana, sin que nadie haya tocado nada, responde algo peor. Lo que no ven tampoco es la &lt;strong>mecánica fina&lt;/strong> de la inferencia local: por qué un llama.cpp en un edge device tarda 200 ms cuando debería tardar 100, qué función del runtime concreta es el cuello. Este post cierra las dos series de la semana con las dos capas que faltaban: &lt;strong>eBPF aplicado a inferencia local&lt;/strong> (uprobes en &lt;code>llama.cpp&lt;/code>, &lt;code>vLLM&lt;/code>, &lt;code>libcudart.so&lt;/code>, hardware perf counters integrados, con &amp;lt;4% de overhead — formalizado en el paper &lt;a href="https://arxiv.org/abs/2601.20755">ProfInfer 2026&lt;/a> que es a inferencia local lo que Hubble es a la red) y &lt;strong>análisis estadístico de flows de agentes&lt;/strong> para detectar drift antes de que tu usuario lo note (KS, PSI, MMD, embedding-space clustering, con &lt;a href="https://www.evidentlyai.com/">Evidently AI&lt;/a>, &lt;a href="https://www.nannyml.com/">NannyML&lt;/a> y &lt;a href="https://whylabs.ai/">WhyLabs&lt;/a> como herramientas dominantes). Las tres tipologías de drift LLM en 2026 — &lt;strong>prompt drift, model drift, eval-score drift&lt;/strong> — exigen tests distintos. El stack completo —tracing, evals, guardrails, MCP observability, eBPF observability, drift detection— forma el bucle que cualquier sistema agentic serio necesita para operar con SLA real, no con esperanza.&lt;/p>
&lt;blockquote>
&lt;p>Este post &lt;strong>cierra dos series&lt;/strong>: la serie post-tracing (&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/guardrails-safety-llm/">Guardrails&lt;/a>, &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&lt;/a>) y la serie eBPF (&lt;a href="https://blog.lo0.es/posts/ebpf-cilium-tcp-ip-bypass/">eBPF de cero a Cilium&lt;/a>, &lt;a href="https://blog.lo0.es/posts/tetragon-runtime-security/">Tetragon&lt;/a>, &lt;a href="https://blog.lo0.es/posts/hubble-observabilidad-ebpf/">Hubble&lt;/a>, &lt;a href="https://blog.lo0.es/posts/agentsight-tracing-llm/">AgentSight&lt;/a>). Junta los dos hilos: eBPF aplicado al motor de inferencia local + análisis estadístico de los flows que todas las capas producen.&lt;/p>
&lt;/blockquote>
&lt;h2 id="la-analogía-el-cardiograma-del-agente">La analogía: el cardiograma del agente&lt;/h2>
&lt;p>Un médico que sólo mira síntomas agudos —el paciente llega con fiebre alta, hay que actuar— está haciendo medicina &lt;strong>reactiva&lt;/strong>. Para hacer medicina &lt;strong>preventiva&lt;/strong>, necesita series temporales: la tensión arterial cada año, el colesterol cada seis meses, el ECG cuando hay sospecha. No es información de &amp;ldquo;ahora mismo&amp;rdquo;, es información sobre &lt;strong>cómo evoluciona&lt;/strong> algo que debería estar estable. Cuando una serie temporal se desvía de su línea base, hay que investigar antes de que sea fiebre alta.&lt;/p>
&lt;p>Las capas de observabilidad LLM que llevamos vistas son &lt;strong>medicina reactiva&lt;/strong>: tracing te dice qué pasó en una conversación concreta; evals te dice si esa conversación fue buena; guardrails te dice si había una amenaza específica; MCP observability te dice qué tools se invocaron y cómo. Todas miran &lt;strong>eventos&lt;/strong>, no &lt;strong>tendencias&lt;/strong>.&lt;/p>
&lt;p>Drift detection es la &lt;strong>medicina preventiva&lt;/strong>. Mira series temporales —de embeddings de prompts, de scores de evaluación, de distribuciones de tokens generados— y dispara alertas cuando algo se aleja de su normalidad. No te dice &amp;ldquo;esta respuesta es mala&amp;rdquo;; te dice &amp;ldquo;la distribución de prompts de las últimas 6 horas no se parece a la distribución del último mes&amp;rdquo;. Ahí decides si investigar.&lt;/p>
&lt;p>Y la otra mitad del post —eBPF en inferencia local— es el equivalente al &lt;strong>resonador magnético&lt;/strong>: cuando ya sabes que hay un problema, te permite ver el interior del modelo a una resolución que ningún wrapper externo da. Ver qué función concreta del runtime tarda, qué kernel CUDA es el cuello, cómo se mueven los tokens en los buffers internos antes de salir al cliente.&lt;/p>
&lt;p>Las dos juntas cierran el ciclo: las series temporales detectan que algo va mal, el resonador localiza dónde.&lt;/p>
&lt;h2 id="parte-1--ebpf-aplicado-a-inferencia-local">Parte 1 — eBPF aplicado a inferencia local&lt;/h2>
&lt;h3 id="por-qué-la-inferencia-local-cambia-el-juego">Por qué la inferencia local cambia el juego&lt;/h3>
&lt;p>Cuando el LLM corre &lt;strong>localmente&lt;/strong> —vLLM en un nodo Kubernetes, llama.cpp en un edge device, Ollama en una workstation, MLX en macOS— y no detrás de una API externa, la observabilidad cambia de forma:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Controlas el binario&lt;/strong>: puedes adjuntar hooks que de otra manera serían imposibles.&lt;/li>
&lt;li>&lt;strong>Los buffers internos existen en RAM accesible&lt;/strong>: el stream de tokens-output, los logits, las cachés KV, las estructuras de scheduler están &lt;strong>ahí&lt;/strong>, en direcciones que un uprobe puede leer.&lt;/li>
&lt;li>&lt;strong>No hay cable que esnifar&lt;/strong>: la analogía de AgentSight con SSL hooks no aplica porque no hay TLS — el modelo te responde con un retorno de función en proceso, no con una respuesta HTTPS.&lt;/li>
&lt;li>&lt;strong>La distancia entre kernel y modelo es mínima&lt;/strong>: los kernels CUDA que ejecutan la atención están a una syscall de profundidad; eBPF puede observar ambos lados de esa frontera con el mismo trazador.&lt;/li>
&lt;/ul>
&lt;p>Esto abre una clase de observabilidad que con LLM-as-a-service (API de Anthropic, OpenAI, Vertex) es estructuralmente imposible. Para apps que sirven inferencia on-premise o on-edge — un cluster de inference, un dispositivo móvil, un servidor RTX 4090 en el rack — es una capa nueva.&lt;/p>
&lt;h3 id="profinfer-el-paper-que-formaliza-el-patrón">ProfInfer: el paper que formaliza el patrón&lt;/h3>
&lt;p>&lt;a href="https://arxiv.org/abs/2601.20755">ProfInfer (arxiv 2601.20755, 2026)&lt;/a> es la pieza académica de referencia que sistematiza lo que el ecosistema venía haciendo de manera ad-hoc. El subtítulo del paper lo dice todo: &lt;em>An eBPF-based Fine-Grained LLM Inference Profiler&lt;/em>.&lt;/p>
&lt;p>Lo que propone:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Atachar uprobes dinámicamente&lt;/strong> a funciones runtime de motores como &lt;code>llama.cpp&lt;/code> (y por extensión vLLM, Ollama). No recompila, no modifica el código fuente. Es como &lt;code>bpftrace&lt;/code> para inferencia LLM.&lt;/li>
&lt;li>&lt;strong>Combinar runtime events con hardware performance counters&lt;/strong>. Una uprobe te dice cuándo se ejecuta &lt;code>llama_decode&lt;/code>; un hardware counter te dice cuántas instrucciones flotantes se ejecutaron mientras estaba dentro. La correlación entre ambas es lo que da la &lt;strong>resolución fina&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>&amp;lt;4% overhead medido&lt;/strong> en cargas reales. Es coste de producción.&lt;/li>
&lt;li>&lt;strong>Visualizaciones&lt;/strong> en tres vistas: operadores (qué operaciones tensoriales se ejecutaron), grafos (cómo se relacionan), timelines (cuándo).&lt;/li>
&lt;/ul>
&lt;p>El paper se enfoca especialmente en &lt;strong>modelos en plataformas móviles&lt;/strong> (Llama servido en un Pixel o iPhone), donde la observabilidad clásica con Prometheus y métricas exportadas casi no existe. Pero el patrón aplica a cualquier inferencia local.&lt;/p>
&lt;h3 id="dónde-hookear-el-mapa-por-motor">Dónde hookear: el mapa por motor&lt;/h3>
&lt;p>Vamos al detalle de los hooks. Las funciones objetivo varían por motor:&lt;/p>
&lt;h4 id="llamacpp">llama.cpp&lt;/h4>
&lt;p>&lt;code>llama.cpp&lt;/code> es C++ puro, símbolos visibles en el binario. Los hooks típicos:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>llama_decode&lt;/code>&lt;/strong>: la función que ejecuta una pasada de inferencia (procesa el batch actual). Spans para latencia por iteration, tokens procesados.&lt;/li>
&lt;li>&lt;strong>&lt;code>llama_token_to_piece&lt;/code>&lt;/strong>: convierte un token ID a texto. Hook aquí captura el &lt;strong>stream de tokens generados&lt;/strong> antes de devolver al caller. Es el equivalente local a las uprobes de SSL: ves la salida del modelo sin que llegue siquiera al consumidor.&lt;/li>
&lt;li>&lt;strong>&lt;code>llama_get_logits&lt;/code>&lt;/strong>: lee los logits del último decode. Si quieres registrar las probabilidades del modelo (no solo el token elegido), aquí.&lt;/li>
&lt;li>&lt;strong>&lt;code>ggml_compute_forward_*&lt;/code>&lt;/strong> (varias funciones): los kernels de operaciones (matmul, attention, layernorm). Hooks para profiling por operación.&lt;/li>
&lt;li>&lt;strong>&lt;code>ggml_backend_*&lt;/code>&lt;/strong>: las funciones del backend (CPU, Metal, CUDA, ROCm). Hooks aquí desglosan el coste por dispositivo.&lt;/li>
&lt;/ul>
&lt;p>Ejemplo con &lt;code>bpftrace&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Latencia y count de llama_decode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">bpftrace -e &lt;span class="s1">&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">uprobe:/path/to/llama-server:llama_decode {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> @start[tid] = nsecs;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">uretprobe:/path/to/llama-server:llama_decode /@start[tid]/ {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> @decode_lat = hist((nsecs - @start[tid]) / 1000);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> delete(@start[tid]);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">}
&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>Salida: histograma de latencias de decode en microsegundos. Cero modificación al binario.&lt;/p>
&lt;h4 id="vllm">vLLM&lt;/h4>
&lt;p>vLLM es Python en su mayor parte. Los símbolos C/CUDA están en sus extensiones nativas (&lt;code>vllm._C&lt;/code>, &lt;code>vllm._moe_C&lt;/code>). Los hooks típicos:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>uprobes en &lt;code>vllm._C.*&lt;/code>&lt;/strong> para operadores custom (paged attention kernel, sampling kernel).&lt;/li>
&lt;li>&lt;strong>uprobes en &lt;code>libcudart.so&lt;/code> y &lt;code>libcuda.so&lt;/code>&lt;/strong> para capturar &lt;code>cudaMalloc&lt;/code>, &lt;code>cudaLaunchKernel&lt;/code>, &lt;code>cudaMemcpy&lt;/code>. Sirve para mapear costes de transferencias host↔device y de lanzamientos de kernels.&lt;/li>
&lt;li>&lt;strong>Tracepoints Python con &lt;code>bpftrace&lt;/code> sobre &lt;code>usdt&lt;/code> puntos&lt;/strong>: vLLM no expone tracepoints estáticos nativos, pero se pueden colocar con USDT (&lt;code>dtrace&lt;/code> style) en lugares estratégicos del scheduler.&lt;/li>
&lt;/ul>
&lt;p>vLLM expone además métricas Prometheus nativas (&lt;code>vllm:num_requests_running&lt;/code>, &lt;code>vllm:gpu_cache_usage_perc&lt;/code>, etc.). El valor añadido del enfoque eBPF es &lt;strong>bajar de las métricas del scheduler a las funciones individuales&lt;/strong>: cuando una request es lenta, ver si fue prefill, decode, scheduler overhead, transferencia o sincronización.&lt;/p>
&lt;h4 id="cuda-en-general">CUDA en general&lt;/h4>
&lt;p>Independiente del motor, las uprobes en &lt;code>libcudart.so&lt;/code> capturan &lt;strong>toda la actividad CUDA del proceso&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>cudaMalloc(size)&lt;/code> → tracking de allocations en device memory.&lt;/li>
&lt;li>&lt;code>cudaLaunchKernel(func, ...)&lt;/code> → spans por cada lanzamiento de kernel.&lt;/li>
&lt;li>&lt;code>cudaMemcpyAsync(dst, src, size, kind)&lt;/code> → transferencias host↔device.&lt;/li>
&lt;li>&lt;code>cudaStreamSynchronize(stream)&lt;/code> → puntos de sincronización (donde el host espera al device).&lt;/li>
&lt;/ul>
&lt;p>Esto te da una &lt;strong>timeline completa de actividad CUDA&lt;/strong> sin necesidad de NVIDIA Nsight Systems (que es excelente pero pesado y orientado a desarrollo, no a producción continua).&lt;/p>
&lt;h3 id="hardware-counters-la-otra-mitad">Hardware counters: la otra mitad&lt;/h3>
&lt;p>eBPF puede leer &lt;strong>performance counters&lt;/strong> del PMU (Performance Monitoring Unit) del CPU/GPU. Esto incluye instrucciones ejecutadas, cache misses, branch mispredictions y, en GPUs con soporte, FLOPS, ocupación de SM, ancho de banda HBM.&lt;/p>
&lt;p>Combinar:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>uprobe&lt;/strong>: &amp;ldquo;se ejecutó &lt;code>llama_decode&lt;/code> desde T1 a T2 con tokens=4&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>perf counter&lt;/strong>: &amp;ldquo;durante esa ventana, cache misses L2 = 15000, instrucciones = 2.3 millones&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;p>Permite responder: ¿por qué tarda? ¿es memory-bound (muchos cache misses), compute-bound (todas las instrucciones en FPU), bandwidth-bound (mucho movimiento de datos)? Estado del arte para profiling profesional.&lt;/p>
&lt;h3 id="comparativa-con-agentsight">Comparativa con AgentSight&lt;/h3>
&lt;p>Hay dos productos eBPF para LLMs hoy con foco distinto:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://blog.lo0.es/posts/agentsight-tracing-llm/">AgentSight&lt;/a>&lt;/strong> (cubierto en la serie eBPF): observa &lt;strong>agentes que llaman a APIs externas&lt;/strong>. Hookea SSL para ver el plaintext de las llamadas HTTPS al LLM remoto, más stdio para servers MCP locales. &lt;strong>Visión cliente&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>ProfInfer / patrón de eBPF en inferencia local&lt;/strong>: observa &lt;strong>el motor que ejecuta el modelo localmente&lt;/strong>. Hookea las funciones internas del motor (llama.cpp, vLLM) y la capa CUDA. &lt;strong>Visión servidor (interno)&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>Son complementarios. Si tu agente usa Claude API + tu propio vLLM local con Llama 3 para tareas específicas, AgentSight ve lo primero, eBPF/ProfInfer ve lo segundo. Si todo es local, dominio claramente del segundo. Si todo es API externa, del primero.&lt;/p>
&lt;h3 id="casos-de-uso-de-ebpf-en-inferencia-local">Casos de uso de eBPF en inferencia local&lt;/h3>
&lt;p>Tres casos donde es la herramienta correcta:&lt;/p>
&lt;p>&lt;strong>Profiling fino para optimización&lt;/strong>: tu vLLM tarda 50ms más por token de lo esperado. Con eBPF + hardware counters localizas en qué kernel concreto. Antes esto requería Nsight Systems en una sesión de desarrollo; ahora es continuo en producción.&lt;/p>
&lt;p>&lt;strong>Token-level observability sin modificar el motor&lt;/strong>: capturar el stream de tokens generados antes de devolverlos al cliente. Útil para auditoría, para drift detection sobre los outputs, para tracing local sin pasar por instrumentación del wrapping.&lt;/p>
&lt;p>&lt;strong>Detección de degradación específica&lt;/strong>: una versión nueva de vLLM mete una regresión sutil en el paged attention. Con baselines de perf counters, detectas el cambio incluso si la métrica externa (tokens/sec) parece igual.&lt;/p>
&lt;h2 id="parte-2--análisis-estadístico-de-flows-detectar-drift">Parte 2 — Análisis estadístico de flows: detectar drift&lt;/h2>
&lt;p>Pasamos al otro lado del problema: las series temporales.&lt;/p>
&lt;h3 id="por-qué-tracing-evals-y-guardrails-no-detectan-drift">Por qué tracing, evals y guardrails no detectan drift&lt;/h3>
&lt;p>Las capas que ya hemos visto operan sobre &lt;strong>eventos individuales&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>Tracing: una traza de una conversación.&lt;/li>
&lt;li>Evals: un score de una respuesta.&lt;/li>
&lt;li>Guardrails: un veredicto sobre un prompt o respuesta.&lt;/li>
&lt;li>MCP observability: spans de una invocación de tool.&lt;/li>
&lt;/ul>
&lt;p>Cada uno responde a una pregunta puntual (&amp;quot;¿está bien esto?&amp;quot;). Ninguno responde a la pregunta de &lt;strong>evolución&lt;/strong> (&amp;quot;¿está cambiando algo a lo largo del tiempo?&amp;quot;).&lt;/p>
&lt;p>El problema operacional: &lt;strong>drift es invisible en eventos individuales&lt;/strong>. Si el score medio de eval baja de 0.92 a 0.85 a lo largo de tres semanas, ninguna evaluación individual marcará alarma —todas siguen siendo &amp;ldquo;razonables&amp;rdquo;—. Lo que cambia es la &lt;strong>distribución&lt;/strong>. Y eso solo se ve mirando muchas evaluaciones agregadas en el tiempo.&lt;/p>
&lt;h3 id="las-tres-tipologías-de-drift-llm-en-2026">Las tres tipologías de drift LLM en 2026&lt;/h3>
&lt;p>&lt;a href="https://futureagi.com/blog/what-is-llm-drift-2026">FutureAGI&lt;/a> las consolida así, y la industria está convergiendo en este vocabulario:&lt;/p>
&lt;p>&lt;strong>1. Prompt drift&lt;/strong>: alguien actualiza el prompt sistema y los efectos secundarios rompen casos que antes funcionaban. Casi siempre intencional pero con consecuencias no anticipadas. &lt;strong>Detección&lt;/strong>: comparar distribuciones de respuestas antes y después del cambio, monitorizar eval scores por versión de prompt (linked en Langfuse, ver post de &lt;a href="https://blog.lo0.es/posts/agentsight-tracing-llm/">AgentSight&lt;/a> donde cubrimos prompt management).&lt;/p>
&lt;p>&lt;strong>2. Model drift&lt;/strong>: el proveedor (OpenAI, Anthropic) actualiza el modelo sin avisar. El mismo prompt produce respuestas con tonalidad ligeramente distinta, calidad similar pero diferente, o degradación en algún subset. &lt;strong>Detección&lt;/strong>: comparar embeddings de respuestas de hoy con baseline; monitorizar rubric scores; alertar si la varianza intra-modelo crece.&lt;/p>
&lt;p>&lt;strong>3. Eval-score drift&lt;/strong>: la rolling mean de tus métricas de eval (faithfulness, answer relevancy, custom rubrics) tiende a la baja. Causa raíz puede ser cualquiera de las anteriores o un cambio en el mix de usuarios. &lt;strong>Detección&lt;/strong>: alertas sobre tendencias de las series de evals.&lt;/p>
&lt;p>A estas tres se suma una cuarta más sutil:&lt;/p>
&lt;p>&lt;strong>4. Persona drift / user mix shift&lt;/strong>: la población de usuarios que usa el sistema cambia. No es que el modelo o el prompt empeoraron; es que los nuevos usuarios hacen preguntas distintas y el sistema, aunque sigue siendo igual de bueno en lo que era bueno, falla en lo nuevo. &lt;strong>Detección&lt;/strong>: embedding clustering de prompts, monitorizar aparición de clusters nuevos o crecimiento de uno minoritario.&lt;/p>
&lt;h3 id="el-concepto-técnico-clave-embedding-space-shift">El concepto técnico clave: embedding-space shift&lt;/h3>
&lt;p>&lt;a href="https://stackpulsar.com/blog/llm-model-drift-detection/">Stack Pulsar&lt;/a> lo dice claro: en LLMs, &lt;strong>el drift se mide mejor en el espacio de embeddings&lt;/strong>. Las distancias clásicas en espacio de tokens no capturan semántica fina; en embedding space sí.&lt;/p>
&lt;p>El pipeline canónico:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Establecer baseline&lt;/strong>: durante un periodo estable (digamos las primeras dos semanas tras un release), captura una muestra grande de embeddings de prompts y respuestas.&lt;/li>
&lt;li>&lt;strong>Monitorización continua&lt;/strong>: cada hora o cada día, captura una nueva muestra del tráfico de producción.&lt;/li>
&lt;li>&lt;strong>Comparar distribuciones&lt;/strong>: aplica un test estadístico que compare la distribución actual con la baseline en el espacio de embeddings.&lt;/li>
&lt;li>&lt;strong>Alertar&lt;/strong>: si la divergencia supera un umbral, dispara una alerta y un workflow de investigación.&lt;/li>
&lt;/ol>
&lt;p>Como bonus, &lt;strong>monitorizar clusters&lt;/strong>: si tu baseline tiene 5 clusters de prompts (preguntas técnicas, soporte general, ventas, etc.) y de pronto aparece un sexto cluster que no estaba, lo más probable es que un nuevo segmento de usuarios haya llegado.&lt;/p>
&lt;h3 id="tests-estadísticos-ks-psi-mmd">Tests estadísticos: KS, PSI, MMD&lt;/h3>
&lt;p>Tres tests que cualquier sistema de drift usa, cada uno con su lugar:&lt;/p>
&lt;p>&lt;strong>Kolmogorov-Smirnov (KS)&lt;/strong>: no-paramétrico. Calcula la máxima distancia entre dos CDFs empíricas. Devuelve un statistic y un p-value. &lt;strong>Ventaja&lt;/strong>: muy sensible a cambios sutiles, especialmente en colas. &lt;strong>Desventaja&lt;/strong>: con datasets grandes, &amp;ldquo;demasiado sensible&amp;rdquo; — dispara alarmas por cambios reales pero clínicamente irrelevantes.&lt;/p>
&lt;p>&lt;strong>Population Stability Index (PSI)&lt;/strong>: bineas la distribución de referencia y la actual, luego sumas &lt;code>(p_actual - p_ref) × log(p_actual / p_ref)&lt;/code> sobre los bines. Interpretación canónica: PSI &amp;lt; 0.1 estable, 0.1-0.25 drift suave, &amp;gt; 0.25 drift significativo. &lt;strong>Ventaja&lt;/strong>: interpretable, threshold-based, tradición de uso en credit scoring (Capital One, Goldman Sachs). &lt;strong>Desventaja&lt;/strong>: menos sensible que KS — pierde drift en colas.&lt;/p>
&lt;p>&lt;strong>Maximum Mean Discrepancy (MMD)&lt;/strong>: mide la divergencia entre dos distribuciones embebiendo cada una en un espacio de Hilbert vía kernel. Sirve para &lt;strong>distribuciones multivariadas complejas&lt;/strong> (embeddings de alta dimensión). &lt;strong>Ventaja&lt;/strong>: la única que escala razonablemente a embeddings de 768/1024/4096 dimensiones. &lt;strong>Desventaja&lt;/strong>: más compleja de interpretar.&lt;/p>
&lt;p>La práctica recomendada en 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>PSI para features simples&lt;/strong> (longitud de prompt, tokens, número de tools invocadas).&lt;/li>
&lt;li>&lt;strong>KS para features continuos&lt;/strong> donde quieras alta sensibilidad.&lt;/li>
&lt;li>&lt;strong>MMD para embeddings&lt;/strong> (espacios de alta dimensión).&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://www.evidentlyai.com/blog/data-drift-detection-large-datasets">Análisis de Evidently&lt;/a> en datasets reales mostró que &lt;strong>KS detecta drift 6+ horas antes que PSI&lt;/strong> en algunos incidentes. La consecuencia operativa: usa KS para early warning, PSI para confirmación con threshold interpretable.&lt;/p>
&lt;h3 id="herramientas-2026">Herramientas 2026&lt;/h3>
&lt;p>Tres productos dominan el campo:&lt;/p>
&lt;h4 id="evidently-ai">Evidently AI&lt;/h4>
&lt;p>&lt;a href="https://github.com/evidentlyai/evidently">Evidently&lt;/a> es open-source (Apache 2.0), Python-first. Su valor:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Drift reports HTML&lt;/strong>: generas un report comparando dos datasets (referencia vs actual) y obtienes un archivo HTML con todos los tests estadísticos, visualizaciones, conclusiones. Sin servidor, sin infra; un fichero compartible.&lt;/li>
&lt;li>&lt;strong>Soporte de LLM nativo&lt;/strong>: además de tabular, soporta texto. Compute embeddings, aplica los tests adecuados.&lt;/li>
&lt;li>&lt;strong>100+ métricas&lt;/strong> en la suite. Te lo cubre todo desde un único framework.&lt;/li>
&lt;li>&lt;strong>Integración con MLflow y kube&lt;/strong>: workflows de CI con reports en cada release.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">evidently&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Report&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">evidently.metrics&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">DataDriftPreset&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ref&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">load_baseline_dataset&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># prompts de la semana pasada&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cur&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">load_current_dataset&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># prompts de la última hora&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">report&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Report&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">metrics&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">DataDriftPreset&lt;/span>&lt;span class="p">()])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">report&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">reference_data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ref&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">current_data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">cur&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">report&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">save_html&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;drift_report.html&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Cuando esta funcionalidad detecta drift, además te dice &lt;strong>qué columna&lt;/strong> y &lt;strong>qué test&lt;/strong> disparó.&lt;/p>
&lt;h4 id="nannyml">NannyML&lt;/h4>
&lt;p>&lt;a href="https://www.nannyml.com/">NannyML&lt;/a> tiene un foco distinto: &lt;strong>estimar el rendimiento del modelo cuando no tienes ground truth&lt;/strong>. Las técnicas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CBPE (Confidence-Based Performance Estimation)&lt;/strong>: estima accuracy usando la confianza del modelo en sus predicciones.&lt;/li>
&lt;li>&lt;strong>DLE (Direct Loss Estimation)&lt;/strong>: estima la pérdida directamente.&lt;/li>
&lt;/ul>
&lt;p>Útil cuando tu app LLM no tiene feedback humano inmediato pero quieres saber si su calidad ha bajado. Apache 2.0, Python.&lt;/p>
&lt;h4 id="whylabs">WhyLabs&lt;/h4>
&lt;p>&lt;a href="https://whylabs.ai/">WhyLabs&lt;/a> es comercial (con whylogs como librería OSS subyacente), enfocada a producción enterprise:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>SaaS managed&lt;/strong> con SOC 2 Type 2 y HIPAA compliance.&lt;/li>
&lt;li>&lt;strong>Real-time monitoring&lt;/strong> vía ingesta continua de logs.&lt;/li>
&lt;li>&lt;strong>Embedding tracking&lt;/strong>: soporte nativo para distribuciones de embeddings, no solo features tabulares.&lt;/li>
&lt;li>&lt;strong>Token probability shifts&lt;/strong>: monitorea la distribución de probabilidades de tokens generados, no solo metadata.&lt;/li>
&lt;/ul>
&lt;p>Para empresas regulated que no quieren operar su propia plataforma de drift detection, es la opción de menos fricción.&lt;/p>
&lt;h4 id="otras-menciones">Otras menciones&lt;/h4>
&lt;p>&lt;a href="https://phoenix.arize.com/">Arize Phoenix&lt;/a> (visto en post de Evals) incluye drift detection como módulo. &lt;a href="https://galileo.ai/">Galileo&lt;/a> tiene productos comerciales especializados en LLM monitoring. &lt;a href="https://www.fiddler.ai/">Fiddler AI&lt;/a> y &lt;a href="https://github.com/SeldonIO/alibi-detect">Alibi Detect&lt;/a> (Seldon) son alternativas más generalistas que también cubren LLM.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Herramienta&lt;/th>
&lt;th>Licencia&lt;/th>
&lt;th>Foco&lt;/th>
&lt;th>Stack típico&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Evidently AI&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Drift reports + LLM&lt;/td>
&lt;td>OSS Python, reports HTML&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>NannyML&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Performance sin GT&lt;/td>
&lt;td>OSS Python, batch&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>WhyLabs&lt;/strong>&lt;/td>
&lt;td>Comercial (whylogs OSS)&lt;/td>
&lt;td>SaaS enterprise, embeddings&lt;/td>
&lt;td>Logs continuos, compliance&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Arize Phoenix&lt;/strong>&lt;/td>
&lt;td>ELv2&lt;/td>
&lt;td>Tracing + drift unificado&lt;/td>
&lt;td>OSS, OTel-native&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Galileo&lt;/strong>&lt;/td>
&lt;td>Comercial&lt;/td>
&lt;td>LLM monitoring premium&lt;/td>
&lt;td>SaaS, ML expert team&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Alibi Detect&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Drift detection general&lt;/td>
&lt;td>OSS Python, Seldon ecosystem&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Fiddler AI&lt;/strong>&lt;/td>
&lt;td>Comercial&lt;/td>
&lt;td>Explainability + monitoring&lt;/td>
&lt;td>Enterprise SaaS&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="parte-3--el-stack-completo-cómo-encaja-todo">Parte 3 — El stack completo: cómo encaja todo&lt;/h2>
&lt;p>Recapitulemos las capas que las dos series han cubierto, ordenadas de &lt;strong>más cercana al request individual&lt;/strong> a &lt;strong>más cercana a la tendencia agregada&lt;/strong>:&lt;/p>
&lt;pre tabindex="0">&lt;code>EVENTOS individuales TENDENCIAS agregadas
│ │
Tracing ──→ Evals ──→ Guardrails ──→ MCP obs ──→ Drift detection
│ │
AgentSight ──→ Tetragon ──→ Hubble ──→ eBPF on-device
│ │
(qué pasa) (qué cambia)
&lt;/code>&lt;/pre>&lt;p>Cada capa responde una pregunta distinta:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Capa&lt;/th>
&lt;th>Pregunta que responde&lt;/th>
&lt;th>Granularidad&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Tracing&lt;/strong> (Langfuse, AgentSight)&lt;/td>
&lt;td>¿Qué hizo el agente exactamente?&lt;/td>
&lt;td>Una sesión&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Evals&lt;/strong>&lt;/td>
&lt;td>¿Fue buena la respuesta?&lt;/td>
&lt;td>Una respuesta&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Guardrails&lt;/strong>&lt;/td>
&lt;td>¿Es seguro este prompt/respuesta?&lt;/td>
&lt;td>Un mensaje&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MCP observability&lt;/strong>&lt;/td>
&lt;td>¿Qué tools invocó, cuánto coste?&lt;/td>
&lt;td>Una llamada tool&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>eBPF en agente/red&lt;/strong> (AgentSight, Hubble)&lt;/td>
&lt;td>¿Cómo se comportó el sistema?&lt;/td>
&lt;td>Por proceso/conexión&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>eBPF en motor local&lt;/strong> (ProfInfer-like)&lt;/td>
&lt;td>¿Cómo se ejecutó el modelo?&lt;/td>
&lt;td>Por función runtime&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Drift detection&lt;/strong>&lt;/td>
&lt;td>¿Está cambiando algo silenciosamente?&lt;/td>
&lt;td>Distribución&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Ninguna sustituye a las demás. La cobertura completa requiere las siete. La operación práctica:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Capas 1-3&lt;/strong> (tracing, evals, guardrails) son &lt;strong>obligatorias desde el día uno&lt;/strong>. Cualquier app LLM en producción que no las tenga está pilotando a ciegas.&lt;/li>
&lt;li>&lt;strong>Capa 4&lt;/strong> (MCP) se vuelve obligatoria cuando hay agentes con tools, que es la mayoría en 2026.&lt;/li>
&lt;li>&lt;strong>Capas 5-6&lt;/strong> (eBPF) se vuelven valiosas cuando la escala justifica el coste de operación (&amp;gt;10 servicios, &amp;gt;100 pods de inferencia).&lt;/li>
&lt;li>&lt;strong>Capa 7&lt;/strong> (drift) es la que &lt;strong>más se descuida y más caro sale ignorar&lt;/strong>: se cubre con un día de trabajo para tener el pipeline básico y ahorra semanas de incidencias futuras.&lt;/li>
&lt;/ol>
&lt;h2 id="patrón-operativo-de-drift-en-2026">Patrón operativo de drift en 2026&lt;/h2>
&lt;p>La receta mínima que cualquier app LLM seria debería tener:&lt;/p>
&lt;h3 id="paso-1--establecer-baseline">Paso 1 — Establecer baseline&lt;/h3>
&lt;p>Durante un periodo estable post-release (2 semanas mínimo), almacena:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Embeddings de todos los prompts&lt;/strong> (vector + metadata: timestamp, user_segment, tenant).&lt;/li>
&lt;li>&lt;strong>Embeddings de las respuestas&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Scores de evals&lt;/strong> automatizados sobre muestra (eg 5-10% del tráfico con G-Eval).&lt;/li>
&lt;li>&lt;strong>Distribución de tools invocadas&lt;/strong> (qué tools, con qué argumentos típicos, con qué frecuencia).&lt;/li>
&lt;/ul>
&lt;p>Storage: cualquier vector store + relational. Cardinalidad razonable a la escala que tengas.&lt;/p>
&lt;h3 id="paso-2--pipeline-continuo-de-comparación">Paso 2 — Pipeline continuo de comparación&lt;/h3>
&lt;p>Cada hora (o cada día según escala):&lt;/p>
&lt;ul>
&lt;li>Toma la muestra del periodo actual (última hora).&lt;/li>
&lt;li>Aplica los tests estadísticos contra el baseline:
&lt;ul>
&lt;li>&lt;strong>PSI&lt;/strong> sobre features simples (longitud prompt, tokens, num tools).&lt;/li>
&lt;li>&lt;strong>KS&lt;/strong> sobre features continuos (latencia, score).&lt;/li>
&lt;li>&lt;strong>MMD&lt;/strong> sobre embeddings.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Genera un drift report (Evidently lo hace en una línea de Python).&lt;/li>
&lt;/ul>
&lt;h3 id="paso-3--alertas-y-workflow-de-investigación">Paso 3 — Alertas y workflow de investigación&lt;/h3>
&lt;p>Configurar thresholds y rutas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>PSI &amp;gt; 0.25 sobre tokens consumidos&lt;/strong>: alerta moderada (puede ser legítimo, investigar segmentos).&lt;/li>
&lt;li>&lt;strong>MMD significativo sobre embeddings de prompts&lt;/strong>: alerta alta (cambio en user mix o ataque coordinado).&lt;/li>
&lt;li>&lt;strong>Eval rubric score baja &amp;gt;5% en rolling 7d&lt;/strong>: alerta crítica.&lt;/li>
&lt;li>&lt;strong>Nuevo cluster en embedding space del 10%+ del tráfico&lt;/strong>: workflow de revisión (puede ser nuevo segmento legítimo o anomalía).&lt;/li>
&lt;/ul>
&lt;p>Cada alerta debe llevar a &lt;strong>un dashboard de drill-down&lt;/strong> con los segmentos afectados, no a un Slack message vacío. La regla operativa: si alguien no puede investigar el alert en &amp;lt;5 minutos, no se va a investigar.&lt;/p>
&lt;h3 id="paso-4--refresh-de-baseline">Paso 4 — Refresh de baseline&lt;/h3>
&lt;p>El baseline no es estático. Cada N semanas, &lt;strong>refresca el baseline&lt;/strong> incorporando lo &amp;ldquo;estable nuevo&amp;rdquo;. Si en 3 meses el patrón de uso ha cambiado legítimamente (más usuarios internacionales, idiomas nuevos), el baseline debe reflejarlo. La cadencia típica: trimestral.&lt;/p>
&lt;h2 id="trampas-operativas">Trampas operativas&lt;/h2>
&lt;h3 id="baseline-contaminado">Baseline contaminado&lt;/h3>
&lt;p>Tomas el baseline de un periodo que ya contenía el problema en germen. Resultado: el baseline incluye el comportamiento malo, los tests no disparan nunca. Solución: verificar el baseline contra una segunda muestra independiente (por ejemplo, la primera semana vs la segunda) antes de bendecirlo.&lt;/p>
&lt;h3 id="threshold-demasiado-bajo">Threshold demasiado bajo&lt;/h3>
&lt;p>PSI &amp;gt; 0.05 dispara constantemente. Tu equipo aprende a ignorar las alertas. &lt;strong>Calibrar thresholds&lt;/strong> según el ruido natural de tu sistema: corre el sistema con baseline + muestras semanales sucesivas y mide la distribución de PSI; pon el threshold un par de desviaciones por encima de lo normal.&lt;/p>
&lt;h3 id="embeddings-no-representativos">Embeddings no representativos&lt;/h3>
&lt;p>Usas el embedding model de OpenAI &lt;code>text-embedding-3-small&lt;/code> para detectar drift en un sistema que sirve preguntas técnicas en español sobre redes Cisco. Resultado: el embedding model no captura la semántica fina del dominio. Solución: usar embeddings finetuned para tu dominio o uno fuerte en multilenguaje y técnico.&lt;/p>
&lt;h3 id="sobrecarga-de-almacenamiento">Sobrecarga de almacenamiento&lt;/h3>
&lt;p>Almacenar embedding de cada prompt en producción a escala (millones de prompts/día) llena disco y aumenta coste. &lt;strong>Sampling estratificado&lt;/strong>: guarda 5-10% del tráfico, pero asegúrate de que los segmentos minoritarios están sobrerrepresentados para no perderlos.&lt;/p>
&lt;h3 id="confundir-drift-con-el-sistema-funciona">Confundir drift con &amp;ldquo;el sistema funciona&amp;rdquo;&lt;/h3>
&lt;p>A veces el drift es &lt;strong>buen drift&lt;/strong>: los usuarios nuevos descubren que el agente sabe hacer X cosa, y de pronto el 30% del tráfico es para X cosa. La distribución cambió porque el producto encontró un nuevo uso. &lt;strong>Antes de tirar de la alarma&lt;/strong>, verifica si el cambio es deseable.&lt;/p>
&lt;h3 id="privacy-en-almacenamiento-de-embeddings">Privacy en almacenamiento de embeddings&lt;/h3>
&lt;p>Embeddings pueden ser invertidos parcialmente a su texto original con técnicas de embedding inversion. Si los prompts contienen PII, almacenar embeddings durante meses para drift detection es un vector de fuga. &lt;strong>Cifrar at rest y rotar regularmente&lt;/strong>, o trabajar con embeddings agregados/promediados.&lt;/p>
&lt;h3 id="ebpf-en-producción-sin-profile-guardrails">eBPF en producción sin profile guardrails&lt;/h3>
&lt;p>Adjuntar uprobes en funciones de hot path como &lt;code>llama_decode&lt;/code> puede impactar throughput si no se hace con cuidado. &lt;strong>Probar siempre en staging&lt;/strong> y monitorizar overhead. ProfInfer reporta &amp;lt;4%; lo que tú midas puede variar según tu binario y kernel.&lt;/p>
&lt;h2 id="cerrando-las-dos-series">Cerrando las dos series&lt;/h2>
&lt;p>Esta semana hemos escrito &lt;strong>12 artículos&lt;/strong> que recorren el stack moderno de inferencia LLM en producción de arriba abajo:&lt;/p>
&lt;p>&lt;strong>Serie inferencia LLM (4 artículos)&lt;/strong>:&lt;/p>
&lt;ul>
&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> — fundamentos.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes&lt;/a> — el motor.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention deep dive&lt;/a> — cómo funciona por dentro.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators LLM en Kubernetes&lt;/a> — orquestación.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Serie eBPF (4 artículos)&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/ebpf-cilium-tcp-ip-bypass/">eBPF de cero a Cilium&lt;/a> — el sustrato.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/tetragon-runtime-security/">Tetragon&lt;/a> — seguridad runtime.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/hubble-observabilidad-ebpf/">Hubble&lt;/a> — observabilidad de red.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/agentsight-tracing-llm/">AgentSight&lt;/a> — observabilidad de agentes.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Serie post-tracing (4 artículos)&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals&lt;/a> — calidad reactiva.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">Guardrails&lt;/a> — seguridad preventiva.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&lt;/a> — protocolo de herramientas.&lt;/li>
&lt;li>&lt;strong>Este&lt;/strong> — drift detection y eBPF en inferencia local.&lt;/li>
&lt;/ul>
&lt;p>Si lees los doce en orden tienes un mapa razonablemente completo de &lt;strong>qué hace falta para operar agentes IA en producción seria en 2026&lt;/strong>, con el detalle suficiente para no chocarte con los problemas habituales en el primer mes. Y, sobre todo, con la mentalidad de que &lt;strong>observabilidad LLM es un stack, no un producto&lt;/strong>: cada capa resuelve un problema, ninguna las resuelve todas, y la combinación es lo que define a un sistema operable de uno que aguanta hasta el primer incidente.&lt;/p>
&lt;h2 id="lo-que-queda-para-futuras-series">Lo que queda para futuras series&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>MLOps específico para LLMs&lt;/strong>: fine-tuning continuo, RAG over data lakes, agent training.&lt;/li>
&lt;li>&lt;strong>Constitutional AI y alignment runtime&lt;/strong>: cómo el modelo se autorregula con guardrails internos.&lt;/li>
&lt;li>&lt;strong>GPU networking&lt;/strong>: InfiniBand, NCCL, GPUDirect — el ángulo que dejamos sin tocar.&lt;/li>
&lt;li>&lt;strong>Edge inference&lt;/strong>: llama.cpp en móviles, MLX en macOS, Snapdragon NPU.&lt;/li>
&lt;li>&lt;strong>Inference scheduling teórico&lt;/strong>: CFS-like algorithms aplicados a LLM serving multi-tenant.&lt;/li>
&lt;/ul>
&lt;p>Los iremos cubriendo. Hasta aquí, gracias por leer estos doce posts. Si te ha aportado algo, compártelo con un colega.&lt;/p>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>eBPF en inferencia local:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://arxiv.org/abs/2601.20755">ProfInfer: An eBPF-based Fine-Grained LLM Inference Profiler (arxiv 2601.20755)&lt;/a> — paper de referencia 2026.&lt;/li>
&lt;li>&lt;a href="https://www.glukhov.org/observability/monitoring-llm-inference-prometheus-grafana/">Monitor LLM Inference in Production 2026 (Glukhov)&lt;/a> — Prometheus + Grafana para vLLM/TGI/llama.cpp.&lt;/li>
&lt;li>&lt;a href="https://www.armosec.io/blog/observability-for-ai-inference-servers/">AI Inference Server Observability in Kubernetes (ARMO)&lt;/a> — las cuatro señales que MLOps tools no capturan.&lt;/li>
&lt;li>&lt;a href="https://developers.redhat.com/articles/2025/09/30/vllm-or-llamacpp-choosing-right-llm-inference-engine-your-use-case">vLLM vs llama.cpp: Choosing the right engine (Red Hat)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Drift detection conceptos:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://futureagi.com/blog/what-is-llm-drift-2026">What is LLM Drift? Types, Detection, Mitigation 2026 (FutureAGI)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://stackpulsar.com/blog/llm-model-drift-detection/">LLM Model Drift Detection 2026 (Stack Pulsar)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://galileo.ai/blog/best-llm-output-drift-monitoring-platforms">9 Best LLM Drift Monitoring Platforms in 2026 (Galileo)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2404.18673">Open-Source Drift Detection Tools in Action (arxiv 2404.18673)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2309.10000">Detecting covariate drift in text data using document embeddings (arxiv 2309.10000)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://futureagi.com/blog/best-ai-drift-detection-tools-2026">Best AI Drift Detection Tools in 2026 (FutureAGI)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Herramientas:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/evidentlyai/evidently">Evidently AI (GitHub)&lt;/a> — open-source.&lt;/li>
&lt;li>&lt;a href="https://www.evidentlyai.com/">Evidently — sitio oficial&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.nannyml.com/">NannyML&lt;/a> — performance sin ground truth.&lt;/li>
&lt;li>&lt;a href="https://whylabs.ai/">WhyLabs&lt;/a> — managed observability.&lt;/li>
&lt;li>&lt;a href="https://github.com/SeldonIO/alibi-detect">Alibi Detect (Seldon)&lt;/a> — drift detection general.&lt;/li>
&lt;li>&lt;a href="https://phoenix.arize.com/">Arize Phoenix&lt;/a> — drift integrado con tracing.&lt;/li>
&lt;/ul>
&lt;p>Tests estadísticos:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://mlpipeline-cloud.com/blog/data-drift-detection-psi-ks">Data drift detection: PSI vs Kolmogorov–Smirnov (MLPipeline)&lt;/a> — comparativa práctica.&lt;/li>
&lt;li>&lt;a href="https://brandonwie.dev/posts/psi-model-drift-detection">Population Stability Index for Model Drift Detection&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.evidentlyai.com/blog/data-drift-detection-large-datasets">Which test is the best? 5 methods to detect data drift (Evidently)&lt;/a> — los 6 horas de ventaja de KS.&lt;/li>
&lt;/ul>
&lt;p>Cross-references (las tres series completas):&lt;/p>
&lt;ul>
&lt;li>Serie inferencia LLM: &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 K8s&lt;/a>, &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention&lt;/a>, &lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators LLM K8s&lt;/a>.&lt;/li>
&lt;li>Serie eBPF: &lt;a href="https://blog.lo0.es/posts/ebpf-cilium-tcp-ip-bypass/">eBPF de cero a Cilium&lt;/a>, &lt;a href="https://blog.lo0.es/posts/tetragon-runtime-security/">Tetragon&lt;/a>, &lt;a href="https://blog.lo0.es/posts/hubble-observabilidad-ebpf/">Hubble&lt;/a>, &lt;a href="https://blog.lo0.es/posts/agentsight-tracing-llm/">AgentSight&lt;/a>.&lt;/li>
&lt;li>Serie post-tracing: &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/guardrails-safety-llm/">Guardrails&lt;/a>, &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&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 00:00:00 +0000</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 &lt;strong>anunciar&lt;/strong> &amp;ldquo;contexto de 128 K&amp;rdquo; y &lt;strong>servirlo&lt;/strong> de verdad bajo carga.&lt;/p>
&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 a la tercera sesión 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;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;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;pre tabindex="0">&lt;code>Σ i (i=1..N) = N·(N+1) / 2 ≈ N² / 2
&lt;/code>&lt;/pre>&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;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;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;!-- 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;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;/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;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;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;!-- 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;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;/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;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;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;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;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;text x="180" y="190" text-anchor="middle" class="lbl-sm">→ ~70 % de VRAM reservada y vacía&lt;/text>
&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;rect x="0" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="30" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="60" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="90" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="120" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="150" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="180" y="22" width="30" height="20" class="free blk"/>
&lt;rect x="210" y="22" width="30" height="20" class="free blk"/>
&lt;rect x="0" y="44" width="30" height="20" class="used blk"/>
&lt;rect x="30" y="44" width="30" height="20" class="used blk"/>
&lt;rect x="60" y="44" width="30" height="20" class="used blk"/>
&lt;rect x="90" y="44" width="30" height="20" class="free blk"/>
&lt;rect x="120" y="44" width="30" height="20" class="free blk"/>
&lt;rect x="150" y="44" width="30" height="20" class="free blk"/>
&lt;rect x="180" y="44" width="30" height="20" class="free blk"/>
&lt;rect x="210" y="44" width="30" height="20" class="free blk"/>
&lt;/g>
&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="dos-escenarios-workstation-vs-cluster">Dos escenarios: workstation vs cluster&lt;/h2>
&lt;p>Bajemos a casos concretos y comparemos las dos puntas del espectro de hardware on-premise: una &lt;strong>workstation con una GPU consumer&lt;/strong> frente a un &lt;strong>cluster de GPUs de datacenter&lt;/strong>. Mismas matemáticas, dos órdenes de magnitud de diferencia en lo que pueden servir.&lt;/p>
&lt;h3 id="escenario-a--una-rtx-4090-24-gb-ada-lovelace">Escenario A — Una 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="escenario-b--cluster-de-5h100-sxm-400-gb-agregados-nvlink">Escenario B — Cluster de 5×H100 SXM (400 GB agregados, 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="a-y-b-lado-a-lado">A y B, lado a lado&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th style="text-align:right">Escenario A (1×4090)&lt;/th>
&lt;th style="text-align:right">Escenario B (5×H100 SXM)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>VRAM total&lt;/td>
&lt;td style="text-align:right">24 GB&lt;/td>
&lt;td style="text-align:right">400 GB (≈ 17×)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Modelo servible (BF16)&lt;/td>
&lt;td style="text-align:right">Hasta ~8 B&lt;/td>
&lt;td style="text-align:right">Hasta ~70 B&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KV cache disponible&lt;/td>
&lt;td style="text-align:right">~6 GB&lt;/td>
&lt;td style="text-align:right">~250 GB (≈ 42×)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokens totales de cache&lt;/td>
&lt;td style="text-align:right">~43 K&lt;/td>
&lt;td style="text-align:right">~800 K (≈ 19×)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ancho de banda HBM&lt;/td>
&lt;td style="text-align:right">1.0 TB/s (GDDR6X)&lt;/td>
&lt;td style="text-align:right">3.35 TB/s/GPU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Coste indicativo&lt;/td>
&lt;td style="text-align:right">~2 K €&lt;/td>
&lt;td style="text-align:right">~250 K € (≈ 125×)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La asimetría interesante: pasar de A a B cuesta unas 125× más, pero sólo da ~19× más cache total. Lo que el cluster compra de verdad no es proporcionalmente más contexto: es &lt;strong>modelos un orden de magnitud más grandes&lt;/strong> servibles a la vez (8 B → 70 B), &lt;strong>ancho de banda HBM 3× mayor por GPU&lt;/strong> y &lt;strong>concurrencia muy alta sin degradar latencia&lt;/strong>. Si tu carga real es &amp;ldquo;modelos pequeños con mucho contexto y baja concurrencia&amp;rdquo;, la 4090 cuantizada rinde mucho más cerca del cluster de lo que el precio sugiere. Si es &amp;ldquo;modelo grande, baja latencia, muchos usuarios&amp;rdquo;, el salto a HBM3 + NVLink no se sustituye con más 4090s.&lt;/p>
&lt;h3 id="implicaciones-operativas">Implicaciones operativas&lt;/h3>
&lt;p>Tres observaciones para llevarse:&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 comprometerse a un SLA.&lt;/p>
&lt;p>Segundo, &lt;strong>cuantizar el KV cache es de las optimizaciones con mejor relación coste/beneficio&lt;/strong>. No toca los pesos, no afecta a la reproducibilidad de las salidas 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="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>