Эта страница была переведа с помощью Cloud Translation API.
Switch to English

Анализируйте производительность tf.data с помощью TF Profiler

обзор

Это руководство предполагает знакомство с TensorFlow Profiler и tf.data . Его цель - предоставить пошаговые инструкции с примерами, которые помогут пользователям диагностировать и устранять проблемы с производительностью конвейера ввода.

Для начала соберите профиль вашей работы TensorFlow. Инструкции о том, как это сделать, доступны для процессоров / графических процессоров и облачных TPU .

TensorFlow Trace Viewer

Рабочий процесс анализа, подробно описанный ниже, ориентирован на средство просмотра трассировки в профилировщике. Этот инструмент отображает временную шкалу, которая показывает продолжительность операций, выполняемых вашей программой TensorFlow, и позволяет вам определить, какие операции выполняются дольше всего. Дополнительные сведения о средстве просмотра трассировки см. В этом разделе руководства TF Profiler. Как правило, события tf.data будут отображаться на шкале времени центрального процессора.

Рабочий процесс анализа

Пожалуйста, следуйте инструкциям ниже. Если у вас есть отзывы, которые помогут нам улучшить его, создайте проблему на github с меткой «comp: data».

1. Достаточно ли быстро ваш конвейер tf.data производит данные?

Начните с определения, является ли входной конвейер узким местом для вашей программы TensorFlow.

Для этого IteratorGetNext::DoCompute в программе просмотра трассировки IteratorGetNext::DoCompute ops. В общем, вы ожидаете увидеть их в начале шага. Эти срезы представляют время, необходимое вашему входному конвейеру, чтобы выдать пакет элементов по запросу. Если вы используете keras или перебираете свой набор данных в функции tf.function , их следует найти в tf_data_iterator_get_next .

Обратите внимание, что если вы используете стратегию распространения , вы можете видеть события IteratorGetNextAsOptional::DoCompute вместо IteratorGetNext::DoCompute ( IteratorGetNext::DoCompute с TF 2.3).

image

Если звонки возвращаются быстро (<= 50 нас), это означает, что ваши данные доступны по запросу. Входной конвейер не является вашим узким местом; см. руководство по Profiler для получения более общих советов по анализу производительности.

image

Если вызовы возвращаются медленно, tf.data не tf.data за запросами потребителя. Переходите к следующему разделу.

2. Вы выполняете предварительную выборку данных?

Лучшая практика для повышения производительности конвейера ввода - вставить преобразование tf.data.Dataset.prefetch в конец конвейера tf.data . Это преобразование перекрывает вычисление предварительной обработки входного конвейера со следующим этапом вычисления модели и требуется для оптимальной производительности входного конвейера при обучении вашей модели. Если вы выполняете предварительную выборку данных, вы должны увидеть срез Iterator::Prefetch в том же потоке, что и IteratorGetNext::DoCompute .

image

Если у вас нет prefetch в конце конвейера , вам следует добавить ее. Для получения более подробной информации о tf.data рекомендациях производительности см tf.data руководство производительности .

Если вы уже выполняете предварительную выборку данных , а конвейер ввода по-прежнему является вашим узким местом, перейдите к следующему разделу для дальнейшего анализа производительности.

3. Достигаете ли вы высокой загрузки ЦП?

tf.data обеспечивает высокую пропускную способность, пытаясь максимально использовать доступные ресурсы. В общем, даже при запуске вашей модели на ускорителе, таком как GPU или TPU, конвейеры tf.data запускаются на CPU. Вы можете проверить свою загрузку с помощью таких инструментов, как sar и htop , или в консоли облачного мониторинга, если вы используете GCP.

Если ваша загрузка низкая, это говорит о том, что ваш входной конвейер может не использовать все преимущества центрального процессора. Вам следует обратиться к руководству по производительности tf.data за лучшими практиками. Если вы применили передовой опыт, а коэффициент использования и пропускная способность остаются низкими, перейдите к анализу узких мест ниже.

Если ваше использование приближается к пределу ресурсов , для дальнейшего повышения производительности вам необходимо либо повысить эффективность вашего входного конвейера (например, избежать ненужных вычислений), либо разгрузить вычисления.

Вы можете повысить эффективность своего входного конвейера, избегая ненужных вычислений в tf.data . Один из способов сделать это - вставить преобразование tf.data.Dataset.cache после ресурсоемкой работы, если ваши данные умещаются в памяти; это сокращает объем вычислений за счет увеличения использования памяти. Кроме того, отключение внутриоперационного параллелизма в tf.data может повысить эффективность более чем на 10% и может быть выполнено путем установки следующей опции в вашем входном конвейере:

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

4. Анализ узких мест

В следующем разделе рассказывается, как читать события tf.data в средстве просмотра трассировки, чтобы понять, где находится узкое место, и возможные стратегии смягчения.

Понимание событий tf.data в tf.data

Каждое событие tf.data в tf.data имеет имя Iterator::<Dataset> , где <Dataset> - имя источника или преобразования набора данных. Каждое событие также имеет длинное имя Iterator::<Dataset_1>::...::<Dataset_n> , которое вы можете увидеть, щелкнув событие tf.data . В длинном имени <Dataset_n> соответствует <Dataset> из (короткого) имени, а другие наборы данных в длинном имени представляют собой нисходящие преобразования.

image

Например, приведенный выше снимок экрана был создан из следующего кода:

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

