Familiarizarea cu arhitectura CELL BE

Obiective

În cadrul acestui laborator vom începe lucrul pe arhitectura Cell Broadband Engine. Vom programa pe această arhitectura folosind limbajele C/C++ și SDK-urile pentru Cell. Veți învăța cum să scrieți un program care să se execute pe toate unitățile de calcul și cum să folosiți tipurile de date vectoriale suportate de Cell BE.

Arhitectura Cell Broadband Engine

Arhitectura Cell BE a fost dezvoltată de către un consorțiu format de Sony, Toshiba și IBM și lansat comercial în 2005, o dată cu apariția Sony Playstation 3. Caracteristicile acestei arhitecturi au facut ca utilizarea sa să nu se limiteze doar la consola PS3, ducând la folosirea sa în domeniul High Performance Computing. Un exemplu ar fi cel mai puternic supercomputer din 2008, IBM Roadrunner, care conținea 12,960 de procesoare IBM PowerXCell 8iCell.

Arhitectura procesorului Cell este concepută special pentru a oferi o putere de calcul mare (pentru vremea respectivă), putând prelucra datele în mod SIMD, folosind instrucțiuni și tipuri de date vectoriale. Un Cell conține un Power Processor Element (PPE) și opt Synergistic Processor Elements (SPE) interconectate prin magistrala Element Interconnect Bus (EIB) ca în figure 1. Procesorul PPE și SPE-urile comunică între ele, cu spațiul principal de stocare (main storage) și cu elementele I/O prin intermediul magistralei EIB.

Fig. 1: Diagrama bloc a arhitecturii Cell Broadband Engine

În acest laborator vom rezuma doar caracteristicile principale ale arhitecturii. Mai multe detalii puteți găsi pe pagina Arhitecturii Cell BE, dar și în link-urile de la referințe.

Componente

Magistrala EIB, prin care comunică PPE-ul și SPE-urile, are o structură bazată pe 4 inele (două în sens orar și două în sens anti-orar) pentru transferul datelor între componente. 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).

Power Processor Element conține un procesor de uz general pe 64 de biți bazat pe arhitectura Power, versiunea 2.02. Pe acesta se pot instala sisteme de operare cum ar fi Linux, și de pe el se pornesc aplicațiile care ajung să se execute pe SPE-uri. Setului său de instrucțiuni de tip RISC i-au fost adăugate instrucțiuni pentru calcul vectorial și un set de 32 registre de 128 biți. Instrucțiunile vectoriale sunt executate de către o unitate SIMD, iar celelalte instrucțiuni de către unitatea de execuție pentru numere întregi, în virgula fixă, sau de către cea pentru numere în virgula mobilă.

PPE-ul este format din Power Porcessor Unit (PPU) și din Power Processor Storage Subsystem (PPSS). Power Processor Storage Subsystem se ocupă cu cererile de acces la memorie venite din partea PPE sau din partea altor procesoare și dispozitive I/O. Acesta conține un cache de nivel 2 (L2) de 512 KB, o serie de cozi (queues) și o unitate de interfață cu magistrala (arbitru de magistrală).

O versiune modificată a PPE-ului este folosită și în procesoarele Xenon din Xbox 360.

Synergistic Processor Element conține un procesor RISC pe 128 biți cu un set de instrucțiuni vectoriale, numit Synergistic Processor Unit (SPU) și un Memory Flow Controller. Acest tip de arhitectură este potrivită aplicațiilor ce necesită calcul intens asupra unor seturi multiple de date (stream-uri video, prelucrare imagini etc). SPU-urile sunt procesoare simple fără cache, cu o memorie locală (Local Store) pentru instruțiuni și date (256 KB SRAM). Ca structură, procesorul folosește un banc de 128 registre de 128 biți, o unitate de execuție în virgulă mobilă și două unități în virgulă fixă. Aceste unități execută numai instrucțiuni vectoriale (SIMD).

