Традиционные процессоры специализируются на последовательном выполнении инструкций общего назначения. Напротив, графические процессоры могут одновременно выполнять тысячи инструкций. Это различие возникает из-за их соответствующих архитектур.

Графический процессор хорош для эффективного запуска большого количества потоков и параллельного выполнения большого количества потоков. Таким образом, он может выполнять ресурсоемкие задачи намного быстрее, чем ЦП. В программировании с ускорением на GPU эта возможность используется для повышения производительности пользователя.

Программирование CUDA

В модели программирования CUDA программа разделена на два основных раздела. Один раздел работает на процессоре, а другой - на графическом процессоре. Более ресурсоемкие задачи выделяются графическому процессору, а более общие задачи выполняются центральным процессором.

Программа CUDA с четырьмя основными функциями

  1. Выделение памяти CPU на GPU - cudaMalloc
  2. ЦП копирует входные данные с ЦП на графический процессор - cudaMemcpy
  3. CPU запускает ядра на GPU для обработки данных
  4. ЦП копирует результаты обратно в ЦП из графического процессора

В программе CUDA ЦП должен выполнять вышеупомянутые четыре функции. Это основная часть кода ЦП программы CUDA. Проще говоря, то, что происходит в приведенных выше четырех функциях, - это копирование данных в графический процессор для обработки, а затем обратное копирование результатов из графического процессора. Чтобы программа CUDA была эффективной, объем данных, передаваемых между процессором и графическим процессором, должен быть меньше по сравнению с объемом обработки данных, выполняемым графическим процессором.

Возведение чисел в квадрат на примере графического процессора

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

Чтобы установить и настроить CUDA и Visual Studio, обратитесь к этой документации.

В CPU для возведения массива чисел в квадрат мы можем использовать следующий код.

const int ARRAY_SIZE = 64;
void cpuCode() {
   float in[ARRAY_SIZE];
   //initialize the input array   
   for (int i = 0; i < ARRAY_SIZE; i++) {
      in[i] = float(i);
   }
   float out[ARRAY_SIZE];
   //perform the calculation
   for (int i = 0; i < ARRAY_SIZE; i++) {
      out[i] = in[i] * in[i];
   }
}

Теперь давайте посмотрим, как это сделать в программе CUDA.

  1. В качестве первого шага мы должны инициализировать массив, как и раньше.
float h_in[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; i++) {
   h_in[i] = float(i);
}

(Я использую именование переменной h_in вместо in, чтобы подразумевать, что эта переменная находится на CPU, а для переменной на GPU я использую d_in и т. Д.)

2. Затем выделите память на графическом процессоре

float * d_in;  // input array on GPU
float * d_out; // output array on GPU
const int ARRAY_BYTES = ARRAY_SIZE * sizeof(float);
//allocate memory on GPU
cudaMalloc((void **)&d_in, ARRAY_BYTES);
cudaMalloc((void**)&d_out, ARRAY_BYTES);

Аналогично malloc в программировании на C, используемом для выделения памяти, cudaMalloc используется для выделения памяти на GPU.

3. Следующим шагом является копирование входного массива из ЦП в ГП.

cudaMemcpy(d_in, h_in, ARRAY_BYTES, cudaMemcpyHostToDevice);

h_in (память на CPU) копируется в d_in (память на GPU).

4. Теперь произведем расчет на графическом процессоре.

square << <1, ARRAY_SIZE >> >  (d_out, d_in); 

«Квадрат» - это имя функции, которую мы выполняем на графическом процессоре. Эта функция описана ниже.

‹--------------------------------…. ›› - это оператор запуска CUDA, который указывает выполнение на графическом процессоре. Принимает два параметра; количество выделяемых блоков и количество потоков в одном блоке. В модели программирования CUDA блоки и потоки графического процессора структурированы как трехмерные векторы.

Настоящая форма этого оператора - ‹** dim3 (x, y, z), dim3 (x, y, z) ›››. Но в нашей программе мы выполняем линейную задачу. (мы не работаем с 2-м массивом), и поэтому он упрощен до ‹-------------------------------- dim3 (1,1,1), dim3 (64,1,1) ›››.

В этой инструкции CPU просит GPU запустить квадратную функцию на одном блоке GPU с 64 потоками. В каждом потоке графический процессор будет выполнять квадратную функцию.

квадратная функция, как показано ниже,

__global__ void square(float * d_out, float * d_in) {
int idx = threadIdx.x;
float f = d_in[idx];
d_out[idx] = f*f;
}

В функции квадрата нет циклов. Но вместо этого он выполняет только один единственный расчет. Переменная threadIdx.x дает текущую позицию потока по оси x. Мы используем эту переменную, чтобы определить, какую операцию возведения в квадрат должен выполнить конкретный поток.

64 операции возведения в квадрат, которые мы выполняли последовательно в CPU, выполняются одновременно в 64 потоках GPU.

5. Последний шаг - это копирование вывода из GPU в CPU.

cudaMemcpy(h_out, d_out, ARRAY_BYTES, cudaMemcpyDeviceToHost);

Полный исходный код программы доступен здесь