Upload
rodrigo-saraguro
View
34
Download
6
Embed Size (px)
Citation preview
INTRODUCCION A MESSAGE PASSING INTERFACE (MPI)
Dr. Pablo Guillén
CeCalCULA
INTRODUCCION
Qué es MPI? La interface de pases de mensajes MPI, por sus siglas en Inglés, (Message Passing Interface), es una biblioteca de funciones y subrutinas que pueden ser usadas en programas C, FORTRAN y C++. Con el uso de MPI en programas que modelan algún fenómeno o proceso de Ciencias e Ingeniería, se intenta explotar la existencia de múltiples procesadores a través del pase de mensajes. MPI fue desarrollado en los años 1993-1994 por un grupo de investigadores de la Industria y la comunidad académica. Hoy en día MPI es una biblioteca estándar en la programación paralela basada en el pase de mensajes.
COMUNICACION PUNTO A PUNTO
Un Primer Programa Posiblemente el programa de más uso e introductorio de múltiples procesos, es el que cada proceso escribe el mensaje “Hola Mundo”. En el siguiente programa cada proceso escribe en pantalla el mensaje “Hola Mundo”. #include <iostream.h> #include <mpi.h> int main(int argc, char **argv) { MPI_Init(&argc,&argv); cout << “Hola Mundo” << endl; MPI_Finalize(); } Qué observamos en este programa?
• mpi.h. Nos provee de las declaraciones de funciones para todas las funciones de MPI.
• Tenemos un comienzo y un final. El comienzo está en la forma de una llamada a MPI_Init(), lo cual le indica al sistema operativo que este es un programa MPI y permite al sistema operativo a realizar cualquier inicialización necesaria. El final está en la forma de una llamada a MPI_Finalize(), lo cual le indica al sistema operativo que el ambiente de programación MPI ha culminado.
Cuando se compila y ejecuta este programa, se obtiene una colección de mensajes impresos “Hola Mundo” por pantalla. El número de mensajes es igual al número de procesos los cuales ha ejecutado el programa. Las dos funciones MPI que hemos usado en el programa tienen la siguiente forma: MPI_Init y MPI_Finalize
MPI_Init Inicializar el entorno de MPI
#include <mpi.h>
int MPI_Init(int *argc, char **argv)
argc puntero al número de argumentos
argv puntero al vector de argumentos
MPI_Finalize Terminar el entorno de MPI
#include <mpi.h>
int MPI_Finalize()
Nota: Todos los procesos deben llamar esta rutina antes de terminar. Después de haber llamado esta rutina el número de procesos no está definido. Debe llamarse poco antes de terminar el programa en cada proceso. En MPI, los procesos involucrados en la ejecución de un programa paralelo son identificados por una secuencia de números enteros no negativos. Si existen p procesos ejecutando un programa, cada proceso tendrá como identificador un número: 0, 1,..., p - 1. Nos surge la siguiente pregunta: Cómo un proceso conoce su identificador? Existen dos comandos en MPI: MPI_Comm_size y MPI_Comm_rank
MPI_Comm_size Determina el tamaño del grupo asociado con un comunicador
#include <mpi.h>
int MPI_Comm_size ( MPI_Comm comm, int *size )
Input:
comm comunicador (handle)
Output:
size puntero a un número entero que recoge el número de procesos en el grupo de comm
MPI_Comm_rank Determina el rango (identificador) del proceso actual dentro del comunicador
#include <mpi.h>
int MPI_Comm_rank ( MPI_Comm comm, int *rank )
Input:
comm comunicador (handle)
Output:
rank puntero a un número entero que recoge el rango del proceso actual en el grupo de comm
En ambas funciones el argumento comm es llamado el comunicador, y éste esencialmente es una designación para una colección de procesos que pueden comunicarse uno con el otro. MPI tiene la funcionalidad de permitir la especificación de varios comunicadores (diferenciar la colección de procesos); sin embargo en los ejemplos que se presentan en estas notas, siempre se usará el comunicador MPI_COMM_WORLD, el cual está predefinido dentro de MPI y consiste de todos los procesos inicializados cuando se ejecuta el programa paralelo. Cómo usamos esta información? Modifiquemos nuestro primer programa para que no sólo cada proceso imprima el mensaje “Hola Mundo”, sino que también imprima de cual proceso el mensaje proviene y el número total de procesos.
#include <iostream.h> #include <math.h> #include <mpi.h> int main(int argc, char ** argv){ int mynode, totalnodes; MPI_Init(&argc,&argv); MPI_Comm_size(MPI_COMM_WORLD, &totalnodes); MPI_Comm_rank(MPI_COMM_WORLD, &mynode);
cout << "Hello world from processor " << mynode; cout << " of " << totalnodes << endl; MPI_Finalize(); }
A este punto hacemos la siguiente observación: Cuando se ejecuta un programa con MPI, todos los procesos usan el mismo objeto compilado, y por lo tanto, todos los procesos están ejecutando exactamente el mismo código. Nos surge la siguiente pregunta: Qué es lo que en MPI distingue un programa paralelo ejecutandose en P procesadores de la versión serial del código ejecutandose en P procesadores? Dos cosas distinguen el programa paralelo: 1. Cada proceso usa su identificador de proceso para determinar que parte de las instrucciones del algoritmo le corresponden. 2. Los procesos se comunican uno con el otro para llevar a cabo la tarea final. Aunque cada proceso recibe una copia idéntica de las instrucciones a ser ejecutadas, ésto no implica que todos los procesos ejecutarían las mismas instrucciones. Debido a que cada proceso es capaz de obtener su identificador de proceso (usando MPI_Comm_rank), éste puede determinar que parte del código le es suministrado para ejecutar. Esto es llevado a cabo a trávez del uso de la sentencia if. La sección del código que va a ser ejecutado por un proceso particular debe estar encerrado dentro de una sentencia if, lo cual verifica el número de identificación del proceso. Si el código no está situado entre sentencias if específicas a un identificador particular, entonces el código sería ejecutado por todos los procesos (como en el caso del código mostrado anteriormente). Ahora con respecto al segundo punto, comunicación entre los procesos, recordemos que MPI es una biblioteca de pase (envío y recepción) de mensajes, el envío y la recepción de mensajes es hecho a través de las siguientes dos funciones: MPI_Send y MPI_Recv:
MPI_Send
MPI_Send Envia datos en un mensaje
#include <mpi.h"
int MPI_Send( void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm )
Input:
buf dirección del primer elemento del buffer
count número de elementos en el buffer
datatype tipo de datos de cada elemento en buffer
dest identificador del destinatario
tag bandera del mensaje
comm comunicador
El tipo de datos de cada elemento puede ser de los siguientes:
(C/C++) MPI_CHAR (char), MPI_SHORT (short), MPI_INT (int), MPI_LONG (long), MPI_FLOAT (float), MPI_DOUBLE (double), MPI_UNSIGNED_CHAR (unsigned char), MPI_UNSIGNED_SHORT (unsigned short), MPI_UNSIGNED (unsigned int), MPI_UNSIGNED_LONG (unsigned long), MPI_LONG_DOUBLE (long double) (FORTRAN) MPI_REAL (REAL), MPI_INTEGER (INTEGER), MPI_LOGICAL (LOGICAL), MPI_DOUBLE_PRECISION (DOUBLE PRECISION), MPI_COMPLEX (COMPLEX), MPI_DOUBLE_COMPLEX (complex*16 o complex*32 si existe) MPI_Recv
MPI_Recv Recibe datos de un mensaje
#include <mpi.h>
int MPI_Recv( void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status )
Output:
buf dirección del primer elemento del buffer que recibe
status una estructura que indica el estatus
Input:
count número máximo de elementos para el buffer
datatype tipo de datos de cada elemento en buffer
source identificador del remitente
tag bandera del mensaje
comm comunicador
Nota: se utilizan los mismos tipos de datos como en MPI_Send. El parámetro count indica el número máximo de elementos que pueden recibirse, el número de elementos recibidos puede determinarse con MPI_Get_count. Para mostrar el uso de MPI_Send y MPI_Recv mediante un ejemplo (programa), consideremos el siguiente ejemplo numérico (código secuencial): #include<iostream.h> int main(int argc, char **argv){ int sum;
sum = 0; for(int i=1;i<=1000;i=i+1) sum = sum + i; cout << "The sum from 1 to 1000 is: " << sum << endl; } que realiza la suma de todos los números de 1 a 1000. Qué estrategia debemos seguir para que la suma se realice en múltiples procesos? La estrategia se enfoca en particionar los cálculos (la suma) a través de los procesos. Supongamos que usamos sólo dos procesos, entonces el proceso 0 suma los números de 1 a 500, y el proceso 1 suma los números de 501 a 1000, y luego al final, los dos valores son sumados para obtener la suma total de todos los números de 1 a 1000. Una fórmula para particionar las sumas a través de los procesos está dada por: startval =1000*mynode/totalnodes+1 endval = 1000*(mynode+1)/totalnodes Si un solo proceso es usado, entonces totalnodes = 1 y mynode = 0, por tanto startval = 1 y endval = 1000. Ahora, si usamos dos procesos, entonces totalnodes = 2 y mynode toma los valores 0 y 1. Para mynode = 0, startval = 1 y endval = 500, y para mynode = 1, startval = 501 y endval = 1000. Una vez que se tienen los valores de comienzo y final donde se realizarán las sumas, cada proceso puede ejecutar un lazo (for loop) para sumar los valores entre su startval y su endval, seguidamente que la acumulación local es hecha (por cada proceso), cada proceso (diferente del proceso 0) envía su suma al proceso 0. El siguiente código lleva a cabo lo anteriormente descrito: #include<iostream.h> #include<mpi.h> int main(int argc, char ** argv){ int mynode, totalnodes; int sum,startval,endval,accum; MPI_Status status; MPI_Init(&argc,&argv); MPI_Comm_size(MPI_COMM_WORLD, &totalnodes); MPI_Comm_rank(MPI_COMM_WORLD, &mynode); sum = 0; startval = 1000*mynode/totalnodes+1; endval = 1000*(mynode+1)/totalnodes; for(int i=startval;i<=endval;i=i+1) sum = sum + i; if(mynode!=0) MPI_Send(&sum,1,MPI_INT,0,1,MPI_COMM_WORLD); else for(int j=1;j<totalnodes;j=j+1){ MPI_Recv(&accum,1,MPI_INT,j,1,MPI_COMM_WORLD, &status); sum = sum + accum;
} if(mynode == 0) cout << "The sum from 1 to 1000 is: " << sum << endl; MPI_Finalize(); }
COMUNICACION PUNTO A PUNTO
Resumen Las llamadas básicas a MPI:
Las llamadas imprescindibles:
MPI_INIT - iniciar el sistema MPI MPI_FINALIZE - terminar la cómputos con MPI MPI_COMM_SIZE - determinar el número de procesos MPI_COMM_RANK - determinar el identificador del propio proceso MPI_SEND - mandar un mensaje MPI_RECV - recibir un mensaje
Otras funciones útiles:
MPI_Barrier Bloquea hasta que todos los procesos han alcanzado esta rutina
#include "mpi.h"
int MPI_Barrier (MPI_Comm comm )
Input:
comm comunicador (handle)
Nota: Esta función es útil para asegurar que todos los procesos se encuentran en un cierto estado antes de seguir en el cálculo.
MPI_Wtime devuelve los segundos desde un momento dado no especificado
#include "mpi.h"
double MPI_Wtime()
Devuelve:
tiempo en segundos desde un instante arbitrario
Nota: Esta función puede emplearse para medir el tiempo de cálculo transcurrido: t1=MPI_Wtime(); ... cálculos ...; t2=MPI_Wtime(); printf("%e\n",t2-t1);
MPI_Get_processor_name devuelve el nombre del procesador actual
#include "mpi.h"
int MPI_Get_processor_name( char *name, int *resultlen)
Output:
name cadena de caracteres que recibe el nombre del nodo. Longitud mínima MPI_MAX_PROCESSOR_NAME
resultlen longitud del nombre devuelto
Nota: para asegurar que los procesos se ejecutan en las máquinas que hemos especificado, resulta útil incluir una llamada a esta función cuando empezamos con MPI.
COMUNICACION COLECTIVA EN MPI
Comunicaciones colectivas en MPI
En esta sección explicaremos algunas comunicaciones colectivas (comunicaciones que involucran todos los procesos de un grupo). MPI ofrece una grán variedad de este tipo de comunicaciones, aquí trataremos sólo las versiones básicas de estas comunicaciones.
MPI_Barrier - Sincronización mediante barrera MPI_Bcast - mandar datos a todos los procesos MPI_Gather - obtener datos de todos los procesos MPI_Scatter - repartir datos sobre todos los procesos MPI_Reduce - realizar operación (suma, máximo, ..) sobre todos los procesos
MPI_Barrier
MPI_Barrier Bloquea hasta que todos los procesos han alcanzado esta rutina
#include "mpi.h"
int MPI_Barrier (MPI_Comm comm )
Input:
comm comunicador (handle)
Nota: Esta función es util para asegurar que todos los procesos se encuentran en un cierto estado antes de seguir en el cálculo.
MPI_Bcast
MPI_Bcast mandar datos a todos los procesos
#include "mpi.h"
int MPI_Bcast ( void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm )
Input/Output:
buffer direccion de los datos
Input:
count número de elementos en buffer
datatype tipo de datos
root rango del proceso que contiene los datos que serán
replicados
comm comunicador
Nota: Una vez terminada la llamada todos los procesos disponen de los mismos datos que root en buffer. Esta función se suele utilizar para comunicar valores iniciales de un cálculo a todos los procesos si el procesos root se encarga del I/O.
MPI_Gather
MPI_Gather Obtener datos de todos los procesos
#include "mpi.h"
int MPI_Gather ( void *sendbuf, int sendcnt, MPI_Datatype sendtype, void *recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm )
Input:
sendbuf datos que manda cada proceso
sendcount número de elementos en sendbuf
sendtype tipo de datos en sendbuf
Output:
recvbuf aqui root recibe los datos
Input:
recvcount número de elementos en cada receive(=sendcount)
recvtype tipo de datos a recibir
root rango del proceso root
comm comunicador
Notas: Todos los procesos (root incluido) mandan su sendbuf a root. En recvbuf de root se guardan estos datos ordenados por el rango del proceso que los ha mandado. recvbuf se ignora en todos los procesos menos root. En root, recvbuf debe ser lo suficientemente grande para guardar np*recvcount datos, si np es el número de procesos.
MPI_Scatter
MPI_Scatter Repartir datos sobre todos los procesos
#include "mpi.h"
int MPI_Scatter ( void *sendbuf, int sendcnt, MPI_Datatype sendtype, void *recvbuf, int recvcnt, MPI_Datatype recvtype, int root, MPI_Comm comm )
Input:
sendbuf datos que manda root
sendcnt número de elementos en sendbuf
sendtype tipo de datos en sendbuf
Output:
recvbuf aqui cada proceso recibe los datos
Input:
recvcnt número de elementos en cada receive(=sendcount)
recvtype tipo de datos a recibir
root rango del proceso root
comm comunicador
Nota: Esta función es la "inversa" de MPI_Gather. sendbuf es repartido en np segmentos iguales y es mandado a todos los procesos (root incluido) por orden de rango. sendbuf es ignorado en todos los procesos menos root.
MPI_Reduce
MPI_Reduce Repartir datos sobre todos los procesos
#include "mpi.h"
int MPI_Reduce ( void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm )
Input:
sendbuf datos a los que se aplicará la reducción
Output:
recvbuf resultado de la operación en root
Input:
count número de elementos en sendbuf
datatype tipo de datos en sendbuf
op operación a realizar sobre elementos de sendbuf
root rango del proceso root
comm comunicador
Nota: Esta función puede utilizarse por ejemplo para calcular la suma de un número que se tiene en cada proceso y se requiere la suma en root. Las siguiente operaciones están previstos: MPI_MAX (máximo), MPI_MIN (mínimo), MPI_SUM (suma), MPI_PROD (producto) MPI_LAND (AND lógico), MPI_LOR
(OR lógico), MPI_LXOR (XOR lógico), MPI_BAND (AND binario), MPI_BOR (OR binario) MPI_BXOR (XOR binario), MPI_MAXLOC y MPI_MINLOC. Si sendbuf es un vector, la operación se realiza para cada elemento de sendbuf.