Об Intel OpenCL

Щупаю тут за вымя Intel OpenCL (для CPU) и эта игрушка мне все больше нравится.

Помимо оффлайн-компилятора (который выдает ассемблер или LLVM-код) эту хреновину можно профайлить при помощи VTune (нужно пару переменных окружения поставить, см. доку).

Беру я, значит, прямо тамошний пример MedianFilter и начинаю профайлить. Он содержит "CPU"-реализацию (в том смысле, что компилятор C++ ее превратит в обычный код) и OpenCL-вариант (который будет скомпилирован на лету). Обе реализации имеют настолько одинаковый C/C++ код внутри, насколько это вообще возможно (OpenCL-ядро - это обработка одной строки изображения, а CPU-код имеет еще цикл по всем строкам). Код, собственно, простой как пробка: берем пикселы из окрестности +-1, слегка сортируем (unrolled), вот и медиана.

И вот что я вижу:

  • CPU-вариант при компиляции Visual C++ 2008: 389 msec на исполнение. Это без processor-specific оптимизаций и без распараллеливания.
  • OpenCL-вариант: 15 msec.
В 26 раз быстрее, однако.

Ну ладно, VC++2008 - не самый новый, да и процессора моего не знает. Переключаю компилятор на Intel C++ 12-й версии (который Composer XE), включаю оптимизацию для AVX, уже лучше: 151 msec. Но тоже в 10 раз.

Нет, я все понимаю, OpenCL-реализация фигачит на всех ядрах (коих 4, в hyperthreading я не очень верю), а CPU-версия - линейная. И 4 раза так можно объяснить, ну 5 с поправкой на HT.

А остальные два раза берутся, вероятно, из разных предположений компилятора об окружающем мире: в OpenCL-случае мы уверены в куче вещей сразу:

  • Размер задачи задан снаружи и не изменится на ходу.
  • Параллельный запуск любого (большого) количества kernels на разных данных - явно предполагается, никаких неявных взаимозависимостей между потоками нет. Все синхронизации внутри группы, все атомарные действия - все написано явно.
Как следствие, хочешь - параллель SIMD, хочешь - на ядрах, хочешь оба - пожалуйста.

Ложка дегтя: помимо времени на исполнение есть еще время на компиляцию. Для ~4kb кода kernel получается 110msec по профайлеру т.е. для обрабатываемого битмепа 1024x1024 особого выигрыша нет. Для битмепа побольше он, естественно, будет.

Бочка меда: но исходник kernel можно генерировать на лету и прямо на рантайме его скомпилируют. А можно хранить скомпилированный и тогда вышеупомянутые 110msec не потратятся.

Короче, как только оно выйдет в релиз и появится нормальный redistributable - весьма рекомендую использовать для data-parallel задач. Всяко проще, чем руками выписывать SSE/AVX-код под все версии горшков.

Comments

А на ipsc того же афтара (Интел) ты не смотрел? Они там переизобрели язык С.

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

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

И он, кстати, поумнее AMD-шной реализации. Векторизовать умеет.

О да. AMD-шная для (интеловских?) CPU явно сделана для галочки.

Хотя внутре у них у всех LLVM, с чего бы не векторизовать то...

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

Мне казалось, что для LLVM это (векторизация) как-то уже есть, что-то я такое видел (правда с векторными типами т.е. это еще проще для компилятора).

Это помимо помянутого выше ipsc, который тоже векторизует и вполне неплохо (и диалект C тамошний на OpenCL достаточно похож). Может быть ipsc - это отход от производства OpenCL, конечно.

Но у интела то векторизация не внутри кода кода, а посредством объединения нескольких work-items. Если в LLVM что и есть на тему векторизации, то здесь оно никак помочь не может.

Я в компиляторах понимаю мало, но из общих соображений - это не бог весть какая задача.
OpenCL устроен так, спасибо NVidia, что для скалярных типов несколько work items можно смело на одном SIMD произвольной ширины исполнять. Ну разве что следить, чтобы на хвосте задачи, где на всю ширину не хватает данных, не было какой-то гадкой гадости.

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

> Ну то есть да, программировать надо, но никакого секса с определением "вот эти вот - не взаимозависимы, можно параллелить" - нет

Есть. Вот совсем без секса - это то, как было в Intel OpenCL Alpha. Любой if - и все, никакой векторизации ядра. В Beta явно видно, что инженеры Интел постарались. Впрочем, там явно видно, что есть куда еще улучшать.

Ветвления - да, с ними сексуально в SIMD.

Пока отвечал - вы отредактировали.

Интеловский компилятор тоже векторизовать умеет. Хотя сделал ли он это в данном случае достаточно хорошо - вопрос, который лень изучать.

Важно то, что OpenCL для того же самого кода - векторизовал с виду неплохо.

Да, прошу прощения, руки за мыслями не поспевают.

Впрочем, посмотрел.

Хреново векторизовал. Понятно откуда столько разницы.

Наверное он соседние итерации цикла не смог объединить в SIMD?
Можно ввести новую прагму для циклов типа unroll - simd_is_here_Take_it_baby!

