Panoramica
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, useremo 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, sceglieresti il .download basato su deb. Il file scaricato non avrà un .deb per impostazione predefinita, quindi consiglio di rinominarlo per avere a .deb alla fine. Quindi, puoi installare con:
sudo dpkg -i nome-pacchetto.debProbabilmente 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 updatesudo apt-get install cuda -y
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 prende uno o più input ma restituisce un solo 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:
#includere
#includere
#includere
#includere
#includere
typedef std::chrono::high_resolution_clock Orologio;
#define ITER 65535
// Versione CPU della funzione vector add vector
void vector_add_cpu(int *a, int *b, int *c, int n)
int io;
// Aggiungi gli elementi del vettore a e b al vettore c
per (i = 0; i < n; ++i)
c[i] = a[i] + b[i];
// Versione GPU della funzione di aggiunta vettoriale
__global__ void vector_add_gpu(int *gpu_a, int *gpu_b, int *gpu_c, int n)
int i = threadIdx.X;
// Nessun ciclo for necessario perché il runtime CUDA
// infilerà questo ITER volte
gpu_c[i] = gpu_a[i] + gpu_b[i];
int main()
int *a, *b, *c;
int *gpu_a, *gpu_b, *gpu_c;
a = (int *)malloc(ITER * sizeof(int));
b = (int *)malloc(ITER * sizeof(int));
c = (int *)malloc(ITER * sizeof(int));
// Abbiamo bisogno di variabili accessibili alla GPU,
// quindi cudaMallocManaged fornisce questi
cudaMallocManaged(&gpu_a, ITER * sizeof(int));
cudaMallocManaged(&gpu_b, ITER * sizeof(int));
cudaMallocManaged(&gpu_c, ITER * sizeof(int));
per (int i = 0; i < ITER; ++i)
a[i] = io;
b[i] = io;
c[i] = io;
// Chiama la funzione CPU e cronometrala
auto cpu_start = Orologio::ora();
vector_add_cpu(a, b, c, ITER);
auto cpu_end = Orologio::ora();
std::cout << "vector_add_cpu: "
<< std::chrono::duration_cast
<< " nanoseconds.\n";
// Chiama la funzione GPU e cronometrala
// Le parentesi a triplo angolo sono un'estensione di runtime CUDA che consente
// parametri di una chiamata al kernel CUDA da passare.
// In questo esempio, stiamo passando un blocco di thread con thread ITER.
auto gpu_start = Orologio::ora();
vector_add_gpu <<<1, ITER>>> (gpu_a, gpu_b, gpu_c, ITER);
cudaDeviceSynchronize();
auto gpu_end = Orologio::ora();
std::cout << "vector_add_gpu: "
<< std::chrono::duration_cast
<< " nanoseconds.\n";
// 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
libero(a);
libero(b);
libero(c);
restituisce 0;
Makefile contenuti di seguito:
INC=-I/usr/local/cuda/includeNVCC=/usr/local/cuda/bin/nvcc
NVCC_OPT=-std=c++11
tutti:
$(NVCC) $(NVCC_OPT) esempio gpu.cpp -o gpu-esempio
pulito:
-rm -f gpu-esempio
Per eseguire l'esempio, compilalo:
rendereQuindi eseguire il programma:
./esempio-gpuCome puoi vedere, la versione CPU (vector_add_cpu) funziona molto più lentamente della versione GPU (vector_add_gpu).
In caso contrario, potrebbe essere necessario modificare la definizione di ITER in gpu-example.cu a un numero più alto. Ciò è dovuto al fatto che il tempo di configurazione della GPU è più lungo di alcuni loop più piccoli che richiedono molta 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.