Curso / HF LLM Course / Capítulo 8
HF LLM Course · 8 de 12

Hugging Face · LLM Course · Capítulo 8

Cómo pedir ayuda (y cómo no necesitarla)

El capítulo más corto y menos técnico del curso — y el más transferible. Nada de esto es específico de transformers: es la disciplina básica de debuggear y reportar bugs en cualquier proyecto de código abierto.

Antes de leercap. 3 §4, leer curvas de entrenamiento · PyTorch 3 §4, el training loop

🎯 Al terminar vas a poder

  • Leer un traceback de Python de abajo hacia arriba y decir, en una frase, cuál es la línea tuya que disparó el error y cuál es la causa real.
  • Escribir un ejemplo mínimo reproducible y generar la información de entorno con transformers env antes de pedir ayuda.
  • Verificar en cinco líneas que tu data collator produce los shapes, dtypes y rangos de etiquetas que el modelo espera.
  • Sobreajustar un solo batch a propósito y usar el resultado para decidir si el problema está en los datos, el modelo o los hiperparámetros.
  • Recorrer un checklist ordenado frente a CUDA out of memory en vez de bajar el batch size a ciegas.

1. Leer un error de abajo hacia arriba

Un traceback de Python se lee al revés de como uno lo mira instintivamente: la línea de más abajo es la causa real, todo lo de arriba es la cadena de llamadas que llevó hasta ahí. El error más común al arrancar con el Hub — un identificador de modelo mal escrito, o un archivo config.json faltante en el repo — se diagnostica en segundos si vas directo a la última línea en vez de leer todo de arriba para abajo.

Un ejemplo real, del tipo que el curso usa: el nombre del checkpoint tiene una letra de más ("distillbert" con dos eles) y el pipeline explota. Esto es lo que ves:

