Introducción a la Programación en ambientes con Memoria-Compartida y Memoria-Distribuída.

Por Cory Quammen
Traducido por Javier A. Comín

Introducción

En el diseño de una computadora con múltiples procesadores, surge una importante pregunta: Cómo coordinan dos procesadores para resolver un problema?. Los procesadores deben tener la habilidad para comunicarse entre si en forma ordenada para completar una tarea. Este artículo discute dos métodos de comunicación inter-procesador, cada una disponible para diferentes sistemas de arquitecturas.

Una arquitectura de computadora paralela usa un simple espacio de dirección. Los sistemas basados en este concepto, conocidos como multiprocesadores de memoria-compartida, permiten la comunicación entre procesadores a través de variables almacenadas y comparten el mismo espacio de memoria. Otra arquitectura para computadoras paralelas emplea un esquema en el cual, cada procesador tiene su propio módulo de memoria. Este esquema es conocido como multiprocesadores de memoria-distribuída, y es construído conectando cada componente con redes de comunicaciones de alta velocidad. Los procesadores se pueden comunicar a traves de redes de trabajo [3].

Las diferencias entre la arquitectura de memoria-compartida y memoria-distribuída tiene implicaciones en la forma en que cada una es programada. Con multiprocesadores de memoria-compartida, diferentes procesos pueden acceder a las mismas variables. Esto se logra referenciando los datos almacenados en una memoria similar a los tradicionales programas de simple-procesador, pero agrega una complejidad a la integridad de los datos almacenados. Un sistema de memoria-distribuída, introduce un problema diferente: Cómo distribuír una tarea computacional a múltiples procesadores con distintos espacios de memoria y reensamblar los resultados de cada procesador en una sola solución.

Este artículo introduce dos estándares existentes para la programación de multiprocesadores con memoria-compartida y memoria-distribuída. El primero, OpenMP, es un conjunto de directivas del compilador, funciones de librerías y un entorno de variables para el desarrollo de programas en multiprocesadores de memoria-compartida. La segunda, Message Passing Interface (MPI), es una interface para un conjunto de funciones de librerías que los procesadores en un esquema de memoria-distribuída pueden usar para comunicarse el uno con el otro. Presentaré lo básico de cada estándar con su correspondiente paradigma, lo que sucede cuando cada uno esta trabajando, y un simple ejemplo de un programa escrito para ilustrar los rasgos básicos de cada estandar. Finalmente, haré algunas conclusiones de las ventajas y desventajas de cada estandar. 

 

OpenMP

OpenMP es un estandar abierto para proveer mecanismos de paralelización en multiprocesadores de memoria-compartida. Las especificaciones existen para C/C++ y FORTRAN, uno de los lenguajes mas comunmente usados para escribir programas paralelos. Este estandar provee una especificación de directiva de compilador, rutinas de librerías y un entorno de variables que controlan el paralelismo y características en tiempo de ejecución de un programa. Desde que se convirtió en un estandar en el cual uno se da el lujo de incrementar niveles de implementaciones, el código escrito con OpenMP es portátil  con respecto a los demás multiprocesadores de memoria-compartida [8]. Las directivas de compilador definidas por OpenMP le dicen al compilador cuál(es) región(es) de código(s) debería(n) ser paralelizada(s) y define además opciones específicas para el paralelismo. En adición, algunas herramientas de precompilador que existen, pueden automáticamente convertir programas seriales en programas paralelos, insertando directivas de compilador en lugares apropiados, haciendo el paralelismo de un programa mucho mas fácil. Un ejemplo es el ahora discontinuado producto de Kuck & Associates (ahora pertenece a KAI Software), Visual KAP para OpenMP [12].

