Optimice el rendimiento de la GPU TensorFlow con TensorFlow Profiler

Descripción general

Esta guía le mostrará cómo usar TensorFlow Profiler con TensorBoard para obtener información y obtener el máximo rendimiento de sus GPU, y depurar cuando una o más de sus GPU estén infrautilizadas.

Si eres nuevo en Profiler:

Tenga en cuenta que descargar cálculos a la GPU puede no siempre ser beneficioso, especialmente para modelos pequeños. Puede haber gastos generales debido a:

  • Transferencia de datos entre el host (CPU) y el dispositivo (GPU); y
  • Debido a la latencia involucrada cuando el host inicia los núcleos de GPU.

Flujo de trabajo de optimización del rendimiento

Esta guía describe cómo depurar problemas de rendimiento comenzando con una sola GPU y luego pasando a un solo host con varias GPU.

Se recomienda depurar problemas de rendimiento en el siguiente orden:

  1. Optimice y depure el rendimiento en una GPU:
    1. Compruebe si la tubería de entrada es un cuello de botella.
    2. Depurar el rendimiento de una GPU.
    3. Habilite la precisión mixta (con fp16 (float16)) y, opcionalmente, habilite XLA .
  2. Optimice y depure el rendimiento en el host único con múltiples GPU.

Por ejemplo, si está utilizando una estrategia de distribución de TensorFlow para entrenar un modelo en un único host con varias GPU y observa una utilización subóptima de la GPU, primero debe optimizar y depurar el rendimiento de una GPU antes de depurar el sistema de múltiples GPU.

Como punto de partida para obtener código de alto rendimiento en GPU, esta guía supone que ya está utilizando tf.function . Las API de Keras Model.compile y Model.fit utilizarán tf.function automáticamente bajo el capó. Al escribir un bucle de entrenamiento personalizado con tf.GradientTape , consulte Mejor rendimiento con tf.function sobre cómo habilitar tf.function s.

Las siguientes secciones analizan los enfoques sugeridos para cada uno de los escenarios anteriores para ayudar a identificar y solucionar los cuellos de botella en el rendimiento.

1. Optimice el rendimiento en una GPU

En un caso ideal, su programa debería tener una alta utilización de la GPU, una comunicación mínima entre la CPU (el host) y la GPU (el dispositivo) y sin sobrecarga del canal de entrada.

El primer paso para analizar el rendimiento es obtener un perfil para un modelo que se ejecuta con una GPU.

La página de descripción general del Profiler de TensorBoard, que muestra una vista de nivel superior de cómo se desempeñó su modelo durante la ejecución de un perfil, puede brindar una idea de qué tan lejos está su programa del escenario ideal.

TensorFlow Profiler Overview Page

Los números clave a los que prestar atención en la página de descripción general son:

  1. ¿Cuánto del tiempo del paso proviene de la ejecución real del dispositivo?
  2. El porcentaje de operaciones realizadas en el dispositivo frente al host.
  3. ¿Cuántos núcleos usan fp16

Lograr un rendimiento óptimo significa maximizar estos números en los tres casos. Para obtener una comprensión profunda de su programa, deberá estar familiarizado con el visor de seguimiento Profiler de TensorBoard. Las secciones siguientes muestran algunos patrones comunes del visor de seguimiento que debe buscar al diagnosticar cuellos de botella en el rendimiento.

A continuación se muestra una imagen de una vista de seguimiento de modelo ejecutándose en una GPU. En las secciones Alcance del nombre de TensorFlow y Operaciones de TensorFlow , puede identificar diferentes partes del modelo, como el paso hacia adelante, la función de pérdida, el cálculo de gradiente/paso hacia atrás y la actualización del peso del optimizador. También puede ejecutar las operaciones en la GPU junto a cada Stream , que se refieren a flujos CUDA. Cada flujo se utiliza para tareas específicas. En este seguimiento, Stream#118 se utiliza para iniciar núcleos informáticos y copias de dispositivo a dispositivo. La secuencia n.º 119 se utiliza para la copia de host a dispositivo y la secuencia n.º 120 para la copia de dispositivo a host.

El siguiente seguimiento muestra las características comunes de un modelo de alto rendimiento.

