Phase 11 - Lesson 08

Ajuste Fino con LoRA & QLoRA

El ajuste fino completo (full fine-tuning) de un modelo de 7B requiere 56GB de VRAM. No tienes eso. Tampoco la mayoría de las empresas. LoRA te permite ajustar el mismo modelo con 6GB al entrenar menos del 1% de los parámetros. Esto no es un compromiso: iguala la calidad del ajuste fino completo en la mayoría de las tareas. Todo el ecosistema de ajuste fino de código abierto funciona con este único truco.

Tipo: Build Idiomas: Python Prerrequisitos: Fase 10, Lección 06 (Instruction Tuning / SFT) Tiempo: ~75 minutos Relacionado: La Fase 10 cubre los bucles SFT/DPO desde cero. Esta lección los integra con los toolkits de PEFT de 2026 (PEFT, TRL, Unsloth, Axolotl, LLaMA-Factory).

Objetivos de Aprendizaje

  • Implementar LoRA inyectando matrices adaptadoras de bajo rango (A y B) en las capas de atención de un modelo preentrenado
  • Calcular el ahorro de parámetros de LoRA frente al ajuste fino completo: un rango r con dimensiones d_model entrena 2rd parámetros en lugar de d^2
  • Ajustar un modelo utilizando QLoRA (base cuantizada de 4 bits + adaptadores LoRA) para que quepa en la memoria GPU de consumo
  • Fusionar los pesos de LoRA de vuelta en el modelo base para el despliegue y comparar la velocidad de inferencia con y sin adaptadores

El Problema

Tienes un modelo base. Llama 3 8B. Quieres que responda a los tickets de soporte al cliente con la voz de tu empresa. SFT es la respuesta. Pero SFT tiene un problema de costo.

El ajuste fino completo actualiza cada parámetro del modelo. Llama 3 8B tiene 8 mil millones de parámetros. En fp16, cada parámetro ocupa 2 bytes. Eso es 16GB solo para cargar los pesos. Durante el entrenamiento, también necesitas gradientes (16GB), estados del optimizador para Adam (32GB para momentum + varianza) y activaciones. Total: aproximadamente 56GB de VRAM para un solo modelo de 8B.

Una GPU A100 de 80GB apenas puede albergar esto. Dos A100 cuestan entre $3 y $4 por hora en proveedores de la nube. Entrenar durante 3 épocas con 50,000 ejemplos toma de 6 a 10 horas. Eso equivale a $30-40 por experimento. Realiza 10 experimentos para ajustar bien los hiperparámetros y habrás gastado $400 antes de desplegar nada.

Escala esto a Llama 3 70B y los números se vuelven absurdos. 140GB solo para los pesos. Necesitas un clúster. Más de

00 por experimento.

Existe un problema aún más profundo. El ajuste fino completo modifica cada peso del modelo. Si realizas un ajuste fino con datos de soporte al cliente, podrías degradar las capacidades generales del modelo. Esto se conoce como olvido catastrófico (catastrophic forgetting). El modelo mejora en tu tarea específica y empeora en todo lo demás.

Necesitas un método que entrene menos parámetros, use menos memoria y no destruya el conocimiento existente del modelo.

El Concepto

LoRA: Low-Rank Adaptation

Edward Hu y sus colegas en Microsoft publicaron LoRA en junio de 2021. El hallazgo clave del artículo: las actualizaciones de peso durante el ajuste fino tienen un rango intrínseco bajo. No necesitas actualizar los 16.7 millones de parámetros en una matriz de pesos de 4096x4096. La información útil de la actualización se puede capturar con una matriz de rango 16 o 32.

Aquí están las matemáticas. Una capa lineal estándar calcula:

y = Wx

Donde W es una matriz d_out x d_in. Para una proyección de atención de 4096x4096, eso es 16,777,216 parámetros.

LoRA congela W y añade una descomposición de bajo rango:

y = Wx + BAx

Donde B es (d_out x r) y A es (r x d_in). El rango r es mucho menor que d, típicamente 8, 16 o 32.

Para r=16 en una capa de 4096x4096:

  • Parámetros originales: 4096 x 4096 = 16,777,216
  • Parámetros LoRA: (4096 x 16) + (16 x 4096) = 65,536 + 65,536 = 131,072
  • Reducción: 131,072 / 16,777,216 = 0.78%

