---
title: "QLoRA y multi-LoRA al límite en modelos pequeños"
url: https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/
markdown: https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/index.md
lang: es
date: 2026-06-09
lastmod: 2026-06-12
author: "José Manuel Román"
description: "Cómo QLoRA permite fine-tunear un SLM 3-8B en una sola RTX 4090 (NF4, doble cuantización, paged optimizers) y por qué un SLM base + N adapters de rank bajo es el patrón natural de despliegue soberano."
tags: ["qlora","lora","multi-lora","nf4","adapters","s-lora","punica","fine-tuning","slm","modelos-pequenos","fundamentos"]
translations:
  en: https://blog.lo0.es/en/posts/qlora-multi-lora-limit-small-models/index.md
---

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


> Este post es el complemento de entrenamiento de [Multi-LoRA serving](https://blog.lo0.es/posts/multi-lora-serving-fundamentos/index.md). 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](https://blog.lo0.es/posts/multi-lora-serving-fundamentos/index.md)) = 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](https://blog.lo0.es/posts/quantization-fundamentos-inferencia/index.md); 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.

> **Diagrama:** QLoRA: base 4-bit congelado, adapter BF16 y el gradiente fluyendo solo por el adapter — [verlo en la versión HTML](https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/)

## 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](https://blog.lo0.es/posts/multi-lora-serving-fundamentos/index.md). 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:

| Componente | VRAM 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 |
| **Total** | **cabe 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.

> **Diagrama:** Un SLM base compartido en 4-bit con N adapters por cliente, batching heterogéneo — [verlo en la versión HTML](https://blog.lo0.es/posts/qlora-multi-lora-agresivo-slm/)

## 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.

### En un cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo)

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](https://blog.lo0.es/posts/multi-lora-serving-fundamentos/index.md): 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](https://blog.lo0.es/posts/multi-lora-serving-fundamentos/index.md). 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](https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/index.md).

## Ver también

- [Runbook QLoRA: del dataset al adapter servido](https://blog.lo0.es/posts/qlora-runbook-fine-tuning-serving/index.md) — el compañero operativo de este post: el procedimiento ejecutable paso a paso (entorno, script TRL/PEFT, monitorización, versionado y serving en vLLM con carga en caliente). Con comandos.
- [Multi-LoRA serving](https://blog.lo0.es/posts/multi-lora-serving-fundamentos/index.md) — 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](https://blog.lo0.es/posts/quantization-fundamentos-inferencia/index.md) — el marco de formatos (NF4, INT4, FP8, AWQ) que sostiene el base cuantizado bajo el adapter.
- [Knowledge distillation](https://blog.lo0.es/posts/knowledge-distillation-fundamentos/index.md) — 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](https://blog.lo0.es/posts/fine-tuning-continuo-produccion/index.md) — 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](https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/index.md) — 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

- Dettmers, T., Pagnoni, A., Holtzman, A., Zettlemoyer, L. *QLoRA: Efficient Finetuning of Quantized LLMs*. NeurIPS 2023. <https://arxiv.org/abs/2305.14314>
- Hu, E., Shen, Y., Wallis, P., Allen-Zhu, Z., Li, Y., Wang, S., Wang, L., Chen, W. *LoRA: Low-Rank Adaptation of Large Language Models*. ICLR 2022. <https://arxiv.org/abs/2106.09685>
- Xu, Y., Xie, L., Gu, X., Chen, X., Chang, H., Zhang, H., Chen, Z., Zhang, X., Tian, Q. *QA-LoRA: Quantization-Aware Low-Rank Adaptation of Large Language Models*. ICLR 2024. <https://arxiv.org/abs/2309.14717>
- Sheng, Y. et al. *S-LoRA: Serving Thousands of Concurrent LoRA Adapters*. MLSys 2024. <https://arxiv.org/abs/2311.03285>
- Chen, L. et al. *Punica: Multi-Tenant LoRA Serving*. MLSys 2024. <https://arxiv.org/abs/2310.18547>
- Repo oficial QLoRA / bitsandbytes: <https://github.com/artidoro/qlora>
- Hugging Face PEFT (LoRA, QLoRA): <https://github.com/huggingface/peft>

