МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ Федеральное агентство по образованию Государственное образовательное учреждение высшего профессионального образования Московский физико-технический институт (государственный университет) Казённов А.М. ОСНОВЫ ТЕХНОЛОГИИ 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