Sunteți pe pagina 1din 12

4.

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 .

4.1. Aspecte suplimentare despre execu ia firelor


Se vor prezenta ini ial câteva aspecte ale execu iei firelor, care pot limita performan a. La lansarea unui
kernel se genereaz un grid de fire care sunt organizate într-o ierarhie cu dou nivele: la primul nivel se afl
un tablou bidimensional de blocuri, iar la al doilea nivel se g se te un tablou tridimensional de fire pentru
fiecare bloc. Blocurile se pot executa în orice ordine, aspect care conduce la scalabilitatea transparent a
kernel-urilor CUDA. În continuare se va discuta ordinea de execu ie a firelor dintr-un bloc.
Din punct de vedere conceptual, firele dintr-un bloc se pot executa în orice ordine. Sincroniz rile la barier
asigur faptul c toate firele au completat o anumit faz a kernel-ului, înainte ca anumite fire s treac în
urm toarea faz . Corectitudinea execu iei firelor nu ar trebui se depind de ordinea de execu ie a firelor din
cadrul unui bloc sau de execu ia simultan a acestora. Datorit unor considerente de cost hardware,
genera ia curent de dispozitive CUDA grupeaz firele la momentul execu iei. Aceast strategie de
implementare conduce la limit ri de performan pentru anumite tipuri de kernel-uri (anumite variante de
cod CUDA). Pentru programatori, este avantajos s modifice aceste variante de cod în alte variante
echivalente care conduc la performan e mai bune.
Dup cum s-a prezentat în capitolul anterior, arhitecturile G80/GT200 grupeaz firele la execu ia acestora:
fiecare bloc este împ it în warp-uri. Aceast tehnic de implementare permite reducerea costurilor
hardware i permite efectuarea unor optimiz ri la accesarea memoriei. Organizarea pe warp-uri va r mâne
valabil la genera iile viitoare de dispozitive, dar e posibil ca dimensiunea warp-urilor s varieze.
Dimensiunea warp-urilor este de 32 pentru arhitecturile G80/GT200, arhitecturi care vor fi folosite în
continuare pentru expunerea modului de parti ionare a warp-urilor.
Blocurile de fire sunt împ ite în warp-uri pe baza indicilor firelor de execu ie. Dac un bloc de fire este
unidimensional, adic doar variabila threadIdx.x este utilizat , atunci modul de parti ionare este
evident(se bazeaz pe valorile cresc toare ale variabilei threadIdx.x). Pentru dimensiunea de 32 de
fire, warp-ul 0 începe de la firul 0 i se termin cu firul 31, warp-ul 1 începe cu firul 32 i se termin cu firul
63, etc. În general warp-ul n începe cu firul 32*n i de termin cu firul 32*(n+1)-1. Dac dimensiunea
unui bloc nu este un multiplu de 32, ultimul warp va fi completat cu fire suplimentare. De exemplu, dac un
bloc are 48 de fire, atunci aceasta va fi împ it în 2 warp-uri, iar warp-ul 1 va fi completat cu 16 fire
suplimentare.
Pentru blocuri care au mai multe dimensiuni, acestea vor fi proiectate într-o ordine liniar înainte de a
realiza împ irea pe warp-uri. Ordinea liniar este determinat prin în iruirea rândurilor cu coordonate y i
z mari dup cele cu valori mici. Astfel, dac un bloc este bidimensional, ordinea liniar este ob inut prin
plasarea tuturor firelor cu threadIdx.y egal cu 1 dup firele cu threadIdx.x egal cu 0. Firele cu
threadIdx.y egal cu 2 vor fi plasate dup firele cu threadIdx.x egal cu 1, .a.m.d.
Fig. 4.1 prezint un exemplu de plasare în ordinea liniar a unor fire dintr-un bloc bidimensional. În partea
de sus se prezint structura bidimensional , fiecare fir fiind marcat cu T(x, y), unde x este threadIdx.x
i y este threadIdx.y. În partea de jos a figurii se prezint ordinea liniar a firelor. Primele patru fire
sunt cele cu threadIdx.y egal cu 0 i sunt plasate în ordinea valorilor variabilei threadIdx.x.
Urm toarele patru fire sunt cele cu threadIdx.y egal cu 1, .a.m.d. În cazul acestui exemplu toate cele
16 fire formeaz o jum tate de warp i warp-ul va fi completat cu alte 16 fire. În cazul unui bloc cu 8x8 fire,
cele 64 de fire ale blocului sunt organizate în dou warp-uri: primul warp începe de la T(0, 0) i se termin
cu T(3, 7). Al doilea warp începe cu T(4, 0) i se termin cu T(7, 7).

