Diese Seite wurde von der Cloud Translation API übersetzt.
Switch to English

Analysieren Sie die Leistung von tf.data mit dem TF Profiler

Überblick

In diesem Handbuch wird davon tf.data mit dem TensorFlow Profiler und tf.data . Ziel ist es, schrittweise Anweisungen mit Beispielen bereitzustellen, mit denen Benutzer Leistungsprobleme bei der Eingabe-Pipeline diagnostizieren und beheben können.

Sammeln Sie zunächst ein Profil Ihres TensorFlow-Jobs. Anweisungen dazu finden Sie für CPUs / GPUs und Cloud-TPUs .

TensorFlow Trace Viewer

Der unten beschriebene Analyse-Workflow konzentriert sich auf das Trace-Viewer-Tool im Profiler. Dieses Tool zeigt eine Zeitleiste an, die die Dauer der von Ihrem TensorFlow-Programm ausgeführten Operationen anzeigt und es Ihnen ermöglicht, zu ermitteln, welche Operationen am längsten dauern. Weitere Informationen zum Trace-Viewer finden Sie in diesem Abschnitt des TF Profiler-Handbuchs. Im Allgemeinen werden tf.data Ereignisse auf der Host-CPU-Zeitachse tf.data .

Analyse-Workflow

Bitte folgen Sie dem Workflow unten. Wenn Sie Feedback haben, um uns bei der Verbesserung zu helfen, erstellen Sie bitte ein Github-Problem mit der Bezeichnung "comp: data".

1. Produziert Ihre tf.data Pipeline Daten schnell genug?

Stellen Sie zunächst fest, ob die Eingabepipeline der Engpass für Ihr TensorFlow-Programm ist.

IteratorGetNext::DoCompute im Trace-Viewer nach IteratorGetNext::DoCompute ops. Im Allgemeinen erwarten Sie, dass diese zu Beginn eines Schritts angezeigt werden. Diese Slices stellen die Zeit dar, die Ihre Eingabe-Pipeline benötigt, um einen Stapel von Elementen zu erhalten, wenn sie angefordert wird. Wenn Sie Keras verwenden oder in einer tf.function über Ihr Dataset tf.function , sollten diese in tf_data_iterator_get_next Threads tf_data_iterator_get_next zu finden tf_data_iterator_get_next .

Beachten Sie, dass bei Verwendung einer Verteilungsstrategie möglicherweise IteratorGetNextAsOptional::DoCompute Ereignisse anstelle von IteratorGetNext::DoCompute (ab TF 2.3) IteratorGetNext::DoCompute .

image

Wenn die Anrufe schnell zurückkehren (<= 50 us), bedeutet dies, dass Ihre Daten verfügbar sind, wenn sie angefordert werden. Die Eingabepipeline ist nicht Ihr Engpass. Weitere Tipps zur Leistungsanalyse finden Sie im Profiler-Handbuch .

image

Wenn die Anrufe langsam zurückkehren, kann tf.data nicht mit den Anforderungen des Verbrauchers Schritt halten. Fahren Sie mit dem nächsten Abschnitt fort.

2. Rufen Sie Daten vorab ab?

Die beste Vorgehensweise für die Leistung der Eingabepipeline besteht tf.data.Dataset.prefetch , am Ende Ihrer tf.data Pipeline eine tf.data.Dataset.prefetch Transformation tf.data . Diese Transformation überlappt die Vorverarbeitungsberechnung der Eingabepipeline mit dem nächsten Schritt der Modellberechnung und ist für eine optimale Leistung der Eingabepipeline beim Training Ihres Modells erforderlich. Wenn Sie Daten vorab IteratorGetNext::DoCompute , sollte ein Iterator::Prefetch Slice im selben Thread wie IteratorGetNext::DoCompute op IteratorGetNext::DoCompute .

image

Wenn Sie am Ende Ihrer Pipeline keinen prefetch haben , sollten Sie einen hinzufügen. Weitere Informationen zu Leistungsempfehlungen für tf.data Leistungshandbuch für tf.data .

Wenn Sie bereits Daten vorab abrufen und die Eingabepipeline immer noch Ihr Engpass ist, fahren Sie mit dem nächsten Abschnitt fort, um die Leistung weiter zu analysieren.

3. Erreichen Sie eine hohe CPU-Auslastung?

tf.data erzielt einen hohen Durchsatz, indem versucht wird, die verfügbaren Ressourcen bestmöglich zu nutzen. Selbst wenn Sie Ihr Modell auf einem Beschleuniger wie einer GPU oder TPU tf.data , werden die tf.data Pipelines im tf.data auf der CPU ausgeführt. Sie können Ihre Auslastung mit Tools wie sar und htop oder in der Cloud-Überwachungskonsole überprüfen, wenn Sie mit GCP arbeiten.

