Capítulo III: El Modelo de Redes El Modelo de Redes III.1 Grafos El modelo de redes considera que una máquina paralela esta constituida por nodos de procesamiento conectados según un grafo que permite la transmisión de datos entre procesadores vecinos. El modelo promueve la creación de algoritmos paralelos que se adapten a la topología de la máquina donde se va a realizar la ejecución. Si al modelo PRAM se le acusa de exceso de idealismo, a este modelo se le suele acusar de exceso de concreción. El argumento utilizado es que la necesidad de adaptar el algoritmo a la topología implica una pérdida de portabilidad. Matemáticamente, la red de interconexión se puede ver como un modelo de grafo G = (V, A) dirigido o no: Los N procesadores están localizados en los vértices (nodos) del grafo y se comunican a través de los arcos (aristas). El grafo debe estar definido mediante una función recursiva (computable) en el número de N de nodos. El uso práctico de estas estructuras está limitado tanto por restricciones de cableado, como por principios de diseño y fabricación. Teniendo en cuenta estas limitaciones, se han sugerido una serie de criterios para evaluar estas organizaciones. Estos criterios ayudan a entender la efectividad de las redes en la codificación de algoritmos paralelos eficientes sobre el hardware real. Estos criterios son los siguientes: Grado o número de aristas por nodo. Interesa que el número de aristas por vértice sea constante e independiente del tamaño de la red, ya que así es posible construir redes con un número de procesadores arbitrariamente grande utilizando el mismo componente básico. Diámetro. El diámetro de una red es la distancia más larga entre dos nodos cualesquiera. El diámetro es una cota inferior en la complejidad de los algoritmos paralelos que requieren comunicaciones entre pares de vértices arbitrarios. Ancho de Bisección. Es el mínimo número b de aristas que deben ser eliminadas con el fin de dividir la red en dos mitades desconectadas del mismo tamaño (salvo una unidad). Tales aristas nos miden el ancho del "cuello" de botella para cierto tipo de comunicaciones. Si todos los procesadores en una de las mitades del "cuello" intenta simultáneamente enviar un volumen de datos D a la otra mitad, todos los datos deberán atravesar el cuello de botella. Por tanto, una cota inferior del tiempo tardado será Ω(D/b). 44 Ancho de Entrada/Salida. Si sólo se dispone de io puertos de entrada/salida a la red y es necesario introducir los D datos del problema en la red (o sacarlos), es obvio que se tardará Ω(D/io). III.2 Segmentos, Anillos, Mallas y Toros. En un segmento o pipeline de n procesadores, todos los nodos excepto el último n-1 y el primero 0 tienen dos vecinos. Cada nodo i<n-1 tiene como vecino a i+1 y cada nodo i > 0 tiene como vecino al nodo (i-1). Sus parámetros son Grado = 2, Diámetro = n, Ancho de Bisección = 1. Un anillo de n procesadores, se obtiene de un segmento enlazando sus extremos: cada nodo i tiene como vecinos a (i+1) MOD n e (i+n-1) MOD n. Sus parámetros son Grado = 2, Diámetro = n/2, Ancho de Bisección = 2. En una malla rectangular de n×n procesadores, los nodos están organizados en un plano. Cada nodo (i, j) comunica con los procesadores (i±1, j) e (i, j±1), supuesto que existan. El grado de cada vértice es igual a 4. El diámetro es igual a 2*(n-1). El ancho es igual a n. Si se conectan los extremos opuestos de cada segmento se obtiene una topología de toro. El resto del capítulo está dedicado a algunas de las topologías más utilizadas: anillos, redes, mallas, árboles, hipercubos Para cada topología se muestra un algoritmo de ejemplo, que intenta ser la concreción de un paradigma: segmentación, divide y vencerás, algoritmos ascendentes (llamados por otros autores árboles binarios equilibrados), programación dinámica y memoria compartida-distribuida. III.2.1 Ordenación en un Anillo La Figura 3.1 nos muestra una estructura de procesos en anillo descrita en el lenguaje occam. La Figura 3.2 muestra el código occam sort.element() para un elemento genérico i del anillo. Se supone que el tamaño del anillo number.elements es igual o mayor que el número N de elementos a ordenar. [number.elements + 1]CHAN OF LETTERS pipe: PAR inout(pipe[number.elements], pipe[0]) PAR i = 0 FOR number.elements sort.element(pipe[i], pipe[i+1]) Figura 3.1. Procesos en Anillo El proceso inout() inyecta en el anillo los elementos a ordenar a través del canal pipe[0]. El algoritmo es sencillo: cada procesador guarda en highest el elemento 45 mayor de entre los que recibe y envía los restantes hacia el resto del anillo usando el canal output. Al recibir el último elemento, el proceso inout() genera una señal de terminación end.of.letters que hace que los elementos salgan ordenados del "pipe". El proceso continúa activo ordenando nuevos vectores hasta que se recibe la señal terminate que finaliza el programa. PROC sort.element (CHAN OF LETTERS input, output) BYTE highest: BOOL going: SEQ going := TRUE WHILE going input ? CASE terminate going := FALSE letter; highest BYTE next: BOOL inline: SEQ inline := TRUE WHILE inline input ? CASE letter; next IF next > highest SEQ output ! letter; highest highest := next TRUE output ! letter; next end.of.letters SEQ inline := FALSE output ! letter; highest output ! end.of.letters output ! terminate : Figura 3.2. Código occam para el proceso sort.element Es trivial observar que el algoritmo tiene complejidad O(N), siendo N el número de elementos a ordenar. La complejidad del algoritmo es óptima para topologías en anillo. En el análisis del caso peor, cualquier algoritmo de ordenación implica una comunicación entre procesadores situados a distancia igual al diámetro del anillo. Por tanto, todo algoritmo de ordenación en un anillo tiene complejidad Ω(N). 46 III.2.2 La Mochila Entera El problema de la mochila entera puede ser formulado como sigue: dado un conjunto Q de n objetos diferentes Q = {1,2,...,n} y M una mochila de capacidad M, donde el objeto i tiene peso wi y beneficio pi, encontrar una combinación de enteros no negativos x1,...,xn s M/2 tal que el peso total no sobrepase la capacidad de la mochila, w1x1+...+wnxn ≤ M, y que s/2 el beneficio total, p1x1+...+pnxn, sea máximo. Si denotamos por bk el valor óptimo para una M mochila entera de capacidad k, se cumple s 0 Figura 3.3 Las fases del algoritmo entonces la siguiente relación: convolutivo s/2 b s = MAX ( bk + b s - k ) k =1 A partir de esta fórmula similar a la de la convolución de dos vectores es posible diseñar un algoritmo para una máquina "pipeline" con M procesadores [III.1]. El código paralelo para el procesador s está descrito en la Figura 3.4. Se utiliza una notación binaria para el operador MAX en vez de la clásica notación funcional. Así a MAX b denota el máximo de a y b. El procesador 0 ejecuta el código de iniciación. Para cada capacidad k ≤ s, el procesador s computa los valores de bk. En el primer bucle, para cada k < s/2, el procesador s recibe del procesador s-1 los valores bk. bk contiene la solución óptima para la capacidad k. Por tanto, después de almacenar este valor en bk, el procesador s únicamente tiene que replicarlo a su vecino derecho. Después de la recepción de bk en el segundo bucle, el valor de bss puede actualizarse de acuerdo con la fórmula anterior. Como s/2 ≤ k ≤ s, el valor bs-k fue actualizado en el primer bucle, por tanto los valores bk y bs-k están disponibles. Al final del segundo bucle, se envía el valor de bs completamente actualizado. Durante 1. for k := 1 to (s/2)-1 2. begin 3. in ? b[k]; 4. out ! b[k]; 5. end; 6. for k := s/2 to s-1 7. begin 8. in ? b[k]; 9. out ! b[k]; 10. b[s] := b[s] MAX (b[k]+b[s-k]); 11. end; 12. in ? temp; 13. b[s] := temp MAX b[s]; 14. out ! b[s]; 15. for k := (s+1) to M 16. begin 17. in ? b[k]; 18. out ! b[k]; 19. end; Figura 3.4. Algoritmo convolutivo paralelo. 47 el tercer bucle el procesador s permanece como un simple guía de los valores iniciales recibidos. Bajo la hipótesis más realista de que tenemos P+1, con P ≤ M, procesadores enumerados de 0 a P conectados de acuerdo a una topología de anillo. El siguiente algoritmo puede modificarse para ser ejecutado en M/P etapas. En la etapa i, el procesador k (P ≥ k ≥ 1) computa los valores b[i⋅P+k]. Los valores emitidos por el último procesador P, necesitados para la siguiente etapa, se almacenan en una cola por el procesador 0. Estos valores se envían tan pronto como los solicite el procesador 1. El algoritmo es óptimo y su complejidad es O(M2/p+n). III.2.3 Parentización Optima Considérese el problema de evaluar los productos de n matrices M = M1 x M2 x ... x Mn, donde cada Mi es una matriz con ri-1 filas y ri columnas. El orden en que se multiplican las matrices puede tener un efecto significativo en el número total de operaciones necesitadas para evaluar M, sin importar el algoritmo que se utilice para realizar el producto de matrices. Por ejemplo, sea: M = M1 M2 M3 M4 × × × [10x100] [20x50] [50x1] [1x100] [10x20] donde las dimensiones de cada matriz se muestran entre corchetes. Evaluar M asociando M1 × (M2 × (M3 × M4)) requiere 125000 operaciones, mientras que evaluar M asociando (M1 × (M2 × M3)) × M4 requiere sólo 2200 operaciones. Intentar todas los posibles órdenes con que evaluar el producto de las n matrices se convierte en un proceso exponencial, que resulta inaplicable cuando n es grande. Sin embargo, la programación dinámica ofrece un procedimiento eficiente para resolver este problema. Sea G(i, j) el mínimo costo de computar Mi x Mi+1 × ... × Mj, con 1 ≤ i ≤ j ≤ n. G(i, j) puede computarse entonces utilizando la fórmula: (III.1) G(i, j) = min {G(i, k) + G(k+1, j) + ri-1rkrj}, i ≤ k < j } Muchos problemas de ciencias de la computación pueden resolverse mediante el uso de esta ecuación de recurrencia biádica. Entre otros problemas de caminos mínimos, parentizaciones óptimas, problemas de emparejamiento, triangulaciones óptimas de un polígono, etc... La Figura 3.5 muestra el algoritmo secuencial.de orden O(n3) que resulta de aplicar la formula (III.1). Si se dispusiera de un "pipeline" con n procesadores podría asignarse a cada procesador j el cálculo de los valores óptimos G(i,j) para el conjunto Pj = {(i, j) / 1 ≤ i ≤ j}, 1 ≤ j ≤ n. El algoritmo paralelo de la Figura 3.6 realiza una asignación cíclica de las columnas Pj en un anillo con P = NUMPROCESSORS+1. El último procesador en el anillo administra una cola en la que guarda los valores generados por el penúltimo procesador. La complejidad de este algoritmo es el resultado del tiempo invertido en el cómputo, más el tiempo invertido en las comunicaciones. En la etapa k este tiempo es 48 O(n2). El número de etapas es n/p, por tanto, la complejidad total del algoritmo es O(n3/p). 1: begin 2: for i := 0 to num_matrices - 1 do 3: G[i][i] := 0; 4: for l := 0 to num_matrices - 2 do 5: for i := 0 to num_matrices - (l + 1) do 6: begin 7: j := (i + l) + 1; 8: for k := i to (i + j - 1) do 9: G[i][j] := min {G[i][j], G[i][k] + G[k+1][j] + r[i] * r[k+1]) * r[j+1]}; 10: end; 11: end; Figura 3.5. Algoritmo secuencial. 1: for b := 0 to bands 2: begin 3: j := (NUMPROCESSORS * b) + NAME; -- current column 4: last_column := (j = (num_matrices - 1)); 5: G[j][j] := 0; 6: if last_column then 7: out ! value; G[j][j]; 8: for m := 1 to j do 9: begin 10: i := j - m; 11: G[i][j] := INFINITY; 12: for k := i to j - 1 do -- receive, send and compute 13: begin 14: in ? CASE value; G[i][k]; 15: if NOT last_column then 16: out ! value; G[i][k]; 17: G[i][j] := min { G[i][j], G[i][k] + G[k+1][j] + r[i] * r[k+1] * r[j+1]}; 18: end 19: if NOT last_column then 20: out ! value; G[i][j]; -- send current value 21: end; 22: end; Figura 3.6. Algoritmo paralelo. Código para el procesador NAME. 49 III.3 Arboles y N-grafos En una topología de árbol binario (Figura 3.7) de profundidad k, los 2k+1-1 procesadores se organizan en un árbol binario completo de la misma profundidad. A cada nodo se le asocia un identificador entre 0 y 2k+1-2 mediante un recorrido del árbol por niveles. El máximo número de aristas de un nodo es tres. Cada nodo interior puede comunicar con sus dos hijos y cada vértice distinto de la raíz puede hacerlo con su padre. Su diámetro es 2*(k-1). Sin embargo el ancho de bisección es 1. 0 1 2 4 3 7 8 9 6 5 10 11 12 13 14 Figura 3.7. Arbol. La topología de N-Grafo introducida en [III.2] es una topología que presenta un grado constante no superior a 4 y un diámetro logarítmico en el número de procesadores. root leaf end bud Figura 3.8. Estructura 2-N-Grafo El número de nodos en un N-Grafo es 2k para k ≥ 2. Dado un k se habla de un k-N-Grafo de profundidad k. La estructura básica a partir de la cual se construyen todos los k-N-Grafos se denomina 2-N-Grafo (Figura 3.8). Como se puede observar, el proceso raíz (root) no tiene predecesores y los procesos hoja (leaf) no tienen sucesores. Al proceso hoja que no es descendiente directo del proceso raíz se le denomina final (end) y al predecesor de éste se le denomina brote (bud). 50 Para obtener un k-N-Grafo N, se combinan dos (k-1)-N-Grafos N1 y N2 de la siguiente manera (Figura 3.9): El nodo final (end) de N1 pasa a ser el nodo raíz de N y se insertan dos nuevos canales de comunicación desde el nuevo nodo raíz hacia los nodos raíces de N1 y N2. El nodo final (end) de N2 se convierte en el nodo final de N. Las hojas (leaves) y brotes (buds) de N1 y N2 se corresponden con las hojas y brotes de N. En el k-N-Grafo N los nodos distintos del nodo raíz, de las hojas, de los brotes y del nodo final son denominados nodos intermedios (inners). La propiedad que hace interesante a los N-grafos es la siguiente: Todo nodo interior de un N-grafo (incluyendo al nodo raíz) está conectado a una hoja o a un brote. N1 root leaf N2 end root bud leaf end bud N root inner leaf inner bud leaf end bud Figura 3.9. Creación de un k-N-grafo N III.3.1 Divide y Vencerás en Arboles y N-grafos. Los árboles y los N-grafos son topologías especialmente adecuadas para la realización de algoritmos divide y vencerás binarios. La aplicación de la técnica en un árbol es trivial y se obtiene simplificando la técnica que pasamos a describir para el N-grafo. 51 root inner leaf inner bud leaf end bud root inner inner end leaf bud leaf bud root inner inner end leaf bud leaf bud Figura 3.10. Proceso de División La implementación de la fase de división en un N-grafo, se divide en tres etapas. 52 root inner inner end leaf bud leaf bud root inner inner end leaf bud leaf bud root inner leaf inner bud leaf end bud Figura 3.11. Una forma de hacer la combinación en un N-grafo 53 • El proceso raíz divide el problema en dos subproblemas y envía cada uno de ellos a los procesos intermedios a sus hijos. • Cada nodo intermedio realiza las mismas operaciones que el proceso raíz. • En la última división los procesadores hoja y brote reciben un subproblema desde un proceso intermedio y lo dividen en dos, se quedan con una de las particiones y envían la otra al proceso intermedio desde el cual recibieron el subproblema. El procesador final (end) simplemente recibe un subproblema desde el procesador brote al que está conectado. La Figura 3.10 presenta la distribución de particiones en un 3-N-Grafo, en ella se han sombreado los procesos que realizan una división del subproblema concurrentemente. A continuación todos los procesadores del N-Grafo ejecutan el algoritmo secuencial elegido para la resolución del subproblema que les ha sido asignado. Hay diversas formas de hacer la fase inversa de combinación. Esta es una: • Los procesos brotes y los procesos intermedios reciben un resultado parcial del problema desde los procesos intermedios y desde los procesos hojas respectivamente y lo combinan con la solución parcial que ellos tienen. • Los procesos brotes envían la solución combinada al proceso intermedio al que están conectados. • Los procesos intermedios conectados a brotes, hacen una segunda combinación de la partición que les llega estos con la partición resultante de la combinación anterior. Una vez hecha esta segunda combinación, los procesadores envían el resultado al proceso intermedio al que están conectados. • A continuación, los procesos intermedios reciben una partición del problema de su hijo izquierdo y otra de su hijo derecho la combinan y envían a su padre. A todos los efectos el proceso raíz puede ser considerado como un proceso intermedio. En la Figura 3.11 se muestra el proceso de recogida de resultados, en ella se presentan sombreados los procesos que están realizando la combinación de resultados en paralelo. Mientras que en un árbol los procesadores en los nodos intermedios trabajan en las fases de división y combinación y sólo las hojas trabajan en la resolución de los subproblemas, en un N-grafo los nodos intermedios también trabajan en la fase de resolución. Esto ha sido posible porque todo nodo intermedio (incluyendo el nodo 54 raíz) puede verse como un "hijo" de un brote o de un nodo hoja. Es obvio entonces que la aceleración alcanzable en un divide y vencerás en un N-grafo esta limitada a dos veces la aceleración alcanzable en un árbol. III.4 Hipercubos Este tipo de topología consta de 2k procesadores formando un hipercubo k dimensional. Si se denotan a los nodos con los índices 0, 1, ..., 2k-1, dos vértices son adyacentes si sus etiquetas difieren en exactamente un único bit. El diámetro D de un hipercubo k-dimensional es k y su ancho A es 2k-1. Su grado no es constante, G = k. 100 101 000 001 110 111 010 011 Figura 3.12. Hipercubo Un hipercubo se puede definir recursivamente: Un hipercubo 0-dimensional esta formado por una sola máquina con nombre 0. Un hipercubo d dimensional se obtiene conectando los dos nodos con el mismo nombre name de dos hipercubos d-1 dimensionales. Cada nodo name en el segundo de los hipercubos se renombra como 2d+name. Nos referiremos a este segundo hipercubo d-1 dimensional como "el hipercubo alto en dimensión d". Naturalmente, al primero, lo denominaremos hipercubo bajo en dimensión d". El código en la Figura 3.14 describe una topología de hipercubo utilizando una de las extensiones de configuración de occam (versión D7205 [III.4]). De la definición se sigue que hay 2dim nodos en un hipercubo de dimensión dim. Puesto que, fijada la dimensión d, existe un enlace por cada uno de los nc = 2dim-1 nodos del hipercubo bajo se sigue que el número de enlaces necesitado es dim*2dim-1. Como los canales occam son unidireccionales, necesitamos declarar uno en cada sentido. Así, el vector de canales c se estructura en tres dimensiones: [dim][nc][io]CHAN OF INT c: 55 #INCLUDE "hostio.inc" VAL dim IS 3: -- three dimensional hypercube VAL P IS (1 << dim): -- number of nodes: 2dim VAL nc IS (1 << (dim -1)): -- number of links in each dimension VAL io IS 2: -- input and output #USE "root.cah" #USE "node.cah" -- code generated for PROC node CONFIG CHAN OF SP fs,ts: -- to connect with the "Host" machine PLACE fs, ts ON HostLink: [dim][nc][io] CHAN OF INT c: PAR PROCESSOR worker[0] root (fs,ts,[c[0][0][0],c[1][0][0],c[2][0][0]], [c[0][0][1],c[1][0][1],c[2][0][1]]) PAR i=1 FOR P-1 PROCESSOR worker[i] VAL b0 IS (i BITAND 1): VAL b1 IS ((i >> 1) BITAND 1): VAL b2 IS ((i >> 2) BITAND 1): VAL n0 IS ((b2 << 1) + b1): VAL n1 IS ((b2 << 1) + b0): VAL n2 IS ((b1 << 1) + b0): node (i, [c[0][n0][b0],c[1][n1][b1],c[2][n2][b2]], [c[0][n0][(1-b0)],c[1][n1][(1-b1)],c[2][n2][(1-b2)]]) : Figura 3.14. Topología de Hipercubo descrita en occam D7205 6 n 8 4 n 8 5 2 n 8 root n 0 worker 7 worker 3 n 8 n 4 n 4 1 n 2 worker 6 n 8 4 n 8 5 2 n 8 root 0 3 n 8 n 4 1 n 2 worker 7 worker n 4 worker Figura 3.13. Esquema de división/combinación en un hipercubo 56 Los canales con la tercera componente io a cero son de entrada para el hipercubo bajo. Como en C, los operadores << y >> indican desplazamiento a la izquierda y derecha respectivamente. El operador BITAND, equivalente del operador & de C, devuelve la palabra que resulta de la conjunción entre los bits de los dos operandos. III.4.1 Divide y Vencerás en un Hipercubo La configuración de procesadores en hipercubo puede ser utilizada para aplicar la técnica divide y vencerás. El procesador 0 recibe un problema de tamaño n a resolver desde el canal de entrada. Divide el problema en dos subproblemas, envía uno de los subproblemas a su vecino en dimensión 0 y mantiene el otro para sí. Posteriormente el procesador 0 y el procesador 1 simultáneamente dividen el problema en dos subproblemas, se quedan con un subproblema y envían el otro a sus vecinos en dimensión 1. En la iteración d-1 los procesadores 0, 1, 2, ...2d-1 - 1 dividen el subproblema que poseen en dos y envían una de las particiones a sus vecinos en dimensión d-1. Al final cada nodo tiene una partición del problema. En este momento, todos los nodos en paralelo hallan la solución al problema que les ha sido asignado. La parte superior de la Figura 3.13 muestra gráficamente cómo se hace la distribución de las particiones del problema. La combinación se realiza procediendo en sentido inverso a como se realizó la distribución. Los nodos 0, 1, 2,..,2d-1 - 1 reciben una solución desde sus vecinos en dimensión d -1, la combinan con la solución que ellos tienen en una partición y la envían a su vecino en dimensión d - 2. Se procede de esta manera hacia las dimensiones inferiores hasta que el proceso 0 reciba una solución en dimensión 0 y la combina con la que tiene formando la solución del problema original. Como veremos en la siguiente sección introduciendo una ligera variante en el algoritmo puede hacerse que la solución no sólo quede en el procesador 0 sino en todos los procesadores del hipercubo. Para ello basta convertir las comunicaciones unidireccionales hacia los hipercubos bajos en bidireccionales y hacer que los nodos en los nodos en el extremo "alto" de la dimensión también trabajen en la fase de combinación. III.4.2 Algoritmos Ascendentes. Un caso particular de la técnica divide y vencerás la constituye la familia de algoritmos ascendentes. En algunos casos, la fase de división del algoritmo puede hacerse de manera estática, en "tiempo de diseño". No es necesario esperar al tiempo de ejecución para saber la forma en la que es necesario dividir los datos. En tales casos la única fase requerida, si se dispone de los datos en los procesadores, es la de combinación. Algunos ejemplos de este tipo de problemas son los algoritmos de Reducción, las Sumas de Prefijos y la Transformada Rápida de Fourier. 57 Suma de Prefijos En este apartado retomamos el problema de la suma de prefijos presentado en el Capítulo I. Suponemos la existencia de un vector B[j] con j∈{0,..., M -1} de tamaño M =N*P distribuido en P trozos Bname[t] con t∈{0,..., N -1} de tamaño N entre los P procesadores name∈ {0,..., P-1}. Se trata de obtener las sumas parciales de los j=name*N+t primeros elementos de B. El resultado se deja también en un vector A[j] distribuido en trozos Aname[t] entre los procesadores, de manera que: (III.2) A[j] = ∑i=0, j Ai[t] = Aname[t] =∑i=0, name-1∑s=0, N-1 Bi[s]+ ∑s=0, t Bi [s] PROC node(VAL INT name, [] CHAN OF INT fn, tn) VAL N IS 1024: VAL dim IS 3: [N] INT A: INT sum, total, tname: SEQ ... initialize A SEQ i=1 FOR N-1 A[i] := A[i-1] + A[i] total := A[N-1] tname := 0 SEQ i=0 FOR dim SEQ PAR tn[i] ! total fn[i] ? sum IF ((name BITAND (1<<i)) = (1<<i)) tname := tname + sum TRUE SKIP total := total + sum SEQ i=0 FOR N A[i] := A[i] + tname : Figura 3.15. Suma de prefijos. Se pretende además que la suma A[M-1]de todos los elementos del vector B quede replicada en todos los procesadores. Este requerimiento se conoce como "Reducción todos a todos". Utilizaremos esta vez una topología de hipercubo. Cada procesador computa en total la suma de los elementos de su parte del vector (Figura 3.15). Después los procesadores pasan a intercambiar sus sumas en cada una de las dimensiones i= 0,...,dim-1. En cada dimensión i el procesador acumula en total el resultado de su suma y la del "hipercubo del otro lado". Además, si pertenece al hipercubo alto en la dimensión actual también acumula en tname la suma del "hiper58 cubo bajo". Al final del bucle, la variable total contiene la suma de todos los elementos de A y tname contiene la suma de los elementos almacenados en los procesadores anteriores. El algoritmo funciona no solamente para la suma, sino para cualquier operación binaria @ que sea asociativa y conmutativa. La Figura 3.16 ilustra el proceso. A 6 110 A 2 101 A 7 101 A 3 101 A 6 @ A 7 110 A 7 @ A 6 101 A 5 101 A 0 000 A0 A 1 001 A 2 @ A 3 101 A 3 @ A 2 101 A1 A 5 @A 4 101 A 0 @A 1 000 A 6 @ A 7@ A 4 @A 5 110 A 2 @ A 3@ A 0@ A 1 101 A 1 @A 0 001 A 7 @ A 6@ A 5 @A 4 101 A 3 @ A 2@ A 1@ A 0 101 A 7 @ A 6@ A 5@ A 4 101 A 0 @A 1 @A 2 @ A 3 000 A 1 @A 0@ A 3 @ A 2 001 Figura 3.16. Se muestran los contenidos de la variable total del código de la Figura 3.15. Las flechas indican la dimensión en la que ocurre el intercambio de valores. El Problema de la Mochila 0-1 En el problema de la mochila 0/1 KNAP(G, C) se supone dado un conjunto G de n objetos diferentes y una mochila. El objeto k tiene un peso w[k] y una ganancia p[k], 1 ≤ k ≤ n, y la mochila tiene una capacidad C; w[k], p[k] y C son enteros positivos. Podemos asumir que w[k] ≤ C, 1 ≤ k ≤ n. El problema consiste en encontrar una combinación z z = (z[1], z[2], ..., z[n]) con z[k] ∈ {0, 1} 59 donde z[k] = 1 si k esta incluido en la mochila y z[k] = 0 si no. Se busca z tal que ∑k=1,n z[k]*p[k] es máximo sujeto a la condición ∑k=1,n z[k]*w[k] ≤ C Denotemos por f[k][c] el valor de la solución óptima para el problema de la mochila con los primeros k objetos y capacidad c. Se puede obtener la siguiente relación: (III.3) f[k][c] = max { f[k-1][c], f[k-1][c - w[k]] + p[k]}, para 0 ≤ c ≤ C, y k = 1, 2, ..., n. Los valores iniciales vienen dados por: (III.4) f[0][c] = 0 si c ≥ 0 y f[0][c] =-∞ si c < 0. El problema se resuelve usando la técnica de programación dinámica clásica, generando los vectores f[1], f[2], ..., f[n] sucesivamente Figura 3.17. 1. for c = 0 to C do 2. f[0][c] := 0; 3. for k = 1 to n do 4. for c = 0 to C do 5. begin 6. if c < w[k] then f[k][c] := f[k-1][c] 7. else f[k][c] := max {(f[k-1][c-w[k]] + p[k]), f[k-1][c]}; 8. end; Figura 3.17. Algoritmo Secuencial de Programación Dinámica. El primer bucle (línea 3) se realiza sobre los objetos y el segundo (línea 4) sobre las capacidades, de tal forma que el algoritmo resultante calcula f[k][c] para cada pareja (k, c), 1 ≤ k ≤ n, 0 ≤ c ≤ C. La complejidad del algoritmo es obviamente O(nC). Sea P el número de procesadores en el hipercubo, el algoritmo consiste en que los P procesadores ejecutan los siguientes pasos: Paso 1.- Descomposición: El problema original KNAP(G, C) es dividido en P p-1 subproblemas KNAP(Gi, C) con i = 0, 1, ..., P - 1, tal que G = ∪G i y Gi ∩ Gj = i=0 ∅ si i ≠ j, y | Gi |= | G| = s para todo i. Asignando el subproblema KNAP(Gi, C) al p 60 procesador con NAME = i. El tamaño del problema a resolver por el procesador NAME es n/P. Los subproblemas con objetos k pertenecientes al intervalo [NAME * n/P, (NAME + 1) * n/P) son asignados al procesador NAME. Paso 2.- Programación Dinámica: El subproblema KNAP(GNAME,C) es resuelto por el procesador NAME usando el algoritmo de la Figura 3.17. Cada procesador genera el vector aNAME = (aNAME[0], aNAME[1],..., aNAME[C]). de beneficios óptimo para cada subproblema Paso 3.- Combinación: Combinar los vectores de beneficio obtenidos como resultado de resolver los P subproblemas generados en un vector de beneficios resultado del problema KNAP(G, C). Sea b y d dos vectores de beneficio óptimo KNAP(B, C), KNAP(D, C) tal que B ∩ D = ∅. La combinación se define mediante la operación @ conmutativa y asociativa: e = b @ d, donde e[i] = max{b[j] + d[i-j]} con i = 0, ..., C; j = 0, ..., i. Es fácil, comprobar que una operación de combinación conlleva una complejidad de O(C2) para dos vectores de tamaño C. La operación de combinación es conmutativa y asociativa. Debido a esto, podemos decir que el vector de beneficios final para KNAP(G, C) puede ser obtenido del conjunto {a0, a1, a2, ....., ap-1} de vectores de beneficio generados combinándolos en cualquier orden. La aplicación directa del algoritmo de la figura 6 genera un número de operaciones (log P)*C2. Balanceando la carga entre los procesadores la complejidad puede ser mejorada hasta O((n*C)/P+C2). III.5 Mariposas Una mariposa o "butterfly" N-dimensional tiene (N+1)*2N nodos y N*2N+1 aristas. Los nodos se corresponden con pares (w,i) donde i es el nivel o dimensión del nodo y w es un número que denota la fila del nodo. Dos nodos (w,i) y (w',i') están conectados si, y sólo si (i' = i+1 ∧ w = w' ) ∨ ( i' = i+1 ∧ w = w' XOR 2i ) Figura 3.18 Es costumbre convertir la mariposa en una topología 61 cilíndrica haciendo que las columnas inicial y final coincidan. Existe una correspondencia natural entre cada nodo del hipercubo y cada fila de la mariposa. La Figura 3.18 muestra una mariposa con 3 dimensiones y 8 filas. Se puede obtener un hipercubo de una mariposa simplemente colapsando los nodos en la misma fila y observando como se solapan los enlaces proyectados. Recíprocamente, se puede obtener una mariposa de un hipercubo desplegando cada nodo en una fila de N+1 nodos y haciendo que cada uno de estos N+1 procesadores se ocupe de un enlace en una dimensión diferente. Cada uno de los nodos desplegado de una fila se enlaza con el nodo siguiente en la fila correspondiente a la dimensión de la que se hace cargo. De aquí se sigue que un hipercubo con N nodos puede ser simulado en log(N) pasos por una mariposa con N*(log(N)+1) nodos. La mariposa tiene un grado de 4, un diámetro O(log(N)) y un ancho de bisección Θ(N/log(N)). III.5.1 Simulación del Modelo PRAM por una Mar iposa Cualquier algoritmo Λ que se ejecuta en tiempo T =TPar(Λ,P) en una (P,M)EREW PRAM con P procesadores y M celdas de memoria compartida puede traducirse a un algoritmo Λb que se ejecuta en tiempo O(T*(P/P'+log(P'))) en una máquina mariposa (butterfly) con P' ≤ P procesadores con una probabilidad próxima a uno. Esquema de la demostración/implementación: Supongamos que P' = 2r y que M = 2s. Cada uno de los P' procesadores en la mariposa emula a un conjunto P/P' de los P procesadores de la (P,M)-PRAM. Cada procesador se encarga también de la emulación de M/P' celdas de memoria compartida usando para ello parte de su memoria privada. El problema principal es ¿Qué hacer si algunos de los P procesadores PRAM acceden a la memoria compartida?. La solución pasa por disponer de una función hΛ hΛ : [0,M-1] → [0,P'-1]×[0,M/P'-1] Λ h (m) = (hΛP(m), hΛM(m))∈ [0,2r-1]×[0,2s-r-1] Esta aplicación puede ser generada por el compilador a partir del programa PRAM Λ El valor hΛ (m) para una dirección m de memoria compartida dada, nos determina el procesador hΛP(x) que la guarda y la dirección hΛM(x) en la que está almacenada en dicho procesador hΛP(x). Entonces cada procesador físico, según avanza en el bucle de tamaño P/P' de virtualización va lanzando las demandas del correspondiente procesador virtual utilizando la función hΛ. De esta manera se solapan cómputo y comunicaciones. El paralelismo de segmentación que se produce hace que el tiempo de latencia O(log(P')) marcado por el diámetro de la mariposa sólo tenga que ser contabilizado una vez. La función hΛ puede construirse a partir una función gΛ biyectiva en el espacio de las direcciones de memoria compartida: gΛ : [0, M-1] → [0, M-1] 62 y definiendo las funciones componentes hP y hM de h en el espacio de nombres de procesador y de direcciones de memoria como los números que cumplen la ecuación: gΛ (m) = hM(m)*2r +hP(m) o lo que es lo mismo: hM(m) = gΛ (m) div 2r y hP(m) = gΛ (m) mod 2r La función gΛ es construida por el compilador adaptada a los patrones de acceso del algoritmo Λ de manera que los accesos resulten lo más uniformemente distribuidos y la congestión sea escasa. Una mejora adicional es hacer que el compilador calcule una función gΛ,A distinta adaptada a los patrones de acceso a la variable A declarada como compartida en el programa PRAM Λ. Lo ideal es que el cómputo de gΛ,A pueda hacerse en tiempo constante. Normalmente, la construcción de la función gΛ,A está basada en técnicas de "hashing". En ocasiones una combinación de "mappings" cíclicos y por bloques como las utilizadas en algunos lenguajes paralelos es suficiente [III.1]. El problema de encontrar la asignación de una variable a los procesadores de manera que se minimice la congestión en los procesadores puede formalizarse como sigue. Para un paso PRAM s y un procesador i denotamos por hs,i el valor: [III.1] hs,i (gΛ,A)= max {ins,i (gΛ,A), outs,i (gΛ,A)} donde ins,i(gΛ,A) es el número de peticiones a la memoria compartida administrada por el procesador i de la mariposa en el paso s para la distribución de memoria fijada gΛ,A • outs,i(gΛ,A) es el número de peticiones realizadas por el procesador i de la mariposa en el paso s, para la distribución de memoria gΛ,A El operador de máximo en [III.1] puede ser sustituido por el de suma según sea el número de puertos y la capacidad de entrada/salida paralela que tenga la red. El problema es buscar la función g dentro de una familia Ψ de funciones analíticas cuyo cómputo pueda ser implementado de manera muy eficiente (en tiempo constante) y tal que minimice el desequilibrio de la congestión en los procesadores de la mariposa: • [III.2] ming∈Ψ ∑ s =0,..,R max i = 0,..,P-1|hs,i (g)-E(hs(g))| donde R denota el número de pasos del programa PRAM y E(hs(g)) es el promedio de peticiones a memoria en el paso s. (III.5) E(hs(g)) = (∑ i =0,..,P-1 hs,i (g))/P Si la variable A implicada en los accesos a memoria en el paso es relativamente pequeña, puede ser factible renunciar a que g se pueda expresar en forma analítica y 63 duplicar la cantidad de memoria requerida para A, utilizando para su implementación una estructura de punteros a procesadores. La resolución del problema [III.2] resuelve también parte del problema de equilibrio de la carga de trabajo entre los procesadores planteado en la sección I.7. Obsérvese que en esta simulación la asignación de los procesadores PRAM a los procesadores de la mariposa es estática y se determina al comienzo de la computación. Puede ocurrir que los procesadores asignados a algunos procesadores físicos de la mariposa dejen de participar temporalmente en la ejecución como consecuencia de la evaluación de condiciones lógicas en sentencias condicionales y bucles, mientras los procesadores PRAM de otros procesadores físicos de la mariposa se mantienen participando. De esta manera se produciría un desequilibrio en la carga de trabajo. El problema de encontrar una buena asignación de los procesadores PRAM a los procesadores físicos de la mariposa está asociado al problema de la asignación de las direcciones de memoria compartida a los procesadores. Se puede demostrar que para cada programa PRAM Λ y cada variable EREW A existen funciones gΛ,A que garantizan que el envío de las peticiones y las respuestas está libre de congestiones con una probabilidad próxima a uno [III.5]. Por tanto los accesos pueden realizarse en tiempo O(P/P' + log(P')). III.6 Referencias [III.1] Almeida, García, Morales y Rodríguez. A Parallel Algorithm for the Integer Knapsack Problem for Pipeline Networks. Journal of Parallel Algorithms and Applications. Vol 6 (3). 1995. Gordon and Breach. [III.2] Gorlatch S., Lengauer. C. A Topology for Parallel Divide-and-Conquer on Transputer Networks. Transputer Applications and Systems’95. 394-409. IOS Press, 1995. [III.3] High Performance Fortran Forum. High Performance Fortran Language Specifications, 1993. [III.4] Inmos Limited. Occam Toolset Manual parts I and II. D7205 Release. Inmos Ltd. 1991. Se pueden encontrar compiladores de occam disponibles en la dirección: http://www.hensa.ac.uk/parallel/occam/index.html [III.5] Leighton, F.T. Introduction to Parallel Algorithms and Architectures. Arrays. Trees. Hypercubes. Morgan Kaufman Pub. 1992. [III.6] Morales D.G., Roda J.L,, Almeida F., Rodríguez C., García F. Integral Knapsack Problems: Parallel Algorithms and their Implementations on Distributed Systems. International Conference on Supercomputing. 218-226. 1995. ACM Press. 64 EL MODELO DE REDES ....................................................... 44 III.1 Grafos......................................................................................................... 44 III.2 Segmentos, Anillos, Mallas y Toros. .......................................................... 45 III.2.1 Ordenación en un Anillo ........................................................................ 45 La Mochila Entera............................................................................................ 47 III.2.3 Parentización Optima............................................................................. 48 III.3 Arboles y N-grafos ..................................................................................... 50 III.3.1 Divide y Vencerás en Arboles y N-grafos. ............................................. 51 III.4 Hipercubos ................................................................................................. 55 III.4.1 Divide y Vencerás en un Hipercubo ....................................................... 57 III.4.2 Algoritmos Ascendentes. ....................................................................... 57 III.5 Mariposas................................................................................................... 61 III.5.1 Simulación del Modelo PRAM por una Mariposa................................... 62 III.6 Referencias................................................................................................. 64 65