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

Обзор

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

Если вы новичок в профайлере:

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

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

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

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

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

  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. Оптимизируйте производительность одного графического процессора.

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

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

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

TensorFlow Profiler Overview Page

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

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

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

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

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

image

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

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

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

image

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

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

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

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

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

1. Анализ промежутков между этапами

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

image

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

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

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

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

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

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

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

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

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

image

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

image

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

image

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

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

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

image

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

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

Обратите внимание, что в некоторых случаях, даже если вы указываете операцию для размещения на конкретном устройстве, ее реализация может переопределить это условие (пример: tf.unique ). Даже при обучении с одним графическим процессором указание стратегии распределения, например 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®, чтобы использовать тензорные ядра и добиться общего ускорения до трех раз по сравнению с использованием только точности 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_float16 для автоматического включения масштабирования потерь в оптимизаторе Keras.

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

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. Проверьте время шага, чтобы убедиться, что каждая реплика выполняет одинаковую работу. Например, может случиться так, что один графический процессор (обычно 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. Конфликт потоков хоста графического процессора

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

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

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

image

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

image

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

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