Wenn Ihre Auslastung gering ist, deutet dies darauf hin, dass Ihre Eingabepipeline die Host-CPU möglicherweise nicht voll ausnutzt. Best Practices finden Sie im Leistungshandbuch für tf.data . Wenn Sie die Best Practices angewendet haben und die Auslastung und der Durchsatz niedrig bleiben, fahren Sie mit der unten stehenden Engpassanalyse fort .

Wenn sich Ihre Auslastung dem Ressourcenlimit nähert , müssen Sie entweder die Effizienz Ihrer Eingabepipeline verbessern (z. B. unnötige Berechnungen vermeiden) oder die Berechnung auslagern, um die Leistung weiter zu verbessern.

Sie können die Effizienz Ihrer Eingabepipeline verbessern, indem Sie unnötige Berechnungen in tf.data . Eine Möglichkeit, dies zu tun, besteht tf.data.Dataset.cache , nach rechenintensiver Arbeit eine tf.data.Dataset.cache Transformation tf.data.Dataset.cache , wenn Ihre Daten in den Speicher tf.data.Dataset.cache . Dies reduziert die Berechnung auf Kosten einer erhöhten Speichernutzung. Durch Deaktivieren der Intra-Op-Parallelität in tf.data kann die Effizienz um> 10% gesteigert werden. tf.data kann durch Festlegen der folgenden Option in Ihrer Eingabepipeline erreicht werden:

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

4. Engpassanalyse

Im folgenden Abschnitt tf.data wie Sie tf.data Ereignisse im Trace-Viewer lesen, um zu verstehen, wo sich der Engpass befindet und welche Strategien zur Schadensbegrenzung möglich sind.

tf.data Ereignissen im Profiler

Jedes tf.data Ereignis im Profiler hat den Namen Iterator::<Dataset> , wobei <Dataset> der Name der Dataset-Quelle oder -Transformation ist. Jedes Ereignis hat auch den langen Namen Iterator::<Dataset_1>::...::<Dataset_n> , den Sie durch Klicken auf das Ereignis tf.data . Im <Dataset_n> stimmt <Dataset_n> mit <Dataset> vom (Kurz-) Namen <Dataset_n> , und die anderen Datensätze im <Dataset_n> für nachgeschaltete Transformationen.

image

Der obige Screenshot wurde beispielsweise aus dem folgenden Code generiert:

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

Hier hat das Iterator::Map Ereignis den Iterator::BatchV2::FiniteRepeat::Map . Beachten Sie, dass der Name des Datasets geringfügig von der Python-API abweichen kann (z. B. FiniteRepeat anstelle von Repeat), aber intuitiv genug sein sollte, um analysiert zu werden.

Synchrone und asynchrone Transformationen

Bei synchronen tf.data Transformationen (wie Batch und Map ) werden Ereignisse aus Upstream-Transformationen im selben Thread tf.data . Da im obigen Beispiel alle verwendeten Transformationen synchron sind, werden alle Ereignisse im selben Thread angezeigt.

Bei asynchronen Transformationen (wie Prefetch , ParallelMap , ParallelInterleave und MapAndBatch ) befinden sich Ereignisse aus Upstream-Transformationen in einem anderen Thread. In solchen Fällen kann Ihnen der „lange Name“ dabei helfen, festzustellen, welcher Transformation in einer Pipeline ein Ereignis entspricht.

image

Der obige Screenshot wurde beispielsweise aus dem folgenden Code generiert:

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

Hier befinden sich die Iterator::Prefetch Ereignisse in den Threads tf_data_iterator_get_next . Da Prefetch asynchron ist, befinden sich seine Eingabeereignisse ( BatchV2 ) in einem anderen Thread und können durch Suchen nach dem Iterator::Prefetch::BatchV2 . In diesem Fall befinden sie sich im Thread tf_data_iterator_resource . Aus dem langen Namen können Sie ableiten, dass BatchV2 Prefetch . Darüber hinaus parent_id die parent_id des BatchV2 Ereignisses mit der ID des Prefetch Ereignisses überein.

Den Engpass identifizieren

Um den Engpass in Ihrer Eingabe-Pipeline zu identifizieren, führen Sie die Eingabe-Pipeline im Allgemeinen von der äußersten Transformation bis zur Quelle. Beginnen Sie ab der endgültigen Transformation in Ihrer Pipeline mit Upstream-Transformationen, bis Sie eine langsame Transformation finden oder einen Quelldatensatz wie TFRecord . Im obigen Beispiel würden Sie mit Prefetch und dann stromaufwärts zu BatchV2 , FiniteRepeat , Map und schließlich Range .