SPU-ul execută instrucțiunile din Local Store și lucrează cu datele din Local Store. Acesta nu poate accesa direct memoria principală sau Local Store-urile altor SPE-uri. Pentru a permite transferul de date între PPE și SPE, sau între SPE-uri, se folosesc mecanisme precum transfer DMA și mailboxes. Despre aceste transferuri vom oferi mai multe detalii în laboratoarele următoare. Componenta Memory Flow Controller a SPE-ului este cea care controlează transferurile de date, fiind conectată la magistrala EIB.

Pentru un SPE, vom folosi termenul de Main Storage pentru a ne referi la memoria principală (accesată direct doar de PPU), la Local Store-urile altor SPE-uri și la registrele mapate în memorie (MMIO).

 Fig. 2: Componentele principale ale unui PPE și ale unui SPE

PPE-ul:

  • procesor de uz general
  • pe el se lansează în execuție programul
  • suporta și instrucțiuni vectoriale

SPE-ul:

  • execută numai instrucțiuni vectoriale
  • lucrează cu datele și execută instrucțiunile din memoria sa locală, Local Store
  • nu puteți executa direct programe pe SPE, acestea sunt controlate din programul de pe PPE.

Structura unei aplicații

Cell BE oferă un procesor de uz general cu arhitectura PowerPC (PPE) și 8 procesoare specializate, cu arhitectura SIMD (SPE). Ca mod de lucru, o aplicație care folosește atât PPE-ul cât și SPE-urile:

  • conține un program PPE - este executat pe PPE și încarcă și pornește execuția programelor SPE
  • conține unul sau mai multe programe SPE - programele care sunt executate de către SPU-uri
  • folosește un API pentru C/C++, cu extensii pentru arhitectura Cell
  • folosește versiune diferite ale compilatorului GCC pentru programele PPE și SPE

Într-un sistem care rulează Linux, thread-ul inițial al unui program este un thread Linux care rulează pe PPE. Acest thread poate crea unul sau mai multe task-uri Linux pentru Cell Broadband Engine.

Un task Linux pentru Cell Broadband Engine are unul sau mai multe thread-uri Linux asociate cu acesta, care rulează fie pe PPE (thread-uri PPE), fie pe SPE (thread-uri SPE), în funcție de codul executabil încărcat. Un thread SPE este un thread de Linux care rulează pe SPE. Acesta are asociat un context care include starea celor 128 de registre, contorul program si cozile de comenzi pentru MFC.

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-uri, independent de thread-urile 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.

Pentru debugging, toolchain-ul GNU pentru Cell oferă și o versiune de GDB ce poate fi folosită atât pentru programele PPU cât și SPU. Pe acest wiki găsiți un tutorial pentru folosirea acestuia.

Programele SPE-urilor nu pot fi lansate direct în execuție. Ele pot fi pornite numai din codul programului PPE.

Pentru a compila programele pentru PPE și SPE se folosesc compilatoare diferite.

Scheletul de laborator oferă un exemplu de structurare a fișierelor sursă ale aplicației voastre. Recomandăm folosirea de directoare separate, unul pentru codul și makefile-ul PPU-ului, și altul pentru programele SPU și makefile-ul acestora.

Programul PPE. Contexte SPE

În cazul aplicațiilor care sunt compuse din cod care ruleaza pe PPE și cod pentru SPE-uri, programele PPE sunt cele care inițiază și controlează execuția programului(programelor) pentru SPE-uri. API-ul pentru Cell BE pune la dispoziție structuri de date și funcții pentru lucrul cu contextele SPE. Practic, programele SPE sunt executate în cadrul unor threaduri SPE care rulează într-un context, adică au propriile registre SPU, propriul contor program (PC) și cozi MFC. Din cadrul unui context, thread-ul unui SPE poate comunica cu alte SPE-uri, cu PPE-ul și cu memoria. De asemenea, este recomandat ca programele/threadurile executate pe SPU-uri să se axeze pe folosirea capabilităților de calcul vectorial, și să nu conțină foarte multe branch-uri (SPU-ul nu are branch predictor).