Estás entrenando el 0.78% de los parámetros y obteniendo entre el 95% y el 100% de la calidad.

graph LR
    X["Entrada x"] --> W["W Congelada (d x d)"]
    X --> A["A (r x d)"]
    A --> B["B (d x r)"]
    W --> Plus["+ (fusionar)"]
    B --> Plus
    Plus --> Y["Salida y"]

    style W fill:#1a1a2e,stroke:#e94560,color:#fff
    style A fill:#0f3460,stroke:#16213e,color:#fff
    style B fill:#0f3460,stroke:#16213e,color:#fff

A se inicializa con una distribución gaussiana aleatoria. B se inicializa en cero. Esto significa que la contribución de LoRA comienza en cero; el modelo inicia el entrenamiento a partir de su comportamiento original y aprende gradualmente la adaptación.

El Factor de Escala: Alpha

LoRA introduce un factor de escala alpha que controla cuánto afecta la actualización de bajo rango a la salida:

y = Wx + (alpha / r) * BAx

Cuando alpha = r, la escala es de 1x. Cuando alpha = 2r (el valor por defecto común), la escala es de 2x. Este hiperparámetro controla la tasa de aprendizaje del camino de LoRA independientemente de la tasa de aprendizaje base.

Guía práctica:

  • alpha = 2 * rank es una convención común de la comunidad (el artículo original utilizó alpha = rank en la mayoría de los experimentos)
  • alpha = rank proporciona una escala de 1x, conservadora pero estable
  • Un alpha más alto significa actualizaciones más grandes por paso, lo que puede acelerar la convergencia o causar inestabilidad

Dónde Aplicar LoRA

Un transformer tiene muchas capas lineales. No necesitas añadir LoRA a todas ellas. El artículo original probó diferentes combinaciones:

Capas Objetivo Parámetros Entrenables (7B) Calidad
solo q_proj 4.7M Buena
q_proj + v_proj 9.4M Mejor
q_proj + k_proj + v_proj + o_proj 18.9M La mejor para atención
Todas las lineales (atención + MLP) 37.7M Ganancia marginal, 2x parámetros

El punto óptimo para la mayoría de las tareas: q_proj + v_proj. Esto apunta a las proyecciones de consulta (query) y valor (value) en la autoatención, que controlan a qué atiende el modelo y qué información extrae. Añadir capas MLP ayuda en tareas complejas como la generación de código, pero duplica el recuento de parámetros para obtener retornos decrecientes en tareas más simples.

Selección de Rango (Rank)

El rango r controla la expresividad de la adaptación:

Rango (Rank) Parámetros Entrenables (por capa) Ideal Para
4 32,768 Clasificación simple, análisis de sentimiento
8 65,536 Preguntas y respuestas de dominio único, resumen
16 131,072 Tareas multidominio, seguimiento de instrucciones
32 262,144 Razonamiento complejo, generación de código
64 524,288 Retornos decrecientes para la mayoría de las tareas
128 1,048,576 Raramente justificado

Hu et al. demostraron que r=4 ya captura la mayor parte de la adaptación para tareas simples. r=8 y r=16 son las opciones más comunes en la práctica. Ir más allá de r=64 rara vez mejora la calidad y comienza a perder la ventaja de memoria de LoRA.

QLoRA: Cuantización de 4 Bits + LoRA

Tim Dettmers y sus colegas en la Universidad de Washington publicaron QLoRA en mayo de 2023. La idea: cuantizar el modelo base congelado a una precisión de 4 bits, y luego superponer adaptadores LoRA en fp16.

Esto cambia drásticamente la ecuación de memoria:

Método Memoria de Pesos (7B) Memoria de Entrenamiento (7B) GPU Requerida
Ajuste fino completo (fp16) 14GB ~56GB 1x A100 80GB
LoRA (base fp16) 14GB ~18GB 1x A100 40GB
QLoRA (base 4 bits) 3.5GB ~6GB 1x RTX 3090 24GB

QLoRA realiza tres contribuciones técnicas:

NF4 (Normal Float 4-bit): Un nuevo tipo de datos diseñado específicamente para los pesos de redes neuronales. Los pesos de las redes neuronales siguen una distribución aproximadamente normal. NF4 sitúa sus 16 niveles de cuantización en los cuantiles de una distribución normal estándar. Esto es óptimo desde el punto de vista de la teoría de la información para datos distribuidos normalmente. Pierde menos información que la cuantización uniforme de 4 bits (INT4) o la estándar Float4.

Cuantización doble (Double quantization): Las constantes de cuantización en sí mismas ocupan memoria. Cada bloque de 64 pesos necesita un factor de escala fp32 (4 bytes). Para un modelo de 7B, eso representa 0.4GB adicionales. La cuantización doble cuantiza estas constantes a fp8, reduciendo la sobrecarga a 0.1GB. Es pequeño, pero suma.

Optimizadores paginados (Paged optimizers): Durante el entrenamiento, los estados del optimizador (el momentum y la varianza de Adam) pueden superar la memoria de la GPU en secuencias largas. Los optimizadores paginados utilizan la memoria unificada de NVIDIA para paginar automáticamente los estados del optimizador en la memoria RAM de la CPU cuando se agota la memoria de la GPU, y los vuelven a paginar cuando es necesario. Esto evita las caídas por falta de memoria (OOM) a costa de un poco de rendimiento (throughput).

La Cuestión de la Calidad

¿Reducir parámetros o cuantizar la base perjudica la calidad? Los resultados de múltiples artículos científicos:

Método MMLU (5-shot) MT-Bench HumanEval
Ajuste fino completo (Llama 2 7B) 48.3 6.72 14.6
LoRA r=16 47.9 6.68 14.0
QLoRA r=16 (NF4) 47.5 6.61 13.4
QLoRA r=64 (NF4) 48.1 6.70 14.2

LoRA con r=16 está dentro del 1% del ajuste fino completo en la mayoría de los puntos de referencia (benchmarks). QLoRA con r=16 pierde otra fracción de porcentaje. QLoRA con r=64 prácticamente iguala al ajuste fino completo mientras consume un 90% menos de memoria.

Costos en el Mundo Real

Ajuste fino de Llama 3 8B en 50,000 ejemplos (3 épocas):

Método GPU Tiempo Costo
Ajuste fino completo 2x A100 80GB 8 horas ~$32
LoRA r=16 1x A100 40GB 4 horas ~$8
QLoRA r=16 1x RTX 4090 24GB 6 horas ~$5
QLoRA r=16 (Unsloth) 1x RTX 4090 24GB 2.5 horas ~
QLoRA r=16 1x T4 16GB 12 horas ~$4

QLoRA en una sola GPU de consumo cuesta menos que un almuerzo. Esta es la razón por la que la comunidad de ajuste fino de pesos abiertos explotó en 2023 y por la que todos los entornos de entrenamiento a continuación incorporan QLoRA por defecto en 2026.

El stack de PEFT en 2026

Entorno (Framework) Qué es Elige cuando
Hugging Face PEFT La biblioteca canónica para LoRA/QLoRA/DoRA/IA3 Quieres un control directo y tu bucle de entrenamiento ya está en transformers.Trainer
TRL Entrenadores de aprendizaje por refuerzo a partir de comentarios (SFT, DPO, GRPO, PPO, ORPO) de HF Necesitas DPO/GRPO después de SFT; construido sobre PEFT
Unsloth Reescritura en kernel de Triton de los pases hacia adelante y hacia atrás Quieres una aceleración de 2 a 5 veces más rápida + la mitad de VRAM sin pérdida de precisión; familias Llama/Mistral/Qwen
Axolotl Envoltorio con configuración YAML sobre PEFT + TRL + DeepSpeed + Unsloth Buscas ejecuciones de entrenamiento reproducibles y con control de versiones
LLaMA-Factory GUI/CLI/API sobre PEFT + TRL Quieres un ajuste fino sin código; más de 100 familias de modelos compatibles
torchtune Recetas nativas de PyTorch, sin dependencia de transformers Deseas dependencias mínimas y tu organización ya está estandarizada en PyTorch

Regla general: uso de investigación o experimento único → PEFT. Pipeline de producción repetible → Axolotl con kernels Unsloth habilitados. Prototipado rápido y desechable → LLaMA-Factory.

