De los límites de Colab a DDP en dos GPUs: Fine-Tuning de Qwen3-8B en Kaggle
QWEN3 // QLORA // DISTRIBUTED TRAINING
La escena es un clásico del entrenamiento de modelos: preparas todo para dejar corriendo el fine-tuning antes de irte a dormir. Checkpoints configurados para guardarse automáticamente en Google Drive, dataset cargado, notebook limpio en Google Colab. A la mañana siguiente, con la ansiedad de querer probar el nuevo modelo para controlar mi casa, abro el navegador para ver los avances y me encuentro con la pantalla de la muerte: la sesión se había desconectado.
Al intentar reconectar la Tesla T4 de 16 GB, saltó el mensaje de límite de uso alcanzado, exigiendo esperar un tiempo indeterminado sin dar una sola métrica de cuándo se restablecería la cuota. Para ser totalmente honestos, el consumo de cuota no fue culpa exclusiva de esta prueba con Qwen3-8B. Antes de esto, ya habíamos logrado afinar un modelo Qwen2.5-Coder-7B-Instruct completo en Colab. El problema fue que, tras evaluarlo, los resultados no fueron los esperados: presentaba alucinaciones e incumplimientos de esquema al generar comandos de Home Assistant. Decidimos junto a Tuxbot no parchear un modelo defectuoso, sino empezar de cero seleccionando una nueva base. El límite de Colab simplemente llegó tras varias sesiones acumuladas de trabajo duro. No se trata de desanimar a nadie a usar Colab ni de criticar a Google —es una herramienta gratuita excelente—, sino de entender cómo operar cuando los límites inevitables de las capas gratuitas te cortan el ritmo.
Sin intención de quedarme de brazos cruzados, empecé a buscar alternativas inmediatamente. La prioridad era clara: aprovechar cada recurso gratuito disponible en la red antes de pasar a opciones de pago como Hugging Face Jobs. Así fue como desembarqué en Kaggle.
Este no es un tutorial comercial de “entrena gratis en 5 minutos”. Es la bitácora real de debugging, decisiones de arquitectura e infraestructura distribuida para migrar el entrenamiento de mi modelo Home Assistant Specialist v0.2 a dos GPUs en Kaggle, trabajando mano a mano con mi agente personal Tuxbot.
El error de la Tesla P100: VRAM correcta, arquitectura incompatible
La primera tentación en Kaggle fue seleccionar el entorno con una GPU Tesla P100. Sobre el papel, ofrecía 16 GB de VRAM, suficiente para meter a unsloth/Qwen3-8B-unsloth-bnb-4bit con QLoRA.
Sin embargo, al inicializar el runtime, el diagnóstico arrojó una realidad incómoda:
Python: 3.12.13
Torch: 2.10.0+cu128
CUDA: 12.8
GPU: Tesla P100-PCIE-16GB
Compute capability: 6.0 (sm_60)
El problema no era la falta de memoria. Era incompatibilidad directa de arquitectura CUDA. El build de PyTorch preinstalado en Kaggle exigía capacidades de cómputo sm_70 a sm_120. La P100, al ser una arquitectura Pascal (sm_60), disparaba advertencias críticas de que la GPU no era soportada por la versión de PyTorch activa. Tener 16 GB de VRAM inservibles por falta de soporte en el runtime es una lección rápida sobre por qué hay que inspeccionar la infraestructura antes de lanzar un script.
La trampa de una sola T4 y el mito de device_map="balanced"
Descartada la P100, cambiamos al acelerador T4x2 de Kaggle, que ofrece dos GPUs Tesla T4 de 16 GB cada una (14.56 GB de VRAM visible y compute capability 7.5).
Probar con una sola T4 confirmó la lentitud esperada:
3 pasos (smoke test single-GPU):
train_runtime: 431.37 s
train_steps_per_second: 0.007
Tras la fase de calentamiento, la velocidad apenas subió a 0.015 steps/s. Para completar 1 epoch sobre el dataset tuxevil/Home-Assistant-Requests-V3 (3,561 ejemplos de entrenamiento), la proyección era de unas 8 a 9 horas continuas.
Surgió entonces la consulta sobre usar device_map="balanced". Aquí es donde hay que romper un mito: Model Parallelism (partir el modelo entre GPUs) no es una optimización de velocidad. Como explican los docs de Hugging Face sobre paralelismo, dividir las capas de Qwen3-8B entre las dos T4 solo agrega latencia de comunicación secuencial sobre el bus PCIe de la máquina virtual; una GPU pasa tiempo muerta esperando que la otra termine de procesar su fragmento del modelo.
Como Qwen3-8B en 4-bit con adapters LoRA cabe holgadamente en la VRAM de una sola T4, la estrategia correcta no era repartir el modelo, sino usar Distributed Data Parallel (DDP) apoyado en las guías de Unsloth DDP Documentation: tener una copia completa del modelo cargada en cada GPU procesando batches distintos en paralelo y sincronizando únicamente los gradientes.
Convirtiendo el notebook a script DDP con torchrun
torchrun (el launcher distribuido de PyTorch Distributed) no puede ejecutarse directamente sobre las celdas de un Jupyter Notebook interactivo. Para lanzar 2 procesos independientes, Tuxbot estructuró un script standalone: train_ddp.py.
Los puntos críticos del script para sobrevivir al entorno de Kaggle incluyeron:
- Aislamiento por proceso: Leer
LOCAL_RANKdesde las variables de entorno para que cada proceso tome su GPU asignada sin forzarCUDA_VISIBLE_DEVICES. - Control de IO: Asegurar que únicamente el
rank 0guarde checkpoints locales y sincronice los adapters con el Hub de Hugging Face. - Manejo de memoria y precisión: Forzar
FP16=True,BF16=False(las T4 no tienen soporte nativo bfloat16 eficiente), y establecerddp_find_unused_parameters=Falsepara evitar sobrecoste en el grafo de gradientes. - Cierre limpio de NCCL: Añadir llamadas explícitas a
torch.distributed.barrier()ytorch.distributed.destroy_process_group()al finalizar.
El comando de lanzamiento dentro del notebook fue:
torchrun --standalone --nproc_per_node=2 train_ddp.py
Smoke Test y depuración de warnings
El smoke test de DDP confirmó que la distribución estaba activa y funcional:
Rank 0: local_rank 0, Tesla T4
Rank 1: local_rank 1, Tesla T4
Data Parallel GPUs: 2
Batch por GPU: 1
Gradient accumulation steps: 8
Global batch size: 16
Trainable parameters: 43,646,976 de 5,235,454,976
En la prueba de 3 pasos DDP, el tiempo de ejecución cayó drásticamente:
global_step: 3
train_runtime: 57.41 s
train_samples_per_second: 0.836
train_steps_per_second: 0.052
Saltar de 0.015 a 0.052 steps/s demostró la ventaja del paralelismo de datos. Durante el proceso corregimos advertencias como use_return_dict is deprecated forzando model.config.return_dict = True, e ignoramos warnings inofensivos de comunicación interna de Kaggle (hostname of client socket cannot be retrieved).
Unsloth reportó correctamente: Num GPUs used = 1, Data Parallel GPUs = 2, reflejando que cada uno de los dos procesos manejaba 1 GPU local mientras se coordinaban vía NCCL.
La realidad física: Nube gratuita vs. Energía Solar local
Mucha gente se pregunta por qué no hacer todo el fine-tuning localmente. La respuesta no es solo de software o VRAM, sino de infraestructura física y energética en mi casa.
Entrenar un modelo localmente sobre mi GPU a pleno rendimiento añade unos 125W de consumo constante. Con mi sistema de respaldo de baterías, ese consumo nocturno no es sostenible sin arriesgar la continuidad del resto de los nodos de mi homelab en Proxmox. Mi ventana de producción fotovoltaica con sobrante energético real ocurre en el día, entre las 8:00 y las 17:00.
Por eso, mi enfoque como curioso y explorador es pragmático:
- Nube gratuita (Kaggle / Colab): Primera opción para procesar tareas pesadas de varias horas sin tocar mi banco de baterías ni pagar consumo eléctrico extra.
- Entrenamiento local: Reserva estratégica para cuando las capas gratuitas fallen o no existan, ejecutado estrictamente dentro de la ventana fotovoltaica.
Estado actual y próximos pasos
El entrenamiento completo de Home Assistant Specialist v0.2 sobre el contrato ha-action-v3 fue lanzado desde la celda 11 de Kaggle y continúa corriendo en segundo plano. Siguiendo el protocolo de no inventar métricas, los datos finales de loss, tiempo total y evaluación se analizarán una vez que el trabajo termine por completo.
Una vez obtenido el adapter:
- Cuantizaremos el modelo resultante a formato GGUF con llama.cpp.
- Lo desplegaremos en Ollama sobre la RTX 4000 local.
- Tuxbot ejecutará un benchmark directo contra nuestro baseline actual (
qwen2.5-coder-baseline) para verificar si las alucinaciones de esquemas en Home Assistant disminuyeron.
El objetivo final sigue siendo el mismo: construir inteligencia artificial local, altamente especializada y resiliente, aprovechando al máximo cada recurso disponible en el camino.