Programarea CELL BE folosind Eclipse

Scopul acestui laborator este familiarizarea cu mediul de programare IDE pentru arhitectura Cell/B.E. și conține:

  • un tutorial pentru lucrul în mediul Cell - IDE Eclipse,
  • noțiuni despre crearea threadurilor și contextelor pentru SPE-uri de catre PPE
  • un exemplu aplicat despre lucrul cu tipul vector in Cell/B.E.

La finalul acestui laborator veți avea de scris, compilat si executat un program pentru înmulțirea șirurilor de numere complexe.

Tutorial Cell - Eclipse

Crearea in PPE a threadurilor corespunzatoare SPE-urilor

În laboratorul trecut a fost prezentat un exemplu simplu de program în care se executau threaduri pen SPE-uri. Acum vor fi explicate pe scurt funcțiile folosite în acel program și parametrii acestora.

Pentru a porni SPE-urile din PPE, în programul PPE-ului se realizează următorii pași:

  1. Crearea unui context SPE.
  2. Încărcarea unui obiect executabil pe SPE in 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. 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).
  4. Distrugerea contextului SPE.

După cum s-a observat și în laboratorul trecut, într-un SPE se poate executa un singur thread la un moment dat, fiecare thread având propriul context - registrii, contor program, LS (local store), cozi ale controlului de memorie. 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.

Crearea unui context SPE

- spe_context_create este functia care creaza si initializeaza un context pentru un thread SPE care contine informatie persistenta despre un SPE logic. Functia intoarce un pointer spre noul context creat, sau NULL in 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 aplicarii operatorului OR pe biti pe diverse valori (modificatori) ce se aplica la crearea contextului. Valori acceptate:
    • 0 - nu se aplica nici un modificator.
    • SPE_EVENTS_ENABLE - configureaza contextul pentru a permite lucrul cu evenimente (!Foarte important pentru mailboxes; laboratorul urmator)
    • SPE_CFG_SIGNOTIFY1_OR - configureaza registrul 1 de SPU Signal Notification pentru a fi in modul OR; default e in mod Overwrite (cu alte cuvinte, se va face o operatie logica OR intre noul semnal primit si cel deja existent, si nu 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 si in mod default nu poate fi accesata decat SPE-ul propriu, iar din exterior doar prin cereri DMA. Daca acest flag e setat, se specifica la crearea contextului ca PPE vrea acces la memoria PS a respectivului SPE.
  • gang - Asociaza noul context SPE cu un grup(gang) de contexte. Daca valoarea pentru gang e NULL, noul context SPE nu va fi asociat vreunui grup.

Incarcarea unui executabil in Local Store-ul contextului SPE creat

Se realizeaza folosind functia cu urmatorul antet:

  1. int spe_program_load(spe_context_ptr spe, spe_program_handle_t *program)
  • spe - un pointer valid al unui context SPE (intors de spe_context_create) in care se va incarca executabilul (programul specificat de urmatorul argument)
  • program - o adresa valida la un program mapat pe un SPE. In exemplul prezentat in laboratorul trecut, acesta era declarat ca extern spe_program_handle_t simple_spu, unde simple_spu era numele executabilului pentru SPU(SPE).

Rularea contextului SPE

Pentru a executa un context SPE se utilizează funcția spe_context_run, cu urmatorul antet:

  1. #include <libspe2.h>
  2.  
  3. int spe_context_run(spe_context_ptr_t spe, unsigned int *entry, unsigned int runflags,
  4.  
  5. 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 biti intre ele) care specifică o anumită comportare în cazul rulării contextului SPE:
    • 0 - default, nici un flag.
    • SPE_RUN_USER_REGS - regiștrii de setup r3, r4 si r5 din SPE vor fi inițializați cu 48 octeți (16 pe fiecare din cei 3 regiștrii) specificați de pointerul argp.
    • 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 (vezi figura).
  • envp - Un pointer (opțional) la date specifice environmentului. Este pasat SPE-ului ca al treilea argument din main (vezi figura 17).
  • stopinfo - Un pointer (opțional) la o structura de tip spe_stop_info_t (aceasta structură conține informații despre modul în care s-a terminat execuția SPE-ului)
Fig. 17. Comunicare PPU - SPU-uri prin parametrii functiei main

Distrugerea contextului SPE

Se face folosind functia cu urmatorul antet:

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

Functia intoarce 0 in caz de succes, -1 in caz de eroare.

  • spe - Pointer spre contextul SPE care va fi distrus.

Lucrul cu tipul vector in Cell/B.E.

Notiuni preliminare

Un compilator care transforma automat scalari in structuri SIMD impachetate paralel este un compilator cu auto-vectorizare. Asemenea compilatoare trebuie sa manevreze toate constructiile unui limbaj de nivel inalt si din aceasta cauza rezultatul nu il constituie intotdeauna un cod optim.

O alta varianta, folosita in Cell, este ca vectorizarea sa se faca inca de la scrierea codului.

O prezentare a tipurilor de date vector a fost facuta in laboratorul 6. Mai jos este prezentat un tabel cu functiile ce pot lucra cu aceste tipuri de date.

SPU Intrinsic Vector/SIMD Multimedia Extension Intrinsic For Data Types
spu_addvec_addvector operands only, no scalar operands
spu_andvec_andvector operands only, no scalar operands
spu_andcvec_andcall
spu_avgvec_avgall
spu_cmpeqvec_cmpeqvector operands only, no scalar operands
spu_cmpgtvec_cmpgtvector operands only, no scalar operands
spu_convtfvec_ctflimited scale range (5 bits)
spu_convtsvec_ctslimited scale range (5 bits)
spu_convtuvec_ctulimited scale range (5 bits)
spu_extractvec_extractall
spu_gencvec_addcall
spu_insertvec_insertall
spu_maddvec_maddfloat only
spu_mulhhvec_muleall
spu_muovec_mulohalfword vector operands only, no scalar operands
spu_nmsubvec_nmsubfloat only
spu_norvec_norall
spu_orvec_orvector operands only, no scalar operands
spu_promotevec_promoteall
spu_revec_reall
spu_rlvec_rlvector operands only, no scalar operands
spu_rsqrtevec_rsqrteall
spu_selvec_selall
spu_splatsvec_splatsall
spu_subvec_subvector operands only, no scalar operands
spu_genbvec_genblvector operands only, no scalar operands
spu_xorvec_xorvector operands only, no scalar operands

Tabelul 1. Intrisincs SPU cu mapare unu-la-unu pe Vector/SIMD Multimedia Extension

Cateva dintre aceste functii pentru vectori insotite de explicatii:

  • vec = spu_splats(scal) - replica un scalar in fiecare element al unui vector ex: vec1111 = spu_splats((float)1)
  • vec_float = spu_convtf(vec_int, scale) - converteste un vector de int intr-un vector de float
  • vec = spu_add(vec_a, vec_b) - adunare de vectori element cu element
  • vec = spu_sub(vec_a, vec_b) - scadere de vectori element cu element
  • vec = spu_mul(vec_a, vec_b) - inmultire de vectori element cu element (produs scalar)
  • vec = spu_madd(vec_a, vec_b, vec_c) - multiply (vec_a cu vec_b) si add (produsul se aduna 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 in loc de add
  • vec = spu_nmsub(vec_a, vec_b, vec_c) - analog nmadd, dar cu sub in loc de add
  • vec = spu_shuffle(vec_a, vec_b, vec_perm) - vec este rezultatul unui amestec (shuffle) controlat intre vec_a si vec_b; vec_perm specifica ce octeti din vec_a si din vec_b se vor afla in vectorul rezultat vec.

Tipuri de date de tip vector:

  • vector [unsigned] {char, short, int, float, double} ex: “vector float”, “vector signed short”, “vector unsigned int”, …
    • Numarul de elemente din fiecare astfel de vector depinde de tipul elementelor. Trebuie tinut cont ca indiferent de tip, un vector are 128 biti. El contine astfel 4 * int, 4 * float, 8 * short, 16 * char …
    • Se poate face cast intre diferite tipuri vector
    • Vectorii sunt aliniati la stanga in blocuri de dimensiunea quadword (16 octeti)

Pointeri la vectori :

  • Ex: “vector float *p”
  • p+1 e pointer spre urmatorul vector (16B) dupa vectorul la care refera p
  • Se poate face cast din pointeri la scalari si din pointeri la tipuri vector

Vectorizarea unei bucle

In continuare este prezentat un exemplu simplu de inmultire a doi vectori, element cu element. Programele (functia de inmultire si main-ul) sunt prezentate in varianta nevectorizata, in varianta vectoriala cand dimensiunile vectorilor sunt divizibile cu 4 (tipurile vector sunt pe 128 biti, deci contin 4 elemente pe 32 biti - in cazul nostru float) si in varianta vectoriala cand dimensiunile vectorilor nu sunt divizibile cu 4.

a) Varianta nevectorizata:

mult1.c
  1. /* mult1.c */
  2. #include <stdio.h>
  3.  
  4. int mult1(float *in1, float *in2, float *out, int N){
  5. int i;
  6. for (i=0;i<N;i++){
  7. out[i] = in1[i] * in2[i];
  8. }
  9. return 0;
  10. }
main.c
  1. /* main.c */
  2. #include <stdio.h>
  3. #define N 16
  4.  
  5. int mult1(float *in1, float *in2, float *out, int num);
  6. float a[N] = { 1.1, 2.2, 4.4, 5.5,
  7. 6.6, 7.7, 8.8, 9.9,
  8. 2.2, 3.3, 3.3, 2.2,
  9. 5.5, 6.6, 6.6, 5.5};
  10.  
  11. float b[N] = { 1.1, 2.2, 4.4, 5.5,
  12. 5.5, 6.6, 6.6, 5.5,
  13. 2.2, 3.3, 3.3, 2.2,
  14. 6.6, 7.7, 8.8, 9.9};
  15.  
  16. float c[N];
  17.  
  18. int main(){
  19. int num = N;
  20. int i;
  21. mult1(a, b, c, num);
  22.  
  23. for (i=0;i<N;i+=4)
  24. printf("%.2f %.2f %.2f %.2f\n", c[i], c[i+1], c[i+2], c[i+3]);
  25. return 0;
  26. }
Makefile
  1. # Makefile
  2. # daca nu aveti CELL_TOP definit ca variabila globala, schimbati al doilea rand cu: include /opt/cell/sdk/buildutils/make.footer
  3. PROGRAM_ppu = example1
  4. include $(CELL_TOP)/buildutils/make.footer

b) Varianta vectoriala in care dimensiunea vectorilor initiali e multiplu de 4.

