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