ОСНОВЫ ТЕХНОЛОГИИ CUDA И OpenCL

advertisement
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ
Федеральное агентство по образованию
Государственное образовательное учреждение высшего
профессионального образования
Московский физико-технический институт (государственный
университет)
Казённов А.М.
ОСНОВЫ ТЕХНОЛОГИИ CUDA И OpenCL
Рекомендовано
Учебно-методическим объединением
высших учебных заведений Российской Федерации
по образованию в области прикладных математики и физики
в качестве учебного пособия
МОСКВА 2013
Содержание
1
Введение
3
2
Современные многоядерные системы
3
3
2.1
Архитектура процессора Intel Nehalem
. . . . . . . . . . . . . . . . . . . . . .
5
2.2
Архитектура процессора AMD Istanbul . . . . . . . . . . . . . . . . . . . . . .
6
2.3
Архитектура процессора IBM Cell . . . . . . . . . . . . . . . . . . . . . . . . .
7
Краткий обзор основных схем построения кластеров
8
3.1
Схема сети «звезда» . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
3.2
Схема сети «кольцо»
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
3.3
Схема сети «3D тор»
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
4
Основные термины курса
12
5
Архитектура графических адаптеров Nvidia
13
5.1
Архитектура чипа G80
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
5.2
Архитектура чипа GT200 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
5.3
Архитектура чипа Fermi
17
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
Программная модель CUDA и OpenCL
19
7
Модель платформы OpenCL
20
7.1
Основные функции инициализации OpenCL . . . . . . . . . . . . . . . . . . .
21
7.2
Получение информации о платформах OpenCL . . . . . . . . . . . . . . . . .
21
7.3
Получение информации об устройствах OpenCL
. . . . . . . . . . . . . . . .
23
7.4
Контекст OpenCL
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
7.5
Очереди исполнения OpenCL . . . . . . . . . . . . . . . . . . . . . . . . . . . .
29
8
Основные функции инициализации CUDA Runtime API
30
9
Пример кода инициализации для OpenCL
32
10 Парадигма параллельных вычислений в CUDA
35
11 Компиляция программ в CUDA и OpenCL
37
12 Новые типы в CUDA
38
1
13 Новые типы в OpenCL
38
14 Спецификаторы для функций в CUDA
39
15 Спецификаторы для переменных в CUDA
40
16 Встроенные переменные в CUDA
41
17 Директива запуска ядра в CUDA
41
18 Директива запуска ядра в OpenCL
42
19 Типы памяти
45
19.1 Типы памяти в CUDA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
45
19.2 Типы памяти в OpenCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
19.3 Использование глобальной памяти в CUDA
. . . . . . . . . . . . . . . . . . .
47
. . . . . . . . . . . . . . . . . .
49
19.5 Вычисление числа Пи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
52
19.4 Использование глобальной памяти в OpenCL
19.6 Использование объединения при работе с глобальной памятью на графических картах NVIDIA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
55
19.7 Использование разделяемой памяти . . . . . . . . . . . . . . . . . . . . . . . .
57
19.8 Задача перемножения матриц. Глобальная память. . . . . . . . . . . . . . . .
60
19.9 Задача перемножения матриц. Разделяемая память.
. . . . . . . . . . . . . .
63
. . . . . . . . . . . . . . . . . . . . . . .
65
. . . . . . . . . . . . . . . . . . . . . . . .
65
19.10Использование константной памяти.
19.11Использование текстурной памяти.
2
1
Введение
Данное методическое пособие предназначено для людей, желающих производить высокопроизводительные вычисления с использованием, в первую очередь, графических ускорителей. В последнее время данное направление развития вычислительного программирование приобрело большую известность, что связанно с выпуском кампанией Nvidia в 2006
году технологии CUDA, а затем в 2008 году группой Khronos Group стандарта OpenCL,
предназначенных для быстрого написания кода на расширенном языке C для выполнения
на графических картах компании Nvidia (CUDA) и для вообще всех более-менее распространенных вычислительных устройств (OpenCL). В основу данного методического пособия легли материалы курсов по CUDA и OpenCL, читающихся в МФТИ с 2010 года. В
пособии содержаться теоретические материалы по основам технологии CUDA и OpenCL, а
так же практические примеры и задачи, позволяющие быстро освоить программирование
на базовом уровне, позволяющее, однако, получать весьма хорошее ускорение при использовании графических ускорителей. Сразу стоит отметить, что в данном пособии будут
рассмотрены отнюдь не самые новые версии предложенных технологий, так как развитие
технологий идет не в сторону повышения производительности итогового кода, а в сторону упрощения написания, что наоборот влечет за собой понижение производительности.
Поэтому будут рассмотрены CUDA 2.3 и OpenCL 1.1. Основой курса является технология
CUDA, стандарт OpenCl будет всегда упоминаться именно в сравнении с CUDA. Такое
изложение позволяет лучше изучить сходства и различия между двумя стандартами.
2
Современные многоядерные системы
Прежде чем приступить к изучению технологии CUDA и архитектуры ГА, требуется понять, чем же принципиально они отличаются от стандартных многоядерных систем, почему на некоторых задачах они показывают себя лучше чем классические многоядерные
системы, а на некоторых наоборот дают большое увеличение времени работы. Введем
общепринятую классификацию вычислительных систем по Флинну (Таблица 1).
3
Таблица 1: Классификация вычислительных систем по Флинну.
Single Data
Multiple Data
Single Instruction
SISD
MISD
Multiple Instruction
SIMD
MIMD
SISD – В один момент времени выполняется одна инструкция над одним набором
данных (Одноядерные процессоры) SIMD – В один момент времени выполняется одна
инструкция над несколькими наборами данных (Старые графические карты) MIMD – В
один момент времени выполняется несколько инструкций над несколькими наборами данных (Многоядерные процессоры, современные графические карты) MISD – В один момент
времени выполняется несколько инструкций над одним набором данных (Иногда к этому
классу относят конвейер)
Для прояснения с классификацией рассмотрим несколько самых распространенных
многоядерных вычислительных систем и определим к какому классу они относятся:
∙
Процессор Intel
∙
Процессор Amd
∙
Процессор Cell
4
2.1
Архитектура процессора Intel Nehalem
Рис. 1: Схематичное изображение строения процессора Intel Nehalem
Как видно из Рисунка 1, процессор Intel Nehalem содержит 4 независимых процессорных
ядра, каждое из которых обладает полной функциональностью центрального процессора. Одним из самых важных индикаторов независимости ядра является наличие Логики
выборки инструкций, что означает, что ядро самостоятельно осуществляет планировании
последовательности собственных инструкций. Такое ядро способно обрабатывать системные прерывания, работать с устройствами ввода-вывода, то есть абсолютно полноценно
поддерживать операционную систему. Каждое ядро содержит кэши первого уровня для
данных и инструкций, содержит логику выборки инструкций и кэш данных второго уровня. Все ядра абсолютно симметрично присоединены к КЭШу третьего уровня и к QPI
(QuickPath Interconnect) - системе присоединения процессоров к чипсету. Так же они присоединены к IMC (Integrated Memory Controller) - система связи с памятью, пришедшая
взамен северного моста. В некоторых версиях современных процессоров Intel так же присутствует встроенный графический контроллер. Так как каждое ядро является незави-
5
симым, при этом само по себе оно относится к классу SISD, то многоядерный процессор
Intel относится к архитектуре MIMD. Как и говорилось выше.
2.2
Архитектура процессора AMD Istanbul
Рис. 2: Схематичное изображение строения процессора AMD Istanbul
Из Рисунка 2 видно, что в архитектуре AMD Istanbul предусмотрено 6 независимых ядер,
каждое из которых на данном уровне абстракции неотличимо от процессорного ядра Intel.
Кроме того видно, что в AMD Istanbul, так же как и в Intel Nehalem есть кэш 3го уровня, общий для всех ядер, есть встроенный северный мост и система связи между ядрами
внутри процессора. Вообще, два представителя центральных процессоров семейства Intel
и AMD с вычислительной точки зрения очень похожи. Оба относятся MIMD классу, если
рассматривать их в целом, а каждое ядро относится к SISD классу. Кроме того, они обладают одними и теми же преимуществами и недостатками.
Преимущества:
6
∙
Самостоятельность ядер, а, следовательно, возможность выполнять независимые вычислительные задачи
∙
Высокая частота вычислительного ядра
∙
Большая точность (80 или 128 бит) внутренних вычислительных операций
Недостатки:
∙
Самостоятельность ядер, а, следовательно, перегруженность с вычислительной точки зрения ненужным функционалом
∙
Большая «стоимость» создания отдельного потока
∙
Необходимость выполнения когерентности КЭШей.
2.3
Архитектура процессора IBM Cell
Рис. 3: Схематичное изображение строения процессора IBM Cell
7
Архитектура процессора Cell уже сильно отличается от стандартной (см. Рисунок 3). Имеется восемь SPE (Synergistic Processing Elements), каждый из которых является мощным
вычислителем чисел с плавающей точкой. Процессор Cell уже относится к SIMD архитектуре, то есть он является векторной машиной, уже неспособной одновременно выполнять
несколько принципиально различных задач. Процессор Cell адаптирован под увеличение
пропускной способности памяти.
Преимущества:
∙
Высокая скорость расчета чисел с плавающей точкой
∙
Эффективная работа с памятью
Недостатки:
∙
3
Ядра не самостоятельны. Процессор является специализированным
Краткий обзор основных схем построения кластеров
Как уже говорилось выше, производительности отдельного процессора чаще всего не хватает для научных расчетов, поэтому строятся вычислительные кластера и суперкомпьютеры.
Кластер — группа компьютеров, объединённых высокоскоростными каналами связи и
представляющая с точки зрения пользователя единый аппаратный ресурс.
Суперкомпьютер – очень большой кластер. Чаще всего является законченным инженерным решением, не подлежащим доработке и расширению. Часто состоит из частей,
недоступных обычным пользователям.
Есть много способов объединять вычислительные узлы в кластере, рассмотрим самые
распространенные из них:
∙
Схему звезда
∙
Схему кольцо
∙
Схему 3D тор
Рассмотрим каждую из схем поподробнее:
8
3.1
Схема сети «звезда»
Рис. 4: Схема построения сети "Звезда"
Суть схемы построения сети «звезда» заключается в том, что явно существует головная
машина, которая часто имеет выход во внешний мир и выполняет роль DHCP сервера
для остальных машин кластера. Все машины кластера присоединены к одному роутеру,
образуя, таким образом, звезду, с центром в роутере (Рисунок 4). Преимущества:
∙
Все узлы равнозначны (одинаковое время доступа между любыми двумя из них)
∙
На узле требуется только одно сетевое устройство
Недостатки:
∙
Большая нагрузка на головную машину или центральный роутер
9
3.2
Схема сети «кольцо»
Рис. 5: Схема построения сети "Звезда"
Схема «кольцо» изначально не предполагает существование явно выделенного головного
узла. Конечно, какой-то из узлов должен выполнять функции точки входа в кластер и
осуществлять связь с внешним миром. Схема предполагает подключение каждого узла
к двум соседним, так что в итоге архитектура сети замыкается в кольцо (Рисунок 5).
Преимущества:
∙
Требует малое число кабелей
∙
Дает преимущества на линейных задачах малой связанности
Недостатки:
∙
Между некоторыми узлами получается очень большое время доступа
∙
Требует 2 сетевых адаптера на каждом узле
10
3.3
Схема сети «3D тор»
Рис. 6: Схема построения сети "3D тор"
Схема «3D тор» является трехмерным обобщением схемы «кольцо». Схема предполагает
подключение каждого узла к 4м соседним, так что в итоге архитектура сети замыкается
в тор (Рисунок 6). Преимущества:
∙
На 3х мерных задачах с малой связанностью дает огромные преимуществами
Недостатки:
∙
Между некоторыми узлами получается очень большое время доступа
∙
Требует 4 сетевых адаптера на каждом узле
На самом деле, кластера редко имеют одну сеть. Чаще всего делается 2 сети по схеме
звезда, одна из которых обладает не слишком большой пропускной способностью (1Гб/с
Ethernet) и выполняет функции управления. Вторая сеть, имеющая высокую пропускную
способность (10Гб/с Myrinet, или 40Гб/с Infiniband), осуществляет передачу данных.
11
Суперкомпьютеры чаще устроены ещё сложнее и в своей архитектуре имеют до 5 различных сетей. Общим недостатком и кластеров и суперкомпьютеров является падение
эффективности распараллеливания на них с ростом количества узлов из-за задержек
в сети и сложности синхронизации между узлами. Таким образом, возникает желание
максимально увеличить производительность каждого отдельного узла, что делается при
помощи добавление в узел какого-либо дополнительного вычислителя (процессоров Cell,
графических адаптеров). Так возникают гибридные машины и кластера.
Рис. 7: Принципиальная схема гибридного кластера
4
Основные термины курса
Прежде чем приступать к детальному рассмотрению архитектуры графических процессоров, требуется ввести основные термины курса, чтобы можно было легко ориентироваться
в том что где выполняется.
Хост (Host) – центральный процессор, который управляет выполнением программы.
Устройство (Device) – видеокарта, являющаяся сопроцессором к центральному процес-
12
сору (хосту).
Ядро (Kernel) – Параллельная часть алгоритма, выполняется на гриде.
Грид (Grid) – объединение блоков, которые выполняются на одном устройстве.
Блок (Block) – объединение потоков, которое выполняется целиком на одном SM. Имеет
свой уникальный идентификатор внутри грида.
Тред (Thread) – единица выполнения программы. Имеет свой уникальный идентификатор внутри блока.
Варп (Warp) – 32 последовательно идущих треда, выполняется физически одновременно.
5
Архитектура графических адаптеров Nvidia
Одним из наиболее распространенных видом гибридных машин является гибридная машина на основе графических адаптеров компании Nvidia. Для того чтобы эффективно
уметь программировать под такого рода машины необходимо детально понимать устройство графического адаптера с аппаратной точки зрения. Любая графическая карта может
быть схематически изображена следующим образом (Рисунок 8).
Рис. 8: Схематичное изображение графического адаптера
Все вычислительные ядра на графическом адаптере объединены в независимые блоки TPC (Texture process cluster) количество которых зависит как от версии чипа (G80 –
13
максимум 8, G200 – максимум 10), так и просто от конкретной видеокарты внутри чипа
(GeForce 220GT – 2, GeForce 275 – 10). Так же, как от видеокарты к видеокарте может
меняться количество TPC, так же может меняться и количество DRAM партиций и соответственно общий объем оперативной памяти. DRAM партиции имеют кэш второго уровня
и объединены между собой коммуникационной сетью, в которую, так же подключены все
TPC. Любая видеокарта подключается к CPU через мост, который может быть, как интегрирован в CPU (Intel Core i7), так и дискретным (Intel Core 2 Duo).
От чипа к чипу (от G80 до G200) менялись детали в TPC, а общая архитектура оставалась
одинаковой. В новом чипе Fermi произошли изменения и в общей архитектуре, поэтому о
нем речь пойдет отдельно.
5.1
Архитектура чипа G80
Рис. 9: Архитектура чипа G80
Основные составляющие TPC:
∙
TEX – логика работы с текстурами, содержит в себе участки конвейера, предназначен-
ные для обработки особой текстурной памяти о которой речь пойдет позже.
∙ SM – потоковый мультипроцессор, самостоятельный вычислительный модуль, именно на
нем осуществляется выполнение блока. В архитектуре чипа G80 в одном TPC находится
2 SM.
Основные составляющие SM:
14
∙
SP – потоковый процессор, непосредственно вычислительный модуль, способен совер-
шать арифметические операции с целочисленными операндами и с операндами с плавающей точкой (одинарная точность). Не является самостоятельной единицей, управляется
SM.
∙
SFU – модуль сложных математических функций. Проводит вычисления сложных ма-
тематических функций (exp, sqr, log). Использует вычислительные мощности SP.
∙ Регистровый файл – единый банк регистров, на каждом SM имеется 32Кб. Самый быстрый тип памяти на графическом адаптере.
∙
Разделяемая память – специальный тип памяти, предназначенный для совместного ис-
пользования данных тредов из одного блока. На каждом SM – 16Кб разделяемой памяти.
∙
Кэш констант – место кэширования особого типа памяти (константной). Об особенно-
стях применения речь пойдет позже.
∙ Кэш инструкций, блок выборки инструкций – управляющая система SM. Не играет роли
при программировании.
Итого, чип G80 имеет максимально 128 вычислительных модулей (SP) способных выполнять вычисления с целыми числами и числами с плавающей точкой с одинарной точностью. Такого функционала было недостаточно для многих научных задач, требовалась
двойная точность.
15
5.2
Архитектура чипа GT200
Рис. 10: Архитектура чипа GT200
В чипе GT200 происходят следующие изменения по сравнению с чипом G80:
∙
Число SM в TPC увеличено с двух до 3х.
∙
Максимальное количество SP увеличено до 240.
∙
Появился блок работы с числами с двойной точностью, который в качестве вычисли-
тельных мощностей использует одновременно все 8 SP. Таким образом, скорость расчета
double в 8 раз меньше чем скорость расчета float. Что по-прежнему является не очень
приемлемым для многих научных задач.
16
5.3
Архитектура чипа Fermi
Рис. 11: Архитектура чипа Fermi
Ключевыми архитектурными особенностями Fermi являются:
Третье поколение потокового мультипроцессора (SM):
- 32 ядра CUDA на SM, вчетверо больше, чем у GT200;
- восьмикратный прирост производительности в FP-операциях двойной точности в сравнении с предшественником;
- два блока Warp Scheduler на SM вместо одного;
- 64 КБ ОЗУ с конфигурируемым разделением на разделяемую память и L1-кэш.
Второе поколение набора инструкций параллельного выполнения потоков (Parallel Thread
Execution, PTX 2.0):
- унифицированное адресное пространство с полной поддержкой С++;
17
- оптимизация для OpenCL и DirectCompute;
- полная 32- и 64-битная точность в соответствии с IEEE 754-2008;
- инструкции доступа к памяти для поддержки перехода на 64-битную адресацию;
- улучшенная производительность предсказаний.
Улучшенная подсистема памяти:
- иерархия NVIDIA Parallel DataCache с конфигурируемым L1-кэшем и общим L2;
- поддержка ECC, впервые на GPU;
- существенно увеличенная производительность операций чтения и записи в память.
Движок NVIDIA GigaThread:
- десятикратное ускорение процедуры контекстного переключения;
- параллельное выполнение ядер;
- непоследовательное исполнение потоков.
Подведем итоги по характеристикам перечисленных чипов (Таблица 2)
Таблица 2: Сравнение характеристик различных чипов Nvidia.
Архитектура
G80
GT200
Fermi
Год вывода на рынок
2006
2008
2009
Число транзисторов, млн.
681
1400
3000
Количество CUDA-ядер
128
24
512
16
16
48 или 16 (конфигурируется)
0
0
16 или 48 (конфигурируется)
0
0
768
Отсутствует
Отсутствует
Имеется
Объем разделяемой памяти на SM, Кб
Объем кэш-памяти первого уровня в расчете на SM,
Кб
Объем кэш-памяти второго уровня, Кб
Функция ECC
Кроме архитектурных отличий между чипами есть ещё и функциональные, кроме того,
внутри одного чипа графические адаптеры более старших версий могут иметь функционал, отличный от младших версий. Возможности графических адаптеров обозначаются
при помощи Compute Capability, старшая цифра которой обозначает версию архитектуры,
18
а младшая небольшим изменениям внутри архитектуры. На данный момент существуют
1.0, 1.1, 1.2, 1.3, 2.0 Compute Capability. В частности, Compute Capability влияет на правила работы с глобальной памятью.
Таблица 3: Примеры Compute Capability
GPU
Compute Capability
GeForce 8800GTX
1.0
GeForce 9800GTX
1.1
GeForce 210
1.2
GeForce 275GTX
1.3
Tesla C2050
2.0
На этом разговор об аппаратной части технологии CUDA можно считать законченным.
Для OpenCL ситуация немного сложнее, так как устройством, на котором выполняется
параллельная часть алгоритма может являтся как центральный процессор, так и графический ускоритель ATI или Nvidia. Таким образом, возможности устройства определяется
этим устройством, процессор в OpenCL умеет делать то, что умеет делать процессор в
обычной ситуации, аналогично и с графическим ускорителем. То есть, если графический
ускоритель имеет Compute Capability 1.0, то нет никакой возможности посчитать на нем
с аппаратной двойной точностью (как на CUDA, так и на OpenCL).
6
Программная модель CUDA и OpenCL
Прежде чем начать рассмотрение программных моделей стоит отметить что для технологии CUDA существует два API:
∙
CUDA RunTime API (Программа и вычислительные ядра компилируются компилято-
ром, графический ускоритель получает уже готовый PTX код для себя)
∙ CUDA Driver API (Программа компилируется компилятором, ядра компилируются драйвером устройства)
Для OpenCl существует только один API – OpenCL Driver API (OpenCL).
Для OpenCl существует только один API – OpenCL Driver API (OpenCL). Данное методическое пособие не будет рассматривать CUDA Driver API, так как он является более
сложным, но при этом в большинстве научных задач в нем нет необходимости. Однако
19
для сравнения CUDA и OpenCL будет приведен процесс инициализации для CUDA Driver
API и OpenCL:
CUDA Driver API:
1 Инициализация драйвера
2 Выбор устройства (GPU)
3 Создание контекста
OpenCL API:
1 Выбор платформы
2 Создание контекста, привязанного к платформе
3 Выбор устройства из контекста
Как видно, идеологически эти два процесса очень похожи, они различаются лишь в
терминологии первого этапа. Дело в том, что в случае CUDA платформа может быть только одна – NVIDIA, в случае OpenCL выбор платформу означает инициализацию драйвера
для соответствующего устройства.
7
Модель платформы OpenCL
Модель платформы (platform model) дает высокоуровневое описание гетерогенной системы. Центральным элементом данной модели выступает понятие хоста (host) - первичного устройства, которое управляет OpenCL-вычислениями и осуществляет все взаимодействия с пользователем. Хост всегда представлен в единственном экземпляре, в то время как OpenCL-устройства (devices), на которых выполняются OpenCL-инструкции могут быть представлены во множественном числе. OpenCL-устройством может быть CPU,
GPU, DSP или любой другой процессор в системе, поддерживающийся установленными в
системе OpenCL-драйверами. OpenCL-устройства логически делятся моделью на вычислительные модули (compute units), которые в свою очередь делятся на обрабатывающие
элементы (processing elements). Вычисления на OpenCL-устройствах в действительности
происходят на обрабатывающих элементах. На рис. 12 схематически изображена OpenCLплатформа из 3-х устройств.
20
Рис. 12: Платформа OpenCL
7.1
Основные функции инициализации OpenCL
Приведем синтаксис основных функций для инициализации OpenCL:
7.2
Получение информации о платформах OpenCL
cl_platform_id – дескриптор платформы
clGetPlatformIDs
cl_int clGetPlatformIDs (cl_uint num_entries, cl_platform_id platforms, cl_uint num_platforms)
Функция для получения информации об идентификаторах платформ установленх в системе. Параметры
num_entries – размер буфера platforms.
platforms – буфер платформ. Может быть NULL.
num_platforms по этому адресу возвращается реальный размер списка платформ. Может
быть NULL.
21
Возвращаемое значение
Возвращает CL_SUCCESS в случае успеха.
Если num_entries равно нулю, а platforms не равно нулю или если и num_entries и platforms
равно нулю возвращает CL_INVALID_VALUE
При ошибке выделения ресурсов на хосте возвращает CL_OUT_OF_HOST_MEMORY.
clGetPlatformInfo
1 cl_int
clGetPlatformInfo
size_t
( cl_platform_id
param_value_size ,
void
platform ,
* param_value ,
cl_platform_info
param_name ,
* param_value_size_ret )
size_t
Функция для получения информации об одной платформе. За один вызов возвращает
значения одного свойства платформы.
Параметры
platform дескриптор платформы, полученный функцией clGetPlatformIDs.
param_name – перечислимая константа, задающая параметр, который требуется получить
(см. таблицу).
Таблица 4: Параметры clGetPlatformInfo
cl_platform_info
Тип
CL_PLATFORM_PROFILE
char[]
Описание
Возвращает название профайлера поддерживаемого реализацией.
CL_PLATFORM_VERSION
char[]
Возвращает
та
OpenCL
данной
дующем
версию
стандар-
поддерживаемую
платформой
формате:
в
сле-
OpenCL
<major.minor> <платформа>
CL_PLATFORM_NAME
char[]
Название платформы.
CL_PLATFORM_VENDOR
char[]
Производитель платформы.
CL_PLATFORM_EXTENSIONS
char[]
Поддерживаемые
через пробел.
22
расширения,
param_value_size – размер буфера param_value.
param_value – указатель на буфер под значение свойства платформы. Может быть NULL.
param_value_size_ret – по этому адресу возвращается реальный размер значения свойства платформы в байтах. Может быть NULL.
Возвращаемое значение
Возвращает CL_SUCCESS в случае успеха.
Возвращает CL_INVALID_PLATFORM если platform не является корректным дескриптором платформы.
Если param_name не является значением из таблицы или фактический размер значения свойства платформы больше чем param_value_size и param_value не равно нулю
возвращает CL_INVALID_VALUE При ошибке выделения ресурсов на хосте возвращает
CL_OUT_OF_HOST_MEMORY.
7.3
Получение информации об устройствах OpenCL
cl_device_id – идентификатор устройства
clGetDeviceIDs
1 cl_int
clGetDeviceIDs
cl_uint
( cl_platform_id
num_entries ,
cl_device_id
platform ,
* devices
,
cl_device_type
cl_uint
device_type ,
* num_devices )
Функция для получения идентификаторов вычислительных устройств доступных на платформе.
Параметры
platform дескриптор платформы, полученный функцией clGetPlatformIDs.
device_type – перечислимая константа, задающая тип устройств, идентификаторы которых будут возвращены (см. таблицу)
23
Таблица 5: Параметры clGetDeviceIDs
cl_device_type
Описание
CL_DEVICE_TYPE_CPU
Одно- или многоядерный центральный процессор общего назначения.
CL_DEVICE_TYPE_GPU
Графические
процессоры
(ви-
деокарты).
CL_DEVICE_TYPE_ACCELERATOR
Акселераторы (например IBM
CELL Blade).
CL_DEVICE_TYPE_DEFAULT
Вычислительное
установленное
в
устройство,
системе
как
устройство по умолчанию.
CL_DEVICE_TYPE_ALL
Все устройства.
num_entries – размер буфера platforms.
devices – буфер идентификаторов вычислительных устройств. Может быть NULL.
num_devices по этому адресу возвращается реальный размер списка устройств. Может
быть NULL.
Возвращаемое значение
Возвращает CL_SUCCESS в случае успеха.
Если num_entries равно нулю, а devices не равно нулю или если и num_entries и devices
равно нулю возвращает CL_INVALID_VALUE
Возвращает CL_INVALID_PLATFORM если platform не является корректным дескриптором платформы.
Если ни одного устройства заданного типа не найдено, возвращает CL_DEVICE_NOT_FOUND.
При ошибке выделения ресурсов на хосте возвращает CL_OUT_OF_HOST_MEMORY.
При ошибке выделения ресурсов на устройстве возвращает CL_OUT_OF_RESOURCES.
cl_device_info
Тип зна-
Описание
чения
CL_DEVICE_ADDRESS_BITS
cl_uint
Разрядность устройства (32 или 64).
CL_DEVICE_AVAILABLE
cl_bool
Доступность устройства.
CL_DEVICE_EXTENSIONS
char[]
Поддерживаемые расширения, через
пробел.
CL_DEVICE_GLOBAL_MEM_SIZE
cl_ulong
Размер глобальной памяти устройства.
24
CL_DEVICE_HOST_UNIFIED_MEMORY
cl_bool
Является ли память устройства памятью хоста.
CL_DEVICE_IMAGE_SUPPORT
cl_bool
Поддержка текстурной памяти.
CL_DEVICE_IMAGE2D_MAX_HEIGHT
size_t
Максимальная
высота
двумерных
ширина
двумерных
текстур.
CL_DEVICE_IMAGE2D_MAX_WIDTH
size_t
Максимальная
текстур.
CL_DEVICE_IMAGE3D_MAX_DEPTH
size_t
Максимальная глубина трехмерных
текстур.
CL_DEVICE_IMAGE3D_MAX_HEIGHT
size_t
Максимальная
высота
трехмерных
текстур.
CL_DEVICE_IMAGE3D_MAX_WIDTH
size_t
Максимальная ширина трехмерных
текстур.
CL_DEVICE_LOCAL_MEM_SIZE
cl_ulong
Максимальный размер локальной памяти.
CL_DEVICE_LOCAL_MEM_TYPE
enum
Тип локальной памяти. Может быть
CL_LOCAL
(более
быстрая)
или
CL_GLOBAL (более медленная)
CL_DEVICE_MAX_CLOCK_FREQUENCY
cl_uint
Максимальная тактовая частота.
CL_DEVICE_MAX_COMPUTE_UNITS
cl_uint
Максимальное
число
вычислитель-
ных ядер.
CL_DEVICE_MAX_WORK_GROUP_SIZE
size_t
CL_DEVICE_MAX_WORK_ITEM_DIMENSIONScl_uint
Максимальный размер work-group.
Максимальное
число
измерений
NDRange.
CL_DEVICE_MAX_WORK_ITEM_SIZES
size_t[]
Максимальное
число
work-item
по
каждому измерению NDRange.
CL_DEVICE_NAME
char[]
Название устройства.
CL_DEVICE_OPENCL_C_VERSION
char[]
Версия OpenCL C, поддерживаемая
устройством.
CL_DEVICE_TYPE
enum
Тип
устройства:
CL_DEVICE_TYPE_CPU,
CL_DEVICE_TYPE_GPU,
CL_DEVICE_TYPE_ACCELERATOR,
или CL_DEVICE_TYPE_DEFAULT
CL_DEVICE_VENDOR
char[]
Производитель устройства.
CL_DEVICE_VERSION
char[]
Версия устройства.
CL_DRIVER_VERSION
char[]
Версия драйвера устройства.
25
param_value_size – размер буфера param_value.
param_value – указатель на буфер под значение свойства устройства. Может быть NULL.
param_value_size_ret – по этому адресу возвращается реальный размер значения свойства устройства в байтах. Может быть NULL.
Возвращаемое значение
Возвращает CL_SUCCESS в случае успеха.
Возвращает CL_INVALID_DEVICE если device не является корректным дескриптором
устройства.
Если param_name не является значением из таблицы или фактический размер значения
свойства
устройства больше чем param_value_size и param_value не равно нулю возвращает CL_INVALID_VA
При ошибке выделения ресурсов на хосте возвращает CL_OUT_OF_HOST_MEMORY.
При ошибке выделения ресурсов на устройстве возвращает CL_OUT_OF_RESOURCES.
7.4
Контекст OpenCL
cl_context – дескриптор контекста.
clCreateContext
1 cl_context
clCreateContext
num_devices ,
errinfo ,
const
const
user_data ,
cl_context_properties
cl_device_id
void
cl_int
( const
* devices
* private_info
,
,
size_t
void
cb ,
* properties
,
(* pfn_notify ) ( const
void
* user_data )
,
cl_uint
char
void
*
*
* errcode_ret )
Функция создает контекст OpenCL и возвращает его дескриптор.
Параметры
properties – массив, где на нечетных местах стоят перечислимые константы-имена свойств
создаваемого контекста, а на четных – значения свойств, последний элемент NULL. Обязательно должно присутствовать свойство CL_CONTEXT_PLATFORM, со значением типа
cl_platform_id, задающее платформу.
num_devices – размер списка devices.
devices – список устройств, входящих в создаваемый контекст.
pfn_notify и user_data – указатель на функцию-обработчик ошибок создания контекста
26
и пользовательские данные для нее. Могут быть NULL.
errcode_ret – адрес, по которому будет записан код ошибки. Может быть NULL.
Возвращаемое значение
В случае успеха функция возвращает дескриптор вновь созданного контекста, а в errcode_ret
записывается CL_SUCCESS. В противном случае функция вернет NULL, а в errcode_ret
будет записан код ошибки. Наиболее распространенные коды ошибок:
∙
CL_INVALID_PLATFORM – значение свойства CL_CONTEXT_PLATFORM не
является корректным идентификатором платформы.
∙
CL_INVALID_PROPERTY – properties содержит некорректные имена или значения
свойств.
∙
CL_INVALID_DEVICE – devices содержит некорректные дескрипторы устройств.
∙
CL_DEVICE_NOT_AVAILABLE devices содержит дескрипторы недоступных устройств.
∙
CL_OUT_OF_HOST_MEMORY ошибка выделения ресурсов на хосте.
∙
CL_OUT_OF_RESOURCES ошибка выделения ресурсов на устройстве.
clCreateContextFromType
1 cl_context
clCreateContextFromType
cl_device_type
void
device_type ,
* private_info
,
size_t
( const
void
cb ,
cl_context_properties
(* pfn_notify ) ( const
void
* user_data )
,
void
char
* properties
* errinfo
* user_data
,
,
,
const
cl_int
*
errcode_ret )
Функция создает контекст OpenCL и возвращает его дескриптор. В созданный контекст
включаются все устройства заданного типа, доступные на платформе.
Параметры
properties – массив, где на нечетных местах стоят перечислимые константы-имена свойств
создаваемого контекста, а на четных – значения свойств, последний элемент NULL. Обязательно должно присутствовать свойство CL_CONTEXT_PLATFORM, со значением типа
cl_platform_id, задающее платформу.
device_type – тип вычислительных устройств: CL_DEVICE_TYPE_CPU, CL_DEVICE_TYPE_GP
CL_DEVICE_TYPE_ACCELERATOR, или CL_DEVICE_TYPE_DEFAULT.
27
pfn_notify и user_data – указатель на функцию-обработчик ошибок создания контекста
и пользовательские данные для нее. Могут быть NULL.
errcode_ret – адрес, по которому будет записан код ошибки. Может быть NULL.
Возвращаемое значение
В случае успеха функция возвращает дескриптор вновь созданного контекста, а в errcode_ret
записывается CL_SUCCESS. В противном случае функция вернет NULL, а в errcode_ret
будет записан код ошибки. Наиболее распространенные коды ошибок:
∙
CL_INVALID_PLATFORM – значение свойства CL_CONTEXT_PLATFORM не
является корректным идентификатором платформы.
∙
CL_INVALID_PROPERTY – properties содержит некорректные имена или значения
свойств.
∙
CL_INVALID_DEVICE_TYPE device_type не является корректным типом устройства.
∙
CL_OUT_OF_HOST_MEMORY ошибка выделения ресурсов на хосте.
∙
CL_OUT_OF_RESOURCES ошибка выделения ресурсов на устройстве.
clReleaseContext
1 cl_int
clReleaseContext
( cl_context
context )
Функция уничтожает контекст OpenCL и высвобождает занятые им ресурсы.
Параметры
context – дескриптор освобождаемого контекста.
Возвращаемое значение
Возвращает CL_SUCCESS в случае успеха.
Возвращает CL_INVALID_CONTEXT если context не является корректным дескриптором контекста.
При ошибке выделения ресурсов на хосте возвращает CL_OUT_OF_HOST_MEMORY.
При ошибке выделения ресурсов на устройстве возвращает CL_OUT_OF_RESOURCES.
Как видно, процесс инициализации в OpenCL, является достаточно сложным, но, к счастью, из программы в программу он практически не претерпевает изменений, что позволя-
28
ет написать готовую функцию инициализации и применять её во всех своих программах.
Ниже такая функция будет приведена.
7.5
Очереди исполнения OpenCL
cl_command_queue – дескриптор очереди исполнения. clCreateCommandQueue
1 cl_command_queue clCreateCommandQueue
cl_command_queue_properties
( cl_context
properties ,
cl_int
context ,
cl_device_id
device ,
* errcode_ret )
Функция создает очередь исполнения OpenCL и возвращает ее дескриптор.
Параметры
context – дескриптор контекста в котором создается очередь.
device – устройство, на которое очередь будет передавать команды.
properties – набор битовых флагов-свойств создаваемой очереди. Возможные флаги:
Таблица 7: Параметры clCreateCommandQueue
Флаг
Описание
CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE
Допускается
запуск
команд
в
произвольном порядке.
CL_QUEUE_PROFILING_ENABLE
Допускаются команды профилирования.
errcode_ret – адрес, по которому будет записан код ошибки. Может быть NULL.
Возвращаемое значение
В случае успеха функция возвращает дескриптор вновь созданной очереди исполнения, а
в errcode_ret записывается CL_SUCCESS. В противном случае функция вернет NULL, а
в errcode_ret будет записан код ошибки. Наиболее распространенные коды ошибок:
∙
CL_INVALID_CONTEXT – context не является корректным дескриптором контекста.
∙
CL_INVALID_DEVICE – device не является корректным дескриптором устройства.
29
∙
CL_INVALID_VALUE – properties содержит некорректные флаги.
∙
CL_INVALID_QUEUE_PROPERTIES - значение properties является корректным,
но не поддерживается устройством.
∙
CL_OUT_OF_HOST_MEMORY ошибка выделения ресурсов на хосте.
∙
CL_OUT_OF_RESOURCES ошибка выделения ресурсов на устройстве.
clReleaseCommandQueue
1 cl_int
clReleaseCommandQueue ( cl_command_queue command_queue )
Функция уничтожает очередь исполнения OpenCL и высвобождает занятые ей ресурсы.
Параметры
command_queue – дескриптор освобождаемой очереди исполнения.
Возвращаемое значение
Возвращает CL_SUCCESS в случае успеха.
Возвращает CL_INVALID_COMMAND_QUEUE если command_queue не является корректным дескриптором очереди исполнения.
При ошибке выделения ресурсов на хосте возвращает CL_OUT_OF_HOST_MEMORY.
При ошибке выделения ресурсов на устройстве возвращает CL_OUT_OF_RESOURCES.
8
Основные функции инициализации CUDA Runtime
API
Runtime API основано на driver API, работает в рамках контекста, созданного через driver
API. Если контекста нет, то он создается неявно перед первым вызовом функции из
runtime API. ВНИМАНИЕ! В одной программе нельзя смешивать Driver и Runtime API!
Ниже приведем основные функции для инициализации: cudaGetDeviceCount
Функция для определения количества доступных графических ускорителей
1 c u d a E r r o r \_t c u d a G e t D e v i c e C o u n t
( int
*
count )
30
Параметры count – количество доступных для расчета графических карт Nvidia. Возвращаемое значение Возвращает cudaSuccess в случае успеха
cudaGetDeviceProperties Функция для определения параметров выбранного устройства
1 cudaError_t
cudaGetDeviceProperties
( struct
cudaDeviceProp
*
pro p ,
int
device )
Параметры prop – Указатель на структуру cudaDeviceProp, в который запишутся параметры выбранного устройства.
1
struct
cudaDeviceProp
char
3
5
7
name [ 2 5 6 ] ;
size_t
totalGlobalMem ;
size_t
shar edMemPerBlock ;
int
regsPerBlock ;
int
warpSize ;
size_t
9
11
15
maxThreadsPerBlock ;
int
maxThreadsDim [ 3 ] ;
int
maxGridSize [ 3 ] ;
19
21
totalConstMem ;
int
major ;
int
minor ;
int
clockRate ;
size_t
17
memPitch ;
int
size_t
13
{
textureAlignment ;
int
deviceOverlap ;
int
multiProcessorCount ;
int
kernelExecTimeoutEnabled ;
int
integrated ;
int
canMapHostMemory ;
int
computeMode ;
}
Device – Номер выбранного устройства Возвращаемое значение Возвращает cudaSuccess в
случае успеха cudaSetDevice Функция для выбора устройства
cudaError_t
cudaSetDevice
( int
device )
Параметры device – Номер выбираемого устройства. Возвращаемое значение Возвращает
31
cudaSuccess в случае успеха cudaErrorInvalidDevice, cudaErrorSetOnActiveProcess в случае
ошибки
cudaGetDevice Функция возвращает номер выбранного устройства
1 cudaError_t
cudaGetDevice
* device )
( int
Параметры device – Номер выбранного устройства. Возвращаемое значение Возвращает
cudaSuccess в случае успеха
9
Пример кода инициализации для OpenCL
Приведем пример кода инициализации OpenCL
1 void
3
i n i t i a l i z e C L ( void )
cl_int
status = 0;
size_t
deviceListSize ;
cl_uint
5
{
numPlatforms ;
cl_platform_id
p l a t f o r m = NULL ;
status = clGetPlatformIDs (0 ,
7
i f ( status
!= CL_SUCCESS)
p r i n t f ( " Error :
9
Getting
NULL,
&n u m P l a t f o r m s ) ;
{
Platforms .
( c l G e t P l a t f o r m s I D s ) \n" ) ;
return ;
}
11
i f ( numPlatforms > 0 )
cl_platform_id
{
* platforms
= ( cl_platform_id
* ) m a l l o c ( numPlatforms *
sizeof (
cl_platform_id ) ) ;
13
s t a t u s = c l G e t P l a t f o r m I D s ( numPlatforms ,
i f ( status
15
!= CL_SUCCESS)
p r i n t f ( " Error :
platforms ,
NULL) ;
{
Getting
Platform
Ids .
( c l G e t P l a t f o r m s I D s ) \n" ) ;
return ;
17
}
platform = platforms [ 0 ]
19
delete
platforms ;
}
21
cl_context_properties
platform ,
0
cps [ 3 ]
= { CL_CONTEXT_PLATFORM,
( cl_context_properties )
};
cl_context_properties *
c p r o p s = (NULL == p l a t f o r m )
? NULL
:
cps ;
23 / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
//
Create
an OpenCL
context
25 / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
32
c o n t e x t = clCreateContextFromType ( cprops ,
27
CL_DEVICE_TYPE_ALL,
NULL,
29
NULL,
&s t a t u s ) ;
31
i f ( status
!= CL_SUCCESS)
p r i n t f ( " Error :
33
{
Creating
Context .
( clCreateContextFromType ) \n" ) ;
return ;
}
/*
35
First ,
get
the
size
of
device
list
*/
data
s t a t u s = clGetContextInfo ( context ,
37
CL_CONTEXT_DEVICES,
0,
39
NULL,
&d e v i c e L i s t S i z e ) ;
41
i f ( status
!= CL_SUCCESS)
p r i n t f ( " Error :
43
Getting
( device
list
{
Context
size ,
Info
\
c l G e t C o n t e x t I n f o ) \n" ) ;
return ;
45
}
// / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
47 / /
Detect
OpenCL
devices
// / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
49
unsigned
int
deviceCount = d e v i c e L i s t S i z e
i f ( d e v i c e C o u n t <= DEVICE_NUM)
51
p r i n t f ( " Error :
Cannot
u s e %i
/
s i z e o f ( cl_device_id ) ;
{
device
( have
o n l y %i
deviceCount ) ;
return ;
53
};
p r i n t f ( "%u
55
devices
d e t e c t e d \n" ,
devices = ( cl_device_id
deviceCount ) ;
*) malloc ( d e v i c e L i s t S i z e ) ;
i f ( d e v i c e s == 0 )
57
{
p r i n t f ( " Error :
59
No
devices
found . \ n" ) ;
return ;
}
61
/ * Now ,
get
the
device
list
data
*/
status = clGetContextInfo (
63
context ,
CL_CONTEXT_DEVICES,
33
d e v i c e s ) . \ n " , DEVICE_NUM,
65
deviceListSize ,
devices ,
67
NULL) ;
i f ( status
69
!= CL_SUCCESS)
p r i n t f ( " Error :
( device
71
Getting
list ,
{
Context
Info
\
c l G e t C o n t e x t I n f o ) \n" ) ;
return ;
}
73
size_t
char
75
ret ;
pbuf [ 2 5 6 ] ;
s t a t u s = c l G e t D e v i c e I n f o ( d e v i c e s [DEVICE_NUM] , CL_DEVICE_NAME,
256 ,
;
i f ( status
77
!= CL_SUCCESS)
p r i n t f ( " Error :
( device
79
Getting
list ,
{
Device
Info
\
c l G e t D e v i c e I n f o ) \n" ) ;
return ;
};
81
p r i n t f ( " Device
c h o o s e d : %s \ n " ,
pbuf ) ;
83 / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
//
Create
an OpenCL command
queue
85 / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
commandQueue = clCreateCommandQueue (
87
context ,
d e v i c e s [DEVICE_NUM] ,
89
0,
&s t a t u s ) ;
91
i f ( status
!= CL_SUCCESS)
p r i n t f ( " Creating
93
{
Command Queue .
( clCreateCommandQueue ) \ n " ) ;
return ;
}
95
size_t
const
97
SourcesSize = Sources . length () ;
char
*
SourcesPtr = Sources . c_str ( ) ;
program = c l C r e a t e P r o g r a m W i t h S o u r c e (
context ,
99
1,
&S o u r c e s P t r ,
101
&S o u r c e s S i z e ,
&s t a t u s ) ;
103
i f ( status
!= CL_SUCCESS)
{
34
pbuf ,
&r e t )
p r i n t f ( " Error :
105
Loading
Binary
into
cl_program
\
( clCreateProgramWithBinary ) \n" ) ;
return ;
107
}
}
Как уже говорилось выше, процесс инициализации OpenСL сложен, но его необходимо
провести один раз, затем достаточно просто копировать готовую функцию из программы
в программу.
10
Парадигма параллельных вычислений в CUDA
Классический подход к параллельным вычислениям (OpenMP, MPI) заключается в разбиении исходной задачи на максимально независимые участки, каждый из которых считается в самостоятельном потоке (OpenMP, MPI) или даже самостоятельном процессе
операционной системы. Причем, чаще всего (ввиду SISD архитектуры отдельного ядра
центрального процессора и высокой стоимости создания потока) количество потоков равно количеству ядер, задействованных под расчет. В CUDA подход к параллелизму значительно отличается, частично это связано с изначальной настроенностью на работу с общей
памятью, частично связано с низкой стоимостью создания потока. Количество потоков не
привязано напрямую к количеству вычислительных ядер, мало того, число потоков обычно в разы превосходит его. Вычислительную конфигурацию потоков можно представить
так:
35
Рис. 13: Вычислительная конфигурация грида
Из рисунка 13 видно, что даже если в блоке число тредов не кратно 32, каждый из
блоков будет независимо разбит на варпы по 32 треда, а последний будет иметь меньшее
число тредов, что иногда может сильно повлиять на скорость работы.
Существуют ограничения на размеры грида и блока, они приведены в таблице 8.
Таблица 8: Ограничения на размер грида и блока
Грид
Блок
X
65536
512 (1024)
Y
65536
512 (1024)
Z
1
64
Всего
4294967296
512 (1024)
Как видно блок может иметь трехмерную топологию, а грид лишь двухмерную. Это
надо учитывать, когда производится распараллеливание изначально трехмерной задачи.
36
Грид выполняется на всей графической карте одновременно. Нет возможности указать,
например, только 2 TPC из 10.
Одновременно на графической карте может выполняться несколько ядер, а значит и гридов.
Блок выполняется целиком на одном SM. Независимо от его размера. На одном SM одновременно может выполняться до 8ми блоков. Количество блоков определяется ограничениями по регистрам и разделяемой памяти.
Стоит отметить, что для CUDA первичным является именно блок, то есть когда программист пишет программу, он настраивает её (чаще всего) из соображений оптимального
блока для алгоритма. В то время как размер грида определятся по принципу «размер
задачи делить на размер блока». Для OpenCL ситуация радикально противоположная,
программист может вообще не указывать размер блока, а лишь назначить грид на размер задачи, тогда программа сама разобьет задачу на блоки. Таким образом, OpenCL
получается более гибким и универсальным, но зато менее производительным на графических ускорителях. Для реализации программы под ГА компания Nvidia сделала свои
расширения для языка С и выпустила компилятор NVCC для сборки таких программ,
ввела в обиход новое расширение *.cu, для файлов, которые содержат CUDA вызовы. К
расширениям языка С относятся:
∙
спецификаторы для функций и переменных
∙
новые встроенные типы
∙
встроенные переменные (внутри ядра)
∙
директива для запуска ядра из C кода
11
Компиляция программ в CUDA и OpenCL
Подход к компиляции программ в CUDA и OpenCL значительно различается, так как для
CUDA Nvidia создало свой собственный компилятор и свои расширения языка C, которые
только он и может компилировать. В OpenCL подход несколько иной, там компиляция
происходит непосредственно драйвером устройства, на котором будет происходить запуск
программы.
37
12
Новые типы в CUDA
В CUDA добавлены множество векторных типов, для удобства копирования и доступа к
данным.
∙
(u/) char, char2, char3, char4
∙
(u/) int, int2, int3, int4
∙
float, float2, float3, float4
∙
longlong, longlong2
∙
double, double2
Для создания переменных таких типов требуется применять функции вида make_(тип)(размерность)
например:
char2
a = make_char2 ( ‘ a ’ , ’ b ’ ) ;
2 p r i n t f ( “%c %c ” ,
float4
a.x,
a . y) ;
b =m a k e _ f l o a t 4 ( 1 . 0 ,
4 p r i n t f ( “%f %f ” ,
b.x,
b.y,
2.0 ,
b.z ,
3.0 ,
4.0) ;
b . w) ;
Для всех типов в cuda > 3.0 определены покомпонентные операции. Так же существует
специальный тип dim3, основанный на типе uint3, имеющий нормальный конструктор и
умеющий инициализировать недостающие координаты единицами. Данный тип используется для задания параметров запуска ядра. Обращаем ваше внимание на то, что именно
единицами! Если вы напишете:
dim3
b l o c k = dim3
(16 ,16 ,
0) ;
программа будет выполняться на блоке с количеством тредов 0! То есть расчет производиться не будет,но программа завершиться абсолютно нормально, не выдав ошибки.
13
Новые типы в OpenCL
∙
(u/) charN
∙
(u/) shortN
38
∙
(u/) intN
∙
(u/) longN
∙
floatN
Где N = 2, 3, 4, 8, 16
Так же существуют специальные типы данных:
∙
image2d_t
∙
image3d_t
∙
sampler_t
∙
event_t
14
Спецификаторы для функций в CUDA
В CUDA добавлено несколько спецификаторов функций, которые позволяют определить,
откуда запускается и где выполняется данная функция (таблица 9).
Таблица 9: Спецификаторы функций
Спецификатор
Выполняется на
Может вызываться из
__device__
устройство
устройство
__global__
устройство
хост
__host__
хост
хост
Спецификатор __global__ применяется для функций, которые задают ядро (в них
передаются несколько специфических переменных). Функции __global__ могут возвращать только void. Спецификатор __global__ применяется исключительно обособленно.
Спецификаторы __host__ и __device__ могут применяться одновременно для задания
функций, выполняющихся и на хосте и на устройстве (компилятор сам создает обе версии кода). Обособленный спецификатор __host__ можно опускать. Для старых версий
видеокарт (до чипа Fermi) существуют дополнительные ограничения на выполняемые на
видеокарте функции:
1 Нельзя брать адрес от функции (за исключением __global__)
39
2 Нет стека, а, следовательно, нет рекурсии
3 Нет static переменных внутри функций
4 Не поддерживается переменное число аргументов в функциях.
Хотя в новых версиях CUDA и есть возможность построения рекурсии, написание
рекурсивного кода крайне не рекомендуется, так как такой алгоритм разрывает SIMD
архитектуру, что влечет за собой значительные замедления в скорости работы программы.
15
Спецификаторы для переменных в CUDA
Кроме спецификаторов функций, в CUDA добавлено несколько спецификаторов переменных, которые в основном задают тот или иной особый тип памяти (таблица 12).
Таблица 10: Спецификаторы переменных
Спецификатор
Находится на
Доступна из
Вид доступа
__device__
устройство
устройство
только чтение
__constant__
устройство
устройство и хост
чтение для устройства и
чтение/запись для хоста
__shared__
устройство
разделяется
совместно
между потоками блока
чтение и запись, требует
явной синхронизации
Спецификатор __device__ является аналог const на центральном процессоре
Спецификатор __shared__ применяется для задания разделяемой памяти и не может
быть инициализирован при объявлении и, как правило, требует явной синхронизации.
Запись в __constant__ может выполнять только через специальные функции с CPU. Модификатор используется для объявления переменных, хранящихся в константной памяти,
речь о которой пойдет позже.
Все спецификаторы нельзя применять к полям структур или union
40
16
Встроенные переменные в CUDA
В CUDA существует несколько особых переменных, которые существуют внутри каждого
вычислительного ядра, И позволяют отличать один тред от другого.
dim3 gridDim – содержит в себе информацию о конфигурации грида при запуске ядра.
uint3 blockIdx – координаты текущего блока внутри грида.
dim3 blockDim – размерность блока при запуске ядра.
uint3 threadIdx – координаты текущего треда внутри блока.
int warpSize – размер варпа (на данный момент всегда равен 32).
17
Директива запуска ядра в CUDA
Для запуска ядра используются специальные директивы для задания параметров ядра и
передачи ему необходимых параметров.
Объявление функции для ядра с параметром params:
1
__global__
void
Kernel_name ( params ) ;
Запуск ядра:
1
Kernel_name<<<g r i d ,
block ,
mem,
s t r e a m >>> (
params
)
,где
dim3 grid – конфигурация грида для запуска. Размер грида указывается в количестве блоков по каждой координате.
dim3 block – конфигурация блока при запуске. Размер блока задается в количестве тредов
по каждой координате.
size_t mem – количество разделяемой памяти на блок, которая выделяется для данного
запуска под динамическое выделение внутри ядра.
cudaStream_t stream – описание потока, в котором запускается данное ядро. Пример общей схемы программы:
1
#d e f i n e
#d e f i n e
BS 2 5 6
N 1024
//
Размер
// В с е г о
блока
элементов
для
расчета
3 / / Объявляем ядро
41
__global__
void
kernel
( int *
data ) {
5 / / Проводим вычисление абсолютной координаты в линейных б л о к е и г р и д е .
int
7
idx = blockIdx . x
*
BS + t h r e a d I d x . x ;
. . . some code. . .
}
9 int
main
() {
/ / Объявляем
int *
11
15
на
массив
на ГА
data ;
/ / Задаем
13
указатель
конфигурацию
ядра ( В с е г о N т р е д о в ,
dim3
b l o c k = dim3 ( BS ) ;
dim3
g r i d = dim3 (N / BS ) ;
конфигурация
линейная )
. . . some code. . .
/ / Запускаем
ядро
17 k e r n e l <<<g r i d ,
с
заданной
конфигурацией
и
передаем
ему
параметры
b l o c k >>> ( d a t a ) ;
. . . some code. . .
19 }
Однако в этом коде нет одной очень важной части – это работы с памятью. Мы только
обозначили передачу параметра в ядро, но при этом реально нигде его не использовали.
18
Директива запуска ядра в OpenCL
Директива запуска ядра в OpenCL отличается от таковой в CUDA, так как для неё нет
специальных символов
1 cl_int
5
7
cl_uint
(
cl_command_queue command_queue ,
kernel ,
work_dim ,
const
size_t
* global_work_offset
const
size_t
* global_work_size
const
size_t
* local_work_size
cl_uint
const
9
позволяющих задать параметры запуска ядра.
clEnqueueNDRangeKernel
cl_kernel
3
≪«»>“,
,
,
num_events_in_wait_list ,
cl_event
cl_event
,
* event_wait_list
,
* event )
command_queue - очередь, в которой необходимо выполнить ядро
kernel - скопмилированное ядро, предназначенное для выполнения
42
work_dim - размерность грида (значение 1-3)
global_work_offset - смещение внутри грида, на данный момент всегда равен NULL
global_work_size - указатель на массив размерности work_dim, задающий размер грида
по каждому из направлений
local_work_size - аналогично global_work_size задает размеры блока
num_events_in_wait_list - количество событий, которые должны произойти перед выполнением ядра
event_wait_list - массив событий, размером num_events_in_wait_list, которые необходимы для выполнения данного ядра
event - массив событий, которые генерирует ядро по завершению
Объявлени ядра
1 kernel
void
Kernel_name ( t y p e 0
arg0 ,
type1 ,
arg1 ) ;
Запуск:
1 cl_event
Event ;
c l S e t K e r n e l A r g ( Kernel_name ,
0,
s i z e o f ( t y p e 0 ) , &a r g 0 _ v a l u e ) ;
3 c l S e t K e r n e l A r g ( Kernel_name ,
0,
s i z e o f ( t y p e 1 ) , &a r g 1 _ v a l u e ) ;
c l E n q u e u e N D R a n g e K e r n e l ( commandQueue ,
NULL,
0 , NULL,
NULL) ;
//
Запуск
Kernel_name ,
dimantions ,
NULL,
&g l o b a l S i z e ,
ядра
Пример общей схемы программы:
#d e f i n e
BS 2 5 6
//
2 #d e f i n e N 1 0 2 4
/ / Объявляем
4 kernel
6
int
// В с е г о
блока
элементов
для
расчета
ядро
void
/ / Проводим
Размер
some_kernel
вычисление
int *
( global
абсолютной
data ) {
координаты
в
линейных
блоке
и
гриде .
idx = get_global_id (0) ;
. . . some code. . .
8 }
int
10
main
() {
/ / Создаем
и
инициализируем
и с п о л не н и я
clKernel
12
/ / Объявляем
int *
и программу ,
kernel
контекст ,
создаем
платформу ,
ядро
=...;
указатель
на
массив
на ГА
data ;
43
устройсто ,
очередь
14
/ / Создаем
cl_mem
буфер
для данных
устройстве
d a t a _ d e v i c e = c l C r e a t e B u f f e r ( c o n t e x t , CL_MEM_READ_WRITE,
some_size_in_byte ,
16
на
/ / копируем
данные
в
NULL,
NULL) ;
буфер
c l E n q u e u e W r i t e B u f f e r ( commandQueue ,
data_size ,
18
/ / Задаем
data ,
аргумент
0 , NULL,
/ / Задаем
22
true ,
0,
sizeof ( int )
*
NULL) ;
ядра
clSetKernelArg ( kernel ,
20
data_device ,
конфигурацию
0,
s i z e o f ( cl_mem ) , &r e s _ d e v i c e ) ;
ядра ( В с е г о N т р е д о в ,
size_t
g l o b a l S i z e = N;
size_t
localSize
конфигурация
линейная )
= BS ;
. . . some code. . .
24
/ / Запускаем
ядро
с
заданной
конфигурацией
c l E n q u e u e N D R a n g e K e r n e l ( commandQueue ,
NULL,
26
0 , NULL,
и
kernel ,
передаем
ему
параметры
1 , &l o c a l S i z e ,
&g l o b a l S i z e ,
NULL) ;
. . . some code. . .
}
Как видно из примера, в OpenCL нету специальных встроенных переменных, вместо них
есть две функции, позволяющих узнать координаты текущего треда:
1 size_t
get_global_id
( uint
dimindx )
Функция позволяет узнать координату треда внутри грида по каждой из размерности
dimindx.
1 size_t
get_local_id
( uint
dimindx )
Функция позволяет узнать координату треда внутри блока по каждой из размерности
dimindx.
Аналогично CUDA, в OpenCL есть функции, позволяющие узнать размерности грида и
блока, а так же координаты блока внутри грида.
1 size_t
get_group_id
( uint
dimindx )
Функция позволяет узнать координату блока внутри грида по каждой из размерности
dimindx.
44
1 size_t
get_global_size
( uint
dimindx )
Функция позволяет узнать размер грида по каждой из размерности dimindx.
1 size_t
get_local_size
( uint
dimindx )
Функция позволяет узнать размер блока по каждой из размерности dimindx. Таким образом
19
Типы памяти
В CUDA и OpenCL есть большое количество различных типов памятей, оптимизированных под один вид деятельности. Ниже будыт подробно разобраны все типы памятей.
19.1
Типы памяти в CUDA
Таблица 11: Типы памяти в CUDA
Тип памяти
Доступ
Выделяется
Скорость работы
Регистры
чтение и запись
индивидуально на поток
Высокая(on-chip)
Локальная
чтение и запись
индивидуально на поток
Низкая(DRAM)
Разделяемая
чтение и запись
совместно на блок
Высокая(on-chip)
Глобальная
чтение и запись
совместно на грид
Низкая(DRAM)
Константная
только чтение
совместно на грид
Низкая(DRAM), но может
кэшироваться
Текстурная
только чтение
совместно на грид
Низкая(DRAM), но может
кэшироваться
Регистры – 32Кб на SM, используется для хранения локальных переменных. Расположены непосредственно на чипе, скорость доступа самая быстрая. Выделяются отдельно для
каждого треда.
45
Локальная – используется для хранения локальных переменных когда регистров не хватает, скорость доступа низкая, так как расположена в DRAM партициях. Выделяется
отдельно для каждого треда.
Разделяемая – 16Кб (или 48Кб на Fermi) на SM, используется для хранения массивов
данных, используемых совместно всеми тредами в блоке. Расположена на чипе, имеет чуть
меньшую скорость доступа чем регистры (около 10 тактов). Выделяется на блок.
Глобальная – основная память видеокарты (на данный момент максимально 6Гб на Tesla
c2070). Используется для хранения больших массивов данных. Расположена на DRAM
партициях и имеет медленную скорость доступа (около 80 тактов). Выделяется целиком
на грид.
Константная – память, располагающаяся в DRAM партиции, кэшируется специальным
константным кэшем. Используется для передачи параметров в ядро, превышаюших допустимые размеры для параметров ядра. Выделяется целиком на грид.
Текстурная – память, располагающаяся в DRAM партиции, кэшируется. Используется
для хранения больших массивов данных, выделяется целиком на грид.
Обращение к памяти в CUDA осуществляется одновременно из половины варпа.
Все типы памяти, кроме регистровой и локальной, можно использовать как эффективно так и не эффективно. Поэтому необходимо четко понимать их особенности и области
применения.
46
19.2
Типы памяти в OpenCL
Таблица 12: Типы памяти в OpenCL
GPU
Тип памяти
Доступ
Вид памяти
Скорость
Глобальная
чтение/запись
Глобальная
Медленно
Локальная
чтение/запись
Разделяемая
Быстро
Глобальная
Медленно
Частная
чтение/запись
Регистры
Константная
Очень быст
только чтение
Регистры, глобальная с кэшем
Быстро
только чтение
Глобальная с кэшем Кэш – быстро, иначе – медленно
RAM
только запись
Глобальная
медленно
Текстурная
Как видно из таблиц 13 и 14, набор типов памяти в CUDA и OpenCL одинаков, различаются лишь названия. Так же видно, что эти типы, по сути, существуют только для
графических ускорителей, для центральных процессоров это одна и та же память.
19.3
Использование глобальной памяти в CUDA
Как уже говорилось выше, глобальная это основная память в CUDA, однако, она является
при этом и самой медленной (не по отдельному, а по большому количеству обращений).
Процесс работы с глобальной памятью очень похож на обычную работу с памятью на центральном процессоре. Сначала происходит инициализация (выделение), затем заполнение
(копирование), затем использование и обратное копирование и в конце освобождение. Все
специальные функции работы с глобальной памятью вызываются на хосте. На устройстве
работа происходит как с обычным массивом. Основные функции работы с глобальной
памятью, все они вызываются на хосте:
1 cudaError_t
cudaMalloc
(
void
**
devPtr ,
size_t
size
) ;
Выделение size памяти на устройстве и записывание адреса в devPtr
1 cudaError_t
size_t
cudaMallocPitch
height
(
void
**
devPtr ,
) ;
47
size_t
*
pitch ,
size_t
width ,
Выделение памяти под двухмерный массив размером width * height, возвращает указатель
devPtr на память и смещение для каждой строки pitch
1 cudaError_t
cudaFree
(
*
void
devPtr
) ;
Освобождение памяти по адресу devPtr на устройстве
1 c u d a E r r o r _ t cudaMemset
( void *
devPtr ,
int
value ,
size_t
count
) ;
Заполнение памяти по адресу devPtr значениями value на размер count
1 c u d a E r r o r _ t cudaMemcpy
cudaMemcpyKind
kind
(
void
*
dst ,
const
void
*
src ,
size_t
count ,
enum
size_t
count ,
enum
);
Копирование данных между устройством и хостом
dst – указатель на память приемник
src – указатель на память источник
count – размер копируемой памяти в байтах
kind – направление копирования может принимать значения:
∙
cudaMemcpyHostToDevice – c хоста на устройство
∙
cudaMemcpyDeviceToHost – с устройства на хост
∙
cudaMemcpyDeviceToDevice – с устройства на устройство
∙
cudaMemcpyHostToHost – с хоста на хост
1 c u d a E r r o r _ t cudaMemcpyAsync
cudaMemcpyKind
kind ,
(
void
*
cud aS tr eam _t
dst ,
const
stream
void
*
src ,
) ;
Аналогично cudaMemcpy только асинхронно в потоке stream.
48
19.4
Использование глобальной памяти в OpenCL
Использование глобальной памяти в OpenCL очень похоже на аналогичное использование в CUDA, только для работы в OpenCL требуется большее количество параметров.
clCreateBuffer
1 cl_mem
clCreateBuffer
* host_ptr
,
cl_int
( cl_context
context ,
cl_mem_flags
flags ,
size_t
size ,
void
* errcode_ret )
Функция создает буфер в памяти устройств и возвращает его дескриптор.
Параметры
context – дескриптор контекста в устройствах которого создается буфер.
flags – битовые флаги-свойства буфера (см таблицу).
Таблица 13: Флаги clCreateBuffer
Флаг
Описание
CL_MEM_READ_WRITE
Буфер доступен ядрам для чтения и записи.
CL_MEM_WRITE_ONLY
Буфер доступен ядрам только для записи.
CL_MEM_READ_ONLY
Буфер доступен ядрам только для чтения.
CL_MEM_USE_HOST_PTR
host_ptr должен быть не NULL. Использовать в качестве буфера область памяти на хосте. Во время исполнения ядер часть или вся область может копироваться на устройство, однако между запусками ядер
информация, записанная в эту память актуальна.
CL_MEM_ALLOC_HOST_PTR
Выделить
хосте.
и
использовать
Флаги
область
памяти
CL_MEM_ALLOC_HOST_PTR
на
и
CL_MEM_USE_HOST_PTR взаимоисключающие.
CL_MEM_COPY_HOST_PTR
host_ptr
должен
создаваемый
памяти.
быть
буфер
Флаги
не
NULL.
данные
из
Скопировать
заданной
в
области
CL_MEM_COPY_HOST_PTR
и
CL_MEM_USE_HOST_PTR взаимоисключающие.
49
size – размер создаваемого буфера в байтах.
host_ptr – указатель на область памяти на хосте. Использование зависит от флагов.
errcode_ret – адрес, по которому будет записан код ошибки. Может быть NULL.
Возвращаемое значение
В случае успеха функция возвращает дескриптор вновь созданной очереди исполнения, а
в errcode_ret записывается CL_SUCCESS. В противном случае функция вернет NULL, а
в errcode_ret будет записан код ошибки. Наиболее распространенные коды ошибок:
∙
CL_INVALID_CONTEXT – context не является корректным дескриптором контекста.
∙
CL_INVALID_VALUE – flags содержит некорректные флаги.
∙
CL_INVALID_BUFFER_SIZE - size равен нулю или больше максимального для
устройства.
∙
CL_INVALID_HOST_PTR – host_ptr не согласуется с флагами.
∙
CL_OUT_OF_HOST_MEMORY ошибка выделения ресурсов на хосте.
∙
CL_OUT_OF_RESOURCES - ошибка выделения ресурсов на устройстве.
clEnqueueReadBuffer
1 cl_int
clEnqueueReadBuffer
cl_bool
blocking_read ,
( cl_command_queue command_queue ,
size_t
num_events_in_wait_list ,
const
offset ,
size_t
cl_event
cb ,
void
cl_mem
* ptr
* event_wait_list
,
,
buffer ,
cl_uint
cl_event
* event )
Помещает в очередь исполнения команду на чтение данных из буфера.
Параметры
command_queue – очередь исполнения.
buffer – дескриптор буфера.
blocking_read –если значение этого параметра равно CL_TRUE, то чтение блокирующее,
clEnqueueReadBuffer не вернет управление, пока чтение не будет завершено.
offset – смещение от начала буфера, откуда начинать чтение.
cb – количество байт, которые необходимо прочитать.
50
ptr – указатель на область памяти хоста, куда осуществляется запись прочитанных данных. event_wait_list, num_events_in_wait_list – задают спсок событий, завершения которых необходимо дождаться до начала чтения. Может быть пустым.
event – адрес, по которому сохраняется дескриптор создаваемого события. В случае неблокирующего чтения по этому событию можно будет отследить состояние процесса чтения.
Может быть NULL.
Возвращаемое значение
Возвращает CL_SUCCESS в случае успеха.
В противном случае вернет один из следующих кодов ошибки:
∙
CL_INVALID_COMMAND_QUEUE command_queue не является корректным дескриптором очереди исполнения.
∙
CL_INVALID_CONTEXT – context не является корректным дескриптором контекста.
∙
CL_INVALID_MEM_OBJECT - buffer не является корректным дескриптором буфера.
∙
CL_INVALID_VALUE – offset и сb задают некорректный регион для чтения, выходящий за границы буфера, либо нулевого размера.
∙
CL_OUT_OF_HOST_MEMORY - ошибка выделения ресурсов на хосте.
∙
CL_OUT_OF_RESOURCES - ошибка выделения ресурсов на устройстве.
clEnqueueWriteBuffer
1 cl_int
clEnqueueWriteBuffer
cl_bool
blocking_write ,
num_events_in_wait_list ,
( cl_command_queue command_queue ,
size_t
const
offset ,
cl_event
size_t
cb ,
void
cl_mem
* ptr
* event_wait_list
,
,
buffer ,
cl_uint
cl_event
* event )
Помещает в очередь исполнения команду на запись данных в буфер.
Параметры
command_queue – очередь исполнения.
buffer – дескриптор буфера.
blocking_write – если значение этого параметра равно CL_TRUE, то запись блокирующее,
51
clEnqueueWriteBuffer не вернет управление, пока запись не будет завершена.
offset – смещение от начала буфера, откуда начинать запись.
cb – количество байт, которые необходимо записать.
ptr – указатель на область памяти хоста, откуда осуществляется чтение исходных данных.
event_wait_list, num_events_in_wait_list – задают спсок событий, завершения которых
необходимо дождаться до начала записи. Может быть пустым.
event – адрес, по которому сохраняется дескриптор создаваемого события. В случае неблокирующей записи по этому событию можно будет отследить состояние процесса чтения.
Может быть NULL.
Возвращаемое значение
Возвращает CL_SUCCESS в случае успеха.
В противном случае вернет один из следующих кодов ошибки:
∙
CL_INVALID_COMMAND_QUEUE command_queue не является корректным дескриптором очереди исполнения.
∙
CL_INVALID_CONTEXT – context не является корректным дескриптором контекста.
∙
CL_INVALID_MEM_OBJECT buffer не является корректным дескриптором буфера.
∙
CL_INVALID_VALUE –offset и сb задают некорректный регион для записи, выходящий за границы буфера, либо нулевого размера.
∙
CL_OUT_OF_HOST_MEMORY ошибка выделения ресурсов на хосте.
∙
CL_OUT_OF_RESOURCES ошибка выделения ресурсов на устройстве.
19.5
Вычисление числа Пи
Для иллюстрации основ работы с глобальной памяти приведем пример задачи нахождения
числа Пи: Требуется вычислить число пи, методом интегрирование четверти окружности
радиуса 1
52
Рис. 14: Вычисление числа Пи
1 #i n c l u d e < s t d i o . h>
#i n c l u d e <t i m e . h>
3
#d e f i n e CUDA_FLOAT f l o a t
5 #d e f i n e GRID_SIZE 2 5 6
#d e f i n e
BLOCK_SIZE 2 5 6
7
//
Проверка
9 void
на
ошибку
выполнения функций
check_cuda_error ( c o n s t
char
из
cuda
API
* message )
{
11
cudaError_t
e r r = cudaGetLastError ( ) ;
i f ( e r r != c u d a S u c c e s s )
13
p r i n t f ( "ERROR: %s : %s \ n " ,
message ,
cudaGetErrorString ( err ) ) ;
}
15
53
17
__global__
void
* res )
p i _ k e r n (CUDA_FLOAT
19 {
int
21
*
n = threadIdx . x + blockIdx . x
*
CUDA_FLOAT x0 = n
BLOCK_SIZE ;
1. f
/
(BLOCK_SIZE
s q r t f (1
−
x0
*
GRID_SIZE ) ;
//
Начало
отрезка
интегрирования
CUDA_FLOAT y0 =
23
CUDA_FLOAT dx = 1 . f
CUDA_FLOAT s = 0 ;
25
CUDA_FLOAT x1 ,
/
//
*
*
(1. f
x0 ) ;
BLOCK_SIZE
Значение
интеграла
*
GRID_SIZE ) ;
по
отрезку ,
/ / Шаг и н т е г р и р о в а н и я
данному
текущему
треду
y1 ;
x1 = x0 + dx ;
27
y1 =
−
s q r t f (1
*
s = ( y0 + y1 )
29
res [n]
= s;
//
*
x1
dx
x1 ) ;
/
2. f ;
Запись
//
Площадь трапеции
результата
в
глобальную
память
}
31
int
main ( i n t
c h a r **
argc ,
argv )
33 {
35
37
c u d a S e t D e v i c e (DEVICE) ;
//
check_cuda_error ( " Error
selecting
CUDA_FLOAT
* res_d ;
//
cudaMalloc ( ( void
Выделение
*
//
Рамеры
грида
и
блока
g r i d ( GRID_SIZE ) ;
43
dim3
b l o c k (BLOCK_SIZE) ;
45
p i _ k e r n<<<g r i d ,
res_d ,
51
//
Запуск
в
хостовой
GRID_SIZE
*
памяти
BLOCK_SIZE) ;
//
Копируем
results
Освобождаем
check_cuda_error ( " F r e e i n g
ядра
работы
ядра
kernel ") ;
*
s i z e o f (CUDA_FLOAT)
check_cuda_error ( " Copying
//
*
/ / Ожидаем завершения
check_cuda_error ( " Executing
cudaFree ( res_d ) ;
Результаты
memory on GPU" ) ;
b l o c k >>>(r e s _ d ) ;
cudaMemcpyDeviceToHost ) ;
49
//
на GPU
cudaThreadSynchronize ( ) ;
cudaMemcpy ( r e s ,
устройстве
на CPU
dim3
47
на
s i z e o f (CUDA_FLOAT)
check_cuda_error ( " A l l o c a t i n g
41
device ") ;
BLOCK_SIZE ] ;
* * )&res_d ,
памяти
устройства
Результаты
CUDA_FLOAT r e s [ GRID_SIZE
39
Выбор
device
GRID_SIZE
результаты
f r o m GPU" ) ;
память
на GPU
memory" ) ;
CUDA_FLOAT p i = 0 ;
54
*
на
BLOCK_SIZE,
хост
//
53
for
( int
i =0;
i < GRID_SIZE
*
BLOCK_SIZE ;
i ++)
{
55
p i += r e s [ i ] ;
}
*=
57
pi
59
p r i n t f ( " PI = %.12 f \ n " , p i ) ;
4;
return
0;
61 }
В данном примере все операции происходят с типом CUDA_FLOAT, который может быть
как float так и double, что бы читатель смог проверить работоспособность ГА на одинарной
и на двойной точности. Однако данный пример является идеальным, используется один
линейный массив, происходит только последовательная запись. Такие условия выполняются далеко не всегда, поэтому необходимо уточнить некоторые особенности эффективного
использования глобальной памяти.
Код для вычисления числа Пи на OpenCL, можете посмотреть в приложении данного
методического пособия.
19.6
Использование объединения при работе с глобальной памятью на графических картах NVIDIA
ГА умеет объединять ряд запросов к глобальной памяти в один блок (транзакцию) при
условии:
∙
Независимо происходит для каждого half-warp’а
∙
Длина блока должна быть 32/64/128 байт
∙
Блок должен быть выровнен по своему размеру
Кроме того, условия выполнения объединения зависят от Compute Capability (Таблица
14):
55
Таблица 14: Условия возникновения объединения
СС 1.0, 1.1
СС >= 1.2
Нити обращаются к 32-битовым словам,
Нити обращаются к 8-битовым словам,
давая 64-байтовый блок или 64-битовым
дающим один 32-байтовый сегмент, или
словам, давая 128-байтовый блок
16-битовым
словам,
дающим
один
64-
байтовый сегмент, или 32-битовым словам,
дающим один 128-байтовый сегмент
Все 16 слов лежат в пределах блока и при
Получающийся сегмент выровнен по свое-
этом k-ая нить half-warp’а обращается к k-
му размеру
му слову блока
Ниже приведены примеры существования и отсутсвия объединения:
Рис. 15: Пример существования объединения
56
Рис. 16: Пример отсутствия объединения для СС 1.0,1.1
Для СС >= 1.2 в первом случае (Рисунок 16) будет существовать, так как блок выровнен по своему размеру. В случае если объединение не произошло в СС 1.0, 1.1 будет
проведено 16 отдельных транзакций, а в СС >= 1.2 будет создано несколько блоков объедиения, которые покрывают запрашиваемую область. Использование объединения позволяет значительно повысить производительность программы. Однако существует способ
ещё сильнее ускорить производительность ГА, в случае если обращение к данным сильно
локализовано внутри блока – использование разделяемой памяти.
19.7
Использование разделяемой памяти
Основной особенностью разделяемой памяти, является то, что она сильно оптимизирована под обращение к ней одновременно 16 тредов. Для повышения пропускной способности
вся разделяемая память разбита на 16 банков, состоящих из 32 битовых слов, причем,
последовательно идущие слова попадают в последовательно идущие блоки. Обращение к
каждому банку происходит независимо. Если несколько тредов из полуварпа обращают-
57
ся к одному и тому же банку, то возникает конфликт банков, то есть последовательное
чтение, что замедляет скорость работы программы. Второй важной особенностью разделяемой памяти является то, что она выделяется на блок, то есть сначала треды копируют
в ней каждый свой участок данных, а потом совместно используют полученный массив.
Для задания разделяемой памяти используется спецификатор __shared__ (в OpenCL
__local). Часто использование разделяемой памяти требует явной синхронизации внутри
блока.
Стоит отметить, что синхронизация в CUDA внутри вычислительного ядра возможна
только в рамках отдельного блока при помощи команды __syncthreads(). В то время
как в OpenCL есть возможность синхронизировать внутри ядра как отдельный блок, так
и весь грид целиком при помощи команды barrier(CLK_LOCAL_MEM_FENCE), или
barrier(CLK_GLOBAL_MEM_FENCE) соответственно.
Приведем несколько простых примеров:
Пример первый: Расположение float массива в разделяемой памяти.
1 #d e f i n e BS 2 5 6 ;
__global__
void
3 // Создается
__shared__
5 int
/ / Размер
kern ( f l o a t
массив
float
float
из
*
в
data ) {
// разделяемой
памяти :
a [ BS ] ;
idx = blockIdx . x
/ / Копируем
блока
*
BS + t h r e a d I d x . x ;
глобальной
памяти
в
разделяемую
7 a [ idx ] = data [ idx ] ;
/ / Перед
использованием
надо
быть
уверенным
Синхронизируем .
9 __syncthreads ( ) ;
/ / Используем
11 d a t a [ i d x ] = a [ i d x ]+ a [ ( i d x + 1 ) % BS ] ;
}
58
что
все
данные
скопированы .
Рис. 17: Расположение float в разделяемой памяти
Конфликтов не возникает, так как float занимают 32 бита, и последовательные float располагаются в последовательных банках.
Пример второй: Расположение short массива в разделяемой памяти.
#d e f i n e
BS
256;
2 __global__ v o i d
// Создается
float
a [ idx ]
в
data ) {
// разделяемой
памяти :
a [ BS ] ;
idx = blockIdx . x
6 / / Копируем и з
блока
kern ( s h o r t *
массив
4 __shared__ s h o r t
int
/ / Размер
*
BS + t h r e a d I d x . x ;
глобальной
памяти
в
разделяемую
= data [ idx ] ;
8 / / Перед и с п о л ь з о в а н и е м надо быть уверенным что
Синхронизируем .
__syncthreads ( ) ;
10 / / Используем
data [ idx ]
= a [ i d x ]+
a [ ( i d x + 1 ) % BS ] ;
12 }
59
все
данные
скопированы .
Рис. 18: Расположение short в разделяемой памяти
Возникают конфликты 2го порядка, так как short занимают 16 бит, и два последовательных short располагаются в одном банке. Аналогично если объявлять массив char, возникнет конфликт 4го порядка, так как char это 8 бит.
Что бы продемонстрировать, чем же разделяемая память помогает ускорить работу программы, разберем задачу перемножения матриц.
19.8
Задача перемножения матриц. Глобальная память.
В ядре нам будут доступны следующие переменные:
WA, WB
BS – размер блока по любому измерению (блок квадратный)
60
BX = blockIdx.x
BY = blockIdx.y
TX = threadIdx.x
TY = threadIdx.y
Все матрицы хранятся в линейных массивах.
Каждый тред будет считать отдельный элемент матрицы C. Для этого ему понадобится
строка из A и столбец из B. Так как мы используем только глобальную память, строка и
столбец будут полностью считываться каждым тредом(см рисунок 19)!
61
Рис. 19: Перемножение матриц с глобальной памятью.
#d e f i n e
BLOCK_SIZE 1 6
2 __global__ v o i d
matMult
(
float
*
a,
float
*
{
4
6
8
int
bx
= blockIdx . x ;
int
by
= blockIdx . y ;
int
tx
= threadIdx . x ;
int
ty
= threadIdx . y ;
float
sum = 0 . 0 f ;
int
ia
= wa
*
BLOCK_SIZE
*
by + wa
*
62
ty ;
b,
float
*
с,
int
wa ,
int
wb
)
10
12
int
ib
= BLOCK_SIZE
int
ic
= wb
int
k = 0;
for
14
c
(
*
bx + t x ;
BLOCK_SIZE
k < n;
sum += a
[ ia + k ]
[ ic
*
+ wb
*
*
ty + tx ]
b
*
by + BLOCK_SIZE
*
bx ;
k++ )
[ ib + k
*
wb ] ;
= sum ;
}
Таким образом, происходят:
∙
2 * WA обращения к глобальной памяти
∙
2 * WA арифметические операции
Если посмотреть, на что больше всего ГА тратит времени при работе, то получается
следующая картина (рисунок 20)
Рис. 20: Профиль программы перемножения матриц с использованием глобальной памяти
ТО видно, что большую часть времени (около 85%) ушло на работу с памятью. Попробуем применить разделяемую память.
19.9
Задача перемножения матриц. Разделяемая память.
При использовании разделяемой памяти, мы не можем полностью скопировать все строки
и все столбцы, требуемые для блока, так как не хватит памяти. Поэтому, будем осуществлять поблочное умножение:
С = A1 * B1 + A2 * B2 + . . . .
И каждую пару блоков будем копировать в разделяемую память.
Данную задачу предлагается решить читателю самостоятельно!
63
Если читатель справился с поставленной задачей, то он может убедиться, что в таком
варианте программы выполняется:
∙
WA / 8 обращения к глобальной памяти
∙
2 * WA арифметические операции
Потому как каждый тред теперь копирует лишь отмеченные черными квадратиками элементы(рисунок 21).
Рис. 21: Перемножение матриц с глобальной памятью.
64
И профиль программы имеет примерно такой вид (Рисунок 22):
Рис. 22: Профиль программы перемножения матриц с использованием разделяемой памяти
Теперь вычисления занимают 81% времени, а обращения к памяти лишь 13% (Рисунок
22). Достигается ускорение более чем на порядок.
19.10
Использование константной памяти.
Константная память используется тогда, когда в ядро необходимо передать много различных данных, которые будут одинаково использоваться всеми тредами ядра.
1 __constant__
float
contsData
для
contsData
использования
cudaMemcpyToSymbol
(
constData ,
cudaMemcpyHostToDevice
константную
[256];
);
в
//
объявление
качестве
hostData ,
// копирование
глобальной
константной
sizeof
(
данных
data
с
переменной
с
именем
памяти .
) ,
0,
центрального
процессора
в
память .
Использование внутри ядра ничем не отличается от использования любой глобальной
переменной на хосте.
19.11
Использование текстурной памяти.
При рассмотрении разделяемой памяти, мы предполагали, что использование памяти
внутри блока сильно локализовано, то есть что блоку для его работы требуется лишь
какой-то определенный участок массива, если же локализовать обращение не удается, а
скорость обращения к памяти является узким местом программы, можно попробовать использовать разделяемую память.
Основная особенность текстурной памяти – это использования КЭШа. Однако существуют
65
дополнительные стадии конвейера (преобразование адресов, фильтрация, преобразование
данных), которые снижают скорость первого обращения. Поэтому текстурную память разумно использовать с следующих случаях:
∙
Объем данных не влезает в shared память
∙
Паттерн доступа хаотичный
∙
Данные переиспользуются разными потоками
Для использования текстурной памяти необходимо задать объявление текстуры как глобальную переменную:
texture < type
,
dim ,
t e x _ t y p e> g_TexRef ;
Type – тип хранимых переменных Dim – размерность текстуры (1, 2, 3) Tex_type – тип
возвращаемых значений
∙
cudaReadModeNormalizedFloat
∙
cudaReadModeElementType
Кроме того, для более полного использования возможностей текстурной памяти можно
задать описание канала:
1 struct
int
3
cudaChannelFormatDesc
x,
enum
y,
z,
{
w;
cudaChannelFormatKind
f ;
};
Задает формат возвращаемого значения
int x, y, z, w; - число [0,32] проекция исходного значения по битам
cudaChannelFormatKind – тип возвращаемого значения
∙
cudaChannelFormatKindSigned – знаковые int
∙
cudaChannelFormatKindUnsigned – беззнаковые int
∙
cudaChannelFormatKindFloat – float
В CUDA существуют два типа текстур линейная и cudaArray (Таблица 14).
66
Download