!--Tradedoubler site verification 1264796 --> El Rincon del Cluster: 2011

Cluster Beowulf + OpenMPI + NFS

Instalación y configuración
Condiciones previas
Antes de comenzar tenemos que tener por lo menos dos maquinas con Ubuntu 9.10 instalado y conectadas en red mediante el protocolo TCP/IP, para este caso todas las maquinas tienen que ser de la misma arquitectura para poder usar el binario compilado en el nodo maestro en cada uno de los nodos esclavos, caso sontrario sera necesario compilar la aplicacion segun cada plataforma. De aca en adelante me referire a los nodos del cluster como nodo maestro o nodo esclavo segun corresponda.
Desde  el nodo maestro podrás controlar el cluster y ejecutar los programas de administracion del cluster, en los nodos esclavos se ejecutaran las aplicaciones distribuidas desde el nodo maestro.
Instalación de los paquetes básicos
En Ubuntu 9.10 es necesario instalar los siguientes paquetes openmpi-bin, openmpi-common, libopenmpi1.3, libopenmpi-dev en todos los nodos del cluster.
Esto se puede hacer como root, en Ubuntu 9.10, con:
sudo apt-get install openmpi-bin openmpi-common libopenmpi1.3 libopenmpi-dev
Los nombres pueden variar en su distribución, pero lo importante es tener en cuenta que debe tener la misma versión de Open MPI en todas las máquinas para que funcione correctamente.
Para el control y administración remoto instalaremos también en cada integrante del cluster el servidor ssh.
sudo apt-get install ssh

Acceso remoto a los nodos

Para que el maestro sea capaz de ejecutar comandos en cada uno de los nodos necesitamos que estos últimos permitan el acceso ssh sin clave para lograr esto haremos lo siguiente.
Primero vamos a generar nuestra clave publica en el nodo meastro, esta sera la que nos permitirá acceder remotamente a cada nodo sin necesidad de proveer una clave por linea de comandos. Es importante que este proceso se haga en el maestro y los nodos esclavos usando el usuario erluco o el que usted halla elegido.
erluco@master:~$ ssh-keygen
Cuando se ejecute ese comando aceptar el directorio por defecto para almacenar las claves y cuando solicita la frase de paso pulsar enter sin ingresar nada.
Tenemos ahora que copiar la llave generada a cada uno de los nodos del cluster, para ello usaremos el comando de copia remota scp.
Primero en cada nodo esclavo creamos el directorio .ssh que es donde pondremos las llaves.
erluco@nodo1:~$ mkdir .ssh
erluco@nodo1:~$ chmod 700 .ssh

Ahora procedemos a la copia.
erluco@master:~$ scp .ssh/id_rsa.pub erluco@192.168.0.101:

Accedemos ahora al nodo donde copiamos la llave y lo ubicamos en el archivo correcto.
erluco@nodo1:~$ mv id_rsa.pub .ssh/authorized_keys

Los dos pasos anteriores, es decir el proceso de copia y mover el archivo a la ubicación correcta se tiene que repetir para cada nodo del cluster.
Ya estamos en condiciones de probar si el nodo maestro es capaz de ejecutar comandos sin necesidad de proveer la clave de cada nodo. Para hacer esta prueba, desde el maestros y como usuario erluco ejecutamos el siguiente comando:
erluco@master:~$ ssh erluco@192.168.0.101 hostname
y nos dara una respuesta :
nodo1
Si responde con el nombre de la maquina remota es que funciona correctamente. Ese comando lo que hace es usar el acceso ssh para a la maquina con el hostname (nodo1)  y ejecutar en ella el comando hostname.
Ahora vamos a corregir nuestros archivos hosts para lograr la identificación de los nodos del cluster mediante el nombre. Editamos en cada componente del nodo el archivo /etc/hosts
sudo nano /etc/hosts
y agregamos las siguientes lineas al contenido preexistente

192.168.0.100   master  master
192.168.0.101   nodo1  nodo1
192.168.0.102   nodo2  nodo2

Servidor de archivos NFS

