Publi

GPGPU: Exprimiendo el potencial de tu GPU para propósito general (I)

Hace unas semanas estuve en el Curso Avanzado de GPU impartido por el Dr. Manuel Ujaldón en la Universidad de Málaga; y me parece interesante compartir algunas conclusiones y un poco de investigación sobre el tema con todos ustedes.

Una arquitectura diferente

Disponemos de un chip con gran capacidad de procesamiento, pero a su estilo. Aunque originalmente está diseñado para su uso en juegos, recientemente (no tan recientemente porque el primer uso de GPU (Graphics Processing Unit, o Unidad de Procesamiento Gráfico) para propósito general fue en el 1997, lo que sí es más reciente es la implicación de empresas en este tipo de sistemas) muchas personas están concienciadas en aprovechar sus capacidades, y es muy interesante ya que con poco dinero podemos compararnos en capacidad de procesamiento a algunos superordenadores, y es que en ciertas tareas de cálculo muy pesado que en procesadores CPU actuales tardan varios días, con procesamiento GPU y con un coste mucho menor podemos completarlas en pocos minutos.

Ya podríamos pensar que es el momento de prescindir de la CPU, pero ese momento aún no ha llegado, ya que la GPU puede ser muy buena en ciertas tareas relacionadas con el paralelismo, pero la CPU es buena con las sentencias de control y los saltos en el código.

Paralelismo en GPU

Las GPU actuales pueden tener 128, 256 ó 512 núcleos (frente a los 4 ó 6 núcleos de los actuales procesadores más avanzados). Es decir, podemos tener 512 hilos corriendo concurrentemente en GPU. Si estamos acostumbrados a utilizar pthreads, vemos cómo tenemos que programar el hilo y crearlos, y si tenemos 512 núcleos deberíamos crear los 512 hilos; el problema y la solución vienen, en la programación GPU, en que todos los núcleos ejecutarán el mismo código, y cada hilo tendrá una serie de identificadores que nos indicarán qué procesador está funcionando. Por tanto podemos lanzar 512 tareas simultáneas y cada una será lanzada por un núcleo diferente. Si, por ejemplo queremos sumar un vector de 512 elementos con otro, en CPU, realizamos una operación cada vez (hasta 512 operaciones), en GPU lanzaremos las 512 a la vez, y terminarán en el tiempo de una operación (más o menos y a grosso modo)

Velocidad de acceso

Las tarjetas gráficas, por lo general, tienen una memorial RAM, unas cuántas generaciones por delante (mientras en CPU vamos por DDR3, las gráficas ya usan DDR5). La principal razón es que esta memoria está soldada en la plata y precisamente, muy cerca de la GPU, y además, el controlador de memoria está integrado en GPU, lo que facilita las transacciones. Por esto y mucho más, una lectura o escritura en la memoria de la tarjeta gráfica consumirá pocos ciclos en GPU, mientras que los accesos a memoria en CPU consumen muchos ciclos de reloj.
Con esto, tenemos una lectura/escritura en memoria mucho más rápida, y además, tenemos una memoria compartida, mucho más pequeña (de varios Kilobytes) que es mucho más rápida, y que nos permitirá una interacción más rápida. Es algo así como la caché de la CPU, pero en este caso es una memoria que sí podemos controlar (leer y escribir en esa memoria lo que queremos y cuando queremos, mientras que la caché de la CPU es automática).

Metodología de programación

El código que podemos utilizar se ejecuta en CPU, por tanto tenemos que programar la tarea que queremos que realice la GPU aparte (en una función, por ejemplo), ese será nuestro kernel. Justo antes de ejecutar nuestro kernel en GPU, debemos copiar los datos necesarios desde la RAM de nuestro sistema (host) a la memoria de nuestro sistema gráfico (device), tras ello ejecutar el kernel, y luego volver a copiar los datos obtenidos desde el device al host.
Sólo podemos ejecutar un kernel a la vez; éste se ejecutará en todos los núcleos a la vez, y es nuestro trabajo hacer que merezca la pena, si dejaos núcleos sin usar, es potencia que no estamos aprovechando.
Por otra parte, tenemos que conseguir que la realización de las tareas en GPU merezca la pena, ya que:

T_copia_H2D + Procesamiento GPU + T_copia_D2H

Es decir, el tiempo que tardamos en copiar los datos, primero desde el host al device y luego desde el device al host, más el tiempo que tarde el procesamiento en GPU tiene que ser menor que el tiempo que tarde el procesamiento en CPU, si no, todo esto no merecería la pena.

Dada la naturaleza de los kernels, y que todos los procesadores tienen que ir sincronizados, ejecutando la tarea encomendada, lo óptimo es que estos sean lo más pequeños posible y que tengan el menor número de decisiones posible (un if … else, implica que si la condición se cumple en un conjunto de núcleos, al ir todos los procesadores a la par, mientras unos núcleos procesan, otros pierden tiempo).

¿Cómo empezamos?

Aunque tenemos varias posibilidades, siempre podemos programar en ensamblador, específico a cada modelo de Chip, pero tal y como ocurre en CPU, van surgiendo lenguajes y sistemas que nos permiten generalizar nuestro código y poder ejecutarlo en una amplia gama de Chips (sí, se pierde un poco de rendimiento, pero perderíamos mucho tiempo haciéndolo así, por tanto es un coste que podemos permitirnos). Actualmente hay dos sistemas que podemos utilizar: CUDA (sólo para tarjetas nVidia, a partir de la serie 8) y OpenCL desarrollado por Apple y propuesto al grupo Khronos para ser un estándar de programación GPU libre del control de una empresa y compatible con una amplia gama de chips gráficos y que tanto nVidia, como ATI, y otros muchos fabricantes de chips gráficos.

Lectura recomendada

Giz Explains: GPGPU Computing
Wikipedia: Unidad de Procesamiento Gráfico

También podría interesarte...

There are 2 comments left Ir a comentario

  1. Pingback: Bitacoras.com /

  2. Pingback: Configuración Nvidia Optimus con driver privativo y Bumblebee. – Poesía Binaria /

Leave a Reply