Fusión de Adaptadores

Después del entrenamiento, tienes dos cosas: el modelo base congelado y un adaptador LoRA pequeño (normalmente de 10 a 100MB). Puedes hacer lo siguiente:

  1. Mantenerlos separados: Cargar el modelo base y superponer el adaptador. Intercambiar adaptadores para diferentes tareas. Así es como sirves múltiples variantes ajustadas a partir de un modelo base.

  2. Fusionarlos permanentemente: Calcular W' = W + (alpha/r) * BA y guardar el resultado como un modelo completo nuevo. El modelo fusionado es del mismo tamaño que el original. Sin sobrecarga de inferencia. Sin adaptadores que gestionar.

Para servir múltiples tareas (adaptador de soporte al cliente, adaptador de código, adaptador de traducción), mantenlos separados. Para desplegar un único modelo especializado, fusiónalos.

Técnicas avanzadas de fusión para combinar múltiples adaptadores:

  • TIES-Merging (Yadav et al. 2023): Recorta parámetros de pequeña magnitud, resuelve conflictos de signos y luego fusiona. Reduce la interferencia entre adaptadores.
  • DARE (Yu et al. 2023): Elimina aleatoriamente parámetros del adaptador antes de fusionarlos y escala el resto. Sorprendentemente eficaz para combinar capacidades.
  • Aritmética de tareas (Task arithmetic): Suma o resta pesos de adaptadores. Sumar un adaptador de "código" y otro de "matemáticas" a menudo genera un modelo que es bueno en ambos.

Cuándo NO Ajustar un Modelo

El ajuste fino es la tercera opción, no la primera.

Primero: ingeniería de prompts (prompt engineering). Escribe un mejor prompt del sistema. Añade ejemplos few-shot (de pocos disparos). Utiliza cadena de pensamiento (chain-of-thought). Esto no cuesta nada y toma minutos. Si los prompts te llevan al 80% del camino, probablemente no necesites un ajuste fino.

Segundo: RAG. Si el modelo necesita conocer datos específicos (documentos, base de conocimientos, catálogo de productos), la recuperación de información es más barata y fácil de mantener que grabarla en los pesos. Consulta la Lección 06.

Tercero: ajuste fino. Utiliza esto cuando necesites que el modelo adopte un estilo, formato o patrón de razonamiento específico que no se pueda lograr a través de prompts. Cuando necesites salidas estructuradas consistentes. Cuando necesites destilar un modelo grande en uno más pequeño. Cuando la latencia sea clave y no puedas permitirte el costo de tokens adicionales de los prompts few-shot.

graph TD
    Start["¿Necesitas mejorar el comportamiento del modelo?"] --> PE["Intenta ingeniería de prompts"]
    PE -->|"Funciona"| Done["Despliégalo"]
    PE -->|"No es suficiente"| RAG["¿Necesitas conocimiento externo?"]
    RAG -->|"Sí"| RAGBuild["Construye un pipeline RAG"]
    RAG -->|"No, necesito cambiar estilo/formato"| FT["Ajusta con LoRA/QLoRA"]
    RAGBuild -->|"Funciona"| Done
    RAGBuild -->|"También necesito cambiar el estilo"| FT
    FT --> Done

    style Start fill:#1a1a2e,stroke:#e94560,color:#fff
    style Done fill:#0f3460,stroke:#16213e,color:#fff

Constrúyelo

Implementamos LoRA desde cero en PyTorch puro. Sin bibliotecas. Sin magia. Construirás la capa LoRA, la inyectarás en un modelo, la entrenarás y volverás a fusionar los pesos.

Paso 1: La Capa LoRA

import torch
import torch.nn as nn
import math

class LoRALayer(nn.Module):
    def __init__(self, in_features, out_features, rank=8, alpha=16):
        super().__init__()
        self.rank = rank
        self.alpha = alpha
        self.scaling = alpha / rank

        self.A = nn.Parameter(torch.randn(in_features, rank) * (1 / math.sqrt(rank)))
        self.B = nn.Parameter(torch.zeros(rank, out_features))

    def forward(self, x):
        return (x @ self.A @ self.B) * self.scaling

A se inicializa con valores aleatorios escalados. B se inicializa en cero. El producto BA comienza en cero, por lo que el modelo empieza con su comportamiento original.

