Saltar al contenido

Dos GPU, DataParallel y por qué todavía no hay broker

Por Marco A. Parra, 11 de noviembre de 2026 · 6 min de lectura · Read this article in English

Segunda parte de la plataforma de visión con YOLO: por qué dos GPU no son dos carriles, por qué DataParallel y no DDP, por qué un turno por GPU antes que Celery, cómo el servicio de IA pasó de script a contenedor, y cuatro decisiones que hoy tomaría distinto.

architecturecomputer-visiondjangofastapimlopsyolo

Proyecto relacionado: YOLO Training & Inference Orchestration Architecture

YOLO Training & Inference Orchestration Architecture
YOLO Training & Inference Orchestration Architecture

Este artículo es la segunda parte de Sin cola hasta que haga falta, sobre una plataforma interna de visión por computador para entrenar e inferir con YOLO. La primera parte contó la frontera entre la web y la GPU y cómo la plataforma creció por evidencia. Esta baja un piso: lo que pasa cerca de la GPU, cómo el servicio de IA llegó a ser un servicio, y algunas decisiones que hoy tomaría distinto. Como el case study de la arquitectura, no nombra organización, campo de aplicación ni resultados medidos, y no incluye código.

Dos GPU no son dos carriles

La plataforma corría en una sola máquina con dos GPU NVIDIA. Es fácil leer eso como capacidad para dos trabajos a la vez, pero no fue así, y entender por qué ordena todo lo demás.

Para entrenar en las dos GPU hay dos caminos en PyTorch. DistributedDataParallel (DDP) es el que escala mejor: lanza un proceso por GPU, cada uno con su copia del modelo, y sincroniza los gradientes entre ellos. Lo intenté y no logré dejarlo estable. DDP necesita un lanzador y que los procesos se encuentren entre sí, y esa forma de trabajar encaja mal dentro de un servicio web, que tiene su propio modelo de procesos. DataParallel es más modesto: un solo proceso reparte cada lote entre las dos GPU y junta los resultados. Escala menos, porque una GPU hace de coordinadora, pero entrenó en las dos sin tocar la arquitectura del servicio.

La consecuencia es que, mientras se entrenaba, las dos GPU eran un solo carril. La inferencia, por su parte, corría en una GPU elegida. Una cola delante de ese carril no habría agregado capacidad: solo habría ordenado la espera.

Flujo de una petición de entrenamiento: Django envía la petición, FastAPI la despacha, la GPU entrena varias semillas y se elige el mejor modelo por validación; la petición HTTP queda abierta durante todo el entrenamiento.
El flujo de entrenamiento de la primera versión: un solo trabajo ocupa la GPU y la petición queda abierta hasta que termina.

Admisión antes que broker

El paso natural es repartir la inferencia entre las dos GPU. Ahí sí hay dos carriles, y aparece un problema nuevo: que dos trabajos no caigan en la misma GPU y se peleen su memoria. Eso se llama control de admisión, y en una sola máquina se resuelve con algo tan simple como un turno por GPU: un semáforo que un trabajo toma antes de tocar el dispositivo y suelta al terminar.

Celery no resuelve esto por sí solo. La forma habitual de usarlo con GPU es un worker por GPU, fijado con CUDA_VISIBLE_DEVICES y con concurrencia uno: los mismos dos carriles, pero fuera del proceso del servicio. Lo que un broker sí compra es otra cosa: que reiniciar la API no mate un trabajo en curso, reintentos, cancelación y la posibilidad de sumar una segunda máquina. Mientras ninguna de esas necesidades sea real, el semáforo alcanza.

Aquí corrijo mi propia hoja de ruta. La documentación de la revisión decía que una segunda GPU sería motivo para un broker. No lo es: una segunda GPU en la misma máquina es un segundo carril de admisión. El broker se justifica con una segunda máquina o con la necesidad de reintentar y cancelar. La revisión dejó la admisión sin controlar y lo escribió como el riesgo abierto del diseño: es lo siguiente que falta.

De script a servicio

El servicio de IA no nació como servicio. Empezó como scripts de Python que se ejecutaban desde la terminal. Cuando la web necesitó pedirle trabajo, lo envolví en FastAPI, todavía en el host, y la aplicación Django, que corría en su contenedor Docker, lo llamaba por la red.

