Steps3D - Tutorials - Основи CUDA

Вступ, GPGPU

CUDA (Compute Unified Device Architecture) - це технологія від компанії NVidia, призначена для розробки програм для масивно-паралельних обчислювальних пристроїв (насамперед для GPU починаючи з серії G80).

Основними плюсами CUDA є її безкоштовність (SDK для всіх основних платформ вільно скачується з developer.nvidia.com), простота (програмування ведеться на "розширеному С") та гнучкість.

Фактично CUDA є подальшим розвитком GPGPU (General Purpose computation on GPU). Справа в тому, що вже з самого початку GPU активно використовували паралельність (як вершини, так і окремі фрагменти можуть оброблятися паралельно та незалежно один від одного, тобто дуже добре лягають на паралельну архітектуру).

У міру розвитку GPU зростала як ступінь розпаралелювання, так і гнучкість самих GPU. Найперші GPU для PC (Voodoo) фактично були просто розтеризатором з можливістю накладання текстури і буфером глибини. Досить швидко з'явилися GPU з T&L, тобто. повною обробкою вершин на самому GPU - на вхід надходять тривимірні дані та на виході отримуємо готове зображення (наприклад, Riva TNT). Однак гнучкість у них була невелика - адже всі обчислення велися в рамках фіксованого конвеєра (FFP).

Наступним кроком (GeForce 2) стало поява вершинних шейдерів (розширення ARB_vertex_program) - обробку вершин стало можливим задавати у вигляді програми, написаної на спеціальному асемблері. При цьому вершини оброблялися паралельно та незалежно одна від одної. Нижче наводиться приклад простої програми на такому асемблері.

Цілком логічним наступним кроком стала поява фрагментних програм (розширення ARB_fragment_program), що дозволяєзадавати розрахунок кожного пікселя також за допомогою програми на асемблері. Важливим моментом є те, що всі ці обчислення (як для вершин, так і для фрагментів) ведуться з використанням 32-бітовихfloating-pointчисел.

В архітектурі GPU з'явилися окремі вершинні та фрагментні процесори, які виконують відповідні програми. Дані процесори спочатку були вкрай прості - можна було виконувати лише найпростіші операції, практично повністю було розгалуження і всі процесори одного типу одночасно виконували ту саму команду (класична SIMD-архітектура).

За рахунок великої кількості вершинних і фрагментних процесорів, що виконують такі програми, виявилося, що за швидкодією (вимірюваної в кількості floating-point операцій в секунду) GPU в рази обганяють CPU.

Заключним кроком, який перетворив GPU на потужні паралельні обчислювачі, стало підтримкаfloating-pointтекстур, тобто. стало можливим зберігати значення в текстурах як 32-бітовіfloating-pointчисла.

В результаті GPU фактично стало пристроєм, що реалізує потокову обчислювальну модель (stream computing model) - є потоки вхідних та вихідних даних, що складаються з однакових елементів, які можуть бути оброблені незалежно один від одного. Обробка елементів здійснюється ядром (kernel) (див. рис 1.).

Рис 1. Потокові обчислення.

Фактично GPU виявилося потужним SIMD (Single Instruction Multiple Data) процесором. В результаті з'явилося GPGPU – використання величезної обчислювальної потужності GPU для вирішення неграфічних завдань. Незважаючи на значні результати GPGPU мало ряд недоліків:

  • вся робота йшла через графічний API, код для GPU писався на GLSL/HLSL/Cg, решта коду - на традиційномумовою програмування
  • наявність обмежень на розміри та розмірність текстур
  • повністю була відсутня можливість взаємодії між паралельно оброблюваними пікселями
  • відсутня підтримка так званогоscatter'а (хоча було знайдено обхідні шляхи)

Крім того, на GPU GeForce 6xxx/7xxx була відсутня нативна підтримка цілих чисел і побітових операцій над ними.