Traceback (most recent call last):
  File "demo.py", line 4, in <module>
    reader = pipeline("question-answering", model=model_checkpoint)
  File ".../site-packages/transformers/pipelines/__init__.py", line ..., in pipeline
    config = AutoConfig.from_pretrained(model, _from_pipeline=task, **hub_kwargs, **model_kwargs)
  File ".../site-packages/transformers/models/auto/configuration_auto.py", line ..., in from_pretrained
    config_dict, unused_kwargs = PretrainedConfig.get_config_dict(pretrained_model_name_or_path, **kwargs)
  File ".../site-packages/transformers/configuration_utils.py", line ..., in get_config_dict
    config_dict, kwargs = cls._get_config_dict(pretrained_model_name_or_path, **kwargs)
  File ".../site-packages/transformers/configuration_utils.py", line ..., in _get_config_dict
    resolved_config_file = cached_file(...)
  File ".../site-packages/transformers/utils/hub.py", line ..., in cached_file
    raise EnvironmentError(
OSError: lewtun/distillbert-base-uncased-finetuned-squad-d5716d28 is not a local folder and is not a valid model identifier listed on 'https://huggingface.co/models'
If this is a private repository, make sure to pass a token having permission to this repo either by logging in with `huggingface-cli login` or by passing `token=<your_token>`

Leído de abajo hacia arriba, línea por línea:

  1. Última línea (el mensaje): OSError: ... is not a local folder and is not a valid model identifier. Ya está el diagnóstico completo: lo que le pasaste no es ni una carpeta local ni un repo del Hub. La frase sobre repos privados es la segunda hipótesis que la librería te ofrece — no la ignores si el repo es tuyo y no hiciste login.
  2. Justo arriba (raise EnvironmentError( en utils/hub.py): es el lugar donde transformers decidió rendirse. No es tu código, es la librería explicándote que no encontró el archivo. Todo lo que está en site-packages es "de ellos".
  3. Las cuatro líneas siguientes hacia arriba (configuration_utils.py, configuration_auto.py, pipelines/__init__.py): la cadena interna. Te dicen qué estaba buscando: el config.json. Útil para confirmar que el problema es "no existe el repo" y no "existe pero le falta un archivo" — si fuera esto último, el mensaje diría que falta config.json, no que el identificador es inválido.
  4. La primera línea con un archivo tuyo (demo.py, línea 4): la línea de tu código que disparó todo. Es el único lugar donde vas a tocar algo. Acá miramos el valor de model_checkpoint y aparece la doble ele.

La regla que sale de esto: el mensaje final te dice qué falló; el primer archivo tuyo (contando desde abajo) te dice dónde lo provocaste; todo lo del medio te dice por qué camino, y casi nunca hace falta leerlo entero — salvo cuando el error viene de una librería que llama a otra y el mensaje final es genérico (un KeyError: 'labels', por ejemplo, no dice nada sin ver quién pidió esa clave).

Antes de asumir que el bug es tuyo: huggingface_hub.list_repo_files() te dice qué archivos tiene realmente un repo, y pegar el mensaje de error tal cual en un buscador casi siempre te lleva a alguien que ya lo resolvió — la mayoría de los errores que vas a ver ya los vio otra persona antes.

from huggingface_hub import list_repo_files
print(list_repo_files("lewtun/distilbert-base-uncased-finetuned-squad-d5716d28"))
→ ['.gitattributes', 'README.md', 'config.json', 'pytorch_model.bin', 'special_tokens_map.json', 'tokenizer.json', 'tokenizer_config.json', 'training_args.bin', 'vocab.txt']

Si en esa lista no aparece config.json, el error que vas a ver es otro (la librería lo dice explícitamente), y la solución es que el dueño del repo lo suba — o que lo cargues vos con AutoConfig.from_pretrained("distilbert-base-uncased") y se lo pases al modelo a mano, como hace el curso.

2. Pedir ayuda bien

Un buen post de foro (o de cualquier canal de soporte) tiene cuatro ingredientes, siempre en este orden de importancia:

Título descriptivo

"Error de shape en la capa 12 al hacer fine-tuning de BERT" — no "ayuda porfa" o "no funciona".

Traceback completo

El mensaje final sin el resto del stack a veces oculta justo el dato que hace obvia la causa.

Código formateado

Bloques de código, no una captura de pantalla ni texto pegado sin indentación.

Ejemplo mínimo reproducible

El fragmento más chico posible que dispara el bug — sin tu dataset de 50GB ni tu pipeline completo alrededor.

El último punto es, con diferencia, el que más tiempo ahorra — a vos y a quien te ayuda. Reducir el problema a un ejemplo mínimo casi siempre te hace encontrar la causa antes incluso de terminar de escribir la pregunta.

Hay un quinto ingrediente que el curso pide en el capítulo de issues pero que conviene incluir siempre: el entorno. transformers trae un comando que lo imprime listo para pegar:

transformers env      # en versiones anteriores: transformers-cli env
→ - `transformers` version: 4.x.y
- Platform: Linux-6.x-x86_64-with-glibc2.39
- Python version: 3.12.x
- Huggingface_hub version: 0.xx
- Safetensors version: 0.x
- Accelerate version: 1.x
- PyTorch version (GPU?): 2.x (True)
- Using GPU in script?: <fill in>
- Using distributed or parallel set-up in script?: <fill in>

Los dos campos con <fill in> los completás vos. El resto es exactamente lo que alguien necesita para reproducir tu problema — y con frecuencia, lo que vos necesitás para darte cuenta de que el notebook donde funciona tiene una versión de transformers y el servidor donde falla tiene otra. Esa diferencia de versiones explica una fracción enorme de los "funciona acá pero no allá".

⚠️ Cuidado

Una captura de pantalla del error no se puede buscar, no se puede copiar, y en el celular de quien te responde no se lee. Texto siempre: el traceback completo dentro de un bloque de código. Lo mismo para el código que lo dispara — si alguien tiene que retipear tu ejemplo para probarlo, no lo va a probar.

3. Debuggear un entrenamiento que no crashea

Los bugs más peligrosos no son los que tiran una excepción — son los que entrenan sin quejarse y producen un modelo inútil. Para el caso con excepción, la receta es sistemática: revisar los datos, después cómo se arman los batches (el data_collator correcto, Capítulo 3), después el forward del modelo, después el paso del optimizador, y recién ahí sospechar de la config o el hardware (incluido el clásico CUDA out of memory, que tiene su propio checklist en la sección 4).

Paso 1: mirar lo que sale del collator

El collator es el punto donde "mi dataset" se convierte en "lo que el modelo recibe", y es el lugar donde más bugs silenciosos se esconden. Antes de entrenar, armá un batch a mano e imprimí shapes y dtypes — cinco líneas que ahorran horas:

batch = data_collator([tokenized["train"][i] for i in range(4)])
print({k: (tuple(v.shape), v.dtype) for k, v in batch.items()})
→ {'input_ids': ((4, 67), torch.int64), 'attention_mask': ((4, 67), torch.int64), 'labels': ((4,), torch.int64)}

Qué mirar en esa salida:

  • Que estén las claves que el modelo espera. Si el collator falla con un error críptico, casi siempre es porque el dataset todavía tiene columnas de texto ("sentence", "idx") que no se pueden convertir a tensor — Trainer las saca solo con remove_unused_columns=True, pero si armás el batch a mano, sacalas vos con remove_columns en el map.
  • Que labels tenga el shape de la tarea: (4,) para clasificación de secuencias, (4, 67) para clasificación de tokens o LM. Un (4, 67) donde esperabas (4,) significa que tokenizaste la columna equivocada.
  • Que los dtypes sean enteros para ids y labels (int64), y flotantes solo para cosas como pesos o pixel_values. Un float32 en labels rompe la cross-entropy con un error que no menciona el dtype.

Y un assert de rangos, porque una etiqueta fuera de rango en GPU no dice "etiqueta fuera de rango" — dice CUDA error: device-side assert triggered, que no te orienta nada:

labels = batch["labels"]
validos = labels[labels != -100]
assert validos.min() >= 0 and validos.max() < model.config.num_labels, \
    f"labels en [{validos.min()}, {validos.max()}] pero num_labels={model.config.num_labels}"

Este es exactamente el bug del ejemplo del curso: un dataset con tres clases (MNLI) y un modelo cargado con el num_labels=2 por default. En CPU el error es legible (IndexError: Target 2 is out of bounds); en GPU es el assert de CUDA. Por eso vale la pena correr un forward de prueba en CPU antes de mandar todo a la GPU.

Paso 2: sobreajustar un solo batch

Para el caso silencioso — entrena, la loss baja, pero el modelo no sirve — dos técnicas de sanity check:

  • Sobreajustar deliberadamente en un solo batch: si el modelo no puede llegar a accuracy ≈100% memorizando 8 ejemplos, el problema está en los datos o en cómo está planteado el modelo — no tiene sentido seguir entrenando en el dataset completo hasta arreglar esto.
  • No tocar ningún hiperparámetro hasta tener un primer baseline funcionando: ajustar learning rate o arquitectura antes de confirmar que el pipeline básico aprende algo es optimizar en la dirección equivocada.

El primero, en código — el mismo batch, una y otra vez, hasta que la loss se vaya a cero:

import torch
from transformers import set_seed

set_seed(42)
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device).train()
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)

