Домой / Офис / Майнинг на видеокарте GPU – полное руководство. Эффективное использование GPU Почему у меня нет GPU в диспетчере задач

Майнинг на видеокарте GPU – полное руководство. Эффективное использование GPU Почему у меня нет GPU в диспетчере задач

Разработчику следует научиться эффективно использовать графический процессор устройства (GPU), чтобы приложение не тормозило и не выполняло лишнюю работу.

Настроить параметры GPU визуализации

Если ваше приложение тормозит, значит часть или все кадры обновления экрана обновляются больше чем 16 миллисекунд. Чтобы визуально увидеть обновления кадров на экране, можно на устройстве включить специальную опцию (Profile GPU Rendering).

У вас появится возможность быстро увидеть, сколько времени занимает отрисовка кадров. Напомню, что нужно укладываться в 16 миллисекунд.

Опция доступна на устройствах, начиная с Android 4.1. На устройстве следует активировать режим разработчика. На устройствах с версией 4.2 и выше режим по умолчанию скрыт. Для активации идёт в Настройки | О телефоне и семь раз щёлкаем по строке Номер сборки .

После активации заходим в Опции разработчика и находим пункт Настроить параметры GPU визуализации (Profile GPU rendering), который следует включить. В всплывающим окне выберите опцию На экране в виде столбиков (On screen as bars). В этом случае график будет выводиться поверх запущенного приложения.

Вы можете протестировать не только своё приложение, но и другие. Запустите любое приложение и начинайте работать с ним. Во время работы в нижней части экрана вы увидите обновляемый график. Горизонтальная ось отвечает за прошедшее время. Вертикальная ось показывает время для каждого кадра в миллисекундах. При взаимодействии с приложением, вертикальные полосы рисуются на экране, появляясь слева направо, показывая производительность кадров в течение какого-то времени. Каждый такой столбец представляет собой один кадр для отрисовки экрана. Чем выше высота столбика, тем больше времени уходит на отрисовку. Тонкая зелёная линия является ориентиром и соответствует 16 миллисекундам за кадр. Таким образом, вам нужно стремиться к тому, чтобы при изучении вашего приложения график не выбивался за эту линию.

Рассмотрим увеличенную версию графика.

Зелёная линия отвечает за 16 миллисекунд. Чтобы уложиться в 60 кадров в секунду, каждый столбец графика должен рисоваться ниже этой линии. В каких-то моментах столбец окажется слишком большим и будет гораздо выше зелёной линии. Это означает торможение программы. Каждый столбец имеет голубой, фиолетовый (Lollipop и выше), красный и оранжевый цвета.

Голубой цвет отвечает за время, используемое на создание и обновление View .

Фиолетовая часть представляет собой время, затраченное на передачу ресурсов рендеринга потока.

Красный цвет представляет собой время для отрисовки.

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

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

Отладить показатель GPU overdraw

Другая настройка позволяет узнать, как часто перерисовывается один и тот же участок экрана (т.е. выполняется лишняя работа). Опять идём в Опции разработчика и находим пункт Отладить показатель GPU overdraw (Debug GPU Overdraw), который следует включить. В всплывающим окне выберите опцию Показывать зоны наложения (Show overdraw areas). Не пугайтесь! Нкоторые элементы на экране изменят свой цвет.

Вернитесь в любое приложение и понаблюдайте за его работой. Цвет подскажет проблемные участки вашего приложения.

Если цвет в приложении не изменился, значит всё отлично. Нет наложения одного цвета поверх другого.

Голубой цвет показывает, что один слой рисуется поверх нижнего слоя. Хорошо.

Зелёный цвет - перерисовывается дважды. Нужно задуматься об оптимизации.

Розовый цвет - перерисовывается трижды. Всё очень плохо.

Красный цвет - перерисовывается много раз. Что-то пошло не так.

Вы можете самостоятельно проверить своё приложение для поиска проблемных мест. Создайте активность и поместите на неё компонент TextView . Присвойте корневому элементу и текстовой метке какой-нибудь фон в атрибуте android:background . У вас получится следующее: сначала вы закрасили одним цветом самый нижний слой активности. Затем поверх неё рисуется новый слой от TextView . Кстати, на самом TextView рисуется ещё и текст.

В каких-то моментах наложения цветов не избежать. Но представьте себе, что вы таким же образом установили фон для списка ListView , который занимает все площадь активности. Система будет выполнять двойную работу, хотя нижний слой активности пользователь никогда не увидит. А если вдобавок вы создадите ещё и собственную разметку для каждого элемента списка со своим фоном, то вообще получите перебор.

Маленький совет. Поместите после метода setContentView() вызов метода, который уберёт перирисовку экрана цветом темы. Это поможет убрать одно лишнее наложение цвета:

GetWindow().setBackgroundDrawable(null);

Использование 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() , мы исключаем копирование result из памяти обычного процессора в память ускорителя, но эти данные будут копироваться в обратном направлении.

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

Параметр лямбда-функции, index<1> объекта, представляет одномерный индекс. Он должен соответствовать используемому объекту extent - если бы мы объявили объект extent двумерным (например, определив форму исходных данных в виде двумерной матрицы), индекс также должен был бы быть двумерным. Пример такой ситуации приводится чуть ниже.

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

На этом мы заканчиваем наше первое знакомство с миром 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); float invDistCube = invDist*invDist*invDist; acceleration + = dist*PARTICLE_MASS*invDistCube; }

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

Struct particle { float4 position, velocity; // реализации конструктора, конструктора копирования и // оператора = с restrict(amp) опущены для экономии места }; void 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; ElementIdx

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

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

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

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

Вычисления на графических процессорах

Технология CUDA (англ. Compute Unified Device Architecture) - программно-аппаратная архитектура, позволяющая производить вычисления с использованием графических процессоров NVIDIA, поддерживающих технологию GPGPU (произвольных вычислений на видеокартах). Архитектура CUDA впервые появились на рынке с выходом чипа NVIDIA восьмого поколения - G80 и присутствует во всех последующих сериях графических чипов, которые используются в семействах ускорителей GeForce, ION, Quadro и Tesla.

CUDA SDK позволяет программистам реализовывать на специальном упрощённом диалекте языка программирования Си алгоритмы, выполнимые на графических процессорах NVIDIA и включать специальные функции в текст программы на Cи. CUDA даёт разработчику возможность по своему усмотрению организовывать доступ к набору инструкций графического ускорителя и управлять его памятью, организовывать на нём сложные параллельные вычисления.

История

В 2003 г. Intel и AMD участвовали в совместной гонке за самый мощный процессор. За несколько лет в результате этой гонки тактовые частоты существенно выросли, особенно после выхода Intel Pentium 4.

После прироста тактовых частот (между 2001 и 2003 гг. тактовая частота Pentium 4 удвоилась с 1,5 до 3 ГГц), а пользователям пришлось довольствоваться десятыми долями гигагерц, которые вывели на рынок производители (с 2003 до 2005 гг.тактовые частоты увеличились 3 до 3,8 ГГц).

Архитектуры, оптимизированные под высокие тактовые частоты, та же Prescott, так же стали испытывать трудности, и не только производственные. Производители чипов столкнулись с проблемами преодоления законов физики. Некоторые аналитики даже предрекали, что закон Мура перестанет действовать. Но этого не произошло. Оригинальный смысл закона часто искажают, однако он касается числа транзисторов на поверхности кремниевого ядра. Долгое время повышение числа транзисторов в CPU сопровождалось соответствующим ростом производительности - что и привело к искажению смысла. Но затем ситуация усложнилась. Разработчики архитектуры CPU подошли к закону сокращения прироста: число транзисторов, которое требовалось добавить для нужного увеличения производительности, становилось всё большим, заводя в тупик.

Причина, по которой производителям GPU не столкнулись с этой проблемой очень простая: центральные процессоры разрабатываются для получения максимальной производительности на потоке инструкций, которые обрабатывают разные данные (как целые числа, так и числа с плавающей запятой), производят случайный доступ к памяти и т.д. До сих пор разработчики пытаются обеспечить больший параллелизм инструкций - то есть выполнять как можно большее число инструкций параллельно. Так, например, с Pentium появилось суперскалярное выполнение, когда при некоторых условиях можно было выполнять две инструкции за такт. Pentium Pro получил внеочередное выполнение инструкций, позволившее оптимизировать работу вычислительных блоков. Проблема заключается в том, что у параллельного выполнения последовательного потока инструкций есть очевидные ограничения, поэтому слепое повышение числа вычислительных блоков не даёт выигрыша, поскольку большую часть времени они всё равно будут простаивать.

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

GPU отличается от CPU не только этим. Доступ к памяти в GPU очень связанный - если считывается тексель, то через несколько тактов будет считываться соседний тексель; когда записывается пиксель, то через несколько тактов будет записываться соседний. Разумно организуя память, можно получить производительность, близкую к теоретической пропускной способности. Это означает, что GPU, в отличие от CPU, не требуется огромного кэша, поскольку его роль заключается в ускорении операций текстурирования. Всё, что нужно, это несколько килобайт, содержащих несколько текселей, используемых в билинейных и трилинейных фильтрах.

Первые расчёты на GPU

Самые первые попытки такого применения ограничивались использованием некоторых аппаратных функций, таких, как растеризация и Z-буферизация. Но в нынешнем веке, с появлением шейдеров, начали ускорять вычисления матриц. В 2003 г. на SIGGRAPH отдельная секция была выделена под вычисления на GPU, и она получила название GPGPU (General-Purpose computation on GPU) - универсальные вычисления на GPU).