Necesitamos en cada nodo de nuestro cluster los programas distribuidos que vamos a ejecutar. Para ello vamos a instalar y configurar un servidor de archivos NFS en el nodo maestro y los clientes NFS en los nodos esclavos de manera que trabajemos únicamente en el maestro con nuestros programas y luego mediante la exportación del directorio y el montaje remoto desde los nodos podamos tener los mismos archivos en cada uno de los componentes, existen otras formas de hacer esto, por ejemplo rsync, pero para mi gusto esta me resulto mas cómoda y transparente.
En el nodo maestro:
erluco@master:~# sudo apt-get install nfs-kernel-server nfs-common portmap
Ahora a configurar un recurso compartido, es nuestro caso crearemos un nuevo directorio dentro del home de erluco al que llamaremos clusterdir.
erluco@master:~$ mkdir clusterdir
Ahora nuevamente como root vamos a exportar este directorio mediante NFS para que los nodos puedan montarlo remotamente, editamos el archivo /etc/exports
sudo nano /etc/exports
y agregamos la siguiente linea.
/home/erluco/clusterdir 192.168.0.0/24(rw,no_subtree_check,async,no_root_squash)
Luego reiniciamos el resvicio de NFS con el comando:
erluco@master:~# /etc/init.d/nfs-kernel-server restart
Ahora vamos a instalar lo necesario en cada nodo.
erluco@nodo1:~# sudo apt-get install nfs-common portmap
Una vez que termino la instalacion probaremos si el directorio compartido es accesible desde el nodo:
erluco@nodo1:~# showmount -e 192.168.0.100
y respondera
Export list for 192.168.0.100:
/home/erluco/clusterdir 192.168.0.0/24
Bien ahora vamos a montar el recurso desde linea de comando para luego agregarlo a fstab para el montado automatico cada vez que arranque el nodo. Creamos en la home de la cuenta erluco un directorio llamado clusterdir al igual que el que compartimos en el nodo maestro, este sera el directorio donde montaremos el recursos compartido mediante NFS en el maestro.
erluco@nodo1:~$ mkdir clusterdir
Ahora montamos el directorio remoto, esto lo hacemos como root:
erluco@nodo1:~# sudo mount -t nfs 192.168.0.100:/home/erluco/clusterdir /home/erluco/clusterdir
Podemos comprobar mediante el siguiente comando si todo salio como corresponde:
nodo1:~# mount
y respondera
/dev/xvda2 on / type ext3 (rw,noatime,nodiratime,errors=remount-ro)
tmpfs on /lib/init/rw type tmpfs (rw,nosuid,mode=0755)
proc on /proc type proc (rw,noexec,nosuid,nodev)
sysfs on /sys type sysfs (rw,noexec,nosuid,nodev)
udev on /dev type tmpfs (rw,mode=0755)
none on /dev/pts type devpts (rw)
192.168.0.100:/home/erluco/clusterdir on /home/erluco/clusterdir type nfs (rw,addr=192.168.0.100)
Observe que el ultimo montaje corresponde al directorio compartido por NFS.
Para automatizar el proceso de montaje del recurso exportado en cada nodo esclavo del cluster modificamos el archivo /etc/fstab
sudo nano /etc/fstab
agregando la siguiente linea:
192.168.0.100:/home/erluco/clusterdir  /home/erluco/clusterdir   nfs      rw,sync,hard,intr  0     0
Para probar en caliente si funciona correctamente procedemos primero a desmontar el recurso con el comando:
erluco@nodo1:~# umount /home/erluco/clusterdir/
Y luego le decimos como al sistema que monte los sistemas declarado en fstab:
erluco@nodo1:~# mount –a
Ya esta todo listo en el NFS para continuar.

Entorno de desarrollo

En el nodo maestro es necesario instalar las herramientas de desarrollo o por lo menos los compiladores y librerías básicas para poder compilar nuestros programas, para ello ejecutamos el siguiente comando:
erluco@master:~# apt-get install build-essential
Configuramos MPI para decirle los nodos que componen el cluster, esto se hace creando un archivo de configuración llamado .mpi_hostfile en el home del usuario erluco del nodo maestros
nano .mpi_hostfile
 con el siguiente contenido:
# Nodo maestro
localhost slots=2

# Nodos esclavos
nodo1 slots=2
nodo2 slots=1

