II. Paradigmas de Programación Paralela

Anuncio
Paralelización
Paralelización en la Supercomputadora
“Cray Origin 2000”
Oscar Rafael García Regis
Enrique Cruz Martínez
2003-III
Paralelización
Oscar Rafael García Regis
Laboratorio de Dinámica No Lineal
Facultad de Ciencias, UNAM
Enrique Cruz Martínez
Dirección General de Servicios de Cómputo Académico. (DGSCA)
UNAM. Cómputo Aplicado
Paralelización
Paralelización en la Supercomputadora “Cray Origin 2000”
Resumen
Se describe el cómputo paralelo a través de conceptos básicos, arquitecturas de máquinas paralelas
y técnicas de programación paralela que se implementa en máquinas de tipo Memoria Compartida
Distribuida como la Cray Origin 2000.
Desde un principio se debe justificar el paralelizar un código y posteriormente elegir la mejor opción
para paralelizarlo. Para ello se debe reconocer y dividir el código en pedazos “independientes”, y
ejecutarse en múltiples procesadores. Este procedimiento se ejemplifica usando varios paradigmas
de programación paralela.
Se analiza un caso de estudio en varios modelos de programación, mostrando las ventajas y
desventajas de las diversas técnicas de programación paralela.
Paralelización
Indice
Introducción
Razón de ser de la paralelización.
Sistemas de Alto Rendimiento.
3
3
3
P a r t e I: Conceptos
I. Fundamentos de paralelización.
I.1 ¿Qué es paralelismo y qué es cómputo paralelo?
I.2 ¿Qué es computación Paralela y que aspectos involucra?
I.3 ¿Qué es una computadora paralela?
I.4 ¿Cuándo paralelizar?
I.5 ¿Qué se necesita para paralelizar?
5
5
5
5
5
7
II. Etapas en la creación de programas paralelos
II.1 Particionamiento
II.1.1. Descomposición
II.1.2. Aglomeración (Asignación)
II.2 Orquestación
II.2.1. Sincronización
II.2.2. Comunicación
II.3 Mapeo
7
7
8
8
10
10
11
12
III. Paradigmas de Programación Paralela
III.1 Basado en Arquitecturas de computadoras
III.1.1 SISD
III.1.2 SIMD
III.1.3 MIMD
III.1.3.1 Sistemas de Memoria Compartida
III.1.3.2 Sistemas de Memoria Distribuida
III.1.3.3 Sistemas de Memoria Compartida Distribuida
III.1.4 MISD
III.2 Basado en la Naturaleza del Algoritmo
III.2.1 Teoría de la Paralelización Homogénea (Homoparalelismo)
III.2.2 Teoría de la Paralelización Heterogénea (Hetereoparalelismo)
14
14
14
15
15
16
17
17
18
18
19
19
IV. Niveles de paralelismo
IV.1 Programación Paralela
IV.2 Granularidad
IV.2.1 Teoría de la Paralelización de Grano Fino
IV.2.2 Teoría de la Paralelización de Grano Grueso
20
20
20
20
20
P a r t e II: Paradigmas de Programación Paralela
I. ¿Que hacer ante un problema a paralelizar?
22
II. Paradigmas de Programación Paralela
II.1 Código C, Fortran y C++ usando procesos UNIX. (Nivel proceso)
II.2 Código fuente C con hebras POSIX. (Nivel hilo)
22
22
24
1
Paralelización
II.3 Código fuente en C o Fortran usando envío de Mensajes (Nivel proceso)
25
II.4 Código fuente en FORTRAN, C o C++ usando directivas o pragmas
respectivamente. (Nivel Sentencia)
26
II.4.1 OpenMP
26
III. Compiladores Paralelizadores
III.1 Dependencia de datos
III.2 Bases Teóricas de la Paralelización Automática y Técnicas de Paralelización de
Programas Secuenciales
III.2.1 Casos de Estudio de No Paralelización
III.2.2 Analizadores PCA y PFA
III.3 Paralelización Manual
III.3.1 Sintaxis de las directivas de paralelización
III.3.2 Creando bloques independientes
III.3.3 Ejemplo de paralelización automática
29
29
33
36
37
38
39
IV. Análisis de Rendimiento
IV.1 Modelación de Rendimiento
IV.1.1 Ley de Amdhal
IV.1.2 Extrapolación a partir de observaciones
IV.1.3 Análisis Asintótico
IV.1.4 Basado en modelos idealizados
IV.2 Tiempo de Ejecución
IV.2.1 Tiempo de Cómputo
IV.2.2 Tiempo de Comunicación
IV.2.3 Tiempo de Espera
IV.3 Modelo de Comunicación
IV.4 Eficiencia y Aceleración
IV.4.1 Eficiencia y Aceleración Absolutas
IV.5 Análisis de Escalabilidad
IV.5.1 Escalabilidad de un algoritmo
IV.5.2 Escalabilidad con problema variable
IV.6 Perfiles de Ejecución
IV.7 Estudios Experimentales
IV.7.1 Diseño Experimental
IV.7.2 Obteniendo y validando datos experimentales
IV.7.3 Ajuste de datos
43
43
43
44
44
45
45
45
45
46
46
46
47
47
47
48
49
49
49
50
50
V. Estudio Comparativo
IV.1 Programa Secuencial
IV.2 Empleando Procesos Unix
IV.3 Empleando Hilos Posix
IV.4 Comunicación con MPI
IV.5 Comunicación con PVM
IV.6 Comunicación con SHMEM
IV.7 Empleando directivas de PCA
IV.8 Comparación de los modelos anteriores
51
51
52
56
57
60
65
66
69
Glosario
Referencias
72
75
2
28
28
Paralelización
I. Introducción
Intuitivamente paralelismo esta relacionado con la simultaneidad, por ejemplo: el cuerpo
humano realiza actividades paralelas a cada instante, el medio ambiente que nos rodea esta
basado en este tipo de actividad. Cualquier partícula esta sometida bajo la influencia de todo el
universo, y esa interacción es simultánea, cada planeta y estrella están movimiento de manera
permanente y realizando complejas trayectorias debido a la influencia de los demás objetos
estelares.
Dentro del cerebro humano se llevan a cabo simultáneamente una cantidad increíblemente
grande de fenómenos eléctricos, las neuronas son las responsables de estos eventos, muchas
de ellas recogen señales eléctricas de sus vecinas, muchas otras disparan potenciales de
acción, todo esto ocurre de manera simultánea. En fin, cualquier escenario de nuestro universo
manifiesta un perfecto paradigma paralelo.
Razón de ser de la paralelización
Desde el modelo propuesto por Von Neuman, la computación secuencial ha sido el modo
habitual de computación. Sin embargo, existe una demanda permanente de mayor rendimiento
computacional, por ende se recurre al cómputo paralelo, pues la solución en máquinas
secuénciales no sería óptimo o posible en tiempos razonables. El uso del cómputo paralelo
comprende una amplia gama de aplicaciones, tales como: la predicción del clima, modelado de
la biosfera, exploración petrolera, procesamiento de imágenes, fusión nuclear, modelado de
océanos, sincronización de osciladores entre otras.
Este modelo de programación ofrece:
•
•
•
•
•
•
•
•
Alternativas a los relojes rápidos de mejorar el rendimiento.
Aplicación a todos los niveles del diseño de un sistema de cómputo.
Mayor importancia en aplicaciones que demandan alto rendimiento.
Una insaciable necesidad de computadoras rápidas.
Tendencias en la Tecnología, en la arquitectura y en la economía.
Apoyo para el Cómputo Científico: Física, Química, Biología, Oceanografía,
Astronomía, etc., y para el aceleramiento del cómputo de propósito general:
video, CAD, Bases de Datos, Procesamiento de Transacciones, etc.
Una forma natural de mejorar el rendimiento.
Explotación a muchos niveles.
- A nivel de instrucciones.
- Servidores multiprocesadores.
- Computadoras paralelas masivas.
Sistemas de Alto Rendimiento
Procesadores superescalares (1992)
Relojes de alta velocidad.
Paralelismo a nivel de instrucciones.
Varios niveles de memoria cache.
Procesadores para Vectores (1970´s)
Supercomputadoras (1980)
3
Paralelización
Arreglos de procesadores VLSI (1985)
Custom Computing (1992)
Computadoras Paralelas (1986)
La evolución de los sistemas de alto rendimiento ha sido consecuencia de:
El rendimiento de los procesadores se incrementa entre 50% y 100% cada año.
La densidad de transistores en Circuitos Integrados se duplica cada 3 años.
La capacidad de la DRAM se cuadruplica cada 3 años.
4
Paralelización
P a r t e I: Conceptos
I. Fundamentos de paralelización
I.1 ¿Qué es Paralelismo y qué es Cómputo Paralelo?
El paralelismo es la realización de varias actividades al mismo tiempo que tienen una
interrelación.
El cómputo paralelo es la ejecución de más de un cómputo (cálculo) al mismo tiempo usando
más de un procesador.
Se trata de reducir al mínimo el tiempo total de cómputo, distribuyendo la carga de trabajo entre
los procesadores disponibles. Obtener un alto rendimiento o mayor velocidad al ejecutar un
programa es una de las razones principales para utilizar el paralelismo en el diseño de
hardware o software.
I.2 ¿Qué es Computación Paralela y que aspectos involucra?
Es el proceso de información que enfatiza la manipulación concurrente de elementos de datos
pertenecientes a uno o más procesos resolviendo un problema común (Quinn 1994).
Entre los aspectos más importantes relacionados con la computación paralela están:
Diseño de Computadoras Paralelas.
Diseño de Algoritmos Eficientes.
Métodos para Evaluar Algoritmos Paralelos.
Programación Automática de Computadoras Paralelas.
Lenguajes de Programación Paralela.
Herramientas para Programación Paralela.
Portabilidad de Programas Paralelos.
I.3 ¿Qué es una computadora paralela?
Una computadora paralela es un conjunto de procesadores capaces de trabajar
cooperativamente para resolver un problema computacional. (Quinn 1994)
Esta definición es suficientemente amplia para incluir supercomputadoras paralelas que tienen
cientos o miles de procesadores, redes de estaciones de trabajo, etc. Las computadoras
paralelas son interesantes porque ofrecen recursos computacionales potenciales.
Otra definición: (Culler 1994) “Una colección de elementos de procesamiento que se comunican
y cooperan entre sí para resolver problemas grandes de manera rápida”.
I.4 ¿Cuando Paralelizar?
La respuesta a esta pregunta no es sencilla, dado que debe analizarse si se cuenta con el
hardware y software necesario, para determinar si vale la pena paralelizar el programa o
viceversa.
5
Paralelización
En términos del hardware, paralelizar código involucra conocer la arquitectura de la
supercomputadora o "cluster" donde se pretende paralelizar, conocer el número de
procesadores con los que cuenta, la cantidad de memoria, espacio en disco, los niveles de
memoria disponible, el medio de interconexión, etc.
En términos de software, se debe conocer qué sistema operativo esta manejando, si los
compiladores instalados permiten realizar aplicaciones con paralelismo, si se cuenta con
herramientas como PVM o MPI (en sistemas distribuidos), PCA, PFA u OpenMP (en sistemas
de memoria compartida), etc.
Para decidir si es conveniente paralelizar un código, podemos ubicar el problema considerando
los siguientes puntos:
1. Necesidad de respuesta inmediata de resultados
Al ejecutar varias veces un programa en una máquina secuencial con diferentes datos
de entrada, donde cada tiempo de ejecución es considerable (horas o días), es
inapropiado o costoso esperar tanto tiempo para volver a realizar otra ejecución,
afectando notablemente el desempeño de su máquina. Esto, sin considerar las
modificaciones al código o fallas en la ejecución del modelo. La paralelización y la
ejecución del programa en una máquina paralela permiten realizar más análisis en
menos tiempo.
2. Problema de Gran Reto
Cuando un problema se tiene que resolver mediante algoritmos de cálculo científico
intensivo que demandan grandes recursos de cómputo (CPU, memoria, disco), la frase
"divide y vencerás" tiene un significado más amplio. Las tareas se dividen entre varios
procesadores, se ejecutan en paralelo y se obtiene una mejora en la relación costo desempeño. Actualmente las arquitecturas de cómputo incorporan paralelismo en los
más altos niveles de sus sistemas, satisfaciendo las exigencias de los problemas de
gran reto.
3. Elegancia de programación
Es decisión del programador escribir un algoritmo paralelo aunque no se justifique ni por
tiempo ni por uso intensivo de otros recursos.
Para paralelizar un programa es importante identificar en el código:
Tareas independientes, por ejemplo, que existan ciclos for o do independientes, y
rutinas o módulos independientes. De tal forma que no existan dependencias de datos
que dificulten la paralelización.
Zonas donde se efectúa la mayor carga de trabajo y que por lo tanto consumen la mayor
parte de tiempo de ejecución. Para detectar dichas zonas existen herramientas que
permiten obtener una perspectiva o perfil del programa. Por ejemplo en Origin 2000
existen una amplia variedad de herramientas para llevar a cabo estas tareas, tanto
herramientas que ofrece el mismo sistema de Origin 2000: SpeedShop, Perfex, etc como
herramientas de dominio público que se pueden descargar e instalar: JumpShot,
UpShot, Vampir, Paradyn, PAPI, etc
6
Paralelización
I.5 ¿Qué se necesita para paralelizar?
Es necesario disponer de un ambiente paralelo, constituido de:
•
•
•
Hardware de Multiprocesamiento.
Soporte del sistema operativo para paralelizar.
Herramientas de desarrollo de software paralelizable.
II. Etapas en la creación de programas paralelos
II.1 Particionamiento
Esta etapa consiste en resaltar las posibilidades de ejecución paralela.
Se concentra en la definición de un gran número de pequeñas tareas a fin de producir lo
que se conoce como la descomposición de un problema en grano fino.
Esta etapa se subdivide en 2 partes:
Descomposición, y
Aglomeración (Asignación).
7
Paralelización
Un buen particionamiento divide tanto los cálculos asociados con el problema como los datos
sobre los cuales opera.
II.1.1. Descomposición
Hay alternativas en esta sub-etapa:
a) Descomposición del dominio: Se concentra en el particionamiento de los datos.
Primero, se busca descomponer los datos asociados al problema.
Se trata de dividir los datos en piezas del mismo tamaño.
Después, se dividen los cálculos que serán realizados.
Asociando cada operación con los datos sobre los cuales opera.
Los datos a ser descompuestos pueden ser:
La entrada del programa
La salida computada por el programa.
Los valores intermedios mantenidos por el programa.
Regla
Concentrarse en la estructura de datos más grandes o la que se accede con mayor frecuencia.
b) Descomposición funcional: Se concentra en la descomposición de funciones o tareas.
Primero, se concentra en los cálculos que serán realizados.
Se divide el procesamiento en tareas disjuntas.
Después, se procede a examinar los datos que serán utilizados por esas tareas.
Si los datos son disjuntos, el particionamiento es completo.
Si los datos no son disjuntos, se requiere replicar los datos o comunicarlos entre tareas
diferentes.
Objetivos de la Descomposición
Definir al menos un orden de magnitud de más tareas que procesadores en la computadora
paralela., Pues en caso contrario, se tendrá poca flexibilidad en las etapas siguientes.
Debe evitar cálculos y almacenamientos redundantes. En caso contrario, difícilmente se logrará
un algoritmo escalable.
Debe generar tareas de tamaño comparable. En caso contrario, puede ser difícil asignar a cada
procesador cantidades de trabajo similares.
Debe generar tareas escalables con el tamaño del problema.
Un incremento en el tamaño del problema debe incrementar el número de tareas en
lugar del tamaño de las tareas individuales.
Es recomendable considerar varias alternativas de descomposición.
En las etapas siguientes se puede obtener mayor flexibilidad teniendo varias
alternativas.
II.1.2 Aglomeración (Asignación)
Para reducir comunicaciones y explorar la vecindad de los cálculos es conveniente considerar si
es útil aglomerar o combinar las tareas identificadas en la fase de descomposición.
Puede ser útil también replicar datos para evitar comunicaciones.
8
Paralelización
El número de procesos producidos en la fase de aglomeración, aunque reducido, aún
puede ser mayor que el número de procesadores.
Por tal razón, el diseño de un programa paralelo en la fase de aglomeración aún
permanece abstracto.
En la fase de aglomeración se persiguen los siguientes objetivos:
Balancear la carga de trabajo.
Limite de la aceleración
Speedup ≤
TrabajoSecuencial
MaxTrabajoenCualquier Pr ocesador
El trabajo incluye acceso a datos y otros costos.
El trabajo no solo debe ser repartido equitativamente sino también debe de
realizarse al mismo tiempo.
Actividades para lograr un buen balance de carga
Identificar suficiente paralelismo.
Decidir como manejar la aglomeración.
Determinar la granularidad del paralelismo.
Reducir las tareas seriales y los costos de sincronización.
Incrementar la granularidad del cómputo y de la comunicación.
Retener flexibilidad con respecto a las decisiones de escalabilidad y mapeo.
Reducir los costos de desarrollo.
¿Cómo manejar la aglomeración?
Puede manejarse de manera:
• Estática
• Dinámica
a) Aglomeración Estática
La aglomeración se basa en la entrada y en el problema, no cambia.
Los requerimientos de cómputo deben ser predecibles.
Es preferible a otros enfoques.
No aplicable en ambientes heterogéneos.
b) Aglomeración Dinámica
Para casos en donde la distribución de trabajo o el medio ambiente es impredecible.
Se adapta en tiempo de ejecución para balancear la carga de trabajo.
Puede incrementar las necesidades de comunicación y reducir la localidad.
Puede incurrir en un trabajo adicional para manejar a las tareas.
Basada en perfiles (semi-estática)
Se obtiene el perfil de una distribución de un trabajo en tiempo de ejecución.
Se hace particionamiento de manera dinámica.
Tareas Dinámicas.
Permite abordar programas y medio ambientes impredecibles.
Alta interacción entre cómputo, comunicaciones y sistema de memoria.
Aplicable a multiprogramación y heterogeneidad.
Se utiliza por los sistemas operativos.
Modelo de programación
“pool” de tareas
Granja de procesos
9
Paralelización
Maestro-Esclavo
Colas de Entrada
Centralizada vs Distribuida
II.2 Orquestación
En la fase de orquestación se establecen los patrones de:
•
•
Comunicación y
Sincronización del programa paralelo.
La arquitectura, el modelo de programación y el lenguaje de programación juegan un papel
importante.
Se requieren mecanismos para:
Nombrar (identificar) datos.
Intercambiar datos con otros procesadores (comunicación).
Sincronización entre todos los procesadores.
Decisiones acerca de la organización de estructuras de datos, la calendarización de
tareas, mezclar o dividir mensajes, traslapar cómputo con comunicación.
Objetivos de la Orquestación
Reducción de costos de comunicación y sincronización.
Promover la localidad de referencias a datos.
Facilitar la calendarización de tareas para evitar tiempos latentes.
Reducción del trabajo adicional para controlar el paralelismo.
II.2.1 Sincronización
Una buena sincronización de actividades paralelas se logra mediante las actividades de
calendarización y mapeo.
•
•
Calendarización: Determinar el momento en que se realiza cada una de las
actividades.
Mapeo: Determinar quien ejecutará el trabajo a realizar.
La calendarización y el mapeo en la fase de orquestación trabajan a nivel de procesos.
La calendarización y el mapeo de procesadores se realizan en la fase final del diseño de
un programa paralelo.
Sin embargo, durante la orquestación se deben determinar actividades seriales
provocadas en la fase de particionamiento.
Dos aspectos fundamentales se deben de considerar:
•
Reducir el uso de sincronización conservadora.
Ej. Punto a punto en lugar de barreras, cuidar la granularidad de comunicaciones punto
a punto.
Sincronización en grano fino es más difícil de programar y requiere más operaciones.
•
Exclusión Mutua
10
Paralelización
Usar candados diferentes para datos diferentes.
Candados por tarea en una cola de tareas, no por la cola total.
Grano fino implica menor contención y serialización, más espacio y menos
reusabilidad.
Reducir el tamaño de las secciones críticas de código.
No haber pruebas por condiciones dentro de una sección crítica.
II.2.2 Comunicación
La comunicación se establece en dos fases:
a) Canales de comunicación entre emisores y receptores
Lógicos o físicos
b) Estructura de los mensajes entre emisores y receptores
Patrones de comunicación.
Dependiendo de la tecnología de programación, puede no ser necesario crear lógica y
explícitamente esos canales de comunicación.
Definir un canal de comunicación involucra un costo intelectual.
Transmitir un mensaje por un canal de comunicación implica un costo físico.
¿Cómo se establecen los mecanismos de comunicación de acuerdo a la estrategia de
particionamiento?
Descomposición de dominio
Difícil de determinar los requerimientos de comunicación.
Para hacer un cálculo se pueden requerir datos de varias tareas.
Análisis de dependencia de datos.
Descomposición funcional
Los requerimientos de comunicación corresponden al flujo de datos entre tareas.
Clasificación de la Comunicación
Local vs. Global:
En comunicación local cada tarea se comunica con un pequeño conjunto de tareas
vecinas.
En comunicación global se requiere que cada tarea se comunique con muchas tareas.
Estructurada vs. No Estructurada
En comunicación estructurada se establecen patrones regulares de comunicación.
En comunicación no estructurada no existen patrones definidos.
Estática vs. Dinámica
En comunicación estática los participantes no cambian con el tiempo.
En comunicación dinámica los patrones de comunicación se determinan en tiempo de
ejecución.
Síncrona vs. Asíncrona
Los participantes de una comunicación síncrona lo hacen de manera coordinada.
11
Paralelización
En comunicación asíncrona es posible solicitar datos sin el acuerdo de un emisor o
productor de datos.
Objetivos de la Comunicación
Se debe balancear las comunicaciones entre todas las tareas.
Un desbalance en las comunicaciones conduce a un programa no escalable.
Distribuir estructuras de datos frecuentemente accedidas entre las tareas.
Siempre que se pueda, se debe tratar de tener comunicaciones locales.
Las operaciones de comunicación deben promover el paralelismo, esto es, se deben ejecutar
en forma paralela.
Utilice divide y vencerás para descubrir concurrencia.
Las operaciones de comunicación deben permitir que las tareas del cómputo se realicen en
forma concurrente.
Si esto no sucede, el algoritmo seguramente será ineficiente y no escalable.
Estructurando la Comunicación
Dada una cantidad de comunicaciones (inherente o artificial) el objetivo es reducir su costo.
El objetivo es reducir los términos del tiempo de espera e incrementar el traslape entre cómputo
y comunicación.
II.3. Mapeo
En la fase de mapeo se hace una asignación de procesos a procesadores físicos.
El objetivo es minimizar el tiempo de ejecución de un programa paralelo.
Las estrategias son:
Colocar procesos que son capaces de ejecutarse concurrentemente en procesadores
diferentes.
Colocar procesos que se comunican frecuentemente en el mismo procesador para
fomentar la vecindad (espacial y temporal).
El problema del mapeo no aparece en computadoras con un solo procesador o en
computadoras con memoria compartida.
En multiprocesadores, mecanismos de hardware o del sistema operativo hace una
calendarización de procesos automática.
No existen, hasta el momento, mecanismos de mapeo de propósito general.
Mapeo es un problema difícil (NP-completo) que tiene que ser abordado de manera explícita
cuando se diseñan algoritmos y programas paralelos.
Se tienen distintos enfoques:
Mapeo Estático
Descomposición del dominio, número fijo de procesos del mismo tamaño, comunicación
estructurada local y global.
Modelos de programación para Mapeo
Las interacciones entre procesos puede expresarse por tres modelos diferentes:
12
Paralelización
13
Paralelización
III. Paradigmas de Programación Paralela
III.1 Basado en Arquitecturas de computadoras
En 1966, Michael Flynn propuso un mecanismo clásico de clasificación de las computadoras, y
aunque no cubre todas las posibles arquitecturas, sí proporciona una importante penetración en
varias arquitecturas de computadoras modernas. Esta clasificación esta basada en el número
de instrucciones y secuencia de datos que la computadora utiliza para procesar la información.
Puede haber secuencias de instrucciones sencillas o múltiples y secuencias de datos sencillas
o múltiples. Dando lugar a 4 tipos de computadoras, de las cuales sólo dos son aplicables a las
computadoras paralelas.
Clasificación de Flynn:
III.1.1 SISD (Single Instruction Single Data)
Modelo tradicional de computación secuencial, donde una unidad de procesamiento recibe sólo
una secuencia de instrucciones que opera en una secuencia de datos.
Modelo SISD (Single Instruction Single Data)
Ejemplo: Para realizar la suma de N números a1, a2, ..., aN, el procesador necesita acceder a
memoria N veces consecutivas (para recibir un número). Ejecutando en secuencia N-1
adiciones. Es decir, los algoritmos para las computadoras SISD no contienen ningún
paralelismo, éstas están constituidas de un procesador.
14
Paralelización
III.1.2 SIMD (Single Instruction Multiple Data)
A diferencia del SISD, aquí existen múltiples procesadores que sincronizadamente ejecutan la
misma secuencia de instrucciones, pero en diferentes datos. La memoria es distribuida.
Modelo SIMD(Single Instruction Multiple Data)
Hay N secuencias de datos, una por procesador: Cada procesador ejecuta la misma instrucción
con diferentes datos. Los procesadores operan sincronizadamente y un reloj global se utiliza
para asegurar esta operación.
A este tipo de máquinas corresponden: ICL DAP (Distributed Array Processor) y computadoras
vectoriales como CRAY 1 & 2 y CIBER 205.
Ejemplo: Sumar las matrices A, B, con A y B de orden 2, si la suma se realiza con 4
procesadores, entonces se pueden ejecutar simultáneamente las siguientes instrucciones:
A11 + B11 = C11
A21 + B21 = C21
A12 + B12 = C12
A22 + B22 = C22
De esta forma la suma de dos matrices se realiza en un paso, en comparación con cuatro pasos
en una máquina secuencial.
III.1.3 MIMD (Multiple Instruction Multiple Data)
Esta computadora también es paralela como la SIMD, pero MIMD es asíncrono. No tiene un
reloj central. Cada procesador en un sistema MIMD puede ejecutar su propia secuencia de
instrucciones y tener sus propios datos. Esta característica es la más general y poderosa de
esta clasificación.
Modelo MIMD(Single Instruction Multiple Data)
15
Paralelización
Se tienen N procesadores, N secuencias de instrucciones y N secuencias de datos. Cada
procesador ejecuta su propia secuencia de instrucciones con diferentes datos. Los
procesadores operan asincrónicamente; pueden estar haciendo diferentes cosas en diferentes
datos al mismo tiempo. Los sistemas MIMD se clasifican en:
Sistemas de Memoria Compartida
Sistemas de Memoria Distribuida
Sistemas de Memoria Compartida Distribuida
III.1.3.1. Sistemas de Memoria Compartida
En este sistema cada procesador tiene acceso a toda la memoria, es decir, hay un espacio de
direccionamiento compartido. Los tiempos de acceso a memoria son uniformes, pues todos los
procesadores se encuentran igualmente comunicados con la memoria principal, sus lecturas y
escrituras tienen las mismas latencias, y el acceso a memoria es por medio de un canal común.
En esta configuración, debe asegurarse que los procesadores no tengan acceso simultáneo a
regiones de memoria que provoquen algún error. Actualmente los sistemas operativos de estos
equipos controlan todo esto mediante mecanismos tipo semáforos.
Ventaja:
Más fácil programar que en sistemas de memoria distribuida.
Desventajas:
El acceso simultáneo a memoria es un problema.
En PCs y estaciones de trabajo:
Todos los CPUs comparten el camino a memoria.
Un CPU que acceda la memoria, bloquea el acceso de todos los otros CPUs
En computadoras vectoriales como Cray, etc.
Todos los CPUs tienen un camino libre a la memoria.
No hay interferencia entre CPUs.
La razón principal por el alto precio de Cray es la memoria.
Sistemas de Memoria Compartida
Las computadoras MIMD con memoria compartida son conocidas como sistemas de
multiprocesamiento simétrico (SMP), donde múltiples procesadores comparten un mismo
sistema operativo y memoria. Otro término con que se le conoce es máquinas firmemente
acopladas o de multiprocesadores. Ejemplos son: SGI Power Challenge, SGI/Cray C90,
SGI/Onyx, ENCORE, MULTIMAX, SEQUENT y BALANCE, entre otras.
16
Paralelización
III.1.3.2. Sistemas de Memoria Distribuida
Estos sistemas tienen su propia memoria local. Se comparte información sólo a través de
mensajes, es decir, si un procesador requiere datos contenidos en la memoria de otro, deberá
enviar un mensaje solicitándolos. Esta comunicación se le conoce como Paso de Mensajes.
Ventajas:
Los equipos son más económicos que los de memoria compartida.
Escalabilidad: Las computadoras con sistemas de memoria distribuida son fáciles de
escalar, pues cuando la demanda de los recursos crece, se puede agregar más memoria
y procesadores.
Desventajas:
El acceso remoto a memoria es lento conforme crece la máquina.
La programación puede ser complicada porque requiere de programación paralela
explícita.
Sistemas de Memoria Distribuida
Las computadoras MIMD de memoria distribuida son conocidas como sistemas de
procesamiento masivamente paralelas (MPP), donde múltiples procesadores trabajan en
diferentes partes de un programa, usando su propio sistema operativo y memoria. También se
les conoce como multicomputadoras, máquinas libremente juntas o cluster. Algunos
ejemplos de este tipo de máquinas son IBM SP2 y SGI/Cray T3D/T3E.
III.1.3.3 Sistemas de Memoria Compartida - Distribuida
Es un conjunto de procesadores que tienen acceso a una memoria compartida pero sin un
canal compartido. Esto es, físicamente cada procesador posee su memoria local y se
interconecta con otros procesadores por medio de un dispositivo de alta velocidad, y todos ven
las memorias de cada uno como un espacio de direcciones globales.
El acceso a memoria se realiza bajo el esquema de Acceso a Memoria No Uniforme (NUMA), la
cual toma menos tiempo en acceder a la memoria local de un procesador que acceder a
memoria remota de otro procesador.
Ventajas:
Escalable como en los sistemas de memoria distribuida.
17
Paralelización
Fácil de programar como en los sistemas de memoria compartida.
No existe el cuello de botella, como en máquinas de sólo memoria compartida.
Sistemas de Memoria Compartida Distribuida
Algunos ejemplos de este tipo de sistemas son HP/Convex SPP-2000 y SGI Origin2000.
III.1.4 MISD (Multiple Instruction Single Data)
En este modelo, secuencias de instrucciones pasan a través de múltiples procesadores.
Diferentes operaciones son realizadas en diversos procesadores. N procesadores, cada uno
con su propia unidad de control comparten una memoria común.
MISD (Multiple instruction Single Data)
Existen N secuencias de instrucciones (algoritmos/programas) y una secuencia de datos. Cada
procesador ejecuta diferentes secuencias de instrucción, al mismo tiempo, con el mismo dato.
Las máquinas MISD son útiles donde la misma entrada esta sujeta a diferentes operaciones. No
existe ningún equipo comercial basado en este tipo de arquitectura.
III.2 Basado en la Naturaleza del Algoritmo
La naturaleza del algoritmo determina el modelo apropiado para su paralelización.
Tenemos dos tipos de algoritmos:
Homogéneos: Aplican el mismo código a múltiples elementos de datos.
Heterogéneos: Aplican múltiples códigos a múltiples elementos de datos.
18
Paralelización
Estas dos alternativas describen diferentes formas de generar pedazos que pueden ser
ejecutados en paralelo.
La independencia entre los pedazos asegura que un pedazo no modificará una variable que
otro (pedazo) pueda estar leyendo o modificando simultáneamente.
III.2.1 Teoría de la Paralelización Homogénea (Homoparalelismo)
Se lleva a cabo este tipo de paralelismo cuando el trabajo realizado por el algoritmo puede ser
descompuesto en tareas idénticas (homogéneas), cada una es una porción del trabajo total. Por
ejemplo, los lazos o ciclos compuestos de un número finito de iteraciones. En este caso todos
los pedazos se ejecutaran y terminaran al mismo tiempo y solo tendrán dos puntos de
sincronización, al inicio y al final.
III.2.2 Teoría de la Paralelización Heterogénea (Hetereoparalelismo)
Es posible cuando el trabajo realizado por el algoritmo puede ser distribuido en tareas
diferentes, siendo cada tarea una porción del algoritmo total. Por ejemplo, cualquier algoritmo
con múltiples componentes independientes. En este caso solo tendrán un punto de
sincronización al inicio de las tareas y posteriormente terminaran a diferentes tiempos
dependiendo del tamaño de los pedazos.
19
Paralelización
IV. Niveles de paralelismo
Normalmente la computadora se concibe como una máquina secuencial, donde el CPU lee la
instrucción de la memoria y la ejecuta una por una. De la misma manera, al especificar
algoritmos se concibe una secuencia de instrucciones a ser ejecutadas una a una.
Aunque internamente, las computadoras secuénciales presentan un paralelismo en diversas
funciones (paralelismo a nivel de instrucciones), por mencionar algunas: el traslape de algunas
operaciones, mientras un proceso esta escribiendo a disco, otro proceso esta ejecutándose en
el CPU; a nivel de micro operación, señales de control se generan y viajan al mismo tiempo a
través de canales paralelos, como el caso de la comunicación a una impresora conectada al
puerto paralelo.
En computadoras modernas de tipo SISD el paralelismo viene implementado en la arquitectura
del procesador, conocido como paralelismo a nivel instrucción, por ejemplo: Pipeline o
procesamiento en línea de instrucciones. Este consiste en ejecutar al mismo tiempo diversas
etapas de instrucciones del programa; mientras en una etapa se hace la ejecución de una
instrucción, simultáneamente en otra etapa se está realizando una lectura de la siguiente
instrucción.
Una característica de procesadores recientes es que poseen varias ALUs (Unidad Aritmética
Lógica) para realizar operaciones de suma, resta, multiplicación y división en forma paralela. El
paralelismo a nivel de programación puede ser complicado o sencillo, depende de la
arquitectura, del modelo para programar en paralelo e inclusive del programa en sí.
IV.1 Programación Paralela
Para paralelizar una aplicación, es necesario contar con un lenguaje o biblioteca que brinde los
mecanismos necesarios para ello. Dependiendo de la herramienta y limitantes del equipo
disponible que se tenga, se particionará el código en piezas para que se ejecute en paralelo en
varios procesadores, originando el término “granularidad”.
IV.2 Granularidad
Granularidad es el tamaño de las piezas en que se divide una aplicación. Dichas piezas pueden
ser desde una sentencia de código, una función hasta un proceso en sí que se ejecutarán en
paralelo.
IV.2.1 Teoría de la Paralización de Grano Fino
Se presenta cuando el código se divide en una gran cantidad de piezas pequeñas. A nivel de
sentencia, o en un ciclo cuando se divide en varios subciclos que se ejecutan en paralelo.
También se le conoce como Paralelismo de Instrucción.
IV.2.1 Teoría de la Paralización de Grano Grueso
Se realiza a nivel de subrutinas o segmentos de código, donde las piezas son pocas y de
cómputo más intensivo que las de grano fino. También se le conoce como Paralelismo de
Tareas.
20
Paralelización
El nivel más alto de paralelismo de grano grueso se presenta cuando se detectan tareas
independientes y estas se ejecutan como procesos independientes en más de un procesador.
Este esquema es común en las aplicaciones de Productor/Consumidor, Lector/Escritor,
Maestro/Esclavo y Cliente/Servidor. En los modelos de Memoria Distribuida (paso de mensajes)
solo se implementa paralelismo de grano grueso.
El paralelismo de grano fino y grueso se puede presentar en sistemas de memoria compartida
sólo que el de grano grueso es más complicado de programar que el de grano fino.
21
Paralelización
P a r t e II: Paradigmas de Programación Paralela
I. ¿Que hacer ante un problema a paralelizar?
Es una pregunta abierta, para la cual no existe una única respuesta, pues depende de la
naturaleza del problema. Solo se podrán paralelizar aquellos problemas cuya estructura lógica
subyacente sea de naturaleza paralela; es decir que nuestro problema pueda ser analizado a
partir de la descomposición de sus partes, y estas partes tengan cierto grado de independencia.
Aunque no hay una receta para paralelizar, en la Origin 2000 se aconseja:
•
•
•
•
•
Primero debemos considerar el código serial y optimizado.
Aplicarle paralelización automática.
Revisar el código paralelizado, para mejorar y afinas zonas en las que todavía no han
sido explotado su paralelismo.
Compilar y ejecutar el programa.
Revisar y comparan los resultados, con los resultados que se pretenden obtener. Si los
resultados son alentadores entonces restaría afinar aún más las partes de código, como
ciclos y estructuras. En caso contrario, se buscaría algún otro paradigma de
paralelización y realizar estudios comparativos de los resultados.
En general quizá sea más conveniente:
Iniciar el diseño de códigos paralelos desde cero.
II. Paradigmas de Programación Paralela
Se pueden desarrollar aplicaciones en paralelo mediante el uso de los siguientes paradigmas:
Lenguaje C, Fortran y C++ usando procesos UNIX (Nivel Proceso).
Lenguaje C con hebras POSIX (Nivel hilo).
Código fuente en FORTRAN, C o C++ usando directivas o pragmas respectivamente
(Nivel Sentencia).
Lenguaje C o Fortran usando envío de Mensajes (Nivel Proceso).
II.1 Código C, Fortran y C++ usando procesos UNIX (Nivel proceso)
Un proceso UNIX es una instancia de un programa en ejecución. Consta de un espacio de
direcciones, atributos del proceso y un hilo de ejecución. El kernel o núcleo es el encargado de
crear procesos y distribuirlos a diferentes CPUs, así como de maximizar la utilización del
sistema.
Los procesos UNIX se clasifican en:
Procesos pesados.
Procesos ligeros.
Tienen su propia pila, registros, datos y texto.
Toman un largo tiempo para crearse y destruirse. Se crean mediante el
uso de fork().
Comparten atributos con sus procesos padres.
Se crean y destruyen rápidamente.
22
Paralelización
Se conocen también como hilos.
Se crean mediante el uso de sproc().
La función fork() crea un nuevo proceso (hijo). El proceso hijo es una copia exacta del proceso
padre, el hijo corre en un espacio duplicado de direcciones del padre.
Ejemplo de fork en C:
/* fork.c: Función fork, se genera un proceso hijo que se ejecuta en paralelo con el padre */
#include <stdio.h>
#include <unistd.h>
main( )
{
}
printf("Inicio del proceso padre. PID = %d\n", getpid() );
if ( fork() == 0 ) { /* Proceso hijo con su propio codigo*/
printf ("Inicio proceso hijo. PID = %d, PPID = %d\n", getpid(), getppid());
/* codigo que se ejecutara en paralelo con el código del padre */
...
sleep(1); }
else
{
/* Proceso padre */
printf ("Continuación del padre. PID=%d\n", getpid ());
/* codigo que se ejecutara en paralelo con el hijo */
.
sleep (1);
}
printf ("Fin del proceso %d\n", getpid ());
exit (0);
sproc() es una llamada al sistema que permite crear un proceso que comparte algunos de los
atributos (espacio de direcciones virtuales, uid, descriptor de archivo, etc.) del proceso padre. El
padre y el hijo tienen su propio contador de programa, registros, apuntador a la pila, pero todo el
texto y datos que esta en el espacio compartido de direcciones (memoria) es visible a ambos
procesos. Por lo tanto, un proceso creado con sproc() no tiene su propio espacio de
direcciones, este continúa ejecutándose en el espacio de direcciones del proceso original. En
este sentido, cuando se paraleliza un programa se generan varios hilos de ejecución donde
cada hilo es una secuencia de instrucciones que corre en cada procesador. Existe un tipo de
hilos ligeros llamados hilos POSIX que ofrecen ciertas ventajas sobre los hilos convencionales.
Cuando usar sproc()
Cuando el espacio de direcciones es
compartido. Se pueden usar variables globales
para compartir datos.
Cuando el desempeño es crítico.
Cuando la portabilidad no es importante. No es
disponible en otros UNIX.
En IRIS, es el método más rápido y eficiente.
Cuando usar fork()
Cuando los procesos no puedan compartir
recursos.
fork() crea una copia exacta del padre.
El hijo obtiene su propia copia de espacio de
direcciones.
23
Paralelización
II.2 Código fuente C con hebras POSIX. (Nivel hilo)
En este método es necesario que el sistema operativo de la máquina paralela soporte hilos
POSIX. Por ejemplo IRIX, que es el sistema operativo de la Origin 2000 soporta hilos POSIX.
Cuando el sistema operativo crea un proceso, el proceso en sí es un hilo de ejecución. Un
programa puede crear muchos hilos que se ejecutan en el mismo espacio de memoria en forma
paralela.
Hilos POSIX (pthreads) son similares a los procesos ligeros de IRIX que se generan con
sproc(). Los hilos POSIX son los más recomendables a emplear, dado que son portables y
tienen mejor desempeño que los procesos ligeros de UNIX.
La portabilidad de código permite compilar y ejecutar el programa en otra plataforma que
soporte hilos POSIX. Se paraleliza el código insertando funciones de hilos POSIX, lo compila y
lo ejecuta en una plataforma de un procesador o de múltiples procesadores (IRIX) y el mismo
código lo puede compilar y ejecutar en otra plataforma (por ejemplo en Solaris).
Hilos POSIX pueden paralelizar:
Algunas dependencias de datos, usando
herramientas de sincronización.
E/S en C.
Llamadas a subrutinas o funciones.
Hilos POSIX no pueden paralelizar:
Ciclos individuales dentro de un programa
(deben ser reescritos como funciones).
Pipas gráficas.
Ejemplo de uso de hilos POSIX:
/* hola.c: Se generan 2 hilos POXIS para imprimir a pantalla "hola mundo". compilar:
cc hola.c -o hola -lpthread */
#include <pthread.h>
#include <stdio.h>
void* imprime_mensaje( void *ptr ); /*Prototipo de Función*/
main()
{
}
pthread_t hilo1, hilo2;
char *mensaje1 = "hola";
char *mensaje2 = "mundo";
/* se generan 2 hilos que se ejecutaran en paralelo con el padre */
pthread_create(&hilo1, NULL, imprime_mensaje, (void*) mensaje1);
/* El hilo padre puede estar haciendo otra cosa en paralelo con los 2 hilos POSIX hijos. Por ejemplo: */
printf("A lo mejor esto se imprime primero... \n");
pthread_create(&hilo2, NULL, imprime_mensaje, (void*) mensaje2);
sleep(1);
exit(0);
void* imprime_mensaje( void *ptr )
{
char *mensaje;
mensaje = (char *) ptr;
24
Paralelización
}
printf("%s \n", mensaje);
pthread_exit(0);
II.3 Código fuente en C o Fortran usando envío de Mensajes (Nivel
proceso)
En sistemas de memoria distribuida donde cada procesador tiene su propia memoria, el
programa a ejecutarse en paralelo debe hacer uso del modelo de “Envío de Mensajes”. Este
modelo es apropiado para la comunicación y sincronización entre procesos que se ejecutan de
manera independiente (con su propio espacio de direcciones) en diferentes computadoras. La
programación consiste en enlazar y hacer llamadas dentro del programa por medio de
bibliotecas que manejarán el intercambio de datos entre los procesadores.
Existen principalmente tres bibliotecas para programar bajo este esquema:
Message Passing Interface (MPI)
Parallel Virtual Machine (PVM)
Shared Memory (SHMEM)
En la Cray Origin 2000 la memoria esta físicamente distribuida entre todos los nodos y
lógicamente es compartida, por lo tanto los modelos que se hacen mención son aplicables.
Este esquema de implementación de paralelismo mediante el envío de mensajes es a nivel de
grano grueso, dada la generación de procesos o tareas independientes que se ejecutan en
varios procesadores. En MPI, PVM y SHMEM uno de los principales esquemas que se emplean
en la comunicación y generación de los procesos es el modelo de Maestro - Esclavo. Un
proceso maestro divide y distribuye el trabajo en sub-tareas, que las asigna a cada nodo
conocido como "trabajador" o "esclavo". Terminando cada "trabajador" su parte, envían los
resultados al maestro para que este recopile la información y la presente.
El siguiente ejemplo con MPI, no emplea el modelo de “Maestro-Esclavo” sino de "Todos
Trabajadores" que se ejecutan independientemente sin notificar a un maestro sus resultados.
Ejemplo:
/* MPIHola.c: Genera procesos independientes que imprimen a pantalla la cadena "Hola Mundo" */
#include <stdio.h>
#include "mpi.h"
main(int argc, char **argv)
{
int node, size;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &size );
MPI_Comm_rank(MPI_COMM_WORLD, &node);
printf("Hola Mundo del proceso %d de %d procesos \n",node, size);
MPI_Finalize();
}
Compilación en Origin:
Ejecución:
% cc -o MPIHola MPIHola.c -lmpi
% mpirun -np 4 MPIHola
25
Paralelización
II.4 Código fuente en FORTRAN, C o C++ usando directivas o pragmas
respectivamente. (Nivel Sentencia)
El paralelismo a nivel sentencia da la facilidad de que un programa escrito en forma serial
pueda re-escribirse manual o automáticamente en forma paralela, mediante el uso de algunas
herramientas (PCA, PFA, OpenMP) de los sistemas de memoria compartida.
El paralelismo a nivel sentencia consiste en paralelizar declaraciones de ciclos DO o for en el
código fuente de un programa, mediante la implementación de directivas, además de rutinas o
procedimientos más complejos.
Una vez que al código fuente se le han insertado las directivas o pragmas, el programa puede
compilarse y ejecutarse en un ambiente serial o paralelo. Serial en el sentido de que si no se
cuenta con un compilador paralelizador que soporte el uso de directivas o que la plataforma no
sea de multiprocesamiento, las directivas se ignoran totalmente y se ejecuta en forma
secuencial.
La concurrencia generada con las directivas o pragmas del compilador son del tipo de hilos
(procesos ligeros de sproc() ).
Dos de las herramientas más conocidas en los sistemas de memoria compartida como Origin
2000 son:
IRIS Power C (PCA)
IRIS Power Fortran (PFA)
Estas herramientas son propias de SGI, cuyas directivas y pragmas son exclusivas de esta
arquitectura.
II.4.1 OpenMP
OpenMP es un API estándar para programar en paralelo en sistemas de memoria compartida,
las funciones se pueden llamar desde C/C++ o fortran. Consta de directivas estándares, que
permiten la portabilidad y la escalabilidad, presenta además una interfase simple y flexible para
desarrollar aplicaciones en paralelo desde una PC hasta una supercomputadora paralela o
masivamente paralela.
El paralelismo de grano fino y grueso se implementa usando directivas o pragmas. Por ejemplo:
#define MAX 1000000
#include <stdio.h>
double a[MAX], b[MAX], c[MAX];
void diddle(double [], double [], double [], long);
void initialize()
{
26
Paralelización
}
int i;
for (i = 0; i < MAX; i++)
a[i] = b[i] = c[i] = 100.0;
main()
{
}
initialize();
diddle(a, b, c, MAX);
printf("a[0] = %g, b[3] = %g, c[0] = %g\n", a[0], b[3], c[0]);
void diddle(double a[], double b[], double c[], long max)
{
long i;
#pragma parallel local(i) shared(a, b, c) byvalue(max)
#pragma pfor iterate(i=0; max; 1)
for (i=0; i<max; i++)
a[i] = b[i] + c[i];
#pragma parallel local(i) shared(a, b, c) byvalue(max)
#pragma pfor iterate(i=0; max; 1)
for (i=0; i<max; i++)
b[i] = a[i] + c[i];
if (b[3] > 0.01)
b[3] = 0.01;
#pragma parallel local(i) shared(a, b, c) byvalue(max)
#pragma pfor iterate(i=0; max; 1)
for (i=0; <max; i++)
c[i] = a[i] + b[i];
}
27
Paralelización
III. Compiladores Paralelizadores
En cada arquitectura de multiprocesamiento, existen compiladores que aprovechan al máximo
las capacidades implícitas del microprocesador y manejo de la memoria. Tal es el caso de los
MIPSpro Compilers y el High Perfomance Fortran (HPF) de la Origin 2000 de SGI.
Estos compiladores soportan el uso de directivas de multiprocesamiento que se pueden
implementar de dos maneras:
Implementación Explícita (Paralelización Manual): La inserción de directivas lo realiza
el programador y aplica otras técnicas de programación dentro de código fuente.
Implementación Implícita (Paralelización Automática): El compilador analiza y reestructura el programa con una pequeña o ninguna intervención del programador.
En la paralelización automática se utilizan los analizadores para C o Fortran, cuya función es
detectar e incluir paralelismo de grano fino en el código, es decir, solo en los ciclos en donde no
exista dependencia de datos.
El uso del paralelismo explícito requiere de conocer las directivas y su colocación dentro del
código fuente. Después, mediante el compilador paralelizador como PFA o PCA, se genera el
código objeto con las transformaciones necesarias para que el programa se ejecute en paralelo
en un ambiente de multiprocesadores.
III.1 Dependencia de Datos
En la programación paralela es importante identificar que parte del código se puede dividir en
piezas para su ejecución independiente. Idealmente al tomar un código serial, se distribuyen las
operaciones en números iguales entre la cantidad de procesadores, de esta forma los
procesadores ejecutarían la misma cantidad de instrucciones en paralelo. Sin embargo,
usualmente los códigos tienen algunas operaciones que se deben ejecutar serialmente. Si estas
operaciones son forzadas para ejecutarse en paralelo, producirán resultados incorrectos, puesto
que las variables serán utilizadas antes de que su valor sea calculado o actualizado. Esta
condición es llamada dependencia de datos.
La condición esencial requerida para paralelizar correctamente un ciclo es que cada iteración
debe ser independiente del resto de las iteraciones. Si un ciclo cumple con esta condición,
entonces el orden de ejecución de las iteraciones no importa. Pueden ser ejecutadas al revés o
al mismo tiempo, y la respuesta sigue siendo la misma. Es decir hay independencia de datos.
En un programa, las posiciones de memoria son representadas por nombres de variables. Así
pues, determinar si un ciclo en particular puede ejecutarse en paralelo, consiste en examinar la
manera en que las variables son utilizadas en los ciclos. Es necesario poner particular atención
en las variables que aparecen en el lado izquierdo de las asignaciones, porque la dependencia
de datos ocurre solamente cuando las posiciones de memoria son modificadas. Si una variable
no se modifica, no hay dependencia de datos asociada a ella.
Ejemplo: Hay un ciclo en el cual cada elemento de un arreglo es multiplicado por 2. Si el ciclo se
ejecuta en 2 procesadores, a cada procesador se le asignará un grupo de índices del ciclo. En
el ejemplo, al procesador 1 se le asigna i = 1, 50, y al procesador 2 se le asigna i = 51, 100.
28
Paralelización
En este ejemplo no hay dependencia de datos. Pero si el ciclo lo escribimos diferente:
Si las dimensiones del ciclo son de 0 a 100, y a(0) = 1, el ciclo serial generará resultados para la
variable a = 1, 2, 4, 8,... En cambio, si el ciclo se distribuye en dos procesadores, se presenta
un problema, pues el procesador 2 inicia el cómputo con a(51) = a(50)*2.0, pero a(50) todavía
no ha encontrado su valor inicial, así que el resultado a(51)=2.0, no es el resultado que se
espera. Este es un ejemplo de dependencia de datos.
III.2 Bases Teóricas de la Paralelización Automática y Técnicas de Paralelización
de Programas Secuenciales
Las opciones de paralelización automática del compilador analizan y reestructuran el programa
con poco o nada de intervención del programador. Esta paralelización ayuda a tomar ventaja
del paralelismo en programas para proveer un mejor rendimiento en sistemas de
multiprocesamiento. Es una extensión del compilador controlada con banderas en la línea de
instrucciones que invoca la paralelización automática de algunos compiladores.
La paralelización automática inicia con la determinación de la dependencia de datos de
variables y arreglos en ciclos. La dependencia de datos puede evitar que los ciclos sean
ejecutados en paralelo, pues el resultado final puede variar dependiendo del orden de acceso
de los procesadores a las variables y arreglos.
III.2.1 Casos de Estudio de No Paralelización
Hay situaciones donde no se puede paralelizar los ciclos con la opción automática:
a. Llamada a funciones en ciclos.
b. Declaraciones GO TO en ciclos.
29
Paralelización
c. Subíndices complicados en arreglos.
d. Referencia indirecta a un arreglo.
e. Índices imposibles de analizar
f. Conocimiento oculto.
g. Variables condicionales temporales asignadas en un ciclo.
h. Uso de punteros en C/C++ imposibles de analizar.
i. Arreglos de Arreglos
j. Ciclos limitados por comparaciones de punteros
k. Parámetros "aliados"
l. Ciclos anidados paralelizados incorrectamente
a. Llamada a funciones en ciclos
Por defecto, el paralelizador automático no paraleliza un ciclo que contiene una llamada a
función, pues la función en una iteración puede modificar o depender de datos de otras
iteraciones del ciclo.
b. Declaraciones GO TO en ciclos
Su uso puede causar dos problemas:
No es posible paralelizar ciclos con salidas tempranas, ni automática o manualmente.
Flujos de control no estructurados.
c. Subíndices complicados en arreglos
Casos donde los índices del arreglo son muy complicados para permitir la paralelización.
d. Referencia indirecta a un arreglo
El paralelizador automático no puede analizar referencias indirectas del arreglo. Por ejemplo:
for (i = 1; i < n; i++)
a[b[i]]...
Este ciclo no puede ser ejecutado en paralelo si la referencia indirecta a b[i] tiene el mismo valor
para diferentes iteraciones de i. Si cada elemento del arreglo b es único, el ciclo se puede
ejecutar en paralelo. En tales casos, es posible utilizar los métodos manuales o las directivas
para permitir la paralelización.
e. Índices imposibles de analizar
El analizador automático no puede paralelizar ciclos que contienen arreglos con índices
imposibles de analizar. En el siguiente caso, el paralelizador automático no puede analizar el
símbolo "/" en el subíndice del arreglo y no puede reordenar el ciclo.
for (i = 1; i < u; i+=2)
a(i/2) = ...; /* No se puede paralelizar ni reordenar*/
f. Conocimiento oculto
Puede haber conocimiento oculto en la relación entre las variables. m y n en este ejemplo:
for (i = 1; i < n; i++) a[i] = a[i+m]; /* Error */
El ciclo puede ser ejecutado en paralelo si n > m, porque los arreglos no se traslaparán. Sin
embargo, el paralelizador automático no conoce el valor de las variables, este no puede
paralelizar el ciclo.
30
Paralelización
g. Variables condicionales temporales asignadas en un ciclo
Cuando se paraleliza un ciclo, el paralelizador automático localiza (privatiza) variables
temporales escalares y arreglos. Por ejemplo:
for (i = 1; i < n; i++) {
for (j = 1; j < n; j++)
for (j = 1; j < n; j++)
tmp[j] = …
a[j][i] = a[j][i] + tmp[j]
}
El arreglo tmp es utilizado para "guardar" espacio local. Para paralelizar adecuadamente el
ciclo externo “for (i)”, cada procesador se debe dar una parte distinta del arreglo tmp. En este
ejemplo, el paralelizador automático puede localizar el tmp y paralelizar el ciclo. El paralelizador
automático se ejecuta con problemas cuando una variable condicional asignada temporalmente
puede ser utilizada fuera del ciclo, como en el ejemplo siguiente:
function s1(a, b)
{
real t;
for (j = 1; j < n; j++) if (b[j])
}
{
t = ...
a(i) = a(i) + t;
}
s2(); /* Llamada a la función s2*/
Si el ciclo se ejecutara en paralelo, habría un problema si el valor de t fuera utilizado dentro de
la subrutina s2(). ¿Cuál copia de t utilizaría la subrutina s2()? Si t no fuera asignada
condicionalmente, la respuesta sería que el procesador ejecute la iteración n. Pero t es
condicionalmente asignada y el paralelizador automático no puede determinar cual copia debe
utilizar.
El ciclo es paralelo si la variable t asignada condicionalmente es localizada. Si el valor de t no
es utilizado fuera del ciclo, se podría reemplazar a t con una variable local. A menos que t sea
una variable local, el paralelizador automático debe asumir que s2() pudo ser utilizada.
h. Uso de punteros imposibles de analizar en C/C++
C y C++ tienen características que dificultan paralelizar automáticamente. Muchas de estas
características se relacionan con la utilización de punteros. En Fortran es más fácil paralelizar
ya que los punteros tienen un uso muy restringido.
i. Arreglos de Arreglos
Los arreglos multidimensionales son algunas veces implementados como arreglos de arreglos.
double **p;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
p[i][j] = ...
Si p es un arreglo multidimensional, el ciclo externo puede ejecutarse en paralelo. Si dos de los
punteros del arreglo, por ejemplo p[2] y p[3], se refieren al mismo arreglo, el ciclo no debe
ejecutarse en paralelo. Aunque esta referencia duplicada es inverosímil, el paralelizador
31
Paralelización
automático no puede probar que no existe. Se puede evitar este problema utilizando siempre
arreglos verdaderos. La forma adecuada de paralelizar el fragmento de código anterior será:
double p[n][n];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
p[i][j] = ...
j. Ciclos limitados por comparaciones de punteros
El paralelizador automático reordena sólo ciclos donde el número de iteraciones pueda ser
determinado exactamente. En Fortran es raro que esto sea un problema, pero en C y C++
pueden surgir problemas referentes a sobreflujo y aritmética sin signo. En consecuencia los
ciclos no se deben limitar por comparaciones de punteros tales como:
int* pl, pu;
f
or (int *p = pl; p != pu; p++)
Este ciclo no puede ser paralelizado, y la compilación dará un archivo de resultados *.l que
indica que el límite no puede ser evaluado. Para evitarlo, se debe reestructurar el ciclo así:
int lb, ub;
for (int i = lb; i <= ub; i++)
k. Parámetros "aliados"
Los alias ocurren cuando existen múltiples maneras de referirse al mismo dato. Por ejemplo,
cuando el direccionamiento de una variable global se pasa como argumento a una subrutina,
puede ser referenciado con su nombre global o vía puntero. Quizás el impedimento más
frecuente para paralelizar en C y C++ es la información "aliada". Aunque Fortran garantiza que
los múltiples parámetros para una subrutina no son aliados unos a otros, C y C++ no lo hacen.
Ejemplo:
void sub(double *a, double *b, n) {
for (int i = 0; i < n; i++) a[i] = b[i];
Este ciclo puede ser paralelizado solamente si los arreglos a y b no poseen el mismo espacio
en memoria.
l. Ciclos anidados paralelizados incorrectamente
El paralelizador automático paraleliza un ciclo distribuyendo sus iteraciones entre los
procesadores disponibles, normalmente intenta paralelizar el ciclo exterior, pues el resultado del
rendimiento es usualmente mejor. Si no puede hacerlo, probablemente es por una de las
razones que se mencionaron, entonces intenta hacer un intercambio del ciclo externo por el
interno que si puede ser paralelizado. Ejemplo de ciclos anidados.
for (i = 1; i < n; i++)
for (j = 1; j < n; j++)
...
Sin embargo, es posible que un ciclo este paralelizado incorrectamente. Dada una jerarquía de
ciclos, el paralelizador automático sólo paralelizará uno de los ciclos en la anidación. En
32
Paralelización
general, es mejor paralelizar ciclos externos que internos. Intentará paralelizar el ciclo externo o
intercambiar el ciclo paralelizado de modo que sea externo, pero esto a veces no es posible.
Es mejor paralelizar ciclos que tienen gran número de iteraciones. Ejemplo:
for (i = 1; i < m; i++)
for (j = 1; j < n; j++)
El paralelizador automático puede decidir paralelizar el ciclo de i, pero si m es muy pequeña,
sería mejor intercambiar el ciclo de j para que sea exterior y después paralelizarlo. El
paralelizador automático no tiene manera de saber si m es pequeña. En tales casos, se debe
utilizar cualquier directiva insertada manualmente para decirle al paralelizador automático que
paralelize el ciclo.
Debido a la jerarquía de la memoria, el rendimiento puede ser mejorado si los procesadores
tienen acceso a los mismos datos en todos los ciclos anidados paralelos.
Ejemplo: Ciclo ineficiente
for (i = 1; j < n; j++)
for (i = n; i > 1; j--)
a[j]…
a[i]...
Asumiendo que existen p procesadores. En el primer ciclo, el primer procesador tendrá acceso
a los primeros n/p elementos de a, el segundo procesador tendrá acceso a los siguientes n/p y
así sucesivamente. En el segundo ciclo el primer procesador tendrá acceso a los últimos n/p
elementos de a. Asumiendo que n no es muy grande, esos elementos estarán en el caché de
los procesadores. Acceder a datos que están en el caché de algún otro procesador puede ser
muy costoso. El ejemplo mencionado, puede ser más eficientemente si se invierte la dirección
de uno de los ciclos.
Ejemplo: Ciclo Eficiente
for (i = 1; i < n; i++)
for (j = 1; j < n; j++)
for (i = 1; i < n; i++)
for (j = 1; j < n; j++)
a[i][j] = b[j][i] + ...
b[i][j] = a[j][i] + ...
Aquí, el paralelizador automático puede elegir paralelizar el ciclo externo en ambas anidaciones.
Esto significa que en el primer ciclo el primer procesador esta accesando los primeros n/p
renglones de a y las primeras n/p columnas de b, mientras que en el segundo ciclo el primer
procesador esta accesando las primeras n/p columnas de a y los primeros n/p renglones de b.
Este ejemplo será más eficiente si se paraleliza el ciclo i en una anidación y el ciclo j en la otra.
Alternativamente puede preferir agregar directivas para resolver este problema.
III.2.2 Analizadores PCA y PFA
Cuando una aplicación se ejecuta en una plataforma de multiprocesadores y el rendimiento es
crítico, se puede hacer que algunas partes se ejecuten en paralelo. Los compiladores de SGI, C
y Fortran, tienen pre-procesadores que analizan el código fuente y donde es posible, producen
el código objeto que utiliza un ambiente de multiprocesadores.
Los analizadores PCA y PFA son pre-procesadores que asisten en la optimización y
paralelización de programas en C o Fortran.
33
Paralelización
PCA:
Permite hacer uso eficiente de las plataformas de multiprocesamiento de SGI,
generando segmentos de código que se ejecuten en paralelo.
Consta del compilador estándar de C y un pre-procesador que automáticamente analiza
el código secuencial para determinar qué ciclos pueden ser ejecutados en paralelo. El
pre-procesador genera una versión modificada del código fuente con las directivas de
multiprocesamiento agregadas.
El compilador de C, cuando compila el código fuente modificado, interpreta las directivas y
produce un código objeto que utiliza múltiples procesadores. Una ventaja de PCA es que se
puede usar para recompilar programas seriales existentes de modo que se ejecuten
eficientemente en computadoras de multiprocesamiento sin tener que volver a codificar.
PCA es un pre-procesador de optimización de código en C que detecta el paralelismo.
El analizador PCA tiene las siguientes funciones:
•
•
•
•
Generar código en C para ejecutarse en paralelo.
Determinar dependencia de datos, pues evitan que el código se ejecute en paralelo.
Distribuir los ciclos "bien portados" a través de multiprocesadores.
Optimizar el código fuente.
Se puede utilizar PCA como herramienta independiente o como una fase del compilador de C.
También se pueden incorporar directivas directamente en el programa que producen código
concurrente, mejor que utilizando PCA. La siguiente figura ilustra el papel de PCA en producir
un módulo ejecutable que pueda utilizar más de un procesador en un sistema de
multiprocesamiento.
Utilizando PCA para producir un programa paralelizado
PCA produce un listado que contiene información acerca de los ciclos que pueden o no pueden
ser paralelizados. Utilizando esta información, se puede modificar el código fuente de modo que
una compilación subsecuente de PCA produzca un código más eficiente. Se puede seleccionar
una compilación de PCA para el código fuente especificando la opción del compilador PCA
donde se compila el programa.
PFA:
Es un pre-procesador de Fortran, que permite ejecutar eficientemente programas de
Fortran en sistemas de multiprocesamiento.
PFA analiza un programa e identifica ciclos independientes de datos. Cuando determina que un
ciclo no contiene dependencia de datos, automáticamente inserta directivas especiales del
34
Paralelización
compilador en el programa para producir una copia modificada del código fuente. El compilador
Fortran 77 de SGI puede entonces interpretar estas directivas para generar código que puede
ejecutarse a través de todos los procesadores disponibles.
Como las directivas insertadas por PFA parecen declaraciones estándares de comentarios de
Fortran 77, PFA no afecta la portabiblidad del código a los sistemas que no son de SGI. Si PFA
no puede determinar que ciclo es independiente, produce un archivo de listado detallando
donde existe el problema. La siguiente figura ilustra el papel de PFA en producir un módulo que
puede utilizar más de un procesador en un sistema de multiprocesadores.
Utilizando PFA para producir un programa paralelizado
Si se requiere PFA puede producir un archivo de listado, que explica qué ciclos fueron
paralelizados y cuales no, explicando ¿por qué?. Podemos utilizar esta información para
modificar la aplicación para un uso eficiente en múltiples procesadores.
Compilando con PCA/PFA
Los compiladores MIPSpro invocan la paralelización automática utilizando las banderas: -pfa o pca en la línea de instrucciones. La sintaxis para compilar con paralelización automática es:
Para Fortran 77 y Fortran 90 se usa -pfa:
Para C y C++ se usa -pca:
% f77 -pfa [{ list | keep }] [ -mplist ] archivo.f
% f90 -pfa [{ list | keep }] [ -mplist ] archivo.f
% cc -pca [{ list | keep }] [ -mplist ] archivo.c
% CC -pca [{ list | keep }] [ -mplist ] archivo.cc
Donde:
Opciones
-pfa y –pca
list
keep
-mplist
Descripción
Invocan la paralelización automática y habilitan las directivas de
multiprocesamiento.
Produce un listado de las partes del programa que pueden (o no) ser ejecutadas
en paralelo en múltiples procesadores. El archivo de listado tiene la extensión *.l.
o *.list. En al Origin 2000 de la DGSCA (UNAM) la extensión es *.list.
Genera el archivo de listado (*.list), el programa equivalente transformado (*.m), y
crea un archivo de salida para ser utilizado con WorkShop MPF (*.anl).
Genera un programa equivalente transformado en un archivo a.w2f.f para Fortran
77 o a.w2c.c para C.
La paralelización automática presenta las siguientes desventajas
Inserta directivas de paralelización en regiones que no tienen gran peso en el cómputo.
Todos los ciclos for o do que encuentre intentara paralelizarlos.
Asume que hay dependencias de datos donde con la intervención del programador se
puede eliminar esa dependencia.
35
Paralelización
Por tal razón, es recomendable recurrir a la paralelización manual después de haber analizado
el código con los analizadores PCA o PFA.
Limitantes de los Analizadores:
PCA
Solo puede analizar “ciclos for”.
No puede paralelizar “ciclos for” con apuntadores
de notación aritmética.
No puede analizar “ciclos while o do”.
No busca paralelizar bloques de código.
PFA
Sólo puede analizar “ciclos do”.
III.3 Paralelización Manual
La paralelización manual se realiza mediante el uso de directivas del compilador y otras
técnicas de programación dentro del código. Antes de proceder con esta técnica de
paralelización, el programador debe conocer la respuesta a las cuestiones como:
¿Cuales ciclos se pueden paralelizar?,
¿Qué variables son locales para cada hilo?,
¿Qué variables son compartidas?,
¿Cómo se distribuyen los datos?, etc.
Con la inserción de directivas en el código, se especifica al compilador lo que debe realizar.
Se emplea la paralelización manual cuando:
El paralelizador automático no paraleliza suficientemente el código y se recurre a
insertar directivas manualmente.
El paralelizador automático detecta que hay casos en que la paralelización es imposible,
como por ejemplo: una lectura/escritura, llamadas a subrutinas, y dependencias de datos
en el ciclo.
Región paralela:
Es una sección de código con ciclos o bloques ejecutados independientemente.
Una región paralela de código puede contener secciones que se ejecutan secuencialmente, así
como otras secciones que se ejecutan concurrentemente (ver figura).
Esquema de un programa con partes ejecutándose en modo secuencial y paralelo
Regiones paralelas dentro del código fuente usando directivas o pragmas
36
Paralelización
PCA:
#pragma parallel [modificador (lista) ...] ...
{
/* codigo "local" ejecutado por todos los hilos (threads) */
}
Las demás directivas de paralelización deben estar dentro de una región paralela.
PFA:
C$DOACROSS [modificador (lista), ...] ...
DO I = 1, N
C
Cuerpo del ciclo
END DO
III.3.1 Sintaxis de las directivas de paralelización
El uso de directivas para paralelizar código ya escrito previamente resulta ventajoso.
Mencionamos las directivas más importantes para llevarlo a cabo la paralelización de códigos:
En Fortran:
Directiva C$DOACROSS
Es una directiva de PFA para crear una región paralela, por ejemplo:
c$doacross share(a), local(x),
c$& lastlocal(j)
do j =1, n
x = a(j) ** j
enddo
Modificadores para C$DOACROSS
Los modificadores se escriben como una lista separada por comas sobre la misma línea
o una línea separada usando C$&
shared (variables): Variables compartidas entre los hilos.
reduction (variables): Variables involucradas en una reducción.
local (variables): Variables locales a cada hilo.
lastlocal (variables): Variables locales en el ciclo; el valor de la ultima iteración
del ciclo original es guardado.
En C:
Directiva “#pragma parallel”
Esta directiva de PCA es usada para crear una región paralela, por ejemplo:
main()
{
int cuenta;
....
#pragma parallel shared(a,b,c) local(i) byvalue(max)
#pragma if (cuenta > 5)
{
/* Codigo del bloque A */
}
37
Paralelización
...
}
Modificadores para “#pragma parallel”
Los modificadores pueden ser colocados en la misma línea, o como una sentencia
pragma por separado después de “#pragma parallel”. Los más comunes son:
shared (nombres de variables): Variables compartidas entre los hilos.
byvalue (nombres de variables): Variables compartidas de solo lectura.
local (nombres de variables): Variables locales a cada hilo.
Directiva #pragma pfor
#pragma parallel ...
{
#pragma pfor iterate (i = 0; 1000; 1)
#pragma schedtype(dynamic) chunksize(10)
}
for(i=0; i<1000; i++) {
/* Bloque de codigo B */ }
Modificadores:
iterate (index = start; num_iters; incr): Significa que inicializa un valor, el número
total de iteraciones y los brincos.
La variable index debe ser inicializada antes del ciclo.
schedtype(arg): Especifica que tipo de calendarización se aplicará al ciclo.
chunksize(arg): Número de iteraciones por hilo cuando la calendarización es
dinámica (dynamic) o intercalada (interleave).
III.3.2 Creando bloques independientes
No solo los ciclos pueden paralelizarse, sino también el código delimitado con la directiva
“#pragma independent” llamado código independiente dentro de una región paralela (esto solo
es valido para PCA). Su ejecución en paralelo es una tarea que se ejecuta en un hilo. Este tipo
de paralelismo es de grano grueso o "paralelismo heterogéneo", ya que el código de cada hilo
de ejecución es diferente, en comparación al de grano fino o "paralelismo homogéneo", en la
que todos los hilos ejecutan el mismo código sobre diferentes datos.
PCA:
Cada bloque de código independiente es calendarizado en su propio hilo.
Cuando finaliza un hilo, la ejecución de un bloque independiente, empieza a trabajar sobre otro.
#pragma parallel ....
{
#pragma independent
{
/* tarea 1, corre en paralelo pero independiente*/
#pragma independent
{
/* tarea 2, corre en paralelo pero independiente*/
<código local es ejecutado por todos los hilos>
}
38
}
}
Paralelización
PFA:
c$doacross local(i)
do i=1, 2
if (i .eq. 1) then
else
enddo
endif
call tarea1
call tarea2
III.3.3 Ejemplo de paralelización automática
//Archivo de nombre: Par_aut.c de Paralelización Automática
#define MAX 1000000
#include <stdio.h>
double a[MAX], b[MAX], c[MAX];
void diddle(double [], double [], double [], long);
void initialize();
main()
{
}
initialize();
diddle(a, b, c, MAX);
printf("a[0] = %g, b[3] = %g, c[0] = %g\n", a[0], b[3], c[0]);
void initialize()
{
int i;
for (i = 0; i < MAX; i++)
}
a[i] = b[i] = c[i] = 100.0;
void diddle(double a[], double b[], double c[], long max)
{
long i;
for (i=0; i<max; i++)
a[i] = b[i] + c[i];
for (i=0; i<max; i++)
b[i] = a[i] + c[i];
if (b[3] > 0.01) b[3] = 0.01;
for (i=0; i<max; i++)
c[i] = a[i] + b[i];
39
Paralelización
}
Compilando este programa con la opción de paralelización automática:
% cc -pca list Par_aut.c
El archivo de listado (Par_aut.list) muestra que los ciclos no han sido paralelizados debido a la
dependencia de datos detectada, y muestra el estado de paralelización de cada lazo:
Si se sabe que un ciclo se puede paralelizar, aunque en la forma automática no lo haya
realizado, entonces se procede a eliminar esa dependencia insertando directivas manualmente.
#define MAX 1000000
#include <stdio.h>
double a[MAX], b[MAX], c[MAX];
void diddle(double [], double [], double [], long);
void initialize()
{
int i;
for (i = 0; i < MAX; i++)
}
main()
{
a[i] = b[i] = c[i] = 100.0;
initialize();
diddle(a, b, c,MAX);
40
Paralelización
printf("a[0] = %g, b[3] = %g, c[0] = %g\n", a[0], b[3], c[0]);
}
void diddle(double a[], double b[], double c[], long max)
{
long i;
#pragma parallel local(i) shared(a, b, c) byvalue(max)
#pragma pfor iterate(i=0; max; 1)
for (i=0; i<max; i++)
a[i] = b[i] + c[i];
#pragma parallel local(i) shared(a, b, c) byvalue(max)
#pragma pfor iterate(i=0; max; 1)
for (i=0; i<max; i++)
b[i] = a[i] + c[i];
if (b[3] > 0.01)
b[3] = 0.01;
#pragma parallel local(i) shared(a,b,c) byvalue(max)
#pragma pfor iterate( i = 0; max; 1)
for (i=0; i<max; i++)
c[i] = a[i] + b[i];
}
Al compilar el programa con las directivas insertadas se genera el ejecutable para un sistema
de multiprocesamiento, de la siguiente manera:
%cc -mp -o salida ParautDi.c
la opción –mp genera código de multiprocesamiento para los archivos a ser compilados. Esta
opción origina que el compilador reorganice todas las directivas de multiprocesamiento. La
opción –apo contiene esta opción.
Se define la variable de ambiente MP_SET_NUMTHREADS para indicar con cuantos hilos
(usualmente se ejecuta uno por procesador) se ejecutará el programa:
%setenv MP_SET_NUMTHREADS 4
Al final, se ejecuta el programa salida monitoreándolo con la instrucción top.
41
Paralelización
42
Paralelización
IV. Análisis de Rendimiento
El rendimiento de un programa paralelo es una cuestión compleja que depende de muchos
factores:
Tiempo de ejecución, escalabilidad, generación, almacenamiento y comunicación de
datos.
Métricas diversas.
Tiempo de ejecución, eficiencia, requerimientos de memoria, throughput, latencia,
rango de entrada/salida, uso de la red, costos de implementación, requerimientos
de hardware, portabilidad, escalabilidad, etc.
Las métricas pueden ser dependientes de la aplicación.
Predicción de tiempo: tiempo de ejecución, costos de hardware, costos de
implementación, exactitud, confiabilidad y escalabilidad.
Bases de datos paralelas: throghput, costos de implementación y mantenibilidad.
Sistemas de visión: cantidad de imágenes procesadas por segundos (throughput) y el
tiempo de espera (latencia).
Aplicaciones bancarias: rango de tiempo de ejecución/costo.
Puntos a revisar:
•
•
Tiempo de Ejecución.
Escalabilidad
IV. 1 Modelación del Rendimiento
Enfoques usuales:
•
•
•
Ley de Amdahl
Extrapolación de Observaciones
Análisis Asintótico.
IV.1.1 Ley de Amdahl
Si la fracción s del tiempo es inherentemente secuencial, entonces, la aceleración es ≤
T p = st s +
S ( P) =
(1 − s )t s
p
ts
p
=
st s + (1 − s )t s / p 1 + ( p − 1)s
¿Qué representa la “inherentemente secuencial”?
Es relevante en la paralelización gradual y parcial de programas secuénciales.
¿Se puede aplicar a una solución paralela nueva?
43
1
.
s
Paralelización
IV.1.2 Extrapolación a partir de observaciones
Ejemplo:
T1 = n +
n2
,
p
(
)
(
T2 = n + n 2 / p + 100 ,
)
T3 = n + n 2 / p + 0.6 p 2
Aceleración de 10.8 con 12 procesadores para n = 100
IV.1.3 Análisis Asintótico
“Un algoritmo requiere O(n log n ) tiempo en O(n ) procesadores”
Existen constantes c y n 0 , tal que, para todo n > n 0 , cos to( n) ≤ cn log n en n
procesadores.
Si el costo actual del algoritmo es 10n + n log n el termino 10n es mayor para n < 1024 .
Costo asintótico, no absoluto.
Ejemplo: 100n log n > 10n 2 , n < 996
44
Paralelización
IV.1.4 Basado en modelos idealizados
Ejemplo: PRAM (Parallel Random Access Machine)
Un buen modelo de rendimiento
Deber ser capaz de explicar observaciones.
Debe predecir circunstancias futuras.
Debe abstraer detelles no relevantes.
Técnicas para modelado de rendimiento a un nivel de detalle intermedio
Adecuadas para multicomputadoras: T = f (n, p, u ,...)
El objetivo es desarrollar expresiones matemáticas que especifiquen el tiempo de
ejecución como funciones de n, p,...
Los modelos deben ser suficientemente simples pero con un nivel de exactitud
aceptable.
IV. 2 Tiempo de Ejecución
El tiempo de ejecución de un programa paralelo es el tiempo que transcurre desde que el primer
procesador inicia su ejecución hasta que el último procesador termina su ejecución.
T = Tcomp + Tcomm + Tidle
Tcomp = Tiempo de cómputo
Tcomm = Tiempo de comunicación
Tidle = Tiempo de espera
IV.2.1 Tiempo de Cómputo
Tiempo invertido en procesamiento (no en comunicación ni en espera)
Depende normalmente del tamaño del problema, n .
Replicación: depende también de la cantidad de tareas.
Heterogeneidad: varía de procesador a procesador.
IV.2.2 Tiempo de Comunicación
45
Paralelización
Tiempo invertido en comunicar información de un procesador a otro.
Comunicación entre procesadores, es distinto a la comunicación dentro de un procesador.
IV.2.3 Tiempo de espera
Tiempo perdido en espera de la ocurrencia de eventos, tiempos de sincronización, tiempos de
trabajo adicional.
IV. 3 Modelo de Comunicación
Modelo de Comunicación Simple
Tmsg = t s + t w L
t s = Tiempo de arranque (latencia)
t w = Tiempo de transferencia por palabra
L = Longitud del mensaje en palabras
IV. 4. Eficiencia y aceleración
Eficiencia Relativa: la fracción del tiempo que los procesadores se mantienen haciendo trabajo
útil.
E relativa =
T1
pT p
Dado que los tiempos de ejecución varían con el tamaño del problema, los tiempos de ejecución
deben ser normalizados cuando se compara el rendimiento de un algoritmo para problemas de
diferente tamaño.
Aceleración Relativa: el factor por el cual el tiempo de ejecución se reduce en p procesadores.
S relativa = pE relativa
Las métricas son relativas ya que se calculan con respecto al algoritmo paralelo ejecutado en
un solo procesador.
Las métricas relativas son útiles para explorar la escalabilidad de un algoritmo pero no
constituyen una figura de mérito absoluta.
T1 = 10,000 seg , T1000 = 20seg , E relativa = 0.5 , S relativa = 500
46
Paralelización
T1 = 1,000seg , T1000 = 5seg , E relativa = 0.2 , S relativa = 200
IV.4.1 Eficiencia y Aceleración Absoluta
*
T1 el mejor algoritmo secuencial conocido
*
E absoluta =
T1
pT p
S absoluta = pE absoluta
IV. 5. Análisis de Escalabilidad
IV.5.1 Escalabilidad de un algoritmo
Que tan efectivamente se puede usar con un número creciente de procesadores.
¿Cómo varía el rendimiento del algoritmo con varios parámetros como el tamaño del problema,
el número de procesadores y el costo de arranque de mensajes?
Problema Fijo:
La variación del tiempo de ejecución, T , y eficiencia, E , con un número creciente de
procesadores para el mismo tamaño del problema.
¿Que tan rápido se puede resolver un problema en una computadora?
¿Cuál es el máximo número de procesadores que se pueden utilizar para mantener la
eficiencia arriba de un cierto nivel?
Problema: Revolvedor de Ecuaciones
Tcomp = t c n 2 / p
(
Tcomm = 2 p(t s + t w 2 * n )
E = t c n 2 / t c n 2 + t s 2 p + t w 4np
T = t c n 2 / p + 2 pt s + 4npt w
)
47
Paralelización
IV.5.2. Escalabilidad con problema variable
Problema Escalado:
Función de Isoeficiencia
Considera cómo la cantidad de cómputo realizado se debe escalar con el número
de procesadores para mantener la eficiencia constante.
O ( p ) es altamente escalable, pues la cantidad de cómputo que se
requiere incrementar crece únicamente en forma lineal para mantener la
eficiencia constante.
O( p 2 ) u O(2 p ) indicarían algoritmos no escalables.
E=
Tcomp
T1
+ Tcomm + Tidle
El tiempo de ejecución en un solo procesador se debe incrementar al mismo rango en que se
incrementa el tiempo paralelo total.
La cantidad de cómputo esencial se debe incrementar al mismo rango que el trabajo
adicional debido a replicación, comunicación y tiempos de espera.
El análisis de escalabilidad no se aplica a cualquier problema.
En algunas aplicaciones, como predicción de tiempo, es más importante terminar
una actividad dentro de ciertos límites de tiempo.
Ejemplo: Revolvedor de Ecuaciones
Para eficiencia constante, una función de p , cuando se sustituye por n , debe satisfacer la
siguiente relación para cuando p crece y E permanece constante.
(
t c n 2 ≈ E t c n 2 + t s 2 p + t w 4np
)
La función n = p , satisface este requerimiento y produce la siguiente relación la cual es válida
excepto cuando p es pequeña,


2
t c ≈ E  t c + t s + t w 4 
p


48
Paralelización
Escalando n con p provoca que el número de puntos de la malla y la cantidad de cómputo se
( )
escale con p 2 . La función de isoeficiencia es O p 2 .
IV. 6. Perfiles de Ejecución
Si el análisis de escalabilidad sugiere que el rendimiento es bajo en problemas de cierto tamaño
o con cierta cantidad de procesadores, entonces, se pueden usar modelos para identificar
ineficiencias y aspectos en los cuales un algoritmo puede ser mejorado.
Cómputo redundante, tiempos de espera, tiempo de inicio de mensajes, costos de
transferencia de datos, etc.
El perfil de ejecución de un algoritmo indica las contribuciones de estos factores diversos a los
tiempos de ejecución como factores de n y de p .
IV. 7. Estudios Experimentales
Los estudios experimentales se pueden usar para medir el valor de los parámetros de un
modelo (t c , t s , t w ,...) o para validar al modelo.
•
•
•
Diseño Experimental
Obteniendo y validando datos experimentales
Ajuste de datos
IV.7.1 Diseño Experimental
Identificación de datos:
Tiempo de cómputo para un problema unitario,
49
Paralelización
Tiempo de transferencia,
Tiempo de espera,
Promedio de búsqueda en un árbol, etc.
Diseño de experimentos
IV.7.2 Obteniedno y validando datos experimentales
Medición repetitiva de tiempos de ejecución.
Obtención de datos exactos y reproducibles.
Algoritmos no deterministas
Uso de relojes inexactos
Costos de inicio y terminación variables con el resto del sistema
Interferencia con otros programas.
Contención (varias computadoras comunicándose al mismo tiempo)
Asignación aleatoria de recursos.
IV.7.3 Ajuste de Datos
Cuando los estudios experimentales se realizan con el propósito de calibrar parámetros, se
ajustan los resultados observados a la función de interés para obtener los valores de los
parámetros conocidos.
Ajuste por mínimos cuadrados.
Ajuste por mínimos cuadrados escalados.
∑ (obs(i) − f (i) )
 obs (i ) − f (i ) 
