Conexión de MPI en Visual Studio. Introducción a las tecnologías de programación paralela (MPI)

Esta nota muestra cómo instalar MPI, conectarlo a Visual Studio y luego usarlo con los parámetros especificados (número de nodos de cálculo). Este artículo utiliza Visual Studio 2015, porque... Esta es con la que mis alumnos tuvieron problemas (esta nota fue escrita por estudiantes para estudiantes), pero las instrucciones probablemente también funcionarán para otras versiones.

Paso 1:
Debes instalar el HPC Pack 2008 SDK SP2 (en tu caso puede que ya exista una versión diferente), disponible en la web oficial de Microsoft. La capacidad de bits del paquete y del sistema deben coincidir.

Paso 2:
Necesita configurar las rutas; para hacer esto, vaya a la pestaña Depurar - Propiedades:

“C:\Archivos de programa\Microsoft HPC Pack 2008 SDK\Incluir”

En el campo Directorios de bibliotecas:

“C:\Archivos de programa\Microsoft HPC Pack 2008 SDK\Lib\amd64”

En el campo de la biblioteca, si hay una versión de 32 bits, debe ingresar i386 en lugar de amd64.

Msmpi.lib

:

Paso 3:

Para configurar el inicio, debe ir a la pestaña Depuración y en el campo Comando especificar:

“C:\Archivos de programa\Microsoft HPC Pack 2008 SDK\Bin\mpiexec.exe”

En el campo Argumentos de comando, especifique, por ejemplo,

N 4 $(Ruta de destino)

El número 4 indica el número de procesos.

Para ejecutar el programa es necesario conectar la biblioteca.

La ruta al proyecto no debe contener cirílico. Si se producen errores, puede utilizar Microsoft MPI, disponible en el sitio web de Microsoft.

Para hacer esto, después de la instalación, simplemente ingrese la ruta en el campo Comando de la pestaña Depuración:

“C:\Archivos de programa\Microsoft MPI\Bin\mpiexec.exe”

Además, antes de ejecutar el programa, no olvides indicar su profundidad de bits:

Ejemplo de ejecución de un programa con MPI:

#incluir #incluir usando el espacio de nombres estándar; int main(int argc, char **argv) ( int rango, tamaño; MPI_Init(&argc, &argv); MPI_Comm_size(MPI_COMM_WORLD, &size); MPI_Comm_rank(MPI_COMM_WORLD, &rank); cout<< "The number of processes: " << size << " my number is " << rank << endl; MPI_Finalize(); return 0; }

Ejecutando el programa en 2 nodos:

Dio la casualidad de que tuve un encuentro cercano con el estudio de la computación paralela y en particular con MPI. Quizás esta dirección sea muy prometedora hoy en día, por lo que me gustaría mostrar a los navegadores los conceptos básicos de este proceso.

Principios básicos y ejemplo.
Se utilizará como ejemplo el cálculo de la exponencial (e). Una de las opciones para encontrarlo es la serie de Taylor:
e^x=∑((x^n)/n!), donde la sumatoria ocurre desde n=0 hasta el infinito.

Esta fórmula se puede paralelizar fácilmente, ya que el número requerido es la suma de los términos individuales y gracias a esto, cada procesador individual puede comenzar a calcular los términos individuales.

El número de términos que se calcularán en cada procesador individual depende tanto de la longitud del intervalo n como del número disponible de procesadores k que pueden participar en el proceso de cálculo. Entonces, por ejemplo, si la longitud del intervalo es n=4 y cinco procesadores (k=5) participan en los cálculos, entonces del primero al cuarto procesador recibirá cada uno un término y el quinto no se utilizará. Si n=10 y k=5, cada procesador obtendrá dos términos para el cálculo.

Inicialmente, el primer procesador, utilizando la función de transmisión MPI_Bcast, envía a los demás el valor de la variable n especificada por el usuario. En general, la función MPI_Bcast tiene el siguiente formato:
int MPI_Bcast(void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm), donde buffer es la dirección del buffer con el elemento, count es el número de elementos, datatype es el tipo de datos correspondiente en MPI, root es el rango del procesador principal que maneja el reenvío y comm es el nombre del comunicador.
En mi caso, el papel del procesador principal, como ya mencioné, será el primer procesador con rango 0.

Una vez que el número n se envíe con éxito, cada procesador comenzará a calcular sus términos. Para hacer esto, en cada paso del ciclo, al número i, que inicialmente es igual al rango del procesador, se le sumará un número igual al número de procesadores que participan en los cálculos. Si el número i en los siguientes pasos excede el número n especificado por el usuario, la ejecución del bucle para ese procesador se detendrá.

Durante la ejecución del ciclo, los términos se sumarán a una variable separada y, una vez finalizado, el monto resultante se enviará al procesador principal. Para ello se utilizará la función de operación de reducción MPI_Reduce. En general se ve así:
int MPI_Reduce(void *buf, void *resultado, int count, tipo de datos MPI_Datatype, MPI_Op op, int root, MPI_Comm comm)

Concatena los elementos del búfer de entrada de cada proceso en el grupo usando la operación op y devuelve el valor combinado al búfer de salida del número de proceso raíz. El resultado de dicha operación será un valor único, razón por la cual la función de conversión recibió su nombre.

Luego de ejecutar el programa en todos los procesadores, el primer procesador recibirá la suma total de términos, que será el valor del exponente que necesitamos.

Cabe señalar que tanto en el método paralelo como en el secuencial para calcular el exponente, se utiliza una función recursiva para encontrar el factorial. Al decidir cómo paralelizar la tarea que se estaba realizando, consideré la opción de encontrar el factorial también en diferentes procesadores, pero al final consideré esta opción irracional.

La tarea principal sigue siendo encontrar el valor del exponente, y si los procesadores comienzan a calcular cada factorial de cada término por separado, esto puede llevar al efecto exactamente opuesto, es decir, una pérdida significativa en el rendimiento y la velocidad de cálculo.
Esto se explica por el hecho de que en este caso habrá una carga muy grande en el entorno de comunicación, que a menudo ya es un eslabón débil en los sistemas informáticos paralelos. Si el factorial se calcula de forma privada en cada procesador, la carga en las líneas de comunicación será mínima. Este caso puede considerarse un buen ejemplo del hecho de que la tarea de paralelización a veces también debe tener sus límites.

Algoritmo de ejecución de código
1. El valor del número n se transfiere desde el shell visual al programa, que luego se envía a todos los procesadores mediante la función de transmisión.
2. Cuando se inicializa el primer procesador principal, se inicia un temporizador.
3. Cada procesador ejecuta un bucle, donde el valor de incremento es el número de procesadores en el sistema. En cada iteración del ciclo, se calcula un término y la suma de dichos términos se almacena en la variable drobSum.
4. Una vez que se completa el ciclo, cada procesador agrega su valor drobSum a la variable Result usando la función de reducción MPI_Reduce.
5. Después de completar los cálculos en todos los procesadores, el primer procesador principal detiene el temporizador y envía el valor resultante de la variable Resultado al flujo de salida.
6. El valor de tiempo medido por nuestro temporizador en milisegundos también se envía al flujo de salida.
Listado de códigos
El programa está escrito en C++, asumiremos que los argumentos para la ejecución se pasan desde el shell externo. El código se ve así:
#incluir "mpi.h"
#incluir
#incluir
usando el espacio de nombres estándar;

doble hecho(int n)
{
si (n==0)
devolver 1;
demás
devolver n*Hecho(n-1);
}

int principal(int argc, char *argv)
{
EstablecerConsoleOutputCP(1251);
int n;
int miid;
int números;
ent yo;
intrc;
long double drob,drobSum=0,Resultado, suma;
hora de inicio doble = 0,0;
doble tiempo de finalización;

N = atoi(argv);

si (rc= MPI_Init(&argc, &argv))
{
corte<< "Error de inicio, ejecución detenida" << endl;
MPI_Abort(MPI_COMM_WORLD, rc);
}

MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);

si (miid == 0)
{

Hora de inicio = MPI_Wtime();
}
MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);

para (i = miid; yo<= n; i += numprocs)
{
drob = 1/Hecho(i);
drobSum += drob;
}

MPI_Reduce(&drobSum, &Resultado, 1, MPI_LONG_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
cout.precisión(20);
si (miid == 0)
{
corte<< Result << endl;
tiempo final = MPI_Wtime();
corte<< (endwtime-startwtime)*1000 << endl;
}

MPI_Finalize();
devolver 0;
}


* Este código fuente fue resaltado con Resaltador de código fuente.
Conclusión
Así, obtuvimos un programa sencillo para calcular el exponente utilizando varios procesadores a la vez. Probablemente, el cuello de botella sea almacenar el resultado en sí, porque con un aumento en el número de dígitos, almacenar un valor usando tipos estándar no será trivial y este lugar requiere elaboración. Quizás una solución bastante racional sea escribir el resultado en un archivo, aunque, dada la función puramente educativa de este ejemplo, no es necesario prestar mucha atención a esto.

Anotación: La conferencia está dedicada a la consideración de la tecnología MPI como estándar de programación paralela para sistemas de memoria distribuida. Se consideran los principales modos de transmisión de datos. Se introducen conceptos como grupos de procesos y comunicadores. Cubre tipos de datos básicos, operaciones punto a punto, operaciones colectivas, operaciones de sincronización y mediciones de tiempo.

Objetivo de la conferencia: La conferencia tiene como objetivo estudiar la metodología general para el desarrollo de algoritmos paralelos.

Grabación de vídeo de la conferencia - (volumen - 134 MB).

5.1. MPI: conceptos básicos y definiciones

Consideremos una serie de conceptos y definiciones que son fundamentales para el estándar MPI.

5.1.1. El concepto de programa paralelo.

Bajo programa paralelo En el marco de MPI, entendemos un conjunto de acciones ejecutadas simultáneamente. procesos. Los procesos se pueden ejecutar en diferentes procesadores, pero también se pueden ubicar varios procesos en el mismo procesador (en este caso, se ejecutan en modo de tiempo compartido). En el caso extremo, se puede utilizar un solo procesador para ejecutar un programa paralelo; por regla general, este método se utiliza para comprobar inicialmente la corrección del programa paralelo.

Cada proceso de un programa paralelo se genera a partir de una copia del mismo código de programa ( modelo SPMP). Este código de programa, presentado en forma de programa ejecutable, debe estar disponible en el momento en que se inicia el programa paralelo en todos los procesadores utilizados. El código fuente del programa ejecutable se desarrolla en los lenguajes algorítmicos C o Fortran utilizando una u otra implementación de la biblioteca MPI.

La cantidad de procesos y la cantidad de procesadores utilizados se determinan en el momento en que se inicia el programa paralelo utilizando el entorno de ejecución del programa MPI y no pueden cambiar durante los cálculos (el estándar MPI-2 brinda la posibilidad de cambiar dinámicamente la cantidad de procesos). Todos los procesos del programa están numerados secuencialmente del 0 al p-1, Dónde pag es el número total de procesos. El número de proceso se llama rango proceso.

5.1.2. Operaciones de transferencia de datos

MPI se basa en operaciones de paso de mensajes. Entre las funciones proporcionadas como parte de MPI, existen diferentes dobles (punto a punto) operaciones entre dos procesos y colectivo (colectivo) acciones de comunicación para la interacción simultánea de varios procesos.

Para realizar operaciones emparejadas, se pueden utilizar diferentes modos de transmisión, incluido el síncrono, el bloqueo, etc.; una consideración completa de los posibles modos de transmisión se realizará en la subsección 5.3.

Como se señaló anteriormente, el estándar MPI prevé la necesidad de implementar la mayoría de las operaciones básicas de transferencia de datos colectivos; consulte las subsecciones 5.2 y 5.4.

5.1.3. Concepto de comunicadores

Los procesos de un programa paralelo se combinan en grupos. Bajo comunicador MPI se refiere a un objeto de servicio especialmente creado que combina un grupo de procesos y una serie de parámetros adicionales ( contexto) utilizado al realizar operaciones de transferencia de datos.

Normalmente, las operaciones de transferencia de datos emparejadas se realizan para procesos que pertenecen al mismo comunicador. Las operaciones colectivas se aplican simultáneamente a todos los procesos del comunicador. Como resultado, especificar el comunicador a utilizar es obligatorio para las operaciones de transferencia de datos en MPI.

Durante los cálculos, se pueden crear nuevos grupos de procesos y comunicadores y eliminar los existentes. Un mismo proceso puede pertenecer a diferentes grupos y comunicadores. Todos los procesos presentes en el programa paralelo están incluidos en el comunicador creado por defecto con el identificador MPI_COMM_WORLD.

Si es necesario transferir datos entre procesos de diferentes grupos, es necesario crear un comunicador global ( intercomunicador).

En la subsección 5.6 se realizará una discusión detallada de las capacidades de MPI para trabajar con grupos y comunicadores.

5.1.4. Tipos de datos

Al realizar operaciones de paso de mensajes, debe especificar los datos que se enviarán o recibirán en las funciones MPI. tipo datos enviados. MPI contiene un gran conjunto tipos básicos datos que coinciden en gran medida con los tipos de datos en los lenguajes algorítmicos C y Fortran. Además, MPI tiene la capacidad de crear nuevos tipos derivados datos para una descripción más precisa y concisa del contenido de los mensajes reenviados.

En la subsección 5.5 se realizará una discusión detallada de las capacidades de MPI para trabajar con tipos de datos derivados.

5.1.5. Topologías virtuales

Como se señaló anteriormente, las operaciones de transferencia de datos emparejadas se pueden realizar entre cualquier proceso del mismo comunicador, y todos los procesos del comunicador participan en una operación colectiva. En este sentido, la topología lógica de las líneas de comunicación entre procesos tiene la estructura de un gráfico completo (independientemente de la presencia de canales de comunicación físicos reales entre procesadores).

Al mismo tiempo (y esto ya se señaló en la Sección 3), para la presentación y posterior análisis de una serie de algoritmos paralelos, es aconsejable tener una representación lógica de la red de comunicación existente en forma de determinadas topologías.

MPI tiene la capacidad de representar múltiples procesos en la forma rejas dimensión arbitraria (ver subsección 5.7). En este caso, los procesos límite de las celosías se pueden declarar vecinos y, por lo tanto, en función de las celosías, estructuras del tipo toro.

Además, MPI cuenta con herramientas para generar topologías lógicas (virtuales) de cualquier tipo requerido. En la subsección 5.7 se realizará una discusión detallada de las capacidades de MPI para trabajar con topologías.

Y finalmente, una última serie de notas antes de empezar a analizar MPI:

  • Las descripciones de funciones y todos los ejemplos de programas proporcionados se presentarán en el lenguaje algorítmico C; Las características del uso de MPI para el lenguaje algorítmico Fortran se darán en la sección 5.8.1.
  • En la sección 5.8.2 se analizará una breve descripción de las implementaciones disponibles de las bibliotecas MPI y una descripción general del entorno de ejecución de los programas MPI.
  • La presentación principal de las capacidades de MPI se centrará en la versión 1.2 del estándar ( MPI-1); Las propiedades adicionales del estándar versión 2.0 se presentarán en la cláusula 5.8.3.

Al comenzar a estudiar MPI, se puede observar que, por un lado, MPI es bastante complejo: el estándar MPI prevé la presencia de más de 125 funciones. Por otro lado, la estructura de MPI está cuidadosamente pensada: el desarrollo de programas paralelos puede comenzar después de considerar sólo 6 funciones de MPI. Todas las funciones adicionales de MPI se pueden dominar a medida que aumenta la complejidad de los algoritmos y programas desarrollados. Es en este estilo, desde lo simple hasta lo complejo, que se presentará todo el material educativo sobre MPI.

5.2. Introducción al desarrollo de programas paralelos utilizando MPI

5.2.1. Conceptos básicos del IPM

Presentemos el conjunto mínimo requerido de funciones MPI, suficiente para el desarrollo de programas paralelos bastante simples.

5.2.1.1 Inicialización y terminación de programas MPI

Primera función llamada MPI debería ser una función:

int MPI_Init (int *agrc, char ***argv);

para inicializar el entorno de ejecución del programa MPI. Los parámetros de la función son el número de argumentos en la línea de comando y el texto de la línea de comando misma.

Última función llamada MPI debe ser una función:

int MPI_Finalize(nulo);

Como resultado, se puede observar que la estructura de un programa paralelo desarrollado utilizando MPI debe tener la siguiente forma:

#incluye "mpi.h" int main (int argc, char *argv) (<программный код без использования MPI функций>MPI_Init(&agrc, &argv);<программный код с использованием MPI функций>MPI_Finalize();<программный код без использования MPI функций>devolver 0; )

Cabe señalar:

  1. Archivo mpi.h contiene definiciones de constantes con nombre, prototipos de funciones y tipos de datos de la biblioteca MPI,
  2. Funciones MPI_Inicio Y MPI_Finalizar son obligatorios y deben ser ejecutados (y solo una vez) por cada proceso del programa paralelo,
  3. antes de la llamada MPI_Inicio La función se puede utilizar. MPI_Inicializado para determinar si se ha realizado una llamada previamente MPI_Inicio.

Los ejemplos de funciones discutidos anteriormente dan una idea de la sintaxis para nombrar funciones en MPI. El nombre de la función está precedido por el prefijo MPI, seguido de una o más palabras del nombre, la primera palabra del nombre de la función comienza con un carácter mayúscula y las palabras están separadas por un guión bajo. Los nombres de las funciones MPI, por regla general, explican el propósito de las acciones realizadas por la función.

Cabe señalar:

  • Comunicador MPI_COMM_WORLD, como se señaló anteriormente, se crea de forma predeterminada y representa todos los procesos del programa paralelo que se está ejecutando,
  • Rango obtenido usando la función MPI_Comm_rank, es el rango del proceso que realizó la llamada a esta función, es decir variable ProcRank Tomará diferentes valores en diferentes procesos.
  • Tutorial

En esta publicación hablaremos sobre cómo organizar el intercambio de datos usando MPI usando el ejemplo de la biblioteca Intel MPI. Creemos que esta información será de interés para cualquiera que quiera familiarizarse con el campo de la computación paralela de alto rendimiento en la práctica.

Proporcionaremos una breve descripción de cómo se organiza el intercambio de datos en aplicaciones paralelas basadas en MPI, así como enlaces a fuentes externas con una descripción más detallada. En la parte práctica, encontrará una descripción de todas las etapas del desarrollo de la aplicación de demostración MPI "Hello World", desde la configuración del entorno necesario hasta el lanzamiento del programa en sí.

MPI (interfaz de paso de mensajes)

MPI es una interfaz de paso de mensajes entre procesos que realizan la misma tarea. Está destinado principalmente a sistemas de memoria distribuida (MPP) a diferencia de, por ejemplo, OpenMP. Un sistema distribuido (clúster), por regla general, es un conjunto de nodos informáticos conectados por canales de comunicación de alto rendimiento (por ejemplo, InfiniBand).

MPI es el estándar de interfaz de datos más común para programación paralela. La estandarización de MPI la lleva a cabo el Foro MPI. Existen implementaciones de MPI para la mayoría de las plataformas, sistemas operativos e idiomas modernos. MPI se utiliza ampliamente para resolver diversos problemas en física computacional, productos farmacéuticos, ciencia de materiales, genética y otros campos del conocimiento.

Desde el punto de vista de MPI, un programa paralelo es un conjunto de procesos que se ejecutan en diferentes nodos informáticos. Cada proceso se genera a partir del mismo código de programa.

La operación principal en MPI es el paso de mensajes. MPI implementa casi todos los patrones de comunicación básicos: punto a punto, colectivo y unilateral.

Trabajar con MPI

Veamos un ejemplo en vivo de cómo se estructura un programa MPI típico. Como aplicación de demostración, tomemos el código fuente de ejemplo que viene con la biblioteca Intel MPI. Antes de ejecutar nuestro primer programa MPI, debemos preparar y configurar un entorno de trabajo para los experimentos.

Configurar un entorno de clúster

Para los experimentos, necesitaremos un par de nodos informáticos (preferiblemente con características similares). Si no tienes dos servidores a mano, siempre puedes utilizar los servicios en la nube.

Para la demostración, elegí el servicio Amazon Elastic Compute Cloud (Amazon EC2). Amazon ofrece a los nuevos usuarios un año de prueba gratuito de servidores de nivel básico.

Trabajar con Amazon EC2 es intuitivo. Si tienes alguna duda, puedes consultar la documentación detallada (en inglés). Si lo desea, puede utilizar cualquier otro servicio similar.

Creamos dos servidores virtuales en funcionamiento. En la consola de administración seleccione Servidores Virtuales EC2 en la Nube, entonces Instancia de lanzamiento("Instancia" significa una instancia de servidor virtual).

El siguiente paso es seleccionar el sistema operativo. La biblioteca Intel MPI es compatible con Linux y Windows. Para conocer por primera vez MPI, elegiremos OS Linux. Elegir Red Hat Enterprise Linux 6.6 de 64 bits o SLES11.3/12.0.
Elegir Tipo de instancia(tipo de servidor). Para experimentos, nos conviene t2.micro (1 vCPU, 2,5 GHz, familia de procesadores Intel Xeon, 1 GiB de RAM). Como usuario registrado recientemente, podría usar este tipo de forma gratuita, marcado como "Elegible para el nivel gratuito". Nos fijamos Número de instancias: 2 (número de servidores virtuales).

Después de que el servicio nos solicite ejecutar Instancias de lanzamiento(servidores virtuales configurados), guardamos las claves SSH que serán necesarias para comunicarnos con los servidores virtuales desde el exterior. El estado de los servidores virtuales y las direcciones IP para la comunicación con los servidores informáticos locales se pueden monitorear en la consola de administración.

Punto importante: en la configuración. Red y seguridad / Grupos de seguridad Necesitamos crear una regla que abra puertos para conexiones TCP; esto es necesario para el administrador de procesos MPI. La regla podría verse así:

Tipo: Regla TCP personalizada
Protocolo: TCP
Rango de puertos: 1024-65535
Fuente: 0.0.0.0/0

Por razones de seguridad, puedes establecer una regla más estricta, pero para nuestra demostración esto es suficiente.

Y por último, una breve encuesta sobre posibles temas para futuras publicaciones sobre informática de alto rendimiento.

Sólo los usuarios registrados pueden participar en la encuesta. , Por favor.

Funciones básicas de MPI

La tecnología de programación más común para sistemas paralelos con memoria distribuida es actualmente MPI (Message Passing Interface). La principal forma en que los procesos paralelos interactúan entre sí en dichos sistemas es el paso de mensajes. Básicamente, MPI es una biblioteca y un entorno de ejecución para programas paralelos en C o Fortran. Este tutorial describirá ejemplos de programas en lenguaje C.

Inicialmente, MPI permite el uso del modelo de programación MIMD (Instrucciones múltiples y datos múltiples): muchos flujos de instrucciones y datos, es decir. combinando diferentes programas con diferentes datos. Pero la programación para un modelo de este tipo en la práctica resulta demasiado compleja, por lo que generalmente se usa el modelo SIMD (Single Program Multiple Data): un programa y muchos flujos de datos. Aquí, un programa paralelo se escribe de tal manera que sus diferentes partes puedan realizar simultáneamente su parte de la tarea, logrando así el paralelismo. Dado que todas las funciones MPI están contenidas en la biblioteca, al compilar un programa paralelo será necesario vincular los módulos correspondientes.

En MPI, un programa paralelo se entiende como un conjunto de procesos que se ejecutan simultáneamente. Los procesos se pueden ejecutar en diferentes procesadores, pero también se pueden ubicar varios procesos en el mismo procesador (en este caso, se ejecutan en modo de tiempo compartido). Al ejecutar un programa MPI en un clúster, cada uno de sus nodos ejecutará su propia copia del programa, realizando su parte de la tarea, de lo que se deduce que un programa paralelo es un conjunto de procesos que interactúan, cada uno de los cuales trabaja en su propia dirección. espacio. En el caso extremo, se puede utilizar un solo procesador para ejecutar un programa paralelo; por regla general, este método se utiliza para comprobar inicialmente la corrección del programa paralelo.

La cantidad de procesos y la cantidad de procesadores utilizados se determinan en el momento en que se inicia el programa paralelo utilizando el entorno de ejecución del programa MPI y no pueden cambiar durante los cálculos. Todos los procesos del programa están numerados secuencialmente del 0 al np-1, donde np es el número total de procesos. El número de proceso se llama rango de proceso.

Los procesos paralelos interactúan entre sí enviando mensajes. Hay dos tipos de métodos de envío (se llaman comunicaciones): colectivo y punto a punto. En las comunicaciones colectivas, un proceso envía la información necesaria simultáneamente a todo un grupo de procesos; también existe un caso más general cuando, dentro de un grupo de procesos, la información se transfiere de cada proceso a cada uno. Las comunicaciones más simples son las comunicaciones punto a punto, donde un proceso envía información a otro o ambos intercambian información. Las funciones de comunicación son las funciones principales de la biblioteca MPI. Además, las funciones requeridas son las funciones de inicialización y terminación de MPI: MPI_Init y MPI_Finalize. MPI_Init debe llamarse al principio de los programas y MPI_Finalize al final. Todas las demás funciones MPI deben llamarse entre estas dos funciones.

¿Cómo sabe un proceso qué parte del cálculo debe realizar? Cada proceso que se ejecuta en el clúster tiene su propio número único: rango. Una vez que un proceso conoce su rango y el número total de procesos, puede determinar su participación en el trabajo. Para ello existen funciones especiales en MPI: MPI_Comm_rank y MPI_Comm_size. MPI_Comm_rank devuelve un rango entero del proceso que lo llamó y MPI_Comm_size devuelve el número total de procesos en ejecución.

Los procesos del programa de usuario paralelo que se está depurando se combinan en grupos. En MPI, se entiende por comunicador un objeto de servicio especialmente creado que combina un grupo de procesos y una serie de parámetros adicionales (contexto) utilizados al realizar operaciones de transferencia de datos. Un comunicador que se crea automáticamente cuando se inicia el programa e incluye todos los procesos en el clúster se llama MPI_COMM_WORLD. Durante los cálculos, se pueden crear nuevos grupos de procesos y comunicadores y se pueden eliminar grupos de procesos y comunicadores existentes. Un mismo proceso puede pertenecer a diferentes grupos y comunicadores. Las operaciones colectivas se aplican simultáneamente a todos los procesos del comunicador, por lo que para ellos uno de los parámetros siempre será el comunicador.

Al realizar operaciones de paso de mensajes en funciones MPI, es necesario especificar el tipo de datos que se envían. MPI contiene un gran conjunto de tipos de datos básicos basados ​​en los tipos de datos estándar del lenguaje C. Además, el programador puede construir sus propios tipos de datos utilizando funciones especiales de MPI. A continuación se muestra una tabla de mapeo para tipos de datos básicos.

Constantes MPI tipo de datos en lenguaje C
MPI_INT firmado entero
MPI_UNSIGNED entero sin firmar
MPI_SHORT firmado entero
MPI_LONG firmado int largo
MPI_UNSIGNED_SHORT entero sin firmar
MPI_UNSIGNED_LONG int largo sin firmar
MPI_FLOAT flotar
MPI_DOBLE doble
MPI_LONG_DOUBLE doble largo
MPI_UNSIGNED_CHAR carácter sin firmar
MPI_CHAR carácter firmado

Ejemplo de inicio de la biblioteca MPI: inicio de sesión de estudiante, contraseña s304.

#incluir

#incluir

int principal (int argc, char *argv)

/* Inicialización de MPI */

MPI_Init(&argc, &argv);

/* obteniendo el rango del proceso */

MPI_Comm_rank (MPI_COMM_WORLD, & rango);

/* obtenemos el número total de procesos */

MPI_Comm_size (MPI_COMM_WORLD, &tamaño);

printf("Hola mundo desde el proceso %d de %d\n", rango, tamaño);

/* completar MPI */

Para la compilación se utilizan un compilador y un vinculador. Línea de comando mpcc. (ver mpicc….- ayuda)

Cada proceso en ejecución debe mostrar su clasificación y el número total de procesos. Intentemos compilar y ejecutar este programa.

$ mpicc hola.c –o hola.o

$ mpicc hola.o –o hola

El archivo de saludo será el archivo ejecutable de ejemplo. Puede ejecutarlo en una máquina y ver que la cantidad de procesadores será igual a 1 y el rango del proceso será 0:

$./hola

Hola mundo del proceso 0 de 1

Cuando se trabaja en un servidor, el comando se utiliza para iniciar mpirun . Tiene dos argumentos principales: el nombre del archivo que contiene las direcciones de los nodos y el número de nodos en los que se ejecutará el programa.

$ mpirun n0-6 –v hosts hola

Hola mundo del proceso 0 de 7

Hola mundo del proceso 3 de 7

Hola mundo del proceso 5 de 7

Hola mundo del proceso 4 de 7

Hola mundo del proceso 2 de 7

Hola mundo del proceso 6 de 7

Hola mundo del proceso 1 de 7

El programa se ejecutará en 7 nodos (incluido el servidor) y las direcciones de estos nodos están en archivo de hosts. La impresión en pantalla se realizaba mediante procesos fuera de orden de sus filas. Esto se debe a que la ejecución de procesos no está sincronizada, pero MPI tiene funciones especiales para sincronizar procesos.

El programa más simple no contiene funciones de transferencia de mensajes. En problemas reales, los procesos necesitan interactuar entre sí. Naturalmente, se dedica tiempo a transmitir mensajes, lo que reduce el coeficiente de paralelización de la tarea. Cuanto mayor sea la velocidad de la interfaz de transferencia de mensajes (por ejemplo, Ethernet de 10 Mb/s y Gigabit Ethernet), menores serán los costos de transferencia de datos. Porque el tiempo para el intercambio de datos entre procesos es mucho (en órdenes de magnitud) más largo que el tiempo de acceso a su propia memoria, la distribución del trabajo entre procesos debe ser "gruesa" y deben evitarse transferencias de datos innecesarias.

Entre los problemas del análisis numérico, hay muchos problemas cuya paralelización es obvia. Por ejemplo, la integración numérica en realidad se reduce a (numerosos) cálculos de la función integrando (que naturalmente se confía a procesos individuales), mientras que el proceso principal controla el proceso de cálculo (determina la estrategia para distribuir los puntos de integración entre procesos y recopila sumas parciales). . Los problemas de buscar y ordenar en una lista lineal, encontrar numéricamente raíces de funciones, buscar extremos de una función de muchas variables, calcular series y otros tienen una paralelización similar. En esta práctica de laboratorio veremos dos algoritmos paralelos para calcular π.

Cálculo del número π por método de integración numérica.

Se sabe que

Reemplazando el cálculo de la integral con una suma finita, tenemos , donde n es el número de secciones de suma durante la integración numérica. El área de cada sección se calcula como el producto del ancho de la 'franja' y el valor de la función en el centro de la 'franja', luego las áreas se suman mediante el proceso principal (se forma una cuadrícula uniforme usado).

Obviamente, es fácil paralelizar este problema si cada proceso calcula su suma parcial y luego pasa el resultado del cálculo al proceso principal. ¿Cómo puedo evitar cálculos repetidos aquí? El proceso debe conocer su rango, el número total de procesos y el número de intervalos en los que se dividirá el segmento (cuantos más intervalos, mayor será la precisión). Luego, en un ciclo de 1 al número de intervalos, el proceso calculará el área de la tira en el intervalo i-ésimo y luego no pasará al siguiente intervalo i+1, sino al intervalo i+m. , donde m es el número de procesos. Como ya sabemos, existen funciones para obtener el rango y número total de procesos. MPI_Comm_rank Y MPI_Comm_size . Antes de comenzar los cálculos, el proceso principal debe transmitir el número de intervalos a todos los demás, y después de los cálculos, recopilar las sumas parciales recibidas de ellos y resumirlas en MPI, esto se implementa pasando mensajes; Es conveniente utilizar aquí la función de interacción colectiva para enviar mensajes. MPI_Bcast , que envía los mismos datos de un proceso a todos los demás. Para cobrar importes parciales hay 2 opciones: puede utilizar MPI_Reunir , que recopila datos de todos los procesos y se los entrega a uno (se obtiene una matriz de m elementos, donde m es el número de procesos) o MPI_Reducir . MPI_Reducir actúa de manera similar MPI_Reunir – recopila datos de todos los procesos y se los entrega a uno, pero no en forma de matriz, sino que primero realiza una determinada operación entre los elementos de la matriz, por ejemplo, la suma, y ​​luego entrega un elemento. Para esta tarea parece más conveniente utilizar MPI_Reducir . El texto del programa se proporciona a continuación.

#incluir "mpi.h"

#incluir

#incluir

doble f(doble a)

retorno (4.0 / (1.0 + a*a));

int principal(int argc, char *argv)

int n, myid, numprocs, i;

doble PI25DT = 3,141592653589793238462643;

doble mypi, pi, h, suma, x;

doble hora de inicio, hora de finalización;

MPI_Init(&argc,&argv);

MPI_Comm_size(MPI_COMM_WORLD,&numprocs);

MPI_Comm_rank(MPI_COMM_WORLD,&myid);

hora de inicio = MPI_Wtime();

MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);

h = 1,0 / (doble)n;

para (i = miid + 1; yo<= n; i += numprocs)

x = h * ((doble)i - 0,5);

MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);

printf("pi es aproximadamente %.16f, el error es %.16f\n",

pi, fábricas(pi - PI25DT));

tiempo final = MPI_Wtime();

printf("hora del reloj de pared = %f\n",

hora de finalización-hora de inicio);

Echemos un vistazo más de cerca a las llamadas a funciones. MPI_Bcast Y MPI_Reducir :

MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD): el contenido de la variable n y un elemento de tipo MPI_INT de un proceso con rango 0 se envían a todos los demás procesos (MPI_COMM_WORLD: todos los procesos en el comunicador) en la misma variable n . Luego de esta llamada, cada proceso sabrá el número total de intervalos. Al final de los cálculos, MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD) suma (el parámetro MPI_SUM) los valores de las variables mypi de tipo MPI_DOUBLE de cada proceso y escribe el resultado en el proceso. variable pi con rango 0. Para medir el tiempo de cálculo, el proceso principal utiliza la función MPI_Wtime.

doble MPI_Wtime();

MPI_Wtime devuelve el número de segundos en formato de punto flotante, que representa el tiempo transcurrido desde que se inició el programa.

Calcular el número π usando el método de Monte Carlo

Para calcular el valor de π, puede utilizar el método de 'disparo'. Aplicado a este caso, el método consiste en generar puntos distribuidos uniformemente en un área bidimensional y determinar .

El valor de π calculado de esta manera es aproximado; en general, la precisión del cálculo del valor deseado aumenta con el número de "disparos" y la calidad del generador de números aleatorios; Se utilizan métodos similares cuando es difícil hacer una estimación numérica precisa.

El algoritmo paralelo para calcular el número π usando este método es en muchos aspectos similar al algoritmo anterior que consideramos. Para generar números aleatorios, es necesario utilizar la función srand, que establece el rango del proceso como argumento (secuencia inicial), de modo que cada proceso tenga su propia secuencia.

#incluir

void srand(semilla sin firmar);

La función srand() establece el número de semilla para la secuencia generada por la función rand().

#incluir

int rand(nulo);

La función rand() genera una secuencia de números pseudoaleatorios. Cada vez que se llama a la función, se devuelve un número entero entre cero y el valor RAND_MAX.

Para recopilar el resultado aquí, también es conveniente utilizar MPI_Reduce con una operación de suma (MPI_SUM), luego dividir la suma resultante por el número de procesadores, obteniendo el promedio aritmético.

int MPI_Reduce(void* sendbuf, void* recvbuf, int recuento,

MPI_Datatype tipo de datos, MPI_Op op, int root, MPI_Comm comm);

Parámetros:

dirección sendbuf del búfer de envío

recvbuf dirección del buffer de recepción

operación de reducción de operaciones

comunicador de comunicaciones

int MPI_Bcast(void *buffer, int count, tipo de datos MPI_Datatype, int root,

comunicación MPI_Com);

Parámetros:

dirección del búfer del búfer de envío/recepción

contar el número de elementos en el búfer de envío (entero)

tipo de datos tipo de datos de envío de elementos del buffer

número de proceso principal raíz (entero)

comunicador de comunicaciones

Ejercicio: de acuerdo con el número de opción, compila y ejecuta un programa paralelo que calcula el número π usando un algoritmo dado.

Ejecute una tarea en un nodo y desde un clúster en una cantidad específica de nodos. Evalúe el tiempo de cálculo, la precisión y el coeficiente de paralelización de Amdahl teniendo en cuenta el retraso de la red de forma teórica y en función de los resultados del trabajo.

Opciones de tarea

Opción No. Algoritmo Número de procesadores Número de iteraciones en cada procesador.
Integración numérica
Montecarlo
Integración numérica
Montecarlo
Integración numérica
Montecarlo
Integración numérica
Montecarlo
Integración numérica
Montecarlo
Integración numérica
Montecarlo
Integración numérica
Montecarlo
Integración numérica
Montecarlo
Integración numérica
Montecarlo
Integración numérica
Montecarlo

· Planteamiento del problema, opción.

· Texto de un programa paralelo en lenguaje C según el encargo.

· Resultados de ejecutar el programa en un nodo, tiempo de ejecución ti, resultado del cálculo, error.

· Resultados de ejecutar el programa en el servidor, tiempo de ejecución, resultados de cálculo, error.

· Describir el algoritmo paralelo, los flujos de información durante la ejecución del programa y la carga de la memoria caché del nodo. Calcule el coeficiente de Amdahl - K j según los resultados del programa.

· Teniendo en cuenta los resultados del trabajo de un grupo de estudiantes, construya un histograma de la dependencia de K j , t i del número de procesadores que participan en los cálculos.




Arriba