QLoRA y multi-LoRA al límite en modelos pequeños

Este post es el complemento de entrenamiento de Multi-LoRA serving. Aquel desmonta el consumidor —cómo se sirven cientos de adapters concurrentes con kernels SGMV y unified paging—; este desmonta el productor —cómo se entrena un adapter sobre un base cuantizado en una sola GPU, y por qué el patrón “un SLM base congelado + N adapters de rank bajo” es el encaje natural de los modelos pequeños. Aquí no repetimos los internals del serving; los damos por leídos.

TL;DR

QLoRA (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. Una: congelar el base y cuantizarlo a 4-bit con un formato nuevo, NF4 (NormalFloat 4-bit), cuantil-óptimo para pesos que se distribuyen casi como una gaussiana. Dos: 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. Tres: dos trucos de memoria, la doble cuantización (cuantizar las propias constantes de cuantización) y los paged optimizers (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 RTX 4090 (24 GB, Ada Lovelace), no en un cluster. Y como el producto del entrenamiento es un adapter de megabytes, no gigabytes, 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.

La analogía: la guitarra congelada y la pedalera intercambiable

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 una sola guitarra —su instrumento de confianza, afinado, con un sonido base que conoce de memoria—. Lo que no hace es comprarse una guitarra nueva para cada canción. Lo que hace es tener una pedalera de efectos: 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.

El mapeo es exacto:

  • La guitarra = el SLM base. Una sola copia, afinada de fábrica, congelada. En QLoRA, además, está guardada en una funda comprimida: cuantizada a 4-bit. No la tocas: ni cambias sus pastillas ni reajustas el mástil. Pesa lo que pesa y ahí se queda.
  • Cada pedal = un adapter LoRA. Pequeño, barato, específico de un sonido. Lo entrenas para una tarea y lo guardas en un cajón.
  • Entrenar QLoRA = 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 corrección que el pedal aplica encima.
  • Servir multi-LoRA (Multi-LoRA serving) = tener toda la pedalera montada en el escenario y elegir el pedal correcto por nota —por request—. La guitarra es la misma; lo que cambia entre requests es qué pedal está activo.

La analogía aguanta hasta el detalle que más confunde: el gradiente del entrenamiento solo “toca” el pedal. 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.

El mecanismo desnudo: LoRA, y por qué se puede entrenar sobre un base 4-bit

Recordatorio mínimo de LoRA (Hu et al., ICLR 2022). Un adapter modifica una matriz W del base sumándole un producto de bajo rango:

$$W’ = W + B A, \qquad A \in \mathbb{R}^{r \times d}, \quad B \in \mathbb{R}^{d \times r}$$

con r el rank, mucho menor que d. En el forward pass no se materializa BA; se calcula:

$$y = W x + B(A x)$$

El cómputo del base (Wx) ocurre igual; el adapter añade dos matmuls baratos. La clave de QLoRA está en quién recibe gradiente. El base W está congelado: ∂L/∂W no se calcula ni se almacena. Solo A y B son entrenables. Por eso W puede vivir cuantizado a 4-bit sin problema: en el forward se deshace la cuantización al vuelo para hacer Wx (dequant → matmul en BF16), pero como W nunca se actualiza, no necesita la precisión de un peso entrenable. El adapter A, B sí está en BF16, y es el único camino por el que fluye el gradiente.

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 12-16 bytes por parámetro entrenable. Con QLoRA, los pesos del base ocupan 0.5 bytes (4-bit) y no tienen 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.

NF4: por qué un formato nuevo en lugar de INT4

QLoRA no usa INT4 lineal para el base, sino NF4 (NormalFloat 4-bit). La intuición: los pesos de un transformer entrenado se distribuyen, empíricamente, muy cerca de una gaussiana centrada en cero. 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 cuantiles de una normal: más niveles donde hay más masa de probabilidad. Es, por construcción, information-theoretically optimal para datos exactamente gaussianos —cada nivel cubre aproximadamente la misma cantidad de pesos—. Además es simétrico respecto al cero y garantiza una representación exacta del 0 (importante para sparsity y padding). El detalle de los formatos de cuantización está en Quantization para inferencia LLM; aquí basta con la idea de que NF4 gasta sus bits donde están los pesos.

Doble cuantización y paged optimizers

Cuantizar a 4-bit no es gratis del todo: necesitas guardar, por cada bloque de pesos (típicamente 64), una constante de escala en FP32 para poder deshacer la cuantización. Esas constantes pesan. Con bloques de 64 y una escala FP32 (32 bits) por bloque, son 32/64 = 0.5 bits por parámetro solo en metadatos — un 12.5 % de overhead sobre los 4 bits útiles. La doble cuantización ataca eso: cuantiza las propias constantes de escala (a 8-bit, en bloques de 256), bajando el overhead a ~0.127 bits/param. 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 “cabe” de “no cabe” en una 4090.

Los paged optimizers 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 unificada NVIDIA: cuando la VRAM aprieta, esas páginas se expulsan a la RAM del host automáticamente y se traen de vuelta cuando hacen falta. No acelera nada; evita el crash en los picos. Convierte un “OOM intermitente” en “un poco más lento en los peores momentos”, que para un entrenamiento desatendido en una sola GPU es la diferencia entre terminar y no terminar.

Forward (azul) hacia delante · Gradiente (rojo) solo por el adapter

xentradaW · x (base congelado)NF4 4-bit · dequant al vuelo · SIN gradienteAdapter LoRA (BF16)A: r×dshrink d→rB: d×rexpand r→d+sumaysalida∂L/∂A , ∂L/∂B — el gradiente solo entra al adapterel base NO recibe gradiente: por eso puede vivir en 4-bit

Entrenamiento “agresivo”: rank muy bajo y QA-LoRA

“Agresivo” en este contexto significa dos cosas, a veces combinadas.

Rank muy bajo (r = 4-8). El rank es el cuello de la corrección: cuánta “capacidad” 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 estrecha y bien definida —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 underfitting: 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 “por si acaso”. 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.

QA-LoRA (quantization-aware LoRA, Xu et al., arXiv:2309.14717). Hay una fricción sutil en QLoRA estándar: entrenas el adapter en BF16 contra un base 4-bit, pero si luego quieres fusionar el adapter en el base (W' = W + BA) 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 consciente de la cuantización del destino: 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 fusiona limpio 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 “QLoRA + servir multi-adapter” y “QA-LoRA + fusionar por tarea” es una decisión de arquitectura de despliegue, no de calidad pura.

La matemática que importa

Tres cuentas mueven cualquier decisión con QLoRA sobre SLMs.

Parámetros del adapter. Para cada matriz objetivo de dimensión d con rank r, el adapter aporta A (r×d) más B (d×r), es decir 2·r·d parámetros. Sumando sobre las matrices objetivo y multiplicando por el número de capas:

$$\text{params}{\text{adapter}} = L \cdot \sum{\text{matrices}} 2 \cdot r \cdot d$$

Ejemplo trabajado — Llama-3-8B, atención (q, k, v, o), d = 4096, L = 32 capas, r = 8. Tomando las cuatro proyecciones de atención con la misma d = 4096 (simplificación; en Llama-3 K y V son más estrechas por GQA, lo que da menos params aún):

$$\text{params} \approx 32 \cdot 4 \cdot (2 \cdot 8 \cdot 4096) = 32 \cdot 4 \cdot 65,536 \approx 8.4\text{M params}$$

En BF16 (2 bytes/param): 8.4M · 2 ≈ 16.8 MB ≈ ~17 MB. Diecisiete megabytes. Compáralo con el base: un 8B en NF4 ocupa 8\text{G} · 0.5\,\text{bytes} ≈ 4\text{ GB} (más el pequeño overhead de constantes tras doble cuantización). El adapter es el 0.4 % 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.

¿Cuántos adapters caben en una 4090 tras el base + KV? 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 ~15 GB libres (siendo conservadores, llamémoslos ~12-15 GB). Con adapters de ~17 MB (r=8, attention-only):

$$\frac{15,000\ \text{MB}}{17\ \text{MB/adapter}} \approx 880 \text{ adapters}$$

Del orden de miles 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 Multi-LoRA serving. El resumen relevante aquí: el compute del adapter es casi gratis (rango bajo, dos matmuls finos); el reto de rendimiento del serving no es ese compute sino el gather/scatter 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.

VRAM de entrenamiento QLoRA en 24 GB. El presupuesto aproximado para fine-tunear el 8B en una 4090:

ComponenteVRAM aprox.
Base 8B en NF4 (pesos congelados)~4.0 GB
Adapter (params BF16 + gradiente + estados Adam, ~16 B/param sobre ~8-40M params)~0.3-0.7 GB
Activaciones (depende de batch y longitud de secuencia; el grueso variable)~6-14 GB
Buffers de dequant, escalas, workspace~1-2 GB
Totalcabe en 24 GB con margen

La pieza grande y variable son las activaciones, que escalan con batch × longitud de secuencia. Por eso el QLoRA real en una 4090 se hace con batch pequeño + gradient accumulation (simular batch grande acumulando gradientes de microbatches) + gradient checkpointing (recomputar activaciones en backward en lugar de guardarlas, cambiando compute por memoria) + secuencias acotadas. Los paged optimizers son el airbag para los picos de activación que, sin ellos, reventarían. La afirmación “QLoRA fine-tunea un 8B en una 4090” es cierta con esa configuración; 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.

Batch heterogéneo: 4 requests, 3 clientes, 3 adapters — un solo SLM base compartido

req_1 → cliente Areq_2 → cliente Areq_3 → cliente Breq_4 → cliente CSLM BASE — Llama-3-8B NF4 (~4 GB) — cargado UNA vez, compartidoW·x se calcula igual para los 4 requests, sin importar el adapter

Pedalera(adapters ~17 MB)adapter A (cliente A)adapter B (cliente B)adapter C (cliente C)… miles más, MB cada uno

El delta del adapter se aplica por fila del batch:reqs 1-2 → adapter A · req 3 → adapter B · req 4 → adapter CEl reto NO es el compute del delta (casi gratis) — es el gather/scatter heterogéneo.Internals (SGMV, unified paging, batching heterogéneo): ver Multi-LoRA serving.

El encaje con modelos pequeños y la soberanía

Aquí es donde QLoRA + SLM deja de ser un truco de VRAM y se vuelve un patrón de arquitectura.

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 tanto el productor como el consumidor: entrenas el adapter de un cliente nuevo en horas, en la misma clase de hardware donde luego lo sirves. El artefacto que circula entre “entrenar” y “desplegar” es un adapter de MB, no GB: se versiona, se firma, se mueve por la red, se almacena en MinIO/S3 sin pensar en el coste.

El patrón soberano se cae por su propio peso:

  • Aislamiento por cliente. 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 (A, B). Borrar un cliente es borrar un fichero de MB, no reentrenar nada.
  • Footprint mínimo. Un base + N adapters cabe donde N bases no cabrían ni de lejos. La economía de “un modelo por cliente” (decenas de GB cada uno) es prohibitiva; la de “un base + adapters” (MB cada uno) es trivial. Es exactamente la diferencia entre la pedalera y comprar una guitarra por canción.
  • Despliegue soberano. 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.

La elección de adaptar por dominio (un adapter por área de conocimiento) frente a recuperar por contexto (RAG que inyecta el conocimiento en el prompt) es real y no excluyente: el adapter cambia el comportamiento y el estilo del modelo, el RAG cambia los hechos a los que accede. Lo trabaja el post hermano de RAG agresivo en modelos pequeños de esta serie; la regla corta es: adapta lo que es estable y conductual, recupera lo que es volátil y factual.

Aplicado a la infraestructura on-premise

En una RTX 4090 (24 GB, Ada Lovelace)

Es el banco de trabajo natural de QLoRA. Caso canónico: base SLM 3-8B en NF4, fine-tuning de un adapter r=8 attention-only, 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 “técnica de paper” a “lo puede hacer cualquiera con una GPU de consumo”, y ese es exactamente su valor. La regla honesta: cabe con la configuración de memoria descrita; con secuencias largas, batch grande o rank alto, sube el hardware.

Aquí QLoRA deja de ser estrictamente necesario para caber —un 8B en BF16 entra de sobra— pero sigue siendo útil por otra razón: paralelizar la producción de adapters. 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 Multi-LoRA serving: base FP8 + cientos de adapters concurrentes. La regla de pulgar: en la 4090, QLoRA es la herramienta para poder fine-tunear; en el cluster H100, es la herramienta para fine-tunear muchos a la vez, barato, manteniendo el formato cuantizado consistente entre entrenamiento y serving.

Lo que no hemos cubierto

  • Los internals del serving heterogéneo (kernels SGMV, MBGMM/MBGMV, unified paging, cold start, eviction): están enteros en Multi-LoRA serving. Este post es deliberadamente el lado del productor.
  • DoRA y variantes (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.
  • Cuantización sub-4-bit y ternaria del base: qué pasa cuando el base baja de NF4 a 2-bit o ternario bajo el adapter; lo trabaja el post hermano de la serie.
  • Recolección del dataset de fine-tuning: cómo se construye el corpus de cada adapter a partir de feedback de producción está en Retrain: cerrar el bucle.

Ver también

  • Multi-LoRA serving — 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.
  • Quantization para inferencia LLM — el marco de formatos (NF4, INT4, FP8, AWQ) que sostiene el base cuantizado bajo el adapter.
  • Knowledge distillation — la alternativa/complemento a adaptar: comprimir el conocimiento en el propio modelo en lugar de en un adapter encima.
  • Fine-tuning continuo en producción — el ciclo operacional que produce adapters nuevos de forma continua a partir de señal de producción.
  • Retrain: cerrar el bucle feedback → dataset → adapter — de dónde sale el dataset con el que se entrena cada adapter QLoRA.
  • Roofline invertido en modelos pequeños (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.
  • Cuantización agresiva sub-4-bit / ternaria (hermano de la serie) — qué pasa con el base cuantizado por debajo de NF4 bajo el adapter.
  • RAG agresivo en modelos pequeños (hermano de la serie) — adaptar por dominio (este post) frente a recuperar por contexto; cuándo cada uno.

Referencias