Введение в 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