¡Reserva! Google I / O regresa del 18 al 20 de mayo Regístrese ahora
Se usó la API de Cloud Translation para traducir esta página.
Switch to English

Optimice el rendimiento de la GPU de TensorFlow con TensorFlow Profiler

Descripción general

Esta guía está dirigida a usuarios de TensorFlow que utilizan GPU para mejorar el rendimiento del modelo. Al usar TensorFlow Profiler como la herramienta principal para obtener información sobre el rendimiento, esta guía lo ayudará a depurar cuando una o más de sus GPU estén infrautilizadas. Se puede encontrar una guía de inicio rápido para TensorFlow Profiler en el tutorial de TensorFlow Profiler , y las formas adicionales de obtener un perfil se documentan en Optimizar el rendimiento de TensorFlow con la guía Profiler .

Tenga en cuenta que la descarga de cálculos a la GPU puede no ser siempre beneficiosa, especialmente para modelos pequeños. Hay gastos generales debido a la transferencia de datos entre el host (CPU) y el dispositivo (GPU), así como los gastos generales debido a la latencia involucrada cuando el host inicia los núcleos de la GPU. Se logra un buen rendimiento cuando el host mantiene la GPU ocupada con éxito descargando suficiente trabajo.

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 múltiples GPU. Se recomienda depurar los problemas de rendimiento en este orden. Por ejemplo, si está utilizando una estrategia de distribución de TensorFlow para entrenar un modelo en un solo host con varias GPU y observa un uso de GPU subóptimo, primero debe optimizar y depurar el rendimiento para 1 GPU, antes de depurar el sistema de múltiples GPU. El orden recomendado es el siguiente:

  1. Optimice y depure el rendimiento en 1 GPU
    1. Compruebe si la canalización de entrada es un cuello de botella
    2. Rendimiento de depuración de 1 GPU
    3. Habilite fp16 y, opcionalmente, habilite XLA
  2. Optimice y depure el rendimiento en un solo host con varias GPU

Como punto de partida para obtener código de rendimiento en GPU, esta guía asume que ya está usando tf.function . La API de compilación / ajuste de Keras utilizará tf.function automáticamente bajo el capó. Al escribir un ciclo de entrenamiento personalizado, consulte esta guía sobre cómo habilitar tf.function .

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

Optimizar el rendimiento en 1 GPU

En un caso ideal, su programa debe tener una alta utilización de GPU, una comunicación mínima de CPU (host) a GPU (dispositivo) y ninguna sobrecarga de la canalización 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 de TensorFlow Profiler proporciona una idea de qué tan lejos está tu programa del escenario ideal.

TensorFlow Profiler Overview Page

Los números clave que debe buscar en la página de descripción general son:

  1. ¿Cuánto tiempo del paso proviene de la ejecución real del dispositivo?
  2. El porcentaje de operaciones colocadas en el dispositivo frente al host
  3. ¿Cuántos kernels 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 de TensorFlow Profiler. 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 que se ejecuta en 1 GPU. En las secciones Tensorflow Name Scope y Tensorflow Ops , puede identificar diferentes partes del modelo, como el pase hacia adelante, la función de pérdida, el cálculo del gradiente / pase hacia atrás y la actualización del peso del optimizador. También puede ver las operaciones que se ejecutan en la GPU uno al lado del arroyo, que se refieren a CUDA arroyos. Cada secuencia se utiliza para tareas específicas. En esta traza, la secuencia n. ° 118 se usa para iniciar kernels de cómputo y copias de dispositivo a dispositivo. La secuencia # 119 se utiliza para la copia de host a dispositivo y la secuencia # 120 para la copia de dispositivo a host.

image

Este rastro muestra características comunes de un modelo de desempeño. Por ejemplo, la línea de tiempo de cómputo de la GPU ( flujo n. ° 118 ) parece ocupada con muy pocas brechas. Hay copias mínimas de host a dispositivo ( secuencia n. ° 119 ) y de dispositivo a host ( secuencia n. ° 120 ), así como espacios mínimos entre los pasos. Cuando ejecuta TensorFlow Profiler para su programa, es posible que no vea estas características ideales en su vista de seguimiento. El resto de esta guía cubre escenarios comunes y cómo solucionarlos.

Canalización de entrada de depuración

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 fácil de resolver esto es utilizar el TensorFlow perfiles de entrada-Pipeline analizador , lo que da una visión general de tiempo invertido en la tubería de entrada.

image

Las siguientes son acciones potenciales que puede tomar si su canal de entrada contribuye significativamente al tiempo de paso:

  • Consulte la guía específica de tf.data para aprender a depurar su canalización de entrada.
  • Otra forma rápida de comprobar si la canalización 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 ver un rendimiento similar con datos reales y con datos sintéticos / aleatorios generados. La única sobrecarga en el caso de datos sintéticos se deberá a la copia de datos de entrada que, de nuevo, se puede obtener previamente y optimizar.

Rendimiento de depuración de 1 GPU

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

Analizar brechas entre pasos

Una observación común cuando su programa no se está ejecutando de manera óptima son los intervalos entre los pasos de entrenamiento. En la imagen de abajo, hay una gran brecha 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á vinculado a la entrada. En ese caso, debe consultar la sección anterior sobre la 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 ver espacios entre el final de un paso y el comienzo de otro debido a la contención de subprocesos de la CPU. tf.data utiliza subprocesos en segundo plano para paralelizar el procesamiento de la canalización. 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 ve grandes lagunas 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 asegura que los núcleos de GPU se inicien desde sus propios subprocesos dedicados y no se pongan en cola detrás del trabajo de tf.data .

Las brechas entre los pasos también pueden deberse a cálculos de métricas, devoluciones de llamada 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 de ida y vuelta desde la GPU.

Si después de optimizar su canalización de entrada aún observa brechas entre los pasos en el visor de seguimiento, debe mirar el código del modelo entre los pasos y ver si deshabilitar las devoluciones de llamada / métricas mejora el rendimiento. Algunos detalles de estas operaciones también están en el visor de seguimiento (tanto del dispositivo como del host). La recomendación en este escenario es amortizar la sobrecarga de estas operaciones ejecutándolas después de un número fijo de pasos en lugar de cada paso. Cuando se utiliza el método de compile en la API tf.keras , la configuración de la tf.keras experimental_steps_per_execution hace esto automáticamente. Para bucles de entrenamiento personalizados, use tf.while_loop . tf.while_loop .

Consiga una mayor utilización de dispositivos

Pequeños núcleos de GPU y retrasos en el lanzamiento del núcleo 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) involucrada antes de que los núcleos se ejecuten realmente en la GPU. En un caso ideal, el host pone en cola suficientes kernels en la GPU de modo que la GPU pasa la mayor parte de su tiempo ejecutándose, en lugar de esperar a que el host ponga en cola más kernels.

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

image

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

image

Al iniciar muchas operaciones pequeñas en la GPU (como un complemento escalar, por ejemplo), es posible que el host no se mantenga al día con la GPU. La página de estadísticas de Tensorflow para el mismo perfil de TensorFlow muestra 126,224 operaciones Mul que demoran 2,77 segundos. Por lo tanto, cada kernel tiene aproximadamente 21,9 μs, que es muy pequeño (aproximadamente al mismo tiempo que la latencia de inicio) y puede provocar retrasos en el inicio del kernel del host.

image

Si su visor de seguimiento muestra muchas brechas pequeñas entre operaciones en la GPU como en la imagen de arriba, puede:

  • Concatenar tensores pequeños y utilizar operaciones vectorizadas o utilizar un tamaño de lote más grande para hacer que cada kernel lanzado haga más trabajo, lo que mantendrá la GPU ocupada por más tiempo.
  • Asegúrese de que está usando tf.function para crear gráficos TF y no ejecutando operaciones en un modo ansioso puro (Usar tf.keras.Model.compile automáticamente hace esto).
  • Fusionar núcleos con XLA. Para obtener más detalles, consulte la sección a continuación sobre cómo habilitar XLA para obtener un mayor rendimiento. Esta es una característica experimental, pero conduce a una alta utilización del dispositivo.
Colocación de operaciones de Tensorflow