batch = data_collator([tokenized["train"][i] for i in range(8)])
batch = {k: v.to(device) for k, v in batch.items()}

for step in range(30):
    out = model(**batch)
    out.loss.backward()
    optimizer.step()
    optimizer.zero_grad()
    if step % 5 == 0:
        print(step, round(out.loss.item(), 4))

model.eval()
with torch.no_grad():
    preds = model(**batch).logits.argmax(-1)
print("accuracy sobre el mismo batch:", (preds == batch["labels"]).float().mean().item())
→ 0 1.1023
5 0.4871
10 0.0912
15 0.0138
20 0.0031
25 0.0011
accuracy sobre el mismo batch: 1.0

Qué observar, y qué significa cada desvío:

  • La loss baja monótonamente a ≈0 y la accuracy llega a 1.0: el pipeline es capaz de aprender. Si después el entrenamiento completo no mejora, el problema es de generalización o de hiperparámetros, no de plomería.
  • La loss se queda clavada (por ejemplo en ln(num_labels), que es 1.10 para 3 clases o 0.69 para 2): el modelo no recibe señal. Labels todas iguales, labels desalineadas con los inputs, learning rate cero, o los gradientes no llegan (¿congelaste todo por error? ¿hay un torch.no_grad() de más?).
  • La loss baja y después sube o da nan: learning rate demasiado alto para 8 ejemplos, o un problema numérico (fp16 sin escalado de gradientes).
  • La loss baja pero la accuracy no llega a 1.0: mirá si hay ejemplos idénticos con etiquetas distintas en el batch — datos contradictorios que ningún modelo puede memorizar.

El set_seed(42) no es decorativo: fija las semillas de Python, NumPy y PyTorch (CPU y CUDA) de una vez. Sin él, dos corridas del mismo snippet dan curvas distintas y no sabés si "mejoró" porque cambiaste algo o porque el azar cayó distinto.

