Logo de Torre de Babel
Portada Libros Artículos Perfil Scholar

Programación y paralelismo (CPU vs GPU)

Hace unos días, en la entrada titulada Programación y paralelismo (threads, GPGPU, MPI), situaba en términos generales el contexto actual al que deben enfrentarse los programadores para poder aprovechar lo mejor posible la potencia que ofrece el hardware del que disponen no ya solo las empresas, sino cualquier usuario de un ordenador o red de ordenadores doméstica. En esta segunda entrada pretendo, siendo algo más específico, centrarme precisamente en las dos alternativas de paralelismo presentes en prácticamente cualquier máquina que no tenga más de 4 ó 5 años, incluyendo a los portátiles de última generación.

Todos los ordenadores cuentan con una CPU o microprocesador (en ocasiones con varios) y, desde hace unos años, dichos circuitos integrados dejaron de competir en velocidad bruta, regida únicamente por la frecuencia de funcionamiento, para incrementar su potencia por otra vía: la integración de múltiples núcleos. Una configuración hardware con un único microprocesador de 4 núcleos resulta mucho más barata que otra que cuente con 4 microprocesadores (que también existen) de un único núcleo, ya que en la placa base es necesario un único zócalo y el número de interconexiones es también mucho menor, aparte de que fabricar un único chip es más barato que fabricar cuatro. La potencia de estos dos hipotéticos sistemas no es idéntica, pero sí muy similar.

Además de varios núcleos ciertos fabricantes, como es el caso de Intel con su tecnología Hyperthreading, diseñan sus microprocesadores de manera que cada núcleo (duplicando ciertas unidades operativas) tiene capacidad para ejecutar dos hilos simultáneamente. Con un micro de 4 núcleos podrían ejecutarse 8 hilos con paralelismo real (sin recurrir a técnicas de tiempo compartido). Este fabricante ya cuenta con microprocesadores que ofrecen 6 núcleos/12 hilos y en muestras de tecnología futura, puesta a disposición de universidades, experimenta con un chip que dispone de 48 núcleos. Es fácil darse cuenta de que en pocos años tendremos ordenadores cuyas CPU integrarán decenas sino cientos de núcleos, equivalente cada uno de ellos a un procesador tipo Pentium.

La escalada del número de núcleos integrados en un mismo circuito no se inició, sin embargo, en el campo de las CPU, sino que viene precedida por una carrera similar en el de las GPU desde hace ya varios años, producto de la directa competencia entre nVidia y ATI (ahora AMD). Los productos de estos fabricantes hace tiempo que se componen de cientos de núcleos de procesamiento y grandes cantidades de memoria, ofreciendo una potencia impensable hasta hace muy poco. Aunque en un principio esos núcleos se empleaban exclusivamente para ejecutar shaders, pequeños programas encargados de realizar transformaciones sobre vértices, aplicar texturas, cálculos de iluminación, etc., (véase la serie sobre shaders que he ido publicando en los últimos meses, en el margen derecho), gracias a las técnicas GPGPU ahora también es posible usarlos para otros fines.

A pesar de que la denominación núcleo ha sido utilizada tanto por fabricantes de GPU como de CPU, debe tenerse en cuenta que no hace referencia exactamente al mismo concepto. En una CPU cada núcleo es equivalente a un microprocesador completo, con sus propios registros, unidades aritméticas y de ejecución e incluso memoria propia, si bien comparte también memoria con los demás núcleos, así como las líneas de entrada/salida y buses que permiten a la CPU comunicarse con el exterior. Los núcleos de una GPU son mucho más sencillos, básicamente unidades aritmético-lógicas con capacidad para operar en coma flotante y llevar a cabo algunas operaciones específicas, pero no ejecutar código de propósito general.

A la hora de planificar el grado de concurrencia de un software o algoritmo hay que diferenciar, por tanto, entre paralelismo SIMD y paralelismo MIMD. Las GPU son ideales para el primer caso, en el que un mismo conjunto de sentencias se aplica simultáneamente sobre múltiples datos independientes entre sí, produciendo por lo general tantos resultados como datos de entrada existan. Es una configuración ideal para, por ejemplo, realizar operaciones sobre matrices que, dependiendo de su tamaño, pueden ser calculadas en un único ciclo de reloj. Para paralelizar tareas del segundo tipo, en las que flujos de sentencias diferentes se aplican sobre conjuntos de datos que pueden ser independientes o no, es necesario recurrir a los núcleos de la CPU, ya que cada uno de ellos puede ejecutar un programa distinto: un núcleo puede controlar la interfaz de usuario de un programa mientras otro se dedica a procesar datos introducidos en dicha interfaz, tareas paralelas pero que no tienen mucho que ver entre sí.