OpenMP está basado en un paradigma de thread. Un programa corriendo, es referido como un proceso, es alojado en su propio espacio de memoria por el sistema operativo cuando el programa es cargado en la memoria. Dentro de un proceso, múltiples threads pueden existir. Un thread es una secuencia activa de ejecución de instrucciones dentro de un proceso. Los threads dentro de un proceso comparten el mismo espacio de memoria y pueden acceder a las mismas variables. Tienen la ventaja de permitir a los procesos ejecutar múltiples tareas, aparentemente en simultáneo. Por ejemplo, un browser, (Ntd: programa para visualizar páginas webs), puede tener un thread que requiera y reciba páginas web, otro thread para presentarlas en la pantalla, y otro thread para "escuchar" los pedidos del usuario y responder apropiadamente. Sin threads, el browser podría bloquearse mientras espera que una página web sea descargada, prohibiendo al usuario de hacer cosas como acceder al menú de opciones [10].

El paradigma thread es una opción lógica para multiprocesadores de memoria-compartida. El concepto está basado en el modelo fork-join de la computación paralela. Un thread maestro corre serialmente hasta que se encuentra con una directiva y se produce una bifurcación con nuevos threads. Estos threads pueden ser distribuídos y ejecutados en diferentes procesadores, reduciendo el tiempo de ejecución haciendo que mas ciclos de procesos estén disponibles por unidad de tiempo. Los resultados de cada ejecución de threads pueden luego ser combinados. Un usuario puede establecer el números de threads creados para regiones paralelas estableciendo el entorno de la variable OMP_NUM_THREADS, o también puede establecerlo usando una llamada a la librería omp_set_num_threads. La Figura 1 muestra la ejecución de un modelo simple de programa en OpenMP [8].

OpenMP execution model

Figura 1. Flujo de un programa en un modelo de ejecucíon bajo OpenMP

Load Balancing en OpenMP

Una de las cuestiones que se levantan en cualquier sistema de multiprocesamiento es el "load balancing". Load balancing es el problema de distribuír una tarea a un conjunto de procesadores en donde cada procesador tiene aproximadamente la misma cantidad de trabajo para ejecutar. Como una analogía, considere un grupo de personas trantado de desagotr un bote con baldes. Hay dos tamaños de baldes, un es el doble de grande que el otro. Cada uno saca el mismo número de baldes de agua del bote, pero los baldes grandes toman el doble de tiempo en vaciarse, porque son mucho mas pesados. Las personas que cargan los baldes mas pequeños quedarán libres durante bastante tiempo. En el tiempo que le toma a las personas con baldes grandes vaciarlos, las personas que cargan baldes chicos, podrá vaciar dos baldes. Claramente sacar el agua podría ser mucho mas eficiente. 

En OpenMP, load balancing es a veces un problema cronológico. Por defecto, una vez que un thread finalizó una región de código, éste esperará por otros threads que completen la misma región, como las personas que sacan el agua del ejemplo anterior. OpenMP tiene varias opciones para establecer un orden cronológico entre threads, improvisando y mejorando la inifiencia que puede resultar en el algoritmo cronológico. Algunas opciones para establecer cronológicamente a threads en el contexto de un for, por ejemplo, incluyen:

Esto podría parecer que el Orden cronológico dinámico, es el mejor algoritmo de cronología entre threads, pero no siempre es el mismo caso. Una cierta cantidad de sobrecargas están envueltas en la asignación de iteraciones adicionales a un thread, bajando el rendimiento de la ejecución general de un programa OpenMP. Por ésta razón, encontrar un óptimo n podría ser muy difícil. 

Cuestiones en la Programación de Multiprocesadores de Memoria-Compartida

La programación en entornos threads, brindan bastantes cuestiones que estrictamente los programas seriales nunca necesitan ser direccionados. Uno de los problemas pueden originarse cuando usar threads significa que suceda una condición de concurso. Una condición de concurso, ocurre cuando mas de un threads puede modificar la(s) misma(s) variable(s) en el mismo momento [10]. Considere dos threads T1 y T2 en un programa de finanzas el cual comparta la tarea de combinar los intereses de un cliente en una base de datos todos los meses. Cada thread combina los intereses en medios préstamos en una base de datos. Ambos deben mirar el balance y la taza de interés del préstamo y luego multiplicar el balance por la taza de interés. El pseudo-código siguiente muestra la secuencia de operaciones que deben ser ejecutadas:

    balance = getBalance(loanID)
    rate = getRate(loanID)
    setBalance(loanID, balance * (1.0 + rate))

