Меню Закрыть

Занятие 6. Программирование на CUDA C. Часть 2.

image_pdfimage_print

Скалярное произведение с использованием shared-памяти

Треды на первый взгляд кажутся избыточными. Параллельные треды, в отличие параллельных блоков, имеют механизмы для, так называемых, коммуникации и синхронизации. Давайте рассмотрим этот вопрос.
Возьмем в качестве примера на этот раз не сложение, а скалярное произведение.

Треды в одном блоке могут иметь общую память 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**)&dev_a, size );
cudaMalloc( (void**)&dev_b, size );
cudaMalloc( (void**)&dev_c, sizeof( int ) );
 
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<<< 1, N >>>( 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 — это встроенная переменная, обозначающая число тредов в блоке.

Сложение двух векторов

При создании любого проекта на CUDA в Visual Studio происходит генерация кода, который демонстрирует сложение двух векторов. Это очень полезный и показательный пример работы с CUDA. Он будет представлен в следующем разделе, а пока давайте создадим собственную программу с нуля.

Необходимые библиотеки:

#include <cstdlib>
#include <iostream>
#include <cuda_runtime.h>

<cuda_runtime.h> является библиотекой от расширения CUDA C. Она нужна для того, чтобы можно было получать встроенные модули (например, типы данных) и была возможность использования API CUDA во время выполнения компиляции с хоста. Если вы будете использовать компилятор CUDA nvcc, то прописывать данную библиотеку необязательно, так как этот заголовок подключается автоматически (помимо cuda_runtime.h, это также касается cuda_runtime_api.h и cuda.h).

Следующим шагом будет добавление спецификатора __global__, его функция с аргументами. Среди аргументов обозначим 2 вектора const float * a и const float * b, результат float * result и счетчик int n:

__global__ void vecAdd_kernel(
const float * a, const float * b, float * result, int n)

Далее введем новую переменную целочисленного типа i со значением blockIdx.x * blockDim.x + threadIdx.x, обозначающую размерность блоков и грида. В данном случае одномерная модель блоков и грида. В таком случае номер треда будет определяться как номер блока, умноженный на количество тредов в блоке, плюс номер треда внутри блока.

Дальше идет сравнение переменной i со счетчиком и запись результата сложения.

if (i < n)
result[i] = a[i] + b[i];

После чего в главной функции программы задаем нашему счетчику значение 100, также в главной функции выделяем память под каждую переменную, включая полученный результат.

int n = 100;
float * a = new float[n], * a_gpu;

cudaMalloc((void**)&a_gpu, n * sizeof(float));
float * b = new float[n], * b_gpu;
cudaMalloc((void**)&b_gpu, n * sizeof(float));
float * result = new float[n], * result_gpu;
cudaMalloc((void**)&result_gpu, n * sizeof(float));

Добавляем цикл со счетчиком for с условиями i< n; i++, а в теле цикла будет сравниваться a и b с i при этом i будет увеличиваться на единицу (i++).

Далее происходит копирование данных из памяти CPU в память GPU с помощью функции cudaMemcpy. В аргументах указываем переменную с GPU и CPU, выделяемый размер и команду cudaMemcpyHostToDevice:

cudaMemcpy(a_gpu, a, n * sizeof(float), cudaMemcpyHostToDevice);

cudaMemcpy(b_gpu, b, n * sizeof(float), cudaMemcpyHostToDevice);

Далее обозначаем размер блоков const int block_size = 256 и считаем количество блоков, которое потом будет вызываться в ядре:

int num_blocks = (n + block_size - 1) / block_size

Вызываем ядро, записываем результат и снова через cudaMemcpy передаем его уже с GPU на CPU (cudaMemcpyDeviceToHost):

vecAdd_kernel <<< num_blocks, block_size >>> (a_gpu, b_gpu, result_gpu, n);
cudaMemcpy(result, result_gpu,
n * sizeof(float), cudaMemcpyDeviceToHost);

Снова создаем цикл, который в дальнейшем напечатает результаты сложения векторов, которых будет равно количеству x:

for (int x = 0; x < 10; x++)
cout<<result[x]<<endl;

Уничтожаем (освобождаем) массивы a, b, result на CPU и на GPU с помощью команды cudaFree:

delete [] a;
delete [] b;
delete [] result;

cudaFree(a_gpu);
cudaFree(b_gpu);
cudaFree(result_gpu);

Таким образом, была реализована программа, которая складывает 2 вектора и показывает на экране результат их сложения. Листинг всей программы приведен ниже:

#include <cstdlib>
#include <iostream>
#include <cuda_runtime.h>
 
using namespace std;
 
__global__ void vecAdd_kernel(
const float * a, const float * b, float * result, int n) {
 
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n)
result[i] = a[i] + b[i];
}
 
int main() {
int n = 100;
float * a = new float[n], * a_gpu;
cudaMalloc((void**)&a_gpu, n * sizeof(float));
 
float * b = new float[n], * b_gpu;
cudaMalloc((void**)&b_gpu, n * sizeof(float));
 
float * result = new float[n], * result_gpu;
cudaMalloc((void**)&result_gpu, n * sizeof(float));
 
for (int i = 0; i < n; i++)
a[i] = b[i] = i;
 
cudaMemcpy(a_gpu, a, n * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(b_gpu, b, n * sizeof(float), cudaMemcpyHostToDevice);
const int block_size = 256;
int num_blocks = (n + block_size - 1) / block_size;
 
vecAdd_kernel <<< num_blocks, block_size >>> (a_gpu, b_gpu, result_gpu, n);
 
cudaMemcpy(result, result_gpu,
n * sizeof(float), cudaMemcpyDeviceToHost);
 
for (int x = 0; x < 10; x++)
cout<<result[x]<<endl;
 
delete [] a;
delete [] b;
delete [] result;
 
cudaFree(a_gpu);
cudaFree(b_gpu);
cudaFree(result_gpu);
 
return 0;
 
}

А результат выполнения программы вы можете увидеть на рисунке:

Связанные записи