figure 3 prezintă pașii care sunt efectuați într-un program PPE pentru a executa contexte SPE. În thread-ul principal al programului său PPE-ul utilizează thread-uri POSIX, pthreads, pentru a controla contextele SPE.

  1. PPU-ul creează thread-uri folosind pthread_create
  2. PPU-ul creează contexte folosind spe_context_create
  3. Programul SPE care trebuie să ruleze într-un context este dat folosind spe_program_load
  4. Contextele se pornesc folosind spe_context_run
  5. Se așteaptă execuția thread-urilor folosind pthread_join
  6. După terminarea execuției contextelor, deci după terminarea thread-urilor, se pot distruge contextele folosind spe_context_destroy

În programul PPE se folosesc variabile de tip spe_context_ptr_t pentru contextul SPE și pthread_t pentru thread-urile PPE-ului.

Fig. 3: Rularea programelor SPE

De ce este nevoie de thread-uri pe PPE? pentru că funcția ce pornește contextele, spe_context_run, este blocantă!

Ce se întamplă după spe_context_run? Se transferă controlul sistemului de operare, care cere planificarea efectiva a contextului pe un SPE fizic din sistem. Pe partea de planificare se urmează un model M:N, în care M threaduri SPE sunt distribuite pe N SPU-uri, putând de asemenea fi preemptate (însă la intervale mai mari decât threadurile PPU). SPU-urile au o memorie locală, Local Store, cu care lucrează direct, ele execută instrucțiuni și folosesc date doar din aceasta. Prin urmare, planificarea contextului presupune și aducerea codul programului în Local Store-ul SPU-ului.

Variante recomandate pentru declararea și folosirea contextelor:

  • În funcția fiecărui thread se creează, se încărcă, se pornește și se distruge un context.
  • În main sau în altă funcție se creează, se încarcă și apoi se dau ca parametru la crearea thread-ulurilor. Fiecare thread va da drumul execuției unui context.

În exemplul următor codul PPU pornește pe un singur SPU un program care afișează id-ul SPE-ului. Versiunea completă a codului și fișierele Makefile corespunzătoare le găsiți în scheletul acestui laborator.

void *ppu_pthread_function(void *thread_arg) {
    spe_context_ptr_t ctx;
    thread_arg_t *arg = (thread_arg_t *) thread_arg;
 
    /* Create SPE context */
    if ((ctx = spe_context_create (0, NULL)) == NULL) {
	perror ("Failed creating context");
	exit (1);
    }
 
    /* Load SPE program into context */
    if (spe_program_load (ctx, &lab7_spu)) {
        perror ("Failed loading program");
        exit (1);
    }
 
    /* Run SPE context */
    unsigned int entry = SPE_DEFAULT_ENTRY;
    if (spe_context_run(ctx, &entry, 0, NULL, NULL, NULL) < 0) { 
        perror ("Failed running context");
        exit (1);
    }
 
    /* Destroy context */
    if (spe_context_destroy (ctx) != 0) {
        perror("Failed destroying context");
        exit (1);
    }
    pthread_exit(NULL);
}
 
int main()
{
    pthread_t thread;
    if (pthread_create (&thread, NULL, &ppu_pthread_function, NULL))  {
        perror ("Failed creating thread");
        exit (1);
    }
 
    /* Wait for SPU-thread to complete execution.  */
    if (pthread_join (thread, NULL)) {
        perror("Failed pthread_join");
        exit (1);
    }
}

Programul pentru SPE:

int main(unsigned long long speid, unsigned long long argp, unsigned long long envp) {
	printf("Hello World! from Cell with id (0x%llx)\n", speid); 
	return 0;
}

Programele PPU și SPU se compilează pe mașina Cell, pentru execuție trimiteți de pe fep, cu qsub, job-ul pe coada ibm-cell-qs22.q.

API contexte

Pentru a utiliza funcțiile pentru controlul contextelor trebuie inclusă biblioteca libspe2:

#include <libspe2.h>

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.

