Извлечение максимальной производительности из параллельных архитектур

Введение

Графические процессоры (GPU) обеспечивают огромную вычислительную мощность благодаря массивно-параллельной архитектуре. Но сопоставление приложений для эффективного использования всех этих FLOPS нетривиально. В этом подробном практическом руководстве мы подробно рассмотрим проверенные методы оптимизации кода для обеспечения высокой производительности на графических процессорах на основе извлеченных уроков из более чем десятилетнего опыта исследований и реальной практики.

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

Оптимизация доступа к памяти

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

Использование встроенной памяти

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

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

Константы — кеш-память только для чтения с малой задержкой, совместно используемая всеми потоками. Храните здесь неизменяемые данные, такие как строки запроса, чтобы избежать перезагрузки из глобальной памяти при каждом использовании. Обычно наблюдается двукратное-пятикратное ускорение доступа к глобальной памяти.

Регистры — чрезвычайно быстрое локальное хранилище для каждого потока. Разверните горячие циклы и разделите код (плитку), чтобы максимизировать повторное использование регистров. Но чрезмерное использование снижает параллелизм блоков потоков, ограничивая контекстное пространство. Сбалансируйте локальность регистров и параллелизм.

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

Оптимизация доступа к внешней памяти DRAM

Объединенный доступ. Консолидация транзакций памяти из множества потоков в выровненные транзакции строк кэша. Следуйте рекомендациям по архитектуре, чтобы включить. Обеспечивает ускорение в 1,8-4 раза за счет сокращения транзакций.

Макеты данных — оптимизируйте структуры данных, чтобы они соответствовали шаблонам доступа. Примеры: массив структур в структуру массивов, заполнение, транспонирование матриц.

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

Слияние ядер. Объединение ядер, работающих с одними и теми же данными, для минимизации передачи через границы ядра (запуски). Также снижает затраты на запуск.

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

Сжатие. Сжатие данных по всему конвейеру от передачи хоста в память ядра. Используйте вычислительные ресурсы для распаковки.

Пересчитать — повторять дешевые вычисления вместо сохранения/перезагрузки результатов. Компромисс памяти для вычислений.

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

Укрощение неправильных кодов на графических процессорах

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

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

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

Разреженные форматы: оптимизируйте форматы для повышения пропускной способности памяти за счет объединения, сжатия, уменьшения расхождений и т. д. Тесты показывают ускорение в 1,1-40 раз по сравнению с обычными форматами.

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

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

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

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

Балансировка ресурсов графического процессора

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

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

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

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

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

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

Автоматическая настройка – автоматизируйте исследование областей оптимизации, таких как занятость, детализация, макеты данных и т. д.

Никаких фиксированных формул не существует. Требуется профилирование в сочетании с эмпирической настройкой.

Оптимизация взаимодействия хоста и устройства

В стандартной модели узел ЦП управляет устройством ГП. Полезные приемы:

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

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

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

Единая память. Упрощение координации за счет единой виртуальной адресации на хосте и устройстве с подкачкой по запросу.

Коммуникационные API — такие библиотеки, как rCUDA и PyTorch, упрощают координацию устройств.

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

Заключение

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

Использованная литература :

  1. https://dl.acm.org/doi/pdf/10.1145/3570638