O Dia da Comunidade de ML é dia 9 de novembro! Junte-nos para atualização de TensorFlow, JAX, e mais Saiba mais

Otimize o desempenho da GPU do TensorFlow com o TensorFlow Profiler

Visão geral

Use as otimizações neste guia para garantir que você obtenha o máximo desempenho de suas GPUs. Usando o TensorFlow Profiler como a principal ferramenta para obter insights sobre o desempenho, este guia o ajudará a depurar quando uma ou mais de suas GPUs são subutilizadas.

Leia o tutorial Profiler para saber mais sobre como começar a usar o Profiler. Além disso, leia o guia Profiler para saber mais sobre as várias ferramentas de perfis disponíveis e os vários métodos disponíveis para o desempenho TensorFlow optimize no host (CPU).

Lembre-se de que descarregar cálculos para a GPU nem sempre pode ser benéfico, principalmente para modelos pequenos. Existem sobrecargas devido à transferência de dados entre o host (CPU) e o dispositivo (GPU), bem como sobrecargas devido à latência envolvida quando o host inicia os kernels da GPU. O bom desempenho é obtido quando o host com êxito mantém a GPU ocupada, descarregando trabalho suficiente.

Fluxo de trabalho de otimização de desempenho

Este guia descreve como depurar problemas de desempenho começando com uma única GPU e, em seguida, passando para um único host com várias GPUs. Recomenda-se depurar problemas de desempenho nesta ordem. Por exemplo, se você estiver usando uma estratégia de distribuição do TensorFlow para treinar um modelo em um único host com várias GPUs e observar a utilização de GPU abaixo do ideal, primeiro otimize e depure o desempenho de 1 GPU antes de depurar o sistema multi-GPU. A ordem recomendada é a seguinte:

  1. Otimize e depure o desempenho em 1 GPU
    1. Verifique se o pipeline de entrada é um gargalo
    2. Desempenho de depuração de 1 GPU
    3. Habilite o fp16 e, opcionalmente, habilite o XLA
  2. Otimize e depure o desempenho em um único host multi-GPU

Como uma base para obter o código performance na GPU, este guia assume que você já está usando tf.function . O Keras compilação / ajuste API irá utilizar tf.function automaticamente sob o capô. Ao escrever um loop de formação personalizada, consulte este guia sobre como ativar tf.function .

As próximas seções discutem abordagens sugeridas para cada um dos cenários acima para ajudar a identificar e corrigir gargalos de desempenho.

Otimize o desempenho em 1 GPU

Em um caso ideal, seu programa deve ter alta utilização de GPU, comunicação mínima de CPU (host) para GPU (dispositivo) e nenhuma sobrecarga do pipeline de entrada. A primeira etapa na análise de desempenho é obter um perfil para um modelo executado com uma GPU.

A Visão geral página da TensorFlow Profiler fornece uma ideia de quão longe o seu programa é a partir do cenário ideal.

TensorFlow Profiler Overview Page

Os números-chave a procurar na página de visão geral são:

  1. Quanto do tempo da etapa é da execução real do dispositivo
  2. A porcentagem de operações colocadas no dispositivo versus host
  3. Quantos kernels usam fp16

Alcançar o desempenho ideal significa maximizar esses números em todos os três casos. Para se ter uma compreensão profunda do seu programa, você precisará estar familiarizado com o TensorFlow Profiler visualizador de rastreamento . As seções abaixo mostram alguns padrões comuns do visualizador de rastreamento que você deve procurar ao diagnosticar gargalos de desempenho.

Abaixo está uma imagem de uma visualização de rastreamento de modelo em execução em 1 GPU. Do âmbito Tensorflow Nome e Tensorflow Ops seções, é possível identificar diferentes partes do modelo, como o passe para frente, a função de perda, passe para trás / cálculo de gradiente, e a atualização de peso otimizador. Você também pode ver ops em execução no GPU ao lado de cada córrego, que se referem a CUDA córregos. Cada fluxo é usado para tarefas específicas. Neste traço, Stream # 118 é usado para lançar núcleos de computação e dispositivo para cópias de dispositivo. Transmitir # 119 é usado para hospedar a cópia do dispositivo e fluxo # 120 para dispositivo para cópia host.

O rastreamento abaixo mostra características comuns de um modelo de desempenho.

image

Por exemplo, a linha de tempo de computação GPU (Stream # 118) parece agitado com muito poucas lacunas. Existem cópias mínimas de acolhimento para o dispositivo (Stream # 119) e do dispositivo de acolhimento (Stream # 120), bem como aberturas mínimas entre etapas. Ao executar o TensorFlow Profiler para seu programa, você pode não ver essas características ideais em sua visualização de rastreamento. O restante deste guia cobre cenários comuns e como corrigi-los.

Depurar canal de entrada

A primeira etapa na depuração de desempenho da GPU é determinar se seu programa está vinculado à entrada. A maneira mais fácil de descobrir isso é usar do TensorFlow Profiler Input-Pipeline Analyzer , que dá uma visão geral do tempo gasto no pipeline de entrada.

image

A seguir estão as possíveis ações que você pode realizar se o pipeline de entrada contribuir significativamente para o tempo da etapa:

  • Consulte a tf.data específica guia para aprender como depurar seu pipeline de entrada.
  • Outra maneira rápida de verificar se o pipeline de entrada é o gargalo é usar dados de entrada gerados aleatoriamente que não precisam de nenhum pré-processamento. Aqui está um exemplo do uso desta técnica para um modelo ResNet. Se o pipeline de entrada for ideal, você verá um desempenho semelhante com dados reais e dados aleatórios / sintéticos gerados. A única sobrecarga no caso de dados sintéticos será devido à cópia de dados de entrada que, novamente, pode ser pré-buscada e otimizada.

Ver também a orientação aqui .

Desempenho de depuração de 1 GPU

Existem vários fatores que podem contribuir para a baixa utilização da GPU. Abaixo estão alguns cenários comumente observados ao olhar para o visualizador de rastreamento e soluções potenciais.

Analise as lacunas entre as etapas

Uma observação comum quando seu programa não está funcionando de maneira ideal são as lacunas entre as etapas de treinamento. Na imagem abaixo, há uma grande lacuna entre as etapas 8 e 9, o que significa que a GPU está ociosa durante esse tempo.

image

Se seu visualizador de rastreamento mostrar grandes lacunas entre as etapas, isso pode ser uma indicação de que seu programa está vinculado à entrada. Nesse caso, você deve consultar a seção anterior sobre depuração do pipeline de entrada, caso ainda não tenha feito isso. No entanto, mesmo com um pipeline de entrada otimizado, você ainda pode ver lacunas entre o final de uma etapa e o início de outra devido à contenção de thread da CPU. tf.data faz uso de threads em segundo plano para paralelizar processamento pipeline. Esses threads podem interferir na atividade do lado do host da GPU que acontece no início de cada etapa, como copiar dados ou agendar operações de GPU.

Se você ver grandes lacunas no lado do anfitrião, que horários esses PO com GPU, você pode definir a variável de ambiente TF_GPU_THREAD_MODE=gpu_private . Isso garante que kernels GPU são lançados a partir de suas próprias linhas dedicadas, e não ficar na fila atrás de tf.data trabalho.

Lacunas entre etapas também pode ser causada por cálculos métricas, callbacks Keras, ou ops fora do tf.function que são executados no host. Essas operações não têm um desempenho tão bom quanto as operações dentro de um gráfico do TensorFlow. Além disso, alguns desses ops são executados na CPU e copiam tensores para frente e para trás da GPU.

Se, depois de otimizar seu pipeline de entrada, você ainda perceber lacunas entre as etapas no visualizador de rastreamento, deverá examinar o código do modelo entre as etapas e ver se a desativação de callbacks / métricas melhora o desempenho. Alguns detalhes dessas operações também estão no visualizador de rastreamento (no dispositivo e no host). A recomendação neste cenário é amortizar a sobrecarga dessas operações executando-as após um número fixo de etapas em vez de cada etapa. Ao usar a compile método na tf.keras API, definindo o experimental_steps_per_execution bandeira faz isso automaticamente. Loops de treinamento personalizado, use tf.while_loop .

Alcance maior utilização do dispositivo

Pequenos Kernels de GPU e Atrasos de inicialização do Kernel do Host

O host enfileira os kernels para serem executados na GPU, mas há uma latência (cerca de 20-40 μs) envolvida antes que os kernels sejam realmente executados na GPU. Em um caso ideal, o host enfileira kernels suficientes na GPU de forma que a GPU passe a maior parte do tempo executando, em vez de esperar que o host enfileire mais kernels.

De O TensorFlow Profiler visão geral da página mostra quanto tempo a GPU foi devido ocioso à espera no host para kernels de lançamento. Na imagem abaixo, a GPU fica ociosa por cerca de 10% do tempo da etapa esperando que os kernels sejam lançados.

image

O visualizador de rastreamento para este mesmo programa mostra pequenos intervalos entre os kernels onde o host está ocupado lançando kernels na GPU.

image

Ao lançar vários pequenos ops na GPU (como um acréscimo escalar, por exemplo), o host pode não acompanhar a GPU. O Tensorflow Estatísticas página para o mesmo TensorFlow mostra Perfil 126,224 operações Mul tomar 2,77 segundos. Portanto, cada kernel tem cerca de 21,9 μs, o que é muito pequeno (quase ao mesmo tempo que a latência de inicialização) e pode resultar em atrasos na inicialização do kernel do host.

image

Se o visualizador de rastreamento mostrar muitas pequenas lacunas entre as operações na GPU, como na imagem acima, você pode:

  • Concatene pequenos tensores e use operações vetorizadas ou use um tamanho de lote maior para fazer com que cada kernel iniciado trabalhe mais, o que manterá a GPU ocupada por mais tempo.
  • Certifique-se de que você está usando tf.function para criar gráficos TF e não correr ops em um modo ansioso puro. Usando tf.keras.Model.compile faz isso automaticamente.
  • Fundir kernels usando XLA. Para mais detalhes, consulte a seção abaixo sobre como habilitar XLA para obter maior desempenho. Este é um recurso experimental, mas leva a uma alta utilização do dispositivo.
Posicionamento de operações do Tensorflow

O TensorFlow Profiler Overview página informa a porcentagem de ops colocado no host vs dispositivo (você também pode verificar a colocação de ops específicos, olhando para o visualizador de rastreamento). Como na imagem abaixo, você deseja que a porcentagem de operações no host seja muito pequena em comparação com o dispositivo.

image

Idealmente, a maioria das operações de computação intensiva deve ser colocada na GPU. Para descobrir quais dispositivos as operações e tensores no seu modelo são atribuídos a, conjunto tf.debugging.set_log_device_placement(True) como a primeira instrução do seu programa. Note-se que, em alguns casos, mesmo se você especificar um op para ser colocado em um determinado dispositivo, a sua implementação pode substituir essa condição (exemplo: tf.unique ). Mesmo para formação GPU único, especificando uma estratégia de distribuição, tais como tf.distribute.OneDeviceStrategy , pode resultar em uma colocação mais determinista do ops no seu dispositivo.

Uma razão para ter a maioria das operações colocadas na GPU é evitar cópias de memória excessivas entre o host e o dispositivo (são esperadas cópias de memória para dados de entrada / saída do modelo entre o host e o dispositivo). Um exemplo de cópia em excesso pode ser visto na vista em traço abaixo na GPU fluxos # 167, # 168 e # 169.

image

Essas cópias às vezes podem prejudicar o desempenho se bloquearem a execução dos kernels da GPU. As operações de cópia de memória no visualizador de rastreamento têm mais informações sobre os ops que são a fonte desses tensores copiados, mas nem sempre é fácil associar um memCopy a um op. Nesses casos, é útil olhar as operações próximas para ver se a cópia da memória acontece no mesmo local em todas as etapas.

Kernels mais eficientes em GPUs

Uma vez que a utilização da GPU do seu programa seja aceitável, a próxima etapa é olhar para aumentar a eficiência dos kernels da GPU utilizando Tensor Cores ou fusing ops.

Utilizar núcleos tensores

As GPUs modernas têm núcleos tensores especializados que podem melhorar significativamente o desempenho dos kernels elegíveis. A página de estatísticas do kernel GPU indica que kernels GPU são Tensor Núcleo elegíveis e que kernels estão usando a Tensor Core. Permitindo fp16 (ver secção Mixed Precision abaixo Enabling) é uma maneira de fazer do seu programa Matrix Geral Multiply (GEMM) kernels (ops matmul) utilizar o Tensor Core. Kernels GPU usar os núcleos tensor eficientemente quando a precisão é FP16 e dimensões tensor de entrada / saída são divisível por 8 ou 16 (para int8 ).

Para outras recomendações detalhadas sobre como fazer kernels eficiente para GPUs, consulte a NVIDIA profunda Desempenho de Aprendizagem guia.

Fuse ops

Use tf.xla.experimental_compile para fundir ops menores para formar núcleos maiores que levam a ganhos significativos de desempenho.

Habilitar precisão mista e XLA

Depois de seguir as etapas acima, ativar a precisão mista e o XLA são duas etapas opcionais que você pode seguir para melhorar ainda mais o desempenho. A abordagem sugerida é habilitá-los um por um e verificar se os benefícios de desempenho são os esperados.

Habilitar precisão mista

Os TensorFlow precisão Mixed guia mostra como habilitar fp16 precisão em GPUs. Ativar AMP em GPUs NVIDIA® usar Tensor Cores e perceber até 3x speedups geral quando comparado ao uso apenas fp32 precisão na Volta e arquiteturas de GPU mais recentes.

Certifique-se de que as dimensões da matriz / tensor atendam aos requisitos para chamar kernels que usam núcleos do Tensor. Os núcleos da GPU usam os núcleos do Tensor com eficiência quando a precisão é fp16 e as dimensões de entrada / saída são divisíveis por 8 ou 16 (para int8). Observe que com cuDNN v7.6.3 e posterior, as dimensões de convolução serão preenchidas automaticamente quando necessário para aproveitar os núcleos do Tensor.

Siga as melhores práticas a seguir para maximizar os benefícios de desempenho fp16 precisão.

Use Kernels fp16 ideais

Com fp16 habilitado, produto de matrizes de seu programa (GEMM) kernels, deve usar o correspondente fp16 versão que utiliza os núcleos Tensor. No entanto, em alguns casos, isso não acontece e você não vê a aceleração esperada de permitindo fp16 , como seu programa cai de volta para a implementação ineficiente em seu lugar.

image

O núcleo GPU estatísticas mostra página que ops são elegíveis e que kernels Tensor do núcleo estão realmente usando o eficiente Tensor Core. O guia NVIDIA no desempenho de aprendizagem profunda contém sugestões adicionais sobre como alavancar Tensor Cores. Além disso, os benefícios da utilização fp16 também irá mostrar em kernels que antes eram de memória ligada, como agora os ops levará metade do tempo.

Escala de perda estática vs dinâmica

Perda de escala é necessário quando se utiliza fp16 para evitar estouro negativo devido à baixa precisão. Existem dois tipos de perda de escala, dinâmica e estática, ambas as quais são explicadas em maior detalhe no guia Precision Mixed . Você pode usar o mixed_float16 política para ativar automaticamente escala perda dentro do otimizador Keras.

Ao tentar otimizar o desempenho, é importante lembrar que o dimensionamento de perda dinâmica pode introduzir operações condicionais adicionais que são executadas no host e levar a lacunas que serão visíveis entre as etapas no visualizador de rastreamento. Por outro lado, o escalonamento de perda estática não tem tais sobrecargas e pode ser uma opção melhor em termos de desempenho com a captura de que você precisa para especificar o valor de escala de perda estática correto.

Habilitar XLA

Como uma etapa final para obter o melhor desempenho com uma única GPU, você pode experimentar habilitar o XLA, o que fundirá as operações e levará a uma melhor utilização do dispositivo e menor consumo de memória. Para obter detalhes sobre como ativar o XLA em seu programa, consulte o guia .

Você pode definir o nível de JIT global para -1 (off), 1 ou 2 . Um nível mais alto é mais agressivo e pode reduzir o paralelismo e usar mais memória. Defina o valor para 1 se você tiver restrições de memória. Observe que o XLA não tem um bom desempenho para modelos com formas de tensor de entrada variável, pois o compilador XLA teria que continuar compilando kernels sempre que encontrasse novas formas.

Otimize o desempenho em um único host multi-GPU

O tf.distribute.MirroredStrategy API pode ser usado para treinamento do modelo escala de 1 GPU para várias GPUs em um único host. Para saber mais sobre como fazer o treinamento distribuído com Tensorflow, consulte o treinamento distribuída com Keras guia. Embora a transição de uma GPU para várias GPUs deva, idealmente, ser escalonável fora da caixa, às vezes você pode encontrar problemas de desempenho.

Ao passar do treinamento com uma única GPU para várias GPUs no mesmo host, o ideal é que você veja o escalonamento de desempenho apenas com a sobrecarga adicional de comunicação gradiente e maior utilização de thread de host. Por causa dessa sobrecarga, você não verá uma aceleração exata de 2x se passar de 1 para 2 GPUs, por exemplo. A visualização de rastreamento abaixo mostra um exemplo da sobrecarga de comunicação extra durante o treinamento em várias GPUs. Há alguma sobrecarga para concatenar os gradientes, comunicá-los entre réplicas e dividi-los antes de fazer a atualização de peso.

image

A lista de verificação a seguir o ajudará a obter um melhor desempenho ao otimizar o desempenho no cenário multi-GPU:

  1. Tente maximizar o tamanho do lote, o que levará a uma maior utilização do dispositivo e amortizará os custos de comunicação entre várias GPUs. Usando o profiler de memória ajuda a ter uma noção de quão perto o seu programa é a utilização de memória de pico. Observe que, embora um tamanho de lote maior possa afetar a convergência, isso geralmente é superado pelos benefícios de desempenho.
  2. Ao passar de uma única GPU para várias GPUs, o mesmo host agora precisa processar muito mais dados de entrada. Portanto, após (1), é recomendável verificar novamente o desempenho do pipeline de entrada e certificar-se de que não seja um gargalo.
  3. Verifique a linha do tempo da GPU na visualização de rastreamento do programa para ver se há chamadas AllReduce desnecessárias, pois isso resulta em uma sincronização em todos os dispositivos. Na visualização de rastreamento mostrada acima, o AllReduce é feito por meio do kernel NCCL e há apenas uma chamada NCCL em cada GPU para os gradientes em cada etapa.
  4. Verifique se há operações de cópia D2H, H2D e D2D desnecessárias e veja se elas podem ser minimizadas.
  5. Verifique o tempo da etapa para certificar-se de que cada réplica está fazendo o mesmo trabalho. Pode acontecer que uma GPU (normalmente GPU0) esteja sobrecarregada porque o host por engano acaba colocando mais trabalho nela.
  6. Por último, verifique a etapa de treinamento em todas as GPUs em sua visualização de rastreamento para todas as operações que estão sendo executadas sequencialmente. Isso geralmente acontece quando seu programa inclui dependências de controle de uma GPU para outra. O desempenho de depuração nessa situação foi resolvido caso a caso no passado. Se você observar esse comportamento em seu programa, apresentar uma questão Github com imagens de sua visão vestígio.

Optimize Gradient AllReduce

Ao treinar com uma estratégia síncrona, cada dispositivo recebe uma parte dos dados de entrada. Depois de calcular as passagens para frente e para trás no modelo, os gradientes calculados em cada dispositivo precisam ser agregados e reduzidos. Este gradiente AllReduce ocorre após o cálculo do gradiente em cada dispositivo e antes que o otimizador atualize os pesos do modelo. Cada GPU primeiro encadeia os gradientes entre as camadas do modelo, comunica-os através GPUs usando tf.distribute.CrossDeviceOps ( tf.distribute.NcclAllReduce é o padrão), e, em seguida, retorna os gradientes após redução por camada. O otimizador usará esses gradientes reduzidos para atualizar os pesos do seu modelo. Idealmente, esse processo deve acontecer ao mesmo tempo em todas as GPUs para evitar sobrecargas. O tempo para AllReduce deve ser aproximadamente o mesmo que:

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

Este cálculo é útil como uma verificação rápida para entender se o desempenho que você vê ao executar um job de treinamento distribuído é o esperado ou se você precisa fazer mais depuração de desempenho. Você pode obter o número de parâmetros em seu modelo de tf.keras.Model.summary .

Observe que cada parâmetro do modelo tem 4 bytes, já que o Tensorflow usa fp32 para comunicar gradientes. Mesmo quando você habilitou o fp16, o NCCL AllReduce utiliza os parâmetros do fp32. No futuro, o Tensorflow oferecerá suporte a operações AllReduce usando fp16, bem como canalizará o gradiente AllReduce para que ele se sobreponha ao cálculo do gradiente.

Para ver os benefícios do dimensionamento, o tempo de etapa precisa ser muito maior em comparação com essas despesas gerais. Uma maneira de conseguir isso é usar um tamanho de lote maior, pois o tamanho do lote afeta o tempo da etapa, mas não afeta a sobrecarga de comunicação.

Contenção de thread de host de GPU

Ao executar várias GPUs, o trabalho da CPU é manter todos os dispositivos ocupados, iniciando de forma eficiente os kernels da GPU em todos os dispositivos. No entanto, quando há muitas operações independentes que a CPU pode agendar em uma GPU, a CPU pode decidir usar muitos de seus threads de host para manter uma GPU ocupada e, em seguida, lançar kernels em outra GPU em uma ordem não determinística . Isso pode causar uma inclinação ou escala negativa, o que pode prejudicar o desempenho.

O visualizador de rastreamento abaixo mostra a sobrecarga quando a CPU balança o kernel da GPU de forma ineficiente, pois a GPU1 está ociosa e começa a executar as operações depois que a GPU2 foi iniciada.

image

A visualização de rastreamento para o host mostra que o host está iniciando kernels na GPU2 antes de lançá-los na GPU1 (observe que as operações tf_Compute * abaixo não são indicativas de threads de CPU).

image

Se você observar este escalonamento de kernels de GPU na visualização de rastreamento do seu programa, a ação recomendada é:

  • Defina a variável de ambiente TensorFlow TF_GPU_THREAD_MODE para gpu_private . Esta variável de ambiente dirá ao host para manter os threads de uma GPU privada.
  • Por padrão, TF_GPU_THREAD_MODE=gpu_private define o número de threads a 2, o que é suficiente na maioria dos casos. No entanto, este número pode ser alterado ajustando o ambiente variável TensorFlow TF_GPU_THREAD_COUNT para o número desejado de segmentos.