2. COMPUTACIÓN DISTRIBUIDA. 12 2.1. INTRODUCCIÓN. A la hora de hablar de computación distribuida, aparece el concepto de programación distribuida, que es un modelo de programación enfocado a desarrollar sistemas distribuidos, abiertos, escalables, transparentes y tolerantes a fallos. Casi cualquier lenguaje de programación que tenga acceso al más bajo nivel del hardware del sistema puede manejar la programación distribuida, teniendo en cuenta que hace falta una gran cantidad de tiempo y código. La programación distribuida utiliza alguna de las arquitecturas básicas: cliente-servidor, 3tier, n-tier, objetos distribuidos, etc. Existen lenguajes específicamente diseñados para programación distribuida, como son: Ada, Alef, E, Erlang, Limbo y Oz. El sistema por antonomasia para lograr el cálculo distribuido es la supercomputadora, que es una computadora con capacidades de cálculo muy superiores a las de cualquier ordenador de trabajo convencional. Hoy en día, el diseño de supercomputadoras se sustenta en cuatro importantes tecnologías, de las cuales, las dos primeras que citaremos son las verdaderamente denominadas supercomputadoras. Pasemos a verlas: La tecnología de registros vectoriales, creada por Seymour Cray, considerado el padre de la súper computación, quien inventó y patentó diversas tecnologías que condujeron a la creación de máquinas de computación ultra-rápidas. Esta tecnología permite la ejecución de innumerables operaciones aritméticas en paralelo. El sistema conocido como M.P.P. (Massively Parallel Processors o Procesadores Masivamente Paralelos), que consiste en la utilización de cientos y, a veces, miles de microprocesadores estrechamente coordinados. La tecnología de computación distribuida propiamente dicha: los clusters y los grids, de los que más tarde hablaremos. Por último, el cuasi-súper cómputo o computación de ciclos redundantes, también llamada computación zombi. Recientemente, con el éxito de Internet, han surgido proyectos de computación distribuida a nivel mundial, en los que programas especiales aprovechan el tiempo ocioso de miles de ordenadores personales para realizar grandes tareas. Consiste en que un servidor o grupo de servidores distribuyen trabajo de procesamiento a un grupo de computadoras voluntarias a ceder capacidad de procesamiento no utilizada. A diferencia de las tres últimas categorías, el software que corre en estas plataformas debe ser capaz de dividir las tareas en bloques de cálculo independientes, que no se ensamblarán ni comunicarán durante grandes periodos de tiempo, como pueden ser horas. En esta categoría destacan BOINC y Folding@home. Este tipo de máquinas, generalmente, presenta una arquitectura proyectada y optimizada enteramente para la aplicación final en concreto. El inconveniente de utilizar supercomputadoras es su alto coste de adquisición. Por esta razón, el uso de superordenadores auténticos está limitado a organismos gubernamentales, militares y grandes centros de investigación, donde se dispone de suficiente capital. El resto de colectivos no pueden afrontar el costo económico que supone adquirir una máquina de estas características, y aquí es donde toma la máxima importancia la idea de 13 poder disponer de esa potencia de cálculo, pero a un precio muy inferior. El concepto de cluster nació cuando los pioneros de la súper computación intentaban difundir diferentes procesos entre varias computadoras, para luego poder recoger los resultados que dichos procesos debían producir. Con un hardware más asequible, se pudo perfilar que podrían conseguirse resultados muy parecidos a los obtenidos con aquellas máquinas mucho más costosas, como se ha venido probando desde entonces. Esto último, nos lleva a fijar nuestra atención en la computación distribuida. Es un modelo relativamente nuevo, destinado a resolver problemas de computación masiva utilizando un gran número de computadoras organizadas en racimos incrustados en una infraestructura de telecomunicaciones distribuida. Esta computación distribuida consiste en compartir recursos heterogéneos, basados en distintas plataformas, arquitecturas y lenguajes de programación, situados en distintos lugares y pertenecientes a diferentes dominios de administración sobre una red que utiliza estándares abiertos. En definitiva, es tratar de forma virtual los recursos informáticos y telemáticos disponibles. La aparición de la computación distribuida se debe a la necesidad de resolver problemas demasiado grandes para cualquier supercomputadora, con el objetivo adicional de mantener la flexibilidad de trabajar en múltiples problemas más pequeños. Por tanto, la computación distribuida es naturalmente un entorno multiusuario; esto hace que las técnicas de autorización segura sean esenciales antes de permitir que los recursos informáticos sean controlados por usuarios remotos. Basándonos en la funcionalidad, las redes de computación distribuida se clasifican en redes computacionales y redes de datos. A continuación, describiremos brevemente algunas de las herramientas y aspectos relacionados con la computación distribuida. x Grid. La computación en grid o en malla es un nuevo paradigma de computación distribuida en el cual todos los recursos de un número indeterminado de computadoras son englobados para ser tratados como un único superordenador de manera transparente. Las computadoras asociadas al grid no están conectadas o enlazadas firmemente, es decir no tienen porqué estar en el mismo lugar geográfico. El grid ofrece una forma de resolver grandes problemas, como el plegamiento de las proteínas y descubrimiento de medicamentos, construcción de modelos financieros, simulación de terremotos, inundaciones y otras catástrofes naturales, modelado del clima y el tiempo, etc. En un sistema SSI (Single System Image), todas las computadoras vinculadas dependen de un sistema operativo común, diseñado al efecto. Este es el caso general de un cluster. En cambio, un grid es heterogéneo, en el sentido de que las computadoras pueden tener diferentes sistemas operativos. x Globus. La herramienta Globus ha emergido como el estándar de facto para la capa intermedia (middleware) del grid. Algunos de los servicios que ofrece Globus son: - La gestión de recursos: Protocolo de Gestión de Recursos en Rejilla. 14 - Servicios de Información: Servicio de Descubrimiento y Monitorización. Gestión y Movimiento de Datos: Acceso Global al Almacenamiento Secundario y FTP en grid, GridFTP. La mayoría de grids que se expanden sobre las comunidades académicas y de investigación de Europa y Norteamérica están basadas en la herramienta Globus como núcleo de la capa intermedia. x XML. Los servicios Web basados en XML, ofrecen una forma de acceder a diversos servicios en un entorno distribuido. Recientemente, el mundo de la informática en grid y los servicios Web caminan juntos para ofrecer el grid como un servicio Web. La arquitectura está definida por la Open Grid Services Architecture (OGSA). La versión 3.0 de Globus Toolkit, será una implementación de referencia acorde con el estándar OGSA. x Clustering. Otro método para crear sistemas de supercomputadoras es el clustering. Un cluster o racimo de computadoras consiste en un grupo de ordenadores de bajo coste en relación al de una supercomputadora, conectados entre sí mediante una red de alta velocidad (Gigabit de fibra óptica, Myrinet, etc.) y un software que realiza la distribución de carga del trabajo entre los equipos. En un cluster, todos los nodos (ordenadores) se encuentran en el mismo lugar geográfico, conectados por una red local para englobar todos lo recursos. Por lo general, éste tipo de sistemas cuentan con un centro de almacenamiento de datos único. El sistema utilizado para realizar nuestro trabajo es un cluster, del cual haremos larga mención posteriormente. x Aspectos de seguridad. El tema de la seguridad es delicado en el ámbito de la computación distribuida pues las conexiones se hacen de forma remota, razón por la cual surgen problemas para controlar el acceso a los distintos nodos de la red. Hemos visto que los dos grandes sistemas de computación distribuida son el grid y el cluster (sin mencionar las carísimas supercomputadoras), cada uno con sus ventajas e inconvenientes particulares. Si tenemos presente nuestro objetivo final, el cual es tener un conjunto de computadoras dedicadas exclusivamente al cálculo numérico distribuido, llegamos a la conclusión de que la solución conveniente para llevar a cabo nuestro trabajo es un cluster. Esto, además, coincide con la configuración que se dispone en el lugar de trabajo. Así pues, nos centraremos en la descripción de la composición y el funcionamiento de un cluster de ordenadores. 15 2.2. CLUSTER DE ORDENADORES. La arquitectura de un cluster convencional viene determinada por un conjunto de computadoras que se comunican por medio de una conexión de red local muy rápida, para trabajar en un proyecto que sería demasiado grande para una sola computadora, resolviéndolo en un tiempo razonable. Este conjunto de ordenadores se comporta como si fuese una única máquina. El cómputo con clusters surge como resultado de la convergencia de varias tendencias actuales, que incluyen: la disponibilidad de microprocesadores económicos de alto rendimiento y redes de alta velocidad, el desarrollo de herramientas software para cómputo distribuido de alto rendimiento, así como la creciente necesidad de potencia computacional para aplicaciones que la requieran. La tecnología de clusters ha evolucionado en apoyo de actividades que van desde aplicaciones de súper cómputo, hasta servidores Web y de comercio electrónico y bases de datos de alto rendimiento, entre otros usos. Ni que decir tiene el gran papel que juegan en la solución de problemas de Ciencia e Ingeniería, que es la disciplina que a nosotros compete. Veamos la clasificación de los tipos de cluster existentes: x Alta disponibilidad (Fail-over o High-Availability): este tipo de cluster esta diseñado para mantener uno o varios servicios disponibles, incluso a costa de rendimiento, ya que su foco principal es que el servicio jamás tenga interrupciones, como es el caso de una base de datos. x Alto rendimiento (HPC o High Performance Computing): este tipo de cluster está diseñado para obtener el máximo rendimiento de la aplicación utilizada, incluso a costa de la disponibilidad del sistema, es decir el cluster puede sufrir caídas. Este tipo de configuración esta orientada a procesos que requieran mucha capacidad de cálculo. x Balanceo de carga (Load-balancing): este tipo de cluster esta diseñado para balancear la carga de trabajo entre varios servidores; esto permite tener, por ejemplo, un sitio Web sin caídas por una carga excesiva de peticiones en un momento dado (excepto cuando se sobrepase la capacidad de todas las máquinas). Actualmente un cluster loadbalancing es un fail-over, con el extra del balanceo de la carga y, a menudo, con mayor número de nodos. En consecuencia, de un cluster se espera que presente combinaciones de los tres servicios anteriores (alta disponibilidad, alto rendimiento y balanceo de carga) y, además, que sea escalable. Esta última característica es importante, ya que la forma de trabajar con un cluster es empezar con pocos nodos y comenzar probando el funcionamiento de diversas aplicaciones, y si todo va bien y necesitamos mejorar el rendimiento, se van añadiendo más nodos al conjunto. La adición de nuevos nodos al cluster no provoca cambio alguno en las aplicaciones ya desarrolladas, solamente en los resultados obtenidos, como puede ser la mejora en el rendimiento. Una característica a destacar es la flexibilidad a la hora de construir un cluster. Todos los nodos pueden tener la misma configuración de hardware y sistema operativo (cluster homogéneo), o bien, tener arquitecturas y sistemas operativos similares, no iguales (cluster 16 semi-homogéneo), o por el contrario, tener diferente hardware y sistema operativo (cluster heterogéneo). Para que un cluster funcione como tal, no basta solamente con conectar entre sí los ordenadores, sino que es necesario dotarlo de un sistema de manejo del cluster, que se encargue de interactuar con el usuario y los procesos que corren en él para optimizar el funcionamiento. Pasemos a ver los componentes de un cluster. En general, un cluster necesita de varios componentes software y hardware para poder funcionar. A saber: - Nodos (ordenadores - servidores). Sistemas Operativos. Conexiones de Red. Middleware (capa de abstracción entre el usuario y los sistemas operativos). Protocolos de comunicación y servicios. Aplicaciones (pueden ser paralelas o no). Veamos cada uno en detalle. Nodos. Pueden ser simples ordenadores, sistemas multiprocesador o estaciones de trabajo (workstations). Sistema Operativo (S.O.). Debe ser de fácil manejo y acceso, y permitir además múltiples procesos y usuarios. Ejemplos se S.O. son: GNU/Linux, Unix (Solaris / HP-Ux / Aix), Windows (NT / 2000 / 2003 Server), Mac OS X, S.O. especiales para Clusters, etc. Conexiones de Red. Los nodos de un cluster pueden conectarse mediante una simple red Ethernet, o a través de tecnologías especiales de alta velocidad como Fast-Ethernet, Gigabit-Ethernet, Myrinet, Infiniband, SCI, etc. Myrinet es una red de interconexión de clusters de altas prestaciones. La empresa fabricante de Myrinet es Myricom. Desde 1995, han ido mejorando en rendimiento, hasta obtener en la actualidad latencias de 3 microsegundos y anchos de banda de hasta 10Gbps. Una de sus principales características, además de su rendimiento, es que el procesamiento de las comunicaciones de red se hace a través de chips integrados en las tarjetas de red de Myrinet (Lanai chips), descargando a la CPU de parte del procesamiento de las comunicaciones. Físicamente, Myrinet consiste en dos cables de fibra óptica, upstream y downstream, conectados mediante un único conector. Las especiales características de Myrinet hacen que sea altamente escalable, gracias a la tecnología existente de conmutadores y routers. Su presencia en clusters de gran tamaño es importante. De hecho, en la lista del Top500 (www.top500.org), dentro de los clusters, la inmensa mayoría utilizan redes Myrinet. Para nuestro trabajo particular, no se utiliza una red Myrinet, sino Gigabit-Ethernet, de 1 Gbps, que es de lo que se dispone. 17 Middleware. El middleware es un software que generalmente actúa entre el sistema operativo y las aplicaciones, con la finalidad de proveer a un cluster de lo siguiente: - - Una interfaz única de acceso al sistema, denominada SSI (Single System Image), la cual genera la sensación al usuario de que utiliza un único ordenador muy potente. Herramientas para la optimización y mantenimiento del sistema: migración de procesos, checkpoint-restart (congelar uno o varios procesos, mudarlos de servidor y continuar su funcionamiento en el nuevo host), balanceo de carga, tolerancia a fallos, etc. Escalabilidad: debe poder detectar automáticamente nuevos servidores conectados al cluster para proceder a su utilización. Existen diversos tipos de middleware, como por ejemplo: MOSIX, OpenMOSIX, Cóndor, OpenSSI, etc. Al igual que ocurría con las conexiones de red, la inmensa mayoría de clusters utilizan middleware desarrollado por Myricom, y distribuido bajo la fórmula de Software Libre. Destacan las librerías a bajo nivel GM y MX, las implementaciones de MPI MPICH-GM y MPICH-MX y las implementaciones de Sockets de alto rendimiento SocktesGM y Sockets-MX. El middleware recibe el trabajo entrante al cluster, y lo distribuye de manera que la aplicación se ejecute lo más rápido posible y el sistema no sufra sobrecargas en un nodo particular. Esto se realiza mediante políticas definidas en el sistema (automáticamente o por un administrador) que le indican dónde y cómo debe distribuir los procesos, a través de un sistema de monitorización, el cual controla la carga de cada CPU y la cantidad de procesos en cada máquina. El middleware también debe poder migrar procesos entre servidores con distintas finalidades: - balancear la carga: si un servidor está muy cargado de procesos y otro está ocioso, pueden transferirse procesos a este último para liberar de carga al primero y optimizar el funcionamiento. - mantenimiento de servidores: si hay procesos corriendo en un servidor que necesita mantenimiento o una actualización, es posible migrar los procesos a otro servidor y proceder a desconectar del cluster al primero. - priorización de trabajos: en caso de tener varios procesos corriendo en el cluster, pero uno de ellos de mayor importancia que los demás, puede migrarse este proceso a los servidores que posean más o mejores recursos para acelerar su procesamiento. Los modelos de clusters más conocidos por su amplia utilización en función del middleware son: - NUMA (Non-Uniform Memory Access). - PVM (Parallel Virtual Machine). - MPI (Message Pass Interface), que es la que nosotros utilizamos para desarrollar nuestro trabajo. Las máquinas de tipo NUMA, tienen acceso compartido a la memoria donde pueden ejecutar su código de programa. En el kernel de Linux hay ya implementado NUMA, que hace variar el número de accesos a las diferentes regiones de memoria. 18 MPI y PVM son herramientas ampliamente utilizadas, y son muy conocidas por aquellos que entiende de súper computación basada en GNU/Linux. MPI es el estándar abierto de bibliotecas de paso de mensajes. MPICH es una de las implementaciones más usadas de MPI; tras MPICH se puede encontrar LAM, otra implementación basada en MPI, que también son bibliotecas de código abierto. PVM es un middleware semejante a MPI, ampliamente utilizado en clusters Beowulf. PVM habita en el espacio de usuario, y tiene la ventaja de que no hacen falta modificaciones en el kernel de Linux. Básicamente, cada usuario con derechos suficientes puede ejecutar PVM. En el apartado 2.4. tendrá lugar una discusión sobre las diferencias entre MPI y PVM, y se explicará en detalle el funcionamiento de MPI, ya que es la solución escogida para desarrollar nuestro objetivo. Para entender bien el fin de utilizar la MPI, es necesario realizar un estudio sobre el Cálculo en Paralelo, que será de gran importancia para nuestro trabajo. La próxima sección se ha dedicado a dicha tarea. 19 2.3. CÁLCULO EN PARALELO. Empecemos viendo qué es el paradigma del Cálculo en Paralelo. Tradicionalmente, todo el software ha sido escrito para computación en serie: - El programa se diseña para correr en un solo ordenador con una única CPU. El problema a abordar es dividido en series discretas de instrucciones. Las instrucciones son ejecutadas una detrás de otra. Sólo una instrucción puede ser ejecutada en cada instante de tiempo. En la figura 3, se muestra lo descrito para el caso de computación en serie. Figura 3. División en instrucciones de un problema para ejecución en serie. Teniendo en cuenta ya qué es la computación en serie, para el caso más sencillo, podemos definir la computación en paralelo como la utilización simultánea de múltiples recursos de computación para resolver un problema. Esto es: - El programa correrá utilizando múltiples CPU’s. El problema a abordar es dividido en partes discretas que pueden ser resueltas concurrentemente. Cada parte es, además, descompuesta en series de instrucciones. Las instrucciones procedentes de cada parte se ejecutan simultáneamente en diferentes CPU’s. El esquema del cálculo en paralelo se muestra en la figura 4. Los recursos de computación pueden incluir: - Un simple ordenador con múltiples procesadores. Un número arbitrario de ordenadores conectados en red. Una combinación de ambos. El problema a resolver, normalmente, muestra características tales como la habilidad para ser: - Separados en partes discretas del trabajo, que pueden ser resueltas simultáneamente. Ejecutar múltiples instrucciones del programa en cualquier instante de tiempo. 20 - Resuelto en menos tiempo con múltiples recursos computacionales que con un recurso simple para la resolución en serie. Figura 4. División en instrucciones de un problema para ejecución en paralelo. La computación en paralelo es una evolución de la computación en serie que intenta emular lo que siempre ha sido el estado de ciertas cosas en el mundo real, muchos hechos complejos e interrelacionados que ocurren al mismo tiempo, pero dentro de una secuencia, por ejemplo: órbitas galácticas y planetarias, patrones del tiempo y del océano, tendencia de las placas tectónicas, línea de ensamblado de automóviles, etc. Existen diversas razones para utilizar la computación en paralelo, como ahorrar tiempo, resolver grandes problemas, proporcionar concurrencia, superar los límites de memoria existentes al utilizar un solo ordenador, etc. 2.3.1. CONCEPTOS GENERALES DEL CÁCULO EN PARALELO. Presentaremos ahora algunos de los conceptos relacionados con la computación en paralelo. La mayoría de ellos serán discutidos posteriormente con más profundidad. - Tarea: Sección lógica discreta de trabajo computacional. Suele ser un programa o un conjunto de instrucciones que es ejecutable por un procesador. - Tarea Paralela: Tarea que puede ser ejecutada de forma segura (produciendo resultados correctos) por múltiples procesadores simultáneamente. - Ejecución en serie: Ejecución de un programa de forma secuencial, una expresión en cada instante de tiempo. No obstante, todas las tareas paralelas tendrán secciones de un programa paralelo que deben ser ejecutadas en serie. - Ejecución en paralelo: Ejecución de un programa por más de una tarea, siendo cada tarea capaz de ejecutar la misma expresión o una diferente, y todas en un mismo instante de tiempo. 21 - Memoria compartida: Desde el punto de vista del hardware, describe una arquitectura de ordenador donde todos los procesadores tienen acceso directo a una memoria física común. Desde el punto de vista del software, describe un modelo donde todas las tareas paralelas tienen la misma representación de la memoria, y pueden direccionar y acceder directamente a las mismas localizaciones de una memoria lógica, sin preocuparse de donde se encuentra la memoria física. - Memoria distribuida: En el sentido del hardware, se refiere a un acceso a memoria basado en red para una memoria física que no es común. Como modelo de programación, las tareas sólo pueden ver lógicamente la memoria de la máquina local, y deben utilizar comunicaciones para acceder a la memoria de otras máquinas donde se ejecutan el resto de tareas. - Comunicaciones: Típicamente, las tareas paralelas necesitan intercambiar datos. Hay varios caminos para realizar esto, tales como tener una memoria compartida en bus o sobre una red. Sin embargo, en la actualidad, al hecho de intercambiar datos se le denomina comunicaciones, independientemente del método empleado. - Sincronización: Es la coordinación de tareas paralelas en tiempo real, a menudo asociado a las comunicaciones. La forma en que se implementa la sincronización entre tareas es poniendo un punto de sincronismo dentro del código de una aplicación, de modo que las tareas no continuarán con su trabajo hasta que todas las tareas hayan llegado al mismo punto de sincronismo o al lógicamente equivalente. La sincronización implica la espera de, al menos, una tarea, y, por lo tanto, puede causar el incremento del tiempo de ejecución de la aplicación paralela. El tema del sincronismo es muy importante, y es uno de los temas más delicados a tratar a la hora de escribir aplicaciones en paralelo. - Granularidad: En la computación en paralelo, la granularidad es una medida cualitativa de la tasa de tiempo de computación entre tiempo de comunicaciones. En el límite, granularidad gruesa (fina) es cuando grandes (pequeñas) cantidades de trabajo computacional son realizadas en medio de eventos de comunicaciones. - Aumento de la velocidad observado: Es la diferencia entre el tiempo de ejecución en serie de una aplicación y el tiempo de ejecución en paralelo de la misma aplicación. - Coste operativo paralelo: Cantidad de tiempo requerido para coordinar las tareas paralelas, contrario al tiempo útil de ejecución. El coste operativo paralelo puede incluir factores como el tiempo de arranque y terminación de una tarea, sincronización, comunicaciones de datos, coste del software impuesto por compiladores, librerías, herramientas y sistema operativo paralelos, etc. - Masivamente paralelo: Término referido al hardware que incluye un sistema paralelo dado, teniendo en cuenta muchos procesadores. El significado de ‘muchos’ va incrementándose, pero actualmente BG/L apuesta por seis procesadores. - Escalabilidad: Capacidad de un sistema paralelo (hardware o software) para demostrar un incremento proporcional en la velocidad de ejecución en paralelo con el aumento del número de procesadores. Un factor importante que influye en la escalabilidad es la forma en que se ha diseñado el código de la aplicación paralela a ejecutar. 22 2.3.2. ARQUITECTURAS DE MEMORIA DE ORDENADORES PARALELOS. 2.3.2.1. MEMORIA COMPARTIDA. Los ordenadores paralelos de memoria compartida varían mucho de unos a otros en cuanto a su arquitectura pero, generalmente, tienen en común la capacidad, para todos los procesadores, de acceder a toda la memoria mediante un espacio de direccionamiento global. El esquema general de memoria compartida se presenta en la figura 5. Múltiples procesadores pueden operar de forma independiente compartiendo los mismos recursos de memoria. Los cambios efectuados por un procesador en una localización concreta de la memoria son visibles por el resto de procesadores. Figura 5. Memoria compartida por un conjunto de procesadores. Las máquinas de memoria compartida se pueden dividir en dos clases principales, basadas en los tiempos de acceso a memoria: UMA (Acceso Uniforme a Memoria) y NUMA (Acceso No-Uniforme a Memoria). En el caso de UMA, las máquinas son del tipo SMP (Multiprocesador Simétrico – procesadores idénticos-), y el acceso a memoria y los tiempos asociados a dicho acceso son idénticos para todos los procesadores. La arquitectura de NUMA viene determinada por dos o más SMP’s enlazados. Un SMP puede acceder directamente a la memoria de otro SMP. No todos los procesadores tienen el mismo tiempo de acceso a todas las memorias, ya que el acceso a través del enlace es más lento. Una de las ventajas del método de memoria compartida es que el espacio de direccionamiento global presenta un fácil manejo desde el punto de vista del programador a la hora de acceder a memoria. Además, la compartición de datos entre tareas es rápido y uniforme, debido a la proximidad de la memoria a las CPU’s. Por el contrario, una gran desventaja que presenta este sistema es la falta de escalabilidad, ya que añadir más CPU’s incrementa de forma geométrica el tráfico asociado con la gestión de la memoria. Asimismo, es responsabilidad del programador asegurar el acceso correcto a la memoria para garantizar la sincronización entre las tareas. A todo esto se le añade lo caro que resulta diseñar y producir máquinas de memoria compartida con cada incremento del número de procesadores. 2.3.2.2. MEMORIA DISTRIBUIDA. Al igual que los sistemas de memoria compartida, los sistemas de memoria distribuida son muy variados, pero comparten características comunes. Los sistemas de memoria distribuida requieren una red de comunicación para conectar las memorias de cada procesador. Esta configuración se muestra en la figura 6. 23 Cada procesador tiene su propia memoria local. Las direcciones de memoria de un procesador no se mapean al resto de procesadores, de modo que no existe el concepto de espacio de direccionamiento global a través de todos los procesadores. Figura 6. Sistema de memoria distribuida. Como cada procesador tiene su memoria local, cada cual opera independientemente del resto. Los cambios hechos por cada uno en su memoria local no tienen efecto sobre las memorias de los otros procesadores. Cuando un procesador necesita acceder a los datos residentes en otro procesador, suele ser tarea del programador definir explícitamente definir cómo y cuando se comunican los datos. Asimismo, es responsabilidad del programador el lograr la sincronización entre tareas. Este sistema, a diferencia del anterior (sistema de memoria compartida), sí que es escalable en cuanto a lo que a memoria se refiere a la hora de aumentar el número de procesadores. Cada procesador puede acceder rápidamente a su propia memoria sin ninguna interfaz y sin ningún coste operativo incurrido, siempre manteniendo la coherencia de la caché. Lógicamente, estos sistemas son de tipo NUMA, es decir, los tiempos de acceso a memoria son no uniformes. 2.3.2.3. HÍBRIDO MEMORIA COMPARTIDA - DISTRIBUIDA. Hoy en día, los ordenadores más potentes en el mundo emplean una composición entre las arquitecturas de memoria compartida y distribuida, así se muestra en la figura 7. En la tabla 1, se presenta una comparativa entre las arquitecturas de memoria compartida y distribuida. Figura 7. Sistema híbrido memoria compartida-distribuida. 24 La componente de memoria compartida es, normalmente, una máquina SMP con coherencia de caché. Los procesadores de un SMP dado pueden direccionar la memoria de esa máquina de forma global. Por otro lado, la componente de memoria distribuida se corresponde con el funcionamiento en red de múltiples SMP’s. Cada SMP solamente tiene constancia de su propia memoria, no de la memoria de otros SMP’s. Por tanto, son requeridas comunicaciones de red para mover datos de un SMP a otro. Arquitectura UMA NUMA DISTRIBUIDA Ejemplos SMPs Sun Vexx DEC/Compaq SGI Challenge IBM POWER3 SGI Origin Sequent HP Exemplar DEC/Compaq IBM POWER4 (MCM) Cray T3E Maspar IBM SP2 Comunicaciones MPI Hilos OpenMP Mem. Compartida MPI Hilos OpenMP Mem. Compartida MPI Escalabilidad Hasta decenas de procesadores. Hasta cientos de procesadores. Hasta miles de procesadores. Características destacables. Ancho de banda Memoria-CPU. Ancho de banda Memoria-CPU. Tiempos de acceso no uniformes. Administración del sistema. Programación complicada a la hora del desarrollo y el mantenimiento. Disponibilidad del Software Miles de ISVs. Miles de ISVs. Cientos ISVs. Tabla 1. Comparativa entre arquitecturas de memoria compartida y distribuida. Las tendencias actuales parecen indicar que este tipo de arquitectura de manejo de memoria continuará prevaleciendo, y se incrementará el uso de este tipo de sistemas para la computación en paralelo dentro del futuro visible. Las ventajas e inconvenientes de este tipo de sistemas son la unión de las indicadas para cada arquitectura por separado. 2.3.3. MODELOS DE PROGRAMACIÓN EN PARALELO. Existen varios modelos de programación en paralelo de uso común: Memoria compartida. Hilos (tareas paralelas). Paso de mensajes. Datos paralelos. Híbrido. 25 Los modelos de programación existen como una abstracción sobre las arquitecturas de hardware y de memoria. Aunque no parezca claro a simple vista, estos modelos no son específicos para un tipo particular de máquina o arquitectura de memoria. En realidad, cualquiera de estos modelos, teóricamente, pueden ser implementados en cualquier hardware subyacente. El modelo a utilizar suele ser una combinación entre lo disponible y la elección personal. No existe un modelo general y óptimo, aunque sí es verdad que hay mejores implementaciones que otras de algunos modelos. En las siguientes secciones, se describe cada uno de los modelos mencionados. 2.3.3.1. MODELO DE MEMORIA COMPARTIDA. En este modelo de programación, las tareas comparten un espacio de direcciones común, en el cual leen y escriben de forma asíncrona. Por ende, es necesario mecanismo como cerrojos y semáforos para controlar el acceso a la memoria compartida. Desde el punto de vista del programador, la ventaja de utilizar este modelo es la escasa noción de “dominio” de datos, de modo que no es necesario especificar explícitamente la comunicación de datos entre tareas. Esto hace que se simplifique el desarrollo del programa. Por el contrario, en términos de funcionamiento, la comprensión y gestión del área de datos puede llegar a ser muy complicado. 2.3.3.2. MODELO DE HILOS. En el modelo de hilos de programación en paralelo (programación multihilo), un simple proceso puede tener caminos (hilos) de ejecución múltiples y concurrentes. El programa principal es ejecutado por el sistema operativo. Dicho programa, ejecuta algo de trabajo en serie, y entonces crea un número determinado de tareas (hilos) que pueden ser ejecutados por el sistema operativo de forma concurrente. Cada hilo tiene sus datos locales, pero también comparte los recursos completos del programa principal. Esto ahorra el coste operativo asociado a la replicación de los recursos del programa para cada hilo. Cada hilo también se beneficia de la vista de la memoria global, ya que todos comparten el espacio de memoria del programa principal; tanto es así, que es la forma que tienen los hilos para intercomunicarse. El programa principal permanece en funcionamiento para proveer los recursos compartidos necesarios hasta que todos los hilos han acabado su ejecución. Los hilos son comúnmente asociados con arquitecturas y sistemas operativos de memoria compartida. Desde la perspectiva de la programación, las implementaciones de hilos comprenden: - Una librería de subrutinas que son llamadas dentro del código fuente paralelo. - Un conjunto de directivas para el compilador embebidas en el código fuente serie o paralelo. En última instancia, es el programador es el responsable de determinar todo el paralelismo. Diversos esfuerzos no relacionados para lograr la estandarización de este modelo han dado lugar dos implementaciones muy diferentes para sistemas UNIX: POSIX Threads y OpenMP. Microsoft tiene su propia implementación de hilos. 26 2.3.3.3. MODELO DE PASO DE MENSAJES. El modelo de paso de mensajes presenta las siguientes características: Un conjunto de tareas pueden utilizar sus propias memorias locales durante la computación. Múltiples tareas pueden residir en la misma máquina así como en un número arbitrario de máquinas. Las tareas intercambian datos por medio de comunicaciones enviando y recibiendo mensajes. Normalmente, la transferencia de datos requiere operaciones colectivas, que ejecutará cada proceso. Por ejemplo, una operación de tipo ‘send’ en un proceso comunicante debe tener su correspondiente operación ‘recieve’ en el proceso comunicado. Figura 8. Modelo de paso de mensajes. Desde el punto de vista de la programación, las implementaciones de modelos de paso de mensajes comúnmente consisten en una librería de subrutinas para ser incluidas en el código fuente. El programador es responsable de utilizar estas subrutinas adecuadamente para determinar el paralelismo. Históricamente, una gran variedad de librerías de paso de mensajes han estado disponibles desde los años 80. Estas implementaciones difieren sustancialmente de unas a otras, haciendo difícil para los programadores el desarrollo de aplicaciones portables. En 1992, se creó el MPI Forum, con el principal objetivo de establecer una interfaz estándar para las implementaciones de paso de mensajes. La primera parte de MPI (Interfaz de Paso de Mensajes), entró en vigor en 1994. La segunda parte (MPI-2) a partir de 1996. Hoy en día, MPI es el estándar para paso de mensajes más importante que existe, reemplazando al resto de modelos de paso de mensajes. La mayoría de plataformas más populares, si no todas, ofrecen al menos una implementación de MPI. Unas pocas ofrecen una implementación completa de MPI-2. Para arquitecturas de memoria compartida, las implementaciones de MPI, normalmente, no utilizan una red para comunicaciones entre tareas, sino que hacen uso de un sistema de memoria compartida (copias de la memoria) por razones de rendimiento. Para el proyecto presente, se utiliza MPI como soporte para la implementación de aplicaciones que se ejecutarán en paralelo en el cluster montado en el entorno de trabajo. 2.3.3.4. MODELO DE DATOS PARALELOS. La mayoría de trabajos paralelos se centran en operaciones de ejecución de un conjunto de datos. Típicamente, este conjunto de datos está organizado dentro de una estructura común, semejante a un array de una, dos o más dimensiones. 27 Un conjunto de tareas trabajan colectivamente en la misma estructura de datos; sin embargo, cada tarea trabaja en una partición diferente de dicha estructura. Todas las tareas realizan la misma operación en su partición de trabajo. En arquitecturas de memoria compartida, todas las tareas deben tener acceso a la estructura de datos por medio del direccionamiento global a la memoria común. En arquitecturas de memoria distribuida, la estructura de datos es dividida, y reside como “trozos” en la memoria local de cada tarea. La programación con el modelo de datos paralelos suele llevarse a cabo escribiendo un programa con construcciones de datos paralelos. Estas construcciones pueden ser logradas mediante llamadas a una subrutina de una cierta librería, o directivas reconocidas por un compilador de datos paralelos. La mayoría de las plataformas más comunes disponen de Fortran 90 y 95. HPF (High Performance Fortran), son extensiones de Fortran 90 para soportar la programación de datos en paralelo. HPF incluye, entre otras cosas, directivas para decirle al compilador cómo distribuir los datos paralelos. Las implementaciones de este modelo en sistemas de memoria distribuida, normalmente, disponen de un compilador para convertir el programa a código estándar con llamadas a librerías de paso de mensajes (habitualmente MPI), para distribuir los datos a todos los procesos implicados en la computación paralela. Todo el paso de mensajes es transparente para el programador. 2.3.3.5. OTROS MODELOS. Citaremos sólo los tres modelos más comunes distintos a los anteriores. Híbrido. Este modelo es una combinación de dos o más modelos de programación en paralelo. Actualmente, un ejemplo común de modelo híbrido es la combinación de paso de mensajes (MPI) con un modelo de hilos (POSIX threads) o con un modelo de memoria compartida (OpenMP). Este modelo híbrido se presta bien, a los entornos hardware cada vez más comunes de máquinas SMP en red. Otro ejemplo común de un modelo híbrido, es la combinación del modelo de datos paralelos con el de paso de mensajes. Como ya se ha mencionado en la sección anterior, las implementaciones de datos en paralelo (F90, HPF) en arquitecturas de memoria distribuida utilizan, actualmente, el paso de mensajes para transmitir datos entre tareas, de forma transparente para el programador. SPMD (Single Program Multiple Data). Actualmente, es un modelo de programación de “alto nivel” que puede ser construido sobre cualquier combinación de los modelos de programación en paralelo ya mencionados. Figura 9. Modelo SPMD. 28 Como se muestra en la figura 9, un mismo programa es ejecutado por todas las tareas simultáneamente. En cualquier instante de tiempo, las tareas pueden estar ejecutando las mismas o diferentes instrucciones dentro del mismo programa. Los programas SPMD normalmente tienen la lógica necesaria programada para permitir a las diferentes tareas la ramificación o ejecución condicional de aquellas partes del programa que les son designadas para ejecutarlas. Esto es, las tareas no tienen que ejecutar necesariamente todo el programa, tal vez sólo una porción del mismo. Además, cada tarea puede utilizar datos diferentes. MPMD (Multiple Program Multiple Data). Al igual que SPMD, MPMD es, actualmente, es un modelo de programación de “alto nivel” que puede ser construido sobre cualquier combinación de los modelos de programación en paralelo ya mencionados. Las aplicaciones MPMD típicamente tienen múltiples ficheros objetos ejecutables (programas); así se muestra en la figura 10. Figura 10. Modelo MPMD. Mientras la aplicación corre en paralelo, cada tarea puede ejecutar el mismo programa o uno diferente al del resto de tareas. Todas las tareas pueden usar diferentes datos. 29 2.4. DISEÑO DE PROGRAMAS EN PARALELO. 2.4.1. PARALELIZACIÓN AUTOMÁTICA FRENTE A MANUAL. El diseño y desarrollo de programas paralelos ha sido, característicamente, un proceso muy manual. El programador es típicamente el responsable de identificar e implementar actualmente el paralelismo de las aplicaciones. A menudo, el desarrollo manual de códigos paralelos es un proceso complejo, que consume mucho tiempo, propenso a errores e iterativo. Hoy en día, existen varias herramientas disponibles para asistir al programador a la hora de convertir programas en serie a programas paralelos. El tipo de herramienta más común utilizada para realizar el paso de programas de serie a paralelo es un compilador o preprocesador de paralelización. Generalmente, un compilador para códigos paralelos puede trabajar de dos formas distintas: - Totalmente automático: el compilador analiza el código fuente e identifica oportunidades para el paralelismo. El análisis incluye inhibidores identificados para el paralelismo y, posiblemente, un coste de carga para ver si el paralelismo mejoraría el funcionamiento. - Dirigido al programador: utilizando directivas del compilador o, tal vez, indicadores para el compilador, el programador le dice explícitamente al compilador cómo paralelizar el código. Puede que también sea posible utilizar este método en conjunción con el de paralelización automática. Si se comienza a paralelizar a partir de un código serie existente, y se dispone de un tiempo limitado o de un presupuesto reducido, entonces puede que la paralelización automática sea la solución. Sin embargo, hay varias advertencias importantes a tener en cuenta: pueden producirse malos resultados, se puede degradar el funcionamiento, es mucho menos flexible que la paralelización manual, limitado a un subconjunto de código (mayoritariamente bucles) o puede que no se lleve a cabo si el análisis previo indica que hay inhibidores o el código es muy complejo. Por último, indicar que la mayoría de herramientas de paralelización implementadas son para Fortran. Las siguientes secciones se dedican a explicar las consideraciones previas a tener en cuenta a la hora del desarrollo de códigos paralelos de forma manual. Es importante tenerlas en cuenta, ya que para nuestro trabajo particular utilizaremos técnicas manuales de paralelización en paralelo, ya sea desde cero o a partir de un código fuente escrito en serie. 2.4.2. COMPRENSIÓN DEL PROBLEMA A PARALELIZAR. Indudablemente, el primer paso en el desarrollo de software paralelo es comprender el problema que se desea resolver en paralelo. Si se está comenzando con un programa en serie, también es necesaria la comprensión del código existente. Antes de perder tiempo en intentar desarrollar una solución en paralelo para un problema, es conveniente determinar si el problema actualmente puede o no ser paralelizado. Hemos de identificar los puntos relevantes del programa: saber dónde se realiza la mayoría del trabajo real. Herramientas de análisis del funcionamiento y de perfiles pueden ayudar a realizar esta tarea. Es importante focalizar la paralelización en estos puntos relevantes e ignorar aquellas partes del programa que necesitan poco uso de CPU. 30 Otro aspecto importante a tener en cuenta es el tema de los cuellos de botella: hay que localizar aquellas áreas del programa que son desproporcionadamente lentas o que provocan que el trabajo paralelizable sea detenido o aplazado. Por ejemplo, todo lo referente a la entrada / salida de datos es, normalmente, algo que ralentiza un programa. Puede que sea posible reestructurar el programa o utilizar algoritmos diferentes para reducir o eliminar áreas lentas innecesarias. Por otro lado, es de vital importancia encontrar inhibidores del paralelismo; una clase común de inhibidor del paralelismo es la dependencia de datos. Es aconsejable estudiar varios algoritmos posibles para buscar mejoras en el funcionamiento de la aplicación paralela. 2.4.3. PARTICIÓN. Uno de los primeros pasos en el diseño de un programa paralelo es dividir el problema en trozos discretos de trabajo que pueden ser distribuidos a múltiples tareas. Esto es conocido como descomposición o particionamiento. Hay dos formas básicas para lograr la partición de trabajo computacional entre tareas paralelas: descomposición de dominio y descomposición funcional. Descomposición de dominio. En este tipo de partición, se descomponen los datos asociados con un problema. Entonces, cada tarea paralela trabaja en una porción de los datos, tal y como se muestra en la figura 11. Figura 11. Partición mediante descomposición de dominio. Hay varios caminos para particionar datos. Se presentan en la figura 12, para una y dos dimensiones. Para dos dimensiones (2D), se particiona en función de los índices de fila y columna (‘*’ es no partición para el índice al que se hace referencia). 31 Figura 12. Partición mediante descomposición de dominio. Descomposición funcional. En este método, el objetivo se centra en la computación que va a ser realizada, más que en los datos manipulados por la computación. El problema es descompuesto de acuerdo al trabajo que se va realizar. Entonces, cada tarea es una porción del trabajo total. Figura 13. Partición mediante descomposición funcional. 2.4.4. COMUNICACIONES. Algunos tipos de problemas pueden ser descompuestos y ejecutados en paralelo con ninguna, o casi ninguna, necesidad de compartir datos entre tareas. Sin embargo, la mayoría de las aplicaciones no son tan simples, y requieren que las tareas compartan datos entre ellas. Hay un número importante de factores a considerar cuando se diseñan comunicaciones entre las tareas de un programa paralelo, a saber: Coste de comunicaciones: - La comunicación entre tareas siempre implica un coste operativo. 32 - - - Ciertos recursos de las máquinas y varios ciclos de CPU que podrían ser empleados para la computación son utilizados para empaquetar y transmitir datos. Frecuentemente, las comunicaciones requieren algún tipo de sincronización entre tareas, que puede resultar en tareas que se esperan unas a otras en vez de estar realizando el trabajo de computación. El cursar el tráfico de comunicaciones puede saturar el ancho de banda disponible de la red, agravando los problemas de rendimiento. Latencia frente a ancho de banda: - Latencia es el tiempo que tarda una tarea en enviar el mínimo mensaje (0 byte) desde un punto A a otro punto B. - Ancho de banda es la cantidad de datos que pueden ser enviados por unidad de tiempo. Es el régimen binario. - El envío de muchos mensajes pequeños puede causar que la latencia domine sobre el coste operativo de comunicaciones. A menudo, es más eficiente empaquetar mensajes pequeños dentro de otro mensaje más grande, así se incrementa el ancho de banda efectivo de las comunicaciones. Visibilidad de las comunicaciones: - Con el modelo de paso de mensajes, las comunicaciones son explícitas y, generalmente, bastante visibles y bajo el control del programador. - Con el modelo de datos paralelos, las comunicaciones a menudo ocurren de forma transparente para el programador, particularmente en arquitecturas de memoria distribuida. Puede incluso que el programador no sepa exactamente cómo se están llevando a cabo las comunicaciones entre tareas. Comunicaciones asíncronas frente a síncronas: - Las comunicaciones síncronas requieren algún tipo de coordinación entre las tareas que están compartiendo datos. Esto puede ser explícitamente estructurado en código por el programador, o puede ocurrir a un nivel más bajo desconocido por el programador. - A menudo, se hace referencia a las comunicaciones síncronas como comunicaciones “bloqueantes”, puesto que otro trabajo debe esperara hasta que las comunicaciones se hayan completado. - A las comunicaciones asíncronas se hace referencia como comunicaciones “no bloqueantes”, puesto que pueden realizarse mientras tiene lugar otro trabajo. - La computación entrelazada con las comunicaciones es el único y mayor beneficio para decidirse por utilizar comunicaciones asíncronas. Alcance de las comunicaciones: - Saber qué tareas se deben comunicar con otras es crítico durante la parte de diseño del código paralelo. Las dos formas de comunicación descritas a continuación, pueden ser implementadas síncronamente o asíncronamente: a) b) Comunicación punto a punto: involucra a dos tareas, una actuando como el transmisor / productor de datos y la otra como el receptor / consumidor. Comunicación colectiva: implica la compartición de datos entre más de dos tareas, que pueden pertenecer a un mismo grupo o a otro 33 distinto, según se especifique. Ejemplos de comunicaciones colectivas son: difusión, recolección, reducción y dispersión de datos entre tareas. Eficiencia de las comunicaciones: - Muy a menudo, el programador tomará una elección en consideración a los factores que pueden afectar al funcionamiento de las comunicaciones. Esto lleva a plantearse qué implementación de un modelo concreto debería utilizarse. Por ejemplo, para el modelo de paso de mensajes, una implementación de MPI será más rápida que otras. - Otro aspecto a tener en cuenta es el tipo de operaciones de comunicación que deberían usarse. Como ya se ha comentado anteriormente, las operaciones de comunicación asíncrona pueden mejorar el funcionamiento global del programa. - También habrá que tener en cuenta que algunas plataformas pueden ofrecer más medios que una simple red para llevar a cabo las comunicaciones. Por tanto, tendremos que elegir entre las diferentes plataformas disponibles. 2.4.5. SINCRONIZACIÓN Veamos ahora una pequeña síntesis de los tipos de sincronización posibles: Barrera: normalmente, implica a todas las tareas. Cada tarea realiza su trabajo hasta que alcanza la barrera. Entonces, la tarea que llega a la barrera se para o se bloquea. Cuando la última tarea alcanza la barrera, todas las tareas son sincronizadas. Lo que ocurre a partir de aquí, varía de unos programas a otros. Frecuentemente, se realizará un conjunto de trabajos en serie. En otros casos, las tareas son liberadas automáticamente para continuar con sus trabajos. Cerrojo / semáforo: pueden involucrar cualquier número de tareas. Típicamente, se utilizan para serializar (proteger) el acceso a los datos globales o a una sección de código. Solamente una tarea, en el mismo instante de tiempo, puede utilizar el cerrojo / semáforo / bandera. La primera tarea que adquiere el cerrojo lo cierra; a continuación, esta tarea puede acceder de forma segura al código o datos protegidos. El resto de tareas pueden intentar adquirir el cerrojo, pero deben esperar hasta que la tarea que posee el cerrojo lo libere. Este tipo de métodos pueden ser bloqueantes o no bloqueantes. Operaciones de comunicación síncronas: involucra solamente a aquellas tareas que estén ejecutando una operación de comunicación. Cuando una tarea realiza una operación de este tipo, es necesario alguna forma de coordinación entre las tareas que participan en ella. Por ejemplo, cuando una tarea vaya a realizar una operación de envío de datos a otra tarea, la primera debe recibir un asentimiento positivo, así la segunda indica que está lista para recibir. Este tema ya se ha descrito en la sección de comunicaciones. 34 2.4.6. DEPENDENCIA DE DATOS. Se dice que existe una dependencia entre sentencias de un programa cuando el orden de ejecución de las sentencias afecta al resultado del programa. Una dependencia de datos resulta del uso múltiple de la misma zona de almacenamiento por diferentes tareas. Las dependencias son muy importantes para la programación en paralelo, ya que son uno de los inhibidores primarios para el paralelismo. Aunque todas las dependencias son importantes para identificar cuándo es posible la programación en paralelo, las dependencias acarreadas por los bucles son particularmente importantes, puesto que los bucles son, posiblemente, el objetivo más común de los esfuerzos de paralelización. En arquitecturas de memoria distribuida, se han de comunicar los datos requeridos en los puntos de sincronización. Para arquitecturas de memoria compartida, se han de sincronizar las operaciones de lectura / escritura entre tareas. 2.4.7. BALANCE DE CARGA. El balance de carga hace referencia a la práctica de distribuir el trabajo entre tareas, de modo que todas las tareas estén ocupadas todo el tiempo. Esto se puede considerar como una minimización del tiempo en que las tareas se encuentran desocupadas. El balance de carga es importante para los programas paralelos, por razones de funcionamiento. Por ejemplo, si todas las tareas están sujetas a un punto de sincronización de barrera, la tarea más lenta determinará el funcionamiento del resto. Veamos algunos métodos para lograr el balance de carga: Particionar equitativamente el trabajo que recibe cada tarea: - Para operaciones con matrices / arrays donde cada tarea realiza un trabajo similar, se han de distribuir el conjunto de datos entre las distintas tareas. Es importante no olvidarse de las dependencias de datos. - Para iteraciones de bucles donde el trabajo realizado en cada iteración es similar, se han de distribuir las iteraciones entre las tareas. Al igual que antes, la dependencia de datos es muy importante. - Si se está utilizando una mezcla heterogénea de máquinas con distintas características de funcionamiento, se ha de utilizar algún tipo de herramienta de análisis para detectar cualquier carga no balanceada. Asignación de trabajo de forma dinámica: - Ciertas clases de problemas resultan en desequilibrio de carga, aun cuando los datos se han distribuido de forma equitativa entre las tareas. Ejemplo de esto son las matrices dispersas, donde algunas tareas tendrán datos en los que trabajar y otras no debido a que contienen la mayoría de ceros de la matriz. - Cuando la cantidad de trabajo de que cada tarea realizará es intencionalmente variable, o se es incapaz de predecirlo, puede ser útil utilizar un método de “charco de tareas programado”. A medida que cada tarea finaliza su trabajo, pide una nueva porción de trabajo. - Puede que llegue a ser necesario diseñar un algoritmo dentro del código que detecte y maneje los desequilibrios de carga dinámicamente. 35 2.4.8. GRANULARIDAD. En computación en paralelo, la granularidad es una medida cualitativa de la tasa de computación frente a comunicación. Típicamente, los periodos de computación son separados de los periodos de comunicación por eventos de sincronización. Se distinguen dos tipos de granularidad: fina y gruesa. Veámoslas detenidamente: Paralelismo de grano fino: En esta situación, se realizan cantidades relativamente pequeñas de trabajo computacional entre eventos de comunicaciones: baja tasa de computación frente a comunicación. Esto facilita el balance de carga. Granularidad fina implica alto coste operativo y menos oportunidad para mejorar el rendimiento. Si la granularidad es demasiado fina, es posible que la sobrecarga requerida para las comunicaciones y la sincronización entre tareas tome más tiempo que la computación. Figura 14. Grano fino. Paralelismo de grano grueso: En este caso, se realizan grandes cantidades de computación entre los eventos de comunicación / sincronización: alta tasa de computación frente a comunicación. En contraposición al paralelismo de grano fino, con granularidad gruesa hay más oportunidad de mejorar el funcionamiento; sin embargo, es más difícil lograr el balance de carga de forma eficiente. Figura 15. Grano grueso. El tipo granularidad más eficiente, en general, no es ni una ni otra, sino que depende del algoritmo utilizado y del entorno hardware en que corre. En la mayoría de los casos, la sobrecarga asociada con las comunicaciones y la sincronización es alta en relación con la velocidad de ejecución, de modo que es ventajoso utilizar granularidad gruesa. El paralelismo de granularidad fina puede reducir la sobrecarga debida al desequilibrio de carga. 2.4.9. ENTRADA / SALIDA (E/S). Las operaciones de E/S son estimadas, generalmente, como inhibidores del paralelismo. Los sistemas de E/S paralelos son relativamente nuevos, o no se encuentran disponibles para todas las plataformas. En un entorno donde todas las tareas ven el mismo espacio de archivos, las operaciones de escritura resultarán en sobre escritura de ficheros. Las operaciones de lectura se verán afectadas por la capacidad del servidor de ficheros para manejar múltiples 36 peticiones de lectura al mismo tiempo. Las operaciones de E/S que necesitan ser conducidas a través de la red (sistema de ficheros no local, como es el caso de un sistema NFS), podrán causar cuellos de botella severos. Por otro lado, decir que existen sistemas de ficheros paralelos disponibles, por ejemplo: GPFS (General Parallel File System for AIX, de IBM), Lustre (para clusters Linux), PVFS/PVFS2 (Parallel Virtual File System, para clusters Linux), PanFS (Panasas ActiveScale File System, para clusters Linux), HP SFS (HP StorageWorks Scalable File Share, que es un sistema de ficheros paralelo basado en Lustre, también para clusters Linux). La especificación de la interfaz de E/S paralela para MPI ha estado disponible desde 1996 como parte de MPI-2. Actualmente, existen implementaciones de pago y libres de MPI-2. Algunos aspectos a tener en cuenta a la hora de realizar operaciones de E/S paralelas son: - - - Regla de oro: reducir todo lo posible el número de operaciones de E/S. Si se dispone de un sistema de ficheros paralelo, utilizarlo con la mayor destreza posible para conseguir un mayor rendimiento. Confinar las operaciones de E/S a porciones en serie específicas del trabajo, y entonces utilizar comunicaciones en paralelo para distribuir los datos a las tareas paralelas. Para sistemas de memoria distribuida con espacio de archivos compartido, realizar la E/S en el espacio de ficheros local, no en el compartido. Por ejemplo, cada procesador puede disponer de una carpeta temporal compartida en su espacio de archivos. Esto, normalmente, es mucho más eficiente que realizar la E/S sobre la red al directorio local. Crear nombres de archivos únicos para cada fichero de E/S de las tareas. 2.4.10. LÍMITES Y COSTES DE LA PROGRAMACIÓN EN PARALELO. A continuación, se presenta una serie de consideraciones a tener en cuenta a la hora de trabajar en paralelo. Ley de Amdahl: declara que el aumento de velocidad (speedup) del programa potencial es definido por la fracción de código P que puede ser paralelizado: speedup 1 1 P (1) Si ninguna parte del código puede ser paralelizada, P es cero, por tanto, speedeup es uno (no se produce incremento de velocidad). Por el contrario, si P es uno (todo el código es paralelizable), speedup es infinito, en teoría (existen límites hard y software, evidentemente). Si el 50% del código es paralelizable, quiere decir que el código paralelo será el doble de rápido que el código serie. Si introducimos en el modelo el número de procesadores que realizan una fracción del trabajo paralelo, la relación de Amdahl puede ser expresada como se presenta en (2). 37 speedup 1 (2) P S N Los factores que intervienen en (2) son: - P: fracción de código serie paralelizable. - N: número de procesadores que trabajan en paralelo. - S: Fracción de código serie no paralelizable (S = 1 – P). Es obvio que hay límites marcados para el paralelismo, marcados por la escalabilidad. A continuación se muestra una tabla comparativa de lo mencionado. N 10 100 1000 10000 P = 0.50 1.82 1.98 1.99 1.99 speedup P = 0.90 5.26 9.17 9.91 9.91 P = 0.99 9.17 50.25 90.99 99.02 Tabla 2. Speedup en función de N y P. Sin embargo, ciertos problemas muestran un incremento en el funcionamiento al aumentar el tamaño del problema. Este tipo de problemas que incrementan el porcentaje de tiempo paralelo con sus tamaños, son más escalables que los problemas con un porcentaje fijo de tiempo paralelo. Complejidad. En general, las aplicaciones paralelas son mucho más complejas que las correspondientes aplicaciones serie, incluso puede que lo sean en un orden de magnitud. No sólo se tiene múltiples flujos de instrucciones ejecutándose a la vez, sino también datos fluyendo por ellos. Los costes de complejidad son medidos virtualmente en tiempo de programación, teniendo en cuenta cada etapa del ciclo de desarrollo del software, esto es: diseño, codificación, depurado, ajustes y mantenimiento. Adquirir buenas costumbres para desarrollar software es esencial cuando se trabaja con aplicaciones paralelas, especialmente si alguien ajeno a la labor actual tendrá que trabajar sobre ella posteriormente. Portabilidad. Gracias a la estandarización de varias API’s (Interfaces para el Programa de Aplicación), tales como MPI, Hilos POSIX, HPF y OpenMP, las cuestiones de portabilidad no son tan problemáticas como antiguamente; sin embargo, aún existen pequeños asuntos que dificultan esta tarea. Todos los temas actuales asociados con la portabilidad de programas serie son aplicables a la portabilidad de programas paralelos. Aunque existan estándares para varios API’s, las implementaciones diferirán en algunos detalles, lo que se traduce en modificaciones del código para efectuar la portabilidad. Los sistemas operativos pueden jugar un papel crucial en los asuntos de portabilidad de códigos. Las arquitecturas hardware son altamente variables de unas a otras, lo que afecta en gran medida a la portabilidad. 38 Requerimientos de recursos. El primer objetivo de programación paralela es decrementar el tiempo de ejecución; sin embargo, en orden a lograr esto, se requiere mayor tiempo de CPU. Por ejemplo, un código paralelo que corre en una hora en ocho procesadores a la vez, en uno sólo procesador tardará ocho horas (tardará un poco menos, ya que en paralelo hay que tener en cuenta las comunicaciones interprocesos, etc.). La cantidad de memoria requerida puede ser mayor para códigos paralelos que para códigos en serie, debido a la necesidad de replicar datos y por la sobrecarga asociada con las librerías y subsistemas de soporte paralelo. Para un programa paralelo de corta ejecución, se puede producir un empeoramiento del funcionamiento comparado con el de su programa serie análogo. Los costes operativos asociados con el establecimiento del entorno paralelo, la creación y terminación de tareas, y las comunicaciones pueden consistir en una significante porción del tiempo total de ejecución para un programa paralelo corto. Esto es muy ineficiente. Escalabilidad. La capacidad de escalar el funcionamiento de un programa paralelo es el resultado de un cierto número de factores interrelacionados. Añadir simplemente más máquinas es raramente la solución El algoritmo utilizado para paralelizar puede tener límite de escalabilidad inherentes. En algún punto, añadir más recursos causa el decremento del rendimiento. La mayoría de las soluciones paralelas muestran esta característica en algún momento. Los factores relacionados con el hardware juegan un papel significante en la escalabilidad. Por ejemplo: ancho de banda del bus memoria-CPU en una máquina SMP, ancho de banda de la red, cantidad de memoria disponible en una máquina dada o en un conjunto de ellas, velocidad de reloj del procesador, etc. El software de las librerías y subsistemas de soporte, independiente de la aplicación paralela, puede limitar la escalabilidad. 39 2.5. MPI Vs. PVM. IMPLEMENTACIONES DE MPI. Ya se hablado sobre el modelo de programación paralelo de paso de mensajes. Concretamente, en la sección 2.3.8., se ha hecho una pequeña introducción al estándar MPI. No obstante, MPI no es la única implementación del modelo mencionado, sino que existen otros, y de entre ellos, el más popular junto con MPI es PVM. A continuación, hablaremos de estos dos modelos, y llegaremos a la conclusión de que MPI es la implementación que necesitamos para nuestro trabajo. Veamos, primeramente, una breve descripción de PVM. 2.5.1. SÍNTESIS DE PVM. El software PVM (Parallel Virtual Machine) proporciona un soporte unificado, dentro del cual los programas paralelos pueden ser desarrollados de forma eficiente y directa, utilizando el hardware existente. PVM habilita a un conjunto de sistemas de ordenadores heterogéneos para ser vistos como una máquina paralela virtual. PVM maneja, de forma transparente, todo el encaminamiento de mensajes, conversión de datos y programación de tareas, a través de una red de arquitecturas de ordenadores incompatibles. El software PVM es muy portable. El modelo de computación de PVM es simple, pero muy general, y da cabida a una amplia variedad de estructuras de programas de aplicación. La interfaz de programación es intencionadamente directa, de modo que permite la implementación de estructuras de programa simples, de forma intuitiva. La filosofía es la siguiente: el usuario escribe su código de aplicación como una colección de tareas que cooperan entre ellas. Cada tarea accede a los recursos de PVM a través de una librería de interfaces de rutinas estándar. Estas rutinas permiten la inicialización y terminación de tareas a través de la red, así como la comunicación y sincronización entre las mismas. Las primitivas de paso de mensajes de PVM están orientadas a operación heterogénea, involucrando construcciones fuertemente tipadas para buffering y transmisión. Las construcciones de comunicación incluyen estructuras de datos para el envío y recepción de mensajes, así como primitivas de alto nivel, tales como difusión, sincronización de barrera y suma global (reducción). Las tareas PVM, pueden tener estructuras de dependencia y de control absoluto, es decir, en cualquier punto de la ejecución de una aplicación concurrente, cualquier tarea existente puede comenzar o parar otras tareas, o añadir o eliminar ordenadores de la máquina virtual. Todo proceso se puede comunicar y/o sincronizar con otro. Cualquier estructura de dependencia y de control específica puede ser implementada bajo el sistema PVM, con el apropiado uso de construcciones PVM y sentencias de control de flujo de lenguaje máquina. Con PVM se pueden resolver grandes problemas de computación, aprovechando el agregado de potencia y memoria del conjunto de ordenadores que pertenecen a la máquina virtual. Debido a su naturaleza general (específicamente, el concepto de máquina virtual), y también por su simple pero completa interfaz de programación, el sistema PVM ha logrado una gran aceptación en la comunidad científica de computación de alto rendimiento. Como ya se ha comentado, en su momento vimos una pequeña introducción a MPI, de modo que lo siguiente es comparar los dos modelos: MPI y PVM. En la sección 2.5.4., tendrá lugar una explicación más profunda sobre el funcionamiento de MPI, ya que es el modelo escogido para llevar a cabo nuestro trabajo. 40 2.5.2. DIFERENCIAS ENTRE MPI Y PVM. A menudo se comparan MPI y PVM. Normalmente, estas comparaciones empiezan con la no mencionada suposición de que ambas representan diferentes soluciones para el mismo problema. En realidad, a menudo los dos sistemas están resolviendo problemas diferentes. Aunque MPI y PVM hayan surgido de orígenes diferentes, ambas son especificaciones de librerías que pueden utilizarse para el cómputo en paralelo, por esta razón es natural compararlas. La complicación de comparar estos dos métodos es que surgen determinados problemas, tales como que ambos utilizan la misma palabra para conceptos distintos. Por ejemplo, un grupo de MPI y un grupo de PVM son objetos bastantes distintos, aunque tengan similaridades superficiales. No veremos aquí casi ninguna diferencia. Si es necesario, se aconseja acudir a [22], donde se realiza una comparativa de MPI y PVM bastante aclaratoria. Sí comentaremos una diferencia notable, por la importancia que tiene para nuestro trabajo, y que es una de las explicaciones de porqué se ha elegido MPI como solución adoptada. A continuación se comenta esta diferencia. PVM, a través de su máquina virtual (implementada como demonios PVM), proporciona un sistema operativo distribuido, simple pero útil. Algunas interfaces especiales, tales como “pvm_reg_tasker”, permiten al sistema PVM interactuar con otros sistemas de gestión de recursos. MPI no suministra una máquina virtual, ni siquiera MPI-2. Según el MPI Forum, MPI sería una librería para escribir programas de aplicación, no un sistema operativo distribuido. MPI, más bien, proporciona un camino, a través de un nuevo objeto MPI (MPI_Info), para comunicarse con cualquier mecanismo que suministre los servicios del sistema operativo distribuido. El cluster montado en nuestro entorno de trabajo es homogéneo, todos los nodos disponen del mismo sistema operativo (ya instalado) y tienen la misma configuración hardware. Esta es la razón de que utilicemos MPI, ya que el sistema operativo ya instalado en los nodos es el que se encarga de la gestión de recursos. No obstante, la razón de mayor peso para escoger MPI frente a PVM, es que la librería de cálculo en paralelo PETSc, que en breve describiremos, hace uso de MPI. Esta simple razón marca la forma de trabajar y sus implicaciones pertinentes. 2.5.3. IMPLEMENTACIONES DE MPI. Existen diversos grupos trabajando en implementaciones de MPI. A continuación, mostraremos una lista con las principales. MPICH: implementación MPI portable y de libre distribución, creado por ANL/MSU. ANL es Argonne National Laboratory, que también es el que ha implementado la librería PETSc. Sobre MPI y PETSc se centra nuestro trabajo, siendo MPICH la implementación de MPI escogida. Es por ello, que es de agradecer todo el trabajo realizado por el Laboratorio Argonne, ya que nos ha facilitado en gran medida nuestra labor, sobre todo a la hora de la implantación del sistema. MP-MPICH: MP viene de Multi-Plataforma. MP-MPICH está basado en MPICH 1.1.2. Actualmente, MP-MPICH consiste en dos partes que están integradas dentro del único árbol MPICH: 41 - - NT-MPICH: es un puerto-Windows NT. Contiene un nuevo DispositivoWinsocket (ch_wsock) para comunicación de baja latencia vía TCP/IP, y una adaptación del dispositivo de memoria compartida ch_lfshmem. Contiene el dispositivo ch_wsock2, que básicamente es una combinación de ch_wsock y ch_lfshmem. NT-MPICH soporta las funciones de logging de MPE y MPI-IO (MPI-E/S) vía ROMIO. También están disponibles un servicio de ejecución remota y una shell gráfica. SCI-MPICH: es la primera implementación de MPI disponible de libre distribución, que se comunica directamente a través de una red SCI (Scalable Coherent Interface) rápida. SCI es un estándar cuya implementación más destacada es una red de área de sistema (SAN) de altas prestaciones para interconexión de clusters. SCI-MPICH está disponible para Solaris x86, Linux y también para Windows NT. winmpich: es una implementación de MPI para Windows NT, creada por la Universidad del Estado de Misisipi. WMPI II, WMPI 1.5 y PaTENT MPI: son implementaciones comerciales de MPI. WMPI II es una implementación completa del estándar MPI-2 para las versiones de 32 y 64 bit de los sistemas operativos de Windows y Linux. Máquina Virtual de Alto Rendimiento de Illinois: incluye una implementación de MPI (basada en MPICH). Es un sucesor del proyecto Mensajes Rápidos. MPI-BIP: es una implementación de MPI utilizando la API BIP. ScaMPI: viene de Scali AS thread-safe, implementación de MPI de alto rendimiento. Actualmente corre sobre memoria compartida de SMP y SCI. Los sistemas operativos soportados son Solaris (x86 y SPARC), Linux (x86) y Windows NT (x86). MPICH-Madeleine: implementación multi-protocolo de MPI. Implementación UNIFY de la Universidad del Estado de Misisipi: proporciona un subconjunto de MPI dentro del entorno PVM, pero sin sacrificar las llamadas PVM ya existentes. Implementación LAM de MPI. MPI para el Fujitsu AP1000: de la Universidad Nacional Australiana. Cray MPI Product, para el T3D: de la Investigación Cray y el Centro de Computación Paralela de Edimburgo. MPI de IBM: par el SP. MPI de IBM: par el OS/390. MPI de SGI: disponible para máquinas SGI de 64 bit mips3 y mips4. 42 PowerMPI, para sistemas Parsytec. Implementaciones para multicomputador, para NT y para cluster comercial MPI/Pro: de MPI Software Technology, Inc. Implementación parcial de MPI para el cluster Macintosh G3. STAMPI: es una librería de comunicación basada en MPI para clusters heterogéneos. Como ya se ha comentado, la implementación escogida de todas las citadas (y más que no se ha citado) de MPI es MPICH. La elección se debe a que a MPICH es de libre distribución, y proporciona buenos manuales de instalación y de usuario. Además, para la utilización de librería PETSc, parte central de nuestro trabajo, es una buena opción el escoger MPICH, ya que funciona a la perfección (así se ha demostrado en las pruebas realizadas). MPICH se distribuye para varias plataformas distintas, así como en código fuente. Esta última ha sido nuestra elección, para así compilar este código fuente y adaptarlo a nuestro sistema, que trabaja sobre un entorno Cygwin (emulación de UNIX sobre Windows). En el ANEXO I, se explica con detalle el proceso de instalación de MPICH en nuestro sistema. Para obtener información adicional, se aconseja dirigirse a la guía de instalación de MPICH, que se incluye dentro del paquete mpich2-1.0.4p1.tar.gz, al que se hace referencia en el ANEXO I. 43 2.6. PRINCIPIOS BÁSICOS DE FUNCIONAMIENTO DE MPI. 2.6.1. INTRODUCCIÓN. En el inicio del apartado 2.3.3, se presentaba una introducción a los modelos de programación en paralelo, donde se incluía el modelo de paso de mensajes. Posteriormente, en la sección 2.3.3.5., se habló brevemente de los modelos de computación en paralelo SPMD (Single Program, Multiple Data) y MPMD (Multipe Program, Multiple Data). A lo largo del tiempo, se han ido implementando diversas variantes de los modelos que se han ido proponiendo. Por ejemplo, HPF es una interfaz SPMD. Realmente, SPMD y MPMD son esencialmente lo mismo, puesto que cualquier modelo MPMD puede descomponerse en SPMD’s. Una computación en paralelo, consiste en que varios procesos trabajan a la vez sobre algunos datos locales. Cada proceso tiene variables puramente locales, y no hay ningún mecanismo por el que cualquier proceso pueda acceder directamente a la memoria de otro. La compartición de datos entre procesos tiene lugar a través del paso de mensajes, esto es, envío y recepción explícitos de datos entre procesos. Hacer notar que el modelo involucra a procesos que, en principio, no necesariamente han de correr en diferentes procesadores. Una primera razón para la utilización de este modelo, es que es extremadamente general, por tanto, puede ser implementado en una gran variedad de plataformas. De forma general, este modelo permite más control sobre la localización de datos y el flujo dentro de una aplicación paralela que, por ejemplo, el modelo de memoria compartida. MPI, del inglés Message Pass Interface, es una implementación del paradigma de paso de mensajes, que pretende ofrecer una realización del modelo SPMD. MPI es una librería de funciones (en C) o subrutinas (en Fortran) que pueden ser incluidas en el código fuente de nuestro programa de aplicación para lograr la comunicación de datos entre procesos. MPI fue desarrollado tras dos años de discusiones dirigidas por el MPI Forum, un grupo de sesenta personas representando unas cuarenta organizaciones. El estándar MPI-1 fue definido en 1994. Este especifica los nombres, secuencias de llamada y resultados de subrutinas y funciones para ser invocados desde Fortran 77 y C, respectivamente. Todas las implementaciones de MPI deben ser conformes a las reglas definidas, de modo que se asegure la portabilidad. Los programas MPI deberían correr en cualquier plataforma que soporte el estándar MPI. La implementación detallada de las librerías se deja para los fabricantes, que son libres de producir versiones optimizadas para sus máquinas. El estándar MPI-2 provee características adicionales no presentes en MPI-1, incluyendo herramientas para E/S paralela y gestión dinámica de procesos, entre otras. MPI, en su interés de buscar la portabilidad del código, proporciona soporte para diversas arquitecturas paralelas heterogéneas. Además, provee una gran cantidad de funcionalidad, incluyendo distintas formas de comunicación, rutinas especiales para operaciones colectivas comunes y la capacidad de manejar tipos de datos definidos por el usuario y diferentes topologías. 44 Los programas de paso de mensajes, consisten en múltiples instancias de un programa serie que se comunican por llamadas de librería. En líneas generales, estas llamadas se pueden dividir en cuatro clases: 1. Llamadas utilizadas para inicializar, gestionar y terminar las comunicaciones. 2. Llamadas usadas para la comunicación entre dos procesadores. 3. Llamadas para realizar operaciones de comunicaciones entre grupos de procesadores. 4. Llamadas utilizadas para crear tipos de datos arbitrarios. La primera clase, consiste en llamadas para comenzar las comunicaciones, identificando el número de procesadores que se utilizan para la ejecución en paralelo, creando grupos de procesadores e identificando en qué procesador está corriendo una instancia particular de un programa. La segunda clase de llamadas, denominadas operaciones de comunicaciones punto a punto, consiste en diferentes tipos de operaciones de envío y recepción. La tercera clase son las operaciones colectivas, que proporcionan sincronización o ciertos tipos de operaciones de comunicaciones bien definidas entre grupos de procesos. La última clase, proporciona flexibilidad en atención a estructuras de datos complicadas. 2.6.2. CARACTERÍSTICAS DE UN PROGRAMA MPI. Todos los programas que hacen uso de MPI presentan la siguiente estructura: 1. 2. 3. 4. 5. Inclusión del fichero de cabecera MPI. Declaraciones de variables. Inicialización del entorno MPI. Realización de la computación y de las llamadas de comunicación MPI. Cierre de las comunicaciones MPI. El fichero de cabecera de MPI contiene definiciones y prototipos de funciones. Para código escrito en C, este fichero de cabecera es ‘mpi.h’, y para Fortran es ‘mpif.h’. Posteriormente, tras la declaración de variables, cada proceso llama a una rutina MPI que inicializa el entorno de paso de mensajes. Todas las llamadas a las rutinas de comunicación MPI deben ser posteriores a la inicialización. Finalmente, antes de que finalice el programa, cada proceso debe llamar una rutina MPI que finaliza las comunicaciones. Ninguna rutina MPI debe ser llamada después de esto. Si algún proceso no alcanza este punto durante la ejecución, el programa se colgará. Los nombres de todas las entidades MPI (rutinas, constantes, tipos, etc.), comienzan con ‘MPI_’, para evitar conflictos. Así, los nombres de las rutinas en Fortran tienen el formato “MPI_XXXXX (parámetros, IERR)” (todo en mayúsculas), y en C “MPI_Xxxxx (parámetros)”. Las constantes MPI van todas en mayúsculas, tanto en C como en Fortran, por ejemplo: MPI_COMM_WORLD, MPI_REAL, etc. Los nombres de los tipos definidos en C, corresponden a muchas entidades MPI (en Fortran son todos enteros), y siguen la convención de nombrado de funciones C ya descrita; por ejemplo, MPI_Comm es un tipo correspondiente a un comunicador MPI. 45 Todos los prototipos de rutinas MPI, comandos y constantes, se encuentran muy bien explicados en el manual Web de MPI [18]. 2.6.2.1. RUTINAS MPI Y VALORES DE RETORNO. Las rutinas MPI son implementadas como funciones en C y subrutinas en Fortran. En cualquier caso, se devuelve un código de error, el cual determina si la ejecución de la rutina fue o no exitosa. En el caso de C, las funciones MPI devuelven un entero, que indica el estado de salida de la llamada, esto es: int err; … err = MPI_Xxxx(parámetros) ; … En Fortran, las subrutinas MPI tienen un argumento entero adicional, que siempre es el último de la lista de argumentos, y que contiene el estado de error cuando retorna la llamada. Esto se muestra a continuación: INTEGER IERR; … CALL MPI_XXXXX(parámetros, IERR) ; … El código de error devuelto es MPI_SUCCESS si la rutina se ejecutó satisfactoriamente. Si, por el contrario, ocurrió un error, el valor del entero devuelto depende de la implementación. 2.6.2.2. INDICADORES MPI. MPI define y mantiene sus propias estructuras de datos internas, relacionadas con la comunicación y demás aspectos que conciernen el uso de MPI. Se puede hacer referencia a esos datos a través de indicadores, que son devueltos por distintas llamadas MPI, y pueden ser utilizados cono argumentos en otras llamadas MPI. En C, los indicadores son punteros a tipos de datos especialmente definidos (creados mediante el mecanismo ‘typedef’ de C). Los arrays son indexados comenzando por ‘0’. En Fortran, los indicadores son enteros (posiblemente arrays de enteros), y los arrays son indexados comenzando por ‘1’. Por ejemplo, MPI_SUCCESS es un entero en C y en Fortran. MPI_COMM_WORLD en C es un objeto de tipo MPI_Comm (un comunicador), y en Fortran es un entero. Los indicadores pueden ser copiados utilizando la operación de asignación estándar en C y en Fortran. 2.6.2.3. TIPOS DE DATOS MPI. MPI proporciona sus propios tipos de datos de referencia, correspondientes a los distintos tipos de datos elementales de C y Fortran. 46 Así mismo, MPI permite la traducción automática entre las diferentes representaciones de tipos en un entorno heterogéneo. Como norma general, los tipos de datos MPI dados en una recepción deben coincidir con los tipos de datos especificados en un envío. Además, MPI permite definir tipos de datos arbitrarios construidos a partir de los tipos básicos, aunque esto no se verá aquí (si es necesario, se aconseja dirigirse al estándar de MPI, cuya dirección Web se proporciona en la sección 6). En las tablas 3 y 4, se presenta una comparativa los tipos de datos MPI y los de C y Fortran. Tipos básicos MPI MPI_CHAR MPI_SHORT MPI_INT MPI_LONG MPI_UNSIGNED_CHAR MPI_UNSIGNED_SHORT MPI_UNSIGNED MPI_UNSIGNED_LONG MPI_FLOAT MPI_DOUBLE MPI_LONG_DOUBLE MPI_BYTE MPI_PACKED Tipos básicos de C signed char signed short int signed int signed long int unsigned char unsigned short int unsigned int unsigned long int float double long double (none) (none) Tipos básicos MPI MPI_INTEGER MPI_REAL MPI_DOUBLE_PRECISION MPI_COMPLEX MPI_CHARACTER MPI_LOGICAL MPI_BYTE MPI_PACKED Tipos básicos de Fortran integer real double precision complex character(1) logical (none) (none) Tabla 4. Tipos de datos MPI frente a tipos de datos de Fortran. Tabla 3. Tipos de datos MPI frente a tipos de datos de C. 2.6.2.4. INICIALIZACIÓN DE MPI. La primera rutina llamada en cualquier programa MPI debe ser la rutina de inicialización ‘MPI_INIT’. Ésta establece el entorno MPI, devolviendo un código de error si ha habido algún problema. Además, esta rutina debe ser llamada una sola vez en cualquier programa. En C, la declaración es la siguiente: int err; … err = MPI_Init(&argc, &argv); Observar que los argumentos de ‘MPI_Init’ son las direcciones de argc y argv, las variables que contienen los argumentos de línea de comandos pasados al programa. En Fortran: INTEGER IERR … CALL MPI_INIT(IERR) 47 2.6.2.5. CIERRE DE LAS COMUNICACIONES MPI. Tras terminar toda la fase de computación y de comunicaciones MPI dentro de un programa, es importante no olvidarse de destruir el entorno MPI creado al principio con MPI_INIT antes de terminar la ejecución. Para lograr esto, se hace uso de la rutina ‘MPI_FINALIZE’, que destruye las estructuras de datos MPI, cancela las operaciones no completadas, etc. Esta rutina debe ser llamada por todos los procesos, si no, el programa se colgará. La forma de llamar a esta rutina en C es: int err; ... err = MPI_Finalize(); En Fortran: INTEGER IERR ... call MPI_FINALIZE(IERR) 2.6.3. COMUNICACIONES PUNTO A PUNTO. La comunicación punto a punto es la facilidad de comunicación fundamental proporcionada por la librería MPI. Este tipo de comunicación es conceptualmente simple: un proceso envía un mensaje y otro lo recibe. Los datos no son transferidos sin la participación de ambos procesos, es decir, se requiere explícitamente un envío y una recepción.Sin embargo, en la práctica no es tan sencillo. Por ejemplo, puede que un proceso tenga muchos mensajes esperando para ser recibidos. En este caso, un aspecto decisivo es cómo determinan MPI y el proceso receptor la forma en que se reciben los mensajes. Otra cuestión importante es si las rutinas de envío y recepción de mensajes inician las operaciones de comunicación y retornan inmediatamente o esperan a que se complete la operación iniciada antes de retornar. Las operaciones de comunicación que están por debajo son las mismas en ambos casos, pero la interfaz de programación es muy diferente. 2.6.3.1. FUNDAMENTOS DE LAS COMUNICACIONES PUNTO A PUNTO. Los tres aspectos fundamentales de las comunicaciones punto a punto MPI son: fuente y destino, mensajes y envío y recepción de mensajes. Las comunicaciones punto a punto requieren la participación activa de los procesos en ambos lados, el proceso fuente envía y el proceso destino recibe. En general, los procesos fuente y destino operan de forma asíncrona. El proceso fuente puede terminar de enviar un mensaje mucho antes de que el proceso destino lo empiece a recibir (mensajes pendientes). Así mismo, el proceso destino puede que comience a recibir un mensaje que todavía no terminado de ser enviado. 48 Los mensajes pendientes no se mantienen en una simple cola FIFO, sino que poseen varios atributos que puede utilizar el proceso destino para determinar qué mensaje recibir. Un mensaje está formado por un conjunto de bloques de datos, que constituyen el cuerpo del mensaje, y por un envoltorio, que indica los procesos origen y destino. MPI utiliza tres partes de información para caracterizar el cuerpo del mensaje de forma flexible: un buffer, el tipo de datos a enviar y el número de elementos del tipo de datos a ser enviados. El buffer hace referencia a la localización de comienzo en memoria donde se encuentran los datos salientes (para enviar) o donde se almacenan los datos entrantes (para recibir). En el caso más sencillo, el tipo de datos es un tipo elemental tal como flota/REAL, int/INTEGER, etc. En aplicaciones más avanzadas, puede ser un tipo definido por el usuario, que es combinación de los tipos básicos. MPI estandariza la designación de tipos elementales. Esto quiere decir que no hemos de preocuparnos por la representación de dichos tipos en las distintas máquinas que conforman un entorno heterogéneo. El envoltorio tiene cuatro partes: el proceso fuente, el proceso destino, el comunicador que incluye a los procesos fuente y destino, y una etiqueta utilizada para clasificar mensajes. El envío de mensajes es directo. La identidad del emisor está determinada de forma implícita, pero el resto del mensaje (cuerpo y envoltorio) es dado explícitamente por el proceso emisor. La recepción del mensaje no es tan simple, debido a los mensajes pendientes que aún no han sido procesados por el receptor. Para recibir un mensaje, un proceso especifica un envoltorio de un mensaje que MPI compara con los envoltorios de los mensajes pendientes. Si hay alguna coincidencia, se recibe un mensaje. En caso contrario, la operación de recepción no puede ser completada hasta que se envíe un mensaje coincidente. Además, el proceso receptor del mensaje debe proporcionar el almacenamiento suficiente para copiar el cuerpo del mensaje. 2.6.3.2. MODOS DE ENVÍO Y COMUNICACIONES BLOQUEANTES. MPI proporciona una gran flexibilidad a la hora de especificar cómo son enviados los mensajes. Hay una gran variedad de modos de comunicación que definen el procedimiento utilizado para transmitir el mensaje, así como un conjunto de criterios para determinar cuando se completa un evento de comunicación (un envío o recepción particular). Por ejemplo, un envío síncrono es completado cuando el acuse de recibo del mensaje ha sido aceptado en su destino. Un envío bufferado es completado cuando los datos salientes han sido copiados a un buffer local (no se especifica nada sobre la llegada del mensaje a su destino). En todos los casos, la finalización de un envío implica que es seguro sobrescribir las áreas de memoria donde los datos fueron almacenados originalmente. Hay cuatro modos de comunicación disponibles para el envío de datos: estándar, síncrono, bufferado y preparado. Para recepción sólo hay un modo: una recepción es completada cuando los datos entrantes realmente han llegado, y están disponibles para su utilización. Además del modo de envío, un envío o recepción puede ser bloqueante o no-bloqueante. Un envío o recepción bloqueante no regresa de la llamada a la subrutina hasta que la operación ha sido realmente completada. Así se asegura que el criterio de terminación pertinente ha sido satisfecho antes de que el proceso llamante pueda proceder con su ejecución. 49 Un envío o recepción no-bloqueante regresa inmediatamente de la subrutina, sin ninguna información sobre si el criterio de terminación ha sido satisfecho. Esto tiene la ventaja de que el procesador puede realizar otras tareas, mientras las comunicaciones tienen lugar en segundo plano. Posteriormente, se puede verificar si la operación ha sido realmente completada. La utilización de comunicaciones bloqueantes o no-bloqueantes será función de las necesidades de programación, dejándose esta tarea al programador de aplicaciones paralelas. 2.6.3.3. ENVÍO Y RECEPCIÓN BLOQUEANTES. Las dos funciones, ‘MPI_SEND’ y ‘MPI_RECV’, son las rutinas de comunicación básicas en MPI. Ambas funciones bloquean al proceso llamante hasta que la operación de comunicación ha sido completada. El bloqueo puede dar lugar a atascos del programa. Un atasco ocurre cuando dos o más procesos son bloqueados, y cada uno espera a otro para continuar con su ejecución, ya que ésta depende, a su vez, de la ejecución de otro proceso. Es importante detectar la posibilidad de que ocurran estos atascos, y hacer todo lo necesario para evitarlos. Al realizar una llamada ‘MPI_SEND’, la rutina acepta los siguientes argumentos: del cuerpo del mensaje tomará el buffer, el tipo de datos y el número de los mismos dentro del buffer; del envoltorio aceptará el proceso destino, la etiqueta con la clase del mensaje y el comunicador (el proceso fuente se define implícitamente). Al llamar a ‘MPI_RECV’, los argumentos que toma dicha rutina son similares a los que acepta ‘MPI_SEND’, con la diferencia de que algunos de los parámetros se utilizan de forma distinta. El cuerpo del mensaje tiene los mismos parámetros que los citados para el envío; el envoltorio contendrá el proceso fuente, la etiqueta de la clase del mensaje y el comunicador (el proceso destino se define de forma implícita). Además hay un argumento más: el estado, que da información sobre el mensaje que ha sido recibido. Los argumentos del envoltorio determinan qué mensaje puede ser recibido por la llamada, de modo que el proceso fuente, la etiqueta y el comunicador deben coincidir con alguno de los mensajes pendientes, en orden a ser recibidos. Se pueden utilizar comodines para el valor del proceso fuente y de la etiqueta, de modo que se puedan recibir mensajes desde cualquier origen y con cualquier etiqueta. No se puede hacer lo mismo con los comunicadores. En general, el emisor y el receptor deben estar de acuerdo en el tipo de datos del mensaje, y es responsabilidad del programador garantizar este acuerdo. Si el emisor y el receptor utilizan tipos de datos incompatibles, el resultado será indefinido. Cuando se envía un mensaje utilizando ‘MPI_SEND’, puede ocurrir una de las dos cosas siguientes: - El mensaje puede ser copiado dentro un buffer MPI interno y transferido a su destino más tarde, en segundo plano. Este tipo de comunicación es asíncrona. O bien, el mensaje se puede dejar donde está, en las variables del programa, hasta que el proceso destino esté listo para recibirlo. En ese momento, el mensaje se transfiere a su destino. En este caso, es necesaria una sincronización entre emisor y receptor del mensaje. 50 La primera opción permite al proceso remitente dedicarse a otras cosas después de que se complete la copia. La segunda opción minimiza el uso de memoria, pero puede dar lugar a un retardo extra para el proceso remitente, retardo que puede ser significativo. 2.6.3.4. ENVÍO Y RECEPCIÓN NO-BLOQUEANTES. MPI proporciona otra manera de invocar las operaciones de envío y recepción. Es posible separar el comienzo de una operación de envío o recepción de su finalización, haciendo dos llamadas separadas a MPI. La primera llamada inicia la operación y, la segunda, la completa. Entre las dos llamadas, el programa puede seguir ejecutando las operaciones que se precisen. Las operaciones de comunicación subyacentes son las mismas, tanto si se realiza el envío y recepción en una llamada simple o en dos llamadas separadas. La diferencia es la interfaz utilizada. Al hecho de iniciar una operación de envío se le llama ordenar un envío, y para recepción se denomina ordenar una recepción. Una vez que un envío o recepción ha sido mandado, MPI proporciona dos maneras distintas de completarlo. Un proceso puede testear para ver si la operación ha sido completada, sin bloquear la finalización de dicha operación. Alternativamente, un proceso puede esperar a que la operación se complete. Después de mandar un envío o recepción con una llamada a una rutina no-bloqueante, el proceso de mandado necesita alguna forma de referirse a la operación ordenada. MPI utiliza indicadores de petición para este propósito. Con estos indicadores, se puede chequear el estado de las operaciones ordenadas o esperar a su finalización. Las rutinas de envío y recepción sin bloqueo son, respectivamente, ‘MPI_ISEND’ y ‘MPI_IRECV’. La secuencia de llamada para ambas rutinas es muy similar a sus rutinas análogas bloqueantes, pero incluyendo un indicador de petición. Ninguno de los argumentos pasados a estas rutinas debería ser leído o escrito hasta que se complete la operación en cuestión. Las rutinas de finalización de la operaciones de envío y recepción son: ‘MPI_WAIT’ y sus variantes para espera de finalización con bloqueo, y ‘MPI_TEST’ y sus variantes para evitar el bloqueo y realizar un chequeo sobre la terminación de la operación. El uso selectivo de rutinas no-bloqueantes hace mucho más fácil escribir código evitando los posibles atascos, cosa que es bastante ventajosa, sobre todo en sistemas que presentan grandes latencias. Por contra, se incrementa la complejidad del código, haciendo más difícil el depurado y el mantenimiento del mismo. 2.6.4. COMUNICACIONES COLECTIVAS. Aparte de las comunicaciones punto a punto entre pares individuales de procesadores, MPI incluye rutinas para realizar comunicaciones colectivas. Estas rutinas permiten que grandes grupos de procesadores se comuniquen de diferentes maneras, por ejemplo, comunicación de uno a muchos o de muchos a uno. Las rutinas de comunicación colectiva, están construidas utilizando rutinas de comunicación punto a punto, pero teniendo en cuenta algunos detalles importantes y usando los algoritmos más eficientes conocidos para realizar la operación. 51 El programador debe asegurar que todos los procesos que han de comunicarse ejecuten las mismas operaciones colectivas, y en el mismo orden. Las principales ventajas de utilizar rutinas de comunicación colectiva frente a las construcciones equivalentes con comunicaciones punto a punto son: a) Posibilidad de error significativamente reducida: con una sola línea de código realizamos una operación colectiva, lo que equivaldría a varias líneas de código con operaciones punto a punto. b) El código fuente es mucho más legible, de modo que se simplifica la depuración y el mantenimiento del mismo. c) Las formas optimizadas de rutinas colectivas son, a menudo, más rápidas que la operación equivalente expresada en términos de rutinas punto a punto. MPI proporciona las siguientes rutinas de comunicación colectiva: - Operación de difusión (broadcast). Operaciones globales de reducción (reduction), tales como: sum, min, max o reducciones definidas por el usuario. Operación de recolección (gather) de datos desde todos los procesos a uno solo. Operación de dispersión (scatter) de datos desde un proceso a todos los demás. Operación de sincronización de barrera (barrier) a través de todos los procesos. Operaciones avanzadas, donde todos los procesos reciben el mismo resultado de una operación de recolección, dispersión o reducción. A continuación, veremos las operaciones colectivas mencionadas. x Operación de difusión: El tipo más simple de operación colectiva es la difusión (‘MPI_BROADCAST’). En este tipo de operación, un proceso envía una copia de los mismos datos al resto de procesos de su grupo (más adelante veremos que es esto de los grupos). Esta forma de operar se presenta en la figura 16. Cada fila de la figura representa un proceso distinto, y cada bloque sombreado Figura 16. Broadcast. en una columna representa la localización de una porción de los datos. Los bloques con la misma letra que están localizados en múltiples procesos, contienen copias de los mismos datos. 52 x Operaciones de recolección y dispersión: Las operaciones de dispersión y recolección son las encargadas de distribuir los datos de un procesador sobre un grupo de procesadores y viceversa, respectivamente. En la figura 17 se muestran las operaciones comentadas. En una operación de dispersión (‘MPI_SCATTER’), todos los datos se encuentran inicialmente recogidos en un procesador. Después de la dispersión, los trozos de datos son distribuidos en diferentes procesadores. Figura 17. Scatter & gather. La recolección (‘MPI_GATHER’) es la operación inversa a la dispersión. Acumula partes de datos que se encuentran distribuidos a través de un conjunto de procesadores, y los reensambla en el orden apropiado en un simple procesador. x Operaciones de reducción: Una reducción es una operación colectiva, en la cual un proceso (proceso raíz) colecciona datos que se encuentran en un grupo de procesos, y los combina en un solo elemento de datos. Por ejemplo, la operación de reducción se puede utilizar para computar la suma de los elementos de un array que está distribuido en varios procesadores. Esto se muestra en la figura 18. Figura 18. Reducción. También son posibles operaciones distintas a las aritméticas, tal y como se muestra en la tabla 5. OPERACIÓN MPI_MAX MPI_MIN MPI_SUM MPI_PROD MPI_LAND MPI_BAND MPI_LOR MPI_BOR MPI_LXOR MPI_BXOR DESCRIPCIÓN Máximo. Mínimo. Suma. Producto. AND lógico. AND binario. OR lógico. OR binario. XOR lógico. XOR binario. MPI_MINLOC Calcula un mínimo global y un índice ligado al valor mínimo. Puede ser utilizado para determinar qué proceso que contiene el valor mínimo. MPI_MAXLOC Calcula un máximo global y un índice ligado al valor máximo. Al igual que antes, se puede saber cuál es el proceso que contiene el valor máximo. Tabla 5. Operaciones predefinidas disponibles para ‘MPI_REDUCE’. 53 x Operación de sincronización de barrera: Hay ocasiones en las que algunos procesadores no pueden continuar con su ejecución hasta que otros procesadores han completado las instrucciones que están ejecutando. Un ejemplo común de este comportamiento ocurre cuando el proceso raíz lee datos y los transmite a otros procesadores. El resto de procesadores deben esperar hasta que se complete la operación de E/S y se terminen de mover los datos. La rutina ‘MPI_BARRIER’ bloquea al proceso llamante hasta que todos los procesos del grupo han llamado a dicha función. Cuando ‘MPI_BARRIER’ devuelve el control al programa principal, todos los procesos son sincronizados en la barrera. ‘MPI_BARRIER’ está implementada mediante software, lo que provoca que pueda generar una sobrecarga sustancial en algunas máquinas. Esto implica que sólo se utilice sincronización por barrera en casos realmente necesarios. x Operaciones avanzadas: 1. ‘MPI_ALLGATHER’: ofrece el mismo resultado que una operación de recolección seguida de una difusión, pero de forma más eficiente. 2. ‘MPI_ALLREDUCE’: se utiliza para combinar los elementos de cada buffer de entrada de los procesos. Almacena el valor final en el buffer de recepción de todos los procesos miembros del grupo. 3. Operaciones definidas por el usuario: la operación de reducción puede definirse para que sea una operación arbitraria. 4. ‘MPI_GATHERV’ y ‘MPI_SCATTERV’: permiten trabajar con un número variante de datos (el sufijo ‘V’ de las rutinas viene de vector). 5. Otras variantes de Scattery y Gather. 2.6.5. COMUNICADORES. Un comunicador es un indicador que representa un grupo de procesadores, que se puede comunicar con otro. Se requiere el nombre del comunicador como argumento para todas las comunicaciones punto a punto y colectivas. El comunicador especificado en las llamadas de envío y recepción debe coincidir para que tenga lugar la comunicación. Los procesadores se pueden comunicar sólo si comparten un mismo comunicador. Puede que haya muchos comunicadores, y un proceso dado puede ser miembro de un número de diferentes comunicadores. Dentro de cada comunicador, los procesos son numerados consecutivamente, empezando por ‘0’. Este número de identificación es conocido como el ‘rank’ (orden) del proceso en ese comunicador. El rank se utiliza también para especificar la fuente y el destino en las llamadas de envío y recepción de datos. Si un proceso pertenece a más de un comunicador, su rank en cada uno puede ser distinto y, normalmente, lo será. MPI proporciona automáticamente un comunicador básico llamado ‘MPI_COMM_WORLD’, que es un comunicador consistente en todos los procesos. Utilizando ‘MPI_COMM_WORLD’, cada proceso se puede comunicar con cada uno del resto de procesos. 54 También es posible definir comunicadores adicionales consistentes en subconjuntos de los procesos disponibles. Con respecto a esto, decir que en MPI existen dos tipos de comunicadores: intra-comunicadores e inter-comunicadores. Los intra-comunicadores son, esencialmente, un conjunto de procesos que pueden enviarse mensajes los unos a los otros y estar involucrados en operaciones de comunicación colectiva. Por ejemplo, ‘MPI_COMM_WORLD’ es un intra-comunicador. Los inter-comunicadores, como su propio nombre indica, son utilizados para enviar mensajes entre procesos pertenecientes a intracomunicadores disjuntos. Un comunicador está formado por un grupo y un contexto. Un grupo es una colección ordenada de procesos. Si un grupo consiste en ‘p’ procesos, a cada proceso en el grupo se le asigna un único rank, que será un entero no negativo en el rango [0, p-1]. Un contexto puede ser visto como una etiqueta definida por el sistema que se asocia al grupo. Así, dos procesos que pertenecen al mismo grupo y que utilizan el mismo contexto pueden comunicarse. Este par grupo-contexto es la forma más básica de un comunicador. Se pueden asociar más datos a un comunicador. En particular, se puede imponer una topología o estructura a los procesos de un comunicador, permitiendo un esquema de direccionamiento más natural. Un proceso puede determinar su rank haciendo una llamada a la rutina ‘MPI_COMM_RANK’. La definición de esta rutina en C sería: int MPI_Comm_rank(MPI_Comm comm, int *rank); El argumento ‘comm’ es una variable del tipo ‘MPI_Comm’, un comunicador. Por ejemplo, se podría emplear ‘MPI_COMM_WORLD’ aquí o, alternativamente, se podría pasar el nombre de otro comunicador definido por el programador en la forma: MPI_Comm comm; En caso de Fortran, el prototipo de la rutina es: MPI_COMM_RANK(COMM, RANK, IERR) En este caso, los argumentos ‘COMM’, ‘RANK’ e ‘IERR’ son todos del tipo ‘INTEGER’. Un proceso cualquiera, puede obtener el tamaño (o número de procesos) que pertenecen a un comunicador. Esto se hace con la llamada a la rutina ‘MPI_COMM_SIZE’. Existen diversas rutinas en la librería MPI que realizan diversas tareas relacionadas con la gestión de comunicadores. Enunciaremos algunas de ellas, las más importantes: ‘MPI_COMM_GROUP’: determina el indicador de grupo de un comunicador. ‘MPI_GROUP_INCL’: crea un nuevo grupo de un grupo existente, especificando los procesos miembro por inclusión. ‘MPI_GROUP_EXCL’: crea un nuevo grupo de un grupo existente, especificando los procesos miembro por exclusión. ‘MPI_GROUP_RANK’: obtiene el rank del grupo del proceso llamante. 55 ‘MPI_GROUP_FREE’: libera un grupo al sistema cuando no se necesita más. ‘MPI_COMM_CREATE’: crea un nuevo comunicador para incluir procesos específicos de un comunicador existente. ‘MPI_COMM_SPLIT’: establece nuevos comunicadores a partir de uno existente. Muchos cálculos científicos y de Ingeniería tratan con matrices o rejillas (especialmente rejillas Cartesianas) consistentes en filas y columnas. Esto, a su vez, lleva a la necesidad de mapear lógicamente los procesos a geometrías de rejilla similares. Por tanto, puede que sea necesario tratar los procesos de forma distinta a la tradicional; por ejemplo, en vez de tratarlos como filas individuales, puede que sea ventajoso o incluso necesario tratar con grupos de filas, o con otras configuraciones arbitrarias. ‘MPI_COMM_SPLIT’ permite la creación de nuevos comunicadores con las flexibilidades comentadas. 2.6.6. TOPOLOGÍAS VIRTUALES. Como ya se ha comentado en la sección anterior, muchos problemas de cálculo científico y de ingeniería se reducen al final o a una serie de matrices o a alguna forma de operaciones en rejilla, ya sea integral, diferencial u otros métodos. Las dimensiones de las matrices o rejillas vienen determinadas, a menudo, por problemas físicos; de hecho, esta es la parte que más se acerca al nivel de abstracción de nuestro trabajo con estructuras. Frecuentemente, en multiprocesamiento, estas matrices o rejillas a las que hacemos referencia, son particionadas o descompuestas en dominios, de modo que cada partición o subdominio se asigna a un proceso. Un ejemplo simple es el de una matriz m x n, descompuesta en p submatrices de dimensiones q x n, a cada una de las cuales se le asigna uno de los p procesos para trabajar sobre ellas. En este caso, cada proceso representa una submatriz distinta de manera directa. Sin embargo, un algoritmo puede ordenar que la matriz sea descompuesta en un rejilla lógica de dimensiones p x q, cuyos elementos son matrices r x s. Esto se puede deber a varias razones: por consideraciones de eficiencia, facilidad en la implementación del código, legibilidad del código, etc. Aunque es posible referirse a cada una de esos subdominios p x q mediante una numeración lineal, es obvio que un mapeado de una ordenación de procesos lineal a una numeración virtual en 2D, daría lugar a una representación computacional más clara y natural. Para lograr este mapeado de numeración, así como otros esquemas de topología virtuales, la librería MPI proporciona dos tipos de rutinas: rutinas para topologías Cartesianas y rutinas para topologías gráficas. No veremos aquí las rutinas mencionadas para no alargar este pequeño acercamiento a MPI, además de que no ha sido necesaria su utilización a la hora de trabajar con la librería de cálculo PETSc. La librería PETSc proporciona métodos de más alto nivel que MPI para la partición y numeración de matrices y rejillas. No obstante, PETSc hace uso de MPI como capa de nivel inferior para conseguir estos propósitos. Como ya se ha comentado a lo largo de todo el acercamiento a la librería MPI, para obtener información sobre el uso de cualquier rutina MPI se aconseja dirigirse a [18]. 56 2.6.7. ENTRADA / SALIDA PARALELA: MPI-2 Vs. MPI-1 2.6.7.1. INTRODUCCIÓN. En este punto, veremos cómo múltiples procesos pueden compartir datos almacenados en espacios de memoria separados. Esto se logra mediante el envío de mensajes entre procesos, puesto que seguimos hablando de MPI. En esta introducción a la E/S paralela, nos centraremos en la E/S referida al manejo de ficheros. El tema de la E/S paralela cubre la cuestión de cómo son distribuidos los datos entre los dispositivos de E/S. Mientras el subsistema de memoria puede ser diferente de una máquina a otra, los métodos lógicos de acceso a memoria son, en general, comunes, esto es, debería aplicarse el mismo modelo de una máquina a otra. La E/S paralela es complicada por el asunto de que las configuraciones físicas y lógicas difieren de un ordenador a otro. MPI-2 es la primera versión de MPI que incluye rutinas para el maneja de E/S paralela, no ocurre así con MPI-1. El estilo de programación tradicional, enseña que los programas pueden ser descompuestos por funciones en tres secciones principales: Entrada. Computación. Salida. Para aplicaciones científicas, la mayoría de lo que se ha visto hasta ahora sobre MPI se dirige a la fase de computación. Con sistemas paralelos que permiten modelos de computación mayores, estas aplicaciones, a menudo, producen grandes cantidades de datos de salida. En esta situación, la E/S serie de una máquina paralela puede dar lugar a importantes penalizaciones por diversas razones: - - Las grandes cantidades de datos generadas por las aplicaciones paralelas presentarán un cuello de botella en serie si la E/S se realiza en un solo nodo. Muchas máquinas multiprocesador se construyen a partir de varios procesadores más lentos, que incrementan el tiempo de penalización a medida que la E/S serie se dirige a través de un solo procesador más lento. Algunos conjuntos de datos son demasiado largos para ser enviados de vuelta a un nodo para que los almacene en un fichero de E/S. Puede ser que el tiempo requerido para la fase de E/S a un fichero, canalizada a través de un solo procesador, sea del mismo orden o mayor que el tiempo requerido para la computación en paralelo. Esto lleva a replantearse el modelo de E/S, y a ver que el problema de tiempo del cálculo en paralelo no está muchas veces en la propia computación, sino en la E/S de datos. Por esta razón aparece la E/S paralela, para dar solución a los problemas que presenta la E/S serie. La capacidad de paralelizar la E/S, puede ofrecer mejoras significantes del rendimiento de nuestras aplicaciones paralelas. Como ejemplo de aplicaciones reales en la que esto es posible, destacar las grandes rejillas o mallas computacionales. Muchas aplicaciones nuevas, 57 están utilizando mallas y rejillas de una resolución más fina de lo habitual. Con frecuencia, los cálculos realizados en cada nodo necesitan ser almacenados para análisis posteriores. Estas grandes rejillas o mallas computacionales incrementan los requerimientos de E/S por el gran número de puntos de datos a ser salvados. Además, incrementan el tiempo de E/S, porque los datos están siendo dirigidos a través de procesadores más lentos en máquinas masivamente paralelas. Otro ejemplo de aplicaciones que incrementan su rendimiento con el uso de la E/S paralela, son aquellas que tienen pocos cálculos, pero muchas referencias on-line a bases de datos. 2.6.7.2. E/S SERIE. Para ayudar a comprender lo que conlleva la utilización de la E/S paralela, es importante echar un vistazo a las características de la E/S serie, y entonces comparar ambos métodos. En lo que a la estructura física se refiere, suele haber un procesador conectado un disco físico. Con respecto a la estructura lógica, las características son: - La E/S serie es la visión tradicional que tienen los lenguajes de alto nivel de la E/S en general. Existe un manejador de fichero único. El acceso a ficheros puede ser secuencial o aleatorio, teniendo también en cuenta los atributos de los ficheros, tales como sólo lectura/escritura, etc. Los datos pueden ser escritos con o sin formato, en modo binario o modo texto. Existen funciones integradas en el propio sistema para acceso a la E/S. 2.6.7.3. E/S PARALELA. La E/S paralela se clasifica en dos categorías principales: descomposición física y descomposición lógica. En la situación de descomposición física, múltiples procesadores escriben datos en múltiples discos, discos que son locales a cada nodo. Toda la E/S es local, con lo cual el rendimiento es mayor: cada proceso abre su propio fichero en su sistema local. Se reservan un número determinado de nodos para realizar la E/S, por lo que el rendimiento de la E/S se incrementará con el número de nodos reservados para la E/S. Este método es excelente para almacenar datos temporales que no se utilizarán tras la ejecución del programa. Los datos de los múltiples procesos se pueden combinar al final de la ejecución, si se desea. En el caso de la descomposición lógica, múltiples procesadores escriben datos en un único fichero de un único disco, de modo que los datos de los distintos procesos serán entrelazados. Desde el punto de vista de la terminología de lenguaje de alto nivel, el acceso a la E/S es directo. MPI-2 implementa este método de E/S paralela, pero con la particularidad de que los datos se almacenan en ficheros distintos, uno por cada nodo. Cada fichero se identifica por el 58 rank de cada proceso. Al final de la ejecución, los ficheros se combinan en un solo fichero de datos. Las máquinas paralelas pueden tener sistemas de E/S paralela que se ajusten a una de las dos categorías presentadas, aunque es posible tener sistemas híbridos que cumplan las características de ambas. 2.6.7.4. E/S PARALELA DE MPI-2. Uno de los cambios significantes del paso de MPI-1 a MPI-2, fue la adición de la E/S paralela. Antes de la aparición de MPI-2, se podía lograr la E/S paralela sin llamadas a MPI con métodos tales como los sistemas de ficheros paralelos, donde se llevaba a cabo una partición lógica de ficheros. La E/S paralela de MPI-2, proporciona una interfaz de alto nivel con soporte para la partición de ficheros de datos entre procesos, y una interfaz colectiva para soporte de transferencias completas de estructuras de datos entre ficheros y memorias de procesos. Ahora, la lectura y escritura de datos son tratadas mucho más como envío de mensajes a disco y recepción de mensajes desde los dispositivos de almacenamiento de datos. No se utilizan las rutinas estándar de E/S de C, C++ y Fortran. Algunas de las características destacables de la E/S paralela de MPI-2 son: - MPI-2 permite realizar la E/S paralela de forma similar al modo de enviar mensajes entre dos procesadores. No todas la implementaciones actuales de MPI implementan la E/S completa de MPI-2. MPI-2 soporta rutinas de E/S bloqueantes y no-bloqueantes, así como rutinas colectivas y no-colectivas. Incluye soporte para E/S asíncrona. Proporciona rutinas para el acceso a datos mediante pasos (strides). Tiene control sobre el esquema de ficheros físicos de los dispositivos de almacenamiento (discos). 59 2.7. LIBRERÍAS DE MATEMÁTICAS PARALELAS. 2.7.1. INTRODUCCIÓN. La gran ventaja de las librerías de Matemáticas paralelas, es que no hemos de escribir el código MPI que se necesita como soporte para implementar un programa paralelo. Lo único que se ha de hacer es llamar a una subrutina de la librería de matemáticas, que ya incluye el código MPI necesario. Estas librerías son completas y legibles, y han sido optimizadas desde un punto de vista serie y paralelo, a partir de algoritmos paralelos excelentes. Las librerías de Matemáticas de libre distribución más conocidas son: BLAS: Basic Linear Algebra Subprograms. PBLAS: Parallel Basic Linear Algebra Subprograms. BLACS: Basic Linear Algebra Communication Subprograms. LAPACK: Linear Algebra PACKage. ScaLAPACK: Scalable Linear Algebra PACKage. En la figura 19, se muestra la jerarquía que siguen estas librerías. El término “local” que se observa en la figura, quiere decir que las librerías son de tipo serie. LAPACK contiene y está construida sobre BLAS. Cada fabricante optimiza las librerías para sus propios productos, y trabaja para sistemas de procesador único. Típicamente, la optimización se realiza por medio de algoritmos bloqueantes, diseñados para mantener y reutilizar los datos críticos en los niveles inferiores de la jerarquía de memoria: registros –> caché primaria –> caché secundaria –> memoria local. Figura 19. Jerarquía de las principales librerías Matemáticas. Las librerías que yacen sobre el área con el término “global”, son las librerías paralelas. De forma análoga a lo que ocurre en el caso de las librerías serie, ScaLAPACK contiene y está construida sobre PBLAS. Estas librerías se utilizan en sistemas multiprocesador para cálculos de Álgebra lineal en paralelo. En la figura 19, se puede observar que ScaLAPACK está construida también sobre BLACS, y esto se debe a que esta última librería es la encargada de transferir los datos de la memoria local de un procesador a otra. En realidad, las rutinas de la librería BLACS son rutinas de envoltorio que llaman a una librería de paso de mensajes de más bajo nivel, la cual suele ser MPI. BLAS y PBLAS son las librerías que contienen las versiones serie y paralela, respectivamente, de los procedimientos del Álgebra lineal básica. Cada librería contiene rutinas que pertenecen a uno de los tres niveles siguientes: - Nivel 1: Operaciones vector – vector. 60 - Nivel 2: Operaciones matriz – vector. - Nivel 3: Operaciones matriz – matriz. LAPACK y ScaLAPACK contienen rutinas para cálculos de Álgebra lineal más sofisticados. Hay tres tipos de problemas avanzados que pueden resolver estas librerías: - Solución de un conjunto de ecuaciones lineales simultáneas. - Problemas de autovalores / autovectores. - Método de aproximación de mínimos cuadrados ScaLAPACK es una de las librerías paralelas más utilizadas y probadas en la comunidad científica mundial. Sin embargo, no la hemos utilizado para alcanzar nuestros objetivos, sino que hemos hecho uso de la librería PETSc, la cual introduciremos en breve. Antes de pasar a lo comentado, veremos cómo descomponer el producto de vectores y matrices para la computación en paralelo. Esto será de gran ayuda a la hora de comprender el funcionamiento de la PETSc. 2.7.2. MULTIPLICACIÓN DE VECTORES Y MATRICES. En este apartado, veremos dos ejemplos de algoritmos que implementan el paso de mensajes. En el primero, descompondremos la multiplicación vector-matriz, y en el segundo la multiplicación matriz-matriz. 2.7.2.1. EJEMPLO 1 La figura 20 muestra el esquema de descomposición de un producto de matriz por vector b = A · c. Esto se logra dividiendo la matriz en columnas o grupos de columnas, las cuales se reparten entre los distintos procesos que intervienen en la operación de cálculo paralela. Cada proceso computa su producto correspondiente (ya que son operaciones independientes, y no requieren comunicaciones) y, al final, se realiza una operación de suma de todos los vectores resultantes (podría ser una operación de reducción de MPI), dando lugar al resultado final. Figura 20. Descomposición de un producto paralelo matriz por vector. Las columnas de la matriz y los elementos del vector columna se reparten entre los procesadores mediante operaciones de dispersión (scatter) de MPI. El problema que se ha presentado está bien para implementarlo en Fortran, ya que este lenguaje almacena las matrices por columnas. Además, sólo es necesario difundir una parte del vector multiplicado a cada proceso, no el vector entero. El caso del lenguaje C es el contrario, almacena las matrices por filas, y sería necesario que todos los procesos contengan el vector multiplicado en su totalidad, para poder realizar el producto fila de la matriz por vector. Esto se presenta en la figura 21. 61 Para realizar esto mismo en Fortran, lo primero es difundir (broadcast de MPI) el vector multiplicado completo a todos los procesos. Posteriormente, se distribuyen las filas (scatter de MPI) entre los procesos y cada uno realiza su multiplicación independiente. Por último, se hace uso de la operación de recolección (gather de MPI) para obtener el vector final. Figura 21. Producto paralelo matriz por vector descompuesto por filas. 2.7.2.2. EJEMPLO 2 De forma similar, se puede descomponer la multiplicación de una matriz por otra. La filosofía es la misma, se utilizan las rutinas MPI pertinentes para la distribución de bloques de datos entre los distintos procesos, cada uno de los cuales realiza con posterioridad sus operaciones independientes. Al final, se recolectan los resultados obtenidos por cada procesador y se unen de alguna forma para dar lugar al resultado final. Para el caso de Fortran, la forma más eficiente se presenta en la figura 22, y para C en la figura 23. Figura 22. Producto paralelo matriz-matriz en Fortran. Figura 23. Producto paralelo matriz-matriz en C. 2.7.3. EVOLUCIÓN HASTA LLEGAR A PETSC. Aparte de las librerías paralelas presentadas en la introducción correspondiente a la sección 2.7.1., existen otras librerías paralelas (de Matemáticas o de cualquier otro tipo) que hacen uso de MPI; veamos algunas importantes: Las ya mencionadas PLAPACK y ScaLAPACK. Librería MPIX, que contiene un conjunto de extensiones para MPI que habilitan muchas funciones para trabajar con intercomunicadores, que previamente sólo lo hacían con intracomunicadores. Una versión paralela del nivel 3 de la librería BLAS. METIS y ParMETIS, que son paquetes software serie y paralelo respectivamente, para la partición de gráficos no estructurados y para computar el número de elementos de matrices dispersas. MP_SOLVE, utilizado para resolver sistemas de ecuaciones irregulares y dispersos con múltiples vectores solución, en sistemas multiprocesador de memoria distribuida utilizando MPI. Prometheus, un sistema lineal basado en multirejilla paralela que sirve para resolver problemas no estructurados de elementos finitos en 3D. 62 PARASOL, que es un entorno integrado para la solución de sistemas lineales dispersos; está escrito en Fortran 90 y utiliza MPI-1.1 para las comunicaciones. PETSc: “The Portable, Extensible Toolkit for Scientific Computation”. PETSc es un conjunto de librerías numéricas para la solución paralela de sistemas lineales dispersos, ecuaciones no lineales provenientes de la discretización de PDE’s, etc. El acercamiento y la familiarización con esta librería es el objetivo final de nuestro estudio, de ahí que se dedique una sección completa a dicho fin. 63