Что такое Куда

Собственная разработанная NVIDIA архитектура, называемая CUDA (Computer Unified Device Architecture), используется для параллельных вычислений и представляет собой модуль интерфейса прикладного программирования. Он предлагает простые API для управления устройствами и памятью и выполняет все вычисления параллельно на графических процессорах, используя сетку потоков и блоков. CUDA использует гетерогенную систему или ЦП (хост) и графический процессор, чтобы обеспечить общую память и синхронизацию потоков (устройство). Языки C/C++ поддерживаются CUDA, а для компиляции программы используется компилятор NVIDIA nvcc.
Давайте более подробно обсудим логическую иерархию CUDA, прежде чем переходить к программированию CUDA.

Зачем вам использовать GPU?

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

Терминология

Хост: Доступный ЦП системы является хостом. Память хоста — это системная память (D RAM), подключенная к хосту.
Устройства: графический процессор — это устройство, поэтому память графического процессора называется памятью устройства.
Ядро: выполняется один поток в графическом процессоре. функции ядра.
Запуск ядра: это когда ЦП сообщает графическому процессору о необходимости одновременного выполнения кода.
Конфигурация выполнения: сколько потоков выполняется на графическом процессоре и сколько их в целом.
Видеокарты NVDIA имеют модуль с плавающей запятой, называемый ядрами CUDA, который способен выполнять карты с плавающей запятой.

Рабочий процесс

Посмотрим рабочий процесс-

Перенос с хоста на устройство: память выделяется памяти устройства с помощью cudaMalloc. CudaMemcpy используется для копирования данных из памяти хоста на целевое устройство. Связь между устройством и хостом осуществляется через шину PCI.
Загрузка ядра: загрузка программы графического процессора, ее запуск и локальное кэширование данных для повышения производительности.
Перенос с устройства на хост: с помощью cudaMemory скопируйте результаты из памяти устройства в память хоста. Используя cudaFree, память стирается с оборудования.

Давайте сделаем основную программу в cuda

#include <cuda.h>


__global__ void Cuda_programming() {
    printf("Cuda programming\n");
}

int main() {
    Cuda_programming<<<1,1>>>();
    cudaDeviceSynchronize();
    return 0;
}
  • Фраза «__global__: is a» означает, что функция вызывается с хоста и выполняется на GPU (ЦП). Код ядра определен.
  • Cuda_programming‹‹‹1,1››(): Запуск ядра обозначается символом <<<M,T>>>.Для кода, содержащего символ __global__, он должен быть вызван. После запуска ядра M блоков потоков располагаются в сетке. В каждом блоке потоков имеется T параллельных потоков.
  • cudaDeviceSynchronize(): код cuda запускается асинхронно, поэтому хост может не всегда ждать, пока устройство завершит его выполнение. cudaDeviceSyncronize ожидает завершения выполнения устройства.

После выполнения кода-

$ nvcc -o Cuda programming Cuda programming.cu

Код устройства и код хоста разделены компилятором NVIDIA. Основная функция построена с помощью компилятора gcc, а код устройства: Cuda_programming создан с помощью компилятора NVDIA.

Программное обеспечение просто печатает «Программирование Cuda». Оно не демонстрирует все возможности cuda. Программирование Cuda выполняется только в этом потоке. Но давайте сначала изучим немного больше теории.

Иерархия потоков

  • Блоки: блок представляет собой группу потоков.
  • grid: коллекции блоков составляют сетки.
  • Поток — это отдельный процесс, который выполняется в ядре cuda. Единая сетка создается каждым вызовом ядра.

Встроенные переменные

  • threadIdx.x: используя внутреннюю переменную, мы можем получить индекс блока текущего потока.
  • blockId.x: у нас есть доступ к индексу текущего блока сетки.
  • blockDim.x: количество потоков в блоке можно получить с помощью встроенной переменной.
  • gridDim.x: Мы можем настроить размер сетки, используя внутреннюю переменную.

давайте возьмем пример

Распараллеливание добавления векторов с использованием многопоточности

