Cette page a été traduite par l'API Cloud Translation.
Switch to English

Analysez les performances de tf.data avec TF Profiler

Aperçu

Ce guide suppose que vous tf.data familiarisé avec TensorFlow Profiler et tf.data . Il vise à fournir des instructions étape par étape avec des exemples pour aider les utilisateurs à diagnostiquer et à résoudre les problèmes de performances du pipeline d'entrée.

Pour commencer, collectez un profil de votre tâche TensorFlow. Des instructions sur la façon de procéder sont disponibles pour les CPU / GPU et les Cloud TPU .

TensorFlow Trace Viewer

Le flux de travail d'analyse détaillé ci-dessous se concentre sur l'outil de visualisation de trace dans le profileur. Cet outil affiche une chronologie qui montre la durée des opérations exécutées par votre programme TensorFlow et vous permet d'identifier les opérations qui prennent le plus de temps à s'exécuter. Pour plus d'informations sur la visionneuse de trace, consultez cette section du guide TF Profiler. En général, les événements tf.data apparaîtront sur la chronologie du processeur hôte.

Flux de travail d'analyse

Veuillez suivre le flux de travail ci-dessous. Si vous avez des commentaires pour nous aider à l'améliorer, veuillez créer un problème github avec l'étiquette «comp: data».

1. Votre pipeline tf.data produit-il des données assez rapidement?

Commencez par vérifier si le pipeline d'entrée est le goulot d'étranglement de votre programme TensorFlow.

Pour ce faire, recherchez les opérations IteratorGetNext::DoCompute dans le visualiseur de trace. En général, vous vous attendez à les voir au début d'une étape. Ces tranches représentent le temps nécessaire à votre pipeline d'entrée pour produire un lot d'éléments lorsqu'il est demandé. Si vous utilisez des keras ou que vous effectuez une itération sur votre ensemble de données dans une fonction tf.function , ceux-ci doivent être trouvés dans les threads tf_data_iterator_get_next .

Notez que si vous utilisez une stratégie de distribution , vous pouvez voir des événements IteratorGetNextAsOptional::DoCompute au lieu de IteratorGetNext::DoCompute (à partir de TF 2.3).

image

Si les appels reviennent rapidement (<= 50 us), cela signifie que vos données sont disponibles au moment où elles sont demandées. Le pipeline d'entrée n'est pas votre goulot d'étranglement; consultez le guide Profiler pour des conseils d'analyse des performances plus génériques.

image

Si les appels reviennent lentement, tf.data est incapable de suivre les demandes du consommateur. Passez à la section suivante.

2. Préléchargez-vous les données?

La meilleure pratique pour les performances du pipeline d'entrée consiste à insérer une transformation tf.data.Dataset.prefetch à la fin de votre pipeline tf.data . Cette transformation chevauche le calcul de prétraitement du pipeline d'entrée avec l'étape suivante du calcul du modèle et est requise pour des performances optimales du pipeline d'entrée lors de l'entraînement de votre modèle. Si vous préchargez des données, vous devriez voir une tranche Iterator::Prefetch sur le même thread que l'op IteratorGetNext::DoCompute .

image

Si vous n'avez pas de prefetch à la fin de votre pipeline , vous devez en ajouter une. Pour plus d'informations sur les recommandations de performances de tf.data , consultez le guide de performances de tf.data .

Si vous préchargez déjà des données et que le pipeline d'entrée est toujours votre goulot d'étranglement, passez à la section suivante pour analyser plus en détail les performances.

3. Atteignez-vous une utilisation élevée du processeur?

tf.data atteint un débit élevé en essayant d'utiliser au mieux les ressources disponibles. En général, même lorsque vous exécutez votre modèle sur un accélérateur comme un GPU ou un TPU, les pipelines tf.data sont exécutés sur le CPU. Vous pouvez vérifier votre utilisation avec des outils tels que sar et htop , ou dans la console de surveillance du cloud si vous utilisez GCP.

