Современные
информационные технологии/Вычислительная техника и программирование
Мясіщев О.А., Рогоза Я.А.
Хмельницький національний
університет, Україна
Порівняння продуктивності обчислень лінійної алгебри на CPU та GPU.
Особливістю
розвитку сучасних обчислювальних систем є пошук різних шляхів збільшення
продуктивності обчислень не шляхом нарощування потужності окремих компонентів
системи, а використання технологій паралельної обробки інформації. Тактові
частоти сучасних процесорів настільки великі, що нарощувати їх далі не дозволяє
особливості технологічної конструкції мікропроцесорних пристроїв. Подальшим
кроком у розвитку мікропроцесорів була побудова пристроїв на основі декількох
обчислювальних ядер. Такий підхід дав змогу забезпечити фізичну паралельність
виконання задач, але теоретичного приросту у швидкодії він не дав, оскільки
продовжує використовуватися відносно повільна оперативна пам’ять для обміну і
синхронізації даних.
Найпоширенішою
сучасною задачею, яка потребує великої швидкості обчислень і обміну даними є
тривимірна графіка. Історично склалося так, що саме графіка є одним із основних
рушійних елементів для подальшого вдосконалення обчислювальних систем. Тому,
графічні системи розвиваються дещо швидше від інших комп’ютерних комплектуючих,
використовуючи у своїй основі найновіші технологічні рішення та досягнення.
Саме тому, компанією-виробником графічних адаптерів, фірмою NVIDIA, був
запропонований підхід, суть якого лежить у використанні графічних підсистем для
проведення різних обчислень, які раніше проводилися лише при використанні
центрального процесора і ОЗП.
Теоретично –
використанні графічних підсистем може дати значний приріст при проведенні
різних видів обчислень, оскільки графічний процесор використовує
модульно-багатоядерний принцип у своїй роботі, працюючи при цьому із власним
швидким оперативним запам’ятовуючим пристроєм. Тому, задачі, для яких
паралельність виконання дає змогу значно підвищити продуктивність роботи,
актуально вирішувати саме на графічних підсистемах. В свою чергу, потужним
графічним адаптером обладнаний майже кожен сучасний комп’ютер.
Технологія
CUDA — це середовище розробки компанії NVIDIA, яка дозволяє програмістам і
розробникам писати програмне забезпечення для вирішення складних обчислювальних
завдань за менший час завдяки багатоядерній обчислювальній потужності графічних
процесорів. Простіше кажучи, графічна підсистема комп'ютера з підтримкою CUDA
може бути використана, як обчислювальна.
Така операція
як перемноження матриць є складовою багатьох задач, які виконуються при різних
складних розрахунках. Наприклад, для вирішення лінійних рівнянь, для
організації руху полігонів при реалізації растрового зображення, для обчислення
характеристик взаємодії металу з формою при його плавці тощо. Тому, метою
подальшого дослідження є перевірка ефективності використання обчислень на
графічному адаптері для процесу перемноження двох квадратних матриць. Для цього
спочатку необхідно розглянути різницю між структурою та процесом обчислення на
CPU та GPU.
Робота GPU
полягає в прийнятті групи полігонів з однієї сторони й генерації групи пікселів
з іншої. Полігони й пікселі незалежні друг від друга, тому їх можна обробляти
паралельно. Таким чином, в GPU можна виділити велику частину кристала на
обчислювальні блоки, які, на відміну від CPU, будуть реально використовуватися.
GPU
відрізняється від CPU не тільки цим. Доступ до пам'яті в GPU дуже зв'язаний -
якщо зчитується тексель, то через кілька тактів буде зчитуватися сусідній
тексель; коли записується піксель, те через кілька тактів буде записуватися
сусідній. Розумно організовуючи пам'ять, можна одержати продуктивність, близьку
до теоретичної пропускної здатності. Це означає, що GPU, на відміну від CPU, не
потрібно величезного кешу, оскільки його роль полягає в прискоренні операцій текстурування.
Усе, що потрібно, це декілька кілобайт, що містять декілька текселів,
використовуваних у білінійних і трилінійних фільтрах.
До основних
елементів, з якими працює GPU, відносяться наступні:
Потік
(stream) являє собою потік елементів одного типу, в GPU він може бути
представлений текстурою. В принципі, у класичному програмуванні є такий аналог,
як масив.
Ядро (kernel)
- функція, яка буде застосовуватися незалежно до кожного елемента потоку; є
еквівалентом піксельного шейдера. У класичному програмуванні можна привести
аналогію циклу - він застосовується до великої кількості елементів.
Щоб зчитувати
результати застосування ядра до потоку, повинна бути створена текстура. На CPU
еквівалента немає, оскільки там є повний доступ до пам'яті.
Керування місцем
розташування в пам'яті, куди буде проводитися запис (в операціях
розкиду/scatter), здійснюється через вершинний шейдер, оскільки піксельний
шейдер не може змінювати координати оброблюваного пікселя.
Рис 1.
Взаємодія модулів з пам’яттю
Ядро шейдерів
відеоадаптера складається з декількох кластерів текстурних мультипроцесорів
(Texture Processor Cluster, TPC) . Кожен мультипроцесор має певний набір
ресурсів. Є невелика область пам'яті під назвою "Загальна пам'ять/Shared
Memory", по 16 кбайт на мультипроцесор. Дана область пам'яті відкриває
можливість обміну інформацією між потоками в одному блоці. Важливо підкреслити
це обмеження: усі потоки в блоці гарантовано виконуються одним
мультипроцесором. Загальна пам'ять - не єдина, до якої можуть звертатися мультипроцесори.
Вони можуть використовувати відеопам'ять, але з меншою пропускною здатністю й
більшими затримками. Тому, щоб знизити частоту звертання до цієї пам'яті, у
мультипроцесорах є кеш (приблизно 8 кбайт на мультипроцесор), що зберігає
константи й текстури.
Для
тестування продуктивності перемноження матриць на CPU було написано три
програми. У всіх програмах відбувалося перемноження двох квадратних матриць
розміром 1000x1000, 1500x1500, 2000х2000. Програми, що використовувалися для
обчислення на CPU, були побудовані на основі технології OpenMP для забезпечення
паралельного виконання операцій. Програма для обчислення на GPU була побудована
з розмірами блоку у 32 потоки. В якості платформи для проведення обчислень була
обрана система з процесором AMD Phenom X3 720 (3 ядра по 2.8 GHz),
відеоадаптером Nvidia GeForce 9600 GT, операційною системою Windows 7 x32
Ultimate Edition. В якості компілятора використовувалась система Microsoft
Visual Studio Express 2008 + CUDA Libraries and Runtimes. Усі обчислення
проводилися з FP числами одинарної точності.
Вихідний код програми для обчислення на GPU
#include
<stdio.h>
#define
BLOCK_SIZE 32
#define
N
__global__
void matMult ( float * a, float * b, int n, float * c )
{
int bx = blockIdx.x;
int by = blockIdx.y;
int tx = threadIdx.x;
int ty = threadIdx.y;
int aBegin = n * BLOCK_SIZE * by;
int aEnd = aBegin + n - 1;
int aStep = BLOCK_SIZE;
int bBegin = BLOCK_SIZE * bx;
int bStep = BLOCK_SIZE * n;
float sum = 0.0f;
for ( int ia = aBegin, ib = bBegin; ia
<= aEnd; ia += aStep, ib += bStep )
{
__shared__ float as
[BLOCK_SIZE][BLOCK_SIZE];
__shared__ float bs
[BLOCK_SIZE][BLOCK_SIZE];
as [ty][tx] = a [ia + n * ty + tx];
bs [ty][tx] = b [ib + n * ty +
tx];
__syncthreads();
for ( int k = 0; k < BLOCK_SIZE;
k++ )
sum += as [ty][k] * bs
[k][tx];
__syncthreads();
}
int ic = n * BLOCK_SIZE * by + BLOCK_SIZE
* bx;
c [ic + n * ty + tx] = sum;
}
int main (
int argc, char * argv [] )
{
int numBytes = N * N * sizeof ( float );
float * a = new float [N*N];
float * b = new float [N*N];
float * c = new float [N*N];
for ( int i = 0; i < N; i++ )
for ( int j = 0; j < N; j++ )
{
a [i] = 6.0f;
b [i] = 11.0f;
}
float * adev = NULL;
float * bdev = NULL;
float * cdev = NULL;
cudaMalloc ( (void**)&adev, numBytes
);
cudaMalloc ( (void**)&bdev, numBytes
);
cudaMalloc ( (void**)&cdev, numBytes
);
dim3 threads ( BLOCK_SIZE, BLOCK_SIZE );
dim3 blocks ( N / threads.x, N / threads.y);
cudaEvent_t start, stop;
float gpuTime = 0.0f;
cudaEventCreate ( &start );
cudaEventCreate ( &stop );
cudaEventRecord ( start, 0 );
cudaMemcpy ( adev, a, numBytes, cudaMemcpyHostToDevice );
cudaMemcpy ( bdev, b, numBytes, cudaMemcpyHostToDevice );
matMult<<<blocks,
threads>>> ( adev, bdev, N, cdev );
cudaMemcpy ( c, cdev, numBytes, cudaMemcpyDeviceToHost );
cudaEventRecord ( stop, 0 );
cudaEventSynchronize ( stop );
cudaEventElapsedTime ( &gpuTime,
start, stop );
printf("time spent executing by the
GPU: %.2f millseconds\n", gpuTime );
cudaEventDestroy ( start );
cudaEventDestroy ( stop );
cudaFree ( adev );
cudaFree ( bdev );
cudaFree ( cdev );
delete a;
delete b;
delete c;
getchar();
return 0;
}
Результати
проведення обчислень приведені нижче у таблиці.
Розмір
матриці |
Час
розрахунку (сек) /продуктивність (mf) |
||
CPU одне
ядро |
CPU три
ядра |
GPU 32
блоки |
|
1000x1000 |
3,98/500 |
1,37/1459 |
0,120/16658 |
1500x1500 |
14,151/476 |
4,828/1397 |
0,217/31095 |
2000x2000 |
29,72/538 |
11,24/1423 |
0,290/55158 |
Значне
прискорення (майже в 11 раз), яке було отримане порівняно з обчисленням на
трьох ядрах CPU виникло у випадку перемноження матриць тому що, дві матриці
розкладались на вектори і операції множення виконувались паралельно над блоками
у 32 потоки. Це надає можливість не витрачати багато часу на обмін даними з
пам’яттю, порівняно з CPU. Незначне зростання часу, витраченого на обчислення
матриць різних розмірів зумовлене тим, що у одному мультипроцесорі можуть
оброблюватись одночасно декілька блоків, тому збільшення розмірів матриць не
зумовлювало ситуацію черги блоків до мультипроцесорів і обчислення все одно
виконувались паралельно над усіма частинами матриці. Оптимізація паралельних
обчислень на GPU можлива шляхом
знаходження оптимальної кількості потоків для конкретної задачі.
Висновки
1.
При виконанні великої кількості однотипних обчислень, доцільно
використовувати обчислення на GPU.
2. На GPU не доцільно проводити
обчислення, в яких є необхідність постійного обміну даними при здійсненні
частих операцій. В останньому випадку можна навпаки: отримати меншу
продуктивність при обчисленні на GPU порівняно з CPU,
оскільки можливий варіант виконання операцій на різних мультипроцесорах, обмін
даними між якими значно складніший, ніж використання загального доступу до
пам’яті.
Література
1.
CUDA C Programming Guide (Nvidia Corporation www.nvidia.com/object/cuda_get.html) - 2009
2.
Joel Yliluoma. Guide into OpenMP: Easy multithreading programming for C++ (http://bisqwit.iki.fi/story/howto/openmp/)
– 2008