Analise o desempenho do tf.data com o TF Profiler

Visão geral

Este guia pressupõe familiaridade com o TensorFlow Profiler e tf.data . O objetivo é fornecer instruções passo a passo com exemplos para ajudar os usuários a diagnosticar e corrigir problemas de desempenho do pipeline de entrada.

Para começar, colete um perfil do seu trabalho do TensorFlow. As instruções sobre como fazer isso estão disponíveis para CPUs/GPUs e Cloud TPUs .

TensorFlow Trace Viewer

O fluxo de trabalho de análise detalhado abaixo se concentra na ferramenta de visualização de rastreamento no Profiler. Essa ferramenta exibe uma linha do tempo que mostra a duração das operações executadas pelo seu programa TensorFlow e permite identificar quais operações levam mais tempo para serem executadas. Para obter mais informações sobre o visualizador de rastreamento, confira esta seção do guia do TF Profiler. Em geral, os eventos tf.data aparecerão na linha do tempo da CPU do host.

Fluxo de Trabalho de Análise

Siga o fluxo de trabalho abaixo. Se você tiver comentários para nos ajudar a melhorá-lo, crie um problema no github com o rótulo “comp:data”.

1. Seu pipeline tf.data está produzindo dados rápido o suficiente?

Comece verificando se o pipeline de entrada é o gargalo do seu programa TensorFlow.

Para fazer isso, procure IteratorGetNext::DoCompute ops no visualizador de rastreamento. Em geral, você espera vê-los no início de uma etapa. Essas fatias representam o tempo que leva para o pipeline de entrada produzir um lote de elementos quando solicitado. Se você estiver usando keras ou iterando sobre seu conjunto de dados em um tf.function , eles devem ser encontrados em tf_data_iterator_get_next threads.

Observe que, se você estiver usando uma estratégia de distribuição , poderá ver eventos IteratorGetNextAsOptional::DoCompute em vez de IteratorGetNext::DoCompute (a partir do TF 2.3).

image

Se as chamadas retornarem rapidamente (<= 50 nós), isso significa que seus dados estão disponíveis quando solicitados. O pipeline de entrada não é seu gargalo; consulte o guia Profiler para dicas de análise de desempenho mais genéricas.

image

Se as chamadas retornarem lentamente, tf.data não conseguirá acompanhar as solicitações do consumidor. Continue na próxima seção.

2. Você está pré-buscando dados?

A prática recomendada para o desempenho do pipeline de entrada é inserir uma transformação tf.data.Dataset.prefetch no final do pipeline tf.data . Essa transformação se sobrepõe ao cálculo de pré-processamento do pipeline de entrada com a próxima etapa do cálculo do modelo e é necessária para otimizar o desempenho do pipeline de entrada ao treinar seu modelo. Se você estiver pré-buscando dados, deverá ver uma fatia Iterator::Prefetch no mesmo thread que a IteratorGetNext::DoCompute .

image

Se você não tiver uma prefetch -busca no final do pipeline , deverá adicionar uma. Para obter mais informações sobre as recomendações de desempenho do tf.data , consulte o guia de desempenho do tf.data .

Se você já estiver pré-buscando dados e o pipeline de entrada ainda for seu gargalo, continue na próxima seção para analisar melhor o desempenho.

3. Você está atingindo uma alta utilização da CPU?

tf.data atinge alta taxa de transferência tentando fazer o melhor uso possível dos recursos disponíveis. Em geral, mesmo ao executar seu modelo em um acelerador como uma GPU ou TPU, os pipelines tf.data são executados na CPU. Você pode verificar sua utilização com ferramentas como sar e htop ou no console de monitoramento de nuvem se estiver executando no GCP.

Se sua utilização for baixa, isso sugere que seu pipeline de entrada pode não estar aproveitando ao máximo a CPU do host. Você deve consultar o guia de desempenho tf.data para obter as melhores práticas. Se você aplicou as práticas recomendadas e a utilização e a taxa de transferência permanecem baixas, continue com a análise de gargalo abaixo.

Se a sua utilização estiver se aproximando do limite de recursos , para melhorar ainda mais o desempenho, você precisará melhorar a eficiência do pipeline de entrada (por exemplo, evitando computação desnecessária) ou descarregar a computação.

