Saltar al contenido
Lección 5 de 12

LoRA y QLoRA en Profundidad

8 min read

Entendiendo LoRA Desde Primeros Principios

LoRA (Low-Rank Adaptation of Large Language Models) es la tecnica que hizo el fine-tuning accesible para todos. Antes de sumergirte en el codigo de entrenamiento, necesitas entender como funciona — no solo a alto nivel, sino lo suficientemente profundo para tomar decisiones informadas sobre rango, alpha, modulos objetivo y cuando elegir QLoRA sobre LoRA. Esta leccion te da esa comprension.

Como Funciona LoRA

La Idea Central: Descomposicion de Bajo Rango

En un transformer preentrenado, cada capa contiene matrices de pesos que transforman las entradas. Para una capa lineal con matriz de pesos W de dimensiones (d_out x d_in), una actualizacion estandar durante el entrenamiento produce una nueva matriz W' = W + delta_W, donde delta_W tiene las mismas dimensiones que W.

La idea clave de LoRA: la actualizacion delta_W para la adaptacion de tarea tiene un rango intrinseco bajo. En lugar de aprender una matriz completa (d_out x d_in), puedes descomponer la actualizacion en dos matrices mas pequenas:

delta_W = B @ A

Donde:
  A tiene dimensiones (r x d_in)   — la proyeccion descendente
  B tiene dimensiones (d_out x r)  — la proyeccion ascendente
  r << min(d_in, d_out)            — el rango

Para una capa de atencion tipica donde d_in = d_out = 4096 y rango r = 16:

  • Actualizacion completa: 4096 x 4096 = 16,777,216 parametros
  • Actualizacion LoRA: (4096 x 16) + (16 x 4096) = 131,072 parametros
  • Reduccion: 99.2%

Durante la inferencia, las matrices LoRA se fusionan: W_final = W + (alpha/r) * B @ A. El modelo fusionado tiene exactamente la misma arquitectura y velocidad que el original — no hay sobrecarga de inferencia.

Inicializacion

La matriz A se inicializa con valores Gaussianos aleatorios. La matriz B se inicializa en ceros. Esto significa que la actualizacion LoRA comienza como cero (B @ A = 0), asi que el entrenamiento comienza desde los pesos preentrenados y gradualmente se aleja de ellos. Esta inicializacion es critica para la estabilidad.

Parametros Clave

Rango (r)

El rango determina la expresividad de la adaptacion. Mayor rango significa mas parametros y mas capacidad para aprender adaptaciones complejas.

# Comparacion de rango para una capa de 4096 dimensiones
# r=8:   2 * 4096 * 8  = 65,536 params por capa
# r=16:  2 * 4096 * 16 = 131,072 params por capa
# r=32:  2 * 4096 * 32 = 262,144 params por capa
# r=64:  2 * 4096 * 64 = 524,288 params por capa

Guias practicas:

  • r=8: Adaptacion minima. Bueno para cambios simples de estilo o cumplimiento de formato con datasets grandes.
  • r=16: El punto de partida por defecto. Suficiente para la mayoria de tareas incluyendo seguimiento de instrucciones, adaptacion de dominio y formateo de salida.
  • r=32: Cuando r=16 muestra subajuste (la perdida de entrenamiento se estanca demasiado alta). Bueno para tareas de razonamiento complejo.
  • r=64: El maximo que tipicamente necesitaras. Solo cuando la tarea requiere un cambio de comportamiento significativo. Rendimientos decrecientes mas alla de esto.

Como elegir: Comienza con r=16. Si la perdida de validacion sigue disminuyendo cuando termina el entrenamiento, prueba r=32. Si r=16 entrena bien pero la calidad de evaluacion es insuficiente, prueba r=32 o r=64. Si r=8 funciona tan bien como r=16, usa r=8 para un tamano de adaptador mas pequeno.

Alpha (lora_alpha)

El parametro alpha escala la actualizacion LoRA: actualizacion_efectiva = (alpha / r) * B @ A. Controla la tasa de aprendizaje para los pesos LoRA relativa al modelo base.

Configuraciones comunes:

  • alpha = r (ej., alpha=16, r=16): Factor de escala de 1.0. Conservador. Buen valor por defecto.
  • alpha = 2*r (ej., alpha=32, r=16): Factor de escala de 2.0. Adaptacion mas agresiva. A menudo funciona bien en la practica.
  • alpha = r/2: Muy conservador. Usar cuando quieres desviacion minima del modelo base.

Consejo: La relacion alpha/r importa mas que los valores individuales. Un alpha=32 con r=16 es equivalente a alpha=64 con r=32 en terminos de escalado. Muchos profesionales establecen alpha = 2 * r y ajustan la tasa de aprendizaje en su lugar.

Modulos Objetivo

LoRA puede aplicarse a cualquier capa lineal, pero los objetivos mas comunes son las capas de atencion:

from peft import LoraConfig

# Conservador: solo proyecciones de query y value de atencion
config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
)

# Recomendado: todas las proyecciones de atencion
config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
)

# Agresivo: todas las capas lineales (atencion + MLP)
config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules="all-linear",  # Abreviatura en versiones recientes de PEFT
)

Lo que dice la investigacion: Aplicar LoRA a todas las capas lineales (incluyendo gate_proj, up_proj, down_proj en el MLP) produce consistentemente mejores resultados que solo atencion, con un aumento moderado en parametros entrenables. Para QLoRA en hardware con memoria limitada, comienza solo con atencion y expande si la calidad es insuficiente.

