Módulo 06 · Eficiencia

QLoRA, precisión y memoria

Reentrenar un modelo no falla por falta de ideas, falla por falta de VRAM. Este módulo separa dos técnicas que a menudo se confunden: LoRA, que congela la base y entrena adaptadores pequeños, y QLoRA, que además comprime la base a 4 bits para que quepa donde antes no cabía. La recomendación práctica es directa: para un modelo chico como Qwen3-0.6B empieza con LoRA normal en BF16/FP16; recurre a QLoRA solo cuando escales a modelos más grandes o cuando la VRAM se quede corta. El resto del módulo es la contabilidad que te deja predecir, antes de lanzar, si un entrenamiento va a caber.

Al terminar sabrás

  • Distinguir LoRA (base en BF16/FP16) de QLoRA (base en 4-bit + cómputo en precisión superior).
  • Explicar qué es NF4 y por qué sus niveles se ubican en los cuantiles de una normal.
  • Leer los flags de bitsandbytes: bnb_4bit_quant_type, bnb_4bit_compute_dtype, double quantization.
  • Enumerar qué consume VRAM: pesos, adaptadores, gradientes, estados del optimizador, activaciones, buffers.
  • Estimar un presupuesto de memoria y saber qué palanca mover: checkpointing, accumulation, packing, longitud máxima.

1LoRA y QLoRA no son lo mismo

En el módulo anterior viste LoRA: la base queda congelada y solo entrenas unas matrices de bajo rango $A, B$ que se suman al peso original, $W' = W + \frac{\alpha}{r} BA$. En LoRA la base vive en precisión de 16 bits (BF16 o FP16). Los gradientes y el optimizador solo cubren los adaptadores, que son diminutos frente al modelo completo.

QLoRA añade una idea ortogonal: guardar la base ya cuantizada a 4 bits en memoria, descomprimirla sobre la marcha a una precisión de cómputo superior (BF16) solo cuando se necesita para el forward y el backward, y —como en LoRA— entrenar únicamente los adaptadores, que se mantienen en 16 bits. El resultado: los pesos base ocupan cuatro veces menos, y la calidad se conserva porque el cómputo nunca ocurre realmente en 4 bits.

LoRA   : base BF16 (congelada) + adapters BF16 (entrenables)
QLoRA  : base NF4 4-bit (congelada) → dequant a BF16 sobre la marcha + adapters BF16 (entrenables)
La diferencia vive en cómo se almacena la base, no en qué se entrena.
Nota · orden recomendado

Para Qwen3-0.6B, LoRA en BF16 cabe de sobra en una GPU modesta y evita el coste de dequantizar en cada paso. QLoRA brilla cuando el modelo es grande (7B, 13B, 70B) o cuando tu VRAM es el cuello de botella. No es "mejor" en abstracto: es la herramienta para cuando la base no cabe en 16 bits.

2Precisiones: FP32, FP16, BF16, INT8, INT4

Antes de cuantizar hay que ordenar el vocabulario. Un número en memoria ocupa un número fijo de bits, y ese tamaño determina cuánto rango y cuánta precisión tiene:

FormatoBitsBytes/parámPara qué se usa
FP32324Referencia exacta; estados del optimizador (momentos AdamW).
FP16162Cómputo rápido; rango limitado, puede desbordar (overflow).
BF16162Mismo rango que FP32, menos mantisa; el compute_dtype favorito.
INT881Inferencia cuantizada; optimizadores de 8-bit.
NF4 / INT440.5Almacenar la base congelada en QLoRA.

La clave: almacenar en 4 bits no significa calcular en 4 bits. QLoRA guarda los pesos en NF4 pero los descomprime a bnb_4bit_compute_dtype (típicamente BF16) para cada multiplicación. La compresión ahorra memoria; el cómputo mantiene la calidad.

3Cuantización por escala (absmax)

