Обзор
В этом руководстве предполагается знакомство с профилировщиком TensorFlow и tf.data
. Его цель — предоставить пошаговые инструкции с примерами, которые помогут пользователям диагностировать и устранять проблемы с производительностью входного конвейера.
Для начала соберите профиль вашего задания TensorFlow. Инструкции о том, как это сделать, доступны для процессоров/графических процессоров и облачных TPU .
Рабочий процесс анализа, подробно описанный ниже, сосредоточен на инструменте просмотра трассировки в 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).
Если звонки возвращаются быстро (<= 50 мкс), это означает, что ваши данные доступны, когда они запрошены. Входной конвейер не является вашим узким местом; дополнительные советы по анализу производительности см. в руководстве по профилировщику .
Если вызовы возвращаются медленно, tf.data
не сможет обрабатывать запросы потребителя. Перейдите к следующему разделу.
2. Вы выполняете предварительную выборку данных?
Лучший способ повысить производительность конвейера ввода — вставить преобразование tf.data.Dataset.prefetch
в конец конвейера tf.data
. Это преобразование перекрывает вычисления предварительной обработки входного конвейера со следующим шагом вычисления модели и необходимо для оптимальной производительности входного конвейера при обучении модели. Если вы выполняете предварительную выборку данных, вы должны увидеть срез Iterator::Prefetch
в том же потоке, что и IteratorGetNext::DoCompute
.
Если у вас нет 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>
из (короткого) имени, а другие наборы данных в длинном имени представляют последующие преобразования.
Например, приведенный выше снимок экрана был создан из следующего кода:
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
) события восходящих преобразований будут находиться в другом потоке. В таких случаях «длинное имя» может помочь вам определить, какому преобразованию в конвейере соответствует событие.
Например, приведенный выше снимок экрана был создан из следующего кода:
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
Приведенный выше снимок экрана создается из следующего входного конвейера:
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
Приведенный выше снимок экрана создается из следующего входного конвейера:
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)
Дополнительные ресурсы
- Руководство по производительности tf.data о том, как писать конвейеры ввода производительности
tf.data
- Видео изнутри TensorFlow: лучшие практики
tf.data
- Руководство по профайлеру
- Учебное пособие по профилировщику с Colab