Capítulo III: El Modelo de Redes

Anuncio
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
Descargar