Вычисляем на видеокартах. Технология OpenCL. Часть 1. Краткая история GPGPU

22 июня автор курса «Разработчик C++» в Яндекс.Практикуме Георгий Осипов провёл вебинар «Вычисляем на видеокартах. Технология OpenCL».

Мы подготовили для вас его текстовую версию, для удобства разбив её на смысловые блоки.

  1. Зачем мы здесь собрались. Краткая история GPGPU.
  2. Пишем для OpenCL.
  3. Алгоритмы в условиях массового параллелизма.
  4. Сравнение технологий.


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

Цикл будет полезен и тем, кто уже знаком с OpenCL: в нём мы поделимся некоторыми хаками и неочевидными наблюдениями из собственного опыта.

CPU — в помойку?


В статье будем рассматривать технологию GPGPU. Разберёмся, что значат все эти буквы. Начнем с последних трёх — GPU. Все знают аббревиатуру CPU — Central Processor Unit, или центральный процессор. А GPU — Graphic Processor Unit. Это графический процессор. Он предназначен для решения графических задач.

Но перед GPU есть ещё буквы GP. Они расшифровываются как General-Purpose. В аббревиатуре опускают словосочетание Computing on. Если собрать всё вместе, получится General-Purpose Computing on Graphic Processor Unit, что по-русски — вычисления общего назначения на графическом процессоре.

image-loader.svg

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

Давайте немного отвлечёмся от видеокарты и вернёмся в 2000 год, когда процессоры CPU набирали всё больше и больше мегагерц. Компания Intel тогда прогнозировала, что к 2010 году тактовая частота одного ядра процессора достигнет 10 гигагерц. Сейчас мы знаем, что эти ожидания не сбылись.

image-loader.svg

По графику видно: рост действительно был очень хороший, но где-то в районе 2004–2005 года он резко остановился. И дальше процессоры стали даже немножко терять в мощности. Развитие центральных процессоров в итоге пошло за счёт увеличения количества ядер. Сейчас в мощных компьютерах у CPU может быть 16 ядер. Но тактовая частота ядра за последние 10 лет не росла.

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

image-loader.svg
Источник. По вертикали GFLOPS, т. е. миллиарды арифметических операций с плавающей точкой в секунду

Остаётся вопрос: в чём GPU уступают CPU? Может, дело не в процессоре, а в памяти? Для сравнения возьмём современную оперативную память DDR4 и графическую память GDDR5. Но и тут оказывается, что графический контроллер во много раз превосходит обычный CPU: доступ к графической памяти у него гораздо быстрее — 35 Гб/сек против 400 Гб/сек.

И ещё сравним какой-нибудь современный процессор и не очень современную видеокарту. Например, GeForce GTX 1080 Ti, которую сейчас можно купить только на Авито, и довольно современный процессор Core i9 с хорошей памятью.

image-loader.svg

Получается, видеокарта обгоняет процессор… И если Core i9 может сделать 460 миллиардов арифметических операций с плавающей точкой в секунду, то GTX1080Ti уже 11 триллионов. В общем, GTX намного превосходит Core i9 и по производительности, и по скорости доступа к памяти.

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

GPU хорош для массового параллелизма, когда одновременно выполняется огромное количество очень похожих вычислений. Но тут есть такие весы. На одной чаше latency — задержка, на другой флопсы — количество операций. Например, если операция длится секунду, это очень долго. Но если вы одновременно можете выполнить 100 миллиардов таких операций, у вас получились 100 миллиардов операций в секунду. Для CPU это довольно плохо, потому что нам нужно, чтобы всё работало последовательно. А для GPU — нормально. Главное — не задержка, а результат. Числа в этом примере, конечно, сильно утрированы, реальный параллелизм GPU — порядка нескольких тысяч.

Массовый параллелизм


Хороший пример задачи массового параллелизма — Ray Tracing, или трассировка лучей. В экране много пикселей, и из каждого пикселя выпускается луч, чтобы получить качественный рендер всей сцены. Эти лучи можно обрабатывать параллельно, независимо вычисляя цвет каждого пикселя экрана. Значит, это задача массового параллелизма. Существует прекрасный сайт, на котором вы можете, не отходя от браузера, попрактиковаться в написании трассировщика лучей, работающего на GPU. Там можно найти немало любопытных примеров.

Майнинг биткоинов. Чтобы намайнить биткоины, нужно вычислить много хешей и найти среди них обладающий определёнными свойствами. И у этой задачи, к сожалению, или к счастью, нет существенно лучшего решения, чем просто вычислять хеши, пока вам наконец не повезёт. Хеши считаются независимо, и это можно делать параллельно. Значит, это задача массового параллелизма. Майнинг прекрасно ложится на видеокарту.