🔧 La misma disciplina, en tus propios proyectos

"Sobreajustar en un batch para confirmar que el mecanismo funciona antes de confiar en él a escala" es exactamente el espíritu de scripts/model_acceptance.py en tu gate anti-alucinación: antes de confiar un modelo nuevo a producción, lo hacés fallar contra un caso que sabés que tiene que fallar (el incidente Micron) y pasar contra dos que sabés que tienen que pasar. Mismo principio — validar contra un caso conocido antes de confiar en el comportamiento general.

4. CUDA out of memory: el checklist

El error de hardware más frecuente merece más que "bajá el batch size". Es un problema de presupuesto: pesos + gradientes + estados del optimizador + activaciones tienen que entrar en la VRAM, y cada uno de esos términos tiene su propia perilla. Recorrelo en este orden:

PasoQué tocarQué cambia y qué cuesta
0. Medirnvidia-smi (o watch -n 1 nvidia-smi) antes de arrancarConfirmá que la GPU está vacía: un proceso zombie de un notebook anterior ocupando 20 GB es la causa más tonta y más común. Matalo antes de tocar el script.
1. Activacionesper_device_train_batch_size ↓ y gradient_accumulation_steps ↑ en la misma proporciónLas activaciones escalan con el batch; los pesos y el optimizador no. Batch 32 → 8 con acumulación 4 da el mismo batch efectivo (32) con un cuarto de las activaciones en memoria. Gratis en calidad; algo más lento.
2. Longitudmax_length del tokenizerLas activaciones de atención escalan con la longitud de secuencia (y sin FlashAttention, con su cuadrado). Si el 95 % de tus ejemplos tiene menos de 256 tokens, no pagues por 2048. Mirá la distribución real de longitudes antes de elegir.
3. Precisiónbf16=True (o fp16=True en GPUs que no soportan bf16)Activaciones a la mitad de bytes. En una GPU moderna (Ampere en adelante) bf16 es la opción segura: mismo rango que fp32, sin escalado de gradientes.
4. Recomputargradient_checkpointing=TrueNo guarda las activaciones intermedias; las recalcula en el backward. Baja mucho la memoria de activaciones a cambio de ~20–30 % más de tiempo por paso. Es el paso que hace entrar un modelo que "casi" entra.
5. Pesos y optimizadorLoRA (Capítulo 11), optimizador de 8 bits, o cargar el modelo base cuantizadoRecién acá tocás los términos que no dependen del batch. Adam guarda dos estados por parámetro; con LoRA solo los guarda para los adapters, que son una fracción ínfima.

📐 Física del PyTorch CUDA Caching Allocator y fragmentación de memoria

Llamar a cudaMalloc a nivel de driver de NVIDIA es una operación síncrona sumamente costosa que detiene el flujo de ejecución de la GPU. Por esta razón, PyTorch implementa un Caching Allocator: solicita al sistema operativo grandes segmentos contiguos (típicamente bloques de 2 MB a varios GB) y gestiona internamente la partición de esos bloques para cada tensor mediante un algoritmo de split & coalesce.

El presupuesto de memoria en GPU responde a la siguiente ecuación de estados:

MGPU=Malloc+Mcached_free+Mos_driver

Donde:

  • Malloc=torch.cuda.memory_allocated(): Memoria activa ocupada por tensores vivos en Python.
  • Mreserved=torch.cuda.memory_reserved(): Memoria total solicitada al driver (Malloc+Mcached_free).
  • Fragmentación: Ocurre cuando Mcached_free tiene espacio total suficiente (ej. 4 GB libres acumulados), pero está dividido en pequeños fragmentos no contiguos. Al solicitar un tensor de 1.5 GB, ningún bloque individual alcanza, forzando a PyTorch a solicitar un nuevo segmento a CUDA. Si la VRAM física está llena, se dispara un OutOfMemoryError a pesar de existir memoria libre aparente.

Herramienta de diagnóstico: Para capturar exactamente qué tensor detonó el OOM y visualizar el mapa de fragmentación:

import torch
torch.cuda.memory._record_memory_history(max_entries=100000)
try:
    # loop de entrenamiento
    train_step()