Paso 2: Capa Lineal Envuelta con LoRA

class LinearWithLoRA(nn.Module):
    def __init__(self, linear, rank=8, alpha=16):
        super().__init__()
        self.linear = linear
        self.lora = LoRALayer(
            linear.in_features, linear.out_features, rank, alpha
        )

        for param in self.linear.parameters():
            param.requires_grad = False

    def forward(self, x):
        return self.linear(x) + self.lora(x)

La capa lineal original se congela. Solo los parámetros de LoRA (A y B) son entrenables.

Paso 3: Inyectar LoRA en un Modelo

def inject_lora(model, target_modules, rank=8, alpha=16):
    for param in model.parameters():
        param.requires_grad = False

    lora_layers = {}
    for name, module in model.named_modules():
        if isinstance(module, nn.Linear):
            if any(t in name for t in target_modules):
                parent_name = ".".join(name.split(".")[:-1])
                child_name = name.split(".")[-1]
                parent = dict(model.named_modules())[parent_name]
                lora_linear = LinearWithLoRA(module, rank, alpha)
                setattr(parent, child_name, lora_linear)
                lora_layers[name] = lora_linear
    return lora_layers

Primero, congela cada parámetro del modelo. Luego recorre el árbol del modelo, busca capas lineales que coincidan con tus nombres objetivo y reemplázalas por versiones envueltas con LoRA. Las matrices A y B de LoRA son los únicos parámetros entrenables en todo el modelo.

Paso 4: Contar Parâmetros

def count_parameters(model):
    total = sum(p.numel() for p in model.parameters())
    trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
    frozen = total - trainable
    return {
        "total": total,
        "trainable": trainable,
        "frozen": frozen,
        "trainable_pct": 100 * trainable / total if total > 0 else 0
    }

Paso 5: Fusionar los Pesos de Vuelta

def merge_lora_weights(model):
    for name, module in model.named_modules():
        if isinstance(module, LinearWithLoRA):
            with torch.no_grad():
                merged = (
                    module.lora.A @ module.lora.B
                ) * module.lora.scaling
                module.linear.weight.data += merged.T
            parent_name = ".".join(name.split(".")[:-1])
            child_name = name.split(".")[-1]
            if parent_name:
                parent = dict(model.named_modules())[parent_name]
            else:
                parent = model
            setattr(parent, child_name, module.linear)

Después de la fusión, las capas de LoRA desaparecen. El modelo tiene el mismo tamaño que el original con la adaptación integrada en los pesos. Sin sobrecarga de inferencia.

Paso 6: Cuantización Simulación QLoRA

def quantize_to_nf4(tensor, block_size=64):
    blocks = tensor.reshape(-1, block_size)
    scales = blocks.abs().max(dim=1, keepdim=True).values / 7.0
    scales = torch.clamp(scales, min=1e-8)
    quantized = torch.round(blocks / scales).clamp(-8, 7).to(torch.int8)
    return quantized, scales

def dequantize_from_nf4(quantized, scales, original_shape):
    dequantized = quantized.float() * scales
    return dequantized.reshape(original_shape)

Esto simula la cuantización de 4 bits mapeando los pesos en 16 niveles discretos dentro de bloques de 64. QLoRA en producción utiliza la biblioteca bitsandbytes para implementar NF4 real en la GPU.

Paso 7: Bucle de Entrenamiento

def train_lora(model, data, epochs=5, lr=1e-3, batch_size=4):
    optimizer = torch.optim.AdamW(
        [p for p in model.parameters() if p.requires_grad], lr=lr
    )
    criterion = nn.MSELoss()

    losses = []
    for epoch in range(epochs):
        epoch_loss = 0.0
        n_batches = 0
        indices = torch.randperm(len(data["inputs"]))

        for i in range(0, len(indices), batch_size):
            batch_idx = indices[i:i + batch_size]
            x = data["inputs"][batch_idx]
            y = data["targets"][batch_idx]

            output = model(x)
            loss = criterion(output, y)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            epoch_loss += loss.item()
            n_batches += 1

        avg_loss = epoch_loss / n_batches
        losses.append(avg_loss)

    return losses

Paso 8: Demo Completa

