Sunteți pe pagina 1din 62

Capitolul 1

Introducere în arhitectura CELL

În acest capitol se face o familiarizare cu arhitectura Cell, se descriu succint


componentele hardware ce compun acest procesor, se prezintă caracteristicile setului de
instrucţiuni şi tipurile de date disponibile.

1.1. Descrierea arhitecturii CELL

Arhitectura Cell Broadband Engine constă din nouă procesoare pe un singur cip (şapte
pe Cell – PS3), toate interconectate şi având conexiuni la dispozitive externe printr-o
magistrală cu lăţime mare de bandă.
Diagrama bloc a arhitecturii Cell Broadband Engine este prezentată în Figura 1.1.
Principalele elemente sunt:
• PowerPC Processor Element (PPE): PPE este procesorul principal şi conţine un core
RISC cu arhitectura PowerPC pe 64 biţi şi subsistem tradiţional de memorie virtuală. PPE
rulează sistemul de operare, se ocupă de managementul resurselor întregului sistem şi are ca
rol primar asigurarea controlului resurselor, inclusiv alocarea şi managementul threadurilor
SPE. Poate rula software scris pentru arhitectura PowerPC şi e eficient în rularea de cod de
control de sistem. Suportă atât setul de instructiuni pentru PowerPC cât şi setul de instrucţiuni
Vector/SIMD Multimedia Extension.
• Synergistic Processor Elements (SPE-uri). Cele opt SPE-uri (şase pe Cell – PS3) sunt
procesoare de tip SIMD optimizate pentru operaţii cu seturi multiple de date ce le sunt
alocate de către PPE. SPE-urile sunt identice ca arhitectură şi conţin un core RISC, cu
memorie locală de instrucţiuni şi date controlată software ( LS - Local Store) de 256 KB şi un
fişier de regiştri generali cu 128 regiştri de 128 biţi fiecare. SPE-urile suportă un set special
de instrucţiuni SIMD şi folosesc transferuri DMA asincrone pentru a muta date şi instrucţiuni
între spaţiul principal de stocare (main storage, spaţiul de adrese efective care include şi
memoria principală) şi memoriile locale (Local Stores). Transferurile DMA ale SPE-urilor
accesează memoria principală (main storage) folosind adrese efective PowerPC. În cazul
PPE, translatarea adreselor se face de către segmentul de arhitectura PowerPC şi folosind
tabele de paginare. Diferenţa constă în faptul că SPE-urile nu sunt proiectate să se ocupe de
rularea unui sistem de operare.
• Element Interconnect Bus (EIB). Procesorul PPE şi SPE-urile comunică în mod
coerent între ele, cu spaţiul principal de stocare (main storage) şi cu elementele I/O prin
intermediul magistralei EIB. Magistrala EIB are o structură bazată pe 4 inele (două în sens
orar şi două în sens anti-orar) pentru transferul datelor şi o structură arborescentă pentru
comenzi. Lăţimea de bandă internă a magistralei EIB este de 96 bytes pe ciclu şi suportă mai
mult de 100 de cereri DMA în aşteptare între SPE-uri şi spaţiul principal de stocare (main
storage).

Fig. 1.1. Diagrama bloc a arhitecturii Cell Broadband Engine

Aşa cum se observă în Figura 1.1, magistrala EIB cu acces coerent la memorie are
două interfeţe externe:
• Controlerul de interfaţă cu memoria (Memory Interface Controller - MIC) asigură
interfaţa dintre magistrala EIB şi spaţiul principal de stocare. Suportă două canale cu
memoria de tip Rambus Extreme Data Rate (XDR) I/O (XIO) şi accese la memorie pe fiecare
canal de 1-8, 16, 32, 64, sau 128 bytes.
• Interfaţa cu Cell Broadband Engine (Cell Broadband Engine Interface (BEI)) asigură
managementul transferurilor de date între magistrala EIB şi dispozitivele I/O. Asigură
translatarea adreselor, procesarea comenzilor, interfaţarea cu magistrala şi pune la dispoziţie
un controller intern de întreruperi. Suportă două canale de tip Rambus FlexIO external I/O.
Unul dintre aceste canale suportă doar dispozitive I/O non-coerente cu memoria. Cel de-al
doilea canal poate fi configurat să suporte atât transferuri non-coerente cât şi transferuri

2
coerente cu memoria care extind la nivel logic magistrala EIB cu alte dispozitive externe
compatibile, cum ar fi de exemplu un alt Cell Broadband Engine.

Fig. 1.2: Diagrama Bloc a arhitecturii CELL Broadband Engine

Fig. 1.3: Diagrama Bloc a procesorului PPE

3
Fig. 1.4: Diagrama Bloc a procesorului SPE

Fig. 1.5: Topologia de date a magistralei Element Interconnect Bus (EIB)

Dezvoltarea de software in limbajul C/C++ este susţinută de către un set bogat de


extensii de limbaj care definesc tipurile de date din C/C++ pentru operaţii SIMD şi conţin
C/C++ intrinsics (comenzi sub forma de apeluri de funcţii) spre una sau mai multe
instrucţiuni de asamblare.
Aceste extensii de limbaj oferă programatorilor în C/C++ un control mai mare asupra
performanţelor ce pot fi obţinute din cod, fără a fi nevoie de programare în limbaj de
asamblare. Dezvoltarea de software este susţinută şi de existenţa:
• Unui SDK complet bazat pe Linux;
• Unui simulator de sistem;
• Unui set bogat de librării de aplicaţii, unelte de performanţă şi debugging.

4
1.2 PowerPC Processor Element (PPE)

PowerPC Processor Element (PPE) este un procesor cu scop general, dual-threaded,


cu arhitectura RISC pe 64 de biţi conformă cu arhitectura PowerPC, versiunea 2.02, avand
setul de extensii Multimedia Vector/SIMD. Programele scrise pentru procesorul PowerPC
970, de exemplu, pot fi rulate pe Cell Broadband Engine fără nici o modificare.
Aşa cum reiese şi din Figura 1.6, procesorul PPE are doua unităţi principale:
• Power Processor Unit (PPU);
• Power Processor Storage Subsystem (PPSS).
PowerPC Processor Element (PPE) este responsabil de controlul general asupra
sistemului şi rulează sistemele de operare pentru toate aplicaţiile ce rulează pe Cell
Broadband Engine.

Fig. 1.6. Diagrama Bloc a PowerPC Processor Element (PPE)

Power Processor Unit (PPU) se ocupă de controlul şi execuţia instrucţiunilor. Acesta


conţine:
• setul complet de regiştri PowerPC pe 64 biţi;
• 32 regiştri de vector pe 128 de biţi;
• un cache de instrucţiuni de nivel 1 (L1) de 32 KB;
• un cache de date de nivel 1 (L1) de 32 KB;
• o unitate de control de instrucţiuni;
• o unitate pentru load and store;
• o unitate pentru numere întregi în virgulă fixă;
• o unitate pentru numere în virgulă mobilă;
• o unitate pentru vectori;
5
• o unitate de predicţie de ramificaţie;
• o unitate de management a memoriei virtuale.
Power Processor Unit (PPU) suportă execuţia simultană a două threaduri şi poate fi
privit ca un multiprocesor 2-way cu flux de date partajat (shared dataflow). Din punct de
vedere software, acesta este văzut ca două unităţi de procesare independente.
Power Processor Storage Subsystem (PPSS) se ocupă cu cererile de acces la memorie
venite din partea PPE şi cererile externe pentru PPE venite din partea altor procesoare şi
dispozitive I/O.
Acesta conţine:
• un cache de nivel 2 (L2) unificat de date şi instrucţiuni de 512 KB;
• o serie de cozi (queues);
• o unitate de interfaţă cu magistrala cu rol de arbitru de magistrală EIB.
Memoria este văzută ca vector liniar de bytes indexaţi de la 0 la 264 - 1. Fiecare byte
este identificat prin indexul său, numit adresa, şi conţine o valoare. Se face câte un singur
acces la memorie odată.
Cache-ul de nivel 2 (L2) şi cache-urile folosite pentru translatarea adreselor tabele de
management care permit controlul lor din software. Acest control software asupra resurselor
de cache este în special util pentru programarea de timp real.

1.3 Synergistic Processor Elements (SPE-uri)

Fiecare dintre cele opt Synergistic Processor Elements (SPE-uri) este un procesor
RISC pe 128 biţi specializat în aplicaţii SIMD ce necesită calcul intens asupra unor seturi
multiple de date.
Aşa cum reiese şi din Figura 1.7, fiecare Synergistic Processor Element (SPE) conţine
două unităţi principale:
• Synergistic Processor Unit (SPU);
• Memory Flow Controller (MFC).

6
Fig. 1.7. Diagrama Bloc a Synergistic Processor Element (SPE)

Synergistic Processor Unit (SPU) se ocupă în primul rând de controlul şi execuţia


instrucţiunilor.
Conţine:
• un singur fişier de regiştri cu 128 regiştri, fiecare de 128 biţi;
• o memorie locală (Local Store - LS) unificată (instrucţiuni şi date) de 256 KB;
• o unitate de control a instrucţiunilor;
• o unitate de load and store;
• două unităţi pentru numere în virgulă fixă;
• o unitate pentru numere în virgulă mobilă;
• o interfaţă DMA.
Synergistic Processor Element (SPU) implementează un nou set de instrucţiuni SIMD,
numit SPU Instruction Set Architecture, care e specific pentru Broadband Processor
Architecture. Fiecare Synergistic Processor Unit (SPU) este un procesor independent cu
numărător (counter) propriu de program şi este optimizat pentru rularea de threaduri SPE
lansate de către PowerPC Processor Element (PPE). Instrucţiunile pentru Synergistic
Processor Unit (SPU) sunt aduse din memoria locală (Local Store – LS) iar datele sunt aduse
şi salvate tot în memoria locală. Fiind proiectată pentru a fi accesată în primul rand de către
SPU-ul propriu, memoria locală este neprotejată şi netranslatată. Memory Flow Controller
(MFC) conţine un controller DMA pentru transferurile DMA. Programele care rulează pe
SPU, pe PPE sau pe alt SPU, folosesc transferuri DMA controlate de MFC pentru mutarea
datelor şi instrucţiunilor între memoria locală (local store – LS) a SPU-urilor şi spaţiul
principal de stocare (main storage). Spaţiul principal de stocare este format din spaţiul de
adrese efective care include memoria principală (main memory), memoriile locale ale altor

7
SPE-uri şi regiştri mapaţi în memorie cum ar fi regiştrii I/O [MMIO]. Memory Flow
Controller (MFC) interfaţează Synergistic Processor Unit (SPU) cu Element Interconnect Bus
(EIB), implementează facilităţile de rezervare bandwidth pe magistrală şi sincronizează
operaţiile dintre Synergistic Processor Unit (SPU) şi celelalte procesoare din sistem.
Pentru transferurile DMA, Memory Flow Controller (MFC) foloseşte cozi de comenzi
DMA. După ce o comandă DMA a fost transmisă către Memory Flow Controller (MFC),
Synergistic Processor Unit (SPU) poate continua execuţia instrucţiunilor în timp ce Memory
Flow Controller (MFC) procesează comenzile DMA autonom şi asincron. Execuţia de
comenzi DMA de către Memory Flow Controller (MFC) autonom faţă de execuţia de
instrucţiuni de către Synergistic Processor Unit (SPU) permite planificarea eficientă a
transferurilor DMA pentru a acoperi latenţa de memorie.
Fiecare transfer DMA poate avea maxim 16 KB. Totuşi, doar SPU-ul asociat MFC-
ului poate lansa lista de comenzi DMA. Acestea pot conţine până la 2048 transferuri DMA,
fiecare de câte 16 KB. Informaţia cu privire la translatarea adreselor de memorie virtuală este
pusă la dispoziţia MFC de către sistemul de operare ce ruleaza pe PPE. Atributele sistemului
de stocare (translatarea şi protecţia adreselor) sunt controlate prin tabelele de segmentare şi
paginare ale arhitecturii PowerPC. Totuşi există software special pentru PPE care poate mapa
adresele şi memoriile locale (local store – LS) şi anumite resurse MFC în spaţiul de adrese
din main-storage, permiţând astfel PPE şi altor SPU-uri din sistem să acceseze aceste resurse.
SPE-urile oferă un mediu de operare determinist. Acestea nu au memorii cache, astfel
că nu există cache miss-uri care să le afecteze performanţa. Regulile de planificare pe
pipeline sunt simple, astfel că performanţele codului sunt uşor de evaluat static. Deşi
memoria locală (local store – LS) este partajată între operaţiile DMA de citire şi scriere, load
and store şi de prefetch de instrucţiuni, operaţiile DMA sunt cumulate şi pot accesa memoria
locală (LS) cel mult unul din 8 cicluri. La prefetch de instrucţiuni sunt aduse cel puţin 17
instrucţiuni secvenţiale de pe ramura ţintă. În acest mod, impactul operaţiilor DMA asupra
timpilor de operaţii load and store şi de execuţie a programelor este limitată din designul
arhitecturii.

1.4 Ordonarea byte-ilor şi numerotarea biţilor

Setul de instrucţiuni pentru PPE este o versiune extinsă a setului de instrucţiuni


PowerPC. Extensiile sunt reprezentate de către setul de instrucţiuni Multimedia Vector/SIMD
plus câteva adăugări şi schimbări aduse setului de instrucţiuni PowerPC. Setul de instrucţiuni
8
pentru SPE este asemănător cu setul de instrucţiuni Multimedia Extins Vector/SIMD al PPE.
Deşi PPE şi SPE-urile execută instrucţiuni SIMD, seturile de instrucţiuni sunt diferite pentru
fiecare din ele (PPE şi SPE), iar programele scrise pentru PPE şi SPE-uri trebuie compilate cu
compilatoare diferite.
Stocarea datelor şi instrucţiunilor în Cell Broadband Engine respectă ordonarea big-
endian. Acest tip de ordonare are următoarele caracteristici:
• Byte-ul cel mai semnificativ este stocat la cea mai mica adresă, iar cel mai puţin
semnificativ byte este stocat la cea mai mare adresă.
• Numerotarea biţilor într-un byte începe de la cel mai semnificativ bit (bitul 0) până la
cel mai puţin semnificativ bit (bitul n). Acest lucru diferă faţă de alte procesoare care
folosesc tot ordonarea big-endian. Aceste aspecte sunt reprezentate grafic în Figura 1.8.

Fig. 1.8. Ordonarea Big-endian a byte-ilor şi numerotarea biţilor în arhitectura Cell BE

1.5 Vectorizarea SIMD

Un vector este un operand pentru o instrucţiune şi conţine un set de elemente (date)


grupate sub forma unui tablou (array) uni-dimensional. Elementele pot fi numere întregi sau
în virgulă mobilă. Majoritatea instrucţiunilor SPU şi din setul Multimedia Extins
9
Vector/SIMD au ca operanzi vectori. Vectorii mai sunt numiţi şi operanzi SIMD sau operanzi
împachetaţi.
Procesarea SIMD exploatează paralelismul la nivel de date. Paralelismul la nivel de
date se referă la faptul că operaţiile ce trebuie aplicate pentru a transforma un set de elemente
grupate într-un vector pot fi aplicate simultan asupra tuturor elementelor. Cu alte cuvinte,
aceeaşi instrucţiune poate fi aplicată simultan asupra mai multor elemente de date.
Suportul pentru operaţii SIMD este omniprezent în arhitectura Cell Broadband
Engine. În PPE, suportul este asigurat prin setul de instrucţiuni Multimedia Extins
Vector/SIMD. În SPE-uri, suportul este asigurat de către setul de instruţiuni al SPU.
Atât în PPE cât şi în SPE-uri, regiştrii de vectori conţin mai multe elemente de date
sub forma unui singur vector. Regiştrii şi căile de date care suportă operaţiile SIMD sunt pe
128 biţi. Aceasta înseamnă că patru cuvinte pe 32 biţi pot fi încărcate într-un singur registru
şi, de exemplu, pot fi adunate cu alte patru cuvinte dintr-un alt registru într-o singură operaţie.
Acest exemplu este reprezentat grafic în Figura 1.9. Operaţii similare pot fi efectuate cu
operanzi vectori conţinând 16 bytes, 8 semicuvinte sau 2 dublucuvinte.

Fig. 1.9: Patru operaţii de adunare executate simultan

Procesul de pregătire al unui program pentru a fi folosit pe un procesor ce lucrează cu


vectori se numeşte vectorizare (vectorization sau SIMDization). Acest proces poate fi făcut
manual de către programator sau de către un compilator capabil de auto-vectorizare.
În Figura 1.10 se poate vedea un alt exemplu de operaţie SIMD – operaţia de byte-
shuffle. Selecţia byte-ilor pentru operaţia de shuffle din regiştrii sursa (VA şi VB) se face pe
baza informaţiilor din vectorul de control din registrul VC, în care un 0 indica VA ca sursa iar
un 1 indica VB ca sursă. Rezultatul operaţiei de shuffle este salvat în registrul VT.

10
Fig. 1.10: Operaţia de Byte-shuffle

1.6 Tipurile de date vector

Modelul Multimedia Extins Vector/SIMD adaugă un set de tipuri de date


fundamentale, numite tipuri de vectori (vector types).
Tipurile de vectori sunt afişate în Tabelul 1.1. Valorile reprezentate sunt în notaţie
zecimală (baza 10). Regiştrii de vectori sunt pe 128 biţi şi pot conţine:
• 16 valori pe 8 biţi, cu semn sau fără semn;
• 8 valori pe 16 biţi, cu semn sau fără semn;
• 4 valori pe 32 biţi, cu semn sau fără semn;
• 4 valori de numere în virgulă mobilă IEEE-754 în simplă precizie.
Toate tipurile de vectori folosesc prefixul ”vector” în faţa tipului de date standard C -
de exemplu: vector signed int sau vector unsigned short. Un tip de date
vector reprezintă un vector cu atâtea elemente de tip standard C, cât încap într-un registru de
128 biţi. Astfel, un vector signed int este un operand pe 128 de biţi care conţine patru
elemente signed int pe 32 de biţi. Un vector unsigned short este un operand pe 128 de biţi
care conţine opt elemente unsigned short pe 16 biţi.

Tabelul 1.1 – Tipurile de date din setul Multimedia Extins Vector/SIMD

Tipuri de date vector Semnificaţie Valori SPU/PPU

vector unsigned char Sixteen 8-bit unsigned values 0…255 Ambele

vector signed char Sixteen 8-bit signed values -128…127 Ambele

vector bool char Sixteen 8-bit unsigned 0(false), 255 (true) Ambele

11
boolean

vector unsigned short Eight 16-bit unsigned values 0…65535 Ambele

vector unsigned short


Eight 16-bit unsigned values 0…65535 Ambele
int

vector signed short Eight 16-bit signed values -32768…32767 Ambele

vector signed short int Eight 16-bit signed values -32768…32767 SPU

vector bool short Eight 16-bit unsigned values 0(false), 65535 (true) Ambele

vector bool short int Eight 16-bit unsigned values 0(false), 65535 (true) SPU

vector unsigned int Four 32-bit unsigned values 0…232-1 SPU

vector signed int Four 32-bit signed values -231…231-1 PPU

vector bool int Four 32-bit signed values 0 (false), 231-1 (true) PPU

vector float Four 32-bit unsigned values IEEE-754 values PPU

vector pixel Eight 16-bit unsigned values 1/5/5/5 pixel PPU

1.7 Threaduri şi taskuri

Într-un sistem care rulează sistemul de operare Linux, threadul principal al unui
program este un thread Linux care rulează pe PPE. Threadul principal Linux al programului
poate crea unul sau mai multe taskuri Linux pentru Cell Broadband Engine.
Un task Linux pentru Cell Broadband Engine are unul sau mai multe threaduri de
Linux asociate cu acesta, care pot fi rulate fie pe PPE fie pe SPE. Un thread SPE este un
thread de Linux care rulează pe SPE. Aceste noţiuni sunt detaliate în continuare.
Threadurile software descrise în aceasta secţiune nu au legătură cu capacitatea de
hardware multithreading a PPE.

Linux Thread
Un thread ce rulează sub sistemul de operare Linux;
PPE thread
Un thread Linux ce rulează pe PPE;
SPE thread
Un thread Linux ce rulează pe SPE. Fiecare astfel de thread:

12
- Are propriul context SPE ce include un set de regiştri 128 x 128-bit, program counter
şi coada de comenzi MFC.
- Poate comunica cu alte unităţi de execuţie (sau cu memoria principală prin
intermediul unităţii MFC).
Cell Broadband Engine Linux task
Un task ce rulează pe PPE şi SPE.
- Fiecare astfel de task are unul sau mai multe thread-uri Linux.
- Toate thread-urile Linux din interiorul unui task împart resursele task-ului.

Un thread de Linux poate interacţiona direct cu un thread SPE prin memoria locală a
SPE-ului (local store – LS) şi indirect prin memoria de adrese efective (EA) sau prin interfaţa
oferită de către subrutinele din SPE Runtime Management library.
Sistemul de operare oferă mecanismul şi politicile de rezervare a unui SPE disponibil.
Acesta are şi rolul de a prioritiza aplicaţiile de Linux pentru sistemul Cell Broadband Engine
şi de a planifica execuţia pe SPE, independentă de threadurile normale Linux. Este, de
asemenea, responsabil şi de încărcarea runtime-ului, transmiterea parametrilor către
programele SPE, notificarea în cazul evenimentelor şi erorilor din SPE-uri şi asigurarea
suportului pentru debugger.

Fig. 1.11: Vedere generală asupra unui cip Cell Broadband Engine

13
Capitolul 2
Instrumentele de dezvoltare Cell SDK 3.0

În acest capitol se face o familiarizare cu mediul Cell SDK 3.0 (Software


Development Kit) şi are ca finalitate scrierea, compilarea şi rularea unui program simplu ce
va afişa ”Hello World”.
PPE rulează aplicaţii şi sisteme de operare, care pot include instrucţiuni din setul
Multimedia Extins Vector/SIMD. PPE necesită un sistem de operare extins pentru a oferi
suport caracteristicilor hardware a arhitecturii Cell Broadband Engine, cum ar fi:
multiprocesarea cu SPE-uri, accesul la funcţiile din setul Multimedia Extins Vector/SIMD
pentru PPE, controllerul de întreruperi din arhitectura Cell Broadband Engine şi restul de
funcţionalităţi particulare din arhitectura Cell Broadband Engine. În acest mediu de operare,
PPE se ocupă cu alocarea de threaduri şi managementul de resurse între SPE-uri. Kernelul
Linux de pe SPE-uri controlează rularea programelor pe SPU-uri.
Threadurile SPE urmează modelul de thread M:N, ceea ce înseamnă că M threaduri
sunt distribuite la N elemente de procesare. În mod normal, threadurile SPE rulează până la
terminare. Totuşi, rularea acestora este controlată de către priorităţile şi politicile de
planificare a threadurilor. Cuanta de timp alocată pentru threadurile SPE este în mod normal
mai mare decât cea a threadurilor PPE, doarece o schimbare de context pe SPE are un cost
mai ridicat.
Kernelul Linux se ocupă cu managementul memoriei virtuale, inclusiv maparea
fiecărei memorii locale (local store – LS) şi fiecarei zone problem state (PS) în spaţiul de
adrese efective. Kernelul controlează atât maparea memoriei virtuale a resurselor MFC, cât şi
manipularea segment-fault-urilor şi page-fault-urilor MFC. Sunt suportate şi paginile mari
(16 MB), care folosesc extensia Linux hugetlbfs.

2.1 Instalarea FC8 cu Cell SDK 3.0, Arhitectura şi set-area PS3 Cluster

S-a ales utilizarea sistemului PS3 în construirea cluster-ului, datorită caracteristicilor


deosebite ale acestora, care-l fac potrivit pentru calculul ştiinţific. Câteva dintre aceste
caracteristici ar fi:
- PS3 este open-platform – adică poate rula diferite sisteme de operare (ex. Fedora Core
8 for PPC);
14
- Sistemul PS3 conţine procesorul CELL/B.E. puţin diferit de modelul original (1xPPU
şi 6xSPU);
- Preţul foarte scăzut ~300$ îl face foarte atractiv ca nod de calcul într-un sistem
cluster.
Arhitectura sistemului de comunicaţie este de tip stea, prezentată în figura 2.1.
S-au parcurs următorii paşi în set-area cluster-ului:
- Formatarea celor 9 noduri PS3;
- Instalarea sistemului de operare Fedora Core 8 for PPC 64;
- Se instalează pe fiecare staţie serviciul SSH şi NFS folosite la comunicaţia MPI şi la
partajarea fisierelor;
- Se configurează NFS separat pentru staţia server şi celelalte 8 staţii client;
- Se instlează şi configurează libraria OpenMPI pe toate staţiile;
- Instalarea Cell SDK 3.0 pe fiecare staţie.

Fig. 2.1: Arhitectura cluster PS3

Conectarea la sistemul de calcul cluster 9xPS3 se face cu ajutorul unui client


Telnet/SSH Putty, Figura 2.2: Host Name – 172.20.6.81, Port – 22, Connection type – SSH.
După conectarea pe staţia master se foloseşte user-ul: student şi parola: student.

15
Fig. 2.2: Client Telnet/SSH Putty

2.2 Scrierea primului program pentru Cell Broadband Engine

Pot fi mai multe tipuri de programe: programe PPE, programe SPE şi programe pentru
Cell Broadband Engine (programe PPE care au programme SPE embedded).
Programele pentru PPE şi SPE folosesc compilatoare diferite. Compilatorul, flagurile
compilatorului şi librăriile trebuie folosite în funcţie de tipul de procesor şi program. De
obicei, un PPE setează, porneşte şi opreşte SPE-uri. Un aspect important ce trebuie luat în
consideraţie este comunicarea dintre PPE-uri şi SPE-uri.
Există două modalităţi de bază pentru a testa un program pentru Cell Broadband
Engine: prima se referă la folosirea de fişiere Makefile iar cea de a doua la folosirea unui
mediu IDE (folosind Eclipse). Se va exemplifica lucrul cu fişiere Makefile.
În fişierele Makefile se pot declara tipul programelor, compilatorul ce va fi folosit,
opţiunile de compilare şi librăriile ce vor fi folosite. Cele mai importante tipuri de ţinte (target
types) sunt: PROGRAM_ppu şi PROGRAM_spu, pentru compilarea programelor PPE şi
respectiv SPE. Pentru a folosi definiţiile pentru makefile din kitul SDK, trebuie inclusă
următoarea linie la sfârşitul fişierului makefile:
16
include /opt/cell/sdk/buildutils/make.footer
În Figura 2.3 este prezentată structura de directoare şi fişiere Makefile pentru un
sistem cu un program PPU şi un program SPU. Acest proiect sampleproj are un director de
proiect şi două subdirectoare. Directorul “ppu” conţine codul sursă şi fişierul Makefile pentru
programul PPU. Directorul „spu” conţine codul sursă şi fişierul Makefile pentru programul
SPU. Fişierul Makefile din directorul de proiect lansează în execuţie fişierele makefile din
cele două subdirectoare. Aceasta structură de organizare pe directoare nu este unică.

Fig. 2.3 Exemplu de structură de directoare a unui proiect şi fişiere Makefile

2.3 Scrierea unui program multi-threaded pentru CBE

Pentru a scrie un program pentru CBE, sunt recomandaţi paşii descrişi mai jos.
Proiectul se numeşte “sampleproj”.
1. Creaţi un director numit “sampleproj”.
2. În directorul “sampleproj”, creaţi un fişier cu numele “Makefile”, în care scrieţi
următoarea secvenţă de cod:

########################################################################
# Target
########################################################################

DIRS = spu ppu

17
########################################################################
# buildutils/make.footer
########################################################################

include /opt/cell/sdk/buildutils/make.footer

3. Creaţi un director numit “ppu”.


4. În directorul “/sampleproj/ppu”, creaţi un fişier cu numele “Makefile”, în care scrieţi
următoarea secvenţă de cod:

########################################################################
# Target
########################################################################

PROGRAM_ppu = simple

########################################################################
# Local Defines
########################################################################

IMPORTS = ../spu/spu.a -lspe2

INSTALL_DIR = ../ppu/
INSTALL_FILES = $(PROGRAM_ppu)

########################################################################
# buildutils/make.footer
########################################################################

include /opt/cell/sdk/buildutils/make.footer

5. În directorul “/sampleproj/ppu”, creaţi un fişier cu numele “ppu.c”, în care scrieţi


următoarea secvenţă de cod:

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <libspe2.h>
#include <pthread.h>

extern spe_program_handle_t spu;

#define MAX_SPU_THREADS 6

18
void *ppu_pthread_function(void *arg) {
spe_context_ptr_t ctx;
unsigned int entry = SPE_DEFAULT_ENTRY;

ctx = *((spe_context_ptr_t *)arg);


if (spe_context_run(ctx, &entry, 0, NULL, NULL, NULL) < 0) {
perror ("Failed running context");
exit (1);
}
pthread_exit(NULL);
}

int main()
{
int i, spu_threads;
spe_context_ptr_t ctxs[MAX_SPU_THREADS];
pthread_t threads[MAX_SPU_THREADS];

//Determine the number of SPE threads to create.


spu_threads = spe_cpu_info_get(SPE_COUNT_USABLE_SPES, -1);
if (spu_threads > MAX_SPU_THREADS) spu_threads = MAX_SPU_THREADS;

//Create several SPE-threads to execute 'spu'.


for(i=0; i<spu_threads; i++) {
// Create context
if ((ctxs[i] = spe_context_create (0, NULL)) == NULL) {
perror ("Failed creating context");
exit (1);
}

// Load program into context


if (spe_program_load (ctxs[i], &spu)) {
perror ("Failed loading program");
exit (1);
}

// Create thread for each SPE context


if (pthread_create (&threads[i], NULL, &ppu_pthread_function, &ctxs[i])) {
perror ("Failed creating thread");
exit (1);
}
}

// Wait for SPU-thread to complete execution.


for (i=0; i<spu_threads; i++) {

19
if (pthread_join (threads[i], NULL)) {
perror("Failed pthread_join");
exit (1);
}

// Destroy context
if (spe_context_destroy (ctxs[i]) != 0) {
perror("Failed destroying context");
exit (1);
}
}

printf("\nThe program has successfully executed.\n");


return 0;
}

6. Se revine în directorul numit “sampleproj”.


7. Creaţi un director numit “spu”.
8. În directorul “/sampleproj/spu”, creaţi un fişier cu numele “Makefile”, în care scrieţi
următoarea secvenţă de cod:

#######################################################################
# Target
########################################################################

PROGRAMS_spu := spu
LIBRARY_embed := spu.a

########################################################################
# Local Defines
########################################################################

########################################################################
# buildutils/make.footer
########################################################################

include /opt/cell/sdk/buildutils/make.footer

9. În directorul “/sampleproj/spu”, creaţi un fişier cu numele “spu.c”, în care scrieţi


următoarea secvenţă de cod:
#include <stdio.h>
int main(unsigned long long id)
{

20
printf("Hello World! from Cell (0x%llx)\n", id);
return 0;
}

10. Se revine în directorul numit “sampleproj”.


11. Compilaţi programul folosind următoarea comandă în consolă, în timp ce vă aflaţi în
directorul “sampleproj”:

make

Dacă se rulează fişierul “simple” din directorul “/sampleproj/ppu”, se va afişa mesajul


“Hello World! from Cell (#)\n” la linia de comandă, unde # este spe_id-ul threadului SPE
care execută comanda de afişare.

2.4 Descrierea fişierelor sursă

Vor fi explicate pe scurt funcţiile folosite în acest program şi parametrii acestora.


Pentru a porni SPE-urile din PPE, în programul PPU-ului s-au urmat 4 paşi:
1. Crearea unui context SPE.
2. Încărcarea unui obiect executabil pe SPE în local store-ul contextului SPE creat.
3. Rularea contextului SPE. Se transferă controlul sistemului de operare, care cere
scheduling-ul efectiv al contextului pe un SPE fizic din sistem.
4. Distrugerea contextului SPE.

Crearea unui context SPE


- spe_context_create este funcţia care creează şi iniţializează un context pentru un thread
SPE care conţine informaţie persistentă despre un SPE logic. Funcţia întoarce un pointer spre
noul context creat, sau NULL în caz de eroare. Exemplu:

1. #include <libspe2.h>
2. spe_context_ptr_t spe_context_create(unsigned int flags,
spe_gang_context_ptr_t gang)

flags - Rezultatul aplicării operatorului OR pe biţi pe diverse valori (modificatori) ce se


aplică la crearea contextului. Valori acceptate:
21
1. 0 - nu se aplică nici un modificator.
2. SPE_EVENTS_ENABLE - configurează contextul pentru a permite lucrul cu evenimente
(foarte important pentru mailboxes)
3. SPE_CFG_SIGNOTIFY1_OR - configurează registrul 1 de SPU Signal Notification
pentru a fi în modul OR; default e în mod Overwrite (cu alte cuvinte, se va face o
operaţie logică OR între noul semnal primit şi cel deja existent, şi nu o suprascriere)
4. SPE_CFG_SIGNOTIFY2_OR - analog SPE_CFG_SIGNOTIFY1_OR, pentru registrul 2
de SPU Signal Notification
5. SPE_MAP_PS - pentru cerere permisiune pentru acces mapat la memoria “problem state
area” (notată prescurtat PS) a threadului corespunzător SPE-ului. PS conţine flagurile
de stare pentru SPE-uri şi în mod default nu poate fi accesată decât SPE-ul propriu, iar
din exterior doar prin cereri DMA. Daca acest flag e setat, se specifică la crearea
contextului că PPE vrea acces la memoria PS a respectivului SPE.

gang - Asociază noul context SPE cu un grup (gang) de contexte. Dacă valoarea pentru gang
e NULL, noul context SPE nu va fi asociat vreunui grup.

Încărcarea unui executabil în Local Store-ul contextului SPE creat

Se realizează folosind funcţia cu următorul antet:


1. int spe_program_load(spe_context_ptr spe, spe_program_handle_t
*program)

spe - un pointer valid al unui context SPE (întors de spe_context_create) în care se va


încarcă executabilul (programul specificat de următorul argument)
program - o adresă validă la un program mapat pe un SPE. În exemplul prezentat, acesta era
declarat ca: extern spe_program_handle_t spu, unde spu era numele executabilului pentru
SPU.
Rularea contextului SPE

Se realizează folosind funcţia cu următorul antet:


1. #include <libspe2.h>
2. int spe_context_run(spe_context_ptr_t spe, unsigned int *entry,
unsigned int runflags, void *argp, void *envp, spe_stop_info_t
*stopinfo)

22
spe - Pointer către contextul SPE care trebuie rulat.
entry - Input: punctul de intrare, adică valoarea iniţială a Intruction Pointer-ului de pe SPU,
de unde va începe execuţia programului. Dacă această valoare e SPE_DEFAULT_ENTRY,
punctul de intrare va fi obţinut din imaginea de context SPE încărcată.
runflags - Diferite flaguri (cu OR pe biţi între ele) care specifică o anumită comportare în
cazul rulării contextului SPE:
1. 0 - default, nici un flag.
2. SPE_RUN_USER_REGS - regiştrii de setup r3, r4 şi r5 din SPE vor fi iniţializaţi cu 48
octeti (16 pe fiecare din cei 3 regiştri) specificaţi de pointerul argp.
3. SPE_NO_CALLBACKS - SPE library callbacks pentru regiştri nu vor fi executate
automat. Acestea includ şi “PPE-assisted library calls” oferite de SPE Runtime
library.
argp - Un pointer (opţional) la date specifice aplicaţiei. Este pasat SPE-ului ca al doilea
argument din main.
envp - Un pointer (opţional) la date specifice environmentului. Este pasat SPE-ului ca al
treilea argument din main.
stopinfo - Un pointer (opţional) la o structura de tip spe_stop_info_t (această structură
conţine informaţii despre modul în care s-a terminat execuţia SPE-ului)

Distrugerea contextului SPE

Se realizează folosind funcţia cu următorul antet:

1. #include <libspe2.h>
2. int spe_context_destroy (spe_context_ptr_t spe)

Funcţia întoarce 0 în caz de succes, -1 în caz de eroare.


spe - Pointer spre contextul SPE care va fi distrus.

23
Capitolul 3
Lucrul cu tipul vector in Cell/B.E.

3.1 Introducere

Un compilator care transformă automat scalari în structuri SIMD împachetate paralel


este un compilator cu auto-vectorizare. Asemenea compilatoare trebuie să manevreze toate
construcţiile unui limbaj de nivel înalt şi din această cauză rezultatul nu îl constituie
întotdeauna un cod optim.

O altă variantă, folosită în Cell, este ca vectorizarea să se facă încă de la scrierea


codului. Mai jos este prezentat un tabel cu funcţii dedicate structurilor SIMD.

Tabelul 3.1 – Intrisincs SPU cu mapare unu-la-unu pe Vector/SIMD Multimedia Extension

SPU Vector/SIMD Multimedia Extension


Pentru ce tipuri de date
Intrinsic PPU Intrinsic
vector operands only, no scalar
spu_add vec_add
operands
vector operands only, no scalar
spu_and vec_and
operands
spu_andc vec_andc all
spu_avg vec_avg all
vector operands only, no scalar
spu_cmpeq vec_cmpeq
operands
vector operands only, no scalar
spu_cmpgt vec_cmpgt
operands
spu_convtf vec_ctf limited scale range (5 bits)
spu_convts vec_cts limited scale range (5 bits)
spu_convtu vec_ctu limited scale range (5 bits)
spu_extract vec_extract all
spu_genc vec_addc all
spu_insert vec_insert all
spu_madd vec_madd float only
spu_mulhh vec_mule all
halfword vector operands only, no
spu_muo vec_mulo
scalar operands
spu_nmsub vec_nmsub float only

24
spu_nor vec_nor all
vector operands only, no scalar
spu_or vec_or
operands
spu_promote vec_promote all
spu_re vec_re all
vector operands only, no scalar
spu_rl vec_rl
operands
spu_rsqrte vec_rsqrte all
spu_sel vec_sel all
spu_splats vec_splats all
vector operands only, no scalar
spu_sub vec_sub
operands
vector operands only, no scalar
spu_genb vec_genbl
operands
vector operands only, no scalar
spu_xor vec_xor
operands

Sunt explicate câteva dintre aceste funcţii pentru vectori:

• vec = spu_splats(scal) – replică un scalar în fiecare element al unui vector ex:


vec1111 = spu_splats((float)1)
• vec_float = spu_convtf(vec_int, scale) - converteşte un vector de int într-un
vector de float
• vec = spu_add(vec_a, vec_b) - adunare de vectori element cu element
• vec = spu_sub(vec_a, vec_b) - scădere de vectori element cu element
• vec = spu_mul(vec_a, vec_b) - înmulţire de vectori element cu element (produs
scalar)
• vec = spu_madd(vec_a, vec_b, vec_c) - multiply (vec_a cu vec_b) şi add
(produsul se adună cu vec_c);
• vec = spu_nmadd(vec_a, vec_b, vec_c) - (multiply & add) negat
• vec = spu_msub(vec_a, vec_b, vec_c) - analog madd, dar cu sub în loc de add
• vec = spu_nmsub(vec_a, vec_b, vec_c) - analog nmadd, dar cu sub în loc de add
• vec = spu_shuffle(vec_a, vec_b, vec_perm) - vec este rezultatul unui amestec
(shuffle) controlat între vec_a si vec_b; vec_perm specifica ce octeti din vec_a şi din
vec_b se vor afla în vectorul rezultat vec.

Tipuri de date de tip vector:


25
• vector [unsigned] {char, short, int, float, double} ex: “vector float”,
“vector signed short”, “vector unsigned int”, …
o Numărul de elemente din fiecare astfel de vector depinde de tipul elementelor.
Trebuie ţinut cont că indiferent de tip, un vector are 128 biţi. El conţine astfel
4 * int, 4 * float, 8 * short, 16 * char …
o Se poate face cast între diferite tipuri vector
o Vectorii sunt aliniaţi la stânga în blocuri de dimensiunea quadword (16 octeţi)

Pointeri la vectori :

• Ex: “vector float *p”


• p+1 e pointer spre următorul vector (16B) după vectorul la care referă p
• Se poate face cast din pointeri la scalari şi din pointeri la tipuri vector

3.2 Vectorizarea unei bucle

În continuare este prezentat un exemplu simplu de înmulţire a doi vectori, element cu


element. Programele (funcţia de înmulţire şi main-ul) sunt prezentate în varianta
nevectorizată, în varianta vectorială când dimensiunile vectorilor sunt divizibile cu 4 (tipurile
vector sunt pe 128 biţi, deci conţin 4 elemente pe 32 biţi - în cazul nostru float) şi în varianta
vectorială când dimensiunile vectorilor nu sunt divizibile cu 4.

a) Varianta nevectorizată:

Se va realiza un proiect ca cel din capitolul 2, numit “mulvect”. În fişierul


“/ppu/Makefile” se modifică numele fişierului generat în PROGRAM_ppu = mulvect. În
sursa “/ppu/ppu.c” se reduc numărul de SPU-uri start-ate la 1: #define
MAX_SPU_THREADS 1.
Se modifică fişierul sursă “/spu/spu.c” ca cel de mai jos:
#include <stdio.h>
#define N 16
int mult1(float *in1, float *in2, float *out, int num);
float a[N] = { 1.1, 2.2, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 2.2, 3.3, 3.3, 2.2, 5.5,
6.6, 6.6, 5.5};
float b[N] = { 1.1, 2.2, 4.4, 5.5, 5.5, 6.6, 6.6, 5.5, 2.2, 3.3, 3.3, 2.2, 6.6,
7.7, 8.8, 9.9};
float c[N];

26
int mult1(float *in1, float *in2, float *out, int num)
{
int i;
for(i = 0; i < num; i++){
out[i] = in1[i] * in2[i];
}
return 0;
}

int main(unsigned long long id)


{
int num = N;
int i;
mult1(a, b, c, num);
printf("MulVect SPU - 0x%llx \n", id);
for (i = 0;i < N;i += 4)
printf("%.2f %.2f %.2f %.2f\n", c[i], c[i+1], c[i+2], c[i+3]);
return 0;
}

b) Varianta vectorială în care dimensiunea vectorilor iniţiali e multiplu de 4:

În funcţia de înmulţire (mult1) vectorii de tip float se convertesc la vectori de vector


float şi se micşorează numărul de paşi din bucla (de 4 ori). Pentru înmulţirea a două variabile
de tip vector (element cu element), se utilizează functia spu_mul(), din spu_intrinsics.
Atenţie, aici elementele c[i], a[i] şi b[i] sunt vectori ce conţin fiecare câte 4 float-uri:

int mult1(float *in1, float *in2, float *out, int num){


int i;
vector float *a = (vector float *) in1;
vector float *b = (vector float *) in2;
vector float *c = (vector float *) out;

int Nv = N >> 2; //N/4->fiecare vector float are 128 bytes = 4 * float


pe 32 bytes
for (i = 0;i < Nv;i++){
c[i] = spu_mul(a[i], b[i]);
}
return 0;
}

Vectorii a[i], b[i], c[i] se declară ca fiind aliniaţi la 128 de biţi:


27
float a[N] __attribute__ ((aligned(16))) = { 1.1, 2.2, 4.4, 5.5, 6.6, 7.7, 8.8,
9.9, 2.2, 3.3, 3.3, 2.2, 5.5, 6.6, 6.6, 5.5};
float b[N] __attribute__ ((aligned(16))) = { 1.1, 2.2, 4.4, 5.5, 5.5, 6.6, 6.6,
5.5, 2.2, 3.3, 3.3, 2.2, 6.6, 7.7, 8.8, 9.9};
float c[N] __attribute__ ((aligned(16)));

c) Varianta vectorială în care dimensiunea vectorilor iniţiali nu e multiplu de 4.

În main singura modificare făcută a fost asupra numărului de elemente din vectori
(19), pentru a nu mai fi multiplu de 4. Se observă că valoarea de la aliniere (numărul de
octeţi) rămâne tot 16.
În funcţia de înmulţire (mult1) trebuie reţinut câtul (Nv) dar şi restul (j) împărţirii
dimensiunii N la 4. Astfel, vor fi vectori de Nv elemente de tipul vector float, care se vor
înmulţi folosind funcţia spu_mul(), la fel ca la punctul b). Dar vor fi şi j elemente (j < 4) de
tip float, care nu pot compune un vector float, şi care vor trebui înmulţite în modul
tradiţional:

float a[N] __attribute__ ((aligned(16))) = { 1.1, 2.2, 4.4, 5.5, 6.6, 7.7, 8.8,
9.9, 2.2, 3.3, 3.3, 2.2, 5.5, 6.6, 6.6, 5.5, 1.2, 2.2, 3.3};
float b[N] __attribute__ ((aligned(16))) = { 1.1, 2.2, 4.4, 5.5, 5.5, 6.6, 6.6,
5.5, 2.2, 3.3, 3.3, 2.2, 6.6, 7.7, 8.8, 9.9, 1.2, 2.2, 3.3};
float c[N] __attribute__ ((aligned(16)));

int mult1(float *in1, float *in2, float *out, int num){


int i;
vector float *a = (vector float *) in1;
vector float *b = (vector float *) in2;
vector float *c = (vector float *) out;

int Nv = N >> 2; // N/4 -> fiecare vector float are 128 bytes = 4 * float pe 32
bytes
int j = N % 4;
for (i = 0;i < Nv;i++){
c[i] = spu_mul(a[i], b[i]);
}
for (i = N - j;i < N;i++){
out[i] = in1[i] * in2[i];
}
return 0;
}

Ca temă se detemină timpul de calcul în cele două versiuni: nevectorizat şi vectorizat.

28
Capitolul 4
Mecanisme de comunicare PPU - SPU

Scopul acestui capitol este familiarizarea cu mecanismele de comunicare între PPU -


SPU şi, respectiv, între SPU - SPU. Pentru fiecare SPU, MFC-ul asociat gestionează canale
SPU şi regiştrii MMIO asociaţi acestora pentru a asigura comunicaţia SPU-ului respectiv cu
exteriorul (solicitarea şi monitorizarea transferurilor DMA, monitorizarea evenimentelor
SPU, comunicaţia inter-procesor prin mailbox şi notificare prin semnale, accesarea resurselor
auxiliare etc).
Între PPE şi SPE există trei mecanisme principale de comunicare:
1. transferul DMA: folosit pentru a trimite date între spaţiul principal de stocare şi
memoria locală. SPE-urile folosesc transfer DMA asincron pentru a ascunde latenţa
memoriei şi overhead-ul, ocupându-se în paralel de calcule;
2. mailbox-urile: folosite pentru comunicaţia de control între un SPE şi PPE sau alte
dispozitive, prin mesaje de 32 de biţi;
3. notificarea prin semnale: folosită pentru comunicaţia de control între PPE şi alte
dispozitive, prin regiştrii de 32 de biţi, care pot fi configuraţi (în termeni de expeditor-
destinatar) ca unu-la-unu sau mulţi-la-unu.

4.1 Mailbox

Comunicarea prin mailbox-uri


Mailbox-urile oferă un mecanism simplu de comunicare, folosit în general de PPE
pentru a trimite comenzi scurte la SPE şi pentru a primi înapoi statusul efectuării comenzii.
Practic, permit şi comunicarea între SPE-uri sau între SPE-uri şi alte dispozitive.
Fiecare SPE are acces la trei mailbox-uri (direcţiile sunt date relativ la SPE):
1. Inbound mailbox: coadă cu o capacitate de 4 mesaje de 32 de biţi, în care PPE (sau
alte SPE-uri sau dispozitive) scriu mesaje pentru SPU;
2. Outbound mailbox: coadă cu o capacitate de 1 mesaj de 32 de biţi, în care SPU scrie
mesaje pentru PPE sau alte dispozitive;
3. Outbound interrupt mailbox: coadă cu o capacitate 1 un mesaj de 32 de biţi, în care
SPU scrie mesaje pentru PPE sau alte dispozitive, cu întrerupere pentru acestea.

29
Termenul de mailbox poate referi colectiv toate elementele care asigură acest
mecanism: regiştrii MMIO, canale, stări, întreruperi, cozi şi evenimente.

Accesul la mailbox: blocant pentru SPU, non-blocant pentru PPE

SPU accesează mailbox-urile, gestionate de MFC-ul său, prin canale proprii, unul
pentru fiecare mailbox. Aceste canale sunt blocante: SPU va aştepta dacă i se cere să scrie
într-un outbound mailbox plin (interrupt sau nu) sau să citească dintr-un inbound mailbox
gol. Comportamentul blocant pentru SPU al mailbox-urilor este folosit pentru sincronizare.
PPE şi alte dispozitive accesează mailbox-urile şi statusul lor prin regiştrii MMIO
asociaţi. Acest acces nu este blocant. În cazul în care PPE vrea să scrie într-un inbound
mailbox plin, se va suprascrie cea mai recentă intrare (e.g. dacă PPE scrie de cinci ori inainte
ca SPE să citească, mailbox-ul va conţine mesajele cu indicii 1, 2, 3 şi 5; mesajul cu indice 4
s-a pierdut).
Acesta este comportamentul uzual, însă se poate imprima caracter blocant sau non-
blocant atât SPU cât şi PPE. Pe lângă operaţiile de citire/scriere în sine, mai sunt disponibile
şi operaţii de interogare a contorului fiecărui mailbox în parte. Aceste operaţii nu sunt
blocante. Astfel, dacă se doreşte prevenirea blocării SPU, se pot folosi astfel de operaţii
pentru a vedea dacă este cazul să se facă o citire/scriere. La capătul celălat, funcţiile de acces
ale PPU la inbound mailbox şi la outbound interrupt mailbox au un parametru care poate fi
setat pe blocant.
În considerarea metodei de acces trebuie avute în vedere şi criterii de performanţă.
Pentru SPU accesul la mailbox este “intern” şi cu o latenţă foarte mică: cel mult 6 ciclii de
ceas pentru acces non-blocant. Însă pentru PPE şi alte SPE-uri, accesul la mailbox trebuie
făcut prin intermediul EIB, are o latenţă mai mare şi duce la încărcarea magistralei.
Pentru mai multe detalii in privinta accesului blocant si non-blocant pe SPU si PPE
consultati Fig.4.1 din sectiunea “Mailbox la nivel de cod”.

Scenarii de folosire

Concepute în principal pentru trimiterea de flaguri şi stări de program, mailbox-urile,


cu cei 32 de biţi per mesaj ai lor, pot fi folosite inclusiv pentru a trimite adrese de memorie,
parametrii de funcţii, comenzi etc.

30
1. Un exemplu de folosire a mailbox-urilor se regăseşte în cazul unei aplicaţii SPU
bazate pe comenzi. SPU se găseşte în aşteptare până la primirea unei comenzi de la
PPE prin intermediul inbound mailbox. După ce termină operaţiunea, trimite un cod
de răspuns prin outbound interrupt mailbox şi intră în aşteptare până la o nouă
comandă;
2. O altă manieră de abordare presupune activarea mecanismului de întreruperi la nivelul
programului SPE, pentru a răspunde la evenimente asociate unui mailbox. La citirea
din outbound mailbox şi scrierea în inbound mailbox, PPE poate seta un astfel
eveniment SPE, aşa cum la scrierea în outbound interrupt mailbox, SPU poate solicita
întrerupere la nivelul PPU;
3. Mailbox-urile sunt folosite, de asemenea, când un SPE trimite rezultate în memoria
principală prin DMA: SPE solicită transferul şi asteaptă terminarea acestuia, după
care comunică PPE acest lucru printr-un outbound mailbox. PPE poate atunci să
lanseze comanda lwsync pentru a verifica încheierea cu succes a operaţiunii în
memoria principală şi a folosi datele. Alternativ, SPE poate notifica PPE că a finalizat
operaţiunea scriind notificarea direct în memoria principală prin DMA, de unde PPE o
poate citi;
4. Mailbox-urile pot fi folosite şi pentru comunicarea între SPE-uri, prin transferul DMA
al datelor de către un SPE direct în mailbox-ul unui alt SPE. Pentru aceasta, software
privilegiat trebuie să permită accesul unui SPE la registrul mailbox al unui alt SPE
mapând zona de regiştrii problem-state a SPE-ului ţintă în spaţiul Effective Address al
SPE-ului sursă. Dacă acest lucru nu este permis din software, atunci pentru
comunicarea între SPE-uri se pot folosi doar operaţii atomice şi semnale de notificare.

Mailbox la nivel de cod


La nivel de cod, există instrucţiuni specifice SPU şi instrucţiuni specifice PPU, pentru
fiecare din cele trei tipuri de mailbox-uri, atât pentru citire, cât şi pentru scriere (i.e. 6
instrucţiuni pentru SPU şi 6 instrucţiuni pentru PPU):
• Un program SPU poate accesa mailbox-urile locale prin funcţii de forma spu_*_mbox,
definite în spu_mfcio.h;
• Un program PPU poate accesa mailbox-urile unui SPE prin funcţii de forma
spe_*_mbox_*, definite în libspe2.h;

31
• Pe lângă acestea, un program SPU poate accesa mailbox-urile unui alt SPE prin
funcţii DMA definite în spu_mfcio.h, dacă acestea sunt mapate în problem state-ul
local al SPU-ului.

Fig. 4.1 Funcţii Mailbox API

Cod SPU: SPU intrinsics


Într-un program SPE, scrierea în mailbox-urile outbound şi outbound interrupt se
poate face prin instrucţiunea de scriere write-channel (în assembler wrch), iar citirea dintr-un
mailbox inbound cu instrucţiunea de citire read-channel (în assembler rdch). În C:
• (uint32_t) spu_read_in_mbox (void), implementare spu_readch(SPU_RdInMbox);
o citeşte următorul mesaj din inbound mailbox, SPU intră în asteptare dacă mailboxul este
gol;
o data este definită în mod particular aplicaţiei;
• (uint32_t) spu_stat_in_mbox (void), implementare
spu_readchcnt(SPU_RdInMbox);

o returnează numărul de mesaje din inbound mailbox, dacă este diferit de zero atunci
mailbox-ul conţine date necitite de SPU;
32
• (void) spu_write_out_mbox (uint32_t data), implementare
spu_writech(SPU_WrOutMbox, data);

o scrie date în outbound mailbox, SPU intră în aşteptare dacă mailboxul este plin;
o data este definită în mod particular aplicaţiei;
• (uint32_t) spu_stat_out_mbox (void), implementare
spu_readchcnt(SPU_WrOutMbox)
o întoarce capacitatea disponibilă a outbound mailbox, rezultat zero arată că mailbox-ul
este plin;
• (void) spu_write_out_intr_mbox (uint32_t data), implementare
spu_writech(SPU_WrOutIntrMbox, data)
o scrie date în outbound interrupt mailbox, SPU intră în aşteptare dacă mailboxul este plin;
o data este definită în mod particular aplicaţiei;
• (uint32_t) spu_stat_out_intr_mbox (void), implementare
spu_readchcnt(SPU_WrOutIntrMbox)
o întoarce capacitatea disponibilă a outbound interrupt mailbox, rezultat zero arată că
mailbox-ul este plin;

Cod PPU: API disponibil pentru PPE

Următoarele funcţii sunt definite în libspe2.h:


• int spe_out_mbox_read(spe_context_ptr_t spe, unsigned int *mbox_data,
int count)
o citeşte maxim count mesaje din outbound mailbox corespunzător SPE-ului dat de spe;
o dacă nu sunt disponibile count mesaje, va citi câte sunt disponibile;
• int spe_out_mbox_status(spe_context_ptr_t spe)
o citeşte statusul lui outbound mailbox corespunzător SPE-ului dat de spe;
• int spe_in_mbox_write(spe_context_ptr_t spe, unsigned int *mbox_data,
int count, unsigned int behavior)
o scrie până la count mesaje în inbound mailbox;
o poate fi blocant sau non-blocant în funcţie de valoarea lui behavior:
SPE_MBOX_ALL_BLOCKING
SPE_MBOX_ANY_BLOCKING
SPE_MBOX_ANY_NONBLOCKING
o versiunea blocantă este utilă pentru a trimite o secventa de mesaje, iar cea non-blocantă
când se folosesc evenimente;

33
• int spe_in_mbox_status(spe_context_ptr_t spe)
o citeşte statusul lui inbound mailbox corespunzător SPE-ului dat de spe;
• int spe_out_intr_mbox_read(spe_context_ptr_t spe, unsigned int
*mbox_data, int count, unsigne dint behavior)
• int spe_out_intr_mbox_status(spe_context_ptr_t spe)

Lucrul cu evenimente

Evenimentele se referă la un mecanism SPE care permite codului rulat pe SPU să


anunţe evenimente ale rulării programului. Suntem interesaţi în primul rând de evenimentele
declanşate de scrierea sau citirea în mailbox şi în regiştrii de notificare prin semnale.
PPE poate intercepta o parte din aceste evenimente, sincron sau asincron:
• sincron:
o blocant: citirea statusului evenimentului blochează programul până la apariţia
unui eveniment;
o non-blocant: interogarea evenimentelor disponibile se face într-o buclă;
• asincron:
o se setează un handler care răspunde întreruperii setate de eveniment.

Lucrări practice Mailbox

De notat că prefixurile funcţiilor de lucru cu SPU sunt diferite, în cele două librării:
• funcţiile pentru programele PPU au denumiri de forma 'spe_*' (spe_out_mbox_read)
• funcţiile pentru programele SPU au denumiri de forma 'spu_*'

(spu_write_out_mbox)

Aspectul cel mai important legat de comunicare este caracterul blocant / neblocant.
Varianta blocantă presupune că receptorul se dedică aşteptării unui răspuns, ceea ce face ca
această variantă să fie, deşi uşor de implementat, ineficientă. A doua variantă se bazează pe
mecanismul de evenimente (asemănător unui mecanism de întreruperi sau celui de semnale
învăţat la Sisteme de Operare). Ce are programatorul de făcut este să înregistreze un handler
care intervine în momentul declanşării evenimentelor de interes.

34
Începem prin a construi pe baza scheletului de cod prezentat anterior un mecanism
rudimentar de trimitere a parametrilor de iniţializare. Prin exemplele următoare vom acoperi
trei modele de comunicare:
1. trimiterea de mesaje de la SPU la PPU
2. trimiterea de mesaje de la PPU la SPU
3. trimiterea de mesaje de la SPU la PPU folosind evenimente

Trimiterea unor parametri de iniţializare către SPU

Trebuie menţionat că acest mecanism nu este unul foarte elegant şi ne va servi la


trimiterea parametrilor pentru început, în exemplele mai avansate fiind înlocuit de trimiterea
parametrilor prin DMA şi mailbox sau prin alte metode de comunicare.
Se va modifica un pic programul din capitolul 2 pentru a trimite câţiva parametri de
iniţializare. De exemplu am dori să ştim pe care SPU ne aflăm când rulăm. Ne vom folosi de
cei doi parametrii adiţionali din funcţia main de tipul unsigned long long (ull este tipul de
date cel mai mare posibil).
În codul PPU, parametrii sunt: “spe“, “argp“ şi “envp“.
#include <libspe2.h>
int spe_context_run(spe_context_ptr_t spe, unsigned int *entry, unsigned
int runflags, void *argp, void *envp, spe_stop_info_t *stopinfo)

Echivalentul lor în codul SPU, pe funcţia main sunt respectiv


“speid“,“argp“,“envp“.
int main(unsigned long long speid, unsigned long long argp, unsigned long
long envp)

Pentru a putea trimite toţi parametri necesari prin funcţia “pthread_create“ (care
primeşte ca parametri o funcţie de tipul “void* myfunc(void *arg)“ şi un singur parametru
de tip void*) vom defini o structură ce îi va încapsula pe toţi:
typedef struct {
int cellno;
spe_context_ptr_t spe;
} thread_arg_t;

35
Structura va fi populată pentru fiecare SPE şi trimisă ca parametru (după cast la
void*) cu ajutorul vectorului “arg“:
int main(void) {
int i;
spe_context_ptr_t ctxs[SPU_THREADS];
pthread_t threads[SPU_THREADS];
thread_arg_t arg[SPU_THREADS];
...
ctxs[i] = spe_context_create (0, NULL);
...
arg[i].cellno = i;
arg[i].spe = ctxs[i];

/* Create thread for each SPE context */


pthread_create (&threads[i], NULL, &ppu_pthread_function,
&arg[i]));
...
}
În interiorul funcţiei “ppu_pthread_function”, se face cast la loc în
“thread_arg_t“:

void *ppu_pthread_function(void *thread_arg) {


thread_arg_t *arg = (thread_arg_t *) thread_arg;
...
spe_context_run(arg->spe, &entry, 0, (void *) arg->cellno, NULL,
NULL);
...

În codul SPU vom primi valoarea în “argp“ şi va fi nevoie de cast la tipul original (int
în acest caz):
#include <stdio.h>
int main(unsigned long long speid, unsigned long long argp, unsigned long
long envp){
printf("[SPU %d] is up.\n", (int) argp);
return 0;
}

Mesaje Mailbox de la SPU la PPU

36
Pentru a implementa această metodă, trebuie folosite o funcţie care trimite de pe SPU
spu_write_out_mbox şi una care citeşte datele pe PPU
spe_out_mbox_read(<speid>,<&data>). Înainte de a trimite date, trebuie verificat că este
loc la destinatie (bufferul PPU nu e plin), cu spu_stat_out_mbox şi, respectiv, că avem date
de citit pe PPU cu spe_out_mbox_status(<speid>).

Codul pentru SPU:

#include <stdio.h>
#include <spu_mfcio.h>
int main(unsigned long long speid, unsigned long long argp,
unsigned long long envp){
if (spu_stat_out_mbox() > 0) {
printf("[SPU %d] sending data=%d ...\n", (int) argp, (int)envp);
spu_write_out_mbox((uint32_t) envp);
} else {
printf("Mailbox full.\n");
}
return 0;
}

iar pentru PPU:


#include <stdio.h>
#include <libspe2.h>
#include <pthread.h>
extern spe_program_handle_t spu_mailbox;
int main(void) {
spe_context_ptr_t speid;
unsigned int entry = SPE_DEFAULT_ENTRY;
spe_stop_info_t stop_info;
unsigned int mbox_data;
speid = spe_context_create(0, NULL);
spe_program_load(speid, &spu_mailbox);
spe_context_run(speid, &entry, 0, (void*) 0, (void*) 55, &stop_info);
/*
* spe_context_run e blocant.
*/
while (spe_out_mbox_status(speid) == 0) { ; }
spe_out_mbox_read (speid, &mbox_data, 1);

37
printf("[PPU] SPU 0 sent data=%d\n",mbox_data);
spe_context_destroy(speid);
return 0;
}

Mesaje Mailbox de la PPU la SPU

În mod analog cu exemplul precedent, pentru a implementa această metodă, trebuie să


folosim o funcţie care trimite mesaje de pe PPU: spe_in_mbox_write şi una care citeşte
datele pe SPU: spu_read_in_mbox. Din moment ce trimitem un singur mesaj fiecărui SPU,
putem trimite datele neblocant fără grija unei eventuale suprascrieri (parametrul al patrulea al
spe_in_mbox_write este setat pe SPU_MBOX_ANY_NONBLOCKING). La citire, pentru
acest exemplu neavând alte procesări de executat, folosim un spinlock (busy-waiting cu
ajutorul funcţiei spu_stat_in_mbox).

În continuare codul schelet pentru un singur SPU.

#include <stdio.h>
#include <spu_mfcio.h>
int main(unsigned long long speid, unsigned long long argp,
unsigned long long envp){
uint32_t mbox_data; // variabila in care se citeste data din
mailbox
while (spu_stat_in_mbox()<=0); // busy-waiting...
// dacă aveam ceva de facut in acest timp, unde scriam
codul corespunzator?
mbox_data = spu_read_in_mbox();
printf("[SPU %d] received data=%d.\n", (int) argp, (int)data);
return 0;
}

iar pentru PPU:


#include <stdio.h>
#include <libspe2.h>
#include <pthread.h>
extern spe_program_handle_t spu_mailbox;
int main(void) {
spe_context_ptr_t speid;

38
unsigned int entry = SPE_DEFAULT_ENTRY;
spe_stop_info_t stop_info;
unsigned int mbox_data;
speid = spe_context_create(0, NULL);
spe_program_load(speid, &spu_mailbox);
spe_context_run(speid, &entry, 0, (void*) 0, (void*) 55, &stop_info);
// scriem o intrare in mailbox; in mod sigur trimitem un singur mesaj
pentru fiecare SPU asa ca nu e nevoie sa fie blocant
spe_in_mbox_write(speid, mbox_data, 1, SPE_MBOX_ANY_NONBLOCKING);
printf("[PPU] data sent to SPU# = %d\n",mbox_data);
spe_context_destroy(speid);
return 0;
}

Outbound Interrupt Mailbox - lucrul cu evenimente

Ideea în folosirea Outbound Interrupt Mailbox este evitarea situaţiei de busy-waiting


în codul PPU, pentru a vedea când vine un mesaj de la SPU. Pentru aceasta vom folosi
evenimente. În privinţa codului SPU scrierea se face la fel ca în Outbound Mailbox.

spe_event_unit_t pevents[NO_SPU], events_received[NO_SPU];


spe_event_handler_ptr_t event_handler;
event_handler = spe_event_handler_create();
/* unde spe_event_unit_t e (pre)definit astfel:
typedef struct spe_event_unit
{
unsigned int events;
spe_context_ptr_t spe;
spe_event_data_t data;
} spe_event_unit_t;
*/
// daca vrem sa lucram cu evenimente trebuie sa specificam acest lucru inca
de la crearea contextelor:
ctx[i] = spe_context_create(SPE_EVENTS_ENABLE,NULL);
// precizam tipul de evenimente cu care vom lucra
pevents[i].events = SPE_EVENT_OUT_INTR_MBOX;
pevents[i].spe = ctx[i]; // asociem cate un context eventurilor
// In Outbound Interrupt Mailbox PPE poate primi mesaj de la orice SPE;
// vrem sa stim exact de la ce SPE vine mesajul, asa ca vom asocia un numar
// fiecarui SPE, numar care va fi continut si in mesaj.

39
// Acest numar ni se va intoarce nemodificat in spe_event_wait(), atunci
cand // se va primi un eveniment de la SPEul asociat contextului

pevents[i].data.u32 = i;
// Inregistram un handler pentru evenimente
spe_event_handler_register(event_handler, &pevents[i] );
// Asteptarea unui eveniment in PPE:
spe_event_wait(handler, events_received, NO_SPU, 1);
printf("Am primit ceva de la speul %d:", events_received[0].data.u32);
// PPE citeste date din mailboxul de intreruperi corespunzator spe-ului de
// la care am primit evenimentul

spe_out_intr_mbox_read (events_received[0].spe,(unsigned int*) &data, 1,


SPE_MBOX_ANY_BLOCKING);

// ... sau in bucla cu procesare:


for (;!done;ret=spe_event_wait(event_handler, events_received, 1, 0)) {
// procesare
if (ret!=0) {
if (ret<0)
printf("Error: event wait error\n");
else {
if (events_received[0].events & SPE_EVENT_OUT_INTR_MBOX) {
printf("SPU%d sent me a
message\n",events_received[0].data.u32);
// citeste mailboxul corespunzator SPE care a trimis mesaj
spe_out_intr_mbox_read(events_received[0].spe, (unsigned int*)
&data, 1, SPE_MBOX_ANY_BLOCKING);
printf("SPU%d says he is no.%d\n",
events_received[0].data.u32,data);
done = 1;
}
}
}
}

// PPE poate raspunde scriind date in mailboxul INBOUND corespunzator spe-


ului // de la care a primit mesajul
spe_in_mbox_write( events_received[0].spe, msg, 1, SPE_MBOX_ANY_BLOCKING);

40
Parametrul al doilea al funcţiei spe_in_mbox_write() este un pointer la un array
(deci o adresă de adresă).

4.2 Transfer DMA

Prin design, un SPU poate accesa în mod direct doar memoria sa locală (local store).
Orice operaţie de citire/scriere de date în spaţiul principal de stocare (main storage) se face de
către MFC, prin transfer DMA. Optimizarea acestor transferuri joacă un rol crucial în scrierea
de programe eficiente pentru Cell. Se vor discuta concepte de bază pentru înţelegerea
mecanismului DMA, urmând ca în capitolul următor să discutăm mecanisme avansate de
folosire DMA (double-buffering). Mărimea unui transfer DMA este limitată la 16 KB.

Roluri: sender şi receiver

Direcţia transferului DMA este numită din perspectiva SPU:


• pentru transfer de date la SPU, adică din spaţiul principal de stocare în memoria
locală, se folosesc comenzi de tip get;
• pentru transfer de date de la SPU, adică din memoria locală în spaţiul principal de
stocare, se folosesc comenzi de tip put;

Cu toate acestea, din punctul de vedere al MFC, atât SPU asociat, cât şi PPU sau alte
SPE-uri pot iniţia transferul DMA. MFC gestionează două cozi pentru comenzile fără
caracter imediat: MFC SPU command queue (cu 16 intrări) pentru comenzi venite de la SPU
asociat şi MFC proxy command queue (cu 8 intrări) pentru comenzi venite de la PPU sau alte
SPE-uri.
În concluzie, atenţie la direcţia de transfer (dacă PPU cere date de la un SPU va folosi
comanda de tip put).

Ordonarea transferurilor: fence şi barrier

Comenzile DMA pot fi procesate nu neaparat în ordine FIFO. De aceea, dacă situaţia
o cere, este important să se folosească forme speciale ale comenzilor get şi put (getf, putb
etc.) care utilizeaza mecanisme de sincronizare (fence sau barrier). Mai mult decât atât, MFC
dispune de comenzi de sincronizare (e.g. barrier, mfceieio, mfcsync etc.).
41
Pentru realizarea sincronizării se foloseşte conceptul de tag group. Fiecărei comenzi
MFC care intră în coada de comenzi îi este asociat un tag group ID de 5 biţi. Tag group-urile
sunt independente de la o coadă la alta (MFC proxy command queue vs. MFC SPU command
queue). În implementarea de Linux a libspe2, tag group id poate lua valori între 0 şi 15.

Comenzi get şi put cu fence sau barrier

Comenzile cu sufix ce indică fence impun completarea în prealabil a tuturor


comenzilor DMA din acelaşi tag group iniţiate înaintea comenzii curente. Astfel, o comandă
iniţiată ulterior comenzii cu flag-ul fence se poate executa înaintea acesteia din urmă.
Comenzile cu sufix ce indică barrier impun completarea în prealabil a tuturor
comenzilor DMA din acelaşi tag group.

Comenzi de sincronizare

Comanda barrier (funcţia mfc_barrier pe SPU, indisponibilă pe PPU), în contrast


cu formele cu barieră ale comenzilor get şi put, impune finalizarea tuturor comenzilor MFC
din coada de comandă DMA (DMA command queue) lansate în prealabil, indiferent de tag
group. Comanda barrier nu are efect asupra comenzilor DMA cu caracter imediat: getllar,
putllc, putlluc, care nu pot aparţine unui tag group.
Comanda mfcsync (funcţia mfc_sync pe SPU, intrinsic __sync pe PPU) asigură
completarea operaţiilor get şi put din tag group-ul specificat înaintea altor unităţi de procesare
şi mecanisme din sistem.

DMA la nivel de cod


Atât SPU, cât şi PPU au funcţii ce mapează comenzi de tip get si put (* indică
posibilitatea de adăugare de sufixe):
• Un program SPU poate apela funcţii definite în spu_mfcio.h de tipul:
o mfc_put*
o mfc_get*
• Un program PPU poate apela funcţii definite în libspe2.h de tipul:
o spe_mfcio_put*
o spe_mfcio_get*

42
SDK 3.0 defineşte şi un nou set de funcţii pentru DMA în cbe_mfc.h, care nu sunt
foarte clar descrise, dar sunt considerate mai performante. Atât comenzile get, cât şi cele put
au mai multe forme, prin adăugarea de sufixe (e.g. mfc_get, spe_mfcio_getf, mfc_putlf etc.).
Sufixele au următoarele semnificaţii:

Luăm ca exemplu o instrucţiune de SPU şi una pentru PPU (celelalte se tratează în


mod asemănător):
(void) mfc_get(volatile void *ls, uint64_t ea, uint32_t size, uint32_t tag,
uint32_t tid, uint32_t rid)

implementare:
spu_mfcdma64(ls, mfc_ea2h(ea), mfc_ea2l(ea), size, tag,
( (tid«24)|(rid«16)|MFC_GET_CMD) )

respectiv:
int spe_mfcio_put (spe_context_ptr_t spe, unsigned int lsa, void *ea,
unsigned int size, unsigned int tag, unsigned int tid, unsigned int rid)

Din punct de vedere al parametrilor este important de remarcat că locaţia din spaţiu
principal de stocare este dată de parametrul ea (effective address), iar cea din memoria locală
de parametrul ls/lsa (local store address). Între ceilalţi parametri mai recunoaştem size
(dimensiunea datelor transmise) şi tag (care reprezintă tag group id-ul ales). Funcţia pentru
PPU are în plus, evident, parametrul spe (prin care se alege SPU cu care se comunică).

43
În mod explicit s-a amintit doar de o parte din funcţiile MFC disponibile pentru lucrul
cu DMA. Acestea sunt numeroase şi apar clasificate în:
• tag manager (e.g. mfc_tag_reserve, mfc_tag_release)
• comenzi DMA (e.g. mfc_put, mfc_get)
• comenzi pentru liste DMA (e.g. mfc_putl, mfc_getl)
• operaţii atomice (e.g. mfc_getllar, mfc_putllc)
• comenzi de sincronizare (e.g. mfc_barrier)
• comenzi pentru statusul DMA (e.g. mfc_stat_cmd_queue, mfc_read_tag_status) etc.

Obţinerea unei performanţe ridicate

SPE-urile folosesc transfer DMA asincron pentru a ascunde latenţa memoriei şi


overhead-ul transferului, ocupându-se în paralel de calcule (buffer-are dublă).
Performanţa unui transfer DMA (mai mare de 128 de octeţi) este maximă atunci când
adresele sursă şi destinaţie sunt aliniate la dimensiunea liniei de cache.
Sunt de preferat transferurile iniţiate de SPE celor iniţiate de PPE, deoarece: sunt de 8
ori mai multe SPE-uri, într-un MFC coada de comenzi pentru SPU este de două ori mai mare
decât cea pentru PPE şi celelalte SPE-uri (16 intrări faţă de 8), transferurile iniţiate de
“consumatori” sunt mai uşor de sincronizat etc.
Pentru a ne face o idee cu privire la costul unui transfer: un thread rulând pe SPU
poate face o cerere DMA în 10 ciclii de ceas (in condiţii de încărcare optimă). Pentru fiecare
dintre cei cinci parametrii ai unei comenzi cum ar fi mfc_get se scriu date pe un canal SPU
(latenţa instrucţiunilor pentru canale SPU este de 2 ciclii de ceas). Latenţa de la emiterea
comenzii de catre DMA si până la ajungerea acesteia în EIB este de aproximativ 30 de ciclii
(dacă se cere aducerea unui element al unei liste se pot adăuga alţi 20 de ciclii). Faza de
comanda a transferului necesita o verificare a elementelor de pe magistrala şi are nevoie de
circa 50 de ciclii magistrala (100 de ciclii SPU). Pentru operaţii get, se mai adauga latenţa de
aducere a datelor din memoria off-chip la controller-ul de memorie, apoi pe magistrala la
SPE, după care se scriu în Local Store. Pentru operaţii put, latenta DMA include doar
transmiterea datelor până la controller-ul de memorie, fără transferul acestora pe memoria
off-chip.

Lucrări practice. Transfer DMA

44
Transfer DMA iniţiat de SPU

Se va învăţa folosirea comenzilor iniţiate de SPE pentru a transfera date cu spaţiul


principal de stocare şi anume:

• SPE rezervă şi elibereaza id-uri tag cu ajutorul tag manager;


• programul SPU foloseşte comanda de tip get pentru a aduce date din memoria
principală în memoria locală;
• programul SPU foloseşte comanda de tip put pentru a duce date din memoria locală în
memoria principală;
• programul SPU aşteaptă finalizarea comenzilor.

Cod SPU - transfer DMA iniţiat de SPU

#include <spu_mfcio.h>

// Macro ce asteapta finalizarea grupului de comenzi DMA cu acelasi tag


// 1. scrie masca de tag
// 2. citeste statusul - blocant pana la finalizarea comenzilor
#define waitag(t) mfc_write_tag_mask(1<<t); mfc_read_tag_status_all();

// Local store buffer: aliniere la marime si adresa DMA


// - trebuie sa fie aliniat la 16B, altfel se genereaza eroare pe
magistrala
// - poate fi aliniat la 128B pentru performante mai bune
volatile char str[256] __attribute__ ((aligned(16)));
// argp - adresa efectiva in spatiul principal de stocare
// envp - marimea bufferului (in cazul nostru string) in octeti

int main( uint64_t spuid , uint64_t argp, uint64_t envp ){


// rezervam tag folosind tag manager
uint32_t tag_id = mfc_tag_reserve();
if (tag_id==MFC_TAG_INVALID){
printf("SPU: ERROR can't allocate tag ID\n"); return -1;
}

// get: ia datele din spatiul principal de stocare


mfc_get((void *)(str), argp, (uint32_t)envp, tag_id, 0, 0);

45
// asteapta sa se finalizeze comanda get (pe acest tag - in caz ca
citesc mai multe SPU-uri)
waitag(tag_id);

// proceseaza datele
printf("SPU: %s\n", str);
strcpy(str, "Completeaza formularul: Nume: Synergistic Processing
Element");
printf("SPU: %s\n", str);

// put: trimite datele in spatiul principal de stocare


mfc_put((void *)(str), argp, (uint32_t)envp, tag_id, 0, 0);

// asteapta sa se finalizeze comanda put (pe acest tag - in caz ca


scriu mai multe SPU-uri)
waitag(tag_id);

// nu mai avem nevoie de tag


mfc_tag_release(tag_id);
return (0);
}

Cod PPU - transfer DMA iniţiat de SPU

#include <libspe2.h>

// macro pentru rotunjirea superioara a valorii la multiplu de 16 (pentru


conditiile DMA)
#define spu_mfc_ceil16(value) ((value + 15) & ~15)
volatile char str[256] __attribute__ ((aligned(16)));

extern spe_program_handle_t spu;

int main(int argc, char *argv[])


{
void *spe_argp, *spe_envp;
spe_context_ptr_t spe_ctx;
spe_program_handle_t *program;
unsigned int entry = SPE_DEFAULT_ENTRY;

// trimiterea de parametrii catre SPE

46
strcpy( str, "Completeaza formularul: Nume:
.........................");
printf("PPU: %s\n", str);
spe_argp=(void*)str; // adresa
spe_envp=(void*)strlen(str);
spe_envp=(void*)spu_mfc_ceil16((unsigned int)spe_envp); //rotunjeste
dimensiunea bufferului la 16B

// rularea programului SPU:


// - creare de context
// - incarcarea programului SPU
// - rulare

// asteptam ca SPU sa termine de procesat


printf("PPU: %s\n", str);

return (0);
}

Transfer DMA iniţiat de PPU

Se va învăţa cum să iniţiem transferuri DMA de la PPU şi anume:


• PPU mapează memoria locală a unui SPE în memoria partajată şi primeşte un pointer
la adresa efectivă din Local Store;
• SPU foloseşte mailbox să trimită lui PPU offsetul zonei de date de transmis din
memoria locală;
• PPU foloseşte comanda de tip put pentru a transfera datele din memoria locală în
spaţiul principal de stocare;
• programul PPU aşteaptă finalizarea transferului înainte de a folosi datele.

Cod PPU - transfer DMA iniţiat de PPU

#include <libspe2.h>
#include <ppu_intrinsics.h>

#define BUFF_SIZE 1024


spe_context_ptr_t spe_ctx;

47
unsigned int ls_offset; // offset (adresa) in local store a bufferului de
la SPU

// buffer PPU
volatile char my_data[BUFF_SIZE] __attribute__ ((aligned(128)));

extern spe_program_handle_t spu;

int main(int argc, char *argv[]){


int ret;
unsigned int tag_id, status;
unsigned int entry = SPE_DEFAULT_ENTRY;

// rezervam un tag: trebuie sa folosite taguri intre 0-15 (16-31 sunt


folosite de kernel)
tag_id = 1;

// rularea programului SPU:


// - creare de context
// - incarcarea programului SPU
// - rulare

// preia de la SPU adresa bufferului lui in Local Store prin mailbox


(nu prea eficient)
printf("PPU: Tema: scrie un eseu!\n");
while(spe_out_mbox_read(spe_ctx, &ls_offset, 1)<=0);

// comanda put pentru a prelua datele in spatiul principal de stocare


do{
ret=spe_mfcio_put( spe_ctx, ls_offset, (void*)my_data, BUFF_SIZE,
tag_id, 0,0);
}while( ret!=0);

// asteapta executia comenzii put


ret = spe_mfcio_tag_status_read(spe_ctx,0,SPE_TAG_ALL, &status);
if(ret!=0){
perror ("Error status was returned");
exit (1);
}

// sincronizam inainte de a folosi datele

48
__lwsync();
printf("PPU: SPU mi-a trimis lucrarea sa - %s\n", my_data);

return (0);
}

Cod SPU - transfer DMA iniţiat de PPU

#include <spu_intrinsics.h>
#include <spu_mfcio.h>
#define BUFF_SIZE 1024

// buffer la SPU
volatile char my_data[BUFF_SIZE] __attribute__ ((aligned(128)));
int main(int speid , uint64_t argp)
{
strcpy((char*)my_data, "'Lucrul in echipa' de SPU#\n" );
// trimite PPU offsetul la buffer folosind mailbox - blocant daca
mailboxul este plin
spu_write_out_mbox((uint32_t)my_data);
return 0;
}

4.3 Notificare prin semnale

Notificarea prin semnale este un mecanism foarte uşor de folosit, care permite PPU şi
SPU să trimită semnale unui (alt) SPE folosind regiştrii de 32 de biţi.
Ca şi în cazul mailbox, atunci când sursa este un SPU, el poate poate trimite semnale
altor SPE. Atenţie la aceasta distincţie, este vorba despre SPU local, adică cel aflat pe acelaşi
SPE cu registrul de notificare în discuţie.

Accesarea registrilor pentru notificarea cu semnale


Fiecare SPU are doi regiştrii identici pentru notificarea cu semnale. Aceştia, locali
unui SPU, sunt folosiţi exclusiv pentru primirea de semnale de la alte elemente (PPU, SPE).
Programele pot accesa aceşti regiştri astfel:
• SPU local citeşte notificarea prin canale proprii;
• PPU trimite semnal unui SPU scriind pe interfaţa MMIO corespunzătoare;

49
• SPU trimite semnal unui alt SPE folosind comenzi de semnalizare (sndsig, sndsigf,
sndsigb) care practic se mapează la comenzi DMA (e.g. put).

La citirea registrului de către SPU local, registrul se resetează.

Moduri: many-to-one si one-to-one


Scrierile multiple într-un registru de notificare pot fi gestionate în unul din două
moduri:
• OR mode (many-to-one): MFC adună mai multe semnale prin operaţie logică OR
• Overwrite mode (one-to-one): orice acţiune de scriere produce pierderea informaţiei
vechi
Configurarea modului de lucru poate fi precizată de PPU la crearea contextului SPE
corespunzător, setând un flag (SPE_CFG_SIGNOTIFY1_OR) pe funcţia spe_context_create.

Acces blocant vs. non-blocant


Accesul la regiştrii de notificare a semnalelor se face:
• pentru PPU: non-blocant (vezi şi modul de scriere mai sus);
• pentru SPU care scrie: similar cu comanda DMA put (se blochează doar dacă coada
MFC este plină);
• pentru SPU care citeşte: blocant până la apariţia unui eveniment.

Notificare prin semnale la nivel de cod


Mecanismul de notificare prin semnale poate fi utilizat foarte uşor cu ajutorul
funcţiilor MFC:
• SPU local poate citi regiştrii săi folosind spu_read_signal* şi starea lor cu
spu_stat_signal* din spu_mfcio.h;
• PPU poate trimite semnale unui SPU cu spe_signal_write din libspe2.h;
• alte SPU pot trimite semnale unui SPU cu mfc_sndsig* din spu_mfcio.h;
o pentru a folosi această abordare, în prealabil: PPU mapează zona de semnale
în spaţiul principal de stocare cu spe_ps_area_get, setând flagul
SPE_SIG_NOTIFY_x_AREA, iar PPE transmite SPU sursă adresa de bază a
zonei de semnale.
Asteriscul denotă opţiunile 'f' (fence), 'b' (barrier) sau nimic.

50
Capitolul 5
Ascunderea duratei transferurilor DMA
(dubla - buffer-are)

Modul de lucru normal pentru SPE este să primească date de la PPE, să le proceseze
şi să trimită rezultatele înapoi în spaţiul principal de stocare.

5.1 Simpla buffer-are


Simpla buffer-are este, în principiu, soluţia folosită până acum în exemple precum cel
cu DMA din capitolul precedent. Din punct de vedere al SPU se petrec următoarele lucruri:
• SPU alocă un buffer local, aliniat la 128 de octeţi, pentru stocarea datelor primite şi
a datelor de răspuns;
• Rezervă un tag;
• Primeşte primul bloc de date (de control), în care vede câte blocuri are de procesat;
• Pentru fiecare dintre blocurile de procesat:
o Transferă blocul în memoria locală (get) şi aşteaptă ca transferul să se
finalizeze;
o Procesează datele;
o Transferă rezultatele în memoria sistemului (put) şi aşteaptă ca transferul să se
finalizeze.

5.2 Dubla buffer-are


Folosind soluţia simpla buffer-are, se consumă foarte mult timp aşteptând încheierea
transferurilor DMA. O bună optimizare este alocarea de două buffere de lucru în loc de unul
şi intercalarea alternativă a calculelor pe un buffer cu transferul în celălalt buffer. Din punct
de vedere al SPU se petrec următoarele lucruri:

51
Fig. 5.1 Organigrama de funcţionare pentru dubla – buffer-are