La página de descripción general del generador de perfiles de TensorFlow le indica el porcentaje de operaciones ubicadas 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 que el porcentaje de operaciones en el host sea 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 averiguar a qué dispositivos se asignan las operaciones y tensores de 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 una operación se coloque 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 resultar en una ubicación más determinista de las operaciones en su dispositivo.

Una razón por la que la mayoría de las operaciones se colocan en la GPU es para evitar copias de memoria excesivas 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). Se puede ver un ejemplo de copia excesiva en la vista de seguimiento a continuación en los flujos de GPU # 167 , # 168 y # 169 .

image

Estas copias a veces 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 no siempre es fácil asociar una memCopy con una operación. En estos casos, es útil mirar las operaciones cercanas para ver si la copia de la memoria ocurre en la misma ubicación en cada paso.

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 kernels de la GPU utilizando Tensor Cores o fusionando operaciones.

Utilizando núcleos tensoriales

Las GPU modernas tienen núcleos tensoriales especializados que pueden mejorar significativamente el rendimiento de los núcleos elegibles. La página de estadísticas del kernel de GPU indica qué kernels de GPU son elegibles para Tensor Core y qué kernels usan Tensor Core. Habilitar fp16 (consulte la sección Habilitación de precisión mixta a continuación) es una forma de hacer que los kernels General Matrix Multiply (GEMM) de su programa (operaciones matmul) utilicen Tensor Core.

Para obtener otras recomendaciones detalladas sobre cómo hacer que los kernels sean eficientes para las GPU, consulte la guía NVIDIA Deep Learning Performance , que cubre una variedad de técnicas con las que puede experimentar, como usar el formato NCHW vs NHWC para representar entradas, o hacer que las dimensiones de entrada un múltiplo de 8.

Operaciones de fusión

Con la función tf.xla.experimental_compile , TensorFlow puede fusionar operaciones más pequeñas para formar núcleos más grandes que conducen a ganancias de rendimiento significativas. Se discuten más detalles en la sección Habilitar XLA a continuación.

Habilitar fp16 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 desempeño sean los esperados.

Habilitación de precisión mixta

La guía TensorFlow Mixed Precision muestra cómo habilitar la precisión fp16 en las GPU. Cuando se trata de darse cuenta de los beneficios de rendimiento de fp16, hay algunos consejos a tener en cuenta.

Uso de núcleos óptimos fp16

Con fp16 habilitado, los núcleos de multiplicación de matrices (GEMM) de su programa deben usar la versión correspondiente de fp16 que utiliza Tensor Cores. Sin embargo, en algunos casos, esto no sucede y no ve 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 la GPU muestra qué operaciones son elegibles para Tensor Core y qué kernels utilizan realmente 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 fp16 también se mostrarán en los núcleos que antes estaban limitados a la memoria, ya que ahora la operación tomará la mitad del tiempo.

Escalado de pérdida dinámico vs estático

La escala de pérdida es necesaria cuando se usa fp16 para evitar el subdesbordamiento debido a la baja precisión. Hay dos tipos de escalado de pérdidas, dinámico y estático, los cuales se explican con mayor detalle en la guía Precisión mixta . Al intentar optimizar el rendimiento, es importante recordar que el escalado de pérdidas dinámicas puede introducir operaciones condicionales adicionales que se ejecutan en el host y generar brechas que serán visibles entre los pasos en el visor de seguimiento. Por otro lado, el escalado de pérdida estática no tiene tales gastos generales y puede ser una mejor opción en términos de rendimiento con la trampa de que necesita especificar el valor correcto de escala de pérdida estática.

Habilitación de XLA

Como paso final para obtener el mejor rendimiento con una sola GPU, puede experimentar habilitando XLA, que fusionará las 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, consulte la guía XLA: Optimizing Compiler for Machine Learning .

Optimice el rendimiento en un solo host con varias GPU

La API tf.distribute.MirroredStrategy se puede utilizar para escalar el entrenamiento de modelos de 1 GPU a múltiples GPU en un solo host. Para obtener más información sobre cómo realizar entrenamiento distribuido con Tensorflow, consulte la guía Entrenamiento distribuido con Keras . Aunque la transición de una GPU a varias GPU idealmente debería ser escalable desde el primer momento, a veces puede encontrar problemas de rendimiento.