def demo():
    torch.manual_seed(42)
    d_model = 256
    n_classes = 10

    model = nn.Sequential(
        nn.Linear(d_model, 512),
        nn.ReLU(),
        nn.Linear(512, 512),
        nn.ReLU(),
        nn.Linear(512, n_classes),
    )

    n_samples = 500
    x = torch.randn(n_samples, d_model)
    y = torch.randint(0, n_classes, (n_samples,))
    y_onehot = torch.zeros(n_samples, n_classes).scatter_(1, y.unsqueeze(1), 1.0)

    data = {"inputs": x, "targets": y_onehot}

    params_before = count_parameters(model)

    lora_layers = inject_lora(
        model, target_modules=["0", "2"], rank=8, alpha=16
    )

    params_after = count_parameters(model)

    losses = train_lora(model, data, epochs=20, lr=1e-3)

    merge_lora_weights(model)
    params_merged = count_parameters(model)

    return {
        "params_before": params_before,
        "params_after": params_after,
        "params_merged": params_merged,
        "losses": losses,
    }

La demo crea un modelo pequeño, inyecta LoRA en dos capas, lo entrena y vuelve a fusionar los pesos. El recuento de parámetros pasa de totalmente entrenable a aproximadamente un 1% entrenable durante el entrenamiento de LoRA, y luego regresa a la arquitectura original después de la fusión.

Úsalo

Con el ecosistema de Hugging Face, implementar LoRA en un modelo real requiere unas 20 líneas:

from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType

model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    target_modules=["q_proj", "v_proj"],
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

Para QLoRA, añade la cuantización con bitsandbytes:

from transformers import BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    quantization_config=bnb_config,
    device_map="auto",
)

model = get_peft_model(model, lora_config)

Eso es todo. Mismo bucle de entrenamiento. Mismo pipeline de datos. El modelo base ahora reside en 4 bits, los adaptadores LoRA se entrenan en fp16 y todo cabe en 6GB.

Para entrenar con Hugging Face Trainer:

from transformers import TrainingArguments, Trainer
from datasets import load_dataset

dataset = load_dataset("tatsu-lab/alpaca", split="train[:5000]")

training_args = TrainingArguments(
    output_dir="./lora-llama",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    fp16=True,
    logging_steps=10,
    save_strategy="epoch",
    optim="paged_adamw_8bit",
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
)

trainer.train()

model.save_pretrained("./lora-adapter")

El adaptador guardado es de 10 a 100MB. El modelo base permanece intacto. Puedes compartir adaptadores en Hugging Face Hub sin necesidad de redistribuir el modelo completo.

Despliégalo

Esta lección produce:

  • outputs/prompt-lora-advisor.md — un prompt que te ayuda a decidir el rango (rank) de LoRA, los módulos objetivo y los hiperparámetros para tu tarea específica
  • outputs/skill-fine-tuning-guide.md — una habilidad que enseña a los agentes el árbol de decisión sobre cuándo y cómo ajustar un modelo

Ejercicios

  1. Estudo de ablación del rango (Rank ablation study). Ejecuta la demo con rangos 2, 4, 8, 16, 32 y 64. Grafica la pérdida final frente al rango. Encuentra el punto de retornos decrecientes donde duplicar el rango ya no reduce la pérdida a la mitad. Para una tarea de clasificación simple en características de 256 dimensiones, esto debería estar al rededor de r=8-16.

  2. Comparación de módulos objetivo. Modifica inject_lora para apuntar solo a la capa "0", solo a la capa "2", solo a la capa "4", y a las tres. Entrena cada variante durante 20 épocas. Compara la velocidad de convergencia y la pérdida final. Esto refleja la decisión real de apuntar a q_proj frente a v_proj frente a todas las capas lineales.

  3. Análisis de errores de cuantización. Toma las matrices de pesos del modelo entrenado antes y después de quantize_to_nf4 / dequantize_from_nf4. Calcula el error cuadrático medio, el error absoluto máximo y la correlación entre los pesos originales y reconstruidos. Experimenta con valores de block_size de 32, 64, 128 y 256.

  4. Servicio multiadaptador (Multi-adapter serving). Entrena dos adaptadores LoRA en diferentes subconjuntos de datos (índices pares frente a impares). Guarda ambos adaptadores. Carga el modelo base una vez, luego intercambia los adaptadores y verifica que cada uno produzca salidas diferentes con la misma entrada. Así es como los sistemas de producción sirven múltiples modelos ajustados a partir de una sola base.

  5. Inferencia fusionada frente a no fusionada. Compara la salida del modelo LoRA antes y después de merge_lora_weights con las mismas 100 entradas. Verifica que las salidas sean idénticas (dentro de una tolerancia de punto flotante de 1e-5). Luego realiza un benchmark de la velocidad de inferencia para ambos: el fusionado debería ser ligeramente más rápido ya que es una única multiplicación de matriz en lugar de dos.