except torch.cuda.OutOfMemoryError:
    torch.cuda.memory._dump_snapshot("oom_snapshot.pickle")
    # Subir oom_snapshot.pickle a https://pytorch.org/memory_viz

⚠️ torch.cuda.empty_cache() no es una solución

Libera bloques que el allocator de PyTorch tiene reservados pero no está usando — no libera nada que un tensor vivo esté ocupando. Si te quedaste sin memoria en medio del forward, llamarlo no cambia el presupuesto: la próxima iteración va a necesitar exactamente lo mismo. Sirve para devolverle memoria a otro proceso (o para que nvidia-smi muestre un número más honesto), no para que tu entrenamiento entre. Si lo ves en un loop de entrenamiento, es un síntoma de que alguien no entendió el problema — y casi siempre está tapando una referencia viva a un tensor que debería haberse liberado (una lista donde se acumulan losses con gráfico de gradientes, por ejemplo: usá loss.item()).

🔧 En tu RTX 5090

Con 24 GB de VRAM, el orden del checklist es el mismo pero el punto 5 llega antes: un modelo de 31B no entra ni en bf16 (62 GB solo de pesos), así que tu pipeline NVFP4 y el LoRA de interpretante-lacaniano no son optimizaciones, son la condición de posibilidad. Los pasos 1 a 4 son lo que ajustás después de que el modelo entra, para exprimir el batch efectivo.

5. Cómo escribir un buen issue

Si el bug es de la librería y no tuyo, un buen issue en GitHub agrega tres cosas al ejemplo mínimo reproducible de la sección 2: la información de entorno (versión de la librería, de Python, de CUDA — generada automáticamente con transformers env), una descripción clara de qué esperabas que pasara versus qué pasó realmente, y etiquetar a la persona correcta sin spamear a todo el equipo.

Antes de abrir el issue, tres verificaciones que evitan la respuesta más frecuente de un mantenedor ("no puedo reproducirlo"):

  • Reproducilo con la última versión (pip install -U transformers en un entorno limpio). Una fracción grande de los bugs reportados ya están arreglados en main.
  • Fijá la semilla con set_seed() y confirmá que el bug aparece siempre, no una de cada tres corridas. Si es intermitente, decilo, y decí con qué frecuencia.
  • Buscá el issue tracker con el mensaje de error exacto. Si ya existe uno, sumá tu caso ahí (con tu transformers env) en vez de abrir un duplicado.

🔧 Un ejemplo real tuyo que sigue esta receta al pie de la letra

Los tres fixes que mandaste a vLLM son exactamente esto en la práctica: cada uno referencia un issue o número real, viene con un test que reproduce el bug sin depender de tener el hardware exacto a mano, y el commit describe con precisión la causa real, no solo el síntoma. La cadena de commits de Gemma-4 en el backend de Transformers, en particular, es un caso de libro de qué significa iterar bien con el mantenedor que revisa tu código en vez de a la defensiva.

6. Errores comunes

Errores sobre cómo debuggear, no sobre el código en sí — los que hacen que tardes tres días en algo de veinte minutos.

SíntomaCausa probableQué hacer
Leés el traceback entero, te perdés en site-packages y terminás googleando la primera línea.Se lee de arriba hacia abajo, como prosa. La primera línea es la menos informativa: solo dice desde dónde arrancó la llamada.Última línea primero (qué falló), después subir hasta el primer archivo tuyo (dónde). Lo del medio solo si el mensaje final es genérico. (§1)
Pediste ayuda con una captura de pantalla y nadie responde, o te piden "pegá el texto".Una imagen no se puede buscar, copiar ni leer en el celular; quien ayuda no va a retipear tu error.Traceback y código como texto, en bloques de código. Nombre del modelo, versión de la librería (transformers env) y el ejemplo mínimo. (§2)
"Cambié X y mejoró" — pero al correr de nuevo sin cambiar nada, también cambia.No fijaste seeds: inicialización del head, orden de los batches y dropout son aleatorios en cada corrida.set_seed(42) de transformers al principio del script (fija Python, NumPy, PyTorch CPU y CUDA). Comparar dos corridas con la misma seed antes de atribuir una mejora a un cambio. (§3)
Funciona en el notebook, falla en el servidor (o al revés) con el mismo código.Versiones distintas de transformers, torch, datasets o CUDA entre los dos entornos.Correr transformers env en ambos y comparar línea por línea. Fijar versiones exactas en un requirements.txt. (§2, §5)
CUDA error: device-side assert triggered al arrancar a entrenar en GPU.Casi siempre una etiqueta fuera de [0, num_labels); el mensaje de CUDA no lo dice.Correr un forward de un batch en CPU para ver el error real (IndexError: Target N is out of bounds) y el assert de rangos de §3.
La loss baja lindo en el dataset completo pero el modelo predice siempre la misma clase.Etiquetas desalineadas con los inputs (un shuffle aplicado a una columna y no a la otra, o un map que devolvió las labels en otro orden).Sobreajustar un batch (§3): si no llega a accuracy 1.0 memorizando 8 ejemplos, mirá los pares (input, label) uno por uno.
CUDA out of memory que aparece recién después de varios cientos de pasos.Fuga: un tensor con gráfico de gradientes acumulándose en una lista (losses.append(loss) sin .item()), o un batch inusualmente largo.loss.item() al acumular; revisar la distribución de longitudes y fijar max_length. empty_cache() no lo arregla. (§4)

