Unsloth · Fine-tuning acelerado y Triton · 2
SFT, CPT y DPO en la práctica
Entrenar rápido con Triton no sirve si los datos están mal procesados: rellenar secuencias con ceros desperdicia hasta un 70% de cómputo, y calcular pérdida sobre el prompt arruina la calibración del modelo. Esta unidad cubre la ingeniería precisa de secuencias empacadas, chat templates con loss masking y la formulación matemática de DPO para alinear preferencias humanas sin modelos de recompensa intermedios.
Antes de leerUnsloth 1, arquitectura interna y Triton · HF cap. 3 §5, LoRA · HF cap. 11, alineación y RLHF/DPO
🎯 Al terminar vas a poder
- Distinguir cuándo un problema de dominio requiere Continued Pre-Training (CPT) y cuándo basta con Supervised Fine-Tuning (SFT).
- Implementar secuencia empacada (sequence packing) con FlashAttention-2 garantizando que documentos contiguos no intercambien atención cruzada.
- Construir pipelines de SFT con
DataCollatorForCompletionOnlyLMo Chat Templates nativos enmascarando los tokens del prompt (). - Derivar la pérdida de DPO () y explicar cómo el gradiente ajusta el ratio entre política activa y política de referencia congelada.
- Diseñar una estrategia de buffer de repetición (replay buffer) para mitigar el olvido catastrófico de razonamiento general.
0. Continued Pre-Training vs. Supervised Fine-Tuning
Un error frecuente al adaptar un modelo a un dominio corporativo, médico, legal o científico es intentar enseñarle conocimiento nuevo mediante Supervised Fine-Tuning (SFT). SFT está diseñado para enseñar formato, estilo y seguimiento de instrucciones, no para transferir enciclopedias enteras de hechos no estructurados.
Existen dos etapas complementarias de adaptación:
- Continued Pre-Training (CPT): Se alimenta al modelo con texto plano del dominio sin formato de conversación (manuales técnicos, código interno, jurisprudencia). La función objetivo es modelado de lenguaje causal estándar (). El learning rate es muy bajo ( a ), y se entrenan todas las matrices o bien adaptadores LoRA de rango alto () en todas las proyecciones lineales.
- Supervised Fine-Tuning (SFT): Se utilizan pares de entrada y respuesta estructurados (preguntas y respuestas, conversaciones multi-turno, transformaciones de texto). Se enmascaran las preguntas para que el gradiente sólo se propague a través de los tokens que el modelo debe generar.
Unsloth optimiza ambas etapas permitiendo cambiar el modo de entrenamiento sobre la misma instancia del modelo en memoria, aprovechando sus kernels de cálculo fusionado en GPU.
1. Secuencias empacadas (packing) sin contaminación de atención
En un lote de entrenamiento tradicional de PyTorch con secuencias de longitud variable (por ejemplo, textos de 120 tokens,
850 tokens y 2100 tokens), el framework rellena todas las secuencias hasta la longitud máxima del lote o de la ventana
de contexto (por ejemplo, 4096 tokens) con tokens de relleno (<pad>).
Cómo se lee. En conjuntos de datos con muchas respuestas cortas, entre el 50% y el 80% de los tokens procesados en cada pasada son ceros de relleno que consumen memoria y ciclos de cómputo sin aportar información útil.
Sequence Packing (empaquetado): La solución consiste en concatenar múltiples secuencias cortas
dentro de un único vector continuo de 4096 o 8192 tokens, separadas únicamente por el token de fin de secuencia (<|end_of_text|> o <|eot_id|>).
El peligro de contaminación (Cross-Contamination): Si se concatenan secuencias sin modificar la matriz de atención, el token 50 del segundo documento prestaría atención a los tokens del primer documento. En SFT, esto provocaría que el modelo asocie instrucciones no relacionadas.
Unsloth y FlashAttention-2 resuelven esto pasando un vector de longitudes acumuladas (cumulative sequence lengths o cu_seqlens).
El kernel de atención calcula la softmax causal bloque a bloque únicamente dentro de los límites de cada documento:
Cómo se lee. Cada sub-documento dentro del tensor empacado se evalúa en forma estrictamente aislada. Cero tokens de padding, 100% de ocupación en los núcleos de cálculo de la GPU.
2. Chat templates, tokens de parada y Loss Masking en el prompt
El objetivo de un LLM instructivo es responder cuando un usuario pregunta, no aprender a predecir la pregunta del usuario. Si durante el fine-tuning se calcula la función de pérdida sobre todos los tokens de la secuencia (prompt + respuesta), ocurren dos anomalías graves:
- Descalibración del estilo del usuario: El modelo desperdicia capacidad de gradiente intentando memorizar la distribución léxica de las preguntas.
- Respuestas circulares y alucinaciones de cierre: Si el modelo penaliza o premia los encabezados de turno, tiende a generar tokens de corte en momentos equivocados.
Mapeo de etiquetas con valor -100
En PyTorch, la función CrossEntropyLoss ignora de forma nativa cualquier índice de etiqueta igual a -100 (ignore_index=-100).
Por lo tanto, la técnica estándar en Unsloth consiste en estructurar el vector de labels asignando -100 a cada token que pertenezca al sistema o al usuario:
# Vector de Input IDs:
[ <|begin_of_text|>, <|start_header_id|>, "user", <|end_header_id|>, "¿", "Capital", "de", "Francia", "?", <|eot_id|>, <|start_header_id|>, "assistant", <|end_header_id|>, "París", ".", <|eot_id|> ]
# Vector de Labels (Loss Masking):
[ -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, "París", ".", <|eot_id|> ]
El token de fin de turno es obligatorio en el cálculo de pérdida: Notá que el token final <|eot_id|> (en Llama 3)
o <end_of_turn> (en Gemma) sí debe tener etiqueta real y no -100. Si se enmascara el token de fin de turno,
el modelo jamás recibe gradiente para aprender a callarse, y al inferir continuará generando texto sin detenerse hasta agotar la ventana de contexto.
Chat Templates nativos con Unsloth
Unsloth incorpora la función get_chat_template() optimizada para inyectar automáticamente los encabezados
correctos según la familia de modelo y configurar el enmascarador de respuestas:
from unsloth.chat_templates import get_chat_template, train_on_responses_only
tokenizer = get_chat_template(
tokenizer,
chat_template="llama-3.1", # Configura roles <|start_header_id|> y <|eot_id|>
)
# Aplicar máscara para entrenar exclusivamente sobre las respuestas del asistente:
trainer = train_on_responses_only(
trainer,
instruction_part="<|start_header_id|>user<|end_header_id|>\n\n",
response_part="<|start_header_id|>assistant<|end_header_id|>\n\n",
)
3. Direct Preference Optimization (DPO): matemática y gradiente implícito
El paradigma clásico de RLHF (Reinforcement Learning from Human Feedback) con PPO requiere cuatro modelos en memoria simultáneamente: la política activa (), la política de referencia (), el modelo de recompensa () y el crítico (). Esto hacía imposible el alineamiento en una sola GPU.
En 2023, Rafailov et al. formularon Direct Preference Optimization (DPO), demostrando que existe una equivalencia analítica exacta entre la política óptima bajo penalización KL y la función de recompensa subyacente.
Derivación de la recompensa implícita
Bajo el objetivo de regularización de Kullback-Leibler con parámetro de escala :
La solución de forma cerrada de esta optimización es:
Despejando analíticamente la función de recompensa :
Al sustituir esta formulación en el modelo de preferencias de Bradley-Terry, donde la probabilidad de que una respuesta ganadora sea preferida sobre una perdedora es , el término de partición se cancela exactamente por sustracción.
La función de pérdida DPO
Cómo se lee. La pérdida aumenta la probabilidad de la respuesta preferida () y reduce la probabilidad de la respuesta rechazada (), ponderada por la discrepancia respecto al modelo base original. Si el modelo ya prefiere , el gradiente tiende a cero.
Cómo lo ejecuta Unsloth eficientemente: Unsloth concatena en un solo paso hacia adelante el prompt con y , utilizando su kernel de Cross Entropy fusionado para calcular los log-probs en SRAM sin guardar las distribuciones de probabilidad completas en VRAM. Además, mantiene los pesos de referencia desactivando los adaptadores LoRA (pasada en modo base), sin necesidad de duplicar el modelo en memoria física.
4. Prevención de olvido catastrófico y buffers de repetición
Cuando un modelo se adapta intensamente a una tarea especializada (por ejemplo, resumir expedientes clínicos o generar código en Rust), sufre de olvido catastrófico (catastrophic forgetting): su capacidad para razonar en lógica general, matemática básica o mantener conversaciones generales se degrada drásticamente.
Para prevenir esto en producción con Unsloth, se aplican tres salvaguardas operativas:
- Replay Buffer (Buffer de repetición): Se mezcla entre un 5% y un 15% de datos de pre-entrenamiento general o instrucciones de propósito general (por ejemplo, muestras de OpenHermes o UltraChat) dentro del dataset de fine-tuning. Esto ancla los gradientes en el espacio latente original.
- Tasa de aprendizaje conservadora y warmup con decaimiento coseno: Un learning rate excesivo (como ) destruye las representaciones previas. En Unsloth se recomienda para LoRA de rango 16/32 y para adaptadores grandes.
-
Restricción del target de LoRA: Si el dominio es léxico, suele bastar con adaptar las matrices de atención
(
q_proj, v_proj) y las capas MLP (gate_proj, up_proj, down_proj), evitando modificar los embeddings de entrada (embed_tokens) y salida (lm_head) salvo que se agreguen nuevos tokens al vocabulario.
5. Errores comunes
⚠️ Calibración y pérdida en SFT
Calcular pérdida sobre el prompt: Usar un data collator genérico de lenguaje causal en un dataset de instrucciones hace que el modelo intente predecir lo que el usuario va a escribir. Esto triplica la pérdida erróneamente y causa que el modelo empiece sus respuestas repitiendo la pregunta del usuario.
⚠️ Hiperparámetro beta en DPO
Elegir un beta () demasiado pequeño o grande: En DPO, controla qué tan pegada debe quedar la política activa respecto a la de referencia. Con , el modelo ignora la referencia y colapsa en respuestas degeneradas repetitivas. Con , el gradiente es tan débil que el modelo apenas cambia. El rango óptimo empírico es .
⚠️ Tokens de parada faltantes en secuencias empacadas
Empacar sin token EOS: Si concatenás dos ejemplos sin intercalar <|eot_id|>, el modelo interpretará que el segundo texto es la continuación natural del primero. En inferencia, jamás terminará de responder.
6. Resumen
- Continued Pre-Training enseña conocimiento de base no estructurado con baja tasa de aprendizaje; SFT enseña formato de conversación y seguimiento de instrucciones.
- Sequence Packing empaqueta documentos cortos hasta llenar la ventana de contexto sin rellenar con ceros, logrando aceleraciones de 2× a 3× y 100% de uso de GPU.
- FlashAttention con cu_seqlens garantiza aislamiento estricto entre documentos empaquetados, impidiendo contaminación de atención.
- Loss Masking asigna
-100a las etiquetas de los tokens de usuario y sistema, forzando al modelo a especializar sus gradientes exclusivamente en la generación de respuestas. - DPO elimina la necesidad de entrenar y mantener un Reward Model separado, optimizando directamente la política con la probabilidad implícita de respuestas preferidas vs rechazadas.
7. Ponete a prueba
1. ¿Por qué el padding tradicional es ineficiente en lotes con longitudes variadas?
Porque rellena todas las secuencias hasta el tamaño máximo del lote con tokens <pad>. La atención estándar calcula productos punto y asignaciones de memoria cuadráticas sobre tokens irrelevantes, desperdiciando entre 50% y 80% de cómputo en VRAM. (§1)
2. ¿Qué ocurre si se asigna el valor -100 al token de fin de turno (<|eot_id|>)?
PyTorch ignorará ese token durante el cálculo de la pérdida. El modelo nunca aprenderá cuándo debe finalizar su generación, produciendo bucles infinitos o alucinaciones continuas en inferencia. (§2)
3. ¿Cómo elimina DPO la necesidad de un modelo de recompensa (Reward Model)?
DPO demuestra matemáticamente que la recompensa óptima bajo regularización KL puede despejarse directamente en función del log-ratio entre las probabilidades de la política activa y la política de referencia congelada (), insertándola en la función de verosimilitud de Bradley-Terry. (§3)
4. ¿Para qué sirve un buffer de repetición (replay buffer) durante la adaptación a un dominio específico?
Para evitar el olvido catastrófico. Mezclar un 5% a 15% de datos de conversación general o pre-entrenamiento impide que los pesos del modelo colapsen en el subespacio estrecho del nuevo dominio y pierdan razonamiento general. (§4)
8. Para practicar
✏️ Ejercicio 1 — Implementar máscara de pérdida en un dataset de instrucciones
Tomá un conjunto de datos simple con pares de pregunta y respuesta. Usando el tokenizer de Llama 3 o Gemma, tokenizá las cadenas y creá un tensor de labels donde todos los tokens previos al rol assistant tengan el valor -100. Verificá que la suma de gradientes sobre los tokens de usuario sea exactamente 0.
Pista / código orientativo
import torch
# Suponiendo input_ids ya tokenizado
labels = input_ids.clone()
# Encontrar índice donde comienza la respuesta del asistente:
assistant_start = (input_ids == assistant_token_id).nonzero(as_tuple=True)[0][0]
labels[:assistant_start + 1] = -100
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=-100)
# loss_fn ignorará los primeros tokens completamente
9. Fuentes y para seguir
- Rafailov et al. — Direct Preference Optimization: Your Language Model is Secretly a Reward Model (2023) — El paper seminal que introdujo la pérdida analítica de DPO.
- Unsloth SFT & DPO Documentation — Guía de implementación de empaquetado de secuencias y templates de chat sin overhead de memoria.
- Meta AI — The Llama 3 Herd of Models (2024) — Detalles técnicos sobre el régimen de pre-entrenamiento continuado, alineación con DPO y buffers de calidad.