Составители: доц. Андреев А.Е., Шаповалов О.В. Волгоградский государственный технический университет.
Содержание методички (без картинок):
Введение
CUDA (англ. Compute Unified Device Architecture) — технология GPGPU (англ. General-Purpose computing on Graphics Processing Units – вычисления общего назначения на графических процессорах), позволяющая программистам реализовывать на упрощённом языке программирования Си алгоритмы, выполнимые на графических процессорах видеокарт GeForce восьмого поколения и старше. Технология CUDA разработана компанией Nvidia.
Фактически CUDA позволяет включать в текст Си программы специальные функции. Эти функции пишутся на упрощённом языке программирования Си и выполняются на графических процессорах Nvidia.
Чтобы понять, какие преимущества приносит перенос расчётов на видеокарты, приведём усреднённые цифры, полученные исследователями по всему миру. В среднем, при переносе вычислений на GPU, во многих задачах достигается ускорение в 5-30 раз, по сравнению с быстрыми универсальными процессорами.
CUDA Program Diagram — Intro to Parallel Programming
Состав NVIDIA CUDA
CUDA включает два API: высокого уровня (CUDA Runtime API) и низкого (CUDA Driver API), хотя в одной программе одновременное использование обоих невозможно, нужно использовать или один или другой. Высокоуровневый работает «сверху» низкоуровневого, все вызовы runtime транслируются в простые инструкции, обрабатываемые низкоуровневым Driver API. Но даже «высокоуровневый» API предполагает знание основ об устройстве и работе видеокарт NVIDIA.
Есть и ещё один уровень, даже более высокий — две библиотеки:
CUBLAS — CUDA вариант BLAS (Basic Linear Algebra Subprograms), предназначенный для вычислений задач линейной алгебры и использующий прямой доступ к ресурсам GPU;
CUFFT — CUDA вариант библиотеки Fast Fourier Transform для расчёта быстрого преобразования Фурье, широко используемого при обработке сигналов. Поддерживаются следующие типы преобразований: complex-complex (C2C), real-complex (R2C) и complex-real (C2R).
Написание программ на CUDA
Для разработки собственных программ следует разбираться в базовых архитектурных особенностях видеочипов NVIDIA. Все инструкции в видекарте выполняются по принципу SIMD, когда одна инструкция применяется ко всем потокам в warp (в CUDA это группа из 32 потоков — минимальный объём данных, обрабатываемых мультипроцессорами). Этот способ выполнения назвали SIMT (single instruction multiple threads — одна инструкция и много потоков).
При исполнении программы, центральный процессор выполняет свои порции кода, а GPU выполняет CUDA код с наиболее тяжелыми параллельными вычислениями. Эта часть, предназначенная для GPU, называется ядром (kernel). В ядре определяются операции, которые будут исполнены над данными.
Таким образом, CUDA использует параллельную модель вычислений, когда каждый из SIMD процессоров выполняет ту же инструкцию над разными элементами данных параллельно. GPU является вычислительным устройством(device) для центрального процессора(host), обладающим собственной памятью и обрабатывающим параллельно большое количество потоков. Ядром (kernel) называется функция для GPU, исполняемая потоками. Видеочип отличается от CPU тем, что может обрабатывать одновременно сотни тысяч потоков, что обычно для графики, которая хорошо распараллеливается.
Разработка на CUDA
Модель программирования в CUDA предполагает группирование потоков. Потоки объединяются в блоки потоков (thread block) — одномерные или двумерные сетки потоков, взаимодействующих между собой при помощи разделяемой памяти и точек синхронизации. Программа (ядро, kernel) исполняется над сеткой (grid) блоков потоков (thread blocks), см. рисунок ниже. Одновременно исполняется одна сетка. Каждый блок может быть одно-, двух- или трехмерным по форме, и может состоять из 512 потоков на текущем аппаратном обеспечении.
Пример программы на CUDA
В качестве примера рассмотрим задачу умножения квадратной матрицы на вектор.
Исходные данные:
A[n][n] – матрица размерности n x n;
b[n] – вектор, состоящий из n элементов.
Результат:
c[n] – вектор из n элементов.
//Последовательный алгоритм умножения матрицы на вектор
Теперь рассмотрим решение этой задачи на видеокарте. Следующий код иллюстрирует пример вызова функции CUDA:
int Size = 1000;
// обычные массивы в оперативной памяти
float *h_a,*h_b,*h_c;
h_a = new float[Size*Size];
h_b = new float[Size];
h_c = new float[Size];
// указатели на массивы в видеопамяти
float *d_a,*d_b,*d_c;
// копирование из оперативной памяти в видеопамять
CUDA_SAFE_CALL(cudaMemcpy(d_a, h_a, sizeof(float)*Size*Size,
cudaMemcpyHostToDevice) );
CUDA_SAFE_CALL(cudaMemcpy(d_b, h_b, sizeof(float)*Size,
cudaMemcpyHostToDevice) );
// установка количества блоков
dim3 grid((Size+255)/256, 1, 1);
// установка количества потоков в блоке
dim3 threads(256, 1, 1);
// вызов функции
MatrVectMul>> (d_c, d_a, d_b,Size);
// копирование из видеопамяти в оперативную память
CUDA_SAFE_CALL(cudaMemcpy(h_c, d_c, sizeof(float)*Size,
cudaMemcpyDeviceToHost) );
// освобождение памяти
CUDA_SAFE_CALL(cudaFree(d_a));
CUDA_SAFE_CALL(cudaFree(d_b));
CUDA_SAFE_CALL(cudaFree(d_c));
- инициализация CUDA
- выделение видеопамяти для хранения данных программы
- копирование необходимых для работы функции данных из оперативной памяти в видеопамять
- вызов функции CUDA
- копирование возвращаемых данных из видеопамяти в оперативную
- освобождение видеопамяти
Пример функции, исполнимой на видеокарте
extern «C» __global__ void MatrVectMul(float *d_c, float *d_a, float *d_b, int Size)
- threadIdx.x – идентификатор потока в блоке по координате x,
- blockIdx.x – идентификатор блока в гриде по координате x,
- blockDim.x – количество потоков в одном блоке.
Пока же следует запомнить, что таким образом получается уникальный идентификатор потока (в данном случае i), который можно использовать в программе, работая со всеми потоками как с одномерным массивом.
Важно помнить, что функция, предназначенная для исполнения на видеокарте, не должна обращать к оперативной памяти. Подобное обращение приведет к ошибке. Если необходимо работать с каким-либо объектом в оперативной памяти, предварительно его надо скопировать в видеопамять, и обращаться из функции CUDA к этой копии.
Среди основных особенностей CUDA следует отметить отсутствие поддержки двойной точности (типа double). Также для функций CUDA установлено максимальное время исполнения, отсутствует рекурсия, нельзя объявить функцию с переменным числом аргументов.
Следует отметить следующее, что функция, работающая на видеокарте, должна выполняться не более 1 секунды. Иначе, функция будет не завершена, и программа завершится с ошибкой.
Для синхронизации потоков в блоке существует функция __syncthreads(), которая ждет, пока все запущенные потоки отработают до этой точки. Функция __syncthreads() необходима, когда данные, обрабатываемые одним потоком, затем используются другими потоками.
Замер времени в CUDA
Для измерения времени выполнения отдельных частей программы и анализа скорости выполнения того или иного участка кода удобно использовать встроенные в CUDA функции по замеру времени.
unsigned int timer;
… // код, время исполнения которого необходимо замерить
// остановка таймера
cutStopTimer( timer);
// получение времени
printf(«time: %f (ms)n», cutGetTimerValue(timer));
// удаление таймера
cutDeleteTimer( timer);
Создание проекта CUDA в VS 2005
После установки CUDA SDK, CUDA Toolkit и VS-интегратора в меню создания проекта появится новый пункт. Чтобы создать проект CUDA надо выбрать закладку Visual C++, CUDA, CUDAWinApp. Далее можно выбрать тип проекта (по умолчанию создается консольное) и другие параметры. В итоге будет создан проект, который выводит на экран строку «Hello CUDA!».
Чтобы приложение могло запуститься, в текущем каталоге должен присутствовать файл cutil32D.dll для работы в режиме Debug и файл cutil32.dll для работы в режиме Release (например, для проекта с названием lab4 необходимо положить эти файлы в директорию lab4/lab4). Причём, путь к файлу проекта не должен содержать русских букв.
Задания
Необходимо написать программу согласно варианту, при этом реализовать 2 функции: одну для выполнения на процессоре, вторую для выполнения на видеокарте. Затем сравнить результаты (возвращаемые значения) и скорость работы, задав большой размер матрицы (Size=1000). Массивы должны быть типа float.
Варианты заданий
- Нахождение скалярного произведения векторов (протестировать для Size=1000000);
- Нахождение суммы для каждой строки матрицы;
- Вычисление среднего арифметического строк (столбцов) матрицы;
- Умножение матрицы на множитель;
- Сложение матриц;
- Нахождение суммы квадратов элементов строк матрицы для всех строк матрицы;
- Вычисление матричного выражения A=B+k*C, где A, B, C — матрицы, k — скалярный множитель;
- Умножение матрицы на матрицу;
- Вычитание матриц;
- Транспонирование матрицы.
Контрольные вопросы
- Что подразумевается под терминами kernel, host и device?
- Объясните понятия grid, thread block, thread.
- На каких видеокартах доступна технология CUDA?
- Как вы считаете, будут ли перечисленные выше задания выполняться быстрее для Size=5 на видеокарте, чем на процессоре и почему?
- Что общего в технологиях OpenMP и Nvidia CUDA и чем они различаются (с точки зрения программирования)
Похожие материалы:
- ЛР №1 — Работа с Adobe Photoshop
- Организация вычислительных машин и систем
- Исходники программы на QT, похожей на базу данных 2
- Работа с Quartus II — пример создания и анализа простой схемы
- Пример синтеза процессора с минимальным набором команд + Задание на семестровую работу
Источник: fevt.ru
Шпоры
Что такое CUDA? Смотрите в википедии.
Рассмотрим коротко простейшие примеры использования этой интересной технологии. Этот пост является конспектом презентации Introduction to CUDA C автора Jason Sanders на конференции NVIDIA GTC 2010 — GPU Technology Conference . (Pre-Conference Tutorials, GTC 2010 — ID 2131).
Итак. Самая базовая терминология:
Host — CPU и его память (host memory);
Device — GPU и его память (device memory);
Начнем с “Hello, World!”. А куда ж без него?
Код делится на тот, что выполняется на CPU и тот что выполняется на GPU. Следующий самый просто пример, это программа, которая выводит приветствие миру и вызывает функцию из GPU, которая ничего не делает:
__global__ void kernel( void ) >
int main( void ) kernel>>();
printf( «Hello, World!n» );
return 0;
>
Ключевое слово __global__ в объявлении и определении функции обозначает, что функция выполняется на графической карте. И что эта функция вызывается из CPU. Неизменным аттрибутом такой функции являются скобки >> при вызове этой функции. Аргументы внутри этих скобок обсудим ниже. Компилятор nvcc разделяет исходный код на части, которые выполняются отдельно на хосте и девайсе.
Код, сгенерированный для таких функций как kernel из данного примера, выполняется на девайсе. Кстати, как я понял, device — это не обязательно GPU.
Рассмотрим более сложный пример. CPU передает некие величины в GPU для сложения их там.
__global__ void add( int *a, int *b, int *c ) *c = *a + *b;
>
int main( void ) int a, b, c; // host копии a, b, c
int *dev_a, *dev_b, *dev_c; // device копии of a, b, c
int size = sizeof( int );
//выделяем память для device копий для a, b, c
cudaMalloc( (void**)
cudaMalloc( (void**)
cudaMalloc( (void**)
a = 2;
b = 7;
// копируем ввод на device
cudaMemcpy( dev_a,
cudaMemcpy( dev_b,
// запускаем add() kernel на GPU, передавая параметры
add>>( dev_a, dev_b, dev_c );
// copy device result back to host copy of c
cudaMemcpy(
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
>
Базовый CUDA API для работы с памятью GPU (и девайса вообще):
cudaMalloc();
cudaFree();
cudaMemcpy();
Их Си аналоги: malloc(), free(), memcpy().
__global__ void add( int *a, int *b, int *c ) c[blockIdx.x] = a[blockIdx.x] + b[blockIdx.x];
>
#define N 512
int main( void ) int *a, *b, *c; // host копии a, b, c
int *dev_a, *dev_b, *dev_c; // device копии a, b, c
int size = N * sizeof( int );
//выделяем память для device копий a, b, c
cudaMalloc( (void**)
cudaMalloc( (void**)
cudaMalloc( (void**)
a = (int*)malloc( size );
b = (int*)malloc( size );
c = (int*)malloc( size );
random_ints( a, N );
random_ints( b, N );
// копируем ввод на device
cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );
// launch add() kernel with N parallel blocks
add>>( dev_a, dev_b, dev_c );
// копируем результат работы device обратно на host — копию c
cudaMemcpy( c, dev_c, size, cudaMemcpyDeviceToHost );
free( a ); free( b ); free( c );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
>
Главное отличие этого кода от предыдущих это аргумент N в вызове функции add >>. Он обозначает число так называемых блоков, на которые разбивается код на девайсе при распараллеливании. Обращаться к отдельному блоку можно через индекс массива блоков blockIdx.x (это встроенная переменная). Блоки, в свою очередь могут разбиваться на нити (threads)
Перепишем наш код для использования нитей, а не блоков (будем использовать не N блоков по одной нити, а один блок с N нитями):
__global__ void add( int *a, int *b, int *c ) c[ threadIdx.x ] = a[ threadIdx.x ] + b[ threadIdx.x ];
// blockIdx.x blockIdx.x blockIdx.x
>
#define N 512
int main( void ) int *a, *b, *c; //host копии a, b, c
int *dev_a, *dev_b, *dev_c; //device копии of a, b, c
int size = N * sizeof( int );
//выделяем память для копий a, b, c
cudaMalloc( (void**)
cudaMalloc( (void**)
cudaMalloc( (void**)
a = (int*)malloc( size );
b = (int*)malloc( size );
c = (int*)malloc( size );
random_ints( a, N );
random_ints( b, N );
// копируем ввод device
cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );
// запускаем на выполнение add() kernel с N нитями в блоке
add>>( dev_a, dev_b, dev_c );
// N, 1
// копируем результат работы device обратно на host (копия c)
cudaMemcpy( c, dev_c, size, cudaMemcpyDeviceToHost );
free( a ); free( b ); free( c );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
>
Теперь рассмотрим код, который использует и несколько блоков и несколько нитей в каждом блоке:
__global__ void add( int *a, int *b, int *c ) int index = threadIdx.x + blockIdx.x * blockDim.x;
c[index] = a[index] + b[index];
>
#define N (2048*2048)
#define THREADS_PER_BLOCK 512
int main( void ) int *a, *b, *c; // host копии a, b, c
int *dev_a, *dev_b, *dev_c; // device копии of a, b, c
int size = N * sizeof( int );
//выделяем память на device для of a, b, c
cudaMalloc( (void**)
cudaMalloc( (void**)
cudaMalloc( (void**)
a = (int*)malloc( size );
b = (int*)malloc( size );
c = (int*)malloc( size );
random_ints( a, N );
random_ints( b, N );
//копируем ввод на device
cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );
//запускаем на выполнение add() kernel с блоками и нитями
add>>( dev_a, dev_b, dev_c );
// копируем результат работы device на host ( копия c )
cudaMemcpy( c, dev_c, size, cudaMemcpyDeviceToHost );
free( a ); free( b ); free( c );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
>
Нити на первый взгляд кажутся избыточными. Параллельные нити, в отличие параллельных блоков, имеют механизмы для, так называемых, коммуникации и синхронизации. Давайте рассмотрим этот вопрос.
Возьмем в качестве примера на этот раз не сложение, а скалярное произведение.
Нити в одном блоке могут иметь общую память shared memory. Эта память декларируется в коде ключевым словом __shared__ . Эта память не видна в другом блоке.
Также нити можно «синхронизировать». То есть каждая нить в некой «точке синхронизации» будет ждать пока выполнение других нитей тоже не достигнет этой точки. Синхронизация нитей истинна только для нитей одного блока. Синхронизация вводиться функцией __syncthreads() .
Вот код, с синхронизацией и использованием shared memory:
//__global__ void dot( int *a, int *b, int *c ) // Каждая нить вычисляет одно слагаемое скалярного произведения
// int temp = a[threadIdx.x] * b[threadIdx.x];
//Не может вычислить сумму
//Каждая копия ‘temp’ приватна для своей нити
//>
__global__ void dot( int *a, int *b, int *c ) __shared__ int temp[N];
temp[threadIdx.x] = a[threadIdx.x] * b[threadIdx.x];
__syncthreads();
if( 0 == threadIdx.x ) int sum = 0;
for( int i = 0; i < N; i++ )
sum += temp[i];
*c = sum;
>
>
#define N 512
int main( void ) int *a, *b, *c;
int *dev_a, *dev_b, *dev_c;
int size = N * sizeof( int );
cudaMalloc( (void**)
cudaMalloc( (void**)
cudaMalloc( (void**)
a = (int *)malloc( size );
b = (int *)malloc( size );
c = (int *)malloc( sizeof( int ) );
random_ints( a, N );
random_ints( b, N );
// копируем ввод на device
cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );
//запускаем на выполнение dot() kernel с 1 блоком и N нитями
dot>>( dev_a, dev_b, dev_c );
//копируем результат работы device на host копией c
cudaMemcpy( c, dev_c, sizeof( int ) , cudaMemcpyDeviceToHost );
free( a ); free( b ); free( c );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
>
threadIdx.x используется для доступе к отдельной нити. threadIdx — это встроенная переменная. Для индексации вводимых/выводимых элементов используем выражение (threadIdx.x + blockIdx.x * blockDim.x). blockDim.x — это встроенная переменная, обозначающая число нитей в блоке.
В последнем примере использовался только один блок. А если нам надо использовать много блоков? Скалярное произведение же у нас тут только для примера.
__global__ void dot( int *a, int *b, int *c ) __shared__ int temp[THREADS_PER_BLOCK];
int index = threadIdx.x + blockIdx.x * blockDim.x;
temp[threadIdx.x] = a[index] * b[index];
__syncthreads();
if( 0 == threadIdx.x ) int sum = 0;
for( int i = 0; i < THREADS_PER_BLOCK; i++ )
sum += temp[i];
*c += sum;
>
>
В таком варианте функции dot у нас возникает race condition. Что это? Не тема этого поста. Смотрим википедию. С этой проблемой боремся с помощью атомарных функций.
В данном случае — это atomicAdd.
__global__ void dot( int *a, int *b, int *c ) __shared__ int temp[THREADS_PER_BLOCK];
int index = threadIdx.x + blockIdx.x * blockDim.x;
temp[threadIdx.x] = a[index] * b[index];
__syncthreads();
if( 0 == threadIdx.x ) int sum = 0;
for( int i = 0; i < THREADS_PER_BLOCK; i++ )
sum += temp[i];
atomicAdd( c , sum );
>
>
Функция main будет такая:
#define N (2048*2048)
#define THREADS_PER_BLOCK 512
int main( void ) int *a, *b, *c; // host копии a, b, c
int *dev_a, *dev_b, *dev_c; // device копии a, b, c
int size = N * sizeof( int );
//выделяем место в памяти для device копий a, b, c
cudaMalloc( (void**)
cudaMalloc( (void**)
cudaMalloc( (void**)
a = (int *)malloc( size );
b = (int *)malloc( size );
c = (int *)malloc( sizeof( int ) );
random_ints( a, N );
random_ints( b, N );
//копируем ввод на device
cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );
//запускаем на выполнение dot() kernel
dot>>( dev_a, dev_b, dev_c );
//копируем результат работы device обратно на host (копия c)
cudaMemcpy( c, dev_c, sizeof( int ) , cudaMemcpyDeviceToHost );
free( a ); free( b ); free( c );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
>
CUDA предоставляет атомарные функции, которые гарантируют «безопасность» read-modify-write операции.
Пример атомарных функций в CUDA:
atomicInc()
atomicAdd()
atomicDec()
atomicSub()
atomicExch()
atomicMin()
atomicCAS()
atomicMax()
С помощью этих примеров продемонстрированы приемы, которые можно использовать для применения в параллельных вычислениях на видеокартах NVIDIA.
Источник: sporgalka.blogspot.com
CUDA мы катимся: технология NVIDIA CUDA
Согласно Дарвинской теории эволюции, первая человекообразная обезьяна (если
быть точным – homo antecessor, человек-предшественник) превратилась впоследствии
в нас. Многотонные вычислительные центры с тысячью и больше радиоламп,
занимающие целые комнаты, сменились полукилограммовыми ноутами, которые, кстати,
не уступят в производительности первым. Допотопные печатные машинки превратились
в печатающие что угодно и на чем угодно (даже на теле человека)
многофункциональные устройства. Процессорные гиганты вдруг вздумали замуровать
графическое ядро в «камень». А видеокарты стали не только показывать картинку с
приемлемым FPS и качеством графики, но и производить всевозможные вычисления. Да
еще как производить! О технологии многопоточных вычислений средствами GPU,
NVIDIA CUDA и пойдет речь.
Почему GPU?
Интересно, почему всю вычислительную мощь решили переложить на графический
адаптер? Как видно, процессоры еще в моде, да и вряд ли уступят свое теплое
местечко. Но у GPU есть пара козырей в рукаве вместе с джокером, да и рукавов
хватает. Современный центральный процессор заточен под получение максимальной
производительности при обработке целочисленных данных и данных с плавающей
запятой, особо не заботясь при этом о параллельной обработке информации. В то же
время архитектура видеокарты позволяет быстро и без проблем «распараллелить»
обработку данных. С одной стороны, идет обсчет полигонов (за счет 3D-конвейера),
с другой – пиксельная обработка текстур. Видно, что происходит «слаженная
разбивка» нагрузки в ядре карты. Кроме того, работа памяти и видеопроцессора
оптимальнее, чем связка «ОЗУ-кэш-процессор». В тот момент, когда единица данных
в видеокарте начинает обрабатываться одним потоковым процессором GPU, другая
единица параллельно загружается в другой, и, в принципе, легко можно достичь
загруженности графического процессора, сравнимой с пропускной способностью шины,
однако для этого загрузка конвейеров должна осуществляться единообразно, без
всяких условных переходов и ветвлений. Центральный же процессор в силу своей
универсальности требует для своих процессорных нужд кэш, заполненный
информацией.
Ученые мужи задумались насчет работы GPU в параллельных вычислениях и
математике и вывели теорию, что многие научные расчеты во многом схожи с
обработкой 3D-графики. Многие эксперты считают, что основополагающим фактором в
развитии GPGPU (General Purpose computation on GPU – универсальные
расчеты средствами видеокарты) стало появление в 2003 году проекта Brook GPU.
Создателям проекта из Стэндфордского университета предстояло решить непростую
проблему: аппаратно и программно заставить графический адаптер производить
разноплановые вычисления. И у них это получилось. Используя универсальный язык C,
американские ученые заставили работать GPU как процессор, с поправкой на
параллельную обработку. После Brook появился целый ряд проектов по VGA-расчетам,
таких как библиотека Accelerator, библиотека Brahma, система
метапрограммирования GPU++ и другие.
CUDA!
Предчувствие перспективности разработки заставило AMD и NVIDIA
вцепиться в Brook GPU, как питбуль. Если опустить маркетинговую политику, то,
реализовав все правильно, можно закрепиться не только в графическом секторе
рынка, но и в вычислительном (посмотри на специальные вычислительные карты и
серверы Tesla с сотнями мультипроцессоров), потеснив привычные всем CPU.
Естественно, «повелители FPS» разошлись у камня преткновения каждый по своей
тропе, но основной принцип остался неизменным – производить вычисления
средствами GPU. И сейчас мы подробнее рассмотрим технологию «зеленых» – CUDA
(Compute Unified Device Architecture).
Работа нашей «героини» заключается в обеспечении API, причем сразу двух.
Первый – высокоуровневый, CUDA Runtime, представляет собой функции, которые
разбиваются на более простые уровни и передаются нижнему API – CUDA Driver. Так
что фраза «высокоуровневый» применима к процессу с натяжкой. Вся соль находится
именно в драйвере, и добыть ее помогут библиотеки, любезно созданные
разработчиками NVIDIA: CUBLAS (средства для математических расчетов) и
FFT (расчет посредством алгоритма Фурье). Ну что ж, перейдем к практической
части материала.
Терминология CUDA
NVIDIA оперирует весьма своеобразными определениями для CUDA API. Они
отличаются от определений, применяемых для работы с центральным процессором.
Поток (thread) – набор данных, который необходимо обработать (не
требует больших ресурсов при обработке).
Варп (warp) – группа из 32 потоков. Данные обрабатываются только
варпами, следовательно варп – это минимальный объем данных.
Блок (block) – совокупность потоков (от 64 до 512) или совокупность
варпов (от 2 до 16).
Сетка (grid) – это совокупность блоков. Такое разделение данных
применяется исключительно для повышения производительности. Так, если число
мультипроцессоров велико, то блоки будут выполняться параллельно. Если же с
картой не повезло (разработчики рекомендуют для сложных расчетов использовать
адаптер не ниже уровня GeForce 8800 GTS 320 Мб), то блоки данных обработаются
последовательно.
Также NVIDIA вводит такие понятия, как ядро (kernel), хост (host)
и девайс (device).
Работаем!
Для полноценной работы с CUDA нужно:
1. Знать строение шейдерных ядер GPU, так как суть программирования
заключается в равномерном распределении нагрузки между ними.
2. Уметь программировать в среде C, с учетом некоторых аспектов.
Разработчики NVIDIA раскрыли «внутренности» видеокарты несколько
иначе, чем мы привыкли видеть. Так что волей-неволей придется изучать все
тонкости архитектуры. Разберем строение «камня» G80 легендарной GeForce 8800
GTX.
Шейдерное ядро состоит из восьми TPC (Texture Processor Cluster) – кластеров
текстурных процессоров (так, у GeForce GTX 280 – 15 ядер, у 8800 GTS
их шесть, у 8600 – четыре и т.д.). Те, в свою очередь, состоят из двух
потоковых мультипроцессоров (streaming multiprocessor – далее SM). SM (их всего
16) состоит из front end (решает задачи чтения и декодирования инструкций) и
back end (конечный вывод инструкций) конвейеров, а также восьми scalar SP (shader
processor) и двумя SFU (суперфункциональные блоки). За каждый такт (единицу
времени) front end выбирает варп и обрабатывает его. Чтобы все потоки варпа
(напомню, их 32 штуки) обработались, требуется 32/8 = 4 такта в конце конвейера.
Каждый мультипроцессор обладает так называемой общей памятью (shared memory).
Ее размер составляет 16 килобайт и предоставляет программисту полную свободу
действий. Распределяй как хочешь :). Shared memory обеспечивает связь потоков в
одном блоке и не предназначена для работы с пиксельными шейдерами.
Также SM могут обращаться к GDDR. Для этого им «пришили» по 8 килобайт
кэш-памяти, хранящих все самое главное для работы (например, вычислительные
константы).
Мультипроцессор имеет 8192 регистра. Число активных блоков не может быть
больше восьми, а число варпов – не больше 768/32 = 24. Из этого видно, что G80
может обработать максимум 32*16*24 = 12288 потоков за единицу времени. Нельзя не
учитывать эти цифры при оптимизации программы в дальнейшем (на одной чашу весов
– размер блока, на другой – количество потоков). Баланс параметров может сыграть
важную роль в дальнейшем, поэтому NVIDIA рекомендует использовать блоки
со 128 или 256 потоками. Блок из 512 потоков неэффективен, так как обладает
повышенными задержками. Учитывая все тонкости строения GPU видеокарты плюс
неплохие навыки в программировании, можно создать весьма производительное
средство для параллельных вычислений. Кстати, о программировании.
Программирование
Для «творчества» вместе с CUDA требуется видеокарта GeForce не ниже
восьмой серии. С
официального сайта нужно скачать три программных пакета: драйвер с
поддержкой CUDA (для каждой ОС – свой), непосредственно пакет CUDA SDK (вторая
бета-версия) и дополнительные библиотеки (CUDA toolkit). Технология поддерживает
операционные системы Windows (XP и Vista), Linux и Mac OS X. Для изучения я
выбрал Vista Ultimate Edition x64 (забегая вперед, скажу, что система вела себя
просто превосходно). В момент написания этих строк актуальным для работы был
драйвер ForceWare 177.35. В качестве набора инструментов использовался
программный пакет Borland C++ 6 Builder (хотя подойдет любая среда, работающая с
языком C).
Человеку, знающему язык, будет легко освоиться в новой среде. Требуется лишь
запомнить основные параметры. Ключевое слово _global_ (ставится перед функцией)
показывает, что функция относится к kernel (ядру). Ее будет вызывать центральный
процессор, а вся работа произойдет на GPU. Вызов _global_ требует более
конкретных деталей, а именно размер сетки, размер блока и какое ядро будет
применено. Например, строчка _global_ void saxpy_parallel>>, где X –
размер сетки, а Y – размер блока, задает эти параметры.
Символ _device_ означает, что функцию вызовет графическое ядро, оно же
выполнит все инструкции. Эта функция располагается в памяти мультипроцессора,
следовательно, получить ее адрес невозможно. Префикс _host_ означает, что вызов
и обработка пройдут только при участии CPU. Надо учитывать, что _global_ и
_device_ не могут вызывать друг друга и не могут вызывать самих себя.
Также язык для CUDA имеет ряд функций для работы с видеопамятью: cudafree
(освобождение памяти между GDDR и RAM), cudamemcpy и cudamemcpy2D (копирование
памяти между GDDR и RAM) и cudamalloc (выделение памяти).
Все программные коды проходят компиляцию со стороны CUDA API. Сначала берется
код, предназначенный исключительно для центрального процессора, и подвергается
стандартной компиляции, а другой код, предназначенный для графического адаптера,
переписывается в промежуточный язык PTX (сильно напоминает ассемблер) для
выявления возможных ошибок. После всех этих «плясок» происходит окончательный
перевод (трансляция) команд в понятный для GPU/CPU язык.
Набор для изучения
Практически все аспекты программирования описаны в документации, идущей
вместе с драйвером и двумя приложениями, а также на сайте разработчиков. Размера
статьи не хватит, чтобы описать их (заинтересованный читатель должен приложить
малую толику стараний и изучить материал самостоятельно).
Специально для новичков разработан CUDA SDK Browser. Любой желающий может
ощутить силу параллельных вычислений на своей шкуре (лучшая проверка на
стабильность – работа примеров без артефактов и вылетов). Приложение имеет
большой ряд показательных мини-программок (61 «тест»). К каждому опыту имеется
подробная документация программного кода плюс PDF-файлы. Сразу видно, что люди,
присутствующие со своими творениями в браузере, занимаются серьезной работой.
Тут же можно сравнить скорости работы процессора и видеокарты при обработке
данных. Например, сканирование многомерных массивов видеокартой GeForce 8800
GT 512 Мб с блоком с 256 потоками производит за 0.17109 миллисекунды.
Технология не распознает SLI-тандемы, так что если у тебя дуэт или трио,
отключай функцию «спаривания» перед работой, иначе CUDA увидит только один
девайс. Двуядерный AMD Athlon 64 X2 (частота ядра 3000 МГц) тот же опыт
проходит за 2.761528 миллисекунды. Получается, что G92 более чем в 16 раз
быстрее «камня» AMD! Как видишь, далеко не экстремальная система в
тандеме с нелюбимой в массах операционной системой показывает неплохие
результаты.
Помимо браузера существует ряд полезных обществу программ. Adobe
адаптировала свои продукты к новой технологии. Теперь Photoshop CS4 в полной
мере использует ресурсы графических адаптеров (необходимо скачать специальный
плагин). Такими программами, как Badaboom media converter и RapiHD можно
произвести декодирование видео в формат MPEG-2. Для обработки звука неплохо
подойдет бесплатная утилита Accelero. Количество софта, заточенного под CUDA API,
несомненно, будет расти.
А в это время…
А пока ты читаешь сей материал, трудяги из процессорных концернов
разрабатывают свои технологии по внедрению GPU в CPU. Со стороны AMD все
понятно: у них есть большущий опыт, приобретенный вместе с ATI.
Творение «микродевайсеров», Fusion, будет состоять из нескольких ядер под
кодовым названием Bulldozer и видеочипа RV710 (Kong). Их взаимосвязь будет
осуществляться за счет улучшенной шины HyperTransport. В зависимости от
количества ядер и их частотных характеристик AMD планирует создать целую ценовую
иерархию «камней». Также планируется производить процессоры как для ноутбуков (Falcon),
так и для мультимедийных гаджетов (Bobcat). Причем именно применение технологии
в портативных устройствах будет первоначальной задачей для канадцев. С развитием
параллельных вычислений применение таких «камней» должно быть весьма популярно.
Intel немножко отстает по времени со своей Larrabee. Продукты AMD,
если ничего не случится, появятся на прилавках магазинов в конце 2009 – начале
2010 года. А решение противника выйдет на свет божий только почти через два
года.
Larrabee будет насчитывать большое количество (читай – сотни) ядер. Вначале
же выйдут продукты, рассчитанные на 8 – 64 ядера. Они очень сходны с Pentium, но
довольно сильно переработаны. Каждое ядро имеет 256 килобайт кэша второго уровня
(со временем его размер увеличится). Взаимосвязь будет осуществляться за счет
1024-битной двунаправленной кольцевой шины. Интел говорит, что их «дитя» будет
отлично работать с DirectX и Open GL API (для «яблочников»), поэтому никаких
программных вмешательств не потребуется.
А к чему я все это тебе поведал? Очевидно, что Larrabee и Fusion не вытеснят
обычные, стационарные процессоры с рынка, так же, как не вытеснят с рынка
видеокарты. Для геймеров и экстремалов пределом мечтаний по-прежнему останется
многоядерный CPU и тандем из нескольких топовых VGA. Но то, что даже
процессорные компании переходят на параллельные вычисления по принципам,
аналогичным GPGPU, говорит уже о многом. В частности о том, что такая
технология, как CUDA, имеет право на существование и, по всей видимости, будет
весьма популярна.
Небольшое резюме
Параллельные вычисления средствами видеокарты – всего лишь хороший инструмент
в руках трудолюбивого программиста. Вряд ли процессорам во главе с законом Мура
придет конец. Компании NVIDIA предстоит пройти еще длинный путь по
продвижению в массы своего API (то же можно сказать и о детище ATI/AMD).
Какой он будет, покажет будущее. Так что CUDA will be back :).
P.S. Начинающим программистам и заинтересовавшимся людям рекомендую посетить
следующие «виртуальные заведения»:
официальный сайт NVIDIA и сайт
GPGPU.com. Вся
предоставленная информация – на английском языке, но, спасибо хотя бы, что не на
китайском. Так что дерзай! Надеюсь, что автор хоть немного помог тебе в
захватывающих начинаниях познания CUDA!
Источник: xakep.ru