7. Resumen

  1. Un traceback se lee de abajo hacia arriba — la causa real está en la última línea; el lugar donde lo provocaste es el primer archivo tuyo contando desde abajo.
  2. Pedir ayuda bien es, en el fondo, reducir el problema a su ejemplo mínimo — el paso que más rápido te acerca a la solución, incluso antes de que alguien te responda — y acompañarlo del entorno (transformers env), como texto, nunca como imagen.
  3. Un entrenamiento que no crashea puede seguir estando roto — imprimir shapes/dtypes del collator, un assert de rangos de labels, sobreajustar en un batch y no tunear antes de tener un baseline son los cuatro sanity checks que lo detectan temprano.
  4. CUDA out of memory es un problema de presupuesto con un orden: medir, activaciones (batch + acumulación), longitud, precisión, checkpointing, y recién después pesos/optimizador. empty_cache() no cambia el presupuesto.
  5. Un buen issue = ejemplo mínimo + entorno + comportamiento esperado vs. real, reproducido con seed fija y la última versión — la misma disciplina que ya aplicaste mandando fixes reales aguas arriba.

8. Ponete a prueba

Respondé antes de abrir cada respuesta. Si fallás una, volvé a la sección indicada.

1. En el traceback de §1 hay siete líneas File "...". ¿Cuál es la única que vas a editar, y cómo la identificás sin leer las demás?

La de demo.py, línea 4: es el primer archivo que no está en site-packages contando desde abajo. Todo lo que está en site-packages es código de la librería, que no vas a tocar; te sirve solo para entender qué estaba intentando hacer (buscar config.json). (§1)

2. Un compañero te manda un issue con el mensaje OSError: ... is not a valid model identifier y jura que el repo existe. ¿Qué dos cosas verificás antes de sospechar de la librería?

Que el nombre esté bien escrito (con list_repo_files(): si el repo no existe, esa llamada también falla) y que, si el repo es privado, esté logueado o pasando token= — la segunda mitad del mensaje de error lo dice explícitamente. (§1)

3. Sobreajustás un batch de 8 ejemplos de un problema de 3 clases y la loss se queda en 1.10 durante 30 pasos. ¿Qué te dice ese número exacto?

ln(3) ≈ 1.0986 es la loss de predecir uniforme entre 3 clases: el modelo no está recibiendo ninguna señal útil. No es "aprende lento", es "no aprende": labels todas iguales o desalineadas, learning rate cero, o gradientes que no llegan a los pesos. Ajustar hiperparámetros ahí es perder el tiempo. (§3)

4. Tenés per_device_train_batch_size=32 y te quedás sin memoria. Cambiás a 8 con gradient_accumulation_steps=4. ¿Qué se mantiene igual, qué baja y qué empeora?

El batch efectivo (32) y por lo tanto la dinámica del entrenamiento se mantienen. La memoria de activaciones baja a un cuarto. Empeora el tiempo por paso efectivo (cuatro forwards/backwards en vez de uno, con menos paralelismo). Pesos, gradientes y estados del optimizador no cambian: si el OOM viene de ahí, este paso no alcanza y hay que ir al punto 5 del checklist. (§4)

5. ¿Por qué torch.cuda.empty_cache() dentro del loop de entrenamiento no evita el OOM?

