Otimize o desempenho da GPU do TensorFlow com o TensorFlow Profiler

Visão geral

Este guia mostrará como usar o TensorFlow Profiler com TensorBoard para obter insights e obter o máximo desempenho de suas GPUs e depurar quando uma ou mais de suas GPUs são subutilizadas.

Se você for novo no Profiler:

Lembre-se de que descarregar cálculos para a GPU nem sempre pode ser benéfico, principalmente para modelos pequenos. Pode haver sobrecarga devido a:

  • Transferência de dados entre o host (CPU) e o dispositivo (GPU); e
  • Devido à latência envolvida quando o host inicia os kernels da GPU.

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 na seguinte ordem:

  1. Otimize e depure o desempenho em uma GPU:
    1. Verifique se o pipeline de entrada é um gargalo.
    2. Depure o desempenho de uma GPU.
    3. Activar precisão misto (com fp16 (float16)) e, opcionalmente, activar XLA .
  2. Otimize e depure o desempenho no host único multi-GPU.

Por exemplo, se você estiver usando um TensorFlow estratégia de distribuição para treinar um modelo em um único host com múltiplas GPUs e utilização de GPU aviso abaixo do ideal, você deve primeiro otimizar e depurar o desempenho de uma GPU antes de depuração do sistema de multi-GPU.

Como uma base para obter o código performance em GPUs, este guia assume que você já está usando tf.function . O Keras Model.compile e Model.fit APIs irá utilizar tf.function automaticamente sob o capô. Ao escrever um loop de treinamento personalizado com tf.GradientTape , referem-se ao desempenho melhor com tf.function sobre como ativar tf.function s.

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

1. Otimize o desempenho em uma GPU

Em um caso ideal, seu programa deve ter alta utilização de GPU, comunicação mínima de CPU (o host) para GPU (o dispositivo) e nenhuma sobrecarga do pipeline de entrada.

A primeira etapa na análise do desempenho é obter um perfil para um modelo rodando com uma GPU.

Profiler de TensorBoard página de visão geral -que mostra uma vista de topo de como seu modelo realizada durante um perfil run-pode fornecer uma idéia de quão longe o seu programa é a partir do cenário ideal.

TensorFlow Profiler Overview Page

Os números-chave para prestar atenção à 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. Quantas kernels usar 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 Profiler de TensorBoard 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 uma 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 ter o 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 rastreio, o fluxo # 118 é usado para lançar os kernels de computação e cópias dispositivo-a-dispositivo. Transmitir # 119 é utilizado para cópia host-to-device and Stream nº 120 de dispositivo para cópia host.

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

image