image

Por ejemplo, la línea de tiempo de cómputo de la GPU ( Stream#118 ) parece "ocupada" con muy pocos espacios en blanco. Hay copias mínimas del host al dispositivo ( Transmisión #119 ) y del dispositivo al host ( Transmisión #120 ), así como espacios mínimos entre los pasos. Cuando ejecuta Profiler para su programa, es posible que no pueda identificar estas características ideales en su vista de seguimiento. El resto de esta guía cubre escenarios comunes y cómo solucionarlos.

1. Depurar la canalización de entrada

El primer paso en la depuración del rendimiento de la GPU es determinar si su programa está vinculado a la entrada. La forma más sencilla de resolver esto es utilizar el analizador de canalización de entrada de Profiler, en TensorBoard, que proporciona una descripción general del tiempo invertido en la canalización de entrada.

image

Puede tomar las siguientes acciones potenciales si su canal de entrada contribuye significativamente al tiempo del paso:

  • Puede utilizar la guía específica tf.data para aprender cómo depurar su canal de entrada.
  • Otra forma rápida de comprobar si el canal de entrada es el cuello de botella es utilizar datos de entrada generados aleatoriamente que no necesitan ningún procesamiento previo. A continuación se muestra un ejemplo del uso de esta técnica para un modelo ResNet. Si la canalización de entrada es óptima, debería experimentar un rendimiento similar con datos reales y con datos aleatorios/sintéticos generados. La única sobrecarga en el caso de los datos sintéticos se deberá a la copia de los datos de entrada, que nuevamente se puede recuperar y optimizar.

Además, consulte las mejores prácticas para optimizar la canalización de datos de entrada .

2. Depurar el rendimiento de una GPU

Hay varios factores que pueden contribuir a una baja utilización de la GPU. A continuación se muestran algunos escenarios que se observan comúnmente al observar el visor de seguimiento y posibles soluciones.

1. Analizar las brechas entre los pasos.

Una observación común cuando su programa no se ejecuta de manera óptima son los espacios entre los pasos de entrenamiento. En la imagen de la vista de seguimiento a continuación, hay un gran espacio entre los pasos 8 y 9, lo que significa que la GPU está inactiva durante ese tiempo.

image

Si su visor de seguimiento muestra grandes espacios entre los pasos, esto podría ser una indicación de que su programa está limitado por entradas. En ese caso, debe consultar la sección anterior sobre depuración de su canal de entrada si aún no lo ha hecho.

Sin embargo, incluso con una canalización de entrada optimizada, aún puede haber espacios entre el final de un paso y el inicio de otro debido a la contención de subprocesos de la CPU. tf.data utiliza subprocesos en segundo plano para paralelizar el procesamiento de canalizaciones. Estos subprocesos pueden interferir con la actividad del lado del host de la GPU que ocurre al comienzo de cada paso, como copiar datos o programar operaciones de la GPU.

Si nota grandes brechas en el lado del host, que programa estas operaciones en la GPU, puede configurar la variable de entorno TF_GPU_THREAD_MODE=gpu_private . Esto garantiza que los núcleos de GPU se inicien desde sus propios subprocesos dedicados y no queden en cola detrás del trabajo tf.data .

Los espacios entre pasos también pueden deberse a cálculos métricos, devoluciones de llamadas de Keras u operaciones fuera de tf.function que se ejecutan en el host. Estas operaciones no tienen un rendimiento tan bueno como las operaciones dentro de un gráfico de TensorFlow. Además, algunas de estas operaciones se ejecutan en la CPU y copian tensores desde y hacia la GPU.

Si después de optimizar su canal de entrada aún nota espacios entre los pasos en el visor de seguimiento, debe mirar el código del modelo entre los pasos y verificar si deshabilitar las devoluciones de llamadas/métricas mejora el rendimiento. Algunos detalles de estas operaciones también se encuentran en el visor de seguimiento (tanto del lado del dispositivo como del host). La recomendación en este escenario es amortizar la sobrecarga de estas operaciones ejecutándolas después de una cantidad fija de pasos en lugar de cada paso. Cuando se utiliza el método Model.compile en la API tf.keras , configurar el steps_per_execution lo hace automáticamente. Para bucles de entrenamiento personalizados, utilice tf.while_loop .

2. Lograr una mayor utilización del dispositivo

1. Kernels de GPU pequeños y retrasos en el lanzamiento del kernel del host

El host pone en cola los núcleos para que se ejecuten en la GPU, pero hay una latencia (alrededor de 20-40 μs) antes de que los núcleos se ejecuten realmente en la GPU. En un caso ideal, el host pone en cola suficientes núcleos en la GPU de modo que la GPU pase la mayor parte del tiempo ejecutándose, en lugar de esperar a que el host ponga en cola más núcleos.

La página de descripción general del Profiler en TensorBoard muestra cuánto tiempo estuvo inactiva la GPU debido a la espera de que el host iniciara los kernels. En la imagen a continuación, la GPU está inactiva durante aproximadamente el 10% del tiempo del paso esperando que se inicien los kernels.

image

El visor de seguimiento de este mismo programa muestra pequeños espacios entre los núcleos donde el host está ocupado iniciando núcleos en la GPU.

image

Al iniciar muchas operaciones pequeñas en la GPU (como una adición escalar, por ejemplo), es posible que el host no pueda seguir el ritmo de la GPU. La herramienta TensorFlow Stats en TensorBoard para el mismo perfil muestra 126.224 operaciones Mul en 2,77 segundos. Por lo tanto, cada kernel tiene aproximadamente 21,9 μs, lo cual es muy pequeño (aproximadamente el mismo tiempo que la latencia de lanzamiento) y potencialmente puede provocar retrasos en el lanzamiento del kernel del host.

image

Si su visor de seguimiento muestra muchos espacios pequeños entre operaciones en la GPU, como en la imagen de arriba, puede:

  • Concatene tensores pequeños y use operaciones vectorizadas o use un tamaño de lote más grande para que cada núcleo lanzado haga más trabajo, lo que mantendrá la GPU ocupada por más tiempo.
  • Asegúrese de utilizar tf.function para crear gráficos de TensorFlow, de modo que no esté ejecutando operaciones en un modo ansioso puro. Si está utilizando Model.fit (a diferencia de un bucle de entrenamiento personalizado con tf.GradientTape ), entonces tf.keras.Model.compile lo hará automáticamente por usted.
  • Fusione kernels usando XLA con tf.function(jit_compile=True) o agrupación automática. Para obtener más detalles, vaya a la sección Habilitar precisión mixta y XLA a continuación para aprender cómo habilitar XLA para obtener un mayor rendimiento. Esta característica puede conducir a una alta utilización del dispositivo.
2. Colocación de operaciones de TensorFlow

La página de descripción general del Generador de perfiles le muestra el porcentaje de operaciones realizadas en el host frente al dispositivo (también puede verificar la ubicación de operaciones específicas mirando el visor de seguimiento . Como en la imagen a continuación, desea conocer el porcentaje de operaciones en el host). ser muy pequeño en comparación con el dispositivo.

image

Idealmente, la mayoría de las operaciones informáticas intensivas deberían ubicarse en la GPU.

Para saber a qué dispositivos están asignadas las operaciones y tensores en su modelo, configure tf.debugging.set_log_device_placement(True) como la primera declaración de su programa.

Tenga en cuenta que en algunos casos, incluso si especifica que se coloque una operación en un dispositivo en particular, su implementación podría anular esta condición (ejemplo: tf.unique ). Incluso para el entrenamiento de una sola GPU, especificar una estrategia de distribución, como tf.distribute.OneDeviceStrategy , puede dar como resultado una ubicación más determinista de las operaciones en su dispositivo.

Una razón para colocar la mayoría de las operaciones en la GPU es evitar copias excesivas de memoria entre el host y el dispositivo (se esperan copias de memoria para los datos de entrada/salida del modelo entre el host y el dispositivo). En la vista de seguimiento siguiente se muestra un ejemplo de copia excesiva en las secuencias de GPU n.° 167 , n.° 168 y n.° 169 .

image

A veces, estas copias pueden afectar el rendimiento si bloquean la ejecución de los núcleos de la GPU. Las operaciones de copia de memoria en el visor de seguimiento tienen más información sobre las operaciones que son el origen de estos tensores copiados, pero puede que no siempre sea fácil asociar una memCopy con una operación. En estos casos, es útil observar las operaciones cercanas para verificar si la copia de la memoria ocurre en la misma ubicación en cada paso.

3. Kernels más eficientes en GPU

Una vez que la utilización de la GPU de su programa sea aceptable, el siguiente paso es buscar aumentar la eficiencia de los núcleos de la GPU mediante el uso de Tensor Cores o la fusión de operaciones.

1. Utilice núcleos tensoriales

Las GPU NVIDIA® modernas tienen núcleos tensoriales especializados que pueden mejorar significativamente el rendimiento de los núcleos elegibles.

Puede utilizar las estadísticas del kernel de GPU de TensorBoard para visualizar qué kernels de GPU son elegibles para Tensor Core y qué kernels utilizan Tensor Cores. Habilitar fp16 (consulte la sección Habilitación de precisión mixta a continuación) es una forma de hacer que los núcleos (matmul ops) de General Matrix Multiply (GEMM) de su programa utilicen Tensor Core. Los núcleos de GPU utilizan los Tensor Cores de manera eficiente cuando la precisión es fp16 y las dimensiones del tensor de entrada/salida son divisibles por 8 o 16 (para int8 ).

Para obtener otras recomendaciones detalladas sobre cómo hacer que los kernels sean eficientes para las GPU, consulte la guía de rendimiento de aprendizaje profundo de NVIDIA® .

2. Operaciones con fusibles

Utilice tf.function(jit_compile=True) para fusionar operaciones más pequeñas para formar núcleos más grandes, lo que generará ganancias de rendimiento significativas. Para obtener más información, consulte la guía XLA .

3. Habilite la precisión mixta y XLA

Después de seguir los pasos anteriores, habilitar la precisión mixta y XLA son dos pasos opcionales que puede seguir para mejorar aún más el rendimiento. El enfoque sugerido es habilitarlos uno por uno y verificar que los beneficios de rendimiento sean los esperados.

1. Habilitar precisión mixta

La guía de precisión mixta de TensorFlow muestra cómo habilitar la precisión fp16 en GPU. Habilite AMP en las GPU NVIDIA® para usar Tensor Cores y obtenga velocidades generales de hasta 3 veces en comparación con el uso de solo precisión fp32 (float32) en Volta y arquitecturas de GPU más nuevas.

Asegúrese de que las dimensiones de la matriz/tensor cumplan los requisitos para llamar a núcleos que utilizan Tensor Cores. Los núcleos de GPU utilizan los Tensor Cores de manera eficiente cuando la precisión es fp16 y las dimensiones de entrada/salida son divisibles por 8 o 16 (para int8).

Tenga en cuenta que con cuDNN v7.6.3 y posteriores, las dimensiones de convolución se rellenarán automáticamente cuando sea necesario para aprovechar Tensor Cores.

Siga las mejores prácticas a continuación para maximizar los beneficios de rendimiento de la precisión fp16 .

1. Utilice núcleos fp16 óptimos

Con fp16 habilitado, los núcleos de multiplicaciones de matrices (GEMM) de su programa deben usar la versión fp16 correspondiente que utiliza Tensor Cores. Sin embargo, en algunos casos, esto no sucede y no experimenta la aceleración esperada al habilitar fp16 , ya que su programa recurre a la implementación ineficiente.

image

La página de estadísticas del kernel de GPU muestra qué operaciones son elegibles para Tensor Core y qué kernels realmente utilizan el eficiente Tensor Core. La guía de NVIDIA® sobre el rendimiento del aprendizaje profundo contiene sugerencias adicionales sobre cómo aprovechar Tensor Cores. Además, los beneficios de usar fp16 también se mostrarán en los núcleos que anteriormente estaban vinculados a la memoria, ya que ahora las operaciones tomarán la mitad del tiempo.

2. Escalado de pérdidas dinámico versus estático

La escala de pérdida es necesaria cuando se utiliza fp16 para evitar un desbordamiento insuficiente debido a la baja precisión. Hay dos tipos de escalado de pérdidas, dinámico y estático, y ambos se explican con mayor detalle en la guía Mixed Precision . Puede utilizar la política mixed_float16 para habilitar automáticamente el escalado de pérdidas dentro del optimizador de Keras.

Al intentar optimizar el rendimiento, es importante recordar que el escalado de pérdida dinámica puede introducir operaciones condicionales adicionales que se ejecutan en el host y generar espacios que serán visibles entre los pasos en el visor de seguimiento. Por otro lado, el escalado de pérdidas estáticas no tiene tales gastos generales y puede ser una mejor opción en términos de rendimiento con el inconveniente de que es necesario especificar el valor correcto de escala de pérdidas estáticas.

2. Habilite XLA con tf.function(jit_compile=True) o agrupación automática

Como paso final para obtener el mejor rendimiento con una sola GPU, puede experimentar habilitando XLA, lo que fusionará operaciones y conducirá a una mejor utilización del dispositivo y una menor huella de memoria. Para obtener detalles sobre cómo habilitar XLA en su programa con tf.function(jit_compile=True) o agrupación automática, consulte la guía XLA .

Puede establecer el nivel JIT global en -1 (apagado), 1 o 2 . Un nivel más alto es más agresivo y puede reducir el paralelismo y utilizar más memoria. Establezca el valor en 1 si tiene restricciones de memoria. Tenga en cuenta que XLA no funciona bien para modelos con formas de tensor de entrada variable, ya que el compilador XLA tendría que seguir compilando núcleos cada vez que encuentre nuevas formas.

2. Optimice el rendimiento en el host único con múltiples GPU

La API tf.distribute.MirroredStrategy se puede utilizar para escalar el entrenamiento de modelos desde una GPU a varias GPU en un solo host. (Para obtener más información sobre cómo realizar capacitación distribuida con TensorFlow, consulte las guías Capacitación distribuida con TensorFlow , Usar una GPU y Usar TPU y el tutorial Capacitación distribuida con Keras ).

Aunque lo ideal es que la transición de una GPU a varias GPU sea escalable desde el primer momento, a veces se pueden encontrar problemas de rendimiento.

Al pasar del entrenamiento con una sola GPU a varias GPU en el mismo host, lo ideal sería experimentar el aumento del rendimiento solo con la sobrecarga adicional de la comunicación en gradiente y una mayor utilización de los subprocesos del host. Debido a esta sobrecarga, no tendrás una aceleración exacta de 2x si pasas de 1 a 2 GPU, por ejemplo.

La vista de seguimiento a continuación muestra un ejemplo de la sobrecarga de comunicación adicional cuando se entrena en varias GPU. Hay algunos gastos generales para concatenar los gradientes, comunicarlos entre réplicas y dividirlos antes de realizar la actualización de peso.

image

La siguiente lista de verificación lo ayudará a lograr un mejor rendimiento al optimizar el rendimiento en el escenario de múltiples GPU:

  1. Intente maximizar el tamaño del lote, lo que conducirá a una mayor utilización del dispositivo y amortizará los costos de comunicación entre múltiples GPU. El uso del perfilador de memoria ayuda a tener una idea de qué tan cerca está su programa de la utilización máxima de la memoria. Tenga en cuenta que, si bien un tamaño de lote mayor puede afectar la convergencia, esto generalmente se ve compensado por los beneficios de rendimiento.
  2. Al pasar de una única GPU a varias GPU, el mismo host ahora tiene que procesar muchos más datos de entrada. Entonces, después de (1), se recomienda volver a verificar el rendimiento de la canalización de entrada y asegurarse de que no sea un cuello de botella.
  3. Verifique la línea de tiempo de la GPU en la vista de seguimiento de su programa para ver si hay llamadas AllReduce innecesarias, ya que esto da como resultado una sincronización en todos los dispositivos. En la vista de seguimiento que se muestra arriba, AllReduce se realiza a través del kernel NCCL y solo hay una llamada NCCL en cada GPU para los gradientes en cada paso.
  4. Compruebe si hay operaciones de copia D2H, H2D y D2D innecesarias que puedan minimizarse.
  5. Verifique el tiempo del paso para asegurarse de que cada réplica esté haciendo el mismo trabajo. Por ejemplo, puede suceder que una GPU (normalmente, GPU0 ) tenga un exceso de suscripción porque el host, por error, termine dedicándole más trabajo.
  6. Por último, verifique el paso de entrenamiento en todas las GPU en su vista de seguimiento para ver si hay operaciones que se estén ejecutando secuencialmente. Esto suele suceder cuando su programa incluye dependencias de control de una GPU a otra. En el pasado, la depuración del rendimiento en esta situación se resolvía caso por caso. Si observa este comportamiento en su programa, presente un problema de GitHub con imágenes de su vista de seguimiento.

1. Optimizar el gradiente AllReduce

Cuando se entrena con una estrategia sincrónica, cada dispositivo recibe una parte de los datos de entrada.

Después de calcular los pasos hacia adelante y hacia atrás a través del modelo, es necesario agregar y reducir los gradientes calculados en cada dispositivo. Este gradiente AllReduce ocurre después del cálculo del gradiente en cada dispositivo y antes de que el optimizador actualice los pesos del modelo.

Cada GPU primero concatena los gradientes entre las capas del modelo, los comunica entre las GPU mediante tf.distribute.CrossDeviceOps ( tf.distribute.NcclAllReduce es el valor predeterminado) y luego devuelve los gradientes después de la reducción por capa.

El optimizador utilizará estos gradientes reducidos para actualizar los pesos de su modelo. Idealmente, este proceso debería realizarse al mismo tiempo en todas las GPU para evitar gastos generales.

El tiempo para AllReduce debería ser aproximadamente el mismo que:

(number of parameters * 4bytes)/ (communication bandwidth)

Este cálculo es útil como verificación rápida para comprender si el rendimiento que tiene al ejecutar un trabajo de entrenamiento distribuido es el esperado o si necesita realizar más depuraciones de rendimiento. Puede obtener la cantidad de parámetros en su modelo en Model.summary .

Tenga en cuenta que cada parámetro del modelo tiene un tamaño de 4 bytes, ya que TensorFlow usa fp32 (float32) para comunicar gradientes. Incluso cuando tiene fp16 habilitado, NCCL AllReduce utiliza parámetros fp32 .

Para obtener los beneficios del escalado, el tiempo de paso debe ser mucho mayor en comparación con estos gastos generales. Una forma de lograrlo es utilizar un tamaño de lote mayor, ya que el tamaño del lote afecta el tiempo del paso, pero no afecta la sobrecarga de comunicación.

2. Contención de subprocesos del host de GPU

Cuando se ejecutan varias GPU, el trabajo de la CPU es mantener todos los dispositivos ocupados iniciando de manera eficiente los núcleos de GPU en todos los dispositivos.

Sin embargo, cuando hay muchas operaciones independientes que la CPU puede programar en una GPU, la CPU puede decidir usar muchos de sus subprocesos de host para mantener ocupada una GPU y luego lanzar núcleos en otra GPU en un orden no determinista. . Esto puede provocar un sesgo o un escalado negativo, lo que puede afectar negativamente al rendimiento.

El visor de seguimiento a continuación muestra la sobrecarga cuando la CPU hace tambalear el kernel de la GPU al iniciarse de manera ineficiente, ya que GPU1 está inactiva y luego comienza a ejecutar operaciones después de que se ha iniciado GPU2 .

image

La vista de seguimiento del host muestra que el host está iniciando kernels en GPU2 antes de iniciarlos en GPU1 (tenga en cuenta que las operaciones tf_Compute* siguientes no son indicativas de subprocesos de CPU).

image

Si experimenta este tipo de asombro de los núcleos de GPU en la vista de seguimiento de su programa, la acción recomendada es:

  • Establezca la variable de entorno de TensorFlow TF_GPU_THREAD_MODE en gpu_private . Esta variable de entorno le indicará al host que mantenga privados los subprocesos de una GPU.
  • De forma predeterminada, TF_GPU_THREAD_MODE=gpu_private establece el número de subprocesos en 2, lo cual es suficiente en la mayoría de los casos. Sin embargo, ese número se puede cambiar configurando la variable de entorno de TensorFlow TF_GPU_THREAD_COUNT en el número deseado de subprocesos.