У цій замітці зібрана інформація яка допоможе зрозуміти загальні принципи GPU-програмування.
Введення в архітектуру GPU
Розділяють два види пристроїв - то яке управляє загальною логікою - host. і то яке вміє швидко виконати певний набір інструкцій над великим об'ємом даних - device.
Програмування для GPU
Програми пишуться на розширенні мови Сі від NVidia / OpenCL і компілюються за допомогою спеціальних компіляторів входять в SDK. У кожного виробника зрозуміло свій. Є два варіанти складання - під цільову платформу - коли явно вказується на якому залозі буде здійсняться код або в певний проміжний код, який при запуску на цільовому залозі буде перетворений драйвером в набір конкретних інструкцій для використовуваної архітектури (з поправкою на обчислювальні можливості заліза).
Виконувана на GPU програма називається ядром - kernel - що для CUDA що для OpenCL це і буде той набір інструкцій які застосовуються до всіх даних. Функція одна, а дані на яких вона виконується - різні - принцип SIMD.Драйвер CUDA / OpenCL розбиває вхідні дані на безліч частин (потоки виконання об'єднані в блоки) і призначає для виконання на кожен потоковий процесор. Програміст може і повинен вказувати драйверу як максимально ефективно задіяти існуючі обчислювальні ресурси, задаючи розміри блоків і число потоків в них. Зрозуміло, максимально допустимі значення варіюються від пристрою до пристрою. Хороша практика - перед виконанням запросити параметри заліза, на якому буде виконуватися ядро і на їх підставі обчислити оптимальні розміри блоків.
Схематично, розподіл завдань на GPU відбувається так:
Виконання програми на GPU
work-item (OpenCL) або thread (CUDA) - ядро і набір даних, виконується на Stream Processor (Processing Element в разі ATI пристроїв).
work group (OpenCL) або thread block (CUDA) виконується на Multi Processor (SIMD Engine)
Grid (набір блоків таке поняття є тільки у НВідіа) = виконується на цілому пристрої - GPU. Для виконання на GPU все потоки об'єднуються в варпа (warp - CUDA) або вейффронти (wavefront - OpenCL) - пул потоків, призначених на виконання на одному окремому мультипроцесорі. Тобто якщо число блоків або робочих груп виявилося більше ніж число мултіпроцессоров - фактично, в кожен момент часу виконується група (або групи) об'єднані в варп - всі інші чекають своєї черги.
Одне ядро може виконуватися на кількох GPU пристроях (як для CUDA так і для OpenCL, як для карток ATI так і для NVidia).
Одне GPU-пристрій може одночасно виконувати кілька ядер (як для CUDA так і для OpenCL, для NVidia - починаючи з архітектури 20 і вище). Посилання по даних питаннях см. В кінці статті.
Модель пам'яті OpenCL (в дужках - термінологія CUDA)
Тут головне запам'ятати про час доступу до кожного виду пам'яті. Найповільніший це глобальна пам'ять - у сучасних відекарти її аж до 6 Гб. Далі по швидкості йде колективна пам'ять (shared - CUDA, local - OpenCL) - загальна для всіх потоків в блоці (thread block - CUDA, work-group - OpenCL) - проте її завжди мало - 32-48 Кб для мультипроцессора. Найшвидшим є локальна пам'ять за рахунок використання регістрів і кешування, але треба розуміти що все що ні вмістилося в кеши \ регістри - буде зберігається в глобальній пам'яті з усіма наслідками, що випливають.
Патерни паралельного програмування для GPU
Map - GPU parallel pattern
Тут все просто - беремо вхідний масив даних і до кожного елементу застосовуємо якийсь оператор - ядро - ніяк що не зачіпає інші елементи - тобто читаємо і пишемо в певні осередки пам'яті.
Ставлення - як один до одного (one-to-one).
приклад - множення матриць, оператор інкремента або декремента застосований до кожного елементу матриці і т.п.
Scatter - GPU parallel pattern
Для кожного елемента вхідного масиву ми обчислюємо позицію в вихідному масиві, на яке він вплине (шляхом застосування відповідного оператора).
Ставлення - як один до багатьох (one-to-many).
3. Transpose
Transpose - GPU parallel pattern
Даний патерн можна розглядати як окремий випадок паттерна scatter.
Використовується для оптимізації обчислень - перерозподіляючи елементи в пам'яті можна досягти значного підвищення продуктивності.
Gather - GPU parallel pattern
Є зворотним до паттерну Scatter - для кожного елемента у вихідному масиві ми обчислюємо індекси елементів з вхідного масиву, які нададуть на нього вплив:
Ставлення - кілька до одного (many-to-one).
Stencil - GPU parallel pattern
Даний патерн можна розглядати як окремий випадок паттерна gather. Тут для отримання значення в кожному осередку вихідного масиву використовується певний шаблон для обчислення всіх елементів вхідного масиву, які вплинуть на фінальне значення. Всілякі фільтри побудовані саме за цим принципом.
Ставлення кілька до одного (several-to-one)
Приклад: фільтр Гауссіана.
Reduce - GPU parallel pattern
Ставлення все до одного (All-to-one)
Приклад - обчислення суми або максимуму в масиві.
При обчисленні значення в кожному осередку вихідного масиву необхідно враховувати значення кожного елемента вхідного. Існує дві основні реалізації - Hillis and Steele і Blelloch.
out [i] = F [i] = operator (F [i-1], in [i])
Ставлення все до всіх (all-to-all).
Приклади - сортування даних.
Корисні посилання
Введення в CUDA: