Cómo acelerar la inferencia de LLM: batching asíncrono para eliminar tiempos muertos
El batching asíncrono rompe el patrón donde CPU y GPU se turnan y desperdician ciclos. En lugar de esperar a que termine una etapa para empezar la siguiente, se preparan batches en la CPU mientras la GPU está ocupada, reduciendo tiempos muertos y aumentando throughput.
TL;DR
Separar la preparación de batches en CPU del cómputo en GPU —es decir, usar batching asíncrono— permite mantener la GPU ocupada casi todo el tiempo y borrar importantes tiempos muertos. En un experimento con un modelo de 8B y batch de 32, la ejecución total fue de 300.6 s y el GPU estuvo inactivo un 24% esperando a la CPU; en teoría eso significa hasta ~24% de mejora si se eliminan esas pausas. La técnica requiere coordinar streams y sincronizaciones de CUDA, sin cambios en kernels o modelos.
El problema: batching síncrono y tiempos muertos
En implementaciones ingenuas de batching, CPU y GPU se turnan: la CPU prepara el próximo batch (selecciona requests, actualiza KV cache, admite o expulsa entradas) y transfiere los tensores a la GPU; la GPU ejecuta el forward y devuelve los tokens muestreados; la CPU procesa esa salida y vuelve a preparar el siguiente lote. En este patrón secuencial nunca hay solapamiento útil: cuando la GPU calcula, la CPU está inactiva; cuando la CPU prepara, la GPU espera.
Ese comportamiento puede parecer aceptable en una sola pasada, pero en loops de inferencia continua con cientos de pasos por segundo los minutos acumulados de inactividad son significativos. En el caso mencionado, generar 8K tokens con batch 32 en un modelo de 8B tomó 300.6 s, y el GPU estuvo inactivo por un 24.0% del tiempo esperando a la CPU. Vista optimista: si pudiéramos eliminar ese overhead de CPU, la latencia total caería de 300.6 s a ~228 s, un ahorro notable sin tocar modelos ni kernels.
Objetivo: concurrencia real entre CPU y GPU
La idea central es sencilla: mientras la GPU procesa el batch N, la CPU debe preparar el batch N+1. Para lograr esto hay que resolver tres retos claves: cómo lanzar trabajo en GPU sin bloquear la CPU; cómo garantizar que los datos necesarios estén listos cuando cada tarea se ejecute; y cómo preparar un batch que dependa de las predicciones del batch anterior. La solución técnica pasa por coordinar operaciones CUDA (kernels y copias) en colas distintas y usar mecanismos de sincronización que permitan verificar cuándo una operación ha terminado.
Streams de CUDA: permitir paralelismo entre operaciones GPU
CUDA organiza operaciones GPU en streams, que son colas ordenadas de comandos. Dentro de un mismo stream, las operaciones se ejecutan secuencialmente; entre streams diferentes, las operaciones pueden ejecutarse de forma concurrente. Por ejemplo, lanzar tres kernels en tres streams distintos permite que la GPU los ejecute de manera solapada, sujeto a la disponibilidad del hardware.
Un detalle práctico importante es el overhead de lanzamiento desde CPU: cada comando a la GPU requiere que la CPU localice el kernel, prepare la llamada y la envíe al controlador; ese tiempo de lanzamiento se suma a la línea temporal y puede escalonar los inicios de operaciones en distintos streams. Al diseñar un bucle asíncrono hay que tener en cuenta estas latencias de lanzamiento para entender qué se lanza y cuándo.
Crear concurrencia: estrategias generales
Para obtener concurrencia útil entre CPU y GPU se hace lo siguiente a alto nivel:
- Separar las responsabilidades: una tarea en CPU dedicada a preparar inputs y otra a consumir outputs; la GPU ejecuta los forward en su propio conjunto de streams.
- Usar streams no por defecto para lanzar kernels y copias que puedan solaparse entre sí.
- Sincronizar sólo cuando sea estrictamente necesario, en lugar de imponer barreras globales entre CPU y GPU cada paso.
Con esta arquitectura, la CPU puede estar preparando el batch N+1 (admisión de requests, actualización de estructuras en memoria host, copia a buffers intermedios) mientras la GPU calcula el forward del batch N en sus streams. Así se reduce el tiempo en que la GPU está ociosa esperando a la CPU.
Coordinación fina: eventos y comprobaciones de readiness
Para que esto funcione sin errores hay que comprobar que los datos requeridos por cada operación estén efectivamente listos antes de su ejecución en GPU o CPU. En CUDA se usan primitivas como eventos para marcar puntos en la ejecución de un stream y comprobar, desde otra stream o desde la CPU, si una operación anterior ha terminado. De ese modo se puede, por ejemplo, lanzar una copia o un kernel dependiente sólo cuando el evento indique que el buffer de origen ha sido escrito.
Otra responsabilidad importante es manejar dependencias entre batches: el batch N+1 puede necesitar la predicción (token muestreado) producida por N. La solución es diseñar el pipeline para que la CPU obtenga rápidamente los tokens de salida y solo esa parte mínima bloquee la preparación dependiente, mientras el resto de la preparación (evicción, re-ordenamiento, asignación de buffers) se hace en paralelo.
Consideraciones prácticas y costes para Latinoamérica
La eficiencia en inferencia no es un tema puramente técnico: tiene impacto directo en costos de operación en la nube. Por ejemplo, si una instancia H200 cuesta alrededor de $5/h, usarla de manera subóptima por horas o días incrementa costes para startups, equipos de investigación o proyectos gubernamentales en América Latina. Mejorar la utilización del GPU mediante batching asíncrono reduce tiempo de cómputo y, por ende, consumo de instancia, lo que se traduce en ahorro real en facturación y en mayor capacidad para escalar servicios.
Además, en regiones con presupuestos ajustados o con acceso limitado a hardware de última generación, exprimir rendimiento de cada GPU disponible mediante optimizaciones de software es una estrategia práctica y rentable.
Implementación y resultados esperables
Implementar batching asíncrono requiere cambios en la orquestación del bucle de inferencia: usar streams no por defecto, insertar eventos para coordinar dependencias y reorganizar la lógica de admisión/evicción de requests. La línea de trabajo se puede integrar en librerías existentes de inferencia; en el caso de referencia, el equipo implementó esta lógica dentro del módulo de continuous batching de una librería conocida (transformers), permitiendo comparar el comportamiento síncrono y asíncrono.
Aunque la mejora real dependerá del modelo, tamaño de batch y latencias de CPU, el ejemplo citado muestra que hasta casi una cuarta parte del tiempo total de generación puede corresponder a inactividad de GPU causada por sincronizaciones innecesarias. Reducir o eliminar esos puntos de bloqueo ofrece un beneficio inmediato y directo en throughput.
Conclusión
El batching asíncrono es una optimización de alto impacto para despliegues de inferencia continua: sin tocar modelos ni kernels, mejora la utilización de GPU coordinando mejor las cargas entre CPU y GPU. Para entornos en América Latina donde optimizar costos y recursos es crítico, esta técnica es especialmente relevante. Si su infraestructura ejecuta cientos de pasos por segundo, invertir en una arquitectura de inferencia que permita concurrencia entre CPU y GPU puede traducirse en ahorros económicos y en capacidad para atender más consultas con los mismos recursos.
Si quieren profundizar, revisar implementaciones concretas en librerías públicas (por ejemplo, integraciones en transformers) es un buen siguiente paso para adaptar la técnica a su stack y medir el impacto en sus cargas reales.
Fuente original: Hugging Face Blog