Documente Academic
Documente Profesional
Documente Cultură
Considera ii de performan
De i un kernel CUDA poate rula corect pe orice dispozitiv CUDA, viteza sa de execu ie, poate varia mult în
func ie de constrângerile de resurse ale fiec rui dispozitiv. În acest capitol vor fi prezentate constrângerile
principale de resurse ale unui dispozitiv CUDA i modul în care acestea limiteaz execu ia paralel .
Constrângerile de resurse principale pot varia de la un program la altul i astfel performan a unui kernel
poate fi îmbun it , uneori în mod dramatic, prin echilibrarea consumului de resurse (sc derea consumului
unei resurse i cre terea consumului altei resurse). F în elegerea modului de interdependen dintre
resurse, cre terea performan ei unei aplica ii nu este posibil .
Pentru un bloc tridimensional, ini ial se plaseaz în ordine liniar toate firele cu threadIx.z egal cu 0.
Toate aceste fire sunt tratate precum un bloc bidimensional. Toate firele cu threadIdx.z egal cu 1 sunt
plasate apoi în ordine liniar , .a.m.d. Pentru un bloc de 4x8x2, cele 64 de fire vor fi plasate în 2 warp-uri:
firele T(0, 0, 0) pân la T(3, 7, 0) formeaz primul warp, iar firele T(0, 0, 1) pân la T(3, 7, 1) formeaz al
doilea warp.
Hardware-ul execut o instruc iune pentru toate firele dintr-un warp înainte s treac la urm toarea
instruc iune. Acest stil de execu ie se nume te SIMT (Single Instruction Multiple Thread) i e determinat de
constrângerile hardware (permite amortizarea costului de preg tire i procesare a unei instruc iuni pentru un
num r mare de fire). Acest stil func ioneaz bine dac toate firele dintr-un warp urmeaz aceea i cale de
execu ie la procesarea datelor. De exemplu, dac se folose te o instruc iune de tip if-then-else, atunci
execu ia se realizeaz rapid dac toate firele din warp aleg fie calea then fie calea else. Când firele dintr-un
warp aleg c i de execu ie diferite, stilul SIMT va conduce la întârzieri, deoarece sunt necesare mai multe
faze de execu ie: o faz este necesar pentru firele de pe calea then i o alt faz este necesar pentru calea
else. Aceste faze sunt secven iale, conducând astfel la cre terea timpului de execu ie.
Când firele din acela i warp urmeaz c i de execu ie diferite, se spune c firele sunt divergente în execu ie.
Divergen a poate s apar i în alte situa ii. De exemplu, dac firele dintr-un warp execut o bucl for care
realizeaz 6, 7 sau 8 itera ii în func ie de firul de execu ie, atunci toate firele vor încheia primele 6 itera ii
împreun . Dou faze sunt îns necesare pentru cea de-a aptea itera ie: una pentru firele care execut itera ia
i una pentru firele care nu o execut . De asemenea sunt necesare dou faze i pentru cea de-a opta itera ie.
O instruc iune de tip if-then-else poate conduce la divergen atunci când expresia folosit pentru luarea
deciziei e bazat pe valorile variabilei threadIdx. De exemplu, expresia if(threadIdx.x > 2) {}
conduce la urmarea a dou c i de execu ie diferite: firele 0, 1 i 2 vor urma o alt cale decât firele 3, 4, 5 etc.
În mod similar, instruc iunile de iterare pot conduce la divergen dac expresia folosit ca i condi ie a
buclei e bazat pe valorile variabilei threadIdx. În continuare se va folosi un a a-numit algoritm de
reducere pentru a ilustra aceste aspecte.
Un algoritm de reducere extrage o singur valoare dintr-un tablou de variabile. Acea valoare poate fi suma,
maximul, minimul, etc. Toate aceste tipuri de reducere au aceea i structur de calcul. O opera ie de reducere
poate fi executat simplu prin parcurgerea secven ial a elementelor. Când se proceseaz un anumit element,
ac iunea care trebuie executat depinde de tipul opera iei de reducere: în cazul sumei valoarea se adun la
suma curent , pentru maxim valoarea curent este comparat cu maximul elementelor parcurse pân la
momentul respectiv i se realizeaz suprascrierea valorii maxime doar dac valoarea curent este mai mare,
etc.
Când sunt prezente multe elemente în tablou, timpul necesar pentru parcurgerea tuturor elementelor din
tablou devine suficient de mare pentru a justifica execu ia paralel . Un algoritm de reducere paralel se
aseam cu un turneu de fotbal organizat în format eliminatoriu: procesul de eliminare a echipelor este o
reducere de tip maxim. Reducerea se realizeaz în mai multe etape: echipele sunt împ ite în grupuri de câte
dou i toate echipele joac în paralel. Câ tig toarele primei runde merg în runda a doua, câ tig toarele celei
de-a doua runde merg în runda a treia, .a.m.d. Dac ini ial sunt 1024 de echipe, e nevoie de doar 10 runde
pentru a determina câ tig torul final. Problema este îns faptul c e nevoie de 512 terenuri pe care s se
execute meciurile primei runde, 256 de terenuri pentru runda a doua, .a.m.d.
Fig. 4.2. prezint un kernel care realizeaz în paralel opera ia de reducere de tip sum . Tabloul original se
afl în memoria global . Fiecare bloc de fire reduce o sec iune a tabloului prin înc rcarea elementelor
sec iunii în memoria shared i prin realizarea opera iei de reducere. Pentru simplitate codul care aduce
datele în memoria shared este omis din fig. 4.2. Opera ia de reducere este realizat pe loc, ceea ce înseamn
elementele din memoria shared sunt înlocuite cu sumele par iale. Fiecare itera ie a buclei for realizeaz o
rund de reducere. Instruc iunea __syncthreads() din interiorul buclei for asigur faptul c toate
sumele par iale de la itera ia precedent au fost determinate i astfel toate firele sunt preg tite s intre în
itera ia curent . Astfel, toate firele care intr în a doua itera ie vor folosi valori determinate la prima itera ie.
Dup prima rund , elementele pare sunt înlocuite de sumele par iale determinate în prima rund . Dup cea
de-a doua rund , elementele ale c ror indici sunt multiplu de 4 sunt înlocuite de sume par iale. Dup ultima
rund , suma total a întregii sec iune se va reg si în elementul 0. În fig. 4.2., linia 3 ini ializeaz variabila
stride cu valoarea 1. În prima itera ie, instruc iunea if de pe linia 6 este folosit pentru a selecta doar
firele pare pentru realizarea adun rii dintre dou elemente vecine.
Modul de execu ie al kernel-ului este ilustrat în fig. 4.3. Firele i elementele tabloului sunt prezentate în
figur ca i coloane, iar con inutul tabloului la finalul itera iilor este indicat prin s ge i. Dup cum este indicat
pe rândul 1, elementele pare ale tabloului con in sumele par iale a câte dou elemente la finalul itera iei 1.
Înaintea celei de-a doua itera ii, valoarea variabilei stride este dublat , devenind 2. În cea de-a doua
itera ie, numai acele fire care sunt multiplu de 4 vor realiza o opera ia de adunare de pe linia din fig. 4.2.
Fiind 512 de elemente în fiecare sec iune, kernel-ul va determina suma întregii sec iuni în 9 runde. Prin
folosirea variabilei blockDim.x ca limit a buclei, kernel-ul presupune c este lansat cu acela i num r de
fire ca i num rul de elemente din fiecare sec iune (adic pentru o sec iune de dimensiunea 512, kernel-ul
trebuie lansat cu 512 de fire).
Fig. 4.3. Execu ia kernel-ului de însumare.
În mod evident, kernel.ul din fig. 4.2 prezint divergen la execu ie. În prima itera ie a buclei, numai acele
fire care au valori pare pentru variabila threadIdx.x vor executa o opera ie de adunare. E nevoie de o
faz pentru a executa aceste fire i de o alt faz pentru firele care nu execut linia 8 din fig. 4.2. La fiecare
itera ie, tot mai pu ine fire vor fi executate, dar în continuare vor fi necesare dou faze pentru execu ia
tuturor firelor. Aceast divergen poate fi îns redus prin modificarea algoritmului.
Fig. 4.4 prezint un kernel u or modificat: în loc de a aduna elementele vecine în prima rund , se adun
elementele care se afl la o distan de o jum tate de sec iune unele de altele. Acest lucru este realizat prin
ini ializarea variabilei stride cu jum tate din dimensiunea sec iunii. Toate elementele perechilor adunate
în prima rund se afl la o distan de jum tate de sec iune unele de altele. Dup prima itera ie, toate sumele
par iale sunt stocate în prima jum tate a tabloului. Se împarte la doi variabila stride înainte de a trece la
urm toarea itera ie. Acest lucru este realizat prin realizarea unei opera ii de shift la dreapta cu un bit (un
mod economicos de realizare a împ irii la doi). Astfel, pentru a doua itera ie, variabila stride devine un
sfert din dimensiunea sec iunii, ceea ce înseamn ca algoritmul adun elemente aflate la o distan de un
sfert de sec iune.
Kernel-ul din fig. 4.4 con ine i el o instruc iune if (linia 6) în interiorul buclei. Num rul de fire care execut
linia 7 r mâne acela i ca i în fig. 4.2. În acest caz îns performan a va fi mai bun din cauza pozi iei
relative a firelor care execut instruc iunea de pe linia 7.
Fig. 4.5. ilustreaz execu ia kernel-ului revizuit. La prima itera ie, toate firele pentru care variabila
threadIdx.x are valori mai mici de jum tate din dimensiunea sec iunii execut instruc iunea de pe linia
7. Pentru o sec iune de 512 elemente, firele 0 pân la 255 execut instruc iunea de adunare la prima itera ie,
în timp ce firele 256 pân la 511 nu o execut . Sumele par iale sunt stocate în elementele 0 pân la 255 dup
prima itera ie. Deoarece un warp const din 32 de fire cu valori consecutive ale variabilei threadIdx.x,
toate firele din warp-urile 0 pân la 7 vor executa instruc iunea de adunare, în timp ce warp-urile 8 pân la
15 sar peste instruc iune. Deoarece toate firele din acela i warp aleg aceea i cale, nu exist divergen .
Fig. 4.4. Kernel revizuit (cu mai pu ine divergen la execu ie).
Kernel-ul din fig. 4.4. nu elimin complet divergen a datorit instruc iunii if. Începând cu cea de-a cincia
itera ie, num rul de fire care execut linia va sc dea sub 32. Astfel, la ultimele cinci itera ii, doar 16, 8, 4, 2,
respectiv un fir vor executa opera ia de adunare. Aceasta înseamn c programul tot va avea divergen în
aceste itera ii.
(a) (b)
Fig. 4.6. Accesarea combinat a memoriei.
Toate loca iile din memoria global formeaz un singur spa iu de adrese, astfel încât fiecare loca ie din
memoria global are o adres unic . De exemplu, dac memoria global con ine 1024 de loca ii, aceste
loca ii vor fi accesate prin adresele 0 pân la 1023. Procesorul GT200 poate avea pân la 4GB (232) loca ii,
cu adrese de la 0 la 232-1. Toate variabilele unui program CUDA sunt plasate în acest spa iu de adrese liniar,
primind o adres unic .
Dup cum a fost prezentat în fig. 1.8 din capitolul 1, pentru limbajele C i CUDA, elementele matricelor
sunt plasate în ordine liniar în memorie pe baza conven iei row major. Astfel, elementele de pe rândul 0 al
unei matrice sunt plasate ini ial în ordine liniar i consecutiv în memorie, urmeaz elementele de pe
rândul 1, .a.m.d. Termenul row major face referire la faptul c plasarea datelor conserv structura
rândurilor. Aceast conven ie este ilustrat în fig. 4.7 (identic cu fig. 1.8), cele 16 elemente ale matricei
4x4 M fiind plasate în loca ii adresate liniar. E clar c M0,0 i M0,1, de i par s fie consecutive în matricea
2D, sunt de fapt plasate la o distan de 4 loca ii în memoria adresat liniar.
Fig. 4.7. Plasarea elementelor unei matrice în ordine liniar .
Se pot acum detalia modurile de accesare favorabile i nefavorabile ale elementelor matricelor stocate în
memoria global din fig. 4.6. Fig. 4.8. prezint un mic exemplu al modului de accesare favorabil pentru
matricea de 4x4 elemente. S geata din partea de sus a fig. 4.8 prezint modul de accesare pentru singur fir.
Acces rile sunt realizate prin intermediul unei bucle, în interiorul c reia firele dintr-un warp acceseaz
elementul 0 al coloanelor la prima itera ie. Dup cum este prezentat în partea de jos a fig. 4.8, aceste
elemente se afl în ordine consecutiv în memoria global (loca iile sunt accesate de firele T(0) pân la
T(3)). Hardware-ul detecteaz faptul c aceste acces ri sunt adresate unor loca ii consecutive din memoria
global i combin acces rile într-una singur .
Fig. 4.9 prezint un mod de accesare a datelor mai pu in favorabil. S geata din partea de sus a figurii arat
fiecare fir al kernel-ului acceseaz succesiv elementele unui rând. Acces rile sunt generate de c tre o
bucl în interiorul c reia firele (T(0), T(1), T(2), T(3)) din acela i warp acceseaz elementul 0 (M0,0, M0,1,
M0,2, M0,3) al celor patru rânduri la prima itera ie. Dup cum se poate observa în partea de jos a fig. 4.9,
aceste elemente se afl în loca ii care sunt la distan de patru elemente unele de altele.
Fig. 4.9. Model de accesare necombinat .
su a Itera ie de înc rcare 1 din partea de jos arat cum firele acceseaz loca ii care nu sunt consecutive la
prima itera ie. Hardware-ul va determina c acces rile acestor elemente nu pot fi combinate. Prin urmare,
când un kernel itereaz peste un rând, acces rile memoriei globale sunt mult mai pu in eficiente decât în
cazul în care un kernel itereaz peste o coloan .
Dac în mod implicit algoritmul necesit parcurgerea datelor pe rânduri, atunci se poate folosi memoria
shared pentru a asigura combinarea acces rilor. Tehnica este ilustrat în fig. 4.10 pentru înmul irea
matricelor. Fiecare fir cite te un rând din matricea Md, un mod de accesare care nu poate fi combinat. Din
fericire se poate îns folosi un algoritm de por ionare care permite accesare combinat . Dup cum a fost
prezentat în capitolul 3, firele unui bloc coopereaz ini ial pentru a aduce datele din aceste por iuni în
memoria shared. Trebuie avut îns grij ca aceste por iuni s fie înc rcate într-un mod combinat. Odat ce
datele au fost aduse în memoria shared, ele pot fi accesate pe baza de rând sau de coloan f s afecteze
performan a, deoarece memoriile shared sunt în a a fel proiectate încât s fie de mare vitez f a necesita
combinarea acces rilor.
Fig. 4.10. Utilizarea memoriei shared pentru facilitarea acces rii combinate a memoriei globale.
În fig. 4.11 se reia fig. 3.7, prezentându-se modul de înc rcare a dou por iuni din matricele Md i Nd în
memoria shared. Fiecare fir din fiecare bloc este responsabil de înc rcarea unui element Md i a unui
element Nd în matricele Mds i Nds în cadrul fiec rei faze definite de bucla for de pe linia 8. În total sunt
LATIME_PORTIUNE2 fire implicate în fiecare por iune. Firele folosesc variabilele threadIdx.y i
threadIdx.y pentru a determina elementul care trebui citit din matrice.
Elementele din Md sunt înc rcate pe linia 9, unde calculul indicelui pentru fiecare fir folose te variabila m
pentru a localiza cap tul din stânga al por iunii. Fiecare rând al por iunii este apoi înc rcat de
LATIME_PORTIUNE fire ale c ror valori threadIdx sunt diferite în dimensiunea x. Deoarece aceste fire
au valori consecutive pentru variabilele threadIdx.x, ele se afl în acela i warp. De asemenea calculul
indicelui din expresia Md[rand * Latime + m*LATIME_PORTIUNE + tx] asigur faptul c firele
acceseaz elemente de pe acela i rând. Întrebarea este dac fire adiacente într-adev r acceseaz elemente
adiacente de pe rând. R spunsul este da, deoarece to i termenii indicelui m*LATIME_PORTIUNE + tx,
mai pu in tx, sunt identici pentru toate firele warp-ului (iar valorile tx sunt adiacente). Hardware-ul
detecteaz faptul c aceste fire, situate în acela i warp, acceseaz loca ii consecutive din memoria global i
ca urmare combin toate acces rile.
global__ void InmultMatrKernel (float* Md, float* Nd, float* Pd, int
latime)
{
7. float Pval = 0;
// Parcurgerea portiunilor din Md si Nd necesare pentru calculul
// elementului Pd
8. for (int m = 0; k < latime/ LATIME_PORTIUNE; ++m) {
Fig. 4.11. Kernel de înmul ire pe por iuni a matricelor care folose te memorie shared.
În cazul matricei Nd, to i termenii indicelui (m*LATIME_PORTIUNE + ty)*Latime + Col, mai
pu in termenul tx din variabila Col, au aceea i valoare pentru toate firele dintr-un warp. Astfel se vor
realiza din nou acces ri combinate.