Logo de Torre de Babel
Portada Libros Diseño web Artículos Glosario RSS
Buscar

Sobre la imprecisión y la lógica difusa

Durante nuestra actividad cotidiana, a lo largo de la vida pero también en lapsos de tiempo reducido como puede ser un día o incluso pocos segundos, tomamos infinidad de decisiones que exigen la evaluación de situaciones de diversa complejidad. Aunque de manera inconsciente, no nos paramos a pensar en cómo lo hacemos, esa evaluación implica la valoración de múltiples variables, cuyo estado (valor) en conjunto nos lleva a inclinarnos por una acción u otra.

En la mayoría de las ocasiones, por no decir siempre, es más importante el hecho de que la decisión se tome a tiempo: lo antes posible, que de manera lo más exacta posible. Si la ejecución de una acción se demora en exceso es posible que cuando se tome ya no resulte útil, por tardía. Un claro ejemplo lo encontramos al pensar en cómo frenábamos nuestro automóvil al detectar la presencia de un peatón: es obvio que lo importante es frenar a tiempo, más que de manera absolutamente precisa, si no queremos que haya un siniestro. Otro ejemplo similar es el del péndulo invertido (vídeo).

Pienso que ese requerimiento de actuar rápido, que resultaba imprescindible para la supervivencia hace miles de años y que hemos ido desarrollando durante nuestra evolución y adaptando a las necesidades de cada momento, justifican en gran parte el funcionamiento de nuestra mente y, en consecuencia, la manera en que nos enfrentamos a las situaciones y decidimos. Es un método suficientemente probado, sabemos que funciona porque de lo contrario posiblemente no estaríamos aquí, así que ¿por qué no utilizarlo para automatizar la toma de decisiones y otros procesos en un ordenador? Ése es precisamente el objetivo de la teoría de conjuntos difusos y la lógica difusa.

Sobre la terminología

Es habitual que al hablar no seamos excesivamente precisos en los términos que utilizamos y que, en muchos casos, utilicemos una cierta palabra asumiendo que representa el concepto que deseamos transmitir por ser sinónima (en mayor o menor grado) de la que seria correcta. La imprecisión, por tanto, está presente incluso en la forma que tenemos de hablar y por extensión en la de pensar (según diferentes estudios incluso cuando pensamos en silencio es imposible hacerlo sin utilizar la lengua materna, usando palabras y oraciones).

Para situarse correctamente es importante no confundir la imprecisión con la incertidumbre, ni tampoco con la indeterminación, términos éstos que, en una conversación, podrían pasar por sinónimos sin serlo en realidad.

La RAE define incertidumbre como la falta de certidumbre, ésta como sinónimo de certeza y ésta, a su vez, como el conocimiento seguro y claro de algo. Tener incertidumbre, por tanto, es no tener seguro ni claro el conocimiento sobre algo, por regla general algún evento. Obviamente la incertidumbre desaparece por completo una vez que dicho evento ha tenido lugar.

Análogamente la definición de imprecisión nos lleva a la falta de precisión, expresada en la tercera de las acepciones como la concisión y exactitud rigurosa en el lenguaje. En consecuencia al ser imprecisos usamos un lenguaje no conciso y cuya exactitud no es rigurosa, lo cual se ajusta bastante bien a la forma de expresarnos habitualmente: ese coche es bastante rápido, sin especificar su velocidad; mi compañero es muy alto, sin concretar la estatura, etc.

Un proceso de búsqueda similar para la palabra indeterminación nos lleva a concluir que es la imposibilidad de distinguir, discernir o fijar los términos de algo, por lo que podría decirse que la indeterminación se obtiene de la siguiente ecuación: indeterminación = incertidumbre + imprecisión.

Cuando algo es tan impreciso como para no poder ser distinguido de otros similares y, además, no somos capaces de aportar conocimiento seguro sobre él nos encontramos ante una indeterminación. El tratamiento de la indeterminación es un tema que queda fuera del ámbito de este artículo, lo que me interesaba era señalar el hecho de que no es sinónimo en ningún caso del término imprecisión.

Distinguir claramente entre estos términos es importante porque la forma de actuar del ser humano, y los métodos derivados de ella, son específicos según se trate de un imprecisión, incertidumbre o una combinación de ambas.

El sentido común humano

