¡Google I/O es una envoltura! Póngase al día con las sesiones de TensorFlow Ver sesiones

Optimice el rendimiento de la GPU de 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 es nuevo en Profiler:

Tenga en cuenta que la descarga de cálculos a la GPU puede no ser siempre beneficiosa, 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 lanza kernels 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 múltiples GPU.

Se recomienda depurar problemas de rendimiento en el siguiente orden:

  1. Optimice y depure el rendimiento en una GPU:
    1. Compruebe si la canalización de entrada es un cuello de botella.
    2. Depura el rendimiento de una GPU.
    3. Habilitar precisión mixto (con fp16 (float16)) y, opcionalmente, permitir XLA .
  2. Optimice y depure el rendimiento en el host único de varias GPU.

Por ejemplo, si está utilizando un TensorFlow estrategia de distribución para entrenar un modelo en un único host con múltiples GPU y la utilización de la GPU aviso subóptima, se debe en primer lugar a optimizar y depurar el rendimiento de una GPU antes de depurar el sistema multi-GPU.

Como línea de base para obtener el código performant en GPU, esta guía asume que ya está utilizando tf.function . El Keras Model.compile y Model.fit APIs utilizarán tf.function automáticamente bajo el capó. Al escribir un bucle de formación a medida con tf.GradientTape , consulte el rendimiento mejor con tf.function sobre cómo activar tf.function s.

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.

1. Optimiza el rendimiento en una GPU

En un caso ideal, su programa debe tener una alta utilización de la GPU, una comunicación mínima de la CPU (el host) a la GPU (el dispositivo) y sin gastos generales 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.

Profiler de TensorBoard página de información general -que muestra una vista de nivel superior de cómo realiza su modelo durante un perfil de ejecución puede dar una idea de lo lejos que su programa es el escenario ideal.

TensorFlow Profiler Overview Page

Los números clave para prestar atención a 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 núcleos usan fp16

Lograr un rendimiento óptimo significa maximizar estos números en los tres casos. Para tener una comprensión profunda de su programa, que tendrá que estar familiarizado con el Perfil de TensorBoard visor de seguimiento . 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 una GPU. Alcance de la TensorFlow Nombre y TensorFlow secciones de Operaciones, se puede identificar diferentes partes del modelo, como el pase hacia adelante, la función de pérdida, pase hacia atrás / cálculo del gradiente, y la actualización de peso optimizador. También puede hacer que 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, Corriente # 118 se utiliza para poner en marcha los núcleos de cómputo y copia de dispositivo a dispositivo. Arroyo # 119 se utiliza para la copia de host a dispositivo y Corriente # 120 para el dispositivo de copia de acogida.

La siguiente traza muestra las características comunes de un modelo de desempeño.

image

Por ejemplo, la línea de tiempo de cómputo GPU (Corriente nº 118) ve "ocupado" con muy pocos huecos. Hay copias mínimas de anfitrión a dispositivo (corriente # 119) y de un dispositivo a host (corriente # 120), así como las lagunas mínimas entre 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. Depura 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 fácil de resolver esto es utilizar el Perfil del analizador de entrada-ducto , en TensorBoard, que proporciona una visión general de tiempo invertido en la tubería de entrada.

image

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

  • Se puede utilizar el tf.data específica de guía para aprender cómo depurar su tubería 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. Este es un ejemplo de 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 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.

Además, se refieren a las mejores prácticas para la optimización de la tubería de entrada de datos .

2. Depura el rendimiento de una GPU

Hay varios factores que pueden contribuir a una baja utilización de la GPU. A continuación se presentan algunos escenarios observados comúnmente cuando se mira en el visor de seguimiento y las posibles soluciones.

1. Analice las brechas entre los 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 la vista de seguimiento a continuación, 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 haber espacios entre el final de un paso y el comienzo de otro debido a la contención de subprocesos de la CPU. tf.data hace uso de hilos de fondo para paralelizar procesamiento en paralelo. 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 que las grandes lagunas en el lado del anfitrión, que estos horarios ops en la GPU, se puede establecer la variable de entorno TF_GPU_THREAD_MODE=gpu_private . Esto asegura que los núcleos de la GPU son lanzados desde sus propios hilos dedicados, y no quedar en cola detrás de tf.data trabajo.

Los espacios entre pasos también pueden ser causadas por cálculos métricos, las devoluciones de llamada, o Keras operaciones fuera del 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 todavía nota brechas 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 llamada / métricas mejora el rendimiento. Algunos detalles de estas operaciones también están 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 un número fijo de pasos en lugar de cada paso. Cuando se utiliza la compile método en el tf.keras API, estableciendo el experimental_steps_per_execution bandera hace esto automáticamente. Para bucles de formación personalizada, el uso tf.while_loop .

2. Consiga una mayor utilización del dispositivo

1. Pequeños kernels de GPU 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) 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.

Del Profiler página de información general en programas de TensorBoard cuánto tiempo la GPU era inactivo debido a la espera en el host a los núcleos de lanzamiento. En la imagen a continuación, la GPU está inactiva durante aproximadamente el 10% del tiempo de paso esperando a que se inicien los kernels.