Наиболее известен BrookGPU - компилятор потокового языка программирования Brook, созданный для выполнения неграфических вычислений на GPU. До его появления разработчики, использующие возможности видеочипов для вычислений, выбирали один из двух распространённых API: Direct3D или OpenGL. Это серьёзно ограничивало применение GPU, ведь в 3D графике используются шейдеры и текстуры, о которых специалисты по параллельному программированию знать не обязаны, они используют потоки и ядра. Brook смог помочь в облегчении их задачи. Эти потоковые расширения к языку C, разработанные в Стэндфордском университете, скрывали от программистов трёхмерный API, и представляли видеочип в виде параллельного сопроцессора. Компилятор обрабатывал файл.br с кодом C++ и расширениями, производя код, привязанный к библиотеке с поддержкой DirectX, OpenGL или x86.

Появление Brook вызвал интерес у NVIDIA и ATI и в дальнейшем, открыл целый новый его сектор - параллельные вычислители на основе видеочипов.

В дальнейшем, некоторые исследователи из проекта Brook перешли в команду разработчиков NVIDIA, чтобы представить программно-аппаратную стратегию параллельных вычислений, открыв новую долю рынка. И главным преимуществом этой инициативы NVIDIA стало то, что разработчики отлично знают все возможности своих GPU до мелочей, и в использовании графического API нет необходимости, а работать с аппаратным обеспечением можно напрямую при помощи драйвера. Результатом усилий этой команды стала NVIDIA CUDA.

Области применения параллельных расчётов на GPU

При переносе вычислений на GPU, во многих задачах достигается ускорение в 5-30 раз, по сравнению с быстрыми универсальными процессорами. Самые большие цифры (порядка 100-кратного ускорения и даже более!) достигаются на коде, который не очень хорошо подходит для расчётов при помощи блоков SSE, но вполне удобен для GPU.

Это лишь некоторые примеры ускорений синтетического кода на GPU против SSE-векторизованного кода на CPU (по данным NVIDIA):

Флуоресцентная микроскопия: 12x.

Молекулярная динамика (non-bonded force calc): 8-16x;

Электростатика (прямое и многоуровневое суммирование Кулона): 40-120x и 7x.

Таблица, которую NVIDIA, показывает на всех презентациях, в которой показывается скорость графических процессоров относительно центральных.

Перечень основных приложений, в которых применяются вычисления на GPU: анализ и обработка изображений и сигналов, симуляция физики, вычислительная математика, вычислительная биология, финансовые расчёты, базы данных, динамика газов и жидкостей, криптография, адаптивная лучевая терапия, астрономия, обработка звука, биоинформатика, биологические симуляции, компьютерное зрение, анализ данных (data mining), цифровое кино и телевидение, электромагнитные симуляции, геоинформационные системы, военные применения, горное планирование, молекулярная динамика, магнитно-резонансная томография (MRI), нейросети, океанографические исследования, физика частиц, симуляция свёртывания молекул белка, квантовая химия, трассировка лучей, визуализация, радары, гидродинамическое моделирование (reservoir simulation), искусственный интеллект, анализ спутниковых данных, сейсмическая разведка, хирургия, ультразвук, видеоконференции.

Преимущества и ограничения CUDA

С точки зрения программиста, графический конвейер является набором стадий обработки. Блок геометрии генерирует треугольники, а блок растеризации - пиксели, отображаемые на мониторе. Традиционная модель программирования GPGPU выглядит следующим образом:

Чтобы перенести вычисления на GPU в рамках такой модели, нужен специальный подход. Даже поэлементное сложение двух векторов потребует отрисовки фигуры на экране или во внеэкранный буфер. Фигура растеризуется, цвет каждого пикселя вычисляется по заданной программе (пиксельному шейдеру). Программа считывает входные данные из текстур для каждого пикселя, складывает их и записывает в выходной буфер. И все эти многочисленные операции нужны для того, что в обычном языке программирования записывается одним оператором!

Поэтому, применение GPGPU для вычислений общего назначения имеет ограничение в виде слишком большой сложности обучения разработчиков. Да и других ограничений достаточно, ведь пиксельный шейдер - это всего лишь формула зависимости итогового цвета пикселя от его координаты, а язык пиксельных шейдеров - язык записи этих формул с Си-подобным синтаксисом. Ранние методы GPGPU являются хитрым трюком, позволяющим использовать мощность GPU, но без всякого удобства. Данные там представлены изображениями (текстурами), а алгоритм - процессом растеризации. Нужно особо отметить и весьма специфичную модель памяти и исполнения.

Программно-аппаратная архитектура для вычислений на GPU компании NVIDIA отличается от предыдущих моделей GPGPU тем, что позволяет писать программы для GPU на настоящем языке Си со стандартным синтаксисом, указателями и необходимостью в минимуме расширений для доступа к вычислительным ресурсам видеочипов. CUDA не зависит от графических API, и обладает некоторыми особенностями, предназначенными специально для вычислений общего назначения.

Преимущества CUDA перед традиционным подходом к GPGPU вычислениям

CUDA обеспечивает доступ к разделяемой между потоками памяти размером в 16 Кб на мультипроцессор, которая может быть использована для организации кэша с широкой полосой пропускания, по сравнению с текстурными выборками;

Более эффективная передача данных между системной и видеопамятью;

Отсутствие необходимости в графических API с избыточностью и накладными расходами;

Линейная адресация памяти, и gather и scatter, возможность записи по произвольным адресам;

Аппаратная поддержка целочисленных и битовых операций.

Основные ограничения CUDA:

Отсутствие поддержки рекурсии для выполняемых функций;

Минимальная ширина блока в 32 потока;

Закрытая архитектура CUDA, принадлежащая NVIDIA.

Слабыми местами программирования при помощи предыдущих методов GPGPU является то, что эти методы не используют блоки исполнения вершинных шейдеров в предыдущих неунифицированных архитектурах, данные хранятся в текстурах, а выводятся во внеэкранный буфер, а многопроходные алгоритмы используют пиксельные шейдерные блоки. В ограничения GPGPU можно включить: недостаточно эффективное использование аппаратных возможностей, ограничения полосой пропускания памяти, отсутствие операции scatter (только gather), обязательное использование графического API.

Основные преимущества CUDA по сравнению с предыдущими методами GPGPU вытекают из того, что эта архитектура спроектирована для эффективного использования неграфических вычислений на GPU и использует язык программирования C, не требуя переноса алгоритмов в удобный для концепции графического конвейера вид. CUDA предлагает новый путь вычислений на GPU, не использующий графические API, предлагающий произвольный доступ к памяти (scatter или gather). Такая архитектура лишена недостатков GPGPU и использует все исполнительные блоки, а также расширяет возможности за счёт целочисленной математики и операций битового сдвига.

CUDA открывает некоторые аппаратные возможности, недоступные из графических API, такие как разделяемая память. Это память небольшого объёма (16 килобайт на мультипроцессор), к которой имеют доступ блоки потоков. Она позволяет кэшировать наиболее часто используемые данные и может обеспечить более высокую скорость, по сравнению с использованием текстурных выборок для этой задачи. Что, в свою очередь, снижает чувствительность к пропускной способности параллельных алгоритмов во многих приложениях. Например, это полезно для линейной алгебры, быстрого преобразования Фурье и фильтров обработки изображений.