Dropout (lora_dropout)

El dropout de LoRA pone aleatoriamente en cero elementos en las capas LoRA durante el entrenamiento, actuando como regularizacion.

  • 0.0: Sin dropout. Usar para datasets grandes donde el sobreajuste es menos probable.
  • 0.05: Regularizacion ligera. Buen valor por defecto.
  • 0.1: Regularizacion moderada. Usar para datasets pequenos (menos de 500 ejemplos).

QLoRA: Cuantizacion de 4 Bits

QLoRA extiende LoRA cuantizando el modelo base a precision de 4 bits, reduciendo dramaticamente la memoria.

NF4 (NormalFloat 4-bit)

QLoRA introduce el tipo de dato NF4, disenado especificamente para pesos de redes neuronales con distribucion normal. NF4 proporciona mejor representacion teorico-informacional que la cuantizacion estandar INT4.

from transformers import BitsAndBytesConfig
import torch

# Configuracion de cuantizacion QLoRA
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",           # NormalFloat 4-bit
    bnb_4bit_compute_dtype=torch.bfloat16, # Computar en bfloat16
    bnb_4bit_use_double_quant=True,        # Doble cuantizacion
)

Doble Cuantizacion

La cuantizacion estandar almacena constantes de cuantizacion (factores de escala) en FP32. La doble cuantizacion cuantiza estas constantes tambien, ahorrando aproximadamente 0.4 bits adicionales por parametro. Para un modelo de 7B, esto ahorra aproximadamente 350MB de VRAM.

Optimizadores Paginados

QLoRA usa optimizadores paginados que descargan estados del optimizador a memoria CPU cuando la memoria GPU se agota. Esto previene errores de memoria insuficiente durante picos de entrenamiento:

from transformers import TrainingArguments

training_args = TrainingArguments(
    optim="paged_adamw_8bit",  # Adam de 8 bits con paginacion
    # ... otros argumentos
)

Calculo de Ahorro de Memoria

Calculemos los requisitos exactos de memoria para fine-tuning de Llama 3.1 8B:

Fine-tuning completo (FP16):
  Pesos del modelo:      8B * 2 bytes = 16 GB
  Gradientes:            8B * 2 bytes = 16 GB
  Optimizador (Adam):    8B * 8 bytes = 64 GB  (2 estados * 4 bytes cada uno)
  Total:                 ~96 GB

LoRA (base FP16, r=16, capas de atencion):
  Pesos del modelo:      8B * 2 bytes = 16 GB (congelados, sin gradientes)
  Params LoRA:           ~20M * 2 bytes = 40 MB
  Gradientes LoRA:       ~20M * 2 bytes = 40 MB
  Optimizador:           ~20M * 8 bytes = 160 MB
  Total:                 ~17 GB

QLoRA (base 4-bit, r=16, capas de atencion):
  Pesos del modelo:      8B * 0.5 bytes = 4 GB (cuantizado 4-bit)
  Params LoRA:           ~20M * 2 bytes = 40 MB
  Gradientes LoRA:       ~20M * 2 bytes = 40 MB
  Optimizador (8-bit):   ~20M * 4 bytes = 80 MB
  Total:                 ~5 GB

Estos son aproximados — el uso real varia con el tamano de batch, longitud de secuencia y memoria de activaciones. Pero las proporciones son claras: QLoRA usa aproximadamente el 5% de la memoria requerida para fine-tuning completo.

LoRA vs QLoRA: Cuando Usar Cada Uno

| Factor | LoRA (FP16) | QLoRA (4-bit) | |--------|-------------|---------------| | Memoria | ~17GB para 8B | ~5GB para 8B | | Velocidad de entrenamiento | Mas rapido | ~10-15% mas lento | | Calidad | Ligeramente mejor | Muy cercano | | Hardware | GPU 24GB+ | GPU 16GB funciona | | Ideal para | Cuando tienes la VRAM | Cuando la memoria es limitada |

Recomendacion practica: Si tienes suficiente VRAM para LoRA, usa LoRA — es ligeramente mas rapido y evita artefactos de cuantizacion. Si tienes memoria limitada (como la mayoria), QLoRA produce resultados que estan dentro del 1-2% de la calidad de LoRA. La brecha de calidad es lo suficientemente pequena como para que la calidad de tu dataset importe mucho mas que la eleccion entre LoRA y QLoRA.

Ejemplo de Configuracion Completa

from peft import LoraConfig, TaskType
from transformers import BitsAndBytesConfig
import torch

# QLoRA: cuantizacion de 4 bits para el modelo base
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

# Configuracion LoRA
lora_config = LoraConfig(
    r=16,                          # Rango
    lora_alpha=32,                 # Alpha (2x rango)
    target_modules=[               # Que capas adaptar
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj"
    ],
    lora_dropout=0.05,             # Regularizacion ligera
    bias="none",                   # No entrenar terminos de sesgo
    task_type=TaskType.CAUSAL_LM,  # Modelado de lenguaje causal
)

Esta configuracion es un fuerte punto de partida para la mayoria de los proyectos de fine-tuning. En la proxima leccion, la usaremos para entrenar un modelo de principio a fin con el ecosistema de Hugging Face.