¿Cómo se enfrenta una persona a una situación cualquiera en su vida cotidiana con independencia de cuál sea su preparación o conocimiento previo sobre la misma? Asumimos que para ello tenemos el sentido común, una suerte de método innato en cuyo funcionamiento nunca nos paramos a pensar. ¿Cómo abordamos, por ejemplo, la necesidad de cruzar andando una vía en una ciudad?

Si la vía es una calle estrecha y larga, sabemos que normalmente nos dará tiempo a cruzarla si no vemos ningún vehículo en uno de los extremos (será de un único sentido). Si a pesar de ser estrecha es corta (de poca longitud), hemos de tener en cuenta la probabilidad de que un vehículo aparezca de repente por algún lado. La situación se complica si la vía es ancha y, aparte de permitir la circulación en dos o más carriles, da facilidad a los conductores para ir a mayor velocidad. Es necesario, por tanto, tener en cuenta los vehículos que se desplazan en cada sentido, la velocidad a la que lo hacen y nuestra propia agilidad para cruzar esa vía.

Analizando el problema soy capaz de identificar al menos las siguientes variables:

  • La velocidad de los vehículos que circulan por la vía
  • El ancho de la vía
  • El tiempo en que somos capaces de cruzar la vía
  • La posibilidad de que aparezcan vehículos de manera inesperada

De la evaluación de éstas surgirá la decisión de en qué momento cruzar y cómo hacerlo (con más o menos celeridad).

La incertidumbre ante los sucesos

Comienzo por la cuarta variable de las antes mencionadas: posibilidad de que otros vehículos entren en la vía y, en consecuencia, debamos tenerlos en cuenta a la hora de cruzarla. ¿Cómo se evalúa esa posibilidad? En ella influirá la longitud de la vía: si es muy larga y estamos en la parte central nos preocupará poco que entren vehículos por los extremos; la existencia de intersecciones con otras calles y también la de garajes.

Salvo que seamos adivinos o capaces de ver el futuro, esta variable lo que hace es agregar incertidumbre a la situación. ¿Cómo la tratamos para poder decidir? Echando mano de las probabilidades: el número de garajes, intersecciones con otras vías e incluso la densidad del tráfico (según la hora que sea) influyen en la probabilidad de que otros vehículos entren en la vía.

Aunque en un documento como éste, en el que se realiza un análisis detallado de la situación, podríamos dar valores a las variables y, sirviéndonos de la teoría de probabilidades, saber aproximadamente qué posibilidad hay de que ese evento suceda, en la situación real esto es imposible. En términos generales a las personas no se nos da bien el cálculo de probabilidades, por ello nos empeñamos en jugar a la lotería y nos preocupa más viajar en avión que cruzar la calle. En el primer caso la posibilidad de ganar es ínfima y en el segundo la de que tengamos un accidente es mucho mayor al cruzar la calle que en el avión.

La incertidumbre ante un evento potencial, por tanto, se evalúa como la probabilidad a priori de que se dé y tenemos herramientas para tratarla: el cálculo de probabilidades. Esa incertidumbre desaparece en el momento en que el evento concluye: nuestra incertidumbre ante el lanzamiento de una moneda nos dice que hay un 0.5 de probabilidad para cada resultado, pero una vez lanzada la probabilidad sera 1 para el resultado obtenido y 0 para el opuesto.

La necesidad de la imprecisión

Solamente una de las cuatro variables antes citadas aporta incertidumbre a la situación, mientras que las otras tres pueden ser establecidas con completa certeza: el ancho de la vía será un número concreto de metros, centímetros, milímetros, etc.; la velocidad de los vehículos se puede medir en m/s y también podemos medir el tiempo que tardamos en cruzar la calle.

No obstante ningún peatón conoce esas medidas de manera exacta (no llevamos una cinta métrica para saber el ancho de la vía y un radar para conocer la velocidad de los vehículos) sino que las estima, por lo que aportan imprecisión a la situación.

En realidad si nos diesen esos datos con absoluta precisión no nos ayudarían en nada para tomar nuestra decisión de si debemos cruzar o no. ¿De qué nos sirven los centímetros y milímetros de ancho de la vía o los m/s exactos de velocidad de cada vehículo? Es suficiente con saber que la vía es muy estrecha, estrecha, normal, ancha o muy ancha y que los vehículos se acercan despacio, a velocidad normal o rápidamente. Como es fácil apreciar, éstas son etiquetas lingüísticas que asocian un valor impreciso (difuso) pero útil a cada variable.