Prueba del cluster

Antes que nada vamos a ver un programa de ejemplo que es el que utilizaremos para probar el cluster, a continuacion el codigo fuente en C++ de un programa que suma numeros primos.
nano primos.c++
y pegamos esto
# include <cstdlib>
# include <iostream>
# include <iomanip>
# include <cmath>
# include <ctime>

# include "mpi.h"

using namespace std;

int main ( int argc, char *argv[] );
int prime_number ( int n, int id, int p );
void timestamp ( );

int main ( int argc, char *argv[] )

{
  int i;
  int id;
  int master = 0;
  int n;
  int n_factor;
  int n_hi;
  int n_lo;
  int p;
  int primes;
  int primes_part;
  double wtime;

  n_lo = 1;
  n_hi = 131072;
  n_factor = 2;
  MPI::Init ( argc, argv );
  p = MPI::COMM_WORLD.Get_size (  );
  id = MPI::COMM_WORLD.Get_rank ( );

  if ( id == master )
  {
    cout << "\n";
    cout << "'Cuenta primos\n";
    cout << "  C++/MPI version\n";
    cout << "\n";
    cout << "  Programa para contar cantidad de primos para un N dado.\n";
    cout << "  Corriendo en " << p << " procesos\n";
    cout << "\n";
    cout << "         N        S          Tiempo\n";
    cout << "\n";
  }

  n = n_lo;

  while ( n <= n_hi )
  {
    if ( id == master )
    {
      wtime = MPI::Wtime ( );
    }
    MPI::COMM_WORLD.Bcast ( &n, 1, MPI::INT, master );

    primes_part = prime_number ( n, id, p );

    MPI::COMM_WORLD.Reduce ( &primes_part, &primes, 1, MPI::INT, MPI::SUM,
      master );

    if ( id == master )
    {
      wtime = MPI::Wtime ( ) - wtime;

      cout << "  " << setw(8) << n
           << "  " << setw(8) << primes
           << "  " << setw(14) << wtime << "\n";
    }
    n = n * n_factor;
  }
  MPI::Finalize ( );

  if ( id == master )
  {
    cout << "\n";
    cout << "PRIME_MPI - Procesos maestro:\n";
    cout << "  Finalizacion del calculo normal.\n";
  }

  return 0;
}

int prime_number ( int n, int id, int p )

{
  int i;
  int j;
  int prime;
  int total;

  total = 0;

  for ( i = 2 + id; i <= n; i = i + p )
  {
    prime = 1;
    for ( j = 2; j < i; j++ )
    {
      if ( ( i % j ) == 0 )
      {
        prime = 0;
        break;
      }
    }
    total = total + prime;
  }
  return total;
}

void timestamp ( )

{
# define TIME_SIZE 40

  static char time_buffer[TIME_SIZE];
  const struct tm *tm;
  size_t len;
  time_t now;

  now = time ( NULL );
  tm = localtime ( &now );

  len = strftime ( time_buffer, TIME_SIZE, "%d %B %Y %I:%M:%S %p", tm );

  cout << time_buffer << "\n";

  return;
# undef TIME_SIZE
}

Lo compilamos con el comando:
erluco@master:~/clusterdir$ mpic++ primos.c++ -o primos
Esto nos dara como resultado el binario ejecutable del programa de prueba, podremos verificar que gracias a la comparticion NFS este binaro se encuentra disponible tambien en cada uno de los nodos que hemos agregado a nuestro cluster.
Ahora lo vamos a ejecutar y verificar su funcionamiento primero solo en el nodo maestro y luego en todos los nodos del cluster.
Primero en el nodo maestro unicamente:
erluco@master:~/clusterdir$ ./primos
Ahora distribuido en todos los nodos del cluster, para ello vamos a usar la herramienta mpirun con el parametro -np 5 con el que le decimos la cantidad de procesos a ejecutar.
erluco@master:~/clusterdir$ mpirun -np 5 --hostfile ../.mpi_hostfile ./primos
o
erluco@master:~/clusterdir$ mpirun -np 5./primos
y se obtendra este resultado :
Cuenta primos
 C++/MPI version

 Programa para contar cantidad de primos para un N dado.
 Corriendo en 3 procesos

 N        S          Tiempo

 1         0         0.33965
 2         1       0.0965161
 4         2       0.0619619
 8         4        0.125543
 16         6        0.179036
 32        11        0.122198
 64        18        0.152973
 128        31        0.117323
 256        54         0.17525
 512        97        0.182207
 1024       172        0.123595
 2048       309       0.0812861
 4096       564        0.162669
 8192      1028        0.207587
 16384      1900        0.494075
 32768      3512         1.62275
 65536      6542         3.71538
 131072     12251         1.0127