Cuantizar es mapear un rango continuo de valores a un conjunto finito de enteros. El esquema absmax por bloque escala cada bloque de pesos por su valor absoluto máximo para que quepa en el rango entero disponible, redondea, y guarda solo el entero:

$$ x_{\text{int}} = \mathrm{round}(x/s), \qquad s = \dfrac{\mathrm{absmax}(\text{bloque})}{2^{b-1}-1}, \qquad \hat x = s\cdot x_{\text{int}} $$

Aquí $s$ es la escala del bloque y $\hat x$ es el valor recuperado (dequantizado). Se hace por bloque —una escala por cada, por ejemplo, 64 pesos— porque un solo peso extremo (outlier) en todo el tensor destrozaría la precisión de los demás si se compartiera una única escala global. Bloques pequeños ⇒ más escalas que almacenar, pero mejor fidelidad.

Cuidado

El error de cuantización es irreversible: $\hat x \neq x$. Lo que hace viable a QLoRA es que la base congelada tolera algo de ruido, y los adaptadores en 16 bits compensan afinando encima. No cuantices los adaptadores ni los estados del optimizador.

4NF4: NormalFloat de 4 bits

Un entero de 4 bits tiene 16 niveles. INT4 los distribuye uniformemente, pero los pesos de una red neuronal no son uniformes: se parecen a una gaussiana centrada en cero. NF4 (NormalFloat-4) aprovecha esto ubicando sus 16 niveles en los cuantiles de una normal estándar, de modo que cada nivel representa aproximadamente la misma masa de probabilidad:

$$ q_i = \Phi^{-1}\!\left(\dfrac{i+0.5}{16}\right), \qquad i = 0, 1, \dots, 15 $$

donde $\Phi^{-1}$ es la inversa de la CDF normal. La fórmula desnuda produce niveles en aproximadamente $[-1.86,\,1.86]$; NF4 los reescala dividiéndolos por su valor absoluto máximo para que caigan en $[-1,1]$, y fuerza un nivel exacto en $0$. En paralelo, los pesos del bloque se normalizan por su absmax a ese mismo rango $[-1,1]$, de modo que niveles y pesos viven en la misma escala. NF4 es "óptimo para información" cuando los datos son gaussianos: dedica más resolución cerca del cero, donde se concentran los pesos. Lo eliges con bnb_4bit_quant_type="nf4" (la alternativa "fp4" usa una rejilla de punto flotante).

5Double quantization

Cada bloque genera una escala $s$ que hay que guardar en FP32. Con bloques de 64, eso son 32 bits extra cada 64 pesos: un sobrecosto de metadatos de $0.5$ bits/parámetro.

$$ \text{overhead de metadatos} \approx \dfrac{32}{\text{blocksize}}\ \text{bits/parámetro} $$

La double quantization (doble cuantización) ataca ese overhead cuantizando también esas escalas (a 8 bits, con una segunda escala), y recupera la mayor parte de él —no todo—, del orden de $0.4$ bits/parámetro. Se activa con bnb_4bit_use_double_quant=True: modesto en porcentaje pero gratis en calidad, y en modelos grandes esos fragmentos de bit suman gigabytes. Es la configuración por defecto recomendada de QLoRA.

6Qué consume la VRAM

Un entrenamiento no se queda sin memoria por una sola razón. Hay seis consumidores, y conviene tenerlos separados en la cabeza:

  • Pesos: la base. En LoRA, $P$ parámetros × 2 bytes (BF16); en QLoRA, × 0.5 bytes (NF4).
  • Adaptadores: las matrices LoRA entrenables, $P_{\text{entrenable}}$ × 2 bytes. Diminutas.
  • Gradientes: uno por parámetro entrenable, misma precisión que el adaptador.
  • Estados del optimizador: AdamW guarda dos momentos en FP32 ⇒ 8 bytes por parámetro entrenable.
  • Activaciones: los tensores intermedios del forward que el backward necesita. Crecen con batch y longitud.
  • Buffers: temporales de CUDA, workspace de kernels, el KV cache si evalúas generando.