Por exemplo, a linha do tempo GPU computação (Stream # 118) parece "ocupado" 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. Quando você executa o Profiler para seu programa, pode não ser capaz de identificar essas características ideais em sua visualização de rastreamento. O restante deste guia cobre cenários comuns e como corrigi-los.

1. Depure o pipeline de entrada

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

image

Você pode realizar as seguintes ações possíveis se o pipeline de entrada contribuir significativamente para o tempo da etapa:

  • Você pode usar o tf.data espec�ico 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ê deverá experimentar 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.

Além disso, referem-se às melhores práticas para otimizar o pipeline de dados de entrada .

2. Depure o desempenho de uma GPU

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

1. 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 da visualização do traço 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 como depurar o pipeline de entrada, caso ainda não tenha feito isso.

No entanto, mesmo com um pipeline de entrada otimizado, você ainda pode ter 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 ocorre no início de cada etapa, como copiar dados ou programar operações de GPU.

Se você observar grandes lacunas no lado do host, 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 verificar 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 .

2. Alcance maior utilização do dispositivo

1. Pequenos kernels de GPU e atrasos na 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.

Do Profiler página de visão geral em programas de TensorBoard quanto tempo a GPU estava ocioso devido à espera no host para kernels de lançamento. Na imagem abaixo, a GPU fica ociosa por cerca de 10% do tempo da etapa esperando pelo lançamento dos kernels.

image

O visualizador de rastreamento para este programa mesmos programas pequenos espaços entre grãos, onde o anfitrião é kernels lançamento ocupadas em 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 ferramenta na TensorBoard para os mesmos perfil mostra 126,224 operações Mul tomando 2,77 segundos. Assim, 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 seus Trace Viewer mostra muitos pequenos espaços entre ops 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 TensorFlow, de modo que você não está executando ops em um modo ansioso puro. Se você estiver usando Model.fit (como se opor a um ciclo de treinamento personalizado com tf.GradientTape ), então tf.keras.Model.compile irá automaticamente fazer isso por você.
  • Fusível kernels usando XLA com tf.function(jit_compile=True) ou auto-agrupamento. Para mais detalhes, acesse o Ativar precisão mista e XLA secção abaixo para saber como ativar XLA para obter maior desempenho. Esse recurso pode levar à alta utilização do dispositivo.
2. Posicionamento de operação do TensorFlow

O Profiler visão geral página mostra o percentual de ops colocado no host contra o dispositivo (você também pode verificar a colocação de ops específicos, olhando para o visualizador de rastreamento . Como na imagem abaixo, você quer a percentagem de ops no host ser muito pequeno 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 dos ops colocados na GPU é evitar cópias de memória excessivas entre o host e o dispositivo (cópias de memória para dados de entrada / saída do modelo entre o host e o dispositivo são esperadas). Um exemplo de cópia excessiva é demonstrado na vista em traço abaixo 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. Cópia de memória operações no traço espectador tem mais informações sobre as operações que são a fonte desses tensores copiados, mas pode não ser sempre fácil para associar um memcopy com um op. Nesses casos, é útil olhar as operações próximas para verificar se a cópia da memória acontece no mesmo local em todas as etapas.

3. 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.

1. Utilizar núcleos tensores

Modern GPUs NVIDIA® se especializaram Tensor Cores que podem melhorar significativamente o desempenho de sementes elegíveis.

Você pode usar de TensorBoard estatísticas do kernel GPU para visualizar quais kernels GPU são Tensor Núcleo-elegíveis e que kernels estão usando Tensor Cores. 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 o NVIDIA® aprendizagem profunda desempenho guia.

2. Operações de fusível

Use tf.function(jit_compile=True) para fundir ops menores para formar núcleos maiores que levam a ganhos significativos de desempenho. Para saber mais, consulte o XLA guia.

3. Habilite 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.

1. Ative a precisão mista

Os TensorFlow Mixed precisão 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 (float32) 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.

1. 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 experimentar 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.

2. Escala de perda dinâmica vs. estática

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 necessária para especificar o valor de escala de perda estática correto.

2. Ative o XLA com tf.function (jit_compile = True) ou clustering automático

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 com tf.function(jit_compile=True) ou auto-agrupamento, consulte o XLA 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.

2. Otimize o desempenho no host único multi-GPU

O tf.distribute.MirroredStrategy API pode ser usado para treinamento do modelo escala de um GPU para várias GPUs em um único host. (Para saber mais sobre como fazer o treinamento distribuído com TensorFlow, referem-se à formação distribuída com TensorFlow , Use uma GPU , e Use TPUs guias ea formação distribuída com Keras tutorial.)

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ê experimente o dimensionamento 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 terá 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 seu programa para quaisquer chamadas AllReduce desnecessárias, pois isso resulta em uma sincronização em todos os dispositivos. Na vista de rastreio mostrado acima, o AllReduce é feito através da NCCL kernel, e existe apenas uma chamada NCCL em cada GPU para os gradientes em cada passo.
  4. Verifique se há operações de cópia D2H, H2D e D2D desnecessárias que podem ser minimizadas.
  5. Verifique o tempo da etapa para certificar-se de que cada réplica está fazendo o mesmo trabalho. Por exemplo, pode acontecer que uma GPU (tipicamente, GPU0 ) é oversubscribed porque o anfitrião erroneamente acaba colocando mais trabalho sobre ele.
  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. No passado, a depuração do desempenho nessa situação era resolvida caso a caso. Se você observar esse comportamento em seu programa, apresentar uma questão GitHub com imagens de sua visão vestígio.

1. Otimizar gradiente 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 acontece após o cálculo gradiente em cada dispositivo, e antes de o optimizador actualiza os pesos 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ê tem 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 Model.summary .

Note-se que cada um dos parâmetros do modelo é de 4 bytes em tamanho desde TensorFlow utiliza fp32 (float32) para comunicar gradientes. Mesmo que você tenha fp16 habilitado, NCCL AllReduce utiliza fp32 parâmetros.

Para obter 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.

2. 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 afetar negativamente o desempenho.

O visualizador de rastreamento abaixo mostra a sobrecarga quando a CPU cambaleia lançamentos de kernel GPU de forma ineficiente, como GPU1 está ocioso e, em seguida, começa a correr ops após GPU2 começou.

image

A visão de rastreio para os shows de acolhimento que o host está lançando kernels em GPU2 antes de lançar-los em GPU1 (note que o abaixo tf_Compute* ops não são indicativos de tópicos CPU).

image

Se você experimentar esse tipo de desconforto nos kernels da 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.