Нет, там другое. Там вручную расписана сортировка (9 элементов) в окрестности пиксела. Это не так просто векторизовать, но можно хотя бы min/max одной инструкцией сделать.

В OpenCL-случае просто 4(8) пикселов обрабатываются параллельно.

Ну так я и говорю, что прагма simd_is_here_Take_it_baby! , подскажет компилятору, что 4(8) соседних итераций можно сделать как одну с использованием SIMD.
То есть я говорю не о векторизации simd внутри итерации, я говорю об автоматическом обьеденении нескольких итераций в simd, что-то типа OpenMP, но для simd (для простых случаев OpenCL это слишком жирно)

Ну, по идее, если оно делает циклу unroll, то дальше само уже должно придумать.

Кроме того, есть помянутый выше ispc, который ровно это и делает. Но мультитредность сам не умеет

Ну, по идее, если оно делает циклу unroll, то дальше само уже должно придумать.

всё же unroll'а не достаточно, что бы компилятор поверил в то, что итерации можно simd.

А вот ispc, да я как раз про это и говорил, спасибо за наводку! Но всё же я бы предпочёл прагмы встроенные в C++ компиляторы по типу OpenMP, чем отдельный компилятор(но хотя у этого метода тоже есть свои плюсы)

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

Не-не, C++ гнать надо из этого места. Слишком сложный язык, его усложнять не надо дальше (параллельностью).
Достаточно вспомнить про сочетание C++ и OpenMP.

И с SIMD то же самое будет и на том же самом месте. Ну хоть exceptions возьмите для примера.....

Не, я не говорю что его нужно усложнять. сам язык трогать не надо. (ох чувствую придёт C++0x через пару лет с его блэк джэком и шлюхами - будет весело).
Просто добавить прагму как extension в компилятор.
А что кстати с OpenMP не так? Мне беглого поверхностного изучения хватило для использования, и грабли ещё не встречались.
Не хотите в C++, пусть в C - для меня главное чтобы под рукой было.
Просто как-то не кайф иметь лишний dependence для сборки, ну чисто субъективно.

Претензий к бутерброду две

1) Нет способа указать "доступ" (shared/private...) для поля структуры.
2) с exceptions (изнутри параллельного блока) - бред (и если оно надо, то приходится в блоке ставить флаг, быстро из блока выходить, а потом - флаг проверять и райзить если надо)

1) int &t=mystruct.a;

Такой финт не сработает?

2) можно ловить exceptions внутри блока (то есть весь код трэда, будет завёрнут в try ()) сохранять их куда-то, и потом уже кидать их вне многонитевого кода. Конечно тут есть свои ограничения (на типы исключений), но это не вина C++. В смысле тот же ispc если и будет многонитвевым, исключений он вообще не будет давать(и опять же придётся делать то что вы описали - играться с флагами, errorState и т.п. в C-style) - в C++ хоть какие-то exceptions, но есть.

Финт может и сработает, не пробовал. Я бы на месте компилятора от алиасинга просто с ума бы сошел.

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

Проблема в том, что если вызывается что-то чужое, что может выплюнуть исключение (а это же в духе C++, особенно для библиотек, даже new может плюнуть исключением если не удалось аллоцировать), то остается только сливать воду, других вариантов нет: перехватить его внутри OpenMP блока (и поставить флаг, чтобы потом плюнуть дальше) нельзя, все валится.
Заворачивать внутренности треда в try нельзя (точнее, я пробовал и конкретно на gcc у меня не вышло)

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

> в hyperthreading я не очень верю

ХЗ
На 4х головом i7 компиляция http://svn.webkit.org/repository/webkit/trunk/ c -j4 - порядка 18 минут, с -j8 чуть больше 12 минут, а самый быстрый вариант с -j12 - около 11 минут.

Ну тут бы правильно включить HT, потом выключить HT.

Потому как непонятно какие ожидания попрятаны, но лично я ставлю на IO в первую очередь

IO - это когда с 8 до 12 потоков. Больше потоков - уже время начинает расти.
Также на 6ти головом феноме (без ГТ естественно) оптимальное число 9 потоков.
Да и вообще не понятно - IO ли это. При моих 24х гигах памяти (16ти на феноме) перенос билда на рэйд/ссд вообще не даёт никакого выигрыша - винт и так еле моргает с частотой раз в 3-4 секунды. Только сетевой диск через 100Mbps увеличивает время билда на пару минут (в основном на линковке монстрячих бинарников).

Но отключать HT я попробую - когда будет время фигней пострадать в очередной раз ;)

А не подскажите толковой литературы по OpenCL?
С английским дружу, но лучше на русском и в открытом доступе =)

На русском, как мне кажется, вообще ничего нет.
Если дружны с CUDA, то есть AMD-шный гайд по преобразованию из CUDA в OpenCL.

А в остальном
- документация соответствующих SDK (Nvidia, AMD, Intel, IBM)
- примеры оттуда же
- официальные спеки от Khronos Group