Você pode melhorar a eficiência de seu pipeline de entrada evitando computação desnecessária em tf.data . Uma maneira de fazer isso é inserir uma transformação tf.data.Dataset.cache após um trabalho de computação intensiva se seus dados couberem na memória; isso reduz a computação ao custo de maior uso de memória. Além disso, desabilitar o paralelismo intra-operacional em tf.data tem o potencial de aumentar a eficiência em > 10% e pode ser feito definindo a seguinte opção em seu pipeline de entrada:

dataset = ...
options = tf.data.Options()
options.experimental_threading.max_intra_op_parallelism = 1
dataset = dataset.with_options(options)

4. Análise de gargalos

A seção a seguir mostra como ler eventos tf.data no visualizador de rastreamento para entender onde está o gargalo e possíveis estratégias de mitigação.

Noções básicas sobre eventos tf.data no Profiler

Cada evento tf.data no Profiler tem o nome Iterator::<Dataset> , em que <Dataset> é o nome da origem ou transformação do conjunto de dados. Cada evento também possui o nome longo Iterator::<Dataset_1>::...::<Dataset_n> , que você pode ver clicando no evento tf.data . No nome longo, <Dataset_n> corresponde a <Dataset> do nome (abreviado) e os outros conjuntos de dados no nome longo representam transformações downstream.

image

Por exemplo, a captura de tela acima foi gerada a partir do seguinte código:

dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x)
dataset = dataset.repeat(2)
dataset = dataset.batch(5)

Aqui, o evento Iterator::Map tem o nome longo Iterator::BatchV2::FiniteRepeat::Map . Observe que o nome dos conjuntos de dados pode diferir ligeiramente da API python (por exemplo, FiniteRepeat em vez de Repeat), mas deve ser intuitivo o suficiente para analisar.

Transformações síncronas e assíncronas

Para transformações tf.data síncronas (como Batch e Map ), você verá eventos de transformações upstream no mesmo thread. No exemplo acima, como todas as transformações utilizadas são síncronas, todos os eventos aparecem na mesma thread.

Para transformações assíncronas (como Prefetch , ParallelMap , ParallelInterleave e MapAndBatch ), os eventos de transformações upstream estarão em um encadeamento diferente. Nesses casos, o “nome longo” pode ajudar a identificar a qual transformação em um pipeline um evento corresponde.

image

Por exemplo, a captura de tela acima foi gerada a partir do seguinte código:

dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x)
dataset = dataset.repeat(2)
dataset = dataset.batch(5)
dataset = dataset.prefetch(1)

Aqui, os eventos Iterator::Prefetch estão nos threads tf_data_iterator_get_next . Como o Prefetch é assíncrono, seus eventos de entrada ( BatchV2 ) estarão em um thread diferente e podem ser localizados procurando pelo nome longo Iterator::Prefetch::BatchV2 . Nesse caso, eles estão no thread tf_data_iterator_resource . A partir de seu nome longo, você pode deduzir que BatchV2 é upstream de Prefetch . Além disso, o parent_id do evento BatchV2 corresponderá ao ID do evento Prefetch .

Identificando o gargalo

Em geral, para identificar o gargalo em seu pipeline de entrada, percorra o pipeline de entrada desde a transformação mais externa até a origem. A partir da transformação final em seu pipeline, recurse em transformações upstream até encontrar uma transformação lenta ou alcançar um conjunto de dados de origem, como TFRecord . No exemplo acima, você começaria em Prefetch , então caminharia até BatchV2 , FiniteRepeat , Map e finalmente Range .

Em geral, uma transformação lenta corresponde àquela cujos eventos são longos, mas cujos eventos de entrada são curtos. Alguns exemplos seguem abaixo.

Observe que a transformação final (mais externa) na maioria dos pipelines de entrada do host é o evento Iterator::Model . A transformação de Modelo é introduzida automaticamente pelo tempo de execução tf.data e é usada para instrumentar e ajustar automaticamente o desempenho do pipeline de entrada.

