Введение в CUDA

advertisement
Введение в CUDA





1981г. - MDA (Monochrome Display
Adapter) для IBM PC
1996 г. - 3dfx Voodoo - первые графические
ускорители
2000 г. - DirectX 8.0 и первые шейдеры с
аппаратной поддержкой
2004 г. - OpenGL 2.0 и GL Shading Language
2007 г. - CUDA SDK
CUDA – это программно-аппаратная архитектура.
 Общие концепции:
 GPU – сопроцессор
 Большое количество легковесных нитей исполнения
Специальная организация потоков, памяти
 Расширение языка C++ (атрибуты, типы,
переменные)
 CUDA Runtime API (библиотека функций):

 CUDA driver API
 CUDA API
Ядро Core i7
Ядро GF100
NVIDIA GTX 470
Графический процессор состоит из
нескольких (3 - 30) потоковых
мультипроцессоров (SM, streaming
multiprocessor)
 Каждый SM представляет собой
полнофункциональный многоядерный
вычислительный процессор

 Для архитектуры Tesla в одном SM 8 ядер
 Для архитектуры Fermi в одном SM 32 ядра
Кроме вычислительных ядер SM содержит
собственную регистровую память,
разделяемую память, кэш первого уровня,
блок аппаратного планировщика и
различные специализированные блоки
 (!) Все ядра SM’a всегда выполняют одну и
ту же инструкцию (но результат
выполнения для каждого ядра свой)

С токи зрения аппаратной части,
минимальной единицей исполнения
является не поток (инструкции, которые
выполняются на одном ядре), а warp - 32
последовательно взятых потока, которые
выполняются на одном SM’e.
 Все потоки в warp’e всегда выполняются
синхронно, даже когда разные потоки
warp’a испытывают ветвление

if (thread_index % 2 == 0) {
/* Здесь четные потоки warp’a делают
присваивание, а нечетные ожидают (или
выполняют инструкции «вхолостую») */
A[thread_index] = 1;
} else {
/* Теперь присваивание делают нечетные
потоки, а ожидают четные */
A[thread_index] = 2;
}
/* Эту инструкцию выполняют уже все потоки */
B[thread_index] = 3
Для большинства CPU архитектур верно,
что для оптимального быстродействия
число ядер должно совпадать с
количеством запущенных потоков
 Связано это, в первую очередь, с очень
долгим процессом переключения ядра
контекста процессора с одного потока на
другой

В CUDA каждый SM содержит аппаратный
блок планирования warp’ов, поэтому
смена одного warp’a на другой происходит
практически без накладных расходов
 В отличие от архитектур x86 и x86-64, где
регистры процессора жестко привязаны к
конкретному ядру, регистровая память в
GPU динамическая, и каждый поток имеет
собственные регистры

GPU не накладывает особых ограничений
на количество запускаемых нитей
исполнения, и что характерно, запуск
большого (гораздо большего, чем число
ядер ≤ 1024) количества нитей не
замедляет работу GPU, а в некоторых
случаях даже немного ускоряет
 Такое большое число нитей требует
введение некоторой организации

В архитектуре CUDA принят следующий
способ организации потоков
 Grid - самая крупная единица выполнения.
Представляет все потоки, выполняющиеся
функцией-ядром. Состоит из блоков.
 Block - единица выполнения на SM’e.
Каждому блоку предоставляется один SM.
Это самая большая единица, в которой
возможно взаимодействие потоков


Все потоки в блоке автоматически
разбиваются на warp’ы согласно своему
номеру (32 подряд идущих потока
образуют warp). Все warp’ы из одного
блока выполняются на SM’е, так же, как
несколько потоков выполняется на одном
ядре CPU, только планированием
выполнения занимается блок
планирования warp’ов самого SM’а

Для программиста, grid - это описание
двумерной сетки из блоков. По каждому из
двух измерений grid не превосходит 65535

dim3 grid(20,50);

описывает сетку из блоков размерами 20
на 50 блоков
Для программиста, block - это описание
трехмерной сетки из нитей. Максимальные
размеры блока зависят от конкретной
модели, и указаны в структуре
cudaDeviceProperties.
 Типичные значения максимальных
