Unidad que genera el documento: Facultad de Ingeniería
Programa de Ingeniería de Sistemas
Nombre del documento: Guía de Laboratorio No.1- Redes 2
Tema: Programación de Hora: Horario Duración: 2
MPI de clase / casa horas
Autor: Ing. German E. Campos H.
El objetivo: Profundizar los conocimientos sobre MPI aprovechando los diversos
comandos al respecto.
MPI es un estándar de una interface de paso de mensajes (Message Passing Interface) cuyas
aplicaciones son fácilmente portables y como tal no es un lenguaje, de hecho, es una librería que
se añade a lenguajes como C y C++, de ahí que se necesiten los algoritmos ya escritos para que
se puedan correr en este estándar.
Éste estándar se apoya más en el rendimiento que en la heterogeneidad o tolerancia a fallos del
sistema del clúster.
Lo mínimo para correr una aplicación con MPI son estas 5 rutinas:
Inicio y finalización (MPI_init y MPI_Finalize)
Envío y recepción bloqueantes
Envío y recepción no bloqueantes
Sincronización (barreras)
Envío y recepción múltiple
La librería que hay que incluir en los códigos para utilizar MPI es la siguiente:
#include "mpi.h"
La compilación es idéntica a la compilación con gcc, por tanto, se pueden usar los mismos
parámetros. Para compilar un programa con MPI en C, tenga en cuenta el siguiente código:
1 de 19
mpicc codigo_fuente.c -o ejecutable
mpicc codigo_fuente.c -c codigo_objeto.o
mpicc -o ejecutable codigo_objeto.o
Para compilar un programa con MPI en C++, se pueden usar los comandos mpicxx o bien mpiCC
(para C++), es indiferente.
mpicxx codigo_fuente.cpp -o ejecutable
mpiCC codigo_fuente.cpp -o ejecutable
Para ejecutar un programa paralelo se usa el siguiente comando:
mpirun -np 4 ejecutable
En este caso, np revela la cantidad de number of process, luego se han ejecutado cuatro
PROCESOS. Ojo, no son hilos o hebras.
Para ejecutar un programa paralelo en varias máquinas, se digita:
mpirun -machinefile red_de_nodos.txt -np 4 ejecutable
mpirun -nolocal -machinefile red_de_nodos.txt -np 4 ejecutable
Se tiene el supuesto que en el archivo especificado (red_de_nodos.txt) se han especificado la
dirección de las máquinas a utilizar. El archivo tendría, por ejemplo, las siguientes máquinas:
Compute_0_0
Compute_0_1
Compute_0_2
Compute_0_3
Compute_0_4
Compute_0_5
En el primer caso, el computador que ejecuta el programa (el Front End) se asigna como proceso
0, el proceso 1 sería la máquina Compute_0_0, el proceso 2 sería Compute_0_1 y el proceso 3
sería Compute_0_2, etc.
En el segundo caso, al especificar el parámetro -nolocal, el programa no se ejecuta en el
computador que lo lanza, entonces el proceso 0 sería Compute_0_0, el proceso 1 sería
Compute_0_1, el proceso 2 sería Compute_0_2, etc., y por último el proceso 5 sería
Compute_0_5.
A Continuación, se listan los parámetros más importantes que se especifican para un programa
con MPI.
np Numero de procesos, va seguido de la cantidad (Por ejemplo, -np 4).
Especifica un archivo con la dirección de las máquinas que ejecutarán el código
machinefile
(Por ejemplo, -machinefile red_de_nodos).
nolocal Indica que el programa no se va a ejecutar en la máquina que lo lanza. Solo tiene
2 de 19
sentido si se especifica un archivo de máquinas después (-machinefile), (Por
ejemplo -nolocal -machinefile red_de_nodos).
Por ejemplo, si se quiere hacer un programa que imprima por pantalla el saludo "Hola Mundo soy
el proceso A, de B que somos" donde A será el identificador del proceso (rango), y B el número de
procesos que fueron lanzados de forma paralela.
Para ello, las funciones necesarias son las siguientes:
MPI_Init
MPI_Finalize
MPI_Comm_size
MPI_Comm_rank
Luego, el código a ejecutar es el siguiente (NOTA: para evitar problemas de compilación, si lo
desea, elimine los comentarios en gris):
#include "mpi.h"
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
int rank, size;
MPI_Init(&argc, &argv); // Inicialización del entorno MPI
MPI_Comm_size(MPI_COMM_WORLD, &size); // Obtenemos el número de procesos en el
comunicador global
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // Obtenemos la identificación de nuestro
proceso en el comunicador global
cout<<"¡Hola Mundo desde el proceso "<<rank<<" de "<<size<<" que somos!"<<endl;
// Terminamos la ejecución de los procesos, después de esto solo existirá
// la hebra 0
// ¡Ojo! Esto no significa que los demás procesos no ejecuten el resto
// de código después de "Finalize", es conveniente asegurarnos con una
// condición si vamos a ejecutar mas código (Por ejemplo, con "if(rank==0)".
MPI_Finalize();
return 0;
}
Ahora, ¿qué se obtiene a la salida?
>mpiCC 1_HolaMundo.cpp -o holaMundo
>mpirun -np 4 holaMundo
¡Hola Mundo desde el proceso 2 de 4 que somos!
¡Hola Mundo desde el proceso 3 de 4 que somos!
¡Hola Mundo desde el proceso 0 de 4 que somos!
¡Hola Mundo desde el proceso 1 de 4 que somos!
La instrucción "mpirun -np 4 holaMundo" lanza 4 procesos del programa holaMundo,
indicado con "-np". Cada uno de ellos ejecuta el código indicando su número identificador,
obtenido de la función MPI_Comm_rank, y el número de procesos que hay en ejecución, obtenido
de la función MPI_Comm_size.
MPI_Init se encarga de inicializar el entorno MPI para que se puedan comunicar los distintos
procesos, por tanto debe llamarse antes de cualquier otra función MPI.
Del mismo modo MPI_Finalize libera el entorno MPI y debe ser la última llamada MPI realizada.
3 de 19
Ahora, para comunicar proceso, se requieren los siguientes comandos:
MPI_Send y MPI_Recv
Si se quiere hacer un programa paralelo que encadene el envío y recepción de un mensaje, en
nuestro caso el mensaje será el rango (identificador) del proceso que envía.
Los mensajes se enviarán de forma encadenada, lo que quiere decir que el primero enviará un
mensaje al segundo, el segundo recibirá uno del primero y enviará uno al tercero, y así
sucesivamente para todos los procesos lanzados.
Todo proceso que reciba un mensaje debe imprimirlo de la forma "Soy el proceso X y he recibido
M", siendo X el rango del proceso y M el mensaje recibido.
#include "mpi.h"
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
int rank, size, contador;
MPI_Status estado;
MPI_Init(&argc, &argv); // Inicializamos la comunicación de los procesos
MPI_Comm_size(MPI_COMM_WORLD, &size); // Obtenemos el numero total de hebras
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // Obtenemos el valor de nuestro identificador
if(rank == 0){
MPI_Send(&rank //referencia al vector de elementos a enviar
,1 // tamaño del vector a enviar
,MPI_INT // Tipo de dato que envías
,rank+1 // pid del proceso destino
,0 //etiqueta
,MPI_COMM_WORLD); //Comunicador por el que se manda
}else{
MPI_Recv(&contador // Referencia al vector donde se almacenara lo recibido
,1 // tamaño del vector a recibir
,MPI_INT // Tipo de dato que recibe
,rank-1 // pid del proceso origen de la que se recibe
,0 // etiqueta
,MPI_COMM_WORLD // Comunicador por el que se recibe
,&estado); // estructura informativa del estado
cout<<"Soy el proceso "<<rank<<" y he recibido "<<contador<<endl;
contador++;
if(rank != size-1)
MPI_Send(&contador, 1 ,MPI_INT ,rank+1 , 0 ,MPI_COMM_WORLD);
}
// Terminamos la ejecución de las hebras, después de esto solo existirá
// la hebra 0
// ¡Ojo! Esto no significa que las demás hebras no ejecuten el resto
// de código después de "Finalize", es conveniente asegurarnos con una
// condición si vamos a ejecutar mas código (Por ejemplo, con "if(rank==0)".
MPI_Finalize();
return 0;
}
Obtenido el número de procesos (size) y el rango (rank) el envío del mensaje se realiza al
proceso con un rango mayor en una unidad, por tanto el proceso con ese rango debe indicar que
quiere recibir un mensaje del proceso con un rango inferior en una unidad al suyo.
4 de 19
En nuestro programa debemos hacer una distinción ya que no todos los procesos hacen lo mismo,
el primero no recibirá de nadie y el último no enviará a nadie, esto se puede especificar mediante
condiciones que filtren el rango del proceso.
Luego, ¿qué se tiene a la salida?
> mpiCC 2_Send_Receive.cpp -o sendrecv
> mpirun -np 8 sendrecv
Soy el proceso 1 y he recibido 0
Soy el proceso 2 y he recibido 1
Soy el proceso 3 y he recibido 2
Soy el proceso 4 y he recibido 3
Soy el proceso 5 y he recibido 4
Soy el proceso 6 y he recibido 5
Soy el proceso 7 y he recibido 6
Ahora, para repartir trabajo entre procesos, se requieren los siguientes comandos:
MPI_Bcast y MPI_Reduce
En este caso, se llega a un paso un poco más avanzado de MPI: la repartición de trabajo entre
procesos, pero para este caso, se ejecuta la paralelización del cálculo de la aproximación del
número π (pi) indicando un factor de precisión.
Téngase en cuenta que dicho valor se define como una integral entre 0 y 1 de 4/(1+x 2), o sea, el
área que forma bajo la curva del semicírculo que dibuja, esto es:
Como opera: El proceso “0” o P0 es el que recibe la precisión (n) con la que se desea calcular el
número π, o sea, el número de iteraciones. Supóngase en este caso que n=4.
Cada proceso por su lado calcula su parte del loop del cálculo de π y obtienen una parte del
numero en una variable temporal, lo suman entre ellos y se lo envían al proceso P0 para que este
tenga el resultado final.
Como la aproximación a una integral se puede hacer por una sumatoria de Riemann, entonces se
puede dividir el trabajo en unidades independientes siendo justamente más preciso entre más
divisiones se tengan.
Ese factor de precisión puede pedirlo el proceso P0 y repartirlo a los otros procesos mediante el
comando MPI_Bcast. Luego de que cada proceso haga su respectivo calculo, se han de unificar
todas esas partes en el P0 para mostrar el resultado con MPI_Reduce.
El siguiente es el código en C++ para plantear dicho calculo:
#include <math.h>
#include <cstdlib> // Incluido para el uso de atoi
#include <iostream>
5 de 19
using namespace std;
int main(int argc, char *argv[])
{
// Calculo de “pi”
int n;
cout<<"Introduzca la precisin del calculo (n > 0): ";
cin>>n;
double PI25DT = 3.141592653589793238462643;
double h = 1.0 / (double) n;
double sum = 0.0;
for (int i = 1; i <= n; i++) {
double x = h * ((double)i - 0.5);
sum += (4.0 / (1.0 + x*x));
}
double pi = sum * h;
cout << "El valor aproximado de PI es: " << pi << ", con un error de " <<
fabs(pi - PI25DT) << endl;
return 0;
}
Ahora, ¿qué comando emplear? En este punto, junto con lo ya revisados, los comandos
MPI_Bcast y MPI_Reduce le permiten paralelizar las cargas entre procesos...
#include <math.h>
#include "mpi.h" // Biblioteca de MPI
#include <cstdlib> // Incluido para el uso de atoi
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
int n, // Numero de iteraciones
rank, // Identificador de proceso
size; // Numero de procesos
double PI25DT = 3.141592653589793238462643;
double mypi, // Valor local de “pi”
pi, // Valor global de “pi”
h, // Aproximación del área para el cálculo de “pi”
sum; // Acumulador para la suma del área de “pi”
bool valor_por_parametros = true; // Comprueba si hay valores por parámetros
MPI_Init(&argc, &argv); // Inicializamos los procesos
MPI_Comm_size(MPI_COMM_WORLD, &size); // Obtenemos el número total de procesos
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // Obtenemos el valor de nuestro identificador
// Solo el proceso 0 va a conocer el numero de iteraciones que vamos a
// ejecutar para la aproximación de “pi”
if (rank == 0) {
cout<<"Introduzca la precision del calculo (n > 0): ";
cin>>n;
}
// El proceso 0 reparte al resto de procesos el número de iteraciones
// que se calculan para la aproximación de “pi”
MPI_Bcast(&n, // Puntero al dato que vamos a enviar
1, // Numero de datos a los que apunta el puntero
MPI_INT, // Tipo del dato a enviar
0, // Identificación del proceso que envía el dato
MPI_COMM_WORLD);
if (n <= 0){
MPI_Finalize();
exit(0);
}else {
// Calculo de “pi”
h = 1.0 / (double) n;
sum = 0.0;
for (int i = rank + 1; i <= n; i += size) {
6 de 19
double x = h * ((double)i - 0.5);
sum += (4.0 / (1.0 + x*x));
}
mypi = h * sum;
// Todos los procesos ahora comparten su valor local de “pi”,
// lo hacen reduciendo su valor local a un proceso
// seleccionado a través de una operación aritmética.
MPI_Reduce(&mypi, // Valor local de “pi”
&pi, // Dato sobre el que vamos a reducir el resto
1, // Numero de datos que vamos a reducir
MPI_DOUBLE, // Tipo de dato que vamos a reducir
MPI_SUM, // Operación que aplicaremos
0, // proceso que va a recibir el dato reducido
MPI_COMM_WORLD);
// Solo el proceso 0 imprime el mensaje, ya que es el único que
// conoce el valor de “pi” aproximado.
if (rank == 0)
cout << "El valor aproximado de PI es: " << pi
<< ", con un error de " << fabs(pi - PI25DT)
<< endl;
}
// Terminamos la ejecución de los procesos, después de esto solo existirá
// el proceso 0
// ¡Ojo! Esto no significa que los demás procesos no ejecuten el resto
// de código después de "Finalize", es conveniente asegurarnos con una
// condición si vamos a ejecutar mas código (Por ejemplo, con "if(rank==0)".
MPI_Finalize();
return 0;
}
Ahora, ¿qué se vería a la salida?
> mpiCC 3_Calculo_de_PI.cpp -o PI
> mpirun -np 5 PI
Introduzca la precision del calculo (n > 0): 20
El valor aproximado de PI es: 3.1418, con un error de 0.000208333
7 de 19