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