Очень многие, пробовавшие «на вкус» технологию использования графических ускорителей CUDA/OpenCL получили не слишком хорошие результаты. Да, тесты идут и простые примеры показывают впечатляющее ускорение, но вот когда дело доходит до реальных алгоритмов, хороший результат получить очень непросто.
Как же заставить эту технологию работать?
В данной статье я постарался обобщить свой полугодовой опыт бодания с технологией OpenCL под Mandriva linux и MacOS X 10.6 на задачах сложного поиска строковых данных для биоинформатики. OpenCL был выбран т. к. для Мака он является «родной» технологией (часть маков комплектуется видеокартами AMD и CUDA под ними недоступна даже теоретически), но предлагаемые рекомендации достаточно универсальны и подходят в том числе и для NVIDIA CUDA.
Итак, что необходимо, чтобы графический ускоритель заработал?
Параллельность кода
1. Нужно вспомнить, а для тех, кто не знал — изучить приемы рефакторинга кода. Если у вас нетиповой алгоритм, будьте готовы к тому, что его придется долго терзать, прежде чем удастся правильно распараллелить. В ходе этих терзаний результаты работы программы не должны меняться, потому без хорошего тестового примера к работе можно даже не приступать. Пример должен отрабатывать не слишком долго, т. к. запускать его придется часто, но не слишком быстро, — мы должны оценивать скорость его работы. Оптимально — секунд 20.
2. Массовая параллельность на ускорителе подразумевает, что на входе и на выходе алгоритма будут массивы данных, причем размерность этих массивов не может быть меньше, чем количество нитей параллельного алгоритма. Ускоряемый алгоритм обычно выглядит как цикл for (или несколько вложенных таких циклов) каждая итерация которого не зависит от результата предыдущих. У вас именно такой алгоритм?
3. Параллельное программирование само по себе непростая штука, даже без использования графических ускорителей. Потому очень рекомендуется распараллелить свой алгоритм сначала чем-нибудь попроще, например с помощью OpenMP. Там параллельность включается одной директивой… только не забудьте, что если в цикле используются какие-то буферные переменные, то в параллельном исполнении они должны быть размножены на количество итераций или заводиться внутри цикла!
4. Чтоб не потерять десятки часов, нужно быть 100% уверенным, что хотя бы параллельная часть алгоритма программы полностью свободна от ошибок работы с памятью. Это достигается, например, с помощью valgrind. Также эта чудесная программка умеет ловить ошибки распараллеливания через OpenMP, так что лучше выловить все заранее, пока не попало на ускоритель — там инструментов намного меньше.
Учет особенностей работы ускорителя
1. Нужно понимать, что ускоритель работает с собственной памятью, объем которой ограничен, а трансфер туда-сюда довольно дорог. Конкретные цифры зависят от конкретной системы, но этот вопрос придется «держать в голове» все время. Лучше всего с графическими ускорителями работают алгоритмы, которые принимают не так много (но не меньше количества нитей!) данных, и достаточно долго их математически обрабатывают. Достаточно, но не очень долго, во многих системах установлен лимит на время максимального исполнения нити!
2. Память туда-сюда копируется неразрывными областями. Потому, например, обычные для Си N мерные массивы, организованные как массивы указателей на указатели совершенно не подходят, приходится использовать линейно организованные многомерные массивы данных.
3. Во «внутреннем» коде, исполняемом на ускорителе, нет операций выделения-освобождения памяти, ввода-вывода, невозможна рекурсия. Ввод-вывод, временные буфера обеспечиваются кодом, исполняемым на центральном процессоре.
4.«Идеальный» для ускорителя алгоритм — когда один и тот же код работает для разных данных (SIMD), когда код от них не зависит (пример — сложение векторов). Любое ветвление, любой цикл с переменным, зависящим от данных числом итераций дают значительное замедление.
Некоторые выводы
Таким образом, скорее всего для использования всей мощности GPGPU придется переписывать код, имея в голове вышеперечисленные ограничения, и далеко не факт, что это получится сразу и вообще получится. Не все задачи хорошо параллелятся по данным, увы. Но результат стоит свеч, так перевод на GPGPU задач молекулярной динамики позволил специалистам NVIDIA получить очень интересные результаты что в реалиях нашего института позволило бы считать не три месяца на суперкомпьютере в другом города, а один день на настольной машине. А для этого стоит поработать.
Использованные материалы:
NVIDIA OpenCL
Цикл статей на Хабре
OpenCL for Mac OS X