El gran alivio de LoRA/QLoRA es que gradientes y estados del optimizador solo cubren los adaptadores, no la base. Por eso un modelo que en full fine-tuning necesitaría estados del optimizador para todos sus parámetros, con LoRA solo los necesita para una fracción minúscula.

Nota · prepare_model_for_kbit_training

Al cargar un modelo cuantizado con bitsandbytes, PEFT ofrece prepare_model_for_kbit_training(model): congela la base, castea capas sensibles (LayerNorm, embeddings, la cabeza) a FP32 por estabilidad y activa el gradient checkpointing. Llámalo antes de envolver el modelo con get_peft_model.

7El presupuesto de memoria

Todo lo anterior se resume en una estimación. Con $P$ parámetros totales, $b_w$ bytes por peso de la base, y $P_{\text{entrenable}}$ parámetros de adaptador:

$$ \mathrm{Mem} \approx P\cdot b_w \;+\; P_{\text{entrenable}}\cdot(\text{grad} + \text{estados opt}) \;+\; \text{activaciones} $$

Para AdamW, los estados del optimizador son dos momentos en FP32 ⇒ 8 bytes por parámetro entrenable (más los gradientes). El término de las activaciones es el que más varía y el más fácil de controlar: el gradient checkpointing no guarda todas las activaciones intermedias, sino un subconjunto, y recomputa el resto durante el backward. Reduce la memoria de activaciones a $O(\sqrt{L})$ capas guardadas a cambio de más cómputo:

$$ \text{activaciones}: \; O(L) \;\longrightarrow\; O(\sqrt{L}) \quad \text{(recompute en el backward)} $$

Otras palancas del mismo presupuesto: gradient accumulation simula un batch grande sumando gradientes de micro-batches (memoria de un micro-batch, efecto de uno grande); sequence packing concatena ejemplos cortos para no desperdiciar padding; flash attention calcula la atención sin materializar la matriz $t\times t$, y la longitud máxima (max_seq_len) recorta directamente el tamaño de las activaciones.

8LoRA vs QLoRA, lado a lado

DimensiónLoRA (BF16/FP16)QLoRA (NF4)
Base en memoria16 bits (2 B/parám)4 bits (0.5 B/parám) + double quant
Precisión de cómputoBF16/FP16 nativoDequant a BF16 sobre la marcha
Qué se entrenaAdaptadores BF16Adaptadores BF16
VRAM de pesosAlta~4× menor
Coste por pasoMenor (sin dequant)Mayor (dequant en cada forward)
OptimizadorAdamW / 8-bit opcionalAdamW de 8-bit (recomendado)
Cuándo usarloEl modelo cabe en 16 bitsEl modelo no cabe / es grande
Calidad esperadaBaseline de 16 bits≈ igual a 16 bits (NF4 preserva)

La fila que importa: la calidad de QLoRA se mantiene a la par con la de LoRA en 16 bits. La cuantización NF4 compra memoria sin vender calidad. Lo que sí pagas es tiempo por paso, por el trabajo de descomprimir. Con un modelo pequeño ese peaje no vale la pena; con uno grande que ni siquiera cargaría, es el único camino.

En el teléfono

QLoRA (NF4 de 4 bits) es una técnica de entrenamiento con poca VRAM, no el formato de despliegue. Para el teléfono, la cuantización que importa es la de inferencia en GGUF (k-quants, Módulo 9). No confundas NF4-entrenamiento con Q4_K-despliegue.

Cómo practicar
  1. Instala pip install bitsandbytes peft transformers.
  2. Carga Qwen3-0.6B en 4-bit NF4 con double-quant.
  3. Activa gradient checkpointing y entrena QLoRA.
  4. Mide la VRAM pico para {full FT fp16, LoRA bf16, QLoRA NF4} × {checkpointing on/off}.
  5. Desglosa cada fila en pesos/optimizador/activaciones y confirma que el eval de QLoRA queda cerca de LoRA bf16.
