<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Optimización on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/optimizaci%C3%B3n/</link><description>Recent content in Optimización on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 04 Jun 2026 23:00:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/tags/optimizaci%C3%B3n/index.xml" rel="self" type="application/rss+xml"/><item><title>Optimizando el decode en vLLM: exprimir cada token en hardware pequeño</title><link>https://blog.lo0.es/posts/decode-optimizaciones-vllm/</link><pubDate>Thu, 04 Jun 2026 23:00:00 +0000</pubDate><guid>https://blog.lo0.es/posts/decode-optimizaciones-vllm/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El decode es la fase en la que vLLM genera tokens de salida uno a uno. Es &lt;strong>memory-bound&lt;/strong>, no compute-bound: la GPU pasa más tiempo esperando que lleguen los pesos desde VRAM que haciendo cálculos. En hardware pequeño —RTX 4090 (24 GB) o L40 (48 GB)— el decode mal configurado desaprovecha la mitad de la capacidad de la tarjeta. Cinco parámetros de vLLM cambian la ecuación: &lt;code>gpu-memory-utilization&lt;/code>, &lt;code>max-num-seqs&lt;/code>, speculative decoding, KV cache en FP8 y un &lt;code>swap-space&lt;/code> correctamente en cero. Bien calibrados, la diferencia es real: de 15 tokens/s a 35–50 tokens/s en el mismo hardware.&lt;/p>
&lt;hr>
&lt;h2 id="la-analogía">La analogía&lt;/h2>
&lt;p>Un obrero de cadena de montaje que ensambla coches. Cada coche requiere exactamente el mismo proceso: va a buscar la pieza al almacén, vuelve, la atornilla, repite. El tiempo de transporte al almacén —la latencia de VRAM— es fijo y no se puede eliminar. Pero hay formas de hacerlo menos doloroso:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Tener varios coches en paralelo en la cadena&lt;/strong> (más concurrencia, mismo tiempo de transporte amortizado).&lt;/li>
&lt;li>&lt;strong>Que un ayudante prefabrique piezas comunes&lt;/strong> (speculative decoding: el draft model propone, el verifier confirma).&lt;/li>
&lt;li>&lt;strong>Almacenar en el taller sólo las piezas más usadas&lt;/strong> (KV cache cuantizado: caben más contextos en el mismo espacio).&lt;/li>
&lt;/ol>
&lt;p>Las tres estrategias son exactamente los tres ejes de optimización del decode en vLLM.&lt;/p>
&lt;hr>
&lt;h2 id="por-qué-el-decode-es-memory-bound">Por qué el decode es memory-bound&lt;/h2>
&lt;p>Durante el prefill, la GPU procesa N tokens en paralelo: la operación de atención es un matmul grande y las unidades de cómputo están ocupadas. Durante el decode, procesa &lt;strong>1 token por paso&lt;/strong>: el matmul se convierte en un vector-matrix product, operación que infrautiliza los tensor cores.&lt;/p>
&lt;p>El ratio de utilización de cómputo durante decode típico en una RTX 4090:&lt;/p>
&lt;p>$$\text{MFU}_{decode} \approx 5–15% \quad \text{(vs 40–60% en prefill)}$$&lt;/p>
&lt;p>El cuello no es la potencia de cálculo, sino el ancho de banda. Para generar cada token, el modelo tiene que leer sus pesos completos desde VRAM:&lt;/p>
&lt;p>$$\text{tiempo_por_token} \approx \frac{\text{tamaño_pesos_bytes}}{\text{ancho_banda_VRAM}}$$&lt;/p>
&lt;p>Para Qwen2.5-7B en BF16 (14 GB de pesos) en una RTX 4090 (1.008 GB/s):&lt;/p>
&lt;p>$$t \approx \frac{14 \times 10^9}{1.008 \times 10^{12}} \approx 13.9 \text{ ms/token} \approx 72 \text{ tokens/s teórico máximo}$$&lt;/p>
&lt;p>El valor real es menor (~30–50 tok/s) por overhead de scheduler, atención sobre el KV cache creciente y otras latencias. Pero el límite teórico marca el techo.&lt;/p>
&lt;p>Con Q4_K_M (pesos ~4 GB):&lt;/p>
&lt;p>$$t \approx \frac{4 \times 10^9}{1.008 \times 10^{12}} \approx 3.97 \text{ ms/token} \approx 252 \text{ tokens/s teórico}$$&lt;/p>
&lt;p>Cuantizar el modelo es la forma más directa de mejorar el throughput de decode en hardware memory-bound. Todo lo demás optimiza sobre ese techo.&lt;/p>
&lt;hr>
&lt;h2 id="las-cinco-palancas">Las cinco palancas&lt;/h2>
&lt;h3 id="1-darle-a-vllm-toda-la-vram-que-puedas----gpu-memory-utilization">1. Darle a vLLM toda la VRAM que puedas — &lt;code>--gpu-memory-utilization&lt;/code>&lt;/h3>
&lt;p>&lt;code>--gpu-memory-utilization&lt;/code> (abreviado &lt;code>--gpu-mem-util&lt;/code>) define la fracción de VRAM disponible que vLLM puede usar para el &lt;strong>KV cache&lt;/strong>, una vez cargados los pesos del modelo. El resto lo reserva para activaciones durante el forward pass y el contexto CUDA.&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">vllm serve mi-modelo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.92
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El valor por defecto es &lt;code>0.90&lt;/code>. En bare metal donde ningún otro proceso usa la GPU, &lt;code>0.92–0.95&lt;/code> es seguro. No subas de &lt;code>0.95&lt;/code>: vLLM necesita margen para activaciones durante picos de batch, y quedarse sin VRAM en medio de una inferencia resulta en un crash del proceso, no en un error limpio.&lt;/p>
&lt;p>&lt;strong>Por qué importa:&lt;/strong> más KV cache disponible = más requests simultáneos en vuelo = mejor utilización de la GPU durante decode. PagedAttention asigna el KV cache en bloques de tamaño fijo (16 tokens/bloque por defecto), y vLLM los gestiona como páginas de memoria virtual. A más bloques disponibles, más requests puede servir sin que ninguna se quede esperando por espacio.&lt;/p>
&lt;pre tabindex="0">&lt;code>RTX 4090, Qwen2.5-7B-BF16 (14 GB pesos):
VRAM total: 24 GB
Pesos: 14 GB
Disponible para KV cache: 10 GB
gpu-memory-utilization 0.90 → 0.90 × 10 GB = 9 GB para KV cache
gpu-memory-utilization 0.94 → 0.94 × 10 GB = 9.4 GB → ~4% más de tokens en vuelo
&lt;/code>&lt;/pre>&lt;p>El impacto es modesto con modelos que caben cómodos, pero se amplifica con modelos que apuran la VRAM.&lt;/p>
&lt;hr>
&lt;h3 id="2-concurrencia-real----max-num-seqs">2. Concurrencia real — &lt;code>--max-num-seqs&lt;/code>&lt;/h3>
&lt;p>&lt;code>--max-num-seqs&lt;/code> es el número máximo de requests que vLLM puede tener en proceso simultáneamente (sumando prefill y decode). Es el parámetro que controla la concurrencia efectiva del sistema.&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">vllm serve mi-modelo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">128&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El efecto es directo: más requests en decode simultáneo = mejor amortización del coste fijo de leer pesos. Cuando el batch de decode crece de 1 a 8, el tiempo de generar 8 tokens es casi el mismo que generar 1 (los pesos se leen una sola vez para todos). El throughput agregado escala casi linealmente hasta que el KV cache o la VRAM de activaciones se convierten en el cuello.&lt;/p>
&lt;p>$$\text{throughput_agregado}(B) \approx B \times \text{throughput}(1) \quad \text{para } B \ll B_{max}$$&lt;/p>
&lt;p>&lt;strong>Error común:&lt;/strong> subir &lt;code>--max-num-seqs&lt;/code> sin asegurarse de que hay suficiente KV cache en VRAM para todas las requests. Si vLLM no puede alojar los KV cache de 128 requests simultáneas, hace preemption (pausa alguna request y libera su KV cache) con coste de latencia. Monitoriza &lt;code>vllm:num_preemptions_total&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Interacción con &lt;code>--max-num-batched-tokens&lt;/code>:&lt;/strong> el scheduler de vLLM procesa hasta &lt;code>max-num-batched-tokens&lt;/code> tokens por paso. Si tienes 128 requests en decode generando 1 token cada una, eso son 128 tokens de decode. El presupuesto de decode consume 128 tokens del presupuesto total; el resto lo dedica a prefill en chunks. Ajusta ambos valores conjuntamente.&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"># Para RTX 4090 sirviendo ~50 usuarios concurrentes con respuestas de hasta 512 tokens&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve mi-modelo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">64&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-batched-tokens &lt;span class="m">8192&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 64 tokens de decode por paso + hasta 8128 tokens de prefill chunked&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="3-speculative-decoding----speculative-model----num-speculative-tokens">3. Speculative decoding — &lt;code>--speculative-model&lt;/code> + &lt;code>--num-speculative-tokens&lt;/code>&lt;/h3>
&lt;p>Speculative decoding es el cambio más impactante para decode en hardware pequeño. La idea es simple: un modelo draft pequeño propone varios tokens a la vez, y el modelo verifier los valida o rechaza en un solo forward pass.&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">vllm serve Qwen/Qwen2.5-7B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-model Qwen/Qwen2.5-0.5B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-speculative-tokens &lt;span class="m">5&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-draft-tensor-parallel-size &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Por qué funciona:&lt;/strong> el verifier de 7B tiene que leer 14 GB de pesos por paso. Con 5 tokens propuestos, si el acceptance rate es del 80%, se generan de media 4–5 tokens por paso de verifier en vez de 1. El throughput efectivo sube sin que la GPU trabaje más.&lt;/p>
&lt;p>El acceptance rate (α) depende de qué tan bien el draft predice la distribución del verifier. Para el mismo dominio, modelos de la misma familia suelen tener α &amp;gt; 0.75:&lt;/p>
&lt;p>$$\text{speedup} \approx \frac{1 + \alpha \cdot k}{1 + \alpha \cdot k / \text{cost_ratio}}$$&lt;/p>
&lt;p>Donde $k$ es el número de tokens propuestos y cost_ratio es el ratio de coste draft/verifier. Para un 0.5B draft y 7B verifier (ratio ~14×):&lt;/p>
&lt;p>$$\text{speedup} \approx 1 + 0.8 \times 5 \approx 5 \text{ (teórico máximo, no alcanzable)}$$&lt;/p>
&lt;p>En práctica, con α = 0.75 y k = 5 en hardware sin NVLink: &lt;strong>1.8–2.5× más tokens/s&lt;/strong> comparado con decode solo.&lt;/p>
&lt;p>&lt;strong>EAGLE-3 en 2026:&lt;/strong> los mejores drafters actuales no son versiones small del mismo modelo, sino redes especializadas en predecir la distribución del verifier. EAGLE-3 reporta 3–6.5× speedup sobre decode vanilla en benchmarks públicos. En producción con batches mixtos el speedup real es más conservador (1.5–3×). vLLM soporta EAGLE/EAGLE-2 via &lt;code>--speculative-model&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"># Con un drafter EAGLE (requiere drafter entrenado específicamente para el base model)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve meta-llama/Llama-3.1-8B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-model yuhuili/EAGLE3-LLaMA3.1-Instruct-8B &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-speculative-tokens &lt;span class="m">6&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Cuándo el speculative decoding NO ayuda:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Batches muy grandes (&amp;gt;32 requests): el acceptance rate varía entre requests y el batch pasa más tiempo en re-draft que en aceptar.&lt;/li>
&lt;li>Tareas de alta entropía (brainstorming, código muy creativo): el draft predice peor, α cae por debajo de 0.5 y el overhead del draft pesa más que la ganancia.&lt;/li>
&lt;li>Si el modelo draft no cabe en la VRAM disponible junto al verifier.&lt;/li>
&lt;/ul>
&lt;p>En una RTX 4090 con un 7B verifier y un 0.5B draft (BF16): 14 + 1 GB = 15 GB. Quedan 9 GB para KV cache. Funciona.&lt;/p>
&lt;hr>
&lt;h3 id="4-kv-cache-cuantizado----kv-cache-dtype-fp8">4. KV cache cuantizado — &lt;code>--kv-cache-dtype fp8&lt;/code>&lt;/h3>
&lt;p>Ya se cubrió en el artículo de prefill para su efecto en capacidad de contexto. Desde el punto de vista del decode, el beneficio es diferente: más tokens caben en el KV cache → más requests simultáneas sin preemption → mejor throughput agregado.&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">vllm serve mi-modelo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Advertencia de precisión en decode:&lt;/strong> el KV cache se lee en cada paso de atención del decode. La cuantización introduce ruido en las activaciones de atención. Para textos largos (&amp;gt;4K tokens de contexto) puede acumularse. En benchmarks de calidad (MMLU, HellaSwag) la degradación con FP8 KV y &lt;code>--calculate-kv-scales&lt;/code> es &amp;lt;0.5% en modelos modernos. Sin &lt;code>--calculate-kv-scales&lt;/code>, la degradación puede ser mayor porque las escalas se fijan estáticamente.&lt;/p>
&lt;p>&lt;strong>Combinación óptima para RTX 4090:&lt;/strong>&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">vllm serve Qwen/Qwen2.5-7B-Instruct-AWQ &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --quantization awq &lt;span class="se">\ &lt;/span> &lt;span class="c1"># pesos en INT4: 4 GB modelo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --kv-cache-dtype fp8 &lt;span class="se">\ &lt;/span> &lt;span class="c1"># KV cache a mitad de tamaño&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --calculate-kv-scales &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.94
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># VRAM disponible: 24 - 4 = 20 GB para KV cache&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Con FP8: ~40 KB/token (vs 80 KB BF16) → 20 GB / 40 KB = 500.000 tokens de contexto total&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Con max-num-seqs 64 y ctx de 4K: 64 × 4096 × 40KB = 10 GB → cabe con margen&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="5-eliminar-el-swap----swap-space-0">5. Eliminar el swap — &lt;code>--swap-space 0&lt;/code>&lt;/h3>
&lt;p>&lt;code>--swap-space&lt;/code> define cuánta RAM de sistema (no VRAM) puede usar vLLM para hacer preemption de KV caches. Cuando vLLM tiene más requests activas de las que caben en VRAM, puede &amp;ldquo;pausar&amp;rdquo; algunas moviendo su KV cache a RAM y reactivarlas más tarde.&lt;/p>
&lt;p>El problema: mover un KV cache de 4K tokens de VRAM a RAM y de vuelta tiene una latencia de decenas de milisegundos vía PCIe. Para un sistema donde quieres latencia predecible, el swap introduce jitter inaceptable.&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">vllm serve mi-modelo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --swap-space &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Con &lt;code>--swap-space 0&lt;/code>, cuando vLLM no puede alojar más requests en VRAM, directamente las encola en vez de hacer preemption. La cola añade latencia de espera, pero es predecible y no interrumpe las requests ya en vuelo.&lt;/p>
&lt;p>&lt;strong>¿Cuándo sí tener swap?&lt;/strong> Si tu workload tiene picos de demanda cortos y puedes tolerar jitter ocasional a cambio de no rechazar requests, un swap de 4–8 GB puede ser útil. En despliegues ENS donde la latencia es un SLA contrato, &lt;code>--swap-space 0&lt;/code> es la opción correcta.&lt;/p>
&lt;hr>
&lt;h2 id="la-configuración-de-referencia-por-hardware">La configuración de referencia por hardware&lt;/h2>
&lt;h3 id="rtx-4090-24-gb--modelo-7b-uso-interno">RTX 4090 (24 GB) — modelo 7B, uso interno&lt;/h3>
&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">vllm serve Qwen/Qwen2.5-7B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.92 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-model-len &lt;span class="m">8192&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">64&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-batched-tokens &lt;span class="m">8192&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-chunked-prefill &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-prefix-caching &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --swap-space &lt;span class="m">0&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-model Qwen/Qwen2.5-0.5B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-speculative-tokens &lt;span class="m">5&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-draft-tensor-parallel-size &lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dtype bfloat16
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Throughput esperado: &lt;strong>35–55 tokens/s por usuario&lt;/strong>, hasta 64 simultáneos, TTFT &amp;lt;500ms para prompts &amp;lt;1K tokens.&lt;/p>
&lt;h3 id="l40-48-gb--modelo-14b-multi-usuario">L40 (48 GB) — modelo 14B, multi-usuario&lt;/h3>
&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">vllm serve Qwen/Qwen2.5-14B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.90 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-model-len &lt;span class="m">16384&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">128&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-batched-tokens &lt;span class="m">16384&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-chunked-prefill &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-prefix-caching &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --swap-space &lt;span class="m">0&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-model Qwen/Qwen2.5-1.5B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-speculative-tokens &lt;span class="m">5&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dtype bfloat16
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Throughput esperado: &lt;strong>25–40 tokens/s por usuario&lt;/strong>, hasta 128 simultáneos con speculative decoding activo, TTFT &amp;lt;800ms para prompts &amp;lt;2K tokens.&lt;/p>
&lt;hr>
&lt;h2 id="cómo-medir-que-el-decode-está-optimizado">Cómo medir que el decode está optimizado&lt;/h2>
&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"># Métricas clave en vllm:8000/metrics&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">vllm:generation_tokens_total &lt;span class="c1"># tokens generados en total → tendencia&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm:e2e_request_latency_seconds_* &lt;span class="c1"># latencia end-to-end por percentil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm:time_per_output_token_seconds_* &lt;span class="c1"># ITL (inter-token latency)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm:num_preemptions_total &lt;span class="c1"># si sube, KV cache se está llenando&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm:spec_decode_draft_acceptance_rate &lt;span class="c1"># hit rate del speculative decoding&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Si &lt;code>spec_decode_draft_acceptance_rate&lt;/code> &amp;lt; 0.6, el drafter no está ayudando: desactiva speculative decoding o busca un drafter mejor entrenado para tu modelo/dominio.&lt;/p>
&lt;p>Si &lt;code>num_preemptions_total&lt;/code> crece, tienes demasiadas requests simultáneas para el KV cache disponible. Opciones: bajar &lt;code>max-num-seqs&lt;/code>, activar FP8 KV cache, bajar &lt;code>max-model-len&lt;/code>, o cuantizar más el modelo.&lt;/p>
&lt;hr>
&lt;h2 id="implicaciones-para-inferencia-on-premise">Implicaciones para inferencia on-premise&lt;/h2>
&lt;p>En un despliegue soberano con hardware fijo, no puedes comprar más GPUs a voluntad. Cada décima de &lt;code>gpu-memory-utilization&lt;/code> bien calibrada, cada punto de acceptance rate del speculative decoding y cada MB de KV cache liberado por FP8 son capacidad real que no tienes que provisionar con otro nodo.&lt;/p>
&lt;p>La combinación de pesos cuantizados (AWQ/GPTQ), KV cache FP8 y speculative decoding permite que un 14B sirva en una L40 lo que sin optimizaciones requeriría dos L40 en tensor parallel. Eso es el argumento económico para invertir tiempo en estos parámetros.&lt;/p>
&lt;p>El decode no se puede acelerar infinitamente en hardware memory-bound: el límite teórico lo pone el ancho de banda de VRAM. Pero la diferencia entre el mínimo y el máximo alcanzable en ese hardware puede ser 3–4× con las palancas correctas.&lt;/p>
&lt;hr>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>https://blog.lo0.es/posts/prefill-optimizaciones-vllm/ — las optimizaciones de la fase de prefill: chunked prefill, prefix caching y FP8 KV&lt;/li>
&lt;li>https://blog.lo0.es/posts/speculative-decoding-fundamentos/ — cómo funciona el speculative decoding por dentro&lt;/li>
&lt;li>https://blog.lo0.es/posts/kv-cache-fundamentos/ — la estructura que el decode consulta en cada token&lt;/li>
&lt;li>https://blog.lo0.es/posts/quantization-fundamentos-inferencia/ — por qué cuantizar pesos cambia el techo de velocidad del decode&lt;/li>
&lt;li>https://blog.lo0.es/posts/continuous-batching-fundamentos/ — la base de la gestión de concurrencia en vLLM&lt;/li>
&lt;li>https://blog.lo0.es/posts/vllm-otel-instrumentacion-optimizaciones/ — cómo medir con Prometheus si speculative decoding, gpu-memory-utilization y max-num-seqs están funcionando: &lt;code>spec_decode_draft_acceptance_rate&lt;/code>, &lt;code>num_preemptions_total&lt;/code> y la matriz de diagnóstico&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://arxiv.org/abs/2309.06180">Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/a> — Kwon et al., 2023&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2406.16858">EAGLE-2: Faster Inference of Language Models with Dynamic Draft Trees&lt;/a> — Li et al., 2024&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2211.17192">Fast Inference from Transformers via Speculative Decoding&lt;/a> — Leviathan et al., 2022&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/en/stable/configuration/optimization/">vLLM Optimization and Tuning — documentación oficial&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/en/stable/features/spec_decode.html">vLLM Speculative Decoding&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/">FP8 KV Cache en vLLM&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Optimizando el prefill en vLLM: los knobs que tu TTFT no perdona</title><link>https://blog.lo0.es/posts/prefill-optimizaciones-vllm/</link><pubDate>Thu, 04 Jun 2026 22:00:00 +0000</pubDate><guid>https://blog.lo0.es/posts/prefill-optimizaciones-vllm/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El prefill es la fase en la que vLLM procesa tu prompt de entrada y produce el KV cache inicial. Es compute-bound (a diferencia del decode, que es memory-bound), tarda más cuanto más largo es el prompt, y bloquea el decode de todas las demás requests en cola. Hay cuatro palancas en vLLM que cambian radicalmente su comportamiento: chunked prefill, prefix caching, FP8 KV cache y el presupuesto de tokens por batch. Con hardware modesto —una RTX 4090 de 24 GB o una L40 de 48 GB— la diferencia entre ignorarlas y usarlas bien puede ser un TTFT 3× menor y un 40% más de throughput agregado.&lt;/p>
&lt;hr>
&lt;h2 id="la-analogía">La analogía&lt;/h2>
&lt;p>Imagina una imprenta de principios del siglo XX. Componer los tipos de plomo (preparar el molde) es lento y bloquea la prensa. Imprimir las páginas ya compuestas es rápido, pero necesita el molde listo antes de empezar.&lt;/p>
&lt;p>El prefill es componer los tipos. El decode es imprimir. Una prensa que sólo puede hacer una cosa a la vez —o compone o imprime— deja la maquinaria parada la mitad del tiempo. La solución histórica fue tener un obrero componiendo la siguiente plana mientras la anterior ya estaba en prensa. Eso es, exactamente, chunked prefill.&lt;/p>
&lt;hr>
&lt;h2 id="qué-es-el-prefill-realmente">Qué es el prefill realmente&lt;/h2>
&lt;p>Cuando llega un request a vLLM, el motor tiene que procesar todos los tokens del prompt de una vez antes de poder emitir el primer token de respuesta. Durante ese procesamiento calcula, para cada token del prompt, sus vectores &lt;strong>Key&lt;/strong> y &lt;strong>Value&lt;/strong> de la atención. El resultado —el KV cache inicial— se almacena en VRAM y se usa durante todo el decode posterior.&lt;/p>
&lt;p>A diferencia del decode, donde el modelo procesa &lt;strong>un token nuevo&lt;/strong> por paso, en el prefill procesa &lt;strong>N tokens de golpe&lt;/strong>. Eso lo hace mucho más eficiente en FLOPs/token (las GPUs son buenas en matmuls grandes), pero tiene un coste cuadrático en atención:&lt;/p>
&lt;p>$$\text{FLOPs_atención_prefill} \approx 4 \cdot N^2 \cdot d_{model}$$&lt;/p>
&lt;p>Con un prompt de 1.000 tokens y $d_{model} = 4096$ (Qwen2.5-7B): $4 \cdot 10^6 \cdot 4096 \approx 16 \times 10^9$ FLOPs sólo en atención. Con 4.000 tokens, &lt;strong>256× más&lt;/strong> por la naturaleza cuadrática.&lt;/p>
&lt;pre tabindex="0">&lt;code>Prefill (compute-bound):
prompt tokens → [attention O(N²)] → [FFN] → KV cache inicial
Decode (memory-bound):
1 token nuevo → [cross-attention sobre KV cache] → siguiente token
&lt;/code>&lt;/pre>&lt;hr>
&lt;h2 id="por-qué-el-prefill-es-un-problema-en-hardware-pequeño">Por qué el prefill es un problema en hardware pequeño&lt;/h2>
&lt;p>En una H100 con 3,35 TB/s de ancho de banda, un prefill largo se amortiza rápido. En una RTX 4090 (1,008 TB/s) o una L40 (864 GB/s), el cuello de botella aparece antes y tiene consecuencias concretas:&lt;/p>
&lt;p>&lt;strong>El problema del head-of-line blocking.&lt;/strong> Por defecto, vLLM procesa un prefill completo antes de hacer cualquier decode. Si tienes 10 requests en cola —9 en decode, 1 con un prompt de 8.000 tokens— esas 9 requests se detienen mientras la GPU mastica el prefill largo. Sus usuarios ven que el streaming se congela. Esto se llama &lt;strong>head-of-line blocking&lt;/strong> y es el enemigo número uno del TTFT en producción.&lt;/p>
&lt;hr>
&lt;h2 id="las-cuatro-palancas">Las cuatro palancas&lt;/h2>
&lt;h3 id="1-chunked-prefill----enable-chunked-prefill----max-num-batched-tokens">1. Chunked prefill — &lt;code>--enable-chunked-prefill&lt;/code> + &lt;code>--max-num-batched-tokens&lt;/code>&lt;/h3>
&lt;p>Chunked prefill parte el prefill largo en trozos (&lt;em>chunks&lt;/em>) y los intercala con pasos de decode en el mismo batch. En vLLM V1 (≥ 0.6) está activo por defecto.&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">vllm serve mi-modelo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-chunked-prefill &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-batched-tokens &lt;span class="m">4096&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--max-num-batched-tokens&lt;/code> es el presupuesto total de tokens que vLLM puede procesar en un único paso del motor, sumando prefill y decode. Es el parámetro más importante para controlar el trade-off:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">&lt;code>max-num-batched-tokens&lt;/code>&lt;/th>
&lt;th style="text-align:left">Efecto&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">Bajo (512–2048)&lt;/td>
&lt;td style="text-align:left">Más pasos de decode por ciclo → mejor ITL, peor TTFT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Alto (8192–32768)&lt;/td>
&lt;td style="text-align:left">Chunks de prefill grandes → mejor TTFT y throughput, peor ITL&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Para una RTX 4090 sirviendo modelos 7B–13B con contextos mixtos (256–4096 tokens):&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">--max-num-batched-tokens &lt;span class="m">8192&lt;/span> &lt;span class="c1"># punto de equilibrio razonable&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Para una L40 (48 GB) con modelos más grandes y prompts más largos:&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">--max-num-batched-tokens &lt;span class="m">16384&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Cómo funciona internamente:&lt;/strong> con un presupuesto de 4.096 tokens y un prefill de 10.000, vLLM lo parte en 3 chunks (4.096 + 4.096 + 1.808). Entre chunks, procesa los pasos de decode pendientes. Las requests en decode siguen avanzando; el prefill largo tarda más en terminar, pero no congela nada.&lt;/p>
&lt;pre tabindex="0">&lt;code>Sin chunked prefill:
t=0 [prefill 10k tokens]─────────────────────────────┐
t=1 └─[decode r1,r2...r9]
Con chunked prefill (budget 4096):
t=0 [prefill chunk 4096][decode r1..r9]
t=1 [prefill chunk 4096][decode r1..r9]
t=2 [prefill chunk 1808][decode r1..r9]
t=3 [decode todos, incluido el nuevo]
&lt;/code>&lt;/pre>&lt;p>El TTFT del request largo aumenta ligeramente (3 pasos en vez de 1), pero el ITL de las otras 9 requests no se interrumpe.&lt;/p>
&lt;hr>
&lt;h3 id="2-prefix-caching----enable-prefix-caching">2. Prefix caching — &lt;code>--enable-prefix-caching&lt;/code>&lt;/h3>
&lt;p>Si múltiples requests comparten el mismo prefijo —un system prompt, un few-shot, un documento de contexto— vLLM puede calcular el KV cache de ese prefijo una sola vez y reutilizarlo.&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">vllm serve mi-modelo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-prefix-caching
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esto se llama &lt;strong>Automatic Prefix Caching (APC)&lt;/strong>. Internamente, vLLM divide el KV cache en bloques de tamaño fijo (por defecto 16 tokens/bloque) y les asigna un hash SHA basado en el contenido. Cuando llega un request nuevo, comprueba si algún bloque inicial ya está en la caché. Si hay hit, se salta ese prefill.&lt;/p>
&lt;p>&lt;strong>El impacto numérico:&lt;/strong> supón un system prompt de 512 tokens que aparece en el 80% de tus requests, y una tasa de 100 req/min:&lt;/p>
&lt;ul>
&lt;li>Sin APC: 80 req/min × 512 tokens × costo_prefill = 41.000 tokens/min de prefill redundante&lt;/li>
&lt;li>Con APC (hit rate 80%): 20 req/min × 512 = 10.240 tokens/min de prefill → &lt;strong>reducción del 75%&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>El TTFT de esos 80 requests cae a lo que cuesta procesar sólo el sufijo nuevo.&lt;/p>
&lt;p>&lt;strong>Limitación con chunked prefill:&lt;/strong> cuando chunked prefill está activo, sólo el primer chunk del prefill se beneficia de APC en la implementación actual de vLLM. Para workloads donde el hit de caché es muy alto y los sufijos son cortos, considera reducir &lt;code>--max-num-batched-tokens&lt;/code> para que el primer chunk cubra más del prefijo compartido.&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"># Configuración optimizada para alto hit rate de prefix cache&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve mi-modelo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-prefix-caching &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-chunked-prefill &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-batched-tokens &lt;span class="m">4096&lt;/span> &lt;span class="c1"># chunks más pequeños = prefijo cabe en chunk 1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="3-fp8-kv-cache----kv-cache-dtype-fp8">3. FP8 KV cache — &lt;code>--kv-cache-dtype fp8&lt;/code>&lt;/h3>
&lt;p>El KV cache ocupa VRAM. Cuanta más VRAM consume, menos requests concurrentes puedes mantener en vuelo. En una RTX 4090 de 24 GB, el modelo Qwen2.5-14B en BF16 ya ocupa ~28 GB —no cabe. En Q4 ocupa ~9 GB, dejando ~14 GB para KV cache.&lt;/p>
&lt;p>¿Cuántos tokens de contexto caben en 14 GB de KV cache BF16 para un 14B con GQA?&lt;/p>
&lt;p>$$\text{KV_size_por_token} = 2 \cdot n_{kv_heads} \cdot d_{head} \cdot n_{layers} \cdot 2 \text{ bytes}$$&lt;/p>
&lt;p>Para Qwen2.5-14B: $n_{kv_heads}=8$, $d_{head}=128$, $n_{layers}=40$, BF16 → $2 \cdot 8 \cdot 128 \cdot 40 \cdot 2 = 163.840$ bytes ≈ 160 KB/token.&lt;/p>
&lt;p>14 GB / 160 KB ≈ &lt;strong>87.500 tokens&lt;/strong> de contexto total. Con 8 usuarios en paralelo y 4.096 tokens de contexto cada uno: 32.768 tokens ocupados de 87.500. Hay margen, pero es finito.&lt;/p>
&lt;p>Pasando a FP8 (1 byte en vez de 2):&lt;/p>
&lt;p>$$\text{KV_FP8} = 80 \text{ KB/token} \implies 14 \text{ GB} / 80 \text{ KB} = 175.000 \text{ tokens}$$&lt;/p>
&lt;p>El doble de capacidad de contexto con la misma VRAM. Eso permite o bien más concurrencia, o bien contextos más largos.&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">vllm serve mi-modelo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales &lt;span class="c1"># calibra las escalas dinámicamente; sin esto hay degradación&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Advertencia para RTX 4090 y L40:&lt;/strong> Ada Lovelace tiene instrucciones FP8 a nivel CUDA pero sin el hardware de scaling dedicado de Hopper (H100). La reducción de memoria es real; la aceleración de cómputo es menor que en H100. No esperes el mismo speedup que en un datacenter Hopper. En L40S (la variante con tensor cores FP8 optimizados) el beneficio es mayor que en RTX 4090.&lt;/p>
&lt;hr>
&lt;h3 id="4-presupuesto-de-contexto----max-model-len">4. Presupuesto de contexto — &lt;code>--max-model-len&lt;/code>&lt;/h3>
&lt;p>&lt;code>--max-model-len&lt;/code> define el máximo de tokens que vLLM puede manejar en un único request (prompt + generación). Es el límite duro que determina cuánta VRAM se reserva para el KV cache en el peor caso.&lt;/p>
&lt;p>En hardware pequeño, reducirlo libera VRAM para más concurrencia:&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"># Modelo 7B en RTX 4090, contexto típico de 4K pero el modelo soporta 128K&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve mi-modelo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-model-len &lt;span class="m">8192&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># en vez de 131072&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --gpu-memory-utilization 0.92
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Con contexto recortado a 8.192 tokens, vLLM no reserva KV cache para 131.072 tokens potenciales y puede meter más requests simultáneas. El riesgo es obvio: requests que superen 8.192 tokens fallan con un error. Ajústalo al P99 de tu distribución real de longitudes.&lt;/p>
&lt;hr>
&lt;h2 id="interacción-entre-parámetros">Interacción entre parámetros&lt;/h2>
&lt;p>Los cuatro parámetros no son independientes. Un error común es activar prefix caching sin ajustar el tamaño de bloque, o subir &lt;code>max-num-batched-tokens&lt;/code> sin revisar que &lt;code>max-num-seqs&lt;/code> permita llenarlo:&lt;/p>
&lt;pre tabindex="0">&lt;code>max-num-batched-tokens = 8192
max-num-seqs = 4
prompt medio = 512 tokens → 4 × 512 = 2048 tokens de prefill &amp;lt; 8192
El presupuesto de 8192 nunca se llena porque max-num-seqs limita antes.
Solución: subir max-num-seqs o bajar max-num-batched-tokens.
&lt;/code>&lt;/pre>&lt;p>Configuración equilibrada para RTX 4090 + modelo 7B:&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">vllm serve Qwen/Qwen2.5-7B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.92 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-model-len &lt;span class="m">8192&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-chunked-prefill &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-batched-tokens &lt;span class="m">8192&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">64&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-prefix-caching &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Configuración para L40 (48 GB) + modelo 14B:&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">vllm serve Qwen/Qwen2.5-14B-Instruct-AWQ &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.90 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-model-len &lt;span class="m">16384&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-chunked-prefill &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-batched-tokens &lt;span class="m">16384&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">128&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-prefix-caching &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="cómo-medir-que-está-funcionando">Cómo medir que está funcionando&lt;/h2>
&lt;p>Las métricas que confirman que el prefill está optimizado:&lt;/p>
&lt;pre tabindex="0">&lt;code># En las métricas Prometheus de vLLM (puerto 8000/metrics):
vllm:time_to_first_token_seconds_bucket → distribución de TTFT
vllm:gpu_cache_usage_perc → utilización de KV cache
vllm:prefix_cache_hit_rate → hit rate de APC (si está activo)
vllm:num_running_seqs → requests en vuelo simultáneos
&lt;/code>&lt;/pre>&lt;p>Un &lt;code>prefix_cache_hit_rate&lt;/code> por debajo del 30% en workloads con system prompt fijo indica que algo en el hash no está funcionando (system prompt que varía por timestamp, formato de fecha en el prompt, etc.).&lt;/p>
&lt;hr>
&lt;h2 id="implicaciones-para-inferencia-on-premise">Implicaciones para inferencia on-premise&lt;/h2>
&lt;p>Chunked prefill y prefix caching son &lt;strong>cero coste&lt;/strong>: se activan con flags, no requieren hardware adicional. FP8 KV cache requiere que el modelo sea compatible (casi todos los transformers modernos lo son) y que estés en Ada Lovelace o superior.&lt;/p>
&lt;p>Para despliegues soberanos ENS donde el hardware es fijo y no puedes escalar horizontalmente a demanda, el prefill bien configurado es la diferencia entre necesitar 4 nodos y necesitar 2 para la misma carga.&lt;/p>
&lt;p>El segundo artículo de esta serie cubre las optimizaciones del &lt;strong>decode&lt;/strong>: speculative decoding, tuning del KV cache para maximizar concurrencia y cómo configurar &lt;code>gpu-memory-utilization&lt;/code> sin que vLLM se quede sin VRAM a medianoche.&lt;/p>
&lt;hr>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>https://blog.lo0.es/posts/kv-cache-fundamentos/ — cómo funciona el KV cache por dentro&lt;/li>
&lt;li>https://blog.lo0.es/posts/flashattention-fundamentos/ — por qué FlashAttention cambia el consumo de memoria en prefill&lt;/li>
&lt;li>https://blog.lo0.es/posts/continuous-batching-fundamentos/ — la base sobre la que chunked prefill construye&lt;/li>
&lt;li>https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/ — cuando escalar un solo nodo ya no basta&lt;/li>
&lt;li>https://blog.lo0.es/posts/vllm-otel-instrumentacion-optimizaciones/ — cómo medir con Prometheus y OTel si chunked prefill y prefix caching están funcionando: &lt;code>ttft p99&lt;/code>, &lt;code>gpu_prefix_cache_hit_rate&lt;/code> y las alertas concretas&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://arxiv.org/abs/2309.06180">Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/a> — Kwon et al., 2023&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2403.02310">Sarathi-Serve: Chunked Prefill and Stall-Free Scheduling&lt;/a> — Agrawal et al., 2024&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/en/stable/configuration/optimization/">vLLM Optimization and Tuning — documentación oficial&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/">Quantized KV Cache en vLLM&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Instrumentar vLLM con OTel: medir lo que las optimizaciones realmente hacen</title><link>https://blog.lo0.es/posts/vllm-otel-instrumentacion-optimizaciones/</link><pubDate>Thu, 04 Jun 2026 20:00:00 +0000</pubDate><guid>https://blog.lo0.es/posts/vllm-otel-instrumentacion-optimizaciones/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>vLLM expone dos señales de observabilidad independientes: métricas Prometheus (pull, agregadas) y trazas OTel (push, por request). Para medir si chunked prefill, prefix caching, speculative decoding, KV cache FP8 y la concurrencia realmente están funcionando, necesitas ambas. Las métricas dicen qué está pasando en el sistema; las trazas dicen por qué un request concreto fue lento. Este artículo configura el pipeline completo y mapea cada optimización a su métrica diagnóstica.&lt;/p>
&lt;hr>
&lt;h2 id="la-analogía">La analogía&lt;/h2>
&lt;p>Un piloto de Fórmula 1 y sus ingenieros de telemetría. El piloto siente que el coche &amp;ldquo;va raro&amp;rdquo; en la curva 3, pero sin los datos de los sensores no sabe si es el neumático trasero, el diferencial o el combustible. Los ingenieros ven exactamente qué pasó en ese giro, temperatura por sensor, carga lateral por milisegundo.&lt;/p>
&lt;p>vLLM sin OTel es el piloto solo: notas que el TTFT &amp;ldquo;parece alto&amp;rdquo; pero no sabes si es el prefill largo, un miss de prefix cache, o una preemption de KV cache. Con OTel tienes el cuadro completo: las métricas son el resumen de carrera (aggregated), las trazas son la telemetría vuelta a vuelta (per-request).&lt;/p>
&lt;hr>
&lt;h2 id="arquitectura-de-las-dos-señales">Arquitectura de las dos señales&lt;/h2>
&lt;p>vLLM separa intencionalmente sus dos canales de observabilidad:&lt;/p>
&lt;pre tabindex="0">&lt;code> ┌─────────────────────────────┐
│ vLLM │
│ │
requests ────────────►│ SchedulerStats │
│ │ │
│ ├─► Prometheus /metrics │◄── scrape (pull)
│ │ (agregado, ~15s) │
│ │ │
│ └─► OTLP exporter │──► push (por request)
│ (spans, inmediato) │
└─────────────────────────────┘
│ OTLP gRPC/HTTP
▼
┌─────────────────────┐
│ OTel Collector │
│ │
│ receivers: │
│ otlp (traces) │
│ prometheus │
│ exporters: │
│ langfuse │
│ prometheus remote│
│ loki (logs) │
└─────────────────────┘
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Prometheus pull&lt;/strong> expone métricas con prefijo &lt;code>vllm:&lt;/code> en &lt;code>:8000/metrics&lt;/code>. Son histogramas, gauges y contadores actualizados cada iteración del scheduler. Buenos para dashboards y alertas sobre el sistema completo.&lt;/p>
&lt;p>&lt;strong>OTLP push&lt;/strong> envía un span por cada request, inmediatamente al completarse. Contiene atributos del request concreto: tokens de prompt, tokens generados, TTFT, modelo. Bueno para debuggear requests anómalos y para Langfuse.&lt;/p>
&lt;hr>
&lt;h2 id="instalación-y-configuración-básica">Instalación y configuración básica&lt;/h2>
&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"># vLLM con soporte OTel&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install &lt;span class="s2">&amp;#34;vllm[otel]&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Instala: opentelemetry-sdk, opentelemetry-api,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># opentelemetry-exporter-otlp, opentelemetry-semantic-conventions-ai&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Arrancar vLLM con OTel habilitado&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OTEL_SERVICE_NAME&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;vllm-produccion&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OTEL_EXPORTER_OTLP_TRACES_ENDPOINT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://otel-collector:4317&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OTEL_EXPORTER_OTLP_TRACES_PROTOCOL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;grpc&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OTEL_EXPORTER_OTLP_TRACES_INSECURE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span> &lt;span class="c1"># en red interna sin TLS&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">vllm serve Qwen/Qwen2.5-7B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --otlp-traces-endpoint http://otel-collector:4317 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-chunked-prefill &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-prefix-caching &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-model Qwen/Qwen2.5-0.5B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-speculative-tokens &lt;span class="m">5&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Las métricas Prometheus no requieren configuración extra: siempre están en &lt;code>:8000/metrics&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="otel-collector-configuración-mínima">OTel Collector: configuración mínima&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># otel-collector-config.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">receivers&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">otlp&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">protocols&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">grpc&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">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.0.0.0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http&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">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.0.0.0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="m">4318&lt;/span>&lt;span 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">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">config&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">scrape_configs&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">job_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">scrape_interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">15s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static_configs&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">targets&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;vllm:8000&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">processors&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">batch&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">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resource&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">attributes&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">deployment.environment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;produccion&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">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">upsert&lt;/span>&lt;span 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">exporters&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">otlphttp/langfuse&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">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://cloud.langfuse.com/api/public/otel&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">headers&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">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Basic &amp;lt;base64(pk:sk)&amp;gt;&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prometheusremotewrite&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">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://prometheus:9090/api/v1/write&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">service&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">pipelines&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">traces&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">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlp]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">batch, resource]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlphttp/langfuse]&lt;/span>&lt;span class="w">
&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">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&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">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">batch]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">prometheusremotewrite]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="las-cinco-métricas-que-importan">Las cinco métricas que importan&lt;/h2>
&lt;p>Cada optimización tiene una señal diagnóstica primaria. Si la métrica no se mueve como se espera después de activar el flag, hay un problema de configuración o de carga.&lt;/p>
&lt;h3 id="1-chunked-prefill--vllmtime_to_first_token_seconds">1. Chunked prefill → &lt;code>vllm:time_to_first_token_seconds&lt;/code>&lt;/h3>
&lt;p>Chunked prefill debería reducir la varianza del TTFT, no necesariamente la mediana. Su objetivo principal es que los percentiles altos (p99) bajen aunque el p50 suba ligeramente.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># TTFT p50 y p99 — esperar que p99 baje con chunked prefill activo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kr">histogram_quantile&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="mf">0.50&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">time_to_first_token_seconds_bucket&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kr">histogram_quantile&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="mf">0.99&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">time_to_first_token_seconds_bucket&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Señal de que funciona:&lt;/strong> ratio p99/p50 se acerca a 1. Sin chunked prefill, un prefill largo de un request bloquea todos los demás y el p99 sube desproporcionadamente.&lt;/p>
&lt;p>&lt;strong>Señal de problema:&lt;/strong> p50 y p99 ambos suben. &lt;code>--max-num-batched-tokens&lt;/code> demasiado bajo hace que los chunks sean tan pequeños que el prefill tarda muchos pasos en completarse aunque las demás requests no se bloqueen. Subir el budget.&lt;/p>
&lt;p>También útil observar en las trazas OTel el atributo &lt;code>llm.usage.prompt_tokens&lt;/code> por span: los requests con muchos tokens de prompt deberían tener TTFT proporcional, no bloqueante.&lt;/p>
&lt;hr>
&lt;h3 id="2-prefix-caching--vllmgpu_prefix_cache_hit_rate">2. Prefix caching → &lt;code>vllm:gpu_prefix_cache_hit_rate&lt;/code>&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Hit rate del prefix cache en GPU (0.0–1.0)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">gpu_prefix_cache_hit_rate&lt;/span>&lt;span 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="c1"># Evolución en ventana de 5 minutos&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">gpu_prefix_cache_hit_rate&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Señal de que funciona:&lt;/strong> hit rate sostenido &amp;gt; 0.5 en workloads con system prompt compartido. Con hit rate = 0.8, el 80% de los requests omite el prefill del prefijo; el TTFT de esos requests cae al coste del sufijo variable únicamente.&lt;/p>
&lt;p>&lt;strong>Señal de problema: hit rate cercano a cero&lt;/strong> pese a system prompts que &amp;ldquo;parecen&amp;rdquo; iguales. Causas habituales:&lt;/p>
&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="c1"># ❌ Esto rompe el hash de prefix caching:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">system_prompt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Hoy es &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">datetime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">. Eres un asistente...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ^^^ timestamp diferente en cada request&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ✅ El system prompt debe ser idéntico byte a byte:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">system_prompt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Eres un asistente especializado en infraestructura...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Cualquier variación en el system prompt —timestamps, IDs de sesión, versiones de prompt interpoladas— produce un hash distinto y un miss de caché. Las trazas OTel no exponen directamente el hit/miss por request en la implementación actual; úsalas para correlacionar &lt;code>llm.usage.prompt_tokens&lt;/code> alto con TTFT alto en el mismo request.&lt;/p>
&lt;hr>
&lt;h3 id="3-speculative-decoding--vllmspec_decode_draft_acceptance_rate">3. Speculative decoding → &lt;code>vllm:spec_decode_draft_acceptance_rate&lt;/code>&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Acceptance rate del draft model (0.0–1.0)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">spec_decode_draft_acceptance_rate&lt;/span>&lt;span 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="c1"># Speedup efectivo estimado (con k=5 tokens propuestos)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># speedup ≈ (1 + α·k) / (1 + overhead_draft)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Simplificado: si α=0.75 y k=5 → speedup ≈ 1 + 0.75×5×(1 - cost_ratio) &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Señal de que funciona:&lt;/strong> acceptance rate &amp;gt; 0.70 sostenido. Por debajo de 0.60, el overhead del draft model supera la ganancia de los tokens aceptados y el speculative decoding es contraproducente.&lt;/p>
&lt;p>&lt;strong>Señal de problema:&lt;/strong> acceptance rate &amp;lt; 0.50. Causas habituales:&lt;/p>
&lt;ul>
&lt;li>Drafter de familia distinta al verifier (p.ej., Mistral 0.5B como draft de Qwen 7B).&lt;/li>
&lt;li>Temperatura de generación alta (&amp;gt;0.9): a mayor temperatura, más diverge la distribución del draft de la del verifier.&lt;/li>
&lt;li>Batch muy grande: a alta concurrencia, el draft puede quedar fuera del dominio de los requests actuales.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Alerta: speculative decoding ineficiente&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">ALERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">SpecDecodeIneficiente&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">IF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">spec_decode_draft_acceptance_rate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mf">0.60&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">LABELS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nl">severity&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">warning&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="w"> &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="nv">ANNOTATIONS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nl">summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">Draft acceptance rate bajo: desactivar spec decode o cambiar drafter&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>En las trazas OTel, el span completo del request incluye el tiempo total de decode. Sin acceptance rate por span, la forma de detectar spec decode funcionando es comparar el tiempo total de decode dividido por los tokens generados: si es significativamente menor que el baseline sin spec decode, está ayudando.&lt;/p>
&lt;hr>
&lt;h3 id="4-kv-cache-fp8-y-concurrencia--vllmgpu_cache_usage_perc--vllmnum_preemptions_total">4. KV cache FP8 y concurrencia → &lt;code>vllm:gpu_cache_usage_perc&lt;/code> + &lt;code>vllm:num_preemptions_total&lt;/code>&lt;/h3>
&lt;p>Estas dos métricas son las dos caras de la gestión del KV cache:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Utilización del KV cache (0.0–1.0)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Con FP8 activo, el mismo hardware soporta más requests antes de saturar&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">gpu_cache_usage_perc&lt;/span>&lt;span 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="c1"># Preemptions acumuladas (contador)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Sube cuando vLLM no puede alojar más requests y pausa alguna&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">num_preemptions_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Señal de que FP8 funciona:&lt;/strong> con &lt;code>--kv-cache-dtype fp8&lt;/code> activo, &lt;code>gpu_cache_usage_perc&lt;/code> debería saturar a niveles de concurrencia ~2× superiores respecto al baseline BF16 antes de que &lt;code>num_preemptions_total&lt;/code> empiece a crecer.&lt;/p>
&lt;p>&lt;strong>Señal de problema:&lt;/strong> &lt;code>num_preemptions_total&lt;/code> crece en tasas &amp;gt; 1/minuto con &lt;code>gpu_cache_usage_perc&lt;/code> por debajo de 0.90. Indica que &lt;code>max-num-seqs&lt;/code> está demasiado alto para el KV cache disponible: las requests entran al sistema pero no hay bloques libres para asignarles. Bajar &lt;code>max-num-seqs&lt;/code> o reducir &lt;code>max-model-len&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Alerta: KV cache saturado con preemptions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">ALERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">KVCacheSaturado&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">IF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">num_preemptions_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">2m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mf">0.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="nv">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">gpu_cache_usage_perc&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mf">0.85&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">3m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">LABELS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nl">severity&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">critical&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="w"> &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="nv">ANNOTATIONS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nl">summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">KV cache saturado: bajar max-num-seqs o max-model-len&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El impacto del FP8 en la capacidad se puede cuantificar:&lt;/p>
&lt;p>$$\Delta\text{capacity} = \frac{\text{tokens_max_FP8}}{\text{tokens_max_BF16}} \approx 2\times$$&lt;/p>
&lt;p>Medir antes y después de activar &lt;code>--kv-cache-dtype fp8&lt;/code>: el nivel de &lt;code>gpu_cache_usage_perc&lt;/code> para una concurrencia dada debería caer a la mitad.&lt;/p>
&lt;hr>
&lt;h3 id="5-concurrencia-efectiva--vllmnum_running_seqs--vllmnum_waiting_seqs">5. Concurrencia efectiva → &lt;code>vllm:num_running_seqs&lt;/code> + &lt;code>vllm:num_waiting_seqs&lt;/code>&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Requests activos en el motor (decode + prefill en curso)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">num_running_seqs&lt;/span>&lt;span 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="c1"># Requests en cola esperando slot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">num_waiting_seqs&lt;/span>&lt;span 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="c1"># Ratio de espera: si &amp;gt; 0.2 sostenido, hay cuello de concurrencia&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">num_waiting_seqs&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">num_running_seqs&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">num_waiting_seqs&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Señal saludable:&lt;/strong> &lt;code>num_running_seqs&lt;/code> estable cerca del valor de &lt;code>--max-num-seqs&lt;/code> configurado, &lt;code>num_waiting_seqs&lt;/code> bajo (&amp;lt; 10% de running).&lt;/p>
&lt;p>&lt;strong>Señal de problema:&lt;/strong> &lt;code>num_waiting_seqs&lt;/code> elevado con &lt;code>gpu_cache_usage_perc&lt;/code> bajo. Indica que el scheduler no está llenando los slots disponibles porque &lt;code>max-num-batched-tokens&lt;/code> es demasiado bajo: el budget de tokens por paso no permite procesar los prefills pendientes rápido enough. Subir &lt;code>max-num-batched-tokens&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="dashboard-de-referencia-las-5-métricas-en-grafana">Dashboard de referencia: las 5 métricas en Grafana&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;panels&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;TTFT p50 / p99 (chunked prefill)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;expr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;histogram_quantile(0.50, rate(vllm:time_to_first_token_seconds_bucket[5m]))&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;legendFormat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;p50&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;expr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;histogram_quantile(0.99, rate(vllm:time_to_first_token_seconds_bucket[5m]))&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;legendFormat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;p99&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Prefix cache hit rate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;expr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vllm:gpu_prefix_cache_hit_rate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;legendFormat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;GPU hit rate&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Spec decode acceptance rate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;expr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vllm:spec_decode_draft_acceptance_rate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;legendFormat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;acceptance rate&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;KV cache usage + preemptions&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;expr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vllm:gpu_cache_usage_perc&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;legendFormat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;cache uso&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;expr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;rate(vllm:num_preemptions_total[2m]) * 60&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;legendFormat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;preemptions/min&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Concurrencia efectiva&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;expr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vllm:num_running_seqs&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;legendFormat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;running&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;expr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vllm:num_waiting_seqs&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;legendFormat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;waiting&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="conectar-trazas-a-langfuse">Conectar trazas a Langfuse&lt;/h2>
&lt;p>Las trazas OTel de vLLM son spans GenAI semconv compatibles. Langfuse los acepta directamente via OTLP:&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"># En el OTel Collector (ya configurado arriba)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># El exporter otlphttp/langfuse envía trazas a Langfuse Cloud o self-hosted&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Para Langfuse self-hosted (ENS/soberano):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">exporters:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> otlphttp/langfuse:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> endpoint: &lt;span class="s2">&amp;#34;http://langfuse-interno:3000/api/public/otel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> headers:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Authorization: &lt;span class="s2">&amp;#34;Basic &amp;lt;base64(pk_xxx:sk_xxx)&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>En Langfuse, cada request de vLLM aparece como una traza con:&lt;/p>
&lt;ul>
&lt;li>&lt;code>gen_ai.system&lt;/code>: modelo servido&lt;/li>
&lt;li>&lt;code>gen_ai.usage.input_tokens&lt;/code>: tokens de prompt&lt;/li>
&lt;li>&lt;code>gen_ai.usage.output_tokens&lt;/code>: tokens generados&lt;/li>
&lt;li>Duración del span: latencia end-to-end&lt;/li>
&lt;/ul>
&lt;p>Lo que &lt;strong>no&lt;/strong> aparece directamente en el span: acceptance rate de speculative decoding, prefix cache hit/miss, ni número de preemptions. Esos datos sólo están en Prometheus. El workflow correcto es:&lt;/p>
&lt;ol>
&lt;li>Langfuse identifica un request anómalo por latencia.&lt;/li>
&lt;li>Prometheus/Grafana muestra si en ese intervalo hubo preemptions elevadas, spec decode bajo, o prefix cache miss.&lt;/li>
&lt;li>Se correlacionan por timestamp.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="matriz-de-diagnóstico-rápido">Matriz de diagnóstico rápido&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Síntoma observable&lt;/th>
&lt;th>Métrica Prometheus&lt;/th>
&lt;th>Causa probable&lt;/th>
&lt;th>Acción&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>TTFT p99 muy alto&lt;/td>
&lt;td>&lt;code>ttft p99/p50 &amp;gt;&amp;gt; 2&lt;/code>&lt;/td>
&lt;td>Prefills largos bloqueantes&lt;/td>
&lt;td>Subir &lt;code>--max-num-batched-tokens&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TTFT p50 alto, p99 idem&lt;/td>
&lt;td>&lt;code>ttft p50 &amp;gt; 500ms&lt;/code>&lt;/td>
&lt;td>Prefix cache no funciona&lt;/td>
&lt;td>Verificar hash del system prompt&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Decode lento sin mejora&lt;/td>
&lt;td>&lt;code>spec_decode_acceptance &amp;lt; 0.60&lt;/code>&lt;/td>
&lt;td>Drafter incompatible&lt;/td>
&lt;td>Cambiar drafter o desactivar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OOM / crash esporádico&lt;/td>
&lt;td>&lt;code>gpu_cache_usage_perc = 1.0&lt;/code> + preemptions&lt;/td>
&lt;td>KV cache lleno&lt;/td>
&lt;td>Bajar &lt;code>max-num-seqs&lt;/code> o activar FP8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cola alta con cache libre&lt;/td>
&lt;td>&lt;code>waiting &amp;gt;&amp;gt; 0&lt;/code> + &lt;code>cache &amp;lt; 0.70&lt;/code>&lt;/td>
&lt;td>Budget de tokens bajo&lt;/td>
&lt;td>Subir &lt;code>--max-num-batched-tokens&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="implicaciones-para-inferencia-on-premise-soberana">Implicaciones para inferencia on-premise soberana&lt;/h2>
&lt;p>En un despliegue ENS donde no puedes usar Langfuse Cloud ni DataDog, el stack self-hosted completo es:&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="c"># docker-compose.yml (o manifests K8s equivalentes)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&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">otel-collector&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel/opentelemetry-collector-contrib: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">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">./otel-config.yaml:/etc/otel/config.yaml]&lt;/span>&lt;span 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">langfuse&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse/langfuse: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">environment&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">DATABASE_URL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres://...&lt;/span>&lt;span 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">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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">prom/prometheus: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>&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grafana/grafana:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Todo el pipeline corre on-premise. Las trazas nunca salen del perímetro. El cumplimiento ENS no depende de qué observabilidad eliges: depende de que los datos de inferencia no salgan a terceros. Con stack local, ambas condiciones se cumplen.&lt;/p>
&lt;hr>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>https://blog.lo0.es/posts/tracing-llm-otel-genai/ — los fundamentos de OTel GenAI semconv: qué son los spans, los atributos estándar y cómo fluyen desde el SDK al collector&lt;/li>
&lt;li>https://blog.lo0.es/posts/prefill-optimizaciones-vllm/ — las optimizaciones de prefill que este artículo instrumenta: chunked prefill, prefix caching, FP8 KV&lt;/li>
&lt;li>https://blog.lo0.es/posts/decode-optimizaciones-vllm/ — las optimizaciones de decode: speculative decoding, gpu-memory-utilization, max-num-seqs&lt;/li>
&lt;li>https://blog.lo0.es/posts/anatomia-metricas-dcgm-vllm-anomalias/ — DCGM para las métricas de GPU debajo de vLLM: SM utilization, memory bandwidth, temperatura; la capa de hardware bajo las métricas de aplicación&lt;/li>
&lt;li>https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/ — cómo correlacionar métricas de GPU (DCGM) con métricas de aplicación (vLLM Prometheus) para diagnóstico completo&lt;/li>
&lt;li>https://blog.lo0.es/posts/continuous-batching-fundamentos/ — el scheduler que produce las métricas de num_running_seqs y num_waiting_seqs; sin entender el scheduler, las métricas de concurrencia no tienen contexto&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.vllm.ai/en/stable/examples/online_serving/opentelemetry/">vLLM OpenTelemetry setup — documentación oficial&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/en/stable/design/metrics/">vLLM Metrics — diseño y lista completa&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/integrations/model-providers/vllm">Tracing vLLM with Langfuse via OpenTelemetry&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.dash0.com/blog/observing-vllm-with-opentelemetry-and-dash0">Observing vLLM with OpenTelemetry and Dash0&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/blog/2024/llm-observability/">OpenTelemetry GenAI Semantic Conventions&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Knowledge Distillation: enseñar a un modelo pequeño a pensar como uno grande</title><link>https://blog.lo0.es/posts/knowledge-distillation-fundamentos/</link><pubDate>Thu, 04 Jun 2026 19:00:00 +0000</pubDate><guid>https://blog.lo0.es/posts/knowledge-distillation-fundamentos/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Knowledge Distillation es la técnica de entrenar un modelo pequeño (&lt;em>student&lt;/em>) usando como supervisión las probabilidades de salida de un modelo grande (&lt;em>teacher&lt;/em>), en vez de usando sólo las etiquetas duras del dataset de entrenamiento. El resultado es un modelo pequeño que razona mejor de lo que sugiere su tamaño, porque aprende las distribuciones de incertidumbre del teacher en vez de memorizar respuestas binarias. Es la razón por la que Phi-4 (14B) supera en razonamiento a la mayoría de modelos de 70B, y por la que los modelos de la familia Gemma 3 son sorprendentemente capaces para su tamaño. No es una técnica de compresión de modelo existente: es un proceso de entrenamiento que produce un modelo más pequeño desde cero o desde un punto de partida diferente.&lt;/p>
&lt;hr>
&lt;h2 id="la-analogía">La analogía&lt;/h2>
&lt;p>Un maestro cirujano con treinta años de experiencia y un residente de primer año. Si el residente sólo aprende del manual de anatomía —respuestas correctas binarias: &amp;ldquo;aquí se corta, aquí no&amp;rdquo;— tardará años en desarrollar el juicio clínico del maestro. Pero si opera a su lado, observando sus microdecisiones, sus dudas, los casos ambiguos donde el maestro sabe que dos opciones son casi igualmente válidas, aprende algo que el manual no puede enseñar: la estructura de la incertidumbre.&lt;/p>
&lt;p>Knowledge distillation es exactamente eso. El &amp;ldquo;manual de anatomía&amp;rdquo; son las etiquetas duras (la respuesta correcta). El &amp;ldquo;maestro cirujano&amp;rdquo; es el teacher LLM. Las distribuciones de probabilidad sobre el vocabulario son la materialización de esa incertidumbre que el student absorbe.&lt;/p>
&lt;hr>
&lt;h2 id="qué-es-realmente">Qué es realmente&lt;/h2>
&lt;p>Cuando un LLM genera texto, no produce una sola palabra: produce una distribución de probabilidad sobre todo su vocabulario en cada posición. Para el token siguiente, el modelo podría decir:&lt;/p>
&lt;pre tabindex="0">&lt;code>&amp;#34;París&amp;#34;: 42%
&amp;#34;Lyon&amp;#34;: 8%
&amp;#34;Marsella&amp;#34;: 6%
&amp;#34;la ciudad&amp;#34;: 5%
...resto del vocabulario: 39%
&lt;/code>&lt;/pre>&lt;p>Esta distribución es información densa. Dice no sólo &lt;em>qué&lt;/em> es la respuesta correcta, sino también &lt;em>qué otras respuestas eran plausibles y en qué medida&lt;/em>. Un student entrenado sólo con la etiqueta &amp;ldquo;París&amp;rdquo; (probabilidad 1.0 al token correcto, 0.0 al resto) no ve esta riqueza.&lt;/p>
&lt;p>Destilación usa la distribución completa del teacher como objetivo de entrenamiento del student. La función de pérdida tiene dos términos:&lt;/p>
&lt;p>$$\mathcal{L}&lt;em>{total} = (1 - \alpha) \cdot \mathcal{L}&lt;/em>{CE}(y, \hat{y}&lt;em>S) + \alpha \cdot \mathcal{L}&lt;/em>{KD}(p_T, p_S, T)$$&lt;/p>
&lt;p>Donde:&lt;/p>
&lt;ul>
&lt;li>$\mathcal{L}_{CE}$ es la cross-entropy estándar con las etiquetas duras (supervisión clásica).&lt;/li>
&lt;li>$\mathcal{L}_{KD}$ es la KL-divergencia entre las distribuciones del teacher y el student.&lt;/li>
&lt;li>$\alpha$ controla el peso relativo de cada término (típicamente 0.5–0.9 a favor de KD).&lt;/li>
&lt;li>$T$ es la &lt;em>temperatura&lt;/em>, un parámetro que suaviza las distribuciones para hacer la señal de KD más informativa.&lt;/li>
&lt;/ul>
&lt;h3 id="el-papel-de-la-temperatura">El papel de la temperatura&lt;/h3>
&lt;p>Si el teacher asigna 99% a &amp;ldquo;París&amp;rdquo; y 0.001% a cada otra palabra, la distribución es casi tan informativa como una etiqueta dura. La temperatura $T &amp;gt; 1$ suaviza esa distribución:&lt;/p>
&lt;p>$$p_T(k) = \frac{\exp(z_k / T)}{\sum_j \exp(z_j / T)}$$&lt;/p>
&lt;p>Con $T = 4$ y los logits originales, la distribución que antes era [99%, 0.001%, 0.001%&amp;hellip;] pasa a ser algo como [42%, 8%, 6%&amp;hellip;]. El student ve el vecindario de probabilidad real del teacher, no sólo su respuesta puntual.&lt;/p>
&lt;p>&lt;strong>Ejemplo numérico con temperatura:&lt;/strong>&lt;/p>
&lt;p>Logits del teacher para &amp;ldquo;La capital de Francia es _____&amp;rdquo;:&lt;/p>
&lt;pre tabindex="0">&lt;code>París: 8.5
Lyon: 3.2
Europa: 2.1
una: 1.8
&lt;/code>&lt;/pre>&lt;p>Con T=1 (softmax estándar):
$$p(\text{París}) = \frac{e^{8.5}}{e^{8.5} + e^{3.2} + e^{2.1} + e^{1.8}} \approx 99.3%$$&lt;/p>
&lt;p>Con T=4:
$$p(\text{París}) = \frac{e^{8.5/4}}{e^{8.5/4} + e^{3.2/4} + e^{2.1/4} + e^{1.8/4}} = \frac{e^{2.125}}{e^{2.125} + e^{0.8} + e^{0.525} + e^{0.45}} \approx 54%$$&lt;/p>
&lt;p>La señal con T=4 es mucho más informativa para el student: aprende que Lyon es más plausible que Europa, que Europa es más plausible que &amp;ldquo;una&amp;rdquo;, etcétera.&lt;/p>
&lt;hr>
&lt;h2 id="los-tres-modos-de-destilación">Los tres modos de destilación&lt;/h2>
&lt;h3 id="offline-o-black-box">Offline (o &amp;ldquo;black-box&amp;rdquo;)&lt;/h3>
&lt;p>El teacher genera un dataset sintético de respuestas antes del entrenamiento. El student se entrena sobre ese dataset como si fuera etiquetas duras normales.&lt;/p>
&lt;pre tabindex="0">&lt;code>teacher → genera 100M pares (prompt, completion) → dataset
student → se entrena sobre ese dataset
&lt;/code>&lt;/pre>&lt;p>Es la forma más barata de escalar: el teacher se ejecuta una sola vez, el student se entrena sobre los datos generados con hardware convencional. La mayoría de los modelos de instrucción open source (Alpaca, Vicuna, WizardLM en sus primeras versiones) usaron esta estrategia: GPT-4 como teacher, datos guardados, Llama-7B como student.&lt;/p>
&lt;p>&lt;strong>Limitación:&lt;/strong> el student no ve las distribuciones de probabilidad del teacher, sólo sus respuestas. Es destilación de &amp;ldquo;comportamiento&amp;rdquo;, no de &amp;ldquo;conocimiento&amp;rdquo; en el sentido estricto. Si el teacher se equivoca (y GPT-4 se equivoca), el error queda cristalizado en el dataset.&lt;/p>
&lt;h3 id="online-o-white-box">Online (o &amp;ldquo;white-box&amp;rdquo;)&lt;/h3>
&lt;p>Teacher y student se ejecutan juntos durante el entrenamiento. El student procesa cada batch, el teacher procesa el mismo batch en paralelo, y la pérdida KD se calcula en tiempo real con las distribuciones de probabilidad completas.&lt;/p>
&lt;pre tabindex="0">&lt;code>for batch in dataset:
logits_teacher = teacher(batch) # forward pass del teacher
logits_student = student(batch) # forward pass del student
loss = KL(softmax(logits_teacher/T), softmax(logits_student/T))
loss.backward() # sólo actualiza student
&lt;/code>&lt;/pre>&lt;p>El teacher tiene los gradientes desactivados (&lt;code>torch.no_grad()&lt;/code>). La señal de aprendizaje es richer que en offline, pero el coste es alto: necesitas mantener el teacher en VRAM durante todo el entrenamiento. Para destilación de un teacher de 405B a un student de 8B, necesitarías varias H100 sólo para el teacher.&lt;/p>
&lt;h3 id="en-policy-on-policy">En-policy (on-policy)&lt;/h3>
&lt;p>Variante reciente (2024–2026) que combina lo mejor de ambos: el teacher genera respuestas dinámicamente durante el entrenamiento, pero el student las evalúa con su propia distribución. El ciclo es:&lt;/p>
&lt;ol>
&lt;li>Student genera una propuesta de respuesta (&lt;em>rollout&lt;/em>).&lt;/li>
&lt;li>Teacher puntúa esa propuesta con su distribución de probabilidad.&lt;/li>
&lt;li>El student actualiza con la señal del teacher.&lt;/li>
&lt;/ol>
&lt;p>Esto evita que el student aprenda de distribuciones fuera de su propio dominio (problema de &lt;em>distribution shift&lt;/em> en offline). Es la base de algoritmos como &lt;a href="https://github.com/nick7nlp/Awesome-LLM-On-Policy-Distillation">SimCT (2026)&lt;/a> que usan teachers de diferentes familias (Qwen, Phi, Gemma) para generar señal cross-tokenizer.&lt;/p>
&lt;hr>
&lt;h2 id="por-qué-los-mejores-modelos-pequeños-usan-destilación">Por qué los mejores modelos pequeños usan destilación&lt;/h2>
&lt;p>Phi-4 (Microsoft, 14B), Gemma 3 (Google, 9B/27B), y los modelos de la familia Qwen3 compactos son los ejemplos más claros. Sus benchmarks son anómalos respecto a su tamaño: Phi-4-14B supera a LLaMA-3-70B en MATH y GPQA-Diamond, dos benchmarks de razonamiento matemático y científico donde el tamaño suele ser determinante.&lt;/p>
&lt;p>¿Por qué? La clave está en qué supervisa el entrenamiento:&lt;/p>
&lt;ul>
&lt;li>Un modelo entrenado con datos de internet aprende la distribución de texto humano, que incluye mucho texto de baja calidad, errores, ambigüedades.&lt;/li>
&lt;li>Un student que aprende de un teacher frontier (GPT-4o, Claude 3 Opus, Gemini 1.5 Pro) absorbe una distribución filtrada hacia texto de alta calidad y razonamiento correcto.&lt;/li>
&lt;/ul>
&lt;p>El student con 14B parámetros no &amp;ldquo;sabe más&amp;rdquo; que uno sin destilación del mismo tamaño, pero ha aprendido a usarlos mejor porque sus gradientes de entrenamiento nunca estuvieron contaminados por texto de baja calidad.&lt;/p>
&lt;p>&lt;strong>Dato empírico:&lt;/strong> Phi-4 (14B destilado) vs LLaMA-3-70B (no destilado) en MATH benchmark (2025):&lt;/p>
&lt;ul>
&lt;li>Phi-4: 80.4%&lt;/li>
&lt;li>LLaMA-3-70B: 68.0%&lt;/li>
&lt;/ul>
&lt;p>Un modelo 5× más pequeño supera al grande porque la señal de entrenamiento es mejor, no porque tenga más parámetros.&lt;/p>
&lt;hr>
&lt;h2 id="destilación-de-razonamiento-el-caso-de-los-thinking-models">Destilación de razonamiento: el caso de los thinking models&lt;/h2>
&lt;p>Los modelos de razonamiento (DeepSeek-R1, Qwen3-thinking, QwQ) generan cadenas de pensamiento internas antes de dar la respuesta final. Destilar razonamiento es más complejo porque no sólo se quiere transferir la respuesta: se quiere transferir la &lt;em>forma de pensar&lt;/em>.&lt;/p>
&lt;p>La estrategia actual (2025–2026) es &lt;strong>destilación de trazas de razonamiento&lt;/strong>:&lt;/p>
&lt;ol>
&lt;li>El teacher (modelo thinking grande) genera respuestas con su cadena de pensamiento interna completa.&lt;/li>
&lt;li>El dataset incluye esas cadenas de pensamiento como parte del output.&lt;/li>
&lt;li>El student aprende a imitar tanto la cadena como la respuesta final.&lt;/li>
&lt;/ol>
&lt;p>Esto explica por qué Qwen3-7B-thinking puede razonar formalmente sobre matemáticas siendo 10× más pequeño que los modelos que lo precedieron sin destilación: aprendió el &lt;em>proceso&lt;/em>, no sólo el &lt;em>resultado&lt;/em>.&lt;/p>
&lt;hr>
&lt;h2 id="cuándo-usar-destilación-vs-las-alternativas">Cuándo usar destilación vs. las alternativas&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Técnica&lt;/th>
&lt;th>Qué hace&lt;/th>
&lt;th>Requiere reentrenamiento&lt;/th>
&lt;th>Resultado&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Quantización&lt;/td>
&lt;td>Reduce precisión de pesos&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Mismo modelo, más pequeño&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Poda&lt;/td>
&lt;td>Elimina pesos irrelevantes&lt;/td>
&lt;td>No (PTQ)&lt;/td>
&lt;td>Mismo modelo, más disperso&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Destilación&lt;/td>
&lt;td>Entrena modelo nuevo&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Modelo diferente, más pequeño&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La destilación no comprime un modelo existente: produce uno nuevo. Por eso es complementaria, no sustitutiva: puedes destilar un 405B a un 8B, y luego cuantizar ese 8B a INT4 para reducir su coste de inferencia.&lt;/p>
&lt;p>&lt;strong>Cuándo es la opción correcta:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Necesitas un modelo 5–10× más pequeño que el mejor disponible.&lt;/li>
&lt;li>Tienes acceso (API o local) a un teacher de calidad.&lt;/li>
&lt;li>Tienes datos de entrenamiento o capacidad de generarlos.&lt;/li>
&lt;li>La latencia o el coste de inferencia son un constraint duro.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Cuándo no:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Quieres comprimir un modelo existente rápidamente: usa cuantización + poda.&lt;/li>
&lt;li>No tienes presupuesto de entrenamiento (destilación online requiere semanas de GPU).&lt;/li>
&lt;li>El teacher no es significativamente mejor que el student base: la señal de KD será débil.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="implicaciones-para-inferencia-on-premise">Implicaciones para inferencia on-premise&lt;/h2>
&lt;p>En un despliegue soberano, el teacher puede ser un modelo grande que se ejecuta localmente (no es necesaria una API externa). El flujo es:&lt;/p>
&lt;pre tabindex="0">&lt;code>4×H100 genérico:
teacher: Llama-3.3-70B-Instruct (en los 4×H100, carga completa)
→ genera dataset de 10M pares (prompt, completion con logits)
→ 3-4 semanas de generación a batch 32
Después del dataset:
student: Qwen2.5-7B (fine-tuned con KD loss sobre el dataset)
→ 2-3 días de entrenamiento en los mismos H100
→ resultado: 7B que razona como el 70B en el dominio específico
Producción:
RTX 4090: sirve el student 7B cuantizado a INT4 (4 GB)
&lt;/code>&lt;/pre>&lt;p>El teacher sólo se necesita para generar los datos. El student es lo que va a producción. La inversión en cómputo de entrenamiento se amortiza en meses de inferencia más barata.&lt;/p>
&lt;p>Para ENS/NIS2: este flujo es 100% on-premise, cero dependencia de APIs externas, y el modelo resultante es tuyo en todos los sentidos.&lt;/p>
&lt;hr>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>https://blog.lo0.es/posts/poda-pruning-llm-fundamentos/ — alternativa técnica: en vez de entrenar un modelo nuevo, eliminar partes del modelo existente; destilación y poda son complementarias&lt;/li>
&lt;li>https://blog.lo0.es/posts/quantization-fundamentos-inferencia/ — el paso siguiente después de destilar: cuantizar el student para inferencia eficiente&lt;/li>
&lt;li>https://blog.lo0.es/posts/speculative-decoding-fundamentos/ — los drafters de speculative decoding son frecuentemente students destilados del model base que aprenden a predecir su distribución&lt;/li>
&lt;li>https://blog.lo0.es/posts/fine-tuning-continuo-produccion/ — destilación como forma de fine-tuning continuo: el teacher es el modelo en producción, el student es la siguiente versión&lt;/li>
&lt;li>https://blog.lo0.es/posts/alignment-moderno-dpo-kto-orpo-simpo/ — DPO y sus variantes pueden verse como destilación de preferencias humanas hacia el modelo; la matemática de la distribución de referencia es análoga al teacher en KD&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://arxiv.org/abs/1503.02531">Distilling the Knowledge in a Neural Network&lt;/a> — Hinton, Vinyals &amp;amp; Dean, 2015 (paper fundacional)&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2412.08905">Phi-4 Technical Report&lt;/a> — Microsoft Research, 2024&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2501.12948">DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning&lt;/a> — DeepSeek, 2025 (destilación de razonamiento)&lt;/li>
&lt;li>&lt;a href="https://github.com/nick7nlp/Awesome-LLM-On-Policy-Distillation">Awesome LLM On-Policy Distillation&lt;/a> — colección de papers de destilación en-policy, 2025–2026&lt;/li>
&lt;li>&lt;a href="https://openreview.net/pdf/cf5bed8b71779ae42d0e681f1e2a7de3b3c8f6ad.pdf">Knowledge Distillation for LLMs: Survey&lt;/a> — ICLR 2025&lt;/li>
&lt;/ul></description></item><item><title>Poda de modelos LLM: eliminar sin amputar</title><link>https://blog.lo0.es/posts/poda-pruning-llm-fundamentos/</link><pubDate>Thu, 04 Jun 2026 18:00:00 +0000</pubDate><guid>https://blog.lo0.es/posts/poda-pruning-llm-fundamentos/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Un modelo de 7B parámetros tiene decenas de miles de millones de conexiones neuronales. Muchas de ellas contribuyen tan poco que podrías eliminarlas sin que ningún benchmark razonable lo notase. Eso es la poda (&lt;em>pruning&lt;/em>): identificar los pesos irrelevantes y suprimirlos para obtener un modelo más pequeño, más rápido o que consuma menos memoria. Las técnicas modernas (SparseGPT, Wanda, 2:4 structured sparsity) hacen esto sin reentrenamiento, en pocas horas de GPU, y con menos de 1 punto de perplexity de penalización. No reemplaza a la cuantización, se combina con ella.&lt;/p>
&lt;hr>
&lt;h2 id="la-analogía">La analogía&lt;/h2>
&lt;p>Un árbol de roble con cien ramas. Cuando llega el invierno, el árbol poda sus ramas débiles: redirige los recursos hacia los troncos principales. Un podador experto no corta al azar, observa cuáles ramas tienen poco follaje, cuáles están secas, cuáles crecen en dirección equivocada, y corta sólo esas.&lt;/p>
&lt;p>Un modelo de lenguaje es ese árbol. Sus &amp;ldquo;ramas&amp;rdquo; son los pesos que conectan neuronas. Después del entrenamiento, muchas de esas conexiones son vestigios del proceso de optimización: existían para que el gradiente descendiera con suavidad, pero en producción apenas modifican la salida. El podador que las elimina con precisión es SparseGPT o Wanda. El que corta al azar es &lt;em>magnitude pruning&lt;/em> sin calibración. Ambos dan un árbol más pequeño; sólo el experto da uno que sigue produciendo el mismo fruto.&lt;/p>
&lt;hr>
&lt;h2 id="qué-es-la-poda-realmente">Qué es la poda realmente&lt;/h2>
&lt;p>Un modelo de lenguaje transformer almacena su conocimiento en matrices de pesos. Una capa de atención tiene cuatro matrices: $W_Q, W_K, W_V, W_O$. Una capa FFN tiene al menos dos ($W_{up}, W_{down}$, más $W_{gate}$ en SwiGLU). Para un modelo de 7B con 32 capas, el número de parámetros individuales supera los 7.000 millones.&lt;/p>
&lt;p>Poda es el proceso de fijar a cero un subconjunto de esos parámetros de forma que:&lt;/p>
&lt;ol>
&lt;li>El modelo resultante ocupe menos memoria (si se almacena en formato disperso) o compute menos operaciones.&lt;/li>
&lt;li>La calidad de las respuestas no caiga de forma apreciable.&lt;/li>
&lt;/ol>
&lt;p>Hay dos dimensiones de clasificación que importan:&lt;/p>
&lt;p>&lt;strong>Granularidad:&lt;/strong> qué unidad se elimina.&lt;/p>
&lt;ul>
&lt;li>&lt;em>Poda no estructurada&lt;/em>: pesos individuales, dispersos por toda la matriz. Alta compresión, difícil de acelerar en hardware convencional.&lt;/li>
&lt;li>&lt;em>Poda estructurada&lt;/em>: cabezas de atención completas, neuronas FFN enteras, o capas completas. Menor compresión, pero el modelo resultante es denso y compatible con cualquier hardware.&lt;/li>
&lt;li>&lt;em>Semi-estructurada N:M&lt;/em>: para cada grupo de M pesos consecutivos, exactamente N son cero. El caso 2:4 (2 zeros de cada 4) es el que soportan los Tensor Cores de NVIDIA Ampere y posteriores.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Momento:&lt;/strong> cuándo se elimina.&lt;/p>
&lt;ul>
&lt;li>&lt;em>Post-entrenamiento&lt;/em> (PTQ de pesos): no requiere gradient, es el estándar en LLMs grandes.&lt;/li>
&lt;li>&lt;em>Durante entrenamiento&lt;/em> (gradual/iterativa): más precisa, incompatible con modelos de 70B+ por coste.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="por-qué-existen-tantos-pesos-redundantes">Por qué existen tantos pesos redundantes&lt;/h2>
&lt;p>La respuesta está en cómo se entrenan los modelos. El descenso de gradiente estocástico con millones de pasos y learning rate decreciente produce redes &lt;em>sobre-parametrizadas por diseño&lt;/em>: los parámetros extra no representan conocimiento adicional, sino margen de maniobra para que la optimización converja más fácilmente.&lt;/p>
&lt;p>La &lt;strong>Hipótesis del Ticket de Lotería&lt;/strong> (Frankle &amp;amp; Carlin, ICLR 2019) formalizó esta intuición: dentro de cualquier red densa entrenada existe una subred que, entrenada desde cero en aislamiento, alcanza la misma calidad. La red original es esa subred envuelta en ruido paramétrico generado por el proceso de entrenamiento.&lt;/p>
&lt;p>Para LLMs, la evidencia empírica es consistente: modelos de 7B–70B toleran hasta el 50% de sparsidad no estructurada sin degradación observable en tareas conversacionales. En modelos más grandes, el umbral de tolerancia aumenta.&lt;/p>
&lt;hr>
&lt;h2 id="las-matemáticas-que-importan">Las matemáticas que importan&lt;/h2>
&lt;h3 id="qué-pesos-son-seguros-eliminar">¿Qué pesos son seguros eliminar?&lt;/h3>
&lt;h4 id="magnitude-pruning-el-criterio-ingenuo">Magnitude pruning: el criterio ingenuo&lt;/h4>
&lt;p>$$\text{importance}(w_{ij}) = |w_{ij}|$$&lt;/p>
&lt;p>Se eliminan los pesos con menor valor absoluto. Intuitivo, pero incompleto: un peso pequeño conectado a una activación muy grande sigue contribuyendo significativamente a la salida.&lt;/p>
&lt;h4 id="wanda-magnitud--activación">Wanda: magnitud × activación&lt;/h4>
&lt;p>$$\text{importance}(w_{ij}) = |w_{ij}| \cdot |x_j|_2$$&lt;/p>
&lt;p>Donde $x_j$ es el vector de activación de entrada correspondiente al peso $j$, calculado sobre un dataset de calibración de ~128 samples. El producto captura ambas dimensiones: un peso es seguro eliminar sólo si &lt;em>él&lt;/em> es pequeño &lt;em>y&lt;/em> su neurona de entrada está poco activa.&lt;/p>
&lt;p>&lt;strong>Ejemplo numérico:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Peso A: $|w| = 0.001$, $|x|_2 = 500$ → importancia = 0.5&lt;/li>
&lt;li>Peso B: $|w| = 0.01$, $|x|_2 = 10$ → importancia = 0.1&lt;/li>
&lt;/ul>
&lt;p>Magnitude pruning eliminaría A (valor absoluto menor). Wanda elimina B (importancia menor). B es más seguro suprimir.&lt;/p>
&lt;p>Wanda no requiere gradientes ni inversas de matriz hessiana. Corre en minutos sobre un modelo de 70B en una sola GPU. En benchmarks de perplexity WikiText-2 con 50% de sparsidad no estructurada, Wanda alcanza resultados comparables a SparseGPT con 10–100× menos coste computacional.&lt;/p>
&lt;h4 id="sparsegpt-compensación-hessiana">SparseGPT: compensación hessiana&lt;/h4>
&lt;p>SparseGPT aplica el mismo marco matemático que GPTQ (cuantización capa a capa), pero para poda. Cuando elimina un peso $w_p$, calcula una corrección $\delta w$ sobre los pesos restantes de la misma fila para minimizar el cambio en la salida de la capa:&lt;/p>
&lt;p>$$\min_{\delta w} |W x - (W + \delta W) x|_2^2 \quad \text{s.t.} \quad w_p + \delta w_p = 0$$&lt;/p>
&lt;p>La solución usa la inversa de la matriz Hessiana de segundo orden $H = X X^T$. El coste extra justifica la mayor precisión cuando la sparsidad objetivo es alta (&amp;gt;70%) o el modelo es pequeño (&amp;lt;7B, donde la redundancia es menor).&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Método&lt;/th>
&lt;th>Criterio&lt;/th>
&lt;th>Coste&lt;/th>
&lt;th>Sparsidad 50% (7B, ppl WikiText-2)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Magnitude&lt;/td>
&lt;td>|w|&lt;/td>
&lt;td>Instantáneo&lt;/td>
&lt;td>+2–5 puntos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Wanda&lt;/td>
&lt;td>|w| · |x|&lt;/td>
&lt;td>Minutos&lt;/td>
&lt;td>~+0.5 puntos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SparseGPT&lt;/td>
&lt;td>Hessiana&lt;/td>
&lt;td>1–4h GPU&lt;/td>
&lt;td>~+0.4 puntos&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="24-structured-sparsity-el-caso-especial-de-nvidia">2:4 Structured Sparsity: el caso especial de NVIDIA&lt;/h3>
&lt;p>NVIDIA Ampere (A100) y posteriores (H100, Ada Lovelace) incluyen hardware dedicado para el patrón 2:4: exactamente 2 de cada 4 pesos consecutivos son cero. Esto permite al hardware omitir las multiplicaciones por cero de forma eficiente, obteniendo hasta &lt;strong>2× speedup en matmul&lt;/strong> sobre modelos con pesos 2:4.&lt;/p>
&lt;p>La restricción es que la sparsidad tiene que ser exactamente 2:4, no un patrón arbitrario. Las herramientas NVIDIA (APEX Sparse, cuSPARSELt) y frameworks como PyTorch 2.x soportan esto nativamente:&lt;/p>
&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">torch.sparse&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">to_sparse_semi_structured&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SparseSemiStructuredTensor&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Convertir pesos densos a 2:4 sparse&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sparse_weight&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">to_sparse_semi_structured&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">dense_weight&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Forward pass automáticamente usa sparse tensor cores&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">output&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">F&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">linear&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">input&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sparse_weight&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Qué esperar en la práctica con 2:4:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>RTX 4090 (Ada Lovelace): soporta 2:4 sparse tensor cores para FP16/BF16. Speedup teórico 2×, real 1.3–1.6× dependiendo del tamaño de batch y secuencia.&lt;/li>
&lt;li>H100 (Hopper): ídem con mejoras adicionales en FP8 + 2:4 combinados.&lt;/li>
&lt;li>A100: soportado, sin FP8.&lt;/li>
&lt;li>GPUs consumer anteriores a Ada (3090, etc.): &lt;strong>sin soporte de hardware&lt;/strong>. 2:4 sparsity da un modelo más pequeño en disco pero no acelera la inferencia.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="poda-estructurada-eliminar-cabezas-y-capas-enteras">Poda estructurada: eliminar cabezas y capas enteras&lt;/h2>
&lt;h3 id="poda-de-cabezas-de-atención">Poda de cabezas de atención&lt;/h3>
&lt;p>Un transformer de 32 capas con 32 cabezas por capa tiene 1.024 cabezas de atención. Estudios sistemáticos en modelos Llama-2 y Qwen muestran que entre el 20–40% de las cabezas tienen una influencia marginal en la salida final: su salida puede fijarse a cero sin que el benchmark cambie dentro del margen de error.&lt;/p>
&lt;p>La métrica más usada es la &lt;em>Taylor importance&lt;/em>: el producto del gradiente de la pérdida respecto a la salida de la cabeza por el valor de esa salida, sumado sobre un dataset de calibración:&lt;/p>
&lt;p>$$\text{I}_{head} = \left| \sum_t \frac{\partial \mathcal{L}}{\partial o_t} \cdot o_t \right|$$&lt;/p>
&lt;p>Las cabezas con $I_{head}$ más bajo se eliminan primero. Después de eliminar el 25% de cabezas en Llama-3-8B, la degradación en MMLU es &amp;lt;1% y el tiempo de inferencia de la atención cae ~20% porque los matmuls de atención son más pequeños.&lt;/p>
&lt;h3 id="layer-dropping-el-atajo-más-agresivo">Layer dropping: el atajo más agresivo&lt;/h3>
&lt;p>Eliminar una capa transformer completa suprime su bloque de atención y su FFN. El criterio más robusto es la &lt;strong>Block Influence (BI)&lt;/strong>, introducida en ShortGPT (2024):&lt;/p>
&lt;p>$$\text{BI}(l) = 1 - \cos(\text{input}_l, \text{output}_l)$$&lt;/p>
&lt;p>Una capa cuya salida es casi idéntica a su entrada (coseno próximo a 1, BI próximo a 0) actúa como función identidad: eliminarla no cambia el flujo de información. Las capas del centro del transformer suelen tener BI más bajo que las capas iniciales y finales.&lt;/p>
&lt;p>&lt;strong>Ejemplo numérico en LLaMA-2-70B:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Capas 0–5 (early): BI &amp;gt; 0.3 → no eliminar&lt;/li>
&lt;li>Capas 20–45 (mid): BI &amp;lt; 0.05 → candidatas a eliminar&lt;/li>
&lt;li>Capas 76–80 (final): BI &amp;gt; 0.2 → no eliminar&lt;/li>
&lt;/ul>
&lt;p>Eliminando 8 capas de 80 (10%): el modelo pasa de ~140 GB a ~126 GB en BF16. Speedup de inferencia: ~10% (proporcional al número de capas eliminadas). Degradación en benchmarks de razonamiento: 1–3%.&lt;/p>
&lt;hr>
&lt;h2 id="implicaciones-para-inferencia-on-premise">Implicaciones para inferencia on-premise&lt;/h2>
&lt;p>La poda no estructurada (50% sparsidad) produce modelos con el mismo número de parámetros pero con la mitad a cero. Sin kernels sparse especializados, eso no da speedup: la GPU sigue ejecutando las multiplicaciones, sólo que multiplica por cero muy eficientemente. El beneficio real es de almacenamiento y transferencia (el modelo ocupa menos en disco y en RAM de sistema).&lt;/p>
&lt;p>Con 2:4 structured sparsity sobre hardware Ada/Hopper, el speedup es real pero moderado (1.3–1.7×) y requiere herramientas adicionales (cuSPARSELt o PyTorch sparse).&lt;/p>
&lt;p>La poda estructurada (cabezas, capas) sí acelera en cualquier hardware porque reduce el tamaño real del modelo. Es la opción correcta si el objetivo es throughput en hardware sin tensor cores sparse.&lt;/p>
&lt;p>&lt;strong>Combinación con cuantización:&lt;/strong> poda + cuantización son ortogonales. Un modelo 50% sparse a INT4 ocupa aproximadamente un octavo del original en FP32. Es el punto de llegada de muchos pipelines de compresión agresiva para edge inference.&lt;/p>
&lt;hr>
&lt;h2 id="aplicado-a-hardware-on-premise-genérico">Aplicado a hardware on-premise genérico&lt;/h2>
&lt;h3 id="rtx-4090-24-gb-ada-lovelace">RTX 4090 (24 GB, Ada Lovelace)&lt;/h3>
&lt;p>Soporta 2:4 sparse tensor cores para FP16/BF16. Con Wanda + 2:4 sparsity sobre un Qwen2.5-14B:&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"># Pipeline de poda: Wanda 2:4 + quantización INT4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. Ejecutar Wanda con calibración sobre 128 muestras&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python wanda/main.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model Qwen/Qwen2.5-14B &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sparsity_ratio 0.5 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sparsity_type 2:4 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --save pruned_model/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. Cuantizar el modelo podado (opcional pero complementario)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python -m awq.entry --model_path pruned_model/ &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --w_bit &lt;span class="m">4&lt;/span> --output_path pruned_awq_model/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Resultado esperado: ~13 GB BF16 → ~6.5 GB tras poda 2:4 en sparse format → ~3.2 GB con AWQ INT4. El modelo 14B cabrá en la RTX 4090 con margen para KV cache.&lt;/p>
&lt;h3 id="4-h100-sxm-320-gb-total-hopper">4× H100 SXM (320 GB total, Hopper)&lt;/h3>
&lt;p>En este hardware la poda estructurada (layer dropping) tiene más sentido que 2:4 para inferencia de alta concurrencia: reduces el número de operaciones FLOPs por token de forma proporcional, lo que beneficia al throughput bajo batch grande donde el cuello es compute, no memoria.&lt;/p>
&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="c1"># Aplicar layer dropping con ShortGPT BI metric&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">shortgpt&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">compute_block_influence&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">drop_layers&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">bi_scores&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">compute_block_influence&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">calibration_data&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Eliminar el 15% de capas con BI más bajo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">model&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">drop_layers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">bi_scores&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">drop_ratio&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.15&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Un Llama-3-70B podado al 15% de capas cabe en 3 H100 en vez de 4, liberando una GPU para otra tarea.&lt;/p>
&lt;hr>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>https://blog.lo0.es/posts/quantization-fundamentos-inferencia/ — la palanca complementaria: cuantizar reduce la precisión de los pesos que la poda ha decidido conservar; combinadas dan compresión máxima&lt;/li>
&lt;li>https://blog.lo0.es/posts/kv-cache-fundamentos/ — la poda reduce el tamaño del modelo, pero el KV cache sigue creciendo con el contexto; son costes separados en VRAM&lt;/li>
&lt;li>https://blog.lo0.es/posts/speculative-decoding-fundamentos/ — los drafters de speculative decoding son a menudo versiones podadas del modelo base, no modelos entrenados desde cero&lt;/li>
&lt;li>https://blog.lo0.es/posts/decode-optimizaciones-vllm/ — cómo el modelo podado se sirve en vLLM: los parámetros de throughput cambian con un modelo estructuralmente más pequeño&lt;/li>
&lt;li>https://blog.lo0.es/posts/knowledge-distillation-fundamentos/ — alternativa conceptual a la poda: en vez de eliminar partes del modelo grande, entrenar uno pequeño para que imite su comportamiento&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://arxiv.org/abs/2301.00774">SparseGPT: Massive Language Models Can be Accurately Pruned in One Shot&lt;/a> — Frantar &amp;amp; Alistarh, 2023&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2306.11695">A Simple and Effective Pruning Approach for Large Language Models (Wanda)&lt;/a> — Sun et al., ICLR 2024&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/1803.03635">The Lottery Ticket Hypothesis&lt;/a> — Frankle &amp;amp; Carlin, ICLR 2019&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2403.03853">ShortGPT: Layers in Large Language Models are More Redundant Than You Expect&lt;/a> — Men et al., 2024&lt;/li>
&lt;li>&lt;a href="https://pytorch.org/blog/when-quantization-isnt-enough-why-24-sparsity-matters/">NVIDIA 2:4 Sparsity in PyTorch&lt;/a> — PyTorch Blog&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/html/2605.06402">SparseForge: Efficient Semi-Structured LLM Sparsification&lt;/a> — 2025&lt;/li>
&lt;/ul></description></item></channel></rss>