Удобнее в CUDA и доступ к памяти. Программный код в графических API выводит данные в виде 32-х значений с плавающей точкой одинарной точности (RGBA значения одновременно в восемь render target) в заранее предопределённые области, а CUDA поддерживает scatter запись - неограниченное число записей по любому адресу. Такие преимущества делают возможным выполнение на GPU некоторых алгоритмов, которые невозможно эффективно реализовать при помощи методов GPGPU, основанных на графических API.

Также, графические API в обязательном порядке хранят данные в текстурах, что требует предварительной упаковки больших массивов в текстуры, что усложняет алгоритм и заставляет использовать специальную адресацию. А CUDA позволяет читать данные по любому адресу. Ещё одним преимуществом CUDA является оптимизированный обмен данными между CPU и GPU. А для разработчиков, желающих получить доступ к низкому уровню (например, при написании другого языка программирования), CUDA предлагает возможность низкоуровневого программирования на ассемблере.

Недостатки CUDA

Один из немногочисленных недостатков CUDA - слабая переносимость. Эта архитектура работает только на видеочипах этой компании, да ещё и не на всех, а начиная с серии GeForce 8 и 9 и соответствующих Quadro, ION и Tesla. NVIDIA приводит цифру в 90 миллионов CUDA-совместимых видеочипов.

Альтернативы CUDA

Фреймворк для написания компьютерных программ, связанных с параллельными вычислениями на различных графических и центральных процессорах. В фреймворк OpenCL входят язык программирования, который базируется на стандарте C99, и интерфейс программирования приложений (API). OpenCL обеспечивает параллелизм на уровне инструкций и на уровне данных и является реализацией техники GPGPU. OpenCL является полностью открытым стандартом, его использование не облагается лицензионными отчислениями.

Цель OpenCL состоит в том, чтобы дополнить OpenGL и OpenAL, которые являются открытыми отраслевыми стандартами для трёхмерной компьютерной графики и звука, пользуясь возможностями GPU. OpenCL разрабатывается и поддерживается некоммерческим консорциумом Khronos Group, в который входят много крупных компаний, включая Apple, AMD, Intel, nVidia, Sun Microsystems, Sony Computer Entertainment и другие.

CAL/IL(Compute Abstraction Layer/Intermediate Language)

ATI Stream Technology - это набор аппаратных и программных технологий, которые позволяют использовать графические процессоры AMD, совместно с центральным процессором, для ускорения многих приложений (не только графических).

Областями применения ATI Stream являются приложения, требовательные к вычислительному ресурсу, такие, как финансовый анализ или обработка сейсмических данных. Использование потокового процессора позволило увеличить скорость некоторых финансовых расчётов в 55 раз по сравнению с решением той же задачи силами только центрального процессора.

Технологию ATI Stream в NVIDIA не считают очень сильным конкурентом. CUDA и Stream - это две разные технологии, которые стоят на различных уровнях развития. Программирование для продуктов ATI намного сложнее - их язык скорее напоминает ассемблер. CUDA C, в свою очередь, гораздо более высокоуровневый язык. Писать на нём удобнее и проще. Для крупных компаний-разработчиков это очень важно. Если говорить о производительности, то можно заметить, что её пиковое значение в продуктах ATI выше, чем в решениях NVIDIA. Но опять всё сводится к тому, как эту мощность получить.

DirectX11 (DirectCompute)

Интерфейс программирования приложений, который входит в состав DirectX - набора API от Microsoft, который предназначен для работы на IBM PC-совместимых компьютерах под управлением операционных систем семейства Microsoft Windows. DirectCompute предназначен для выполнения вычислений общего назначения на графических процессорах, являясь реализацией концепции GPGPU. Изначально DirectCompute был опубликован в составе DirectX 11, однако позже стал доступен и для DirectX 10 и DirectX 10.1.

NVDIA CUDA в российской научной среде.

По состоянию на декабрь 2009 г., программная модель CUDA преподается в 269 университетах мира. В России обучающие курсы по CUDA читаются в Московском, Санкт-Петербургском, Казанском, Новосибирском и Пермском государственных университетах, Международном университете природы общества и человека "Дубна", Объединённом институте ядерных исследований, Московском институте электронной техники, Ивановском государственном энергетическом университете, БГТУ им. В. Г. Шухова, МГТУ им. Баумана, РХТУ им. Менделеева, Российском научном центре "Курчатовский институт", Межрегиональном суперкомпьютерном центре РАН, Таганрогском технологическом институте (ТТИ ЮФУ).

Как-то раз довелось мне побеседовать на компьютерном рынке с техническим директором одной из многочисленных компаний, занимающихся продажами ноутбуков. Этот «специалист» пытался с пеной у рта объяснить, какая именно конфигурация ноутбука мне нужна. Главный посыл его монолога заключался в том, что время центральных процессоров (CPU) закончилось, и сейчас все приложения активно используют вычисления на графическом процессоре (GPU), а потому производительность ноутбука целиком и полностью зависит от графического процессора, а на CPU можно не обращать никакого внимания. Поняв, что спорить и пытаться вразумить этого технического директора абсолютно бессмысленно, я не стал терять времени зря и купил нужный мне ноутбук в другом павильоне. Однако сам факт такой вопиющей некомпетентности продавца меня поразил. Было бы понятно, если бы он пытался обмануть меня, как покупателя. Отнюдь. Он искренне верил в то, что говорил. Да, видимо, маркетологи в NVIDIA и AMD не зря едят свой хлеб, и им-таки удалось внушить некоторым пользователям идею о доминирующей роли графического процессора в современном компьютере.

Тот факт, что сегодня вычисления на графическом процессоре (GPU) становятся всё более популярными, не вызывает сомнения. Однако это отнюдь не принижает роли центрального процессора. Более того, если говорить о подавляющем большинстве пользовательских приложений, то на сегодняшний день их производительность целиком и полностью зависит от производительности CPU. То есть подавляющее количество пользовательских приложений не используют вычисления на GPU.

Вообще, вычисления на GPU главным образом выполняются на специализированных HPC-системах для научных расчетов. А вот пользовательские приложения, в которых применяются вычисления на GPU, можно пересчитать по пальцам. При этом следует сразу же оговориться, что термин «вычисления на GPU» в данном случае не вполне корректен и может ввести в заблуждение. Дело в том, что если приложение использует вычисление на GPU, то это вовсе не означает, что центральный процессор бездействует. Вычисление на GPU не предполагает переноса нагрузки с центрального процессора на графический. Как правило, центральный процессор при этом остается загруженным, а использование графического процессора, наряду с центральным, позволяет повысить производительность, то есть сократить время выполнения задачи. Причем сам GPU здесь выступает в роли своеобразного сопроцессора для CPU, но ни в коем случае не заменяет его полностью.

Чтобы разобраться, почему вычисления на GPU не являются эдакой панацеей и почему некорректно утверждать, что их вычислительные возможности превосходят возможности CPU, необходимо уяснить разницу между центральным и графическим процессором.

Различия в архитектурах GPU и CPU

Ядра CPU проектируются для выполнения одного потока последовательных инструкций с максимальной производительностью, а GPU - для быстрого исполнения очень большого числа параллельно выполняемых потоков инструкций. В этом и заключается принципиальное отличие графических процессоров от центральных. CPU представляет собой универсальный процессор или процессор общего назначения, оптимизированный для достижения высокой производительности единственного потока команд, обрабатывающего и целые числа, и числа с плавающей точкой. При этом доступ к памяти с данными и инструкциями происходит преимущественно случайным образом.

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

Графический процессор устроен принципиально иначе. Он изначально проектировался для выполнения огромного количества параллельных потоков команд. Причем эти потоки команд распараллелены изначально, и никаких накладных расходов на распараллеливание инструкций в графическом процессоре просто нет. Графический процессор предназначен для визуализации изображения. Если говорить упрощенно, то на входе он принимает группу полигонов, проводит все необходимые операции и на выходе выдает пикселы. Обработка полигонов и пикселов независима, их можно обрабатывать параллельно, отдельно друг от друга. Поэтому из-за изначально параллельной организации работы в GPU используется большое количество исполнительных блоков, которые легко загрузить, в отличие от последовательного потока инструкций для CPU.