Cuando ambos threads ejecuten esta secuencia de código sucede un caos. El siguiente ejemplo muestra la secuencia de instrucciones que el procesador podría ejecutar: 

T1: balance = getBalance(loan1)
T1: rate = getRate(loan1)
T2: balance = getBalance(loan2)
T1: setBalance(loan1, balance * (1.0 + rate))

Note que el balance para loan1 será establecido al balance del loan2 multiplicado por la taza de interés de loan1. Las instrucciones de cada thread podría ser ejecutadas en un(os)  procesador(es) en forma arbitraria por ello generaría resultados no deseados. Claramente, el programa anterior podría producir clientes de algún banco no muy felices. 

El problema en el ejemplo anterior es que ambos procesos ejecutan el código en lo que es conocido como región crítica. Varios métodos de sincronización existen para prevenir que ambos threads ejecuten simultaneamente la región crítica. Típicamente, un thread trabará la región crítica, prohibiendo a otros threads ejecutar simultaneamente regiones críticas. Los threads que intenten ejecutar una región crítica cerrada o trabada, deberán esperar a que sea liberada por el thread que la trabó [10].

Impidiendo una condición de concurso puede conducir a otro problema que es conocido como deadlock. Un deadlock puede ocurrir entre dos threads cuando cada uno espera para cerrar. Ningún thread libera la traba antes que el otro, ambos threads esperarán para siempre, esencialmente muertos [10].

Sincronización de threads en OpenMP

OpenMP provee capacidades de sincronización para el programador evitando trampas potenciales de un paradigma de thread. Una puede especificar una región crítica usando #pragma critical. La región crítica puede sólo ser ejecutada por un sólo thread a la vez, evitando cualquier condición de concurso en esa región.

Una puede especificar que ciertas variables puedan ser localizadas a threads individuales usando la directiva #pragma local(list) o #pragma threadprivate(list) donde list es una lista de variables a ser privatizadas. Por defecto, cada thread puede leer y escribir todas las variables en la sección de código paralelizada. Declarando una variable local o privada esencialmente crea una copia de la variable para que cada thread lo use en forma privada.

Por supuesto, la noción de paralelismo está basada en la combinación de esfuerzos computacionales de múltiples procesadores. Otra directiva provista por OpenMP es #pragma reduction(op: list). Esencialmente, todas las variables en list están privadas para cada thread. Cuando los threads terminan la ejecución de códigos en la región crítica en la cual las variables son decrementadas, una copia compartida recibe el valor de cada copia local por el operador. De esta forma, todos los threads reciben la computación final de la región paralela [8].

Adicionalmente, la rutinas de librerías pueden ser usadas para especificar ejecución paralela en ciertas regiones críticas. Algunos parámetros pueden ser cambiados durante la ejecución de un programa, por ejemplo, el número de threads bifurcados en una sección crítica. Ellos pueden ser usados para la sincronización de datos desarrollados por el programador. El entorno de las variables de la especificación son usadas para establecer las características de la ejecución paralela no definida por el compilador o rutinas de librerías. Por ejemplo, uno puede establecer en número de threads para regiones paralelas de código cambiando el entorno de la variable OMP_NUM_THREADS.

Un ejemplo de programa en OpenMP

Mientras OpenMP ofrece tareas en paralelismo, esto no es usado para distribuír trabajo en los loops for. Tome la Lista de Código 1 por ejemplo. Este es un loop escrito en C que suma un arreglo de 100 elementos, y queremos dividir el trabajo entre cuatro threads en un esquema de multiprocesamiento de memoria-compartida. Un proceso serial ejecuta cada iteración a través del loop, haciendo todo el trabajo. En un esquema de memoria-compartida, sin embargo, podríamos tene un thread sumando los elementos 0 hasta 24, otro que sume los elementos 25 hasta 49, otro que sume los elementos 50 hasta 74 y otro que sume elementos desde 75 hasta 99.

Lista de Código 1