Por lo tanto en este contexto hay que entender la imprecisión no como la existencia de ambigüedad o generalidad, sino como la ocultación de detalles innecesarios en una situación en la que el detalle excesivo en la información dificulta la operación en lugar de facilitarla. Es esta reducción del nivel de detalle, adecuándolo a la necesidad de la situación, lo que hace de la imprecisión una herramienta robusta en la toma de decisiones, una fortaleza del lenguaje humano que le permite hacer frente a sucesos muy variopintos que encuentra en su entorno.

Cómo trasladar a un ordenador la lógica humana

Se indicaba antes que para el tratamiento de la incertidumbre se ha desarrollado una teoría sólida y aceptada de manera general: la teoría de probabilidades. Ésta es fácilmente trasladable a un ordenador y permite automatizar, mediante herramientas como las redes bayesianas, la toma de decisiones para ciertos tipos de problemas.

Sería deseable, por tanto, contar con una teoría análoga que nos permitiese operar cuando lo que se tiene es imprecisión en lugar de incertidumbre. Frente a la imprecisión la teoría de probabilidades resulta totalmente inútil. En realidad la matemática tradicional en su conjunto no servirá de demasiado y, por ello, se hace necesaria alguna herramienta más específica.

Teorías sobre la imprecisión

En palabras de Bertrand Rusell "toda la lógica tradicional asume el empleo de símbolos precisos", por lo que ya de entrada descarta por completo el uso de la imprecisión. Sin embargo ésta está presente en la vida real, como bien resumió Einstein en su famosa frase "Cuando las leyes de la matemática se refieren a la realidad no son ciertas y cuando son ciertas no se refieren a la realidad".

Según se ha visto, la imprecisión es necesaria cuando se desea aportar información útil y relevante sobre algún problema o situación compleja, ante la cual el uso de abundancia de detalles dificultarían la comprensión global. Podría decirse que cuanto mayor sea el nivel de detalle mejor conoceremos los componentes del problema, pero será menos relevante para comprender el todo.

Ya en la década de los 30 Max Black, citando la incertidumbre en la geometría de Platón, habla de conjuntos vagos y argumenta que los objetos con los que se trata en distintas teorías, como la geometría perfecta, los planetas como puntos de la astronomía o los gases perfectos de la termodinámica, nada tienen que ver con los objetos de la vida real. Se precisa una lógica que trate de la experiencia, más que de la teoría, tratando con objetos que están lejos de la perfección matemática.

Black define una proposición vaga como aquella en la que no están nítidamente definidos sus posibles estados respecto a la inclusión o exclusión, ya que éstos dependen en cierta medida de la interpretación que cada individuo haga de ellos.

Un ejemplo de este tipo de proposiciones podría ser "ser una persona mayor". Para mi hijo una persona mayor es alguien que tiene la edad de su padre o más, pero para mi madre yo no estaría incluido en el grupo de personas mayores porque soy mucho más joven que ella. Ante un problema así la lógica binaria clásica no es aplicable y, por tanto, hay que buscar una lógica alternativa.

A partir de este tipo de proposición Black habla del nivel de pertenencia en que los objetos son miembros de un conjunto, usando diferentes gradaciones y sentando las bases para una teoría general sobre la imprecisión (vagueness).

Las aportaciones de Black no fueron las únicas en ocuparse de la imprecisión, pero todo parece indicar que fueron la base sobre la que se asentó Zadeh para crear la teoría más aceptada en este campo: la de los conjuntos y la lógica difusa.

La propuesta de Zadeh

Es en la década de los 60, con la publicación de Lotfi Zadeh titulada Fuzzy sets, cuando comienza verdaderamente a desarrollarse una teoría útil para el tratamiento de la imprecisión. De hecho Zadeh se ha dedicado en gran parte durante las últimas cuatro décadas a extender las ideas recogidas en dicha publicación, desarrollando la teoría de conjuntos difusos, la lógica difusa y sus aplicaciones.