spe_context_ptr_t spe_context_create(unsigned int flags, spe_gang_context_ptr_t gang)
  • flags - rezultatul aplicarii operatorului OR pe biti pe diverse valori (modificatori) ce se aplica la crearea contextului. Valori acceptate:
    • 0 - nu se aplică nici un modificator.
    • SPE_EVENTS_ENABLE - configurează contextul pentru a permite lucrul cu evenimente (!Foarte important pentru mailboxes; laboratorul 9)
    • SPE_CFG_SIGNOTIFY1_OR - configurează registrul 1 de SPU Signal Notification pentru a fi în modul OR; default e in mod Overwrite (OR între noul semnal primit și cel deja existent, în loc de o suprascriere)
    • SPE_CFG_SIGNOTIFY2_OR - analog SPE_CFG_SIGNOTIFY1_OR, pentru registrul 2 de SPU Signal Notification
    • SPE_MAP_PS - pentru cerere permisiune pentru acces mapat la memoria “problem state area” (notata prescurtat PS) a threadului corespunzator SPE-ului. PS contine flagurile de stare pentru SPEuri și în mod default nu poate fi accesată decat SPE-ul propriu, iar din exterior doar prin cereri DMA. Dacă 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ă e NULL, noul context SPE nu va fi asociat vreunui grup.
int spe_program_load(spe_context_ptr spe, spe_program_handle_t *program)
  • spe - contextul SPE în care se va încarca executabilul
  • program - o adresă validă la un program mapat pe un SPE. Poate fi declarat în cod ca extern spe_program_handle_t simple_spu, unde simple_spu era numele executabilului pentru SPE.
int spe_context_destroy (spe_context_ptr_t spe)
  • spe - contextul SPE care va fi distrus.
  • întoarce 0 in caz de succes, -1 în caz de eroare.

Trimiterea unor parametri de inițializare către SPE

Programul PPE-ului poate comunica cu programele SPE-urilor prin modalitățile studiate în laboratorul 8 (transfer DMA) si laboratorul 9 (mailbox), însă dacă este nevoie să trimită doar 1-2 parametri la inițializare, poate face acest lucru la pornirea contextului.

Funcția de pornire a execuției contextului este:

#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)
  • 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 programul execuția. Dacă această valoare e SPE_DEFAULT_ENTRY, punctul de intrare va fi obținut din imaginea de context SPE incarcată.
  • runflags - diferite flaguri (cu OR pe biți intre ele) care specifică o anumită comportare în cazul rulării contextului SPE:
    • 0 - default, nici un flag.
    • SPE_RUN_USER_REGS - registrele de setup r3, r4 si r5 din SPE vor fi inițializate cu 48 octeți (16 pe fiecare din cei 3 regiștri) specificați de pointerul argp.
    • SPE_NO_CALLBACKS - SPE library callbacks pentru registre 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 structură de tip spe_stop_info_t. Aceasta structură conține informații despre modul în care s-a terminat execuția SPE-ului.

Parametrii argp și envp sunt transferați catre programul SPE. Funcția main a programului SPE primește trei parametri:

int main(unsigned long long speid, unsigned long long argp, unsigned long long envp)
  • speid - identificatorul thread-ului SPE
  • argp (optional) - date primite de la PPE (argp-ul din spe_context_run)
  • envp (optional) - date primite de la PPE (envp-ul din spe_context_run)
Comunicare PPU - SPU-uri prin parametrii functiei main

Exemplu

...in programul PPE...
int i = 10;
if (spe_context_run(args->spe, &entry, 0, (void*)i, NULL, NULL) < 0) 
    ....
 
...in programul SPE...
 
int main(unsigned long long speid, unsigned long long argp, unsigned long long envp)
{
    printf("Am primit %d\n", (int) argp);
}

Tipuri de date

PPU-ul și SPU-ul lucrează atât cu date scalare cât și cu date vector, însă tipurile de date și instrucțiunile SIMD sunt diferite.

Tipuri de date PPU

PPE-ul (PowerPC Processor Element) conține un procesor cu arhitectura PowerPC și cu o extensie de instrucțiuni pentru calcul vectorial, Vector/SIMD Multimedia Extension. Astfel, pe lângă tipurile de date cu care lucrează instrucțiunile PowerPC, procesorul suportă și tipuri de date vector folosite de instrucțiunile SIMD. Într-un program PPU se pot amesteca instrucțiuni scalare cu instrucțiuni SIMD.