Графические и центральные процессоры различаются и по принципам доступа к памяти. В GPU доступ к памяти легко предсказуем: если из памяти читается тексель текстуры, то через некоторое время придет срок и для соседних текселей. При записи происходит то же самое: если какой­то пиксел записывается во фрейм­буфер, то через несколько тактов будет записываться пиксел, расположенный рядом с ним. Поэтому GPU, в отличие от CPU, просто не нужна кэш­память большого размера, а для текстур требуются лишь несколько килобайт. Различен и принцип работы с памятью у GPU и CPU. Так, все современные GPU имеют несколько контроллеров памяти, да и сама графическая память более быстрая, поэтому графические процессоры имеют гораздо бо льшую пропускную способность памяти, по сравнению с универсальными процессорами, что также весьма важно для параллельных расчетов, оперирующих огромными потоками данных.

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

Сами же исполнительные блоки занимают в универсальном процессоре относительно немного места.

В графическом процессоре, наоборот, основную площадь занимают именно многочисленные исполнительные блоки, что позволяет ему одновременно обрабатывать несколько тысяч потоков команд.

Можно сказать, что, в отличие от современных CPU, графические процессоры предназначены для параллельных вычислений с большим количеством арифметических операций.

Использовать вычислительную мощь графических процессоров для неграфических задач возможно, но только в том случае, если решаемая задача допускает возможность распараллеливания алгоритмов на сотни исполнительных блоков, имеющихся в GPU. В частности, выполнение расчетов на GPU показывает отличные результаты в случае, когда одна и та же последовательность математических операций применяется к большому объему данных. При этом лучшие результаты достигаются, если отношение числа арифметических инструкций к числу обращений к памяти достаточно велико. Эта операция предъявляет меньшие требования к управлению исполнением и не нуждается в использовании емкой кэш­памяти.

Можно привести множество примеров научных расчетов, где преимущество GPU над CPU в плане эффективности вычислений неоспоримо. Так, множество научных приложений по молекулярному моделированию, газовой динамике, динамике жидкостей и прочему отлично приспособлено для расчетов на GPU.

Итак, если алгоритм решения задачи может быть распараллелен на тысячи отдельных потоков, то эффективность решения такой задачи с применением GPU может быть выше, чем ее решение средствами только процессора общего назначения. Однако нельзя так просто взять и перенести решение какой­то задачи с CPU на GPU, хотя бы просто потому, что CPU и GPU используют разные команды. То есть когда программа пишется под решение на CPU, то применяется набор команд х86 (или набор команд, совместимый с конкретной архитектурой процессора), а вот для графического процессора используются уже совсем другие наборы команд, которые опять-таки учитывают его архитектуру и возможности. При разработке современных 3D-игр применяются API DirectX и OрenGL, позволяющие программистам работать с шейдерами и текстурами. Однако использование API DirectX и OрenGL для неграфических вычислений на графическом процессоре - это не лучший вариант.

NVIDIA CUDA и AMD APP

Именно поэтому, когда стали предприниматься первые попытки реализовать неграфические вычисления на GPU (General Purpose GPU, GPGPU), возник компилятор BrookGPU. До его создания разработчикам приходилось получать доступ к ресурсам видеокарты через графические API OpenGL или Direct3D, что значительно усложняло процесс программирования, так как требовало специфических знаний - приходилось изучать принципы работы с 3D-объектами (шейдерами, текстурами и т.п.). Это явилось причиной весьма ограниченного применения GPGPU в программных продуктах. BrookGPU стал своеобразным «переводчиком». Эти потоковые расширения к языку Си скрывали от программистов трехмерный API и при его использовании надобность в знаниях 3D-программирования практически отпала. Вычислительные мощности видеокарт стали доступны программистам в виде дополнительного сопроцессора для параллельных расчетов. Компилятор BrookGPU обрабатывал файл с кодом Cи и расширениями, выстраивая код, привязанный к библиотеке с поддержкой DirectX или OpenGL.

Во многом благодаря BrookGPU, компании NVIDIA и ATI (ныне AMD) обратили внимание на зарождающуюся технологию вычислений общего назначения на графических процессорах и начали разработку собственных реализаций, обеспечивающих прямой и более прозрачный доступ к вычислительным блокам 3D-ускорителей.

В результате компания NVIDIA разработала программно-аппаратную архитектуру параллельных вычислений CUDA (Compute Unified Device Architecture). Архитектура CUDA позволяет реализовать неграфические вычисления на графических процессорах NVIDIA.

Релиз публичной бета-версии CUDA SDK состоялся в феврале 2007 года. В основе API CUDA лежит упрощенный диалект языка Си. Архитектура CUDA SDK обеспечивает программистам реализацию алгоритмов, выполнимых на графических процессорах NVIDIA, и включение специальных функций в текст программы на Cи. Для успешной трансляции кода на этом языке в состав CUDA SDK входит собственный Си­компилятор командной строки nvcc компании NVIDIA.

CUDA - это кроссплатформенное программное обеспечение для таких операционных систем, как Linux, Mac OS X и Windows.

Компания AMD (ATI) также разработала свою версию технологии GPGPU, которая ранее называлась AТI Stream, а теперь - AMD Accelerated Parallel Processing (APP). Основу AMD APP составляет открытый индустриальный стандарт OpenCL (Open Computing Language). Стандарт OpenCL обеспечивает параллелизм на уровне инструкций и на уровне данных и является реализацией техники GPGPU. Это полностью открытый стандарт, его использование не облагается лицензионными отчислениями. Отметим, что AMD APP и NVIDIA CUDA несовместимы друг с другом, тем не менее, последняя версия NVIDIA CUDA поддерживает и OpenCL.

Тестирование GPGPU в видеоконвертерах

Итак, мы выяснили, что для реализации GPGPU на графических процессорах NVIDIA предназначена технология CUDA, а на графических процессорах AMD - API APP. Как уже отмечалось, использование неграфических вычислений на GPU целесообразно только в том случае, если решаемая задача может быть распараллелена на множество потоков. Однако большинство пользовательских приложений не удовлетворяют этому критерию. Впрочем, есть и некоторые исключения. К примеру, большинство современных видеоконвертеров поддерживают возможность использования вычислений на графических процессорах NVIDIA и AMD.

Для того чтобы выяснить, насколько эффективно используются вычисления на GPU в пользовательских видеоконвертерах, мы отобрали три популярных решения: Xilisoft Video Converter Ultimate 7.7.2, Wondershare Video Converter Ultimate 6.0.3.2 и Movavi Video Converter 10.2.1. Эти конвертеры поддерживают возможность использования графических процессоров NVIDIA и AMD, причем в настройках видеоконвертеров можно отключить эту возможность, что позволяет оценить эффективность применения GPU.

Для видеоконвертирования мы применяли три различных видеоролика.

Первый видеоролик имел длительность 3 мин 35 с и размер 1,05 Гбайт. Он был записан в формате хранения данных (контейнер) mkv и имел следующие характеристики:

  • видео:
    • формат - MPEG4 Video (H264),
    • разрешение - 1920*um*1080,
    • режим битрейта - Variable,
    • средний видеобитрейт - 42,1 Мбит/с,
    • максимальный видеобитрейт - 59,1 Мбит/с,
    • частота кадров - 25 fps;
  • аудио:
    • формат - MPEG-1 Audio,
    • аудиобитрейт - 128 Кбит/с,
    • количество каналов - 2,

Второй видеоролик имел длительность 4 мин 25 с и размер 1,98 Гбайт. Он был записан в формате хранения данных (контейнер) MPG и имел следующие характеристики:

  • видео:
    • формат - MPEG-PS (MPEG2 Video),
    • разрешение - 1920*um*1080,
    • режим битрейта - Variable.
    • средний видеобитрейт - 62,5 Мбит/с,
    • максимальный видеобитрейт - 100 Мбит/с,
    • частота кадров - 25 fps;
  • аудио:
    • формат - MPEG-1 Audio,
    • аудиобитрейт - 384 Кбит/с,
    • количество каналов - 2,

