Grafos de dependencias + Espacio de iteraciones do i = 2, N-1 do j = 1, N-2 1 A(i,j) = A(i,j-1) * 2 2 C(i,j) = A(i-2,j+1) + 1 enddo enddo (Gp:) 1 (Gp:) 2 (Gp:) A 2,-1 (Gp:) A 0,1
(Gp:) i (Gp:) j
Dependencias ? Test de dependencias: únicamente para funciones lineales del índice del bucle. (Gp:) do i = L1, L2 X(a*i+b) = = X(c*i+d) enddo (Gp:) ?
(Gp:) L1 (Gp:) L2 (Gp:) i
(Gp:) a*i+b
(Gp:) c*i+d
i1 i2 (Gp:) d – b (Gp:) MCD(c,a) (Gp:) ? Z ? no hay depend.
? Paralelizar el código significa repartir tareas (iteraciones de un bucle) a procesadores. Pero hay que respetar las dependencias de datos. El problema principal son las dependencias de distancia > 0, y sobre todo aquellas que forman ciclos en el grafo de dependencias.
? Siempre es posible ejecutar un bucle en P procesa-dores, añadiendo sincronización para respetar las dependencias de datos… … lo que no significa que siempre sea sensato hacerlo, pues hay que tener en cuenta el coste global: cálculo + sincronización (comunicación). Dependencias
Paralelización de bucles Objetivos: ? Repartir las iteraciones de un bucle entre los procesadores, para que se ejecuten “a la par”. ? Siempre que se pueda, que se ejecuten de manera independiente, sin necesidad de sincronizar (dependencias de distancia 0). ? En función de las características del sistema (comunicación, reparto de tareas…) intentar que las tareas tengan un cierto tamaño.
Paralelización de bucles Objetivos: ? Tal vez sólo se pueda utilizar un número limitado de procesadores. ? Atención al rendimiento (p. e., problemas en la cache).
Objetivos: ? En los bucles de más de una dimensión, paralelizar aquella dimensión que minimice la necesidad de sincronización entre procesadores y aumente el tamaño de grano. Paralelización de bucles do i = 0, N-1 do j = 1, M-1 A(i,j) = A(i,j-1) + 1 enddo enddo (Gp:) P0 (Gp:) P1 (Gp:) P2 (Gp:) P3
Paralelización de bucles (Gp:) P0 (Gp:) P1 (Gp:) P2 (Gp:) P3
do i = 0, N-1 do j = 1, M-1 A(i,j) = A(i,j-1) + 1 enddo enddo Objetivos: ? En los bucles de más de una dimensión, paralelizar aquella dimensión que minimice la necesidad de sincronización entre procesadores y aumente el tamaño de grano.
? Si todas las dependencias son de distancia 0, las iteraciones se pueden repartir como se quiera entre los procesadores.
? Si no es así: ? si todas las dependencias son hacia adelante, sincronizarlas mediante barreras. ? si van hacia adelante y hacia atrás, sincronizarlas punto a punto (contadores o vectores de eventos). ? intentar alguna transformación del bucle para llevar a distancia 0 las dependencias. Bucles con y sin sincron.
Ejemplos do i = 0, N-1 C(i) = C(i) * C(i) A(i) = C(i) + B(i) 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 (Gp:) 1 (Gp:) 2 (Gp:) 3 (Gp:) C, 0 (Gp:) A, 0
(Gp:) i=0 1 2 3
Ejemplos do i = 1, N-1 C(i) = C(i) * C(i) A(i) = C(i) + B(i) D(i) = C(i-1) / A(i) enddo 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 (Gp:) i=1 2 3 4
(Gp:) barrera
(Gp:) 1 (Gp:) 2 (Gp:) 3 (Gp:) C, 0 (Gp:) A, 0 (Gp:) C, 1
Ejemplos do i = 1, N-1 C(i) = C(i) * C(i) A(i) = C(i) + B(i) D(i) = C(i-1) / A(i) enddo (Gp:) i=1 2 3 4
(Gp:) barrera
doall i = 1, N-1 C(i) = C(i) * C(i) A(i) = C(i) + B(i) enddoall [ BARERRA (…) ] doall i = 1, N-1 D(i) = C(i-1) / A(i) enddoall (Gp:) 1 (Gp:) 2 (Gp:) 3 (Gp:) C, 0 (Gp:) A, 0 (Gp:) C, 1
Vectores de eventos no ordena las dependencias mucho espacio en memoria Ejemplos do i = 2, N-2 A(i) = B(i-2) + 1 B(i+1) = A(i-2) * 2 enddo (Gp:) 1 (Gp:) 2 (Gp:) A,2 (Gp:) B,3
(Gp:) 1 1 1 . . .
2 2 2 . . .
1 1 1
2 2 2 (Gp:) i=2 3 4 5 6 7
doacross i = 2, N-2
wait (vB,i-3) A(i) = B(i-2) + 1 post (vA,i)
wait (vA,i-2) B(i+1) = A(i-2) * 2 post (vB,i)
enddoacross
Ejemplos do i = 2, N-2 A(i) = B(i-2) + 1 B(i+1) = A(i-2) * 2 enddo doacross i = 2, N-2
wait (vB,i-3) A(i) = B(i-2) + 1 post (vA,i)
wait (vA,i-2) B(i+1) = A(i-2) * 2 post (vB,i)
enddoacross (Gp:) 1 (Gp:) 2 (Gp:) A,2 (Gp:) B,3
(Gp:) , 3
(Gp:) 12 13 14
22 23 24
15 16 17
25 26 27
… (Gp:) P0 P1 P2
Ejemplos do i = 2, N-2 A(i) = B(i-2) + 1 B(i+1) = A(i-2) * 2 enddo (Gp:) 1 (Gp:) 2 (Gp:) A,2 (Gp:) B,3
(Gp:) 12 13 14
22 23 24
15 16 17
25 26 27
… (Gp:) P0 P1 P2
doacross i = 2, N-2,3
A(i) = B(i-2) + 1 wait (KA, i-1) post (KA)
B(i+1) = A(i-2) * 2
enddoacross Contadores ordena las dependencias sin problemas de espacio en memoria
1 1 1 1 1 1 1 1
2 2 2 2 2 2 2 2 ? En general, los bucles de dependencias siempre son problemáticos y hay que analizar detenidamente qué se gana y qué se pierde (coste de la sincronización, problemas de acceso a cache…). (Gp:) 1 (Gp:) 2 (Gp:) C,1 (Gp:) D,2
> Alternativas para el ejemplo — wait / post ?? — 3 procesos independientes ?? — ejecución serie ?? Ejemplos
Optimizaciones 0. Deshacer la definición de constantes y las variables de inducción.
1. Eliminar todas las dependencias que no son intrínsecas al bucle. Por ejemplo: do i = 0, N-1 X = A(i) * B(i) C(i) = SQRT(X) D(i) = X – 1 enddo doall i = 0, N-1 X(i) = A(i) * B(i) C(i) = SQRT(X(i)) D(i) = X(i) – 1 enddoall (Gp:) convertir X en una variable privada
Optimizaciones 2. Fisión del bucle. Si una parte del bucle hay que ejecutarla en serie, romper el bucle convenientemente y ejecutar lo que sea posible en paralelo. do i = 1, N-1 A(i) = A(i-1) / 2 D(i) = D(i) + 1 enddo do i = 1, N-1 A(i) = A(i-1) / 2 enddo
doall i = 1, N-1 D(i) = D(i) + 1 enddoall
Optimizaciones 3. Ordenar las dependencias (hacia adelante). do i = 1, N-1 A(i) = D(i-1) / 2 D(i) = C(i) + 1 enddo forall i = 1, N-1 D(i) = C(i) + 1 BARRERA (…) A(i) = D(i-1) / 2 endforall (Gp:) 1 (Gp:) 2 (Gp:) D, 1
(Gp:) 2………….1 2………1 2…….1
(Gp:) 1…2.. 1…2.. 1…2.. (Gp:) ??
Optimizaciones 4. Alinear las dependencias: peeling. do i = 1, N-1 A(i) = B(i) C(i) = A(i-1) + 2 enddo (Gp:) 1 (Gp:) 2 (Gp:) A, 1
doall i = 1, N-2 A(i) = B(i) C(i+1) = A(i) + 2 enddoall C(1) = A(0) + 2 A(N-1) = B(N-1)
Optimizaciones do i = 2, N-1 A(i) = B(i) C(i) = A(i-1) + A(i-2) enddo (Gp:) 1 (Gp:) 2 (Gp:) A, 2 (Gp:) A, 1
C(2) = A(1) + A(0) C(3) = B(2) + A(1)
doall i = 2, N-3 A(i) = B(i) X(i+1) = B(i+1) C(i+2) = X(i+1) + A(i) enddoall
A(N-2) = B(N-2) A(N-1) = B(N-1) do i = 2, N-1 A(i) = B(i) X(i) = B(i) C(i) = X(i-1) + A(i-2) enddo (Gp:) 1’ (Gp:) 2 (Gp:) A, 2 (Gp:) X, 1 (Gp:) 1
Optimizaciones 5. Generar hilos independientes do i = 0, N-3 A(i+1) = B(i) + 1 B(i+2) = A(i) * 3 C(i) = B(i+2) – 2 enddo B(2) = A(0) * 3 C(0) = B(2) – 2
doall k = 0, 2 do i = pid, N-4, 3 A(i+1) = B(i) + 1 B(i+3) = A(i+1) * 3 C(i+1) = B(i+3) – 2 enddo enddoall
A(N-2) = B(N-3) + 1 (Gp:) 1 (Gp:) 2 (Gp:) 3 (Gp:) B,0 (Gp:) B,2 (Gp:) A,1
1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3
Optimizaciones 6. Minimizar la sincronización do i = 2, N-1 B(i) = B(i) + 1 C(i) = C(i) / 3 A(i) = B(i) + C(i-1) D(i) = A(i-1) * C(i-2) E(i) = D(i) + B(i-1) enddo (Gp:) 1 (Gp:) 2 (Gp:) 3 (Gp:) B,0 (Gp:) B,1 (Gp:) A,1 (Gp:) 4 (Gp:) 5 (Gp:) C,1 (Gp:) C,2 (Gp:) D,0
doacross i = 2, N-1 B(i) = B(i) + 1 C(i) = C(i) / 3 post (vC,i)
wait (vC,i-1) A(i) = B(i) + C(i-1) post (vA,i)
wait (vA,i-1) D(i) = A(i-1) * C(i-2) E(i) = D(i) + B(i-1) enddoacross (Gp:) 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 (Gp:) i=2
3
4 …
Optimizaciones 7. Tratamiento de bucles: intercambio. (Gp:) do i do_par j
(Gp:) do_par i do j
(Gp:) do j do_par i
(Gp:) do_par j do i
Optimizaciones Ejemplo: do i = 1, N-1 do j = 0, N-1 A(i,j) = A(i-1,j) + 1 enddo enddo (Gp:) do i = 1, N-1
doall j = 0, N-1 A(i,j) = A(i-1,j) + 1 enddoall
enddo
(Gp:) j=0 1 2 3 (Gp:) i=1
2
3
4
Optimizaciones Ejemplo: (Gp:) doall j = 0, N-1
do i = 1, N-1 A(i,j) = A(i-1,j) + 1 enddo
enddoall
do i = 1, N-1 do j = 0, N-1 A(i,j) = A(i-1,j) + 1 enddo enddo (Gp:) j=0 1 2 3 (Gp:) i=1
2
3
4
Optimizaciones 8. Tratamiento de bucles: cambio de sentido. (Gp:) j=0 1 2 (Gp:) i=1
2
3
4
do i = 1, 100 do j = 0, 2 A(i,j) = A(i,j) – 1 D(i) = A(i-1,j+1) * 3 enddo enddo do j = 2, 0, -1
doall i = 1, 100 A(i,j) = A(i,j) – 1 D(i) = A(i-1,j+1) * 3 enddoall
enddo
Optimizaciones do j = 1, 2N-1 doall i = max(1,j-N+1), min(j,N) A(i,j-(i-1)) = A(i-1,j-(i-1)) + A(i,j-1-(i-1)) enddoall enddo 9. Skew do i = 1, N do j = 1, N A(i,j) = A(i-1,j) + A(i,j-1) enddo enddo
Optimizaciones 10. Otras optimizaciones típicas
Aumento del tamaño de grano y reduccción del overhead de la paralelización.
– Juntar dos bucles en uno (¡manteniendo la semántica!): fusión. – Convertir un bucle de dos dimensiones en otro de una sola dimensión: colapso o coalescencia. …
Reparto de tareas / iterac. ? ¿Cómo se reparten las iteraciones de un bucle entre los procesadores? Si hay tantos procesadores como iteraciones, tal vez una por procesador.
? Pero si hay menos (lo normal), hay que repartir. El reparto puede ser:
estático: en tiempo de compilación. dinámico: en ejecución.
Reparto de tareas / iterac. ? El objetivo: intentar que el tiempo de ejecución de los trozos que se reparten a cada procesador sea similar, para evitar tiempos muertos (load balancing). ? En todo caso, OJO con las dependencias (sincronización), el tamaño de grano, la localidad de los accesos y el coste del propio reparto.
Reparto de tareas / iterac. ? Planificación estática Qué ejecuta cada procesador se decide en tiempo de compilación. Es por tanto una decisión prefijada.
Cada proceso tiene una variable local que lo identifica, pid [0..P-1].
Dos opciones básicas: reparto consecutivo y reparto entrelazado.
Reparto de tareas / iterac. – No añade carga a la ejecución de los threads. – Pero no asegura el equilibrio de la carga entre los procesos. – Permite cierto control sobre la localidad de los accesos a cache. ? Consecutivo
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 ? Entrelazado
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3 principio = pid * N/P fin = (pid+1) * N/P – 1
do i = principio, fin … enddo do i = pid, N, P … enddo
Reparto de tareas / iterac. ? Equilibrio en el reparto de carga (Gp:) 1-20 (Gp:) 21-40 (Gp:) 41-60 (Gp:) 61-80 (Gp:) estático (fijo)
(Gp:) Tej
(Gp:) asignación dinámica de una nueva tarea
(Gp:) 1-10 (Gp:) 21-30 (Gp:) 11-20 (Gp:) 31-40
(Gp:) Tej
do i = 1, 80 if (A(i) > 0) calcular() enddo
Reparto de tareas / iterac. ? Planificación dinámica Para intentar mantener la carga equilibrada, las tareas se van escogiendo en tiempo de ejecución de un cola de tareas. Cuando un proceso acaba con una tarea (un trozo del bucle) se asigna un nuevo trozo.
Dos opciones básicas: los trozos que se van repartiendo son de tamaño constante o son cada vez más pequeños.
Reparto de tareas / iterac. Las iteraciones se reparten una a una o por trozos ? Self / Chunk scheduling Añade carga a la ejecución de los threads. Hay que comparar ejecución y reparto. LOCK (C); mia = i; i = i + Z; Z = 1 ? self UNLOCK (C); while (mia <= N-1)
endwhile do j = mia, min(mia+Z-1, N-1) … enddo LOCK (C) mia = i; i = i + Z; UNLOCK (C)
Reparto de tareas / iterac. Los trozos de bucle que se reparten son cada vez más pequeños según nos acercamos al final. ? Guided / Trapezoidal ? Guided : parte proporcional de lo que queda por ejecutar:
Zs = (N – i) / P (entero superior)
que equivale a:
Zi = Zi-1 (1 – 1/P)
Reparto de tareas / iterac. ? Trapezoidal: reduciendo el trozo anterior en una constante: Zi = Zi-1 – k (Gp:) op. de planificación (Gp:) Z1 (Gp:) Zn (Gp:) 1 (Gp:) n (Gp:) 2 (Gp:) i (Gp:) k (Gp:) Z2
Reparto de tareas / iterac. ? En general, el reparto dinámico busca un mejor equilibrio en el reparto de carga, pero:
– hay que considerar la carga que se añade (overhead), en relación al coste de las tareas que se asignan.
– hay que considerar la localidad en los accesos a los datos y los posibles problemas de falsa compartición.
Reparto de tareas / iterac. ? Ejemplo de reparto (1.000 iteraciones, 4 procesadores):
? chunk (Z = 100) 100 100 100 100 100 100 100 100 100 100 (10) ? guided quedan: 1000 750 562 421 … 17 12 9 6 4 3 2 1 se reparten: 250 188 141 106 … 5 3 3 2 1 1 1 1 (22)
? trapezoidal (Z1 = 76, Zn = 4 >> k = 3) 76 73 70 67 … 16 13 10 7 4 (25)
Paralelismo de función ? Paralelismo a nivel de procedimiento o función:
(Gp:) F2 (Gp:) F3 (Gp:) F1 (Gp:) F4 (Gp:) F5
– modelo Fork / Join – Parallel sections
Fork F1 F2 Join
Fork F3 F4 Join F5 doall k = 0, 1 if (pid = 0) then F1 if (pid = 1) then F2 endoall [barrera]
doall k = 0, 1 if (pid = 0) then F3 if (pid = 1) then F4 endoall F5
? El objetivo de un sistema de P procesadores es ejecutar el código P veces más rápido. ? Problemas (algunos): – ¿puede ejecutarse todo el código en paralelo? ¿está bien repartida la carga? – ¿dónde están los datos? ¿cuál es el coste de la transmisión de datos? – ¿cuál es el coste de la sincronización? – ¿es eficiente el uso de la memoria cache? Rendimiento
? Ejecución en un procesador (código serie, no la versión paralela de 1 procesador) ? Ts
Ejecución en P procesadores ? Tp fa = Ts / Tp ? límite P ideal: crecer linealmente con P
efic = fa / P ? límite 1 ideal: constante Rendimiento
La parte más lenta (serie) en la ejecución de un pro-grama marcará la velocidad de ejecución del mismo. Serie Ts ? Paralelo Ts / P Parte en paralelo (f), pero parte en serie (1-f) fa (speed-up) = Ts / Tp = Ts / (f*Ts/P + (1-f)Ts) = 1 / (1-f) (máximo) = independiente de P! ? ¿Es todo el código paralelizable? En general, no.
– Ley de Amdahl (tamaño constante) Rendimiento
Rendimiento
– Ley de Gustafson (tiempo constante) En muchos casos, se utiliza el paralelismo no para ir más rápido, sino para ejecutar tareas de mayor tamaño. fa = (1-f) + f*p (Gp:) f*Ts (Gp:) (1-f)*Ts (Gp:) f*Ts*p (Gp:) (1-f)*Ts (Gp:) (1-f)*Ts (Gp:) mayor tamaño (Gp:) p procesadores (Gp:) f*Ts
Rendimiento
? A la fracción de código en paralelo, hay que añadirle una serie de overheads; por ejemplo, la comunicación debida al reparto o recolección de datos en unos casos y a la sincronización de procesos en otros. (Gp:) T_ej
(Gp:) T_com
(Gp:) n. de procesadores (Gp:) T_total
Tp = Ts / P + Tcom(P) Rendimiento
? Load balancing
Otra fuente típica de perdida de eficiencia en la ejecución en paralela es el reparto no equilibrado de tareas. Ejemplo: 6 tareas independientes, de peso equivalente, ? entre 3 procesadores Tp = Ts/3 ? entre 4 procesadores Tp = Ts/3 Rendimiento
? No podemos olvidarnos de la memoria cache. Para que su uso sea eficiente, se necesita reutilizar los datos que se cargan (un bloque o line). Por ejemplo, si A(1) y A(2) están en posiciones consecutivas de memoria, tal vez sea conveniente que se procesen en el mismo procesador para aumentar la tasa de aciertos de la cache. ? Por otra parte, todos los overheads que añada la ejecución paralela hay que valorarlos en función del tamaño de grano, es decir en relación al tiempo de ejecución. Rendimiento
Página anterior | Volver al principio del trabajo | Página siguiente |