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

Обзор

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

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

TensorFlow Trace Viewer

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

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

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

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

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

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

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

image

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

image

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

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

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

image

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

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

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

tf.data достигает высокой пропускной способности, пытаясь максимально эффективно использовать доступные ресурсы. В общем, даже при запуске вашей модели на ускорителе, таком как графический процессор или TPU, конвейеры tf.data выполняются на процессоре. Вы можете проверить использование с помощью таких инструментов, как 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 в профилировщике имеет имя 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 . В данном случае они находятся в потоке tf_data_iterator_resource . Из его длинного названия можно сделать вывод, что BatchV2 находится выше Prefetch . Кроме того, parent_id события BatchV2 будет соответствовать идентификатору события Prefetch .

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

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

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

Обратите внимание, что последним (самым внешним) преобразованием в большинстве конвейеров ввода хоста является событие Iterator::Model . Преобразование модели автоматически вводится средой выполнения 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()

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

Обратите внимание, что на снимке экрана событие 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()

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

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

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

Если вы определили источник набора данных как узкое место, например чтение из файлов TFRecord, вы можете повысить производительность, распараллелив извлечение данных. Для этого убедитесь, что ваши данные разбиты на несколько файлов, и используйте tf.data.Dataset.interleave с параметром num_parallel_calls , установленным в tf.data.AUTOTUNE . Если детерминизм не важен для вашей программы, вы можете еще больше повысить производительность, установив флаг 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.AUTOTUNE,
  deterministic=False)

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

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

Если вы определили промежуточное преобразование tf.data как узкое место, вы можете устранить его, распараллелив преобразование или кэшировав вычисления , если ваши данные помещаются в память и это подходит. Некоторые преобразования, такие как Map , имеют параллельные аналоги; руководство по производительности 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.AUTOTUNE)
dataset = dataset.prefetch(tf.data.AUTOTUNE)

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