Сколько процессорных ядер Intel в вашем компьютере? Если вы пользуетесь системой на базе Intel, то в абсолютном большинстве случаев к вашему ответу надо будет прибавить единицу. В состав почти всех процессоров Intel — от Atom и до Xeon E3, естественно, не пропуская Core, уже много лет входит интегрированное графическое ядро Intel Graphics, являющееся по сути полноценным процессором, и соответственно, способное не только показывать на экране картинки и ускорять видео, но и выполнять «обычные» вычисления общего назначения. Как это можно эффективно использовать? Смотрите под катом.
Сначала кратко поясним, почему рассчитывать на Intel GPU стоит. Конечно же, производительность CPU в системе почти всегда значительно превышает GPU, на то он и Центральный Процессор.
Но интересно заметить, что производительность интегрированных GPU Intel за последнее десятилетие в процентном отношении выросла гораздо больше, чем у CPU, и эта тенденция обязательно продолжится с появлением новых дискретных видеокарт Intel. Кроме того, GPU в силу своей архитектуры (множество векторных исполнительных устройств) гораздо лучше приспособлен к исполнению определенного типа задач – обработке изображений, то есть, фактически, к проведению любых однотипных операций над массивами данных. GPU делает это с полным внутренним распараллеливанием, тратит на это меньше энергии, чем CPU, и в некоторых случаях даже превосходит его по абсолютной скорости. Наконец, GPU и CPU могут работать в параллель, каждый над своими задачами, обеспечивая максимальную производительность и/или минимальное энергопотребление всей системы в целом.
— Ок, Intel. Мы решили использовать Intel GPU для расчетов общего назначения, как это сделать?
— Простейший путь, не требующий никаких специальных знаний в графике (шейдеров Direct3D и OpenGL) — это OpenCL.
Ядра OpenCL платформонезависимы и автоматически выполнятся на всех доступных в системе вычислительных устройствах – CPU, GPU, FPGA и т.д. Но плата за такую универсальность – далеко не максимальная возможная производительность на каждом типе устройств, и особенно — на интегрированном Intel GPU. Здесь можно привести такой пример: при исполнении на любом Intel GPU кода, транспонирующего матрицу битовых значений 16х16, преимущество в производительности «прямого программирования» Intel GPU в сравнении c OpenCL версией будет 8 раз!
Кроме того, некоторую функциональность, требуемую для реализации распространенных алгоритмов (например, «широких фильтров», использующих в одном преобразовании данные большой группы пикселей), OpenCL просто не поддерживает.
Поэтому, если вам требуется максимальная скорость на GPU иили что-то более сложное, чем независимая работа с каждым элементом массива и его ближайшими соседями, то вам поможет Intel C for Metal (ICM)– инструмент для разработки приложений, исполняемых на Intel Graphics.
ICM – добро пожаловать в кузницу!
С точки зрения производительности и функциональности ICM можно считать «ассемблером для графических карт Intel», а с точки зрения схемы и удобства использования – «аналогом OpenCL для графических карт Intel».
Много лет ICM использовался внутри Intel в разработке продуктов для обработки медиа на Intel GPU. Но в 2018 ICM был выпущен в публичный доступ, да еще и с открытым кодом!
Свое текущее имя Intel C for Metal получил несколько месяцев назад, до этого он именовался Intel C for Media (тот же акроним ICM или просто CM или даже Cm), а еще раньше — Media Development Framework (MDF). Так что, если где-то в названии компонент, в документации или в комментариях открытого кода встретятся старые названия — не пугайтесь, перед вами историческая ценность.
Итак, код ICM приложения, точно также, как в OpenCL, содержит две части: «административную», выполняемую на процессоре, и ядра, исполняемые на GPU. Неудивительно, что первая часть называется хост (host), а вторая – кернел (kernel).
Кернелы представляют собой функцию обработки заданного блока пикселей (или просто данных), пишутся на языке Intel C for Metal и компилируются в набор инструкций (ISA) Intel GPU с помощью компилятора ICM.
Хост — это своеобразный «менеджер команды кернелов», он администрирует процесс передачи данных между CPU и GPU и выполняет другую «менеджерскую работу» посредством библиотеки времени исполнения ICM Runtime и медиа-драйвера Intel GPU.
Подробная схема работы ICM выглядит так:
- ICM код хоста компилируется любым x86 C/C++ компилятором вместе со всем приложением;
- ICM код кернелов компилируется компилятором ICM в бинарный файл с некоторым общим набором инструкций (Common ISA);
- Во время исполнения этот общий набор инструкций JIT транслируется под конкретный Intel GPU;
- ICM хост вызывет ICM библиотеку времени исполнения для коммуникации с GPU и операционной системой.
Еще пара важных и полезных моментов:
- Используемые в ICM для представленияхранения данных поверхности могут разделяться с DirectX 11 и 9 (DXVA в Linux).
- GPU может брать и записывать данные как из видео-памяти, так и из системной памяти, разделяемой с CPU. В состав ICM входят специальные функции для обоих случаев передачи данных в обе стороны. При этом, системная память именно разделяемая, и реальное копирование в ней не потребуется – для этого в ICM предусмотрено так называемое нулевое копирование (zero copy).
ICM – в жерле вулкана!
Уже из самого названия «Си для железа» следует, что устройство языка соответствует внутреннему устройству графики Intel. То есть, учитывается тот факт, что код будет выполняться на нескольких десятках исполнительных устройств (Execution Unit) графической карты, каждое из которых представляет собой полностью векторный процессор, способный исполнять несколько потоков одновременно.
Сам язык ICM – это C++ с некоторыми ограничениями и расширениями. По сравнению с C++ в ICM отсутствуют … указатели, выделение памяти и статические переменные. Под запретом также рекурсивные функции. Зато присутствует явная модель векторного (SIMD) программирования: векторные типы данных – вектор, матрица и поверхность; векторные операции на этих типах данных, векторные условия if/else, независимо выполняемые для каждого элемента вектора; а также встроенные функции для доступа к хардверной фиксированной функциональности Intel GPU.
Работу с векторами, матрицами и поверхностями в реальных задачах облегчают объекты «подмножеств» – из cоответствующих базовых объектов вы можете выбирать только интересующие вас, «референсные» блоки или, как частный случай, отдельные элементы по маске.
Например, давайте посмотрим на ICM код, реализующий линейный фильтр – замену значения
RGB цвета каждого пикселя усредненным значением его и 8 соседей по картинке:
I(x,y) = [I(x-1, y-1) + I(x-1, y) + I(x-1, y+1) + I(x, y-1) + + I (x, y) + I(x, y+1) + I(x+1, y-1) + I(x+1, y) + I(x+1, y+1)] / 9 |
Если цвета (данные) в матрице расположены как R8G8B8, то вычисление с разбиением входного изображения на блоки размером 6х8 пикселей (6х24 байтовых элемента данных) будет таким:
_GENX_MAIN_ void linear(SurfaceIndex inBuf, SurfaceIndex outBuf,
uint h_pos, uint v_pos){
// объявляем входную матрицу 8x32
matrix<uchar, 8, 32> in;
// матрица результата 6x24
matrix<uchar, 6, 24> out;
matrix<float, 6, 24> m;
// считываем входную матрицу
read(inBuf h_pos*24, v_pos*6, in);
// считаем сумму элементов - соседей
m = in.select<6,1,24,1>(1,3);
m += in.select<6,1,24,1>(0,0); m += in.select<6,1,24,1>(0,3); m += in.select<6,1,24,1>(0,6);
m += in.select<6,1,24,1>(1,0); m += in.select<6,1,24,1>(1,6);
m += in.select<6,1,24,1>(2,0); m += in.select<6,1,24,1>(2,3); m += in.select<6,1,24,1>(2,6);
// считаем среднее- деление на 9 примерно соответствует * 0.111f;
out = m * 0.111f;
// сохраняем результат
write(outBuf, h_pos*24, v_pos*6, out); }
- Размер матриц задается в виде <тип данных, высота, ширина>;
- оператор select<v_size, v_stride, h_size, h_stride>(i, j) возвращает подматрицу начинающуюся с элемента (i, j), v_size показывает количество выбранных рядов, v_stride – расстояние между выбранными рядами h_size – количество выбранных столбцов, h_stride — расстояние между ними.
Обратите внимание, что размер входной матрицы 8х32 выбран потому, что хотя для вычислений значений всех пикселей в блоке 6х24 алгоритмически достаточно блока 8х30, чтение блока данных в ICM происходит не по байтам, а по 32-битным dword-элементам.
Вышеприведенный код — это, фактически, полноценный ICM-кернел. Как было сказано, он будет скомпилирован компилятором ICM в две стадии (предкомпиляция и последующая JIT трансляция). Компилятор ICM построен на основе LLVM и, при желании, может быть изучен в исходниках и собран вами самостоятельно.
А что же делает ICM-хост? Вызывает функции библиотеки времени исполнения ICM Runtime, которые:
- Создают, инициализируют и удаляют после использования GPU устройство (CmDevice), а также поверхности, содержащие пользовательские данные, используемые в кернелах (CmSurface);
- Работают с кернелами – загружают их из предкомпилированных .isa файлов, подготавливают их аргументы, указывающие на часть данных, с которыми будет работать каждый кернел;
- Создают и управляют очередью выполнения кернелов;
- Управляют схемой работы потоков, выполняющих каждый кернел на GPU;
- Управляют событиями (CmEvent ) — объектами синхронизации работы GPU и CPU;
- Передают данные между GPU и CPU, а точнее – между системной и видеопамятью;
- Сообщают об ошибках, измеряют время работы кернелов.
Простейший код хоста схематично выглядит так:
// Создаем CmDevice
cm_result_check(::CreateCmDevice(p_cm_device, version));
// Загружаем hello_world_genx.isa
std::string isa_code = isa::loadFile("hello_world_genx.isa");
// Создаем из кода isa объект CmProgram
CmProgram *p_program = nullptr;
cm_result_check(p_cm_device->LoadProgram(const_cast<char* >(isa_code.data()),isa_code.size(), p_program));
// Создаем hello_world кернел.
CmKernel *p_kernel = nullptr;
cm_result_check(p_cm_device->CreateKernel(p_program,
"hello_world",
p_kernel));
// Создаем схему потоков для исполнения каждого CmKernel
CmThreadSpace *p_thread_space = nullptr;
cm_result_check(p_cm_device->CreateThreadSpace(thread_width,
thread_height,
p_thread_space));
// Устанавливаем аргументы кернелов.
cm_result_check(p_kernel->SetKernelArg(0,
sizeof(thread_width),
&thread_width));
// Создаем CmTask – контейнер для указателей на кернелы
// Он требуется для постановки кернелов в очередь исполнения
// и добавляем в него кернелы.
CmTask *p_task = nullptr;
cm_result_check(p_cm_device->CreateTask(p_task));
cm_result_check(p_task->AddKernel(p_kernel));
// Создаем очередь
CmQueue *p_queue = nullptr;
cm_result_check(p_cm_device->CreateQueue(p_queue));
// Запускаем исполнение задачи GPU (ставим в очередь на исполнение).
CmEvent *p_event = nullptr;
cm_result_check(p_queue->Enqueue(p_task, p_event, p_thread_space));
// Ждём завершения исполнения.
cm_result_check(p_event->WaitForTaskFinished());
Как видите, ничего сложного в создании и использовании кернелов и хоста нет. Всё просто!
Единственная сложность, о которой стоит предупредить, чтобы вернуться в реальный мир: в настоящее время в доступной публично версии ICM единственная возможность отладки кернелов – это printf сообщения. Как ими корректно пользоваться, можно посмотреть в примере Hello, World.
ICM – не heavy metal!
Теперь посмотрим, как это работает на практике. Пакет разработчика ICM доступен для Windows и Linux, и для обеих операционных систем содержит Компилятор ICM, документацию и обучающие примеры использования. Подробное описание этих обучающих примеров скачивается отдельно.
Для Linux в пакет дополнительно включен Media Driver пользовательского режима для VAAPI со встроенной в него библиотекой времени исполнения ICM Runtime. Для Windows же работу с ICM выполнит обычный Intel Graphics Driver для Windows. Библиотека времени исполнения ICM Runtime включена в набор dll этого драйвера. В пакет ICM входит только линковочный .lib файл для неё. Если драйвер по какой-то причине отсутствует на вашей системе, то он загружается с сайта Intel, при этом гарантируется корректная работа ICM в драйверах, начиная с версии 15.60 — 2017 года выпуска).
Исходный код компонент можно найти здесь:
- Intel Media Driver for VAAPI and Intel C for Media Runtime: github.com/intel/media-driver
- Intel C for Media Compiler and examples: github.com/intel/cm-compiler
- Intel Graphics Compiler: github.com/intel/intel-graphics-compiler
Дальнейшее содержание этого раздела относится исключительно к Windows, но общие принципы работы c ICM применимы и под Linux.
Для «штатной» работы с ICM-пакетом потребуется Visual Studio начиная с 2015 и Cmake начиная с версии 3.2. При этом, конфигурационные и скриптовые файлы учебных примеров рассчитаны на VS 2015, для использования более новых версий VS файлы придется изучать и править пути к компонентам VS самостоятельно.
Итак, знакомимся с ICM для Windows:
- Загружаем архив;
- Распаковываем его;
- Запускаем (желательно в командной строке VS) скрипт конфигурирования окружения setupenv.bat с тремя параметрами — поколением Intel GPU (соответствующим процессору, в который встроен GPU, его можно оставить по-умолчанию: gen9), платформой компиляции: x86x64 и версией DirectX для совместного использования с ICM: dx9/dx11.
После чего можно просто построить все обучающие примеры – в папке examples это сделает скрипт build_all.bat или сгенерировать проекты для Microsoft Visual Studio – это сделает скрипт create_vs.bat с именем конкретного примера в качестве параметра.
Как можно видеть, ICМ-приложение будет представлять собой .exe файл с хостовой частью и .isa файл с соответствующей предкомпилированной GPU-частью.
В пакет ICM включены различные примеры – от простейшего Hello, World, показывающего основные принципы работы ICM, до достаточно сложного – реализации алгоритма поиска «максимального потока – минимального разреза» графа (max-flow min-cut problem), используемого в сегментации и сшивке изображений.
Все учебные примеры ICM хорошо документированы прямо в коде и уже упомянутом отдельном описании. Вникать в ICM рекомендуется именно по нему – последовательно изучая и запуская примеры, а далее – модифицируя их под свои нужды.
Для общего понимания всех существующих возможностей ICM настоятельно рекомендуется изучить «спецификацию» — описание ICM cmlangspec.html в папке documentscompilerhtmlcmlangspec.
В частности, там описан API реализованных в железе функций ICM – доступ к так называемым текстурным сэмплерам (Sampler) – механизму фильтрации изображений разного формата, а также к оценке движения (Motion Estimation) между видеокадрами и некоторым возможностям видео-аналитики.
ICM – куй железо, пока горячо!
Говоря о производительности ICM приложений, надо обязательно отметить, что учебные примеры включают в себя измерение времени своей работы, так что, запустив их на целевой системе и сравнив со своими задачами, вы можете оценить целесообразность использования для них ICM.
А общие соображения насчет производительности ICM достаточно простые:
- При выгрузке вычислений на GPU следует помнить о накладных расходах на передачу данных CPU<-> GPU и синхронизацию этих устройств. Поэтому пример типа Hello, World — не лучший кандидат на ICM-реализацию. Зато алгоритмы компьютерного зрения, AI и любой нетривиальной обработки массивов данных, особенно с изменением порядка этих данных в процессе или на выходе – это то, что надо для ICM.
- Кроме того, при проектировании ICM–кода надо обязательно учитывать внутреннее устройство GPU, то есть, желательно создавать достаточное количество (>1000) GPU потоков и загружать их все работой. При этом, хорошей идеей будет разделять изображения для обработки на небольшие блоки. Но конкретный способ разбиения, также как и выбор конкретного алгоритма обработки для достижения максимальной производительности – задача нетривиальная. Впрочем, это относится к любому способу работы с любым GPU (и CPU).
У вас есть OpenCL-код, но его производительность вас не радует? Или CUDA-код, но вы хотите работать на гораздо большем числе платформ? Тогда стоит посмотреть на ICM.
ICM – это живой и развивающийся продукт. Вы можете поучаствовать как в его использовании, так и его развитии – соответствующие репозитории на github ждут ваших коммитов. Вся необходимая для обоих процессов информация есть в данной статье и файлах readme на github. А если чего-то нет, то появится после ваших вопросов в комментариях.
Автор: Victoria Zhislina