Herramientas: Python · bitsandbytes · peft · transformers
Rust · on-device

La cuantización de entrenamiento (bitsandbytes NF4) es Python/CUDA.

La cuantización de inferencia sí es territorio Rust: candle corre modelos cuantizados y llama.cpp (Módulos 9–10) genera los GGUF que van al teléfono.

Lecturas y recursos

Ejercicios

De menor a mayor complejidad. El último es el que hace un practicante de verdad.

Ejercicio 1 · calentamiento

Mide el peso de un modelo por precisión

Toma el número de parámetros de Qwen3-0.6B y calcula, a mano, cuántos MB ocupa la base en FP32, BF16 y NF4. Verifica el orden de magnitud cargando el modelo en cada dtype e imprimiendo la memoria reservada.

Entrega: tabla precisión → bytes/parám → MB estimados vs medidos.   Pista: $\text{MB} = P \cdot \text{bytes} / 10^6$; torch.cuda.memory_allocated().

Ejercicio 2

Cuantiza un bloque a mano

Implementa la cuantización absmax por bloque en NumPy: para un vector de 64 pesos calcula $s$, los enteros $x_{\text{int}}$ y la dequantización $\hat x$. Reporta el error de reconstrucción $\lVert x - \hat x \rVert$.

Entrega: función quantize/dequantize + error.   Pista: $s = \mathrm{absmax}/(2^{b-1}-1)$ con $b=4$.

Ejercicio 3

NF4 vs INT4 uniforme

Genera pesos gaussianos, cuantízalos con niveles uniformes (INT4) y con los cuantiles de la normal (NF4), y compara el error de reconstrucción. Grafica dónde ubica cada esquema sus 16 niveles.

Entrega: dos errores + una figura de niveles.   Pista: los niveles NF4 salen de $\Phi^{-1}((i+0.5)/16)$; usa scipy.stats.norm.ppf.

Ejercicio 4

Presupuesto de VRAM en papel

Para Qwen3-0.6B con un adaptador LoRA de rango 16, estima los términos del presupuesto: pesos base (BF16 y NF4), adaptadores, gradientes y estados de AdamW. Predice la memoria pico con y sin gradient checkpointing y luego mídela.

Entrega: desglose estimado vs medido por término.   Pista: estados AdamW = 8 B por parám entrenable; checkpointing baja el término de activaciones.

Ejercicio 5 · ejercicio top

Contabilidad de VRAM de un fine-tune QLoRA

Fine-tunea Qwen3-0.6B con QLoRA (bitsandbytes NF4 + double-quant + gradient checkpointing) y produce una tabla de contabilidad de VRAM: memoria pico medida para {full FT fp16, LoRA bf16, QLoRA NF4} × {checkpointing on/off}, cada fila desglosada en pesos / optimizador / activaciones, y confirma que la métrica de eval del run QLoRA se mantiene dentro de un delta pequeño frente a LoRA bf16.

Entrega: la tabla de desglose de memoria + el chequeo de paridad de eval demostrando que NF4 ≈ calidad de 16-bit.   Pista: BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=torch.bfloat16); mide con torch.cuda.max_memory_allocated().

Puntos clave
  • LoRA guarda la base en 16 bits; QLoRA la guarda en 4 bits (NF4) y dequantiza sobre la marcha a BF16.
  • Almacenar en 4 bits ≠ calcular en 4 bits: el compute_dtype preserva la calidad.
  • NF4 ubica sus 16 niveles en los cuantiles de una normal; double quant comprime hasta las escalas.
  • La VRAM la comen seis cosas: pesos, adaptadores, gradientes, estados del optimizador, activaciones, buffers.
  • Para bajar activaciones: gradient checkpointing ($O(\sqrt{L})$), accumulation, packing, flash attention, longitud máxima.
  • Empieza con LoRA BF16 en modelos chicos; usa QLoRA cuando el modelo no cabe.