Оптимизируйте производительность графического процессора TensorFlow с помощью профилировщика TensorFlow.

Обзор

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

Если вы новичок в Profiler:

Имейте в виду, что перенос вычислений на GPU не всегда может быть выгоден, особенно для небольших моделей. Накладные расходы могут быть из-за:

  • Передача данных между хостом (CPU) и устройством (GPU); а также
  • Из-за задержки, возникающей, когда хост запускает ядра графического процессора.

Рабочий процесс оптимизации производительности

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

Рекомендуется отлаживать проблемы с производительностью в следующем порядке:

  1. Оптимизируйте и отладьте производительность на одном графическом процессоре:
    1. Проверьте, не является ли входной конвейер узким местом.
    2. Отладка производительности одного графического процессора.
    3. Включите смешанную точность (с fp16 (float16)) и при необходимости включите XLA .
  2. Оптимизируйте и отладьте производительность на одном хосте с несколькими графическими процессорами.

Например, если вы используете стратегию распределения TensorFlow для обучения модели на одном хосте с несколькими графическими процессорами и замечаете неоптимальное использование графического процессора, вам следует сначала оптимизировать и отладить производительность для одного графического процессора, прежде чем отлаживать систему с несколькими графическими процессорами.

В качестве основы для получения производительного кода на графических процессорах в этом руководстве предполагается, что вы уже используете tf.function . API-интерфейсы Keras Model.compile и Model.fit будут автоматически использовать tf.function под капотом. При написании пользовательского цикла обучения с помощью tf.GradientTape обратитесь к разделу Повышение производительности с помощью tf.function , чтобы узнать, как включить tf.function s.

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

1. Оптимизируйте производительность на одном графическом процессоре

В идеальном случае ваша программа должна иметь высокую загрузку графического процессора, минимальное взаимодействие ЦП (хост) с графическим процессором (устройством) и отсутствие накладных расходов от входного конвейера.

Первым шагом в анализе производительности является получение профиля для модели, работающей с одним GPU.

Обзорная страница TensorBoard Profiler, на которой показано общее представление о том, как ваша модель работала во время выполнения профиля, может дать представление о том, насколько ваша программа далека от идеального сценария.

TensorFlow Profiler Overview Page

Ключевые цифры, на которые следует обратить внимание на обзорной странице:

  1. Сколько времени шага приходится на фактическое выполнение устройства
  2. Процент операций, размещенных на устройстве по сравнению с хостом
  3. Сколько ядер использует fp16

Достижение оптимальной производительности означает максимизацию этих показателей во всех трех случаях. Чтобы получить более глубокое представление о вашей программе, вам необходимо ознакомиться с TensorBoard's Profiler trace viewer . В разделах ниже показаны некоторые распространенные шаблоны средства просмотра трассировки, на которые следует обращать внимание при диагностике узких мест в производительности.

Ниже приведено изображение представления трассировки модели, работающего на одном графическом процессоре. В разделах TensorFlow Name Scope и TensorFlow Ops можно определить различные части модели, такие как прямой проход, функция потерь, обратный проход/вычисление градиента и обновление веса оптимизатора. Вы также можете запускать операции на графическом процессоре рядом с каждым Stream , которые относятся к потокам CUDA. Каждый поток используется для определенных задач. В этой трассировке Stream#118 используется для запуска вычислительных ядер и копий с устройства на устройство. Поток № 119 используется для копирования с хоста на устройство, а поток № 120 — для копирования с устройства на хост.

На приведенной ниже трассировке показаны общие характеристики производительной модели.

image

