Ricerca…


Osservazioni

CUDA è una tecnologia di elaborazione parallela proprietaria NVIDIA e linguaggio di programmazione per le loro GPU.

Le GPU sono macchine altamente parallele in grado di eseguire migliaia di thread leggeri in parallelo. Ogni thread GPU di solito è più lento in esecuzione e il loro contesto è più piccolo. D'altra parte, GPU è in grado di eseguire diverse migliaia di thread in parallelo e anche più simultaneamente (i numeri precisi dipendono dal modello GPU effettivo). CUDA è un dialetto C ++ progettato specificamente per l'architettura della GPU NVIDIA. Tuttavia, a causa delle differenze di architettura, la maggior parte degli algoritmi non può essere semplicemente copiata da un semplice C ++ - essi verrebbero eseguiti, ma sarebbero molto lenti.

Terminologia

  • host - fa riferimento al normale hardware basato su CPU e ai normali programmi eseguiti in quell'ambiente
  • dispositivo - fa riferimento a una GPU specifica in cui vengono eseguiti i programmi CUDA. Un singolo host può supportare più dispositivi.
  • kernel - una funzione che risiede sul dispositivo che può essere richiamata dal codice host.

Struttura del processore fisico

Il processore GPU abilitato CUDA ha la seguente struttura fisica:

  • il chip - l'intero processore della GPU. Alcune GPU ne hanno due.
  • streamming multiprocessor (SM) - ogni chip contiene fino a ~ 100 SM, a seconda del modello. Ogni SM opera in modo quasi indipendente da un'altra, utilizzando solo la memoria globale per comunicare tra loro.
  • Core CUDA : una singola unità di calcolo scalare di un SM. Il loro numero preciso dipende dall'architettura. Ogni core può gestire alcuni thread eseguiti contemporaneamente in una rapida successione (simile all'hyperthreading nella CPU).

Inoltre, ogni SM presenta uno o più schedulatori di warp . Ogni schedulatore invia una singola istruzione a diversi core CUDA. Ciò causa efficacemente l'SM per operare in modalità SIMD 32-wide.

Modello di esecuzione CUDA

La struttura fisica della GPU ha un'influenza diretta su come i kernel vengono eseguiti sul dispositivo e su come li programma in CUDA. Il kernel viene invocato con una configurazione di chiamata che specifica quanti thread paralleli vengono generati.

  • la griglia - rappresenta tutti i thread che vengono generati in seguito alla chiamata del kernel. È specificato come uno o due set di blocchi dimensionali
  • il blocco - è un insieme semi-indipendente di thread . Ogni blocco è assegnato a un singolo SM. In quanto tale, i blocchi possono comunicare solo attraverso la memoria globale. I blocchi non sono sincronizzati in alcun modo. Se ci sono troppi blocchi, alcuni possono eseguire in sequenza dopo altri. D'altra parte, se le risorse lo consentono, più di un blocco può essere eseguito sullo stesso SM, ma il programmatore non può trarre vantaggio da ciò che sta accadendo (tranne che per l'evidente aumento delle prestazioni).
  • il thread : una sequenza scalare di istruzioni eseguite da un singolo core CUDA. I thread sono "leggeri" con un contesto minimo, consentendo all'hardware di scambiarli e inserirli rapidamente. A causa del loro numero, i thread CUDA operano con pochi registri a loro assegnati e uno stack molto breve (preferibilmente nessuno!). Per questo motivo, il compilatore CUDA preferisce incorporare tutte le chiamate di funzione per appiattire il kernel in modo che contenga solo salti e loop statici. Le chiamate pon pon di funzioni e le chiamate a metodi virtuali, mentre sono supportate nella maggior parte dei dispositivi più recenti, di solito comportano una maggiore penalità delle prestazioni.

Ogni thread è identificato da un blocco blockIdx e indice del thread all'interno del blocco threadIdx . Questi numeri possono essere controllati in qualsiasi momento da qualsiasi thread in esecuzione ed è l'unico modo per distinguere un thread da un altro.

Inoltre, i thread sono organizzati in orditi , ciascuno contenente esattamente 32 thread. I thread all'interno di un singolo warp vengono eseguiti in una sincronizzazione perfetta, in SIMD fahsion. Thread da diversi orditi, ma all'interno dello stesso blocco possono essere eseguiti in qualsiasi ordine, ma possono essere forzati a sincronizzarsi dal programmatore. Thread da blocchi diversi non possono essere sincronizzati o interagire direttamente in alcun modo.

