Nombre y Apellidos:................................................................................................................... Segundo Control de PAR- 1112 -Q1 21 de Diciembre de 2011 1. (4.0 puntos) MPI: Dado el siguiente código: /* Función que inicializa los N*N elementos de u */ void init_matrix(u); /* Función que procesa las N columnas (N elementos) de la fila que se le pasa, y * obtiene un resultado que devuelve con return */ double process(double *u); void main() { int i,j; double result; double *u; /* Malloc de los N*N elementos de la matriz u */ u = malloc(N*N*sizeof(double *)); init_matrix(u); for (i=0;i<N;i++) result+=process(&u[i*N]); printf("%f\n",result); } Realiza una implementación con MPI, Master/Worker, donde cada worker reciba cada vez una fila a procesar. El proceso master imprimirá el resultado por pantalla. Podéis suponer que tenéis estos tags: • docomp: para los mensajes enviados por el master conteniendo la fila a procesar por el worker. • compdone: para los mensajes enviados por los workers al master con el cálculo del resultado de procesar la fila con la función process. • shutdown: para los mensajes enviados por el master al los workers para indicarles que tienen que finalizar. En este caso, el mensaje es un puntero a NULL con 0 bytes de envío. Nota: La cabecera de la función MPI_Probe es int MPI_Probe(int source, int tag, MPI_Comm comm, MPI_Status *status). /* Función que inicializa los N*N elementos de u */ void init_matrix(u); /* Función que procesa las N columnas (N elementos) de la fila que se le pasa, y * obtiene un resultado que devuelve con return */ double process(double *u); /* Esta solución puede contener errores de compilación. */ int main(int argc, char *argv[]) { int i,j; double result; double *u; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &mpiRank); MPI_Comm_size(MPI_COMM_WORLD, &mpiSize); if (mpiRank==0) { /* Master */ /* Malloc de los N*N elementos de la matriz u */ int fila =0; int iproc = 0; double result, global_result=0.0; MPI_Status status; u = malloc(N*N*sizeof(double *)); init_matrix(u); for (iproc=0; iproc<mpiSize && fila<N; iproc++, fila++) MPI_Send(&u[fila*N] , N, MPI_DOUBLE, iproc, docomp, MPI_COMM_WORLD); while (fila<N) { MPI_Recv(&result, 1 , MPI_DOUBLE, MPI_ANY_SOURCE, compdone, MPI_COMM_WORLD, &status global_result += result; fila ++; MPI_Send(&u[fila*N] , N, MPI_DOUBLE, status.MPI_SOURCE , docomp, MPI_COMM_WORLD); } while (iproc>0) { MPI_Recv(&result, 1 , MPI_DOUBLE, MPI_ANY_SOURCE, compdone, MPI_COMM_WORLD, &status global_result += result; iproc--; MPI_Send(NULL , 0, MPI_DOUBLE, status.MPI_SOURCE , shutdown, MPI_COMM_WORLD); } printf("%f\n",global_result); MPI_Finalize(); } else { /* Worker */ int final=0; MPI_Status status; double result; u = malloc(N*sizeof(double *)); while(!final) { MPI_Probe(0,MPI_ANY_TAG, MPI_COMM_WORLD, &status); if (status.MPI_TAG == docomp) { MPI_Recv(u, N , MPI_DOUBLE, 0, docomp, MPI_COMM_WORLD, &status); result = process(u); MPI_Send(&result, 1, MPI_DOUBLE, 0, compdone, MPI_COMM_WORLD); } else { MPI_Recv(NULL, 0 , MPI_DOUBLE, 0, shutdown, MPI_COMM_WORLD, &status); final = 1; } } MPI_Finalize(); } } 2. (3 puntos) OMP: Dada las siguientes cabeceras de funciones y el código secuencial del quicksort: /* Ordena con quicksort el vector v de n elementos */ void quicksort_base(int * v, int n); /* Devuelve el índice del pivote en el vector */ int find_pivot(int *v, int n); void quicksort(int *v, int n) { int index; if (n<N_BASE) { quicksort_base(v,n); } else { index = find_pivot(v,n); quicksort(v,index); quicksort(&v[index], n-index); } return; } void main() { .... quicksort(v,N); ... } Implementa un quicksort paralelo para memoria compartida con OpenMP utilizando la estrategia Divide & Conquer. Esta implementación debería tener en consideración los overheads de creación de tareas. /* Ordena con quicksort el vector v de n elementos */ void quicksort_base(int * v, int n); /* Devuelve el índice del pivote en el vector */ int find_pivot(int *v, int n); void quicksort(int *v, int n, int d) { int index; if (n<N_BASE) { quicksort_base(v,n); } else { if (d>cutoff) { index = find_pivot(v,n); quicksort(v,index,d); quicksort(&v[index], n-index,d); } else { index = find_pivot(v,n); #pragma omp task quicksort(v,index,d+1); #pragma omp task quicksort(&v[index], n-index, d+1); } } return; } void main() { .... #pragma omp parallel { #pragma omp single /* Parámetro extra para controlar el * número de tasks creadas con el cutoff */ quicksort(v,N,0); /* El taskwait, de hecho, no es * necesario ya que todos los * threads hara join al final * del parallel . */ #pragma omp taskwait } ... } 3. (1.0 punto) Indica tres formas en las que cada procesador puede comunicar el boundary superior de un vecino, y que él tiene, y recibir su boundary superior, y que otro procesador tiene, con tal de que no se produzca un deadlock entre todos los procesadores. Suponiendo que el vecino inferior del procesador size-1 es el procesador 0, y viceversa. (a) Cambiando el orden de los send/recv bloqueantes. anticlock = (rank+size-1)%size; clock = (rank+1)%size; if (rank%2) \{ MPI_Recv (buf2, count, MPI_INT, MPI_Send (buf1, count, MPI_INT, \} else \{ MPI_Send (buf1, count, MPI_INT, MPI_Recv (buf2, count, MPI_INT, \} anticlock, tag, comm, &status); clock, tag, comm); clock, tag, comm); anticlock, tag, comm, &status); (b) Utilizando el MPI_Sendrecv. MPI_Sendrecv (buf1, count, MPI_INT, clock, tag, buf2, count, MPI_INT, anticlock, tag, comm, &status); (c) Utilizando comunicaciones no bloqueantes. ... MPI_Irecv (buf2, count, MPI_INT, anticlock, tag, comm, &request); MPI_Send (buf1, count, MPI_INT, clock, tag, comm): MPI_Wait (&request, &status); ... (d) (2.0 puntos) Dado el siguiente código: /* Función que inicializa los N*N elementos de u */ void init_matrix(u); /* Dado dos elementos de tipo double realiza un cálculo complejo y devuelve el resultado double compute(double u, double z); void main() { int i,j; double result=0.0; double *u, *z; /* Malloc de los N*N elementos de la matriz u y z*/ u = malloc(N*N*sizeof(double *)); z = malloc(N*N*sizeof(double *)); init_matrix(u); init_matrix(z); for (i=0;i<N;i++) for (j=i+1; j<N; j++) result+=compute(u[i*N+j],z[i*N+j]); printf("%f\n",result); } Y suponiendo que realizamos una paralelización con MPI del código usando Geometric Decomposition de tipo block, que el malloc y la inicialización de las matrices u y z se hace en paralelo en cada uno de los P procesadores, y que el resultado lo escribirá por pantalla el master, contesta a las siguientes preguntas: • (1.0 punto) Realiza el modelo de ejecución (cómputo y comunicación) de la estrategia de paralelización indicada, teniendo en cuenta que la inicialización de cada elemento de la matriz es ti , y que la función compute tiene un coste de tc . Suponed que la inicialización se hace sólo de los datos que son procesados y que estáis en una red de interconexión de tipo ring. Descomposición geométrica de tipo block significa que las filas se distribuyen equitativamente entre los procesadores tal y como muestra el dibujo de la izquierda de la figura, es decir, N/P filas por procesador, siendo P el número de procesadores. Además se indica que estas filas se allocatan y se inicializan de forma paralela, por lo que no hay comunicación inicial. Por otra parte, la única dependencia entre los procesos es el cálculo del result, el cual se puede solucionar con una comunicación de tipo Alltoone o Reducción. Esta comunicación colectiva será la única comunicación que tendremos. k k P0 k P1 P0 k P2 P3 El tiempo de comunicación de un Alltoone en una ring, utilizando el algoritmo visto en clase es: Tcomm = (ts + 1 × tw ) × log P En cuanto al cómputo, si nos fijamos en el bucle con variable de inducción j (bucle interno), el procesamiento que se realiza es de todos los elementos por encima de la diagonal superior (comienza en i + 1). Como todos los procesos pueden operar en paralelo, el procesador que tardará más en realizar la inicialización de las matrices y el cálculo de result será el procesador 0, determinando así el tiempo de cómputo del modelo a realizar. Tcomp = (N ∗ N/P X N − i)(tc + 2 × ti ) P i=1 Por lo que el tiempo total es: Ttotal = (N ∗ N/P X N − i)(tc + 2 × ti ) + (ts + 1 × tw ) × log P P i=1 • (0.5 puntos) Calcula el desbalanceo de trabajo entre el proceso 0 y el proceso P − 1. El desbalanceo entre el proceso 0 y el proceso P − 1 es exactamente el trabajo realizado desde que el proceso 0 empieza a procesar una columna completa de las filas que le han tocado (se acaba el triangulo de su parte), hasta el final del su procesamiento. Esto es porque el proceso P − 1 justamente sólo tiene que calcular una cantidad de elementos igual a los que tiene que procesar el proceso 0 al inicio. Por consiguiente, el desbalanceo total de cómputo es: Tcomp = ((N − N N ) ∗ )(tc ) P P Si contamos también la inicialización, entonces tenemos: Tcomp = ((N − N N ) ∗ )(tc + 2 × ti ) P P • (0.5 puntos) ¿Qué tipo de distribución de trabajo realizarías para reducir el desbalanceo? Razona tu respuesta. Realizaríamos una distribución block-cyclic (dibujo de la derecha de la figura), con tal de reducir el desbalanceo de carga entre los procesos, reduciendo así el tiempo de ejecución final.