Din punct de vedere al arhitecturii, ca să suporte toate aceste instrucțiuni, procesorul conține:

  • 3 unități de calcul care funcționează concurent, una pentru operații în virgulă fixă, una pentru operații în virgulă mobilă și una pentru operații vectoriale
  • registre pentru uz general (32 x 64 biți), pentru operații în virgulă mobilă (32 x 64 biți) și registre pentru instrucțiuni vectoriale (32 x 128 biți). În registrele pentru instrucțiuni vectoriale se pun date care au fost declarate de tip vector.

Datele de tip vector sunt utilizate de către operațiile SIMD, și au următoarele proprietăți:

  • sunt aliniate in memorie la 16 Bytes
  • din date de tip scalar se poate face cast la un tip de date vector
  • dintr-un tip de date vector se poate face cast la un alt tip de date vector
Tipul de date Descriere Valori
vector unsigned char 16 x valori fara semn pe 8 biți 0 … 255
vector signed char 16 x valori fara semn pe 8 biți -128 … 127
vector bool char 16 x valori boolene pe 8 biți 0 (false), 255 (true)
vector unsigned short 8 x valori fara semn pe 16 biți 0 … 65535
vector unsigned short int 8 x valori fara semn pe 16 biți 0 … 65535
vector signed short 8 x valori cu semn pe 16 biți -32768 … 32767
vector signed short int 8 x valori cu semn pe 16 biți -32768 … 32767
vector bool short 8 x valori boolene pe 16 biți 0 (false), 65535 (true)
vector bool short int 8 x valori boolene pe 16 biți 0 (false), 65535 (true)
vector unsigned int 4 x valori fara semn pe 32 biți 0 … 232 - 1
vector signed int 4 x valori cu semn pe 32 biți -232 … 232 - 1
vector bool int 4 x valori fara semn pe 32 biți 0 (false), 232 - 1 (true)
vector float 4 x 32-biti single precision IEEE-754 values
vector pixel 8 x valori cu semn pe 16 biți 1/5/5/5 pixel

PPE-ul suportă doar modul big-endian pentru ordonarea octeților tuturor tipurilor de date, ceea ce înseamnă că byte-ul cel mai semnificativ este byte-ul 0, cum este ilustrat și în imaginea următoare, pentru date de tip vector:

 MSB

Tipuri de date SPU

SPU-ul, procesorul din SPE, are o arhitectură SIMD, iar instrucțiunile și tipurile de date sunt diferite de cele ale PPU-ului. Majoritatea instrucțiunilor lucrează cu operanzi de tip vector, însă unele suportă și operanzi scalari. SPU-ul lucrează cu 128 de registre a 128 de biți.

Tipurile de date scalare pentru SPU sunt:

  • Byte 8 biți
  • Halfword 16 biți
  • Word 32 biți
  • Doubleword 64 biți
  • Quadword 128 biți

SPU-ul suportă o parte din tipurile de date vector suportate și de PPU, la care se adaugă cateva tipuri corespunzătoare unor valori de mai mult de 32 de biți (în limita a 128 biți, e.g. vector double care are două valori pe 64 de biți).

Tipul de date Descriere
vector unsigned char 16 x valori fara semn pe 8 biți
vector signed char 16 x valori fara semn pe 8 biți
vector unsigned short 8 x valori fara semn pe 16 biți
vector signed short 8 x valori cu semn pe 16 biți
vector unsigned int 4 x valori fara semn pe 32 biți
vector signed int 4 x valori cu semn pe 32 biți
vector unsigned long long 2 x 64-bit unsigned doublewords
vector signed long long 2 x 64-bit signed doublewords
vector float 4 x 32-bit single-precision floating-point numbers
vector double 2 x 64-bit double-precision floating-point numbers
qword quadword (16-byte)

Atunci când pe SPU se utilizează scalari, aceștia sunt stocați în registre conform unei poziții indicate de către “preferred scalar spot”, așa cum este ilustrat în figura următoare:

 Preferred scalar spot pentru valori scalare

Alinierea datelor

