Aperçu
Ce guide suppose que vous êtes 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 manière de procéder sont disponibles pour les CPU/GPU et les Cloud TPU .
Le flux de travail d'analyse détaillé ci-dessous se concentre sur l'outil de visionneuse de traces dans le profileur. Cet outil affiche une chronologie indiquant 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 traces, 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 le libellé "comp: data".
1. Votre pipeline tf.data
produit-il des données assez rapidement ?
Commencez par déterminer si le pipeline d'entrée est le goulot d'étranglement de votre programme TensorFlow.
Pour ce faire, recherchez IteratorGetNext::DoCompute
ops dans la visionneuse 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 keras ou itérez sur votre ensemble de données dans un 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).
Si les appels reviennent rapidement (<= 50 us), cela signifie que vos données sont disponibles lorsqu'elles sont demandées. Le pipeline d'entrée n'est pas votre goulot d'étranglement ; consultez le guide du profileur pour obtenir des conseils d'analyse des performances plus génériques.
Si les appels reviennent lentement, tf.data
n'est pas en mesure de répondre aux demandes du consommateur. Passez à la section suivante.
2. Pré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 nécessaire pour optimiser les performances du pipeline d'entrée lors de la formation 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' IteratorGetNext::DoCompute
.
Si vous n'avez pas de prefetch
à la fin de votre pipeline , vous devez en ajouter un. Pour plus d'informations sur les recommandations de performances de tf.data
, consultez le guide des performances de tf.data .
Si vous prélevez déjà des données et que le pipeline d'entrée constitue 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 de tirer le meilleur parti possible des 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 cloud si vous utilisez GCP.
Si votre utilisation est faible, cela suggère que votre pipeline d'entrée ne tire peut-être pas pleinement parti du processeur hôte. Vous devriez consulter le guide des performances tf.data pour connaître 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 de la limite de ressources , afin d'améliorer davantage les performances, vous devez soit améliorer l'efficacité de votre pipeline d'entrée (par exemple, en évitant 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 procéder consiste à 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 traces 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 porte 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 porte é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 jeux de données dans le nom long représentent les transformations en aval.
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
porte 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 synchrones tf.data
(telles que Batch
et Map
), vous verrez les événements des transformations en amont sur le même thread. Dans l'exemple ci-dessus, toutes les transformations utilisées étant 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.
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
. Étant donné que 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
. De plus, le parent_id
de l'événement BatchV2
correspondra à l'ID de l'événement Prefetch
.
Identification du 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 une récurrence dans les transformations en amont jusqu'à ce que vous trouviez une transformation lente ou atteigniez un jeu de données source, tel que TFRecord
. Dans l'exemple ci-dessus, vous commenceriez par Prefetch
, puis marcheriez en amont 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 hôtes est l'événement Iterator::Model
. La transformation de modèle est introduite automatiquement par le runtime tf.data
et est utilisée pour instrumenter et ajuster automatiquement les performances du pipeline d'entrée.
Si votre travail utilise une stratégie de distribution , la visionneuse de trace contiendra des événements supplémentaires qui correspondent au pipeline d'entrée de l'appareil. La transformation la plus externe du pipeline de périphérique (imbriquée sous IteratorGetNextOp::DoCompute
ou IteratorGetNextAsOptionalOp::DoCompute
) sera un événement Iterator::Prefetch
avec un événement Iterator::Generator
en amont. Vous pouvez trouver le pipeline hôte correspondant en recherchant les événements Iterator::Model
.
Exemple 1
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 Map séquentielle est le goulot d'étranglement.
Notez que dans la capture d'écran, l'événement InstantiatedCapturedFunction::Run
correspond au temps d'exécution de la fonction map.
Exemple 2
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 au précédent, mais utilise ParallelMap au lieu de Map. Nous remarquons ici que (1) les é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.
S'attaquer au goulot d'étranglement
Ensembles de données sources
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 de données. Pour ce faire, assurez-vous que vos données sont réparties sur plusieurs fichiers et utilisez tf.data.Dataset.interleave
avec le paramètre num_parallel_calls
défini sur tf.data.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.AUTOTUNE,
deterministic=False)
Notez que les fichiers partitionnés doivent être raisonnablement volumineux pour amortir la surcharge liée à 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 contreparties parallèles ; le guide des performances 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 le "parallélisme externe". Par exemple, supposons 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 un « 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.AUTOTUNE)
dataset = dataset.prefetch(tf.data.AUTOTUNE)
Ressources additionnelles
- tf.data guide de performance sur la façon d'écrire des pipelines d'entrée de performance
tf.data
- Vidéo Inside TensorFlow : bonnes pratiques
tf.data
- Guide du profileur
- Tutoriel sur le profileur avec Colab