PRIME_MPI - Procesos maestro:
 Finalizacion del calculo normal.


DIRECTIVAS DE PARALELIZACION FORTRAN MP

Introducción
Este curso pretende dar una visión general sobre las directivas de paralelización Fortran MP, que permiten ejercer un mayor control sobre el código de programa del que tiene la paralelización automática. Esto da lugar a un mejor aprovechamiento de los recursos de la máquina Seymour, al especificarle como debe trabajar con cada región de nuestro programa. Las directivas Fortran MP se caracterizan por:


  • Son faciles de usar, permitiendo una paralelización muy efectiva de bucles con la introducción de unos pocos comandos. Estos comandos no son compatibles con compiladores de otros vendedores, aunque si con la paralelización automática PFA.
  • Se activan con la opción de compilación -mp. Esta opcion debe introducirse entre las que acompañan al comando f77 para permitir la paralelización del código.
  • Se inician siempre con la clave c$. Por tanto, sin la opción -mp se leen como comentarios en el código, y el programa se ejecuta normalmente en serie. (Esto facilita la portabilidad del código, pudiendo ser ejecutado tanto en serie como en paralelo.)
Los contenidos del curso se muestran en el siguiente índice:

  1. Directiva c$doacross.
  2. Control del número de procesos.
  3. Elementos de control del paralelismo.
  4. Dependencia de datos.
  5. Algunos consejos sobre como trabajar con MP.
  6. Algunos ejemplos prácticos.
  7. Documentación sobre Fortran MP.

Directiva c$doacross
c$doacrosses la directiva fundamental, que permite la paralelizacion de un bucle en distintos procesos. Su estructura general es:
c$doacross
c$&  share      (variable11,variable12,...),
c$&  local      (variable21,variable22,...),
c$&  lastlocal  (variable31,variable32,...),
c$&  reduction  (variable41,variable42,...),
c$&  if         (expresion logica),
c$&  mp_schedtype = type,
c$&  chunk        = chunksize
        
  • c$& son extensiones de linea; continuan con la descripción de c$doacross y cualquier otra directiva iniciada en la linea superior.
  • Las salidas y entradas de datos que puedan darse en el bucle afectado, deben ejecutarse en serie. Si no, el bucle no puede funcionar. Para ello se emplea una serie de directivas, con algunos ejemplos ilustrados más adelante.
  • No esta permitido el anidamiento de c$doacross; al hacerlo el programa no puede funcionar.
A continuación se hace una explicación de los modificadores que pueden acompañar a este comando.
Definición de Variables
Se facilita el manejo de las variables optimizando el tiempo de ejecución.
  • share: variables accesibles portodos los procesos.
  • local: variables propias de cada región paralela, no accesibles por el resto.
  • lastlocal: escalares de los que solo se salva el valor tras la última iteración.
  • reduction: escalares empleadas en operaciones con eliminacion de otras variables:
    Ej: A = A + B; A = A*B; A = min(A,B); A = max(A,B) (En estos casos se puede definir c$doacross reduction(A); consultense los ejemplos).
Por defecto los índices de bucle son lastlocal; las variables nuevas dentro del bucle c$doacross son local; el resto de variables share. Nota importante: hay que tener en cuenta que estas variables se definen partiendo de los tres tipos principales de variables en serie de Fortran:
  • Globales: corresponden a variables share.
  • Automaticas (creadas en la llamada a los procesos en los que se encuentran, y destruidas a la salida de los mismos): corresponden a variables local.
  • Estaticas (inicializadas a cero al inicio del programa, existen a lo largo de todo el mismo): no se permiten en paralelización MP. Para ello, no esta permitido emplear la opcion de compilacion -static (que convierte las variables locales en estáticas), y variables definidas con save y con data (también estáticas).
Un problema grave se da con las variables definidas dentro de un common, que por defecto son siempre de tipo share. Para definir un common con variables local son necesarias dos opciones de compilación, que indican al compilador que hacer con tales variables: -Wl,-Xlocal,***_, donde *** es el nombre del common con unicamente variables locales. Ejemplo: Suma de matrices
subroutine suma(A,B,C,M,N)
dimension A(M,N), B(M,N), C(M,N)
 
c$doacross local(i,j), share(A,B,C,M,N)   
   do 100 i = 1, M
     do 100 j = 1, N
       C(i,j) = A(i,j) + B(i,j)
100  continue

   return
   end

Modificador if
Solo se efectua la paralelizacion si se cumple la condicion impuesta. Se emplea:
  • Para paralelizar solo cuando los bucles se hacen grandes:
    Ej:
    c$doacross local(i,j)
    c$& if(imax.gt.1000.and.jmax.gt.1000)
     do i = 1,imax
       do j = 1,jmax
         ...
       enddo
     enddo
  • Para evitar dependencia de datos:
    Ej:
    c$doacross if(jmax.gt.imax), local(i)
     do i = 1, imax
       a(i) = b(i) + b(i+jmax)
     enddo
  • Para equilibrar trabajo entre procesos:
    Ej:
    num = valor
    c$doacross local(i), if (num.eq.2)
       do i = 1, 2
         if(i.eq.1) call work1
         if(i.eq.2) call work2
       enddo
 

mp_schedtype y chunk.
mp_schedtype define el modo de distribución del trabajo en los distintos procesos. Junto a ella se define el valor de chunk (número de iteraciones de bucle incluidas en cada envio). Ambas permiten un mejor equilibrio de carga entre procesos, mejorando el rendimiento del programa (limitado por el proceso más lento). Las opciones de mp_schedtype son:

  • simple (sin chunk): el total de iteraciones se divide entre el número de procesos: la primera división se envia al primer proceso, la segunda al segundo, ..., y la última al último proceso.
    Ej: Dadas 13 iteraciones en 4 procesos, los envios son: 1111 222 333 444.
  • interleave (chunk=1 por defecto): cada envio (de tantas iteraciones como define chunk) es llevado a cada proceso de manera correlativa: el primero a la primera, el segundo a la segunda,...).
    Ej: Dadas 13 iteraciones en 4 procesos, con chunk=1, los envios son: 1234 1234 1234 1
  • dynamic (chunk=1 por defecto): los envios se realizan a los procesos que van quedando libres.
  • gss (sin chunk): modo dinámico, que se inicia con envios grandes y termina con envios de un tamaño mas pequeño. Es el mejor modo de conseguir que todos los procesos acaben al mismo tiempo.
  • runtime: mp_schedtype y chunk quedan definidas por medio de variables de entorno: setenv MP_SCHEDTYPE type setenv CHUNK n .
Ejemplo del uso de mp_schedtype y chunk: conjunto de Mandelbrot.
El siguiente programa genera un conjunto de Mandelbrot de dimension 128x128, en el cada columna de puntos es determinada por un proceso distinto (paralelizacion de los bucles 'j'). Todas las variables se definen como locales, ya que son independientes en cada punto (i,j), excepto la variable data que es compartida en todos los casos.
Las iteraciones (i,j) tienen una duración bastante variable, ya que la condición de finalización del do while determina valores muy diferentes de 'k' dependiendo del punto al que se aplica.
integer i,j
  integer k, data(0:127,0:127)
  complex*8 c,m
c$doacross local(i,j,c,m,k), share(data),
c$& mp_schedtype = {simple | interleave | dynamic | gss}  
  do j = 0,127
   do i = 0,127
    c = cmplx (-2.+(4./127.)*j,-2.+(4./127.)*i)
    m = cmplx (0.,0.)
    k = 0           
    do while (abs(m).le.4..and.k.lt.5000)
     m = m**2 + c
     k = k + 1
    enddo
    data(i,j) = k
   enddo
  enddo
  stop
  end
La distribución de los valores de 'k' es bastante aleatoria. De ese modo, empleando mp_schedtype = simple o mp_schedtype = interleave el rendimiento conseguido puede no ser óptimo por una carga no equilibrada de los procesos. mp_schedtype = dynamic, y sobre todo mp_schedtype = gss reparten mejor el trabajo y posibilitan que todos los procesos puedan terminar a la vez, reduciendo el tiempo de ejecución.

Control del número de procesos.
El mejor modo de definir el número de procesos en que se debe ejecutar un programa es por medio de la variable de entorno:
%setenv MP_SET_NUMTHREADS N
Por defecto, esta es igual al número de CPUs en el sistema. 

Elementos de de control del paralelismo.
Existe una serie de funciones externas que, añadidas a nuestro codigo Fortran, nos permiten un mejor control sobre la ejecucion paralela de nuestro programa. Informacion completa sobre ellas se puede obtener a traves de las man pages, por medio de:

man mp
Lo que hacemos aqui es unicamente una enumeracion de las mas importantes:
  1. Control de procesos:
    subroutine mp_set_numthreads()
    Determina el numero de procesos que deben ejecutar un programa.
    integer function mp_numthreads()
    Indica el numero de procesos que estan ejecutando un programa.
    integer function mp_my_threadnum()
    Indica que proceso esta determinando aquella parte de programa en la que se incluye.
    logical function mp_in_doacross_loop()
    Responde a la pregunta: ¿Estoy dentro de un bucle paralelo?

  2. Sincronización de procesos:
    subroutine mp_setlock()
    subroutine mp_unsetlock()
    La parte de codigo situada entre estas dos subrutinas se ejecuta una sola vez por cada proceso y de manera no simultanea (acceso limitado a un unico proceso a la vez).
    subroutine mp_barrier()
    Ningun proceso puede seguir adelante mientras no hayan llegado todos a este punto.

  3. Bloqueo de procesos 'esclavos'.
    subroutine mp_block()
    subroutine mp_unblock()
    Estas dos subrutinas rodean a una parte del código que no precisa de los procesos 'esclavos' para su ejecución. Entre ellas se dejan libres estos procesos, para que se puedan usar en otros trabajos.
    subroutine mp_blocktime(integer iters)
    Proceso automático de bloqueo o liberación de procesos esclavos, cuando estos dejan de formar parte del trabajo activo, y entran en el 'bucle de espera' un numero de veces definido por 'iters'.

  4. Creación y destrucción de procesos.
    subroutine mp_create(integer num)
    Subrutina de creacion e inicializacion de procesos, de modo que el número total de procesos activos sea el indicado por 'num'.
    subroutine mp_destroy()
    Subrutina que elimina todos los procesos 'esclavos'.
    subroutine mp_setup()
    Subrutina que crea e inicializa procesos, en un número que si no se especifica en cualquier otra rutina de definición de procesos, adopta como valor el número total de CPUs de la maquina. No es necesaria; es llamada automaticamente en la entrada a la primera región paralela. 
     
    Dependencia de datos.
    Cuando se paralelice un bucle se debe uno asegurar de que no exista dependencia de datos (dependencia de los datos que se computan en una iteracion de los obtenidos en las iteraciones anteriores). Esto no se debe pasar por alto en paralelización MP, pues toda la responsabilidad de programación recae sobre el usuario (por lo que este debe estar siempre al tanto de no dar lugar a cálculos erroneos). En comparación, en paralelización automática PFA esto no es necesario, ya que el compilador se encarga de paralelizar solo los bucles en los que esta seguro de no encontrar dependencias de datos. Un modo fácil de saber si un bucle no tiene dependencia de datos, es que su ejecución sea reversible (la última iteración se pueda ejecutar la primera). Por otro lado, muchas veces es fácil eliminar la dependencia de datos en un bucle. Por ejemplo:
    integer a(20,20)
    k = 0
     do i = 1,20
      do j = 1,20
       k = k + 1
       a(i,j) = k
      enddo
     enddo
    Este bucle se puede paralelizar por medio de:
    integer a(20,20)
    c$doacross local(i,j) share(a)
      do i = 1,20
       do j = 1,20
        a(i,j) = 20*(i-1) + j
       enddo
      enddo
    En general, los casos mas dificiles de paralelizar son aquellos en los que los datos se van uniendo en una cadena de dependencias hasta el dato inicial. Sin embargo, en un bucle anidado estas dependencias pueden deberse a un unico indice, pudiendo paralelizar en otros indices del bucle. Por ejemplo:
    c$doacross local(i,j) share(imax,jmax,a,b)
      do j = 1,jmax
       a(1,j) = b(i)
       do i = 2,imax
        a(i,j) = a(i-1,j) + b(i)
       enddo
      enddo
    Finalmente, hay que tener especial cuidado con las llamadas a subrutinas externas desde un bucle que queramos paralelizar, ya que a veces las subrutinas hace llamadas a datos hallados en llamadas anteriores a la misma.  

 
  Algunos consejos sobre como trabajar con Fortran MP.
Una vez escrito el codigo de programa con todas las directivas MP, lo primero que hay que hacer es correrlo en serie para asegurarnos de que no hay errores de lenguaje y los resultados del programa son correctos. Para ello basta con compilar sin la opción -mp, con lo que 'olvida' las directivas de paralelización y crea un ejecutable que corre sin problemas sobre un procesador en serie.
Una vez que el programa en serie funciona sin errores, se debe hacer un estudio de dependencias de datos en los bucles, para saber cuando es posible paralelizar. Aqui nos puede ser muy util la paralelizacion automatica PFA; en los archivos de ayuda que genera el compilador (programa.w2f.f, programa.l), se nos indican los problemas por los que no se paralelizan muchos bucles, lo que nos puede ayudar a la hora de solventarlos. (Sin embargo hay que recordar que la paralelizacion automatica tiene un criterio muy conservador y que muchas veces se puede llegar mas lejos de los que esta indica; se recomienda consultar las preguntas más frecuentes para saber mas sobre paralelización automática).
Este proceso es iterativo: se debe hacer un estudio de cada bucle y comprobar con cada transformacion que los resultados son correctos. Este es un proceso bastante lento, por lo que se recomienda concentrarse en las partes del programa que tardan mas tiempo, y que en realidad son las unicas que nos van a permitir un mejor rendimiento del programa (en lo que se llama una paralelizacion de grano grueso). Por medio de la herramienta SpeedShop y sus diversas aplicaciones (ssusage, ssrun,...) podremos orientarnos sobre donde merece la pena paralelizar (consultense tambien las preguntas más frecuentes para saber más del tema).
Se debe tener tambien en cuenta que, a veces, los resultados erroneos son debidos a una mala definicion de las variables en nuestro programa (variables share que debian ser local, etc...). Esto se puede comprobar comparando la version serie del programa, con la version paralela corrida sobre un solo procesador: si los resultados no son iguales hay un problema de definción de variables. 

Algunos Ejemplos Prácticos.
  1. Paralelizacion de bucles anidados:
    Paralelizacion de las unidades de trabajos mas largas, buscando el equilibrio entre procesos.
    c    Paralelizacion del bucle 
    c    interno (bucle mas largo).
    c    No vale la pena paralelizar 
    c    el bucle externo: la ganancia
    c    de tiempo se consigue con el 
    c    interno, y se pueden dar 
    c    problemas de equilibrio.
    
      do 10 j = 1,5
    c$doacross local(i),share(j)
       do 10 i = 1,1000
        b(i,j) = a(i,j)
    10  continue
  2. Paralelizacion de operaciones con reduccion de variables:
    Una operacion de reduccion es del tipo:
    A = A+B, A = A*B, A = max(A,B), A = min(A,B).
    Como ya se indico, el modo mas simple de paralelizacion se limita a definir la variable A como reduction.
    c    Por medio de 'reduction', 
    c    uno se asegura de sumar una 
    c    vez todos los elementos de 'A' 
    c    a la variable 'red', aunque
    c    estemos trabajando en distintos 
    c    procesos.
    
      red = 0.0
    c$doacross local(i,j), 
    c$& share(A, imax, jmax), 
    c$& reduction(red)
      do j = 1,jmax
       do i = 1,imax
        red = red + A(i,j)
       enddo
      enddo


    Otro modo consiste en definir una variable auxiliar de reduccion para cada uno de los procesos en que estemos paralelizando. Este metodo esta especialmente recomendado en bucles grandes, ya que evita un grave problema de comparticion innecesaria o false sharing.
    Las variables reduction son en realidad un caso especial de variables share; se comparten en todos los procesos, y cada vez que son modificadas deben ser actualizadas en todos ellos, algo que no es necesario y que ralentiza enormemente el tiempo de ejecución.

    c En este programa se establece 
    c una variable de suma 'redj', que
    c es local en cada proceso y que 
    c no sufre 'false sharing'. Una vez 
    c obtenida la suma parcial en cada 
    c proceso, se obtiene la suma total, 
    c por medio de:
    c         c$call mp_setlock()
    c            red = red + redj
    c         c$call mp_setunlock()
    c Las funciones de sincronizacion 
    c 'mp_setlock' y 'mp_setunlock'
    c garantizan que a la sentencia de 
    c suma se accede una sola vez desde 
    c todos los procesos, contribuyendo 
    c cada uno con su suma parcial 'redj'.
    c ¡¡¡¡ Estas sentencias deben 
    c incluirse en el bucle que ha sido 
    c paralelizado, teniendo en cuenta 
    c que en cada proceso 'redj' es una 
    c variable distinta !!!!
    
      red = 0.0
    c$doacross local(i,j,redj)
      do j = 1,jmax
       redj = 0.0
       do i = 1,imax
        redj = redj + A(i,j)
       enddo
    c$call mp_setlock()
       red = red + redj
    c$call mp_setunlock()
      enddo 
  3. Paralelización de bucles con entrada y salida de datos.
    c$doacross solo permiten entrada y salida de datos en serie. Para ello los comandos I/O deben estar dentro de llamadas a mp_setlock y mp_setunlock, que permiten ejecutarlos en serie.
    Empleando de nuevo el ejemplo anterior:
    red = 0.0
    c$doacross local(i,j,redj)
      do j = 1,3
       redj = 0.0
       do i = 1,3
        redj = redj + A(i,j)
       enddo
    c$call mp_setlock()
       write (*,*) redj
       red = red + redj
    c$call mp_setunlock()
      enddo 
      write(*,*) red
  4. Paralelización de bucles con llamadas a funciones externas.
    as llamadas a funciones externas no plantean problemas a la hora de emplearlas en paralelo dentro de c$doacross.
    Uno debe preocuparse solo de definir correctamente de que tipo son las variables incluidas dentro de la funcion externa, para que estas sean interpretadas correctamente: (local, share...)
    La estructura general del bucle seria por tanto:
    c$doacross local(i)
      do i = i,imax
       call work(i)
      enddo
      ...
    
      subroutine work(i)
      ...
         
  5. Problemas con un vector o matriz local.
    Un grave problema que presenta un programa Fortran es que el tamaño de un vector o matriz debe ser especificado de manera explicita en el código, al inicio del programa o subprograma en que se aplica. No se puede emplear para definir el tamaño una variable no especificada o un valor definido en un bloque common.
    Esto obliga a hacer vectores y matrices de gran tamaño, que en genera pueden quedar semivacios, pero que garantizan la validez del programa cuando el numero de elementos que se precisan se hace muy grande.
    c    Suma de vectores
    
      subroutine vector(A,B,C)
      dimension A(100), B(100), C(100)
    c$doacross local(i), share(A,B,C)
      do i = 1,100
       C(i) = A(i) + B(i)
      enddo
      return
      end 
    
    c    No es valido hacer lo mismo con 
    c    el siguiente codigo, cuando el 
    c    valor de 'n' no esta especificado 
    c    en el codigo.
    
      subroutine vector(A,B,C,n)
      dimension A(n), B(n), C(n)
    c$doacross local(i), share(A,B,C,n)
      do i = 1,n
       C(i) = A(i) + B(i)
      enddo
      return
      end
 


Cluster Beowulf + OpenMPI + NFS

Instalación y configuración Condiciones previas Antes de comenzar tenemos que tener por lo menos dos maquinas con Ubuntu ...