Proyecto de grado
Concurrencia y alto desempeño
Yoann Lecuyer1, Rafael Gómez2, Andrés González Mancera3
1 Estudiante de pregrado del departamento de ingeniería de sistemas y computación de la Universidad de Los Andes (doble titulación con Ecole Nationale Supérieure des Mines de SaintEtienne, ICM), [email protected]
2 Profesor asociado de ingeniería de sistemas y computación de la Universidad de Los Andes, [email protected]
Resumen
En el departamento de ingeniería mecánica de la Universidad de Los Andes desarrolló se desarrollaron varias simulaciones de flujo basadas sobre el método dicho de LatticeBoltzmann. Se logró tener prototipos funcionando para diferentes situaciones sin embargo estos prototipos son muy lentos y eso no le permite sacar los resultados que se ecesitan en un tiempo razonable. Por eso, este proyecto intenta mejorar el desempeño de esos prototipos usando la potencia de la GPU a través de la paralelización del problema sobre los datos. Usando el framework CUDA de nVidia, se mejoró el desempeño en términos de velocidad de esos algoritmos de un orden de magnitud de un factor 2.5 en el peor de los casos y de un factor 18 en el mejor de los casos. Ahora, esos resultados permiten sacar más datos de esas simulaciones para los estudios que usan esos prototipos.Résumé
Dans le département d'ingénierie mécanique de l'Université de Los Andes (Bogotá, Colombie), plusieurs simulations de fluides basées sur la méthode de LatticeBoltzmann ont été dévelopées. Il est possible de les faire fonctionner dans différentes situations cependant ils sont très lents et ne permettent pas d'obtenir des résultats en un temps raisonnable. L'objectif de ce projet est donc d'améliorer les performances de ces différents prototypes notamment en utilisant la puissance de calcul de la GPU et en parallélisant le problème au niveau des données. Grâce au framework CUDA de nVidia, le gain de performance a été d'un facteur 2.5 dans le pire des cas et d'un facteur 18 dans le meilleur des cas. Maintenant, il est possible d'utiliser ces prototypes et d'obtenir de bons résultats en un temps raisonnable.Agradecimientos
Primero quiero agradecer a Rafael Gómez (Profesor asociado de ingeniería de sistemas y computación) por su asesoría a lo largo del proyecto.
También quiero agradecer a Andrés González Mancera (Profesor asistente de ingeniería mecánica) por proponer los códigos de sus proyectos con los cuales podíamos trabajar y por su ayuda en la evaluación de los resultados.
Finalmente, gracias a Julian Urán (Admonsis) por el préstamo de la máquina de desarrollo y su soporte técnico a lo largo del proyecto.
Índice de contenido
Resumen...2 Résumé...2 Agradecimientos...2 Introducción...4 Descripción general...4 Objetivos...4 Cronograma...5 Antecedentes...5 Contexto...6 Definición del problema...6 El método LatticeBoltzmann...6 Calcular las variables físicas...6 Propagación...7 Colisiones...8 La paralelización con la GPU...8 Paralelización...11 Primer prototipo...11 Segundo prototipo...12 Tercer prototipo...13 Resultados...15 Primer prototipo...15 Segundo prototipo...16 Tercer prototipo...17 Validación...18 Conclusiones...18 Trabajo futuro...19 Referencias...19 Apéndices...20 Apéndice A...20 Caracteristicas del CPU...20 Caracteristicas de la memoria...22 Caracteristicas de la GPU...23 Apéndice B...24 Códigos...24 Prototipo 1...24 Prototipo 2...24 Prototipo 3...24Introducción
La ley de Moore nos dice que mientras va pasando el tiempo más poderosos van a ser los computadores. Eso permitió tener una potencia de cálculo cada vez mayor a lo largo de los años y así lograr resolver problemas cada vez más complejos. Sin embargo, hoy en día, las leyes de la física están poniendo límites a esta ley así que algunos expertos dicen que desaparecerá dentro de unos años. No obstante, los problemas de hoy también son más complejos en términos de algoritmos y de manejo de datos, por eso, se desarrolló la programación paralela para seguir trabajando con problemas aún más complejos de manera efectiva. Entre las soluciones propuestas, encontramos CUDA un framework de nVidia que permite usar los múltiples procesadores que tienen las tarjetas gráficas para hacer computación paralela.En este proyecto, se usa el framework CUDA para mejorar el desempeño de programas que solucionan problemas reales. Los programas que se trabajaron en este proyecto los propuso Andrés González Mancera ([email protected]) profesor asistente del departamento de ingeniería mecánica de la Universidad de Los Andes. Esos programas son simulaciones de flujos basadas en el método LatticeBoltzmann y cada uno trata un problema diferente. Sin embargo, al ser secuenciales se demoran un tiempo nototio para sacar los resultados esperados. El algoritmo LatticeBoltzmann es interesante por lo que está basado en una arquitectura de cuadrícula lo que lo convierte en un buen candidato para ser paralelizado a través del framework CUDA.
A continuación, haré una descripción general del problema explicando los objetivos del proyecto y una revisión de los trabajos relacionados. Luego, explicaré el método LatticeBoltzmann y como se paraleliza con el framework CUDA. Eso me permitirá presentar los resultados obtenidos y sus evaluaciones respectivas. Finalmente concluiré sobre el uso de CUDA para mejorar el desempeño del método LatticeBoltzmann.
Descripción general
Objetivos
El objetivo principal del proyecto es trabajar con el código de los prototipos de simulaciones de fluidos propuestos por Andrés González Mancera con el fin de mejorar el desempeño. Por lo menos, se desea lograr un mejoramiento del 50% de los tiempos de referencias. Por supuesto, los resultados deben ser iguales o al menos equivalentes a los obtenidos con los prototipos originales. Además de este objetivo principal existen otros objetivos específicos para Andrés González Mancera y para mí. Andrés quiere mostrar que se puede ganar en desempeño con el uso de la GPU y quiere tener ejemplos de códigos para poder seguir implementado la paralelización con CUDA a sus futuros prototipos. En lo que me concierne, mis objetivos fueron aprender a usar el framework CUDA o más generalmente la paralelización de manera eficiente y trabajar con otras ingenierías que no están tan familiarizadas con el área de sistemas y computación.Cronograma
El desarrollo de este proyecto ha sido planeado según el cronograma siguiente: Tuve reuniones semanales con mi asesor para revisar el trabajo hecho durante la semana previa y para socializar dudas o problemas técnicos. También se definían las metas para la siguiente semana con respecto al cronograma.Antecedentes
Desde que CUDA existe muchos estudios se desarrollaron para paralelizar algoritmos existentes y estudiar el comportamiento. Así existe mucha documentación relacionada con este tema y dentro los artículos existentes algunos se centran sobre el algoritmo LatticeBoltzmann. Por ejemplo, Rinaldi, Dari, Vénere y Clausse (2012) desarrollaron un nuevo algoritmo basado sobre el método Lattice Boltzmann que permite incrementar el número de celdas actualizadas por segundos lo que implica un mejoramiento en el desempeño del algoritmo. De igual manera, Ye y Li (2013) desarrollaron una variante del algoritmo LatticeBoltzmann y lograron tener un mejoramiento de la velocidad de un factor 3.14 con la implementación usando CUDA con respecto a la implementación basada sobre un solo CPU. Otros estudios como el de Rosales (2011) y el de Li et al. (2012) mostraron que al usar varias GPU se puede ganar aún más en términos de tiempo de ejecución.También es importante destacar el trabajo previo de Andrés González Mancera ([email protected]) profesor asistente del departamento de ingeniería mecánica de la Universidad de Los Andes. El hizo tres prototipos de simulación de flujos, basados sobre el método LatticeBoltzmann, que nos entregó para desarrollar este proyecto. El primer prototipo es una simulación de flujo externo sobre estructuras de gran tamaño, esta escrito en C++ de manera secuencial. El segundo prototipo es la misma simulación de flujo a la cual se le agregó modelos de glóbulos rojos en suspensión. Igual está escrito en C++ de manera secuencial. Finalmente, el último
prototipo es un simulación de flujos de múltiples componentes. No obstante, este código esta escrito de manera secuencial en python con el uso de la librería numpy. Esos algoritmos han sido desarrollados según la metodología disponible en libro de Sukop y Thorne (2006).
Contexto
Definición del problema
En este proyecto se trabajaron tres prototipos distintos, una simulación de flujo externo sobre estructuras de gran tamaño, una simulación de glóbulos rojos en suspensión y finalmente una simulación de flujo de múltiples componentes. El primer código y el segundo son relacionados en el sentido que los glóbulos rojos de la segunda simulación están en suspensión en el mismo flujo que el de la primera simulación. Estos códigos están escritos en C++, se manejan los parámetros de la simulación directamente dentro el código y el resultado es un conjunto de archivos VTK que se pueden leer con ParaView (ParaView, n.d) para tener una animación de la simulación. El tercer código al contrario está escrito en python y el resultado es un conjunto de imágenes representando datos de interés de la simulación.El método LatticeBoltzmann
El método LatticeBoltzmann permite hacer una simulación discreta de las ecuaciones de la dinámica de fluidos. Es un buen candidato para ser paralelizado dado que la arquitectura del algoritmo está basada en una cuadrícula y cómo lo vamos a ver con el pseudocódigo en seguida, las etapas del algoritmo hacen cálculos sobre cada cuadros que son independientes de sus vecinos y de los demás cuadros. El fluido está representado a través de la cuadrícula en la cual cada cuadro representa unas partículas con respecto a una dirección de movimiento. Como lo explican Sukop y Thorne (2006), el algoritmo básico tiene tres fases claves: cálculo de las variables físicas, propagación y colisión. Esas tres fases hacen cálculos con los valores de cada cuadro y por eso son los que requieren más tiempo de procesamiento. Así, esas tres fases son las que se van a paralelizar con CUDA.Calcular las variables físicas
Para cada cuadro de la cuadrícula, se calcula independientemente de la posición, la suma de los valores presentes en cada dirección para después calcular las variables físicas:for i in [0..Xmax] for j in [0..Ymax] for k in [0..Zmax] rho = 0 u = 0 for l in [0..19] f = cell[i][j][k][l] rho+= f u = f*e[l] end for calcular_velocidad(i,j,k,rho,u) end for end for end for
Propagación
Sigue la parte de propagación que simula el movimiento de los átomos dentro el fluido. Como el fluido se representa a través de una cuadrícula, los átomos se mueven de un cuadro a otro según la dirección del movimiento que tienen. En esta etapa también se aplican las condiciones de bordes como el rebote, la periodicidad, la velocidad... temp_cell = copiar(cell) for i in [0..Xmax] for j in [0..Ymax] for k in [0..Zmax] for l in [0..19] a = i b = j c = k d = l aplicar_condiciones_de_frontera(a,b,c,d,i,j,k,l) temp_cell[i][j][k][l] = cell[a][b][c][d] end for end for end for end for cambiar(temp_cell, cell)Colisiones
Luego del movimiento de los átomos, viene la gestión de colisiones y del equilibrio. Una vez completada esta etapa se puede hacer una nueva iteración. for i in [0..Xmax] for j in [0..Ymax] for k in [0..Zmax] for l in [0..19] feq = calcular_equilibrio(i, j, k, l) cell[i][j][k][l] = C*(cell[i][j][k][l] – feq) + K end for end for end for end for
La paralelización con la GPU
La GPU (Graphics Processing Unit) es un componente material que se encuentra ubicado sobre las tarjetas gráficas de los computadores. Tiene una arquitectura multi core que se usa generalmente para representar escenas 3D como las de los videojuegos. Sin embargo, se empezó a usarlas para hacer otras operaciones más generales llamadas GPGPU (GeneralPurpose Computing on Graphics Processing Units). Para facilitar el desarrollo de GPGPU, los constructores de GPU han creado frameworks, entre ellos está CUDA (Compute Unified Device Architecture) del fabricante nVidia. En este proyecto se usó CUDA con una máquina de desarrollo equipada con una tarjeta gráfica nVidia GeForce GTX 480. Al momento de desarrollar con una GPU es importante tomar en cuenta unos parámetros claves. Para introducirlos, miramos las características de la tarjeta gráfica usada en este proyecto: $ ./deviceQuery ./deviceQuery Starting... CUDA Device Query (Runtime API) version (CUDART static linking) Detected 1 CUDA Capable device(s) Device 0: "GeForce GTX 480" CUDA Driver Version / Runtime Version 5.5 / 5.5 CUDA Capability Major/Minor version number: 2.0 Total amount of global memory: 1535 MBytes (1609760768 bytes) (15) Multiprocessors, ( 32) CUDA Cores/MP: 480 CUDA Cores GPU Clock rate: 1401 MHz (1.40 GHz) Memory Clock rate: 1848 Mhz Memory Bus Width: 384bitL2 Cache Size: 786432 bytes Maximum Texture Dimension Size (x,y,z) 1D=(65536), 2D=(65536, 65535), 3D=(2048, 2048, 2048) Maximum Layered 1D Texture Size, (num) layers 1D=(16384), 2048 layers Maximum Layered 2D Texture Size, (num) layers 2D=(16384, 16384), 2048 layers Total amount of constant memory: 65536 bytes Total amount of shared memory per block: 49152 bytes Total number of registers available per block: 32768 Warp size: 32 Maximum number of threads per multiprocessor: 1536 Maximum number of threads per block: 1024 Max dimension size of a thread block (x,y,z): (1024, 1024, 64) Max dimension size of a grid size (x,y,z): (65535, 65535, 65535) Maximum memory pitch: 2147483647 bytes Texture alignment: 512 bytes Concurrent copy and kernel execution: Yes with 1 copy engine(s) Run time limit on kernels: No Integrated GPU sharing Host Memory: No Support host pagelocked memory mapping: Yes Alignment requirement for Surfaces: Yes Device has ECC support: Disabled Device supports Unified Addressing (UVA): No Device PCI Bus ID / PCI location ID: 1 / 0 Compute Mode: < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) > deviceQuery, CUDA Driver = CUDART, CUDA Driver Version = 5.5, CUDA Runtime Version = 5.5, NumDevs = 1, Device0 = GeForce GTX 480 Result = PASS De la salida de este programa se puede destacar estos parámetros: número de hilos de ejecución por bloques (Threads per blocks) y el tamaño máximo de un bloque (Block).
Un hielo de ejecución es la parte donde se va a ejecutar el código, con esta tarjeta gráfica, se pueden ejecutar simultáneamente 1024 hielos de ejecución por bloque. Para poder usar más hielos de ejecución, se puede definir varios bloques. Sin embargo, lo bloques no pueden ejecutarse al mismo tiempo y van a ejecutarse de manera secuencial. Entonces, es de vital importancia configurar bien la ejecución de CUDA para que todo el dominio sea tomado en cuenta. Además, la arquitectura de CUDA permite organizar los bloques y los hielos de ejecución en un espacio 1D, 2D o 3D. Está organización es conveniente dado que nos permite cambiar bucles de este estilo: for x in [0..Xmax]: for y in [0..Ymax]: for z in [0..Zmax]: tarea(x,y,z); A un código que se va ejecutar en la GPU de este estilo: CPU: ejecutar_tarea<<dim_grid, dim_bloque>>(); GPU: __global__ void tarea() { int x = blockIdx.x*blockDim.x + threadIdx.x; int y = blockIdx.y*blockDim.y + threadIdx.y; int z = blockIdx.z*blockDim.z + threadIdx.z; } Esta estructura es la que encontramos en el código del algoritmo LatticeBoltzmann y es por eso que este algoritmo es un buen candidato para ser paralelizado con CUDA. Podemos ver en el pseudo código que además de llamar la función para que se ejecute desde la GPU, hay que definir el número de bloques (dim_grid) que van a ser usados y el tamaño de esos bloques (dim_bloque). El manejo de esos parámetros es lo que se llama tuning y es importante cambiar esos parámetros para tener las dimensiones muy parecidas a las de los problemas para no perder en eficiencia. Pero también hay que tener en cuenta las limitaciones de la GPU para quedarse dentro la configuración autorizada. En efecto, vimos que la GPU usada para desarrollar este proyecto puede correr en paralelo 1024 hielos de ejecución, para correr más hielos de ejecución, se debe organizarlos en bloques que se van a ejecutar de manja secuencial. Dado la arquitectura del algoritmo LatticeBoltzmann, se usan los hielos de ejecución para hacer operaciones sobre un cuadro de la cuadrícula base del modelo. Así, si el dominio es de un tamaño X ×Y ×Z , para hacer los cálculos sobre cada cuadro se debe tomar conjuntos de cuadros de tal manera que el total de cuadros sea inferior que 1024. Por ejemplo, para un tamaño 21×21×21=9261 se puede tomar subconjuntos de tamaño 10×10×10=1000 y así recoger toda la cuadrícula. Sin embargo, hay que prestar atención a las fronteras. En efecto, es
importante escribir una condición para asegurarse que los cálculos que se van a hacer no van a salir de la cuadrícula dado que el subconjunto que se usa tiene más puntos que lo que se necesita.
Paralelización
Primer prototipo
El primer prototipo con el cual trabajé es una simulación de flujo externo sobre estructuras de gran tamaño. Dicho de otra manera tenemos un fluido entre dos paredes que van moviéndose hacia direcciones diferentes y se desea estudiar el comportamiento del fluido. Con este prototipo se paralelizó las tres fases descritas en la explicación del método Lattice Boltzmann. Para lograrlo se necesitó hacer unos cambios a la arquitectura general del programa. Por ejemplo los valores decimales eran del tipo double pero la GPU usada durante este proyecto sólo podían manejar variables de tipo float. Igualmente, los arreglos eran definidos con apuntadores a apuntadores. Dicho de otra manera, había un arreglo de apuntadores apuntando a las filas que estaban apuntando a las columnas que estaban apuntando a la profundidad... No obstante, es difícil de mandar los datos así a la GPU a través de la función cudaMemCpy de CUDA parecida a la función memcpy de la biblioteca C estándar. En efecto, los datos no están enseguida en la memoria y no se puede copiar el bloque de datos de una vez. Así se hizo un cambio de todos los arreglos de dimensión N a arreglos de dimensión 1 y para acceder a los datos de esos arreglos de una dimensión se crearon funciones de soporte con el fin de facilitar el proceso. Esta etapa permitió mejorar el desempeño de manera significante pero nos dimos cuenta gracias al profiler de CUDA que se podía mejorar aún más el desempeño cambiando el manejo de los datos y de la memoria. En efecto, al principio se mandaban los datos desde la CPU hasta la GPU, se hacían los cálculos y se bajaban los datos de la GPU hasta la CPU para seguir el procesamiento. Esta transferencia de datosentre la CPU y la GPU es muy costosa en términos de tiempo de ejecución y para mitigar esto se cambió la manera de guardar los datos en la memoria. Así, para mejorar el desempeño, se mandan los datos de la simulación a la GPU antes de empezar y se guardan dentro la GPU todo el tiempo durante la simulación. Solo se bajan los datos de la GPU cuando la CPU los necesita para guardarlos dentro los archivos VTK. Finalmente, se hizo la etapa de tuning para tener el mejor desempeño posible.
Como lo vimos anteriormente, es importante armar bloques para cubrir la totalidad de la cuadricula y deben ser tal que no tengan más que 1024 hielos de ejecución para nuestra GPU. También, vimos que es importante considerar las fronteras dado que se puede tener un bloque con la mayoría de los hielos de ejecución sin actividad. Por eso, la etapa de tunning es importante ya que intenta buscar la mejor forma del subconjunto que nos permite sacar los mejor resultados. Para hacer el tunning de este prototipo, escribé un codigo en java que va calculando todos los bloques posibles a×b×c y sus ocupaciónes según los ejes tal que a×b×c≤1024 . Luego se ordenan según el numero de hielos de ejecución que contiene el bloque de tal manera que se pueda encontrar la configuración del bloque que maximisa el numero de hielos de ejecución y la ocupación.
Segundo prototipo
En el segundo prototipo, se usa como base la simulación de flujo sobre estructuras de gran tamaño. A esto, se suma modelos de glóbulos rojos con una posición fija pero con una membrana deformable. A través de métodos de elementos finitos, se calcula la interacción del fluido con respecto a la membrana de cada glóbulo rojo presente. Dado que este código usa la misma base que el primer prototipo se usó el código de este para empezar el proceso de paralelización. La segunda fase del proceso de paralelización se hizo a cerca de la interacción entre los modelos 3D y el flujo. Los modelos 3D son constituidos por nodos y para cada uno de los nodos se debe calcular la interacción con el fluido. Así, se paralelizó el código sobre el recorrido de los nodos de los modelos. También se hizo la etapa de tuning para la parte paralelizada del fluido y la parte paralelizada de los modelos. Esa etapa siguió la misma metodología que para el prototipo anterior dado que son muy parecidos.Tercer prototipo
El tercer prototipo es una simulación de flujo de múltiples componentes, está hecho en python usando la librería numpy. Este prototipo es aún más importante para nosotros dado que para científicos sin conocimientos avanzados en programación, python es un lenguaje que permite hacer prototipos de manera sencilla. En efecto, además de tener una sintaxis sencilla, es un lenguaje orientado a objetos interpretado sin ser fuertemente tipado. Con respecto al ambiente de desarrollo, python también cuenta con muchas librerías de uso sencillo para la programación científica como numpy. En lugar de convertir el código python a C++ para después paralelizar lo sobre CUDA se usó la librería NumbaPro de la distribución de python Anaconda (Anaconda, n.d.). Esta librería permite usar la potencia de CUDA desde python a través de un proceso de tipo JIT (Just In Time). Además, tiene la ventaja de ser compatible con numpy lo que permite usarla con el prototipo existente y los futuros sin tener que hacer muchos cambios al código ni tener que aprender una nueva librería o lenguaje de programación. Dado el uso intensivo de la librería numpy, el código de este prototipo no tiene como tal una arquitectura hecha para ser paralelizada. En efecto, no tiene bucles que van recorriendo todos los cuadros del modelo dado que estos bucles están escondidos dentro de las funciones de numpy. Hice pruebas para ver si era viable escribir sus propias funciones con CUDA en lugar de usarlas definidas dentro numpy. El código usado es lo siguiente:Los resultados son los siguientes: Numpy CUDA 0.38302 ±0.00217s 0.92327 ±0.00363s Las funciones de numpy se ejecutan más rápidamente que el equivalente CUDA. Así, en términos prácticos y de desempeño es mejor usar las funciones de la librería numpy que intentar escribir sus propias funciones parecidas con CUDA. Para ganar en desempeño con este prototipo, se paralelizó la inicialización de los datos que sí tiene una arquitectura adecuada para ser paralelizada. También, se hizó un refactoring de unas parte del código que habían sido escritas desde cero aunque existen funciones muy parecidas dentro la librería numpy que tienen un mejor desempeño.
Resultados
Las medidas de desempeño se midieron sobre una máquina Intel DualCore equipada de una tarjeta gráfica nVidia GeForce GTX 480. Para mayores informaciones sobre las características del hardware ver Apéndice A. Las mediciones de tiempo se hicieron con el comando time de unix corriendo el programa varias veces para evitar los resultados aberrantes.
Primer prototipo
El dominio usado para sacar los resultados fue X = 51 Y = 51 Z = 51 Los parametros para llamar a los kernels de CUDA (archivo cuda_settings.h) fueron:Se ejecutó la prueba con el flag de optimización (03) y sin usar el flag de debug (g) varias veces para evitar resultados aberantes. Secuencial Paralelizado 69.85 ±0.02s 3.56311 ±0.05142s Podemos ver con los resultados que en este caso, se logro una mejora en un factor de 18 en términos de tiempo de ejecución.
Segundo prototipo
El dominio usado para sacar los resultados fue: X = 51 Y = 51 Z = 51 También estaban dos glóbulos rojos posicionados de tal manera: X = 13 Y = 25 Z = 35 para el primer glóbulo rojo y X = 30 Y = 25 Z = 15 Los parametros para llamar a los kernels de CUDA (archivo cuda_settings.h) fueron:Y para las membranas: Se ejecutó la prueba con el flag de optimización (03) y sin usar el flag de debug (g) varias veces para evitar resultados aberantes. Secuencial Paralelizado 627.042 ±2.25746s 75.74167 ±0.20683s Podemos ver con los resultados que en este caso, se logro una mejora en un factor de 8 en términos de tiempo de ejecución.
Tercer prototipo
Este prototipo se usó con los parámetros definidos en el programa original. Sin embargo, como no se podía usar la GPU para hacer cálculos y mostrar imágenes sobre la pantalla al mismo tiempo, se desactivó la posibilidad de mostrar la imagen resultante mientras estaba corriendo el programa. Secuencial Paralelizado 1476.22 ±4.05151s 604.54333 ±1.19396s Aunque no se pudo paralelizar el bucle principal del programa, podemos ver que solo con el refactoring del código que consumaba más tiempo, se pudo lograr un mejoramiento en tiempo de ejecución en unfactor de 2.5. Luego de ver esos resultados, intenté cambiar las funciones de numpy por mi propio código que permitía paralelizar el bucle principal. Sin embargo, al hacer pruebas sobre mi código, me di cuenta que las funciones de numpy son más eficientes que el mismo código corriendo sobre la GPU. Además, escribir sus propias funciones en lugar de usar las de numpy quita la ventaja que tiene NumbaPro de tener compatibilidad con numpy y quita la simplicidad de python dado que se necesita escribir sus funciones.
Validación
Los resultados descritos en este documento se validaron en dos etapas. Primero al mismo tiempo que se estaba trabajando el prototipo se guardaba una copia de los resultados del programa original para tener los resultados de referencia. Una vez el prototipo paralelizado, se calculaban los resultados y se hacía una comparación con respecto a los resultados de referencia. Para lograr esta comparación, se usó numdiff que permite, entre otros usos, comparar los valores que conforman los archivos VTK. Este programa permite decir si los datos tienen una diferencia absoluta y/o relativa más importante que un umbral dado. Luego de esta primera validación, Andrés González Mancera miró los resultados del programa paralelizado y confirmó la consistencia de los resultados. Usando esta metodología, se validaron los resultados de cada prototipo trabajado durante este proyecto.Conclusiones
En conclusión, se comprobó a través de este proyecto que se gana en desempeño si se paraleliza con CUDA el algoritmo de LatticeBoltzmann. Sin embargo, los mejores resultados se lograron con los
Grafico 1: Tiempo de ejecución en segundos
Prototipo 1 Prototipo 2 Prototipo 3 0 200 400 600 800 1000 1200 1400 1600 secuencial paralelizado
códigos escritos en C++ dado que la arquitectura del código es perfecta para que sea paralelizada. Sin embargo, era obligatorio aplicar cambios sobre las estructuras de datos y los tipos de datos para lograr la paralelización. Eso hace que el proceso de paralelización requiere conocimientos avanzados en programación para lograrlo. Al contrario, el lenguaje python con el framework NumbaPro de la distribución Anaconda (Anaconda, n.d.) permite simplificar este proceso de paralelización con un sistema de compilación JIT y su compatibilidad con la biblioteca numpy. Pero, aunque los científicos estén acostumbrados a escribir prototipos en python con numpy, la estructura de un código usando numpy no es un buen candidato para ser paralelizado. En efecto, con numpy no se hacen bucles embebidos, sino que se usan funciones que manejan los bucles por el usuario. Como vimos con el tercer prototipo, se puede escribir su propio código para hacer que el código se pueda paralelizar. No obstante, así se pierde la calidad y la simplicidad de la librería numpy y en este caso me parece que es mejor escribir el código en C++.
Trabajo futuro
En mi opinión veo una oportunidad muy buena de poder usar el framework CUDA a través de python. Esto permite a las personas que no saben mucho de programación de hacer prototipos paralelizados de manera sencilla. Sin embargo, como lo vimos en este proyecto, la paralelización de códigos usando mucho numpy no es posible pero sería bueno investigar otros tipos de códigos que sí se pueden paralelizar bien desde python, como, por ejemplo, códigos de tratamiento de imágenes, de exploración de grafos y de manipulación de big data.Referencias
Anaconda. (n.d.). Python Visualization and Data Exploration. Retrieved November 13, 2013, from https://store.continuum.io/cshop/anaconda/Kirk, David, and Wen Hwu. Programming massively parallel processors: a handson approach. Burlington, MA: Morgan Kaufmann Publishers, 2010. Print. Li, Q., Zhong, C., Li, K., Zhang, G., Lu, X., Zhang, Q., Zhao, K., and Chu, X. (2012). Implementation of a lattice boltzmann method for large eddy simulation on multiple gpus. In Proceedings of the 2012 IEEE 14th International Conference on High Performance Computing and Communication & 2012 IEEE 9th International Conference on Embedded Software and Systems, HPCC '12, pages 818823, Washington, DC, USA. IEEE Computer Society.
ParaView Open Source Scientific Visualization. (n.d.). ParaView Open Source Scientific Visualization. Retrieved November 13, 2013, from http://www.paraview.org/
Sanders, Jason, and Edward Kandrot. CUDA by example: an introduction to generalpurpose GPU programming. Upper Saddle River, NJ: AddisonWesley, 2011. Print.
Sukop, M. C., & Thorne, D. T. (2006). Lattice Boltzmann modeling an introduction for geoscientists and engineers. Berlin: Springer.
Rinaldi, P., Dari, E., Vénere, M., and Clausse, A. (2012). A latticeboltzmann solver for 3d fluid simulation on gpu. Simulation Modelling Practice and Theory, 25(0):163 – 171.
Rosales, C. (2011). Multiphase lbm distributed over multiple gpus. In Proceedings of the 2011 IEEE
International Conference on Cluster Computing, CLUSTER '11, pages 17, Washington, DC, USA.
IEEE Computer Society.
Ye, Y. and Li, K. (2013). Entropic lattice boltzmann method based high reynolds number flow simulation using cuda on gpu. Computers & Fluids, 88(0):241 – 249.
Apéndices
Apéndice A
Caracteristicas del CPU
$ cat /proc/cpuinfo processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Core(TM)2 CPU 6300 @ 1.86GHz stepping : 2 microcode : 0x5d cpu Mhz : 1596.000 cache size : 2048 KB physical id : 0 siblings : 2 core id : 0 cpu cores : 2 apicid : 0 initial apicid : 0 fdiv_bug : no hlt_bug : no f00f_bug : no coma_bug : no fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe nx lm constant_tsc arch_perfmon pebs bts aperfmperf pni dtes64monitor ds_cpl vmx est tm2 ssse3 cx16 xtpr pdcm lahf_lm dtherm tpr_shadow bogomips : 3723.99 clflush size : 64 cache_alignment : 64 address sizes : 36 bits physical, 48 bits virtual power management: processor : 1 vendor_id : GenuineIntel cpu family : 6 model : 15 model name : Intel(R) Core(TM)2 CPU 6300 @ 1.86GHz stepping : 2 microcode : 0x5d cpu MHz : 1596.000 cache size : 2048 KB physical id : 0 siblings : 2 core id : 1 cpu cores : 2 apicid : 1 initial apicid : 1 fdiv_bug : no hlt_bug : no f00f_bug : no coma_bug : no fpu : yes fpu_exception : yes cpuid level : 10 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe nx lm constant_tsc arch_perfmon pebs bts aperfmperf pni dtes64 monitor ds_cpl vmx est tm2 ssse3 cx16 xtpr pdcm lahf_lm dtherm tpr_shadow bogomips : 3723.99 clflush size : 64 cache_alignment : 64 address sizes : 36 bits physical, 48 bits virtual power management :
Caracteristicas de la memoria
$ cat /proc/meminfo MemTotal: 4071388 kB MemFree: 3621496 kB Buffers: 71852 kB Cached: 317816 kB SwapCached: 0 kB Active: 85180 kB Inactive: 321736 kB Active(anon): 17516 kB Inactive(anon): 600 kB Active(file): 67664 kB Inactive(file): 321136 kB Unevictable: 0 kB Mlocked: 0 kB HighTotal: 3215052 kB HighFree: 2866488 kB LowTotal: 856336 kB LowFree: 755008 kB SwapTotal: 4126716 kB SwapFree: 4126716 kB Dirty: 44 kB Writeback: 0 kB AnonPages: 17152 kB Mapped: 13992 kB Shmem: 864 kB Slab: 19908 kB SReclaimable: 11244 kB SUnreclaim: 8664 kB KernelStack: 1304 kB PageTables: 900 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 6162408 kB Committed_AS: 148976 kB VmallocTotal: 122880 kB VmallocUsed: 13740 kB VmallocChunk: 102460 kB HardwareCorrupted: 0 kB AnonHugePages: 0 kB HugePages_Total: 0HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB DirectMap4k: 59384 kB DirectMap2M: 854016 kB
Caracteristicas de la GPU
$ ./deviceQuery ./deviceQuery Starting... CUDA Device Query (Runtime API) version (CUDART static linking) Detected 1 CUDA Capable device(s) Device 0: "GeForce GTX 480" CUDA Driver Version / Runtime Version 5.5 / 5.5 CUDA Capability Major/Minor version number: 2.0 Total amount of global memory: 1535 MBytes (1609760768 bytes) (15) Multiprocessors, ( 32) CUDA Cores/MP: 480 CUDA Cores GPU Clock rate: 1401 MHz (1.40 GHz) Memory Clock rate: 1848 Mhz Memory Bus Width: 384bit L2 Cache Size: 786432 bytes Maximum Texture Dimension Size (x,y,z) 1D=(65536), 2D=(65536, 65535), 3D=(2048, 2048, 2048) Maximum Layered 1D Texture Size, (num) layers 1D=(16384), 2048 layers Maximum Layered 2D Texture Size, (num) layers 2D=(16384, 16384), 2048 layers Total amount of constant memory: 65536 bytes Total amount of shared memory per block: 49152 bytes Total number of registers available per block: 32768 Warp size: 32 Maximum number of threads per multiprocessor: 1536 Maximum number of threads per block: 1024 Max dimension size of a thread block (x,y,z): (1024, 1024, 64) Max dimension size of a grid size (x,y,z): (65535, 65535, 65535) Maximum memory pitch: 2147483647 bytes Texture alignment: 512 bytes Concurrent copy and kernel execution: Yes with 1 copyengine(s) Run time limit on kernels: No Integrated GPU sharing Host Memory: No Support host pagelocked memory mapping: Yes Alignment requirement for Surfaces: Yes Device has ECC support: Disabled Device supports Unified Addressing (UVA): No Device PCI Bus ID / PCI location ID: 1 / 0 Compute Mode: < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) > deviceQuery, CUDA Driver = CUDART, CUDA Driver Version = 5.5, CUDA Runtime Version = 5.5, NumDevs = 1, Device0 = GeForce GTX 480 Result = PASS