INTRODUCCION A MESSAGE PASSING INTERFACE (MPI) by pharmphresh36

VIEWS: 193 PAGES: 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.
      •     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
                                  Determina el tamaño del grupo asociado con un
   MPI_Comm_size
                                  comunicador

   #include <mpi.h>

   int MPI_Comm_size ( MPI_Comm comm, int *size )

   Input:

   comm                           comunicador (handle)
   Output:

                               puntero a un número entero que recoge el número de
   size
                               procesos en el grupo de comm




                                Determina el rango (identificador) del proceso actual
   MPI_Comm_rank
                                dentro del comunicador

   #include <mpi.h>

   int MPI_Comm_rank ( MPI_Comm comm, int *rank )

   Input:

   comm                         comunicador (handle)

   Output:

                                puntero a un número entero que recoge el rango del
   rank
                                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:
                                          Bloquea hasta que todos los procesos han alcanzado esta
              MPI_Barrier
                                          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.

                                         devuelve los segundos desde un momento dado no
              MPI_Wtime
                                         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:

                                                 cadena de caracteres que recibe el nombre
    name                                         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
                                Bloquea hasta que todos los procesos han alcanzado esta
    MPI_Barrier
                                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:

                                         número      de      elementos               en       cada
    recvcount
                                         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:

                                         número      de      elementos               en        cada
    recvcnt
                                         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.

								
To top