Fine-Tuning Strategies: SFT y Low-Rank Adaptation (LoRA) en LLMs
Análisis técnico de la adaptación de Large Language Models (LLMs) mediante Supervised Fine-Tuning (SFT) y Parameter-Efficient Fine-Tuning (PEFT). Implementación de LoRA para la reducción de requerimientos de VRAM y preservación de pesos pre-entrenados frente al catastrophic forgetting.
El ciclo de vida de los modelos generativos actuales (familia GPT-3, LLaMA, BLOOM) se divide en dos fases críticas con objetivos divergentes: Pre-training y Fine-Tuning.
El pre-entrenamiento busca minimizar la negative log-likelihood sobre un corpus masivo, generando un predictor de tokens generalista. Sin embargo, estos modelos base ("foundation models") carecen de alineación con instrucciones específicas. No responden preguntas; completan patrones.
Para transformar un predictor de tokens en un asistente o experto en dominio, se requiere SFT (Supervised Fine-Tuning). El enfoque tradicional implicaba actualizar el 100% de los parámetros del modelo ($\theta$). Esto presenta dos problemas críticos
- Coste Computacional: Reentrenar modelos de >7B parámetros requiere clusters de GPUs A100, inaccesibles para la mayoría de entornos de producción.
- Catastrophic Forgetting: La modificación agresiva de los pesos originales degrada la capacidad de generalización del modelo.
La solución actual del estado del arte reside en métodos PEFT (Parameter-Efficient Fine-Tuning), específicamente LoRA (Low-Rank Adaptation), que congela los pesos del modelo base e inyecta matrices de rango bajo entrenables, reduciendo el uso de memoria de la GPU hasta en un 60-70% sin introducir latencia de inferencia adicional.
Fundamentos matemáticos
LoRA se basa en la hipótesis de que las actualizaciones de pesos durante la adaptación de un modelo denso tienen un "rango intrínseco" bajo.
Sea $W_0 \in \mathbb{R}^{d \times k}$ la matriz de pesos pre-entrenada y congelada. En lugar de actualizar todos los parámetros tal que $W = W_0 + \Delta W$, LoRA descompone $\Delta W$ en el producto de dos matrices de rango bajo:
$$W = W_0 + BA$$
Donde:
- $B \in \mathbb{R}^{d \times r}$
- $A \in \mathbb{R}^{r \times k}$
- $r \ll \min(d, k)$ es el rango (hiperparámetro).
La operación forward para una entrada $x$ se modifica de la siguiente manera:
$$h = W_0 x + \Delta W x = W_0 x + BAx$$
Inicialmente, $A$ se instancia con una distribución normal aleatoria y $B$ se inicializa a cero, asegurando que al inicio del entrenamiento:
$$BA = 0 \implies h = W_0 x$$
Esto garantiza que el modelo comienza comportándose exactamente como el modelo pre-entrenado. Durante el entrenamiento, solo se optimizan $A$ y $B$. El número de parámetros entrenables se reduce drásticamente, dado que:
$$|\Theta_{LoRA}| = (d + k) \times r \ll d \times k$$
Adicionalmente, se aplica un factor de escala $\frac{\alpha}{r}$ para mantener la magnitud de las actualizaciones constante al variar $r$.
El siguiente diagrama ilustra la arquitectura de LoRA, donde la entrada x fluye simultáneamente a través de los pesos congelados W₀ y las matrices de
rango bajo BA:
Implementación práctica
A continuación, se detalla una implementación utilizando la librería peft de Hugging Face y bitsandbytes para cuantización en 8-bit (estándar actual para consumer hardware).
Entorno: Python 3.9+, PyTorch 1.13+, Transformers 4.28+.
import torch
import torch.nn as nn
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model, TaskType
# 1. Configuración de hiperparámetros
MODEL_NAME = "bigscience/bloom-7b1" # O variante LLaMA si está disponible
LORA_R = 8 # Rango intrínseco
LORA_ALPHA = 32 # Scaling factor
LORA_DROPOUT = 0.05
# 2. Carga del modelo en 8-bit (requiere bitsandbytes)
# Esto permite entrenar modelos de 7B en GPUs con ~12GB VRAM
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
load_in_8bit=True,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
# Congelar pesos base explícitamente (aunque PEFT lo maneja, es buena práctica)
for param in model.parameters():
param.requires_grad = False
# Estabilidad numérica para 8-bit
if param.ndim == 1:
param.data = param.data.to(torch.float32)
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
# 3. Inyección de adaptadores LoRA
config = LoraConfig(
r=LORA_R,
lora_alpha=LORA_ALPHA,
target_modules=["query_key_value"], # Específico para arquitectura BLOOM/GPT
lora_dropout=LORA_DROPOUT,
bias="none",
task_type=TaskType.CAUSAL_LM
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
# Salida típica: "trainable params: 4,194,304 || all params: 7,000,000,000 || trainable%: 0.06"
# 4. Loop de entrenamiento estándar (pseudocódigo simplificado)
# Se utiliza Trainer de Hugging Face normalmente
# optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
# ...
Esta implementación permite ajustar un modelo de 7 billones de parámetros en una sola GPU de consumo (ej. RTX 3090 o RTX 4090), algo imposible con Full Fine-Tuning.
Análisis de comportamiento
Al implementar LoRA en flujos de trabajo de SFT, se observan los siguientes comportamientos empíricos:
- Eficiencia de VRAM: El consumo de memoria de los gradientes se reduce linealmente con la reducción de parámetros entrenables. En Full Fine-Tuning, el optimizador (AdamW) requiere almacenar momentum y varianza para cada parámetro del modelo (aprox. 2x el tamaño del modelo en memoria extra). Con LoRA, solo se almacena el estado del optimizador para las matrices $A$ y $B$.
- Estabilidad Numérica: A diferencia de otros métodos de adaptación (como Prompt Tuning), LoRA es menos sensible a la tasa de aprendizaje, convergiendo de manera estable con valores estándar (ej. $3e^{-4}$).
- Modularidad: Los pesos $A$ y $B$ pesan pocos megabytes. Es posible almacenar un único modelo base ($W_0$) y cargar dinámicamente diferentes adaptadores ($BA_{code}$, $BA_{chat}$, $BA_{medical}$) en tiempo de ejecución.
- Inferencia: En producción, las matrices $BA$ se pueden fusionar algebraicamente con $W_0$ ($W_{final} = W_0 + BA$). Esto significa latencia cero añadida en inferencia, a diferencia de los Adapter Layers tradicionales que insertan capas secuenciales.
La siguiente figura muestra cómo un único modelo base puede servir múltiples casos de uso mediante adaptadores intercambiables:
Comparativas técnicas
Comparativa de estrategias de adaptación en un modelo de arquitectura Transformer Decoder (7B params):
| Métrica | Full Fine-Tuning | Adapter Layers | LoRA |
|---|---|---|---|
| Parámetros Entrenables | 100% | 2-4% | 0.05-0.1% |
| Uso de VRAM (Entrenamiento) | Muy Alto (>80GB) | Medio | Bajo (<24GB en 8-bit) |
| Throughput (Tokens/s) | Base | Base - $\delta$ | Base |
| Latencia de Inferencia | Nula | Alta (capas extra) | Nula (si se fusionan pesos) |
| Riesgo de Forgetting | Alto | Bajo | Muy Bajo |
La ventaja de LoRA sobre los Adapter Layers tradicionales (Houlsby et al., 2019) reside en la estructura paralela y fusionable, evitando el cuello de botella secuencial.
A continuación se visualiza la diferencia arquitectónica entre las tres estrategias de adaptación:
Limitaciones y casos donde no conviene usarlo
A pesar de su eficiencia, LoRA y SFT no son soluciones universales:
- Inyección de Conocimiento Factual: El Fine-Tuning (incluido LoRA) no es el mecanismo adecuado para enseñar nuevos hechos al modelo. Si se intenta enseñar historia reciente a un modelo de 2021 mediante SFT, el modelo tiende a alucinar. Para conocimiento, se debe usar Retrieval Augmented Generation (RAG) o inyección en contexto.
- Capacidades de Razonamiento Complejo: Si la tarea requiere un cambio fundamental en la lógica de razonamiento del modelo (y no solo en el formato de salida o tono), un rango $r$ muy bajo puede actuar como un cuello de botella de información, impidiendo la adaptación necesaria.
- Dependencia del Modelo Base: LoRA no puede reparar un modelo base deficiente. Si el pre-entrenamiento es pobre, la adaptación supervisada amplificará los sesgos o la incoherencia subyacente.
- Elección de Módulos Objetivo: En la implementación actual, aplicar LoRA solo a las proyecciones de Query y Value suele ser suficiente. Aplicarlo a todas las capas densas aumenta el rendimiento marginalmente pero duplica el coste de memoria de los adaptadores.