Aparte de los conjuntos vagos de Black antes mencionados, Zadeh se apoya en la lógica multivalorada de Lukaszewicz para dar a luz a los conjuntos difusos y la lógica difusa que, inicialmente, no tienen una buen acogida en la comunidad científica en gran medida por el sentido peyorativo asociado a la palabra difuso. Otro de los pilares es el principio de incompatibilidad de Pierre Duhem, que Zadeh redefine de la siguiente manera: "en la medida en que se incrementa la complejidad de un sistema se reduce proporcionalmente nuestra capacidad para explicar su funcionamiento de una manera precisa y que resulte relevante, hasta llegar a un límite a partir del cual precisión y relevancia se convierten en características prácticamente excluyentes".

El principal mérito del trabajo de Zadeh es que evidencia la posibilidad de usar las matemáticas para crear un vínculo entre el lenguaje humano y la inteligencia subyacente que conduce nuestra forma de actuar, tomando como base la lógica matemática clásica para crear una nueva capaz de operar con la imprecisión de manera sistemática, análoga a como ya se venía tratando la incertidumbre.

Los distintos trabajos de Zadeh, principalmente los publicados en las décadas de los 70, 80 y 90, introducen las herramientas principales de esta nueva teoría: los conjuntos difusos y funciones de pertenencia, las relaciones difusas, las sentencias cuantificadas, los números difusos, la lógica difusa, la extracción de reglas difusas, etc.

Pienso que la idea fundamental de toda la teoría es el hecho de definir cómo se opera sobre variables sin necesidad de cuantificarlas de manera exacta, empleando las variables lingüísticas que se ajustan mucho mejor al pensamiento humano. Para ello ha sido necesario inventar nuevos operadores, como las t-normas y t-conormas o los α-cortes, la proyección y extensión cilíndrica que permiten analizar las relaciones, etc.

Las etiquetas lingüísticas actúan como las distintas gradaciones de una variable difusa y pueden tener diferente granularidad. Es importante el hecho de que se preserve la idea de continuidad: teóricamente una variable puede tomar infinitos valores distintos, aunque por regla general se representen con 3, 5 ó 7 etiquetas. Es la continuidad precisamente la que posibilita que una pequeña diferencia entre una cierta propiedad de dos objetos no los haga distintos: dos personas con estatura de 1,90 y 1,95 se considerarán altas, a pesar de la variación de 5cm entre una y otra.

Personalmente encuentro una cierta relación entre esa continuidad o suavidad en la transición entre las etiquetas de una variable difusa y el método de inducción que empleamos con mucha frecuencia: si consideramos que una estatura N es alta, también serán altas las estaturas N-1 y N+1. Lo que cambia es la medida en que se pertenece a ese grupo.

A partir de esta teoría se han obtenido aplicaciones prácticas concretas, de las cuales quizá el mayor exponente sean los controladores difusos. La base de éstos es, en principio, bastante sencilla: la propia descripción imprecisa de un proceso por parte de una persona se convierte en un manual que permite controlarlo. Con un conjunto reducido de reglas pueden controlarse procesos aparentemente complejos y es un método que se emplea actualmente en multitud de electrodomésticos.

Con el inicio del nuevo siglo Zadeh dio un nuevo giro a sus investigaciones y propuso la idea del CW (Computing with Words) que, según sus propias palabras, permitirá abrir una nueva vía en la evolución de la lógica difusa y sus aplicaciones.

Si bien la teoría de conjuntos difusos y lógica difusa de Zadeh es la más aceptada en la actualidad, no es la única y existen varias teorías más sobre la imprecisión. Una de ellas, bastante desarrollada, se denomina AST: Alternative Set Theory. Su base es que la incertidumbre surge por el fenómeno de la infinitud al tratar con grandes variedades de estados posibles, argumentando que puede demostrarse que los números muy grandes, pero finitos, se comportan como los infinitos desde la perspectiva de la mentalidad humana. La infinitud es una cualidad dependiente del contexto.

Conclusiones

El pensamiento humano es impreciso por naturaleza, es el principio que le permite ajustar el nivel de detalle que necesita en cada momento para poder dar respuesta a todos los problemas a los que ha de enfrentarse a lo largo de su vida. En contraposición, en el cálculo matemático y la lógica clásica es precisamente la precisión el pilar fundamental. Anecdóticamente resulta bastante más sencillo formular operaciones sobre datos precisos que con datos imprecisos. Por ello teorías como las de la probabilidad llevan asentadas desde hace más de dos siglos y, por el contrario, hasta hace pocas décadas se carecía de una teoría que permitiese tratar de forma sistemática la imprecisión.

