Questa pagina è stata tradotta dall'API Cloud Translation.
Switch to English

Analizza le prestazioni di tf.data con TF Profiler

Panoramica

Questa guida presuppone familiarità con TensorFlow Profiler e tf.data . Ha lo scopo di fornire istruzioni passo passo con esempi per aiutare gli utenti a diagnosticare e risolvere i problemi di prestazioni della pipeline di input.

Per iniziare, raccogli un profilo del tuo lavoro TensorFlow. Le istruzioni su come farlo sono disponibili per CPU / GPU e Cloud TPU .

TensorFlow Trace Viewer

Il flusso di lavoro di analisi descritto di seguito si concentra sullo strumento di visualizzazione delle tracce nel Profiler. Questo strumento mostra una sequenza temporale che mostra la durata delle operazioni eseguite dal programma TensorFlow e consente di identificare le operazioni che richiedono più tempo per essere eseguite. Per ulteriori informazioni sul visualizzatore di tracce, consulta questa sezione della guida di TF Profiler. In generale, tf.data eventi tf.data verranno visualizzati nella sequenza temporale della CPU host.

Flusso di lavoro di analisi

Si prega di seguire il flusso di lavoro di seguito. Se hai feedback per aiutarci a migliorarlo, crea un problema GitHub con l'etichetta "comp: data".

1. La tua pipeline tf.data producendo dati abbastanza velocemente?

Inizia verificando se la pipeline di input è il collo di bottiglia per il tuo programma TensorFlow.

A tale scopo, cercare operazioni IteratorGetNext::DoCompute nel visualizzatore di tracce. In generale, ti aspetti di vederli all'inizio di un passaggio. Queste sezioni rappresentano il tempo impiegato dalla pipeline di input per produrre un batch di elementi quando viene richiesto. Se stai usando keras o stai iterando sul tuo set di dati in una funzione tf.function , questi dovrebbero essere trovati nei thread tf_data_iterator_get_next .

Nota che se stai usando una strategia di distribuzione , potresti vedere eventi IteratorGetNextAsOptional::DoCompute invece di IteratorGetNext::DoCompute (a partire da TF 2.3).

image

Se le chiamate tornano rapidamente (<= 50 us), significa che i tuoi dati sono disponibili quando vengono richiesti. La pipeline di input non è il tuo collo di bottiglia; vedere la guida Profiler per suggerimenti più generici sull'analisi delle prestazioni.

image

Se le chiamate tornano lentamente, tf.data non è in grado di tenere il passo con le richieste del consumatore. Continua alla sezione successiva.

2. Stai precaricando i dati?

La procedura migliore per le prestazioni della pipeline di input consiste nell'inserire una trasformazione tf.data.Dataset.prefetch alla fine della pipeline tf.data . Questa trasformazione si sovrappone al calcolo di pre-elaborazione della pipeline di input con il passaggio successivo del calcolo del modello ed è necessaria per prestazioni ottimali della pipeline di input durante l'addestramento del modello. Se stai eseguendo il precaricamento dei dati, dovresti vedere una sezione Iterator::Prefetch sullo stesso thread IteratorGetNext::DoCompute .

image

Se non disponi di un prefetch alla fine della pipeline , dovresti aggiungerne uno. Per ulteriori informazioni sui consigli sulle prestazioni di tf.data , consultare la guida alle prestazioni di tf.data .

Se stai già precaricando i dati e la pipeline di input è ancora il collo di bottiglia, vai alla sezione successiva per analizzare ulteriormente le prestazioni.

3. Stai raggiungendo un elevato utilizzo della CPU?

tf.data raggiunge un throughput elevato cercando di fare il miglior uso possibile delle risorse disponibili. In generale, anche quando si esegue il modello su un acceleratore come una GPU o TPU, le pipeline tf.data vengono eseguite sulla CPU. Puoi controllare il tuo utilizzo con strumenti come sar e htop o nella console di monitoraggio del cloud se utilizzi GCP.