Третий видеоролик имел длительность 3 мин 47 с и размер 197 Мбайт. Он был записан в формате хранения данных (контейнер) MOV и имел следующие характеристики:

  • видео:
    • формат - MPEG4 Video (H264),
    • разрешение - 1920*um*1080,
    • режим битрейта - Variable,
    • видеобитрейт - 7024 Кбит/с,
    • частота кадров - 25 fps;
  • аудио:
    • формат - AAC,
    • аудиобитрейт - 256 Кбит/с,
    • количество каналов - 2,
    • частота семплирования - 48 кГц.

Все три тестовых видеоролика конвертировались с использованием видеоконвертеров в формат хранения данных MP4 (кодек H.264) для просмотра на планшете iPad 2. Разрешение выходного видеофайла составляло 1280*um*720.

Отметим, что мы не стали использовать абсолютно одинаковые настройки конвертирования во всех трех конвертерах. Именно поэтому по времени конвертирования некорректно сравнивать эффективность самих видеоконвертеров. Так, в видеоконвертере Xilisoft Video Converter Ultimate 7.7.2 для конвертирования применялся пресет iPad 2 - H.264 HD Video. В этом пресете используются следующие настройки кодирования:

  • кодек - MPEG4 (H.264);
  • разрешение - 1280*um*720;
  • частота кадров - 29,97 fps;
  • видеобитрейт - 5210 Кбит/с;
  • аудиокодек - AAC;
  • аудиобитрейт - 128 Кбит/с;
  • количество каналов - 2;
  • частота семплирования - 48 кГц.

В видеоконвертере Wondershare Video Converter Ultimate 6.0.3.2 использовался пресет iPad 2 cо следующими дополнительными настройками:

  • кодек - MPEG4 (H.264);
  • разрешение - 1280*um*720;
  • частота кадров - 30 fps;
  • видеобитрейт - 5000 Кбит/с;
  • аудиокодек - AAC;
  • аудиобитрейт - 128 Кбит/с;
  • количество каналов - 2;
  • частота семплирования - 48 кГц.

В конвертере Movavi Video Converter 10.2.1 применялся пресет iPad (1280*um*720, H.264) (*.mp4) со следующими дополнительными настройками:

  • видеоформат - H.264;
  • разрешение - 1280*um*720;
  • частота кадров - 30 fps;
  • видеобитрейт - 2500 Кбит/с;
  • аудиокодек - AAC;
  • аудиобитрейт - 128 Кбит/с;
  • количество каналов - 2;
  • частота семплирования - 44,1 кГц.

Конвертирование каждого исходного видеоролика проводилось по пять раз на каждом из видеоконвертеров, причем с использованием как графического процессора, так и только CPU. После каждого конвертирования компьютер перезагружался.

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

Конфигурация стенда для тестирования

Стенд для тестирования имел следующую конфигурацию:

  • процессор - Intel Core i7-3770K;
  • материнская плата - Gigabyte GA-Z77X-UD5H;
  • чипсет системной платы - Intel Z77 Express;
  • память - DDR3-1600;
  • объем памяти - 8 Гбайт (два модуля GEIL по 4 Гбайт);
  • режим работы памяти - двухканальный;
  • видеокарта - NVIDIA GeForce GTX 660Ti (видеодрайвер 314.07);
  • накопитель - Intel SSD 520 (240 Гбайт).

На стенде устанавливалась операционная система Windows 7 Ultimate (64-bit).

Первоначально мы провели тестирование в штатном режиме работы процессора и всех остальных компонентов системы. При этом процессор Intel Core i7-3770K работал на штатной частоте 3,5 ГГц c активированным режимом Turbo Boost (максимальная частота процессора в режиме Turbo Boost составляет 3,9 ГГц).

Затем мы повторили процесс тестирования, но при разгоне процессора до фиксированной частоты 4,5 ГГц (без использования режима Turbo Boost). Это позволило выявить зависимость скорости конвертирования от частоты процессора (CPU).

На следующем этапе тестирования мы вернулись к штатным настройкам процессора и повторили тестирование уже с другими видеокартами:

  • NVIDIA GeForce GTX 280 (драйвер 314.07);
  • NVIDIA GeForce GTX 460 (драйвер 314.07);
  • AMD Radeon HD6850 (драйвер 13.1).

Таким образом, видеоконвертирование проводилось на четырех видеокартах различной архитектуры.

Старшая видеокарта NVIDIA GeForce 660Ti основана на одноименном графическом процессоре с кодовым обозначением GK104 (архитектура Kepler), производимом по 28-нм техпроцессу. Этот графический процессор содержит 3,54 млрд транзисторов, а площадь кристалла составляет 294 мм2.

Напомним, что графический процессор GK104 включает четыре кластера графической обработки (Graphics Processing Clusters, GPC). Кластеры GPC являются независимыми устройствами в составе процессора и способны работать как отдельные устройства, поскольку обладают всеми необходимыми ресурсами: растеризаторами, геометрическими движками и текстурными модулями.

Каждый такой кластер имеет два потоковых мультипроцессора SMX (Streaming Multiprocessor), но в процессоре GK104 в одном из кластеров один мультипроцессор заблокирован, поэтому всего имеется семь мультипроцессоров SMX.

Каждый потоковый мультипроцессор SMX содержит 192 потоковых вычислительных ядра (ядра CUDA), поэтому в совокупности процессор GK104 насчитывает 1344 вычислительных ядра CUDA. Кроме того, каждый SMX-мультипроцессор содержит 16 текстурных модулей (TMU), 32 блока специальных функций (Special Function Units, SFU), 32 блока загрузки и хранения (Load-Store Unit, LSU), движок PolyMorph и многое другое.

Видеокарта GeForce GTX 460 основана на графическом процессоре с кодовым обозначением GF104 на базе архитектуры Fermi. Этот процессор производится по 40-нм техпроцессу и содержит порядка 1,95 млрд транзисторов.

Графический процессор GF104 включает два кластера графической обработки GPC. Каждый из них имеет четыре потоковых мультипроцессора SM, но в процессоре GF104 в одном из кластеров один мультипроцессор заблокирован, поэтому существует всего семь мультипроцессоров SM.

Каждый потоковый мультипроцессор SM содержит 48 потоковых вычислительных ядра (ядра CUDA), поэтому в совокупности процессор GK104 насчитывает 336 вычислительных ядра CUDA. Кроме того, каждый SM-мультипроцессор содержит восемь текстурных модулей (TMU), восемь блоков специальных функций (Special Function Units, SFU), 16 блоков загрузки и хранения (Load-Store Unit, LSU), движок PolyMorph и многое другое.

Графический процессор GeForce GTX 280 относится ко второму поколению унифицированной архитектуры графических процессоров NVIDIA и по своей архитектуре сильно отличается от архитектуры Fermi и Kepler.

Графический процессор GeForce GTX 280 состоит из кластеров обработки текстур (Texture Processing Clusters, TPC), которые, хоть и похожи, но в то же время сильно отличаются от кластеров графической обработки GPC в архитектурах Fermi и Kepler. Всего таких кластеров в процессоре GeForce GTX 280 насчитывается десять. Каждый TPC-кластер включает три потоковых мультипроцессора SM и восемь блоков текстурной выборки и фильтрации (TMU). Каждый мультипроцессор состоит из восьми потоковых процессоров (SP). Мультипроцессоры также содержат блоки выборки и фильтрации текстурных данных, используемых как в графических, так и в некоторых расчетных задачах.

Таким образом, в одном TPC-кластере - 24 потоковых процессора, а в графическом процессоре GeForce GTX 280 их уже 240.

Сводные характеристики используемых в тестировании видеокарт на графических процессорах NVIDIA представлены в таблице .

В приведенной таблице нет видеокарты AMD Radeon HD6850, что вполне естественно, поскольку по техническим характеристикам ее трудно сравнивать с видеокартами NVIDIA. А потому рассмотрим ее отдельно.

Графический процессор AMD Radeon HD6850, имеющий кодовое наименование Barts, изготовляется по 40-нм техпроцессу и содержит 1,7 млрд транзисторов.

Архитектура процессора AMD Radeon HD6850 представляет собой унифицированную архитектуру с массивом общих процессоров для потоковой обработки многочисленных видов данных.

Процессор AMD Radeon HD6850 состоит из 12 SIMD-ядер, каждое из которых содержит по 16 блоков суперскалярных потоковых процессоров и четыре текстурных блока. Каждый суперскалярный потоковый процессор содержит пять универсальных потоковых процессоров. Таким образом, всего в графическом процессоре AMD Radeon HD6850 насчитывается 12*um*16*um*5=960 универсальных потоковых процессоров.