In functia de inmultire (mult1) vectorii de tip float se convertesc la vectori de vector float si se micsoreasa numarul de pasi din bucla (de 4 ori). Pentru inmultirea a doua variabile de tip vector (element cu element), se utilizeaza functia spu_mul(), din spu_intrinsics. Atentie, aici elementele c[i], a[i] si b[i] sunt vectori ce contin fiecare cate 4 float-uri:

mult1.c
  1. /* mult1.c */
  2. #include <stdio.h>
  3. #include <spu_intrinsics.h>
  4.  
  5. int mult1(float *in1, float *in2, float *out, int N){
  6. int i;
  7. vector float *a = (vector float *) in1;
  8. vector float *b = (vector float *) in2;
  9. vector float *c = (vector float *) out;
  10.  
  11. int Nv = N >> 2; // N/4 -> fiecare vector float are 128 bytes =
  12. // 4 * float pe 32 bytes
  13. for (i=0;i<Nv;i++){
  14. c[i] = spu_mul(a[i], b[i]);
  15.  
  16. }
  17. return 0;
  18. }

In main se aliniaza vectorii in memorie la limite de quadword (128 biti = 4 cuvinte pe 32 biti ):

main.c
  1. /* main.c */
  2. #include <stdio.h>
  3. #define N 16
  4.  
  5. int mult1(float *in1, float *in2, float *out, int num);
  6. float a[N] __attribute__ ((aligned(16))) = { 1.1, 2.2, 4.4, 5.5,
  7. 6.6, 7.7, 8.8, 9.9,
  8. 2.2, 3.3, 3.3, 2.2,
  9. 5.5, 6.6, 6.6, 5.5};
  10.  
  11. float b[N] __attribute__ ((aligned(16))) = { 1.1, 2.2, 4.4, 5.5,
  12. 5.5, 6.6, 6.6, 5.5,
  13. 2.2, 3.3, 3.3, 2.2,
  14. 6.6, 7.7, 8.8, 9.9};
  15.  
  16. float c[N] __attribute__ ((aligned(16)));
  17. int main(){
  18. int num = N;
  19. int i;
  20. mult1(a, b, c, num);
  21. for (i=0;i<N;i+=4)
  22. printf("%.2f %.2f %.2f %.2f\n", c[i], c[i+1], c[i+2], c[i+3]);
  23. return 0;
  24. }

c) Varianta vectoriala in care dimensiunea vectorilor initiali nu e multiplu de 4.

In main singura modificare facuta a fost asupra numarului de elemente din vectori(19), pentru a nu mai fi multiplu de 4. Observati ca valoarea de la aliniere (numarul de biti) ramane tot 16.

In functia de inmultire (mult1) trebuie retinut catul (Nv) dar si restul (j) impartirii dimensiunii N la 4. Astfel, vor fi vectori de Nv elemente de tipul vector float, care se vor inmulti folosind functia spu_mul(), la fel ca la punctul b). Dar vom fi si j elemente (j<4) de tip float, care nu pot compune un vector float, si care vor trebui inmultite in modul traditional:

mult1.c
  1. /* mult1.c */
  2. #include <stdio.h>
  3. #include <spu_intrinsics.h>
  4.  
  5. int mult1(float *in1, float *in2, float *out, int N){
  6. int i;
  7. vector float *a = (vector float *) in1;
  8. vector float *b = (vector float *) in2;
  9. vector float *c = (vector float *) out;
  10. int Nv = N >> 2; // N/4 -> fiecare vector float are 128 biti =
  11.  
  12. // 4 * float pe 32 biti
  13. int j = N % 4;
  14. for (i=0; i<Nv; i++){
  15. c[i] = spu_mul(a[i], b[i]);
  16. }
  17. for (i=N-j; i<N; i++){
  18. out[i] = in1[i] * in2[i];
  19. }
  20. return 0;
  21. }
