Харченко Никита Олегович
CV
RU
UA
EN

Автор: Michael Wolfe, PGI Compiler Engineer

Автор перевода: Харченко Н.О.

Оригинальное название статьи: Understanding the CUDA Data Parallel Threading Model. A Primer

Источник: http://www.pgroup.com/lit/articles/insider/v2n1a5.htm


Разбираясь в параллельной поточной модели технологии CUDA. Пособие для начинающих.


Главной целью программирования с помощью графических процессоров (GPU) является сравнительно новое явление. Графические процессоры изначально являлись аппаратными структурами, оптимизированными для небольшого круга графических операций. Поскольку спрос на большую гибкость возростал, GPU стали более программируемыми. К ранним подходам вычислений на GPU относятся монолитные вычисления в условиях графической системы с помощью выделения памяти для буферов (массивов) и написания шейдеров (функций ядра). Несколько исследовательских проектов обратили внимание на создание языков, уменьшающих сложность этих задач. В конце 2006 года корпорация nVidia представила архитектуру CUDA и сопутствующий инструментарий с целью упрощения параллельной обработки данных с помощью графических процессоров. Неудивительно, что особенности параллельной обработки данных учтены в nVidia GPU продуктах. Далее будет описана модель параллельной обработки данных, поддерживаемой архитектурой CUDA и графическими процессорами от корпорации nVidia.

По какой же причине пользователи PGI должны интересоваться и понимать поточную модель CUDA? Очевидно, что пользователи CUDA PGI Fortran должны достаточно хорошо знать эту технологию для того, чтобы настраивать их ядра. Программисты, используя программную модель PGI-ускорителя, базирующуюся на директивах, также сочтут ее поучительной для того, чтобы понимать и использовать обратную связь компилятора (-Minfo сообщения), говорящую о том, какие циклы были запущены в параллельном или векторном режиме на GPU. Также важным является знание того, как настраивать производительность ресурсов, используя выражения, влияющие на изменение циклов.

Итак, давайте начнём с обзора аппаратного обеспечения современных графических процессоров nVidia Tesla и Fermi.



Блок-схема nVidia Tesla



Блок-схема nVidia Fermi


Аппаратное обеспечение графических процессоров (GPU)

В современных высокопроизводительных системах графический процессор связан с HOST-компьютером с помощью высокоскоростной шины ввода/вывода. Обычно в качестве шины выступает PCI-Express. В настоящее время доступны конфигурации имеющие несколько Гбайт собственной памяти, присутствующей в каждом из представителей современных GPU. Данные обычно передаются между GPU и HOST-памятью посредством DMA, который может работать одновременно с вычислительными элементами как HOST-компьютера, так и GPU, хотя существуют определенные ограничения на прямой доступ к памяти HOST-компьютера со стороны графического процессора. Так как GPU разработаны для поточных производительных вычислений, они не зависят от глубокой иерархии кэш-памяти. В памяти устройство поддерживается очень высокая скорость передачи с использованием широкой магистрали данных. На базе nVidia GPU ее ширина составляет 512 бит и позволяет осуществлять выборку шестнадцати последовательных 32-битных слов за один цикл. Это означает, что отсутствует существенное ухудшение пропускной способности при пошаговом доступе.

GPU от компании nVidia имеют многочисленные мультипроцессоры, каждый из которых выполняет действия параллельно с другими мультипроцессорами. В Tesla каждый мультипроцессор обладает группой из 8 потоковых процессоров, в Fermi мультипроцессор обладает двумя группами по 16 процессоров в каждой. Я буду использовать более общий термин "ядро" для обозначения потокового процессора. Новейшие nVidia Tesla ускорители обладают 30-ю мультипроцессорами, причем общее количество ядер равняется 240. Последние nVidia Fermi ускорители обладают 16-ю мультипроцессорами, которые объединяют 512 ядер. Каждое ядро может выполнять последовательный поток, но сами ядра действуют по принципу, названному в nVidia как SIMT (Single Instruction, Multiple Thread - одна команда, много потоков), причем все ядра в одной группе выполняют одни и те же инструкции в каждый момент времени, что является очень схожим с SIMD-процессорами. Условные обработчики в SIMT работают несколько иначе, чем это устроено в SIMD-процессорах, хотя эффект получается одинаковым: во время условных операций часть ядер отключается.

Код фактически выполняется в группах по 32 потока, которые представителями корпорации nVidia называются warp-ами. При использовании Tesla 8 ядер в группе выполняют четырёхкратное выполнение одной команды для всего warp-а (32 потока) за четыре временных цикла. Каждое ядро Tesla обладает блоком для целочисленных вычислений и вычислений одинарной точности. Специальный распределенный модуль в каждом мультипроцессоре выполняет трансцендентные вычисления и вычисления с двойной точностью, затрачивая ресурсы 1/8 вычислительной пропускной способности. Мультипроцессоры nVidia Fermi мобилизуют каждую группу из 16 ядер для выполнения одной команды для каждого из двух warp-ов за два временных цикла для целочисленных вычислений и вычислений одинарной точности. Для команд арифметики двойной точности мультипроцессоры Fermi комбинируют две группы ядер для того, чтобы они представляли собой один 16-ядерный мультипроцессор для вычислений двойной точности. Это означает, что пиковая пропускная способность вычислений двойной точности равна 1/2 пропускной способности вычислений одиночной точности.

