Sunteți pe pagina 1din 10

3.

Memorii CUDA
3.1. Importan a acces rii eficiente a memoriei
De i existen a multor fire de execu ie care s poat fi selectate pentru execu ie poate teoretic conduce la
tolerarea laten elor lungi ale acces rilor memoriilor, se poate u or ajunge în situa ia în care acces rile
simultane ale memoriei globale s blocheze execu ia tuturor firelor conducând astfel la utilizarea ineficient
a resurselor multiprocesoarelor. Pentru a evita aceast problem , CUDA furnizeaz metode suplimentare de
accesare a memoriei, care s elimine majoritatea cererilor de date adresate memoriei globale.
Pentru a ilustra efectul acces rii eficiente a memoriei globale, se va calcula nivelul de performan al kernel-
ului de înmul ire a matricelor, kernel care a fost prezentat în fig. 2.6, i care este reluat în fig. 3.1. Cea mai
important parte din punctul de vedere al timpului de execu ie este bucla for, care realizeaz produsul scalar.
La fiecare itera ie a acestei bucle, se realizeaz dou acces ri ale memoriei globale i dou opera ii în
virgul mobil (o înmul ire i o adunare). Astfel raportul dintre num rul de opera ii aritmetice i acces ri ale
memoriei globale este de 1:1, sau mai simplu 1.0. Acest raport va fi numit în continuare raport calcule –
acces ri memorie global (CAMG).
Raportul CAMG este extrem de important pentru performan a unui kernel CUDA. De exemplu, GPU-ul
G80 poate realiza acces ri ale memoriei globale cu o rat de 86.4GB/s. Capacitatea de calcul în virgul
mobil este limitat de rata la care se pot aduce datele din memoria global . Deoarece sunt 4 octe i pentru
fiecare variabil de tip float, nu se pot atinge mai multe de 21.6 (adic 86.4/4) giga instruc iuni în virgul
mobil (gigaflops) pe secund . De i 21.6 de gigaflops este un num r acceptabil, acesta reprezint doar o
frac iune din cei 367 de gigaflops de care este capabil G80. Astfel este nevoie s se m reasc raportul
CGMA pentru a atinge un nivel de performan mai înalt.

global__ void InmultMatrKernel (float* Md, float* Nd, float* Pd, int
latime)
{
// Calculul indexului de rand al elementelor din matricele Pd si Md
int Row = blockIdx.y * blockDim.y + threadIdx.y;
// Calculul indexului de coloana al elementelor din matricele Pd si Nd
int Col = blockIdx.x * blockDim.x + threadIdx.x;
float Pval = 0;
// fiecare fir calculeaza un element din portiunea matricei
for (int k = 0; k < latime; ++k)
Pval += Md[Row*latime+k] * Nd[k*latime+Col];
Pd[Row*latime + Col] = Pval;
}

Fig. 3.1. Kernel-ul de înmul ire a matricelor (fig. 2.6).

3.2. Tipuri de memorii CUDA


CUDA pune la dispozi ie diferite tipuri de memorii care pot fi folosite de utilizatori pentru a ob ine raporturi
CGMA ridicate i astfel i viteze de execu ie mari pentru kernel-uri. Fig. 3.2 prezint tipurile de memorie pe
un dispozitiv CUDA. În partea de jos se afl memoria global i memoria constant . Aceste tipuri de
memorie pot fi scrise i citite de c tre codul host prin diverse func ii ale API-ului, introduse în capitolul 1.
Memoria constant poate fi doar citit de c tre dispozitiv, asigur o laten sc zut i o l rgime de band
mare când toate firele citesc simultan aceea i loca ie.
Regi trii i memoria shared sunt memorii on-chip. Aceasta înseamn c variabilele care sunt stocate în
aceste tipuri de memorii pot fi accesate la viteze foarte mari i în paralel. Regi trii sunt aloca i separat
pentru fiecare fir de execu ie (fiecare fir poate accesa doar proprii regi trii). O func ie kernel folose te de
obicei regi trii pentru a stoca valorile variabilelor accesate frecvent. Memoria shared este alocat la nivelul
blocurilor de fire de execu ie. Toate firele dintr-un bloc pot accesa variabile stocate în loca iile memoriei
shared alocate pentru blocul respectiv. Memoria shared reprezint un mod eficient de cooperare între fire:
firele pot utiliza în comun un anumit set de date de intrare sau pot stoca rezultatele intermediare. Prin
declararea unei variabile într-o anumit zon de memorie, practic, un programator CUDA determin
vizibilitatea i viteza de acces a acelei variabile.

