Segundo Control de PAR- 1112 -Q1 21 de Diciembre de 2011

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