размеров блока 512 на 512 на 16 потоков


Аналогично grid’у, чтобы указать размеры
block’а нужно описать переменную
dim3 block(10,20,1);
или просто
 dim3 block(10,20);


для определения блока размерами 10 на
20 нитей (двумерный блок)
Однако, сделать блок размерами 512x512
не выйдет
 Регистровая память на одном SM’е должна
быть разделена между всеми потоками,
которые на этом SM’е выполняются, то
есть регистровой памяти на одном SM’е
должно хватить на все нити в блоке
 16К регистров / ~32 регистра на поток =
~512 потоков в блоке


В CUDA определены следующие типыструктуры
 dim3
 int2, int3, int4
 float2, float3, float4
 double2


Поля этих структур называются x, y, z и w
Работа с ними почти не отличается от
работы с простыми типами int, float, double

Введены следующие спецификаторы для
функций
Спецификатор
Функция вызывается
из кода
Функция выполняется
на
__device__
GPU
GPU
__global__
CPU
GPU
__host__
CPU
CPU

Ядро (kernel) всегда имеет спецификатор
__global__ и возвращает void

Для запуска ядра, требуется указать
конфигурацию запуска, которая в
простейшем варианте состоит из размеров
grid’a и block’a. Конфигурация запуска
указывается в тройных угловых скобках
после имени функции

kernel<<<grid,block>>>(arguments);
После запуска ядра в коде CPU происходит
помещение необходимых для запуска ядра
параметров в специальную очередь
выполнения на GPU и управление сразу
возвращается CPU.
 Это означает, что ядро выполняется
асинхронно по отношению к CPU коду
 Пока ядро выполняется на GPU, CPU может
продолжать выполнять свой код, а может
просто ждать, пока GPU закончит





После фактического запуска ядра на устройстве
образуется набор задач-блоков, которые
необходимо выполнить
GPU последовательно выполняет
задачи-блоки на свободных SM’ах
 Нет никакой гарантии, что один блок выполнится
до или после другого, или одновременно с ним
В каждом блоке параллельно выполняются все
warp’ы.
После выполнения всех задач-блоков GPU
переходит к следующему ядру из очереди

Поскольку физически оперативная память
CPU и, ее аналог, глобальная память GPU
разделены, требуется ее специально
выделять и выполнять копирования из
одной в другую.

Глобальная память на GPU выделяется с
помощью функции
cudaError_t cudaMalloc(void **p, size_t size)

Вызов
cudaMalloc(&p, 10*sizeof(float));

соответствует вызову
p = malloc(10*sizeof(float));

для выделения памяти для CPU

Глобальная память на GPU освобождается
с помощью функции
cudaError_t cudaFree(void *p)

Вызов
cudaFree(p);

соответствует вызову
free(p);

для освобождения памяти для CPU

Содержимое памяти можно скопировать
как с GPU на CPU, так и обратно с помощью
функции
cudaError_t cudaMemcpy(void *dst, void *src,
size_t size,
enum cudaMemcpyKind dir)

Функция аналогична memcpy, за
исключением последнего параметра

Параметр направления копирования dir
может принимать следующие значения




cudaMemcpyHostToHost
cudaMemcpyHostToDevice
cudaMemcpyDeviceToHost
cudaMemcpyDeviceToDevice

Чтобы в функциях-ядрах можно было
отличить один поток от другого, у каждой
нити есть несколько специальных
связанных с ней переменных
dim3
dim3
dim3
dim3
threadIdx - положение нити в блоке
blockIdx - положение блока в grid’е
blockDim - размеры block’а
gridDim - размеры grid’а


Поэлементно сложим два массива на GPU
Для этого нужно
 создать два массива в оперативной памяти
 создать их двойники на GPU
 скопировать данные на GPU
 выполнить ядро
 скопировать обратно
 распечатать
Возьмите файл hello.cu из директории
/home/summer2012/shared
 Скопируйте его себе в домашнюю папку
 Скомпилируйте командой

 nvcc hello.cu -o hello

Запустите полученный файл
 ./hello
Download