Al pasar del entrenamiento con una sola GPU a varias GPU en el mismo host, lo ideal sería que el rendimiento se escalara solo con la sobrecarga adicional de la comunicación de gradiente y una mayor utilización de subprocesos del host. Debido a esta sobrecarga, no verá una aceleración exacta de 2x si pasa 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. Existe una sobrecarga para concatenar los degradados, comunicarlos entre réplicas y dividirlos antes de realizar la actualización de peso.

image

La siguiente lista de verificación le 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 generador de perfiles 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 más alto puede afectar la convergencia, esto generalmente se ve compensado por los beneficios de rendimiento.
  2. Al pasar de una sola 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 tubería 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 de AllReduce no necesarias, 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 y vea si se pueden minimizar.
  5. Verifique el tiempo de paso para asegurarse de que cada réplica esté haciendo el mismo trabajo. Puede suceder que una GPU (normalmente GPU0) tenga una suscripción excesiva porque el host termine por error poniendo más trabajo en ella.
  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 de forma secuencial. Esto suele ocurrir cuando su programa incluye dependencias de control de una GPU a otra. El rendimiento de depuración en esta situación se ha resuelto caso por caso en el pasado. Si observa este comportamiento en su programa, presente un problema de Github con imágenes de su vista de seguimiento.

Optimizar degradado AllReduce

Cuando se entrena con una estrategia síncrona, cada dispositivo recibe una parte de los datos de entrada. Después de calcular las pasadas hacia adelante y hacia atrás a través del modelo, los gradientes calculados en cada dispositivo deben agregarse y reducirse. 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 en las capas del modelo, los comunica a través de 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 degradados reducidos para actualizar los pesos de su modelo. Idealmente, este proceso debería ocurrir al mismo tiempo en todas las GPU para evitar gastos generales. El tiempo para AllReduce debe ser aproximadamente el mismo que:

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

Este cálculo es útil como una comprobación rápida para comprender si el rendimiento que ve cuando ejecuta un trabajo de entrenamiento distribuido es el esperado o si necesita realizar una depuración de rendimiento adicional. Puede obtener el número de parámetros en su modelo de tf.keras.Model.summary .

Tenga en cuenta que cada parámetro del modelo tiene 4 bytes, ya que Tensorflow usa fp32 para comunicar gradientes. Incluso cuando haya habilitado fp16, NCCL AllReduce utiliza parámetros fp32. En el futuro, Tensorflow admitirá las operaciones de AllReduce mediante fp16, además de canalizar el gradiente AllReduce para que se superponga con el cálculo del gradiente.

Para ver los beneficios del escalado, el tiempo de paso debe ser mucho mayor en comparación con estos gastos generales. Una forma de lograr esto es utilizar un tamaño de lote más alto, ya que el tamaño de lote afecta el tiempo de paso, pero no afecta los gastos generales de comunicación.

Contención de subprocesos de host de GPU

Cuando se ejecutan varias GPU, el trabajo de la CPU es mantener ocupados todos los dispositivos mediante el lanzamiento eficiente de kernels 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 una GPU ocupada y luego lanzar kernels en otra GPU en un orden no determinista. . Esto puede provocar un sesgo o una escala negativa, lo que puede afectar el rendimiento.

El visor de seguimiento a continuación muestra la sobrecarga cuando la CPU se tambalea. El kernel de GPU se inicia de manera ineficiente, ya que GPU1 está inactivo y luego comienza a ejecutar operaciones después de que GPU2 haya comenzado.

image

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

image

Si ve este asombroso kernel de GPU en la vista de seguimiento de su programa, la acción recomendada es:

  • Configura la variable de entorno TF_GPU_THREAD_MODE de gpu_private 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, que es suficiente en la mayoría de los casos. Sin embargo, ese número se puede cambiar configurando la variable de entorno TF_GPU_THREAD_COUNT de TF_GPU_THREAD_COUNT en el número deseado de subprocesos.