Las teorías sobre la imprecisión han tratado de dar respuesta a la necesidad de contar con herramientas que permitan de alguna manera modelar los problemas como lo hace la mente humana, entrando en campos como la psicología, neurología, biología y computación. Todo este esfuerzo se explica por el importante papel que en ingeniería y ciencias de la computación tiene la capacidad para tratar con la imprecisión.

Se puede ampliar este artículo con la bibliografía referenciada a continuación si se quiere obtener una idea global sobre multitud de aspectos que explican porqué es interesante aprender sobre conjuntos difusos, lógica difusa, reglas difusas, etc., ya que son las herramientas que permitirán simular/emular en un programa de ordenador el comportamiento de uno mismo y, por tanto, abre las puertas a otras maneras de resolver los problemas, seguramente más coherentes con el pensamiento humano.

Si quieres saber más
  • Nikravesh, M. (2007) Evolution of fuzzy logic: from intelligent systems and computation to human mind. Studies in Fuzziness and Soft Computing, 2007, Volume 217/2007, 37-53, DOI: 10.1007/978-3-540-73182-5_3.
  • Novák, V.(2005) Are fuzzy sets a reasonable tool for modeling vague phenomena? Fuzzy Sets and Systems Volume 156, Issue 3, 16 December 2005, Pages 341-348.
  • Russell, B. (1923) Vagueness.
  • Black, M. (1937) Vagueness: An exercise in logical analysis, International Journal of General Systems, Volume 17, Issue 2 & 3 June 1990 , pages 107 - 128.

Publicado el 20/7/2011

Introducción a la programación GPGPU con CUDA

A medida que se han ido incorporando en la tarjeta de vídeo funciones más avanzadas, influidas por la necesidad de satisfacer requisitos más exigentes en la generación de gráficos y también las nuevas API de programación (OpenGL y DirectX), ha surgido un nuevo concepto: el de GPU (Graphics Processor Unit), como analogía de las CPU (Central Processing Unit) o microprocesadores clásicos. En realidad es algo que lleva casi dos décadas evolucionando, aunque para los programadores ajenos al campo de los vídeojuegos pueda considerarse algo nuevo.

Si bien la denominación GPU hace referencia a una arquitectura especializada, dirigida específicamente al tratamiento gráfico, cada vez es mayor el número de aplicaciones que aprovechan la potencia de estos circuitos integrados para otro tipo de propósitos. Es aquí, precisamente, donde cobran protagonismo soluciones como Cg, ATI Stream, OpenCL y CUDA, infraestructuras compuestas de bibliotecas, compiladores y lenguajes que dan a luz a una nueva filosofía de desarrollo: GPGPU (General Purpose computing on Graphics Processing Units), la computación de propósito general usando GPUs en lugar de CPUs.

¿Cuál es la razón de que los programadores se interesen en desarrollar aplicaciones que se ejecuten en una GPU? La respuesta surge por sí sola simplemente aportando algunos datos: las actuales CPU cuentan con cuatro, seis u ocho núcleos y son capaces de ejecutar hasta doce hilos de manera simultánea (dos por núcleo en algunos casos), las GPU más avanzadas disponen de hasta 1024 núcleos de procesamiento y tienen capacidad para ejecutar hasta 128 hilos por procesador, lo que ofrece un total de hilos muy superior. Sistemas compuestos únicamente de ocho tarjetas de vídeo de este tipo están superando, en cuanto a rendimiento se refiere, a clústeres de ordenadores como Blue Gene, formados por 512 nodos con CPUs clásicas.

Programación de GPU

Para aprovechar la potencia de las GPU es necesario contar con herramientas de desarrollo adecuadas, capaces de explotar el alto nivel de paralelismo que ofrecen estos dispositivos. Hasta no hace mucho dichas herramientas eran bastante primitivas, ya que su objetivo era facilitar exclusivamente la programación de shaders que (véase el Curso de shaders en el margen derecho) son pequeños bloques de código que aplican un cierto procesamiento a los vértices de la geometría de una escena y los fragmentos resultantes de la rasterización. Ese código se ejecuta paralelamente en cada núcleo, lo cual permite aplicar un cierto algoritmo masivamente a miles o millones de vértices y píxeles.