int main(int argc, int *argv[]) {
   int i, intArray[100];
   int sum = 0;

   /* Asumimos que initArray se inicializa en número del 1 al 100. */
   initArray(intArray, 100);

   /* Suma los elementos del arreglo. */
   for (i=0;i<100;i++) {
      sum = sum + intArray[i];
   }
}

Con OpenMP, todo lo que se necesita para que ocurra paralelismo en esta región es agregar tres directivas #pragma al código original, instruyendo al compilador a paralelizar el loop for. La modificación es mostrada en la Lista de Código 2. Luego, antes de que ejecutemos el programa, establecemos el entorno de la variable OMP_NUM_THREADS en 4, indicando que queremos este loop y otro loop paralelizado con OpenMP que corra con 4 threads.

Lista de Código 2

int main(int argc, int *argv[]) {
   int i, intArray[100];
   int sum = 0;

   /* Almacena algunos valores en el arreglo. */
   initArray(intArray, 100);

   #pragma parallel for      /* Hace en el loop for, una región paralela */
   #pragma threadprivate(i) 
   #pragma reduction(+: sum)
   for (i=0;i<100;i++) {
      sum = sum + intArray[i];
   }
}
El primer pragma le dice al compilador que paralelice el loop for mientras el segundo declara la variable i como privada para cada proceso. La localización de i es necesaria para cada procesador para mantener el camino con cada iteración del loop. Finalmente, el decremento de la variable sum combina el trabajo de cada thread en la variable sum usando el operador +

Message Passing Interface

MPI es un estandar para la comunicación inter-proceso en un esquema de multiprocesador de memoria-distribuída. El estandar ha sido desarrollado por un comitee de proveedores, laboratorios del gobierno y universidades [6]. La implementación del estandar es usualmente dejada para los diseñadores de sistemas en el cual MPI corre, pero una implementación de dominio público está disponible [2]. MPI es un conjuno de rutinas de librería para C/C++ y FORTRAN. Al igual que OpenMP, MPI es una interfaz estandar, por lo tanto el código escrito para un sistema puede fácilmente ser transportado hacia otro sistema con aquellas librerías.

La ejecución del modelo de un programa escrito con MPI es bastante diferente de uno escrito con OpenMP. Cuando un programa bajo MPI comienza, el programa se descompone en un número de procesos especificados por el usuario. Cada proceso corre y se comunica con otras instancias del programa, posiblemente corriendo en el mismo procesador o en diferentes. Lo mejor ocurre cuando los procesos están en medio de procesadores. La comunicación básica consiste en enviar y recibir datos de un proceso a otro y no como en OpenMP en donde la comunicación entre threads ocurre mediante variables compartidas. Esta comunicación toma lugar en una red de trabajo muy rápida que conecta a los procesadores en un sistema de memoria-distribuída.

Un paquete de datos enviados con MPI requiere bastantes piezas de información: el proceso de envío, el proceso de recepción, la dirección de comienzo en memoria a ser enviados, el número de datos que han sido enviados, un identificador de mensajes, y el grupo de todos los procesos que puede recibir el mensaje. Todos estos objetos están disponibles para ser enviados por el programador. Por ejemplo, uno puede definir un grupo de procesos, y luego enviar mensajes solamente a ese grupo.

Algunas rutinas colectivas de comunicación no requieren todos los objetos. Por ejemplo, una rutina que permite a un proceso comunicarse con otro en un grupo, cuando es llamada por aquellos procesos, podría no requerir la especificación de recepción sino hasta que todos los procesos en el grupo, dispongan de un receptor.

En uno de los programas mas simples en MPI, un proceso maestro despacha trabajo a los procesos trabajadores. Esos procesos reciben el dato, realizan tareas en él, y envían los resultados al proceso maestro, el que combinará los resultados. Un esquema de coordinación mas compleja es posible con MPI, pero introduce cuestiones que serán brevemente discutidas. Note que los otros procesos corren continuamente desde el comienzo del programa, una diferencia del modelo fork-join en OpenMP. La figura 2 muestra la ejecución de un modelo en un programa básico bajo MPI.

MPI execution model

Figura 2. Modelo de ejecución MPI.

Load Balancing en MPI

Al igual que programas de esquema multiprocesador de memoria-compartida, los programas bajo un esquema de memoria-distribuída debe resolver el problema de load balancing. El objetivo es mantener todos los procesos ocupados computando los resultados útiles, minimizando el costo de sobrecarga en comunicación. Le permitiré volver a la analogía de los baldes de agua. Esta vez, asuma que cada persona que tiene un balde, puede arrojar la misma cantidad de agua en el mismo lapso de tiempo (usando los mismos baldes, en cuanto a longitud se refiera) y cada persona con un balde se encuentra en diferentes compartimientos del barco, y los mismos están totalmente aislados entre ellos. Podríamos querer que el agua sea distribuída de igual manera entre los compartimientos, de manera que el trabajo sea mas rápido y eficiente.

Para algunos problemas, como multiplicar una gran matriz por un vector, determinar un buen esquema de load balancing es relativo: enviar r/p filas de la matriz y el vector entero a cada procesador donde r es el número de filas y p es el número de procesos, teniendo cada proceso que computar una sub-matriz y recolectar el resultado devuelto en una matriz final. Distribuyendo otros problemas, sin embargo no es tan fácil. Podría ser a veces bastante dificultoso o imposible para determinar la cantidad de cómputos por un subdominio de un problema. Por ejemplo, distribuír un tetrahedro en una simulación de fluídos dinámicos es verdaderamente un problema, uno de los cuales fué investigado por [11, 5].

Cuestiones en la Programación de Multiprocesadores de Memoria-Distribuída

Uno de los grandes retos en la programación de esquemas de multiprocesamiento de memoria-distribuída es implementar una eficiente comunicación inter-proceso. La comunicación no está limitada por la simple relación maestro-trabajador motrada en la Figura2. Podría ser muy bueno para el caso de que los procesos requieran datos o computar resultados desde cualquier proceso durante la ejecución. Puede, incluso, ser el caso de que cada proceso requiera el mismo dato de todos los demás procesos.  Asegurando la sincronización de los procesos, es esos casos agrega un nivel de complejidad a programas desarrollados en un esquema de memoria-distribuída. Haciendo la comunicación eficiente, es de este modo, minimizar la sobrecarga involucrada en el pasaje de mensajes, agrega mucha mas complejidad.

MPI provee varias rutinas de comunicación para socorrer al programador en un ambiente de comunicación inter-proceso. Estas rutinas incluyen:

Tabla 1 lista algunas rutinas definidas en MPI describiendo su uso [4].

Rutina MPI  Uso
MPI_Barrier Causa a todos los procesos en un grupo a bloquearse hasta que otros procesos alcancen esta rutina.
MPI_Send Envía un dato a un proceso; se bloquea hasta que sea recibido
MPI_Recv Recibe un dato de un proceso; se bloquea hasta que sea enviado
MPI_Isend Envía un mensaje; no se bloquea
MPI_Irecv Recibe un mensaje; no se bloquea
MPI_Probe Chequea si un mensaje está esperando; se bloquea hasta que el mensaje es detectado
MPI_Iprobe Verifica si un mensaje está esperando; no se bloquea
MPI_Bcast Envía un dato a todos los procesos de un grupo
MPI_Reduce Colecta una variable de todos los procesos en un grupo con una operación de combinación
MPI_Allreduce Todos los procesos reciben la reducción de la variable después que ha sido combinado
MPI_Gather Colecta datos de todos los procesos en un grupo dentro de un arreglo. Los datos son del mismo tamaño
MPI_Allgather Todos los procesos reciben datos recolectados de todos los procesos en un grupo. Los datos son del mismo tamaño
MPI_Scatter Distribuye datos a todos los procesos en un grupo. Los datos son del mismo tamaño
MPI_Gatherv Colecta datos de todos los procesos de un grupo dentro de un arreglo. Los datos pueden ser de diferentes tamaños
MPI_Scatterv Distribuye datos a todos los procesos en un grupo. Los datos pueden ser de diferentes tamaños
MPI_Alltoall Distribuye datos a todos los procesos en un grupo de todos los procesos del grupo. Los datos son del mismo tamaño
MPI_Alltoallv Distribuye datos a todos los procesos en un grupo de todos los procesos en el grupo. Los datos pueden ser de diferentes tamaños