Organizzazione della memoria

Nella normale programmazione della CPU, l'organizzazione della memoria è solitamente nascosta al programmatore. I programmi tipici agiscono come se ci fosse solo RAM. Tutte le operazioni di memoria, come la gestione dei registri, l'utilizzo di L1-L2-L3-caching, lo scambio su disco, ecc. Sono gestite dal compilatore, dal sistema operativo o dall'hardware stesso.

Questo non è il caso di CUDA. Mentre i modelli di GPU più recenti nascondono parzialmente l'onere, ad esempio attraverso la memoria unificata in CUDA 6, vale comunque la pena di comprendere l'organizzazione per motivi di prestazioni. La struttura di base della memoria CUDA è la seguente:

  • Memoria host : la RAM normale. Utilizzato principalmente dal codice host, ma anche i nuovi modelli di GPU possono accedervi. Quando un kernel accede alla memoria host, la GPU deve comunicare con la scheda madre, di solito attraverso il connettore PCIe e come tale è relativamente lento.
  • Memoria del dispositivo / memoria globale : la principale memoria off-chip della GPU, disponibile per tutti i thread.
  • La memoria condivisa , situata in ogni SM, consente un accesso molto più rapido rispetto a quello globale. La memoria condivisa è privata per ogni blocco. I thread all'interno di un singolo blocco possono usarlo per la comunicazione.
  • Registri : memoria più veloce, privata e non indirizzabile di ogni thread. In generale, questi non possono essere usati per la comunicazione, ma alcune funzioni intrinseche permettono di mescolare il loro contenuto all'interno di una distorsione.
  • Memoria locale - la memoria privata di ogni filo che è indirizzabile. Questo è usato per le perdite di registro e gli array locali con indicizzazione variabile. Fisicamente, risiedono nella memoria globale.
  • Memoria texture, memoria costante - una parte della memoria globale contrassegnata come immutabile per il kernel. Ciò consente alla GPU di utilizzare cache speciali.
  • Cache L2 - on-chip, disponibile per tutti i thread. Data la quantità di thread, la durata prevista di ciascuna riga della cache è molto inferiore rispetto alla CPU. Viene utilizzato principalmente con schemi di accesso di memoria disallineati e parzialmente casuali.
  • Cache L1 - si trova nello stesso spazio della memoria condivisa. Di nuovo, l'importo è piuttosto piccolo, dato il numero di thread che lo utilizzano, quindi non aspettatevi che i dati rimangano a lungo. La cache L1 può essere disabilitata.

Versioni

Capacità di calcolo Architettura Nome in codice GPU Data di rilascio
1.0 Tesla G80 2006-11-08
1.1 Tesla G84, G86, G92, G94, G96, G98, 2007-04-17
1.2 Tesla GT218, GT216, GT215 2009-04-01
1.3 Tesla GT200, GT200b 2009-04-09
2.0 Fermi GF100, GF110 2010-03-26
2.1 Fermi GF104, GF106 GF108, GF114, GF116, GF117, GF119 2010-07-12
3.0 Kepler GK104, GK106, GK107 2012-03-22
3.2 Kepler GK20A 2014/04/01
3.5 Kepler GK110, GK208 2013/02/19
3.7 Kepler GK210 2014/11/17
5.0 Maxwell GM107, GM108 2014/02/18
5.2 Maxwell GM200, GM204, GM206 2014/09/18
5.3 Maxwell GM20B 2015/04/01
6.0 Pascal GP100 2016/10/01
6.1 Pascal GP102, GP104, GP106 2016/05/27

La data di rilascio segna il rilascio della prima GPU che supporta la capacità di calcolo fornita. Alcune date sono approssimative, ad esempio la carta 3.2 è stata rilasciata nel Q2 2014.

Prerequisiti

Per iniziare a programmare con CUDA, scarica e installa CUDA Toolkit e il driver dello sviluppatore . Il toolkit include nvcc , il compilatore NVIDIA CUDA e altro software necessario per sviluppare applicazioni CUDA. Il driver assicura che i programmi GPU vengano eseguiti correttamente su hardware compatibile CUDA , di cui avrete anche bisogno.

È possibile verificare che CUDA Toolkit sia installato correttamente sulla macchina eseguendo nvcc --version da una riga di comando. Ad esempio, su una macchina Linux,

$ nvcc --version
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2016 NVIDIA Corporation
Built on Tue_Jul_12_18:28:38_CDT_2016
Cuda compilation tools, release 8.0, V8.0.32

restituisce le informazioni del compilatore. Se il comando precedente non ha avuto esito positivo, probabilmente il CUDA Toolkit non è installato, oppure il percorso di nvcc ( C:\CUDA\bin su macchine Windows, /usr/local/cuda/bin su sistemi POSIX) non fa parte del tuo Variabile d'ambiente PATH .

Inoltre, avrai anche bisogno di un compilatore host che lavori con nvcc per compilare e creare programmi CUDA. Su Windows, questo è cl.exe , il compilatore Microsoft, fornito con Microsoft Visual Studio. Nei sistemi operativi POSIX sono disponibili altri compilatori, inclusi gcc o g++ . La Guida rapida ufficiale CUDA può dirti quali versioni del compilatore sono supportate sulla tua piattaforma specifica.

Per assicurarci che tutto sia impostato correttamente, compila ed esegui un banale programma CUDA per assicurarti che tutti gli strumenti funzionino correttamente.

__global__ void foo() {}

int main()
{
  foo<<<1,1>>>();

  cudaDeviceSynchronize();
  printf("CUDA error: %s\n", cudaGetErrorString(cudaGetLastError()));

  return 0;
}

Per compilare questo programma, copialo in un file chiamato test.cu e compilarlo dalla riga di comando. Ad esempio, su un sistema Linux, il seguente dovrebbe funzionare:

$ nvcc test.cu -o test
$ ./test
CUDA error: no error

Se il programma ha esito positivo senza errori, allora iniziamo a programmare!

Sommare due array con CUDA

Questo esempio illustra come creare un semplice programma che sommerà due array int con CUDA.

Un programma CUDA è eterogeneo e consiste di parti eseguite sia su CPU che su GPU.

Le parti principali di un programma che utilizzano CUDA sono simili ai programmi della CPU e consistono in

  • Allocazione di memoria per i dati che verranno utilizzati sulla GPU
  • Copia dei dati dalla memoria dell'host alla memoria delle GPU
  • Richiamo della funzione del kernel per elaborare i dati
  • Copia il risultato nella memoria della CPU

Per allocare la memoria dei dispositivi cudaMalloc funzione cudaMalloc . Per copiare i dati tra dispositivo e host è possibile utilizzare la funzione cudaMemcpy . L'ultimo argomento di cudaMemcpy specifica la direzione dell'operazione di copia. Ci sono 5 tipi possibili:

  • cudaMemcpyHostToHost - Host -> Host
  • cudaMemcpyHostToDevice - Host -> Dispositivo
  • cudaMemcpyDeviceToHost - Dispositivo -> Host
  • cudaMemcpyDeviceToDevice - Dispositivo -> Dispositivo
  • cudaMemcpyDefault : spazio di indirizzi virtuali unificato basato su predefinito

Successivamente viene richiamata la funzione del kernel. Le informazioni tra i triple chevrons sono la configurazione di esecuzione, che stabilisce quanti thread di dispositivo eseguono il kernel in parallelo. Il primo numero ( 2 nell'esempio) specifica il numero di blocchi e il secondo ( (size + 1) / 2 nell'esempio): numero di thread in un blocco. Nota che in questo esempio aggiungiamo 1 alla dimensione, in modo da richiedere un thread in più anziché avere un thread responsabile per due elementi.

Poiché la chiamata al kernel è una funzione asincrona, cudaDeviceSynchronize viene chiamato ad attendere fino al completamento dell'esecuzione. Gli array di risultati vengono copiati nella memoria dell'host e tutta la memoria allocata sul dispositivo viene liberata con cudaFree .

Per definire la funzione viene usato lo specificatore della dichiarazione __global__ kernel. Questa funzione sarà invocata da ogni thread. Se vogliamo che ogni thread elabori un elemento dell'array risultante, abbiamo bisogno di un mezzo per distinguere e identificare ciascun thread. CUDA definisce le variabili blockDim , blockIdx e threadIdx . Il blockDim variabile predefinitoDim contiene le dimensioni di ciascun blocco di thread come specificato nel secondo parametro di configurazione dell'esecuzione per l'avvio del kernel. Le variabili predefinite threadIdx e blockIdx contengono l'indice del thread all'interno del suo blocco di thread e il blocco di thread all'interno della griglia, rispettivamente. Nota che, poiché potenzialmente richiediamo un thread in più rispetto agli elementi negli array, dobbiamo passare in size per assicurarci di non accedere oltre la fine dell'array.

