tesinapablotintor final
DESCRIPTION
Programacin en paralelo CudaTRANSCRIPT
1
UNIVERSIDAD NACIONAL AUTÓNOMA DE MÉXICO
FACULTAD DE CIENCIAS
TÍTULO DE TESINA
“Propagación de ondas en medios heterogéneos:
simulaciones y aplicaciones usando tarjetas gráficas
para acelerar el cómputo”
T E S I N A
QUE PARA OBTENER EL TÍTULO DE:
LICENCIADO EN CIENCIAS DE LA COMPUTACIÓN
PRESENTA:
TINTOR JIMÉNEZ PABLO ALBERTO
DIRECTORA DE TESINA:
DRA. ÚRSULA XIOMARA ITURRARÁN VIVEROS
2012
2
Hoja de Datos del Jurado
DATOS DEL ALUMNO
Apellido paterno Tintor
Apellido materno Jiménez
Nombre(s) Pablo Alberto
Teléfono 55 29 05 65 51
Universidad Nacional Autónoma de México Universidad Nacional Autónoma de México
Facultad de Ciencias Facultad de Ciencias
Carrera Ciencias de la Computación
Número de cuenta 305248336
DATOS DEL TUTOR
Grado Dra.
Nombre(s) Úrsula Xiomara
Apellido paterno Iturrarán
Apellido materno Viveros
DATOS DEL SINODAL 1
Grado Dr.
Nombre(s) José David
Apellido paterno Flores
Apellido materno Peñaloza
DATOS DEL SINODAL 2
Grado Dr.
Nombre(s) Pablo
Apellido paterno Barrera
Apellido materno Sánchez
DATOS DEL SINODAL 3
Grado Dr.
Nombre(s) Pedro
Apellido paterno González
Apellido materno Casanova
DATOS DEL SINODAL 4
Grado Dr.
Nombre(s) Francisco José
Apellido paterno Sánchez
Apellido materno Sesma
DATOS DEL TRABAJO ESCRITO
Título Propagación de ondas en medios heterogéneos: simulaciones y aplicaciones usando tarjetas gráficas para acelerar el cómputo.
Número de páginas 34p
Año 2012
3
AGRADECIMIENTOS
A mis padres Blanca Jiménez e Ignacio Tintor que me han apoyado
incondicionalmente para alcanzar mis metas y objetivos. Que me educaron y me
orientaron a llevar una vida escolar que ahora alcanza una meta importante.
A la Dra. Úrsula Iturrarán que me permitió trabajar con ella y me orientó en la
realización de este trabajo. Muchas gracias por ayudarme a terminar esta etapa de
mi vida académica.
A toda mi familia, en especial a mis hermanas Jazmín, Azucena y Dalia por haber
sido mis principales cómplices y compañeras de juegos infantiles. Y ahora por ser
mis principales cómplices y compañeras de vida.
A la profesora Sonia Valery por todo su apoyo profesional prestado en estos dos
años de conocerla.
A J.G.B.P y Ruth, son dos personas muy especiales en mi vida.
A todos los profesores con los que tuve el gusto de tomar clase y aprender de
ellos, en especial a Elisa Viso y Aida Byrd que me enseñaron a programar.
A mis cinco sinodales Dra. Úrsula Iturrarán, Dr. David Flores, Dr. Pablo Barrera,
Dr. Pedro González y Dr. Francisco Sánchez.
A todos mis compañeros y amigos de escuela, en especial a Carlos, Daniel,
Leticia, Jessica, Genaro, Guadalupe, Elizabeth con quienes compartimos trabajos,
tareas y momentos muy divertidos.
4
ÍNDICE
Introducción………………………………………………………………………………..5
Objetivo..………………………………………………………………………….………..6
Propiedades de las GPU’s………………………………………………………….........7
Memoria de Textura…………………………………………………………........9
Eventos……………………………………………………………………….........9
Solución de la ecuación de onda acústica……………………………………………10
Condiciones iniciales…………………………………………………….………11
Fronteras absorbentes………………………………………………………......11
Condición de Courant……………………………………………………………12
Definición de la malla……………………………………………………………12
Programación de la ecuación de onda acústica……………………………………...13
Animación…………………………………………………………………………15
Diagrama de flujo………………………………………………………………...17
Código fuente…………………………………………………………………….18
Resultados………………………………………………………………………………..26
Conclusiones……………………………………………………………………………..27
Apéndice A, instalación y configuración de CUDA…………………………………29
5
INTRODUCCIÓN
Una onda sonora es una onda longitudinal que transmite lo que se asocia con sonido. Si
se propaga en un medio elástico y continuo genera una variación local de presión o
densidad, que se propaga en forma de onda esférica periódica o cuasi periódica. El
modelado numérico de este fenómeno físico se ha desarrollado como un método para
crear modelos realistas y controlables de los instrumentos musicales. Con este propósito
se han desarrollado numerosos modelos matemáticos que describen el comportamiento
de las ondas acústicas. Uno de ellos es el método de diferencias finitas que permite
aproximar y discretizar la solución a la ecuación de onda acústica, este método consiste
en un conjunto de condiciones físicas y un sistema de actualización, los cuales calculan el
desplazamiento de las ondas acústicas basados en los valores de los tiempos previos, ver
[4].
El método de diferencias finitas permite simular con gran precisión, la propagación de
ondas acústicas sobre cualquier espacio definido. Sin embargo hay dos temas
importantes que tratar antes de llevar a cabo esta aproximación. La primera es la
complejidad para obtener dichos sistemas físicos y la derivación matemática del sistema.
El segundo es la complejidad computacional que se deriva de calcular las soluciones, esta
depende del tamaño de la malla que a su vez depende de la precisión que se desee, la
calidad y el tamaño de las imágenes.
Afortunadamente la programación paralela se ha desarrollado mucho durante los últimos
años. En primera instancia se ha incrementado considerablemente la velocidad de
procesamiento de las Unidades de Procesamiento Central por sus siglas en inglés
CPU’s, adicionalmente se ha incrementado la cantidad de ellas en nuestras computadoras
permitiendo que a la fecha se cumpla la llamada ley de Moore [8]. Al mismo tiempo se
comenzó a hacer uso de las Unidades de Procesamiento Gráfico GPU’s para cómputo de
propósito general, las cuales es sabido contienen cientos de unidades de procesamiento
dotadas de un gran poder de cómputo y canales de datos con gran ancho de banda,
como se puede observar en las siguientes gráficas:
Fig. 1. Operaciones de punto flotante por segundo para CPU
y GPU[3].
Fig. 2. Ancho de banda de los canales de datos para CPU y
GPU[3].
6
La diferencia en la capacidad de cómputo entre las CPU’s y las GPU’s radica en que una
GPU está diseñada para realizar computo en paralelo, esto es que puede ejecutar el
mismo programa sobre varias unidades de datos al mismo tiempo. El procesamiento de
imágenes es una de las principales aplicaciones para las GPU’s, grandes conjuntos de
pixeles y vértices pueden ser procesados por hilos de ejecución en paralelo.
En Noviembre 2006, NVIDIA introdujo en el mercado CUDATM, una plataforma de
propósito general y un modelo de programación que aprovecha los recursos del motor de
computo en paralelo de las GPU’s de NVIDIA, solucionando cómputos complejos de una
forma más eficiente que una CPU.
CUDA fue lanzado con un ambiente de software que permite a los desarrolladores usar C
como lenguaje de programación de alto nivel, manteniendo baja la curva de aprendizaje
para programadores familiarizados con este lenguaje de programación.
En su núcleo hay tres conceptos claves -- grupos de hilos jerárquicos, memoria
compartida y sincronización -- que pueden ser vistos por el programador como una
mínima extensión del lenguaje de programación. Estos conceptos llevan al programador a
dividir el problema en sub-problemas que puedan ser resueltos de manera independiente
en bloques paralelos de hilos, y cada sub-problema en pequeñas piezas que puedan
resolverse cooperativamente en paralelo por todos los hilos dentro de un bloque.
De esta manera un programa multi-hilos puede ser dividido en bloques independientes
uno de otro, permitiendo aprovechar mejor las capacidades de computo de las diferentes
GPU’s disponibles en el mercado, es decir una GPU con más multiprocesadores puede
automáticamente ejecutar el mismo programa en menor tiempo que una GPU con menos
multiprocesadores.
OBJETIVO
En este trabajo exponemos las propiedades de los procesadores gráficos, haciendo
énfasis en la explotación de éstos mediante la tecnología CUDA. Esta tecnología se
implementa en la solución de la ecuación de onda acústica por el método de diferencias
finitas.
7
PROPIEDADES DE LAS GPU´S
Normalmente una CPU contiene solo 2, 4, 6 o hasta 8 núcleos (ALU´s Unidad Lógica
Aritmética) que comparten una memoria cache y una RAM en común. En una PC, es
posible usar memoria distribuida y modelos de comunicación entre procesos como
OpenMPI o MPI (ver sitio oficial openMPI o la documentación de la Interfaz de Paso de
Mensajes [7]), que utilizan la misma o varias memorias en una o varias computadoras.
Cuando nos enfrentamos a la ejecución de grandes y pesadas rutinas, unos cuantos
núcleos por computadora no son suficientes, entonces nos damos a la tarea de unir varias
para construir un clúster de modo que la carga de trabajo y memoria queda distribuida
sobre cada una de ellas.
Las GPU´s trabajan un poco diferente a las CPU’s. Una GPU contiene cientos de
unidades lógico-aritméticas, agrupadas en bloques que comparten una gran cantidad de
memoria global, sin embargo cada uno contiene memoria local que no puede ser
accedida entre bloques. Dependiendo de las necesidades del usuario las unidades lógico-
aritméticas por sus siglas en inglés ALU´s pueden agruparse en 1, 2 o 3 dimensiones, las
imágenes 3 y 4 muestran una agrupación en 1D y 2D.
Antes de programar una tarjeta gráfica es conveniente conocer sus capacidades y
limitaciones físicas, además de determinar si es la única o existen varias de ellas en
nuestro equipo de cómputo. Se puede determinar la cantidad de estos dispositivos
mediante la función cudaGetDeviceCount. El programa completo se muestra al final de
este tema.
int count; HANDLE_ERROR( cudaGetDeviceCount( &count ) );
Fig.3. Arreglo unidimensional Fig.4. Arreglo bidimensional
8
Conocida la cantidad dispositivos dentro del sistema de cómputo, podemos iterar a través
de cada uno de ellos y conocer sus propiedades. Esta información se proporciona en una
estructura de tipo cudaDeviceProp, las propiedades que contiene esta estructura se
muestran en la tabla siguiente.
Propiedad de la GPU Descripción
char name [256] Una cadena en código ASCII que identifica al dispositivo (Ej.,”GeForce GTX 590”).
size_t totalGlobalMem Cantidad de memoria global en el dispositivo en bytes.
size_t sharedMemPerBlock Cantidad máxima en bytes de memoria compartida en un solo bloque.
int regsPerBlock Número de registros de 32-bits disponibles por bloque.
Int warpSize Número de procesos por unidad de cómputo.
Int size_t mem_pitch Tamaño en bytes máximo para copiar memoria.
int maxThreadsPerBlock Máximo de hilos de ejecución que un bloque puede contener.
int maxThreadsDim[3] Número máximo de hilos que un bloque puede contener a lo largo de cada una de sus dimensiones.
int maxGridSize[3] Máximo número de bloques a lo largo de cada dimensión de la malla.
size_t totalConstMem Cantidad de memoria constante disponible.
int major Revisión más a fondo de las capacidades de cómputo del dispositivo.
int minor Revisión mínima de las capacidades de cómputo del dispositivo.
size_t textureAlignment Requerimiento del dispositivo para el alineamiento de textura.
int deviceOverlap Valor booleano para indicar que se puede realizar simultáneamente una llamada a la función cudaMemcpy y la ejecución del kernel.
int multiprocessorCount Número de multiprocesadores en el dispositivo.
Int kernelExecTimeoutEnabled
Valor booleano, indica que existe un límite en el tiempo de ejecución de los kernels ejecutados en el dispositivo.
int integrated Valor booleano que indica si el dispositivo está integrado a la GPU.
int canMapHostMemory Valor booleano que indica si el dispositivo puede mapear memoria de la computadora al espacio de direcciones de CUDA.
int computeMode Valor que representa el modo de cómputo del dispositivo. Por defecto, exclusivo o prohibido.
int maxTexture1D Cantidad máxima de memoria de textura 1D soportada.
int maxTexture2D[2] Dimensiones máximas para memoria de textura 2D soportadas.
int maxTexture3D[3] Dimensiones máximas para memoria de textura 3D soportadas.
int maxTexture2DArray[3] Dimensiones máximas soportadas para arreglos de memoria de textura 2D.
int concurrentKernels Valor booleano que indica si en un momento determinado, el dispositivo puede ejecutar múltiples kernels dentro del mismo contexto, simultáneamente.
9
Siendo conscientes de esta información podremos programar nuestras GPU’s, haciendo
mejor uso de sus capacidades y sin sobrepasar sus límites. El código para consultar
nuestros dispositivos lucirá más o menos así:
Además de conocer las capacidades de cómputo de nuestros dispositivos, debemos
tomar en cuenta que existe dentro de las GPU’s memoria de textura que puede ayudar a
mejorar el desempeño si se le usa adecuadamente.
Memoria de textura
La memoria de textura es una variante de memoria de solo lectura que se aloja en la
circuitería del dispositivo, esto puede ayudar a su desempeño y reducir el tránsito de
datos desde la memoria global externa. Esta memoria fue diseñada principalmente para
aplicaciones gráficas, NVIDIA diseño las unidades de textura para interactuar con los
canales de OpenGL y DirectX. Específicamente fue diseñada para procesos que
requieren acceder continuamente a grandes espacios de memoria.
En el caso de la simulación que se lleva a cabo es necesario actualizar las imágenes
( ) accediendo continuamente a las regiones de memoria que contienen a las
imágenes actual ( ) y a la anterior ( ). El acceso a dichas memorias parece ser más
eficiente cuando se alojan en la memoria de textura. En el caso de la propagación de
ondas en 2D será necesario acceder a los nodos vecinos de la izquierda, derecha,
superior e inferior, por lo que el uso de memoria de textura 2D optimiza el indexado de los
nodos y por tanto el acceso es más eficiente que con memoria de textura 1D.
Memoria Compartida.
La memoria compartida después de los registros es la más cercana a los procesos, sin
embargo solo es visible durante la ejecución de un kernel y solo por procesos del mismo
bloque, además de ser muy pequeña, a pesar de tener estas desventajas no deja de ser
la más rápida, por ello es conveniente tenerla en cuenta y analizar en qué tipo de
problemas es conveniente usarla.
El uso de memoria compartida puede presentar problemas de sincronización de procesos,
los cuales pueden generar incongruencia en los datos, la función __syncthreads()
#include "../common/book.h" int main( void ) {
cudaDeviceProp prop; int count; HANDLE_ERROR( cudaGetDeviceCount( &count ) ); for (int i=0; i< count; i++) {
HANDLE_ERROR( cudaGetDeviceProperties( &prop, i ) ); printf(“Numero de procesos por unidad de computo: %i”,prop.warpSize); // El resultado es 32 para el caso de una tarjeta GeForce GT 240
} }
10
sincroniza los procesos, de tal manera que antes de ejecutar la siguiente instrucción en el
código todos los proceso del bloque han terminado de ejecutar las instrucciones previas.
Eventos
Los eventos en CUDA son esencialmente marcas de tiempo del GPU. En la simulación
haremos uso de ellos para marcar el inicio y el fin del cómputo de las imágenes que se
mostrarán.
11
SOLUCIÓN DE LA ECUACIÓN DE ONDA ACÚSTICA
Sea Ω = [0,1] Χ [0,1], f, u0 ∈ C 0(Ω). Hallar la función ∈ C 2(Ω) que satisface la
ecuación de onda.
Podemos discretizar el problema usando un esquema de diferencias finitas para dar
solución al sistema, tomando como la velocidad de propagación de las ondas.
Dividiremos las direcciones , de Ω en puntos con resultando 1024
intervalos en cada dirección. Denotaremos = = =
al delta espacial y =
al delta temporal. De acuerdo con esto y , ver
Laurent Michel 2011[1]. La aproximación numérica queda de la siguiente manera:
Sustituyendo en la primera ecuación y despejando tenemos que
(
)
Fig. 5. Valor del punto i,j
en el tiempo anterior. Fig. 6. Valores del punto i,j y
sus vecinos en el tiempo actual. Fig.7. Valor del punto i,j calculado
en base a los tiempos anteriores.
12
Donde
Que para los tiempos t=0.0, t=0.5 y t=1.0 se ve como lo muestran las siguientes figuras.
Condiciones iniciales
Para las condiciones iniciales decidí graficar un aro en el centro usando la función.
(
)
Donde
√
Fronteras absorbentes.
En el mejor de los casos las ondas incidentes en los bordes deben ser absorbidas en su
totalidad, imitando una continuidad en el espacio de propagación, sin embargo al ser
nuestra malla un espacio finito las ondas se ven reflejadas. La forma de hacer frente a
este problema será implementar una zona de absorción como se muestra en la figura de
abajo.
Las barreras absorbentes presentan la siguiente función , donde es el
índice del nodo a partir del borde más cercano y es el tamaño de las barreras
absorbentes, ver Cêrjan 1986[6].
x
Espacio no
absorbente Banda con celdas absorbentes
Fig. 8. f(x,y,t) en el inicio
de la simulación. t=0.0
Fig. 9.f(x,y,t) en el tiempo medio
de la simulación t=0.5
Fig. 10.f(x,y,t) en el fin
de la simulación t=1.0
Fig. 12. Espacio finito de propagación de las ondas, las bandas absorbentes ayudan a simular una continuidad en el espacio, según Cêrjan 1986 [6].
Fig. 11. Gráfica de la función u0 que establece las condiciones iniciales para la simulación.
x
x y
x
y
y
t t
t
y
t
13
Condición de Courant
El método de diferencias finitas tiene la dificultad de los problemas de inestabilidad. Las
soluciones inestables se presentan si es relativamente grande respecto a . La
relación a cumplir entre estos dos parámetros se conoce como la condición de Courant.
√ donde es la velocidad de propagación de las ondas.
En el caso de esta simulación
por tanto
√ cumple la
condición de Courant.
Definición de la malla
Para esta simulación he definido una imagen de tamaño 1024 x 1024, la cual será dividida
en 64 bloques con 16 procesos cada uno, que serán responsables de una región de
memoria representada por los puntos negros de la siguiente imagen.
Fig. 13. Agrupación de los procesos paralelos en los bloques de memoria definidos.
14
PROGRAMACIÓN DE LA ECUACIÓN DE ONDA ACÚSTICA
En el código utilizado para el censo de los dispositivos del sistema, podemos observar
que la sintaxis CUDA no es muy diferente a la del lenguaje C. El uso de variables,
constantes, estructuras, sentencias de control de flujo, uso de bibliotecas, entre otras
cosas es muy similar en ambos lenguajes, tal como podremos apreciar a continuación. La
documentación completa de CUDA se puede consultar en la página oficial de NVIDIA [3].
El código hasta aquí mostrado es ejecutado en la CPU y su memoria. Según la
documentación de CUDA, un kernel es código compilado para ser ejecutado
simultáneamente en los microprocesadores de la GPU, aplicar la misma instrucción sobre
secciones de memoria diferentes. Veamos el kernel que calcula .
La firma de este método no es muy diferente a uno escrito en lenguaje C, pero podemos
notar una importante alteración que es el calificador __global__. Este mecanismo alerta
al compilador para que el método sea compilado para ser ejecutado en la GPU.
El paso de parámetros a estas funciones se asemeja a la manera del lenguaje C. Sin
embargo en la invocación de un kernel con la sintaxis <<<m , n>>>, indicamos que
#include "../common/book.h" #include "../common/gpu_anim.h" /*DEFINIMOS EL NÚMERO DE THREADS Y EL NÚMERO DE BLOQUES PARA NUESTROS KERNELS **RESPETANDO: n_blocks = DIM / n_threads */ #define _nthreads 16 #define _nblocks 64 #define DIM 1024 /* ** N = DIM-1: Tamaño de la malla ** h = 1.0/N: Delta espacial ** tf: Tiempo final ** dt = h/2.0 : Delta temporal */ #define N 1023 #define h 1.0f/(float)N #define tf 1.0f #define dt h/2.0f float t=0; //Tiempo
__global__ void evolve(float *n, float t, int step) {
//Código que se ejecuta en GPU
}
//...Código
evolve<<<blocks, threads>>>(data->dev_uNext,t,0);
//...Código
15
ejecutaremos el programa dividiendo la sección de memoria original en “m” bloques y que
cada uno contendrá “n” procesos paralelos trabajando cooperativamente, teniendo
cuidado de no sobrepasar los límites físicos de nuestro dispositivo. Ver Fig. 4 “Arreglo
bidimensional”.
La memoria global, es el tipo de memoria de uso más general con la que cuenta una
GPU. Al igual que reservamos la memoria RAM de un CPU para los arreglos de un
programa escrito en C, podemos reservar memoria global para la ejecución de un kernel
con la función cudaMalloc. Veamos un ejemplo de su uso.
En este ejemplo con la función cudaMalloc alojamos dentro de la memoria del
dispositivo, el vector que guardará los valores de a lo largo del programa.
También haremos uso de la memoria de textura para mejorar el desempeño de nuestro
dispositivo. Veamos un ejemplo del uso de esta memoria.
En este ejemplo declaramos texOld, un bloque de memoria de textura 2D. Después
inicializamos este bloque y creamos la conexión con el bloque de memoria global
dev_uOld, mediante la función cudaBindTexture2D. Finalmente leemos datos de ella con
la función tex2D.
He descrito y ejemplificado la sintaxis que usaremos en los cálculos de esta modelación,
entonces podemos describir las funciones y utilidades que CUDA ofrece para realizar los
gráficos y la animación de este trabajo, basado en las API’s del libro “CUDA BY
EXAMPLE” [2]. Aunque existen otras plataformas para visualizar los resultados como
Matlab, gnuplot, dislin, etc. CUDA hace este trabajo más eficiente y dinámico utilizando
los recursos de la GPU, evitando la transferencia de datos entre CPU y GPU.
HANDLE_ERROR( cudaMalloc( (void**)&data.dev_uOld,1024*1024*sizeof(float) ) );
//…Código
texture<float,2> texOld; //Memoria de textura para u^n-1
//…Código
HANDLE_ERROR( cudaBindTexture2D( NULL,
texOld,data.dev_uOld,desc,DIM,DIM,DIM*sizeof(float)) );
//…Código
oidx = tex2D(texOld,i,j);
16
Animación
Teniendo en cuenta las dimensiones de nuestra malla, primero definimos un cuadro que
graficará los mapas de bits correspondientes a las imágenes calculadas por cada paso de
tiempo durante la simulación.
La función anim_gpu es llamada por el cuadro de animación cada vez que se muestra una
nueva imagen, anim_exit es llamada al final de la animación para liberar los recursos
utilizados y anim_and_exit llama a estas dos rutinas para realizar la animación.
Los argumentos de esta función son: un apuntador a una estructura DataBlock y un
entero ticks que cuenta las imágenes transcurridas, debido a que DataBlock contiene
los cálculos de , , en sus respectivos arreglos, el parámetro ticks no es
utilizado.
Ya que usamos memoria de textura, no podemos pasar parámetros que requieran
grandes cantidades de memoria, por ello alternamos , , tres veces de manera
que al terminar cada una retome su lugar, indicamos esta alternancia con un numero 0,1,2
al método evolve que lleva a cabo el cómputo de la propagación. Incrementamos la
variable t después de cada ejecución del kernel evolve.
Al llegar a la frontera las ondas rebotan, para evitarlo lanzamos el kernel siguiente:
GPUAnimBitmap bitmap( DIM, DIM, &data );//creamos una imagen de tamaño DIM*DIM
bitmap.anim_and_exit( (void (*)(uchar4*,void*,int))anim_gpu,(void (*)(void*))anim_exit );
void anim_gpu( uchar4* outputBitmap, DataBlock *data, int ticks ) {
//Código de animación
}
if(paso==0){ evolve<<<blocks, threads>>>(data->dev_uNext,t,0,ticks); in=data->dev_uNext; incrementDT(); } if(paso==1){ evolve<<<blocks, threads>>>(data->dev_uOld,t,1,ticks); in=data->dev_uOld; incrementDT(); } if(paso==2){ evolve<<<blocks, threads>>>(data->dev_uCurr,t,2,ticks);
in=data->dev_uCurr;
incrementDT();
}
paso++;
frontera<<<blocks, threads>>>(in,20,20,0.035);
17
La función up_Color permite tener colores llamativos sin afectar el cómputo de la
propagación, la función float_to_color mapea valores de punto flotante en pixeles de
color susceptibles de graficar y la función sleepP retarda la simulación.
Elemento de animación Descripción GPUAnimBitmap bitmap Estructura CUDA que define el cuadro de
animación y sus funciones. DataBlock data Estructura definida por el usuario utilizada
por el GPUAnimBitmap para guardar los buffers de lectura y escritura de datos para la animación, además de variables particulares de la animación.
anim_gpu Rutina de animación definida por el usuario es llamada cada vez que se calcula una nueva imagen.
anim_exit Rutina de liberación de recursos, una vez que se termina la animación será necesario liberar los buffers y las variables de animación.
anim_and_exit Función para llamar a las dos rutinas anteriores, anim_gpu y anim_and_exit.
Esta fue la forma de graficar y animar la propagación de ondas acústicas. En las
siguientes páginas se muestran un diagrama de flujo y el código fuente de este programa.
upColor<<<blocks, threads>>>(data->dev_color,in);
float_to_color<<<blocks,threads>>>(outputBitmap,data->dev_color);
sleepP(10000);
18
Diagrama de flujo de la operación del programa presentado.
19
Código fuente
#include "../common/book.h" #include "../common/gpu_anim.h" #include <stdio.h> #include <stdlib.h> /*DEFINIMOS EL NUMERO DE THREADS Y EL NUMERO DE BLOQUES PARA NUESTROS KERNELS **RESPETANDO: n_blocks = DIM / n_threads */ #define DIM 1024 #define _nthreads 16 #define _nblocks (DIM/_nthreads) /* ** N = DIM-1: Tamaño de la malla ** h = 1.0/N: Delta espacial ** tf: Tiempo final ** dt = h/2.0 : Delta temporal */ #define N (DIM-1) #define h (1.0f/(float)N) #define tf 1.0f #define dt (h/2.0f) texture<float,2> texOld; //Memoria de textura para u^n-1 texture<float,2> texCurr;//Memoria de textura para u^n texture<float,2> texNext;//Memoria de textura para u^n+1 float t=0; //Tiempo int paso=0; /* *Estructura DataBlock usada para *guardar datos para realizar la animación */ struct DataBlock { float *dev_uOld; float *dev_uCurr; float *dev_uNext; float *host_uOld; float *host_uCurr; float *host_uNext; float *dev_color; cudaEvent_t start, stop; float totalTime; float frames; };
20
/*Función f(ih,ij,ndt)
** x = i*h
** y = j*h
** t = n*dt
**/
__device__ float f(float *x, float *y, float *t) {
return expf(-*t)*(*x*(*x-1.0f)**y*(*y-1.0f) - 2.0f*(*y*(*y-1.0f)+*x*(*x-1.0f)));
}
/*Función equivalente a f(ih,ij,ndt) pero ejecutada en el host
** x = i*h
** y = j*h
** t = n*dt
**/
float g(float *x, float *y, float *t) {
return expf(-*t)*(*x*(*x-1.0f)**y*(*y-1.0f) - 2.0f*(*y*(*y-1.0f)+*x*(*x-1.0f)));
}
/*Método para convertir un arreglo de tipo float en un arreglo de tipo uchar4
**con las escalas de grises equivalentes, susceptible de graficar.
**uchar4 *in arreglo donde se guarda el mapeo float-escala de grises
**float *ou arreglo original para realizar el mapeo float-escala de grises
*/
__global__ void float_to_grey(uchar4* in,float* ou){
// mapeo de threadIdx/BlockIdx a coordenas x,y de los pixeles
int x = threadIdx.x + blockIdx.x * blockDim.x;
int y = threadIdx.y + blockIdx.y * blockDim.y;
int idx = x + y * blockDim.x * gridDim.x;
if(idx > DIM*DIM) return;
in[idx].x=(1.0f-ou[idx])*255;
in[idx].y=(1.0f-ou[idx])*255;
in[idx].z=(1.0f-ou[idx])*255;
in[idx].w=255;
}
21
/*Método para establecer las condiciones iniciales. **Estable la fuerza que inicia la propagación de las ondas. **Es un aro blanco en el centro. **float u arreglo en el que se guardan las condiciones iniciales */ __global__ void init(float *u){ // mapeo de threadIdx/BlockIdx a coordenas x,y de los pixeles int x = threadIdx.x + blockIdx.x * blockDim.x; int y = threadIdx.y + blockIdx.y * blockDim.y; int idx = x + y * blockDim.x * gridDim.x; if(idx > DIM*DIM) return; //movemos el origen de coordenadas al centro de la imagen float fx = x - DIM/2; float fy = y - DIM/2; float d = sqrt(fx * fx + fy * fy); float grey = cos(d/10.0f - 100.0f/7.0f) /(d/10.0f + 1.0f); //grey=fx*(fx-1)+fy*(fy-1); u[idx]=grey; if(u[idx]<0.20f)u[idx]=0.0f;//Eliminamos puntos basura. else u[idx]=1.0f;//Subimos el color de los puntos importantes al máximo. } /*Método para subir el color de las ondas **float *color arreglo modificado **float *in arreglo original */ __global__ void upColor(float *color, float *in){ // mapeo de threadIdx/BlockIdx a coordenas x,y de los pixeles int x = threadIdx.x + blockIdx.x * blockDim.x; int y = threadIdx.y + blockIdx.y * blockDim.y; int idx = x + y * blockDim.x * gridDim.x; if(idx > DIM*DIM) return; float px=2.0f*in[idx];//Subimos el color de los puntos. if(px>1.0f)px=1.0f; color[idx]=px; } /*Método para incrementar el tiempo */ bool incrementDT() { float tmp = t+dt; if(tmp < tf) { t=tmp; return false; } return true; }
22
//Método para contener las ondas, al llegar a las mallas //de contención las ondas se debilitan __global__ void frontera(float *in, int nxa,int nya,float a){ // mapeo de threadIdx/BlockIdx a coordenas x,y de los pixeles int x = threadIdx.x + blockIdx.x * blockDim.x; int y = threadIdx.y + blockIdx.y * blockDim.y; int idx = x + y * blockDim.x * gridDim.x; if(idx > DIM*DIM) return; float c=1.0f; if(x<nxa) c=expf(-((a*(nxa-x))*((nxa-x)))); if(y<nya) c=expf(-((a*(nya-y))*((nya-y)))); if(x>DIM-nxa+1) c=expf(-((a*(x-DIM+nxa-1))*((x-DIM+nxa-1)))); if(y>DIM-nya+1) c=expf(-((a*(y-DIM+nya-1))*((y-DIM+nya-1)))); in[idx]=c*in[idx]; } /* *Método para calcular la propagación de onda acústica secuencialmente */ void evolve_s(float *old, float *curr, float *next, float t) { //Inicia uso de apuntadores float g(float *,float *, float*); float ih; float jh; float *pih; float *pjh; float *pt; float oidx,cidx,cleft,cright,cbottom,ctop; //Termina uso de apuntadores for(int i=0;i<(DIM*DIM);i++){ int x = i % DIM; int y = i / DIM; if(x==0||x==(DIM-1)||y==0||y==(DIM-1)){ next[i]=0; } else{ oidx = old[i]; cidx = curr[i]; cleft = curr[i-1]; cright = curr[i+1]; cbottom = curr[i+DIM]; ctop = curr[i-DIM]; //Diferencias finitas pih=&ih; pjh=&jh; pt=&t; ih=h*x; jh=h*y; next[i]= -oidx + 2*cidx + (dt*dt)*(N*N*(cleft + cright + cbottom + ctop- 4 * cidx)+g(pih, pjh, pt)); } } }
23
/* *Método para calcular la propagación de onda acústica haciendo uso *de memoria global. */ __global__ void evolve_gm(float *old, float *curr, float *next, float t) { // mapeo de threadIdx/BlockIdx a coordenas x,y de los pixeles int i = threadIdx.x + blockIdx.x*blockDim.x; int j = threadIdx.y + blockIdx.y*blockDim.y; int idx = i + j*blockDim.x*gridDim.x; if(idx > DIM*DIM) return; //Puntos en la frontera if(i==0 || i == DIM-1 || j==0 || j == DIM-1) { next[idx] = 0.0f; return; } //Inicia uso de punteros float f(float *,float *, float*); float ih=i*h; float jh=j*h; float *pih; float *pjh; float *pt; pih=&ih; pjh=&jh; pt=&t; //Termina uso de apuntadores
float oidx,cidx,cleft,cright,cbottom,ctop;
oidx = old[idx];
cidx = curr[idx];
cleft = curr[idx-1];
cright = curr[idx+1];
cbottom = curr[idx+DIM];
ctop = curr[idx-DIM];
//Diferencias finitas
next[idx]= -oidx + 2*cidx + (dt*dt)*(N*N*(cleft + cright + cbottom + ctop- 4 * cidx)+f(pih, pjh, pt));
}
24
/*Método principal, aquí se calcula u^n+1 con memoria compartida **float *n arreglo en el que guardaremos el cálculo de u^n+1 **t tiempo actual transcurrido del inicio de propagación n*dt **step u^n-1,u^n,u^n+1 se alternan de acuerdo a este parámetro */ __global__ void evolve_sh(float *n, float t,int step,int ticks) { // mapeo de threadIdx/BlockIdx a coordenas x,y de los pixeles int i = threadIdx.x + blockIdx.x*blockDim.x; int j = threadIdx.y + blockIdx.y*blockDim.y; int idx = i + j*blockDim.x*gridDim.x; if(idx > DIM*DIM) return; __shared__ float sh_old[_nthreads][_nthreads]; __shared__ float sh_curr[_nthreads][_nthreads]; __shared__ float sh_next[_nthreads][_nthreads]; if(threadIdx.x==_nthreads-1){ sh_old[threadIdx.x+2][threadIdx.y]=tex2D(texOld,i+1,j); sh_curr[threadIdx.x+2][threadIdx.y]=tex2D(texCurr,i+1,j); sh_next[threadIdx.x+2][threadIdx.y]=tex2D(texNext,i+1,j); } if(threadIdx.y==_nthreads-1){ sh_old[threadIdx.x][threadIdx.y+2]=tex2D(texOld,i,j+1); sh_curr[threadIdx.x][threadIdx.y+2]=tex2D(texCurr,i,j+1); sh_next[threadIdx.x][threadIdx.y+2]=tex2D(texNext,i,j+1); } if(threadIdx.x==0){ sh_old[threadIdx.x][threadIdx.y]=tex2D(texOld,i-1,j); sh_curr[threadIdx.x][threadIdx.y]=tex2D(texCurr,i-1,j); sh_next[threadIdx.x][threadIdx.y]=tex2D(texNext,i-1,j); } if(threadIdx.y==0){ sh_old[threadIdx.x][threadIdx.y]=tex2D(texOld,i,j-1); sh_curr[threadIdx.x][threadIdx.y]=tex2D(texCurr,i,j-1); sh_next[threadIdx.x][threadIdx.y]=tex2D(texNext,i,j-1); } sh_old[threadIdx.x+1][threadIdx.y+1]=tex2D(texOld,i,j); sh_curr[threadIdx.x+1][threadIdx.y+1]=tex2D(texCurr,i,j); sh_next[threadIdx.x+1][threadIdx.y+1]=tex2D(texNext,i,j); __syncthreads();
//Puntos en la frontera
if(i==0 || i == DIM-1 || j==0 || j == DIM-1) { n[idx] = 0.0f; return; }
25
//Obtenemos los datos de la memoria de textura float oidx,cidx,cleft,cright,cbottom,ctop; if(step==0){ oidx = sh_old[threadIdx.x+1][threadIdx.y+1]; cidx = sh_curr[threadIdx.x+1][threadIdx.y+1]; cleft = sh_curr[threadIdx.x][threadIdx.y+1]; cright = sh_curr[threadIdx.x+2][threadIdx.y+1]; cbottom = sh_curr[threadIdx.x+1][threadIdx.y+2]; ctop = sh_curr[threadIdx.x+1][threadIdx.y]; } if(step==1){ oidx = sh_curr[threadIdx.x+1][threadIdx.y+1]; cidx = sh_next[threadIdx.x+1][threadIdx.y+1]; cleft = sh_next[threadIdx.x][threadIdx.y+1]; cright = sh_next[threadIdx.x+2][threadIdx.y+1]; cbottom = sh_next[threadIdx.x+1][threadIdx.y+2]; ctop = sh_next[threadIdx.x+1][threadIdx.y]; } if(step==2){ oidx = sh_next[threadIdx.x+1][threadIdx.y+1]; cidx = sh_old[threadIdx.x+1][threadIdx.y+1]; cleft = sh_old[threadIdx.x][threadIdx.y+1]; cright = sh_old[threadIdx.x+2][threadIdx.y+1]; cbottom = sh_old[threadIdx.x+1][threadIdx.y+2]; ctop = sh_old[threadIdx.x+1][threadIdx.y]; } __syncthreads(); //Inicia uso de punteros float f(float *,float *, float*); float ih=i*h; float jh=j*h; float *pih; float *pjh; float *pt; pih=&ih; pjh=&jh; pt=&t; //Termina uso de punteros //Diferencias finitas n[idx]= -oidx + 2*cidx + (dt*dt)*(N*N*(cleft + cright + cbottom + ctop- 4 * cidx)+f(pih, pjh, pt)); }
26
/*Método principal, aquí se calcula u^n+1 **float *n arreglo en el que guardaremos el cálculo de u^n+1 **t tiempo actual transcurrido del inicio de propagación n*dt **step u^n-1,u^n,u^n+1 se alternan de acuerdo a este parámetro */ __global__ void evolve(float *n, float t,int step,int ticks) { // mapeo de threadIdx/BlockIdx a coordenas x,y de los pixeles int i = threadIdx.x + blockIdx.x*blockDim.x; int j = threadIdx.y + blockIdx.y*blockDim.y; int idx = i + j*blockDim.x*gridDim.x; if(idx > DIM*DIM) return; //Puntos en la frontera if(i==0 || i == DIM-1 || j==0 || j == DIM-1) { n[idx] = 0.0f; return; } //Obtenemos los datos de la memoria de textura float oidx,cidx,cleft,cright,cbottom,ctop; if(step==0){ oidx = tex2D(texOld,i,j); cidx = tex2D(texCurr,i,j); cleft = tex2D(texCurr,i-1,j); cright = tex2D(texCurr,i+1,j); cbottom = tex2D(texCurr,i,j-1); ctop = tex2D(texCurr,i,j+1); } if(step==1){ oidx = tex2D(texCurr,i,j); cidx = tex2D(texNext,i,j); cleft = tex2D(texNext,i-1,j); cright = tex2D(texNext,i+1,j); cbottom = tex2D(texNext,i,j-1); ctop = tex2D(texNext,i,j+1); } if(step==2){ oidx = tex2D(texNext,i,j); cidx = tex2D(texOld,i,j); cleft = tex2D(texOld,i-1,j); cright = tex2D(texOld,i+1,j); cbottom = tex2D(texOld,i,j-1); ctop = tex2D(texOld,i,j+1); } //Inicia uso de punteros float f(float *,float *, float*); float ih=i*h; float jh=j*h; float *pih; float *pjh; float *pt; pih=&ih; pjh=&jh; pt=&t; //Termina uso de punteros //Diferencias finitas n[idx]= -oidx + 2*cidx + (dt*dt)*(N*N*(cleft + cright + cbottom + ctop- 4 * cidx)+f(pih, pjh, pt)); }
27
//Método para retrasar la simulación void sleepP(unsigned int mseconds) { clock_t goal = mseconds + clock(); while (goal > clock()); } //Rutina de para graficar y animar los cómputos void anim_gpu( uchar4* outputBitmap, DataBlock *data, int ticks ) { //if(t==0.0f)system("pause"); HANDLE_ERROR( cudaEventRecord( data->start, 0 ) ); dim3 blocks(_nblocks, _nblocks); dim3 threads(_nthreads, _nthreads); float *in; paso=paso%3; //lanzamos el kernel evolve para calcular u^n+1 if(paso==0){ evolve<<<blocks, threads>>>(data->dev_uNext,t,0,ticks); //evolve_gm<<<blocks, threads>>>(data->dev_uOld,data->dev_uCurr,data->dev_uNext,t); //evolve_s(data->host_uOld,data->host_uCurr,data->host_uNext,t); //HANDLE_ERROR( cudaMemcpy( data->dev_uNext, data->host_uNext,DIM*DIM*sizeof(float),cudaMemcpyHostToDevice ) ); in=data->dev_uNext; incrementDT(); } if(paso==1){ evolve<<<blocks, threads>>>(data->dev_uOld,t,1,ticks); //evolve_gm<<<blocks, threads>>>(data->dev_uCurr,data->dev_uNext,data->dev_uOld,t); //evolve_s(data->host_uCurr,data->host_uNext,data->host_uOld,t); //HANDLE_ERROR( cudaMemcpy( data->dev_uOld, data->host_uOld,DIM*DIM*sizeof(float),cudaMemcpyHostToDevice ) ); in=data->dev_uOld; incrementDT(); } if(paso==2){ evolve<<<blocks, threads>>>(data->dev_uCurr,t,2,ticks); //evolve_gm<<<blocks, threads>>>(data->dev_uNext,data->dev_uOld,data->dev_uCurr,t); //evolve_s(data->host_uNext,data->host_uOld,data->host_uCurr,t); //HANDLE_ERROR( cudaMemcpy( data->dev_uCurr, data->host_uCurr,DIM*DIM*sizeof(float),cudaMemcpyHostToDevice ) ); in=data->dev_uCurr; incrementDT(); } paso++; //Debilitamos las ondas que estan en la frontera frontera<<<blocks, threads>>>(in,10,10,0.015f); //Subimos el color de las ondas upColor<<<blocks, threads>>>(data->dev_color,in); //Convertimos valores float en pixeles de color float_to_color<<<blocks,threads>>>(outputBitmap,data->dev_color); HANDLE_ERROR(cudaEventRecord( data->stop, 0 ) ); HANDLE_ERROR( cudaEventSynchronize( data->stop ) ); float elapsedTime; HANDLE_ERROR( cudaEventElapsedTime( &elapsedTime,data->start, data->stop ) ); data->totalTime += elapsedTime; ++data->frames; printf( "Average Time per frame: %3.12f ms\n",data->totalTime/data->frames ); //Retrasamos la animación sleepP(10000); }
28
//Rutina para liberar recursos void anim_exit( DataBlock *data ) { HANDLE_ERROR( cudaUnbindTexture( texOld ) ); HANDLE_ERROR( cudaUnbindTexture( texCurr ) ); HANDLE_ERROR( cudaUnbindTexture( texNext ) ); HANDLE_ERROR( cudaFree( data->dev_uOld) ); HANDLE_ERROR( cudaFree( data->dev_uCurr ) ); HANDLE_ERROR( cudaFree( data->dev_uNext ) ); HANDLE_ERROR( cudaEventDestroy( data->start ) ); HANDLE_ERROR( cudaEventDestroy( data->stop ) ); } int main (int argc, char *argv[]) { DataBlock data; //Estructura usada por anim_gpu GPUAnimBitmap bitmap( DIM, DIM, &data );//creamos una imagen de tamaño DIM*DIM data.totalTime = 0; data.frames = 0; HANDLE_ERROR( cudaEventCreate( &data.start ) ); HANDLE_ERROR( cudaEventCreate( &data.stop ) ); int imageSize = bitmap.image_size(); //Asignamos memoria global HANDLE_ERROR( cudaMalloc( (void**)&data.dev_uOld,DIM*DIM*sizeof(float) ) ); HANDLE_ERROR( cudaMalloc( (void**)&data.dev_uCurr,DIM*DIM*sizeof(float) ) ); HANDLE_ERROR( cudaMalloc( (void**)&data.dev_uNext,DIM*DIM*sizeof(float) ) ); HANDLE_ERROR( cudaMalloc( (void**)&data.dev_color,DIM*DIM*sizeof(float) ) ); //Ligamos la memoria de textura a la memoria glogal cudaChannelFormatDesc desc = cudaCreateChannelDesc<float>(); HANDLE_ERROR( cudaBindTexture2D( NULL, texOld,data.dev_uOld,desc,DIM,DIM,DIM*sizeof(float)) ); HANDLE_ERROR( cudaBindTexture2D( NULL, texCurr,data.dev_uCurr,desc,DIM,DIM,DIM*sizeof(float) ) ); HANDLE_ERROR( cudaBindTexture2D( NULL, texNext,data.dev_uNext,desc,DIM,DIM,DIM*sizeof(float) ) ); //Asignamos memoria RAM data.host_uOld = (float*)malloc( DIM*DIM*sizeof(float)); data.host_uCurr = (float*)malloc( DIM*DIM*sizeof(float)); data.host_uNext = (float*)malloc( DIM*DIM*sizeof(float)); //Inicializamos// dim3 threads(_nthreads,_nthreads); dim3 blocks(_nblocks,_nblocks); //Establecemos condiciones iniciales init<<<blocks, threads>>>(data.dev_uOld); init<<<blocks, threads>>>(data.dev_uCurr); HANDLE_ERROR( cudaMemcpy( data.host_uCurr, data.dev_uCurr,DIM*DIM*sizeof(float),cudaMemcpyDeviceToHost ) ); HANDLE_ERROR( cudaMemcpy( data.host_uOld, data.dev_uOld,DIM*DIM*sizeof(float),cudaMemcpyDeviceToHost ) ); //Llamamos a la rutina de animación bitmap.anim_and_exit( (void (*)(uchar4*,void*,int))anim_gpu,(void (*)(void*))anim_exit ); return 0; }
29
RESULTADOS
El objetivo principal de este trabajo fue hacer uso de las propiedades de una GPU para
acelerar el cómputo de la propagación de ondas acústicas en un sistema 2D. El cómputo
de esta simulación fue realizado en una GPU GeForce GT240 (modificar las constantes
n_threads y n_blocks, si se cuenta con un dispositivo de mayores capacidades). A
continuación se muestran los tiempos de cómputo obtenidos para los tres algoritmos:
evolve hace uso de la GPU y memoria de textura, evolve_gm hace uso de la GPU y
memoria global, evolve_s hace uso de una CPU (AMDFx-4100 Quad-Core Processor) y
su memoria RAM.
Tamaño de imagen
Tiempo de cálculo en milisegundos
evolve GPU + Mem.
Textura
evolve_gm GPU + Mem.
Global
evolve_sh GPU + Mem. Compartida
evolve_s CPU + RAM
64 x 64 px 0.03 0.03 0.04 1.04
128 x 128 px 0.07 0.08 0.10 5.37
256 x 256 px 0.18 0.19 0.29 21.62
512 x 512 px 0.58 0.65 0.99 84.5
1024 x 1204 px 2.17 2.42 3.81 335.75
2048 x 2048 px 8.52 9.62 15.14 1342.81
La siguiente gráfica muestra la superioridad de cómputo de una GPU sobre una CPU para
diferentes tamaños de imagen.
0.01
0.1
1
10
100
1000
10000
64 128 256 512 1024 2048
Comparación de Tiempos de Cálculo
GPU + Mem Textura
GPU + Mem Global
GPU + Mem Compartida
CPU + RAM
Fig. 14. Tiempos de cómputo de los algoritmos para diferentes tamaños de imagen. El tiempo es en milisegundos por imagen.
Fig. 15. Gráfica de comparación de tiempos de cómputo para los algoritmos definidos.
30
La superioridad de cómputo en paralelo de una GPU sobre una CPU queda comprobada
en los resultados anteriores, una GPU (GeForce GT240) requiere el 2.88% del tiempo
requerido por una CPU (AMDFx-4100 Quad-Core Processor) para procesar imágenes de
64x64 px, mientras que para imágenes de 2048x2048 px el tiempo requerido por la GPU
es el 0.63% del tiempo requerido por la CPU. A continuación se muestran los resultados
arrojados por la herramienta de gráficos.
t = 0.00978 t = 0.005865 t = 0.011730 t =0.020039
t=0.038612 t = 0.074780 t = 0.112903 t = 0.173020
t = 0.320137 t = 0.478983 t = 0.521994
CONCLUSIONES
La velocidad de cómputo en paralelo de una GPU tiene mayor potencial sobre
implementaciones hechas en Matlab o C. Conforme los ambientes de programación en
CUDA vayan progresando, será más fácil programar código ejecutable en tarjetas gráficas
que utilicen de manera eficiente los recursos que ofrecen. Permitiéndonos realizar
cómputos extensos, en el mejor de los casos sin interactuar con los recursos del sistema
que las alberga, o en un caso menos ideal disminuyendo el uso de los mismos,
liberándolos para la realización de otras tareas, tal como se hizo en esta simulación.
Fig. 15. Imágenes en diferentes momentos de la simulación.
31
El uso de memoria de textura facilita el acceso a los datos a las unidades de
procesamiento de las GPU’s, mejorando su desempeño en la realización de los cálculos,
ya que se encuentra integrada en la circuitería del dispositivo, disminuye el tránsito de los
datos a diferencia de la memoria global que es externa. Hacer uso de ella es una tarea
sencilla con la función cudaBindTexture2D para ligarla a la memoria global y tex2D para
leer datos de ella. En el caso de la presente simulación la diferencia no es muy notoria sin
embargo si hubo una mejora al desempeño de la GPU.
El procesamiento en paralelo de las GPU’s es la principal aceleración de los cómputos y
el motivo principal de trabajar con ellas, ya que dividimos el área de trabajo en bloques y
en cada uno de estos ejecutamos cierta cantidad de procesos que trabajan al mismo
tiempo y cooperativamente. Utilizar esta técnica para definir una malla bidimensional
sobre secciones de memoria unidimensionales, conlleva a realizar un mapeo de índices a
coordenadas (x,y), afortunadamente CUDA cuenta con los mecanismos necesarios para
facilitar esta tarea.
El modelado de la propagación de ondas acústicas por el método de diferencias finitas
presenta problemas de discretización y de recursos finitos, por ello fue importante hacer
uso de la condición de Courant e implementar barreras absorbentes en los bordes.
32
APENDICE A
INSTALACIÓN Y CONFIGURACIÓN DE CUDA
Este manual fue probado en una PC con sistema operativo Ubuntu 10.10.
1. Verificar la lista de GPU’s compatibles con CUDA.
https://developer.nvidia.com/cuda-gpus
2. Descargar los drivers de nuestra tarjeta gráfica desde:
http://la.nvidia.com/Download/index.aspx
3. Limpiar todo lo referente a NVIDIA y apagar el X server, para no generar
conflictos.
aptitude remove nvidia*
aptitude purge xserver-xorg-video-nouveau
/etc/init.d/lightdm stop
Nota: Si no se tiene instalado aptitude ejecutar.
sudo apt-get install aptitude
4. Dar permisos de ejecución y ejecutar el instalador de drivers.
chmod +x NVIDIA-Linux-x86-304.64.run
./NVIIDIA-Linux-x86-304.64.run
5. Descargar CUDA Toolkit y SDK de:
33
https://developer.nvidia.com/cuda-toolkit-archive
6. Dar permisos de ejecución y ejecutar el instalador de CUDA Toolkit.
chmod +x cudatoolkit_4.2.9_linux32_ubuntu11.04.run
./cudatoolkit_4.2.9_linux32_ubuntu11.04.run
7. Terminada la instalación, procedemos a configurar el Path.
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${CUDA}/lib:${CUDA}/lib64
export PATH=PATH:{cuda}/bin
8. Dar permisos de ejecución y ejecutar el instalador de SDK.
chmod +x gpucomputingsdk_4.2.9_linux.run
./ gpucomputingsdk_4.2.9_linux.run
Durante el proceso el instalador preguntará si se desea instalar los
complementos, Aceptar la instalación de ellos.
libgl1-mesa-dev
libglu1-mesa-dev
libxmu-dev
freeglut3-dev
libxi-dev
9. Tan pronto esté listo, moverse a la carpeta donde se instalaron los programas y
ejecutamos los comandos:
cd C
Para entrar a la carpeta que contiene los códigos de ejemplo.
make
Para compilar los ejemplos del SDK.
Nota: Este manual muestra la forma de instalación que se realizó en el año 2010 para la
computadora donde se desarrolló el presente trabajo, la cual cuenta con un Sistema
Operativo Ubuntu 10.10. Para versiones más recientes del instalador, así como los
manuales de instalación para otros sistemas operativos se recomienda la siguiente página
del sitio oficial de NVIDIA.
https://developer.nvidia.com/cuda-downloads
34
REFERENCIAS
[1] Laurent Michel. Solving a wave equation with CUDA. Technical report. May 2011.
http://sma.epfl.ch/~lmichel/cuda_report.pdf
[2] Jason Sanders, Edward Kandrot. CUDA by Example, An introduction to general-
purpose GPU programming. Addison-Wesley. July 2010.
[3]NVIDIA Corporation. CUDA C Programming Guide. October 2012.
http://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html
[4] Randall J. LeVeque. Finite Difference Method for Ordinary and Partial Differential
Equation. SIAM. July 2007.
[5] Alfredo Ghisays Ruiz, Carlos Alberto Vargas J, Luis Alfredo Montes Vides. Método
híbrido de fronteras no reflectivas en límites de modelos sísmicos. 2006.
[6] Cerjan, C., D. Kosloff, R. Kosloff, and M. Reshef. A non-reflecting bound-
ary condition for direct acoustic and elastic wave equations. Geophysics,
50, 705 – 708. 1985.
[7] http://www.open-mpi.org http://www.mpi-forum.org/docs
[8] http://www.intel.com/cd/corporate/techtrends/emea/spa/209840.htm