Si votre utilisation est faible, cela suggère que votre pipeline d'entrée ne tire pas pleinement parti du processeur hôte. Vous devriez consulter le guide des performances de tf.data pour les meilleures pratiques. Si vous avez appliqué les meilleures pratiques et que l'utilisation et le débit restent faibles, passez à l'analyse des goulots d' étranglement ci-dessous.

Si votre utilisation approche la limite des ressources , afin d'améliorer encore les performances, vous devez soit améliorer l'efficacité de votre pipeline d'entrée (par exemple, éviter les calculs inutiles), soit décharger le calcul.

Vous pouvez améliorer l'efficacité de votre pipeline d'entrée en évitant les calculs inutiles dans tf.data . Une façon de faire est d'insérer une transformation tf.data.Dataset.cache après un travail intensif en calcul si vos données tiennent dans la mémoire; cela réduit le calcul au prix d'une utilisation accrue de la mémoire. De plus, la désactivation du parallélisme intra-op dans tf.data a le potentiel d'augmenter l'efficacité de> 10%, et peut être effectuée en définissant l'option suivante sur votre pipeline d'entrée:

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

4. Analyse des goulots d'étranglement

La section suivante explique comment lire les événements tf.data dans la visionneuse de trace pour comprendre où se trouve le goulot d'étranglement et les stratégies d'atténuation possibles.

Comprendre les événements tf.data dans le profileur

Chaque événement tf.data dans le profileur a le nom Iterator::<Dataset> , où <Dataset> est le nom de la source ou de la transformation de l'ensemble de données. Chaque événement a également le nom long Iterator::<Dataset_1>::...::<Dataset_n> , que vous pouvez voir en cliquant sur l'événement tf.data . Dans le nom long, <Dataset_n> correspond à <Dataset> partir du nom (court), et les autres ensembles de données dans le nom long représentent les transformations en aval.

image

Par exemple, la capture d'écran ci-dessus a été générée à partir du code suivant:

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

Ici, l'événement Iterator::Map a le nom long Iterator::BatchV2::FiniteRepeat::Map . Notez que le nom des ensembles de données peut différer légèrement de l'API python (par exemple, FiniteRepeat au lieu de Repeat), mais doit être suffisamment intuitif pour être analysé.

Transformations synchrones et asynchrones

Pour les transformations tf.data synchrones (telles que Batch et Map ), vous verrez les événements des transformations en amont sur le même thread. Dans l'exemple ci-dessus, comme toutes les transformations utilisées sont synchrones, tous les événements apparaissent sur le même thread.

Pour les transformations asynchrones (telles que Prefetch , ParallelMap , ParallelInterleave et MapAndBatch ), les événements des transformations en amont seront sur un thread différent. Dans de tels cas, le «nom long» peut vous aider à identifier à quelle transformation d'un pipeline correspond un événement.

image

Par exemple, la capture d'écran ci-dessus a été générée à partir du code suivant:

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

Ici, les événements Iterator::Prefetch sont sur les threads tf_data_iterator_get_next . Puisque Prefetch est asynchrone, ses événements d'entrée ( BatchV2 ) seront sur un thread différent et peuvent être localisés en recherchant le nom long Iterator::Prefetch::BatchV2 . Dans ce cas, ils se trouvent sur le thread tf_data_iterator_resource . De son nom long, vous pouvez déduire que BatchV2 est en amont de Prefetch . En outre, le parent_id de l'événement BatchV2 correspondra à l'ID de l'événement Prefetch .

Identifier le goulot d'étranglement

En général, pour identifier le goulot d'étranglement dans votre pipeline d'entrée, parcourez le pipeline d'entrée de la transformation la plus externe jusqu'à la source. À partir de la transformation finale de votre pipeline, effectuez de nouveau des transformations en amont jusqu'à ce que vous trouviez une transformation lente ou atteigniez un ensemble de données source, tel que TFRecord . Dans l'exemple ci-dessus, vous commenceriez à partir de Prefetch , puis BatchV2 vers BatchV2 , FiniteRepeat , Map et enfin Range .