Términos Clave

Término Lo que la gente dice Lo que realmente significa
LoRA "Ajuste fino eficiente" Low-Rank Adaptation: congela los pesos base y entrena dos matrices pequeñas A y B cuyo producto aproxima la actualización completa de pesos
QLoRA "Ajuste fino en una laptop" Quantized LoRA: carga el modelo base en NF4 de 4 bits, entrena adaptadores LoRA en fp16 por encima, lo que permite el ajuste fino de un 7B con 6GB de VRAM
Rango / Rank (r) "Cuánto puede aprender el modelo" La dimensión interna de las matrices A y B; controla la expresividad frente a la cantidad de parámetros
Alpha "Tasa de aprendizaje de LoRA" Factor de escala aplicado a la salida de LoRA; alpha/r escala la contribución de la adaptación a la salida final
NF4 "Cuantización de 4 bits" Normal Float 4: un tipo de datos de 4 bits con niveles de cuantización en los cuantiles de la distribución normal, óptimo para pesos de redes neuronales
Adaptador (Adapter) "La pequeña parte entrenada" Las matrices A y B de LoRA guardadas como un archivo independiente (10-100MB), que se pueden cargar sobre cualquier copia del modelo base
Módulos objetivo (Target modules) "A qué capas aplicar LoRA" Las capas lineales específicas (q_proj, v_proj, etc.) en las que se inyectan los adaptadores LoRA
Fusión (Merging) "Integrarlo de forma permanente" Computar W + (alpha/r) * BA y reemplazar el peso original, eliminando la sobrecarga del adaptador en la inferencia
Optimizadores paginados "Evitar OOM durante el entrenamiento" Descarga de estados del optimizador (momentum de Adam, varianza) a la CPU cuando se agota la memoria de la GPU
Olvido catastrófico "El ajuste fino rompió todo lo demás" Cuando la actualización de todos los pesos hace que el modelo pierda capacidades previamente aprendidas

Lecturas Adicionales

  • Hu et al., "LoRA: Low-Rank Adaptation of Large Language Models" (2021) — el artículo original que introduce el método de descomposición de bajo rango, probado en GPT-3 175B con un rango tan bajo como 4
  • Dettmers et al., "QLoRA: Efficient Finetuning of Quantized Language Models" (2023) — presenta NF4, cuantización doble y optimizadores paginados, lo que permite el ajuste fino de un 65B en una sola GPU de 48GB
  • Documentación de la biblioteca PEFT (huggingface.co/docs/peft) — la biblioteca estándar para LoRA, QLoRA y otros métodos de eficiencia de parámetros en el ecosistema de Hugging Face
  • Yadav et al., "TIES-Merging: Resolving Interference When Merging Models" (2023) — técnicas para combinar múltiples adaptadores LoRA sin degradación de la calidad
  • Rafailov et al., "Direct Preference Optimization: Your Language Model is Secretly a Reward Model" (NeurIPS 2023) — derivación de DPO; la etapa de ajuste de preferencias que viene después de SFT, sin necesidad de un modelo de recompensa.
  • Documentación de TRL — referencia oficial para SFTTrainer, DPOTrainer, KTOTrainer y la superficie de integración con PEFT/bitsandbytes/Unsloth.
  • Documentación de Unsloth — kernels fusionados que duplican el rendimiento del ajuste fino y reducen la memoria a la mitad; la capa de rendimiento bajo TRL.
  • Documentación de Axolotl — entrenador de SFT/DPO/QLoRA multi-GPU configurado por YAML; la alternativa de configuración como código para scripts escritos a mano.
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).