Частота графического процессора видеокарты AMD Radeon HD6850 составляет 775 МГц, а эффективная частота памяти GDDR5 - 4000 МГц. При этом объем памяти составляет 1024 Мбайт.

Результаты тестирования

Итак, давайте обратимся к результатам тестирования. Начнем с первого теста, когда используется видеокарта NVIDIA GeForce GTX 660Ti и штатный режим работы процессора Intel Core i7-3770K.

На рис. 1-3 показаны результаты конвертирования трех тестовых видеороликов тремя конвертерами в режимах с применением графического процессора и без.

Как видно по результатам тестирования, эффект от использования графического процессора налицо. Для видеоконвертера Xilisoft Video Converter Ultimate 7.7.2 в случае применения графического процессора время конвертирования сокращается на 14, 9 и 19% для первого, второго и третьего видеоролика соответственно.

Для видеоконвертера Wondershare Video Converter Ultimate 6.0.32 использование графического процессора позволяет сократить время конвертирования на 10, 13 и 23% для первого, второго и третьего видеоролика соответственно.

Но более всех от применения графического процессора выигрывает конвертер Movavi Video Converter 10.2.1. Для первого, второго и третьего видеоролика сокращение времени конвертирования составляет 64, 81 и 41% соответственно.

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

Теперь посмотрим, каков будет выигрыш по времени конвертирования при разгоне процессора Intel Core i7-3770K до частоты 4,5 ГГц. Если считать, что в штатном режиме все ядра процессора при конвертировании загружены и в режиме Turbo Boost работают на частоте 3,7 ГГц, то увеличение частоты до 4,5 ГГц соответствует разгону по частоте на 22%.

На рис. 4-6 показаны результаты конвертирования трех тестовых видеороликов при разгоне процессора в режимах с использованием графического процессора и без. В данном случае применение графического процессора позволяет получить выигрыш по времени конвертирования.

Для видеоконвертера Xilisoft Video Converter Ultimate 7.7.2 в случае применения графического процессора время конвертирования сокращается на 15, 9 и 20% для первого, второго и третьего видеоролика соответственно.

Для видеоконвертера Wondershare Video Converter Ultimate 6.0.32 использование графического процессора позволяет сократить время конвертирования на 10, 10 и 20% для первого, второго и третьего видеоролика соответственно.

Для конвертера Movavi Video Converter 10.2.1 применение графического процессора позволяет сократить время конвертирования на 59, 81 и 40% соответственно.

Естественно, интересно посмотреть, как разгон процессора позволяет уменьшить время конвертирования при использовании графического процессора и без него.

На рис. 7-9 представлены результаты сравнения времени конвертирования видеороликов без использования графического процессора в штатном режиме работы процессора и в режиме разгона. Поскольку в данном случае конвертирование проводится только средствами CPU без вычислений на GPU, очевидно, что увеличение тактовой частоты работы процессора приводит к сокращению времени конвертирования (увеличению скорости конвертирования). Столь же очевидно, что сокращение скорости конвертирования должно быть примерно одинаково для всех тестовых видеороликов. Так, для видеоконвертера Xilisoft Video Converter Ultimate 7.7.2 при разгоне процессора время конвертирования сокращается на 9, 11 и 9% для первого, второго и третьего видеоролика соответственно. Для видеоконвертера Wondershare Video Converter Ultimate 6.0.32 время конвертирования сокращается на 9, 9 и 10% для первого, второго и третьего видеоролика соответственно. Ну а для видеоконвертера Movavi Video Converter 10.2.1 время конвертирования сокращается на 13, 12 и 12% соответственно.

Таким образом, при разгоне процессора по частоте на 20% время конвертирования сокращается примерно на 10%.

Сравним время конвертирования видеороликов с использованием графического процессора в штатном режиме работы процессора и в режиме разгона (рис. 10-12).

Для видеоконвертера Xilisoft Video Converter Ultimate 7.7.2 при разгоне процессора время конвертирования сокращается на 10, 10 и 9% для первого, второго и третьего видеоролика соответственно. Для видеоконвертера Wondershare Video Converter Ultimate 6.0.32 время конвертирования сокращается на 9, 6 и 5% для первого, второго и третьего видеоролика соответственно. Ну а для видеоконвертера Movavi Video Converter 10.2.1 время конвертирования сокращается на 0,2, 10 и 10% соответственно.

Как видим, для конвертеров Xilisoft Video Converter Ultimate 7.7.2 и Wondershare Video Converter Ultimate 6.0.32 сокращение времени конвертирования при разгоне процессора примерно одинаково как при использовании графического процессора, так и без его применения, что логично, поскольку эти конвертеры не очень эффективно используют вычисления на GPU. А вот для конвертера Movavi Video Converter 10.2.1, который эффективно использует вычисления на GPU, разгон процессора в режиме использования вычислений на GPU мало сказывается на сокращении времени конвертирования, что также понятно, поскольку в данном случае основная нагрузка ложится на графический процессор.

Теперь посмотрим результаты тестирования с различными видеокартами.

Казалось бы, чем мощнее видеокарта и чем больше в графическом процессоре ядер CUDA (или универсальных потоковых процессоров для видеокарт AMD), тем эффективнее должно быть видеоконвертирование в случае применения графического процессора. Но на практике получается не совсем так.

Что касается видеокарт на графических процессорах NVIDIA, то ситуация следующая. При использовании конвертеров Xilisoft Video Converter Ultimate 7.7.2 и Wondershare Video Converter Ultimate 6.0.32 время конвертирования практически никак не зависит от типа используемой видеокарты. То есть для видеокарт NVIDIA GeForce GTX 660Ti, NVIDIA GeForce GTX 460 и NVIDIA GeForce GTX 280 в режиме использования вычислений на GPU время конвертирования получается одно и то же (рис. 13-15).

Рис. 1. Результаты конвертирования первого
тестового видеоролика в штатном режиме
работы процессора

процессора видеокартах в режиме использования графического процессора

Рис. 14. Результаты сравнения времени конвертирования второго видеоролика

Рис. 15. Результаты сравнения времени конвертирования третьего видеоролика
на различных видеокартах в режиме использования графического процессора

Объяснить это можно лишь тем, что алгоритм вычислений на графическом процессоре, реализованный в конвертерах Xilisoft Video Converter Ultimate 7.7.2 и Wondershare Video Converter Ultimate 6.0.32, просто неэффективен и не позволяет активно задействовать все графические ядра. Кстати, именно этим объясняется и тот факт, что для этих конвертеров разница по времени конвертирования в режимах использования GPU и без использования невелика.

В конвертере Movavi Video Converter 10.2.1 ситуация несколько иная. Как мы помним, этот конвертер способен очень эффективно использовать вычисления на GPU, а поэтому в режиме использования GPU время конвертирования зависит от типа используемой видеокарты.

А вот с видеокартой AMD Radeon HD 6850 всё как обычно. То ли драйвер видеокарты «кривой», то ли алгоритмы, реализованные в конвертерах, нуждаются в серьезной доработке, но в случае применения вычислений на GPU результаты либо не улучшаются, либо ухудшаются.

Если говорить более конкретно, то ситуация следующая. Для конвертера Xilisoft Video Converter Ultimate 7.7.2 при использовании графического процессора для конвертирования первого тестового видеоролика время конвертирования увеличивается на 43%, при конвертировании второго ролика - на 66%.

Причем, конвертер Xilisoft Video Converter Ultimate 7.7.2 характеризуется еще и нестабильностью результатов. Разброс по времени конвертирования может достигать 40%! Именно поэтому мы повторяли все тесты по десять раз и рассчитывали средний результат.

А вот для конвертеров Wondershare Video Converter Ultimate 6.0.32 и Movavi Video Converter 10.2.1 при использовании графического процессора для конвертирования всех трех видеороликов время конвертирования не изменяется вообще! Вероятно, что конвертеры Wondershare Video Converter Ultimate 6.0.32 и Movavi Video Converter 10.2.1 либо не используют технологию AMD APP при конвертировании, либо видеодрайвер AMD попросту «кривой», в результате чего технология AMD APP не работает.

Выводы