Estos bloques de código tienen una longitud generalmente muy limitada y se programan en una suerte de lenguaje ensamblador a medida, por lo que difícilmente pueden aplicarse más que a la función para la que están pensados desde un principio. Existen diferentes versiones, denominadas Shader Models, que han ido evolucionando en paralelo a Microsoft DirectX y OpenGL y que tanto ATI como nVidia han ido implementando en su hardware. Al desarrollar una aplicación gráfica se utiliza una API, como las citadas DirectX u OpenGL, para escribir el código que se ejecutará en la CPU, usando el ensamblador del shader model correspondiente para escribir el código a ejecutar en la GPU. Tanto el tipo de operaciones que puede llevar a cabo ese código como la memoria a la que tiene acceso están limitados.

El desarrollo de Cg por parte de nVidia, hace prácticamente una década, fue un primer avance al facilitar la codificación de funciones a ejecutar en la GPU. En lugar de escribir el código en ensamblador se usa un lenguaje de más alto nivel, similar al C. Una función como la mostrada en el siguiente fragmento se ejecutaría una vez para cada vértice, pero no de manera secuencial sino paralelamente.

A diferencia de CUDA, no obstante, Cg se dirige específicamente a la generación de gráficos. A medida que el número de núcleos de proceso en una GPU se fue incrementando, y ganando en rendimiento al poder operar con datos en coma flotante, se hizo cada vez más patente la necesidad de aprovechar esa potencia bruta de cálculo para propósitos alternativos, aparte de la evidente aplicación en videojuegos de última generación. Solamente se precisaban herramientas de trabajo de corte más general, con un espectro de aplicación más amplio.

¿Qué es CUDA?

La mayoría de los lenguajes de programación no cuentan con estructuras nativas que faciliten la paralelización de procesos, entendiendo como tales partes de un algoritmo que pueden ser ejecutados de manera simultánea y no como lo que se entiende por procesos en el contexto de un sistema operativo.

Es cierto que existen API y bibliotecas de funciones que facilitan, hasta cierto punto, la programación paralela, pero prácticamente ninguna de ellas está pensada para ejecutar el código explotando una GPU. En la mayoría de los casos lo único que hacen es iniciar varios hilos de ejecución dejando en manos del sistema operativo el reparto de tiempo de proceso entre las unidades con que cuente la CPU. Para trasladar la aplicación a otro tipo de procesador, así como para ampliar o reducir el número de hilos en ejecución, es corriente tener que alterar, o incluso rescribir por completo, el código fuente.

La solución que ofrece CUDA (Compute Unified Device Architecture) es mucho más flexible y potente y, además, se basa en estándares existentes. Los programas se escriben en lenguaje C, no en el ensamblador de un cierto procesador o en un lenguaje especializado como es el caso de Cg. Esto facilita el acceso a un grupo mucho mayor de programadores.

Al desarrollar una aplicación CUDA el programador escribe su código como si fuese a ejecutarse en un único hilo, sin preocuparse de crear y lanzar threads, controlar la sincronización, etc. Ese código será ejecutado posteriormente en un número arbitrario de hilos, asignado cada uno de ellos a un núcleo de proceso, de manera totalmente transparente para el programador. Éste no tendrá que modificar el código fuente, ni siquiera recompilarlo, dependiendo de la arquitectura del hardware donde vaya a ejecutarse.

Incluso existe la posibilidad de recompilar el código fuente, dirigido originalmente a ejecutarse sobre una GPU, para que funcione sobre una CPU clásica, asociando los hilos CUDA a hilos de CPU en lugar de a núcleos de ejecución de GPU. Obviamente el rendimiento será muy inferior ya que el paralelismo al nivel de CPU no es, actualmente, tan masivo como en una GPU.

Componentes de CUDA

Los objetivos planteados en el desarrollo de CUDA han dado como fruto un conjunto de tres componentes, disponibles gratuitamente en Developer Zone de nVidia para versiones de 32 y 64 bits de Windows XP, Windows Vista, Windows 7, múltiples distribuciones de GNU/Linux y Mac OS X.

El controlador CUDA es el componente básico, ya que es el encargado de facilitar la ejecución de los programas y la comunicación entre CPU y GPU. Este controlador se aplica a prácticamente toda la gama GeForce 8XX, 9XX y GTX 2XX y posteriores, así como a la línea de adaptadores Quadro y los procesadores Tesla. En cualquier caso se requiere una cantidad mínima de 256 MB de memoria gráfica para poder funcionar, por lo que en adaptadores con menos memoria no es posible aprovechar CUDA.