Fig. 4.1. Plasarea firelor într-o ordine liniar .

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.

1. __shared float sumaPartiala[]


2. unsigned int t = threadIdx.x;
3. for (unsigned int stride = 1; stride < blockDim.x;
stride*=2)
4. {
5. __syncthreads();
6. if (t % (2*stride) == 0)
7. sumaPartiala[t] += sumaPartiala[t+stride];
8. }
Fig. 4.2. Exemplul unui kernel de însumare.

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 .

1. __shared float sumaPartiala[]


2. unsigned int t = threadIdx.x;
3. for (unsigned int stride = blockDim.x>>1; stride > 0;
stride>>=1)
4. {
5. __syncthreads();
6. if (t < stride)
7. sumaPartiala[t] += sumaPartiala[t+stride];
8. }

Fig. 4.4. Kernel revizuit (cu mai pu ine divergen la execu ie).

Fig. 4.5. Execu ia algoritmului revizuit.

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.

4.2. L rgimea de band a memoriei globale


Unul din cele mai importante considerente ale performan ei kernel-urilor CUDA este modul de accesare a
memoriei globale. Aplica iile CUDA prezint un paralelism extins al datelor, ceea ce înseamn în general c
se proceseaz o cantitate foarte mare de date din memoria global într-un timp foarte scurt. În capitolul 3 s-
au prezentat tehnici de parti ionare care folosesc memoria shared i care reduc cantitatea total de date care
trebuie accesate de câtre o colec ie de fire dintr-un bloc. În acest capitol, se vor discuta suplimentar tehnici
de aliniere care pot transfera mai eficient datele din memoria global în memoria shared i în regi trii.
Tehnicile de aliniere a memoriei sunt de multe ori folosite cu tehnicile de por ionare pentru a permite
dispozitivelor CUDA s i ating maximul de performan prin utilizarea eficient a l rgimii de band a
memoriei globale.
Memoria global dintr-un sistem CUDA este în mod tipic o memorie DRAM (Dynamic Random Access
Memories). Deoarece procesul de citire a acestor memorii este foarte lent, memoriile DRAM moderne
folosesc un proces paralel pentru a cre te rata de accesare a datelor. De fiecare dat când se acceseaz o
loca ie, de fapt se acceseaz multe loca ii consecutive care includ i loca ia cerut ini ial. Astfel toate datele
acestor loca ii vor fi transferate cu mare vitez la procesor. Dac aplica ia poate utiliza date de la loca ii
multiple i consecutive înainte de a trece la urm toarele loca ii, memoriile DRAM pot furniza date la o rat
mult mai mare decât în cazul în care loca iile s-ar accesa într-o ordine aleatoare. Pentru a ajunge aproape de
valoarea maxim a l rgimii de band a memoriei anun ate de produc tor, un kernel trebuie s i organizeze
acces rile datelor în a a fel încât fiecare cerere adresat memoriei RAM s fie realizat pentru un num r
mare de loca ii DRAM consecutive.
Procesoarele G80/GT200 prezint aceste propriet i i permit programatorilor s ob in o eficien mare a
acces rii datelor prin organizarea acces rilor realizate de fire într-un mod favorabil. Aceast tehnic profit
de faptul c firele dintr-un warp execut aceea i instruc iune la orice moment dat. Când toate firele dintr-un
warp execut o instruc iune de înc rcare, hardware-ul determin dac firele acceseaz loca ii consecutive.
Astfel cel mai favorabil mod de accesare este ob inut când o instruc iune a firelor unui warp acceseaz
loca ii consecutive din memoria global . În acest caz, hardware-ul combin toate acces rile i realizeaz
practic o singur opera ie cu memoria global . Numai în acest fel pot fi ob inute rate de accesare apropiate
de cele maxime.
Fig. 4.6 ilustreaz atât un mod de accesare favorabil cât i unul nefavorabil pentru datele unei matrice. Fig.
4.6a prezint modul de accesare realizat de o bucl în interiorul c reia fiecare fir cite te un rând al matricei
Md. Se presupune c firele dintr-un warp citesc rânduri adiacente, adic la itera ia 0, firele dintr-un warp
citesc elementul 0 al rândurilor 0 pân la 31. La itera ia 1, acelea i fire citesc elementul 1 al rândurilor 0
pân la 31. Nici una din aceste acces ri nu va fi combinat . Un mod de accesare mult mai favorabil este
prezentat în fig. 4.6b, unde fiecare fir cite te o coloan a matricei Nd. La itera ia 0, firele din warp-ul 0
citesc elementul 0 al coloanelor 0 pân la 31. Toate aceste acces ri vor fi combinate. Pentru a în elege de ce
un mod de accesare este superior celuilalt, trebuie în eles modul în care sunt plasate elementele matricelor în
memoria global .

