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. 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 concentra-se na ferramenta de visualização de rastreamento no Profiler. Esta 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, 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. Seu pipeline tf.data está produzindo dados com rapidez suficiente?

Comece verificando se o pipeline de entrada é o gargalo do 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 produzir um lote de elementos quando solicitado. Se você estiver usando keras ou iterando seu conjunto de dados em um 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 eventos IteratorGetNextAsOptional::DoCompute em vez de IteratorGetNext::DoCompute (a partir do TF 2.3).

image

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

image

Se as chamadas retornarem lentamente, tf.data não consegue acompanhar as solicitações do consumidor. Continue para a 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 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 no final do pipeline , adicione uma. Para obter mais informações sobre recomendações de desempenho 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, passe para a próxima seção para analisar melhor o desempenho.

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

tf.data atinge alto rendimento tentando fazer o melhor uso possível dos recursos disponíveis. Em geral, mesmo ao executar seu modelo em um acelerador como 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 a sua utilização for baixa, isso sugere que o seu pipeline de entrada pode não estar aproveitando totalmente a CPU do host. Você deve consultar o guia de desempenho tf.data para conhecer as práticas recomendadas. Se você aplicou as práticas recomendadas e a utilização e o rendimento permanecem baixos, continue com a análise de gargalos 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 de computação, se seus dados couberem na memória; isso reduz a computação ao custo do aumento do 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 explica 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> , 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 <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 um pouco diferente da API python (por exemplo, FiniteRepeat em vez de Repeat), 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 thread. No exemplo acima, como todas as transformações utilizadas são síncronas, todos os eventos aparecem no mesmo thread.

Para transformações assíncronas (como Prefetch , ParallelMap , ParallelInterleave e MapAndBatch ) os eventos das 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 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 Prefetch é assíncrono, seus eventos de entrada ( BatchV2 ) estarão em um thread diferente e podem ser localizados pesquisando o nome longo Iterator::Prefetch::BatchV2 . Nesse caso, eles estão no thread tf_data_iterator_resource . Pelo 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 no 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, recorra às transformações upstream até encontrar uma transformação lenta ou chegar a um conjunto de dados de origem, como TFRecord . No exemplo acima, você começaria em Prefetch , depois subiria 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 do modelo é introduzida automaticamente pelo tempo de execução tf.data e é usada para instrumentar e ajustar 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 Upstream Iterator::Generator . Você pode encontrar o pipeline de 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. Isto sugere que a transformação sequencial do mapa é o gargalo.

Observe que na captura de tela, o evento InstantiatedCapturedFunction::Run corresponde ao tempo necessário para executar a função do 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) os 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. Isto sugere que a transformação ParallelMap é o gargalo.

Resolvendo o gargalo

Conjuntos de dados de origem

Se você identificou uma origem de conjunto de dados como o gargalo, como a leitura de arquivos TFRecord, poderá melhorar o desempenho paralelizando a extração de dados. Para fazer isso, certifique-se de que seus dados estejam 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 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, poderá 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 possuem 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