Instalado el controlador, el siguiente componente fundamental para el desarrollo de aplicaciones es el toolkit CUDA, compuesto a su vez de un compilador de C llamado nvcc, un depurador específico para GPU, un perfilador de código y una serie de bibliotecas con funciones de utilidad ya predefinidas, entre ellas la implementación de la Transformada rápida de Fourier (FFT) y unas subrutinas básicas de álgebra lineal (BLAS). También en la misma web se encuentran múltiples documentos de introducción a la programación de GPUs con CUDA, manuales de referencia, etc.

El tercer componente de interés es el CUDA Developer SDK, un paquete formado básicamente por código de ejemplo y documentación. Se ofrece más de medio centenar de proyectos en los que se muestra cómo integrar CUDA con DirectX y OpenGL, cómo paralelizar la ejecución de un algoritmo y cómo utilizar las bibliotecas FFT y BLAS para realizar diversos trabajos: generación de un histograma, aplicación de convolución a una señal, operaciones con matrices, etc.

Conjuntamente estos tres componentes ponen al alcance del programador todo lo que necesita para aprender a programar una GPU con CUDA y comenzar a desarrollar sus propias soluciones, apoyándose en código probado como el de los ejemplos facilitados o el de las bibliotecas FFT y BLAS.

Estructura de una aplicación CUDA

El código de un programa escrito para CUDA siempre estará compuesto de dos partes: una cuya ejecución quedará en manos de la CPU y otra que se ejecutará en la GPU. Al código de la primera parte se le denomina código para el host y al de la segunda código para el dispositivo.

Al ser procesado por el compilador nvcc, el programa generará por una parte código objeto para la GPU y por otra código fuente u objeto para la CPU. El código objeto específico para la GPU se denomina cubin. El código fuente para la CPU será procesado por un compilador de C/C++ corriente, enlazando el código cubin como si de un recurso se tratase.

La finalidad del código host es inicializar la aplicación, transfiriendo el código cubin a la GPU, reservando la memoria necesaria en el dispositivo y llevando a la GPU los datos de partida con los que se va a trabajar. Esta parte del código puede escribirse en C o en C++, lo cual permite aprovechar el paradigma de orientación a objetos si se quiere.

El código a ejecutar en el dispositivo debe seguir estrictamente la sintaxis de C, contemplándose algunas extensiones de C++. Normalmente se estructurará en funciones llamadas kernels, cuyas sentencias se ejecutarán en paralelo según la configuración hardware del dispositivo final en el que se ponga en funcionamiento la aplicación.

Lo que hace el entorno de ejecución de CUDA, a grandes rasgos, es aprovechar el conocido como paralelismo de datos o SIMD (Simple Instruction Multiple Data), consistente en dividir la información de entrada, por ejemplo una gran matriz de valores, en tantos bloques como núcleos de procesamiento existan en la GPU. Cada núcleo ejecuta el mismo código, pero recibe unos parámetros distintivos que le permiten saber la parte de los datos sobre los que ha de trabajar.

El listado siguiente corresponde a una función kernel muy sencilla, cuyo objetivo es hallar el producto escalar de una matriz por una constante. La función solamente opera sobre un elemento de la matriz, el que le indica la variable threadIdx.x que identifica el hilo en que está ejecutándose el código. Esta función se ejecutaría paralelamente en todos los núcleos de la GPU, por lo que en un ciclo se obtendría el producto de una gran porción de la matriz o incluso de ésta completa, dependiendo de su tamaño y el número de núcleos disponibles.

En una CPU moderna, como los Athlon Phenom o Core i7, es posible dividir los datos de entrada en cuatro o seis partes pero sin ninguna garantía de que se procesarán en paralelo, salvo que se programe explícitamente el reparto trabajando a bajo nivel. En una GPU y usando CUDA, por el contrario, esos datos se dividirán en bloques mucho más pequeños, al existir 240, 512, 1024 o más núcleos de procesamiento, garantizándose la ejecución en paralelo si necesidad de recurrir a la programación en ensamblador.


Publicado el 6/7/2011

Curso de shaders

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