Сложение векторов на GPU

Опубликовано марта 6, 2018 в Технология CUDA

То же самое сложение можно реализовать на GPU, представив add () в виде функции устройства. Это будет выглядеть примерно так, как код из предыдущей главы.

Но прежде чем перейти к коду, исполняемому устройством, рассмотрим функцию main (). Хотя ее реализация для GPU отличается от версии для CPU, ничего существенно нового здесь нет:

Сложение векторов на GPU

 

Еще раз обращаем ваше внимание на общие приемы.

  • Мы выделяем три массива в памяти устройства, обращаясь к функции cudaMalloc (): два массива, dev_a и dev_a, будут содержать исходные данные, а третий, dev_c, — результат.
  • Поскольку мы заботимся об окружающей среде, то перед уходом прибираем за собой, вызывая функцию cudaFree ().
  • С помощью функции cudaMemcpy () мы копируем входные данные в память устройства, передавая четвертым параметром константу cudaMemcpyHost-ToDevice, а результат копируем назад в память CPU, передавая константу cudaMemcpyDeviceToHost.
  • Мы вызываем функцию add (), исполняемую устройством, из функции main (), исполняемой CPU. При этом используется синтаксис с тремя угловыми скобками.

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

Пойдем дальше. Наша функция add () похожа на ту, что была написана для CPU:

И снова мы видим общий прием.

  • Мы написали функцию add (), которая исполняется устройством. Для этого мы взяли соответствующий код на С и добавили перед именем функции квалификатор _ _global_ _.

До сих пор в этом примере не было ничего нового, разве что он умеет складывать не только числа 2 и 7. Однако стоит отметить две детали: параметры в угловых скобках и код, содержащийся в ядре. То и другое — новые концепции.

Ну и что?

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

Например, если при запуске ядра мы пишем kernel«<2,1>»(), то можете считать, что исполняющая среда создает два ядра и исполняет их параллельно. Каждый параллельно выполняемый экземпляр называется блоком (block). Запись kernel<256,1>() означает, что GPU будет исполнять 256 блоков. Никогда еще параллельное программирование не было таким простым делом.

Но тогда возникает законный вопрос: GPU исполняет N копий кода ядра, но как внутри кода определить, в каком блоке он исполняется? И тут мы подходим ко второй особенности нашего примера — самому коду ядра. Точнее, переменной blockldx.x: