Esta página foi traduzida pela API Cloud Translation.
Switch to English

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 no TensorFlow. Estão disponíveis instruções sobre como fazer isso para CPUs / GPUs e Cloud TPUs .

TensorFlow Trace Viewer

O fluxo de trabalho de análise detalhado abaixo concentra-se na ferramenta visualizador de rastreio 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 demoram mais para serem executadas. Para obter mais informações sobre o visualizador de rastreamento, consulte 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 comentários 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 com rapidez suficiente?

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

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

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

image

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

image

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

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

A melhor prática para o desempenho do pipeline de entrada é inserir uma transformação tf.data.Dataset.prefetch no final do pipeline tf.data . Essa transformação sobrepõe a computação de pré-processamento do pipeline de entrada com a próxima etapa da computação 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 thread que a IteratorGetNext::DoCompute .

image

Se você não tiver uma prefetch - prefetch no final do seu pipeline , adicione 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á está pré-buscando dados e o pipeline de entrada ainda é seu gargalo, continue na próxima seção para analisar melhor o desempenho.

3. Você está alcançando alta utilização da CPU?

tf.data alcança alto rendimento, 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 em 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 práticas recomendadas. Se você aplicou as melhores práticas, a utilização e a taxa de transferência permanecem baixas, continue na análise 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 seu pipeline de entrada (por exemplo, evitando computação desnecessária) ou descarregar a computação.

Você pode melhorar a eficiência do seu 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 intensivo em computação, se seus dados tf.data.Dataset.cache na memória; isso reduz a computação ao custo de maior uso de memória. Além disso, a desativação do paralelismo intra-op no tf.data tem o potencial de aumentar a eficiência em> 10% e pode ser feita configurando a seguinte opção no 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 rastreio para entender onde está o gargalo e possíveis estratégias de mitigação.

Entendendo os 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 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> no nome (curto) e os outros conjuntos de dados no nome longo representam transformações a jusante.

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 possui o nome longo Iterator::BatchV2::FiniteRepeat::Map . Observe que o nome dos conjuntos de dados pode diferir um pouco 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 das 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 das 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 a Prefetch - Prefetch é assíncrona, seus eventos de entrada ( BatchV2 ) estarão em um encadeamento diferente e podem ser localizados procurando o nome longo Iterator::Prefetch::BatchV2 . Nesse caso, eles estão no encadeamento tf_data_iterator_resource . Pelo nome longo, você pode deduzir que o BatchV2 está a montante da 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 no seu pipeline de entrada, siga o pipeline de entrada da transformação mais externa até a origem. A partir da transformação final em seu pipeline, retorne às 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 a partir de Prefetch - Prefetch , em seguida, BatchV2 para 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 do 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 seu trabalho estiver usando uma estratégia de distribuição , o visualizador de rastreio 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 upstream Iterator::Generator . Você pode encontrar o pipeline do host correspondente pesquisando 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 seqüencial do mapa é o gargalo.

Observe que na captura de tela, o evento InstantiatedCapturedFunction::Run corresponde ao tempo necessário 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 acima, mas usa ParallelMap em vez de Map. Observamos aqui que (1) os eventos Iterator::ParallelMap são longos, mas (2) os eventos de entrada Iterator::FlatMap (que estão em um encadeamento diferente, pois o 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, verifique se seus dados estão agrupados em vários arquivos e use tf.data.Dataset.interleave com o parâmetro num_parallel_calls definido como tf.data.experimental.AUTOTUNE . Se o determinismo não for importante para o seu programa, você poderá melhorar ainda mais o desempenho definindo o sinalizador deterministic=False no tf.data.Dataset.interleave partir do TF 2.2. Por exemplo, se você está lendo TFRecords, pode fazer o seguinte:

 dataset = tf.data.Dataset.from_tensor_slices(filenames)
dataset = dataset.interleave(tf.data.TFRecordDataset,
  num_parallel_calls=tf.data.experimental.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 extração de dados paralelos, consulte esta seção do guia de desempenho tf.data .

Conjuntos de dados de transformação

Se você identificou uma transformação intermediária tf.data como o gargalo, poderá resolvê-la paralelizando a transformação ou armazenando em cache o cálculo se seus dados couberem na memória e for apropriado. Algumas transformações, como o Map têm contrapartes paralelas; o guia de desempenho tf.data demonstra como paralelizar esses. Outras transformações, como Filter , Unbatch e Batch são inerentemente sequenciais; você pode paralelizá-los introduzindo o “paralelismo externo”. Por exemplo, supondo que seu pipeline de entrada inicialmente seja 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 o “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.experimental.AUTOTUNE)
dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)
 

Recursos adicionais