6.1 introducciÓn

172
6 La Red de Comunicación de los Computadores Paralelos. Comunicación mediante Paso de Mensajes. 6.1 INTRODUCCIÓN Los principales componentes de un sistema paralelo son los computadores (procesador, memoria...), la red de comunicación y el interfaz entre ambos. En los capítulos anteriores hemos analizado los multiprocesadores que utilizan un bus como red de interconexión (sistemas SMP). En estos sistemas el espacio de direccionamiento es común para todos los procesadores y el tiempo de acceso a cualquier posición de memoria es el mismo desde cualquier procesador. Pero el bus no es una red adecuado para interconectar los procesadores de un sistema paralelo cuando el número de nodos es

Upload: others

Post on 11-Jul-2022

2 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: 6.1 INTRODUCCIÓN

▪ 6 ▪

La Red de Comunicación de los Computadores Paralelos.

Comunicación mediante Paso de Mensajes.

6.1 INTRODUCCIÓN

Los principales componentes de un sistema paralelo son los computadores (procesador, memoria...), la red de comunicación y el interfaz entre ambos. En los capítulos anteriores hemos analizado los multiprocesadores que utilizan un bus como red de interconexión (sistemas SMP). En estos sistemas el espacio de direccionamiento es común para todos los procesadores y el tiempo de acceso a cualquier posición de memoria es el mismo desde cualquier procesador. Pero el bus no es una red adecuado para interconectar los procesadores de un sistema paralelo cuando el número de nodos es

Page 2: 6.1 INTRODUCCIÓN

▪ 170 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

elevado, ya que la cantidad de tráfico que puede soportar un bus es limitada. Dicho tráfico, sin embargo, crece según aumenta el número de procesadores del sistema, y, además, no de manera lineal, por lo que los sistemas SMP más habituales suelen ser de 4 – 8 procesadores (o, como mucho, hasta 32).

Para construir sistemas con centenares o miles de procesadores tenemos que utilizar otras estructuras y otro tipo de redes de comunicación. Sea privada o sea compartida, es necesario distribuir la memoria entre los diferentes nodos del sistema, con lo que ya no podremos usar un bus como red de conexión. En este capítulo vamos a analizar las redes de interconexión que se utilizan en los sistemas paralelos, tanto de memoria compartida (DSM) como de memoria privada (MPP).

La necesidad de comunicación entre procesadores no surge con los sistemas MPP. Las redes de ordenadores son "antiguas" en el mundo de la informática; por ejemplo, la red ARPANET es de 1969. Sin embargo, a pesar de compartir ciertos aspectos con las redes denominadas LAN y WAN (local & wide area network), las redes de interconexión para sistemas MPP son especiales, ya que la necesidad de comunicación es mucho mayor, y, además, se debe llevar a cabo en mucho menos tiempo. Aunque en los dos casos se pasa información entre los procesadores, no se pueden comparar las necesidades de comunicación asociadas a la resolución de un sistema de ecuaciones mediante un sistema paralelo y el envío de un mensaje de correo electrónico entre dos usuarios de Internet. En el primer caso, la comunicación se deberá completar en microsegundos, siendo la latencia un aspecto crítico para lograr una ejecución adecuada del problema; en el segundo caso, en cambio, no. Otro aspecto que diferencia ambos ámbitos es la distancia entre los procesadores, que en las redes LAN y WAN suele ser mucho mayor (puede variar desde varios metros a kilómetros) que en los sistemas MPP (como máximo, de unos pocos metros).

En cualquier caso, no podemos olvidar arquitecturas intermedias de gran difusión como los clusters o similares, especialmente interesantes desde el punto de vista del binomio coste/rendimiento. En esos sistemas se están utilizando tanto redes de banda ancha provenientes del mundo de las redes de computadores (por ejemplo, Gigabit Ethernet), como redes de diseño específico, más rápidas y más caras (por ejemplo, InfiniBand o Myrinet).

La función de una red de comunicación en un sistema MPP es clara: en función del algoritmo que se está ejecutando en el sistema paralelo, llevar la información de un procesador a otro. Además, la latencia de la comunicación debe de ser lo más baja posible, se deben admitir muchas

Page 3: 6.1 INTRODUCCIÓN

6.2 TOPOLOGÍA DE LA RED ▪ 171 ▪

comunicaciones simultáneamente, el coste de la red debe ser bajo, y, en cierta medida, el sistema debe mantenerse operativo a pesar de que pueda haber algunos fallos en la red. Y si es posible, todo lo anterior debería ser independiente del número de procesadores que estén conectados al sistema.

La red de comunicación de los sistemas paralelos que hemos analizado hasta el momento ha sido muy simple: un bus. Desde el punto de vista del coste, un bus es una red adecuada (barata), pero tiene muchos inconvenientes. Cuando se utiliza un bus como mecanismo de interconexión los procesadores deben compartir en el tiempo el ancho de banda del bus (no se pueden enviar dos mensajes a la vez); por este motivo, no se pueden conectar demasiados procesadores mediante un bus, ya que cada vez se producirían más problemas en la comunicación (colisiones), y, en consecuencia, las latencias de las comunicaciones crecerían excesivamente. Si nos vamos al otro extremo, y conectamos todos los procesadores con todos, obtenemos un crossbar. Desde el punto de vista del rendimiento de las comunicaciones es la mejor red posible, ya que cada procesador tiene un enlace privado con todos y cada uno de los procesadores del sistema; los componentes de la red no se deben compartir, por lo que la latencia de las comunicaciones va a ser mínima. Evidentemente, el coste de esta red es muy alto, y, además, dicho coste crece exponencialmente con el número de procesadores a interconectar. Por tanto, debemos encontrar otras formas de conectar un número elevado (miles) de procesadores.

En una red de comunicación podemos distinguir dos aspectos. Por un lado, el hardware: los conmutadores o encaminadores de mensajes, los enlaces o links, y el interfaz (conexión del procesador con la red). Por otro lado, el “software”: protocolos de comunicación, a diferentes niveles. Para definir una red, se deben concretar diferentes aspectos: la topología, el algoritmo de encaminamiento, la estrategia de conmutación, el control de flujo... En los próximos apartados vamos a analizar estos aspectos, comenzando por la topología de una red, y terminando con los conflictos de comunicación en este tipo de redes.

6.2 TOPOLOGÍA DE LA RED

La topología de la red de comunicación determina las conexiones existentes entre los procesadores, y podemos analizarla desde el punto de vista de la teoría de grafos. Los nodos del grafo representan a los procesadores (o, más precisamente, a los gestores de la comunicación), y los

Page 4: 6.1 INTRODUCCIÓN

▪ 172 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

arcos del grafo se corresponden con los enlaces de la red. El comportamiento de la red se puede estudiar utilizando diferentes parámetros topológicos. Por tanto, antes de analizar las redes más utilizadas, definamos algunos de estos parámetros.

• Distancia media, d: es la media de las distancias entre todos los posibles pares de nodos. Para calcular la distancia entre dos nodos se contabiliza el número de nodos que hay que atravesar para ir desde el nodo origen al nodo destino utilizando el camino de longitud mínima. Por tanto:

)1(1,

,

−=∑=

PP

dd

P

jiji

• Diámetro, D: es la distancia máxima que existe entre cualquier par de nodos del grafo (tomando la distancia mínima de cada par de nodos). El diámetro está relacionado con la latencia máxima que podría tener una comunicación, en ausencia de otros problemas (tráfico, etc.).

Ambos parámetros, distancia media y diámetro, nos dan una primera aproximación a la latencia de las comunicaciones en la red (en promedio y en el caso peor) cuando la comunicación es aleatoria.

• Grado: indica el número de enlaces que tiene un nodo (procesador). Si el grado de todos los nodos es el mismo, se dice que la red es regular. Interesa que las redes estén compuestas por nodos con grados relativamente bajos (grado 4, por ejemplo), ya que si el grado es alto el número de conexiones también lo será, con lo que la implementación de la red puede llegar a ser muy compleja.

• Simetría: cuando todos los nodos tienen la misma visión de la red se dice que la red es simétrica. Es una característica deseable, ya que facilita la elección de los caminos para la comunicación.

• Escalabilidad: una red de comunicación debería ser fácilmente ampliable, para permitir aumentar, sin grandes problemas, el número de procesadores.

• Tolerancia a fallos: una red de comunicación debe ser segura; el sistema debe seguir funcionando aunque algún componente de la red deje de funcionar. Se trata de una condición imprescindible si el número de nodos es elevado, porque la probabilidad de que algún

Page 5: 6.1 INTRODUCCIÓN

6.3 REDES FORMADAS POR CONMUTADORES ▪ 173 ▪

nodo falle crece con el número de nodos del computador. En la misma línea, es una condición básica para aquellos sistemas que deben estar siempre en funcionamiento y cuyo mantenimiento es muy difícil o imposible.

• Conectividad: la conectividad de una red determina el número mínimo de enlaces o nodos —arco-conectividad o nodo-conectividad— que se deben estropear para que la red quede dividida en dos o más trozos. Es un parámetro relacionado con la tolerancia a fallos.

• Bisección de la red: es el número mínimo de enlaces que se deben cortar (eliminar) para dividir la red en dos partes iguales. Como veremos más adelante, este parámetro da una idea del tráfico máximo (throughput) que puede gestionar una red (bajo unas determinadas condiciones).

• Tipo de enlace: los enlaces entre nodos pueden ser unidireccionales —es decir, A→B—, o bidireccionales —esto es, A↔B—. El tipo de enlace tiene gran influencia en los parámetros de una red relacionados con la distancia (es decir, con la comunicación).

En función del tipo de enlaces, las redes se suelen dividir a veces en dos categorías: directed, si los enlaces tienen una dirección marcada; y non-directed (o undirected), si los enlaces aceptan comunicación en ambos sentidos.

En general, vamos a trabajar con enlaces bidireccionales.

Después de haber presentado algunos parámetros topológicos, analicemos las principales redes de comunicación, tanto estáticas como dinámicas. A pesar de que en la literatura existe una gran cantidad de propuestas, sólo vamos a presentar las más utilizadas.

6.3 REDES FORMADAS POR CONMUTADORES

La función de la red de comunicación de un sistema paralelo es permitir la comunicación entre procesadores (o entre procesadores y memoria), es decir, transmitir datos. Las redes de transmisión de datos se han utilizado desde hace mucho tiempo en otras áreas tecnológicas, más en concreto en la telefonía. Por ello, se ha aprovechado la experiencia acumulada durante años

Page 6: 6.1 INTRODUCCIÓN

▪ 174 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

en esa área y se ha adaptado el uso de las redes más habituales en telefonía, redes construidas a partir de conmutadores, a los sistemas de cómputo paralelo. A estas redes se les conoce también como redes dinámicas, porque el camino de comunicación entre origen y destino se construye dinámicamente (no existen enlaces fijos entre los nodos). Analicemos en qué consisten y cómo se usan estas redes.

6.3.1 El conmutador (switch)

Comencemos describiendo qué es un conmutador, “dispositivo” que sirve para construir redes dinámicas. El conmutador más simple, de grado k = 2, es un dispositivo que conecta dos entradas (E0 y E1) y dos salidas (S0 y S1). Mediante él, y en función de una señal de control, se pueden establecer las siguientes cuatro conexiones27:

Por medio de las dos primeras conexiones, la información de una de las entradas se distribuye a ambas salidas (ese tipo de comunicación se conoce como broadcast). Sin embargo, las conexiones que más nos interesan son las otras dos, en las que se conecta cada entrada con una salida, seguidas o cruzadas, para transmitir información.

Visto desde otro punto de vista, mediante un conmutador se consigue una “permutación” de las entradas en las salidas. Por ejemplo, con un conmutador de grado 2 podemos establecer las dos siguientes conexiones: (0, 1) → (0, 1) o (0, 1) → (1, 0), es decir, las dos permutaciones de las entradas.

27 En su versión más simple, un conmutador está formado por unos multiplexores y la lógica necesaria

para su control.

E0 → S0, S1 E1 → S0, S1

E0 → S0 E1 → S1

E0 → S1 E1 → S0

0

1

0

1

Conmutador k = 2

E0

E1

S0

S1

Señales de control

Page 7: 6.1 INTRODUCCIÓN

6.3 REDES FORMADAS POR CONMUTADORES ▪ 175 ▪

Las conexiones que se crean en cada conmutador son "dinámicas", ya que van a ir cambiado en el tiempo en función de las necesidades de comunicación.

6.3.2 Red crossbar

Tal y como hemos comentado anteriormente, la red de comunicación ideal sería la que permitiera, en cualquier momento y en un solo "paso", que se comunicaran cualquier par de procesadores simultáneamente. Según lo hemos definido, un conmutador de P entradas cumple con esa condición: permite efectuar P conexiones simultáneas. A esta red se la denomina también crossbar.

Un crossbar puede construirse de muchas maneras, generalmente mediante conmutadores de menor grado. En la figura se muestra un ejemplo de implementación de un crossbar que conecta cuatro procesadores entre sí, o con cuatro módulos de memoria, construido mediante conmutadores de grado 2. Cada conmutador permite conectar una fila con una columna, y ofrece la posibilidad de seguir o girar.

Tal como aparece en la figura, esta estructura de conmutadores permite

efectuar simultáneamente P comunicaciones (cualesquiera, siempre que los destinos sean todos diferentes). Sin embargo, el coste de esta red es muy alto si el número de procesadores es elevado, ya que el número de conmutadores y de conexiones (la complejidad de la red) es del orden de P2; además, el control tiene que ser centralizado.

Las redes de tipo crossbar se han venido utilizando, normalmente, con un número pequeño de procesadores, aunque en ocasiones también en sistemas con un número de procesadores elevado (por ejemplo, en el computador Earth Simulator), a veces con conmutadores organizados en varias etapas.

Conmutadores

Procesadores o módulos de memoria

0 1 2 3

0 1 2 3

Page 8: 6.1 INTRODUCCIÓN

▪ 176 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

6.3.3 Redes multietapa (multistage)

El coste de los conmutadores crece cuadráticamente con el número de entradas. Por tanto, para reducir el coste de la red (para usar menos hardware) es necesario organizar la red de otra manera, aunque con ello no obtengamos la misma capacidad de comunicación y latencia reducida que podemos conseguir con un crossbar.

Las redes dinámicas más utilizadas son las denominadas redes multietapa, en las que las conexiones se establecen mediante conmutadores organizados por niveles o etapas, tal y como se muestra en la siguiente figura. Las conexiones entre etapa y etapa se pueden definir de múltiples maneras; en función del esquema o patrón de conexionado se obtienen diferentes tipos de redes: perfect shuffle, butterfly, cube connection, etc.

La red del dibujo es unidireccional (de izquierda a derecha); si se necesita que sea bidireccional (p.e., para el caso de conexiones con módulos de memoria) basta con superponer dos redes unidireccionales.

6.3.3.1 La red Omega

Como ejemplo de las redes multietapa veamos una de las más utilizadas: la red Omega. Una red Omega con P entradas consta de logk

P niveles o etapas de conmutación, cada una con P/k conmutadores. Por tanto, el número de conmutadores de una red Omega de grado k es (P/k) logk

P, mucho menor que el de un crossbar. Por ejemplo, un crossbar de 256 nodos utiliza 2562 = 65536 conmutadores de grado 2, mientras que la red Omega correspondiente sólo utiliza 128 × 8 = 1024; por contra, la latencia de las

Procesadores Procesadores (o módulos de memoria)

Conexiones entre etapas de conmutación

Conmutadores

0

P–1

0

P–1

Page 9: 6.1 INTRODUCCIÓN

6.3 REDES FORMADAS POR CONMUTADORES ▪ 177 ▪

conexiones es mayor en la red Omega, ya que hay que superar 8 etapas de conmutación (una sola en el crossbar).

Las conexiones entre las etapas de conmutación siguen un esquema denominado barajado perfecto (perfect shuffle), tal y como se muestra en la figura (con conmutadores de grado 2).

El esquema de conexionado denominado barajado perfecto es muy simple: para el caso de grado 2, las P entradas (de 0 a P–1) se dividen en dos grupos por la mitad, y se reordenan de la siguiente manera: una de la primera mitad (0), otra de la segunda mitad (P/2); una de la primera mitad (1), otra de la segunda mitad (P/2+1); etc. Es decir, se efectúa la siguiente permutación:

[0, 1, 2... P–1] → [0, P/2, 1, P/2+1, ... P/2–1, P–1].

En el caso general de grado k, se dividen en k grupos y se reordenan de la misma manera que acabamos de comentar. Se puede comprobar que la permutación de barajado perfecto se corresponde con una rotación hacia la izquierda de la dirección binaria del origen. La tabla siguiente muestra dicha rotación para el caso de 8 procesadores.

Barajado perfecto (perfect shuffle)

posición antigua nueva posición

(0) 000 → (0) 000 (1) 001 → (2) 010 (2) 010 → (4) 100 (3) 011 → (6) 110 (4) 100 → (1) 001 (5) 101 → (3) 011 (6) 110 → (5) 101 (7) 111 → (7) 111

0

2

4

6

1

3

5

7

0

2

4

6

1

3

5

7

0

0

1

2

3

5

6

7

4

0 1

Barajado perfecto

Red Omega de 8 nodos

orig

en

dest

ino

Page 10: 6.1 INTRODUCCIÓN

▪ 178 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

Analicemos los parámetros topológicos de una red Omega. La distancia media y el diámetro coinciden: todos los procesadores están a la misma distancia, logk P, el número de etapas de la red. Por tanto, desde el punto de vista topológico la latencia de todos los mensajes sería la misma: no se puede aprovechar la “localidad” de las comunicaciones (la comunicación suele ser más frecuente con los procesadores que están más "cerca").

La red es simétrica y regular. El grado de los conmutadores es fijo y el mismo para todos (puede ser 2, como en la figura, pero son más habituales los conmutadores de grado 4). La red no tiene tolerancia a fallos; tal como vamos a ver a continuación, sólo existe un camino que comunique el nodo i con el nodo j, por lo que si algo falla en dicho camino no es posible establecer esa comunicación. En algunos casos, si no se rompe la red, una comunicación entre i y j que no se puede establecer puede dividirse en dos fases: se manda un mensaje de i a k, y se reenvía luego de k a j (aunque, obviamente, la latencia total será mucho mayor). Si no, la única opción para hacer frente a fallos en la red consiste en replicar el hardware (p.e. duplicar los conmutadores, para que si uno de ellos falla podamos usar el otro).

6.3.3.2 Encaminamiento en la red Omega

La finalidad de una red de comunicación es, evidentemente, permitir la comunicación entre los procesadores. Por ello, es importante que se pueda conectar el origen y el destino de una manera fácil. Pero, ¿cómo se construye una conexión (un camino) entre dos nodos de la red? A la construcción (o elección) de un camino determinado se le denomina encaminamiento (routing). En las redes Omega el encaminamiento es bastante simple.

Si analizamos el funcionamiento de los conmutadores (k = 2) y de la red Omega, se puede ver que cuando se elige una salida en cada conmutador se modifica el último bit de la dirección (posición): a 0, si se elige la salida de arriba, o a 1, si se elige la de abajo. Además, en cada uno de los barajados que se hace entre las etapas de conmutación se rota un bit de la dirección. Por ello, para llegar al destino basta tener en cuenta la dirección de destino: la salida escogida en cada nivel de conmutación se va correspondiendo con los bits de la dirección de destino, comenzando por el bit de más peso.

Veamos un ejemplo (el de la figura anterior). Para ir desde el procesador 4 (100) al 2 (010), hay que elegir el siguiente camino en los conmutadores: arriba (0), abajo (1) y arriba (0).

Page 11: 6.1 INTRODUCCIÓN

6.3 REDES FORMADAS POR CONMUTADORES ▪ 179 ▪

barajado barajado barajado 100 → 001 000 → 000 001 → 010 010 salida superior salida inferior salida superior Otra alternativa para seleccionar el camino es calcular la función xor

entre las direcciones del origen y del destino. El resultado, denominado routing record o registro de encaminamiento (RE), se debe utilizar de la siguiente manera: en cada etapa, se procesa un bit del registro de encaminamiento, comenzando por el de más peso; si es 0, se prosigue sin cruzar; en cambio, si es 1, se cruza dentro del conmutador, eligiendo la salida contraria.

Para el ejemplo anterior, 4 → 2: RE = 100 xor 010 = 110. Por tanto, en las dos primeras etapas cruzar, y en la tercera no cruzar.

Sea utilizando la dirección de destino o el registro de encaminamiento, es muy sencillo encontrar el camino para ir del nodo i al nodo j en una red Omega. Sin embargo, existe sólo un camino para ir de un nodo a otro, lo cual no resulta muy favorable si se quiere poder hacer frente a fallos de funcionamiento de la red (o a situaciones de tráfico elevado).

6.3.3.3 Conflictos de salida y bloqueos

Uno de los objetivos de una red de interconexión es poder realizar más de una comunicación a la vez, lo cual no es posible en el caso de un bus. Si tenemos P procesadores en la red, en el mejor de los casos podríamos tener P comunicaciones simultáneamente (por supuesto, siendo diferentes los destinos); tal y como hemos comentado, una crossbar permite efectuar siempre esas P comunicaciones.

Una red Omega también permite efectuar P comunicaciones simultáneamente, pero, a pesar de tener toda la conectividad entre entradas y salidas —ya que no se ponen restricciones al encaminamiento—, no es posible efectuar esa comunicación en todos los casos, ya que en muchos de ellos se generan conflictos en el uso de los recursos de la red.

Por ejemplo, en una red Omega no es posible realizar estas dos comunicaciones a la vez: 0 → 1 y 6 → 0.

Page 12: 6.1 INTRODUCCIÓN

▪ 180 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

Como se puede observar, los dos mensajes quieren tomar la misma salida en el segundo conmutador. Por tanto, surge un conflicto en dicha salida, y se debe de tomar una decisión. La decisión puede ser una de estas dos: o se rechaza una de las comunicaciones (y en muchos casos eso no se puede permitir: ¿se enterará el emisor? ¿cuándo?), o se guarda uno de los “mensajes” en un búfer hasta que quede libre la salida. En este último caso, se complica el diseño del conmutador —hay que añadir búferes, autómatas para gestionarlos...— y, en consecuencia, el tiempo de respuesta del conmutador será más alto.

Si esos conflictos se producen a menudo, el rendimiento de la red de comunicación no será muy bueno. Podemos calcular de manera sencilla cuántas permutaciones (P comunicaciones simultáneas) se pueden realizar sin conflicto en una red Omega en comparación con el número total de permutaciones posibles. Para P procesadores, el número total de permutaciones es P! (por ejemplo, para el caso de P = 3, son 6: 012 → 012, 021, 102, 120, 201, 210). En una red Omega (k = 2), cada conmutador ofrece dos posibilidades (seguir o cruzar), y tenemos (P/2) × log2

P conmutadores; por tanto, el número de permutaciones que se pueden realizar es:

2log

2 22

PPP

P= [ en general, si el grado es k: P

kP

kk

log)!( ]

A medida que crece la red, el porcentaje de comunicaciones sin conflicto se va reduciendo (P = 8 → 10%; P = 16 → 0,02%; en todo caso, se trata de un número muy grande). Sin embargo, en muchas aplicaciones algunas de esas permutaciones son mucho más comunes que otras (por ejemplo, la permutación Pi → Pi+1 es muy habitual en muchas aplicaciones). Por eso, a pesar de que no se puedan realizar todas las permutaciones, una red de comunicación adecuada debe de poder realizar las más utilizadas.

0

2

4

6

1

3

5

7

0

2

4

6

1

3

5

7

Page 13: 6.1 INTRODUCCIÓN

6.3 REDES FORMADAS POR CONMUTADORES ▪ 181 ▪

Si al tener que efectuar una determinada permutación (por ejemplo, en una máquina SIMD) supiéramos que se van a generar conflictos por los recursos, podríamos desdoblar la permutación en dos permutaciones que sí se pudieran realizar. Tal como hemos comentado, esa misma solución se puede aplicar para hacer frente a los fallos de funcionamiento de la red.

En resumen: una red Omega es una red "bloqueante" (blocking network), ya que no admite, simultáneamente, implementar cualquier comunicación, pero, a pesar de ello, es una red adecuada y ampliamente utilizada, porque en muchos casos (los más utilizados) no presenta problemas para que todos los nodos puedan comunicarse entre sí.

6.3.3.4 Otro patrón de comunicación: broadcast

Las permutaciones son un tipo de comunicación muy utilizado en determinadas aplicaciones, pero no el único. Por ejemplo, es habitual tener que efectuar un tipo de comunicación que se conoce como broadcast: enviar información desde un procesador a todos los demás i → j, ∀j.

Este tipo de comunicación se puede realizar de un modo sencillo en una red Omega. Los conmutadores, tal y como hemos visto anteriormente, tienen la posibilidad de enviar una entrada a las dos salidas. Por tanto, repitiendo esa operación en todas las etapas se produce un broadcast. Por ejemplo para efectuar un broadcast desde el nodo 5:

6.3.3.5 Otras redes

Podríamos enumerar muchos otros ejemplos de redes multietapa que se han usado en multiprocesadores. Entre las más conocidas está la denominada red butterfly (mariposa). En la figura se presenta un esquema de una red butterfly de 16 nodos, con conmutadores de grado 2 o de grado 4.

0

2

4

6

1

3

5

7

0

2

4

6

1

3

5

7

BC

BC

BC

BC

BC

BC

BC

Page 14: 6.1 INTRODUCCIÓN

▪ 182 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

red butterfly (k = 2) red butterfly (k = 4)

Las redes butterfly y Omega son muy similares; por ejemplo, puede comprobarse fácilmente que puede usarse en ambas el mismo algoritmo de encaminamiento, el que utiliza la función xor para generar el registro de encaminamiento.

Las redes Omega y butterfly, son redes bloqueantes; por su parte, un crossbar es una red no bloqueante (admite cualquier comunicación simultánea), pero su coste es muy elevado. En todo caso, es posible construir redes no bloqueantes con un coste menor que el del crossbar.; ese tipo de redes se conoce, de manera general, como redes de Clos. Entre las redes no bloqueantes se encuentran las redes "reorganizables" (rearrangeable), en las que siempre es posible encontrar un camino para cualquier comunicación, aunque en algunas ocasiones es necesario reorganizar los caminos existentes en un momento dado. Un ejemplo de ese tipo de redes son las redes de Benes, que permiten cualquier permutación si se conoce de antemano cuál es (para elegir los caminos adecuados), ya que hay muchos caminos para conectar origen y destino.

La siguiente figura presenta una red de Benes de 16 nodos, que utiliza 7 etapas de conmutación (en las redes de Clos el número de etapas de conmutación es siempre un número impar). La red permite construir múltiples caminos para unir dos nodos; por ejemplo, hemos dibujado dos caminos diferentes para ir del nodo P3 al P11. Como es obvio, el coste de esta red es mayor que el de una red Omega, y la latencia de los paquetes será también mayor, ya que tienen que superar más etapas de comunicación.

Si "doblamos" una red de Benes sobre sí misma, la red que se consigue se denomina árbol (fat tree), en la que los paquetes van hacia adelante (hasta la raíz) y luego vuelven hacia atrás para llegar al destino (es decir, los enlaces y puertos son bidireccionales).

Page 15: 6.1 INTRODUCCIÓN

6.3 REDES FORMADAS POR CONMUTADORES ▪ 183 ▪

Red de Benes Árbol (fat tree)

6.3.3.6 Resumen

Las redes multietapa intentan superar algunas de las limitaciones que nos encontramos cuando utilizamos un bus como red de comunicación. Este tipo de redes tienen larga historia en el área de paralelismo. Al principio se utilizaron en los sistemas SIMD, en los que la unidad de control distribuye las instrucciones de un único programa entre los procesadores para ejecutarlas en modo síncrono. Para algunas aplicaciones —por ejemplo, tratamiento de imagen, cálculo matricial, aplicaciones numéricas, etc.— este tipo de máquinas es adecuado, dado que el reparto de trabajo y la transferencia de datos es muy regular: se aprovecha el paralelismo de datos. En relación con el reparto de datos, es muy común que los esquemas de comunicación sean permutaciones (por ejemplo, i → i+1), o tener que hacer un broadcast... Además de en máquinas tipo SIMD, también se han utilizado este tipo de redes en multicomputadores MIMD, para sustituir al bus; sin embargo, hoy en día no se utilizan demasiado (salvo en forma de fat tree y topologías similares, tal como veremos a continuación).

En la siguiente tabla se presenta una comparación entre un bus, una red crossbar y una red Omega. Si no hay conflictos, la latencia de las comunicaciones en un bus es constante; en una red Omega, en cambio, la latencia es mayor y función de P. En contrapartida, pueden efectuarse más comunicaciones simultáneamente, ya que el bus hay que compartirlo entre todos los procesadores (w/P), y la red Omega no (el "ancho de banda" es mayor). En todo caso, la red Omega es más compleja que un simple bus.

Page 16: 6.1 INTRODUCCIÓN

▪ 184 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

P procesadores / canales de w bits conmutadores de grado k

(O(x) = orden de x) Bus Red Omega Crossbar

Latencia constante O(log2P) constante

Ancho de banda por procesador O(w/P) → O(w) O(w) → O(w × P) O(w × P)

Complejidad del cableado O(w) O(w × P × logk P) O(w × P2)

Complejidad de la conmutación O(P) O(P/k × logk P) O(P2)

Capacidad de comunicación de una en una algunas permutaciones, broadcast...

todas las permutaciones

Ejemplos Symmetry S-1 Encore Multimax

BBN TC-2000 IBM RP3

Cray Y-MP/816 Fujitsu VPP500

6.4 REDES FORMADAS POR ENCAMINADORES DE MENSAJES

En las redes multietapa que acabamos de analizar, los enlaces entre conmutadores se establecen dinámicamente en función de las necesidades de comunicación, y los nodos y los dispositivos de la red están "separados". Tenemos un segundo tipo de redes, en el que cada nodo de la red utiliza un dispositivo propio para gestionar la comunicación, un encaminador de mensajes; estas redes se conocen como redes estáticas, Para formar la red, se conectan entre sí los dispositivos específicos de gestión de mensajes de cada nodo, los encaminadores de mensajes (routers), de acuerdo a una determinada topología. Este tipo de redes son las que habitualmente utilizan los sistemas paralelos actuales, sobre todo si el número de procesadores que se desea conectar es elevado.

Red de comunicación

Procesador y memoria local

router

Gestor de comunicaciones

Enlaces de la red

Page 17: 6.1 INTRODUCCIÓN

6.4 REDES FORMADAS POR ENCAMINADORES DE MENSAJES ▪ 185 ▪

6.4.1 Encaminadores de mensajes

Como ya sabemos, la comunicación entre procesadores en sistemas paralelos de memoria distribuida se realiza mediante paso de mensajes. Para enviar un mensaje de un procesador a otro se pasa dicho mensaje al encaminador de mensajes local, desde donde irá avanzando, encaminador a encaminador, hasta el nodo destino.

La siguiente figura representa, de manera esquemática, un encaminador de mensajes típico. Por un lado, tenemos cierto número de puertos de entrada y salida, mediante los cuales se va a formar la red, más un puerto específico para la conexión con el procesador local. Por otro, un autómata que decidirá en cada caso el puerto de salida correspondiente a un mensaje que se ha recibido en uno de los puertos de entrada, bien para pasarlo a otro encaminador, bien para pasarlo al procesador local.

Es posible que un mensaje quede bloqueado en algún punto intermedio de su recorrido, debido a que no esté libre el camino de salida que necesita para seguir avanzando. Un poco más adelante concretaremos qué hay que hacer en esos casos, aunque es habitual que el encaminador disponga de búferes para almacenar mensajes en esas circunstancias.

Un encaminador de mensajes no es sino un tipo de conmutador algo más complejo. El objetivo de ambos dispositivos es el mismo, conectar entradas y salidas para abrir un camino a los mensajes; es la organización de la red la que los hace algo diferentes. El encaminador de mensajes es un elemento más de los nodos que forman la red, no como los conmutadores de una red Omega, que son independientes de los nodos de la misma, como consecuencia de ello, por ejemplo, en el caso de las redes estáticas no todos los mensajes recorren el mismo número de pasos en la red: unos nodos están mas cerca que otros.

Puertos entrada

Puertos salida

Mensajes del procesador local

Mensajes para el procesador local

Enlaces de la red de comunicación

Función de routing

Enlaces de la red de comunicación

Búferes

+

Crossbar

Page 18: 6.1 INTRODUCCIÓN

▪ 186 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

6.4.2 Topologías de red más utilizadas

Como ya hemos visto, la red ideal es la que conecta todos con todos, un crossbar, tal como el de la figura.

Es claro que se trata de una topología de difícil aplicación si el número de

procesadores a conectar es elevado: el número de enlaces crece cuadráticamente y el grado de los nodos (número de conexiones) es grande (y no es constante).

Tenemos que analizar por tanto las alternativas más viables y que más se utilizan en los sistemas MPP actuales. En general, cada nodo de las topologías que vamos a analizar consta de procesador, memoria y encaminador de mensajes; los enlaces son siempre bidireccionales.

6.4.2.1 Redes de una dimensión: la cadena y el anillo

Aunque no es una topología que se utilice en casos reales, analicemos como punto de partida dos redes de una sola dimensión: la cadena y el anillo.

cadena anillo

El grado de ambas redes es 2 (2 enlaces por nodo). El anillo es regular y simétrico, pero la cadena no. La tolerancia a fallos es baja: basta con que falle un enlace, cualquiera, para romper la cadena, o dos enlaces en el caso del anillo. Los parámetros de distancia (siendo P, el número de procesadores, par) son los siguientes (ver apéndice del capítulo):

diámetro distancia media

cadena → P – 1 (P + 1) / 3 → P / 3

anillo → P / 2 P2 / 4(P–1) → P / 4

Page 19: 6.1 INTRODUCCIÓN

6.4 REDES FORMADAS POR ENCAMINADORES DE MENSAJES ▪ 187 ▪

Las redes de una dimensión no son adecuadas para conectar un número alto de procesadores. Pero pueden generalizarse fácilmente a 2, 3 o más dimensiones.

6.4.2.2 Mallas y Toros (mesh, torus)

Las mallas y los toros son las topologías resultantes de generalizar la cadena y el anillo respectivamente a n dimensiones. En la figura aparecen una malla y un toro de 2 dimensiones. Hemos representado redes cuadradas, pero el número de procesadores por dimensión puede ser cualquiera. Para obtener un toro a partir de una malla, basta con enlazar entre sí, formando anillos, los nodos de los bordes de la malla en cada dimensión (con otro tipo de conexión entre los nodos de los bordes, se obtiene otro tipo de redes).

En general, se utilizan mallas o toros de n dimensiones (n = 2 o 3), con k nodos por dimensión. Por tanto, el número total de nodos de la red es P = kn. En el caso de dos dimensiones, P = k × k (si la red no es cuadrada, k1 × k2).

El grado de ambas redes es 2n (hay dos enlaces por dimensión), un valor bajo. El toro es regular y simétrico28, pero la malla no, ya que el grado de todos los nodos no es el mismo (por ejemplo, en dos dimensiones, los nodos de los vértices sólo tienen 2 enlaces, los de los lados 3, y el resto 4), por lo que los nodos no tienen la misma imagen de la red.

La tolerancia a fallos es alta, ya que hay muchos caminos para ir de un nodo a otro. En el caso peor, hay que quitar n enlaces en la malla (los enlaces de un nodo de uno de los vértices) y 2n en el toro (en dos dimensiones, los cuatro enlaces de cualquiera de los nodos) para romper la

28 Para conseguir la simetría se necesita que todos los enlaces sean de la misma longitud, lo que se

consigue con la red denominada folded torus. Se propone como ejercicio dibujar nuevamente el toro de 4×4, pero con todos los enlaces de la misma longitud.

0 1 2 3

0 1 2 3 0

1 2

3

Page 20: 6.1 INTRODUCCIÓN

▪ 188 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

red; por tanto, la arco-conectividad es n y 2n respectivamente. Sin embargo, en general, se pueden eliminar (estropear) muchos más enlaces de la red sin que ésta pierda la conectividad entre todos sus nodos. Respecto al número de enlaces, una malla tiene n × kn-1 × (k–1) enlaces, y un toro n × kn.

Para dividir la red en dos partes iguales (bisección), es necesario eliminar k enlaces en la malla y 2k enlaces en el toro (debido a los anillos).

Los parámetros de distancia de ambas redes son los siguientes (k par; ver apéndice):

diámetro distancia media

malla → n × (k – 1)

1312

−−

n

n

kk

kkn → n × (k / 3)

toro → n × k / 2

14 −n

n

kkkn → n × (k / 4)

La distancia máxima de la malla corresponde a ir de un extremo a otro en

cada dimensión; en el toro, en cambio, la distancia máxima por dimensión nunca es mayor que medio anillo. Simplificando el cálculo para el caso en el que el número de nodos sea grande, la distancia media en una malla es un tercio del número de nodos en cada dimensión, y en un anillo un cuarto.

6.4.2.3 Hipercubos (hypercube)

Un hipercubo es una malla de n dimensiones que sólo tiene dos nodos por dimensión, por lo que también se le denomina n-cubo binario. En las siguientes figuras tenemos hipercubos de 1, 2, 3 y 4 dimensiones. Para generar un hipercubo de n dimensiones, hay que construir dos hipercubos de n–1 dimensiones y enlazar, uno a uno, los nodos de la misma posición en cada red.

(000)

(110) (111)

(101)

(011)

(100)

(010)

(001)

(0) (1) (0000) (0001)

(0010) (0011)

(0100) (0101)

(0110) (0111)

(1000) (1001)

(1010) (1011)

(1100) (1101)

(1110) (1111)

(01)

(10) (11)

(00)

Page 21: 6.1 INTRODUCCIÓN

6.4 REDES FORMADAS POR ENCAMINADORES DE MENSAJES ▪ 189 ▪

Cada nodo de la red tiene una dirección de n bits, (xn–1, xn–2, ..., x1, x0), en la que cada bit valdrá 1 o 0 en función de la posición que ocupe el nodo en la correspondiente dimensión. Así, el nodo (xn-1, xn-2, ..., x1, x0) está conectado con los nodos (/xn-1, xn-2, ..., x1, x0), (xn-1, /xn-2, ..., x1, x0), (xn-1, xn-2, ..., /x1, x0), (xn-1, xn-2, ..., x1, /x0) [/ = not]; es decir, con aquellos nodos cuya dirección difiere únicamente en un bit. Por ejemplo, el nodo 0000 está conectado con los nodos 1000, 0100, 0010 y 0001.

Un hipercubo de n dimensiones tiene 2n nodos. Un hipercubo de P nodos es de log2 P dimensiones. El grado de la red es n (o sea, log2 P), la dimensión de la misma, y no es constante, ya que crece con el tamaño de la red. El hipercubo es simétrico y regular. El número de enlaces es alto, (P/2) × log2 P, y en la misma medida es alta también la tolerancia a fallos; la arco-conectividad es n (hay que eliminar los n enlaces de un nodo dado para romper la red).

La bisección de un hipercubo es de P/2 enlaces (para formar un hipercubo de P nodos unimos uno a uno los nodos de dos hipercubos de P/2 nodos).

Los parámetros de distancia del hipercubo son los siguientes (ver apéndice):

diámetro distancia media

hipercubo → n n × 2n–1 / (2n–1) → n / 2

La distancia máxima es el número de dimensiones, ya que sólo se puede

dar un paso en cada dimensión; si el número de nodos es grande, la distancia media resulta ser la mitad del número de dimensiones.

Los hipercubos presentan dos inconvenientes claros. Por una parte, el grado no es constante, lo que quiere decir que el número de enlaces del procesador (mejor, del interfaz de la red) no es constante, sino que varía en función del tamaño de la red. Por tanto, es complicado ampliar la red, ya que habría que cambiar todos los elementos de la red (o tener prevista desde el principio la posible ampliación del sistema). Además la construcción de hipercubos de muchos nodos no es sencilla, ya que el cableado de la red es muy denso. Por otra parte, no se puede construir un hipercubo con cualquier número de procesadores, sino sólo con potencias de 2. Por ejemplo, 512 o 1024 procesadores, pero no 800.

Topologías tales como mallas, toros e hipercubos pueden englobarse dentro de una clase más general, denominada k-ary n-cube. Se trata de redes

Page 22: 6.1 INTRODUCCIÓN

▪ 190 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

de n dimensiones, con k nodos en cada dimensión. Los nodos de cada dimensión se unen mediante un anillo (o una cadena) y los enlaces son unidireccionales (por ejemplo un toro 4×4 sería un 4-ary 2-cube).

6.4.2.4 Árboles y árboles densos (fat tree)

Otra topología muy utilizada en los sistemas paralelos MPP es el árbol. Un árbol puede tener estructuras diversas; la más utilizada consiste en disponer los procesadores en las hojas del árbol y los encaminadores de mensajes en el resto de los nodos, tal como aparece en la siguiente figura.

El grado del árbol es k, el número de nodos que salen de cada nodo (como excepción, el grado no representa en este caso el número de enlaces). Así, el número de niveles del árbol resulta ser logk P. En la figura se representa un árbol binario, con k = 2.

En principio, la red no es regular, ya que el nodo raíz y las hojas sólo tiene

dos enlaces, mientras que los intermedios tienen tres. Sin embargo, si los procesadores únicamente están en las hojas del árbol, la visión que tiene cada uno de ellos de la red es la misma, por lo que podríamos decir que, para los procesadores, es regular.

Considerando el tráfico de paquetes, la red es muy desequilibrada. Por ejemplo, todo el tráfico que se genere en la mitad derecha del árbol, y que vaya dirigido a procesadores situados en la mitad izquierda, tiene que utilizar el encaminador de mensajes de la raíz, zona de la red que va a estar muy saturada. Además, si se estropeara un enlace de ese nodo o el propio encaminador, la red quedaría inconexa. Para superar ese problema, las redes en forma de árbol que se utilizan en la realidad disponen de mayor cantidad de recursos —enlaces y encaminadores— según se avanza hacia la raíz, con

Encaminadores

Procesadores

fat tree o árbol denso

Page 23: 6.1 INTRODUCCIÓN

6.4 REDES FORMADAS POR ENCAMINADORES DE MENSAJES ▪ 191 ▪

lo que la capacidad de gestionar mensajes es mayor en las zonas en las que el tráfico va a ser mayor. A este tipo de árbol se le conoce como árbol denso (fat tree). Por tanto, la bisección de un fat tree es P/2.

Los parámetros de distancia de un árbol son los siguientes (grado k; ver apéndice):

diámetro distancia media

árbol → 2 logk P

12log

12

−−

− kP

PP

k

→ 2 log2 P – 2 (k = 2)

En las redes implementadas mediante conmutadores, como las redes

Omega, los diferentes componentes que forman la red están perfectamente diferenciados —por un lado, los procesadores y, por otro, los conmutadores— y no hay una relación directa entre ellos. En el caso de las redes estáticas, en cambio, cada procesador (o, tal vez, un conjunto pequeño de ellos) utiliza un encaminador de mensajes privado para gestionar la comunicación. Los árboles que hemos analizado toman la apariencia de redes dinámicas, porque los procesadores sólo están en los nodos hoja del árbol; los demás elementos de la red sólo se encargan de gestionar los mensajes. De hecho, se puede demostrar que los árboles densos y las redes butterfly son isomorfos (“equivalentes”). La única diferencia es que para llegar al destino en una red butterfly es necesario atravesar toda la red (la distancia siempre es la misma), pero en los árboles no siempre es necesario llegar hasta el nodo raíz, porque se puede volver hacia atrás en cualquiera de los encaminadores intermedios (y de esta forma hacer el camino más corto).

6.4.2.5 Resumen de topologías

Acabamos de resumir las características principales de las topologías más utilizadas hoy en día en los sistemas paralelos MPP. La siguiente tabla resume las principales características topológicas de las redes que hemos analizado. Por claridad, hemos simplificado algunos parámetros para el caso de un número grande y par de procesadores.

Page 24: 6.1 INTRODUCCIÓN

▪ 192 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

(P = núm. de procesadores, n = núm. de dimensiones, k = grado de los conmutadores o del árbol, o bien número de procesadores por dimensión).

Proc. Grado Regul./ Simetr.

Enlaces (×w) [Conm.] d media Diámetro Bisecc. Arco-

conec.

Crossbar P P–1 si P (P–1) 1 1 P2/4 P–1

Omega P k si P (logk P + 1) [(P/k) logk P] logk P logk P -- --

Malla P = kn 2n no n kn–1 (k–1) ~ n k/3 n (k–1) kn–1 n

Toro P = kn 2n si n P ~ n k/4 n k/2 2 kn–1 2n

Hipercubo P = 2n n (log P) si (P/2) log P ~ n/2 n P/2 n

Árbol (fat tree) P k si P logk P ~ 2 logk P –

2/(k–1) 2 logk P P/2 1

Para analizar con más claridad los parámetros de distancia, la siguiente

tabla muestra el diámetro y la distancia media de mallas, toros e hipercubos. Tal como veremos a continuación, estos parámetros pueden ser esenciales en la latencia de la comunicación. Por ejemplo, en el caso de una máquina de 1024 procesadores, la distancia media del hipercubo es 3 veces menor que la del toro y 4 veces menor que la de la malla. Pero el principal problema del hipercubo es su elevado grado (el número de enlaces). Por ejemplo, para el caso de 1024 nodos, el grado del hipercubo es 10: cada nodo se conecta con otros 10; en cambio, en el toro o la malla de dos dimensiones el grado es solamente 4.

Nodos (número de procesadores) D / d (med.) 16 64 256 1024 16384

Malla 2D 6 / 2,7 14 / 5,3 30 / 10,7 62 / 21,3 254 / 85,3

Toro 2D 4 / 2,13 8 / 4,06 16 / 8,03 32 / 16 128 / 64

Hipercubo 4 / 2,13 6 / 3,05 8 / 4,02 10 / 5 14 / 7

Árbol (binario) 8 / 6,53 12 / 10,19 16 / 14,06 20 / 18 28 / 26

En muchos casos, la topología de la red es de tipo jerárquico: los nodos de una determinada red con una topología dada, son a su vez sistemas paralelos con otra topología (o tal vez sistemas SMP). Por ejemplo, un sistema puede ser un hipercubo hasta por ejemplo 64 procesadores, y a partir de ahí un árbol en el que los nodos son hipercubos. Una de las jerarquías más utilizadas es aquella en la que los nodos son sistemas SMP de 4-8 procesadores.

Page 25: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 193 ▪

6.4.2.6 Los enlaces físicos

Además de los conmutadores o de los encaminadores de mensajes, la red está formada por links o enlaces físicos. Estos enlaces pueden ser de diferentes tipos en función de las características de la máquina:

• Largos o cortos. Si los cables son largos, es posible tener más de un dato a la vez en diferentes “zonas” del mismo (por ejemplo, en una red LAN). Cuando son cortos, sólo admiten un dato. Considerados como elementos de transmisión digital, el tiempo de transmisión en un cable corto es básicamente el tiempo de carga, necesario para tener el mismo valor en ambos extremos del cable, tiempo que crece logarítmicamente con la longitud del cable. Cuando los cables son largos, en cambio, el tiempo de transmisión es proporcional a la longitud.

• "Anchos" o "estrechos”. Los enlaces pueden ser de un bit (serie) o de varios bits en paralelo: 4, 8, 16 o más. Cuanto más anchos son, más información pueden transmitir simultáneamente, y, por tanto, menor será la latencia de un mensaje de tamaño dado. En algunos casos, algunos de los bits de los enlaces se utilizan para transmitir información de control, y el resto para datos.

Por otra parte, la comunicación puede ser síncrona (mediante un reloj global) o asíncrona (mediante un protocolo específico punto a punto).

Para indicar la capacidad de transmisión de los enlaces, se utiliza el ancho de banda (bandwidth), habitualmente en (Mega) Gigabit/s; por ejemplo, enlaces de 10 Gb/s.

Por último, los enlaces pueden ser de par trenzado de cobre (típicos en telefonía), cable coaxial (habitual en redes LAN), o de fibra óptica (cada vez más utilizados en las redes de comunicación de alta velocidad).

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS

La red no es sino el soporte físico de la comunicación entre procesadores; por encima de ella, es necesario construir la lógica adecuada que permita llevar los mensajes desde el nodo origen al nodo destino. Para analizar el comportamiento de una red de comunicación, además de su topología, es necesario tener en consideración muchas otras cuestiones: ¿cómo se organiza

Page 26: 6.1 INTRODUCCIÓN

▪ 194 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

el camino (switching strategy)? ¿por dónde se llega al destino (routing algorithm)? ¿hay que usar siempre el mismo camino? ¿cómo avanzan los mensajes por la red? ¿qué hay que hacer si el camino está ocupado (flow control)? Todas estas decisiones hay que tomarlas teniendo en cuenta que el objetivo debe ser que la latencia de la comunicación sea baja y el throughput de la red alto.

6.5.1 Los mensajes

En función de la estructura del sistema paralelo, la información que se intercambia entre los procesadores puede organizarse de formas diversas. Si se utilizan variables compartidas, la comunicación se efectuará mediante operaciones de tipo rd/wr, es decir, pequeños paquetes de control más los datos. Si la comunicación se efectúa mediante paso de mensajes, éstos serán más largos y estructurados. Por otra parte, la longitud máxima de los mensajes que procesa la red suele estar limitada a un valor máximo. Si hay que enviar un mensaje más largo, entonces habrá que dividirlo en varios paquetes o unidades de transmisión de tamaño fijo.

En un paquete es habitual distinguir los siguientes campos: • Cabecera: información de control que identifica al paquete, y que

incluye, junto a la longitud, tipo, prioridad... del paquete, información sobre la dirección de destino.

• Payload o carga de datos: datos a transmitir (contenido del mensaje). • Cola: información de control para indicar fin de paquete, códigos de

detección de errores tipo checksum (más común en las redes LAN que en los MPP), etc.

control datos control.

cola cabecera

Así pues, tenemos que distinguir la información de control y los datos dentro de un paquete. Si los enlaces entre encaminadores son muy anchos (de muchos bits), los datos y la información de control pueden ir en paralelo. Por ejemplo, en el Cray T3D, los enlaces son de 24 bits, 16 bits para datos y 4 para control (y 4 más para control de flujo). Con los 4 bits de control se indica, por ejemplo, el comienzo y el final de la transmisión de un paquete, etc. En el Cray T3E, en cambio, la información se envía en paquetes tales como los que hemos comentado (se dice que se hace framing).

Page 27: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 195 ▪

Toda la información de control que se añade a los paquetes (cabecera, checksum...) supone una sobrecarga en la comunicación y, por tanto, una pérdida de eficiencia, por lo que hay que mantenerla acotada. Por ejemplo, si para mandar 100 bytes de datos hay que enviar un paquete con 128 bytes, entonces sólo aprovecharemos 100 / 128 = 78% del ancho de banda del sistema (para transmitir información útil).

La anchura de los enlaces de red que se utilizan para transmitir la información suele ir desde un bit (transmisión serie, se ha utilizado poco) hasta 16 bits (o más); según los casos, por tanto, puede que se necesite más de un “ciclo de transmisión” para transmitir “la unidad lógica más pequeña” de un paquete. Desde el punto de vista del control, los paquetes se suelen dividir en flits. En este contexto, un flit se define como la cantidad mínima de información con contenido semántico, normalmente la información mínima que se requiere para poder encaminar el paquete. Por ejemplo, si se utiliza transmisión en serie, cuando recibimos el primer bit de un paquete no podemos decir nada sobre dicho paquete; necesitamos más bits para poder saber a dónde va dicho paquete.

Por tanto, los mensajes/paquetes se miden en flits. Lo más habitual es que un flit sean 8 o 16 bits, coincidiendo con la anchura de los enlaces que unen los encaminadores de la red. Normalmente, en uno o dos bytes se puede codificar la información necesaria para poder encaminar un paquete. De este modo, un flit se podrá transmitir entre encaminadores en un solo “ciclo”.

6.5.2 Patrones de comunicación: con quién y cuándo hay que efectuar la comunicación.

No es posible responder de manera precisa a esa cuestión, ya que las necesidades de comunicación, obviamente, dependen de la aplicación a ejecutar. Sin embargo, podemos aclarar algunos aspectos. Por una parte, hay que tener en cuenta que el esquema de comunicación de una aplicación y el esquema de comunicación en la red son dos cosas diferentes, ya que en medio se encuentra la asignación física de procesos a procesadores. Es decir, si los procesos P1 y P2 tienen que intercambiar información, no es lo mismo que se asignen a procesadores contiguos en la red que asignarlos a procesadores en dos extremos de la red. Por otra parte, las necesidades de comunicación no suelen ser siempre de todos con todos, sino entre algunos de los procesos. Tenemos por tanto que considerar múltiples aspectos: las

Page 28: 6.1 INTRODUCCIÓN

▪ 196 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

necesidades de comunicación de la aplicación, el nivel de paralelismo, el reparto de procesos, etc.

Al esquema de comunicación que hay que ejecutar en la red se le conoce como patrón de comunicación. El patrón de comunicación puede ser espacial o temporal, es decir, puede indicar la distribución espacial de los paquetes (a dónde van) o su distribución en el tiempo (cuándo se transmiten). Analicemos algunos de los casos más típicos.

1. Aleatorio

Uno de los patrones que más se utiliza para analizar el comportamiento de las redes es el patrón aleatorio, tanto en el espacio como en el tiempo. La comunicación es aleatoria si la probabilidad de enviar un paquete del nodo i al j, PrCij, es la misma para cualquier par de nodos de la red. En este caso, la distancia media de la comunicación y la de la red (topológica) coinciden.

Aunque tal vez no parezca un patrón de comunicación “lógico”, se obtiene de manera sencilla si el propio proceso de reparto de procesos a procesadores es aleatorio, lo cual puede ser útil para utilizar los recursos de la red de manera homogénea.

2. Esferas de localidad La comunicación entre procesos, al igual que ocurre con los accesos a

memoria, posee la propiedad de localidad: los procesos se comunican entre sí formando grupos; dentro del grupo, la comunicación es muy habitual, y fuera de él muy escasa. De cara a reducir la latencia de la comunicación, puede ser interesante asignar esos grupos a procesadores cercanos, con lo que la comunicación se limitará dentro de una “esfera”. La probabilidad de comunicación de este tipo de patrones es función, por tanto, de la distancia. En lo que al tiempo se refiere, puede ser aleatoria.

En la siguiente figura aparecen algunos ejemplos, en los que se representa la probabilidad de comunicación Pi → Pj en función de la distancia i → j.

PrC

distancia

Page 29: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 197 ▪

3. Broadcast / Multicast / Reporting Las necesidades de comunicación pueden analizarse en el espacio y en

el tiempo. Un tipo habitual de comunicación es la conocida como broadcast (difusión): desde el procesador i se envía un mensaje a todos los procesadores, a la vez. La operación termina cuando todos los nodos han recibido el mensaje. Es una operación muy habitual en muchas situaciones, por ejemplo para sincronizar procesos. Así pues, en un determinado momento se produce un pico de comunicación. El caso de multicast es similar, pero el mensaje no se envía a todos los nodos, sino solamente a un subconjunto de ellos. Existen muchos algoritmos que implementan estos patrones de manera eficiente (spanning tree...) pero no los vamos a analizar.

El caso conocido como reporting es justamente el contrario: todos los procesadores envían un mensaje al mismo nodo destino, más o menos a la vez. Es un patrón de comunicación que genera muchos problemas, ya que podemos tener niveles elevados de tráfico en las cercanías del nodo destino, que pueden llegar a saturar la capacidad de enlaces y encaminadores. También para este caso existen algoritmos para intentar reducir el problema y efectuar la comunicación de manera eficiente (combining messages...).

4. Matriz transpuesta, FFT, perfect shuffle... Algunas aplicaciones de cálculo muy habituales —matriz transpuesta,

transformada discreta de Fourier...— suelen generar patrones de comunicación específicos. En estos patrones la comunicación se concentra en momentos determinados, por lo que en esos momentos crecerá la densidad del tráfico de paquetes, puede que se saturen algunas zonas de la red, y, en consecuencia, la latencia de los paquetes será más alta. En todo caso, se trata de momentos concretos de saturación, ya que la red debe ir recuperando su estado normal según va transportando los paquetes a su destino.

Otra característica de la comunicación de una determinada aplicación es el tamaño de los mensajes que genera: grandes, pequeños, de todo tipo... En base a este parámetro suelen distinguirse dos tipos de paralelismo. Por un lado, el paralelismo de grano fino, en el que los mensajes que se intercambian los procesos son cortos (10 - 100 bytes), pero la comunicación es muy frecuente. En el lado opuesto tenemos el paralelismo de grano

Page 30: 6.1 INTRODUCCIÓN

▪ 198 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

grueso, con mensajes largos (1 kB - 100 kB) pero mucho menos frecuentes. Por otra parte, junto con los paquetes de datos se van a generar muchos paquetes de control (para mantener la coherencia, para la sincronización...), que, comparados con los de datos, son mucho más cortos.

En resumen, las aplicaciones paralelas van a generar mensajes de tamaño muy diverso, que habrá que procesar de manera eficiente en todos los casos. Por ello, en algunos sistemas de comunicación se distinguen clases de mensajes (cortos y largos, o de datos y de control) y se tratan de manera diferente; por ejemplo, se da preferencia a los mensajes cortos (de control).

6.5.3 Construcción del camino (switching strategy)

El camino que va a "unir" el emisor y el receptor (dos procesadores o un procesador y un módulo de memoria) puede “construirse” de maneras diferentes (que se suelen conocer como técnica de conmutación o switching). Las dos técnicas habituales son:

1. Conmutación de circuitos (circuit switching) Antes de comenzar con la transmisión de datos, se construye (se

reserva) un camino físico específico que une emisor y receptor, para lo que se envía un mensaje de control especial, una sonda, que según avanza hacia el destino va reservando los recursos de red que utiliza. Al llegar al destino, se envía una respuesta al nodo origen utilizando el camino que ha “construido” el mensaje sonda. Cuando se recibe este mensaje de confirmación, se procede a enviar los datos por el camino privado establecido entre emisor y receptor.

Reservar el camino requiere cierto tiempo, pero, una vez construido, la transmisión de datos se efectúa sin ninguna limitación, ya que todo el ancho de banda de los enlaces está disponible para nuestro mensaje. Este mecanismo resulta interesante si el tiempo de construcción del camino es mucho menor que el de transmisión de datos (como ocurre, por ejemplo, en el caso de una llamada telefónica).

Cuando se utiliza conmutación de circuitos no es necesario dividir los mensajes en paquetes, ya que no vamos a utilizar nunca búferes intermedios. En todo caso, no hay que olvidar que si los mensajes son muy largos se van a mantener ocupados los enlaces entre encaminadores durante largo tiempo, por lo que la latencia de otros

Page 31: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 199 ▪

mensajes que querrían utilizar esos recursos puede crecer mucho, lo cual puede no ser aceptable en un entorno de computación paralela.

El ejemplo más conocido de este tipo de comunicación es la telefonía. Aunque se ha utilizado en algún multicomputador comercial (por ejemplo, en el iPSC2 de Intel o en Meiko CS-2 y BBN butterfly), no es la alternativa habitual en los sistemas MPP.

2. Conmutación de paquetes (packet switching) Para efectuar la comunicación no se construye un camino, sino que se

envían paquetes a la red y éstos van escogiendo el camino adecuado, en función de la dirección de destino (como en el tráfico postal). Ésta es la técnica más habitual para efectuar la comunicación en los sistemas paralelos. Los encaminadores de mensajes de la red reciben los paquetes, procesan la información de control, y los reenvían al siguiente encaminador, hasta llegar así al nodo destino.

La información de control que indica el destino de los paquetes debe ser sencilla de interpretar, ya que no podemos perder mucho tiempo en ese proceso si queremos que la comunicación sea eficiente, es decir, que la latencia de los paquetes sea la menor posible. En el próximo apartado veremos cómo indicar dicha información.

Cuando, por ser grande, un mensaje se divide en varios paquetes se genera una cierta sobrecarga en la comunicación, ya que hay que añadir la información de control en cada paquete. Por ello, salvo que exista otro tipo de razones, no conviene dividir los mensajes en paquetes muy cortos. En general, el tamaño máximo de los paquetes suele estar relacionado con el de los búferes de los encaminadores.

6.5.4 Encaminamiento de los mensajes (routing)

Para llegar a su destino, los paquetes deben atravesar la red de comunicación. Pero, ¿cuál es el camino para llegar al destino? Tenemos que resolver dos problemas. Por un lado, cómo indicar la dirección de destino del paquete, y, por otro, qué camino elegir entre los múltiples caminos que enlazan el nodo origen y el destino. En todo caso, y salvo que tuviéramos que resolver algún problema particular, el camino escogido debe ser siempre un camino de distancia mínima (los paquetes nunca se alejan de su destino). Veamos cómo resolver estos problemas.

Page 32: 6.1 INTRODUCCIÓN

▪ 200 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

6.5.4.1 El registro de encaminamiento

Cuando se utiliza conmutación de paquetes, el procesador (más exactamente, el interfaz con la red) inyecta los paquetes a transmitir en la red para que éstos lleguen a su destino utilizando por el camino los diferentes recursos de la red (de manera similar a lo que ocurre con el correo habitual). Para llegar desde el nodo origen al destino hay que utilizar la información que lleva el propio paquete. ¿Por dónde avanzar en la red? El proceso de hacer avanzar un paquete en la red se conoce como encaminamiento (routing). El encaminamiento de los paquetes en la red debe ser sencillo. Dos son las opciones más habituales.

• En la cabecera del paquete se incluye la dirección de destino; así, en cada encaminador de mensajes intermedio se decidirá por dónde debe avanzar el paquete en función de dicha dirección. Para ello pueden usarse dos técnicas diferentes: (a) utilizar una tabla que indique los puertos de salida adecuados para cada dirección de destino; o (b) efectuar una operación sencilla con la dirección que dé como resultado el puerto de salida.

• La cabecera del paquete indica el número de pasos que debe dar el paquete en cada dimensión para llegar al destino, en un campo que se conoce como registro de encaminamiento (routing record). En cada paso se actualiza el registro de encaminamiento (±1), y cuando todos sus elementos valgan 0, el paquete ha llegado a su destino.

De una manera o de otra, la elección del camino debe ser lo más rápida posible, ya que no podemos demorar demasiado tiempo los paquetes en los encaminadores intermedios, so pena de que la latencia de la comunicación crezca mucho. Ya que los registros de encaminamiento son, probablemente, la opción más utilizada, veamos cómo calcular el registro de encaminamiento en las topologías de red que hemos analizado en el apartado anterior.

a. Mallas Los procesadores que forman la malla se suelen identificar según sus coordenadas en la misma29. En una malla de n dimensiones (k nodos por dimensión), para ir del nodo X(xn–1, xn–2, ..., x1, x0) al

29 A partir de la dirección absoluta de un nodo, es fácil obtener sus coordenadas; por ejemplo, en una

malla de 8×8, las coordenadas del nodo 50 son: x1 = 50 / 8 = 6; x0 = 50 mod 8 = 2. P50 → (6, 2).

Page 33: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 201 ▪

nodo Y(yn–1, yn–2, ..., y1, y0) hay que dar yi – xi pasos en la dimensión i (si es positivo, en una dirección, y si es negativo, en la contraria). Justamente ésa es la información que se añade en el registro de encaminamiento:

RE = [yn–1 – xn–1, yn–2 – xn–2, ..., y1 – x1, y0 – x0]

Veamos un ejemplo en dos dimensiones:

del nodo (1, 0) al nodo (3, 3):

RE0 = 3 – 0 = 3 RE1 = 3 – 1 = 2

RE = [2, 3]

Según va avanzando el paquete por la red, hay que ir actualizando el

registro de encaminamiento en los encaminadores intermedios; si en el ejemplo anterior se escoge el camino remarcado en la figura, el registro de encaminamiento irá modificándose de la siguiente manera en cada paso:

[2, 3] → [2, 2] → [2, 1] → [2, 0] ↓ [1, 0] ↓ [0, 0]

En cada “paso” en la red se actualiza un elemento (el de la dimensión correspondiente) del registro de encaminamiento en una unidad. Cuando todos los elementos son 0, el paquete ha llegado a su destino.

b. Toros El procedimiento es el mismo, pero en este caso hay que tener en cuenta que tenemos un anillo en cada dimensión. Y en un anillo siempre hay dos caminos para ir de un punto a otro: por “delante” o por “detrás”; eso sí, escogiendo siempre el camino mínimo. Por tanto, en un anillo de k nodos no se dan nunca más de d = k/2 pasos en una dirección, ya que siempre es posible efectuar k – d pasos en la dirección contraria, es decir, recorrer una distancia menor.

Así pues, el registro de encaminamiento se calcula como en el caso anterior, pero se efectúa luego una fase de corrección para buscar el camino mínimo. En un caso general, para ir de X(xn–1, xn–2, ..., x1, x0) a

(1, 0)

(3, 3)

0 1 2 3 0

1

2

3

Page 34: 6.1 INTRODUCCIÓN

▪ 202 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

Y(yn–1, yn–2, ..., y1, y0) el registro de encaminamiento se calcula de la siguiente manera:

for i = 0 to n-1 do ; para todas las dimensiones begin REi = Yi – Xi ; elemento i del reg. de encaminamiento if (|REi| > k/2) then ; corregir el camino, es muy largo if (REi > 0) then REi = REi – k else REi = REi + k end

Por ejemplo, en dos dimensiones:

del nodo (1, 0) al nodo (3, 3)

RE0 = 3 – 0 = 3 3 > 2 → 3 – 4 = –1 RE1 = 3 – 1 = 2

RE = [2, –1]

Como en el caso de las mallas, el registro de encaminamiento se va actualizando en cada paso en la red; en el ejemplo: [2, –1] → [2, 0] → [1, 0] → [0, 0].

c. Hipercubos. También en este caso el encaminamiento de los paquetes

es sencillo. El hipercubo es un cubo binario, es decir, tiene sólo dos nodos en cada dimensión, con etiquetas 1 y 0. La etiqueta de cada nodo es simplemente el conjunto de etiquetas en cada dimensión.

Para ir de i a j hay que cruzar las dimensiones en las que las etiquetas de cada nodo son diferentes. Por ejemplo, para ir del nodo (0001) al nodo (0100) hay que dar dos pasos en la red, en la primera y en la tercera dimensión, ya que las direcciones sólo se diferencian en esos bits. Por tanto, el registro de encaminamiento se calcula simplemente efectuando una operación xor bit a bit entre las etiquetas del nodo destino y origen:

RE = [origen xor destino].

Por ejemplo, para ir del nodo (0010) al nodo (1100):

0 1 2 3

0 1 2 3

Page 35: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 203 ▪

RE = 0010 xor 1100 = [1110]

Hay que dar tres pasos, en las dimensiones 2, 3 y 4, tal como aparece en la figura. Con cada paso que se efectúa se pone a 0 el bit correspondiente del registro de encaminamiento:

[1110] → [1100] → [1000] → [0000]

d. Árboles. También en este caso es sencillo el encaminamiento de los paquetes. En primer lugar, habrá que decidir hasta qué nivel del árbol hay que subir (mediante una comparación de las direcciones origen y destino), y, después, por qué rama hay que descender para llegar al destino (normalmente utilizando la dirección destino). Dejamos a modo de ejercicio cómo generar y utilizar el registro de encaminamiento en los árboles.

6.5.4.2 Elección del camino: estático o adaptativo

Cuando un paquete llega a un encaminador de la red se analiza la cabecera del mismo para saber si va destinado al procesador local o a otro procesador, y para ello se utiliza el registro de encaminamiento. Cuando todos los campos del registro de encaminamiento son 0, entonces el paquete ha llegado al destino; si no, debe continuar hacia adelante.

Aunque el registro de encaminamiento esté perfectamente definido, el camino entre origen y destino no es único, y, en general, dispondremos de muchas alternativas. Por ejemplo, si hay que dar dos pasos en ambas direcciones de una malla —RE = [2, 2]—, tenemos 6 posibilidades diferentes de recorrer el camino: x-x-y-y, x-y-x-y, x-y-y-x, y-x-x-y, y-x-y-x, y-y-x-x. ¿Qué camino hay que seguir? ¿Hay que seguir siempre el mismo camino?

▪ Encaminamiento estático

El encaminamiento de los paquetes es estático si todos los paquetes que van del nodo i al nodo j utilizan siempre el mismo camino, sea cual sea la situación de tráfico en la red. La elección más habitual se conoce como DOR (dimension order routing) o, en el caso de dos dimensiones, como

(0000) (0001)

(0010) (0011)

(0100) (0101)

(0110) (0111)

(1000) (1001)

(1010) (1011)

(1100) (1101)

(1110) (1111)

Page 36: 6.1 INTRODUCCIÓN

▪ 204 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

"primero-X-luego-Y". Los paquetes avanzan hacia el destino recorriendo primero todo el camino en una dimensión, luego en la siguiente, etc. Por tanto, en el encaminamiento estático sólo se utiliza uno de todos los caminos posibles para ir del nodo i al nodo j.

Dado que se utiliza un sólo camino, se pierde la tolerancia a fallos de bajo nivel; si no se puede avanzar, porque hay una avería, habrá que utilizar algoritmos de otro nivel para que la comunicación se pueda efectuar.

▪ Encaminamiento dinámico o adaptativo

El encaminamiento es dinámico si los paquetes que van de i a j no siguen siempre el mismo camino, sino que escogen el camino más adecuado en función de la situación concreta de la red. El encaminamiento dinámico es más flexible, y permite utilizar caminos en los que la densidad de tráfico sea menor para evitar zonas de la red que estén temporalmente saturadas.

En la siguiente figura se muestran tres opciones (con línea continua la correspondiente al encaminamiento DOR) para poder ir del nodo (1, 0) al nodo (3, 3). En los tres casos el registro de encaminamiento es RE = [2, 3]. Si, por ejemplo, hubiera una congestión de tráfico alrededor del nodo (1, 1), podría ser más adecuado utilizar el camino (b) en lugar del (a). La misma idea se puede aplicar para evitar fallos en la red: aunque se estropee el enlace (1, 2) → (1, 3), todavía tenemos posibilidades de llegar al nodo (3, 3), utilizando, por ejemplo, el camino (c).

Existen muchas maneras diferentes de efectuar la adaptación. Una de las

más conocidas es la denominada zigzag: siempre que sea posible, los paquetes no agotan el recorrido en una dimensión, sino que los van intercambiando. De esa manera se consigue mantener hasta el final la posibilidad de utilizar caminos alternativos frente a posibles problemas de tráfico o de fallos en la red. Para el caso de dos dimensiones, un ejemplo de algoritmo simplificado podría ser el siguiente:

(1, 0)

(3, 3)

0 1 2 3

0

1

2

3

a

b

c

Page 37: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 205 ▪

si (|REx| > |REy|) entonces Sigue_por_el_eje_X ; X+, X- si_no Sigue_por_el_eje_Y ; Y+, Y- si (salida_elegida_ocupada) entonces Intenta_la_otra ; si hay alternativa

Aunque el encaminamiento dinámico ofrece ciertas ventajas, hay que valorar, como siempre, los posibles inconvenientes que introduce. A menudo, el encaminamiento dinámico es consecuencia de una apuesta; por ejemplo, si en un momento determinado no se puede utilizar una salida porque está ocupada, se decide avanzar por otro camino. Por desgracia, para tomar esa decisión se utiliza sólo información parcial, basada normalmente en el tráfico local, y no se sabe en qué situación se encuentra el nuevo camino elegido (quizás peor que el rechazado). En todo caso, cuando la red no es simétrica o cuando el tráfico se reparte de manera no homogénea, es fácil que surjan zonas de alta densidad de tráfico en la red (hot spots), y en esos casos puede resultar efectivo el encaminamiento adaptativo.

Por otra parte, si se divide un mensaje en varios paquetes y éstos utilizan caminos diferentes para llegar al destino, lo más probable es que lleguen en desorden, por lo que habrá que recomponer el mensaje en el destino (y los paquetes deberán incluir más información de control). Además, los encaminadores deberán ser más complejos para poder aplicar encaminamiento adaptativo y, por tanto, el tiempo de procesamiento de los paquetes será mayor. Por último, como analizaremos más adelante, cuando se utiliza encaminamiento adaptativo se pueden producir bloqueos (deadlock) en la red: los paquetes no son capaces de avanzar, porque se impiden avanzar unos a otros. Por tanto, hay que evaluar cuidadosamente el uso de encaminamiento adaptativo.

▪ Encaminamiento no mínimo

Aunque el camino recorrido por los paquetes debiera ser de longitud mínima, en algunos casos puede ser necesario o útil utilizar caminos más largos para llegar a destino, para, por ejemplo, rodear una zona de tráfico denso, o, sobre todo, para evitar zonas con fallos en la red.

Por ejemplo, un paquete va desde el procesador (1, 0) al procesador (3, 2), y cuando está en el encaminador del nodo (1, 2) detecta que no puede avanzar porque el encaminador del nodo (2, 2) no responde (se ha estropeado). En se caso, tiene la posibilidad de tomar un camino más largo, como el que aparece en la figura, para poder completar la comunicación.

Page 38: 6.1 INTRODUCCIÓN

▪ 206 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

Salvo las excepciones citadas, y con las precauciones requeridas, los caminos que van a recorrer los paquetes serán siempre de distancia mínima.

6.5.5 Control del flujo de información

Los paquetes avanzan en la red de encaminador a encaminador hasta llegar a su destino. Pero, ¿cómo se efectúa el avance? ¿Qué hay que hacer si los recursos que necesita un paquete para seguir adelante están ocupados? Este tipo de cuestiones se conocen como “control del flujo” de los paquetes.

6.5.5.1 Avance de los paquetes: SF, WH, CT

¿Cómo se transmite un paquete de encaminador a encaminador? Supongamos que los enlaces de la red son de un byte, con lo que en un “ciclo” de transmisión se pasará un byte entre dos encaminadores. Los paquetes que hay que transmitir van a ser bastante más largos, por lo que necesitaremos varios ciclos de transmisión para pasar todo el paquete (todos los flits o bytes) de un encaminador al siguiente. Mientras tanto, ¿qué hay que hacer con un paquete que se está recibiendo? Veamos las dos alternativas principales: store-and-forward y wormhole / cut-through.

6.5.5.1.1 Store-and-forward (SF)

Store-and-forward es el nombre de la técnica empleada en la primera generación de multicomputadores para pasar los paquetes entre dos encaminadores sucesivos (técnica habitual en las redes LAN y WAN).

(1, 0)

(3, 2)

0 1 2 3

0

1

2

3

Page 39: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 207 ▪

En este caso, el encaminador que está recibiendo el paquete no hace nada

con el mismo hasta que no lo termina de recibir del todo. A continuación, analiza la cabecera del paquete y decide por dónde debe retransmitirlo. De esa manera, la transmisión de los paquetes se limita siempre a dos encaminadores: el que transmite y el que recibe. Mientras se produce la transmisión, el paquete se guarda en un búfer interno del encaminador (véase la figura siguiente).

En primera aproximación, el tiempo de comunicación de un paquete resulta proporcional al tamaño del paquete (L) y a la longitud del camino recorrido (d):

Tsf ~ L × d

Así pues, cuando se duplica la distancia a recorrer, también se duplica la latencia de un paquete.

6.5.5.1.2 Wormhole (WH) / Cut-through (CT)

La técnica SF no es muy adecuada para los sistemas MPP, ya que la latencia de la comunicación puede resultar muy elevada (en función de la distancia a recorrer), y la comunicación es crucial en muchas de las aplicaciones que se ejecutan en paralelo. Por ello, los sistemas paralelos actuales utilizan otra técnica conocida como wormhole (WH) o cut-through (CT).

1

2 3 4

2 3 4

3 4

4

1

1

2

2 3

1

2 3 4

2 3 4

3 4

4

1

1

2

1

2 3

1

2 3 4

2 3 4

3 4

4

1

1

2

1

2 3

1

2 3 4

store-and-forward

encaminadores

tiempo

Page 40: 6.1 INTRODUCCIÓN

▪ 208 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

Cuando un encaminador recibe el primer flit de la cabecera de un paquete

(normalmente una parte del registro de encaminamiento) analiza a dónde se dirige. Si debe continuar con su recorrido, entonces se le asigna directamente un puerto de salida y se comienza a retransmitir al siguiente encaminador, sin esperar a recibir todo el paquete; el resto de flits del paquete se enviará tras el primero, por el mismo camino, según vayan llegando. Así, el paquete queda distribuido a lo largo del camino hacia el destino: la comunicación se ha “segmentado”, ya que muchos encaminadores toman parte simultáneamente en la transmisión de un paquete.

Se necesitan d ciclos para que el primer flit de la cabecera del paquete llegue al destino, tras lo cual llegarán, ciclo a ciclo, el resto de flits del paquete. Por tanto, en primera aproximación, la latencia de un paquete en modo WH o CT será proporcional a la suma de L y d. Es decir,

Tct/wh ~ L + d

Claramente, la latencia de la comunicación en modo wormhole será menor que en modo store-and-forward, ya que L y d se suman en lugar de multiplicarse: el efecto de la distancia a recorrer en el tiempo de transmisión es mucho menor en modo CT/WH que en modo SF.

La transmisión de un paquete se efectúa de la misma manera en modo wormhole que en modo cut-through, pero si la cabecera de un paquete no puede continuar avanzando, porque la salida que necesita utilizar está ocupada por otro paquete, la respuesta es diferente en ambos casos:

▪ Wormhole. Se detiene la recepción del paquete, y éste queda bloqueado a lo largo de todo el camino que recorre. Cuando se libera el camino de salida, se retoma la transmisión de los flits en todos los encaminadores implicados.

1

2 3 4

2 3 4

3 4

4

1

2 3

4

1

1

1

2

1

2 3

1

2 3 4

cut-through / wormhole

1

2 3

4

tiempo

Page 41: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 209 ▪

El paquete que se ha parado va a mantener ocupados muchos recursos de la red, por lo que es posible que aumenten los conflictos entre paquetes.

▪ Cut-through. Aunque la cabecera del paquete no pueda avanzar, se

siguen recibiendo el resto de los flits del paquete, y se almacenan en un búfer propio del encaminador hasta que el canal de salida que se necesita se libere, en cuyo caso se proseguirá con la transmisión de la cabecera del paquete. De esta manera, el paquete cuya cabecera no puede avanzar no mantiene ocupados tantos recursos de la red, con lo que se producirán muchos menos conflictos con otros paquetes.

Si se utiliza cut-through, es necesario que los encaminadores dispongan de búferes con capacidad para almacenar paquetes de manera transitoria, hasta que prosigan su camino; si utilizamos wormhole, ese espacio, en principio, no es necesario (bastaría con poder guardar un flit: el que se está

wormhole

encaminadores

tiempo 1

2 3 4

2 3 4

3 4

4

1

2

3

4

1

1

1

2

1

2 3

1

2 3 4

1

2 3

4

3 4 1

3 4 1

3 4 1

2

2

2

1

2 3 4

2 3 4

3 4

4

1

1

2

1

2 3

1

2 3 4

cut-through

1

1

2 3

4

tiempo

1

2 3

4

1

2 1 3 2 1

2 3 4

3 4

4

Page 42: 6.1 INTRODUCCIÓN

▪ 210 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

procesando en ese momento). En muchas máquinas se aplican estrategias intermedias entre WH y CT: no hay capacidad para almacenar un paquete completo, pero pueden guardarse varios flits (por ejemplo, pueden almacenarse 4 flits de un paquete a la espera de que quede libre el puerto de salida).

Analizado desde otro punto de vista, podemos decir que CT representa un compromiso entre WH y SF. Cuando hay poco tráfico en la red, CT se comporta igual que WH: no habrá conflictos en el uso de los recursos y, por tanto, no se almacenan los paquetes en los encaminadores intermedios. En cambio, cuando el tráfico es elevado CT se acerca al comportamiento de SF: los conflictos son muy habituales en los encaminadores de la red, y los mensajes se almacenan casi siempre.

6.5.5.2 Conflictos en el uso de recursos: los búferes

Los paquetes recorren la red utilizando los enlaces de la misma y los encaminadores de mensajes. La red es por tanto un recurso compartido, que va a ser utilizado simultáneamente por muchos paquetes. ¿Cómo hay que afrontar los conflictos que, inevitablemente, se van a producir al usar esos recursos? Ya hemos comentado en qué difieren CT y WH al tratar los conflictos: el paquete que no puede seguir adelante se almacena temporalmente en el encaminador intermedio, o se deja bloqueado a lo largo de toda la red.

En el caso de CT (y, por definición, de SF) se necesita de un “poco” de memoria para almacenar transitoriamente los paquetes (o algunos flits de un paquete) en los encaminadores, hasta que la salida correspondiente esté libre. Normalmente, los encaminadores no disponen de gran cantidad de memoria para almacenar mensajes, sino que tienen la posibilidad de almacenar unos cuantos bytes o unos cuantos paquetes. Por ejemplo, si los paquetes son de 64 bytes, sería suficiente con una capacidad de 1 o 2 kB (16 o 32 paquetes) por canal; no parece razonable tener una memoria de 1 MB para gestionar un posible bloqueo de 16000 paquetes. Además, el funcionamiento de los encaminadores debe ser eficiente y rápido, con el fin de que la latencia de los paquetes sea lo menor posible, y para ello lo más adecuado es que los encaminadores sean lo más sencillos posibles.

Hay muchas maneras de organizar y gestionar esos búferes (normalmente en forma de cola FIFO), y cada una de ellas tiene sus ventajas e inconvenientes. Además, sea cual sea el número de búferes, siempre es posible que una aplicación genere un tráfico muy intenso en un momento

Page 43: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 211 ▪

dado, que sature la capacidad de guardar más paquetes. ¿Qué hacer en esos casos? Analicemos esos problemas uno a uno, aunque sea de manera somera.

▪ Estructura de los búferes: compartidos o separados

El espacio de memoria de los encaminadores puede ser compartido, para paquetes que lleguen por cualquier entrada, o puede repartirse a cada entrada, sólo para paquetes que lleguen por cada entrada30.

búferes compartidos búferes repartidos

Si el espacio de memoria es compartido, la capacidad de almacenamiento disponible se utiliza de manera más eficaz, ya que siempre hay sitio para un paquete hasta que se agota toda la memoria del encaminador; sin embargo, si lo repartimos entre los diferentes puertos de entrada del encaminador, puede que se llene una de las colas de entrada y no se pueda admitir más paquetes en esa entrada aunque pudiera quedar sitio libre en otras colas. Pero, por otra parte, la gestión de una memoria común para todas las entradas es más compleja, tanto en la carga de nuevos paquetes como en la salida de los mismos: se necesita una cola multientrada, porque la carga de paquetes (de diferente tamaño) puede ser simultánea durante varios ciclos y desde varios puertos de entrada; también debe ser multisalida, para que un paquete que no puede continuar su viaje no bloquee a otros paquetes que sí podrían continuar. Como siempre, se trata de buscar un compromiso entre eficiencia y complejidad.

En muchos sistemas, los paquetes tienen diferentes niveles de prioridad; por ejemplo, los paquetes de control (en general, paquetes cortos) pueden tener prioridad sobre los paquetes de datos (más largos). En esos casos, los búferes se organizan en varias colas, una por cada clase de paquete. ▪ Búferes en las entradas o en las salidas 30 Se trata de una cuestión que ya hemos debatido en otros contextos; por ejemplo, en el uso de las

estaciones de reserva en los procesadores superescalares (las instrucciones esperan en las unidades de reserva a que esté disponible la unidad funcional que necesitan). Las conclusiones que obtenemos son las mismas.

Page 44: 6.1 INTRODUCCIÓN

▪ 212 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

Los búferes de espera pueden asociarse tanto a los puertos de entrada como a los de salida. Si se colocan en la entrada, los paquetes pasan directamente al búfer de espera si su salida no está disponible o si hay paquetes esperando en los búferes de esa entrada. En cambio, si se colocan en la salida, primeramente se efectúa la operación de asignación de puerto de salida (encaminamiento) y luego se pasa, en su caso, al búfer de espera.

búferes en las entradas búferes en las salidas

Las ventajas y desventajas parecen claras. La gestión es más sencilla a la entrada, pero su uso no es tan eficiente (el primer paquete de una cola bloquea al resto de los paquetes, aunque éstos tengan camino libre). La gestión de los paquetes es más eficiente si se colocan a la salida, ya que ya se ha efectuado la asignación de salida para cuando se bloquea el paquete, pero su estructura es más compleja, ya que de nuevo deben ser colas multientrada.

▪ ¿Y si se llenan las colas de búferes?

En momentos en que el tráfico de paquetes sea muy intenso puede que se sature la capacidad de almacenamiento de los búferes de los encaminadores y no se pueda admitir más paquetes. ¿Cómo se controla esa situación?

Lo más habitual es utilizar un protocolo de control entre encaminadores adyacentes (link flow control). Cuando hay que enviar un paquete de un encaminador a otro, primeramente se solicita permiso para la transmisión; cuando se recibe dicho permiso, se procede a transmitir los flits correspondientes. Si el receptor no dispone de sitio para el paquete que va a recibir, entonces no dará permiso de transmisión y ésta no se efectuará (por otra parte, la comunicación en modo WH requiere de un protocolo similar).

¿hay sitio?

si/no

datos

Page 45: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 213 ▪

De esa manera se produce lo que se conoce como back-pressure: el no poder transmitir los paquetes hace que se llenen los búferes del encaminador y que éste transmita hacia atrás la imposibilidad de seguir recibiendo paquetes, con lo que, en un momento u otro, los nodos dejarán de inyectar más paquetes hasta que se recupere la posibilidad de seguir transmitiendo.

En la misma línea, en algunas máquinas se limita el número de paquetes que puede enviar un nodo, para evitar “inundaciones” de paquetes en determinados momentos. Cuando ya se ha recibido en destino el cupo de paquetes permitido, el procesador de destino da permiso al emisor para que pueda enviar más paquetes (una especie de handshake global). Una idea similar se aplica en el caso del conocido protocolo token ring (anillo con testigo), en el que un procesador no puede inyectar paquetes en la red salvo que disponga del testigo (token) que da permiso para ello. Dicho testigo es un mensaje de control especial que circula por toda la red de manera cíclica recorriendo todos los nodos de la misma.

Existen más técnicas para controlar el desbordamiento de la capacidad de almacenamiento de los encaminadores31. Por ejemplo, si llega un nuevo paquete a un encaminador y éste no dispone de sitio para recibirlo por estar la correspondiente cola de búferes llena, siempre puede cogerse uno de los paquetes que está en la cola y “echarlo fuera”, desviándolo a otro sitio, para dejar sitio al nuevo paquete. Está claro que utilizando esa estrategia siempre hay sitio para acoger paquetes, pero algunos paquetes van a verse penalizados al tener que efectuar recorridos más largos (con lo que su latencia será mayor). Para escoger el paquete que se va a desviar pueden utilizarse diferentes alternativas: al azar, por ejemplo, o tal vez en función del tiempo que el paquete lleve en la red, o de la distancia que le falte para llegar a destino. Por ejemplo, el encaminador conocido con el nombre de chaos router utilizó esta técnica.

En resumen: la estructura de un encaminador —puertos de entrada y de salida, búferes, lógica del autómata, control del flujo, prioridades...— es un aspecto fundamental en el sistema de comunicación de un multicomputador. Los paquetes se tienen que transmitir con una latencia mínima y la red tiene

31 En el caso de Internet, por ejemplo, los paquetes se "eliminan". Por tanto, cuando el tráfico es muy

elevado, la red no es nada fiable (reliable), por lo que es necesario utilizar protocolos tipo TCP/IP (tras un cierto tiempo de espera, los paquetes se retransmiten). En general, este tipo de alternativa no es muy eficiente en un sistema de cálculo paralelo.

Page 46: 6.1 INTRODUCCIÓN

▪ 214 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

que ser capaz de manejar una cantidad de tráfico elevada. En general, sigue siendo válida la regla de que lo más simple es lo más rápido.

6.5.6 Eficiencia de la comunicación: latencia y throughput

La red de comunicación, y junto con ella todo el sistema de paso de mensajes, no es sino un intermediario necesario para poder llevar a cabo la comunicación entre procesos. Dado que la comunicación es una parte más del proceso de ejecución en paralelo, el subsistema de comunicación de una máquina MPP debe ser muy eficiente.

Dos son los parámetros más habituales para medir la calidad del sistema de comunicación de un computador paralelo: la latencia de los paquetes y el número de paquetes que puede gestionar (throughput). La latencia de un paquete define el tiempo necesario para efectuar la comunicación en la red. El throughput, por su parte, marca el número de paquetes que es capaz de gestionar la red sin saturarse. La red ideal es capaz de gestionar un número muy elevado de paquetes con una latencia muy baja.

Antes de analizar los diferentes parámetros que conforman el tiempo de comunicación, repasemos algunos conceptos:

• Anchura de los enlaces: número de bits que puede transmitir simultáneamente el enlace; por ejemplo, 8 bits (en los enlaces serie, un solo bit). En algunos casos, a este parámetro se le conoce también como phit (physical unit).

• Ciclo de transmisión: tiempo necesario para transmitir un phit. Si el sistema es síncrono, normalmente un ciclo del reloj correspondiente.

• Ancho de banda de los enlaces: cantidad de información que se puede transmitir en un segundo, habitualmente en (Mega) Gigabit/s (109 bit/s). Por ejemplo, enlaces de 10 Gb/s.

• Ancho de banda útil: la información a transmitir se estructura en paquetes, y eso significa cierta sobrecarga, ya que además de los datos hay que transmitir información de control (cabecera...). Por ello, para transmitir n bits de datos tenemos que transmitir n + nk bits. El ancho de banda útil se define como n / (n + nk). Es obvio que necesitamos que nk sea lo menor posible.

Page 47: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 215 ▪

• Tiempo de encaminamiento (routing time, tr): tiempo necesario en cada encaminador para procesar la cabecera del paquete y decidir por dónde debe ir. Sin duda, interesa que este tiempo sea el menor posible.

Veamos ahora cómo modelar el tiempo de comunicación en la red.

6.5.6.1 Tiempo de comunicación en la red

Los paquetes avanzan en la red de encaminador a encaminador. El tiempo de transmisión de un paquete va a depender de muchos factores: topología de la red, camino elegido, método de avance de los paquetes, estrategia de control de flujo... y, por supuesto, el nivel de tráfico en la red. Para empezar, analicemos la red en situación de tráfico cero. Supongamos, por simplicidad, que flit = phit = byte. Vamos a considerar los siguientes parámetros:

• L, tamaño del paquete en bytes • d, longitud del camino que va desde el nodo origen al destino • tr, tiempo de proceso de la cabecera de un paquete en un encaminador • B, ancho de banda de los enlaces

El tiempo de transmisión de un paquete —su latencia— depende básicamente de la manera en que avanza en la red: SF o CT/WH.

6.5.6.1.1 Store-and-forward

Como hemos comentado, en modo SF los paquetes se retransmiten por completo de un encaminador al siguiente antes de continuar su viaje. Por ello, por cada paso que se da en la red, se necesita el tiempo para transmitir todo el paquete —L / B— más el de procesamiento de la cabecera —tr—.

Por tanto, la latencia de un paquete puede darse de la siguiente manera32:

Tsf = d × (L / B + tr)

Es decir, la latencia es proporcional a la distancia a recorrer y al tamaño del paquete (d × L); si se duplica la distancia, se duplica la latencia.

32 Cuidado con las unidades. Por una lado, L y B, ambas en bits o en bytes; y, por otro, L/B y tr, ambas

en nanosegundos (o en microsegundos).

Page 48: 6.1 INTRODUCCIÓN

▪ 216 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

6.5.6.1.2 Cut-through / Wormhole

En este segundo caso, la cabecera avanza de encaminador a encaminador hasta el destino, y detrás viene el resto del paquete. La latencia de la transmisión puede darse, por tanto, así:

Tct = d × (1 / B + tr) + (L–1) / B

es decir, se transmite y se procesa la cabecera (1 flit = 1 byte) d veces, y finalmente se reciben en destino el resto de bytes del paquete (L–1)33.

En este caso, por tanto, la latencia de un paquete es de tipo d + L: si la distancia a recorrer se duplica, la latencia no lo hará.

De acuerdo a las expresiones anteriores, si se utiliza SF para enviar los paquetes resulta crucial que las distancias a recorrer sean bajas. Éste era el caso de la primera generación de multicomputadores, por lo que la red escogida fue el hipercubo. El hipercubo tiene en cambio unos cuantos problemas: el grado de la red es elevado y crece con el tamaño del sistema, con lo que es más complicado diseñar un módulo de comunicación válido para cualquier red (¿cuántos puertos de entrada/salida se necesitan?). Además, la densidad del cableado es alta en los hipercubos, lo que dificulta la construcción del sistema en un plano. Por último, no es posible construir hipercubos regulares de cualquier tamaño, ya que el número de procesadores se limita a potencias de dos (256, 512, 1024...). A pesar de todas las dificultades, ésa fue la red elegida debido a sus buenos parámetros de distancia.

En la medida en que fue avanzando la tecnología y se acumuló experiencia, se abandonó SF en favor de técnicas más eficientes, tales como CT/WH, en las que la distancia a recorrer no es un parámetro tan crítico en la latencia de los paquetes. Por ello, las redes más utilizadas actualmente son las redes topológicamente sencillas y de pocas dimensiones (2 o 3).

En la siguiente tabla aparece una comparación entre las latencias máximas y medias, en ciclos, de un paquete de 256 bytes en una red de 1024 nodos (sin tráfico), cuando la comunicación es en modo CT y SF. El tiempo de procesamiento del flit de cabecera, tr, es un ciclo (= 1/B, tiempo de transmitir un byte (flit)). 33 Si, en un caso general, el tamaño del flit (mínima información necesaria para poder procesar un

paquete) es de k bytes, entonces, en lugar de tener 1/B y (L–1)/B, tendremos k/B y (L–k)/B.

Page 49: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 217 ▪

Hipercubo (210) Toro (32 × 32) Malla (32 × 32) diámetro

distancia media

10 5

32 16

62 22

SF Tmax Tmed

2570 1285

8224 (+ 289%) 4112 (+ 220%)

15934 (+ 520%) 5654 (+ 340%)

CT/WH Tmax Tmed

275 265

319 (+ 16%) 287 (+ 8%)

379 (+ 38%) 299 (+ 13%)

Como podemos ver, si el control de flujo es tipo CT la latencia máxima

sólo sube de 275 a 379 (un 38%) del caso de un hipercubo a una malla, aunque la distancia a recorrer, el diámetro, es 6 veces mayor. En media, el incremento es aún menor, de 265 a 299, un 13% (en el caso del toro, los incrementos son menores, un 16% en el máximo y un 8% en media).

6.5.6.2 Considerando el tráfico en la red

Dado que los recursos de la red se comparten entre todos los procesadores, cuando el tráfico es elevado los paquetes no encuentran siempre disponibles los recursos que necesitan, y se pararán en el camino, normalmente en los búferes de los encaminadores, hasta que puedan continuar al liberarse el recurso que necesitan (puerto de salida, enlace...); por tanto, la latencia de los paquetes crecerá. No es sencillo modelar el comportamiento de la latencia de los paquetes en función del tráfico en la red. En general, este modelado se hace basándose en la teoría de colas, los modelos cliente/servidor, etc. Como resultado de esos modelos se obtiene el tiempo de espera de los paquetes en los búferes intermedios, la ocupación de dichos búferes, etc. (un modelo sencillo se plantea en los ejercicios).

El comportamiento habitual de una red de comunicación en un sistema MPP es el que se muestra en estas figuras, tanto para la latencia como para el throughput (para paquetes que van distancias medias):

Tráfico (b/s)

Latencia (s)

Latencia sin tráfico

Throughput (b/s)

Tráfico (b/s)

Tráfico máximo i

Page 50: 6.1 INTRODUCCIÓN

▪ 218 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

Inicialmente, cuando el tráfico es muy reducido, los mensajes llegan a destino en el tiempo mínimo esperado (en función de la distancia recorrida) sin sufrir ningún retraso. Según el tráfico va creciendo, la latencia de la comunicación va creciendo suavemente, debido a conflictos sueltos que hacen que algunos paquetes tengan que pararse algunos ciclos (por lo demás, los búferes están prácticamente vacíos). Pero, a partir de cierto nivel de tráfico, los conflictos entre paquetes se multiplican y la red se satura: no puede gestionar más paquetes, los búferes se llenan, y la latencia de los paquetes crece indefinidamente. Se trata de un estado de la red no admisible, que hay que evitar.

Si analizamos el throughput, es decir, el número de paquetes que puede gestionar la red por segundo, vemos que cuando no hay problemas de tráfico la red lleva a su destino todos los paquetes que se le encomiendan (su comportamiento es lineal). En cambio, superado cierto nivel de tráfico, la red no puede gestionar más paquetes, estamos en el throughput máximo, en el nivel de saturación. En algunas redes, si se solicita un nivel de tráfico superior al de saturación, el número de paquetes que efectivamente se procesa desciende del valor máximo. Se trata de un comportamiento que se debe evitar, ya que si se produce un momento de alta congestión en el tráfico en la red, va a costar mucho tiempo volver a una situación normalizada.

A la hora de diseñar la red de comunicación de un sistema paralelo es importante prever el tráfico máximo que puede soportar, ya que habrá que dimensionar la red para que trabaje normalmente lejos de su punto de saturación. En todo caso, siempre es posible que una determinada aplicación genere un número inusualmente alto de paquetes en determinados momentos. Aunque no se pueda admitir tal cantidad de paquetes de manera sostenida (saturaríamos la red), el sistema de comunicación debería ser capaz de gestionar esas “avalanchas” puntuales de paquetes de manera eficaz, aunque fuera con latencias superiores a las habituales, y de recuperar la situación normal de procesamiento de paquetes al volver el tráfico a su nivel habitual; es decir, la red no debe bloquearse porque en momentos concretos se produzcan demandas de tráfico por encima del máximo permitido.

6.5.6.2.1 WH versus CT

Como ya hemos comentado, la única diferencia entre wormhole y cut-through se encuentra en cómo se tratan los conflictos: cuando un paquete no puede continuar su recorrido, se deja parado en la red (WH), o se va recogiendo, ciclo a ciclo, en el encaminador que ha parado la cabecera del

Page 51: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 219 ▪

paquete (CT). Ambos comportamientos tienen su reflejo en la latencia de los paquetes y en el throughput de la red.

La latencia de los paquetes que avanzan en modo WH crece más rápido con el nivel de tráfico en la red, ya que los paquetes que se detienen mantienen ocupados muchos recursos, lo que lleva a que se detengan más paquetes y, por tanto, crezca su latencia. Así, la capacidad de gestionar paquetes de la red será menor: la red se satura con un tráfico menor.

6.5.6.3 Cálculo del throughput máximo

Ya hemos mencionado que uno de los parámetros de calidad de una red de comunicación es el throughput, el número de paquetes que puede procesar por unidad de tiempo. Aunque normalmente el nivel de tráfico sea bajo, habrá momentos en los que la demanda puede crecer muy rápidamente, y la red debe ser capaz de dar respuesta adecuada a dicha demanda.

El número de paquetes que puede gestionar la red depende de muchos parámetros: patrones de comunicación, topología, estructura de los encaminadores de mensajes, control del flujo, etc. Si nos fijamos exclusivamente en los aspectos topológicos, podemos obtener un valor máximo para el throughput de la red.

Supongamos que la comunicación es aleatoria, tanto en el tiempo como en el espacio: los paquetes que se envían desde un procesador se distribuyen homogéneamente en el tiempo y entre todos los procesadores de la red. Uno de los parámetros topológicos de la red es el ancho de banda de la bisección, es decir, el ancho de banda acumulado de los enlaces que es necesario eliminar para dividir la red en dos partes iguales.

Si la probabilidad de comunicación es la misma entre dos procesadores cualesquiera, de todos los paquetes que se generan en una mitad de la red, el 50% irá destinado a procesadores de esa mitad de la red, pero el otro 50% irá

Tráfico (b/s)

Latencia (s) Throughput (b/s)

Tráfico (b/s)

WH

CT CT WH

Page 52: 6.1 INTRODUCCIÓN

▪ 220 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

destinado a procesadores de la otra mitad de la red, y para ello deberán utilizar los enlaces que forman parte de la bisección de la red.

Por tanto, el ancho de banda de esos enlaces marca un límite al tráfico máximo que puede admitir la red. Es decir:

P/2 × NPaq × 1/2 × L = ABB

donde P/2 → número de procesadores de una mitad de la red NPaq → número de paquetes que genera un procesador por ciclo 1/2 → fracción de paquetes que pasa a la otra mitad de la red L → tamaño o longitud de los paquetes ABB → ancho de banda acumulado de los enlaces de la bisección de la red Así pues, si el tráfico es aleatorio, el máximo número de paquetes que

puede generar un procesador por ciclo es:

NPaq = 4 × ABB / (P × L) (o, en flits, 4 × ABB / P)

En la siguiente tabla se muestran el tráfico aleatorio máximo que admiten una malla, un toro y un hipercubo (1 flit = 1 byte). El resultado se da en byte/ciclo (es decir, 4 × ABB / P, sin tener en cuenta el ancho de banda de los enlaces)

256 procesadores malla 2D toro 2D hiperc. 8D

Bisección de la red (núm. enlaces) 16 kn–1

32 2kn–1

128 2n–1 = P/2

Número máximo de flits por ciclo que puede generar cada procesador (med.)

0,25 4 / k

0,5 8 / k

2 (constante)

El hipercubo, una red muy densa, admite un mayor nivel de tráfico aleatorio que la malla o el toro, pero a costa de que la topología sea muy compleja de construir, sobre todo si el número de nodos es grande. Por su parte, el nivel teórico máximo de tráfico en el toro dobla al de la malla, esta

enlaces de la bisección

P/2 P/2

NPaq/2

NPaq/2

Page 53: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 221 ▪

vez con muy poco coste: convertir las cadenas en anillos. En todo caso, los niveles de tráfico de la tabla son muy elevados: no es habitual tener que enviar un byte por ciclo en todos los ciclos y procesadores, ya que para poder enviar datos es necesario efectuar cálculo previamente.

El nivel de tráfico máximo que acabamos de obtener representa un límite teórico; en la realidad, aunque el tráfico sea aleatorio la red se satura antes de dicho límite debido a que la homogeneidad del tráfico es de tipo estadístico (en promedio).

6.5.6.4 Análisis global

En los apartados anteriores hemos especificado la latencia de un paquete en función de la técnica de transmisión (SF / CT) y de los parámetros principales (d, L, tr, B). En muchos casos, y considerando solamente el tamaño del mensaje a transmitir entre dos nodos dados, la latencia de la comunicación puede modelarse de la siguiente manera:

Tcom = tini + tflit × L

es decir, un tiempo de inicio y un tiempo por cada flit (byte) transmitido. A partir de esa expresión, es fácil deducir (igual que hemos hecho al analizar la ejecución vectorial de bucles) parámetros tales como Rmax y L1/2: máxima velocidad de comunicación y longitud de los paquetes para conseguir al menos la mitad de la velocidad máxima:

R = L / Tcom = L / (tini + tflit × L)

Rmax = lim L→∞ R = 1 / tflit L1/2 = tini / tflit

Para terminar el análisis del rendimiento de la comunicación no tenemos que olvidar que nos hemos centrado exclusivamente en el aspecto “físico” de la comunicación: la transmisión en la red. El proceso de comunicación global es, en cambio, más amplio que la pura transmisión de datos. Los paquetes tienen que ir de la memoria de un procesador a la memoria de otro procesador, por lo que un análisis más completo de la comunicación debe incluir todos los procesos a efectuar tanto en el emisor como en el receptor. Bien podría ser que todos esos procesos (copias entre los espacios de memoria y de entrada/salida, generación de los paquetes, etiquetado, ordenación, interrupciones, llamadas, cambios de contexto...) llevaran más tiempo que la pura transmisión física del paquete en la red. Es por tanto

Page 54: 6.1 INTRODUCCIÓN

▪ 222 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

necesario optimizar correctamente todos esos procesos para minimizar el “tiempo total” de comunicación. En resumen, los elementos principales de la latencia de la comunicación son los siguientes:

6.5.7 Problemas de la comunicación

La función de la red de comunicación es permitir la comunicación entre procesadores gestionando un número alto de paquetes con una latencia baja. En todo caso, lo que sí debe asegurar es que todos los paquetes enviados llegan finalmente a su destino. Por tanto, debemos hacer frente a algunos de los problemas habituales en sistemas distribuidos que ya hemos comentado (por ejemplo en los controladores snoopy): deadlock, livelock y starvation. Analicemos su efecto en un sistema de comunicación.

6.5.7.1 Deadlock (interbloqueos)

Tenemos un deadlock de comunicación si un grupo de paquetes se impiden el avance entre ellos y quedan indefinidamente retenidos en la red: m paquetes forman un ciclo y cada uno de ellos impide el avance del siguiente, no llegando nunca a su destino (latencia infinita). No es difícil que se produzca esa situación, en función de cómo se gestionen los paquetes en la red. Veamos un ejemplo (comunicación tipo WH).

(0,0) (0,3)

(3,0) (3,3)

m1: 0,1 → 2,3

m2: 1,3 → 3,1

m3: 3,2 → 1,1

m4: 2,1 → 0,2

emisor receptor transmisión entre encaminadores

espera en búferes

Sist. Op. proc E/S. SF - CT - WH // B - L - d - tr // tráfico

Sist. Op. proc. E/S

+ +

Page 55: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 223 ▪

Tenemos cuatro paquetes en la red, y los caminos elegidos forman un ciclo cerrado; tal como vemos en la figura anterior, los paquetes se autobloquean y no llegarán nunca al destino fijado.

En todos los casos, las situaciones de interbloqueo aparecen cuando se agotan los recursos de la red: en el caso wormhole, los enlaces, y en el caso cut-through, los búferes. Desde luego, el interbloqueo no aparecería nunca si las colas de búferes para guardar paquetes fueran infinitas, pero eso no es posible, y aunque sean grandes siempre cabe la posibilidad de que se llenen.

El problema del interbloqueo puede aparecer con más facilidad si los caminos que siguen los paquetes no están previamente planificados, es decir, si los caminos se escogen en función de la situación del tráfico en la red (por ejemplo, si se utiliza encaminamiento adaptativo). En caso contrario, si los caminos están prefijados, es más sencillo evitar el problema.

Sea como fuere, necesitamos que los procesos de comunicación estén libres de este problema, o sepan cómo resolverlo. Antes de comentar las estrategias principales, conviene recordar que tenemos dos alternativas para hacer frente al problema: (a) no utilizar ningún mecanismo de comunicación que pueda presentar deadlock; y (b) admitir que se pueda producir el bloqueo, y en cambio, detectar si se produce o no, y dar una solución concreta en ese momento.

6.5.7.1.1 El encaminamiento estático ayuda

Cuando los caminos que siguen los paquetes son únicos y conocidos, no es difícil asegurar caminos libres de deadlock para todos los paquetes. Por ejemplo, para una malla de 2 dimensiones, el encaminamiento DOR (primero-X-luego-Y) está libre de bloqueos. De hecho, para poder formar un ciclo con los paquetes es necesario que alguno de ellos tome un camino de tipo “primero-Y-luego-X” (por ejemplo, m2 y m4 en la figura anterior), lo que va en contra de la estrategia DOR. En resumen, si el encaminamiento de los paquetes es estático DOR, en una malla no tendremos paquetes bloqueados. En la figura se ve cómo se rompe el deadlock al usar encaminamiento estático DOR.

Page 56: 6.1 INTRODUCCIÓN

▪ 224 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

6.5.7.1.2 Pero no es suficiente

Aunque facilita las cosas, el encaminamiento estático no asegura la no existencia de deadlock en cualquier topología, por ejemplo, en un toro. Los ciclos de paquetes pueden formarse no porque los paquetes escogen caminos inadecuados, sino por los propios ciclos que presenta la topología de la red. En un anillo, por ejemplo, tenemos un ciclo de paquetes simplemente con que todos ellos decidan ir a la derecha:

La misma situación se nos presentan en toros de dos o tres dimensiones, o

topologías similares. Para estas topologías necesitamos, por tanto, una estrategia que no genere bloqueos.

6.5.7.1.3 Canales virtuales

Los enlaces entre encaminadores de mensajes unen un puerto de salida con un puerto de entrada. Ese enlace físico podría ser multiplexado (compartido) por varias salidas, tal como aparece en la figura. De esa manera, disponemos de varios canales virtuales que comparten (se reparten) el uso de un único canal físico. Como se ve en la figura, para formar un

(0,0) (0,3)

(3,0) (3,3)

m1: 0,1 → 2,3

m2: 1,3 → 3,1

m3: 3,2 → 1,1

m4: 2,1 → 0,2

0

3

1

2

0→2 1→3

2→0 3→1

m0 m1

m2 m3

Page 57: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 225 ▪

canal virtual basta con añadir más búferes a los encaminadores (o distribuir los que hay en subconjuntos).

Los canales virtuales pueden utilizarse para mejorar la eficiencia de la

comunicación. Por ejemplo, si un paquete no puede seguir adelante se bloquea el correspondiente canal virtual, pero no el canal físico, con lo que otros paquetes pueden seguir avanzando en la red (utilizando otro subconjunto de búferes).

Además de eso, los canales virtuales pueden utilizarse para evitar interbloqueos. Ya sabemos que el deadlock aparece si tenemos un conjunto de paquetes que forman un ciclo y que quieren utilizar los recursos que ocupa el siguiente. Esos ciclos de paquetes pueden romperse si se utilizan recursos diferentes para los paquetes en función del camino que están recorriendo. Por ejemplo, en un anillo: los paquetes utilizan el canal virtual 0 en su recorrido salvo que utilicen el enlace que cierra el anillo (el que une el procesador 0 con el N-1), en cuyo caso pasan a utilizar el canal virtual 1.

Así, por ejemplo, podemos hacer desaparecer el bloqueo del ejemplo anterior: los paquetes m0 y m1 utilizan el canal virtual 0; el paquete m2 utiliza al principio el canal 0, y luego el 1; y, finalmente, el paquete m3 usa el canal 1: lo que antes era un ciclo ahora es una “espiral”. De esa manera se rompe el posible ciclo de paquetes, y los paquetes pueden avanzar (en la medida en que lo haga el primero de ellos).

puerto de salida

puerto de entrada

canal o enlace entre encaminadores

dos canales virtuales sobre un canal físico

CV0

CV1

m0: 0→2 m1: 1→3

m2: 2→0

m3: 3→1

3

cv0

cv1

2

0

1

Page 58: 6.1 INTRODUCCIÓN

▪ 226 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

En el caso de un toro 2D basta con definir 2 canales virtuales para evitar el bloqueo si el encaminamiento es estático. Si se quiere utilizar encaminamiento dinámico, entonces se puede demostrar que es necesario utilizar dos canales virtuales en una malla y cuatro en un toro. Recuerda que un canal virtual no es sino un subconjunto de búferes al que se le aplica una política específica de asignación de paquetes.

En general, para utilizar encaminamiento adaptativo se organizan los recursos de la red (búferes) en canales virtuales y se gestionan de manera adecuada. En todo caso, conviene recordar que el uso de canales virtuales va a tener efecto en el tiempo de proceso de los paquetes en los encaminadores, por lo que siempre es necesario valorar lo que se va a perder (real) y lo que se va a ganar (hipotético). Veamos algunos ejemplos más del uso de canales virtuales.

6.5.7.1.4 Mallas virtuales

Existen diferentes técnicas de evitar bloqueos que se basan, de una manera u otra, en el uso de canales virtuales. Por ejemplo, las mallas virtuales. Para el caso de una topología en malla 2D, cada canal físico se divide en dos canales virtuales (dos grupos de búferes), y los paquetes se clasifican en cuatro categorías, en función del recorrido a realizar: NE, ES, SW y WN (noreste, sureste, suroeste, y noroeste). Cada clase de paquetes sólo va a utilizar dos de los 8 canales virtuales de cada encaminador (una "malla virtual") —por ejemplo, los paquetes de la clase NE solamente el canal N1 y el E0— y utilizando dichos canales virtuales pueden seguir el camino que deseen, puesto que no es posible que se forme un ciclo y, por tanto, que se produzca una situación de deadlock.

6.5.7.1.5 Giros controlados (Turn model)

En una malla 2D, para formar un ciclo de paquetes necesitamos paquetes que giren en los cuatro sentidos, tal como vemos en la siguiente figura (lo

N0

E0 E1

N1 malla virtual NE

S1 S0

W1 W0

Page 59: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 227 ▪

mismo en sentido contrario). Cuando se utiliza encaminamiento DOR, dos de esos cuatro giros están prohibidos (N→E y S→W). Por eso no es posible que se forme el ciclo y se bloqueen los paquetes.

DOR Turn model Pero para evitar la formación del ciclo bastaría con prohibir uno solo de

los giros; por ejemplo, S→W. Así los paquetes avanzarían por la red tal como quisieran, pero sin poder efectuar nunca un giro al oeste, tanto desde el sur como desde el norte. En consecuencia, si un paquete debe ir hacia el oeste para llegar a su destino, ése es el camino que deberá recorrer al comienzo, ya que no podrá girar más tarde en esa dirección. Esta técnica se conoce con el nombre de west-first (estrategias similares pueden hacerse con el resto de los giros).

Usando esta técnica se puede utilizar encaminamiento adaptativo en una malla 2D, aunque de manera desequilibrada: algunos paquetes tomarán el camino que quieran, pero otros están obligados a seguir un camino fijo, primero-X-luego-Y.

6.5.7.1.6 Control de la inyección de paquetes

<Para que se produzca un deadlock es condición necesaria que se agoten los recursos (búferes para recoger paquetes) en un grupo de encaminadores (y que se forme un ciclo con paquetes que quieren utilizar dichos recursos). Eliminaremos la posibilidad de deadlocks si, bajo ciertas condiciones y de acuerdo a determinadas prioridades, no aceptamos un paquete en el encaminador si con ello se llenan los búferes e, hipotéticamente, pudiera formarse un ciclo de paquetes (dejamos una "burbuja" para que puedan avanzar algunos paquetes y liberar con ello recursos de la red). En general, tendrán preferencia los paquetes que ya están en la red frente a los que se desea inyectar, con lo que se intenta controlar de alguna manera una posible "inundación" de paquetes. Una técnica de este estilo se utiliza, por ejemplo, en el encaminador de mensajes del clúster Mare Nostrum.

N-E E-S

S-W W-N

N-E E-S

S-W W-N

N-E E-S

S-W W-N

Page 60: 6.1 INTRODUCCIÓN

▪ 228 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

6.5.7.1.7 Utilización de caminos seguros

Las técnicas que acabamos de comentar para tratar el problema de los bloqueos están basadas en proponer un encaminamiento que esté libre de deadlock, para lo que se restringe la posibilidad de elegir caminos para llegar a destino. Como ya hemos comentado, existe una segunda manera de enfrentar el problema: no se asegura que no se produzcan los bloqueos, pero se detecta su aparición y se les da una solución en ese momento.

Detectar que algunos paquetes están bloqueados en la red no es tarea sencilla, ya que no disponemos de un supervisor global del tráfico, por lo que tenemos que utilizar información local. Una posibilidad de detección del bloqueo es utilizar temporizadores. Cuando un paquete se detiene se pone en marcha un temporizador; si tras pasar un tiempo umbral prefijado el paquete no ha avanzado, se decide que existe un posible bloqueo; se trata de una hipótesis, porque en realidad no sabemos si simplemente está detenido porque hay una gran congestión de tráfico.

Veamos un ejemplo en una malla 2D. Definimos dos canales virtuales, A y B, y con ellos dos mallas virtuales: la que utiliza el canal virtual A y la que utiliza el B. En la malla virtual A se admite el encaminamiento adaptativo, pero en la malla virtual B sólo se utiliza encaminamiento seguro tipo DOR. Los paquetes se inyectan en la red en la malla virtual A, en la que avanzan sin ninguna restricción, utilizando el camino que prefieran. Cuando un paquete se detiene en un encaminador, se pone en marcha un temporizador. Si se supera el tiempo máximo establecido sin avanzar, el paquete se reinyecta en la malla virtual B, es decir, pasa a avanzar utilizando los búferes tipo B. A partir de ese momento el paquete seguirá un estricto camino DOR, que asegura que en una malla se llega al destino.

La idea en la que se basa esta estrategia es que los bloqueos no ocurren muy a menudo (en hipótesis), por lo que la mayor parte de los paquetes llegará a su destino sin problemas. Solamente los paquetes que sufran un supuesto bloqueo deberán restringirse a utilizar caminos seguros. Como siempre, quedan por resolver problemas tales como la definición del tiempo umbral máximo de espera, la implementación de los contadores, etc. Y no olvidar nunca el análisis de costes reales y beneficios hipotéticos.

6.5.7.1.8 Otras posibilidades

Otra técnica conocida (para el caso SF) es la denominada structured buffer pool, en la que se estructura el uso de los recursos en función de la distancia al destino. Por ejemplo, un paquete que va a un nodo a distancia 3

Page 61: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 229 ▪

utiliza búferes de la clase 3; si va a distancia 2, de la clase 2; etc.; y según avanza en la red va cambiando de búferes. De esta manera el uso de los recursos forma una cadena, d → d–1 → d–2..., que, por definición, está libre de ciclos.

Existen más soluciones para casos particulares. Por ejemplo, si el diámetro de la red es menor que la longitud de los paquetes, y se utiliza WH, cuando la cabecera del paquete alcanza el destino la cola del mismo aún está en el emisor. Utilizando una estrategia ya comentada, los temporizadores, el propio emisor puede detectar que el paquete no avanza, decidir que está bloqueado, y, por ejemplo, retirarlo de la red para intentar su retransmisión más tarde, en “hipotéticas” mejores condiciones (las latencias podrían llegar a ser muy grandes). En cualquier caso, si se decide retirar un paquete de la red, el emisor debe ser informado de ello, para no que la información no se pierda.

6.5.7.2 Problemas de livelock y starvation

Existe otro tipo de problemas a resolver relacionados con la comunicación en la red, aunque de menor importancia que el que hemos tratado. El problema de livelock aparece en la red si los paquetes se mueven, no están parados, pero nunca consiguen llegar al destino. Por ejemplo, este problema puede aparecer si utilizamos una estrategia de control de flujo que ya hemos citado: si no hay sitio para recibir un paquete se “echa fuera” del encaminador —se desvía de su camino— un paquete que hemos recogido antes. A continuación, el paquete que acabamos de desviar vuelve a solicitar sitio en el encaminador, y desviamos al paquete que acabamos de recoger. Es decir, moverse se mueven, pero no van a ninguna parte.

Otro problema que puede aparecer en la gestión de la red de comunicación es el de starvation (hambruna). La red de comunicación y los protocolos correspondientes deben tratar a todos los procesadores de la misma manera. Salvo por cuestiones de prioridades, que tienen que estar bien explícitas, no es de recibo que un procesador siempre pueda inyectar sus mensajes en la red mientras que otro nunca pueda hacerlo. Por ejemplo, si existe una zona de la red en la que se concentra el tráfico (tal vez en el centro de una malla si todos los mensajes van hacia el centro), hay que asegurar que los procesadores de esa zona de la red puedan inyectar sus mensajes y no estén siempre a la espera de encontrar un hueco.

Page 62: 6.1 INTRODUCCIÓN

▪ 230 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

6.5.8 Protocolos de comunicación

El proceso completo de paso de información entre dos procesos que se ejecutan en procesadores diferentes, además de la transmisión física en la red de comunicación, implica una serie de procedimientos a ejecutar tanto en el nodo emisor como en el receptor, procedimientos que no podemos desdeñar, ya que bien pudiera ocurrir que la latencia de los mismos llegara a superar a la de la propia transmisión física de los mensajes. Aunque no vamos a efectuar un análisis detallado de los protocolos implicados en esos procesos, vamos a acabar este capítulo haciendo un pequeño resumen de los mismos.

Protocolos estándar en el mundo de las redes de computadores, tales como TCP/IP, no resultan adecuados para los sistemas MPP (aunque se usan en clusters de bajo nivel). La implementación habitual de los mismos hace entrar en juego al sistema operativo del nodo emisor, para efectuar una copia del mensaje a transmitir de la memoria de usuario a la del propio sistema operativo. El mismo proceso se repite en el nodo receptor.

El overhead añadido por estas intervenciones del sistema operativo puede

ser aceptable si la velocidad de transmisión es baja (100 Mb/s), pero no lo es cuando la velocidad es alta (10 Gb/s).

En los últimos tiempos se han desarrollado protocolos más eficientes para ser usados tanto en sistemas MPP de diseño específico como en clusters de procesadores. El objetivo de estos protocolos es reducir al máximo el coste (tiempo) de generación y recepción de los paquetes que transmite la red, para lo que se eliminan las copias en memoria del S.O. (protocolos de 0 copias) y se gestiona la generación y la recepción de paquetes de manera muy eficiente. Dos de las alternativas más conocidas son VIA (Virtual Interface Architecture) e InfiniBand. Las redes comerciales estándar suelen traer implementados de forma nativa (más rápido) estos protocolos o los soportan mediante emulación (más lento). Además de ello, en otros casos —Myrinet, por ejemplo— se utilizan protocolos de comunicación propios (GM).

memoria de usuario

memoria de usuario

copia en memoria del sistema

copia en memoria del sistema

interrup. SO interrup. SO

transmisión en la red

Page 63: 6.1 INTRODUCCIÓN

6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 231 ▪

Aunque hay un poco de todo, aparte de las redes de diseño específico (propias de los sistemas MPP de alta gama), las redes más utilizadas en los clusters son Gigabit Ethernet por un lado, barata pero de no muy alto rendimiento (por ejemplo, la latencia de paquetes pequeños con MPI anda en torno a los 50 µs o más), e InfiniBand y Myrinet por otro (más rápidas que la primera, pero también más caras).

InfiniBand es una infraestructura y protocolo de comunicaciones de alto rendimiento; la figura representa un nodo general de un sistema paralelo que utiliza InfiniBand.

Los elementos que forman el nodo pueden ser muy variados: en el caso

más simple, un solo procesador, y en el más complejo, uno o varios sistemas paralelos (SMP), sitios específicos de entrada/salida o de almacenamiento masivo de datos (RAID), etc. La comunicación entre los elementos que forman el nodo se realiza mediante conmutadores, mientras que la comunicación entre nos se realiza mediante encaminadores de mensajes.

Myrinet es otro referente en las redes de alta velocidad. Los nodos se conectan a la red mediante un NIC (network interface card) de diseño específico que ejecutan el protocolo de comunicación (GM); así, el procesador del nodo solamente ejecutará "cálculo", y el de la tarjeta de comunicaciones se encargará de todo lo relacionado con la comunicación

Page 64: 6.1 INTRODUCCIÓN

▪ 232 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

(además, el usuario puede programar las funciones de la tarjeta de comunicaciones, para adaptarla a necesidades concretas). En general, los nodos se conectan en una red de Clos (fat tree), tal como la de la figura (64 nodos). Los enlaces son de 10 + 10 Gb/s y la latencia de paquetes pequeños con MPI llega a estar por debajo de los 2 µs

enlaces para el siguiente nivel del árbol

Red de Clos de 64 nodos (fat tree). Los conmutadores son de 8 + 8 puertos (enlaces bidireccionales).

6.6 EVOLUCIÓN DE LOS COMPUTADORES

PARALELOS

Los intentos de utilización de paralelismo para mejorar la velocidad de cálculo son tan antiguos casi como la propia historia de los computadores. Los prototipos experimentales y las máquinas comerciales han sido tan abundantes como a menudo efímeros. Comentaremos nada más algunos de los más relevantes. Los primeros computadores paralelos fueron en gran medida experimentos universitarios. Entre los pioneros, uno de los más conocidos es el ILLIAC IV (University of Illinois, 1968). Era una máquina de tipo SIMD, que explotaba el paralelismo de datos. Tenía 64 procesadores unidos en una red de características similares a las de un toro 2D. Otras máquinas de éxito con el mismo modelo de cómputo han sido el DAP (1980) —procesadores muy simples de 1 bit, conectados en una malla 2D, que admitía hasta un máximo teórico de 16 k procesadores— y los computadores de Thinking Machines CM-1 y CM-2 (1985) —una estructura similar a la anterior, pero formando hipercubos, hasta 64 k procesadores—.

Pasando al modelo MIMD, entre los multiprocesadores de memoria compartida (SMP) está el NYU Ultracomputer (New York Univ., 1983), que utilizaba como red de comunicación una red Omega. Otros ejemplos en la

Page 65: 6.1 INTRODUCCIÓN

6.6 EVOLUCIÓN DE LOS COMPUTADORES PARALELOS ▪ 233 ▪

misma línea son Balance (1985) y Symmetry (1990) —30 procesadores i386—, ambos de la casa Sequent, o la BBN TC2000 (1989) —512 procesadores MC88000—. En ese tipo de sistemas la red de comunicación más habitual ha sido el bus o una red multietapa. Aunque en todos los casos la intención era utilizar un número elevado de procesadores, en la realidad se limitaron siempre a un número reducido; sin embargo, la experiencia acumulada con el diseño y el uso de ese tipo de máquinas ha hecho que hoy en día casi todos los sistemas “monoprocesador” sean en realidad pequeños multiprocesadores de 2-4 procesadores conectados en un bus común.

Los sistemas multicomputador (MPP) han seguido una evolución similar. Una de las primeras máquinas de este tipo fue el Cosmic Cube —con intención de unir en un hipercubo de 6 dimensiones 64 procesadores i8086, con comunicación SF—, diseñado en los laboratorios de CalTech en 1980. A partir de esa máquina evolucionaron posteriormente las máquinas de la familia iPSC (Intel Personal SuperComputer): los hipercubos iPSC1 e iPSC2 (este último, con conmutación de circuitos). La última versión de esa serie, Paragon, permite conectar hasta .048 procesadores i860 en una malla de 2 dimensiones (varias versiones de esa máquina, con 540 procesadores, funcionaron con gran éxito). Aunque lo habitual es que todas estas máquinas tengan su origen en los Estados Unidos, también ha habido desarrollos equivalentes en Europa. El más conocido, sin duda es el SuperNode de Parsys, basado en procesadores Transputer (1985 - 1990).

El desarrollo de este tipo de arquitecturas es cada vez mayor: el T3D de CRAY (un toro de 3 dimensiones, con 128 - 2048 procesadores Alpha, 4 canales virtuales, WH), el SP2 de IBM (128 procesadores en una red multietapa), el SPPA de Convex y el VPP500 de Fujitsu (usando un crossbar), la J-Machine MIT, la CM-5 de Thinking Machines..., y las máquinas de la serie ASCII, con las que se logró por vez primera una velocidad de cálculo de 1 Teraflop/s. Muchos de estos ejemplos han tenido una vida comercial de cierto éxito, aunque ya no estén en vigor. En los últimos años han aparecido supercomputadores como el Earth Simulator o el Blue Gene, que han conseguido velocidades de cálculo enormes (en estos momentos por encima de los 200 TF/s).

Por su parte, entre las arquitecturas de memoria compartida distribuida podemos citar el DASH de Stanford (en su primera versión, 64 MIPS RS3000). El esquema de conexión es jerárquico, en 16 grupos de 4 procesadores. Dentro de cada grupo, los 4 procesadores se conectan en un bus común, y los grupos se conectan formando una malla 2D. Este tipo de

Page 66: 6.1 INTRODUCCIÓN

▪ 234 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

máquina evolucionó posteriormente hacia las máquinas de la serie Origin, que analizaremos en el próximo capítulo.

En lo que a la topología del sistema se refiere, las últimas máquinas son todas mallas, toros o árboles; por ejemplo, el Cray XT3 (30508 pr.) o el IBM Blue Gene/L (65536 × 2 pr.) son toros de tres dimensiones, y el Thunder de Intel (1024 × 4 pr.) o el IBM ASC Purple (1280 × 8 pr.) son árboles hechos mediante conmutadores de ocho puertos.

No podemos dejar de lado las arquitecturas con más empuje, los clusters, tanto los de alto nivel —Roadrunner, Mare Nostrum...—, como los más modestos, habituales ya en los centros de cálculo de universidades, laboratorios y empresas, debido a su buena relación coste/eficiencia, formados por un número relativamente alto de nodos de cómputo (64 – 1024) unidos por algún tipo de red de comunicación de alto rendimiento (InfiniBand, Myrinet, Quadrics) o simplemente por Gigabit Ethernet.

Es habitual agrupar el desarrollo de las máquinas paralelas en “generaciones”; estaríamos en la tercera (con los clusters, tal vez en la cuarta) generación de las mismas. Inicialmente se utilizó store-and-forward e hipercubos, pero hoy en día se han impuesto tanto wormhole/cut-through como las topologías en malla, toro, árboles y similares. Es cada vez más difícil hacer una clasificación clara y sencilla de estas máquinas, ya que en muchos casos utilizan características de varios tipos para conseguir la mayor eficiencia posible (en muchos casos, estructuras jerárquicas que utilizan diferentes modelos en cada nivel de la jerarquía).

Por otra parte, se han establecido ya dos estándares de facto para programar estos sistemas paralelos. Por un lado, OpenMP, para el caso de aplicaciones de memoria compartida; y, por otra parte, MPI, para la programación de aplicaciones mediante paso de mensajes. Igualmente, cada vez están más desarrolladas herramientas complementarias para la gestión de estos sistemas que permiten observar la máquina como una unidad, efectuar debugging, profiling, etc.

En el último capítulo haremos un breve resumen de las máquinas paralelas más rápidas del mundo y la evolución de las mismas, así como de las herramientas para programar aplicaciones paralelas.

Page 67: 6.1 INTRODUCCIÓN

APÉNDICE. Cálculo de las distancias medias en diferentes topologías. ▪ 235 ▪

APÉNDICE. Cálculo de las distancias medias en diferentes topologías.

Como complemento al capítulo, veamos cómo se calcula la distancia media en diferentes topologías. La distancia media de una red de P procesadores se define como:

)1(1,

,

−=∑=

PP

dd

P

jiji

es decir, la suma de distancias entre parejas de procesadores dividida por el número de parejas. En la definición no se considera la comunicación de un procesador consigo mismo. Si queremos tomar en cuenta también esa posibilidad, la suma de las distancias habría que dividirla por P × P (a este segundo caso le vamos a llamar distancia media topológica).

Para calcular las distancias medias hay que efectuar algunos sumatorios. Éstos son los resultados de los más habituales:

→ serie aritmética: nnin

i 21

1

+=∑

=

→ serie cuadrática )12)(1(61

0

2 ++=∑=

nnnin

i

→ serie exponencial 1

)1(

1 −−

=∑= k

kkknn

i

i 2

1

1 )1()1)1((

+−−=

+

=∑ k

kkknki

nn

i

i

→ serie binomial nn

i in

20

=

∑=

A partir de estos resultados no es difícil calcular las distancias medias de las redes que hemos estudiado: hipercubos, mallas, toros y árboles.

A1. Hipercubos (n dimensiones, 2n procesadores)

El hipercubo es una red simétrica. Basta por tanto con calcular las distancias desde un nodo cualquiera. Los nodos del hipercubo se etiquetan de esta manera: (xn-1, xn-2, ..., x1, x0); siendo la coordenada en cada dimensión 1 o 0.

Page 68: 6.1 INTRODUCCIÓN

▪ 236 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

En esta topología, cada nodo está conectado con n nodos, que se diferencian en un bit en las coordenadas. Por ejemplo, el nodo (0,0,0,0) está conectado con los nodos (1,0,0,0), (0,1,0,0), (0,0,1,0), (0,0,0,1).

Por tanto, a distancia d = 1 hay n nodos,

1n en concreto; a distancia d = 2,

2n ; etc. Así pues, la suma de distancias desde un procesador es:

1

1

2...02

=

=

++

=

∑ nn

i

nnnnni

in

y la distancia media:

2122 1 nnd n

n

→−

=−

Cuando el número de procesadores es elevado, la distancia media de un hipercubo se aproxima a n/2 (la mitad del número de dimensiones).

A2. Mallas (n dimensiones, k procesadores por dimensión)

En el caso de las mallas (cadenas, mallas 2D, 3D, etc.), es útil calcular primeramente la distancia media topológica de la red; es decir, dividir la suma de distancias por el número total de parejas, incluida la pareja [i → i] (si P = k, k × k parejas). La distancia media topológica de una red de n dimensiones es n veces la de una dimensión. A partir de ahí, es sencillo obtener la distancia media real de la red en cuestión. Tenemos que calcular, por tanto, la distancia media topológica de una cadena.

Partiendo del procesador i, la suma de distancias al resto de procesadores

resulta ser:

2)1()1()1(

221 2

1

1

1

+++−=−−

−+

+=+∑ ∑

=

−−

=

kkkiiikikiijji

j

ik

j

0 i–1 i i+1 k–2 k–1

1 i–1

i

1 k–2–i

k–1–i

1

Page 69: 6.1 INTRODUCCIÓN

APÉNDICE. Cálculo de las distancias medias en diferentes topologías. ▪ 237 ▪

La red no es simétrica, por lo que la suma de distancias cambia en función del nodo de partida elegido. Por tanto, la suma de todas las distancias partiendo de cualquier nodo es:

)1(3

12

)1(2

1)1()12()1(61)

2)1()1((

1

0

2 −+

=+

+−

+−−−=+

++−∑−

=

kkkkkkkkkkkkkkkiik

i

Para calcular la distancia media topológica, basta dividir por el número de

caminos sumados (k2):

kkk

kkkkdt 3

)1)(1(3

)1()1(2

−+=

−+=

Como hemos comentado, la distancia media topológica de una malla de n dimensiones es n veces la de una cadena. A partir de ese valor, podemos obtener la distancia media que nos interesa (la de la “comunicación”, que no toma en cuenta la comunicación de distancia 0) efectuando una ligera corrección: multiplicando por P2 / [P(P–1)] = P / (P–1).

Si tenemos una malla de k procesadores por dimensión, y, por tanto, P = kn, entonces la distancia media de la misma resulta ser:

3)1(3)1)(1( kn

kk

kkknd n

n

→−

−+= (para muchos procesadores)

En particular,

n = 1 (cadena) n = 2 (malla 2D) n = 3 (malla 3D)

331 kk→

+ 3

2k kkk

kk→

+++

1)1(

2

2

En resumen, cuando el número de procesadores es elevado, la distancia media de una malla se aproxima a n × k / 3.

Page 70: 6.1 INTRODUCCIÓN

▪ 238 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS COMPUTADORES PARALELOS

A3. Toros (n dimensiones, k procesadores por dimensión)

Los toros (anillos) y las mallas son topologías de características similares. El análisis del toro es sin embargo más sencillo, ya que se trata de una topología simétrica, por lo que basta analizar la suma de distancias partiendo de un nodo cualquiera. Como en el caso anterior, es conveniente analizar primeramente el caso de una dimensión, un anillo, y generalizarlo luego a cualquier número de dimensiones.

Aunque la red es simétrica, los resultados son un poco diferentes en función de si el número de procesadores del anillo es par o impar. Vamos a suponer que P (o k) es un número par.

Partiendo de un procesador, la suma de distancias resulta ser:

422

212

1

kkj

k

j=+∑

=

Por tanto, y siguiendo la misma estrategia que en el caso de las mallas, el resultado para n dimensiones (n veces el de una dimensión, junto con la corrección para eliminar los casos de distancia 0) es:

41414

2 knk

kknk

kk

knd n

n

n

n→

−=

−= (para muchos procesadores)

Por ejemplo,

n = 1 (anillo) n = 2 (toro 2D) n = 3 (toro 3D)

4)1(4

2 kkk

→−

2)1(2 2

3 kkk

→−

4

3)1(4

33

4 kk

k→

Cuando el número de procesadores es elevado, la distancia media de un toro se aproxima a n × k / 4.

k–2

k–1

0

1

2

k/2

1 1

2 2

k/2

Page 71: 6.1 INTRODUCCIÓN

APÉNDICE. Cálculo de las distancias medias en diferentes topologías. ▪ 239 ▪

A4. Árboles (procesadores en las hojas)

Un árbol es una estructura simétrica. Si el grado del árbol es k, partiendo de un nodo tenemos k–1 nodos a distancia 2; k(k–1), a distancia 4; k2(k–1), a distancia 6; etc. (si el árbol es binario, 1 a distancia 2; 2 a distancia 4; 4 a distancia 6...).

La suma de distancias es, por tanto:

1)1(2lg2)1(2)1(2

lg

1

lg

1

1

−−

−=−

=− ∑∑==

kPPPki

kkkki k

P

i

iP

i

ikk

y la distancia media:

12lg

12

−−

−=

kP

PPd k

Por ejemplo (P grande),

k = 2 (binario) k = 4

2lg2 2 −= Pd 32lg2 4 −= Pd

procesadores

encaminadores

Page 72: 6.1 INTRODUCCIÓN
Page 73: 6.1 INTRODUCCIÓN

▪ 7 ▪

Coherencia de los Datos en los Computadores DSM

7.1 INTRODUCCIÓN

Como ya sabemos, la coherencia de los datos es uno de los principales problemas que hay que resolver en los sistemas paralelos de memoria compartida. En la cache de cada procesador se van a cargar copias de los datos compartidos, pero, dado que el espacio de direccionamiento de la memoria es único y común, todos los procesadores deben tener la misma imagen del sistema de memoria. Por tanto, hay que mantener coherentes (“iguales entre sí”) todas las copias de los datos. Ya hemos analizado cómo se resuelve este problema en los multiprocesadores SMP, sistemas de memoria compartida que utilizan un bus para comunicar procesadores y memoria. Todos los procesadores comparten la infraestructura de comunicación "centralizada", el bus, a través del cual se realizan todas las operaciones de memoria, por lo que basta “espiar” las transacciones que se

Page 74: 6.1 INTRODUCCIÓN

▪ 242 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

efectúan en el bus para enterarse de las operaciones que realiza el resto de los procesadores, así como utilizar el bus para informar al resto de procesadores de una determinada operación. Cada procesador utiliza un dispositivo especial de control para mantener la coherencia de los datos, el snoopy, que se dedica a espiar el bus y a enviar información al resto de controladores. La información que se obtiene de esa manera permite adecuar el estado de los bloques de datos de la cache, para asegurar la coherencia de los datos en todo el sistema.

Sin embargo, en los sistemas de memoria compartida pero físicamente distribuida (DSM), en los que la red de comunicación es descentralizada (malla, toro, hipercubo...), no es posible aplicar la misma estrategia para mantener la coherencia de los datos. Por ejemplo, si los procesadores están conectados mediante una malla 2D, ¿cómo sabrá el procesador (0, 0) que el procesador (4, 3) acaba de cargar en su cache un bloque que él tiene en estado E, y que, por tanto, tiene que cambiarlo a estado S? O, ¿cómo sabrá el procesador (2, 1) que el bloque que necesita se halla en otro procesador en estado M, y que no puede utilizar la copia que está en memoria principal? En este capítulo vamos a analizar cómo se resuelve el problema de la coherencia en los sistemas de tipo DSM (Distributed Shared Memory).

Pero, antes de continuar, planteemos una cuestión más. ¿Es necesario mantener la coherencia de los datos mediante hardware? Algunas máquinas no lo hacen, y dejan que el problema se resuelva mediante software. Recuerda que es necesario asegurar la coherencia de los datos sólo cuando éstos son compartidos. Por ejemplo, para ejecutar A(i) = A(i) + 1 con los elementos de un vector grande no es necesario el hardware de coherencia, porque, si se realiza un reparto adecuado de los datos, éstos no se van a compartir. Además, siempre tenemos la posibilidad de no llevar a la cache los datos compartidos. A los sistemas que no aseguran la coherencia de las caches se les denomina NUMA (Non-Uniform Memory Access) (por ejemplo, el T3D de la casa Cray); los computadores que garantizan la coherencia de los datos mediante hardware se denominan cc-NUMA (Cache Coherent NUMA). Por supuesto, desde el punto de vista del rendimiento, nos interesan estos últimos.

Cada vez más, los sistemas de muchos procesadores se organizan de manera jerárquica: los nodos que se conectan mediante una red estática no son simplemente procesadores, sino pequeños sistemas multiprocesador tipo SMP (4-8 procesadores conectados mediante un bus). No hay que olvidar que este tipo de arquitecturas, las basadas en un bus, se conocen muy bien, y

Page 75: 6.1 INTRODUCCIÓN

7.2 DIRECTORIOS DE COHERENCIA ▪ 243 ▪

la coherencia se puede mantener de forma relativamente sencilla (snoopy), por lo que se pueden utilizar tarjetas de 4-8 procesadores como elementos básicos para construir sistemas paralelos más grandes. De esa manera, la coherencia de los datos se mantiene también jerárquicamente: dentro de cada nodo, mediante el snoopy local, pero entre los nodos del sistema... ¿qué hay que hacer para anular las copias de un bloque? ¿dónde están dichas copias? ¿de dónde hay que traer un bloque de datos, de memoria principal o de otra cache? ¿de cuál? ¿en qué estado hay que cargar el bloque?

La solución general al problema que plantean las preguntas anteriores son los directorios de coherencia. En este tipo de directorios se guarda información sobre los bloques de datos que se encuentran en las caches de los procesadores y, antes de hacer cualquier operación, se debe consultar dicha información, para saber qué hay que hacer. Como es lógico, una estructura y funcionamiento adecuados de los directorios de coherencia es crucial para el sistema; en caso contrario, se corre el riesgo de que dicho dispositivo se convierta en el “cuello de botella” del sistema.

Antes de pasar a analizar la estructura, funcionamiento, etc. de los directorios de coherencia, un par de comentarios. Por un lado, aunque el sistema de memoria es común, lo esperable es que la mayoría de los accesos de cada procesador sean accesos al trozo de memoria local de cada uno. Por ejemplo, si el procesador Pi tiene que utilizar los datos que están en la memoria del procesador Pj, y viceversa, la ejecución será muy lenta. Mejor sería intercambiar los datos al comienzo, para que cada procesador pudiera trabajar con los datos localmente. Por otro lado, conviene recordar que la coherencia hay que mantenerla con las variables (bloques) que se comparten; normalmente, la mayoría de las variables que se utilizan en los procesos que se ejecutan en paralelo serán privadas, y solamente algunas de las variables serán compartidas. Dicho de otro modo, la mayoría de los bloques de datos estarán en los estados E o M, y no tomarán parte en las operaciones de mantenimiento de la coherencia.

7.2 DIRECTORIOS DE COHERENCIA

7.2.1 Introducción y clasificación

En el directorio de coherencia se almacena información de control sobre los bloques de memoria (dónde están y en qué estado). Cuando un

Page 76: 6.1 INTRODUCCIÓN

▪ 244 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

procesador quiere leer una palabra de la cache (en fallo) o quiere escribir, primero tiene que obtener información sobre dicha palabra (el bloque) en el directorio, para saber exactamente qué hacer: por ejemplo, de dónde debe traer el bloque, en qué estado lo debe poner, etcétera.

Uno de los primeros problemas a los que hay que hacer frente es: ¿dónde está el directorio de coherencia? Atendiendo a dónde están y cómo se estructuran, los directorios se pueden clasificar en:

• Centralizados. Existe un gran directorio centralizado, en el que se guarda una palabra asociada a cada uno de los bloques de memoria del sistema. Una estructura centralizada no es adecuada cuando el número de procesadores es elevado, menos aún cuando la propia memoria está distribuida entre todos los nodos, ya que se producirán gran cantidad de conflictos al usar el directorio y la latencia de esas operaciones será muy elevada.

• Jerárquicos. El directorio se divide en varios dispositivos, que se estructuran de manera jerárquica (por ejemplo, en un árbol). La jerarquía de buses que hemos visto en el capítulo 3 podría ser un ejemplo de este tipo de directorios. No se utilizan demasiado.

• Planos. El directorio de coherencia se encuentra distribuido entre todos los procesadores, como la memoria, sin formar una estructura jerárquica. En función de qué información de coherencia se guarda y dónde se guarda, tenemos dos tipos de directorios:

- directorios junto a MP: la información de coherencia de cada bloque de datos se guarda en una sola palabra, en el directorio junto a la MP, en el nodo correspondiente.

- directorios distribuidos en las caches: la información de coherencia se distribuye, junto con los datos, en las caches del sistema, formando listas ligadas.

Los directorios de las máquinas actuales son de uno de estos dos tipos.

El crecimiento del directorio de coherencia según aumenta el número de procesadores debería ser moderado, desde dos puntos de vista:

• Tamaño. El tamaño del directorio no debería crecer con el número de procesadores (por lo menos, no excesivamente). Tal y como veremos, el tamaño del directorio va a variar en función de las estructuras que se utilicen para guardar la información.

Page 77: 6.1 INTRODUCCIÓN

7.2 DIRECTORIOS DE COHERENCIA ▪ 245 ▪

• Ancho de banda. El uso del directorio no debe aumentar excesivamente la comunicación (cantidad de mensajes); hay que tener en cuenta que la red de comunicación se utiliza para comunicar los procesos, y los mensajes asociados a la coherencia no deberían consumir una parte importante del ancho de banda de la red. Además, desde el punto de vista de la latencia, los caminos críticos de los mensajes del protocolo de coherencia deben ser lo más cortos posible.

Por lo demás, los protocolos de coherencia mediante directorios se basan en las mismas ideas que ya conocemos: estados de los bloques (I, E, M, S, O); invalidar o actualizar todas las copias del bloque al modificar una de ellas (la actualización casi no se utiliza), etcétera. Por tanto, ¿en qué consiste la diferencia entre directorios y snoopy-s? Principalmente, en cómo se guarda la información asociada a la coherencia y en cómo se implementa la comunicación para mantener la coherencia.

7.2.1.1 Problemas

Para poder utilizar controladores de coherencia basados en directorios hay que resolver muchos problemas. Para empezar, el del tamaño del directorio: ¿cuánta memoria se necesita para almacenar la información del directorio de coherencia? ¿cómo crece ese tamaño con el número de procesadores? El directorio debería ser de tamaño “limitado” y, aún más importante, ese tamaño no debería crecer excesivamente según crece el sistema.

De todas maneras, y a pesar de que el tamaño es importante, el principal problema de los directorios de coherencia es otro: la falta de atomicidad de las operaciones de coherencia. La estructura de datos es distribuida y se utiliza de manera totalmente descentralizada, por lo que no es sencillo asegurar que la información del directorio esté siempre actualizada, o que no se intercalarán en el tiempo las operaciones de dos procesadores sobre el mismo bloque para producir resultados no previstos (carreras). Por ejemplo, en caso de una escritura en un procesador "remoto", hay que avisar al directorio para invalidar el resto de copias de dicho bloque, lo que va a llevar cierto tiempo. ¿Qué sucede si, mientras tanto, otro procesador tiene que utilizar el directorio de coherencia, porque quiere hacer una operación sobre ese bloque? ¿Qué información obtendrá? Este tipo de problemas son los más difíciles de solucionar cuando se utilizan directorios, ya que hay que asegurar que no se mezclan varias operaciones, que lleven a un mal funcionamiento del protocolo. Como vamos a ver a continuación, la

Page 78: 6.1 INTRODUCCIÓN

▪ 246 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

atomicidad se consigue utilizando estados transitorios y señales de control del tipo ACK y NACK, para aceptar o rechazar determinadas operaciones.

7.2.2 Estructura de los directorios

Comencemos analizando qué información se guarda en los directorios y cómo. Dos son los esquemas más utilizados. Por un lado, los directorios construidos junto a memoria principal. Y, por otro lado, los directorios implementados en memoria cache, que utilizan listas encadenadas.

7.2.2.1 Directorios implementados en memoria principal

Se dice que un directorio está implementado en memoria principal si la información de coherencia se almacena en un dispositivo junto a la memoria principal —el directorio—. Como la memoria principal está físicamente distribuida, el directorio de coherencia también está distribuido por todos los nodos del sistema paralelo. Para guardar la información sobre coherencia, las dos opciones más utilizadas son las denominadas full bit vector y limited bit vector.

7.2.2.1.1 Full bit vector (Censier & Feautrier, 1978)

El directorio de coherencia está formado por una palabra por cada bloque de datos de memoria principal, que guarda la información de control asociada a dicho bloque.

La información de coherencia correspondiente a un bloque de datos se guarda en una palabra de P+k bits: P bits, uno por procesador (o nodo), para indicar si el bloque está o no en la cache de cada procesador; y k bits más para indicar el estado del bloque. Por ejemplo, en un sistema de 4 procesadores y utilizando un solo bit para indicar el estado del bloque (M = 0/1, el bloque no ha cambiado/ha sido modificado), la palabra de coherencia correspondiente a un bloque podría ser:

P0 P1 P2 P3 M 1 0 0 0 0 el bloque está en P0, en estado E 1 1 0 0 0 el bloque está en P0 y P1, en estado S 0 1 0 0 1 el bloque está en P1, en estado M 0 0 0 0 0 el bloque no está en ninguna cache (I) 1 0 1 0 1 imposible (en un protocolo MESI)

Page 79: 6.1 INTRODUCCIÓN

7.2 DIRECTORIOS DE COHERENCIA ▪ 247 ▪

Cuando se utiliza una estructura de este tipo para implementar el directorio de coherencia, podemos tener un problema con el tamaño del directorio. Analicemos unos ejemplos (un bit para indicar el estado).

núm. de procesadores bloque

bits/bloque en el directorio

memoria a añadir

64 64 bytes → 65 (8 bytes) 12,5% 256 64 bytes → 257 (32 bytes) 50%

1024 64 bytes → 1025 (128 bytes) 200% !!

Como aparece en la tabla, el tamaño de este tipo de directorios crece demasiado (linealmente) con el número de procesadores, llegando a ser inasumible. Convendría encontrar alguna otra solución.

Para reducir el tamaño del directorio, podemos seguir varios caminos: • Hacer que los bloques de datos sean más grandes y, como

consecuencia, tener menos bloques. Teniendo en cuenta que se añade una palabra de coherencia por cada bloque, si el número total de bloques es menor, el directorio también lo será. Sin embargo, ya sabemos que no es conveniente utilizar bloques demasiado grandes, porque se acentúa el efecto de la "polución de la cache": llevar a cache mucha información que no se va a utilizar (los bloque de datos más habituales son de 64 o 128 bytes).

• Reducir el número de nodos de la red, colocando en cada nodo un sistema SMP completo (por ejemplo, 4 procesadores / bus / snoopy) en lugar de un simple procesador. En lugar de tener un procesador en cada nodo, se utiliza un sistema SMP completo. De esta manera se construye una jerarquía de redes: el directorio de coherencia se limitará a controlar los nodos de la red, y dentro de cada nodo la coherencia se gestionará mediante el snoopy local. Por tanto, el tamaño de la palabra con la información de coherencia será menor.

Por ejemplo, combinando las dos estrategias anteriores, tendríamos los siguientes tamaños para un directorio full bit vector:

núm. de procesadores bloque

bits/bloque en el directorio

memoria a añadir

256 64 bytes → 257 (32 bytes) 50% 64 × 4 pr./nodo 128 bytes → 65 (8 bytes) 6%

A pesar de haberse reducido algo el tamaño, el directorio sigue creciendo linealmente con el número de procesadores del sistema.

Page 80: 6.1 INTRODUCCIÓN

▪ 248 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

7.2.2.1.2 Limited bit vector

Otra alternativa para reducir el tamaño del directorio consiste en guardar menos información sobre cada bloque de datos. En muchas aplicaciones, a pesar de utilizar muchos procesadores (procesos), el número de copias de las variables que se comparten no es muy alto (por ejemplo, 2-4 en tratamiento de imagen); en otros muchos casos, aunque el número de copias sea mayor, no se utilizan todas al mismo tiempo (L1 S1 ... L2 S2 ... L3 S3 ...). En general, se ha comprobado estadísticamente que el número de copias de las variables compartidas no crece demasiado con el número de procesadores.

En el apartado anterior hemos utilizado un bit por procesador para indicar dónde están las copias de un determinado bloque; es decir, admitimos que pueda haber copias del mismo en todos los procesadores (todos los bits a 1). ¿Es eso realista? Seguramente no. Por tanto, ¿por qué no limitar el número de copias que puede haber de un bloque? Si hiciéramos eso, sería suficiente con guardar en la palabra de coherencia las direcciones de los procesadores que tienen una copia del bloque, en lugar de guardar P bits.

Debemos de tomar una serie de decisiones. Por un lado, ¿cuántas copias permitimos? Por ejemplo, en un sistema MPP con 1024 procesadores, se necesitan 10 bits para representar una dirección; si admitiéramos hasta 100 copias de un bloque, necesitaríamos 1000 bits para almacenar las direcciones de los procesadores que tuvieran una copia del bloque, menos todavía que los 1024 bits que necesitamos para tener un bit por procesador. Además, normalmente es suficiente con muchas menos copias (5/10).

Con esta estrategia, el tamaño del directorio se reduce notablemente. Por ejemplo:

núm. de procesadores bloque k, máx. núm.

de copias bits/bloque en el

directorio memoria a

añadir 256: 64 × 4 128 bytes 5 → 5 × 6 + 1 = 31 3%

1024: 256 × 4 128 bytes 5 → 5 × 8 + 1 = 41 4%

En el primer caso del ejemplo, necesitamos 6 bits para indicar la dirección de un procesador (o nodo), y en el segundo 8 bits; como puede comprobarse en la tabla, aunque el número de procesadores se ha multiplicado por cuatro, el directorio crece muy poco.

Al usar direcciones, el tamaño del directorio crece logarítmicamente con el número de procesadores, ya que se utilizan k × log2 P bits en cada palabra de coherencia (más los bits de estado). En cualquier caso, el número de copias debe ser limitado, y siempre cumpliendo que: k × log2 P << P.

Page 81: 6.1 INTRODUCCIÓN

7.2 DIRECTORIOS DE COHERENCIA ▪ 249 ▪

Sin embargo, si limitamos el número de copias de un bloque, ¿qué sucede si una aplicación necesita utilizar más copias de las permitidas? Evidentemente, si en un momento dado una aplicación necesita superar ese umbral de copias, deberemos de optar por: (a) no admitir la petición: no se llevará el bloque a la cache y la operación (rd/wr) se hará en memoria principal; o (b) eliminar alguna de las copias del bloque (utilizando la estrategia de selección que se quiera) para poder cargar la nueva copia.

7.2.2.1.3 Un ejemplo de utilización de los directorios de coherencia

Aunque vamos a analizar en profundidad este tipo de protocolo un poco más adelante, veamos un par de ejemplos sencillos que nos permitan entender el uso de un directorio de coherencia. Un multiprocesador de memoria compartida utiliza una malla 2D como red de comunicación. Cada nodo dispone de procesador, memoria cache y un trozo de la memoria principal, junto con la parte correspondiente del directorio de coherencia (y el hardware para gestionar los paquetes). El protocolo de coherencia que utiliza es de tipo MESI.

El primer ejemplo es una fallo en lectura y el segundo un fallo en escritura. En ambos casos sólo vamos a ver una de las posibles opciones (de manera simplificada).

▪ Lectura (fallo)

El procesador Pi (L, local) quiere leer una palabra que pertenece a un bloque del trozo de memoria principal que le corresponde al procesador Pj (H, home), y es un fallo (no tiene el bloque en la cache). Además, el bloque de datos que contiene dicha palabra está en la cache del procesador Pk (R, remote), en estado M. Ésta es la secuencia de acciones que se deberá seguir para conseguir el bloque y mantener la coherencia de los datos:

1. Se envía un mensaje de control de L a H —el nodo que tiene la información sobre la coherencia de ese bloque—, para pedir el bloque o información sobre el mismo.

2. Cuando llega el mensaje a H, se leerá el directorio y se mandará a L la respuesta: “el bloque que necesitas está en el nodo R, en estado M”.

3. A continuación, L enviará un mensaje a R solicitándole el bloque.

4. Cuando R procese el mensaje, enviará a L el bloque de datos solicitado (que tiene en su cache, en estado M).

Page 82: 6.1 INTRODUCCIÓN

▪ 250 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

5. Por último, R también enviará el bloque a H, para actualizar la memoria principal y la información asociada al bloque en el directorio de coherencia (ahora ambas copias son S).

▪ Escritura

El procesador L quiere escribir en una variable que no tiene en la cache (fallo); el bloque que contiene la variable corresponde a la memoria principal del procesador H, y existen múltiples copias del bloque en las caches del multiprocesador, en estado S (shared). Para conseguir el bloque y mantener la coherencia, se debe realizar la siguiente lista de acciones:

1. El procesador L solicita al directorio (en el nodo H) información sobre el bloque, indicándole la operación que se va a realizar; dado que se va a escribir, hay que invalidar todas las copias de dicho bloque de datos

2. El directorio (H) le envía la respuesta: “el bloque está en los procesadores Pi, Pj... en estado S”. Como el bloque está en estado S (coherente), también le enviará una copia del bloque de datos existente en la memoria principal. Además, actualizará el estado del bloque en el directorio (S → M, ya que se va a producir una escritura).

3. En cuanto llegue la respuesta al nodo L, éste deberá invalidar todas las copias del bloque, para lo que enviará un mensaje a cada uno de los procesadores que tienen una copia del bloque, para que la invaliden.

P

C

CC

D

MP

H

R

1

2

4

3

5

L

CC = controlador de coherencia D = directorio de coherencia

L = local H = home R = remote

Page 83: 6.1 INTRODUCCIÓN

7.2 DIRECTORIOS DE COHERENCIA ▪ 251 ▪

4. Por último, los nodos a los que se les ha invalidado la copia del bloque enviarán un mensaje de confirmación a L, indicando que ya han eliminado su copia. Cuando todos esos mensajes lleguen a L, se dará por finalizada la operación.

7.2.2.2 Directorios implementados en memoria cache (listas encadenadas)

En la estructura de directorio que acabamos de ver, toda la información correspondiente a la coherencia de un bloque de datos —estado del bloque, cuántas copias hay y dónde están— se encuentra centralizada en una palabra en un dispositivo junto a la memoria principal. Eso sí, dado que la memoria principal está distribuida, el directorio también queda distribuido por todos los nodos (procesadores) de la red.

Analicemos una segunda opción para construir directorios. En lugar de guardar en un sitio determinado la información sobre la coherencia de un bloque de datos, esa información se puede repartir por las caches que tienen copias de ese bloque. Por tanto, la información de coherencia de un determinado bloque de datos queda repartida por todo el sistema, pero esa información se enlaza formando con ella listas doblemente encadenadas.

Para construir el directorio de coherencia, hay que añadir la siguiente información en cada procesador:

• En el directorio junto a la memoria principal: una palabra por cada bloque de datos, para almacenar el estado del bloque y la dirección del procesador que tiene la “primera” copia del mismo (un puntero).

estado @copia 1

• En el directorio de la memoria cache: dos direcciones (punteros) por cada bloque, para indicar dónde están la anterior y la siguiente copia del bloque. Además, como es habitual en la cache, se guarda el estado del bloque.

estado @copia i–1 @copia i+1

Los estados de los bloques dependerán del protocolo de coherencia que se implemente (MESI, MOSI...), aunque, como veremos, se van a utilizar más estados para poder gestionar de manera adecuada las listas de copias de los bloques.

Page 84: 6.1 INTRODUCCIÓN

▪ 252 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

En esta figura se muestra una lista de tres copias de un bloque de datos (aunque no aparece en la figura, también se guarda el estado del bloque en el comienzo de la lista y en el directorio de cada cache).

En resumen, no hay una palabra que indique dónde están todas las copias del correspondiente bloque, sino una lista de direcciones distribuida. Además, no hemos limitado el número de copias, ya que la lista puede ser de cualquier tamaño.

La cantidad de memoria que se requiere ahora para el directorio de coherencia es menor que en los casos anteriores. Veamos un ejemplo.

Un sistema MPP tiene 1024 procesadores, cada uno con 128 MB de RAM y 512 kB de cache. Los bloques de datos son de 128 bytes, y se utilizan 3 bits para representar los estados. Compara el tamaño del directorio de coherencia en estos dos casos: (a) directorio junto a MP, con 5 copias máximo; y (b) directorio distribuido en MC.

(a) Número de boques de MP: 1 M Tamaño de una palabra del directorio: 5 × 10 + 3 = 53 bits Total del directorio en cada nodo: 53 × 1 M → 53 Mb (6,6 MB) Por tanto, el directorio añade un 5% de memoria en cada nodo. (b) Número de bloques en la cache: 4 k Información de coherencia (por bloque) En la MC: 2 × 10 + 3 = 23 bits En MP: 10 + 3 = 13 bits Total del directorio en cada nodo: (13 × 1 M + 23 × 4 k) → 13,1 Mb esto es, cuatro veces menos que el anterior.

7.2.2.2.1 Un ejemplo de uso del directorio distribuido en las caches

En un apartado posterior analizaremos más detalladamente cómo se utiliza este tipo de directorios para mantener la coherencia de los datos; pero antes de eso, veamos esquemáticamente, al igual que hemos hecho en el caso anterior, los mensajes de datos y de control que se intercambian los procesadores para mantener la coherencia.

Pj, * Pi, Pk

Pi

*, Pj bloque de datos

bloque de datos bloque de datos

bloque de datos

Home (memoria principal)

Pi (cache) Pj (cache) Pk (cache)

MP D

MC

dir

dat

Page 85: 6.1 INTRODUCCIÓN

7.2 DIRECTORIOS DE COHERENCIA ▪ 253 ▪

▪ Lectura (fallo)

El procesador L quiere leer una palabra de la memoria del procesador H, pero no la tiene en la cache. El procesador R tiene en su cache la única copia del bloque que contiene dicha palabra, en estado coherente. La lista de coherencia de dicho bloque es, por tanto, la siguiente: H → R. Para realizar la operación de lectura se enviarán los siguientes mensajes:

1. L envía un mensaje a H solicitándole el bloque de datos. 2. Al recibir el mensaje, H mira en el directorio; como el bloque de datos

no está modificado, enviará a L, desde su MP, el bloque solicitado. Además de ello, actualiza la lista de coherencia del bloque, ya que ahora hay dos copias, para lo que modifica el puntero que contiene el directorio: la nueva cabeza de la lista es L en lugar de R. De paso, junto con el bloque de datos, H envía a L la dirección de la anterior cabeza de la lista de copias del bloque de datos, R.

3. Cuando recibe la respuesta de H, L guarda el bloque en la cache, pero todavía tiene que actualizar la lista de copias, para lo que envía un mensaje a R: a partir de ahora L es la primera copia.

4. Para finalizar la operación, R envía una respuesta a L, indicándole que ya ha actualizado los punteros de la lista de coherencia.

Finalmente, la lista de copias del bloque de datos quedará de la siguiente manera: H → L ↔ R.

▪ Escritura

Por ejemplo, supongamos que se quiere escribir en la copia situada en la cabeza de la lista, que está en estado S. Se trata de un acierto en la cache, por lo que lo único que hay que hacer es eliminar el resto de copias. La operación se realizará de la siguiente manera:

1/2. Se envía un mensaje al nodo H, para que actualice el estado del bloque de datos en el directorio (junto a MP): el estado del bloque pasa de S a M. Como respuesta, H envía a L un mensaje de confirmación de la operación.

3/4. A continuación, L envía un mensaje de invalidación a la segunda copia de la lista (de la que conoce su dirección). Como respuesta, ésta le envía un mensaje de confirmación junto con la dirección de la siguiente copia, que sólo ella conoce.

3'/4'. Se repiten los dos pasos anteriores hasta eliminar todas las copias del bloque de datos.

Page 86: 6.1 INTRODUCCIÓN

▪ 254 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

En resumen. Hemos presentado las principales estructuras que se utilizan para construir directorios de coherencia. En una de ellas, la información de coherencia se concentra en cada nodo en un dispositivo junto a la memoria principal; en la otra, esa información se distribuye por las caches de los procesadores. En el primer caso, el tamaño del directorio puede llegar a ser un problema; en el segundo, la latencia de las operaciones, sobre todo si el número de copias del bloque (la longitud de la lista) es elevado. Sea cual sea la estructura empleada para implementar el directorio, los procesadores van a enviar mensajes de control a través de la red de comunicación, y por tanto es imprescindible procesar esos mensajes de la manera más eficiente posible, ya que forman parte de la ejecución de la operaciones más habituales.

7.2.3 Optimización del tráfico de coherencia

Tal y como hemos comentado anteriormente, para mantener la coherencia en un sistema MPP se deben intercambiar mensajes o paquetes entre los procesadores: paquetes de control (cortos), y paquetes de datos (bloques de datos, más largos). Todos estos paquetes no se generan por necesidades específicas de la aplicación que se esté ejecutando, sino que hay que considerarlos como una sobrecarga que se genera por el hecho de ejecutarse en paralelo. El tráfico asociado a la coherencia deberá competir con el resto de los paquetes que utilizan la red de comunicación, por lo que es muy importante mantener ese tráfico lo más bajo posible. Además, la latencia de las operaciones de coherencia puede ser alta (mucho mayor que en un bus), ya que hay que generar paquetes, transmitirlos por la red junto con el resto de paquetes, procesarlos en destino, etc.

Es necesario asegurar la coherencia de los datos, pero no podemos permitir que el procesador esté parado, sin hacer nada, durante mucho tiempo o que genere mucho tráfico. Por eso, es muy importante gestionar de manera eficiente las “conversaciones” que mantienen los procesadores (mejor dicho, los gestores de los mensajes). Partiendo de un determinado protocolo, dos son los aspectos que debemos intentar:

- reducir el número de mensajes que se envían (tráfico) - reducir la latencia de todo el proceso (del camino crítico, mensajes

que se envían en serie, una tras otro) Estos dos parámetros, la latencia de las operaciones y el tráfico que se

genera, deberían ser “constantes”, es decir, independientes del número de

Page 87: 6.1 INTRODUCCIÓN

7.2 DIRECTORIOS DE COHERENCIA ▪ 255 ▪

procesadores y, en la medida de lo posible, independientes de la situación del tráfico (hot spots).

El protocolo de comunicación más simple es el denominado "pregunta/respuesta", en el que sólo dos procesadores toman parte en una operación de comunicación: el que realiza la petición y el que responde. Tal y como veremos, no es el protocolo más eficiente, pero es el más sencillo de gestionar, porque los “agentes” de la comunicación son pocos y bien identificados (esta característica es importante cuando los protocolos son complejos, para, por ejemplo, poder recuperarse de hipotéticos errores). Desde el punto de vista del rendimiento de la comunicación, sin embargo, conviene tener protocolos que minimicen la latencia y/o el tráfico de los mensajes de control. Veamos, por tanto, el protocolo de comunicación "pregunta/respuesta" y las dos optimizaciones habituales.

Consideremos la siguiente operación: un fallo en lectura en un procesador (L, local) sobre un bloque de datos que pertenece a la memoria principal de otro procesador (H, home), que está copiado en la cache de un tercer procesador (R, remote) en estado M. El directorio de coherencia está centralizado junto a MP en cada nodo, y el protocolo es de tipo MESI.

1. Cuando la comunicación es de tipo pregunta/respuesta, los paquetes que se envían para completar la operación son los que aparecen en la figura. L pide el bloque a H, y H le contesta: “está en R” (mensajes 1 y 2 de la figura). Cuando termina esa "conversación", comienza otra nueva, ahora entre L y R (mensajes 3 y 4a), para conseguir el bloque de datos. Además, también hay que enviar el bloque a H, para actualizar la memoria principal (las dos copias quedarán en estado S).

▪ pregunta/respuesta

mensajes = 5 camino crítico = 4

Como se puede ver en la figura, se han enviado 5 mensajes, de los que cuatro (1→2→3→4) están en el camino crítico que determina la latencia de la operación, ya que se realizarán en serie, uno detrás de otro. Los paquetes 4a y 4b se pueden enviar en paralelo, a la vez.

L

H

R 1. Petición

2. Resp. 4b. Bloque (actualiz.)

4a. Resp. (Bloque)

3. Petición

0100 / M

M I

Page 88: 6.1 INTRODUCCIÓN

▪ 256 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

2. La primera optimización tiene como objeto reducir el tráfico y se denomina intervention forwarding. H no va a contestar directamente a la petición que le ha llegado desde L; en lugar de ello, enviará un mensaje a R, para pedirle el bloque de datos (2). R responderá a H, enviándole el bloque (3), y, finalmente, H se lo enviará a L, el procesador que necesitaba el bloque (4).

▪ intervention forwarding

mensajes = 4 camino crítico = 4

Por tanto, un nodo no implicado en la operación, H, toma parte (intervention) en el intercambio de mensajes, y genera la segunda petición sin responder a la primera. El camino crítico (la latencia) no se reduce, pero el tráfico sí, ya que sólo se mandan cuatro paquetes.

3. Para mejorar la latencia, se puede utilizar la estrategia denominada reply forwarding. En lugar de enviar el bloque de memoria dos veces en serie (R→H→L), se envía en paralelo, a la vez: R→H y R→L.

▪ reply forwarding

mensajes = 4 camino crítico = 3

El tráfico no se reduce —se necesitan cuatro mensajes para realizar la

operación y actualizar la memoria principal— pero sólo tres están en el camino crítico: 1→2→3a. Sin embargo, el protocolo ahora es más “complicado”, ya que la respuesta a la petición de L va a llegar de R, una dirección que L no conoce.

Estas tres estrategias o modos de comunicación que acabamos de ver también se pueden plantear para los protocolos de coherencia implementados mediante listas. A modo de ejemplo, en esta figura se muestran las tres alternativas para una operación de invalidación (invalidar tres copias de un bloque). La escritura se produce en el nodo L.

L

H

R 1. Petición

4. Resp. (Bloque) 3. Resp. (Bloque)

2. Petición

M I

0100 / M

L

H

R 1. Petición

3b. Bloque (actualiz.)

3a. Resp. (Bloque)

2. Petición

M I

0100 / M

Page 89: 6.1 INTRODUCCIÓN

7.2 DIRECTORIOS DE COHERENCIA ▪ 257 ▪

pregunta / respuesta intervention forwarding mensajes = 2k; camino crítico = 2k (k = núm. cop.) mensajes = 2k; camino crítico = k+1

reply forwarding mensajes = k+1; camino crítico = k+1

7.2.4 Atomicidad de las operaciones: carreras

Ya hemos analizado anteriormente el problema de las carreras —por ejemplo, a consecuencia de la falta de atomicidad, dos copias de un mismo bloque de datos pasan a estado M—, y hemos visto cómo se puede resolver en los sistemas SMP que utilizan snoopy. Cuando se utilizan directorios, sin embargo, el problema es más complejo, y para evitar las carreras es necesario diseñar con sumo cuidado el protocolo de coherencia (las comunicaciones entre los procesadores). Veamos un ejemplo.

En un momento determinado, existen dos copias de un bloque en el sistema, en los procesadores Pi y Pj, en estado S. Esa información está actualizada en el directorio (MP): Pi = 1; Pj = 1; estado = S. Simultáneamente, los dos procesadores realizan una escritura en ese bloque. Los dos envían un mensaje al directorio, para saber dónde están las posibles copias de ese bloque y poder invalidarlas. Supongamos que el mensaje de Pi llega el primero al directorio; el directorio le envía la respuesta (existe otra copia, en Pj), y actualiza la información sobre la coherencia (Pj = 0; estado = M). A continuación llega el mensaje de invalidación enviado por Pj. Por desgracia, la información que hay en el directorio y la petición que acaba de llegar son incoherentes, no son acordes: según la información que se tiene

L 1. INV

2. ACK 4. ACK

3. INV

R1 R2 R3 5. INV

6. ACK L

1. INV

2b. ACK 3b. ACK

2a. INV

R1 R2 R3 3a. INV

4. ACK

L

1. INV 2. INV

R1 R2 R3 3. INV

4. ACK

Page 90: 6.1 INTRODUCCIÓN

▪ 258 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

sobre ese bloque, Pj no tiene copia del mismo y, por tanto, ¡no puede invalidar el resto de las copias! ¿Qué se ha hecho mal? ¿Cómo arreglarlo?

Las operaciones de coherencia deben ser atómicas, sin interferencias desde el comienzo hasta su finalización, para que el protocolo funcione correctamente. Sin embargo, las operaciones de coherencia que hemos definido son en sí mismo no atómicas: se envían mensajes de control a través de la red y se espera la contestación. Mientras tanto, puede generarse una interferencia con esa operación por parte de otro procesador, tal y como hemos visto en el ejemplo anterior. Es decir, de acuerdo al primer mensaje que ha llegado, hemos actualizado el directorio como si ya se hubiera terminado toda la operación, y eso no era cierto (todavía no se había invalidado el resto de las copias), y de ahí ha venido el error. Por tanto, ¿cómo asegurar que todas las operaciones se ejecutan atómicamente?

Para asegurar la atomicidad de las operaciones, se utilizan estados transitorios (busy) para los bloques, tanto en las caches como en el directorio, mientras dura una determinada operación. Mientras tanto, si llega alguna petición de operación con un bloque que está en un estado transitorio, ¿qué respuesta se debe de dar? Hay dos posibilidades:

a. Rechazar la petición, enviando un mensaje de control de tipo NACK (negative acknowledgment). Cuando llegue el mensaje NACK al procesador que ha realizado la petición, el controlador local de coherencia decidirá qué hacer (normalmente, intentar de nuevo la operación, reenviando la petición). Este diálogo (petición / NACK) se mantendrá mientras el bloque esté en un estado transitorio.

El uso de paquetes NACK incrementa el tráfico, porque algunos mensajes se van a repetir, pero es un mecanismo relativamente simple para tratar las operaciones de coherencia en serie (de forma atómica).

b. Guardar la petición en un búfer (normalmente en una cola FIFO), para tratarla posteriormente, cuando el bloque pase a un estado estable.

Utilizando búferes para las peticiones se reduce el tráfico, pero la gestión de los búferes puede ser difícil: ¿cuántas peticiones se pueden almacenar? ¿qué sucede si se llenan esos búferes? ...

Ambas alternativas tienen ventajas e inconvenientes, por lo que en los protocolos comerciales suele usarse una u otra en función de la situación que haya que resolver.

Page 91: 6.1 INTRODUCCIÓN

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS ▪ 259 ▪

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS

En los párrafos anteriores hemos presentado los principales aspectos relacionados con los directorios de coherencia: estructura, tamaño, comunicación y atomicidad. En los siguientes dos apartados vamos a presentar dos instanciaciones comerciales de este tipo de protocolos: el de los multiprocesadores SGI Origin, y el que se define en el estándar SCI (utilizado, por ejemplo en el multiprocesador NUMA-Q). Analizaremos ambos ejemplos porque utilizan diferentes alternativas en cuanto a la estructura del directorio (MP o MC), método de comunicación (reply forwarding o pregunta/respuesta), y método para afrontar la atomicidad (paquetes NACK o colas para almacenar las peticiones). Los protocolos de coherencia de ambas máquinas son muy complejos, por lo que vamos a limitarnos a analizar las operaciones de coherencia principales y los problemas de atomicidad que surgen y cómo se resuelven.

7.3.1 Protocolo de coherencia de los multicomputa-dores SGI Origin

El primer protocolo que vamos a analizar es el de los multicomputadores Origin, máquinas de tipo DSM (cc-NUMA); en su primera versión, Origin 2000 (1996), cada nodo de la máquina contaba con 2 procesadores MIPS R10000 (los dos utilizan el directorio, no existe un snoopy local). Como máximo, admitía 512 nodos, es decir, 1024 procesadores. La red de comunicación es un hipercubo cuando el número de procesadores es menor que 64. A partir de ese número, la red toma una estructura jerárquica con hipercubos cuyos nodos son hipercubos. El encaminamiento de los paquetes es adaptativo, y utiliza canales virtuales para evitar el deadlock. El protocolo de comunicación es de tipo reply forwarding34.

El directorio de coherencia está junto a la memoria principal de cada nodo. El contenido es de tipo full bit vector (una variante). El protocolo de

34 La versión actual de esa serie es el multicomputador Origin 3900 (2003). El procesador de los nodos

es el MIPS R16000 (800 MHz). La topología del sistema ha cambiado ligeramente (crossbar / hipercubos); por ejemplo, el ancho de banda de la bisección de la red es ahora de 210 GB/s, unas tres veces el de las versiones anteriores

Page 92: 6.1 INTRODUCCIÓN

▪ 260 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

coherencia es de tipo MESI, invalidación, y la política de escritura es write-back (en los casos en los que es posible). El estado de los bloques de datos en el directorio puede ser uno de estos siete (6+1):

• tres estados estables: I, E, S. El significado de los estados I y S es el habitual. El estado E, en cambio, agrupa a los estados E y M. Por tanto, si en el directorio hay un bloque en estado E, se debe interpretar que sólo hay una copia del mismo en el sistema, pero no se sabe si está modificada o no35.

• tres estados transitorios o busy, para asegurar la atomicidad de las operaciones. A pesar de que los tres tienen significados diferentes, no los vamos a diferenciar a la hora de presentar el protocolo.

• otro estado más, para casos especiales (cambio de páginas en el TLB). Además de la información del directorio, las caches también guardan el

estado de los bloques (los estados habituales I, E, S y M, y busy, en 3 bits). Como hemos visto, para mantener la coherencia de los datos los

procesadores se intercambian diferentes mensajes de control. En los ejemplos que vamos a analizar utilizaremos los siguientes: Rd (petición de un bloque de datos); INV (invalidación de las copias de un bloque); RdEx (petición de un bloque e invalidación de todas las copias que haya de ese bloque); ACK (confirmación de una operación); NACK (rechazo de una petición); y Wr (escritura de un bloque en memoria principal).

Para presentar el protocolo de coherencia, vamos a analizar tres casos: (a) lectura de una variable que no está en la cache; (b) escritura de una variable; y (c) reemplazo de un bloque modificado. Analicemos, pues, la ejecución de estas operaciones una por una.

7.3.1.1 Lecturas (fallo)

Cuando se produce un acierto en una lectura, evidentemente, no hay que hacer nada (en lo que se refiere a la coherencia). Por tanto, debemos analizar los fallos: hay que conseguir el bloque de datos, y asignarle el estado que le corresponda. El bloque que se quiere leer pertenece al espacio de direccionamiento del nodo H (home), y, por tanto, se deberá ir a esa parte del

35 Con el objeto de reducir el tráfico, cuando en la cache se modifica un bloque de datos que está en

estado E, se pasa directamente a estado M y no se le avisa al directorio. Recuerda que la gran mayoría de los bloques de datos serán privados (una sola copia), y que solamente unos pocos serán compartidos, y el estado de los bloques privados en el directorio no tiene especial significado.

Page 93: 6.1 INTRODUCCIÓN

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS ▪ 261 ▪

directorio en busca de la información sobre el bloque. Los mensajes que esto conlleva son:

1. L manda una petición de lectura a H (si la referencia fuera local, L = H, no haría falta mandar el paquete a la red), reserva espacio en su cache para el bloque, y lo pone en estado busy (1a y 1b en la figura).

2. Cuando el mensaje Rd llega al nodo H, se lee la información del directorio de coherencia (para minimizar la latencia, se lee también la MP en paralelo). El estado del bloque en el directorio puede ser uno de estos cuatro:

• I/S → No existe ninguna copia de ese bloque en las caches, o todas son coherentes. Por tanto, la información de MP está actualizada y se puede enviar a L (2b); además, hay que actualizar la información del directorio (2a): se activa el bit correspondiente a L (el último en la figura), y si no había copias (I) se cambia el estado a E.

Cuando el mensaje 2b llegue a L, se cargará el bloque en la cache y se actualizará el estado del bloque: estado E (si en el directorio el estado era I) o estado S (si hay más copias del bloque en el sistema).

• E → El bloque no está actualizado en el nodo H (recuerda que el estado E significa "copia única", no “copia única coherente"; es decir, podría estar en la cache en estado M). Como el modo de comunicación es de tipo reply forwarding, será H el que solicite el bloque.

L

H

1b. Rd A

2b. Bloque I → busy → E/S

0000 / I → 0001 / E 1100 / S → 1101 / S

2a

1a 3

L

H

R

1b. Rd A

2b. Bloque (espec.)

3c. ACK / Wr (bloque)

3b. ACK / Bloque

2c. Rd A (+@L)

I → busy → S E/M → S

1000 / E → 1001 / busy → 1001 / S

1a 4

4 2a

3a

Page 94: 6.1 INTRODUCCIÓN

▪ 262 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

Por eso, cambia el estado del bloque en el directorio, de E a busy, para asegurar la atomicidad de la operación (2a); envía un mensaje al procesador R, que tiene la copia del bloque (2c); y por si acaso, de manera especulativa, envía a L el bloque que tiene en memoria principal (2b), ya que tal vez no ha sido modificado36.

Cuando la petición llega a R, se analiza el estado del bloque en la cache, que tendría que ser E o M. Se cambia el estado del bloque a S y se envían dos mensajes: - Uno a L (3b). Si el bloque estaba en estado E, simplemente se

le confirma el bloque enviado desde H: ACK; en cambio, si estaba en estado M (modificado), se le envía el bloque de datos, ya que la información enviada desde H estaba sin actualizar.

- Y otro, con el mismo contenido, al nodo H (3c), bien un mensaje de tipo ACK para cambiar el estado del bloque en el directorio a S y para dar por finalizada la operación, o bien, si el bloque estaba en estado M, el bloque de datos para actualizar la memoria principal.

Mientras se realiza la operación, el bloque está en estado busy en el directorio (estado transitorio, porque se está realizando una operación sobre el bloque). No se puede pasar el bloque de estado E a estado S directamente cuando llega la petición (1b), ya que si lo hacemos podemos tener fácilmente problemas de coherencia. Por ejemplo, si mientras se está realizando la operación anterior llega a H otra petición para trabajar con el bloque desde el procesador R2, H pensaría que el bloque que tiene en la memoria es válido (porque en el directorio estaría en estado S), y le enviaría esa copia a R2... ¡a pesar de que el bloque podría estar modificado en la cache del procesador R! Necesitamos utilizar el estado busy para asegurar el buen funcionamiento del protocolo.

36 Ese mensaje no tiene nada que ver con la coherencia, sino con la eficiencia del proceso. L debe

esperar a que lleguen los mensajes 2b y 3b, para saber si el bloque es el adecuado o no. Pero (1) si llega primero el mensaje 2b se podría seguir con la ejecución en L, en modo especulativo, sin esperar; y (2) si el bloque estaba en estado E en R y se ha reemplazado, no se podrá mandar desde R; habrá que mandarlo definitivamente desde H, con lo que la latencia de la operación sería mayor. En cualquier caso, si hay que volver a enviar el bloque desde R en muchas ocasiones, el tráfico crecerá, por lo que es importante que la tasa de acierto en el envío especulativo sea alta (E = E, y no E = M).

Page 95: 6.1 INTRODUCCIÓN

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS ▪ 263 ▪

• Busy → La información del bloque no está disponible en ese momento, puesto que se está haciendo otra operación sobre el mismo bloque. Para asegurar la atomicidad hay que rechazar la petición, enviando un mensaje de tipo NACK. Cuando L reciba la respuesta, tendrá que decidir qué hacer, normalmente repetir la petición hasta que se acepte.

7.3.1.2 Escrituras

Tras analizar las lecturas, analicemos las escrituras. Para empezar, si lo que sucede es un acierto en escritura y el bloque está en estado E o M, la operación se resuelve localmente; aunque hubiera que cambiar el estado del bloque de E a M, no se informará de ello al directorio, para reducir el tráfico (razón por la cual no se distingue en el directorio entre los estados E y M). Así pues, nos quedan dos casos por analizar:

- El bloque en la cache está en estado S: existen más copias en el sistema que hay que invalidar. Para ello, se debe enviar un mensaje de tipo INV al nodo H, en cuyo directorio se encuentra la información que necesitamos: dónde están las copias.

- El bloque no está en la cache (I): es, por tanto, un fallo. Hay que enviar un mensaje a H, para conseguir el bloque e invalidar las posibles copias del mismo: RdEx (read exclusive).

Vamos a analizar estas dos posibilidades a la vez. Cuando el mensaje INV o RdEx llega a H, la respuesta será una de las siguientes, dependiendo del estado en el que esté el bloque en el directorio:

• Busy → El bloque está en un estado transitorio, y no se puede dar una respuesta adecuada en ese momento. Por tanto, se enviará un mensaje NACK para rechazar la petición. Habrá que volver intentarlo más tarde.

L

H

1b. Rd A

2. NACK

I → busy

xxxx / busy

1a

L

H

1b. INV A / RdEx A

2. NACK S / I → busy

xxxx / busy

1a

Page 96: 6.1 INTRODUCCIÓN

▪ 264 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

• S → Hay que invalidar todas las copias, operación que queda en manos del nodo H. Para ello, a. Se envía un mensaje a L (2b), en el que se indica cuántas copias

se van a invalidar. Esta información es necesaria para saber cuándo se puede dar por terminada la operación de coherencia en L. Además, si la petición era RdEx, también se enviará el bloque de datos, ya que los datos de la memoria de H están actualizados (estado S).

b. Se envía un mensaje INV a todos los procesadores que tengan copia de ese bloque (2c, 2d...); al llegar al destino, se invalidarán en las caches las copias del bloque de dato (I), tras lo cual se enviarán los correspondientes mensajes ACK a L —al procesador que ha hecho la escritura— para confirmar que se han realizado las invalidaciones (3b, 4b...).

Desde el punto de vista de L, la operación de coherencia termina cuando llegan todos esos mensajes: la información desde H y todos los ACK. En ese momento, el bloque pasará a estado M (5).

Mientras se está realizando la operación, ¿qué ha sucedido con el

estado del bloque en el directorio? Al final, el bloque habrá que ponerlo en estado E, pero existen dos opciones. Por un lado, del estado S se puede llevar a un estado busy intermedio, para asegurar la atomicidad. Mientras tanto, si llega a H alguna petición para trabajar con ese bloque, será rechazada. Sin embargo, si se hace así, alguien se tiene que encargar de avisar a H de que la operación ha terminado, para que cambie el estado del bloque de busy a E. ¿Quién? El procesador L, ya que es el único que sabe cuándo termina realmente la operación. Por tanto, habrá que enviar un último mensaje, de L a H.

L

H

R1

1b. INV A / RdEx A

2b. #cop / +Bloque 3b. ACK

2c. INV A (+@L)

R2 S/I → busy → M S → I

1101 / S → 0001 / E 1100 / S → 0001 / E

5

2a

3a 1a

4b. ACK

2d. INV A (+@L)

S → I 4a

Page 97: 6.1 INTRODUCCIÓN

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS ▪ 265 ▪

Existe una segunda opción: pasar directamente el bloque de S a E en el directorio, a pesar de que la operación no haya terminado aún. El objetivo es doble: reducir el tráfico (no es necesario el último mensaje del caso anterior), y tener una latencia menor (se puede comenzar otra operación sin que haya terminado la anterior). Pero, por supuesto, si se hace eso se debe asegurar que no existe ningún problema con la atomicidad.

Así pues, si llega una nueva petición para ese bloque, el directorio la aceptará, cambiando el estado del bloque a busy, y enviará la petición a L, atendiendo a la información que en ese momento tiene el directorio: en L está la única copia correcta. Cuando llegue esta petición a L, si la operación de invalidación anterior todavía no ha terminado, el bloque estará en un estado busy en la cache; por tanto ¿qué hay que hacer? ¿rechazar la petición que ha llegado desde el directorio? Esa posibilidad sería adecuada para mantener la coherencia, pero se puede aplicar una segunda optimización: guardar ese paquete en un búfer, hasta que termine la operación anterior. Además, sólo habrá que guardar, como máximo, una petición, ya que no es posible recibir otra petición antes de que termine la operación anterior, puesto que el directorio está en estado busy y, por tanto, rechazará cualquier otra petición para trabajar con ese bloque.

En el computador Origin 2000 se utiliza esta optimización; por tanto, el bloque en el directorio pasa directamente de S a E (2a); como consecuencia, la única petición que podría llegar a L se guardará en un búfer, para ser tratada más tarde.

Un último comentario: ¿y si la petición que llega a H en ese momento procede del procesador que tenía la única copia del bloque, R1? Resolveremos este caso en el próximo apartado (estado en el directorio = E; petición = INV).

• E → Hay una única copia, pero no se sabe si está modificada o no. En función del mensaje que ha llegado a H, sucederá lo siguiente:

+ RdEx (wr fallo). El estado del bloque en el directorio pasa a busy (2a), y se envía a L el bloque que está en memoria principal, por si fuera válido (2b). Además, se envía a R, el procesador que tiene el bloque, la petición RdEx (Rd + INV), junto con la dirección del nodo que la efectuó (2c).

Page 98: 6.1 INTRODUCCIÓN

▪ 266 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

Cuando llega la petición a R, el bloque puede estar en la cache en uno de dos estados: E, coherente, o M, modificado. Tal y como hemos visto en el caso anterior, se deben enviar dos mensajes: uno a L (3b), para confirmar el bloque que ha recibido de manera especulativa de H, o, por el contrario, para enviar el bloque; y otro mensaje a H (3c), para que dé por terminada la operación, y, si procede, para actualizar el bloque en memoria principal. En ese momento, el estado del bloque en el directorio pasará de busy a E (4).

+ INV (wr acierto). Ha sucedido algo raro: el estado del bloque en el directorio debería ser S, y no E (por eso se ha enviado el mensaje INV al directorio, porque había varias copias).

¿Qué ha sucedido? Pues que otro procesador que tenía una copia de ese mismo bloque ha hecho lo mismo, una escritura, y ha enviado un mensaje de INV a H. Esta petición ha llegado antes que la nuestra, y el estado del bloque en el directorio ha cambiado a E, con lo que está en camino un mensaje de INV, enviado desde H a L.

Para mantener la atomicidad, se debe rechazar la petición: NACK (2). Finalmente, llegará un mensaje de INV a L, se invalidará la copia, y se repetirá la petición de escritura, pero esta vez con un RdEx, y no con un INV.

L

H

R

1b. RdEx A

2b. #cop + Bloque (espec.)

3c. ACK / Wr (Bloque)

3b. ACK / Bloque

2c. RdEx A (+@L)

I → busy → M E/M → I

1000 / E → 0001 / busy → 0001 / E

1a 4

4 2a

3a

L

H

1b. INV A

2. NACK S → busy

1000 / E

1a

Page 99: 6.1 INTRODUCCIÓN

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS ▪ 267 ▪

• I → Si el bloque está en el directorio en estado I, nuevamente tenemos que distinguir dos casos, según sea el mensaje que haya venido de L:

+ RdEx (wr fallo). No existe ninguna copia del bloque en el sistema; se debe poner el bloque en el directorio en estado E, el bit correspondiente a L se debe poner a 1, y se enviará el bloque al procesador que lo ha solicitado (2b). Cuando el bloque llegue a L, se cargará en la cache en estado M, ya que se va a escribir sobre él (3).

+ INV (wr acierto). ¿Es posible? Según la información del directorio, no hay ninguna copia de dicho bloque, pero por lo menos L tenía una. ¿Qué ha sucedido?

Probablemente ha ocurrido lo siguiente: otro procesador que tenía una copia del bloque (R) lo ha modificado, y, por tanto, la información del directorio ha cambiado a 0010/E (además, se ha enviado a L un mensaje de invalidación, que aún no ha llegado). A continuación, el bloque ha sido reemplazado en R37, y al estar modificado, se ha enviado a H par actualizar la MP, con lo que la información en el directorio a pasado a ser 0000/I. Tras ello, llega la petición de L

El mensaje de invalidación de L ya no tiene sentido, por lo que hay que rechazarlo; la respuesta, por tanto, debe ser NACK (2).

37 La operación anterior no había terminado todavía (al menos faltaba el mensaje ACK de L); sin

embargo, al ser una escritura local, tal vez se decidió seguir adelante, o efectuar un cambio de contexto para ejecutar otro proceso.

L

H

1b. RdEx A

2b. Bloque I → busy → M

0000 / I → 0001 / E 2a

1a 3

L

H

1b. INV A

2. NACK S → busy

0000 / I

1a

Page 100: 6.1 INTRODUCCIÓN

▪ 268 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

7.3.1.3 Actualización de la memoria principal

Para terminar con las operaciones básicas de coherencia, nos falta por analizar un caso: el reemplazo de un bloque que esté en estado M. Como ya sabemos, antes de reemplazarlo (borrarlo) hay que escribir el bloque en memoria principal (y actualizar el directorio), ya que, si no, se perdería la información. Para ello, se envía un mensaje de tipo Wr al directorio, con el bloque que hay que actualizar.

En el directorio, el estado del bloque no puede ser ni I ni S, ya que en esos casos no se envía el bloque a memoria principal para actualizar (porque es coherente); por tanto, el bloque estará en el directorio en estado E o busy.

• E → Si el bloque está en estado E, la operación es simple: hay que cambiar el estado a I (no existe ninguna copia en el sistema), y hay que responder a L con un mensaje ACK. Cuando termina la operación, el bloque se elimina de la cache de L.

• Busy → Es un caso curioso. El bloque que hay que actualizar en la memoria principal está "ocupado". La única interpretación posible es que se han cruzado una operación realizada por otro procesador (1b: R le pide a H el bloque que tiene L) y la actualización que viene desde L a consecuencia del reemplazo (2d).

Tanto H como L han dejado el bloque en estado busy; por tanto, se

mantendría la atomicidad rechazando el mensaje de actualización (2d), para que se volviera a intentar más tarde. Por desgracia, el paquete que se rechazaría contiene todo el bloque de datos y, por

L

H

1b. Wr (Bloque)

2b. ACK M → busy → x

0001 / E → 0000 / I

1a

2a

3

L

H

R

1b. Rd A

2b.Bloque (espec.)

3c. ACK

2c. Rd A

2d. Wr (Bloque)

3b. Bloque

I → busy → E M → busy → x

0001 / E → 1001 / busy → 1000 / E

1a

3a 2a

2 4 4

Page 101: 6.1 INTRODUCCIÓN

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS ▪ 269 ▪

tanto, el tráfico crecería considerablemente (además, ¿cuántas veces puede suceder eso?). Existe, sin embargo, una posible optimización de la operación, combinar las dos operaciones: el bloque de datos que llega de L (2d) se envía a R (3b), y se confirma la actualización (3c). Además el estado del bloque en el directorio pasa de busy a E. Cuando finalmente llega la petición del bloque a L (2c) simplemente no se le hace caso (estará en estado busy, o ya no estará en la cache).

7.3.2 El protocolo de coherencia estándar SCI en la máquina NUMA-Q de Sequent

Como segundo ejemplo de protocolos de coherencia, vamos a analizar el protocolo que se define en el estándar SCI (Scalable Coherent Interface) de IEEE. La información de coherencia se distribuye entre las caches formando listas doblemente ligadas, y se ha utilizado, por ejemplo, en la máquina NUMA-Q (Sequent), un multiprocesador pensado para desarrollar aplicaciones comerciales (o también en el Exemplar de Convex, en el ámbito del cálculo científico).

NUMA-Q es un multiprocesador "pequeño", con 32 procesadores. Los nodos de la red consisten en una tarjeta con 4 procesadores —4 Pentium pro, bus, snoopy—. Para conectar los 8 nodos se utiliza una red simple: un anillo (con enlaces de 1 GB/s).

Los nodos (las tarjetas de 4 procesadores) se conectan a la red (al anillo) mediante un interfaz o gestor de comunicación específico, denominado IQ-link. Además de encaminar los paquetes, este dispositivo también gestiona la coherencia, para lo que utiliza una "cache" de 32 MB, denominada remote access cache, en la que se guardan los bloques de datos que se han traído

4P 4P

4P

4P 4P

4P

P MC

M E/S

PCI

E/S

IQ link

P MC

P MC

P MC

4P 4P

Page 102: 6.1 INTRODUCCIÓN

▪ 270 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

desde otros nodos, es decir, los que se han copiado en alguna de las caches locales (similar a la solución que hemos analizamos en el caso de una jerarquía de buses). La cache remota y las caches internas del nodo cumplen el "principio de inclusión"; por tanto, si se reemplaza un bloque en la cache remota, se debe invalidar en las caches internas. Del mismo modo, si se modifica un bloque en una cache interna, hay que actualizar el estado de ese bloque en la cache remota. Los bloques son de 64 bytes.

La coherencia de los datos se mantiene de manera jerárquica: dentro de cada nodo mediante los snoopy locales (un protocolo MOESI de invalidación), y entre los nodos mediante el directorio distribuido por los IQ, que implementa el protocolo SCI, con listas distribuidas en las caches. Analicemos, aunque sea por encima, algunos aspectos del protocolo38.

7.3.2.1 SCI: estados y operaciones

El estado correspondiente a un determinado bloque va a estar repartido en MP y en las caches. En la parte de directorio que está junto a MP, se indica la dirección de la primera copia y el estado del bloque. Un bloque puede estar en alguno de estos tres estados:

• Home: no existe ninguna copia en ninguna cache (la lista está vacía).

• Fresh: existen copias, y todas son iguales y coherentes con la información de memoria principal.

• Gone: la información de MP no está actualizada; el bloque está modificado en las caches.

En esta parte del directorio no se utilizan los estados de tipo busy.

También en las caches se guarda información sobre los bloques (se utilizan 7 bits, y el estándar define 29 estados normales y una gran cantidad de estados transitorios, de los que sólo vamos a ver unos pocos). Los estados permanentes de un bloque están divididos en dos partes:

• La posición que ocupa el bloque en la lista de copias (dónde está el bloque). Hay cuatro posibilidades: Only (copia única), Head (primera copia), Mid (copia intermedia), y Tail (última copia).

• El estado: Dirty (M), Fresh (S), Valid (S’), Exclusive (E), Stale...

38 El estándar SCI admite cualquier tipo de topología: cadena, anillo, redes jerárquicas, redes multietapa

(Omega), etc. Admite hasta 64 k procesadores; las direcciones son de 64 bits: 16 bits para identificar el procesador, y los restantes 48 bits para indicar la dirección de memoria.

Page 103: 6.1 INTRODUCCIÓN

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS ▪ 271 ▪

Por tanto, el estado de un bloque puede ser: head-dirty, tail-valid... En la siguiente tabla se presentan los estados más utilizados:

Memoria principal

Memorias cache primero medio último

Home - - - no hay copias

Fresh Only-Fresh - - una sola copia, coherente (E)

Fresh Head-Fresh - Tail-Valid dos copias, coherentes (S)

Fresh Head-Fresh Mid-Valid Tail-Valid muchas copias, coherentes

Gone Only-Dirty - - una sola copia, modificada (M)

Gone Head-Dirty - Tail-Valid dos copias, modificadas (O/S’)

Gone Head-Dirty Mid-Valid Tail-Valid muchas copias, modificadas (O/S’)

Gone Head-Exclusive - Tail-Stale dos copias (ping-pong)39

Gone Head-Stale - Tail-Exclusive dos copias (ping-pong)

Como sabemos, para mantener la coherencia de los bloques hay que

realizar muchas operaciones de comunicación. El estándar de coherencia SCI define tres funciones de coherencia para realizar todas esas funciones:

• Insertion o List Construction: para añadir una copia a la lista.

• Deletion o Roll-out: para eliminar una copia de la lista.

• Reduction o Purge: para invalidar el resto de las copias de la lista.

En cuanto a la comunicación, todas las operaciones son del tipo pregunta/respuesta. Además, no utiliza mensajes NACK para asegurar la ejecución atómica de las operaciones, sino búferes (hay alguna excepción).

Aplicando el estándar SCI se pueden definir diferentes protocolos: minimal (no admite copias), typical, full... Analicemos, mediante unos pocos ejemplos, cómo funciona el protocolo en los tres casos habituales: lecturas, escrituras y reemplazos.

39 La pareja de estados Exclusive / Stale se utiliza para dar una respuesta adecuada a un caso típico de

compartición de datos, en el que el bloque sólo se comparte entre dos procesadores. Cuando uno escribe, la copia del otro pasa a estado Stale (vieja) pero no se borran los enlaces. De esa manera, cuando el segundo tiene que volver a usar el bloque, ya sabe dónde tiene que ir a buscarlo, sin necesidad de tener que consultar el directorio.

Page 104: 6.1 INTRODUCCIÓN

▪ 272 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

7.3.2.2 Lecturas (fallo)

Como en los ejemplos anteriores, L (local) es el nodo que ejecuta la operación, H (home) es el nodo que tiene la variable (bloque) en memoria, y R (remote) es el que tiene la primera copia del bloque. Se quiere realizar una lectura en L, pero la variable (el bloque) no está en la cache, por lo que se debe conseguir el bloque de datos que contiene la variable, y colocarlo en la primera posición de la lista de copias, para lo que hay que ejecutar la función List Construction:

1. Se reserva sitio en la cache para el bloque, y se pone en estado busy.

2. Se envía una petición a H. Cuando llega la petición, el estado del bloque en el directorio en H puede ser uno de los siguientes:

• Home → No existen copias del bloque en las caches. Ésta será, por tanto, la primera copia. Se cambia el estado a Fresh, y se modifica el puntero para que apunte a la nueva copia (@L). Por último, se envía el bloque de MP (2b). Cuando llega a L, el bloque se carga en la cache en estado Only-Fresh. Dado que no hay más copias, no es necesario guardar punteros.

• Fresh → Existen varias copias del bloque en las caches de los

procesadores, unidas en una lista, y hay que situar la nueva copia como primera copia de la lista.

Para ello, se modifica el puntero (2a) para que apunte a la nueva copia, y se envía el bloque de datos (2b) (la copia de MP es Fresh = coherente) junto con la dirección del anterior cabeza de lista (@R), ya que L tendrá que comunicarse con él para actualizar los enlaces de la lista.

L

H

1b. LC (Rd A)

2b. Bloque

Dir (MC) I → busy → O-F | *-*

Dir (MP) H | * → F | @L

2a 1a 3

Page 105: 6.1 INTRODUCCIÓN

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS ▪ 273 ▪

Cuando la respuesta de H llega a L, se carga el bloque en la cache, se mantiene en estado busy (será otro código de busy), y se inicia la segunda suboperación: adecuar el comienzo de la lista. Por tanto, se enviará un mensaje al antiguo cabeza de lista, R, para indicarle quién es ahora la nueva cabeza (new head, 3). Cuando llegue el mensaje a R, se modificará el estado del bloque y los enlaces:

Only-Fresh | * - * → Tail-Valid | @L - * o Head-Fresh | * - @R2 → Mid-Valid | @L - @R2

Para terminar la operación, R enviará un mensaje de ACK a L (4), La operación se da por terminada al recibir el mensaje y cambiar el estado del bloque a Head-Fresh | * - @R.

• Gone → La copia adecuada del bloque de datos no es la de MP, sino la que está en la cache en la cabecera de la lista. El proceso es el mismo que el anterior, salvo la obtención del bloque, que debe provenir de R. Por tanto, cuando se actualice el estado del bloque y el enlace en la antigua cabeza de lista, se enviará a L, junto con el mensaje ACK, el bloque de datos (4b). El estado del bloque en la cache de L será finalmente Head-Dirty (el bloque está modificado).

7.3.2.3 Escrituras

En el protocolo SCI, únicamente el cabeza de la lista de copias puede modificar el bloque. Además, como es un protocolo de invalidación, habrá que invalidar todas las copias, y quedará como única copia.

R

L

H

1b. LC (Rd A)

2b. Bloque + @R Dir (MC)

I → busy → H-F | *-@R

Dir (MP) F | @R → F | @L

2a

1a

3. New Head (@L)

Dir (MC) O-F | *-* → T-V | @L-* H-F | *-@R2 → M-V | @L-@R2

4b. ACK 4a

5

Page 106: 6.1 INTRODUCCIÓN

▪ 274 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

Tenemos que distinguir estos casos:

1. (acierto / cabeza). La escritura es en la copia que está en la cabeza de la lista. Se escribe y se invalida el resto de copias (INV).

→ Purge

2. (fallo). El bloque sobre el que se quiere escribir no está en la cache. Hay que conseguir el bloque y colocarlo como cabeza de la lista de copias (List Construction); luego, siendo cabeza, se deben invalidar el resto de las copias. Es decir, "wr-fallo" = "rd-fallo + wr-acierto".

→ List Construction + Purge

3. (acierto / intermedio). El bloque que se quiere modificar se encuentra en una posición intermedia de la lista. Dado que sólo puede escribir la cabeza de la lista, hay que hacer lo siguiente: (a) sacar la copia de la lista de copias (Roll-out), y (b) efectuar las operaciones del caso 2 (fallo).

→ Roll-out + List Construction + Purge

Analicemos, con un poco más de detalle, el primer caso: la escritura sobre la copia de la cabeza de la lista. Para mantener la coherencia, lo único que hay que hacer es invalidar las restantes copias, para lo que se ejecuta la función de coherencia Purge. Según el estado del bloque, el procedimiento es el siguiente (cuatro posibilidades):

1. El estado de la copia de la cabeza es Only-Fresh, es decir, es la única copia. De todas maneras, hay que cambiar el estado del bloque, tanto en la cache como en el directorio. En la cache se debe poner en estado Only-Dirty, y en el directorio junto a MP en estado Gone. Para actualizar la información en el directorio de MP, se envía un mensaje a H, para que cambie el estado del bloque: Fresh → Gone. Con la confirmación (ACK) que llega desde el directorio (2b), se termina la operación.

L

H

1b. Wr (cambiar estado de A)

2b. ACK

Dir (MC) O-F | *-* → busy → O-D | *-*

Dir (MP) F | @L → G | @L

2a 1a 3

Page 107: 6.1 INTRODUCCIÓN

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS ▪ 275 ▪

2. El estado de la copia de la cabeza es Only-Dirty. Es el caso más simple; sólo hay una copia y, además, ya está modificada (en el directorio de MP estará en estado Gone). Por tanto, no hay que hacer nada para mantener la coherencia.

3. El estado del bloque en la cabeza es Head-Fresh. Ahora sí, como hay más de una copia (aunque no se sepa cuántas), hay que invalidar el resto de las copias. Al comienzo de la operación40, hay que avisarle al directorio (1b) para que cambie el estado del bloque: Fresh → Gone. Cuando llegue la confirmación del directorio (2b), se pasará a invalidar las copias, de la siguiente manera:

→ Se envía un mensaje de INV al segundo de la lista (3); cuando llegue el mensaje a R1, se invalidará la copia en la cache (estado = I, y puntero = null), y en la respuesta (4b) se enviará la confirmación —ACK— de la invalidación y la dirección de la siguiente copia de la lista.

→ Se repite el procedimiento con las demás copias. La respuesta de la última copia (Tail) indicará que ya no hay más copias para invalidar.

Desde el comienzo de la operación (1a) el bloque se ha mantenido en estado busy en la cache de L. Al final, el estado pasará a ser: Only-Dirty.

4. Por último, el estado de la copia de la cabeza es Head-Dirty. En el directorio, el bloque ya está en estado Gone. Por tanto, basta con ejecutar la función Purge, tal y como hemos visto en el caso anterior.

40 Hay que efectuar las operaciones en este orden: primero, avisar al directorio, y luego comenzar con la

invalidación, porque, si no, podrían aparecer carreras. Si efectuamos primero la invalidación, y, en el camino, llega una petición al directorio solicitando ese bloque, H enviaría el bloque (estaba en estado Fresh), con lo que no se mantendría la coherencia de datos (dos copias distintas del mismo bloque).

3. INV A

Dir (MC) H-F | *-@R1 → busy → O-D | *-*

Dir (MP) F | @L → G | @L

4a 1a

1b. Wr (cambiar estado de A)

Dir (MC) T-V | @R1-* → I | *-*

2b. ACK 2a

7

4b. ACK + @R2

5. INV A

6b. ACK + *

Dir (MC) M-V | @L-@R2 → I | *-*

H

L

R1

R2

6a

Page 108: 6.1 INTRODUCCIÓN

▪ 276 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

Tal y como hemos visto, la latencia del proceso de invalidación puede ser alta en la medida en que haya un número elevado de copias del bloque. En el multicomputador que hemos tomado como ejemplo, sin embargo, el número de copias no va a ser muy elevado, ya que en la red no hay más que 8 nodos.

El segundo caso, una escritura con fallo en cache, se resuelve con una combinación de las dos funciones que hemos visto ya: List Construction, para conseguir una copia del bloque y ponerse en la cabeza de la lista (ya que se va a hacer una escritura, se cambia el estado a Gone en el directorio en MP); y Purge, para invalidar el resto de las copias.

Por último, tenemos que analizar el tercer caso, es decir, un acierto en la cache, pero cuando el bloque no está en la cabeza de la lista. Tal y como hemos comentado anteriormente, sólo se puede modificar la copia que está en la cabeza de la lista; por tanto, para poder escribir, primero hay que sacar la copia de la lista, y luego introducirla de nuevo, ahora en la cabeza de la lista. Para sacar un bloque de la lista, hay que ejecutar la función de coherencia Roll-out, de la siguiente manera:

1. Se envían sendos mensajes desde L a R1 y R2, anterior y siguiente de la lista (L tiene ambas direcciones), para que actualicen sus punteros, ya que las copias i–1 e i+1 deben de apuntarse mutuamente. Cuando se reciben esos mensajes, se actualizan los punteros y se envían los mensajes respuesta de tipo ACK. Mientras no se reciban ambas respuestas, el bloque se mantiene en un estado transitorio

Si en la lista no hay más que dos copias, el procedimiento es el mismo

pero con el único vecino, que cambiará el estado del bloque en la cache a Only-x | *-*.

2. Cuando lleguen los dos ACK, el bloque queda fuera de la lista de copias (su estado variará en función de la operación), tras lo cual, si es necesario, se continuará con otra operación.

R1

L

R2

1c. RO A + @R1

3b. ACK

Dir (MC) M-V | @R1-@R2 → busy → busy / I | *-*

Dir (MC) T-V | @L-* → T-V | @R1-*

3a

4

1b. RO A + @R2

Dir (MC) H-D | *-@L → H-D | *-@R2

2b. ACK

2a

1a

Page 109: 6.1 INTRODUCCIÓN

7.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS DE COHERENCIA: DOS EJEMPLOS ▪ 277 ▪

La operación de Roll-Out puede producir deadlock. Dos procesadores contiguos quieren abandonar la lista; se envían mutuamente los correspondientes mensajes, y se quedan esperando la respuesta del otro: el sistema se ha bloqueado. El bloqueo se puede evitar fácilmente si se obliga a contestar este tipo de mensajes a pesar de estar en un estado busy, y se establece un sistema de prioridades. Por ejemplo, para este caso, la copia que esté más cerca de la cabeza de la lista tiene prioridad para hacer Roll-Out.

7.3.2.4 Actualización de la memoria principal

Para terminar con las principales operaciones de coherencia tenemos que analizar el reemplazo de un bloque en la cache. A pesar de que la copia del bloque sea coherente con memoria principal, hay que ejecutar la función Roll-Out para eliminar el bloque de la lista de copias y, por tanto, actualizar los punteros de la lista. Además, si el bloque que se reemplaza está en estado Only-Dirty es necesario guardar los datos en memoria principal y actualizar la información del directorio (en el nodo H). Por tanto, ya hemos analizado todas las operaciones que se deben realizar.

Quizás merezca la pena comentar un caso particular de la función Roll-out, que no ha aparecido cuando hemos analizado las escrituras: el bloque que se quiere eliminar está en la cabeza de la lista. En este caso, hay que hacer lo siguiente:

1. Se envía un mensaje de control al segundo de la lista para que cambie de estado: Mid(Tail)-Valid(Fresh) → Head(Only)-Dirty(Fresh). Mientras tanto, el bloque está en estado busy.

2. Cuando llega la respuesta de éste, se envía un mensaje a H, para que actualice el puntero en el directorio. La operación termina cuando llega el mensaje ACK del directorio.

7.3.2.5 Atomicidad y carreras

Tal y como ha quedado reflejado en los párrafos anteriores, las operaciones de coherencia no son, en absoluto, atómicas, por lo que son de esperar interferencias entre ellas. Además, en el directorio, al comienzo de las listas, no se utilizan estados busy. ¿Cómo se evita el problema de carreras? El procedimiento general es el siguiente: (a) si no se puede procesar una petición porque en ese momento el bloque está en estado busy, se almacena en un búfer para ser tratada posteriormente; y (b) si la petición

Page 110: 6.1 INTRODUCCIÓN

▪ 278 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

que llega no es coherente con los datos que tenemos, simplemente se rechaza. Veamos tres ejemplos.

1. Se está añadiendo una copia más, en L1, a la lista de las copias de un bloque de datos, y se está ejecutando la función List Construction. Ya se ha avisado al procesador H, y éste ha actualizado el puntero a la nueva cabeza y ha respondido con la dirección del procesador que era la cabeza anterior. La copia de L1 todavía está en estado busy, porque aún no ha terminado la operación.

Mientras se desarrolla esa operación, el procesador L2 lee una variable de ese bloque y falla (no está en la cache), por lo que envía a H una petición de bloque. De acuerdo a la información del directorio, la cabeza de la lista es L1, y eso es lo que se responde; además, a partir de ahora, apuntará a L2 como cabeza de la lista. Una vez recibida la contestación, L2 envía un mensaje a L1 (new head), pero cuando dicho mensaje llega el estado del bloque es todavía busy.

¿Cómo se resuelve el problema? En lugar de rechazar el mensaje, L1 guarda la petición para tratarla más tarde. No hay problemas con el número de peticiones pendientes, ya que la siguiente petición, en caso de haberla, llegará a L2 (pues el directorio indica L2 como cabeza de lista) y, de esta manera, los procesadores irán formando una lista provisional. Cuando termine la operación del primero de esta lista, y cuando llegue a un estado estable, se procesará el mensaje que queda pendiente, y así la cabeza de la lista se irá moviendo hasta completar la lista definitiva.

La misma estrategia se utiliza en otros muchos casos; por ejemplo, mientras se está ejecutando la función Purge, las peticiones para situarse en la cabeza de la lista se van dejando en una cola provisional.

NewHead

busy

Dir (MP) G | @R1 → G | @L1

H-D | *-@R2

H

L1 R1 R2

T-V | @R1-* NewHead

busy

Dir (MP) G | @L1 → G | @L2

H-D | *-@R2

H

L1 R1 R2

T-V | @R1-*

L2

NewHead

busy

Page 111: 6.1 INTRODUCCIÓN

7.4 RESUMEN ▪ 279 ▪

2. La cabeza de lista de las copias de un bloque tiene que escribir y, por consiguiente, invalidar todas las copias. Su estado es Head-Fresh. Antes de comenzar a invalidar las copias, enviará un mensaje a H (directorio) para que cambie el estado del bloque: Fresh → Gone. Pero al llegar a H, el estado del bloque no es Fresh, sino Gone. ¿Es posible?

Sí; ha sucedido lo siguiente: se ha producido una escritura (en fallo) en un procesador que no estaba en la lista, y ya se ha dirigido al directorio, para situarse en al cabeza de la lista (List Construction).

Por tanto, además de haber cambiado el estado, también ha cambiado el puntero, para que apunte a la nueva copia. En el directorio no se utilizan estados transitorios (busy), pero se detecta fácilmente que la petición que llega no es coherente con la información que hay en el directorio, y, por consiguiente, se rechaza la petición enviando un mensaje de tipo NACK. El que estaba (creía) en la cabeza de la lista ya no está (aunque todavía no lo sabe), y deberá intentarlo de nuevo, pero de otra manera (Roll-Out + List Construction + Purge).

3. Por último, veamos un tercer caso. Hay que reemplazar en una cache un bloque de datos que está en la cabeza de una lista. Ya se ha separado del segundo de la lista y mientras se está comunicando con el directorio, para terminar la operación, otro procesador se ha querido meter como cabeza de la lista, y ya se ha marcado en el directorio como nueva cabeza de lista (List Construction).

Al igual que en el caso anterior, cuando llega la petición de Roll-Out del antiguo cabeza de lista, la petición no es coherente con la información del directorio, por lo que se debe rechazar (NACK). ¡Cuidado! cuando llegue el mensaje del nuevo cabeza de lista al anterior cabeza de lista, es necesario enviar una respuesta especial: dónde está la verdadera segunda copia de la lista.

7.4 RESUMEN

En los multiprocesadores DSM el espacio de memoria es único, pero la memoria está repartida entre todos los nodos del sistema. De cara a poder utilizar un número elevado de procesadores, la red de comunicación no es

Page 112: 6.1 INTRODUCCIÓN

▪ 280 ▪ Capítulo 7: COHERENCIA DE DATOS EN LOS COMPUTADORES DSM

una red centralizada (un bus) sino redes tales como mallas, toros, etc. Por tanto, no se puede utilizar una estrategia tipo snoopy para mantener la coherencia de los datos. En su lugar, la coherencia se mantiene mediante el uso de directorios de coherencia, en los que se recoge información sobre las copias de cada bloque de datos, en una sola palabra o distribuida entre las caches de los procesadores.

El proceso de mantenimiento de la coherencia puede tener un efecto importante sobre el rendimiento del sistema, por lo que se debe hacer de la forma más eficiente posible. Los protocolos de coherencia de este tipo son complejos y es necesario analizar con sumo cuidado todos los casos posibles, para darles la respuesta adecuada. Hay que tener en cuenta que no existe un dispositivo centralizado que controle todo el sistema, por lo que resulta complicado asegurar la atomicidad de las operaciones. y evitar los problemas de carreras. Para ello, se sigue un doble camino: se utilizan estados transitorios para los bloques de datos, desde que comienza la operación hasta que se da por finalizada, y se envían paquetes de tipo NACK para rechazar operaciones que en ese momento no pueden procesarse (o, en su caso, se guardan para ser procesadas más tarde). Una de las primeras máquinas en las que se pusieron en juego este tipo de ideas fue el computador DASH, máquina de la que derivaron posteriormente versiones comerciales como las de la serie Origin de SGI.

Page 113: 6.1 INTRODUCCIÓN

▪ 8 ▪

Paralelización de Bucles y Planificación de Tareas

8.1 INTRODUCCIÓN

En los capítulos anteriores hemos estudiado algunos de los nuevos problemas que aparecen cuando se quiere ejecutar una determinada aplicación en paralelo, entre P procesadores: la coherencia de los datos, la sincronización, la comunicación, etc. En este capítulo vamos a analizar cómo ejecutar en paralelo de manera eficiente bucles, estructuras en las que es sencillo obtener altos niveles de paralelismo.

Tal y como ocurre en el caso de la vectorización, cuando se ejecuta un bucle en paralelo se modifica el orden original de las instrucciones: las iteraciones del bucle se van a ejecutar en diferentes procesadores, y por tanto

Page 114: 6.1 INTRODUCCIÓN

▪ 282 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

no tendremos control de en qué orden se ejecutarán. Ya sabemos que no se puede reordenar el código de cualquier manera, ya que hay que respetar las dependencias de datos entre instrucciones, por lo que habrá que analizar las dependencias entre instrucciones para encontrar la mejor manera de paralelizar un bucle. El análisis de las dependencias es el mismo que hemos visto en el primer capítulo para vectorizar los bucles, pero las decisiones que vamos a tomar serán diferentes. Como vamos a ver, para asegurar que se cumplan las dependencias entre instrucciones que se ejecuten en diferentes procesadores hay que utilizar funciones de sincronización.

El objetivo del paralelismo es conseguir que los programas se ejecuten más rápido. Aunque la ley de Amdahl nos indica que no es fácil conseguir ir P veces más rápido usando P procesadores, tenemos que intentar que los factores de aceleración sean los más altos posibles. En todo caso, siempre tenemos que considerar la sobrecarga que va a añadir al programa su paralelización: comunicación, sincronización, etc. Por ello, antes de perder el tiempo intentando paralelizar el código, bastante tiempo en algunos casos, es necesario analizar en profundidad el algoritmo que se va a ejecutar, para estimar los beneficios que se van a poder obtener de la paralelización del código. Si los resultados del análisis no son claros, tal vez sea mejor ejecutar el código en serie o quizás merezca la pena diseñar nuevos algoritmos específicos para máquinas paralelas.

En general, el primer paso en la paralelización de código consiste en identificar las tareas que pueden ser ejecutadas en paralelo: bucles, funciones, subprogramas... A continuación, hay que asignar dichas tareas a los procesadores del sistema (planificación, scheduling), y, junto con ello, especificar la sincronización entre procesos y efectuar el reparto de datos, para todo lo cual hay que tener en cuenta el modelo de paralelismo, del sistema. Podemos plantearnos muchas preguntas sobre el proceso global de paralelización: ¿qué tipo de programas son los más adecuados para paralelizar? ¿qué parte de esos programas se ejecuta en paralelo y cuál no? ¿cómo podemos saberlo? ...

▪ Tipos de paralelismo

En función de qué se va a paralelizar, suelen distinguirse dos tipos de paralelismo: paralelismo de datos y paralelismo de función. Se utiliza paralelismo de datos si todos los procesos ejecutan el mismo algoritmo pero sobre datos diferentes; es decir, se reparten los datos y se ejecutan

Page 115: 6.1 INTRODUCCIÓN

8.1 INTRODUCCIÓN ▪ 283 ▪

localmente. El ejemplo más sencillo de este tipo de paralelismo es la ejecución de un bucle largo entre P procesadores cuando todas las iteraciones son independientes entre sí; por ejemplo, este caso (utilizando 3 procesadores):

do i = 1, 300000 A(i) = B(i) + C(i) enddo

P0 do i = 1, 100000 A(i) = B(i) + C(i) enddo P1 do i = 100001, 200000 A(i) = B(i) + C(i) enddo P2 do i = 200001, 300000 A(i) = B(i) + C(i) enddo

Es bastante claro que no hay ningún inconveniente para ejecutar las iteraciones de ese bucle en procesadores diferentes, ya que todas ellas son independientes (recuerda que también es éste el caso más fácil de vectorizar). Los tres procesos son iguales, pero procesan datos diferentes. El paralelismo de datos es muy adecuado para muchas aplicaciones de cálculo, en las que se procesan estructuras de datos muy grandes y muy regulares.

Por otra parte, se utiliza paralelismo de función cuando se reparten los "cálculos" (procedimientos, funciones, subprogramas...) entre los procesadores. Por ejemplo, podemos repartir un programa con cuatro funciones entre dos procesadores de la siguiente manera, haciendo que cada procesador ejecute un trozo del programa original:

Programa

(grafo de dependencias) P0 P1

En muchos casos, se utilizan ambos modelos dentro de un mismo

programa, ejecutándose unos trozos según un modelo y otros según el otro.

F1

F3

F2

F4

F1

F3

F2

F4

Page 116: 6.1 INTRODUCCIÓN

▪ 284 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

También podemos clasificar el paralelismo en función del tamaño de las tareas que se reparten: el tamaño de “grano”. Cuando se reparten funciones, procedimientos, etc., decimos que el paralelismo es de grano grueso (coarse grain); si se reparten, por ejemplo, bucles, de grano medio (medium grain), y si se reparten las iteraciones de los bucles, de grano fino (fine grain). El tipo de paralelismo a emplear depende de la estructura del computador paralelo. El paralelismo de grano grueso es interesante cuando el número de procesadores no es elevado y/o el coste de la comunicación es grande. En cambio, en sistemas con un alto grado de paralelismo suele ser más interesante explotar el paralelismo de grano medio o fino, ya que se ha minimizado el coste de la comunicación y puede llegar a utilizarse un número de procesadores muy alto.

Cuando se trabaja con paralelismo de grano medio o fino, el candidato más adecuado para paralelizar son los bucles, ya que es ahí donde se consume gran parte del tiempo de ejecución. Como ya hemos comentado, tenemos dos maneras de paralelizar bucles: repartir las iteraciones entre los procesadores (paralelismo de datos), o repartir las instrucciones que forman el bucle (paralelismo de función). Veamos un ejemplo.

do i = 0, N-1 A(i) = A(i) + 1 B(i) = B(i) * 2 enddo

repartir las iteraciones entre P procesadores repartir las instrucciones entre dos procesadores

do i = pid, N-1, P A(i) = A(i) + 1 B(i) = B(i) * 2 enddo

do i = 0, N-1 A(i) = A(i) + 1 enddo

do i = 0, N-1 B(i) = B(i) * 2 enddo

pid = 0..P–1 P0 P1 En el primer caso, el nivel de paralelismo que se consigue es elevado si

disponemos de muchos procesadores (en el límite, si disponemos de N procesadores, cada procesador ejecutará una sola iteración); en el segundo caso, en cambio, el tamaño de las tareas (grano) es mayor, pero el paralelismo está limitado por el número de instrucciones del bucle (en el ejemplo, dos). En general, vamos a optar por la primera opción.

Page 117: 6.1 INTRODUCCIÓN

8.1 INTRODUCCIÓN ▪ 285 ▪

▪ Dependencias de datos: sincronización

En los ejemplos que hemos presentado hasta ahora todas las tareas son independientes entre sí, sin ninguna dependencia de datos; es sin duda el caso ideal, ya que la ejecución de los procesos será completamente independiente. En general, sin embargo, existirán dependencias entre las tareas, por lo que no podrán ejecutarse completamente en paralelo. Para respetar la semántica de los programas, las dependencias de datos entre tareas se van a convertir en operaciones de sincronización si dichas tarea tienen que ejecutarse en procesadores diferentes.

En este ejemplo, el procesador P0 no puede ejecutar la función F3 hasta que P1 haya ejecutado F2.

Programa (grafo de dependencias) P0 P1

La necesidad de sincronizar los procesos va a hacer que los tiempos de ejecución sean más altos y, en consecuencia, que los factores de aceleración sean más bajos. Por ello, uno de los objetivos del proceso de paralelización será evitar al máximo la sincronización.

▪ Entorno paralelo

¿Qué podemos esperar de un sistema paralelo? Básicamente lo que ya hemos citado: que genere procesos a partir de un determinado programa serie, que gestione correctamente el espacio de memoria asociado a las variables compartidas, que reparta tareas a los procesadores, y que implemente eficientemente la sincronización y la comunicación.

Suelen distinguirse dos estructuras de programas paralelos: Maestro/Esclavo y SPMD. En el primer caso, un proceso maestro genera en un momento dado P procesos esclavos, que van a ejecutar la parte de código que les corresponde, y que avisarán al maestro cuando terminen. Mientras tanto, el maestro no hace nada salvo esperar la finalización de todos los procesos esclavos (o tal vez se reserve una tarea para él). En el segundo caso (SPMD, Single-Program-Multiple-Data), en cambio, se replica en cada procesador el mismo programa, que se ejecuta de manera descentralizada y

F1

F3

F2

F4

F1

F3

F2

F4

sinc

Page 118: 6.1 INTRODUCCIÓN

▪ 286 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

con los datos correspondientes en cada uno de ellos. Cada proceso utiliza una variable que lo identifica, pid, y se utiliza dicha variable para repartir tareas: if (pid = i) then … else …

Como hemos comentado, en la mayoría de los casos los procesos van a tener que sincronizar su ejecución. Los mecanismos de sincronización son los que ya conocemos: lock y unlock (o semáforos), para entrar en una sección crítica; barreras (barrier), para agrupar un conjunto de procesos en un punto dado de la ejecución; y eventos (wait, para esperar, y post, para activar), para implementar la sincronización punto a punto (modelo productor/consumidor).

En todo caso, ¿de quién es la tarea de explicitar el programa en forma paralela? ¿Hay que rehacer todos los programas serie para poder ejecutarlos en un sistema paralelo? Como en otros casos similares, tenemos dos posibilidades: (a) el programador debe indicar qué ejecutar en paralelo y qué no, utilizando lenguajes de programación específicos (o extensiones de lenguajes comunes); o (b) se realiza automáticamente, en tiempo de compilación, tal como hemos visto en el caso de la vectorización de bucles. Lo mejor sería que se encargara el compilador, ya que de esa manera podríamos reutilizar la infinidad de programas escritos para un solo procesador sin más que recompilarlo para las nuevas condiciones de trabajo. Además, con ello conseguiríamos programas más portables, independientes de una máquina específica. Sin embargo, ese tipo de tareas es complejo y todavía no se dispone de herramientas automáticas de gran eficiencia para paralelizar código. Por otra parte, muchos de los algoritmos desarrollados para ser ejecutados en serie no son adecuados para un sistema paralelo, por lo que es necesario desarrollar nuevos algoritmos que puedan explotar las ventajas de este tipo de sistemas. Así pues, será el programador el que analice el código y decida qué y cómo se debe ejecutar en paralelo.

Se han desarrollado muchos lenguajes específicos para programar en paralelo, en función del entorno en el que hay que trabajar (memoria compartida, memoria distribuida...), pero lo más habitual es utilizar una serie de APIs estándar (OpenMP, MPI…); también se han adaptado lenguajes clásicos a estas nuevas tareas (Fortran y similares)41.

41 En este capítulo vamos a considerar principalmente el modelo de memoria compartida, para el que

existe "de facto" un estándar de programación, OpenMP, en el que se aplican todos los conceptos que vamos a analizar en los próximos apartados. También existe un estándar de programación para el

Page 119: 6.1 INTRODUCCIÓN

8.1 INTRODUCCIÓN ▪ 287 ▪

Junto a ello, existen diferentes herramientas de ayuda: schedulers, analizadores de rendimiento, debuggers, etc., que no vamos a presentar.

En los próximos apartados vamos a analizar algunos de los problemas planteados, básicamente la paralelización de bucles, si es posible sin tener que sincronizar las iteraciones. Por último, estudiaremos las principales estrategias de reparto o planificación de las iteraciones de un bucle entre P procesadores.

8.1.1 Ideas básicas sobre paralelización de bucles

Queremos paralelizar un bucle repartiendo sus iteraciones entre los procesadores del sistema paralelo. Pues bien, siempre es posible ejecutar un bucle entre P procesadores, pero no siempre es adecuado hacerlo, ya que el coste de comunicación/sincronización puede superar los hipotéticos beneficios de usar P procesadores; en más de un caso, la mejor solución es usar un único procesador.

La pérdida de eficiencia proviene de dos fuentes. Por un lado, las dependencias entre instrucciones de distancia mayor que 0, es decir, entre iteraciones, ya que habrá que sincronizarlas si se van a ejecutar en procesadores diferentes. Y, por otro lado, el reparto de datos; si se trata de una máquina de memoria compartida, tendremos problemas con la falsa compartición de datos y, en función de la arquitectura, con la capacidad de transmisión del bus o la latencia de los mensajes.

Analicemos el efecto de las dependencias de datos mediante unos ejemplos.

(1)

do i = 0, N-1 (1) A(i) = A(i) + 1 (2) B(i) = A(i) * 2 enddo

Se trata del caso más simple, ya que todas las iteraciones son

independientes (la dependencia 1 → 2 es de distancia 0). El bucle puede paralelizarse repartiendo las iteraciones como se desee entre los procesadores del sistema (por ejemplo, una iteración por procesador).

modelo de memoria distribuida, MPI, que define un conjunto amplio de funciones de comunicación (send/receive) para efectuar la comunicación entre procesos mediante paso de mensajes.

P0 P1 P2 P3 … en paralelo

i=0 1 2 3 …

Page 120: 6.1 INTRODUCCIÓN

▪ 288 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

(2a)

do i = 0, N-2 (1) A(i) = B(i) + 1 (1) B(i+1) = A(i) * 2 enddo

En este segundo ejemplo, en cambio, el bucle tiene, además de la dependencia de distancia 0, una dependencia 2 → 1 a distancia 1, por lo que los procesadores no pueden trabajar de manera independiente: P1 tiene que esperar a que acabe P0 para poder ejecutar su iteración, P2 a que acabe P1, etc. El reparto de una iteración a cada procesador es una mala solución, ya que el ciclo de dependencias 1 → 2 → 1 hace que el código se ejecute peor que si lo hiciéramos en un solo procesador: el bucle de este ejemplo hay que ejecutarlo en serie.

(2b)

do i = 0, N-3 A(i+2) = A(i) + 1 enddo

El ejemplo más simple de ciclo de dependencias que no se puede paralelizar es una recurrencia (dependencia consigo mismo) de distancia d = 1. Sin embargo, si la recurrencia fuera de distancia d > 1, tendríamos una posibilidad de ejecutar en paralelo, usando un número reducido de procesadores. Por ejemplo, para el caso de este ejemplo con d = 2, el procesador P0 puede ejecutar las iteraciones pares (0, 2...) y el P1 las impares (1, 3...). Aunque sea de manera limitada, podemos aprovechar el paralelismo para intentar ejecutar el bucle dos veces más rápido (sin contar con otros problemas).

(3) Los bucles de dos o más dimensiones pueden paralelizarse en

cualquiera de ellas y conviene escoger la mejor posibilidad en función de las dependencias. El bucle de este ejemplo no tiene ningún tipo de dependencia, por lo que puede paralelizarse en cualquier dimensión.

do i = 0, N-1 do j = 0, M-1 A(i,j) = A(i,j) + 1 enddo enddo

P0 P1 P2 P3 … en paralelo ?

i=0 1 2 3 …

P0 P1 P0 P1 P0

i=0 1 2 3 …

j=0 1 2 3 … i=0

1

2

Page 121: 6.1 INTRODUCCIÓN

8.1 INTRODUCCIÓN ▪ 289 ▪

dopar i = 0, N-1 do j = 0, M-1 A(i,j) = A(i,j) + 1 enddo enddopar

do i = 0, N-1 dopar j = 0, M-1 A(i,j) = A(i,j) + 1 enddopar enddo

Si se paraleliza el bucle externo i, se generan N tareas en paralelo, en

las que se ejecuta en cada una de ellas un bucle j completo. En cambio, si se paraleliza el bucle interno j, se generan N tareas serie (una por fila) y en cada una se ejecutarán M tareas en paralelo.

(4) En el caso más general, existirán dependencias entre las iteraciones y

habrá que sincronizar su ejecución, aunque se debe reducir al máximo su uso. Por ejemplo, el primer intento de paralelización del siguiente bucle no parece muy adecuado; se paraleliza el bucle interno (j) pero, dado que se trata de una recurrencia de distancia (0, 1), es necesario sincronizar los procesadores Pk y Pk-1, ya que Pk necesita los datos producidos por Pk-1. El segundo intento es más afortunado: se ejecutan en paralelo las iteraciones del bucle externo i, y a cada procesador se le ha asignado un bucle j completo; de esa manera, las dependencias quedan dentro de cada procesador.

do i = 0, N-1 dopar j = 1, M-1 A(i,j) = A(i,j-1) + 1 enddopar enddo

?

j=0 1 2 3 …

i=0

1

2

P0

P1

P2

en paralelo

i=0

1

2

j=0 1 2 3 …

P0 P1 P2 P3

paral.

j=1 2 3 4 …

P0 P1 P2 P3

Page 122: 6.1 INTRODUCCIÓN

▪ 290 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

dopar i = 0, N-1 do j = 1, M-1 A(i,j) = A(i,j-1) + 1 enddo enddopar

A continuación vamos a estudiar las estrategias más eficientes de paralelización de bucles y la asignación de iteraciones a los procesadores, en sistemas paralelos de memoria compartida.

8.2 ESTRUCTURAS BÁSICAS PARA EXPRESAR EL

PARALELISMO DE LOS BUCLES

Como ya hemos comentado, los bucles son estructuras adecuadas para poder aplicar un alto nivel de paralelismo: cálculos relativamente sencillos que se ejecutan una y otra vez sobre datos diferentes. Por ello, se han desarrollado diferentes maneras de indicar el tipo de ejecución paralela más adecuada para cada bucle (normalmente para Fortran o C).

Previo a la ejecución en paralelo, es necesario realizar el correspondiente análisis de dependencias entre instrucciones, ya que la ejecución en paralelo implica un cambio en el orden de ejecución secuencial, tras el cual podremos decidir qué partes se pueden ejecutar en paralelo, la sincronización que hay que añadir, etc. El análisis de dependencias es el que ya conocemos (grafos de dependencias y espacio de iteraciones, distancia de las dependencias, test MCD, eliminación de las variables de inducción...), aunque las decisiones a tomar van a ser diferentes. Tras ello, intentaremos aplicar algunas optimizaciones que permitan aliviar el coste añadido por la sincronización. Veamos las tres estructuras básicas que se utilizan para indicar diferentes niveles de paralelización de un bucle.

8.2.1 Bucles sin dependencias entre iteraciones: bucles doall

Es el caso más sencillo. Si todas las dependencias del bucle son de distancia d = 0 (es decir, no hay dependencias entre iteraciones), entonces las iteraciones del bucle pueden ejecutarse en cualquier orden en

j=1 2 3 4 …

P0

P1

P2

Page 123: 6.1 INTRODUCCIÓN

8.2 ESTRUCTURAS BÁSICAS PARA EXPRESAR EL PARALELISMO DE LOS BUCLES ▪ 291 ▪

cualquier procesador; para indicar esa situación suele utilizarse un bucle doall. Por ejemplo:

do i = 0, N-1 (1) C(i) = C(i) * C(i) (2) A(i) = C(i) + B(i) (3) D(i) = C(i) / A(i) enddo

doall i = 0, N-1 C(i) = C(i) * C(i) A(i) = C(i) + B(i) D(i) = C(i) / A(i) enddoall

Las iteraciones del bucle pueden asignarse a los procesadores con total libertad, ya que no importa en qué orden se ejecuten. Al ejecutarse un bucle doall, se generarán múltiples tareas independientes, para ser ejecutadas en cualquier procesador y en cualquier orden. Por ejemplo:

A1 doall i = 1, 5 A2 enddoall A3

En general, el final del bucle doall lleva implícita una barrera de

sincronización, para sincronizar todos los procesos (o para devolver control al thread principal). En algunos lenguajes, el programador debe decidir si existe o no esa barrera final.

En resumen, un bucle doall puede ejecutarse como se desee y en el número de procesadores de que se disponga. Sin considerar otros problemas (reparto de datos, compartición falsa, uso de la red de comunicación…), en una máquina de P procesadores el bucle se ejecutará P veces más rápido.

8.2.2 Bucles con dependencias entre iteraciones

Cuando existen dependencias entre iteraciones no es posible paralelizar el bucle de cualquier manera, por lo que tenemos que analizar las dependencias para decidir, en función del grafo de dependencias, qué hacer. En general,

C, 0

C, 0

1

2

3

A, 0

A2 A2 A2 A2 A2

A3

A1

Page 124: 6.1 INTRODUCCIÓN

▪ 292 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

cuando en una iteración se necesitan los resultados producidos en una iteración anterior del bucle, es necesario introducir en el código funciones de sincronización, globales —barreras— o punto a punto —mediante eventos—. Veamos las dos alternativas principales.

8.2.2.1 Bucles forall (sincronización global)

Analicemos este bucle:

do i = 1, N-1 (1) C(i) = C(i) * C(i) (2) A(i) = C(i) + B(i) (2) D(i) = C(i-1) / A(i) enddo

1 1 1 1... 2 2 2 2... 3 3 3 3...

La dependencia entre las instrucciones 1 y 3 es de distancia d = 1, es decir, a una iteración posterior. Supongamos que se han repartido las iteraciones entre N–1 procesadores, una a una, para ejecutarse en paralelo. En ese caso, el segundo procesador no podrá ejecutar la instrucción 3 hasta que el primero no haya ejecutado la instrucción 1 y le llegue el “permiso” correspondiente. Es decir, necesitamos sincronizar la ejecución en los procesadores (iteraciones): no se puede paralelizar el bucle “sin más” (utilizando un doall).

Si todas las dependencias entre instrucciones van hacia “adelante” en el grafo, la sincronización puede realizarse simplemente con una barrera:

forall i = 1, N-1 C(i) = C(i) * C(i) A(i) = C(i) + B(i) BARRERA(...) D(i) = C(i-1) / A(i) endforall

1 1 1 1... 2 2 2 2... 3 3 3 3...

Todos los procesadores ejecutarán las instrucciones 1 y 2 en paralelo, y entrarán en la barrera de sincronización. El último procesador que llegue a ese punto abrirá la barrera, con lo que todos pasarán a ejecutar la instrucción 3, ya sin ningún problema, puesto que se han solucionado todas las dependencias 1i–1→3i. Este tipo de bucle se suele denominar forall (o doall + barrier).

C, 0

C, 1

1

2

3

A, 0

barrera

Page 125: 6.1 INTRODUCCIÓN

8.2 ESTRUCTURAS BÁSICAS PARA EXPRESAR EL PARALELISMO DE LOS BUCLES ▪ 293 ▪

Básicamente, hemos dividido el bucle en dos (en general, n) “segmentos” —grupos de instrucciones consecutivas sin dependencias entre iteraciones y que pueden ejecutarse en paralelo sin problemas—, y ejecutamos cada segmento como un doall, colocando en medio una barrera de sincronización.

forall i = 1, N-1 C(i) = C(i) * C(i) A(i) = C(i) + B(i) BARRERA(...) D(i) = C(i-1) / A(i) endforall

=

doall i = 1, N-1 C(i) = C(i) * C(i) A(i) = C(i) + B(i) enddoall

[BARRERA(...)]

doall i = 1, N-1 D(i) = C(i-1) / A(i) enddoall

(en muchos casos, la implementación del bucle doall lleva implícita una barrera de sincronización al final, por lo que sobraría la barrera entre ambos doall.)

Aunque la barrera de sincronización global resuelve el problema de las dependencias del bucle anterior, añade una sincronización mayor de la necesaria; por ejemplo, en el bucle anterior, no es necesario que hayan acabado todas las instrucciones 1 para poder ejecutar la 2; bastaría con que la hubiera ejecutado el procesador Pk-2 para que Pk pudiera pasar a ejecutar su instrucción 3. De todas maneras, es un mecanismo de sincronización simple y fácil de implementar.

8.2.2.2 Bucles doacross (sincronización punto a punto)

En el bucle anterior, todas las dependencias iban hacia adelante en el grafo de dependencias. En un caso más general, en cambio, las dependencias irán hacia atrás y hacia adelante, formado ciclos. Aunque los ciclos de dependencias siempre dan problemas (por ejemplo, no pueden vectorizarse) en muchas ocasiones podemos encontrar una forma de ejecutarlos en paralelo (tal vez de manera limitada).Veamos un ejemplo.

do i = 2, N-2 (1) A(i) = B(i-2) + 1 (2) B(i+1) = A(i-2) * 2 enddo

i=2 3 4 5 6 7

1 1 1 2 2 2

1 1 1 2 2 2

1

2

A, 2 B, 3

Sinc.

Page 126: 6.1 INTRODUCCIÓN

▪ 294 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

El bucle tiene dos dependencias, una a distancia 2 y otra a distancia 3. Por ejemplo, no es posible ejecutar la instrucción 2 de la iteración i = 4 antes de que acabe la instrucción 1 de la iteración i = 2; de la misma manera, la instrucción 1 de la iteración i = 5 tiene que esperar a que se ejecute la instrucción 2 de la iteración i = 2. Por tanto, si queremos ejecutar las iteraciones en paralelo, tenemos que sincronizar la ejecución de las instrucciones, para cada dependencia de distancia d > 0. En este caso, la sincronización tiene que ser punto a punto, mediante el uso de eventos. Un bucle de esas características suele indicarse como doacross.

Para la sincronización se utilizan vectores de eventos o flags, un vector por cada dependencia y un elemento por cada iteración. Las funciones de sincronización con esos vectores son las dos que ya conocemos:

post(ve,i) → activar el elemento i del vector de eventos ve ve(i) = 1;

wait(ve,i) → esperar a que se active el elemento i de ve while (ve(i) == 0) { };

El bucle anterior deberíamos escribirlo así para su ejecución en paralelo:

doacross i = 2, N-2 wait(vB,i-3) ; esperar a que finalice la instr. 2 de hace 3 iteraciones (1) A(i) = B(i-2) + 1 post(vA,i) ; indicar que ha acabado la instr. 1 de la iteración i wait(vA,i-2) ; esperar a que finalice la instr. 1 de hace 2 iteraciones (2) B(i+1) = A(i-2) * 2 post(vB,i) ; indicar que ha acabado la instr. 2 de la iteración i enddoacross

Dada la dependencia en el vector B, la instrucción 1 de la iteración i debe ejecutarse tras la instrucción 2 de la iteración i–3. Para sincronizar dicha dependencia hemos utilizado el vector vB —wait(vB,i-3)—. Por ello, tras ejecutar la instrucción 2 de la iteración i, tenemos que indicar en vB que dicha tarea ya ha finalizado —post(vB,i)—. La dependencia en el vector A se sincroniza de la misma manera; tras ejecutar la instrucción 1, activamos el elemento i del vector vA, y antes de ejecutar la 2, esperamos a que haya terminado la instrucción 1 de la iteración i–2.

Ten en cuenta que las iteraciones se ejecutan en paralelo, en procesadores diferentes, por lo que aunque haya finalizado la instrucción 1 de la iteración

Page 127: 6.1 INTRODUCCIÓN

8.2 ESTRUCTURAS BÁSICAS PARA EXPRESAR EL PARALELISMO DE LOS BUCLES ▪ 295 ▪

i, bien podría ser que todavía no se hubiera ejecutado dicha instrucción en anteriores iteraciones.

Con un bucle doacross no se pueden conseguir niveles altos de paralelismo, ya que lo impiden las dependencias. En el ejemplo anterior, aunque tengamos P procesadores para ejecutar el bucle, nunca utilizaremos más de 3, como puede comprobarse en la figura anterior (más adelante veremos otras opciones). Además, el factor de aceleración será menor que 3, ya que en cada procesador, además de las instrucciones del bucle, hay que ejecutar ahora 4 funciones de sincronización. La relación entre el coste de ejecución de las funciones de sincronización y las instrucciones originales del bucle nos dirá si merece la pena la ejecución en paralelo42.

Si decidimos utilizar sólo 3 procesadores para ejecutar el bucle anterior, se simplifica un poco la sincronización, ya que algunas dependencias van a quedar dentro de cada procesador; en este ejemplo, no es necesario sincronizar la dependencia 2 → 1 del vector B.

P0 P1 P2 12 13 14

22 23 24

15 16 17

25 26 27 ...

doacross i = 2, N-2, 3 A(i) = B(i-2) + 1 post(vA,i)

wait(vA,i-2) B(i+1) = A(i-2) * 2 enddoacross

En resumen: un bucle doacross utiliza sincronización punto a punto para resolver las dependencias entre instrucciones. En general, ofrece un nivel limitado de paralelismo, y habrá que analizar cada caso considerando todos los costes del proceso.

El caso peor de paralelización es el bucle que contiene un ciclo de dependencias cuyas distancias suman 1 (en general, un ciclo como éste: 1i–2i–3i → 1i+1–2i+1–3i+1 → 1i+2–...). Por ejemplo,

42 Únicamente estamos analizando cómo especificar un bucle paralelo, no la eficiencia de dicha

ejecución. De hecho, ese bucle no es muy adecuado para ser ejecutado en paralelo, ya que el coste de la sincronización va a superar con creces el de las instrucciones del bucle; además, habría que considerar otros factores, tales como el uso de la cache. Un poco más adelante analizaremos cuestiones relacionadas con la eficiencia.

Page 128: 6.1 INTRODUCCIÓN

▪ 296 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

do i = 0, N-2 (1) A(i) = B(i) + C(i) (2) B(i+1) = A(i) / 2 enddo

doacross i = 0, N-2 wait(vB,i-1) (1) A(i) = B(i) + C(i) ?? (2) B(i+1) = A(i) / 2 post(vB,i) enddoacross

En este caso, nunca tendremos más de un procesador trabajando

simultáneamente (el procesador i debe esperar a los resultados del i–1), y, por tanto, el tiempo de ejecución será siempre peor que en el caso serie, ya que, además del código del bucle, habrá que ejecutar funciones de sincronización. Salvo que existan otras razones, deberemos ejecutar el bucle en un solo procesador.

Veamos algunos ejemplos más. ▪ Ejemplo 1

do i = 1, N-1 (1) A(i) = B(i) + C(i) (2) C(i) = A(i-1) * 2 (3) D(i) = A(i) + A(i-1) enddo

doacross i = 1, N-1 (1) A(i) = B(i) + C(i) post(vA,i) wait(vA,i-1) (2) C(i) = A(i-1) * 2 (3) D(i) = A(i) + A(i-1) enddoacross

Aunque existen dos dependencias de distancia 1, es claro que basta con sincronizar la dependencia 1i–1 → 2i (wait), ya que con eso también se sincroniza la dependencia 1i–1 → 3i.

1

2

A, 0 B, 1

1 2 p

w 1 2 p

w 1 2

1

2

3

A, 1

A, 1 A, 0

C, 0

Page 129: 6.1 INTRODUCCIÓN

8.2 ESTRUCTURAS BÁSICAS PARA EXPRESAR EL PARALELISMO DE LOS BUCLES ▪ 297 ▪

Tendríamos una segunda opción de paralelización en el bucle anterior; ya que todas las dependencias son hacia adelante en el grafo de dependencias, podemos usar una barrera de sincronización.

forall i = 1, N-1 (1) A(i) = B(i) + C(i) BARRERA(...) (2) C(i) = A(i-1) * 2 (3) D(i) = A(i) + A(i-1) endforall

▪ Ejemplo 2

do i = 2, N-2 (1) A(i) = B(i) + E(i-2) (2) C(i) = A(i-1) + C(i) (3) D(i) = A(i) * 2 (4) E(i+1) = D(i) - 1 enddo

doacross i = 2, N-2 wait(vE,i-3) (1) A(i) = B(i) + E(i-2) post(vA,i) wait(vA,i-1) (2) C(i) = A(i-1) + C(i) (3) D(i) = A(i) * 2 (4) E(i+1) = D(i) - 1 post(vE,i) enddoacross

En este bucle tenemos que utilizar sincronización punto a punto; no es posible usar una barrera, ya que no todas las dependencias van hacia adelante.

▪ Ejemplo 3

do i = 0, N-2 do j = 0, N-2 A(i+1,j+1) = A(i+1,j) + 1 B(i,j) = A(i,j) enddo enddo

1

2

3

A, 1

E, 3

A, 0

4

D, 0

i

j

Page 130: 6.1 INTRODUCCIÓN

▪ 298 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

doacross i = 0, N-2 do j = 0, N-2 A(i+1,j+1) = A(i+1,j) + 1 post(vA,i,j) wait(vA,i-1,j-1) B(i,j) = A(i,j) enddo endoacross

Tal como aparece en el grafo del espacio de iteraciones, es necesario sincronizar, ya sea por filas o por columnas. Dado que los vectores son de dos dimensiones, también lo son los vectores de eventos. (Hay más opciones para paralelizar ese bucle, en este momento sólo queremos ver cómo usar los vectores de eventos.)

8.2.3 Efecto de las antidependencias y de las dependencias de salida

Ya sabemos que las antidependencias no tienen un efecto muy grave al vectorizar un bucle: es suficiente con leer los vectores en el momento adecuado. En cambio, su efecto en la paralelización de un bucle es mayor. Veamos un ejemplo (un caso similar puede plantearse con dependencias de salida).

do i = 0, N-3 (1) A(i) = B(i+2) / A(i) (2) B(i) = B(i) + C(i) enddo

doacross i = 0, N-3 (1) A(i) = B(i+2) / A(i) post(vB,i) wait(vB,i-2) (2) B(i) = B(i) + C(i) enddoacross

load B <post> load A / store A load C load B + <wait> store B

La antidependencia del bucle se ha tratado como una dependencia normal, mediante funciones de sincronización wait/post (o una barrera). Un análisis más fino indica, en cambio, que lo que hay que sincronizar es que la lectura de B(i+2) en la primera instrucción de la iteración i vaya antes que

1

2

B, 2

Page 131: 6.1 INTRODUCCIÓN

8.2 ESTRUCTURAS BÁSICAS PARA EXPRESAR EL PARALELISMO DE LOS BUCLES ▪ 299 ▪

la escritura de B(i) en la segunda instrucción de la iteración i+2. Esa sincronización aparece más clara en el código en lenguaje máquina: la función post se ejecuta en cuanto se efectúa la lectura, e, igualmente, la función wait se ejecuta justo antes de la escritura. Por tanto, aunque hay que sincronizar las dos instrucciones, es más flexible (da más tiempo) que en el caso de una dependencia normal.

Si se quiere, también se pueden utilizar variables auxiliares para tratar las antidependencias (igual que en el caso de la vectorización). Por ejemplo,

do i = 0, N-2 A(i) = A(i+1) + 1 enddo

doacross i = 0, N-2 wait(vA,i-1) A(i) = A(i+1) + 1 ? post(vA,i) enddoacross

do i = 0, N-2 B(i) = A(i+1) A(i) = B(i) + 1 enddo

doacross i = 0, N-2 B(i) = A(i+1) ld A+1 / [st B]

post(vA,i) <post>

wait(vA,i-1) <wait> A(i) = B(i) + 1 [ld B] / add / st A enddoacross

Como en casos similares, no necesitaremos utilizar la variable auxiliar B si disponemos de sitio en los registros, donde dejaremos el resultado de la lectura para ser utilizado posteriormente.

Las dependencias de salida se tratan de manera similar; en este caso, la sincronización es necesaria para asegurar el orden de las escrituras.

8.2.4 Atención con las instrucciones if

Hay que ser muy cuidadoso con el uso de las funciones de sincronización. Observa este caso:

1 A, 1

1

2

B, 0 A, 1

Page 132: 6.1 INTRODUCCIÓN

▪ 300 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

do i = 1, N-1 (1) if B(i)>0 then (2) A(i) = A(i) + B(i) (3) C(i) = A(i-1) / 2 endif enddo

doacross i = 1, N-1 (1) if B(i)>0 then (2) A(i) = A(i) + B(i) post(vA,i) wait(vA,i-1) ?? (3) C(i) = A(i-1) / 2 endif enddoacross

Hemos sincronizado las instrucciones 2 y 3 para ejecutar el bucle en paralelo; pero, ¿qué ocurre si no se cumple la condición del if en la iteración i pero sí en la i+1? Pues que la instrucción 3 de la iteración i+1 se quedará esperando (wait) a que se active el flag de la iteración i, lo que no va a ocurrir nunca. El programa está bloqueado (hay un deadlock de sincronización). Para evitar el problema basta con añadir la función post en ambas ramas, then y else (o al final):

doacross i = 1, N-1 (1) if B(i)>0 then (2) A(i) = A(i) + B(i) post(vA,i) wait(vA,i-1) (3) C(i) = A(i-1) / 2 else post(vA,i) endif enddoacross

8.3 IMPLEMENTACIÓN DE LA SINCRONIZACIÓN

Para llevar a cabo la sincronización entre los procesadores hemos utilizado hasta ahora vectores de eventos (junto a las funciones wait/post). Se trata de un mecanismo sencillo y flexible de sincronización, pero que puede presentar algunos problemas:

• Hay que inicializar los vectores de eventos. Si la distancia de la dependencia es k, no hay que sincronizar las primeras k iteraciones,

1

2

3

A, 1

Page 133: 6.1 INTRODUCCIÓN

8.3 IMPLEMENTACIÓN DE LA SINCRONIZACIÓN ▪ 301 ▪

para lo que basta inicializar a 1 los primeros k elementos del correspondiente vector de eventos, y el resto a 0 (o, lo que es equivalente, ejecutar la sincronización de esta manera: if (i>k) wait(...).

• Necesitan mucho espacio en memoria: un bit por iteración, o por proceso/procesador.

• Atención a la falsa compartición; para minimizar el número de invalidaciones, conviene que los elementos de un vector de eventos estén en bloques diferentes.

Por ello, en ocasiones es preferible utilizar otro tipo de solución.

8.3.1 Sincronización mediante contadores

Para reducir el espacio de memoria necesario para la sincronización pueden utilizarse contadores en lugar de vectores de eventos. Un contador de sincronización indica hasta qué iteración ha llegado la ejecución de un determinado bucle. Por ejemplo, si cA = 5, la instrucción que utiliza dicho contador de sincronización ha llegado ya a la iteración 5, y todavía no se ha completado la 6. Del resto de iteraciones no tenemos información, por lo que hay que considerarlas como no terminadas. Como en el caso de los vectores de eventos, el procesador comprobará el valor del contador correspondiente antes de ejecutar una instrucción que dependa de una iteración anterior.

El valor de un contador de sincronización se va incrementado en orden estricto. Aunque se hayan terminado los cálculos de la iteración i, no se marcará el contador como cA = i hasta que no terminen los cálculos de la iteración i–1 y, por tanto, veamos que cA = i–1. De no hacerse así, interpretaríamos mal el valor del contador: se ha llegado hasta la iteración i. Por tanto, al terminar la ejecución de una instrucción que tengamos que sincronizar, esperaremos a que la ejecución haya llegado hasta la iteración anterior, y entonces indicaremos que también han terminado los cálculos de nuestra iteración: los puntos de sincronización quedan así estrictamente ordenados.

Básicamente, lo que hacemos es codificar los valores del vector de eventos (binarios, 1/0) en una sola variable escalar.

Page 134: 6.1 INTRODUCCIÓN

▪ 302 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

vector de eventos vA contador de sincronización

1 1 1 1 1 0 1 0 ... → cA = 5 vA1 vA2 vA3 vA4 vA5 vA6 vA7 vA8

cA = 5 → han finalizado todas las iteraciones hasta la 5; las siguiente no (no sabemos).

Vamos a controlar los contadores de sincronización mediante estas dos funciones:

post(c,i) → se asigna al contador el valor de la iteración, c = i wait(c,i) → se espera mientras sea c < i

Veamos en un par de ejemplos cómo utilizar los contadores de sincronización.

▪ Ejemplo 1

do i = 2, N-1 A(i) = B(i) + C(i) D(i) = A(i) + A(i-2) C(i) = 2 * A(i) enddo

doacross i = 2, N-1 A(i) = B(i) + C(i) post(vA,i) wait(vA,i-2) D(i) = A(i) + A(i-2) C(i) = 2 * A(i) enddoacross

doacross i = 2, N-1 A(i) = B(i) + C(i) wait(cA,i-1) c < i-1?

post(cA,i) c = i [wait(CA,i-2)] no se necesita D(i) = A(i) + A(i-2) C(i) = 2 * A(i) enddoacross

sincronización mediante vectores de eventos sincronización mediante contadores

▪ Ejemplo 2

do i = 3, N-1 B(i) = A(i-2) - 1 A(i) = B(i) + 1 C(i) = A(i) + B(i-3) enddo

1

2

3

A, 0 A, 0

C, 0

A, 2

1

2

3

A, 0

B, 0 A, 2

B, 3

Page 135: 6.1 INTRODUCCIÓN

8.3 IMPLEMENTACIÓN DE LA SINCRONIZACIÓN ▪ 303 ▪

doacross i = 3, N-1 wait(vA,i-2) B(i) = A(i-2) - 1 post(vB,i) A(i) = B(i) + 1 post(vA,i) wait(vB,i-3) C(i) = A(i) + B(i-3) enddoacross s

doacross i = 3, N-1 wait(cA,i-2) B(i) = A(i-2) - 1 wait(cB,i-1) post(cB,i) A(i) = B(i) + 1 wait(cA,i-1) post(cA,i) C(i) = A(i) + B(i-3) enddoacross

sincronización mediante vectores de eventos sincronización mediante contadores Utilizamos un contador para sincronizar cada dependencia, y esperamos a

que c = i–1 antes de hacer c = i. En comparación con el uso de vectores de eventos hemos perdido flexibilidad, pero utilizamos menos memoria y la iniciación es más simple.

8.3.2 Un único contador por procesador

En los casos anteriores hemos utilizado un contador por cada dependencia. En realidad, bastaría con utilizar un único contador por procesador (y no uno por dependencia). Veamos un ejemplo. Hay que ejecutar en paralelo el siguiente bucle:

do i = 0, N-6 (1) A(i+2) = B(i+1) - 1 (2) B(i+5) = A(i+1) * 3 (3) C(i) = A(i) + B(i) enddo

Dado el ciclo de dependencias entre las instrucciones 1 y 2, lo más

adecuado es utilizar sólo 4 procesadores. Repartimos las iteraciones a los procesadores tal como aparece en la figura, con lo que sería suficiente sincronizar los procesadores, no las instrucciones, mediante un contador cPi por procesador, de esta manera (por ejemplo, para P2):

1

2

3

B, 5

A, 1 B, 4

A, 2

Page 136: 6.1 INTRODUCCIÓN

▪ 304 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

do i = 2, N-6, 4 wait(cP1,i-1) (1) A(i+2) = B(i+1) – 1

wait(cP0,i-2) (2) B(i+5) = A(i+1) * 3 post(cP2,i)

(3) C(i) = A(i) + B(i) enddo

Mediante los contadores cP1 y cP0 hemos sincronizado las iteraciones 9-

10 y 8-10 (las instrucciones 1i-1 → 2i y 1i-2 → 3i), con lo que no es necesario sincronizar las iteraciones 5-10 (instrucciones 2i-5 → 3i), ya que la iteración 5 se ejecutará siempre antes que la 9. De la misma manera, no es necesario sincronizar las instrucciones 2i-4 → 1, ya que se ejecutarán en el mismo procesador. Al final, actualizamos el valor del contador de sincronización cP2, para indicar que, en lo que a las dependencias se refiere, esa iteración ya ha finalizado.

8.4 OPTIMIZACIONES PARA PARALELIZAR BUCLES DE MANERA EFICIENTE

Para respetar las dependencias de datos entre iteraciones es necesario añadir funciones de sincronización al bucle, lo cual conlleva una pérdida de eficiencia. Por ello, debemos limitar el uso de estas funciones al mínimo posible, eliminando las dependencias en todos aquellos casos en que no sean esenciales, o transformado el bucle original en otro equivalente pero con menos dependencias. Analicemos las optimizaciones más representativas.

8.4.1 Eliminación del efecto de las dependencias que no son esenciales (variables escalares, sumas parciales...)

Cuando utilizamos variables escalares dentro de un bucle se generan todo tipo de dependencias iteración a iteración. Dichas dependencias desaparecen si convertimos la variable escalar en un vector (un elemento por iteración) o, lo que es equivalente en este entorno, si utilizamos en cada procesador una variable privada, diferente en cada uno de ellos.

P0 P1 P2 P3

0 1 2 3

4 5 6 7

8 9 10 11

12 13 …

A,1 A,2

Page 137: 6.1 INTRODUCCIÓN

8.4 OPTIMIZACIONES PARA PARALELIZAR BUCLES DE MANERA EFICIENTE ▪ 305 ▪

En los dos ejemplos siguientes vemos una aplicación de esta estrategia: expansión escalar y tratamiento de una recurrencia (sumas parciales). El segundo ejemplo es un caso de producto escalar de dos vectores, y es conveniente detectar ese tipo de operaciones para darles una solución adecuada, es decir, reconocer ciertos patrones de cálculo y aplicarles soluciones estándar ya optimizadas (funciones de biblioteca). El compilador (o el programador) introducirá dicho código optimizado (en función de los recursos hardware y software de la máquina) para ejecutar dichas operaciones.

▪ Expansión escalar

do i = 0, N-1 X = A(i)*A(i) + B(i)*B(i) C(i) = SQRT(X) D(i) = X - 1 enddo

doall i = 0, N-1 X(i) = A(i)*A(i) + B(i)*B(i) C(i) = SQRT(X(i)) D(i) = X(i) - 1 enddoall o declarar X como variable privada (una variable diferente en cada procesador)

▪ Sumas parciales do i = 0, N-1 C(i) = A(i) * B(i) SUMA = SUMA + C(i) enddo

(P procesadores, pid: 0..P-1) do i = pid, N-1, P C(i) = A(i) * B(i) SUM = SUM + C(i) enddo

LOCK(C) SUMA = SUMA + SUM UNLOCK(C) SUM: variable privada de cada procesador. C: cerrojo.

8.4.2 Fisión de bucles

El bucle se divide en varios trozos, para poder aplicar a cada uno de ellos la estrategia de paralelización más conveniente Se trata de una técnica básica, y hay que utilizarla, por ejemplo, para poder ejecutar parte del bucle en paralelo cuando otra parte debe ser ejecutada en serie. El grafo de dependencias se divide en bloques (que se conocen como bloques ∏), subconjuntos de los nodos del grafo de dependencias en los que no existen ciclos de dependencias. Veamos un ejemplo.

Page 138: 6.1 INTRODUCCIÓN

▪ 306 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

do i = 1, N-2 (1) A(i) = B(i) (2) C(i) = A(i) + B(i-1) (3) D(i) = C(i+1) (4) B(i) = C(i) * 2 enddo

doall i = 1, N-2 ; 1 y 3 se pueden ejecutar en paralelo (1) A(i) = B(i) (3) D(i) = C(i+1) enddoall

[barrera]

do i = 1, N-2 ; este bloque hay que ejecutarlo en serie (2) C(i) = A(i) + B(i-1) (4) B(i) = C(i) * 2 enddo

Merece la pena que hagamos un pequeño cálculo. ¿Cuántas veces más

rápido será el bucle paralelo del ejemplo? Para simplificar, supongamos que el tiempo de ejecución de una instrucción es T = 1. El tiempo de ejecución del bucle en serie es, por tanto, 4N. En cambio, el tiempo de ejecución del nuevo bucle en paralelo será: 2N/P + 2N + X, donde P es el número de procesadores y X la latencia de la barrera de sincronización. Así pues, y sin considerar X, el factor de aceleración será siempre menor que 2.

8.4.3 Ordenación de las dependencias

Siempre que sea posible, las dependencias que van hacia atrás en el grafo deben convertirse en dependencias hacia adelante, simplemente reordenando el código original. De no hacerlo así, el resultado sería una paralelización muy poco eficiente. Por ejemplo:

Núm. procesadores Núm. procesadores

T. d

e ej

ecuc

ión

Fac.

de

acel

erac

ión

1 2 3 4 5 6

4N

2N

2

1

1,5

1 2 3 4 5 6

1

2

3

4

A, 0

C, 0

B, 0

B, 1

C, 1

1 3

2

4

bloques Π

Page 139: 6.1 INTRODUCCIÓN

8.4 OPTIMIZACIONES PARA PARALELIZAR BUCLES DE MANERA EFICIENTE ▪ 307 ▪

do i = 1, N-1 (1) A(i) = B(i-1) (2) B(i) = C(i) enddo

doacross i = 1, N-1 wait(vB,i-1) (1) A(i) = B(i-1) (2) B(i) = C(i) post(vB,i) enddoacross ??

t 1 2 1 2 1 2 1 2

¡todo en serie!

do i = 1, N-1 (2) B(i) = C(i) (1) A(i) = B(i-1) enddo

forall i = 1, N-1 (2) B(i) = C(i) BARRERA(...) (1) A(i) = B(i-1) endforall

2 1 2 1 2 1 2 1

Cambiado el orden, el bucle puede paralelizarse de manera adecuada, mediante una barrera o mediante funciones wait y post.

8.4.4 Alineación de las dependencias (peeling)

Las dependencias de distancia d = 0 no ofrecen ningún problema al paralelizar un bucle, y no hay que sincronizarlas. En algunos casos, es posible convertir un bucle que tiene dependencias de distancia d > 0 en otro cuyas dependencias son todas de distancia 0, y, por tanto, eliminar las funciones de sincronización (doall). Veamos un ejemplo.

do i = 1, N-1 (1) A(i) = B(i) (2) C(i) = A(i-1) + 2 enddo

El código original puede paralelizarse sin más que introducir una barrera de sincronización entre las instrucciones 1 y 2. Pero tenemos otra solución: ¿por qué no definir una nuevo bucle, mezclando instrucciones de diferentes iteraciones del bucle original, en el que las dependencias sean de distancia 0? En la figura anterior vemos cómo se define la nueva iteración, utilizando instrucciones de las iteraciones i e i+1: 1i y 2i+1. Con esa nueva iteración podemos recorrer todo el espacio original de iteraciones, salvo algunas instrucciones al principio y al final (en el ejemplo, 21 y 1N–1) que tendrán que ejecutarse aparte. El nuevo bucle paralelo quedará así:

1

2

B, 1

2

1

B, 1

1

2

A, 1

nueva iteración

i=1 2 3 4…

Page 140: 6.1 INTRODUCCIÓN

▪ 308 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

C(1) = A(0) + 2

doall i = 1, N-2 ; una iteración menos A(i) = B(i) C(i+1) = A(i) + 2 enddoall

A(N-1) = B(N-1)

No se necesita ninguna función de sincronización: las iteraciones del bucle pueden ejecutarse en paralelo como se desee. Dado que hemos utilizado instrucciones de dos (en general, k) iteraciones diferentes para formar el nuevo cuerpo del bucle, el número total de iteraciones será una (en general, k–1) menos. En contrapartida, hay que ejecutar fuera del bucle dos instrucciones (una (k–1) iteración)43.

La alineación de las dependencias no es siempre tan sencilla como en el caso anterior, por lo que en algunos casos es necesario recurrir a un artificio: replicar algunas instrucciones. Esto es necesario, por ejemplo, cuando hay más de una dependencia entre dos instrucciones, a distancia diferente, como en este caso:

do i = 2, N-1 (1) A(i) = B(i) (2) C(i) = A(i-1) + A(i-2) enddo

do i = 2, N-1 (1) A(i) = B(i) (1´) X(i) = B(i) (2) C(i) = X(i-1) + A(i-2) enddo

C(2) = A(1) + A(0) C(3) = B(2) + A(1)

doall i = 2, N-3 A(i) = B(i) -- 1i X(i+1) = B(i+1) -- 1'i+1 C(i+2) = X(i+1) + A(i) -- 2i+2 enddoall

A(N-2) = B(N-2) A(N-1) = B(N-1)

43 Esas instrucciones no tienen dependencias con las instrucciones del nuevo bucle, por lo que pueden

ejecutarse previamente o posteriormente al bucle (o algunas al principio y otras al final).

1

2

A, 1 A, 2

nueva iteración

1

2

X, 1 A, 2

1' 1

1'

Page 141: 6.1 INTRODUCCIÓN

8.4 OPTIMIZACIONES PARA PARALELIZAR BUCLES DE MANERA EFICIENTE ▪ 309 ▪

Hemos replicado la instrucción 1 y así hemos logrado definir un nuevo cuerpo para el bucle, mezclando tres iteraciones del bucle original (por lo que haremos dos iteraciones menos en el nuevo bucle), quedando fuera del nuevo bucle algunas instrucciones de las primeras y últimas iteraciones: 22, 23, 1N–2, 1N–1.

8.4.5 Extracción de threads independientes (switching)

Los ciclos de dependencias son los más complicados de paralelizar. En general, si la suma de las distancias de las dependencias que forman el ciclo es k, suele ser posible conseguir k procesos independientes para ser ejecutados en paralelo. Para ello aplicamos una técnica parecida al peeling: utilizar instrucciones de diferentes iteraciones para formar una nueva.

Por ejemplo: do i = 0, N-3 A(i+1) = B(i) + 1 B(i+2) = A(i) * 3 C(i) = B(i+2) - 2 enddo

Al analizar el espacio de iteraciones, se ve que pueden generarse 3 procesos paralelos independientes, del estilo de:

1i / 2i+1 / 3i+1 // 1i+3 / 2i+4 / 3i+4 // 1i+6 / 2i+7 / 3i+7 // ...

Por tanto, podemos escribir el bucle de esta manera, para generar tres tareas independientes:

B(2) = A(0) * 3 ; prólogo C(0) = B(2) - 2

doall k = 0, 2 ; 3 hilos en paralelo

do i = pid, N-4, 3 ; el paso del bucle es 3 A(i+1) = B(i) + 1 B(i+3) = A(i+1) * 3 ; de la iteración i+1 C(i+1) = B(i+3) – 2 ; de la iteración i+1 enddo

enddoall

A(N-2) = B(N-3) + 1 ; epílogo

1

2

3

B, 0

A, 1 B, 2 1 1 1 1 1 1

2 2 2 2 2 2

3 3 3 3 3 3

Page 142: 6.1 INTRODUCCIÓN

▪ 310 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

En todo caso, tenemos que analizar con cuidado el efecto que va a tener en el rendimiento del sistema el hecho de que cada hilo trabaje con elementos no consecutivos de los vectores.

8.4.6 Minimización de las operaciones de sincronización

La sincronización es el “peaje” que hay que pagar para poder ejecutar un bucle en paralelo. Debemos por tanto minimizar su uso y utilizarla únicamente en los casos imprescindibles, para lo cual conviene tener en cuenta la transitividad de esa operación. Por ejemplo, si se sincronizan las instrucciones 1i → 2i+1 y 2i+1 → 3i+2, entonces no es necesario sincronizar las instrucciones 1i → 3i+2, ya que quedarán automáticamente sincronizadas. Por ejemplo, tenemos ejecutar el siguiente bucle en paralelo:

do i = 2, N-1 (1) B(i) = B(i) + 1 (2) C(i) = C(i) / 3 (3) A(i) = B(i) + C(i-1) (4) D(i) = A(i-1) * C(i-2) (5) E(i) = D(i) + B(i-1) enddo

Es sencillo comprobar que estos dos bucles están bien sincronizados, pero

el segundo utiliza menos funciones de sincronización.

doacross i = 2, N-1 (1) B(i) = B(i) + 1 post(vB,i) (2) C(i) = C(i) / 3 post(vC,i) wait(vC,i-1) (3) A(i) = B(i) + C(i-1) post(vA,i) wait(vA,i-1) wait(vC,i-2) (4) D(i) = A(i-1) * C(i-2) wait(vB,i-1) (5) E(i) = D(i) + B(i-1) enddoacross

doacross i = 2, N-1 (1) B(i) = B(i) + 1 (2) C(i) = C(i) / 3 post(vC,i) wait(vC,i-1) (3) A(i) = B(i) + C(i-1) post(vA,i) wait(vA,i-1) (4) D(i) = A(i-1) * C(i-2) (5) E(i) = D(i) + B(i-1) enddoacross

2

3

4

5

C, 1

B, 1

D, 0

A, 1

C, 2

1

B, 0

Page 143: 6.1 INTRODUCCIÓN

8.4 OPTIMIZACIONES PARA PARALELIZAR BUCLES DE MANERA EFICIENTE ▪ 311 ▪

i=2 1 2 3 4 5 3 1 2 3 4 5 4 1 2 3 4 5

Si se respeta el orden de las instrucciones, entonces tendremos que 1i >> 2i >> 3i ... Así, con la sincronización 2i-1 → 3i —wait(vC,i–1)— también se cumple la sincronización entre las instrucciones 1i-1 y 5i: en efecto, 1i-1 >> 2i-1 → 3i >> 4i >> 5i ⇒ 1i-1 → 5i, por lo que no son necesarias las funciones de sincronización post(vB,i)/wait(vB,i–1). Por otra parte, no es necesario sincronizar la dependencia 2i → 4i+2, porque ya se han sincronizado las dependencias 2i → 3i+1 y 3i+1→ 4i+2; por tanto, 2i → 3i+1 → 4i+2 ⇒ 2i → 4i+2.

En todo caso, lo anterior es cierto si se respeta el orden de las instrucciones en cada procesador.

En la misma línea, si cada proceso va a ejecutar más de una iteración, no habrá que sincronizar aquellas dependencias que sean internas a cada proceso (véase el ejemplo del apartado 8.2.2.2).

8.4.7 Tratamiento de bucles (reordenación...)

Como ya hemos visto anteriormente, el intercambio o reordenación de bucles es una técnica habitual en los procesadores vectoriales para tratar bucles de dos o más dimensiones. En el caso de la paralelización de bucles, también es una técnica habitual, aunque con otros objetivos: ya que siempre es posible paralelizar un bucle, lo que se busca es conseguir mayor eficiencia (evitar sincronizaciones, modificar el tamaño de grano...).

8.2.5.1 Intercambio de bucles

Modificamos el orden de ejecución de los bucles buscando mayor eficiencia en la ejecución, para incrementar el tamaño de grano o para eliminar operaciones de sincronización, respetando en todo caso las dependencias originales del bucle. Antes de ver ejemplos concretos, resumamos en un esquema gráfico las cuatro posibilidades de paralelización de un bucle de dos dimensiones y su significado.

Page 144: 6.1 INTRODUCCIÓN

▪ 312 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

do i = do_par j =

orden original

i tareas serie, cada una con j tareas en paralelo

do_par i = do j =

orden original

i tareas en paralelo

do j = do_par i =

bucles intercambiados j tareas serie, cada una con i tareas en paralelo

do_par j = do i =

bucles intercambiados

j tareas en paralelo

Así pues, podemos paralelizar tanto el bucle interior como el exterior, y

además intercambiar o no el orden de los bucles. Al paralelizar el bucle interno obtenemos paralelismo de grano fino, y al paralelizar el externo tareas de mayor tamaño. Intercambiaremos el orden de los bucles si con ello minimizamos la sincronización o tal vez si queremos tareas de mayor o menor grano. Veamos un ejemplo:

j →

do i = 1, 3 do j = 0, 5 A(i,j) = A(i-1,j) + 1 enddo enddo

espacio de iteraciones

do i = 1, 3 do_par j = 0, 5 A(i,j) = A(i-1,j) + 1 enddo_par enddo

6 procesad. a la vez (doall), 3 veces, poco trabajo

o bien do_par i = 1, 3 do j = 0, 5 A(i,j) = A(i-1,j) + 1 enddo enddo_par

3 procesad. a la vez (?), una sola vez, más trabajo ¡Ojo! hay que utilizar sincronización (doacross)

P5 P0

tiempo

P0 P1 P2

tiempo

Page 145: 6.1 INTRODUCCIÓN

8.4 OPTIMIZACIONES PARA PARALELIZAR BUCLES DE MANERA EFICIENTE ▪ 313 ▪

Tal como aparece en la figura, tenemos dos maneras de paralelizar. En la primera se ejecuta en paralelo el bucle interno (j), y en serie el externo (i): es decir, las filas se procesan en serie, una a una, y los elementos de cada fila (columnas) en paralelo. El nivel de paralelismo que se consigue puede ser alto, pero el tamaño de grano tal vez sea pequeño. En la segunda, el nivel de paralelismo es menor, sólo 3 procesadores trabajando a la vez, pero, en compensación, las tareas asignadas son mayores. Por desgracia, en este ejemplo necesitamos sincronizar la ejecución de las filas (doacross), como se ve en la figura. ¿Existe otra alternativa?

Sí: podemos volver a analizar las posibilidades de paralelización, pero tras intercambiar el orden de los bucles. En este ejemplo no hay problema para intercambiar los bucles y recorrer el espacio de iteraciones por columnas, ya que respetamos todas las dependencias. De nuevo tenemos dos posibilidades: paralelizar el bucle interior o el exterior. Si paralelizamos el bucle exterior, ahora j, tendríamos lo siguiente:

do_par j = 0, 5 do i = 1, 3 A(i,j) = A(i-1,j) + 1 end enddo_par

P1 6 proces. a la vez, doall, una sola vez, trabajo intermedio

Como vemos en el espacio de iteraciones, no hay problemas para ejecutar todas las columnas en paralelo (doall, sin sincronización), como en el primer caso, y además el tamaño de las tareas es mayor, toda una columna. Ésta es la mejor alternativa de las cuatro, ya que la cuarta opción, paralelizar ahora el bucle interno, (i), es muy mala: tareas muy pequeñas que además requieren sincronización.

do j = 0, 5 do_par i = 1, 3 A(i,j) = A(i-1,j) + 1 enddo_par enddo

3 proces. a la vez (?, doacross), 6 veces, poco trabajo

En resumen, el objetivo del intercambio de bucles es lograr el mayor nivel de paralelismo posible compatible con la menor necesidad de sincronización y, si es útil, con tareas de grano más grueso.

Page 146: 6.1 INTRODUCCIÓN

▪ 314 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

Conviene recordar que no siempre es posible el intercambio de bucles; la regla dice que: "no es posible el intercambio si, tras él, el primer elemento no 0 del vector de distancias de las dependencias es negativo".

8.4.7.2 Cambio de sentido

No siempre es posible el cambio de orden de los bucles debido a las dependencias; en algunos casos, en cambio, bastan ligeros cambios en el bucle original para poder efectuar dicho intercambio; por ejemplo, cómo se recorre una de las dimensiones. He aquí un ejemplo:

do i = 1, 100 do j = 0, 2 A(i,j) = A(i-1,j+1) + 1 enddo enddo

Paralelizar el bucle externo no es adecuado, ya que hay que sincronizar

los procesadores. Se puede paralelizar el bucle interno, pero sólo conseguiríamos 3 tareas en paralelo. Y no se pueden intercambiar los bucles... aunque algo se puede hacer si se recorre el bucle j hacia atrás, de 2 a 0 (dadas las dependencias, da los mismo en qué orden se recorra el bucle).

Por tanto, la opción es intercambiar los bucles, cambiar el orden en que se recorre el bucle j y paralelizar el bucle interno (ahora i), con lo que conseguimos un nivel más alto de paralelismo (100 tareas independientes).

do j = 2, 0, -1 doall i = 1, 100 A(i,j) = A(i-1,j+1) + 1 enddoall enddo

8.4.7.3 Desplazamientos (skew)

En algunos casos, la paralelización no es eficiente en ninguna dimensión, dadas las dependencias del bucle. Por ejemplo, en este caso.

j=0 1 2

i=1 2 3

j=0 1 2

i=1 2 3

Page 147: 6.1 INTRODUCCIÓN

8.4 OPTIMIZACIONES PARA PARALELIZAR BUCLES DE MANERA EFICIENTE ▪ 315 ▪

do i = 1, N do j = 1, M A(i,j) = A(i,j-1) + A(i-1,j) enddo enddo

De todas maneras, también aquí es posible definir una nueva iteración sin dependencias; un “frente de onda” que avanza por diagonales. Como primer paso, desplazamos las iteraciones de j que corresponden a cada fila i, tal como aparece en la siguiente figura; y a continuación, aplicamos el intercambio de bucles, para lograr una paralelización adecuada.

j=1 2 3 4 5 6 ... ... 9 (M+N–1)

do i = 1, N do j = 1+(i-1), M+(i-1) A(i,j-(i-1)) = A(i,j-1-(i-1)) + A(i-1,j-(i-1)) enddo enddo

do j = 1, M+N-1 doall i = max(1,j-M+1), min(j,N) A(i,j-(i-1)) = A(i,j-1-(i-1)) + A(i-1,j-(i-1)) enddoall enddo

8.4.7.4 Colapso y coalescencia de bucles

Si tenemos un bucle anidado, y el interno es pequeño, podemos aplicar el colapso o la coalescencia de bucles (es decir, convertir un bucle de dos dimensiones en uno de una dimensión) para conseguir mayor grado de paralelismo, tal como vemos en este ejemplo:

Page 148: 6.1 INTRODUCCIÓN

▪ 316 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

do i = 0, M-1 do j = 0, N-1 A(i,j) = A(i,j) + 1 enddo enddo

colapso coalescencia doall k = 0, M*N-1 A(k) = A(k) + 1 enddoall

doall k = 0, M*N-1 i = k/M j = k mod N A(i,j) = A(i,j) + 1 enddoall

8.5 PLANIFICACIÓN DE TAREAS (scheduling)

Tras analizar un bucle y decidir que puede paralelizarse, la siguiente operación consiste en el reparto de tareas entre los procesadores. Si se trata de un bucle doall, podemos ejecutar, en el mejor de los casos, una iteración por procesador, pero normalmente no dispondremos de tantos procesadores como iteraciones a ejecutar, y por tanto habrá que repartir (planificar) las tareas entre los procesadores. Si se trata de un bucle doacross, el número de procesadores que pueden utilizarse y el reparto de iteraciones a procesadores vendrán dados por las necesidades de sincronización.

El reparto de iteraciones o tareas puede hacerse de varias maneras, pero el objetivo debe ser siempre el mismo: por un lado, mantener equilibrada la carga (load balancing) de cada procesador (si no, el factor de aceleración será bajo), y por otro, que el coste de las operaciones de sincronización (latencia y tráfico) sea reducido. En resumen: tenemos que conseguir que la ejecución de bucle sea lo más eficiente posible

Tiempo de ejecución →

P0 final

P1 P2 P3

reparto de carga desequilibrado

Tiempo de ejecución →

P0 P1 P2 final

P3

reparto de carga equilibrado

Page 149: 6.1 INTRODUCCIÓN

8.5 PLANIFICACIÓN DE TAREAS (scheduling) ▪ 317 ▪

Dos cuestiones debemos analizar. Por un lado, cuál es el reparto (qué ejecuta cada procesador); y, por otro, cuándo se efectúa el reparto: en tiempo de compilación o en tiempo de ejecución.

8.5.1 Reparto de las iteraciones: consecutivo o entrelazado

En esta primera clasificación consideramos qué iteraciones se reparten a cada procesador: básicamente si son iteraciones consecutivas o no. Dos son las posibilidades principales:

• Reparto consecutivo: cada procesador ejecuta un grupo de iteraciones consecutivas. Por ejemplo, el reparto de N iteraciones entre P = 3 procesadores se efectúa así:

i → 0 ... N/3–1 N/3 ... 2N/3–1 2N/3 ... N–1

Proc. P0 P1 P2

• Reparto entrelazado: las iteraciones se van repartiendo de una en una

a los procesadores. Por ejemplo,

i → 0 1 2 3 4 5 6 7 8 ...

Proz. P0 P1 P2 P0 P1 P2 P0 P1 P2 ...

En el ejemplo, el nivel de entrelazado ha sido de 1 (las iteraciones se han repartido de una en una), pero podría ser cualquier otro valor; por ejemplo, de dos en dos: P0 → 0, 1, 6, 7, 12, 13,...; P1 → 2, 3, 8, 9, 14, 15,...; P2 → 4, 5, 10, 11, 16, 17... En el límite, cuando el nivel de entrelazado es N/P, lo que se hace es un reparto consecutivo.

En función de la estrategia de planificación escogida, el código correspondiente a cada procesador sería el siguiente: (N = número de iteraciones, P = número de procesadores, pid = identificador del procesador):

doall i = 0, N-1 < > enddoall

Page 150: 6.1 INTRODUCCIÓN

▪ 318 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

▪ Planificación consecutiva

ini = (pid)*N/P ; N/P = tamaño de cada trozo fin = (pid+1)*N/P – 1

do i = ini, fin < > enddo

▪ Planificación entrelazada, una a una44

do i = pid, N-1, P < > enddo

Si el bucle a planificar es de tipo doall (todas las iteraciones son independientes) ambas estrategias pueden ser adecuadas. En cambio, si hay que sincronizar las iteraciones, entonces, la planificación adecuada es la entrelazada, ya que la consecutiva implicaría una ejecución casi en serie, como se ve en el siguiente esquema.

P0 P1 P2 Además de las dos estrategias básicas, reparto consecutivo y entrelazado,

pueden utilizarse otras alternativas adecuadas a casos concretos. Por ejemplo, si sabemos que el coste de computación de las iteraciones crece con cada iteración (por ejemplo, cuando se procesan matrices triangulares), puede utilizarse una planificación que se conoce como doubling, en la que reparten entre los procesadores parejas de iteraciones, una corta y una larga —1 y N, 2 y N–1, 3 y N–2...—, para que la carga repartida esté equilibrada.

8.5.2 Planificación estática o dinámica

En esta segunda clasificación tomamos en cuenta cuándo se produce el reparto de tareas: en compilación (un reparto fijo, siempre igual), o en ejecución, según se van procesando las tareas (variable, por definición).

44 En general, si el nivel de entrelazado es k, el bucle se reparte de la siguiente manera: do i = pid*k, N-1, P*k do j = i, i+k-1

Page 151: 6.1 INTRODUCCIÓN

8.5 PLANIFICACIÓN DE TAREAS (scheduling) ▪ 319 ▪

8.5.2.1 Planificación estática

Decimos que la planificación es estática si se efectúa en tiempo de compilación. El compilador o el programador deciden a priori qué iteraciones va a ejecutar cada procesador.

Cuando la planificación es estática no se añade ninguna sobrecarga a la ejecución de las tareas, pero (a) hay que conocer previamente, en compilación, el número de iteraciones a ejecutar, y muchas veces ese parámetro se decide en ejecución; y (b) puede que no sea fácil lograr un reparto equilibrado de la carga entre los procesadores, ya que no sabremos, en general, el coste de ejecución de cada iteración (por ejemplo, s hay instrucciones tipo if, o debido a fallos en cache...). En definitiva, es sencillo, pero tal vez no eficiente.

8.5.2.2 Planificación dinámica

Para obtener un reparto de carga equilibrado puede efectuarse una planificación dinámica de las tareas, en tiempo de ejecución. Al comenzar el programa, cada proceso toma una parte del bucle a ejecutar; cuando termina con ese trozo, va a buscar otro trozo más, y así continúan todos los procesos hasta completar la ejecución del bucle. Esto va suponer una carga añadida a la ejecución del bucle, dado que ahora hay que añadir el tiempo necesario para estas operaciones de planificación, por lo que interesa que esa sobrecarga sea lo más pequeña posible.

Se han desarrollado varias estrategias de planificación dinámica. Por un lado, tenemos los métodos que reparten siempre trozos del bucle del mismo tamaño, y por otro los que van repartiendo trozos cada vez más pequeños. Analicemos las estrategias de planificación dinámicas más utilizadas.

8.5.2.2.1 Autoplanificación (self-scheduling)

Las iteraciones del bucle se reparten una a una. Cuando un proceso termina una iteración va a buscar otra iteración para ejecutar, la primera que todavía no se haya ejecutado. El código correspondiente a esa estrategia es el siguiente:

Page 152: 6.1 INTRODUCCIÓN

▪ 320 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

doall i = 0, N-1 A(i) = B(i) + C(i) → enddoall

LOCK(CER); mi_it = siguiente; siguiente = siguiente + 1; UNLOCK(CER);

while (mi_it ≤ N-1) do A(mi_it) = B(mi_it) + C(mi_it);

LOCK(CER); mi_it = siguiente; siguiente = siguiente + 1; UNLOCK(CER); endwhile

[Lock/unlock → mi_it = Fetch&Inc(siguiente)]

Cada procesador ejecuta las iteraciones una a una. Para asignar las iteraciones a los procesadores se usa un contador, siguiente, que se accede e incrementa en modo atómico mediante la variable cerrojo CER. Este tipo de estrategia genera un reparto de carga muy equilibrado, pero el coste de las operaciones de sincronización puede resultar elevado en relación al tiempo de ejecución de las tareas (como en el caso del ejemplo). Para que sea adecuado, debe cumplirse que Teje >> Tsinc.

8.5.2.2.2 Planificación por trozos (chunk scheduling)

Para aliviar el coste de la sincronización del caso anterior, podemos repartir trozos de bucle más grandes en cada operación de planificación: Z iteraciones consecutivas45. De esta manera:

doall i = 0, N-1 A(i) = B(i) + C(i) → enddoall

LOCK(CER); mi_it = siguiente; siguiente = siguiente + Z; UNLOCK(CER); while (mi_it ≤ N-1) do i = mi_it, min(mi_it+Z-1, N-1) A(i) = B(i) + C(i) enddo

LOCK(CER); mi_it = siguiente; siguiente = siguiente + Z; UNLOCK(CER); endwhile

[Lock/unlock → mi_it = Fetch&Add(siguiente,Z)]

45 Si hay que sincronizar las iteraciones que se reparten, no resulta adecuado repartir iteraciones

consecutivas. Hay que utilizar una estrategia similar, pero repartiendo trozos con iteraciones entrelazadas, siendo el nivel de entrelazado el número de procesadores.

Page 153: 6.1 INTRODUCCIÓN

8.5 PLANIFICACIÓN DE TAREAS (scheduling) ▪ 321 ▪

Como en el caso anterior, el contador siguiente lleva en cuenta el número de iteraciones ejecutadas hasta el momento, y se actualiza incrementándolo en Z iteraciones. Cuando se termina de ejecutar el trozo correspondiente (Z iteraciones), se va a buscar un nuevo trozo, hasta acabar así con todo el bucle. El coste de sincronización es menor, ya que sólo se efectúa una operación de planificación cada Z iteraciones. Por contra, el reparto de carga puede que no sea tan equilibrado como en el caso anterior.

8.5.2.2.3 Autoplanificación guiada (GSS, guided self-scheduling)

Los trozos de bucle que se planifican en las estrategias anteriores son todos de tamaño fijo (Z), pero no sabemos si el reparto es equilibrado, porque el tiempo de ejecución de cada trozo es desconocido (por ejemplo, la instrucción if A then B tiene diferente tiempo de ejecución en función de A). Podría ocurrir que el último trozo asignado tenga un tiempo de ejecución muy grande, y en ese momento ya no hay remedio: sólo habrá un procesador en ejecución, mientras los demás están parados esperando a que termine.

Por tanto, en algunos casos puede que no sea una buena estrategia asignar trozos de tamaño fijo. Por ello, puede ser interesante, de cara a un mejor reparto de la carga, ir asignado trozos de bucle de tamaño cada vez menor; así, al acercarnos al final del bucle la planificación es cada vez más “fina”. La planificación GSS busca ese objetivo, y reparte trozos del siguiente tamaño:

Zs = (N – i) / P (parte entera por arriba46)

donde N – i representa el número de iteraciones que falta por ejecutar y P el número de procesadores; es decir, cada procesador coge la parte que le corresponde de las iteraciones que quedan por ejecutar. La planificación se efectúa igual que en el caso anterior, salvo que Z no es una constante.

Una variante de dicha planificación, conocida cono factoring, asigna en cada operación de planificación P trozos de tamaño (N – i) / 2P; es decir

46 En lugar de calcular N/P suele ser más sencillo calcular (N+P–1)/P. Por otra parte, si nos interesa

que el tamaño de los trozos (salvo quizás el último) sea siempre mayor que un determinado valor k, entonces hay que asignar trozos de tamaño (N+kP–1)/P (N = número de iteraciones que falta por repartir).

Page 154: 6.1 INTRODUCCIÓN

▪ 322 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

reparte la mitad de lo queda por ejecutar en trozos del mismo tamaño para cada procesador.

Aunque usando planificación guiada la carga de trabajo esté más equilibrada, no hay que olvidar el coste de estas operaciones de planificación, ya que hay que calcular el tamaño de los trozos en cada operación de planificación (hay que hacer una división). La última estrategia que vamos a ver tiene como objetivo reducir ese coste.

8.5.2.2.4 Autoplanificación trapezoidal (trapezoid self-scheduling)

Como en el caso anterior, los trozos de bucle que se asignan son de tamaño cada vez menor, pero, de cara a reducir el tiempo necesario para ello, en lugar de efectuar una división se realiza una resta:

Zs+1 = Zs – k

El tamaño del trozo más grande, Z1, y del más pequeño, Zn, junto al valor de k, se definen previamente a la ejecución. Los tamaños de todos los trozos forman una serie aritmética cuya suma debe ser N; por tanto, definidos los tamaños del trozo mayor y menor, obtenemos k de la siguiente manera:

)(21

22 1

22111

1

1

n

nnnn

s

ns ZZN

ZZkNk

ZZZZnZZZ+−

−=→=

+

−+=

+=∑

=

En resumen. Las tareas a ejecutar en paralelo —iteraciones, funciones,

subprogramas…— tienen que ser repartidas entre los procesadores. Si el número de procesadores disponibles es mayor que el de tareas, no hay problema, ya que cada procesador ejecutará una tarea. Pero, en general, el número de tareas paralelas será mucho mayor, sobre todo en el caso de iteraciones de bucles, por lo que es necesario efectuar un reparto lo más eficiente posible, que mantenga a los procesadores trabajando siempre. Si sabemos que el tiempo de ejecución de las tareas es similar, lo más adecuado

operación de planificación

Z1

Zn

1 n 2 s

k Z2

núm

. de

iter

acio

nes

Page 155: 6.1 INTRODUCCIÓN

8.6 SECCIONES PARALELAS: Fork / Join ▪ 323 ▪

es la planificación estática, ya que no añade ninguna carga; en caso contrario, utilizaremos algún tipo de planificación dinámica, pero analizando con cuidado el coste de planificación. Por otra parte, de cara a evitar problemas de falsa compartición en la cache, es útil que el tamaño de los trozos sea un múltiplo del tamaño de bloque de la cache.

Como resumen, en esta tabla podemos ver los tamaños de los trozos que se reparten en las diferentes estrategias, para el caso N = 1000 y P = 4.

por trozos

(Chunk) Z = 100 GSS

Zs = (N – i) / P TSS

Z1 = 76, Zn = 4 → k = 3

oper

ació

n de

pla

nific

ació

n

1 2 3 4 5 6 7 8 9

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

100 100 100 100 100 100 100 100 100 100

/

250 188 141 106 79 59 43 34 25 19 14 11 8 6 5 3 3 2 1 1 1 1 /

76 73 70 67 64 61 58 55 52 49 46 43 40 37 34 31 28 25 22 19 16 13 10 7 4 /

núm. planif. 10 (N / Z) 22 25

8.6 SECCIONES PARALELAS: Fork / Join

Como dijimos al principio, los bucles son uno de los candidatos más adecuados para ser ejecutados en paralelo (al menos en determinadas aplicaciones de cálculo intensivo). Pero el paralelismo se puede explotar también a nivel de procedimientos o funciones (o, en casos de paralelismo de grano fino, en las instrucciones), definiendo secciones de código (sections) que puedan ejecutarse en paralelo.

Este tipo de paralelismo se puede indicar de diversas maneras, pero las más conocidas son las directivas Fork y Join o Parallel Sections. La directiva Fork indica al compilador que las tareas que a

Page 156: 6.1 INTRODUCCIÓN

▪ 324 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

continuación se indican pueden repartirse sin problema entre los procesadores. Por su parte, Join indica un punto de sincronización de todas las tareas (una barrera), tras del cual se continúa con la ejecución secuencial. Las directivas Parallel Section y End Parallel Section cumplen la misma función.

El siguiente programa presenta un ejemplo sencillo, en el que hay que ejecutar 5 funciones:

Grafo de dependencias Fork ; sección paralela (2 proc.) F1; F2; Join ; final

Fork ; sección paralela (2 proc.) F3; F4; Join ; final

F5

Al principio se generan dos hilos o procesos paralelos, uno para ejecutar F1 y el otro para ejecutar F2. Al terminar, ambos procesos se sincronizan antes de empezar con la siguiente "región paralela", donde ejecutarán las funciones F3 y F4. Finalmente, se ejecuta la función F5, en un solo procesador.

En caso de ser necesario, las funciones Fork / Join pueden emularse mediante un doall; viceversa, es sencillo emular un bucle doall mediante las directivas Fork y Join. Por ejemplo, para el caso anterior:

doall k = 0, 1 ; ambas iteraciones en paralelo if (pid = 0) then F1; ; P0 ejecuta F1 if (pid = 1) then F2; ; P1ejecuta F2 enddoall [ BARRERA(...) ]

doall k = 0, 1 ; ambas iteraciones en paralelo if (pid = 0) then F3; if (pid = 1) then F4; enddoall

[ BARRERA(...) ]

F5; ; en un procesador

F1

F2

F3

F4

F5

Page 157: 6.1 INTRODUCCIÓN

8.7 ANÁLISIS DEL RENDIMIENTO ▪ 325 ▪

8.7 ANÁLISIS DEL RENDIMIENTO

Cuando se ejecuta un bucle en paralelo en P procesadores, el objetivo es, obviamente, conseguir una ejecución P veces más rápida. En este capítulo hemos estudiado las estrategias principales para paralelizar bucles. Sin embargo, no es fácil conseguir factores de aceleración elevados, ya que hay que superar otros muchos problemas.

El primero y más obvio es el número de procesadores disponible. Si disponemos de 4 procesadores, nunca iremos más de 4 veces más rápido. En todo caso, hay problemas más de fondo que afectan al rendimiento, relacionados principalmente con la arquitectura del sistema paralelo y el reparto de datos.

En una máquina SMP, los datos van de MP a MC a través del bus. Nos vamos a encontrar con dos problemas. Por un lado, la capacidad del bus, y, por otro, la compartición de datos. Si las tareas a ejecutar son muy pequeñas o se reutilizan muy poco los datos (paralelismo de grano fino; por ejemplo, el tipo de bucles que hemos utilizado como ejemplos en este capítulo), los procesadores solicitarán continuamente bloques de datos, y puede que lleguen a saturar la capacidad del bus. Si así ocurre, los tiempos de ejecución subirán, y el rendimiento bajará. Por otra parte, la planificación de tareas debe tener en cuenta la colocación de los datos en memoria. Por ejemplo, si los datos de las iteraciones i e i+1 están en el mismo bloque de memoria, probablemente será más adecuado asignar dichas iteraciones al mismo procesador y no a dos diferentes, para evitar el problema de falsa compartición (y de anulación de bloques).

En los sistemas DSM (muchos procesadores) el problema es similar, aunque la situación de los datos en la memoria es más crítica, ya que la memoria está repartida entre todos los procesadores, por lo que es esencial considerar dónde están los datos a la hora de decidir el reparto de tareas.

Junto a ello, la sincronización de los procesos también afecta de manera importante al rendimiento del sistema. Al coste de ejecución de los procesos hay que añadir el de sincronización, que habrá que estimar en función de la carga de trabajo de la máquina, la estructura de la misma, la red de comunicación… Por ejemplo, si la sincronización cuesta del orden de milisegundos y las tareas a ejecutar son del orden de segundos no hay problema, pero si el tiempo de ejecución de las tareas es de microsegundos, el coste de la sincronización resulta inadmisible: las tareas a ejecutar son

Page 158: 6.1 INTRODUCCIÓN

▪ 326 ▪ Capítulo 8: PARALELIZACIÓN DE BUCLES Y PLANIFICACIÓN DE TAREAS

demasiado pequeñas para ese sistema. En definitiva, es necesario mantener un determinado equilibrio entre los tiempos de ejecución de las tareas paralelas y los de la sincronización de las mismas.

Todos esos aspectos deben ser valorados a la hora de paralelizar un programa, ya que el rendimiento que obtengamos va a depender de las decisiones que tomemos.

Page 159: 6.1 INTRODUCCIÓN

▪ 9 ▪

Computadores Paralelos de Alto Rendimiento.

Programación Paralela: OpenMP y MPI (introducción).

En los capítulos anteriores hemos analizado las principales características de los computadores paralelos. Inicialmente, este tipo de computadores sólo se utilizaba en aplicaciones de cálculo masivo en ámbitos científico-técnicos; hoy en día, en cambio, se encuentran en cualquier laboratorio, universidad o empresa. En este último capítulo analizaremos la situación actual y su evolución en el tiempo de los supercomputadores más rápidos, y terminaremos haciendo una pequeña introducción a las herramientas más habituales para el desarrollo de programas paralelos: OpenMP en los sistemas de memoria compartida y MPI en los de memoria distribuida.

Page 160: 6.1 INTRODUCCIÓN

▪ 328 ▪ Capítulo 9: COMPUTADORES PARALELOS DE ALTO RENDIMIENTO

9.1 COMPUTADORES PARALELOS DE ALTO RENDIMIENTO

En el ámbito de los computadores paralelos podemos encontrar sistemas muy diferentes, desde pequeños SMP con 8-32 cores, hasta máquinas con más de de 1 millón de núcleos. Estas últimas son las máquinas más rápidas del mundo, pero no son máquinas que se puedan comprar normalmente, sino proyectos “estratégicos” de los gobiernos. Los sistemas paralelos comerciales más comunes son los clusters de 128–4096 procesadores (o más) unidos mediante una red de comunicación avanzada. Aunque no son las máquinas más rápidas, son “baratos” y se utilizan cada vez más para ejecutar múltiples aplicaciones científicas y empresariales.

En el sitio web www.top500.org se publica, dos veces al año (junio y noviembre), la lista de los 500 computadores más rápidos del mundo. No es sencillo comparar máquinas, ya que tanto la arquitectura y los recursos físicos como las aplicaciones suelen ser muy diferentes. En todo caso, todos los fabricantes “acordaron” utilizar el banco de pruebas LINPACK (resolución de sistemas de ecuaciones lineales muy grandes) para medir la velocidad de ejecución lograda.

En junio de 2012, la máquina más rápida del mundo es Sequoia, un Blue Gene/Q de IBM en los laboratorios Laurence Livermore (USA). Se trata de un MPP que ha logrado una velocidad de cálculo de 16,32 Petaflop/s con 1 572 864 núcleos (98 304 Power BQC de 16 núcleos a 1,6 GHz). Por vez primera se ha superado la barrera de los 10 PF/s y se han utilizado más de un millón de núcleos en una máquina paralela. El consumo de energía es así mismo de record: sólo 7 890 kw, es decir, 5 w por núcleo o 0,48 w por GF/s.

El segundo es el cluster K computer de Fujitsu (RIKEN A. I. Comput. Sciences, Japón), con 705 024 cores (88 128 SPARC64 VIIIfx, 8 núcleos, 2 GHz) que ha logrado una velocidad de cálculo de 10,5 PF/s. El tercero es Mira (Argonne, USA), una máquina similar a la primera pero de tamaño mitad. La cuarta máquina, la primera de Europa, es el cluster SuperMUC de IBM en Alemania (Leibniz Rechenzentrum), con 2,9 PF/s y 147 546 núcleos. Finalmente, la quinta (segunda hace un año) es el cluster Tianhe-1A de NUDT (Tianjin NSC, China) que ha alcanzado 2,57 PF/s con 186 368 núcleos (EM64T Xeon X5670 y NVIDIA GPU, FT-1000 8C).

En total, 20 máquinas superan ya el PF/s y 186 máquinas los 100 TF/s. De cumplirse las expectativas, para el 2018 se alcanzará el Exaflop/s: ¡1018 operaciones de coma flotante por segundo!

Page 161: 6.1 INTRODUCCIÓN

9.1 COMPUTADORES PARALELOS DE ALTO RENDIMIENTO ▪ 329 ▪

En la posición 145 se encuentra el sucesor del multicomputador vectorial que ya mencionamos en el primer capítulo, el Earth Simulator (Japón); alcanza una velocidad de 122,4 TF/s cono 1280 procesadores vectoriales (SX-9, tarjetas SMP de 8 procesadores, conectadas por un fat tree), y es la única máquina de la lista que utiliza procesadores vectoriales. En la posición 177 aparece el cluster Bullx B505 del Centro de Supercomp. de Barcelona, con 103,2 TF/s y 5544 núcleos; el “viejo” cluster Mare Nostrum, con 63,8 TF/s y 10 240 procesadores (PowerPC, Myrinet), es ya el 465 de la lista.

El siguiente gráfico muestra la evolución en el tiempo de la velocidad de cálculo de los computadores más rápidos (el primero de la lista, el último y la suma total). Como se puede ver en la figura, en el año 1993 se consiguió superar por primera vez una velocidad de cálculo de 100 GF/s, y en el año 1997 se superó 1 TF/s. En los 19 años que recoge el gráfico, la velocidad máxima ha pasado de 59,7 GF/s a 16,32 PF/s; es decir, una velocidad 273 447 veces mayor. De acuerdo a la ley de Moore46, la velocidad de cálculo casi se ha ido duplicando (x 1,93) cada año; así, la máquina más rápida de 2004 ¡no aparecería entre las 500 más rápidas de 2012!).

46 Según la ley de Moore, la evolución de los computadores, en muchos aspectos, no es lineal, sino

exponencial; así, cada cierto tiempo se duplica el número de transistores en un chip, la capacidad de memoria, la frecuencia de reloj, la velocidad de cálculo, la velocidad de transmisión, etc. En los últimos 20-30 años esa previsión se ha cumplido con cierta precisión, aunque no está claro cuánto tiempo más se va a cumplir, ya que las tecnologías actuales están ya muy cerca de sus límites físicos.

ASCI Red Intel

ASCI White IBM

Earth Sim. NEC

BlueGene IBM

× 1,93 / año

Roadrunner IBM

Jaguar Cray Inc.

K computer Fujitsu

Sequoia IBM

16,32 PF/s

CM5 Th. M.

Tianhe-1A NUDT

Page 162: 6.1 INTRODUCCIÓN

Evolución de los supercomputadores - Breve historia de los computadores más rápidos del mundo (top 1)

Año Computador Fabric. Procesador Arquitectura Lugar Núcleos Vel. Cálc. GF/core Watt/GF

93-I CM-5 Thinking M. SPARC+vect. pr., 32 MHz MPP - hypercube, fat tree Los Alamos N.L. (USA) 1024 59,7 GF/s 0,06

93-II Num. Wind Tunnel Fujitsu VPP500, 105 MHz vector MPP, crossbar N. Aerospace L. (Japan) 140 124 GF/s 0,9 3387

94-I Paragon XP/S140 Intel i860 MPP - 2D torus Sandia N.L. (USA) 3680 143 GF/s 0,04

94-II 95-II Numerical Wind Tunnel Fujitsu VPP500, 105 MHz vector MPP - crossbar N. Aerospace L. (Japan) 140 192 GF/s 1.4

96-I SR2201 Hitachi HARP-1E (PA-RISC), pseudovector, 150 MHz MPP - 3D crossbar U. of Tokyo 1024 232 GF/s 0,2

96-II CP-PACS Hitachi = = U. of Tsukuba 2048 368 GF/s 0,18 747

97-I 00-I ASCI Red Intel Pentium Pro, 200 MHz

Pentium II, 333 MHz MPP - 3D mesh - Propriet. Sandia N.L. (USA) 7264 9632

1,1 TF/s 2,4 TF/s

0,15 0,25 354

00-II 01-II ASCI White IBM Power3-II, 375 MHz MPP - SP switch

(512 nodes x 16 P (sm)) L. Livermore N.L. (USA) 8192 7,3 TF/s 0,9 02-I 04-I Earth Simulator NEC SX-6 vectorial, 1 GHz MPP - crossbar J.A.M.E.S.T (Japan) 5120 35,9 TF/s 7 89

04-II Blue Gene/L IBM PowerPC 440, 0,7 GHz MPP - 3D torus - Propriet. IBM/DOE (USA) 32 768 70,7 TF/s 2,2

05-I Blue Gene/L IBM = = L. Livermore N.L. (USA) 65 536 137 TF/s 2,1 5,2 05-II 07-I Blue Gene/L IBM = = = 131 072 281 TF/s 2,14

07-II Blue Gene/L IBM = = = 212 992 478 TF/s 2,25 08-I 09-I Road Runner IBM PowerXCell 8i, 3,2 GHz

Opteron, 1,8 GHz Cluster - Infinib. - fat tree L. Livermore N.L. (USA) 122 400 1,03 PF/s 8,4 2,2 09-II 10-I Jaguar, XT5 Cray Inc. Opteron, 6c, 2,6 GHz Cluster - Cray Gemini Interc.

Cray SeaStar2+ / Infinib. Oak Ridge N.L. (USA) 224 162 1,76 PF/s 7,85 3,9

10-II Tianhe-1A NUDT Xeon X5670, 6c, 2,9 GHz MPP - Proprietary N.S.C, Tianjing (China) 186 368 2,57 PF/s 13,8 1,6

11-I K computer Fujitsu SPARC64 VIIIfx, 2 GHz Cluster - Tofu Interconnect 6D torus-mesh RIKEN AICS (Japan) 548 352 8,16 PF/s 14,9 1,2

11-II K computer Fujitsu = = = 705 024 10,5 PF/s 14,9 1,2

12-I Sequoia, Blue Gene/Q IBM Power BQC,16c, 1,6 GHz MPP - Custom L. Livermore N.L. (USA) 1 572 864 16,3 PF/s* 10,4 0,48

Tera = 1012 Peta = 1015 Flop/s = operaciones de coma flotante / s *a 1 operación por seg. ≫ 16,3 x 1015 s = 516,5 millones de años = 3,8% de la edad del universo

1993≫2012: La velocidad de cálculo es 270 000 veces mayor (un año de ejecución se ha reducido a 2 minutos); el rendimiento por núcleo es 173 veces mayor, y la máquina tiene 1536 veces más núcleos.

Page 163: 6.1 INTRODUCCIÓN

9.1 COMPUTADORES PARALELOS DE ALTO RENDIMIENTO ▪ 331 ▪

En junio de 2012, 15 máquinas utilizan ya más de 128 k núcleos, y 140 están en el intervalo de 16 a 128 k; la mayoría dispone de 8 a 16 k núcleos. La mayoría tienen 8-16 k núcleos.

La arquitectura de los sistemas paralelos más rápidos ha sufrido importantes cambios en los últimos años. De los 500 computadores más rápidos un 81% son de tipo cluster y el 19% restante son MPP; no hay más tipos. Si nos centramos en la velocidad de ejecución de las máquinas más que en el número de sistemas, vemos que las máquinas MPP son más rápidas, ya que representan el 46% de la suma total de rendimiento siendo solamente el 19% de las máquinas (es decir, son 3,6 veces más rápidas).

Respecto a las redes de comunicación de estos sistemas, se acentúa la tendencia de los últimos años: las redes más utilizadas son Infiniband (42%) seguida de Gigabit/10G Ethernet (41%); por otra parte, un 3,2% son redes de diseño específico (proprietary) y un 9% de tipo custom. Si normalizamos respecto a la velocidad de cálculo, los porcentajes cambian notablemente: los computadores que utilizan Infiniband representan el 32% del rendimiento de las 500 máquinas; G/10G Ethernet el 13%; y los de red de tipo proprietary o custom, a pesar de ser menos, son más grandes y rápidos, y alcanzan el 49% del total (aunque hay que tener en cuenta que de los 5 primeros cuatro utilizan ese tipo de red).

MPP

Cluster

Constellation

SMP

P1

SIMD

Page 164: 6.1 INTRODUCCIÓN

▪ 332 ▪ Capítulo 9: COMPUTADORES PARALELOS ACTUALES

Las dos características anteriores, tipo de sistema y tipo de red de interconexión, parecen definir claramente un camino: cada vez más, los sistemas paralelos son de tipo cluster y se utilizan redes “comunes” del estilo de G/10G Ethernet (no son las más rápidas, pero son baratas). Por otro lado, las máquinas más rápidas (y también las más caras) son de tipo MPP y utilizan redes de diseño específico.

Un último apunte cuantitativo. Hace algunos años, los sistemas paralelos eran máquinas sofisticadas y se utilizaban en ámbitos específicos y controlados; hoy en día, en cambio, el 51% de las 500 máquinas más rápidas del mundo se utilizan en la industria y en ámbitos comerciales. Ese tendencia es muy clara a lo largo del tiempo; aunque no son las máquinas más rápidas (que siguen estando ligadas a la investigación y a las instituciones relacionadas con los gobiernos), los sistemas paralelos se utilizan cada vez más en áreas industriales y comerciales en las que se requiere efectuar bastante cálculo o procesar gran cantidad de datos (telecomunicaciones, electrónica y semiconductores, energía, diseño de automóviles y aviones, instituciones financieras, procesamiento de información...)

De hecho, formar un cluster sencillo no es excesivamente caro; por ejemplo, el coste de un cluster de 8 nodos de 4 procesadores de 4 núcleos conectados mediante Infiniband puede estar en torno a los 40 000 euros (utilizando Gigabit Ethernet, puede reducirse a unos 25 000).

9.2 PROGRAMACIÓN PARALELA: OpenMP y MPI

(introducción)

Queda fuera de nuestros objetivos estudiar las diferentes herramientas que se utilizan para programar aplicaciones paralelas; sin embargo, merece la pena analizar las características básicas de las más utilizadas.

Tal y como hemos comentado en capítulos previos, tenemos dos modelos de sistema paralelo: los que utilizan un espacio de direccionamiento de memoria compartido y los que usan uno privado. En cada uno de los casos, es habitual utilizar un modelo de programación (y sus correspondientes herramientas) diferente. En los sistemas SMP, se suele utilizar OpenMP, y en los sistemas de memoria distribuida, DSM y MPP, la opción más utilizada es MPI (para ambos casos existen más alternativas). Veamos sus características principales.

Page 165: 6.1 INTRODUCCIÓN

9.2 PROGRAMACIÓN PARALELA: OpenMP y MPI (introducción) ▪ 333 ▪

9.2.1 OpenMP

La herramienta más utilizada para programar sistemas de memoria compartida es OpenMP. Recordemos las características principales de esos sistemas: los procesadores comparten un único espacio de memoria, y la comunicación entre procesadores se realiza mediante el uso de variables compartidas, que habitualmente hay que "proteger" con funciones de sincronización.

OpenMP no es un lenguaje de programación, sino que es un API (Application Programming Interface) que se utiliza con los lenguajes C y Fortran.

OpenMP no efectúa ningún análisis de dependencias, por lo que el programador debe decidir qué ejecutar en paralelo y qué no, la naturaleza de las variables, etc. El modelo de ejecución de OpenMP es Fork/Join. En los trozos de código que se deben ejecutar en paralelo se generan los correspondientes hilos o threads, que desaparecen al finalizar su tarea, cuando se retoma la ejecución secuencial del programa. Las variables que utilizan los hilos pueden ser privadas o compartidas. Aunque está dirigido básicamente a la paralelización de bucles, también pueden definirse secciones paralelas.

OpenMP ofrece estos elementos para generar programas paralelos: - Un conjunto de directivas al compilador, para especificar los trozos de

código que deben ejecutarse en paralelo, para indicar el tipo de reparto del trabajo, y para sincronizar los procesos o tareas.

- Una librería con unas pocas funciones (principalmente para asignar identificadores a los procesos, para saber el número de hilos, o funciones tipo lock/unlock para generar secciones críticas).

- Unas cuantas variables de entorno (por ejemplo, para especificar el número de hilos o el modo de planificación).

Algunas de las principales directivas que se pueden utilizar son las siguientes:

▪ Para definir secciones paralelas >> #pragma omp parallel [variables,…] { región paralela }

Page 166: 6.1 INTRODUCCIÓN

▪ 334 ▪ Capítulo 9: COMPUTADORES PARALELOS ACTUALES

Se generan múltiples hilos, y todos ejecutan el mismo código, replicado; para repartir las tareas se pueden utilizar los identificadores de los hilos o las opciones que para ello ofrece OpenMP. Hay que declarar explícitamente el carácter de las variables: compartidas (shared) o privadas (private).

▪ Para repartir tareas -- Para repartir las iteraciones de un bucle for entre los hilos que

ejecutan la región paralela.

>> #pragma omp for [variables, sched,…] for ...

Además de las variables, se puede especificar el tipo de reparto (scheduling): static, dynamic, guided...

Si la región paralela es simplemente un bucle for, ambos pragma pueden juntarse en uno: #pragma parallel for […]

-- Para repartir funciones o procedimientos (no iteraciones de un bucle) entre los hilos que ejecutan la región paralela.

>> #pragma omp sections [variables,…] { #pragma omp section { ... } #pragma omp section { ... } ... }

▪ Para sincronizar los hilos -- Para definir un trozo de código que se va a ejecutar en exclusión

mutua, es decir, una sección crítica.

>> #pragma omp critical [variables,…] { sección crítica }

Además, se pueden utlizar operaciones de tipo lock / unlock mediante funciones de biblioteca:

omp_set_lock(&S); omp_unset_lock(&S);

Page 167: 6.1 INTRODUCCIÓN

9.2 PROGRAMACIÓN PARALELA: OpenMP y MPI (introducción) ▪ 335 ▪

-- Una barrera de sincronización

>> #pragma omp barrier

En general, al final de cada pragma hay una barrera implícita (si se desea, se puede eliminar).

A modo de ejemplo, veamos cómo se puede paralelizar un bucle simple

utilizando OpenMP. El bucle calcula mediante múltiples sumas, la integral (una suma) de la función 4 / (1 + x2), dividiendo el área bajo la curva en múltiples polígonos y sumando sus áreas.

void main () { int i, n; double h, x, suma = 0.0, pi;

printf(\n Numero de intervalos --> ”); scanf("%d",&n); h = 1.0 / (double) n;

for (i=0; i<n; i++) { x = (i + 0.5) * h; suma = suma + 4.0 / (1.0 + x*x); } pi = suma * h; printf("\n PI(+/-) = %.16f \n", pi); }

Las iteraciones del bucle for son independientes, pero se utiliza la variable compartida suma. El bucle se puede ejecutar en paralelo, sin problemas, siempre y cuando los hilos utilicen variables privadas para dejar los resultados parciales, y, al final, esos resultados parciales, uno por hilo, se acumulen en la variable compartida suma, de manera atómica. Tenemos dos posibilidades para hacer esto: utilizar una sección crítica, o utilizar un tipo de variable, que OpenMP permite definir, denominado reduction (ambas hacen lo mismo, pero en el segundo caso queda en manos de OpenMP que la actualización de la variable compartida se haga de manera atómica). Además, cada hilo utiliza un variable privada, x.

El código sería el siguiente:

Page 168: 6.1 INTRODUCCIÓN

▪ 336 ▪ Capítulo 9: COMPUTADORES PARALELOS ACTUALES

#include <omp.h> #define NUM_THREADS 8 void main () { int i, n; double h, x, pi, suma = 0.0; omp_set_num_threads(NUM_THREADS);

printf(“\n Numero de intervalos --> ”); scanf("%d",&n); h = 1.0 / (double) n;

#pragma omp parallel for private(i,x) reduction(+:suma) schedule(static)

for (i=0; i<n; i++) { x = (i + 0.5) * h; suma = suma + 4.0 / (1.0 + x*x); } pi = suma * h;

printf("\n pi(+/-) = %.16f \n", pi); }

Para establecer el número de hilos paralelos a ejecutar hemos utilizado la

función omp_set_num_threads (se puede indicar también mediante una variable de entorno). Por tanto, tal como lo indica el pragma parallel for, el bucle se va a ejecutar en paralelo entre 8 hilos.

Las variables i y x son privadas (diferentes para cada hilo), y la variable suma es de tipo reduction, compartida pero especial (no se va a actualizar la verdadera variable suma hasta el final; mientras tanto, OpenMP utilizará variables auxiliares).

Para repartir las iteraciones hemos utilizado planificación (scheduling) estática consecutiva (se pueden utilizar otras estrategias: estática entrelazada, dinámica chunk scheduling y guided).

El programa anterior es muy simple, pero todos los programas escritos en OpenMP tienen una estructura y apariencia similar.

Page 169: 6.1 INTRODUCCIÓN

9.2 PROGRAMACIÓN PARALELA: OpenMP y MPI (introducción) ▪ 337 ▪

9.2.2 MPI (Message Passing Interface)

En los sistemas de memoria distribuida (MPP) el espacio de memoria de los procesos es privado: no hay variables compartidas. Por tanto, la comunicación entre los procesos se realizará mediante paso de mensajes. Por otro lado, aunque en los sistemas DSM la memoria es compartida, es necesario enviar mensajes entre los procesadores para poder usar la memoria "externa".

En ambos casos, la herramienta más utilizada para explicitar la comunicación entre los procesadores es MPI. Las dos distribuciones libres más utilizadas son LAM y MPICH.

MPI no es un lenguaje de programación, sino que es un API (un conjunto grande de funciones de comunicación) que permite indicar, de manera explícita, el movimiento de datos y la sincronización entre los procesadores.

MPI sigue un modelo de programación de tipo SPMD; para repartir el trabajo entre los procesos se utiliza el identificador de cada proceso:

if (pid == 1) then ... else ...

La comunicación entre los procesos puede ser punto a punto —entre dos procesos— o global —todos los procesos toman parte en la misma—. Existen muchos modos de comunicación, siendo dos los principales: síncrono (las funciones de comunicación se bloquean hasta que ésta tiene lugar), y buffered (la información se envía y se deja en un búfer si es que el receptor no está preparado).

Aunque existen muchas funciones MPI, en muchos casos es suficiente con estas seis funciones:

- MPI_Init(&argc,&argv);

MPI_Finalize();

Todo programa MPI debe empezar y terminar con estas dos funciones.

- MPI_Comm_rank(comm,&pid);

MPI_Comm_size(comm,&npr);

Funciones para identificar los procesos: la primera devuelve el pid, y la segunda el número de procesos, npr.

Los procesos pueden agruparse en grupos denominados "comunicadores", y dentro de cada grupo utilizan un identificador

Page 170: 6.1 INTRODUCCIÓN

▪ 338 ▪ Capítulo 9: COMPUTADORES PARALELOS ACTUALES

diferente; la variable comm indica el grupo correspondiente. Inicialmente, todos los procesos están contenidos en el grupo denominado MPI_COMM_WORLD.

- MPI_Send(&message,count,datatype,dest,tag,comm);

- MPI_Recv(&message,count,datatype,source,tag,comm,&status);

Son las funciones principales de comunicación, para enviar o recibir un mensaje. Utilizan tres parámetros para especificar el mensaje: la dirección (&message), la longitud (count) y el tipo de dato (datatype). Además, se puede indicar un tag o marca, para poder establecer clases de mensajes (de datos, de control...). Para indicar el emisor (source) o el receptor (dest), se deben de utilizar los correspondientes pid.

Además de las funciones de comunicación punto a punto, se pueden realizar comunicaciones colectivas. En una comulación colectiva toman parte todos los procesos (o un determinado grupo de ellos). Entre otras, se pueden realizar las siguientes funciones de comunicación colectiva:

- MPI_Bcast(...): un broadcast, para enviar datos desde de un proceso a todos los demás.

- MPI_Scatter(...): para repartir los datos de un proceso entre todos.

- MPI_Gather(...): para agrupar en un proceso datos que están repartidos por todos los procesos (lo contrario de la función scatter).

- MPI_Reduce(...): para efectuar una operación asociativa (una suma, por ejemplo) con datos repartidos por todos los procesos y dejar el resultado en uno de ellos.

Finalmente, se dispone de una función explícita de sincronización de todos los procesos: MPI_Barrier.

En general, programar aplicaciones de memoria distribuida es más difícil

que en el caso de memoria compartida, ya que hay que indicar, expresamente, la comunicación entre los procesos; por ejemplo, si no se hace con gran cuidado, es muy fácil generar deadlocks o bloqueos. Veamos, como

Page 171: 6.1 INTRODUCCIÓN

9.2 PROGRAMACIÓN PARALELA: OpenMP y MPI (introducción) ▪ 339 ▪

ejemplo de un programa MPI, cómo efectuar el cálculo de pi (tal como hemos hecho antes con OpenMP):

#include <mpi.h> void main (int argc, char **argv) { int pid, npr, i, n; double h, x, pi, pi_loc, suma = 0.0; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&pid); MPI_Comm_size(MPI_COMM_WORLD,&npr); if (pid == 0) { printf(“\n Numero de intervalos --> ”); scanf("%d",&n); } MPI_Bcast(&n,1,MPI_INT,0,MPI_COMM_WORLD);

h = 1.0 / (double) n;

for (i=pid; i<n; i+=npr) { x = (i + 0.5) * h; suma = suma + 4.0 / (1.0 + x*x); } pi_loc = h * suma; MPI_Reduce(&pi_loc,&pi,1,MPI_DOUBLE,MPI_SUM,0, MPI_COMM_WORLD); if (pid == 0) printf("\n pi(+/-) = %.16f \n", pi); MPI_Finalize(); }

Todos los procesos ejecutan el mismo código, pero cada uno tiene un pid

diferente, que lo obtienen mediante la función MPI_Comm_rank. La función MPI_Comm_size devuelve el número de procesos paralelos (el número de procesos a ejecutar en paralelo se indica cuando se lanza a ejecutar el programa paralelo; por ejemplo, >mpirun -np 8 programa, para ejecutar 8 procesos).

Page 172: 6.1 INTRODUCCIÓN

▪ 340 ▪ Capítulo 9: COMPUTADORES PARALELOS ACTUALES

Luego, un proceso concreto, el que tiene el pid = 0, solicita el número de intervalos que se quieren utilizar para la suma (cuanto mayor sea ese número, el resultado será más preciso). Pero, dado que no existen variables compartidas, sólo él conocerá el valor de n. Por tanto, hay que ejecutar la función MPI_Bcast para que todos los procesos conozcan el valor de n.

A continuación viene la ejecución del bucle, pero repartido entre npr procesos. Para el reparto hemos utilizado planificación estática entrelazada, utilizando el identificador pid de cada proceso.

Una vez realizados los cálculos en cada proceso, hay que sumar todos los resultados parciales, para lo que ejecutamos la función MPI_Reduce: todos los procesos envían a P0 los resultados parciales y, además, se suman, quedando la suma global, pi, en el nodo indicado.

Por último, el proceso con pid = 0 imprime el resultado global, pi, que sólo él conoce.

Aunque el ejemplo anterior es muy simple, todos los programas MPI tienen una estructura similar.

OpenMP y MPI son las API más utilizadas para generar aplicaciones

paralelas, aunque no son las únicas; por ejemplo, también se puede usar UPC (Unified Parallel C), para programar sistemas de memoria compartida distribuida (por ejemplo, un cluster). Por otro lado, se están desarrollando lenguajes como OpenCL, derivados de los que se utilizan para programar las tarjetas gráficas, para utilizar los núcleos de los procesadores y los recursos de cómputo de la tarjeta gráfica del computador para efectuar cálculo paralelo

Junto a este tipo de herramientas de programación, cada vez hay más herramientas de ayuda al diseño, tales como debugger paralelos, analizadores de rendimiento, gestores de reparto de trabajo, o de gestión del propio cluster.