<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Slm on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/slm/</link><description>Recent content in Slm on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Tue, 09 Jun 2026 02:30:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/tags/slm/index.xml" rel="self" type="application/rss+xml"/><item><title>QLoRA y multi-LoRA al límite en modelos pequeños</title><link>https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/</link><pubDate>Tue, 09 Jun 2026 02:30:00 +0000</pubDate><guid>https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/</guid><description>&lt;blockquote>
&lt;p>Este post es el complemento de entrenamiento de &lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a>. Aquel desmonta el &lt;strong>consumidor&lt;/strong> —cómo se sirven cientos de adapters concurrentes con kernels SGMV y unified paging—; este desmonta el &lt;strong>productor&lt;/strong> —cómo se entrena un adapter sobre un base cuantizado en una sola GPU, y por qué el patrón &amp;ldquo;un SLM base congelado + N adapters de rank bajo&amp;rdquo; es el encaje natural de los modelos pequeños. Aquí no repetimos los internals del serving; los damos por leídos.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>&lt;strong>QLoRA&lt;/strong> (Dettmers et al., NeurIPS 2023) resuelve un problema concreto: fine-tunear un modelo sin tener la VRAM para cargar sus pesos en BF16, sus gradientes y los estados del optimizador. La idea tiene tres piezas. &lt;strong>Una&lt;/strong>: congelar el base y cuantizarlo a 4-bit con un formato nuevo, &lt;strong>NF4&lt;/strong> (NormalFloat 4-bit), cuantil-óptimo para pesos que se distribuyen casi como una gaussiana. &lt;strong>Dos&lt;/strong>: no entrenar el base —ni un solo peso suyo se mueve—, sino un par de matrices LoRA pequeñas en BF16 enchufadas en paralelo; el gradiente fluye únicamente por ese adapter. &lt;strong>Tres&lt;/strong>: dos trucos de memoria, la &lt;em>doble cuantización&lt;/em> (cuantizar las propias constantes de cuantización) y los &lt;em>paged optimizers&lt;/em> (estados del optimizador que se paginan a RAM cuando la VRAM aprieta). El resultado operacional medible: un SLM de 3-8B se fine-tunea en una &lt;strong>RTX 4090 (24 GB, Ada Lovelace)&lt;/strong>, no en un cluster. Y como el producto del entrenamiento es un adapter de &lt;strong>megabytes, no gigabytes&lt;/strong>, el patrón que emerge es un único SLM base congelado en 4-bit más N adapters —uno por cliente, dominio o tarea—, servidos sobre la base compartida con el stack que ya cubrimos en multi-LoRA serving. Aislamiento por cliente, footprint mínimo, despliegue soberano.&lt;/p>
&lt;h2 id="la-analogía-la-guitarra-congelada-y-la-pedalera-intercambiable">La analogía: la guitarra congelada y la pedalera intercambiable&lt;/h2>
&lt;p>Piensa en un guitarrista de estudio que graba para clientes muy distintos: un disco de jazz, una sintonía corporativa, un tema de metal. Tiene &lt;strong>una sola guitarra&lt;/strong> —su instrumento de confianza, afinado, con un sonido base que conoce de memoria—. Lo que &lt;strong>no&lt;/strong> hace es comprarse una guitarra nueva para cada canción. Lo que hace es tener una &lt;strong>pedalera de efectos&lt;/strong>: un pedal de distorsión, uno de chorus, uno de delay. Para cada tema enchufa el pedal que toca, y la misma guitarra suena completamente distinta.&lt;/p>
&lt;p>El mapeo es exacto:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>La guitarra&lt;/strong> = el SLM base. Una sola copia, afinada de fábrica, &lt;strong>congelada&lt;/strong>. En QLoRA, además, está &lt;em>guardada en una funda comprimida&lt;/em>: cuantizada a 4-bit. No la tocas: ni cambias sus pastillas ni reajustas el mástil. Pesa lo que pesa y ahí se queda.&lt;/li>
&lt;li>&lt;strong>Cada pedal&lt;/strong> = un adapter LoRA. Pequeño, barato, específico de un sonido. Lo entrenas para una tarea y lo guardas en un cajón.&lt;/li>
&lt;li>&lt;strong>Entrenar QLoRA&lt;/strong> = diseñar un pedal nuevo escuchando la guitarra (congelada) a través de él, ajustando solo los potenciómetros del pedal hasta que suene como quieres. El sonido base de la guitarra no se modifica; aprendes la &lt;strong>corrección&lt;/strong> que el pedal aplica encima.&lt;/li>
&lt;li>&lt;strong>Servir multi-LoRA&lt;/strong> (&lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a>) = tener toda la pedalera montada en el escenario y elegir el pedal correcto &lt;strong>por nota&lt;/strong> —por request—. La guitarra es la misma; lo que cambia entre requests es qué pedal está activo.&lt;/li>
&lt;/ul>
&lt;p>La analogía aguanta hasta el detalle que más confunde: &lt;strong>el gradiente del entrenamiento solo &amp;ldquo;toca&amp;rdquo; el pedal&lt;/strong>. La guitarra está congelada en su funda comprimida; el aprendizaje no la mueve. Eso es lo que permite que el base viva en 4-bit durante todo el fine-tuning sin que la cuantización estorbe: nunca se le calcula gradiente.&lt;/p>
&lt;h2 id="el-mecanismo-desnudo-lora-y-por-qué-se-puede-entrenar-sobre-un-base-4-bit">El mecanismo desnudo: LoRA, y por qué se puede entrenar sobre un base 4-bit&lt;/h2>
&lt;p>Recordatorio mínimo de LoRA (Hu et al., ICLR 2022). Un adapter modifica una matriz &lt;code>W&lt;/code> del base sumándole un producto de bajo rango:&lt;/p>
&lt;p>$$W&amp;rsquo; = W + B A, \qquad A \in \mathbb{R}^{r \times d}, \quad B \in \mathbb{R}^{d \times r}$$&lt;/p>
&lt;p>con &lt;code>r&lt;/code> el &lt;strong>rank&lt;/strong>, mucho menor que &lt;code>d&lt;/code>. En el forward pass no se materializa &lt;code>BA&lt;/code>; se calcula:&lt;/p>
&lt;p>$$y = W x + B(A x)$$&lt;/p>
&lt;p>El cómputo del base (&lt;code>Wx&lt;/code>) ocurre igual; el adapter añade dos matmuls baratos. La clave de QLoRA está en quién recibe gradiente. El base &lt;code>W&lt;/code> está &lt;strong>congelado&lt;/strong>: &lt;code>∂L/∂W&lt;/code> no se calcula ni se almacena. Solo &lt;code>A&lt;/code> y &lt;code>B&lt;/code> son entrenables. Por eso &lt;code>W&lt;/code> puede vivir cuantizado a 4-bit sin problema: en el forward se &lt;em>deshace&lt;/em> la cuantización al vuelo para hacer &lt;code>Wx&lt;/code> (dequant → matmul en BF16), pero como &lt;code>W&lt;/code> nunca se actualiza, no necesita la precisión de un peso entrenable. El adapter &lt;code>A, B&lt;/code> sí está en BF16, y es el único camino por el que fluye el gradiente.&lt;/p>
&lt;p>Esto es lo que rompe el muro de memoria. En un fine-tuning completo necesitas, por cada peso: el peso (2 bytes BF16), su gradiente (2 bytes), y los dos estados de Adam (momento y varianza, típicamente 4+4 bytes en FP32) — del orden de &lt;strong>12-16 bytes por parámetro entrenable&lt;/strong>. Con QLoRA, los pesos del base ocupan &lt;strong>0.5 bytes&lt;/strong> (4-bit) y &lt;strong>no tienen&lt;/strong> ni gradiente ni estados de optimizador. Solo los pocos millones de parámetros del adapter pagan el coste de 16 bytes. Para un 8B, eso es la diferencia entre ~130 GB y caber en 24 GB.&lt;/p>
&lt;h3 id="nf4-por-qué-un-formato-nuevo-en-lugar-de-int4">NF4: por qué un formato nuevo en lugar de INT4&lt;/h3>
&lt;p>QLoRA no usa INT4 lineal para el base, sino &lt;strong>NF4 (NormalFloat 4-bit)&lt;/strong>. La intuición: los pesos de un transformer entrenado se distribuyen, empíricamente, muy cerca de una &lt;strong>gaussiana centrada en cero&lt;/strong>. INT4 reparte sus 16 niveles de forma uniforme en el rango, lo que desperdicia niveles en las colas (donde casi no hay pesos) y deja pocos en el centro (donde se amontonan). NF4 reparte los 16 niveles según los &lt;strong>cuantiles&lt;/strong> de una normal: más niveles donde hay más masa de probabilidad. Es, por construcción, &lt;em>information-theoretically optimal&lt;/em> para datos exactamente gaussianos —cada nivel cubre aproximadamente la misma cantidad de pesos—. Además es &lt;strong>simétrico respecto al cero&lt;/strong> y garantiza una representación exacta del 0 (importante para sparsity y padding). El detalle de los formatos de cuantización está en &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization para inferencia LLM&lt;/a>; aquí basta con la idea de que NF4 gasta sus bits donde están los pesos.&lt;/p>
&lt;h3 id="doble-cuantización-y-paged-optimizers">Doble cuantización y paged optimizers&lt;/h3>
&lt;p>Cuantizar a 4-bit no es gratis del todo: necesitas guardar, por cada bloque de pesos (típicamente 64), una &lt;strong>constante de escala&lt;/strong> en FP32 para poder deshacer la cuantización. Esas constantes pesan. Con bloques de 64 y una escala FP32 (32 bits) por bloque, son &lt;code>32/64 = 0.5 bits por parámetro&lt;/code> solo en metadatos — un 12.5 % de overhead sobre los 4 bits útiles. La &lt;strong>doble cuantización&lt;/strong> ataca eso: cuantiza las propias constantes de escala (a 8-bit, en bloques de 256), bajando el overhead a ~&lt;code>0.127 bits/param&lt;/code>. Cuantizar la cuantización suena recursivo y lo es; el ahorro es pequeño en términos absolutos (~0.37 bits/param) pero en un 8B son cientos de MB, que es exactamente el margen que separa &amp;ldquo;cabe&amp;rdquo; de &amp;ldquo;no cabe&amp;rdquo; en una 4090.&lt;/p>
&lt;p>Los &lt;strong>paged optimizers&lt;/strong> atacan los picos de memoria. Durante el entrenamiento, ciertos momentos —un batch con secuencia muy larga, una activación grande— hacen que la VRAM se acerque al límite y reviente con un OOM. La idea, prestada del paging de los sistemas operativos, es alojar los estados del optimizador en memoria &lt;em>unificada&lt;/em> NVIDIA: cuando la VRAM aprieta, esas páginas se &lt;strong>expulsan a la RAM del host&lt;/strong> automáticamente y se traen de vuelta cuando hacen falta. No acelera nada; &lt;strong>evita el crash&lt;/strong> en los picos. Convierte un &amp;ldquo;OOM intermitente&amp;rdquo; en &amp;ldquo;un poco más lento en los peores momentos&amp;rdquo;, que para un entrenamiento desatendido en una sola GPU es la diferencia entre terminar y no terminar.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="QLoRA: base 4-bit congelado, adapter BF16 y el gradiente fluyendo solo por el adapter">
&lt;defs>&lt;marker id="qm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;marker id="qg" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#a52a2a"/>&lt;/marker>&lt;/defs>
&lt;p>&lt;text x="20" y="22" font-family="sans-serif" font-size="13" font-weight="600" fill="currentColor">Forward (azul) hacia delante · Gradiente (rojo) solo por el adapter&lt;/text>&lt;/p>
&lt;rect x="30" y="120" width="90" height="44" rx="6" fill="#eef2f6" stroke="currentColor" stroke-width="1.4"/>
&lt;text x="75" y="140" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="currentColor">x&lt;/text>
&lt;text x="75" y="156" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#555">entrada&lt;/text>
&lt;rect x="200" y="60" width="200" height="60" rx="8" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.6"/>
&lt;text x="300" y="84" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">W · x (base congelado)&lt;/text>
&lt;text x="300" y="102" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">NF4 4-bit · dequant al vuelo · SIN gradiente&lt;/text>
&lt;rect x="200" y="170" width="200" height="84" rx="8" fill="#fff4d6" stroke="#a48000" stroke-width="1.6"/>
&lt;text x="300" y="192" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#7a5e00">Adapter LoRA (BF16)&lt;/text>
&lt;rect x="220" y="202" width="74" height="40" rx="5" fill="#fffbe9" stroke="#a48000" stroke-width="1.2"/>
&lt;text x="257" y="220" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#7a5e00">A: r×d&lt;/text>
&lt;text x="257" y="234" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#7a5e00">shrink d→r&lt;/text>
&lt;rect x="306" y="202" width="74" height="40" rx="5" fill="#fffbe9" stroke="#a48000" stroke-width="1.2"/>
&lt;text x="343" y="220" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#7a5e00">B: d×r&lt;/text>
&lt;text x="343" y="234" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#7a5e00">expand r→d&lt;/text>
&lt;circle cx="500" cy="150" r="26" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.6"/>
&lt;text x="500" y="148" text-anchor="middle" font-family="sans-serif" font-size="15" font-weight="600" fill="#1d5a2e">+&lt;/text>
&lt;text x="500" y="163" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#1d5a2e">suma&lt;/text>
&lt;rect x="600" y="128" width="90" height="44" rx="6" fill="#eef2f6" stroke="currentColor" stroke-width="1.4"/>
&lt;text x="645" y="148" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="currentColor">y&lt;/text>
&lt;text x="645" y="164" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#555">salida&lt;/text>
&lt;path d="M120,142 L195,92" stroke="#1f5fa8" stroke-width="1.6" fill="none" marker-end="url(#qm)"/>
&lt;path d="M120,144 L195,205" stroke="#1f5fa8" stroke-width="1.6" fill="none" marker-end="url(#qm)"/>
&lt;path d="M400,90 L476,140" stroke="#1f5fa8" stroke-width="1.6" fill="none" marker-end="url(#qm)"/>
&lt;path d="M400,210 L476,162" stroke="#1f5fa8" stroke-width="1.6" fill="none" marker-end="url(#qm)"/>
&lt;path d="M526,150 L598,150" stroke="#1f5fa8" stroke-width="1.6" fill="none" marker-end="url(#qm)"/>
&lt;path d="M495,178 C470,250 410,258 386,256" stroke="#a52a2a" stroke-width="1.8" fill="none" stroke-dasharray="6 3" marker-end="url(#qg)"/>
&lt;text x="430" y="290" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#a52a2a">∂L/∂A , ∂L/∂B — el gradiente solo entra al adapter&lt;/text>
&lt;text x="300" y="50" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#1f5fa8">el base NO recibe gradiente: por eso puede vivir en 4-bit&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="entrenamiento-agresivo-rank-muy-bajo-y-qa-lora">Entrenamiento &amp;ldquo;agresivo&amp;rdquo;: rank muy bajo y QA-LoRA&lt;/h2>
&lt;p>&amp;ldquo;Agresivo&amp;rdquo; en este contexto significa dos cosas, a veces combinadas.&lt;/p>
&lt;p>&lt;strong>Rank muy bajo (r = 4-8).&lt;/strong> El rank es el cuello de la corrección: cuánta &amp;ldquo;capacidad&amp;rdquo; tiene el adapter para desviar al base. Un rank alto (64, 128) acerca el adapter a un fine-tuning completo pero pesa más y tarda más en entrenar. Para un SLM adaptado a una tarea &lt;strong>estrecha y bien definida&lt;/strong> —un formato de salida, un dominio léxico, un estilo de respuesta—, un rank de 4-8 suele bastar, y el adapter resultante pesa una fracción. El riesgo del rank bajo es el &lt;em>underfitting&lt;/em>: si la tarea exige reescribir mucho comportamiento del base, r=4 se queda corto. La regla honesta es empírica: sube el rank solo si el eval lo pide, no &amp;ldquo;por si acaso&amp;rdquo;. En SLMs pequeños, donde la base tiene menos capacidad de sobra, el rank bajo tiende a funcionar mejor proporcionalmente que en modelos grandes, pero esto depende de la tarea y hay que medirlo, no asumirlo.&lt;/p>
&lt;p>&lt;strong>QA-LoRA (quantization-aware LoRA, Xu et al., arXiv:2309.14717).&lt;/strong> Hay una fricción sutil en QLoRA estándar: entrenas el adapter en BF16 contra un base 4-bit, pero si luego quieres &lt;strong>fusionar&lt;/strong> el adapter en el base (&lt;code>W' = W + BA&lt;/code>) para servir un modelo cuantizado limpio, la fusión reintroduce precisión que el formato 4-bit no puede representar, y al recuantizar pierdes parte de lo aprendido. QA-LoRA entrena el adapter siendo &lt;strong>consciente de la cuantización del destino&lt;/strong>: equilibra los grados de libertad de la cuantización y de la adaptación (con cuantización por grupos) de modo que, al terminar, el adapter se &lt;strong>fusiona limpio&lt;/strong> en un base cuantizado sin un paso de recuantización que degrade. El resultado es un modelo final cuantizado-más-adaptado, sin adapter separado en runtime, útil cuando quieres un único artefacto desplegable por tarea en lugar del patrón base-compartido + adapters. La elección entre &amp;ldquo;QLoRA + servir multi-adapter&amp;rdquo; y &amp;ldquo;QA-LoRA + fusionar por tarea&amp;rdquo; es una decisión de arquitectura de despliegue, no de calidad pura.&lt;/p>
&lt;h2 id="la-matemática-que-importa">La matemática que importa&lt;/h2>
&lt;p>Tres cuentas mueven cualquier decisión con QLoRA sobre SLMs.&lt;/p>
&lt;p>&lt;strong>Parámetros del adapter.&lt;/strong> Para cada matriz objetivo de dimensión &lt;code>d&lt;/code> con rank &lt;code>r&lt;/code>, el adapter aporta &lt;code>A&lt;/code> (r×d) más &lt;code>B&lt;/code> (d×r), es decir &lt;code>2·r·d&lt;/code> parámetros. Sumando sobre las matrices objetivo y multiplicando por el número de capas:&lt;/p>
&lt;p>$$\text{params}&lt;em>{\text{adapter}} = L \cdot \sum&lt;/em>{\text{matrices}} 2 \cdot r \cdot d$$&lt;/p>
&lt;p>&lt;strong>Ejemplo trabajado — Llama-3-8B, atención (q, k, v, o), &lt;code>d = 4096&lt;/code>, &lt;code>L = 32&lt;/code> capas, &lt;code>r = 8&lt;/code>.&lt;/strong> Tomando las cuatro proyecciones de atención con la misma &lt;code>d = 4096&lt;/code> (simplificación; en Llama-3 K y V son más estrechas por GQA, lo que da menos params aún):&lt;/p>
&lt;p>$$\text{params} \approx 32 \cdot 4 \cdot (2 \cdot 8 \cdot 4096) = 32 \cdot 4 \cdot 65,536 \approx 8.4\text{M params}$$&lt;/p>
&lt;p>En BF16 (2 bytes/param): &lt;code>8.4M · 2 ≈ 16.8 MB ≈ ~17 MB&lt;/code>. &lt;strong>Diecisiete megabytes.&lt;/strong> Compáralo con el base: un 8B en NF4 ocupa &lt;code>8\text{G} · 0.5\,\text{bytes} ≈ 4\text{ GB}&lt;/code> (más el pequeño overhead de constantes tras doble cuantización). El adapter es el &lt;strong>0.4 %&lt;/strong> del tamaño del base cuantizado. Esto es lo que hace operacionalmente trivial tener cientos: un adapter no es un modelo, es casi un fichero de configuración pesado.&lt;/p>
&lt;p>&lt;strong>¿Cuántos adapters caben en una 4090 tras el base + KV?&lt;/strong> Presupuesto de una RTX 4090 (24 GB): base 8B NF4 ~4 GB, dejemos ~5 GB para KV cache y activaciones de inferencia con concurrencia moderada → quedan &lt;strong>~15 GB libres&lt;/strong> (siendo conservadores, llamémoslos ~12-15 GB). Con adapters de ~17 MB (r=8, attention-only):&lt;/p>
&lt;p>$$\frac{15,000\ \text{MB}}{17\ \text{MB/adapter}} \approx 880 \text{ adapters}$$&lt;/p>
&lt;p>Del orden de &lt;strong>miles&lt;/strong> si bajas el KV cache reservado o usas rank 4 (~8.5 MB/adapter → ~1750 en 15 GB). El cuello de botella nunca es el espacio de los adapters; es el KV cache y la concurrencia. Para los detalles de cómo se sirven concurrentemente esos miles —el batching heterogéneo, el unified paging, los kernels SGMV— ver &lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a>. El resumen relevante aquí: &lt;strong>el compute del adapter es casi gratis&lt;/strong> (rango bajo, dos matmuls finos); el reto de rendimiento del serving no es ese compute sino el &lt;em>gather/scatter&lt;/em> de los adapters correctos por fila del batch cuando un mismo batch mezcla requests de adapters distintos. Eso es problema del consumidor, no del productor.&lt;/p>
&lt;p>&lt;strong>VRAM de entrenamiento QLoRA en 24 GB.&lt;/strong> El presupuesto aproximado para fine-tunear el 8B en una 4090:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Componente&lt;/th>
&lt;th>VRAM aprox.&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Base 8B en NF4 (pesos congelados)&lt;/td>
&lt;td>~4.0 GB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Adapter (params BF16 + gradiente + estados Adam, ~16 B/param sobre ~8-40M params)&lt;/td>
&lt;td>~0.3-0.7 GB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Activaciones (depende de batch y longitud de secuencia; el grueso variable)&lt;/td>
&lt;td>~6-14 GB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Buffers de dequant, escalas, workspace&lt;/td>
&lt;td>~1-2 GB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Total&lt;/strong>&lt;/td>
&lt;td>&lt;strong>cabe en 24 GB con margen&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La pieza grande y variable son las &lt;strong>activaciones&lt;/strong>, que escalan con batch × longitud de secuencia. Por eso el QLoRA real en una 4090 se hace con batch pequeño + &lt;em>gradient accumulation&lt;/em> (simular batch grande acumulando gradientes de microbatches) + &lt;em>gradient checkpointing&lt;/em> (recomputar activaciones en backward en lugar de guardarlas, cambiando compute por memoria) + secuencias acotadas. Los &lt;strong>paged optimizers&lt;/strong> son el airbag para los picos de activación que, sin ellos, reventarían. La afirmación &amp;ldquo;QLoRA fine-tunea un 8B en una 4090&amp;rdquo; es cierta &lt;strong>con esa configuración&lt;/strong>; sin gradient checkpointing y con secuencias largas y batch grande, no cabe. Como con cualquier número, la metodología importa más que el titular.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Un SLM base compartido en 4-bit con N adapters por cliente, batching heterogéneo">
&lt;defs>&lt;marker id="bm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;p>&lt;text x="20" y="22" font-family="sans-serif" font-size="13" font-weight="600" fill="currentColor">Batch heterogéneo: 4 requests, 3 clientes, 3 adapters — un solo SLM base compartido&lt;/text>&lt;/p>
&lt;rect x="20" y="40" width="120" height="36" rx="5" fill="#f6e0c8" stroke="#a76b1f" stroke-width="1.3"/>
&lt;text x="80" y="63" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#7a4d12">req_1 → cliente A&lt;/text>
&lt;rect x="150" y="40" width="120" height="36" rx="5" fill="#f6e0c8" stroke="#a76b1f" stroke-width="1.3"/>
&lt;text x="210" y="63" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#7a4d12">req_2 → cliente A&lt;/text>
&lt;rect x="280" y="40" width="120" height="36" rx="5" fill="#f6e0c8" stroke="#a76b1f" stroke-width="1.3"/>
&lt;text x="340" y="63" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#7a4d12">req_3 → cliente B&lt;/text>
&lt;rect x="410" y="40" width="120" height="36" rx="5" fill="#f6e0c8" stroke="#a76b1f" stroke-width="1.3"/>
&lt;text x="470" y="63" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#7a4d12">req_4 → cliente C&lt;/text>
&lt;rect x="20" y="110" width="510" height="50" rx="8" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.6"/>
&lt;text x="275" y="132" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">SLM BASE — Llama-3-8B NF4 (~4 GB) — cargado UNA vez, compartido&lt;/text>
&lt;text x="275" y="150" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">W·x se calcula igual para los 4 requests, sin importar el adapter&lt;/text>
&lt;p>&lt;text x="560" y="100" font-family="sans-serif" font-size="11" font-weight="600" fill="currentColor">Pedalera&lt;/text>
&lt;text x="560" y="114" font-family="sans-serif" font-size="10" fill="#555">(adapters ~17 MB)&lt;/text>
&lt;rect x="560" y="120" width="200" height="120" rx="6" fill="#fff4d6" stroke="#a48000" stroke-width="1.4"/>
&lt;rect x="572" y="132" width="176" height="22" rx="3" fill="#fffbe9" stroke="#a48000" stroke-width="1"/>&lt;text x="660" y="147" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#7a5e00">adapter A (cliente A)&lt;/text>
&lt;rect x="572" y="160" width="176" height="22" rx="3" fill="#fffbe9" stroke="#a48000" stroke-width="1"/>&lt;text x="660" y="175" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#7a5e00">adapter B (cliente B)&lt;/text>
&lt;rect x="572" y="188" width="176" height="22" rx="3" fill="#fffbe9" stroke="#a48000" stroke-width="1"/>&lt;text x="660" y="203" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#7a5e00">adapter C (cliente C)&lt;/text>
&lt;text x="660" y="228" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#7a5e00">&amp;hellip; miles más, MB cada uno&lt;/text>&lt;/p>
&lt;path d="M80,76 L200,108" stroke="#666" stroke-width="1.3" fill="none" marker-end="url(#bm)"/>
&lt;path d="M210,76 L240,108" stroke="#666" stroke-width="1.3" fill="none" marker-end="url(#bm)"/>
&lt;path d="M340,76 L300,108" stroke="#666" stroke-width="1.3" fill="none" marker-end="url(#bm)"/>
&lt;path d="M470,76 L360,108" stroke="#666" stroke-width="1.3" fill="none" marker-end="url(#bm)"/>
&lt;p>&lt;text x="20" y="200" font-family="sans-serif" font-size="11" fill="#555">El delta del adapter se aplica por fila del batch:&lt;/text>
&lt;text x="20" y="216" font-family="sans-serif" font-size="11" fill="#555">reqs 1-2 → adapter A · req 3 → adapter B · req 4 → adapter C&lt;/text>
&lt;text x="20" y="232" font-family="sans-serif" font-size="11" font-weight="600" fill="#a52a2a">El reto NO es el compute del delta (casi gratis) — es el gather/scatter heterogéneo.&lt;/text>
&lt;text x="20" y="270" font-family="sans-serif" font-size="10" fill="#555">Internals (SGMV, unified paging, batching heterogéneo): ver Multi-LoRA serving.&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;h2 id="el-encaje-con-modelos-pequeños-y-la-soberanía">El encaje con modelos pequeños y la soberanía&lt;/h2>
&lt;p>Aquí es donde QLoRA + SLM deja de ser un truco de VRAM y se vuelve un patrón de arquitectura.&lt;/p>
&lt;p>Un SLM (3-8B) ya cabe holgado en una sola GPU para inferencia. Si encima el base vive en 4-bit (~4 GB para un 8B), te sobra memoria. Lo que QLoRA habilita es que ese mismo equipo —la 4090— sea &lt;strong>tanto el productor como el consumidor&lt;/strong>: entrenas el adapter de un cliente nuevo en horas, en la misma clase de hardware donde luego lo sirves. El artefacto que circula entre &amp;ldquo;entrenar&amp;rdquo; y &amp;ldquo;desplegar&amp;rdquo; es un adapter de &lt;strong>MB, no GB&lt;/strong>: se versiona, se firma, se mueve por la red, se almacena en MinIO/S3 sin pensar en el coste.&lt;/p>
&lt;p>El patrón soberano se cae por su propio peso:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Aislamiento por cliente.&lt;/strong> Cada cliente tiene su adapter, entrenado solo con sus datos. El base es genérico y compartido; lo específico del cliente vive aislado en su par &lt;code>(A, B)&lt;/code>. Borrar un cliente es borrar un fichero de MB, no reentrenar nada.&lt;/li>
&lt;li>&lt;strong>Footprint mínimo.&lt;/strong> Un base + N adapters cabe donde N bases no cabrían ni de lejos. La economía de &amp;ldquo;un modelo por cliente&amp;rdquo; (decenas de GB cada uno) es prohibitiva; la de &amp;ldquo;un base + adapters&amp;rdquo; (MB cada uno) es trivial. Es exactamente la diferencia entre la pedalera y comprar una guitarra por canción.&lt;/li>
&lt;li>&lt;strong>Despliegue soberano.&lt;/strong> Todo cabe on-premise, en tu hardware, sin sacar un dato del perímetro. El entrenamiento (QLoRA en la 4090) y el serving (multi-LoRA sobre el mismo base) viven dentro. No hay dependencia de una API externa para fine-tunear ni para servir.&lt;/li>
&lt;/ul>
&lt;p>La elección de &lt;strong>adaptar por dominio&lt;/strong> (un adapter por área de conocimiento) frente a &lt;strong>recuperar por contexto&lt;/strong> (RAG que inyecta el conocimiento en el prompt) es real y no excluyente: el adapter cambia el &lt;em>comportamiento&lt;/em> y el &lt;em>estilo&lt;/em> del modelo, el RAG cambia los &lt;em>hechos&lt;/em> a los que accede. Lo trabaja el post hermano de &lt;strong>RAG agresivo en modelos pequeños&lt;/strong> de esta serie; la regla corta es: adapta lo que es estable y conductual, recupera lo que es volátil y factual.&lt;/p>
&lt;h2 id="aplicado-a-la-infraestructura-on-premise">Aplicado a la infraestructura on-premise&lt;/h2>
&lt;h3 id="en-una-rtx-4090-24-gb-ada-lovelace">En una RTX 4090 (24 GB, Ada Lovelace)&lt;/h3>
&lt;p>Es el banco de trabajo natural de QLoRA. Caso canónico: &lt;strong>base SLM 3-8B en NF4, fine-tuning de un adapter r=8 attention-only&lt;/strong>, con gradient checkpointing + gradient accumulation + paged optimizer. Entrena en horas para datasets de tarea estrecha (miles a decenas de miles de ejemplos), y el mismo equipo sirve después el base + decenas o cientos de adapters para demos multi-tenant y prototipos de plataforma. La 4090 es donde QLoRA pasó de &amp;ldquo;técnica de paper&amp;rdquo; a &amp;ldquo;lo puede hacer cualquiera con una GPU de consumo&amp;rdquo;, y ese es exactamente su valor. La regla honesta: cabe &lt;strong>con&lt;/strong> la configuración de memoria descrita; con secuencias largas, batch grande o rank alto, sube el hardware.&lt;/p>
&lt;h3 id="en-un-cluster-genérico-4h100-sxm-320-gb-nvlink-fp8-nativo">En un cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo)&lt;/h3>
&lt;p>Aquí QLoRA deja de ser estrictamente necesario para &lt;em>caber&lt;/em> —un 8B en BF16 entra de sobra— pero sigue siendo útil por otra razón: &lt;strong>paralelizar la producción de adapters&lt;/strong>. Con 320 GB y FP8 nativo puedes entrenar varios adapters a la vez (un job por cliente, varios en paralelo), o fine-tunear modelos algo mayores con QLoRA sin TP. El consumidor en este cluster es el setup serio de &lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a>: base FP8 + cientos de adapters concurrentes. La regla de pulgar: en la 4090, QLoRA es la herramienta para &lt;em>poder&lt;/em> fine-tunear; en el cluster H100, es la herramienta para fine-tunear &lt;em>muchos a la vez, barato&lt;/em>, manteniendo el formato cuantizado consistente entre entrenamiento y serving.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Los internals del serving heterogéneo&lt;/strong> (kernels SGMV, MBGMM/MBGMV, unified paging, cold start, eviction): están enteros en &lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a>. Este post es deliberadamente el lado del productor.&lt;/li>
&lt;li>&lt;strong>DoRA y variantes&lt;/strong> (descomposición magnitud-dirección): cierran parte del gap con el full fine-tuning; patrón de entrenamiento distinto, patrón de serving idéntico.&lt;/li>
&lt;li>&lt;strong>Cuantización sub-4-bit y ternaria del base&lt;/strong>: qué pasa cuando el base baja de NF4 a 2-bit o ternario bajo el adapter; lo trabaja el post hermano de la serie.&lt;/li>
&lt;li>&lt;strong>Recolección del dataset de fine-tuning&lt;/strong>: cómo se construye el corpus de cada adapter a partir de feedback de producción está en &lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle&lt;/a>.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a> — el consumidor: los internals de cómo se sirven miles de adapters concurrentes (SGMV, unified paging, batching heterogéneo). Léelo: este post da por sabido todo lo de serving.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization para inferencia LLM&lt;/a> — el marco de formatos (NF4, INT4, FP8, AWQ) que sostiene el base cuantizado bajo el adapter.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/knowledge-distillation-fundamentos/">Knowledge distillation&lt;/a> — la alternativa/complemento a adaptar: comprimir el conocimiento en el propio modelo en lugar de en un adapter encima.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">Fine-tuning continuo en producción&lt;/a> — el ciclo operacional que produce adapters nuevos de forma continua a partir de señal de producción.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle feedback → dataset → adapter&lt;/a> — de dónde sale el dataset con el que se entrena cada adapter QLoRA.&lt;/li>
&lt;li>&lt;strong>Roofline invertido en modelos pequeños&lt;/strong> (hermano de la serie) — el régimen de rendimiento donde un SLM se mueve, que explica por qué el footprint mínimo del adapter encaja con GPUs de consumo.&lt;/li>
&lt;li>&lt;strong>Cuantización agresiva sub-4-bit / ternaria&lt;/strong> (hermano de la serie) — qué pasa con el base cuantizado por debajo de NF4 bajo el adapter.&lt;/li>
&lt;li>&lt;strong>RAG agresivo en modelos pequeños&lt;/strong> (hermano de la serie) — adaptar por dominio (este post) frente a recuperar por contexto; cuándo cada uno.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Dettmers, T., Pagnoni, A., Holtzman, A., Zettlemoyer, L. &lt;em>QLoRA: Efficient Finetuning of Quantized LLMs&lt;/em>. NeurIPS 2023. &lt;a href="https://arxiv.org/abs/2305.14314">https://arxiv.org/abs/2305.14314&lt;/a>&lt;/li>
&lt;li>Hu, E., Shen, Y., Wallis, P., Allen-Zhu, Z., Li, Y., Wang, S., Wang, L., Chen, W. &lt;em>LoRA: Low-Rank Adaptation of Large Language Models&lt;/em>. ICLR 2022. &lt;a href="https://arxiv.org/abs/2106.09685">https://arxiv.org/abs/2106.09685&lt;/a>&lt;/li>
&lt;li>Xu, Y., Xie, L., Gu, X., Chen, X., Chang, H., Zhang, H., Chen, Z., Zhang, X., Tian, Q. &lt;em>QA-LoRA: Quantization-Aware Low-Rank Adaptation of Large Language Models&lt;/em>. ICLR 2024. &lt;a href="https://arxiv.org/abs/2309.14717">https://arxiv.org/abs/2309.14717&lt;/a>&lt;/li>
&lt;li>Sheng, Y. et al. &lt;em>S-LoRA: Serving Thousands of Concurrent LoRA Adapters&lt;/em>. MLSys 2024. &lt;a href="https://arxiv.org/abs/2311.03285">https://arxiv.org/abs/2311.03285&lt;/a>&lt;/li>
&lt;li>Chen, L. et al. &lt;em>Punica: Multi-Tenant LoRA Serving&lt;/em>. MLSys 2024. &lt;a href="https://arxiv.org/abs/2310.18547">https://arxiv.org/abs/2310.18547&lt;/a>&lt;/li>
&lt;li>Repo oficial QLoRA / bitsandbytes: &lt;a href="https://github.com/artidoro/qlora">https://github.com/artidoro/qlora&lt;/a>&lt;/li>
&lt;li>Hugging Face PEFT (LoRA, QLoRA): &lt;a href="https://github.com/huggingface/peft">https://github.com/huggingface/peft&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>RAG agresivo en modelos pequeños: compensar parámetros con recuperación</title><link>https://blog.lo0.es/posts/rag-agresivo-modelos-pequenos/</link><pubDate>Tue, 09 Jun 2026 02:20:00 +0000</pubDate><guid>https://blog.lo0.es/posts/rag-agresivo-modelos-pequenos/</guid><description>&lt;blockquote>
&lt;p>Este post pertenece a la serie sobre rendimiento de inferencia en modelos pequeños. Su pieza hermana, &lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">El roofline se invierte en modelos pequeños&lt;/a>, explica por qué el prefill compute-bound es el cuello de botella que aquí da forma a toda la discusión. Conviene leerlo antes: aquí asumimos que &lt;strong>meter más contexto no es gratis&lt;/strong>.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Un SLM (digamos 1B–8B de parámetros) sabe &lt;strong>menos hechos&lt;/strong> que un modelo de 70B–700B, simplemente porque tiene menos pesos donde memorizarlos. Pero su capacidad de &lt;strong>razonar sobre texto que tiene delante&lt;/strong> —seguir instrucciones, extraer, sintetizar, comparar— se degrada mucho menos con el tamaño que su conocimiento enciclopédico. La consecuencia operacional es directa: usa el SLM como &lt;strong>motor de razonamiento sobre contexto curado&lt;/strong>, no como base de datos. Mueve el conocimiento de los pesos al contexto vía recuperación. El problema es que &amp;ldquo;recuperación agresiva&amp;rdquo; se interpreta a menudo como &amp;ldquo;meter muchos chunks&amp;rdquo;, y eso choca de frente con tres hechos sobre los SLM: ventanas de contexto más cortas, peor aprovechamiento del contexto largo (el efecto &lt;em>lost in the middle&lt;/em> es más severo cuanto más pequeño el modelo) y un &lt;strong>prefill compute-bound&lt;/strong> cuyo coste crece con la longitud del contexto $C$ —lineal en las proyecciones, cuadrático en la atención—. No puedes simplemente añadir tokens. La salida no es recuperar menos, sino recuperar &lt;strong>mejor&lt;/strong>: reranking de precisión sobre recall, compresión de contexto antes de inyectarlo, prefix caching de los documentos estables, caché semántico de respuestas y structured output con herramientas externas que sustituyen al conocimiento interno. Este post trabaja las matemáticas y da un número de TTFT antes y después de comprimir un contexto de 4000 a 1000 tokens en una RTX 4090.&lt;/p>
&lt;h2 id="la-analogía-el-examen-a-libro-abierto">La analogía: el examen a libro abierto&lt;/h2>
&lt;p>Dos estudiantes se presentan al mismo examen. El primero tiene una memoria prodigiosa: ha memorizado el temario entero, párrafo a párrafo. El segundo tiene una memoria normal —olvida fechas, confunde nombres— pero le permiten entrar con una &lt;strong>chuleta&lt;/strong>.&lt;/p>
&lt;p>Si la chuleta del segundo estudiante es un caos de fotocopias amontonadas, pierde: tarda en encontrar lo que busca, se distrae con páginas irrelevantes y se le acaba el tiempo. Pero si su chuleta es &lt;strong>excelente&lt;/strong> —recortada a lo esencial, reordenada por relevancia, con lo importante arriba y sin paja—, entonces no solo no pierde: a menudo &lt;strong>gana&lt;/strong>, porque razona igual de bien que el primero y además trabaja sobre material verificado en lugar de sobre recuerdos borrosos que puede estar inventando.&lt;/p>
&lt;p>La moraleja tiene tres capas, y cada una mapea a una decisión de ingeniería:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Memorizarlo todo es caro.&lt;/strong> El primer estudiante invirtió meses. Un modelo grande invierte parámetros —y VRAM, y FLOPs de inferencia— en memorizar hechos.&lt;/li>
&lt;li>&lt;strong>La chuleta importa más que su tamaño.&lt;/strong> Una chuleta de una página bien hecha bate a diez páginas mal organizadas. Más contexto recuperado no es mejor contexto: la precisión del material gana al volumen.&lt;/li>
&lt;li>&lt;strong>Saber buscar y sintetizar es una habilidad distinta de saber.&lt;/strong> Es la que el SLM conserva. La estrategia entera consiste en apoyarse en esa habilidad y subcontratar la memoria.&lt;/li>
&lt;/ul>
&lt;p>El resto del post es, esencialmente, cómo construir una chuleta excelente bajo la restricción de que el estudiante (el SLM) lee despacio y se cansa con los textos largos.&lt;/p>
&lt;h2 id="el-argumento-de-capacidad-cuántos-hechos-caben-en-los-pesos">El argumento de capacidad: cuántos hechos caben en los pesos&lt;/h2>
&lt;p>Empecemos por justificar la tesis con orden de magnitud, no con fe. ¿Cuánto conocimiento factual cabe realmente en los pesos de un modelo?&lt;/p>
&lt;p>Hay una estimación empírica recurrente en la literatura de interpretabilidad y memorización: un modelo denso es capaz de almacenar del orden de &lt;strong>2 bits de información memorizada por parámetro&lt;/strong> antes de saturar (la cifra exacta varía según el estudio y el régimen de entrenamiento; tómese como orden de magnitud, no como ley). Un modelo de 8B parámetros tiene entonces un techo de almacenamiento de información del orden de:&lt;/p>
&lt;p>$$8 \times 10^9 \text{ params} \times 2 \text{ bits/param} = 1.6 \times 10^{10} \text{ bits} \approx 2 \text{ GB de información}$$&lt;/p>
&lt;p>Y ese presupuesto &lt;strong>no es solo para hechos&lt;/strong>: la inmensa mayoría se gasta en gramática, sintaxis, capacidad de razonamiento, código, formato, y solo una fracción queda para conocimiento enciclopédico. Compáralo con el otro lado: un corpus recuperable de varios millones de documentos —una wiki corporativa, un repositorio documental, una base de conocimiento técnica— ocupa fácilmente &lt;strong>cientos de GB a terabytes&lt;/strong> de texto, indexado y consultable con latencia de milisegundos. La asimetría es de &lt;strong>dos o tres órdenes de magnitud&lt;/strong> a favor del corpus externo.&lt;/p>
&lt;p>La conclusión no es que los pesos sean inútiles —son donde vive el razonamiento, que es lo caro de replicar— sino que &lt;strong>competir con un índice externo por capacidad de hechos es perder por construcción&lt;/strong>. Un modelo de 70B tiene ~9× más presupuesto de memorización que uno de 8B, pero sigue siendo despreciable frente al corpus. Por eso el modelo grande &lt;em>también&lt;/em> hace RAG en producción. La diferencia es que el SLM &lt;strong>lo necesita&lt;/strong>: sin recuperación, su conocimiento factual es demasiado escaso y, peor, &lt;strong>propenso a alucinar&lt;/strong> justo en los huecos que no memorizó.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 250" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Conocimiento en pesos frente a conocimiento en contexto">
&lt;text x="390" y="24" text-anchor="middle" font-size="15" font-weight="700" fill="currentColor">Dónde vive el conocimiento&lt;/text>
&lt;p>&lt;text x="200" y="58" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">En los pesos (memorizado)&lt;/text>
&lt;rect x="90" y="70" width="220" height="60" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4"/>
&lt;text x="200" y="95" text-anchor="middle" font-size="12" fill="#1f3550">~2 GB de info útil en 8B&lt;/text>
&lt;text x="200" y="113" text-anchor="middle" font-size="11" fill="#1f3550">fijo, caro de actualizar, alucina en huecos&lt;/text>&lt;/p>
&lt;p>&lt;text x="580" y="58" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">En el contexto (recuperado)&lt;/text>
&lt;rect x="430" y="70" width="300" height="60" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.4"/>
&lt;text x="580" y="95" text-anchor="middle" font-size="12" fill="#1c3a26">cientos de GB – TB indexados&lt;/text>
&lt;text x="580" y="113" text-anchor="middle" font-size="11" fill="#1c3a26">fresco, citable, verificable, sin reentrenar&lt;/text>&lt;/p>
&lt;p>&lt;text x="390" y="165" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">El SLM como motor de razonamiento&lt;/text>
&lt;rect x="240" y="178" width="300" height="46" fill="#fff4d6" stroke="#a48000" stroke-width="1.4"/>
&lt;text x="390" y="200" text-anchor="middle" font-size="12" fill="#5a4500">razona sobre el contexto curado&lt;/text>
&lt;text x="390" y="216" text-anchor="middle" font-size="11" fill="#5a4500">no es la base de datos: es quien la lee y sintetiza&lt;/text>&lt;/p>
&lt;path d="M200,130 L360,178" stroke="currentColor" stroke-width="1.2" fill="none"/>
&lt;path d="M580,130 L420,178" stroke="currentColor" stroke-width="1.2" fill="none"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-tensión-central-recuperar-más-no-es-meter-más">La tensión central: recuperar más no es meter más&lt;/h2>
&lt;p>Aquí es donde la mayoría de los diseños ingenuos se rompen. &amp;ldquo;Recuperación agresiva&amp;rdquo; suena a &lt;em>top-k&lt;/em> grande: si recuperar ayuda, recupera 20 chunks en vez de 5. Pero en un SLM eso falla por dos razones independientes, una de &lt;strong>calidad&lt;/strong> y otra de &lt;strong>coste&lt;/strong>.&lt;/p>
&lt;h3 id="a-los-slm-usan-peor-el-contexto-largo">(a) Los SLM usan peor el contexto largo&lt;/h3>
&lt;p>El efecto &lt;em>lost in the middle&lt;/em> (Liu et al., 2023) es bien conocido: los LLM recuperan mejor la información situada al principio y al final del contexto, y peor la del medio. Lo que se enfatiza menos es que &lt;strong>el efecto es más severo cuanto más pequeño el modelo&lt;/strong>. Un SLM tiene menos cabezas de atención, menos capas y representaciones internas más pobres para &amp;ldquo;rastrear&amp;rdquo; un hecho relevante enterrado en la posición 14 de 20 chunks. Además, su ventana de contexto nominal suele ser más corta (4K–32K frente a los 128K+ de los grandes), y la &lt;strong>ventana efectiva&lt;/strong> —la longitud a partir de la cual la calidad se desploma— es todavía menor. Meter 20 chunks no significa que el modelo los lea los 20: significa que probablemente ignore o malinterprete los del medio, mientras paga el coste de todos.&lt;/p>
&lt;h3 id="b-el-prefill-crece-con-el-contexto-y-es-compute-bound">(b) El prefill crece con el contexto y es compute-bound&lt;/h3>
&lt;p>Este es el golpe que la gente subestima. El &lt;strong>prefill&lt;/strong> —procesar el prompt completo antes de emitir el primer token— es la fase &lt;strong>compute-bound&lt;/strong> de la inferencia (a diferencia del decode, memory-bound; el detalle vive en &lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">El roofline se invierte&lt;/a>). Su coste &lt;strong>crece con la longitud del contexto&lt;/strong> $C$, y determina el &lt;strong>TTFT&lt;/strong> (time to first token). Más chunks → más tokens de prefill → más TTFT y más coste de cómputo por petición. En un SLM, donde el prefill es proporcionalmente más caro respecto al modelo, esto duele especialmente.&lt;/p>
&lt;p>La conclusión operacional es incómoda pero clara: &lt;strong>no puedes compensar menos parámetros simplemente metiendo más contexto.&lt;/strong> Cada token recuperado se paga dos veces —en calidad degradada y en TTFT— y el SLM es el peor situado para absorber ambos costes. La salida es recuperar &lt;strong>menos pero mejor&lt;/strong>, y &lt;strong>comprimir&lt;/strong> lo que recuperas.&lt;/p>
&lt;h2 id="las-matemáticas-del-prefill">Las matemáticas del prefill&lt;/h2>
&lt;p>Pongamos números a &amp;ldquo;el prefill crece con el contexto&amp;rdquo;. Para un contexto de $C$ tokens, una capa transformer hace dos clases de trabajo:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Proyecciones lineales&lt;/strong> (QKV, salida de atención, FFN): cada token se multiplica por matrices de pesos de tamaño fijo. El coste es $O(C)$ en FLOPs —lineal en el número de tokens.&lt;/li>
&lt;li>&lt;strong>Atención&lt;/strong> ($QK^\top$ y la multiplicación por $V$): cada token atiende a todos los demás. El coste es $O(C^2)$ —cuadrático en el número de tokens.&lt;/li>
&lt;/ol>
&lt;p>El coste total de prefill por capa es de la forma:&lt;/p>
&lt;p>$$\text{FLOPs}&lt;em>{\text{prefill}} \approx \underbrace{a \cdot C}&lt;/em>{\text{proyecciones}} + \underbrace{b \cdot C^2}_{\text{atención}}$$&lt;/p>
&lt;p>con $a$ y $b$ constantes que dependen de la dimensión del modelo. Para contextos moderados (unos pocos miles de tokens) en un SLM, el término lineal aún domina o es comparable al cuadrático; el término cuadrático se vuelve dominante a contextos largos. Lo relevante: &lt;strong>si comprimes el contexto&lt;/strong> $C \to C/k$, el término lineal cae $\times k$ y el cuadrático cae $\times k^2$. Comprimir es la única palanca que ataca &lt;strong>ambos&lt;/strong> términos a la vez, y ataca el peor de forma desproporcionada.&lt;/p>
&lt;h3 id="ejemplo-numérico-ttft-antes-y-después-de-comprimir-rtx-4090">Ejemplo numérico: TTFT antes y después de comprimir, RTX 4090&lt;/h3>
&lt;p>Modelemos el TTFT como el tiempo de procesar los tokens de prefill a un throughput de prefill dado. Tomemos una RTX 4090 (24 GB, Ada Lovelace) sirviendo un SLM cuantizado, con un &lt;strong>throughput de prefill de ~5000 tok/s&lt;/strong> (cifra ilustrativa; el valor real depende del modelo, la cuantización y el batch —mídelo, no lo asumas).&lt;/p>
&lt;p>Sea un contexto recuperado de &lt;strong>4000 tokens&lt;/strong> (8 chunks de ~500 tokens). Aproximando el TTFT como dominado por el prefill del contexto:&lt;/p>
&lt;p>$$\text{TTFT}_{\text{antes}} \approx \frac{4000 \text{ tok}}{5000 \text{ tok/s}} = 0.80 \text{ s}$$&lt;/p>
&lt;p>Ahora comprimimos ese contexto a &lt;strong>1000 tokens&lt;/strong> ($k = 4$). El throughput de prefill no es constante con $C$ —baja un poco a contextos largos por el término cuadrático— pero, tomando la aproximación lineal conservadora de tokens/throughput:&lt;/p>
&lt;p>$$\text{TTFT}_{\text{después}} \approx \frac{1000 \text{ tok}}{5000 \text{ tok/s}} = 0.20 \text{ s}$$&lt;/p>
&lt;p>El TTFT cae de &lt;strong>0.80 s a 0.20 s&lt;/strong>, una reducción de $4\times$ en la parte lineal. Pero la cuenta de FLOPs es más favorable todavía en la componente de atención: esa parte del trabajo cae $\sim k^2 = 16\times$. En la práctica el TTFT total no cae 16× porque el coste no es puramente cuadrático a esta escala, pero la reducción real está &lt;strong>entre 4× y un valor mayor según cuánto pesara la atención&lt;/strong>, y el ahorro de cómputo agregado (lo que paga la factura eléctrica y libera la GPU para otra petición) es sustancialmente mayor que el simple 4× del recuento de tokens.&lt;/p>
&lt;p>El argumento se generaliza: &lt;strong>comprimir el contexto un factor $k$ reduce el TTFT al menos $\sim k\times$ y el coste de atención $\sim k^2\times$.&lt;/strong> Para un SLM, donde el TTFT es a menudo el SLA que importa, esto es la diferencia entre un asistente que responde al instante y uno que se siente lento.&lt;/p>
&lt;h2 id="las-cinco-palancas-para-resolver-la-tensión">Las cinco palancas para resolver la tensión&lt;/h2>
&lt;p>La estrategia no es &amp;ldquo;recuperar menos y conformarse&amp;rdquo;. Es &lt;strong>recuperar agresivamente del índice y luego destilar agresivamente lo recuperado&lt;/strong> antes de que llegue al SLM. Cinco palancas, en orden de aplicación dentro del pipeline.&lt;/p>
&lt;h3 id="1-reranking-agresivo-precisión-sobre-recall">1. Reranking agresivo: precisión sobre recall&lt;/h3>
&lt;p>El retriever inicial (denso, sparse o híbrido) optimiza &lt;strong>recall&lt;/strong>: trae 50–100 candidatos para no dejarse nada fuera. El reranker —un cross-encoder que ve la query y el documento juntos— optimiza &lt;strong>precisión&lt;/strong>: reordena esos candidatos y te quedas con los &lt;strong>3–5 mejores&lt;/strong>. Para un SLM esto no es un lujo, es estructural: como el modelo usa mal el contexto largo, cada chunk que entra debe &lt;strong>ganarse su sitio&lt;/strong>. Mejor 4 chunks de altísima relevancia que 15 mediocres. El detalle de retrieval híbrido y reranking está en &lt;a href="#ver-tambi%C3%A9n">Reranking e hybrid retrieval&lt;/a>; aquí basta con la regla: &lt;strong>maximiza recall en el retriever, maximiza precisión en el reranker, e inyecta pocos&lt;/strong>.&lt;/p>
&lt;h3 id="2-compresión-de-contexto-destilar-la-chuleta">2. Compresión de contexto: destilar la chuleta&lt;/h3>
&lt;p>Una vez tienes los mejores chunks, todavía contienen paja —frases de relleno, redundancia, contexto irrelevante a la query concreta. La &lt;strong>compresión de contexto&lt;/strong> los recorta antes de inyectarlos:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Compresión extractiva&lt;/strong> (estilo LLMLingua / LongLLMLingua, Jiang et al. 2023): un modelo pequeño puntúa la &lt;em>perplejidad&lt;/em> o relevancia de cada token/frase respecto a la query y &lt;strong>elimina&lt;/strong> los de baja información, quedándose con el subconjunto extractivo más denso. Reduce tokens sin un segundo modelo generativo grande de por medio. LongLLMLingua añade reordenación consciente de la posición para mitigar &lt;em>lost in the middle&lt;/em>.&lt;/li>
&lt;li>&lt;strong>Compresión abstractiva&lt;/strong>: un modelo resume los chunks recuperados en un texto más corto. Más agresiva en reducción de tokens, pero introduce un paso generativo (coste y posible pérdida de fidelidad).&lt;/li>
&lt;li>&lt;strong>Soft prompts / context distillation&lt;/strong>: comprimir el contexto recuperado no a texto, sino a un puñado de &lt;strong>embeddings/soft tokens&lt;/strong> que el modelo consume directamente. Reduce el número de tokens de prefill al mínimo, a costa de un componente entrenado y específico del modelo.&lt;/li>
&lt;/ul>
&lt;p>El punto clave conecta con las matemáticas de arriba: &lt;strong>comprimir lo recuperado un factor $k$ reduce los tokens de prefill $\times k$, y por tanto el TTFT $\sim\times k$ y el coste de atención $\sim\times k^2$.&lt;/strong> Es la palanca con mejor retorno cuando el contexto largo es el cuello de botella.&lt;/p>
&lt;h3 id="3-prefix-caching-del-contexto-estable">3. Prefix caching del contexto estable&lt;/h3>
&lt;p>No todo el contexto cambia entre peticiones. Instrucciones de sistema, definiciones, documentos de referencia recurrentes, esquemas: son &lt;strong>prefijos estables&lt;/strong>. El &lt;strong>prefix caching&lt;/strong> guarda el KV cache ya computado de esos prefijos y lo reutiliza, de modo que el prefill solo procesa la parte nueva (la query y los chunks específicos). Si el 60 % de tu contexto es estable, te ahorras el 60 % del prefill de ese segmento en cada hit. Para que funcione, &lt;strong>el contexto estable debe ir al principio del prompt&lt;/strong> (el KV cache es prefijo-dependiente) y conviene maximizar el &lt;em>hit rate&lt;/em>; el detalle de ingeniería de hit rate está en &lt;a href="#ver-tambi%C3%A9n">Prefix cache hit rate&lt;/a>. Combina especialmente bien con RAG: documentos recuperados que se repiten entre sesiones se cachean una vez.&lt;/p>
&lt;h3 id="4-caché-semántico-de-respuestas">4. Caché semántico de respuestas&lt;/h3>
&lt;p>Una capa por delante del modelo: si una query es &lt;strong>semánticamente equivalente&lt;/strong> a una respondida antes (similitud de embeddings por encima de un umbral), devuelve la respuesta cacheada y &lt;strong>sáltate el modelo entero&lt;/strong> —retrieval, prefill y decode incluidos. En cargas reales con colas largas de preguntas repetidas o casi-repetidas (FAQ, soporte), el ahorro es enorme porque elimina el coste completo, no solo el de prefill. La trampa es el umbral: demasiado laxo y sirves respuestas equivocadas a preguntas parecidas-pero-distintas. El diseño está en &lt;a href="#ver-tambi%C3%A9n">Caché semántico para RAG&lt;/a>.&lt;/p>
&lt;h3 id="5-structured-output-y-function-calling-apoyarse-en-herramientas-no-en-memoria">5. Structured output y function calling: apoyarse en herramientas, no en memoria&lt;/h3>
&lt;p>La última palanca cambia de qué depende el SLM. En lugar de pedirle que &lt;strong>sepa&lt;/strong> un dato (su punto débil), haz que &lt;strong>llame a una herramienta&lt;/strong> que lo sabe: una consulta a base de datos, una API, una calculadora, un validador. El &lt;strong>structured output&lt;/strong> (forzar JSON conforme a un esquema) y el &lt;strong>function calling&lt;/strong> convierten al SLM en un orquestador que extrae argumentos del contexto y delega el cálculo o la consulta. Un SLM razonablemente capaz emite un &lt;em>tool call&lt;/em> bien formado mucho más fiablemente de lo que recuerda un hecho concreto. Esto reduce la presión sobre el conocimiento paramétrico &lt;strong>y&lt;/strong> sobre la recuperación: para datos estructurados y frescos (precios, inventario, estados), consultar bate a recuperar texto y a memorizar. Los fundamentos están en &lt;a href="#ver-tambi%C3%A9n">Structured output&lt;/a> y &lt;a href="#ver-tambi%C3%A9n">Function calling&lt;/a>.&lt;/p>
&lt;h2 id="el-pipeline-completo">El pipeline completo&lt;/h2>
&lt;p>Las cinco palancas no son alternativas: se encadenan. El flujo, con el contador de tokens cayendo en cada paso:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 290" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pipeline retrieve, rerank, comprimir, SLM con el contador de tokens cayendo">
&lt;defs>&lt;marker id="rag1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;p>&lt;text x="390" y="22" text-anchor="middle" font-size="15" font-weight="700" fill="currentColor">Recuperar agresivo, destilar agresivo, razonar barato&lt;/text>&lt;/p>
&lt;rect x="20" y="50" width="120" height="60" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4"/>
&lt;text x="80" y="76" text-anchor="middle" font-size="12" font-weight="700" fill="#1f3550">Retriever&lt;/text>
&lt;text x="80" y="93" text-anchor="middle" font-size="11" fill="#1f3550">híbrido, recall&lt;/text>
&lt;text x="80" y="130" text-anchor="middle" font-size="12" font-weight="700" fill="#1f5fa8">~80 chunks&lt;/text>
&lt;rect x="180" y="50" width="120" height="60" fill="#e6d0ff" stroke="#5a2db0" stroke-width="1.4"/>
&lt;text x="240" y="76" text-anchor="middle" font-size="12" font-weight="700" fill="#3a1d70">Reranker&lt;/text>
&lt;text x="240" y="93" text-anchor="middle" font-size="11" fill="#3a1d70">precisión&lt;/text>
&lt;text x="240" y="130" text-anchor="middle" font-size="12" font-weight="700" fill="#5a2db0">5 chunks · 4000 tok&lt;/text>
&lt;rect x="340" y="50" width="120" height="60" fill="#fff4d6" stroke="#a48000" stroke-width="1.4"/>
&lt;text x="400" y="76" text-anchor="middle" font-size="12" font-weight="700" fill="#5a4500">Compresión&lt;/text>
&lt;text x="400" y="93" text-anchor="middle" font-size="11" fill="#5a4500">extractiva k=4&lt;/text>
&lt;text x="400" y="130" text-anchor="middle" font-size="12" font-weight="700" fill="#a48000">1000 tok&lt;/text>
&lt;rect x="500" y="50" width="120" height="60" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.4"/>
&lt;text x="560" y="73" text-anchor="middle" font-size="12" font-weight="700" fill="#1c3a26">Prefix cache&lt;/text>
&lt;text x="560" y="90" text-anchor="middle" font-size="11" fill="#1c3a26">+ caché&lt;/text>
&lt;text x="560" y="103" text-anchor="middle" font-size="11" fill="#1c3a26">semántico&lt;/text>
&lt;text x="560" y="130" text-anchor="middle" font-size="12" font-weight="700" fill="#2a7a40">prefill mínimo&lt;/text>
&lt;rect x="660" y="50" width="100" height="60" fill="#f6caca" stroke="#a52a2a" stroke-width="1.4"/>
&lt;text x="710" y="73" text-anchor="middle" font-size="12" font-weight="700" fill="#6a1a1a">SLM&lt;/text>
&lt;text x="710" y="90" text-anchor="middle" font-size="11" fill="#6a1a1a">razona +&lt;/text>
&lt;text x="710" y="103" text-anchor="middle" font-size="11" fill="#6a1a1a">tool calls&lt;/text>
&lt;text x="710" y="130" text-anchor="middle" font-size="12" font-weight="700" fill="#a52a2a">respuesta&lt;/text>
&lt;path d="M140,80 L180,80" stroke="currentColor" stroke-width="1.6" fill="none" marker-end="url(#rag1)"/>
&lt;path d="M300,80 L340,80" stroke="currentColor" stroke-width="1.6" fill="none" marker-end="url(#rag1)"/>
&lt;path d="M460,80 L500,80" stroke="currentColor" stroke-width="1.6" fill="none" marker-end="url(#rag1)"/>
&lt;path d="M620,80 L660,80" stroke="currentColor" stroke-width="1.6" fill="none" marker-end="url(#rag1)"/>
&lt;p>&lt;text x="390" y="180" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">El contador de tokens de prefill cae a lo largo del pipeline&lt;/text>
&lt;rect x="100" y="200" width="560" height="22" fill="none" stroke="currentColor" stroke-width="1"/>
&lt;rect x="100" y="200" width="560" height="22" fill="#d4ecff"/>
&lt;rect x="240" y="200" width="280" height="22" fill="#fff4d6"/>
&lt;rect x="240" y="200" width="70" height="22" fill="#cdebd0"/>
&lt;text x="180" y="216" text-anchor="middle" font-size="11" fill="#1f3550">retrieve: mucho&lt;/text>
&lt;text x="380" y="216" text-anchor="middle" font-size="11" fill="#5a4500">rerank: 4000 tok&lt;/text>
&lt;text x="275" y="216" text-anchor="middle" font-size="11" fill="#1c3a26">1000&lt;/text>&lt;/p>
&lt;p>&lt;text x="390" y="252" text-anchor="middle" font-size="12" fill="currentColor">TTFT en RTX 4090 a ~5000 tok/s · 4000 tok = 0.80 s → 1000 tok = 0.20 s&lt;/text>
&lt;text x="390" y="272" text-anchor="middle" font-size="11" fill="currentColor">atención cae ~k² = 16× en esa parte del cómputo&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;p>El orden importa. Recuperar agresivo (recall alto) &lt;strong>antes&lt;/strong> de filtrar garantiza que el material correcto está entre los candidatos; rerankear y comprimir &lt;strong>después&lt;/strong> garantiza que solo lo denso y relevante paga el peaje del prefill; cachear envuelve todo para no repetir trabajo. El SLM solo ve la chuleta final, corta y ordenada.&lt;/p>
&lt;h2 id="implicaciones-para-inferencia-on-premise">Implicaciones para inferencia on-premise&lt;/h2>
&lt;p>La trampa mental a evitar: tratar el SLM como un modelo grande con menos calidad. No lo es. Es un &lt;strong>perfil de coste distinto&lt;/strong> que premia un diseño distinto. Tres consecuencias prácticas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>El presupuesto de tokens es un recurso de primera clase.&lt;/strong> Con un modelo grande de 128K de ventana, &amp;ldquo;meter un poco más&amp;rdquo; es barato relativo al modelo. Con un SLM, cada token de contexto se nota en el TTFT y en la calidad. Trata el tamaño del contexto como una cantidad a &lt;strong>minimizar bajo restricción de cubrir la respuesta&lt;/strong>, no a maximizar.&lt;/li>
&lt;li>&lt;strong>La inversión vale la pena precisamente porque el modelo es barato.&lt;/strong> Reranker, compresor y cachés añaden complejidad, pero el modelo que sirven es lo suficientemente económico como para correr muchas réplicas. El cuello de botella se desplaza del modelo al pipeline de datos, que es justo donde quieres que esté.&lt;/li>
&lt;li>&lt;strong>Recuperar no sustituye a adaptar; se combinan.&lt;/strong> Para conocimiento de dominio profundo y recurrente, adaptar el SLM con LoRA (ver el hermano &lt;a href="https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/">QLoRA y multi-LoRA agresivo&lt;/a>) puede meter parte del conocimiento &amp;ldquo;en los pesos&amp;rdquo; de forma barata, reduciendo lo que hay que recuperar. RAG agresivo y adaptación agresiva no compiten: la primera da frescura y citabilidad, la segunda da fluidez y formato de dominio. El diseño bueno usa ambas.&lt;/li>
&lt;/ul>
&lt;h3 id="en-la-rtx-4090-24-gb-ada-lovelace">En la RTX 4090 (24 GB, Ada Lovelace)&lt;/h3>
&lt;p>El escenario canónico: un SLM cuantizado (4B–8B en INT4/FP8) cabe holgado, dejando VRAM para un KV cache generoso —imprescindible para el prefix caching— y para el reranker (un cross-encoder de unos cientos de MB). El compresor extractivo tipo LLMLingua corre en un modelo pequeño aparte o en CPU. El cálculo de TTFT de arriba (0.80 s → 0.20 s comprimiendo 4× a ~5000 tok/s) es representativo de esta tarjeta. La regla de pulgar: si el TTFT se va por encima de tu SLA, &lt;strong>el primer ajuste es comprimir el contexto, no cambiar de modelo&lt;/strong>.&lt;/p>
&lt;h3 id="en-un-cluster-genérico-4h100-sxm-320-gb-nvlink-fp8-nativo">En un cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo)&lt;/h3>
&lt;p>Con 320 GB y FP8 nativo el prefill es mucho más rápido, así que la tentación es relajar la disciplina de tokens. No conviene del todo: la palanca cambia de &lt;strong>TTFT&lt;/strong> a &lt;strong>throughput agregado&lt;/strong>. Comprimir el contexto no solo acelera cada petición sino que &lt;strong>libera cómputo de prefill&lt;/strong> para servir más peticiones por GPU —el prefill compute-bound es exactamente el recurso que satura primero bajo carga. Aquí el prefix caching y el caché semántico, compartidos entre réplicas, son los que más rinden: a alto QPS, el trabajo de prefill que evitas cachear es throughput puro que ganas. El SLM sigue siendo el motor de razonamiento barato; la diferencia es que ahora corres muchos en paralelo y el pipeline de datos es lo que decide cuántas peticiones caben.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Evaluación de la compresión&lt;/strong>: cómo medir que comprimir $k=4$ no tira respuestas correctas (faithfulness, answer recall sobre un set de preguntas con ground truth).&lt;/li>
&lt;li>&lt;strong>Compresión consciente de la query frente a agnóstica&lt;/strong>: comprimir antes o después de conocer la pregunta cambia qué se puede cachear y qué se puede tirar.&lt;/li>
&lt;li>&lt;strong>Chunking y granularidad&lt;/strong>: el tamaño de chunk interactúa con el reranking y la compresión; queda para el post de curación de corpus.&lt;/li>
&lt;li>&lt;strong>Multi-hop y agentes&lt;/strong>: cuando una pregunta requiere varias rondas de recuperación, el presupuesto de tokens se reparte entre hops y la disciplina de compresión se vuelve crítica.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/rag-reranker-hybrid-retrieval-fundamentos/">Reranking e hybrid retrieval para RAG&lt;/a> — la palanca 1 en detalle: maximizar recall en el retriever y precisión en el reranker para inyectar pocos chunks pero excelentes, que es lo que un SLM necesita.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/rag-corpus-curation-fundamentos/">Curación del corpus para RAG&lt;/a> — un corpus limpio y bien chunked reduce la paja que el compresor tiene que eliminar; la calidad de la chuleta empieza aguas arriba.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/semantic-cache-rag/">Caché semántico para RAG&lt;/a> — la palanca 4: saltarse el modelo entero cuando una query es semánticamente equivalente a una ya respondida.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/embeddings-modelos-2026-dense-sparse-multivector/">Embeddings 2026: dense, sparse y multivector&lt;/a> — la base del retrieval híbrido y del umbral del caché semántico; qué representación recupera mejor con menos ruido.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/prefix-cache-hit-rate-engineering/">Ingeniería del prefix cache hit rate&lt;/a> — la palanca 3: cómo estructurar el prompt (contexto estable primero) para maximizar la reutilización del KV cache del contexto recuperado.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/prefill-optimizaciones-vllm/">Optimizaciones de prefill en vLLM&lt;/a> — el prefill compute-bound es el coste que toda esta discusión intenta minimizar; aquí están los parámetros concretos para acelerarlo.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/structured-output-fundamentos/">Structured output: fundamentos&lt;/a> — la palanca 5: forzar JSON conforme a esquema para que el SLM orqueste herramientas en vez de recordar datos.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/function-calling-tool-augmented-retrieval/">Function calling y recuperación aumentada con herramientas&lt;/a> — cuando consultar una API o base de datos bate a recuperar texto y a memorizar; el SLM como orquestador de tools.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">El roofline se invierte en modelos pequeños&lt;/a> — por qué el prefill compute-bound es el cuello de botella que da forma a todo este post: meter más contexto no es gratis.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/">QLoRA y multi-LoRA agresivo en SLM&lt;/a> — la alternativa complementaria: adaptar el SLM por dominio para meter parte del conocimiento &amp;ldquo;en los pesos&amp;rdquo; y reducir lo que hay que recuperar.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Lewis, P., et al. &lt;em>Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks&lt;/em>. NeurIPS 2020. &lt;a href="https://arxiv.org/abs/2005.11401">https://arxiv.org/abs/2005.11401&lt;/a>&lt;/li>
&lt;li>Liu, N.F., et al. &lt;em>Lost in the Middle: How Language Models Use Long Contexts&lt;/em>. TACL 2024. &lt;a href="https://arxiv.org/abs/2307.03172">https://arxiv.org/abs/2307.03172&lt;/a>&lt;/li>
&lt;li>Jiang, H., et al. &lt;em>LLMLingua: Compressing Prompts for Accelerated Inference of Large Language Models&lt;/em>. EMNLP 2023. &lt;a href="https://arxiv.org/abs/2310.05736">https://arxiv.org/abs/2310.05736&lt;/a>&lt;/li>
&lt;li>Jiang, H., et al. &lt;em>LongLLMLingua: Accelerating and Enhancing LLMs in Long Context Scenarios via Prompt Compression&lt;/em>. ACL 2024. &lt;a href="https://arxiv.org/abs/2310.06839">https://arxiv.org/abs/2310.06839&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Cuantización agresiva (estado del arte): del 4-bit al ternario</title><link>https://blog.lo0.es/posts/cuantizacion-agresiva-sub-4-bit-ternario/</link><pubDate>Tue, 09 Jun 2026 02:10:00 +0000</pubDate><guid>https://blog.lo0.es/posts/cuantizacion-agresiva-sub-4-bit-ternario/</guid><description>&lt;blockquote>
&lt;p>Este post es la continuación directa de &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization para inferencia LLM&lt;/a>, que cubre el régimen &amp;ldquo;resuelto&amp;rdquo; (FP8, INT4 con GPTQ/AWQ). Léelo primero: aquí asumo la matemática del scale+zero-point, qué hacen GPTQ y AWQ, y la distinción PTQ/QAT. Lo que añadimos es la &lt;strong>frontera sub-4-bit&lt;/strong>, donde la cuantización post-hoc escalar deja de funcionar y hay que cambiar de herramienta.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Hay una línea divisoria nítida alrededor de los 4 bits. Por encima, cuantizar es un problema &lt;strong>resuelto&lt;/strong>: INT8 es indistinguible de BF16, e INT4 con un método bueno (AWQ, GPTQ) pierde 1-2 puntos de MMLU y poco más. El método sigue siendo el mismo de siempre —tomar cada peso, escalarlo, redondearlo a un entero corto— y funciona. Por debajo de 4 bits, ese método &lt;strong>colapsa&lt;/strong>: a 2 bits la cuantización escalar ingenua puede duplicar la perplexity. La razón es geométrica —cada peso tiene solo 4 valores posibles, el error de redondeo deja de ser despreciable— y la salida no es &amp;ldquo;redondear mejor&amp;rdquo;, es &lt;strong>cambiar de representación&lt;/strong>. Los métodos SOTA de 2 bits (AQLM, QuIP#, QTIP) dejan de cuantizar pesos individuales y cuantizan &lt;strong>vectores&lt;/strong> de pesos contra diccionarios (códigos), y &amp;ldquo;blanquean&amp;rdquo; la matriz de pesos para repartir su energía y aplastar outliers (incoherence processing). El ternario es otra cosa todavía: BitNet b1.58, con pesos en {-1, 0, +1} (~1.58 bits), &lt;strong>no es PTQ&lt;/strong> —es un modelo entrenado nativamente con esa restricción— y cambia la aritmética de la matmul de multiplicaciones a sumas/restas, tocando a la vez el techo de cómputo y el de memoria. La regla mental: ≥4-bit comprimes la foto; &amp;lt;4-bit tienes que repintarla.&lt;/p>
&lt;h2 id="la-analogía-el-jpeg-que-ya-no-se-puede-comprimir-más">La analogía: el JPEG que ya no se puede comprimir más&lt;/h2>
&lt;p>En &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">el post de quantization&lt;/a> usamos el JPEG con detector de bordes para explicar INT4. Aquí la analogía sigue, pero hay que llevarla hasta su límite.&lt;/p>
&lt;p>Un JPEG con factor de calidad 90 es indistinguible del original. A calidad 60 ya se nota un poco, pero sigue siendo &amp;ldquo;la misma foto&amp;rdquo;. A calidad 30 aparecen los bloques 8×8, los halos alrededor de los bordes, el banding en los degradados. A calidad 10 la imagen está destruida: reconoces que &lt;strong>había&lt;/strong> una cara, pero los detalles han desaparecido bajo los artefactos. Y aquí está la clave: &lt;strong>no existe ningún encoder JPEG que comprima a calidad 10 sin esos artefactos&lt;/strong>, porque el algoritmo JPEG (DCT por bloques + cuantización de coeficientes) tiene un suelo de información por debajo del cual su propio mecanismo introduce el ruido.&lt;/p>
&lt;p>¿Qué haces si necesitas la foto a ese tamaño de archivo y que se siga viendo bien? No comprimes más la original. &lt;strong>Repintas la foto sabiendo de antemano que va a vivir comprimida&lt;/strong>: un ilustrador la redibuja con líneas limpias, paleta reducida, cero degradados sutiles —una imagen diseñada para sobrevivir a la compresión brutal—. El resultado a &amp;ldquo;10 KB&amp;rdquo; se ve infinitamente mejor que el JPEG original aplastado a 10 KB, porque no es el mismo proceso: uno destruye información existente, el otro genera información nueva ya adaptada a la restricción.&lt;/p>
&lt;p>Esa es exactamente la frontera de este post:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>PTQ escalar (≥4-bit)&lt;/strong> = comprimir el JPEG. Hasta cierto ratio, sigue siendo la misma foto.&lt;/li>
&lt;li>&lt;strong>PTQ vectorial SOTA (2-bit: AQLM, QuIP#, QTIP)&lt;/strong> = un códec de imagen mucho más sofisticado (diccionarios, transformadas que decorrelacionan) que estira el ratio comprimible un poco más antes del colapso.&lt;/li>
&lt;li>&lt;strong>Ternario nativo (BitNet b1.58)&lt;/strong> = repintar la foto. No comprimes un modelo BF16 existente; entrenas uno nuevo que nace ternario.&lt;/li>
&lt;/ul>
&lt;h2 id="el-mapa-de-la-frontera-bit-a-bit">El mapa de la frontera, bit a bit&lt;/h2>
&lt;p>Cuantizar un modelo es decidir cuántos valores distintos puede tomar cada peso. Con &lt;code>b&lt;/code> bits por peso hay &lt;code>2^b&lt;/code> valores posibles. La pregunta central es: ¿a partir de qué &lt;code>b&lt;/code> el número de valores es tan pequeño que el redondeo destruye el modelo?&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Bits&lt;/th>
&lt;th>Valores/peso&lt;/th>
&lt;th>Estado del arte&lt;/th>
&lt;th>Método necesario&lt;/th>
&lt;th>Pérdida típica vs BF16&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>256&lt;/td>
&lt;td>&lt;strong>Resuelto&lt;/strong>&lt;/td>
&lt;td>RTN, SmoothQuant, FP8&lt;/td>
&lt;td>~0 (indistinguible)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>16&lt;/td>
&lt;td>&lt;strong>Resuelto&lt;/strong>&lt;/td>
&lt;td>AWQ, GPTQ&lt;/td>
&lt;td>1-2 pp MMLU, +0.1-0.3 PPL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>8&lt;/td>
&lt;td>Degradación pequeña&lt;/td>
&lt;td>GPTQ/AWQ tuneado, GGUF Q3_K&lt;/td>
&lt;td>3-5 pp MMLU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>4&lt;/td>
&lt;td>Serio salvo SOTA&lt;/td>
&lt;td>&lt;strong>AQLM, QuIP#, QTIP&lt;/strong> (no escalar)&lt;/td>
&lt;td>escalar: colapso; SOTA: 4-8 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1.58&lt;/td>
&lt;td>3 (ternario)&lt;/td>
&lt;td>Solo nativo&lt;/td>
&lt;td>&lt;strong>BitNet b1.58&lt;/strong> (QAT/entrenamiento nativo)&lt;/td>
&lt;td>n/a (no es PTQ)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2 (binario)&lt;/td>
&lt;td>Investigación&lt;/td>
&lt;td>nativo, claims dudosos&lt;/td>
&lt;td>grande / sin metodología clara&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Las tres transiciones que importan:&lt;/p>
&lt;p>&lt;strong>8 → 4 bits: nada se rompe.&lt;/strong> Con 16 niveles por peso y un scale por bloque de 128, el error de redondeo es pequeño relativo a la dinámica de los pesos. GPTQ compensa el error propagándolo a los pesos vecinos; AWQ protege el ~1 % de canales salientes. El modelo casi no lo nota. Esto está en el post anterior.&lt;/p>
&lt;p>&lt;strong>4 → 2 bits: el codo.&lt;/strong> Aquí pasan dos cosas a la vez. Primero, con solo 4 niveles, el cuantizador escalar ya no puede representar la distribución de pesos —que es aproximadamente gaussiana con colas largas— sin un error de redondeo enorme en proporción. Segundo, y más sutil: el error de cuantización deja de ser &amp;ldquo;ruido pequeño que el modelo absorbe&amp;rdquo; y se vuelve &lt;strong>estructurado&lt;/strong>, sesgando sistemáticamente las activaciones. La PTQ escalar ingenua a 2 bits sobre un Llama 8B típicamente &lt;strong>duplica la perplexity o más&lt;/strong>. Es el codo de la curva.&lt;/p>
&lt;p>&lt;strong>2 → 1.58 bits: cambio de naturaleza.&lt;/strong> No se cruza con un método de compresión mejor. Se cruza entrenando el modelo desde el principio con la restricción. Es una discontinuidad: a la izquierda estás haciendo PTQ, a la derecha estás haciendo entrenamiento.&lt;/p>
&lt;h2 id="por-qué-la-ptq-escalar-colapsa-por-debajo-de-4-bits">Por qué la PTQ escalar colapsa por debajo de 4 bits&lt;/h2>
&lt;p>El cuantizador escalar tiene una limitación de fondo: cuantiza &lt;strong>cada peso por separado&lt;/strong>, ignorando que los pesos de una fila/columna están correlacionados y que el error de uno se podría compensar con otro. A 4 bits esto importa poco; a 2 bits es letal. Hay tres ataques posibles, y los métodos SOTA usan los tres.&lt;/p>
&lt;h3 id="1-cuantización-vectorial-diccionarios-en-lugar-de-escalas">1. Cuantización vectorial: diccionarios en lugar de escalas&lt;/h3>
&lt;p>En lugar de mapear cada peso a uno de 4 valores, agrupa los pesos en &lt;strong>vectores&lt;/strong> (p. ej. de 8 pesos) y mapea cada vector al entrada más cercana de un &lt;strong>diccionario&lt;/strong> (codebook) aprendido. Si el diccionario tiene 256 entradas, codificar un vector de 8 pesos cuesta 8 bits (el índice) → 1 bit/peso, pero cada &amp;ldquo;valor reconstruido&amp;rdquo; es un punto en un espacio de 8 dimensiones elegido para minimizar el error sobre la distribución real de pesos.&lt;/p>
&lt;p>La ventaja es de teoría de la información: un diccionario de vectores puede colocar sus puntos de reconstrucción donde &lt;strong>realmente&lt;/strong> están los pesos (en racimos), mientras que el cuantizador escalar está obligado a poner sus 4 niveles en una rejilla regular, gastando resolución en zonas vacías. Es la diferencia entre un mapa de carreteras con cuadrícula uniforme y uno que pone más detalle donde hay ciudades.&lt;/p>
&lt;p>&lt;strong>AQLM&lt;/strong> (Additive Quantization of Language Models, arXiv:2401.06118) lleva esto al extremo con &lt;strong>cuantización aditiva&lt;/strong>: cada vector de pesos se reconstruye como &lt;strong>suma de varios códigos&lt;/strong> de varios diccionarios (multi-codebook). Es más expresivo que un solo diccionario porque el número de combinaciones es el producto de los tamaños, no la suma. AQLM fue uno de los primeros métodos en hacer 2-bit &amp;ldquo;usable&amp;rdquo; (no colapsado) en modelos grandes, a costa de un proceso de calibración caro y kernels de inferencia especializados.&lt;/p>
&lt;h3 id="2-incoherence-processing-blanquear-la-matriz">2. Incoherence processing: blanquear la matriz&lt;/h3>
&lt;p>El segundo ataque es contra los &lt;strong>outliers&lt;/strong>. Las matrices de pesos de un transformer tienen unas pocas entradas (y unos pocos canales) con magnitud mucho mayor que el resto. Esos outliers dominan el rango del cuantizador: si tienes que representar un peso de magnitud 8 y el resto son de magnitud 0.5, tu scale se estira para cubrir el 8 y desperdicias casi toda la resolución.&lt;/p>
&lt;p>&lt;strong>Incoherence processing&lt;/strong> (la idea central de QuIP y QuIP#) ataca esto multiplicando la matriz de pesos &lt;code>W&lt;/code> por matrices ortogonales aleatorias por la izquierda y la derecha: &lt;code>W' = U W V^T&lt;/code>. Como &lt;code>U&lt;/code> y &lt;code>V&lt;/code> son ortogonales, la operación es invertible y la matemática del producto se puede deshacer en inferencia absorbiéndola en las capas vecinas (igual que AWQ absorbe sus escalas). Pero la rotación &lt;strong>reparte la energía&lt;/strong>: una matriz &amp;ldquo;incoherente&amp;rdquo; tiene sus valores repartidos de forma casi uniforme, sin outliers concentrados, porque mezclar coordenadas con una rotación aleatoria aplana la distribución (es, en esencia, el teorema central del límite actuando sobre combinaciones lineales). Una matriz sin outliers se cuantiza muchísimo mejor a 2 bits. Es el equivalente a &amp;ldquo;blanquear&amp;rdquo; una señal antes de digitalizarla.&lt;/p>
&lt;p>&lt;strong>QuIP#&lt;/strong> (arXiv:2402.04396) combina incoherence processing con &lt;strong>códigos reticulares E8&lt;/strong>: en vez de un diccionario arbitrario, usa el retículo E8 (un empaquetamiento de esferas óptimo en 8 dimensiones, el mejor conocido). Cuantizar vectores de 8 pesos contra el retículo E8 da el menor error de reconstrucción posible para una densidad de bits dada, porque E8 es literalmente la forma más eficiente de colocar puntos en 8D. Es teoría de codificación clásica aplicada a pesos de LLM.&lt;/p>
&lt;h3 id="3-codificación-con-memoria-trellis">3. Codificación con memoria: trellis&lt;/h3>
&lt;p>&lt;strong>QTIP&lt;/strong> (arXiv:2406.11235) añade el tercer ataque: &lt;strong>trellis-coded quantization&lt;/strong>. En lugar de cuantizar cada vector de forma independiente, modela la secuencia de pesos como un camino a través de un trellis (la misma estructura de los códigos convolucionales de las telecomunicaciones) y elige la secuencia de códigos óptima con el algoritmo de Viterbi. La intuición: introducir &lt;strong>memoria&lt;/strong> entre cuantizaciones sucesivas permite errores correlacionados que se cancelan, en vez de errores independientes que se acumulan. QTIP, sobre incoherence processing, mejora a QuIP# en calidad a 2-3 bits manteniendo kernels de inferencia rápidos.&lt;/p>
&lt;p>La idea común a los tres: &lt;strong>dejar de cuantizar escalares y empezar a cuantizar vectores con diccionarios, y decorrelacionar la matriz antes de hacerlo&lt;/strong>. Ninguno es &amp;ldquo;redondear mejor&amp;rdquo;; los tres cambian la representación de raíz. Por eso, por debajo de 4 bits, ya no basta con un flag en vLLM: hace falta co-diseño de método de cuantización + kernel de inferencia.&lt;/p>
&lt;h2 id="el-ternario-nativo-bitnet-b158">El ternario nativo: BitNet b1.58&lt;/h2>
&lt;p>Aquí cambiamos de continente. Todo lo anterior es &lt;strong>PTQ&lt;/strong>: parte de un modelo BF16 entrenado y lo comprime. El ternario de BitNet no comprime nada.&lt;/p>
&lt;p>&lt;strong>BitNet b1.58&lt;/strong> (arXiv:2402.17764) entrena un transformer desde cero donde &lt;strong>cada peso está restringido a {-1, 0, +1}&lt;/strong> durante todo el entrenamiento. Tres valores ⇒ log₂(3) ≈ &lt;strong>1.58 bits/peso&lt;/strong>. La cuantización no es un paso posterior: las capas lineales (&lt;code>BitLinear&lt;/code>) cuantizan sus pesos a ternario en el forward pass de cada step de entrenamiento, y los gradientes fluyen a través de un estimador straight-through. El modelo &lt;strong>aprende a funcionar con pesos ternarios&lt;/strong>. Esto es QAT llevado al extremo: no un fine-tune corto de robustez, sino la restricción presente desde el primer token de entrenamiento.&lt;/p>
&lt;p>Esa diferencia es la que esquiva el codo de la curva. La PTQ a 2 bits intenta encontrar la mejor aproximación ternaria/quaternaria de un modelo que se entrenó esperando precisión completa —y ese modelo tiene pesos &amp;ldquo;frágiles&amp;rdquo; que dependen de matices que 2 bits no capturan—. BitNet, en cambio, nunca tuvo esos matices: sus pesos nacieron ternarios, así que la red distribuyó su capacidad representacional de forma compatible con la restricción. Es repintar la foto en vez de comprimirla.&lt;/p>
&lt;h3 id="lo-que-cambia-no-es-solo-la-memoria-es-la-aritmética">Lo que cambia no es solo la memoria, es la aritmética&lt;/h3>
&lt;p>El punto que más se subestima de BitNet: con pesos en {-1, 0, +1}, &lt;strong>la multiplicación desaparece de la matmul&lt;/strong>. Multiplicar una activación &lt;code>x&lt;/code> por un peso ternario &lt;code>w&lt;/code> es trivial: si &lt;code>w = +1&lt;/code> sumas &lt;code>x&lt;/code>, si &lt;code>w = -1&lt;/code> restas &lt;code>x&lt;/code>, si &lt;code>w = 0&lt;/code> no haces nada. La operación dominante de un transformer —el producto matriz-vector— pasa de ser un mar de multiplica-acumula (MAC) en coma flotante a ser &lt;strong>sumas y restas enteras&lt;/strong>.&lt;/p>
&lt;p>Esto importa porque conecta con el roofline. Como se explica en &lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">El roofline invertido de los modelos pequeños&lt;/a>, la inferencia LLM tiene dos techos: el de &lt;strong>memoria&lt;/strong> (ancho de banda HBM para cargar pesos) y el de &lt;strong>cómputo&lt;/strong> (FLOPs de las tensor cores). La cuantización normal (INT4, FP8) ataca &lt;strong>solo el techo de memoria&lt;/strong>: el peso ocupa menos, pero para multiplicarlo lo descuantizas a FP16 y haces la misma multiplicación de siempre. El ternario ataca &lt;strong>ambos techos a la vez&lt;/strong>: el peso ocupa 1.58 bits (memoria) &lt;strong>y&lt;/strong> la operación es una suma en lugar de una multiplicación (cómputo). Por eso BitNet necesita kernels propios —&lt;strong>bitnet.cpp&lt;/strong>— que ejecutan la matmul ternaria sin pasar nunca por FP16; un kernel que descuantizara a FP16 para multiplicar tiraría a la basura la mitad de la ventaja.&lt;/p>
&lt;p>La contrapartida honesta: BitNet b1.58 es entrenamiento desde cero. No puedes &amp;ldquo;convertir tu Llama 8B a BitNet&amp;rdquo;. Si quieres ternario, entrenas (o usas) un modelo nativamente ternario, con todo lo que implica en coste de pre-entrenamiento y en disponibilidad de pesos. Hoy es una línea de investigación con modelos publicados a escalas modestas, no un drop-in para reemplazar tu serving actual.&lt;/p>
&lt;h2 id="qat-como-puente-entre-ptq-y-nativo">QAT como puente entre PTQ y nativo&lt;/h2>
&lt;p>Entre &amp;ldquo;comprimir post-hoc&amp;rdquo; (PTQ) y &amp;ldquo;entrenar nativamente ternario&amp;rdquo; (BitNet) hay un punto intermedio: &lt;strong>QAT&lt;/strong> (Quantization-Aware Training). Tomas un modelo ya entrenado y haces un fine-tune corto &lt;strong>con las operaciones de cuantización dentro del bucle&lt;/strong>, para que aprenda a ser robusto a bits bajos sin pagar un pre-entrenamiento completo.&lt;/p>
&lt;p>&lt;strong>Gemma 3&lt;/strong> publica variantes &lt;strong>QAT&lt;/strong> oficiales precisamente para esto: modelos que, tras el fine-tune QAT, sostienen INT4 con una pérdida de calidad mucho menor que la PTQ pura sobre el mismo modelo. El coste es de entrenamiento (horas-días de GPU sobre un modelo ya existente), no de inferencia. Para INT4 con QAT recuperas casi toda la calidad; para 2-bit, QAT ayuda pero sigue siendo terreno difícil; para ternario, el QAT deja de ser &amp;ldquo;fine-tune corto&amp;rdquo; y se convierte en entrenamiento nativo (BitNet).&lt;/p>
&lt;p>La jerarquía de decisión:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>PTQ&lt;/strong> = default a ≥4 bits. Minutos-horas, sin tocar pesos de entrenamiento. Cubre el 90 % de producción.&lt;/li>
&lt;li>&lt;strong>QAT&lt;/strong> = cuando PTQ pierde demasiado y la diferencia importa. Bits bajos (2-3), o modelos sensibles. Pagas fine-tune.&lt;/li>
&lt;li>&lt;strong>Nativo (ternario)&lt;/strong> = cuando quieres bajar de 2 bits &lt;strong>y&lt;/strong> cambiar la aritmética. Pagas pre-entrenamiento. Solo tiene sentido si controlas el modelo desde su creación.&lt;/li>
&lt;/ul>
&lt;h2 id="las-matemáticas-que-importan-footprint-y-cuántos-caben">Las matemáticas que importan: footprint y cuántos caben&lt;/h2>
&lt;p>El footprint de los pesos es directo: &lt;code>bytes = (bits/param / 8) × N&lt;/code>, con &lt;code>N&lt;/code> el número de parámetros. Para un modelo de 8B:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Nivel&lt;/th>
&lt;th>bits/param&lt;/th>
&lt;th>Footprint 8B&lt;/th>
&lt;th>Ratio vs BF16&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>BF16&lt;/td>
&lt;td>16&lt;/td>
&lt;td>16.0 GB&lt;/td>
&lt;td>1.0×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT8&lt;/td>
&lt;td>8&lt;/td>
&lt;td>8.0 GB&lt;/td>
&lt;td>2.0×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4&lt;/td>
&lt;td>4&lt;/td>
&lt;td>4.0 GB&lt;/td>
&lt;td>4.0×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3-bit&lt;/td>
&lt;td>3&lt;/td>
&lt;td>3.0 GB&lt;/td>
&lt;td>5.3×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2-bit&lt;/td>
&lt;td>2&lt;/td>
&lt;td>2.0 GB&lt;/td>
&lt;td>8.0×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1.58-bit (ternario)&lt;/td>
&lt;td>~1.58&lt;/td>
&lt;td>~1.6 GB&lt;/td>
&lt;td>~10×&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>(El ternario real ocupa algo más de 1.58 bits/param porque hay que empaquetar 5 valores ternarios en 8 bits —5 × log₂(3) ≈ 7.92 bits— y porque las normas y embeddings suelen quedarse en más precisión. La cifra de ~1.6 GB para 8B es el orden de magnitud correcto.)&lt;/p>
&lt;h3 id="cuántos-modelos-de-8b-caben-en-una-rtx-4090">¿Cuántos modelos de 8B caben en una RTX 4090?&lt;/h3>
&lt;p>Una &lt;strong>RTX 4090 (24 GB, Ada Lovelace)&lt;/strong> tiene 24 GB. Reservamos ~4 GB para KV cache y activaciones, dejando &lt;strong>20 GB&lt;/strong> para pesos. Cuántos modelos de 8B distintos caben cargados simultáneamente:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Nivel&lt;/th>
&lt;th>Footprint 8B&lt;/th>
&lt;th>Modelos en 20 GB&lt;/th>
&lt;th>Comentario&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>BF16&lt;/td>
&lt;td>16.0 GB&lt;/td>
&lt;td>&lt;strong>1&lt;/strong>&lt;/td>
&lt;td>uno y queda margen escaso&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT8&lt;/td>
&lt;td>8.0 GB&lt;/td>
&lt;td>&lt;strong>2&lt;/strong>&lt;/td>
&lt;td>dos modelos distintos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4&lt;/td>
&lt;td>4.0 GB&lt;/td>
&lt;td>&lt;strong>5&lt;/strong>&lt;/td>
&lt;td>régimen resuelto; calidad ~lossless con AWQ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3-bit&lt;/td>
&lt;td>3.0 GB&lt;/td>
&lt;td>&lt;strong>6&lt;/strong>&lt;/td>
&lt;td>degradación pequeña ya visible&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2-bit&lt;/td>
&lt;td>2.0 GB&lt;/td>
&lt;td>&lt;strong>10&lt;/strong>&lt;/td>
&lt;td>solo viable con AQLM/QuIP#/QTIP&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1.58-bit&lt;/td>
&lt;td>~1.6 GB&lt;/td>
&lt;td>&lt;strong>~12&lt;/strong>&lt;/td>
&lt;td>solo modelos nativamente ternarios&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La cuenta es seductora —de 1 a 12 modelos en la misma tarjeta— pero hay que leerla con escepticismo. Saltar de INT4 (5 modelos, casi sin pérdida) a 2-bit (10 modelos) duplica la capacidad, pero solo si usas un método SOTA y aceptas 4-8 puntos de MMLU. Y el salto de 2-bit a ternario (10 → 12) es marginal en memoria: el ternario &lt;strong>no se justifica por footprint&lt;/strong> frente a un 2-bit SOTA, se justifica por la aritmética (el techo de cómputo) y porque evita el codo de calidad al ser nativo. Si tu única métrica es &amp;ldquo;cuántos GB ocupa&amp;rdquo;, el 2-bit SOTA ya te da casi todo. El ternario es para cuando además quieres el ahorro de cómputo.&lt;/p>
&lt;h3 id="la-curva-conceptual-perplexity-vs-bits">La curva conceptual: perplexity vs bits&lt;/h3>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 380" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Curva conceptual de perplexity frente a bits por peso">
&lt;text x="390" y="26" text-anchor="middle" fill="currentColor" font-size="15" font-weight="700">Perplexity vs bits por peso (conceptual): el codo y la rama nativa&lt;/text>
&lt;line x1="90" y1="320" x2="730" y2="320" stroke="currentColor" stroke-width="1.5"/>
&lt;line x1="90" y1="60" x2="90" y2="320" stroke="currentColor" stroke-width="1.5"/>
&lt;text x="410" y="362" text-anchor="middle" fill="currentColor" font-size="13">bits por peso (eje invertido: más comprimido a la derecha)&lt;/text>
&lt;text x="30" y="190" text-anchor="middle" fill="currentColor" font-size="13" transform="rotate(-90 30 190)">perplexity (peor arriba)&lt;/text>
&lt;text x="120" y="338" text-anchor="middle" fill="currentColor" font-size="12">16&lt;/text>
&lt;text x="250" y="338" text-anchor="middle" fill="currentColor" font-size="12">8&lt;/text>
&lt;text x="380" y="338" text-anchor="middle" fill="currentColor" font-size="12">4&lt;/text>
&lt;text x="470" y="338" text-anchor="middle" fill="currentColor" font-size="12">3&lt;/text>
&lt;text x="560" y="338" text-anchor="middle" fill="currentColor" font-size="12">2&lt;/text>
&lt;text x="650" y="338" text-anchor="middle" fill="currentColor" font-size="12">1.58&lt;/text>
&lt;line x1="380" y1="60" x2="380" y2="320" stroke="currentColor" stroke-width="0.8" stroke-dasharray="4 3"/>
&lt;text x="384" y="74" fill="currentColor" font-size="11">frontera 4-bit&lt;/text>
&lt;polyline points="120,300 250,298 380,292 470,278 560,170 620,95" fill="none" stroke="#c0392b" stroke-width="2.6"/>
&lt;circle cx="120" cy="300" r="4" fill="#c0392b"/>
&lt;circle cx="250" cy="298" r="4" fill="#c0392b"/>
&lt;circle cx="380" cy="292" r="4" fill="#c0392b"/>
&lt;circle cx="470" cy="278" r="4" fill="#c0392b"/>
&lt;circle cx="560" cy="170" r="4" fill="#c0392b"/>
&lt;text x="600" y="92" fill="#c0392b" font-size="12" font-weight="700">PTQ escalar ingenua&lt;/text>
&lt;text x="600" y="108" fill="#c0392b" font-size="11">colapsa &amp;lt;3 bits&lt;/text>
&lt;polyline points="380,292 470,284 560,250 650,232" fill="none" stroke="#2471a3" stroke-width="2.6" stroke-dasharray="6 3"/>
&lt;circle cx="560" cy="250" r="4" fill="#2471a3"/>
&lt;circle cx="650" cy="232" r="4" fill="#2471a3"/>
&lt;text x="560" y="282" fill="#2471a3" font-size="12" font-weight="700">PTQ SOTA vectorial&lt;/text>
&lt;text x="560" y="298" fill="#2471a3" font-size="11">AQLM / QuIP# / QTIP&lt;/text>
&lt;circle cx="650" cy="225" r="6" fill="#27ae60"/>
&lt;text x="600" y="208" fill="#27ae60" font-size="12" font-weight="700">ternario nativo&lt;/text>
&lt;text x="600" y="222" fill="#27ae60" font-size="11">BitNet b1.58 (no PTQ)&lt;/text>
&lt;text x="120" y="290" fill="currentColor" font-size="11">≈ plano ≥ 4 bits con buen método&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Tres lecturas de la curva. &lt;strong>Uno&lt;/strong>: a la derecha de 4 bits, las tres ramas están casi pegadas y casi planas —el régimen resuelto—. &lt;strong>Dos&lt;/strong>: la rama roja (PTQ escalar ingenua) tiene un codo brutal entre 3 y 2 bits; ahí es donde duplica la perplexity. La rama azul (PTQ SOTA vectorial) aplana ese codo —no lo elimina, pero lo hace tolerable hasta 2 bits—. &lt;strong>Tres&lt;/strong>: el punto verde del ternario nativo &lt;strong>no está en ninguna de las dos curvas de PTQ&lt;/strong>, porque no se obtiene comprimiendo: se obtiene entrenando, y por eso puede caer por debajo del codo sin pagar el precio de calidad que paga cualquier PTQ a esa densidad de bits. Es la diferencia entre el JPEG aplastado y la foto repintada.&lt;/p>
&lt;h2 id="escepticismo-obligatorio-el-1-bit-sin-pérdida-y-los-benchmarks-sin-metodología">Escepticismo obligatorio: el 1-bit &amp;ldquo;sin pérdida&amp;rdquo; y los benchmarks sin metodología&lt;/h2>
&lt;p>Tres alertas para leer la literatura de cuantización agresiva:&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;1-bit sin pérdida&amp;rdquo; casi siempre tiene letra pequeña.&lt;/strong> El binario puro {-1, +1} (1 bit) pierde la capacidad de representar el cero, que en transformers es importante (muchos pesos efectivamente nulos). Por eso el verdadero estado del arte de baja densidad es &lt;strong>ternario&lt;/strong> (1.58 bits), no binario: el cero vale su 0.58 de bit extra. Cuando un paper anuncia &amp;ldquo;1-bit&amp;rdquo;, conviene mirar si (a) es realmente 1 bit o 1.58 redondeado hacia abajo en el titular, (b) &amp;ldquo;sin pérdida&amp;rdquo; se mide en perplexity de WikiText (fácil) o en benchmarks de razonamiento (donde el colapso aparece), y (c) compara contra un baseline del mismo tamaño efectivo o contra un modelo mucho mayor para inflar la ventaja.&lt;/p>
&lt;p>&lt;strong>Perplexity plana ≠ calidad preservada.&lt;/strong> La perplexity en un corpus genérico es la métrica más indulgente con la cuantización agresiva. Un modelo 2-bit puede tener perplexity casi idéntica al BF16 y a la vez caer 10 puntos en GSM8K o en un benchmark de código, porque el razonamiento multi-paso amplifica errores que la perplexity media no ve. Desconfía de cualquier claim sub-4-bit que solo reporte perplexity. Como ya dijimos en &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">el post de quantization&lt;/a>, la pérdida hay que medirla en la tarea de destino.&lt;/p>
&lt;p>&lt;strong>Comparabilidad de hardware.&lt;/strong> Los números de &amp;ldquo;X veces más rápido&amp;rdquo; del ternario solo aplican &lt;strong>con los kernels especializados&lt;/strong> (bitnet.cpp) y en el hardware donde la aritmética suma/resta gana de verdad. En una GPU con tensor cores diseñadas para FP16/FP8, un kernel ternario ingenuo puede ser &lt;strong>más lento&lt;/strong> que INT4 bien optimizado, porque desaprovecha el silicio. La ventaja del ternario es real, pero es una ventaja de &lt;strong>co-diseño&lt;/strong> (modelo + kernel + a veces hardware), no un flag que activas sobre tu stack actual. Cualquier benchmark que no especifique el kernel y el hardware exacto es ruido.&lt;/p>
&lt;h2 id="implicaciones-para-inferencia-on-premise">Implicaciones para inferencia on-premise&lt;/h2>
&lt;p>En la &lt;strong>RTX 4090 (24 GB, Ada Lovelace)&lt;/strong>: el régimen práctico hoy sigue siendo INT4 AWQ para modelos de 7-14B —resuelto, casi lossless, soportado nativamente—. El 2-bit SOTA (AQLM/QuIP#/QTIP) es viable y permite cargar modelos más grandes o más modelos a la vez, pero exige los kernels específicos de cada método y una calibración cara, y paga calidad. Tiene sentido cuando el cuello es la VRAM y aceptas el trade-off; no como default. El ternario en 4090 es experimental: sin tensor cores diseñadas para suma/resta ternaria, la ventaja de cómputo se diluye, aunque el ahorro de memoria se mantiene.&lt;/p>
&lt;p>En un &lt;strong>cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo)&lt;/strong>: aquí el default es FP8 (calidad casi indistinguible, throughput nativo) o INT4 AWQ para modelos que no caben en FP8. El sub-4-bit SOTA es para servir modelos enormes (200B+) cuando ni FP8 ni INT4 caben con el margen de KV cache que quieres, a costa de calidad y de complejidad de kernel. El ternario nativo, hoy, es objeto de investigación más que de producción: su promesa —tocar ambos techos del roofline— es mayor en CPU/edge (donde no hay tensor cores FP8 que aprovechar) que en un cluster H100, que ya tiene hardware FP8 dedicado.&lt;/p>
&lt;p>La regla de pulgar, junio 2026: &lt;strong>≥4-bit es ingeniería resuelta; 2-bit SOTA es una palanca real pero con coste de método y de calidad; ternario es una apuesta de arquitectura, no un ajuste de despliegue&lt;/strong>.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization para inferencia LLM: FP8, INT4, GGUF&lt;/a> — la base imprescindible: la matemática del scale+zero-point, GPTQ/AWQ y PTQ vs QAT que aquí se dan por sabidas; este post es su continuación hacia la frontera sub-4-bit.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fp8-end-to-end-pesos-kv-calidad/">FP8 end-to-end: pesos, KV y calidad&lt;/a> — el otro extremo del espectro, el régimen resuelto del datacenter donde la cuantización ya casi no cuesta calidad.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/knowledge-distillation-fundamentos/">Knowledge distillation&lt;/a> — la palanca complementaria: destilar reduce parámetros, cuantizar reduce bits por parámetro; a 2-bit suelen combinarse para llegar al footprint objetivo.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/poda-pruning-llm-fundamentos/">Poda de modelos LLM&lt;/a> — sparsidad y cuantización agresiva son ortogonales y se acumulan: 50 % sparso + 2-bit es otra ruta al mismo footprint que el ternario.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo&lt;/a> — los ~4 GB que reservamos para KV en la cuenta de la 4090 salen de aquí; cuantizar el cache es la otra mitad del presupuesto de memoria.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">El roofline invertido de los modelos pequeños&lt;/a> — por qué el ternario es especial: ataca a la vez el techo de memoria y el de cómputo, mientras INT4/FP8 solo tocan el de memoria.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/test-time-quantization-en-caliente/">Test-time quantization en caliente&lt;/a> — cuantizar dinámicamente en inferencia frente a la cuantización estática y calibrada que describen AQLM/QuIP#/QTIP.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/arquitecturas-nativas-device-moe-grano-fino/">Arquitecturas nativas device + MoE de grano fino&lt;/a> — el Q4 en device como punto de partida del que el sub-4-bit y el ternario son la siguiente frontera para edge.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/">QLoRA y multi-LoRA agresivo en SLM&lt;/a> — fine-tune sobre una base ya cuantizada; el límite de cuánto puedes comprimir la base antes de que el adapter no pueda recuperar la calidad.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Ma, S. et al. &lt;em>The Era of 1-bit LLMs: All Large Language Models are in 1.58 Bits&lt;/em> (BitNet b1.58). &lt;a href="https://arxiv.org/abs/2402.17764">https://arxiv.org/abs/2402.17764&lt;/a>&lt;/li>
&lt;li>Egiazarian, V., Panferov, A., Kuznedelev, D. et al. &lt;em>Extreme Compression of Large Language Models via Additive Quantization&lt;/em> (AQLM). &lt;a href="https://arxiv.org/abs/2401.06118">https://arxiv.org/abs/2401.06118&lt;/a>&lt;/li>
&lt;li>Tseng, A., Chee, J., Sun, Q., Kuleshov, V., De Sa, C. &lt;em>QuIP#: Even Better LLM Quantization with Hadamard Incoherence and Lattice Codebooks&lt;/em>. &lt;a href="https://arxiv.org/abs/2402.04396">https://arxiv.org/abs/2402.04396&lt;/a>&lt;/li>
&lt;li>Tseng, A., Sun, Q., Hou, D., De Sa, C. &lt;em>QTIP: Quantization with Trellises and Incoherence Processing&lt;/em>. &lt;a href="https://arxiv.org/abs/2406.11235">https://arxiv.org/abs/2406.11235&lt;/a>&lt;/li>
&lt;li>Frantar, E., Ashkboos, S., Hoefler, T., Alistarh, D. &lt;em>GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers&lt;/em>. &lt;a href="https://arxiv.org/abs/2210.17323">https://arxiv.org/abs/2210.17323&lt;/a>&lt;/li>
&lt;li>Lin, J., Tang, J., Tang, H., Yang, S., Dang, X., Han, S. &lt;em>AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration&lt;/em>. &lt;a href="https://arxiv.org/abs/2306.00978">https://arxiv.org/abs/2306.00978&lt;/a>&lt;/li>
&lt;li>Google DeepMind. &lt;em>Gemma 3 QAT (Quantization-Aware Training) models&lt;/em> — blog oficial: &lt;a href="https://developers.googleblog.com/en/gemma-3-quantized-aware-trained-state-of-the-art-ai-to-consumer-gpus/">https://developers.googleblog.com/en/gemma-3-quantized-aware-trained-state-of-the-art-ai-to-consumer-gpus/&lt;/a>&lt;/li>
&lt;li>Microsoft. &lt;em>bitnet.cpp&lt;/em> — kernels de inferencia ternaria 1-bit: &lt;a href="https://github.com/microsoft/BitNet">https://github.com/microsoft/BitNet&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Test-time quantization: cuantizar en caliente sin dataset de calibración</title><link>https://blog.lo0.es/posts/test-time-quantization-en-caliente/</link><pubDate>Tue, 09 Jun 2026 02:00:00 +0000</pubDate><guid>https://blog.lo0.es/posts/test-time-quantization-en-caliente/</guid><description>&lt;blockquote>
&lt;p>Este post es la continuación natural de &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization para inferencia LLM&lt;/a>, que conviene leer primero: allí están GPTQ, AWQ, el scale + zero-point y por qué los outliers de activación son el problema central. Aquí no discutimos &lt;em>cuántos bits&lt;/em> usar, sino &lt;strong>cuándo y con qué información se calculan las escalas&lt;/strong>: offline contra un corpus (PTQ) o en caliente contra el tráfico real (TTQ).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>La cuantización activation-aware (AWQ, SmoothQuant) decide qué canales proteger midiendo la magnitud de las activaciones sobre un &lt;strong>dataset de calibración&lt;/strong> en un &lt;strong>pase offline&lt;/strong>, antes de desplegar. El supuesto implícito es que ese corpus representa el tráfico futuro. Pero los outliers de activación —los canales de magnitud 10-100× la mediana que dominan el error de cuantización— &lt;strong>dependen del input&lt;/strong>: cambian con el dominio, el idioma y la distribución del cliente. Cuando el tráfico real se aleja de la calibración, las escalas fijas dejan de ser óptimas y la calidad cae. &lt;strong>Test-time quantization (TTQ)&lt;/strong> elimina el corpus y el pase offline: deriva las escalas activation-aware &lt;strong>en tiempo de inferencia&lt;/strong>, a partir de las activaciones que realmente se observan, por token o por batch. La contrapartida es honesta y no menor: introduce &lt;strong>overhead en runtime&lt;/strong> —calcular estadísticas, detectar outliers, recomputar escalas en cada step— que compite directamente con el ahorro de cuantizar. En modelos pequeños ese overhead pesa proporcionalmente más, porque el forward es corto y los costes fijos por step dominan (el marco está en &lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">roofline invertido para SLM&lt;/a>). TTQ es &lt;strong>ortogonal&lt;/strong> al formato: no es un competidor de INT4 o FP8, es una forma distinta de derivar &lt;em>s&lt;/em>. Compensa cuando no hay pipeline de calibración, cuando la distribución del tráfico es cambiante o desconocida, y en multitenant donde no existe un corpus representativo.&lt;/p>
&lt;h2 id="estás-aquí-deploy">Estás aquí: DEPLOY&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Deploy">
&lt;defs>&lt;marker id="ttqm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">Estás aquí: DEPLOY · derivar escalas de cuantización en caliente&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" rx="6" fill="#f4f4f4" stroke="#444" stroke-width="1.4"/>&lt;text x="85" y="58" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" rx="6" fill="#f4f4f4" stroke="#444" stroke-width="1.4"/>&lt;text x="210" y="58" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" rx="6" fill="#f4f4f4" stroke="#444" stroke-width="1.4"/>&lt;text x="335" y="58" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" rx="6" fill="#7ad88f" stroke="#444" stroke-width="3"/>&lt;text x="460" y="58" text-anchor="middle" fill="#111" font-size="12" font-weight="600">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" rx="6" fill="#f4f4f4" stroke="#444" stroke-width="1.4"/>&lt;text x="585" y="58" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" rx="6" fill="#f4f4f4" stroke="#444" stroke-width="1.4"/>&lt;text x="710" y="58" text-anchor="middle" fill="currentColor" font-size="12" font-weight="600">6 · Retrain&lt;/text>
&lt;path d="M140,52 L155,52" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttqm)"/>
&lt;path d="M265,52 L280,52" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttqm)"/>
&lt;path d="M390,52 L405,52" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttqm)"/>
&lt;path d="M515,52 L530,52" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttqm)"/>
&lt;path d="M640,52 L655,52" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttqm)"/>
&lt;path d="M710,72 L710,82 L85,82 L85,72" stroke="#888" stroke-width="1.2" fill="none" stroke-dasharray="4 2" marker-end="url(#ttqm)"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-analogía-el-sastre-que-toma-medidas-frente-a-las-tallas-pre-confeccionadas">La analogía: el sastre que toma medidas frente a las tallas pre-confeccionadas&lt;/h2>
&lt;p>Una tienda de ropa tiene dos formas de vestir a un cliente.&lt;/p>
&lt;p>La primera es &lt;strong>vender tallas pre-confeccionadas&lt;/strong>. La fábrica midió en su día a un &amp;ldquo;cliente medio&amp;rdquo; —un maniquí promedio construido sobre una muestra de población— y cortó las prendas según esas medidas. Cuando entra un cliente, le das la talla que más se le acerca. Es rapidísimo: la prenda ya está cosida, solo se entrega. El problema aparece cuando el cliente no se parece al maniquí promedio: si tiene los hombros mucho más anchos que la media —su outlier particular—, la talla estándar le tira o le sobra tela, porque se cortó protegiendo &lt;em>otras&lt;/em> zonas. Esto es la &lt;strong>PTQ offline calibrada&lt;/strong>: AWQ midió la importancia de cada canal sobre un corpus y fijó las escalas de una vez; rápido en inferencia, pero ciego al cliente concreto.&lt;/p>
&lt;p>La segunda es &lt;strong>el sastre que toma medidas en el momento&lt;/strong>. Cuando entra el cliente, el sastre saca el metro, mide &lt;em>a ese cliente&lt;/em>, detecta dónde está su volumen particular y ajusta el corte a su anatomía real. El resultado encaja mejor, sobre todo en los clientes que se salen del molde. Pero cada cliente cuesta tiempo: medir, marcar, decidir. Esto es &lt;strong>TTQ&lt;/strong>: las escalas se derivan en caliente de las activaciones que ese input genera realmente.&lt;/p>
&lt;p>La analogía se sostiene en tres detalles:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>El maniquí promedio = el dataset de calibración.&lt;/strong> Si la población que entra a la tienda se parece al maniquí, las tallas funcionan; si no, fallan en los extremos.&lt;/li>
&lt;li>&lt;strong>Tomar medidas en cada cliente = calcular estadísticas de activación por token/batch.&lt;/strong> Mejor ajuste, pero un coste fijo que se paga en &lt;em>cada&lt;/em> prenda.&lt;/li>
&lt;li>&lt;strong>Los hombros anchos = los canales outlier de activación.&lt;/strong> Son precisamente las zonas donde el ajuste importa y donde la talla genérica más se equivoca.&lt;/li>
&lt;/ul>
&lt;p>El sastre gana cuando los clientes son variados o desconocidos. Pierde cuando tienes una población homogénea y un maniquí que la representa bien: ahí pagar la medición en cada cliente es tirar el tiempo.&lt;/p>
&lt;h2 id="el-problema-que-ttq-resuelve-la-calibración-fija-envejece-con-el-tráfico">El problema que TTQ resuelve: la calibración fija envejece con el tráfico&lt;/h2>
&lt;p>Recordemos del &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">post de quantization&lt;/a> qué hacen exactamente AWQ y SmoothQuant. No cuantizan todos los canales por igual: identifican el ~1 % de canales cuyas activaciones tienen magnitud grande —los &lt;em>salient channels&lt;/em>— y los protegen escalándolos antes de cuantizar. Para medir esa importancia necesitan ver activaciones, y las ven sobre un &lt;strong>dataset de calibración&lt;/strong> (128-512 muestras, típicamente WikiText o un slice del dominio) en un &lt;strong>pase offline&lt;/strong> previo al despliegue.&lt;/p>
&lt;p>El supuesto es fuerte: que la distribución de activaciones del corpus de calibración &lt;strong>representa la del tráfico de producción&lt;/strong>. Dos razones por las que ese supuesto se rompe:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Los outliers de activación dependen del input.&lt;/strong> No son una propiedad fija del modelo como los pesos. El canal que es outlier procesando código C++ puede no serlo procesando árabe conversacional o JSON de logs. La magnitud y la posición de los picos cambian con el dominio, el idioma y el formato de entrada.&lt;/li>
&lt;li>&lt;strong>El tráfico real rara vez es el corpus.&lt;/strong> Calibras con WikiText en inglés y el cliente te manda tickets de soporte en español con tablas pegadas. La calibración protegió los canales que &lt;em>WikiText&lt;/em> activaba, no los que activa el tráfico real. Las escalas son subóptimas justo donde el cliente vive.&lt;/li>
&lt;/ol>
&lt;p>El resultado es &lt;strong>degradación dependiente de la distribución&lt;/strong>: el modelo cuantizado mantiene la calidad mientras el input se parece a la calibración y la pierde a medida que se aleja. El caso más incómodo es el &lt;strong>multitenant&lt;/strong>: si sirves a clientes con dominios distintos desde el mismo modelo cuantizado, no existe un único corpus representativo; cualquier calibración fija favorece a unos tenants y penaliza a otros.&lt;/p>
&lt;h2 id="el-mecanismo-de-ttq-medir-las-activaciones-reales-y-escalar-en-caliente">El mecanismo de TTQ: medir las activaciones reales y escalar en caliente&lt;/h2>
&lt;p>TTQ (arXiv:2603.19296, marzo 2026) propone derivar la cuantización &lt;strong>activation-aware en tiempo de inferencia&lt;/strong>, sin pase offline ni dataset de calibración. La idea, en su forma desnuda y conceptual:&lt;/p>
&lt;p>&lt;strong>Paso 1 — Observar.&lt;/strong> Cuando llega el tensor de activaciones &lt;code>X&lt;/code> a una capa lineal (por token o por batch), se calculan estadísticas baratas sobre los canales: una medida de tendencia central (mediana o media de magnitud) y una de dispersión por canal. Esto es el equivalente a que AWQ mirase su corpus, pero hecho sobre las activaciones que &lt;em>de verdad&lt;/em> están entrando ahora.&lt;/p>
&lt;p>&lt;strong>Paso 2 — Detectar outliers en caliente.&lt;/strong> Con esas estadísticas se identifican los canales cuya magnitud se dispara respecto a la mediana del tensor —el criterio típico es un umbral del estilo &amp;ldquo;magnitud &amp;gt; k × mediana&amp;rdquo;. Son los canales que, si se cuantizan con la misma escala que el resto, disparan el error.&lt;/p>
&lt;p>&lt;strong>Paso 3 — Derivar escalas y segregar.&lt;/strong> Para los canales normales se calcula una escala que aprovecha el rango; para los outliers se aplica un tratamiento distinto —una escala propia, o mantenerlos en precisión más alta— al estilo &lt;em>mixed-precision en caliente&lt;/em>. Es la misma filosofía que LLM.int8() (segregar outliers a FP16) o AWQ (escalar salient channels), pero con el umbral y las escalas &lt;strong>recalculados sobre el input actual&lt;/strong>, no congelados desde la calibración.&lt;/p>
&lt;p>&lt;strong>Paso 4 — Cuantizar y multiplicar.&lt;/strong> Con las escalas frescas se cuantiza y se ejecuta el GEMM. Las activaciones que entran al siguiente layer compensan el reescalado, igual que en AWQ, para que la matemática se cancele.&lt;/p>
&lt;p>La diferencia clave con AWQ no está en &lt;em>qué&lt;/em> se hace (proteger outliers de activación) sino en &lt;em>cuándo&lt;/em> y &lt;em>contra qué&lt;/em>: AWQ lo decide una vez, offline, contra un corpus; TTQ lo decide en cada step, en caliente, contra el tráfico real. Es la traslación a inferencia de la idea de &amp;ldquo;test-time&amp;rdquo;: adaptar el cómputo a la muestra concreta que tienes delante en lugar de a un promedio precomputado.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="PTQ offline calibrada frente a TTQ en caliente">
&lt;defs>&lt;marker id="ttq2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="195" y="22" text-anchor="middle" fill="currentColor" font-size="13" font-weight="700">PTQ offline-calibrada (AWQ / GPTQ)&lt;/text>
&lt;text x="585" y="22" text-anchor="middle" fill="currentColor" font-size="13" font-weight="700">TTQ en-caliente&lt;/text>
&lt;line x1="390" y1="35" x2="390" y2="285" stroke="#bbb" stroke-width="1" stroke-dasharray="4 3"/>
&lt;p>&lt;rect x="30" y="45" width="150" height="38" rx="6" fill="#ffe6d6" stroke="#a05a2c" stroke-width="1.4"/>&lt;text x="105" y="68" text-anchor="middle" fill="#111" font-size="11" font-weight="600">dataset calibración&lt;/text>
&lt;rect x="210" y="45" width="150" height="38" rx="6" fill="#ffe6d6" stroke="#a05a2c" stroke-width="1.4"/>&lt;text x="285" y="64" text-anchor="middle" fill="#111" font-size="11" font-weight="600">pase OFFLINE&lt;/text>&lt;text x="285" y="78" text-anchor="middle" fill="#444" font-size="10">fija escalas s, outliers&lt;/text>
&lt;path d="M180,64 L210,64" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;rect x="120" y="105" width="150" height="34" rx="6" fill="#f0f0f0" stroke="#444" stroke-width="1.4"/>&lt;text x="195" y="126" text-anchor="middle" fill="#111" font-size="11" font-weight="600">escalas CONGELADAS&lt;/text>
&lt;path d="M285,83 L285,98 L195,98 L195,105" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;rect x="30" y="165" width="150" height="34" rx="6" fill="#d6eaff" stroke="#1f5fa8" stroke-width="1.4"/>&lt;text x="105" y="186" text-anchor="middle" fill="#111" font-size="11" font-weight="600">input parecido → OK&lt;/text>
&lt;rect x="210" y="165" width="150" height="34" rx="6" fill="#f6caca" stroke="#a52a2a" stroke-width="1.4"/>&lt;text x="285" y="182" text-anchor="middle" fill="#111" font-size="11" font-weight="600">input lejano →&lt;/text>&lt;text x="285" y="195" text-anchor="middle" fill="#a52a2a" font-size="10" font-weight="600">degradación&lt;/text>
&lt;path d="M150,139 L105,165" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;path d="M240,139 L285,165" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;text x="195" y="225" text-anchor="middle" fill="#444" font-size="10">overhead inferencia ≈ 0 · calidad depende de la calibración&lt;/text>&lt;/p>
&lt;p>&lt;rect x="510" y="45" width="150" height="38" rx="6" fill="#d9f5d6" stroke="#2a7a40" stroke-width="1.4"/>&lt;text x="585" y="62" text-anchor="middle" fill="#111" font-size="11" font-weight="600">activaciones REALES&lt;/text>&lt;text x="585" y="77" text-anchor="middle" fill="#444" font-size="10">del tráfico actual&lt;/text>
&lt;rect x="510" y="100" width="150" height="34" rx="6" fill="#d9f5d6" stroke="#2a7a40" stroke-width="1.4"/>&lt;text x="585" y="115" text-anchor="middle" fill="#111" font-size="11" font-weight="600">medir + detectar&lt;/text>&lt;text x="585" y="128" text-anchor="middle" fill="#444" font-size="10">outliers EN CALIENTE&lt;/text>
&lt;path d="M585,83 L585,100" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;rect x="510" y="151" width="150" height="34" rx="6" fill="#d9f5d6" stroke="#2a7a40" stroke-width="1.4"/>&lt;text x="585" y="166" text-anchor="middle" fill="#111" font-size="11" font-weight="600">escalas FRESCAS&lt;/text>&lt;text x="585" y="179" text-anchor="middle" fill="#444" font-size="10">por token / batch&lt;/text>
&lt;path d="M585,134 L585,151" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;rect x="510" y="202" width="150" height="34" rx="6" fill="#fff5b0" stroke="#9a8400" stroke-width="1.4"/>&lt;text x="585" y="217" text-anchor="middle" fill="#111" font-size="11" font-weight="600">cuantizar + GEMM&lt;/text>&lt;text x="585" y="230" text-anchor="middle" fill="#9a6b00" font-size="10" font-weight="600">+ overhead por step&lt;/text>
&lt;path d="M585,185 L585,202" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ttq2)"/>
&lt;text x="585" y="258" text-anchor="middle" fill="#444" font-size="10">sin corpus · calidad robusta a la distribución · overhead ≠ 0&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;h2 id="las-matemáticas-que-importan">Las matemáticas que importan&lt;/h2>
&lt;h3 id="el-error-de-cuantizar-un-outlier-con-la-escala-equivocada">El error de cuantizar un outlier con la escala equivocada&lt;/h3>
&lt;p>Recordemos la cuantización uniforme afín del &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">post base&lt;/a>: un código entero &lt;code>q = round(x/s) - z&lt;/code> con escala &lt;code>s&lt;/code> y zero-point &lt;code>z&lt;/code>, y reconstrucción &lt;code>x̂ = s·(q + z)&lt;/code>. Para un cuantizador de &lt;code>b&lt;/code> bits con rango simétrico, la escala que cubre un tensor de magnitud máxima &lt;code>M&lt;/code> es aproximadamente &lt;code>s = M / (2^{b-1} - 1)&lt;/code>. El error de redondeo de cada elemento está acotado por media escala: &lt;code>|x - x̂| ≤ s/2&lt;/code>.&lt;/p>
&lt;p>Aquí está el problema del outlier. La escala &lt;code>s&lt;/code> se elige para cubrir el valor &lt;strong>más grande&lt;/strong> del grupo. Si un canal tiene magnitud 30× la mediana y compartes una sola escala con el resto del tensor, esa magnitud manda: &lt;code>M&lt;/code> es el outlier, así que &lt;code>s&lt;/code> se infla 30× respecto a lo que necesitaría la mayoría. El error absoluto de redondeo de los valores normales sube proporcionalmente.&lt;/p>
&lt;p>Cuenta concreta. Tomemos un grupo donde la mediana de magnitudes es 1.0 y un canal outlier vale 30.0, cuantizado a INT4 (&lt;code>b = 4&lt;/code>, niveles ±7):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Con escala compartida&lt;/strong>, &lt;code>s = 30 / 7 ≈ 4.29&lt;/code>. El error de redondeo de un valor típico (magnitud ~1) es de hasta &lt;code>s/2 ≈ 2.14&lt;/code>. Es decir, &lt;strong>el error sobre los valores normales es del orden de su propio valor&lt;/strong>: el outlier ha destruido la resolución de todo lo demás. Error relativo de un valor de magnitud 1: hasta ~214 %.&lt;/li>
&lt;li>&lt;strong>Segregando el outlier&lt;/strong> (lo sacas a FP16 o le das su propia escala) y cuantizando el resto con &lt;code>M = 1&lt;/code>, &lt;code>s = 1/7 ≈ 0.143&lt;/code>. El error de un valor típico baja a &lt;code>s/2 ≈ 0.071&lt;/code>, ~7 % relativo. &lt;strong>Treinta veces menos error&lt;/strong> sobre la mayoría de los pesos del grupo.&lt;/li>
&lt;/ul>
&lt;p>Esa es toda la razón de ser de la cuantización activation-aware: &lt;strong>detectar y tratar aparte el ~1 % de canales que, de no segregarse, secuestran la escala&lt;/strong>. AWQ lo hace contra el corpus; TTQ lo hace contra el input real. Y si el canal que es outlier &lt;em>en producción&lt;/em> no era outlier &lt;em>en la calibración&lt;/em>, AWQ no lo protegió: cuantizó el tráfico real con la escala inflada del caso de arriba. Ahí TTQ gana precisión.&lt;/p>
&lt;h3 id="el-overhead-el-coste-de-medir-en-cada-step">El overhead: el coste de medir en cada step&lt;/h3>
&lt;p>El precio es simétrico. Calcular las estadísticas por token —magnitudes por canal, mediana o percentil, umbral de outlier, escalas— son reducciones sobre el tensor de activación que &lt;strong>no existían&lt;/strong> en el forward con escalas congeladas. Llamemos:&lt;/p>
&lt;ul>
&lt;li>&lt;code>T&lt;/code> = tiempo del forward por token con escalas fijas (PTQ estática), en µs.&lt;/li>
&lt;li>&lt;code>Δ&lt;/code> = coste extra por token de derivar las estadísticas y escalas en caliente, en µs.&lt;/li>
&lt;/ul>
&lt;p>El overhead relativo es simplemente:&lt;/p>
&lt;p>$$\text{overhead} = \frac{\Delta}{T}$$&lt;/p>
&lt;p>La clave es que &lt;code>Δ&lt;/code> es relativamente &lt;strong>fijo por step&lt;/strong> (depende del número de canales y capas, no de cuánto trabajo &amp;ldquo;útil&amp;rdquo; haga el modelo), mientras que &lt;code>T&lt;/code> escala con el tamaño del modelo. Por eso el cociente se comporta de forma muy distinta según el modelo:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Modelo grande&lt;/strong> (p. ej. 70B): &lt;code>T&lt;/code> es grande —cada forward mueve decenas de GB de pesos desde HBM—. Si &lt;code>Δ ≈ 8 µs&lt;/code> y &lt;code>T ≈ 800 µs&lt;/code>, el overhead es &lt;code>8/800 = 1 %&lt;/code>. Despreciable frente al ahorro de cuantizar.&lt;/li>
&lt;li>&lt;strong>SLM&lt;/strong> (p. ej. 1B): &lt;code>T&lt;/code> es pequeño —el forward por token es corto—. Con el mismo &lt;code>Δ ≈ 8 µs&lt;/code> y &lt;code>T ≈ 60 µs&lt;/code>, el overhead es &lt;code>8/60 ≈ 13 %&lt;/code>. Ya no es despreciable: se come buena parte de lo que ganaste cuantizando.&lt;/li>
&lt;/ul>
&lt;p>Esto conecta directamente con el &lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">roofline invertido para modelos pequeños&lt;/a>: en SLM los &lt;strong>costes fijos por step&lt;/strong> (lanzamiento de kernels, sincronizaciones, overheads que no escalan con el modelo) pesan proporcionalmente más, porque hay menos trabajo útil entre los que repartirlos. El &lt;code>Δ&lt;/code> de TTQ es exactamente uno de esos costes fijos. Per-batch en lugar de per-token amortiza &lt;code>Δ&lt;/code> entre todos los tokens del batch y baja el overhead relativo, a costa de escalas menos finas; es el primer parámetro a tocar.&lt;/p>
&lt;p>La conclusión incómoda: TTQ regala robustez a la distribución pero &lt;strong>gasta parte del presupuesto de aceleración en medir&lt;/strong>, y en el régimen donde la aceleración más escasea —los SLM, los que más se despliegan en el edge— es donde ese gasto más duele. No es gratis; es un cambio de moneda.&lt;/p>
&lt;blockquote>
&lt;p>Nota de escepticismo metodológico: arXiv:2603.19296 es de &lt;strong>marzo de 2026&lt;/strong>, muy reciente, y a la fecha de este post no hay reproducciones independientes amplias. Las cifras de speedup y de calidad que circulen conviene tomarlas con la misma cautela que cualquier número sin metodología publicada: ¿qué hardware, qué tamaño de batch, qué &lt;code>Δ&lt;/code> real medido, contra qué baseline (PTQ bien calibrada o mal calibrada), en qué dominio? El argumento &lt;em>conceptual&lt;/em> —robustez a la distribución a cambio de overhead por step— es sólido; los multiplicadores concretos, pendientes de validación.&lt;/p>
&lt;/blockquote>
&lt;h2 id="qué-no-es-ttq-deslindando-del-resto-del-zoo">Qué NO es TTQ: deslindando del resto del zoo&lt;/h2>
&lt;p>TTQ se confunde fácilmente con técnicas vecinas. La distinción que importa es que &lt;strong>TTQ es el &lt;em>cómo&lt;/em> derivas las escalas, no el formato ni el momento del entrenamiento&lt;/strong>.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Técnica&lt;/th>
&lt;th>Cuándo se fijan las escalas&lt;/th>
&lt;th>Necesita corpus calibración&lt;/th>
&lt;th>Toca entrenamiento&lt;/th>
&lt;th>Es un formato&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>PTQ estática&lt;/strong> (GPTQ, AWQ)&lt;/td>
&lt;td>Offline, antes de desplegar&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>No&lt;/td>
&lt;td>No (usa INT4/INT8)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>QAT&lt;/strong>&lt;/td>
&lt;td>Durante el entrenamiento&lt;/td>
&lt;td>No (datos de train)&lt;/td>
&lt;td>Sí (re-entrena)&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>FP8 end-to-end&lt;/strong>&lt;/td>
&lt;td>En runtime, pero escalas simples por tensor&lt;/td>
&lt;td>Mínimo / ninguno&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;strong>Sí&lt;/strong> (E4M3/E5M2)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TTQ&lt;/strong>&lt;/td>
&lt;td>En runtime, activation-aware por token/batch&lt;/td>
&lt;td>&lt;strong>No&lt;/strong>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>No (ortogonal al formato)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Las cuatro distinciones, una a una:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Frente a PTQ estática (GPTQ/AWQ).&lt;/strong> Misma meta (proteger outliers), mismo formato posible (INT4), pero PTQ congela las decisiones offline contra un corpus y TTQ las recalcula en caliente. TTQ es, en cierto sentido, &amp;ldquo;AWQ sin la fase de calibración, pagada en runtime&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Frente a QAT.&lt;/strong> QAT mete la cuantización dentro del bucle de entrenamiento para que el modelo aprenda a ser robusto a ella; cuesta re-entrenar. TTQ no toca el entrenamiento: opera sobre un modelo ya entrenado, en inferencia. Son ataques en momentos opuestos del pipeline.&lt;/li>
&lt;li>&lt;strong>Frente a FP8 end-to-end.&lt;/strong> FP8 es un &lt;strong>formato&lt;/strong> con su propio rango logarítmico; su &amp;ldquo;dynamic scaling&amp;rdquo; calcula un escalar simple por tensor en runtime, pero no hace detección activation-aware de outliers por canal. TTQ podría, conceptualmente, derivar escalas en caliente &lt;em>para&lt;/em> un cuantizador FP8 o INT4: es ortogonal al formato.&lt;/li>
&lt;li>&lt;strong>TTQ es ortogonal al formato.&lt;/strong> Decide &lt;em>cómo&lt;/em> obtener &lt;code>s&lt;/code>, no en cuántos bits guardas &lt;code>q&lt;/code>. Puedes imaginar &amp;ldquo;TTQ sobre INT4&amp;rdquo; o &amp;ldquo;TTQ sobre FP8&amp;rdquo;. Lo que define a TTQ es la fuente de la escala —activaciones reales en caliente— no el ancho del código.&lt;/li>
&lt;/ul>
&lt;h2 id="cuándo-compensa-y-cuándo-no">Cuándo compensa (y cuándo no)&lt;/h2>
&lt;p>TTQ no es un reemplazo universal de AWQ. Es una herramienta para un perfil concreto de despliegue. &lt;strong>Compensa cuando:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>No tienes pipeline de calibración.&lt;/strong> Quieres desplegar un modelo cuantizado &lt;em>ya&lt;/em>, sin montar el dataset de calibración, ejecutar el pase offline ni validar que el corpus representa el tráfico. TTQ recorta esa fase entera: cargas el modelo y sirves.&lt;/li>
&lt;li>&lt;strong>La distribución del tráfico es cambiante o desconocida.&lt;/strong> Un asistente que un día recibe código y otro día contratos legales en otro idioma. Ninguna calibración fija cubre bien ambos; la adaptación en caliente sigue la distribución sin re-calibrar.&lt;/li>
&lt;li>&lt;strong>Multitenant sin corpus representativo.&lt;/strong> Sirves el mismo modelo a clientes con dominios dispares. No existe un corpus único que represente a todos; cualquier calibración fija crea ganadores y perdedores entre tenants. TTQ ajusta a cada input, sea del tenant que sea.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>No compensa cuando:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tienes un dominio estable y un buen corpus de calibración.&lt;/strong> Si tu tráfico es homogéneo y representativo, AWQ offline te da la misma calidad con &lt;strong>cero overhead en runtime&lt;/strong>. Pagar &lt;code>Δ&lt;/code> en cada token para reaprender lo que un corpus ya capturó es desperdicio.&lt;/li>
&lt;li>&lt;strong>Sirves SLM con SLA de latencia ajustado.&lt;/strong> Es justo el caso donde &lt;code>Δ/T&lt;/code> es alto. Si el modelo es pequeño y el TPOT importa, el overhead de medir puede borrar la ganancia de cuantizar. Mide tu &lt;code>Δ&lt;/code> real antes de asumir que sale a cuenta.&lt;/li>
&lt;li>&lt;strong>El batch es grande y compute-bound.&lt;/strong> Con concurrencia alta el forward ya no está memory-bound y el coste de las reducciones extra compite peor; conviene al menos amortizar &lt;code>Δ&lt;/code> per-batch.&lt;/li>
&lt;/ul>
&lt;h2 id="implicaciones-en-hardware-on-premise">Implicaciones en hardware on-premise&lt;/h2>
&lt;h3 id="en-una-rtx-4090-24-gb-ada-lovelace">En una RTX 4090 (24 GB, Ada Lovelace)&lt;/h3>
&lt;p>El caso natural de la 4090 es el SLM —Qwen 3 1.5B, Llama 3 8B AWQ-INT4— sirviendo a baja concurrencia. Es precisamente el régimen donde TTQ es más arriesgado: &lt;code>T&lt;/code> por token es pequeño y la 4090 no tiene FP8 nativo acelerado (lo discutimos en el &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">post de quantization&lt;/a>), así que las reducciones extra de TTQ corren en CUDA cores compitiendo por el mismo tiempo. Aquí la pregunta no es &amp;ldquo;¿mejora la calidad?&amp;rdquo; sino &amp;ldquo;¿el overhead me deja un TPOT aceptable?&amp;rdquo;. Si el tráfico es homogéneo, AWQ offline gana por simplicidad y latencia. TTQ solo justifica su &lt;code>Δ&lt;/code> si la distribución de inputs es genuinamente impredecible y la degradación de la calibración fija es medible.&lt;/p>
&lt;h3 id="en-un-cluster-genérico-4h100-sxm-320-gb-nvlink-fp8-nativo">En un cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo)&lt;/h3>
&lt;p>Aquí el cálculo se invierte parcialmente. Con modelos grandes &lt;code>T&lt;/code> es alto y el &lt;code>Δ/T&lt;/code> baja a la zona de pocos puntos porcentuales, así que el overhead de TTQ es más digerible. El caso de uso fuerte es el &lt;strong>multitenant&lt;/strong>: un cluster que sirve un modelo grande a clientes con dominios heterogéneos, donde no hay un corpus de calibración que contente a todos. Ahí la robustez a la distribución de TTQ tiene valor real y el overhead se diluye en un forward grande. Aun así, sobre H100 con FP8 nativo, el baseline a batir es exigente: FP8 estático casi no pierde calidad (ver tabla del post de quantization) y no cuesta nada en runtime. TTQ tiene que demostrar que su ganancia de robustez en los tenants outlier supera lo que regala en overhead. Con un paper de marzo de 2026 y sin reproducciones, esa demostración está pendiente.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>El coste de memoria de las estadísticas en caliente&lt;/strong>: buffers por canal, su impacto en el footprint y en la presión de cache.&lt;/li>
&lt;li>&lt;strong>Interacción con continuous batching&lt;/strong>: cómo se derivan escalas cuando un batch mezcla requests de dominios distintos en el mismo step.&lt;/li>
&lt;li>&lt;strong>TTQ + speculative decoding&lt;/strong>: si el draft y el target derivan escalas en caliente por separado, y cómo afecta eso a la tasa de aceptación.&lt;/li>
&lt;li>&lt;strong>Estabilidad numérica&lt;/strong>: qué pasa cuando un batch tiene un outlier extremo puntual que infla la escala de todos los tokens de ese step.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization para inferencia LLM&lt;/a> — la base imprescindible: scale + zero-point, GPTQ, AWQ y por qué los outliers de activación son el problema; TTQ es AWQ con las escalas derivadas en caliente en vez de offline.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">Roofline invertido para modelos pequeños&lt;/a> — por qué los costes fijos por step pesan más en SLM; explica directamente por qué el overhead &lt;code>Δ&lt;/code> de TTQ duele más en modelos pequeños.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cuantizacion-agresiva-sub-4-bit-ternario/">Cuantización agresiva sub-4-bit y ternario&lt;/a> — la frontera estática por debajo de 4 bits; complementa a TTQ, que ataca el &lt;em>cómo&lt;/em> de la escala en vez del &lt;em>cuántos bits&lt;/em>.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/">QLoRA y multi-LoRA agresivo en SLM&lt;/a> — adapters sobre un base cuantizado; el base podría derivar escalas en caliente mientras los adapters van en BF16.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fp8-end-to-end-pesos-kv-calidad/">FP8 end-to-end: pesos, KV y calidad&lt;/a> — el formato del datacenter Hopper/Blackwell; TTQ es ortogonal y podría derivar escalas para un cuantizador FP8.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo de la inferencia LLM&lt;/a> — el KV cache también se cuantiza; sus escalas son otro candidato a derivarse en caliente por la misma lógica.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/knowledge-distillation-fundamentos/">Knowledge distillation&lt;/a> — la otra vía para servir modelos pequeños robustos; destilar reduce el modelo, TTQ ajusta su cuantización al tráfico.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/decode-optimizaciones-vllm/">Optimizando el decode en vLLM&lt;/a> — donde se materializan en parámetros las palancas de cuantización en runtime para exprimir una 4090.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;em>TTQ: Activation-Aware Test-Time Quantization to Accelerate LLM Inference On The Fly&lt;/em> (marzo 2026). &lt;a href="https://arxiv.org/abs/2603.19296">https://arxiv.org/abs/2603.19296&lt;/a>&lt;/li>
&lt;li>Lin, J., Tang, J., Tang, H., Yang, S., Dang, X., Han, S. &lt;em>AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration&lt;/em> (MLSys 2024). &lt;a href="https://arxiv.org/abs/2306.00978">https://arxiv.org/abs/2306.00978&lt;/a>&lt;/li>
&lt;li>Frantar, E., Ashkboos, S., Hoefler, T., Alistarh, D. &lt;em>GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers&lt;/em> (ICLR 2023). &lt;a href="https://arxiv.org/abs/2210.17323">https://arxiv.org/abs/2210.17323&lt;/a>&lt;/li>
&lt;li>Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J., Han, S. &lt;em>SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models&lt;/em> (ICML 2023). &lt;a href="https://arxiv.org/abs/2211.10438">https://arxiv.org/abs/2211.10438&lt;/a>&lt;/li>
&lt;li>Dettmers, T., Lewis, M., Belkada, Y., Zettlemoyer, L. &lt;em>LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale&lt;/em> (NeurIPS 2022). &lt;a href="https://arxiv.org/abs/2208.07339">https://arxiv.org/abs/2208.07339&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Arquitecturas nativas para device: MoE de grano fino y pre-attention router</title><link>https://blog.lo0.es/posts/arquitecturas-nativas-device-moe-grano-fino/</link><pubDate>Tue, 09 Jun 2026 01:50:00 +0000</pubDate><guid>https://blog.lo0.es/posts/arquitecturas-nativas-device-moe-grano-fino/</guid><description>&lt;blockquote>
&lt;p>Este post es de la serie sobre rendimiento de inferencia en modelos pequeños. Es la cara arquitectónica de un problema que ya hemos mirado por el lado del régimen de cómputo (el roofline invertido del SLM) y por el lado de la carga de pesos en &lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">Del disco a la HBM&lt;/a>. Aquí la pregunta es distinta: ¿y si en lugar de adaptar un modelo grande al device, diseñamos el modelo &lt;em>para&lt;/em> el device desde el primer commit?&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El gesto por defecto para llevar un LLM a un portátil, un móvil o un edge box es &lt;strong>coger un denso pensado para cloud y comprimirlo&lt;/strong>: destilación, poda, cuantización. Es un gesto de &lt;em>reducción&lt;/em>: partes de algo grande y le quitas. SmallThinker (arXiv:2507.20984, SJTU IPADS + Zenergize AI) defiende el gesto inverso —&lt;em>diseñar desde cero&lt;/em>— y lo articula en tres piezas. &lt;strong>Primera: MoE de grano fino&lt;/strong>, muchos expertos pequeños con muy pocos activados por token, de modo que los parámetros totales &lt;code>N&lt;/code> (la capacidad) se desacoplan de los parámetros activados &lt;code>A&lt;/code> (el coste de cómputo por token). &lt;strong>Segunda: sparse FFN&lt;/strong>, sparsity de activación tipo ReLU dentro de cada bloque, que añade un segundo nivel de dispersión sobre el primero. &lt;strong>Tercera: un pre-attention router&lt;/strong> que predice qué expertos hará falta &lt;em>antes&lt;/em> de ejecutar el bloque de atención y lanza el prefetch de esos pesos desde SSD/flash en paralelo con el cómputo de la atención, ocultando la latencia de almacenamiento —que es el cuello de botella real cuando el modelo no cabe entero en RAM. Los autores reportan SmallThinker-4B-A0.6B y SmallThinker-21B-A3B superando ~20 tok/s en CPU de consumo con Q4_0, consumiendo ~1 GB y ~8 GB de RAM. Los números son interesantes y la dirección es correcta; la metodología de evaluación y el coste de calidad de activar tan poco merecen escepticismo, y a eso dedicamos la última parte.&lt;/p>
&lt;h2 id="la-analogía-el-bibliotecario-que-se-adelanta-a-tu-pedido">La analogía: el bibliotecario que se adelanta a tu pedido&lt;/h2>
&lt;p>Imagina una biblioteca enorme con una sala de lectura pequeña. Tú estás sentado en la sala con un único pupitre: ahí caben pocos libros a la vez (eso es la RAM). El grueso del fondo está en la trastienda, en estanterías largas y lentas de recorrer (eso es el SSD/flash). Y hay un bibliotecario.&lt;/p>
&lt;p>El método ingenuo: tú lees, llegas a un punto donde necesitas un libro concreto, lo pides, y entonces el bibliotecario se levanta, va a la trastienda, lo busca y vuelve. Mientras tanto, tú esperas con la página abierta sin avanzar. Cada vez que necesitas un libro nuevo, pagas el viaje completo a la trastienda. La sala de lectura está la mayor parte del tiempo esperando, no leyendo.&lt;/p>
&lt;p>El método de SmallThinker: el bibliotecario es listo y se adelanta. Mientras tú todavía estás leyendo el &lt;strong>índice&lt;/strong> del capítulo —averiguando de qué va, relacionando ideas, lo que en el modelo es el bloque de &lt;strong>atención&lt;/strong>—, él ya ha mirado por encima de tu hombro, ha &lt;strong>predicho&lt;/strong> qué tres o cuatro libros vas a pedir y se ha ido a la trastienda a buscarlos. Para cuando terminas el índice y formulas el pedido, los libros ya están sobre tu pupitre. No has esperado: el viaje a la trastienda ocurrió &lt;em>en paralelo&lt;/em> con tu lectura del índice.&lt;/p>
&lt;p>La analogía se sostiene en cuatro detalles:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>El pupitre pequeño es la RAM&lt;/strong>; la trastienda lenta es el &lt;strong>SSD/flash&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Los libros son los expertos&lt;/strong> del MoE: solo unos pocos están sobre el pupitre en cada momento.&lt;/li>
&lt;li>&lt;strong>Leer el índice es el bloque de atención&lt;/strong>; pedir y usar los libros es el bloque FFN/expertos.&lt;/li>
&lt;li>&lt;strong>El bibliotecario que predice y se adelanta es el pre-attention router&lt;/strong>: la predicción se hace antes, y el viaje a buscar (el prefetch) se solapa con la lectura del índice (la atención).&lt;/li>
&lt;/ul>
&lt;p>La pregunta cuantitativa que recorre todo el post es: ¿llega el bibliotecario a tiempo? Solo se oculta la espera si el viaje a la trastienda dura menos que tu lectura del índice. Esa es la condición &lt;code>t_{\text{atención}} \ge t_{\text{prefetch}}&lt;/code>, y la haremos con números.&lt;/p>
&lt;h2 id="comprimir-un-denso-vs-diseñar-para-device">Comprimir un denso vs. diseñar para device&lt;/h2>
&lt;p>Conviene poner los dos enfoques en frío, porque no son grados de lo mismo: son filosofías distintas.&lt;/p>
&lt;p>&lt;strong>Enfoque A — comprimir un denso pensado para cloud.&lt;/strong> Partes de, digamos, un modelo denso de 7B–14B entrenado para correr en una RTX 4090 (24 GB, Ada Lovelace) o en un cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo). Para meterlo en un device aplicas tres palancas, cada una con su post propio: &lt;a href="https://blog.lo0.es/posts/knowledge-distillation-fundamentos/">destilación&lt;/a> (entrenas un student pequeño que imita al teacher), &lt;a href="https://blog.lo0.es/posts/poda-pruning-llm-fundamentos/">poda&lt;/a> (eliminas pesos o estructuras enteras) y cuantización agresiva (bajas a 4 bits o menos). El modelo resultante &lt;strong>sigue siendo denso&lt;/strong>: todos sus parámetros se activan en cada token. Has reducido el número de parámetros, pero el patrón de cómputo es el del cloud, solo que más pequeño.&lt;/p>
&lt;p>&lt;strong>Enfoque B — diseñar para device desde cero.&lt;/strong> Aquí las restricciones del device entran en la &lt;em>arquitectura&lt;/em>, no en una fase posterior de compresión. Las restricciones son tres y muy concretas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Cómputo débil.&lt;/strong> Una CPU de portátil o un SoC móvil hace órdenes de magnitud menos FLOPs que una GPU de datacenter. Esto empuja a minimizar los parámetros &lt;strong>activados&lt;/strong> por token, no los totales.&lt;/li>
&lt;li>&lt;strong>Poca RAM.&lt;/strong> No caben decenas de GB. Esto empuja a tener residente solo lo imprescindible y a &lt;em>streamear&lt;/em> el resto.&lt;/li>
&lt;li>&lt;strong>Almacenamiento lento.&lt;/strong> El SSD o la flash a la que te ves obligado a streamear tiene un ancho de banda muy inferior al de la HBM de una GPU. Esto convierte la I/O de almacenamiento en el cuello de botella, y empuja a &lt;em>ocultarla&lt;/em>.&lt;/li>
&lt;/ol>
&lt;p>SmallThinker es el enfoque B llevado al detalle: cada una de esas tres restricciones tiene una respuesta arquitectónica. El cómputo débil se ataca con MoE de grano fino + sparse FFN (minimizar &lt;code>A&lt;/code>). La RAM escasa se ataca con streaming desde SSD (residente ≈ &lt;code>A&lt;/code> + caché, no &lt;code>N&lt;/code>). El almacenamiento lento se ataca con el pre-attention router (ocultar la I/O tras la atención). No es casual que las tres piezas encajen: cada una resuelve una restricción, y juntas se refuerzan.&lt;/p>
&lt;p>Un matiz importante, para no caer en el hype: el enfoque B &lt;strong>no es gratis ni universalmente superior&lt;/strong>. Requiere entrenar un modelo nuevo (no reutilizas pesos existentes), y el techo de calidad de un modelo con &lt;code>A&lt;/code> muy pequeño está intrínsecamente acotado, como veremos. El argumento no es &amp;ldquo;B gana siempre&amp;rdquo;, sino &amp;ldquo;para el régimen del device, B ataca los cuellos correctos, y A solo los ataca de refilón&amp;rdquo;.&lt;/p>
&lt;h2 id="dos-niveles-de-sparsity">Dos niveles de sparsity&lt;/h2>
&lt;p>La idea central de capacidad es vieja y bien entendida en &lt;a href="https://blog.lo0.es/posts/moe-inference-fundamentos/">MoE&lt;/a>: separar &lt;strong>capacidad&lt;/strong> de &lt;strong>coste de cómputo&lt;/strong>. En un MoE, el modelo tiene &lt;code>N&lt;/code> parámetros totales repartidos en expertos, pero para cada token solo se activan &lt;code>A&lt;/code> parámetros (los del top-k de expertos que el router elige). El coste de cómputo por token escala con &lt;code>A&lt;/code>; la capacidad de conocimiento escala con &lt;code>N&lt;/code>. SmallThinker aplica esta idea en &lt;strong>dos niveles superpuestos&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Nivel 1 — MoE de grano fino.&lt;/strong> &amp;ldquo;Grano fino&amp;rdquo; significa muchos expertos pequeños en vez de pocos expertos grandes, con muy pocos activados por token. En vez de, digamos, 8 expertos de los que activas 2, tienes decenas de expertos de los que activas un puñado. Con expertos más pequeños, el mismo &lt;code>A&lt;/code> se reparte entre más combinaciones posibles, lo que da granularidad fina al router y mantiene &lt;code>A&lt;/code> muy bajo respecto a &lt;code>N&lt;/code>. El resultado es un cociente &lt;code>N/A&lt;/code> agresivo: mucha capacidad, poquísimo cómputo por token.&lt;/p>
&lt;p>&lt;strong>Nivel 2 — sparse FFN (sparsity de activación tipo ReLU).&lt;/strong> Este nivel es ortogonal y opera &lt;em>dentro&lt;/em> de cada FFN. Con una no-linealidad tipo ReLU, una fracción grande de las neuronas de la capa intermedia produce exactamente cero para un token dado. Una neurona que sale a cero no contribuye nada a la salida: su multiplicación matriz-vector se puede saltar. Esto es &lt;em>sparsity de activación&lt;/em>: predecible token a token, y aprovechable para no cargar ni multiplicar las filas/columnas de peso correspondientes a neuronas inactivas. Es el mismo fenómeno que explotan trabajos como Deja Vu o PowerInfer; SmallThinker lo incorpora de fábrica eligiendo activaciones que lo favorecen.&lt;/p>
&lt;p>El efecto combinado, en una frase: &lt;strong>&lt;code>N&lt;/code> grande (capacidad), &lt;code>A&lt;/code> minúsculo (coste de cómputo por token ≈ proporcional a &lt;code>A&lt;/code>)&lt;/strong>, y además dentro de ese &lt;code>A&lt;/code> una fracción de las multiplicaciones se ahorra por la sparsity de activación. Es sparsity sobre sparsity.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="MoE clásico vs MoE de grano fino con sparse FFN">
&lt;defs>&lt;marker id="ar1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="195" y="22" text-anchor="middle" font-size="13" font-weight="600" fill="currentColor">MoE clásico (grano grueso)&lt;/text>
&lt;text x="585" y="22" text-anchor="middle" font-size="13" font-weight="600" fill="currentColor">MoE de grano fino + sparse FFN&lt;/text>
&lt;line x1="390" y1="35" x2="390" y2="285" stroke="currentColor" stroke-width="1" stroke-dasharray="4 3"/>
&lt;!-- clasico: 8 expertos grandes, 2 activos -->
&lt;p>&lt;text x="40" y="50" font-size="11" fill="currentColor">8 expertos grandes · activa 2&lt;/text>
&lt;rect x="40" y="60" width="64" height="40" fill="#1f5fa8" stroke="#13335c" stroke-width="1.4"/>
&lt;rect x="114" y="60" width="64" height="40" fill="#cfd8e3" stroke="#7b8794" stroke-width="1.2"/>
&lt;rect x="188" y="60" width="64" height="40" fill="#cfd8e3" stroke="#7b8794" stroke-width="1.2"/>
&lt;rect x="262" y="60" width="64" height="40" fill="#1f5fa8" stroke="#13335c" stroke-width="1.4"/>
&lt;rect x="40" y="106" width="64" height="40" fill="#cfd8e3" stroke="#7b8794" stroke-width="1.2"/>
&lt;rect x="114" y="106" width="64" height="40" fill="#cfd8e3" stroke="#7b8794" stroke-width="1.2"/>
&lt;rect x="188" y="106" width="64" height="40" fill="#cfd8e3" stroke="#7b8794" stroke-width="1.2"/>
&lt;rect x="262" y="106" width="64" height="40" fill="#cfd8e3" stroke="#7b8794" stroke-width="1.2"/>
&lt;text x="183" y="172" text-anchor="middle" font-size="11" fill="currentColor">A grande por experto · granularidad gruesa&lt;/text>&lt;/p>
&lt;!-- fino: muchos expertos pequeños, varios activos pero A total bajo -->
&lt;p>&lt;text x="410" y="50" font-size="11" fill="currentColor">muchos expertos pequeños · activa pocos&lt;/text>
&lt;g>
&lt;rect x="410" y="60" width="28" height="22" fill="#2a7a40" stroke="#1a4d29" stroke-width="1.2"/>
&lt;rect x="444" y="60" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="478" y="60" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="512" y="60" width="28" height="22" fill="#2a7a40" stroke="#1a4d29" stroke-width="1.2"/>
&lt;rect x="546" y="60" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="580" y="60" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="614" y="60" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="648" y="60" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="410" y="88" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="444" y="88" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="478" y="88" width="28" height="22" fill="#2a7a40" stroke="#1a4d29" stroke-width="1.2"/>
&lt;rect x="512" y="88" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="546" y="88" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="580" y="88" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;rect x="614" y="88" width="28" height="22" fill="#2a7a40" stroke="#1a4d29" stroke-width="1.2"/>
&lt;rect x="648" y="88" width="28" height="22" fill="#cfe3d5" stroke="#7b948a" stroke-width="1"/>
&lt;/g>
&lt;text x="543" y="128" text-anchor="middle" font-size="11" fill="currentColor">A total bajo · granularidad fina&lt;/text>&lt;/p>
&lt;!-- sparse FFN dentro -->
&lt;p>&lt;text x="410" y="158" font-size="11" font-weight="600" fill="currentColor">+ sparse FFN dentro de cada experto activo:&lt;/text>
&lt;g>
&lt;rect x="410" y="168" width="14" height="40" fill="#a48000"/>
&lt;rect x="428" y="168" width="14" height="40" fill="#f1e7c2" stroke="#b8a96a" stroke-width="0.8"/>
&lt;rect x="446" y="168" width="14" height="40" fill="#a48000"/>
&lt;rect x="464" y="168" width="14" height="40" fill="#f1e7c2" stroke="#b8a96a" stroke-width="0.8"/>
&lt;rect x="482" y="168" width="14" height="40" fill="#f1e7c2" stroke="#b8a96a" stroke-width="0.8"/>
&lt;rect x="500" y="168" width="14" height="40" fill="#a48000"/>
&lt;rect x="518" y="168" width="14" height="40" fill="#f1e7c2" stroke="#b8a96a" stroke-width="0.8"/>
&lt;rect x="536" y="168" width="14" height="40" fill="#f1e7c2" stroke="#b8a96a" stroke-width="0.8"/>
&lt;rect x="554" y="168" width="14" height="40" fill="#a48000"/>
&lt;rect x="572" y="168" width="14" height="40" fill="#f1e7c2" stroke="#b8a96a" stroke-width="0.8"/>
&lt;/g>
&lt;text x="600" y="192" font-size="11" fill="currentColor">neuronas a 0 (ReLU) →&lt;/text>
&lt;text x="600" y="206" font-size="11" fill="currentColor">se saltan en el cómputo&lt;/text>&lt;/p>
&lt;p>&lt;text x="40" y="245" font-size="11.5" font-weight="600" fill="currentColor">Capacidad = N (todos los expertos) · Coste/token ≈ A (activados) · y dentro de A, sparse FFN ahorra más&lt;/text>
&lt;text x="40" y="270" font-size="11" fill="currentColor">El truco: subir N sin subir A. La granularidad fina permite un cociente N/A mucho más agresivo.&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;h2 id="el-pre-attention-router-predecir-y-prefetchar">El pre-attention router: predecir y prefetchar&lt;/h2>
&lt;p>Aquí está la pieza específica del paper, y la que da nombre al post. El problema que resuelve es de &lt;em>scheduling de I/O&lt;/em>, no de calidad.&lt;/p>
&lt;p>Cuando el modelo no cabe entero en RAM, los pesos de los expertos viven en SSD/flash y se cargan bajo demanda. El flujo ingenuo de una capa MoE es secuencial: ejecutas la atención, luego el router decide qué expertos tocan, luego &lt;strong>cargas esos expertos desde SSD&lt;/strong> (esperando), luego ejecutas la FFN de esos expertos. El paso de carga es una espera pura: la CPU está bloqueada esperando bytes del SSD. En el régimen del device, donde el SSD es lento, ese tiempo de espera domina el step de decode.&lt;/p>
&lt;p>El &lt;strong>pre-attention router&lt;/strong> rompe la secuencialidad invirtiendo el orden de la decisión. La observación es que el router no necesita la salida de la atención de &lt;em>esta&lt;/em> misma capa para hacer una predicción razonable de qué expertos harán falta: puede predecirlo a partir del estado que ya tiene &lt;em>antes&lt;/em> de ejecutar la atención. Así que:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Antes&lt;/strong> de ejecutar el bloque de atención de la capa, el router predice los expertos que se necesitarán.&lt;/li>
&lt;li>Lanza el &lt;strong>prefetch&lt;/strong> de esos expertos desde SSD/flash de forma asíncrona.&lt;/li>
&lt;li>&lt;strong>En paralelo&lt;/strong>, la CPU ejecuta el bloque de atención —que es cómputo puro, no necesita el SSD.&lt;/li>
&lt;li>Cuando la atención termina, los expertos prefetchados ya están (idealmente) en RAM, y la FFN procede sin esperar.&lt;/li>
&lt;/ol>
&lt;p>El I/O de almacenamiento se ha &lt;strong>solapado&lt;/strong> con el cómputo de atención. Es exactamente el bibliotecario que va a la trastienda mientras tú lees el índice.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 270" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Flujo pre-attention router con prefetch solapado">
&lt;defs>&lt;marker id="ar2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;p>&lt;text x="20" y="22" font-size="12.5" font-weight="600" fill="currentColor">Ingenuo (secuencial): la carga desde SSD bloquea&lt;/text>
&lt;rect x="20" y="32" width="110" height="34" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4"/>
&lt;text x="75" y="53" text-anchor="middle" font-size="11" fill="#13335c">atención&lt;/text>
&lt;rect x="138" y="32" width="80" height="34" fill="#e6d0ff" stroke="#5a2db0" stroke-width="1.4"/>
&lt;text x="178" y="53" text-anchor="middle" font-size="11" fill="#3a1d70">router&lt;/text>
&lt;rect x="226" y="32" width="180" height="34" fill="#f6caca" stroke="#a52a2a" stroke-width="1.4"/>
&lt;text x="316" y="49" text-anchor="middle" font-size="11" fill="#6e1d1d">carga expertos desde SSD&lt;/text>
&lt;text x="316" y="61" text-anchor="middle" font-size="10" fill="#6e1d1d">(espera bloqueante)&lt;/text>
&lt;rect x="414" y="32" width="110" height="34" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.4"/>
&lt;text x="469" y="53" text-anchor="middle" font-size="11" fill="#1a4d29">FFN expertos&lt;/text>
&lt;path d="M130,49 L138,49" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ar2)"/>
&lt;path d="M218,49 L226,49" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ar2)"/>
&lt;path d="M406,49 L414,49" stroke="#666" stroke-width="1.4" fill="none" marker-end="url(#ar2)"/>
&lt;text x="540" y="53" font-size="11" fill="currentColor">t_total = t_att + t_load + t_ffn&lt;/text>&lt;/p>
&lt;line x1="20" y1="92" x2="760" y2="92" stroke="currentColor" stroke-width="0.8" stroke-dasharray="3 3"/>
&lt;p>&lt;text x="20" y="118" font-size="12.5" font-weight="600" fill="currentColor">Pre-attention router: el prefetch se solapa con la atención&lt;/text>&lt;/p>
&lt;p>&lt;text x="20" y="140" font-size="11" font-weight="600" fill="currentColor">hilo de cómputo (CPU)&lt;/text>
&lt;rect x="170" y="130" width="80" height="30" fill="#e6d0ff" stroke="#5a2db0" stroke-width="1.4"/>
&lt;text x="210" y="149" text-anchor="middle" font-size="10.5" fill="#3a1d70">router (pre)&lt;/text>
&lt;rect x="258" y="130" width="150" height="30" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4"/>
&lt;text x="333" y="149" text-anchor="middle" font-size="11" fill="#13335c">atención (t_att)&lt;/text>
&lt;rect x="416" y="130" width="120" height="30" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.4"/>
&lt;text x="476" y="149" text-anchor="middle" font-size="11" fill="#1a4d29">FFN expertos&lt;/text>&lt;/p>
&lt;p>&lt;text x="20" y="190" font-size="11" font-weight="600" fill="currentColor">hilo de I/O (SSD)&lt;/text>
&lt;rect x="258" y="180" width="130" height="30" fill="#ffe0a8" stroke="#a48000" stroke-width="1.4"/>
&lt;text x="323" y="199" text-anchor="middle" font-size="10.5" fill="#6b5400">prefetch expertos (t_prefetch)&lt;/text>&lt;/p>
&lt;path d="M250,145 C254,145 254,150 258,150" stroke="#5a2db0" stroke-width="1.2" fill="none"/>
&lt;path d="M250,150 L256,150 L256,195 L258,195" stroke="#a48000" stroke-width="1.2" fill="none" marker-end="url(#ar2)" stroke-dasharray="3 2"/>
&lt;line x1="258" y1="122" x2="258" y2="218" stroke="currentColor" stroke-width="0.7" stroke-dasharray="2 2"/>
&lt;line x1="408" y1="122" x2="408" y2="218" stroke="currentColor" stroke-width="0.7" stroke-dasharray="2 2"/>
&lt;p>&lt;text x="20" y="245" font-size="11.5" fill="currentColor">El prefetch queda oculto si &lt;tspan font-weight="700">t_att ≥ t_prefetch&lt;/tspan>: para cuando la atención termina, los expertos ya están en RAM.&lt;/text>
&lt;text x="20" y="262" font-size="11" fill="currentColor">Si t_prefetch &amp;gt; t_att, asoma una burbuja de espera (t_prefetch − t_att) antes de la FFN. Ese es el caso a evitar.&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;p>La &lt;strong>condición de ocultamiento&lt;/strong> es la desigualdad de arriba: el prefetch se oculta completamente si y solo si&lt;/p>
&lt;p>$$t_{\text{atención}} ;\ge; t_{\text{prefetch}}.$$&lt;/p>
&lt;p>Si la atención tarda más que cargar los expertos, la carga es gratis (ya estaba hecha). Si los expertos son demasiado grandes o el SSD demasiado lento, &lt;code>t_prefetch &amp;gt; t_att&lt;/code> y asoma una burbuja de espera igual a &lt;code>t_prefetch − t_att&lt;/code>. Por eso el diseño &lt;em>necesita&lt;/em> que &lt;code>A&lt;/code> sea pequeño (expertos pequeños → menos bytes a prefetchar → &lt;code>t_prefetch&lt;/code> bajo) y que el grano sea fino: las dos cosas que hace el nivel 1 de sparsity no son solo para ahorrar FLOPs, son para que el prefetch quepa debajo de la atención.&lt;/p>
&lt;h2 id="las-matemáticas-que-importan">Las matemáticas que importan&lt;/h2>
&lt;h3 id="footprint-de-memoria-n-residente-vs-a--caché">Footprint de memoria: N residente vs. A + caché&lt;/h3>
&lt;p>El parámetro que decide si el modelo cabe es cuánto tienes que tener &lt;strong>residente en RAM&lt;/strong> a la vez.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Todo en RAM.&lt;/strong> Si exiges que todos los expertos estén cargados, el footprint es &lt;code>\approx N&lt;/code> (todos los parámetros, multiplicados por bytes/parámetro según la cuantización). Para un 21B esto es prohibitivo en un device.&lt;/li>
&lt;li>&lt;strong>Streaming desde SSD.&lt;/strong> Si solo mantienes residentes los expertos activos más una caché de los recientes/probables, el footprint cae a &lt;code>\approx A + \text{caché}&lt;/code>. Los pesos que no están en RAM viven en SSD y se prefetchan cuando toca. Aquí está el ahorro real: el residente escala con &lt;code>A&lt;/code>, no con &lt;code>N&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>La parte no-experta del modelo (embeddings, atención, router, layernorms) sí está siempre residente, pero en un MoE de grano fino el grueso de &lt;code>N&lt;/code> está en los expertos, así que la aproximación &lt;code>residente ≈ A + caché + parte_densa&lt;/code> es buena.&lt;/p>
&lt;h3 id="el-cálculo-de-prefetch-con-números">El cálculo de prefetch, con números&lt;/h3>
&lt;p>Pongamos los números de la analogía. Supón un SSD de consumo a &lt;strong>5 GB/s&lt;/strong> de lectura secuencial y un experto cuantizado de tamaño &lt;code>X&lt;/code> MB. El tiempo de cargar un experto es&lt;/p>
&lt;p>$$t_{\text{1 experto}} = \frac{X \text{ MB}}{5000 \text{ MB/s}} = \frac{X}{5000}\ \text{s} = \frac{X}{5}\ \text{ms}.$$&lt;/p>
&lt;p>Concretemos &lt;code>X&lt;/code>. En SmallThinker-4B-A0.6B con Q4_0 (~0.5 byte/param efectivo contando overhead de bloques), un experto pequeño de, digamos, 4M parámetros pesa &lt;code>\approx 4\text{M} \times 0.5 = 2&lt;/code> MB. Cargarlo cuesta &lt;code>t_{\text{1 experto}} = 2/5 = 0.4&lt;/code> ms.&lt;/p>
&lt;p>Ahora la pregunta de scheduling: si el bloque de atención de la capa toma &lt;code>Y&lt;/code> ms, &lt;strong>¿cuántos expertos puedo prefetchar mientras la atención corre?&lt;/strong> El número es&lt;/p>
&lt;p>$$n_{\text{prefetch}} = \left\lfloor \frac{Y}{t_{\text{1 experto}}} \right\rfloor = \left\lfloor \frac{Y \cdot 5}{X} \right\rfloor.$$&lt;/p>
&lt;p>Con &lt;code>Y = 2&lt;/code> ms de atención y &lt;code>X = 2&lt;/code> MB por experto: &lt;code>n_{\text{prefetch}} = \lfloor 2 \times 5 / 2 \rfloor = 5&lt;/code> expertos. Es decir, en la ventana de atención de esa capa el SSD alcanza a traer 5 expertos. Si el top-k de la capa activa ≤ 5 expertos, el prefetch los oculta todos y &lt;code>t_prefetch ≤ t_att&lt;/code>: latencia de carga cero. Si la capa necesitara 8 expertos, traerías 5 gratis y pagarías la carga de los 3 restantes como burbuja: &lt;code>(8-5) \times 0.4 = 1.2&lt;/code> ms de espera por capa. De ahí que el diseño quiera grano fino con top-k pequeño: para caber debajo de la ventana de atención.&lt;/p>
&lt;p>Dos observaciones críticas sobre este cálculo:&lt;/p>
&lt;ul>
&lt;li>Los 5 GB/s son &lt;strong>lectura secuencial idealizada&lt;/strong>. Los expertos están dispersos en disco; lecturas aleatorias 4K en un SSD de consumo van mucho más lentas. El ancho de banda efectivo puede ser una fracción del nominal, lo que reduce &lt;code>n_{\text{prefetch}}&lt;/code>. La metodología que reporte tok/s debería decir si mide con expertos pre-ordenados en disco o con acceso realista.&lt;/li>
&lt;li>La ventana &lt;code>Y&lt;/code> de atención &lt;strong>encoge con el contexto corto&lt;/strong> y al inicio de la generación. Con prompts cortos, la atención es barata y puede que &lt;em>no&lt;/em> cubra el prefetch; la ventaja del solapamiento crece con secuencias más largas. Otro detalle que un benchmark honesto debería desglosar.&lt;/li>
&lt;/ul>
&lt;h3 id="footprint-de-pesos-por-qué-reportan-1-gb-para-un-4b">Footprint de pesos: por qué reportan ~1 GB para un 4B&lt;/h3>
&lt;p>Hagamos la cuenta del 4B en Q4_0. Cuantización a 4 bits ≈ 0.5 byte/param, más un pequeño overhead de escalas por bloque (Q4_0 añade un FP16 de escala cada 32 pesos, ~0.56 byte/param efectivos). Entonces:&lt;/p>
&lt;p>$$4\text{B} \times 0.5\ \text{B/param} \approx 2\ \text{GB}.$$&lt;/p>
&lt;p>Es decir, &lt;strong>el modelo completo en Q4_0 ocupa ~2 GB en disco&lt;/strong>. Pero los autores reportan &lt;strong>~1 GB de RAM&lt;/strong>. ¿Contradicción? No, y entender por qué es entender el diseño:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>No todos los expertos están residentes.&lt;/strong> Solo los activados (&lt;code>A = 0.6B&lt;/code>) y una caché caben en RAM; el resto vive en SSD y se streamea. &lt;code>0.6\text{B} \times 0.5 \approx 0.3&lt;/code> GB de expertos activos, más la parte densa (atención, embeddings, router) y una caché de expertos calientes.&lt;/li>
&lt;li>&lt;strong>La sparse FFN reduce el trabajo y el residente útil.&lt;/strong> Las neuronas que salen a cero no necesitan estar materializadas para ese token.&lt;/li>
&lt;/ul>
&lt;p>Sumando expertos activos + parte densa + caché razonable, ~1 GB es plausible. Pero ojo con el matiz: ~1 GB es el &lt;strong>residente en RAM&lt;/strong>, no el footprint total en almacenamiento, que sigue siendo ~2 GB en SSD. Confundir ambos —reportar &amp;ldquo;1 GB&amp;rdquo; a secas— es engañoso si el lector entiende &amp;ldquo;el modelo ocupa 1 GB&amp;rdquo;. Ocupa 2 GB; &lt;em>mantiene&lt;/em> 1 GB en RAM. La distinción importa para un device con 2 GB de almacenamiento libre: ahí no entra.&lt;/p>
&lt;p>Análogamente, SmallThinker-21B-A3B: &lt;code>21\text{B} \times 0.5 \approx 10.5&lt;/code> GB en disco; &lt;code>3\text{B} \times 0.5 \approx 1.5&lt;/code> GB de expertos activos, y el ~8 GB de RAM reportado incluye expertos activos + caché generosa + parte densa. La caché grande es lo que sube de 1.5 a ~8 GB: mantienes muchos expertos calientes residentes para no golpear el SSD constantemente.&lt;/p>
&lt;h2 id="el-coste-de-calidad-el-escepticismo-necesario">El coste de calidad: el escepticismo necesario&lt;/h2>
&lt;p>Toda la maquinaria anterior reduce el cómputo por token a &lt;code>\approx A&lt;/code>. Pero &lt;code>A = 0.6B&lt;/code> activados es &lt;strong>muy&lt;/strong> poco. Aquí es donde hay que poner el freno al entusiasmo:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Capacidad de razonamiento acotada.&lt;/strong> Un modelo que activa 0.6B de parámetros por token tiene, por token, la potencia de cómputo de un modelo de 0.6B, no de 4B. La capacidad total &lt;code>N=4B&lt;/code> ayuda a &lt;em>almacenar&lt;/em> más conocimiento (más expertos especializados), pero el &lt;em>procesamiento&lt;/em> de cada token sigue limitado por &lt;code>A&lt;/code>. Para tareas que requieren composición y razonamiento multi-paso intensivo, esto es un techo real, no un detalle.&lt;/li>
&lt;li>&lt;strong>El router es un punto único de fallo de calidad.&lt;/strong> Si el router de grano fino elige mal los expertos —y con grano fino hay más decisiones que tomar—, la calidad cae sin que ninguna métrica de velocidad lo refleje. El pre-attention router agrava esto: predice los expertos &lt;em>antes&lt;/em> de ver la atención, con menos información que un router post-atención. Los autores deberían reportar cuánta calidad se pierde por predecir antes (mismatch entre experto prefetchado y experto que el router post-atención habría elegido).&lt;/li>
&lt;li>&lt;strong>Los ~20 tok/s necesitan letra pequeña.&lt;/strong> ¿En qué CPU exactamente? ¿Con qué longitud de contexto y de generación (la ventaja del solapamiento depende de &lt;code>Y&lt;/code>)? ¿Cold start incluido o steady state? ¿El SSD estaba con los expertos pre-ordenados secuencialmente? Un &amp;ldquo;supera 20 tok/s&amp;rdquo; sin esas condiciones es un número de marketing, no de metodología.&lt;/li>
&lt;li>&lt;strong>Comparación justa.&lt;/strong> La pregunta correcta no es &amp;ldquo;¿es rápido?&amp;rdquo;, sino &amp;ldquo;¿a igualdad de calidad en un benchmark independiente, es más rápido o más pequeño que un denso comprimido equivalente?&amp;rdquo;. Eso requiere evals que el lector pueda reproducir, no solo tok/s en la máquina de los autores.&lt;/li>
&lt;/ul>
&lt;p>Nada de esto invalida la dirección. Diseñar para device es, conceptualmente, el enfoque correcto: ataca los cuellos reales (cómputo, RAM, I/O) en la arquitectura en vez de paliar­los después. Pero &amp;ldquo;20 tok/s en ~1 GB&amp;rdquo; es una afirmación de &lt;em>eficiencia&lt;/em>, y la eficiencia solo significa algo anclada a un nivel de &lt;em>calidad&lt;/em> medido honestamente. Mientras esa ancla no esté clara, el número correcto de escepticismo es alto.&lt;/p>
&lt;h2 id="implicaciones-para-inferencia-on-premise-y-edge">Implicaciones para inferencia on-premise y edge&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>El SSD pasa a ser parte de la jerarquía de inferencia.&lt;/strong> En cloud, la jerarquía es HBM → RAM. En device, el SSD/flash entra como un nivel más, y su ancho de banda y latencia de acceso aleatorio se vuelven parámetros de rendimiento de primer orden. Esto conecta con &lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">Del disco a la HBM&lt;/a>: el cold start y el streaming de pesos dejan de ser solo un problema de arranque y pasan a ser parte del &lt;em>steady state&lt;/em>.&lt;/li>
&lt;li>&lt;strong>El edge box hetero­géneo gana sentido.&lt;/strong> En un patrón de &lt;a href="https://blog.lo0.es/posts/entornos-mixtos-nvidia-intel-servidores-nucs/">entornos mixtos&lt;/a>, un modelo nativo-device como SmallThinker corre en el NUC/edge con CPU y SSD, sirviendo localmente, mientras lo pesado se queda en el cluster central. El pre-attention router es lo que hace viable el edge box sin GPU.&lt;/li>
&lt;li>&lt;strong>El capacity planning cambia de ejes.&lt;/strong> Como discute &lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">Capacity planning de inferencia&lt;/a>, en device el recurso a planificar no es VRAM sino la terna RAM-residente / ancho-de-banda-SSD / FLOPs-de-CPU. Un modelo con &lt;code>A&lt;/code> pequeño y prefetch solapado mueve el cuello de botella de &amp;ldquo;¿cabe en RAM?&amp;rdquo; a &amp;ldquo;¿el SSD alimenta el prefetch a tiempo?&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;h2 id="conclusión">Conclusión&lt;/h2>
&lt;p>SmallThinker es, sobre todo, un cambio de pregunta. No &amp;ldquo;¿cómo encojo este modelo cloud para que quepa en el device?&amp;rdquo; sino &amp;ldquo;¿cómo sería el modelo si lo diseñara para el device desde el primer parámetro?&amp;rdquo;. La respuesta —MoE de grano fino para desacoplar &lt;code>N&lt;/code> de &lt;code>A&lt;/code>, sparse FFN para ahorrar dentro de &lt;code>A&lt;/code>, y un pre-attention router que oculta la I/O de almacenamiento bajo la atención— ataca las tres restricciones del device (cómputo, RAM, I/O) en la arquitectura, no en una fase de compresión posterior. La condición clave, &lt;code>t_att ≥ t_prefetch&lt;/code>, explica por qué las piezas encajan: el grano fino no solo ahorra FLOPs, hace que el prefetch quepa debajo de la atención. Los números reportados (~20 tok/s, ~1 GB / ~8 GB de RAM) son prometedores y la dirección es sólida; el coste de activar tan poco y la falta de detalle metodológico sobre calidad piden cautela. Diseñar para device es la apuesta correcta; medirlo honestamente es la asignatura pendiente.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/moe-inference-fundamentos/">MoE inference: el call center con 256 especialistas&lt;/a> — la base conceptual de este post: cómo un router enruta tokens a expertos y por qué &lt;code>N&lt;/code> y &lt;code>A&lt;/code> se desacoplan; léelo primero si MoE te suena lejano.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">Del disco a la HBM: cold start y carga de modelo&lt;/a> — el streaming de pesos desde almacenamiento lento, que aquí deja de ser problema de arranque y pasa a steady state vía prefetch.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/knowledge-distillation-fundamentos/">Knowledge distillation&lt;/a> — la palanca canónica del enfoque &amp;ldquo;comprimir un denso de cloud&amp;rdquo;, el contrapunto exacto del enfoque nativo-device.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/poda-pruning-llm-fundamentos/">Poda de modelos LLM&lt;/a> — la otra palanca de reducción; útil para comparar &amp;ldquo;quitar a un grande&amp;rdquo; frente a &amp;ldquo;diseñar pequeño desde cero&amp;rdquo;.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/entornos-mixtos-nvidia-intel-servidores-nucs/">Entornos mixtos NVIDIA + Intel&lt;/a> — dónde encaja un modelo nativo-device: el edge box con CPU y SSD que sirve localmente sin GPU.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">Capacity planning de inferencia LLM on-premise&lt;/a> — en device los ejes a planificar son RAM-residente, ancho de banda de SSD y FLOPs de CPU, no VRAM.&lt;/li>
&lt;li>&lt;strong>Roofline invertido en modelos pequeños&lt;/strong> (hermano de esta serie, próximamente) — el régimen de rendimiento del SLM que explica por qué &lt;code>A&lt;/code> pequeño mantiene el decode memory-bound y dónde está el techo real.&lt;/li>
&lt;li>&lt;strong>Self-speculative decoding con early-exit&lt;/strong> (hermano de esta serie, próximamente) — self-spec aplicado a MoE on-device: cómo acelerar el decode sin draft externo cuando el modelo ya es pequeño.&lt;/li>
&lt;li>&lt;strong>Cuantización agresiva sub-4-bit y ternaria&lt;/strong> (hermano de esta serie, próximamente) — Q4_0 y más allá en device: ternario y 2-bit para bajar aún más el footprint de expertos en SSD.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Equipo SmallThinker (SJTU IPADS + Zenergize AI). &lt;em>SmallThinker: A Family of Efficient Large Language Models Natively Trained for Local Deployment&lt;/em>. arXiv:2507.20984. &lt;a href="https://arxiv.org/abs/2507.20984">https://arxiv.org/abs/2507.20984&lt;/a>&lt;/li>
&lt;li>Repositorio oficial SmallThinker: &lt;a href="https://github.com/SJTU-IPADS/SmallThinker">https://github.com/SJTU-IPADS/SmallThinker&lt;/a>&lt;/li>
&lt;li>&lt;em>Self-Speculative Decoding for On-device MoE Acceleration&lt;/em>. ACM The Web Conference (WWW) 2026. doi:10.1145/3774904.3792218. &lt;a href="https://doi.org/10.1145/3774904.3792218">https://doi.org/10.1145/3774904.3792218&lt;/a>&lt;/li>
&lt;li>Liu, Z. et al. &lt;em>Deja Vu: Contextual Sparsity for Efficient LLMs at Inference Time&lt;/em>. ICML 2023. &lt;a href="https://arxiv.org/abs/2310.17157">https://arxiv.org/abs/2310.17157&lt;/a>&lt;/li>
&lt;li>Song, Y. et al. &lt;em>PowerInfer: Fast Large Language Model Serving with a Consumer-grade GPU&lt;/em> (sparse activation + hot/cold experts). SJTU IPADS, 2023. &lt;a href="https://arxiv.org/abs/2312.12456">https://arxiv.org/abs/2312.12456&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Self-speculative decoding: el modelo que se adelanta a sí mismo</title><link>https://blog.lo0.es/posts/self-speculative-decoding-early-exit/</link><pubDate>Tue, 09 Jun 2026 01:40:00 +0000</pubDate><guid>https://blog.lo0.es/posts/self-speculative-decoding-early-exit/</guid><description>&lt;blockquote>
&lt;p>Este post es el complemento directo de &lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">Speculative decoding: el secretario que adelanta&lt;/a>. Allí draft y target son &lt;strong>dos modelos distintos&lt;/strong>; aquí son &lt;strong>el mismo modelo a dos profundidades&lt;/strong>. Léelo primero: damos por sabidos el rejection sampling, el techo &lt;code>1/(1-α)&lt;/code> y la fórmula del speedup, y aquí solo cambiamos qué es el draft.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Speculative decoding clásico exige una pareja: un modelo &lt;em>draft&lt;/em> barato propone γ tokens y un &lt;em>target&lt;/em> caro los verifica en un único forward pass paralelo. En modelos grandes el draft puede ser un 1 % del target y caber holgado. En &lt;strong>modelos pequeños&lt;/strong> (SLM, 1B–8B) esa receta se rompe por dos lados: un draft que sea 1/10 de un 3B es un 0.3B que apenas acierta (α se desploma), y cargar un segundo modelo —por pequeño que sea— &lt;strong>dobla las piezas a mantener y se come VRAM que en una 4090 o en device no sobra&lt;/strong>. &lt;em>Self-speculative decoding&lt;/em> resuelve ambos: el draft es el &lt;strong>propio modelo ejecutado de forma superficial&lt;/strong>. Un modelo de &lt;code>L&lt;/code> capas produce tokens borrador saliendo en una capa intermedia &lt;code>k &amp;lt; L&lt;/code> (&lt;em>early-exit&lt;/em>) o saltando un subconjunto de capas (&lt;em>layer-skip&lt;/em>), y luego verifica esos tokens con el forward completo de las &lt;code>L&lt;/code> capas. Como draft y verify &lt;strong>comparten pesos y comparten el KV cache de las capas comunes&lt;/strong>, el coste extra de memoria es &lt;strong>cero&lt;/strong>: no hay un segundo modelo, no hay un segundo KV cache, no hay nada nuevo que cargar. El precio es que el draft early-exit es &lt;strong>más caro&lt;/strong> que un draft externo minúsculo (recorre &lt;code>k/L&lt;/code> del modelo en vez de un 1 %), así que el coste relativo &lt;code>c&lt;/code> sube. El trade-off honesto: con draft dedicado bien entrenado (EAGLE-3) que &lt;strong>quepa&lt;/strong> en memoria, su α suele ser mayor y gana; self-spec gana cuando no hay draft entrenado, no cabe, o estás en device.&lt;/p>
&lt;h2 id="la-analogía-el-ajedrecista-que-juega-a-ojo-y-luego-calcula">La analogía: el ajedrecista que juega a ojo y luego calcula&lt;/h2>
&lt;p>Un buen jugador de ajedrez hace dos cosas con el mismo cerebro. Primero mira el tablero y, &lt;strong>a ojo&lt;/strong>, en medio segundo, propone una jugada &amp;ldquo;que pinta bien&amp;rdquo;: es intuición de patrones, reconocimiento rápido, las capas superficiales del juicio. Después, antes de mover, &lt;strong>calcula a fondo&lt;/strong>: tres jugadas por delante, las respuestas del rival, las líneas tácticas. Ese cálculo profundo confirma la intuición o la corrige.&lt;/p>
&lt;p>Lo decisivo es que &lt;strong>es la misma persona&lt;/strong> haciendo de borrador y de revisor. No contrata a un segundo ajedrecista más débil para que adivine la jugada y luego él la valida —eso sería el speculative clásico con draft externo—. Aquí el borrador rápido y la verificación lenta salen del mismo cerebro, recorrido a dos profundidades.&lt;/p>
&lt;p>La analogía se sostiene punto por punto:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>El vistazo a ojo es el forward early-exit&lt;/strong>: el modelo recorre solo las primeras &lt;code>k&lt;/code> capas y emite un token borrador. Rápido, aproximado.&lt;/li>
&lt;li>&lt;strong>El cálculo a fondo es el forward completo de las &lt;code>L&lt;/code> capas&lt;/strong>, que verifica el borrador con rejection sampling exacto.&lt;/li>
&lt;li>&lt;strong>Que sea la misma persona es el reuso de pesos y de KV cache&lt;/strong>: las &lt;code>k&lt;/code> capas superficiales del draft son &lt;strong>literalmente las mismas&lt;/strong> que las &lt;code>k&lt;/code> primeras capas del verify; lo ya computado no se recomputa.&lt;/li>
&lt;li>&lt;strong>Que la jugada final sea idéntica a la que el jugador habría elegido calculando siempre a fondo&lt;/strong> es la garantía de rejection sampling: la calidad del output no se degrada (la prueba está en el &lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">post de speculative&lt;/a>).&lt;/li>
&lt;/ul>
&lt;h2 id="por-qué-el-draft-externo-no-encaja-en-modelos-pequeños">Por qué el draft externo no encaja en modelos pequeños&lt;/h2>
&lt;p>Repasemos el coste del speculative clásico con dos números. El speedup depende de la tasa de aceptación α (cuánto acierta el draft) y del coste relativo &lt;code>c = T_draft / T_target&lt;/code>. Un draft útil necesita &lt;strong>α alto y c bajo a la vez&lt;/strong>. En modelos grandes eso es alcanzable: un draft de 1B para un target de 70B tiene &lt;code>c ≈ 0.015&lt;/code> y, si está bien destilado (EAGLE), α &amp;gt; 0.8. El producto sale rentable.&lt;/p>
&lt;p>En un modelo pequeño el equilibrio se rompe:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>El draft proporcional es inservible.&lt;/strong> Si quieres &lt;code>c ≈ 0.1&lt;/code> para un target de 3B, tu draft es un ~0.3B. Un 0.3B genérico tiene una distribución tan distinta del 3B que α cae a la zona 0.3–0.5. Y &lt;code>1/(1-α)&lt;/code> con α = 0.4 es un techo de 1.67 tokens/step: ni con γ infinito sacas más. El premio se evapora.&lt;/li>
&lt;li>&lt;strong>Cargar un segundo modelo dobla las piezas.&lt;/strong> Aunque el draft sea pequeño en VRAM, es &lt;strong>otro checkpoint que versionar, cuantizar, validar y servir&lt;/strong>, y tiene &lt;strong>su propio KV cache&lt;/strong>. En una RTX 4090 (24 GB, Ada Lovelace) con un 8B cuantizado y un contexto largo, el KV cache ya aprieta; meter un segundo modelo y su cache puede forzarte a bajar la concurrencia o el contexto máximo. En &lt;strong>device&lt;/strong> (un móvil, un NUC, un edge box) directamente no hay sitio.&lt;/li>
&lt;li>&lt;strong>No siempre existe un draft entrenado&lt;/strong> para tu modelo exótico o fine-tuneado. EAGLE necesita entrenar el draft on-policy contra ese target concreto (ver &lt;a href="https://blog.lo0.es/posts/knowledge-distillation-fundamentos/">knowledge distillation&lt;/a>). Si tu SLM es un fine-tune propio, no hay draft oficial publicado.&lt;/li>
&lt;/ol>
&lt;p>Self-speculative ataca los tres a la vez con una idea: &lt;strong>no traigas un segundo modelo; usa el primero a media profundidad.&lt;/strong>&lt;/p>
&lt;h2 id="el-mecanismo-early-exit-como-draft-forward-completo-como-verify">El mecanismo: early-exit como draft, forward completo como verify&lt;/h2>
&lt;p>Un transformer de &lt;code>L&lt;/code> capas, en cada posición, transforma el hidden state capa a capa: &lt;code>h_0 → h_1 → ... → h_L&lt;/code>, y la &lt;em>LM head&lt;/em> proyecta &lt;code>h_L&lt;/code> a logits. La observación que lo habilita todo: &lt;strong>&lt;code>h_k&lt;/code> para &lt;code>k &amp;lt; L&lt;/code> ya es un hidden state razonable&lt;/strong>. Si lo pasas por la misma LM head (o por una head ligera dedicada), obtienes una distribución de salida &amp;ldquo;prematura&amp;rdquo; pero a menudo correcta para los tokens fáciles. Esa es la fuente del borrador.&lt;/p>
&lt;p>La iteración de self-speculative tiene la misma estructura que el speculative clásico —draft, verify, accept/reject— pero ambos roles son el mismo modelo:&lt;/p>
&lt;p>&lt;strong>Paso 1 — Draft superficial.&lt;/strong> Para producir γ tokens borrador, el modelo recorre solo las primeras &lt;code>k&lt;/code> capas (o un subconjunto de capas en el caso layer-skip) y aplica la LM head. Cada token borrador cuesta ≈ &lt;code>k/L&lt;/code> de un forward completo. Llamamos &lt;code>c = k/L&lt;/code> al coste relativo del draft. Los γ borradores se generan autoregresivamente a este coste reducido.&lt;/p>
&lt;p>&lt;strong>Paso 2 — Verify completo.&lt;/strong> El modelo ejecuta &lt;strong>un único forward pass de las &lt;code>L&lt;/code> capas&lt;/strong> sobre &lt;code>prompt + x_1...x_γ&lt;/code>. Por la atención causal obtiene &lt;code>p(·|prompt, x_&amp;lt;i)&lt;/code> para cada posición, exactamente igual que en el speculative clásico.&lt;/p>
&lt;p>&lt;strong>Paso 3 — Accept/reject.&lt;/strong> Rejection sampling idéntico al del post anterior: se aceptan tokens de izquierda a derecha, se corrige en la primera divergencia muestreando del residual &lt;code>norm(max(0, p−q))&lt;/code>, y si se aceptan los γ se añade el token bonus. La calidad del output es &lt;strong>exactamente&lt;/strong> la del modelo completo.&lt;/p>
&lt;h3 id="el-truco-que-hace-c-aún-más-barato-reuso-de-kv-cache-de-capas-compartidas">El truco que hace &lt;code>c&lt;/code> aún más barato: reuso de KV cache de capas compartidas&lt;/h3>
&lt;p>Aquí está la diferencia clave frente a un draft externo. Cuando el modelo hace el draft recorriendo las capas &lt;code>0..k&lt;/code>, calcula y &lt;strong>almacena el KV cache de esas &lt;code>k&lt;/code> capas&lt;/strong> para los tokens del prompt y los borradores. Cuando llega el verify completo, las capas &lt;code>0..k&lt;/code> del forward de &lt;code>L&lt;/code> capas son &lt;strong>bit a bit las mismas operaciones sobre los mismos pesos&lt;/strong> que ya hizo el draft. No hay que recomputarlas: el verify &lt;strong>reusa directamente el KV cache&lt;/strong> que el draft dejó para las capas &lt;code>0..k&lt;/code>, y solo computa de verdad las capas &lt;code>k..L&lt;/code> que faltan.&lt;/p>
&lt;p>Eso tiene dos consecuencias:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Memoria extra cero.&lt;/strong> No hay un segundo KV cache. El KV de las capas comunes es uno solo, compartido entre draft y verify. Contrasta con vanilla SD, donde el draft tiene su propio cache completo (ver &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a>).&lt;/li>
&lt;li>&lt;strong>Cómputo parcialmente reusado.&lt;/strong> El verify solo paga las capas &lt;code>k..L&lt;/code> &amp;ldquo;nuevas&amp;rdquo; para los tokens que ya pasaron por el draft. El forward completo no es tan caro como sugiere &lt;code>L&lt;/code>, porque las primeras &lt;code>k&lt;/code> capas vienen del cache.&lt;/li>
&lt;/ul>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 400" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Early-exit draft y forward completo verify con KV compartido">
&lt;text x="390" y="22" text-anchor="middle" fill="currentColor" font-size="14" font-weight="700">Un solo modelo de L=32 capas, recorrido a dos profundidades&lt;/text>
&lt;!-- DRAFT column -->
&lt;p>&lt;text x="180" y="50" text-anchor="middle" fill="currentColor" font-size="12" font-weight="700">DRAFT · early-exit en k=8&lt;/text>
&lt;rect x="120" y="60" width="120" height="120" fill="#fff4d6" stroke="#a48000" stroke-width="1.4" rx="6"/>
&lt;text x="180" y="95" text-anchor="middle" fill="#a48000" font-size="12" font-weight="600">capas 0..8&lt;/text>
&lt;text x="180" y="115" text-anchor="middle" fill="#a48000" font-size="11">recorrido superficial&lt;/text>
&lt;text x="180" y="133" text-anchor="middle" fill="#a48000" font-size="11">coste ≈ k/L = 0.25&lt;/text>
&lt;rect x="120" y="190" width="120" height="30" fill="#fff4d6" stroke="#a48000" stroke-width="1.4" rx="6"/>
&lt;text x="180" y="210" text-anchor="middle" fill="#a48000" font-size="11" font-weight="600">LM head → borrador&lt;/text>
&lt;text x="180" y="245" text-anchor="middle" fill="currentColor" font-size="11">x₁ x₂ x₃ x₄ (γ=4)&lt;/text>&lt;/p>
&lt;!-- VERIFY column -->
&lt;p>&lt;text x="600" y="50" text-anchor="middle" fill="currentColor" font-size="12" font-weight="700">VERIFY · forward completo L=32&lt;/text>
&lt;rect x="540" y="60" width="120" height="120" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4" rx="6"/>
&lt;text x="600" y="92" text-anchor="middle" fill="#1f5fa8" font-size="12" font-weight="600">capas 0..8&lt;/text>
&lt;text x="600" y="110" text-anchor="middle" fill="#1f5fa8" font-size="11">(reusadas, no&lt;/text>
&lt;text x="600" y="125" text-anchor="middle" fill="#1f5fa8" font-size="11">se recomputan)&lt;/text>
&lt;line x1="540" y1="135" x2="660" y2="135" stroke="#1f5fa8" stroke-width="1" stroke-dasharray="4 2"/>
&lt;text x="600" y="158" text-anchor="middle" fill="#1f5fa8" font-size="12" font-weight="600">capas 8..32&lt;/text>
&lt;text x="600" y="174" text-anchor="middle" fill="#1f5fa8" font-size="11">cómputo nuevo&lt;/text>
&lt;rect x="540" y="190" width="120" height="30" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4" rx="6"/>
&lt;text x="600" y="210" text-anchor="middle" fill="#1f5fa8" font-size="11" font-weight="600">LM head → p(·)&lt;/text>&lt;/p>
&lt;!-- shared KV cache box -->
&lt;rect x="300" y="80" width="180" height="80" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.6" rx="8"/>
&lt;text x="390" y="110" text-anchor="middle" fill="#2a7a40" font-size="12" font-weight="700">KV cache COMPARTIDO&lt;/text>
&lt;text x="390" y="128" text-anchor="middle" fill="#2a7a40" font-size="11">capas 0..8 · un solo cache&lt;/text>
&lt;text x="390" y="145" text-anchor="middle" fill="#2a7a40" font-size="11">memoria extra = 0&lt;/text>
&lt;!-- arrows draft->KV and KV->verify -->
&lt;p>&lt;defs>&lt;marker id="ssd1" 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="#2a7a40"/>&lt;/marker>&lt;/defs>
&lt;path d="M240,120 L300,120" fill="none" stroke="#2a7a40" stroke-width="1.6" marker-end="url(#ssd1)"/>
&lt;text x="270" y="112" text-anchor="middle" fill="#2a7a40" font-size="10">escribe KV 0..8&lt;/text>
&lt;path d="M480,120 L540,120" fill="none" stroke="#2a7a40" stroke-width="1.6" marker-end="url(#ssd1)"/>
&lt;text x="510" y="112" text-anchor="middle" fill="#2a7a40" font-size="10">lee KV 0..8&lt;/text>&lt;/p>
&lt;!-- accept/reject row -->
&lt;p>&lt;text x="390" y="280" text-anchor="middle" fill="currentColor" font-size="12" font-weight="700">Rejection sampling (idéntico al speculative clásico)&lt;/text>
&lt;rect x="180" y="295" width="80" height="34" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.4" rx="6"/>&lt;text x="220" y="316" text-anchor="middle" fill="#2a7a40" font-size="12" font-weight="600">x₁ ✓&lt;/text>
&lt;rect x="270" y="295" width="80" height="34" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.4" rx="6"/>&lt;text x="310" y="316" text-anchor="middle" fill="#2a7a40" font-size="12" font-weight="600">x₂ ✓&lt;/text>
&lt;rect x="360" y="295" width="80" height="34" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.4" rx="6"/>&lt;text x="400" y="316" text-anchor="middle" fill="#2a7a40" font-size="12" font-weight="600">x₃ ✓&lt;/text>
&lt;rect x="450" y="295" width="80" height="34" fill="#f6caca" stroke="#a52a2a" stroke-width="1.4" rx="6"/>&lt;text x="490" y="316" text-anchor="middle" fill="#a52a2a" font-size="12" font-weight="600">x₄ ✗&lt;/text>
&lt;text x="390" y="360" text-anchor="middle" fill="currentColor" font-size="11">Output = exactamente el del modelo completo · 0 modelos extra · 0 KV extra&lt;/text>
&lt;text x="390" y="385" text-anchor="middle" fill="currentColor" font-size="11">El draft y el verify son el mismo modelo; las capas 0..8 se computan una sola vez.&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;h2 id="las-familias-estado-2026">Las familias (estado 2026)&lt;/h2>
&lt;p>No hay una sola forma de hacer self-speculative. Difieren en &lt;strong>qué capas se saltan&lt;/strong> y en &lt;strong>si hace falta entrenar&lt;/strong>.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Familia&lt;/th>
&lt;th>Año / venue&lt;/th>
&lt;th>Cómo elige qué saltar&lt;/th>
&lt;th>¿Entrenamiento?&lt;/th>
&lt;th>KV extra&lt;/th>
&lt;th>Idea distintiva&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>LayerSkip&lt;/strong> (Elhoushi et al.)&lt;/td>
&lt;td>2024, arXiv:2404.16710&lt;/td>
&lt;td>Early-exit en capa fija &lt;code>k&lt;/code>; una sola LM head sirve a todas las salidas&lt;/td>
&lt;td>Sí — &lt;em>layer dropout&lt;/em> + &lt;em>early-exit loss&lt;/em> en train/fine-tune&lt;/td>
&lt;td>0&lt;/td>
&lt;td>Un único modelo entrenado para hacer draft y verify; reusa cómputo parcial&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SWIFT&lt;/strong>&lt;/td>
&lt;td>ICLR 2025 (OpenReview EKJhH5D5wA)&lt;/td>
&lt;td>Selecciona qué capas saltar &lt;strong>on-the-fly&lt;/strong>, sin tocar pesos&lt;/td>
&lt;td>&lt;strong>No&lt;/strong> — plug-and-play sobre el modelo dado&lt;/td>
&lt;td>0&lt;/td>
&lt;td>Self-spec &lt;em>training-free&lt;/em>: optimiza el conjunto de capas saltadas en caliente&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CLaSp&lt;/strong>&lt;/td>
&lt;td>2025, arXiv:2505.24196&lt;/td>
&lt;td>&lt;em>In-context&lt;/em> layer skip dinámico: el patrón de capas saltadas se adapta al contexto&lt;/td>
&lt;td>No (dinámico en inferencia)&lt;/td>
&lt;td>0&lt;/td>
&lt;td>El skip no es fijo; cambia según lo que se está generando&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ConfLayers&lt;/strong>&lt;/td>
&lt;td>2026, arXiv:2604.14612&lt;/td>
&lt;td>Salta capas según &lt;strong>confianza&lt;/strong> del estado intermedio (adaptativo por token)&lt;/td>
&lt;td>No (criterio de confianza)&lt;/td>
&lt;td>0&lt;/td>
&lt;td>Profundidad variable: tokens fáciles salen antes, difíciles llegan más hondo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Saguaro&lt;/strong>&lt;/td>
&lt;td>2025–26&lt;/td>
&lt;td>Formulación &lt;strong>asíncrona&lt;/strong>: el draft sigue especulando en paralelo mientras corre la verificación&lt;/td>
&lt;td>Depende de la variante&lt;/td>
&lt;td>0&lt;/td>
&lt;td>Solapa draft y verify en el tiempo en lugar de alternarlos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SSD para MoE on-device&lt;/strong>&lt;/td>
&lt;td>ACM Web Conf. 2026, doi 10.1145/3774904.3792218&lt;/td>
&lt;td>Self-spec aprovechando la &lt;em>sparsity&lt;/em> del MoE (pocos expertos activos por token)&lt;/td>
&lt;td>Variante específica MoE&lt;/td>
&lt;td>0&lt;/td>
&lt;td>El draft superficial activa aún menos expertos; encaja con MoE en device&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Tres lecturas operacionales de la tabla:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>El eje que más importa es entrenamiento sí/no.&lt;/strong> LayerSkip da el mejor α porque el modelo aprende a ser un buen draft superficial (con early-exit loss las capas intermedias se entrenan explícitamente para predecir bien). Pero exige fine-tune. SWIFT, CLaSp y ConfLayers son &lt;strong>training-free&lt;/strong>: peor α, pero se aplican a cualquier modelo ya entrenado sin tocar nada. Para un SLM que no controlas, training-free es lo realista.&lt;/li>
&lt;li>&lt;strong>El skip adaptativo (CLaSp, ConfLayers) sube α&lt;/strong> porque ajusta la profundidad del draft al token: gasta poco en lo fácil y más en lo difícil, en vez de un &lt;code>k&lt;/code> fijo. A cambio, el &lt;code>c&lt;/code> efectivo deja de ser constante.&lt;/li>
&lt;li>&lt;strong>Saguaro ataca otra cosa:&lt;/strong> no sube α, solapa el tiempo de draft y verify. Es ortogonal al resto y combinable.&lt;/li>
&lt;/ol>
&lt;h2 id="la-matemática-mismo-marco-distinto-c">La matemática: mismo marco, distinto &lt;code>c&lt;/code>&lt;/h2>
&lt;p>Reutilizamos el aparato del &lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">post de speculative&lt;/a> sin cambiar una letra. Con α la tasa de aceptación y γ el número de borradores:&lt;/p>
&lt;p>$$E[\text{tokens por step}] = \frac{1 - \alpha^{\gamma+1}}{1 - \alpha}, \qquad \text{Speedup} = \frac{1 - \alpha^{\gamma+1}}{(1 - \alpha)(\gamma c + 1)}$$&lt;/p>
&lt;p>Y el techo algorítmico es el mismo: &lt;code>lim_{γ→∞} = 1/(1-α)&lt;/code>. Lo único que cambia en self-speculative es &lt;strong>el valor de &lt;code>c&lt;/code>&lt;/strong>: ya no es el ratio de tamaños de dos modelos, sino &lt;code>c = k/L&lt;/code>, la fracción de capas que recorre el draft early-exit.&lt;/p>
&lt;h3 id="ejemplo-numérico-self-spec-con-l32-salida-en-k8">Ejemplo numérico: self-spec con L=32, salida en k=8&lt;/h3>
&lt;p>Tomemos un SLM de &lt;code>L = 32&lt;/code> capas que sale en &lt;code>k = 8&lt;/code> para el draft: &lt;code>c = k/L = 8/32 = 0.25&lt;/code>. Supongamos α = 0.7 (razonable para early-exit en tokens conversacionales) y γ = 4.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tokens esperados por step:&lt;/strong> &lt;code>(1 − 0.7⁵) / (1 − 0.7) = (1 − 0.168) / 0.3 = 0.832 / 0.3 = 2.77&lt;/code>&lt;/li>
&lt;li>&lt;strong>Speedup:&lt;/strong> &lt;code>2.77 / (4 × 0.25 + 1) = 2.77 / 2.0 = 1.39×&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>El factor del denominador es &lt;code>γc + 1 = 4·0.25 + 1 = 2.0&lt;/code>: el draft early-exit, al costar un cuarto del modelo cada token, se come parte del beneficio. Salir más arriba ayuda: con &lt;code>k = 4&lt;/code> (&lt;code>c = 0.125&lt;/code>), denominador &lt;code>= 1.5&lt;/code> y speedup &lt;code>= 2.77/1.5 = 1.85×&lt;/code> — pero salir más arriba normalmente baja α, así que hay tensión real entre &lt;code>k&lt;/code> pequeño (barato) y α alto (acierta).&lt;/p>
&lt;h3 id="comparación-honesta-con-un-draft-externo">Comparación honesta con un draft externo&lt;/h3>
&lt;p>Pongamos al lado un draft externo minúsculo bien destilado: &lt;code>c = 0.1&lt;/code> y α = 0.78 (lo que un EAGLE-style draft puede dar), mismo γ = 4.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tokens/step:&lt;/strong> &lt;code>(1 − 0.78⁵)/(1 − 0.78) = (1 − 0.289)/0.22 = 0.711/0.22 = 3.23&lt;/code>&lt;/li>
&lt;li>&lt;strong>Speedup:&lt;/strong> &lt;code>3.23 / (4 × 0.1 + 1) = 3.23 / 1.4 = 2.31×&lt;/code>&lt;/li>
&lt;/ul>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Configuración&lt;/th>
&lt;th>c&lt;/th>
&lt;th>α&lt;/th>
&lt;th>tokens/step&lt;/th>
&lt;th>speedup&lt;/th>
&lt;th>VRAM extra&lt;/th>
&lt;th>piezas a mantener&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Self-spec early-exit (k=8)&lt;/td>
&lt;td>0.25&lt;/td>
&lt;td>0.70&lt;/td>
&lt;td>2.77&lt;/td>
&lt;td>&lt;strong>1.39×&lt;/strong>&lt;/td>
&lt;td>&lt;strong>0&lt;/strong>&lt;/td>
&lt;td>&lt;strong>0&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Self-spec early-exit (k=4)&lt;/td>
&lt;td>0.125&lt;/td>
&lt;td>0.65&lt;/td>
&lt;td>2.50&lt;/td>
&lt;td>&lt;strong>1.67×&lt;/strong>&lt;/td>
&lt;td>&lt;strong>0&lt;/strong>&lt;/td>
&lt;td>&lt;strong>0&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Draft externo destilado&lt;/td>
&lt;td>0.10&lt;/td>
&lt;td>0.78&lt;/td>
&lt;td>3.23&lt;/td>
&lt;td>&lt;strong>2.31×&lt;/strong>&lt;/td>
&lt;td>sí (+modelo +KV)&lt;/td>
&lt;td>1 modelo extra&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La lectura es exactamente la que cabe esperar y conviene &lt;strong>no maquillar&lt;/strong>: si tienes un draft dedicado, entrenado contra tu target, y &lt;strong>cabe en memoria&lt;/strong>, su α mayor y su &lt;code>c&lt;/code> menor le dan más speedup. EAGLE-3 con draft bien entrenado suele ganar en speedup bruto. &lt;strong>Self-spec no compite en speedup bruto; compite en coste total.&lt;/strong> Sus columnas ganadoras son las dos de la derecha: cero VRAM extra y cero piezas que mantener. Self-spec gana cuando:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>no hay draft entrenado&lt;/strong> para tu modelo (SLM propio, fine-tune raro),&lt;/li>
&lt;li>&lt;strong>el draft no cabe&lt;/strong> (4090 ya llena, contexto largo que necesita el KV),&lt;/li>
&lt;li>&lt;strong>estás en device&lt;/strong> (móvil, NUC, edge), donde un segundo modelo y su KV simplemente no entran.&lt;/li>
&lt;/ul>
&lt;p>Es el mismo patrón que con MTP en el post anterior: a veces el mejor draft es &lt;strong>el que no tienes que cargar&lt;/strong>.&lt;/p>
&lt;h2 id="por-qué-encaja-justo-con-modelos-pequeños-y-device">Por qué encaja justo con modelos pequeños y device&lt;/h2>
&lt;p>El régimen donde self-spec brilla es el de baja concurrencia, memory-bandwidth-bound, con presupuesto de memoria escaso — exactamente el de un SLM en una sola GPU o en device (el porqué del régimen está en &lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">el roofline invertido&lt;/a>). Tres razones:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Cero memoria extra es decisivo donde no sobra.&lt;/strong> En una RTX 4090 (24 GB, Ada Lovelace) sirviendo un 7B–8B cuantizado con contexto largo, cada GB cuenta. Self-spec no pide ni uno: reusa pesos y KV. Un draft externo, aunque pequeño, te obliga a recortar contexto o concurrencia. En device la diferencia es binaria: con self-spec aceleras; con draft externo no hay sitio y punto.&lt;/li>
&lt;li>&lt;strong>No hay segundo checkpoint que versionar.&lt;/strong> Operacionalmente, un SLM en edge desplegado en cientos de cajas se vuelve insostenible si cada una necesita dos modelos sincronizados. Un solo binario que hace draft y verify es muchísimo más simple de mantener.&lt;/li>
&lt;li>&lt;strong>Encaja con MoE en device.&lt;/strong> En un MoE de grano fino para device (ver &lt;a href="https://blog.lo0.es/posts/arquitecturas-nativas-device-moe-grano-fino/">arquitecturas nativas para device&lt;/a>), el draft superficial activa aún menos expertos, y el régimen memory-bound persiste incluso a batch medio — justo lo que el trabajo de SSD para MoE on-device (ACM WWW 2026) explota.&lt;/li>
&lt;/ol>
&lt;p>El contrapunto, repetido para que no se olvide: en un &lt;strong>cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo)&lt;/strong>, donde la memoria no es el cuello de botella, un draft EAGLE-3 dedicado &lt;strong>sí cabe&lt;/strong> y su α mayor le da más speedup. Allí self-spec es plan B: lo usas si el modelo es exótico y no hay draft entrenado, no porque la memoria apriete.&lt;/p>
&lt;h2 id="pitfalls">Pitfalls&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>El α depende muchísimo de &lt;code>k&lt;/code>.&lt;/strong> Salir demasiado arriba (&lt;code>k&lt;/code> pequeño) abarata el draft pero hunde α; salir demasiado abajo (&lt;code>k&lt;/code> cercano a &lt;code>L&lt;/code>) sube α pero el draft cuesta casi un forward completo y &lt;code>c → 1&lt;/code>, matando el speedup. El óptimo es empírico y específico del modelo. Desconfía de cualquier número de speedup que no diga en qué &lt;code>k&lt;/code> se midió.&lt;/li>
&lt;li>&lt;strong>Training-free no es gratis en calidad de draft.&lt;/strong> SWIFT/CLaSp dan α menores que LayerSkip precisamente porque las capas intermedias del modelo no se entrenaron para ser buenas salidas prematuras. El número que importa es α medido en &lt;em>tu&lt;/em> distribución, no el del paper.&lt;/li>
&lt;li>&lt;strong>Sampling temperature y outputs creativos&lt;/strong> bajan α igual que en el speculative clásico. A T alta, el speedup de self-spec se erosiona más rápido todavía porque parte de un α más bajo.&lt;/li>
&lt;li>&lt;strong>Batch grande lo neutraliza igual que al speculative clásico.&lt;/strong> En cuanto el decode pasa a compute-bound, los borradores dejan de ser &amp;ldquo;casi gratis&amp;rdquo;. Self-spec es para baja concurrencia.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">Speculative decoding: el secretario que adelanta&lt;/a> — el complemento directo y prerequisito: draft + verify + rejection sampling, el techo &lt;code>1/(1-α)&lt;/code> y la fórmula del speedup que aquí reutilizamos tal cual.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/">El roofline invertido en modelos pequeños&lt;/a> — por qué el SLM vive en régimen memory-bound, que es justo lo que habilita cualquier forma de speculative.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/arquitecturas-nativas-device-moe-grano-fino/">Arquitecturas nativas para device: MoE de grano fino&lt;/a> — dónde aterriza el self-spec sobre MoE en device, aprovechando la sparsity del router.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo&lt;/a> — el reuso del KV de las capas compartidas entre draft y verify es lo que hace que la memoria extra sea cero; aquí está el mecanismo del cache.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/knowledge-distillation-fundamentos/">Knowledge distillation&lt;/a> — el early-exit loss de LayerSkip es pariente de la destilación: enseña a las capas intermedias a predecir como el modelo completo.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/poda-pruning-llm-fundamentos/">Poda de modelos LLM&lt;/a> — saltar capas es una forma de poda estructurada &lt;em>en inferencia&lt;/em>; layer-skip y layer-dropping comparten raíz conceptual.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/moe-inference-fundamentos/">MoE inference: el call center con 256 especialistas&lt;/a> — el régimen memory-bound persistente del MoE hace que el self-spec sobre MoE gane incluso a batch medio.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/decode-optimizaciones-vllm/">Optimizando el decode en vLLM&lt;/a> — dónde se configuran en la práctica los métodos speculative en producción.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/entornos-mixtos-nvidia-intel-servidores-nucs/">Entornos mixtos NVIDIA + Intel&lt;/a> — el caso device/edge donde &amp;ldquo;cero modelo extra&amp;rdquo; deja de ser una comodidad y pasa a ser la única opción viable.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Elhoushi, M., et al. &lt;em>LayerSkip: Enabling Early Exit Inference and Self-Speculative Decoding&lt;/em>. 2024. &lt;a href="https://arxiv.org/abs/2404.16710">https://arxiv.org/abs/2404.16710&lt;/a>&lt;/li>
&lt;li>&lt;em>SWIFT: On-the-Fly Self-Speculative Decoding for LLM Inference Acceleration&lt;/em>. ICLR 2025. &lt;a href="https://openreview.net/forum?id=EKJhH5D5wA">https://openreview.net/forum?id=EKJhH5D5wA&lt;/a>&lt;/li>
&lt;li>&lt;em>CLaSp: In-Context Layer Skip for Self-Speculative Decoding&lt;/em>. 2025. &lt;a href="https://arxiv.org/abs/2505.24196">https://arxiv.org/abs/2505.24196&lt;/a>&lt;/li>
&lt;li>&lt;em>ConfLayers: Confidence-Adaptive Layer Skipping for Self-Speculative Decoding&lt;/em>. 2026. &lt;a href="https://arxiv.org/abs/2604.14612">https://arxiv.org/abs/2604.14612&lt;/a>&lt;/li>
&lt;li>&lt;em>Self-Speculative Decoding for MoE on Device&lt;/em>. ACM Web Conference 2026. &lt;a href="https://doi.org/10.1145/3774904.3792218">https://doi.org/10.1145/3774904.3792218&lt;/a>&lt;/li>
&lt;li>Hugging Face blog. &lt;em>Faster Text Generation with Self-Speculative Decoding&lt;/em>. &lt;a href="https://huggingface.co/blog/layerskip">https://huggingface.co/blog/layerskip&lt;/a>&lt;/li>
&lt;li>Leviathan, Y., Kalman, M., Matias, Y. &lt;em>Fast Inference from Transformers via Speculative Decoding&lt;/em>. ICML 2023. &lt;a href="https://arxiv.org/abs/2211.17192">https://arxiv.org/abs/2211.17192&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>El roofline se invierte: por qué optimizar modelos pequeños es otro partido de rendimiento</title><link>https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/</link><pubDate>Tue, 09 Jun 2026 01:30:00 +0000</pubDate><guid>https://blog.lo0.es/posts/roofline-invertido-modelos-pequenos/</guid><description>&lt;blockquote>
&lt;p>Este post es el ancla de una mini-serie sobre rendimiento de inferencia en &lt;strong>modelos pequeños (SLM)&lt;/strong>. Casi todos los posts de optimización del blog —&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a>, &lt;a href="https://blog.lo0.es/posts/decode-optimizaciones-vllm/">decode&lt;/a>, &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">quantization&lt;/a>— se escribieron con un 70B en la cabeza. Aquí defiendo que cuando el modelo encoge un orden de magnitud, el &lt;em>roofline&lt;/em> cambia de régimen y varias de esas intuiciones se invierten. No es un matiz: es otro partido.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El decode autoregresivo de un LLM grande está &lt;strong>memory-bandwidth-bound&lt;/strong>: en cada step hay que mover todos los pesos del modelo desde la HBM hasta los registros de los SM, y eso domina sobre las operaciones aritméticas. La GPU se pasa el rato esperando bytes, no calculando. Esa única frase —que el decode &amp;ldquo;espera a la HBM&amp;rdquo;— es la raíz de la mitad de las optimizaciones del blog. En un &lt;strong>modelo pequeño&lt;/strong> (SLM, digamos 0.5B–7B) la frase deja de ser cierta de la forma simple en que la contábamos. A &lt;em>batch&lt;/em> 1 sigues siendo memory-bound respecto al hardware, sí, pero el forward pass es tan barato (mover 6 GB a 1 TB/s son ~6 ms, no 70 ms) que los &lt;strong>costes fijos por step&lt;/strong> —lanzamiento de kernels, overhead del scheduler de Python, el &lt;code>sampler&lt;/code>, las copias host↔device, los &lt;code>synchronize&lt;/code>— dejan de ser ruido y pasan a comerse un 20-30 % del tiempo. El cuello se desplaza de la HBM a la &lt;strong>orquestación&lt;/strong>. Consecuencias concretas y cuantitativas: (1) los &lt;strong>CUDA graphs&lt;/strong> y reducir el overhead del scheduler rinden &lt;em>más&lt;/em> en SLM que en modelos grandes; (2) la &lt;strong>cuantización de pesos&lt;/strong> da &lt;em>menos&lt;/em> mejora de latencia a batch 1 en SLM, porque proporcionalmente hay menos pesos que mover frente a activaciones, KV cache y overhead fijo; (3) el &lt;strong>batching&lt;/strong> tiene más headroom porque cruzas el &lt;em>ridge point&lt;/em> tarde; (4) el &lt;strong>KV cache&lt;/strong> puede dominar la memoria relativa. Todo esto sale de un único modelo —el roofline— aplicado con honestidad numérica.&lt;/p>
&lt;h2 id="la-analogía-la-despensa-y-el-camarero">La analogía: la despensa y el camarero&lt;/h2>
&lt;p>Una cocina con dos servicios muy distintos.&lt;/p>
&lt;p>&lt;strong>Servicio de degustación, un plato enorme y lento (el LLM de 70B).&lt;/strong> Cada plato lleva ingredientes pesados que el ayudante tiene que ir a buscar a la despensa del fondo, varias veces, cargando cajas. El cocinero, en cambio, monta el plato en un momento: lo lento es &lt;strong>traer los ingredientes&lt;/strong>, no cocinarlos. Si quieres que el servicio vaya más rápido, no compras un cocinero más hábil: ensanchas el pasillo a la despensa o haces que cada viaje traiga más cajas. La despensa es la &lt;strong>HBM&lt;/strong>; el viaje es el &lt;strong>ancho de banda de memoria&lt;/strong>; cocinar es el &lt;strong>compute&lt;/strong>. El plato grande está &lt;em>bound&lt;/em> por la despensa.&lt;/p>
&lt;p>&lt;strong>Servicio de tapas, platillos minúsculos (el SLM).&lt;/strong> Ahora cada tapa lleva dos ingredientes y se monta en un segundo. El viaje a la despensa por tapa es brevísimo. Pero aparece un coste que en el plato grande era despreciable: el &lt;strong>camarero&lt;/strong>. Por cada tapa, el camarero tiene que ir a la cocina, recoger el platillo, llevarlo a la barra, volver, anotar la comanda, cantarla. Ese ir y venir es &lt;strong>fijo&lt;/strong>: cuesta lo mismo para una tapa que para el plato enorme. Cuando la tapa se monta en un segundo, el camarero —no la despensa— es el cuello de botella. Acortar el pasillo a la despensa (ensanchar la HBM, cuantizar los pesos) ya casi no mejora el servicio; lo que mejora es que el camarero &lt;strong>encadene&lt;/strong> varias comandas sin volver a la cocina cada vez (CUDA graphs) o que sirva varias mesas de una pasada (batching).&lt;/p>
&lt;p>El roofline es la herramienta que dice, con números, &lt;strong>a partir de qué punto el camarero domina sobre la despensa&lt;/strong>. Esa frontera es el &lt;em>ridge point&lt;/em>, y el chiste del título es que en SLM cruzamos el régimen mucho antes de lo que la intuición de los modelos grandes nos hizo creer.&lt;/p>
&lt;h2 id="el-mecanismo-desnudo-qué-dice-el-roofline">El mecanismo desnudo: qué dice el roofline&lt;/h2>
&lt;p>El modelo roofline (Williams, Waterman y Patterson, 2009) parte de una sola magnitud: la &lt;strong>arithmetic intensity&lt;/strong> (intensidad aritmética), que es cuántas operaciones haces por cada byte que mueves desde memoria.&lt;/p>
&lt;p>$$\text{AI} = \frac{\text{FLOPs}}{\text{bytes movidos desde memoria}} \quad [\text{FLOP/byte}]$$&lt;/p>
&lt;p>El hardware tiene dos techos: el de &lt;strong>cómputo&lt;/strong> (peak FLOPS) y el de &lt;strong>memoria&lt;/strong> (peak bandwidth × AI). El rendimiento alcanzable es el mínimo de ambos:&lt;/p>
&lt;p>$$\text{Perf} = \min\big(\text{peak FLOPS},; \text{BW} \times \text{AI}\big)$$&lt;/p>
&lt;p>Donde se cortan las dos líneas está el &lt;strong>ridge point&lt;/strong>, la AI a partir de la cual dejas de estar limitado por memoria y pasas a estarlo por cómputo:&lt;/p>
&lt;p>$$\text{AI}_{\text{ridge}} = \frac{\text{peak FLOPS}}{\text{peak BW}}$$&lt;/p>
&lt;p>Si tu kernel tiene AI por debajo del ridge, estás &lt;strong>memory-bound&lt;/strong> (la GPU espera bytes). Por encima, &lt;strong>compute-bound&lt;/strong> (la GPU calcula a tope y la memoria sobra). Lo importante es que el ridge point es una propiedad &lt;strong>del hardware&lt;/strong>, no del modelo. Veamos los números —aproximados, y los marco como tales porque las cifras de marketing mezclan &lt;em>dense&lt;/em> y &lt;em>sparse&lt;/em>, distintos dtypes y condiciones térmicas irreales.&lt;/p>
&lt;p>&lt;strong>Cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo).&lt;/strong> Por GPU, ~989 TFLOPS BF16 &lt;em>dense&lt;/em> (~1979 TFLOPS FP8 &lt;em>dense&lt;/em>; la cifra con &lt;em>sparsity&lt;/em> es el doble y casi nunca aplica a inferencia LLM). HBM3 ~3.35 TB/s. El ridge en BF16:&lt;/p>
&lt;p>$$\text{AI}_{\text{ridge}}^{\text{H100,BF16}} \approx \frac{989 \times 10^{12}}{3.35 \times 10^{12}} \approx 295 \ \text{FLOP/byte}$$&lt;/p>
&lt;p>En FP8 el ridge sube a ~590 FLOP/byte (el doble de FLOPS contra el mismo BW). &lt;strong>Cuidado&lt;/strong>: estas son cifras de pico de datasheet; en la práctica un kernel real raramente pasa del 70-80 % de cualquiera de los dos techos.&lt;/p>
&lt;p>&lt;strong>RTX 4090 (24 GB, Ada Lovelace).&lt;/strong> ~330 TFLOPS FP16 con acumulación FP16 vía tensor cores (la cifra &amp;ldquo;660 TOPS&amp;rdquo; que circula es con sparsity), y ~1 TB/s de GDDR6X. El ridge:&lt;/p>
&lt;p>$$\text{AI}_{\text{ridge}}^{\text{4090,FP16}} \approx \frac{330 \times 10^{12}}{1.0 \times 10^{12}} \approx 330 \ \text{FLOP/byte}$$&lt;/p>
&lt;p>Curiosamente del mismo orden que la H100 en BF16: la 4090 tiene menos BW pero también menos FLOPS, y el cociente queda parecido. El ridge ronda &lt;strong>300 FLOP/byte&lt;/strong> en ambos casos. Quédate con ese número.&lt;/p>
&lt;p>¿Y dónde cae el decode? En decode a &lt;em>batch&lt;/em> 1, cada peso se carga una vez desde HBM y se usa para una sola multiplicación-acumulación (un token, una fila de activación). La AI del GEMM de decode a batch 1 es del orden de &lt;strong>AI ≈ 1-2 FLOP/byte&lt;/strong> (cada byte de peso participa en ~2 FLOP). Con &lt;em>batch&lt;/em> B, el mismo peso cargado una vez sirve a B filas de activación, así que la AI escala aproximadamente lineal:&lt;/p>
&lt;p>$$\text{AI}_{\text{decode}}(B) \approx 2B \ \text{FLOP/byte} \quad (\text{para la parte GEMM de los pesos})$$&lt;/p>
&lt;p>Cruzas el ridge cuando &lt;code>2B ≈ 300&lt;/code>, es decir &lt;strong>B ≈ 150&lt;/strong> en orden de magnitud (en la práctica antes, por atención y overheads, pero ese es el marco). Conclusión limpia: &lt;strong>el decode a batch bajo está siempre profundamente memory-bound&lt;/strong>, lejísimos del ridge. Por eso decimos que &amp;ldquo;el decode espera a la HBM&amp;rdquo; y por eso cuantizar pesos (mover menos bytes) acelera el decode de un modelo grande casi linealmente. Hasta aquí, todo es el discurso estándar de los posts de modelos grandes.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 420" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Diagrama roofline con ridge point y dónde caen modelo grande y pequeño">
&lt;!-- ejes -->
&lt;line x1="80" y1="360" x2="740" y2="360" stroke="currentColor" stroke-width="1.6"/>
&lt;line x1="80" y1="360" x2="80" y2="40" stroke="currentColor" stroke-width="1.6"/>
&lt;text x="410" y="400" text-anchor="middle" fill="currentColor" font-size="13" font-weight="600">Arithmetic intensity (FLOP/byte) — escala log&lt;/text>
&lt;text x="28" y="200" text-anchor="middle" fill="currentColor" font-size="13" font-weight="600" transform="rotate(-90 28 200)">Rendimiento alcanzable (FLOPS)&lt;/text>
&lt;!-- techo de memoria (rampa) -->
&lt;line x1="80" y1="360" x2="470" y2="90" stroke="#1f5fa8" stroke-width="2.6"/>
&lt;!-- techo de compute (plano) -->
&lt;line x1="470" y1="90" x2="740" y2="90" stroke="#a52a2a" stroke-width="2.6"/>
&lt;!-- ridge point -->
&lt;line x1="470" y1="90" x2="470" y2="360" stroke="currentColor" stroke-width="1" stroke-dasharray="4 3"/>
&lt;circle cx="470" cy="90" r="5" fill="#5a2db0"/>
&lt;text x="470" y="78" text-anchor="middle" fill="#5a2db0" font-size="12" font-weight="600">ridge ≈ 300 FLOP/byte&lt;/text>
&lt;text x="270" y="200" text-anchor="middle" fill="#1f5fa8" font-size="12" font-weight="600" transform="rotate(-35 270 200)">memory-bound (BW × AI)&lt;/text>
&lt;text x="600" y="80" text-anchor="middle" fill="#a52a2a" font-size="12" font-weight="600">compute-bound (peak FLOPS)&lt;/text>
&lt;!-- punto decode batch 1 -->
&lt;circle cx="120" cy="332" r="6" fill="#a48000"/>
&lt;text x="120" y="322" text-anchor="middle" fill="#a48000" font-size="11" font-weight="600">decode B=1 · AI≈2&lt;/text>
&lt;!-- punto decode batch grande -->
&lt;circle cx="360" cy="167" r="6" fill="#2a7a40"/>
&lt;text x="360" y="157" text-anchor="middle" fill="#2a7a40" font-size="11" font-weight="600">decode B≈64 · AI≈128&lt;/text>
&lt;!-- nota SLM -->
&lt;rect x="90" y="300" width="250" height="48" fill="#fff4d6" stroke="#a48000" stroke-width="1.2"/>
&lt;text x="100" y="318" fill="#222" font-size="11" font-weight="600">SLM a B=1: el punto está aquí (memory-bound),&lt;/text>
&lt;text x="100" y="333" fill="#222" font-size="11">pero el roofline NO modela el overhead fijo&lt;/text>
&lt;text x="100" y="346" fill="#222" font-size="11">por step → ver segundo diagrama.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="el-matiz-del-título-por-qué-se-invierte-en-slm">El matiz del título: por qué se invierte en SLM&lt;/h2>
&lt;p>El roofline clásico tiene un punto ciego que en modelos grandes no importa y en pequeños lo es todo: &lt;strong>solo modela el trabajo dentro del kernel&lt;/strong>. Asume que el único tiempo es &lt;code>bytes/BW&lt;/code> o &lt;code>FLOPs/FLOPS&lt;/code>. Pero un step de decode real no es solo el GEMM. Es una secuencia de &lt;strong>decenas de kernels&lt;/strong> (proyecciones QKV, atención, las dos capas del MLP, normalizaciones, residuales, la cabeza de logits, el sampling) y, alrededor de cada uno, hay un &lt;strong>coste fijo de orquestación&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Lanzamiento de kernels&lt;/strong> (&lt;code>kernel launch&lt;/code>): cada &lt;code>cudaLaunchKernel&lt;/code> cuesta del orden de &lt;strong>5-10 µs&lt;/strong> de overhead de CPU/driver, independientemente del tamaño del kernel. Un forward de decode con ~30-60 kernels lanzados secuencialmente arrastra ~0.3-0.6 ms solo en lanzar.&lt;/li>
&lt;li>&lt;strong>Overhead del scheduler de Python&lt;/strong>: el bucle de &lt;a href="https://blog.lo0.es/posts/scheduler-step-vllm/">scheduler de vLLM&lt;/a> prepara metadatos, decide qué requests entran en el step, construye los tensores de entrada. En Python puro esto son cientos de µs a un par de ms por step, sobre todo a concurrencia baja donde no se amortiza.&lt;/li>
&lt;li>&lt;strong>Sampling y post-proceso&lt;/strong>: aplicar temperatura, top-p, penalizaciones, el &lt;code>argmax&lt;/code>/multinomial, copiar el token de vuelta. Otro bloque de cientos de µs.&lt;/li>
&lt;li>&lt;strong>Sincronizaciones y copias host↔device&lt;/strong>: cada &lt;code>synchronize&lt;/code> o copia pequeña añade latencia que no es ni FLOPs ni bytes de HBM.&lt;/li>
&lt;/ul>
&lt;p>Llamemos a la suma de todo esto &lt;strong>T_fijo&lt;/strong>, el coste por step &lt;strong>independiente del tamaño del modelo&lt;/strong>, del orden de &lt;strong>1-3 ms&lt;/strong> en un stack Python sin optimizar. Ahora el tiempo real de un step es:&lt;/p>
&lt;p>$$T_{\text{step}} \approx \underbrace{\frac{\text{bytes de pesos}}{\text{BW}}}&lt;em>{T&lt;/em>{\text{HBM}} \text{ (memory-bound)}} + ; T_{\text{fijo}}$$&lt;/p>
&lt;p>En un &lt;strong>70B BF16&lt;/strong>, mover ~140 GB a 3.35 TB/s son ~42 ms de &lt;code>T_HBM&lt;/code>. Frente a eso, &lt;code>T_fijo&lt;/code> de 1-3 ms es &lt;strong>ruido (2-7 %)&lt;/strong>. El roofline clásico acierta: el modelo &lt;em>está&lt;/em> memory-bound y punto. Pero en un &lt;strong>3B BF16&lt;/strong>, &lt;code>T_HBM&lt;/code> cae a unos pocos ms, y de pronto &lt;code>T_fijo&lt;/code> es del &lt;strong>mismo orden&lt;/strong> que &lt;code>T_HBM&lt;/code>. El cuello deja de ser la despensa y pasa a ser el camarero. Esto es la inversión del título, y de ella se derivan cuatro consecuencias contraintuitivas:&lt;/p>
&lt;p>&lt;strong>(a) A batch 1 sigues memory-bound &lt;em>respecto al hardware&lt;/em>.&lt;/strong> La AI no ha cambiado: sigue siendo ~2 FLOP/byte, debajo del ridge. Quien lea solo el roofline concluirá &amp;ldquo;memory-bound, cuantiza los pesos&amp;rdquo;. Es cierto pero &lt;strong>incompleto&lt;/strong>: el roofline no ve &lt;code>T_fijo&lt;/code>.&lt;/p>
&lt;p>&lt;strong>(b) Los costes fijos pasan a ser una fracción enorme del step.&lt;/strong> Es el punto central. En el 70B, &lt;code>T_fijo / T_step ≈ 5 %&lt;/code>. En el 3B puede ser &lt;strong>20-30 %&lt;/strong>. El cuello efectivo del 3B es mitad HBM, mitad orquestación.&lt;/p>
&lt;p>&lt;strong>(c) Por eso los CUDA graphs y reducir el overhead del scheduler rinden MÁS en SLM.&lt;/strong> Un &lt;a href="https://blog.lo0.es/posts/sm-cuda-streams-cuda-graphs-inferencia/">CUDA graph&lt;/a> captura toda la secuencia de kernels del step y la relanza con &lt;strong>un único&lt;/strong> &lt;code>cudaGraphLaunch&lt;/code>, eliminando casi todo el overhead de lanzamiento por kernel y buena parte del trabajo del scheduler de Python por iteración. En el 70B, recortar 0.5 ms de un step de 42 ms es un +1 % que apenas se nota. En el 3B, recortar esos mismos 0.5 ms de un step de ~7 ms es un &lt;strong>+7 %&lt;/strong>, y si te llevas casi todo &lt;code>T_fijo&lt;/code> puedes ganar &lt;strong>20-30 %&lt;/strong>. La misma optimización, distinto premio, porque el denominador cambió.&lt;/p>
&lt;p>&lt;strong>(d) La cuantización de pesos da MENOS mejora de latencia a batch 1 en SLM.&lt;/strong> Esta es la más contraintuitiva. En el 70B, &lt;code>T_HBM&lt;/code> es casi todo el step; pasar de BF16 a INT4 cuadruplica el ancho de banda efectivo de pesos y casi cuadruplica la velocidad de decode. En el 3B, &lt;code>T_HBM&lt;/code> es solo &lt;em>parte&lt;/em> del step (el resto es &lt;code>T_fijo&lt;/code> + atención + KV). Por la ley de Amdahl, si los pesos son el 60 % del step y los aceleras 4×, el step total mejora solo &lt;code>1/(0.4 + 0.6/4) = 1.8×&lt;/code>, no 4×. Y proporcionalmente hay &lt;strong>menos pesos que mover&lt;/strong> frente a activaciones, KV cache y el overhead fijo. La cuantización agresiva en SLM ayuda, sí, pero &lt;strong>no por la latencia pura a batch 1&lt;/strong> —ahí da rendimientos decrecientes— sino por capacidad y concurrencia (lo veremos al final).&lt;/p>
&lt;p>&lt;strong>(e) El KV cache puede dominar la memoria relativa.&lt;/strong> Con pesos de 6 GB (3B BF16), una sola sesión de contexto largo puede acercarse a ese orden de magnitud en KV cache. En un 70B (140 GB de pesos) el KV es proporcionalmente pequeño hasta concurrencias altas. En SLM el balance de VRAM se inclina hacia el KV mucho antes (el detalle está en &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a>), y eso cambia qué optimización de memoria es la palanca.&lt;/p>
&lt;h2 id="la-matemática-que-importa-el-3b-en-una-4090">La matemática que importa: el 3B en una 4090&lt;/h2>
&lt;p>Hagamos el cálculo entero, que es donde se ve la inversión sin retórica.&lt;/p>
&lt;p>&lt;strong>Modelo:&lt;/strong> 3B parámetros, BF16 → &lt;strong>2 bytes/param&lt;/strong> → ~&lt;strong>6 GB de pesos&lt;/strong>. &lt;strong>Hardware:&lt;/strong> RTX 4090, BW ≈ 1 TB/s.&lt;/p>
&lt;p>&lt;strong>Techo memory-bound del decode (batch 1).&lt;/strong> Cada token requiere cargar los 6 GB una vez:&lt;/p>
&lt;p>$$T_{\text{HBM}} = \frac{6 \times 10^{9} \ \text{bytes}}{1 \times 10^{12} \ \text{bytes/s}} = 6 \times 10^{-3}\ \text{s} = 6\ \text{ms/token}$$&lt;/p>
&lt;p>$$\text{Techo} = \frac{1}{6\ \text{ms}} \approx 166\ \text{tok/s}$$&lt;/p>
&lt;p>Eso es el &lt;strong>techo teórico memory-bound&lt;/strong>: 166 tok/s, asumiendo que mover los pesos es el único coste. El roofline clásico se pararía aquí y diría &amp;ldquo;166 tok/s, ve a por más BW o cuantiza&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Ahora el overhead fijo.&lt;/strong> Pongamos &lt;code>T_fijo ≈ 2 ms/step&lt;/code> (un valor razonable de scheduler de Python + ~40 kernels lanzados + sampling, sin CUDA graphs). El step real:&lt;/p>
&lt;p>$$T_{\text{step}} = T_{\text{HBM}} + T_{\text{fijo}} = 6 + 2 = 8\ \text{ms} ;\Rightarrow; \frac{1}{8\ \text{ms}} = 125\ \text{tok/s}$$&lt;/p>
&lt;p>El overhead se ha comido &lt;strong>41 tok/s de los 166&lt;/strong> teóricos: el &lt;code>T_fijo&lt;/code> es el &lt;strong>25 % del step&lt;/strong> (2 de 8 ms). Compara con el 70B: &lt;code>T_HBM ≈ 42 ms&lt;/code>, &lt;code>T_step ≈ 44 ms&lt;/code>, &lt;code>T_fijo&lt;/code> es el &lt;strong>4.5 %&lt;/strong>. &lt;strong>Mismo overhead absoluto, impacto relativo 5-6× mayor en el SLM.&lt;/strong>&lt;/p>
&lt;p>&lt;strong>Qué pasa si aplicas CUDA graphs&lt;/strong> y te llevas, digamos, 1.5 de los 2 ms de &lt;code>T_fijo&lt;/code>:&lt;/p>
&lt;p>$$T_{\text{step}}^{\text{graphs}} = 6 + 0.5 = 6.5\ \text{ms} ;\Rightarrow; 154\ \text{tok/s}$$&lt;/p>
&lt;p>De 125 a 154 tok/s: &lt;strong>+23 %&lt;/strong> solo por orquestación, sin tocar el modelo ni el hardware de memoria. En el 70B la misma intervención habría dado de 44 a 42.5 ms, &lt;strong>+3.5 %&lt;/strong>. Aquí está, en dos números, &amp;ldquo;otro partido&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Qué pasa si cuantizas los pesos a INT4&lt;/strong> (1.5 GB en vez de 6 GB), con &lt;code>T_fijo&lt;/code> aún en 2 ms:&lt;/p>
&lt;p>$$T_{\text{HBM}}^{\text{INT4}} = \frac{1.5 \times 10^{9}}{1 \times 10^{12}} = 1.5\ \text{ms};\quad T_{\text{step}} = 1.5 + 2 = 3.5\ \text{ms} ;\Rightarrow; 285\ \text{tok/s}$$&lt;/p>
&lt;p>La cuantización 4× de pesos &lt;strong>no&lt;/strong> dio 4× de latencia: pasó de 125 a 285 tok/s, un &lt;strong>2.3×&lt;/strong>, porque el &lt;code>T_fijo&lt;/code> de 2 ms ahora domina (es el &lt;strong>57 %&lt;/strong> del step). En el 70B, cuantizar a INT4 da casi el 4× completo porque &lt;code>T_fijo&lt;/code> sigue siendo ruido. &lt;strong>La misma cuantización rinde el doble de aceleración en el grande que en el pequeño&lt;/strong>, a batch 1. Y si además aplicas CUDA graphs sobre el INT4 (&lt;code>T_fijo → 0.5 ms&lt;/code>): &lt;code>1.5 + 0.5 = 2 ms → 500 tok/s&lt;/code>. El orden de las optimizaciones importa: en SLM &lt;strong>atacar &lt;code>T_fijo&lt;/code> primero&lt;/strong> desbloquea el resto.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Configuración (3B, 4090, batch 1)&lt;/th>
&lt;th>T_HBM&lt;/th>
&lt;th>T_fijo&lt;/th>
&lt;th>T_step&lt;/th>
&lt;th>tok/s&lt;/th>
&lt;th>vs. base&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>BF16, sin graphs (base)&lt;/td>
&lt;td>6.0 ms&lt;/td>
&lt;td>2.0 ms&lt;/td>
&lt;td>8.0 ms&lt;/td>
&lt;td>125&lt;/td>
&lt;td>1.00×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>BF16 + CUDA graphs&lt;/td>
&lt;td>6.0 ms&lt;/td>
&lt;td>0.5 ms&lt;/td>
&lt;td>6.5 ms&lt;/td>
&lt;td>154&lt;/td>
&lt;td>1.23×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4, sin graphs&lt;/td>
&lt;td>1.5 ms&lt;/td>
&lt;td>2.0 ms&lt;/td>
&lt;td>3.5 ms&lt;/td>
&lt;td>285&lt;/td>
&lt;td>2.28×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4 + CUDA graphs&lt;/td>
&lt;td>1.5 ms&lt;/td>
&lt;td>0.5 ms&lt;/td>
&lt;td>2.0 ms&lt;/td>
&lt;td>500&lt;/td>
&lt;td>4.00×&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;em>(Cifras ilustrativas con &lt;code>T_fijo&lt;/code> redondeado; el punto es el patrón, no el decimal. El &lt;code>T_fijo&lt;/code> real depende del stack, la versión de PyTorch/CUDA y si hay tensor parallelism. Mídelo en tu setup antes de creerte ninguna fila.)&lt;/em>&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Desglose de tiempo por step: modelo grande contra modelo pequeño">
&lt;text x="390" y="24" text-anchor="middle" fill="currentColor" font-size="13" font-weight="600">Desglose del tiempo por step de decode (batch 1)&lt;/text>
&lt;!-- 70B -->
&lt;text x="40" y="78" fill="currentColor" font-size="12" font-weight="600">70B BF16&lt;/text>
&lt;text x="40" y="94" fill="currentColor" font-size="11">step ≈ 44 ms&lt;/text>
&lt;rect x="160" y="60" width="560" height="40" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4"/>
&lt;rect x="704" y="60" width="16" height="40" fill="#f6caca" stroke="#a52a2a" stroke-width="1.4"/>
&lt;text x="440" y="85" text-anchor="middle" fill="#1f5fa8" font-size="12" font-weight="600">T_HBM (pesos desde HBM) ≈ 95 %&lt;/text>
&lt;text x="712" y="118" text-anchor="middle" fill="#a52a2a" font-size="10" font-weight="600">T_fijo ~5%&lt;/text>
&lt;!-- 3B -->
&lt;text x="40" y="190" fill="currentColor" font-size="12" font-weight="600">3B BF16&lt;/text>
&lt;text x="40" y="206" fill="currentColor" font-size="11">step ≈ 8 ms&lt;/text>
&lt;rect x="160" y="172" width="420" height="40" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4"/>
&lt;rect x="580" y="172" width="140" height="40" fill="#f6caca" stroke="#a52a2a" stroke-width="1.4"/>
&lt;text x="370" y="197" text-anchor="middle" fill="#1f5fa8" font-size="12" font-weight="600">T_HBM ≈ 75 %&lt;/text>
&lt;text x="650" y="197" text-anchor="middle" fill="#a52a2a" font-size="11" font-weight="600">T_fijo ≈ 25 %&lt;/text>
&lt;!-- nota -->
&lt;text x="160" y="252" fill="currentColor" font-size="11">Mismo T_fijo absoluto (~2 ms): ruido en el 70B, un cuarto del step en el 3B.&lt;/text>
&lt;text x="160" y="270" fill="currentColor" font-size="11">CUDA graphs atacan la franja roja → impacto desproporcionado en el SLM.&lt;/text>
&lt;text x="160" y="288" fill="#5a2db0" font-size="11" font-weight="600">El roofline solo modela la franja azul. El cuello del SLM vive en la roja.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="implicaciones-por-optimización">Implicaciones por optimización&lt;/h2>
&lt;p>Con el modelo en la mano, las palancas del blog se reordenan al cambiar de régimen.&lt;/p>
&lt;p>&lt;strong>Batching: mucho más headroom en SLM.&lt;/strong> Recuerda que cruzas el ridge en &lt;code>B ≈ ridge/2 ≈ 150&lt;/code> en orden de magnitud. En un modelo grande, la VRAM se acaba mucho antes de saturar compute (los pesos + KV no te dejan llegar a batch 150). En un &lt;strong>SLM los pesos ocupan poco&lt;/strong>, así que puedes meter batches grandes en VRAM y &lt;strong>seguir memory-bound&lt;/strong> durante mucho más rango: el &lt;code>T_HBM&lt;/code> de los pesos se &lt;strong>amortiza entre las B requests&lt;/strong> (lo cargas una vez, sirve a B), de modo que el throughput agregado por GPU sube casi linealmente con B hasta muy arriba. Es justo lo contrario del miedo del 70B a saturar compute. En SLM, &lt;strong>batchear es la palanca de throughput por excelencia&lt;/strong> porque saturas compute tarde; el &lt;a href="https://blog.lo0.es/posts/batch-sizing-vllm-grid-search/">grid search de batch en vLLM&lt;/a> tiene una meseta de buen comportamiento mucho más ancha. Ojo: batchear mejora &lt;em>throughput&lt;/em>, no &lt;em>latencia&lt;/em> por request; para latencia single-stream el premio está en &lt;code>T_fijo&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Speculative decoding: otro punto de cruce.&lt;/strong> &lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">Speculative&lt;/a> gana cuando el verify de γ tokens es &amp;ldquo;casi gratis&amp;rdquo; por estar memory-bound. En SLM el target ya es barato, así que el draft tiene que ser &lt;strong>minúsculo&lt;/strong> para que &lt;code>c = T_draft/T_target&lt;/code> siga siendo pequeño, y el &lt;code>T_fijo&lt;/code> del propio draft (lanzar sus kernels) muerde más. El cruce a compute-bound con batch también llega antes en términos absolutos de tok/s servidos. La variante que mejor encaja aquui evita un draft separado: &lt;a href="https://blog.lo0.es/posts/self-speculative-decoding-early-exit/">self-speculative / early-exit&lt;/a> reutiliza capas tempranas del propio modelo y ahorra el &lt;code>T_fijo&lt;/code> de orquestar dos modelos.&lt;/p>
&lt;p>&lt;strong>Cuantización: ayuda por capacidad, no por latencia a batch 1.&lt;/strong> Como mostró la tabla, INT4 en un SLM a batch 1 da rendimientos decrecientes en latencia. Su verdadero premio en SLM es &lt;strong>capacidad&lt;/strong>: pesos 4× más pequeños liberan VRAM para &lt;strong>más KV cache → más concurrencia&lt;/strong>, y es a concurrencia alta (throughput agregado) donde el ahorro de bytes vuelve a pagar. La &lt;a href="https://blog.lo0.es/posts/cuantizacion-agresiva-sub-4-bit-ternario/">cuantización agresiva sub-4-bit y ternaria&lt;/a> lleva esto al extremo: en SLM tiene sentido sobre todo para &lt;strong>encajar más sesiones por GPU&lt;/strong>, no para bajar la latencia de una sola. Y conviene recordar (ver &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">quantization&lt;/a>) que a batch 1 el &lt;code>dequantize&lt;/code> añade trabajo de cómputo que, en un régimen ya rozado por &lt;code>T_fijo&lt;/code>, no siempre sale gratis.&lt;/p>
&lt;p>&lt;strong>Arquitectura: MoE de grano fino cambia qué bytes mueves.&lt;/strong> Un &lt;a href="https://blog.lo0.es/posts/arquitecturas-nativas-device-moe-grano-fino/">MoE device-native de grano fino&lt;/a> activa pocos parámetros por token, así que &lt;code>T_HBM&lt;/code> baja respecto a un denso del mismo tamaño total —pero la fracción &lt;code>T_fijo&lt;/code> sube todavía más, y el router añade su propio overhead fijo. Es el régimen SLM llevado a su límite: casi todo el partido se juega en la orquestación.&lt;/p>
&lt;p>&lt;strong>Scheduler y CUDA graphs primero.&lt;/strong> La conclusión operacional invertida respecto a los posts de modelos grandes: en SLM, &lt;strong>antes de tocar el modelo, mata el &lt;code>T_fijo&lt;/code>&lt;/strong>. CUDA graphs (ver &lt;a href="https://blog.lo0.es/posts/sm-cuda-streams-cuda-graphs-inferencia/">SM, streams y graphs&lt;/a>), un &lt;a href="https://blog.lo0.es/posts/scheduler-step-vllm/">scheduler de vLLM&lt;/a> con su parte de Python minimizada o compilada, y persistencia de kernels son las palancas de primer orden. En un 70B serían un pulido marginal; en un 3B son la mitad del speedup disponible.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise">Aplicado a hardware on-premise&lt;/h2>
&lt;p>&lt;strong>En una RTX 4090 (24 GB, Ada Lovelace).&lt;/strong> Es el escenario donde la inversión es más visible, porque la 4090 tiene ~1 TB/s (un tercio de la H100) pero el &lt;code>T_fijo&lt;/code> es el mismo en términos absolutos. Un 3B BF16 sin CUDA graphs deja ~125 tok/s sobre la mesa cuando el techo memory-bound son 166; activar graphs y limpiar el scheduler recupera la mayor parte. La 4090 cabe holgada para SLM en VRAM, así que el cuello casi nunca es la memoria total sino la &lt;strong>orquestación&lt;/strong> y, a alta concurrencia, el &lt;strong>KV cache&lt;/strong>. Regla de pulgar: en 4090 con SLM, perfila primero el overhead por step (Nsight Systems sobre el gap entre kernels) antes de cuantizar.&lt;/p>
&lt;p>&lt;strong>En un cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo).&lt;/strong> La H100 tiene 3.35 TB/s, así que &lt;code>T_HBM&lt;/code> de un SLM es aún más pequeño (un 3B FP8 son ~3 GB → ~0.9 ms) y el &lt;code>T_fijo&lt;/code> domina &lt;strong>todavía antes&lt;/strong>: un SLM mal orquestado en H100 puede pasar &lt;strong>más tiempo en el scheduler de Python que moviendo pesos&lt;/strong>. Es casi un desperdicio servir un único SLM single-stream en una H100; el modo correcto es &lt;strong>batching agresivo&lt;/strong> (saturas compute tarde, así que metes batches grandes y el throughput por GPU se dispara) o &lt;strong>multiplexar muchos SLM/sesiones&lt;/strong> por GPU vía MPS/MIG. Aquí conecta con &lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">capacity planning&lt;/a>: para SLM el cálculo de capacidad lo gobiernan concurrencia y KV cache, no los pesos. Y con el dilema de &lt;a href="https://blog.lo0.es/posts/tp-replicas-una-grande-vs-n-pequenas/">una grande vs N pequeñas&lt;/a>: replicar SLM tiene sentido precisamente porque cada réplica satura compute tarde y el TP no aporta (el modelo ya cabe; el TP solo añadiría &lt;code>T_fijo&lt;/code> de comunicación).&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>El &lt;code>T_fijo&lt;/code> exacto medido&lt;/strong>, kernel a kernel, con Nsight Systems: cuánto es launch, cuánto scheduler, cuánto sampling. Es el contenido del siguiente post de la serie.&lt;/li>
&lt;li>&lt;strong>&lt;code>torch.compile&lt;/code> / capturas parciales&lt;/strong>: alternativas y complementos a los CUDA graphs cuando hay control flow dinámico.&lt;/li>
&lt;li>&lt;strong>El régimen prefill en SLM&lt;/strong>: el prefill es compute-bound incluso en modelos pequeños (procesa muchos tokens a la vez, AI alta), así que su roofline es el opuesto del decode; ver &lt;a href="https://blog.lo0.es/posts/prefill-optimizaciones-vllm/">prefill&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Atención y KV como segundo término de &lt;code>T_HBM&lt;/code>&lt;/strong>: aquí los hemos metido implícitamente; el desglose fino de la atención (que escala con la longitud de secuencia, no con los pesos) merece su propio tratamiento.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo de la inferencia&lt;/a> — el fenómeno memory-bound del decode nace del KV cache; en SLM el KV pasa a dominar la VRAM relativa antes que en modelos grandes.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/batch-sizing-vllm-grid-search/">Grid search de batch sizing en vLLM&lt;/a> — la meseta de buen batch es mucho más ancha en SLM porque cruzas el ridge tarde; este post da el método empírico.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/decode-optimizaciones-vllm/">Optimizando el decode en vLLM&lt;/a> — los flags concretos (CUDA graphs, eager vs captured) cuyo impacto este post reordena para el caso SLM.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/prefill-optimizaciones-vllm/">Optimizando el prefill en vLLM&lt;/a> — el reverso compute-bound del roofline: el prefill ya vive por encima del ridge incluso en modelos pequeños.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/sm-cuda-streams-cuda-graphs-inferencia/">SM, CUDA streams y CUDA graphs&lt;/a> — el mecanismo que ataca el &lt;code>T_fijo&lt;/code>; aquí explicamos por qué su premio es desproporcionado en SLM.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/scheduler-step-vllm/">El scheduler step de vLLM&lt;/a> — buena parte de &lt;code>T_fijo&lt;/code> vive en este bucle de Python; en SLM minimizarlo es palanca de primer orden.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization para inferencia&lt;/a> — por qué la cuantización de pesos rinde menos latencia a batch 1 en SLM (ley de Amdahl sobre &lt;code>T_HBM&lt;/code>) y más por capacidad.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">Speculative decoding: fundamentos&lt;/a> — el punto de cruce memory/compute se desplaza en SLM, cambiando cuándo speculative paga.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">Capacity planning de inferencia on-premise&lt;/a> — para SLM la capacidad la gobiernan concurrencia y KV, no los pesos; este post da las fórmulas.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/tp-replicas-una-grande-vs-n-pequenas/">Una grande vs N pequeñas&lt;/a> — replicar SLM bate al TP porque cada réplica satura compute tarde y el TP solo añade &lt;code>T_fijo&lt;/code> de comunicación.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/self-speculative-decoding-early-exit/">Self-speculative decoding / early-exit&lt;/a> — hermano de serie: acelerar sin draft separado, evitando el &lt;code>T_fijo&lt;/code> de orquestar dos modelos, encaje natural en SLM.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/arquitecturas-nativas-device-moe-grano-fino/">MoE de grano fino device-native&lt;/a> — hermano de serie: el régimen SLM llevado al límite, donde el router y la orquestación dominan sobre el &lt;code>T_HBM&lt;/code>.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cuantizacion-agresiva-sub-4-bit-ternario/">Cuantización agresiva sub-4-bit y ternaria&lt;/a> — hermano de serie: por qué en SLM sub-4-bit paga sobre todo en capacidad/concurrencia, no en latencia a batch 1.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Williams, S., Waterman, A., Patterson, D. &lt;em>Roofline: An Insightful Visual Performance Model for Multicore Architectures&lt;/em>. Communications of the ACM, 52(4), 2009. &lt;a href="https://doi.org/10.1145/1498765.1498785">https://doi.org/10.1145/1498765.1498785&lt;/a>&lt;/li>
&lt;li>&lt;em>Mind the Memory Gap: Unveiling GPU Bottlenecks in Large-Batch LLM Inference&lt;/em>. arXiv:2503.08311, 2025. &lt;a href="https://arxiv.org/abs/2503.08311">https://arxiv.org/abs/2503.08311&lt;/a>&lt;/li>
&lt;li>Databricks. &lt;em>LLM Inference Performance Engineering: Best Practices&lt;/em>. &lt;a href="https://www.databricks.com/blog/llm-inference-performance-engineering-best-practices">https://www.databricks.com/blog/llm-inference-performance-engineering-best-practices&lt;/a>&lt;/li>
&lt;li>NVIDIA. &lt;em>NVIDIA H100 Tensor Core GPU Datasheet&lt;/em>. &lt;a href="https://resources.nvidia.com/en-us-tensor-core/nvidia-tensor-core-gpu-datasheet">https://resources.nvidia.com/en-us-tensor-core/nvidia-tensor-core-gpu-datasheet&lt;/a>&lt;/li>
&lt;li>NVIDIA. &lt;em>GeForce RTX 4090 — especificaciones de producto&lt;/em> (cifras de tensor cores Ada Lovelace; tratar como aproximadas, mezclan dense/sparse).&lt;/li>
&lt;li>Yuan, Z. et al. &lt;em>LLM Inference Unveiled: Survey and Roofline Model Insights&lt;/em>. arXiv:2402.16363, 2024 — aplicación del roofline específicamente a inferencia LLM.&lt;/li>
&lt;/ul></description></item></channel></rss>