∑i  obs(i) 


2
i
50
2
Paralelización
V. Estudio Comparativo
En esta sección se analiza el rendimiento del programa que calcula el valor de pi por
aproximaciones rectangulares, aplicándole los diferentes modelos de programación paralela
mencionados. Para conocer cual es el modelo recomendable a emplearse en un sistema Cray
Origin 2000 de memoria distribuida - compartida, en base a los criterios de:
•
•
Tiempo de ejecución, y
Facilidad en la programación
De inicio se debe obtener un perfil del programa ejecutado en forma secuencial y después
compararlo con los otros perfiles obtenidos en cada paradigma aplicado, finalmente se debe ir
midiendo el rendimiento del programa conforme se aumenta el número de procesadores, este
proceso es conocido como “aceleración” o “SpeedUp”.
Nota: Un buen perfil del programa, implicó ejecutarlo con 1 millon (10 6 ) de intervalos.
IV.1 Programa Secuencial (pi.c)
/*pi.c : Calcula el valor de pi, usando una aproximación rectangular a la integral de la función f(x) = 4/(1+x*x) */
#define MAXRECT 100000000 /* Número de intervalos */
#include <stdio.h>
void pieza_de_pi(int);
double total_pi = 0.0;
/* variable global */
main(int argc, char *argv[])
{
int nrects = MAXRECT; /* Número de rectangulos*/
pieza_de_pi(nrects);
printf("El valor de pi es = %f \n", total_pi);
}
void pieza_de_pi (int nrects)
{
double ancho; /* ancho del rectangulo de aproximación */
double x;
/* valor de x */
int i;
/* contador */
ancho = 1.0 / nrects;
/* peso de cada iteración */
for ( i = 0; i < nrects; i++ )
{
x = ((i - 0.5) * ancho); /* calcula x */
total_pi = total_pi + (4.0/(1.0 + x*x)); /* suma los valores de f(x) */
}
total_pi = total_pi * ancho; /* integra la curva */
}
Compilación:
%cc -o pi pi.c
Ssusage pone en ejecución un programa ejecutable e imprime los recursos utilizados.
51
Paralelización
Es una instrucción de SpeedShop. Se utiliza exactamente en la misma forma que time, pero
ssusage imprime información adicional. Aquí lo usamos para obtener el tiempo de ejecución
Salida: % ssusage pi y time pi (Observe las diferencias de información reportada)
Comparación al ejecutar el programa pi con las instrucciones ssusage y time
Al emplear diversos modelos de programación paralela, se encontrarán distintos tiempos de
ejecución. En consecuencia podremos determinar cual es el modelo más apropiado en términos
de tiempo y aceleración para esta máquina.
IV.2 Empleando Procesos Unix (SGI_pi.c)
Se anexan las funciones correspondientes de Irix para la generación de procesos ligeros, como
la función sproc().
/* SGI_pi.c: Calcula el valor de pi, usando una aproximación rectangular a la integral de la función f(x) = 4/(1+x*x)
usando llamadas de SGI [sproc()] y una arena. Usando distinta cantidades de procesadores: 1, 2, 4, 6, 8, 10*/
#include <stdio.h>
#include <ulocks.h>
#include <task.h>
#define MAXRECT 100000000 /* Número de intervalos */
#define MAX_PROCS 10;
void pieza_de_pi(void *); /* Prototipo de funcion*/
usptr_t *arena;
barrier_t *barr;
double cada_pi[MAX_PROCS];
int nrects = MAXRECT;
int nprocs;
/* variable global */
/* Número de rectangulos*/
main(int argc, char *argv[])
{
int i;
double total_pi = 0.0;
nprocs = prctl(PR_MAXPPROCS);
arena = usinit("/usr/tmp/theArena");
barr = new_barrier(arena);
for (i=0; i< nprocs -1; i++)
sproc(pieza_de_pi, PR_SALL, (void *)i);
pieza_de_pi((void *)(nprocs - 1));
/* sumando todas las piezas de pi */
for (i = 0; i < nprocs; i = i+1)
total_pi = total_pi + cada_pi[i];
printf("El valor de pi es = %f \n", total_pi);
}
52
Paralelización
void pieza_de_pi (void *arg)
{
double total_pi = 0.0;
double ancho;
/* ancho del rectangulo de aproximación */
double x;
/* valor de x */
int i;
/* contador */
int mi_id = (int) arg, inicio, fin, pedazo;
}
/* Esperar hasta que todos estén listos */
barrier(barr, nprocs);
pedazo = nrects/nprocs;
inicio = mi_id * pedazo;
fin = inicio + pedazo;
if (mi_id == (nprocs - 1)) fin = nrects;
ancho = 1.0 / nrects;
/* peso de cada iteración */
for ( i = inicio; i < fin; i++ )
{
x = ((i - 0.5) * ancho);
/* calcula x */
total_pi = total_pi + (4.0/(1.0 + x*x));
/* suma los valores de f(x) */
}
total_pi = total_pi * ancho; /* integra la curva */
cada_pi[mi_id] = total_pi;
/* espera hasta que todos hayan terminado */
barrier(barr, nprocs);
Compilación: % cc -o SGI_pi SGI_pi.c
Salida (Utilizando distintas cantidades de procesadores):
% ssusage ./SGI_pi
Prueba de SGI_pi sometido a miser con un procesador
53
Paralelización
Prueba de SGI_pi sometido a miser con 2 procesadores
Prueba de SGI_pi sometido a miser con 4 procesadores
Prueba de SGI_pi sometido a miser con 6 procesadores
54
Paralelización
Prueba de SGI_pi sometido a miser con 8 procesadores
Prueba de SGI_pi sometido a miser con 10 procesadores
55
Paralelización
Prueba de SGI_pi sometido a ssusage (arriba) y ejecución del "top" (abajo)
IV.3. Empleando Hilos Posix (POSIX_pi.c)
/* POSIX_pi.c: Calcula el valor de pi, usando una aproximación rectangular a la integral de la función f(x) = 4/(1+x*x)
usando hilos POSIX. Usando distinta cantidades de hilos POSIX : 1, 2, 4, 6, 8, 10 */
#include <pthread.h>
#include <stdio.h>
#include <math.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MAXRECT 100000000
#define NUMTHREADS 8
/* Numero de intervalos */
/* Numero de hilos */
double total_pi = 0.0;
pthread_mutex_t mi_mutex;
int nrects = MAXRECT; /* Numero de rectangulos*/
void* pieza_de_pi(void*);
main()
{
pthread_t mis_hilos[NUMTHREADS], temp;
int i, retval;
retval = pthread_mutex_init(&mi_mutex, NULL); /* Inicializa un mutex, para la sincronizacion de los hilos */
for ( i = 0; i < NUMTHREADS; i++)
{
retval = pthread_create(&temp, NULL, pieza_de_pi, (void*) i);
mis_hilos[i] = temp;
if (retval !=0 )
{
puts(strerror(retval));
exit(1);
}
56
Paralelización
}
}
for (i = 0; i < NUMTHREADS; i++ )
printf("valor de pi = %f \n", total_pi);
pthread_join(mis_hilos[i],0);
void* pieza_de_pi(void* arg)
{
double mi_pieza = 0.0;
double ancho; /* anchura del rectangulo */
double x;
int i;
int mi_id = (int) arg,inicio,fin,pedazo;
int retval;
pedazo = nrects/NUMTHREADS;
inicio = mi_id * pedazo;
fin = inicio + pedazo;
if (mi_id == (NUMTHREADS-1)) fin = nrects; /*Ult. proceso toma el resto de los intervalos */
ancho = 1.0 / nrects; /* peso de cada iteracion */
for (i=inicio; i<fin; i=i+1 ) {
x = ((i - 0.5) * ancho); /* calcula x */
mi_pieza = mi_pieza + (4.0/(1.0 + x*x)); } /* suma de f(x) */
mi_pieza = mi_pieza * ancho; /* integra la curva */
retval = pthread_mutex_lock(&mi_mutex);
total_pi = total_pi + mi_pieza;
retval = pthread_mutex_unlock(&mi_mutex);
pthread_exit(0);
}
Compilación:
% cc -o POSIX_pi POSIX_pi.c -lpthread
Salida (con varios hilos): % ssusage ./POSIX_pi
IV.4 Comunicación con MPI (MPI_pi.c)
/* MPI_pi.c: Calcula pi, usando una aproximación rectangular a la integral de la funcion f(x) = 4/(1+x*x) usando MPI.*/
#include <stdio.h>
#include "mpi.h"
double pieza_de_pi(int, int, long);
void main(int argc, char ** argv)
{
long intervalos = 100000000;
int miproc, numproc;
double pi, di;
int i;
MPI_Status status;
double t3, t4;
t3 = MPI_Wtime(); /* tiempo de inicio */
57
Paralelización
MPI_Init (&argc, &argv); /* Inicializar MPI */
MPI_Comm_rank(MPI_COMM_WORLD,&miproc); /* Determinar posición de procesos*/
MPI_Comm_size(MPI_COMM_WORLD,&numproc); /* Numero de procesadores */
MPI_Barrier(MPI_COMM_WORLD); /* sincronizar la distribución y cálculos */
if (miproc == 0) /* Si el maestro distribuye los intervalos */
{
for (i = 1; i < numproc; i++)
MPI_Send(&intervalos, 1, MPI_LONG, i, 98, MPI_COMM_WORLD);
}
else /* es un esclavo, recibe la seccion de intervalos */
MPI_Recv(&intervalos, 1, MPI_LONG, 0, 98, MPI_COMM_WORLD, &status);
/* cada proceso ejecuta pieza_de_pi */
pi = pieza_de_pi(miproc, numproc, intervalos);
MPI_Barrier (MPI_COMM_WORLD); /* sección de sincronización */
if (miproc == 0) /* maestro recoge los resultados, suma y los imprime */
{
for ( i = 1; i < numproc; i++)
{
MPI_Recv (&di, 1, MPI_DOUBLE, i, 99, MPI_COMM_WORLD, &status);
pi += di;
}
MPI_Barrier(MPI_COMM_WORLD);
t4 = MPI_Wtime(); /* tiempo final */
printf("Valor de pi: %lf \n",pi);
printf("Tiempo de ejecucion: %.3lf seg\n",t4-t3);
}
else
{ /* el esclavo envia los resultados al maestro */
MPI_Send(&pi, 1, MPI_DOUBLE, 0, 99, MPI_COMM_WORLD);
MPI_Barrier(MPI_COMM_WORLD);
}
MPI_Finalize ();
}
double pieza_de_pi(int idproc, int nproc, long intervalos)
{
double ancho, x, localsum;
long j;
}
ancho = 1.0 / intervalos; /* peso de la muestra */
localsum = 0.0;
for (j = idproc; j < intervalos; j += nproc)
{
x = (j + 0.5) * ancho;
localsum += 4 / (1 + x * x);
}
return(localsum * ancho);
Compilación:
Salida(con varios hilos):
% cc -o MPI_pi MPI_pi.c -lmpi
% ssusage mpirun -np 8 ./MPI_pi
58
Paralelización
Con 1 solo procesador:
Tiempo consumido (Arriba). Se ejecuta un solo proceso (Abajo con el comando “top”)
Con 2 procesadores:
Tiempo consumido (Arriba). Se ejecutan 2 procesos (Abajo con el comando “top”)
Con 4 procesadores:
Tiempo consumido (Arriba). Se ejecutan 4 procesos (Abajo con el comando “top”)
Con 6 procesadores:
59
Paralelización
Tiempo consumido (Arriba). Se ejecutan 4 procesos (Abajo con el comando “top”)
IV.5 Comunicación con PVM (PVM_Masterpi.c)
¿Qué es PVM (Parallel Virtual Machine)?
Es un paquete de software (conjunto de bibliotecas) para crear y ejecutar aplicaciones
concurrentes o paralelas, basado en el Modelo de paso de mensajes.
Funciona sobre un conjunto heterogéneo de ordenadores con sistema operativo UNIX
conectados por una o mas redes.
Permite:
- Arquitecturas de ordenadores diferentes.
- Redes de varios tipos.
Tiene dos componentes básicos:
A). El proceso daemon (pvmd3)
• Proceso Unix encargado de controlar el funcionamiento de los procesos de usuario
en la aplicación PVM y de coordinar las comunicaciones.
• Un daemon se ejecuta en cada máquina configurada en la máquina virtual paralela.
• Cada daemon mantiene una tabla de configuración e información de los procesos
relativa a su máquina virtual.(/tmp/pvmd.uid).
• Los procesos de usuario se comunican unos con otros a través de los daemons.
• Primero se comunican con el daemon local vía la librería de funciones de interfase.
• Luego el daemon local manda/recibe mensajes de/a daemons de los hots remotos.
• Cada máquina debe tener su propia versión de pvmd3 instalada y construida de
acuerdo con su arquitectura y además accesible con $PVM_ROOT.
B). La librería de rutinas de interface(libpvm3.a, libfpvm3.a & libgpvm3.a)
• libpvm3.a - Librería en lenguaje C de rutinas de interfase: Son simples llamadas a
Funciones que el programador puede incluir en la aplicación paralela.
Proporciona capacidad de:
60
Paralelización
•
•
•
•
- iniciar y terminar procesos.
pack, send, and receive mensajes.
sincronizar mediante barreras.
conocer y dinámicamente cambiar la configuración de la máquina virtual
paralela.
Las rutinas de la librería no se conectan directamente con otros procesos, si no que
mandan y reciben la configuración de la máquina virtual paralela.
libgpvm3.a- Se requiere para usar grupos dinámicos.
libfpvm3.a- Para programas en Fortran
Es un software de dominio Público desarrollado por el Oak Ridge National
Laboratory
¿Por qué usar PVM
Promete cumplir los requerimientos que necesita la computación distribuída del futuro,
permitiéndo escribir programas que utilicen una serie de recursos capaces de operar
independientemente y que son coordinados para obtener un resultado global que depende de
los cálculos realizados en todos ellos.
Reduce el "wall clock execution time"
Fácil instalación y uso. Con una interface de programación simple y completa.
Es un software de dominio público con creciente aceptación y uso.
Flexibilidad:
- Se adapta a diversas arquitecturas.
- Permite combinaciones de redes locales con las de área extendida.
- Cada aplicación "decide" donde y cuando sus componentes son ejecutados y
determina su propio control y dependencia.
- Se pueden programar diferentes componentes en diferentes lenguajes.
- Es fácil la definición y la posterior modificación de la propia Máquina Virtual Paralela.
Permite aprovechar el hardware existente, asignando cada tarea a la arquitectura más
adecuada.
Se puede implementar aplicaciones tolerantes a fallos.
Configuración de PVM
Hay dos formas de iniciar PVM:
A). Con la consola, llamada pvm.
Este programa inicia pvmd3 si no está ya corriendo y puede ser invocado un número
arbitrario de veces en cualquiera de los ordenadores que forman la máquina virtual. Este
programa permite añadir y quitar ordenadores de la máquina virtual, así como el inicio y
terminación de procesos PVM con los comandos:
add: Añade uno o más hosts.
conf:Muestra la configuración de la máquina virtual( hostnames, task ,ID, ....).
halt: Para la ejecución de todos los procesos y sale de PVM.
spawn: Inicia un proceso PVM.
help: Ayuda para los comandos interactivos.
delete: Elimina uno o más hosts.
61
Paralelización
ps: Muestra el estado de la aplicación; con el flag -a , muestra todas las
aplicaciones.
quit: Sale de la consola sin terminar el proceso pvmd3.
kill: Termina el proceso PVM especificado.
B). Mediante pvmd3.
Acepta como argumento opcional un fichero de configuración en el que se describen las
características que PVM necesita conocer de cada sistema que vaya a formar parte de
la máquina virtual. El formato del fichero de configuración, que puede tener cualquier
nombre, son líneas de texto con el nombre del computador y tras él las siguientes
opciones:
lo=userid Permite especificar un login alternativo para este ordenador. Si no se
usa esta opción, se utilizará el login empleado en la máquina donde se inició la
ejecución de PVM.
dx= localización_de_pvmd Permite al usuario especificar para este computador
una ruta distinta de la establecida por defecto para encontrar pvmd3. (Esta ruta
debe incluir el nombre del programa pvmd3).
ep= path_a_los_ ejecutables. Con esta opción se puede indicar una serie de
paths para buscar los ficheros que se requieran ejecutar en este sistema.
Distintos paths están separados por un punto y coma. Si no se especifica ep,
PVM busca las tareas de aplicación en el directorio $HOME/pvm3/bin/ARCH,
siendo ARCH el nombre de la arquitectura considerada.
Las líneas en blanco se ignoran, y aquellas que comiences con # son líneas de
comentarios. Un & antes del nombre de un host indica que no se añadirá inicalmente
pero que se podrá añadir desde consola con las opciones especificadas en el fichero.
NOTA: La consola PVM también admite un hostfile como argumento opcional
Cuando usamos PVM es necesario codificar un programa maestro y otro esclavo.
Programa Maestro:
/* PVM_Masterpi.c : Programa PVM maestro que distribuye los intervalos entre los esclavos y recolecta los
resultados enviados por los esclavos y los suma para obtener el valor de pi. Se hace uso de una cantidad variable de
procesadores: 1, 2, 4, 6, 8, 10*/
#include <stdio.h>
#include "/usr/array/PVM/include/pvm3.h”
#define NPROC 10
/* son 10 procesadores disponibles */
#define NDATOS 1000000
/* numero de intervalos */
void main(int argc, char **argv)
{
int *a;
/* variable que toma los intervalos del 1 al 1000 000 */
double *cada_pi; /* variable que toma el valor calculado de pi de cada tarea*/
int mitid, tid, i, j, tids[NPROC];
int pedazo;
double total_pi = 0.0;
mitid = pvm_mytid();
pedazo = NDATOS/NPROC;
a = (int *)malloc(NDATOS*sizeof(int));
62
Paralelización
cada_pi = (double *)malloc(NPROC*sizeof(double));
if (a == NULL || cada_pi == NULL) {
printf("Error al abrir memoria");
pvm_exit();
exit(0);
}
for ( i = 0; i < NDATOS; i++) /* se llena el arrreglo con valores del 1 a X cantidad.. */
a[i] = i;
/* Se crean N tareas esclavas */
i=pvm_spawn("/usr/inv/orgr/parall",(char**)0,0,"",NPROC,tids);
if(i!= NPROC) {
printf("Error al crear tareas \n");
pvm_exit();
exit(0);
}
/* Se envían los datos a cada proceso esclavo */
for (i=0; i<NPROC; i++)
{
pvm_initsend(PvmDataRaw); /* Enviar sin codificar */
pvm_pkint(&pedazo,1,1); /* Tamaño del intervalo */
pvm_pkint(&a[i*pedazo],pedazo,1); /* Envió del arreglo */
pvm_send(tids[i],tids[i]);
}
/* Recibe los resultados de todos los procesos esclavos */
for ( i = 0; i < NPROC; i++)
{
pvm_recv(-1,1); /* Recibe de quien sea con tag 1 */
pvm_upkint(&tid,1,1); /* Desempaca que proceso lo envia*/
for(j=0; j<NPROC; j++) /* Busca el renglon segun el */
if(tids[j]==tid) /* proceso que lo envia */
break;
/* Desempaca el valor calculado de cada proceso */
pvm_upkdouble(&cada_pi[j],1,1);
}
}
/*Obtiene el valor de pi sumando todos los intervalos calculados */
for(i=0; i<NPROC; i++) total_pi = total_pi + cada_pi[i];
printf("\nEl valor de pi = %lf \n",total_pi);
Programa esclavo:
/* PVM_Esclavopi.c: Programa PVM esclavo que calcula el valor de pi usando aproximaciones rectangulares de la
función * f(x) = 4/(1+x*x) */
#include <stdio.h>
#include "/usr/array/PVM/include/pvm3.h"
#define NDATOS 1000000 /* Numero de intervalos */
main()
{
int *a, inicio, fin;
double total_pi = 0.0;
63
Paralelización
}
double ancho, x;
int i, maestro;
int mitid, datos;
mitid = pvm_mytid(); /* Obtener el ID de esta tarea */
maestro = pvm_parent(); /* Encuentra el id del proceso maestro */
pvm_recv(maestro, mitid); /* Recibe del maestro con tag = mi tid */
pvm_upkint(&datos,1,1); /* Desempaca el Tamanio del arreglo */
a=(int *)malloc(datos*sizeof(int)); /* abre memoria para datos */
if(a == NULL)
{
printf("Error al abrir memoria");
pvm_exit();
exit(0);
}
pvm_upkint(a, datos, 1); /* Desempaca el pedazo de datos en el arreglo a */
inicio = a[0]; /* Numero inicial del intervalo o pedazo */
fin = a[datos-1]; /* Numero final del pedazo */
ancho = 1.0 / NDATOS; /* peso de cada iteracion */
for ( i = inicio; i < fin; i++ )
{
x = ((i - 0.5) * ancho); /* calcula x */
total_pi = total_pi + (4.0/(1.0 + x*x)); /* suma los valores de f(x) */
}
total_pi = total_pi * ancho; /* integra la curva */
/* Envia el resultado al proceso maestro */
pvm_initsend(PvmDataRaw); /* Envio sin codificar */
pvm_pkint(&mitid,1,1); /* Empaca el identificador del esclavo */
pvm_pkdouble(&total_pi,1,1); /* Empaca el resultado */
pvm_send(maestro,1); /* Envia al maestro con tag = 1 */
pvm_exit(); /* Salir de PVM */
Compilación:
Para llevar a cabo una correcta compilación en PVM, son necesarias variables de ambiente.
a) Definir la arquitectura del procesador. % setenv PVM_ARCH SGIMP64
b) Definir la trayectoria de las bibliotecas de PVM. % setenv PVM_ROOT /usr/array/PVM
c) Actualizar la variable PATH.
% setenv PATH ${PATH}:${PVM_ROOT}/lib/$PVM_ARCH
Nota: Para establecer estas variables de ambiente en cada nueva sesión, es recomendable
modificar el archivo:
.cshrc si se utiliza csh o cts., o
el archivo de configuración .login si se usa sh o bash
O bien, Adicionar el siguiente script para la “Configuración del ambiente de PVM”
# Para PVM
if ( ! $?MANPATH ) then
setenv MANPATH /usr/share/catman
endif
setenv PVM_ROOT /usr/array/PVM
setenv PVM_ARCH `$PVM_ROOT/lib/pvmgetarch`
setenv MANPATH $PVM_ROOT/man:$MANPATH
setenv XPVM_ROOT $PVM_ROOT/xpvm
set path=( $PVM_ROOT/lib $PVM_ROOT/lib/$PVM_ARCH
64
Paralelización
$PVM_ROOT/bin/$PVM_ARCH $XPVM_ROOT $path )
if ( ! $?PVM_ROOT ) then
setenv PVM_ROOT /usr/array/PVM
# setenv PVM_ROOT /usr/array/PVM.290999
setenv PVM_ARCH `$PVM_ROOT/lib/pvmgetarch`
# setenv PVM_ARCH SGIMP64
setenv MANPATH $PVM_ROOT/man:$MANPATH
setenv XPVM_ROOT $PVM_ROOT/xpvm
set path=( $PVM_ROOT/lib $PVM_ROOT/lib/$PVM_ARCH
$PVM_ROOT/bin/$PVM_ARCH $XPVM_ROOT $path )
endif
d) Compilar el programa esclavo:
% cc -o PVM_Esclavopi PVM_Esclavopi.c -64 -lpvm3
e) Compilar el programa maestro:
% cc -o PVM_Masterpi PVM_Masterpi.c -64 -lpvm3
f) Activar el demonio de PVM.
% pvmd3 &
Ejecución (con 10 procesos esclavos). Solo ejecutar el maestro:
% PVM_Masterpi
IV.6 Comunicación con SHMEM (shmem_pi.c)
Shmem es un conjunto de rutinas de envío de datos similar a las rutinas de envío de mensajes
de MPI o PVM. Así como las rutinas de envío de mensajes, las rutinas de shmem envían datos
entre procesos paralelos.
Las rutinas de shmem se usan en programas que realizan cálculos en espacios de direcciones
separadas y que explícitamente envían datos a y desde diferentes procesos en el programa.
Dichos procesos son llamados elementos de procesamiento (denotados como PEs).
Shmem minimiza el overhead asociado con los requerimientos de envío de datos, maximiza el
ancho de banda y minimiza la latencia de los datos. Por latencia de los datos debemos entender
el periodo de tiempo que inicia cuando un PE inicia una transferencia de datos y termina cuando
un PE puede usar el dato.
Soporta transferencia remota de datos a través de operaciones put, las cuales transfieren datos
a distintos PEs, y operaciones get, que transfieren datos desde un PE. Algunas otras
operaciones soportadas son: trabajo compartido, difusión y reducción, sincronización de
barreras y operaciones atómicas de memoria. Una operación atómica de memoria es una
operación atómica de lectura y actualización, tal como un fetch e incremento, sobre un dato
local o remoto. El valor leído garantiza ser el valor del dato previo a la actualización.
Rutinas de SHMEM pueden ser usadas junto con rutinas de MPI en el mismo programa. Estos
programas deben llamar MPI_Init y MPI_Finalize pero omitir la llamada a la rutina start_pes,
pues el número de PEs será igual a la cantidad de procesos indicados en MPI.
Compilación de programas en Shmem
Las rutinas de Shmem residen en libsma.so.
Sistemas IRIX:
cc -64 c_program.c -lsma
65
Paralelización
CC -64 cplusplus_program.c -lsma
f90 -64 -LANG:recursive=on fortran_program.f -lsma
f77 -64 -LANG:recursive=on fortran_program.f -lsma
Sistemas IRIX con Fortran 90 version 7.2.1:
f90 -64 -LANG:recursive=on -auto_use shmem_interface fortran_program.f -lsma
Nota: El software Shmem esta empaquetado con el “Message Passing Toolkit”.
Ejemplos:
El siguiente programa en C corre en IRIX:
#include <mpp/shmem.h>
main()
{
}
long source[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
static long target[10];
start_pes(0);
if ( _my_pe() == 0 )
{
/* coloca 10 palabras en el “target” sobre el PE 1 */
shmem_long_put(target, source, 10, 1);
}
shmem_barrier_all(); /* sincroniza emisor y receptor */
if ( _my_pe() == 1 )
shmem_udcflush();
/* necesario en T90 */
printf("target[0] on PE %d is %d\n", _my_pe(), target[0]);
Este programa en C, realiza lo siguiente:
El PE 0 envía 10 enteros al arreglo “target” del PE 1.
Para ejecutar el programa bastará con 2 PEs y usamos la siguiente variable:
env NPES=2 a.out
IV.7 Empleando directivas de PCA (Power_pi.c)
El uso de directivas del compilador de multiprocesamiento ofrece dos formas de emplearlas,
manual o automática. Usando el analizador automático de PCA se genera un archivo *.list que
proporciona la paralelización para cada ciclo, mediante la instrucción:
Compilación:
% cc -o pi -pca list pi.c
El archivo pi.list contiene la información siguiente:
66
Paralelización
Al compilarlo del modo % cc -o pi -pca -mplist pi.c, se generan dos archivos transformados,
nombrados: pi.w2c.c y pi.w2c.h. Cada uno de ellos contiene la información siguiente:
pi.w2c.c
pi.w2c.h
67
Paralelización
/* POWER_pi.c : Calcula el valor de pi, usando una aproximación rectangular a la integral de la funcion f(x) =
4/(1+x*x) */
#include <stdio.h>
#define MAXRECT 100000000
double pieza_de_pi(int);
/* Numero de intervalos */
main(int argc, char *argv[])
{
double pi_valor; int nrects = MAXRECT; /* Numero de rectangulos*/
pi_valor = pieza_de_pi(nrects);
printf("El valor de pi es = %f \n", pi_valor);
}
double pieza_de_pi (int nrects)
{
double ancho; /* ancho del rectangulo de aproximación*/
double x;
/* valor de x */
int i;
/* contador */
double parte_pi, total_pi = 0.0;
ancho = 1.0 / nrects; /* peso de cada iteracion */
parte_pi = 0.0;
#pragma parallel local(i, x, parte_pi) shared(total_pi) byvalue(nrects)
{
#pragma pfor iterate(i=0; nrects; 1)
for ( i = 0; i < nrects; i++ )
{
x = ((i - 0.5) * ancho);
/* calcula x */
parte_pi = parte_pi + (4.0/(1.0 + x*x));
/* suma los valores de f(x) */
}
#pragma critical
68
Paralelización
}
total_pi = total_pi + parte_pi;
}
total_pi = total_pi * ancho;
/* integra la curva */
return(total_pi);
Compilación:
% cc –o Power_pi Power_pi.c -mp
Salida (con varios procesadores) % ssusage ./Power_pi
IV.7 Comparativo de los modelos de programación paralela
Realizamos un comparativo de resultados para el cálculo del valor de pi, para tener una mejor
perspectiva del desempeño del programa con cada paradigma empleado. (Resultados en
segundos). Se utilizó una aproximación rectangular a la integral de la función f(x) = 4/(1+x*x)
con 100000000 intervalos en dicha aproximación.
Usando ./ssusage y se considero el tiempo real, pues según el manual de este comando el
tiempo real corresponde al tiempo de pared del sistema. La información obtenida esta en
segundos.
Estas pruebas se realizaron varias veces para cada uno de los casos mostrados, y se considero
el promedio de cada uno de ellos.
Paradigma /
#Procesadores
1
2
4
6
8
10
Secuencial
Procesos Unix
Hilos POSIX
MPI
PVM
PCA
26.26
38.31
26.24
27.46
20.15
18.11
14.44
8.6
10.10
8.00
5.28
6.88
5.87
5.65
4.70
32.46
19.7
10.69
7.00
7.7
Comportamiento del rendimiento del programa de acuerdo a los paradigmas mostrados:
69
Paralelización
Tabla de SpeedUp para cada paradigma de programación paralela mostrado
SpeedUp /
#Procesadores
Secuencial
Procesos Unix
Hilos POSIX
MPI
PVM
PCA
1
2
4
6
8
10
1
1
1
1
1.9012
1.448
1.902
4.4546
2.5980
3.4325
7.255
3.8139
4.678
4.644
5.5829
1
1.647
3.0364
4.637
4.2155
70
Paralelización
71
Paralelización
Glosario
Algoritmo
Conjunto de sentencias / instrucciones en lenguaje nativo, los cuales expresan la lógica
de un programa.
Algoritmo cualitativo, Son aquellos que resolver un problema no ejecuta operaciones
matemática en el desarrollo de algoritmo.
Algoritmo cuantitativo, Son aquellos algoritmos que ejecutan operaciones numéricas
durante su jecución.
Archivo
Conjunto de datos relacionados entre sí que se tratan como una unidad.
Arena
Archivo de intercambio de memoria propio de SGI, usado para compartir memoria y
herramientas de sincronización entre procesos.
Barrera
Herramienta de sincronización. Todos los procesos esperaran hasta que el ultimo
proceso llegue, y proceder a continuar simultáneamente.
Biblioteca de Programas
Conjunto de programas y rutinas ampliamente usados en diversa aplicaciones y
compactados para su fácil uso.
Buffer
Memoria intermedia, una porción reservada de la memoria, que se utiliza para
almacenar datos mientras son procesados.
C
Lenguaje de programación de alto nivel, de aplicación general que además permite a los
usuarios escribir instrucciones de máquina de bajo nivel que se acercan en densidad y
eficiencia a las que se codifican en lenguaje de máquina.
Cluster
Conjunto de PC's (granja), interconectadas por un dispositivo de red de alta velocidad
para efectuar procesamiento en paralelo.
Código fuente
Programa en su forma original, tal y como fue escrito por el programador, el código
fuente no es ejecutable directamente por el computador, debe convertirse en lenguaje de
maquina mediante compiladores, ensambladores o interpretes.
Compilador
Programa de computadora que produce un programa en lenguaje de maquina, desde un
programa fuente.
Concurrente
Código corriendo en paralelo.
Dependencia de datos
72
Paralelización
Es un constructor en el programa el cual hace que el resultado dependa del orden de
ejecución de un ciclo o hilo, esto se refleja en los índices de los contadores de ciclos.
Directiva
Comentario especial que se inserta en el código fuente, para dar información al
compilador o al analizador y así poder explotar otras características del equipo.
Granularidad
Es el tamaño de la pieza de código que se ejecuta en un procesador y es la unidad de
ejecución de un código paralelo.
Interfaz
Una conexión e interacción entre hardware, software y usuario, es decir como la
plataforma o medio de comunicación entre usuario o programa.
IRIX
Sistema operativo de multiprocesamiento y multiusuario basado en UNIX System V
Release 4 propio de SGI.
Kernel
Núcleo del sistema operativo, encargado de administrar los recursos de la máquina.
Lógica
Es una secuencia de operaciones realizadas por el hardware o por el software.
Lógica del hardware: Circuitos y Chips que realizan las operaciones de control de la
computadora.
Lógica del software o lógica del programa: Secuencia de instrucciones en un programa.
MPI (Message Passing Interface)
Conjunto de rutinas estándares para la comunicación entre procesadores de
multicomputadoras o "clusters" usando el modelo de envío de mensajes, también se le
conoce como Interface de Envío de Mensajes.
Llamada
Transferencia del control de un programa principal, rutina o función a una función o
rutina.
NUMA (Non-Uniform Memory Access)
Diseño de memoria de una computadora, la cual se encuentra distribuida en varios
nodos, cuyos accesos son no uniformes, varían de acuerdo a la ubicación de la memoria
respecto al CPU, por cuántos hubs y/o enrutadores deben pasar los datos. Esta
arquitectura puede tener diferentes topologías de interconexión, por ejemplo Hipercubo
como en el caso del sistema Origin 2000, también se le conoce como sistemas de
Acceso No-Unifome a la Memoria.
POSIX (Portable Operating System Interface)
Conjunto de interfaces estándares del sistema operativo basado en UNIX. Con la
finalidad de lograr la portabilidad del código sobre diversas arquitecturas sin la
necesidad de ser re-codificado.
73
Paralelización
PCA
Producto de SGI que incluye el Analizador de C, el compilador de multiprocesamiento en
C y las rutinas de multiprocesamiento.
Procesamiento Paralelo
Es una forma eficiente de procesar la información con énfasis en la explotación de
eventos concurrentes en el proceso de cómputo. Este concepto implica: paralelismo,
simultaneidad y pipelining.
PVM (Parallel Virtual Machine)
Es un conjunto de bibliotecas que permiten realizar cómputo paralelo en una red de
computadoras de la misma o diferente arquitectura basado en el modelo de envío de
mensajes, también se le conoce como Máquina Virtual Paralela.
Programa
1) Plan para obtener la solución a un problema;
2) Conjunto de instrucciones en secuencia que hacen que la computadora lleve a cabo
determinadas acciones.
Programa Fuente: Programa escrito en algún lenguaje de alto nivel como C, Fortran, etc.
Programa Objeto: Programa completamente compilado o ensamblado que está listo para
cargarse en la computadora.
Solaris
Sistema operativo UNIX basado en el System V Release 4 y del BSD 4.2 de SUN
Corporation.
74
Paralelización
Bibliografia
IRIS Power C User's Guide. SGI.
Parallel Programming Student Handbook. SGI. September 1996.
Parallel Programming for Everyone
http://csdocs.cs.nyu.edu:80/Dienst/UI/2.0/Describe/ncstrl.nyu_cs/TR1999779?abstract=Parallel+Programming
Introduction to Parallel Processing on SGI Shared Memory Computers
http://scv.bu.edu/SCV/Tutorials/SMP/smp.html
Designing and Building Parallel Programs
http://www-unix.mcs.anl.gov/dbpp/text/book.html
SGI TechPubs Library Display (auto_p.z)
http://techpubs.sgi.com/library
Distributed and Parallel Computing
http://www.manning.com/El-Rewini/Intro.html
Parallel Computing CSCI 6356
http://www.cs.panam.edu/~meng/Course/CS6356/Notes/master/master.html
Parallel Programming with the Origin2000: Introduction
http://www-jics.cs.utk.edu/origin2000/labO2K/
75
Descargar