Se seu trabalho estiver usando uma estratégia de distribuição , o visualizador de rastreamento conterá eventos adicionais que correspondem ao pipeline de entrada do dispositivo. A transformação mais externa do pipeline de dispositivo (aninhada em IteratorGetNextOp::DoCompute ou IteratorGetNextAsOptionalOp::DoCompute ) será um evento Iterator::Prefetch com um evento Iterator::Generator upstream. Você pode localizar o pipeline de host correspondente pesquisando por eventos Iterator::Model .

Exemplo 1

image

A captura de tela acima é gerada a partir do seguinte pipeline de entrada:

dataset = tf.data.TFRecordDataset(filename)
dataset = dataset.map(parse_record)
dataset = dataset.batch(32)
dataset = dataset.repeat()

Na captura de tela, observe que (1) os eventos Iterator::Map são longos, mas (2) seus eventos de entrada ( Iterator::FlatMap ) retornam rapidamente. Isso sugere que a transformação de Mapa sequencial é o gargalo.

Observe que na captura de tela, o evento InstantiatedCapturedFunction::Run corresponde ao tempo que leva para executar a função map.

Exemplo 2

image

A captura de tela acima é gerada a partir do seguinte pipeline de entrada:

dataset = tf.data.TFRecordDataset(filename)
dataset = dataset.map(parse_record, num_parallel_calls=2)
dataset = dataset.batch(32)
dataset = dataset.repeat()

Este exemplo é semelhante ao anterior, mas usa ParallelMap em vez de Map. Notamos aqui que (1) os eventos Iterator::ParallelMap são longos, mas (2) seus eventos de entrada Iterator::FlatMap (que estão em uma thread diferente, já que ParallelMap é assíncrono) são curtos. Isso sugere que a transformação ParallelMap é o gargalo.

Lidando com o gargalo

Conjuntos de dados de origem

Se você identificou uma fonte de conjunto de dados como o gargalo, como a leitura de arquivos TFRecord, pode melhorar o desempenho paralelizando a extração de dados. Para fazer isso, certifique-se de que seus dados sejam fragmentados em vários arquivos e use tf.data.Dataset.interleave com o parâmetro num_parallel_calls definido como tf.data.AUTOTUNE . Se o determinismo não for importante para o seu programa, você pode melhorar ainda mais o desempenho definindo o sinalizador deterministic=False em tf.data.Dataset.interleave a partir do TF 2.2. Por exemplo, se estiver lendo de TFRecords, você pode fazer o seguinte:

dataset = tf.data.Dataset.from_tensor_slices(filenames)
dataset = dataset.interleave(tf.data.TFRecordDataset,
  num_parallel_calls=tf.data.AUTOTUNE,
  deterministic=False)

Observe que os arquivos fragmentados devem ser razoavelmente grandes para amortizar a sobrecarga de abrir um arquivo. Para obter mais detalhes sobre a extração paralela de dados, consulte esta seção do guia de desempenho tf.data .

Conjuntos de dados de transformação

Se você identificou uma transformação tf.data intermediária como o gargalo, pode resolvê-la paralelizando a transformação ou armazenando em cache a computação se seus dados couberem na memória e for apropriado. Algumas transformações, como Map , têm contrapartes paralelas; o guia de desempenho tf.data demonstra como paralelizá-los. Outras transformações, como Filter , Unbatch e Batch são inerentemente sequenciais; você pode paralelizá-los introduzindo “paralelismo externo”. Por exemplo, suponha que seu pipeline de entrada inicialmente se pareça com o seguinte, com Batch como gargalo:

filenames = tf.data.Dataset.list_files(file_path, shuffle=is_training)
dataset = filenames_to_dataset(filenames)
dataset = dataset.batch(batch_size)

Você pode introduzir “paralelismo externo” executando várias cópias do pipeline de entrada em entradas fragmentadas e combinando os resultados:

filenames = tf.data.Dataset.list_files(file_path, shuffle=is_training)

def make_dataset(shard_index):
  filenames = filenames.shard(NUM_SHARDS, shard_index)
  dataset = filenames_to_dataset(filenames)
  Return dataset.batch(batch_size)

indices = tf.data.Dataset.range(NUM_SHARDS)
dataset = indices.interleave(make_dataset,
                             num_parallel_calls=tf.data.AUTOTUNE)
dataset = dataset.prefetch(tf.data.AUTOTUNE)

Recursos adicionais