Pentru a crea variabile vector sau array-uri de variabile vector din array-uri de scalari, trebuie ținut cont de alinierea acestora în memorie. SPE și PPE folosesc vectori de 128 de biți pentru instrucțiuni SIMD. Pentru a putea utiliza instrucțiunile SIMD (Single Instruction Multiple Data), arhitectura Cell are nevoie de o anumită poziționare a datelor în memorie, și anume, acestea trebuie să fie aliniate în memorie la o limită de 16 bytes (quadword) sau multiplu de 16 bytes.

Astfel, array-urile create în programul PPE și SPE trebuie să fie aliniate la adrese multiplu de 16 octeți folosind keyword-ul __attribute__((aligned(16))) (în cazul alocării statice)

 int v[16] __attribute__((aligned(16))); 

Ca alinierea să funcționeze, variabila trebuie să fie globală sau locală funcției, dar statică, deoarece alocarea normală pe stivă se face la runtime.

Pentru date alocate dinamic, folosim funcțiile malloc_align și free_align din libmisc.h (+ biblioteca libmisc.a - trebuie inclus flagul -lmisc) pentru alocare aliniată, astfel:

input = (float*)malloc_align(N*sizeof(float),7); // aliniere la 2^7 = 128 bytes


SPU intrinsics

Atât PPU-ul cât și SPU-ul suportă seturi de instrucțiuni pentru calcule vectoriale (SIMD), iar API-ul de C pentru Cell-BE oferă două seturi de funcții, intrinsics, ca wrappere peste aceste instrucțiuni. În cadrul acestui laborator ne axăm pe lucrul cu operațiile vectoriale pe SPE-uri.

Toate funcțiile pentru SPU încep cu spu_(mapare 1:n cu instr assembler) sau si_(mapare 1:1 cu instrucțiuni assembler), în timp ce cele pentru PPU încep cu vec_. Pentru a folosi funcțiile SPU-ului trebuie inclusă biblioteca spu_intrinsics.

#include <spu_intrinsics.h>

Tabelele următoare conțin o parte din funcțiile folosite pentru operații cu date de tip vector. Lista completă acestora o puteți găsi în documentație (secțiunea B2 - C/C++ Language Extensions (Intrinsics) for SPU Instructions), și include și funcții ce nu țin direct de lucrul cu vectori.

Operații aritmetice, logice și de control:

FuncțieDescriere
spu_add(a, b)a+b, a și b de tip vector
spu_sub(a, b)a-b, a și b de tip vector
spu_mul(a, b)a*b, a și b de tip vector float/double
spu_madd(a, b, c)a*b + c, a, b, c de tip vector
spu_nmadd(a, b, c)-a*b - c, a, b, c de tip vector
spu_msub(a, b, c)a*b - c, a și b de tip vector
spu_nmsub(a, b, c)-(a*b - c), a și b de tip vector
spu_avg(a, b)byte average(a,b), a și b de tip vector
spu_and(a, b), spu_nand(a,b)a and b, not(a and b), a și b de tip vector
spu_or(a, b), spu_nor(a,b)a or b, not(a or b) , a și b de tip vector
spu_xor(a, b) a xor b, a și b de tip vector
spu_cmpeq(a, b)compara a==b, a și b de tip vector
spu_cmpgt(a, b)compara a>b, a și b de tip vector

Rezultatele intoarse de aceste funcții sunt tot variabile de tip vector. Nu toate tipurile de vector pot fi folosite pentru toate operațiile, verificați documentația înainte de a le folosi.

În locul apelării funcțiilor, se pot folosi și operatorii +,-,*,/.

Există funcții pentru lucru cu scalari și vectori, dar și funcții ce operează pe elementele dintr-o variabila vector.

FuncțieDescriere
d = spu_splats(a)se replică scalarul a într-un vector d
d = spu_extract(a, element) sau
d = a[element]
se extrage un element scalar din vector
d = spu_insert(a, b, element) sau
b[element] = a
se inserează scalarul a pe poziția element în vector b
d = spu_promote(a, element) se transformă scalarul a într-o variabilă vector d, punându-l pe poziția element

Exemple vectorizare

Exemplu de cast de la un array de scalari la un array de vectori:

unsigned int a1[8] = {10, 20, 30, 40, 50, 60, 70, 80};
vector unsigned int *v1 = (vector unsigned int *)a1;

