Programare GPU cu C ++

Gpu Programming With C



În acest ghid, vom explora puterea programării GPU cu C ++. Dezvoltatorii se pot aștepta la performanțe incredibile cu C ++, iar accesarea puterii fenomenale a GPU cu un limbaj de nivel scăzut poate produce unele dintre cele mai rapide calcule disponibile în prezent.

Cerințe

În timp ce orice mașină capabilă să ruleze o versiune modernă de Linux poate accepta un compilator C ++, veți avea nevoie de un GPU bazat pe NVIDIA pentru a urma împreună cu acest exercițiu. Dacă nu aveți un GPU, puteți crea o instanță alimentată de GPU în Amazon Web Services sau un alt furnizor de cloud la alegere.







Dacă alegeți o mașină fizică, asigurați-vă că aveți instalate driverele proprietare NVIDIA. Puteți găsi instrucțiuni pentru acest lucru aici: https://linuxhint.com/install-nvidia-drivers-linux/



Pe lângă driver, veți avea nevoie de setul de instrumente CUDA. În acest exemplu, vom folosi Ubuntu 16.04 LTS, dar sunt disponibile descărcări pentru majoritatea distribuțiilor majore la următoarea adresă URL: https://developer.nvidia.com/cuda-downloads



Pentru Ubuntu, ați alege descărcarea bazată pe .deb. Fișierul descărcat nu va avea în mod implicit o extensie .deb, așa că vă recomand să îl redenumiți pentru a avea un .deb la final. Apoi, puteți instala cu:





sudo dpkg -ipachet-nume.deb

Probabil vi se va solicita să instalați o cheie GPG și, dacă da, urmați instrucțiunile furnizate pentru a face acest lucru.

După ce ați făcut acest lucru, actualizați-vă depozitele:



sudo apt-get update
sudo apt-get installminuni-și

După ce ați terminat, vă recomand să reporniți pentru a vă asigura că totul este încărcat corect.

Avantajele dezvoltării GPU

CPU-urile gestionează multe intrări și ieșiri diferite și conțin o gamă largă de funcții nu numai pentru a face față unui sortiment larg de nevoi de program, ci și pentru gestionarea configurațiilor hardware variate. De asemenea, gestionează memoria, cache-ul, magistrala de sistem, segmentarea și funcționalitatea IO, făcându-le un jack al tuturor tranzacțiilor.

GPU-urile sunt opuse - conțin multe procesoare individuale care sunt axate pe funcții matematice foarte simple. Din această cauză, procesează sarcini de multe ori mai repede decât procesoarele. Specializându-se în funcții scalare (o funcție care ia una sau mai multe intrări, dar returnează doar o singură ieșire), acestea realizează performanțe extreme cu prețul unei specializări extreme.

Exemplu de cod

În exemplul de cod, adăugăm vectori împreună. Am adăugat o versiune CPU și GPU a codului pentru compararea vitezei.
gpu-example.cpp conținutul de mai jos:

#include 'cuda_runtime.h'
#include
#include
#include
#include
#include

typedefore::crono::high_resolution_clockCeas;

#define ITER 65535

// Versiunea CPU a funcției vector add
nulvector_add_cpu(int *la,int *b,int *c,intn) {
inteu;

// Adăugați elementele vectoriale a și b la vectorul c
pentru (eu= 0;eu<n; ++eu) {
c[eu] =la[eu] +b[eu];
}
}

// Versiunea GPU a funcției vector add
__global__nulvector_add_gpu(int *gpu_a,int *gpu_b,int *gpu_c,intn) {
inteu=threadIdx.X;
// Nu este necesară buclă, deoarece runtime-ul CUDA
// va fileta acest timp ITER
gpu_c[eu] =gpu_a[eu] +gpu_b[eu];
}

intprincipal() {

int *la,*b,*c;
int *gpu_a,*gpu_b,*gpu_c;

la= (int *)malloc(ITER* mărimea(int));
b= (int *)malloc(ITER* mărimea(int));
c= (int *)malloc(ITER* mărimea(int));

// Avem nevoie de variabile accesibile GPU-ului,
// deci cudaMallocManaged le oferă
cudaMallocManaged(&gpu_a, ITER* mărimea(int));
cudaMallocManaged(&gpu_b, ITER* mărimea(int));
cudaMallocManaged(&gpu_c, ITER* mărimea(int));

pentru (inteu= 0;eu<ITER; ++eu) {
la[eu] =eu;
b[eu] =eu;
c[eu] =eu;
}

// Apelați funcția CPU și temporizați-o
autocpu_start=Ceas::acum();
vector_add_cpu(a, b, c, ITER);
autocpu_end=Ceas::acum();
ore::cost << 'vector_add_cpu:'
<<ore::crono::durata_cast<ore::crono::nanosecunde>(cpu_end-cpu_start).numara()
<< 'nanosecunde. n';

// Apelați funcția GPU și temporizați-o
// Frânele cu unghi triplu este o extensie de rulare CUDA care permite
// parametrii unui apel kernel CUDA care trebuie trecuți.
// În acest exemplu, trecem un bloc de fire cu fire ITER.
autogpu_start=Ceas::acum();
vector_add_gpu<<<1, ITER>>> (gpu_a, gpu_b, gpu_c, ITER);
cudaDeviceSynchronize();
autogpu_end=Ceas::acum();
ore::cost << 'vector_add_gpu:'
<<ore::crono::durata_cast<ore::crono::nanosecunde>(gpu_end-gpu_start).numara()
<< 'nanosecunde. n';

// Eliberați alocările de memorie bazate pe funcția GPU
cudaFree(la);
cudaFree(b);
cudaFree(c);

// Eliberați alocările de memorie bazate pe funcția CPU
liber(la);
liber(b);
liber(c);

întoarcere 0;
}

Makefile conținutul de mai jos:

INC= -I/usr/local/minuni/include
NVCC=/usr/local/minuni/a.m/nvcc
NVCC_OPT= -std = c ++unsprezece

toate:
$(NVCC)$(NVCC_OPT)gpu-example.cpp-saugpu-exemplu

curat:
-rm -fgpu-exemplu

Pentru a rula exemplul, compilați-l:

face

Apoi rulați programul:

./gpu-exemplu

După cum puteți vedea, versiunea CPU (vector_add_cpu) rulează mult mai lent decât versiunea GPU (vector_add_gpu).

Dacă nu, poate fi necesar să ajustați definiția ITER din gpu-example.cu la un număr mai mare. Acest lucru se datorează faptului că timpul de configurare a GPU-ului este mai lung decât unele bucle mai mici cu intensitate de procesor. Am găsit 65535 care funcționează bine pe mașina mea, dar kilometrajul dvs. poate varia. Cu toate acestea, după ce eliminați acest prag, GPU-ul este dramatic mai rapid decât CPU.

Concluzie

Sper că ați învățat multe din introducerea noastră în programarea GPU cu C ++. Exemplul de mai sus nu realizează foarte mult, dar conceptele demonstrate oferă un cadru pe care îl puteți utiliza pentru a încorpora ideile dvs. pentru a dezlănțui puterea GPU-ului dvs.