Fig. 3.2. Modelul de memorie CUDA.

Tab. 3.1 prezint sintaxa CUDA pentru declararea variabilelor stocate în diverse tipuri de memorie. Fiecare
astfel de declara ie determin domeniul i durata de via a variabilei. Domeniul determin firele care pot
accesa variabila (un singur fir, firele unui bloc sau toate firele grid-ului). Dac domeniul este reprezentat de
un singur fir, o versiune privat a acelei variabile va fi generat pentru fiecare fir, fiecare fir putând accesa
doar versiunea privat dedicat lui. De exemplu, dac un kernel declar o variabil al c rei domeniu este
reprezentat de un singur fir i acel kernel e lansat cu un milion de fire, atunci se vor crea un milion de
versiuni ale acelei variabile, astfel încât fiecare fir ini ializeaz i utilizeaz propria sa variabil .
Durata de via specific intervalul de timp din durata total de execu ie a programului, în care variabila
poate fi utilizat : doar în interiorul execu iei unui kernel sau de-a lungul întregii aplica ii. Dac durata de
via este doar în interiorul unui kernel, atunci variabila trebuie declarat în corpul func iei care reprezint
kernel-ul i va accesibil doar în interiorul codului kernel-ului. Dac kernel-ul este invocat de mai multe ori,
atunci con inutul variabilei nu este men inut între invoc ri. Fiecare invocare trebuie s ini ializeze variabila
pentru a o utiliza. Pe de alt parte, dac durata de via a unei variabile este de-a lungul întregii aplica ii,
atunci trebuie declarat înafara corpului oric rei func ii. Con inutul variabilei este în acest caz men inut de-a
lungul execu iei întregii aplica ii i este disponibil tuturor kernel-urilor

Tab. 3.1. Calificatori de tip ai variabilelor CUDA.


Declararea variabilei Memorie Domeniu Durat de via
Variabile locale, altele decât tablouri Registru Fir Kernel
Variabile locale de tip tablou Local Fir Kernel
__device__, __shared__, int SharedVar; Shared Bloc Kernel
__device__, int GlobalVar; Global Grid Aplica ie
__device__, __constant__, int ConstVar; Constant Grid Aplica ie

Dup cum este indicat în tab. 3.1, toate variabilele scalare (care nu sunt tablouri) declarate local în kernel-uri
sau în func ii de dispozitiv sunt plasate în regi trii. Domeniul acestor variabile este firul de execu ie. Când o
func ie de kernel declar o astfel de variabil , o copie privat a variabilei este generat pentru fiecare fir care
execut func ia. Când un fir î i termin execu ia, toate variabilele sale locale înceteaz s existe. În fig. 3.1,
variabilele tx, ty i Pval sunt toate variabile locale care fac parte din aceast categorie. Accesarea acestor
variabile este extrem de rapid , dar num rul lor trebuie s nu dep easc capacitatea limitat de regi trii a
hardware-ului disponibil (acest aspect va fi adresat în capitolul urm tor).
Variabilele de tip tablou declarate local nu sunt stocate în regi trii. Ele sunt stocate în memoria global i
necesit timpi de accesare lungi. Domeniul acestor variabile este îns , la fel ca i cel al variabilelor scalare,
doar firul de execu ie. Astfel se va crea o versiune privat a fiec rui tablou local pentru fiecare fir de
execu ie. Odat ce un fir î i termin execu ia, con inutul variabilelor locale de tip tablou este eliminat.
Experien a ne arat îns c rareori este nevoie s se foloseasc variabile locale de tip tablou.
Dac declararea unei variabile este precedat de cuvântul cheie __shared__, atunci se va declara o
variabil de tip shared. Op ional se poate ad uga declara ia __device__ în fa a cuvântului cheie
__shared__, dar efectul va fi acela i. În mod tipic aceste declara ii sunt localizate în interiorul func iilor
de kernel sau de dispozitiv. Domeniul unei variabile shared este blocul de fire, ceea ce înseamn c toate
firele de execu ie ale unui bloc au acces la aceea i versiune a variabilei de tip shared. O versiune privat a
variabilei shared este creat pentru i utilizat de c tre fiecare bloc de fire de-a lungul execu iei unui kernel.
Durata de via a unei variabile shared se încadreaz în durata de via a kernel-ului. Când un kernel î i
finalizeaz execu ia, con inutul variabilelor shared este eliminat. Variabilele shared reprezint un mod
eficient de colaborare între firele unui bloc. Accesul memoriei shared este extrem de rapid i poate fi realizat
în paralel de c tre fire. Programatorii CUDA folosesc de obicei memoria shared pentru a stoca o zon din
memoria global care este utilizat intens pe parcursul kernel-ului.
Dac declararea unei variabile este precedat de cuvântul cheie __constant__, acea variabil va fi o
variabil CUDA de tip constant. i în acest caz se poate ad uga op ional cuvântul cheie __device__ în
fa a cuvântului cheie __constant__, dar efectul va fi acela i. Variabilele constante trebuie declarate
înafara corpului unei func ii. Domeniul unei astfel de variabile cuprinde toate grid-urile de fire, ceea ce
înseamn c toate grid-urile au acces la aceea i versiune a variabilei constante. Durata de via a unei
variabile constante este dat de durata de via a întregii aplica ii. Variabilele constante sunt de obicei
folosite ca i variabile de intrare ale kernel-urilor, ele sunt stocate în memoria global dar sunt accesate prin
cache pentru a asigura o accesare eficient . Dac se folosesc modele de accesare corespunz toare, accesarea
devine extrem de rapid . Momentan dimensiunea total a acestor variabile este limitat la 65.535 de octe i.
O variabil a c rei declara ii este precedat doar de cuvântul cheie __device__ este o variabil global i
va fi plasat în memoria global . Accesarea variabilelor globale este lent , dar acestea sunt vizibile pentru
toate firele tuturor kernel-urilor. De asemenea, con inutul lor este men inut de-a lungul execu iei întregii
aplica ii. Astfel memoria global poate fi folosit pentru a asigura colaborarea firelor din blocuri diferite.
Totu i trebuie avut în vedere faptul c nu exist nici un mod de sincronizare între firele din diferite blocuri
sau de a asigura consisten a datelor când fire din diferite blocuri acceseaz aceea i loca ie (singurul mod de
sincronizare între blocuri este de a finaliza execu ia kernel-ului). De aceea variabilele globale sunt de obicei
folosite pentru a stoca datele între execu iile diferite ale kernel-uri.
Trebuie men ionat faptul c exist o limitare în ceea ce prive te utilizarea de pointeri împreun cu variabile
declarate în memoria unui dispozitiv CUDA. În general pointeri fac referire la obiecte din memoria global .
Sunt dou situa ii tipice de utilizare a pointerilor în kernel-uri i func ii de dispozitiv. În primul rând, dac
un obiect este alocat de c tre host, pointerul c tre obiect este ini ializat de c tre func ia cudaMalloc() i
poate fi folosit ca parametru al func iei kernel-ului (precum parametrii Md, Nd i Pd în fig. 3.1.). Al doilea
mod de utilizare presupune atribuirea adresei unei variabile declarate în memoria global c tre o variabil
de tip pointer (de exemplu {float *ptr = &GlobalVar;}).

5.3. Strategie de reducere a traficului cu memoria global


Exist un compromis intrinsec la utilizarea diferitelor tipuri de memorii CUDA. Memoria global este de
dimensiuni mari dar lent , memoria shared este de dimensiuni mici dar rapid . O strategie aplicat uzual
este de a împ i datele în subseturi numite por iuni care s încap în memoria shared. Un criteriu important
la realizarea acestei ac iuni este ca toate calculele efectuate pentru a anumit por iune s poat fi realizate
independent de calculele altor por iuni. Nu toate structurile de date pot fi împ ite în acest sens.
Conceptul de împ ire poate fi ilustrat cu ajutorul exemplului de înmul ire a matricelor. Fig. 3.3 ilustreaz
un mic exemplu de înmul ire între matrice folosind mai multe blocuri. Exemplul corespunde kernel-ului din
fig. 3.1 i presupune c se folosesc 2x2 blocuri pentru calcula matricea Pd. Fig. 3.3 eviden iaz calculele
efectuate de cele patru fire ale blocului (0, 0). Aceste patru fire calculeaz loca iile Pd0,0, Pd1,0, Pd0,1 i Pd1,1.
Acces rile elementelor matricelor Md i Nd de c tre firele (0, 0) i (0, 1) ale blocului (0, 0) sunt eviden iate
cu s ge i.

Fig. 3.3. Exemplu de înmul ire a matricelor folosind mai multe blocuri.

Fig. 3.4 ilustreaz acces rile memoriei globale realizate de toate firele blocului (0, 0). Firele sunt prezentate
în direc ie orizontal , iar momentele acces rilor sunt aranjate pe direc ia vertical . Trebuie remarcat faptul
fiecare fir acceseaz patru elemente din matricea Md i patru elemente din matricea Nd de-a lungul
execu iei. Printre cele patru fire eviden iate exist numeroase suprapuneri din punctul de vedere al
elementelor accesate din cele dou matrice de intrare: de exemplu firele (0, 0) i (1, 0) acceseaz ambele
loca ia Md1,0 i practic întreg rândul 0 al matricei Md. Similar, firele (1, 0) i (1, 1) acceseaz întreaga
coloan 1 a matricei Nd.
Fig. 3.4. Acces rile memoriei globale realizate de firele din blocul (0,0).

Kernel-ul din fig. 3.1 este scris în a a fel încât firele (0, 0) i (1, 0) acceseaz acelea i elemente din rândul 0
al matricei Md. Dac se reu te într-un anumit fel s se asigure o colaborare între cele dou fire astfel încât
elementele matricei Md s fie accesate o singur dat , atunci se poate reduce num rul total de acces ri la
jum tate. Se poate vedea c în general fiecare element al matricelor Md i Nd este accesat de exact dou ori
de-a lungul execu iei blocului (0, 0). Astfel dac toate cele patru fire ale blocului ar colabora la accesarea
memoriei globale, atunci s-ar putea reduce la jum tate traficul cu memoria global .
Se poate verifica simplu c poten ialul de reducere a traficului cu memoria global este propor ional cu
dimensiunea blocurilor utilizate. Cu blocuri de NxN fire, traficul ar putea fi redus la a N-a parte (pentru
blocuri de 16x16, traficul cu memoria global poate fi redus la 1/16 dac se asigur o colaborare
corespunz toare între fire).
Se prezint în continuare un algoritm care asigur colaborarea firelor în vederea reducerii traficului cu
memoria global . Ideea de baz este c firele colaboreaz la citirea elementelor matricelor Md i Nd,
aducându-le în memoria shared înainte de a utiliza individual elementele la calculul produselor scalare.
Trebuie inut cont de faptul c memoria shared este mic , nefiind permis dep irea capacit ii acesteia la
citirea elementelor matricelor Md i Nd. Acest lucru poate fi asigurat prin generarea unor por iuni mai mici
la împ irea matricelor. Astfel, dimensiunea por iunilor va fi aleas în a a fel încât s nu se dep easc
cantitatea de memorie shared disponibil . În cea mai simpl , dimensiunile por iunilor sunt egale cu cele ale
blocurilor, a a cum este ilustrat în fig. 3.5.
În fig. 3.5, matricele Md i Nd sunt împ ite în por iuni de 2x2, conform delimit rii realizate cu linii
îngro ate. Calculele efectuate pentru realizarea produsului scalar sunt acum împ ite în faze. În fiecare faz ,
toate firele din bloc colaboreaz pentru a înc rca o por iune a matricei Md i o por iune a matricei Nd în
memoria shared, dup cum este ilustrat i în fig. 5.6 (fiecare rând din fig. 5.6 prezint activit ile efectuate
de un fir - timpul este ilustrat pe vertical ). Sunt prezentate doar activit ile firelor din blocul (0, 0) deoarece
toate celelalte blocuri au acela i comportament. Tabloul din memoria shared destinat elementelor matricei
Md se nume te Mds iar tabloul din memoria shared destinat elementelor matricei Nd se nume te Nds. La
începutul fazei 1 cele patru fire ale blocului (0, 0) încarc prin colaborare o por iune a matricei Md în
memoria shared: firul (0, 0) încarc elementul Md0,0 în Mds0,0, firul (1, 0) încarc elementul Md1,0 în
Mds1,0, firul (0, 1) încarc elementul Md0,1 în Mds0,1 iar firul (1, 1) încarc elementul Md1,1 în Mds1,1. În a
doua coloan a fig. 3.6 se încarc o por iune a matricei Nd în memoria shared.
Dup ce au fost înc rcate cele dou por iuni în memoria shared, valorile sunt folosite la calculul produsului
scalar. Fiecare valoarea din memoria shared este utilizat de dou ori. De exemplu, valoarea Md1,1, înc rcat
de firul (1, 1) în Mds1,1 este utilizat de dou ori: o dat de firul (0, 1) i o dat de firul (1, 1). Astfel, prin
înc rcarea elementelor din memoria global în memoria local se reduce num rul de acces ri ale memoriei
globale. În acest caz num rul de acces ri se reduce la jum tate i în general se reduce cu factorul N pentru
por iuni de NxN elemente.
Fig. 3.5. Împ irea pe por iuni a matricelor Md i Nd pentru a facilita utilizarea memoriei shared.

Fig. 3.6. Fazele de execu ie ale înmul irii pe por iuni a matricelor.

Calculul fiec rui produs scalar din fig. 3.6 este în acest caz realizat în dou faze. În fiecare faz produsele a
dou perechi de elemente din cele dou matrice de intrare sunt adunate la valoarea variabilei Pval. Prima
faz de calculare este prezentat în coloana 4 a fig. 3.6, iar cea de-a doua faz este prezentat în coloana 7.
În general dac o matrice de intrare are dimensiunea N i por iunea este de dimensiune
LATIME_PORTIUNE, produsul scalar este realizat în N/LATIME_PORTIUNE faze. Crearea acestor faze
reprezint cheia reducerii acces rilor memoriei globale. Deoarece fiecare faz se concentreaz pe un subset
mic de date, firele pot colabora la înc rcarea subsetului de date în memoria shared i pot apoi folosi valorile
din memoria shared pentru a realiza calculele.
De asemenea, Mds i Nds sunt reutilizate pentru memorarea valorilor de intrare. În fiecare faz , acelea i
zone Mds i Nds sunt utilizate pentru a stoca subsetul de elemente din Md i Nd utilizate în faza respectiv .
Astfel o zon de memorie shared mic deserve te toate necesit ile de accesare a memoriei globale. Acest
comportament de accesare local se nume te localitate. Dac un algoritm are proprietatea de localitate,
atunci este posibil s se utilizeze memorii mici, de vitez mare pentru a asigura majoritatea acces rilor i
pentru a elimina astfel o mare parte din acces rile memoriei globale. Localitatea este important în
ob inerea unor performan e ridicate atât în cazul arhitecturilor CPU multicore cât i în cazul arhitecturilor
many-core de tip GPU. Se poate prezenta în continuare kernel-ul de înmul ire a matricelor care folose te
por iuni i memorie shared pentru a reduce traficul cu memoria global . Kernel-ul din fig. 3.7
implementeaz fazele din fig. 3.6. În fig. 3.7, liniile 1 i 2 declar variabilele Mds i Nds în memoria
shared.

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 = threadIdx.x; int ty = threadIdx.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] * Nds[k][tx];
14. __syncthreads();
}
}
15. Pd[Row*latime + Col] = Pval;

Fig. 3.7. Kernel de înmul ire pe por iuni a matricelor, folosind memorie shared.

Se reaminte te faptul c domeniul variabilelor shared este blocul de fire. Astfel toate firele aceluia i bloc
vor avea acces la acelea i tablouri Mds i Nds. Acest aspect este important, deoarece toate firele dintr-un
bloc trebuie s aib acces la elementele din Md i Nd înc rcate în Mds i Nds de c tre firele blocului
respectiv.
Liniile 3 i 4 stocheaz valorile threadIdx i blockIdx în variabile locale (regi trii) pentru a asigura un
acces rapid. Astfel o versiune privat a variabilelor tx, ty, bx i by este creat de c tre sistemul de
execu ie pentru fiecare fir în parte. Valorile lor sunt stocate în regi trii accesibili doar de c tre un fir. Odat
ce firul î i încheie execu ia, valorile acestor variabile sunt eliminate.
Liniile 5 i 6 determin indec ii de rând i de coloan ai elementului Pd pe care firul respectiv trebuie s -l
calculeze. Dup cum se indic în fig. 3.8, indicele de coloan (x) este calculat cu expresia
bx*LATIME_PORTIUNE + tx. Acest aspect este dat de faptul c fiecare bloc gestioneaz
LATIME_PORTIUNE elemente în dimensiunea x.
Linia 8 din fig. 3.7 marcheaz începutul buclei care parcurge toate fazele necesare calculului valorii finale a
elementului din matricea Pd. Fiecare itera ie a buclei corespunde unei faze de calcul din fig. 3.6. Variabila m
indic num rul de faze care au fost deja parcurse la realizarea produsului scalar. Fiecare faz folose te o
por iune de elemente din Md i o por iune de elemente din Nd. Astfel, la începutul fiec rei faze,
m*LATIME_PORTIUNE perechi de elemente din Md i Nd au fost procesate în faze anterioare.
Toate firele unui grid execut aceea i func ie a kernel-ului. Variabila threadIdx le permite firelor s
identifice por iunea de date pe care trebuie s o proceseze. De asemenea, firul cu by = blockIdx.y i
ty = threadIdx.y proceseaz rândul (by*LATIME_PORTIUNE + ty) din Md, dup cum este
indicat în partea stâng a fig. 3.8. Linia 5 stocheaz acest num r în variabila Row a fiec rui fir. De
asemenea, firul cu bx = blockIdx.x i tx = threadIdx.x proceseaz coloana
(bx*LATIME_PORTIUNE + tx) din Nd, dup cum este indicat în partea superioar a fig. 3.8. Linia 6
stocheaz acest num r în variabila Col a fiec rui fir. Aceste variabile vor fi utilizate când firele încarc
elementele din Md i Nd în memoria shared.
La fiecare faz , linia 9 încarc elementul Md corespunz tor în memoria shared. Deoarece se cunosc deja
indec ii de rând i de coloan ai elementelor din Md i respectiv Nd care trebuie procesate de c tre fir,
aten ia va fi îndreptat înspre indexul de coloan din Md i indexul de rând din Nd. Dup cum este indicat
în fig. 3.8, fiecare bloc are LATIME_PORTIUNE2 fire care colaboreaz pentru a înc rca
LATIME_PORTIUNE2 elemente din Md în memoria shared. Aceast ac iune este realizat cu ajutorul
variabilelor blockIdx i threadIdx. Indicele de început al elementelor din Md, care trebuie înc rcate,
este m*LATIME_PORTIUNE. Astfel o abordare simpl este folosirea fiec rui fir pentru înc rcarea unui
element identificat de valoarea threadIdx. Abordarea aceasta este prezentat pe linia 9, unde fiecare fir
încarc elementul Md[Row*Width + (m*TILE_WIDTH + tx)]. Deoarece valoarea variabilei Row
este o func ie liniar a lui ty, fiecare din cele LATIME_PORTIUNE2 fire va înc rca un singur element din
Md în memoria shared. Împreun , aceste fire vor înc rca por iunea portocalie din Md indicat în fig. 3.8.
Linia 11 apeleaz func ie de sincronizare la barier __synchthreads() pentru a asigura faptul c toate
firele din acela i bloc au terminat înc rcarea elementelor din por iunile din Md i Nd în Mds i Nds. Odat
ce por iunile din Md i Nd sunt înc rcate în Mds i Nds, bucla de pe linia 12 execut fazele produsului
scalar bazate pe baza acestor elemente. Progresul buclei pentru firul (tx, ty) este ilustrat în fig. 3.8, direc ia
utiliz rii datelor din Md i Nd fiind marcat cu k, variabila de iterare a buclei de pe linia 12. Linia 14
apeleaz func ia de sincronizare la barier __synchthreads() pentru a asigura faptul c toate firele
blocului au finalizat utilizarea con inutului matricelor Mds i Nds înainte ca vre-un fir s treac la
urm toare itera ie i s încarce urm toarea por iune din Md i Nd.
Algoritmul cu por iuni prezint avantaje substan iale. Pentru înmul irea matricelor, acces rile memoriei
globale sunt reduse cu un factor egal cu LATIME_PORTIUNE. Dac se folosesc por iuni de 16x16
elemente, acces rile memoriei globale sunt reduse la 1/16. Cu ajutorul acestei reduceri, l rgimea de band
de 86.4GB/s poate deservi un num r mult mai mare de calcule în virgul mobil decât în versiunea ini ial a
codului. Mai exact, se pot ob ine acum [(86.4/4) x 16] = 345.6 gigaflops, adic foarte aproape de
performan a maxim . Astfel l rgimea de band nu mai este factorul limitativ principal al performan ei
aplica iei de înmul ire a matricelor.
Fig. 3.8. Calculul indicilor matricelor în cazul unei înmul iri pe por iuni.

