Занятие 1. Программная модель CUDA
В данном уроке будет изложено описание программной модели CUDA, основные термины и понятия, а также коротко будет описана концепция модели памяти GPU.
Итак, ниже приведены основные термины и понятия, которые будут необходимы для работы с CUDA:
Хост (Host) — центральный процессор, управляющий выполнением программы.
Устройство (Device) — видеоадаптер, выступающий в роли сопроцессора центрального процессора.
Грид (Grid) — объединение блоков, которые выполняются на одном устройстве.
Блок (Block) — объединение тредов, которое выполняется целиком на одном SM. Имеет свой уникальный идентификатор внутри грида.
Тред (Thread, поток) — единица выполнения программы. Имеет свой уникальный идентификатор внутри блока.
Варп (Warp) — 32 последовательно идущих треда, выполняется физически одновременно.
Ядро (Kernel) — параллельная часть алгоритма, выполняется на гриде.
Потоковые мультипроцессоры (Streaming Multiprocessors) — это система, состоящая из нескольких планировщиков варпов (Warps), 32-х процессорных ядра CUDA, использующие совместно память и кэш, то есть они видимые для всех процессоров.
Планировщик варпов (Warp Scheduler) — это система, создающая варпы из 32 потоков и распределяющая их по разным SM-процессорам. Планирование варпов осуществляется таким образом, чтобы максимально скрыть время ожидания данных из глобальной памяти: пока одни группы потоков ожидают данные, над другими исполняются инструкции.
Для реализации программы под GPU компания NVIDIA выпустила свои расширения для языка С, компилятор NVCC для сборки таких программ, ввела в обиход новое расширение *.cu для файлов, которые содержат CUDA вызовы.
Проще говоря, с программной точки зрения архитектура CUDA есть нечто иное, как расширение языков программирования С/С++, но с некоторыми аннотациями для их отличия от кода хоста, а также аннотации для различения типов памяти данных, которые существуют на графическом процессоре. Такие функции могут иметь параметры, и их можно вызвать с помощью синтаксиса, который очень похож на обычный вызов функции C, но немного расширен для возможности указать матрицу потоков GPU, которые должны выполнять вызываемую функцию. В течение своего жизненного цикла хост-процесс может отправлять много параллельных задач графического процессора.
Исходные файлы для приложений CUDA состоят из комбинации обычного хост-кода C ++ и функций устройства GPU. Трассировка CUDA-компиляции отделяет функции устройства от главного кода, компилирует функции устройства с использованием собственных компиляторов NVIDIA (nvcc.exe), компилирует код хоста с использованием компилятора хоста C ++.
Ключевые особенности программной стороны CUDA включают в себя:
-
дополнительные типы переменных.
Итак, разберем каждую ключевую особенность отдельно. Сперва нужно пояснить, что же такое спецификаторы в CUDA и какие они бывают.
Спецификаторы определяют вызов функции, а всего их 3:
Существуют дополнительные ограничения на выполняемые на видеокарте функции:
-
не поддерживается переменное число аргументов в функциях.
Спецификаторы переменных, которые служат для указания типа используемой памяти GPU, приведены в таблице 1.
Таблица 1.
Спецификаторы переменных в CUDA
Спецификатор | Находится в | Доступна для | Вид доступа |
__device__ | устройстве | устройства | только чтение |
__constant__ | устройстве | устройства и хоста | чтение/запись |
__shared__ | устройстве | блока потоков | чтение/запись |
На их использование также накладывается ряд ограничений:
__device__ является аналогом const на центральном процессоре.
__shared__ применяется для задания разделяемой памяти и не может быть инициализирован при объявлении. Он требует явной синхронизации потоков внутри одного блока после любой операции с такой памятью.
__constant__ память осуществляется только через специальные функции с CPU,
kernelName – это имя функции ядра спецификатора __global__.
Dg или gridSize – это размерность сетки для блоков dim3, выделенная для расчетов.
Db или blockSize – размер блока dim3, выделенного для расчетов.
S или cudaStream – переменная cudaStream_t, задающая поток, в котором будет произведен вызов. В дальнейшем будут приведены примеры, где это еще раз будет отмечено.
Рассмотрим следующую конструкцию:
__global__void sum_kernel (int *a, int *a, int *c, int N) { int main ( void ) { kernel <<<1,1>>>(); } ... //код для GPU }
Атрибут __global__, как уже было сказано, запускает программу с хоста, а исполняет ее на девайсе.
А void говорит нам о том, что возвращаемое значение отсутствует. Далее идет название самой функции name_kernel, которая обладает аргументами, определенными в круглых скобках (указатели на переменные). Дальше открывается основная (main) функция, в которой есть следующая строка:
Тройные угловые скобки указывают на то, что происходит вызов от хоста к девайсу, цифры внутри скобок обозначают количество блоков и количество тредов (потоков в блоке) соответственно. Например, мы хотим использовать 1 блок и указываем N потоков в этом блоке, тогда строчка имеет следующий вид:
Обращаться к отдельному блоку можно через индекс массива блоков blockIdx.x. Подробное описание переменной будет дано в следующей главе в листинге программ.
Точно также можно менять число тредов, а блок будет один, либо использовать конструкцию, где задействовано и несколько блоков и несколько тредов одновременно.
Продолжая тему встроенных переменных, следует отметить, что в CUDA добавлено несколько векторных типов для удобства копирования и доступа к данным. Типы (u)char, (u)int, (u)short, (u)long, float могут быть 1-, 2-, 3-, 4-мерными векторами, а longlong, double — только 1- и 2-мерными.
Для создания переменных таких типов требуется применять функции вида make_(тип)(размерность), например:
dim3, основанный на типе uint3, имеющий нормальный конструктор. Этот конструктор позволяет задавать не все компоненты вектора (недостающие компоненты инициализируются единицами). Данный тип используется для задания параметров запуска ядра. dim3 block = dim3 ( 64, 2 ).
К встроенным переменным-идентификаторам тредов и блоков относят:
dim3 gridDim – размерность грида. Содержит в себе информацию о конфигурации сетки.
uint3 blockIdx – индекс текущего блока в вычислении на GPU. Иными словами, позволяет узнать координаты текущего блока внутри сетки.
dim3 blockDim – размерность блока при запуске ядра. Позволяет узнать размерность блока, выделенного при текущем вызове ядра.
uint3 threadIdx – индекс текущего треда в вычислении на GPU, или другими словами, координаты текущего потока внутри блока.