На основании проведенного тестирования можно сделать следующие важные выводы. В современных видеоконвертерах действительно может применяться технология вычислений на GPU, что позволяет повысить скорость конвертирования. Однако это вовсе не означает, что все вычисления целиком переносятся на GPU и CPU остается незадействованным. Как показывает тестирование, при использовании технологии GPGPU центральный процессор остается загруженным, а значит, применение мощных, многоядерных центральных процессоров в системах, используемых для конвертирования видео, остается актуальным. Исключением из этого правила является технология AMD APP на графических процессорах AMD. Например, при использовании конвертера Xilisoft Video Converter Ultimate 7.7.2 с активированной технологией AMD APP нагрузка на CPU действительно снижается, но это приводит к тому, что время конвертирования не сокращается, а, наоборот, увеличивается.

Вообще, если говорить о конвертировании видео с дополнительным использованием графического процессора, то для решения этой задачи целесообразно применять видеокарты с графическими процессорами NVIDIA. Как показывает практика, только в этом случае можно добиться увеличения скорости конвертирования. Причем нужно помнить, что реальный прирост в скорости конвертирования зависит от очень многих факторов. Это входной и выходной форматы видео, и, конечно же, сам видеоконвертер. Конвертеры Xilisoft Video Converter Ultimate 7.7.2 и Wondershare Video Converter Ultimate 6.0.32 для этой задачи подходят плохо, а вот конвертер и Movavi Video Converter 10.2.1 способен очень эффективно использовать возможности NVIDIA GPU.

Что же касается видеокарт на графических процессорах AMD, то для задач видеоконвертирования их не стоит применять вообще. В лучшем случае никакого прироста в скорости конвертирования это не даст, а в худшем - можно получить ее снижение.

Сегодня новости об использовании графических процессоров для общих вычислений можно услышать на каждом углу. Такие слова, как CUDA, Stream и OpenCL, за каких-то два года стали чуть ли не самыми цитируемыми в айтишном интернете. Однако, что значат эти слова, и что несут стоящие за ними технологии, известно далеко не каждому. А для линуксоидов, привыкших "быть в пролете", так и вообще все это видится темным лесом.

Рождение GPGPU

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

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

В каждом ядре приходилось размещать полноценный процессор сложной и запутанной x86-архитектуры, со своим (довольно объемным) кэшем, конвейером инструкций, блоками SSE, множеством блоков, выполняющих оптимизации и т.д. и т.п. Поэтому процесс наращивания количества ядер существенно затормозился, и белые университетские халаты, которым два или четыре ядра было явно мало, нашли способ задействовать для своих научных расчетов другие вычислительные мощности, которых было в достатке на видеокарте (в результате даже появился инструмент BrookGPU, эмулирующий дополнительный процессор с помощью вызовов функций DirectX и OpenGL).

Графические процессоры, лишенные многих недостатков центрального процессора, оказались отличной и очень быстрой счетной машинкой, и совсем скоро к наработкам ученых умов начали присматриваться сами производители GPU (а nVidia так и вообще наняла большинство исследователей на работу). В результате появилась технология nVidia CUDA, определяющая интерфейс, с помощью которого стало возможным перенести вычисление сложных алгоритмов на плечи GPU без каких-либо костылей. Позже за ней последовала ATi (AMD) с собственным вариантом технологии под названием Close to Metal (ныне Stream), а совсем скоро появилась ставшая стандартом версия от Apple, получившая имя OpenCL.

GPU — наше все?

Несмотря на все преимущества, техника GPGPU имеет несколько проблем. Первая из них заключается в очень узкой сфере применения. GPU шагнули далеко вперед центрального процессора в плане наращивания вычислительной мощности и общего количества ядер (видеокарты несут на себе вычислительный блок, состоящий из более чем сотни ядер), однако такая высокая плотность достигается за счет максимального упрощения дизайна самого чипа.

В сущности основная задача GPU сводится к математическим расчетам с помощью простых алгоритмов, получающих на вход не очень большие объемы предсказуемых данных. По этой причине ядра GPU имеют очень простой дизайн, мизерные объемы кэша и скромный набор инструкций, что в конечном счете и выливается в дешевизну их производства и возможность очень плотного размещения на чипе. GPU похожи на китайскую фабрику с тысячами рабочих. Какие-то простые вещи они делают достаточно хорошо (а главное — быстро и дешево), но если доверить им сборку самолета, то в результате получится максимум дельтаплан.

Поэтому первое ограничение GPU — это ориентированность на быстрые математические расчеты, что ограничивает сферу применения графических процессоров помощью в работе мультимедийных приложений, а также любых программ, занимающихся сложной обработкой данных (например, архиваторов или систем шифрования, а также софтин, занимающихся флуоресцентной микроскопией, молекулярной динамикой, электростатикой и другими, малоинтересными для линуксоидов вещами).

Вторая проблема GPGPU в том, что адаптировать для выполнения на GPU можно далеко не каждый алгоритм. Отдельно взятые ядра графического процессора довольно медлительны, и их мощь проявляется только при работе сообща. А это значит, что алгоритм будет настолько эффективным, насколько эффективно его сможет распараллелить программист. В большинстве случаев с такой работой может справиться только хороший математик, которых среди разработчиков софта совсем немного.

И третье: графические процессоры работают с памятью, установленной на самой видеокарте, так что при каждом задействовании GPU будет происходить две дополнительных операции копирования: входные данные из оперативной памяти самого приложения и выходные данные из GRAM обратно в память приложения. Нетрудно догадаться, что это может свести на нет весь выигрыш во времени работы приложения (как и происходит в случае с инструментом FlacCL, который мы рассмотрим позже).

Но и это еще не все. Несмотря на существование общепризнанного стандарта в лице OpenCL, многие программисты до сих пор предпочитают использовать привязанные к производителю реализации техники GPGPU. Особенно популярной оказалась CUDA, которая хоть и дает более гибкий интерфейс программирования (кстати, OpenCL в драйверах nVidia реализован поверх CUDA), но намертво привязывает приложение к видеокартам одного производителя.

KGPU или ядро Linux, ускоренное GPU

Исследователи из университета Юты разработали систему KGPU, позволяющую выполнять некоторые функции ядра Linux на графическом процессоре с помощью фреймворка CUDA. Для выполнения этой задачи используется модифицированное ядро Linux и специальный демон, который работает в пространстве пользователя, слушает запросы ядра и передает их драйверу видеокарты с помощью библиотеки CUDA. Интересно, что несмотря на существенный оверхед, который создает такая архитектура, авторам KGPU удалось создать реализацию алгоритма AES, который поднимает скорость шифрования файловой системы eCryptfs в 6 раз.

Что есть сейчас?

В силу своей молодости, а также благодаря описанным выше проблемам, GPGPU так и не стала по-настоящему распространенной технологией, однако полезный софт, использующий ее возможности, существует (хоть и в мизерном количестве). Одними из первых появились крэкеры различных хэшей, алгоритмы работы которых очень легко распараллелить.

Также родились мультимедийные приложения, например, кодировщик FlacCL, позволяющий перекодировать звуковую дорожку в формат FLAC. Поддержкой GPGPU обзавелись и некоторые уже существовавшие ранее приложения, самым заметным из которых стал ImageMagick, который теперь умеет перекладывать часть своей работы на графический процессор с помощью OpenCL. Также есть проекты по переводу на CUDA/OpenCL (не любят юниксоиды ATi) архиваторов данных и других систем сжатия информации. Наиболее интересные из этих проектов мы рассмотрим в следующих разделах статьи, а пока попробуем разобраться с тем, что нам нужно для того, чтобы все это завелось и стабильно работало.

GPU уже давно обогнали x86-процессоры в производительности

· Во-вторых, в систему должны быть установлены последние проприетарные драйвера для видеокарты, они обеспечат поддержку как родных для карточки технологий GPGPU, так и открытого OpenCL.

· И в-третьих, так как пока дистрибутивостроители еще не начали распространять пакеты приложений с поддержкой GPGPU, нам придется собирать приложения самостоятельно, а для этого нужны официальные SDK от производителей: CUDA Toolkit или ATI Stream SDK. Они содержат в себе необходимые для сборки приложений заголовочные файлы и библиотеки.

Ставим CUDA Toolkit

Идем по вышеприведенной ссылке и скачиваем CUDA Toolkit для Linux (выбрать можно из нескольких версий, для дистрибутивов Fedora, RHEL, Ubuntu и SUSE, есть версии как для архитектуры x86, так и для x86_64). Кроме того, там же надо скачать комплекты драйверов для разработчиков (Developer Drivers for Linux, они идут первыми в списке).

Запускаем инсталлятор SDK:

$ sudo sh cudatoolkit_4.0.17_linux_64_ubuntu10.10.run

Когда установка будет завершена, приступаем к установке драйверов. Для этого завершаем работу X-сервера:

# sudo /etc/init.d/gdm stop

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

$ sudo sh devdriver_4.0_linux_64_270.41.19.run

После окончания установки стартуем иксы:

Чтобы приложения смогли работать с CUDA/OpenCL, прописываем путь до каталога с CUDA-библиотеками в переменную LD_LIBRARY_PATH:

$ export LD_LIBRARY_PATH=/usr/local/cuda/lib64

Или, если ты установил 32-битную версию:

$ export LD_LIBRARY_PATH=/usr/local/cuda/lib32

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

$ export C_INCLUDE_PATH=/usr/local/cuda/include

Все, теперь можно приступить к сборке CUDA/OpenCL-софта.

Ставим ATI Stream SDK

Stream SDK не требует установки, поэтому скачанный с сайта AMD-архив можно просто распаковать в любой каталог (лучшим выбором будет /opt) и прописать путь до него во всю ту же переменную LD_LIBRARY_PATH:

$ wget http://goo.gl/CNCNo

$ sudo tar -xzf ~/AMD-APP-SDK-v2.4-lnx64.tgz -C /opt

$ export LD_LIBRARY_PATH=/opt/AMD-APP-SDK-v2.4-lnx64/lib/x86_64/

$ export C_INCLUDE_PATH=/opt/AMD-APP-SDK-v2.4-lnx64/include/

Как и в случае с CUDA Toolkit, x86_64 необходимо заменить на x86 в 32-битных системах. Теперь переходим в корневой каталог и распаковываем архив icd-registration.tgz (это своего рода бесплатный лицензионный ключ):

$ sudo tar -xzf /opt/AMD-APP-SDK-v2.4-lnx64/icd-registration.tgz - С /

Проверяем правильность установки/работы пакета с помощью инструмента clinfo:

$ /opt/AMD-APP-SDK-v2.4-lnx64/bin/x86_64/clinfo

ImageMagick и OpenCL

Поддержка OpenCL появилась в ImageMagick уже достаточно давно, однако по умолчанию она не активирована ни в одном дистрибутиве. Поэтому нам придется собрать IM самостоятельно из исходников. Ничего сложного в этом нет, все необходимое уже есть в SDK, поэтому сборка не потребует установки каких-то дополнительных библиотек от nVidia или AMD. Итак, скачиваем/распаковываем архив с исходниками:

$ wget http://goo.gl/F6VYV

$ tar -xjf ImageMagick-6.7.0-0.tar.bz2

$ cd ImageMagick-6.7.0-0

$ sudo apt-get install build-essential

Запускаем конфигуратор и грепаем его вывод на предмет поддержки OpenCL:

$ LDFLAGS=-L$LD_LIBRARY_PATH ./confi gure | grep -e cl.h -e OpenCL

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

checking CL/cl.h usability... yes

checking CL/cl.h presence... yes

checking for CL/cl.h... yes

checking OpenCL/cl.h usability... no

checking OpenCL/cl.h presence... no

checking for OpenCL/cl.h... no

checking for OpenCL library... -lOpenCL

Словом "yes" должны быть отмечены либо первые три строки, либо вторые (или оба варианта сразу). Если это не так, значит, скорее всего, была неправильно инициализирована переменная C_INCLUDE_PATH. Если же словом "no" отмечена последняя строка, значит, дело в переменной LD_LIBRARY_PATH. Если все окей, запускаем процесс сборки/установки:

$ sudo make install clean

Проверяем, что ImageMagick действительно был скомпилирован с поддержкой OpenCL:

$ /usr/local/bin/convert -version | grep Features

Features: OpenMP OpenCL

Теперь измерим полученный выигрыш в скорости. Разработчики ImageMagick рекомендуют использовать для этого фильтр convolve:

$ time /usr/bin/convert image.jpg -convolve "-1, -1, -1, -1, 9, -1, -1, -1, -1" image2.jpg

$ time /usr/local/bin/convert image.jpg -convolve "-1, -1, -1, -1, 9, -1, -1, -1, -1" image2.jpg

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

FlacCL (Flacuda)

FlacCL — это кодировщик звуковых файлов в формат FLAC, задействующий в своей работе возможности OpenCL. Он входит в состав пакета CUETools для Windows, но благодаря mono может быть использован и в Linux. Для получения архива с кодировщиком выполняем следующую команду:

$ mkdir flaccl && cd flaccl

$ wget www.cuetools.net/install/flaccl03.rar

$ sudo apt-get install unrar mono

$ unrar x fl accl03.rar

Чтобы программа смогла найти библиотеку OpenCL, делаем символическую ссылку:

$ ln -s $LD_LIBRARY_PATH/libOpenCL.so libopencl.so

Теперь запускаем кодировщик:

$ mono CUETools.FLACCL.cmd.exe music.wav

Если на экран будет выведено сообщение об ошибке "Error: Requested compile size is bigger than the required workgroup size of 32", значит, у нас в системе слишком слабенькая видеокарта, и количество задействованных ядер следует сократить до указанного числа с помощью флага ‘--group-size XX’, где XX — нужное количество ядер.

Сразу скажу, из-за долгого времени инициализации OpenCL заметный выигрыш можно получить только на достаточно длинных дорожках. Короткие звуковые файлы FlacCL обрабатывает почти с той же скоростью, что и его традиционная версия.

oclHashcat или брутфорс по-быстрому

Как я уже говорил, одними из первых поддержку GPGPU в свои продукты добавили разработчики различных крэкеров и систем брутфорса паролей. Для них новая технология стала настоящим святым граалем, который позволил с легкостью перенести от природы легко распараллеливаемый код на плечи быстрых GPU-процессоров. Поэтому неудивительно, что сейчас существуют десятки самых разных реализаций подобных программ. Но в этой статье я расскажу только об одной из них — oclHashcat.

oclHashcat — это ломалка, которая умеет подбирать пароли по их хэшу с экстремально высокой скоростью, задействуя при этом мощности GPU с помощью OpenCL. Если верить замерам, опубликованным на сайте проекта, скорость подбора MD5-паролей на nVidia GTX580 составляет до 15800 млн комбинаций в секунду, благодаря чему oclHashcat способен найти средний по сложности восьмисимвольный пароль за какие-то 9 минут.

Программа поддерживает OpenCL и CUDA, алгоритмы MD5, md5($pass.$salt), md5(md5($pass)), vBulletin < v3.8.5, SHA1, sha1($pass.$salt), хэши MySQL, MD4, NTLM, Domain Cached Credentials, SHA256, поддерживает распределенный подбор паролей с задействованием мощности нескольких машин.

$ 7z x oclHashcat-0.25.7z

$ cd oclHashcat-0.25

И запустить программу (воспользуемся пробным списком хэшей и пробным словарем):

$ ./oclHashcat64.bin example.hash ?l?l?l?l example.dict

oclHashcat откроет текст пользовательского соглашения, с которым следует согласиться, набрав "YES". После этого начнется процесс перебора, прогресс которого можно узнать по нажатию . Чтобы приостановить процесс, кнопаем

Для возобновления — . Также можно использовать прямой перебор (например, от aaaaaaaa до zzzzzzzz):

$ ./oclHashcat64.bin hash.txt ?l?l?l?l ?l?l?l?l

И различные модификации словаря и метода прямого перебора, а также их комбинации (об этом можно прочитать в файле docs/examples.txt). В моем случае скорость перебора всего словаря составила 11 минут, тогда как прямой перебор (от aaaaaaaa до zzzzzzzz) длился около 40 минут. В среднем скорость работы GPU (чип RV710) составила 88,3 млн/с.

Выводы

Несмотря на множество самых разных ограничений и сложность разработки софта, GPGPU — будущее высокопроизводительных настольных компов. Но самое главное — использовать возможности этой технологии можно прямо сейчас, и это касается не только Windows-машин, но и Linux.