(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.8. Model de accesare combinat .

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)
{

1. __shared__ float Mds[LATIME_PORTIUNE][ LATIME_PORTIUNE];


2. __shared__ float Nds[LATIME_PORTIUNE][ LATIME_PORTIUNE];

3. int bx = blockIdx.x; int by = blockIdx.y;


4. int tx = blockIdx.x; int ty = blockIdx.y;

// Identificarea randului si coloanei corespunzatoare elementului Pd de


// calculat
5. int Row = by * LATIME_PORTIUNE + ty;
6. int Col = bx * LATIME_PORTIUNE + tx;

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) {

// Incarcarea prin colaborare a portiunilor din Md si Nd in mem. shared


9. Mds[ty][tx] = Md[Row*latime + (m*LATIME_PORTIUNE + tx)];
10. Nds[ty][tx] = Nd[(m*LATIME_PORTIUNE + ty)*latime + Col];
11. __syncthreads();

12. for (int k = 0; k < LATIME_PORTIUNE; ++k)


13. Pval += Mds[ty][k] * Nd[k][tx];
14. __syncthreads();
}
}
15. Pd[Row*latime + Col] = Pval;

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.

4.3. Împ irea dinamic a resurselor unui multiprocesor


Resursele de execu ie dintr-un multiprocesor cuprind regi trii, pozi ii pentru blocuri de fire i pozi ii pentru
fire. Aceste resurse sunt împ ite dinamic i asociate firelor pentru a permite execu ia acestora. În capitolul
2, s-a men ionat faptul c fiecare multiprocesor al dispozitivului CUDA GT200 are 1024 de pozi ii pentru
fire. Aceste pozi ii de fire sunt împ ite i asociate blocurilor de fire la momentul execu iei. Dac fiecare
bloc const din 256 de blocuri, cele 1024 de pozi ii sunt împ ite la 4 blocuri de fire. Dac fiecare bloc
con ine 128 de fire atunci cele 1024 de pozi ii sunt împ ite la 8 blocuri. Astfel multiprocesoarele sunt
extrem de flexibile, putând executa fie multe blocuri constând din pu ine fire, fie pu ine blocuri constând din
multe fire.
Parti ionarea dinamic a resurselor poate conduce la interac iuni subtile între limit rile de resurse, care la
rândul lor pot conduce la utilizarea ineficient a resurselor. Astfel de interac iuni pot s apar între pozi iile
pentru blocuri i pozi iile pentru fire. Dac fiecare bloc are 64 de fire, atunci cele 1024 de pozi ii pentru fire
pot fi împ ite la 16 blocuri, dar numai 8 blocuri sunt permise pentru un multiprocesor i prin urmare doar 8
blocuri vor fi folosite. Pentru a folosi la maxim pozi iile disponibile pentru blocuri i fire, trebuie ca un bloc
con in cel pu in 128 de fire.
Zona dedicat regi trilor este o alt resurs parti ionat dinamic. Num rul de regi trii ai fiec rui dispozitiv
CUDA depinde de specifica ia acestuia. De exemplu, în cazul dispozitivului G80, fiecare multiprocesor are
pân la 8192 de regi trii. Ace ti regi trii sunt folosi i pentru a stoca variabile folosite frecvent i variabile
generate de compilator pentru a reduce laten a opera iilor i a conserva l rgimea de band a memoriei. Dup
cum a fost prezentat în capitolul 3, variabilele locale ale unui kernel CUDA sunt plasate în regi trii.
Anumite kernel-uri vor fi folosi mai mul i regi trii în timp ce alte kernel-uri vor folosi mai pu ini regi trii.
Prin împ irea dinamic a resurselor între blocuri, multiprocesoarele pot rula simultan mai multe blocuri
dac acestea necesit pu ini regi trii, respectiv mai pu ine blocuri dac acestea necesit mul i regi trii.
Trebui avute îns în vedere i alte interac iuni între limit rile regi trilor i alte limit ri de resurse.
De exemplu, se presupune c la problema de înmul ire a matricelor kernel-ul folose te 10 regi trii. Dac un
bloc are 16x16 fire, câte blocuri pot rula simultan pe un multiprocesor? Pentru a r spunde la întrebare,
trebuie mai întâi calculat num rul de regi trii pentru un bloc: 10x16x16=2560 de regi trii pe bloc. Num rul
de regi trii necesari pentru 3 blocuri este de 7680, care se afl sub limita de 8192. Dac se mai adaug un
bloc, num rul de regi trii cre te la 10240, adic peste limita admis . Astfel, dup cum este ilustrat i în fig.
4.12a, limitarea dat de regi trii permite execu ia simultan a trei blocuri de fire, care împreun totalizeaz
768 de fire (se poate observa c sunt respectate limitele de 8 blocuri i de 1024 de fire pe multiprocesor).
Se presupune c în continuare programatorul mai declar o variabil local , ceea ce înseamn c fiecare fir
are nevoie de 11 regi trii. Ca urmare fiecare bloc va necesita 11x16x16=2816 regi trii, iar num rul de
regi trii necesita i de 3 blocuri devine 8448, dep ind astfel limita maxim . Ca urmare, dup cum este
ilustrat în fig. 4.12b, multiprocesorul gestioneaz aceast situa ie prin reducerea num rului de blocuri cu 1,
reducând astfel num rul de regi trii la 5632. Aceasta îns reduce num rul de fire de la 768 la 512, adic prin
utilizarea unei singure variabile locale suplimentare, paralelismul warp-urilor este redus cu o treime pentru
procesorul G80!
În unele cazuri, ad ugarea unei variabile locale poate conduce la îmbun irea performan ei firelor prin
reducerea num rului de acces ri ale memoriei globale. Îmbun irea ob inut la nivel de fir poate astfel
compensa pierderea paralelismului la nivel de fir de execu ie. De exemplu, se presupune c în kernel-ul
original, exist patru instruc iuni independente între o opera ie de citire a memoriei globale i o opera ie care
utilizeaz valoarea citit . La G80, fiecare instruc iune necesit 4 cicluri pentru procesarea ei, adic cele 4
instruc iuni independente necesit 16 cicluri. Laten a memoriei globale fiind de 200 de cicluri, e nevoie de
200/(4x4) = 14 warp-uri pentru a men ine resursele de calcul mereu ocupate.
Se presupune în continuare c registrul suplimentar permite programatorului sau compilatorului s creasc
num rul de instruc iuni independente de la 4 la 8. Aceste instruc iune independente vor necesita 32 de
cicluri, adic pentru o laten de 200 de cicluri, e nevoie de doar 200/(4x8) = 7 warp-uri pentru a men ine
resursele de calcul mereu ocupate.
Chiar dac num rul de blocuri este redus de la 3 la 2 în cazul ad ug rii unui registru suplimentar, i astfel
num rul de warp-uri scade de la 24 la 16, suficiente warp-uri vor fi prezente pentru a utiliza la maxim
resursele. Un programator trebuie în general s experimenteze pentru a determina varianta optim de cod.