Se il tuo utilizzo è basso, ciò suggerisce che la tua pipeline di input potrebbe non sfruttare appieno la CPU host. È necessario consultare la guida alle prestazioni di tf.data per le migliori pratiche. Se sono state applicate le migliori pratiche e l'utilizzo e la velocità effettiva rimangono bassi, continuare con l' analisi dei colli di bottiglia di seguito.

Se l'utilizzo si avvicina al limite delle risorse , per migliorare ulteriormente le prestazioni, è necessario migliorare l'efficienza della pipeline di input (ad esempio, evitando calcoli non necessari) o scaricare il calcolo.

È possibile migliorare l'efficienza della pipeline di input evitando calcoli non necessari in tf.data . Un modo per farlo è inserire una trasformazione tf.data.Dataset.cache dopo un lavoro ad alta intensità di calcolo se i dati si adattano alla memoria; questo riduce il calcolo al costo di un maggiore utilizzo della memoria. Inoltre, la disabilitazione del parallelismo tf.data in tf.data ha il potenziale per aumentare l'efficienza di> 10% e può essere eseguita impostando la seguente opzione sulla pipeline di input:

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

4. Analisi del collo di bottiglia

La sezione seguente illustra come leggere tf.data eventi tf.data nel visualizzatore di tracce per capire dove si trova il collo di bottiglia e le possibili strategie di mitigazione.

Comprensione tf.data eventi tf.data nel profiler

Ogni evento tf.data nel Profiler ha il nome Iterator::<Dataset> , dove <Dataset> è il nome dell'origine o della trasformazione del set di dati. Ogni evento ha anche il nome lungo Iterator::<Dataset_1>::...::<Dataset_n> , che puoi vedere facendo clic sull'evento tf.data . Nel nome lungo, <Dataset_n> corrisponde a <Dataset> dal nome (breve) e gli altri set di dati nel nome lungo rappresentano trasformazioni a valle.

image

Ad esempio, lo screenshot sopra è stato generato dal codice seguente:

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

Qui, l'evento Iterator::Map ha il nome lungo Iterator::BatchV2::FiniteRepeat::Map . Si noti che il nome del set di dati potrebbe differire leggermente dall'API python (ad esempio, FiniteRepeat invece di Repeat), ma dovrebbe essere abbastanza intuitivo da poter essere analizzato.

Trasformazioni sincrone e asincrone

Per le trasformazioni tf.data sincrone (come Batch e Map ), vedrai gli eventi delle trasformazioni upstream sullo stesso thread. Nell'esempio precedente, poiché tutte le trasformazioni utilizzate sono sincrone, tutti gli eventi vengono visualizzati sullo stesso thread.

Per le trasformazioni asincrone (come Prefetch , ParallelMap , ParallelInterleave e MapAndBatch ) gli eventi delle trasformazioni upstream saranno su un thread diverso. In questi casi, il "nome lungo" può aiutarti a identificare a quale trasformazione in una pipeline corrisponde un evento.

image

Ad esempio, lo screenshot sopra è stato generato dal codice seguente:

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

Qui, gli eventi Iterator::Prefetch si trovano nei thread tf_data_iterator_get_next . Poiché Prefetch è asincrono, i suoi eventi di input ( BatchV2 ) saranno su un thread diverso e possono essere individuati cercando il nome lungo Iterator::Prefetch::BatchV2 . In questo caso, si trovano nel thread tf_data_iterator_resource . Dal suo nome lungo, puoi dedurre che BatchV2 è a monte di Prefetch . Inoltre, il parent_id dell'evento BatchV2 corrisponderà all'ID dell'evento Prefetch .

Identificazione del collo di bottiglia

In generale, per identificare il collo di bottiglia nella pipeline di input, percorri la pipeline di input dalla trasformazione più esterna fino all'origine. A partire dalla trasformazione finale nella pipeline, ricorrere alle trasformazioni a monte fino a trovare una trasformazione lenta o raggiungere un set di dati di origine, come TFRecord . Nell'esempio sopra, inizierai da Prefetch , quindi camminerai a monte verso BatchV2 , FiniteRepeat , Map e infine Range .

In generale, una trasformazione lenta corrisponde a quella i cui eventi sono lunghi, ma i cui eventi di input sono brevi. Di seguito sono riportati alcuni esempi.

Si noti che la trasformazione finale (più esterna) nella maggior parte delle pipeline di input dell'host è l'evento Iterator::Model . La trasformazione del modello viene introdotta automaticamente dal runtime tf.data e viene utilizzata per la strumentazione e la messa a punto automatica delle prestazioni della pipeline di input.

Se il tuo lavoro utilizza una strategia di distribuzione , il visualizzatore di tracce conterrà eventi aggiuntivi che corrispondono alla pipeline di input del dispositivo. La trasformazione più esterna della pipeline del dispositivo (nidificata in IteratorGetNextOp::DoCompute o IteratorGetNextAsOptionalOp::DoCompute ) sarà un evento Iterator::Prefetch con un evento Iterator::Generator upstream. È possibile trovare la pipeline host corrispondente cercando gli eventi Iterator::Model .

Esempio 1

image

Lo screenshot sopra è generato dalla seguente pipeline di input:

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

Nello screenshot, osserva che (1) gli eventi Iterator::Map sono lunghi, ma (2) i suoi eventi di input ( Iterator::FlatMap ) ritornano rapidamente. Ciò suggerisce che la trasformazione sequenziale della mappa è il collo di bottiglia.

Notare che nello screenshot, l'evento InstantiatedCapturedFunction::Run corrisponde al tempo necessario per eseguire la funzione map.

Esempio 2

image

Lo screenshot sopra è generato dalla seguente pipeline di input:

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

Questo esempio è simile al precedente, ma utilizza ParallelMap invece di Map. Notiamo qui che (1) gli eventi Iterator::ParallelMap sono lunghi, ma (2) i suoi eventi di input Iterator::FlatMap (che si trovano su un thread diverso, poiché ParallelMap è asincrono) sono brevi. Ciò suggerisce che la trasformazione ParallelMap è il collo di bottiglia.

Affrontare il collo di bottiglia

Set di dati di origine

Se hai identificato un'origine del set di dati come collo di bottiglia, come la lettura dai file TFRecord, puoi migliorare le prestazioni parallelizzando l'estrazione dei dati. A tale scopo, assicurati che i tuoi dati siano suddivisi in più file e utilizza tf.data.Dataset.interleave con il parametro num_parallel_calls impostato su tf.data.experimental.AUTOTUNE . Se il determinismo non è importante per il tuo programma, puoi migliorare ulteriormente le prestazioni impostando il flag deterministic=False su tf.data.Dataset.interleave partire da TF 2.2. Ad esempio, se stai leggendo da TFRecords, puoi fare quanto segue:

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

Tieni presente che i file frammentati dovrebbero essere ragionevolmente grandi per ammortizzare il sovraccarico di apertura di un file. Per ulteriori dettagli sull'estrazione parallela dei dati, vedere questa sezione della guida alle prestazioni di tf.data .

Set di dati di trasformazione

Se hai identificato una trasformazione intermedia tf.data come collo di bottiglia, puoi affrontarla parallelizzando la trasformazione o memorizzando nella cache il calcolo se i tuoi dati si adattano alla memoria ed è appropriato. Alcune trasformazioni come Map hanno controparti parallele; la guida alle prestazioni di tf.data mostra come parallelizzarli. Altre trasformazioni, come Filter , Unbatch e Batch sono intrinsecamente sequenziali; è possibile parallelizzarli introducendo il "parallelismo esterno". Ad esempio, supponendo che la pipeline di input inizialmente sia simile alla seguente, con Batch come collo di bottiglia:

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

Puoi introdurre il "parallelismo esterno" eseguendo più copie della pipeline di input su input frammentati e combinando i risultati:

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)

Risorse addizionali