Im Allgemeinen entspricht eine langsame Transformation einer Transformation, deren Ereignisse lang sind, deren Eingabeereignisse jedoch kurz sind. Einige Beispiele folgen unten.

Beachten Sie, dass die letzte (äußerste) Transformation in den meisten Host-Eingabe-Pipelines das Ereignis Iterator::Model . Die Modelltransformation wird automatisch von der tf.data Laufzeit eingeführt und zum Instrumentieren und Autotuning der Leistung der Eingabepipeline verwendet.

Wenn Ihr Job eine Verteilungsstrategie verwendet , enthält der Trace-Viewer zusätzliche Ereignisse, die der Geräteeingabepipeline entsprechen. Die äußerste Transformation der IteratorGetNextOp::DoCompute (verschachtelt unter IteratorGetNextOp::DoCompute oder IteratorGetNextAsOptionalOp::DoCompute ) ist ein Iterator::Prefetch Ereignis mit einem Upstream- Iterator::Generator Ereignis. Sie finden die entsprechende Host-Pipeline, indem Sie nach Iterator::Model Ereignissen suchen.

Beispiel 1

image

Der obige Screenshot wird aus der folgenden Eingabe-Pipeline generiert:

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

Iterator::FlatMap im Screenshot, dass (1) Iterator::Map Ereignisse lang sind, (2) die Eingabeereignisse ( Iterator::FlatMap ) jedoch schnell zurückkehren. Dies deutet darauf hin, dass die sequentielle Map-Transformation der Engpass ist.

Beachten Sie, dass im Screenshot das Ereignis InstantiatedCapturedFunction::Run der Zeit entspricht, die zum Ausführen der Kartenfunktion benötigt wird.

Beispiel 2

image

Der obige Screenshot wird aus der folgenden Eingabe-Pipeline generiert:

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

Dieses Beispiel ähnelt dem obigen, verwendet jedoch ParallelMap anstelle von Map. Wir stellen hier fest, dass (1) Iterator::ParallelMap Ereignisse lang sind, aber (2) seine Eingabeereignisse Iterator::FlatMap (die sich in einem anderen Thread befinden, da ParallelMap asynchron ist) kurz sind. Dies deutet darauf hin, dass die ParallelMap-Transformation der Engpass ist.

Behebung des Engpasses

Quelldatensätze

Wenn Sie eine Dataset-Quelle als Engpass identifiziert haben, z. B. das Lesen aus TFRecord-Dateien, können Sie die Leistung verbessern, indem Sie die Datenextraktion parallelisieren. Stellen Sie dazu sicher, dass Ihre Daten über mehrere Dateien verteilt sind, und verwenden Sie tf.data.Dataset.interleave mit dem Parameter num_parallel_calls , der auf tf.data.experimental.AUTOTUNE . Wenn Determinismus für Ihr Programm nicht wichtig ist, können Sie die Leistung weiter verbessern, indem Sie das Flag deterministic=False auf tf.data.Dataset.interleave ab TF 2.2 setzen. Wenn Sie beispielsweise aus TFRecords lesen, können Sie Folgendes tun:

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

Beachten Sie, dass Sharded-Dateien ausreichend groß sein sollten, um den Aufwand für das Öffnen einer Datei zu verringern. Weitere Informationen zur parallelen Datenextraktion finden Sie in diesem Abschnitt des Leistungshandbuchs für tf.data .

Transformationsdatensätze

Wenn Sie eine Zwischen- tf.data Transformation als Engpass identifiziert haben, können Sie diese tf.data , indem Sie die Transformation parallelisieren oder die Berechnung zwischenspeichern, wenn Ihre Daten in den Speicher tf.data und dies angemessen ist. Einige Transformationen wie Map haben parallele Gegenstücke. tf.data Leistungshandbuch für tf.data zeigt, wie diese parallelisiert werden können. Andere Transformationen wie Filter , Unbatch und Batch sind von Natur aus sequentiell. Sie können sie parallelisieren, indem Sie „äußere Parallelität“ einführen. Angenommen, Ihre Eingabepipeline sieht zunächst wie folgt aus, mit Batch als Engpass:

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

Sie können "äußere Parallelität" einführen, indem Sie mehrere Kopien der Eingabepipeline über Sharded-Eingaben ausführen und die Ergebnisse kombinieren:

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)

Zusätzliche Ressourcen