Используя блок потоков с 256 потоками, в этом упражнении мы будем распараллеливать сложение векторов (вектор add.cu). Ниже приведена схема новой установки выполнения ядра.

vector_add <<< 1 , 256 >>> (d_out, d_a, d_b, N);

Переменные, встроенные в ядро ​​CUDA, используются для получения доступа к данным потока. Из них в этом эксперименте будут использоваться 2 threadIdx.x и blockDim.x:

threadIdx.x: этоиндекс потока в блоке.

blockDim.x: это размер блока потока (количество потоков в блоке потока).

Значение threadIdx.x: из диапазона 0–255 и равно blockDim.x: 256 для настройки вектора add().

__global__ void vector_add(float *out, float *a11, float *a12, int a13) {
    int index = 0;
    int stride = 1
    int i = index;
    while(i < n)){
        out[i] = a11[i] + a12[i];
        i += stride;
    }
}
int main() {
    float *b11, *a12, *out;
    float *b12;
    
 
    b11  = (float*)malloc(sizeof(float) * N);
    a12   = (float*)malloc(sizeof(float) * N);
    out   = (float*)malloc(sizeof(float) * N);
   
    int i=0;
    while(i<N){
        b11[i] = 0.1f;
        a12[i]   = 0.2f;
        i++;
    } 
    
    cudaMalloc((void**)&b12, sizeof(float) * N);
    cudaMemcpy(b12, b11, sizeof(float) * N, cudaMemcpyHostToDevice);    
    vector_add<<<1,256>>>(b11, a12, out, N);
    
    cudaFree(b12);

    free(b11);
    free(a12);
    free(out);
    
    return 0;
}

Конфигурация ядра потока передается через M и N. 32-битное ядро ​​выполняется через CUDA. Значение thread(threadIdx) в этом случае находится в диапазоне от 1 до 256. А 256 — это значение block(blockDim.x). Если мы установим 1,1›››, тогда и шаг, и индекс будут равны 1. Тогда код добавления вектора будет следующим:

int i=0;
while(i<0){
   ...
   i++;
}

В этой реализации все массивы перебираются одним потоком для вычисления сложения векторов. Процесс сложения потенциально может быть разделен на 256 потоков и вычисляться одновременно.

Цикл для k-го потока перебирает массив с шагом 256 с помощью цикла, начиная с k-го элемента. Например, сложение вычисляется k-м потоком элемента, который является k-м в 0-й итерации. Добавление (k+245)-элемент вычисляется по k-му с помощью итерации и так далее. Принцип показан на следующем рисунке.

Компоненты графического процессора

глобальная память: эквивалентна оперативной памяти процессора. Он доступен как для хостов, так и для устройств.

Потоковый мультипроцессор: система, которая фактически выполняет вычисления. Его ядра CUDA, говоря простым языком.

Несколько параллельных процессоров, известных как SM, находятся в графических процессорах CUDA. Каждый SM имеет несколько параллельных процессоров и может управлять несколькими параллельными блоками потоков. Используем это в своих интересах. Размер блока 256 уже установлен. Теперь необходимо найти размер сетки, которую мы разделили. по размеру блока, количеству элементов.

int BLOCKS_NUM = (N + BLOCK_SIZE — 1) / BLOCK_SIZE;
vector_add<<<BLOCKS_NUM, BLOCK_SIZE>>>(h_a, b, out, N);

Преимущества:

  • Простота в использовании: API CUDA позволяет нам использовать GPU без необходимости глубокого понимания GPU. Кроме того, учитывая, что CUDA — это просто C с расширениями NADIA. Его можно использовать для существующих кодовых баз практически без модификации кода.
  • Производительность: NVDIA разрабатывает не только оборудование, но и API. Это позволяет им точно настроить API, чтобы получить максимальную производительность от вашего графического процессора.

Недостатки:

  • Частное программное обеспечение: компания NVDIA с закрытой архитектурой является владельцем CUDA. Для запуска в системе нам требуется аппаратное обеспечение NVDIA и инструменты CUDA sdk.
  • Рекурсия возможна только в простых циклах; это трудно построить и трудно уйти с рук.