Documente Academic
Documente Profesional
Documente Cultură
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).
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.
3
Fig. 1.4: Diagrama Bloc a procesorului SPE
4
1.2 PowerPC Processor Element (PPE)
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)
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.
10
Fig. 1.10: Operaţia de Byte-shuffle
vector bool char Sixteen 8-bit unsigned 0(false), 255 (true) Ambele
11
boolean
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 bool int Four 32-bit signed values 0 (false), 231-1 (true) PPU
Î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
2.1 Instalarea FC8 cu Cell SDK 3.0, Arhitectura şi set-area PS3 Cluster
15
Fig. 2.2: Client Telnet/SSH Putty
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ă.
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
########################################################################
17
########################################################################
# buildutils/make.footer
########################################################################
include /opt/cell/sdk/buildutils/make.footer
########################################################################
# Target
########################################################################
PROGRAM_ppu = simple
########################################################################
# Local Defines
########################################################################
INSTALL_DIR = ../ppu/
INSTALL_FILES = $(PROGRAM_ppu)
########################################################################
# buildutils/make.footer
########################################################################
include /opt/cell/sdk/buildutils/make.footer
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <libspe2.h>
#include <pthread.h>
#define MAX_SPU_THREADS 6
18
void *ppu_pthread_function(void *arg) {
spe_context_ptr_t ctx;
unsigned int entry = SPE_DEFAULT_ENTRY;
int main()
{
int i, spu_threads;
spe_context_ptr_t ctxs[MAX_SPU_THREADS];
pthread_t threads[MAX_SPU_THREADS];
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);
}
}
#######################################################################
# Target
########################################################################
PROGRAMS_spu := spu
LIBRARY_embed := spu.a
########################################################################
# Local Defines
########################################################################
########################################################################
# buildutils/make.footer
########################################################################
include /opt/cell/sdk/buildutils/make.footer
20
printf("Hello World! from Cell (0x%llx)\n", id);
return 0;
}
make
1. #include <libspe2.h>
2. spe_context_ptr_t spe_context_create(unsigned int flags,
spe_gang_context_ptr_t gang)
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.
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)
1. #include <libspe2.h>
2. int spe_context_destroy (spe_context_ptr_t spe)
23
Capitolul 3
Lucrul cu tipul vector in Cell/B.E.
3.1 Introducere
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
Pointeri la vectori :
a) Varianta nevectorizată:
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;
}
Î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 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;
}
28
Capitolul 4
Mecanisme de comunicare PPU - SPU
4.1 Mailbox
29
Termenul de mailbox poate referi colectiv toate elementele care asigură acest
mecanism: regiştrii MMIO, canale, stări, întreruperi, cozi şi evenimente.
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
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.
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.
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;
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
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
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];
Î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;
}
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>).
#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;
}
37
printf("[PPU] SPU 0 sent data=%d\n",mbox_data);
spe_context_destroy(speid);
return 0;
}
#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;
}
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;
}
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
40
Parametrul al doilea al funcţiei spe_in_mbox_write() este un pointer la un array
(deci o adresă de adresă).
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.
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).
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 de sincronizare
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:
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.
44
Transfer DMA iniţiat de SPU
#include <spu_mfcio.h>
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);
#include <libspe2.h>
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
return (0);
}
#include <libspe2.h>
#include <ppu_intrinsics.h>
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)));
48
__lwsync();
printf("PPU: SPU mi-a trimis lucrarea sa - %s\n", my_data);
return (0);
}
#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;
}
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.
49
• SPU trimite semnal unui alt SPE folosind comenzi de semnalizare (sndsig, sndsigf,
sndsigb) care practic se mapează la comenzi DMA (e.g. put).
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.
51
Fig. 5.1 Organigrama de funcţionare pentru dubla – buffer-are
52
int size;
} parm_context;
// status
volatile uint32_t status __attribute__ ((aligned(128)));
// Initializare
in_data = ctx.in_data;
out_data = ctx.out_data;
left = ctx.size;
cnt = (left<ELEM_PER_BLOCK) ? left : ELEM_PER_BLOCK;
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;
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]);
// Clean-up
mfc_tag_release(tag_id[0]);
mfc_tag_release(tag_id[1]);
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
55
Capitolul 6
Standardul MPI
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
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.
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.
FILEMPI=pi
MPI=mpicc
$(FILEMPI): $(FILEMPI).c
$(MPI) $^ -o $@
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;
/* --------------------------------------------------- */
/* --------------------------------------------------- */
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); }
/* --------------------------------------------------- */
/* --------------------------------------------------- */
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);
/* --------------------------------------------------- */
/* --------------------------------------------------- */
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;
}
make
î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).
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);
62