Fine-tuning — Módulo 12: el fine-tuning en producción
Duodécimo módulo del Curso de Fine-tuning: lo que hace sostenible un modelo afinado en el tiempo. Versionar dataset y adapter como una unidad, reproducibilidad real y el determinismo que no existe en GPU, cuándo reentrenar y cuándo no, el costo por iteración con una tabla de ejemplo, licencias del modelo base, procedencia de los datos y el checklist final del curso.
☰ Índice del contenido
- La tesis del módulo
- El trío inseparable
- Reproducibilidad: hasta dónde llega
- Cuándo reentrenar
- El costo real por iteración
- Licencias y procedencia: las dos preguntas incómodas
- La licencia del modelo base
- La procedencia de los datos de entrenamiento
- El checklist final del curso
- Errores comunes
- La frase para llevarse
- Qué viene
- Material relacionado
En once módulos aprendiste a decidir si el fine-tuning corresponde, a construir el dataset, a entrenar con LoRA, a evaluar si sirvió, a alinear con preferencias, a mergear y servir, y a extender el repertorio con destilación y otras técnicas. Todo eso te da un modelo. Este módulo trata de lo otro: un modelo afinado no es un entregable, es un compromiso. Alguien va a preguntar en ocho meses por qué responde de cierta forma, alguien va a querer reentrenarlo cuando cambie el modelo base, y alguien —probablemente de legal, probablemente en el peor momento— va a preguntar con qué datos se entrenó. Este módulo es lo que hace que esas tres preguntas tengan respuesta.
La tesis del módulo
Un adapter sin su dataset es un artefacto irreproducible: sabes que funciona pero no sabes por qué, y no lo puedes rehacer. El entregable de un fine-tuning nunca es el archivo de pesos. Es el trío dataset + configuración + pesos, versionado como una sola unidad, porque los tres por separado no sirven de nada.
Antes de seguir, un deslinde. Este módulo no vuelve a explicar el ciclo de vida general de un modelo en producción: eso es el Curso de MLOps completo, y ahí están el experiment tracking con MLflow, el model registry, el monitoreo y la detección de drift explicados en detalle. Acá me concentro en lo que es específico del fine-tuning de LLM y que los cursos de MLOps clásico no cubren, porque asumen un modelo tabular que se reentrena con un fit() de veinte segundos.
El trío inseparable
Un fine-tuning tiene tres artefactos, y guardarlos por separado es la causa raíz de casi todos los problemas de este módulo.
UN RESULTADO DE FINE-TUNING
┌────────────────┐ ┌───────────────┐ ┌──────────────┐
│ DATASET │ │ CONFIGURACIÓN │ │ PESOS │
│ sft-v4.jsonl │ │ r, alpha, lr │ │ adapter.safe │
│ dpo-v2.jsonl │ │ epochs, seed │ │ 200 MB │
│ hash sha256 │ │ base model │ │ │
└───────┬────────┘ └───────┬───────┘ └──────┬───────┘
│ │ │
└───────────────────┼─────────────────┘
▼
┌─────────────────────┐
│ adapter-soporte │
│ v4.1 │ ← una versión
└─────────────────────┘
si falta cualquiera de los tres,
el resultado no es reproducible
Por qué el dataset es el que más importa de los tres, y el que más se pierde. Los pesos los tienes: están en el archivo. La configuración la puedes deducir del adapter_config.json que PEFT guarda al lado del adapter. El dataset es lo único que no está contenido en el artefacto, y es lo que define todo el comportamiento del modelo. Si en seis meses el modelo empieza a responder algo raro y tú no tienes el dataset exacto con el que se entrenó, no tienes forma de encontrar el ejemplo que causó ese comportamiento. Puedes reentrenar desde cero, pero no puedes arreglar lo que hay.
La práctica concreta, tomando prestado el patrón de MLOps — Módulo 2: el dataset se versiona con DVC (o con cualquier esquema que deje un puntero liviano en git y los archivos en un bucket), y el manifiesto del entrenamiento guarda el hash del dataset junto a todo lo demás.
"""Manifiesto de entrenamiento: lo que se guarda JUNTO al adapter.
Regla: si esta información no permite reproducir el resultado,
falta algo. Se escribe automáticamente al terminar el train,
nunca a mano.
"""
import hashlib
import json
import subprocess
from datetime import datetime, timezone
from pathlib import Path
import torch
import transformers
import peft
import trl
def sha256(path: str) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for bloque in iter(lambda: f.read(1 << 20), b""):
h.update(bloque)
return h.hexdigest()
def commit_actual() -> str:
"""El commit del código de entrenamiento. Si el repo está sucio,
lo marca: un resultado producido con cambios sin commitear no
es reproducible y hay que dejarlo dicho."""
sha = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
sucio = bool(subprocess.check_output(["git", "status", "--porcelain"]))
return f"{sha}{'-dirty' if sucio else ''}"
def escribir_manifiesto(dir_adapter: str, dataset: str,
config: dict, metricas: dict) -> None:
manifiesto = {
"version": "adapter-soporte-v4.1",
"fecha_utc": datetime.now(timezone.utc).isoformat(),
# 1) El dato. El hash es lo que hace verificable la afirmación.
"dataset": {
"ruta": dataset,
"sha256": sha256(dataset),
"n_ejemplos": sum(1 for _ in open(dataset, encoding="utf-8")),
# El archivo .dvc de al lado guarda el md5 y la ruta remota.
"dvc_meta": (Path(dataset + ".dvc").read_text(encoding="utf-8")
if Path(dataset + ".dvc").exists() else None),
},
# 2) El modelo base, con REVISIÓN. "Qwen2.5-7B-Instruct" sin
# revision apunta a main, y main puede cambiar bajo tus pies.
"modelo_base": {
"id": config["base_model"],
"revision": config["base_revision"], # commit de HF
},
# 3) La configuración completa del entrenamiento.
"entrenamiento": config,
# 4) El entorno. Las versiones deciden el resultado más de lo
# que a nadie le gustaría admitir.
"entorno": {
"python": subprocess.check_output(
["python", "--version"]).decode().strip(),
"torch": torch.__version__,
"transformers": transformers.__version__,
"peft": peft.__version__,
"trl": trl.__version__,
"cuda": torch.version.cuda,
"gpu": torch.cuda.get_device_name(0),
"commit_codigo": commit_actual(),
},
# 5) El resultado medido (módulo 8). Sin esto no hay comparación
# posible con la versión siguiente.
"evaluacion": metricas,
}
salida = Path(dir_adapter) / "manifiesto.json"
salida.write_text(json.dumps(manifiesto, indent=2, ensure_ascii=False),
encoding="utf-8")
Dos campos de ese manifiesto merecen comentario porque son los que más se omiten.
revision del modelo base. Los repositorios de Hugging Face son repos de git: main avanza. Un modelo puede recibir una corrección de tokenizer, un cambio de chat template o pesos actualizados sin cambiar de nombre. Si entrenaste sobre main de hace tres meses y hoy vuelves a bajar main, puede que no sea el mismo modelo. Anotar el commit exacto —y mejor aún, pasarlo en from_pretrained(..., revision="abc123")— convierte una dependencia flotante en una fija.
Las versiones de las librerías. El ecosistema de fine-tuning se mueve rápido y no siempre con compatibilidad hacia atrás. Un adapter entrenado con una versión de PEFT puede cargar distinto en otra; un cambio de default en TRL puede alterar el loss masking. Es la misma lección del módulo 3 llevada al plano operativo: fija las versiones en el requirements.txt con ==, no con >=.
Reproducibilidad: hasta dónde llega
Y ahora la parte honesta, porque en LLM la reproducibilidad tiene un techo que en ML tabular no existe.
Puedes fijar el seed de Python, de NumPy y de PyTorch, ordenar el dataset determinísticamente y usar el mismo hardware, y aun así dos corridas del mismo entrenamiento no van a dar pesos idénticos bit a bit. Las razones son físicas, no de higiene de código:
- La suma en punto flotante no es asociativa.
(a+b)+cno es exactamentea+(b+c)en fp16 o bf16. Las GPUs paralelizan las reducciones y el orden en que llegan los resultados parciales varía entre corridas. - Los kernels de cuDNN eligen algoritmo según heurísticas que dependen de la ocupación de la GPU en ese instante.
- La acumulación de gradientes y el entrenamiento distribuido suman contribuciones de varios workers en el orden en que llegan por la red.
El determinismo se puede forzar en gran medida, y hay que saber que cuesta velocidad:
"""Determinismo tan lejos como se puede llegar, con su costo."""
import os
import random
import numpy as np
import torch
SEED = 42
# Antes de importar torch en algunos casos; en general antes de crear
# cualquier contexto CUDA. Requerido por PyTorch para reducciones
# deterministas en cuBLAS.
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
os.environ["PYTHONHASHSEED"] = str(SEED)
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed_all(SEED)
# Lanza excepción si algún operador no tiene implementación determinista.
# Es lo correcto: prefieres el error a un falso determinismo.
torch.use_deterministic_algorithms(True, warn_only=False)
# benchmark=True deja que cuDNN pruebe algoritmos y elija el más rápido.
# Es no determinista. Apagarlo cuesta velocidad y es el precio.
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
Lo importante es fijar la expectativa correcta. El objetivo no es que los pesos sean idénticos: es que el modelo sea equivalente. La afirmación reproducible que sí puedes sostener es “con este dataset, esta configuración y este seed, el modelo obtiene 0,87 ± 0,01 en el set de evaluación”. Ese ± no es una debilidad: es la varianza real del proceso, y conocerla es lo que te permite distinguir una mejora verdadera de ruido. Si tu cambio de hiperparámetros mueve la métrica menos que esa varianza, no mejoraste nada.
Ojo: corre el mismo entrenamiento dos veces con seeds distintos una vez en la vida del proyecto y mide la diferencia en tu set de evaluación. Ese número es tu piso de ruido, y sin él no sabes si el 0,86 → 0,88 de tu experimento siguiente es señal o es la GPU tirando una moneda. Es media hora de GPU que te ahorra semanas de perseguir mejoras imaginarias.
Cuándo reentrenar
Un modelo afinado se degrada, y no porque los pesos cambien: porque el mundo alrededor cambia. Los triggers específicos del fine-tuning de LLM son cuatro, y ninguno es “pasaron tres meses”.
1. Llegaron datos nuevos que valen la pena. El feedback de producción del módulo 9 se acumula. La regla práctica es reentrenar cuando el volumen nuevo es del orden del 20% al 30% del dataset actual; por debajo de eso, el costo de una iteración completa no se justifica.
2. Cambió el dominio. Política nueva, producto nuevo, regulación nueva. El modelo sigue respondiendo con la política vieja y con total seguridad, que es la forma más peligrosa de estar equivocado. Este trigger es un evento del negocio, no una métrica: si tu equipo de producto cambia la política de devoluciones, el reentrenamiento es parte de ese cambio.
3. Deriva de los prompts de entrada. Los usuarios empiezan a preguntar cosas que no estaban en tu distribución de entrenamiento. Es exactamente el drift del módulo 10 de MLOps, aplicado a texto: monitoreas la distribución de los prompts (por longitud, por tema, por embedding) y comparas contra la del dataset de entrenamiento.
4. Salió un modelo base mejor. Este es el trigger propio de los LLM y no tiene equivalente en ML clásico. Cada pocos meses aparece una generación nueva de modelos abiertos, y con frecuencia el modelo base nuevo sin afinar supera a tu modelo viejo afinado. Es incómodo y es la realidad del campo.
Ese último caso es exactamente la razón por la que el curso completo insistió en LoRA y en versionar el dataset. Si tienes el dataset, migrar de base es re-correr el pipeline con otro base_model: horas de GPU, no semanas de trabajo. Si no lo tienes, migrar es rehacer el proyecto.
¿migrar al modelo base nuevo?
┌──────────────────────────┐
│ evaluar base NUEVO sin │
│ afinar en TU set (m. 8) │
└────────────┬─────────────┘
│
¿supera a tu modelo actual?
│
no ─────┴───── sí
│ │
▼ ▼
┌──────────┐ ┌─────────────────┐
│ no migres│ │ reentrena tu │
│ todavía │ │ dataset sobre │
└──────────┘ │ el base nuevo │
└────────┬────────┘
│
▼
┌─────────────────┐
│ comparar los 3 │
│ y elegir con │
│ el número │
└─────────────────┘
Y el anti-trigger, que hay que decir con la misma fuerza: no reentrenes por calendario. Un reentrenamiento sin datos nuevos ni cambio de contexto produce un modelo distinto, no mejor, y te obliga a reevaluar, revalidar y redesplegar sin ganancia. El mecanismo de promoción con validación previa —champion/challenger, canary, rollback— está en MLOps — Módulo 11 y aplica igual acá: el modelo nuevo no reemplaza al viejo por ser nuevo, lo reemplaza por ganarle en el set de evaluación.
El costo real por iteración
La conversación de presupuesto casi siempre se hace mal, porque se cotiza solo la GPU. La GPU es la línea más barata de la tabla.
Ejemplo concreto: una iteración de fine-tuning con LoRA sobre un modelo 7B, con un dataset de 5.000 ejemplos, en una GPU arrendada tipo A100 de 40 GB a valores de mercado de 2026. Órdenes de magnitud, no cotización.
| Ítem | Cantidad | Costo aprox (USD) | Nota |
|---|---|---|---|
| Curación del dataset | 20–40 h persona | 800 – 1.600 | La línea que manda |
| Generación sintética con LLM | ~5M tokens | 10 – 50 | Si aplica destilación |
| Entrenamiento (LoRA 7B, 3 épocas) | 3–5 h GPU | 5 – 15 | La GPU es lo barato |
| Corridas fallidas y tuning | 3–5 iteraciones | 20 – 60 | Presupuéstalas: siempre ocurren |
| Evaluación (LLM-as-a-judge) | 300 casos × 3 | 5 – 20 | Por iteración |
| Revisión humana de la evaluación | 4–8 h persona | 160 – 320 | El paso que nadie cotiza |
| Total por iteración | ~1.000 – 2.000 | Sin contar el serving |
La lectura de esa tabla es la lección económica del curso entero: el 90% del costo es tiempo de personas trabajando en datos, no cómputo. De ahí salen tres consecuencias prácticas.
Primero, optimizar el costo de GPU es optimizar el 2% de la factura. Vale la pena elegir bien la instancia, pero no vale la pena una semana de ingeniería para bajarla.
Segundo, el dataset es el activo y hay que tratarlo como tal. Es lo que costó dinero, es lo que se reutiliza en cada iteración y en cada migración de modelo base, y es lo que se pierde si no se versiona.
Tercero, una iteración cuesta del orden de mil dólares, así que hacer cinco al azar cuesta cinco mil. El protocolo de una variable a la vez con medición no es purismo metodológico: es control de gastos.
Al costo de iteración hay que sumarle el de serving, que es recurrente y que el módulo 10 ya cubrió: una GPU permanente por variante mergeada, o una compartida con multi-LoRA. Esa decisión, tomada una vez, pesa más en el total anual que todas las iteraciones juntas.
Licencias y procedencia: las dos preguntas incómodas
Esta sección es la que más gente se salta y la que produce los problemas más caros, porque aparecen tarde y no tienen arreglo técnico.
La licencia del modelo base
Cuando afinas un modelo, la licencia del base viaja con tu modelo afinado. No estás creando un modelo nuevo: estás derivando uno. Las familias de licencias que vas a encontrar:
| Tipo | Ejemplos habituales | Uso comercial | Letra chica típica |
|---|---|---|---|
| Apache 2.0 / MIT | Varias familias abiertas | Sí, sin restricción | Atribución. La opción tranquila |
| Licencia comunitaria propia | Familias tipo Llama | Sí, con condiciones | Umbral de usuarios, naming, política de uso aceptable |
| No comercial | Algunos pesos de investigación | No | Prohíbe el uso en producto |
| Gated / acceso restringido | Varios en Hugging Face | Depende | Hay que aceptar términos para descargar |
Tres cosas concretas que hay que hacer, no solo saber:
- Lee la licencia del base antes de entrenar, no antes de lanzar. Descubrir en la revisión legal previa al lanzamiento que el modelo no se puede usar comercialmente significa botar el trabajo o rehacerlo sobre otro base.
- Anota la licencia en el manifiesto. Es un campo más, cuesta nada, y cuando alguien pregunte en un año la respuesta está en el artefacto.
- Revisa las condiciones de naming. Algunas licencias comunitarias exigen que el modelo derivado incluya el nombre de la familia original o que se declare la relación. Es fácil de cumplir y fácil de incumplir por ignorancia.
La procedencia de los datos de entrenamiento
Y el riesgo mayor, que es específico de quien afina con datos propios: entrenar con datos de clientes.
El problema no es de compliance abstracto, es técnico y concreto: un modelo memoriza. Si entrenas con conversaciones reales que contienen nombres, RUT, direcciones, números de tarjeta o información médica, hay una probabilidad no nula de que el modelo reproduzca alguno de esos datos ante un prompt adecuado, incluso ante un prompt inocente. Y a diferencia de una base de datos, no puedes borrar un dato de un modelo. El derecho de supresión de un titular de datos no se satisface con un DELETE: se satisface reentrenando desde cero sin ese dato. Esa es la asimetría que hay que entender antes de meter el primer dato personal a un dataset de entrenamiento.
El checklist mínimo antes de que un dato entre al pipeline:
- Base legal y consentimiento. Los términos bajo los que recolectaste esos datos, ¿permiten usarlos para entrenar modelos? “Mejorar el servicio” es una fórmula que muchos equipos legales no consideran suficiente para entrenamiento de modelos. Pregunta explícitamente.
- Anonimización antes del entrenamiento, no después. Un pipeline que reemplaza nombres, identificadores, teléfonos y direcciones por marcadores (
[NOMBRE],[RUT]) antes de que el texto llegue al trainer. Y la comprobación de que corrió: un test que grepea patrones de RUT y correos sobre el dataset final. - Test de memorización. Después de entrenar, prueba con prompts que induzcan la reproducción de datos del entrenamiento —fragmentos del inicio de ejemplos reales, “completa esta ficha de cliente”— y verifica que el modelo no complete con datos verdaderos.
- Segregación por cliente. Si sirves a varios clientes, un adapter entrenado con datos del cliente A no puede servir al cliente B. La arquitectura multi-LoRA del módulo 10 hace esto natural: un adapter por cliente, aislado, con su propio dataset y su propia trazabilidad.
- Registro de procedencia por fuente. Qué fracción del dataset viene de dónde, con qué base legal. Es una tabla en el manifiesto y es la primera pregunta de cualquier auditoría.
Todo esto es la versión LLM de lo que MLOps — Módulo 12 llama model governance, y la analogía de la model card aplica igual: un documento corto que describe para qué sirve el modelo, con qué se entrenó, qué licencia hereda, qué limitaciones conocidas tiene y en qué no debe usarse. Escribirla toma una hora y es lo que convierte tu adapter en un artefacto defendible.
Cuidado: el riesgo de memorización crece con el número de épocas y con la repetición de un mismo ejemplo en el dataset. Un dato personal que aparece una vez en 50.000 ejemplos y se ve tres veces (tres épocas) tiene poca probabilidad de quedar memorizado literalmente; el mismo dato repetido en 200 ejemplos, mucha. La deduplicación del módulo 2 no es solo higiene de calidad: también es mitigación de privacidad.
El checklist final del curso
Los doce módulos condensados en las preguntas que hay que poder responder antes de que un modelo afinado atienda tráfico real.
Antes de entrenar
- ¿Descarté prompting, few-shot y RAG, y sé por qué no bastaron? (módulo 1)
- ¿Tengo un set de evaluación con el baseline del modelo sin afinar ya medido? (módulo 8)
- ¿El dataset está deduplicado, revisado por un humano y con un split de test que no se contamina? (módulo 2)
- ¿El chat template del dataset es exactamente el del modelo base? (módulo 3)
- ¿La loss está enmascarada sobre el prompt y aplicada solo a las respuestas? (módulo 3)
- ¿Leí la licencia del modelo base y sé que permite mi uso?
- ¿Los datos tienen base legal para entrenamiento y pasaron por anonimización?
Al entrenar
- ¿El
r, elalphay los módulos objetivo de LoRA están elegidos con criterio? (módulo 5) - ¿Estoy mirando la curva de validación y no solo la de entrenamiento? (módulo 7)
- ¿El experimento está registrado con params, métricas y artefactos? (MLOps módulo 3)
- ¿Cambié una variable respecto de la corrida anterior?
- ¿El manifiesto con hash del dataset, revision del base y versiones se escribe solo?
Antes de desplegar
- ¿El modelo afinado le gana al baseline en el set de evaluación, con margen mayor que el ruido?
- ¿Verifiqué que no se degradó en capacidades que no entrené?
- ¿Mergeé sobre el base en fp16, no sobre el cuantizado? (módulo 10)
- ¿El tokenizer viaja junto al modelo y el chat template del serving es el del entrenamiento?
- ¿Reevalué el artefacto final, después del merge y la cuantización?
- ¿Corrí un test de memorización de datos personales?
- ¿Decidí multi-LoRA o mergeado con la cuenta de VRAM hecha?
En producción
- ¿Estoy recolectando feedback que sirva como dataset de preferencias? (módulo 9)
- ¿Monitoreo la deriva de los prompts de entrada? (MLOps módulo 10)
- ¿Tengo definidos los triggers de reentrenamiento y el mecanismo de rollback?
- ¿Existe una model card que diga para qué sirve, con qué se entrenó y en qué no debe usarse?
- ¿Puedo reproducir el modelo en producción a partir de lo que está versionado?
Si la última casilla es la única que no puedes marcar, todavía tienes un problema serio. Es, literalmente, la tesis del módulo.
Errores comunes
- Guardar el adapter y no el dataset. El artefacto es irreproducible: sabes que funciona y no puedes rehacerlo ni arreglarlo. Es el error estructural del módulo y el más común de todos.
- Apuntar al modelo base sin
revision.mainavanza en Hugging Face. Entrenar sobre una referencia flotante significa que dentro de seis meses tu “misma” configuración puede partir de otro modelo. - Esperar determinismo bit a bit en GPU. No existe, por aritmética de punto flotante y paralelismo. Lo que se sostiene es equivalencia dentro de una varianza conocida, y esa varianza hay que medirla una vez para saber qué mejora es real.
- Reentrenar por calendario. Sin datos nuevos ni cambio de contexto, un reentrenamiento produce un modelo distinto, no mejor, y te obliga a repetir todo el ciclo de validación sin ganancia.
- Presupuestar solo la GPU. El cómputo es del orden del 2% del costo de una iteración. Lo que cuesta son las horas de persona curando datos y revisando evaluaciones, y ese es el número que hay que llevar a la reunión de presupuesto.
- No leer la licencia del base hasta antes de lanzar. La licencia viaja con el modelo derivado. Descubrirlo tarde significa botar el trabajo o rehacerlo sobre otro base.
- Entrenar con datos de clientes sin anonimizar. El modelo memoriza, no se puede hacer
DELETEde un dato en los pesos, y una solicitud de supresión implica reentrenar desde cero. Anonimiza antes del pipeline y verifica con un test. - No tener un plan para el modelo base siguiente. Aparece uno mejor cada pocos meses. Con el dataset versionado, migrar son horas de GPU; sin él, es rehacer el proyecto.
La frase para llevarse
“El adapter es el resultado; el dataset es el activo. Si tuvieras que quemar uno de los dos, quema el adapter: con el dataset lo vuelves a hacer en una tarde, y al revés no.”
Qué viene
Módulo 13, el bonus y el cierre del curso: entrenar tus propios embeddings y rerankers. Todo el curso trató de afinar modelos generativos, pero si operas un RAG, el fine-tuning de mayor retorno por hora invertida no es el del LLM: es el del modelo de recuperación. Vamos a ver por qué un embedding genérico falla con la jerga de tu dominio, cómo se construye el dataset de pares (query, documento) y por qué la minería de negativos difíciles decide el resultado, las funciones de pérdida de sentence-transformers v3, el entrenamiento de un cross-encoder reranker —que suele rendir más que el bi-encoder— y cómo medir la ganancia con recall@k, MRR y nDCG.
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.