Существует также небольшой программно управляемый кэш данных, привязанный к каждому мультипроцессору, распределенный между ядрами. В nVidia этот кэш называют распределенной памятью. Это индексируемая память, обладающая низким уровнем задержки, высокой пропускной способностью, которая работает главным образом на регистровых скоростях. В Tesla объем разделяемой памяти равен 16 Кбайтам. В Fermi объем разделяемой памяти равен 64 Кбайтам, но может быть настроен в качестве 48-ми Кбайтного программно управляемого кэша данных совместно с 16-Кбайтным аппаратным кэшем данных или наоборот (16 Кбайт SW-кэша и 48 Кбайт HW-кэша).

Когда потоки запрашивают операцию памяти устройства, эта инструкция может выполняться в течение достаточно долгого временного промежутка, возможно даже сотен тактов, в связи с долговременным ожиданием памяти. Архитектуры главного потока (Mainstream-архитектуры) потребовали бы добавления иерархии кэшей памяти для уменьшения задержки, и Fermi действительно содержит некоторое количество аппаратных кэшей, но большинство GPU созданы для поточных и высокопроизводительных вычислений, во время которых использование кэшей памяти является неэффективным подходом. Вместо этого графические процессоры являются толерантными ко временным задержкам и преодолевают их с помощью высокой степени мультипоточности. Tesla позволяет использовать 32 активных warp-а на каждом мультипроцессоре, а Fermi поддерживает до 48 warp-ов. Когда один из warp-ов задерживает выполнение операции памяти, мультипроцессор выбирает другой готовый warp и переключается на него. В этом случае ядра могут быть продуктивными настолько долго, насколько достаточен уровень параллелизма для пребывания этих ядер в несвободном состоянии.


Программирование

GPU запрограммированы как последовательность ядер. Как правило, каждое ядро заканчивает обработку до того, как другое ядро начинает работу. Между ядрами существует неявная барьерная синхронизация. Fermi обладает возможностью поддержки множества независимых ядер, работающих одновременно, но большинство ядер достаточно велики, чтобы заполнить всю машину. Как уже упоминалось, мультипроцессоры работают параллельно и асинхронно. Однако, графические процессоры не поддерживают полностью когерентные модели памяти, которые позволяют мультипроцессорам синхронизироваться друг с другом. Поэтому классические техники параллельного программирования не могут быть применены. Потоки не могут порождать несколько потоков. Также один мультипроцессор не может посылать результаты в потоки другого мультипроцессора. Нет возможности использования средства критического разделения между всеми потоками в рамках всей системы. Попытка использования программных моделей PThreads и OpenMP не принесут ничего, кроме боли, разочарования и неудачи.

CUDA дает возможность использования параллельной программной модели, поддерживаемой в графических процессорах корпорации nVidia. В рамках этой модели программа HOST-компьютера запускает последовательность ядер. Ядра организованы в качестве иерархии потоков. Потоки в свою очередь группируются в блоки, а блоки группируются в сетку. Каждый поток обладает уникальным локальным индексом в своем блоке, а каждый блок имеет уникальный индекс в сетке (grid). Например, ядра могут использовать эти индексы для вычисления индексов массива.

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

Существует жесткое ограничение на размер блока потоков: 512 потоков или 16 warp-ов для Tesla, 1024 потока или 32 warp-а для Fermi. Блоки потоков всегда формируются в warp-ы, поэтому нет смысла пытаться создать блок потоков, обладающий размерами, не кратными 32-м потокам. Все блоки потоков, которые формируют сетку, будут обладать одинаковыми размерами и формой. Мультипроцессор для Tesla может иметь 1024 одновременно используюемых активных потока или 32 warp-а. Так как возможны ситуации использования 2-х потоковых блоков из 16 warp-ов, или 3-х блоков из 10 warp-ов, 4-х блоков из 8-ми warp-ов и т.д. до 8-ми блоков из 4-х warp-ов, существует еще одно жесткое ограничение на использование 8-ми потоковых блоков, одновременно активных на одном мультипроцессоре. Как уже упоминалось ранее, Fermi может одновременно использовать 48 активных warp-ов, количество потоков в которых достигает 1536-ти в 8-ми поточных блоках.

Настройка производительности на GPU требует оптимизации всех следующих архитектурных особенностей:

1. Нахождение и выявление достаточного уровня параллелизма для заполнения всех мультипроцессоров системы.

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

3. Оптимизация доступа к памяти устройства для непрерывных данных, что существенно оптимизирует доступ к памяти в пункте 1.

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

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

Если Вы являетесь программистом программной модели PGI акселератора, эта статья поможет Вам понять сообщения, приходящие по обратной связи от компилятора, а также эффективно использовать директивы, изменяющие поведение циклов. Директивы PGI акселератора разработаны для того, чтобы позволить Вам писать краткие, эффективные и портативные X64+GPU программы. Однако, написание эффективных программ по-прежнему требует от Вас понимания целевой архитектуры, а также процесса отображения Вашей программы на целевую задачу. Мы надеемся, что эта статья явилась еще одним шагом на пути такого понимания.