GPU -programmering med C ++

Gpu Programming With C



I den här guiden utforskar vi kraften i GPU -programmering med C ++. Utvecklare kan förvänta sig otroliga prestanda med C ++, och tillgång till den fenomenala kraften hos GPU: n med ett lågnivåspråk kan ge några av de snabbaste beräkningar som för närvarande finns.

Krav

Även om alla maskiner som kan köra en modern version av Linux kan stödja en C ++-kompilator, behöver du en NVIDIA-baserad GPU för att följa med denna övning. Om du inte har en GPU kan du snurra upp en GPU-driven instans i Amazon Web Services eller en annan molnleverantör du väljer.







Om du väljer en fysisk maskin, se till att du har installerade NVIDIA -drivrutiner. Du hittar instruktioner för detta här: https://linuxhint.com/install-nvidia-drivers-linux/



Förutom drivrutinen behöver du CUDA -verktygssatsen. I det här exemplet kommer vi att använda Ubuntu 16.04 LTS, men det finns nedladdningar tillgängliga för de flesta större distributioner på följande URL: https://developer.nvidia.com/cuda-downloads



För Ubuntu väljer du den .deb -baserade nedladdningen. Den nedladdade filen kommer inte att ha något .deb -tillägg som standard, så jag rekommenderar att byta namn på den till en .deb i slutet. Sedan kan du installera med:





sudo dpkg -ipaketnamn.deb

Du kommer sannolikt att bli ombedd att installera en GPG -nyckel, och följ i så fall instruktionerna för att göra det.

När du har gjort det uppdaterar du dina förråd:



sudo apt-get uppdatering
sudo apt-get installmirakel-och

När det väl är gjort rekommenderar jag omstart för att säkerställa att allt är korrekt laddat.

Fördelarna med GPU -utveckling

CPU: er hanterar många olika in- och utgångar och innehåller ett stort utbud av funktioner för att inte bara hantera ett brett sortiment av programbehov utan också för att hantera olika hårdvarukonfigurationer. De hanterar också minne, cachning, systembussen, segmentering och IO -funktionalitet, vilket gör dem till en jack av alla affärer.

GPU: er är motsatsen - de innehåller många individuella processorer som är inriktade på mycket enkla matematiska funktioner. På grund av detta bearbetar de uppgifter många gånger snabbare än processorer. Genom att specialisera sig på skalärfunktioner (en funktion som tar en eller flera ingångar men bara returnerar en enda utgång) uppnår de extrema prestanda på bekostnad av extrem specialisering.

Exempelkod

I exempelkoden lägger vi ihop vektorer. Jag har lagt till en CPU- och GPU -version av koden för hastighetsjämförelse.
gpu-exempel.cpp innehåll nedan:

#inkludera 'cuda_runtime.h'
#omfatta
#omfatta
#omfatta
#omfatta
#omfatta

typedeftimmar::chrono::hög_upplösning_klockaKlocka;

#define ITER 65535

// CPU -version av vektortilläggsfunktionen
tomhetvector_add_cpu(int *till,int *b,int *c,intn) {
inti;

// Lägg till vektorelementen a och b till vektorn c
för (i= 0;i<n; ++i) {
c[i] =till[i] +b[i];
}
}

// GPU -version av vektortilläggsfunktionen
__global__tomhetvector_add_gpu(int *gpu_a,int *gpu_b,int *gpu_c,intn) {
inti=threadIdx.x;
// No for loop behövs eftersom CUDA -körtiden
// kommer att tråda denna ITER gånger
gpu_c[i] =gpu_a[i] +gpu_b[i];
}

inthuvud() {

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

till= (int *)malloc(ITER* storlek av(int));
b= (int *)malloc(ITER* storlek av(int));
c= (int *)malloc(ITER* storlek av(int));

// Vi behöver variabler som är tillgängliga för GPU,
// så tillhandahåller cudaMallocManaged dessa
cudaMallocManaged(&gpu_a, ITER* storlek av(int));
cudaMallocManaged(&gpu_b, ITER* storlek av(int));
cudaMallocManaged(&gpu_c, ITER* storlek av(int));

för (inti= 0;i<ITER; ++i) {
till[i] =i;
b[i] =i;
c[i] =i;
}

// Ring CPU -funktionen och tid
bilcpu_start=Klocka::nu();
vector_add_cpu(a, b, c, ITER);
bilcpu_end=Klocka::nu();
timmar::kosta << 'vector_add_cpu:'
<<timmar::chrono::duration_cast<timmar::chrono::nanosekunder>(cpu_end-cpu_start).räkna()
<< 'nanosekunder. n';

// Ring GPU -funktionen och tid
// Trippelvinkelbromsarna är en CUDA -körtidsförlängning som tillåter
// parametrar för ett CUDA -kärnanrop som ska skickas.
// I det här exemplet passerar vi ett trådblock med ITER -trådar.
bilgpu_start=Klocka::nu();
vector_add_gpu<<<1, ITER>>> (gpu_a, gpu_b, gpu_c, ITER);
cudaDeviceSynchronize();
bilgpu_end=Klocka::nu();
timmar::kosta << 'vector_add_gpu:'
<<timmar::chrono::duration_cast<timmar::chrono::nanosekunder>(gpu_end-gpu_start).räkna()
<< 'nanosekunder. n';

// Frigör GPU-funktionsbaserade minnestilldelningar
cudaFree(till);
cudaFree(b);
cudaFree(c);

// Frigör CPU-funktionsbaserade minnestilldelningar
fri(till);
fri(b);
fri(c);

lämna tillbaka 0;
}

Makefile innehåll nedan:

INC= -Jag/usr/lokal/mirakel/omfatta
NVCC=/usr/lokal/mirakel/am/nvcc
NVCC_OPT= -std = c ++elva

Allt:
$(NVCC)$(NVCC_OPT)gpu-exempel.cpp-ellergpu-exempel

rena:
-rm -fgpu-exempel

För att köra exemplet, kompilera det:

göra

Kör sedan programmet:

./gpu-exempel

Som du kan se går CPU -versionen (vector_add_cpu) betydligt långsammare än GPU -versionen (vector_add_gpu).

Om inte kan du behöva justera ITER-definieringen i gpu-example.cu till ett högre tal. Detta beror på att GPU-installationstiden är längre än några mindre CPU-intensiva slingor. Jag tyckte att 65535 fungerade bra på min maskin, men din körsträcka kan variera. Men när du har rensat denna tröskel är GPU: n dramatiskt snabbare än CPU: n.

Slutsats

Jag hoppas att du har lärt dig mycket av vår introduktion till GPU -programmering med C ++. Exemplet ovan uppnår inte mycket, men de visade koncepten ger en ram som du kan använda för att införliva dina idéer för att släppa loss kraften i din GPU.