Fig. 4.12. Interac iunea limit rilor de resurse.

4.4. Granularitatea firelor


O decizie important care trebuie luat la proiectarea unui algoritm este granularitatea firelor. De multe este
avantajos s se plaseze o cantitate mai mare de calcule în fiecare fir i s se foloseasc în total mai pu ine
fire. Acest avantaj apare atunci când firele realizeaz opera ii redundante. Algoritmul de parti ionare din fig.
4.11 folose te un fir pentru a calcula un element al matricei de ie ire Md (realizarea unui produs scalar între
un rând din Md i un rând din Nd).
Posibilitatea de ajustare a granularit ii firelor provine de la faptul c mai multe blocuri de fire încarc
acelea i por iuni din matricea Md, realizând prin urmare opera ii redundante. Dup cum este prezentat în
fig. 4.13, la calculul a dou elemente din matricea Pd aflate în por iuni adiacente, se folose te acela i rând
din matricea Md, care este citit în mod redundant de cele dou blocuri. Redundan a poate fi eliminat prin
combinarea celor dou blocuri într-unul singur. Astfel, fiecare fir din noul bloc de fire va calcula dou
loca ii din matricea Pd. Acest lucru este realizat prin revizuirea kernel-ului, astfel încât fiecare fir s
realizeze dou produse scalare. Ambele produse scalare folosesc acela i rând din matricea Mds, dar dou
coloane diferite din matricea Nds. Aceast modificare reduce num rul de acces ri a memoriei globale cu un
sfert.
Posibilul dezavantaj este c noul kernel folose te acum mai mul i regi trii i mai mult memorie shared.
Astfel, num rul de blocuri care ruleaz pe acela i multiprocesor poate s scad . De asemenea se reduce
num rul total de blocuri la jum tate, ceea ce poate conduce la paralelism insuficient pentru matrice mici.
Pentru arhitecturile G80/GT200 de exemplu, s-a determinat faptul c prin combinarea a patru blocuri
orizontale adiacente se ob ine cea mai bun performan pentru înmul irea a dou matrice de 2048x2048 de
elemente.
Fig. 4.13. Granularitate sporit prin utilizarea unor por iuni dreptunghiulare.

S-ar putea să vă placă și