Panoramica
Questa guida presuppone familiarità con TensorFlow Profiler e tf.data
. Ha lo scopo di fornire istruzioni dettagliate 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 .
Il flusso di lavoro di analisi descritto di seguito si concentra sullo strumento visualizzatore di tracce nel Profiler. Questo strumento mostra una sequenza temporale che mostra la durata delle operazioni eseguite dal tuo programma TensorFlow e ti consente di identificare quali operazioni richiedono più tempo per essere eseguite. Per ulteriori informazioni sul visualizzatore di tracce, consulta questa sezione della guida di TF Profiler. In generale, gli eventi tf.data
appariranno sulla timeline 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
produce dati abbastanza velocemente?
Inizia accertando se la pipeline di input è il collo di bottiglia per il tuo programma TensorFlow.
Per fare ciò, cerca IteratorGetNext::DoCompute
ops 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 un tf.function
, questi dovrebbero essere trovati nei thread tf_data_iterator_get_next
.
Tieni presente che se utilizzi una strategia di distribuzione , potresti visualizzare gli eventi IteratorGetNextAsOptional::DoCompute
invece di IteratorGetNext::DoCompute
(a partire da TF 2.3).
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; consulta la guida di Profiler per suggerimenti più generici sull'analisi delle prestazioni.
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 best practice 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 il training del modello. Se stai precaricando i dati, dovresti vedere una sezione Iterator::Prefetch
sullo stesso thread IteratorGetNext::DoCompute
.
Se non hai un prefetch
alla fine della tua pipeline , dovresti aggiungerne uno. Per ulteriori informazioni sui consigli sulle prestazioni di tf.data
, consulta la guida alle prestazioni di tf.data .
Se stai già precaricando i dati e la pipeline di input è ancora il tuo collo di bottiglia, passa alla sezione successiva per analizzare ulteriormente le prestazioni.
3. Stai raggiungendo un elevato utilizzo della CPU?
tf.data
raggiunge un throughput elevato cercando di utilizzare al meglio le 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 stai utilizzando GCP.
Se l'utilizzo è basso, ciò suggerisce che la pipeline di input potrebbe non sfruttare appieno la CPU host. Dovresti consultare la guida alle prestazioni di tf.data per le migliori pratiche. Se hai applicato le best practice e l'utilizzo e la velocità effettiva rimangono bassi, continua con l' analisi del collo di bottiglia di seguito.
Se l'utilizzo si sta avvicinando 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 tuoi dati rientrano nella memoria; questo riduce il calcolo a scapito di un maggiore utilizzo della memoria. Inoltre, la disabilitazione del parallelismo intra-operativo 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 dei colli di bottiglia
La sezione seguente illustra come leggere gli eventi tf.data
nel visualizzatore di tracce per capire dove si trova il collo di bottiglia e le possibili strategie di mitigazione.
Comprensione degli 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.
Ad esempio, lo screenshot sopra è stato generato dal seguente codice:
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
. Tieni presente 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 analizzare.
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 si troveranno in un thread diverso. In questi casi, il "nome lungo" può aiutarti a identificare a quale trasformazione in una pipeline corrisponde un evento.
Ad esempio, lo screenshot sopra è stato generato dal seguente codice:
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 relativi eventi di input ( BatchV2
) si troveranno in 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
.
Identificare il collo di bottiglia
In generale, per identificare il collo di bottiglia nella pipeline di input, percorrere la pipeline di input dalla trasformazione più esterna fino all'origine. Partendo dalla trasformazione finale nella tua pipeline, ricorri alle trasformazioni upstream finché non trovi una trasformazione lenta o raggiungi un set di dati di origine, ad esempio TFRecord
. Nell'esempio sopra, dovresti iniziare da Prefetch
, quindi risalire a BatchV2
, FiniteRepeat
, Map
e infine Range
.
In generale, una trasformazione lenta corrisponde a una i cui eventi sono lunghi, ma i cui eventi di input sono brevi. Di seguito 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 l'ottimizzazione 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 (annidata 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
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 relativi eventi di input ( Iterator::FlatMap
) restituiscono rapidamente. Ciò suggerisce che la trasformazione sequenziale della mappa è il collo di bottiglia.
Si noti che nello screenshot, l'evento InstantiatedCapturedFunction::Run
corrisponde al tempo necessario per eseguire la funzione map.
Esempio 2
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 usa 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, ad esempio la lettura da file TFRecord, puoi migliorare le prestazioni parallelizzando l'estrazione dei dati. Per fare ciò, assicurati che i tuoi dati siano ripartiti su più file e usa tf.data.Dataset.interleave
con il parametro num_parallel_calls
impostato su tf.data.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
a 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.AUTOTUNE,
deterministic=False)
Si noti che i file frammentati dovrebbero essere ragionevolmente grandi per ammortizzare il sovraccarico dell'apertura di un file. Per maggiori dettagli sull'estrazione parallela dei dati, consulta questa sezione della guida alle prestazioni di tf.data
.
Set di dati di trasformazione
Se hai identificato una trasformazione tf.data
intermedia come collo di bottiglia, puoi risolverla parallelizzando la trasformazione o memorizzando nella cache il calcolo se i tuoi dati rientrano nella 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; puoi parallelizzarli introducendo il "parallelismo esterno". Ad esempio, supponendo che la pipeline di input sia inizialmente 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.AUTOTUNE)
dataset = dataset.prefetch(tf.data.AUTOTUNE)
Risorse addizionali
- Guida alle prestazioni tf.data su come scrivere pipeline di input
tf.data
per le prestazioni - All'interno del video TensorFlow: best practice
tf.data
- Guida al profiler
- Tutorial per profiler con colab