Programmazione GPU con C ++

Programmazione GPU con C ++

Panoramica

In questa guida, esploreremo il potere della programmazione GPU con C++. Gli sviluppatori possono aspettarsi prestazioni incredibili con C ++ e accedere alla potenza fenomenale della GPU con un linguaggio di basso livello può produrre parte del calcolo più veloce attualmente disponibile.

Requisiti

Mentre qualsiasi macchina in grado di eseguire una versione moderna di Linux può supportare un compilatore C ++, avrai bisogno di una GPU a base di Nvidia per seguire insieme a questo esercizio. Se non hai una GPU, puoi far girare un'istanza basata sulla GPU in Amazon Web Services o in un altro fornitore di cloud a tua scelta.

Se scegli una macchina fisica, assicurati di installare i driver proprietari di Nvidia. Puoi trovare 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 ci sono download disponibili per la maggior parte delle principali distribuzioni al seguente URL: https: // sviluppatore.nvidia.com/cuda-downloads

Per Ubuntu, sceglieresti il .Download basato su Deb. Il file scaricato non avrà un .estensione di deb per impostazione predefinita, quindi ti consiglio di rinominarlo per avere un .Deb alla fine. Quindi, puoi installare con:

sudo dpkg -i pacchetto nome.Deb

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

Una volta che l'hai fatto, aggiorna i tuoi repository:

Sudo Apt-get Aggiornamento
sudo apt -get install cuda -y

Una volta fatto, consiglio di riavviarsi per garantire che tutto sia caricato correttamente.

I benefici dello sviluppo della GPU

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

Le GPU sono l'opposto: contengono molti singoli processori focalizzati 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 solo un singolo output), ottengono prestazioni estreme a costo della specializzazione estrema.

Codice di esempio

Nel codice di esempio, aggiungiamo vettori insieme. Ho aggiunto una versione CPU e GPU del codice per il confronto della velocità.
GPU-Example.CPP Contenuto di seguito:

#include "cuda_runtime.H"
#includere
#includere
#includere
#includere
#includere
typedef std :: chrono :: high_resolution_clock clock;
#define ITER 65535
// versione CPU del vettore aggiungi funzione
void vector_add_cpu (int *a, int *b, int *c, int n)
int i;
// Aggiungi gli elementi vettoriali A e B al vettore C
per (i = 0; i < n; ++i)
c [i] = a [i] + b [i];


// versione GPU del vettore Aggiungi funzione
__global__ void vector_add_gpu (int *gpu_a, int *gpu_b, int *gpu_c, int n)
int i = threadidx.X;
// NO per loop necessario perché il runtime CUDA
// infilerà questi tempi iter
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,
// così cudamallocmanaged li fornisce
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] = i;
b [i] = i;
c [i] = i;

// chiama la funzione CPU e il tempo
auto cpu_start = clock :: now ();
vector_add_cpu (a, b, c, iter);
auto cpu_end = clock :: ora ();
std :: cout << "vector_add_cpu: "
<< std::chrono::duration_cast(cpu_end - cpu_start).contare()
<< " nanoseconds.\n";
// chiama la funzione GPU e il tempo
// Il triplo angolo Brakets è un'estensione di runtime CUDA che consente
// Parametri di una chiamata del kernel CUDA da passare.
// In questo esempio, stiamo passando un blocco di thread con thread ITER.
auto gpu_start = clock :: ora ();
vector_add_gpu <<<1, ITER>>> (gpu_a, gpu_b, gpu_c, iter);
CudadeVicesyncronize ();
auto gpu_end = clock :: now ();
std :: cout << "vector_add_gpu: "
<< std::chrono::duration_cast(gpu_end - gpu_start).contare()
<< " nanoseconds.\n";
// libera le allocazioni di memoria basate sulla funzione GPU
Cudafree (a);
Cudafree (b);
Cudafree (c);
// libera le allocazioni di memoria basate sulla funzionalità della CPU
libero (a);
libero (b);
libero (c);
restituzione 0;

Makefile Contenuto di seguito:

Inc = -i/usr/locale/cuda/include
NVCC =/USR/Local/Cuda/Bin/NVCC
Nvcc_opt = -std = c ++ 11
Tutto:
$ (NVCC) $ (NVCC_OPT) GPU-Example.CPP -O GPU -Example
pulito:
-RM -f GPU -Example

Per eseguire l'esempio, compilalo:

Fare

Quindi eseguire il programma:

./GPU-Example

Come puoi vedere, la versione CPU (vector_add_cpu) è notevolmente più lenta della versione GPU (vector_add_gpu).

In caso contrario, potrebbe essere necessario regolare la definizione 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 ad alta intensità di CPU. Ho trovato 65535 per funzionare bene sulla mia macchina, ma il tuo chilometraggio può variare. Tuttavia, una volta cancellata questa soglia, la GPU è drammaticamente più veloce della CPU.

Conclusione

Spero che tu abbia imparato molto dalla nostra introduzione alla programmazione GPU con C++. L'esempio sopra non raggiunge molto, ma i concetti dimostrati forniscono un quadro che puoi usare per incorporare le tue idee per scatenare il potere della tua GPU.