Контакти

Використання картки для обчислень. Обчислення на GPU. Як вибрати відеокарту для майнінгу

Використання GPU для обчислень за допомогою C++ AMP

Досі в обговоренні прийомів паралельного програмування ми розглядали лише ядра процесора. Ми придбали деякі навички розпаралелювання програм із кількох процесорів, синхронізації доступу до спільно використовуваних ресурсів та використання високошвидкісних примітивів синхронізації без застосування блокувань.

Однак, існує ще один спосіб розпаралелювання програм - графічні процесори (GPU), що мають більшу кількість ядер, ніж навіть високопродуктивні процесори. Ядра графічних процесорів чудово підходять для реалізації паралельних алгоритмів обробки даних, а велика їх кількість з лишком окупає незручності виконання програм на них. У цій статті ми познайомимося з одним із способів виконання програм на графічному процесорі з використанням комплекту розширень мови C++ під назвою C++ AMP.

Розширення C++ AMP засновані мовою C++ і тому у даній статті будуть демонструватися приклади мовою C++. Однак, при помірному використанні механізму взаємодій. NET, ви зможете використовувати алгоритми C++ AMP у своїх програмах для .NET. Але про це ми поговоримо наприкінці статті.

Вступ до C++ AMP

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

    Сучасні графічні процесори мають дуже маленький набір інструкцій. Це передбачає деякі обмеження: відсутність можливості виклику функцій, обмежений набір типів даних, що підтримуються, відсутність бібліотечних функцій та інші. Деякі операції, такі як умовні переходи, можуть коштувати значно дорожче, ніж аналогічні операції, які виконуються на звичайних процесорах. Очевидно, що перенесення великих обсягів коду з процесора на графічний процесор за таких умов потребує значних зусиль.

    Кількість ядер у середньому графічному процесор значно більша, ніж у середньому звичайному процесорі. Однак деякі завдання виявляються занадто маленькими або не дозволяють розбивати себе достатньо велика кількістьчастин, щоб можна було отримати вигоду від застосування графічного процесора.

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

Відразу постає питання, які завдання підходять для вирішення на графічному процесорі? Майте на увазі, що не всякий алгоритм підходить для виконання на графічному процесорі. Наприклад, графічні процесори не мають доступу до пристроїв введення/виводу, тому у вас не вдасться підвищити продуктивність програми, яка витягує стрічки RSS з інтернету, за рахунок використання графічного процесора. Однак на графічний процесор можна перенести багато обчислювальних алгоритмів і забезпечити масове їх розпаралелювання. Нижче наводиться кілька прикладів таких алгоритмів (цей список не повний):

    збільшення та зменшення різкості зображень та інші перетворення;

    швидке перетворення Фур'є;

    транспонування та множення матриць;

    сортування чисел;

    інверсія хеша "в лоб".

Відмінним джерелом додаткових прикладівможе служити блог Microsoft Native Concurrency, де наводяться фрагменти коду та пояснення до них для різних алгоритмів, реалізованих на C++ AMP.

C++ AMP – це фреймворк, що входить до складу Visual Studio 2012, що дає розробникам на C++ простий спосіб виконання обчислень на графічному процесорі і вимагає наявності драйвера DirectX 11. Корпорація Microsoft випустила C++ AMP як відкриту специфікацію , яку може реалізувати будь-який виробник компіляторів.

Фреймворк C++ AMP дозволяє виконувати код на графічних прискорювачів (accelerators), що є обчислювальними пристроями. За допомогою драйвера DirectX 11 фреймворк C++ AMP динамічно виявляє усі прискорювачі. До складу C++ AMP входять також програмний емулятор прискорювача і емулятор на базі звичайного процесора, WARP, які є запасним варіантом в системах без графічного процесора або з графічним процесором, але без драйвера DirectX 11, і використовує кілька ядер та інструкції SIMD.

А тепер приступимо до дослідження алгоритму, який можна розпаралелити для виконання на графічному процесорі. Реалізація нижче приймає два вектори однакової довжини та обчислює поточковий результат. Складно уявити щось більш прямолінійне:

Void VectorAddExpPointwise(float* first, float* second, float* result, int length) ( for (int i = 0; i< length; ++i) { result[i] = first[i] + exp(second[i]); } }

Щоб розпаралелити цей алгоритм на звичайному процесорі, потрібно розбити діапазон ітерацій на кілька піддіапазонів і запустити по одному потоку виконання для кожного з них. Ми присвятили досить багато часу в попередніх статтях саме такому способу розпаралелювання нашого першого прикладу пошуку простих чисел - ми бачили, як це можна зробити, створюючи потоки вручну, передаючи завдання пулу потоків і використовуючи Parallel.For і PLINQ для автоматичного розпаралелювання. Згадайте також, що при розпаралелювання схожих алгоритмів на звичайному процесорі ми особливо дбали, щоб не роздробити завдання на занадто дрібні завдання.

Для графічного процесора ці попередження не потрібні. Графічні процесори мають безліч ядер, що виконують потоки дуже швидко, а вартість перемикання контексту значно нижча, ніж у звичайних процесорах. Нижче наводиться фрагмент, який намагається використати функцію parallel_for_eachіз фреймворку C++ AMP:

#include #include using namespace concurrency; void VectorAddExpPointwise(float* first, float* second, float* result, int length) ( array_view avFirst (length, first); array_view avSecond(length, second); array_view avResult(length, result); avResult.discard_data(); parallel_for_each(avResult.extent, [=](index<1>i) restrict(amp) (avResult[i] = avFirst[i] + fast_math::exp(avSecond[i]); )); avResult.synchronize(); )

Тепер досліджуємо кожну частину коду окремо. Відразу зауважимо, що загальна форма головного циклу збереглася, але цикл, що використовувався, for був замінений викликом функції parallel_for_each. Насправді принцип перетворення циклу у виклик функції або методу для нас не новий - раніше вже демонструвався такий прийом із застосуванням методів Parallel.For() і Parallel.ForEach() з бібліотеки TPL.

Далі, вхідні дані (параметри first, second та result) обгортаються екземплярами array_view. Клас array_view служить для обгортання даних, які передаються графічному процесору (прискорювачу). Його шаблонний параметр визначає тип даних та їх розмірність. Щоб виконати на графічному процесорі інструкції, що звертаються до даних, спочатку оброблюваним на звичайному процесорі, хтось або щось повинен подбати про копіювання даних у графічний процесор, тому що більшість сучасних графічних карток є окремими пристроями з власною пам'яттю. Це завдання вирішують екземпляри array_view - вони забезпечують копіювання даних на вимогу і тільки коли вони справді необхідні.

Коли графічний процесор виконає завдання, копіюються дані назад. Створюючи екземпляри array_view з аргументом типу const, ми гарантуємо, що first і second будуть скопійовані на згадку про графічний процесор, але не копіюватимуться назад. Аналогічно, викликаючи discard_data(), ми виключаємо копіювання результату з пам'яті звичайного процесора в пам'ять прискорювача, але ці дані копіюватимуться у зворотному напрямку.

Функція parallel_for_each приймає об'єкт extent, що визначає форму даних і функцію для застосування до кожного елемента в об'єкті extent. У цьому прикладі ми використовували лямбда-функцію, підтримка яких з'явилася в стандарті ISO C++2011 (C++11). Ключове слово restrict (amp) доручає компілятор перевірити можливість виконання тіла функції на графічному процесорі і відключає більшу частину синтаксису C++, який не може бути скомпільований в інструкції графічного процесора.

Параметр лямбда-функції, index<1>об'єкта представляє одномірний індекс. Він повинен відповідати об'єкту extent, що використовується - якби ми оголосили об'єкт extent двомірним (наприклад, визначивши форму вихідних даних у вигляді двомірної матриці), індекс також повинен був би бути двомірним. Приклад такої ситуації наводиться трохи нижче.

Нарешті, виклик методу synchronize()Наприкінці методу VectorAddExpPointwise гарантує копіювання результатів обчислень з array_view avResult, вироблених графічним процесором, назад у масив результату.

На цьому ми закінчуємо наше перше знайомство зі світом C++ AMP, і тепер ми готові до докладніших досліджень, а також до більш цікавих прикладів, що демонструють вигоди від використання паралельних обчислень на графічному процесорі. Складання векторів - не найкращий алгоритм і не найкращий кандидат для демонстрації використання графічного процесора через великі накладні витрати на копіювання даних. У наступному підрозділі будуть показані два цікавіші приклади.

Розмноження матриць

Перший «справжній» приклад, який ми розглянемо, – множення матриць. Для реалізації ми візьмемо простий кубічний алгоритм множення матриць, а не алгоритм Штрассена, що має час виконання, близький до кубічного ~O(n 2.807). Для двох матриць: матриці A розміром m x w і матриці B розміром w x n, наступна програма виконає їхнє множення і поверне результат - матрицю C розміром m x n:

Void MatrixMultiply(int * A, int m, int w, int * B, int n, int * C) ( for (int i = 0; i< m; ++i) { for (int j = 0; j < n; ++j) { int sum = 0; for (int k = 0; k < w; ++k) { sum += A * B; } C = sum; } } }

Розпаралелити цю реалізацію можна декількома способами, і при бажанні розпаралелити цей код для виконання на звичайному процесорі правильним вибором був прийом розпаралелювання зовнішнього циклу. Однак графічний процесор має досить велику кількість ядер і розпаралелив тільки зовнішній цикл, ми не зможемо створити достатню кількість завдань, щоб завантажити роботою всі ядра. Тому має сенс розпаралелити два зовнішні цикли, залишивши внутрішній цикл незайманим:

Void MatrixMultiply (int * A, int m, int w, int * B, int n, int * C) ( array_view avA(m, w, A); array_view avB(w, n, B); array_view avC(m, n, C); avC.discard_data(); parallel_for_each (avC.extent, [=](index<2>idx) restrict(amp) ( int sum = 0; for (int k = 0; k< w; ++k) { sum + = avA(idx*w, k) * avB(k*w, idx); } avC = sum; }); }

Ця реалізація все ще близько нагадує послідовну реалізацію множення матриць і приклад додавання векторів, що наводилися вище, за винятком індексу, який тепер є двовимірним і доступний у внутрішньому циклі із застосуванням оператора . Наскільки ця версія швидше за послідовну альтернативу, що виконується на звичайному процесорі? Множення двох матриць (цілих чисел) розміром 1024 х 1024 послідовна версія на звичайному процесорі виконує в середньому 7350 мілісекунд, тоді як версія для графічного процесора – тримайтеся міцніше – 50 мілісекунд, у 147 разів швидше!

Моделювання руху частинок

Приклади розв'язання задач на графічному процесорі, представлені вище, мають дуже просту реалізацію внутрішнього циклу. Зрозуміло, що так не завжди буде. У блозі Native Concurrency, посилання на який вже наводилося вище, показується приклад моделювання гравітаційних взаємодій між частинками. Моделювання включає нескінченну кількість кроків; на кожному кроці обчислюються нові значення елементів вектора прискорень кожної частки і потім визначаються їх нові координати. Тут розпаралелювання піддається вектор частинок - при досить великій кількості частинок (від декількох тисяч і вище) можна створити досить велику кількість завдань, щоб завантажити роботою всі ядра графічного процесора.

Основу алгоритму становить реалізація визначення результату взаємодій між двома частинками, як показано нижче, яку легко можна перенести на графічний процесор:

// тут float4 - це вектори з чотирма елементами, // репрезентують частинки, що беруть участь в операціях void bodybody_interaction (float4& acceleration, const float4 p1, const float4 p2) restrict(amp) ( float4 dist = p2 – p1; // w тут не використовується float absDist = dist.x * dist.x + dist.y * dist.y + dist.z * dist.z; float invDist = 1.0f / sqrt (absDist); = dist*PARTICLE_MASS*invDistCube; )

Вихідними даними на кожному кроці моделювання є масив з координатами та швидкостями руху частинок, а в результаті обчислень створюється новий масив з координатами та швидкостями частинок:

Struct particle ( float4 position, velocity; // реалізації конструктора, конструктора копіювання та // оператора = з restrict(amp) опущені для економії місця ); simulation_step (array & previous, array & next, int bodies) ( extent<1>ext(bodies); parallel_for_each (ext, [&](index<1>idx) restrict(amp) ( particle p = previous; float4 acceleration(0, 0, 0, 0); for (int body = 0; body)< bodies; ++body) { bodybody_interaction (acceleration, p.position, previous.position); } p.velocity + = acceleration*DELTA_TIME; p.position + = p.velocity*DELTA_TIME; next = p; }); }

Із залученням відповідного графічного інтерфейсу, моделювання може бути дуже цікавим. Повний приклад, представлений командою розробників C++ AMP, можна знайти у блозі Native Concurrency. На моїй системі з процесором Intel Core i7 і відеокартою Geforce GT 740M, моделювання руху 10 000 частинок виконується зі швидкістю ~2.5 кадру в секунду (кроків в секунду) з використанням послідовної версії, що виконується на звичайному процесорі, і 160 кадрів в секунду з використанням оптимізований версії, що виконується на графічному процесорі – величезне збільшення продуктивності.

Перш ніж завершити цей розділ, необхідно розповісти ще одну важливу особливість фреймворку C++ AMP, яка може ще більше підвищити продуктивність коду, що виконується на графічному процесорі. Графічні процесори підтримують програмований кеш даних(часто званий пам'яттю, що розділяється (shared memory)). Значення, що зберігаються в цьому кеші, спільно використовуються всіма потоками виконання однієї мозаїці (tile). Завдяки мозаїчної організації пам'яті, програми на основі фреймворку C++ AMP можуть читати дані з пам'яті графічної карти в пам'ять мозаїки, що розділяється, і потім звертатися до них з декількох потоків виконання без повторного вилучення цих даних з пам'яті графічної карти. Доступ до пам'яті мозаїки виконується приблизно в 10 разів швидше, ніж до пам'яті графічної карти. Іншими словами, у вас є причини читання.

Щоб забезпечити виконання мозаїчної версії паралельного циклу, методу parallel_for_each передається домен tiled_extent, який ділить багатовимірний об'єкт extent на багатовимірні фрагменти мозаїки, і лямбда-параметр tiled_index, що визначає глобальний та локальний ідентифікатор потоку всередині мозаїки. Наприклад, матрицю 16x16 можна розділити на фрагменти мозаїки розміром 2x2 (як показано на малюнку нижче) і потім передати функції parallel_for_each:

Extent<2>matrix(16,16); tiled_extent<2,2>tiledMatrix = matrix.tile<2,2>(); parallel_for_each (tiledMatrix, [=](tiled_index<2,2>idx) restrict (amp) ( // ...));

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

При виконанні операцій з матрицями, в ядрі графічного процесора, замість стандартного індексу index<2>, як у прикладах вище, можна використовувати idx.global. Грамотне використання локальної мозаїчної пам'яті та локальних індексів може забезпечити суттєвий приріст продуктивності. Щоб оголосити мозаїчну пам'ять, що розділяється усіма потоками виконання в одній мозаїці, локальні змінні можна оголосити зі специфікатором tile_static.

На практиці часто використовується прийом оголошення пам'яті, що розділяється, і ініціалізації окремих її блоків у різних потоках виконання:

Parallel_for_each(tiledMatrix, [=](tiled_index<2,2>idx) restrict(amp) ( // 32 байти спільно використовуються всіма потоками в блоці tile_static int local; // присвоїти значення елементу для цього потоку виконання local = 42; ));

Очевидно, що будь-які вигоди від використання пам'яті, що розділяється, можна отримати тільки в разі синхронізації доступу до цієї пам'яті; тобто потоки не повинні звертатися до пам'яті, поки вона не буде ініціалізована одним із них. Синхронізація потоків у мозаїці виконується за допомогою об'єктів tile_barrier(що нагадує клас Barrier з бібліотеки TPL) - вони зможуть продовжити виконання лише після виклику методу tile_barrier.Wait(), який поверне керування лише коли всі потоки викличуть tile_barrier.Wait. Наприклад:

Parallel_for_each (tiledMatrix, (tiled_index<2,2>idx) restrict(amp) ( // 32 байти спільно використовуються всіма потоками в блоці tile_static int local; // присвоїти значення елементу для цього потоку виконання local = 42; // idx.barrier - екземпляр tile_barrier idx.barrier.wait(); // Тепер цей потік може звертатися до масиву "local", // використовуючи індекси інших потоків виконання!));

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

Суть цього прийому зводиться наступного. Щоб знайти C i,j (елемент у рядку i та в стовпці j у матриці результату), потрібно обчислити скалярний твір між A i,* (i-й рядок першої матриці) та B *,j (j-й стовпець у другій матриці ). Однак це еквівалентно обчисленню часткових скалярних творів рядка і стовпця з подальшим підсумовуванням результатів. Ми можемо використовувати цю обставину для перетворення алгоритму множення матриць на мозаїчну версію:

Void MatrixMultiply(int* A, int m, int w, int* B, int n, int* C) ( array_view avA(m, w, A); array_view avB(w, n, B); array_view avC(m, n, C); avC.discard_data(); parallel_for_each (avC.extent.tile<16,16>(), [=](tiled_index<16,16>idx) restrict(amp) ( int sum = 0; int localRow = idx.local, localCol = idx.local; for (int k = 0; k

Суть оптимізації, що описується в тому, що кожен потік в мозаїці (для блоку 16 х 16 створюється 256 потоків) ініціалізує свій елемент в 16 х 16 локальних копіях фрагментів вихідних матриць A і B. Кожному потоку в мозаїці потрібен тільки один рядок і один стовпець з цих блоків, але всі потоки разом будуть звертатися до кожного рядка і кожного стовпця по 16 разів. Такий підхід суттєво знижує кількість звернень до основної пам'яті.

Щоб обчислити елемент (i,j) у матриці результату, алгоритму потрібен повний i-й рядок першої матриці та j-й стовпець другої матриці. Коли потоки мозаїці 16x16, представлені на діаграмі і k=0, заштриховані області першої і другої матрицях будуть прочитані в пам'ять, що розділяється. Потік виконання, що обчислює елемент (i,j) у матриці результату, обчислить часткове скалярне добуток перших k елементів з i-го рядка та j-го стовпця вихідних матриць.

У цьому прикладі застосування мозаїчної організації забезпечує величезний приріст продуктивності. Мозаїчна версія множення матриць виконується набагато швидше за просту версію і займає приблизно 17 мілісекунд (для тих же вихідних матриць розміром 1024 х 1024), що в 430 швидше за версію, що виконується на звичайному процесорі!

Перш ніж закінчити обговорення фреймворку C++ AMP, хотілося б згадати інструменти (в Visual Studio), наявні розробників. Visual Studio 2012 пропонує налагоджувач для графічного процесора (GPU), що дозволяє встановлювати контрольні точки, досліджувати стек викликів, читати та змінювати значення локальних змінних (деякі прискорювачі підтримують налагодження для GPU безпосередньо; для інших Visual Studio використовує програмний симулятор), та профільник, що дає можливість оцінювати вигоди, одержувані додатком від розпаралелювання операцій із застосуванням графічного процесора. За додатковою інформацією щодо можливостей налагодження у Visual Studio звертайтеся до статті «Покроковий посібник. Налагодження програми C++ AMP на сайті MSDN.

Альтернативи обчислень на графічному процесорі В.NET

До цих пір у цій статті демонструвалися приклади тільки мовою C++, проте є кілька способів використовувати потужність графічного процесора в керованих додатках. Один із способів – використовувати інструменти взаємодій, що дозволяють перекласти роботу з ядрами графічного процесора на низькорівневі компоненти C++. Це рішення відмінно підходить для тих, хто бажає використовувати фреймворк C++ AMP або може використовувати вже готові компоненти C++ AMP в керованих додатках.

Інший спосіб - використовувати бібліотеку, що безпосередньо працює з графічним процесором з керованого коду. Нині існує кілька бібліотек. Наприклад, GPU.NET та CUDAfy.NET (обидві є комерційними пропозиціями). Нижче наводиться приклад з репозиторію GPU.NET GitHub, що демонструє реалізацію скалярного твору двох векторів:

Public static void MultiplyAddGpu(double a, double b, double c) (int ThreadId = BlockDimension.X * BlockIndex.X + ThreadIndex.X; int TotalThreads = BlockDimension.X * GridDimension.X; for (int ElementIdx = ThreadId;

Я дотримуюсь думки, що набагато простіше та ефективніше освоїти розширення мови (на основі C++ AMP), ніж намагатися організовувати взаємодії на рівні бібліотек або вносити суттєві зміни до мови IL.

Отже, після того як ми розглянули можливості паралельного програмування в .NET і використанням GPU, напевно, ні в кого не залишилося сумнівів, що організація паралельних обчислень є важливим способом підвищення продуктивності. У багатьох серверах і робочих станціях по всьому світу залишаються безцінні обчислювальні потужності звичайних і графічних процесорів, тому що програми просто не задіяють їх.

Бібліотека Task Parallel Library дає нам унікальну можливість включити в роботу всі наявні ядра центрального процесора, хоча при цьому доведеться вирішувати деякі цікаві проблеми синхронізації, надмірного дроблення завдань і нерівного розподілу роботи між потоками виконання.

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

Часто постало питання: чому немає GPU прискорення в програмі Adobe Media Encoder CC? А те, що Adobe Media Encoder використовує GPU прискорення, ми з'ясували, а також відзначили нюанси його використання. Також зустрічається твердження: у програмі Adobe Media Encoder CC прибрали підтримку GPU прискорення. Це помилкова думка і випливає з того, що основна програма Adobe Premiere Pro CC тепер може працювати без прописаної та рекомендованої відеокарти, а для включення GPU движка в Adobe Media Encoder CC відеокарта повинна бути обов'язково прописана в документах: cuda_supported_cards або opencl_supported_cards. Якщо з чіпсетами nVidia все зрозуміло, просто беремо ім'я чипсета і вписуємо його в cuda_supported_cards. То при використанні відеокарт AMD треба прописувати не ім'я чіпсету, а кодову назву ядра. Отже, давайте на практиці перевіримо, як на ноутбуці ASUS N71JQ з дискретною графікою ATI Mobility Radeon HD 5730 увімкнути GPU двигун у Adobe Media Encoder CC. Технічні дані графічного адаптера ATI Mobility Radeon HD 5730 показані утилітою GPU-Z:

Запускаємо програму Adobe Premiere Pro CC і включаємо двигун: Mercury Playback Engine GPU Acceleration (OpenCL).

Три DSLR відео на таймлайн, один над одним, два з них, створюють ефект картинка в картинці.

Ctrl+M, вибираємо пресет MPEG2-DVD, прибираємо чорні смуги з боків за допомогою опції Scale To Fill. Включаємо також підвищену якість для тестів без GPU: MRQ (Use Maximum Render Quality). Натискаємо кнопку: Export. Завантаження процесора до 20% та оперативної пам'яті 2.56 Гбайт.


Завантаження GPU чіпсету ATI Mobility Radeon HD 5730 складає 97% та 352Мб бортової відеопам'яті. Ноутбук тестувався при роботі від акумулятора, тому графічне ядро/пам'ять працюють на знижених частотах: 375/810 МГц.

Підсумковий час прорахунку: 1 хвилина та 55 секунд(вкл/откл. MRQ під час використання GPU движка, не впливає підсумковий час прорахунку).
При встановленій галці Use Maximum Render Quality тепер натискаємо кнопку: Queue.


Тактові частоти процесора під час роботи від акумулятора: 930МГц.

Запускаємо AMEEncodingLog і дивимося підсумковий час прорахунку: 5 хвилин та 14 секунд.

Повторюємо тест, але вже за знятої галки Use Maximum Render Quality, натискаємо на кнопку: Queue.

Підсумковий час прорахунку: 1 хвилина та 17 секунд.

Тепер увімкнемо GPU двигун в Adobe Media Encoder CC, запускаємо програму Adobe Premiere Pro CC, натискаємо комбінацію клавіш: Ctrl+F12, виконуємо Console > Console View і в полі Command вбиваємо GPUSniffer, натискаємо Enter.


Виділяємо та копіюємо ім'я в GPU Computation Info.

У директорії програми Adobe Premiere Pro CC відкриваємо документ opencl_supported_cards і в алфавітному порядку вбиваємо кодове ім'я чіпсету, Ctrl+S.

Натискаємо на кнопку: Queue і отримуємо GPU прискорення прорахунку проекту Adobe Premiere Pro CC в Adobe Media Encoder CC.

Підсумковий час: 1 хвилина та 55 секунд.

Підключаємо ноутбук до розетки і повторюємо результати прорахунків. Queue, галка MRQ знята, без включення двигуна, завантаження оперативної пам'яті трохи підросло:


Тактові частоти процесора: 1.6ГГц під час роботи від розетки та включення режиму: Висока продуктивність.

Підсумковий час: 46 секунд.

Включаємо двигун: Mercury Playback Engine GPU Acceleration (OpenCL), як видно від мережі ноутбукова відеокарта працює на своїх базових частотах, завантаження GPU в Adobe Media Encoder CC досягає 95%.

Підсумковий час прорахунку, знизився з 1 хвилини 55 секунд, до 1 хвилини та 5 секунд.

*Для візуалізації в Adobe Media Encoder CC тепер використовується графічний процесор (GPU). Підтримуються стандарти CUDA та OpenCL. В Adobe Media Encoder CC, двигун GPU використовується для наступних процесів візуалізації:
- Зміна чіткості (від високої до стандартної та навпаки).
- Фільтр тимчасового коду.
- Перетворення формату пікселів.
- Розмежування.
Якщо візуалізується проект Premiere Pro, AME використовує установки візуалізації з GPU, задані для цього проекту. При цьому будуть використані всі можливості візуалізації з GPU, реалізовані Premiere Pro. Для візуалізації проектів AME використовується обмежений набір можливостей для візуалізації з GPU. Якщо послідовність візуалізується за допомогою оригінальної підтримки, застосовується налаштування GPU з AME, налаштування проекту ігнорується. У цьому випадку всі можливості візуалізації з GPU Premiere Pro використовуються безпосередньо в AME. Якщо проект містить VST сторонніх виробників, використовується налаштування GPU проекту. Послідовність кодується за допомогою PProHeadless, як і в попередніх версіях AME. Якщо прапорець Enable Native Premiere Pro Sequence Import (Дозволити імпорт вихідної послідовності Premiere Pro) знято, завжди використовується PProHeadless та налаштування GPU.

Читаємо про прихований розділ на системному диску ноутбука ASUS N71JQ.

Ядер багато не буває.

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

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

Причини, за якими GPU ефективні для роботи з великими обсягами даних, що вимагають обробки:

  • у них великі можливості з паралельного виконання завдань (багато-багато процесорів)
  • висока пропускна здатність у пам'яті

Пропускна спроможність пам'яті (memory bandwidth)– це скільки інформації – біт чи гігабайт – може бути передане за одиницю часу секунду чи процесорний такт.

Одне із завдань оптимізації – задіяти по максимуму пропускну здатність – збільшити показники throughput(В ідеалі вона повинна дорівнювати memory bandwidth).

Для покращення використання пропускної спроможності:

  • збільшити обсяг інформації – використовувати пропускний канал на повну (наприклад, кожен потік працює з флоат4)
  • зменшувати латентність – затримку між операціями

Затримка (latency)– проміжок часу між моментами, коли контролер запросив конкретну комірку пам'яті та тим моментом, коли дані стали доступні процесору для виконання інструкцій. На саму затримку ми ніяк не можемо вплинути – ці обмеження присутні на апаратному рівні. Саме за рахунок цієї затримки процесор може одночасно обслуговувати кілька потоків – поки потік А запросив виділити йому пам'яті, потік Б може щось порахувати, а потік С чекати до нього прийдуть запитані дані.

Як знизити затримку (latency) якщо використовується синхронізація:

  • зменшити кількість потоків у блоці
  • збільшити кількість груп-блоків

Використання ресурсів GPU на повну – GPU Occupancy

У високолобих розмовах про оптимізацію часто миготить термін gpu occupancyабо kernel occupancy- Він відображає ефективність використання ресурсів-потужностей відеокарти. Окремо зазначу - якщо ви навіть і використовуєте всі ресурси - це зовсім не означає, що ви використовуєте їх правильно.

Обчислювальні потужності GPU – це сотні процесорів жадібних до обчислень, при створенні програми – ядра (kernel) – на плечі програміста лягатиме тягар розподілу навантаження на них. Помилка може призвести до того, що більшість цих дорогоцінних ресурсів може безцільно простоювати. Зараз я поясню чому. Почати доведеться здалеку.

Нагадаю, що варп ( warp у термінології NVidia, wavefront - В термінології AMD) - набір потоків які одночасно виконують одну і ту ж функцію-кернел на процесорі. Потоки, об'єднані програмістом в блоки розбиваються на варпи планувальником потоків (окремо для кожного мультипроцесора) – поки один варп працює, другий чекає на обробку запитів до пам'яті і т.д. Якщо якісь із потоків варпа все ще виконують обчислення, а інші вже зробили все, що могли – має місце неефективне використання обчислювального ресурсу – у народі іменоване простоювання потужностей.

Кожна точка синхронізації, кожне розгалуження логіки може породити таку ситуацію простою. Максимальна дивергенція (розгалуження логіки виконання) залежить від розміру варпа. Для GPU від NVidia – це 32, для AMD – 64.

Для того щоб знизити простий мультипроцесор під час виконання варпа:

  • мінімізувати час очікування бар'єрів
  • мінімізувати розбіжність логіки виконання у функції-кернелі

Для ефективного розв'язання даної задачі має сенс розібратися - як відбувається формування варпів (для випадку з декількома розмірностями). Насправді порядок простий - в першу чергу по X, потім Y і, в останню чергу, Z.

ядро запускається з блоками розмірністю 64×16, потоки розбиваються по варпам порядку X, Y, Z – тобто. перші 64 елементи розбиваються на два варпи, потім другі і т.д.

Ядро запускається із блоками розмірністю 16×64. У перший варп додаються перші та другі 16 елементів, у другий варп – треті та четверті тощо.

Як знижувати дивергенцію (пам'ятаєте – розгалуження – не завжди причина критичної втрати продуктивності)

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

Як використовувати ресурси GPU по максимуму

Ресурси GPU, на жаль, теж мають обмеження. І, строго кажучи, перед запуском функції-кернела є сенс визначити ліміти і при розподілі навантаження ці ліміти врахувати. Чому це важливо?

У відеокарт є обмеження на загальне число потоків, яке може виконувати один мультипроцесор, максимальна кількість потоків в одному блоці, максимальна кількість варпів на одному процесорі, обмеження різних видів пам'яті і т.п. Всю цю інформацію можна запросити як програмно, через відповідне API так і за допомогою утиліт з SDK. (Модулі deviceQuery для пристроїв NVidia, CLInfo – для відеокарт AMD).

Загальна практика:

  • число блоків/робочих груп потоків має бути кратно кількості потокових процесорів
  • розмір блоку/робочої групи повинен бути кратний розміру варпа

При цьому слід враховувати, що абсолютний мінімум – 3-4 варпи/вейфронти крутяться одночасно на кожному процесорі, мудрі гайди радять виходити з міркування – не менше семи вейфронатів. При цьому – не забувати обмеження щодо заліза!

У голові всі ці деталі тримати швидко набридає, тому для розрахунок gpu-occupancy NVidia запропонувала несподіваний інструмент - ексельний (!) Калькулятор набитий макросами. Туди можна ввести інформацію за максимальною кількістю потоків для SM, число регістрів і розмір загальної (shared) пам'яті доступних на потоковому процесорі, і використовувані параметри запуску функцій – а він видає у відсотках ефективність використання ресурсів (і ви рвете на голові волосся усвідомлюючи що задіяти всі ядра вам не вистачає регістрів).

інформація щодо використання:
http://docs.nvidia.com/cuda/cuda-c-best-practices-guide/#calculating-occupancy

GPU та операції з пам'яттю

Відеокарти оптимізовані для 128-бітових операцій із пам'яттю. Тобто. в ідеалі – кожна маніпуляція з пам'яттю, в ідеалі повинна змінювати за раз 4 чотири-байтні значення. Основна проблема для програміста полягає в тому, що сучасні компілятори для GPU не можуть оптимізувати такі речі. Це доводиться робити у коді функції і, у середньому, приносить частки-відсотка з приросту продуктивності. Набагато більший вплив на продуктивність має частота запитів до пам'яті.

Проблема в наступному - кожен запит повертає у відповідь шматочок даних розміром кратний 128 біт. А кожен потік використовує лише чверть його (у разі звичайної чотирибайтової змінної). Коли суміжні потоки одночасно працюють з даними розташованими послідовно в осередках пам'яті, це знижує загальну кількість звернень до пам'яті. Називається це явище - об'єднані операції читання та запису ( coalesced access – good! both read and write) – і за правильної організації коду ( strided access to contiguous chunk of memory – bad!) може відчутно покращити продуктивність. При організації свого ядра – пам'ятайте – суміжний доступ – у межах елементів одного рядка пам'яті, робота з елементами стовпця – це не так ефективно. Бажаєте більше деталей? мені сподобалася ось ця pdf - або гуглить на предмет memory coalescing techniques “.

Лідируючі позиції у номінації "вузьке місце" займає інша операція з пам'яттю - копіювання даних з пам'яті хоста в ГПУ . Копіювання відбувається не аби як, а із спеціально виділеної драйвером і системою області пам'яті: при запиті на копіювання даних – система спочатку копіює туди ці дані, а вже потім заливає їх у GPU. Швидкість транспортування даних обмежена пропускною спроможністю шини PCI Express xN (де N число ліній передачі даних), через які сучасні відеокарти спілкуються з хостом.

Проте, надмірне копіювання повільної пам'яті на хості – це часом невиправдані витрати. Вихід – використовувати так звану pinned memory – спеціальним чином позначену область пам'яті, так що операційна система не має можливості виконувати з нею будь-які операції (наприклад – вивантажити у свап/перемістити на свій розсуд тощо). Передача даних із хоста на відеокарту здійснюється без участі операційної системи – асинхронно, через DMA (Direct Memory Access).

І, насамкінець, ще трохи про пам'ять. Розділяється пам'ять на мультипроцессоре зазвичай організована як банків пам'яті містять 32 бітні слова – дані. Кількість банків за доброю традицією варіюється від одного покоління GPU до іншого - 16/32 Якщо кожен потік звертається за даними в окремий банк - все гаразд. Інакше виходить кілька запитів на читання/запис до одного банку і ми отримуємо конфлікт ( shared memory bank conflict). Такі конфліктні звернення серіалізуються і виконуються послідовно, а чи не паралельно. Якщо одного банку звертаються все потоки – використовується “широкомовний” відповідь ( broadcast) і конфлікту немає. Існує кілька способів ефективно боротися з конфліктами доступу, мені сподобалося опис основних методик звільнення від конфліктів доступу до банків пам'яті – .

Як зробити математичні операції ще швидше? Пам'ятати що:

  • обчислення подвійної точності – це високе навантаження операції з FP64 >> FP32
  • константи виду 3.13 у коді, за замовчуванням, інтерпретується як fp64 якщо явно не вказувати 3.14f
  • для оптимізації математики не зайвим буде впоратися в гайдах - а чи немає яких прапорців компілятор
  • виробники включають у свої SDK функції, які використовують особливості пристроїв для досягнення продуктивності (часто – на шкоду переносимості)

Для розробників CUDA має сенс звернути увагу на концепцію cuda stream,що дозволяє запускати відразу кілька функцій-ядер на одному пристрої або поєднувати асинхронне копіювання даних з хоста на пристрій під час виконання функцій. OpenCL, поки що, такого функціоналу не надає 🙁

Утиль для профілювання:

NVifia Visual Profiler – цікава утилітка, що аналізує ядра як CUDA так і OpenCL.

P. S. Як більш просторого посібника з оптимізації, можу порекомендувати гуглити всілякі best practices guide для OpenCL та CUDA.

  • ,

Говорячи про паралельні обчислення на GPU ми повинні пам'ятати, в який час ми живемо, сьогодні цей час коли все у світі прискорено настільки, що ми з вами втрачаємо рахунок часу, не помічаючи, як воно проноситиметься повз. Все, що ми робимо, пов'язано з високою точністю та швидкістю обробки інформації, в таких умовах нам неодмінно потрібні інструменти для того, щоб обробити всю інформацію, яка у нас є і перетворити її на дані, до того ж говорячи про такі завдання треба пам'ятати, що дані завдання необхідні не тільки великим організаціям або мегакорпораціям, вирішення таких завдань зараз потребують і рядові користувачі, які вирішують свої життєві завдання, пов'язані з високими технологіямиу себе вдома на персональних комп'ютерах! Поява NVIDIA CUDA була не дивною, а, швидше, обгрунтованою, тому, як незабаром буде необхідно обробляти значно трудомісткі завдання на ПК, ніж раніше. Робота, яка раніше займала дуже багато часу, тепер займатиме лічені хвилини, відповідно це вплине на загальну картину всього світу!

Що таке обчислення на GPU

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

Що таке CUDA

CUDA — технологія програмування спрощеною мовою Си алгоритмів, які виконуються на графічних процесорівприскорювачів GeForce восьмого покоління і старших, а також відповідних карток Quadro і Tesla від компанії NVIDIA. CUDA дозволяє включати у текст Сі програми спеціальні функції. Ці функції пишуться спрощеною мовою програмування Сі і виконуються на графічному процесорі. Початкова версія CUDA SDK була представлена ​​15 лютого 2007 року. Для успішної трансляції коду цією мовою, до складу CUDA SDK входить власний Сі-компілятор командного рядка nvcc компанії NVIDIA. Компілятор nvcc створений на основі відкритого компілятора Open64 і призначений для трансляції host-коду (головного, керуючого коду) і device-коду (апаратного коду) (файлів з розширенням .cu) в об'єктні файли, придатні в процесі складання кінцевої програми або бібліотеки в будь-якій середовище програмування, наприклад Microsoft Visual Studio.

Можливості технології

  1. Стандартна мова C для паралельної розробки програм на GPU .
  2. Готові бібліотеки чисельного аналізу для швидкого перетворення Фур'є та базового пакету програм лінійної алгебри.
  3. Спеціальний драйвер CUDA для обчислень із швидкою передачею даних між GPU та CPU.
  4. Можливість взаємодії драйвера CUDA з графічними драйверами OpenGL та DirectX.
  5. Підтримка операційних систем Linux 32/64-bit, Windows XP 32/64-bit та MacOS.

Переваги технології

  1. Інтерфейс програмування програм CUDA (CUDA API) заснований на стандартній мові програмування Сі з деякими обмеженнями. Це спрощує і згладжує процес вивчення архітектури CUDA.
  2. Пам'ять (shared memory) розміром 16 Кб, що розділяється між потоками, може бути використана під організований користувачем кеш з ширшою смугою пропускання, ніж при вибірці зі звичайних текстур.
  3. Більш ефективні транзакції між пам'яттю центрального процесората відеопам'яттю.
  4. Повна апаратна підтримка цілісних та побітових операцій.

Приклад застосування технології

cRark

Найважче у цій програмі — це настоянка. Програма має консольний інтерфейс, але завдяки інструкції, яка додається до самої програми, вона може користуватися. Далі наведено коротка інструкціяз налаштування програми. Ми перевіримо програму на працездатність і порівняємо її з іншою подібною програмою, яка не використовує NVIDIA CUDA, в даному випадку це відома програма Advanced Archive Password Recovery.

З скаченого архіву cRark нам потрібно лише три файли: crark.exe, crark-hp.exe та password.def. Сrark.exe - це консольна утиліта розкриття паролів RAR 3.0 без шифрованих файлів усередині архіву (тобто розкриваючи архів ми бачимо назви, але не можемо розпакувати архів без пароля).

Сrark-hp.exe - це консольна утиліта розтину паролів RAR 3.0 із шифруванням всього архіву (тобто розкриваючи архів ми не бачимо ні назви, ні самих архівів і не можемо розпакувати архів без пароля).

Password.def - це будь-який перейменований текстовий файл з дуже невеликим змістом (наприклад: 1-й рядок: ## 2-й рядок: ?*, у цьому випадку розтин пароля відбуватиметься з використанням усіх знаків). Password.def – це керівник програми cRark. У файлі містяться правила розкриття пароля (або область знаків, яку crark.exe буде використовувати у своїй роботі). Докладніше про можливості вибору цих знаків написано в текстовому файлі, отриманому при розтині завантаженого на сайті автора програми cRark: russian.def .

Підготовка

Відразу скажу, що програма працює лише якщо ваша відеокарта заснована на GPU із підтримкою рівня прискорення CUDA 1.1. Так що серія відеокарт, що базуються на чіпі G80, таких як GeForce 8800 GTX, відпадає, тому що вони мають апаратну підтримку прискорення CUDA 1.0. Програма підбирає за допомогою CUDA лише паролі на архіви RAR версій 3.0+. Необхідно встановити все програмне забезпечення, пов'язане з CUDA, а саме:

  • Драйвери NVIDIA, що підтримують CUDA, починаючи з 169.21
  • NVIDIA CUDA SDK, починаючи з версії 1.1
  • NVIDIA CUDA Toolkit, починаючи з версії 1.1

Створюємо будь-яку папку в будь-якому місці (наприклад, на диску С:) і називаємо будь-яким ім'ям, наприклад, «3.2». Поміщаємо туди файли: crark.exe, crark-hp.exe та password.def та запаролений/зашифрований архів RAR.

Далі слід запустити консоль командної рядки Windowsі перейти до неї створену папку. В Windows Vistaі 7 слід викликати меню "Пуск" і в полі пошуку ввести "cmd.exe", у Windows XP з меню "Пуск" спочатку слід викликати діалог "Виконати" і вже в ньому вводити "cmd.exe". Після відкриття консолі слід ввести команду виду: cd C:\папка\, cd C:\3.2 у цьому випадку.

Набираємо в текстовому редакторідва рядки (можна також зберегти текст як файл .bat у папці з cRark) для підбору пароля запароленого RAR-архіву з незашифрованими файлами:

echo off;
cmd /K crark (назва архіву).rar

для підбору пароля запароленого та зашифрованого RAR-архіву:

echo off;
cmd /K crark-hp (назва архіву).rar

Копіюємо 2 рядки текстового файлу в консоль і натискаємо Enter (або запускаємо .bat файл).

Результати

Процес розшифровки показаний малюнку:

Швидкість підбору на cRark за допомогою CUDA становила 1625 паролів/секунду. За одну хвилину тридцять шість секунд був підібраний пароль із трьома знаками: «q)$». Для порівняння: швидкість перебору в Advanced Archive Password Recovery на моєму двоядерному процесорі Athlon 3000+ дорівнює максимум 50 паролів/секунду і перебір мав би тривати 5 годин. Тобто підбір по bruteforce у cRark архіву RAR за допомогою відеокарти GeForce 9800 GTX+ відбувається у 30 разів швидше, ніж на CPU.

Для тих, у кого процесор Intel, хороша системна плата з високою частотою системної шини (FSB 1600 МГц), показник CPU rate та швидкість перебору будуть вищими. А якщо у вас чотириядерний процесор та пара відеокарт рівня GeForce 280 GTX, то швидкодія перебору паролів прискорюється в рази. Підбиваючи підсумки прикладу треба сказати, що це завдання було вирішено із застосуванням технології CUDA всього за якихось 2 хвилини замість 5-ти годин, що говорить про високий потенціал можливостей для даної технології!

Висновки

Розглянувши сьогодні технологію для паралельних обчислень CUDA, ми наочно побачили всю міць і величезний потенціал для розвитку даної технології на прикладі програми для відновлення пароля для RAR архівів. Треба сказати про перспективи даної технології, дана технологія неодмінно знайде місце в житті кожної людини, яка вирішить їй скористатися, чи то наукові завдання, чи завдання, пов'язані з обробкою відео, або навіть економічні завдання, які вимагають швидкого точного розрахунку, все це призведе до неминучого. підвищенню продуктивності праці, яку не можна не помітити. Сьогодні в лексикон вже починає входити словосполучення «домашній суперкомп'ютер»; Цілком очевидно, що для втілення такого предмета в реальність у кожному будинку вже є інструмент під назвою CUDA. Починаючи з моменту виходу карт, заснованих на чіпі G80 (2006 р.), випущено велика кількістьприскорювачів на базі NVIDIA, що підтримують технологію CUDA, яка здатна втілити мрії про суперкомп'ютери у кожному будинку на реальність. Просуваючи технологію CUDA, NVIDIA піднімає свій авторитет в очах клієнтів у вигляді надання додаткових можливостейїх обладнання, яке у багатьох уже куплено. Залишається тільки вірити, що незабаром CUDA буде розвиватися дуже швидко і дасть користувачам повною мірою скористатися всіма можливостями паралельних обчислень на GPU.

Особливості архітектури AMD/ATI Radeon

Це схоже на народження нових біологічних видів, коли при освоєнні сфер існування живі істоти еволюціонують для поліпшення пристосованості до середовища. Так і GPU, почавши з прискорення розтеризації та текстурування трикутників, розвинули додаткові здібності з виконання шейдерних програм для розмальовки цих трикутників. І ці здібності виявилися потрібні й у неграфічних обчисленнях, де часом дають значний виграш у продуктивності проти традиційними рішеннями.

Проводимо аналогії далі – після довгої еволюції на суші ссавці проникли в море, де потіснили звичайних морських мешканців. У конкурентній боротьбі ссавці використовували як нові просунуті здібності, що з'явилися на земній поверхні, так і спеціально придбані для адаптації до життя у воді. Так само GPU, ґрунтуючись на перевагах архітектури для 3D-графіки, дедалі більше обзаводяться спеціальними. функціональними можливостями, корисними виконання далеких від графіки завдань.

Отже, що дозволяє GPU претендувати на власний сектор у сфері програм загального призначення? Мікроархітектура GPU побудована зовсім інакше, ніж у стандартних CPU, і в ній спочатку закладені певні переваги. Завдання графіки передбачають незалежну паралельну обробку даних, і GPU спочатку мультипоточний. Але ця паралельність йому лише на радість. Мікроархітектура спроектована так, щоб експлуатувати велику кількість ниток, що вимагають виконання.

GPU складається з декількох десятків (30 для Nvidia GT200, 20 – для Evergreen, 16 – для Fermi) процесорних ядер, які в термінології Nvidia називаються Streaming Multiprocessor, а в термінології ATI – SIMD Engine. У рамках цієї статті ми будемо називати їх мініпроцесорами, тому що вони виконують кілька сотень програмних ниток і вміють майже все те ж, що і стандартний CPU, але все-таки не все.

Маркетингові назви заплутують – у них, для більшої важливості, вказують кількість функціональних модулів, які вміють віднімати та множити: наприклад, 320 векторних «cores» (ядер). Ці ядра більше нагадують зерна. Краще представляти GPU як багатоядерний процесор з великою кількістю ядер, що виконують одночасно безліч ниток.

Кожен мініпроцесор має локальну пам'ять, розміром 16 KБ для GT200, 32 KБ – для Evergreen та 64 KБ – для Fermi (по суті, це програмований L1 кеш). Вона має схоже з кешем першого рівня стандартного CPU час доступу і виконує аналогічні функції якнайшвидшої доставки даних до функціональних модулів. В архітектурі Fermi частина локальної пам'яті може бути налаштована як звичайний кеш. У GPU локальна пам'ять служить для швидкого обміну даними між нитками, що виконуються. Одна із звичайних схем GPU-програми така: на початку локальну пам'ять завантажуються дані з глобальної пам'яті GPU. Це просто звичайна відеопам'ять, розташована (як і системна пам'ять) окремо від «свого» процесора – у разі відео вона розпаяна декількома мікросхемами на текстоліті відеокарти. Далі кілька сотень ниток працюють з цими даними у локальній пам'яті і записують результат у глобальну пам'ять, після чого той передається в CPU. До обов'язку програміста входить написання інструкцій завантаження та вивантаження даних із локальної пам'яті. По суті, це розбиття даних [ конкретного завдання] для паралельної обробки. GPU підтримує також інструкції атомарного запису/читання в пам'ять, але вони неефективні та затребувані зазвичай на завершальному етапі для склеювання результатів обчислень всіх мініпроцесорів.

Локальна пам'ять загальна для всіх ниток, що виконуються в мініпроцесорі, тому, наприклад, в термінології Nvidia вона навіть називається shared, а терміном local memory позначається прямо протилежне, а саме: якась персональна область окремої нитки в глобальній пам'яті, видима і доступна тільки їй. Але крім локальної пам'яті в мініпроцесорі є ще одна область пам'яті, у всіх архітектурах приблизно вчетверо більша за обсягом. Вона розділена порівну між усіма нитками, що виконуються, це регістри для зберігання змінних і проміжних результатів обчислень. На кожну нитку припадає кілька десятків регістрів. Точна кількість залежить від того, скільки ниток виконує мініпроцесор. Ця кількість дуже важлива, так як латентність глобальної пам'яті дуже велика, сотні тактів, і без кешів немає де зберігати проміжні результати обчислень.

І ще одна важлива риса GPU: "м'яка" векторність. Кожен мініпроцесор має велику кількість обчислювальних модулів (8 для GT200, 16 для Radeon і 32 для Fermi), але всі вони можуть виконувати тільки одну і ту ж інструкцію, з однією програмною адресою. А операнди ж при цьому можуть бути різні, у різних ниток свої. Наприклад, інструкція скласти вміст двох регістрів: вона одночасно виконується всіма обчислювальними пристроями, але регістри беруться різні. Передбачається, що всі нитки GPU-програми, здійснюючи паралельну обробку даних, рухаються паралельним курсом за кодом програми. Таким чином, усі обчислювальні модулі завантажуються рівномірно. А якщо нитки через розгалуження у програмі розійшлися у своєму шляху виконання коду, то відбувається так звана серіалізація. Тоді використовуються не всі обчислювальні модулі, тому що нитки подають на виконання різні інструкції, а блок обчислювальних модулів може виконувати, як ми вже сказали, лише інструкцію з однією адресою. І, зрозуміло, продуктивність у своїй падає стосовно максимальної.

Плюсом є те, що векторизація відбувається повністю автоматично, це не програмування з використанням SSE, MMX тощо. І GPU сам обробляє розбіжності. Теоретично можна взагалі писати програми для GPU, не думаючи про векторну природу виконуючих модулів, але швидкість такої програми буде не дуже високою. Мінус полягає у великій ширині вектора. Вона більша, ніж номінальна кількість функціональних модулів, і становить 32 для GPU Nvidia і 64 для Radeon. Нитки обробляються блоками відповідного розміру. Nvidia називає цей блок ниток терміном warp, AMD - wave front, що те саме. Таким чином, на 16 обчислювальних пристроях "хвильовий фронт" довжиною 64 нитки обробляється за чотири такти (за умови звичайної довжини інструкції). Автор вважає за краще в даному випадку термін warp, через асоціацію з морським терміном warp, що позначає пов'язаний з скручених мотузок канат. Так і нитки «скручуються» та утворюють цільну зв'язку. Втім, «wave front» теж може асоціюватися з морем: інструкції також прибувають до виконавчих пристроїв, як хвилі одна за одною накочуються на берег.

Якщо всі нитки однаково просунулися у виконанні програми (перебувають в одному місці) і, таким чином, виконують одну інструкцію, то все чудово, але якщо ні – відбувається уповільнення. У цьому випадку нитки з одного warp або wave front знаходяться в різних місцях програми, вони розбиваються на групи ниток, що мають однакове значення номера інструкції (іншими словами, покажчика інструкцій (instruction pointer)). І як і раніше виконуються одночасно часу нитки однієї групи - всі виконують однакову інструкцію, але з різними операндами. У результаті warp здійснюється в стільки разів повільніше, на скільки груп він розбитий, а кількість ниток у групі значення не має. Навіть якщо група складається з однієї нитки, все одно вона буде виконуватися стільки ж часу, скільки повний warp. У залозі це реалізовано за допомогою маскування певних ниток, тобто інструкції формально виконуються, але результати виконання нікуди не записуються і надалі не використовуються.

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

В даному випадку кожен з 20 SIMD Engine має чотири активні wave front, у кожному з яких 64 нитки. Кожна нитка є короткою лінією. Всього: 64×4×20=5120 ниток

Таким чином, з огляду на те, що кожен warp або wave front складається з 32-64 ниток, мініпроцесор має кілька сотень активних ниток, які виконуються практично одночасно. Нижче ми побачимо, які архітектурні вигоди обіцяє така велика кількість паралельних ниток, але спочатку розглянемо, які обмеження є у складових міні-процесорів GPU.

Головне, що в GPU немає стека, де могли б зберігатись параметри функцій та локальні змінні. Через велику кількість ниток для стека просто немає місця на кристалі. Дійсно, так як GPU одночасно виконує близько 10000 ниток, при розмірі стека однієї нитки в 100 КБ сукупний об'єм складе 1 ГБ, що дорівнює стандартному об'єму відеопам'яті. Тим більше, немає ніякої можливості помістити стек скільки-небудь істотного розміру в самому ядрі GPU. Наприклад, якщо покласти 1000 байт стека на нитку, то тільки на один мініпроцесор знадобиться 1 МБ пам'яті, що майже в п'ять разів більше за сукупний обсяг локальної пам'яті мініпроцесора і пам'яті, відведеної на зберігання регістрів.

Тому в GPU-програмі немає рекурсії, і з викликами функцій особливо не розгорнешся. Усі функції безпосередньо підставляються в код під час компіляції програми. Це обмежує сферу застосування GPU задачами обчислювального типу. Іноді можна використовувати обмежену емуляцію стека з використанням глобальної пам'яті рекурсійних алгоритмів з відомою невеликою глибиною ітерацій, але це нетипове застосування GPU. І тому необхідно спеціально розробляти алгоритм, досліджувати можливість реалізації без гарантії успішного прискорення проти CPU.

У Fermi вперше з'явилася можливість використовувати віртуальні функції, але їх застосування лімітовано відсутністю великого швидкого кеша для кожної нитки. На 1536 ниток припадає 48 КБ або 16 КБ L1, тобто віртуальні функції в програмі можна використовувати відносно рідко, інакше для стека також використовуватиметься повільна глобальна пам'ять, що уповільнить виконання і, швидше за все, не принесе переваг у порівнянні з CPU-варіантом.

Таким чином, GPU представляється в ролі обчислювального співпроцесора, в який завантажуються дані, вони обробляються деяким алгоритмом і видається результат.

Переваги архітектури

Але вважає GPU дуже швидко. І в цьому йому допомагає його висока мультипоточність. Велика кількість активних ниток дозволяє частково приховати велику латентність розташованої окремо глобальної відеопам'яті, що становить близько 500 тактів. Особливо добре вона нівелюється для коду з високою щільністюарифметичних операцій Таким чином, не потрібна дорога з точки зору транзисторів ієрархія кешів L1-L2-L3. Замість неї на кристалі можна розмістити множину обчислювальних модулів, забезпечивши визначну арифметичну продуктивність. А поки виконуються інструкції однієї нитки або варпа, решта сотень ниток спокійно чекають на свої дані.

У Fermi було введено кеш другого рівня розміром близько 1 МБ, але його не можна порівнювати з кешами сучасних процесорів, він більше призначений для комунікації між ядрами та різноманітними програмними трюками. Якщо його розмір розділити між усіма десятками тисяч ниток, на кожну прийдеться зовсім незначний обсяг.

Але, крім латентності глобальної пам'яті, в обчислювальному пристрої існує ще безліч латентностей, які треба приховати. Це латентність передачі всередині кристала від обчислювальних пристроїв до кешу першого рівня, тобто локальної пам'яті GPU, і до регістрів, а також кешу інструкцій. Регістровий файл, як і локальна пам'ять, розташовані окремо від функціональних модулів, і швидкість доступу до них становить приблизно півтора десятки тактів. І знову ж таки велика кількість ниток, активних варпів, дозволяє ефективно приховати цю латентність. Причому загальна смуга пропускання (bandwidth) доступу до локальної пам'яті всього GPU, з урахуванням кількості складових його мініпроцесорів, значно більша, ніж bandwidth доступу до кешу першого рівня у сучасних CPU. GPU може переробити значно більше даних за одиницю часу.

Можна відразу сказати, що якщо GPU не буде забезпечений великою кількістю паралельних ниток, то у нього буде майже нульова продуктивність, тому що він працюватиме з тим самим темпом, начебто повністю завантажений, а виконуватиме набагато менший обсяг роботи. Наприклад, нехай замість 10000 ниток залишиться лише одна: продуктивність впаде приблизно в тисячу разів, бо не тільки не всі блоки будуть завантажені, а й позначаться всі латентності.

Проблема приховування латентностей є гострою і для сучасних високочастотних CPU, для її усунення використовуються витончені способи - глибока конвеєризація, позачергове виконання інструкцій (out-of-order). Для цього потрібні складні планувальники виконання інструкцій, різні буфери і т. п., що займає місце на кристалі. Все це потрібно для кращої продуктивності в однопотоковому режимі.

Але для GPU все це не потрібно, він архітектурно швидший для обчислювальних завдань з великою кількістю потоків. Зате він перетворює багатопоточність у продуктивність, як філософський камінь перетворює свинець на золото.

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

Приклад

Одне з класичних завдань для GPU - це завдання обчислення взаємодії N тіл, що створюють гравітаційне поле. Але якщо нам, наприклад, знадобиться розрахувати еволюцію системи Земля-Місяць-Сонце, то GPU нам поганий помічник: мало об'єктів. Для кожного об'єкта треба обчислити взаємодії з усіма іншими об'єктами, які всього два. У разі руху Сонячної системи з усіма планетами та їх місяцями (приблизно кілька сотень об'єктів) GPU все ще не надто ефективний. Втім, і багатоядерний процесор через високі накладні витрати на управління потоками теж не зможе проявити всю свою міць, працюватиме в однопоточному режимі. Але якщо потрібно також розрахувати траєкторії комет і об'єктів поясу астероїдів, то це вже завдання для GPU, так як об'єктів достатньо, щоб створити необхідну кількість паралельних потоків розрахунку.

GPU також добре себе проявить, якщо необхідно розрахувати зіткнення кульових скупчень із сотень тисяч зірок.

Ще одна можливість використовувати потужність GPU у задачі N тіл з'являється, коли необхідно розрахувати безліч окремих завдань, нехай і з невеликою кількістю тіл. Наприклад, якщо потрібно розрахувати варіанти еволюції однієї системи за різних варіантах початкових швидкостей. Тоді ефективно використовувати GPU вдасться без проблем.

Деталі мікроархітектури AMD Radeon

Ми розглянули базові принципи організації GPU, вони є спільними для відеоприскорювачів усіх виробників, тому що у них спочатку було одне цільове завдання - шейдерні програми. Проте виробники знайшли можливість розійтися в деталях мікроархітектурної реалізації. Хоча і CPU різних вендорів часом сильно відрізняються, навіть сумісними, як, наприклад, Pentium 4 і Athlon або Core. Архітектура Nvidia вже досить широко відома, зараз ми розглянемо Radeon та виділимо основні відмінності у підходах цих вендорів.

Відеокарти AMD отримали повноцінну підтримку обчислень загального призначення починаючи з сімейства Evergreen, в якому також були вперше реалізовані специфікації DirectX 11. Картки сімейства 47xx мають низку суттєвих обмежень, які будуть розглянуті нижче.

Відмінності у розмірі локальної пам'яті (32 КБ у Radeon проти 16 КБ у GT200 і 64 КБ у Fermi) загалом не важливі. Як і розмір wave front у 64 нитках у AMD проти 32 ниток у warp у Nvidia. Практично будь-яку програму GPU можна легко переконфігурувати і налаштувати на ці параметри. Продуктивність може змінитися на десятки відсотків, але у випадку з GPU це не так важливо, бо GPU-програма зазвичай працює в десять разів повільніше, ніж аналог для CPU, або в десять разів швидше, або взагалі не працює.

Більш важливим є використання AMD технології VLIW (Very Long Instruction Word). Nvidia використовує скалярні прості інструкції, що оперують зі скалярними регістрів. Її прискорювачі реалізують простий класичний RISC. Відеокартки AMD мають таку ж кількість регістрів, як GT200, але векторні регістри 128-бітні. Кожна VLIW-інструкція оперує декількома чотирикомпонентними 32-бітними регістрів, що нагадує SSE, але можливості VLIW набагато ширші. Це не SIMD (Single Instruction Multiple Data), як SSE - тут інструкції для кожної пари операнда можуть бути різними і навіть залежними! Наприклад, нехай компоненти регістру А називаються a1, a2, a3, a4; у регістру B – аналогічно. Можна обчислити за допомогою однієї інструкції, яка виконується за один такт, наприклад, число a1×b1+a2×b2+a3×b3+a4×b4 або двовимірний вектор (a1×b1+a2×b2, a3×b3+a4×b4 ).

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

Завдяки векторним інструкціям, пікова продуктивність Radeon у числах одинарної точності дуже висока і вже становить терафлопи.

Один векторний регістр може замість чотирьох чисел одинарної точності зберігати одне число подвійної точності. І одна VLIW-інструкція може або скласти дві пари чисел double, або помножити два числа, або помножити два числа та скласти з третім. Таким чином, пікова продуктивність в double приблизно в п'ять разів нижче, ніж у float. Для старших моделей Radeon вона відповідає продуктивності Nvidia Tesla на новій архітектурі Fermi та набагато вище, ніж продуктивність у double карток на архітектурі GT200. У споживчих відеокартах Geforce на основі Fermi максимальна швидкість double-обчислень була зменшена вчетверо.


Важлива схема роботи Radeon. Представлено лише один мініпроцесор з 20 паралельно працюючих

Виробники GPU, на відміну від виробників CPU (насамперед x86-сумісних), не пов'язані питаннями сумісності. GPU-програма спочатку компілює в якийсь проміжний код, а при запуску програми драйвер компілює цей код в машинні інструкції, специфічні для конкретної моделі. Як було описано вище, виробники GPU скористалися цим, придумавши зручні ISA (Instruction Set Architecture) для своїх GPU та змінюючи їх від покоління до покоління. Це в будь-якому випадку додало якісь відсотки продуктивності через відсутність (за непотрібністю) декодера. Але компанія AMD пішла ще далі, придумавши свій формат розташування інструкцій в машинному коді. Вони розташовані не послідовно (згідно з листингом програми), а по секціях.

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

Секції інструкцій умовних переходів
Секція 0Розгалуження 0Посилання на секцію №3 безперервних арифметичних інструкцій
Секція 1Розгалуження 1Посилання на секцію №4
Секція 2Розгалуження 2Посилання на секцію №5
Секції безперервних арифметичних інструкцій
Секція 3VLIW-інструкція 0VLIW-інструкція 1VLIW-інструкція 2VLIW-інструкція 3
Секція 4VLIW-інструкція 4VLIW-інструкція 5
Секція 5VLIW-інструкція 6VLIW-інструкція 7VLIW-інструкція 8VLIW-інструкція 9

GPU обох виробників (і Nvidia, і AMD) також мають вбудовані інструкції швидкого обчислення за кілька тактів основних математичних функцій, квадратного кореня, експоненти, логарифмів, синусів і косінусів для чисел одинарної точності. І тому є спеціальні обчислювальні блоки. Вони «відбулися» від необхідності реалізації швидкої апроксимації цих функцій у геометричних шейдерах.

Якби навіть хтось не знав, що GPU використовуються для графіки, і ознайомився лише з технічними характеристиками, то за цією ознакою міг би здогадатися, що ці обчислювальні співпроцесори походять від відеоприскорювачів. Аналогічно, за деякими рисами морських ссавців, вчені зрозуміли, що їхні предки були сухопутними істотами.

Але більш очевидна риса, що видає графічне походження пристрою, це блоки читання двовимірних та тривимірних текстур за допомогою білінійної інтерполяції. Вони широко використовуються в GPU-програмах, оскільки забезпечують прискорене та спрощене читання масивів даних read-only. Одним із стандартних варіантів поведінки GPU-додатка є читання масивів вихідних даних, обробка їх у обчислювальних ядрах та запис результату в інший масив, який передається далі назад у CPU. Така схема є стандартною і поширеною, тому що зручна для архітектури GPU. Завдання, що вимагають інтенсивно читати та писати в одну велику область глобальної пам'яті, що містять, таким чином, залежність за даними, важко розпаралелити та ефективно реалізувати на GPU. Також їхня продуктивність сильно залежатиме від латентності глобальної пам'яті, яка дуже велика. А ось якщо завдання описується шаблоном «читання даних – обробка – запис результату», то майже напевно можна отримати великий приріст від її виконання на GPU.

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

Хоча розмір кешів L1-L2 у картках Nvidia і AMD приблизно подібний, що, очевидно, викликане вимогами оптимальності з погляду графіки ігор, латентність доступу до цих кешів суттєво відрізняється. Латентність доступу у Nvidia більша, і текстурні кеші в Geforce насамперед допомагають скоротити навантаження на шину пам'яті, а не безпосередньо прискорити доступ до даних. Це не помітно у графічних програмах, але важливо для програм загального призначення. У Radeon ж латентність текстурного кеша нижче, зате вище за латентність локальної пам'яті мініпроцесорів. Можна навести такий приклад: для оптимального перемноження матриць на картках Nvidia краще скористатися локальною пам'яттю, завантажуючи туди матрицю побічно, а AMD кращепокластися на низьколатентний текстурний кеш, читаючи елементи матриці в міру потреби. Але це досить тонка оптимізація, і вже принципово переведеного на GPU алгоритму.

Ця різниця також проявляється у разі використання 3D-текстури. Один з перших бенчмарків обчислень на GPU, який показував серйозну перевагу AMD, якраз і використав 3D-текстури, оскільки працював із тривимірним масивом даних. А латентність доступу до текстур у Radeon значно швидше, і 3D-випадок додатково більш оптимізований у залозі.

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

Обмеження серії Radeon 47xx

У цьому сімействі підтримка обчислень на GPU неповна. Можна відзначити три важливих моменту. По-перше, немає локальної пам'яті, тобто вона фізично є, але не має можливості універсального доступу, необхідного сучасним стандартом GPU-програм. Вона емулює програмно в глобальній пам'яті, тобто її використання на відміну від повнофункціонального GPU не принесе вигод. Другий момент - обмежена підтримка різних інструкцій атомарних операцій із пам'яттю та інструкцій синхронізації. І третій момент - це досить невеликий розмір кешу вказівок: починаючи з деякого розміру програми відбувається уповільнення швидкості в рази. Є інші дрібні обмеження. Можна сказати, тільки програми, які ідеально підходять для GPU, будуть добре працювати на цій відеокартці. Нехай у простих тестових програмах, Які оперують тільки з регістрами, відеокарта може показувати хороший результат у Gigaflops, щось складне ефективно запрограмувати під неї проблематично.

Переваги та недоліки Evergreen

Якщо порівняти продукти AMD та Nvidia, то, з погляду обчислень на GPU, серія 5xxx виглядає як дуже потужний GT200. Такий потужний, що за піковою продуктивністю перевершує Fermi приблизно в два з половиною рази. Особливо після того, як параметри нових відеокарт Nvidia були урізані, скорочено кількість ядер. Але поява в Fermi кешу L2 спрощує реалізацію на GPU деяких алгоритмів, таким чином розширюючи сферу застосування GPU. Що цікаво, для добре оптимізованих під минуле покоління GT200 CUDA-програм архітектурні нововведення Fermi часто нічого не дали. Вони прискорилися пропорційно до збільшення кількості обчислювальних модулів, тобто менш ніж удвічі (для чисел одинарної точності), або навіть ще менше, бо ПСП пам'яті не збільшилася (чи з інших причин).

І в задачах, що добре лягають на архітектуру GPU, що мають виражену векторну природу (наприклад, перемноженні матриць), Radeon показує відносно близьку до теоретичного піку продуктивність і обганяє Fermi. Не кажучи вже про багатоядерні CPU. Особливо в задачах із числами з одинарною точністю.

Але Radeon має меншу площу кристала, менше тепловиділення, енергоспоживання, більший вихід придатних і, відповідно, меншу вартість. І безпосередньо в завданнях 3D-графіки виграш Fermi, якщо він взагалі є, набагато менший від різниці в площі кристала. Багато в чому це пояснюється тим, що обчислювальна архітектура Radeon з 16 обчислювальними пристроями на мініпроцесор, розміром wave front в 64 нитки та векторними VLIW-інструкціями є прекрасною для його головного завдання - обчислення графічних шейдерів. Для більшості звичайних користувачів продуктивність в іграх і ціна пріоритетні.

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

Наприклад, у повністю паралельному задачі підбору ключів Radeon, що легко векторизується, у кілька разів швидше Geforce і в кілька десятків разів швидше за CPU.

Це відповідає загальної концепції AMD Fusion, згідно з якою GPU повинні доповнювати CPU, і в майбутньому інтегруватися в саме ядро ​​CPU, як раніше математичний співпроцесор був перенесений з окремого кристала в ядро ​​процесора (це сталося двадцять років тому, перед появою перших процесорів Pentium). GPU буде інтегрованим графічним ядром та векторним співпроцесором для потокових завдань.

Radeon використовується хитра техніка змішування інструкцій з різних wave front при виконанні функціональними модулями. Це легко зробити, тому що інструкції повністю незалежні. Принцип аналогічний до конвеєрного виконання незалежних інструкцій сучасними CPU. Очевидно, це дозволяє ефективно виконувати складні, що займають багато байт, векторні VLIW-інструкції. У CPU для цього потрібен складний планувальник для виявлення незалежних інструкцій або використання технології Hyper-Threading, яка також забезпечує CPU незалежними інструкціями з різних потоків.

такт 0такт 1такт 2такт 3такт 4такт 5такт 6такт 7VLIW-модуль
wave front 0wave front 1wave front 0wave front 1wave front 0wave front 1wave front 0wave front 1
інстр. 0інстр. 0інстр. 16інстр. 16інстр. 32інстр. 32інстр. 48інстр. 48VLIW0
інстр. 1VLIW1
інстр. 2VLIW2
інстр. 3VLIW3
інстр. 4VLIW4
інстр. 5VLIW5
інстр. 6VLIW6
інстр. 7VLIW7
інстр. 8VLIW8
інстр. 9VLIW9
інстр. 10VLIW10
інстр. 11VLIW11
інстр. 12VLIW12
інстр. 13VLIW13
інстр. 14VLIW14
інстр. 15VLIW15

128 інструкцій двох wave front, кожен із яких складається з 64 операцій, виконуються 16 VLIW-модулями за вісім тактів. Відбувається чергування, і кожен модуль насправді має два такти виконання цілої інструкції за умови, що він на другому такті почне виконувати нову паралельно. Ймовірно, це допомагає швидко виконати VLIW-інструкцію типу a1×a2+b1×b2+c1×c2+d1×d2, тобто виконати вісім таких інструкцій за вісім тактів. (Формально виходить, одну за такт.)

У Nvidia, мабуть, такої технології немає. І без VLIW, для високої продуктивності з використанням скалярних інструкцій потрібна висока частота роботи, що автоматично підвищує тепловиділення і пред'являє високі вимоги до технологічного процесу(щоб змусити працювати схему більш високої частоті).

Недоліком Radeon з точки зору GPU-обчислень є велика нелюбов до розгалужень. GPU взагалі не шанують розгалуження через вищеописану технологію виконання інструкцій: відразу групою ниток з однією програмною адресою. (До речі, така техніка називається SIMT: Single Instruction - Multiple Threads (одна інструкція - багато ниток), за аналогією з SIMD, де одна інструкція виконує одну операцію з різними даними.) Проте Radeon розгалуження не люблять особливо: це викликано більшим розміром зв'язування ниток . Зрозуміло, що якщо програма не повністю векторна, то чим більший розмір warp або wave front, тим гірше, тому що при розбіжності в дорозі за програмою сусідніх ниток утворюється більше груп, які необхідно виконувати послідовно (серіалізовано). Допустимо, всі нитки розбрелися, тоді у разі розміру warp у 32 нитки програма працюватиме у 32 рази повільніше. А у разі розміру 64, як у Radeon, - у 64 рази повільніше.

Це помітне, але не єдине прояв «неприязні». У відеокартах Nvidia кожен функціональний модуль, інакше званий CUDA core, має спеціальний блок обробки розгалужень. А у відеокартах Radeon на 16 обчислювальних модулів – всього два блоки управління розгалуженнями (вони виведені з домену арифметичних блоків). Так що навіть проста обробка інструкції умовного переходу, нехай її результат і однаковий для всіх ниток у wave front, займає додатковий час. І швидкість просідає.

Компанія AMD виробляє ще й CPU. Вони вважають, що для програм з великою кількістю розгалужень все одно краще підходить CPU, а GPU призначений для векторних програм.

Так що Radeon надає в цілому менше можливостей для ефективного програмування, але забезпечує найкраще співвідношення ціна-продуктивність у багатьох випадках. Іншими словами, програм, які можна ефективно (з користю) перевести з CPU на Radeon, менше, ніж програм, які ефективно працюють на Fermi. Але ті, які ефективно перенести можна, працюватимуть на Radeon ефективніше в багатьох сенсах.

API для GPU-обчислень

Самі технічні специфікації Radeon виглядають привабливо, нехай і не варто ідеалізувати та абсолютизувати обчислення на GPU. Але не менш важливе для продуктивності програмне забезпечення, необхідне для розробки та виконання GPU-програми – компілятори з мови високого рівняі run-time, тобто драйвер, який здійснює взаємодію між частиною програми, що працює на CPU, та безпосередньо GPU. Воно навіть важливіше, ніж у випадку CPU: для CPU не потрібен драйвер, який здійснюватиме менеджмент передачі даних, і з погляду компілятора GPU більш вибагливий. Наприклад, компілятор повинен обійтися мінімальною кількістю регістрів для зберігання проміжних результатів обчислень, а також акуратно вбудовувати виклики функцій, знов-таки використовуючи мінімум регістрів. Адже чим менше регістрів використовує нитку, тим більше ниток можна запустити і повніше навантажити GPU, краще приховуючи час доступу до пам'яті.

І ось програмна підтримка продуктів Radeon поки що відстає від розвитку заліза. (На відміну від ситуації з Nvidia, де відкладався випуск заліза, і продукт вийшов у урізаному вигляді.) Ще недавно OpenCL-компілятор виробництва AMD мав статус бета, з безліччю недоробок. Він дуже часто генерував помилковий код або відмовлявся компілювати код із правильного вихідного тексту, або сам видавав помилку роботи та зависав. Тільки наприкінці весни вийшов реліз із високою працездатністю. Він теж не позбавлений помилок, але їх стало значно менше, і вони зазвичай виникають на бічних напрямках, коли намагаються запрограмувати щось на межі коректності. Наприклад, працюють із типом uchar4, який задає 4-байтову чотирикомпонентну змінну. Цей тип є в специфікаціях OpenCL, але працювати з ним на Radeon не варто, бо реєстри 128-бітні: ті ж чотири компоненти, але 32-бітні. А така змінна uchar4 все одно займе цілий регістр, тільки ще будуть потрібні додаткові операції упаковки та доступу до окремих байтових компонентів. Компілятор не повинен мати жодних помилок, але компіляторів без недоліків не буває. Навіть Intel Compiler після 11 версій має помилки компіляції. Виявлені помилки виправлені у наступному релізі, який вийде ближче до осені.

Але є ще безліч речей, які потребують доопрацювання. Наприклад, стандартний GPU-драйвер для Radeon досі не має підтримки GPU-обчислень з використанням OpenCL. Користувач повинен завантажувати та встановлювати додатковий спеціальний пакет.

Але найголовніше - це відсутність будь-яких бібліотек функцій. Для речових чисел подвійної точності немає навіть синуса, косинуса та експоненти. Що ж, для додавання-множення матриць цього не потрібно, але якщо ви хочете запрограмувати щось складніше, треба писати всі функції з нуля. Або чекати на новий реліз SDK. Незабаром має вийти ACML (AMD Core Math Library) для GPU-родини Evergreen з підтримкою основних матричних функцій.

На даний момент, на думку автора статті, є реальним для програмування відеокарт Radeonбачиться використання API Direct Compute 5.0, зважаючи на обмеження: орієнтацію на платформу Windows 7 і Windows Vista. Microsoft має великий досвід у створенні компіляторів, і можна очікувати повністю працездатний реліз дуже скоро, Microsoft безпосередньо в цьому зацікавлена. Але Direct Compute орієнтований на потреби інтерактивних додатків: щось порахувати і відразу візуалізувати результат - наприклад, перебіг рідини по поверхні. Це не означає, що його не можна використовувати просто для розрахунків, але це не є його природним призначенням. Скажімо, Microsoft не планує додавати в Direct Compute бібліотечні функції - саме ті, яких немає зараз у AMD. Тобто те, що зараз можна ефективно порахувати на Radeon – деякі не надто витончені програми, – можна реалізувати і на Direct Compute, який набагато простіше OpenCL і має бути стабільнішим. Плюс, він повністю портабельний, працюватиме і на Nvidia, і на AMD, так що компілювати програму доведеться лише один раз, тоді як реалізації OpenCL SDK компаній Nvidia та AMD не зовсім сумісні. (У тому сенсі, що якщо розробити OpenCL-програму на системі AMD з використанням AMD OpenCL SDK, вона може не піти так просто на Nvidia. Можливо, потрібно компілювати той самий текст із використанням Nvidia SDK. І, зрозуміло, навпаки.)

Потім, у OpenCL багато надмірної функціональності, оскільки OpenCL задуманий як універсальна мова програмування та API широкого кола систем. І GPU, і CPU, і Cell. Так що на випадок, якщо треба просто написати програму для типової системи користувача (процесор плюс відеокарта), OpenCL не представляється, так би мовити, «високопродуктивним». Кожна функція має десять параметрів, і дев'ять з них повинні бути встановлені в 0. А для того, щоб встановити кожен параметр, треба викликати спеціальну функцію, яка теж має параметри.

І найголовніший поточний плюс Direct Compute – користувачеві не треба встановлювати спеціальний пакет: все, що необхідно, вже є у DirectX 11.

Проблеми розвитку GPU-обчислень

Якщо взяти сферу персональних комп'ютерів, Ситуація така: існує не так багато завдань, для яких потрібна велика обчислювальна потужність і сильно не вистачає звичайного двоядерного процесора. Начебто з моря на сушу вилізли великі ненажерливі, але неповороткі чудовиська, а на суші і є майже нічого. І споконвічні обителі земної поверхні зменшуються у розмірах, вчаться менше споживати, як завжди буває при дефіциті природних ресурсів. Якби зараз була така ж потреба у продуктивності, як 10-15 років тому, GPU-обчислення прийняли б на ура. А так проблеми сумісності та відносної складності GPU-програмування виходять на перший план. Краще написати програму, яка б працювала на всіх системах, ніж програму, яка працює швидко, але запускається тільки на GPU.

Дещо краще перспективи GPU з точки зору використання у професійних додатках та секторі робочих станцій, тому що там більше потреби у продуктивності. З'являються плагіни для 3D-редакторів з підтримкою GPU: наприклад, для рендерингу за допомогою трасування променів – не плутати зі звичайним GPU-рендеренгом! Щось з'являється і для 2D-редакторів та редакторів презентацій з прискоренням створення складних ефектів. Програми обробки відео також поступово мають підтримку GPU. Наведені вище завдання з огляду на свою паралельну сутність добре лягають на архітектуру GPU, але зараз створена дуже велика база коду, налагодженого, оптимізованого під всі можливості CPU, так що потрібен час, щоб з'явилися хороші GPU-реалізації.

У цьому сегменті виявляються такі слабкі сторони GPU, як обмежений обсяг відеопам'яті - приблизно в 1 ГБ для звичайних GPU. Одним з головних факторів, що знижують продуктивність GPU-програм, є необхідність обміну даними між CPU та GPU по повільній шині, а через обмежений обсяг пам'яті доводиться передавати більше даних. І тут перспективною виглядає концепція AMD щодо суміщення GPU і CPU в одному модулі: можна пожертвувати високою пропускною здатністю графічної пам'яті для легкого і простого доступу до загальної пам'яті, до того ж з меншою латентністю. Ця висока ПСП нинішньої відеопам'яті DDR5 набагато більше затребувана безпосередньо графічними програмами, Чим більшістю програм GPU-обчислень. Взагалі, загальна пам'ять GPU і CPU просто істотно розширить сферу застосування GPU, уможливить використання його обчислювальних можливостей у невеликих підзавданнях програм.

І найбільше GPU потрібні у сфері наукових обчислень. Вже збудовано кілька суперкомп'ютерів на базі GPU, які показують дуже високий результат у тесті матричних операцій. Наукові завдання такі різноманітні і численні, що завжди знаходиться безліч, яка чудово лягає на архітектуру GPU, для якого використання GPU дозволяє легко отримати високу продуктивність.

Якщо серед усіх завдань сучасних комп'ютерів вибрати одне, це буде комп'ютерна графіка- Зображення світу, в якому ми живемо. І оптимальна для цієї мети архітектура не може бути поганою. Це настільки важливе та фундаментальне завдання, що спеціально розроблене для неї залізо має нести в собі універсальність і бути оптимальним для різних завдань. Тим більше, що відеокартки успішно еволюціонують.



Сподобалась стаття? Поділіться їй