Procesamiento Paralelo en Redes Linux Utilizando MPI Alumno: Vicente F. Reyes Puerta Director: Jose Antonio Jiménez Millán Julio 2003 2 Resumen El presente documento realiza un estudio de investigación que aborda el procesamiento paralelo en redes de ordenadores. De esta manera se analiza la utilización de los clusters (grupos de ordenadores conectados en red) a modo de computador virtual paralelo como plataforma para la ejecución de aplicaciones paralelas. El entorno utilizado para realizar dicho estudio es el sistema operativo Linux, el cual ofrece todas las facilidades de acceso a dispositivos y comunicación necesarias para la ejecución de los algoritmos paralelos. Para la implementación de los algoritmos paralelos desarrollados hemos utilizado la interfaz MPI (“Message Passing Interface”, Interfaz de Paso de Mensajes). Dicha interfaz es un estándar que define la sintaxis y la semántica de las funciones contenidas en una librería de paso de mensajes diseñada para ser usada en programas que exploten la existencia de múltiples procesadores. De este modo no sólo conseguimos que el desarrollo de los algoritmos sea más claro y sencillo, si no que además logramos que sean portables. El estudio realizado queda dividido en tres partes diferenciadas que coinciden con la estructura del documento. En un principio hacemos un estudio general sobre el procesamiento paralelo, y establecemos una clasificación detallada sobre los distintos tipos de paralelismo existentes en la actualidad. En la segunda parte realizamos un análisis detallado de la interfaz de paso de mensajes MPI, ejemplificando la información ofrecida con diversos algoritmos escritos en C que realizan cómputos paralelos utilizando dicha interfaz. Por último generamos un estudio de los resultados obtenidos en las ejecuciones explicando paso a paso la manera en la que han sido cosechados. 3 4 Índice general Objetivos I Desarrollo III I Sistemas de Procesamiento Paralelo 1 1. Introducción 1.1. Utilidades del Procesamiento Paralelo . . . . . . . 1.2. Definiciones Básicas . . . . . . . . . . . . . . . . 1.2.1. Características Físicas de la Comunicación 1.2.2. Estándares Relacionados . . . . . . . . . . 1.3. Clasificación Sistemas Paralelos . . . . . . . . . . 1.3.1. Paralelismo Implícito o de Bajo Nivel . . . 1.3.2. Paralelismo Explícito o de Alto Nivel . . . 1.4. Arquitecturas Basadas en Paralelismo Implícito . . 1.4.1. Segmentación o pipeline . . . . . . . . . . 1.4.2. Tecnología SWAR . . . . . . . . . . . . . 1.4.3. Procesadores Auxiliares . . . . . . . . . . 1.5. Arquitecturas Basadas en Paralelismo Explícito . . 1.5.1. Multiprocesadores . . . . . . . . . . . . . 1.5.2. Multicomputadores . . . . . . . . . . . . 1.5.3. Procesadores Matriciales . . . . . . . . . 1.5.4. Procesadores Vectoriales . . . . . . . . . . 2. Herramientas Desarrollo Software Paralelo 2.1. Modelos de Interacción entre Procesadores 2.1.1. Paso de Mensajes . . . . . . . . . . 2.1.2. Memoria Compartida . . . . . . . . 2.2. Utilidades de Desarrollo . . . . . . . . . . 2.2.1. PVM . . . . . . . . . . . . . . . . 2.2.2. MPI . . . . . . . . . . . . . . . . 2.2.3. P4 . . . . . . . . . . . . . . . . . . 2.2.4. Express . . . . . . . . . . . . . . . 2.2.5. Linda . . . . . . . . . . . . . . . . 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 4 4 5 6 6 7 9 9 13 15 19 19 26 37 38 . . . . . . . . . 43 43 43 46 49 49 50 51 52 53 ÍNDICE GENERAL 6 II Guía MPI 3. El Estándar MPI 3.1. Origen . . . . . . 3.2. Historia . . . . . 3.3. Objetivos . . . . 3.4. Usuarios . . . . 3.5. Plataformas . . . 3.6. Versiones . . . . 3.6.1. MPI-1 . . 3.6.2. MPI-2 . . 3.7. Implementaciones 55 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 57 58 59 62 62 62 63 64 64 4. Conceptos Básicos 4.1. Algoritmo ¡Hola Mundo! . . . . . . . . . 4.2. Programas MPI en General . . . . . . . . 4.3. Informándonos del Resto del Mundo . . . 4.4. El Problema de la Entrada/Salida . . . . . 4.5. Ubicación de los Procesos . . . . . . . . 4.6. Información Temporal . . . . . . . . . . 4.7. Implementación Algoritmo ¡Hola Mundo! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 67 68 69 69 70 70 71 . . . . . . . . . . 73 73 74 76 77 78 79 80 80 80 86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5. Paso de Mensajes 5.1. Algoritmo Cálculo de Áreas mediante Montecarlo . . . 5.2. El Entorno del Mensaje . . . . . . . . . . . . . . . . . 5.3. Funciones de Paso de Mensajes Bloqueantes . . . . . . 5.4. Funciones de Paso de Mensajes No Bloqueantes . . . . 5.5. Agrupaciones de Datos . . . . . . . . . . . . . . . . . 5.5.1. Tipos Derivados . . . . . . . . . . . . . . . . 5.5.2. Vectores . . . . . . . . . . . . . . . . . . . . . 5.6. Implementación Cálculo de Áreas mediante Montecarlo 5.6.1. Implementación con Mensajes Bloqueantes . . 5.6.2. Implementación con Mensajes No Bloqueantes 6. Comunicación Colectiva 6.1. Algoritmo Regla del Trapecio . . . . . 6.2. Distribución y Recolección de los Datos 6.3. Operaciones de Comunicación Colectiva 6.4. Operaciones de Reducción . . . . . . . 6.5. Implementación Regla del Trapecio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 93 94 96 97 98 7. Comunicadores y Topologías 7.1. Algoritmo Multiplicación de Matrices de Fox . . . . 7.2. Comunicadores . . . . . . . . . . . . . . . . . . . . 7.3. Trabajando con Grupos, Contextos y Comunicadores 7.4. Particionamiento de los Comunicadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 105 106 107 110 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ÍNDICE GENERAL 7 7.5. Topologías . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 7.6. División de Rejillas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 7.7. Implementación Multiplicación de Matrices de Fox . . . . . . . . . . . . . . 114 III Análisis del Rendimiento 125 8. Evaluación del Sistema 8.1. Utilidad mpptest . . . . . . . . . . . . . . . . . . . . . . . 8.1.1. Compilación y Ejecución . . . . . . . . . . . . . . 8.1.2. Formatos de Salida . . . . . . . . . . . . . . . . . 8.1.3. Visualización . . . . . . . . . . . . . . . . . . . . 8.1.4. Gráficos . . . . . . . . . . . . . . . . . . . . . . . 8.1.5. Operaciones . . . . . . . . . . . . . . . . . . . . . 8.2. Pruebas Realizadas . . . . . . . . . . . . . . . . . . . . . 8.2.1. Comunicación Bloqueante . . . . . . . . . . . . . 8.2.2. Comunicación No Bloqueante . . . . . . . . . . . 8.2.3. Participación Total de los Procesos . . . . . . . . . 8.2.4. Solapamiento entre Comunicación y Procesamiento 8.2.5. Comunicación Colectiva . . . . . . . . . . . . . . 8.2.6. Fiabilidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 127 128 128 129 129 130 130 131 132 133 135 136 140 9. Evaluación de los Algoritmos 9.1. Herramientas de Monitorización . . . . . . . . . . . . . . . . . . . . . . 9.1.1. Ficheros de Recorrido . . . . . . . . . . . . . . . . . . . . . . . 9.1.2. Trazado de la Ejecución . . . . . . . . . . . . . . . . . . . . . . 9.1.3. Animación en Tiempo Real . . . . . . . . . . . . . . . . . . . . 9.2. Criterios de Evaluación . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.2.1. Tiempo de Ejecución . . . . . . . . . . . . . . . . . . . . . . . . 9.2.2. Número de Procesadores . . . . . . . . . . . . . . . . . . . . . . 9.2.3. Coste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.3. Resultados Obtenidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.3.1. Algoritmo Cálculo de Áreas mediante Montecarlo Bloqueante . . 9.3.2. Algoritmo Cálculo de Áreas mediante Montecarlo No Bloqueante 9.3.3. Algoritmo Regla del Trapecio . . . . . . . . . . . . . . . . . . . 9.3.4. Algoritmo Multiplicación de Matrices de Fox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 143 144 146 147 148 148 149 149 149 149 151 154 159 . . . . . . . . 163 164 165 165 166 166 167 168 170 A. Instalación, Configuración y Manejo de MPICH A.1. Dispositivos . . . . . . . . . . . . . . . . . . A.2. Obtención . . . . . . . . . . . . . . . . . . . A.3. Compilación e Instalación . . . . . . . . . . A.4. Configuración . . . . . . . . . . . . . . . . . A.4.1. El Fichero de Máquinas . . . . . . . A.4.2. RSH . . . . . . . . . . . . . . . . . . A.4.3. SSH . . . . . . . . . . . . . . . . . A.4.4. Secure Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ÍNDICE GENERAL 8 A.5. Compilación y Enlace de Programas . . . . . . . . . . . . . . . . . . . . . . 171 A.6. Ejecución de Programas con mpirun . . . . . . . . . . . . . . . . . . . . . . 172 A.7. Extensión MPE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 B. Manual de Referencia B.1. MPI_Bcast . . . . . . . . B.2. MPI_Cart_coords . . . . . B.3. MPI_Cart_create . . . . . B.4. MPI_Cart_rank . . . . . . B.5. MPI_Cart_sub . . . . . . . B.6. MPI_Comm_create . . . . B.7. MPI_Comm_group . . . . B.8. MPI_Comm_rank . . . . . B.9. MPI_Comm_size . . . . . B.10. MPI_Comm_split . . . . . B.11. MPI_Finalize . . . . . . . B.12. MPI_Get_processor_name B.13. MPI_Group_incl . . . . . B.14. MPI_Init . . . . . . . . . . B.15. MPI_Irecv . . . . . . . . . B.16. MPI_Isend . . . . . . . . . B.17. MPI_Recv . . . . . . . . . B.18. MPI_Reduce . . . . . . . B.19. MPI_Send . . . . . . . . . B.20. MPI_Sendrecv_replace . . B.21. MPI_Type_commit . . . . B.22. MPI_Type_struct . . . . . B.23. MPI_Type_vector . . . . . B.24. MPI_Wait . . . . . . . . . B.25. MPI_Waitall . . . . . . . . B.26. MPI_Waitany . . . . . . . B.27. MPI_Wtime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 Conclusiones 203 Bibliografía 208 Direcciones URL 209 Índice de figuras 1.1. 1.2. 1.3. 1.4. 1.5. 1.6. 1.7. 1.8. 1.9. 1.10. 1.11. 1.12. 1.13. 1.14. 1.15. 1.16. 1.17. 1.18. 1.19. . . . . . . . . . . . . . . . . . . . 6 7 9 10 11 12 16 19 20 21 22 27 27 27 28 30 37 40 41 2.1. Modelo Básico para Paso de Mensajes . . . . . . . . . . . . . . . . . . . . . 2.2. Modelo Básico para Memoria Compartida . . . . . . . . . . . . . . . . . . . 2.3. Esquema Básico Utilización MPI . . . . . . . . . . . . . . . . . . . . . . . . 44 47 51 3.1. Logo MPI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 5.1. Generación Puntos Aleatorios . . . . . . . . . . . . . . . . . . . . . . . . . 5.2. Cálculo Hipotenusa Punto Aleatorio . . . . . . . . . . . . . . . . . . . . . . 74 75 6.1. Procesos estructurados en árbol . . . . . . . . . . . . . . . . . . . . . . . . . 95 8.1. 8.2. 8.3. 8.4. 8.5. Clasificación Paralelismo Implícito . . . . . . . . . . Clasificación Paralelismo Explícito . . . . . . . . . . Ciclo de Instrucción de 2 etapas (A) . . . . . . . . . Ciclo de Instrucción de 10 etapas . . . . . . . . . . . Ciclo de Instrucción de 2 etapas (B) . . . . . . . . . Cauce de 6 etapas . . . . . . . . . . . . . . . . . . . Procesamiento Digital de Señales . . . . . . . . . . . Esquema Multiprocesador . . . . . . . . . . . . . . Organización Básica Bus de Tiempo Compartido . . Organización Bus de Tiempo Compartido con Caché Organización Memorias Multipuerto . . . . . . . . . Topología Anillo . . . . . . . . . . . . . . . . . . . Topología Malla . . . . . . . . . . . . . . . . . . . . Topología Árbol . . . . . . . . . . . . . . . . . . . . Topología Hipercubo . . . . . . . . . . . . . . . . . Objetivo Procesamiento Paralelo en Clusters . . . . . Esquema Procesador Matricial . . . . . . . . . . . . Esquema Procesador Vectorial . . . . . . . . . . . . Cray SX-6 . . . . . . . . . . . . . . . . . . . . . . . Gráfico Comunicación Bloqueante . . . . . . . . . Comparación Com.Bloqueante - No Bloqueante . . Comparación Com. Participativa - No Participativa Comparación Com. Solapada - No Solapada . . . . Gráfico Comunicación Colectiva . . . . . . . . . . 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 134 135 137 139 ÍNDICE DE FIGURAS 10 9.1. 9.2. 9.3. 9.4. 9.5. 9.6. 9.7. 9.8. 9.9. 9.10. 9.11. 9.12. 9.13. Animación en Tiempo Real . . . . . . . . . . . . . . . . . . . Modelo Ejecución Cálculo de Áreas Bloqueante 8 Procesos . . Gráfico Tiempo Ejecución Cálculo de Áreas Bloqueante . . . Gráfico Coste Cálculo de Áreas Bloqueante . . . . . . . . . . Modelo Ejecución Cálculo de Áreas No Bloqueante 8 Procesos Gráfico Tiempo Ejecución Cálculo de Áreas No Bloqueante . Gráfico Coste Cálculo de Áreas No Bloqueante . . . . . . . . Modelo Ejecución Regla del Trapecio 8 Procesos . . . . . . . Gráfico Tiempo Ejecución Regla del Trapecio . . . . . . . . . Gráfico Coste Regla del Trapecio . . . . . . . . . . . . . . . . Comparación Tiempos de distintas Cantidades de Procesos . . Comparación Costes de distintas Cantidades de Procesos . . . Modelo Ejecución Algoritmo de Fox 9 Procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 150 152 152 153 155 155 156 158 158 160 160 161 Índice de cuadros 1.1. Diámetros Topologías Hipercubo y Malla . . . . . . . . . . . . . . . . . . . 1.2. Ejemplos Computadores Vectoriales . . . . . . . . . . . . . . . . . . . . . . 28 42 5.1. Tipos de Datos MPI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 6.1. Operaciones de Reducción . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 7.1. Partición Matriz 6*6 en 9 Procesos . . . . . . . . . . . . . . . . . . . . . . . 106 7.2. Topología Física 3*3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 9.1. 9.2. 9.3. 9.4. Evaluación Cálculo de Áreas Bloqueante . . . Evaluación Cálculo de Áreas No Bloqueante . Evaluación Regla del Trapecio . . . . . . . . Evaluación Multiplicación de Matrices de Fox 11 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 154 157 159 12 ÍNDICE DE CUADROS Índice de algoritmos 4.1. 5.1. 5.2. 6.1. 7.1. ¡Hola Mundo! . . . . . . . . . . . . . . . . . . . . . . . Cálculo de Áreas mediante Montecarlo (Bloqueante) . . Cálculo de Áreas mediante Montecarlo (No Bloqueante) Regla del Trapecio . . . . . . . . . . . . . . . . . . . . Multiplicación de Matrices de Fox . . . . . . . . . . . . 13 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 . 81 . 86 . 98 . 114 14 ÍNDICE DE ALGORITMOS Objetivos El objetivo principal del presente proyecto de investigación consiste en demostrar la utilidad y potencialidad del procesamiento paralelo, concretamente en lo relativo a su aplicación en clusters (redes de computadores). Debemos probar con resultados reales y análisis gráficos la mejora en el rendimiento que produce la ejecución paralela de determinados algoritmos. Figura 1: Objetivo Procesamiento Paralelo en Clusters Con el objeto de delimitar el campo de estudio lo primero que debemos hacer es situar el paralelismo basado en redes dentro de la amplia variedad de sistemas dedicados al procesamiento paralelo. Propondremos una clasificación para los distintos tipos de paralelismo existentes en la actualidad, y explicaremos las características principales de cada uno de ellos. Un aspecto básico para el desarrollo de la investigación consiste en diseñar e implemetar un conjunto de algoritmos paralelos que pongan en práctica los tipos de comunicación más importantes que se dan en el procesamiento paralelo basado en redes. Para lograr este objetivo estudiaremos las caracteríticas más importantes de MPI (“Message Passing Interface”, Interfaz de Paso de Mensajes) y emplearemos dicha interfaz en el desarrollo de diversos algoritmos paralelos. Por último haremos un estudio detallado de los resultados obtenidos en las ejecuciones de los algoritmos desarrollados, explicando paso a paso la manera en la que han sido realizados. I II OBJETIVOS Dichos resultados serán utilizados para la generación de gráficos que nos ayuden a entender las importantes ventajas que nos aporta el procesamiento paralelo. Desarrollo A continuación explicaremos detenidamente el desarrollo de la investigación llevada a cabo para la realización del presente proyecto. Dicho desarrollo coincide en la práctica con la estructura del documento realizado. Sistemas de Procesamiento Paralelo En primer lugar debemos llevar a cabo una visión general del procesamiento paralelo, para así poder valorar la importancia de este amplio campo de estudio. En el capítulo 1 se estudian los conceptos básicos relacionados con este modelo de procesamiento, y proponemos una clasificación para los distintos tipos de paralelismo existentes en la actualidad, explicando las características principales de cada uno de ellos. Nuestra clasificación divide el paralelismo de los computadores en dos grandes ramas: Paralelismo implícito o de bajo nivel Paralelismo explícito o de alto nivel La técnicas de paralelismo implícito están dirigidas al reforzamiento del nivel de concurrencia dentro de la CPU, de manera que queda oculta a la arquitectura del ordenador. Por lo tanto pueden ser vistos como sistemas de un sólo procesador. Paralelismo Implícito Segmentación Múltiples ALUs Procesadores Auxiliares Figura 1: Clasificación Paralelismo Implícito El paralelismo explícito o de alto nivel hace referencia a aquellos sistemas en los cuales se interconectan varios procesadores para cooperar en la ejecución de los programas de aplicación. Se trata de sistemas que ofrecen una infraestructura explícita para el desarrollo del software del sistema y aplicaciones que exploten el paralelismo. A este tipo de paralelismo dedicaremos el resto de la investigación, centrándonos concretamente en las arquitecturas MIMD con memoria distribuida. III DESARROLLO IV Paralelismo Explícito MISD SIMD (Procesadores Vectoriales) (Procesadores Matriciales) Memoria Compartida Multiprocesadores (SMP) MIMD Memoria Distribuida Multicomputadores (Cluster) Figura 2: Clasificación Paralelismo Explícito Una vez resuelto el sistema de intercomunicación física de los equipos, deben abordarse los mecanismos para lograr la coordinación entre los procesadores. En el capítulo 2 analizamos las características básicas de los dos modelos de interacción entre procesadores (paso de mensajes y memoria compartida), para luego centrarnos en las utilidades de desarrollo de software paralelo más populares que existen en la actualidad: PVM, MPI, p4, Express y Lynda. Guía MPI La segunda parte de nuestro estudio se centra en la utilización de la interfaz MPI (“Message Passing Interface”, Interfaz de Paso de Mensajes) para el desarrollo de aplicaciones paralelas. El estudio de la interfaz MPI comienza en el capítulo 3 con una explicación sobre el origen y la motivación que promovieron la creación del estándar. Por otro lado analizamos los contenidos del estándar, sus objetivos, su funcionamiento básico y sus distintas versiones. Como vemos en la figura 3, MPI es implementado normalmente como interfaz de comunicaciones, utilizando las facilidades ofrecidas por el sistema que vayamos a usar (comunicación vía sockets, operaciones de memoria compartida, etc). MPI_Send() MPI_Recv() MPI MPI SO SO Figura 3: Esquema Funcionamiento MPI A continuación en el capítulo 4 vemos los conceptos básicos sobre la utilización de MPI para el desarrollo de programas escritos en C. Todos los programas MPI comparten una serie de características. La inicialización y finalización del entorno de ejecución se llevan a cabo mediante funciones que explicaremos en este capítulo. También analizamos los métodos para V identificar los procesos en ejecución. Para ejemplificarlo escribiremos un sencillo programa en C que hace uso de funciones MPI para ejecutar una versión paralela del conocido algoritmo ¡Hola Mundo!. El capítulo 5 aborda la función más importante y característica del estándar MPI: el paso de mensajes. Se utiliza básicamente para el intercambio de datos entre los procesos en ejecución. En este capítulo estudiamos las características más importantes de los mensajes bloqueantes y no bloqueantes, aplicando dicha teoría a la implementación de un algoritmo diseñado para el cálculo de áreas circulares. El algoritmo Cálculo de Áreas mediante Montecarlo realiza una aproximación del área de una circunferencia con un radio determinado mediante la simulación de variables aleatorias, empleando un método estadístico (figura 4). Figura 4: Algoritmo Cálculo de Áreas mediante Montecarlo Las operaciones de comunicación colectiva son aquellas que se aplican al mismo tiempo a todos los procesos pertenecientes a un comunicador. Tienen una gran importancia en el estándar MPI, debido a la claridad de su sintaxis y a su eficiencia. En el capítulo 6 analizamos su utilidad y conveniencia implementando el algoritmo Regla del Trapecio de manera que haga un uso inteligente de dichas operaciones. El capítulo 7 aborda el uso de comunicadores y topologías. Esta característica hace a MPI diferente de la mayoría de los demás sistemas de paso de mensajes. En pocas palabras, un comunicador es una colección de procesos que pueden mandarse mensajes entre ellos. Una topología es una estructura impuesta en los procesos de un comunicador que permite a los procesos ser direccionados de diferentes maneras. Para ilustrar estas ideas desarrollaremos el código que implementa el algoritmo de Fox para multiplicar dos matrices cuadradas mediante su subdivisión en una serie de procesos (cuadro 1). Análisis del Rendimiento Todo el trabajo anteriormente realizado no tendría ningún sentido si no viniera avalado por unos resultados satisfactorios en la ejecución de algoritmos paralelos. Para conseguir una cierta objetividad en el análisis de los algoritmos ejecutados en un DESARROLLO VI Proceso 0 Proceso 3 Proceso 6 Proceso 1 Proceso 4 Proceso 7 Proceso 2 Proceso 5 Proceso 8 Cuadro 1: Subdivisión de Matrices Algoritmo de Fox determinado sistema paralelo, lo primero que debemos hacer es medir el rendimiento de dicho sistema. Dicha medición se realiza a través de herramientas especializadas en el análisis, de modo que podemos saber fácilmente y con seguridad cuánto de apropiado es el sistema para la ejecución de determinados algoritmos paralelos. En las figuras 6 y 7 mostramos el rendimiento del sistema en la ejecución de paso de mensajes bloqueantes y operaciones de comunicación colectiva, respectivamente. Comunicacion Bloqueante 1200 Com.Bloqueante 1000 Tiempo (micro-seg) 800 600 400 200 0 0 200 400 600 800 Tamano (bytes) Figura 6: Gráfico Comunicación Bloqueante 1000 1200 VII Comunicacion Colectiva Op.Reduccion-Entero 160000 32 256 512 1024 140000 Tiempo (micro-seg) 120000 100000 80000 60000 40000 20000 0 0 2 4 6 8 10 12 14 16 Num.Procesos Figura 7: Gráfico Comunicación Colectiva El capítulo 9 aborda el estudio de los resultados obtenidos en las ejecuciones explicando paso a paso la manera en la que han sido cosechados. Dichos resultados los analizaremos más detenidamente en el apartado de conclusiones, ya que nos ayudan a demostrar el éxito del presente proyecto de investigación. VIII DESARROLLO Parte I Sistemas de Procesamiento Paralelo 1 Capítulo 1 Introducción Definimos como procesamiento paralelo al concepto de agilizar la ejecución de un programa mediante su división en fragmentos que pueden ser ejecutados simultáneamente, cada uno en un procesador. De este modo un programa que se ejecute en procesadores podría ejecutarse veces más rápido que usando un solo procesador, al menos en teoría... 1.1. Utilidades del Procesamiento Paralelo Aunque el uso de múltiples procesadores puede agilizar muchas operaciones, la mayoría de las aplicaciones no están diseñadas para aprovechar los beneficios del procesamiento paralelo. Básicamente el procesamiento paralelo es apropiado para: Aplicaciones con suficiente paralelismo como para hacer bueno el uso de múltiples procesadores. El problema radica en identificar las porciones de programa que puedan ser ejecutadas independiente y simultáneamente en procesadores separados; ésto en realidad es complejo, ya que encontraremos aplicaciones que podrían ser ejecutadas en paralelo y sin embargo se ralentizan al ser paralelizadas en un sistema particular. Por ejemplo, un programa que tarda 4 segundos en ser ejecutado en una sola máquina podría tardar 1 segundo de procesamiento en cada uno de los cuatro procesadores disponibles en una red, pero no lograríamos nada si la coordinación entre dichos procesadores tardase más de 3 segundos. Implementaciones de algoritmos que o bien ya son paralelos (escritos para obtener las ventajas del procesamiento paralelo) o bien esperamos paralelizar nosotros mismos, codificando de nuevo al menos alguna de sus partes. Si nos encontramos en alguno de estos casos veremos que el procesamiento paralelo puede proporcionarnos el rendimiento de un supercomputador, aplicado a algunos programas que realizan complejas operaciones u operan en grandes bloques de datos. Y lo que es más, ello se puede lograr con hardware relativamente barato. Además es posible utilizar dichos sistemas paralelos para la realización de otros trabajos cuando no estén ocupados con una tarea en paralelo. 3 4 CAPÍTULO 1. INTRODUCCIÓN Si no es el procesamiento paralelo lo que se busca, pero queremos mejorar el rendimiento de nuestra aplicación, se pueden hacer multitud de cosas. En aplicaciones secuenciales podemos usar un procesador más rápido, añadir memoria al sistema, etc. 1.2. Definiciones Básicas Aunque el procesamiento paralelo haya sido utilizado durante muchos años en una gran variedad de sistemas, todavía no es familiar para la mayoría de los usuarios. Antes de comenzar la discusión de las varias alternativas existentes para implementar el procesamiento paralelo, es importante precisar el significado de algunos conceptos fundamentales. 1.2.1. Características Físicas de la Comunicación Velocidad de Transferencia de Datos En el campo de las redes de comunicación el intercambio de información entre ordenadores se realiza generalmente mediante transmisiones analógicas. En una transmisión analógica el ancho de banda se define como la diferencia máxima entre la frecuencia más alta y más baja de la señal sobre la que se transporta la información. Existe una relación directa entre el ancho de banda y la velocidad de transmisión: cuanto mayor es el ancho de banda de un sistema de transmisión, mayor es la velocidad a la que se pueden transmitir los datos en el medio. Podemos definir la velocidad de modulación de una señal analógica como el número de veces por segundo que la señal cambia su valor en la línea o medio de transmisión. Esta velocidad se mide en baudios. El número de baudios determina la cantidad de cambios de estado por segundo que se producen en una transmisión. Cuantos más estados, más cantidad de bits por segundo se podrán transmitir. Definimos la velocidad de transmisión como el número de bits transmitidos por segundo. Su unidad es el bps (bits por segundo). En general si el número de estados posibles de la línea de comunicación es , a cada estado le corresponderán "! bits de información. Imaginemos que en una red telefónica que funciona a 2400 baudios podemos transmitir 4 bits en cada baudio. En ese caso la velocidad de transmisión sería de 2400 baudios x 4 (bits/baudio) = 9600 bits por segundo. En la transmisión de información digital entre computadores es fundamental que aseguremos intercambios de datos libres de errores. El coste de ésto estriba en que a la propia información a transmitir se le deben añadir otras informaciones adicionales para detección/corrección de errores, para establecer y controlar la comunicación, etc. Aparece aquí un nuevo concepto de velocidad que llamaremos velocidad de transferencia de datos, y que representa la cantidad de información útil que puede transmitirse por unidad de tiempo. Latencia En un sistema de comunicación llamamos latencia al mínimo tiempo empleado en transmitir un dato, incluyendo la información de control necesaria para enviarlo o recibirlo. La latencia es muy importante en procesamiento paralelo porque determina el tamaño granular, es decir, 1.2. DEFINICIONES BÁSICAS 5 lo mínimo que debe tardar la ejecución de un segmento de código para que su ejecución en paralelo sea rentable, hablando en términos de rendimiento computacional. Básicamente si un segmento de código se ejecuta en menos tiempo del que se emplea en transmitir su resultado (latencia), entonces será más rápido ejecutar dicho segmento de código de manera secuencial en el procesador que necesita dicho resultado; de este modo la ejecución secuencial evitaría la sobrecarga en la comunicación. 1.2.2. Estándares Relacionados IA32 El modelo IA32 (“Intel Architecture, 32-bit”) es una especificación para microprocesadores que define su arquitectura y funcionalidad, o lo que es lo mismo, su comportamiento desde el punto de vista del software. Dicha especificación define entre otros elementos las siguientes características de los microprocesadores: Los modos de operación La organización de la memoria La estructura de los registros El direccionamiento de los operandos Los tipos de datos El conjunto de instrucciones Interrupciones y excepciones Todos los microprocesadores Intel x86 (a partir del 80386) siguen el modelo IA32, incluidos los más recientes: Pentium, P6, Pentium 4, Pentium M y Xeon. AMD y Cyrix también desarrollan multitud de procesadores compatibles con la arquitectura IA32. Dado que Linux se desarrolló en principio en procesadores IA32, y dado que ésta fue la manera en que se centró en el mercado, es conveniente usar dicha denominación IA32 para distinguir este tipo de procesadores de los PowerPC, Alpha, PA-RISC, MIPS, SPARC, etc. RAID Como en otras áreas de rendimiento en los computadores, los diseñadores de memorias de disco reconocen que si uno de los componentes sólo puede alcanzar un determinado límite, se puede conseguir una ganancia en prestaciones adicional usando varios de esos componentes en paralelo. En el caso de las memorias de disco, esto conduce al desarrollo de conjuntos de discos que operen independientemente y en paralelo. Siguiendo este principio, el esquema RAID (“Redundant Array of Independent Disks”, Conjunto Redundante de Discos Independientes) es una tecnología sencilla para mejorar tanto el ancho de banda como la fiabilidad de la E/S a disco. Consta de 6 niveles independientes, desde 0 hasta 5. Estos niveles no implican una relación jerárquica, pero designan diseños de arquitecturas diferentes que poseen tres características comunes: CAPÍTULO 1. INTRODUCCIÓN 6 Paralelismo Implícito Segmentación Múltiples ALUs Procesadores Auxiliares Figura 1.1: Clasificación Paralelismo Implícito 1. RAID es un conjunto de unidades físicas de disco vistas por el sistema operativo como una única unidad lógica. 2. Los datos se distribuyen a través de las unidades físicas de un conjunto. 3. La capacidad de los discos redundantes se usa para almacenar información de paridad, que garantice la recuperación de los datos en caso de fallo de disco. Los detalles de la característica segunda y tercera cambian según los distintos niveles de RAID. RAID 0 no soporta la tercera característica. Linux soporta los estándares RAID 0, 1, 4 y 5 además de hardware RAID especializado. 1.3. Clasificación Sistemas Paralelos En general, se puede dividir el paralelismo de los computadores en dos grandes ramas: Paralelismo implícito o de bajo nivel Paralelismo explícito o de alto nivel 1.3.1. Paralelismo Implícito o de Bajo Nivel La técnicas de paralelismo implícito están dirigidas al reforzamiento del nivel de concurrencia dentro de la CPU, de manera que queda oculta a la arquitectura del ordenador. Por lo tanto pueden ser vistos como sistemas de un sólo procesador. Como ejemplos de paralelismo de bajo nivel están: Segmentación o pipeline: La ejecución de cada instrucción se divide en una secuencia de etapas, de forma que varias instrucciones pueden ejecutarse en paralelo, cada una en una etapa distinta de su ejecución. Múltiples Unidades Funcionales en el Procesador: La repetición de ALUs permite una aproximación superescalar, con la ejecución paralela de varias instrucciones, todas en la misma etapa de su ejecución. Un procesador superescalar es aquel en el que las instrucciones comunes (aritmética entera y en punto flotante, cargas, almacenamientos y bifurcaciones condicionales) pueden iniciar su ejecución simultáneamente y ejecutarse de manera independiente. Estas implementaciones plantean problemas complejos de 1.3. CLASIFICACIÓN SISTEMAS PARALELOS 7 Paralelismo Explícito MISD SIMD (Procesadores Vectoriales) (Procesadores Matriciales) Memoria Compartida Multiprocesadores (SMP) MIMD Memoria Distribuida Multicomputadores (Cluster) Figura 1.2: Clasificación Paralelismo Explícito diseño relacionados con el cauce de instrucciones. En esta línea, la tecnología SWAR consiste en agilizar las operaciones en registros de enteros mediante la división de dichos registros en una serie de campos de igual longitud, asignando una ALU a cada uno de los campos y realizando la ejecución paralelamente. En este caso se ejecuta la misma instrucción sobre distintos datos en paralelo. Procesadores Auxiliares (“Attached Processors”): Los procesadores auxiliares son esencialmente computadores de propósito específico que se conectan a un sistema host o servidor para acelerar determinados tipos de cómputo. El ejemplo más común es el uso de procesadores de E/S, que liberan a la CPU de la responsabilidad del control detallado de las operaciones de E/S. 1.3.2. Paralelismo Explícito o de Alto Nivel El paralelismo explícito o de alto nivel hace referencia a aquellos sistemas en los cuales se interconectan varios procesadores para cooperar en la ejecución de los programas de aplicación. Se trata de sistemas que ofrecen una infraestructura explícita para el desarrollo del software del sistema y aplicaciones que exploten el paralelismo. La forma más común de clasificar los sistemas de procesadores paralelos fue la introducida por Flynn, la cual establece las siguientes categorías de computadores: SISD (“Single Instruction Single Data”, Flujo de instrucciones único y flujo de datos único): Un procesador interpreta una secuencia de instrucciones para operar con los datos almacenados en una memoria. De este modo en cualquier momento sólo se está ejecutando una única instrucción. Esta categoría responde a la arquitectura de Von Neumann, también llamados computadores serie escalares. SIMD (“Single Instruction Multiple Data”, Flujo de instrucciones único y flujo de datos múltiple): Una única instrucción es aplicada sobre diferentes datos al mismo tiempo. En las máquinas de este tipo un cierto número de elementos procesadores son controlados y sincronizados mediante una unidad de control. Cada elemento procesador tiene una memoria asociada, de manera que cada instrucción es ejecutada simultáneamente por 8 CAPÍTULO 1. INTRODUCCIÓN todos los elementos procesadores pero sobre un conjunto de datos diferentes. Debido a su utilidad en el procesamiento de matrices, a este tipo de máquinas se les llama procesadores matriciales. MISD (“Multiple Instruction Single Data”, Flujo de instrucciones múltiple y flujo de datos único): Varias instrucciones actúan simultáneamente sobre un único trozo de datos. Este tipo de máquinas se pueden interpretar de dos maneras. En principio, podemos considerarlas como máquinas formadas por varias unidades de procesamiento, las cuales reciben instrucciones distintas que operan sobre los mismos datos. Esta clase de arquitectura ha sido clasificada por numerosos arquitectos de computadores como impracticable o imposible, y en estos momentos no existen ejemplos que funcionen siguiendo este modelo. Otra forma de interpretar el modelo MISD es considerarlo como una clase de máquinas donde un mismo flujo de datos fluye a través de numerosas unidades de procesamiento. Arquitecturas altamente segmentadas, como la que poseen los arrays sistólicos o los procesadores vectoriales, son clasificadas a menudo bajo esta categoría. Estas arquitecturas realizan el procesamiento vectorial a través de una serie de etapas, cada una ejecutando una función particular produciendo un resultado intermedio. La razón por la cual dichas arquitecturas son clasificadas como MISD es que los elementos de un vector pueden ser considerados como pertenecientes al mismo dato, y todas las etapas del cauce representan múltiples instrucciones que son aplicadas sobre ese vector. MIMD (“Multiple Instruction Multiple Data”, Flujo de instrucciones múltiple y flujo de datos múltiple): Un conjunto de unidades de procesamiento ejecuta simultáneamente diferentes secuencias de instrucciones sobre conjuntos de datos diferentes. Es la organización que poseen los sistemas multiprocesadores y multicomputadores en general. SPMD es una versión restringida de MIMD en la cual todos los procesadores ejecutan el mismo programa. Al contrario que SIMD, cada procesador ejecuta una secuencia de instrucciones diferente. En la organización MIMD, los procesadores son de uso general, puesto que deben ser capaces de procesar todas las instrucciones necesarias para realizar las transformaciones apropiadas de los datos. Las arquitecturas MIMD se pueden subdividir además según la forma que tienen los procesadores para comunicarse (figura 1.2). Si los procesadores comparten una memoria común, entonces cada procesador accede a los programas y datos almacenados en la memoria compartida, y los procesadores se comunican unos con otros a través de esa memoria. Este tipo de sistemas se conocen como multiprocesadores fuertemente acoplados o simplemente multiprocesadores. Si cada procesador tiene una memoria dedicada, entonces cada elemento de proceso es en sí mismo un computador. La comunicación entre los computadores se realiza a través de caminos fijos o mediante algún mecanismo de conmutación de mensajes. Este tipo de sistemas se conocen como multiprocesadores débilmente acoplados o multicomputadores. 1.4. ARQUITECTURAS BASADAS EN PARALELISMO IMPLÍCITO Istrucción Captación Istrucción Ejecución 9 Resultado Figura 1.3: Ciclo de Instrucción de 2 etapas (A) 1.4. Arquitecturas Basadas en Paralelismo Implícito 1.4.1. Segmentación o pipeline Desde 1960 esta arquitectura ha recibido una gran atención debido a la considerable mejora que ha producido en la velocidad de proceso sin que ello aumente, en la misma medida, el coste del sistema. La segmentación de instrucciones es similar al uso de una cadena de montaje en una fábrica. Una cadena de montaje saca partido al hecho de que el producto pasa a través de varias etapas de producción. De esta manera se puede trabajar sobre los productos en varias etapas simultáneamente. A este proceso se le llama segmentación (“pipelining”) porque, como en una tubería o cauce (“pipeline”), en un extremo se aceptan nuevas entradas antes de que algunas entradas aceptadas anteriormente aparezcan como salidas en el otro extremo. Para aplicar este concepto a la ejecución de instrucciones debemos darnos cuenta de que en realidad una instrucción tiene varias etapas. La figura 1.4 muestra, por ejemplo, una partición del ciclo de instrucción en 10 etapas que tienen lugar secuencialmente. Claramente puede pensarse en la utilización de la segmentación. Como una aproximación sencilla consideremos la subdivisión del procesamiento de una instrucción en dos etapas: captación de instrucción y ejecución de instrucción. Hay períodos en la ejecución de una instrucción en los que no se accede a memoria principal. Este tiempo podría utilizarse para captar la siguiente instrucción en paralelo con la ejecución de la actual. La figura 1.3 representa esta aproximación. El cauce tiene dos etapas independientes. La primera etapa capta una instrucción y la almacena en un buffer. Cuando la segunda etapa está libre la primera le pasa la instrucción almacenada. Mientras que la segunda etapa ejecuta la instrucción, la primera etapa utiliza algún ciclo de memoria no usado para captar y almacenar la siguiente instrucción. A ésto se le llama prebúsqueda o precaptación de la instrucción (“instruction prefetch”) o solapamiento de la captación (“fetch overlap”). Debería estar claro que ese proceso acelerará la ejecución de instrucciones. 10 Captación de instrucción Solicitud de instrucción Cálculo de la dirección de la instrucción Indirección Indirección Captación del operando Almacenamiento del operando Solicitud de operando Decodificación de la operación de la instrucción Cálculo de la dirección del operando Más de un resultado Operación con los datos Cálculo de la dirección del operando Cadena o vector Figura 1.4: Ciclo de Instrucción de 10 etapas Chequeo de interrupciones No interrupción Interrupción CAPÍTULO 1. INTRODUCCIÓN Siguiente instrucción Más de un operando 1.4. ARQUITECTURAS BASADAS EN PARALELISMO IMPLÍCITO Esperar Istrucción Nueva dirección Captación Istrucción 11 Esperar Ejecución Resultado Descartar Figura 1.5: Ciclo de Instrucción de 2 etapas (B) Si las etapas de captación y ejecución fueran de igual duración, el tiempo de ciclo de instrucción se reduciría a la mitad. Sin embargo si miramos más atentamente el cauce de la figura 1.5 veremos que la duplicación de la velocidad de ejecución es poco probable por dos razones: 1. El tiempo de ejecución será generalmente más largo que el tiempo de captación. La ejecución implicará la lectura y almacenamiento de los operandos y la realización de alguna operación. De este modo la etapa de captación puede tener que esperar algún tiempo antes de que pueda vaciar su buffer. 2. Una instrucción de bifurcación condicional hace que la dirección de la siguiente instrucción a captar sea desconocida. De este modo, la etapa de captación debe esperar hasta que reciba la dirección de la siguiente instrucción desde la etapa de ejecución. La etapa de ejecución puede entonces tener que esperar mientras se capta la siguiente instrucción. La pérdida de tiempo debida a la segunda razón puede reducirse haciendo una estimación. Una regla simple es la siguiente: cuando una instrucción de bifurcación condicional pasa de la etapa de captación a la de ejecución, la etapa de captación capta la instrucción de memoria que sigue a la instrucción de salto. Entonces si el salto no se produce no se pierde tiempo. Si el salto se produce debe desecharse la instrucción captada y captar una nueva instrucción. Aunque estos factores reduzcan la efectividad potencial del cauce de dos etapas, se produce alguna aceleración. Para conseguir una mayor aceleración, el cauce debe tener más etapas. Consideremos la siguiente descomposición del procesamiento de una instrucción: Captar Instrucción (“Fetch Instruction”, FI): Leer la supuesta siguiente instrucción en un buffer. Decodificar Instrucción (“Decode Instruction”, DI): Determinar el código de operación y los campos del operando. Calcular Operandos (“Calculate Operands”, CO): Calcular la dirección efectiva de cada operando fuente. Esto puede involucrar varios modos de direccionamiento: mediante desplazamiento, indirecto a través de registro, indirecto u otros. Captar Operandos (“Fetch Operands”, FO): Captar cada operando de memoria. Los operandos en registros no tienen que ser captados. CAPÍTULO 1. INTRODUCCIÓN 12 Instrucción 1 Instrucción 2 Instrucción 3 Instrucción 4 Instrucción 5 Instrucción 6 Instrucción 7 Instrucción 8 Instrucción 9 1 2 3 4 5 6 7 8 9 10 11 12 13 FI DI CO FO EO WO FI DI CO FO FI DI CO FO EO WO FI DI CO FO EO WO FI DI CO FO EO WO FI DI CO FO EO WO FI DI CO FO EO WO FI DI CO FO EO WO FI DI CO FO 14 EO WO EO WO Figura 1.6: Cauce de 6 etapas Ejecutar Instrucción (“Execute Instruction”, EI): Realizar la operación indicada y almacenar el resultado, si lo hay, en la posición de operando destino especificada. Escribir Operando (“Write Operand”, WO): Almacenar el resultado en memoria. Con esta descomposición las diversas etapas tienen casi igual duración. Por motivos de claridad asumamos que tienen igual duración. La figura 1.6 muestra cómo un cauce de 6 etapas puede reducir el tiempo de ejecución de 9 instrucciones de 54 a 14 unidades de tiempo. En realidad dicha simulación es demasiado idealista. Para empezar no todas las instrucciones recorren las 6 etapas del cauce; por ejemplo, una instrucción de carga no necesita la etapa WO. Ello complica el hardware del cauce. Además la mayoría de los sistemas no permiten varios accesos a memoria simultáneos, por lo que las etapas FI, FO y WO no podrían ejecutarse al mismo tiempo. Otros factores como la dificultad en la ejecución de la bifurcación condicional ya comentada, o la espera que se produce en ciertas etapas cuando no todas las etapas son de igual duración (lo cual es lo más común), complican el diseño y limitan la mejora de las prestaciones. Según toda esta discusión precedente, puede parecer que cuanto mayor sea el número de etapas en el cauce, más rápida será la velocidad de ejecución. Algunos diseñadores del IBM S/360 observaron dos factores que frustran este aparente sencillo patrón de diseño de altas prestaciones, y que siguen siendo ciertos hoy día: 1. En cada etapa, hay algún gasto extra debido a la transferencia de datos de buffer a buffer y a la realización de varias funciones de preparación y distribución. 1.4. ARQUITECTURAS BASADAS EN PARALELISMO IMPLÍCITO 2. 13 La cantidad de control lógico necesario para manejar dependencias de memoria y registros y para optimizar el uso del cauce aumenta enormemente con el número de etapas. En definitiva, la segmentación de instrucciones es una poderosa técnica para aumentar las prestaciones pero requiere un diseño cuidadoso si se quieren obtener resultados óptimos con una complejidad razonable. La emergencia de la segmentación y su temprana evolución se produjo en la primera línea de supercomputadores IBM. El modelo IBM 7030 (llamado computador “a toda mecha”) superó en 100 veces la velocidad del computador más rápido en producción de aquella época, el IBM 704. Dicho logro sólo se pudo alcanzar mediante la segmentación de instrucciones. Luego vinieron otros éxitos como el IBM 360/91, el procesador 6502, y un largo etc. Hoy día todas las compañías implementan es sus procesadores este tipo de arquitectura. 1.4.2. Tecnología SWAR SWAR (“SIMD Within A Register”, Paralelismo SIMD Dentro de un Registro) es el término genérico aplicado a un tipo de paralelismo interno al procesador. La idea consiste en agilizar las operaciones en registros de enteros mediante la división de dichos registros en una serie de campos de igual longitud, asignando una unidad de procesamiento (una ALU en definitiva) a cada uno de los campos y realizando la ejecución paralelamente. Dada una máquina con registros de # bits y ALUs, se puede lograr que sus operaciones sobre registros funcionen como operaciones paralelas SIMD en campos de #%$& bits. De todas formas sólo con el empuje de las tecnologías multimedia, que han conseguido acelerar las operaciones en un rango de 2x hasta 8x, se ha llegado a la popularidad e importancia de dicha tecnología. Ya en 1997 la mayoría de los microprocesadores incorporaban soporte de hardware para SWAR: Intel Pentium II & Pentium with MMX (“MultiMedia eXtensions”, Extensiones Multimedia) AMD K6 MMX (“MultiMedia eXtensions”, Extensiones Multimedia) Cyrix M2 MMX (“MultiMedia eXtensions”, Extensiones Multimedia) Digital Alpha MAX (“MultimediA eXtensions”, Extensiones Multimedia) HewlettPackard PARISC MAX (“Multimedia Acceleration eXtensions”, Extensiones de Aceleración Multimedia) Microunity Mediaprocessor SIGD (“Single Instruction on Groups of Data”, Instrucciones Únicas en Grupos de Datos) MDMX, pronunciado Mad Max (“MIPS Digital Media eXtension”, Extensiones Media Digital MIPS) Sun SPARC V9 VIS (“Visual Instruction Set”, Juego de Instrucciones Visuales) CAPÍTULO 1. INTRODUCCIÓN 14 A diferencia de lo que sucede con las tres firmas que aceptaron las primitivas MMX (Intel/AMD/Cyrix), todos los los demás conjuntos de instrucciones son difícilmente comparables, y mutuamente incompatibles. Con el paso del tiempo las MMX también evolucionaron, generandose nuevas extensiones como las EMMX de Cyrix, las 3DNow! de AMD o las SSE y SSE2 de Intel, entre otras. 1.4.2.1. Utilidades de SWAR Aunque todos los procesadores actuales son capaces de ejecutar al menos algún tipo de paralelismo basado en la tecnología SWAR, la realidad es que ni siquiera los juegos de instrucciones SWAR más desarrollados soportan paralelismo de carácter general. De hecho, muchos piensan que la diferencia de rendimiento entre un procesador Pentium y un Pentium MMX se debe más a factores como el mayor tamaño de la caché que a la aparición de las MMX. Así pues... ¿para qué es realmente buena la tecnología SWAR? Operaciones sobre datos de tipo entero, cuanto más pequeños mejor. Dos valores de 32 bits caben en un registro MMX de 64 bits, pero también lo hacen ocho caracteres de un byte o incluso un tablero de ajedrez con valores de un bit. Cuando un dato ocupa varios campos de un registro, determinadas operaciones (llamadas operaciones particionadas) se ven afectadas por interacciones entre los campos debido al acarreo/resto, etc. Por ello, cuanto menor sea el tamaño de dichos enteros, más limpias serán las operaciones y por lo tanto mejor aprovecharemos el parelismo generado por la repetición de ALUs. Nota: En los juegos de instrucciones más actuales, como lal EMMX, las 3DNow! o las SSE2, también se admiten operaciones sobre datos de tipo flotante. Paralelismo SIMD o de vectores, en el cual la misma operación es aplicada a todos los campos simultáneamente. Hay maneras de anular la operación en algunos campos seleccionados (enmascaramiento), pero ello complica el código y disminuye el rendimiento. Referencias a memoria localizadas y regulares. SWAR en general y MMX en particular no obtienen buenos resultados utilizando accesos aleatorios. Reunir con dichos accesos un vector x[y] (donde y es un índice de dicho vector) resulta demasiado costoso. Es cierto que todas éstas son restricciones muy duras, pero también es cierto que dicho tipo de paralelismo se da en muchos algoritmos paralelos (no sólo en aplicaciones multimedia). Enfocada al tipo de paralelismo adecuado, la tecnología SWAR puede ser más eficiente que utilizar SMPs o Clusters... y no cuesta nada utilizarla. 1.4.2.2. Soporte MMX Bajo Linux Bajo Linux los procesadores IA32 son nuestro principal interés. Lo bueno es que tanto los AMD, como los Cyrix y los Intel tienen una base común, las instrucciones MMX. Sin embargo el rendimiento de dichas instrucciones varía, y además, actualmente cada firma tiene sus propias extensiones; por lo tanto programar un soporte para dichas instrucciones puede resultar algo engorroso. Existen tres métodos para utilizar MMX en SWAR: 1.4. ARQUITECTURAS BASADAS EN PARALELISMO IMPLÍCITO 15 1. Utilizar una librería de funciones MMX ya desarrollada. En la Universidad de Purdue existe un grupo de trabajo (URL [15]) que ha desarrollado librerías adaptadas a varios juegos de instrucciones determinados. Así, tenemos la librería libmmx diseñada para las instrucciones MMX en general, la librería libxmmx para las Cyrix EMMX y la librería libsse para las Intel SSE. También existe una librería un tanto curiosa, MMX Emulator (URL [7]), diseñada para ejecutar programas que usen instrucciones MMX en procesadores que no tengan soporte para ellas. Así, afirman tener un 386 MMX... como sus realizadores apuntan, es lento pero funciona. 2. Usar las instrucciones MMX directamente. Esto es algo complicado por dos razones. El primer problema es la portabilidad: no podremos ejecutar un programa así implementado en procesadores que no tengan soporte para MMX. El segundo problema es que el ensamblador para IA32 que utilicemos en Linux podría no soportar dichas instrucciones MMX. 3. Utilizar un lenguage de alto nivel o un compilador que genere directamente las instrucciones MMX apropiadas. En este sentido, en la Universidad de Purdue (URL [15]) se ha desarrollado un lenguaje llamado SWARC y su compilador, el Scc, el cual está en permanente fase experimental, aunque disponible. Como sus autores señalan, Scc no pretende ser un producto de alta calidad en términos de eficiencia; en cambio proporciona una implementación básica del modelo SWAR. Por otro lado tenemos los compiladores de Intel (URL [4]) de C++ y Fortran para Linux, los cuales generan las instrucciones MMX deseadas de manera optimizada. 1.4.3. Procesadores Auxiliares Los procesadores auxiliares son esencialmente computadores de propósito específico que se conectan a un sistema host o servidor para acelerar determinados tipos de cómputo. Por ejemplo, muchas tarjetas de vídeo y audio para PCs contienen procesadores auxiliares diseñados, respectivamente, para acelerar operaciones sobre gráficos y audio. Aunque esta técnica ya no está en auge, es muy difícil que otros métodos de procesamiento paralelo alcancen su bajo coste y alto rendimiento. Una buena manera de sacarle partido a estos procesadores es utilizar un PC bajo Linux como servidor. El problema es que no existe mucho software de soporte disponible, por lo que en ocasiones tendremos que trabajar por nuestra cuenta desarrollándolo nosotros mismos. 1.4.3.1. Beneficios de los PCs bajo Linux como Servidor Antes de que nos desanime el hecho de trabajar por nuestra cuenta, es útil entender que, aunque pueda ser dificultoso hacer que un PC bajo Linux actúe como servidor de un determinado sistema, es una de las pocas plataformas adecuadas para este tipo de uso. Los PCs son buenos servidores por dos razones principalmente. La primera es su capacidad de ampliación, de manera fácil y barata. Recursos como la memoria, discos, redes, etc. son añadidos a un PC de manera trivial. La segunda es su facilidad de interconexión a otros dispositivos a través de sus interfaces estándar. No sólo están ampliamente disponibles los prototipos de los buses ISA y PCI si no que además disponemos del puerto paralelo, el cual CAPÍTULO 1. INTRODUCCIÓN 16 Figura 1.7: Procesamiento Digital de Señales ofrece un rendimiento razonable en un iterfaz independiente. Además la separación del espacio de E/S en los microprocesadores IA32 facilita la interconexión, proporcionando una protección hardware de las direcciones de E/S. Por otra parte Linux es un buen sistema operativo para crear un servidor. La disponibilidad de todo el código fuente y la extensa documentación existente son una ayuda tremenda, obviamente. Además Linux proporciona una planificación cercana al tiempo real, e incluso existe una versión de Linux en tiempo real desarrollada por FSMLabs (URL [3]). Pero lo que quizás es más importante es el hecho de que a la vez que proporciona un entorno UNIX completo, Linux soporta herramientas de desarrollo que están diseñadas para ser ejecutadas bajo Microsoft DOS o Windows. Los programas MSDOS pueden ser ejecutados en Linux usando dosemu, el cual proporciona una máquina virtual protegida que ejecuta MSDOS. El soporte de Linux para los programas diseñados para Windows 3.xx es aún más directo: software libre como wine. Este software (URL [21]) simula Windows 3.xx y Win32 lo suficientemente bien como para que la mayoría de los programas se ejecuten correcta y eficientemente en un entorno UNIX/X. Las siguientes dos secciones arrojan ejemplos de sistemas paralelos que utilizan esta arquitectura que podrían ser soportados bajo Linux. 1.4.3.2. Procesadores DSP Un DSP (“Digital Signal Processor”, Procesador de Señales Digitales) es un tipo de microprocesador muy rápido y potente. Un DSP procesa los datos en tiempo real. Esta capacidad de tiempo real hace de los DSPs perfectos para aplicaciones que no pueden tolerar demoras. Así, suelen emplearse en el Procesamiento Digital de Señales, un método de procesamiento de señales del mundo real que utiliza técnicas matemáticas para realizar transformaciones o extraer información, como se muestra en la figura 1.7. Aquí tenemos algunas de las ventajas de dichos procesadores: Operaciones de multiplicación y suma en un sólo ciclo Funcionamiento en tiempo real, simulación y emulación Flexibilidad Fiabilidad Aumenta rendimiento del sistema 1.4. ARQUITECTURAS BASADAS EN PARALELISMO IMPLÍCITO 17 Disminuye costes Existe un próspero mercado para estos procesadores. Aunque estos chips fueron generalmente diseñados para ser utilizados en sistemas de aplicación específica, también pueden ser utilizados como magníficos computadores paralelos auxiliares. Estos son los beneficios de utilizar dichos procesadores como computadores paralelos auxiliares: Muchos de ellos, como los Texas Instruments TMS320 y los Analog Devices SHARC DSP, están diseñados para construir máquinas paralelas con poca o ninguna lógica de enlace. Son económicos, especialmente por MIP o MFLOP. Incluyendo el coste de la lógica de soporte básica, suele decirse que su coste es una décima parte de lo que cuesta un procesador para PC con un rendimiento comparable. No utilizan mucha energía ni desprenden demasiado calor. Esto significa que es posible tener un grupo de estos chips alimentados por una fuente de alimentación convencional para PCs, y encerrarlos dentro del chasis del PC sin que se convierta en un horno. Existen algunos elementos en los juegos de instrucciones de los DSP que los compiladores de alto nivel no suelen usar de manera apropiada, como por ejemplo la operación “Bit Reverse Addressing”. Usando un sistema paralelo auxiliar, es posible compilar y ejecutar la mayoría del código en el servidor, ejecutando aquellos algoritmos que hacen un mayor consumo de tiempo en los DSPs. Dichos algoritmos pueden ser implementados mediante un código cuidadosamente adaptado. Los procesadores DSP no están diseñados para ejecutar un sistema operativo UNIX, y generalmente no son muy buenos como procesadores de propósito general. Por ejemplo, muchos de ellos no tienen hardware de manejo de memoria. En otras palabras, funcionan mejor como clientes de un servidor de propósito más general... como un PC bajo Linux. Muchas tarjetas de sonido y modems incluyen procesadores DSP que pueden ser accedidos mediante controladores de Linux sin ninguna dificultad. Lo difícil llega cuando utilizamos un sistema paralelo auxiliar que contenga cuatro o más procesadores DSP... y eso es exactamente lo que nos interesa. Existen dos familias de procesadores DSP utilizados ampliamente para procesamiento paralelo: Texas Instruments TMS320 Muy populares durante mucho tiempo, resulta muy sencillo construir un procesador paralelo basado en el TMS320. Existen versiones del TMS320 sólo para enteros y también para punto flotante; los diseños más antiguos usaban algún tipo inusual del formato punto flotante de precisión simple, pero los nuevos modelos soportan los formatos IEEE. El antiguo TMS320C4x consigue hasta 80 Mflops usando el formato punto flotante simple precisión específico de Texas Instruments; en constraste, el TMS320C67x proporciona hasta 1Gflop simple precisión o 420 Mflops doble precisión para los cálculos en formato punto flotante IEEE. No sólo es fácil configurar un grupo de estos chips como un multiprocesador, si no que además existen chips multiprocesadores, como el TMS320C8x que combina en paralelo un procesador RISC maestro con unidad de punto flotante IEEE a 100 Mflops con dos o cuatro DSPs esclavos con unidad de entero. 18 CAPÍTULO 1. INTRODUCCIÓN Analog Devices SHARC Estos chips pueden ser utilizados como un multiprocesador de 6 procesadores con memoria compartida y sin lógica de conexionado externa. Sistemas mayores pueden ser construidos usando 6 enlaces de 4 bits por chip. Muchos de los sistemas más grandes son empleados en aplicaciones militares, y resultan económicos. En esta línea la compañía Integrated Computing Engines, llamada en la actualidad Media100, ofrece una gama de tarjetas PCI llamadas GreeICE y BlueICE, ésta última más actual. La primera versión (GreenICE) contiene una matriz de 16 procesadores SHARC y es capaz de proporcionar picos de velocidad de hasta 1,9 Gflops usando el formato IEEE simple precisión. BlueICE triplica dicha velocidad, y su coste no es elevado. 1.4.3.3. FPGAs y Computadores de Lógica Reprogramable Si el paralelismo consiste en conseguir la máxima velocidad, ¿por qué no construimos nuestro propio hardware? El problema es que cuesta mucho, lleva mucho tiempo su desarrollo, se vuelve inservible cuando cambiamos el algoritmo aunque sea sólo un poco, etc. Sin embargo recientes avances en los FPGAs (“Field Programmable Gate Array”, Matriz de Compuertas Lógicas Programable) eléctricamente reprogramables han anulado dichas objeciones. Ahora la densidad de compuertas es lo suficientemente alta como para un procesador simple pueda ser construido en un único FPGA. Además el tiempo de reconfiguración (reprogramación) de los FPGAs ha bajado a un nivel en el que es razonable incluso reconfigurar el FPGA cuando pasamos de una fase de un algoritmo a otra. Normalmente debemos trabajar con lenguajes de descripción de hardware como VHDL para la reconfiguración del FPGA, así como escribir código de bajo nivel que haga de interfaz con los programas del sistema host Linux. Pero el esfuerzo merece la pena, ya que el coste de los FPGAs es bajo, especialmente para algoritmos que operen con enteros de baja precisión (en realidad un subconjunto de aquellas operaciones para las que fué diseñado SWAR). Los FPGAs pueden realizar complejas operaciones tan rápido como podamos introducir los datos. Por ejemplo, sistemas basados en un único FPGA han alcanzado tiempos mejores que los supercomputadores en la búsqueda de bases de datos genéticas. Las siguientes dos compañías desarrollan hardware basado en FPGA: Virtual Computer Company Esta compañía ofrece una variedad de productos que usan FPGAs Xilinx dinámicamente reconfigurables basados en SDRAM. Destaca su tarjeta “Virtual ISA Proto Board” de 8/16 bit para el puerto ISA. Altera El ARC-PCI (“Altera Reconfigurable Computer PCI bus”, Computador Reconfigurable Altera para bus PCI) es una tarjeta del mismo tipo que la “Virtual ISA Proto Board” de Virtual Computer Company, pero utiliza FPGAs Altera y el interfaz PCI en vez del ISA. Muchas de las herramientas de diseño, lenguajes de descripción de hardware, compiladores, etc. funcionan sólo bajo Windows o DOS, por lo que podríamos utilizar dosemu o wine para ejecutarlos bajo Linux. 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO CPU CPU 19 CPU E/S E/S Interconexión E/S Memoria Principal Figura 1.8: Esquema Multiprocesador 1.5. Arquitecturas Basadas en Paralelismo Explícito 1.5.1. Multiprocesadores Un sistema multiprocesador es aquel que está formado por un conjunto de procesadores que comparten una memoria principal común y están bajo el control de un mismo sistema operativo. Las principales características de un procesador de este estilo son: 1. Posee dos o más procesadores de propósito general similares y de capacidades comparables. 2. Todos los procesadores comparten el acceso a una memoria global (común). También pueden utilizarse memorias locales (privadas). 3. Todos los procesadores comparten el acceso a los dispositivos de E/S, bien a través de los mismos canales o bien a través de canales distintos que proporcionan caminos de acceso a los mismos dispositivos. 4. El sistema está controlado por un sistema operativo integrado que permite la interacción entre los procesadores y los programas. La figura 1.8 describe en términos generales la organización de un sistema multiprocesador. Hay dos o más CPUs. Cada CPU es autónoma, incluyendo una unidad de control, una CAPÍTULO 1. INTRODUCCIÓN 20 CPU CPU E/S E/S Memoria Memoria Bus del Sistema Figura 1.9: Organización Básica Bus de Tiempo Compartido ALU, registros y posiblemente una caché. Cada CPU tiene acceso a una memoria principal compartida y a los dispositivos de E/S a través de algún mecanismo de interconexión. Los procesadores pueden comunicarse entre sí a través de la memoria (mensajes de información de control almacenada en áreas comunes de datos). También es posible que las CPUs intercambien señales directamente, como indican las líneas discontinuas. La memoria a menudo se organiza de forma que sean posibles los accesos simultáneos a bloques de memoria separados. En algunas configuraciones cada CPU puede tener también su propia memoria principal privada y sus canales de E/S además de los recursos compartidos. 1.5.1.1. Organización La organización de un sistema multiprocesador puede clasificarse como sigue: Bus de Tiempo Compartido o Común Memoria Multipuerto Unidad de Control Central Bus de Tiempo Compartido El Bus de Tiempo Compartido es el mecanismo más simple para construir un sistema multiprocesador (figura 1.9). La estructura y las interfaces son básicamente las mismas que las de un sistema monoprocesador que utilice un bus para la interconexión. El bus consta de líneas de control, dirección y datos. Para facilitar las transferencias DMA (“Direct Memory Access”, Acceso Directo a Memoria) con los procesadores de E/S se proporcionan los siguientes elementos: Direccionamiento: Debe ser posible distinguir los módulos del bus para determinar la fuente y el destino del mensaje. Arbitraje: Cualquier módulo de E/S puede funcionar temporalmente como maestro. Se proporciona un mecanismo para arbitrar entre las peticiones que compiten por el control del bus, utilizando algún tipo de esquema de prioridad. 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO CPU CPU Caché E/S E/S Memoria 21 Memoria Caché Bus del Sistema Figura 1.10: Organización Bus de Tiempo Compartido con Caché Tiempo Compartido: Cuando un módulo está controlando el bus, los demás módulos no tienen acceso al mismo y deben, si es necesario, suspender su funcionamiento hasta que dispongan del bus. Estos elementos son utilizables directamente en una configuración de multiprocesador. En este caso hay varias CPUs además de varios procesadores de E/S que intentan acceder a uno o más módulos de memoria a través del bus. La organización de Bus de Tiempo Compartido presenta diversas ventajas en comparación con otras aproximaciones: Simplicidad: Es la aproximación más sencilla para organizar el multiprocesador. La interfaz física y la lógica de cada procesador para el direccionamiento, el arbitraje, y para compartir el tiempo del bus es la misma que la de un sistema con un solo procesador. Flexibilidad: Es generalmente sencillo expandir el sistema conectando más CPUs al bus. Fiabilidad: El bus es esencialmente un medio pasivo, y el fallo de cualquiera de los dispositivos conectados no provocaría el fallo de todo el sistema. La principal desventaja de la organización de bus compartido son las prestaciones. Todas las referencias a memoria pasan por el bus. En consecuencia, la velocidad del sistema está limitada por el tiempo de ciclo. Para mejorar las prestaciones es deseable equipar a cada CPU con una memoria caché (figura 1.10). Ésta reduciría considerablemente el número de accesos. El uso de cachés introduce algunas consideraciones de diseño nuevas. Puesto que cada caché local contiene una imagen de una parte de la memoria, si se altera una palabra en una caché es posible que quede invalidada una palabra en otra caché. Para evitarlo debe avisarse a las otras CPUs de que se ha producido una actualización de la memoria. Esto por lo general complica el diseño del sistema. Memorias Multipuerto La organización de memorias multipuerto permite el acceso directo e independiente a los módulos de memoria desde cada una de las CPUs y los módulos de E/S (figura 1.11). Se necesitará un sistema de arbitraje para resolver los conflictos. El método que se utiliza a menudo para ello consiste en asignar prioridades fijas a cada puerto de memoria. Normalmente CAPÍTULO 1. INTRODUCCIÓN 22 M1 M2 Mk CPU1 E/S 1 CPUn E/S m Figura 1.11: Organización Memorias Multipuerto la interfaz física y eléctrica en cada puerto es idéntica a la que aparece en un módulo de memoria de un sólo puerto. Así, se necesitan pocas o ninguna modificación en las CPUs o en los módulos de E/S para adaptarlas a este modelo de organización. La organización de memorias multipuerto es más compleja que la organización de bus compartido, precisándose añadir al sistema una buena cantidad de lógica. No obstante se consiguen mejores prestaciones puesto que cada procesador tiene un camino específico a cada módulo de memoria. Otra ventaja del multipuerto es que permite configurar partes de la memoria como “privadas” para una o más CPUs y/o módulos de E/S. Estas características permiten incrementar la seguridad frente a accesos no autorizados y para el almacenamiento de rutinas de restablecimiento en zonas de memoria no susceptibles de ser modificadas por otros procesadores. Unidad de Control Central La unidad de control central encauza las distintas secuencias de datos entre los distintos módulos independientes: CPU, memoria, E/S. El controlador puede almacenar temporalmente peticiones y realizar las funciones de arbitraje y temporización. Además puede transmitir mensajes de estado y control entre las CPUs y alertar sobre cambios en las cachés. Puesto que toda la lógica de coordinación de la configuración de multiprocesador se concentra en la unidad de control central, las interfaces de E/S, memoria, y CPU no sufren cambios esenciales. Esto proporciona la flexibilidad y simplicidad de conexión propias de la aproximación del bus compartido. Las desventajas clave de esta aproximación son que la unidad de control es bastante compleja y que representa un cuello de botella potencial para las prestaciones. 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO 1.5.1.2. 23 Multiprocesadores Simétricos (SMP) SMP (“Symmetric Multi-Processor”, Multiprocesadores Simétricos) se refiere, en términos de sistemas operativos, al concepto de grupo de procesadores trabajando juntos como pares, de manera que cualquier parte del trabajo puede ser realizada por cualquiera de los procesadores sin diferencia alguna. Este tipo de sistemas son multiprocesadores, es decir, pertenecen al modelo MIMD con memoria compartida. En el mundo IA32, SMP significa cumplir con la norma MPS (“Intel MultiProcessor Specification”, Especificación Multiprocesador de Intel). En un SMP todos los procesadores pueden acceder a la misma memoria, generalmente mediante un bus compartido de alta velocidad. La comunicación entre los procesadores es fácil en principio. Cualquier procesador puede dejar datos o mensajes en una posición determinada y luego indicar a otro procesador la dirección en la que puede encontrar los datos. Esto es típico del multiprocesamiento; así, un computador ejecuta una o más aplicaciones que se componen de una serie de procesos secuenciales que cooperan entre sí. Este esquema se puede implementar en un sistema monoprocesador, pero también se puede implementar fácilmente en un sistema multiprocesador SMP: en cualquier momento cada uno de los múltiples procesadores está ejecutando un proceso distinto. La comunicación entre procesos se realiza mediante mensajes y señales de estado que pueden intercambiarse los procesadores a través de la memoria. En la práctica el nivel de prestaciones deseado complica la comunicación entre los procesadores. Cuando se tienen muchos procesadores rápidos compitiendo por acceder a la misma memoria a través del mismo bus, los conflictos pueden degradar seriamente el rendimiento del conjunto del sistema. La solución, como hemos visto, es añadir una caché local a cada procesador. Esta solución ocasiona el problema adicional de la coherencia de caché. La coordinación entre los procesadores que se requiere para resolver el problema origina un cuello de botella en el bus compartido. El resultado es que los multiprocesadores típicos están limitados a unas pocas decenas de procesadores. Un multiprocesador SMP con miles de procesadores no parece que sea muy práctico. 1.5.1.3. Soporte SMP Bajo Linux Antes de entrar a discutir aspectos más técnicos, debemos hacernos primero un par de cuestiones acerca de esta arquitectura que, si bien es una de las más extendidas en procesamiento paralelo, tiene un soporte relativamente reciente en Linux. ¿Funciona realmente SMP en Linux? Funciona. Además es una opción con una buena relación coste/rendimiento para paralelizar nuestro sistema, ya que una placa base con soporte para varios procesadores y sus correspondientes micros normalmente elevará poco el precio del sistema con respecto a un sistema uniprocesador. En la mayoría de las ocasiones para hacer que Linux se ejecute en dicho hardware sólo necesitamos realizar una instalación de Linux con soporte para un solo procesador, y luego recompilar el núcleo con la fila SMP=1 del fichero ‘Makefile’ activa. Entonces informaremos a lilo o nuestro gestor de arranque acerca del nuevo núcleo compilado, y a disfrutar. En cuanto a rendimiento y estabilidad podemos decir son sistemas bastante firmes. En resumen, SMP Linux realmente funciona. 24 CAPÍTULO 1. INTRODUCCIÓN La siguiente cuestión es cuánto soporte de alto nivel está disponible para escribir y ejecutar programas paralelos con memoria compartida en SMP Linux. Actualmente existe bastante software dedicado a este propósito. Por ejemplo, ahora podemos contar con una librería muy completa para el manejo de hilos POSIX. Aunque el rendimiento puede ser menor que utilizando mecanismos especialmente creados para trabajar con memoria compartida, un sistema SMP Linux también puede usar la mayoría del software de procesamiento paralelo desarrollado originalmente para clusters de estaciones de trabajo. Dicho tipo de software utiliza comunicación vía sockets, y puede funcionar en un sistema SMP Linux e incluso en múltiples SMP conectados en red formando un cluster. De todas formas la comunicación vía sockets implica mucha sobrecarga innecesaria en un sistema SMP. Gran parte de esta sobrecarga se produce dentro del núcleo y en el manejo de interrupciones; ésto empeora el problema ya que SMP Linux generalmente sólo admite un procesador en el núcleo al mismo tiempo y el manejador de interrupciones está implementado de manera que sólo el procesador de arranque (“boot processor”) puede procesar interrupciones. A pesar de ello el hardware de comunicación de un SMP típico es tan superior a la mayoría de las redes de comunicación de los clusters, que el software diseñado para dichos clusters a menudo funcionará mejor en los sistemas SMP que en los clusters para los que fué creado. En el resto de esta sección estudiaremos qué tipo de sistemas SMP soporta Linux, aclararemos qué es la Especificación Multiprocesador de Intel, y haremos algunas observaciones acerca del uso de la memoria caché y la configuración del bus a utilizar. Especificación Multiprocesador de Intel Aunque los sistemas SMP han existido desde hace muchos años, hasta hace poco cada máquina tendía a implementar sus funciones básicas de manera distinta al resto, de forma que el soporte del sistema operativo no era portable. Lo que ha cambiado esta situación es la MPS (“Intel MultiProcessor Specification”, Especificación Multiprocesador de Intel). Los únicos sistemas que no cumplen la norma MPS ni la IA32 y tienen soporte bajo Linux son las máquinas multiprocesador Sun4m de SPARC. Linux tiene soporte para la mayoría de las máquinas que cumplen la norma MPS 1.1 y MPS 1.4 con hasta 16 procesadores 486DX, Pentium, Pentium Pro o superiores. Los procesadores IA32 que no tienen soporte son los procesadores Intel 386, Intel 486SX/SLC (la falta de unidad de punto flotante interfiere en los mecanismos SMP) y los procesadores AMD y Cyrix (requieren chips de soporte SMP que no parecen estar disponibles por el momento). Es importante entender que el rendimiento de los sistemas que cumplen la norma MPS puede variar ampliamente. Como era previsto, una causa de las diferencias de rendimiento es la velocidad del procesador: mayores frecuencias de reloj tienden a proporcionar sistemas más potentes, teniendo en cuenta que un procesador Pentium Pro es más rápido que un Pentium. De todas formas la norma MPS no especifica realmente cómo implementa el hardware la memoria compartida; lo que sí especifica es la manera en la que esta implementación debe funcionar desde el punto de vista del software. Esto significa que el rendimiento está también en función de cómo interactúa la implementación de la memoria compartida con las características SMP Linux y nuestro sistema particular. La principal diferencia entre los sistemas que cumplen la norma MPS es la manera en la que implementan físicamente el acceso a la memoria compartida. 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO 25 Aspectos sobre la Memoria Caché Algunos sistemas MPS con procesadores Pentium y todos los que llevan procesadores Pentium Pro y superiores tienen cachés L2 (“Level 2”, De Nivel 2) independientes (la memoria caché L2 va dentro del módulo que contiene el procesador). El hecho de tener cachés L2 separadas es considerado generalmente como una optimización del rendimiento; sin embargo ésto no es tan obvio bajo Linux. La principal complicación está en que el planificador de SMP Linux no intenta mantener cada proceso en el mismo procesador, siendo éste un concepto denominado afinidad con el procesador. Ésto podría cambiar pronto; recientemente se han generado discusiones sobre el tema en la comunidad SMP Linux bajo el título “Enlace con el procesador”. Sin afinidad con el procesador, el hecho de tener cachés L2 separadas podría introducir una sobrecarga importante cuando le demos ciclos de reloj a un proceso en un procesador distinto al que estaba ejecutando dicho proceso anteriormente. Muchos sistemas relativamente económicos están organizados de manera que dos procesadores Pentium comparten una memoria caché L2. Lo malo es que ésto provoca competencia entre los procesadores por el uso de la caché, degradando seriamente el rendimiento cuando se ejecutan múltiples programas secuenciales independientes. Sin embargo la mayoría de los programas paralelos se benefician de la memoria caché compartida en situaciones en las que ambos procesadores intentan acceder a la misma línea de la memoria compartida; en dichas situaciones sólo uno de los procesadores debe acceder a la caché y la competencia entre los procesadores es evitada. De este modo se crea el efecto “búsqueda compartida”. Además la falta de afinidad con el procesador provoca menos daño con una caché L2 compartida. Así, en programas paralelos no está realmente claro que la compartición de caché L2 sea tan dañina como cabe esperar. La experiencia demuestra que la caché de los sistemas SMP Linux muestran grandes diferencias en su rendimiento dependiendo del nivel de actividad del núcleo requerida. En el peor de los casos el aumento es 1.2 veces la velocidad del procesador. De todas maneras también se han observado picos de aumento de hasta 2.1 veces la velocidad del procesador, lo cual sugiere que el cómputo intensivo de código con el modelo SPMD saca provecho del efecto “búsqueda compartida”. Configuración del Bus Lo primero que hay que decir es que la mayoría de los sistemas modernos conectan los procesadores (en turnos) a uno o más buses PCI que hacen de puente hacia uno o más buses ISA/EISA. Estos puentes añaden latencia, y además los buses EISA e ISA ofrecen generalmente una menor velocidad de transferencia de datos que los PCI (ISA es el de menor velocidad), de manera que las unidades de disco, tarjetas de vídeo y otros componentes de alto rendimiento deben ser conectados al bus PCI. Aunque un sistema MPS puede alcanzar una buena velocidad en la ejecución de muchos programas paralelos de cómputo intensivo (incluso si sólo hay un bus PCI de tiempo compartido), las operaciones de E/S no se ejecutan mejor que en los sistemas uniprocesador... de hecho irán probablemente peor debido a la competencia por el bus entre los procesadores. Así, si lo que buscamos es mejorar el rendimiento de las operaciones E/S debemos emplear un sistema MPS con múltiples buses PCI independientes y controladoras E/S (por ejemplo, múltiples controladoras SCSI). Antes que nada necesitaremos estar seguros de que SMP Lin- CAPÍTULO 1. INTRODUCCIÓN 26 ux soporta el sistema que se desea emplear. Además debemos tener en cuenta que actualmente Linux permite sólo un procesador en el núcleo al mismo tiempo, de manera que elegiremos aquellas controladoras E/S que minimicen el tiempo requerido en el núcleo para realizar cada operación E/S. Para conseguir un alto nivel de rendimiento tendremos que considerar la posibilidad de manejar la E/S de los dispositivos directamente, sin hacer llamadas al sistema... esto no es tan difícil como parece, y tampoco debería comprometer la seguridad. Es importante hacer notar que la relación entre la velocidad del bus y la frecuencia de reloj del procesador se ha vuelto más difusa durante los últimos años. Actualmente no es extraño encontrar sistemas con mayor frecuencia de reloj en el procesador y a la vez menor frecuencia en el bus, en comparación con otros sistemas. El ejemplo clásico de ésto es el Pentium 133, el cual usa generalmente un bus más rápido que el Pentium 150, debido a la búsqueda de un mayor rendimiento en varios bancos de prueba. Estos efectos se amplifican en los sistemas SMP. De todos modos, en la mayoría de los casos es más importante tener un bus rápido. 1.5.2. Multicomputadores Un sistema multicomputador es aquel que está formado por un conjunto de sistemas relativamente autónomos, en los que cada CPU dispone de su propia memoria principal y sus canales de E/S. En este tipo de sistemas, cada procesador tiene su propio espacio de memoria privado, que no resulta visible para los otros procesadores. Según ésto los resultados y la información de coordinación debe pasarse entre los nodos a través de una red de interconexión, usualmente en forma de mensajes con un formato determinado. Una de las motivaciones principales para el desarrollo de organizaciones de multicomputador es la de superar las limitaciones de escala de los multiprocesadores; de éste modo los multicomputadores poseen una mayor escalabilidad. Se define como escalabilidad de un sistema a su capacidad para responder a cargas de trabajo crecientes. En un sistema de procesamiento paralelo, se mide mediante el número de máquinas que pueden conectarse al sistema sin que el rendimiento del sistema caiga. El objetivo es desarrollar una organización escalable que pueda dar cabida a un amplio rango de número de procesadores. Puesto que los procesadores en un multicomputador deben comunicarse mediante el intercambio de mensajes, un elemento clave en el diseño es la red de interconexión, que debe realizarse para operar lo más eficientemente posible. En general existe un compromiso entre la distancia máxima entre dos nodos y el número de conexiones físicas que se desea en cada nodo. Se han explorado varias tecnologías de interconexión para proporcionar escalabilidad y hacer posible un comportamiento eficiente. A continuación describiremos las topologías más extendidas: Anillo: Si la comunicación es bidireccional a lo largo del anillo, entonces la distancia máxima entre dos nodos cualesquiera para un anillo de nodos es '$)( . Usualmente se utilizan paquetes de mensajes de tamaño fijo que contienen la dirección del destino deseado. Esta topología es apropiada para un número relativamente pequeño de procesadores con una comunicación de datos mínima. La figura 1.12 ilustra un anillo con cuatro nodos. 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO 27 Figura 1.12: Topología Anillo Figura 1.13: Topología Malla Malla: La forma más simple de una malla es una matriz bidimensional en la que cada nodo se conecta a cuatro vecinos. En una malla simple de +* nodos, la distancia máxima entre dos nodos es (-,.0/2143 . Si hay conexiones cíclicas utilizando los nodos de los bordes, la distancia máxima se reduce a (-,.'$)(53 . La organización en malla es adecuada para ejecutar algoritmos orientados a matrices. La figura 1.13 ilustra una malla de 3*3 nodos. Árbol: Las redes con topología de árbol se han investigado para ejecutar algoritmos de tipo divide-y-vencerás, tales como los de búsqueda y ordenación. La figura 1.14 ilustra Figura 1.14: Topología Árbol CAPÍTULO 1. INTRODUCCIÓN 28 Figura 1.15: Topología Hipercubo Número de nodos 16 256 1024 2048 16384 Malla 6 saltos 30 saltos 62 saltos 126 saltos 254 saltos Hipercubo 4 saltos 8 saltos 10 saltos 11 saltos 16 saltos Cuadro 1.1: Diámetros Topologías Hipercubo y Malla un árbol binario sencillo. Hipercubos: Una topología hipercubo utiliza 6 (87 procesadores distribuidos en un hipercubo de dimensión . Cada enlace tiene enlaces bidireccionales a nodos adyacentes. También representa la distacia máxima entre dos nodos de dicho hipercubo. En ( 1:9 procesadores. la figura 1.15 mostramos un hipercubo de dimensión 4, con 6 Desde la perspectiva de la implementación hardware de una topología, la topología hipercubo es la más atractiva. En el cuadro 1.1 comparamos la comunicación en el peor caso en las topologías hipercubo y malla. El hipercubo se escala bien: el diámetro del sistema crece lentamente con el número de nodos, en comparación con la malla. Recientemente ha surgido un interés creciente por pasar de topologías fijas a topologías seleccionables por el usuario y controladas por una red de enrutamiento. En lugar de conectar todos los procesadores juntos mediante enlaces directos, se conectan a una red de enrutamiento rápida que utiliza conmutadores para establecer e interrumpir las conexiones virtuales, de una forma similar a la conmutación de paquetes. Si los conmutadores se diseñan para que ocasionen retardos mínimos en los mensajes, el retardo de comunicación se incrementará ligeramente al añadir más nodos al sistema. Otra propiedad atractiva es que cada procesador necesita sólo una conexión bidireccional a la red de conmutadores. 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO 29 Los sistemas multicomputador pueden dividirse básicamente en dos categorías: Clusters (o agrupamientos de nodos). Estos sistemas son grupos de nodos que se comunican a través de una interfaz de red. Deben tener un buena relación coste/rendimiento, por lo que en principio sólo debemos utilizar para su montaje componentes hardware relativamente comunes. MPPs (“Massively Parallel Processors”, Procesadores Masivamente Paralelos). Estos sistemas pueden distinguirse de los anteriores por las propiedades que los hacen Masivamente Paralelos: ; Utilización de interconexiones de muy alto rendimiento ; ; Capacidad de E/S inmensa Tolerancia a fallos Seguramente quedemos sorprendidos al comprobar que estos sistemas no necesitan CPUs de gama alta. La mayoría de los mejores sistemas MPPs tienen miles de CPUs, siendo más importante para el rendimiento del sistema la calidad de las interconexiones y su topología. 1.5.2.1. Clusters o Agrupamientos de Nodos Una de las posibilidades de mejor relación coste/rendimiento es la proporcionada por los clusters, grupos de nodos conectados a través de una interfaz de red. Dicha organización crea un sistema multicomputador, es decir, un sistema de procesamiento paralelo perteneciente al modelo MIMD con memoria distribuida. Por supuesto su tipología puede ser muy variada, así como el soporte tanto hardware como software. Afortunadamente existe mucho y variado soporte software para el procesamiento paralelo usando PCs bajo Linux, gracias a la popularidad de esta tecnología. La utilización de clusters en procesamiento paralelo supone muchas ventajas, como son: Cada una de las máquinas de un cluster puede ser un sistema completo, que puede ser utilizado para ejecutar una amplia gama de aplicaciones. Ésto lleva a pensar que el procesamiento paralelo bajo clusters no es más que una manera de aprovechar los “ciclos desperdiciados” por los usuarios de PCs. Sin embargo no es tan sencillo aprovechar estos ciclos sin ralentizar el trabajo primario para el que se utilizan dichas estaciones de trabajo, pero puede hacerse. La creciente popularidad de los sistemas en red hace que la mayoría del hardware que necesitamos para construir un cluster se fabrique a gran escala, con la correspondiente bajada de precios como resultado. También reduce el coste del sistema el hecho de que sólo necesitamos una consola (es decir, una tarjeta de vídeo, un monitor y un teclado) para todo el cluster, aunque necesitemos conectar dicha consola a cada máquina al principio para hacer una instalación inicial; una vez hecha la instalación, un PC bajo Linux no necesita una consola para funcionar. En comparación, los SMP y los Procesadores Auxiliares pertenecen a mercados mucho más restringidos, con lo que suelen tener precios más elevados. 30 CAPÍTULO 1. INTRODUCCIÓN Figura 1.16: Objetivo Procesamiento Paralelo en Clusters Los clusters pueden llegar a formar sistemas verdaderamente amplios, no teniendo las limitaciones de escala de los SMP. Mientras que es complicado encontrar sistemas SMP compatibles con Linux con más de 4 procesadores, podemos construir un cluster de hasta 16 sistemas con casi cualquier hardware de red común. Con no mucho trabajo, cientos e incluso miles de máquinas pueden ser interconectadas. De hecho, Internet entero puede ser vista como un cluster verdaderamente gigantesco. El hecho de que sustituir un computador estropeado en un cluster sea trivial comparado con arreglar un SMP parcialmente estropeado hace que la disponibilidad sea mucho mayor en los clusters cuidadosamente diseñados. Ésto es importante no sólo en aplicaciones particulares que no toleren interrupciones en el servicio, si no que también en sistemas de uso general que contengan suficientes procesadores como para que sean muy comunes los fallos aislados. Por ejemplo, incluso cuando la media de fallo de un PC sea de dos años, en un cluster con 32 máquinas la probabilidad de que una falle en los primeros 6 meses es muy alta. La conclusión es que los clusters son económicos, que los dispositivos necesarios para construirlo están disponibles, y que se pueden construir sistemas verdaderamente amplios; pero por supuesto no todo son ventajas. También existen una serie de problemas: Excepto muy pocas excepciones, el hardware de red no está diseñado para el procesamiento paralelo. Normalmente la latencia es muy alta y la velocidad de transferencia de datos relativamente baja comparada con la proporcionada por los SMPs y los procesadores auxiliares. Por ejemplo, la latencia casi nunca supera unos pocos microsegundos en los SMPs, pero casi siempre es igual o mayor a cientos o miles de microsegundos en un cluster. La velocidad de transferencia de datos en los SMP es mayor de 100 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO 31 Mbytes/seg; aunque el hardware de red más rápido (Gigabyte Ethernet) ofrece una velocidad comparable, la mayoría de las redes comunes son entre 10 y 1000 veces más lentas. El rendimiento del hardware de red puede ser suficiente si es una red aislada para cluster. Si la red no está aislada de otro tráfico, el rendimiento puede ser sustancialmente peor. Existe poco soporte de software para manejar un cluster como si se tratara de un sólo sistema. Por ejemplo, el comando ps nos informa de los procesos que concurren en un sistema Linux, pero no nos informa de todos los procesos que concurren en el cluster de sistemas Linux. Con todo ésto, podemos decir que los clusters ofrecen una gran potencia, pero dicha potencia puede ser difícil de alcanzar en la mayoría de las aplicaciones. Lo bueno es que existe una gran cantidad de software que nos puede ayudar a conseguir un alto rendimiento en programas bien adaptados a este entorno, y que existen redes diseñadas específicamente para ampliar el rango de programas que pueden alcanzar un buen rendimiento. 1.5.2.2. Sistemas Beowulf Un sistema Beowulf es básicamente un cluster dedicado al procesamiento paralelo. Es un sistema que usualmente consta de un nodo servidor y uno o más nodos clientes conectados vía Ethernet o algún otro tipo de red. Es un tipo de sistema que se construye empleando hardware común, como por ejemplo PCs bajo Linux, adaptadores Ethernet estándar y hubs convencionales. No contiene ningún hardware especializado y es trivialmente reproducible. Además Beowulf usa software común como Linux, PVM y MPI. El nodo servidor controla todo el cluster y ofrece ficheros a los nodos clientes. También es la consola del cluster y su conexión al exterior. Los sistemas Beowulf más grandes pueden tener más de un nodo servidor, y posiblemente otros nodos dedicados a tareas particulares, por ejemplo consolas o estaciones de monitorización. En la mayoría de los casos los nodos clientes no tienen dispositivos directos de E/S; son controlados y configurados por el nodo servidor. En una configuración de clientes sin disco, los clientes no conocen su IP ni su nombre hasta que el nodo servidor se lo envía. Una de las principales diferencias entre un Beowulf y un COW(“Cluster Of Workstations”, Cluster de Estaciones de Trabajo) es que el comportamiento de un Beowulf es más parecido al de una única máquina que al de un grupo de estaciones de trabajo. En la mayoría de los casos los clientes no tienen teclado ni monitor, y son accedidos sólo a través de login remoto o posiblemente mediante un terminal serie. Podemos pensar en los nodos de un sistema Beowulf como en paquetes CPU+Memoria que pueden ser agregados al cluster, de la misma manera que una CPU o un módulo de memoria puede ser agregado a una placa base. Beowulf no es un paquete especial de software, ni una nueva topología de red, ni la última modificación del núcleo de Linux. Beowulf es una manera de crear clusters de PCs bajo Linux para formar un supercomputador virtual paralelo. Existen muchos paquetes software como modificaciones del núcleo de Linux, librerías PVM y MPI, y herramientas de configuración que hacen la arquitectura Beowulf más rápida, fácil de configurar y útil. Sin embargo podemos construir una máquina de tipo Beowulf usando una distribución estándar Linux sin software adicional. Si tenemos dos ordenadores con Linux en red que comparten un sistema de ficheros CAPÍTULO 1. INTRODUCCIÓN 32 ‘/home’ vía NFS (“Network File System”, Sistema de Ficheros en Red) y podemos ejecutar shell remoto (rsh) desde una máquina a otra, entonces podemos argumentar que tenemos un sistema Beowulf simple de dos nodos. Clasificación Sistemas Beowulf En la práctica los sistemas Beowulf han sido construidos de distintas maneras. Debido a la búsqueda de un mayor rendimiento a veces han sido empleados componentes no comunes, como por ejemplo aquellos que son comercializados por sólo un fabricante. Para tener en cuenta los diferentes tipos de sistemas existe la siguiente clasificación. BEOWULF CLASE I: Este tipo de máquinas son construidas a base de componentes sacados del mercado común. La definición estricta es que un Beowuf CLASE I debe se ensamblado a partir de componentes que se puedan encontrar en al menos tres catálogos de publicidad nacionales o internacionales. Las ventajas de los sistemas de CLASE I son: El hardware está disponible en múltiples fuentes (bajo precio, fácil mantenimiento). Ausencia de dependencia con respecto a un único fabricante. Soporte bajo Linux garantizado en sus distribuciones más comunes. Usualmente basado en estándares (SCSI, Ethernet, etc.). Las desventajas de los sistemas de CLASE I son: Mayor rendimiento puede requerir hardware de la CLASE II. BEOWULF CLASE II: Un Beowulf de CLASE II es simplemente cualquier máquina que no pueda ser ensamblada utilizando únicamente hardware común. Ésto no es un impedimento. De hecho ésto es sólo una clasificación. Las ventajas de los sistemas de CLASE II son: El rendimiento puede llegar a ser realmente alto Las desventajas de los sistemas de CLASE II son: El soporte bajo Linux puede variar de una distribución a otra. Existencia de dependencia con respecto a un único fabricante. Suelen ser sistemas más caros que los de CLASE I. Una clase no tiene porqué ser necesariamente mejor que la otra. Todo depende de nuestras necesidades y nuestro presupuesto. Ésta clasificación sólo pretende que las discusiones sobre los sistemas Beowulf sean coherentes y detalladas. 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO 1.5.2.3. 33 Hardware de Red Las redes de ordenadores pertenecen a un campo en pleno auge. El desarrollo de las tecnologías y la aparición de nuevos productos hacen que cada vez esté más al alcance de todos generar procesamiento paralelo a través de un cluster. Sin embargo no existe una tecnología de red que resuelva todos los problemas de la mejor manera; de hecho, la variedad de posibilidades, costes y rendimientos es tremenda. A continuación definiremos las características de algunos de los tipos de red más populares: ATM Soporte bajo Linux: controladores del núcleo, librería AAL Velocidad de Transferencia de Datos: 155 Mb/seg Latencia: 120 <>=@?A Disponibilidad: varios fabricantes Interfaz: PCI Estructura de Red: conexión mediante hub dedicado Al menos en parte podría decirse que la tecnología ATM (“Asynchronous Transfer Mode”, Modo de Transferencia Asíncrona) es el futuro. ATM no tiene un coste elevado y es más rápida que Fast Ethernet; además puede ser usada a través de las largas distancias que las compañías de teléfono pueden cubrir. El protocolo de red ATM está diseñado para proporcionar una interfaz software con poca sobrecarga y un manejo más eficiente de los mensajes pequeños y de las comunicaciones en tiempo real (p.ej. vídeo y audio digital). Además es una de las redes con mayor velocidad de transferencia de datos que Linux soporta actualmente. Lo malo es que no es una tecnología barata y que aún existen algunos problemas de compatibilidad entre los distintos fabricantes. Ethernet Soporte bajo Linux: controladores del núcleo Velocidad de Transferencia de Datos: 10 Mb/seg Latencia: 100 <>=@?A Disponibilidad: hardware común Interfaz: PCI Estructura de Red: hub dedicado o compartido, o bus multipuerto sin hub CAPÍTULO 1. INTRODUCCIÓN 34 Durante algunos años, la Ethernet de 10 Mb/seg ha sido la tecnología estándar de red. De hecho un gran número de PCs tienen su controladora Ethernet integrada en la placa base. En redes con poca carga podemos organizar las conexiones Ethernet como un bus multipuerto sin hub; esta configuración puede servirnos para conectar hasta 200 ordenadores con un coste mínimo, pero no es apropiado para procesamiento paralelo. Añadir un hub compartido no va a mejorar el rendimiento en realidad. Sin embargo los hubs dedicados proporcionan la máxima velocidad de transferencia de datos a conexiones simultáneas y no tienen un alto coste por ordenador. Linux soporta un amplio rango de interfaces Ethernet, pero es importante recordar que las variaciones en el hardware de la interfaz pueden suponer importantes diferencias en el rendimiento. Fast Ethernet Soporte bajo Linux: controladores del núcleo Velocidad de Transferencia de Datos: 100 Mb/seg Latencia: 80 <>=&?BA Disponibilidad: hardware común Interfaz: PCI Estructura de Red: hub dedicado o compartido Aunque existen varias tecnologías que se llaman a sí mismas “Fast Ethernet” este término se usa principalmente para referirnos a las Ethernet de 100 Mb/seg basadas en hub que son compatibles con los dispositivos y cables utilizados para las Ethernet “10 BaseT” de 10 Mb/seg. Como podíamos esperar esta tecnología posee un enorme mercado, con lo cual su relación prestaciones/precio es bastante mejor que en otras configuraciones. Sin embargo la trampa está en que al dividir el ancho de banda de un único bus de 100 Mb/seg entre un grupo de ordenadores mediante un hub compartido, el rendimiento puede quedar por debajo del que obtendríamos al utilizar una Ethernet de 10 Mb/seg con un hub dedicado, la cual proporciona a cada ordenador una conexión completa de 10 Mb/seg. Los hubs dedicados que proporcionan 100 Mb/seg a cada computador simultáneamente son caros, aunque sus precios bajan a diario y proporcionan un rendimiento muy superior al que obtenemos utilizando los hubs compartidos. Gigabit Ethernet Soporte bajo Linux: controladores del núcleo Velocidad de Transferencia de Datos: 1000 Mb/seg Latencia: 300 <>=&?BA Disponibilidad: varios fabricantes Interfaz: PCI Estructura de Red: hub dedicado o FDRs 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO 35 En principio Gigabit Ethernet no tiene buenas razones tecnológicas para ser llamada Ethernet. Sin embargo dicho nombre refleja la intención de que en un futuro sea una tecnología barata y destinada a un amplio mercado, con soporte nativo para IP. Sin embargo su precio actual hace que no resulte económica su utilización. Al contrario que en otras tecnologías Ethernet, Gigabit Ethernet ofrece un control a bajo nivel que aporta fiabilidad a dicho tipo de red. Los FDRs (“Full Duplex Repeaters”, Repetidores Full Duplex) multiplexan las líneas utilizando buffers y control de flujo localizado para mejorar el rendimiento. La mayoría de los hubs dedicados están siendo construidos con nuevos módulos interfaces para que sean compatibles con los dispositivos Gigabit. Existe un driver bajo Linux para la interfaz Yellowfin G-NIC de Packet Engines (URL [13]). Los primeros test bajo Linux arrojaron un ancho de banda unas 2,5 veces superior a la 100 Mb/seg Fast Ethernet; en las redes Gigabit la configuración del bus PCI es un factor crítico. FireWire (IEEE 1394) Soporte bajo Linux: no Velocidad de Transferencia de Datos: 394 Mb/seg Disponibilidad: varios fabricantes Interfaz: PCI Estructura de Red: aleatorio sin ciclos (configuración automática) FireWire (URL [2]), el estándar IEEE 1394-1995, está destinado a ser la red digital de bajo coste y alta velocidad para consumidores en general. Aunque suela promocionarse como la interfaz a la cual conectar cámaras de vídeo digitales a nuestro ordenador, FireWire está pensada para ser utilizada en aplicaciones que van desde ser el sustituto de SCSI hasta conectar los componentes de reproducción de vídeo y audio. Permite conectar hasta 64k dispositivos utilizando cualquier topología que haga uso de buses y puentes sin ciclos, y automáticamente detecta los componentes cuando son conectados o desconectados (conexión en caliente). Soporta mensajes cortos con baja latencia así como trasmisiones asíncronas al estilo ATM (usadas para mantener sincronizados los mensajes multimedia). Adaptec tiene productos FireWire que permiten la conexión de hasta 63 dispositivos a una tarjeta PCI. Aunque FireWire no será la red de mayor ancho de banda disponible dentro de un tiempo, el mercado de consumo (que puede abaratar bastante los precios) y el soporte para mensajes con baja latencia hacen de ésta una de las mejores tecnologías para construir clusters de PCs bajo Linux utilizando redes de paso de mensajes, al menos durante los próximos años. PLIP Soporte bajo Linux: controladores del núcleo Velocidad de Transferencia de Datos: 1,2 Mb/seg Latencia: 1000 <>=@?A Disponibilidad: hardware común CAPÍTULO 1. INTRODUCCIÓN 36 Interfaz: SPP Estructura de Red: cable entre dos ordenadores Sólo por el coste de un cable LapLink, PLIP (“Parallel Line Interface Protocol”, Protocolo de Interfaz de Línea Paralela) permite comunicar dos máquinas Linux a través del puerto paralelo estándar utilizando software basado en sockets. En términos de velocidad de transferencia de datos, latencia y escalabilidad, no es una tecnología de redes seria; sin embargo, su bajísimo coste y alta compatibilidad lo hacen útil. El controlador forma parte de las distribuciones estándar del núcleo de Linux. SLIP Soporte bajo Linux: controladores del núcleo Velocidad de Transferencia de Datos: 0,1 Mb/seg Latencia: 1000 <>=&?BA Disponibilidad: hardware común Interfaz: RS232C Estructura de Red: cable entre dos ordenadores Aunque SLIP (“Serial Line Interface Protocol”, Protocolo de Interfaz de Línea Serie) está situado al final del espectro de rendimiento, SLIP (o CSLIP o PPP) permite establecer comunicación mediante sockets entre dos ordenadores a través del puerto serie RS232 ordinario. Los puertos RS232 pueden ser conectados mediante un cable serie “null-modem” RS232 o incluso a través de un módem. En cualquier caso la latencia es alta y la velocidad de transferencia de datos baja, de manera que SLIP sólo debe ser usado cuando no exista otra alternativa disponible. Sin embargo es mejor que nada; además la mayoría de los PCs tienen dos puertos RS232, de manera que podemos crear una red conectando un grupo de ordenadores de manera linear o en topología de anillo. También existe un software de compartición llamado EQL. USB Soporte bajo Linux: controladores del núcleo Velocidad de Transferencia de Datos: 12 Mb/seg Disponibilidad: hardware común Interfaz: USB Estructura de Red: bus 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO 37 UNIDAD DE CONTROL DISTRIBUIDOR DE LA RED PE PE PE PE PE PE PE PE PE PE PE PE ELEMENTOS PROCESADORES Figura 1.17: Esquema Procesador Matricial USB (“Universal Serial Bus”, Bus Serie Universal) (URL [20]) es un bus serie que proporciona la velocidad de una interfaz ethernet convencional. Soporta conexión en caliente (podemos “enchufar” cualquier dispositivo USB con el ordenador encendido) y puede manejar simultáneamente hasta 127 periféricos que van desde teclados hasta cámaras de videoconferencia. En realidad no existe una especificación clara sobre la metodología a seguir para conectar varios PCs mediante este puerto; sin embargo la importancia del estándar y su comodidad hacen de esta interfaz una alternativa a tener en cuenta. Se puede decir que USB es la versión barata y de bajo rendimiento de FireWire que podemos utilizar en casi cualquier PC. 1.5.3. Procesadores Matriciales Esta arquitectura es representativa del modelo SIMD, es decir, una sola instrucción opera simultáneamente sobre múltiples datos. Un procesador matricial está formado por un conjunto de elementos procesadores (EPs) sincronizados que funcionan bajo la supervisión de una unidad de control (UC). Normalmente dicha UC es un procesador escalar que cuenta con su propia memoria para almacenar programas y datos. Además, cada EP tendrá también su propia memoria. En la figura 1.17 se muestra el esquema general de este tipo de arquitectura. El funcionamiento de este sistema es el siguiente: la UC busca y decodifica las instrucciones de la memoria central y, dependiendo del tipo de instrucción, son ejecutadas directamente en la unidad de control o bien son transmitidas a los EPs para su ejecución. Las 38 CAPÍTULO 1. INTRODUCCIÓN instrucciones escalares y de control de flujo como saltos, etc. se ejecutan en la UC, mientras que las instrucciones aritmético-lógicas, las de enrutado de datos y otras operaciones locales son transmitidas a los EPs y ejecutadas por éstos sobre su memoria local. Respecto a los datos, los EPs tienen sus propios canales de E/S. De esta manera, la instrucción que ejecutan simultáneamente los EPs es la misma, mientras que los datos serán los de la memoria de cada EP, y por tanto serán diferentes. Por todo ello un procesador sólo requiere un único programa para controlar todos los EPs. La idea de utilización de los procesadores matriciales es la de explotar el parlalelismo en los datos de un problema más que la de paralelizar la secuencia de ejecución de las instrucciones. El problema se paraleliza dividiendo los datos en particiones sobre las que se pueden realizar las mismas operaciones. Un tipo de datos altamente particionable es el formado por vectores y matrices, siendo éste último el tipo de datos hacia el cual están más orientadas este tipo de arquitecturas. Por ello a estos procesadores se les llama matriciales. Aparte del propio paralelismo obtenido por el cálculo en paralelo sobre los elementos de un vector o matriz, la propia red de interconexión entre los EPs reduce el tiempo de ejecución en determinadas tareas. Dicha red se encarga de conectar a los EPs con sus propios vecinos. De esta manera la topología de la red de interconexión determinará qué tipo de cálculos sacan el máximo partido a determinado procesador matricial. Por ejemplo, en el tratamiento de imágenes, donde muchas operaciones son cálculos entre píxels vecinos, una arquitectura matricial con una topología adecuada paralelizaría con facilidad dichos cálculos. Un caso particular de los procesadores matriciales son los Procesadores Asociativos o “Associative Processors” cuya principal característica es la utilización de memorias asociativas. La ejecución de la misma operación sobre distintas informaciones se usa en la simulación de condiciones climatológicas, control de tráfico aéreo, operaciones con matrices, etc. Ejemplos de computadores con esta organización son el ILLIAC IV, PEPE, BSP STARAN y ICLDAP. 1.5.4. Procesadores Vectoriales En esencia los procesadores vectoriales son máquinas con unidades funcionales vectoriales segmentadas (en definitiva ALUs segmentadas) diseñadas para optimizar las operaciones con estructuras de tipo vectorial. Este tipo de paralelismo es considerado MISD por la mayoría de los científicos debido a la utilización de ALUs segmentadas. La segmentación hace posible que varias instrucciones pueden ser ejecutadas simultáneamente sobre un mismo dato (el vector). Al igual que en los procesadores matriciales el paralelismo viene de que al operar con vectores o matrices normalmente sus elementos son independientes entre sí, es decir, en general no existen dependencias entre los datos dentro de los propios vectores o matrices. Esto permite que muchas de las operaciones aplicadas a un determinado vector y todas las que se hacen entre elementos de unos vectores con otros puedan realizarse en paralelo, o al menos en distintas etapas del mismo cauce de instrucciones sin que haya un conflicto entre los datos. Un computador vectorial estará formado normalmente por un procesador escalar y un procesador vectorial diseñado específicamente para operar con vectores. Dicho procesador vectorial contendrá a su vez varias ALUs, cada una de las cuales opera sobre un vector completo. Además dichas ALUs se encuentran completamente segmentadas y pueden comenzar una nueva operación cada ciclo de reloj. De esta manera el paralelismo proviene de la repeti- 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO 39 ción de ALUs y de su segmentación. Sin embargo es la segmentación la que hace que varias instrucciones puedan ser ejecutadas al mismo tiempo sobre un determinado vector. Dependiendo del tipo de procesador vectorial cada ALU será totalmente funcional o bien se encargará de realizar una función específica, como suma/resta, multiplicación, división, etc. Además debe existir una unidad de Carga/Almacenamiento encargada de leer los vectores desde la memoria y almacenarlos en ella. La diferencia entre este tipo de procesadores y los matriciales es que mientras los matriciales son comandados por las instrucciones, los vectoriales son comandados por flujos de datos continuos. Así mientras en los procesadores matriciales sólo existe un flujo de instrucciones entre la unidad de control y los elementos procesadores, en los computadores vectoriales tenemos dos flujos separados: el de instrucciones y el de datos. En este caso, el procesador escalar envía un flujo de datos continuo hacia el procesador vectorial, indicándole la operación que debe realizar mediante el flujo de instrucciones. En la figura 1.18 mostramos el diseño típico de un procesador vectorial con registros. 40 Procesador escalar Cauces funcionales escalares Procesador vectorial Instrucciones escalares Instrucciones vectoriales Unidad de control escalar Unidad de control vectorial Control Instrucciones Datos escalares Cauce func. vectorial Memoria principal (Programa y datos) Computador anfitrión Registros vectoriales E/S Usuario Figura 1.18: Esquema Procesador Vectorial Cauce func. vectorial CAPÍTULO 1. INTRODUCCIÓN Almacenamiento masivo Datos vectoriales 1.5. ARQUITECTURAS BASADAS EN PARALELISMO EXPLÍCITO 41 Figura 1.19: Cray SX-6 Los procesadores vectoriales, según el método de almacenamiento de los operandos, se clasifican en: Máquinas vectoriales con registros : En una máquina de este tipo todas las operaciones vectoriales excepto las de carga y almacenamiento operan con vectores almacenados en registros. La mayoría de máquinas vectoriales modernas utilizan este tipo de arquitectura. Ejemplos: Cray Research (CRAY1, CRAY-2, X-MP, Y-MP y C90), supercomputadores japoneses (NEC SX Series, las Fujitsu VP200 y VP400 y la Hitachi S820). Máquinas vectoriales memoria-memoria : En este tipo de máquinas, todas las operaciones vectoriales son de memoria a memoria. Como la complejidad interna así como el coste son menores, es la primera arquitectura vectorial que se empleó. Ejemplo: el CDC. Los supercomputadores vectoriales empezaron con modelos monoprocesadores como el Cray 1 en 1976. Los supercomputadores vectoriales recientes ofrecen ambos modelos, el monoprocesador y el multiprocesador. La mayoría de supercomputadores de altas prestaciones modernos ofrecen multiprocesadores con hardware vectorial como una característica más de los equipos. En el cuadro 1.2 vemos algunos ejemplos de procesadores vectoriales con sus características principales. CAPÍTULO 1. INTRODUCCIÓN 42 Computadora Año Hitachi S820 Cray 2 Cray Y-MP NEC SX-3 Cray C90 Cray 3 1983 1985 1988 1989 1991 1995 Ciclo Reloj (ns) 4.0 4.1 6.0 2.9 4.0 2.0 Ciclo Reloj (Mhz) 71 166 166 400 240 500 máx CPUs 1 4 8 4 16 16 Pico CPU (Mflop/s) 2000 488 333 5500 1000 2000 Cuadro 1.2: Ejemplos Computadores Vectoriales Pico Total (Mflop/s) 2000 1952 2667 22000 16000 32000 Capítulo 2 Herramientas para el Desarrollo de Software Paralelo Una vez resuelto el sistema de intercomunicación física de los equipos, deben abordarse los mecanismos para lograr la coordinación entre los procesadores. Básicamente existen dos modelos de interacción entre procesadores: paso de mensajes y memoria compartida. Dichos modelos se pueden utilizar de varias formas. Como en todas las áreas de la programación, existen varios métodos para alcanzar el objetivo: Por un lado podemos implementar dichos modelos a bajo nivel, utilizando código máquina, ensamblador o compiladores de lenguajes no especializados. Dicha alternativa nos permite desarrollar un código más adaptado y optimizado para el hardware que vayamos a usar. Por otro lado podemos emplear utilidades de desarrollo de software paralelo, que son básicamente facilidades para la programación a alto nivel de aplicaciones distribuidas (compiladores, librerías, etc.) y herramientas para su ejecución y monitorización. Dichas utilidades de programación permiten al programador generar de manera automática comunicación mediante paso de mensajes y memoria compartida sin que deba preocuparse de los detalles. Las ventajas de emplear dichas utilidades son su facilidad de manejo y la portabilidad de los algoritmos desarrollados. A continuación estudiaremos las características básicas de los dos modelos de interacción antes mencionados, para luego centrarnos en las utilidades de desarrollo más populares que existen en la actualidad. 2.1. Modelos de Interacción entre Procesadores 2.1.1. Paso de Mensajes El paso de mensajes es un modelo de interacción entre los procesadores que forman un sistema paralelo. A grandes rasgos un mensaje es un conjunto de datos definido mediante software por un procesador y enviado a través de una red de comunicaciones hacia otro procesador, el cual debe aceptar el mensaje y actuar según su contenido. 43 44 CAPÍTULO 2. HERRAMIENTAS DESARROLLO SOFTWARE PARALELO MEMORIA CPU MEMORIA CPU MEMORIA CPU MEMORIA CPU MEMORIA CPU Figura 2.1: Modelo Básico para Paso de Mensajes Todas las unidades de proceso pueden ser origen o destino de mensajes en cualquier momento y todas pueden intercambiar datos entre sí. El enrutamiento de mensajes se le deja al sistema operativo y el número de saltos o puntos de intercomunicación por los que el mensaje debe pasar dependerán de la topología de la máquina en uso. Este sistema puede utilizarse alcanzando un rendimiento óptimo en multicomputadores y en multiprocesadores con memoria distribuida. Aunque la sobrecarga en la comunicación (latencia) en cada paso de mensaje puede ser alta normalmente hay pocas restricciones sobre la cantidad de información que pueden contener dichos mensajes. De este modo si contamos con un ancho de banda amplio el paso de mensajes puede ser un método muy efectivo para transmitir grandes bloques de datos de entre los procesadores. Sin embargo para minimizar la necesidad de costosas operaciones de paso de mensajes los datos deben estar distribuidos entre los procesadores, de modo que el conjunto de datos referenciados por cada procesador esté en su memoria local. A ésto se le conoce como distribución de datos (“data layout”). Una gran ventaja del paso de mensajes reside en su elevada portabilidad, puesto que por lo general consiste en el protocolo de comunicaciones estándar de la red más las rutinas de sincronización y comunicación de procesos, que se ejecutan sobre dicho protocolo. Además dicho modelo puede usarse en una red de computadores heterogénea. 2.1.1.1. Comunicación mediante Sockets En un sistema multitarea como Linux los sockets son la base para la comunicación mediante paso de mensajes. Un socket es un punto de comunicación que se comunica con otro socket para enviarle mensajes. Son bidireccionales, los hay de varios tipos y nos permiten comunicarnos con un proceso que esté en otro ordenador o en el mismo. Cuando queremos recibir datos por un socket creamos uno y “escuchamos” por él hasta que nos lleguen datos. En caso de querer enviar datos lo que hacemos es crear un socket, cumplimentar los datos de la dirección de destino y enviar los datos al otro socket a través del nuestro. Por consiguiente para que dos procesos se comuniquen mediante sockets cada uno debe crear el suyo. Para transmitir datos entre nodos bajo Linux usaremos normalmente la familia de sockets AF_INET. Este tipo de sockets se usan principalmente en redes IP, como por ejemplo la extensa Internet. En estas redes los ordenadores se identifican mediante una dirección IP que es propia de cada nodo, o sea, no existen dos nodos con la misma dirección IP en una misma red. De esta manera los sockets pueden ser identificados unívocamente en toda la red. Ello es 2.1. MODELOS DE INTERACCIÓN ENTRE PROCESADORES 45 importante ya que, conociendo todos los datos necesarios, podemos ponernos en contacto con cualquier punto de comunicación de cualquier nodo de nuestra red. Los datos que identifican unívocamente a estos puntos de transmisión son: La dirección IP del nodo en donde reside el proceso que está usando ese punto de conexión. El número del puerto. El puerto es un número entero sin signo de 16 bits cuyo propósito es el de permitir que en un ordenador puedan existir 65536 puntos posibles de comunicación para cada tipo de socket AF_INET. Esto es suficiente para que no suponga un límite (en la práctica) para el número de puntos de comunicación diferentes que puede haber al mismo tiempo en un ordenador. Y el tipo de socket, dato que no reside en la información de direccionamiento, ya que es algo implícito al socket. Si usamos un socket de un tipo, éste sólo se comunicará con sockets del mismo tipo. Para referirnos a un socket usaremos la dirección de socket. Las direcciones de socket son diferentes según la familia. Llevan todos un primer parámetro que identifica la familia de socket y luego, según ésta, los datos correspondientes. En el caso de la familia de sockets para redes IP estos datos son la dirección del ordenador y el número de puerto. Los tipos de sockets de esta familia son dos, que se corresponden con los dos tipos transmisión de datos en una red de paquetes, y son los que definiremos a continuación. Datagramas (UDP) Es el modelo más sencillo y también el menos robusto. Consiste en enviar paquetes de datos a un destino determinado. Cada paquete que enviamos ha de llevar la dirección de destino y es guiado por la red de manera independiente. Estos paquetes con la dirección de destino incluida se llaman datagramas. Los datagramas tienen una longitud limitada, por lo que los datos que enviamos no pueden exceder de esa longitud. Además hay ciertos problemas derivados del hecho de que los paquetes transiten por una red, que se agravan cuanto más extensa sea ésta: Pérdida de paquetes. En el nivel de red y en los nodos de enrutamiento se pueden perder paquetes debido a congestión, problemas en la transmisión, etc. Orden de los paquetes. En una red de área extensa los paquetes atraviesan varios nodos para llegar a su destino. En estos nodos los algoritmos de enrutamiento no suelen ser estáticos. De este modo para llegar al mismo destino un paquete puede seguir diferentes rutas, debido a que en un momento dado un nodo puede decidir encaminar el paquete por un sitio y luego por otro. Esto obedece sobre todo a causas derivadas del control de la congestión (el congestionamiento de las líneas influye a la hora de determinar por cual de ellas se envía el paquete) y de la robustez del sistema (si un nodo se cae se encaminan los paquetes por otro sitio). La consecuencia es que dos paquetes que fueron enviados pueden llegar desordenados por haber seguido distintas rutas. 46 CAPÍTULO 2. HERRAMIENTAS DESARROLLO SOFTWARE PARALELO Estos problemas no suceden si las transmisiones se realizan dentro de una red de área local, ya que no es necesario encaminar los paquetes. El protocolo de soporte sobre IP que sigue este esquema se llama UDP (“User Datagram Protocol”, Protocolo de Datagramas de Usuario) y por consiguiente hablaremos de UDP/IP. Comunicaciones Orientadas a Conexión (TCP) Consiste en realizar una conexión entre las dos partes. Tiene la desventaja de que se pierde tiempo en establecer la conexión, pero después la comunicación es fiable, ordenada y utiliza cabeceras más pequeñas. El protocolo de soporte sobre IP que sigue este esquema se denomina TCP (“Transfer Data Protocol”, Protocolo de Transferencia de Datos); hablaremos de flujos TCP/IP debido a que se establece un canal por donde los datos fluyen de manera continua y ordenada. El protocolo TCP es bastante más complejo que UDP e implementa segmentación, lo cual nos permite pasar datos de gran longitud. Dichos datos serán segmentados en partes más pequeñas, enviados conservando el orden y ensamblados en el destino. 2.1.2. Memoria Compartida La memoria compartida es otro modelo de interacción entre los procesadores que forman un sistema paralelo. Muchos sistemas con MultiProcesadores Simétricos (SMP) comparten físicamente una sola memoria entre sus procesadores, de manera que un dato escrito por un procesador en la memoria compartida puede ser accedido directamente por cualquier otro. En los sistemas en los que cada procesador tiene su propia memoria, la memoria compartida puede implementarse lógicamente convirtiendo cada referencia a memoria no local en una comunicación apropiada entre procesadores. Físicamente la memoria compartida puede tener una velocidad de transferencia de datos amplia y una latencia baja, pero sólo cuando los múltiples procesadores no intentan acceder al bus simultáneamente; así, la distribución de datos influye seriamente en el rendimiento, la utilización de la caché, etc. de manera que puede resultar complicado determinar qué distribución de datos es la mejor para un determinado sistema. Normalmente la idea esencial de este modelo consiste en identificar las regiones paralela y secuencial de un programa. La región secuencial se ejecuta en un solo procesador y la paralela en múltiples procesadores. La región paralela consistirá normalmente en múltiples hilos, cada uno ejecutándose de forma concurrente como se ilustra en la figura 2.2. Los hilos son esencialmente procesos “poco pesados” que son planificados de distinta manera a los procesos UNIX y, lo que es más importante, tienen acceso compartido a un mismo mapa de memoria. En muchos programas la identificación de las regiones paralela y secuencial puede ser sencillo, pero en otros es necesario indicar al compilador que realice la paralelización automática, lo cual hasta el momento es un método poco refinado y requiere intervención del programador para la mejora final del código. Aún así los sistemas de memoria compartida suelen ser más sencillos que los de paso de mensajes, y pueden desarrollarse más rápidamente. En todo caso el rendimiento dependerá del compilador que utilicemos. Un compilador para memoria compartida deberá generar código para creación de hilos, sincronización y acceso a los datos en memoria compartida. En comparación un compilador 2.1. MODELOS DE INTERACCIÓN ENTRE PROCESADORES 47 Hilo 1 Hilo 2 Hilo sencillo Hilo sencillo Hilo 3 Hilo 4 Región secuencial Región paralela Región secuencial Figura 2.2: Modelo Básico para Memoria Compartida para paso de mensajes es considerablemente más simple, pues consistirá sólo en un compilador base y las librerías para comunicaciones. La sincronización es un concepto altamente importante en este tipo de sistemas. La compartición de recursos globales está llena de riesgos. Por ejemplo, si dos procesos hacen uso al mismo tiempo de la misma variable global y ambos llevan a cabo tanto lecturas como escrituras sobre la variable, el orden en el que se ejecuten las lecturas y escrituras es crítico. Así surge el concepto de exclusión mutua. Dos o más procesos no pueden acceder a determinados recursos al mismo tiempo. Así, existen varias maneras de conseguir la exclusión mutua. Por un lado podemos hacerlo a nivel hardware, a través de instrucciones máquina especiales o de inhabilitación por interrupciones. También a nivel de sistema operativo, con la utilización de semáforos u otros mecanismos. A nivel de compilador contamos con la ayuda de los monitores. Como vemos existen varios mecanismos para conseguir la sincronización de los procesos en este tipo de sistemas. 2.1.2.1. Todo Compartido Vs. Algo Compartido Hay dos modelos fundamentales usados comúnmente en programación con memoria compartida: Todo Compartido y Algo Compartido. Ambos modelos permiten la comunicación entre procesadores mediante lecturas y escrituras de/en la memoria compartida; la diferencia consiste en que el modelo Todo Compartido ubica todos los datos en memoria compartida, mientras que el modelo Algo Compartido requiere que el usuario indique explícitamente qué datos pueden ser compartidos y cuáles son privados para un único procesador. ¿Qué modelo de datos debemos usar? Lógicamente debemos usar el modelo que mejor se adapte a nuestras necesidades, aunque muchas veces ésto es sólo cuestión de gustos. Mucha gente prefiere el modelo Todo Compartido porque no hay necesidad de identificar qué datos deben ser compartidos en el momento de su declaración... simplemente bloqueamos los accesos potencialmente conflictivos a objetos compartidos para asegurarnos de que sólo un procesador tenga acceso en un momento dado. De nuevo, ésto no es tan simple... por ello mucha gente prefiere la relativa seguridad del modelo Algo Compartido. 48 CAPÍTULO 2. HERRAMIENTAS DESARROLLO SOFTWARE PARALELO Modelo Todo Compartido Lo mejor de este modelo es que fácilmente podemos analizar un programa secuencial existente y convertirlo incrementalmente en un programa paralelo bajo el modelo Todo Compartido. No hay necesidad de determinar qué datos deben ser accesibles por cada uno de los procesadores. En principio el principal problema de este modelo es que cualquier acción ejecutada por un procesador podría afectar a otros procesadores. El problema sale a la luz de dos maneras: Muchas librerías contienen estructuras de datos que simplemente no se pueden compartir. Por ejemplo, una convención UNIX es que la mayoría de las funciones retornan su código de error en una variable llamada errno; si dos procesos bajo el modelo Todo Compartido ejecutan varias llamadas, tendrían conflictos entre ellas porque en realidad comparten la misma variable errno. Aunque ahora existe una versión de la librería que corrige ésto, todavía existen otros muchos problemas similares en la mayoría de las librerías. Por ejemplo, a menos que sean tomadas precauciones especiales la librería X no funcionará si las llamadas son hechas por múltiples procesos bajo el modelo Todo Compartido. Normalmente en cualquier otro modelo de programación el peor caso para un programa con un puntero defectuoso o un error de segmentación es que el proceso que contiene el código erróneo muera. Incluso en este caso el núcleo podría generar un fichero que nos dé una pista de lo sucedido. En procesamiento paralelo bajo el modelo Todo Compartido es común que los accesos perdidos provoquen el fallecimiento de un proceso distinto al que contiene el error, haciendo casi imposible localizar y corregir el error. Ninguno de estos dos tipos de problemas son usuales cuando se emplea el modelo Algo Compartido, debido a que sólo los datos explícitamente marcados son compartidos. Además resulta obvio que el modelo Todo Compartido sólo funciona si todos los procesos tienen exáctamente la misma imagen de la memoria; de este modo no es posible usar el modelo Todo Compartido con diferentes imágenes del código (sólo se puede usar SPMD, no MIMD en general). El soporte más común en programación bajo el modelo Todo Compartido son las librerías de hilos. Los hilos son esencialmente procesos “poco pesados” que podrían no ser planificados de la misma manera que los procesos UNIX y, lo que es más importante, pueden tener acceso compartido a un mismo mapa de memoria. La primera librería que soportó paralelismo en SMP Linux es la ahora algo obsoleta bb_threads, una librería muy pequeña que utiliza la llamada clone() para crear nuevos procesos planificados independientemente que comparten un mismo espacio de direcciones. Los computadores SMP Linux pueden ejecutar múltiples hilos de este tipo en paralelo, ya que en este caso cada hilo es en realidad un proceso Linux completo; la desventaja es que no obtenemos la misma planificación optimizada que realizan otras librerías de hilos. Más recientemente ha sido desarrollada una versión de la librería de hilos POSIX. Esta librería, LinuxThreads, es claramente la librería preferida para ser usada bajo el modelo Todo Compartido bajo SMP Linux. Dicha librería de hilos POSIX está bien documentada. Ahora el principal problema es que la librería tiene muchos detalles por definir y acabar, de modo que LinuxThreads está todavía en proceso de desarrollo. Además tenemos el problema de 2.2. UTILIDADES DE DESARROLLO 49 que la librería POSIX se ha visto involucrada en el proceso de estandarización, de manera que tenemos que ser cuidadosos para no programar utilizando versiones obsoletas del estándar. Modelo Algo Compartido El modelo Algo Compartido en realidad significa “compartir sólo lo necesario”. Este planteamiento puede funcionar en MIMD de manera general (no sólo en SPMD) teniendo en cuenta que debemos ubicar los objetos compartidos en la misma posición de memoria para cada uno de los mapas de memoria de cada procesador. Y lo que es más importante, el modelo Algo Compartido hace más fácil la predicción y puesta a punto del rendimiento, el filtrado de errores, etc. Los únicos problemas son: Puede ser difícil saber de antemano qué necesita realmente ser compartido. La verdadera posición de los objetos en memoria compartida puede ser difícil de establecer, especialmente para los objetos ubicados en una pila. Por ejemplo, puede ser necesario ubicar explícitamente objetos compartidos en un segmento de memoria separado, requiriendo de este modo rutinas de ubicación en memoria separada e introduciendo punteros indirectos extra en cada referencia. Hoy día existen dos mecanismos muy parecidos para permitir a grupos de procesos de Linux tener espacios de memoria independientes, compartiendo sólo un pequeño segmento de memoria. Asumiendo que no hemos excluido el System V IPC en el momento de la configuración de nuestro sistema Linux, tendremos un mecanismo muy portable que ha venido a denominarse System V Shared Memory. La otra alternativa es una utilidad de mapeo de memoria cuya implementación varía ampliamente entre distintos sistemas UNIX: la llamada al sistema mmap(). 2.2. Utilidades de Desarrollo 2.2.1. PVM PVM (“Parallel Virtual Machine”, Máquina Virtual Paralela) es una librería de paso de mensajes libre y portable, generalmente implementada por encima de los sockets. Está claramente establecida como el estándar de paso de mensajes para el procesamiento paralelo en clusters. PVM puede ser utilizado en monoprocesadores, máquinas SMP, así como clusters conectados por una red que soporte sockets (por ejemplo SLIP, PLIP, Ethernet, ATM, etc). De hecho, PVM funciona incluso en grupos de máquinas con diferentes tipos de procesadores, configuraciones, o diferentes tipos de red (clusters heterogéneos). Puede incluso tratar un grupo bastante amplio de máquinas conectadas a Internet como una sóla máquina paralela (una máquina virtual paralela, de ahí su nombre). Lo mejor de todo, PVM es desde hace tiempo una distribución libre (URL [14]), lo que ha llevado a muchos lenguajes de programación, librerías de aplicaciones, herramientas de programación y depuración de errores, etc. a utilizar PVM como su librería portable de paso de mensajes. 50 CAPÍTULO 2. HERRAMIENTAS DESARROLLO SOFTWARE PARALELO El modelo de funcionamiento de PVM es simple pero muy general, y se acomoda a una amplia variedad de estructuras de programas de aplicación. La interfaz de programación es muy sencilla, permitiendo que las estructuras de los programas sencillos sean implementadas de manera intuitiva. El usuario escribe su aplicación como un conjunto de tareas que cooperan. Dichas tareas acceden a los recursos de PVM a través de una librería de funciones con una interfaz estándar. Estas funciones permiten la inicialización y finalización de las tareas a través de la red, así como la comunicación y la sincronización entre dichas tareas. Las operaciones de comunicación utilizan estructuras predefinidas, tanto las encargadas del envío y recepción de datos como aquellas más complejas (sincronización en barrera, suma global, broadcast, etc.). Debido a su portabilidad, su disponibilidad y su simple pero completa interfaz de programación, el sistema PVM ha ganado una amplia aceptación en la comunicad de cálculo científico de alto rendimiento. 2.2.2. MPI MPI (“Message Passing Interface”, Interfaz de Paso de Mensajes) es un estándar que define la sintaxis y la semántica de las funciones contenidas en una librería de paso de mensajes, diseñada para ser usada en programas que exploten la existencia de múltiples procesadores. Aunque la interfaz no cambie, puede implementarse tanto en sistemas que utilicen paso de mensajes, como sistemas de memoria compartida. La primera versión de MPI fué desarrollada en 1993-1994 por un grupo de investigadores al servicio de la industria, el gobierno y el sector académico. Debido al apoyo recibido MPI es relativamente el nuevo estándar para la programación de procesadores paralelos basado en el paso de mensajes, tomando el testigo de PVM. Sin embargo existen frecuentes dudas y discusiones por parte de los usuarios acerca de cuál es el estándar a utilizar. Así pues haremos un resumen de las diferencias más características entre los sistemas más utilizados, PVM y MPI: Entorno de Ejecución: Simplemente PVM tiene uno definido, mientras que MPI no especifica cómo debe ser implementado. Así el inicio de la ejecución de un programa PVM será realizado de manera idéntica en cualquier plataforma, mientras que para MPI depende de la implementación que estemos utilizando. Soporte para Clusters Heterogéneos: PVM fué desarrollado para aprovechar ciclos de CPU de estaciones de trabajo ociosas, por lo que maneja de manera directa mezclas heterogéneas de máquinas y sistemas operativos. En contraste MPI asume que su principal objetivo son los MPPs y los clusters dedicados de estaciones de trabajo casi idénticas. Sin embargo, dependiendo de la implementación que utilicemos, tendremos un soporte más o menos adecuado. Amplitud del Campo de Estudio: PVM evidencia una unidad de propósito que MPI no tiene. Como veremos en el capítulo 3 la especificación MPI-1 centra su atención en el modelo de paso de mensajes, al igual que PVM. Sin embargo la especificación MPI-2 incluye muchas características que van 2.2. UTILIDADES DE DESARROLLO MPI_Send() 51 MPI_Recv() MPI MPI SO SO Figura 2.3: Esquema Básico Utilización MPI más allá del modelo de paso de mensajes, como el acceso paralelo a ficheros de E/S o el acceso a memoria remota, entre otros muchos ejemplos. Diseño de Interfaz de Usuario: MPI fué diseñado después de PVM, y claramente aprendió de él. MPI ofrece una interfaz más clara y eficiente, con manejo de buffers y abstracciones de alto nivel que permiten definir las estructuras de datos a ser transmitidas por los mensajes. Importancia del Estándar: El hecho de que MPI sea respaldado por un estándar formal ampliamente apoyado significa que el uso de MPI es, en muchas instituciones, cuestión de política. MPI no está concebido para ser una infraestructura software aislada y autosuficiente para ser usada en procesamiento distribuido. MPI no necesita planificación de procesos, herramientas de configuración de la plataforma a usar, ni soporte para E/S. Como resultado MPI es implementado normalmente como interfaz de comunicaciones, utilizando las facilidades ofrecidas por el sistema que vayamos a usar, tal como se indica en la figura 2.3 (comunicación vía sockets, operaciones de memoria compartida, etc). Éste escenario es ideal para que los programas PVM sean portados a MPI, de manera que se aproveche el alto rendimiento de comunicación que ofrecen algunas compañías en sus arquitecturas. Dado que éste es el estándar en el que nos basaremos para el desarrollo del presente documento, en el capítulo 3 haremos una descripción más elaborada de sus características. 2.2.3. P4 El sistema p4 es una librería de macros y funciones desarrollada por el Argonne National Laboratory 1 destinadas a la programación de una gran variedad de máquinas paralelas. El sistema p4 soporta tanto el modelo de memoria compartida (basado en monitores) como el modelo de memoria distribuida (basado en paso de mensajes). Para el modelo de memoria compartida, p4 proporciona un conjunto de monitores así como funciones desarrolladas para crearlos y manipularlos; un monitor es un mecanismo de sincronización de procesos en sistemas con memoria compartida. Para el modelo de memoria distribuida, p4 proporciona operaciones de envío y recepción, y un método de administración 1 Desarrollador de la implementación MPICH, apéndice A 52 CAPÍTULO 2. HERRAMIENTAS DESARROLLO SOFTWARE PARALELO de procesos basado en un fichero de configuración que describe la estructura de los grupos y procesos. Dicho fichero especifica el nombre del servidor, el fichero que será ejecutado en cada máquina, el número de procesos que serán iniciados en cada servidor (principalmente para sistemas multiprocesador) e información auxiliar. Un ejemplo de fichero de configuración es: # start one eslave on each of sun2 and sun3 local 0 sun2 1 /home/mylogin/pupgms/sr_test sun3 1 /home/mylogin/pupgms/sr_test Dos cosas son notables en lo que se refiere al mecanismo de administración de los procesos en p4. Primero, existe la noción de procesos maestros y procesos esclavos, y pueden formarse jerarquías multinivel para implementar el denominado modelo de cluster de procesamiento. Segundo, el principal modo de creación de procesos es estático, a través del fichero de configuración; la creación dinámica de procesos es posible sólo mediante procesos creados estáticamente que deben invocar una función p4 que expande un nuevo proceso en la máquina local. A pesar de estas restricciones una gran variedad de paradigmas de aplicación pueden ser implementados en el sistema p4 de una manera bastante sencilla. El paso de mensajes es conseguido en el sistema p4 a través del uso de primitivas send y receive tradicionales, con casi los mismos parámetros que los demás sistemas de paso de mensajes. Algunas variantes han sido proporcionadas por cuestiones semánticas, como el intercambio heterogéneo y las transferencias bloqueantes o no bloqueantes. Sin embargo una proporción significativa de la administración del buffer y su carga se le deja al usuario. Aparte del paso de mensajes básico, p4 ofrece también una variedad de operaciones globales incluyendo broadcast, máximo y mínimo globales, y sincronización por barrera. 2.2.4. Express En contraste con lo que sucede con otros sistemas de procesamiento paralelo descritos en esta sección, Express es una colección de herramientas que individualmente dirigen varios aspectos del procesamiento concurrente. Dicho paquete software es desarrollado y comercializado por Parasoft Corporation, una compañía creada por algunos miembros del proyecto de procesamiento concurrente de Caltech. La filosofía de Express se basa en comenzar con la versión secuencial de un algoritmo y aplicarle un ciclo de desarrollo recomendado, para así llegar a la versión paralela de dicho algoritmo de manera óptima. Los ciclos de desarrollo típicos comienzan con el uso de la herramienta VTOOL, una aplicación gráfica que permite mostrar por pantalla el progreso de los algoritmos secuenciales dinámicamente. Puede mostrar actualizaciones y referencias a estructuras de datos individuales, con la intención de demostrar la estructura de los algoritmos y proporcionar la información necesaria para su paralelización. En relación con este programa tenemos la herramienta FTOOL, que proporciona un análisis en profundidad de los programas. Dicho análisis incluye información acerca del uso de variables, la estructura de flujo de control, etc. FTOOL opera tanto en las versiones secuenciales de los algoritmos como en las paralelas. Una tercera herramienta llamada ASPAR es 2.2. UTILIDADES DE DESARROLLO 53 usada entonces; esta herramienta es un paralelizador automático que convierte programas secuenciales escritos en C o Fortran en sus versiones paralelas o distribuidas, usando los modelos de programación Express. El núcleo del sistema Express es un conjunto de librerías de comunicación, E/S y gráficos paralelos. Las primitivas de comunicación son parecidas a las encontradas en otros sistemas de paso de mensajes e incluyen una variedad de primitivas para realizar operaciones globales y de distribución de datos. Las funciones de E/S extendida permiten la E/S paralela. Un conjunto de funciones similar es proporcionado para mostrar gráficos de múltiples procesos concurrentes. Express contiene además la herramienta NDB, un depurador paralelo que utiliza comandos basados en la popular interfaz dbx. 2.2.5. Linda Linda es un modelo concurrente de programación fruto de la evolución de un proyecto de investigación de la Universidad de Yale. El concepto principal en Linda es el espacio de tuplas, una abstración a través de la cual se comunican los procesos. Este tema central de Linda ha sido propuesto como paradigma alternativo a los dos métodos tradicionales de procesamiento paralelo: el basado en memoria compartida, y el basado en paso de mensajes. El espacio de tuplas es esencialmente una abstracción de la memoria compartida/distribuida, con una diferencia importante: los espacios de tuplas son asociativos. También existen otras distinciones menores. Las aplicaciones utilizan el modelo Linda introduciendo en los programas secuenciales estructuras que manipulan el espacio de tuplas. Desde el punto de vista de las aplicaciones, Linda es un conjunto de extensiones para lenguajes de programación que facilitan la programación paralela. Proporciona una abstracción de la memoria compartida que no requiere la existencia de hardware específico que comparta memoria físicamente. El término Linda se refiere a menudo a implementaciones específicas software que soportan el modelo de programación Linda. Este tipo de software establece y mantiene espacios de tuplas, siendo utilizado en conjunción con librerías que interpretan y ejecutan primitivas Linda. Dependiendo del entorno (mutiprocesadores con memoria compartida, sistemas de paso de mensajes, etc.) el macanismo de espacio de tuplas será implementado utilizando diferentes técnicas con varios grados de eficiencia. Recientemente ha sido propuesto un nuevo esquema relacionado con el proyecto Linda. Este esquema, denominado Pirhana, propone un planteamiento revolucionario en el procesamiento concurrente: los recursos computacionales (vistos como agentes activos) eligen a los procesos basándose en su disponibilidad y conveniencia. Este esquema puede ser implementado en múltiples plataformas y es conocido como Sistema Pirhana o Sistema Linda-Pirhana. 54 CAPÍTULO 2. HERRAMIENTAS DESARROLLO SOFTWARE PARALELO Parte II Guía MPI 55 Capítulo 3 El Estándar MPI MPI (“Message Passing Interface”, Interfaz de Paso de Mensajes) es un estándar que define la sintaxis y la semántica de las funciones contenidas en una librería de paso de mensajes diseñada para ser usada en programas que exploten la existencia de múltiples procesadores. Dicha librería puede ser utilizada por una amplia variedad de usuarios para implementar programas que utilicen paso de mensajes. Desde la finalización de su primera versión en Junio de 1994, MPI ha sido ampliamente aceptado y usado. Existen implementaciones muy estables y eficientes, incluyendo algunas de dominio público. Además dichas implementaciones están disponibles para un amplia variedad de máquinas. Gracias a ello MPI ha alcanzado uno de sus principales objetivos: darle credibilidad al procesamiento paralelo. Ahora las companías y los investigadores tienen una manera sencilla y segura para desarrollar programas paralelos portables de paso de mensajes. 3.1. Origen El paso de mensajes es un paradigma de programación ampliamente utilizado en computadores paralelos, especialmente en aquellas con memoria distribuida. Aunque existen muchas variantes, el concepto básico de procesos comunicándose mediante mensajes está muy consolidado. Durante los años anteriores a la aparición del estándar MPI se percibía un progreso sustancial en proyectar aplicaciones importantes en este paradigma. De hecho, cada compañía había implementado ya su propia variante. Más recientemente algunos sistemas de dominio público habían demostrado que el paso de mensajes podía implementarse eficientemente de manera portable. Figura 3.1: Logo MPI 57 58 CAPÍTULO 3. EL ESTÁNDAR MPI Por todo ello comprendemos que éste era el momento apropiado para definir tanto la sintaxis como la semántica de una librería estándar de funciones que fuera útil a una amplia variedad de usuarios y que pudiera implementarse eficientemente en una amplia gama de computadores. Y de esa idea nació el estándar MPI. Los diseñadores de MPI pretendieron incorporar la mayoría de las características más atractivas de los sistemas existentes de paso de mensajes, antes que seleccionar uno de ellos y adoptarlo como el estándar. Así, en su origen MPI estuvo muy influenciado por el trabajo del Centro de Investigación T.J.Watson de IBM, el sistema NX/2 de Intel, Express, Vertex de nCUBE y PARMACS. Otras contribuciones importantes fueron las realizadas por Zipcode, Chimp, PVM, Chameleon y PICL. Los diseñadores de MPI identificaron algunos defectos críticos de los sistemas de paso de mensajes existentes en áreas como la composición de datos complejos, la modularidad y las comunicaciones seguras. Ésto permitió la introducción de nuevas características en MPI. 3.2. Historia El esfuerzo de estandarización de MPI involucró a unas 60 personas procedentes de 40 organizaciones, principalmente de EEUU y Europa. La mayoría de las principales compañías de computadores paralelos del momento estuvieron involucradas en MPI, así como investigadores provenientes de universidades, laboratorios de gobierno y la industria. El proceso de estandarización comenzó con el Seminario sobre Estándares de Paso de Mensajes en Entornos de Memoria Distribuida, patrocinado por el Centro de Investigaciones sobre Procesamiento Paralelo, mantenido durante los días 29-30 de Abril de 1992 en Williansburg, Virginia (EEUU). En este seminario se discutieron las características esenciales que debía tener una interfaz estándar de paso de mensajes y se estableció un grupo de trabajo para continuar con el proceso de estandarización. Un borrador preliminar, conocido como MPI1, fué propuesto por Dongarra, Hempel, Hey y Walker en Noviembre de 1992 y una versión revisada fué completada en Febrero de 1993. MPI1 abarcaba las características principales que fueron identificadas en el seminario de Williansburg como necesarias en un estándar de paso de mensajes. Dado que fué generado para promover la discusión, este primer borrador se centró principalmente en las comunicaciones punto-a-punto. Aunque tocaba muchos asuntos referentes a la estandarización, no incluía operaciones colectivas ni tenía en cuenta la utilización de hilos. En Noviembre de 1992 una reunión del grupo de trabajo de MPI tuvo lugar en Minneapolis, en la cual se decidió hacer el proceso de estandarización de una manera más formal, adoptando la organización y los procedimientos del “High Performance Fortran Forum” (Foro Fortran de Alto Rendimiento). Fueron formados subcomités para cada una de las principales áreas que componen el estándar, y se estableció un foro de discusión vía e-mail para cada una de ellas. De esta manera quedó conformado el Foro MPI (URL [8]), a cuyo cargo quedaría el proceso de estandarización de MPI. En dicha reunión se propuso el objetivo de presentar un borrador del estándar MPI en el otoño de 1993. Para conseguirlo el Foro MPI mantuvo reuniones cada 6 semanas durante los primeros 9 meses de 1993, presentando el borrador del estándar MPI en la conferencia Supercomputing ’93 en Noviembre de 1993. Después de un período de comentarios públicos, 3.3. OBJETIVOS 59 que resultaron en algunos cambios en MPI, la versión 1.0 de MPI fué presentada en Junio de 1994. A principios de Marzo de 1995 el Foro MPI empezó a reunirse de nuevo para considerar correcciones y extensiones al documento original del estándar MPI. El primer producto fruto de estas deliberaciones fué la versión 1.1 de MPI, presentada en Junio de 1995. En Julio de 1997 aparecen al mismo tiempo la versión 1.2 de MPI y la especificación MPI2. La versión 1.2 de MPI contiene correcciones y clarificaciones referentes a la versión 1.1. Sin embargo MPI-2 añade nuevos elementos al estándar MPI-1, definiendo la versión 2.0 de MPI. Todavía hoy continúan las discusiones acerca de las áreas hacia las cuales podría ser útil expandir el estándar MPI; sin embargo en muchas de ellas es necesario obtener más experiencia y realizar más discusiones. De todo ello se encarga un documento separado, el MPI “Journal Of Development” (Periódico de Desarrollo), que no forma parte de la especificación MPI-2. Y por supuesto el Foro MPI sigue abierto a las aportaciones que puedan realizar los usuarios del estándar, con el objetivo de mantenerlo y desarrollarlo. 3.3. Objetivos La finalidad de MPI, de manera concisa, es desarrollar un estándar para escribir programas de paso de mensajes que sea ampliamente utilizado. Para ello la interfaz debe establecer un estándar práctico, portable, eficiente, escalable, formal y flexible. Objetivos Principales Portabilidad El principal objetivo de MPI, como ocurre en la mayoría de los estándares, es conseguir un alto grado de portabilidad entre diferentes máquinas. Esta portabilidad sería parecida a la que se consigue con lenguajes de programación como Fortran. De esta manera el mismo código fuente de paso de mensajes debería poder ser ejecutado en una variedad de máquinas tan grande como la soportada por las distintas implementaciones de MPI, aunque pueda ser necesaria una puesta a punto para obtener la máxima ventaja de las características de cada sistema. A pesar de que el paso de mensajes muchas veces es considerado algo propio de los sistemas paralelos de memoria distribuida, el mismo código podría ser ejecutado en un sistema paralelo de memoria compartida. Puede ejecutarse en clusters o incluso en un conjunto de procesos ejecutándose en una misma máquina. El hecho de saber que existen implementaciones de MPI eficientes para una amplia variedad de sistemas nos da cierto grado de flexibilidad en el desarrollo del código, la búsqueda de errores y la elección de la plataforma que finalmente utilizaremos. Heterogeneidad Otro tipo de compatibilidad ofrecido por MPI es su capacidad para ejecutarse en sistemas heterogéneos de manera transparente. Así pues, una implementación MPI debe ser capaz de extender una colección de procesos sobre un conjunto de sistemas con arquitecturas diferentes, de manera que proporcione un modelo de computador virtual que oculte las diferencias en 60 CAPÍTULO 3. EL ESTÁNDAR MPI las arquitecturas. De este modo el usuario no se tiene que preocupar de si el código está enviando mensajes entre procesadores de la misma o distinta arquitectura. La implementación MPI hará automáticamente cualquier conversión que sea necesaria y utilizará el protocolo de comunicación adecuado. Sin embargo MPI no prohibe implementaciones destinadas a un único y homogéneo sistema, así como tampoco ordena que distintas implementaciones MPI deban tener la capacidad de interoperar. En definitiva, los usuarios que quieran ejecutar sus programas en sistemas heterogéneos deberán utilizar implementaciones MPI diseñadas para soportar heterogeneidad. Rendimiento La portabilidad es un factor importante, pero el estándar no conseguiría una amplia utilización si se consiguiera dicha portabilidad a expensas del rendimiento. Por ejemplo, el lenguaje Fortran es comúnmente usado por encima de los lenguajes ensambladores porque los compiladores casi siempre ofrecen un rendimiento aceptable comparado con la alternativa no portable que representa el lenguaje ensamblador. Un punto crucial es que MPI fué cuidadosamente diseñado de manera que permite implementaciones eficientes. Las elecciones en el diseño parecen haber sido hechas correctamente, dado que las implementaciones MPI están alcanzando un alto rendimiento en una amplia variedad de plataformas; de hecho el rendimiento alcanzado en dichas implementaciones es comparable al de los sistemas presentados por las compañías, los cuales están diseñados para arquitecturas específicas y tienen menor capacidad de portabilidad. Un objetivo importante de diseño en MPI fué el de permitir implementaciones eficientes para máquinas de diferentes características. Por ejemplo, MPI evita cuidadosamente especificar la manera en que las operaciones tienen lugar. Sólo especifica qué hace una operación lógicamente. Como resultado MPI puede ser fácilmente implementado en sistemas que tienen buffer de mensajes en el proceso emisor, en el receptor, o que no tienen buffers para nada. Las implementaciones pueden beneficiarse de las ventajas que ofrecen los subsistemas de comunicación de varias máquinas a través de sus características específicas. En máquinas con coprocesadores de comunicación inteligentes podemos cargar sobre dichos coprocesadores la mayoría del procesamiento relativo al protocolo de paso de mensajes. En otros sistemas la mayoría del código de comunicación será ejecutada por el procesador principal. Otro ejemplo es el uso de objetos opacos en MPI; por ejemplo los elementos grupo y comunicador son objetos opacos. Desde un punto de vista práctico ésto significa que los detalles de su representación interna dependen de la implementación MPI particular, y como consecuencia el usuario no puede acceder directamente a ellos. En vez de ello el usuario accede a un manejador que referencia al objeto opaco, de manera que dichos objetos opacos son manipulados por funciones MPI especiales. De esta manera cada implementación es libre de hacer lo mejor en determinadas circunstancias. Otra elección de diseño importante para la eficiencia es la manera de evitar el trabajo innecesario. MPI ha sido cuidadosamente diseñado de modo que no sea necesaria demasiada información extra con cada mensaje, ni complejas codificaciones o decodificaciones sobre las cabeceras de dichos mensajes. MPI también evita computaciones extra o tests en funciones críticas que degraden el rendimiento. Otra manera de minimizar el trabajo es reducir la repetición de computaciones previas. MPI proporciona esta capacidad a través de construcciones 3.3. OBJETIVOS 61 como peticiones de comunicación persistente o los atributos de los comunicadores. El diseño de MPI evita la necesidad de operaciones extra de copia o almacenamiento sobre los datos: en la mayoría de los casos los datos son llevados directamente de la memoria de usuario a la red, y son recibidos directamente de la red a la memoria receptora. MPI ha sido diseñado para reducir la sobrecarga en la comunicación producida por el procesamiento, utilizando agentes de comunicación inteligentes y ocultando latencias en la comunicación. Ésto se ha logrado usando llamadas no bloqueantes, que separan el inicio de la comunicación de su final. Escalabilidad La escalabilidad es otro objetivo importante del procesamiento paralelo. La escalabilidad de un sistema es su capacidad para responder a cargas de trabajo crecientes. De este modo los programas MPI deben mantener su nivel de rendimiento aunque incrementemos el número de procesos a ejecutar. MPI permite o soporta la escalabilidad a través de algunas de sus características de diseño. Por ejemplo, una aplicación puede crear subgrupos de procesos que, en turnos, puedan ejecutar operaciones de comunicación colectiva para limitar el número de procesos involucrados. Formalidad MPI, como todos los buenos estándares, es valioso debido a que define el comportamiento de las implementaciones de manera concisa. Esta característica libera al programador de tener que preocuparse de aquellos problemas que puedan aparecer. Un ejemplo de ello es la garantía de seguridad en la transmisión de mensajes que ofrece MPI. Gracias a esta característica el usuario no necesita comprobar si los mensajes son recibidos correctamente. Resumen de Objetivos Diseñar una interfaz para la programación de aplicaciones. La interfaz debe ser diseñada para que pueda ser implementada en la mayoría de las plataformas, sin necesidad de cambios importantes en la comunicación o el software del sistema. Permitir implementaciones que puedan ser utilizadas en entornos heterogéneos. Permitir una comunicación eficiente. Asumir una interfaz de comunicación fiable: el usuario no debe preocuparse por los fallos en la comunicación. La semántica de la interfaz debe ser independiente del lenguaje. La interfaz debe permitir la utilización de hilos. Proporcionar extensiones que añadan más flexibilidad. CAPÍTULO 3. EL ESTÁNDAR MPI 62 3.4. Usuarios El estándar MPI está pensado para ser utilizado por todo aquel que pretenda desarrollar programas de paso de mensajes codificados en Fortran 77 y C. Ésto incluye programadores de aplicaciones individuales, desarrolladores de software para máquinas paralelas y creadores de entornos y herramientas. Para que esta amplia audiencia lo considere atractivo, el estándar debe proporcionar al usuario básico una interfaz simple y fácil de manejar, que no impida el uso de las operaciones de alto rendimiento disponibles en máquinas avanzadas. 3.5. Plataformas El atractivo del paradigma de paso de mensajes proviene al menos en parte de su portabilidad. Los programas expresados de esta manera podrían ser ejecutados en multicomputadores, multiprocesadores o combinaciones de ambos. Además es posible realizar implementaciones basadas en memoria compartida. El paradigma no queda obsoleto al combinar arquitecturas de memoria distribuida con arquitecturas de memoria compartida, o al incrementar la velocidad de la red. Debe ser a la vez posible y útil implementar dicho estándar en una gran variedad de máquinas, incluyendo dichas “máquinas” que se componen de colecciones de otras máquinas, paralelas o no, conectadas por una red de comunicaciones. La interfaz es apropiada para su uso en programas MIMD y SPMD. Aunque no proporciona un soporte explícito para la ejecución de hilos la interfaz ha sido diseñada de manera que no perjudique su uso. MPI proporciona muchas características encaminadas a mejorar el rendimiento en computadores paralelos con hardware de comunicación especializado entre procesadores. De este modo es posible realizar implementaciones nativas MPI de alto rendimiento para este tipo de máquinas. Al mismo tiempo existen implementaciones MPI que utilizan los protocolos estándares de comunicación entre procesadores de Unix, las cuales proporcionan portabilidad a los clusters y redes heterogéneas. 3.6. Versiones El estándar MPI se divide básicamente en dos especificaciones, MPI-1 y MPI-2. La siguiente clasificación muestra el nivel de compatibilidad con MPI que posee una implementación dada: Mantener compatibilidad con la especificación MPI-1 significa ser compatible con la versión 1.2 de MPI. Mantener compatibilidad con la especificación MPI-2 significa proporcionar toda la funcionalidad definida por la especificación MPI-2. En todo caso la compatibilidad hacia atrás está preservada. De esta manera un programa MPI1.1 válido será un programa MPI-1.2 válido y un programa MPI-2 válido; un programa MPI1.2 válido será un programa MPI-2 válido. 3.6. VERSIONES 63 3.6.1. MPI-1 Las versiones 1.0, 1.1 y 1.2 del estándar MPI se engloban en la especificación MPI-1. Dicha especificación centra su atención en el modelo de paso de mensajes. A continuación mostramos una lista con los elementos contenidos en dicha especificación, y aquellos que quedan fuera de ella. Elementos Incluidos Comunicaciones punto a punto Operaciones colectivas Grupos de procesos Contextos de comunicación Topologías de procesos Administración del entorno Enlaces con Fortran 77 y C Interfaz para la creación de perfiles de ejecución Elementos No Incluidos Operaciones de memoria compartida Operaciones ya soportadas por el sistema operativo de manera estandarizada durante la adopción de MPI; por ejemplo, manejadores de interrupción o ejecución remota Herramientas para la construcción de programas Facilidades para la depuración de errores Soporte específico para hilos Soporte para planificación y creación de procesos Funciones de E/S paralela Como vemos MPI-1 está diseñado para aprovechar todas las facilidades ofrecidas por el sistema que vayamos a usar, tanto aquellas pertenecientes al sistema de comunicación (operaciones de memoria compartida, comunicación vía sockets, etc.) como las relativas al entorno de programación (herramientas para la construcción de programas, facilidades para la depuración de errores, etc). Como resultado MPI es implementado normalmente como interfaz de comunicaciones, utilizando dichas facilidades ofrecidas por el sistema. Existen muchas características que fueron consideradas y no se incluyeron en MPI-1. Ésto ocurrió por algunas razones: las restricciones de tiempo que se propuso el Foro MPI en acabar la especificación; el sentimiento de no tener suficiente experiencia en algunos de los CAPÍTULO 3. EL ESTÁNDAR MPI 64 campos; y la preocupación de que el añadir más características podría retrasar la aparición de implementaciones. De todas maneras las características no incluidas siempre pueden ser añadidas como extensiones en implementaciones específicas, como es el caso de la extensión MPE dentro de la implementación MPICH (sección A.7). 3.6.2. MPI-2 La especificación MPI-2 es básicamente una extensión a MPI-1 que añade nuevos elementos al estándar. La versión 2.0 de MPI pertenece a la especificación MPI-2. Entre las nuevas funcionalidades que se añaden destacan las siguientes: Administración y Creación de Procesos Comunicaciones Unilaterales E/S Paralela Operaciones Colectivas Extendidas Enlaces con Fortran 90 y C++ La razón por la cual se creó la especificación MPI-2 fué la demanda por parte de los usuarios y desarrolladores de nuevas características en el estándar. De todos modos MPI-2 sólo añade nuevos elementos a MPI-1, pero no lo modifica. 3.7. Implementaciones Desde que se completó la primera versión del estándar en 1994 un gran número de implementaciones MPI han sido puestas a disposición de los usuarios. Esto incluye tanto algunas implementaciones portables e independientes como aquellas que han sido desarrolladas y optimizadas por las principales compañías de computadores paralelos. La alta calidad de dichas implementaciones ha sido un punto clave en el éxito de MPI. A continuación destacamos las tres implementaciones de dominio público más importantes que pueden ser utilizadas en clusters de sistemas Linux: MPICH (URL [9]) es sin duda la implementación más importante de MPI. Muchas implementaciones comerciales desarrolladas por las grandes compañías de computadores paralelos se basan en ella. La primera versión de MPICH fué escrita durante el proceso de estandarización de MPI, siendo finalmente presentada al mismo tiempo que la versión 1.0 de MPI. De hecho las experiencias de los autores de MPICH se convirtieron en una gran ayuda para el Foro MPI en el proceso de desarrollo del estándar. Su portabilidad es enorme y su rendimiento elevado. Posee compatibilidad total con MPI-1 e implementa muchos elementos de MPI-2. En el apéndice A se analizan de manera detenida sus características y se informa sobre su instalación, configuración y manejo. LAM (URL [6]) fué desarrollada originalmente en el Centro de Supercómputo de Ohio antes de que el estándar MPI fuera presentado. Cuando MPI apareció, LAM adoptó 3.7. IMPLEMENTACIONES 65 el estándar. LAM no sólo consiste en la librería MPI, si no que además contiene herramientas de depuración y monitorización. Ha sido optimizada para funcionar con clusters heterogéneos de sistemas Unix; sin embargo es muy portable, siendo utilizada en una amplia variedad de plataformas Unix, desde estaciones de trabajo a supercomputadores. LAM posee compatibilidad total con MPI-1 e implementa muchos elementos de MPI-2. CHIMP (URL [1]) fué desarrollada en el Centro de Cómputo Paralelo de Edimburgo. Como LAM, CHIMP comenzó como una infraestructura independiente de paso de mensajes que luego evolucionó hacia una implementación MPI. CHIMP es conocida principalmente por haber sido utilizada como versión optimizada de MPI para los CRAY T3D y T3E. CHIMP es portable, pudiendo ser utilizada en estaciones de trabajo de Sun, SGI, DEC, IBM y HP, en plataformas Meiko y en el Fujitsu AP 1000. 66 CAPÍTULO 3. EL ESTÁNDAR MPI Capítulo 4 Conceptos Básicos Todos los programas MPI comparten una serie de características. La inicialización y finalización del entorno de ejecución se llevan a cabo mediante funciones que explicaremos en este capítulo. También analizaremos los métodos para identificar los procesos en ejecución. Por otro lado estudiaremos funciones para informarnos de dónde está ubicado cada proceso y el momento temporal en que nos encontramos, y explicaremos el problema de la E/S en procesadores paralelos. Para ejemplificar todo ésto implementaremos un sencillo programa que envía un mensaje de saludo e indica el número de procesos en ejecución. 4.1. Algoritmo ¡Hola Mundo! Basándonos en el primer programa que muchos de nosotros vimos en C expondremos una variante que hará uso de múltiples procesos. En MPI los procesos implicados en la ejecución de un programa paralelo se identifican por una secuencia de enteros no negativos. Si hay C procesos ejecutando un programa, éstos tendrán los identificadores DFE:1)EBGBGBG@EHC/I1 . El siguiente programa hace que el proceso 0 (encargado de la E/S, sección 4.4) imprima un mensaje de saludo e informe del número de procesos en ejecución, así como del tiempo empleado en el procesamiento. Los detalles sobre la compilación y ejecución de este programa dependen del sistema que estemos usando. De este modo debemos conocer cómo compilar y ejecutar programas paralelos que usen MPI. En la sección A.5 explicamos cómo compilarlo bajo la implementación MPICH, y en la sección A.6 se habla sobre la ejecución de programas MPI. Cuando el programa es compilado y ejecutado con dos procesos, la salida debería ser de la siguiente manera: Proceso 0 en linux.local Encargado de la E/S ¡Hola Mundo! Numero Procesos: 2 Tiempo Procesamiento: 0.000065 Si lo ejecutamos con cuatro procesos la salida sería: Proceso 0 en linux.local Encargado de la E/S 67 CAPÍTULO 4. CONCEPTOS BÁSICOS 68 ¡Hola Mundo! Numero Procesos: 4 Tiempo Procesamiento: 0.034216 Aunque los detalles de qué ocurre cuando el programa es ejecutado varían de una máquina a otra, lo esencial es lo mismo en todos los sistemas, a condición de que ejecutemos un proceso en cada procesador. 1. El usuario manda una directiva al sistema operativo que crea una copia del programa ejecutable en cada procesador. 2. Cada procesador comienza la ejecución de su copia del ejecutable. 3. Diferentes procesos pueden ejecutar diferentes instrucciones bifurcando el programa a través de condicionantes. Dichos condicionantes suelen basarse, como veremos, en el identificador del proceso. 4.2. Programas MPI en General Todos los programas escritos en MPI deben contener la directiva de preprocesador #include “mpi.h” El fichero ‘mpi.h’ contiene las definiciones, macros y prototipos de función necesarios para compilar los programas MPI. Antes de que podamos llamar a cualquier otra función MPI debemos hacer una llamada a MPI_Init(); esta función sólo debe ser llamada una vez. Sus argumentos son punteros a los parámetros de la función main(), argc y argv. Dicha función permite al sistema hacer todas la configuraciones necesarias para que la librería MPI pueda ser usada. Después de que el programa haya acabado de utilizar la librería MPI debemos hacer una llamada a MPI_Finalize(). Esta función limpia todos los trabajos no finalizados dejados por MPI (por ejemplo, envíos pendientes que no hayan sido completados, etc.). Así, un programa MPI típico tiene la siguiente composición: . . . #include “mpi.h” . . . main(int argc, char** argv){ . . . /*Ninguna llamada a función MPI anterior a esta*/ MPI_Init(&argc, &argv); . . . MPI_Finalize(); /*Ninguna llamada a función MPI posterior a esta*/ . . . }/*main*/ 4.3. INFORMÁNDONOS DEL RESTO DEL MUNDO 69 4.3. Informándonos del Resto del Mundo MPI ofrece la función MPI_Comm_rank(), la cual retorna el identificador de un proceso en su segundo argumento. Su sintaxis es: int MPI_Comm_rank(MPI_Comm comunicador, int* identificador) El primer argumento es el comunicador. Esencialmente un comunicador es una colección de procesos que pueden enviarse mensajes entre sí. Normalmente para diseñar programas básicos el único comunicador que necesitaremos será MPI_COMM_WORLD. Está predefinido en MPI y consiste en todos los procesos que se ejecutan cuando el programa comienza. Muchas de las construcciones que empleamos en nuestros programas dependen también del número de procesos que se ejecutan. MPI ofrece la función MPI_Comm_size() para determinar dicho número de procesos. Su primer argumento es el comunicador. En el segundo argumento retorna el número de procesos pertenecientes a dicho comunicador. Su sintaxis es: int MPI_Comm_size(MPI_Comm comunicador, int* numprocs) 4.4. El Problema de la Entrada/Salida En nuestros algoritmos asumimos que el proceso 0 puede escribir en la salida estándar (la ventana del terminal). Prácticamente todos los procesadores paralelos proporcionan este método de E/S. De hecho la mayoría de ellos permiten a todos los procesos tanto leer de la entrada estándar como escribir en la salida estándar. Sin embargo la dificultad aparece cuando varios procesos están intentando ejecutar operaciones de E/S simultáneamente. Para comprender ésto expondremos un ejemplo. Supongamos que diseñamos un programa de manera que cada proceso intente leer los valores , J y K añadiendo la siguiente instrucción: scanf(“ %d %d %d”, &a, &b, &c); Además supongamos que ejecutamos el programa con dos procesos y que el usuario introduce: 10 20 30 ¿Qué ocurre? ¿Obtienen ambos procesos los datos? ¿Lo hace uno sólo? O lo que es peor, ¿obtiene el proceso 0 el número 10 y el 20 mientras que el proceso 1 obtiene el 30? Si todos los procesos obtienen los datos, ¿qué ocurre cuando escribimos un programa en el que queremos que el proceso 0 obtenga el primer valor de entrada, el proceso 1 el segundo, etc.? Y si sólo un proceso obtiene los datos, ¿qué le ocurre a los demás? ¿Es razonable hacer que múltiples procesos lean de una sola terminal? Por otra parte, ¿qué ocurre si varios procesos intentan escribir datos en la terminal simultáneamente? ¿Se imprimirán antes los datos del proceso 0, después los del proceso 1, y así sucesivamente? ¿O se imprimirán dichos datos aleatoriamente? ¿O, lo que es peor, se CAPÍTULO 4. CONCEPTOS BÁSICOS 70 mostrarán los datos de los distintos procesos todos mezclados (una línea del proceso 0, dos caracteres del proceso 1, 3 caracteres del 0, 2 líneas del 2, etc.)? Las operaciones estándar de E/S en C no proporcionan soluciones simples a dicho problema; así, la E/S sigue siendo objeto de investigación por parte de la comunidad de procesamiento paralelo. Por todo ello asumiremos que el proceso 0 puede al menos escribir en la salida estándar. También asumiremos que puede leer de la entrada estándar. En todos los casos asumiremos que sólo el proceso 0 puede interactuar con la E/S. Ésto podría parecer una debilidad, ya que como mencionamos antes la mayoría de las máquinas paralelas permiten que múltiples procesos interactuen con la E/S. Sin embargo es un método muy sólido que nos permitirá realizar un manejo limpio de la E/S en nuestros programas. 4.5. Ubicación de los Procesos La función MPI_Get_processor_name() nos permite conocer el nombre del procesador donde está ubicado cada proceso. Ésto puede ser útil para monitorizar nuestros programas en redes heterogéneas. Conocer en qué máquina concreta se está ejecutando un proceso específico puede ser determinante para explicar su comportamiento, para lo cual podemos ayudarnos con las herramientas de monitorización (sección 9.1). La sintaxis de dicha función es la siguiente: int MPI_Get_processor_name(char* nombre, int* longnombre) El parámetro nombre es una cadena (vector de caracteres) cuyo tamaño debe ser al menos igual a la constante MPI_MAX_PROCESSOR_NAME. En dicho vector quedará almacenado el nombre del procesador. El parámetro longnombre es otro parámetro de salida que nos informa de la longitud de la cadena obtenida. 4.6. Información Temporal Con el objeto de facilitar el control y la monitorización, MPI nos ofrece funciones encaminadas a proporcionar información temporal sobre la ejecución de nuestros programas. La función MPI_Wtime() nos informa sobre el tiempo transcurrido en un determinado proceso. Dicha función no tiene parámetros; su valor de retorno es de tipo double y representa el tiempo transcurrido en segundos desde el comienzo de la ejecución del proceso. Si el atributo MPI_WTIME_IS_GLOBAL está definido y su valor es true (por defecto es así) el valor devuelto por la función estará sincronizado con todos los procesos pertenecientes al comunicador MPI_COMM_WORLD. Nosotros utilizaremos esta función para ofrecer cierta información al usuario sobre el tiempo de ejecución de nuestro programa por la salida estándar. Nos permitirá en estos casos diferenciar entre el tiempo de ejecución de un programa y el tiempo de procesamiento. Definimos el tiempo de ejecución de un programa como el tiempo que emplea para resolver un problema en un computador paralelo, desde el momento en que es generado el primer proceso hasta que acaba el último. Este tiempo no debe ser calculado con la función 4.7. IMPLEMENTACIÓN ALGORITMO ¡HOLA MUNDO! 71 MPI_Wtime() debido a que dicha función forma parte del entorno MPI, el cual debe ser finalizado antes de que el programa termine. Por lo tanto el tiempo que arrojaría es incorrecto, siempre menor al que debería; en su lugar usaremos las herramientas de monitorización para determinar el tiempo de ejecución. Por otro lado llamamos tiempo de procesamiento al tiempo que emplea el computador en calcular el resultado, eliminando el tiempo de inicialización y finalización de los procesos, y la comunicación con el usuario. De esta manera si el usuario tarda diez segundos en introducir los datos para un problema, ese tiempo no será añadido al tiempo de procesamiento pero sí al tiempo de ejecución. La función MPI_Wtime() nos ayudará a calcular el tiempo de procesamiento de un programa para después mostrarlo al usuario por la salida estándar. En el caso del algoritmo ¡Hola Mundo! el tiempo de procesamiento no es indicativo. Sin embargo es muy útil en programas donde un proceso (normalmente el proceso 0) desencadena la ejecución de los demás (normalmente al distribuir entre todos los procesos los datos que el usuario introduce por la entrada estándar). Éste será el caso más común entre los algoritmos expuestos en el presente documento. 4.7. Implementación Algoritmo ¡Hola Mundo! A continuación exponemos el código del algoritmo ¡Hola Mundo!. Como vemos dicho programa utiliza el paradigma SPMD (“Same Program Multiple Data”, Programa Único y Flujo de Datos Múltiple). Obtenemos el efecto de diferentes programas ejecutándose en distintos procesadores bifurcando la ejecución del programa. Para ello nos basamos en el identificador del proceso: las instrucciones ejecutadas por el proceso 0 son distintas de las ejecutadas por otros procesos, aunque todos los procesos estén ejecutando el mismo programa. Éste es el método más común para escribir programas MIMD. Algoritmo 4.1: ¡Hola Mundo! 1 2 3 4 /********************************************************/ /*Hola Mundo: CODIGO PROGRAMA QUE SALUDA */ /********************************************************/ 5 6 7 8 # define TAMCADENA 100 # include " mpi . h " # include < s t d i o . h> 9 10 11 12 13 14 15 16 17 18 19 20 21 /**************/ /*FUNCION MAIN*/ /**************/ i n t main ( i n t argc , char argv ) { int id ; /*IDENTIFICADOR DEL PROCESO*/ i n t numprocs ; /*NUMERO DE PROCESOS*/ char nombreproc [MPI_MAX_PROCESSOR_NAME] ; /*NOMBRE PROCESADOR*/ i n t lnombreproc ; /*LONGITUD NOMBRE PROCESADOR*/ double t m p i n i c = 0 . 0 ; /*TIEMPO INICIO DE LA EJECUCION*/ double t m p f i n ; /*TIEMPO FINAL DE LA EJECUCION*/ LML CAPÍTULO 4. CONCEPTOS BÁSICOS 72 22 /*INICIALIZAMOS EL ENTRORNO DE EJECUCION MPI*/ M P I _ I n i t (& argc ,& argv ) ; 23 24 25 /*ALMACENAMOS EL IDENTIFICADOR DEL PROCESO*/ MPI_Comm_rank (MPI_COMM_WORLD,& i d ) ; 26 27 28 /*ALMACENAMOS EL NUMERO DE PROCESOS*/ MPI_Comm_size (MPI_COMM_WORLD, & numprocs ) ; 29 30 31 /*E/S:NOMBRE DEL PROCESADOR,PROCESO 0*/ MPI_Get_processor_name ( nombreproc,& lnombreproc ) ; i f ( i d ==0){ f p r i n t f ( s t d o u t , " \ n P r o c e s o %d en %s E n c a r g a d o de l a E / S \ n \ n " , i d , nombreproc ) ; } 32 33 34 35 36 37 38 /*TIEMPO INICIAL DE LA EJECUCION */ t m p i n i c=MPI_Wtime ( ) ; 39 40 41 /*E/S:PROCESAMIENTO*/ i f ( i d ==0){ f p r i n t f ( s t d o u t , " ¡ H o l a Mundo ! \ n \ n " ) ; } 42 43 44 45 46 /*TIEMPO FINAL DE LA EJECUCION*/ t m p f i n =MPI_Wtime ( ) ; 47 48 49 /*E/S:INFORMACION SOBRE LA EJECUCION*/ i f ( i d ==0){ f p r i n t f ( s t d o u t , " Numero P r o c e s o s : %d \ n " , numprocs ) ; f p r i n t f ( s t d o u t , " Tiempo P r o c e s a m i e n t o : % f \ n \ n " , t m p f i n t m p i n i c ) ; } 50 51 N 52 53 54 55 /*FINALIZAMOS EL ENTRORNO DE EJECUCION MPI*/ MPI_Finalize ( ) ; return 0 ; 56 57 58 59 } Capítulo 5 Paso de Mensajes El paso de mensajes es quizás la función más importante y característica del estándar MPI. Se utiliza básicamente para el intercambio de datos entre los procesos en ejecución. En este capítulo estudiaremos las características más importantes de los mensajes bloqueantes y no bloqueantes, aplicando dicha teoría a la implementación de un algoritmo diseñado para el cálculo de áreas circulares. 5.1. Algoritmo Cálculo de Áreas mediante Montecarlo El método de Montecarlo es un método numérico que permite resolver problemas matemáticos mediante la simulación de variables aleatorias. Utilizaremos dicho método para aproximar el área de un círculo con un radio determinado. Supongamos que queremos calcular el área de un círculo de radio O . Para hacerlo tomaremos un cuadrado de lado (@O y lo pondremos alrededor del círculo de radio O , de manera que quede circunscrito. A continuación generaremos un número de puntos aleatorios dentro del área del cuadrado. Algunos caerán dentro del área del círculo y otros no, como se expone en la figura 5.1. Tomaremos 6 como el número de puntos aleatorios generados dentro del cuadrado y como de puntos que caen dentro del círculo. Si sabemos que el área del círculo es P O y elquenúmero el área del cuadrado es ,Q(@OM3 , tendremos la siguiente igualdad: P O Q, (@OM3 6 Con lo cual llegamos fácilmente a que: P P SR * 6 Utilizando dicha aproximación de y sabiendo el radio del círculo aproximaremos fáP cilmente su área mediante O . Lógicamente cuantas más muestras tomemos, o lo que es lo P mismo, cuantos más puntos generemos, mejor será la aproximación de . También interviene mucho la aleatoriedad de las muestras; cuanto más aleatorias sean, mejor. La cuestión ahora es determinar los puntos que caen dentro del círculo y los que caen fuera. Para explicarlo observemos la figura 5.2. Dado un punto ,.T'EU-3 podremos saber si ha caído dentro del círculo mediante la regla de Pitágoras: 73 CAPÍTULO 5. PASO DE MENSAJES 74 Figura 5.1: Generación Puntos Aleatorios V T XW U Si la hipotenusa es mayor que el radio del círculo querrá decir que el punto está fuera de dicho círculo. Si la hipotenusa es menor que el radio, consideraremos que está dentro. Como podemos observar en la figura, también podemos generar puntos en sólo una cuarta parte del cuadrado y el círculo, de manera que la proporción no queda alterada. Así lo hacemos en la implementación del algoritmo Cálculo de Áreas mediante Montecarlo (sección 5.6). 5.2. El Entorno del Mensaje El paso de mensajes bloqueantes se lleva a cabo en nuestros programas por las funciones MPI_Send() y MPI_Recv() principalmente. La primera función envía un mensaje a un proceso determinado. La segunda recibe un mensaje de un proceso. Éstas son las funciones más básicas de paso de mensajes en MPI. Para que el mensaje sea comunicado con éxito, el sistema debe adjuntar alguna información a los datos que el programa intenta transmitir. Esta información adicional conforma el entorno del mensaje. En MPI el entorno contiene la siguiente información: 1. El identificador del proceso receptor del mensaje. 2. El identificador del proceso emisor del mensaje. 3. Una etiqueta. 4. Un comunicador. Estos datos pueden ser usados por el proceso receptor para distinguir entre los mensajes entrantes. El origen puede ser usado para distinguir mensajes recibidos por distintos procesos. 5.2. EL ENTORNO DEL MENSAJE 75 (x,y) h y x Figura 5.2: Cálculo Hipotenusa Punto Aleatorio La etiqueta es un int especificado por el usuario que puede ser usado para distinguir mensajes recibidos por un único proceso. Por ejemplo, supongamos que el proceso está enviando dos mensajes al proceso Y ; ambos mensajes contienen un número flotante. Uno de los flotantes se emplea en un cálculo, mientras que otro debe ser impreso. Para determinar cuál es cuál, puede usar etiquetas diferentes para cada mensaje. Si Y usa las mismas etiquetas que en las recepciones correspondientes, “sabrá” qué hacer con ellas. MPI garantiza que los enteros dentro del intervalo 0-32767 pueden ser usados como etiquetas. La mayoría de las implementaciones permiten valores mucho mayores. Como dijimos antes un comunicador es básicamente una colección de procesos que pueden mandarse mensajes entre sí. La importancia de los comunicadores se acentúa cuando los módulos de un programa han sido escritos independientemente de los demás. Por ejemplo, supongamos que queremos resolver un sistema de ecuaciones diferenciales y, en el transcurso de resolverlo, tenemos que resolver un sistema de ecuaciones lineales. Mejor que escribir la resolución del sistema de ecuaciones lineales desde el principio, podríamos usar una librería de funciones escrita por otra persona y optimizada para el sistema que estamos usando. ¿Có mo evitamos que se confundan los mensajes que nosotros enviamos entre los procesos y Y con los mensajes enviados en la librería de funciones? Sin la ventaja de los comunicadores probablemente haríamos una partición del rango de las posibles etiquetas, haciendo que parte de ellas sólo puedan ser usadas por la librería de funciones. Ésto es tedioso y puede causarnos problemas si ejecutamos el programa en otro sistema: puede que la librería del otro sistema no haga uso del mismo rango de etiquetas. Con la ventaja de los comunicadores, simplemente creamos un comunicador para uso exclusivo en la resolución del sistema de ecuaciones lineales, y se lo pasamos a la librería encargada de hacerlo como argumento en la llamada. Comentaremos los detalles más adelante. Por ahora continuaremos utilizando el comunicador MPI_COMM_WORLD, el cual consiste en todos los procesos que se ejecutan cuando el programa comienza. CAPÍTULO 5. PASO DE MENSAJES 76 Tipo de Datos MPI MPI_CHAR MPI_SHORT MPI_INT MPI_LONG MPI_UNSIGNED_CHAR MPI_UNSIGNED_SHORT MPI_UNSIGNED MPI_UNSIGNED_LONG MPI_FLOAT MPI_DOUBLE MPI_LONG_DOUBLE MPI_BYTE MPI_PACKED Tipo de Datos C signed char signed short int signed int signed long int unsigned char unsigned short int unsigned int unsigned long int float double long double Cuadro 5.1: Tipos de Datos MPI 5.3. Funciones de Paso de Mensajes Bloqueantes Para resumir detallamos la sintaxis de las dos funciones más importantes de paso de mensajes bloqueantes: int MPI_Send(void* mensaje, int contador, MPI_Datatype tipo_datos, int destino, int etiqueta, MPI_Comm comunicador) int MPI_Recv(void* mensaje, int contador, MPI_Datatype tipo_datos, int origen, int etiqueta, MPI_Comm comunicador, MPI_Status* status) Al igual que la mayoría de las funciones de la biblioteca estándar de C, la mayoría de las funciones MPI retornan un entero como código de error. Sin embargo, como la mayoría de los programadores de C, ignoraremos esos valores de retorno en casi todos los casos. Los contenidos del mensaje son almacenados en un bloque de memoria referenciado por el argumento mensaje. Los siguientes dos argumentos, contador y tipo_datos, permiten al sistema identificar el final del mensaje: éste contiene una secuencia de contador valores, cada uno del tipo MPI datatype. Este tipo no es un tipo de C, aunque la mayoría de los tipos predefinidos corresponden a tipos de C. En el cuadro 5.1 se listan los tipos predefinidos de MPI con sus correspondientes tipos de C, si éstos existen. Los últimos dos tipos, MPI_BYTE y MPI_PACKED, no corresponden con tipos estándar de C. El tipo MPI_BYTE puede ser usado si queremos forzar que el sistema no realice conversión alguna entre las distintas representaciones de los datos (por ejemplo, en una red heterogénea de estaciones de trabajo que utilicen una representación de datos distinta). 5.4. FUNCIONES DE PASO DE MENSAJES NO BLOQUEANTES 77 Notar que la cantidad de espacio reservado para el buffer de entrada no tiene porqué coincidir con la cantidad exacta de espacio que ocupa el mensaje que estemos recibiendo. Por ejemplo, podría darse el caso de que el tamaño del mensaje que el proceso 1 envía al proceso 0 sea de 28 caracteres (strlen(mensaje)+1), aunque el proceso 0 reciba el mensaje en un buffer que tiene capacidad para 100 caracteres. Ésto tiene sentido. En general el proceso receptor no conoce el tamaño exacto del mensaje que se le está enviando. MPI permite recibir mensajes tan largos como capacidad reservada tengamos. Si no tenemos suficiente capacidad, tendremos un error de desbordamiento. Los argumentos destino y origen son los identificadores del proceso receptor y del proceso emisor, respectivamente. MPI permite que el argumento origen sea un comodín. Existe una constante predefinida llamada MPI_ANY_SOURCE que puede ser usada si un proceso está preparado para recibir un mensaje procedente de cualquier proceso. No existe comodín para destino. Como dijimos anteriormente MPI contiene dos mecanismos diseñados específicamente para “particionar el espacio de los mensajes”: las etiquetas y los comunicadores. Los argumentos etiqueta y comunicador son, respectivamente, la etiqueta y el comunicador. El argumento etiqueta es un int y, por ahora, nuestro único comunicador es MPI_COMM_WORLD, el cual, como comentamos antes, está predefinido en todos los sistemas MPI y consiste en todos los procesos que se ejecutan cuando el programa comienza. Existe un comodín, MPI_ANY_TAG, que puede ser usado en MPI_Recv() para la etiqueta. No existe un comod ín para el comunicador. En otras palabras, para que el proceso mande un mensaje al proceso Y el argumento comunicador que usa en MPI_Send() debe ser idéntico al argumento que Y usa en MPI_Recv(). El último argumento de MPI_Recv(), status, retorna información acerca de los datos que hemos recibido. Referencia a un registro con dos campos, uno para el origen y otro para la etiqueta. Por ejemplo, si el origen era MPI_ANY_SOURCE en la llamada a MPI_Recv(), el argumento status contendrá el identificador del proceso que envió el mensaje. 5.4. Funciones de Paso de Mensajes No Bloqueantes El rendimiento de los sistemas de paso de mensajes puede ser mejorado mediante el solapamiento entre comunicación y procesamiento en los algoritmos. Un método para realizar ésto es mediante la comunicación no bloqueante. Las funciones de envío de mensajes no bloqueantes inician el envío del mensaje, pero no lo completan; para ello se necesita una llamada a la función de finalización de envíos no bloqueantes. De la misma manera una llamada a una función de recepción de mensajes no bloqueantes no detiene la ejecución del algoritmo hasta que dicho mensaje sea realmente recibido, como ocurre con las versiones bloqueantes. De este modo podemos solapar comunicación y procesamiento en nuestros algoritmos. Las versiones no bloqueantes de las funciones de paso de mensajes son: int MPI_Isend(void* mensaje, int contador, MPI_Datatype tipo_datos, int destino, int etiqueta, MPI_Comm comunicador, MPI_Request* peticion) CAPÍTULO 5. PASO DE MENSAJES 78 int MPI_Irecv(void* mensaje, int contador, MPI_Datatype tipo_datos, int origen, int etiqueta, MPI_Comm comunicador, MPI_Request* peticion) A simple vista la diferencia entre estas funciones y sus versiones bloqueantes es la existencia del argumento peticion en sus llamadas. Este argumento es utilizado por las funciones de finalización de transmisiones bloqueantes para su ejecución. Las funciones de finalización de transmisiones bloqueantes son: int MPI_Wait(MPI_Request* peticion, MPI_Status* status) int MPI_Waitany(int contador, MPI_Request* vector_peticiones, int* indice, MPI_Status* status) int MPI_Waitall(int contador, MPI_Request* vector_peticiones, MPI_Status* status) La función MPI_Wait() espera a que se complete un envío o una recepción de un mensaje. Para ello le pasamos como argumento de entrada el argumento de salida que nos proporciona la función de paso de mensajes no bloqueantes en el momento de la llamada. El argumento de salida status nos proporciona información acerca de los datos que hemos recibido. Por su parte MPI_Waitany() espera a que se complete cualquiera de las peticiones que le pasamos en vector_peticiones, devolviendo en el argumento indice la posición de la petición satisfecha. MPI_Waitall() espera a que se completen todas las peticiones que le pasamos en vector_peticiones. 5.5. Agrupaciones de Datos Con la generación de máquinas actual el envío de mensajes es una operación costosa. Según esta regla, cuantos menos mensajes enviemos mejor será el rendimiento general del algoritmo. Así pues en el algoritmo Cálculo de Áreas mediante Montecarlo podríamos mejorar el rendimiento si distribuimos los datos de entrada en un solo mensaje entre los procesadores. Recordemos que las funciones de paso de mensajes tienen dos argumentos llamados contador y tipo_datos. Estos dos parámetros permiten al usuario agrupar datos que tengan el mismo tipo básico en un único mensaje. Para usarlo dichos datos deben estar almacenados en direcciones de memoria contiguas. Debido a que el lenguaje C garantiza que los elementos de un vector están almacenados en direcciones de memoria contiguas, si queremos enviar los elementos de un vector, o un subconjunto de ellos, podremos hacerlo en un solo mensaje. Desafortunadamente esto no nos ayuda en el algoritmo Cálculo de Áreas mediante Montecarlo si queremos enviar los datos de entrada a cada proceso en un solo mensaje. Ello es 5.5. AGRUPACIONES DE DATOS 79 debido a que las variables que contienen el radio y el número de muestras no tienen porqué estar alojadas en direcciones de memoria contiguas. Tampoco debemos almacenar estos datos en un vector debido a que ello restaría claridad y elegancia al código. Para lograr nuestro objetivo utilizaremos una facilidad de MPI para la agrupación de los datos. 5.5.1. Tipos Derivados Una opción razonable para conseguir nuestro objetivo podría ser almacenar el radio y el número de muestras en una estructura con dos miembros (un flotante largo y un entero largo) como se muestra a continuación: typedef struct{ long double radio; long nmuestras_local; } Tipo_Datos_Entrada; e intentar utilizar el tipo de datos de la estructura como argumento tipo_datos en las funciones de paso de mensajes. El problema aquí es que el tipo de datos de la estructura no es un tipo de datos MPI; necesitamos construir un tipo de datos MPI a partir del tipo de datos de C. MPI propone una solución a ésto permitiendo al usuario construir tipos de datos MPI en tiempo de ejecución. Para construir un tipo de datos MPI básicamente se especifica la distribución de los datos en el tipo (los tipos de los miembros y sus direcciones relativas de memoria). Un tipo de datos MPI construido de este modo se denomina tipo de datos derivado. Para ver cómo funciona ésto expondremos la función que construye dicho tipo de datos. void construir_tipo_derivado(Tipo_Datos_Entrada* pdatos, MPI_Datatype* pMPI_Tipo_Datos){ MPI_Datatype tipos[2]; int longitudes[2]; MPI_Aint direcc[3]; MPI_Aint desplaz[2]; /*PRIMERO ESPECIFICAMOS LOS TIPOS*/ tipos[0]=MPI_LONG_DOUBLE; tipos[1]=MPI_LONG; /*ESPECIFICAMOS EL NUMERO DE ELEMENTOS DE CADA TIPO*/ longitudes[0]=1; longitudes[1]=1; /*CALCULAMOS LOS DESPLAZAMIENTOS DE LOS MIEMBROS DE LA ESTRUCTURA RELATIVOS AL COMIENZO DE DICHA ESTRUCTURA*/ MPI_Address(pdatos,&direcc[0]); MPI_Address(&(pdatos->radio),&direcc[1]); MPI_Address(&(pdatos->nmuestras_local),&direcc[2]); desplaz[0]=direcc[1]-direcc[0]; CAPÍTULO 5. PASO DE MENSAJES 80 desplaz[1]=direcc[2]-direcc[0]; /*CREACION TIPO DATOS DERIVADO*/ MPI_Type_struct(2,longitudes,desplaz,tipos,pMPI_Tipo_Datos); /*CERTIFICARLO DE MANERA QUE PUEDA SER USADO*/ MPI_Type_commit(pMPI_Tipo_Datos); }/*construir_tipo_derivado*/ Las primeras dos sentencias especifican los tipos de los miembros del tipo de datos derivado, y las dos siguientes especifican el número de elementos de cada tipo. La función MPI_Address() nos ayuda a calcular los desplazamientos de cada uno de los miembros con respecto a la dirección inicial del primero. Con esta información ya sabemos los tipos, los tamaños y las direcciones relativas de memoria de cada uno de los miembros del tipo de datos de C, y por lo tanto ya podemos definir el tipo de datos MPI derivado del tipo de datos de C. Ésto se hace llamando a las funciones MPI_Type_struct() y MPI_Type_Commit(). El tipo de datos MPI creado puede ser usado en cualquiera de las funciones de comunicación de MPI. Para usarlo simplemente usaremos como primer argumento de las funciones la dirección inicial de la estructura a enviar, y el tipo de datos MPI creado en el argumento tipo_datos. 5.5.2. Vectores La función MPI_Type_vector() crea un tipo derivado consistente en contador elementos. int MPI_Type_vector(int contador, int longitud_bloque, int salto, MPI_Datatype tipo_datos_elem, MPI_Datatype* nuevo_tipo) Cada elemento contiene longitud_bloque elementos de tipo tipo_datos_elem. El argumento salto es el número de elementos de tipo tipo_datos_elem que hay entre los sucesivos elementos de nuevo_tipo. 5.6. Implementación Cálculo de Áreas mediante Montecarlo A continuación exponemos las versiones bloqueante y no bloqueante del algoritmo Cálculos de Áreas mediante Montecarlo. 5.6.1. Implementación con Mensajes Bloqueantes 5.6. IMPLEMENTACIÓN CÁLCULO DE ÁREAS MEDIANTE MONTECARLO 81 Algoritmo 5.1: Cálculo de Áreas mediante Montecarlo (Bloqueante) 1 2 3 4 5 /***************************************************/ /*Calculo de Areas: CODIGO APROXIMACION DE AREAS */ /*CIRCULARES MEDIANTE MONTECARLO (BLOQUEANTE) */ /***************************************************/ 6 7 8 9 10 # # # # include < s t d i o . h> include < s t d l i b . h> include < math . h> include " mpi . h " 11 12 13 14 15 16 /*DECLARACION ESTRUCTURA DATOS ENTRADA*/ typedef s t r u c t { long double r a d i o ; long n m u e s t r a s _ l o c a l ; } Tipo_Datos_Entrada ; 17 18 19 20 21 22 23 24 25 26 /*DECLARACION DE LA FUNCIONES QUE VAMOS A UTILIZAR*/ long double MonteCarlo ( i n t i d , long double r a d i o , long l o c a l _ n ) ; void o b t e n e r _dat os ( long double p r a d i o , long pnmuestras_global ) ; void d i s t r i b u i r _ d a t o s ( i n t i d , i n t numprocs , long double p r a d i o , long p n m u e s t r a s _ l oc a l ) ; void r e c o l e c t a r _ d a t o s ( i n t i d , i n t numprocs , long double p a p r o x _ l o c a l , long double p a p r o x _ g l o b a l ) ; void c o n s t r u i r _ t i p o _ d e r i v a d o ( Tipo_Datos_Entrada pdatos , MPI_Datatype pMPI_Tipo_Datos ) ; L L L L L L L L 27 28 29 30 31 /**************/ /*FUNCION MAIN*/ /**************/ i n t main ( i n t argc , char LML argv ) { 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 long double PI25DIGITOS=3.141592653589793238462643; /*VALOR PI PARA CALCULAR EL ERROR*/ int id ; /*IDENTIFICADOR DEL PROCESO*/ i n t numprocs ; /*NUMERO DE PROCESOS*/ char nombreproc [MPI_MAX_PROCESSOR_NAME] ; /*NOMBRE PROCESADOR*/ i n t lnombreproc ; /*LONGITUD NOMBRE PROCESADOR*/ long double r a d i o ; /*RADIO CIRCUNFERENCIA*/ long n m u e s t r a s _global ; /*NUMERO DE MUESTRAS*/ long n m u e s t r a s _ l o c a l ; /*NUMERO DE MUESTRAS DE CADA PROCESO*/ long double a p r o x _ l o c a l ; /*APROX LOCAL DE CADA PROCESO*/ long double a p r o x _ g l o b a l ; /*APROX GENERAL (MEDIA DE TODAS LAS LOCALES)*/ int raiz =0; /*PROCESO QUE RECIBE LAS LOCALES Y LAS SUMA*/ double t m p i n i c = 0 . 0 ; /*TIEMPO INICIO DE LA EJECUCION*/ double t m p f i n ; /*TIEMPO FINAL DE LA EJECUCION */ 48 49 50 51 int etiqueta =50; MPI_Status s t a t u s ; int origen ; /*ETIQUETA MENSAJES DE PRUEBA*/ /*STATUS RECEPCION MENSAJES DE PRUEBA*/ /*PROCESO ORIGEN MENSAJES DE PRUEBA*/ 52 53 54 /*INICIALIZAMOS EL ENTRORNO DE EJECUCION MPI*/ CAPÍTULO 5. PASO DE MENSAJES 82 55 M P I _ I n i t (& argc ,& argv ) ; 56 57 58 /*ALMACENAMOS EL IDENTIFICADOR DEL PROCESO*/ MPI_Comm_rank (MPI_COMM_WORLD,& i d ) ; 59 60 61 /*ALMACENAMOS EL NUMERO DE PROCESOS*/ MPI_Comm_size (MPI_COMM_WORLD,& numprocs ) ; 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 /*E/S:NOMBRE DEL PROCESADOR,PROCESADOR 0*/ MPI_Get_processor_name ( nombreproc,& lnombreproc ) ; i f ( i d ==0){ f p r i n t f ( s t d o u t , " \ n P r o c e s o %d en %s E n c a r g a d o de l a E / S \ n " , i d , nombreproc ) ; } /*E/S:NOMBRE DEL PROCESADOR,TODOS LOS PROCESOS (PRUEBAS)*/ /*if(id==0){ * for (origen=1;origen<numprocs;origen++){ * MPI_Recv(nombreproc,MPI_MAX_PROCESSOR_NAME,MPI_CHAR,origen, * etiqueta,MPI_COMM_WORLD,&status); * fprintf(stdout,"Proceso %d en %s\n",origen,nombreproc); * } *} *else{ * MPI_Send(nombreproc,MPI_MAX_PROCESSOR_NAME,MPI_CHAR,0, * etiqueta,MPI_COMM_WORLD); *} */ 82 83 84 85 86 87 88 89 90 /*OBTENEMOS DATOS INTRODUCIDOS POR EL USUARIO*/ i f ( i d ==0){ o b t e n e r _datos(& r a d i o ,& n m u e s t r a s _ global ) ; /*NUMERO DE MUESTRAS PARA CADA PROCESO*/ n m u e s t r a s _ l o c a l=n m u e s t r a s _ global / numprocs ; /*TRUNCAMOS NUMERO DE MUESTRAS GLOBAL*/ n m u e s t r a s _gl obal= n m u e s t r a s _ l o c a l numprocs ; } L 91 92 93 94 /*TIEMPO INICIAL DE PROCESAMIENTO*/ i f ( i d ==0){ f p r i n t f ( stdout , " Procesando . . . \ n \ n " ) ; } t m p i n i c=MPI_Wtime ( ) ; 95 96 97 /*DISTRIBUIMOS DATOS INTRODUCIDOS POR EL USUARIO*/ d i s t r i b u i r _ d a t o s ( i d , numprocs ,& r a d i o ,& n m u e s t r a s _ l o c a l ) ; 98 99 100 /*CALCULAMOS LA INTEGRAL PARCIAL DEL PROCESO*/ a p r o x _ l o c a l = MonteCarlo ( i d , r a d i o , n m u e s t r a s _ l o c a l ) ; 101 102 103 /*RECOLECTAMOS AREAS CALCULADAS POR LOS PROCESOS Y HACEMOS LA MEDIA*/ r e c o l e c t a r _ d a t o s ( i d , numprocs ,& a p r o x _ l o c a l ,& a p r o x _ g l o b a l ) ; 104 105 106 /*TIEMPO FINAL DE PROCESAMIENTO*/ t m p f i n =MPI_Wtime ( ) ; 107 108 109 /*E/S:IMPRIMIMOS RESULTADO Y TIEMPO DE PROCESAMIENTO*/ i f ( i d ==0){ 5.6. IMPLEMENTACIÓN CÁLCULO DE ÁREAS MEDIANTE MONTECARLO f p r i n t f ( s t d o u t , " Con n= %d m u e s t r a s , l a e s t i m a c i o n d e l a r e a de l a \ n " , n m u e s t r a s _global ) ; f p r i n t f ( s t d o u t , " de l a c i r c u n f e r e n c i a con r a d i o %Lf e s \ n " , r a d i o ) ; f p r i n t f ( s t d o u t , " %.60 Lf \ n " , a p r o x _ g l o b a l ) ; f p r i n t f ( s t d o u t , " E r r o r < %f \ n \ n " , f a b s ( PI25DIGITOS r a d i o r a d i o a p r o x _ g l o b a l ) ) ; f p r i n t f ( s t d o u t , " Numero P r o c e s o s : %d \ n " , numprocs ) ; f p r i n t f ( s t d o u t , " Tiempo P r o c e s a m i e n t o : % f \ n \ n " , t m p f i n t m p i n i c ) ; 110 111 112 113 L 114 115 116 117 118 L N N } 119 120 121 /*FINALIZAMOS EL ENTRORNO DE EJECUCION MPI*/ MPI_Finalize ( ) ; 122 123 } /*main*/ 124 125 126 127 128 129 130 /***************************************************************/ /*FUNCION obtener_datos:PROCESO 0 CONSULTA AL USUARIO EL RADIO */ /*DE LA CIRCUNFERENCIA Y EL NUMERO DE MUESTRAS GLOBAL */ /***************************************************************/ void o b t e n e r _dat os ( long double p r a d i o , long pnmuestras_global ) { L L 131 132 /*E/S:PETICION DE DATOS*/ 133 134 135 136 137 f p r i n t f ( s t d o u t , " I n t r o d u z c a r a d i o de l a c i r c u n f e r e n c i a \ n " ) ; s c a n f ( " %Lf " , p r a d i o ) ; f p r i n t f ( s t d o u t , " I n t r o d u z c a numero de m u e s t r a s \ n " ) ; s c a n f ( " %l d " , pnmuestras_global ) ; 138 139 140 141 /*E/S SIN PETICION:UTILIZAR EN CASO DE PRUEBAS*/ /**pradio=1.0;*pnmuestras_global=200000000;*/ 142 143 } /*obtener_datos*/ 144 145 146 147 148 149 150 151 /**********************************************************************/ /*FUNCION distribuir_datos:PROCESO 0 DISTIBUYE RADIO DE LA */ /*CIRCUNFERENCIA Y EL NUMERO DE MUESTRAS (n) ENTRE LOS DEMAS PROCESOS */ /**********************************************************************/ void d i s t r i b u i r _ d a t o s ( i n t i d , i n t numprocs , long double p r a d i o , long p n m u e s t r a s _ l oc a l ) { L L 152 153 154 155 156 157 158 MPI_Datatype MPI_Tipo_Datos_Entrada ; Tipo_Datos_Entrada d a t o s _ e n tr ada ; int origen =0; /*PROCESO ORIGEN MENSAJES*/ int destino ; /*PROCESO DESTINO DATOS*/ int etiqueta ; /*ETIQUETA MENSAJES*/ MPI_Status s t a t u s ; /*STATUS RECEPCION MENSAJES*/ 159 160 161 c o n s t r u i r _ t i p o _ d e r i v a d o (& d a t o s _ e n tr ada,& MPI_Tipo_Datos_Entrada ) ; 162 163 164 i f ( i d ==0){ /*ENVIO DE LOS DATOS*/ 83 CAPÍTULO 5. PASO DE MENSAJES 84 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 L d a t o s _ e nt rada . r a d i o = p r a d i o ; d a t o s _ e nt rada . n m u e s t r a s _ l o c a l = p n m u e s t r a s _ l o c al ; f o r ( d e s t i n o =1; d e s t i n o <numprocs ; d e s t i n o + + ) { e t i q u e t a =30; MPI_Send(& d a t o s _ e nt rada , 1 , MPI_Tipo_Datos_Entrada , d e s t i n o , e t i q u e t a ,MPI_COMM_WORLD ) ; } L } else { /*RECEPCION DE LOS DATOS*/ e t i q u e t a =30; MPI_Recv(& d a t o s _ e n tra da , 1 , MPI_Tipo_Datos_Entrada , o r i g e n , e t i q u e t a ,MPI_COMM_WORLD,& s t a t u s ) ; p r a d i o= d a t o s _ e ntr ada . r a d i o ; p n m u e s t r a s _ l o c al= d a t o s _ e nt rada . n m u e s t r a s _ l o c a l ; } } /*distribuir_datos*/ L L 182 183 184 185 186 187 188 /************************************************************************/ /*FUNCION MonteCarlo: RETORNA EL AREA DE LA CIRCUNFERENCIA CON RADIO */ /*radio UTILIZANDO EL NUM.MUESTRAS nummuestras MEDIANTE MONTECARLO */ /************************************************************************/ long double MonteCarlo ( i n t i d , long double r a d i o , long nummuestras ) { 189 190 191 192 193 194 195 196 197 long long long long long long long long double p i ; double area ; double x ; double y ; double h i p ; aciertos =0; fallos =1; i ; /*VBLE QUE ALMACENA NUMERO PI*/ /*AREA CIRCUNFERENCIA*/ /*EJE ORDENADAS MUESTRA ACTUAL*/ /*EJE COORDENADAS MUESTRA ACTUAL*/ /*HIPOTENUSA MUESTRA ACTUAL*/ /*NUM.MUESTRAS QUE CAEN DENTRO DE LA CIRCUNFERENCIA*/ /*NUM.MUESTRAS QUE CAEN FUERA DE LA CIRCUNFERENCIA*/ /*CONTADOR*/ 198 199 200 201 /*UTILIZAMOS COMO SEMILLA EL IDENTIFICADOR DEL PROCESO*/ srand ( i d ) ; 202 203 204 205 206 207 208 209 210 211 /*ESTIMACION DE PI*/ f o r ( i =0; i ! = nummuestras ; i + + ) { x =( long double ) rand ( ) / RAND_MAX; y =( long double ) rand ( ) / RAND_MAX; h i p = s q r t ( pow ( x , 2 ) +pow ( y , 2 ) ) ; i f ( hip >1){ f a l l o s ++;} else { a c i e r t o s + + ; } } p i = 4 . 0 ( ( long double ) a c i e r t o s / nummuestras ) ; L 212 213 214 /*CALCULAMOS AREA*/ area=pow ( r a d i o , 2 ) p i ; L 215 216 r e t u r n area ; 217 218 219 } /*MonteCarlo*/ 5.6. IMPLEMENTACIÓN CÁLCULO DE ÁREAS MEDIANTE MONTECARLO 220 221 222 223 224 225 226 /*********************************************************************/ /*FUNCION recolectar_datos:PROCESO 0 HACE LA MEDIA DE LAS */ /*APROXIMACIONES LOCALES */ /*********************************************************************/ void r e c o l e c t a r _ d a t o s ( i n t i d , i n t numprocs , long double p a p r o x _ l o c a l , long double p a p r o x _ g l o b a l ) { L L 227 228 229 230 231 232 int origen ; int destino =0; int etiqueta ; MPI_Status s t a t u s ; long double b u f f e r ; /*PROCESO ORIGEN MENSAJES*/ /*PROCESO DESTINO DATOS*/ /*ETIQUETA MENSAJES*/ /*STATUS RECEPCION MENSAJES*/ /*BUFFER RECEPCION APROXIMACIONES LOCALES*/ 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 i f ( i d ==0){ /*RECOLECTAMOS LOS CALCULOS LOCALES Y HACEMOS LA MEDIA*/ p a p r o x _ g l o b a l= p a p r o x _ l o c a l ; f o r ( o r i g e n =1; o r i g e n<numprocs ; o r i g e n + + ) { e t i q u e t a =30+ o r i g e n ; MPI_Recv(& b u f f e r , 1 , MPI_LONG_DOUBLE, o r i g e n , e t i q u e t a , MPI_COMM_WORLD,& s t a t u s ) ; p a p r o x _ g l o b a l =( p a p r o x _ g l o b a l+ b u f f e r ) / 2 . 0 ; } } else { /*ENVIO DE LOS DATOS*/ e t i q u e t a =30+ i d ; MPI_Send ( p a p r o x _ l o c a l , 1 , MPI_LONG_DOUBLE, d e s t i n o , e t i q u e t a , MPI_COMM_WORLD ) ; } } /*distribuir_datos*/ L L L L 251 252 253 254 255 256 257 258 /*********************************************************************/ /*FUNCION construir_tipo_derivado: CONSTRUYE EL TIPO DE DATOS MPI */ /*NECESARIO PARA DISTRIBUIR LOS DATOS DE ENTRADA EMPAQUETADOS */ /*********************************************************************/ void c o n s t r u i r _ t i p o _ d e r i v a d o ( Tipo_Datos_Entrada pdatos , MPI_Datatype pMPI_Tipo_Datos ) { L L 259 260 261 262 263 MPI_Datatype t i p o s [ 2 ] ; int longitudes [ 2 ] ; MPI_Aint d i r e c c [ 3 ] ; MPI_Aint desplaz [ 2 ] ; 264 265 266 267 268 /*PRIMERO ESPECIFICAMOS LOS TIPOS*/ t i p o s [ 0 ] =MPI_LONG_DOUBLE; t i p o s [ 1 ] =MPI_LONG; 269 270 271 272 /*ESPECIFICAMOS EL NUMERO DE ELEMENTOS DE CADA TIPO*/ longitudes [0]=1; longitudes [1]=1; 273 274 /*CALCULAMOS LOS DESPLAZAMIENTOS DE LOS MIEMBROS DE LA ESTRUCTURA 85 CAPÍTULO 5. PASO DE MENSAJES 86 RELATIVOS AL COMIENZO DE DICHA ESTRUCTURA*/ MPI_Address ( pdatos ,& d i r e c c [ 0 ] ) ; MPI_Address ( & ( pdatos >r a d i o ) , & d i r e c c [ 1 ] ) ; MPI_Address ( & ( pdatos >n m u e s t r a s _ l o c a l ) , & d i r e c c [ 2 ] ) ; desplaz [ 0 ] = d i r e c c [1] d i r e c c [ 0 ] ; desplaz [ 1 ] = d i r e c c [2] d i r e c c [ 0 ] ; 275 N 276 277 N N 278 N 279 280 281 /*CREACION TIPO DATOS DERIVADO*/ MPI_Type_struct ( 2 , l o n g i t u d e s , desplaz , t i p o s , pMPI_Tipo_Datos ) ; 282 283 284 /*CERTIFICARLO DE MANERA QUE PUEDA SER USADO*/ MPI_Type_commit ( pMPI_Tipo_Datos ) ; 285 286 287 288 } /*construir_tipo_derivado*/ 5.6.2. Implementación con Mensajes No Bloqueantes Algoritmo 5.2: Cálculo de Áreas mediante Montecarlo (No Bloqueante) 1 2 3 4 5 /***************************************************/ /*Calculo de Areas: CODIGO APROXIMACION DE AREAS */ /*CIRCULARES MEDIANTE MONTECARLO (NO BLOQUEANTE) */ /***************************************************/ 6 7 8 /*NUMERO MAXIMO DE PROCESOS EN EJECUCION */ # define MAX_NUM_PROCS 128 9 10 11 12 13 # # # # include < s t d i o . h> include < s t d l i b . h> include < math . h> include " mpi . h " 14 15 16 17 18 19 /*DECLARACION ESTRUCTURA DATOS ENTRADA*/ typedef s t r u c t { long double r a d i o ; long n m u e s t r a s _ l o c a l ; } Tipo_Datos_Entrada ; 20 21 22 23 24 25 26 27 28 29 /*DECLARACION DE LA FUNCIONES QUE VAMOS A UTILIZAR*/ long double MonteCarlo ( i n t i d , long double r a d i o , long l o c a l _ n ) ; void o b t e n e r _dat os ( long double p r a d i o , long pnmuestras_global ) ; void d i s t r i b u i r _ d a t o s ( i n t i d , i n t numprocs , long double p r a d i o , long p n m u e s t r a s _ l oc a l ) ; void r e c o l e c t a r _ d a t o s ( i n t i d , i n t numprocs , long double p a p r o x _ l o c a l , long double p a p r o x _ g l o b a l ) ; void c o n s t r u i r _ t i p o _ d e r i v a d o ( Tipo_Datos_Entrada pdatos , MPI_Datatype pMPI_Tipo_Datos ) ; L 32 33 34 L L L L L 30 31 L /**************/ /*FUNCION MAIN*/ /**************/ i n t main ( i n t argc , char L8L argv ) { L 5.6. IMPLEMENTACIÓN CÁLCULO DE ÁREAS MEDIANTE MONTECARLO 87 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 long double PI25DIGITOS=3.141592653589793238462643; /*VALOR PI PARA CALCULAR EL ERROR*/ int id ; /*IDENTIFICADOR DEL PROCESO*/ i n t numprocs ; /*NUMERO DE PROCESOS*/ char nombreproc [MPI_MAX_PROCESSOR_NAME] ; /*NOMBRE PROCESADOR*/ i n t lnombreproc ; /*LONGITUD NOMBRE PROCESADOR*/ long double r a d i o ; /*RADIO CIRCUNFERENCIA*/ long n m u e s t r a s _global ; /*NUMERO DE MUESTRAS*/ long n m u e s t r a s _ l o c a l ; /*NUMERO DE MUESTRAS DE CADA PROCESO*/ long double a p r o x _ l o c a l ; /*APROX LOCAL DE CADA PROCESO*/ long double a p r o x _ g l o b a l ; /*APROX GENERAL (MEDIA DE TODAS LAS LOCALES)*/ int raiz =0; /*PROCESO QUE RECIBE LAS LOCALES Y LAS SUMA*/ double t m p i n i c = 0 . 0 ; /*TIEMPO INICIO DE LA EJECUCION*/ double t m p f i n ; /*TIEMPO FINAL DE LA EJECUCION */ 51 52 53 54 int etiqueta =50; MPI_Status s t a t u s ; int origen ; /*ETIQUETA MENSAJES DE PRUEBA*/ /*STATUS RECEPCION MENSAJES DE PRUEBA*/ /*PROCESO ORIGEN MENSAJES DE PRUEBA*/ 55 56 57 58 /*INICIALIZAMOS EL ENTRORNO DE EJECUCION MPI*/ M P I _ I n i t (& argc ,& argv ) ; 59 60 61 /*ALMACENAMOS EL IDENTIFICADOR DEL PROCESO*/ MPI_Comm_rank (MPI_COMM_WORLD,& i d ) ; 62 63 64 /*ALMACENAMOS EL NUMERO DE PROCESOS*/ MPI_Comm_size (MPI_COMM_WORLD,& numprocs ) ; 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 /*E/S:NOMBRE DEL PROCESADOR,PROCESADOR 0*/ MPI_Get_processor_name ( nombreproc,& lnombreproc ) ; i f ( i d ==0){ f p r i n t f ( s t d o u t , " \ n P r o c e s o %d en %s E n c a r g a d o de l a E / S \ n " , i d , nombreproc ) ; } /*E/S:NOMBRE DEL PROCESADOR,TODOS LOS PROCESOS (PRUEBAS)*/ /*if(id==0){ * for (origen=1;origen<numprocs;origen++){ * MPI_Recv(nombreproc,MPI_MAX_PROCESSOR_NAME,MPI_CHAR,origen, * etiqueta,MPI_COMM_WORLD,&status); * fprintf(stdout,"Proceso %d en %s\n",origen,nombreproc); * } *} *else{ * MPI_Send(nombreproc,MPI_MAX_PROCESSOR_NAME,MPI_CHAR,0, * etiqueta,MPI_COMM_WORLD); *} */ 85 86 87 88 89 /*OBTENEMOS DATOS INTRODUCIDOS POR EL USUARIO*/ i f ( i d ==0){ o b t e n e r _dato s(& r a d i o ,& n m u e s t r a s _ global ) ; /*NUMERO DE MUESTRAS PARA CADA PROCESO*/ CAPÍTULO 5. PASO DE MENSAJES 88 n m u e s t r a s _ l o c a l=n m u e s t r a s _ global / numprocs ; /*TRUNCAMOS NUMERO DE MUESTRAS GLOBAL*/ n m u e s t r a s _gl obal= n m u e s t r a s _ l o c a l numprocs ; 90 91 92 93 L } 94 95 96 97 /*TIEMPO INICIAL DE PROCESAMIENTO*/ i f ( i d ==0){ f p r i n t f ( stdout , " \ nProcesando . . . \ n \ n " ) ; } t m p i n i c=MPI_Wtime ( ) ; 98 99 100 /*DISTRIBUIMOS DATOS INTRODUCIDOS POR EL USUARIO*/ d i s t r i b u i r _ d a t o s ( i d , numprocs ,& r a d i o ,& n m u e s t r a s _ l o c a l ) ; 101 102 103 /*CALCULAMOS LA INTEGRAL PARCIAL DEL PROCESO*/ a p r o x _ l o c a l = MonteCarlo ( i d , r a d i o , n m u e s t r a s _ l o c a l ) ; 104 105 106 /*RECOLECTAMOS AREAS CALCULADAS POR LOS PROCESOS Y HACEMOS LA MEDIA*/ r e c o l e c t a r _ d a t o s ( i d , numprocs ,& a p r o x _ l o c a l ,& a p r o x _ g l o b a l ) ; 107 108 109 /*TIEMPO FINAL DE PROCESAMIENTO*/ t m p f i n =MPI_Wtime ( ) ; 110 111 112 113 114 115 116 117 118 119 120 121 /*E/S:IMPRIMIMOS RESULTADO Y TIEMPO DE PROCESAMIENTO*/ i f ( i d ==0){ f p r i n t f ( s t d o u t , " Con n= %d m u e s t r a s , l a e s t i m a c i o n d e l a r e a de l a \ n " , n m u e s t r a s _global ) ; f p r i n t f ( s t d o u t , " de l a c i r c u n f e r e n c i a con r a d i o %Lf e s \ n " , r a d i o ) ; f p r i n t f ( s t d o u t , " %.60 Lf \ n " , a p r o x _ g l o b a l ) ; f p r i n t f ( s t d o u t , " E r r o r < %f \ n \ n " , f a b s ( PI25DIGITOS r a d i o r a d i o a p r o x _ g l o b a l ) ) ; f p r i n t f ( s t d o u t , " Numero P r o c e s o s : %d \ n " , numprocs ) ; f p r i n t f ( s t d o u t , " Tiempo P r o c e s a m i e n t o : % f \ n \ n " , t m p f i n t m p i n i c ) ; } L L N N 122 123 124 /*FINALIZAMOS EL ENTRORNO DE EJECUCION MPI*/ MPI_Finalize ( ) ; 125 126 } /*main*/ 127 128 129 130 131 132 133 /***************************************************************/ /*FUNCION obtener_datos:PROCESO 0 CONSULTA AL USUARIO EL RADIO */ /*DE LA CIRCUNFERENCIA Y EL NUMERO DE MUESTRAS GLOBAL */ /***************************************************************/ void o b t e n e r _dat os ( long double p r a d i o , long pnmuestras_global ) { L L 134 135 /*E/S:PETICION DE DATOS*/ 136 137 138 139 140 f p r i n t f ( s t d o u t , " I n t r o d u z c a r a d i o de l a c i r c u n f e r e n c i a \ n " ) ; s c a n f ( " %Lf " , p r a d i o ) ; f p r i n t f ( s t d o u t , " I n t r o d u z c a numero de m u e s t r a s \ n " ) ; s c a n f ( " %l d " , pnmuestras_global ) ; 141 142 143 144 /*E/S SIN PETICION:UTILIZAR EN CASO DE PRUEBAS*/ /**pradio=1.0;*pnmuestras_global=200000000;*/ 5.6. IMPLEMENTACIÓN CÁLCULO DE ÁREAS MEDIANTE MONTECARLO 89 145 146 } /*obtener_datos*/ 147 148 149 150 151 152 153 154 /**********************************************************************/ /*FUNCION distribuir_datos:PROCESO 0 DISTIBUYE RADIO DE LA */ /*CIRCUNFERENCIA Y EL NUMERO DE MUESTRAS (n) ENTRE LOS DEMAS PROCESOS */ /**********************************************************************/ void d i s t r i b u i r _ d a t o s ( i n t i d , i n t numprocs , long double p r a d i o , long p n m u e s t r a s _ l oc a l ) { L L 155 156 157 158 159 160 161 162 MPI_Datatype MPI_Tipo_Datos_Entrada ; Tipo_Datos_Entrada d a t o s _ e n tr ada ; int origen =0; /*PROCESO ORIGEN MENSAJES*/ int destino ; /*PROCESO DESTINO DATOS*/ int etiqueta ; /*ETIQUETA MENSAJES*/ MPI_Request r e q u e s t ; /*PETICION MENSAJES NO BLOQUEANTES*/ MPI_Status s t a t u s ; /*STATUS RECEPCION*/ 163 164 c o n s t r u i r _ t i p o _ d e r i v a d o (& d a t o s _ e n tr ada,& MPI_Tipo_Datos_Entrada ) ; 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 i f ( i d ==0){ /*ENVIO DE LOS DATOS*/ d a t o s _ e n trada . r a d i o = p r a d i o ; d a t o s _ e n trada . n m u e s t r a s _ l o c a l = p n m u e s t r a s _ l o c al ; f o r ( d e s t i n o =1; d e s t i n o <numprocs ; d e s t i n o + + ) { e t i q u e t a =30; MPI_Isend(& d a t o s _ e ntr ada , 1 , MPI_Tipo_Datos_Entrada , d e s t i n o , e t i q u e t a , MPI_COMM_WORLD,& r e q u e s t ) ; } } else { /*RECEPCION DE LOS DATOS*/ e t i q u e t a =30; MPI_Irecv (& d a t o s _ e n trada , 1 , MPI_Tipo_Datos_Entrada , o r i g e n , e t i q u e t a , MPI_COMM_WORLD,& r e q u e s t ) ; MPI_Wait(& r e q u e s t ,& s t a t u s ) ; p r a d i o= d a t o s _ e ntr ada . r a d i o ; p n m u e s t r a s _ l o c al= d a t o s _ e n trada . n m u e s t r a s _ l o c a l ; } } /*distribuir_datos*/ L L L L 186 187 188 189 190 191 192 /************************************************************************/ /*FUNCION MonteCarlo: RETORNA EL AREA DE LA CIRCUNFERENCIA CON RADIO */ /*radio UTILIZANDO EL NUM.MUESTRAS nummuestras MEDIANTE MONTECARLO */ /************************************************************************/ long double MonteCarlo ( i n t i d , long double r a d i o , long nummuestras ) { 193 194 195 196 197 198 199 long long long long long long double p i ; double area ; double x ; double y ; double h i p ; aciertos =0; /*VBLE QUE ALMACENA NUMERO PI*/ /*AREA CIRCUNFERENCIA*/ /*EJE ORDENADAS MUESTRA ACTUAL*/ /*EJE COORDENADAS MUESTRA ACTUAL*/ /*HIPOTENUSA MUESTRA ACTUAL*/ /*NUM.MUESTRAS QUE CAEN DENTRO DE LA CIRCUNFERENCIA*/ CAPÍTULO 5. PASO DE MENSAJES 90 200 201 long f a l l o s = 1 ; long i ; /*NUM.MUESTRAS QUE CAEN FUERA DE LA CIRCUNFERENCIA*/ /*CONTADOR*/ 202 203 204 205 /*UTILIZAMOS COMO SEMILLA EL IDENTIFICADOR DEL PROCESO*/ srand ( i d ) ; 206 207 208 209 210 211 212 213 214 215 /*ESTIMACION DE PI*/ f o r ( i =0; i ! = nummuestras ; i + + ) { x =( long double ) rand ( ) / RAND_MAX; y =( long double ) rand ( ) / RAND_MAX; h i p = s q r t ( pow ( x , 2 ) +pow ( y , 2 ) ) ; i f ( hip >1){ f a l l o s ++;} else { a c i e r t o s + + ; } } p i = 4 . 0 ( ( long double ) a c i e r t o s / nummuestras ) ; L 216 217 218 /*CALCULAMOS AREA*/ area=pow ( r a d i o , 2 ) p i ; L 219 220 r e t u r n area ; 221 222 } /*MonteCarlo*/ 223 224 225 226 227 228 229 230 /*********************************************************************/ /*FUNCION recolectar_datos:PROCESO 0 HACE LA MEDIA DE LAS */ /*APROXIMACIONES LOCALES */ /*********************************************************************/ void r e c o l e c t a r _ d a t o s ( i n t i d , i n t numprocs , long double p a p r o x _ l o c a l , long double p a p r o x _ g l o b a l ) { L L 231 232 233 234 235 236 237 238 239 int origen ; /*PROCESO ORIGEN MENSAJES*/ int destino =0; /*PROCESO DESTINO DATOS*/ int etiqueta ; /*ETIQUETA MENSAJES*/ long double b u f f e r ; /*BUFFER RECEPCION APROXIMACIONES LOCALES*/ int i ; /*CONTADOR*/ MPI_Request r e q u e s t ; MPI_Request r e q u e s t s [MAX_NUM_PROCS ] ; MPI_Status s t a t u s ; 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 i f ( i d ==0){ paprox_global= paprox_local ; /*MANDAMOS LAS PETICIONES DE DATOS*/ f o r ( o r i g e n =1; o r i g e n<numprocs ; o r i g e n + + ) { e t i q u e t a =30+ o r i g e n ; MPI_Irecv (& b u f f e r , 1 , MPI_LONG_DOUBLE, o r i g e n , e t i q u e t a , MPI_COMM_WORLD,& r e q u e s t s [ o r i g e n ] ) ; } /*RECOLECTAMOS LOS CALCULOS LOCALES Y HACEMOS LA MEDIA*/ f o r ( i =1; i <numprocs ; i + + ) { MPI_Waitany ( numprocs , r e q u e s t s ,& o r i g e n ,& s t a t u s ) ; p a p r o x _ g l o b a l =( p a p r o x _ g l o b a l+ b u f f e r ) / 2 . 0 ; } L L L L 5.6. IMPLEMENTACIÓN CÁLCULO DE ÁREAS MEDIANTE MONTECARLO 255 256 257 258 259 260 261 262 } else { /*ENVIO DE LOS DATOS*/ e t i q u e t a =30+ i d ; MPI_Isend ( p a p r o x _ l o c a l , 1 , MPI_LONG_DOUBLE , d e s t i n o , e t i q u e t a , MPI_COMM_WORLD,& r e q u e s t ) ; } } /*distribuir_datos*/ 263 264 265 266 267 268 269 270 /*********************************************************************/ /*FUNCION construir_tipo_derivado: CONSTRUYE EL TIPO DE DATOS MPI */ /*NECESARIO PARA DISTRIBUIR LOS DATOS DE ENTRADA EMPAQUETADOS */ /*********************************************************************/ void c o n s t r u i r _ t i p o _ d e r i v a d o ( Tipo_Datos_Entrada pdatos , MPI_Datatype pMPI_Tipo_Datos ) { L L 271 272 273 274 275 MPI_Datatype t i p o s [ 2 ] ; int longitudes [ 2 ] ; MPI_Aint d i r e c c [ 3 ] ; MPI_Aint desplaz [ 2 ] ; 276 277 278 279 280 /*PRIMERO ESPECIFICAMOS LOS TIPOS*/ t i p o s [ 0 ] =MPI_LONG_DOUBLE; t i p o s [ 1 ] =MPI_LONG; 281 282 283 284 /*ESPECIFICAMOS EL NUMERO DE ELEMENTOS DE CADA TIPO*/ longitudes [0]=1; longitudes [1]=1; 285 286 287 288 289 290 291 292 /*CALCULAMOS LOS DESPLAZAMIENTOS DE LOS MIEMBROS DE LA ESTRUCTURA RELATIVOS AL COMIENZO DE DICHA ESTRUCTURA*/ MPI_Address ( pdatos ,& d i r e c c [ 0 ] ) ; MPI_Address ( & ( pdatos >r a d i o ) , & d i r e c c [ 1 ] ) ; MPI_Address ( & ( pdatos >n m u e s t r a s _ l o c a l ) , & d i r e c c [ 2 ] ) ; desplaz [ 0 ] = d i r e c c [1] d i r e c c [ 0 ] ; desplaz [ 1 ] = d i r e c c [2] d i r e c c [ 0 ] ; N N N N 293 294 295 /*CREACION TIPO DATOS DERIVADO*/ MPI_Type_struct ( 2 , l o n g i t u d e s , desplaz , t i p o s , pMPI_Tipo_Datos ) ; 296 297 298 /*CERTIFICARLO DE MANERA QUE PUEDA SER USADO*/ MPI_Type_commit ( pMPI_Tipo_Datos ) ; 299 300 } /*construir_tipo_derivado*/ 91 92 CAPÍTULO 5. PASO DE MENSAJES Capítulo 6 Comunicación Colectiva Las operaciones de comunicación colectiva son aquellas que se aplican al mismo tiempo a todos los procesos pertenecientes a un comunicador. Tienen una gran importancia en el estándar MPI, debido a la claridad de su sintaxis y a su eficiencia. Para analizar su utilidad y conveniencia implementaremos el algoritmo Regla del Trapecio de manera que haga un uso inteligente de dichas operaciones. 6.1. Algoritmo Regla del Trapecio La regla del trapecio estima Z\ [] ^ ,.T_3a`5T mediante la división del intervalo mentos iguales, calculando la siguiente suma: b cE Jcd en seg- V b ^ ,.T 3 $)( W ^ ,.T 3$)( W 7Mf e ^ ,.T g 3d 7 gih Wlk V k V g Donde , ,QJj/ 3 $& , y T FD EBGBGBG4E . Una manera de realizar este cálculo mediante un algoritmo paralelo es dividir el intervab EcJcd entre los procesos haciendo que cada proceso calcule la integral de ^ ,.T\3 sobre su lo subintervalo. Para estimar la integral completa se suman los cálculos locales de cada proceso. Supongamos que tenemos C procesos y segmentos, y, para simplificar, supongamos que es divisible entre C . En este caso es natural que el primer proceso calcule la integral de los primeros '$cC segmentos, el segundo proceso calculará el área de los siguientes '$cC segmentos, etc. Siguiendo esta regla el proceso m estimará la integral del subintervalo: b W m V E W ,Hm W 41 3 V d C C Cada proceso necesita la siguiente información: El número de procesos C Su identificador El intervalo de integración completo b cE Jcd 93 CAPÍTULO 6. COMUNICACIÓN COLECTIVA 94 El número de segmentos La función ^ ,.T\3 a integrar Los dos primeros datos se pueden conseguir llamando a las funciones MPI_Comm_size() y MPI_Comm_rank(). Sin embargo tanto el intervalo b EcJcd como el número de segmentos deben ser introducidos por el usuario. Por otro lado sería positivo que el usuario pudiera elegir la función ^ ,.T_3 a integrar (dentro de un conjunto predefinido de funciones). Como explicamos en la sección 4.4 sólo un proceso (normalmente el proceso 0) debe encargarse de obtener los datos de la entrada estándar y distribuirlos entre los demás procesos. Ésto puede hacerse mediante paso de mensajes o mediante comunicación colectiva, siendo la segunda opción más eficiente. En la sección 6.2 se explica el uso de las operaciones de comunicación colectiva con dicho propósito. Por otro lado una buena forma de sumar los cálculos individuales de los procesos es hacer que cada proceso envíe su cálculo local al proceso 0, y que el proceso 0 haga la suma final. De nuevo podemos emplear al menos dos técnicas: paso de mensajes y operaciones de reducción. Dado que las operaciones de reducción son más eficientes, utilizaremos la segunda opción. Las funciones que ofreceremos al usuario estarán codificadas en funciones de código C, por lo que tendremos que modificar el código y compilarlo de nuevo para introducir nuevas funciones. Entre las funciones ofrecidas incluiremos al menos las siguientes: ^ ,.T_3 (@T ^ ,.T_3 T ^ ,.T_3 n ^ ,.T_3 o qp n4rs P Esta última función nos permite realizar la búsqueda del número . El área bajo la curva P U qp n r nos proporciona un método para computar : SR P t WR v R ) u y w x z " u { P R M ` T b . , \ T 3 d , 0 / M D 3 ,1 T 3 P mediante la regla del trapecio utilizaremos Por lo tanto tenemos que para computar q p ^ ,.T\3 o n r s , D y J 1 . 6.2. Distribución y Recolección de los Datos La distribución y recolección de los datos se puede realizar mediante dos técnicas: paso de mensajes (como hicimos en el algoritmo Cálculo de Áreas mediante Montecarlo) y comunicación colectiva. Las operaciones de comunicación colectiva son más eficientes debido a que reducen el tiempo empleado en dichas operaciones, reduciendo el número de pasos necesarios para su ejecución. En la técnica de paso de mensajes el problema radica en que cargamos a un proceso con demasiado trabajo. Al utilizar esta técnica para distribuir los datos damos básicamente los siguientes pasos: 6.2. DISTRIBUCIÓN Y RECOLECCIÓN DE LOS DATOS 95 0 0 4 0 0 2 1 2 4 3 4 6 5 6 7 Figura 6.1: Procesos estructurados en árbol 1. Proceso 0 recoge los datos de la entrada estándar 2. Proceso 0 envía un mensaje a cada uno de los restantes procesos comunicándole los datos de entrada Si utilizamos un bucle para realizar el segundo paso, los procesos con identificador mayor deben continuar esperando mientras el proceso 0 envía los datos de entrada a los procesos con identificador menor. Éste no es sólo un asunto de E/S y por supuesto no es un comportamiento deseable: el punto principal del procesamiento paralelo es conseguir que múltiples procesos colaboren para resolver un problema. Si uno de los procesos está haciendo la mayoría del trabajo, podríamos usar mejor una máquina convencional de un solo procesador. Por otro lado observamos que existe una ineficiencia similar en el final del algoritmo al utilizar paso de mensajes en el proceso de recolección, donde el proceso 0 hace todo el trabajo de recolectar y sumar las integrales locales. La pregunta que surge es... ¿cómo podemos dividir el trabajo más equitativamente entre los procesos? Una solución natural es imaginar que tenemos un árbol de procesos, con el proceso 0 como raíz. Durante la primera etapa de la distribución de los datos pongamos que el proceso 0 envía los datos al 4. Durante la siguiente etapa el 0 envía los datos al 2, mientras que el 4 los envía al 6. Durante la última etapa el 0 se los envía al 1, mientras el 2 los envía al 3, el 4 al 5 y el 6 al 7 (mirar la figura 6.1). Así reducimos el bucle de distribución de la entrada de 7 etapas a 3 etapas. De manera más general, si tenemos C procesos este procedimiento permite distribuir los datos de entrada en "! ,|C\3 etapas, mientras que de la anterior manera lo haciamos en ,|C}/~143 etapas. Si C es grande esta mejora es una gran ventaja. Si queremos utilizar un esquema de distribución estructurado en árbol en nuestro algorit mo, necesitamos introducir un bucle con i"! ,|C\3 etapas. En la implementación de dicho bucle cada proceso necesita calcular en cada etapa: Si recibe, y en caso de hacerlo, el origen; y CAPÍTULO 6. COMUNICACIÓN COLECTIVA 96 Si envía, y en caso de hacerlo, el destino Como puede suponerse estos cálculos pueden ser algo complicados, especialmente porque no hay un ordenamiento fijo. En nuestro ejemplo elegimos: 1. 0 envía al 4 2. 0 envía al 2, 4 al 6 3. 0 envía al 1, 2 al 3, 4 al 5, 6 al 7 Aunque podríamos haber elegido también: 1. 0 envía al 1 2. 0 envía al 2, 1 al 3 3. 0 envía al 4, 1 al 5, 2 al 6, 3 al 7 De hecho, a menos que sepamos algo acerca de la topología subyacente de nuestra máquina no podremos decidir realmente qué esquema es mejor. Lo ideal sería usar una función diseñada específicamente para la máquina que estemos usando de manera que no nos tengamos que preocupar acerca de todos los detalles, y que no tengamos que modificar el código cada vez que cambiamos de máquina. Y como habrá podido adivinar MPI proporciona dicha función. 6.3. Operaciones de Comunicación Colectiva Un patrón de comunicación que engloba a todos los procesos de un comunicador es una comunicación colectiva. Como consecuencia una comunicación colectiva implica a más de dos procesos. Un broadcast es una operación de comunicación colectiva en donde un proceso envía un mismo mensaje a todos los procesos. En MPI la función para hacer ésto es MPI_Bcast(): int MPI_Bcast(void* mensaje, int contador, MPI_Datatype tipo_datos, int raiz, MPI_Comm com); La función envía una copia de los datos contenidos en mensaje por el proceso raiz a cada proceso perteneciente al comunicador com. Debe ser llamada por todos los procesos pertenecientes al comunicador con los mismos argumentos para raiz y com. Por lo tanto un mensaje broadcast no puede ser recibido con la función MPI_Recv(). Los parámetros contador y tipo_datos tienen la misma finalidad que en MPI_Send() y MPI_Recv(): especifican la extensión del mensaje. Sin embargo, al contrario que en las funciones punto a punto, MPI insiste en que en las operaciones de comunicación colectiva contador y tipo_datos deben ser iguales en todos los procesos pertenecientes al comunicador. La razón de ésto es que en algunas operaciones colectivas un único proceso recibe datos de otros muchos procesos, y para que el programa determine qué cantidad de datos han sido recibidos necesitaría un vector de status de retorno. 6.4. OPERACIONES DE REDUCCIÓN 97 6.4. Operaciones de Reducción En el algoritmo Regla del Trapecio, tras la fase de introducción de datos, todos los procesadores ejecutan esencialmente las mismas instrucciones hasta el final de la fase de suma. De este modo, a menos que la función ^ ,.T_3 sea muy complicada (p.ej. que requiera más trabajo ser evaluada en ciertas zonas del intervalo b EcJcd ), esta parte del programa distribuye el trabajo equitativamente entre los procesos. Como hemos dicho antes, éste no es el caso en la fase de suma final en donde si utilizamos paso de mensajes el proceso 0 realiza una cantidad de trabajo desproporcionada. Sin embargo probablemente se haya dado cuenta que invirtiendo las flechas de la figura 6.1 podemos usar la comunicación estructurada en árbol. De esta manera podemos distribuir el trabajo de calcular la suma entre los procesos como sigue: 1. (a) 1 envía al 0, 3 al 2, 5 al 4, 7 al 6 (b) 0 suma su integral a la recibida de 1, 2 suma su integral a la recibida de 3, etc. 2. (a) 2 envía al 0, 6 al 4 (b) 0 suma, 4 suma 3. (a) 4 envía al 0 (b) 0 suma Por supuesto caemos en la misma cuestión que cuando discutiamos la implementación de nuestro propio broadcast: ¿Hace un uso óptimo de la topología de nuestra máquina esta estructura en árbol? Una vez más debemos responder que ello depende de la máquina. Así que, como antes, debemos dejar a MPI hacer dicho trabajo utilizando una función optimizada. La “suma global” que queremos calcular es un ejemplo de una clase general de operaciones de comunicación colectiva llamadas operationes de reducción. Las operaciones de reducción forman un subconjunto de las operaciones de comunicación colectiva. En una operación de reducción global todos los procesos (pertenecientes a un comunicador) aportan datos, los cuales se combinan usando una operación binaria. Las operaciones binarias típicas son la suma, el máximo, el mínimo, AND lógico, etc. La función MPI que implementa una operación de reducción es: int MPI_Reduce(void* operando, void* resultado, int contador, MPI_Datatype tipo_datos, MPI_Op operacion, int raiz, MPI_Comm comunicador); La función MPI_Reduce combina los operandos referenciados por operando usando la operación señalada por operacion y almacena el resultado en la variable resultado perteneciente al proceso raiz. Tanto operando como resultado referencian contador posiciones de memoria del tipo tipo_datos. MPI_Reduce() debe ser llamado por todos los procesos pertenecientes a comunicador; además contador, tipo_datos y operacion deben ser iguales para todos los procesos. El argumento operacion puede adquirir uno de los valores predefinidos mostrados en el cuadro 6.1. También es posible definir operaciones adicionales. CAPÍTULO 6. COMUNICACIÓN COLECTIVA 98 Nombre de Operación MPI_MAX MPI_MIN MPI_SUM MPI_PROD MPI_LAND MPI_BAND MPI_LOR MPI_BOR MPI_LXOR MPI_BXOR MPI_MAXLOC MPI_MINLOC Significado Máximo Mínimo Suma Producto AND Lógico AND Bit a Bit OR Lógico OR Bit a Bit OR Exclusivo Lógico OR Exclusivo Bit a Bit Máximo y su Localización Mínimo y su Localización Cuadro 6.1: Operaciones de Reducción 6.5. Implementación Regla del Trapecio A continuación exponemos la implementación del algoritmo Regla del Trapecio. Notar que cada proceso llama a MPI_Reduce() con los mismos argumentos; de hecho, aunque la variable que almacena la suma global sólo tiene significado para el proceso 0, todos los procesos deben suministrar un argumento. Algoritmo 6.1: Regla del Trapecio 1 2 3 4 5 /********************************************************/ /*Regla del Trapecio: CODIGO APROXIMACION DE INTEGRALES */ /*MEDIANTE LA REGLA DEL TRAPECIO */ /********************************************************/ 6 7 8 # include < s t d i o . h> # include " mpi . h " 9 10 11 12 13 14 15 16 /*DECLARACION ESTRUCTURA DATOS ENTRADA*/ typedef s t r u c t { long double a ; long double b ; long nsegmentos ; i n t nfuncion ; } Tipo_Datos_Entrada ; 17 18 19 20 21 22 23 /*DECLARACION DE LA FUNCIONES QUE VAMOS A UTILIZAR*/ long double T r a p e c i o ( long double l o c a l _ a , long double l o c a l _ b , long l o c a l _ n , long double h , i n t numfuncion ) ; long double F1 ( long double x ) ; long double F2 ( long double x ) ; long double F3 ( long double x ) ; 6.5. IMPLEMENTACIÓN REGLA DEL TRAPECIO 24 25 26 27 28 29 30 31 32 99 long double F4 ( long double x ) ; void o b t e n e r _dat os ( i n t i d , long double pa , long double pb , long pn , i n t pnumfuncion ) ; void d i s t r i b u i r _ d a t o s ( i n t i d , long double pa , long double pb , long pn , i n t pnumfuncion ) ; void r e c o l e c t a r _ d a t o s ( i n t i d , i n t numprocs , long double p p a r c i a l , long double pcompleta ) ; void c o n s t r u i r _ t i p o _ d e r i v a d o ( Tipo_Datos_Entrada pdatos , MPI_Datatype pMPI_Tipo_Datos ) ; L L L L L L L L L L L L 33 34 35 36 37 /**************/ /*FUNCION MAIN*/ /**************/ i n t main ( i n t argc , char LML argv ) { 38 39 40 41 long double a ; long double b ; long n ; /*COMIENZO DEL INTERVALO GENERAL*/ /*FINAL DEL INTERVALO GENERAL*/ /*NUMERO DE SEGMENTOS EN TOTAL*/ 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 int id ; /*IDENTIFICADOR DEL PROCESO*/ i n t numprocs ; /*NUMERO DE PROCESOS*/ char nombreproc [MPI_MAX_PROCESSOR_NAME] ; /*NOMBRE PROCESADOR*/ i n t lnombreproc ; /*LONGITUD NOMBRE PROCESADOR*/ long double h ; /*ANCHO DE CADA SEGMENTO*/ long double l o c a l _ a ; /*COMIENZO DEL SUBINTERVALO DE CADA PROCESO*/ long double l o c a l _ b ; /*FINAL DEL SUBINTERVALO DE CADA PROCESO*/ long l o c a l _ n ; /*NUMERO DE SEGMENTOS PARA CADA PROCESO*/ long double p a r c i a l ; /*INTEGRAL PARCIAL DE CADA PROCESO*/ long double completa ; /*INTEGRAL COMPLETA (SUMA DE TODAS LAS PARCIALES)*/ double t m p i n i c = 0 . 0 ; /*TIEMPO INICIO DE LA EJECUCION*/ double t m p f i n ; /*TIEMPO FINAL DE LA EJECUCION*/ i n t numfuncion ; /*NUMERO DE FUNCION A UTILIZAR*/ i n t e t i q u e t a =50; MPI_Status s t a t u s ; int origen ; 60 61 62 63 /*INICIALIZAMOS EL ENTORNO DE EJECUCION MPI*/ M P I _ I n i t (& argc ,& argv ) ; 64 65 66 /*ALMACENAMOS EL IDENTIFICADOR DEL PROCESO*/ MPI_Comm_rank (MPI_COMM_WORLD,& i d ) ; 67 68 69 /*ALMACENAMOS EL NUMERO DE PROCESOS*/ MPI_Comm_size (MPI_COMM_WORLD,& numprocs ) ; 70 71 72 73 74 75 76 77 78 /*E/S:NOMBRE DEL PROCESADOR,PROCESADOR 0*/ MPI_Get_processor_name ( nombreproc,& lnombreproc ) ; i f ( i d ==0){ f p r i n t f ( s t d o u t , " \ n P r o c e s o %d en %s E n c a r g a d o de l a E / S \ n " , i d , nombreproc ) ; } /*E/S:NOMBRE DEL PROCESADOR,TODOS LOS PROCESOS (PARA PRUEBAS)*/ /*if(id==0){ CAPÍTULO 6. COMUNICACIÓN COLECTIVA 100 79 80 81 82 83 84 85 86 87 88 89 * for (origen=1;origen<numprocs;origen++){ * MPI_Recv(nombreproc,MPI_MAX_PROCESSOR_NAME,MPI_CHAR,origen, * etiqueta,MPI_COMM_WORLD,&status); * fprintf(stdout,"Proceso %d en %s\n",origen,nombreproc); * } *} *else{ * MPI_Send(nombreproc,MPI_MAX_PROCESSOR_NAME,MPI_CHAR,0, * etiqueta,MPI_COMM_WORLD); *} */ 90 91 92 /*OBTENEMOS DATOS INTRODUCIDOS POR EL USUARIO*/ o b t e n e r _d atos ( i d ,& a,& b ,& n,& numfuncion ) ; 93 94 95 96 /*TIEMPO INICIAL DE PROCESAMIENTO*/ i f ( i d ==0){ f p r i n t f ( stdout , " \ nProcesando . . . \ n \ n " ) ; } t m p i n i c=MPI_Wtime ( ) ; 97 98 99 /*DISTRIBUIMOS DATOS INTRODUCIDOS POR EL USUARIO*/ d i s t r i b u i r _ d a t o s ( i d ,& a ,& b,& n ,& numfuncion ) ; 100 101 102 103 104 /*SI NUMSEGMENTOS/NUMPROCS NO ES DIVISION ENTERA NUMSEGMENTOS SE TRUNCA*/ l o c a l _ n=n / numprocs ; /*NUMERO DE SEGMENTOS PARA CADA PROCESO*/ n= l o c a l _ n numprocs ; /*NUMERO DE SEGMENTOS EN TOTAL (TRUNCADO)*/ /*ANCHO DE SEGMENTO.SIMILAR PARA TODOS LOS PROCESOS*/ h =( b a ) / n ; N L 105 106 107 108 109 /*EL SUBINTERVALO DEL PROCESO TENDRA UN ANCHO DE local_n*h Y ESTARA COMPRENDIDO ENTRE local_a Y local_b*/ local_a = a + id local_n h ; local_b = local_a + local_n h ; L L L 110 111 112 /*CALCULAMOS LA INTEGRAL PARCIAL DEL PROCESO*/ p a r c i a l = T r a p e c i o ( l o c a l _ a , l o c a l _ b , l o c a l _ n , h , numfuncion ) ; 113 114 115 /*RECOLECTAMOS INTEGRALES CALCULADAS POR LOS PROCESOS Y LAS SUMAMOS*/ r e c o l e c t a r _ d a t o s ( i d , numprocs ,& p a r c i a l ,& completa ) ; 116 117 118 /*TIEMPO FINAL DE PROCESAMIENTO*/ t m p f i n =MPI_Wtime ( ) ; 119 120 121 122 123 124 125 126 127 /*E/S:IMPRIMIMOS RESULTADO Y TIEMPO DE PROCESAMIENTO*/ i f ( i d ==0){ f p r i n t f ( s t d o u t , " Con n= %d t r a p e c i o s , l a e s t i m a c i o n de l a i n t e g r a l \ n " , n ) ; f p r i n t f ( s t d o u t , " de l a f u n c i o n s e l e c c i o n a d a d e s d e " ) ; f p r i n t f ( s t d o u t , " %Lf h a s t a %Lf e s \ n %.60 Lf \ n \ n " , a , b , completa ) ; f p r i n t f ( s t d o u t , " Numero P r o c e s o s : %d \ n " , numprocs ) ; f p r i n t f ( s t d o u t , " Tiempo P r o c e s a m i e n t o : % f \ n \ n " , t m p f i n t m p i n i c ) ; } N 128 129 130 /*FINALIZAMOS EL ENTRORNO DE EJECUCION MPI*/ MPI_Finalize ( ) ; 131 132 133 } /*main*/ 6.5. IMPLEMENTACIÓN REGLA DEL TRAPECIO 101 134 135 136 137 138 139 140 /***********************************************************************/ /*FUNCION obtener_datos:PROCESO 0 CONSULTA AL USUARIO COMIENZO Y FINAL */ /*DEL INTERVALO [a,b] Y EL NUMERO DE SEGMENTOS (n) */ /***********************************************************************/ void o b t e n e r _dat os ( i n t i d , long double pa , long double pb , long pn , i n t pnumfuncion ) { L L L L 141 142 /*E/S:PETICION DE DATOS*/ 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 i f ( i d ==0){ f p r i n t f ( s t d o u t , " \ n I n t r o d u z c a num . de l a f u n c i o n a i n t e g r a r \ n " ) ; f p r i n t f ( s t d o u t , " \ nF1 =2 x F2=x^2 F3 = 1 / x F4 = 4 / ( 1 + x ^ 2 ) \ nF " ) ; s c a n f ( " %d " , pnumfuncion ) ; i f ( pnumfuncion < 1 | | pnumfuncion > 4 ) { f p r i n t f ( s t d o u t , " E r r o r en l a e n t r a d a , u t i l i z a n d o f u n c i o n F1=2 x \ n " ) ; pnumfuncion =1; } f p r i n t f ( s t d o u t , " I n t r o d u z c a un v a l o r p a r a a ( i n i c i o d e l i n t e r v a l o ) \ n " ) ; s c a n f ( " %Lf " , pa ) ; f p r i n t f ( s t d o u t , " I n t r o d u z c a un v a l o r p a r a b ( f i n a l d e l i n t e r v a l o ) \ n " ) ; s c a n f ( " %Lf " , pb ) ; f p r i n t f ( s t d o u t , " I n t r o d u z c a numero de s e g m e n t o s \ n " ) ; s c a n f ( " %l d " , pn ) ; } L L L 159 160 161 162 /*E/S SIN PETICION:UTILIZAR EN CASO DE PRUEBAS*/ /*if(id==0){*pa=0;*pb=1;*pn=1000000000;*pnumfuncion=4;}*/ 163 164 } /*obtener_datos*/ 165 166 167 168 169 170 171 172 /**************************************************************************/ /*FUNCION distribuir_datos:PROCESO 0 DISTIBUYE COMIENZO Y FINAL DEL */ /*INTERVALO [a,b] Y EL NUMERO DE SEGMENTOS (n) ENTRE LOS DEMAS PROCESOS */ /**************************************************************************/ void d i s t r i b u i r _ d a t o s ( i n t i d , long double pa , long double pb , long pn , i n t pnumfuncion ) { L L L L 173 174 175 176 MPI_Datatype MPI_Tipo_Datos_Entrada ; Tipo_Datos_Entrada d a t o s _ e n tr ada ; int raiz =0; /*PROCESO QUE ENVIA LOS DATOS A LOS DEMAS PROCESOS*/ 177 178 179 180 /*CONSTRUIMOS EL TIPO MPI PARA EMPAQUETAR LOS DATOS DE ENTRADA*/ c o n s t r u i r _ t i p o _ d e r i v a d o (& d a t o s _ e n tr ada,& MPI_Tipo_Datos_Entrada ) ; 181 182 183 184 185 186 187 188 i f ( i d ==0){ /*EMPAQUETADO DE DATOS*/ d a t o s _ e n trada . a= pa ; d a t o s _ e n trada . b= pb ; d a t o s _ e n trada . nsegmentos= pn ; d a t o s _ e n trada . n f u n c i o n = pnumfuncion ; } L L L L CAPÍTULO 6. COMUNICACIÓN COLECTIVA 102 189 190 MPI_Bcast (& d a t o s _ e ntr ada , 1 , MPI_Tipo_Datos_Entrada , r a i z ,MPI_COMM_WORLD ) ; 191 192 193 194 195 196 197 198 if ( id !=0){ /*RECOGIDA DE DATOS*/ pa=d a t o s _ e n tra da . a ; pb=d a t o s _ e n tra da . b ; pn=d a t o s _ e n tra da . nsegmentos ; pnumfuncion= d a t o s _ e nt rada . n f u n c i o n ; } L L L L 199 200 } /*distribuir_datos*/ 201 202 203 204 205 206 207 208 209 /************************************************************************/ /*FUNCION Trapecio:FUNCION QUE APLICA LA REGLA DEL TRAPECIO AL */ /*SUBINTERVALO COMPRENDIDO ENTRE local_a Y local_b, CON UN NUMERO DE */ /*SEGMENTOS local_n Y UN TAMAÑO DE SEGMENTO h */ /************************************************************************/ long double T r a p e c i o ( long double l o c a l _ a , long double l o c a l _ b , long l o c a l _ n , long double h , i n t numfuncion ) { L 210 211 212 213 214 long double ( f ) ( long double ) ; long double i n t e g r a l ; long double x ; int i ; /*VBLE QUE ALMACENA FUNCION A INTEGRAR*/ /*VBLE QUE ALMACENA EL RESULTADO*/ /*POSICION EN EL EJE X*/ /*CONTADOR*/ 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 switch ( numfuncion ) { case 1 : f =F1 ; break ; case 2 : f =F2 ; break ; case 3 : f =F3 ; break ; case 4 : f =F4 ; break ; } i n t e g r a l =( f ( l o c a l _ a )+ f ( l o c a l _ b ) ) / 2 . 0 ; x= l o c a l _ a ; f o r ( i =1; i <= l o c a l _ n 1; i + + ) { x+=h ; i n t e g r a l += f ( x ) ; } i n t e g r a l =h ; return i n t e g r a l ; N L 230 231 } /*Trapecio*/ 232 233 234 235 236 237 238 239 /*******************************************************************/ /*FUNCION recolectar_datos:PROCESO 0 SUMA LAS INTEGRALES PARCIALES */ /*CALCULADAS POR LOS DEMAS PROCESOS. */ /*******************************************************************/ void r e c o l e c t a r _ d a t o s ( i n t i d , i n t numprocs , long double p p a r c i a l , long double pcompleta ) { L L 240 241 int raiz =0; /*PROCESO QUE RECIBE LAS PARCIALES Y LAS SUMA*/ 242 243 /*SUMAMOS LAS INTEGRALES PARCIALES CALCULADAS POR CADA PROCESO*/ 6.5. IMPLEMENTACIÓN REGLA DEL TRAPECIO 103 MPI_Reduce ( p p a r c i a l , pcompleta , 1 , MPI_LONG_DOUBLE, MPI_SUM, 0 ,MPI_COMM_WORLD ) ; 244 245 246 } /*recolectar_datos*/ 247 248 249 250 251 252 253 /********************************/ /*FUNCION F1(x):FUNCION f(x)=2x */ /********************************/ long double F1 ( long double x ) { long double v a l o r _ r e t o r n o ; L 254 v a l o r _ r e t o r n o =2 x ; return valor_retorno ; 255 256 257 } 258 259 260 261 262 263 264 /*********************************/ /*FUNCION F2(x):FUNCION f(x)=x^2 */ /*********************************/ long double F2 ( long double x ) { long double v a l o r _ r e t o r n o ; L 265 v a l o r _ r e t o r n o =x x ; return valor_retorno ; 266 267 268 } 269 270 271 272 273 274 /*********************************/ /*FUNCION F3(x):FUNCION f(x)=1/x */ /*********************************/ long double F3 ( long double x ) { long double v a l o r _ r e t o r n o ; 275 v a l o r _ r e t o r n o =1/ x ; return valor_retorno ; 276 277 278 } 279 280 281 282 283 284 /***************************************/ /*FUNCION F4(x):FUNCION f(x)=4/(1+x^2) */ /***************************************/ long double F4 ( long double x ) { long double v a l o r _ r e t o r n o ; L 285 valor_retorno =4/(1+x x ) ; return valor_retorno ; 286 287 288 } 289 290 291 292 293 294 295 296 /*********************************************************************/ /*FUNCION construir_tipo_derivado: CONSTRUYE EL TIPO DE DATOS MPI */ /*NECESARIO PARA DISTRIBUIR LOS DATOS DE ENTRADA EMPAQUETADOS */ /*********************************************************************/ void c o n s t r u i r _ t i p o _ d e r i v a d o ( Tipo_Datos_Entrada pdatos , MPI_Datatype pMPI_Tipo_Datos ) { L 297 298 MPI_Datatype t i p o s [ 4 ] ; L CAPÍTULO 6. COMUNICACIÓN COLECTIVA 104 299 300 301 int longitudes [ 4 ] ; MPI_Aint d i r e c c [ 5 ] ; MPI_Aint desplaz [ 4 ] ; 302 303 304 305 306 307 308 /*PRIMERO ESPECIFICAMOS LOS TIPOS*/ t i p o s [ 0 ] =MPI_LONG_DOUBLE; t i p o s [ 1 ] =MPI_LONG_DOUBLE; t i p o s [ 2 ] =MPI_LONG ; t i p o s [ 3 ] = MPI_INT ; 309 310 311 312 313 314 /*ESPECIFICAMOS EL NUMERO DE ELEMENTOS DE CADA TIPO*/ longitudes [0]=1; longitudes [1]=1; longitudes [2]=1; longitudes [3]=1; 315 316 317 318 319 320 321 322 323 324 325 326 /*CALCULAMOS LOS DESPLAZAMIENTOS DE LOS MIEMBROS DE LA ESTRUCTURA RELATIVOS AL COMIENZO DE DICHA ESTRUCTURA*/ MPI_Address ( pdatos ,& d i r e c c [ 0 ] ) ; MPI_Address ( & ( pdatos >a ) , & d i r e c c [ 1 ] ) ; MPI_Address ( & ( pdatos >b ) , & d i r e c c [ 2 ] ) ; MPI_Address ( & ( pdatos >nsegmentos ) , & d i r e c c [ 3 ] ) ; MPI_Address ( & ( pdatos >n f u n c i o n ) , & d i r e c c [ 4 ] ) ; desplaz [ 0 ] = d i r e c c [1] d i r e c c [ 0 ] ; desplaz [ 1 ] = d i r e c c [2] d i r e c c [ 0 ] ; desplaz [ 2 ] = d i r e c c [3] d i r e c c [ 0 ] ; desplaz [ 3 ] = d i r e c c [4] d i r e c c [ 0 ] ; N N N N N N N N 327 328 329 /*CREACION TIPO DATOS DERIVADO*/ MPI_Type_struct ( 4 , l o n g i t u d e s , desplaz , t i p o s , pMPI_Tipo_Datos ) ; 330 331 332 /*CERTIFICARLO DE MANERA QUE PUEDA SER USADO*/ MPI_Type_commit ( pMPI_Tipo_Datos ) ; 333 334 } /*construir_tipo_derivado*/ Capítulo 7 Comunicadores y Topologías El uso de comunicadores y topologías hace a MPI diferente de la mayoría de los demás sistemas de paso de mensajes. En pocas palabras, un comunicador es una colección de procesos que pueden mandarse mensajes entre ellos. Una topología es una estructura impuesta en los procesos de un comunicador que permite a los procesos ser direccionados de diferentes maneras. Para ilustrar estas ideas desarrollaremos el código que implementa el algoritmo de Fox para multiplicar dos matrices cuadradas. 7.1. Algoritmo Multiplicación de Matrices de Fox Asumimos que las matrices principales , g 3 y Y ,QJ g 3 tienen orden . También asumimos que el número de procesos C es un cuadrado perfecto, cuya raiz m es tal que el m , y que resultado de '$@m es un entero. Digamos que C '$@m . En el algoritmo de Fox las matrices principales son particionadas en bloques y repartidas entre los procesos. Así vemos nuestros procesos como una tabla virtual de mIm elementos, donde a cada proceso se le asigna una submatriz de cada una de las matrices principales. De manera más formal, - tenemos la siguiente correspondencia: DFE:1)EBG G G EHC/1@/ ,Q=ME3 D~=ME~m/~1@ k La siguiente afirmaciónk define nuestra tabla de procesos: el proceso pertenece a la fila y la columna dadas por , 3 . Además diremos que el proceso con dirección e ,Q=5E3 tiene asignadas las submatrices: q o qp' s 78e 7 7. 7 BB . a o p'.. o ap' .. o p' c s 7e B B s 7e s 7Fe 7 J q 7 7 J o qp' s 7e 7 B B .. .. Y . . J o p' s a oJ ap' s o p' s c 7 7e B B 7 e 7e 105 CAPÍTULO 7. COMUNICADORES Y TOPOLOGÍAS 106 Proceso 0 Proceso 3 Proceso 6 Proceso 1 Proceso 4 Proceso 7 Proceso 2 Proceso 5 Proceso 8 Cuadro 7.1: Partición Matriz 6*6 en 9 Procesos ¡ ,.T_$)¢ET£5¤ ¥¢53 y Por ejemplo, si C , ,.T_3 particionada como se indica en el cuadro 7.1. §¦ y Y , = En el algoritmo de Fox las submatrices acumuladas por el proceso e ,.O@E3 . 9 entonces la matriz estaría DFE:1)EBGBGBG4Em/+1 son multiplicadas y Algoritmo básico Desde paso=0 hasta paso=q-1(aumentar paso en cada iteración): 1. Elegir una submatriz de A de cada una de las filas de procesos. La submatriz elegida en la fila será , donde ¨ ©«ªq¬ ­ ®°¯± ¨«²³8´"µ¶:·¹¸»¼&º ½¾ 2. En cada una de las filas de procesos hacer un broadcast de la submatriz elegida hacia los demás procesos en dicha fila. 3. En cada proceso multiplicar la submatriz recibida de A por la matriz de B que reside en dicho proceso, sin modificar ninguna de estas matrices. Sumar el resultado de la multiplicación al valor que contiene el proceso en la matriz resultado C. 4. En cada proceso enviar la submatriz de B al proceso que está encima de él (los procesos de la primera fila enviarán la submatriz a la última fila). 7.2. Comunicadores Si intentamos implementar el algoritmo de Fox, parece claro que nuestro trabajo se facilitaría si pudiéramos tratar ciertos subconjuntos de procesos como el universo de la comunicación, al menos temporalmente. Por ejemplo, en el pseudocódigo 2. En cada una de las filas de procesos hacer un broadcast de la submatriz elegida hacia los demás procesos en dicha fila. 7.3. TRABAJANDO CON GRUPOS, CONTEXTOS Y COMUNICADORES 107 sería útil tratar cada fila como el universo de la comunicación, así como en la sentencia 4. En cada proceso enviar la submatriz de B al proceso que está encima de él (los procesos de la primera fila enviarán la submatriz a la última fila). sería útil tratar cada columna de procesos como el universo de la comunicación. El mecanismo que MPI proporciona para tratar a un subconjunto de procesos como el universo de la comunicación son los comunicadores. Hasta ahora hemos definido al comunicador como una colección de procesos que pueden mandarse mensajes entre ellos. Ahora que queremos construir nuestros propios comunicadores necesitaremos una discusión más profunda. En MPI existen dos tipos de comunicadores: intracomunicadores e intercomunicadores. Los intracomunicadores son esencialmente una colección de procesos que pueden mandarse mensajes entre ellos y utilizar operaciones de comunicación colectivas. Por ejemplo, MPI_COMM_WORLD es un intracomunicador, y queremos formar para cada fila y cada columna del algoritmo de Fox otro intracomunicador. Los intercomunicadores, como su propio nombre indica, son utilizados para mandar mensajes entre los procesos pertenecientes a intracomunicadores disjuntos. Por ejemplo, un intercomunicador podría ser útil en un entorno que nos permitiera crear procesos dinámicamente: un nuevo conjunto de procesos que formen un intracomunicador podría ser enlazado al conjunto de procesos original (por ejemplo MPI_COMM_WORLD) mediante un intercomunicador. Nosotros sólo utilizaremos intracomunicadores. Un intracomunicador mínimo está compuesto de: Un Grupo, y Un Contexto. Un grupo es una colección ordenada de procesos. Si un grupo consiste en C procesos, a cada proceso en el grupo se le asigna un único identificador, que no es más que un entero no negativo en el rango DFE:1)EBGBGBG@EHC/¿1 . Un contexto puede ser entendido como una etiqueta definida por el sistema que es atribuida al grupo. De este modo dos procesos que pertenecen al mismo grupo y usan el mismo contexto pueden comunicarse. Esta paridad grupo-contexto es la forma más básica de comunicador. Otros datos pueden ser asociados al comunicador. En particular, se le puede asociar una estructura o topología al comunicador, permitiendo un esquema de direccionamiento de los procesos más natural. Abordaremos las topologías en la sección 7.5. 7.3. Trabajando con Grupos, Contextos y Comunicadores Para ilustrar las bases del uso de los comunicadores, vamos a crear un comunicador cuyo grupo consiste en los procesos de la primera fila de nuestra tabla virtual. Supongamos que MPI_COMM_WORLD contiene C procesos, donde m C . Supongamos también que ,.T\3 ,.T_$@m8ETÀ£5¤ ¥m53 . Así la primera fila de procesos consistirá en los procesos cuyo identificador es DFE:1)EBGBGBG&Em§/1 (aquí los identificadores pertenecen a MPI_COMM_WORLD). Para crear el grupo de nuestro nuevo comunicador podemos ejecutar el siguiente código: CAPÍTULO 7. COMUNICADORES Y TOPOLOGÍAS 108 MPI_Group MPI_GROUP_WORLD; MPI_Group grupo_primera_fila; MPI_Comm com_primera_fila; int tam_fila; int* ids_procesos; /* CREA LISTA CON LOS PROCESOS DEL NUEVO COMUNICADOR */ ids_procesos=(int*) malloc(q*sizeof(int)); for (proc=0; proc<q ; proc++) ids_procesos[proc]=proc; /* OBTIENE GRUPOS DEL COMUNICADOR MPI_COMM_WORLD */ MPI_Comm_group(MPI_COMM_WORLD,&MPI_GROUP_WORLD); /* CREA EL NUEVO GRUPO */ MPI_Group_incl(MPI_GROUP_WORLD,q,ids_procesos, &grupo_primera_fila); /* CREA EL NUEVO COMUNICADOR */ MPI_Comm_create(MPI_COMM_WORLD,grupo_primera_fila, &com_primera_fila) Este código construye de una manera muy estricta el nuevo comunicador. Primero crea una lista de los procesos que deberán ser asignados al nuevo comunicador. Luego crea un grupo con dichos procesos. Ésto requiere dos llamadas; primero se obtiene el grupo asociado a MPI_COMM_WORLD, dado que éste es el grupo del cual se extraen los procesos para el nuevo grupo; luego se crea el grupo con MPI_Group_incl(). Finalmente el nuevo comunicador es creado con una llamada a MPI_Comm_create(). La llamada a MPI_Comm_create() asocia implícitamente un contexto al nuevo grupo. El resultado es el comunicador com_primera_fila. Ahora los procesos pertenecientes a com_primera_fila pueden ejecutar operaciones de comunicación colectivas. Por ejemplo, el proceso 0 (en grupo_primera_fila) a los otros procesos del grupo grupo_primera_fila: puede hacer un broadcast de su matriz int mi_id_en_primera_fila; float* A_00; /* mi_id ES EL IDENT. DEL PROCESO EN MPI_GROUP_WORLD */ if(mi_id<q){ MPI_Comm_rank(com_primera_fila,&mi_id_en_primera_fila); /* ASIGNAMOS ESPACIO A A_00, ORDEN=n */ A_00=(float*) malloc(n*n*sizeof(float)); if(mi_id_en_primera_fila==0){ /*INICIALIZAMOS A_00 */ . . . } 7.3. TRABAJANDO CON GRUPOS, CONTEXTOS Y COMUNICADORES 109 MPI_Bcast(A_00,n*n,MPI_FLOAT,0,com_primera_fila); } Los grupos y los comunicadores son objetos opacos. Desde un punto de vista práctico ésto significa que los detalles de su representación interna dependen de la implementación MPI particular, y como consecuencia el usuario no puede acceder directamente a ellos. En vez de esto el usuario accede a un manejador que referencia al objeto opaco, de manera que dichos objetos opacos son manipulados por funciones MPI especiales, como por ejemplo MPI_Comm_create(), MPI_Group_incl() y MPI_Comm_group(). Los contextos no son explícitamente usados en ninguna función MPI. En vez de ello, son implícitamente asociados a los grupos cuando los comunicadores son creados. La sintaxis de los comandos que hemos usado para crear com_primera_fila es bastante sencilla. El primer comando int MPI_Comm_group(MPI_Comm com, MPI_Group* grupo) simplemente retorna el grupo perteneciente al comunicador com. El segundo comando int MPI_Group_incl(MPI_Group antiguo_grupo, int tamano_nuevo_grupo, int* ids_antiguo_grupo, MPI_Group* nuevo_grupo) crea un nuevo grupo a partir de una lista de procesos pertenecientes al grupo existente antiguo_grupo. El número de procesos en el nuevo grupo es tamano_nuevo_grupo, y los procesos que serán incluidos son listados en ids_antiguo_grupo. El proceso 0 en nuevo_grupo tiene el identificador ids_antiguo_grupo[0] en antiguo_grupo, el proceso 1 en nuevo_grupo es el ids_antiguo_grupo[1] en antiguo_grupo, y así sucesivamente. El comando final int MPI_Comm_create(MPI_Comm antiguo_com, MPI_Group nuevo_grupo, MPI_Comm* nuevo_com) asocia el contexto al grupo nuevo_grupo, y crea el comunicador nuevo_com. Todos los procesos en nuevo_grupo pertenecen al grupo subyacente antiguo_com. Existe una diferencia extremadamente importante entre las primeras dos funciones y la tercera. MPI_Comm_group() y MPI_Group_incl() son ambas operaciones locales. Ello quiere decir que no hay comunicación entre los procesos implicados en dicha ejecuciòn. Sin embargo MPI_Comm_create() es una operación colectiva. Todos los procesos en antiguo_grupo deben llamar a MPI_Comm_create() con los mismos argumentos. El estándar MPI da tres razones para ello: Permite a la implementación colocar a MPI_Comm_create() como la primera de las comunicaciones colectivas regulares. CAPÍTULO 7. COMUNICADORES Y TOPOLOGÍAS 110 Proporciona mayor seguridad. Permite que las implementaciones eviten comunicaciones relativas a la creación del contexto. Notemos que como MPI_Comm_create() es colectiva tendrá un comportamiento sincronizador. En particular, si varios comunicadores están siendo creados, todos deben ser creados en el mismo orden en todos los procesos. 7.4. Particionamiento de los Comunicadores En nuestro programa de multiplicación de matrices necesitamos crear múltiples comunicadores (uno para cada fila de procesos y uno para cada columna). Éste podría ser un proceso extremadamente tedioso si C fuera grande y tuviéramos que crear cada comunicador usando las tres funciones comentadas en la sección anterior. Afortunadamente MPI proporciona una función, MPI_Comm_split(), que puede crear varios comunicadores simultáneamente. Para ejemplificar su uso crearemos un comunicador para cada fila de procesos: MPI_Comm com_fila; int fila; /* id ES EL IDENTIFICADOR EN MPI_COMM_WORLD. * q*q = p */ fila = id / q; MPI_Comm_split(MPI_COMM_WORLD, fila, id, &com_fila); Una única llamada a MPI_Comm_split() crea m nuevos comunicadores, todos ellos con el Á el grupo com_fila consistirá en los procesos mismo nombre com_fila. Por ejemplo, si C 0, 1 y 2 para los procesos 0, 1 y 2. En los procesos 3, 4 y 5 el grupo subyacente a nuevo_com será el formado por los procesos 3, 4 y 5; y lo mismo ocurrirá con los procesos 6, 7 y 8. La sintaxis de MPI_Comm_split() es: int MPI_Comm_split(MPI_Comm antiguo_com, int clave_particion, int clave_id, MPI_Comm* nuevo_com) La llamada crea un comunicador para cada valor de clave_particion. Los procesos con el mismo valor en clave_particion forman un nuevo grupo. El identificador de los procesos en el nuevo grupo estará determinado por su valor en clave_id. Si los procesos y Y llaman ambos a la función MPI_Comm_split() con el mismo valor en clave_particion, y el argu mento clave_id pasado por el proceso es menor que el pasado por el proceso Y , entonces el identificador de en el grupo nuevo_com será menor que el identificador del proceso Y . Si llaman a la función con el mismo valor en clave_id el sistema asignará arbitrariamente a uno de los procesos un identificador menor. 7.5. TOPOLOGÍAS 111 MPI_Comm_split() es una operación colectiva y debe ser llamada por todos los procesos pertenecientes a antiguo_com. La función puede ser utilizada incluso si no deseamos asignarle un comunicador a todos los procesos. Ésto puede ser realizado pasando la constante predefinida MPI_UNDEFINED en el argumento clave_particion. Los procesos que hagan ésto obtendrán como valor de retorno en nuevo_com el valor MPI_COMM_NULL. 7.5. Topologías Recordemos que es posible asociar información adicional (más allá del grupo y del contexto) a un comunicador. Una de las piezas de información más importantes que se pueden adjuntar al comunicador es la topología. En MPI una topología no es más que un mecanismo para asociar diferentes esquemas de direccionamiento a los procesos pertenecientes a un grupo. Notar que las topologías en MPI son topologías virtuales, lo que quiere decir que podría no haber una relación simple entre la estructura de procesos de una topología virtual y la estructura física real de la máquina paralela. Esencialmente existen dos tipos de topologías virtuales que se pueden crear en MPI: la topología cartesiana o rejilla, y la topología gráfica. Conceptualmente ésta última engloba a la primera. De todos modos, y debido a la importancia de las rejillas en las aplicaciones, existe una colección separada de funciones en MPI cuyo propósito es la manipulación de rejillas virtuales. En el algoritmo de Fox queremos identificar los procesos en MPI_COMM_WORLD mediante las coordenadas de una rejilla, y cada fila y cada columna de la rejilla necesita formar su propio comunicador. Observemos un método para construir dicha estructura. Comenzamos asociando una estructura de rejilla a MPI_COMM_WORLD. Para hacer esto necesitamos especificar la siguiente información: 1. El número de dimensiones de la rejilla. Tenemos 2. 2. El tamaño de cada dimensión. En nuestro caso, no es más que el número de filas y el número de columnas. Tenemos m filas y m columnas. 3. La periodicidad de cada dimensión. En nuestro caso esta información especifica si la primera entrada de cada fila o columna es “adyacente” a la última entrada de dicha fila o columna, respectivamente. Dado que nosotros queremos un paso “circular” de las matrices en cada columna, sería provechoso que la segunda dimensión fuera periódica. No es importante si la primera dimensión es periódica o no. 4. Finalmente MPI ofrece al usuario la posibilidad de permitir al sistema optimizar el direccionamiento de la rejilla, posiblemente reordenando los procesos pertenecientes al comunicador asociado, para así aprovechar mejor la estructura física de dichos procesos. Dado que no necesitamos preservar el orden de los procesos en MPI_COMM_WORLD, deberíamos permitir al sistema el reordenamiento. Para implementar estas decisiones simplemente ejecutaremos el siguiente código: MPI_Comm com_rejilla; CAPÍTULO 7. COMUNICADORES Y TOPOLOGÍAS 112 int dimensiones[2]; int periodicidad[2]; int reordenamiento = 1; dimensiones[0] = dimensiones[1] = q; periodicidad[0] = periodicidad[1] = 1; MPI_Cart_create(MPI_COMM_WORLD, 2, dimensiones, periodicidad, reordenamiento, &com_rejilla); Tras ejecutar este código el comunicador com_rejilla contendrá los procesos pertenecientes a MPI_COMM_WORLD (posiblemente reordenados) y tendrá asociado un sistema de coordenadas cartesianas de dos dimensiones. Para determinar las coordenadas de un proceso simplemente llamaremos a la función MPI_Cart_coords(): int coordenadas[2]; int id_rejilla; MPI_Comm_rank(com_rejilla, &id_rejilla); MPI_Cart_coords(com_rejilla, id_rejilla, 2, coordenadas); Observemos que necesitamos llamar a MPI_Comm_rank() para obtener el identificador del proceso en com_rejilla. Esto es necesario porque en nuestra llamada a MPI_Cart_create() establecimos la variable reordenamiento a 1, y por lo tanto el identificador original de un proceso en MPI_COMM_WORLD puede cambiar en com_rejilla. La “inversa” a MPI_Cart_coords() es MPI_Cart_rank(): int MPI_Cart_rank(com_rejilla, coordenadas, &id_rejilla) Dadas las coordenadas de un proceso, MPI_Cart_rank() retorna el identificador del proceso en su tercer parámetro id_proceso. La sintaxis de MPI_Cart_create() es: int MPI_Cart_create(MPI_Comm antiguo_com, int numero_dimensiones, int* tam_dimensiones, int* periodicidades, int reordenamiento, MPI_Comm* com_cartesiano) MPI_Cart_create() crea un nuevo comunicador, com_cartesiano, aplicando una topología cartesiana a antiguo_com. La información sobre la estructura de la topología cartesiana está contenida en los parámetros numero_dimensiones, tam_dimensiones y periodicidades. El primero de ellos, numero_dimensiones, contiene el número de dimensiones que forman el sistema de coordenadas cartesianas. Los dos siguientes, tam_dimensiones y periodicidades, son 7.6. DIVISIÓN DE REJILLAS 113 3 0 6 4 1 7 5 2 8 Cuadro 7.2: Topología Física 3*3 vectores que tienen el mismo orden que numero_dimensiones. El vector tam_dimensiones especifica el orden de cada dimensión, mientras que el vector periodicidades especifica para cada dimensión si es circular o linear. Los procesos pertenecientes a com_cartesiano son ordenados de fila en fila. De este modo zu {ÆÅ { Å la primera fila contiene los procesos DFE:1)EBGBGBG&E £ _W ¥%Âi£Äà  à b D@d/¿1 , la segunda fila conzu {%Å { Å zu {%Å { Å zu {%Å { Å tendrá £ _ ¥%£Äà  à b D@dE £ _ ¥%Âi£Ã  à b D@d 1)EBGBGBG&Ec('* £ _ ¥%£Äà  à b D@d4/1 , y así sucesivamente. En este sentido podría ser ventajoso cambiar el orden relativo de los procesos en com_antiguo. Por ejemplo, supongamos que la topología física es una tabla ¢*X¢ y que los procesos pertenecientes com_antiguo, representados por sus identificadores, están asociados a los procesadores como se expone en el cuadro 7.2. Claramente la eficiencia del algoritmo de Fox se mejoraría si reordenáramos los procesos. Sin embargo, y dado que el usuario no conoce cómo se asocian los procesos a los procesadores, deberíamos dejar al sistema hacerlo estableciendo el parámetro reordenamiento a 1. Dado que MPI_Cart_create() construye un nuevo comunicador, se trata de una operación colectiva. La sintaxis de las funciones que nos informan sobre el direccionamiento de los procesos es: int MPI_Cart_rank(MPI_Comm cart_com, int* coordenadas, int* id) int MPI_Cart_coords(MPI_Comm cart_com, int id, int numero_dimensiones, int* coordenadas) La función MPI_Cart_rank() retorna el identificador en cart_com del proceso que tiene las coordenadas cartesianas representadas en el vector coordenadas. Así, coordenadas es un vector de orden igual al número de dimensiones de la topología cartesiana asociada a cart_com. MPI_Cart_coords() es la inversa a MPI_Cart_rank(): retorna las coordenadas del proceso que tiene como identificador id en el comunicador cartesiano cart_com. Notar que ambas funciones son locales. 7.6. División de Rejillas También se puede dividir una rejilla en rejillas de menores dimensiones. Por ejemplo, podemos crear un comunicador para cada fila de la rejilla como sigue: int var_coords[2]; CAPÍTULO 7. COMUNICADORES Y TOPOLOGÍAS 114 MPI_Comm com_fila; var_coords[0]=0; var_coords[1]=1; MPI_Cart_sub(cart_com,var_coords,&com_fila); La llamada a MPI_Cart_sub() crea m nuevos comunicadores. El argumento var_coords es un vector de booleanos. Especifica para cada dimensión si “pertenece” al nuevo comunicador. Dado que estamos creando comunicadores para las filas de la rejilla, cada nuevo comunicador consiste en los procesos obtenidos al fijar la coordenada de la fila, permitiendo que la coordenada de la columna varíe. Con este propósito le asignamos a var_coords[0] el valor 0 (la primera coordenada no varía) y le asignamos a var_coords[1] el valor 1 (la segunda coordenada varía). En cada proceso retornamos el nuevo comunicador en com_fila. Si queremos crear comunicadores para las columnas, simplemente invertimos los valores en los elementos de var_coords. MPI_Com com_columna; var_coords[0]=1; var_coords[1]=0; MPI_Cart_sub(cart_com,var_coords,&com_columna); Notar la similaridad entre MPI_Cart_sub() y MPI_Comm_split(). Ambas llevan a cabo funciones similares (las dos dividen un comunicador en una colección de nuevos comunicadores). Sin embargo MPI_Cart_sub() sólo puede ser usada en comunicadores que tengan asociada una topología cartesiana, y el nuevo comunicador sólo puede ser creado fijando (o variando) una o más dimensiones del antiguo comunicador. Notar también que MPI_Cart_sub() es, como MPI_Comm_split(), una operación colectiva. 7.7. Implementación Multiplicación de Matrices de Fox En esta sección mostramos la implementación del algoritmo de Fox. En este programa la función Config_cart() genera varios comunicadores e información asociada a ellos. Debido a que este procedimiento requiere muchas variables, y dado que la información contenida en dichas variables es necesaria en la ejecución de otras funciones, utilizaremos una estructura tipo Info_Cart para agrupar toda esa información generada. Luego realizaremos la multiplicación de matrices por el método de Fox mediante la llamada a la función Fox(); dicha función se apoya en otra, Mult_Matrices(), creada para ejecutar la indispensable multiplicación de matrices locales. Algoritmo 7.1: Multiplicación de Matrices de Fox 1 2 3 4 5 /********************************************************************/ /*Fox:CODIGO MULTIPLICACION DE MATRICES MEDIANTE EL ALGORITMO DE FOX*/ /********************************************************************/ 7.7. IMPLEMENTACIÓN MULTIPLICACIÓN DE MATRICES DE FOX 6 7 115 /*TAMAÑO MAXIMO DE LA MATRIZ DE CADA PROCESO*/ # define MAX_ORDEN_LOCAL 700 8 9 10 11 12 # # # # include < s t d i o . h> include < s t d l i b . h> include < math . h> include " mpi . h " 13 14 15 16 17 18 19 20 21 22 23 24 25 /*ESTRUCTURA QUE CONTIENE PARA CADA PROCESO LA INFORMACION RELEVANTE A LA TOPOLOGIA CARTESIANA QUE COMPONEMOS*/ typedef s t r u c t { i n t numprocs ; /*NUM DE PROCESOS PERTENECIENTES*/ i n t orden_cart ; /*sqrt(p)--->ORDEN DE LA TOPOLOGIA*/ MPI_Comm comm_global ; /*COMM GLOBAL DE LA TOPOLOGIA COMPLETA*/ MPI_Comm comm_fila ; /*COMM FORMADO POR LOS PROCS DE LA FILA*/ MPI_Comm comm_columna ; /*COMM FORMADO POR LOS PROCS DE LA COLUMNA*/ int f i l a ; /*NUMERO DE FILA*/ i n t columna ; /*NUMERO DE COLUMNA*/ int id ; /*IDENTIFICADOR DENTRO DE LA TOPOLOGIA CREADA*/ } Info_cart ; 26 27 28 /*TIPO MATRIZ DE CADA PROCESO*/ typedef i n t M a t r i z L o c a l [MAX_ORDEN_LOCAL] [ MAX_ORDEN_LOCAL] ; 29 30 31 /*TIPO DE DATOS MPI UTILIZADO PARA EL ENVIO DE MATRICES ENTRE LOS PROCESOS*/ MPI_Datatype T i p o _ e n v i o _ m a t r i z ; 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 /*DECLARACION DE LAS FUNCIONES UTILIZADAS*/ int Config_cart ( Info_cart pcart ) ; void o b t e n e r _dat os ( i n t minimo , i n t maximo , i n t o r d e n _ g l o b a l ) ; void d i s t r i b u i r _ d a t o s ( i n t tamano_local ) ; void i n i c i a l i z a c i o n _ m a t r i c e s ( M a t r i z L o c a l l o c a l A , MatrizLocal localB , MatrizLocal localC , int orden_local , int id ) ; void Fox ( I n f o _ c a r t p c a r t , MatrizLocal matriz1 , MatrizLocal matriz2 , MatrizLocal r e s u l t , int orden_local ) ; i n t i m p r e s i o n _ m a t r i c e s ( i n t i d , i n t numprocs , MatrizLocal matriz1 , MatrizLocal matriz2 , MatrizLocal r e s u l t , i n t tamano , int f i l a , i n t columna ) ; void m u l t _ m a t r i c e s ( M a t r i z L o c a l m a t r i z 1 , MatrizLocal matriz2 , MatrizLocal r e s u l t , int orden_local ) ; void i m p _ m a t r i z ( M a t r i z L o c a l m a t r i z , i n t o r d e n _ l o c a l , FILE s a l i d a ) ; L L L L L CAPÍTULO 7. COMUNICADORES Y TOPOLOGÍAS 116 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 /*********************************************************************/ /*FUNCION MAIN: NORMALMENTE UTILIZAMOS COMUNICADOR MPI_COMM_WORLD EN */ /*LAS COMUNICACIONES.EMPLEAMOS COMUNICADOR TOPOLOGIA CARTESIANA SOLO */ /*PARA LA MULTIPLICACION DE LAS MATRICES */ /*********************************************************************/ i n t main ( i n t argc , char argv ) { Info_cart top_cart ; /*ESTRUCTURA CON INFO SOBRE LA TOP.CARTESIANA*/ MatrizLocal localA ; /*MATRIZ LOCAL*/ MatrizLocal localB ; /*MATRIZ LOCAL*/ MatrizLocal localC ; /*MATRIZ LOCAL (SOLUCION)*/ int orden_local ; /*ORDEN MATRIZ LOCAL DE CADA PROCESO*/ i n t orden_global ; /*ORDEN MATRIZ GLOBAL*/ i n t minimo ; /*MINIMO ORDEN GLOBAL*/ i n t maximo ; /*MAXIMO ORDEN GLOBAL*/ i n t id_comm_world ; /*ID PROCESO EN MPI_COMM_WORLD*/ i n t numprocs_comm_world ; /*NUMERO PROCESOS EN MPI_COMM_WORLD*/ i n t raiz_numprocs ; /*RAIZ DE NUMERO PROCESOS*/ char nombreproc [MPI_MAX_PROCESSOR_NAME] ; /*NOMBRE PROCESADOR*/ i n t lnombreproc ; /*LONGITUD NOMBRE PROCESADOR*/ double t m p i n i c = 0 . 0 ; /*TIEMPO INICIO DE LA EJECUCION*/ double t m p f i n ; /*TIEMPO FINAL DE LA EJECUCION */ int etiqueta =50; /*ETIQUETA MENSAJES DE PRUEBA*/ MPI_Status s t a t u s ; /*STATUS MENSAJES DE PRUEBA*/ int origen ; /*ORIGEN MENSAJES DE PRUEBA*/ L8L L L L 86 87 88 89 90 /*INICIALIZAMOS EL ENTORNO DE EJECUCION MPI*/ M P I _ I n i t (& argc ,& argv ) ; 91 92 93 /*ALMACENAMOS EL IDENTIFICADOR DEL PROCESO EN MPI_COMM_WORLD*/ MPI_Comm_rank (MPI_COMM_WORLD,& id_comm_world ) ; 94 95 96 /*ALMACENAMOS EL NUMERO DE PROCESOS*/ MPI_Comm_size (MPI_COMM_WORLD,& numprocs_comm_world ) ; 97 98 99 100 101 102 103 104 105 106 107 /*FINALIZAMOS SI NUMERO DE PROCESOS NO TIENE RAIZ ENTERA*/ raiz_numprocs= s q r t ( numprocs_comm_world ) ; i f ( pow ( raiz_numprocs , 2 ) ! = numprocs_comm_world ) { i f ( id_comm_world = = 0 ) { f p r i n t f ( s t d o u t , " \ n E r r o r . Numero de p r o c e s o s u t i l i z a d o " ) ; f p r i n t f ( stdout , " debe t e n e r r a i z e n t e r a \ n \ n " ) ; } MPI_Finalize ( ) ; return 0 ; } 108 109 110 111 112 113 114 115 /*E/S:NOMBRE DEL PROCESADOR,PROCESADOR 0*/ MPI_Get_processor_name ( nombreproc,& lnombreproc ) ; i f ( id_comm_world = = 0 ) { f p r i n t f ( s t d o u t , " \ n P r o c e s o %d (MPI_COMM_WORLD) en %s " , id_comm_world , nombreproc ) ; f p r i n t f ( s t d o u t , " E n c a r g a d o de l a E / S \ n " ) ; } 7.7. IMPLEMENTACIÓN MULTIPLICACIÓN DE MATRICES DE FOX 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 117 /*E/S:NOMBRE DEL PROCESADOR,TODOS LOS PROCESOS (PRUEBAS)*/ /*if(id_comm_world==0){ * for (origen=1;origen<numprocs_comm_world;origen++){ * MPI_Recv(nombreproc,MPI_MAX_PROCESSOR_NAME,MPI_CHAR,origen, * etiqueta,MPI_COMM_WORLD,&status); * fprintf(stdout,"PROC %d (MPI_COMM_WORLD) en %s\n", * origen,nombreproc); * } *} *else{ * MPI_Send(nombreproc,MPI_MAX_PROCESSOR_NAME,MPI_CHAR,0, * etiqueta,MPI_COMM_WORLD); *} * */ 131 132 133 134 135 /*RESERVAMOS ESPACIO PARA MATRICES LOCALES*/ l o c a l A =( M a t r i z L o c a l ) m a l l o c ( s i z e o f ( M a t r i z L o c a l ) ) ; l o c a l B =( M a t r i z L o c a l ) m a l l o c ( s i z e o f ( M a t r i z L o c a l ) ) ; l o c a l C =( M a t r i z L o c a l ) m a l l o c ( s i z e o f ( M a t r i z L o c a l ) ) ; L L L 136 137 138 139 140 141 142 143 144 /*OBTENEMOS DATOS INTRODUCIDOS POR EL USUARIO*/ i f ( id_comm_world = = 0 ) { minimo=raiz_numprocs ; /*MINIMO ORDEN GLOBAL*/ maximo=raiz_numprocs MAX_ORDEN_LOCAL; /*MAXIMO ORDEN GLOBAL*/ o b t e n e r _dato s ( minimo , maximo,& o r d e n _ g l o b a l ) ; /*OBTENEMOS DATOS DEL USUARIO*/ o r d e n _ l o c a l= o r d e n _ g l o b a l / raiz_numprocs ; /*CALCULAMOS ORDEN MATRIZ LOCAL*/ o r d e n _ g l o b a l= o r d e n _ l o c a l raiz_numprocs ; /*TRUNCAMOS ORDEN MATRIZ GLOBAL*/ } L L 145 146 147 148 149 150 151 152 153 /*TIEMPO INICIAL DE PROCESAMIENTO*/ i f ( id_comm_world = = 0 ) { f p r i n t f ( s t d o u t , " \ n U t i l i z a n d o %d m a t r i c e s de o r d e n %d c a d a una . \ n " , numprocs_comm_world , o r d e n _ l o c a l ) ; f p r i n t f ( s t d o u t , " Orden M a t r i z G l o b a l : %d \ n " , o r d e n _ g l o b a l ) ; f p r i n t f ( stdout , " Procesando . . . \ n \ n" ) ; } t m p i n i c=MPI_Wtime ( ) ; 154 155 156 /*DISTRIBUIMOS DATOS INTRODUCIDOS POR EL USUARIO*/ d i s t r i b u i r _ d a t o s (& o r d e n _ l o c a l ) ; 157 158 159 /*INICIALIZACION MATRICES*/ i n i c i a l i z a c i o n _ m a t r i c e s ( l o c a l A , l o c a l B , l o c a l C , o r d e n _ l o c a l , id_comm_world ) ; L L L 160 161 162 /*----------------[UTILIZACION TOPOLOGIA CARTESIANA]------------------*/ 163 164 165 166 167 /*DEFINICION TIPO DE DATOS MPI UTILIZADO PARA EL ENVIO DE MATRICES*/ MPI_Type_vector ( o r d e n _ l o c a l , o r d e n _ l o c a l ,MAX_ORDEN_LOCAL, MPI_INT , &T i p o _ e n v i o _ m a t r i z ) ; MPI_Type_commit (& T i p o _ e n v i o _ m a t r i z ) ; 168 169 170 /*CREACION Y CONFIGURACION DE LA TOPOLOGIA CARTESIANA SI LA SALIDA DE Config_cart ES ERRONEA,FINALIZAMOS EL PROCESO*/ CAPÍTULO 7. COMUNICADORES Y TOPOLOGÍAS 118 i f ( C o n f i g _ c a r t (& t o p _ c a r t ) ) { MPI_Finalize ( ) ; return 0 ; } 171 172 173 174 175 /*LLAMADA MULTIPLICACION DE MATRICES MEDIANTE EL ALGORITMO DE FOX*/ Fox (& t o p _ c a r t , l o c a l A , l o c a l B , l o c a l C , o r d e n _ l o c a l ) ; L 176 177 L L 178 /*--------------------------------------------------------------------*/ 179 180 181 182 /*IMPRIMIMOS RESULTADO.UTILIZAMOS FILA Y COLUMNA DE LA TOP.CARTESIANA*/ i m p r e s i o n _ m a t r i c e s ( id_comm_world , numprocs_comm_world , localA , localB , localC , orden_local , t o p _ c a r t . f i l a , t o p _ c a r t . columna ) ; 183 L 184 185 186 L L 187 /*IMPRIMIMOS TIEMPO FINAL DE PROCESAMIENTO*/ t m p f i n =MPI_Wtime ( ) ; i f ( id_comm_world = = 0 ) { f p r i n t f ( s t d o u t , " \ nNumero P r o c e s o s : %d \ n " , numprocs_comm_world ) ; f p r i n t f ( s t d o u t , " Tiempo P r o c e s a m i e n t o : % f \ n \ n " , t m p f i n t m p i n i c ) ; } 188 189 190 N 191 192 193 194 /*FINALIZAMOS EL ENTORNO DE EJECUCION MPI*/ MPI_Finalize ( ) ; return 0 ; 195 196 197 198 199 } /*main*/ 200 201 202 203 204 205 206 /*************************************************************/ /*FUNCION obtener_datos: PROCESO 0 EN MPI_COMM_WORLD OBTIENE */ /*LOS DATOS DE ENTRADA */ /*************************************************************/ void o b t e n e r _dat os ( i n t minimo , i n t maximo , i n t o r d e n _ g l o b a l ) { L 207 i n t ok = 0 ; 208 /*SEMAFORO PETICION DATOS*/ 209 /*E/S:PETICION DE DATOS*/ 210 211 while ( ok = = 0 ) { f p r i n t f ( s t d o u t , " I n t r o d u z c a o r d e n de l a m a t r i z g l o b a l a c a l c u l a r " ) ; f p r i n t f ( s t d o u t , " ( minimo= %d , maximo= %d ) \ n " , minimo , maximo ) ; s c a n f ( " %d " , o r d e n _ g l o b a l ) ; i f ( minimo <=( o r d e n _ g l o b a l ) & & ( o r d e n _ g l o b a l )<=maximo ) { ok = 1 ; } else { f p r i n t f ( s t d o u t , " Orden e r r o n e o . \ n " ) ; } } 212 213 214 L 215 216 217 218 L 219 220 /*E/S SIN PETICION:UTILIZAR EN CASO DE PRUEBAS*/ /**orden_global=2800;*/ 221 222 223 224 225 } 7.7. IMPLEMENTACIÓN MULTIPLICACIÓN DE MATRICES DE FOX 226 227 228 229 230 119 /*********************************************************************/ /*FUNCION distribuir_datos: UTILIZAMOS EL COMUNICACOR MPI_COMM_WORLD */ /*PARA DISTRIBUIR EL DATO DE ENTRADA */ /*********************************************************************/ void d i s t r i b u i r _ d a t o s ( i n t o r d e n _ l o c a l ) { L 231 MPI_Bcast ( o r d e n _ l o c a l , 1 , MPI_INT , 0 ,MPI_COMM_WORLD ) ; 232 233 234 } 235 236 237 238 239 240 241 242 243 244 /***********************************************************/ /*FUNCION inicializacion_matrices:INICIALIZA LAS MATRICES */ /*ALEATORIAMENTE.LA MATRIZ RESULTADO SE DEJA A 0 */ /***********************************************************/ void i n i c i a l i z a c i o n _ m a t r i c e s ( M a t r i z L o c a l l o c a l A , MatrizLocal localB , MatrizLocal localC , int orden_local , int id ){ 245 246 int i , j ; /*CONTADORES*/ 247 248 249 /*UTILIZAMOS COMO SEMILLA EL IDENTIFICADOR DEL PROCESO*/ srand ( i d ) ; 250 251 252 253 254 255 256 257 258 259 260 f o r ( i =0; i < o r d e n _ l o c a l ; i + + ) { f o r ( j =0; j < o r d e n _ l o c a l ; j + + ) { /*MATRICES LOCALES,ELEMENTOS PSEUDOALEATORIOS*/ l o c a l A [ i ] [ j ] = ( i n t ) ( 1 0 0 . 0 rand ( ) / ( RAND_MAX) ) ; l o c a l B [ i ] [ j ] = ( i n t ) ( 1 0 0 . 0 rand ( ) / ( RAND_MAX) ) ; /*RESULTADO,ELEMENTOS=0*/ localC [ i ] [ j ]=0; } } } /*inicializacion_matrices*/ L L 261 262 263 264 265 266 267 268 269 270 271 272 /************************************************************************/ /*FUNCION Config_cart:CREA Y CONFIGURA UN COMUNICADOR CUYA TOPOLOGIA ES */ /*CARTESIANA. ALMACENA EN LA ESTRUCTURA Info_cart LOS DATOS NECESARIOS */ /************************************************************************/ int Config_cart ( Info_cart pcart ) { int antigua_id ; /*ID DENTRO DEL COMUNICADOR INICIAL MPI_COMM_WORLD*/ i n t dims [ 2 ] ; /*VECTOR QUE CONTIENE LA EXTENSION DE CADA DIMENSION*/ int periods [ 2 ] ; /*VECTOR BOOLEANO.CONTIENE PERIODICIDAD DE CADA DIMENSION*/ i n t coords [ 2 ] ; /*VECTOR QUE CONTIENE LAS COORDENADAS DE CADA PROCESO*/ i n t var_coords [ 2 ] ; /*VECTOR PARA GENERAR COMUNICADORES DE FILA Y COLUMNA*/ L 273 274 275 276 277 /*ALMACENAMOS EL NUM DE PROCESOS PERTENECIENTES A MPI_COMM_WORLD*/ MPI_Comm_size (MPI_COMM_WORLD, & ( p c a r t >numprocs ) ) ; /*IDENTIFICADOR DE CADA PROCESO DENTRO DE MPI_COMM_WORLD*/ MPI_Comm_rank (MPI_COMM_WORLD,& a n t i g u a _ i d ) ; N 278 279 280 /*ORDEN DE LA TOPOLOGIA CARTESIANA,QUE ES LA RAIZ DEL NUM DE PROCESOS*/ /*SI DICHA RAIZ NO ES ENTERA,DEBEMOS DESHACERNOS DE LOS PROCESOS SOBRANTES*/ CAPÍTULO 7. COMUNICADORES Y TOPOLOGÍAS 120 281 N N p c a r t >o r d e n _ c a r t =( i n t ) s q r t ( p c a r t >numprocs ) ; 282 283 284 285 286 287 288 /*PREPARAMOS LA CREACION DE LA TOPOLOGIA CARTESIANA.DEFINIMOS LA EXTENSION Y PERIODICIDAD DE CADA UNA DE LAS DIMENSIONES,MEDIANTE SENDOS VECTORES*/ dims [ 0 ] = dims [ 1 ] = p c a r t >o r d e n _ c a r t ; periods [0]= periods [1]=1; /*CREACION DE LA TOPOLOGIA CARTESIANA.LA ALMACENAMOS EN pcart->comm_global*/ MPI_Cart_create (MPI_COMM_WORLD, 2 , dims , p e r i o d s , 1 , & ( p c a r t >comm_global ) ) ; N N 289 290 291 /*SI DICHO PROCESO NO ENCAJA EN LA TOPOLOGIA CARTESIANA,SALIDA ERRONEA*/ i f ( ! p c a r t >comm_global ) r e t u r n 1 ; N 292 293 294 /*IDENTIFICADOR DE CADA PROCESO DENTRO DE LA TOPOLOGIA CARTESIANA*/ MPI_Comm_rank ( p c a r t >comm_global , & ( p c a r t >i d ) ) ; N N 295 296 297 298 299 /*COORDENADAS ASOCIADAS AL PROCESO,SU FILA Y COLUMNA*/ MPI_Cart_coords ( p c a r t >comm_global , p c a r t >i d , 2 , coords ) ; p c a r t > f i l a =coords [ 0 ] ; p c a r t >columna=coords [ 1 ] ; N N N N 300 301 302 303 304 305 306 /*CREAMOS UN COMUNICADOR ENTRE LOS PROCESOS DE LA MISMA FILA*/ var_coords [ 0 ] = 0 ; var_coords [ 1 ] = 1 ; MPI_Cart_sub ( p c a r t >comm_global , var_coords , & ( p c a r t >comm_fila ) ) ; /*CREAMOS UN COMUNICADOR ENTRE LOS PROCESOS DE LA MISMA COLUMNA*/ var_coords [ 0 ] = 1 ; var_coords [ 1 ] = 0 ; MPI_Cart_sub ( p c a r t >comm_global , var_coords , & ( p c a r t >comm_columna ) ) ; N N N N 307 308 return 0 ; 309 310 } /*Config_cart*/ 311 312 313 314 315 316 317 318 319 320 /**************************************************************************/ /*FUNCION Fox:MULTIPLICACION DE MATRICES MEDIANTE EL ALGORITMO DE FOX */ /**************************************************************************/ void Fox ( I n f o _ c a r t p c a r t , MatrizLocal matriz1 , MatrizLocal matriz2 , MatrizLocal r e s u l t , int orden_local ) { L 321 322 323 int i ; int bcast_raiz ; 324 325 int origen , destino ; 326 327 328 329 int etiqueta =50; MPI_Status s t a t u s ; M a t r i z L o c a l temp1 ; L 330 /*CONTADOR*/ /*PROCESO ORIGEN QUE ENVIA MATRIZ LOCAL 1 HACIA LOS PROCESOS DE LA MISMA FILA*/ /*ORIGEN Y DESTINO DE MATRIZ LOCAL 2 ENTRE LOS PROCESOS DE LA MISMA COLUMNA*/ /*ETIQUETA*/ /*STATUS RECEPCION*/ /*VBLE DONDE ALMACENAMOS LA MATRIZ ENVIADA POR bcast_raiz*/ 331 332 333 334 335 /*RESERVAMOS ESPACIO PARA MATRIZ LOCAL*/ temp1=( M a t r i z L o c a l ) m a l l o c ( s i z e o f ( M a t r i z L o c a l ) ) ; L 7.7. IMPLEMENTACIÓN MULTIPLICACIÓN DE MATRICES DE FOX 336 337 338 339 121 /*ESTABLECEMOS EL NUMERO DE FILA DE LOS PROCESOS ORIGEN Y DESTINO DE LA MATRIZ LOCAL 2*/ o r i g e n =( p c a r t > f i l a + 1 ) % p c a r t >o r d e n _ c a r t ; d e s t i n o =( p c a r t > f i l a + p c a r t >o r d e n _ c a r t 1 ) % p c a r t >o r d e n _ c a r t ; N N N N N N 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 /*MIENTRAS QUEDEN PROCESOS EN LA FILA...*/ f o r ( i =0; i <( p c a r t >o r d e n _ c a r t ) ; i + + ) { /*SE ELIGE EL PROCESO RAIZ*/ b c a s t _ r a i z =( p c a r t > f i l a + i ) %( p c a r t >o r d e n _ c a r t ) ; /*SI ES EL TURNO DEL PROPIO PROCESO EN EJECUCION ,SE ENVIA MATRIZ LOCAL 1 HACIA LOS PROCESOS DE LA MISMA FILA Y SE MULTIPLICA LOCALMENTE*/ i f ( b c a s t _ r a i z = = p c a r t >columna ) { MPI_Bcast ( m a t r i z 1 , 1 , T i p o _ e n v i o _ m a t r i z , b c a s t _ r a i z , p c a r t >comm_fila ) ; mult_matrices ( matriz1 , matriz2 , r e s u l t , orden_local ) ; } /*SI NO TOCA EL TURNO,SE RECIBE LA MATRIZ Y SE MULTIPLICA LOCALMENTE*/ else { MPI_Bcast ( temp1 , 1 , T i p o _ e n v i o _ m a t r i z , b c a s t _ r a i z , p c a r t >comm_fila ) ; m u l t _ m a t r i c e s ( temp1 , m a t r i z 2 , r e s u l t , o r d e n _ l o c a l ) ; } N N N N N N L 356 /*POR ULTIMO SE ENVIA HACIA ARRIBA EN LA COLUMNA LA MATRIZ LOCAL 2*/ MPI_Sendrecv_replace ( m a t r i z 2 , 1 , T i p o _ e n v i o _ m a t r i z , destino , etiqueta , origen , etiqueta , p c a r t >comm_columna,& s t a t u s ) ; 357 358 359 360 361 362 N } 363 364 } /*Fox*/ 365 366 367 368 369 370 371 372 373 374 375 376 377 /******************************************************************/ /*FUNCION impresion_matrices:PROCESO 0 RECIBE LAS MATRICES DE LOS */ /*DEMAS PROCESOS ORDENADAMENTE Y LAS IMPRIME */ /******************************************************************/ i n t i m p r e s i o n _ m a t r i c e s ( i n t i d , i n t numprocs , MatrizLocal matrizA , MatrizLocal matrizB , MatrizLocal matrizResult , int orden_local , int f i l a , i n t columna ) { 378 379 380 381 382 383 384 385 386 int BufferFila ; /*BUFFER RECEPCION FILA*/ i n t BufferColumna ; /*BUFFER RECEPCION COLUMNA*/ M a t r i z L o c a l B u f f e r M a t r i z ; /*BUFFER RECEPCION MATRIZ*/ int origen ; /*ORIGEN DE LOS DATOS*/ MPI_Request r e q u e s t s [ 5 ] ; /*VECTOR DE PETICIONES*/ MPI_Status s t a t u s [ 5 ] ; /*VECTOR DE STATUS*/ int etiqueta =10; /*ETIQUETA DE LOS MENSAJES*/ FILE s a l i d a ; /*FICHERO SALIDA*/ L L 387 388 389 390 /*RESERVAMOS ESPACIO PARA MATRIZ LOCAL*/ B u f f e r M a t r i z =( M a t r i z L o c a l ) m a l l o c ( s i z e o f ( M a t r i z L o c a l ) ) ; L CAPÍTULO 7. COMUNICADORES Y TOPOLOGÍAS 122 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 i f ( i d ==0){ /*APERTURA FICHERO SALIDA*/ /*UTILIZAR PREFERIBLEMENTE UN FICHERO LOCAL (EN LINUX PODRIAMOS USAR ALGO COMO "/tmp/Result.txt")*/ i f ( ( s a l i d a=fopen ( " / tmp / R e s u l t . t x t " , "w" ) ) = =NULL ) { f p r i n t f ( s t d e r r , " F a l l o de a p e r t u r a f i c h e r o s a l i d a . \ n " ) ; return 1 ; } else { f p r i n t f ( s t d o u t , " E s c r i b i e n d o r e s u l t a d o en f i c h e r o de s a l i d a . \ n " ) ; } /*IMPRESION MATRICES PROPIAS*/ f p r i n t f ( s a l i d a , "PROC %d : C o o r d e n a d a s( %d, %d ) . M a t r i z A\ n " , i d , f i l a , columna ) ; imp_matriz ( matrizA , orden_local , s a l i d a ) ; f p r i n t f ( s a l i d a , "PROC %d : C o o r d e n a d a s( %d, %d ) . M a t r i z B \ n " , i d , f i l a , columna ) ; imp_matriz ( matrizB , orden_local , s a l i d a ) ; f p r i n t f ( s a l i d a , "PROC %d : C o o r d e n a d a s( %d, %d ) . M a t r i z R e s u l t a d o \ n " , i d , f i l a , columna ) ; imp_matriz ( matrizResult , orden_local , s a l i d a ) ; /*IMPRESION OTRAS MATRICES*/ f o r ( o r i g e n =1; o r i g e n<numprocs ; o r i g e n + + ) { /*MATRIZ A*/ MPI_Irecv (& B u f f e r F i l a , 1 , MPI_INT , o r i g e n , e t i q u e t a o r i g e n +1, MPI_COMM_WORLD,& r e q u e s t s [ 0 ] ) ; MPI_Irecv (& BufferColumna , 1 , MPI_INT , o r i g e n , e t i q u e t a o r i g e n +2, MPI_COMM_WORLD,& r e q u e s t s [ 1 ] ) ; MPI_Irecv ( B u f f e r M a t r i z , 1 , T i p o _ e n v i o _ m a t r i z , o r i g e n , e t i q u e t a o r i g e n +3, MPI_COMM_WORLD,& r e q u e s t s [ 2 ] ) ; MPI_Waitall ( 3 , requests , status ) ; f p r i n t f ( s a l i d a , "PROC %d : C o o r d e n a d a s( %d, %d ) . M a t r i z A \ n " , o r i g e n , B u f f e r F i l a , BufferColumna ) ; i m p _ m a t r iz ( B u f f e r M a t r i z , o r d e n _ l o c a l , s a l i d a ) ; /*MATRIZ B*/ MPI_Irecv ( B u f f e r M a t r i z , 1 , T i p o _ e n v i o _ m a t r i z , o r i g e n , e t i q u e t a o r i g e n +4, MPI_COMM_WORLD,& r e q u e s t s [ 0 ] ) ; MPI_Wait(& r e q u e s t s [ 0 ] , & s t a t u s [ 0 ] ) ; f p r i n t f ( s a l i d a , "PROC %d : C o o r d e n a d a s( %d, %d ) . M a t r i z B \ n " , o r i g e n , B u f f e r F i l a , BufferColumna ) ; i m p _ m a t r iz ( B u f f e r M a t r i z , o r d e n _ l o c a l , s a l i d a ) ; /*MATRIZ Result*/ MPI_Irecv ( B u f f e r M a t r i z , 1 , T i p o _ e n v i o _ m a t r i z , o r i g e n , e t i q u e t a o r i g e n +5, MPI_COMM_WORLD,& r e q u e s t s [ 0 ] ) ; MPI_Wait(& r e q u e s t s [ 0 ] , & s t a t u s [ 0 ] ) ; f p r i n t f ( s a l i d a , "PROC %d : C o o r d e n a d a s( %d, %d ) . M a t r i z R e s u l t \ n " , o r i g e n , B u f f e r F i l a , BufferColumna ) ; i m p _ m a t r iz ( B u f f e r M a t r i z , o r d e n _ l o c a l , s a l i d a ) ; } /*CIERRE FICHERO SALIDA*/ fclose ( salida ) ; } else { /*ENVIO DE DATOS*/ L L L L L L L L 7.7. IMPLEMENTACIÓN MULTIPLICACIÓN DE MATRICES DE FOX L MPI_Isend(& f i l a , 1 , MPI_INT , 0 , e t i q u e t a i d +1, MPI_COMM_WORLD,& r e q u e s t s [ 0 ] ) ; MPI_Isend(& columna , 1 , MPI_INT , 0 , e t i q u e t a i d +2, MPI_COMM_WORLD,& r e q u e s t s [ 1 ] ) ; MPI_Isend ( m a t r i z A , 1 , T i p o _ e n v i o _ m a t r i z , 0 , e t i q u e t a i d +3, MPI_COMM_WORLD,& r e q u e s t s [ 2 ] ) ; MPI_Isend ( m a t r i z B , 1 , T i p o _ e n v i o _ m a t r i z , 0 , e t i q u e t a i d +4, MPI_COMM_WORLD,& r e q u e s t s [ 2 ] ) ; MPI_Isend ( m a t r i z R e s u l t , 1 , T i p o _ e n v i o _ m a t r i z , 0 , e t i q u e t a i d +5, MPI_COMM_WORLD,& r e q u e s t s [ 2 ] ) ; 446 L 447 448 L 449 450 L 451 452 L 453 454 455 } 456 457 458 } /*impresion_matrices*/ 459 460 461 462 463 464 465 466 467 /**************************************************************/ /*FUNCION mult_matrices:MULTIPLICACION DE MATRICES LOCALMENTE */ /**************************************************************/ void m u l t _ m a t r i c e s ( M a t r i z L o c a l m a t r i z 1 , MatrizLocal matriz2 , MatrizLocal r e s u l t , int orden_local ) { 468 int i , j , k ; 469 /*CONTADORES*/ 470 f o r ( i =0; i < o r d e n _ l o c a l ; i + + ) { f o r ( j =0; j < o r d e n _ l o c a l ; j + + ) { /*CALCULAMOS EL ELEMENTO i,j DE LA MATRIZ result*/ f o r ( k =0; k< o r d e n _ l o c a l ; k + + ) { ( r e s u l t ) [ i ] [ j ] + = ( matriz1 ) [ i ] [ k ] ( matriz2 ) [ k ] [ j ] ; } } } 471 472 473 L 474 475 476 477 478 479 480 } /*Mult_Matrices*/ 481 482 483 484 485 486 /*********************************************/ /*FUNCION imp_matriz:IMPRESION DE UNA MATRIZ */ /*********************************************/ void i m p _ m a t r i z ( M a t r i z L o c a l m a t r i z , i n t o r d e n _ l o c a l , FILE 487 int i , j ; 488 /*CONTADORES*/ 489 f o r ( i =0; i < o r d e n _ l o c a l ; i + + ) { f o r ( j =0; j < o r d e n _ l o c a l ; j ++) f p r i n t f ( s a l i d a , " %d " , m a t r i z [ i ] [ j ] ) ; f p r i n t f ( salida , " \ n" ) ; } 490 491 492 493 494 495 } L salida ){ 123 124 CAPÍTULO 7. COMUNICADORES Y TOPOLOGÍAS Parte III Análisis del Rendimiento 125 Capítulo 8 Evaluación del Sistema Para conseguir una cierta objetividad en el análisis de los algoritmos ejecutados en un determinado sistema paralelo, lo primero que debemos hacer es medir el rendimiento de dicho sistema. Además los distintos grupos de desarrolladores de software MPI tienen diferentes razones para medir el rendimiento de sus sistemas: Los desarrolladores de aplicaciones necesitan conocer el rendimiento de las implementaciones MPI sobre el sistema que van a emplear. De esta manera podrán elegir de manera acertada el tipo de operaciones a implementar en sus algoritmos y, si hay varias disponibles, la implementación MPI a utilizar. Los evaluadores encuentran la información sobre el rendimiento crítica a la hora de decidir qué máquina adquirir para ejecutar sus aplicaciones y, si hay varias disponibles, qué implementación MPI utilizar. Los implementadores necesitan entender el comportamiento de sus propias implementaciones MPI para poder plantear modificaciones y medir los efectos de dichas modificaciones. Normalmente la medición del rendimiento se realiza a través de herramientas especializadas en el análisis, de modo que podemos saber fácilmente y con seguridad cuánto de apropiado es el sistema para la ejecución de determinados algoritmos paralelos. 8.1. Utilidad mpptest Mpptest es un programa que mide el rendimiento de las funciones básicas MPI en una amplia variedad de situaciones. Además del clásico test de comunicación entre dos procesos, mpptest puede realizar tests de comunicación con múltiples procesos implicados, exponiendo problemas de escalabilidad. También podremos elegir el tamaño de los mensajes a enviar de manera que se puedan tratar de manera aislada los problemas imprevistos en el rendimiento del sistema. Originalmente mpptest fué desarrollado antes de la aparición del estándar MPI, característica que todavía se refleja en algunos de los nombres de las opciones (-force para readysend, etc.). Mpptest se distribuye con la implementación MPICH aunque puede ser usada en 127 CAPÍTULO 8. EVALUACIÓN DEL SISTEMA 128 cualquier implementación MPI. Las versiones más actuales incluyen junto a mpptest una serie de programas (stress y goptest entre otros) que están adaptados específicamente a cierto tipo de mediciones del rendimiento del sistema. 8.1.1. Compilación y Ejecución La implementación MPICH incluye en su propia distribución la utilidad mpptest, por lo cual los usuarios de MPICH no necesitarán un complicado proceso de instalación. Para compilar mpptest sólo tendremos que ejecutar : $ cd directorio_mpich/examples/perftest $ make Mpptest puede ser utilizado en otras implementaciones MPI; para realizar una instalación adecuada consulte la documentación. Para ejecutar mpptest debemos utilizar el programa de arranque de procesos que generalmente empleamos en la ejecución de programas MPI. Normalmente utilizaremos mpirun (sección A.6). De este modo utilizando la siguiente orden realizaremos un test simple de comunicación bloqueante entre dos procesos: $ mpirun -np 2 mpptest Para conocer todas las opciones de mpptest ejecutaremos: $ mpptest -help También existe un script, llamado runmpptest, que facilita la realización de las pruebas de rendimiento más generales. Para utilizarlo no es necesario emplear el programa mpirun. Podemos conocer sus opciones ejecutando: $ runmpptest -help 8.1.2. Formatos de Salida Por defecto mpptest está configurado para generar datos para el programa CIt. De esta manera si queremos generar por la salida estándar los datos necesarios para visualizar el resultado con el programa CIt usaremos: $ mpirun -np 2 mpptest Sin embargo mpptest también puede generar datos para el programa gnuplot, utilizando la opción -gnuplot. De esta manera obtendremos por la salida estándar los comandos necesarios para que el programa gnuplot utilice los datos que la misma orden almacena en el fichero ‘mppout.gpl’. La siguiente orden guarda en el fichero ‘out.mpl’ los comandos que serán leídos por gnuplot, y agrega los datos del test al fichero ‘mppout.gpl’: $ mpirun -np 2 mpptest -gnuplot > out.mpl 8.1. UTILIDAD MPPTEST 129 También podemos utilizar la opción -fname. Con la siguiente orden guardaremos en el fichero ‘nombre.mpl’ los comandos que serán leídos por gnuplot, agregando los datos del test al fichero ‘nombre.gpl’: $ mpirun -np 2 mpptest -gnuplot -fname nombre.mpl 8.1.3. Visualización En la visualización del contenido de los tests utilizaremos el programa gnuplot. Lo primero que debemos hacer, como se explica en el apartado anterior, es generar el fichero de comandos .mpl y el fichero de datos .gpl asociado. Una vez hecho ésto, para ejecutar los comandos del fichero ‘out.mpl’ (analizando así los datos almacenados en el fichero .gpl asociado) utilizaremos: $ gnuplot out.mpl o bien escribiremos el comando > load ’out.mpl’ dentro del programa gnuplot. 8.1.4. Gráficos Para generar gráficos en formato Encapsulated Postscript tenemos dos opciones. La primera es utilizar la opción -gnuploteps en vez de -gnuplot para generar el fichero de comandos .mpl. La siguiente secuencia de órdenes genera el gráfico ‘out.eps’ en monocromo: $ mpirun -np 2 mpptest -gnuploteps -fname out.mpl $ gnuplot out.mpl > out.eps Otra posibilidad sería generar el fichero de comandos .mpl con la opción -gnuplot (como hacíamos antes): $ mpirun -np 2 mpptest -gnuplot -fname out.mpl Para luego escribir una serie de comandos dentro del programa gnuplot. De esta manera podremos elegir de una manera más precisa el formato del gráfico de salida. Con la siguiente serie de comandos conseguiremos obtener el gráfico en color: > set term postscript eps color > set output “out.eps” > load ’out.mpl’ CAPÍTULO 8. EVALUACIÓN DEL SISTEMA 130 8.1.5. Operaciones Mpptest proporciona una amplia variedad de tests. Los distintos tipos de tests son seleccionados normalmente a través de los argumentos asociados a la ejecución del programa en la línea de comandos, aunque también puede ser necesario el empleo de otros programas para ejecutarlos. La siguiente lista resume las opciones disponibles más interesantes: Comunicación No Bloqueante. La comunicación no bloqueante es importante; en muchas aplicaciones usar funciones de comunicación no bloqueante es la manera más fácil de asegurar la fiabilidad evitando problemas relacionados con la capacidad finita del buffer. El rendimiento de las funciones no bloqueantes puede ser diferente al de las funciones bloqueantes. Para utilizar este tipo de comunicación emplearemos la opción -async. Número de Procesos. Cualquier número de procesos puede ser empleado. Por defecto sólo dos de los procesos se comunican. Utilizando la opción -bisect todos los procesos participan en la comunicación. Solapamiento entre comunicación y procesamiento. La opción -overlap realiza un simple test que mide el solapamiento entre comunicación y procesamiento del sistema enviando mensajes con una longitud determinada y generando una carga variable de procesamiento. Patrones de Comunicación. Existen una gran variedad de patrones de comunicación. De este modo, no sólo existe el modelo punto a punto; un modelo muy importante es el de comunicación colectiva, entre otros. La comunicación colectiva se mide con el programa goptest. Dicho programa utiliza muchas opciones similares a mpptest, y se maneja de manera muy similar. Para conocer su funcionamiento ejecutaremos : $ goptest -help Como ocurre con mpptest, también existe un script que facilita el manejo del programa goptest. Dicho script se denomina rungoptest. Podemos conocer sus opciones ejecutando: $ rungoptest -help Fiabilidad. La fiabilidad se comprueba mediante el programa stress. Este programa envía conjuntos de bits en mensajes de varios tamaños, y luego chequea cada bit del mensaje recibido en el proceso receptor. Para conocer sus opciones ejecutaremos: $ stress -help 8.2. Pruebas Realizadas El sistema en el cual realizamos las pruebas consiste en una red de estaciones de trabajo dedicadas a la realización de prácticas por parte de los alumnos de Ingeniería Técnica de 8.2. PRUEBAS REALIZADAS 131 Informática de Gestión de la Universidad de Cádiz. Dicho sistema se encuentra en el Aula de Informática II de la Escuela Superior de Ingeniería. Las máquinas en las cuales ejecutaremos los algoritmos tienen las siguientes características: Procesador AMD Duron a 1,2 Ghz 256 Mb RAM Distribución SUSE Linux 8.0 Todas las máquinas están conectadas a una red Ethernet (mediante una tarjeta Davicom Ethernet 100/10 Mbit) y comparten el sistema de ficheros ‘/home’ mediante NFS (“Network File System”, Sistema de Ficheros en Red). El servidor de ficheros es un computador Compaq DEC 3000/800S Alpha, con el sistema operativo Tru64 UNIX (Digital UNIX) V4.0E . La implementación MPI utilizada es MPICH Versión 1.2.4 . A continuación analizaremos el rendimiento y la fiabilidad de nuestro sistema al ejecutar distintos tipos de comunicación. 8.2.1. Comunicación Bloqueante Este test analiza el comportamiento del sistema en la situación más habitual: el paso de mensajes bloqueantes. E JECUCIÓN Ejecutaremos 16 procesos de los cuales dos de ellos se comunicarán. El tamaño de los mensajes variará entre 0 y 1024 bytes. Para ejecutarlo daremos la orden: $ mpirun -np 16 mpptest -gnuplot -fname ComBloq.mpl Como podemos observar el número de procesos lo fijamos mediante el programa mpirun. Por defecto mpptest utiliza para las pruebas mensajes cuyo tamaño varía entre 0 y 1024 bytes, con un salto de 32 bytes entre un tamaño y el siguiente. Con esta orden el fichero ‘ComBloq.gpl’ almacenará los datos para el gráfico, mientras que ‘ComBloq.mpl’ contendrá los comandos gnuplot para visualizarlos. Modificaremos éste último para mejorar el aspecto del resultado: set terminal postscript eps color set xlabel "Tamano (bytes)" set ylabel "Tiempo (micro-seg)" set title "Comunicacion Bloqueante" plot [] [:1200] ’ComBloq.gpl’ using 4:5 title "Com.Bloqueante" \ with lines lt 1 lw 3 exit A continuación daremos la siguiente orden para generar el gráfico ‘ComBloq.eps’: CAPÍTULO 8. EVALUACIÓN DEL SISTEMA 132 Comunicacion Bloqueante 1200 Com.Bloqueante 1000 Tiempo (micro-seg) 800 600 400 200 0 0 200 400 600 800 1000 1200 Tamano (bytes) Figura 8.1: Gráfico Comunicación Bloqueante $ gnuplot ComBloq.mpl > ComBloq.eps El gráfico que obtenemos como resultado lo tenemos en la figura 8.1. El eje T mide el tamaño de los mensajes, mientras que el eje U mide el tiempo necesario para transmitirlos. La línea roja representa el rendimiento de la comunicación bloqueante. A NÁLISIS DE LOS R ESULTADOS Como cabía esperar a medida que aumenta el tamaño del mensaje también lo hace el tiempo requerido para transmitirlo. Debemos observar que el retardo mínimo en la comunicación bloqueante es de 200 <>=@?A , tiempo requerido para transmitir un mensaje sin contenido (0 bytes). 8.2.2. Comunicación No Bloqueante Este test analiza el rendimiento de la comunicación no bloqueante en nuestro sistema. E JECUCIÓN Ejecutaremos 16 procesos de los cuales dos de ellos se comunicarán. El tamaño de los mensajes variará entre 0 y 1024 bytes. Utilizaremos comunicación no bloqueante. Para ejecutarlo daremos la orden: 8.2. PRUEBAS REALIZADAS 133 $ mpirun -np 16 mpptest -async -gnuplot -fname ComNoBloq.mpl La opción -async provoca la utilización de mensajes no bloqueantes en la comunicación. Con esta orden el fichero ‘ComNoBloq.gpl’ almacenará los datos para el gráfico, mientras que ‘ComNoBloq.mpl’ contendrá los comandos gnuplot para visualizarlos. Modificaremos éste último para comparar el rendimiento de la comunicación bloqueante y el de la no bloqueante, relacionándolos en el mismo gráfico: set terminal postscript eps color set xlabel "Tamano (bytes)" set ylabel "Tiempo (micro-seg)" set title "Comparacion Comunicacion Bloqueante-No Bloqueante" plot [] [:1200] ’ComNoBloq.gpl’ using 4:5 title "Com.No Bloqueante" \ with lines lt 1 lw 3, \ ’ComBloq.gpl’ using 4:5 title "Com.Bloqueante" \ with lines lt 3 lw 3 exit A continuación daremos la siguiente orden para generar el gráfico ‘ComBloq.eps’: $ gnuplot ComNoBloq.mpl > ComNoBloq.eps El gráfico que obtenemos como resultado lo tenemos en la figura 8.2. El eje T mide el tamaño de los mensajes, mientras que el eje U mide el tiempo necesario para transmitirlo. La línea roja representa el rendimiento de la comunicación no bloqueante y la azul el rendimiento de la comunicación bloqueante. A NÁLISIS DE LOS R ESULTADOS En el gráfico observamos que los resultados son muy parecidos a los del test anterior. La pequeña sobrecarga que genera el proceso de sincronización en este tipo de mensajes no afecta casi nada al rendimiento de la comunicación. 8.2.3. Participación Total de los Procesos Este test analiza el rendimiento de la comunicación bloqueante cuando todos los procesos participan en la comunicación. E JECUCIÓN Ejecutaremos 16 procesos de manera que todos ellos participen en la comunicación. La mitad de los procesos envían mensajes a la otra mitad. El tamaño de los mensajes variará entre 0 y 1024 bytes. Para ejecutarlo daremos la orden: CAPÍTULO 8. EVALUACIÓN DEL SISTEMA 134 Comparacion Comunicacion Bloqueante-No Bloqueante 1200 Com.No Bloqueante Com.Bloqueante 1000 Tiempo (micro-seg) 800 600 400 200 0 0 200 400 600 800 1000 1200 Tamano (bytes) Figura 8.2: Comparación Com.Bloqueante - No Bloqueante $ mpirun -np 16 mpptest -bisect -gnuplot -fname Bisect.mpl La opción -bisect provoca la participación de todos los procesos en la comunicación. Con esta orden el fichero ‘Bisect.gpl’ almacenará los datos para el gráfico, mientras que ‘Bisect.mpl’ contendrá los comandos gnuplot para visualizarlos. Modificaremos éste último para comparar el rendimiento de la comunicación participativa y el de la no participativa, relacionándolos en el mismo gráfico: set terminal postscript eps color set xlabel "Tamano (bytes)" set ylabel "Tiempo (micro-seg)" set title "Comparacion Com.Participativa-No Participativa" plot [] [:1600]’Bisect.gpl’ using 4:5 title "Com.Participativa" \ with lines lt 1 lw 3, \ ’ComBloq.gpl’ using 4:5 title "Com.No Participativa" \ with lines lt 3 lw 3 exit A continuación daremos la siguiente orden para generar el gráfico ‘ComBloq.eps’: $ gnuplot Bisect.mpl > Bisect.eps 8.2. PRUEBAS REALIZADAS 135 Comparacion Com.Participativa-No Participativa 1600 Com.Participativa Com.No Participativa 1400 Tiempo (micro-seg) 1200 1000 800 600 400 200 0 0 200 400 600 800 1000 1200 Tamano (bytes) Figura 8.3: Comparación Com. Participativa - No Participativa El gráfico que obtenemos como resultado lo tenemos en la figura 8.3. El eje T mide el tamaño de los mensajes, mientras que el eje U mide el tiempo necesario para transmitirlos. La línea roja representa el rendimiento de la comunicación participativa y la azul el rendimiento de la comunicación no participativa. A NÁLISIS DE LOS R ESULTADOS En el gráfico observamos que los resultados son algo distintos a los obtenidos en la comunicación bloqueante convencional. La sobrecarga en la comunicación que genera la participación de todos los procesos queda reflejada en un aumento del tiempo requerido para transmitir los mensajes. El retardo en la comunicación aumenta conforme crece el tamaño de la información a transmitir. 8.2.4. Solapamiento entre Comunicación y Procesamiento Los algoritmos paralelos en general suelen solapar comunicación y procesamiento en sus ejecuciones. Este test analiza el impacto que tiene sobre el rendimiento del sistema dicho solapamiento. E JECUCIÓN Ejecutaremos 16 procesos de los cuales dos de ellos se comunicarán. Al mismo tiempo el sistema realizará un determinado procesamiento. El tamaño de los mensajes quedará fijado en CAPÍTULO 8. EVALUACIÓN DEL SISTEMA 136 0 bytes (mensajes sin contenido). Para realizarlo daremos la orden: $ mpirun -np 16 mpptest -overlap -gnuplot -fname Overlap.mpl La opción -overlap realiza un simple test que mide el impacto que produce el solapamiento entre comunicación y procesamiento en el rendimiento del sistema. Este test se lleva a cabo enviando mensajes no bloqueantes sin contenido a la vez que se genera una carga variable de procesamiento. Con esta orden el fichero ‘Overlap.gpl’ almacenará los datos para el gráfico, mientras que ‘Overlap.mpl’ contendrá los comandos gnuplot para visualizarlos. Modificaremos éste último para comparar el rendimiento de la comunicación solapada y el de la no solapada, relacionándolos en el mismo gráfico: set terminal postscript eps color set xlabel "Carga de Procesamiento (bytes)" set ylabel "Tiempo (micro-seg)" set title "Impacto Solapamiento Comunicacion-Procesamiento" plot [] [182:194] ’Overlap.gpl’ using 4:5 title "Com.Solapada" \ with lines lt 1 lw 3 exit A continuación daremos la siguiente orden para generar el gráfico ‘ComBloq.eps’: $ gnuplot Overlap.mpl > Overlap.eps El gráfico que obtenemos como resultado lo tenemos en la figura 8.4. El eje T representa la carga de procesamiento medida en bytes, mientras que el eje U mide el tiempo necesario para transmitir un mensaje con dicha carga. La línea roja representa el rendimiento de la comunicación solapada. A NÁLISIS DE LOS R ESULTADOS Como observamos en el gráfico la presencia de procesamiento no provoca una caída del rendimiento de la comunicación en nuestro sistema. Ésto es lógico debido a que contamos con equipos relativamente potentes. De este modo el tiempo requerido para transmitir un mensaje de 0 bytes es prácticamente el mismo independientemente de la carga de trabajo del procesador. Por otro lado observamos muchos “picos” en la línea de la comunicación solapada; ello se debe a que la carga de procesamiento que genera este test es variable, por lo que la incidencia sobre el rendimiento también será variable. 8.2.5. Comunicación Colectiva Este test analiza el rendimiento de la comunicación colectiva utilizando el programa goptest. 8.2. PRUEBAS REALIZADAS 137 Impacto Solapamiento Comunicacion-Procesamiento 194 Com.Solapada 192 Tiempo (micro-seg) 190 188 186 184 182 0 200 400 600 800 1000 1200 Carga de Procesamiento (bytes) Figura 8.4: Comparación Com. Solapada - No Solapada E JECUCIÓN En el caso de las operaciones colectivas las mediciones no se deben basar sólo en el tamaño de los mensajes, ya que no es la única variable determinante; también debemos analizar el número de procesos que intervienen en su ejecución. Goptest es el programa encargado de generar datos para gráficos basándose en el número de procesos. La dificultad que entraña este tipo de mediciones es que no pueden ser realizadas en un solo paso; ello es debido a que el programa de arranque mpirun sólo puede seleccionar un número de procesos en la ejecución cada vez. Por lo tanto debemos realizar una ejecución del test para cada número de procesos que queramos incluir en el test. Así pues realizaremos cuatro ejecuciones consecutivas para medir el comportamiento del sistema en una comunicación colectiva de 2, 4, 8 y 16 procesos. La operación se comunicación colectiva que realizaremos será una operación de reducción que sumará los valores de todos los procesos. Dichos valores serán de tipo entero: $ mpirun -np 2 goptest -isum -sizelist 32,256,512,1024 -gnuplot -fname Goptest.mpl $ mpirun -np 4 goptest -isum -sizelist 32,256,512,1024 -gnuplot -fname Goptest.mpl $ mpirun -np 8 goptest -isum -sizelist 32,256,512,1024 -gnuplot -fname Goptest.mpl $ mpirun -np 16 goptest -isum -sizelist 32,256,512,1024 138 CAPÍTULO 8. EVALUACIÓN DEL SISTEMA -gnuplot -fname Goptest.mpl Como podemos observar el tipo de operación colectiva se determina mediante una opción. La opción isum indica la realización de una operación de reducción donde se suman los valores de todos los procesos, siendo dichos valores de tipo entero. La opción -sizelist indica los tamaños de mensajes a utilizar en las pruebas. En todo caso vemos que la ejecución de goptest es muy similar a la de mpptest, coincidiendo en la mayoría de las opciones disponibles. Lo siguiente será preparar el fichero de datos. Dado que realizamos cuatro ejecuciones, en cada una de ellas agregamos de manera dispersa información al fichero ‘Goptest.gpl’. Tendremos que preparar dicho fichero para que pueda ser utilizado por el programa gnuplot. Con esta finalidad debemos agrupar la información para que el fichero quede sin líneas vacías, dejándolo de la siguiente manera: #np time (us) for various sizes 2 579.500000 2037.400000 3946.940000 7333.680000 4 1606.820000 5923.540000 11683.560000 22859.700000 8 4418.800000 14938.040000 30276.300000 60126.700000 16 10821.880000 37237.340000 73761.080000 137344.780000 Ahora tenemos el fichero ‘Goptest.gpl’, que almacena los datos para el gráfico, y el fichero ‘Goptest.mpl’, que contiene los comandos gnuplot para visualizarlos. Modificaremos éste último de manera que gnuplot genere un gráfico Encapsulated Postscript manteniendo un buen aspecto: set term postscript eps color set xlabel "Num.Procesos" set ylabel "Tiempo (micro-seg)" set title "Comunicacion Colectiva Op.Reduccion-Entero" plot [] [:160000] ’Goptest.gpl’ using 1:2 title ’32’ \ with lines lt 1 lw 3,\ ’Goptest.gpl’ using 1:3 title ’256’ with lines lt 3 lw 3,\ ’Goptest.gpl’ using 1:4 title ’512’ with line lt 4 lw 3,\ ’Goptest.gpl’ using 1:5 title ’1024’ with lines lt 7 lw 3 exit A continuación daremos la siguiente orden para generar el gráfico ‘Goptest.eps’: $ gnuplot Goptest.mpl > Goptest.eps El gráfico que obtenemos como resultado lo tenemos en la figura 8.5. El eje T representa el número de procesos utilizados, mientras que el eje U mide el tiempo necesario realizar la comunicación colectiva. La línea roja representa el rendimiento de la comunicación colectiva utilizando los mensajes de 32 bytes, la azul los de 256 bytes, la morada los de 512 y la negra los de 1024 bytes. 8.2. PRUEBAS REALIZADAS 139 Comunicacion Colectiva Op.Reduccion-Entero 160000 32 256 512 1024 140000 Tiempo (micro-seg) 120000 100000 80000 60000 40000 20000 0 0 2 4 6 8 10 12 14 16 Num.Procesos Figura 8.5: Gráfico Comunicación Colectiva A NÁLISIS DE LOS R ESULTADOS En el gráfico podemos observar la incidencia que tienen sobre el rendimiento de la comunicación tanto el número de procesos que toman parte en ella como el tamaño de los mensajes que se transmiten . Ésto lo vemos claramente analizando dos datos: Una comunicación colectiva entre 16 procesos utilizando mensajes de 1024 bytes tarda unos 140.000 <>=@?A , mientras que con 4 procesos (una cuarta parte) tarda aproximandamente 22.000 <>=&?BA . Observamos una reducción del 85 %. Una comunicación colectiva de 16 procesos utilizando mensajes de 1024 bytes tarda uno 140.000 <>=&?BA , mientras que utilizando mensajes de 256 bytes (una cuarta parte) el tiempo total es de 35.000 <>=@?A aproximadamente. En este ejemplo sólo observamos una reducción del 75 %. Con estos datos vemos que el número de procesos es una variable algo más determinante que el tamaño de los mensajes, aunque esta última también reviste mucha importancia. Por lo demás vemos que el crecimiento de las funciones es similar en todos los tamaños de mensajes, y que este tipo de comunicación en general consume más tiempo que la comunicación punto a punto. CAPÍTULO 8. EVALUACIÓN DEL SISTEMA 140 8.2.6. Fiabilidad Este test comprueba la fiabilidad de la comunicación en el sistema utilizando el programa stress. E JECUCIÓN El programa stress realiza un test de fiabilidad sobre un determinado sistema. Este programa envía conjuntos de bits en mensajes de varios tamaños, y luego chequea cada bit del mensaje recibido en el proceso receptor. Podemos utilizar el número de procesos que creamos conveniente. Con la siguiente orden realizaremos un test con 4 procesos: $ mpirun -np 4 stress Por defecto el tamaño de los mensajes que utiliza está comprendido entre 0 y 1024 bytes, con un salto de 32 bytes entre un tamaño y el siguiente. La comunicación es de tipo bloqueante, aunque ésto lo podemos cambiar con la opción -async. A continuación exponemos la salida que genera este programa: Each to All ................................+ ................................+ ................................+ ................................+ ................................+ ................................+ ................................+ ................................+ ................................+ ................................+ ................................+ ................................+ ................................+ stress runs to Tue Jul 1 12:15:19 2003 (1) [0.641943 MB/s aggregate] Stress completed 1 tests 2.108621e+08 bytes sent La primera línea , “Each to All”, indica que cada proceso enviará mensajes a todos los demás. Cada una de las líneas de puntos representa una serie de mensajes que enviamos de un proceso a otro. Al utilizar la opción -verbose la línea de puntos es sustituida por una información para cada mensaje, que indica su tamaño y el patrón utilizado para generar el conjunto de bits que forma dicho mensaje; de esta manera cada línea de puntos representa lo siguiente: 8.2. PRUEBAS REALIZADAS .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running .Running + size size size size size size size size size size size size size size size size size size size size size size size size size size size size size size size size = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = A NÁLISIS DE LOS 141 32 longs with pattern ffffffff 64 longs with pattern ffffffff 96 longs with pattern ffffffff 128 longs with pattern ffffffff 160 longs with pattern ffffffff 192 longs with pattern ffffffff 224 longs with pattern ffffffff 256 longs with pattern ffffffff 288 longs with pattern ffffffff 320 longs with pattern ffffffff 352 longs with pattern ffffffff 384 longs with pattern ffffffff 416 longs with pattern ffffffff 448 longs with pattern ffffffff 480 longs with pattern ffffffff 512 longs with pattern ffffffff 544 longs with pattern ffffffff 576 longs with pattern ffffffff 608 longs with pattern ffffffff 640 longs with pattern ffffffff 672 longs with pattern ffffffff 704 longs with pattern ffffffff 736 longs with pattern ffffffff 768 longs with pattern ffffffff 800 longs with pattern ffffffff 832 longs with pattern ffffffff 864 longs with pattern ffffffff 896 longs with pattern ffffffff 928 longs with pattern ffffffff 960 longs with pattern ffffffff 992 longs with pattern ffffffff 1024 longs with pattern ffffffff R ESULTADOS El hecho de no obtener un mensaje de error quiere decir que el test ha sido satisfactorio y que disponemos de un sistema fiable para la ejecución de programas MPI. 142 CAPÍTULO 8. EVALUACIÓN DEL SISTEMA Capítulo 9 Evaluación de los Algoritmos El presente documento aborda dos aspectos de los algoritmos paralelos: su diseño y su análisis. El concepto de análisis se refiere al proceso de determinar cuánto de bueno es un algoritmo, o lo que es lo mismo, cuánto de rápido y eficiente es en el uso que le da a los recursos disponibles. En este capítulo hablaremos sobre las distintas herramientas de monitorización que nos ayudan a estudiar la ejecución de los algoritmos, definiremos más formalmente los criterios utilizados en la evaluación de su rendimiento y aplicaremos dicho análisis a los algoritmos implementados. 9.1. Herramientas de Monitorización La extensión MPE (“Multi-Processing Environment”, Entorno de Multi-Procesamiento) (sección A.7) trata de proporcionar a los programadores un conjunto completo de herramientas de análisis del rendimiento para los programas MPI. Entre estas herramientas tenemos un conjunto de librerías de monitorización, un conjunto de programas de utilidad y un conjunto de herramientas gráficas. Las librerías de monitorización proporcionan una colección de funciones que generan información acerca de la ejecución de los programas MPI. Se pueden utilizar manualmente mediante la inserción de llamadas MPE en los programas MPI, o bien automáticamente, enlazando el código MPI con las librerías MPE adecuadas. Normalmente se utiliza la segunda opción, como veremos más adelante. Las librerías de monitorización que actualmente están incluidas, y que a continuación pasaremos a describir con más precisión, son las siguientes: Librería de generación de ficheros de recorrido de la ejecución Librería de representación del trazado de la ejecución Librería de animación en tiempo real Los programas de utilidad incluidos en MPE son básicamente utilidades para el manejo y tratamiento de ficheros de recorrido, y las herramientas gráficas se utilizan en su visualización. Todo ello se analiza con más detalle en la siguiente sección. 143 CAPÍTULO 9. EVALUACIÓN DE LOS ALGORITMOS 144 9.1.1. Ficheros de Recorrido La librería para la generación de ficheros de recorrido es sin duda la más útil y ampliamente usada en MPE. Dicha librería se utiliza para volcar información relativa a la ejecución de las llamadas MPI en un fichero. Dichos ficheros representan el recorrido de la ejecución de los programas MPI, siendo generados durante su ejecución. Actualmente existen tres formatos de fichero. CLOG Es el formato por defecto. Básicamente es una colección de eventos asociados a etiquetas temporales que representan el momento en que se produjeron. Almacena los datos en formato binario. ALOG Existe por motivos de compatibilidad hacia atrás. Almacena los eventos en formato de texto ASCII. Actualmente no es mantenido ni desarrollado. SLOG Es el más potente. Proviene de “Scalable LOGfile”, Fichero de Recorrido Escalable. Almacena los datos como estados (esencialmente eventos con una duración) en un formato binario especial, diseñado para permitir el tratamiento de ficheros de recorrido muy grandes (del tamaño de varios Gigas). 9.1.1.1. Creación Generalmente el método más sencillo y seguro para la utilización de las librerías MPE es mediante el enlace del código MPI con las librerías MPE adecuadas. Para ello sólo debemos proporcionar un argumento (-mpilog) al comando de compilación que usemos normalmente (sección A.5). En realidad ésto sólo debe hacerse en el momento de enlazar el código. Por ejemplo, para crear una versión del programa RTrap (algoritmo Regla del Trapecio) que genere un fichero de recorrido, compilaremos y enlazaremos dando las siguientes órdenes: $ mpicc -c RTrap.c $ mpicc -o RTrap -mpilog RTrap.o Aunque también podemos compilar directamente: $ mpicc -o RTrap -mpilog RTrap.c Dado que el formato por defecto de salida es CLOG, el fichero de recorrido que obtendremos al ejecutar RTrap se llamará ‘RTrap.clog’. Si queremos que el fichero tenga otro formato de salida debemos utilizar la variable de entorno MPE_LOG_FORMAT. Dicha variable determina el formato de salida de los ficheros de recorrido. Los valores permitidos para MPE_LOG_FORMAT son CLOG, SLOG y ALOG. Cuando la variable MPE_LOG_FORMAT no está establecida se asume CLOG. Así pues, para generar el fichero de recorrido de RTrap en formato SLOG ejecutaremos lo siguiente: $ $ $ $ MPE_LOG_FORMAT=SLOG export MPE_LOG_FORMAT mpicc -c RTrap.c mpicc -o RTrap -mpilog RTrap.o De esta manera el fichero de recorrido que obtendremos al ejecutar RTrap será ‘RTrap.slog’. 9.1. HERRAMIENTAS DE MONITORIZACIÓN 9.1.1.2. 145 Visualización Para visualizar el contenido de los ficheros de recorrido, lo más recomendable es ejecutar el programa logviewer. Este script emplea el visualizador adecuado para tratar el fichero basándose en su extensión. Dependiendo del formato que tenga el fichero, logviewer empleará un visualizador u otro. Si queremos información acerca de sus opciones ejecutaremos: $ logviewer -help Si lo que queremos es visualizar un fichero de recorrido ejecutaremos: $ logviewer nombrefichero.extension El programa logviewer utiliza los siguientes visualizadores para mostrar el contenido de los ficheros de recorrido: upshot Utilizado para visualizar ficheros con el formato ALOG. Analiza eventos asociados a etiquetas temporales que representan el momento en que se produjeron, almacenados en formato de texto ASCII. jumpshot-2 Utilizado para visualizar ficheros con el formato CLOG. Este visualizador es una evolución de nupshot, el cual a su vez es una versión más rápida de upshot. Analiza los ficheros en formato binario y está escrito en Java. Su inconveniente radica en el impacto que tiene sobre su rendimiento el tamaño del fichero a analizar. Cuando el fichero CLOG sobrepasa los 4MB su rendimiento se deteriora de manera significativa. jumpshot-3 Utilizado para visualizar ficheros con el formato SLOG. Es la evolución natural de jumpshot-2 y también está escrito en Java. Este visualizador es fruto del esfuerzo de investigación desarrollado para solucionar los problemas de rendimiento de su antecesor. De este modo visualiza programas de una manera más escalable. Debido al formato de los ficheros que trata, este visualizador no analiza eventos convencionales si no estados (esencialmente eventos con una duración) en un formato binario especial, diseñado para permitir el tratamiento de ficheros de recorrido muy grandes (del tamaño de varios Gigas). 9.1.1.3. Conversión Para convertir los ficheros de recorrido de un formato a otro, tenemos las siguientes herramientas: clog2slog Conversor de ficheros CLOG a SLOG. Es muy útil en aquellos casos en los que la generación directa del fichero SLOG no funciona debido mal comportamiento del programa MPI. Además este conversor nos permite ajustar ciertos parámetros del fichero de recorrido, como el segmento del fichero a mostrar, etc. Para obtener más información ejecutaremos: $ clog2slog -h clog2alog Coversor de ficheros CLOG a ALOG. Se proporciona por motivos de compatibilidad hacia atrás. CAPÍTULO 9. EVALUACIÓN DE LOS ALGORITMOS 146 9.1.1.4. Impresión en Modo Texto Para imprimir los ficheros de recorrido en modo texto, tenemos los siguientes programas: slog_print Programa que imprime ficheros SLOG en modo texto por la salida estándar. Sirve para chequear el contenido del fichero de recorrido. Si el fichero SLOG es demasiado grande, puede no ser útil usar slog_print. Además cuando el fichero SLOG no está completo slog_print no funcionará. Por lo tanto servirá como un simple test para saber si el fichero SLOG está generado completamente. clog_print Programa que imprime ficheros CLOG en modo texto por la salida estándar. 9.1.2. Trazado de la Ejecución La librería para la representación del trazado de la ejecución muestra todas las llamadas a funciones MPI. Cada llamada a MPI imprime dos líneas de texto: una indicando que la llamada ha sido inicializada y otra indicando que la llamada ha sido completada. Tanto la primera como la segunda están precedidas por el identificador del proceso que la ejecuta en MPI_COMM_WORLD. Todo ésto se muestra por la salida estándar. La mayoría de las funciones de envío y recepción de mensajes indican además los valores de algunos de sus parámetros, como contador, etiqueta y compañero (proceso destino en envíos y origen en recepciones). El método más sencillo y seguro para la utilización de la librería de trazado MPE es mediante el enlace del código MPI con las librerías MPE adecuadas. Para ello sólo debemos proporcionar un argumento (-mpitrace) al comando de compilación que usemos normalmente (sección A.5). Como ya dijimos, ésto sólo debe hacerse en el momento de enlazar el código. Así, para crear una versión del programa HolaMundo (algoritmo ¡Hola Mundo!) que muestre el trazado de su ejecución, compilaremos y enlazaremos dando las siguientes órdenes: $ mpicc -c HolaMundo.c $ mpicc -o HolaMundo -mpitrace HolaMundo.o Aunque también podemos compilar directamente: $ mpicc -o HolaMundo -mpitrace HolaMundo.c De este modo al ejecutar: $ mpirun -np 2 HolaMundo la salida que obtendremos será parecida a lo siguiente: Starting MPI_Init... Starting MPI_Init... [0] Ending MPI_Init [0] Starting MPI_Comm_rank... 9.1. HERRAMIENTAS DE MONITORIZACIÓN [0] [0] [0] [0] [0] 147 Ending MPI_Comm_rank Starting MPI_Comm_size... Ending MPI_Comm_size Starting MPI_Get_processor_name... Ending MPI_Get_processor_name Proceso 0 en linux.local Encargado de la E/S ¡Hola Mundo! Numero Procesos: 2 Tiempo Procesamiento: 0.000087 [0] [1] [1] [1] [1] [1] [1] [1] [1] [1] [0] Starting MPI_Finalize... Ending MPI_Init Starting MPI_Comm_rank... Ending MPI_Comm_rank Starting MPI_Comm_size... Ending MPI_Comm_size Starting MPI_Get_processor_name... Ending MPI_Get_processor_name Starting MPI_Finalize... Ending MPI_Finalize Ending MPI_Finalize 9.1.3. Animación en Tiempo Real La librería gráfica MPE contiene funciones que permiten la compartición por parte de un conjunto de procesos de una ventana X, de manera que no esté asociada a ningún proceso específico. La librería de animación en tiempo real utiliza esta capacidad para dibujar flechas que representan el tráfico de mensajes mientras el programa se ejecuta. Observemos que los programas MPI pueden generar eventos de comunicación mucho más rápidamente de lo que un servidor X11 puede mostrar dichos eventos. Al igual que lo que ocurre con otras librerías, el método más sencillo y seguro para la utilización de la librería de animación en tiempo real es mediante el enlace del código MPI con las librerías MPE adecuadas. Para ello debemos proporcionar un argumento (-mpianim) al comando de compilación que usemos normalmente (sección A.5). Como ya dijimos, ésto sólo debe hacerse en el momento de enlazar el código. Sin embargo la librería de animación requiere el uso de operaciones del sistema X Window así como operaciones de la librería matemática. De este modo para crear una versión del programa RTrap (algoritmo Regla del Trapecio) que produzca una animación en tiempo real de su ejecución, compilaremos y enlazaremos dando las siguientes órdenes: $ mpicc -c RTrap.c $ mpicc -o RTrap -L/usr/X11R6/lib -lX11 -lm -mpianim RTrap.o CAPÍTULO 9. EVALUACIÓN DE LOS ALGORITMOS 148 Figura 9.1: Animación en Tiempo Real Aunque también podemos compilar directamente: $ mpicc -o RTrap -L/usr/X11R6/lib -lX11 -lm -mpianim RTrap.c Debido a que dicha librería utiliza operaciones del sistema XWindow, puede que tengamos problemas con los permisos de acceso al servidor X. En ese caso utilizaremos la orden xhost para permitir el acceso al servidor X. Si tiene problemas con la configuración de seguridad consulte al administrador del sistema. Al ejecutar la siguiente orden: $ mpirun -np 4 RTrap obtendremos una salida como que se muestra en la figura 9.1. 9.2. Criterios de Evaluación Una vez que un nuevo algoritmo ha sido diseñado, normalmente es evaluado usando los siguientes criterios: el tiempo de ejecución, el número de procesadores usados y el coste. Aparte de estas medidas estándar pueden utilizarse otras muchas medidas relacionadas con el hardware, aunque sólo deben ser empleadas cuando se conoce bien el entorno particular en donde dicho algoritmo es ejecutado. 9.2.1. Tiempo de Ejecución Dado que la mejora en la velocidad computacional es la principal razón por la cual se construyen sistemas paralelos, podemos decir que la medida más importante en la evaluación de un algoritmo paralelo es su tiempo de ejecución. Se define como el tiempo empleado por un algoritmo para resolver un problema en un computador paralelo, desde el momento en que comienza hasta que acaba. Si los procesadores implicados en al ejecución no empiezan y terminan su procesamiento al mismo tiempo, el tiempo de ejecución será calculado desde el momento en que el primer procesador comienza su ejecución, hasta que el último procesador termina. 9.3. RESULTADOS OBTENIDOS 149 9.2.2. Número de Procesadores El segundo criterio más importante en la evaluación de un algoritmo es el número de procesadores que requiere para resolver un problema. Cuesta dinero comprar, mantener y hacer que funcionen los computadores. Cuando varios procesadores están presentes el problema del mantenimiento se complica, y el precio necesario para garantizar un alto grado de fiabilidad aumenta de manera considerable. De esta manera, cuanto mayor sea el número de procesadores que utiliza un algoritmo para resolver un problema, más cara será la solución que obtendremos. 9.2.3. Coste El coste de un algoritmo se obtiene de la multiplicación de las dos medidas anteriores; de esta manera Coste = Tiempo de ejecución * Número de procesadores Ésta es la medida más indicativa del rendimiento de un algoritmo paralelo. De hecho será la que utilicemos en la evaluación de nuestros algoritmos. Su precisión radica en que relaciona el número de procesadores empleados con el tiempo de ejecución obtenido; por lo tanto, nos será útil para conocer en qué proporción aumenta el rendimiento del sistema al incrementar el número de procesadores. 9.3. Resultados Obtenidos A continuación realizaremos un análisis del rendimiento alcanzado por los algoritmos desarrollados de manera objetiva, intentando eliminar las variables que entorpezcan dicha objetividad. 9.3.1. Algoritmo Cálculo de Áreas mediante Montecarlo Bloqueante E JECUCIÓN En la ejecución del presente algoritmo debemos tener en cuenta algunos asuntos para que los tiempos arrojados por los tests realizados sean válidos. Lo primero que debemos hacer es eliminar el tiempo añadido por el usuario al introducir los datos. De esta manera fijaremos los datos de entrada mediante operaciones de asignación en el código. El segundo asunto es que necesitamos realizar bajo las mismas condiciones todas las ejecuciones. Los datos de entrada deben ser fijados siempre a los mismos valores. De este modo intentaremos buscar el número PI calculando el área de una circunferencia de radio 1 . Para 200.000.000 . computarlo generaremos el número de muestras En la figura 9.2 observamos el modelo de ejecución del algoritmo regla del trapecio con 8 procesos. 150 CAPÍTULO 9. EVALUACIÓN DE LOS ALGORITMOS Figura 9.2: Modelo Ejecución Cálculo de Áreas Bloqueante 8 Procesos 9.3. RESULTADOS OBTENIDOS Número Procesadores 1 2 4 8 16 151 Tiempo 140,1 seg 71,3 seg 36,1 seg 19,4 seg 11,0 seg Coste 140,1 142,6 144,4 155,2 176,0 Cuadro 9.1: Evaluación Cálculo de Áreas Bloqueante El cuadro 9.1 contiene para cada número de procesadores empleado el tiempo de ejecución medido en segundos y el coste obtenido. En el gráfico de la figura 9.3 exponemos la relación entre tiempo de ejecución y número de procesadores. La figura 9.4 contiene la curva de crecimiento del coste con respecto al número de procesadores. A NÁLISIS DE LOS R ESULTADOS El modelo de ejecución de este algoritmo puede ser interpretado fácilmente. En un principio el proceso 0 distribuye los datos de entrada a los demás procesos enviando a cada uno un mensaje bloqueante. Si hubiéramos introducido los datos a través del terminal en vez de haber fijado variables en estas pruebas, este paso hubiera sido más largo debido a que el resto de procesos tendrían que esperar a que el proceso 0 terminara de consultar los datos al usuario. Una vez realizado este paso se produce el procesamiento interno en cada uno de los procesos durante el cual no existe comunicación entre los procesos. Cuando los procesos culminan el tiempo de procesamiento envían su resultado local al proceso 0 mediante un mensaje bloqueante. El proceso 0 recoge dichos resultados locales, calcula la media de todos ellos e imprime el resultado global. Una vez hecho ésto el programa termina. Como podíamos esperar, cuantos más procesadores intervienen en la ejecución menor es el tiempo de ejecución. Sin embargo el coste aumenta gradualmente con respecto al número de procesadores. Por lo tanto deberíamos emplear más o menos procesadores dependiendo de la necesidad que tengamos de acelerar el procesamiento y de los recursos que podamos utilizar, intentando llegar a un compromiso entre rendimiento y coste que sea óptimo para nuestras necesidades. 9.3.2. Algoritmo Cálculo de Áreas mediante Montecarlo No Bloqueante E JECUCIÓN En la ejecución del presente algoritmo debemos tener en cuenta algunos asuntos para que los tiempos arrojados por los tests realizados sean válidos. Lo primero que debemos hacer es eliminar el tiempo añadido por el usuario al introducir los datos. De esta manera fijaremos los datos de entrada mediante operaciones de asignación en el código. El segundo asunto es que necesitamos realizar bajo las mismas condiciones todas las ejecuciones. Los datos de entrada deben ser fijados siempre a los mismos valores. De este modo intentaremos buscar el número PI calculando el área de una circunferencia de radio 1 . Para 200.000.000 . computarlo generaremos el número de muestras CAPÍTULO 9. EVALUACIÓN DE LOS ALGORITMOS 152 Calculo de Areas No Bloqueante: Tiempo Ejecucion 160 Tiempo Ejecucion 140 120 Tiempo (seg) 100 80 60 40 20 0 0 2 4 6 8 10 12 14 16 Numero de Procesadores Figura 9.3: Gráfico Tiempo Ejecución Cálculo de Áreas Bloqueante Calculo de Areas Bloqueante: Coste 180 Coste 175 170 Coste 165 160 155 150 145 140 135 0 2 4 6 8 10 12 Numero de Procesadores Figura 9.4: Gráfico Coste Cálculo de Áreas Bloqueante 14 16 9.3. RESULTADOS OBTENIDOS 153 Figura 9.5: Modelo Ejecución Cálculo de Áreas No Bloqueante 8 Procesos CAPÍTULO 9. EVALUACIÓN DE LOS ALGORITMOS 154 Número Procesadores 1 2 4 8 16 Tiempo 139,9 seg 71,3 seg 35,7 seg 18,8 seg 10,7 seg Coste 139,9 142,6 142,8 150,4 171,2 Cuadro 9.2: Evaluación Cálculo de Áreas No Bloqueante En la figura 9.5 observamos el modelo de ejecución del algoritmo regla del trapecio con 8 procesos. El cuadro 9.2 contiene para cada número de procesadores empleado el tiempo de ejecución medido en segundos y el coste obtenido. En el gráfico de la figura 9.6 exponemos la relación entre tiempo de ejecución y número de procesadores. La figura 9.7 contiene la curva de crecimiento del coste con respecto al número de procesadores. A NÁLISIS DE LOS R ESULTADOS El modelo de ejecución de este algoritmo puede ser interpretado fácilmente. En un principio el proceso 0 distribuye los datos de entrada a los demás procesos enviando a cada uno un mensaje no bloqueante. Si hubiéramos introducido los datos a través del terminal en vez de haber fijado variables en estas pruebas, este paso hubiera sido más largo debido a que el resto de procesos tendrían que esperar a que el proceso 0 terminara de consultar los datos al usuario. Una vez realizado este paso se produce el procesamiento interno en cada uno de los procesos durante el cual no existe comunicación entre los procesos. Cuando los procesos culminan el tiempo de procesamiento envían su resultado local al proceso 0 mediante un mensaje no bloqueante. El proceso 0 recoge dichos resultados locales, calcula la media de todos ellos e imprime el resultado global. Una vez hecho ésto el programa termina. Podemos observar que la versión no bloqueante de este algoritmo es un poco más eficiente que la versión bloqueante. Los tiempos de ejecución son algo menores, y por lo tanto el coste también. De este modo nos percatamos de la mejora en el rendimiento que proporciona la comunicación no bloqueante. 9.3.3. Algoritmo Regla del Trapecio E JECUCIÓN En la ejecución de este algoritmo debemos tener en cuenta algunos asuntos para que los tiempos arrojados por los tests realizados sean válidos. Lo primero que debemos hacer es eliminar el tiempo añadido por el usuario al introducir los datos. De esta manera fijaremos los datos de entrada mediante operaciones de asignación en el código. 9.3. RESULTADOS OBTENIDOS 155 Calculo de Areas No Bloqueante: Tiempo Ejecucion 140 Tiempo Ejecucion 120 Tiempo (seg) 100 80 60 40 20 0 0 2 4 6 8 10 12 14 16 Numero de Procesadores Figura 9.6: Gráfico Tiempo Ejecución Cálculo de Áreas No Bloqueante Calculo de Areas No Bloqueante: Coste 175 Coste 170 165 Coste 160 155 150 145 140 135 0 2 4 6 8 10 12 14 Numero de Procesadores Figura 9.7: Gráfico Coste Cálculo de Áreas No Bloqueante 16 156 CAPÍTULO 9. EVALUACIÓN DE LOS ALGORITMOS Figura 9.8: Modelo Ejecución Regla del Trapecio 8 Procesos 9.3. RESULTADOS OBTENIDOS Número Procesadores 1 2 4 8 16 157 Tiempo 124,4 seg 62,2 seg 31,8 seg 16,5 seg 8,5 seg Coste 124,4 124,4 127,2 132,0 136,0 Cuadro 9.3: Evaluación Regla del Trapecio El segundo asunto es que necesitamos realizar bajo las mismas condiciones todas las ejecuciones. Los datos de entrada deben ser fijados siempre a los mismos valores. De este modo intentaremos buscar el número PI a través de la regla del trapecio. Para este propósito, y como o qp n4rs y el intervalo b DFE:1d . ya explicamos en la sección 6.1, utilizaremos la función ^ ,.T\3 Para computarlo utilizaremos el número de segmentos 1.000.000.000 . En la figura 9.8 observamos el modelo de ejecución del algoritmo regla del trapecio con 8 procesos. El cuadro 9.3 contiene para cada número de procesadores empleado el tiempo de ejecución medido en segundos y el coste obtenido. En el gráfico de la figura 9.9 exponemos la relación entre tiempo de ejecución y número de procesadores. La figura 9.10 contiene la curva de crecimiento del coste con respecto al número de procesadores. A NÁLISIS DE LOS R ESULTADOS El modelo de ejecución de este algoritmo puede ser interpretado fácilmente. En un principio se produce una operación broadcast, con la cual distribuimos los datos de entrada desde el proceso 0 hacia los demás procesos. Si hubiéramos introducido los datos a través del terminal en vez de haber fijado variables en estas pruebas, este paso hubiera sido más largo debido a que el resto de procesos tendrían que esperar a que el proceso 0 terminara de consultar los datos al usuario. Una vez realizado este paso se produce el procesamiento interno en cada uno de los procesos durante el cual no existe comunicación entre los procesos. Cuando cada proceso ha culminado su propio procesamiento el proceso 0 recoge los resultados de cada proceso a través de una operación reduce. Luego el proceso 0 imprime los resultados y el programa termina. Como podíamos esperar, cuantos más procesadores intervienen en la ejecución menor es el tiempo de ejecución. Sin embargo el coste aumenta gradualmente a partir de la utilización de 4 procesadores. Por lo tanto deberíamos emplear más o menos procesadores dependiendo de la necesidad que tengamos de acelerar el procesamiento y de los recursos que podamos utilizar, intentando llegar a un compromiso entre rendimiento y coste que sea óptimo para nuestras necesidades. CAPÍTULO 9. EVALUACIÓN DE LOS ALGORITMOS 158 Regla del Trapecio: Tiempo Ejecucion 140 Tiempo Ejecucion 120 Tiempo (seg) 100 80 60 40 20 0 0 2 4 6 8 10 12 14 16 Numero de Procesadores Figura 9.9: Gráfico Tiempo Ejecución Regla del Trapecio Regla del Trapecio: Coste 138 Coste 136 134 Coste 132 130 128 126 124 122 0 2 4 6 8 10 12 Numero de Procesadores Figura 9.10: Gráfico Coste Regla del Trapecio 14 16 9.3. RESULTADOS OBTENIDOS Orden \ Num.Procs. 700 1400 2100 2800 3500 4200 4900 1 19,0 / 19,1 172,0 / 172,2 2237,2 / 2252,8 - 159 4 10,3 / 14,9 59,8 / 78,4 190,9 / 232,7 430,1 / 504,3 2074,1 / 2202,5 - 9 10,0 / 15,37 50,1 / 70,9 135,1 / 183,6 274,9 / 362,3 491,3 / 626,8 833,1 / 1032,6 1257,1 / 1527,3 16 19,1 / 23,2 57,3 / 78,9 134,0 / 183,3 261,1 / 344,8 458,5 / 599,9 726,7 / 930,2 1016,4 / 1291,9 Cuadro 9.4: Evaluación Multiplicación de Matrices de Fox 9.3.4. Algoritmo Multiplicación de Matrices de Fox E JECUCIÓN El algoritmo que presentamos ahora tiene una serie de características que lo diferencian del resto. Para evaluarlo debemos tener en cuenta una serie de factores. El primer factor es la sobrecarga en la comunicación. Para multiplicar matrices de orden 6 utilizando Ç procesos, este algoritmo necesita hacer Ç operaciones de broadcast y Ç+*ÉÈ Ç operaciones simples de paso de mensajes. El tamaño de cada uno de estos mensajes tampoco es una cuestión a desconsiderar; cada uno de los mensajes deberá pasar una matriz de orden 6$ È Ç , dato que coincide con el orden de las matrices locales de cada uno de los procesos. El segundo factor es que no podemos ejecutarlo con cualquier número de procesadores. El número de procesadores utilizado debe tener raiz entera para que una matriz cuadrada pueda ser distribuida equitativamente entre los procesos. En la figura 9.13 observamos el modelo de ejecución del algoritmo regla del trapecio con 9 procesos. El cuadro 9.4 muestra los tiempos obtenidos en la ejecución del algoritmo Multiplicación de Matrices de Fox. Contiene para cada cantidad de procesadores empleados los tiempos obtenidos en la multiplicación de matrices de distintos órdenes; los dos tiempos separados por el carácter ’/’ representan el tiempo requerido para multiplicar las matrices por un lado, y por otro el tiempo requerido para multiplicarlas y mostrar el resultado. Dichos tiempos se miden en segundos. Para cada orden de matrices utilizado resaltaremos en negrita el tiempo de multiplicación óptimo. Los tiempos marcados con el carácter ’-’ son omitidos debido a que no son razonables; dichos cálculos tardan demasiado. De este modo comprendemos el carácter exponencial de la curva de crecimiento del tiempo con respecto al tamaño de las matrices a multiplicar. La figura 9.11 muestra la curva de crecimiento del tiempo de ejecución con respecto al orden de las matrices para cada una de las cantidades de procesos empleados. La figura 9.12 hace lo propio con el coste. CAPÍTULO 9. EVALUACIÓN DE LOS ALGORITMOS 160 Comparacion Tiempo de distintas Cantidades de Procesos 2500 16 9 4 1 Tiempo (seg) 2000 1500 1000 500 0 500 1000 1500 2000 2500 3000 3500 4000 4500 5000 Orden Matriz Figura 9.11: Comparación Tiempos de distintas Cantidades de Procesos Comparacion Coste de distintas Cantidades de Procesos 16 9 4 1 18000 16000 14000 Coste 12000 10000 8000 6000 4000 2000 0 500 1000 1500 2000 2500 3000 3500 4000 4500 Orden Matriz Figura 9.12: Comparación Costes de distintas Cantidades de Procesos 5000 9.3. RESULTADOS OBTENIDOS 161 Figura 9.13: Modelo Ejecución Algoritmo de Fox 9 Procesos CAPÍTULO 9. EVALUACIÓN DE LOS ALGORITMOS 162 A NÁLISIS DE LOS R ESULTADOS El modelo de ejecución de este algoritmo con 9 procesos demuestra la sobrecarga en la comunicación que genera. Cada uno de los procesos en ejecución realiza 3 operaciones de bradcast y 6 de paso de mensajes bloqueantes (en realidad la función MPI_Sendrecv_replace() realiza un envío y una recepción por cada llamada). Cada una de los mensajes transmitidos tendrá al menos el mismo tamaño que las matrices locales de los procesos. Una vez que los procesos culminan el procesamiento de sus matrices locales, envían los resultados al proceso 0 para generar la salida. Como podemos observar en la figura 9.11 el crecimiento del tiempo de ejecución es exponencial para todas las cantidades de procesos utilizadas; sin embargo dicho crecimiento es más paulatino cuanto mayor es la cantidad de procesos empleados. En la figura 9.12 observamos que el coste depende tanto del número de procesos empleados como del tamaño de las matrices a multiplicar. De hecho vemos que para matrices de orden mayor a 2000 la utilización de 1 sólo procesador es la más costosa. Con este planteamiento podemos prever que para tamaños de matrices superiores a los empleados en las pruebas la utilización de 16 procesadores será la más rentable. Apéndice A Instalación, Configuración y Manejo de MPICH MPICH es una implementación del estándar MPI desarrollada por el Argonne National Laboratory que funciona en una amplia variedad de sistemas. MPICH contiene, además de la librería MPI, un entorno de programación para trabajar con programas MPI. El entorno de programación incluye un programa de arranque de procesos portable, librerías de monitorización destinadas a analizar el rendimiento de los programas, y una interfaz gráfica para X Window. Las características principales de MPICH son las siguientes: Compatibilidad total con MPI-1.2, incluyendo cancelaciones de envíos. Soporte para clusters heterogéneos. Los enlaces con C++ incluidos en la especificación MPI-2 están disponibles para las funciones MPI-1. Enlaces con Fortran 77 y Fortran 90. Una versión de código abierto está disponible para Windows NT. La instalación, configuración y manejo de esta versión son diferentes a las explicadas en este apéndice, el cual cubre sólo la versión Unix de MPICH. Soporte para una amplia variedad de entornos, incluyendo MPPs y clusters de SMPs. Sigue la mayoría de las recomendaciones GNU acerca de la instalación y compilación, incluyendo VPATH. Compatibilidad con algunas partes de la especificación MPI-2, de modo que incluye: ; ; ; La mayoría de las funcionalidades de E/S paralela (MPI-IO) a través de la implementación ROMIO. Algunas funcionalidades de administración de hilos (MPI_INIT_THREAD). Nuevas funciones MPI_Info() y MPI_Datatype(). 163 APÉNDICE A. INSTALACIÓN, CONFIGURACIÓN Y MANEJO DE MPICH 164 Incluye componentes pertenecientes al entorno de programación paralela, como son: ; Herramientas de monitorización. ; ; Herramientas para la visualización de los análisis del rendimiento (upshot y jumpshot). ; A.1. Amplios tests de fiabilidad y rendimiento. Ejemplos de aplicaciones tanto simples como complejos. Dispositivos La portabilidad de MPICH proviene de su diseño en dos capas. La mayor parte del código de MPICH es independiente del hardware y está implementado por encima de una ADI (“Abstract Device Interface”, Interfaz de Dispositivo Abstracto). La interfaz ADI, llamada también dispositivo, oculta la mayoría de los detalles específicos del hardware permitiendo que MPICH sea portado fácilmente a nuevas arquitecturas. La interfaz ADI (o dispositivo) se encarga de facilitar el acceso al hardware de una manera eficiente, mientras que el resto del código por encima de la ADI se encarga de la sintaxis y la semántica de MPI. A continuación mostramos una lista con los dispositivos (ADIs) disponibles en MPICH. Clusters, Beowulf y Estaciones de Trabajo Individuales. Los dispositivos más importantes son ch_p4 y ch_p4mpd. El dispositivo ch_p4 es el más general y soporta nodos SMP, programas MPMD y sistemas heterogéneos. El dispositivo ch_p4mpd (más rápido) soporta sólo clusters homogéneos de uniprocesadores, pero proporciona un arranque más rápido y escalable. Topologías de tipo Rejilla. El dispositivo globus2 utiliza Globus (URL 19) para proporcionar una implementación diseñada para topologías de tipo rejilla. Este dispositivo es apropiado para sistemas en los que está instalado Globus. Multiprocesadores Simétricos. El dispositivo ch_shmem es apropiado para sistemas basados en memoria compartida. Utiliza la memoria compartida para pasar mensajes entre los procesadores, y hace uso de las facilidades proporcionadas por el sistema operativo. El dispositivo ch_lfshmem es una versión de ch_shmem que no utiliza bloqueos, desarrollada para el NEC-SX4. Procesadores Paralelos Masivos (MPPs). MPICH fue desarrollado originalmente para proporcionar una implementación MPI a la mayoría de los MPPs existentes, cada uno de los cuales tiene su propio sistema de paso de mensajes. De este modo se incluyen los dispositivos ch_meiko, ch_nx y ch_mpl en las distribuciones de MPICH. Otros. MPICH ha sido diseñado para permitir que otros grupos lo usen cuando desarrollen sus propias implementaciones MPI. Tanto las compañías privadas como los grupos de investigación han usado MPICH como base para sus implementaciones. Una implementación importante desarrollada de esta manera es MPICH-GM, la cual se emplea en clusters Myrinet conectados mediante hubs dedicados. A.2. OBTENCIÓN 165 A.2. Obtención El primer paso para poder utilizar MPICH será descargarlo e instalar los parches necesarios: 1. Obtener MPICH. El camino más fácil para hacerlo es usar la sección de descargas de su página web (URL [10]). De esta manera podremos descargar fácilmente el fichero ‘mpich.tar.gz’. Dicho fichero contiene la distribución MPICH completa. 2. Descomprimir el fichero ‘mpich.tar.gz’ en un directorio de trabajo. Se recomienda usar una partición montada localmente antes que usar una partición NFS (“Network File System”, Sistema de Ficheros en Red). La mayoría de los sistemas tienen el directorio ‘/tmp’ o ‘/sandbox’ montado localmente. Asegúrese de que existe espacio suficiente disponible (100MB debería ser más que suficiente). Para descomprimir el fichero ‘mpich.tar.gz’ (asumiendo que ha sido descargado en /tmp) usaremos: $ cd /tmp $ gunzip mpich.tar.gz $ tar -xvf mpich.tar 3. Aplicar los parches. Visite la sección de parches de la página web de MPICH (URL [11]) para verificar la existencia de parches que pueda necesitar. Normalmente las versiones disponibles de MPICH tienen ya aplicados dichos parches; ésto queda indicado por cuatro números en el nombre de la versión (p.ej. 1.2.2.3). En otros casos el parche aislado se pone a disposición antes que la versión parcheada. Para aplicar estos parches siga las instrucciones expuestas en la página indicada y en la documentación. A.3. Compilación e Instalación Antes de poder usar MPICH debemos configurarlo y compilarlo. El proceso de configuración analiza nuestro sistema y determina las opciones y ajustes correctos; además crea el fichero ‘Makefile’, utilizado para compilar MPICH. 1. Decida dónde quiere instalar MPICH. Este paso no es estrictamente necesario (excepto en el caso del dispositivo ch_p4mpd); sin embargo la instalación de MPICH (que puede realizarse sin necesidad de privilegios en un directorio de usuario) hace más fácil la aplicación de actualizaciones y permite una reducción del espacio en disco que MPICH necesita, ya que la versión instalada sólo contiene librerías, cabeceras, documentación y algunos programas de soporte. Se recomienda una ruta de instalación que contenga el número de versión de MPICH. Para instalar MPICH de manera que puedan usarlo otros usuarios podríamos escoger el directorio ‘/usr/local/mpich-1.2.4/’, aunque debemos tener en cuenta los permisos necesarios para hacerlo. Si estamos instalando sólo para uso propio, podríamos usar algo como ‘/home/me/software/mpich-1.2.4’. 2. Invocar configure con el argumento -prefix adecuado: 166 APÉNDICE A. INSTALACIÓN, CONFIGURACIÓN Y MANEJO DE MPICH $ ./configure -prefix=$HOME/mpich-1.2.4 | tee c.log Este comando hace que MPICH utilice el dispositivo ch_p4 por defecto ; ésta es normalmente la elección adecuada. La salida de configure es encauzada hacia tee; este programa escribe la salida tanto en el fichero especificado en su argumento (aquí ‘c.log’) como en la salida estándar . Si tenemos problemas en la configuración o la compilación el fichero ‘c.log’ nos servirá de ayuda para identificar los posibles problemas. En la sección A.4.3 se explica cómo utilizar el programa ssh en vez de rsh para el arranque de procesos remotos. 3. Compilar MPICH: $ make | tee make.log Ésto puede llevar un tiempo dependiendo de la carga de nuestro sistema y de nuestro servidor de ficheros; puede durar desde algunos minutos hasta una hora o más. 4. (Opcional) Si queremos instalar MPICH en un lugar público de manera que los demás usuarios puedan usarlo, ejecutaremos: $ make install De este modo instalaremos MPICH en el directorio especificado por el argumento prefix del comando configure. La instalación estará formada por los directorios ‘include’, ‘lib’, ‘bin’, ‘sbin’, ‘www’ y ‘man’, además de un pequeño directorio con ejemplos. Si queremos eliminar la instalación debemos usar el script ‘sbin/mpiuninstall’ dentro del directorio de MPICH. A.4. Configuración A.4.1. El Fichero de Máquinas Cuando utilizamos el dispositivo ch_p4 el programa mpirun utiliza un fichero llamado fichero de máquinas, el cual lista las máquinas o nodos que están disponibles para ejecutar programas MPICH. La manera más fácil de crear un fichero de máquinas consiste en editar el fichero ‘mpich/util/machines/machines.xxxx’, el cual contiene los nombres de las máquinas pertenecientes a la arquitectura xxxx. Cuando mpirun es ejecutado, el número de hosts requerido por dicho comando es seleccionado del fichero de máquinas para su ejecución. No existe una planificación compleja para elegir qué máquina usar; las máquinas son seleccionadas desde el principio del fichero. Para ejecutar todos los procesos en una sola máquina, simplemente pondremos en todas las líneas del fichero la misma máquina. Debemos advertir que el fichero de máquinas no puede contener líneas en blanco. En caso de querer darle cierta estructura y para añadir comentarios utilizaremos el carácter ‘#’. Un ejemplo del fichero ‘machines.LINUX’ podría ser: A.4. CONFIGURACIÓN 167 #FICHERO MAQUINAS ARQ. LINUX galba neron oton # vitelio vespasiano tito #FINAL FICHERO MAQUINAS Los nombres deben ser proporcionados con el mismo formato que la salida del comando hostname. Por ejemplo, si el resultado de hostname en galba fuera galba.aulas y lo mismo sucediera para los demás nombres, el fichero de máquinas sería entonces: #FICHERO MAQUINAS ARQ. LINUX galba.aulas neron.aulas oton.aulas # vitelio.aulas vespasiano.aulas tito.aulas #FINAL FICHERO MAQUINAS En el caso de que tengamos nodos con múltiples procesadores, debemos indicar su número de procesadores tras su nombre y dos puntos. Por ejemplo, si en el ejemplo anterior vitelio tuviera dos procesadores el fichero de máquinas sería: #FICHERO MAQUINAS ARQ. LINUX galba neron oton # vitelio:2 vespasiano tito #FINAL FICHERO MAQUINAS A.4.2. RSH El programa rsh (“Remote SHell”, Shell Remoto) es utilizado para iniciar procesos remotos en general. Si pretendemos usar rsh con el dispositivo ch_p4 necesitaremos configurar APÉNDICE A. INSTALACIÓN, CONFIGURACIÓN Y MANEJO DE MPICH 168 nuestra máquina para permitir el uso de rsh. Por motivos de seguridad sólo debemos hacer ésto si somos un administrador del sistema y entendemos lo que estamos haciedo, o en el caso de que utilicemos una red aislada. Por ejemplo, si estamos utilizando una red Linux en nuestra casa o en el trabajo, y estas máquinas no están conectadas a una red más grande, debemos seguir las siguientes instrucciones. Si por el contrario alguna de nuestras máquinas está conectada a otra red, debemos preguntarle a nuestro administrador del sistema acerca de la política de utilización de rsh. Como alternativa podemos usar ssh. Lo siguiente explica cómo configurar una máquina de manera que pueda usar rsh para iniciar un proceso en ella misma. Para ello debemos asegurarnos de que exista un fichero ‘/etc/hosts.equiv’ que contenga al menos las siguientes líneas: localhost nombre_maquina donde nombre_maquina es el nombre que le damos a nuestra máquina en ‘/etc/hosts’. Para este propósito también podemos utilizar el fichero ‘.rhosts’ en el directorio de casa si queremos aplicarlo sólo a una cuenta de usuario. Por otro lado debemos asegurarnos de que los ficheros ‘/etc/hosts.allow’ y ‘/etc/hosts.deny’ estén vacíos. A.4.3. SSH El mecanismo habitual para el arranque de procesos remotos al utilizar el dispositivo ch_p4 en redes es rsh. El uso de rsh requiere ciertos permisos en las máquinas implicadas. Sin embargo en algunas redes no conviene establecer los permisos de esta manera. La alternativa más simple a rsh es el uso de ssh (“Secure Shell”, Shell Seguro). Éste puede ser utilizado como mecanismo seguro de procesamiento distribuido. Requiere cierta configuración que describiremos seguidamente, aunque su utilización es muy sencilla. Dicha configuración depende de la versión de ssh que vayamos a utilizar. Exponemos a continuación el conjunto de pasos necesarios para que ssh funcione correctamente con MPICH. 1. Debemos asegurarnos de que ssh está instalado en nuestra red y, en caso de estarlo, compruebaremos su versión. Si no está instalado podemos obtenerlo nosotros mismos (URL [12]). 2. Crear nuestra clave de autentificación. a) Si utilizamos SSH Versión 1 ejecutaremos: $ ssh-keygen Ésto creará el par clave privada/pública. La clave privada se almacenará en ~/.ssh/identity y la clave pública se guardará en ~/.ssh/identity.pub b) Si utilizamos SSH Versión 2 ejecutaremos: A.4. CONFIGURACIÓN 169 $ ssh-keygen -t dsa Si no lo hacemos así, generaremos una clave rsa1 que no podrá ser usada con SSH Versión 2. Dicha orden creará el par clave privada/pública. La clave privada se almacenará en ~/.ssh/id_dsa y la clave pública se guardará en ~/.ssh/id_dsa.pub 3. Autorizar el acceso. Si estamos utilizando SSH Versión 1 añada su clave pública al fichero ‘/.ssh/authorized_keys’. Todas las claves de este fichero tendrán permitido el acceso: $ cp ~/.ssh/identity.pub ~/.ssh/authorized_keys Si la máquina a la que queremos conectarnos no comparte el sistema de ficheros, entonces debemos copiar el fichero ‘~/.ssh/identity.pub’ sobre el fichero ‘~/.ssh/authorized_keys’ de la máquina a la que queremos conectarnos. Debemos asegurarnos de que el fichero ‘~/.ssh/authorized_keys’ no tenga permiso de escritura por parte de los miembros de nuestro grupo, de manera que tendremos que ejecutar: $ chmod go-rwx ~/.ssh/authorized_keys En caso de utilizar SSH Versión 2 copiaremos el fichero ‘id_dsa.pub’ sobre ‘authorized_keys2’ en vez de ‘authorized_keys’. Por lo demás todo es igual. Una vez hechos estos cuatro pasos ya podremos utilizar ssh en nuestro sistema. Ahora tendremos que indicar a MPICH que haga uso de dicho programa; ésto puede hacerse de dos formas: Ejecutar configure con la opción -rsh=ssh para que el dispositivo ch_p4 utilice ssh en vez de rsh. De esta manera también los scripts serán modificados para hacer uso de ssh. Si MPICH ya había sido compilado antes debemos reconfigurar y recompilar utilizando los siguientes comandos: $ configure -rsh=ssh $ make Si no es así utilizaremos las opciones -prefix para indicar el directorio de la instalación, y -rsh=ssh para elegir ssh como programa de arranque de procesos remotos. Luego compilaremos: $ ./configure -rsh=ssh -prefix=$HOME/mpich-1.2.4 | tee c.log $ make 170 APÉNDICE A. INSTALACIÓN, CONFIGURACIÓN Y MANEJO DE MPICH Establecer la variable de entorno P4_RSHCOMMAND de manera que contenga el nombre del programa a utilizar como arranque de procesos remotos. En nuestro caso ejecutaremos: $ P4_RSHCOMMAND=ssh $ export P4_RSHCOMMAND En caso de tener problemas: Asegúrese de que las máquinas listadas en nuestro fichero de máquinas estén también listadas en el fichero ‘/etc/ssh_known_hosts’ de la red o en el fichero ‘~/.ssh/known_hosts’ en el directorio de casa. Es importante que el directorio ‘/tmp’ tenga los permisos establecidos a 03777, con root como dueño y grupo 0. Openssh tiene el argumento -v, el cual es muy útil para identificar posibles problemas. A.4.4. Secure Server Dado que todas las estaciones de trabajo pertenecientes a un cluster requieren (usualmente) que cada nuevo usuario se conecte mediante login y password, y debido a que este proceso consume mucho tiempo, MPICH proporciona un programa que puede ser utilizado para acelerar este proceso. Este programa se llama “Secure Server” (Servidor Seguro) y se localiza en el fichero ‘bin/serv_p4’ dentro del directorio de instalación de MPICH. El script ‘bin/chp4_servs’ puede ser utilizado para iniciar el servidor seguro en aquellas estaciones de trabajo en las que podamos ejecutar programas remotos mediante rsh, ssh o similar. También podemos iniciar el servidor a mano y permitir que se ejecute en segundo plano; ésto es apropiado en máquinas que no acepten conexiones remotas y sin embargo tengamos una cuenta en ellas. El servidor seguro requerirá al usuario que introduzca un password si los ficheros ‘/etc/hosts’ o ‘$HOME/.rhosts’ no tienen las entradas adecuadas. En otras palabras, si rsh requiere un password, también lo requerirá el servidor seguro. De este modo el servidor seguro tiene dos implicaciones bien diferenciadas en la seguridad: 1. Para ejecutarlo de manera remota en otras máquinas necesitamos tener conexión rsh, ssh o similar con ellas. 2. El servidor seguro utiliza el mismo procedimiento que rsh para comprobar los permisos de ejecución de los usuarios remotos. Por lo tanto la manera más cómoda de utilizarlo es empleando rsh como programa de arranque de procesos remotos. Por otro lado también podemos usar ssh para ejecutar el servidor de manera remota, y establecer la configuración adecuada para poder ejecutar procesos mediante el servidor seguro (aunque ni siquiera esté instalado rsh). En este caso debemos tener en cuenta que los scripts generados por MPICH utilizan por defecto rsh para su ejecución; si queremos que dichos scripts hagan uso de ssh, tendremos que modificarlos o configurar MPICH con la opción -rsh=ssh. A.5. COMPILACIÓN Y ENLACE DE PROGRAMAS 171 Debemos aclarar que tanto el fichero ‘serv_p4’ como ‘chp4_servs’ se encuentran en directorios pertenecientes a la instalación de MPICH, generados por la ejecución del comando ‘make install’ (sección A.3). Antes de arrancar el servidor debemos comprobar si el servidor seguro ha sido inicializado en nuestro sistema para uso general; si es así, el mismo servidor puede ser utilizado por todos los usuarios. Para realizarlo de esta manera necesitamos acceder al sistema como root. Si el servidor seguro no ha sido inicializado, entonces podemos inicializarlo para nuestro uso personal sin necesisdad de privilegios especiales con la orden: $ chp4_servs -port=1234 Esta orden inicializa el servidor seguro en todas las máquinas listadas en el fichero de máquinas (sección A.4.1). El número de puerto proporcionado por la opción -port (por defecto 1234) debe ser diferente del resto de puertos utilizados en las estaciones de trabajo. Para hacer uso del servidor seguro con el dispositivo ch_p4 debemos establecer las siguientes variables de entorno: $ $ $ $ MPI_USEP4SSPORT=yes export MPI_USEP4SSPORT MPI_P4SSPORT=1234 export MPI_P4SSPORT El valor de MPI_P4SSPORT debe ser el número de puerto en el cual inicializamos el servidor seguro. Cuando estas variables de entorno están establecidas, mpirun intenta utilizar el servidor seguro para arrancar programas que usen el dispositivo ch_p4; si ésto no da resultado utilizará el programa de arranque de procesos remotos que tengamos por defecto (normalmente rsh o ssh). También podemos hacer que mpirun utilice el servidor seguro empleando la opción -p4ssport <num>, que indica el número de puerto donde el servidor seguro está instalado. A.5. Compilación y Enlace de Programas La implementación MPICH proporciona cuatro comandos para compilar y enlazar programas escritos en C (mpicc), C++ (mpiCC), Fortran 77 (mpif77) y Fortran 90 (mpif90). Estos comandos se emplean de la misma manera en la que se utilizan los compiladores de C, Fortran 77, C++ y Fortran 90. Por ejemplo, para compilar utilizaríamos las siguientes órdenes: mpicc -c foo.c mpif77 -c foo.f mpiCC -c foo.C mpif90 -c foo.f Mientras que para enlazar utilizaríamos: 172 APÉNDICE A. INSTALACIÓN, CONFIGURACIÓN Y MANEJO DE MPICH mpicc -o foo foo.o mpif77 -o foo foo.o mpiCC -o foo foo.o mpif90 -o foo foo.o Las órdenes para enlazar código pueden requerir la inclusión de librerías adicionales. Por ejemplo, para utilizar la librería de funciones matemáticas de C usaríamos la siguiente orden: mpicc -o foo foo.o -lm También podemos combinar la compilación y el enlace en una sola orden, como exponemos a continuación: mpicc -o foo foo.c mpif77 -o foo foo.f mpiCC -o foo foo.C mpif90 -o foo foo.f Notar que mientras los sufijos .c para los programas en C y .f para los programas en Fortran-77 son estándar, no existe concenso para los sufijos de los programas escritos en C++ y Fortran90. Los sufijos mostrados en los ejemplos expuestos son aceptados por la mayoría de los sistemas, aunque no todos. MPICH intenta determinar los sufijos aceptados, pero no es capaz siempre. Podemos modificar la elección del compilador a utilizar por MPICH especificando las variables de entorno MPICH_CC, MPICH_F77, MPICH_CCC y MPICH_F90. Sin embargo ésto sólo funcionará si el compilador alternativo es compatible con el compilador por defecto (esto quiere decir que utilice los mismos tamaños para los tipos de datos y sus composiciones, y que genere un código objeto que sea compatible con el utilizado en las librerías MPICH). Si lo que queremos es modificar el enlazador, utilizaremos las variables de entorno MPICH_CLINKER, MPICH_F77LINKER, MPICH_CCLINKER y MPICH_F90LINKER. Si lo que queremos es ver los comandos que utilizaría la orden de compilación sin ejecutarlos realmente emplearemos el argumento -show. Las siguientes opciones nos ayudan a acceder a las librerías de monitorización del entorno MPE: -mpilog Compila una versión que genera el fichero de recorrido de la ejecución. -mpitrace Compila una versión que muestra el trazado de la ejecución. -mpianim Compila una versión que genera animación en tiempo real. A.6. Ejecución de Programas con mpirun Para ejecutar programas MPI debemos utilizar el programa mpirun, localizado en el directorio ‘bin/’ de MPICH. Bajo la mayoría de los sistemas podremos usar la orden mpirun -np 4 a.out A.7. EXTENSIÓN MPE 173 para ejecutar el programa ‘a.out’ en 4 procesadores. La orden ‘mpirun -help’ nos ofrece una lista completa de las opciones que tiene este programa. Como salida mpirun retorna el status de uno de los procesos, usualmente el proceso con identificador 0 en MPI_COMM_WORLD. A.7. Extensión MPE Como ya sabemos, MPI proporciona una base estable para la construcción de programas paralelos. Uno de los objetivos de su diseño es permitir la construcción de extensiones que contengan librerías de software paralelo que ayuden en el desarrollo de aplicaciones paralelas extensas. La extensión MPE (“Multi-Processing Environment”, Entorno de Multi-Procesamiento) explota las características de MPI para ofrecer una serie de facilidades útiles. Dicha extensión fué desarrollada para la implementación MPICH (estando incluida en su distribución), aunque puede ser usada por cualquier implementación MPI. Actualmente los principales componentes de MPE son: Librerías de monitorización utilizadas para generar información acerca del rendimiento de los programas MPI, y herramientas para el estudio y visualización de dicha información. Librería gráfica para la ejecución de programas paralelos en X Window. Funciones para la secuencialización del código ejecutado en paralelo. Funciones para la depuración de errores. 174 APÉNDICE A. INSTALACIÓN, CONFIGURACIÓN Y MANEJO DE MPICH Apéndice B Manual de Referencia Este apéndice describe de manera resumida las funciones MPI utilizadas en los algoritmos implementados. Pretendemos que sea una guía útil como manual de consulta. Para mayor comodidad las funciones están expuestas en orden alfabético. Para cada una de las funciones definimos su funcionalidad y su sintaxis, especificando cuáles son los parámetros de entrada y los de salida. Por último, en algunos casos haremos un breve comentario acerca de su funcionamiento. 175 APÉNDICE B. MANUAL DE REFERENCIA 176 B.1. MPI_Bcast F UNCIONALIDAD Envía un mismo mensaje desde un proceso a todos los demás. Es una operación de comunicación colectiva. S INTAXIS int MPI_Bcast(void* mensaje, int contador, MPI_Datatype tipo_datos, int raiz, MPI_Comm com); PARÁMETROS E NTRADA /S ALIDA mensaje Dirección inicial de memoria del mensaje a enviar/recibir contador Número de elementos del tipo tipo_datos que componen el mensaje tipo_datos Tipo de datos MPI de cada elemento del mensaje (Cuadro 5.1) raiz Identificador del proceso que envía el mensaje com Comunicador en el cual se produce la comunicación C OMENTARIOS Si el soporte hardware se hace responsable de la ejecución de esta función, generalmente se implementa mediante un algoritmo que estructura un modelo de comunicación en árbol entre los procesos. Para más información, léase la sección ??. B.2. MPI_CART_COORDS 177 B.2. MPI_Cart_coords F UNCIONALIDAD Retorna las coordenadas de un proceso en un comunicador de topología cartesiana, dado su identificador. S INTAXIS int MPI_Cart_coords(MPI_Comm cart_com, int id, int numero_dimensiones, int* coordenadas) PARÁMETROS E NTRADA cart_com Comunicador de topología cartesiana id Identificador del proceso dentro del comunicador cart_com numero_dimensiones Número de dimensiones del vector coordenadas PARÁMETROS S ALIDA coordenadas Vector de enteros (de tamaño numero_dimensiones) que contiene las coordenadas cartesianas del proceso APÉNDICE B. MANUAL DE REFERENCIA 178 B.3. MPI_Cart_create F UNCIONALIDAD Crea un nuevo comunicador de topología cartesiana (tipo rejilla). S INTAXIS int MPI_Cart_create(MPI_Comm antiguo_com, int numero_dimensiones, int* tam_dimensiones, int* periodicidades, int reordenamiento, MPI_Comm* com_cartesiano) PARÁMETROS E NTRADA antiguo_com Comunicador original a partir del cual creamos el nuevo comunicador de topología cartesiana numero_dimensiones Número de dimensiones de la rejilla cartesiana que queremos crear tam_dimensiones Vector de enteros de tamaño numero_dimensiones que especifica el número de procesos en cada dimensión periodicidades Vector de booleanos de tamaño numero_dimensiones que especifica para cada dimensión si es periódica (verdadero) o no (falso) reordenamiento Booleano que especifica si pueden ser reordenados los identificadores de los procesos (verdadero) o no (falso). En muchos casos es apropiado para la optimización del sistema, en otros no es significante. PARÁMETROS S ALIDA com_cartesiano Nuevo comunicador de topología cartesiana B.4. MPI_CART_RANK 179 B.4. MPI_Cart_rank F UNCIONALIDAD Retorna el identificador de un proceso en un comunicador de topología cartesiana, dadas sus coordenadas. S INTAXIS int MPI_Cart_rank(MPI_Comm cart_com, int* coordenadas, int* id) PARÁMETROS E NTRADA cart_com Comunicador de topología cartesiana coordenadas Vector de enteros (de tamaño numero_dimensiones, véase función B.3) que especifica las coordenadas cartesianas del proceso PARÁMETROS S ALIDA id Identificador del proceso especificado APÉNDICE B. MANUAL DE REFERENCIA 180 B.5. MPI_Cart_sub F UNCIONALIDAD Divide un comunicador de tipo rejilla (topología cartesiana) en rejillas de menores dimensiones. S INTAXIS int MPI_Cart_sub(MPI_Comm cart_com, int* var_coords, MPI_Comm nuevo_com) PARÁMETROS E NTRADA cart_com Comunicador de topología cartesiana var_coords Vector de booleanos que especifica para cada dimensión de cart_com si pertenece a nuevo_com, dejando que la coordenada varíe (verdadero) o no (falso) PARÁMETROS S ALIDA nuevo_com Nuevo comunicador de topología cartesiana con igual o menor número de dimensiones que cart_com B.6. MPI_COMM_CREATE 181 B.6. MPI_Comm_create F UNCIONALIDAD Crea un nuevo comunicador a partir de otro. S INTAXIS int MPI_Comm_create(MPI_Comm antiguo_com, MPI_Group nuevo_grupo, MPI_Comm* nuevo_com) PARÁMETROS E NTRADA antiguo_com Comunicador original a partir del cual generamos el nuevo comunicador nuevo_com Grupo que contiene los procesos que formarán parte del nuevo comunicador. Debe ser un subconjunto del grupo asociado al comunicador original (antiguo_com). PARÁMETROS S ALIDA nuevo_com Nuevo comunicador APÉNDICE B. MANUAL DE REFERENCIA 182 B.7. MPI_Comm_group F UNCIONALIDAD Retorna el grupo asociado a un comunicador determinado. S INTAXIS int MPI_Comm_group(MPI_Comm com, MPI_Group* grupo) PARÁMETROS E NTRADA com Comunicador PARÁMETROS S ALIDA grupo Grupo asociado al comunicador B.8. MPI_COMM_RANK B.8. MPI_Comm_rank F UNCIONALIDAD Retorna el identificador de un proceso dentro de un comunicador. S INTAXIS int MPI_Comm_rank(MPI_Comm com, int* id) PARÁMETROS E NTRADA com Comunicador dentro del cual el proceso tiene asociado el identificador PARÁMETROS S ALIDA id Identificador del proceso dentro del comunicador com 183 APÉNDICE B. MANUAL DE REFERENCIA 184 B.9. MPI_Comm_size F UNCIONALIDAD Determina el número de procesos pertenecientes al grupo asociado a un comunicador. S INTAXIS int MPI_Comm_size(MPI_Comm com, int* numprocs) PARÁMETROS E NTRADA com Comunicador al cual está asociado el grupo cuyo tamaño queremos conocer PARÁMETROS S ALIDA numprocs Número de procesos en el grupo del comunicador (entero) B.10. MPI_COMM_SPLIT 185 B.10. MPI_Comm_split F UNCIONALIDAD Particiona un comunicador en varios subconjuntos de procesos y crea un nuevo comunicador para cada uno de ellos. S INTAXIS int MPI_Comm_split(MPI_Comm antiguo_com, int clave_particion, int clave_id, MPI_Comm* nuevo_com) PARÁMETROS E NTRADA antiguo_com Comunicador original a partir del cual creamos los nuevos comunicadores clave_particion Número que representa el nuevo comunicador en el cual quedará englobado el proceso clave_id Identificador del proceso dentro del nuevo comunicador PARÁMETROS S ALIDA nuevo_com Nuevo comunicador (leer comentarios) C OMENTARIOS La llamada crea un comunicador para cada valor de clave_particion. Los procesos con el mismo valor en clave_particion quedan englobados en el mismo comunicador. Sin embargo los comunicadores creados tendrán el mismo nombre para todos los procesos. Imaginemos que dividimos de manera escalonada un comunicador con 9 procesos en tres comunicadores con 3 procesos, llamados todos nuevo_com. De esta manera, el grupo nuevo_com consistirá en los procesos 0, 1 y 2 para los procesos 0, 1 y 2. En los procesos 3, 4 y 5 el grupo subyacente a nuevo_com será el formado por los procesos 3, 4 y 5; y lo mismo ocurrirá con los procesos 6, 7 y 8. APÉNDICE B. MANUAL DE REFERENCIA 186 B.11. MPI_Finalize F UNCIONALIDAD Finaliza el entorno de ejecución MPI. Después de que el programa haya acabado de utilizar la librería MPI, se debe hacer una llamada a MPI_Finalize. Esta función limpia todos los trabajos no finalizados dejados por MPI (por ejemplo, envíos pendientes que no hayan sido completados, etc.). S INTAXIS int MPI_Finalize() Esta función no tiene parámetros. B.12. MPI_GET_PROCESSOR_NAME 187 B.12. MPI_Get_processor_name F UNCIONALIDAD Retorna el nombre del procesador donde está ubicado el proceso. S INTAXIS int MPI_Get_processor_name(char* nombre, int* longnombre) PARÁMETROS S ALIDA nombre Vector de caracteres cuyo tamaño debe ser al menos igual a la constante MPI_MAX_PROCESSOR_NAME destinado a contener el nombre del procesador longname Longitud (en caracteres) de la cadena obtenida C OMENTARIOS El nombre retornado debe identificar un elemento hardware específico; el formato exacto estará definido por la implementación. APÉNDICE B. MANUAL DE REFERENCIA 188 B.13. MPI_Group_incl F UNCIONALIDAD Crea un nuevo grupo a partir de una lista de procesos pertenecientes a un grupo existente. S INTAXIS int MPI_Group_incl(MPI_Group antiguo_grupo, int tamano_nuevo_grupo, int* ids_antiguo_grupo, MPI_Group* nuevo_grupo) PARÁMETROS E NTRADA antiguo_grupo Grupo original a partir del cual generamos el nuevo grupo tamano_nuevo_grupo Número de procesos que formarán el nuevo grupo ids_antiguo_grupo Identificadores de los procesos que formarán parte del nuevo grupo (vector de enteros) PARÁMETROS S ALIDA nuevo_grupo Nuevo grupo derivado del existente, reordenado según el vector ids_antiguo_grupo. B.14. MPI_INIT 189 B.14. MPI_Init F UNCIONALIDAD Inicializa el entorno de ejecución MPI. Antes de que podamos llamar a cualquier otra función MPI, debemos hacer una llamada a MPI_Init; esta función sólo debe ser llamada una vez. Permite al sistema hacer todas la configuraciones necesarias para que la librería MPI pueda ser usada. S INTAXIS int MPI_Init(int *argc, char ***argv); PARÁMETROS E NTRADA argc Puntero al número de argumentos argv Puntero a los vectores de argumentos APÉNDICE B. MANUAL DE REFERENCIA 190 B.15. MPI_Irecv F UNCIONALIDAD Inicializa la recepción de un mensaje no bloqueante. S INTAXIS int MPI_Recv(void* mensaje, int contador, MPI_Datatype tipo_datos, int origen, int etiqueta, MPI_Comm com, MPI_Request* peticion) PARÁMETROS E NTRADA contador Número de elementos del tipo tipo_datos que componen el mensaje a recibir tipo_datos Tipo de datos MPI de cada elemento del mensaje a recibir (Cuadro 5.1) origen Identificador del proceso origen etiq Etiqueta del mensaje com Comunicador en el cual se produce la comunicación PARÁMETROS S ALIDA mensaje Dirección inicial de memoria del mensaje a recibir peticion Manejador de la petición C OMENTARIOS Esta función recibe mensajes de tipo no bloqueante. Ello quiere decir que la ejecución del programa no se bloquea con la llamada a esta función. B.16. MPI_ISEND 191 B.16. MPI_Isend F UNCIONALIDAD Inicializa el envío de un mensaje no bloqueante a un proceso determinado. S INTAXIS int MPI_Isend(void* mensaje, int contador, MPI_Datatype tipo_datos, int destino, int etiq, MPI_Comm com, MPI_Request* peticion) PARÁMETROS E NTRADA mensaje Dirección inicial de memoria del mensaje a enviar contador Número de elementos del tipo tipo_datos que componen el mensaje a enviar tipo_datos Tipo de datos MPI de cada elemento del mensaje a enviar (Cuadro 5.1) destino Identificador del proceso destino etiq Etiqueta del mensaje com Comunicador en el que se produce la comunicación PARÁMETROS S ALIDA peticion Manejador de la petición C OMENTARIOS Esta función envía mensajes de tipo no bloqueante. Ello quiere decir que la ejecución del programa no se bloquea con la ejecución de la llamada. APÉNDICE B. MANUAL DE REFERENCIA 192 B.17. MPI_Recv F UNCIONALIDAD Recibe un mensaje básico de un proceso. S INTAXIS int MPI_Recv(void* mensaje, int contador, MPI_Datatype tipo_datos, int origen, int etiqueta, MPI_Comm com, MPI_Status* status) PARÁMETROS E NTRADA contador Número de elementos del tipo tipo_datos que componen el mensaje a recibir tipo_datos Tipo de datos MPI de cada elemento del mensaje a recibir (Cuadro 5.1) origen Identificador del proceso origen etiq Etiqueta del mensaje com Comunicador en el cual se produce la comunicación PARÁMETROS S ALIDA mensaje Dirección inicial de memoria del mensaje a recibir status Objeto que representa el estado de la recepción C OMENTARIOS Esta función recibe mensajes de tipo bloqueante. Ello quiere decir que la ejecución del programa se bloquea hasta que el mensaje ha sido recibido. B.18. MPI_REDUCE 193 B.18. MPI_Reduce F UNCIONALIDAD Reduce valores en todos los procesos a un solo valor. Realiza una operación de reducción. S INTAXIS int MPI_Reduce(void* operando, void* resultado, int contador, MPI_Datatype tipo_datos, MPI_Op operacion, int raiz, MPI_Comm com); PARÁMETROS E NTRADA operando Dirección inicial de memoria de los valores a operar contador Número de elementos del tipo tipo_datos que componen cada uno de los operandos y el resultado tipo_datos Tipo de datos MPI de los valores a operar operacion Operación de reducción a realizar (Cuadro 6.1) raiz Identificador del proceso que recibe el valor calculado com Comunicador en el cual se produce la comunicación PARÁMETROS S ALIDA resultado Dirección inicial de memoria del valor calculado a recibir (significante sólo en el proceso raiz) APÉNDICE B. MANUAL DE REFERENCIA 194 B.19. MPI_Send F UNCIONALIDAD Envía un mensaje básico a un proceso determinado. S INTAXIS int MPI_Send(void* mensaje, int contador, MPI_Datatype tipo_datos, int destino, int etiq, MPI_Comm com) PARÁMETROS E NTRADA mensaje Dirección inicial de memoria del mensaje a enviar contador Número de elementos del tipo tipo_datos que componen el mensaje a enviar tipo_datos Tipo de datos MPI de cada elemento del mensaje a enviar (Cuadro 5.1) destino Identificador del proceso destino etiq Etiqueta del mensaje com Comunicador en el que se produce la comunicación C OMENTARIOS Esta función envía mensajes de tipo bloqueante. Ello quiere decir que la ejecución del programa se bloquea hasta que el mensaje ha sido enviado. B.20. MPI_SENDRECV_REPLACE 195 B.20. MPI_Sendrecv_replace F UNCIONALIDAD Envía y recibe un mensaje básico utilizando un sólo buffer. S INTAXIS int MPI_Sendrecv_replace(void* mensaje, int contador, MPI_Datatype tipo_datos, int destino, int etiqdestino, int origen, int etiqorigen, MPI_Comm com, MPI_Status* status) PARÁMETROS E NTRADA mensaje Dirección inicial de memoria del mensaje a enviar y en la cual recibiremos el nuevo mensaje contador Número de elementos del tipo tipo_datos que componen el mensaje a enviar/recibir tipo_datos Tipo de datos MPI de cada elemento del mensaje a enviar/recibir (Cuadro 5.1) destino Identificador del proceso destino etiqdestino Etiqueta del mensaje a enviar origen Identificador del proceso origen etiqorigen Etiqueta del mensaje a recibir com Comunicador en el que se produce la comunicación C OMENTARIOS Esta función envía/recibe mensajes de tipo bloqueante. Ello quiere decir que la ejecución del programa se bloquea hasta que el mensaje ha sido enviado/recibido. APÉNDICE B. MANUAL DE REFERENCIA 196 B.21. MPI_Type_commit F UNCIONALIDAD Acomete un tipo de datos MPI. S INTAXIS int MPI_Type_commit(MPI_Datatype* tipo_datos_MPI) PARÁMETROS E NTRADA tipo_datos_MPI Tipo de datos MPI B.22. MPI_TYPE_STRUCT B.22. MPI_Type_struct F UNCIONALIDAD Crea un tipo de datos MPI para estructuras. S INTAXIS int MPI_Type_struct(int contador, int longitudes_bloque[], MPI_Aint indices[], MPI_Datatype antiguos_tipos_datos, MPI_Datatype* nuevo_tipo) PARÁMETROS E NTRADA contador Número de bloques longitud_bloque Número de elementos de tipo tipo_datos_elem de cada bloque indices Dirección relativa de cada bloque antigus_tipo_datos Tipos de datos de cada bloque PARÁMETROS S ALIDA nuevo_tipo Nuevo tipo de datos MPI 197 APÉNDICE B. MANUAL DE REFERENCIA 198 B.23. MPI_Type_vector F UNCIONALIDAD Crea un tipo de datos MPI para vectores. S INTAXIS int MPI_Type_vector(int contador, int longitud_bloque, int salto, MPI_Datatype tipo_datos_elem, MPI_Datatype* nuevo_tipo) PARÁMETROS E NTRADA contador Número de bloques longitud_bloque Número de elementos de tipo tipo_datos_elem en cada bloque salto Número de elementos de tipo tipo_datos_elem que hay entre los sucesivos elementos de nuevo_tipo. tipo_datos_elem Antiguo tipo de datos PARÁMETROS S ALIDA nuevo_tipo Nuevo tipo de datos MPI B.24. MPI_WAIT B.24. MPI_Wait F UNCIONALIDAD Espera a que se complete un envío o una recepción de un mensaje. S INTAXIS int MPI_Wait(MPI_Request* peticion, MPI_Status* status) PARÁMETROS E NTRADA peticion Manejador de la petición PARÁMETROS S ALIDA status Objeto que representa el estado del envío/recepción del mensaje 199 APÉNDICE B. MANUAL DE REFERENCIA 200 B.25. MPI_Waitall F UNCIONALIDAD Espera a que se completen todas las peticiones especificadas en un vector. S INTAXIS int MPI_Waitall(int contador, MPI_Request* vector_peticiones, MPI_Status* status) PARÁMETROS E NTRADA contador Longitud del vector de peticiones vector_peticiones Vector de peticiones PARÁMETROS S ALIDA status Objeto que representa el estado del envío/recepción del mensaje B.26. MPI_WAITANY B.26. MPI_Waitany F UNCIONALIDAD Espera a que se complete cualquiera de las peticiones especificadas en un vector. S INTAXIS int MPI_Waitany(int contador, MPI_Request* vector_peticiones, int* indice, MPI_Status* status) PARÁMETROS E NTRADA contador Longitud del vector de peticiones vector_peticiones Vector de peticiones PARÁMETROS S ALIDA indice Posición de la petición satisfecha en el vector de peticiones status Objeto que representa el estado del envío/recepción del mensaje 201 APÉNDICE B. MANUAL DE REFERENCIA 202 B.27. MPI_Wtime F UNCIONALIDAD Retorna el tiempo transcurrido para un proceso. S INTAXIS double MPI_Wtime() VALOR DE R ETORNO Tiempo en segundos desde que comenzó la ejecución del proceso. Conclusiones Para llegar a ciertas conclusiones en el estudio realizado lo primero que debemos hacer es analizar los resultados obtenidos en las ejecuciones de los algoritmos paralelos. Sólo de este modo podremos demostrar la utilidad y potencialidad del procesamiento paralelo ejecutado en redes bajo Linux. Sumario de Resultados Comenzaremos el análisis observando algunos gráficos ciertamente determinantes. La figura 1 muestra un gráfico que representa el tiempo de ejecución del algoritmo Cálculo de Áreas mediante Montecarlo utilizando paso de mensajes no bloqueantes. Calculo de Areas No Bloqueante: Tiempo Ejecucion 140 Tiempo Ejecucion 120 Tiempo (seg) 100 80 60 40 20 0 0 2 4 6 8 10 12 14 16 Numero de Procesadores Figura 1: Gráfico Tiempo Ejecución Cálculo de Áreas No Bloqueante En el gráfico observamos una notoria reducción en el tiempo de ejecución de dicho algoritmo conforme incorporamos procesadores al cómputo. Ello es lógico debido a que se trata de 203 CONCLUSIONES 204 un algoritmo cuyo rendimiento depende básicamente de la capacidad de cómputo que tenga la máquina en la cual lo ejecutamos. Además tenemos la gran ventaja de que si dividimos dicho cómputo en varios elementos procesadores, el proceso de converger los resultados parciales de cada uno de los elementos procesadores es bastante sencillo: sólo tenemos que hacer la media de todos ellos. Así pues este algoritmo se muestra muy apropiado para el procesamiento paralelo. El rendimiento obtenido en la versión bloqueante de dicho algoritmo es sólo un poco inferior al obtenido en la versión no bloqueante, aunque su comportamiento es similar. En la misma línea se encuentra el algoritmo Regla del Trapecio. En la figura 2 tenemos el gráfico que muestra el rendimiento de dicho algoritmo. De nuevo observamos una importante reducción del tiempo necesario para su ejecución. Debemos hacer notar que en este tipo de algoritmos el nivel de detalle es un elemento a tener en cuenta. La calidad de las aproximaciones que hacemos en estos algoritmos dependerá del número de muestras aleatorias recogidas en el algoritmo Cálculo de Áreas mediante Montecarlo, y del número de intervalos generados en el algoritmo Regla del Trapecio. Por supuesto el nivel de detalle queda fijado de antemano antes de realizar las pruebas de rendimiento con distintas cantidades de procesadores. Regla del Trapecio: Tiempo Ejecucion 140 Tiempo Ejecucion 120 Tiempo (seg) 100 80 60 40 20 0 0 2 4 6 8 10 12 14 16 Numero de Procesadores Figura 2: Gráfico Tiempo Ejecución Regla del Trapecio Por otro lado el algoritmo Multiplicación de Matrices de Fox tiene una serie de características que lo diferencian del resto. Una de ellas es la exactitud; aquí no tratamos de hacer aproximaciones precisas, a diferencia de los algoritmos anteriormente expuestos. En este algoritmo multiplicamos matrices cuadradas particionándolas en submatrices. Cada proceso en este caso se encargará de calcular una submatriz de la matriz resultado. La principal dificultad en este caso es la sobrecarga en la comunicación. El hecho de tener que transmitir submatrices enteras entre los procesadores dificulta mucho el cálculo fluido. 205 Sin embargo obtenemos resultados muy interesantes en el cómputo de matrices de un orden elevado. Como podemos observar en la figura 3 el crecimiento del tiempo de ejecución es exponencial para todas las cantidades de procesos utilizadas; sin embargo dicho crecimiento es más paulatino cuanto mayor es la cantidad de procesos empleados. Comparacion Tiempo de distintas Cantidades de Procesos 2500 16 9 4 1 Tiempo (seg) 2000 1500 1000 500 0 500 1000 1500 2000 2500 3000 3500 4000 4500 5000 Orden Matriz Figura 3: Comparación Tiempo Distintas Cantidades de Procesos Fox El cuadro 1 muestra los tiempos obtenidos en la ejecución del algoritmo Multiplicación de Matrices de Fox. Orden \ Num.Procs. 700 1400 2100 2800 3500 4200 4900 1 19,0 / 19,1 172,0 / 172,2 2237,2 / 2252,8 - 4 10,3 / 14,9 59,8 / 78,4 190,9 / 232,7 430,1 / 504,3 2074,1 / 2202,5 - 9 10,0 / 15,37 50,1 / 70,9 135,1 / 183,6 274,9 / 362,3 491,3 / 626,8 833,1 / 1032,6 1257,1 / 1527,3 16 19,1 / 23,2 57,3 / 78,9 134,0 / 183,3 261,1 / 344,8 458,5 / 599,9 726,7 / 930,2 1016,4 / 1291,9 Cuadro 1: Evaluación Multiplicación de Matrices de Fox Contiene para cada cantidad de procesadores empleados los tiempos obtenidos en la multiplicación de matrices de distintos órdenes; los dos tiempos separados por el carácter ’/’ representan el tiempo requerido para multiplicar las matrices por un lado, y por otro el tiempo requerido para multiplicarlas y mostrar el resultado. Dichos tiempos se miden en segundos. CONCLUSIONES 206 Para cada orden de matrices utilizado resaltaremos en negrita el tiempo de multiplicación óptimo. Pero en el análisis de los algoritmos no sólo se deben medir sus tiempos de ejecución. Una variable importante a tener en cuesta es el coste. Dicha variable relaciona el número de procesadores empleados con el tiempo de ejecución obtenido; de este modo nos permite saber si es rentable el uso de una cantidad mayor de procesadores en la ejecución de un algoritmo dado. El coste normalmente aumenta en la mayoría de los algoritmos al emplear una cantidad mayor de procesadores en su ejecución. En la figura 4 mostramos el coste de ejecución del algoritmo Cálculo de Áreas mediante Montecarlo. La curva de crecimiento del coste prácticamente se repite en la mayoría de los algoritmos (figura 5). Calculo de Areas No Bloqueante: Coste 175 Coste 170 165 Coste 160 155 150 145 140 135 0 2 4 6 8 10 12 14 16 Numero de Procesadores Figura 4: Gráfico Coste Cálculo de Áreas No Bloqueante Sin embargo en la comparación de costes del algoritmo de Fox (figura 6) observamos que el coste depende tanto del número de procesos empleados como del tamaño de las matrices a multiplicar. De hecho vemos que para matrices de orden mayor a 2000 la utilización de 1 sólo procesador es la más costosa. Con este planteamiento podemos prever que para tamaños de matrices superiores a los empleados en las pruebas la utilización de 16 procesadores será la más rentable. 207 Regla del Trapecio: Coste 138 Coste 136 134 Coste 132 130 128 126 124 122 0 2 4 6 8 10 12 14 16 Numero de Procesadores Figura 5: Gráfico Coste Regla del Trapecio Comparacion Coste de distintas Cantidades de Procesos 16 9 4 1 18000 16000 14000 Coste 12000 10000 8000 6000 4000 2000 0 500 1000 1500 2000 2500 3000 3500 4000 4500 Orden Matriz Figura 6: Comparación Costes Distintas Cantidades de Procesos Fox 5000 208 CONCLUSIONES Corolario Las demostraciones de la sección anterior nos permiten argumentar que el procesamiento paralelo es una técnica que arroja resultados muy satisfactorios para cierto tipo de computaciones. Ciertamente en todos los algoritmos desarrollados obtenemos una mejora del rendimiento al aumentar el número de procesadores utilizados. Sin embargo debemos advertir de que no sólo debemos tener en cuenta el tiempo de ejecución de los algoritmos para evaluarlos. En muchos ambientes la incorporación de una mayor cantidad de procesadores a la ejecución de un algoritmo realmente cuesta dinero. En estos casos la variable coste nos permitirá medir la rentabilidad de la inclusión de nuevos procesadores en la ejecución. Por lo tanto deberíamos emplear más o menos procesadores dependiendo de la necesidad que tengamos de acelerar el procesamiento y de los recursos que podamos utilizar (o adquirir), intentando llegar a un compromiso entre rendimiento y coste que sea óptimo para nuestras necesidades. Debemos insistir en que el paralelismo es un campo realmente extenso en el cual convergen una amplia variedad de técnicas y tecnologías, como vimos en la primera parte del presente documento. Nosotros sólo hemos investigado sobre el paralelismo llevado a cabo mediante la utilización de clusters; no obstante el hacerlo conlleva el obtener una cierta perspectiva del amplio abanico de posibilidades que existen por explorar. En este panorama cabe destacar la importancia de la existencia de herramientas de desarrollo de software paralelo que hacen del paralelismo una técnica al alcance de todos. Los estándares como MPI no sólo ofrecen este tipo de facilidades, si no que además añaden portabilidad a los algoritmos implementados mediante su utilización. Con todo lo expuesto anteriormente debemos darnos cuenta de que sólo necesitamos un grupo de ordenadores convencionales conectados en red bajo Linux para poder ejecutar aplicaciones paralelas de manera eficiente. De este modo podemos disponer por muy poco dinero (o incluso nada) de todo un computador paralelo con una potencia relativamente alta. Así pues... ¿por qué no aprovecharlo?. Direcciones URL 1. CHIMP Download <ftp://ftp.epcc.ed.ac.uk/pub/chimp/release/> 2. FireWire <http://www.apple.com/firewire/> 3. FSMLabs Inc. creadores de RTLinux <http://www.rtlinux.org/> 4. Intel Compilers <http://www.intel.com/software/products/compilers/> 5. Internet Parallel Computing Archive <http://wotug.ukc.ac.uk/parallel/> 6. LAM/MPI Parallel Computing <http://www.lam-mpi.org/> 7. MMX Emulator <http://www-sop.inria.fr/prisme/personnel/pion/progs/mmx-emu/> 8. MPI Forum <http://www.mpi-forum.org/> 9. MPICH - A Portable Implementation of MPI <http://www.mcs.anl.gov/mpi/mpich/> 10. MPICH Downloads <http://www.mcs.anl.gov/mpi/mpich/download.html> 11. MPICH Parches <http://www.mcs.anl.gov/mpi/mpich/buglist-tbl.html> 12. OpenSSH - Versión libre de SSH <http://www.openssh.com/> 13. Packet Engines Gigabit Ethernet with Linux <http://www.scyld.com/network/yellowfin.html> 209 DIRECCIONES URL 210 14. PVM - Parallel Virtual Machine <http://www.epm.ornl.gov/pvm/pvm_home.html> 15. SWAR Homepage at Purdue University <http://shay.ecn.purdue.edu/~swar/> 16. The Aggregate <http://aggregate.org/> 17. The Beowulf Underground <http://www.beowulf-underground.org/> 18. The Berkeley NOW Project <http://now.cs.berkeley.edu/> 19. The Globus Project <http://www.globus.org/> 20. USB - Universal Serial Bus <http://www.usb.org/> 21. WineHQ - Windows Emulator <http://www.winehq.com/>