Обучение нейросетей. Основная затратная операция, которая используется в Deep Learning, — свёртка изображений. Это вычислительно сложная операция, ведь для подсчёта одного значения свёртки с ядром 5⨯5 нужно произвести 25 умножений и 24 сложения. Радует одно: все значения можно считать независимо. А значит, это задача массового параллелизма.

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

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

image-loader.svg

Здесь приведены усреднённые данные. На блок L1/Local memory пока не смотрите, разберём его чуть позже. Видно, что CPU может иметь производительность в 20 гигафлопс, а GPU — в 5000 гигафлопс. Канал памяти у GPU гораздо выше. Но есть одно узкое местечко — шина PCI-E. Она соединяет RAM и GRAM и пропускает всего 8 Гб. Глядя на эту картинку, попробуйте ответить на вопрос: есть ли смысл решать все задачи массового параллелизма с помощью графического процессора?

Предположим, задача — сложить два массива чисел по 10 миллиардов: первое складываем с первым, второе со вторым и так далее. Нужно получить сумму, третий набор чисел. Поручать ли такую задачу графическому процессору?

Понятно, что это задача массового параллелизма. Но есть один момент. Для того, чтобы эти числа сложить на GPU, их нужно сначала передать на GRAM по узенькому мостику в 8 Гб/сек. Видеокарта посчитает всё очень быстро, это факт. Но потом вам нужно будет эти данные передавать по мостику обратно. И окажется, что гораздо быстрее просто передать по большему каналу в CPU, и он своими жалкими 20 гигафлопсами сложит числа быстрее, чем если бы они ехали по шине в GPU.

Правда, уже есть некоторые решения этой проблемы. Технология NVLink позволяет ускорить передачу данных в видеопамять с 8 до 40 Гб/сек. Более того, у некоторых дешёвых видеокарт нет видеопамяти, и они могут обращаться напрямую к RAM. Но мы не будем затрагивать этот момент.

Краткая история GPGPU


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

До появления OpenCL возникла очень популярная технология, отличающаяся только одной буквой, — OpenGL. Она решала графические задачи, нужные в первую очередь для видеоигр.

Я выделил три этапа развития OpenGL:

image-loader.svg

Первая версия появилась в 1994 году. В 2001 году произошло важное событие: стало возможным написание шейдеров на языке GLSL. Если раньше вы могли просить нарисовать треугольники с конкретными параметрами и текстурами, то в 2001 году появилась возможность запускать свой код на видеокарте, то есть самому писать обработчик, который будет вычислять цвет пикселя и его положение. В 2008 году разработка OpenGL перешла консорциуму Khronos Group. Запомните это название, оно вам ещё встретится.

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

Назвать OpenGL современной технологией уже нельзя. Ей на смену пришли Vulkan и Metal. И если OpenGL направлена на графику, то Vulkan и Metal более универсальны. Останавливаться на них не будем.

image-loader.svg

Как я уже сказал, если вам нужна графика, то можно либо замаскировать задачу под графическую, либо использовать специальную технологию, предназначенную для решения неграфических задач на видеокартах. Для этого есть две основных технологии — OpenCL, виновница сегодняшнего торжества, и её конкурент CUDA.

image-loader.svg

Главное их отличие друг от друга — в том, что CUDA поддерживает только видеокарты NVIDIA. Никакие другие адаптеры запустить CUDA-код не смогут. С другой стороны, в этом есть плюсы. Вы знаете, на каких видеокартах код будет запускаться, поэтому под них можно его хорошо отладить и предварительно скомпилировать.

У карт NVIDIA есть несколько версий API, и вы решаете, под какую версию API из доступных скомпилировать код CUDA. Если выбрать какую-то одну, код на более старых видеокартах не пойдёт, а на более новых не будет супероптимальным. Поэтому лучше компилировать сразу под несколько версий.

В OpenCL возможности предкомпиляции нет, потому что вы не знаете, на каком устройстве код будет запускаться. Исключения, конечно, бывают. Например, вы заключили договор с клиентом, и он говорит, что программа будет запускаться на такой-то видеокарте. Тогда вы можете скомпилировать под неё и отправлять заказчику только собранный OpenCL-код. Но для конечного пользователя так лучше не делать.

Обе эти технологии ушли или уходят с macOS, на ней остаётся Metal. Но что я хочу сказать: видеокарта одна, и бэкенд, на котором всё будет выполняться, один. А значит, любые технологии похожи между собой.

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

© Habrahabr.ru