main.c
  1. /* main.c */
  2. #include <stdio.h>
  3. #define N 19
  4.  
  5. int mult1(float *in1, float *in2, float *out, int num);
  6.  
  7. float a[N] __attribute__ ((aligned(16))) //observati ca aici e tot 16, ca data trecuta
  8. = { 1.1, 2.2, 4.4, 5.5,
  9. 6.6, 7.7, 8.8, 9.9,
  10. 2.2, 3.3, 3.3, 2.2,
  11. 5.5, 6.6, 6.6, 5.5,
  12. 1.1, 2.2, 3.3};
  13.  
  14. float b[N] __attribute__((aligned(16)))
  15. = { 1.1, 2.2, 4.4, 5.5,
  16. 5.5, 6.6, 6.6, 5.5,
  17. 2.2, 3.3, 3.3, 2.2,
  18. 6.6, 7.7, 8.8, 9.9,
  19. 1.1, 2.2, 3.3};
  20.  
  21. float c[N] __attribute__((aligned(16)));
  22.  
  23. int main(){
  24. int num = N;
  25. int i;
  26. mult1(a, b, c, num);
  27. for (i=0;i<N;i+=4)
  28. printf("%.2f %.2f %.2f %.2f\n", c[i], c[i+1], c[i+2], c[i+3]);
  29. return 0;
  30. }

Taskuri

1. Inmultirea sirurilor de numere complexe:

Se dau doua array-uri A si B de N (cu N divizibil cu 4) numere complexe. In fiecare array avem 2N componente de tip float: pentru fiecare numar complex avem partea reala, apoi partea imaginara. Un exemplu de array care tine 4 numere complexe este: {re1, im1, re2, im2, re3, im3, re4, im4}.

Folosind tipuri de date de tip vector si operatii cu aceste tipuri de date, sa se scrie un program care inmulteste fiecare numar complex din A cu numarul complex de pe aceeasi pozitie din B. Se va implementa o functie care rezolva problema cand N = 4 (2N = 8 numere float care intra in doua variabile de tip vector float) si aceasta functie se va apela din main() pentru dimensiuni mai mari ale problemei.

Reminder:

Fie doua numere complexe: z1 = a + i*b si z2 = c + i*d vom avea:
z1 * z2 = (a + ib) * (c + id ) = (ac - bd ) + i (ad + bc)

Se poate optimiza (in sensul ca scapam de o inmultire) calculul lui ad + bc, daca avem deja calculate ac si bd:
(a+b) * (c+d) - ac - bd = ac + ad + bc + bd -ac - bd = ad + bc

Folositi aceasta optimizare in programul vostru.

Hint: pe langa functiile aritmetice pentru tipuri de date de tip vector necesare, se va folosi intens functia: spu_shuffle(vec_a, vec_b, vec_perm)

Exemplu de utilizare a functiei spu_shuffle():

shuffle.c
  1. vector float vec_a = (vector float){1,2,3,4}; // fiecare nr e pe 32 biti = 4 octeti
  2.  
  3. vector float vec_b = (vector float){5,6,7,8};
  4.  
  5. vector float vec1, vec2;
  6.  
  7. //pentru shuffle, se considera ca primul argument contine octetii de la
  8.  
  9. //0 la 15, iar al doilea argument contine octetii de la 16 la 31
  10.  
  11. // vectorul de permutare contine numerele de ordine a 16 octeti din
  12.  
  13. // intervalul 0-31; acesti 16 octeti, in ordinea specificata in
  14.  
  15. // vectorul de permutare, vor fi continuti in vectorul rezultat
  16.  
  17. vector unsigned char vec_perm1 = (vector unsigned char){0,1,2,3,4,5,6,7,16,17,18,19,20,21,22,23};
  18.  
  19. vec1 = spu_shuffle(vec_a, vec_b, vec_perm1)
  20.  
  21. // in urma acestui apel vec1 = {1, 2, 5, 6}
  22.  
  23. vec1 = spu_shuffle(vec_b, vec_a, vec_perm1)
  24.  
  25. //in urma acestui apel vec1 = {5, 6, 1, 2}
  26.  
  27. vector unsigned char vec_perm2 = (vector unsigned char) {0,1,2,3,16,17,18,19,4,5,6,7,20,21,22,23};
  28.  
  29. vec2 = spu_shuffle(vec_a, vec_b, vec_perm2)
  30.  
  31. //in urma acestui apel vec2 = {1, 5, 2, 6}

Linkuri utile

asc/lab7/index.txt · Last modified: 2013/03/26 14:25 by emil.slusanschi
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