Dejar el servicio de GPU en el host fue deliberado: así usaba directamente el driver NVIDIA, CUDA y PyTorch, sin pelear con el acceso a la GPU desde un contenedor. El costo llegó con el tiempo, cuando el host y los contenedores se fueron desalineando en versiones. En la versión siguiente quedó todo en un proyecto y un Docker Compose: la web y el servicio de GPU, cada uno en su contenedor, con el acceso a la GPU declarado explícitamente y las dos imágenes construidas y probadas en integración continua. Esas pruebas corren sin GPU, con un runtime simulado, y la documentación lo dice: el camino real por la GPU no se valida en CI.

En esa misma versión, el entrenamiento salió de la plataforma. Su entorno necesitaba versiones más nuevas que las de la inferencia, y mantenerlos juntos habría frenado cada entrega de inferencia por una actualización de entrenamiento. Ahora el entrenamiento ocurre aparte y los pesos entran por un paso de importación que les toma una huella.

Linux, drivers y por qué no fue un capricho

En una plataforma con GPU, lo más frágil no es el código: es que calcen el driver NVIDIA del host, la versión de CUDA del contenedor y la de PyTorch. Ubuntu fue la base con menos fricción para esa combinación. En Windows lo simple funcionaba, pero el entrenamiento en varias GPU chocaba con el manejo de procesos: allá Python crea procesos de otra forma y PyTorch no ofrece NCCL, el backend de comunicación entre GPU que usa el entrenamiento distribuido en Linux.

Nube o local: dos preguntas, no una

La pregunta por la nube suele contestarse con «es cara» o «es más simple». El criterio que resiste es otro: cuántas horas de GPU se usan al mes y dónde nacen los datos. Entrenamientos largos sobre conjuntos grandes de imágenes que ya están en local pagan GPU por hora y, además, la subida de los datos. Por eso el análisis apuntó a un modelo híbrido: la GPU cerca de los datos y, en la nube, la aplicación y los modelos elegidos. Lo que le falta a ese análisis, y lo digo, es el punto de equilibrio en números: sin él es una dirección, no una conclusión.

Comparación de tres opciones de despliegue: GPU local, GPU en la nube e híbrido, con sus fortalezas, limitaciones y qué artefactos cruzan la frontera entre lo local y la nube.
Dónde entrenar y dónde servir son dos decisiones separadas.

Lo que hoy haría distinto

Separar envío y ejecución desde el primer día. En la primera versión, la petición HTTP quedaba abierta durante todo el trabajo. Enviar, recibir un identificador y consultar el estado cuesta una tabla y un endpoint, y evita el problema entero. La versión siguiente lo hizo, con registros de trabajo que sobreviven a un reinicio.

Elegir FastAPI por los contratos, no por el async. El código que entrena o infiere en la GPU bloquea igual. Un endpoint async que llama a ese código sin delegarlo a un hilo congela el bucle de eventos, incluido el chequeo de salud. Lo que FastAPI sí aporta entre dos servicios son los contratos tipados y la documentación automática de la API.

Nombrar bien la razón de separar. Separar la web de la GPU no fue para escalar cada parte por su lado: en una sola máquina eso es teoría. Las razones reales fueron que un entrenamiento que se cae no bote el sitio y que el entorno de CUDA y PyTorch tenga su propio ritmo de actualización. Esa segunda razón es la misma que después sacó el entrenamiento de la plataforma.

Poner la admisión antes de habilitar un segundo tipo de trabajo en la GPU. Es más barato que el incidente de memoria que la haría necesaria.

Diseñé y documenté esta arquitectura. Las decisiones (ADR), los límites, la hoja de ruta y la revisión posterior están en el repositorio del case study.

Comentarios

Los comentarios viven en GitHub Discussions: inicia sesión con GitHub en el cuadro de abajo para responder, o abre el hilo en GitHub.

¿Sin cuenta de GitHub? Deja tu comentario aquí

Los comentarios se revisan antes de publicarse. Tu correo es opcional y nunca se publica.