En général, une transformation lente correspond à une transformation dont les événements sont longs, mais dont les événements d'entrée sont courts. Quelques exemples suivent ci-dessous.

Notez que la transformation finale (la plus externe) dans la plupart des pipelines d'entrée de l'hôte est l'événement Iterator::Model . La transformation Modèle est introduite automatiquement par le runtime tf.data et est utilisée pour l'instrumentation et le réglage automatique des performances du pipeline d'entrée.

Si votre travail utilise une stratégie de distribution , le visualiseur de trace contiendra des événements supplémentaires qui correspondent au pipeline d'entrée du périphérique. La transformation la plus externe du pipeline de périphériques (imbriquée sous IteratorGetNextOp::DoCompute ou IteratorGetNextAsOptionalOp::DoCompute ) sera un événement Iterator::Prefetch avec un événement Iterator::Generator amont. Vous pouvez trouver le pipeline hôte correspondant en recherchant les événements Iterator::Model .

Exemple 1

image

La capture d'écran ci-dessus est générée à partir du pipeline d'entrée suivant:

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

Dans la capture d'écran, observez que (1) les événements Iterator::Map sont longs, mais (2) ses événements d'entrée ( Iterator::FlatMap ) reviennent rapidement. Cela suggère que la transformation séquentielle de la carte est le goulot d'étranglement.

Notez que dans la capture d'écran, l'événement InstantiatedCapturedFunction::Run correspond au temps nécessaire pour exécuter la fonction de carte.

Exemple 2

image

La capture d'écran ci-dessus est générée à partir du pipeline d'entrée suivant:

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

Cet exemple est similaire à celui ci-dessus, mais utilise ParallelMap au lieu de Map. Nous remarquons ici que (1) Iterator::ParallelMap événements Iterator::ParallelMap sont longs, mais (2) ses événements d'entrée Iterator::FlatMap (qui sont sur un thread différent, puisque ParallelMap est asynchrone) sont courts. Cela suggère que la transformation ParallelMap est le goulot d'étranglement.

Remédier au goulot d'étranglement

Ensembles de données source

Si vous avez identifié une source d'ensemble de données comme goulot d'étranglement, comme la lecture à partir de fichiers TFRecord, vous pouvez améliorer les performances en parallélisant l'extraction des données. Pour ce faire, assurez-vous que vos données sont partagées entre plusieurs fichiers et utilisez tf.data.Dataset.interleave avec le paramètre num_parallel_calls défini sur tf.data.experimental.AUTOTUNE . Si le déterminisme n'est pas important pour votre programme, vous pouvez encore améliorer les performances en définissant l'indicateur deterministic=False sur tf.data.Dataset.interleave partir de TF 2.2. Par exemple, si vous lisez à partir de TFRecords, vous pouvez effectuer les opérations suivantes:

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

Notez que les fichiers partitionnés doivent être raisonnablement volumineux pour amortir les frais généraux liés à l'ouverture d'un fichier. Pour plus de détails sur l'extraction de données en parallèle, consultez cette section du guide des performances de tf.data .

Ensembles de données de transformation

Si vous avez identifié une transformation tf.data intermédiaire comme goulot d'étranglement, vous pouvez y remédier en parallélisant la transformation ou en mettant en cache le calcul si vos données tiennent dans la mémoire et que cela est approprié. Certaines transformations telles que Map ont des équivalents parallèles; le guide des performances de tf.data montre comment les paralléliser. D'autres transformations, telles que Filter , Unbatch et Batch sont intrinsèquement séquentielles; vous pouvez les paralléliser en introduisant un «parallélisme externe». Par exemple, en supposant que votre pipeline d'entrée ressemble initialement à ce qui suit, avec Batch comme goulot d'étranglement:

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

Vous pouvez introduire le «parallélisme externe» en exécutant plusieurs copies du pipeline d'entrée sur des entrées partitionnées et en combinant les résultats:

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)

Ressources additionnelles