UNIVERSIDAD DE MENDOZA
FACULTAD DE INGENIERÍA
INGENIERÍA EN INFORMÁTICA
“GENERACIÓN DE MALLAS DE ELEMENTOS FINITOS EN
PARALELO”
Autor:
Emiliano López
Asesores:
Mg. Ing. Juan José Ciarlante
Dr. Ing. Carlos García Garino
RESUMEN
PROGAM es un programa muy viejo, escrito en FORTRAN IV, que genera mallas estructuradas de elementos finitos. Este trabajo consiste en el desarrollo de una nueva versión del programa, denominada PROGAMP, diseñada para correr en un cluster
beowulf. Como primer paso se realizó una nueva implementación del PROGAM en
Fortran 90, que sirvió como punto de partida para programar la versión paralela, la cual utiliza la biblioteca MPI para implementar el paralelismo. Utilizando 20 procesadores, el PROGAMP generó una malla de elementos finitos 13 veces más rápido que el PROGAM, lo que equivale a una eficiencia del 65%. Los clusters beowulf representan una alternativa sumamente económica a la hora de implementar una máquina paralela. Desde el punto de vista del rendimiento los resultados obtenidos con el PROGAMP son muy buenos. Sin embargo, antes de paralelizar un determinado algoritmo, es necesario estudiar qué partes del mismo son concurrentes y cuáles no, ya que en muchos casos un enfoque paralelo no es la mejor solución.
ÍNDICE GENERAL
I. INTRODUCCIÓN...4 II. MARCO TEÓRICO Y ANTECEDENTES...7 III. DESARROLLO DE INGENIERÍA...28 IV. RESULTADOS...40 V. CONCLUSIONES...47 VI. REFERENCIAS...49 VII. CÓDIGO FUENTE...51INTRODUCCIÓN
La programación de algoritmos complejos con un enfoque secuencial, es cada vez menos factible debido a problemas en la eficiencia de cómputo. El desarrollo de la computación de alto rendimiento ha dado lugar a un crecimiento sostenido de la demanda de potencia de cálculo, que no es posible satisfacer con arquitecturas basadas en esquemas secuenciales. Frente a este problema existen dos alternativas. Una de ellas se basa en el uso de computadoras con arquitecturas paralelas, que tienen por lo general un costo prohibitivo para la mayoría de las organizaciones. La otra opción, mucho más económica, consiste en implementar clusters beowulf, los cuales están formados por un conjunto de PCs interconectadas en red, y coordinadas por un software que permite hacerlas trabajar como si se tratara de una sola máquina paralela.
Este Trabajo Final trata de la extensión a entornos paralelos del Programa de Generación Automática de Mallas (PROGAM), un software que genera mallas
estructuradas de elementos finitos. El método de elementos finitos (MEF) es un procedimiento numérico para resolver ecuaciones diferenciales, ampliamente utilizado en el campo de la física y la ingeniería.
El trabajo fue realizado en el Laboratorio de Producción Integrada por Computadora (LAPIC), ubicado en la sede de la carrera Redes y Telecomunicaciones del ITU (UNCuyo). Dicho laboratorio cuenta con un cluster beowulf para realizar trabajos de investigación vinculados a la computación de alto rendimiento.
Objetivos
1. Realizar una nueva implementación del PROGAM en Fortran 90, ya que el programa original está escrito en FORTRAN IV y sólo se dispone de una versión
impresa del código fuente. 2. Realizar una implementación paralela del PROGAM para ser ejecutada en un cluster beowulf, utilizando la biblioteca de paso de mensajes MPI. 3. Estudiar el rendimiento del programa paralelo poniendo especial énfasis en la comparación con la versión secuencial. 4. Dotar al LAPIC de un generador de mallas estructuradas de elementos finitos que sea de utilidad para los trabajos de investigación que allí se realizan.
EL MÉTODO DE ELEMENTOS FINITOS
El método de elementos finitos (MEF) es un procedimiento numérico para resolver ecuaciones diferenciales, ampliamente utilizado en el campo de la física y la ingeniería.
El concepto fundamental del MEF es que cualquier función continua, como temperatura, presión, o desplazamiento, puede ser aproximada por un modelo discreto formado por un conjunto de funciones continuas definidas sobre un número finito de subdominios.
Construcción del modelo discreto
El modelo discreto se construye de la siguiente manera: 1. Se identifica un número finito de puntos en el dominio. Estos puntos son llamados nodos. 2. El valor de la función continua en cada punto nodal debe ser determinado. 3. El dominio es dividido en un número finito de subdominios denominados elementos. Estos elementos están conectados por los nodos y juntos aproximan la forma del dominio. 4. La función continua es aproximada en cada elemento por un polinomio que se define utilizando los valores de la función en los nodos que corresponden al elemento. Para cada elemento se define un polinomio diferente, pero los polinomios se deben seleccionar de manera tal que se mantenga la continuidad a lo largo de las fronteras del elemento.El concepto fundamental quedará más claro usando como ejemplo la distribución de temperatura en una barra, como se muestra en la Figura 1. La función continua es T(x) y el dominio es el intervalo 0,L a lo largo del eje x. Se identifican cinco puntos en el dominio
(Figura 2a), estos puntos son los nodos, y no es necesario que sean equidistantes. Se podría haber definido más de cinco puntos, pero con estos es suficiente para ilustrar los conceptos básicos. Se calcula en cada nodo el valor de T(x), esto se muestra gráficamente en la Figura 2b.
Figura 1: Distribución de temperatura en
una barra.
Figura 2: Definición de los puntos nodales.
La división del dominio en elementos se puede realizar de dos maneras. Podemos limitar cada elemento a dos nodos, obteniendo cuatro elementos (Figura 3a), o podemos dividir el dominio en dos elementos, cada uno con tres nodos (Figura 3b). El polinomio en
cada elemento se define utilizando los valores de T(x) en los puntos nodales. Si subdividimos la región en cuatro elementos habrá dos nodos por elemento y la función elemental será lineal. La aproximación final de T(x) consistirá de cuatro funciones continuas lineales, cada una definida sobre un elemento (Figura 4a).
Figura 3: División del dominio en elementos.
La división del dominio en dos elementos permite que los polinomios elementales sean de segundo grado. La aproximación final de T(x) en este caso serán dos funciones continuas cuadráticas (Figura 4b).
Figura 4: Aproximación final de T(x). Generalmente la distribución de temperatura se desconoce, y se desea determinar los valores de la función en ciertos puntos. El procedimiento es igual al que se describió anteriormente pero con un paso adicional. Se define un conjunto de nodos y los valores T1, T2, T3, ..., son ahora variables desconocidas. El dominio es dividido en elementos, y se define en cada uno de ellos una ecuación de temperatura. Los valores nodales de T(x) deben ser ajustados de manera que provean la mejor aproximación posible a la distribución real. El ajuste se realiza minimizando alguna función asociada al problema físico. Cuando se consideran problemas de transferencia de calor, se minimiza un funcional relacionado con la ecuación diferencial que gobierna el problema. El proceso de minimización produce un sistema de ecuaciones algebraicas lineales que puede ser resuelto para obtener los valores nodales de T(x).
Los elementos en un dominio bidimensional son funciones de x e y, y generalmente son triángulos o cuadriláteros. La función elemental puede ser un plano (Figura 5), o una superficie curva (Figura 6). El plano está asociado al mínimo número de nodos por elemento, que es tres para el triángulo y cuatro para el cuadrilátero.
Figura 5: Elementos planos en un dominio
bidimensional.
La función elemental puede ser una superficie curva si se agregan más nodos, además, esto permite que los elementos tengan fronteras curvas. La aproximación final de la función continua x , y será una colección de superficies, cada una definida sobre un elemento utilizando los valores de x , y en los puntos nodales.
Figura 6: La función elemental es una superficie
definir la función elemental es un aspecto importante del MEF. Esta propiedad permite que la función elemental sea definida independientemente de la posición final del elemento en el modelo conectado, e independientemente de las otras funciones elementales.
Generación de la malla de elementos finitos
La generación de la malla de elementos finitos constituye el punto 3 en el proceso de construcción del modelo discreto, presentado al comienzo de este capítulo. El resto de los puntos están fuera del alcance de este Trabajo Final, y sólo fueron descriptos para ubicar al lector en un contexto adecuado. La malla de elementos finitos es el resultado de la división del dominio de una función en subregiones o elementos. Para esto el ingeniero debe decidir el número, tamaño y forma de los elementos que utilizará para modelar el cuerpo real. En las zonas en donde la función a aproximar varía con rapidez (gradientes altos) se utilizan elementos pequeños para lograr una buena aproximación, mientras que en las zonas donde la función es relativamente constante, se puede incrementar el tamaño de los elementos para reducir el esfuerzo computacional. La generación de la malla se completa con la numeración de los nodos y los elementos.
Este trabajo se ocupa de la división de dominios bidimensionales en elementos triangulares o cuadrangulares lineales, es decir que los elementos a utilizar serán el triángulo de tres nodos y el cuadrilátero de cuatro nodos. La división de cualquier dominio bidimensional debe comenzar con la división del cuerpo en macroregiones triangulares o cuadrangulares, de acuerdo con la geometría, las propiedades físicas del material y/o la carga aplicada. Estas regiones son luego divididas en triángulos o cuadriláteros, siendo necesario en el primer caso dividir la región en cuadriláteros para luego dividir cada cuadrilátero en dos triángulos. La Figura 7 muestra la
división de una región cuadrangular en elementos triangulares.
Figura 7: División de una región cuadrangular en
COMPUTACIÓN PARALELA
A continuación se introducen los conceptos generales sobre procesamiento paralelo en clusters tipo beowulf, y se describe la interfaz de comunicaciones MPI, el software que permite el intercambio de mensajes entre los procesos en un ambiente multiprocesador.
El cluster beowulf
Un cluster beowulf está formado por un conjunto de PCs conectadas mediante una LAN, con una infraestructura de software libre (GNU/Linux). El hardware utilizado es el que se encuentra disponible en el mercado común, y puede ser tan simple como dos máquinas compartiendo un sistema de archivos o tan complejo como 1024 nodos conectados mediante una red de alta velocidad. El rendimiento es proporcional a la cantidad de nodos instalados, pudiéndose agregar o quitar nodos según las necesidades.
Desde el punto de vista arquitectónico un cluster beowulf es un sistema de memoria distribuida MIMD (multipleinstruction, multipledata). Este sistema consta de múltiples procesadores independientes, es decir que no comparten la memoria principal sino que cada uno tiene su propia memoria, y colaboran entre sí intercambiando mensajes mediante una red de comunicaciones. Así, cada procesador ejecuta su propia secuencia de instrucciones y accede a sus propios datos, almacenados ambos en su memoria local. Eventualmente los procesadores pueden intercambiar datos mediante el envío de mensajes a través de la red.
Paralelismo y concurrencia
Se dice que dos partes de un programa se procesan en forma concurrente, si cada una de ellas puede ser ejecutada en forma independiente de la otra. En cambio decimos
que se ejecutan en paralelo, si ambas partes son procesadas al mismo tiempo, siendo necesario contar para esto con dos procesadores. Podemos concluir entonces, en que la concurrencia es una característica lógica, en tanto el paralelismo es una característica física. Por lo tanto para que una tarea pueda ser ejecutada en forma paralela debe ser concurrente. Esto implica que a la hora de plantear la solución a un problema, debemos determinar qué partes del mismo son concurrentes y cuáles no, lo cual hace que no todos los programas sean “paralelizables”.
La interfaz de pasaje de mensajes MPI
El método más usado en la programación de sistemas de memoria distribuida MIMD es el pasaje de mensajes, o alguna variante de éste. Básicamente, los procesos coordinan sus actividades enviando y recibiendo mensajes.MPI es un estándar desarrollado por el Message Passing Interface Forum (MPIF), con el objetivo de crear una interfaz de programación para el intercambio de mensajes entre procesos. Dado que MPI es un estándar abierto, existen diversas implementaciones, para distintos lenguajes y arquitecturas, siendo las más difundidas MPICH y LAM/MPI. Además, un mismo programa puede ser compilado tanto para ejecutarse en un cluster beowulf, como en una computadora con una arquitectura paralela nativa, sin hacerle modificaciones. La biblioteca MPI es una API que oculta al programador los detalles del manejo de las comunicaciones entre procesos, ofreciendo de este modo un entorno transparente para desarrollar programas paralelos. Las dos funciones más elementales que provee son MPI_SEND y MPI_RECV, para enviar y recibir un mensaje respectivamente; a continuación se muestra la sintaxis de estas subrutinas en Fortran:
MPI_SEND (buffer, count, dataype, destination, tag, communicator, ierr) MPI_RECV (buffer, count, datatype, source, tag, communicator, status, ierr)
Todos los parámetros son enteros, a excepción de buffer, que puede ser de cualquier tipo, dependiendo del tipo de datos a transmitir. El parámetro ierr es un argumento de salida que cumple la función de valor de retorno, indicando si hubo o no error durante la ejecución de la rutina. MPI le asigna a cada proceso en ejecución un identificador único denominado rank, quedando los procesos numerados entre 0 y p1, donde p es el número total de procesos. Cada proceso obtiene su rank como sigue: CALL MPI_COMM_RANK(MPI_COMM_WORLD, my_rank, ierr) De esta manera el rank del proceso quedará guardado en la varialble my_rank. Para ilustrar el uso de estas funciones, supongamos que el proceso 0 quiere enviar al proceso 1 el vector x, compuesto por diez números reales. Para esto debe llamar a la subrutina MPI_SEND como sigue: CALL MPI_SEND (x, 10, MPI_REAL, 1, 0, MPI_COMM_WORLD, ierr) Para recibir el mensaje, el proceso 1 debe llamar a MPI_RECV de la siguiente manera: CALL MPI_RECV (x, 10, MPI_REAL, 0, 0, MPI_COMM_WORLD, status, ierr) Nótese que la instrucciones ejecutadas por el proceso 0 (MPI_SEND) serán diferentes de las ejecutadas por el proceso 1 (MPI_RECV). Sin embargo esto no significa que los programas deban ser diferentes. Simplemente podemos incluir la siguiente estructura condicional en nuestro código: CALL MPI_COMM_RANK (MPI_COMM_WORLD, my_rank, ierr)
IF (my_rank == 0) THEN CALL MPI_SEND (x, 10, MPI_REAL, 1, 0, & MPI_COMM_WORLD, ierr) ELSE IF (my_rank == 1) THEN CALL MPI_RECV (x, 10, MPI_REAL, 0, 0, & MPI_COMM_WORLD, status, ierr) END IF Esta forma de programar sistemas MIMD es llamada SPMD (singleprogram multiple data). El efecto de ejecutar programas diferentes se logra mediante el uso de estructuras condicionales en el código. Este es el enfoque más común utilizado en la programación de sistemas MIMD. Al comenzar la ejecución en un esquema del tipo SPMD, se envía una copia del programa a cada uno de los procesadores. Así, cada copia del programa constituye un proceso. Por ejemplo, para ejecutar un programa utilizando la biblioteca MPICH2, se utiliza un comando desde la consola del sistema operativo denominado mpiexec, al cual se le dan como argumentos el nombre del ejecutable y la cantidad de procesos que se desea crear.
PROGRAMA DE GENERACIÓN AUTOMÁTICA DE MALLAS
(PROGAM)
El Programa de Generación Automática de Mallas (PROGAM), escrito en Fortran IV por Carlos García Garino y C. Ortiz Andino, se basa en el programa GRID publicado por L.J. Segerlind, y genera datos de elementos para distintos programas de elementos finitos. Su función consiste en dividir un dominio bidimensional en elementos cuadrangulares y/o triangulares lineales.
Características del programa
El programa es capaz de modelar dominios bidimensionales complejos que se obtienen como la unión de regiones más sencillas llamadas macroelementos, cuyas fronteras responden a curvas de segundo grado (Figura 8).
Dentro de cada una de estas regiones se generan, mediante interpolación isoparamétrica, los nodos y elementos de la malla. Los elementos generados pueden ser triángulos o cuadriláteros lineales y se contempla la posibilidad de generar cuadriláteros en algunas regiones y triángulos en otras.
Cada región se modela mediante un cuadrilátero de 8 nodos, que puede utilizarse como rectángulo (Figura 9), cuadrilátero general o triángulo (Figura 10). Para este cuadrilátero se define un sistema de coordenadas naturales , que tiene su origen en el centro del mismo, y donde −11 y −11 . Una región triangular se modela alineando dos lados del cuadrilátero general.
Figura 9: Región cuadrangular.
.
Figura 8: Un dominio triangular modelado como la unión de
Figura 10: Región triangular.
Para dividir una región en elementos, el PROGAM realiza el siguiente proceso:
Se parte de la región normalizada definida por el sistema de coordenadas , . Para esta región se generan n filas por m columnas de nodos equidistantes; n y m son datos suministrados por el usuario.
Cada nodo generado en el sistema , se mapea en el sistema x , y empleando la siguiente interpolación isoparamétrica: x=
∑
i=1 8 iXi y=∑
i =1 8 iYi 1=−0.25∗1−∗1− ∗ 1 2=0.5∗1−2∗1− 3=0.25∗1∗1− ∗− −1 4=0.5∗1∗1−2 5=0.25∗1∗1 ∗ −16=0.5∗1−2∗1
7=0.25∗1−∗1∗ −−1
8=0.5∗1−∗1−2
Los vectores X e Y contienen las coordenadas en el plano x , y de los ochos nodos que definen la región, comenzando por la esquina inferior izquierda y procediendo en sentido antihorario. Una vez obtenidas las coordenada (x, y) de los nodos, se le debe asignar a cada uno un número entero para su identificación. Es importante aclarar que esta numeración es global, es decir que abarca a todas las regiones. Además las regiones vecinas comparten los nodos ubicados sobre la frontera común, por lo tanto se debe realizar un control para determinar si los nodos ubicados en las fronteras, han sido numerados previamente, durante el procesamiento de la región vecina. Si el control es positivo, los nodos sobre la frontera analizada conservan la numeración existente. A continuación se numeran secuencialmente los nodos de la región, comenzando por el ubicado en las coordenadas =−1 y =1 , y procediendo de izquierda a derecha y de arriba hacia abajo. Los nodos previamente numerados se saltean. Finalmente se definen los elementos de la región como vectores de tres o cuatro enteros, según sean triángulos o cuadriláteros. Cada vector contiene los números de los nodos que forman el elemento.
Problema de ejemplo
Para ilustrar el procedimiento presentado en la sección anterior, se usará el dominio triangular de la figura 8, el cual se subdivide en una región cuadrilátera y dos triángulos. Las coordenadas (x, y) de los nodos que definen las regiones son datos de entrada suministrados por el usuario. La numeración de los mismos es arbitraria. Los nodos 4 y 6 se desplazan hacia el nodo 5 para obtener elementos más pequeños cerca de la esquina.Para la región 2 se elige una subdivisión de cinco filas y cinco columnas. Esta selección fija el número de filas en la región 3 en cinco y el número de columnas en la región 1 en cinco, ya que las regiones vecinas comparten los nodos ubicados sobre la frontera.
Elementos más grandes se obtienen en las regiones 1 y 3 colocando sólo tres filas de nodos en la región 1, y tres columnas en la región 3. La región 3 será subdividida en elementos cuadrangulares, mientras que para las restantes se utilizarán elementos triangulares.
A continuación se detalla el procedimiento realizado para la generación de los elementos en la región 3, definida por los nodos 1, 2, 3, 18, 13, 15, 16 y 17; para las otras dos regiones se sigue un proceso similar:
1) Se generan en la región normalizada definida por el sistema , cinco filas por tres columnas de nodos (Figura 11).
Figura 11: Generación de
los nodos en la región normalizada. 2) Se calculan las coordenadas (x, y) de los nodos, aplicando para cada uno las fórmulas de la interpolación isoparamétrica (Figura 12). Figura 12: Resultado de la interpolación isoparamétrica. 3) Se numeran los nodos comenzando por el extremo superior izquierdo y procediendo de izquierda a derecha y de arriba hacia abajo. Los nodos ubicados en los bordes comunes son numerados por la región que tenga el menor número, es decir que los nodos ubicados en el borde derecho fueron numerados durante el procesamiento de la región 2, y deben saltearse (Figura 13).
Figura 13: Numeración de los nodos en la región 3. La
secuencia comienza por el número 36 porque los menores a éste fueron asignados en las regiones 1 y 2. Los valores de la frontera derecha fueron asignados durante el procesamiento de la región 2.
4) Finalmente se obtienen los elementos (Figura 14).
Figura 14: Elementos generados dentro de la
Una vez procesadas las tres regiones, se combinan los resultados para formar la malla de elementos finitos (Figura 15).
Figura 15: Malla de elementos finitos generada con el
LA NUEVA IMPLEMENTACIÓN DEL PROGAM
La versión original del PROGAM sólo está disponible en papel, junto con una gran cantidad de documentación acerca de su funcionamiento. Por alguna razón que desconozco, la versión digital del programa se perdió, y para poder comenzar con este trabajo necesitaba un PROGAM ejecutable. Una posible solución a este problema consistía en escanear el código fuente y pasarlo por un OCR. Esto hubiera tomado mucho tiempo, ya que como el OCR no es perfecto, se tendría que revisar todo el código para corregir los errores. Además, el código no es estructurado y contiene muchas instrucciones goto, lo que dificulta su comprensión y estudio.
Decidí entonces realizar una nueva implementación en Fortran 90, analizando la documentación técnica y el código fuente existente. El nuevo programa me serviría como base para desarrollar el PROGAM paralelo, y para comparar el rendimiento de la solución paralela con el de la secuencial. A continuación describiré los aspectos principales de esta nueva implementación.
La estructura de datos region_t
Toda la información referida a una determinada región del dominio está contenida en una estructura de datos, del tipo region_t. la cual está definida en el archivo region.f90.TYPE región_t ! Número de región INTEGER :: num ! Número de filas y columnas de nodos INTEGER :: n, m ! Tipo de elemento (3=triangular, 4=cuadrangular) INTEGER :: elem_type ! Datos de conectividad INTEGER :: vecinas(4) ! Los 8 ocho nodos que definen la región REAL :: def_nodes(8,2) ! Coordenadas (x,y) de los nodos REAL, ALLOCATABLE :: node_coords(:,:,:) ! Números de los nodos INTEGER, ALLOCATABLE :: node_nums(:,:) ! Cada elemento es un vector de 3 o 4 enteros INTEGER, ALLOCATABLE :: elements(:,:) END TYPE region_t Número de región: Las regiones se numeran en forma secuencial a partir de 1. Número de filas y columnas: Definen la cantidad de nodos en la región y determinan la cantidad de elementos: (n1)(m1) elementos cuadrangulares o 2(n1)(m1) elementos triangulares. Tipo de elemento: Los únicos elementos admitidos son el triángulo de tres nodos (elem_type = 3) y el cuadrado de 4 nodos (elem_type = 4). Datos de conectividad: El vector vecinas contiene los números de las regiones vecinas inferior, derecha, superior e izquierda respectivamente. Un cero en cualquiera de sus elementos indica que no hay ninguna región en la frontera correspondiente.
Definición de la región: La matriz def_nodes contiene las coordenadas (x, y) de los ocho nodos que definen la región, comenzando por el del extremo inferior izquierdo y siguiendo en sentido antihorario.
Los campos descriptos hasta acá contienen datos acerca de la geometría de la región y deben ser suministrados por el usuario. Los campos que siguen contendrán la información de los elementos generados por el programa.
Coordenadas de los nodos: En la matriz node_coords se guardarán las coordenadas
(x, y) de los nodos generados. El tamaño de la matriz será igual a 2nm.
Números de los nodos: La matriz node_nums contendrá los números enteros asignados a los nodos para su identificación.
Elementos: Un elemento está representado por un vector que contiene los números de los nodos que lo definen, por lo tanto el tamaño de la matriz elements será igual a 3 *
cantidad_de_elementos ó 4 * cantidad_de_elementos, según se trate de triángulos o
cuadriláteros.
Funcionamiento del programa
Para generar una malla de elementos finitos, el PROGAM ejecuta una serie de funciones en forma secuencial (Figura 16). Estas funciones están implementadas como subrutinas en el archivo region.f90, y son llamadas desde el archivo progam.f90. Lectura del archivo de entrada: Consiste en la inicialización de las estructuras de datos correspondientes a cada región con la información contenida en el archivo de entrada. Esto lo realiza la subrutina region_input, que devuelve un vector del tipo region_t, donde cada elemento corresponde a una región del dominio.
Figura 16: Diagrama de
flujo del PROGAM secuencial.
Generación de los nodos en cada región: La subrutina region_grid calcula las coordenadas (x,y) de los nodos que formarán los elementos, aplicando la interpolación isoparamétrica presentada en el capítulo anterior. Recibe como argumento una estructura
region_t y guarda los resultados en el la matriz node_coords. Es llamada una vez por
cada región contenida en el vector devuelto por la subrutina region_input.
Numeración de los nodos: La subrutina region_nodenums numera los nodos dentro de la región que recibe como primer argumento. El segundo parámetro, denominado nstart,
indica el primer número disponible (recordemos que esta numeración es global). Si la región limita con otra identificada por un número de región menor, entonces a los nodos ubicados sobre la frontera común se les coloca los números asignados durante la numeración de nodos de la región vecina. Los números de los nodos se guardan en la matriz node_nums.
Generación de los elementos: Esta función está implementada en la subrutina
region_elements. Por cada elemento se define un vector de 3 o 4 enteros, según se trate de triángulos o cuadriláteros. Este vector contiene los números de los nodos que forman el elemento. Los vectores de todos los elementos de la región se agrupan en la matriz elements. Escritura del archivo de salida: La subrutina region_output escribe los resultados en un archivo de texto, de acuerdo al formato requerido por GiD (el programa utilizado para visualizar la malla).
PARALELIZACIÓN DEL PROGAM
Para generar una malla de elementos finitos, el PROGAM paralelo (PROGAMP) utiliza un procesador del cluster por cada región del dominio. Supongamos que tenemos un dominio compuesto por cuatro regiones (Figura 17). En este caso, se ejecutarán cuatro procesos PROGAMP en sendos nodos del cluster (Figura 18). Cada proceso generará una malla de elementos finitos dentro de la región que le fue asignada (Figura 19). Finalmente, uniendo los resultados de los cuatro procesos obtendremos la malla completa (Figura 20).
Figura 17: Dominio compuesto por
Figura 18: Para resolver el problema en
paralelo, se necesitan cuatro procesadores.
Figura 19: Cada procesador resuelve una
Figura 20: Finalmente, se
unifican los resultados.
La figura 21 ilustra el diagrama de flujo del PROGAMP. Los bloques en gris representan funciones que requieren comunicación entre procesos. Los bloques en blanco representan subrutinas que utilizan datos de una única región, y su funcionalidad es independiente de los otros procesos, es por esto que su implementación no difiere de la versión secuencial. A continuación describiré la implementación de los bloques grises.
Lectura del archivo de entrada
Este bloque está implementado en la subrutina region_input_p. El proceso 0 es el único que tiene habilitado el canal de entrada estándar. Debe leer los datos de entrada y enviarlos a los procesos correspondientes. La matriz que contiene las coordenadas de los nodos que definen las regiones es enviada a todos los procesos mediante la rutina MPI_BCAST, el proceso receptor debe determinar cuáles corresponden a la región que le toca procesar. Los datos propios de cada región (número de filas, número de columnas, etc) son transmitidos mediante llamadas a MPI_SEND y MPI_RECV al proceso correspondiente.
Figura 21: Diagrama de flujo del PROGAM-P resolviendo un problema de tres regiones.
Las líneas rojas indican el pasaje de mensajes entre procesos. Para no complicar el gráfico, no se muestra la comunicación entre los procesos 0 y 2.
Numeración de los nodos
La subrutina region_nodenums_p asigna un número único a cada nodo de la región que recibe como argumento. El proceso que se sigue es similar al seguido por la rutina
figura 8, el proceso encargado de la región 1 numera los nodos compartidos entre ésta y la región 2. Una vez completada la numeración envía los números al proceso encargado de la región 2 mediante una llamada a MPI_SEND. Este último debe llamar a MPI_RECV para obtener los números de los nodos de su frontera superior, y a MPI_SEND para enviar los números de su frontera izquierda al proceso encargado de la región 3.
Escritura del archivo de salida
La escritura de los resultados está implementada en la subrutina region_output_p. La información de salida se escribe en un único archivo de texto (output.msh), de acuerdo al formato requerido por GiD. Para esto deben resolverse dos problemas fundamentales: 1. Acceso al archivo : Todos los procesos deben escribir en el mismo archivo, por lo tanto éste deberá ubicarse en un sistema de archivos compartido, como NFS. 2. Sincronización : para que la salida sea ordenada el proceso n no podrá comenzar a escribir hasta que el proceso n1 haya terminado. Este problema se resuelve mediante la transmisión de un turno, el cual está inicialmente en posesión del proceso 0. El siguiente pseudocódigo ilustra el procedimiento propuesto:IF (my_rank > 0) CALL MPI_RECV(my_rank1, turno) Abrir archivo. Escribir información de salida. Cerrar archivo. IF (my_rank < max_rank) CALL MPI_SEND(my_rank+1, turno)
RENDIMIENTO
El cluster del LAPIC
El cluster en el que se realizaron las pruebas está compuesto por 13 computadoras con procesador Intel Pentium IV 3GHz HT, 1GB de memoria RAM y sistema operativo GNU/Linux, interconectadas mediante una red Gigabit Ethernet.
La tecnología Hyper Threading (HT) de los procesadores Intel permite ejecutar en forma paralela dos procesos en un mismo procesador. Por lo tanto podemos correr en el cluster hasta 26 procesos simultáneamente. Para la comunicación entre los procesos se utilizó la biblioteca MPICH2, una de las implementaciones existentes del estándar MPI2.0. El PROGAM y el PROGAMP fueron compilados con el Intel Fortran Compiler 9.0.
El perfil NACA23012
Con el fin de ilustrar una aplicación más compleja, que suele ser de interés en problemas de fluidos, escogí el perfil NACA 23012 para mostrar el funcionamiento y las prestaciones del PROGAM, y comparar el rendimiento de la versión paralela con el de la versión secuencial. Como es bien conocido en el caso de fluidos, es importante modelar adecuadamente la capa límite, para lo cual se debe refinar convenientemente la malla alrededor del perfil, es decir que alrededor del mismo se necesitan elementos muy pequeños. En la figura 22 se muestra una malla simple de 1066 elementos, modelada con el PROGAMP a partir de un dominio compuesto por 20 regiones (se ejecutaron 20 procesos).
Figura 22: Malla compuesta por 1066 elementos.
La figura 23 muestra una malla compuesta por 11438 elementos, generada a partir del mismo dominio que la anterior.
Speedup
Se denomina speedup al cociente entre el tiempo que tarda el programa secuencial T y el tiempo que tarda el programa paralelo T en resolver un mismo problema. El speedup depende del número de procesadores (p) utilizados para resolver el problema en paralelo y del tamaño del problema (n). En nuestro caso n será igual a la cantidad de elementos a generar. S n , p= Tn Tn , p ; 0S n , p≤ pEficiencia
La eficiencia del programa paralelo se determina dividiendo el speedup por el número de procesadores utilizados. E n , p=S n , p p = Tn pTn , p ; 0E n , p≤1 Si la eficiencia alcanza el 100% (E = 1 y S = p) se dice que el programa presentaspeedup lineal, ya que S será función lineal del tamaño del problema n. Esta es una
situación ideal que en la práctica no se consigue, debido a la cantidad de trabajo realizado por el programa paralelo que no es realizado por el programa secuencial. Esto incluye el costo de comunicación (determinado por la latencia y el ancho de banda de la red), el tiempo ocioso (un proceso esperando que otro le envíe algún valor necesario para poder continuar su ejecución), y los cálculos extra necesarios para resolver el problema en paralelo.
Rendimiento del PROGAMP
Para evaluar el rendimiento del PROGAMP medí los tiempos que tardan las versiones paralela y secuencial en calcular mallas de elementos finitos alrededor del perfil NACA23012, comenzando con un número pequeño de elementos, e incrementando geométricamente este valor hasta alcanzar los 14 millones de elementos aproximadamente. Esta prueba está programada en los fuentes test.f90 de ambas
implementaciones, pudiendo usarse cualquier dominio para realizarla. Al medir el tiempo de ejecución no se tienen en cuenta las operaciones de entrada y salida. En la figura 24 se muestra un gráfico con los resultados, hecho con Gnuplot. También se muestran algunos valores aproximados de speedup y eficiencia (Tabla 1). En el gráfico se observa que la solución paralela disminuye notablemente el tiempo de ejecución, esto también se percibe cuando uno se encuentra frente a la consola del sistema ejecutando el programa.
Los valores de speedup obtenidos permiten afirmar que el programa paralelo se ejecuta unas 13 veces más rápido que el secuencial, lo que equivale a una eficiencia del 65% aproximadamente.
Figura 24: Comparación entre los tiempos que tardan el PROGAM y el PROGAM-P en resolver el perfil NACA23012 a medida que se incrementa la cantidad de elementos.
n S(n,20) E(n,20)
2 millones 13 0,65
6 millones 16,6 0,83
10 millones 13,6 0,68
14 millones 12 0,60
Tabla 1: Valores aproximados de speedup y eficiencia obtenidos mediante la observación del gráfico anterior.
CONCLUSIONES
Los clusters beowulf representan una forma sumamente económica de implementar un entorno paralelo, ya que están constituidos por hardware disponible en el mercado de computadoras de escritorio y software libre. Los resultados obtenidos con el PROGAMP muestran que se disminuye notablemente el tiempo de ejecución al correr el programa en un cluster de este tipo.Sin embargo, a la hora de diseñar un algoritmo para resolver un determinado problema, es necesario analizar qué partes del mismo se pueden ejecutar en forma concurrente y cuáles no, y realizar una estimación del speedup. Esta medida nos dirá qué tanto más rápido se ejecutará la versión paralela que la secuencial (en algunos casos es posible que la solución paralela sea más lenta), y así poder decidir se conviene o no implementar una solución paralela. El PROGAMP cuenta con una limitación muy seria que deberá ser resuelta en el futuro, y es que se crea un proceso por cada región del dominio, sin importar la cantidad de elementos que se deban generar en cada una de ellas. Esto hace que el balance de carga entre los nodos del cluster no sea el óptimo. Además, en el caso de tener más regiones que procesadores, la biblioteca MPICH les asignará a algunos procesadores más de un proceso, desbalanceando aún más la carga.
REFERENCIAS
1. Fortran, http://es.wikipedia.org/wiki/Fortran
2. García Garino, Carlos y Ortiz Andino, C., 1984, “Descripción del Programa de
Generación Automática de Mallas (PROGAM). Nota Técnica Nº 21”, IMPSA,
Argentina. 3. GiD – The personal pre and postprocessor, http://gid.cimne.upc.es 4. gnuplot hompage, http://www.gnuplot.info 5. Intel Fortran Compiler for Linux, http://www.intel.com/support/performancetools/fortran/linux/ 6. León, Oscar A. y García Garino, Carlos , “Programación Paralela con MPI” 7. Message Passing Interface Forum, http://www.mpiforum.org 8. MPICH2, http://wwwunix.mcs.anl.gov/mpi/mpich2/ 9. Rocks Cluster Distribution, http://www.rocksclusters.org 10. Segerlind, L. J., 1976, “Applied Finite Element Analysis”, Wiley, Estados Unidos. 11. The Beowulf Cluster Site, http://www.beowulf.org
PROGAM SECUENCIAL
progam.f90
1 ! $Id: progam.f90,v 1.2 2006-07-16 23:07:26 emiliano Exp $ 2 ! PROGAM - Programa de Generación Automática de Mallas 3
4 PROGRAM progam_s 5 USE region_mod 6 IMPLICIT NONE 7
8 TYPE(region_t), ALLOCATABLE, TARGET :: regions(:)
9 INTEGER :: i, n_regions, nstart 10
11 ! Procesamiento de la entrada de datos 12 CALL region_input(regions)
13 n_regions = SIZE(regions) 14
15 ! Divide cada región en elementos cuadrangulares 16 DO i=1, n_regions
17 CALL region_grid(regions(i)) 18 END DO
19
20 ! Asigna un número único a cada nodo 21 nstart = 1
22 DO i=1, n_regions
23 CALL region_nodenums(regions(i), nstart, regions) 24 END DO
25
26 ! Genera los elementos 27 DO i=1, n_regions
28 CALL region_elements(regions(i)) 29 END DO
30
31 ! Salida para GiD
32 CALL region_output(regions) 33
34 END PROGRAM progam_s
region.f90
1 ! $Id: region.f90,v 1.2 2006-07-16 21:51:41 emiliano Exp $ 2 ! PROGAM - Programa de Generación Automática de Mallas 3 4 MODULE region_mod 5 !DEC$ REAL:8 6 IMPLICIT NONE 7 8 TYPE region_t
9 INTEGER :: num ! número de región
10 INTEGER :: n, m ! cantidad de filas y columnas 11 INTEGER :: elem_type ! 3=triángulo, 4=cuadrado 12 INTEGER :: vecinas(4) ! datos de conectividad
13 REAL :: def_nodes(8,2) ! los 8 nodos que definen la región 14 REAL, ALLOCATABLE :: node_coords(:,:,:) ! coordenadas de los nodos
15 INTEGER, ALLOCATABLE :: node_nums(:,:) ! números de los nodos
16 INTEGER, ALLOCATABLE :: elements(:,:) ! cada elemento es un vector de 3 o 4 enteros 17 END TYPE region_t
18
19 CONTAINS
20 ! Procesamiento de la entrada de datos 21 SUBROUTINE region_input(regions)
22 TYPE(region_t), TARGET, ALLOCATABLE, INTENT(OUT) :: regions(:) 23 REAL, ALLOCATABLE :: input_nodes(:,:)
26 INTEGER :: node_num, i 27
28 READ(*,*) n_regions, n_input_nodes
29 ALLOCATE(regions(n_regions), input_nodes(n_input_nodes,2)) 30
31 DO i=1, n_input_nodes
32 READ(*,*) node_num, input_nodes(i,:) 33 END DO 34 35 DO i=1, n_regions 36 READ(*,*) regions(i)%n,regions(i)%m,regions(i)%elem_type,regions(i)%vecinas,node_nums 37 regions(i)%num = i 38 regions(i)%def_nodes = input_nodes(node_nums,:) 39 END DO
40 END SUBROUTINE region_input 41
42 ! Divide la región en (n-1)*(m-1) elementos cuadrangulares. 43 SUBROUTINE region_grid(this)
44 TYPE(region_t), INTENT(INOUT) :: this 45 INTEGER :: i, j
46 REAL :: pun(8) ! funciones de forma 47 REAL :: eta, si ! coordenadas normalizadas 48 REAL :: deta, dsi ! incrementos de eta y si 49 50 ALLOCATE(this%node_coords(this%n,this%m,2)) 51 ALLOCATE(this%node_nums(this%n,this%m)) 52 53 deta = 2./(this%n-1) 54 dsi = 2./(this%m-1) 55 DO i=1, this%n 56 eta = 1 - (i-1)*deta 57 DO j=1, this%m 58 si = -1 + (j-1)*dsi
59 pun(1) = -0.25 * (1-si) * (1-eta) * (si+eta+1) 60 pun(2) = 0.5 * (1-si**2) * (1-eta)
61 pun(3) = 0.25 * (1+si) * (1-eta) * (si-eta-1) 62 pun(4) = 0.5 * (1+si) * (1-eta**2)
63 pun(5) = 0.25 * (1+si) * (1+eta) * (si+eta-1) 64 pun(6) = 0.5 * (1-si**2) * (1+eta)
65 pun(7) = 0.25 * (1-si) * (1+eta) * (eta-si-1) 66 pun(8) = 0.5 * (1-si) * (1 - eta**2)
67 this%node_coords(i,j,1) = SUM(this%def_nodes(:,1) * pun) 68 this%node_coords(i,j,2) = SUM(this%def_nodes(:,2) * pun) 69 END DO
70 END DO
71 END SUBROUTINE region_grid 72
73 ! Numera en forma global los nodos de la región, teniendo en cuenta los que 74 ! ya han sido numerados por otras regiones. La variable nstart contiene el 75 ! primer número de la secuencia, y devuelve el primer número de la siguiente 76 ! region.
77 SUBROUTINE region_nodenums(this, nstart, regions) 78 TYPE(region_t), TARGET, INTENT(INOUT) :: this 79 INTEGER, INTENT(INOUT) :: nstart 80 TYPE(region_t), INTENT(IN) :: regions(:)
81 INTEGER :: i, j, istart, jstart, iend, jend 82 INTEGER, POINTER :: bound_nums(:)
83 REAL :: chk_node(2), recv_node(2) 84 85 istart=1 86 jstart=1 87 iend=this%n 88 jend=this%m 89
90 ! Verifica si los nodos de las fronteras son numerados por otra región 91 IF (this%vecinas(1)<this%num .AND. this%vecinas(1)/=0) iend=iend-1 92 IF (this%vecinas(2)<this%num .AND. this%vecinas(2)/=0) jend=jend-1 93 IF (this%vecinas(3)<this%num .AND. this%vecinas(3)/=0) istart=istart+1 94 IF (this%vecinas(4)<this%num .AND. this%vecinas(4)/=0) jstart=jstart+1 95
96 ! Numera los nodos 97 DO i=istart, iend
98 DO j=jstart, jend 99 this%node_nums(i,j) = nstart 100 nstart = nstart+1 101 END DO 102 END DO 103
104 ! Numera las fronteras que no han sido numeradas 105 DO i=1, 4
106 SELECT CASE(i) 107 CASE(1)
108 bound_nums => this%node_nums(this%n,:) ! abajo 109 chk_node = this%node_coords(this%n,1,:)
110 CASE(2)
111 bound_nums => this%node_nums(:,this%m) ! derecha 112 chk_node = this%node_coords(1,this%m,:)
113 CASE(3)
114 bound_nums => this%node_nums(1,:) ! arriba 115 chk_node = this%node_coords(1,1,:)
116 CASE(4)
117 bound_nums => this%node_nums(:,1) ! izquierda 118 chk_node = this%node_coords(1,1,:)
119 END SELECT 120
121 IF (this%vecinas(i)<this%num .AND. this%vecinas(i)/=0) THEN
122 CALL region_bound(regions(this%vecinas(i)), this%num, bound_nums, recv_node) 123 IF (ANY(chk_node/=recv_node)) CALL invertir(bound_nums)
124 END IF 125 END DO
126 END SUBROUTINE region_nodenums 127
128 ! Devuelve los números de nodos que están en la frontera con una región específica. 129 SUBROUTINE region_bound(this, vecina, bound_nums, chk_node)
130 TYPE(region_t), INTENT(IN) :: this ! región origen 131 INTEGER, INTENT(IN) :: vecina ! región destino
132 INTEGER, INTENT(OUT) :: bound_nums(:) ! vector en el que se devuelven los números 133 REAL, INTENT(OUT) :: chk_node(2) ! coordenadas de uno de los extremos 134
135 ! No uso select case porque require expresiones constantes. 136 IF (vecina==this%vecinas(1)) THEN
137 bound_nums = this%node_nums(this%n,:) 138 chk_node = this%node_coords(this%n,1,:) 139 ELSE IF (vecina==this%vecinas(2)) THEN 140 bound_nums = this%node_nums(:,this%m) 141 chk_node = this%node_coords(1,this%m,:) 142 ELSE IF (vecina==this%vecinas(3)) THEN 143 bound_nums = this%node_nums(1,:) 144 chk_node = this%node_coords(1,1,:) 145 ELSE IF (vecina==this%vecinas(4)) THEN 146 bound_nums = this%node_nums(:,1) 147 chk_node = this%node_coords(1,1,:) 148 END IF
149 END SUBROUTINE region_bound 150
151 ! Invierte el orden de los elementos en el vector. 152 SUBROUTINE invertir(vector)
153 INTEGER, INTENT(INOUT) :: vector(:) 154 INTEGER :: aux(SIZE(vector)) 155 INTEGER :: i, n 156 157 n = SIZE(vector) 158 DO i=1, n 159 aux(n-i+1) = vector(i) 160 END DO 161 vector = aux
162 END SUBROUTINE invertir 163
164 ! Genera elementos triangulares (dividiendo los cuadrados según la diagonal más 165 ! corta del elemento (1,1)) o cuadrangulares. Cada elemento es un vector formado 166 ! por los números de los nodos que lo definen.
170 REAL :: diag1, diag2 171
172 IF (this%elem_type == 3) THEN ! elementos triangulares 173 ALLOCATE(this%elements((this%n-1)*(this%m-1)*2, 3)) 174 k = 1
175 diag1 = SQRT((this%node_coords(1,1,1)-this%node_coords(2,2,1))**2 & 176 + (this%node_coords(1,1,2)-this%node_coords(2,2,2))**2) 177 diag2 = SQRT((this%node_coords(2,1,1)-this%node_coords(1,2,1))**2 & 178 + (this%node_coords(2,1,2)-this%node_coords(1,2,2))**2) 179
180 IF (diag1 < diag2) THEN 181 DO i=1, this%n-1 182 DO j=1, this%m-1 183 this%elements(k,1) = this%node_nums(i,j) 184 this%elements(k,2) = this%node_nums(i+1,j) 185 this%elements(k,3) = this%node_nums(i+1,j+1) 186 k = k+1 187 this%elements(k,1) = this%node_nums(i,j) 188 this%elements(k,2) = this%node_nums(i+1,j+1) 189 this%elements(k,3) = this%node_nums(i,j+1) 190 k = k+1 191 END DO 192 END DO 193 ELSE 194 DO i=1, this%n-1 195 DO j=1, this%m-1 196 this%elements(k,1) = this%node_nums(i,j) 197 this%elements(k,2) = this%node_nums(i+1,j) 198 this%elements(k,3) = this%node_nums(i,j+1) 199 k = k+1 200 this%elements(k,1) = this%node_nums(i+1,j) 201 this%elements(k,2) = this%node_nums(i+1,j+1) 202 this%elements(k,3) = this%node_nums(i,j+1) 203 k = k+1 204 END DO 205 END DO 206 END IF 207
208 ELSE IF (this%elem_type == 4) THEN ! elementos cuadrangulares 209 ALLOCATE(this%elements((this%n-1)*(this%m-1), 4)) 210 k = 1 211 DO i=1, this%n-1 212 DO j=1, this%m-1 213 this%elements(k,1) = this%node_nums(i,j) 214 this%elements(k,2) = this%node_nums(i+1,j) 215 this%elements(k,3) = this%node_nums(i+1,j+1) 216 this%elements(k,4) = this%node_nums(i,j+1) 217 k = k+1 218 END DO 219 END DO 220 END IF
221 END SUBROUTINE region_elements 222
223 ! Salida para GiD
224 SUBROUTINE region_output(regions)
225 TYPE(region_t), INTENT(IN) :: regions(:)
226 INTEGER :: i, j, k, elem_num, node_num 227 228 elem_num = 1 229 node_num = 0 230 OPEN(UNIT=1, FILE='output.msh') 231 232 DO k=1, SIZE(regions) 233 ! Cabecera 234 IF (regions(k)%elem_type==3) THEN
235 WRITE(1,*) 'MESH dimension 2 ElemType Triangle Nnode 3' 236 ELSE IF (regions(k)%elem_type==4) THEN
237 WRITE(1,*) 'MESH dimension 2 ElemType Quadrilateral Nnode 4' 238 END IF
239
240 ! Coordenadas de los nodos 241 WRITE(1,*) 'coordinates'
242 WRITE(1,*) '# num x y' 243 DO i=1, regions(k)%n 244 DO j=1, regions(k)%m
245 IF (regions(k)%node_nums(i,j)>node_num) THEN 246 node_num = regions(k)%node_nums(i,j)
247 WRITE(1,*) node_num, regions(k)%node_coords(i,j,:) 248 END IF
249 END DO 250 END DO
251 WRITE(1,*) 'end coordinates' 252
253 ! Elementos
254 WRITE(1,*) 'elements'
255 WRITE(1,*) '# num nodo1 nodo2 nodo3 nodo4' 256 DO i=1, SIZE(regions(k)%elements, DIM=1) 257 WRITE(1,*) elem_num, regions(k)%elements(i,:) 258 elem_num = elem_num+1
259 END DO
260 WRITE(1,*) 'end elements' 261 WRITE(1,*)
262 END DO
263 END SUBROUTINE region_output 264 END MODULE region_mod
test.f90
1 ! $Id: test.f90,v 1.1.1.1 2006-07-16 21:18:46 emiliano Exp $ 2 ! PROGAM - Programa de Generación Automática de Mallas 3
4 ! Esta prueba genera sucesivamente mallas de elementos finitos, 5 ! aumentando la cantidad de elementos geométricamente. Para cada 6 ! malla se toma el tiempo de cálculo, sin considerar la entrada 7 ! ni la salida de datos. 8 9 PROGRAM progam_s_test 10 USE region_mod 11 IMPLICIT NONE 12
13 TYPE(region_t), ALLOCATABLE, TARGET :: regions(:)
14 INTEGER :: i, n_regions, nstart 15 REAL :: ti, tf
16 INTEGER :: ierr, k
17 INTEGER, ALLOCATABLE :: n_init(:), m_init(:) 18
19 ! Procesamiento de la entrada de datos 20 CALL region_input(regions) 21 n_regions = SIZE(regions) 22 23 ALLOCATE(n_init(n_regions), m_init(n_regions)) 24 n_init = regions%n 25 m_init = regions%m 26 27 DO k=1, 100 28 regions%n = n_init*k 29 regions%m = m_init*k 30 31 CALL CPU_TIME(ti)
32 ! Divide cada región en elementos cuadrangulares 33 DO i=1, n_regions
34 CALL region_grid(regions(i)) 35 END DO
36
37 ! Asigna un número único a cada nodo 38 nstart = 1
39 DO i=1, n_regions
40 CALL region_nodenums(regions(i), nstart, regions) 41 END DO
45 CALL region_elements(regions(i)) 46 END DO
47 CALL CPU_TIME(tf) 48
49 WRITE(*,*) DOT_PRODUCT(regions%n, regions%m), tf-ti 50 DO i=1, n_regions 51 DEALLOCATE(regions(i)%node_coords) 52 DEALLOCATE(regions(i)%node_nums) 53 DEALLOCATE(regions(i)%elements) 54 END DO 55 END DO 56
PROGAM PARALELO
progam.f90
1 ! $Id: progam.f90,v 1.2 2006-07-17 00:52:06 emiliano Exp $
2 ! PROGAM-P - Programa de Generacin Automática de Mallas en Paralelo 3 4 PROGRAM progam_p 5 !DEC$ REAL:8 6 USE region_mod 7 IMPLICIT NONE 8 9 TYPE(region_t) :: region_local 10 INTEGER :: ierr 11 12 CALL MPI_INIT(ierr) 13
14 CALL region_init_mod() ! Inicialización 15 CALL region_input_p(region_local) ! Entrada de datos
16 CALL region_grid(region_local) ! Creación de los nodos interiores 17 CALL region_nodenums_p(region_local) ! Numeración de los nodos
18 CALL region_elements(region_local) ! Creación de los elementos 19 CALL region_output_p(region_local) ! Salida para GiD
20
21 CALL MPI_FINALIZE(ierr) 22
23 END PROGRAM progam_p
region.f90
1 ! $Id: region.f90,v 1.2 2006-07-17 00:52:06 emiliano Exp $ 2 ! PROGAM-P(Programa de Generacin Automática de Mallas en Paralelo) 3 4 MODULE region_mod 5 !DEC$ REAL:8 6 IMPLICIT NONE 7 INCLUDE 'mpif.h' 8 9 TYPE region_t
10 INTEGER :: num ! número de región
11 INTEGER :: n, m ! cantidad de filas y columnas 12 INTEGER :: elem_type ! 3=triángulo, 4=cuadrado 13 INTEGER :: vecinas(4) ! datos de conectividad
14 REAL :: def_nodes(8,2) ! los 8 nodos que definen la región 15 REAL, ALLOCATABLE :: node_coords(:,:,:) ! coordenadas de los nodos
16 INTEGER, ALLOCATABLE :: node_nums(:,:) ! números de los nodos
17 INTEGER, ALLOCATABLE :: elements(:,:) ! cada elemento es un vector de 3 o 4 enteros 18 END TYPE region_t
19
20 INTEGER, PRIVATE :: my_rank, p, status(MPI_STATUS_SIZE), ierr 21
22 CONTAINS
23 ! Inicialización
24 SUBROUTINE region_init_mod()
25 CALL MPI_COMM_RANK(MPI_COMM_WORLD, my_rank, ierr) 26 CALL MPI_COMM_SIZE(MPI_COMM_WORLD, p, ierr) 27 END SUBROUTINE region_init_mod
28
29 ! Procesamiento de la entrada de datos 30 SUBROUTINE region_input_p(this) 31 TYPE(region_t), INTENT(OUT) :: this 32 INTEGER :: i
36 INTEGER :: vecinas(4) 37 INTEGER :: node_nums(8) 38
39 IF (my_rank == 0) THEN
40 READ(*,*) n_regions, n_input_nodes 41 IF (n_regions /= p) THEN
42 WRITE(*,*) 'El número de procesos es distinto del número de regiones.' 43 CALL MPI_ABORT(MPI_COMM_WORLD, 1, ierr)
44 ENDIF 45 ENDIF 46
47 CALL MPI_BCAST(n_regions, 1, MPI_INTEGER, 0, MPI_COMM_WORLD, ierr) 48 CALL MPI_BCAST(n_input_nodes, 1, MPI_INTEGER, 0, MPI_COMM_WORLD, ierr) 49 ALLOCATE(input_nodes(n_input_nodes,2))
50
51 ! Lectura de los nodos y sus coordenadas 52 IF (my_rank == 0) THEN
53 DO i=1, n_input_nodes
54 READ(*,*) node_num, input_nodes(i,:) 55 END DO
56 END IF 57
58 ! Los nodos se envían a todos los procesos, sin importar cuáles necesiten. 59 CALL MPI_BCAST(input_nodes, SIZE(input_nodes), MPI_REAL8, 0, MPI_COMM_WORLD, ierr) 60
61 IF (my_rank == 0) THEN
62 ! El proceso 0 se queda con la primera región.
63 READ(*,*) this%n, this%m, this%elem_type, this%vecinas, node_nums 64 this%def_nodes = input_nodes(node_nums,:)
65
66 ! Lee los datos de las regiones restantes y los envía al proceso 67 ! correspondiente.
68 DO i=2, n_regions
69 READ(*,*) n, m, elem_type, vecinas, node_nums
70 CALL MPI_SEND(n, 1, MPI_INTEGER, i-1, 0, MPI_COMM_WORLD, ierr) 71 CALL MPI_SEND(m, 1, MPI_INTEGER, i-1, 0, MPI_COMM_WORLD, ierr) 72 CALL MPI_SEND(elem_type, 1, MPI_INTEGER, i-1, 0, MPI_COMM_WORLD, ierr) 73 CALL MPI_SEND(vecinas, 4, MPI_INTEGER, i-1, 0, MPI_COMM_WORLD, ierr) 74 CALL MPI_SEND(node_nums, 8, MPI_INTEGER, i-1, 0, MPI_COMM_WORLD, ierr) 75 END DO
76
77 ELSE
78 ! Recibe los datos enviados por el proceso 0.
79 CALL MPI_RECV(this%n, 1, MPI_INTEGER, 0, 0, MPI_COMM_WORLD, status, ierr) 80 CALL MPI_RECV(this%m, 1, MPI_INTEGER, 0, 0, MPI_COMM_WORLD, status, ierr)
81 CALL MPI_RECV(this%elem_type, 1, MPI_INTEGER, 0, 0, MPI_COMM_WORLD, status, ierr) 82 CALL MPI_RECV(this%vecinas, 4, MPI_INTEGER, 0, 0, MPI_COMM_WORLD, status, ierr) 83 CALL MPI_RECV(node_nums, 8, MPI_INTEGER, 0, 0, MPI_COMM_WORLD, status, ierr) 84 this%def_nodes = input_nodes(node_nums,:)
85 END IF 86
87 this%num = my_rank + 1 88
89 END SUBROUTINE region_input_p 90
91 ! Divide la región en (n-1)*(m-1) elementos cuadrangulares. 92 SUBROUTINE region_grid(this)
93 TYPE(region_t), INTENT(INOUT) :: this 94 INTEGER :: i, j
95 REAL :: pun(8) ! funciones de forma 96 REAL :: eta, si ! coordenadas normalizadas 97 REAL :: deta, dsi ! incrementos de eta y si 98 99 ALLOCATE(this%node_coords(this%n,this%m,2)) 100 ALLOCATE(this%node_nums(this%n,this%m)) 101 102 deta = 2./(this%n-1) 103 dsi = 2./(this%m-1) 104 DO i=1, this%n 105 eta = 1 - (i-1)*deta 106 DO j=1, this%m 107 si = -1 + (j-1)*dsi
108 pun(1) = -0.25 * (1-si) * (1-eta) * (si+eta+1) 109 pun(2) = 0.5 * (1-si**2) * (1-eta)
110 pun(3) = 0.25 * (1+si) * (1-eta) * (si-eta-1) 111 pun(4) = 0.5 * (1+si) * (1-eta**2)
112 pun(5) = 0.25 * (1+si) * (1+eta) * (si+eta-1) 113 pun(6) = 0.5 * (1-si**2) * (1+eta)
114 pun(7) = 0.25 * (1-si) * (1+eta) * (eta-si-1) 115 pun(8) = 0.5 * (1-si) * (1 - eta**2)
116 this%node_coords(i,j,1) = SUM(this%def_nodes(:,1) * pun) 117 this%node_coords(i,j,2) = SUM(this%def_nodes(:,2) * pun) 118 END DO
119 END DO
120 END SUBROUTINE region_grid 121
122 ! Numera en forma global los nodos de la región, teniendo en cuenta los que 123 ! ya han sido numerados por otras regiones.
124 SUBROUTINE region_nodenums_p(this)
125 TYPE(region_t), INTENT(INOUT), TARGET :: this 126 INTEGER :: i, j, istart, jstart, iend, jend
127 INTEGER :: nstart ! La numeración comienza a partir de este número. 128 INTEGER :: nend ! Este valor se envía a la siguiente región. 129 INTEGER :: ranks(4) ! Ranks de las regiones vecinas.
130 INTEGER, POINTER :: bound_nums(:)
131 REAL :: chk_node(2), recv_node(2) 132 133 nstart = 1 134 istart = 1 135 jstart = 1 136 iend = this%n 137 jend = this%m 138
139 ! Verifica si los nodos de las fronteras son numerados por otra región. 140 IF (this%vecinas(1)<this%num .AND. this%vecinas(1)/=0) iend=iend-1 141 IF (this%vecinas(2)<this%num .AND. this%vecinas(2)/=0) jend=jend-1 142 IF (this%vecinas(3)<this%num .AND. this%vecinas(3)/=0) istart=istart+1 143 IF (this%vecinas(4)<this%num .AND. this%vecinas(4)/=0) jstart=jstart+1 144
145 ! Obtiene el valor nstart
146 IF (my_rank>0) CALL MPI_RECV(nstart, 1, MPI_INTEGER, my_rank-1, 0, MPI_COMM_WORLD, status, ierr)
147 nend = nstart + (iend-istart+1) * (jend-jstart+1)
148 IF (my_rank<p-1) CALL MPI_SEND(nend, 1, MPI_INTEGER, my_rank+1, 0, MPI_COMM_WORLD, ierr) 149
150 ! Numera los nodos 151 DO i=istart, iend 152 DO j=jstart, jend 153 this%node_nums(i,j) = nstart 154 nstart = nstart+1 155 END DO 156 END DO 157
158 ! Recibe los nodos de las fronteras numeradas por una región vecina. 159 ! El chk_node es para verificar que el vector recibido esté en el orden 160 ! correcto. Los mpi_recv se hacen antes que los mpi_send para contemplar 161 ! el caso de que un nodo ubicado en una esquina, sea numerado por una 162 ! región vecina y deba ser enviado a otra región vecina.
163 ranks = this%vecinas - 1 164 DO i=1, 4
165 SELECT CASE(i) 166 CASE(1)
167 bound_nums => this%node_nums(this%n,:) ! abajo 168 chk_node = this%node_coords(this%n,1,:)
169 CASE(2)
170 bound_nums => this%node_nums(:,this%m) ! derecha 171 chk_node = this%node_coords(1,this%m,:)
172 CASE(3)
173 bound_nums => this%node_nums(1,:) ! arriba 174 chk_node = this%node_coords(1,1,:)
175 CASE(4)
179
180 IF (this%vecinas(i)<this%num .AND. this%vecinas(i)/=0) THEN
181 CALL MPI_RECV(bound_nums, SIZE(bound_nums), MPI_INTEGER, ranks(i), 0, MPI_COMM_WORLD, status, ierr)
182 CALL MPI_RECV(recv_node, 2, MPI_REAL8, ranks(i), 1, MPI_COMM_WORLD, status, ierr) 183 IF (ANY(chk_node/=recv_node)) CALL invertir(bound_nums)
184 END IF 185 END DO 186
187 ! Envía los números de las fronteras a las regiones vecinas que tengan mayor 188 ! número de región.
189 DO i=1, 4 190 SELECT CASE(i) 191 CASE(1)
192 bound_nums => this%node_nums(this%n,:) ! abajo 193 chk_node = this%node_coords(this%n,1,:)
194 CASE(2)
195 bound_nums => this%node_nums(:,this%m) ! derecha 196 chk_node = this%node_coords(1,this%m,:)
197 CASE(3)
198 bound_nums => this%node_nums(1,:) ! arriba 199 chk_node = this%node_coords(1,1,:)
200 CASE(4)
201 bound_nums => this%node_nums(:,1) ! izquierda 202 chk_node = this%node_coords(1,1,:)
203 END SELECT 204
205 IF (this%vecinas(i)>this%num) THEN
206 CALL MPI_SEND(bound_nums, SIZE(bound_nums), MPI_INTEGER, ranks(i), 0, MPI_COMM_WORLD, ierr)
207 CALL MPI_SEND(chk_node, 2, MPI_REAL8, ranks(i), 1, MPI_COMM_WORLD, ierr) 208 END IF
209 END DO
210 END SUBROUTINE region_nodenums_p 211
212 ! Invierte el orden de los elementos en el vector. 213 SUBROUTINE invertir(vector)
214 INTEGER, INTENT(INOUT) :: vector(:) 215 INTEGER :: aux(SIZE(vector)) 216 INTEGER :: i, n 217 218 n = SIZE(vector) 219 DO i=1, n 220 aux(n-i+1) = vector(i) 221 END DO 222 vector = aux
223 END SUBROUTINE invertir 224
225 ! Genera elementos triangulares (dividiendo los cuadrados según la diagonal más 226 ! corta del elemento (1,1)) o cuadrangulares. Cada elemento es un vector formado 227 ! por los números de los nodos que lo definen.
228 SUBROUTINE region_elements(this)
229 TYPE(region_t), INTENT(INOUT) :: this 230 INTEGER :: i, j, k 231 REAL :: diag1, diag2 232
233 IF (this%elem_type == 3) THEN ! elementos triangulares 234 ALLOCATE(this%elements((this%n-1)*(this%m-1)*2, 3)) 235 k = 1
236 diag1 = SQRT((this%node_coords(1,1,1)-this%node_coords(2,2,1))**2 & 237 + (this%node_coords(1,1,2)-this%node_coords(2,2,2))**2) 238 diag2 = SQRT((this%node_coords(2,1,1)-this%node_coords(1,2,1))**2 & 239 + (this%node_coords(2,1,2)-this%node_coords(1,2,2))**2) 240
241 IF (diag1 < diag2) THEN 242 DO i=1, this%n-1 243 DO j=1, this%m-1 244 this%elements(k,1) = this%node_nums(i,j) 245 this%elements(k,2) = this%node_nums(i+1,j) 246 this%elements(k,3) = this%node_nums(i+1,j+1) 247 k = k+1 248 this%elements(k,1) = this%node_nums(i,j)