1. Rendimiento de Algoritmos Paralelos de Multiplicación de
Matrices implementados con MPI
Cristhian Parra1, Fernando Mancía1
1
Facultad Politécnica de la Universidad Nacional de Asunción
Estudiantes del 9no Semestre de Ingeniería Informática
{cparra,fmancia}@cnc.una.py
Resumen: El problema de la multiplicación de matrices, ha sido ampliamente
estudiado en el ámbito académico, y sus soluciones han encontrado aplicaciones en
diversos campos del mundo científico. La facilidad para dividir este problema en
diferentes partes independientes, ha causado el surgimiento de numerosos algoritmos
paralelos que lo resuelven eficientemente. En este trabajo, se presentan los detalles de
implementación de 4 algoritmos bien conocidos, aplicados a matrices cuadradas
densas. También se presentan los resultados de algunas pruebas llevadas a cabo para
analizar el rendimiento de cada algoritmo.
Palabras Clave: matrices, algoritmos de multiplicación de matrices, particionamiento
de datos, algoritmos paralelos, MPI, Cannon, DNS, particionamiento por bloque
cíclico, 2D diagonal.
2. 1 Introducción
Los algoritmos de matrices densas, y entre ellos los de multiplicación de matrices, han
encontrado numerosas aplicaciones en la resolución de problemas tanto numéricos, como
aquellos que no son numéricos.
En ciencias de la computación, la multiplicación de matrices ha encontrado usos importantes
en campos como la Criptografía o la Computación y Diseño Gráfico [3]. Todas estas
aplicaciones han motivado el estudio y optimización de algoritmos que resuelvan este
problema. Además, las matrices y sus operaciones son una parte muy importante del Álgebra
Lineal, cuyas aplicaciones en el mundo científico y de la computación son innumerables.
La facilidad con que se puede particionar el problema de multiplicar dos matrices cuadradas
y densas, lo convierten en un candidato ideal para estudiar los conceptos de computación
paralela, y permiten que sea fácil formular algoritmos que aprovechen los recursos de un
entorno paralelo.
En este trabajo, se describen 4 algoritmos paralelos para multiplicar matrices cuadradas y
densas, incluyendo la explicación de los detalles de implementación. Se presentan los
resultados de diversas pruebas realizadas, y las conclusiones extraídas a partir del análisis de
estas pruebas.
El objetivo del trabajo es llevar a la práctica conceptos de computación paralela, analizar
algoritmos paralelos para multiplicación de matrices, realizar las implementaciones y
ejecutar pruebas que nos permitan extender el análisis preliminar.
2 Descripción general.
En primer lugar, es importante definir el problema que queremos resolver y sus parámetros
correspondientes.
Dada dos matrices densas de tamaño NxN, se necesita obtener el resultado de la
multiplicación de estas dos matrices, que es otra matriz NxN igualmente densa, en un tiempo
razonable. Una Matriz es Densa cuando existe un bajo porcentaje de ceros en la misma.
El parámetro fundamental del problema es el tamaño de la matriz, cuantificada por N. En un
entorno de ejecución paralelo, existen dos parámetros más que se tienen en cuenta para
nuestros análisis: la cantidad de procesos y la cantidad de procesadores físicos.
Para resolver el problema, se implementan los siguientes 4 algoritmos:
✔ Algoritmo 2D con particionamiento por bloques y distribución cíclica, basada en
el particionamiento propuesto en el capítulo 3 de [1], que se llamará en adelante
2D cíclico.
✔ Algoritmo DNS, explicado en [1] y extendido en [4] y [5].
✔ Algoritmo de Cannon, explicado en [1] y generalizado en [4].
✔ Algoritmo 2D-Diagonal, presentado en [5].
El trabajo está organizado de la siguiente manera:
1. Las secciones 1 y 2 proporcionan una visión global del trabajo realizado.
2/27
3. 2. La sección 3 proporciona una explicación detallada de cada algoritmo y su
implementación.
3. En la sección 4 se describen las pruebas realizadas.
4. La sección 5 alberga la descripción de las métricas obtenidas, los resultados en forma
de gráficos, acompañados de algunos comentarios que forman parte del análisis de
dichos resultados.
5. La sección 6 proporciona algunas reflexiones de conclusión a las que llegamos al
finalizar el trabajo.
6. La sección 7 propone algunas problemáticas y trabajos futuros.
7. La sección 8 presenta nuestras fuentes de información.
8. La sección 9, presenta en forma de anexo, las tablas resumidas de resultados.
2.1 Preliminares
2.1.1 Detalles de implementación comunes a todos los algoritmos:
La implementación fue hecha usando el lenguaje de programación C (Ansi C) y la
implementación del estándar MPI conocido como OpenMPI. Todo el desarrollo y las
pruebas realizadas, fueron llevadas a cabo en el Sistema Operativo Linux Fedora 9, con
distintas versiones menores del kernel 2.6.25.
Las matrices se generan en tiempo de ejecución, ya que esto era más práctico para realizar
las pruebas de rendimiento, aunque está disponible la posibilidad de leer las matrices de
archivos en formatos de MatrixMarket, como se hacen [6].
Las matrices son representadas en forma lineal, por lo que para saber el elemento Mat[i,j] se
debe acceder de la siguiente manera:
Mat[(i*cant_columas) + j]
La implementación MPI utilizada, OpenMPI, está disponible para su descarga gratuita en [7]
y es Open Source.
Para las pruebas realizadas, establecimos cantidades de procesos fijos: 4, 9 y 16 procesos.
Solo en el caso de DNS utilizamos cantidades de proceso de 8 y 27, ya que en este algoritmo
se requería una cantidad cúbica. Ejecutamos los algoritmos en un entorno de cuatro
computadoras conectadas mediante una red ethernet de 10/100 Mbps.
De las cuatro máquinas que utilizamos para pruebas, tres contaban con procesadores doble
núcleo. Detalles más precisos sobre el ambiente de prueba se presentan en la sección 4.
2.1.2 Disponibilidad del código fuente:
Todo el proyecto está disponible a través de Subversion en el repositorio de Google Code,
http://mmulfpuna.googlecode.com/svn/trunk/srcmpi. Personas que no son miembros del
proyecto pueden acceder al mismo y descargar los fuentes de la siguiente manera:
svn checkout http://mmulfpuna.googlecode.com/svn/trunk/srcmpi mmulfpuna-
read-only
3/27
4. 3 Algoritmos Implementados
3.1 Algoritmo 2D Cíclico
En realidad, no existe un algoritmo 2D con particionamiento por Bloque Cíclico. Solo existe
el concepto sobre el particionamiento por bloque y la distribución cíclica de los bloques, que
se presenta y explica en [1].
3.1.1 Particionamiento por Bloques cíclico:
Cuando se debe particionar un determinado problema, para distribuirlo entre distintos
procesos, dos objetivos igualmente importante, suelen entrar en conflicto:
✔ Balancear correctamente la carga.
✔ Minimizar el tiempo de ociosidad (Idling) de los procesos.
En términos de la multiplicación de matrices, el primer objetivo implica distribuir las tareas
de multiplicación en los procesos, de manera que todos los procesos computen matrices
igualmente densas. El segundo objetivo implica solapamiento de comunicaciones con
cómputo.
Además de estos objetivos, existe la realidad de que normalmente existen menos
procesadores físicos disponibles, que la cantidad de procesos en los que queremos dividir
nuestro problema.
Para dar solución a este último problema, y al mismo tiempo encontrar una optimización
equilibrada de los dos objetivos presentados, el particionamiento por Bloques cíclicos
establece lo siguiente:
✔ Dividir el problema en más tareas que procesos.
✔ Asignar las tareas a los procesos de manera cíclica, de manera tal a que se
solapen adecuadamente las comunicaciones con las tareas de cómputo.
✔ Agrupar los procesos en bloque, de manera tal a que cada proceso, se encargue
de zonas distintas de la matriz y no asuma el cómputo de largos bloques
consecutivos que pueden tener un patrón de carga mayor que otras partes de la
matriz.
Figura 1. Particionamiento por Bloque con distribución o mapeamiento Cíclico. La imagen (a) muestra
la versión 1D y la imagen (b) la versión 2D.
La Figura 1 muestra un ejemplo de este tipo de particionamiento y mapeamiento1.
1 Las figuras fueron extraídas directamente de [1] para extender y clarificar las explicaciones.
4/27
5. Nótese que si la matriz es densa, el agrupamiento de bloques de las matrices hace que al
final, a cada proceso le haya tocado una bloque de tarea de cada zona distinta de la matriz,
haciendo que el balance general de la carga sea equilibrado.
3.1.2 Algoritmo implementado:
El algoritmo implementado, utiliza el particionamiento descrito en la sección anterior, y lo
aplica a la matriz de salida. De esta manera, cada proceso calcula un bloque de la salida.
Se sigue exactamente la idea del particionamiento, y se utiliza un esquema maestro-esclavo
para paralelizar el trabajo.
Lo primero que realiza el algoritmo es dividir la matriz de salida, de tamaño N, en N tareas
diferentes, lo que implica hacer una división en N × N bloques. La elección de la
cantidad de tareas, corresponde a que es el número óptimo que permite tener tamaños de
bloque balanceados.
Luego, se recorren los índices de la matriz resultado en bloques P × P tareas, y se
asignan estas a los procesos P i de manera cíclica, según se muestra en la siguiente
ecuación.
∀ i=0 k ⇒ P i=k mod P
Elegido el proceso al cual asignar una tarea, se empaquetan los datos que requerirá este (filas
y columnas de las matrices de entrada), y se le envía este paquete en un solo Mensaje a
través de MPI_Send.
En cuánto al esquema maestro-esclavo, existen dos posibilidades:
✔ La existencia de un proceso dedicado exclusivamente a la asignación y
distribución de tareas.
✔ El proceso 0 es el maestro, y además procesa localmente algunos bloques.
La segunda opción es atractiva por su simpleza, y sería recomendable en redes de mucha
latencia, sin embargo, cuando estamos en redes de alto rendimiento, puede ocurrir que el
proceso maestro se convierta en un cuello de botella.
La implementación probada en este trabajo corresponde a la segunda opción, por su
simplicidad. La estructura principal del algoritmo es la siguiente:
int SP = sqrt(P); int ST = sqrt(N); int k = 0;
for (i=0; i < N; i += SP) {
for (j=0; j < N; j += SP) {
for (x=i; x < (i+SP); x += ST) {
for (y=j; y < (j+SP); y += ST) {
...
int Pdest = P%k;
...
if (Pdestino != 0) {
Bloque <-- Empaquetar Bloque (x,y)
/* Implica empaquetar desde x, ST
5/27
6. * filas de A, y desde y, ST
* columnas de B
*/
MPI_Send( Bloque, ST * ST *N,
MPI_FLOAT, Pdest, Tag,
MPI_COMM_WORLD);
} else {
Bloque Local <-- Empaquetar Bloque
(x,y) Local
}
...
k++;
}
}
...
Multiplicar Bloque Local.
Cargar resultado en C.
...
for(proc = 0; proc < P, proc++) {
if (proc != 0) {
MPI_Status status;
MPI_Recv( BloqueRec, ST * ST * N,
MPI_FLOAT, 0, Tag,
MPI_COMM_WORLD, &status);
Cargar BloqueRec a matriz C
}
}
}
}
...
El código ejecutado por los esclavos es sencillamente el siguiente:
while (done == 0) {
...
MPI_Recv( Bloques, ST * ST * N, MPI_FLOAT,0, Tag,
MPI_COMM_WORLD, &status);
...
Multiplicar los bloques recibidos.
...
MPI_Send( Salida, ST * ST, MPI_FLOAT, 0, Tag,
MPI_COMM_WORLD);
}
...
El algoritmo presentará una sobrecarga y un cuello de botella importante en redes de alto
rendimiento, en las cuales el tiempo de envío de mensajes será mucho menor que el tiempo
que dura la computación, y muy pronto los procesadores esclavos se quedarán ociosos.
3.1.3 Costo Asintótico.
Para analizar el costo asintótico, debemos basarnos en la longitud del ciclo principal del
algoritmo. Los principales costos de comunicación se encuentran en el Send más interno. Por
cada tarea, se realiza un envío desde el maestro a un esclavo, a lo que este responde con otro
Send de la respuesta.
6/27
7. Además, aunque no figura en nuestro algoritmo simplificado mostrado en la página anterior,
luego de cada envío, a los procesos se les envía una pequeño mensaje de 1 palabra para
confirmar la continuidad del ciclo. Podríamos considerar, para simplificar el análisis, que se
envía un solo mensaje, pero con una palabra más de longitud.
En síntesis, teniendo en cuenta que se realizan N tareas, se realizarían N Sends, menos la
cantidad de veces en la que el proceso asignado es el 0. Para simplificar el análisis, diremos
que en realidad el Proceso 0, se envía su tarea a sí mismo, de manera a tener tantos envíos
como tareas. En cada uno de estos envíos, se envían palabras de tipo float, igual a la cantidad
de elementos de los bloques de matriz enviados. Por cada tarea, se envía un bloque de
N × N (filas de A) más N × N (columnas de B).
A esto hay que sumarle la cantidad de envíos realizados desde los procesos para enviar sus
resultados. En estos envíos la cantidad de palabras es igual a N × N =N , ya que
corresponde al bloque resultado de la multiplicación de los bloques citados más arriba. A
cada proceso, se le asignan N/P tareas, por lo cual, esta es la cantidad de envíos de respuesta.
Además, por cada envío realizado desde el proceso maestro, se realiza 1 envío más para
confirmar la continuación o no del algoritmo. En síntesis, se realiza la siguiente cantidad de
envíos. De esta manera, tenemos la siguiente función sobre el tiempo de comunicaciones.
1
t s× N ×2 t w ×N 2× N × N N 1
P
En cuánto al cómputo, cada proceso realiza un cómputo similar de orden N 3 , sin
P
embargo, hay una importante sobrecarga para el proceso maestro que además de realizar el
cómputo de computación, realiza un cómputo lineal por cada envío (cargar el buffer de
envío) y otro cómputo extra de cargar las matrices en la estructura final a medida que llegan
las respuestas. Aún así, esta sobrecarga no afecta el orden asintótico del cómputo realizado
en cada proceso.
3.2 Algoritmo 2D Diagonal
El algoritmo 2D diagonal es un algoritmo que sirve como base a otro, de tipo 3D, que
optimiza las comunicaciones realizadas.
Se organizan los P procesos en una malla 2D de P× P procesos. La matriz A es
particionada en P grupos de columnas y lo propio se hace con la matriz B, pero en
grupos de filas.
En el primer paso del algoritmo, se realizan P envíos para armar la distribución inicial
del algoritmo. Luego de esta distribución los procesadores pjj en la diagonal principal tienen
cada uno el jth grupo de filas de B y el jth grupo de columnas de A.
A partir de aquí, se crean dos sub-topologías: una que conecta a los procesos con sus pares
de la misma fila, y otra que conecta a los procesos con sus pares de la misma columna.
Entonces se inicia la lógica principal del algoritmo, que consiste en hacer 3 pasos muy
sencillos:
7/27
8. 1. Broadcasting (MPI_Bcast) desde Pjj a los procesos de la misma fila Pj* para enviar a
todos el jth grupo de columnas de A, como se muestra en la Figura 22.
2. One to all personalized Broadcast (MPI_Scatter) de los subbloques de tamaño
N N del jth grupo de filas de B, desde Pjj . De esta manera, cada proceso
×
P P
Pj* se encarga de computar el producto externo de las columnas de A y las filas de B
inicialmente almacenadas en pjj.
3. All to One Reduce (MPI_Reduce) desde cada proceso al proceso diagonal
correspondiente a su columna, a través de la operación de suma.
El término personalizado del paso 2, se refiere a que solo la porción del grupo de filas de B
(Bik) que es necesario para computar una columna i del producto externo de la jth columna de
A por la jth fila de B, se le pasa al procesador Pkj
La ultima etapa reduce el resultado por adición a lo largo de la dirección y. La matriz final C
se obtiene a lo largo de los procesadores diagonales. Cada procesador, envía su producto
externo a la diagonal principal que se encarga de procesar C*,i
Figura 2. Esquema general del algoritmo 2D Diagonal.
De esta manera, a lo largo de la diagonal principal se encuentra el resultado final de la
multiplicación. A continuación se muestra el bloque de código principal del algoritmo. Es
notable la simplicidad del mismo.
...
remains[0] = 0;
remains[1] = 1;
2 Las figuras fueron extraídas directamente de [5] para extender y clarificar las explicaciones.
8/27
9. MPI_Cart_sub(Comm2d,remains,&commF);
remains[0] = 1;
remains[1] = 0;
MPI_Cart_sub(Comm2d,remains,&commC);
...
int rrank = mycoords[0];
MPI_Bcast(IthGrupoA,GrupSize*N,MPI_FLOAT,rrank,commF);
if (mycoords[0] == mycoords[1] && mycoords[0] ) {
cargar_blocks(BloquesB,IthGrupoB,N,GrupSize,GrupSize);
}
MPI_Scatter( BloquesB, GrupSize*GrupSize, MPI_FLOAT,
BloqueB,GrupSize*GrupSize, MPI_FLOAT,
mycoords[0], commF);
calcular_producto_externo();
MPI_Reduce( IProduct,IProductReduced,GrupSize*N,
MPI_FLOAT,MPI_SUM,mycoords[1],commC);
...
3.2.1 Costo Asintótico.
El gasto de comunicación realizado en este algoritmo, se resume en la suma de los gastos en
las tres operaciones fundamentales que realiza el algoritmo:
1. P operaciones de Broadcasting, una en cada proceso diagonal, enviando
N palabras correspondientes a las filas de A.
× N
P
2. Una operación de Scatter en la que se distribuyen N N palabras a a cada
×
P P
proceso de una fila de la malla.
3. Una operación de Reduce a los largo de las columnas que reduce bloques de
N palabras.
× N
P
Teniendo en cuenta estos parámetros, y en base a los costos de las primitivas de
comunicación presentadas y analizadas con profundidad en el capítulo 4 de [1], nuestro
tiempo de ejecución estaría definido por los siguientes componentes:
Broadcasting entre P procesos:
N
t st w × ×N ×log P
P
Scatter entre P procesos:
N N
t s×log P t w × × × P−1
P P
Reduce entre P procesos (mismo costo que el Broadcast):
9/27
10. N
t st w × ×N ×log P
P
La suma final de estos tres componentes es la siguientes:
N2
3×t s×log P t w × × 2×log P P−1
P
3.3 Algoritmo de Cannon
El algoritmo es una versión de uso eficiente de memoria, comparada con el algoritmo simple.
Utiliza una malla de procesadores virtuales y la partición de los datos son de entrada
inicialmente para las matrices A y B, aunque a nivel de mapeamiento de tareas, tenemos un
mapeamiento de salidas, ya que cada proceso computa un bloque de la matriz de salida.
Para comenzar se crea la topología cartesiana de 2 dimensiones, y en cada dimensión se
tienen P procesos. Entonces quedan identificados los procesos desde P 0,0 hasta
P P −1 , P −1 .
N
Las matrices A y B se particionan en bloques de tamaño Tam= quedando en cada
P
procesador en cada momento un bloque de tamaño Tam de A y otro de B. Luego el Proceso
P0,0 es el encargado de pasar al resto de los procesos el bloque que le corresponde.
El algoritmo propone una alineación inicial de los bloques de A y B en cada proceso. En la
implementación presentada el proceso P0,0 es el encargado de enviar al Proceso P(i,j) el bloque
de A y B ya alineado quedando en el proceso P(i, j) el bloque A(i, j+i) y el bloque B(i+j , j).
En el caso de que i j P , hacemos i j− P para simular una estructura
toroidal (circular). Una vez que cada procesador ya tiene los bloques A y B que le
corresponden, los multiplica y almacena su resultado en un bloque para C con las
coordenadas del mismo proceso. Luego se hacen P −1 corrimientos circulares en
cada procesador, de las filas de A por la izquierda y las columnas B por arriba. Las figuras
siguientes, ilustran el proceso de manera gráfica. Luego se muestra el código principal del
algoritmo implementado3.
3 Las imágenes presentadas en esta sección fueron extraídas de [1] para aclarar mejor el algoritmo
implementado.
10/27
11. Figure 3. Alineación inicial de A y B.
Figure 4. A y B luego de la alineación inicial y luego del primer corrimiento.
Figure 5. Alineación de las submatrices luego del tercer y cuarto corrimiento.
for(pasos=1; pasos< raiz_p; pasos++){
// Desplazamiento Para Matriz A
MPI_Cart_shift(malla, 1, -1, &origen,
&destino);
MPI_Sendrecv_replace(buffA, tam_bloq *
tam_bloq, MPI_FLOAT,
destino, 0 , origen,
0, malla, &estado_msg );
// Desplazamiento Para Matriz B
MPI_Cart_shift(malla, 0, -1, &origen,
&destino);
MPI_Sendrecv_replace(buffB, tam_bloq *
tam_bloq, MPI_FLOAT,
destino, 0 , origen,
0, malla, &estado_msg );
//Pasamos del buffer al bloque local
buff2Bloque(bloqueA, tamanho_bloque,
buffA);
buff2Bloque(bloqueB, tamanho_bloque,
buffB);
//Multiplicación
acumular_multiplicar(bloqueA, bloqueB,
bloqueC);
}
11/27
12. En cada paso, cada procesador multiplica su bloque de A por su bloque de B, y le suma al
bloque de C que tiene. Finaliza el algoritmo y cada procesador P(i, j) tiene el bloque C(i, j) de la
matriz resultado.
3.3.1 Diferencias, Mejoras
El alineamiento inicial que se propone en el artículo [4], el algoritmo asume que en cada
procesador P(i,j) se encuentra el bloque A(i,j) y B(i,j) y luego realiza i corrimientos a la izquierda
para A y j corrimientos arriba para B. En nuestra implementación, sin embargo, el proceso
P(0,0) calcula que bloque de A y de B es el que corresponde a los demás procesos después de
la alineación inicial.
Pensamos que esto podría mejorar el tiempo de ejecución en cada proceso evitando los
corrimientos iniciales, pero esta mejora solo se dará en el caso de que efectivamente el
proceso P(0,0) sea el encargado de distribuir los bloques, caso contrario no sería una mejora.
3.3.2 Limitaciones
Las limitaciones y verificaciones de la implementación presentada son las siguientes:
• La cantidad de procesos debe ser un cuadrado perfecto. Para la distribución equitativa
de los bloques a los procesos en cada dimensión.
• La cantidad de elementos (N) debe ser divisible entre la raíz cuadrada de la cantidad de
procesos (P). Para que todos los bloques tengan el mismo tamaño, haciendo mas
sencilla la implementación.
3.3.3 Análisis Asintótico
Considerando solamente los corrimientos de a uno y la multiplicación de cada bloque en los
procesos, tenemos que en cada corrimiento se pasan tam_bloque * tam_bloque elementos,
N N N2
que es × = , que seria para cada proceso, tanto para A y B, por lo cual
P P P
multiplicamos este término por 2.
Si consideramos la comunicación y el tiempo de inicialización, tenemos que el gasto de
comunicación es el siguientes:
N2
2×t st w ×
P
Por su parte, el procesamiento en sí de la multiplicación, asumiendo que esta y la suma
toman una unidad de tiempo, entonces tendríamos un costo de tam_bloq * tam_bloq
*tam_bloq es decir:
N N N N3
× × =
P P P P × P
Entonces el tiempo total de ejecución paralelo sería:
N3 N
T p= 2×t s t w ×
P P
12/27
13. 3.4 Algoritmo DNS (Dekel, Nassimi, Sahni)
El DNS es un algoritmo para la multiplicación de matrices densas, que lleva las siglas de sus
creadores (Dekel, Nassimi, Sahni). Está basado en particionamiento de datos intermedios y
está diseñado para usar una topología de procesadores en tres dimensiones. El algoritmo
asume que en cada procesador se realiza una simple multiplicación escalar o de matrices en
bloques más pequeños. Para la descripción se tienen P procesadores y el tamaño de bloques
entonces es q= P . Generalizamos para que el algoritmo resuelva por bloques.
3
En la implementación realizada, se crea la topología en 3 dimensiones, luego el proceso
P(0,0,0) realiza la distribución de los bloques de A y B en los procesos del plano 0. Después de
este paso, todos los procesos P(i,j0) tienen el bloque A(i,j) y el bloque B(i,j). Este paso de
comunicación inicial en el plano cero no se especifica como parte del algoritmo en su
definición, sin embargo, para la implementación de este trabajo se ha decidido hacerlo.
Luego cada proceso del plano 0 (Pi,j,0), envía las columnas j de A(*,j) y las filas i de B(i,*) a los
pla0nos k=i para el bloque de A y k=j para el bloque de B. En los demás planos cada
proceso recibe su parte de A y B.
A continuación, cada proceso que está en el plano k=j y tiene la columna j debe replicar su
bloque de A a los demás procesos que comparten su plano, para lo cual se utiliza la primitiva
MPI_Bcast. Lo mismo para los procesos de k=i y que tienen la fila i, caso en el que se
replica el bloque de B a los de su mismo plano y fila. A continuación se muestra la parte del
código que realiza dicha acción.
int remainA[3]={0,1,0};
MPI_Comm comm_1d_A;
MPI_Cart_sub(comm_3d, remainA, &comm_1d_A );
MPI_Bcast(buffA, tamanho_bloque *
tamanho_bloque, MPI_FLOAT, micoord[2],
comm_1d_A);
int remainB[3]={1,0,0};
MPI_Comm comm_1d_B;
MPI_Cart_sub(comm_3d, remainB, &comm_1d_B );
MPI_Bcast(buffB, tamanho_bloque *
tamanho_bloque, MPI_FLOAT, micoord[2],
comm_1d_B);
Luego cada uno de los P procesadores ya tiene el bloque de A y de B que le corresponde, es
decir, el procesador P(i,j,k) tiene el bloque A(i,k) y el bloque B(k,j).
Después de eso cada proceso realiza una simple multiplicación entre el bloque de A y B que
tiene, y utilizando la función MPI_Reduce se realiza un all_to_one_reduce entre los
elementos P(i,j,*) a través de la operación de suma. A continuación se resume el código para
el paso final.
acumular_multiplicar(bloqueA, bloqueB,
bloqueC);
bloque2Buff(bloqueC, tamanho_bloque, buffC);
//Creamos un comunicador vertical
MPI_Comm comm_vertical;
int comm_vertical_id;
int remain[3]={0,0,1};
MPI_Cart_sub(comm_3d, remain,
13/27
14. &comm_vertical);
MPI_Comm_rank(comm_vertical,
&comm_vertical_id);
MPI_Reduce( buffC , bufferFinal,
tamanho_bloque * tamanho_bloque,
MPI_FLOAT, MPI_SUM, 0,
comm_vertical);
buff2Bloque(bloqueFinal, tamanho_bloque,
bufferFinal);
Las siguientes dos imágenes, ilustran el algoritmo visualmente.
Figure 6. La distribución inicial de A y B, y luego de realizar el envío de Aij desde Pij0 a Pijj
14/27
15. Figure 7. Distribución luego del los Broadcastings de A y B
3.4.1 Diferencias con la propuesta original.
En la implementación presentada el proceso (0,0,0) es el que lee toda la matriz A y B y
empieza a distribuir a los procesos del plano cero. Esto hace que este proceso se pueda
constituir en un cuello de botella al inicio del algoritmo, sobre todo para el caso de matrices
grandes, en el que se puede producir la saturación del socket de envío de este proceso.
Por otro lado, los elementos se tratan por bloques, en lugar de elementos como se muestra en
[1].
Además, para el paso final del Reduce, todos los procesos P(i,j,k) suman su bloque de C entre
los P(i,j,*) y estos se almacenan en los procesos del plano cero. Entonces al finalizar el
algoritmo, la matriz C está distribuida entre los procesos P(0,0,0) a P(q,q,0), siendo q= P
3
3.4.2 Limitaciones:
Las limitaciones y verificaciones de la implementación presentada son las siguientes:
• La cantidad de procesos debe ser un cubo perfecto. Para la distribución equitativa de
los bloques a los procesos en cada una de las 3 dimensiones.
• La cantidad de elementos (N) debe ser divisible entre la raíz cúbica de la cantidad de
procesos (P). Para que todos los bloques tengan el mismo tamaño, haciendo mas
sencilla la implementación.
3.4.3 Tiempo de ejecución
Toma un paso para multiplicar y q pasos para sumar, con costo Θ (log q), siempre teniendo
q= P .
3
En cuanto a la comunicación, el primer paso de comunicación uno a uno se ejecuta en A y en
n 2
B y toma un tiempo para cada matriz de tstw× tw×log q . El segundo
q
15/27
16. paso es un broadcast de uno a todos que se ejecuta en A y en B y toma un tiempo para cada
n 2
matriz de ts×logqtw× ×log q
q
La acumulación de nodo simple final se ejecuta una sola vez (para la matriz C) y toma un
2
n
tiempo tslogqtw× ×log q
q
4 Plan de Pruebas.
A continuación se describen los detalles de las pruebas, desde las configuraciones y entorno
de ejecución paralelo hasta los cálculos de los datos de prueba y como éstos fueron
ejecutados.
4.1 Datos de prueba
Los parámetros de ejecución de los algoritmos paralelos son los siguientes:
• N = Tamaño de la matrices de entrada y resultado
• P = Cantidad de procesos.
• C = Cantidad de máquinas.
Atendiendo las limitaciones de N y P descritas en cada algoritmo en la sección 3, a
continuación se presenta una tabla que resume el plan de ejecución de las pruebas. La
cantidad de máquinas (C ) fue constante e igual 4 (7 procesadores= 3 dual core + 1 single
core), luego se detallan totalmente las especificaciones de las mismas.
Para que la comparación pueda ser hecha, agrupamos los valores en 3 tamaños de N y 3 de P,
en cada caso, cada algoritmo puede tener una pequeña . La idea de las pruebas era lo
siguiente: como se tenían 4 máquinas y 7 procesadores, entonces probar con P menor a 4
Resumen agrupado de las pruebas.
N= 400 ~500 N= 900 N = 1200~1800
P=4 Cannon Cannon Cannon
2D 2D 2D
2DD 2DD 2DD(N=1800)
P= 8 ~9 Cannon Cannon Cannon
DNS (P=8) DNS (P=8) DNS (P=8)
2D (N= 441) 2D 2D (N =1764)
2DD 2DD 2DD
P=16~27 Cannon Cannon Cannon
DNS (P=27) DNS (P=27) DNS (P=27)
2D 2D 2D
2DD 2DD 2DD
16/27
17. 4.2 Entorno
Para la ejecución se usaron 4 máquinas con sistema operativo Linux Fedora Core 9, kernel
2.6.25.x. Se instaló y configuró la librería OPEN-MPI 1.2.2 y se uso el compilador mpicc,
para las ejecuciones se usó mpirun. También se configuró la red y los servidores SSH de
cada host para que se pueda ejecutar.
Además se configuró la autentificación DSA, para eso en el host maestro donde se lanza la
ejecución se generó la clave pública con ssh-keygen y se le pasó a cada una de las demás
máquinas para que pueda interactuar vía ssh con esa autentificación. El usuario común usado
en cada host uno fue mpiuser.
Máquina Procesador Memoria RAM S.O
cparra-portatil Intel Core Duo1,6 1,5 GB Linux Fedora Core 9.
Ghz. Cache L2 4MB Kernel 2.6.25.xx
liz Intel Core Duo. 1,46 1,0 GB Linux Fedora Core 9.
Ghz. Cache L2 2MB. Kernel 2.6.25.xx
fmanciahome Intel Core Duo. 3,00 1,0 GB Linux Fedora Core 9.
Ghz. Cache L2 4MB Kernel 2.6.25.xx
familia Intel Pentium IV 1,8 512 MB Linux Fedora Core 9.
Ghz (single core) Kernel 2.6.25.xx
5 Resultados Obtenidos
Se presentan en esta sección, los gráficos más importantes que resumen los principales
resultados obtenidos en las pruebas.
5.1 Métricas
Por cada prueba, se extrajeron los valores de las siguientes métricas:
✔ Tiempo de Ejecución en paralelo, obtenido como el tiempo de ejecución de un
proceso paralelo en particular. Los procesos de nuestras implementaciones
medían sus tiempos y estas medidas eran enviadas al proceso cero que se
encargaba de guardarlas para su posterior análisis. De los tiempos de ejecución
de los P procesos, se marca al mayor como el tiempo de ejecución paralelo de
todo el algoritmo.
✔ Aceleración, que es el ratio entre el mejor tiempo secuencial y el tiempo de
ejecución paralelo. Para este propósito, ejecutamos un algoritmo secuencial en
una sola máquina para cada tamaño de N utilizado en las pruebas.
✔ Sobre Costo, definido como la diferencia entre el producto del tiempo paralelo
por P y el tiempo secuencial.
✔ Eficiencia: definido como la relación entre la aceleración y la cantidad de
procesos lanzados.
De estas métricas, presentamos los resultados obtenidos para las dos primeras y la eficiencia.
17/27
18. 5.2 Tiempos de ejecución:
P=4;N=600 P=4;N=900
6.0000 18.0000
16.0000
5.0000
14.0000
4.0000 12.0000
10.0000
3.0000
8.0000
2.0000 6.0000
1.0000 4.0000
2.0000
0.0000 0.0000
2D CANNON 2DDIAGONAL 2D CANNON 2DDIAGONAL
P=4;N=1200
45.0000
40.0000
35.0000
30.0000
25.0000
20.0000
15.0000
10.0000
5.0000
0.0000
2D CANNON 2DDIAGONAL
Figure 8. Tiempos de ejecución de los algoritmos para la ejecución con 4 procesos
La Figure 8 muestra los tiempos de ejecución paralela de cada algoritmo cuando se ejecutan
en cuatro procesos y distintos tamaños de N. En general, se nota el bajo rendimiento del
algoritmo 2D cíclico debido a que el proceso cero distribuye y realiza mucho cómputo. Esto
no variará en ninguna de los resultados que se presentarán en esta sección.
Además, se puede notar que el aumento en los tamaños de la entrada no afectan el
rendimiento relativo de los algoritmos. Esta tendencia se mantuvo en casi todas las pruebas
salvo algunas excepciones.
Cannon se muestra más rápido que el algoritmo 2D diagonal, aunque el algoritmo 2D
diagonal presenta tiempos de ejecución mínimos en los procesos no diagonales, lo cual es un
dato muy interesante. Esto no se muestra en los gráficos, pero forma parte de los resultados
obtenidos.
El cuello de botella precisamente del 2D diagonal está en los procesos diagonales, que
realizan un trabajo de cómputo adicional de empaquetamiento de mensajes y recepción de
resultados. Dicho de otro modo, el 2D diagonal presenta una mayor eficiencia de
comunicación, pero tiene problemas en el desbalanceo del trabajo existente, poniendo mayor
carga de trabajo en los procesos diagonales.
Con 4 procesos no se realizaron pruebas de DNS, que requiere un número cúbico de
procesos.
18/27
19. P=9;N=600 P=9;N=1200
5.0000
4 0.00 00
4.5000
3 5.00 00
4.0000
3.5000 3 0.00 00
3.0000 2 5.00 00
2.5000
2 0.00 00
2.0000
1 5.00 00
1.5000
1 0.00 00
1.0000
0.5000 5 .0 00 0
0.0000 0 .0 00 0
2D DNS CANNON 2DDIAGONAL 2D DNS C AN N ON 2 D D IAGON AL
P=9;N=900
16.0000
14.0000
12.0000
10.0000
8.0000
6.0000
4.0000
2.0000
0.0000
2D DNS CANNON 2DDIAGONAL
Figure 9. Tiempos de ejecución de los algoritmos para la ejecución con 9 procesos
Con nueve procesos, ya se incluye en el análisis al algoritmo DNS, que muestra tiempos
de ejecución muy similares al de Cannon. Sin embargo, las tendencias que se manifestaron
en los primeros resultados con 4 procesos, se mantienen y sigue siendo Cannon el algoritmo
con menor tiempo de ejecución, seguido del DNS y del 2D Diagonal. El 2D cíclico, como
siempre, se mantiene en la última posición por los problemas que ya explicábamos más
arriba.
P=16;N=600 P=16;N=900
5.0000 25.0000
4.5000
4.0000 20.0000
3.5000
3.0000 15.0000
2.5000
10.0000
2.0000
1.5000
5.0000
1.0000
0.5000
0.0000
0.0000
2D DNS CANNON 2DDIAGONAL
2D DNS CANNON 2DDIAGONAL
19/27
20. P=16;N=1200
45.0000
40.0000
35.0000
30.0000
25.0000
20.0000
15.0000
10.0000
5.0000
0.0000
2D DNS CANNON 2DDIAG-
ONAL
Figure 10. Tiempos de ejecución de los algoritmos para la ejecución con 16 procesos
Para la ejecución de los algoritmos en 16 procesos, como se muestra en la Figure 10 se
presentan excepciones.
Para matrices pequeñas, la tendencia se mantiene, pero a medida que aumentamos el número
de matrices llega el caso en el que DNS llega a un tiempo de ejecución inferior al de Cannon.
En realidad, mirando los tres conjuntos de gráficos, podemos ver que DNS presenta mejoras
en su tiempo de ejecución a medida que aumenta el tamaño de las matrices, fenómeno que no
es tan notable en los demás algoritmos.
5.3 Aceleración y Eficiencia.
En esta sección, se presentan los resultados de la aceleración y la eficiencia de los algoritmos
implementados.
La aceleración y la eficiencia de los algoritmos siguen en el mismo patrón relativo que los
gráficos de tiempo de ejecución, lo cual era de esperarse.
Lo notable en estos trabajos es que en ciertos casos, el 2D Cíclico tiene peor rendimiento que
el algoritmo secuencial óptimo, lo que permite reafirmar la necesidad de separar a un proceso
dedicado para que se comporte como maestro.
P=4;N=400
P=4;N=600
3.5000
3.5000
3.0000
3.0000
2.5000
2.0000 2.5000
1.5000 2.0000
1.0000 1.5000
0.5000 1.0000
0.0000
0.5000
2D CANNON 2DDIAG-
ONAL 0.0000
2D CANNON 2DDIAGONAL
20/27
21. P=4;N=1200
3.5000
3.0000
2.5000
2.0000
1.5000
1.0000
0.5000
0.0000
2D CANNON 2DDIAGONAL
Figure 11. Aceleración y Eficiencia (en menor escala) de los algoritmos para la ejecución con 4
procesos
Además, otra cosa notable es que ninguno de los algoritmos tiene una eficiencia mayor al
70%, lo que habla de que las sobrecargas por comunicación y otros cómputos están
aproximadamente entre el 50% y el 70%.
P=9;N=600 P=9;N=900
1.6000 4.0000
1.4000 3.5000
1.2000 3.0000
1.0000 2.5000
0.8000 2.0000
0.6000 1.5000
0.4000 1.0000
0.2000 0.5000
0.0000 0.0000
2D DNS CAN 2DDI- 2D DNS CANNON 2DDIAG-
NON AGONAL ONAL
P=9; N=1200
4.5000
4.0000
3.5000
3.0000
2.5000
2.0000
1.5000
1.0000
0.5000
0.0000
2D DNS CANNON 2DDIAGONAL
Figure 12. Aceleración y Eficiencia (en menor escala) de los algoritmos para la ejecución con 8 y 9
procesos
21/27
22. Al aumentar el número de procesos a 9, podemos incluir a DNS en el análisis y se confirma
lo que se puede notar con los tiempos de ejecución: DNS aumenta notablemente a medida
que se procesan tamaños mayores de la entrada. Para N=600, DNS tiene un rendimiento
inferior al algoritmo secuencial, mientras que al llegar a tamaños superiores al 1000, llega a
alcanzar una eficiencia cercana al 70%.
Otra cosa notable de estos resultados es el rendimiento superescalar de Cannon cuando la N
llega a 1200. Por una lado, el entorno de ejecución paralelo de las pruebas no era totalmente
homogéneo, y las pruebas secuenciales se ejecutaron en uno de los nodos que no
necesariamente podría obtener los mejores resultados. Por otra parte, algunos de los nodos
del entorno estaban equipados con procesadores de dos núcleos, lo cual pudo haber
incrementado el nivel de paralelismo. Cualquiera de estos motivos puede explicar la
superescalabilidad.
O tal vez esta superescalabilidad se debe a alguna de las razones tradicionales explicadas en
[1]: los algoritmos paralelos efectivamente realizan menos trabajo en proporción que el
algoritmo secuencial, o la división de los datos hace que para los bloques más pequeños de
datos, se aproveche mejor las capacidades de la cache de cada nodo.
P=16;N=600 P=16;N=900
5.0000
1.4000
4.5000
1.2000
4.0000
1.0000 3.5000
3.0000
0.8000
2.5000
0.6000
2.0000
0.4000 1.5000
1.0000
0.2000
0.5000
0.0000 0.0000
2D DNS CANNON 2DDIAG- 2D DNS CANNO N 2DDIAGO NAL
ONAL
P=16;N=1200
4.0000
3.5000
3.0000
2.5000
2.0000
1.5000
1.0000
0.5000
0.0000
2D DNS CANNON 2DDIAGONAL
Figure 13. Aceleración y Eficiencia (en menor escala) de los algoritmos para la ejecución con 16 y 27
procesos
22/27
23. Por otro lado, debido a la presencia de nodos con procesadores doble núcleo, teóricamente la
aceleración debería dispararse entre los resultados con 4 procesos y aquellos obtenidos con 8
o 9. Sin embargo esto no sucede lo que nos lleva a la conclusión de que la implementación
MPI utilizada no aprovecha al máximo las posibilidades de multiprocesamiento en los nodos.
Los tres últimos gráficos de aceleración y eficiencia sencillamente confirman las tendencias
presentadas en los demás resultados. Para tamaños grandes de matrices, la aceleración de
DNS es superior a la de Cannon, probablemente por su menos sobrecarga de comunicación.
Por otro lado, aunque esto no se nota en los gráficos, de los resultados extraídos en las
pruebas, también pudimos notar al revisar los tiempos de ejecución de cada proceso
ejecutado por un algoritmo, que el DNS es el que presenta mayor estabilidad. Por su parte,
Cannon es el que presenta mayores variaciones entre los tiempos de ejecución de los
distintos procesos. 2D Diagonal por su parte, presenta la misma inestabilidad que Cannon y
bajos tiempos de ejecución en los procesos no diagonales, mientras que el tiempo de
ejecución se dispara en la diagonal de la topología.
6 Conclusiones
✔ Se alcanzaron los objetivos de profundizar los conceptos de programación
paralela a través de la implementación de algoritmos paralelos.
✔ Se pudo comprobar que la paralización de la solución efectivamente disminuye
el tiempo de solución del mismo.
✔ En el caso particular de la multiplicación de matrices, casi todos los algoritmos
aceleraron la solución, con excepción del algoritmo 2D cíclico.
✔ A partir de tamaños de matrices superiores a 1000, ninguno de los algoritmos
paralelos tiene aceleración inferior a 1, y a medida que aumenta el tamaño,
algunos algoritmos mejoran su rendimiento.
✔ Cannon ofrece, entre todos los algoritmos paralelos implementados, las mejores
prestaciones en cuánto a tiempo de ejecución se refiere.
✔ DNS sin embargo, aumenta su rendimiento a medida que aumenta el tamaño de
las matrices de entrada.
✔ En cuánto a la variabilidad de los algoritmos, entendiendo variabilidad como
variación entre los tiempos de ejecución de cada proceso, DNS ha probado ser el
algoritmo más estable.
✔ Cannon, sin embargo, es el más inestable en este sentido.
✔ 2D diagonal tiene una variabilidad similar a la de Cannon, con el agregado de
que los procesos diagonales presentan tiempos de ejecución muy superiores que
los que no son diagonales.
7 Trabajos Futuros
• Mejorar el modelo maestro-esclavo del algoritmo 2D cíclico: el problema
fundamental en la implementación del algoritmo 2D cíclico, es que el proceso maestro
se convierte en un cuello de botella cuando se procesan matrices grandes. Una solución
interesante que puede aumentar dramáticamente el rendimiento de este algoritmo es
dedicar uno de los procesos a la tarea exclusiva de ser maestro y distribuidor de tareas.
23/27
24. Esto podría permitir que efectivamente se optimice el solapamiento del cómputo con
las comunicaciones, y se reduciría al mínimo el Idling time gracias a que tan pronto un
proceso quede libre, seriá asignado a una nueva tareas.
Otra mejora en este modelo consiste en reducir la restricción de asignación cíclica, y
permitir que la asignación de tareas se realice directamente al procesador que quedó
libre primero. Esto se puede implementar fácilmente reemplazando la asignación
circular, por un mecanismo de cola de procesos a la que se irán agregando los procesos
por orden de llegada a medida que quedan libres.
Finalmente, una mejora más que se le podría realizar al algoritmo 2D consiste en
implementar un mecanismo de pre-empaquetamiento de datos a enviar. Es decir, una
vez que el proceso maestro termino de asignar una ráfaga de tareas, que comience
inmediatamente a empaquetar los próximos mensajes para que cuando llegue el
momento, estos estén listos y se minimice el tiempo de ociosidad del proceso maestro.
• Aprovechamiento de las capacidades multi-hilos de los procesadores con múltiples
núcleos: MPI no aprovecha las capacidades de los procesadores de manejar hilos. El
resultado es que cada proceso es lanzado en cada procesador como un proceso pesado.
Un trabajo interesante seriá realizar una implementación de estos algoritmos sobre una
implementación de MPI que aproveche la capacidad de multihilado de los
procesadores, como la que se propone en [8], y lance las tareas como hilos en cada
nodo y no como procesos pesados. En los resultados obtenidos por este trabajo se notó
que la utilización de procesadores doble-núcleo en la ejecución de los procesos por
medio de MPI, no generaba mejoras muy notables en la aceleración.
• Otro trabajo interesante, sería analizar el rendimiento de estos algoritmos para matrices
no densas, en busca de posibles optimizaciones particulares a los mismos aplicables a
distintos tipos de matrices especiales.
8 Referencias
[1] Ananth Grama, Anshul Gupta, George Karypis, Vipin Kumar: Introduction to Parallel
Computing, Second Edition. Addison Wesley. 2003.
[2] MatrixMarket - Mathematical and Computational Sciences Division NIST (National
Institute Standars and Technology) - http://math.nist.gov/MatrixMarket/
[3] http://en.wikipedia.org/wiki/Matrix_(mathematics)
[4] Generalized Cannon's algorithm for parallel matrix multiplication. Hyuk-Jae Lee, James P.
Robertson, José A. B. Fortes.
[5] Communication Efficient Matrix Multiplication on Hypercubes. Himanshu Gupta, P.
Sadayappan.
[6] Rendimiento de Algoritmos Paralelos de Multiplicación de Matrices implementados con
Hilos. Cristhian Parra, Fernando Mancía. Facultad Politécnica de la Universidad de
Asunción, 2008.
[7] www.open-mpi.org
[8] F. García, A. Calderón, J. Carretero, MiMPI: a multithread-safe implementation of MPI, in:
Recent Advances in Parallel Virtual Machine and Message-Passing Interface, Proceedings
of the Sixth European PVM/MPI Users' Group Meeting, Lecture Notes in Computer
Science 1697, Springer, September 1999, pp. 207-214.
24/27