Estas diferencias entre un tipo de paralelismo y otro dan lugar también a diferentes modelos de programación. Las tareas a ejecutar en los núcleos de un procesador se implementan como hilos o threads. Incluso cuando no se crean explícitamente, cada programa/proceso tiene asociado al menos un hilo de ejecución que se encarga de procesar el flujo principal de sentencias. Los sistemas operativos cuentan con una API de bajo nivel para la creación y control de hilos, siendo mucho más habitual el uso de los servicios de alto nivel que ofrecen la mayoría de plataformas y lenguajes para esta tarea, dejándoles la gestión de los detalles más delicados. Incluso hay plataformas/lenguajes, como es el caso de Erlang, en los que este paralelismo es prácticamente implícito sin necesidad de vérselas con los hilos.

En contraposición, las tareas de tipo SIMD a ejecutar en una GPU se implementan como un kernel o porción de código que se aplicará en paralelo sobre todos los datos a tratar, sería equivalente a tener múltiples hilos ejecutando exactamente las mismas sentencias pero cada uno de ellos sobre un dato diferente de una estructura mayor. Ese grupo de sentencias suele ser breve, llevando a cabo operaciones relativamente sencillas. A diferencia de lo que ocurre con los hilos, ni sistemas operativos ni plataformas/lenguajes (como .NET o Java) permiten crear este tipo de programas para ejecutarlos en GPU, siendo necesario recurrir a soluciones dependientes de cada fabricante como puede ser CUDA en el caso de NVidia o Stream en el de ATI.

¿Cómo podría una misma aplicación, con el objetivo de explotar toda la potencia de un ordenador actual, aprovechar tanto el paralelismo de la CPU como de la GPU? Hasta no hace tanto esto implicaba crear hilos en la CPU para las tareas MIMD y kernels en la GPU para las tareas SIMD, con herramientas distintas y en ocasiones lenguajes de programación distintos, integrando los diversos componentes de la mejor manera posible. Una alternativa que cuenta con el favor de una gran parte de la industria es OpenCL, un estándar que hace posible la ejecución de código escrito en C/C++ distribuyendo las tareas entre CPU y GPU, sin que importe el fabricante de la GPU. Esa capacidad, no obstante, es actualmente teórica en un escenario en el que cada empresa intenta favorecer su oferta sobre la de la competencia.

Si se cuenta con un hardware gráfico de nVidia, los controladores OpenCL de esta empresa solamente reconocerán como dispositivo la propia GPU, tal y como se aprecia en la ventana de la izquierda de la imagen inferior. Los controladores de ATI/AMD (menos avanzados que los de nVidia en el soporte OpenCL), por el contrario, sí reconocen la CPU como dispositivo (ventana de la derecha), pero obviamente no pueden usar la GPU de nVidia.

Es necesario, por tanto, contar con una configuración hardware específica si se quiere usar OpenCL o, de lo contrario, recurrir a la oferta específica de cada fabricante: CUDA o Stream. Una opción adicional, pero que tal como indiqué en la entrada antes citada es válida únicamente para Windows, sería DirectCompute, una API similar a OpenCL que hace posible la programación GPGPU sin que importe el fabricante de hardware. En realidad ésta es una verdad a medias, ya que recientemente Microsoft el equipo de desarrollo de Mesa/Galium3D anunció que habrá una implementación nativa de DirectX 10/11 para Linux, lo cual permitiría usar DirectCompute en Windows/Linux (por ahora no en Mac) sin que importe el fabricante.

Otra alternativa más, aunque habrá que esperar para ver qué ofrece, sería usar CUDA para x86, una versión de CUDA que se ejecutaría sobre las actuales CPU y que acaba de ser anunciada por nVidia. Todavía no conozco los detalles, pero es de suponer que esto permitirá usar CUDA para ejecutar código tanto en la GPU como en la CPU, situando a CUDA como el competidor más directo de OpenCL que, anecdóticamente, debe gran parte de su desarrollo a la propia nVidia.


Publicado el 22/9/2010

Histórico
Curso de shaders

Torre de Babel - Francisco Charte Ojeda - Desde 1997 en la Web