Porque solo devuelve al driver bloques que el allocator de PyTorch reservó y no está usando; no libera tensores vivos. El próximo paso necesita la misma memoria que el anterior, así que si no entraba, sigue sin entrar. Si "parece" ayudar, probablemente hay una referencia viva acumulándose (una loss con gráfico en una lista) que es el bug real. (§4)

6. En GPU ves CUDA error: device-side assert triggered en el primer paso. ¿Cuál es el movimiento más rápido para saber qué pasó?

Correr un forward del mismo batch en CPU: ahí el error es legible (IndexError: Target 2 is out of bounds) y apunta directo a una etiqueta fuera de [0, num_labels). El assert de rangos de §3 lo atrapa antes de llegar a la GPU. (§3)

9. Para practicar

✏️ Ejercicio 1 — Provocar el error y leerlo al revés

Generá el traceback de §1 vos mismo y anotá, sin mirar la sección, las tres cosas: qué falló, dónde lo provocaste, qué buscaba la librería. CPU alcanza; solo necesita red para consultar el Hub.

from transformers import pipeline
from huggingface_hub import list_repo_files

nombre = "distillbert-base-uncased"     # la doble ele es a propósito
try:
    pipeline("fill-mask", model=nombre)
except Exception as e:
    print(type(e).__name__, "→", str(e).splitlines()[0])

print(list_repo_files("distilbert-base-uncased"))   # el nombre correcto
Pista / qué esperar

Un OSError cuya primera línea dice que el identificador no es válido. Después corré el script sin el try y leé el traceback completo de abajo hacia arriba: la primera línea que no sea de site-packages es la del pipeline(...). La lista de archivos del repo correcto tiene que incluir config.json.

✏️ Ejercicio 2 — Inspeccionar el collator y romperlo a propósito

Con un modelo chico y un dataset inventado, imprimí shapes/dtypes, pasá el assert de rangos, y después metele una etiqueta fuera de rango para ver el error en CPU. Sin GPU.

import torch
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification, DataCollatorWithPadding

ckpt = "prajjwal1/bert-tiny"
tok = AutoTokenizer.from_pretrained(ckpt)
model = AutoModelForSequenceClassification.from_pretrained(ckpt, num_labels=2)

ds = Dataset.from_dict({"text": ["muy bueno", "horrible", "excelente", "malísimo"], "label": [1, 0, 1, 2]})
tokenized = ds.map(lambda ex: tok(ex["text"], truncation=True), batched=True, remove_columns=["text"])
collator = DataCollatorWithPadding(tok)

batch = collator([tokenized[i] for i in range(4)])
print({k: (tuple(v.shape), v.dtype) for k, v in batch.items()})

labels = batch["labels"]
validos = labels[labels != -100]
assert validos.max() < model.config.num_labels, f"label {validos.max().item()} pero num_labels={model.config.num_labels}"
print(model(**batch).loss)
Pista / qué esperar

El assert tiene que fallar con un mensaje claro (label 2 con num_labels=2). Comentalo y mirá el error que da el forward en CPU (IndexError: Target 2 is out of bounds). Después arreglá el dato (o cargá el modelo con num_labels=3) y confirmá que labels tiene shape (4,) e int64.

✏️ Ejercicio 3 — Sobreajustar un batch con y sin seed

Sobre el mismo bert-tiny del ejercicio anterior (con las etiquetas ya arregladas), corré el loop de §3 dos veces sin set_seed y dos veces con. Compará las losses del paso 10.

from transformers import set_seed

def sobreajustar(seed=None):
    if seed is not None:
        set_seed(seed)
    m = AutoModelForSequenceClassification.from_pretrained(ckpt, num_labels=2)
    opt = torch.optim.AdamW(m.parameters(), lr=1e-3)
    m.train()
    for step in range(40):
        out = m(**batch); out.loss.backward(); opt.step(); opt.zero_grad()
    return round(out.loss.item(), 5)

print("sin seed:", sobreajustar(), sobreajustar())
print("con seed:", sobreajustar(0), sobreajustar(0))
Pista / qué esperar

Sin seed, los dos números difieren (el head de clasificación se inicializa al azar y hay dropout). Con seed, coinciden a todos los decimales. En ambos casos la loss final tiene que ser cercana a cero: 4 ejemplos son triviales de memorizar. Si no baja, el bug está en el batch, no en el modelo.

10. Fuentes y para seguir