image

El visor de seguimiento para este mismo programa de espectáculos pequeños espacios entre los núcleos donde el anfitrión está ocupado núcleos de lanzamiento de 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. El TensorFlow Stats herramienta en TensorBoard por las mismas perfil muestra 126,224 operaciones Mul teniendo 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 sus Visor de seguimiento muestra muchos pequeños espacios entre ops en la GPU como en la imagen anterior, se puede:

  • Concatenar tensores pequeños y usar operaciones vectorizadas o usar un tamaño de lote más grande para que cada kernel lanzado haga más trabajo, lo que mantendrá ocupada la GPU por más tiempo.
  • Asegúrese de que está utilizando tf.function para crear gráficos TensorFlow, por lo que no se está ejecutando operaciones en un modo ansioso puro. Si está utilizando Model.fit (como se oponen a un circuito de entrenamiento personalizado con tf.GradientTape ), entonces tf.keras.Model.compile lo hará automáticamente para usted.
  • Fusible núcleos usando XLA con tf.function(jit_compile=True) o auto-agrupamiento. Para más detalles, vaya a la Habilitar precisión mixta y XLA sección de abajo 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

El Profiler Descripción de página le muestra el porcentaje de operaciones colocado en el huésped contra el dispositivo (también se puede verificar la colocación de operaciones específicas por mirar el visor de seguimiento . Al igual que en la imagen de abajo, que desea que 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 qué dispositivos tensores de las operaciones y en su modelo se asignan a, juego tf.debugging.set_log_device_placement(True) como la primera declaración de su programa.

Tenga en cuenta que en algunos casos, incluso si se especifica un artículo para ser colocado en un dispositivo en particular, su aplicación podría anular esta condición (ejemplo: tf.unique ). Incluso para la formación de la GPU única, que especifica una estrategia de distribución, tales como tf.distribute.OneDeviceStrategy , puede resultar en una colocación más determinista de operaciones en el dispositivo.

Una razón para tener la mayoría de las operaciones colocadas en la GPU es 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). Un ejemplo de copiado excesiva se demuestra en la vista de traza a continuación en GPU corrientes # 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. Memoria de las operaciones de copia en el Visor de seguimiento tienen más información sobre las operaciones que son la fuente de estos tensores copiados, pero no siempre será fácil asociar un memcopy con un op. En estos casos, es útil mirar las operaciones cercanas para verificar si la copia de memoria ocurre en la misma ubicación en cada paso.

3. Núcleos 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.

1. Utilice núcleos tensoriales

Moderna GPU NVIDIA® han especializado Tensor Núcleos que pueden mejorar significativamente el rendimiento de granos elegibles.

Se puede utilizar de TensorBoard estadísticas GPU núcleo de visualizar el cual los núcleos de la GPU son Tensor-Core admisibles y que los núcleos están utilizando Tensor Cores. Permitiendo fp16 (consulte Activación de la sección mixta de precisión más adelante) es una manera de hacer de su programa general Matriz Multiplicar (GEMM) granos (ops matmul) utilizar el tensor Core. Núcleos GPU utilizan los núcleos de tensor de manera eficiente cuando la precisión es fp16 y dimensiones tensor de entrada / salida son divisibles por 8 o 16 (para int8 ).

Para otras recomendaciones detalladas sobre cómo hacer kernels eficiente para la GPU, consulte la NVIDIA® profunda rendimiento del aprendizaje guía.

2. Operaciones de fusibles

Uso tf.function(jit_compile=True) para fusionar operaciones más pequeñas para formar núcleos más grandes que conducen a ganancias significativas en el rendimiento. Para obtener más información, refiérase a la XLA guía.

3. Habilite 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 desempeño sean los esperados.

1. Habilite la precisión mixta

Los TensorFlow Mixta de precisión de guía muestra cómo habilitar fp16 precisión en la GPU. Habilitar AMP en las GPU NVIDIA® utilizar Tensor Núcleos y darse cuenta de hasta 3x aceleraciones general en comparación con el uso de solo fp32 precisión (float32) en Volta y nuevas arquitecturas de GPU.

Asegúrate de que las dimensiones de la matriz / tensor satisfagan los requisitos para llamar a los kernels que usan Tensor Cores. Los kernels de GPU usan los núcleos Tensor 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 los núcleos tensores.

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

1. Utilice kernels óptimos fp16

Con fp16 habilitado, multiplicaciones de matrices de su programa (GEMM), los núcleos deben utilizar el correspondiente fp16 versión que utiliza los núcleos de tensor. Sin embargo, en algunos casos, esto no sucede y que no experimentan la aceleración esperada de permitir fp16 , como el programa vuelve a caer a la aplicación ineficaz en su lugar.

image

