UNIVERSIDAD NACIONAL AUTÓNOMA DE MÉXICO POSGRADO EN CIENCIA E INGENIERÍA DE LA COMPUTACIÓN “ESTUDIO DE SISTEMAS MULTICORE PARA CÓMPUTO DE ALTO RENDIMIENTO” E T S I S QUE PARA OBTENER EL GRADO DE: MAESTRA EN CIENCIAS (COMPUTACIÓN) P R E S E N T A: CLARA VICENTE HERNÁNDEZ DIRECTOR DE TESIS: HÉCTOR BENÍTEZ PÉREZ. México, D.F. 2009. 0 ÍNDICE: CAPÍTULO 1 INTRODUCCIÓN 1.1 Introducción. 1 1.2 Caso de estudio. 3 1.3 Objetivo general. 4 1.4 Metas. 5 1.5 Contribución y Relevancia. 5 CAPÍTULO 2 GENERALIDADES 2.1 Introducción al capítulo. 7 2.2 Sistemas con memoria compartida. 7 2.3 Sistemas con memoria distribuida. 12 2.4 Características de las arquitecturas referidas. 16 2.5 Sistema Multicore. 17 2.6 Métricas de desempeño en multiprocesamiento. 17 2.7 Ley de Amdahl 19 2.7 Resumen de capítulo. 21 CAPÍTULO 3 ARQUITECTURA DEL SISTEMA 3.1 Introducción al capítulo. 23 3.2 Descripción de la arquitectura de hardware. 23 3.3 Descripción de la arquitectura de software. 25 3.4 OpenMP. 29 3.5 Nivel de paralelismo. 30 3.6 Directivas y construcciones de sincronización. 32 1 3.7 MPI. 33 3.8 Llamadas utilizadas para iniciar, administrar y finalizar comunicaciones 34 3.9 Llamadas utilizadas para transferir datos entre dos procesos. 35 3.10 Llamadas utilizadas para transferir datos entre varios procesos. 36 3.11 Ventajas. 38 3.12 Desventajas. 38 3.13 Resumen de capítulo. 39 CAPÍTULO 4 CASO DE ESTUDIO 4.1 Introducción al capítulo. 41 4.2 Descripción de la red neuronal. 41 4.3 Caso de estudio 1 Implementación híbrida. 45 4.4 Caso de estudio 2 Implementación en sistemas multicore. 53 4.5 Caso de estudio 3 Implementación en serie. 56 4.6 Resumen de de capitulo. CAPÍTULO 5 58 MÉTRICAS Y ANÁLISIS DE RESULTADOS 5.1 Introducción al capítulo. 60 5.2 Aplicación de métricas de desempeño en sistemas multicore 60 5.3 Conclusión de capítulo. 70 CAPÍTULO 6 CONCLUSIONES 72 APÉNDICE. Código Hibrido. Utilizando las interfaces de programación MPI y OpenMP. 74 Código serial y/o Paralelo. 88 2 Referencias Bibliográficas 92 ÍNDICE DE FIGURAS. 1.1 Ejecución de instrucciones de manera secuencial. 2 1.2 Ejecución de instrucciones de manera simultánea sobre diferentes CPU’s 3 2.1 Arquitectura de procesadores con memoria compartida. 8 2.2 Diseño de una región paralela en sistemas con memoria compartida. 10 2.3 Arquitectura de un Sistema Distribuido. 12 2.4 Comunicación a través de sockets. 15 2.5 Caso ideal de aceleración para n procesadores. 19 3.1 Arquitectura híbrida de multiprocesamiento. 24 3.2 Sección Crítica. 27 3.3 Estados de los procesos en una sección crítica. 28 4.1 Algoritmo general de la Art. 44 4.2 Matriz de pesos de la red neuronal (Recurso compartido). 47 4.3 Paralelización de la red ART2A. 48 4.4 Vectores de dimensionalidad X valores mayores y posiciones correspondiente 49 4.5 Un ciclo completo de entrenamiento de la red ART2A en paralelo. 51 4.6 Hilos en acción. 53 4.7 Región paralela con hilos. 54 4.8 (a) Flujo de ejecución de hilos de manera paralela. 56 4.8 (b) Flujo de ejecución de hilos de manera concurrente. 56 4.9 Diagrama de flujo de la red ART2A de manera secuencial. 57 5.1 Aceleración de entrenamiento de la ART2A en 4 procesadores. 67 5.2 Tiempos de ciclo de ejecución del entrenamiento de la ART2A. 69 3 ÍNDICE DE TABLAS Tabla de resultados de entrenamiento de la red neuronal ART2A con hilos (RO=0.4) 62 Tabla de resultados de entrenamiento de la red neuronal ART2A con hilos (RO=0.1) 63 Tabla de resultados de entrenamiento de la red neuronal ART2A de 3 y 4 procesadores 66 Tabla de resultados de entrenamiento de la red neuronal ART2A de 10 procesos 67 4 CAPÍTULO 1. 1.1 INTRODUCCIÓN: Dada la necesidad de contar con máquinas de mayor potencia de proceso, hoy en día es común encontrarse con computadoras que incluyen más de un procesador en su arquitectura. El utilizar varios procesadores no es nuevo, ya que desde hace tiempo existen tarjetas madre que permiten la interacción de varios procesadores (normalmente dos, cuatro u ocho) en un solo equipo, de la misma manera como se pueden agrupar computadoras en clúster de modo que actúen como si fuera una única computadora. Lo que resulta realmente novedoso es que ahora los procesadores estén encapsulados en un único chip (procesadores denominados multicore). El motivo es principalmente económico, ya que resulta más barato producir un circuito integrado con dos microprocesadores (dos cores), que dos procesadores independientes en una tarjeta madre. Por otra parte, al construirse sistemas multicore se ahorra energía, se produce menor calentamiento y al reducirse las distancias se obtiene un mejor rendimiento (performance), que en un solo procesador de rendimiento semejante. Con un solo procesador, una aplicación exigente pone a trabajar al tope al procesador, si existen diversos cores, sólo trabajará al máximo el core que soporte esa aplicación, los otros cores irán mas desahogados. Sin embargo, con los avances tecnológicos en la integración de procesadores no solo existen cambios en su arquitectura o en su comunicación interna sino que también conllevan un cambio en nuestra manera de pensar, ya que desde el inicio de la era de la computadora (1945) a la fecha, los computadores secuenciales han sido diseñados basados en el modelo introducido por John von Newman [18, 20, 21], el cual consiste en una unidad central de proceso (CPU), una memoria, y unidades de entrada y salida, debido a ello, el software y su programación se han orientado tradicionalmente a la computación en serie o secuencial, de este modo es natural que los algoritmos y programas de computadoras fueran primeramente formulados de acuerdo a tal concepto. Aún antes de que la primera computadora electrónica fuera construida el concepto matemático de algoritmo fue definido como una secuencia finita de operaciones 1 [22]. En una máquina secuencial, el procesador ejecuta una secuencia de instrucciones y operan con una secuencia de datos (fig. 1.1). Para su procesamiento un problema es dividido en un conjunto de instrucciones, y su velocidad es limitada por dos factores: la capacidad de obtener y ejecutar dichas instrucciones y la velocidad de transferencia entre memoria y CPU. Instrucciones ... Problema N/N CPU 3/N 2/N 1/N fig. 1.1 Ejecución de instrucciones de manera secuencial. Con el desarrollo de diversas tecnologías de interconexión entre unidades (unidades funcionales redundantes, pipeline, paralelismo, hiperthreding, multicore, entre otras) y la existencia de aplicaciones (científicas, técnicas, medicas, etc.) que requieren procesar grandes cantidades de datos o realizar un alto número de operaciones, se hace necesario el empleo de múltiples procesadores que cooperen en su ejecución, de manera que cada aplicación se divide en un número arbitrario de subprogramas (subconjuntos de instrucciones, o tareas independientes), con un fin determinado y estos a su vez pueden ejecutarse simultáneamente sobre diferentes procesadores en el mismo instante de tiempo[19] (fig. 1.2 ). Los procesadores se comunican unos con otros para intercambiar y combinar los resultados parciales obtenidos durante su ejecución. A este modelo tecnológico de procesamiento alternativo se le denomina cómputo en paralelo [22,3] y pretende conseguir que N procesadores trabajen de forma conjunta para resolver un problema N veces más rápido que en un único procesador [3]. En lugar de procesar instrucción por instrucción como en una computadora tradicional, en un sistema de procesamiento paralelo varias instrucciones se ejecutan simultáneamente, con el propósito de acelerar la velocidad de procesamiento y aumentar así la eficiencia de cómputo [19,3]. No obstante, se debe considerar que el uso de múltiples procesadores en 2 un mismo sistema computacional introduce requerimientos adicionales, es decir, para que varios procesadores puedan trabajar juntos ejecutando el mismo programa computacional, estos deben ser capaces de compartir datos y comunicarse cada uno con el otro, por lo que debe de haber una sincronización para así evitar condiciones de competencia. Problema Instrucciones CPU 1 TAREA 1 . . . . . . CPU 2 TAREA 2 . … . . . . . CPU n . . . TAREA n N/N 3/N 2/N 1/N fig. 1.2 Ejecución de instrucciones de manera simultánea sobre diferentes CPUs El presente trabajo está dirigido hacia el estudio y análisis de la programación paralela, a tal fin se hace uso de un clúster de computadoras, cada una de ellas con arquitectura multicore. Para la programación de estas dos arquitecturas se utiliza programación híbrida, haciendo uso de las interfaces de programación MPI Y OpenMP. MPI distribuye la carga entre las computadoras que conforman el clúster (comunicación inter procesos) y OpenMP maneja los cores en una misma máquina. Cabe hacer notar que la programación de estas dos arquitecturas es completamente diferente y requiere de conceptos con dos visiones diferentes (se explicaran en capitulo 2). 2 1.2 CASO DE ESTUDIO. El caso de estudio es el entrenamiento de una red neuronal llamada ART2A. Es una red de entrenamiento no supervisado, cuya estructura consiste de dos redes neuronales llamada ART [15, 30]. Su objetivo es la detección, localización y clasificación de 3 defectos y fallas internas. La información proporcionada a la red para su entrenamiento, es a través de una base de datos que contiene información suficiente de cada tipo de defecto. La forma de entrenamiento es comparar las entradas presentes con categorías aprendidas seleccionadas. Las categorías aprendidas se encuentran almacenadas en una estructura multidimensional matricial llamada W [15], que es compartida por diferentes procesos. Esta es actualizada o modificada de acuerdo a un parámetro de vigilancia (ρ), que expresa el grado mínimo de similitud entre el vector de entrada y cualquier categoría definida en W. Si el grado de similitud entre la entrada y cualquier categoría es mayor o igual que ρ, entonces el prototipo actual es elegido para representar el grupo contenido. Esto dice que la red logra resonancia. Si el grado de similitud es menor que ρ entonces otra categoría debe ser seleccionada y comparada con el vector I (es el vector normalizado bajo la norma Euclidiana de los datos a ser estudiados). Si cualquier prototipo o categoría es alcanzado por el grado de similitud entonces el vector I debe ser agregado a W como un nuevo prototipo. 1.3 OBJETIVO GENERAL. El objetivo de este trabajo es estudiar el desempeño de sistemas con arquitectura multicore, con base en requerimientos de acceso restringido en recursos comunes a los procesadores, usando sistemas paralelos a través de una red de cómputo. Se requiere distribuir el procedimiento de entrenamiento de una red neuronal de tipo ART2A [15] en una arquitectura de multiprocesamiento que consta de N máquinas con características Multicore. El sistema está diseñado para N procesos 1 que comparten una Red Neuronal ART2A. Este recurso debe ser actualizado o modificado dependiendo de los resultados del entrenamiento de la propia red en cada uno de los procesos. Para medir el desempeño de un programa ejecutándose en este tipo de arquitecturas se hace uso de la Ley de Amdahl para sistemas distribuidos [23] y el Vtune (analizador de rendimiento) en sistemas multicore. 1 Proceso. Un proceso se define como un programa en ejecución. 4 1.4 METAS. Desarrollar un sistema de doble multicore con base a una red tipo Ethernet y dos sistemas Multicore. Esto es conectando dos máquinas con arquitectura core a través de una red de computadoras. Crear un algoritmo paralelo híbrido en lenguaje de programación C++, en el que se implementarán las librerías de MPI y OpenMp. Distribuir la carga de trabajo de manera equitativa entre los procesadores tomando en cuenta el acceso a memoria de estos (multiprocesadores o multicomputadores). Evaluar el desempeño de estos sistemas así como determinar potenciales condiciones de competencia entre los procesos. 1.4 CONTRIBUCIÓN Y RELEVANCIA. En el desarrollo de la presente tesis se abordó el problema de paralelización del método de entrenamiento no supervisado ART2A para redes neuronales [15, 30]. Como es bien sabido este tipo de entrenamiento tiene importantes aplicaciones, por ejemplo, en teoría del control, robótica, reconocimiento de patrones, entre otras. El enfoque con el que se abordó este problema fue mediante el uso de multiprocesamiento híbrido en el que se emplean los esquemas de paralelización de memoria compartida (SMP) [3,5] y paso de mensajes en arquitectura distribuida (MPI)[13]. Para determinar el rendimiento de ejecución del programa híbrido en múltiples procesadores se utilizaron funciones de métricas de tiempo proporcionadas en las librerías del lenguaje de programación C++ [1]. 5 Las afirmaciones que podemos deducir de los resultados obtenidos en los experimentos realizados con la implementación mencionada son los siguientes: 1. En el algoritmo ART2A existe una gran cantidad de dependencia de datos inherentes al mecanismo de aprendizaje en el entrenamiento. Esto induce al hecho de que en la paralelización de dicho algoritmo siempre exista una porción de código no paralelizable, que aunque se encontró como una fracción pequeña, esta porción nos impide escalar el procedimiento de paralelización a un número arbitrario de procesadores [11, 23]. Tenemos entonces que el uso de varios procesadores y procesos no es garantía de un manejo eficiente en el procesamiento de la información. De hecho el análisis de resultados obtenidos durante la ejecución de los programas en donde empleamos ambas arquitecturas (explicadas en las secciones 1.2 y 1.3) determinará la mejor aproximación para la paralelización en cuanto al flujo propio de la información. 2. Cuando la dimensión de los vectores de entrenamiento es alta, entonces la dependencia de datos en las operaciones internas del procedimiento ART2A disminuye, por ejemplo, la multiplicación de matriz por vector, producto interno, y normalización de vectores, pueden entonces descomponerse en procesos paralelos obteniendo así un mejor rendimiento que en el caso secuencial. Remarcamos el hecho de que esto ocurre cuando la dimensión de los vectores es alta, pues de otra manera, el tiempo procesamiento de las instrucciones adicionales para conseguir paralelismo superará al tiempo ganado por el proceso de paralelización. En resumen, pudimos observar en los experimentos, que aunque es posible paralelizar el procedimiento ART2A es necesario verificar si la dimensionalidad de los vectores de entrenamiento, así como también el numero de vectores clasificados en W. La dimensión de estos nos permitirá mejorar o no el rendimiento del proceso. En resumen: el tamaño de los datos a paralelizar es un factor muy importante a considerar en la programación paralela. 6 CAPÍTULO 2. GENERALIDADES: 2.1 INTRODUCCIÓN AL CAPÍTULO. En este capítulo se describen las características de las tecnologías empleadas para abordar el cómputo paralelo, la forma en que se comunican e intercambian información, así como también la manera de sincronización en ambos arquitecturas. Se explica lo que es un sistema multicore y algunas de las métricas de desempeño para evaluar el rendimiento de un programa al ejecutarlo en múltiples procesadores. Para implementar el modelo de cómputo paralelo se consideran dos tipos de arquitecturas ampliamente utilizadas, las cuales difieren en la forma en la que se comunican los procesadores [22, 6]: Sistemas con memoria compartida: En una aplicación paralela lo que se pretende es ejecutar un programa más rápidamente que en una versión secuencial, para ello lo que se hace es utilizar varios procesos, que ejecutan partes de un mismo programa, de tal manera que el tiempo empleado en la ejecución paralela de todos ellos sea menor que el tiempo empleado por un único proceso en resolver el problema [18]. Sistemas con memoria distribuida): En una aplicación distribuida existen diferentes procesos, que ejecutan distintas partes de un programa, dichos procesos colaboran y se comunican entre sí para alcanzar un mismo objetivo. 2.2 SISTEMAS CON MEMORIA COMPARTIDA “MULTIPROCESADOR” Consiste en un número de procesadores idénticos dentro de una misma máquina compartiendo una memoria principal global [3,22]. (fig. 2.1). 7 MEMORIA COMPARTIDA BUS PROCESADOR PROCESADOR 1 2 . . .PROCESADOR n (fig. 2.1). Arquitectura de procesadores con memoria compartida. El acceso a la memoria principal global es a través de un bus. El bus es el canal de comunicación para llevar a cabo las operaciones de solicitud de memoria simultánea por varios procesadores y asegurar que todos los procesadores sean justamente atendidos con un mínimo retardo de acceso. El encargado de arbitrar las tareas, sus prioridades y recursos disponibles es el Sistema Operativo. Una de las mayores dificultades en sistemas de multiprocesamiento de memoria compartida es la competencia por recursos, principalmente por el acceso a memoria entre los procesadores [3]. Esta se incrementa conforme el número de procesadores sea mayor y más procesos activos se tengan en el sistema. A mayor grado de multiprogramación se presentan mayores necesidades de memoria. Cuando varios procesadores intentan accesar a la memoria compartida dentro del un periodo de tiempo pequeño, la memoria no le es posible atender todas las peticiones simultáneamente, por lo que algunos de los procesadores tendrán que esperar mientras otros son atendidos [3, 6, 21]. La programación eficiente en este tipo de arquitecturas se realiza mediante el control de hilos paralelos. Un hilo (thread) es un flujo de control independiente dentro de un proceso. Es esencialmente una ruta de ejecución a través del código de programa en proceso (fig. 2.2). Los hilos dentro de un mismo proceso suelen compartir los recursos asignados al proceso al cual pertenecen. La manera de identificar un hilo dentro de un proceso es por su identificador (ID) que es un valor entero único dentro de cada proceso. 8 Dos hilos en diferentes procesos pueden tener el mismo número de ID. Sin embargo, cada uno realizará operaciones diferentes. Con procesadores multicore se reparten los diferentes hilos entre los cores, de modo que varias aplicaciones avanzan a la vez, permaneciendo menos tiempo en espera, logrando con ello agilizar la ejecución de una tarea [3,22]. Debido a que los hilos dentro de un mismo proceso comparten las mismas direcciones físicas de memoria, estos comparten las mismas estructuras y datos a nivel proceso, sin embargo, cada hilo tiene información propia que no comparte con otros hilos. Esta información se refiere fundamentalmente al contexto de ejecución, pudiéndose destacar los siguientes datos: Contador de programa. Tope de pila. Estado de registros. Estado del hilo (thread). El estado de un hilo (listo, activo, bloqueado) en cualquier instante está definido por el proceso en memoria. Cada procesador ejecuta un hilo diferente, aunque cualquier hilo podría estar corriendo en cualquier procesador en cualquier momento. En un sistema de dos procesadores cada procesador puede estar corriendo diversos hilos, los cuales pueden pertenecer al mismo proceso o a diferentes procesos. Un hilo o proceso no está atado o ligado a un procesador particular. Esto lo determina el Sistema Operativo. Para un uso eficiente en esta arquitectura se propone el empleo de múltiples hilos (multithreads MT), ya que si se tiene un solo hilo esperando por los recursos del sistema no se hace uso adecuado del hardware. Si el hilo tiene que hacer cálculos extensivos entonces todas las demás aplicaciones tendrían que esperar. El usar multihilos puede remover los cuellos de botella. Un programa MT comienza con un hilo inicial, denominado hilo maestro. Este hilo inicial crea nuevos hilos, que son considerados hilos esclavos. (fig 2.2) 9 Región Paralela Hilo Maestro Hilo Maestro Fig. 2.2 Diseño de una región paralela. El hilo maestro crea hilos esclavos Los nuevos hilos corren una rutina independiente y suministran otras corrientes de instrucciones operativas en el mismo espacio de direccionamiento de datos. El Sistema Operativo se ocupa de asegurar que cada hilo se mantenga activo. Esto lo hace con la asistencia del hardware y una gran cantidad de registros de transacciones. Cuando el temporizador del hardware decide que un proceso ha estado ejecutándose por suficiente tiempo entonces ocurre una interrupción2. El Sistema Operativo salva el estado actual del hilo mediante la copia de los registros del hilo desde la pila hacia una estructura de contexto y la guarda para su uso posterior. Para cambiar hacia diferentes hilos el Sistema Operativo direcciona el procesador a la memoria correspondiente al hilo del proceso, luego restaura los registros que fueron guardados en la estructura de contexto en un nuevo hilo. A este proceso es llamado intercambio de contexto (context swich) [4,5,18]. No hay que olvidar que el crear hilos y hacer intercambios de contexto le toma una cantidad finita de tiempo al procesador por lo que al momento de crear hilos se debe tomar en cuenta cuantos hilos se van a manejar en la aplicación paralela y asegurar que no exista una sobrecarga en intercambio de contextos. De no tomar en cuenta esto, se puede consumir mayor tiempo con múltiples hilos al ejecutar el código. Todo esto debido al intercambio de contexto y a la introducción de instrucciones propias del modelo paralelo que no son necesarias en el programa secuencial. 2 A nivel físico, una interrupción se solicita activando una señal que llega a la unidad de control. Ante esta solicitud, la unidad de control realiza un ciclo de aceptación de interrupción. Este ciclo se lleva a cabo cuando termina la ejecución de la instrucción máquina que se está ejecutando y consiste en las siguientes operaciones: 1. Salva algunos registros del procesador, como son el de estado y el contador de programa (CP). 2. Eleva el nivel de ejecución pasándolo al núcleo 3. Carga un nuevo valor en el CP por lo que pasa a ejecutar otro programa. 10 Otro aspecto es el control de condiciones de competencia. Una condición de competencia ocurre cuando varios hilos compiten por un mismo recurso. Para prevenir dichas condiciones y evitar que los hilos sean interrumpidos mientras se encuentran haciendo alguna operación cuya integridad es indispensable en un procedimiento, es necesario protegerlos. Esto se hace mediante mecanismos de sincronización, que no son más que la manera de coordinar los hilos para que puedan trabajar en conjunto. La sincronización entre hilos se realiza mediante variables globales compartidas que pueden ser coordinadas usando secciones críticas, semáforos o mutex. Una Sección Crítica (SC) es una porción de código que accesa a un recurso compartido y que solo a un hilo a la vez se le permite entrar a ejecutar dicho código, convirtiéndose en una operación atómica 3. Cada hilo debe pedir permiso para entrar a la sección crítica, mediante algún fragmento de código que se denomina de forma genérica entrada en la sección crítica. Cuando un hilo sale de la sección crítica debe indicarlo mediante otro fragmento de código, que se denomina salida de sección critica. Mediante la utilización de Mutex (exclusión mutua) si un hilo se encuentra en la SC, ningún otro podrá entrar. Un semáforo es un objeto que permite controlar el acceso a un recurso compartido. Está compuesto de un contador y de una cola de espera. Cuando el contador es positivo, indica que existen recursos disponibles. Cuando es negativo indica la cantidad de procesos o hilos que esperan por el recurso [3, 4, 18.]. Un ejemplo de condición de competencia ocurre cuando varios hilos hacen despliegue de datos en pantalla. Debido a que el Sistema Operativo sólo le proporciona cierta cantidad de tiempo a cada hilo, los hilos compiten por el recurso. Un determinado hilo empezará a hacer su despliegue, pero al transcurrir el tiempo asignado para su ejecución, si aún no ha terminado de hacer su despliegue, el control del dispositivo de despliegue le será quitado y se transferirá a otro hilo, teniendo como resultado una potencial inconsistencia en los datos desplegados. 3 Una operación atómica es una porción de código que no puede ser interrumpida mientras esta en ejecución. 11 2.3 SISTEMAS CON MEMORIA DISTRIBUIDA “MULTICOMPUTADORA”. Un sistema Distribuido se define como un grupo de computadoras separadas físicamente.Cada máquina tiene su propia memoria y sólo se comunican entre ellas por medio de mensajes a través de una red de computadoras [3,18]. Cada una de las computadoras conectadas a la red posee sus componentes de hardware y software que el usuario percibe como un solo sistema. (fig. 2.3), P P P M M M PASE DE MENSAJES COMUNICACIÓN A TRAVÉS DE LA RED P P M M P M fig. 2.3 Arquitectura de un Sistema Distribuido. Cada máquina tiene su propio procesador (P) y memoria (M) y se comunican por medio de mensajes a través de una red de computadoras. 12 Un Sistema Distribuido se define según dos puntos de vista: 1. Desde un punto de vista físico, un sistema distribuido es un conjunto de procesadores (posiblemente heterogéneos), sin memoria ni reloj común, que se encuentran conectados a través de una red de interconexión. fig. 1.4 [3, 21, 22]. 2. Desde el punto de vista lógico, un sistema distribuido es un conjunto de procesos que no comparten recursos y que se ejecutan en una o más computadoras y que colaboran y se comunican entre ellos mediante el intercambio de mensajes. De estos dos puntos de vista se desprende que uno de los aspectos más importantes de un Sistema Distribuido es comunicar diferentes procesos entre sí con independencia del lugar donde se ejecuten. La plataforma fundamental sobre la que se construyen los sistemas distribuidos es una red de computadoras, lo cual permite que puedan compartir diferentes recursos como son: periféricos, datos o incluso procesadores. Una red de computadoras es un sistema de comunicación compuesto por una serie de componentes hardware y software que proporcionan los servicios necesarios para que los procesos que ejecutan en las distintas computadoras puedan comunicarse entre sí. En este tipo de arquitectura, puesto que los diferentes procesadores no comparten una memoria común, la única forma de comunicación posible es mediante el intercambio de mensajes. Un mensaje es un objeto lógico que se intercambia entre dos o más procesos. Para transmitir un mensaje a través de una red de interconexión es necesario descomponerlo en una serie de paquetes. Un paquete es la unidad de información que se intercambian dos dispositivos de comunicación. Para que la comunicación se lleve a cabo, es necesario el empleo de protocolos de comunicación. Un protocolo es un conjunto de reglas e instrucciones que gobiernan el intercambio de paquetes y mensajes. Utilizando pase de mensajes como mecanismo de comunicación entre procesos, únicamente existir un enlace de comunicación entre ellos. Los procesos se comunican mediante dos operaciones básicas: 13 send(destino, mensaje): envía un mensaje al proceso destino. receive(origen, mensaje): recibe un mensaje del proceso origen. La comunicación entre los procesos es de manera directa o indirecta, es decir, deben tener una forma de referirse a cada uno de ellos. Sin embargo, cualquiera que sea el método utilizado, el paso de mensajes siempre se realiza en exclusión mutua. La comunicación entre dos procesos es síncrona cuando los dos procesos han de ejecutar los servicios de comunicación al mismo tiempo, es decir, el emisor debe estar ejecutando la operación send y el receptor ha de estar ejecutando la operación receive. La comunicación es asíncrona en caso contrario. Existen tres combinaciones más habituales que implementan los distintos tipos de paso de mensajes: Envío y recepción con bloqueo: En este caso, tanto el emisor como el receptor se bloquean hasta que tenga lugar la entrega del mensaje. Esta es una técnica de paso de mensajes totalmente síncrona. Envío sin bloqueo y recepción con bloqueo: Esta es la combinación generalmente más utilizada. El emisor no se bloquea hasta que tenga lugar la recepción y por tanto puede continuar su ejecución. El proceso que espera el mensaje, sin embargo, se bloquea hasta que llega. Envío y recepción sin bloqueo: Se corresponde con una comunicación totalmente asíncrona en la que ningún proceso espera. Existen dos modelos muy utilizados para el desarrollo de aplicaciones cliente-servidor en sistemas distribuidos: Los sockets y las llamadas a procedimientos remotos. 1. Los sockets aparecieron en 1981 en la versión BSD 4.2 de UNIX [18]. Un socket es una abstracción que representa un extremo en la comunicación bidireccional entre dos procesos. Ofrece una interfaz para acceder a los servicios de red en el nivel de transporte de los protocolos TCP/IP. Utilizando esta interfaz, dos procesos que deseen comunicarse deben crear un socket o extremo de 14 comunicación cada uno de ellos. Cada socket se encuentra asociado a una dirección y permite enviar o recibir datos a través de él. (fig. 2.4) Máquina A Máquina B proceso proceso envía recibe socket socket NÚCLEO NÚCLEO R E D 2.4 Comunicación a través de sockets En una llamada a procedimiento remoto (RPC, Remote Procedure Call) [Birrel, 1984], el programador no necesita preocuparse de cómo realiza la comunicación entre procesos. Simplemente realiza llamadas a procedimientos que serán ejecutados en computadoras remotas. En este sentido el programador desarrolla sus aplicaciones en forma convencional descomponiendo su software en una serie de procedimientos bien definidos. En una RPC, el proceso que realiza la llamada empaqueta los argumentos en un mensaje, se los envía a otro proceso y espera resultado. Por su parte, el proceso que ejecuta el procedimiento extrae los argumentos del mensaje, realiza la llamada de forma local, obtiene el resultado y se lo envía de vuelta al proceso que realizó la llamada. Este proceso es totalmente transparente al usuario. La sincronización de procesos en sistemas distribuidos es más complicada que en sistemas con memoria compartida, debido a que los procesos no comparten una memoria ni un reloj global común. Esto dificulta la forma de ordenar los eventos que ocurren en un sistema distribuido y la resolución de problemas de sincronización clásicos como los descritos en la sección 2.2. En un sistema con memoria compartida, la ordenación de eventos es un problema trivial puesto que existe un reloj físico común que permite 15 determinar que evento ocurrió antes que el otro. Esto no ocurre de igual manera en un sistema distribuido puesto que cada computadora dispone de su propio reloj. Una forma de ordenar eventos en sistemas distribuidos que carecen de un reloj global es utilizar el concepto de relojes lógicos propuesto por Lamport en 1978. Este concepto se basa en la relación de precedencia definida también por Lamport en 1978. Según Lamport, no es necesario recurrir a un reloj físico global para asegurar el orden de eventos en un sistema distribuido. Basta con asignar unas determinadas marcas lógicas de tiempo a los eventos [25]. 2.4 CARACTERÍSTICAS DE LAS ARQUITECTURAS REFERIDAS La diferencia más importante entre un sistema distribuido y un sistema con memoria compartida es el acceso a memoria y por ente la comunicación entre procesos. Los sistemas con memoria compartida permite dividir las aplicaciones en varias tareas, lo que beneficia la modularidad y con ello disminución en tiempo de ejecución. Por tanto puede atender a varios usuarios de forma eficiente, interactiva y simultánea. Una de las características más importantes de los sistemas con memoria compartida es ahorro en el tiempo de proceso, ya que la información entre los cores viaja en el bus interno, que siempre es más rápido que cualquier bus externo, además de ahorro en energía En los sistemas de memoria distribuida una de las características más importantes es que se tiene la capacidad de crecimiento. Es relativamente fácil que un sistema distribuido crezca para adaptarse a las nuevas demandas de servicio, basta con conectar más computadoras a la red. Por otra parte, si un circuito de un procesador falla, a lo más afectará a un nodo conectado a la red, por lo que no afecta a la ejecución del sistema. Con sistemas de memoria compartida, la falla de un circuito afecta de manera potencial la ejecución del sistema. 16 2.5 SISTEMAS MULTICORE: El empleo de varios procesadores dentro de una sola máquina no es un concepto nuevo. Desde hace tiempo, las tarjetas madre de algunos equipos han permitido utilizar varios procesadores, normalmente dos o cuatro. Así también se tiene que se ha hecho uso de computadoras en clúster, de modo que todos actúen como si fueran una única computadora. La novedad introducida con las nuevas tecnologías es que ahora los procesadores estén montados sobre un único chip, con 2, 4, 6 u hasta 8 núcleos dentro del mismo encapsulado. El motivo principal para esto es la economía, pues resulta más barato producir ordenadores con dos cores que dos procesadores independientes en una misma tarjeta. Además, se produce un ahorro en el tiempo de proceso, ya que la información entre los cores viaja en el bus interno, que siempre es más rápido que cualquier bus externo. Con sistemas multicore, se reparten las diversas tareas entre los diferentes cores, de manera que varias aplicaciones avanzan a la vez, permaneciendo menor tiempo posible en espera del control del procesador. La tarea de repartir los programas entre los diferentes cores es realizada por el sistema operativo, por lo que al no depender de las aplicaciones, cualquier programa, sea antiguo o actual funciona sin problema en un core. Otra de las ventajas de sistemas multicore es el ahorro de energía. Con un único procesador, una aplicación exigente pone a trabajar al tope al procesador, generando gran cantidad de calor. Si existen diferentes cores, solo trabajará al máximo el core que soporte esa aplicación en un determinado momento. Los otros tendrán una menor carga, lo que en conjunto hace que se disipe menos energía [11,3]. 2.6 MÉTRICAS DE DESEMPEÑO EN MULTIPROCESAMIENTO. Existen diversas formas de medir el desempeño de un algoritmo paralelo corriendo sobre procesadores paralelos. Las medidas más comúnmente usadas son: el tiempo de ejecución, precio/desempeño, el speed-up (aceleración) y la eficiencia [23]. 17 La métrica más importante para correr un trabajo sobre una máquina dada es el tiempo de ejecución. Precio/desempeño de un sistema paralelo es simplemente el tiempo transcurrido por un programa dividido por el costo de la máquina que corrió el trabajo. El Speed-up (Sc) y la eficiencia son las medidas más frecuentemente usadas para conocer que tan eficientemente se está utilizando. El Sc (aceleración) es la medida generalmente usada: Para obtenerla se ejecuta el mismo programa sobre un variado número de procesadores. El Sc es entonces el tiempo transcurrido en un programa secuencial por un procesador dividido entre el tiempo de ese mismo programa paralelo en n procesadores. Así la aceleración Sc para n procesadores se define como: Sc = Ec. 2.1 Donde: = Tiempo de ejecución del programa secuencial en un único procesador. = Tiempo de ejecución del programa paralelo en n procesadores. La eficiencia E de un programa es la razón entre la aceleración de un programa y el número de procesadores utilizados para la ejecución del mismo. E= Ec. 2.2 La eficiencia está relacionada con la fracción de tiempo que un procesador está siendo utilizado para ejecutar el programa. Idealmente si n procesadores utilizan el 100 % de su tiempo solamente para operaciones de computación, la eficiencia será igual a 1. Pero como un programa paralelo requiere comunicación entre procesadores, parte del tiempo 18 de los procesadores es consumido por esta actividad, llevando a valores de eficiencia menor a 1 (E<1). Idealmente se espera que el sc crezca linealmente y la E = 1 para todo n. Existen casos donde Sc es superlineal o sea que k procesadores resuelven una tarea en menos que un k-esimo del tiempo de corrida secuencial. Speed-up (sp) Lineal (ideal) Super lineal Típico. Procesadores (n) Fig. 2.5 Caso ideal de aceración para n procesadores. 2.7 LEY DE AMDAHL Una de las maneras de saber el rendimiento de nuestra aplicación paralela es la aplicación de la Ley de Amdahl [3, 11, 23]. El incremento de velocidad de un programa utilizando múltiples procesadores en computación distribuida está limitado por la fracción secuencial del programa, es decir, aquélla que independientemente de cuantos procesadores se tengan para ejecutar el programa, solo puede ser realizada por uno solo de ellos; el resto del cálculo no podrá continuar hasta que se haya completado tal parte secuencial. La Ley de Amdahl propone normalizar el tiempo que toma realizar la operación en un solo procesador al valor de 1. La fracción del cálculo que se puede realizar 19 secuencialmente será (1-P), y la parte paralelizable es P. Con estos datos, el incremento de velocidad máximo que puede obtenerse con n elementos de procesamiento está dado por la siguiente ecuación: Sc = Ec. 2.3 Como un ejemplo, si nuestra aplicación no tiene sección secuencial (es decir 1-P) y (la parte serializada) P=1, entonces el incremento de velocidad estará dado exactamente por el número de elementos de procesamiento: =n Ec. 2.4 Por otra parte, si el 50% del código es secuencial es decir (1-P)=0.5 la ecuación queda: + Ec. 2.5 Suponiendo un número infinito de procesadores, esta ecuación da como resultado dos. Si el 50% del código es secuencial, por más procesadores que se agreguen el rendimiento nunca será mayor que 2 veces con respecto a una implementación en un único procesador. Así pues, la Ley de Amdahl establece un concepto esencial para la planeación y utilización de cualquier tipo de máquina paralela: el incremento en velocidad que podemos obtener está limitado por el algoritmo que emplee nuestra aplicación, y no por el número de procesadores. Si el problema lo permite, se debe buscar implementar programas que tengan la menor cantidad de código secuencial, 20 debido a que este factor tiene mucha más importancia que el número de procesadores a utilizar. 2.8 RESUMEN DE CAPÍTULO. Para que varios procesadores sean capaces de trabajar juntos sobre el mismo problema computacional, deben ser capaces de compartir datos y comunicarse cada uno con otro. A lo largo de este capítulo se han expuesto dos arquitecturas especializadas cumpliendo estos requerimientos: Sistemas con memoria compartida y Sistemas con memoria distribuida. La principal diferencia en estas dos arquitecturas es el acceso a memoria. En computadoras con memoria compartida (multiprocesadores) todos los procesadores tienen acceso a una memoria principal global, por lo que comparten el mismo espacio de direcciones. La comunicación en estas arquitecturas es a través de variables globales y la programación eficiente es mediante el manejo de múltiples hilos. Sin embargo, una de las mayores dificultades es la coordinación en el acceso a los recursos, ya que un recurso, solo puede ser poseído por un solo hilo a la vez. Para evitar que más de un hilo accese a un mismo recurso y prevenir inconsistencia de datos es necesario emplear mecanismos de sincronización, lo cual hace que los hilos se coordinen para decidir en qué momento que hilo tiene derecho a ejecutarse y así prevenir condiciones de competencia. En sistemas con memoria distribuida, tanto la sincronización como la comunicación es por medio de mensajes enviados a través de la red. La comunicación es entre proceso y puede ser de manera directa o indirecta, es decir, deben tener una forma de referirse a cada uno de ellos. El paso de mensajes siempre se realiza en exclusión mutua, teniendo en cuenta los mecanismos de sincronización (síncrona o asíncrona). Debido a que en este tipo de arquitectura no existe un reloj común entre los procesadores la coordinación entre eventos es por medio de relojes lógicos (propuesto por Lamport) [25]. Además de la diferencia en acceso a memoria, existe un punto muy importante a recordar: en una aplicación distribuida lo que se hace es dividir un problema en varios procesos para que cooperen en la solución de éste, en una aplicación paralela lo que se 21 pretende es acelerar el procesamiento, por lo que el proceso puede ser dividido en tareas más granulares y cada una seguir un flujo de ejecución diferente, reuniendo al final resultados obtenidos. Tener recursos compartidos y gran cantidad de comunicación le resta eficiencia a la programación en estas arquitecturas. 22 CAPÍTULO 3. ARQUITECTURA DEL SISTEMA 3.1 INTRODUCCIÓN AL CAPÍTULO. En este capítulo se describe la arquitectura de hardware y software para hacer la implementación de cómputo paralelo. Se da una breve descripción de la interfaz y los pragmas utilizados para realizar paralelismo en cada una de las arquitecturas mencionadas, así como también las ventajas y desventajas que se presentan durante su uso. 3.2 DESCRIPCIÓN DE LA ARQUITECTURA DE HARDWARE. Para la construcción física del sistema propuesto se integran dos máquinas multicore con base a las interfaces de programación MPI y OpenMP. MPI para distribuir la carga entre los nodos que conforman el clúster y OpenMP para el manejo de hilos entre los procesadores en una misma máquina. Se establece un caso de estudio que permite determinar regiones críticas en el acceso a un recurso común que impone las restricciones en tiempo necesarias y permite visualizar la potencial serialización entre procesos. El recurso común es una matriz de pesos de la red neuronal ART2A llamada W. El programa está diseñado para su ejecución en N máquinas agrupadas en un clúster de alto rendimiento. En la práctica, para realizar los experimentos de desempeño se utilizaron dos máquinas conectadas entre sí a través de una interface de red del tipo LAN a una velocidad de 100 Mbps. Cada máquina está equipada con un procesador Intel Core 2 Duo que es capaz de ejecutar 2 hilos paralelos simultáneamente mediante el uso de dos núcleos [3,8]. La plataforma del sistema operativo utilizado es Windows XP de 32 bits. La herramienta utilizada para hacer trabajar estas dos máquinas como un clúster es MPI [3,7,13], con esto tenemos la capacidad de controlar cada uno de los procesos participantes en la ejecución del programa y direccionarlos hacia cualquiera de los dos equipos. La arquitectura de hardware se representa en la figura 3.1. 23 OpenMP OpenMP CPU 1 CPU 2 CPU 1 CPU 2 MEMORIA PRINCIPAL MEMORIA PRINCIPAL 1 1 MPI MPI RED Fig. 3.1 Arquitectura híbrida de multiprocesamiento. Para aprovechar la arquitectura multicore se utiliza el estándar OpenMP. El esquema de programación en tales arquitecturas se realiza mediante el control de hilos. Haciendo uso de las directivas de OpenMP en el programa, solo hay que identificar la región que no tiene dependencia de datos y puede ser paralelizada para ser ejecutada por múltiples hilos [3,4,5,6,10]. Esto sin olvidar sincronizar el trabajo de los hilos para evitar condiciones de competencia [18,21]. Debido a que los sistemas multicore cuentan con la característica de compartir memoria, se dividen las operaciones dentro de cada proceso entre los núcleos (cores) con que cuenta el sistema. Para el desarrollo del código de este programa se utilizó el IDE (Integrate Development Environment) Microsoft para la programación en lenguaje C++ y el compilador de Intel ICC (Intel Compiler Colection) para obtener la funcionalidad de las directivas de OpenMP. Como herramienta de desempeño se utiliza la librería estándar time.h del 24 lenguaje C para analizar el comportamiento que toman los hilos y su mejora con respecto a ese mismo programa de manera secuencial. Así mismo se hace uso de las métricas de desempeño como es la ley de Amdahl. Los resultados parciales obtenidos en cada proceso son comunicados a un proceso maestro dentro de la red. Este coordina a los procesos trabajadores y evalúa los resultados que le son comunicados por la interfaz MPI a través de la red. También envían una respuesta a los procesos trabajadores, indicándoles el procedimiento a seguir para la actualización o incorporación de un nuevo vector al recurso compartido. Haciendo uso de ambas arquitecturas (descritas en la sección 2.2 Y 2.3) se puede observar el desempeño del sistema y su rendimiento en base a métricas de tiempo. Esto es evaluando el tiempo consumido al final de cada uno de los procesos, así como los recursos de comunicación a través de la red empleados en promedio. 3.3 DESCRIPCIÓN DE LA ARQUITECTURA DE SOFTWARE. En los sistemas distribuidos, debido a la ausencia de memoria compartida, toda la comunicación y mecanismos de sincronización se basa en la transferencia de mensajes. Esta es la parte fundamental y básica en cualquier sistema distribuido. Tal sistema tiene la característica de poder compartir recursos como son: hardware, software o datos. Sin embargo, para obtener un buen rendimiento, es importante reducir la latencia 4 de las comunicaciones ya que estas afectan en gran medida al rendimiento. Otro aspecto a tener en cuenta para mejorar el rendimiento es evitar la existencia de cuellos de botella o recursos centralizados en el sistema. Para que el sistema pueda crecer, el sistema operativo debe evitar la existencia de componentes centralizados. En los sistemas de memoria compartida, la comunicación y sincronización es a través de variables globales 4 Latencia: Mide el tiempo necesario para transferir un mensaje vacio. La latencia contabiliza la sobrecarga introducida por el software de comunicaciones. 25 compartidas. Para lograr que varios hilos participen en la solución de un mismo problema, es necesario emplear mecanismos de sincronización, es decir, la manera de coordinar para accesar a un recurso compartido y con ello evitar inconsistencia de datos, esto se realiza a través de fragmentos de código que se denomina sección crítica. Para que un hilo pueda entrar a una sección crítica este debe pedir permiso, de igual manera cuando sale debe notificar para que los demás que están en espera de accesar y ejecutar su parte correspondiente puedan accesar. La estructura por tanto, de cualquier mecanismo que pretenda resolver el problema de la sección es la siguiente: Entrada en la sección crítica Código de la sección crítica Salida de la sección crítica Una forma representativa de dicho mecanismo es el diagrama 3.2, donde se puede observar que una sección crítica es una barrera que protege a un recurso compartido, y para que cada uno de los hilos pueda accesar a dicho recurso debe pedir permiso de entrar. Una vez que un hilo entre a ejecutar código de la sección crítica bloquea el acceso, y ningún otro podrá entrar, hasta que el hilo que la posee salga de ella. Este acceso se controla en la sección crítica mediante un mutex o un semáforo. Sin embargo, el problema está en cómo extender un orden parcial a un ordenamiento total y arbitrario, usándolo en la resolución de problemas de sincronización. 26 n 3 2 1 0 Lista de procesos en espera de accesar a la Sección crítica. MECANISMOS DE ACCESO A LA SECCIÓN CRÍTICA (MUTEX, SEMÁFORO) CÓDIGO DE LA SECCIÓN CRÍTICA ACCESO A RECURSO COMPARTIDO SOLO UN PROCESO A LA VEZ SALIDA DE LA SECCIÓN CRÍTICA (puede accesar el siguiente proceso en espera) Fig. 3.2 Sección Crítica. Solo un hilo a la vez puede ejecutar el código; los demás quedan en espera de la liberación. Como se puede observar en las figura 3.2 y 3.3, dentro de la sección crítica los procesos pueden estar accediendo y modificando variables comunes, registros de Bases de Datos, algún archivo, etc. En general cualquier recurso compartido. La característica más importante es que cuando un proceso se encuentra ejecutando código de la sección crítica, ningún otro proceso se puede ejecutar su sección. Esto es debido a que cuando un proceso entra, la sección crítica se bloquea. Los demás procesos que necesiten accesar a dicha sección quedarán en espera hasta que el proceso la libere. Y así sucesivamente, hasta que no exista proceso alguno esperando por el recurso protegido por la sección crítica. 27 La forma típica en la que una sección crítica protege datos compartidos por múltiples procesos ejecutándose simultáneamente es la Figura 3.3 Por ejemplo, cuando un proceso escribe nuevos datos a un elemento dentro de un arreglo y actualiza el índice máximo de este arreglo, puede ocurrir una inconsistencia si consideramos que otro proceso corriendo simultáneamente en otro procesador trabajando sobre el mismo arreglo realiza algún cálculo sobre cada elemento en el arreglo. Si el segundo proceso ve el nuevo valor del índice antes que el arreglo contenga un dato válido entonces el cálculo es incorrecto. La definición de una sección crítica es una solución general a este tipo de problema. Por lo que solamente un proceso está ejecutando dicha sección crítica de código a la vez. La notificación de este estado será comunicada mediante un bloqueo. El siguiente diagrama de tiempo muestra tres procesos compartiendo los mismos datos que son manipulados dentro de una sección crítica del código. Proc. 2 espera Proc. 3 espera Proceso 1 Proceso 2 Proceso 3 espera Sección crítica ejecutando Proc. 1 bloquea Proc. 1 libera Proc. 2 bloquea Tiempo Proc. 3 bloquea Proc. 2 libera Fig. 3.3 Estados de los procesos en una sección crítica (ejecutándose y en espera). 28 Solo a un proceso a la vez se le permite ejecutar el código de la sección crítica, convirtiéndose con ello en una operación atómica. Si ningún proceso está ejecutando dentro de la sección crítica, la decisión de qué proceso entra en la sección se hará sobre los procesos que desean entrar. Además esta decisión debe realizarse en tiempo finito. TIPOS DE COMUNICACIÓN. 3.4 OpenMP Para hacer la paralelización de hilos en cada uno de los procesos, se utilizan las directivas de OpenMP. Las librerías OpenMp proporcionan una forma rápida y eficiente para dar soporte a la programación multihilos, sin tener que considerar los detalles internos del funcionamiento de los hilos en determinado sistema operativo. Esto permite al programador enfocarse en el diseño sin tener que ocuparse de la implementación a detalle de los hilos. Además de que permite construir aplicaciones paralelas portables entre diferentes plataformas [2, 3, 4, 8]. El mecanismo de OpenMp funciona básicamente a través de directivas de compilación más que en el uso de funciones, de forma que la implementación del paralelismo se realiza de la forma lo más transparente posible para el código del programa. Las directivas OpenMP se aplican sobre un bloque estructurado, es decir, un enunciado simple o compuesto que tenga un solo punto de entrada y un solo punto de salida (sin saltos hacia dentro o fuera del bloque) [3]. La secuencia típica para crear una construcción OpenMP es: Definir un bloque estructurado, el cual será ejecutado por múltiples hilos en paralelo. En OpenMP, este bloque es llamado “región paralela”. Distribuir la ejecución de la región paralela entre múltiples hilos Sincronizar el trabajo de los hilos paralelos durante la ejecución de la región paralela. 29 Controlar el ambiente de datos. Cuando un hilo entra en una región paralela, se crea un conjunto de hilos. El hilo original se convierte en el hilo maestro del conjunto y tiene como identificador el numero 0, y todos los hilos incluyendo al hilo maestro ejecutan la región en paralelo. Al final el hilo maestro recoge los resultados y éste continúa con la ejecución. 3.5 Niveles de Paralelismo: En OpenMP, las dos principales formas para asignación de trabajo en hilos son: Niveles de ciclos paralelos. Regiones paralelas. Los ciclos individuales son paralelizados con cada uno de los hilos empezando una única asignación para el índice del ciclo. A éstos se le llama paralelismo de fina granulación. Con respecto a las regiones paralelas, cualquier sección del código puede ser paralelizado no precisamente por ciclos. Esto es algunas veces llamado paralelismo de gruesa granulación. El trabajo es distribuido explícitamente en cada uno de los hilos usando el único identificador (myid) para cada hilo. Esto frecuentemente se hace por el uso de declaración de if por ejemplo: if (myid == 0) donde myid es identificador de cada thread. Para especificar el número de hilos se puede utilizar la clausula num_threads en la directiva parallel, o bien se puede usar la función omp_set_num_threads. Ejemplo: 30 Int a[5][5]; Omp_set_num_threads(5); #pragma omp parallel { Int thrd_num=omp_get_thread_num(); Región a ser paralelizada. Zero_init(a[thrd_num], 5); } omp_get_thread_num(); regresa el número del hilo dentro del conjunto de hilos que está ejecutando la función. La variable thrd_num es privada para cada hilo del grupo, al que se le pueden anidar unas dentro de los bloques de otras. Por omisión, las construcciones parallel no se anidan, esto se puede cambiar con la función omp_set_nested o con la variable de retorno OMP_NESTED. Las construcciones de compartición de trabajo distribuyen la ejecución del enunciado asociado entre el número de hilos que encuentran, pero no lanzan nuevos hilos. Tampoco existe una barrera implícita sobre la entrada de la construcción. Ejemplo para el ciclo for: #pragma omp for lista_de_clausulas: for (var=1b; var operador_ lógico rb; expresión_incremento) Donde: var operador_logico rb es una variable entera que no debe ser modificada dentro del cuerpo del enunciado for. 31 expresión_incremento: puede ser uno de ++var, --var, var++, var--, var+=incremento, var=var-incremento. lb, rb, incremento son enteros invariantes dentro del ciclo operador_lógico puede ser >,<, >=,<=. La clausula schedule de la forma: Schedule(tipo[ , tamaño_de_trozo]). Especifica cómo son divididas las iteraciones entre los hilos del grupo. Si tipo es static, las iteraciones son divididas en trozos del tamaño especificado por tamaño_de_trozo, y son asignados de acuerdo al número del hilo. Si no, se especifica tamaño_de_trozo, las iteraciones son divididas en trozo aproximadamente iguales, con un trozo asignado a cada hilo. Si tipo es dynamic, las iteraciones son divididas en series de trozos, cada una conteniendo tamaño_de_trozo iteraciones. Cada trozo es asignado a un hilo que esté esperando por una asignación. Cuando no se especifica tamaño_de_trozo por omisión es 1. Si tipo es guided, las iteraciones son asignadas a hilos en trozos con tamaños decrecientes. Cuando un hilo termina su trozo de iteración asignado, se le asigna dinámicamente otro trozo, hasta que no quede ningún trozo. Si tipo es runtime, los tamaños de los trozos se especifican en tiempo de ejecución, esto se establece con la variable de entorno OMP_SCHEDULE. Existe una barrera implícita al final de la construcción para el ciclo for, a menos que la clausula nowait sea especificada. 3.6 Directivas y construcciones de sincronización. En adición a las barreras de sincronización implicadas a algunas construcciones esta la directiva barrier. 32 #pragma omp barrier Se puede declarar explícitamente para sincronizar todos los hilos en un proceso. Cuando esta directiva se encuentra, cada hilo espera hasta que todos los otros hayan alcanzado este punto. Para declarar una sección crítica se emplea la siguiente directiva: #pragma omp critical [(name)] Bloque estructurado A restringir la ejecución del bloque estructurado asociado a un simple ciclo a la vez. Un hilo espera al comienzo de la sección crítica hasta que ningún otro hilo esté ejecutando una región crítica. Estas son algunas de las directivas más utilizadas en OpenMP. Existen otras cuya referencia se pueden ver en [10] 3.7 MPI MPI (“Message Passing Interface”, Interfaz de Paso de Mensajes) es un estándar para la comunicación entre procesos que define la sintáxis y la semántica de las funciones contenidas en una librería de paso de mensajes, diseñada para ser usada en programas que exploten la existencia de múltiples procesadores. Su principal característica es que no precisa de memoria compartida, por lo que es muy importante en la programación para sistemas distribuidos. Los elementos principales que intervienen en el paso de mensajes son: el proceso que envía, el proceso que recibe y el mensaje. Dependiendo de si el proceso que envía espera a que el mensaje sea recibido, se puede hablar de pase de mensajes síncrono o asíncrono. En el paso de mensajes asíncrono, el proceso emisor no espera a que el mensaje sea recibido, y continúa su ejecución, siendo posible que vuelva a generar un nuevo mensaje y a enviarlo antes de que se haya recibido el anterior. Por este motivo se suelen emplear buzones, en los que se almacenan los mensajes a espera de que un proceso los reciba. 33 Generalmente empleando este sistema, el proceso emisor sólo se bloquea o para cuando finaliza su ejecución, o si el buzón está lleno. En el paso de mensajes síncrono, el proceso que envía espera a que un proceso lo reciba para continuar su ejecución. La Interfaz de Paso de Mensajes es un protocolo de comunicación entre computadoras. Es el estándar para la comunicación entre los nodos que ejecutan un programa en un sistema de memoria distribuida. Las implementaciones en MPI consisten en un conjunto de bibliotecas de rutinas que pueden ser utilizadas en programas escritos en los lenguajes de programación como son: C, C++, Fortran y Ada. La ventaja de MPI sobre otras bibliotecas de paso de mensajes es que los programas que utilizan la biblioteca son portables [7,13]. Con MPI el número de procesos requeridos se asigna antes de la ejecución del programa, y no se crean procesos adicionales mientras la aplicación se ejecuta. MPI le asigna a cada proceso una variable que se denomina rank (identificador del proceso dentro del comunicador en el rango de 0 a p-1, donde p es el número total de procesos). El control de la ejecución del programa se realiza mediante la variable rank, que permite determinar qué proceso ejecuta determinada porción de código. En MPI se define un comunicador como una colección de procesos que pueden enviarse mensajes entre sí. El comunicador básico se denomina MPI_COMM_WORLD. MPI también permite definir subgrupos de comunicadores a partir del comunicador básico, teniendo por tanto cada proceso un identificador único dentro de cada grupo [13]. 3.8 Llamadas utilizadas para inicializar, administrar y finalizar comunicaciones. MPI dispone de 4 funciones primordiales que se utilizan en todo programa con MPI. Estas son: 1. MPI _Init. Esta función debe ser utilizada antes de llamar a cualquier otra función de MPI y solo debe ser llamada una sola vez. 34 Int MPI_Init(int *argc, char ***argv) 2. MPI_Comm_size. Permite determinar el número total de procesos que pertenecen a un comunicador. Int MPI_Comm_size(MPI_Comm comunicador, int* numprocs) 3. MPI_Comm_rank permite determinar el identificador (rank) de proceso actual. Su sintaxis es: Int MPI_Comm_rank(MPI_Comm comunicador, int* identificador) 4. MPI_Comm_Finalize. Esta función termina una sesión MPI. Debe ser la última llamada a MPI que un programa realice. Permite liberar la memoria usada por MPI. Int MPI_Init(int *argc, char ***argv) 3.9 Llamadas utilizadas para transferir datos entre dos procesos: La transferencia de datos entre dos procesos se consigue mediante las llamadas: MPI_Send permite enviar información desde un proceso a otro. MPI_Recv permite recibir información desde otro proceso. Estas llamadas devuelven un código que indica su éxito o fracaso, y ambas funciones son bloqueantes, es decir que el proceso que realiza la llamada se bloquea hasta que la operación de comunicación se complete. En MPI un mensaje está conformado por el cuerpo del mensaje, el cual contiene los datos a ser enviados, y su envoltura, que indica el proceso fuente y el destino. El cuerpo del mensaje en MPI se conforma por tres piezas de información: buffer, tipo de dato y count. 35 El buffer es la localidad de memoria donde se encuentran los datos de salida o donde se almacenan los datos de entrada. El tipo de dato, indica el tipo de dato a ser enviado en el mensaje. El count es el número de copias del dato a ser enviado. La envoltura de un mensaje en MPI típicamente contiene la dirección destino, la dirección de la fuente y cualquier otra información que se necesite para transmitir y entregar el mensaje. La envoltura de un mensaje en MPI consta de cuatro partes: 1. La fuente, identifica al proceso transmisor. 2. El destino, identifica al proceso receptor. 3. El comunicador. El comunicador especifica el grupo de procesos a los cuales pertenecen la fuente y el destino. 4. Una etiqueta (tag), permite clasificar el mensaje. El campo etiqueta es un entero definido por el usuario y se utiliza para distinguir los mensajes que recibe un proceso [13]. 3.10 Llamadas utilizadas para transferir datos entre varios procesos. MPI posee llamadas para comunicaciones grupales que incluyen operaciones tipo difusión (broadcast), recolección (gather), distribución (scatter) y reducción. A continuación presentamos algunas funciones empleadas en nuestra aplicación: MPI_Barrier permite realizar operaciones de sincronización. Este tipo de función suele emplearse para dar por finalizada una etapa del programa, asegurándose de que todos los procesos han terminado antes de dar comienzo a la siguiente instrucción. MPI_Bcast permite a un proceso enviar una copia de sus datos a otros procesos dentro de un grupo definido por un comunicador. MPI_Scatter establece una operación de distribución, en la cual un dato se distribuye en diferentes procesos. 36 Cualquier programa paralelo con MPI puede implementarse con tan solo 6 funciones, todas ellas empiezan con MPI_ y obligan a que todos los programas escritos en MPI contengan la directiva: #include “mpi.h” Este archivo contiene las definiciones, macros y prototipos de función necesarios para compilar los programas escritos en MPI. MPI suministra dos tipos de operaciones de comunicación: Una operación punto a punto. Esta operación involucra a dos procesos, uno de los cuales envía un mensaje y el otro recibe el mensaje. Una operación colectiva. Involucra un grupo de procesos. Los grupos definen el alcance de las operaciones de comunicación colectivas. La fuente y el destino de un mensaje se identifican mediante el rango dentro del grupo. Para comunicación colectiva, el emisor especifica el conjunto de procesos que participan en la operación. Luego, el emisor restringe el acceso y proporciona un direccionamiento independiente de la máquina a través de los rangos. No obstante, MPI permite definir otros grupos y dentro de cada grupo cada proceso tiene un identificador (un número entero) único, permitiendo que un proceso pueda pertenecer simultáneamente a varios grupos y tener un identificador distinto en cada uno de ellos [14]. Esto es con el fin de evitar comunicación extra y solo los procesos que tengan más relación se encuentren en un mismo grupo. Esto se hace partiendo del comunicador universal mediante la siguiente función. int MPI_Comm_split (MPI_comm comm, int color, int key, MPI_Comm *newcom); MPI_Comm_split(MPI_COMM_WORLD, color, myid, &comMaTrab); MPI_COMM_WORD es un entero y es el identificador universal del grupo. 37 La palabra clave color divide los procesos y newcom es el nombre del nuevo grupo que se creará. Cada proceso tiene un identificador único dentro de cada grupo. VENTAJAS Y DESVENTAJAS 3.11 VENTAJAS: Las librerías OpenMp proporcionan una forma rápida y eficiente de dar soporte a la programación multihilos sin tener que considerar los detalles internos del funcionamiento de los hilos en determinado Sistema Operativo. Esto permite al programador enfocarse en el diseño sin tener que ocuparse de la implementación a detalle de los hilos. Además de que permite construir aplicaciones paralelas portables entre diferentes plataformas. Un programa multihilos se puede correr de manera concurrente en sistemas con un solo procesador. Un programa secuencial se puede convertir a versión multihilos. Solo se necesita identificar la región a paralelizar e insertar directivas de compilador en lugares apropiados, teniendo en cuenta la dependencia de datos, y con ello evitar deadlocks. Las librerías de MPI se encargan de hacer el intercambio de la información de manera transparente, por lo que el usuario solo se enfoca en el desarrollo lógico del mensaje. Existe una implementación de dominio público llamada MPICH [27,29]. 3.12 DESVENTAJAS: Donde OpenMP tiene una ventaja es en la facilidad de la programación y la portabilidad de los programas seriales, aunque los sistemas de memoria-compartida en general tienen una pobre escalabilidad. Agregando procesadores adicionales a un esquema de memoriacompartida incrementa el tráfico en el bus del sistema, haciendo más lento el tiempo de acceso a memoria y demorando la ejecución de un programa. Un esquema de memoriadistribuida, sin embargo, tiene la ventaja de que cada procesador está separado con su propio bus de acceso a la memoria. Por ésta causa, son mucho más escalables. En 38 adición, es posible construir más grandes y baratos clúster usando las comodidades del sistema conectado vía red de trabajo. Por otra parte, si un circuito dentro de un sistema de memoria compartida falla, es posible la caída del sistema, por el contrario, si esto ocurre en un sistema de memoria distribuida, el sistema puede continuar aun con los procesadores disponibles, ya que solo afectara a un solo nodo. Debido a la compartición de variables, es necesario utilizar secciones críticas, deteniendo de alguna manera la ejecución de otros hilos que requieran accesar a algún recurso protegido por la sección crítica. , ya que solo uno podrá estar dentro de esta. En Sistemas distribuidos debido a la ausencia de memoria compartida, los mecanismos de comunicación y sincronización entre los procesos es a través de la red, por lo que se puede llevar a consumir mayor tiempo en dichos mecanismos que en procedimientos de cálculo propios del problema. 3.13 RESUMEN DE CAPÍTULO: En este capítulo se describen las arquitecturas del hardware y software para llevar a cabo cómputo paralelo en arquitectura multicore y su integración a una red de computadoras (clúster de computadoras), creando en esta forma una plataforma de multiprocesamiento híbrida basada en dos importantes modelos de memoria compartida y distribuida. En este esquema se tienen recursos de software compartidos entre los diferentes procesos. Para la implementación de estas dos arquitecturas se propone el uso de dos interfaces de programación que permiten realizar de manera transparente la programación en paralelo. OpenMP y MPI. OpenMP en sistemas multicore para la ejecución de un proceso de manera más rápida. MPI para distribuir la carga entre los diferentes procesos y procesadores participantes en la aplicación. En el capítulo 4 se plantea la arquitectura de software que realiza división de los procesos que entrenan la red neuronal, en un proceso administrador y n-1 procesos trabajadores. Esto se plantea con el fin de que solo exista comunicación entre los procesos que realizan 39 entrenamiento (procesos trabajadores) y que tengan que comunicarse datos. Sin embargo, para llevar a cabo este modelo de programación en paralelo, es necesario emplear mecanismos tales como: sincronización en el acceso a los recursos compartidos y la forma en que se comunican los diferentes procesos 40 CAPÍTULO 4 CASO DE ESTUDIO. 4.1 INTRODUCCIÓN AL CAPÍTULO. En este capítulo se describe detalladamente las etapas de la red neuronal que es el caso de estudio. Se describen tres casos diferentes: En el primer caso se describe el multiprocesamiento en ambas arquitecturas, se realiza la implementación de la interfaz MPI y los pragmas de OpenMP obteniendo una ejecución hibrida de ambas. Como segundo caso se aborda el paralelismo sólo en sistemas multicore. Debido a la transparencia en la utilización de los pragmas propios de OpenMP, solo es necesario identificar la región a paralelizar en el que no debe existir dependencia de datos. Por último, como tercer caso se hace la implementación de manera serial. En cada uno de los casos se toma tiempo de ejecución, esto para determinar la conveniencia del procesamiento paralelo. 4.2 DESCRIPCIÓN DE LA RED NEURONAL ART2A. ART2A es una red neuronal de entrenamiento no supervisado, cuya estructura consiste de dos redes neuronales ART [16]. Su objetivo de es la detección, localización y clasificación de defectos y fallas internas. La información proporcionada a la red para su entrenamiento es a través de una base de datos que contiene información suficiente de cada tipo de defecto. La forma de entrenamiento es comparar las entradas presentes con categorías aprendidas seleccionadas. Las categorías aprendidas se encuentran almacenadas en una estructura multidimensional (matriz) [15]. Su entrenamiento se basa en 3 fases: 1. La fase de pre-procesamiento. 2. La fase de búsqueda. 3. La fase de adaptación. 41 1. La fase de pre-procesamiento es la creación de un vector de entrada I. El vector de entrada I contiene información del fenómeno a ser estudiado A, bajo la siguiente ecuación: I= . . . . . . . .Ec. 4.1 El vector resultante I es el vector normalizado. Los prototipos iniciales o categorías en W, los parámetros como el valor de vigilancia (ρ), aprendizaje (β) y elegido (α). Los prototipos de W pueden ser inicializados con valores aleatorios o constantes en el rango de [0,1]. El parámetro de vigilancia ρ es el grado mínimo de similitud entre el vector de entrada I, y cualquier categoría definida en que pertenece a W. El grado de aprendizaje β afecta que tan rápido el algoritmo realiza los cambios. Este es elegido para evitar que las categorías se muevan demasiado rápido y por tanto desestabilizando el proceso de aprendizaje. El parámetro de elección α є [0, ∞] define la máxima profundidad de búsqueda para un adecuado agrupamiento suministrando un desborde de punto flotante. 1. La fase de búsqueda, es una fase iterativa que puede ser dividida dentro de dos procesos: A. La elección. B. La comparación. 42 A. La elección. Una vez que el vector de entrada I es calculado, este se debe comparar con los prototipos ya definidos en W. Este proceso está encargado de encontrar la categoría óptima, definida como la categoría similar para I respecto a la conjunción difusa. I= { , }. . . . .Ec. 4.2 El valor máximo de la conjunción difusa corresponde a una alta actividad de red Tj (número de vectores aprendidos en W), donde J es la posición del vector ganador dentro de W. B. El proceso de comparación. Si el grado de similitud entre la entrada y cualquier categoría es mayor o igual a ρ, entonces el prototipo j, es elegido para representar el grupo que contiene la entrada, se dice que la red alcanza resonancia. Si el ρ, entonces otra categoría debe ser grado de similitud es seleccionada y comparada con el vector I. Si cualquier prototipo o categoría no alcanza el grado de similitud, entonces el vector I debe ser agregado a W como un nuevo prototipo. 2. La adaptación es la última fase en el algoritmo, donde el prototipo ganador tiene que ser actualizado con respecto al patrón de entrada por: = β( ) + (1 - β) Ec.4.3 El siguiente diagrama de flujo representa el entrenamiento de la red neuronal ART2A. 43 Figura 4.1 Algoritmo general de la red ART I, W, ρ, β, α T(j)= j=j+1 j=N red T(J)=max{T(j): j=1, . . . . N} Y= W =[W,I] = β(I* Y(J) =0 )+(1-β) CASOS DE ESTUDIO El caso de estudio es el entrenamiento de la red neuronal descrita. El entrenamiento es realizado en un sistema multiprocesador. El objetivo es estudiar el comportamiento de los sistemas multiprocesador cuando se tiene un recurso compartido. Para realizar el entrenamiento, se hace uso de varias técnicas MPI para realizar la comunicación entre procesos y haciendo uso del software de Intel “OpenMP” para 44 manjar hilos dentro de cada proceso. La paralelización se lleva a cabo dentro de un ciclo para manejar los hilos en cada proceso y así acelerar la operación matricial. El número de hilos empleados en la aplicación se maneja de acuerdo al número de procesadores en la máquina. En este caso particular, cada máquina tiene 2 procesadores (núcleos) y creamos 2 hilos, con el fin de que cada hilo se ejecute en un núcleo diferente y evitar consumo de tiempo en intercambio de contexto [4,6]. Para obtener datos correctos se hace la sincronización de los hilos. Al final de una región paralela hay una sincronización implícita manejada de manera transparente por OpenMP. Solo al hilo maestro (0) se le permite continuar con la ejecución. El procedimiento para tratar el problema de entrenamiento de redes ART2A se detalla en las secciones siguientes: 4.3 Caso 1: “IMPLEMENTACIÓN HÍBRIDA” Se cuenta con un clúster de computadoras con arquitectura multicore. Para la programación empleamos un paradigma de programación híbrido MPI y OpenMP, usando MPI para conectar las máquinas dentro del grupo para formar una máquina virtual con mayor capacidad de procesamiento. Por otra parte, utilizamos OpenMp para explotar el paralelismo proporcionado por la arquitectura de memoria compartida. Como se ha mencionado anteriormente, el entrenamiento de la red neuronal está diseñado para N procesos. En la práctica se realizó en dos máquinas con características multicore conectadas a través de una red LAN, teniendo un total de 4 procesadores para la ejecución del problema (dos por máquina). Debido al análisis realizado en el que se determinó que existe dependencia de datos, compartición de recursos y una necesaria comunicación entre los procesos, la resolución de problema se diseñó de la siguiente manera: Los procesos se dividen en: proceso administrador o consumidor (proceso 0) y en procesos trabajadores o productores (n-1 procesos). 45 I. El proceso consumidor (0), se encarga de recoger datos de los n-1 procesos productores. Este proceso no realiza tareas de cálculo matricial, solamente realiza tareas de sincronización, evaluación y envío de ordenes hacia los demás procesos que se derivan de la ponderación de los resultados recolectados. El proceso 0 contiene un buffer de recolección de datos correspondientes a la evaluación de la multiplicación de los vectores de entrada por la matriz de pesos en los procesos trabajadores. Cada proceso trabajador envía su resultado parcial al proceso 0, el cual se encarga de obtener un resultado global para el cada iteración del proceso ART2A. El proceso 0 controla las comparaciones de los resultados parciales con las siguientes variables globales a. Una variable llamada RO que es el valor de vigilancia dentro de la red. b. Una variable llamada ALFA que es la porción de aprendizaje de la red. 1. Al iniciar la aplicación, todos los procesos trabajadores tienen la matriz de pesos de la red neuronal en memoria en un estado inicial “un solo vector” (fig. 4.2). Los procesos trabajadores son los que se encargan de realizar cálculos de entrenamiento de la ART2A. Esta matriz es el recurso compartido que está formada por vectores con categorías aprendidas y es de tamaño MxN. Este recurso será modificado por los procesos trabajadores, dependiendo de resultados administrados por el proceso 0. Cada renglón en W es un vector de pesos (entrenado) de dimensionalidad N. M es el máximo número de vectores que puede contener la matriz, j son los vectores actuales aprendidos en W. El límite de M está definido con anticipación, aún cuando sus últimos renglones se encuentren vacios durante la mayor parte del proceso. 46 Posición Vectores actuales 0 1 1 2 2 3 3 . 4 j 2 3 . . . N vector . Máximo límite de vectores . M MN Fig. 4.2 W= Matriz de pesos de la red neuronal ART2A (Recurso compartido) Cada computadora en el grupo tiene una base de datos (BD) que contiene información suficiente de las características de los defectos y fallas a cerca del fenómeno a ser estudiado. Los diferentes procesos existentes en una misma computadora comparten la misma BD y cada uno trabaja con un vector diferente de entrada de manera paralela. En la figura 4.3 podemos observar la división de los procesos para realizar el entrenamiento de la ART2A. Los procesos trabajadores que comparten W y se encargan de realizar cálculos de entrenamiento de la red se encuentran agrupados en un comunicador exclusivo. Esto es con el fin de evitar comunicación extra con procesos que no tengan nada que ver con proceso de entrenamiento. Una vez realizado operaciones correspondientes de entrenamiento en cada uno de los procesos trabajadores envían datos al proceso 0 (esto de acuerdo a los pasos 1-4). El proceso 0 solo se encarga de administrar datos recibidos de los procesos trabajadores. A su vez es el encargado de determinar si el vector ganador se incorpora o se actualiza, esto es dependiendo del resultado de la comparación con el valor de vigilancia RO (se explica a continuación). 47 Distribución Particionamiento Proc. administrador recibe resultados parciales obtenidos de proc trabajadores. Proceso 0 Problema Proceso 1 (Entrenamiento de la red neuronal) Comunicación Proceso 2 Comunicador entre procesos trabajadores Proceso n Comunicación entre procesos trabajadores Fig 4.3 Paralelización de la red ART2A ART2A El proceso de entrenamiento se detalla en los siguientes puntos. 1. Cada proceso trabajador toma datos de la BD en un vector llamado U de dimensión N. 2. Una vez que se obtiene U[N], éste se normaliza en base a la norma Euclidiana (Ec. 1), Los resultados obtenidos son almacenados en un nuevo vector llamado I de dimensión N. Cada proceso trabajador realiza este paso. Ec. 4.4 3. Continuando con el entrenamiento de la red neuronal, cada componente del vector I multiplica a cada uno de los elementos de cada vector de W haciendo una suma acumulada de estos productos y almacenando resultados en un nuevo vector v que será de dimensión igual al número de renglones actuales en la matriz W (j). El vector v queda entonces definido por la siguiente ecuación: 48 Ec. 4.5 4. Cada proceso trabajador tiene ahora un nuevo vector v[j]. En este vector se realiza una comparación entre cada uno de los elementos que los conforman para obtener el máximo valor junto con su posición y enviarlos al proceso administrador (proceso 0) que está en espera de datos. Esto lo hace cada proceso trabajador. 5. El proceso 0 que está en espera de información de los procesos productores, establece un intervalo de tiempo para recibir información, por lo que solo alcanzarán a pasar X número de datos de los n-1 procesos, y los X datos son almacenando en dos vectores de tamaño X, un vector para almacenar valores y otro para almacenar la posición correspondiente, como se muestra en la figura 4.4. Posición 0 1 Max. Val proc1 Posición proc 1 Max. Val proc 2 Posicion proc. 2 2 . . . X Max. val proc. x Posición proc x Fig. 4.4 Vectores de dimensionalidad X con valores mayores y posición correspondiente. 6. El proceso 0 ahora tiene dos vectores. Uno con los valores mayores de cada proceso y otro con su posición correspondiente. Ya recibidos los datos, éste proceso se da a la tarea de comparar cada uno de los valores recibidos entre ellos para obtener un nuevo valor mayor (general) con su posición correspondiente, obteniendo un vector I ganador. Para determinar el grado de similitud del vector ganador I con cualquiera de las categorías aprendidas en W, este es nuevamente comparado con un valor llamado RO (Ec 3). 49 >= RO Ec. 4.6 7. Si la comparación resulta verdadero, entonces, se envía la orden a todos los procesos trabajadores de incrementar el número de vectores (j+1) para incorporar el nuevo vector I ganador a W. Este vector ganador se envía a través de mensajes al comunicador formado por los procesos trabajadores para que todos actualicen la matriz y exista consistencia de datos. Para esto se utiliza una barrera de sincronización. La barrera de sincronización elimina las condiciones de competencia que pudieran existir entre los múltiples procesos, pues cada uno de ellos detendrá su ejecución hasta que todos los procesos alcancen el punto de ejecución en el que está indicada la barrera. La figura 4.4 muestra la forma en que los procesos coordinan sus entradas a la sección crítica. 8. En caso de no cumplir con la Ec 4.6, el proceso 0 deberá difundir hacia todos los procesos trabajadores la posición del vector I en la matriz sobre la cual se realizará la actualización en base a la Ec 4.7 esto lo hará cada proceso trabajador. De igual manera existe una barrera de sincronización para bloquear a los procesos y no se les permite continuar hasta que todos los procesos tengan la matriz actualizada. I 1 Ec. 4.7 9. Cuando un proceso trabajador logre incorporar o actualizar (punto 7 u 8) regresa a la lectura de otro vector de la BD y ejecuta los pasos a partir de la normalización del vector, esto se repite mientras no exista fin de archivo. La estructura de la implementación del multiprocesamiento con el esquema anteriormente descrito puede representarse mediante el siguiente diagrama de tiempo (fig. 4.5), que indica el estado de cada uno de los procesos durante las etapas de sincronización, ejecución de código y comunicación. El diagrama muestra un ciclo completo de 50 sincronización y este se repite a lo largo de la ejecución de todo el programa mientras no exista fin de archivo de la base de datos. Envia valor mayor Broadcast ganador Proceso 0 Proceso 1 Proceso 2 ejecutando en espera Tiempo Despierta proc. 0 Calcula W*I Despiertan procs. trabajadores Evaluación Actualiza o Incorpora Fig. 4.5 Un ciclo completo de entrenamiento de la red ART2A en paralelo DESCRIPCIÓN DEL DIAGRAMA. En este diagrama de la fig. 4.5 se muestran tres procesos durante un ciclo de ejecución del entrenamiento de la red neuronal descrita anteriormente. La división de los procesos se realizó de acuerdo a la descripción en la sección 4.2. Cuando se inicia la ejecución del programa, el proceso administrador (maestro), pasa inmediatamente a la etapa de espera de resultados que le serán enviados de los procesos trabajadores (procesos 1 y 2). Nótese que este proceso maestro solamente realiza tareas de coordinación para el resto de los procesos. Los procesos 1 y 2 que se encuentran en la etapa de ejecución, realizando 51 cálculos relacionados al entrenamiento de la red neuronal ART2A, que pueden describirse de la siguiente manera: (1). Toman los datos a ser evaluados en un vector U[N], (2). Cada proceso normaliza su vector de entrada con base a la norma Euclidiana obteniendo un vector normal I[N]. (3). Cada uno de los vectores que conforman la matriz Wij es multiplicado por el vector I[N] de cada proceso Vj= , donde j es el número de vectores actuales en W y se obtiene un nuevo vector V[j], (4). El vector V[j] hace una comparación de cada uno de los elementos que lo conforman y toma el valor mayor junto con su posición. (5). Los valores correspondientes al mayor valor y posición le son enviados al proceso 0, (esto lo realiza cada uno de los procesos trabajadores 1 y 2) y se pasa a la etapa de espera. El proceso 0, que se encuentra en la etapa de espera, recibe datos y pasa a la etapa de ejecución. Este proceso hará nuevamente una evaluación (global) de los datos recibidos de los procesos trabajadores y toma el valor mayor junto con la posición correspondiente (para recibir los datos el proceso 0 establece un tiempo, por lo que solo alcanzan a entregar datos solo un x número de procesos). Una vez obtenido el valor mayor lo compara con el valor de vigilancia RO (Ec. 3) que es el que determina el grado mínimo de similitud del vector ganador I con vectores aprendidos en W. El proceso 0 informa a los procesos trabajadores el resultado de comparación mediante una operación Broadcast, que los restaura nuevamente al estado de ejecución. Con esto los procesos incrementan el valor j para incorporar al vector I o bien lo actualizan en la matriz W. Una vez que envía datos a procesos trabajadores, el proceso 0 pasa nuevamente a la etapa de espera y los procesos trabajadores pasan a la etapa de ejecución, comunicándose entre ellos el vector I ganador junto con su posición para incorporar o actualizar. Esto da la descripción de un ciclo completo de entrenamiento de la red neuronal ejecutándose en múltiples procesadores. La implementación realizada en código admite un número arbitrario de procesos trabajadores. Aunque la descripción del mecanismo de 52 paralelización dada en los párrafos anteriores involucra por cuestiones de simplicidad, solamente dos procesos trabajadores. 4.4 Caso 2: “IMPLEMENTACIÓN EN SISTEMAS MULTICORE” Para hacer uso de la tecnología multicore, se parte de un código secuencial. Para llevar a cabo la paralelización sólo se identificó la región óptima a ser paralelizada, tomando en cuenta la dependencia de datos. La región ideal a paralelizar es la multiplicación del vector normalizado I por cada uno de los elementos de los vectores que conforman W. En este tipo de arquitecturas, la programación eficiente es mediante el uso de múltiples hilos de manera que se pueda acelerar el procesamiento de multiplicación matricial. La aceleración se encuentra cuando en W el número de vectores actuales aprendidos (j) es grande, ya que como se puede observar en la fig. 4.6, I[N] multiplica a cada uno de los vectores en W paralelamente, por lo que se puede considerar un incremento en procesamiento de un 50%. Estos datos los analizaremos en el capítulo 5. HILOS I 1 2 I Matriz W 1 = 2 3 I*W1 1 N = 2 3 1 2 3 N 3 N 1M 2 3 MN N = I*W2 I*Wj Fig. 4.6 Hilos en acción. 53 Se puede apreciar una bifurcación como la que se muestra en la fig. 4.7 donde el proceso comienza con un hilo inicial y al encontrar la instrucción para paralelizar, este crea un grupo de hilos esclavos para ejecutar un ciclo for dentro del proceso. Región paralela Hilo maestro Hilo maestro Fig. 4.7 Región paralela con hilos Durante la etapa de ejecución se realizaron ejecuciones con múltiples hilos, mediante el cual se determinó como caso ideal que el número de hilos empleados en la ejecución debe ser igual al número de procesadores (núcleos) existentes. Esto es debido a que el crear un hilo toma una cantidad finita de tiempo al procesador. Por otra parte, si se crean varios hilos para paralelizar, hay que recordar que el sistema operativo, solo le da un cierto tiempo de ejecución a cada hilo. Al terminar el tiempo de ejecución asignado, si el hilo ha terminado o no, el control le es quitado para pasar al siguiente hilo en espera. Si el hilo no ha terminado de realizar la tarea, el sistema operativo salva los registros del hilo, para pasar al siguiente que le toque ejecutar. El siguiente fragmento de código muestra el uso de algunas directivas de OpenMP: El compilador al encontrar la directiva #pragma omp parallel for crea una región paralela. La región a paralelizar debe estar entre llaves {} (que indican inicio y fin). La directiva numproc=omp_get_num_procs(); obtiene el numero de procesadores. Retorna en la variable entera numproc un número entero. Esta función es importante dentro de nuestra aplicación, ya que se ha recomendado manejar hilos de acuerdo al número de procesadores. Posteriormente la variable numproc se utiliza en otra de las funciones para especificar el número de hilos empleados en la región paralela en la siguiente función: omp_set_num_threads(numproc). Otra forma de especificar el número de hilos, es poner directamente el número entero de hilos a manejar. 54 void multiplicaWI(double **W,double *I, int m,int N, double *v) int numproc=omp_get_num_procs(); { int i,j; double sumWI; omp_set_num_threads(numproc); #pragma omp parallel for for(i=0; i<m; i++) { sumWI=0; for(j=0; j<N; j++) sumWI+=W[i][j]*I[j]; v[i]=sumWI; } } Como se puede observar, las directivas OpenMP permiten implementar mecanismos de paralelismo y sincronización en una forma muy transparente con respecto al código secuencial. Cada llamada en OpenMP equivale a una secuencia de llamadas a APIs del sistema operativo que crean, sincronizan y destruyen a los objetos necesarios para la creación del código objeto correspondiente. Liberándonos de implementar estas tareas en el código. Una representación de flujo de ejecución es la figura 4.8. En la figura (a) se observa como la ejecución de un proceso comienza con un hilo inicial. Al encontrar la directiva #pragma omp parallel for de OpenMP automáticamente crea hilos, los cuales se ejecutaran de manera paralela. En la fig. (b) se observa un mayor número de hilos. Si dentro de los sistemas multicore, solo se cuenta con 2 núcleos, éstos se ejecutarán de manera concurrente, ocurriendo intercambio de contexto, y un mayor consumo de tiempo 55 que le será tomado al procesador al crearlos. Aunque para el usuario esto se realice de manera transparente. Fig. 4.8 Flujo de ejecución de hilos HILO 3 HILO 1 HILO 1 HILO 1 HILO 1 HILO 2 HILO 1 HILO 4 HILO 2 a). Flujo de ejecución de hilos de manera paralela. b). Flujo de ejecución de hilos de manera concurrente 4.5 Caso 3: “IMPLEMENTACIÓN EN SERIE” En este caso, se llevó a cabo la ejecución del programa de entrenamiento de la red neuronal de manera secuencial (con los pasos descritos anterior mente). Como se muestra en la siguiente figura. Donde K es un apuntador al inicio de la Base de Datos (BD), Kmax es el límite en la BD. 56 Figura 4.9 Diagrama de flujo de la red ART2A de manera secuencial Inicio 1 W, BD, β, α, ρ ¿j = 0? Si mayor = v[j] i:=0 K:=0 ¿i < j? i++ 2 si ¿K<Kmax? ¿v[i] > v[i+1]? mayor:= v[i] no Lee U de la BD mayor = v[i+1] “Normaliza U” si ¿ρ >=mayor? I= i:=0 ¿i< j? j=j+1 i++ IAW sumWI := 0 =β(I* i:=0 ) + (1-β) ¿i<N? i++ sumWI: = sumWI+W[i,j]*I[i] v[i]: = sumWI K:= K +1 2 1 57 Para obtener el tiempo de ejecución se emplearon las funciones de tiempo de C. Esto es con el fin de saber cuánto tiempo consume en su ejecución y con ello poder probar qué tan conveniente o hasta que punto genera buenos resultados hacer uso de la programación paralela. El programa serial ocupa menos espacio en memoria al necesitar menos instrucciones, ya que se omite el código correspondiente a las pragmas y mecanismos de comunicación y sincronización requeridos en las versiones paralelas. Así mismo se observar que en cuanto crece el número de datos a procesar tales como: dimensión (N), el número de renglones aprendidos en W (j) y el número de registros en la base de datos (Kmax), el tiempo de ejecución es cada vez mayor. 4.6. RESUMEN DE CAPÍTULO: Durante el desarrollo de este capítulo se presenta la implementación de las arquitecturas para realizar cómputo paralelo (arquitectura multicore y sistemas distribuidos). Para el uso de estas dos arquitecturas se presenta un programa híbrido mediante la implementación de funciones y pragmas de MPI y OpenMP, en el que se emplean mecanismos típicos de la programación paralela tales como: secciones críticas y comunicación de datos. Es muy importante tener presente que en estas dos arquitecturas no solo se comunican de manera diferente, sino también existen grandes diferencias durante la programación. Por lo que es necesario conocer tanto sus características como las limitaciones en cada una de ellas, esto con la finalidad de obtener un uso adecuado y un mejor rendimiento de la ejecución de aplicaciones en arquitecturas paralelas con implementación híbrida. Para la implementación se establece un caso de estudio que permite determinar regiones críticas en el acceso a un recurso compartido. Se emplean mecanismos de sincronización 58 y comunicación en el acceso que nos impone restricciones en tiempo necesarias que permite visualizar potenciales condiciones de competencia entre procesos e hilos. 59 CAPÍTULO 5 5.1 MÉTRICAS Y ANÁLISIS DE RESULTADOS INTRODUCCIÓN AL CAPITULO. En este capítulo se analizan resultados de la ejecución del entrenamiento de la red neuronal ART2A en multiprocesadores. Se analizan resultados de los casos descritos en el capítulo anterior, mediante lo cual se puede apreciar el rendimiento conforme al número de procesadores participantes en la aplicación. A través de las gráficas y análisis de resultados de la ejecución del algoritmo en 2 máquinas con arquitectura multicore (4 procesadores) se observa un incremento en la aceleración de ejecución con respecto a su versión secuencial. Debido a no contar con equipo con características similares es que no se pudo obtener datos conforme al número de procesadores el cual determina que: agregar más procesadores para la ejecución del programa no garantiza una aceleración en tiempo de ejecución. Esto es debido a la parte secuencial del programa que solo tiene que ser ejecutada por un solo procesador independientemente del número de procesadores que se tenga para su ejecución (Ley de Amdahl) [11]. 5.2 Aplicación de Métricas de desempeño en sistemas multicore. Sobre un sistema multiprocesador, un problema puede ser divido en un número arbitrario de tareas independientes para un mejor desempeño con respecto al mismo en su versión secuencial. Un sistema con 2 procesadores puede ejecutar 1.95 veces más rápido que un único procesador, un sistema con 3 procesadores 2.9 veces más rápido, un sistema con 4 procesadores 3.8 veces más rápido y así sucesivamente [5]. Sin embargo la aceleración casi siempre cae conforme el número de procesadores se incremente [22]. Esto es debido a la creación de hilos, al intercambio de contexto, la comunicación y mecanismos de 60 sincronización que deben ser tomados en cuenta al desarrollar aplicaciones que requieran la participación de más de 2 procesadores. La tabla 1 muestra datos de la ejecución del programa de entrenamiento de la red neuronal ART2A en 2 procesadores con varios hilos. Los datos de entrenamiento son los siguientes: N=100; BD=10000; W=100000; RO=0.4. Donde: N Es la dimensión del vector. BD Número de registro en la base de datos. W Es el máximo número de vectores que se pueden incorporar a W. RO Es el grado mínimo de similitud entre el vector de entrada I y cualquier categoría definida en W. El tiempo de ejecución para el entrenamiento de la red de manera serial es: 20.32800 seg. Este programa solo se ejecuta en un único procesador. Para que este mismo programa sea ejecutado de manera paralela, se hace la implementación de los pragmas de OpenMP, en el que se observa mejoras en tiempo con respecto a la versión secuencial. Número de hilos Tiempo seg. Tamaño W 2 12.32800 9163 3 14.73400 9163 4 12.79600 9163 61 5 13.89000 9163 6 12.85900 9163 7 13.35900 9163 8 12.87500 9163 9 13.25000 9163 10 12.93700 9163 . . . . . . . . . 10,000 14.06200 9163 100,000 14.10900 9163 1,000,000,000,000 20.32800 9163 Tabla 5.1 Resultados de entrenamiento de la red neuronal ART2A con varios hilos para un valor de RO=0.4 Como se puede observar, el manejo de múltiples hilos en la ejecución de un programa presenta mejoras en tiempo comparado con la versión secuencial. Sin embargo, llega un punto en el que crear varios de éstos ya no es conveniente, por el contrario el incremento en tiempo es notorio, esto es debido a los mecanismos de creación y sincronización de hilos. Se han realizado varias ejecuciones de este mismo programa con variaciones en las variables antes mencionadas, pudiéndose observar que la variable RO presenta un gran 62 impacto en el recurso compartido W. Esta variable es el factor de semejanza del vector a ser estudiado comparado con los vectores clasificados en W, por lo que se pude determinar que a valores mayores (para RO) la red permite mayor clasificación, por el contrario, a valores pequeños se realiza una clasificación más selectiva. Por lo que el crecimiento de W está en función de dicha variable. Considerando que la región a ser paralelizada es la matriz W es conveniente tener dimensiones grandes “tanto para N como para M” para que valga la pena la creación de hilos. El crear y sincronizar hilos le toma una cantidad finita de tiempo al procesador, por lo que si se tiene dimensiones pequeñas en la matriz solo se hará mayor consumo en tiempo en la creación de éstos que en el proceso de entrenamiento de la red. La tabla 2 muestra datos de la ejecución del programa con una variación en el valor de RO=0.1. Los demás datos con el mismo valor. El tiempo de consumo en su ejecución secuencial es: 0.156000 seg. Número de hilos Tiempo seg. Tamaño W 2 0.359000 61 3 0.312000 61 4 0.343000 61 5 0.390000 61 6 0.437000 61 7 0.546000 61 8 0.562000 61 9 0.625000 61 63 10 0.656000 61 . . 61 . . . . . . 100 1.937000 61 200 1.968000 61 300 1.953000 61 400 2.015000 61 500 1.984000 61 1000 2.000000 61 2000 2.000000 61 3000 2.000000 61 . . . . . . . . . 10,000 1.98000 61 100,000 2.000000 61 1,000,000 2 61 64 10,000,000 2 61 100,000,000 2 61 1000,000,000 2 61 Tabla 5.2 Resultados de entrenamiento de la red neuronal ART2A con varios hilos para un valor de RO=0.1 En estos datos se puede observar que el tiempo de ejecución de manera secuencial es menor que el tiempo consumido en su versión paralela. Con esto se puede demostrar la importancia que tiene la dimensión de los datos (N y M en este caso) en arquitecturas paralelas. Los resultados de ejecución de entrenamiento de la red neuronal haciendo uso de ambas arquitecturas (sistemas de memoria compartida y sistemas de memoria distribuida) se muestran a continuación: N=100; BD=100; RO=0.1; W=100; El tiempo en su versión secuencial es: 0.93000 seg. Este mismo programa en la versión paralela (solo haciendo uso de la arquitectura multicore) es: 0.7800 seg. Con un Sc=1.1923 y E=0.5961 65 Para 3 y 4 procesadores, los resultados son los siguientes: Procesadores Tiempo Aceleración Eficiencia RO=0.1 RO=0.4 seg. (Sc) (E) 3 0.68700 1.3537 0.5961 23 99 4 .421000 2.2090 0.5522 23 99 Tabla 5.3 Resultados de entrenamiento de la red neuronal ART2A para 3 y 4 procesadores. Se puede observar un tiempo de ejecución mejor que la versión secuencial. Sin embargo, al igual que el manejo de hilos el crear varios procesos para su ejecución de entrenamiento no resulta conveniente. Esto se observa en los resultados de la siguiente tabla (5.4). Para la ejecución con más de cuatro procesos el procesador comienza a ejecutar cada proceso de manera concurrente, por lo que al compartir el tiempo del procesador en cada una de las tareas y teniendo un recurso compartido no resulta conveniente. Para determinar el speed-up (aceleración) y la eficiencia (E) se hizo uso de la Ec. 2.1 y 2.2 respectivamente del capítulo 2 (sección 2.6), en el que se procedió a ejecutar el algoritmo de entrenamiento de la red neuronal ART2A de manera secuencial (en un solo procesador) y el mismo programa en su versión paralela (en 2, 3 y 4 procesadores). Gráficamente la aceleración se representa en la siguiente figura. 66 4 2 Sc 1 1 2 3 4 procesadores Fig. 5.1 Aceleración de entrenamiento de ART2A en 4 procesadores. Como se puede observar, el incremento en la aceleración es de manera lineal para 4 procesadores, obteniendo de igual manera una buena eficiencia. Pero como un programa paralelo requiere comunicación entre procesadores, parte del tiempo de los procesos es consumido en esta actividad, por lo que aumentar procesadores, nos lleva a valores de eficiencia menores a 1. Durante la prueba de entrenamiento se hicieron varias ejecuciones en el programa híbrido en el que se fue incrementando el número de procesos (Tabla 5.4). Se puedo observar que el tiempo de ejecución aumenta conforme aumentaba el número de procesos, esto es debido a que los procesos se comienzan a ejecutar de manera concurrente, y debido a que comparten un mismo recurso, esto también se ve reflejado en el tiempo que tarda un proceso en espera del acceso a este. Procesadores Tiempo Aceleración Eficiencia RO=0.1 RO=0.4 seg. (Sc) (E) 3 0.68700 1.3537 0.5961 23 99 4 .421000 2.2090 0.5522 23 99 5 .47500 1.9578 .3915 23 99 6 .75600 1.2301 .2050 23 99 67 7 .76900 1.2813 .1830 23 99 8 .92700 1.003 .1253 23 99 9 .94200 .9872 .1096 23 99 10 .97000 .9587 .0958 23 99 Tabla 5.4 Tiempo de ejecución para 10 procesos en 4 procesadores. El diagrama 5.2 representa a 3 procesos en el que se da el tiempo de ejecución en cada una de las etapas: PRIMERA ETAPA (CALCULA W*I) Se calcula el tiempo que tarda cada uno de los procesos trabajadores desde el momento en que toma de la base de datos el fenómeno a ser estudiado, lo normaliza en base a la norma Euclidiana obteniendo resultados en un segundo vector I[N]. Este vector normalizado I multiplica a cada uno de los vectores contenidos en Wij, por lo que hace una suma acumulada y el resultado lo almacena en un vector V cuya dimensión es igual al número de vectores contenidos en W, este vector V[j] hace una comparación de cada uno de sus elementos y elige el valor mayor con su respectiva posición. Cada uno de los procesos obtiene su valor mayor y lo envía al proceso administrador. SEGUNDA ETAPA (EVALUACIÓN): El proceso establece un intervalo de tiempo para recibir información referente al dato mayor y su respectiva posición de los procesos trabajadores, por lo que solo alcanza a recibir X número de valores y los almacena en dos vectores, uno con valor y otro con su posición correspondiente. Este proceso administrador hace una evaluación general y obtiene un valor mayor general nuevamente con su posición correspondiente, el valor 68 ganador lo compara con el valor de RO para determinar el grado de similitud del vector ganador con las categorías aprendidas en W, una vez determinado esto, este proceso envía en mensaje para que los procesos trabajadores incorporen el vector ganador I a W o se actualice I (explicado en capitulo 4). TERCERA ETAPA (ACTUALIZA O INCORPORA) En esta etapa, los procesos trabajadores que están agrupados en un comunicador exclusivo se comunican el vector ganador I ya sea para incorporar o actualizar, esto fue determinado por el proceso de comparación (segunda etapa). Cada uno de estos tiempos está representado en el siguiente diagrama. 5.2 Tiempos de ciclo de ejecución del entrenamiento de la ART2A Envía valor mayor Broadcast ganador Proceso 0 Proceso 1 Proceso 2 Ejecutand o en espera Despierta proc. 0 Calcula m W*I Despiertan trabajadores Evaluación procs. Actualiza o Incorpora Tiempo consumido por el proceso 1 en las etapas de: multiplicación 0.249000 seg. Actualización Dando un tiempo total de ejecución para el proceso 1 de: 1.236000 seg. 1.485000 seg 69 Tiempo consumido por el proceso 2 en las etapas de: multiplicación 0.232000 seg. Actualización Dando un tiempo total de ejecución para el proceso 2 de: El tiempo total de los procesos es: 1.252000 seg. 1.484000 seg 1.485000 seg + 1.484000 seg = 2.969 seg. El tiempo consumido por el proceso 0 en la etapa de evaluación es: 2.078000 seg. El tiempo consumido en comunicación entre procesadores es: 53 nanoseg. Por tanto, el tiempo total consumido para la ejecución del programa es: Tiempo Total = Tiempo Secuencial + Tiempo Paralelo Tiempo Total = 2.078000seg + 2.969 seg = 5.047seg 5.3 RESUMEN DE CAPÍTULO En este capítulo se analizaron los resultados de ejecución del entrenamiento de la red neuronal en 1,2,3 Y 4 procesadores obteniendo resultados alentadores en la arquitectura híbrida y la ejecución en plataforma multicore con respecto al tiempo de ejecución en su versión secuencial,. Además de obtener el tiempo de ejecución en cada una de las etapas. Por lo que se puede concluir que el programa en su versión paralela obtiene buenos resultados debido a la división de los procesos para que no exista comunicación extra. Sin embargo se observa que conforme se aumenta el número de procesos, la comunicación y sincronización aumenta el tiempo de ejecución, por lo que es importante a la hora de programar tener en cuenta la división y participación de los procesadores en los procesos participantes. 70 Además en este capítulo se pudo observar que el desempeño de la red neuronal es bueno, ya que durante la etapa de ejecución se pudo observar que la red se encuentra aprendiendo, es decir, que realiza la clasificación de acuerdo a resultados de comparación del valor de similitud por lo que determina si incorpora o actualiza. Para la variable RO (ρ), se manejaron 2 valores en el que se pudo observar que conforme al valor sea más pequeño, este realiza una clasificación más selectiva, por lo que los renglones (vectores) aprendidos en W incrementaran de acuerdo a esta variable. En cuanto al desempeño del entrenamiento de la red neuronal encontramos que el parámetro de aprendizaje Beta (β) fuee el que determinaba, como era de esperarse, la velocidad del crecimiento de la matriz de pesos W durante las iteraciones del programa, mientras que la selectividad se encuentra gobernada por los valores de RO, por lo que consideramos que el esquema de paralelización aquí propuesto constituye una implementación correcta del algoritmo ART2A. 71 CAPÍTULO 6 CONCLUSIONES. El desarrollo de este trabajo se realizó en el estudio de los sistemas multicore en conjunto con los sistemas de alto rendimiento (Sistemas Distribuidos). Se llevó a cabo la implementación de ambas arquitecturas paralelas en el que se realizó un programa híbrido haciendo uso de las interfaces de programación en este tipo de arquitecturas MPI y OpenMP. La herramienta utilizada para hacer trabajar las máquinas como un clúster es MPI y se emplea OpenMP para agilizar la solución del proceso en cada uno de los cores existentes en cada máquina. Para realizar los experimentos de desempeño se utilizaron dos máquinas conectadas entre sí a través de una interface de red del tipo LAN a una velocidad de 100 Mbps, cada máquina está equipada con un procesador Intel Core 2 Duo que es capaz de ejecutar múltiples hilos paralelos mediante el uso de dos núcleos, la plataforma del sistema operativo utilizado es Windows XP de 32 bits. Con la finalidad de analizar el comportamiento de múltiples procesadores en la ejecución de una misma aplicación, se tomó como caso de estudio el entrenamiento de la red neuronal ART2A en el que se tiene un recurso compartido por los diferentes procesadores participantes en la aplicación, el cual permite determinar regiones críticas en el acceso al dicho recurso que impone las restricciones en tiempo necesarias y permite visualizar potenciales condiciones de competencia. Una de las ventajas en este tipo de arquitecturas el contar con herramientas como MPI y OpenMP que permiten manejar de manera transparente estos mecanismos, por lo que solo hay que preocuparse de la implementación adecuada de las directivas para llevar a cabo una buena distribución de las aplicaciones y con ello determinar el mejor algoritmo paralelo en que el tiempo consumido sea en cálculos de proceso y no en comunicación y mecanismos de sincronización en el que los procesos puedan estar inactivos por mucho tiempo. Durante la ejecución del algoritmo de entrenamiento de la red neuronal se observaron resultados óptimos en tiempo con respecto a su versión secuencial, el programa se 72 ejecutó en cuatro procesadores, debido a no contar con más equipo con características similares es que no se pudo obtener resultados que demuestren el punto en el cual agregar más procesadores no resulta conveniente, esto es debido a la parte secuencial del programa propuesto por la Ley de Amdahl. Por otra parte, para mostrar que el tener múltiples procesos activos en la ejecución de un programa tampoco es garantía en cuanto a tiempo de ejecución en el problema, esto se determinó en base a resultados obtenidos a través de múltiples ejecuciones del programa con diferentes números de procesos, observándose que conforme aumenta el número de procesos incrementa también el tiempo de ejecución, esto es debido al tiempo de comunicación que consumen procesos para intercambiar resultados, así como también el tiempo que esperan los procesos para accesar al recurso compartido debido a las barreras de sincronización para evitar inconsistencia de datos. En cuanto a la aplicación del algoritmo solo en sistemas multicore se pudo observar que el tiempo de ejecución disminuye casi a un 50%, y con ello obteniendo un grado de eficiencia de un 90% , por lo que se recomienda ampliamente el uso de estos sistemas y teniendo en mente que el crear hilos así como el intercambio de contexto le toma una cierta cantidad de tiempo al procesador, por lo que a la hora de programar se recomienda ampliamente se utilicen hilos conforme al número de procesadores disponibles en la máquina y asegurar una ejecución paralela. Por tanto, dados los resultados obtenidos se recomienda ampliamente el uso de la tecnología híbrida en sistemas de control, siempre y cuando se tenga en cuenta el rango de valores para el cual se aprovecha el paralelismo introducido en el modelo presentado. Un vector cuya dimensionalidad es grande (N) es conveniente para la utilización de la plataforma paralela, además de ser altamente confiable, ya que los sistemas de clasificación suelen trabajar con una gran cantidad de características en los patrones, y tales características suelen ser de una dimensionalidad alta por lo que en total se maneja un número N de variables grandes. 73 APÉNDICE. CÓDIGO HÍBRIDO UTILIZANDO LAS INTERFACES DE PROGRAMACIÓN MPI Y OpenMP //Librerias utilizadas por el programa #include <stdlib.h> #include <stdio.h> #include <time.h> #include <math.h> #include "mpi.h" #include <omp.h> #include <dos.h> //Variables globales reconocidas por todas las funciones const int N=1000; //dimension de los vectores con que se trabaja const int mMax=1000; //máximo de vectores que se pueden incorporar a la matriz W const double RO=0.1; //parámetro de vigilancia, es el grado mínimo de similitud //entre el vector de entrada I y cualquier //categoría definida en W const double ALFA=0.3; int numproc=omp_get_num_procs(); //Es un pragma de OpenMP que obtiene el número de procesadores en la PC const long kMax=1000; en la base de datos //número de registros que por ahora tomamos como máximo int procMax=300; //Procesos máximos que pueden manejarse //Prototipos de las funciones utilizadas void leerU(double * u,int N, int j); 74 void normalizaU(double * I,double * U,int N); void muestradatos(double * U,int N); void inicia_matriz(double**, int n, int *m); void multiplicaWI(double **W,double *I, int m,int N, double *v); double mayor(double *v, int M, int *pos); void incorporaW(double **W, double *I, int *m, int N); void actualizaW(double **W, double *I,int N,int j); //Funcion principal void main(int argc, char *argv[]) { //variables para toma de mediciones de tiempos de procesamiento y comunicacion time_t hra1,hra2; hra1=time(NULL); clock_t inicio_cpu = clock(); int namelen,myid, numprocs; //variables para MPI char processor_name[MPI_MAX_PROCESSOR_NAME]; MPI_Init(&argc,&argv); //inicia el ambiente de MPI MPI_Comm_size(MPI_COMM_WORLD,&numprocs); con el núm. de procesos MPI_Comm_rank(MPI_COMM_WORLD,&myid); //Comunicador universal en MPI //Identificador del proceso. MPI_Get_processor_name(processor_name,&namelen); printf("Algoritmo ART2a consumidor 0 dirige a procesos trabajadores \n"); if (numprocs<2 || numprocs>procMax){ printf("Se necesita al menos 2 procesos en este esquema de paralelizacion\n"); printf("el limite máximo son %d procesos\n",procMax); printf("Programa Terminado. Adios!!!\n"); MPI_Abort(MPI_COMM_WORLD, 1); } 75 MPI_Comm comMaTrab; //comunicador dividido en procs master y trabajadores int color; //color para dividir los procesos //divide el comunicador. (Si master, id>0 para trabajadores, el identificador) id=0 para if (myid>0) color=1; else color=0; MPI_Comm_split(MPI_COMM_WORLD, color, myid, &comMaTrab); //Se crea un comunicador para dividir al proceso //maestro (administrador) de los procesos trabajadores. MPI_Status status; //estado de recepción de comunicación int i; //variable para ciclos int pos=0; //posición del mayor valor del vector resultante de la //multiplicación W[i]*I en los procesos trabajadores int k=0; //k es el número de registro de la base de datos //en una base de datos verdadera k puede omitirse y un ciclo while(!eof) double *u; //apuntador al inicio del vector leído en la BD double *v; //apuntador al inicio del vector resultante de W[i]*I double **W; //Apuntador al inicio de apuntadores al inicio de los vectores //Indica el inicio de los datos correspondientes a la matriz de pesos int m=0; //m es el número de renglones ocupados en la matriz double *I; //Apuntador al inicio del vector Normalizado W int ganador; procedimiento //id del proceso ganador en cada iteración del clock_t inicio_evaluacion; clock_t inicio_multiplicacion; clock_t inicio_actualizacion; clock_t evalcont=0; 76 clock_t multcont=0; clock_t actcont=0; W=new double*[mMax]; inicia_matriz(W,N, &m); if (myid==0){ //SOY EL PROCESO CONSUMIDOR QUE RECOGE DATOS DE LOS PROCESOS PRODUCTORES 1 Y 2 int *Posiciones; //buffers para recibir las posiciones y double *Valores; //valores de los mayores resultados de WI=V Valores=new double[procMax]; Posiciones= new int[procMax]; //printf("Soy Proceso Master\n"); while(k<kMax){ //después cambiar por un indicador leído de los procesos //printf("Espero Recibir\n"); //Recibir los mayores valores y posiciones de cada proceso for (i=0;i<(numprocs-1);i++){ MPI_Recv((void //El proceso 0 recibe valores de los *)(Valores+i),1,MPI_DOUBLE,i+1,0, //procesos trabajadores. MPI_COMM_WORLD,&status); MPI_Recv((void *)(Posiciones+i),1,MPI_INT,i+1,0, // El proceso 0 recibe posic MPI_COMM_WORLD,&status); } //printf("Ya Recibi\n"); // {printf("Valores a evaluar en el proceso 0:"); 77 // muestradatos(Valores,numprocs-1); // } /*empieza a medir el tiempo de evaluación*/ inicio_evaluacion = clock(); if (RO>=mayor(Valores,numprocs,&ganador)){ //posición a actualizar, si es igual que el 'm' de los procesos //trabajadores, entonces se incorpora un nuevo vector Posiciones[ganador]=m; //aumenta el valor de m para indicar que habrá un nuevo renglón m++; } // { // printf("Índice ganador %i:",ganador); // } //envía la posición del vector a modificar del proceso ganador //el id del proceso ganador es = ganador + 1 //++ganador; for (i=0;i<(numprocs-1);i++){ MPI_Send((void *)&Posiciones[ganador],1,MPI_INT,i+1, 0,MPI_COMM_WORLD); MPI_Send((void *)&ganador,1,MPI_INT,i+1, 0,MPI_COMM_WORLD); } k++; /*termina de medir el tiempo de evaluación*/ evalcont+= clock()-inicio_evaluacion; // if(k%100==0)printf("Llevo %i iteraciones \n",k); } 78 }else { // SOY ALGUNO DE LOS PROCESOS PRODUCTORES v=new double[mMax]; u=new double[N]; I=new double[N]; double val; //Inicia la semilla rand con valores distintos en cada proceso srand((unsigned int)myid); //printf("\nSoy un proceso trabajador\n"); while(k<kMax){ //después cambiar por while(!eof) /*empieza a medir el tiempo de multiplicación*/ inicio_multiplicacion = clock(); leerU(u,N,k); // printf("U:");muestradatos(u,N); normalizaU(I,u,N); //#pragma omp critical // {sleep(myid); printf("I:");muestradatos(I,N);} multiplicaWI(W,I,m,N,v); // %i:",myid);muestradatos(v,m);} {printf("V=W*I en el proceso val=mayor(v,m,&pos); //envía el valor y la posición donde se encontró el mayor valor MPI_Send((void *)&val,1,MPI_DOUBLE,0,0,MPI_COMM_WORLD); MPI_Send((void *)&pos,1,MPI_INT,0,0,MPI_COMM_WORLD); /*Termina de medir el tiempo de multiplicación*/ 79 multcont+= clock()-inicio_multiplicacion; /*Empieza a medir el tiempo de actualización*/ inicio_actualizacion = clock(); //recibe el id del proceso ganador y la posición a modificar MPI_Recv((void *)&pos,1,MPI_INT,0,MPI_ANY_TAG, MPI_COMM_WORLD,&status); MPI_Recv((void *)&ganador,1,MPI_INT,0,MPI_ANY_TAG, MPI_COMM_WORLD,&status); //este punto es una barrera de sincronización para todos los //procesos trabajadores. MPI_Bcast((void *)I,N,MPI_DOUBLE,0,comMaTrab); //Hasta aquí todos tienen el mismo vector I del proceso ganador if (pos==m){ //Todos incorporan a I en su copia de W incorporaW(W,I,&m,N); }else{ //Todos actualizan la posición pos en us copia de W actualizaW(W,I,N,pos); } //printf("\nun trabajo hecho\n"); k++; //para la siguiente iteración /*Termina de medir el tiempo de actualización*/ actcont+= clock()-inicio_actualizacion; } } MPI_Barrier(comMaTrab); 80 //muestranos la matriz W if (myid==1){ // printf("\nW empieza en la dirección %p:\n",W); // printf("\nValor de m %d:\n",m); printf("\nDatos de la matriz W:\n"); for (i=0;i<m;i++) muestradatos(W[i],N); } if (myid == 0){ hra2=time(NULL); clock_t fin_cpu = clock(); printf("\nTiempo %f segundos\n",difftime(hra2,hra1)); printf("\nTiempo de cpu %f segundos\n", ((double)(fin_cpu - inicio_cpu))/CLOCKS_PER_SEC); printf("\n número de renglones de la matriz %i\n",m); } /*Tiempos de slots en el ciclo*/ if (myid == 0){ printf("\nTiempo evaluación %f segundos\n", ((double)evalcont) /CLOCKS_PER_SEC); } for (i=1;i<numprocs;i++){ if (myid==i){ printf("\nProc %i Tiempo multiplicación %f segundos\n", i,((double)multcont) /CLOCKS_PER_SEC); printf("\nProc %i Tiempo actualización %f segundos\n", i,((double)actcont) /CLOCKS_PER_SEC); 81 } } MPI_Finalize(); } void leerU(double * u,int N, int j){ //estos serian los valores que se leen de la base de datos aquí iría el código //para leer la base de datos por ahora se rellenan aleatoriamente con números entre -100 y 100 for (int i=0; i<N; i++){ u[i]=-100+200*rand()/RAND_MAX; } //aquí los valores de la base de datos se //tendrían referenciados por el apuntador u } void normalizaU(double *I,double * u,int N){ double sumatoria=0; double norma; int i; for (i=0; i<N; i++){ sumatoria+=u[i]*u[i]; } norma=sqrt(sumatoria); if (norma>0.0) //evitar divisiones por cero for (i=0; i<N; i++){ I[i]=u[i]/norma; } } 82 void muestradatos(double * U,int N) { printf("["); for (int i=0; i<N; i++) printf("%5.2f, ", U[i]); printf("]\n"); } void inicia_matriz(double** W, int N, int *m) { int j=0; double *I=new double[N]; W[0]=new double[N]; //llena el primer renglon de W con datos //aleatorios entre -1 y +1 for (j=0; j<N; j++){ W[0][j]=-1+2*rand()/(double)RAND_MAX; } //normaliza el primer renglon de W para //que se cumpla la condicion de ||w|| normalizaU(I,W[0],N); for(j=0; j<N; j++){ W[0][j]=I[j]; } (*m)++; delete [] I; } void multiplicaWI(double **W,double *I, int m,int N, double *v) { int i,j; double sumWI; omp_set_num_procs(numproc); 83 #pragma omp parallel for for(i=0; i<m; i++) { sumWI=0; for(j=0; j<N; j++) sumWI+=W[i][j]*I[j]; v[i]=sumWI; } } double mayor(double *v, int m, int *pos) { double mayor; mayor=v[0]; *pos=0; for (int j=0; j<m;j++) { if (v[j]>mayor){ mayor=v[j]; *pos=j; } } return mayor; } void incorporaW(double **W,double *I, int *m,int N) { W[*m]=new double[N]; for (int j=0;j<N;j++) W[*m][j]=I[j]; (*m)++; } void actualizaW(double **W, double *I,int N,int pos) { 84 int k; for (k=0;k<N;k++){ W[pos][k]=ALFA*W[pos][k]+(1-ALFA)*I[k]; } } CODIGO SERIAL Y/O PARALELO. Este mismo código lo utilizamos en ambos casos, como se ha mencionado solo se le agregan las directivas de OpenMp al código secuencial, además del archivo de cabecera <omp.h>. #include <math.h> #include <time.h> #include <stdlib.h> #include <iostream.h> #include <stdio.h> #include <omp.h> #include <omp.h> const int N=1000; const int mMax=1000; //máximo número de vectores de peso const double RO=0.1; const double ALFA=0.3; const long kMax=1000; int numproc=omp_get_num_procs(); void leerU(double * u,int N, int j); void normalizaU(double * I,double * U,int N); void muestradatos(double * U,int N); void inicia_matriz(double**, int n, int *m); void multiplicaWI(double **W,double *I, int m,int N, double *v); double maximo(double *v, int m,int *j); void incorporaW(double **W, double *I, int *m, int N); 85 void actualizaW(double **W, double *I,int N,int j); int main(){ time_t hra1,hra2; hra1=time(NULL); clock_t inicio_cpu = clock(); double u[N]; //vector con los datos leidos de la BD double I[N]; //vector nomalizado double *W[mMax]; //matriz de los vectores de pesos acumulados double *w; //vector de pesos double *v=new double[mMax]; double valor; int m=0; //cantidad de vectores de pesos al momento int k=0; //contador del numero de registro en la BD int j=0; //contador de vector de peso que se //esta analizando actualmente en la matriz W int i=0; //contador de elementos por vector srand(1); inicia_matriz(W,N,&m); while (k<kMax){ //ciclo principal de iteracion leerU(u,N,k); //printf("U:");muestradatos(u,N); normalizaU(I,u,N); //multiplica W por I para obtener v multiplicaWI(W,I,m,N,v); //printf("U:");muestradatos(u,N); 86 valor=maximo(v,m,&j); // printf("valor mayor= %f",valor); if (RO>=valor){ //incorpora I a W incorporaW(W,I,&m,N); } else{ //actualiza w[j] actualizaW(W,I,N,j); } k++; } //termina ciclo principal de iteracion printf("\nDatos de la matriz W:\n"); for (i=0; i<m; i++) muestradatos(W[i],N); hra2=time(NULL); printf("\nTiempo %i segundos\n", (long)difftime(hra2,hra1)); clock_t fin_cpu = clock(); printf("\nTiempo %f segundos\n", difftime(hra2,hra1)); printf("\nTiempo de inicio_cpu))/CLOCKS_PER_SEC); cpu %f segundos\n",((double)(fin_cpu - printf("\nnumero de renglones de la matriz %i\n",m); printf("\n numero de procesadores en mi CPU %d \n", numproc); return 0; } void leerU(double * u,int N, int j){ //estos serian los valores que se leen //de la base de datos aqui iria el codigo //para leer la base de datos //por ahora se rellenan aleatoriamente 87 //con numeros entre -100 y 100 for (int i=0; i<N; i++){ u[i]=-100+200*rand()/RAND_MAX; } //aqui los valores de la base de datos se //tendrian referenciados por el apuntador u } void normalizaU(double *I,double * u,int N){ double sumatoria=0; double norma; int i; for (i=0; i<N; i++){ sumatoria+=u[i]*u[i]; } norma=sqrt(sumatoria); if (norma>0.0) //evitar divisiones por cero for (i=0; i<N; i++){ I[i]=u[i]/norma; } } void muestradatos(double * U,int N) { printf("["); for (int i=0; i<N; i++) printf("%5.2f, ", U[i]); printf("]\n"); } void inicia_matriz(double** W, int N, int *m) { 88 int j=0; double *I=new double[N]; W[0]=new double[N]; //llena el primer renglon de W con datos //aleatorios entre -1 y +1 for (j=0; j<N; j++){ W[0][j]=-1+2*rand()/(double)RAND_MAX; } //normaliza el primer renglon de W para //que se cumpla la condicion de ||w|| normalizaU(I,W[0],N); for(j=0; j<N; j++){ W[0][j]=I[j]; } (*m)++; delete [] I; } void multiplicaWI(double **W,double *I, int m,int N, double *v) { int i,j; double sumWI; Omp_set_num_threads(numproc) #pragma omp parallel for for(i=0; i<m; i++) { sumWI=0; for(j=0; j<N; j++) sumWI+=W[i][j]*I[j]; 89 v[i]=sumWI; } } double maximo(double *v, int m, int *j) { double dato1, mayor; int i; if (m==1){ mayor=v[0]; *j=0; } else if (m >1){ i=0; mayor=v[i]; while(i<m){ dato1=v[i]; //dato2=v[i+1]; if (dato1>mayor){ mayor=dato1; *j=i; } i=i+1; } } else if (m<0) printf("No hay datos"); return mayor; } double mayor(double *v, int m, int *pos) { 90 double mayor; mayor=v[0]; *pos=0; for (int j=0; j<m;j++) { if (v[j]>mayor){ mayor=v[j]; *pos=j; } } return mayor; } void incorporaW(double **W, double *I, int *m,int N){ W[*m]=new double[N]; for (int j=0; j<N;j++) W[*m][j]=I[j]; (*m)++; } void actualizaW(double **W, double *I,int N,int pos) { int k; for (k=0;k<N;k++){ W[pos][k]=ALFA*W[pos][k]+(1-ALFA)*I[k]; } } 91 REFERENCIAS BIBLIOGRAFICAS: [1] Strouptrup Bjarne, “The C++ Programming Language”. 2 nd edition, Addison Wesley, 1991. [2] Chandra Rohit, Dagum Leonardo. “Parallel Programming in OpenMP”,MORGAN KAUFMANN PUBLISHERS, 2001. [3 ] Lastovetsky Alexey L., “Parallel Computing on Heterogeneous Networks”. University College, Dubln [4] Ireland, WILEY-INTERSCIENCE, 2003. Norton Scott J., Dipasquale Mark D., “The Multithreaded Programming Guide”. Thread Time. PRENTICE HALL PTR, 1997. [5] Lewis Bil, Berg Daniel J., “A Guide to Multithreaded Programing”. Threads Primer. PRENTICE HALL, 1996. [6] Beveridge Jim and Wiener Robert, “Multithreading Applications in Win32” The complete Guide to Threads. Addison-Wesley, 2001. [7] J. Quinn Michael, “Parallel Programming in C with MPI and OpenMP”, Mc Graw-Hill, 2003. 92 [8] Intel Corporation. “Intel Core 2 Duo Processor on 45-nm Proccess Datasheet. Intel Corporation www.intel.com, 2008. [9] Intel Corporation. “Intel VTune Performance Analyzer Documentation”. www.intel.com, 2008. [10] www.openmp.org [11] Dong Hyuk Woo and Hsien-|Hsin S. Lee, “Extending Amdahl’s Law for Energy-Efficent Computing in the Many-Core Era”, Published by the IEEE Computer Society, December 2008. [12] Dan Quilan, Markus Schordan, Qin Yi, and Bronis R. de Supinski. A C++ Infrastructure for Automatic Introduction and Translation of OpenMP Directives. WOWPAT, Berlin Heidelberg, 2003 . [13] Ref. dirección de mpi.org. [14] www.mpi.org (página official de MPI). [15] Moisen, M.C. Benítez-Pérez, H. Medina L. Ultrasonic (XXXX) NDT for flaws characterisation using ARTMAP network and wavelet analysis. Int. J. Materials and Product Technology. [16] Carpenter, G.A., Grossberg, S., Markuzon, N., Reynolds, J.H. and Rosen, D.B. (1992). Fuzzy ARTMAP. A neural network architecture for 93 incremental supervised learning of analog multidimensional maps, IEE Trans. Neural Network, Vol. 3, pp. 698 713. [17] Frank, T., Kraiss, K.F. and Kuhlen, T. (1998) Comparative analysis of fuzzy ART and ART-2A network clustering performance, IEEE Trans. Neural Network, vol. 9, pp. 544 559. [18] Carretero Pérez Jesús, Anasagasti Pedro de Miguel, Sistemas operativos, una visión aplicada, Mc Graw Hill, 2001 [19] M. Morris Mano, Arquitectura de computadoras, Prentice Hall, 2001 [20] Von Neumann Architecture. Página Web. http://www.hostgold.com.br/hospedagem-sites/o_que_e/vonNeumann+architecture/+. [21] Tanenbaum. S. Andrew, Distributed Operating Systems, Prentice Hall 1996 [22] Bruce P. Lester, The Art of Parallel programming, Prentice Hall, 1993. [23] Alan H. Karp and Horace P. Flatt, Measuring Parallel Processor Performance, may 1990 vol 33 num 5. [24] Butenhof R. David, Programming with POSIX Threads, ADDISONWESLEY PROFESSIONAL COMPUTING SERIES, May 2000 [25] Lamport Leslie, Specifying Systems, Addison-Wesley Professional, 2002 [26] Rohit Chandra-Dagum Leonardo, Parallel Programming in OpenMP, MORGAN KAUFMANN PUBLISHERS, 2001. 94 [27] http://www-unix.mcs.anl.gov/mpi/mpich/. [28] ftp://ftp.mcs.anl.gov/pub/mpi/mpich.tar.gz. [29] www.mcs.anl.gov/mpi/mpich1 [30] Gail A. Carpenter and Stephen Grossberg, Pattern Recognition by SelfOrganizing Neuronal Networks. 1991. [31] CARPENTER and Stephen GROSSBERG, NEURONAL NETWORKS FOR VISION AND IMAGE PROCESSING, 1992, Edited by Gail A. 95