#include "cuda_runtime.h"
#include "device_launch_parameters.h"

#include <stdio.h>

__global__ void addKernel(int* c, const int* a, const int* b, int size) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < size) {
        c[i] = a[i] + b[i];
    }
}

// Helper function for using CUDA to add vectors in parallel.
void addWithCuda(int* c, const int* a, const int* b, int size) {
    int* dev_a = nullptr;
    int* dev_b = nullptr;
    int* dev_c = nullptr;

    // Allocate GPU buffers for three vectors (two input, one output)
    cudaMalloc((void**)&dev_c, size * sizeof(int));
    cudaMalloc((void**)&dev_a, size * sizeof(int));
    cudaMalloc((void**)&dev_b, size * sizeof(int));

    // Copy input vectors from host memory to GPU buffers.
    cudaMemcpy(dev_a, a, size * sizeof(int), cudaMemcpyHostToDevice);
    cudaMemcpy(dev_b, b, size * sizeof(int), cudaMemcpyHostToDevice);

    // Launch a kernel on the GPU with one thread for each element.
    // 2 is number of computational blocks and (size + 1) / 2 is a number of threads in a block
    addKernel<<<2, (size + 1) / 2>>>(dev_c, dev_a, dev_b, size);
    
    // cudaDeviceSynchronize waits for the kernel to finish, and returns
    // any errors encountered during the launch.
    cudaDeviceSynchronize();

    // Copy output vector from GPU buffer to host memory.
    cudaMemcpy(c, dev_c, size * sizeof(int), cudaMemcpyDeviceToHost);

    cudaFree(dev_c);
    cudaFree(dev_a);
    cudaFree(dev_b);
}

int main(int argc, char** argv) {
    const int arraySize = 5;
    const int a[arraySize] = {  1,  2,  3,  4,  5 };
    const int b[arraySize] = { 10, 20, 30, 40, 50 };
    int c[arraySize] = { 0 };

    addWithCuda(c, a, b, arraySize);

    printf("{1, 2, 3, 4, 5} + {10, 20, 30, 40, 50} = {%d, %d, %d, %d, %d}\n", c[0], c[1], c[2], c[3], c[4]);

    cudaDeviceReset();

    return 0;
}

Lanciamo un singolo thread CUDA per dire ciao

Questo semplice programma CUDA dimostra come scrivere una funzione che verrà eseguita sulla GPU (ovvero "dispositivo"). La CPU, o "host", crea i thread CUDA chiamando funzioni speciali chiamate "kernel". I programmi CUDA sono programmi C ++ con sintassi aggiuntiva.

Per vedere come funziona, inserisci il seguente codice in un file chiamato hello.cu :

#include <stdio.h>

// __global__ functions, or "kernels", execute on the device
__global__ void hello_kernel(void)
{
  printf("Hello, world from the device!\n");
}

int main(void)
{
  // greet from the host
  printf("Hello, world from the host!\n");

  // launch a kernel with a single thread to greet from the device
  hello_kernel<<<1,1>>>();

  // wait for the device to finish so that we see the message
  cudaDeviceSynchronize();

  return 0;
}

(Si noti che per utilizzare la funzione printf sul dispositivo, è necessario un dispositivo con una capacità di calcolo di almeno 2.0. Vedere la panoramica delle versioni per i dettagli.)

Ora compiliamo il programma usando il compilatore NVIDIA ed eseguiamolo:

$ nvcc hello.cu -o hello
$ ./hello
Hello, world from the host!
Hello, world from the device!

Alcune informazioni aggiuntive sull'esempio precedente:

  • nvcc sta per "NVIDIA CUDA Compiler". Separa il codice sorgente in componenti host e dispositivo.
  • __global__ è una parola chiave CUDA utilizzata nelle dichiarazioni di funzione che indica che la funzione viene eseguita sul dispositivo GPU e viene chiamata dall'host.
  • Le parentesi angolari triple ( <<< , >>> ) contrassegnano una chiamata dal codice host al codice dispositivo (chiamato anche "avvio kernel"). I numeri all'interno di queste parentesi quadre indicano il numero di volte da eseguire in parallelo e il numero di thread.

Compilazione ed esecuzione dei programmi di esempio

La guida all'installazione di NVIDIA termina con l'esecuzione dei programmi di esempio per verificare l'installazione di CUDA Toolkit, ma non indica esplicitamente come. Innanzitutto controlla tutti i prerequisiti. Controllare la directory CUDA predefinita per i programmi di esempio. Se non è presente, può essere scaricato dal sito web ufficiale di CUDA. Passare alla directory in cui sono presenti gli esempi.

$ cd /path/to/samples/
$ ls

Dovresti vedere un risultato simile a:

0_Simple     2_Graphics  4_Finance      6_Advanced       bin     EULA.txt
1_Utilities  3_Imaging   5_Simulations  7_CUDALibraries  common  Makefile

Assicurarsi che il Makefile sia presente in questa directory. Il comando make nei sistemi basati su UNIX costruirà tutti i programmi di esempio. In alternativa, accedere a una sottodirectory in cui è presente un altro Makefile ed eseguire il comando make da lì per creare solo quel campione.

Esegui i due programmi di esempio suggeriti: deviceQuery e bandwidthTest :

$ cd 1_Utilities/deviceQuery/
$ ./deviceQuery 

L'output sarà simile a quello mostrato di seguito:

./deviceQuery Starting...

 CUDA Device Query (Runtime API) version (CUDART static linking)

Detected 1 CUDA Capable device(s)

Device 0: "GeForce GTX 950M"
  CUDA Driver Version / Runtime Version          7.5 / 7.5
  CUDA Capability Major/Minor version number:    5.0
  Total amount of global memory:                 4096 MBytes (4294836224 bytes)
  ( 5) Multiprocessors, (128) CUDA Cores/MP:     640 CUDA Cores
  GPU Max Clock rate:                            1124 MHz (1.12 GHz)
  Memory Clock rate:                             900 Mhz
  Memory Bus Width:                              128-bit
  L2 Cache Size:                                 2097152 bytes
  Maximum Texture Dimension Size (x,y,z)         1D=(65536), 2D=(65536, 65536), 3D=(4096, 4096, 4096)
  Maximum Layered 1D Texture Size, (num) layers  1D=(16384), 2048 layers
  Maximum Layered 2D Texture Size, (num) layers  2D=(16384, 16384), 2048 layers
  Total amount of constant memory:               65536 bytes
  Total amount of shared memory per block:       49152 bytes
  Total number of registers available per block: 65536
  Warp size:                                     32
  Maximum number of threads per multiprocessor:  2048
  Maximum number of threads per block:           1024
  Max dimension size of a thread block (x,y,z): (1024, 1024, 64)
  Max dimension size of a grid size    (x,y,z): (2147483647, 65535, 65535)
  Maximum memory pitch:                          2147483647 bytes
  Texture alignment:                             512 bytes
  Concurrent copy and kernel execution:          Yes with 1 copy engine(s)
  Run time limit on kernels:                     Yes
  Integrated GPU sharing Host Memory:            No
  Support host page-locked memory mapping:       Yes
  Alignment requirement for Surfaces:            Yes
  Device has ECC support:                        Disabled
  Device supports Unified Addressing (UVA):      Yes
  Device PCI Domain ID / Bus ID / location ID:   0 / 1 / 0
  Compute Mode:
     < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) >

deviceQuery, CUDA Driver = CUDART, CUDA Driver Version = 7.5, CUDA Runtime Version = 7.5, NumDevs = 1, Device0 = GeForce GTX 950M
Result = PASS

L'istruzione Result = PASS alla fine indica che tutto funziona correttamente. Ora, esegui l'altro programma di esempio sample bandwidthTest in modo simile. L'output sarà simile a:

[CUDA Bandwidth Test] - Starting...
Running on...

 Device 0: GeForce GTX 950M
 Quick Mode

 Host to Device Bandwidth, 1 Device(s)
 PINNED Memory Transfers
   Transfer Size (Bytes)    Bandwidth(MB/s)
   33554432            10604.5

 Device to Host Bandwidth, 1 Device(s)
 PINNED Memory Transfers
   Transfer Size (Bytes)    Bandwidth(MB/s)
   33554432            10202.0

 Device to Device Bandwidth, 1 Device(s)
 PINNED Memory Transfers
   Transfer Size (Bytes)    Bandwidth(MB/s)
   33554432            23389.7

Result = PASS

NOTE: The CUDA Samples are not meant for performance measurements. Results may vary when GPU Boost is enabled.

Anche in questo caso, la dichiarazione Result = PASS indica che tutto è stato eseguito correttamente. Tutti gli altri programmi di esempio possono essere eseguiti in modo simile.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow