Analise o desempenho de tf.data com o TF Profiler

Visão geral

Este guia pressupõe familiaridade com o TensorFlow Profiler e tf.data . Seu 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 do visualizador 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 que você identifique quais operações demoram mais para serem executadas. Para obter mais informações sobre o visualizador de rastreamento, verifique esta seção do guia 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 feedback para nos ajudar a melhorá-lo, crie um problema no github com o rótulo “comp: data”.

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

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

Para fazer isso, procure operações IteratorGetNext::DoCompute no visualizador de rastreamento. Em geral, você espera ver isso no início de uma etapa. Essas fatias representam o tempo que leva para o pipeline de entrada gerar um lote de elementos quando solicitado. Se você estiver usando keras ou iterando em 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 vez de IteratorGetNext::DoCompute (a partir do TF 2.3).

image

Se as ligações retornam rapidamente (<= 50 us), isso significa que seus dados estarão disponíveis quando solicitados. O pipeline de entrada não é o seu gargalo; consulte o guia do Profiler para dicas de análise de desempenho mais genéricas.

image

Se as chamadas retornam lentamente, tf.data não consegue atender às 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 o desempenho ideal do pipeline de entrada ao treinar seu modelo. Se você estiver pré-buscando dados, deverá ver uma fatia Iterator::Prefetch no mesmo encadeamento que a op IteratorGetNext::DoCompute .

image

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

Se você já estiver pré-buscando dados e o pipeline de entrada ainda for o 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 alto rendimento ao tentar 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 em nuvem, se estiver executando no GCP.

Se a sua utilização for baixa, isso sugere que o pipeline de entrada pode não estar aproveitando totalmente a CPU do host. Você deve consultar o guia de desempenho tf.data para melhores práticas. Se você aplicou as práticas recomendadas e a utilização e o rendimento continuam baixos, continue com a análise de gargalo abaixo.

Se a sua utilização está se aproximando do limite de recursos , para melhorar ainda mais o desempenho, você precisa 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 do pipeline de entrada, evitando cálculos desnecessários 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 tf.data.Dataset.cache na memória; isso reduz a computação ao custo de um maior uso de memória. Além disso, desativar o paralelismo intra-operatório 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 gargalo

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

Compreendendo eventos tf.data no Profiler

Cada evento tf.data no Profiler tem o nome Iterator::<Dataset> , onde <Dataset> é o nome da origem ou transformação do conjunto de dados. Cada evento também tem 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 ser ligeiramente diferente da API do python (por exemplo, FiniteRepeat em vez de Repetir), mas deve ser intuitivo o suficiente para ser analisado.

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 encadeamento. No exemplo acima, como todas as transformações usadas são síncronas, todos os eventos aparecem no mesmo encadeamento.

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

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 a Prefetch - Prefetch é assíncrona, 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 encadeamento tf_data_iterator_resource . Por seu nome longo, você pode deduzir que BatchV2 é anterior à 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 da transformação mais externa até a origem. Começando com a transformação final em seu pipeline, recurse nas transformações upstream até encontrar uma transformação lenta ou chegar a um conjunto de dados de origem, como TFRecord . No exemplo acima, você iniciaria a partir de Prefetch - Prefetch , em seguida, caminharia em BatchV2 a 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 tf.data automaticamente o desempenho do pipeline de entrada.

Se o 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 do dispositivo (aninhada em IteratorGetNextOp::DoCompute ou IteratorGetNextAsOptionalOp::DoCompute ) será um evento Iterator::Prefetch com um evento Iterator::Generator upstream. Você pode encontrar o pipeline de host correspondente procurando 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 sequencial do Mapa é o gargalo.

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

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) eventos Iterator::ParallelMap são longos, mas (2) seus eventos de entrada Iterator::FlatMap (que estão em um thread diferente, já que ParallelMap é assíncrono) são curtos. Isso sugere que a transformação ParallelMap é o gargalo.

Resolvendo 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 partir do TF 2.2. Por exemplo, se estiver lendo 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 abertura de 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 forem apropriados. Algumas transformações, como Map têm contrapartes paralelas; o guia de desempenho tf.data demonstra como tf.data - tf.data em paralelo. 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 o 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 sobre 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