Здесь событие Iterator::Map имеет длинное имя Iterator::BatchV2::FiniteRepeat::Map . Обратите внимание, что имя набора данных может немного отличаться от API Python (например, FiniteRepeat вместо Repeat), но должно быть достаточно интуитивно понятным для анализа.

Синхронные и асинхронные преобразования

Для синхронных преобразований tf.data (таких как Batch и Map ) вы увидите события из восходящих преобразований в том же потоке. В приведенном выше примере, поскольку все используемые преобразования являются синхронными, все события появляются в одном потоке.

Для асинхронных преобразований (таких как Prefetch , ParallelMap , ParallelInterleave и MapAndBatch ) события из восходящих преобразований будут в другом потоке. В таких случаях «длинное имя» может помочь вам определить, какому преобразованию в конвейере соответствует событие.

image

Например, приведенный выше снимок экрана был создан из следующего кода:

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

Здесь события Iterator::Prefetch находятся в tf_data_iterator_get_next . Поскольку Prefetch является асинхронной, ее входные события ( BatchV2 ) будут находиться в другом потоке, и их можно найти, Iterator::Prefetch::BatchV2 поиск по длинному имени Iterator::Prefetch::BatchV2 . В этом случае они находятся в потоке tf_data_iterator_resource . Из его длинного названия можно сделать вывод, что BatchV2 находится выше по течению от Prefetch . Кроме того, parent_id в BatchV2 случае будет соответствовать идентификатор Prefetch события.

Выявление узкого места

В общем, чтобы определить узкое место в вашем входном конвейере, пройдитесь по входному конвейеру от самого внешнего преобразования до источника. Начиная с последнего преобразования в конвейере, рекурсивно переходите к преобразованиям восходящего потока, пока не обнаружите медленное преобразование или не дойдете до исходного набора данных, такого как TFRecord . В приведенном выше примере вы должны начать с Prefetch , а затем BatchV2 к BatchV2 , FiniteRepeat , Map и, наконец, Range .

В общем, медленное преобразование соответствует тому, чьи события длинные, но чьи входные события короткие. Ниже приведены некоторые примеры.

Обратите внимание, что последнее (самое внешнее) преобразование в большинстве входных конвейеров хоста - это событие Iterator::Model . Преобразование модели вводится автоматически tf.data выполнения tf.data и используется для инструментария и автонастройки производительности входного конвейера.

Если ваша работа использует стратегию распространения , средство просмотра трассировки будет содержать дополнительные события, соответствующие конвейеру ввода устройства. Самым внешним преобразованием конвейера устройства (вложенным в IteratorGetNextOp::DoCompute или IteratorGetNextAsOptionalOp::DoCompute ) будет событие Iterator::Prefetch с восходящим событием Iterator::Generator . Вы можете найти соответствующий конвейер хоста, выполнив поиск событий Iterator::Model .

Пример 1

image

Приведенный выше снимок экрана создается из следующего входного конвейера:

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

На Iterator::FlatMap внимание, что (1) события Iterator::Map длинные, но (2) его входные события ( Iterator::FlatMap ) возвращаются быстро. Это говорит о том, что последовательное преобразование карты является узким местом.

Обратите внимание, что на снимке экрана событие InstantiatedCapturedFunction::Run соответствует времени, необходимому для выполнения функции карты.

Пример 2

image

Приведенный выше снимок экрана создан из следующего входного конвейера:

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

Этот пример похож на предыдущий, но использует ParallelMap вместо Map. Мы замечаем, что (1) события Iterator::ParallelMap длинные, но (2) его входные события Iterator::FlatMap (которые находятся в другом потоке, поскольку ParallelMap является асинхронным) короткими. Это говорит о том, что преобразование ParallelMap является узким местом.

Устранение узкого места

Исходные наборы данных

Если вы определили источник набора данных как узкое место, например чтение из файлов TFRecord, вы можете повысить производительность за счет распараллеливания извлечения данных. Для этого убедитесь, что ваши данные разделены на несколько файлов, и используйте tf.data.Dataset.interleave с параметром num_parallel_calls установленным на tf.data.experimental.AUTOTUNE . Если детерминизм не важен для вашей программы, вы можете еще больше повысить производительность, установив в tf.data.Dataset.interleave deterministic=False tf.data.Dataset.interleave с TF 2.2. Например, если вы читаете TFRecords, вы можете сделать следующее:

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

Обратите внимание, что сегментированные файлы должны быть достаточно большими, чтобы компенсировать накладные расходы на открытие файла. Дополнительные сведения о параллельном извлечении данных см. В этом разделе руководства по производительности tf.data .

Наборы данных преобразования

Если вы определили промежуточное преобразование tf.data как узкое место, вы можете решить его, распараллеливая преобразование или кэшируя вычисления, если ваши данные умещаются в памяти и это уместно. Некоторые преобразования, такие как Map имеют параллельные аналоги; tf.data производительности tf.data демонстрирует, как их распараллелить. Другие преобразования, такие как Filter , Unbatch и Batch , по своей сути являются последовательными; их можно распараллелить, введя «внешний параллелизм». Например, предположим, что ваш входной конвейер изначально выглядит следующим образом, с Batch в качестве узкого места:

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

Вы можете ввести «внешний параллелизм», запустив несколько копий входного конвейера поверх сегментированных входов и объединив результаты:

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)

Дополнительные ресурсы