Dos técnicas sencillas para garantizar que el entrenamiento de sus LLM alcance el máximo rendimiento.
El entrenamiento o ajuste fino (finetuning) de un modelo base de varios miles de millones de parámetros requiere un cómputo significativo, que a menudo abarca de cientos a miles de horas de entrenamiento en GPU. Sin embargo, optimizar el rendimiento del entrenamiento para un LLM es todo un desafío, y las métricas de alto nivel, como la utilización de la GPU, podrían no reflejar la utilización real. En esta publicación de blog, mostramos cómo atravesar las capas de abstracción y acelerar 6 veces el ajuste fino del modelo Qwen 3.8 27B más reciente en AI Runtime. Mediante una optimización sistemática del rendimiento, incluso cuando la utilización de la GPU ya estaba en el 98 %, pasamos de 7 horas y 44 minutos a 1 hora y 17 minutos al realizar el ajuste fino de un conjunto de datos fijo.
Profundizaremos en las técnicas detrás de estas mejoras, desde cómo medir lo que importa hasta los detalles de implementación y los compromisos de diseño que implica cada optimización.
Si quieres empezar de inmediato, ve directamente al código de ejemplo en AI Runtime y pruébalo.
Comenzamos con un bucle de entrenamiento estándar para Qwen 3.8 27B utilizando la librería transformers y el conjunto de datos de entrenamiento SFT HuggingFaceH4/ultrachat_200k.
Nuestro entrenamiento de referencia del modelo sobre el conjunto de datos completo tarda 7 horas y 44 minutos, y durante el entrenamiento la GPU muestra un 98 % de utilización promedio. El resultado es un entrenamiento de 9,396 tok/s.
Esta utilización de la GPU parece alta, pero como se mencionó anteriormente, ese número es engañoso. La métrica estándar de utilización de la GPU solo cuenta el porcentaje de tiempo que la GPU estuvo haciendo algo, por lo que no nos dice qué tan bien estaba usando el modelo la GPU mientras estaba activa. Para entender esto, necesitamos mirar más a fondo.
Una forma de hacer esto es el mecanismo de traza de torch; podemos ajustar nuestro bucle de entrenamiento de la siguiente manera (al tiempo que limitamos el número de pasos para obtener una respuesta rápida):
Para un paso de muestra, esto nos da los siguientes datos:
| Operación | Número de llamadas | Tiempo promedio (micros) | Tiempo total (millis) |
|---|---|---|---|
| aten::bmm | 31,105 | 50.5 | 1,571.80 |
| aten::mul | 46,878 | 16.2 | 758.9 |
| MmBackward0 | 497 | 1,111.4 | 552.4 |
| aten::copy_ | 69,444 | 7.2 | 500.6 |
| aten::add | 45,623 | 8.0 | 366.7 |
| aten::select | 68,645 | 3.4 | 230.5 |
| aten::exp | 9,504 | 21.7 | 206.5 |
| aten::sum | 16,194 | 12.2 | 196.8 |
| aten::empty | 40,709 | 4.7 | 190.5 |
| aten::as_strided | 218,214 | 0.8 | 173.3 |
Tabla 1: CPU: 14,4 s con 1 714 382 llamadas. La tabla contiene las operaciones principales por tiempo total.
| Operación | Número de llamadas | Tiempo promedio (micros) | Tiempo total (millis) |
|---|---|---|---|
| nvjet GEMM | 1923 | 1,163.1 | 2,236.8 |
| Suma de punto flotante elemento por elemento | 42,657 | 51.1 | 2,180.5 |
| Multiplicación de punto flotante elemento por elemento | 35,012 | 24 | 841.8 |
| Relleno de punto flotante elemento por elemento | 33,557 | 21.5 | 723.1 |
| Copia de punto flotante elemento por elemento | 59,282 | 11.8 | 700.7 |
| Memcpy DtoD | 5,585 | 90.5 | 505.2 |
| sm80_xmma GEMM | 31,105 | 20.9 | 651.3 |
Tabla 2: GPU: 10,3 s con 269 448 operaciones. La tabla contiene las operaciones principales por tiempo total
A partir de estas tablas, notamos algunos problemas de alto nivel con el modelo actual:
Además, tenemos un problema con el exceso de relleno (padding) en el lote: el tensor de entrada debe ser rectangular y cada fila se rellena hasta el número máximo de tókenes para ese lote. Mostraremos un truco para los LLM que se puede usar para minimizarlo.
En las siguientes secciones, profundizaremos en cada tema técnico, cubriendo qué es, las mejores prácticas recomendadas y las principales ventajas y desventajas a considerar al aplicarlo.
¡Es posible que hayas notado que algunos kernels se ejecutan muchísimo! En particular, para Qwen, el kernel de multiplicación de matrices en lotes se ejecuta más de 30 000 veces en un solo paso.
En este caso, primero exploraremos los kernels fusionados. Un kernel fusionado toma varios kernels de GPU más pequeños y los combina en una sola unidad. Esto puede mejorar el rendimiento al evitar transferencias costosas entre la memoria de alto ancho de banda de una GPU y su memoria en chip de ancho de banda aún mayor. Asimismo, también puede permitir una programación más eficiente de los núcleos de la GPU.
Con tantos kernels adicionales, aunque torch.compile podría funcionar, llevará mucho tiempo ejecutarse. Por lo tanto, reducir la cantidad directa de ops con kernels fusionados manualmente es un buen primer paso. Además, fusiones bien conocidas como Flash Attention no son algo que torch.compile encontraría por sí mismo.
La biblioteca transformers utiliza de forma predeterminada el SDPA de torch para las capas de atención estándar. Sin embargo, resulta que muchas de estas pequeñas operaciones provienen de las capas no estándar de gated delta network. Tenemos suerte de que para estas capas existen dos kernels, causal-conv1d y flash-linear-attention, que se escribieron para fusionar muchas de estas operaciones por capa. ¿Cómo descubrimos esto? La implementación de la biblioteca transformers de Qwen ya nos muestra los kernels que admite:
Hay dos métodos de alto nivel para obtener estos kernels:
Por lo general, recomendamos la primera opción, especialmente combinada con nuestro soporte de contenedores de Docker: crea tu contenedor con todos tus kernels instalados en el momento de la compilación para reducir el tiempo que tarda tu trabajo en iniciarse.
Aunque kernelhub es conveniente, dado que los kernels a menudo dependen explícitamente de la versión instalada de torch y cuda, es posible que no haya un kernel disponible para tu configuración específica y el modelo de transformers recurrirá silenciosamente a un kernel no optimizado con bajo rendimiento.
Después de instalar los paquetes de Python flash-linear-attention y causal-conv1d, mejoramos nuestro tiempo de extremo a extremo en un factor de 3.7x, bajando a 2 horas y 5 minutos. En las siguientes tablas, puedes ver los nuevos kernels (resaltados), que ocupan el lugar de las funciones elementwise de las tablas anteriores. Además, el número total de operaciones ejecutadas se ha reducido enormemente, en un factor de 15x:
| Operación | Número de llamadas | Tiempo promedio (micros) | Tiempo total (millis) |
|---|---|---|---|
| cudaMemsetAsync | 1,854 | 283.9 | 526.3 |
| ChunkGatedDeltaRuleFunction | 96 | 871.7 | 83.7 |
| aten::mm | 1,987 | 39.6 | 78.6 |
| MmBackward0 | 497 | 134.2 | 66.7 |
| ChunkGatedDeltaRuleFunctionBackward | 48 | 1,159.9 | 55.7 |
| DaoAILab::_causal_conv1d_fwd_cpp | 96 | 55.8 | 5.4 |
| CausalConv1dFn | 96 | 51.4 | 4.9 |
Tabla 3: CPU: 3.9 s con 118,316 llamadas. La tabla contiene las principales operaciones por tiempo total.
| Operación | Número de llamadas | Tiempo promedio (micros) | Tiempo total (millis) |
|---|---|---|---|
| nvjet GEMM | 1,923 | 1,183.3 | 2,275.5 |
| Multiplicación flotante elementwise | 2184 | 128.1 | 279.9 |
| Copia + conversión elementwise | 1255 | 101.1 | 126.9 |
| chunk_cat flash-linear-attention | 65 | 1,402.0 | 91.2 |
| flash_attention_3 forwards | 16 | 2,693.8 | 43.1 |
| chunk_gated_delta_rule_fwd | 144 | 255.6 | 36.8 |
| flash_attention_3 backwards | 32 | 965.6 | 30.9 |
Tabla 4: GPU: 3.9 s con 18,060 llamadas. La tabla contiene las principales operaciones por tiempo total.
Advertencia: durante el entrenamiento necesitarás un kernel tanto para el pase forward como para el backward, y algunos kernels son solo forward, ya que solo se han implementado para acelerar la inferencia.
Conclusiones clave
Nuestro modelo Qwen ahora es bastante rápido, pero podemos hacerlo aún más rápido. Por defecto, nuestra configuración de batching utiliza tensores rectangulares en 2D de 8,192 tokens, ya que es el tamaño más grande que podemos ajustar sin quedarnos sin memoria. La desventaja de este enfoque es que tendemos a desperdiciar una cantidad considerable de espacio en el tensor con padding.
Si Qwen solo utilizara mecanismos de atención estándar, el NestedTensor de torch ahora es muy bien compatible y ayudaría a reducir este costo al eliminar por completo el padding. Pero flash-linear-attention solo admite un enfoque de tensor empaquetado en 1D m ás tradicional:

Pero no podemos hacer esto de manera arbitraria; pasar simplemente el tensor en 1D al modelo puede ser aceptable para el preentrenamiento, pero para el ajuste fino (finetuning), debemos asegurarnos de que, dentro de un ejemplo, los tokens solo atiendan a tokens del mismo ejemplo. Para solucionar esto, debemos pasar los position_ids de ejemplo correctos al modelo. A su vez, el modelo los pasará a los kernels de atención fusionados y este aplicará la máscara de atención correcta. Si observas el código de ejemplo aquí, puedes ver cómo calculamos las entradas necesarias para que esto funcione.
Con este cambio de apariencia sencilla, nuestro tiempo de entrenamiento pasa de 2 horas y 5 minutos a 1 hora y 17 minutos.
Esto se debe a dos razones:
Compromiso (trade-off): Este tipo de cambio hace que la canalización de entrada sea más compleja y existe una alta probabilidad de introducir errores que afecten la calidad del modelo. Pero en este caso, el ahorro del 36% vale la pena. Existen otras técnicas para reducir el padding, como mantener el mecanismo de batching original pero ordenar todas las entradas por longitud para minimizar el padding, pero este orden puede introducir correlaciones inesperadas en muestras adyacentes, lo que sesga el modelo.
Conclusiones clave: Al entrenar LLM en un conjunto de datos con entradas más cortas que el máximo de tokens entrenables (por ejemplo, en este caso 8192), empaquetar los datos de manera eficiente en lotes puede ahorrar una cantidad significativa de tiempo. Para este conjunto de datos, vimos una mejora de 1.6x en la velocidad de entrenamiento, reduciendo nuestro tiempo de entrenamiento de 2 horas y 5 minutos a 1 hora y 17 minutos.
En este blog, te mostramos cómo multiplicar por 6 la eficiencia de tu entrenamiento. Se redujo a (1) primero medir lo que importa, no solo la utilización de la GPU, sino también en qué parte el modelo pasa tiempo entrenando, (2) optimizar y fusionar kernels, utilizando bibliotecas conocidas como Flash Linear Attention, y (3) adaptar cómo se alimentan los lotes de datos al modelo.
Para hacer esto a escala, necesitas una plataforma que haga de la ruta de alto rendimiento la opción predeterminada. En AIR, muchas de estas mejoras de rendimiento vienen integradas. Incluimos las últimas versiones de CUDA y flash attention en nuestros entornos base, rastreamos automáticamente métricas clave como el uso de la GPU, la temperatura y su estado por ti, y nos aseguramos de que las versiones de torch (y los perfiladores) estén actualizadas. Nuestro agente interno, Genie Code, también está ajustado para buscar oportunidades como estas y optimizar aún más tu entrenamiento. Además, la lectura de datos desde Databricks está optimizada a través de nuestro UCVolumeDataset.
¡Prueba AIR para tu próxima carga de trabajo de entrenamiento! ¿Listo para probarlo? Consulta el recetario de AI Runtime, con detalles sobre cómo ejecutar este modelo tú mismo.
(Esta entrada del blog ha sido traducida utilizando herramientas basadas en inteligencia artificial) Publicación original
Suscríbete a nuestro blog y recibe las últimas publicaciones directamente en tu bandeja de entrada.