Para mas información de MPI, vea también [9] o [7].

Un ejemplo de un programa en MPI

Como un ejemplo de como podemos usar MPI, podemos convertir el código mostrado en Lista de Código 1, a uno sólo que corra en un esquema de memoria-distribuída con una librería de MPI. Podríamos asumir que el usuario a decidido tener cuatro copias de un programa. La version MPI está listada en Lista de Código 3.

Lista de Código 3

int main(int argc, int *argv[]) {
   int        i, numberProcessors, 
              myProcessorNumber, sum, result;
   int        intArray[100];
   int        myChunk[25];
   int        err; /* Ignoraremos los errores de este código */
   MPI_Status status;

   /* Inicializa MPI. */
   err = MPI_Init(&argc, &argv);
   err = MPI_Comm_size(MPI_COMM_WORLD, &numberProcessors);
   err = MPI_Comm_rank(MPI_COMM_WORLD, &myProcessorNumber);

   /* Toma diferentes acciones dependiendo como este sea el proceso central o no.*/
   if (myProcessorNumber == 0) {

      /* Asumimos que initArray es la misma función que la vista en lista de Código 1 y 2.
      initArray(intArray, 100);

      /* Soy el proceso central, por lo tanto distribuyo el problema a los procesos. */
      for (i=1; i<numberProcessors; i++) {

         /* Envia un trozo de un arreglo a cada procesador. Los argumentos
	    son: un puntero a la dirección de comienzo, el número de objetos enviados,
	    tipo de los objetos enviados, procesador destino, identificador de mensaje,
	    grupo de procesos los cuales son elegibles para recibir el
	    mensaje (en el caso de MPI_COMM_WORLD, todos ellos) */
         err = MPI_Send(&intArray[i*25], 25, MPI_INT, i, 100,
                  MPI_COMM_WORLD);

	 /* Copia los datos del procesador dentro de un trozo de arreglo. Asume
	    que la función copyArray copia n enteros de un arreglo de enteros a 
	    otro arreglo de enteros. */
	 copyArray(intArray, myChunk, 25);
      }
   } else { 

      /* No soy el procesador central, por lo tanto recibo un pedazo de trabajo
         Los argumentos son: buffer en en cual de reciben los datos, números de
         objetos enviados, tipo de los objetos enviados, procesador destino,
         identificador de mensajes, grupo de procesadores, que están elegibles
         el mensaje, un puntero al estado de la variable (contiene información
         sobre el estado de la transmisión) */
      err = MPI_Recv(&myChunk[0], 25, MPI_INT, 0, 100, MPI_COMM_WORLD,
         &status); }

   /* Ahora el problema está distribuído, y resuelto. Cada procesador tendrá
      su propio trabajo distribuído en el arreglo myChunk. */
   sum = 0;

   for (i=0;i<25;i++) {
      sum = sum + myChunk[i];
   }

   /* Imprime la suma que cada procesador calculó. */
   printf("%d summed %d\n", myProcessorNumber, sum);

   /* Envía los resultados de vuelta al procesador central. */
   if (myProcessorNumber == 0) {

      /* Recibo resultados parciales y computo el resultado total . */
      for (i=1;i<numberProcessors;i++) {
         err = MPI_Recv(&result, 1, MPI_INT, i, 200, MPI_COMM_WORLD, &status);
	 sum = sum + result;
      }

   } else {
      /* No soy el procesador centrar, por lo tanto, envío mis resultados. */
      err = MPI_Send(&sum, 1, MPI_INT, 0, 200, MPI_COMM_WORLD);
   }

   /* hacer algo con el resultado final. */


   /* Finaliza. */
   err = MPI_Finalize();
}

Conclusiones

