Programação Paralela e Hierarquia de Memória Nicolas Maillard Roteiro Qual é o problema? Soluções tradicionais para programação paralela MPI Posix Threads OpenMP Soluções emergentes UPC Fortress, Chapel, Titanium, etc... O problema COMO DESCREVER o PARALELISMO num programa? 1
O que se quer de um modelo de programação? Um modelo de programação paralela Precisa ser expressivo: em geral, complexo. Abrange muitos parâmetros. deve levar a uma implementação do algoritmo. Precisa abstrair os fenômenos para capturar as caraterísticas gerais i.e. ser genérico! Precisa possibilitar desempenho bom. i.e. poder levar em consideração a arquitetura. Qual modelo de hardware? Os modelos de programação paralela... Não existe um modelo universal para programação paralela! Existem vários modelos para vários tipos de máquinas e vários tipos de programas; processos leves memória compartilhada. SMPs, CPUS multicores... Processos comunicantes troca de mensagem Memória distribuída Paralelismo de dados Owner Compute Rule Single Program Multiple Data Paralelismo de laços Influência de trabalhos em Compiladores Dificuldade para extração automática do paralelismo Paralelismo funcional / Divisão e Conquista paralela Templates para programação paralela Os resultados mais importantes foram obtidos até agora com modelos de memória compartilhada. Limitados em escalabilidade! Modelos de máquina Desconsiderar as comunicações (PRAM) Funciona para máquinas com memória compartilhada Funciona para processadores Multicore. Considerar uma máquina estática e homogênea, com rede perfeita (e.g. latência = 0) Considerar uma máquina estática homogênea, com rede que sofre de latência/vazão/contenção (LogP) Funciona para um cluster Considerar uma máquina dinâmica (Grid)... Ninguém sabe fazer. 2
Modelo de programa vs. Modelo de máquina RMI/RPC Java Processos comunicantes Threads Modelo de programa OpenMP Posix Threads Cilk PVM/MPI Satin Modelo de máquina Mem. compartilhada Mem. distribuída Message-Passing Interface (Não perder muito tempo!) Posix Threads int int pthread_create (( pthread_t *thr, *thr, const pthread_attr_t *attr; *attr; void*( *start_routine)(void *), *), void void *arg); pthread_t thr ; variável Identificador Atributos Função Ponteiro p/ parâmetros status Uma thread é uma estrutura de dados tipo pthread_t 3
Exemplo de criação de threads #include <pthread.h> int g; void do_it_1(void *arg) { int i, n= *arg; for(i=0; i < n; i++) g = i; void do_it_2(void *arg) { int i, n= *arg; for(i=0; i < n; i++) printf( %d\n, g); int main( int argc, char **argv) { pthread_t th1, th2; int n = 10; pthread_create(&th1, NULL, do_it_1, &N); pthread_create(&th2, NULL, do_it_2, &N);... Primitivas relacionadas com término (cont.) void voidpthread_join (( pthread_t thread, void void **status); pthread_join (t2) pthread_join (t3) T 1 pthread_exit (status) T 2 pthread_exit ( status ) T 3 Válido apenas para threads com atributo undetached (joinable): é o default! int int pthread_detach (pthread_t thread); Exemplo de espera por threads #include <pthread.h> int g; void do_it_1(void *arg) { int i, n= *(int *)arg; for(i=0; i < n; i++) g = i; void do_it_2(void *arg) { int i, n= *(int *)arg; for(i=0; i < n; i++) printf( %d\n, g); int main( int argc, char *argv) { pthread_t th1, th2; int n = 10; pthread_create(&th1, NULL, do_it_1, &n); pthread_create(&th2, NULL, do_it_2, &n);... pthread_join(th1, NULL); pthread_join(th2, NULL); 4
Algumas outras primitivas... pthread_attr_init ((); ); pthread_attr_setdetachstate (); (); pthread_attr_getdatechstate (); (); pthread_attr_setinheritsched (); (); pthread_attr_getinheritsched (); (); pthread_attr_setschedparam(); pthread_attr_getschedparam (); (); pthread_attr_setschedpolicy (); (); pthread_attr_getschedpolicy (); (); pthread_attr_setscope (); (); pthread_attr_getscope (); (); pthread_attr_setstackaddr (); (); pthread_attr_getstackaddr (); (); pthread_attr_setstacksize (); (); pthread_attr_destroy (); (); The problem with Threads... Thread é uma noção de Sis. Op. Gerenciamento de recursos de HW Posix (ou outras soluções) não fornece um modelo de programação paralela limpo. Programação com threads é complexa; Fácil de cometer erros (deadlocks) É difícil achar os erros!!!!! Ler The trouble with Threads. Lee (Berkeley), IEEE Computer, vol. 36, no. 5, May 2006, pp. 33-42. http://www.eecs.berkeley.edu/pubs/techrpts/2006/eecs-2006-1.html OpenMP (Não perder muito tempo) Idéia central: deixa o compilador se virar para mapear as iterações de laços em threads. Memória compartilhada Paralelismo de laços Compilador. 5
Outras abordagens Paralelismo funcional descreve as tarefas o que executam, quais dados acessam. sincroniza as tarefas explicitamente (sync), ou implicitamente (através dos acessos aos dados). Cria o paralelismo durante a execução Paralelismo baseado num Vetor Global Particionado GPAs SPMD + acesso a dois níveis de memória. Paralelismo Funcional: Cilk Projeto do MIT - Blumofe, Leiserson. começou em 1994, ainda atual. http://supertech.csail.mit.edu/cilk/ leva a programas altamente eficientes cilkchess 1996! Descreve tarefas (tasks) as tarefas acessam variáveis compartilhadas, pode-se disparar tarefas recursivamente gera programas paralelos de tipo Fork/Join. Restringe o modelo de programação! Implementação para memória distribuída... Complicado! Exemplo de programa Cilk cilk int fib (int n) { if (n < 2) return n; else { int x, y; x = spawn fib (n-1); y = spawn fib (n-2); sync; return (x+y); Tirando as palavras chave, recupera-se um programa seqüencial. cilk marca o procedimento como sendo potencialmente executado em paralelo. spawn dispara a execução assíncrona do procedimento. sync sincroniza as threads de execução 6
Acessos concorrentes na memória compartilhada cilk int foo (void) { int x = 0, y; spawn bar(&x); y = x + 1; sync; return (y); cilk void bar (int *px) { printf("%d", *px + 1); return; cilk int foo (void) { int x = 0; spawn bar(&x); x = x + 1; sync; return (x); cilk void bar (int *px) { *px = *px + 1; return; Okay! Problema! Cilk - modelo de programação Cilk suporta o modelo chamado strict parallel computation. Mais ou menos um Divisão & Conquista Grande vantagem: pode-se comprovar que a execução executa com tempo: T p = T 1 /p + T * Como? Escalonamento das tarefas com Work-Stealing. Unified Parallel C Paralelismo baseado num Vetor Global Particionado Projeto que começou nos anos 2000 Berkeley http://upc.lbl.gov/ Mais uma extensão de C... Programação SPMD: fluxos de execução se distribuem o trabalho Trabalho intensivo em nível do compilador. Diferença grande com OpenMP: modo de acessar a memória UPC provê uma memória compartilhada distribuída 7
Exemplo de programa UPC (K. Yelick) #include <upc.h> #include <stdio.h> main() { printf("thread %d of %d: hello UPCworld\n, MYTHREAD, THREADS); Example: Monte Carlo Pi Calculation (K. Yelick) Estimate Pi by throwing darts at a unit square Calculate percentage that fall in the unit circle Area of square = r 2 = 1 Area of circle quadrant = ¼ * π r 2 = π/4 Randomly throw darts at x,y positions If x 2 + y 2 < 1, then point is inside circle Compute ratio: # points inside / # points total π = 4*ratio r =1 Pi in UPC (K. Yelick) Independent estimates of pi: main(int argc, char **argv) { int i, hits, trials = 0; double pi; Each thread gets its own copy of these variables if (argc!= 2)trials = 1000000; else trials = atoi(argv[1]); Each thread can use input arguments srand(mythread*17); for (i=0; i < trials; i++) hits += hit(); pi = 4.0*hits/trials; printf("pi estimated to %f.", pi); Initialize random in math library Each thread calls hit separately 8
Helper Code for Pi in UPC (K. Yelick) Required includes: #include <stdio.h> #include <math.h> #include <upc.h> Function to throw dart and calculate where it hits: int hit(){ int const rand_max = 0xFFFFFF; double x = ((double) rand()) / RAND_MAX; double y = ((double) rand()) / RAND_MAX; if ((x*x + y*y) <= 1.0) { return(1); else { return(0); Acessos na memória Variáveis normais são privadas Declara-se variáveis compartilhadas com a palavra chave shared Pi in UPC: Shared Memory Style Parallel computing of pi, but with a bug shared int hits; main(int argc, char **argv) { int i, my_trials = 0; int trials = atoi(argv[1]); shared variable to record hits divide work up evenly my_trials = (trials + THREADS - 1)/THREADS; srand(mythread*17); for (i=0; i < my_trials; i++) hits += hit(); accumulate hits upc_barrier; if (MYTHREAD == 0) { printf("pi estimated to %f.", 4.0*hits/trials); What is the problem with this program? 9
Shared Arrays Are Cyclic By Default Shared scalars always live in thread 0 Shared arrays are spread over the threads Shared array elements are spread across the threads shared int x[threads] /* 1 element per thread */ shared int y[3][threads] /* 3 elements per thread */ shared int z[3][3] /* 2 or 3 elements per thread */ In the pictures below, assume THREADS = 4 Blue elts have affinity to thread 0 Think of linearized x C array, then map in round-robin y z As a 2D array, y is logically blocked by columns z is not Pi in UPC: Shared Array Version Alternative fix to the race condition Have each thread update a separate counter: But do it in a shared array Have one thread compute sum shared int all_hits [THREADS]; main(int argc, char **argv) { declarations an initialization code omitted for (i=0; i < my_trials; i++) all_hits[mythread] += hit(); upc_barrier; if (MYTHREAD == 0) { all_hits is shared by all processors, just as hits was update element with local affinity for (i=0; i < THREADS; i++) hits += all_hits[i]; printf("pi estimated to %f.", 4.0*hits/trials); UPC Pointers Where does the pointer point? Where does the pointer reside? Private Shared Local PP (p1) SP (p2) Shared PS (p3) SS (p4) int *p1; /* private pointer to local memory */ shared int *p2; /* private pointer to shared space */ int *shared p3; /* shared pointer to local memory */ shared int *shared p4; /* shared pointer to shared space */ Shared to private is not recommended. 10
Common Uses for UPC Pointer Types int *p1; These pointers are fast (just like C pointers) Use to access local data in part of code performing local work Often cast a pointer-to-shared to one of these to get faster access to shared data that is local shared int *p2; Use to refer to remote data Larger and slower due to test-for-local + possible communication int *shared p3; Not recommended shared int *shared p4; Use to build shared linked structures, e.g., a linked list forall & shared arrays If this code were doing nearest neighbor averaging (3pt stencil) the cyclic layout would be the worst possible layout. Instead, want a blocked layout Vector addition example can be rewritten as follows using a blocked layout #define N 100*THREADS shared int [*] v1[n], v2[n], sum[n]; void main() { int i; upc_forall(i=0; i<n; i++; &sum[i]) sum[i]=v1[i]+v2[i]; Affinity All non-array objects have affinity with thread zero. Array layouts are controlled by layout specifiers: Empty (cyclic layout) [*] (blocked layout) [0] or [] (indefinite layout, all on 1 thread) [b] or [b1][b2] [bn] = [b1*b2* bn] (fixed block size) The affinity of an array element is defined in terms of: block size, a compile-time constant and THREADS. Element i has affinity with thread (i / block_size) % THREADS In 2D and higher, linearize the elements as in a C representation, and then use above mapping 11
MFlops per Thread UPC Matrix Vector Multiplication Code Matrix-vector multiplication with matrix stored by rows (Contrived example: problems size is PxP) shared [THREADS] int a[threads][threads]; shared int b[threads], c[threads]; void main (void) { int i, j, l; upc_forall( i = 0 ; i < THREADS ; i++; i) { c[i] = 0; for ( l= 0 ; l< THREADS ; l++) c[i] += a[i][l]*b[l]; NAS FT Variants Performance Summary 1100 1000 900 800 700 600 500 400 300 200 MFlops per Thread Best MFlop rates for all NAS FT Benchmark versions Best NAS Fortran/MPI 1000 Best MPI Best (always NAS Slabs) Fortran/MPI Best UPC Best (always MPIPencils) Best UPC 800 600 400 200.5 Tflops 100 0 0 Myrinet 64 Myrinet 64 InfiniBand 256 InfiniBand 256 Elan3 256 Elan3 256 Elan3 512 Elan3 512 Elan4 256 Elan4 256 Elan4 512 Elan4 512 Joint work with Chris Bell, Rajesh Nishtala, Dan Bonachea UPC HPL Performance GFlop/s 1400 1200 1000 800 600 400 200 X1 Linpack Performance MPI/HPL UPC GFlop/s Opteron Cluster Linpack Performance 200 150 100 MPI/HPL UPC 50 GFlop/s Altix Linpack Performance 160 140 120 100 80 60 MPI/HPL 40 UPC 20 MPI HPL numbers from HPCC database Large scaling: 2.2 TFlops on 512p, 4.4 TFlops on 1024p (Thunder) 0 60 X1/64 X1/128 0 Opt/64 Comparison to ScaLAPACK on an Altix, a 2 x 4 process grid ScaLAPACK (block size 64) 25.25 GFlop/s (tried several block sizes) UPC LU (block size 256) - 33.60 GFlop/s, (block size 64) - 26.47 GFlop/s n = 32000 on a 4x4 process grid ScaLAPACK - 43.34 GFlop/s (block size = 64) UPC - 70.26 Gflop/s (block size = 200) Joint work with Parry Husbands 0 Alt/32 12
Outras APIs de programação paralela Fortress: proposta da SUN para uma nova linguagem paralela, OO, alto-nível (script?), que inclui tratamento de arrays distribuídos. Herdado de HPF. Chapel: proposta da CRAY para uma nova linguagem paralela, OO, alto-nível (script?), que inclui tratamento de arrays distribuídos. Herdado de HPF. Titanium: equivalente do UPC, mas em Java. Tendências... Hoje, OpenMP / MPI são a referência para PP em produção alternativa (?): threads Posix. A comunidade de Compiladores apoia muito GPAs (UPC, Chapel, Fortress...) simplicidade de uso devido ao espaço de endereçamento global, desempenho. A comunidade de Processamento de Alto Desempenho defende muito MPI. A comunidade de Processamento Distribuído defende muito Java RMI. Para usar a hierarquia de memória... Basicamente, não tem nada. MPI: esquece. No sentido de deixa o núcleo se virar com processos pesados. Threads Posix. Pode usar chamadas de sistema, mas será que se quer fazer isso? OpenMP: nada. GPAs: tem o conceito de dois níveis de memória. Idéia: acrescentar OpenMP com diretivas para uso de acessos NUMA? 13