3.4. Memoria ca factor limitativ al paralelismului


De i regi trii, memoria shared i memoria constant din modelul CUDA pot reprezenta memorii extrem de
eficiente la reducerea num rului de acces ri ale memoriei globale, trebuie respectat capacitatea maxim
acestor memorii. Fiecare dispozitiv CUDA pune la dispozi ie o anumit cantitate de memorie, limitându-se
astfel num rul de fire de execu ie care se pot afla simultan pe un multiprocesor. În general, cu cât un fir
necesit mai multe loca ii de memorie, cu atât se va limita mai mult num r de fire care se pot afla simultan
pe un multiprocesor i astfel i pe întreg GPU-ul.
Pentru GPU-ul G80, fiecare multiprocesor are 8K (8192) regi trii, adic 128K pentru întreg procesorul.
Acesta pare un num r foarte mare, dar permite fiec rui fir s utilizeze numai un num r limitat de regi trii.
Fiecare multiprocesor poate avea maxim 768 de fire. Pentru a utiliza la maxim aceast capacitate, fiecare fir
poate utiliza maxim 8K/768 = 10 regi trii. Dac fiecare fir folose te 11 regi trii, num rul de fire care se pot
executa simultan pe multiprocesor este redus. Aceast reducere este realizat la nivel de bloc. De exemplu,
dac fiecare bloc con ine 256 de fire, num rul de fire va fi redus cu un multiplu de 256. Astfel de la 768 de
fire se scade direct la 512, deci o reducere cu o treime a firelor care se pot afla simultan pe un multiprocesor.
Astfel se poate reduce semnificativ num rul de warp-uri disponibile la planificare, i astfel i capacitatea de
a g si fire care s fie executate atunci când altele sunt blocate de opera ii cu laten mare.
Utilizarea memoriei globale poate de asemenea s limiteze num rul de fire asociate simultan unui
multiprocesor. La procesorul G80 sunt disponibili 16KB de memorie shared pentru fiecare multiprocesor.
Memoria shared este folosit de blocuri i fiecare multiprocesor poate avea pân la 8 blocuri. Pentru a
respecta aceast limit , este nevoie ca un bloc s nu foloseasc mai mult de 2KB de memorie shared. De
exemplu, dac fiecare bloc folose te 5KB de memorie shared, nu pot fi asociate mai multe de trei blocuri
unui multiprocesor la un moment dat.
Pentru aplica ia de înmul ire a matricelor, memoria shared poate deveni un factor limitativ. Pentru o
por iune de 16x16, fiecare bloc are nevoie de 16x16x4 = 1KB pentru Mds. Înc 1KB de memorie shared
este necesar pentru Nds. Astfel fiecare bloc folose te 2kB de memorie shared. Memoria shared total de
16kB pe un multiprocesor permite în acest caz executarea simultan a 8 blocuri de fire, nereprezentând un
factor limitativ. În acest caz limitarea efectiv este dat de num rul maxim de 768 de fire care se pot afla
simultan pe un multiprocesor. Se limiteaz astfel num rul de blocuri la 3 i ca urmare doar 3x2KB = 6KB
de memorie shared vor fi utiliza i. Limit rile variaz de la o genera ie la alta dar în acela i timp reprezint
propriet i care pot fi determinate la momentul execu iei. De exemplu seria GT200 permite plasarea
simultan a 1024 de fire pe un multiprocesor.

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