Esplora E-book
Categorie
Esplora Audiolibri
Categorie
Esplora Riviste
Categorie
Esplora Documenti
Categorie
Lezione 18
Nello streaming Multiprocessor ci sono 32 cuda core(32 ALU o lane o unità funzionali) e altrettante unità
load/store.
Nella terminologia Nvidia il warp è una raccolta di cuda thread, 32 nelle attuali implementazioni, che
vengono eseguiti simultaneamente da un SM. È possibile eseguire più warp su un SM
contemporaneamente, in parallelo. Warp è dunque gruppo di istruzioni eseguite insieme, e il warp
scheduler decide quale eseguire.
Cuda thread è operazione fatta dalla singola lane mappata su singolo cuda core fisico, quindi in Nvidia
abbiamo tante Unità funzionali(ALU) in grado di eseguire operazioni.
La gpu offre un supporto esplicito per mappare il parallelismo sul codice. Il codice non deve essere
generico, quindi non ci devono essere troppi if, troppi for etc. Il codice deve essere fatto in modo da
contenere cicli, anche innestati con bound regolari, non dipendenti dai dati. Il codice non deve contenere
dipendenze dati e deve avere debole struttura di controllo.In generale il processore , in caso di un if else
esegue entrambi I rami mascherandoli.
Il bound può essere deciso anche a runtime ma non deve cambiare. Nell’esempio il vettore ha per elementi
la somma degli elementi sulla Colonna della matrice. Spazio delle iterazioni in questo caso corrisponde alla
matrice. La parallelizzazione consente di rendere implicito I ciclo più esterno relative alle colonne, in quanto
ogni iterazione sulle colonne può essere fatta in modo indipendente e parallelo.
Cuda descrive le operazioni con uno o più livelli gerarchici, raggruppando le operazioni in una griglia che
può essere mono o multidimensionale(fino a 3 operazioni).
Ciascuno dei blocchi è una struttura mono o multidimensionali di thread. Un thread è un punto nello spazio
delle iterazioni del mio programma. Il blocco di thread è il gruppo di operazioni mappate sullo stesso
streaming multiprocessor(SM). Con la griglia: cosa va su core differenti. Blocco cosa esegue su stesso
processore, non è garantito che blocchi siano nello stesso SM o in diversi SM. Cuda thread rappresentano
singola operazione vettoriale. Blocchi = sequenze di istruzioni vettoriali. Nessuna corrispondenza 1 a 1 tra
griglia e core. Non è possibile fare assunti sull’ordine in cui vengono eseguite le operazioni, per cui ci sono
funzioni esplicite per sincronizzarle. Singolo blocco diviso in warp e ogni warp viene eseguito in parallelo.
Limite a thread su blocco è 1024. 1024 garantisce comunque un numero di warp per gestire
opportunamente le latenze.
2 problemi: 1 separare codice host da device e come descrivere struttura a griglia, blocchi e thread.
Entrambe le cose sono affrontate con piccolo aggiunte sintattiche. Bisogna identificare il kernel che
eseguirà su gpu, ossia __global__ , che fa riferimento al fatto che tale funzione va messa in una regione di
memoria globale, che risiede nella ram della gpu. Questa etichetta si può applicare anche a
variabili/strutture dati.
Per specificare strutture 3 d Dg e Gb dovranno essere di tipo dim3, che è una struct di 3 interi fatta dai
campi x,y,z. Per strutture bidimensionali basta mettere z=0. Se uso un intero sto usando una struttura
monodimensionale.
Per accedere in memoria alla struttura dati iniziale devo calcolare l’indice cui accedere in memoria nel
singola Colonna e nella singola riga.
Int i= blockIdx.x * blockDim.x + threadIdx.x = quanti thread mi precedono nei blocchi+ la mia posizione nel
blocco.
If (i<N e j<N): dico se effettivamente I e j rientrano nei bound, in quanto le matrici possono essere anche
non potenze di 2, e verrebbero usati degli indici non presenti in memoria.
Funzione __syncthreads() fa s che in quell punto tutti I thread vanno in stallo(stesso Streaming
Multiprocessor). Sorta di barriera.
Lezione19
Kernel è accelerabile sole se parallelizzabile. I punti all’interno dello spazio vengono trasformati in una
griglia. Struttura n dimensionale, griglia, viene suddivisa in n blocchi. I singoli thread raccolti in warp, e
rappresenta lunghezza operazione vettoriale. Suddivisione in warp è invisibile al programmatore. Non si
può assumere che ci sia un determinato ordine. Diversi thread block eseguono sullo stesso SM. Cuda fa
assunto che thread interni allo stesso block possono condividere shared memory(memoria cache L1).
Questo succede solo per thread in stesso blocco e non in blocchi diversi. Come gestire la memoria?
Trasferimenti host device possono essere onerosi. Memoria GPU != da memoria CPU.
DMA controlla bus PCIExpress. DMA controller nell’host o nella 0GPU. Questo oggetto deve essere
programmato. Funzione cudaMemcpy(Indirizzo Destinazione,Indirizzo
sorgente,size,direzione_Spostamento)
Source allocato con una malloc generalmente, mentre destination viene dato da cudaMalloc.
Invocazione del Kernel è bloccante per l’host. Numero di blocchi per griglia=N + numero di thread
all’interno dei blocchi -1 tutto diviso il numero dei thread nei blocchi. Sto facendo divisione approssimata
per eccesso. Size_t indica dimensione, è di tipo unsigned_int.
Flusso di compilazione comprende di partire da file .cu , quindi uno entra nel flusso di compilazione GNU e
l’altro nel compilatore Nvidia. Entrambi generano dei file oggetti che vengono linkati assieme per costruire
l’eseguibile. Se codice oggetto può essere eseguito su gpu si chiama cubin, altrimenti viene definito un
codice assembler di tipo ptx, che è un linguaggio molto generico che garantisce portabilità. Codice
ptx/cubine viene unito a file eseguibile per la specifica architettura. Questo processo si chiama
Compilazione Offline. Quando ptx viene convertito in file eseguibile si chiama Just in Time compilation.
OpenCL alternativo a CUda. Architetture virtuali.
Ptx sta per parallel execution. Ambienti grafici oppure strumenti da linea di comando. Nvcc è un compiler
driver, ossia tool che gestisce strumenti di compilazione. Nvcc ha degli opportuni flag quali per esempio –o
etc.
Compute capability è parametro associato alla varie Gpu. Chip Gf104 richiamano architettura generale. Tipi
nativi, supporto hardware per operazioni aritmetiche, disponibilità di tensorcore, dimensione dei warp.
Tutte queste cose potrebbero cambiare. Sm_xy : compila per questa architettura, sm sta per streaming
multiprocessor.
Lezione 20
FUnzine cuda restituisce cudaSuccess se non ci sono stati errori
Se c’è stato un errore invece viene lasciato tipo cudaError_t. cudaDeviceSyncronize blocca device fino al
termine di una specifica operazione.
Abbiamo nozione di stream, ossia coda o sequenza di operazioni da allocare alla GPU. Gli stream sono
diversi per avere tante operazioni sia di calcolo che di trasferimenti . Esempio: interlacciare memcpy con
lancio di kernel, e queste due entità possono essere messe in due stream differenti. Questo permette il
double buffering. Varibile di tipo stream_t. Se manca stream viene chiamato stream di desault, ossia
stream0. Chiamata kernel è asincrono nell’host, mentre è sincrono rispetto alla GPU.
La funzione clock() la posso invocare all’interno del kernel(vale per cpmpute capability >=3) e conta colpi di
clock del device.
Cuda mi dà a disposizione un tipo cudaEvent_t, ossia un evento per misuarare tempo sulla GPU in un
determinato stream. Un uso più ampio è la sincronizzazione, ossia sapere quando sottosequenza ha
completato tramite la funzione cudaEventSunchronize, in modo che sono sicuro che quelle operazioni sono
completate.
Il tempo viene memorizzato tipicamente in variabile float. cudaEventCreate inizializza strutture associate
ad eventi. cudaEventRecord definisce anche lo stream. Per registrate tempo uso cudaEventElapsedTime.
Per profilare l’applicazione uso nvprof. Performance counter sono registri fisici del processore non
direttamente accessibili da programmatore. Diversi blocchi mascherano le latenze in memoria. Questo
serve a tarare numero di blocchi/thread.
Se dichiaro una variabile all’interno di un cuda kernel,p. es. int a, essa verrà allocata separatamente per
ciascun cuda thread, quindi verrà messa in tanti registri quanti sono i cuda thread. Se ho 1024 thread avrò
1024 registri con 1024 istanze di a. Se istanzio un vettore, esso sarà posto nella Ram esterna o in una cache,
ma comunque con tante copie diverse per ciascun cuda thread.
Cuda gestisce anche gli indirizzi, e se devo accedere a un vettore ciascun thread accede a locazioni diverse e
ogni thread avrà il suo putatore per accedere al vettore.
Senza keyword vettore sarà messo nella RAM esterna, ed è locale , ossia accessibile al singolo kernel,
mentre variabile va nei registri.
__shared__ alloca nella memoria condivisa, in cui possono accedervi tutti i thread. Memoria non coerente.
__device__ in opposizione ad host marca variabile globale, ossia accessibile a tutto ciò che c’è nella GPU.
__constant__ sfrutta area di memoria per locazioni costanti, per esempio area texture che è di sola lettura.
Registri e shared memory sono on chip, per cui gli accessi sono veloci. La memoria globale, constant e
texture sono off-chip.
La cache L1 ha 64 kb e da programma si può decidere quanta memoria allocare per la cache e quanta per la
shared memory. Molto spesso non serve avere shared memory, e allora posso utilizzarla interamente come
L1. Numero di registri può essere limitante (nella fermi 32000 registri). Avere pochi registri significa limitare
il numero di blocchi.
Lezione 21
Concetti di programmazione non sono in rapporto 1 a 1 con concetti fisici. Global memory può essere
memoria off chip DRAM o nella cache L2. Global indirizzabile con spazio di indirizzamento comune.
Per variabili o strutture dati in memoria globale la devo indicare con la keyword __device__ . Essendo
globale ci possono essere delle race conditions.
Memoria locale si riferisce a cio che è definito dentro la funzione. Registri si trovano all’interno degli SM.
Compilatore ha dei flag per calcolare numero dei registri per ciascuno dei thread. Da compilatore posso
forzare a usare un certo numero di registri e migliorare prestazioni, tramite flag maregcount, che vincola il
numero massimo di registri utilizzabili. C’è anche uno specificatore che dice numero minimo e massimo di
blocchi allocabili su stesso SM.
Nvcc –ptxas-options =-v main.cu indica informazioni circa memoria locale, costante e shared e numero di
registri. Si usa insieme a nvprof.
Shared memory è on chip e privata per ciascun SM. Se alloco variabile in shared memory, questa è allocata
un’unica volta per tutti i thread del blocco(__shared__). Accessibile in maniera condivisa. Limiti molto
stringenti. Altro aspetto importante è che quei kB sono accessibili parallelamente, per cui per ottimizzare
prestazioni conviene distribuire dati uniformemente sulla shared memory. Share memory allocabile anche
dinamicamente. Extern potrebbe essere usato con altri moduli del mio programma. Buona norma allocare
solo sh. Mem. Che serve.
Area constant fa sì che variabile vada in memoria globale e sia di sola lettura. Tutto ciò che viene allocato in
questa area ha un ciclo di vita lungo per tutto il programma.
Area texture serve per gestire le texture. Questa cache ottimizza pattern di accesso in 2 dimensioni.
Variabile in registro accessibile in pochi colpi di clock, al di fuori dei registri si parla di altri ordini di
grandezza.
Accesso a shared è variabile, può essere molto basso se c’è poca concorrenza , altrimenti più elevato.
Dynamic parallelism permette di invocare esplicitamente dei kernel a partire da altri kernel.
Lezione 22
Occupancy: numero di warp attualmente in esecuzione su un SM diviso il numero massimo di warp che può
eseguire in maniera concorrente sul SM.
E’ possibile controllare il numero massimo di thread per blocco e il numero minimo di blocchi da mappare
sullo stesso SM:
__global__void launch_bounds(MA_THREADS_PER_BLOCK,MIN_BLOCKS_PER_MP)
Queste info sono un modo per controllare numero di registri in maniera implicita, oppure si può usare il
flag maxregcount.
Rapporto tra codice e memoria si riferisce soprattutto a memorie off chip e riflette il fatto che memorie off
chip si può dividere in banchi, e i banchi possono essere larghi. Gli accessi in memoria devono essere
accorpati, in quanto si cerca di fare molte load e store per una unica transazione. Load Store Unit si occupa
di questo(coalescing) . Se gli indirizzi non sono contigui LSU farà tante transazioni. L’accesso deve essere
strided, quindi indirizzi devono essere contigui quanto gli indici nei thread, evitando il disallineamento. Le
cudaMalloc cercano di allocare su indirizzi multipli di 256/512. La pesenza delle cache maschera accessi
disallineati. Stride alti fanno perdere tanto. Questa cosa avviene quando io passo da una riga all’altra, sto
aumentando indirizzo della dimensione di tutta la riga. E’ importante scorrere le matrici sulle righe, a volte
meglio anche sprecare memoria pur di avere accessi strided.
cudaMalloc garantisce allineamento a 256, ma non va bene per strutture multidimensionali. Per allocare
matrici devo fare padding, ossia riempire locazioni di memoria con 0 ma allocandola allineata. Un esempio
è cudaMallocPitch(). Questa cosa può essere fatta anche su più dimensioni. Per strutture su piu dimensioni
cuda ci viene incontro.
Altro aspetto teorico riguarda la memoria on chip, in particolare la shared memory. Gli accessi a un vettore
i shared memory tramite load, posso avere multipli accessi da diversi warp. Per questo shared memory è
organizzata in banchi da 32. L'unità è di 4 byte, i primi 4 sono nel primo banco, i successivi nel secondo
banco e così via fino al 32 indirizzo, poi ricomincio daccapo. La cosa interessante è che fare accessi
differenti su banchi differenti possono essere fatti parallelamente con tempi di accesso molto brevi. Questa
cosa viene meno quando gli accessi vengono effettuati in modo disallineato(esempio, se ho matrice in
sh.memory), quindi accesso strided, consumo subito i banchi di memoria, ma dal punto di vista
prestazionale raddoppio i tempi di esecuzione. Programmatore deve essere consapevole di questi
accessi.Conflitti: thread di warp diversi accedono a stesso banco, in questo caso accessi vengono
serializzati. Caso peggiore è quando thread diversi acedono a indirizzi diversi allocati sullo stesso banco.
Questo avviene se scrivo lData[32*tx]. Nella famiglia Kepler rende i banchi larghi 8 byte per 32 banchi,
proprio per ottimizzare situazione in cui variabile è double. Per int e float uso solo 4 byte e il resto lo
riempio col padding. Questa configurazione si effettua con cudaSharedMemBankSizeFourByte o EightByte.
Nelle architetture successive questo mapping non viene più usato.Shared memory è una memoria
scratchpad, a differenza della cache che è invisibile al programmatore. Difficoltà è nel partizionare strutture
dati in tile (es.sottomatrice)da mettere nella shared memory.
Lezione 23
Problema della consistenza: GPU sono sistemi manycore, per cui possono esserci race conditions, thread
possono appartenere a stesso blocco o a blocchi diversi. Tutti i thread della GPU possono accedere a
quell'indirizzo. Non c'è nessun controllo su come verrà eseguita quella particolare operazione, specie se c'è
una operazione atomica. Syncthread funziona solo su thread sullo stesso blocco, non su blocchi differenti,
nè tantomento su thread mappati su SM diversi. Per questo tipo di problema cuda offre primitive di
operazioni atomiche rispetto a tutti i processori:
Se ho bsogno di sezioni critiche lo posso fare con primitive atomiche per implementare dei lock. La
variabile atomica rappresenta il semaforo. atomicCAS=Compare e Swap. Se i valori sono uguali, ossia se il
lock è libero, scrivo 1. Per liberare la sezione critica scrivo 0.
lock=0;
Queste sono problematiche che programmatore deve sapere. GPU è pensata per eseguire come un treno
per cui sincronizzazione limita le prestazioni. Devo ristrutturare programmi per limitare l'uso della
sincronizzazione. In SM nuovi possono esserci anche più di 32 core fisici,e serializzare esecuzione vuol dire
ammazzare prestazioni.
syncthread è una barriera che vincola thread ad arrivare a un certo punto e poi ripartire insieme a livello di
singolo blocco.
Host ha sistema di gestione della memoria virtuale che viene swappata verso il disco. Problema: in un dato
momento, vettore allocato con malloc potrebbe essere spostato su disco specie se non è stato usato
recentemente. Ciò non è inverosimile in una applicazione che usa GPU. DMA funziona solo se ai 2 capi le
pagine ci sono. Qualsiasi DMA deve lavorare su buffere presente nella ram, e programma deve richiedere
che quel buffer sia pinned, ossia non deve essere spostato in memoria di massa. Uso del pinning può
diminuire le performance. Vantaggio è che non ho overhead nello spostarlo, svantaggio è che uso meno
memoria, specie se quel vettore è molto grande.
cudaHostAlloc() alloca spazio nell’host e garantisce che memoria sia pinned. Altro sviluppo è la zero copy.
Host e device hanno 2 spazi di indirizzamento diversi. Nel tempo cuda ha cercato di astrarre questo
aspetto.
Zero copy omette trasferimenti espliciti e li rende automatici. Controindicazione nell’uso di variabili
atomiche, e altro problema è che resta esplicita gestione della sincronizzazione che esplicitamente non si
vede.
Unified memory. Nella zero copy i puntatori hanno validità nella memoria della GPU e non c’entrano con gli
indirizzi host. Se bisogna trasferire lista linkata da host a device gli indirizzi valgono nell’host. Se gli indirizzi
sono parte dei dati bisogna fare una conversione, per cui potrebbe essere utile avere uno spazio di
indirizzamento comune tra device e host, e questo semplifica la gestione dei puntatori. Ci sono alcuni
vincoli. Per utilizzare Unified memory devo usare keyword __managed__ oppure con cudaMallocManaged.
SI pone problema di coerenza lato host e lato device. SI presuppone che comunque sia kernel prioritario
nell’uso di variabili managed. Svantaggio, non è possibile la oversubscription, ossia avere più memoria
virtuale di quella disponibile.
Lezione 24
Stream si possono sincronizzare l’uno con l’altro. Task possono essere delle memcpy async e come host
fare altro, ossia inserire altri task in altri stream. cudaMemcpyAsync deve lavorare su buffer pinned, ossia
non swappabili in memoria secondaria. cudaMallocHost alloca su memoria Host pagine pinned.
Task possono necessitare di una sincronizzazione sugli stream. Quando ci sono dipendenze, per esempio da
task1 a task2, si usano gli eventi, che non servono solo come marcatori. Funzione di sincronizzazione blocca
l’host. cudaDeviceSyncronize blocca l’host rispetto a tutto cio che sta nello stream finchè tutti i cuda
stream non hanno terminato. cudaStreamSynchronize blocca l’host finchè non si svuota lo stream.
cudaEventRecord inserisce evento in stream. cudaStreamWaitEvent ferma un dato stream finchè un
evento non è completato. cudaStreamQuery viene chiamata per vedere se tutti i comandi nello stream
sono completati. Gli stream si definiscono con cudaStreamCreate, e l’istanza stream serve a decidere in
quale coda eseguire il particolare task kernel. Ottengo in questo modo di spezzettare dati e task.
Rosa rappresenta comunicazione e verde calcolo. Nello stream 2 inserisco altre operazioni e una
operazione che dipende dal primo stream, questo si indica con una freccia.
Dynamic Parallelism
Capita spesso che kernel continuano a lavorare sulla GPU e hanno bisogno di altri kernel. Altra situazione è
quando dim griglia non è nota a priori, e quindi c’è bisogno di ricorsione all’interno dei kernel. Questa cosa
non era prevista a priori. Abbiamo griglia genitore e griglia child. Livelli di ricorsione permessi sono pochi, e
quindi rispetto all’assenza di Dynamic Parallelism c’è aumento delle prestazioni dovute al fatto che dati
non vengono spostati avanti e indietro da host a device. Graficamente l’albero di ricorsione è largo ma ha
pochi rami. Ci sono dei limiti nell’uso dei puntatori in shared memory o in memoria locale, ma possiamo
passarli in memoria globale. D.P. viene usato in applicazioni avanzate.
Le librerie sopra cuda vengono usate in ambiti quali grafica, algebra lineare, deep learning, calcolo di fft.
OpenACC invece funziona per GPU. Anziché mettere cudaMemcpy e <<<mykernel metto #pragma acc
parallel loop, ossia dico a compilatore di parallelizzare loop tramite GPU. Viene usato per C e fortran.
#pragma acc kernels. Kernels è una direttiva in cui delego a compilatore la parallelizzazione da eseguire su
GPU.
#pragma acc end parallel loop Per chiudere loop. Con parallel programmatore ha più controllo perché dice
come parallelizzare. Kernel è privo di parametri. #pragma omp è modo per parallelizzare su
OpenMP(Programmazione Multithreading). In openMP posso anche specificare numero di thread.
#pragma acc data copy: dico di trasferire le matrici nella memoria della GPU, senza fare ping pong tra
device e host.
Parametro gang specifica quanti blocchi lanciare e vector specifica quadi cuda thread lanciare per ogni
blocco. In questo modo posso ottenere vantaggi significativi.
Streaming Multiprocessor diventa Compute Unit. GPu compute device-host resta host.
Stream si chiama command queue. Per il resto i concetti sono simili. In opencl ragiono ugualmente,
tracciando una griglia. Particolarità: Opencl definisce float di dimensioni non standard. Cambia poco
rispetto a Cuda
__kernel = __global
Struttura di memoria è la stessa, abbiamo memoria costante, memoria locale etc. In termini di prestazioni
su NVIDIA non ha senso usare OpenCL, mentre su device non Nvidia si usa OpenCL. OpenCL viene usato
anche su fpga, anche se non è il modo migliore di usare l’fpga.