You are on page 1of 52

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, 3-
tier, 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 load-
balancing 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 Socktes-
GM 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

SMPs SGI Origin


Sun Vexx Sequent Cray T3E
Ejemplos DEC/Compaq HP Exemplar Maspar
SGI Challenge DEC/Compaq IBM SP2
IBM POWER3 IBM POWER4 (MCM)

MPI MPI
Hilos Hilos
Comunicaciones MPI
OpenMP OpenMP
Mem. Compartida Mem. Compartida

Hasta decenas de Hasta cientos de Hasta miles de


Escalabilidad
procesadores. procesadores. procesadores.

Administración del
Ancho de banda sistema.
Características Ancho de banda Memoria-CPU. Programación
destacables. Memoria-CPU. Tiempos de acceso no complicada a la hora
uniformes. del desarrollo y el
mantenimiento.

Disponibilidad
Miles de ISVs. Miles de ISVs. Cientos ISVs.
del Software

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) Comunicación punto a punto: involucra a dos tareas, una actuando


como el transmisor / productor de datos y la otra como el receptor /
consumidor.
b) 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:
1 (1)
speedup
1–P

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
1
speedup (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.

speedup
N
P = 0.50 P = 0.90 P = 0.99
10 1.82 5.26 9.17
100 1.98 9.17 50.25
1000 1.99 9.91 90.99
10000 1.99 9.91 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 Dispositivo-
Winsocket (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


laUniversidad 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. Inclusión del fichero de cabecera MPI.


2. Declaraciones de variables.
3. Inicialización del entorno MPI.
4. Realización de la computación y de las llamadas de comunicación MPI.
5. 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 de Tipos básicos


Tipos básicos MPI Tipos básicos MPI
C de Fortran
MPI_CHAR signed char MPI_INTEGER integer
MPI_SHORT signed short int MPI_REAL real
MPI_INT signed int MPI_DOUBLE_PRECISION double precision
MPI_LONG signed long int MPI_COMPLEX complex
MPI_UNSIGNED_CHAR unsigned char MPI_CHARACTER character(1)
MPI_UNSIGNED_SHORT unsigned short int MPI_LOGICAL logical
MPI_UNSIGNED unsigned int MPI_BYTE (none)
MPI_UNSIGNED_LONG unsigned long int MPI_PACKED (none)
MPI_FLOAT float
MPI_DOUBLE double Tabla 4. Tipos de datos MPI frente a tipos de datos
MPI_LONG_DOUBLE long double de Fortran.
MPI_BYTE (none)
MPI_PACKED (none)

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 Figura 17. Scatter & gather.
procesadores.

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 DESCRIPCIÓN
MPI_MAX Máximo.
MPI_MIN Mínimo.
MPI_SUM Suma.
MPI_PROD Producto.
MPI_LAND AND lógico.
MPI_BAND AND binario.
MPI_LOR OR lógico.
MPI_BOR OR binario.
MPI_LXOR XOR lógico.
MPI_BXOR XOR binario.

Calcula un mínimo global y un índice ligado al valor mínimo. Puede ser


MPI_MINLOC
utilizado para determinar qué proceso que contiene el valor mínimo.

Calcula un máximo global y un índice ligado al valor máximo. Al igual


MPI_MAXLOC
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 intra-
comunicadores 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

You might also like