El siguiente ejemplo es una aplicación mucho más complicada, que implementa el problema del flujo de calor en PVM. De muchos modos, nos da una vsión del trabajo que realiza el ambiente HPF. Resolveremos un flujo de calor en una placa bidimensional con dos fuentes de calor y los bordes inmersos en agua a cero grados, como se muestra en Figure 1.
Los datos se repartirán entre todos los procesos usando una distribución (*, BLOCK). Las columnas se distribuyen entre procesos en bloques contiguos, y todos los elementos de los renglones en una columna se almacenan en el mismo proceso. Como con HPF, el proceso que "posee" una celda de datos realiza los cálculos para esa celda tras recibir cualquier dato necesario para realizar el cálculo.
Usamos un enfoque rojo-negro, pero por simplicidad copiamos los datos de vuelta al final de cada iteración. Para que verdaderamente fuera rojo-negro, debe usted realizar un cálculo en la dirección opuesta cada nuevo paso.
Note que en vez de engendrar procesos esclavos, el proceso padre engendra copias adicionales de sí mismo. Esto es típico de los programas estilo SPMD. Una vez engendrados tales procesos adicionales, todos los procesos esperan en una barrera antes de observar los números de proceso de los miembros del grupo. Una vez que los procesos han llegado a la barrera, todos ellos recuprean una lista de los distintos números de proceso:
% cat pheat.f
PROGRAM PHEAT
INCLUDE ’../include/fpvm3.h’
INTEGER NPROC,ROWS,COLS,TOTCOLS,OFFSET
PARAMETER(NPROC=4,MAXTIME=200)
PARAMETER(ROWS=200,TOTCOLS=200)
PARAMETER(COLS=(TOTCOLS/NPROC)+3)
REAL*8 RED(0:ROWS+1,0:COLS+1), BLACK(0:ROWS+1,0:COLS+1)
LOGICAL IAMFIRST,IAMLAST
INTEGER INUM,INFO,TIDS(0:NPROC-1),IERR
INTEGER I,R,C
INTEGER TICK,MAXTIME
CHARACTER*30 FNAME
* Obtener como va la cosa SPMD - Unirse al grupo pheat
CALL PVMFJOINGROUP(’pheat’, INUM)
* Si somos los primeros en el grupo pheat, creamos algunos ayudantes
IF ( INUM.EQ.0 ) THEN
DO I=1,NPROC-1
CALL PVMFSPAWN(’pheat’, 0, ’anywhere’, 1, TIDS(I), IERR)
ENDDO
ENDIF
* Barrera para asegurarnos que todos estamos en este punto, y así poder buscarnos
CALL PVMFBARRIER( ’pheat’, NPROC, INFO )
* Encontrar a mis camaradas y obtener sus TIDs - Los TIDS son necesarios para los envíos
DO I=0,NPROC-1
CALL PVMFGETTID(’pheat’, I, TIDS(I))
ENDDO
En este punto del código, tenemos NPROC procesos ejecutándose en modo SPMD. El siguiente paso es determinar cuál subconjunto del arreglo debe calcular cada proceso, lo cual se maneja mediante la variable INUM, cuyo rango va de 0 a 3 y que identifica unívocamente esos procesos.
Descomponemos los datos y almacenamos sólo un cuarto de los mismos en cada proceso. Usando la variable INUM, elegimos nuestro conjunto continuo de columnas para almacenar y calcular. La variable OFFSET mapea entre una columna "global" en el arreglo completo, y una columna local en su propio subconjunto del arreglo. Figure 2 muestra un mapa que indica cuáles procesadores almacenan cuáles elementos de datos. Los valores marcados con una B son los valores de frontera, y no cambian durante la simulación. Todos ellos se ponen a 0. Este código a menudo resulta difícil de aprehender. Realizar una distribución (BLOCK, BLOCK) requiere una descomposición bidimensional e intercambiar datos con los vecinos superior e inferior, además de los vecinos izquierdo y derecho:
* Calcula mi geometría - ¿Qué subconjunto debo procesar? (INUM=0 valores)
* Columna actual = OFFSET + Columna (OFFSET = 0)
* Columna 0 = vecinos a la izquierda
* Columna 1 = enviar a la izquierda
* Columnas 1..mylen Las celdas que me toca calcular
* Columna mylen = Enviar a la derecha (mylen=50)
* Column mylen+1 = Vecinos de la derecha (Columna 51)
IAMFIRST = (INUM .EQ. 0)
IAMLAST = (INUM .EQ. NPROC-1)
OFFSET = (ROWS/NPROC * INUM )
MYLEN = ROWS/NPROC
IF ( IAMLAST ) MYLEN = TOTCOLS - OFFSET
PRINT *,’INUM:’,INUM,’ Local’,1,MYLEN,
+ ’ Global’,OFFSET+1,OFFSET+MYLEN
* Inicio en frío
DO C=0,COLS+1
DO R=0,ROWS+1
BLACK(R,C) = 0.0
ENDDO
ENDDO
Ahora estamos ejecutando los pasos consecutivos. La primera acción en cada paso es reiniciar las fuentes de calor. En esta simulación, tenemos cuatro fuentes de calor colocadas cerca del centro de la placa. Debemos reiniciar todos los valores cada vez que ejecutamos la simulación, conforme se ven modificados por el ciclo principal:
* Comenzamos la ejecución de los pasos sucesivos
DO TICK=1,MAXTIME
* Configuramos las fuentes persistentes de calor
CALL STORE(BLACK,ROWS,COLS,OFFSET,MYLEN,
+ ROWS/3,TOTCOLS/3,10.0,INUM)
CALL STORE(BLACK,ROWS,COLS,OFFSET,MYLEN,
+ 2*ROWS/3,TOTCOLS/3,20.0,INUM)
CALL STORE(BLACK,ROWS,COLS,OFFSET,MYLEN,
+ ROWS/3,2*TOTCOLS/3,-20.0,INUM)
CALL STORE(BLACK,ROWS,COLS,OFFSET,MYLEN,
+ 2*ROWS/3,2*TOTCOLS/3,20.0,INUM)
Ahora realizamos el intercambio de los "valores fantasmas" con nuestros procesos vecinos. Por ejemplo, el Proceso 0 contiene los elementos para la columna global 50. Para calcular los valores para la columna 50 del paso siguiente , necesitamos la columna 51, que está almacenada en el Proceso 1. Similarmente, antes de que el Proceso 1 pueda calcular los nuevos valores para la columna 51, requiere los valores del Proceso 0 para la columna 50.
La Figure 3 muestra cómo se transfieren los datos entre procesadores. Cada proceso envía su columna más a la izquierda al proceso a su izquierda, y su columna más a la derecha al proceso a su derecha. Dado que el primer y último procesos están rodeados por valores de frontera que no cambian a la izquierda y derecha, respectivamente, ello no es necesario para las columnas 1 y 200. Si todo se hace apropiadamente, cada proceso puede recibir sus valores fantasmas de sus vecinos izquierdo y derecho.
El resultado neto de todas las transferencias es que para cada espacio que debe calcularse, está rodeado por una capa ya sea de valores de frontera o valores fantasma de los vecinos izquierdo y derecho:
* Enviar izquierdo y derecho
IF ( .NOT. IAMFIRST ) THEN
CALL PVMFINITSEND(PVMDEFAULT,TRUE)
CALL PVMFPACK( REAL8, BLACK(1,1), ROWS, 1, INFO )
CALL PVMFSEND( TIDS(INUM-1), 1, INFO )
ENDIF
IF ( .NOT. IAMLAST ) THEN
CALL PVMFINITSEND(PVMDEFAULT,TRUE)
CALL PVMFPACK( REAL8, BLACK(1,MYLEN), ROWS, 1, INFO )
CALL PVMFSEND( TIDS(INUM+1), 2, INFO )
ENDIF
* Recibir derecho, luego izquierdo
IF ( .NOT. IAMLAST ) THEN
CALL PVMFRECV( TIDS(INUM+1), 1, BUFID )
CALL PVMFUNPACK ( REAL8, BLACK(1,MYLEN+1), ROWS, 1, INFO
ENDIF
IF ( .NOT. IAMFIRST ) THEN
CALL PVMFRECV( TIDS(INUM-1), 2, BUFID )
CALL PVMFUNPACK ( REAL8, BLACK(1,0), ROWS, 1, INFO)
ENDIF
El siguiente segmento es la parte fácil. Todos los valores fantasma apropiados están en su lugar, así que simplemente debemos realizar el cálculo de nuestro subespacio. Al final, copiamos de vuelta desde el arreglo ROJO al arreglo NEGRO; en una simulación real, podemos realizar dos pasos consecutivos, uno desde NEGRO hacia ROJO y el otro de ROJO hacia NEGRO, para ahorrar esta copia extra:
* Realiza el flujo
DO C=1,MYLEN
DO R=1,ROWS
RED(R,C) = ( BLACK(R,C) +
+ BLACK(R,C-1) + BLACK(R-1,C) +
+ BLACK(R+1,C) + BLACK(R,C+1) ) / 5.0
ENDDO
ENDDO
* Copia de vuelta - Normalmente haríamos una versión roja y una negra de este ciclo
DO C=1,MYLEN
DO R=1,ROWS
BLACK(R,C) = RED(R,C)
ENDDO
ENDDO
ENDDO
Ahora encontramos la celda central y la enviamos al proceso maestro (si es necesario) de forma que pueda imprimirse. También volcamos los datos en archivos para depuración o visualización posterior de los resultados. Cada archivo se hace único agregando a su nombre el número de instancia. Después el programa termina:
CALL SENDCELL(RED,ROWS,COLS,OFFSET,MYLEN,INUM,TIDS(0),
+ ROWS/2,TOTCOLS/2)
* Volcado de los datos para verificación
IF ( ROWS .LE. 20 ) THEN
FNAME = ’/tmp/pheatout.’ // CHAR(ICHAR(’0’)+INUM)
OPEN(UNIT=9,NAME=FNAME,FORM=’formatted’)
DO C=1,MYLEN
WRITE(9,100)(BLACK(R,C),R=1,ROWS)
100 FORMAT(20F12.6)
ENDDO
CLOSE(UNIT=9)
ENDIF
* Ponemos todo junto
CALL PVMFBARRIER( ’pheat’, NPROC, INFO )
CALL PVMFEXIT( INFO )
END
La rutina SENDCELL encuentra una celda en particular, y la imprime en el proceso maestro. Esta rutina se invoca en un estilo SPMD: todos los procesos entran en ella, si bien no necesariamente al mismo tiempo. Dependiendo del INUM y de la celda que estemos observando, cada proceso puede hacer algo diferente.
Si la celda en cuestión está en el proceso maestro, y nosotros somos el proceso maestro, imprimimos. Ninguno de los demás procesos hace algo. Si la celda en cuestión está almacenada en otro proceso, aquél que la contenga la envía al proceso maestro. El proceso maestro recibe el valor y lo imprime. El resto de los procesos no hacen nada.
Se tata de un ejemplo simple de un código con un estilo típicamente SPMD. Todos los procesos ejecutan el codigo aproximadamente al mismo tiempo, pero en base a la información local de cada proceso, las acciones realizadas por los diferentes procesos pueden ser muy distintas:
SUBROUTINE SENDCELL(RED,ROWS,COLS,OFFSET,MYLEN,INUM,PTID,R,C)
INCLUDE ’../include/fpvm3.h’
INTEGER ROWS,COLS,OFFSET,MYLEN,INUM,PTID,R,C
REAL*8 RED(0:ROWS+1,0:COLS+1)
REAL*8 CENTER
* Calculamos el número de renglón local, para determinar si es nuestro
I = C - OFFSET
IF ( I .GE. 1 .AND. I.LE. MYLEN ) THEN
IF ( INUM .EQ. 0 ) THEN
PRINT *,’Master has’, RED(R,I), R, C, I
ELSE
CALL PVMFINITSEND(PVMDEFAULT,TRUE)
CALL PVMFPACK( REAL8, RED(R,I), 1, 1, INFO )
PRINT *, ’INUM:’,INUM,’ Returning’,R,C,RED(R,I),I
CALL PVMFSEND( PTID, 3, INFO )
ENDIF
ELSE
IF ( INUM .EQ. 0 ) THEN
CALL PVMFRECV( -1 , 3, BUFID )
CALL PVMFUNPACK ( REAL8, CENTER, 1, 1, INFO)
PRINT *, ’Master Received’,R,C,CENTER
ENDIF
ENDIF
RETURN
END
Como la rutina previa, todos los procesos ejecutan la rutina STORE. La idea es almacenar un valor en una posición de renglón y columna global. Primero, debemos determinar si la celda está en nuestro proceso. Si es así, debemos calcular la columna local (I) en nuestro subconjunto de la matriz global, y luego almacenar el valor:
SUBROUTINE STORE(RED,ROWS,COLS,OFFSET,MYLEN,R,C,VALUE,INUM)
REAL*8 RED(0:ROWS+1,0:COLS+1)
REAL VALUE
INTEGER ROWS,COLS,OFFSET,MYLEN,R,C,I,INUM
I = C - OFFSET
IF ( I .LT. 1 .OR. I .GT. MYLEN ) RETURN
RED(R,I) = VALUE
RETURN
END
Cuando se ejecuta este programa, proporciona la siguiente salida:
% pheat
INUM: 0 Local 1 50 Global 1 50
El maestro recibió 100 100 3.4722390023541D-07
%
Vemos do líneas de impresión. La primera indica los valores que el Proceso 0 usó en su cálculo de geometría. La segunda es la salida del proceso maestro con la temperatura de la celda (100,100), tras 200 ciclos de tiempo.
Una técnica interesante, que resulta útil para depurar este tipo de programa, es cambiar el número de procesos creados. Si el programa no está moviendo sus datos apropiadamente, usualmente obtendrá usted distintos resultados cuando use distinto número de procesos. Si lo observa detalladamente, notará que el código anterior funciona correctamente de 1 a 30 procesos.
Observe que no hay una operación de barrera al final de cada ciclo de tiempo. Ello contrasta con la forma en que operan los ciclos paralelos en los multiprocesadores con acceso a uniforme a memoria compartida, que fuerzan una barrera al final de cada ciclo. Dado que hemos puesto como regla que "el dueño calcule", y que nada se calcule hasta que se hayan recibido todos los datos fantasma, no hay necesidad de tal barrera. El receptor de los mensajes con los valores fantasmas apropiados permite a un proceso comenzar a calcular de inmediato, sin importar lo que los otros procesos estén realizando en ese momento.
Este ejemplo puede usarse ya sea como un marco de trabajo para desarrollar otros cálculos basados en retículas, o como una buena excusa para usar HPF y apreciar el arduo trabajo llevado a cabo por los creadores de compiladores HPF. Una implementación bien hecha de esta simulación en HPF debe presentar mejor rendimiento que la implementación PVM, porque HPF puede llevar a cabo optimizaciones más estrictas. Al contrario que nosotros, el compilador HPF no tiene por qué hacer que sea fácilmente legible el código que genera.