Ambos, esquema de memoria-compartida y esquema de memoria-distribuída tienen ventajas y desventajas en términos de la facilidad de la programación. Colocar un programa serial a un sistema de memoria-compartida puede usualmente ser un problema simple agregando un nivel de paralelismo con OpenMP, pero se debe tener cuidado con las condiciones de concurso, deadlocks y otros problemas asociados con las cuestiones de ese paradigma. Para los programadores que usan un paradigma de thread, moverse a un esquema OpenMP es relativamente bueno. Escribiendo un programa con MPI, en el otro caso, involucra un problema adicional resolviendo el cómo dividir el dominio de las tareas entre procesos con espacios de memoria diferentes. Coordinando los procesos con rutinas de comunicación puede ser un gran reto. No hay preocupación sobre las cuestiones de los threads, pero la sincronización de los datos continúa siendo una consideración.

Donde OpenMP tiene una ventaja es en la facilidad de la programación y la portabilidad de los programas seriales, aunque los sistemas de memoria-compartida en general tienen una pobre escabilidad. Agregando procesadores adicionales a un esquema de memoria-compartida incrementa el tráfico en el bus del sistema, haciendo mas lento el tiempo de acceso a memoria y demorando la ejecución de un programa. Un esquema de memoria-distribuída, sin embargo, toene la ventaja de que cada procesador está separado con su propio bus de acceso a la memoria. Por ésta causa, son mucho mas escalables. En adición, es posible construír mas grandes y baratos clusters usando las comodidades del sistema conectado vía red de trabajo. El proyecto Beowulf esta desarrlloando clusters para sistemas Linux usando MPI [1]. Sin embargo, la latencia de la red de trabajo es una cuestión, como para diseñar esquemas de comunicaciónes mas eficientes.

Reconocimientos

Quisiera agradecer a la Army High Performance Computing Research Center en Minneapolis, MN, por seleccionarme para participar en su 2001 Summer Institute for High Performance Computing e introducirme en los tópicos de la computación paralela. Gracias a sus útiles comentarios de todos los críticos anónimos.

Referencias

1
The Beowulf Project. http://www.beowulf.org/ (October, 2001).
2
Gropp, W., Lusk, E., Doss, N., Skjellum, A. A High-Performance, Portable Implementation of the Message Passing Interface Standard. Parallel Computing. Vol. 22. No. 6. pp. 789-828. Sept., 1996.
3
Hennessy, J. and Patterson, D. Computer Organization & Design. Morgan Kaufmann Publishers. San Francisco, 1998.
4
Johnson, A. Introduction to MPI. Army High Performance Computing Research Center Summer Institute Course Notes. 2001.
5
Karypis, G., Kumar, V. A Fast and High Quality Multilevel Scheme for Partitioning Irregular Graphs. Technical Report 95-035, University of Minnesota, 1995.
6
MPI - The Message Passing Interface Standard. http://www-unix.mcs.anl.gov/mpi/ (October 1, 2001).
7
MPI Forum. MPI: A Message Passing Interface. In Proceedings of 1993 Supercomputing Conference, Portland, Washington. November, 1993.
8
OpenMP C and C++ Application Program Interface. http://www.openmp.org/specs/. OpenMP Architecture Review Board (October, 1998).
9
Pacheco, P. Parallel Programming with MPI. Morgan Kaufmann Publishers. San Francisco, 1996.
10
Silberschatz, A., Galvin, P., Gagne, G. Applied Operating System Concepts. John Wiley and Sons. New York, 2000.
11
Sohn, A. Simon, H. A Scalable Parallel Dynamic Paritioner for Adaptive Mesh-based Computations. Proceedings of Supercomputing 1998, Orlando, Florida.
12
Visual KAP for OpenMP. http://www.kai.com/vkomp/_index.html (Oct. 28, 2001).


Biografía
Cory Quammen es un senior complentado su graduación en ciencias de la computación en el Gustavus Adolphus College en St. Peter, MN. Después de terminar su Ph.D., planea seguir en la investigación y desarrollo de la ciencia de la computación o enseñar en una universidad.


Last Modified:
Location: www.acm.org/crossroads/espanol/xrds8-3/programming.html