12
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.

MPI Tutorial

Embed Size (px)

Citation preview

Page 1: MPI Tutorial

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.

Page 2: MPI Tutorial

• 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)

Page 3: MPI Tutorial

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);

Page 4: MPI Tutorial

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:

Page 5: MPI Tutorial

(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;

Page 6: MPI Tutorial

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;

Page 7: MPI Tutorial

} 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()

Page 8: MPI Tutorial

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.

Page 9: MPI Tutorial

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

Page 10: MPI Tutorial

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:

Page 11: MPI Tutorial

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

Page 12: MPI Tutorial

(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.