Departamento de Electr´onica e Sistemas
Proxecto de Fin de Carreira de Enxe˜
nar´ıa Inform´
atica
Implementaci´
on de una herramienta de
optimizaci´
on iterativa para c´
odigos OpenCL
Autor: Jorge Fern´andez Fabeiro
Directores: Diego Andrade Canosa
Basilio B. Fraguela Rodr´ıguez
T´ıtulo: Implementaci´on de una herramienta de optimizaci´on iterativa para c´odigos OpenCL
Clase: Investigaci´on y desarrollo
Autor : Jorge Fern´andez Fabeiro
Directores: Diego Andrade Canosa
Basilio Bernardo Fraguela Rodr´ıguez
Tribunal :
Fecha de lectura:
Calificaci´on:
Santiago Ram´on y Cajal
En primer lugar, me gustar´ıa agradecer a los profesores Diego Andrade y Basilio B. Fraguela el haber vuelto a darme la oportunidad de trabajar con ellos proponi´endome llevar a cabo el presente proyecto, as´ı como toda la ayuda prestada a lo largo de estos meses.
As´ı mismo, este proyecto no habr´ıa llegado a buen puerto sin los consejos de la gente del laboratorio del Grupo de Arquitectura de Computadores, en especial de Diego Darriba (siempre aguant´andome cuando alguna cosa se torc´ıa), Iv´an Cores (dando cuando uno menos se lo espera ese toque de humor tan necesario) y Mois´es Vi˜nas (siempre dispuesto a echar un cable con todas las pr´acticas y trabajos del M´aster).
Llegar hasta aqu´ı tampoco habr´ıa sido posible sin la compa˜n´ıa de Jorge, Sergio y V´ıctor. Sin ellos, estos tres a˜nos que he necesitado para terminar el segundo ciclo no habr´ıan sido lo mismo. Tampoco me olvido de los compa˜neros (ellos ya saben quienes son) que han compartido conmigo pr´acticamente toda mi vida universitaria desde aquel principio de curso de octubre de 2005.
Ya para terminar, y en absoluto por ello menos importante, nunca podr´e agrade-cer lo suficiente a mis padres que siempre hayan estado ah´ı, apoyando lo que hago y anim´andome a continuar con ello (si es que los recortes no acaban con la Universidad P´ublica...).
A todos vosotros, de nuevo, muchas gracias.
La evoluci´on de la arquitectura de computadores ha permitido que, en la actualidad, cualquier ordenador cuente con capacidades de procesamiento paralelo gracias a que contiene un procesador multin´ucleo y una tarjeta gr´afica con capacidades GPGPU. En este contexto aparece un nuevo paradigma conocido como((computaci´on heterog´enea)), que busca explotar simult´aneamente todos esos recursos para obtener el m´aximo rendi-miento posible. Dentro de dicho paradigma, OpenCL parece ser la opci´on mejor posicio-nada para la programaci´on de este tipo de arquitecturas. Sin embargo, OpenCL cuenta con el inconveniente de que la portabilidad de c´odigo entre arquitecturas no se ve refle-jada de forma efectiva en el rendimiento: un c´odigo optimizado para una determinada plataforma puede ser ejecutado en otra diferente, pero dif´ıcilmente se conseguir´a el mismo rendimiento que en la original para la que fue optimizado.
La herramienta OCLOptimizer implementada en este Proyecto Fin de Carrera bus-ca dotar a un usuario-programador experto de todo lo necesario para conseguir esta portabilidad efectiva. La herramienta recibe como entradas un kernel OpenCL anotado con una serie de directivas y un fichero de configuraci´on en el que especifica informa-ci´on como los par´ametros de entrada del kernel, el tipo de dispositivo sobre el que se ejecutar´a, etc. Las directivas deben ser introducidas por el usuario e indican a la he-rramienta qu´e optimizaciones se deben probar sobre qu´e partes del c´odigo. La salida se compone de un c´odigo de host autogenerado y una versi´on del kernel optimizada para una determinada plataforma. En esta primera versi´on de la herramienta se ha implementado como ´unica t´ecnica a aplicar por la herramienta el desenrollamiento de bucles. Los resultados experimentales muestran que del uso de la herramienta se pueden
asociados a cada kernel, liberando as´ı al programador del tedioso y repetitivo trabajo que supone su escritura.
Palabras clave
1. Introducci´on 1
1.1. Motivaci´on . . . 2
1.2. Objetivos . . . 4
1.3. Estado del arte . . . 5
1.3.1. Procesadores multin´ucleo . . . 5
1.3.2. Surgimiento y popularizaci´on de la computaci´on GPGPU . . . . 6
1.3.3. Herramientas de computaci´on GPGPU y heterog´enea . . . 8
1.3.4. T´ecnicas de optimizaci´on autom´atica . . . 9
1.3.5. Herramientas de an´alisis y transformaci´on de c´odigo . . . 12
1.4. Metodolog´ıa de desarrollo . . . 13
1.5. Planificaci´on del trabajo . . . 13
1.6. Estructura del documento . . . 14
2. El est´andar OpenCL 15 2.1. Introducci´on . . . 15
2.1.1. Comunidad de desarrollo . . . 16
2.1.2. Evoluci´on y situaci´on actual del proyecto . . . 16
2.1.3. Implementaciones disponibles . . . 18 2.2. La arquitectura OpenCL . . . 18 2.2.1. Modelo de plataforma . . . 19 2.2.2. Modelo de ejecuci´on . . . 19 2.2.3. Modelo de memoria . . . 23 ix
2.2.4. Modelo de programaci´on . . . 26
2.3. Programaci´on de aplicaciones con OpenCL . . . 28
2.3.1. Desarrollo de un c´odigo de host . . . 29
2.3.2. Desarrollo de un c´odigo de kernel . . . 35
2.3.3. Comentarios . . . 37
3. LLVM y Clang 39 3.1. La infraestructura de compilaci´on LLVM . . . 39
3.1.1. Inciativas surgidas desde LLVM . . . 40
3.2. El frontend Clang . . . 41
3.2.1. Motivaci´on . . . 41
3.2.2. Objetivos . . . 42
3.2.3. Caracter´ısticas . . . 42
3.2.4. Arquitectura . . . 43
3.3. An´alisis y transformaci´on de c´odigo con Clang . . . 46
3.3.1. Introducci´on . . . 46
3.3.2. An´alisis de c´odigo . . . 46
3.3.3. Transformaci´on de c´odigo . . . 53
3.3.4. Comentario sobre los tutoriales . . . 61
4. La herramienta OCLOptimizer 63 4.1. Descripci´on y funcionamiento de la herramienta . . . 63
4.1.1. Preprocesado . . . 69
4.1.2. Optimizaci´on . . . 69
4.1.3. Evaluaci´on . . . 71
4.2. Ejemplo completo de ejecuci´on . . . 74
4.2.1. C´odigo original y c´odigo anotado . . . 74
4.2.2. Fichero de configuraci´on . . . 75
4.2.3. Descripci´on del proceso de optimizaci´on iterativa . . . 76
4.3. Estructura . . . 80
4.4.1. Introducci´on te´orica . . . 84
4.4.2. Implementaci´on en la herramienta . . . 87
4.5. Detalles de dise˜no e implementaci´on . . . 92
4.5.1. Interacci´on con Clang . . . 92
4.5.2. Modelado de las optimizaciones . . . 95
5. Resultados experimentales 99 5.1. Producto matriz-vector . . . 100
5.2. Convoluci´on de im´agenes . . . 103
5.3. Resumen . . . 109
6. Conclusiones 111 6.1. Resumen del trabajo realizado . . . 111
6.2. Objetivos alcanzados . . . 112
6.3. L´ıneas futuras . . . 114
A. Manual de usuario 115 A.1. Instalaci´on . . . 115
A.1.1. Consideraciones previas . . . 115
A.1.2. Instalaci´on de LLVM y Clang . . . 116
A.1.3. Instalaci´on de la herramienta . . . 117
A.2. Uso de la herramienta . . . 117
A.2.1. Anotaci´on del c´odigo . . . 118
A.2.2. Fichero de configuraci´on . . . 118
B. C´odigos del ejemplo completo de ejecuci´on 121 B.1. Ficheros de entrada . . . 121
B.1.1. C´odigos de kernel . . . 121
B.1.2. Fichero de configuraci´on . . . 124
B.2. Ficheros de salida . . . 126
B.2.1. Versi´on ´optima del kernel . . . 126
B.3. Otros datos de inter´es . . . 132 B.3.1. Salida por pantalla de la herramienta . . . 132 B.3.2. Versi´on intermedia sub´optima . . . 134
1.1. Representaci´on de un pipeline gr´afico gen´erico . . . 7
1.2. Pipeline gr´afico compatible con OpenGL 4 y Direct3D 11 . . . 8
1.3. Situaci´on de OpenCL en las t´ecnicas de paralelizaci´on actuales . . . 10
2.1. Miembros del OpenCL Working Group . . . 16
2.2. Eje temporal de la evoluci´on del proyecto OpenCL . . . 16
2.3. Diagrama del modelo de plataforma de OpenCL . . . 19
2.4. Espacio bidimensional de work-items de un dispositivo OpenCL . . . 20
2.5. Ejemplo de identificaci´on de work-items en un espacio bidimensional . . 21
2.6. Diagrama de ejecuci´on en orden y fuera de orden en una cola OpenCL . 22 2.7. Organizaci´on de las regiones de memoria en un dispositivo OpenCL . . 23
3.1. Arquitectura de alto nivel de Clang . . . 43
4.1. Proceso general de optimizaci´on iterativa implementado en la herramienta 66 4.2. Diagrama general de funcionamiento de OCLOptimizer . . . 68
4.3. Proceso de optimizaci´on iterativa ejecutado en el ejemplo . . . 77
4.4. Diagrama de clases de la herramienta . . . 81
4.5. Esquema de ejecuci´on de un pipeline de 5 etapas . . . 85
4.6. Ejemplo de desenrollamiento de bucles con factor 2 . . . 86
4.7. Ejemplo de desenrollamiento de bucles con factor 3 . . . 86
4.8. Diagrama de clases del patr´on M´etodo Factor´ıa original . . . 97 4.9. Detalle del diagrama de clases sobre el modelado de las optimizaciones . 98
5.1. Aceleraciones obtenidas en GPU para multiplicaciones matriz-vector . . 100 5.2. Aceleraci´on en GPU para una multiplicaci´on matriz-vector 4000 × 4000 102 5.3. Convoluci´on para una imagen de entrada 8 × 8 y m´ascara 3 × 3 . . . 104 5.4. Aceleraciones obtenidas para la convoluci´on de im´agenes en GPU . . . . 105 5.5. Aceleraci´on en GPU para una convoluci´on 8192 × 8192 . . . 107 5.6. Aceleraciones obtenidas para la convoluci´on de im´agenes en CPU . . . . 107 5.7. Aceleraciones obtenidas para una convoluci´on 8192 × 8192 en CPU . . . 109
2.1. Tipos de reserva de memoria en OpenCL . . . 24 2.2. Visibilidad del acceso a memoria en OpenCL . . . 24 2.3. Tipos de dispositivos recogidos en el est´andar OpenCL . . . 31 5.1. Tiempos de referencia de la multiplicaci´on matriz-vector en GPU . . . . 101 5.2. Tiempos de OCLOptimizer para multiplicaciones matriz-vector . . . 103 5.3. Tiempos de referencia de la convoluci´on de im´agenes en GPU . . . 106 5.4. Tiempos de referencia de la convoluci´on de im´agenes en CPU . . . 108 5.5. Tiempos de ejecuci´on de OCLOptimizer para convoluci´on de im´agenes . 108
2.1. Ejemplo de kernel OpenCL: suma de vectores . . . 28
2.2. Ejemplo de definici´on de workspaces OpenCL para una suma de vectores 29 2.3. Ejemplo de obtenci´on de plataformas en un host OpenCL . . . 30
2.4. Ejemplo de obtenci´on de dispositivos en un host OpenCL . . . 30
2.5. Ejemplo de creaci´on de un contexto OpenCL . . . 31
2.6. Ejemplo de creaci´on de una cola de comandos OpenCL . . . 32
2.7. Ejemplo de creaci´on de buffers OpenCL para una suma de vectores . . . 32
2.8. Ejemplo de transferencia de datos para una suma de vectores . . . 33
2.9. Ejemplo de carga y compilaci´on de un kernel OpenCL . . . 34
2.10. Paso de argumentos y ejecuci´on del kernel OpenCL vecsum . . . 34
2.11. Ejemplo de recogida de datos para una suma de vectores . . . 35
2.12. Ejemplo de liberaci´on de recursos OpenCL . . . 35
3.1. Ejemplo de instanciaci´on de un Preprocessor de Clang . . . 48
3.2. Ejemplo de env´ıo de ficheros de c´odigo a un Preprocessor de Clang . . 49
3.3. Ejemplo de separaci´on en tokens de un fichero de c´odigo C con Clang . 50 3.4. Ejemplo de diagn´ostico para un fichero de cabeceras no encontrado . . . 50
3.5. Ejemplo de definici´on de opciones de b´usqueda de cabeceras de Clang . 50 3.6. Definiciones necesarias para llamar a clang::ParseAST() . . . 52
3.7. Ejemplos de declaraciones de variables . . . 52
3.8. Ejemplo de sobreescritura de la funci´on HandleTopLevelDecl . . . 54
3.9. Constructor de la clase MyRewriter . . . 56
3.10. M´etodo parse() del ejemplo MyRewriter . . . 56 xvii
3.11. Implementaci´on del m´etodo Initialize() de MyConsumer . . . 57 3.12. Implementaci´on del m´etodo HandleTopLevelDecl() de MyConsumer . . 58 3.13. Implementaci´on del m´etodo MyConsumer::HandleTopLevelSingleDecl() 59 3.14. Implementaci´on del m´etodo MyConsumer::HandleTranslationUnit() . 60 3.15. M´etodo VisitStmt() de MyConsumer . . . 61 4.1. Expresi´on regular de formato de las directivas #pragma oclopts . . . . 67 4.2. Ejemplo de uso de las anotaciones de optimizaci´on . . . 68 4.3. Ejemplo de transformaci´on a funci´on de las anotaciones de optimizaci´on 69 4.4. Extracto del kernel de ejemplo anotado con directivas #pragma oclopts 75 4.5. Desenrollamiento ´optimo propuesto para el primer bucle . . . 79 4.6. Desenrollamiento ´optimo propuesto para el segundo bucle . . . 79 4.7. Implementaci´on del m´etodo UnrollAnnotation::ApplyAnnotation() . 88 4.8. Esquema del m´etodo UnrollingAnnotation::UnrollIncrement() . . . 89 4.9. Esquema del m´etodo UnrollingAnnotation::AdaptLoopCondition() . 90 4.10. Esquema de implementaci´on de UnrollAnnotation::UnrollBody() . . 91 4.11. Esquema del c´odigo de UnrollingAnnotation::UnrollStatement() . . 92 4.12. Implementaci´on de la clase AnnotationCreator . . . 97 5.1. Pseudoc´odigo del algoritmo de convoluci´on . . . 104 B.1. C´odigo original del kernel de ejemplo . . . 122 B.2. C´odigo del kernel de ejemplo anotado con directivas #pragma oclopts . 123 B.3. Configuraci´on de los par´ametros generales de la ejecuci´on del ejemplo . 124 B.4. Configuraci´on de los par´ametros de compilaci´on del ejemplo . . . 124 B.5. Configuraci´on de las dimensiones del espacio de trabajo del ejemplo . . 124 B.6. Configuraci´on del argumento de salida del ejemplo . . . 124 B.7. Configuraci´on de argumentos de la primera multiplicaci´on del ejemplo . 125 B.8. Configuraci´on de argumentos de la segunda multiplicaci´on del ejemplo . 125 B.9. Configuraci´on del argumento de tama˜no del problema del ejemplo . . . 126 B.10.Desenrollamientos propuestos en la versi´on seleccionada como ´optima . 127 B.11.C´odigo de host : definici´on del espacio de trabajo del ejemplo . . . 128 B.12.C´odigo de host : definici´on e inicializaci´on de buffers de usuario . . . 128
B.13.C´odigo de host : definici´on del contexto de trabajo y la cola de comandos 129
B.14.C´odigo de host : definici´on de los buffers de transferencia de OpenCL . . 130
B.15.C´odigo de host : construcci´on del programa . . . 130
B.16.C´odigo de host : paso de argumentos al kernel y ejecuci´on . . . 131
B.17.C´odigo de host : transferencia de datos a buffers de usuario . . . 131
B.18.C´odigo de host : liberaci´on de recursos de OpenCL . . . 132
B.19.Salida por pantalla: informaci´on de inicio . . . 132
B.20.Salida por pantalla: informaci´on del primer nivel de versiones . . . 133
B.21.Salida por pantalla: tiempos del primer nivel de versiones intermedias . 133 B.22.Salida por pantalla: informaci´on del segundo nivel de versiones . . . 134
B.23.Salida por pantalla: tiempos del segundo nivel de versiones . . . 134
Introducci´
on
La Ley de Moore, formulada por el co-fundador de Intel, Gordon Earl Moore, en 1965, predec´ıa que el n´umero de transistores que ser´ıa posible integrar en un mismo circuito se duplicar´ıa cada dos a˜nos [1]. Este planteamiento, que gobern´o y sigue go-bernando los procesos de fabricaci´on de semiconductores, sent´o asimismo las bases de la arquitectura de computadores, habiendo permitido elevar sistem´aticamente el nivel de integraci´on de transistores en una misma oblea de silicio, y con ´el la capacidad de c´omputo de los diferentes procesadores. Sin embargo, el mantener este crecimiento del nivel de integraci´on a la vez que se aumenta la frecuencia de reloj de funcionamiento de los circuitos ha terminado por revelar las limitaciones de los materiales semiconductores actuales (problemas de disipaci´on de calor, consumo excesivo de energ´ıa, etc.). Estas limitaciones han llevado a los fabricantes a reorientar su trabajo hacia nuevos dise˜nos, como la replicaci´on de n´ucleos con un nivel de integraci´on elevado, pero a la vez con una frecuencia de funcionamiento que, a´un siendo alta, no requiera de unos sistemas de disipaci´on de calor tan complejos. Este paradigma de dise˜no y construcci´on ha dado lugar a lo que actualmente se conoce como procesadores multicore o multin´ucleo.
Hoy en d´ıa, la mayor´ıa de los ordenadores cuentan con capacidades de procesamien-to paralelo gracias al procesador de varios n´ucleos que incorporan y, en una porci´on cada vez creciente, tambi´en a las tarjetas gr´aficas con capacidades GPGPU. Con la
tecnolog´ıa en esta situaci´on, es altamente interesante contemplar la posibilidad de au-nar los recursos convencionales de computaci´on de los procesadores multin´ucleo con las capacidades de prop´osito general de las GPUs, dando lugar a la llamada((computaci´on heterog´enea)), nuevo paradigma que trata de explotar de forma simult´anea y coordina-da las capacicoordina-dades de todos estos recursos de naturaleza tan diversa [2]. OpenCL, que es un est´andar industrial abierto [3][4][5] ideado para la programaci´on de plataformas tan diferentes como CPUs, GPUs, DSPs o FPGAs, goza de creciente relevancia en este contexto.
A continuaci´on se comentar´a la motivaci´on que ha llevado al desarrollo del presente proyecto (secci´on 1.1), as´ı como los objetivos propuestos (secci´on 1.2). Tras ello se realizar´a un breve recorrido por el estado del arte de las tecnolog´ıas m´as relevantes (secci´on 1.3). Finalmente, se especificar´a la metodolog´ıa y planificaci´on seguidas para el desarrollo del proyecto (secciones 1.4 y 1.5) y se detallar´a la estructura en cap´ıtulos del presente documento (secci´on 1.6).
1.1.
Motivaci´
on
Como se ha dicho en el apartado anterior, OpenCL es un est´andar que permite la programaci´on de dispositivos de muy variada ´ındole (CPUs, GPUs, DSPs, FPGAs...), ofreciendo para sus c´odigos portabilidad funcional entre los distintos tipos de plata-formas compatibles con el est´andar. Sin embargo, esta portabilidad funcional no se ve reflejada en una portabilidad del rendimiento: un c´odigo que haya sido expresamen-te optimizado para su ejecuci´on en una determinada plataforma muy probablemente obtendr´a un rendimiento muy por debajo del ´optimo en otra diferente. El trabajo de experimentaci´on consistente en la escritura y prueba de distintas versiones de un c´odigo aplicando m´ultiples optimizaciones con distintos par´ametros para generar las versiones ´
optimas de un c´odigo OpenCL para diferentes plataformas resulta especialmente pesa-do, sobre todo si ´este tiene que ser realizado a mano por el programador. La motivaci´on principal del presente proyecto es facilitar este trabajo al programador automatizando en la medida de lo posible este proceso de optimizaci´on. Para ello es interesante que
el programador pueda guiar el proceso especificando qu´e optimizaciones desea aplicar sobre ciertas partes del codigo. La herramienta deber´ıa evaluar todas las posibilidades sugeridas autom´aticamente, proporcionando como salida una versi´on modificada del c´odigo de entrada de acuerdo a las optimizaciones planteadas.
Los compiladores actuales son capaces de realizar optimizaciones autom´aticas de los c´odigos que procesan. Sin embargo, las transformaciones que realizan suelen ser de tipo source-to-binary, es decir, el compilador baraja diferentes optimizaciones y las aplica di-rectamente al generar el binario correspondiente, que suele estar programado en c´odigo m´aquina (como es el caso de GCC, por ejemplo) o utilizando alg´un tipo de representa-ci´on intermedia (por ejemplo, LLVM-IR). As´ı, con estos compiladores, el programador no es consciente en ning´un momento de qu´e cambios concretos se han realizado en su c´odigo ni puede modificar f´acilmente el resultado a posteriori. Sin embargo, ser´ıa interesante que al final del proceso el usuario obtenga un c´odigo optimizado, lo que le permitir´a conocer qu´e partes del c´odigo han sido transformadas y de qu´e manera, para poder aplicar a posteriori otras optimizaciones adicionales de forma manual sobre el mismo si as´ı lo desea. Por otra parte, una vez aplicada una transformaci´on sobre cierta parte del c´odigo, al generarse el binario correspondiente, resultar´ıa complejo mantener la informaci´on del resto de optimizaciones a probar y las porciones de c´odigo sobre las que ser´ıan aplicadas. Esto es especialmente cierto dado que muchas optimizaciones se refieren a transformaciones de c´odigo expresado sobre estructuras de control de alto nivel, cuyas sem´anticas se pierden o diluyen al transformarlas en c´odigo ejecutable. Por estos motivos, resulta especialmente importante que el usuario manipule en todo momento c´odigo de alto nivel.
As´ı mismo, los trabajos actuales en este campo suelen centrarse en la aplicaci´on de t´ecnicas de optimizaci´on autom´atica en problemas muy concretos, como operaciones SpMV1 [6] o FFT2 [7]. Lo que se pretende con la herramienta a implementar en este proyecto es proporcionar al prorgamador una forma r´apida y accesible de realizar el proceso de optimizaci´on iterativa de cualquier kernel OpenCL.
1
Sparse Matrix-Vector Multiplication, multiplicaci´on matriz dispersa-vector
2
1.2.
Objetivos
El objetivo principal que se pretende alcanzar con el presente proyecto es propor-cionar a la comunidad de desarrolladores de OpenCL un mecanismo de asistencia a la optimizaci´on de sus c´odigos, intentando reducir al m´aximo el tiempo invertido en el pro-ceso de aplicaci´on de las distintas transformaciones sobre los mismos. Dicho mecanismo se basa en dos puntos fundamentales: por una parte, la generaci´on del c´odigo de kernel ´
optimo a partir de la combinaci´on de las optimizaciones propuestas, y por otra, la gene-raci´on autom´atica de los c´odigos de host necesarios para ejecutar dichos kernels. Para la generaci´on autom´atica de las diferentes versiones de prueba, el usuario determinar´a me-diante una serie de directivas de compilaci´on definidas a tal efecto qu´e optimizaciones desea probar sobre qu´e fragmentos de c´odigo, de modo que la herramienta localizar´a la aparici´on de las mismas en el kernel a optimizar y realizar´a sobre dichos fragmentos las transformaciones de c´odigo necesarias para aplicar las optimizaciones solicitadas. En cuanto a la generaci´on autom´atica de c´odigos de host, el usuario solamente debe especificar mediante un fichero de configuraci´on, junto con otros par´ametros de fun-cionamiento (variables de entrada y salida, dimensiones del problema, tama˜no de los grupos de trabajo global y local...), en qu´e tipo de dispositivo desea probar su progra-ma, gener´andose as´ı para cada kernel un c´odigo de host adaptado exactamente a las condiciones establecidas. De esta forma se consigue eximir al programador de una tarea muy tediosa y repetitiva pero imprescindible para poder ejecutar cualquier kernel en dispositivos compatibles con OpenCL.
Para poder alcanzar este objetivo, en primer lugar resulta imprescindible realizar un estudio de las diferentes t´ecnicas de optimizaci´on que pueden ser de aplicaci´on en c´odigos de kernel de OpenCL, siendo de especial relevancia los siguientes dos tipos:
T´ecnicas de optimizaci´on secuencial, como pueden ser el desenrollamiento de bu-cles, con el que se busca explotar al m´aximo el paralelismo a nivel de instrucci´on intr´ınseco a cualquier procesador actual.
procesado y la jerarqu´ıa de memoria de las tarjetas gr´aficas, como el cambio de granularidad de las tareas o el aprovechamiento de la memoria local.
Por otra parte, para poder realizar las transformaciones asociadas a las diferentes optimizaciones, es necesario conocer la estructura del c´odigo de partida y poder mani-pularla en consecuencia. Para ello resulta tambi´en imprescindible estudiar las distintas opciones existentes para la realizaci´on de tareas de an´alisis y transformaci´on de c´odigo. As´ı mismo, tambi´en resulta de vital importancia que el proceso de optimizaci´on iterativa implementado en la herramienta sea lo m´as eficiente y flexible posible.
1.3.
Estado del arte
A continuaci´on se realiza una breve descripci´on de la evoluci´on de las tecnolog´ıas en las que se basa el presente proyecto, haciendo especial hincapi´e en los cambios que ha experimentado la arquitectura de computadores en lo que respecta tanto a los procesa-dores de prop´osito general como a las tarjetas gr´aficas y su conversi´on en plataformas de computaci´on, as´ı como en los diferentes mecanismos de an´alisis, transformaci´on y optimizaci´on de c´odigo disponibles.
1.3.1. Procesadores multin´ucleo
Tal y como se acaba de comentar en la introducci´on del presente cap´ıtulo, la ex-plotaci´on de las capacidades de los dispositivos semiconductores mediante el aumento sistem´atico tanto del nivel de integraci´on de transistores como de la frecuencia de reloj de funcionamiento termin´o por revelar sus limitaciones t´ecnicas, surgiendo problemas derivados principalmente del elevado consumo de energ´ıa que comenzaban a experimen-tar los circuitos, la imposibilidad de disipar de ellos la gran cantidad de calor generada y la problem´atica de sincronizaci´on de la informaci´on entre sus componentes debido a las altas frecuencias de trabajo.
de nuevos procesadores que integraban, en un mismo chip, varias r´eplicas de procesado-res completos conocidas como n´ucleos o cores, dando lugar a los llamados procesadores multin´ucleo o multicore. Ejemplos muy conocidos de esto son las gamas de procesadores fabricados por Intel Core Duo, Core 2 Duo, Xeon o, m´as recientemente, Core i3, Core i5 y Core i7.
Sin embargo, en los ´ultimos a˜nos los distintos fabricantes est´an comenzando a explo-rar nuevas v´ıas que les permitan aplicar esta idea de incluir varios n´ucleos en un mismo procesador sin necesidad de que dichos n´ucleos est´en dedicados a las mismas tareas. As´ı han surgido dise˜nos como las gamas de APUs3 de AMD Fusion y, m´as reciente-mente, Heterogeneous, o las diferentes versiones de los procesadores Tegra de NVIDIA, en los que en un mismo chip se integran diferentes tipos de circuiter´ıa dedicados a dife-rentes tareas: computaci´on de prop´osito general (CPU), procesamiento gr´afico (GPU), decodificaci´on dedicada de audio y v´ıdeo, etc. En lo que respecta a AMD, ha orienta-do m´as sus desarrollos en este campo hacia su instalaci´on en ordenadores completos, mientras que NVIDIA los ha enfocado m´as hacia el mercado de dispositivos m´oviles (tablets, smartphones, etc.).
Por otra parte, inspir´andose en todo el desarrollo que ha experimentado la compu-taci´on de prop´osito general en GPUs (conocida como((computaci´on GPGPU))), el cual ser´a comentado con cierto detalle en el apartado 1.3.2, Intel ha presentado reciente-mente la arquitectura MIC, acr´onimo de Many Integrated Core. Con esta arquitectura masivamente paralela, a´un en pleno desarrollo, es posible integrar en un dispositivo que se conecta a un ordenador mediante una conexi´on PCI-Express m´as de 50 n´ucleos de prop´osito general y que adem´as son compatibles con las herramientas tradicionales de programaci´on paralela para sistemas x86.
1.3.2. Surgimiento y popularizaci´on de la computaci´on GPGPU
En sus inicios, a finales de la d´ecada de 1960 y principios de la de 1970, la finalidad de las tarjetas gr´aficas no era otra que realizar el trabajo necesario para poder mostrar
3
textos y formas sencillas en la pantalla de un ordenador, siendo responsabilidad de la CPU toda la computaci´on previa necesaria.
Con el paso del tiempo, las necesidades de la industria del software en lo que a capacidades gr´aficas se refer´ıa fue creciendo espectacularmente, las cuales fueron siendo satisfechas poco a poco a˜nadiendo a las tarjetas circuitos espec´ıficos que acelerasen ciertas fases del procesamiento gr´afico, liberando as´ı a la CPU de dichos trabajos. Dichas fases constituyen lo que se conoce como un pipeline gr´afico, del cual puede verse un ejemplo sencillo en la figura 1.1. En las primeras implementaciones de estos pipelines, todas las fases realizaban un trabajo previamente establecido en el hardware, no siendo posible ning´un tipo de reprogramaci´on de las mismas. Estos pipelines se fueron sofisticando hasta alcanzar las tecnolog´ıas disponibles actualmente, que permiten la programaci´on de algunas de sus fases, como es el caso del ejemplo mostrado en la figura 1.2. El objetivo de la computaci´on GPGPU es aprovechar las capacidades de ese tipo de unidades de procesamiento para tareas de prop´osito general, en lugar de limitarlas a su uso tradicional.
Figura 1.1: Representaci´on de un pipeline gr´afico gen´erico
Uno de los ejemplos m´as conocidos de este tipo de plataformas de computaci´on es la familia de dispositivos Tesla fabricados por NVIDIA, en la cual es posible en-contrar desde tarjetas gr´aficas dom´esticas con capacidades GPGPU adicionales hasta
Figura 1.2: Representaci´on de un pipeline gr´afico compatible con OpenGL 4 y Direct3D 11
peque˜nos clusters ya ensamblados y exclusivamente dedicados a este tipo de compu-taci´on. Adem´as, de entre los 10 primeros supercomputadores de la edici´on de junio de 2012 del TOP500 [8], varios de ellos cuentan con dispositivos de este tipo como recursos de computaci´on.
1.3.3. Herramientas de computaci´on GPGPU y heterog´enea
Como se acaba de comentar en el apartado anterior, es posible reprogramar algu-nas de las unidades del pipeline de las tarjetas gr´aficas utilizando APIs espec´ıficas para gr´aficos 2D y 3D como OpenGL o Direct3D. Sin embargo, este tipo de APIs se basan en las abstracciones gr´aficas que se manejan en las diferentes fases del pipeline, como pue-den ser texturas, geometr´ıas o proyecciones. Estos conceptos resultan imprescindibles en el tratamiento de gr´aficos, pero pueden resultar complicados de aplicar en tareas de computaci´on de prop´osito general. Con el objetivo de ocultar parcialmente estos
deta-lles o, al menos, ayudar al programador a su gesti´on, diferentes fabricantes y grupos de trabajo han ido liberando al mercado diversas propuestas de lenguajes o herramientas para la programaci´on de dispositivos GPGPU. Entre ellas se pueden destacar algunas como Microsoft DirectCompute, BrookGPU, CUDA o OpenCL. Estas plataformas se sirven de diferentes lenguajes para la programaci´on de los dispositivos compatibles con las mismas, de entre los cuales las extensiones de C para OpenCL y CUDA son los que actualmente han alcanzado una posici´on de relevancia en el mercado.
Comparativamente, se puede atribuir a la extensi´on de CUDA para C cierta ven-taja sobre OpenCL en lo que a rendimiento respecta, en tanto en cuanto se trata de un lenguaje especialmente dise˜nado para el aprovechamiento de las capacidades de los dispositivos fabricados por NVIDIA compatibles con CUDA. Sin embargo, OpenCL no presenta estas restricciones de compatibilidad y permite desarrollar programas para su ejecuci´on ya no s´olo en gran cantidad de tarjetas gr´aficas (independientemente de su fabricante), sino tambi´en, como ya se ha comentado, en otros dispositivos como CPUs, DSPs o FPGAs. La figura 1.3 ilustra este concepto representando OpenCL como la pla-taforma de trabajo que cubre la intersecci´on entre procesadores multin´ucleo capaces de ejecutar una gran cantidad de procesos de forma simult´anea, y por otra, las capacidades existentes en las GPUs actuales para tratar de forma paralela grandes vol´umenes de datos. De esta forma, OpenCL es un est´andar de ((computaci´on heterog´enea)) [3][4][5] que permite ejecutar un mismo c´odigo sobre diferentes plataformas, obteniendo de este modo una aut´entica portabilidad funcional. Sin embargo, el rendimiento de dicho c´ odi-go puede no ser portable, de forma que var´ıe notablemente entre plataformas, y siendo por tanto tarea del programador intentar ajustar su programa a las caracter´ısticas de cada una de ellas.
En el cap´ıtulo 2 se proporciona m´as informaci´on acerca del est´andar OpenCL.
1.3.4. T´ecnicas de optimizaci´on autom´atica
Se procede a continuaci´on a comentar el estado actual de las diferentes t´ecnicas de optimizaci´on de c´odigo.
Figura 1.3: Situaci´on de OpenCL en las t´ecnicas de paralelizaci´on para CPUs y GPUs
Optimizaci´on directa
Es habitual que los compiladores actuales cuenten con multitud de opciones que permitan al desarrollador probar diferentes optimizaciones sobre el c´odigo original a compilar. Ejemplos muy conocidos y ampliamente utilizados de ello son los conjuntos de optimizaciones ofrecidos por compiladores como GNU C Compiler [9] o Intel C++ Compiler [10]. Con estas optimizaciones, el compilador intenta mejorar el rendimiento y/o el tama˜no del programa resultante a expensas de una mayor duraci´on del proceso de compilaci´on o de poder depurar el programa mediante depuradores como GDB.
El nivel de detalle que se puede alcanzar a la hora de aplicar las diferentes opti-mizaciones proporcionadas por alguno de estos compiladores puede llegar a ser muy elevado, dependiendo siempre del dominio que el programador tenga del lenguaje y de las posibilidades que ´este le ofrece. Por regla general, los compiladores ofrecen paque-tes de optimizaciones, como por ejemplo los asociados a las opciones -O1, -O2, -O3, etc. de GCC, que permiten a los desarrolladores mejorar sus c´odigos sin realizar un estudio exhaustivo de las posibles optimizaciones a aplicar. Junto a estos paquetes de optimizaciones, los compiladores ofrecen opciones para aplicar optimizaciones concretas [11] como desenrollamientos o vectorizaciones de bucles, inlining de funciones, infor-maci´on acerca del no solapamiento de regiones de memoria accedidas por determinados punteros...
Optimizaci´on iterativa
Los compiladores hacen un uso extensivo de t´ecnicas directas de optimizaci´on para conseguir mejorar el rendimiento de los programas. Sin embargo, la aplicaci´on de dichas optimizaciones suele estar basada en an´alisis est´aticos del c´odigo a partir de modelos simplificados de las m´aquinas o en heur´ısticas simples que a menudo se muestran insu-ficientes. El problema de este enfoque reside en la incompletitud intr´ınseca del an´alisis est´atico de c´odigo [12], lo que supone que, pese a afinar bastante el rendimiento, los compiladores basados en estas t´ecnicas de optimizaci´on no puedan determinar de for-ma directa cu´al es la mejor optimizaci´on aplicable a un c´odigo para obtener el mejor rendimiento posible en una plataforma concreta.
La soluci´on que se ha venido planteando en los ´ultimos a˜nos como alternativa para suplir estas carencias ha sido la llamada ((compilaci´on iterativa)). Esta t´ecnica consiste en la aplicaci´on expl´ıcita de sucesivas transformaciones de c´odigo sobre un programa dado, las cuales son evaluadas a fin de seleccionar la mejor o las mejores antes de pasar a experimentar con otras transformaciones sobre ellas. El proceso de selecci´on se realiza mediante mediante la ejecuci´on real de las diferentes versiones generadas, o, de forma m´as sofisticada, analizando y prediciendo su comportamiento mediante diferentes modelos de rendimiento o heur´ısticas. La principal desventaja de esta t´ecnica reside en un incremento notable del tiempo de compilaci´on, debido al alto coste de evaluar todas las versiones generadas para seleccionar la ´optima.
Los grandes costes de la compilaci´on iterativa la han relegado tradicionalmente al campo de la computaci´on embebida, donde el tama˜no de los programas suele ser re-ducido y el proceso de compilaci´on se limita a las fases de desarrollo del producto, de modo que una vez establecida la versi´on ´optima para un programa y un dispositivo dados, no deber´ıa de ser necesaria la repetici´on del proceso. Evidentemente, el uso de esta t´ecnica en grandes c´odigos basados en paradigmas tradicionales de programaci´on se seguir´a viendo afectado por este importante inconveniente. Sin embargo, la aplica-ci´on exitosa durante a˜nos de esta t´ecnica en c´odigos embebidos anima a probarla con c´odigos de prop´osito general, pero que cuentan con la ventaja de ser de reducido
ta-ma˜no como, por ejemplo, los kernels que se utilizan en lenguajes orientados a GPGPU como son CUDA u OpenCL.
Otro motivo que pone en valor el uso de t´ecnicas de optimizaci´on iterativa en entor-nos GPGPU es el continuo cambio en el dise˜no de este tipo de arquitecturas, lo que hace especialmente complicado mantener y publicar, con el tiempo suficiente, compiladores que puedan explotar al m´aximo las nuevas caracter´ısticas que dichos dispositivos van incorporando.
1.3.5. Herramientas de an´alisis y transformaci´on de c´odigo
Debido a la elevada complejidad que ello comporta, se ha descartado el desarrollo desde cero de un mecanismo de an´alisis y transformaci´on de c´odigo. As´ı, se procedi´o a la b´usqueda de herramientas alternativas para la realizaci´on de este tipo de tareas, comenzando por aquellas de uso m´as habitual, como, por ejemplo, GNU C Compiler.
Si bien el uso m´as conocido de GCC es como herramienta de compilaci´on de c´odigo, existe tambi´en la posibilidad de utilizar los componentes que lo forman para construir herramientas que realicen diversas transformaciones de c´odigo. Sin embargo, GCC ha sido construido como un compilador monol´ıtico y est´atico, lo que complica bastante su integraci´on en otras herramientas. As´ı mismo, tanto la evoluci´on hist´orica como la pol´ıtica actual que gobierna su dise˜no hacen complicado desacoplar el frontend del resto del compilador [13].
Una alternativa que actualmente est´a tomando una posici´on importante en este cam-po es la infraestructura de compilaci´on LLVM [14]. De todas formas, el uso directo de este compilador no ser´ıa lo m´as adecuado para este proyecto, ya que funciona siguiendo un paradigma source-to-binary. Sin embargo, Clang, frontend para C, C++ y Objective-C del compilador LLVM, permite realizar operaciones de tipo source-to-source con el c´odigo. Si bien su finalidad principal es la misma que la de GCC, es decir, su uso co-mo herramienta de compilaci´on, en este caso s´ı est´a documentado su uso alternativo como mecanismo de an´alisis y transformaci´on de c´odigo. Por otra parte, como ya se
ha comentado, cuenta con un dise˜no m´as apropiado para su integraci´on en otras herra-mientas a trav´es de una API. Estos dos motivos fueron resultaron decisivos para el uso de Clang en el desarrollo de la herramienta.
1.4.
Metodolog´ıa de desarrollo
La metodolog´ıa de desarrollo empleada se basa en un proceso incremental basado en prototipos. Se ha elegido este enfoque de trabajo gracias, principalmente, a la flexibili-dad que ´este permite en caso de ser necesario corregir decisiones err´oneas o modificar funcionalidades procedentes de incrementos anteriores. De hecho, desde el principio se consider´o que la probabilidad de tener que hacer frente a este tipo de problem´aticas era considerable debido a la elevada complejidad asociada al uso a bajo nivel de las capacidades de an´alisis l´exico y sint´actico de c´odigo C proporcionadas por Clang.
En una fase inicial se realiz´o una toma de contacto con las herramientas b´asicas de trabajo (Clang y OpenCL), implementando un prototipo capaz de procesar un kernel OpenCL y de utilizar las funciones correspondientes de Clang para conocer su estructu-ra. En fases posteriores, y tomando como base dicho prototipo, se han ido incorporando diferentes funcionalidades hasta, finalmente, obtener una implementaci´on completa-mente funcional de la herramienta, capaz de aplicar a un kernel las optimizaciones especificadas por el usuario y evaluar las versiones generadas a partir de las mismas.
1.5.
Planificaci´
on del trabajo
Las caracter´ısticas de un proyecto de investigaci´on y desarrollo como ´este hacen es-pecialmente complicado el establecimiento de una planificaci´on tradicional del trabajo, realizada a priori. En concreto, la falta de experiencia con gran parte de las tecnolog´ıas a utilizar, as´ı como la necesidad de estudiar la viabilidad de la realizaci´on de los ob-jetivos marcados utilizando dichas tecnolog´ıas, imposibilit´o el establecimiento a priori de una planificaci´on temporal fiable, por lo que se decidi´o prescindir de ella.
1.6.
Estructura del documento
Junto con este primer cap´ıtulo introductorio, la presente memoria se compone de los siguientes cap´ıtulos, cuyo contenido se resume a continuaci´on:
Cap´ıtulo 2 Este cap´ıtulo se dedica ´ıntegramente a la presentaci´on del est´andar de computaci´on heterog´enea OpenCL, comentando sus principales caracter´ısticas y diseccionando punto por punto su arquitectura.
Cap´ıtulo 3 En este cap´ıtulo se presenta la infraestructura de compilaci´on LLVM, as´ı como la estructura y capacidades de an´alisis y transformaci´on de c´odigo del frontend para C/C++ Clang.
Cap´ıtulo 4 En este cap´ıtulo se describe el resultado final del proceso de desarrollo de esta herramienta de optimizaci´on iterativa, la cual constituye el principal objetivo del presente proyecto.
Cap´ıtulo 5 En este cap´ıtulo se detallar´an los resultados experimentales obtenidos en las pruebas de optimizaci´on realizadas con la herramienta para diferentes proble-mas implementados en OpenCL, tanto sint´eticos como reales.
Cap´ıtulo 6 Este cap´ıtulo, con el que se cierra el presente documento, establece las conclusiones extra´ıdas del trabajo realizado y planteando posibles l´ıneas futuras de investigaci´on y desarrollo.
As´ı mismo, se incluyen los siguientes ap´endices:
Ap´endice A Este ap´endice recoge un breve manual de usuario de la herramienta, explicando el formato de las anotaciones de c´odigo y el proceso de instalaci´on. Ap´endice B En este ap´endice se incluyen los c´odigos y ficheros de configuraci´on m´as
relevantes que intervienen en un ejemplo completo de ejecuci´on de la herramienta. Ap´endice C En este ap´endice se lista el contenido del soporte de almacenamiento
´
El est´
andar OpenCL
El presente cap´ıtulo pretende servir de introducci´on al est´andar de computaci´on heterog´enea OpenCL ya citado y brevemente comentado en apartados anteriores. Se comenzar´a en la secci´on 2.1 con una introducci´on de dicho proyecto, para despu´es entrar en profundidad en la secci´on 2.2 en detalles como la arquitectura de la plataforma y los modelos que la componen. Finalmente se describir´a paso a paso en la secci´on 2.3 el procedimiento a seguir para la implementaci´on de un sencillo kernel OpenCL y su posterior ejecuci´on en un dispositivo compatible con el est´andar.
2.1.
Introducci´
on
OpenCL (acr´onimo de Open Computing Language) es un est´andar industrial abierto [4] ideado para la programaci´on de plataformas tan heterog´eneas como CPUs, GPUs e incluso otros procesadores como DSPs1 o FPGAs2. Bajo el est´andar OpenCL se define un framework de programaci´on paralela compuesto por un lenguaje de programaci´on basado en C99, una API, diversas librer´ıas y una plataforma de ejecuci´on. De esta ma-nera, OpenCL proporciona una abstracci´on de bajo nivel que permite acceder, a trav´es del framework, a una gran cantidad de detalles espec´ıficos del hardware subyacente.
1
Digital Signal Processor, procesador digital de prop´osito espec´ıfico para se˜nales.
2
Field Programmable Gate Array, dispositivo semiconductor de l´ogica programable.
2.1.1. Comunidad de desarrollo
Como ya se ha comentado, OpenCL es un est´andar abierto mantenido por el consor-cio tecnol´ogico sin ´animo de lucro Khronos Group. Sin embargo, el responsable inicial de su desarrollo y evoluci´on fue Apple, a quien despu´es acompa˜naron otras empresas de la talla de AMD, IBM, Intel o NVIDIA. En la figura 2.1 puede apreciarse la diversidad de firmas dentro de los m´ultiples sectores de la investigaci´on y la industria tecnol´ ogi-ca que participan de uno u otro modo en el OpenCL Working Group: fabriogi-cantes de procesadores y FPGAs, desarrolladores de middleware y de aplicaciones, instituciones universitarias, laboratorios y centros de investigaci´on...
Figura 2.1: Miembros del OpenCL Working Group
2.1.2. Evoluci´on y situaci´on actual del proyecto
A continuaci´on se comenta el proceso de desarrollo del proyecto OpenCL desde sus or´ıgenes con la formaci´on de un grupo de trabajo al respecto en el seno del Khronos Group hasta la publicaci´on en noviembre de 2011 de la especificaci´on de la que ser´a la pr´oxima versi´on, OpenCL 1.2. La figura 2.2 resume en un eje temporal los hitos princi-pales de la evoluci´on del proyecto OpenCL, la cual se comenta, a continuaci´on, versi´on por versi´on.
OpenCL 1.0
En junio de 2008 fue constituido el correspondiente grupo de trabajo, contando el mismo con la participaci´on de compa˜n´ıas de los sectores de CPUs, GPUs, procesadores embebidos y software. Este primer grupo trabaj´o durante 5 meses hasta concretar los detalles t´ecnicos de la especificaci´on de OpenCL 1.0 [3] en noviembre de 2008. Un mes m´as tarde, una vez revisado, se aprob´o su lanzamiento al p´ublico, siendo incorporado por Apple a su sistema operativo Mac OS X Snow Leopard.
OpenCL 1.1
La versi´on 1.1 de OpenCL [4] fue ratificada por el Khronos Group en junio de 2010, y con ella se a˜nadieron mejoras de cara al rendimiento y la flexibilidad de programaci´on de los dispositivos, como por ejemplo:
Nuevos tipos de datos como vectores de 3 componentes o formatos de im´agenes. Operaciones sobre regiones completas de un buffer, tales como lectura, escritura y copia de regiones rectangulares en 1D, 2D y 3D.
Uso mejorado de los eventos para gestionar y controlar la ejecuci´on de comandos.
OpenCL 1.2
En noviembre de 2011, el Khronos Group anunci´o la especificaci´on de la versi´on 1.2 de OpenCL [5], que viene a complementar todav´ıa m´as las funcionalidades a˜nadidas en versiones anteriores, sobre todo en lo que a rendimiento y programaci´on paralela se refiere.
Algunas de estas nuevas caracter´ısticas son las siguientes:
Particionado de dispositivos, de modo que sea posible dividir un dispositivo en subdispositivos a los que asignar tareas como si de unidades individuales de computaci´on se tratase. Esto resulta especialmente ´util para reservar zonas
con-cretas de los dispositivos y as´ı reducir la latencia de operaciones que sean cr´ıticas en tiempo.
Separaci´on de la compilaci´on y enlazado de objetos, de manera que sea posible compilar OpenCL en librer´ıas externas para su inclusi´on en otros programas. Built-in kernels que implementan funcionalidades espec´ıficas o no programables propias de los diferentes dispositivos subyacentes, como por ejemplo, codificaci´on y decodificaci´on de v´ıdeo o procesado digital de se˜nales.
2.1.3. Implementaciones disponibles
Aunque se trate de un est´andar mantenido por el Khronos Group, esta organizaci´on simplemente se encarga de gobernar el proceso de desarrollo y evoluci´on de las diferen-tes especificaciones de OpenCL, dejando en manos de la industria la implementaci´on del est´andar para su uso en los diferentes dispositivos compatibles. Han sido precisa-mente algunos de los miembros del OpenCL Working Group quienes han desarrollado sus propias implementaciones de OpenCL, orient´andolas y optimiz´andolas en cada caso de acuerdo con sus propios objetivos empresariales. Ejemplo de ello son AMD (espe-cialmente orientada hacia sus APUs Heterogeneous y Fusion), NVIDIA (como capa de abstracci´on sobre la arquitectura CUDA), Intel (para las ´ultimas generaciones de sus procesadores) o IBM (para sus procesadores Power, habituales en los equipos de supercomputaci´on que comercializan).
2.2.
La arquitectura OpenCL
Las ideas que subyacen de la definici´on del est´andar OpenCL se organizan de acuerdo a una arquitectura basada en una jerarqu´ıa de modelos compuesta por los modelos de plataforma (Platform Model ), ejecuci´on (Execution Model ), memoria (Memory Model ) y programaci´on (Programming Model ).
2.2.1. Modelo de plataforma
OpenCL plantea un modelo que pueda servir de abstracci´on sobre la arquitectura concreta de las diferentes plataformas de computaci´on compatibles con el est´andar. Seg´un este modelo, que pretende ilustrar la figura 2.3, toda plataforma compatible se compone de un host conectado a uno o m´as dispositivos OpenCL. Cada dispositivo OpenCL se divide en una o m´as unidades de computaci´on (CUs, Computing Units), las cuales a su vez est´an divididas en uno o m´as elementos de procesado (PEs, Processing Elements), siendo en este ´ultimo nivel del dispositivo donde se realiza el trabajo de computaci´on.
Figura 2.3: Diagrama del modelo de plataforma de OpenCL
Una aplicaci´on OpenCL se ejecuta en un host teniendo en cuenta las caracter´ısticas espec´ıficas del mismo. Desde dicho host, la aplicaci´on env´ıa comandos a los elementos de procesado de los dispositivos, de modo que aquellos que se encuentran agrupados en una misma unidad de computaci´on ejecutan un mismo flujo de instrucciones, bien como unidades SIMD, bien como unidades SPMD, seg´un las necesidades de la aplicaci´on.
2.2.2. Modelo de ejecuci´on
La ejecuci´on de un programa OpenCL tiene lugar en dos partes: kernels que se ejecutan en uno o m´as dispositivos y un programa host que se ejecuta sobre el mismo y define el contexto de trabajo de los kernels y gestiona su ejecuci´on.
Organizaci´on del trabajo
El n´ucleo del modelo de ejecuci´on de OpenCL viene definido por c´omo se ejecutan los kernels. Cuando el host env´ıa un kernel para su ejecuci´on, se define un espacio de trabajo, de modo que se ejecuta una instancia de dicho kernel para cada punto del espacio previamente definido. Esta instancia recibe el nombre de work-item (elemento de trabajo) y se identifica de forma global por el punto que lo representa en el espacio de trabajo, el cual se define como una matriz de varias dimensiones. Cada work-item ejecuta el mismo c´odigo, aunque el camino concreto de cada ejecuci´on y los datos que en ´esta se manipulan pueden variar entre los diferentes work-items. A su vez, los work-items pueden organizarse en work-groups, los cuales tambi´en se identifican por su posici´on en el espacio de trabajo e, internamente, se organizan como matrices de varias dimensiones formando un espacio local. Las instancias asociadas a los work-items de un work-group dado se ejecutan concurrentemente en los elementos de procesado de una misma unidad de computaci´on. La figura 2.4 muestra c´omo se organiza un espacio de trabajo bidimiensional en OpenCL, pudiendo verse en la figura 2.5 un ejemplo de sencillo de identificaci´on de work-items en un espacio bidimensional de tama˜no (8, 12) subdividido en grupos de tama˜no (4, 4).
Figura 2.5: Ejemplo de identificaci´on de work-items en un espacio bidimensional
Contexto de ejecuci´on
Una de las principales finalidades del host es definir y gestionar el contexto en el que se ejecutar´an los diferentes kernels. Este contexto es creado y manipulado por el host mediante un ((c´odigo de host)) construido utilizando las funciones del API de OpenCL. El contexto gestiona, entre otras cosas, los dispositivos OpenCL presentes en la plataforma, los kernels que se ejecutar´an sobre los dispositivos, los objetos de programa (ejecutables de los kernels) y los objetos de memoria (buffers de transferencia de datos entre host y kernel ).
Colas de comandos
El host crea una estructura de datos llamada ((cola de comandos)) para coordinar la ejecuci´on de los kernels en los diferentes dispositivos, de modo que el host env´ıa comandos a dicha cola para que ´estos sean planificados sobre los dispositivos incluidos en el contexto.
Dichos comandos pueden dividirse de acuerdo a la siguiente clasificaci´on:
Comandos de ejecuci´on de kernel : Ejecutan un kernel sobre los elementos de procesado de un dispositivo.
Comandos de memoria: Transfieren datos a, desde, o entre objetos de memoria, o bien reservan y liberan el espacio asociado a los mismos en el host.
Comandos de sincronizaci´on: Determinan el orden de ejecuci´on del resto de comandos.
Como ya se ha comentado, la cola de comandos planifica la ejecuci´on de los mismos en un dispositivo. Despu´es, ´estos se ejecutan de manera as´ıncrona entre dicho dispositivo y el host de acuerdo a alguno de los siguientes modos cuyo funcionamiento ilustra la figura 2.6 y que a continuaci´on se relacionan:
Figura 2.6: Diagrama de ejecuci´on en orden y fuera de orden en una cola OpenCL
Ejecuci´on en orden: Los comandos son lanzados y completados en el orden en que han sido encolados, de modo que un comando que aparezca primero en la cola siempre se completar´a antes de que comience el siguiente. Como puede deducirse, este modo supone la serializaci´on de todos los comandos encolados.
Ejecuci´on fuera de orden: Los comandos son lanzados en orden, pero no se espera a la finalizaci´on de un comando anterior para la ejecuci´on de otro posterior. En este caso, es responsabilidad expresa del programador utilizar los comandos de sincronizaci´on pertinentes para que la ejecuci´on del c´odigo de host sea la esperada. Los comandos de memoria y de ejecuci´on de kernel encolados generan los llamados ((objetos de evento)), los cuales pueden ser utilizados para controlar la ejecuci´on entre comandos y para coordinar la ejecuci´on entre el host y los dispositivos. As´ı mismo, es posible asociar m´ultiples colas bajo un mismo contexto. Utilizando este modo de trabajo, las colas se ejecutan de forma concurrente pero independientemente, sin que OpenCL garantice ning´un mecanismo expl´ıcito de sincronizaci´on entre las mismas.
Categor´ıas de kernels
El modelo de ejecuci´on de OpenCL soporta dos categor´ıas diferentes de kernels. Por una parte, kernels de OpenCL, escritos utilizando la extensi´on de C para OpenCL y compilados con el compilador de OpenCL. Todas las implementaciones del est´andar han de soportar este tipo de kernels, si bien ´estas pueden proporcionar otros mecanismos para su creaci´on. Por otra se encuentran los llamados kernels ((nativos)), que son accedi-dos mediante un puntero a funci´on y son encolados para su ejecuci´on en un dispositivo con el resto de kernels OpenCL, con los que adem´as comparten los objetos de memoria. Ejemplos de ello ser´ıan funciones definidas en un c´odigo de aplicaci´on o procedentes de una librer´ıa. N´otese que esta funcionalidad es meramente opcional y que la sem´antica de los kernels nativos es dependiente de cada implementaci´on, de modo que lo ´unico que se incluye en el API de OpenCL son funciones para consultar las capacidades de un dispositivo y determinar si alguna concreta est´a soportada.
2.2.3. Modelo de memoria
Los work-items encargados de la ejecuci´on de un kernel pueden acceder a cuatro regiones de memoria diferentes organizadas de acuerdo al esquema de la figura 2.7 y que a continuaci´on se citan ordenadas de menor a mayor velocidad de acceso:
Memoria global: Esta regi´on de memoria permite accesos de lectura-escritura a todos los work-items de todos los work-groups, pudiendo leer o escribir datos en cualquier elemento de un objeto de memoria. As´ı mismo, dependiendo de las capacidades del dispositivo, las lecturas y escrituras en memoria global podr´ıan almacenarse en cach´e.
Memoria constante: Es una regi´on de la memoria global que permanece cons-tante durante la ejecuci´on de un kernel, siendo el host el encargado de reservar e inicializar los objetos de memoria almacenados en esta regi´on.
Memoria local: Es una regi´on local respecto de un work-group, la cual puede ser utilizada para reservar variables compartidas por todos los work-items del grupo. En algunos dispositivos se implementa como una regi´on exclusiva, mientras que en otros se utilizan secciones de la memoria global dedicadas.
Memoria privada: Una regi´on de memoria privada para cada work-item, de modo que las variables definidas en la misma no son visibles por los dem´as items.
Las tablas 2.1 y 2.2 describen en qu´e casos en que tanto el kernel como el host pueden reservar memoria de una regi´on concreta, en qu´e forma pueden hacerlo (est´atica o en tiempo de compilaci´on, di´animca o en tiempo de ejecuci´on) y qu´e tipo de acceso les est´a permitido (s´olo lectura, lectura-escritura o acceso prohibido).
Global Constante Local Privada
Host Din´amica Din´amica Din´amica Sin reserva
Kernel Sin reserva Est´atica Est´atica Est´atica
Tabla 2.1: Tipos de reserva de memoria en OpenCL
Global Constante Local Privada
Host R-W R-W Sin acceso Sin acceso
Kernel R-W S´olo lectura R-W R-W
El c´odigo de host se sirve del API de OpenCL para crear objetos de memoria en la regi´on global, as´ı como para encolar aquellos comandos de memoria que operen sobre dichos objetos. Los modelos de memoria del host y del dispositivo OpenCL son, en su mayor´ıa, independientes entre s´ı. Esto resulta necesario dado que el host est´a definido fuera de OpenCL. Sin embargo, ambas partes tienen que poder interactuar, lo cual sucede de una de las dos siguientes formas: copiando datos expl´ıcitamente o asignando y desasignando regiones de un objeto de memoria. Para copiar datos de forma expl´ıci-ta, el host encola comandos para transferir los datos entre el objeto de memoria y la suya propia, pudiendo ser estos comandos tanto bloqueantes como no bloqueantes. La llamada a la funci´on de OpenCL encargada de realizar una transferencia bloqueante de memoria finaliza una vez los recursos de memoria asociados al host pueden ser reuti-lizados de forma segura. Para una transferencia no bloqueante, la funci´on de OpenCL finaliza tan pronto como el comando es encolado, sin importar si es seguro o no reutilizar la memoria del host.
El m´etodo de asignaci´on/desasignaci´on permite al host asociar una regi´on del objeto de memoria a su espacio de direcciones. De la misma forma que sucede con el m´etodo de copia expl´ıcita, en este caso el comando de memoria correspondiente tambi´en puede ser bloqueante o no bloqueante. Una vez se ha realizado la asociaci´on entre el objeto de memoria y el host, este ´ultimo puede leer o escribir en esa regi´on. El propio host se encarga de desasignar la regi´on una vez se hayan completado los accesos (lecturas y/o escrituras) a la misma.
Consistencia de memoria
OpenCL se basa en un modelo de consistencia relajada de memoria, de modo que no se garantiza que el estado de la memoria visible para un work-item sea consistente en todo momento para el resto de work-items. Dentro de cada work-item s´ı se cumple una consistencia de carga/almacenamiento (load/store consistency ). La memoria local es consistente para todos los work-items dentro del mismo grupo cuando la ejecuci´on alcanza una barrera que afecte a dicho grupo, lo cual tambi´en se cumple para la memoria
global. Sin embargo, para este tipo de memoria no existen garant´ıas de consistencia entre grupos diferentes. Finalmente, la ´unica forma de garantizar la consistencia de memoria entre objetos compartidos por distintos comandos encolados es introducir un punto de sincronizaci´on en el momento que se desee comprobar el estado de la memoria.
2.2.4. Modelo de programaci´on
Como ya se introdujo en apartados anteriores, el modelo de ejecuci´on de OpenCL soporta los modelos de programaci´on paralela basados tanto en datos como en tareas, as´ı como versiones h´ıbridas de los mismos. Aunque sea compatible con ambos, el dise˜no de OpenCL ha sido realizado siguiendo el modelo de paralelismo de datos.
Paralelismo de datos
Este modelo parte de la definici´on de operaci´on como una secuencia de instrucciones aplicada simult´aneamente a m´ultiples elementos de un objeto de memoria. El espacio de ´ındices asociado al modelo de ejecuci´on de OpenCL define los work-items y la asocia-ci´on del conjunto de datos a los mismos. Si se aplicase el paralelismo de datos de forma estricta, existir´ıa siempre una relaci´on uno a uno entre cada work-item y cada elemento del objeto de memoria sobre el que se ejecutar´a, en paralelo, el kernel. OpenCL, en cambio, implementa una versi´on relajada del modelo que no siempre requiere de esta asociaci´on. OpenCL proporciona un modelo jer´arquico de programaci´on paralela de datos, habiendo dos formas para especificar esta subdivisi´on jer´arquica. En el modelo expl´ıcito el programador define el n´umero total de work-items que trabajar´an en para-lelo, as´ı como la agrupaci´on de ´estos para formar work-groups. En el modelo impl´ıcito, el programador solamente especifica el n´umero total de work-items, dejando que sea la implementaci´on de OpenCL quien gestione la divisi´on en work-groups.
Paralelismo de tareas
El paralelismo basado en tareas implementado en OpenCL se basa en un modelo en el que se ejecuta una sola instancia de un kernel independientemente del espacio de ´ındices. L´ogicamente, equivale a ejecutar un kernel en una unidad de computaci´on que cuente con un work-group formado por un ´unico work-item. Bajo este modelo, los usuarios pueden extraer paralelismo utilizando los tipos de datos vectoriales implementados por el dispositivo, encolando varias tareas o encolando kernels nativos desarrollados usando un modelo de programaci´on ortogonal a OpenCL.
Sincronizaci´on
OpenCL ofrece dos posibilidades diferentes para la inserci´on de puntos de sincro-nizaci´on, bien entre los work-items de un mismo work-group, bien entre los comandos encolados bajo un mismo contexto. La sincronizaci´on entre work-items dentro de un mismo work-group se consigue mediante una barrera de work-group que forzar´a que to-dos los items la alcancen antes de que sea posible continuar con la ejecuci´on m´as all´a de la misma. N´otese que no es posible la definici´on de barreras parciales de este tipo, de modo que la ejecuci´on solamente proseguir´a si est´a definida y la alcanzan todos los items, o bien si directamente no se encuentra definida. As´ı mismo, no existe mecanismo alguno de sincronizaci´on entre grupos.
En lo que respecta a la sincronizaci´on entre comandos de una misma cola, existen dos posibilidades:
Barrera de cola de comandos: Este tipo de barrera asegura que todos los co-mandos previamente encolados han finalizado su ejecuci´on y que todas las actua-lizaciones de los objetos de memoria implicados ser´an visibles para los comandos subsiguientes antes de que comiencen a ejecutarse. Esta barrera solamente puede usarse para sincronizar comandos dentro de una misma cola.
Espera por un evento: Todas las funciones de la API que resultan en la inser-ci´on de comandos en la cola devuelven un evento que identifica al comando y a
los objetos de memoria que modifica. Si un comando subsiguiente espera por la aparici´on de dicho evento, queda garantizada la visibilidad de las modificaciones previas sobre los objetos de memoria antes de su ejecuci´on.
2.3.
Programaci´
on de aplicaciones con OpenCL
Tal y como se ha ido comentando a lo largo del presente cap´ıtulo, para poder progra-mar cualquier tipo de aplicaci´on utilizando OpenCL es necesario establecer el comporta-miento de las dos principales partes de cualquier plataforma de computaci´on compatible con el est´andar: el host y los dispositivos. Para la programaci´on de los dispositivos, en-cargados de realizar las tareas de computaci´on, es necesario desarrollar lo que se conoce como((c´odigo de kernel)). Este c´odigo es el que implementar´a las funciones propiamente dichas, y se escribe utilizando la extensi´on de C para OpenCL. Por su parte, el host debe ejecutar lo que se conoce como((c´odigo de host)), cuya funci´on principal es definir y gestionar del contexto de ejecuci´on utilizando las funciones del API que OpenCL proporciona a tal efecto. A continuaci´on se explica, para un ejemplo sencillo (una suma de vectores), los pasos a seguir para desarrollar ambos tipos de c´odigo.
El listado 2.1 contiene la implementaci´on de un kernel que realiza la suma de dos vectores con valores de tipo float y longitud N. Aunque a simple vista se asemeja bastante a una funci´on convencional escrita en C, existen ciertas caracter´ısticas que ser´an aclaradas posteriormente.
1 _ _ k e r n e l void vecsum ( _ _ g l o b a l const float *a , _ _ g l o b a l const float *b , _ _ g l o b a l f l o a t * c , u n s i g n e d int N )
2 {
3 u n s i g n e d int xid = g e t _ g l o b a l _ i d (0) ; 4 if ( xid < N )
5 c [ xid ] = a [ xid ]+ b [ xid ]; 6 }
De todas formas, para poder explicar de forma clara los detalles del c´odigo mostrado del listado, es necesario comentar antes los pasos que es necesario seguir para imple-mentar el c´odigo de host que cree y gestione el contexto adecuado para la ejecuci´on del kernel.
2.3.1. Desarrollo de un c´odigo de host
Para poder definir el contexto adecuado para ejecutar el kernel del listado, es nece-sario desarrollar un c´odigo de host que realice las siguientes operaciones:
Definici´on de los espacios de trabajo
Tal y como se ha comentado en el apartado 2.2.2, OpenCL distribuye la ejecuci´on de los kernels de acuerdo a un espacio de trabajo que modela la organizaci´on de los diferentes n´ucleos de los dispositivos de la plataforma. En este caso, al tratarse la suma de vectores de una operaci´on extremadamente sencilla, la definici´on de los espacios de trabajo (listado 2.2) no reviste complejidad alguna: se necesitar´an tantos work-items como elementos tengan los arrays que representan a los vectores A y B (l´ınea 1), y cada uno de dichos items realizar´an su propio c´alculo de Ci= Ai+ Bi correspondiente
a una componente del vector suma C (l´ınea 2).
1 size_t g l o b a l _ w o r k _ s i z e = N ; 2 size_t l o c a l _ w o r k _ s i z e =1;
Listado 2.2: Ejemplo de definici´on de workspaces OpenCL para una suma de vectores
Obtenci´on de las plataformas OpenCL a usar
La primera operaci´on a realizar en un c´odigo de host OpenCL es comprobar la existencia de plataformas compatibles con el est´andar para, posteriormente, seleccionar aquella o aquellas que se desean utilizar para ejecutar sobre ellas el kernel. La funci´on que permite obtener esta informaci´on es clGetPlatformIDs, la cual es utilizada en
primer lugar para saber cu´antas plataformas hay disponibles en el sistema (l´ınea 3 del listado 2.3). Una vez conocido el n´umero de platformas (almacenado en la variable nPlatforms), se reserva memoria para almacenar los identificadores de las mismas (l´ınea 4) y se vuelve a llamar a clGetPlatformIDs (l´ınea 5) para obtener dichos datos y volcarlos en la estructura pltfrmIds.
1 c l _ u in t n P l a t f o r m s ;
2 c l _ p l a t f o r m _ i d * p l t f r m I d s ;
3 c l G e t P l a t f o r m I D s (0 , NULL , & n P l a t f o r m s ) ;
4 p l t f r m I d s = malloc ( sizeof ( c l _ p l a t f o r m _ i d ) * n P l a t f o r m s ) ; 5 c l G e t P l a t f o r m I D s ( nPlatforms , p l t f r m I d s , NULL ) ;
Listado 2.3: Ejemplo de obtenci´on de plataformas en un host OpenCL
Obtenci´on de los dispositivos OpenCL a usar
Una vez conocidas las plataformas disponibles, se ha de elegir qu´e dispositivos de las mismas se desean utilizar. De forma similar a como sucede para las plataformas, en el caso de los dispositivos tambi´en resulta necesario llamar dos veces a clGetDeviceIDs, funci´on que permite obtener sus identificadores. En primer lugar (l´ınea 1 del listado 2.4) se obtiene, para un identificador de plataforma dado, el n´umero de dispositivos dispo-nibles. Una vez reservado el espacio necesario (l´ınea 2), se obtienen los identificadores correspondientes (l´ınea 3). N´otese que para obtener tanto el n´umero de dispositivos co-mo la lista de identificadores, es necesario especificar qu´e tipo de dispositivos se desean utilizar (v´ease tabla 2.3). En este caso, el dispositivo en cuesti´on es una GPU, cuyo identificador de tipo es CL DEVICE TYPE GPU.
1 c l G e t D e v i c e I D s ( pl_ID , C L _ D E V I C E _ T Y P E _ G P U ,0 , NULL ,& nGPUS ) ; 2 clDevs = malloc ( sizeof ( c l _ d e v i c e _ i d ) * nGPUS ) ;
3 c l G e t D e v i c e I D s ( pl_ID , C L _ D E V I C E _ T Y P E _ G P U , nGPUS , clDevs , NULL ) ;
Identificador Descripci´on
CL DEVICE TYPE CPU CPU del host
CL DEVICE TYPE GPU GPU
CL DEVICE TYPE ACCELERATOR Acelerador dedicado (un blade, por ejemplo) CL DEVICE TYPE DEFAULT Dispositivo por defecto de la plataforma CL DEVICE TYPE ALL Todos los dispositivos
Tabla 2.3: Tipos de dispositivos recogidos en el est´andar OpenCL
Creaci´on de un contexto para los dispositivos
Una vez seleccionados plataforma y dispositivo kernel, se procede a crear el contexto que servir´a de base para la ejecuci´on del kernel. Previamente (l´ınea 1 del listado 2.5) a la creaci´on del contexto se define un array cps de propiedades en el que se indica qu´e plataforma se va a usar. A partir de dicho array se crea el contexto context llamando a la funci´on clCreateContextFromType (l´ınea 2) especificando, adem´as de las propiedades, el tipo de dispositivo a usar. Tambi´en ser´ıa posible especificar un puntero a funci´on que ser´a invocada por OpenCL para informar de cualquier error sucedido en el seno del contexto creado y gestionarlo seg´un establezca el usuario.
1 c l _ c o n t e x t _ p r o p e r t i e s cps [3] = { C L _ C O N T E X T _ P L A T F O R M ,( c l _ c o n t e x t _ p r o p e r t i e s ) p l a t f o r m s [ s e l e c t e d _ p l a t f o r m ] , 0};
2 c o n t ex t = c l C r e a t e C o n t e x t F r o m T y p e ( cps , C L _ D E V I C E _ T Y P E _ G P U , NULL , NULL ,& err ) ;
Listado 2.5: Ejemplo de creaci´on de un contexto OpenCL
Creaci´on de colas de comandos
Con el contexto creado, es el momento de establecer la cola o colas que servir´an al host para enviar diferentes tipos de comandos al dispositivo. Para crear una cola basta llamar a la funci´on clCreateCommandQueue (listado 2.6) indicando el contexto para el que se est´a creando la cola, a qu´e dispositivo se enviar´an los comandos que en ella se inserten y el modo o modos de ejecuci´on de la misma. Existen dos posibles modos de ejecuci´on: CL QUEUE PROFILING ENABLE, que indica que la cola admitir´a operaciones de