Поява CUDA (а також GPU G80) повністю зняла всі ці обмеження, запропонувавши для GPGPU просту та зручну модель. У цій моделі GPU розглядається як спеціалізований обчислювальний пристрій (званіdevice), який:

  • є співпроцесором до CPU (host)
  • має власну пам'ять (DRAM)
  • має можливість паралельного виконання величезної кількості окремих ниток (threads)

При цьому між нитками на CPU та нитками на GPU є принципові відмінності -

  • нитки на GPU мають вкрай "невелику вартість" - їх створення та управління вимагає мінімальних ресурсів (на відміну від CPU)
  • для ефективної утилізації можливостей GPU потрібно використовувати багато тисяч окремих ниток (для CPU зазвичай потрібно не більше 10-20 ниток)

Самі програми пишуться на "розширеному" С, при цьому їхня паралельна частина (ядра) виконується на GPU, а звичайна частина - на CPU. CUDA автоматично здійснює поділ частин і управління їх запуском.

CUDA використовує велику кількість окремих ниток для обчислень, часто кожному обчислюваному елементами відповідає одна нитка. Усі нитки групуються в ієрархію -grid/block/thread(див. рис. 2).

Рис 2. Ієрархія ниток у CUDA.

Верхній рівень -grid- відповідає ядру та поєднує всі ниткивиконують це ядро.gridявляє собою одновимірний або двовимірний масив блоків (block). Кожен блок (block) являє собою один/двох/тривимірний масив ниток (threads).

При цьому кожен блок являє собою повністю незалежний набір ниток, що взаємодіють між собою, нитки з різних блоків не можуть між собою взаємодіяти.

Фактично блок відповідає незалежно вирішуваній підзадачі, наприклад якщо потрібно знайти добуток двох матриць, то матрицю-результат можна розбити на окремі підматриці однакового розміру. Знаходження кожної такої підматриці може відбуватися абсолютно незалежно від знаходження інших підматриць. Знаходження такої підматриці – завдання окремого блоку, всередині блоку кожному елементу підматриці відповідає окрема нитка.

При цьому нитки всередині блоку можуть взаємодіяти між собою (тобто спільно вирішувати підзавдання) через

  • загальну пам'ять (shared memory)
  • функцію синхронізації всіх ниток блоку (__synchronize)

Подібна ієрархія досить природна - з одного боку хочеться мати можливість взаємодії між окремими нитками, а з іншого - чим більше таких ниток, тим вища ціна подібної взаємодії.

Тому вихідне завдання (застосування ядра до вхідних даних) розбивається ряд підзадач, кожна з яких вирішується абсолютно незалежно (тобто. ніякої взаємодії між підзадачами немає) і в довільному порядку.

Сама ж підзавдання вирішується за допомогою набору ниток, що взаємодіють між собою.

З апаратної точки зору всі нитки розбиваються на так звані warp 'и - блоки ниток, що підряд ідуть, які одночасно (фізично) виконуються і можуть взаємодіяти один з одним. Коженблок ниток розбивається на кількаwarp'ів, розмірwarp'а для всіх існуючих зараз GPU дорівнює 32.

Важливим моментом є те, що нитки фактично виконують ту саму команду, але кожна зі своїми даними. Тому якщо всерединіwarp'а відбувається розгалуження (наприклад в результаті виконання оператораif), то всі ниткиwarp'а виконують всі гілки, що при цьому виникають. Тому вкрай бажано зменшити розгалуження в межах кожного окремого warp а.

Також використовується поняттяhalf-warp'а - це перша або друга половинаwarp'а. Подібне розбиттяwarp'а на половини пов'язане з тим, що зазвичай звернення до пам'яті робляться окремо для кожногоhalf-warp'а.

Крім ієрархії ниток, існує також кілька різних типів пам'яті. Швидкодія програми дуже залежить від швидкості роботи з пам'яттю. Саме тому в традиційних CPU більшу частину кристала займають різні кеші, призначені для прискорення роботи з пам'яттю (тоді як для GPU основну частину кристала займають ALU).

У CUDA для GPU існує кілька різних типів пам'яті, доступних ниткам, які сильно різняться між собою (див. табл. 1).