Fig. 4: Cast-ul unui array de scalari la un array de vectori.

Exemplu de vectorizare a unei bucle, în care, datorită faptul că folosim variabile vector, se fac mai puține operații/iterații.

#define N 8
...
 
unsigned int a[N] __attribute__ ((aligned(16)))  = {10, 20, 30, 40, 50, 60, 70, 80};
unsigned int b[N] __attribute__ ((aligned(16)))  = {1, 2, 3, 4, 5, 6, 7, 8};
unsigned int c[N] __attribute__ ((aligned(16)));
....
 
/* lucru cu scalari */
for (i = 0; i < N; i++){
     c[i] = a[i] + b[i];
}
 
/* lucru cu vectori */
vector unsigned int *va = (vector unsigned int *) a;
vector unsigned int *vb = (vector unsigned int *) b;
vector unsigned int *vc = (vector unsigned int *) c;
int n = N/(16/sizeof(unsigned int));
for (i = 0; i < n; i++){     // N/4 iterații
     vc[i] = va[i] + vb[i];  // putea fi folosit si vc[i] = spu_add(va[i], vb[i])
}
 
int i;
for (i=0; i<N; i+=4)
     printf("%d %d %d %d ", c[i], c[i+1], c[i+2], c[i+3]);

:!: Dacă în exemplul anterior dimensiunea vectorilor nu era multiplu de 4, atunci restul elementelor trebuia înmulțite ca scalari sau făcut 'padding' la vectori.

Exerciții

Primele două exerciții se vor testa local (compilați cu flag-ul -lpthread), restul pe Cell.

Scheletul de laborator este pentru exercițiile de Cell. Scheletul conține atât cod pentru PPE cât și pentru SPE-uri, cât și fișiere Makefile pentru compilare. Pentru a le compila pe toate, folosiți Makefile-ul din rădăcina arhivei. Compilarea și execuția programelor se face pe sistemele Cell.

În cadrul laboratoarelor și temelor vom folosi sistemele Cell BE puse la dispoziție în cluster, conform tutorialului de pe wiki: Rulare programe Cell in Cluster

  1. (2p) Folosind phtreads scrieți un program in C ce afișează “Hello World!” din fiecare thread. Creați 10 astfel de threaduri.
  2. (1p) Modificați programul anterior pentru a permite transferul de parametri catre thread. Definiți structura thread_param cu câmpul index de tip int. Trimiteți fiecarui thread indexul său folosind această structură. Metoda thread-ului va avea ca parametru aceasta structura.
  3. (3p) Scheletul de cod conține un program ppu și un program spu. Programul lab7_ppu porneste un context SPU, iar programul lab7_spu afișează id-ul acestuia. Modificați codul PPU-ului astfel încât să pornească același program pe toate SPU-urile.
    • Hint: puteți afla numărul de SPE-uri folosind: spe_cpu_info_get(SPE_COUNT_USABLE_SPES, -1);
  4. (2p) Modificați codul de la exercițiul anterior astfel încât să transmiteți fiecărui SPE un index. Programul SPE-ului va afișa indexul primit.
  5. (2p) Lucrul cu tipuri vector și operațiile vectoriale: în programul pentru SPE din scheletul de cod aveți declarațe 3 array-uri, A, B și C. Convertiți aceste array-uri la tipul vector și efectuați următoarele operații pe acestea:
    • a) inmultiti valorile array-ul B cu 20.0 si adunați valorile corespunzătoare din A folosind SPU intrinsics.
    • b) comparați array-ul rezultat cu array-ul C și afișați care elemente sunt egale si care nu.
    • Hint: vedeți exemplul de transformare dintr-un array de scalari într-un array de vectori din subsecțiunea Exemple Vectorizare
    • Hint: pentru operația de comparare folosiți o funcție descrisă în tabelul din secțiunea SPU Intrinsics. Aceste funcții compara element cu element din variabilele vector.
    • pentru a observa output-ul mai ușor, este suficient să rulați exercițiul acesta doar pe 1 SPE.

Resurse

Referințe

asc/lab7/index.txt · Last modified: 2017/04/07 05:33 by radu_petru.daia
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0