Dacă punem această schemă în pseudocod obţinem următoarele (transferul de primire


GET este explicitat în două operaţii: cer şi aştept):
• Aloc două buffere de întrare şi două buffere de ieşire, două taguri etc.;
• Cer bloc de control (parameter context) de la PPU;
• Aştept bloc de control de la PPU;
• Cer primul bloc (buffer) de date de la PPU;
• Cât timp nu s-au procesat toate datele:
o Cer bufferul următor de date de la PPU;
o Aştept bufferul precedent de date de la PPU;
o Procesez bufferul precedent;
o Trimit bufferul precedent la PPU;
• Aştept ultimul buffer de date de la PPU;
• Procesez ultimul buffer;
• Trimit ultimul buffer la PPU.

Se folosesc următoarele headere:


#include <spu_intrinsics.h>
#include <spu_mfcio.h>

Pentru transmiterea parametrilor (blocul de control) folosim structura:


typedef struct {
uint32_t *in_data;
uint32_t *out_data;
uint32_t *status;

52
int size;
} parm_context;

De asemenea, vom folosi următoarele date:


//ctx: contextul venit de la PPU, contine o serie de parametri:
//adresa la in_data si out_data in main storage si status
volatile parm_context ctx __attribute__ ((aligned(16)));

//ls_in_data: doua buffere de intrare (avansat: double buffering se


poate //face si 'in-place'
volatile uint32_t ls_in_data[2][ELEM_PER_BLOCK] __attribute__
((aligned(128)));

// ls_out_data: doua buffere de iesire


volatile uint32_t ls_out_data[2][ELEM_PER_BLOCK] __attribute__
((aligned(128)));

// status
volatile uint32_t status __attribute__ ((aligned(128)));

// tag_id: doua tag groups


uint32_t tag_id[2];

Expandăm pseudocodul de mai sus şi obţinem un schelet de cod (funcţia main):


tag_id[0] = mfc_tag_reserve();
tag_id[1] = mfc_tag_reserve();
// ... alte declaratii de variabile

// Cer blocul de control de la PPU


mfc_get((void*)(&ctx), (uint32_t)argv, sizeof(parm_context), tag_id[0], 0,
0);
// Astept blocul de control de la PPU
waitag(tag_id[0]);

// Initializare
in_data = ctx.in_data;
out_data = ctx.out_data;
left = ctx.size;
cnt = (left<ELEM_PER_BLOCK) ? left : ELEM_PER_BLOCK;

// Cer primul bloc (buffer) de date de la PPU


53
buf = 0;
mfc_getb((void *)(ls_in_data), (uint32_t)(in_data), cnt*sizeof(uint32_t),
tag_id[0], 0, 0);
while (cnt < left) { // cat timp nu s-a terminat de procesat

left -= SPU_Mbox_Statnt;
nxt_in_data = in_data + cnt;
nxt_out_data = out_data + cnt;
nxt_cnt = (left<ELEM_PER_BLOCK) ? left : ELEM_PER_BLOCK;

// Cer bufferul urmator de date de la PPU


// Atentie la bariera!
nxt_buf = buf^1;
mfc_getb((void*)(&ls_in_data[nxt_buf][0]), (uint32_t)(nxt_in_data),
nxt_cnt*sizeof(uint32_t), tag_id[nxt_buf], 0, 0);

// Astept bufferul precedent de date de la PPU


waitag(tag_id[buf]);

// Procesez bufferul precedent


for (i=0; i<ELEM_PER_BLOCK; i++){
// ... whatever
}

// Trimit bufferul precedent la PPU


mfc_put((void*)(&ls_out_data[buf][0]), (uint32_t)(out_data),
cnt*sizeof(uint32_t),tag_id[buf],0,0);

// Pregatim urmatoarea iteratie


in_data = nxt_in_data;
out_data = nxt_out_data;
buf = nxt_buf;
cnt = nxt_cnt;
}

// Astept ultimul buffer de date de la PPU


waitag(tag_id[buf]);

// Procesez ultimul buffer


for (i=0; i<ELEM_PER_BLOCK; i++){
// ... whatever

54
}
// Trimit ultimul buffer la PPU
// Punem bariera pentru a ne asigura ca s-a trimis si ultimul rezultat
inainte de a confirma statusul
mfc_putb((void*)(&ls_out_data[buf][0]), (uint32_t)(out_data),
cnt*sizeof(uint32_t), tag_id[buf],0,0);
waitag(tag_id[buf]);

// Actualizam status pentru PPU


status = STATUS_DONE;
mfc_put((void*)&status, (uint32_t)(ctx.status), sizeof(uint32_t),
tag_id[buf],0,0);
waitag(tag_id[buf]);

// Clean-up
mfc_tag_release(tag_id[0]);
mfc_tag_release(tag_id[1]);

La PPU vom folosi următoarele headere:


#include <libspe2.h>
#include <cbe_mfc.h>
#include <pthread.h>

Codul PPU este mult mai simplu (schelet de cod pentru funcţia main):
// Initializari (printre altele):
status = STATUS_NO_DONE;
ctx.in_data = in_data;
ctx.out_data = out_data;
ctx.size = NUM_OF_ELEM;
ctx.status = &status;
data.argp = &ctx;

// Creeaza context
// Incarca program
// Ruleaza threaduri SPE
// Asteapta ca SPE sa finalizeze

// Asteapta sa se finalizeze scrierea datelor


while (status != STATUS_DONE);
// Verificari si clean-up

55
Capitolul 6
Standardul MPI

MPI este un protocol de comunicaţie folosit pentru programarea paralelă, menit să


ofere funcţionalitate pentru sincronizarea şi comunicarea între procese într-un mod
independent de limbaj şi de platformă (există implementări ale MPI pentru aproape orice
platformă). Programele MPI sunt orientate către procese, aşadar pentru obţinerea de
performanţe maxime trebuiesc definite pe fiecare computer atâtea procese câte procesoare
există (sau core-uri).

Rutine C utile:
- MPI_Init int MPI_Init(int *argc, char ***argv)
Iniţializează mediul de execuţie.

- int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int
tag, MPI_Comm comm)
Transmite un mesaj către un alt proces.

- int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int
tag, MPI_Comm comm, MPI_Status *status)
Primeşte un mesaj de la un alt proces

- int MPI_Comm_size(MPI_Comm comm, int *size)


Determină mărimea unui grup de procese

- int MPI_Comm_rank(MPI_Comm comm, int *rank)


Determină rangul procesului apelant

- int MPI_Get_processor_name(char *name, int *resultlen)


Determină numele procesorului
- int MPI_Bcast(void *buffer, int count, MPI_Datatype datatype, int root,
MPI_Comm comm)
Transmite un mesaj de la procesul root la toate celelalte procese din grup.

56
- int MPI_Reduce(void *sendbuf, void *recvbuf, int count, MPI_Datatype
datatype, MPI_Op op, int root, MPI_Comm comm)
Reduce valorile de la toate procesele, la o singură valoare.

- int MPI_Finalize()
Închide mediul de execuţie MPI.

6.1 Descrierea aplicaţiei practice

Iniţial s-a plecat de la implementarea unui algoritm de calcul aproximativ a valorii PI


calculând aria integralei prin metoda trapezului, (figura 6.1).
Folosind librăria MPI, se distribuie fiecărui nod câte două thread-uri (PPU este dual-
threading). Cu comanda mpirun –np 18 pi se lansează 18 thread-uri pe cele 9 noduri. Fiecare
din aceste thread calculează o parte egal distribuită din integrala definită de blocul:
h = 1. / (double) n;
for (i=me+1; i <= n; i+=nprocs){
x = (i-1)*h;
piece = piece + (4/(1+(x)*(x)) + 4/(1+(x+h)*(x+h))) / 2 * h;
}
Variabila “me” indică rangul procesului (între 1 şi 18). Variabila “n” defineşte
precizia de calcul. “h” este pasul cu care se incrementează variabila “x” în calculul ariei
integralei 0 → 1.

Fig. 6.1: Formula de calcul a valorii PI prin metoda trapezului

57
După rularea programului de test (metoda trapezului), cu ”n” între 105 şi 1012 se obţin
următoarele rezultate (figura 6.2):

Fig. 6.2: Rezultatele obţinute în calculul valorii PI prin metoda trapezului folosind doar
librăria MPI, cu distribuţie echilibrată doar pe procesoarele PPU;
(stânga - SQRT(Timp(sec)), dreapta – 20 + LOG(Eroare))

Din grafic rezultă că odată cu creşterea rezoluţiei de calcul creşte şi timpul de calcul.
Eroarea scade până la valoarea lui 1011 după care creşte datorită faptului că se folosesc
variabile de tip double.

6.2 Scrierea primului program pentru Cluster 9 x PS3 folosind librăria


MPI

1. Creaţi un director numit “pimpi”.


2. În directorul “pimpi”, creaţi un fişier cu numele “Makefile”, în care scrieţi următoarea
secvenţă de cod:

FILEMPI=pi
MPI=mpicc
$(FILEMPI): $(FILEMPI).c
$(MPI) $^ -o $@

3. În directorul “pimpi”, creaţi un fişier cu numele “pi.c”, în care scrieţi următoarea


secvenţă de cod:
#include <stdio.h>
#include <stdlib.h>

58
#include "mpi.h"
int main(int argc, char *argv[])
{
double i, n;
double h, pi, x;
struct timeval tim;
double t1, t2;
int me, nprocs;
double piece, picalc =
3.14159265358979323846264338327950288419716939937510;

/* --------------------------------------------------- */

MPI_Init (&argc, &argv);


MPI_Comm_size (MPI_COMM_WORLD, &nprocs);
MPI_Comm_rank (MPI_COMM_WORLD, &me);

/* --------------------------------------------------- */

if (me == 0)
{
//printf("%s", "Input number of intervals:\n");
//scanf ("%d", &n);
n = atof(argv[1]);
printf("n = %lf\n", n);
gettimeofday(&tim, NULL);
t1 = tim.tv_sec + (tim.tv_usec/1000000.0); }

/* --------------------------------------------------- */

MPI_Bcast (&n, 1, MPI_INT, 0, MPI_COMM_WORLD);

/* --------------------------------------------------- */

h = 1. / (double) n;
piece = 0.;
for (i=me+1; i <= n; i+=nprocs)
{
x = (i-1)*h;
piece = piece + ( 4 / (1+(x)*(x)) + 4 / (1+(x+h)*(x+h))) / 2 * h;
}

59
//printf("%d: pi = %25.15f\n", me, piece);

/* --------------------------------------------------- */

MPI_Reduce (&piece, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);

/* --------------------------------------------------- */

if (me == 0)
{
gettimeofday(&tim, NULL);
t2 = tim.tv_sec + (tim.tv_usec/1000000.0);
printf("pi = %1.50f\n", pi);
printf("Error = %1.50f\n", pi - picalc);
printf("Elapsed time = %.10lf sec\n\n", t2 - t1);
}

/* --------------------------------------------------- */

MPI_Finalize();
return 0;
}

4. Compilaţi programul folosind următoarea comandă în consolă, în timp ce vă aflaţi în


directorul “pimpi”:

make

5. Rulaţi programul executabil generat folosind următoarea comandă în consolă:

mpirun –np pp pi iiiii

în care pp este numărul de procese mpi lansate. Poate fi un număr cuprins între 1 şi 18. iiiii
este numărul de iteraţii pentru calculul valorii lui PI.

60
Capitolul 7
Distribuţia MPI-SDK

Aplicaţia descrisă în capitolul anterior folosea distribuţia MPI pentru a accesa thread-
urile PPU. Se puteau lansa maxim 18 thread-uri (9 noduri PPU x 2 thread-uri). Unităţile de
calcul SPU nu erau accesate. În acest capitol se va realiza o aplicaţie care va permite
activarea tuturor unităţilor de calcul PPU-SPU. Astfel în thread-urile pare din PPU se
implementează şi secvenţa de creare-activare şi distrugere a thread-urilor SPU (Figura 7.1).

Fig. 7.1: Modul de activare a thread-urilor SPU

Secvenţa de program ppu.c ce realizează acest lucru este prezentată mai jos:
if(rank%2 == 0 ){
// Create a context and thread for each SPU
for (i=0; i<spus; i++) {
// Create context
// Load program into the context
// Create thread
}
// perform PPU – even thread job
printf("End PPE thread!!! from rank: %d @ %s \n", rank, host);

// Wait for the threads to finish processing


for (i = 0; i < spus; i++) {
// pthread_join
// Destroy context
}
61
}
else{
// perform PPU – odd thread job
printf("End PPE thread!!! from rank: %d @ %s \n", rank, host);
}
Se propune ca temă realizarea unei aplicaţii care să afişeze rangul fiecărui thread PPU
şi rangul şi id-ul fiecărui thread SPU.

62

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