Programmazione GPU con C++

Gpu Programming With C



In questa guida, esploreremo la potenza della programmazione GPU con C++. Gli sviluppatori possono aspettarsi prestazioni incredibili con C++ e l'accesso alla potenza fenomenale della GPU con un linguaggio di basso livello può produrre alcuni dei calcoli più veloci attualmente disponibili.

Requisiti

Sebbene qualsiasi macchina in grado di eseguire una versione moderna di Linux possa supportare un compilatore C++, avrai bisogno di una GPU basata su NVIDIA per seguire questo esercizio. Se non disponi di una GPU, puoi avviare un'istanza basata su GPU in Amazon Web Services o in un altro provider cloud di tua scelta.







Se scegli una macchina fisica, assicurati di avere i driver proprietari NVIDIA installati. Puoi trovare le istruzioni per questo qui: https://linuxhint.com/install-nvidia-drivers-linux/



Oltre al driver, avrai bisogno del toolkit CUDA. In questo esempio, utilizzeremo Ubuntu 16.04 LTS, ma sono disponibili download per la maggior parte delle principali distribuzioni al seguente URL: https://developer.nvidia.com/cuda-downloads



Per Ubuntu, dovresti scegliere il download basato su .deb. Il file scaricato non avrà un'estensione .deb per impostazione predefinita, quindi consiglio di rinominarlo per avere un .deb alla fine. Quindi, puoi installare con:





sudo dpkg -ionome-pacchetto.deb

Probabilmente ti verrà richiesto di installare una chiave GPG e, in tal caso, segui le istruzioni fornite per farlo.

Dopo averlo fatto, aggiorna i tuoi repository:



sudo apt-get update
sudo apt-get installmiracoli-e

Una volta fatto, ti consiglio di riavviare per assicurarti che tutto sia caricato correttamente.

I vantaggi dello sviluppo GPU

Le CPU gestiscono molti input e output diversi e contengono un vasto assortimento di funzioni non solo per gestire un vasto assortimento di esigenze del programma, ma anche per gestire diverse configurazioni hardware. Gestiscono anche la memoria, la memorizzazione nella cache, il bus di sistema, la segmentazione e la funzionalità IO, rendendoli un tuttofare.

Le GPU sono l'opposto: contengono molti processori individuali che si concentrano su funzioni matematiche molto semplici. Per questo motivo, elaborano le attività molte volte più velocemente delle CPU. Specializzandosi in funzioni scalari (una funzione che accetta uno o più input ma restituisce solo un singolo output), ottengono prestazioni estreme a costo di un'estrema specializzazione.

Codice di esempio

Nel codice di esempio, aggiungiamo i vettori insieme. Ho aggiunto una versione CPU e GPU del codice per il confronto della velocità.
gpu-esempio.cpp contenuti di seguito:

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

typedefore::crono::orologio_alta_risoluzioneOrologio;

#define ITER 65535

// Versione CPU della funzione vector add
vuotovector_add_cpu(int *a,int *B,int *C,intn) {
intio;

// Aggiungi gli elementi del vettore a e b al vettore c
per (io= 0;io<n; ++io) {
C[io] =a[io] +B[io];
}
}

// Versione GPU della funzione di aggiunta vettoriale
__globale__vuotovector_add_gpu(int *gpu_a,int *gpu_b,int *gpu_c,intn) {
intio=threadIdx.X;
// Nessun ciclo for necessario perché il runtime CUDA
// infilerà questo ITER volte
gpu_c[io] =gpu_a[io] +gpu_b[io];
}

intprincipale() {

int *a,*B,*C;
int *gpu_a,*gpu_b,*gpu_c;

a= (int *)malloc(ITER* taglia di(int));
B= (int *)malloc(ITER* taglia di(int));
C= (int *)malloc(ITER* taglia di(int));

// Abbiamo bisogno di variabili accessibili alla GPU,
// quindi cudaMallocManaged fornisce questi
cudaMallocGestito(&gpu_a, ITER* taglia di(int));
cudaMallocGestito(&gpu_b, ITER* taglia di(int));
cudaMallocGestito(&gpu_c, ITER* taglia di(int));

per (intio= 0;io<ITER; ++io) {
a[io] =io;
B[io] =io;
C[io] =io;
}

// Chiama la funzione CPU e cronometrala
autocpu_start=Orologio::Ora();
vector_add_cpu(a, b, c, ITER);
autocpu_end=Orologio::Ora();
ore::costo << 'vector_add_cpu: '
<<ore::crono::duration_cast<ore::crono::nanosecondi>(cpu_end-cpu_start).contare()
<< ' nanosecondi. ';

// Chiama la funzione GPU e cronometrala
// Le parentesi a triplo angolo sono un'estensione di runtime CUDA che consente
// i parametri di una chiamata al kernel CUDA da passare.
// In questo esempio, stiamo passando un blocco di thread con thread ITER.
autogpu_start=Orologio::Ora();
vector_add_gpu<<<1, ITER>>> (gpu_a, gpu_b, gpu_c, ITER);
cudaDeviceSynchronize();
autogpu_end=Orologio::Ora();
ore::costo << 'vector_add_gpu: '
<<ore::crono::duration_cast<ore::crono::nanosecondi>(gpu_end-gpu_start).contare()
<< ' nanosecondi. ';

// Libera le allocazioni di memoria basate sulla funzione GPU
cudaFree(a);
cudaFree(B);
cudaFree(C);

// Libera le allocazioni di memoria basate sulla funzione della CPU
gratuito(a);
gratuito(B);
gratuito(C);

Restituzione 0;
}

Makefile contenuti di seguito:

INC=-I/usr/Locale/miracoli/includere
NVCC=/usr/Locale/miracoli/sono/nvcc
NVCC_OPT=-std=c++undici

Tutti:
$(NVCC)$(NVCC_OPT)gpu-esempio.cpp-ogpu-esempio

pulire:
-rm -Fgpu-esempio

Per eseguire l'esempio, compilalo:

fare

Quindi eseguire il programma:

./gpu-esempio

Come puoi vedere, la versione della CPU (vector_add_cpu) funziona molto più lentamente della versione della GPU (vector_add_gpu).

In caso contrario, potrebbe essere necessario modificare la definizione ITER in gpu-example.cu su un numero più alto. Ciò è dovuto al fatto che il tempo di configurazione della GPU è più lungo di alcuni cicli più piccoli a uso intensivo della CPU. Ho trovato che 65535 funziona bene sulla mia macchina, ma il tuo chilometraggio può variare. Tuttavia, una volta superata questa soglia, la GPU è notevolmente più veloce della CPU.

Conclusione

Spero che tu abbia imparato molto dalla nostra introduzione alla programmazione GPU con C++. L'esempio sopra non fa molto, ma i concetti dimostrati forniscono un framework che puoi usare per incorporare le tue idee per liberare la potenza della tua GPU.