У NV есть 'OpenCL Jumpstart Guide' на 15 страницах. Ну и по 'OpenCL getting started' гугл много нашел.

Книжки всякие уже есть, но пиратки я пока не нашел, а покупать как-то обидно :).

Хорошая новость - спасибо

Бочка меда: но исходник kernel можно генерировать на лету и прямо на рантайме его скомпилируют. А можно хранить скомпилированный и тогда вышеупомянутые 110msec не потратятся.

note: "скомпилированный" - можно генерировать тем же рантаймом

Ну да, естественно.

Лёш, а могёшь эту штуковину компильнуть с заточкой по 64бит и многопоточность
и прочие интеловские феньки
https://qlandkartegt.svn.sourceforge.net/svnroot/qlandkartegt/map2jnx/tr...

то что сейчас гуляет скомпилёным похоже делалось для калькулятора
грузит проц на 10% и работает годами на относительно небольшом файле

я с компиляторами ввобще никак не дружу, ну тоесть совсем

кстати птичий глаз купил и разочаровался
наполнение его гораздо бедней гугля
в интересных для туристов местах

Тут есть моментики
1) Странно что работает "годами", я снимки размера близкого к предельному (50x50k) конвертировал за приемлемое время. Быстрее чем GlobalMapper их мне сохранял. Несколько минут на файл.
Может быть ты в GlobalMapper координаты оставляешь как есть и оно у тебя каждый пиксель пересчитывает?

2) Если оно однопоточное, то перекомпиляцией многопоточность не добавится

3) Использовать последнюю версию map2jnx у меня вовсе не получилось.

1 действую строго по инструкции с твоей страницы
http://alextutubalin.livejournal.com/240280.html?view=3042968
вот такую картинку программа судя по мелькающим циферкам планирует обрабатывать 133минуты

projection: +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
width: 57324 pixel height: 27276 pixel
area (top/left, bottom/right): 28.214454 83.981380, 27.697474 85.211420
xscale: 0.000021 ┬░/px, yscale: -0.000019 ┬░/px
real scale: 2.102486 m/px

2 вот про это ничего не скажу

3 после апдейта фирмвари прибора до 3.0, выходной файл с 0.2.4 прекрасно в приборе видится

Ну я не пробовал 57x27k, пробовал 30x30k. Там время было какое-то приемлемое, точно не два часа и не час.

Интересно, а что выдаёт clGetDeviceInfo c CL_DEVICE_LOCAL_MEM_SIZE ?
Я к тому, что локальная (shared в терминологии cuda c), вроде должна хорошо эмулироваться с помощью процессорного кэша, особенно если один блок обслуживается одним ядром.
Конечно ещё надо учитывать использование регистров - по моим оценкам для соответствия CC2.0 по регистрам, нужно 4byte*32768regs*8simd_threadsAVX/64minGPU_BlockSize=16KiB кэша,
у I7 32KiB L1 data cache, остаётся 16KiB L1 для shared, что не плохо, учитывая наличие ещё L2 и L3.

clInfo (AMD-шная от 2.3):
Local memory type: Global
Local memory size: 32768

т.е. банально кэш туда вписали. А в качестве размера кэша - L2 (256k).

Но, конечно, kernel который хочет 48k shared + 128k регистров на блок (сколько есть в CC2.0) - пролетит немножко мимо.

128k регистров на блок (сколько есть в CC2.0)

Надо учитывать, что все запрошенные регистры на блок, одновременно не нужны. На железках NVidia минимальный разумный размер блока, который выедает все регистры, это 64, поэтому я и поделил на 64minGPU_BlockSize и умножил на 8simd_threads, то есть из минимально возможных 64 потоков блока, параллельно будут исполнятся только 8(а если double, то вообще 4), то есть память под регистры для всех потоков одновременно не нужна (единственное при синхронизации внутри блока будет перетасовка - но всё в пределах L2 кэша).
Поэтому и получилось 16KiB кэша под регистры для соответствия CC2.0

Тут какбэ другая беда. Если у нас размер блока - 64, то его раскидает по 4(8) ядрам в 8-way SIMD. Отчего начнутся всякие приколы с инвалидацией кэшей (если shared мы делаем в 32к и со всех ядер туда ходим) и так далее. Будет (относительно) плохо.

Т.е. я к тому клоню, что эффективный kernel разный для разного железа и никуда от этого не деться.

А зачем раскидывать блок по разным ядрам? В задаче ведь как правило много блоков. Проще говоря, нужно относится к ядру, как к SM.
Пусть потоки блока выполняются одним ядром, что я и сказал ранее ("особенно если один блок обслуживается одним ядром")

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

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

А в невырожденном - да, надо посмотреть как работает планировщик у разных CPU OpenCL.

Да, если всего один блок, то имеет смысл.
Но сейчас ведь говорим о переносе GPU OpenCL кода (ведь про shared из-за GPU начали говорить), на CPU, а там потоков достаточно.

Интересно, а что Intel C++ с /Qparallel даст?

Конкретно на данном примере, если его пальцами не трогать и прагмы не писать - ничего.

Add new comment