Например, временная шкала вычислений графического процессора ( Stream#118 ) выглядит «занятой» с очень небольшим количеством пробелов. Минимум копий с хоста на устройство ( поток #119 ) и с устройства на хост ( поток #120 ), а также минимальные промежутки между шагами. Когда вы запускаете Profiler для своей программы, вы, возможно, не сможете идентифицировать эти идеальные характеристики в своем представлении трассировки. В остальной части этого руководства рассматриваются распространенные сценарии и способы их устранения.

1. Отладка входного конвейера

Первым шагом в отладке производительности графического процессора является определение того, привязана ли ваша программа к вводу. Самый простой способ выяснить это — использовать анализатор Input-pipeline от Profiler, на TensorBoard, который предоставляет обзор времени, проведенного во входном конвейере.

image

Вы можете предпринять следующие возможные действия, если ваш входной конвейер значительно влияет на время шага:

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

Кроме того, обратитесь к рекомендациям по оптимизации конвейера входных данных .

2. Отладка производительности одного графического процессора

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

1. Проанализируйте промежутки между шагами

Распространенным наблюдением, когда ваша программа работает не оптимально, являются промежутки между этапами обучения. На представленном ниже изображении трассировки между шагами 8 и 9 имеется большой разрыв, означающий, что GPU в это время простаивает.

image

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

Однако даже с оптимизированным входным конвейером между концом одного шага и началом другого могут возникать промежутки из-за конкуренции за потоки ЦП. tf.data использует фоновые потоки для распараллеливания конвейерной обработки. Эти потоки могут мешать действиям на стороне хоста GPU, которые происходят в начале каждого шага, например копированию данных или планированию операций GPU.

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

Промежутки между шагами также могут быть вызваны расчетами метрик, обратными вызовами Keras или операциями вне tf.function , которые выполняются на хосте. У этих операций не такая хорошая производительность, как у операций внутри графа TensorFlow. Кроме того, некоторые из этих операций выполняются на ЦП и копируют тензоры с графического процессора туда и обратно.

Если после оптимизации входного конвейера вы по-прежнему замечаете пробелы между шагами в средстве просмотра трассировки, вам следует просмотреть код модели между шагами и проверить, улучшает ли производительность отключение обратных вызовов/метрик. Некоторые сведения об этих операциях также находятся в средстве просмотра трассировки (как на стороне устройства, так и на стороне хоста). В этом сценарии рекомендуется амортизировать накладные расходы на эти операции, выполняя их после фиксированного количества шагов, а не после каждого шага. При использовании метода compile в API tf.keras установка флага experimental_steps_per_execution делает это автоматически. Для пользовательских циклов обучения используйте tf.while_loop .

2. Добейтесь более высокой степени использования устройства

1. Небольшие ядра графического процессора и задержки запуска ядра хоста.

Хост ставит ядра в очередь для запуска на графическом процессоре, но существует задержка (около 20-40 мкс) перед тем, как ядра будут фактически выполняться на графическом процессоре. В идеальном случае хост ставит в очередь достаточное количество ядер на GPU, так что GPU тратит большую часть своего времени на выполнение, а не ждет, пока хост поставит в очередь больше ядер.

Обзорная страница Profiler на TensorBoard показывает, сколько времени GPU простаивал из-за ожидания на хосте запуска ядер. На изображении ниже GPU бездействует около 10% времени шага, ожидая запуска ядер.

image

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

image

Запустив множество небольших операций на GPU (например, скалярное добавление), хост может не успевать за GPU. Инструмент TensorFlow Stats в TensorBoard для того же профиля показывает 126 224 операции Mul, занимающие 2,77 секунды. Таким образом, время каждого ядра составляет около 21,9 мкс, что очень мало (примерно столько же, сколько задержка запуска) и потенциально может привести к задержкам запуска ядра хоста.

image

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

  • Объедините небольшие тензоры и используйте векторизованные операции или используйте больший размер пакета, чтобы каждое запущенное ядро ​​выполняло больше работы, что дольше будет загружать GPU.
  • Убедитесь, что вы используете tf.function для создания графов TensorFlow, чтобы вы не запускали операции в чистом нетерпеливом режиме. Если вы используете Model.fit (в отличие от пользовательского цикла обучения с tf.GradientTape ), то tf.keras.Model.compile автоматически сделает это за вас.
  • Объединение ядер с помощью XLA с tf.function(jit_compile=True) или автокластеризация. Дополнительные сведения см. в разделе Включение смешанной точности и XLA ниже, чтобы узнать, как включить XLA для повышения производительности. Эта функция может привести к высокой загрузке устройства.
2. Опция TensorFlow

На странице обзора Profiler показано процентное соотношение операций, размещенных на хосте, по сравнению с устройством (вы также можете проверить размещение определенных операций, просмотрев средство просмотра трассировки . Как на изображении ниже, вам нужно процентное соотношение операций на хосте). быть очень маленьким по сравнению с устройством.

image

В идеале большинство операций с интенсивными вычислениями следует размещать на графическом процессоре.

Чтобы узнать, каким устройствам назначены операции и тензоры в вашей модели, установите tf.debugging.set_log_device_placement(True) в качестве первого оператора вашей программы.

Обратите внимание, что в некоторых случаях, даже если вы укажете операцию для размещения на определенном устройстве, ее реализация может переопределить это условие (пример: tf.unique ). Даже для обучения с одним GPU указание стратегии распределения, такой как tf.distribute.OneDeviceStrategy , может привести к более детерминированному размещению операций на вашем устройстве.

Одной из причин размещения большинства операций на графическом процессоре является предотвращение чрезмерного копирования памяти между хостом и устройством (ожидается копирование памяти для данных ввода/вывода модели между хостом и устройством). Пример избыточного копирования показан в представлении трассировки ниже для потоков графического процессора #167 , #168 и #169 .

image

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

3. Более эффективные ядра на графических процессорах

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

1. Используйте тензорные ядра

Современные графические процессоры NVIDIA® имеют специализированные тензорные ядра , которые могут значительно повысить производительность подходящих ядер.

Вы можете использовать статистику ядра графического процессора TensorBoard, чтобы визуализировать, какие ядра графического процессора подходят для Tensor Core, а какие ядра используют Tensor Cores. Включение fp16 (см. раздел «Включение смешанной точности» ниже) — это один из способов заставить ядра вашей программы General Matrix Multiply (GEMM) (matmul ops) использовать Tensor Core. Ядра графического процессора эффективно используют тензорные ядра, когда точность равна fp16, а размеры тензора ввода/вывода кратны 8 или 16 (для int8 ).

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

2. Предохранители

Используйте tf.function(jit_compile=True) для объединения небольших операций в более крупные ядра, что приводит к значительному увеличению производительности. Чтобы узнать больше, обратитесь к руководству XLA .

3. Включите смешанную точность и XLA

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

1. Включите смешанную точность

В руководстве по смешанной точности TensorFlow показано, как включить точность fp16 на графических процессорах. Включите AMP на графических процессорах NVIDIA®, чтобы использовать тензорные ядра и получить общее ускорение до 3 раз по сравнению с использованием только fp32 (float32) на Volta и более новых архитектурах графических процессоров.

Убедитесь, что размеры матрицы/тензора удовлетворяют требованиям для вызова ядер, использующих тензорные ядра. Ядра графического процессора эффективно используют тензорные ядра, когда точность составляет fp16, а размерность ввода/вывода кратна 8 или 16 (для int8).

Обратите внимание, что в cuDNN v7.6.3 и более поздних версиях размеры свертки будут автоматически дополняться там, где это необходимо для использования тензорных ядер.

Следуйте приведенным ниже рекомендациям, чтобы максимизировать преимущества точности fp16 в производительности.

1. Используйте оптимальные ядра fp16

При включенном fp16 ядра матричных умножений (GEMM) вашей программы должны использовать соответствующую версию fp16 , в которой используются тензорные ядра. Однако в некоторых случаях этого не происходит, и вы не получаете ожидаемого ускорения от включения fp16 , поскольку вместо этого ваша программа возвращается к неэффективной реализации.

image

На странице статистики ядра графического процессора показано, какие операции подходят для Tensor Core и какие ядра фактически используют эффективное Tensor Core. Руководство NVIDIA® по производительности глубокого обучения содержит дополнительные рекомендации по использованию тензорных ядер. Кроме того, преимущества использования fp16 также проявятся в ядрах, которые ранее были привязаны к памяти, так как теперь операции будут занимать вдвое меньше времени.

2. Динамическое и статическое масштабирование потерь

Масштабирование потерь необходимо при использовании fp16 для предотвращения потери памяти из-за низкой точности. Существует два типа масштабирования потерь, динамическое и статическое, оба из которых более подробно описаны в руководстве Mixed Precision . Вы можете использовать политикуmixed_float16 для автоматического включения масштабирования потерь в оптимизаторе mixed_float16 .

Пытаясь оптимизировать производительность, важно помнить, что динамическое масштабирование потерь может ввести дополнительные условные операции, которые выполняются на хосте, и привести к промежуткам, которые будут видны между шагами в средстве просмотра трассировки. С другой стороны, масштабирование статических потерь не имеет таких накладных расходов и может быть лучшим вариантом с точки зрения производительности с загвоздкой в ​​том, что вам нужно указать правильное значение масштаба статических потерь.

2. Включите XLA с помощью tf.function(jit_compile=True) или автоматической кластеризации.

В качестве последнего шага в достижении максимальной производительности с одним графическим процессором вы можете поэкспериментировать с включением XLA, что позволит объединить операции и приведет к лучшему использованию устройства и меньшему объему памяти. Подробнее о том, как включить XLA в вашей программе с помощью tf.function(jit_compile=True) или автоматической кластеризации, см. в руководстве по XLA .

Вы можете установить глобальный уровень JIT на -1 (выкл.), 1 или 2 . Более высокий уровень является более агрессивным и может уменьшить параллелизм и использовать больше памяти. Установите значение 1 , если у вас есть ограничения по памяти. Обратите внимание, что XLA плохо работает с моделями с переменными входными тензорными формами, поскольку компилятору XLA придется продолжать компилировать ядра каждый раз, когда он сталкивается с новыми формами.

2. Оптимизируйте производительность на одном хосте с несколькими графическими процессорами.

API tf.distribute.MirroredStrategy можно использовать для масштабирования обучения модели с одного графического процессора на несколько графических процессоров на одном хосте. (Чтобы узнать больше о том, как проводить распределенное обучение с помощью TensorFlow, см. руководства « Распределенное обучение с TensorFlow », «Использование графического процессора » и « Использование TPU », а также руководство « Распределенное обучение с помощью Keras ».)

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

При переходе от обучения с одним графическим процессором к нескольким графическим процессорам на одном хосте в идеале вы должны испытать масштабирование производительности только с дополнительными накладными расходами на градиентную связь и увеличенное использование потоков хоста. Из-за этих накладных расходов у вас не будет точного двукратного ускорения, если вы, например, перейдете с 1 на 2 графических процессора.

Представление трассировки ниже показывает пример дополнительных затрат на связь при обучении на нескольких графических процессорах. Существуют некоторые накладные расходы, связанные с объединением градиентов, передачей их между репликами и их разделением перед выполнением обновления веса.

image

Следующий контрольный список поможет вам повысить производительность при оптимизации производительности в сценарии с несколькими графическими процессорами:

  1. Постарайтесь максимально увеличить размер пакета, что приведет к более высокому использованию устройства и амортизирует затраты на связь между несколькими графическими процессорами. Использование профилировщика памяти помогает понять, насколько ваша программа близка к пиковому использованию памяти. Обратите внимание, что, хотя больший размер пакета может повлиять на конвергенцию, это обычно перевешивается преимуществами производительности.
  2. При переходе с одного графического процессора на несколько графических процессоров одному и тому же хосту теперь приходится обрабатывать гораздо больше входных данных. Итак, после (1) рекомендуется перепроверить производительность входного конвейера и убедиться, что он не является узким местом.
  3. Проверьте временную шкалу графического процессора в представлении трассировки вашей программы на наличие ненужных вызовов AllReduce, так как это приводит к синхронизации на всех устройствах. В представлении трассировки, показанном выше, AllReduce выполняется через ядро ​​NCCL , и на каждом графическом процессоре есть только один вызов NCCL для градиентов на каждом шаге.
  4. Проверьте наличие ненужных операций копирования D2H, H2D и D2D, которые можно свести к минимуму.
  5. Проверьте время шага, чтобы убедиться, что каждая реплика выполняет одинаковую работу. Например, может случиться так, что один GPU (обычно GPU0 ) перегружен, потому что хост по ошибке в конечном итоге увеличивает нагрузку на него.
  6. Наконец, проверьте шаг обучения для всех графических процессоров в представлении трассировки для любых операций, которые выполняются последовательно. Обычно это происходит, когда ваша программа включает зависимости управления от одного графического процессора к другому. В прошлом отладка производительности в этой ситуации решалась в каждом конкретном случае. Если вы наблюдаете такое поведение в своей программе, зарегистрируйте проблему GitHub с изображениями вашего представления трассировки.

1. Оптимизируйте градиент AllReduce

При обучении с синхронной стратегией каждое устройство получает часть входных данных.

После вычисления прямых и обратных проходов по модели градиенты, рассчитанные на каждом устройстве, необходимо агрегировать и уменьшить. Этот градиент AllReduce происходит после вычисления градиента на каждом устройстве и до того, как оптимизатор обновит веса модели.

Каждый графический процессор сначала объединяет градиенты между слоями модели, передает их между графическими процессорами с помощью tf.distribute.CrossDeviceOps (по умолчанию используется tf.distribute.NcclAllReduce ), а затем возвращает градиенты после сокращения для каждого слоя.

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

Время до AllReduce должно быть примерно таким же, как:

(number of parameters * 4bytes)/ (communication bandwidth)

Этот расчет полезен в качестве быстрой проверки, чтобы понять, соответствует ли производительность при выполнении распределенного задания обучения ожидаемой или вам нужно выполнить дальнейшую отладку производительности. Вы можете получить количество параметров в вашей модели из Model.summary .

Обратите внимание, что размер каждого параметра модели составляет 4 байта, поскольку TensorFlow использует fp32 (float32) для передачи градиентов. Даже если у вас включен fp16 , NCCL AllReduce использует параметры fp32 .

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

2. Конфликт потоков хоста GPU

При работе с несколькими графическими процессорами задача ЦП заключается в том, чтобы обеспечить занятость всех устройств за счет эффективного запуска ядер графических процессоров на устройствах.

Однако, когда есть много независимых операций, которые ЦП может запланировать на одном графическом процессоре, ЦП может решить использовать множество своих хост-потоков, чтобы занять один графический процессор, а затем запускать ядра на другом графическом процессоре в недетерминированном порядке. . Это может привести к перекосу или отрицательному масштабированию, что отрицательно скажется на производительности.

Средство просмотра трассировки ниже показывает накладные расходы, когда ЦП колеблется, ядро ​​графического процессора запускается неэффективно, поскольку GPU1 простаивает, а затем запускает операции после GPU2 .

image

Представление трассировки для хоста показывает, что хост запускает ядра на GPU2 прежде чем запускать их на GPU1 (обратите внимание, что приведенные ниже tf_Compute* не указывают на потоки ЦП).

image

Если вы столкнулись с таким ошеломлением ядер графического процессора в представлении трассировки вашей программы, рекомендуется выполнить следующие действия:

  • Установите для переменной среды TF_GPU_THREAD_MODE значение gpu_private . Эта переменная среды сообщает хосту, что потоки для графического процессора должны оставаться закрытыми.
  • По умолчанию TF_GPU_THREAD_MODE=gpu_private устанавливает количество потоков равным 2, чего в большинстве случаев достаточно. Однако это число можно изменить, задав для переменной среды TF_GPU_THREAD_COUNT желаемое количество потоков.