El núcleo de la GPU estadísticas muestra páginas que ops son elegibles y que los núcleos Tensor Core se utilizan realmente la eficiencia Tensor Core. La guía en el rendimiento NVIDIA® aprendizaje profundo contiene sugerencias adicionales sobre cómo aprovechar Tensor Cores. Además, los beneficios de la utilización fp16 también mostrará en los granos que antes eran de memoria obligado, como ahora las operaciones se llevarán a la mitad del tiempo.

2. Escalado de pérdidas dinámicas frente a estáticas

Escalamiento pérdida es necesario cuando se utiliza fp16 para evitar desbordamiento debido a la baja precisión. Hay dos tipos de pérdida de escala, dinámica y estática, los cuales se explican con mayor detalle en la guía de precisión mixta . Se puede utilizar el mixed_float16 política para habilitar automáticamente la escala de pérdida dentro del optimizador Keras.

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érdidas estáticas 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 la 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á las operaciones y conducirá a una mejor utilización del dispositivo y una menor huella de memoria. Para más detalles sobre cómo habilitar XLA en su programa con tf.function(jit_compile=True) o auto-agrupación, consulte la XLA guía.

Se puede establecer el nivel global para JIT -1 (apagado), 1 , o 2 . Un nivel más alto es más agresivo y puede reducir el paralelismo y usar 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 de XLA tendría que seguir compilando núcleos cada vez que encuentre nuevas formas.

2. Optimice el rendimiento en el host único de varias GPU

El tf.distribute.MirroredStrategy API se puede utilizar para la formación de un modelo a escala de la GPU a varias GPU en un único host. (Para obtener más información sobre cómo hacer el entrenamiento distribuido con TensorFlow, se refieren a la formación distribuida con TensorFlow , uso de una GPU y Uso TPU guías y la formación distribuida con Keras tutorial.)

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 es que experimente el escalado del rendimiento con solo la sobrecarga adicional de la comunicación de gradiente y una mayor utilización de subprocesos del host. Debido a esta sobrecarga, no tendrá una aceleración exacta del doble 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 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. Utilizando el visor de memoria ayuda a tener una idea de lo cerca que su programa es el de la utilización de memoria pico. 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 sola GPU a varias GPU, el mismo host ahora tiene que procesar muchos más datos de entrada. Por lo tanto, 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 detectar cualquier llamada innecesaria a AllReduce, ya que esto da como resultado una sincronización en todos los dispositivos. En la vista de traza se muestra arriba, la AllReduce se realiza mediante el NCCL kernel, y sólo hay una llamada NCCL en cada GPU para los gradientes de cada paso.
  4. Compruebe si hay operaciones de copia D2H, H2D y D2D innecesarias que puedan minimizarse.
  5. Verifique el tiempo de paso para asegurarse de que cada réplica esté haciendo el mismo trabajo. Por ejemplo, puede suceder que una GPU (por lo general, GPU0 ) se suscribe en exceso porque el anfitrión termina por error poner más trabajo en él.
  6. Por último, verifique el paso de entrenamiento en todas las GPU en su vista de seguimiento para ver si hay operaciones que se ejecutan secuencialmente. Esto suele ocurrir 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 se observa este comportamiento en su programa, presentar un problema de GitHub con imágenes de la vista rastro.

1. Optimizar gradiente 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 sucede después de la de cálculo de gradiente en cada dispositivo, y antes de que el optimizador actualiza los pesos del modelo.

Cada GPU concatena primero los gradientes a través de las capas del modelo, ellos se comunica a través de las GPU utilizando 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 verificación rápida para comprender si el rendimiento que tiene al ejecutar un trabajo de entrenamiento distribuido es el esperado, o si necesita hacer más depuración de rendimiento. Usted puede obtener el número de parámetros en el modelo de Model.summary .

Tenga en cuenta que cada parámetro del modelo es de 4 bytes en tamaño desde TensorFlow utiliza fp32 (float32) para comunicarse gradientes. Incluso cuando se han fp16 permitido, NCCL AllReduce utiliza fp32 parámetros.

Para obtener 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 usar un tamaño de lote más alto, ya que el tamaño de lote afecta el tiempo de paso, pero no afecta la sobrecarga de comunicación.

2. 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 negativamente al rendimiento.

El visor de seguimiento de abajo muestra la sobrecarga cuando la CPU se tambalea lanzamientos del kernel GPU forma ineficiente, como GPU1 está inactivo y luego se pone en marcha después de operaciones GPU2 ha comenzado.

image

La vista de rastreo para los host muestra que el anfitrión es el lanzamiento de granos en GPU2 antes de lanzarlos sobre GPU1 (nota que el siguiente tf_Compute* ops no son indicativos de hilos de CPU).

image

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

  • Establecer la variable de entorno TensorFlow TF_GPU_THREAD_MODE a gpu_private . Esta variable de entorno le indicará al host que mantenga privados los subprocesos de una GPU.
  • Por defecto, TF_GPU_THREAD_MODE=gpu_private establece el número de hilos a 2, que es suficiente en la mayoría de los casos. Sin embargo, ese número puede ser cambiado por ajuste de la variable de entorno TensorFlow TF_GPU_THREAD_COUNT para el número deseado de hilos.