<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Transformers on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/transformers/</link><description>Recent content in Transformers on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Mon, 18 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/tags/transformers/index.xml" rel="self" type="application/rss+xml"/><item><title>KV cache: la memoria de trabajo que sostiene la inferencia LLM</title><link>https://blog.lo0.es/posts/kv-cache-fundamentos/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://blog.lo0.es/posts/kv-cache-fundamentos/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El KV cache es la &lt;strong>memoria de trabajo&lt;/strong> que un modelo de lenguaje mantiene durante una conversación. Sin él, cada token nuevo obligaría a recalcular toda la conversación desde el principio, con un coste &lt;strong>cuadrático&lt;/strong> en la longitud del texto. Con él, el coste es lineal pero a cambio el cache &lt;strong>vive en VRAM y crece con cada token&lt;/strong>. En la práctica, no es el modelo lo que limita cuánto contexto puedes servir: es el KV cache. Para una RTX 4090 con Llama 3 8B, cabe el modelo en 16 GB y queda apenas espacio para ~64 K tokens de cache totales (sumando todas las sesiones simultáneas). Entender este número es la diferencia entre &lt;strong>anunciar&lt;/strong> &amp;ldquo;contexto de 128 K&amp;rdquo; y &lt;strong>servirlo&lt;/strong> de verdad bajo carga.&lt;/p>
&lt;h2 id="la-analogía-el-orador-con-amnesia">La analogía: el orador con amnesia&lt;/h2>
&lt;p>Imagina que asistes a una conferencia técnica de dos horas. El ponente, cada vez que va a decir una frase nueva, &lt;strong>rebobina mentalmente toda la charla desde el inicio&lt;/strong>, recompone el hilo, y solo entonces continúa. Su próxima frase requiere rememorar la anterior; la siguiente, las dos anteriores; al cabo de una hora, cada palabra nueva le cuesta una hora de recapitulación. Una conferencia así sería materialmente imposible.&lt;/p>
&lt;p>Ahora imagina al mismo ponente con un cuaderno donde apunta, mientras habla, las dos o tres ideas clave de cada frase: sujeto, objeto, vínculo con lo anterior. Antes de cada frase nueva, ojea el cuaderno y sigue. Su próxima palabra sólo cuesta una ojeada al cuaderno, no rebobinar la charla entera.&lt;/p>
&lt;p>Ese cuaderno, en un transformer, se llama &lt;strong>KV cache&lt;/strong>. Sin él, los modelos de lenguaje conversacionales serían inviables. Con él, son productos comerciales. Pero el cuaderno &lt;strong>pesa&lt;/strong>: y entender cuánto, dónde y por qué, es lo que separa una infraestructura de inferencia que funciona de una que se cae a la tercera sesión concurrente.&lt;/p>
&lt;h2 id="el-mecanismo-en-sí-en-cristiano">El mecanismo en sí (en cristiano)&lt;/h2>
&lt;p>Un transformer genera texto &lt;strong>un token cada vez&lt;/strong>. Para decidir el siguiente token, el modelo aplica un mecanismo llamado &lt;strong>atención&lt;/strong> sobre todos los tokens previos: pregunta &amp;ldquo;¿qué partes del contexto anterior son relevantes para predecir lo que viene ahora?&amp;rdquo;.&lt;/p>
&lt;p>Internamente, cada token de entrada se proyecta a tres vectores:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Q&lt;/strong> (Query): &amp;ldquo;qué estoy buscando&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>K&lt;/strong> (Key): &amp;ldquo;qué oferta este token&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>V&lt;/strong> (Value): &amp;ldquo;qué información lleva este token&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>La atención del token actual contra el contexto se calcula multiplicando su &lt;strong>Q&lt;/strong> contra las &lt;strong>K&lt;/strong> de todos los tokens previos, normalizando con softmax, y ponderando las &lt;strong>V&lt;/strong> correspondientes. Resultado: una representación contextualizada del token actual.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 260" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Diagrama del cálculo de atención con Q, K, V">
&lt;style>
.box { fill: #f4f4f4; stroke: #333; stroke-width: 1.5; }
.box-q { fill: #ffe9d6; }
.box-k { fill: #d6eaff; }
.box-v { fill: #d9f5d6; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
.arr { stroke: #444; stroke-width: 1.4; fill: none; marker-end: url(#ah); }
&lt;/style>
&lt;defs>
&lt;marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
&lt;path d="M0,0 L10,5 L0,10 z" fill="#444"/>
&lt;/marker>
&lt;/defs>
&lt;text x="360" y="22" text-anchor="middle" class="lbl">Cálculo de atención para el token N&lt;/text>
&lt;rect x="40" y="60" width="120" height="40" rx="6" class="box box-q"/>
&lt;text x="100" y="85" text-anchor="middle" class="lbl">Q (token N)&lt;/text>
&lt;text x="100" y="115" text-anchor="middle" class="lbl-sm">"qué busco"&lt;/text>
&lt;rect x="280" y="60" width="160" height="40" rx="6" class="box box-k"/>
&lt;text x="360" y="85" text-anchor="middle" class="lbl">K (tokens 1..N)&lt;/text>
&lt;text x="360" y="115" text-anchor="middle" class="lbl-sm">del cache&lt;/text>
&lt;rect x="560" y="60" width="120" height="40" rx="6" class="box box-v"/>
&lt;text x="620" y="85" text-anchor="middle" class="lbl">V (tokens 1..N)&lt;/text>
&lt;text x="620" y="115" text-anchor="middle" class="lbl-sm">del cache&lt;/text>
&lt;path class="arr" d="M160,80 L280,80"/>
&lt;path class="arr" d="M440,80 L560,80"/>
&lt;text x="220" y="74" text-anchor="middle" class="lbl-sm">Q·Kᵀ → softmax&lt;/text>
&lt;text x="500" y="74" text-anchor="middle" class="lbl-sm">× V&lt;/text>
&lt;rect x="240" y="170" width="240" height="44" rx="6" class="box"/>
&lt;text x="360" y="197" text-anchor="middle" class="lbl">representación del token N&lt;/text>
&lt;path class="arr" d="M620,100 C620,150 480,150 480,170"/>
&lt;path class="arr" d="M100,100 C100,150 240,150 240,170"/>
&lt;/svg>
&lt;/div>
&lt;p>Aquí está la clave: para predecir el token N, sólo necesito &lt;strong>Q nuevo&lt;/strong> (el del token N) y &lt;strong>K, V de todos los tokens anteriores&lt;/strong>. Las K y V de los tokens 1..N-1 no han cambiado desde la iteración anterior. Recalcularlas sería tirar trabajo.&lt;/p>
&lt;p>&lt;strong>El KV cache es exactamente eso: la memoria que guarda K y V de cada token ya procesado, en cada capa del modelo, para no recalcularlos.&lt;/strong>&lt;/p>
&lt;h2 id="por-qué-existe-el-coste-cuadrático-sin-él">Por qué existe: el coste cuadrático sin él&lt;/h2>
&lt;p>Generar un texto de N tokens implica N pasos. En el paso &lt;code>i&lt;/code>, se calcula la atención sobre &lt;code>i&lt;/code> tokens anteriores. Sin cache, en cada paso recomputas las K, V de los &lt;code>i-1&lt;/code> tokens anteriores &lt;strong>más&lt;/strong> las del nuevo. La cuenta total de cómputos de atención crece como:&lt;/p>
&lt;pre tabindex="0">&lt;code>Σ i (i=1..N) = N·(N+1) / 2 ≈ N² / 2
&lt;/code>&lt;/pre>&lt;p>Con KV cache, sólo procesas el token nuevo en cada paso: coste &lt;strong>lineal en N&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparativa de coste lineal vs cuadrático">
&lt;style>
.ax { stroke: #333; stroke-width: 1.5; }
.grid { stroke: #ddd; stroke-width: 1; stroke-dasharray: 3,3; }
.lin { stroke: #2a9d8f; stroke-width: 2.5; fill: none; }
.quad { stroke: #e76f51; stroke-width: 2.5; fill: none; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #555; }
.tag-lin { fill: #2a9d8f; font: 600 12px sans-serif; }
.tag-quad { fill: #e76f51; font: 600 12px sans-serif; }
&lt;/style>
&lt;text x="360" y="20" text-anchor="middle" class="lbl">Cómputo acumulado para generar N tokens&lt;/text>
&lt;line class="ax" x1="80" y1="270" x2="680" y2="270"/>
&lt;line class="ax" x1="80" y1="40" x2="80" y2="270"/>
&lt;text x="380" y="300" text-anchor="middle" class="lbl-sm">tokens generados (N)&lt;/text>
&lt;text x="30" y="155" text-anchor="middle" class="lbl-sm" transform="rotate(-90 30 155)">cómputo relativo&lt;/text>
&lt;line class="grid" x1="80" y1="220" x2="680" y2="220"/>
&lt;line class="grid" x1="80" y1="170" x2="680" y2="170"/>
&lt;line class="grid" x1="80" y1="120" x2="680" y2="120"/>
&lt;line class="grid" x1="80" y1="70" x2="680" y2="70"/>
&lt;text x="75" y="274" text-anchor="end" class="lbl-sm">0&lt;/text>
&lt;text x="75" y="224" text-anchor="end" class="lbl-sm">25%&lt;/text>
&lt;text x="75" y="174" text-anchor="end" class="lbl-sm">50%&lt;/text>
&lt;text x="75" y="124" text-anchor="end" class="lbl-sm">75%&lt;/text>
&lt;text x="75" y="74" text-anchor="end" class="lbl-sm">100%&lt;/text>
&lt;text x="80" y="285" text-anchor="middle" class="lbl-sm">0&lt;/text>
&lt;text x="230" y="285" text-anchor="middle" class="lbl-sm">1K&lt;/text>
&lt;text x="380" y="285" text-anchor="middle" class="lbl-sm">2K&lt;/text>
&lt;text x="530" y="285" text-anchor="middle" class="lbl-sm">3K&lt;/text>
&lt;text x="680" y="285" text-anchor="middle" class="lbl-sm">4K&lt;/text>
&lt;!-- Lineal: pendiente suave -->
&lt;path class="lin" d="M80,270 L680,265"/>
&lt;!-- Cuadrática: parábola -->
&lt;path class="quad" d="M80,270 Q380,270 680,70"/>
&lt;text x="640" y="258" class="tag-lin">con KV cache (lineal)&lt;/text>
&lt;text x="500" y="100" class="tag-quad">sin KV cache (cuadrático)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Los números concretos son demoledores:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Tokens generados&lt;/th>
&lt;th style="text-align:right">Sin KV cache (operaciones)&lt;/th>
&lt;th style="text-align:right">Con KV cache&lt;/th>
&lt;th style="text-align:right">Ratio&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">8 256&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">64×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">1 024&lt;/td>
&lt;td style="text-align:right">524 800&lt;/td>
&lt;td style="text-align:right">1 024&lt;/td>
&lt;td style="text-align:right">512×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">4 096&lt;/td>
&lt;td style="text-align:right">8 390 656&lt;/td>
&lt;td style="text-align:right">4 096&lt;/td>
&lt;td style="text-align:right">2 048×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">32 768&lt;/td>
&lt;td style="text-align:right">536 887 296&lt;/td>
&lt;td style="text-align:right">32 768&lt;/td>
&lt;td style="text-align:right">16 384×&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A los 32 K tokens, &lt;strong>el cache te ahorra cuatro órdenes de magnitud&lt;/strong> de cómputo. No es una optimización: es lo que hace que la inferencia conversacional sea posible.&lt;/p>
&lt;h2 id="el-precio-cuánto-pesa-la-mochila">El precio: cuánto pesa la mochila&lt;/h2>
&lt;p>El KV cache se paga en VRAM. La fórmula, por &lt;strong>secuencia&lt;/strong>, es:&lt;/p>
&lt;pre tabindex="0">&lt;code>KV_size = 2 · n_layers · n_kv_heads · head_dim · context_len · bytes_per_param
↑
K y V
&lt;/code>&lt;/pre>&lt;p>Por &lt;strong>token&lt;/strong> (sin el &lt;code>context_len&lt;/code>), es una constante propia del modelo. Veamos números reales:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Modelo&lt;/th>
&lt;th style="text-align:right">n_layers&lt;/th>
&lt;th style="text-align:right">n_kv_heads&lt;/th>
&lt;th style="text-align:right">head_dim&lt;/th>
&lt;th style="text-align:right">Bytes/token (BF16)&lt;/th>
&lt;th style="text-align:right">GB a 8 K ctx&lt;/th>
&lt;th style="text-align:right">GB a 32 K&lt;/th>
&lt;th style="text-align:right">GB a 128 K&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Llama 3 8B (MHA hipotético)&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">524 288&lt;/td>
&lt;td style="text-align:right">4.00&lt;/td>
&lt;td style="text-align:right">16.00&lt;/td>
&lt;td style="text-align:right">64.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Llama 3 8B (GQA real)&lt;/strong>&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">131 072&lt;/td>
&lt;td style="text-align:right">&lt;strong>1.00&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>4.00&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>16.00&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 3 70B (GQA)&lt;/td>
&lt;td style="text-align:right">80&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">327 680&lt;/td>
&lt;td style="text-align:right">2.50&lt;/td>
&lt;td style="text-align:right">10.00&lt;/td>
&lt;td style="text-align:right">40.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Qwen3 8B (GQA)&lt;/td>
&lt;td style="text-align:right">36&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">147 456&lt;/td>
&lt;td style="text-align:right">1.12&lt;/td>
&lt;td style="text-align:right">4.50&lt;/td>
&lt;td style="text-align:right">18.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Mistral 7B (GQA)&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">131 072&lt;/td>
&lt;td style="text-align:right">1.00&lt;/td>
&lt;td style="text-align:right">4.00&lt;/td>
&lt;td style="text-align:right">16.00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Dos lecturas inmediatas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Sin GQA, no hay 128 K que valga.&lt;/strong> Un Llama 3 8B con atención multi-head clásica necesitaría 64 GB sólo de KV cache para una única secuencia con 128 K tokens. Es decir, &lt;strong>no cabe en ninguna GPU consumer&lt;/strong>. Por eso Meta, Mistral y compañía adoptaron Grouped Query Attention.&lt;/li>
&lt;li>&lt;strong>El KV cache puede ser mayor que el modelo.&lt;/strong> Llama 3 8B BF16 ocupa ~16 GB. Con 128 K de contexto, su cache son otros 16 GB. Una sola sesión empata al modelo en VRAM.&lt;/li>
&lt;/ol>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Crecimiento del KV cache con la longitud de contexto">
&lt;style>
.ax { stroke: #333; stroke-width: 1.5; }
.grid { stroke: #ddd; stroke-width: 1; stroke-dasharray: 3,3; }
.l8b { stroke: #2a9d8f; stroke-width: 2.5; fill: none; }
.l70b { stroke: #e76f51; stroke-width: 2.5; fill: none; }
.lq8 { stroke: #6a4c93; stroke-width: 2.5; fill: none; stroke-dasharray: 5,3; }
.lim { stroke: #c1121f; stroke-width: 1.5; stroke-dasharray: 4,4; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #555; }
.tag { font: 600 11px sans-serif; }
&lt;/style>
&lt;text x="360" y="20" text-anchor="middle" class="lbl">KV cache (GB) vs longitud de contexto (1 secuencia, BF16)&lt;/text>
&lt;line class="ax" x1="80" y1="240" x2="680" y2="240"/>
&lt;line class="ax" x1="80" y1="40" x2="80" y2="240"/>
&lt;line class="grid" x1="80" y1="190" x2="680" y2="190"/>
&lt;line class="grid" x1="80" y1="140" x2="680" y2="140"/>
&lt;line class="grid" x1="80" y1="90" x2="680" y2="90"/>
&lt;text x="75" y="244" text-anchor="end" class="lbl-sm">0&lt;/text>
&lt;text x="75" y="194" text-anchor="end" class="lbl-sm">10&lt;/text>
&lt;text x="75" y="144" text-anchor="end" class="lbl-sm">20&lt;/text>
&lt;text x="75" y="94" text-anchor="end" class="lbl-sm">30&lt;/text>
&lt;text x="75" y="44" text-anchor="end" class="lbl-sm">40 GB&lt;/text>
&lt;text x="80" y="258" text-anchor="middle" class="lbl-sm">0&lt;/text>
&lt;text x="180" y="258" text-anchor="middle" class="lbl-sm">8K&lt;/text>
&lt;text x="305" y="258" text-anchor="middle" class="lbl-sm">32K&lt;/text>
&lt;text x="430" y="258" text-anchor="middle" class="lbl-sm">64K&lt;/text>
&lt;text x="680" y="258" text-anchor="middle" class="lbl-sm">128K&lt;/text>
&lt;!-- Limite VRAM disponible RTX 4090 (~8 GB libres tras modelo) -->
&lt;line class="lim" x1="80" y1="200" x2="680" y2="200"/>
&lt;text x="680" y="196" text-anchor="end" class="tag" fill="#c1121f">≈ VRAM libre tras cargar 8B en una 4090&lt;/text>
&lt;!-- Llama 3 8B GQA: lineal, 1 GB @8K, 16 GB @128K -->
&lt;path class="l8b" d="M80,240 L180,235 L305,220 L430,200 L680,160"/>
&lt;!-- Qwen3 8B GQA -->
&lt;path class="lq8" d="M80,240 L180,234 L305,217 L430,194 L680,150"/>
&lt;!-- Llama 3 70B GQA -->
&lt;path class="l70b" d="M80,240 L180,228 L305,190 L430,140 L680,40"/>
&lt;text x="690" y="160" class="tag" fill="#2a9d8f">Llama 3 8B&lt;/text>
&lt;text x="690" y="148" class="tag" fill="#6a4c93">Qwen3 8B&lt;/text>
&lt;text x="690" y="42" class="tag" fill="#e76f51">Llama 3 70B&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>La línea roja punteada marca la VRAM realista disponible en una RTX 4090 después de cargar el modelo. &lt;strong>Cualquier modelo cuya curva cruza esa línea no podrá servir ese contexto&lt;/strong> sin estrategias adicionales (cuantización del cache, offload, particionado).&lt;/p>
&lt;h2 id="la-inferencia-es-memory-bound-no-compute-bound">La inferencia es memory-bound, no compute-bound&lt;/h2>
&lt;p>Hay un equívoco común: pensar que &amp;ldquo;GPU rápida = inferencia rápida&amp;rdquo;. En el régimen donde realmente operan los servicios de inferencia con KV cache, &lt;strong>lo que se mide es el ancho de banda de memoria&lt;/strong>. Cada token nuevo exige leer las K y V de todos los tokens anteriores desde HBM. El cómputo es modesto; el movimiento de datos, masivo.&lt;/p>
&lt;p>Por eso, una H100 SXM (3.35 TB/s de HBM3) puede ser 2–3× más rápida que una A100 (1.55–2 TB/s) &lt;strong>sin que la frecuencia ni el número de cores expliquen del todo la diferencia&lt;/strong>. Lo explica el ancho de banda.&lt;/p>
&lt;p>Y por eso, también, las ofertas de &amp;ldquo;GPU baratas con mucha VRAM pero HBM lenta&amp;rdquo; (algunas variantes con GDDR6 o LPDDR5) decepcionan en inferencia con contextos largos: tienen sitio para guardar el cache pero les cuesta una eternidad releerlo.&lt;/p>
&lt;h2 id="trucos-para-que-el-cuaderno-sea-más-fino">Trucos para que el cuaderno sea más fino&lt;/h2>
&lt;p>Tres técnicas, en orden cronológico, han ido aplanando el tamaño del KV cache:&lt;/p>
&lt;p>&lt;strong>Multi-Head Attention (MHA).&lt;/strong> El planteamiento original del transformer (Vaswani et al., 2017). Cada cabeza de atención tiene su propia K y V. Caro en cache pero teóricamente máximo en expresividad. Es lo que tenían los modelos hasta ~2023.&lt;/p>
&lt;p>&lt;strong>Multi-Query Attention (MQA).&lt;/strong> Una sola K y V compartida por todas las cabezas. Reduce el cache &lt;code>n_heads&lt;/code> veces. Funciona razonablemente pero degrada calidad de generación en algunos benchmarks.&lt;/p>
&lt;p>&lt;strong>Grouped Query Attention (GQA).&lt;/strong> El término medio que ha ganado. Las cabezas se agrupan: en Llama 3 8B, 32 cabezas de query comparten K, V en grupos de 4 → 8 grupos de KV. Reduce el cache 4× respecto a MHA con casi idéntica calidad. Es el estándar de facto desde 2024.&lt;/p>
&lt;p>&lt;strong>Multi-Head Latent Attention (MLA).&lt;/strong> La innovación de DeepSeek-V2/V3: en vez de almacenar K, V por cabeza, comprime el estado en un vector latente más pequeño y proyecta a K, V en el momento. El cache puede llegar a 70 bytes/token, dos órdenes de magnitud menos que GQA. Es la razón principal por la que DeepSeek-V3 (671 B parámetros, 37 B activos) es servible en infraestructura abordable.&lt;/p>
&lt;div class="diagram" style="max-width: 640px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 640 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Reducción del KV cache por técnica">
&lt;style>
.bar { stroke: #333; stroke-width: 1; }
.b-mha { fill: #e76f51; }
.b-gqa { fill: #f4a261; }
.b-mqa { fill: #e9c46a; }
.b-mla { fill: #2a9d8f; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm{ font: 11px sans-serif; fill: #444; }
&lt;/style>
&lt;text x="320" y="20" text-anchor="middle" class="lbl">KB de cache por token (Llama 3 8B equivalente, BF16)&lt;/text>
&lt;rect x="200" y="40" width="380" height="22" class="bar b-mha"/>
&lt;text x="170" y="56" text-anchor="end" class="lbl-sm">MHA (32 KV heads)&lt;/text>
&lt;text x="595" y="56" class="lbl-sm">512 KB&lt;/text>
&lt;rect x="200" y="76" width="95" height="22" class="bar b-gqa"/>
&lt;text x="170" y="92" text-anchor="end" class="lbl-sm">GQA (8 KV heads)&lt;/text>
&lt;text x="310" y="92" class="lbl-sm">128 KB&lt;/text>
&lt;rect x="200" y="112" width="12" height="22" class="bar b-mqa"/>
&lt;text x="170" y="128" text-anchor="end" class="lbl-sm">MQA (1 KV head)&lt;/text>
&lt;text x="225" y="128" class="lbl-sm">16 KB&lt;/text>
&lt;rect x="200" y="148" width="3" height="22" class="bar b-mla"/>
&lt;text x="170" y="164" text-anchor="end" class="lbl-sm">MLA (DeepSeek-V3)&lt;/text>
&lt;text x="215" y="164" class="lbl-sm">~0.5 KB (real V3)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;blockquote>
&lt;p>&lt;strong>Nota:&lt;/strong> la barra de MLA es ilustrativa con valores típicos publicados por DeepSeek; la implementación exacta depende del tamaño latente. Lo importante es el orden de magnitud.&lt;/p>
&lt;/blockquote>
&lt;p>A esto se suma una cuarta técnica ortogonal: &lt;strong>cuantizar el cache&lt;/strong> a FP8, INT8 o incluso INT4. vLLM y TensorRT-LLM ya lo soportan en producción. Pasar de BF16 (2 bytes) a FP8 (1 byte) &lt;strong>divide el cache por dos&lt;/strong> con coste pequeño en calidad. Pasar a INT4, por cuatro, con coste algo mayor.&lt;/p>
&lt;h2 id="el-siguiente-dragón-la-fragmentación">El siguiente dragón: la fragmentación&lt;/h2>
&lt;p>Hasta aquí hemos hablado del cache como si fuera un bloque contiguo. En la práctica, un servidor de inferencia atiende &lt;strong>decenas de sesiones simultáneas&lt;/strong>, cada una con su propio cache que crece a un ritmo distinto. La asignación naïve —reservar el máximo posible por sesión— &lt;strong>desperdicia entre el 60 % y el 80 % de la VRAM&lt;/strong> según el paper original de PagedAttention.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 240" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Fragmentación del KV cache: naïve vs PagedAttention">
&lt;style>
.used { fill: #2a9d8f; stroke: #1a6e63; stroke-width: 1; }
.free { fill: #f0e7d8; stroke: #aaa; stroke-width: 1; }
.blk { stroke: #555; stroke-width: 0.5; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
&lt;/style>
&lt;text x="180" y="22" text-anchor="middle" class="lbl">Asignación naïve (contigua)&lt;/text>
&lt;text x="540" y="22" text-anchor="middle" class="lbl">PagedAttention (bloques)&lt;/text>
&lt;!-- Naive: 4 sesiones reservan el máximo, usan poco -->
&lt;text x="30" y="60" class="lbl-sm">sesión A&lt;/text>
&lt;rect x="90" y="48" width="50" height="18" class="used"/>
&lt;rect x="140" y="48" width="180" height="18" class="free"/>
&lt;text x="30" y="92" class="lbl-sm">sesión B&lt;/text>
&lt;rect x="90" y="80" width="25" height="18" class="used"/>
&lt;rect x="115" y="80" width="205" height="18" class="free"/>
&lt;text x="30" y="124" class="lbl-sm">sesión C&lt;/text>
&lt;rect x="90" y="112" width="100" height="18" class="used"/>
&lt;rect x="190" y="112" width="130" height="18" class="free"/>
&lt;text x="30" y="156" class="lbl-sm">sesión D&lt;/text>
&lt;rect x="90" y="144" width="35" height="18" class="used"/>
&lt;rect x="125" y="144" width="195" height="18" class="free"/>
&lt;text x="180" y="190" text-anchor="middle" class="lbl-sm">→ ~70 % de VRAM reservada y vacía&lt;/text>
&lt;!-- PagedAttention: bloques pequeños, ocupación densa -->
&lt;g transform="translate(400,40)">
&lt;!-- 8 bloques x 5 filas -->
&lt;g>
&lt;!-- fila 1 -->
&lt;rect x="0" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="30" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="60" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="90" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="120" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="150" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="180" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="210" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="0" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="30" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="60" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="90" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="120" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="150" y="22" width="30" height="20" class="used blk"/>
&lt;rect x="180" y="22" width="30" height="20" class="free blk"/>
&lt;rect x="210" y="22" width="30" height="20" class="free blk"/>
&lt;rect x="0" y="44" width="30" height="20" class="used blk"/>
&lt;rect x="30" y="44" width="30" height="20" class="used blk"/>
&lt;rect x="60" y="44" width="30" height="20" class="used blk"/>
&lt;rect x="90" y="44" width="30" height="20" class="free blk"/>
&lt;rect x="120" y="44" width="30" height="20" class="free blk"/>
&lt;rect x="150" y="44" width="30" height="20" class="free blk"/>
&lt;rect x="180" y="44" width="30" height="20" class="free blk"/>
&lt;rect x="210" y="44" width="30" height="20" class="free blk"/>
&lt;/g>
&lt;/g>
&lt;text x="540" y="190" text-anchor="middle" class="lbl-sm">→ &amp;lt; 4 % desperdicio (paper vLLM)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>&lt;strong>PagedAttention&lt;/strong> —la idea de Kwon et al. (2023) que dio origen a vLLM— resuelve esto pidiendo prestada una técnica de los sistemas operativos: dividir la VRAM en &lt;strong>bloques&lt;/strong> pequeños (típicamente de 16 tokens) y mantener una &lt;strong>tabla de páginas&lt;/strong> lógicas → físicas por sesión. Una sesión ya no reserva un bloque contiguo enorme: crece un bloque cada vez, y los bloques pueden estar dispersos por la VRAM. Resultado: ocupación efectiva del 90 % en lugar del 30 %, y por tanto &lt;strong>2–4× más throughput agregado&lt;/strong> en el mismo hardware.&lt;/p>
&lt;p>PagedAttention merece artículo propio. Lo dejo apuntado para el siguiente.&lt;/p>
&lt;h2 id="dos-escenarios-workstation-vs-cluster">Dos escenarios: workstation vs cluster&lt;/h2>
&lt;p>Bajemos a casos concretos y comparemos las dos puntas del espectro de hardware on-premise: una &lt;strong>workstation con una GPU consumer&lt;/strong> frente a un &lt;strong>cluster de GPUs de datacenter&lt;/strong>. Mismas matemáticas, dos órdenes de magnitud de diferencia en lo que pueden servir.&lt;/p>
&lt;h3 id="escenario-a--una-rtx-4090-24-gb-ada-lovelace">Escenario A — Una RTX 4090 (24 GB, Ada Lovelace)&lt;/h3>
&lt;p>Configuración típica con Qwen3-8B BF16:&lt;/p>
&lt;pre tabindex="0">&lt;code>Modelo BF16: ~16 GB
Activations + overhead: ~2 GB
VRAM disponible para KV cache: ~6 GB (con margen)
&lt;/code>&lt;/pre>&lt;p>Con 144 KB/token (Qwen3-8B GQA), eso son &lt;strong>~43 K tokens totales de cache&lt;/strong> distribuidos entre todas las sesiones simultáneas. En la práctica:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Concurrencia&lt;/th>
&lt;th style="text-align:right">Contexto máximo por sesión&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">32 768&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">4&lt;/td>
&lt;td style="text-align:right">8 192&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">4 096&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">2 048&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Si necesitas anunciar &amp;ldquo;soportamos 32 K de contexto&amp;rdquo; con concurrencia 4+, hay que &lt;strong>cuantizar el cache&lt;/strong> (FP8 baja a 72 KB/token, duplica capacidad) o &lt;strong>subir el modelo de gama&lt;/strong> (un 4B con GQA y cache cuantizado holgaría).&lt;/p>
&lt;h3 id="escenario-b--cluster-de-5h100-sxm-400-gb-agregados-nvlink">Escenario B — Cluster de 5×H100 SXM (400 GB agregados, NVLink)&lt;/h3>
&lt;p>Con tensor parallel = 5 y Llama 3 70B BF16:&lt;/p>
&lt;pre tabindex="0">&lt;code>Modelo BF16: ~140 GB (28 GB/GPU)
Overhead vLLM por GPU: ~2 GB
VRAM libre para KV por GPU: ~50 GB → ~250 GB agregados
&lt;/code>&lt;/pre>&lt;p>Con 320 KB/token (Llama 3 70B GQA), eso son &lt;strong>~800 K tokens totales de cache&lt;/strong>. Mucho margen para servir contextos largos con concurrencia alta:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Concurrencia&lt;/th>
&lt;th style="text-align:right">Contexto máximo por sesión&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">4&lt;/td>
&lt;td style="text-align:right">200 000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">50 000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">64&lt;/td>
&lt;td style="text-align:right">12 500&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Para DeepSeek-V3 671 B con MLA: la economía cambia radicalmente porque el cache es ~100× más fino. Lo que limita ya no es el cache sino la VRAM del propio modelo (cuantizado FP8 son ~671 GB → no cabe en 5×H100, hace falta cluster mayor o FP4).&lt;/p>
&lt;h3 id="a-y-b-lado-a-lado">A y B, lado a lado&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th style="text-align:right">Escenario A (1×4090)&lt;/th>
&lt;th style="text-align:right">Escenario B (5×H100 SXM)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>VRAM total&lt;/td>
&lt;td style="text-align:right">24 GB&lt;/td>
&lt;td style="text-align:right">400 GB (≈ 17×)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Modelo servible (BF16)&lt;/td>
&lt;td style="text-align:right">Hasta ~8 B&lt;/td>
&lt;td style="text-align:right">Hasta ~70 B&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KV cache disponible&lt;/td>
&lt;td style="text-align:right">~6 GB&lt;/td>
&lt;td style="text-align:right">~250 GB (≈ 42×)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokens totales de cache&lt;/td>
&lt;td style="text-align:right">~43 K&lt;/td>
&lt;td style="text-align:right">~800 K (≈ 19×)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ancho de banda HBM&lt;/td>
&lt;td style="text-align:right">1.0 TB/s (GDDR6X)&lt;/td>
&lt;td style="text-align:right">3.35 TB/s/GPU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Coste indicativo&lt;/td>
&lt;td style="text-align:right">~2 K €&lt;/td>
&lt;td style="text-align:right">~250 K € (≈ 125×)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La asimetría interesante: pasar de A a B cuesta unas 125× más, pero sólo da ~19× más cache total. Lo que el cluster compra de verdad no es proporcionalmente más contexto: es &lt;strong>modelos un orden de magnitud más grandes&lt;/strong> servibles a la vez (8 B → 70 B), &lt;strong>ancho de banda HBM 3× mayor por GPU&lt;/strong> y &lt;strong>concurrencia muy alta sin degradar latencia&lt;/strong>. Si tu carga real es &amp;ldquo;modelos pequeños con mucho contexto y baja concurrencia&amp;rdquo;, la 4090 cuantizada rinde mucho más cerca del cluster de lo que el precio sugiere. Si es &amp;ldquo;modelo grande, baja latencia, muchos usuarios&amp;rdquo;, el salto a HBM3 + NVLink no se sustituye con más 4090s.&lt;/p>
&lt;h3 id="implicaciones-operativas">Implicaciones operativas&lt;/h3>
&lt;p>Tres observaciones para llevarse:&lt;/p>
&lt;p>Primero, &lt;strong>el contexto máximo anunciado por un modelo no es el que puedes servir en tu hardware&lt;/strong>. Llama 3 8B &amp;ldquo;soporta&amp;rdquo; 128 K, pero en una 4090 con 4 sesiones simultáneas tu contexto efectivo son ~8 K. Es trivial comprobarlo antes de comprometerse a un SLA.&lt;/p>
&lt;p>Segundo, &lt;strong>cuantizar el KV cache es de las optimizaciones con mejor relación coste/beneficio&lt;/strong>. No toca los pesos, no afecta a la reproducibilidad de las salidas y duplica capacidad. vLLM lo soporta vía &lt;code>--kv-cache-dtype fp8&lt;/code>.&lt;/p>
&lt;p>Tercero, &lt;strong>si los SLA dictan contextos largos con muchos usuarios concurrentes, GQA es necesario pero no suficiente&lt;/strong>. A medio plazo, hay que mirar modelos con MLA o variantes de attention con compresión.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>PagedAttention&lt;/strong> y su implementación en vLLM: bloques, tabla de páginas, evicción.&lt;/li>
&lt;li>&lt;strong>Prefix caching&lt;/strong>: cuando varias peticiones comparten el system prompt, no hace falta recomputar las K, V de la parte común.&lt;/li>
&lt;li>&lt;strong>Speculative decoding&lt;/strong> y su interacción con el cache.&lt;/li>
&lt;li>&lt;strong>Cache offloading&lt;/strong>: mover bloques fríos a RAM o a NVMe, técnica clave para contextos &amp;gt; 1 M.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Vaswani et al., &lt;em>Attention Is All You Need&lt;/em> (NeurIPS 2017) — paper fundacional del transformer.&lt;/li>
&lt;li>Ainslie et al., &lt;em>GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints&lt;/em> (EMNLP 2023).&lt;/li>
&lt;li>Kwon et al., &lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em> (SOSP 2023) — paper original de vLLM.&lt;/li>
&lt;li>DeepSeek-AI, &lt;em>DeepSeek-V2 Technical Report&lt;/em> (2024) — introducción de Multi-Head Latent Attention.&lt;/li>
&lt;li>Documentación oficial de vLLM: &lt;a href="https://docs.vllm.ai/">https://docs.vllm.ai/&lt;/a>.&lt;/li>
&lt;li>Llama 3 model card (Meta): especificaciones GQA, n_layers, n_kv_heads.&lt;/li>
&lt;/ul></description></item></channel></rss>