Laboratoare Pp

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

description

Procesare Pararela

Transcript of Laboratoare Pp

Page 1: Laboratoare Pp

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

Page 2: Laboratoare Pp

2

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

Page 3: Laboratoare Pp

3

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

Page 4: Laboratoare Pp

4

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.

Page 5: Laboratoare Pp

5

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;

Page 6: Laboratoare Pp

6

• 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).

Page 7: Laboratoare Pp

7

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

Page 8: Laboratoare Pp

8

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

Page 9: Laboratoare Pp

9

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

Page 10: Laboratoare Pp

10

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.

Page 11: Laboratoare Pp

11

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

Page 12: Laboratoare Pp

12

boolean

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

vector unsigned short

int Eight 16-bit unsigned values 0…65535 Ambele

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:

Page 13: Laboratoare Pp

13

- 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

Page 14: Laboratoare Pp

14

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

Page 15: Laboratoare Pp

15

- 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.

Page 16: Laboratoare Pp

16

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:

Page 17: Laboratoare Pp

17

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

Page 18: Laboratoare Pp

18

########################################################################

# 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

Page 19: Laboratoare Pp

19

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

Page 20: Laboratoare Pp

20

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)

{

Page 21: Laboratoare Pp

21

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:

Page 22: Laboratoare Pp

22

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)

Page 23: Laboratoare Pp

23

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.

Page 24: Laboratoare Pp

24

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 Intrinsic

Vector/SIMD Multimedia Extension PPU Intrinsic

Pentru ce tipuri de date

spu_add vec_add vector operands only, no scalar operands

spu_and vec_and vector operands only, no scalar operands

spu_andc vec_andc all

spu_avg vec_avg all

spu_cmpeq vec_cmpeq vector operands only, no scalar operands

spu_cmpgt vec_cmpgt vector operands only, no scalar 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

spu_muo vec_mulo halfword vector operands only, no scalar operands

spu_nmsub vec_nmsub float only

Page 25: Laboratoare Pp

25

spu_nor vec_nor all

spu_or vec_or vector operands only, no scalar operands

spu_promote vec_promote all

spu_re vec_re all

spu_rl vec_rl vector operands only, no scalar operands

spu_rsqrte vec_rsqrte all

spu_sel vec_sel all

spu_splats vec_splats all

spu_sub vec_sub vector operands only, no scalar operands

spu_genb vec_genbl vector operands only, no scalar operands

spu_xor vec_xor vector operands only, no scalar 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:

Page 26: Laboratoare Pp

26

• 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];

Page 27: Laboratoare Pp

27

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:

Page 28: Laboratoare Pp

28

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.

Page 29: Laboratoare Pp

29

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.

Page 30: Laboratoare Pp

30

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.

Page 31: Laboratoare Pp

31

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;

Page 32: Laboratoare Pp

32

• 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;

Page 33: Laboratoare Pp

33

• (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;

Page 34: Laboratoare Pp

34

• 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.

Page 35: Laboratoare Pp

35

Î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;

Page 36: Laboratoare Pp

36

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

Page 37: Laboratoare Pp

37

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

Page 38: Laboratoare Pp

38

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;

Page 39: Laboratoare Pp

39

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.

Page 40: Laboratoare Pp

40

// 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);

Page 41: Laboratoare Pp

41

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

Page 42: Laboratoare Pp

42

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*

Page 43: Laboratoare Pp

43

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ă).

Page 44: Laboratoare Pp

44

Î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

Page 45: Laboratoare Pp

45

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

Page 46: Laboratoare Pp

46

// 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

Page 47: Laboratoare Pp

47

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;

Page 48: Laboratoare Pp

48

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

Page 49: Laboratoare Pp

49

__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;

Page 50: Laboratoare Pp

50

• 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.

Page 51: Laboratoare Pp

51

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:

Page 52: Laboratoare Pp

52

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;

Page 53: Laboratoare Pp

53

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

Page 54: Laboratoare Pp

54

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

Page 55: Laboratoare Pp

55

}

// 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

Page 56: Laboratoare Pp

56

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.

Page 57: Laboratoare Pp

57

- 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

Page 58: Laboratoare Pp

58

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>

Page 59: Laboratoare Pp

59

#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;

}

Page 60: Laboratoare Pp

60

//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.

Page 61: Laboratoare Pp

61

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

}

Page 62: Laboratoare Pp

62

}

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.