1 Università degli Studi di Milano Bicocca Facoltà di Scienze Matematiche Fisiche e Naturali Corso di Laurea in Informatica Sviluppo di un framework in OpenCL e C++ per Image Processing Supervisori: Dott. Prof. Gianluigi Ciocca Dott. Prof. Alessandro Colombo Relazione della prova nale di: Paolo Surricchio Matricola: 708622 Anno Accademico 2009/2010 2 Is a man not entitled to the sweat of his own brow? 'No!' says the man in Washington, 'It belongs to the poor.' 'No!' says the man in the Vatican, 'It belongs to God.' 'No!' says the man in Moscow, 'It belongs to everyone.' I rejected those answers; instead, I chose something dierent. I chose the impossible. ... 1 (Andrew Ryan, a character from Bioshock the videogame ) 1 http://www.2kgames.com/bioshock/ 3 0.1 Ringraziamenti Ho voluto dedicare una pagina di ringraziamenti in quanto questa tesi è il risultato del lavoro congiunto di così tante persone, vicine e lontane, conosciuti e sconosciuti, che sarebbe sbagliato non ringraziare. Sicuramente il primo ringraziamento va ai Dott.ri Gianluigi Ciocca e Alessandro Colombo che mi hanno dato l'opportunità di lavorare su questo argomento veramente molto aascinante e, insieme, abbiamo deciso come sviluppare il lavoro, giorno per giorno. Grazie per avermi accompagnato in questa ricerca che mi ha dato molto più di quanto può sembrare. Avete svolto un ruolo ben oltre la normale didattica dimostrando una professionalità che solo la passione per questa scienza può veramente giusticare. Grazie anche a tutto il team del laboratorio IVL che condivide con loro professionalità e passione. Un ringraziamento speciale va a David Gohara, ricercatore al centro di biologia computazionale di St. Louis[13]. Grazie per aver rilasciato gratuitamente i tutorial di OpenCL, l'unico barlume di luce che ho trovato all'inizio quando tutto mi sembrava complicato ed oscuro. Ringrazio direttamente tutto lo sta del sito http://www.macresearch.org/ che rilasciano gratuitamente strumen- ti professionali per lavorare in ambito scientico; è grazie alle persone come loro che l'essere umano continua a crescere ed imparare senza limiti economici, politici e/o religiosi. Un altro ringraziamento va al materiale rilasciato da AMD e NVIDIA relativo ad OpenCL. In particolare, ringrazio Justin Hensley, Senior Member dello sta tecnico per i tutorial ed il materiale, compreso il SDK installato su linux per lavorare in OpenCL. Un piccolo ringraziamento all'utente del forum nou per aver integrato il SDK di ATI/AMD di OpenCL in dei pacchetti .deb che sono comodamente installabili senza dover impostare nient'altro. Un ultimo ringraziamento, prima di concludere, va a tutti quelli che, dietro al sipario , ogni giorno lavorano per persone che non vedranno mai. Grazie a tutti coloro che hanno reso possibile la realizzazione di questa tesina e non sono stati menzionati. E per ultimo, ma non per importanza, un ringraziamento speciale alla mia famiglia. Sapete benissimo cosa avete fatto per me e, alla ne, non esisteranno parole che possano essere scritte su un foglio di carta per ringraziarvi. In particolare grazie per avere creduto in me, grazie per aver creduto sin dall'inizio che ce l'avrei fatta, quando neanche io ci credevo, quando neanche io credevo che fosse possibile. Grazie per avermi fatto capire il signicato della parola credere . Grazie a tutti. Paolo Surricchio. Indice 0.1 Ringraziamenti . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 0.2 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 0.2.1 7 Struttura del documento . . . . . . . . . . . . . . . . . . . 1 Architettura delle GPU 1.1 1.2 . . . . . . . . . . . . . 9 1.1.1 Evoluzione delle GPU . . . . . . . . . . . . . . . . . . . . 9 1.1.2 La situazione odierna . . . . . . . . . . . . . . . . . . . . 10 Architettura generica di una GPU 1.2.1 1.2.2 1.3 9 La storia delle GPU: radici ed evoluzione Architettura . . . . . . . . . . . . . . . . . 11 . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.2.1.1 GPU: Graphics Processing Unit 1.2.1.2 Video Bios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.2.1.3 Video Memory . . . . . . . . . . . . . . . . . . . 13 1.2.1.4 RAMDAC . . . . . . . . . . . . . . . . . . . . . 13 1.2.1.5 Output . . . . . . . . . . . . . . . . . . . . . . . 14 1.2.1.6 Motherboard Interface . . . . . . . . . . . . . . . 14 1.2.1.7 Cooling device . . . . . . . . . . . . . . . . . . . 14 1.2.1.8 Power Demand . . . . . . . . . . . . . . . . . . . 14 1.2.1.9 Osservazioni 14 . . . . . . . . . . . . . . . . . . . . 13 Pipeline di rendering[11, 12] . . . . . . . . . . . . . . . . . 15 1.2.2.1 Trasformation 15 1.2.2.2 Per-Vertex Lightning 1.2.2.3 Viewing Trasformation . . . . . . . . . . . . . . 16 1.2.2.4 Primitives Generation . . . . . . . . . . . . . . . 16 1.2.2.5 Projection Trasformation . . . . . . . . . . . . . 16 1.2.2.6 Clipping . . . . . . . . . . . . . . . . . . . . . . 16 1.2.2.7 Scan Conversion, Rasterization . . . . . . . . . . 17 1.2.2.8 Texturing, fragment shading . . . . . . . . . . . 17 1.2.2.9 Display . . . . . . . . . . . . . . . . . . . . . . . 17 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 Un esame di una scheda: NVIDIA GeForce GTX 285 . . . . . . . 17 1.3.1 Descrizione generale . . . . . . . . . . . . . . . . . . . . . 18 1.3.2 Dettagli dell'analisi . . . . . . . . . . . . . . . . . . . . . . 18 1.3.3 Dierenze qualitative fra GPU e CPU . . . . . . . . . . . 19 Conclusioni dell'analisi fra CPU e GPU . . . . . 22 Il chip GT200: organizzazione logica . . . . . . . . . . . . 22 1.3.3.1 1.3.4 4 5 INDICE 1.3.4.1 1.4 Streaming Multiprocessor . . . . . . . . . . . . . Conclusioni sull'architettura delle GPU . . . . . . . . . . . . . . 2 OpenCL: Open Computing Language 2.1 2.2 28 2.1.1 Storia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 2.1.2 Cos'è . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 2.1.3 Perchè . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 2.1.4 Come 30 2.1.5 Perchè OpenCL per Image Processing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 2.2.1 The OpenCL Architecture . . . . . . . . . . . . . . . . . . 31 2.2.1.1 Modello di piattaforma 31 2.2.1.2 Modello di esecuzione . . . . . . . . . . . . . . . 33 2.2.1.3 Modello di memoria . . . . . . . . . . . . . . . . 34 2.2.1.4 Modello di programmazione . . . . . . . . . . . . 35 . . . . . . . . . . . . . . 2.2.2 Il Framework OpenCL . . . . . . . . . . . . . . . . . . . . 35 2.2.3 Il livello piattaforma di OpenCL 36 . . . . . . . . . . . . . . 2.2.3.1 Richieste sulla piattaforma . . . . . . . . . . . . 36 2.2.3.2 OpenCL Device . . . . . . . . . . . . . . . . . . 36 2.2.3.3 Il Contesto . . . . . . . . . . . . . . . . . . . . . 38 The OpenCL Runtime . . . . . . . . . . . . . . . . . . . . 38 2.2.4.1 Code di comando 38 2.2.4.2 Oggetti di memoria . . . . . . . . . . . . . . . . 38 2.2.4.3 Oggetti programma . . . . . . . . . . . . . . . . 40 2.2.4.4 Oggetti kernel . . . . . . . . . . . . . . . . . . . 40 2.2.4.5 Esecuzione dei kernels . . . . . . . . . . . . . . . 40 2.2.4.6 Oggetti evento . . . . . . . . . . . . . . . . . . . 41 2.2.4.7 Flush and nish . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . 42 Un esempio di OpenCL . . . . . . . . . . . . . . . . . . . . . . . 3.2 42 2.3.1 Il kernel . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 2.3.2 Inizializzazione delle risorse . . . . . . . . . . . . . . . . . 44 2.3.3 Compilazione . . . . . . . . . . . . . . . . . . . . . . . . . 47 2.3.4 Creazione degli oggetti memoria 47 2.3.5 Esecuzione dei kernel . . . . . . . . . . . . . . . . . . . . . 49 2.3.6 Lettura dei dati e Release delle risorse . . . . . . . . . . . 49 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Progettazione del Framework 3.1 30 The OpenCL Specication . . . . . . . . . . . . . . . . . . . . . . 2.2.5 2.4 28 OpenCL - Open Computing Language . . . . . . . . . . . . . . . 2.2.4 2.3 24 26 50 52 Introduzione alla progettazione: framework di OpenCL e librerie IVLLIB . . . . . . . . . . . . . . 53 3.1.1 Le librerie IVLLIB . . . . . . . . . . . . . . . . . . . . . . 53 Requisiti e speciche del framework . . . . . . . . . . . . . . . . . 54 3.2.1 Requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 3.2.2 Speciche . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 6 INDICE 3.3 3.4 3.2.2.1 Astrazione delle meccaniche di OpenCL . . . . . 3.2.2.2 Oggetti . . . . . . . . . . . . . . . . . . . . . . . 56 3.2.2.3 Singleton Design Pattern 56 3.2.2.4 Kernel OpenCL . . . . . . . . . . . . . . . . . . 57 3.2.2.5 Politiche di gestione . . . . . . . . . . . . . . . . 57 3.2.2.6 Gestione degli errori . . . . . . . . . . . . . . . . 58 3.2.2.7 Trasparenza diversa per utenti diversi . . . . . . 58 Diagrammi di usso e UML . . . . . . . . . . . . . . . . . . . . . 59 3.3.1 Flow Chart 60 3.3.2 Diagramma UML delle classi 3.3.3 Note progettuali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 . . . . . . . . . . . . . . . . . . . . . . . 62 Design di Algoritmi di Image Processing con il framework di OpenCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.4.1 63 3.4.2 Programmatore della libreria: Gamma Correction 3.4.3 . . . . 3.4.1.1 La Gamma Correction . . . . . . . . . . . . . . . 64 3.4.1.2 Progettazione Kernel . . . . . . . . . . . . . . . 64 3.4.1.3 Possibile Codice del Programma . . . . . . . . . 65 Utilizzatore del Framework di OpenCL: Filtro di Smoothing 3.5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.2.1 Filtro di Smoothing 3.4.2.2 Progettazione Kernel 3.4.2.3 67 . . . . . . . . . . . . . . . . 67 . . . . . . . . . . . . . . . 68 Possibile Codice del Programma . . . . . . . . . 69 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . 71 Possibilità di modica ed evoluzione del progetto . . . . . . . . . 71 4 Conclusioni 4.1 56 72 OpenCL: perchè . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 4.1.1 Ecienza Computazionale . . . . . . . . . . . . . . . . . . 72 4.1.2 OpenCL è Open 73 4.1.3 Con questo framework, OpenCL è facile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 4.2 Cosa porta di nuovo . . . . . . . . . . . . . . . . . . . . . . . . . 73 4.3 Note nali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 0.2 Introduzione Questa tesi ha lo scopo di analizzare tutte le sfaccettature di un argomento che negli ultimi anni ha visto crescere la sua rilevanza in ogni ambito applicativo: il calcolo GPGPU. 2 Negli ultimi anni il calcolo tradizionale si è rivelato insuciente a soddisfare la richiesta di potenza necessaria all'elaborazione, in real-time o meno, di algoritmi sempre più sosticati e mole di dati di qualità e complessità sempre maggiori. Nonostante l'evoluzione ingegneristica produca ogni anno processori sempre più potenti, da subito è stato percepito il bisogno dello sviluppo di un 2 GPGPU: General-purpose computing on graphics processing units. 7 INDICE componente a se stante dedicato alla realizzazione di calcoli specializzati su dati di uno specico tipo. Nei computer di tutti giorni è cresciuto un componente di importanza sem- 3 nel 1981 pre maggiore: la scheda video. Fin da quando è nata, (dalla MDA 4 allo standard SVGA [1]) la scheda video è stata costruita per permettere l'esecuzione di calcoli in parallelo necessari alla realizzazione di complessi programmi graci. Negli anni questo componente è stato aggiornato e potenziato, versione dopo versione, arrivando no ad oggi dove con poche decine di euro è possibile comprare un processore graco con ottime prestazioni. Da questa presa di coscienza nasce la consapevolezza di poter sfruttare questo componente non solo per lo scopo pressato, ovvero come motore della 5 pipeline di rendering , ma anche come piattaforma di calcolo generico. Il 15 febbraio 2007, NVIDIA rilascia il primo SDK per CUDA[3]. Quasi un anno dopo nel Dicembre 2007 AMD, dopo l'acquisizione di ATI avvenuta 6 circa un anno prima , rilascia lo Stream Computing SDK[2]: l'era del calcolo GPU-based è incominciata. La Apple denisce un gruppo di lavoro con AMD, IBM, Intel e NVIDIA ed inizia lo sviluppo di OpenCL[5]. Bisogna aspettare il 16 Luglio 2008 per veder 7 il gruppo della Khronos [5], composto dai maggiori esponenti nel campo fra cui costruttori di CPU, GPU e processori embedded, unirsi per creare una specica di un linguaggio per il calcolo parallelo. Il 18 Novembre 2008 viene rilasciata la specica 1.0 delle OpenCL. Obiettivo nale di questa tesi è la progettazione e la programmazione di un framework che possa astrarre molte delle funzioni della specica OpenCL e che sia in grado di integrarsi con delle librerie di image processing IVLLIB 8 fornendo al programmatore uno strumento facile quanto potente per realizzare i suoi algoritmi sull'architettura OpenCL. 0.2.1 Struttura del documento La prima parte di questa tesi analizzerà l'architettura logica delle GPU, l'esame di una pipeline di rendering e inne verrà preso come caso di studio un esempio di scheda video recente (NVIDIA GTX 285[6, 7]). Verranno inoltre analizzate le principale analogie e dierenze architetturali fra GPU e CPU. Nella seconda parte verrà presentato il linguaggio OpenCL partendo da una descrizione generale no ad arrivare all'analisi di alcune funzioni della specica 1.0 di OpenCL. Verranno inoltre analizzate le sue caratteristiche e le logiche in cui opera e verrà mostrato un esempio di un programma completo scritto in C e OpenCL. 3 MDA: Monochrome Display Adapter. 4 SVGA: Super Video Graphics Array. 5 Verrà spiegato in dettaglio nelle sezioni successive. 6 Acquisizione di ATI da parte di AMD: 24 Luglio 2006[4]. 7 Khronos Compute Working Group. 8 L'integrazione e il commento di queste librerie compongono il terzo capitolo di questa tesi. INDICE 8 Nella terza parte di questa tesi si discuteranno la progettazione e le scelte implementative del framework realizzato in C++ e OpenCL e verranno analizzati alcuni esempi di possibili utilizzi per la programmazione di algoritmi di image processing in OpenCL da parte di diversi utenti. Inoltre verrà descritta la libreria IVLLIB per comprendere l'integrazione di questo framework con la libreria. Inne l'ultimo capitolo accoglierà le conclusioni a cui lo studio di OpenCL e la progettazione del framework sono arrivate. Capitolo 1 Architettura delle GPU In questo capitolo viene analizzata l'architettura logica delle schede video, modellizzando gli esempi, per spiegare come è costruito questo componente presente ormai in ogni computer in vendita oggi. Verranno analizzate le caratteristiche comuni delle architetture GPU per poi entrare nel caso specico di una scheda video recente, la GeForce GTX 285 [6, 7]. 1.1 La storia delle GPU: radici ed evoluzione Dalla realizzazione dei primi computer con terminale a schermo si è sentita la necessità di progettare un componente staccato ed indipendente dal processore che fosse responsabile della visualizzazione sul display delle operazioni che avvenivano nella macchina. Una GPU 1 è un processore collegato alla scheda video creato per compiere questo lavoro in maniera autonoma risperro alla CPU. Un accelleratore graco contiene microchip costruiti appositamente per la realizzazione delle comuni operazioni matematiche in oating point usate nel rendering graco. [8] 1.1.1 Evoluzione delle GPU 2 Il primo esempio di GPU si può trovare nel chip realizzato dalla ANTIC e dalla CTIA negli anni 70: il chip svolgeva il compito di controllore hardware per la modalità graca e testuale, la posizione delle sprite 3 e il display delle immagini a schermo. Questo chip era il primo esempio di processore dedicato esclusivamente al mapping del testo e della graca a schermo. L' IBM Professional Graphics Controller fu il primo vero esempio di processore 2D/3D disponibile per i pc IBM: nel 1984, costava circa 4500$ e questo, 1 GPU: Graphics Processing Unit, unità di calcolo graco. 2 Viene mantenuta la stessa formattazione della fonte [8] da cui si ispira tutta questa sezione. 3 In graca informatica, gura bidimensionale che può essere spostata rispetto allo sfondo. 9 CAPITOLO 1. 10 ARCHITETTURA DELLE GPU complice la mancata compatibilità con i sistemi del tempo, non permisero la diusione di questa scheda. L'Amiga Commodore fu il primo computer di massa a montare una scheda dedicata esclusivamente al calcolo graco ed alla implementazione di base delle primitive 2D via hardware. Nel 1991, la S3 Graphics fu la prima a costruire un chip ad alte prestazioni per la graca 2D: da allora ebbe inizio, in tutto il corso degli anni 90, la realizzazione di schede video dalle caratteristiche di volta in volta migliori: una gara di prestazioni tutt'oggi ancora aperta. prime API In quegli anni si svilupparono le 4 grache ed i primi sistemi operativi con una GUI5 . A metà degli anni novanta le schede video dovevano essere in grado di compiere calcoli per la graca 3D sia per il computer che per le console. Sempre all'inizio degli anni novanta nascono le OpenGL e qualche anno dopo le DirectX: le schede video devono essere in grado di soddisfare i requisiti e le richieste di un mercato in continua evoluzione. Dagli anni 2000 ci fu l'avvento più importante nel campo della computer graphics: la programmazione shader. Gli shader sono un insieme di istruzioni software che permettono di calcorare eetti di rendering su hardware graco con 6 grande essibilità[19] . Le schede video, dopo la crescita di OpenGL e DirectX come librerie grache, devono quindi adattarsi alle richieste del mercato e supportare shading programmabili per permettere ai programmatori di accedere alle funzionalità che abilitano il render della scena sullo schermo. NVIDIA fu la pri- 7 ma a costruire una scheda video con shader programmabili . Pochi mesi dopo, con l'introduzione della ATI Radeon 9700, viene creato il primo accelleratore delle librerie Direct3D 9.0 in grado di compiere loop e lunghi calcoli matematici in oating point grazie a pixel e vertex shader: queste GPU sono estremamente più veloci del processore nella gestione di operazioni su immagini immagazzinate in memoria come array. 1.1.2 La situazione odierna Oggi, le GPU sono un componente fondamentale in ogni computer. Lo sviluppo della graca real time o batch rappresenta una fetta del mercato dell'informatica rilevante. Le due aziende leader nel settore sono NVIDIA e ATI/AMD. Queste due case, come scritto prima, hanno una lunga storia nella produzione di chip graci e ogni anno producono set di schede video sempre più potenti e 8 e delle speciche più economiche. Lo sviluppo delle nuove librerie DirectX 11 9 OpenGL 4.0 richiedono, al giorno d'oggi, che le schede video siano in grado di supportare calcoli sempre più complessi. Con poche centinaia di dollari è 4 API: Application Programming Interface. 5 GUI: Graphics user interface. 6 Esistono più tipi di shader e sarebbe interassante Viene evitato in quanto non oggetto di questa tesi. un approfondimento sull'argomento. 7 La serie GeForce 3, nome in codice NV20. 8 http://www.microsoft.com/games/en-US/aboutGFW/pages/directx.aspx 9 http://www.khronos.org/opengl/ CAPITOLO 1. 11 ARCHITETTURA DELLE GPU Figura 1.1: Scheda Video Ati Radeon 5850[9]. possibile entrare in possesso di una scheda che no a qualche anno fa era architetturalmente impossibile da costruire. Vengono presi due esempi di scheda video, un esemplare di ATI/AMD e un esemplare di NVIDIA. Entrambe queste schede sono presenti sul mercato internazionale ad un prezzo di circa 250¿. Entrambe queste schede video sono compatibili con le tecnologie sopra citate e sono in grado di supportare senza problemi ogni applicazione videoludica e non presente sul mercato in data odierna. Inoltre, oggi, l'applicazione di schede per calcolo general purpose è sempre più diuso in ogni ambito dove venga richiesta la possibilità di eseguire calcoli paralleli il più velocemente possibile 10 . 1.2 Architettura generica di una GPU In questa sezione verrà analizzata schematicamente l'architettura delle GPU e la realizzazione di una generica pipeline di rendering. Verranno mostrare le analogie fra la pipeline di rendering e le caratteristiche del calcolo GPGPU. 10 Gli ambiti in cui vengono applicati il calcolo GPGPU sono veramente vari e spaziano dalle applicazioni grache in real time alla medicina, dalle ricerche metereologiche agli studi dei fenomeni di evoluzione di massa. CAPITOLO 1. 12 ARCHITETTURA DELLE GPU Figura 1.2: Scheda Video GeForce GTX 285[10]. 1.2.1 Architettura Esistono principalmente due tipi di schede video: le soluzioni integrate nella motherboard del computer o le schede video dedicate (come gure 1.1 nella pagina precedente e 1.2). Verrà approfondita l'architettura di schede video dedicate (si ricorda che in questa sezione si fa riferimento ad un modello teorico di scheda video, senza fare nessun riferimento a nessuna scheda in particolare). Una scheda video dedicata è generalmente composta da GPU; Video BIOS; Video Memory; RAMDAC; Outputs; Motherboard Interface; Cooling Device; Power Demand. 11 Interamente ispirato a [1]. 11 : CAPITOLO 1. 13 ARCHITETTURA DELLE GPU 1.2.1.1 GPU: Graphics Processing Unit La GPU è il componente principale di una scheda video: si occupa delle operazioni e dei calcoli in oating point, fondamentali per il calcolo 3D. Il componente principale, e sicuramente anche il più complesso, il processore graco si caratterizza per velocità di clock espressa in Hertz (Hz ) e per il numero di pipelines che deniscono la potenza necessaria con cui questo componente può eettuare i calcoli in parallelo. 1.2.1.2 Video Bios Il Bios Video è il programma di base che governa le operazioni della scheda video e permette l'interazione fra la macchina e la scheda gestendo tutti i particolari dovuti alla comunicazione fra questi due soggetti. Contiene tutte le informazioni di basso livello come le frequenze di GPU, memorie, timings e voltaggio di ogni componente che opera nella scheda video. 1.2.1.3 Video Memory La memoria video svolge il componente di memoria centrale per la scheda video. Dato che la memoria deve permettere accesso al processore ed agli altri componenti, deve essere il più veloce possibile: per questo vengono usate speciali memorie high-speed o multi-port 12 . Dal 2003 a oggi, sono state costruite schede 13 sempre più veloci. video con memorie DDR La tabella qui sotto riassume la velocità di accesso e di trasmissione delle memorie usate nelle schede video[1]: Type Memory clock rate (MHz) Bandwidth (GB/s) 1.2 - 30.4 DDR 166 - 950 DDR2 533 - 1000 8.5 - 16 GDDR3 700 - 2400 5.6 - 156.6 GDDR4 2000 - 3600 128 - 200 GDDR5 3400 - 5600 130 - 230 1.2.1.4 RAMDAC14 La RAMDAC è un componente analogico/digitale che ha il compito di regolare il funzionamento della scheda video rispetto allo schermo. Converte il signale da digitale ad analogico per permettere ai dispositivi video come schermi CRT una corretta visualizzazione dell'immagine. In seguito alla diusione di schermi per computer digitali e alla integrazione di questo componente nel die della GPU, la RAMDAC è andata sparendo come componente a se stante nelle schede video. 12 VRAM, WRAM, SGRAM, etc. 13 Non viene specicata la denizione tesi; per informazioni dettagliate: 14 Random di memorie DDR, in quanto non oggetto di questa http://en.wikipedia.org/wiki/Double_data_rate Access Memory Digital-to-Analog Converter. CAPITOLO 1. 14 ARCHITETTURA DELLE GPU 1.2.1.5 Output Nelle schede video odierne è possibile trovare uno o più fra i seguenti connettori 15 : per collegare uno o più schermi alla scheda video VGA; DVI; VIVO (Video In Video Out); HDMI. 1.2.1.6 Motherboard Interface L'interfaccia di comunicazione fra scheda video e scheda madre è un componente fondamentale, sia ai ni del l'uso GPGPU 16 , sia per gli scopi standard della GPU. Un bus veloce permette una comunicazione agile fra sistema e scheda; oggi il bus più usato è il PCI Express 16x 2.0 17 in continua evoluzione. 1.2.1.7 Cooling device Come ogni componente del computer, anche la scheda video (ed in particolare la GPU) ha bisogno di dissipare il calore che produce in seguito all'uso. Per 18 o di 19 20 un meccanismo che mantenga la temperatura del chip sotto un certo valore . questo motivo ogni modello di scheda video è provvisto di un dissipatore 1.2.1.8 Power Demand Dato l'aumento di prestazioni delle schede video, col passare del tempo e della continua evoluzione dei modelli è aumentata anche la richiesta in termini energetici. Pochi modelli riescono a essere alimentati dalla corrente presente sul bus di comunicazione della scheda madre ed ormai ogni modello presente oggi sul mercato dispone dei connettori da collegare direttamente all'alimentatore per fornire la corrente necessaria ad ogni componente della scheda. 1.2.1.9 Osservazioni Dalla descrizione precedente, è possibile intuire la presenza di una relazione fra l'architettura di un intero computer (scheda madre, processore e memoria centrale) e l'achitettura di una scheda video. Questa relazione è indice di due 15 Non è interesse in questa tesi entrare nello specico dei vari connettori e delle loro dierenze. 16 Questo dettaglio 17 PCIe x16 2.0: sarà chiarito più avanti. Width (bits)=1 Ö 16, Bandwidth(MB/s)=8000 / 16000, Type=Serial. Clock Rate (MHz)=5000 18 Denito attivo se ci sono ventole o passivo senza ventole. 19 Dissipazione ad aria; sistema a liquido; dissipatore con ventole. 20 Circa 100 gradi centigradi, dipendente dal chip e dalla sua qualità. / 10000, CAPITOLO 1. 15 ARCHITETTURA DELLE GPU principali fattori: la complessità necessaria a sviluppare un hardware dedicato sempre più potente e indipendente dalla macchina sottostante e la qualità ingegneristica raggiunta nei nostri giorni. 1.2.2 Pipeline di rendering[11, 12] Nella computer graphics, la pipeline di rendering è quel processo svolto dalla scheda video in grado di renderizzare a schermo, quindi in un ambiente 2D, la rappresentazione di oggetti descritti matematicamente in una scena tridimensionale; vengono quindi calcolate luci, ombre, la posizione dell'osservatore ed il suo orientamento. Verrà quindi descritto un approcio ad alto livello in grado di evidenziare i passi fondamentali di cui questo processo è composto per capire come le GPU possano essere utilizzate dai programmatori come motori per calcoli paralleli. Il processo della pipeline di rendering è generalmente suddiviso in Trasformation; Per-Vertex Lightning; Viewing Trasformation; Primitives Generation; Projection Trasformation; Clipping; Rasterizazion; Texturing, Fragment Shading; Display. 21 : 1.2.2.1 Trasformation Una GPU specica un oggetto in relazione alla sua posizione nell'ambiente in un certo spazio di coordinate; questa operazione risulta comoda alla scheda per l'organizzazione dei dati nella rappresentazione logica di un ambiente. Prima di eettuare il rendering occorre però eettuare una serie di trasformazioni per costruire l'ambiente in un sistema di coordinate comuni. Queste trasformazioni 22 . Queste operazioni23 portano sono limitate a rotazioni, traslazioni e scaling le schede video a compiere calcoli vettoriali su matrici e il bisogno di compiere una quantità elevata di operazioni oating point ha richiesto che la scheda video elabori queste operazioni con grande ecienza. Citando David Luebke e Greg Humphreys[12]: 21 Ci sono dierenza fra la pipeline di rendering in tempo reale o quella batch 22 Esistono molte trasformazioni che non vengono citate qui in quanto non oggetto analisi. 23 Come, ad esempio, la rappresentazione degli oggetti in coordinate omogenee. di questa CAPITOLO 1. 16 ARCHITETTURA DELLE GPU The need for ecient hardware to perform oating-point vector arithmetic for millions of vertices each second has helped drive the GPU parallel-computing revolution. 1.2.2.2 Per-Vertex Lightning Dopo aver mappato ogni gura nel sistema di coordinate omogenee, la scheda 24 . video deve occuparsi di colorare gli oggetti in base alle luci presenti nel mondo La GPU gestisce più luci e calcola il contributo di ognuna di queste in ogni punto 25 . Per adempiere a questo compito basta sapere che, ancora una volta, la GPU dovrà compiere un numero elevato di calcoli vettoriali. 1.2.2.3 Viewing Trasformation In questa parte, la scheda deve compiere calcoli per trasportare l'ambiente tridimensionale nel sistema di riferimento della camera, rispetto al punto di vista dell'osservatore. Anche qui, una grande mole di dati vengono elaborati con calcoli matriciali e vettoriali, sviluppati sempre via hardware per avere un'ecienza più alta possibile. 1.2.2.4 Primitives Generation In questo processo vengono semplicemente creati i poligoni in base alle regole di costruzione dei vertici e vengono rimappate le primitive generate dalle trasformazioni eettuate. 1.2.2.5 Projection Trasformation La trasformazione prospettica consiste nel trasformare un ambiente tridimensionale in una gura bidimensionale rispetto al punto di osservazione della camera virtuale. Anche qui la GPU ha più modalità per determinare come gli oggetti devono essere portati dallo spazio 3D ad una rappresentazione bidimensionale dipendente dal punto di vista da cui la scena viene osservata. 1.2.2.6 Clipping Ora la scheda video deve calcolare le coordinate che niscono fuori dalla viewing frustum 26 ed deve ignorare tutti i vertici che non appartengono all'insieme. Questa operazione accellera il processo successivo di rasterizzazione eliminando le porzioni dell'immagine non necessarie. 24 Non viene specicato, in quanto non oggetto di questa sezione, a quali modelli di illuminazione stiamo facendo riferimento. 25 In questa sezioni non ci interessa nello specico la matematica usata; l'unico nostro interesse è la modalità di calcolo, ovvero calcolo vettoriale. 26 Potrebbe essere tradotto nestra di visione e corrisponde alla zona che la telecamera vede realmente e quindi non interessa analizzare i vertici che niscono fuori da questa zona. CAPITOLO 1. ARCHITETTURA DELLE GPU 17 1.2.2.7 Scan Conversion, Rasterization E' sicuramente possibile che ogni poligono nella gura incroci uno o più pixel: determinare quali sono i pixel appartenenti alla gura e quali non lo sono è compito del processo di rasterizzazione. In questo processo, l'immagine 2D viene direttamente convertita in un'immagine raster. Dato che ogni pixel è indipendente dagli altri, le architetture delle schede video sono state concepite con l'obiettivo di compiere queste operazioni in parallelo. Questa osservazione ha portato a costruire schede con più pipelines che lavorano in parallelo, una indipendentemente dall'altra. 1.2.2.8 Texturing, fragment shading In questo stadio vengono determinati i colori dei pixel mediante interpolazione fra questi, prendendo il colore dalla matrice raster o da texture presenti in memoria. Le GPU salvano queste texture in memorie ad alta velocità; la scheda permette quindi ad ogni pixel di accedere indipendentemente alla memoria texture. Dato che l'accesso a questa memoria è un comportamento modale, sono state sviluppare tecniche di caching per permettere che questa funzione operi con grande ecienza nascondendo così la latenza che può avere l'accesso in memoria. 1.2.2.9 Display La matrice di pixel colorata può essere nalmente mandata allo schermo, o al dispositivo di visione, per essere visualizzata. 1.3 Un esame di una scheda: NVIDIA GeForce GTX 285 27 La premessa che occorre fare riguarda la scelta di questa sezione: per spiegare la tecnologia OpenCL è utile entrare nel dettaglio di un esempio di architettura di scheda video. La scelta è caduta sulla GeForce GTX 285 (immagine 1.2 nella pagina 12) per semplici ragioni: è una scheda abbastanza nuova e permette l'esecuzione delle tecnologie CUDA[3] e OpenCL; è la scheda su cui è stato trovato più materiale, ispirandomi prevalentemente ai tutorial distribuiti sul sito 28 ; http://www.macresearch.org/[14] da David Gohara[13] grazie alla tecnologia CUDA[3], è stato più facile reperire informazioni a riguardo dell'architettura NVIDIA. 27 Questa sezione è pesantemente ispirata a [13]. 28 A cui viene dedicato un ringraziamento speciale. CAPITOLO 1. 18 ARCHITETTURA DELLE GPU 1.3.1 Descrizione generale Uscita nel 15 Gennaio 2009[6] al prezzo di 340$, questa scheda viene costruita con processo produttivo a 55 nanometri. Vengono riportate direttamente le speciche dal sito del produttore[7]: Dato Valore Memory Specs: Memory Clock (MHz) 1242 Standard Memory Cong 1 GB GDDR3 Memory Interface Width 512-bit Memory Bandwidth (GB/sec) 159.0 Feature Support: NVIDIA SLI Ready 2 way/3 way NVIDIA 3D Vision Ready yes NVIDIA Pure Video Technology HD NVIDIA PhysX Ready yes NVIDIA CUDA Technology yes Microsoft DirectX 10 OpenGL 2.1 Certied for Windows 7 yes Maximum Digital Resolution 2560x1600 Maximum VGA Resolution 2048x1536 Standard Display Connectors HDTV Two Dual Link DVI Multi Monitor yes HDCP yes HDMI* Via adapter Audio Input for HDMI SPDIF Thermal and Power Specs: Maximum GPU Temperature (in C) 105 C Maximum Graphics Card Power (W) 204 W Minimum System Power Requirement (W) 550 W Supplementary Power Connectors 6-pin x2 1.3.2 Dettagli dell'analisi Fra tutte le parti di cui si compone la scheda video, verrà analizzato solo il processore graco per capirne l'architettura e comprendere i dettagli di cui una GPU è composta. Viene svolto quindi un approfondimento sulla struttura del processore e viene fatto riferimento alla memoria all'interno della GPU. I disegni e le ragurazioni (a meno delle immagini 1.3 nella pagina successiva, 1.4 nella pagina 20 e 1.5 nella pagina 21) sono astrazioni molto lontane dalla realtà implementativa del chip. Inoltre viene mostrata una dierenza qualitativa una CPU ed una GPU. 29 Verranno mostrate una foto di una CPU e una foto di una GPU. 29 fra CAPITOLO 1. 19 ARCHITETTURA DELLE GPU Figura 1.3: Processore Intel Core 2 Duo[18]. 1.3.3 Dierenze qualitative fra GPU e CPU Come si può vedere dalle immagini 1.3 e 1.4 nella pagina successiva, la CPU (immagine 1.3) è composta principalmente di due parti: la parte dedicata all'elaborazione (in gura nella parte superiore) contenente tutti i meccanismi 30 ) e la memoria necessari allo svolgimento delle operazioni basilari (ALU, ... cache che occupa più di metà del chip (nella parte inferiore dell'immagine). La memoria cache serve principalmente per mascherare le latenze delle operazioni da eettuare con la memoria centrale di un computer (accesso in lettura e scrittura dalla ed alla memoria). Se osserviamo piu attentamente l'immagine di una GPU (immagine 1.4 nella pagina successiva) possiamo vedere come manchi quasi totalmente la memoria di tipo cache ed invece il die sia completamente coperto da processori ai lati (ai quattro angoli), e di unità di calcolo in mezzo al die. Viene mostrata un'altra gura che permette di capire come è suddiviso il chip della GeForce GT200 (gura 1.5 nella pagina 21). Si nota subito la quantità impressionante di processori agli angoli della scheda video: verrà analizzato in seguito come questi processori sono logicamente 30 Non vengono esplicitati tutti i componenti in quanto questa tesi, ed in particolare questa sezione, non hanno l'intenzione di spiegare specicatamente le dierenze architetturali fra CPU e GPU, ma piuttosto quella di dare un'idea sulle dierenze logiche e qualitative fra i due oggetti. CAPITOLO 1. 20 ARCHITETTURA DELLE GPU Figura 1.4: Die di una GeForce GT200[15]. organizzati. Grande parte del chip è dedicata al Frame Buer (ai lati, in azzurro), componente responsabile di rendere a schermo il contenuto del buer di memoria che contiene un frame completo di dati ed immagine. In giallo sono evidenziate le unità ROP, i Raster Processor Pipeline. Nella pipeline di rendering, la pixel pipeline prende le informazioni dei pixel e dei texel e le processa con operazioni di matrici e vettori no al valore del pixel. I ROP compiono questa transazione dai buer nella memoria locale e questo include la scrittura e la lettura dei valori, insieme alle tecniche per miscelare questi dati[21]. Inne in viola sono evidenziare le Texture Processing Unit che hanno il compito di registrare i dati ed eseguire le operazioni riferite al texturing degli oggetti. Al centro troviamo uno dei componenti più importanti, e più caratteristici, di una GPU che consiste nel Thread Manager: le schede video, e questa è una dif- 31 responsabile ferenza abissale rispetto ai processori, hanno hardware dedicato della gestione dei thread. Da questo possiamo capire che le GPU svolgono operazioni legate al multithreading come context switching e gestione delle priorità fra thread completamente via hardware e quindi con una ecienza veramente alta. Poco sopra, in arancione, troviamo il setup raster, il processore responsabile dell'operazione di rasterization, l'ultima parte della pipeline di rendering. 31 Nell'immagine 1.5 nella pagina successiva corrisponde all'area colorata in grigio. CAPITOLO 1. Figura 1.5: ARCHITETTURA DELLE GPU 21 Die di una GeForce GT200[16] che evidenzia ogni particolare architetturale. CAPITOLO 1. 22 ARCHITETTURA DELLE GPU 1.3.3.1 Conclusioni dell'analisi fra CPU e GPU Questa disserzione fra CPU e GPU serve solo a sottolineare la diversa natura architetturale fra i due chip, talmente diversa da giusticare i risultati che si ottengono su una GPU rispetto ad una CPU durante certe elaborazioni. In proposito occorre citare il lavoro del collega Pigazzini Andrea[30] che ha eettuato nell'anno 2007/2008 la tesi dal titolo Utilizzo di CUDA nell'imple- mentazione di algoritmi di supporto all'Elaborazione delle Immagini . Il lavoro del collega era completamente incentrato sullo studio delle prestazioni di alcuni algoritmi di image processing eseguiti sulla scheda video rispetto che sul processore centrale e i suoi risultati sono stati sconcertanti: con una GPU da 100¿ le prestazioni erano totalmente a favore della GPU in ogni test. 1.3.4 Il chip GT200: organizzazione logica Quì di seguito verrà mostrata un modello teorico necessario per capire i dettagli fondamentali, comuni comunque a tutte le GPU, del chip NVIDIA GTX285 derivante dal chip GT200[6]. Negli schemi sono stati riportati solo i particolari interessanti all'analisi del chip, particolari che avranno ancora più senso quando verrà spiegata la logica di OpenCL. L'immagine 1.6 nella pagina seguente rappresenta un modello della GPU della GTX285 32 . Il miglior modo per descriverla è citare la fonte [22]: There are no two ways about it Nvidia's GT200 GPU is an absolute brute. It's manufactured using TSMC's 65nm process, features approximately 1.4 billion transistors and packs a total of 240 thread processors running at 1,296MHz (on the GeForce GTX 280) the result is a GPU that delivers 933.12 gigaFLOPS of compute power at peak. These numbers alone make it the largest and most complex GPU ever to be made. Dalla gura 1.6 nella pagina successiva è possibile osservare subito come è organizzata la struttura generale della GPU. Verranno analizzate solo le caratterisriche di interesse per poter poi comprendere l'architettura logica su cui opera OpenCL. Il chip GT200 è composto 33 da 10 TPC, Thread Processing Cluster34 . Ques- ta è la prima divisione gerarchica che abbiamo all'interno della GPU e d'ora in poi parlemo di come un TPC è suddiviso al suo interno. In particolare un Thread Processing Cluster è diviso al suo interno in tre SM, gli Streaming Multiprocessor. 32 Il modello reference della citazione [23] fa riferimento alla GTX280 che a livello architet- turale è praticamente identica alla GTX285; inoltre essendo un modello teorico questi due chip sono concettualmente uguali. 33 Nella gura 1.6 nella pagina seguente i rettangoli verdi grandi. 34 Questa serie di schede video è stata chiamata la serie 10 probabilmente da 10 TPC rispetta ai soli 8 della serie precedente, la serie g92[23]. perchè costituita CAPITOLO 1. ARCHITETTURA DELLE GPU 23 Figura 1.6: Architettura logica della GPU GTX285[23]. Figura 1.7: GT200[23]. Architettura logica di un TPC della serie g92 e uno della serie CAPITOLO 1. ARCHITETTURA DELLE GPU 24 Figura 1.8: Particolare di un TPC con memoria cache[23]. Uno Streaming Multiprocessor ha una memoria texture L1 cache condivisa di 24 KByte. Questo genere di memoria permette un accesso read/write molto veloce e condiviso fra gli Streaming Multiprocessor. In gura 1.9 nella pagina succes- siva, il SMC è lo Streaming Multiprocessor Controller, il responsabile della suddivisione del lavoro fra più Streaming Multiprocessor. 1.3.4.1 Streaming Multiprocessor Scendendo nel dettaglio, evitando i particolari non utili all'analisi in corso, uno Streaming Multiprocessor (in gura 1.9 nella pagina seguente) è a sua volta suddiviso in: 35 ; 8 Streaming Processor 2 Special Function Unit; 1 Double Precision Unit; 16 KB di memoria locale (shared memory) condivisa fra gli otto Streaming Processor; Gli 8 Streaming Processor sono le unità di base, quelle che eseguono il calcolo. Vengono ben schematizzate dalla gura 1.10 nella pagina 26. Per una chiara e breve descrizione cito la fonte [23]: 35 Chiamati anche streaming cores, NVIDIA) o, generalmente, cores. CUDA core (dato che stiamo analizzando un'architettura CAPITOLO 1. ARCHITETTURA DELLE GPU Figura 1.9: Particolare di uno Streaming Multiprocessor[23]. 25 CAPITOLO 1. ARCHITETTURA DELLE GPU 26 Figura 1.10: Schema logico di uno Streaming Processor. In sintesi uno Stream Processor o Processor Core è un microprocessore completo dotato di due ALU e una FPU, non ha cache e la sua dote è quella di ripetere tonnellate di operazioni matematiche! Un SP spende gran parte del suo tempo lavorando sui pixel o vertex data e non è importante il fatto che non abbia cache. Un solo SP è abbastanza inutile, ma se inserito insieme ad altri in unità come le SM (Streaming Multiprocessor) si inizia a sentire la sua potenza. Le due Special Function Unit sono delle unità responsabili di compiere operazioni e calcoli particolari: funzioni trigonometriche, esponenziali e logaritmiche. Il fatto che venga dedicato dell'hardware specico permette a queste operazioni, frequenti nel calcolo della pipeline di rendering, di essere svolte con grande ecienza. L'unità a doppia precisione ha il compito di svolgere calcolo in virgola mobile e di gestire dati molto più complessi e precisi di quelli in oating point con precisione singola, necessario in alcuni tratti della pipeline ed utile nel calcolo general purpose. La memoria locale di 16 KB è la memoria più veloce che uno Streaming Processor può usare. Sfruttata in un certo modo, questa memoria ha prestazioni elevatissime e permette agli Streaming Processor di usarla in maniera molto simile e con prestazioni molto vicine ai registri dei core. Esistono tecniche di programmazione in CUDA e in OpenCL che permettono di sfruttare questa memoria il più ecientemente possibile. Un programmatore che riesce a usufruire di questo strumento, riesce a creare del codice con performance assolutamente incredibili. 1.4 Conclusioni sull'architettura delle GPU Ci sono 10 TPC, suddivisi in 3 SM con all'interno 8 SP. In un chip GT200 esistono quindi: 10 T P C ∗ 3 SM ∗ 8 SP = 240 Streaming P rocessor CAPITOLO 1. ARCHITETTURA DELLE GPU 27 Un numero così elevato di core ed un'unità hardware interamente dedicata 36 vogliono suggerire una sola cosa: le schede video sono alla gestione dei thread state create per macinare calcoli in parallelo, in quantità normalmente non concepibili con task multithreading svolti su CPU ordinarie. Le GPU sono state create per gestire milioni di operazioni in parallelo: amano il context switching, hanno dell'hardware dedicato per compiere queste operazioni e occorre creare dei linguaggi che permettono di usare e piegare così tanta potenza all'esecuzione di calcoli complessi, che ormai permeano ogni ambito scientico e non. Da queste esigenze è nato il calcolo GPGPU. Un nuovo modo di concepire i problemi quotidiani ed un nuovo modo di risolverli. 36 Si ricorda il Thread Manager in gura 1.5 nella pagina 21. Capitolo 2 OpenCL: Open Computing Language In questo capitolo ci concentreremo sullo studio, sotto ogni punto di vista, del linguaggio OpenCL[5]. Questo capitolo aronterà, usando la stessa struttura della specica OpenCL rilasciata dalla Khronos[24], gli argomenti necessari a spiegare cosa è, perchè viene usato, come è stato pensato e come funziona il linguaggio OpenCL. Questo capitolo trae ispirazione dalla specica del linguaggio, fonte [24], ed dalla fonte [5]. Alcuni cenni derivano dai tutorial di David 1 Gohara[13] , presi da http://www.macresearch.org/opencl[14]. Questo capitolo è suddiviso in tre parti principali: un'introduzione ad OpenCL, come e perchè è stato creato, la sua storia e le motivazioni per cui è stato scelto per questa tesi. La seconda parte parlerà dell'architettura logica di OpenCL, i suoi modi di intedere i device, i dati ed i programmi. La terza ed ultima parte mostrerà un esempio classico per far capire i meccanismi basilari di OpenCL. L'esempio, commentato riga per riga, guida il lettore in un programma completo dalla fase di inizializzazione alla fase di chiusura e pulizia dell'ambiente. 2.1 OpenCL - Open Computing Language Questa sezione ha il compito di introdurre il linguaggio OpenCL e spiegare le scelte basilari per cui è stato costruito e per cui è stato scelto per programmare il framework in oggetto di questa tesi. 2.1.1 Storia Il 15 Febbraio 2007 la prima beta di CUDA veniva rilasciata per i sistemi Microsoft Windows e Linux[3]; sempre nel 2007, a Dicembre, ATI/AMD rilascia il 1A cui viene dedicato un ringraziamento speciale in questa tesi. 28 CAPITOLO 2. SDK OPENCL: OPEN COMPUTING LANGUAGE 29 2 di ATI Stream[2]. Apple incomincia a sviluppare OpenCL e, dopo una collaborazione con 3 le maggiori industrie nel campo , propone la realizzazione della specica alla Khronos Group. Il 16 Giugno 2008, viene formato il Khronos Compute Working Group con la partecipazione dei maggiori rappresentanti nel campo delle CPU, GPU, processori embedded e compagnie software. Dopo cinque mesi di lavoro questo gruppo crea la specica 1.0 di OpenCL, il 18 Novembre 2008. Il giorno 8 Dicembre 2008 viene rilasciata al pubblico la specica. 2.1.2 Cos'è OpenCL è un framework per scrivere programmi che possano essere eseguiti su un insieme eterogeneo di piattaforme fatte di CPUs, GPUs e altri processori. OpenCL è un linguaggio, basato sul C99, per scrivere funzioni speciali (kernel) da eseguire sui device OpenCL e per creare un ambiente in cui queste funzioni possano lavorare. OpenCL permette di scrivere istruzioni con un altissimo tasso di parallelismo di dati e di programmi. Gestito dall'azienda no-prot Khronos, OpenCL è l'analogo delle speciche Open come OpenGL e OpenAL. 2.1.3 Perchè OpenCL nasce dall'esigenza di creare un linguaggio totalmente indipendente dalla piattaforma per risolvere problemi paralleli e permettere al programmatore di usare tutti i device all'interno di un computer senza limitazioni e senza dover creare del codice dedicato per ogni singolo device. La portabilità del codice è uno degli obiettivi primari di OpenCL. Ogni implementazione deve rispettare i vincoli della specica OpenCL[24] e, se soddisfa i requisiti, il codice può essere eseguito su qualsiasi computer che abbia installato un ambiente, software e hardware, compatibile con OpenCL. Un'altro vantaggio, ed uno degli obiettivi principali che OpenCL si impone, è quello di creare un unico codice indipendente non solo dalla macchina, ma anche dai device sottostanti. Questo vuol dire che su una macchina lo stesso codice OpenCL può essere eseguito, senza MAI essere modicato, su uno o più device all'interno del computer ed il codice del kernel, precedentemente compilato o compilato in tempo reale, rimane lo stesso codice scritto una sola volta dal programmatore senza che questo debba ricompilare il programma ogni volta che viene cambiato un device o ne viene scelto un altro in tempo reale. Ovviamente il programma verrà eseguito con prestazioni dipendenti dal (o 4 dai) device scelti . 2 Source Development Kit 3 AMD, Intel, Ati e NVIDIA. 4 C'è un video molto interessante che analizza questo aspetto realizzato da ATI/AMD: http://www.youtube.com/watch?v=7PAiCinmP9Y CAPITOLO 2. 30 OPENCL: OPEN COMPUTING LANGUAGE Questa caratteristica rende OpenCL uno strumento versatile, potente e agile; rimane al programmatore creare applicativi che sfruttano queste carattestiche al meglio delle loro potenzialità. 2.1.4 Come Prima di entrare nel dettaglio di OpenCL, della sua architettura e delle sue caratteristiche, introduco brevemente la logica di una computazione OpenCL. Un programma OpenCL dovrà, in ordine: cercare dei device OpenCL validi; costruire un contesto, un ambiente che racchiude l'insieme di device, code (queue), dati e programmi OpenCL; creare delle code di esecuzione; compilare e creare dei programmi OpenCL, compilando e creando in tempo reale uno o più programmi composti da uno o più kernel; creare i dati necessari alla computazione OpenCL; eseguire i kernel e leggere i risultati. Nella terza parte verrà spiegato ogni passo sopra descritto e la logica che questo comporta. 2.1.5 Perchè OpenCL per Image Processing Esistono due valide ragioni per cui è stato scelto OpenCL come linguaggio per programmare questo framework e utilizzarlo per creare algoritmi di image processing: OpenCL è un linguaggio che permette di risolvere problemi con alto tasso di parallelismo e la maggior parte degli algoritmi di image processing sono 5 strutturalmente compatibili con un usso di esecuzione parallelo ; OpenCL, come dice la parola stessa, è Open e completamente platform indipendent. Questo porta a scegliere OpenCL come linguaggio per realizzare un framework di elaborazione delle immagini in quanto verrà scritto codice in grado di girare su ogni macchina che abbia un ambiente OpenCL funzionante; infatti, al giorno d'oggi, sono stati creati ambienti pratica- 6 mente per ogni sistema operativo . Inoltre, la losoa Open di OpenCL 5 Come, perchè e quali algoritmi di image processing fanno parte di questa famiglia non verrà arontato qui in quanto non oggetto di questo capitolo. Per ogni riferimento sull'argomento si consiglia il lavoro del collega [30] 6 Insieme alla tesi vengono consegnati gli eseguibili per impostare un ambiente OpenCL su sistema operativo Windows e Linux (Debian based e Open SUSE). Per quanto riguarda i sistemi Mac esiste una implementazione di OpenCL già installata nel sistema operativo Snow Leopard. CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 31 si sposa perfettamente con le librerie in cui questo framework dovrà essere 7 integrato . 2.2 The OpenCL Specication Questa sezione si ispira fortemente al documento The OpenCL Specication 1.0 [24]. Obiettivo di questa parte consiste nel dare un'idea generale dell'architettura logica con cui è stato costruito questo linguaggio. Quindi, dall'inizio alla ne, verranno ripercorsi gli stessi passi in cui è divisa la specica di OpenCL, citando anche la fonte [25] e integrando con [13]. Dalla specica viene presa solo la prima parte. Verranno discussi solo gli argomenti necessari a capire un programma OpenCL in via generica, dando un ampio taglio alle informazioni non necessarie e sottolineando le conoscenze necessarie a capire l'esempio dopo mostrato e le scelte implementative del framework realizzato. 2.2.1 The OpenCL Architecture OpenCL è un framework per la programmazione parallela che include un linguaggio, API, librerie ed un sistema in tempo reale per supportare lo sviluppo del software. Per descrivere l'idea principale di OpenCL, useremo una gerarchia di modelli: modello di piattaforma; modello di esecuzione; modello di memoria; modello di programmazione. 2.2.1.1 Modello di piattaforma La piattaforma di OpenCL (come in gura 2.1 nella pagina successiva) consiste in un host connesso ad uno o più OpenCL device (compute device). Un device OpenCL è diviso in uno o più Compute Units, unità di computazione, che sono a loro volta divise in più Processing Elements, elementi di processo. La computazione vera e propria avviene all'interno dei Processing Elements. Le applicazioni OpenCL mandano comandi dall'host ai Processing Elements all'interno dei device. Ogni Processing Element esegue un singolo usso di instruzioni come unità SIMD 8 o come unità SPMD9 . Se si vuole fare un'analogia fra il modello di piattaforma e un computer, l'host può essere un programma che esegue su un computer e i compute device 7 La losoa della libreria IVLLIB e come questo framework verrà integrato, sono oggetto del capitolo 3. 8 Single 9 Single Instruction, Multiple Data[26]. Process, Multiple Data[27]. CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE Figura 2.1: Architettura di OpenCL 32 CAPITOLO 2. 33 OPENCL: OPEN COMPUTING LANGUAGE possono essere il processore e/o la scheda video (ad esempio, per ricollegarci al capitolo 1, una GTX285). Nella GeForce GTX285, gli Streaming Proces- sor possono essere visti come le compute units di OpenCL ed all'interno degli streaming processor si possono identicare i thread che sono l'ultima sottodivisione dei core di una scheda NVIDIA; i thread sono i processing elements di OpenCL. In un Intel Core2 Duo, i due core sono le due compute units e in ogni core 10 . ci sono i processing elements 2.2.1.2 Modello di esecuzione L'esecuzione in OpenCL avviene in due parti: i kernels che eseguono codice sui device OpenCL e il programma host che esegue sull'host. Per quanto riguarda i kernel, quando uno di questi è eseguito su un device OpenCL deve essere denito uno spazio di lavoro (o spazio di indici), ovvero una dimensione che indichi come dividere le istanze del problema. L'istanza di un kernel viene chiamata work-item e rappresenza un'istanza del generico kernel in un punto specico del problema; questo work-item è identicato da un indice globale (e in alcuni casi anche un indice locale) che denisce di che parte del problema fa parte. Quando viene lanciata l'esecuzione di un kernel, vengono create n istanze del kernel (con n (spazio degli indici)) ognuna per ogni indice presente nello spazio di lavoro degli indici. Questo approcio viene spiegato meglio con un esempio: astraendo per un attimo da un possibile ambiente OpenCL, supponiamo di avere un immagine e di voler fare un'operazione per ogni pixel e supponiamo che questa operazione sia indipendente dal valore degli altri pixel. Rappresentiamo l'immagine a livelli di grigio e quindi come una matrice in cui ogni pixel rappresenta il livello di grigio in quella posizione. Se l'immagine è, ad esempio, 1024 ∗ 768 = 786432 pixels allora questo problema verrà denito, in OpenCL, come un problema a due dimensioni, in cui la prima dimensione sarà uguale a 1024 e la seconda sarà uguale a 768. 786432 work-items che eseguono Questo vuol dire che in esecuzione ci saranno 11 ed ognuno compierà l'operazione su pixel che rappresenta. in parallelo Tornando all'architettura di OpenCL, i work-items sono raggruppati in workgroups. I work-items avranno quindi un identicativo globale univoco ed un identicativo locale al work-group univoco. In OpenCL 1.0, lo spazio di indici supportato è chiamato NDRange. Un NDRange è uno spazio di indici N-Dimensionale. OpenCL 1.0 supporta spazi di 1, 2 o 3 dimensioni. Contesto e coda comandi L'host denisce un contesto per l'esecuzione dei kernel. Il contesto include le seguenti risorse: 10 Questi riferimenti non sono precisi e non sono presi da nessuna fonte. Il loro scopo è solo quello di rendere l'idea fra il modello di piattaforma di OpenCL e un'architettura logica di un processore e/o di una scheda video. 11 In relazione alla potenza e capacità del device OpenCL scelto. CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 34 Device: la collezione di device utilizzabili dall'host; Kernel: le funzioni OpenCL che vengono eseguiti su dispositivi OpenCL; Oggetti programma: il codice e/o gli eseguibili che implementano i kernel; Oggetti memoria: un insieme di oggetti memoria visibile all'host ed ai device OpenCL; contengono i dati che saranno trasferiti ed usati sui device OpenCL; L'host deve inotre creare una o più code di comando (command queue) per coordinare l'esecuzione dei kernel sui device. Categorie di kernel Esistono due tipi di kernel: i kernel OpenCL e i kernel nativi. I kernel OpenCL sono i kernel scritti nel linguaggio OpenCL C e compilati con un compilatore OpenCL. I kernel nativi sono un altro tipo di kernel, opzionali rispetto ai primi e dipendenti dalla piattaforma 12 . 2.2.1.3 Modello di memoria Diversamente dalla programmazione ordinaria, in OpenCL, esiste una gerarchia di memoria che il programmatore può (e deve) controllare. I work-items che stanno eseguendo un kernel hanno accesso a quattro diverse zone di memoria: Memoria globale: questa memoria permette accessi in lettura e scrittura a tutti i work-items in tutti i work-groups; Memoria costante: questa memoria è una regione che rimane costante durante l'esecuzione del kernel; viene inizializzata dall'host. Memoria locale: la memoria locale è una memoria condivisa fra i workitems in un work-group; viene usata come memoria di sincronizzazione all'interno di un work-group; Memoria privata: la memoria privata è lo spazio di memoria riservato ad ogni work-item. Ogni work-item ha la sua memoria privata e questa non può essere condivisa con nessuno e nemmeno l'host può inizializzarla. Per leggere e scrivere dalla e nella memoria dei device OpenCL, l'host deve usare apposite funzioni presenti nel framework OpenCL. Per quanto riguarda la consistenza della memoria, OpenCL non dispone di nessun meccanismo implicito per gestire gli accessi alla e dalla memoria. Non è quindi garantita la consistenza dello stato di una variabile condivisa fra più work-items. OpenCL mette a disposizione, tramite funzioni built-in, meccanismi espliciti di sincronizzazione fra work-items in un work-group. 12 In questa tesi non verranno trattati i kernel di questo tipo. CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 35 Figura 2.2: Architettura di memoria di un device OpenCL[28] 2.2.1.4 Modello di programmazione Il modello di programmazione di OpenCL supporta il parallelismo di dati e il parallelismo di processi. La programmazione riferita al parallelismo di dati consiste in una sequenza di istruzioni applicate ad elementi multipli di oggetti in memoria. Lo spazio di indici denisce una esecuzione di un work-item e come i dati vengono mappati nel work-item. Per parallelismo di processi, OpenCL denisce un modello in cui una singola istanza di un kernel viene eseguita indipendetemente su ogni spazio dell'indice. Da questo modello di programmazione nasce l'esigenza di un meccanismo di sincronizzazione. OpenCL denisce due domini di sincronizzazione: sincronizzazione di work-items all'interno di un work-group; sincronizzazione di comandi immessi in una o più code all'interno di un contesto. 2.2.2 Il Framework OpenCL Il framework OpenCL permette alle applicazioni di usare un host e uno o più device OpenCL come un unico eterogeneo sistema di computer parallelo. framework è composto da: Il CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 36 OpenCL platform layer: il layer di piattaforma permette di scoprire devices e le loro caratteristiche e inizializzare contesti; OpenCL runtime: il runtime permette al programma dell'host di manipolare i contesti una volta creati; OpenCL compiler: il compilatore OpenCL crea programmi eseguibili che contengono kernels OpenCL. Il linguaggio di programmazione OpenCL C implementato dal compilatore supporta un sottoinsieme dell'ISO C99 con l'estensione per il parallelismo. 2.2.3 Il livello piattaforma di OpenCL Questa parte riguarda principalmente le funzioni necessarie ad inizializzare l'ambiente di lavoro di OpenCL. Vengono riassunti i passi fondamentali presi dalla specica del linguaggio OpenCL[24]. Per quanto riguarda i comandi, occorre specicare che molti dei comandi di OpenCL ritornano il controllo al programma host appena possibile e non c'è nessuna garanzia che quando una funzione venga chiamata e ritorni il controllo al programma, questa abbia nito la sua esecuzione. Il disegno 2.3 nella pagina seguente chiarisce questo concetto. Inoltre, praticamente tutte le funzioni di OpenCL ritornano un codice di errore che dice se l'esecuzione della funzione è andato a buon ne o se c'è stato qualche errore; in caso di errore il codice è diverso in base all'errore riscontrato nell'esecuzione della funzione. 2.2.3.1 Richieste sulla piattaforma Prima di tutto occorre scegliere ed inizializzare la piattaforma su cui intendiamo lavorare. In caso ce ne sia più di una, la funzione clGetPlatformIDs ritorna una lista con tutte le piattaforme disponibili. Se volessimo conoscere più informazioni su una piattaforma, possiamo interrogare il sistema con una query con la funzione clGetPlatformInfo 13 . 2.2.3.2 OpenCL Device Dopo aver scelto la piattaforma, procediamo ad una delle fasi più importanti di un programma OpenCL: la scelta dei device. La funzione clGetDeviceIDs ritorna la lista dei device OpenCL disponibili nel computer. OpenCL, inoltre, permette di formulare richieste anche per device specici (solo CPU, solo GPU, ...). Anche in questo caso OpenCL mette a disposizione una funzione, clGet- DeviceInfo, per interrogare un device sulle sue caratteristiche. Questa funzione è molto utile se si vogliono conoscere tutti i dettagli del device su cui si sta lavorando: dalle estensioni, al supporto immagini, dal massimo numero di work-items al massimo numero di work-group. 13 Per eseguire in un ambiente con SDK ATI Stream è necessario scegliere una piattaforma per la crezione del contesto OpenCL. CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 37 Figura 2.3: Esempio di usso di un comando OpenCL rispetto all'esecuzione di una chiamata a funzione di un qualsiasi linguaggio procedurale. CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 38 2.2.3.3 Il Contesto L'ultima parte fontamentale del livello piattaforma consiste nella creazione di uno o più contesti. Un constesto in OpenCL rappresenta un ambiente, un insieme di dati, programmi (intesi come insieme di kernel) e un insieme di device che lavorano su una piattaforma. Con la funzione clCreateContext viene creato un contesto valido su una piattaforma che andrà a lavorare su un insieme (una lista) di device. OpenCL permette anche di creare un contesto specico per un solo device con la funzione clCreateContextFromType. Anche in questo caso OpenCL permette di avere informazioni sul contesto con la solita procedura, svolta questa volta dalla funzione clGetContextInfo. 2.2.4 The OpenCL Runtime Questa è la sezione principale per capire come funziona in via generale un programma OpenCL. Verranno discusse tutte le chiamate API che gestiscono le code di comandi, gli oggetti memoria, come scrivere e leggere su questi, i kernel e le modalità di esecuzione. 2.2.4.1 Code di comando Gli oggetti OpenCL come oggetti memoria, programmi e kernel sono creati all'interno di un contesto. Le operazioni su questi oggetti sono eettuate at- traverso l'uso di code di comando (command queue). Le code di comando rappresentano oggetti in cui il programmatore impila una serie di comandi e questa tenta di risolverli il prima possibile. Avere code multiple permette al- l'applicazione di incodare più comandi in parallelo anche se OpenCL non ha un meccanismo specico per la gestione della concorrenza fra code. La funzione clCreateCommandQueue crea un oggetto coda su uno specico device. Occorre notare che su un device possono essere aperte più code ma una coda può e deve essere aperta su uno ed un solo device OpenCL. Il solito comando clCommandQueueInfo permette di interrogare la coda sulle sue proprietà. 2.2.4.2 Oggetti di memoria In OpenCL esistono due tipi di oggetti di memoria: gli oggetti buer e gli oggetti immagine. Un buer è usato per memorizzare dati monodimensionali mentre l'immagine è usata per creare texture, frame buer o immagini bi- o tri- dimensionali. Gli elementi di un buer possono essere i tipi base, i tipi di OpenCL (come vectors, ad esempio) o struct C. Le immagini rappresentano texture o frame-buer. Le principali dierenze fra buer e immagine sono: gli elementi in un buer sono memorizzati sequenzialmente e si accede in scrittura e/o lettura tramite puntatori. Le immagini sono immagazzinate CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 39 in un formato nascosto al programmatore che deve usare funzioni built-in di OpenCL C per accedere in scrittura e/o lettura di un'immagine. Per un oggetto buer, i dati sono organizzati nello stesso modo in cui vengono visti nel kernel mentre nel caso degli oggetti immagine il formato con cui l'oggetto viene scritto potrebbe non essere lo stesso di quello usato per leggere l'oggetto nel kernel. Oggetti Buer La funzione clCreateBuffer crea un oggetto buer di tipo cl_mem. Per scrivere, leggere e copiare si usano i comandi clEnqueueReadBuffer, clEn- queueWriteBuffer e clEnqueueCopyBuffer. In questo caso, clEn- queueReadBuffer serve per leggere un buer dalla memoria del device a quella dell'host mentre clEnqueueWriteBuffer scrive uno stream di dati dalla memoria dell'host a quella del device. All'interno dei kernel, l'accesso ai buer avviene tramite puntatori, con la stessa sintassi del linguaggio C. Per conoscere le proprietà degli oggetti memoria si usa il comando clGetMemObjectInfo. Oggetti Immagine La funzione clCreateImage2D e clCreateImage3D creano, rispettivamente, immagini bidimensionali e tridimensionali. Il processo di creazione di un'immagine è più laborioso di quello del buer in quanto bisogna decidere di quanti canali è composta l'immagine e con quanti bit rappresento ogni canale. I dettagli non verranno elencati e si lascia al lettore la fonte [24] per maggiori particolari. Dato che il trattamento di una immagine, con tutte le conseguenze del caso, non è banale, OpenCL mette a disposizione la funzione clGetSupportedIm- ageFormat per controllare i formati di immagine supportati dal device su cui si sta lavorando 14 . Per scrivere, leggere e copiare immagini si usano i comandi clEnqueueRead- Image, clEnqueueWriteImage e clEnqueueCopyImage. L'uso di queste funzioni è analogo a quello applicato ai buer. OpenCL fornisce anche la possibilità di copiare un'immagine in un buer con la funzione clEnqueueCopyIm- ageToBuffer. Per conoscerne le proprietà si usa il comando clGetImageInfo. Le immagini hanno una caratteristica peculiare: la lettura (e scrittura) nel kernel non avviene semplicemente tramite puntatore ma occorre usare funzioni built-in di OpenCL C per leggere i dati. Per leggere, inoltre, occorre costru- ire un oggetto Sampler ovvero un oggetto che dice quali sono le modalità di campionamento dell'immagine. Questo può essere fatto nel kernel creando un oggetto Sampler costante o nel codice host con il comando clCreateSampler da cui si possono prelevare informazioni con clGetSamplerInfo. 14 La CPU e la GPU, ad esempio, gestiscono un insieme di immagini di tipo diverso. CAPITOLO 2. 40 OPENCL: OPEN COMPUTING LANGUAGE 2.2.4.3 Oggetti programma Un programma in OpenCL è un insieme di kernel 15 e di funzioni ausiliarie rappresentate come stringa di caratteri. Un programma incapsula al suo interno le seguenti informazioni: un contesto, la stringa o il binario del programma, l'ultimo build avvenuto con successo compresi la lista dei device per cui è stato fatto il build e le opzioni di build, e per ultimo il numero di kernel collegati al programma. Un programma viene creato con la funzione clCreateProgramWith- Source se abbiamo il programma sotto forma di stringa mentre, se abbiamo il binario del programma, lo creiamo con clCreateProgramWithBinary. 16 . Per quanto riguarda la compilazione (building) abbiamo clBuildProgram Per avere informazioni sul programma e sulla sua compilazione (warning, errori, ...) usiamo clGetProgramInfo e clGetProgramBuildInfo. 2.2.4.4 Oggetti kernel Un oggetto kernel è una funzione dichiarata in un programma. Un kernel è identicato dal qualicatore __kernel nella signature della funzione. Per creare un kernel da un programma dobbiamo specicarne il nome nella funzione clCreateKernel. Se vogliamo creare tutti i kernel in un programma passiamo un array alla funzione clCreateKernelsInProgram. Quando creiamo un oggetto kernel, prima di usarlo dobbiamo impostare gli argomenti che quel kernel necessita per essere eseguito. Questo compito viene svolto dalla funzione clSetKernelArg, argomento per argomento. Se vogliamo informazioni a proposito delle caratteristiche di un kernel possiamo interrogare il sistema con la funzione clGetKernelInfo, mentre se vogliamo sapere i particolari di un kernel per uno specico device usiamo clGetK- ernelWorkGroupInfo. 2.2.4.5 Esecuzione dei kernels L'esecuzione dei kernel viene svolta dalla funzione clEnqueueNDRangeKer- nel. Conviene spendere qualche particolare in più su questa funzione in quanto l'esecuzione è un momento delicato in una computazione OpenCL. Come molte funzioni OpenCL, anche questa funzione ritorna subito il controllo al usso del programma dell'host appena chiamata. Questo implica che se abbiamo due kernel dove il secondo usa i dati del primo e nel programma questi vengono lanciati uno di seguito all'altro, i dati saranno molto probabilmente corrotti. Questo particolare è molto importante nell'esecuzione di code multiple e quando abbiamo bisogno di leggere i dati subito dopo l'esecuzione del kernel. Quando lanciamo l'esecuzione di un kernel, dobbiamo specicare, tramite l'oggetto stesso, che kernel andiamo ad eseguire e la coda in cui impiliamo questa operazione. Anche la scelta della coda è una scelta non univoca e non banale 15 Speciali funzioni 16 Per le opzioni di dichiarate con l'identicatore __kernel. building guardare la fonte [24]. CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 41 in quanto apre molte scelte prograttuali in fase di costruzione di un programma OpenCL. Un'altra funzione particolare è clEnqueueTask che permette l'esecuzione di un singolo kernel alla volta. Per quanto riguarda i kernel nativi, la funzione clEnqueueNativeKer- nel permette l'esecuzione di un kernel scritto in C/C++ non compilato con il compilatore OpenCL. 2.2.4.6 Oggetti evento Gli oggetti evento sono oggetti che servono ad identicare le azioni OpenCL che si riferiscono a esecuzione di comandi o lettura, scrittura e copia su e da oggetti di memoria. Gli eventi servono quindi a tracciare gli stati dell'esecuzione di un comando. Quando mettiamo nella coda una esecuzione, la sua chiamata ritorna un oggetto evento che identica quel comando. La funzione clWaitForEvents permette di bloccare il thread host nchè uno o più eventi non si compiono. Si può interrogare il sistema a proposito delle informazioni di un evento con la funzione clGetEventInfo. Dato che le code possono essere eseguite in ordine o fuori ordine, le funzioni di OpenCL per sincronizzare gli eventi sono molto importanti. Sono quindi di rilievo anche le funzioni clEnqueueMarker, clEnqueueWaitForEvents e clEnqueueBarrier che metteno nella coda, rispettivamente, un marker che ritorna un evento che può essere usato per impostare una wait su questo marker, una lista di uno o più eventi che devono accadere prima che la coda continui ad eseguire comandi e una barriera che blocca l'esecuzione di una coda anchè tutti i comandi impilati prima di questo comando siano eseguiti prima che la coda continui con i rimanenti. Gli oggetti evento si possono anche usare per catturare le informazioni del prolo che misura l'esecuzione del tempo di un comando con la funzione clGetEventProfileInfo. 2.2.4.7 Flush and nish Inne occorre analizzare due comandi molto importanti nell'esecuzione di kernels in una coda: clFlush e clFinish. Il primo comando forza tutti i comandi impilati in una coda ad essere eseguiti sul device a cui è associata la coda ma non da nessuna garanzia che quando ritorni il controllo al programma i comandi siano niti. Per ovviare a questo, la funzione clFinish ha il compito di bloccare il usso di programma dell'host e di ritornare il controllo se e solo se i comandi impilati nella coda specicata hanno completamente terminato la loro esecuzione. funzione clFinish è un punto di blocco per il programma host. La CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 42 2.2.5 Conclusioni Questa sezione è servita per mostrare un'idea generale sulla specica del linguaggio OpenCL. Non ha assolutamente lo scopo di sostituire la specica e non andrebbe presa come reference. Per ogni particolare bisogna riferirsi alla fonte [24]. Inoltre non è stata arontata tutta la specica OpenCL ma solo una piccola parte di questa, necessaria a capire l'esempio che seguirà e necessaria a capire le scelte implementative del framework senza dover per forza padroneggiare e conoscere tutte le chiamate e le caratteristiche delle funzioni OpenCL. OpenCL è un linguaggio molto potente e fornisce i mezzi al programmatore per sfruttare tutta la forza all'interno di una macchina. Occorre ricordare ed appuntarsi una nota sul fatto che non tutti i problemi possono (e devono) essere risolti in OpenCL. Una delle fasi più complicate consiste infatti nel capire come parallelizzare un problema e se questo problema è eettivamente parallelizzabile. In caso si forzi questo meccanismo, OpenCL si trasforma da un linguaggio utile ad uno strumento male usato e questo porta, come la maggior parte delle volte che si agisce secondo questo comportamento, ad un programma ineciente, di dicile comprensione e, molto probabilmente, errato. 2.3 Un esempio di OpenCL In questa sezione verrà mostrato un esempio di codice OpenCL, dalla creazione del programma host e del kernel alla vera e propria esecuzione del codice. Il programma in esempio somma due array, cella per cella, e scrive il risultato in un altro array. E' stato scelto questo programma perchè è il classico Hel- lo World del calcolo GPGPU e perchè è semplice e permette di capire come funzionano i meccanismi basilari. Farò riferimento ai tutorial e/o al codice di[13, 25]. Un programma semplice come questo permette di identicare una struttura generale di un codice OpenCL dividendo il programma in cinque grandi parti: inizializzazione delle risorse, compilazione del programma, creazione degli oggetti in memoria, esecuzione dei kernel e lettura dei dati e release delle risorse. La progettazione del kernel, obiettivo primario del programmatore, viene discussa come primo argomento. Il codice in queste pagine ha il solo scopo di dare una visione generale di come può essere una computazione OpenCL dall'inizializzazione dell'ambiente alla ne della computazione. 2.3.1 Il kernel Il design di un kernel dovrebbe essere la prima parte della realizzazione di un programma OpenCL. Obiettivo del programmatore è quello di ottenere il massimo dell'ecienza dalla macchina su cui sta lavorando o creare codice che possa funzionare il più ecientemente possibile su un gruppo eterogeneo di macchine. CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 43 Davanti ad un problema il programmatore deve chiedersi: Posso parallelizzare il problema? La sua natura si presta a dividere l'esecuzioni in più parti indipendenti? Se si, quali? Quali strumenti mi fornisce OpenCL per ottimizzare la gestione della memoria? Spesso si possono ottenere enormi dierenze con scelte implementative leggermente diverse, assolutamente indierenti per quanto riguarda il sorgente, ma totalmente diverse per quanto riguarda il codice generato per il device. In questo esempio, questo kernel permette di sommare due array, cella per cella, e di mettere il risultato nel terzo array. Si può subito notare il parallelismo; il codice del kernel infatti è molto simile ad un codice C che compie lo stesso compito, a meno di un ciclo. Vediamo perchè. Il codice C che svolge questo compito è costituito da un ciclo che per ogni cella dell'array, somma la i-esima cella del primo array con la i-esima cella del secondo array e mette il risultato nella i-esima cella del terzo array(∀ i [0, count − 1]): //Codice C per sommare due array cella per cella void add( float *primo, float *secondo, float *risultato, int length){ int i; for(i = 0; i < length; ++i){ risultato[i] = primo[i] + secondo[i]; } } mentre il codice del kernel OpenCL è: //Kernel OpenCL per sommare due array cella per cella __kernel void add( __global float *primo, __global float *secondo, __global float *risultato){ int i = get_global_id(0); risultato[i] = primo[i] + secondo[i]; } Come scritto prima, la grande dierenza è l'assenza, nel codice OpenCL, del ciclo. Come si agisce in OpenCL? CAPITOLO 2. 44 OPENCL: OPEN COMPUTING LANGUAGE In OpenCL, quando deniamo uno spazio di indici, la chiamata di esecuzione di un kernel esegue un'istanza di kernel, work-item, su ogni indice presente nello spazio degli indici. Mentre il codice C sopra scritto verrà eseguito solo una volta (in cui ci sarà un ciclo denito da i=0 alla lunghezza degli array length − 1), un kernel OpenCL viene eseguito tante volte quanti sono gli indici appartenenti allo spazio degli indici. La funzione get_global_id serve infatti per interrogare il sistema e sapere quale kernel sono nello spazio di lavoro dei kernel . Questa piccola, ed allo stesso tempo enorme, dierenza sta alla base di ogni programma OpenCL. E' quindi compito del programmatore usare questo strumento come meglio può per risolvere i problemi che gli vengono posti volta per volta. 2.3.2 Inizializzazione delle risorse Supponendo di avere già gli array allocati nella memoria dell'host, scriviamo solo la funzione che gestisce l'ambiente OpenCL e le sue chiamate. In ingresso prenderà quindi tre puntatori ad array di, ad esempio, la lunghezza degli array f loat 17 . La funzione ritorna un valore e un n rappresentante int , CL− SU CCESS , se la funzione ha svolto il calcolo correttamente. //include dei file headers di OpenCL #include <CL/cl.h> //stringa del kernel add const char *stringaProgramma = "\n" \ "__kernel void add( "__global float *primo, "__global float *secondo, "__global float *risultato) "{ " int i = get_global_id(0); " risultato[i] = primo[i] + secondo[i]; "} "\n"; /** \n" \n" \n" \n" \n" \n" \n" \n" \ \ \ \ \ \ \ \ Funzione che imposta l'ambiente OpenCL ed esegue la computazione. Codice eseguito su cpu: GenuineIntel, Intel(R) Pentium(R) D CPU 2.66GHz */ int runCL( float *primoArray, float *secondoArray, 17 Per comodità supponiamo gli array di lunghezza uguale. Lo scopo di questa parte è quella di dare un'idea dei meccanismi di OpenCL di base senza guardare a controlli o correttezza formale del codice dell'host. CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE float *risultato, int lunghezzaArray) { //oggetto programma cl_program programma; //oggetto kernel cl_kernel kernel; //coda dei comandi cl_command_queue coda_comandi; //contesto OpenCL cl_context contesto; //device cpu cl_device_id deviceCpu = NULL; //memorizza gli errori che ritornano //le chiamate OpenCL cl_int errore = 0; //grandezza del buffer size_t grandezza_buffer; //piattaforma di lavoro OpenCL cl_platform_id* piattaforma = NULL; //oggetti di memoria corrispondenti ai //parametri di ingresso cl_mem primoArrayMem, secondoArrayMem, risultatoMem; //numero di piattaforme disponibili cl_uint numeroDiPiattaforme; //stringa per registrare il nome del venditore //del device cl_char nomeVenditore[1024] = {0}; //stringa per registrare il nome del device cl_char nomeDevice[1024] = {0}; //global work size size_t global_work_size = lunghezzaArray; // Vedo quante piattaforme ci sono: //sul sistema testato era solo una errore = clGetPlatformIDs(0, NULL, &numeroDiPiattaforme); piattaforma = new cl_platform_id[numeroDiPiattaforme]; // Registro la piattaforma errore |= clGetPlatformIDs( numeroDiPiattaforme, piattaforma, NULL); 45 CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE assert(errore == CL_SUCCESS); // Scelgo come device la CPU errore = clGetDeviceIDs( *piattaforma, CL_DEVICE_TYPE_CPU, 1, &deviceCpu, NULL); assert(errore == CL_SUCCESS); assert(deviceCpu); // Prendo alcune informazioni sul device CPU errore = clGetDeviceInfo( deviceCpu, CL_DEVICE_VENDOR, sizeof(nomeVenditore), nomeVenditore, NULL); errore |= clGetDeviceInfo( deviceCpu, CL_DEVICE_NAME, sizeof(nomeDevice), nomeDevice, NULL); assert(errore == CL_SUCCESS); printf("Connessione a %s, %s...\n", nomeVenditore, nomeDevice); // Creazione di un contesto su uno specifico device contesto = clCreateContext( 0, 1, &deviceCpu, NULL, NULL, &errore); assert(errore == CL_SUCCESS); // Creazione coda comandi sul contesto appena creato coda_comandi = clCreateCommandQueue( contesto, deviceCpu, 0, NULL); // continua ... 46 CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 47 Questa prima parte è abbastanza chiara e non necessita di spiegazioni. Consiste esclusivamente in una serie di chiamate a funzioni che inizializzano gli oggetti OpenCL. 2.3.3 Compilazione In OpenCL il programma viene creato e poi compilato in due modi: in fase di creazioni si passa una stringa di caratteri, un array di char, oppure il codice binario compilato del programma. In questo caso è stato scelto di passare il programma come stringa di caratteri. Viene poi creato il kernel dal programma compilato selezionandolo per nome (il nome della funzione kernel). Il codice si autocommenta: // continua da sopra ... // Creo il programma da una stringa programma = clCreateProgramWithSource( contesto, 1, (const char**)&stringaProgramma, NULL, &errore); assert(errore == CL_SUCCESS); // Compilazione programma errore = clBuildProgram(programma, 0, NULL, NULL, NULL, NULL); assert(errore == CL_SUCCESS); // Creo l'oggetto kernel da usare per la computazione kernel = clCreateKernel(programma, "add", &errore); //continua ... 2.3.4 Creazione degli oggetti memoria Dopo aver creato i kernel, un programma ha la necessità di creare oggetti che siano compatibili con i device OpenCL. Vengono creati i tre array ed i primi due vengono riempiti con i dati passati alla funzione runCL: // continua da sopra ... // Alloco memoria necessaria per i // buffer della memoria del device OpenCL grandezza_buffer = sizeof(float) * lunghezzaArray; // Primo oggetto memoria primoArrayMem = clCreateBuffer( CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE contesto, CL_MEM_READ_ONLY, grandezza_buffer, NULL, NULL); // Scrivo dentro il primo oggetto memoria i dati // del primo array errore = clEnqueueWriteBuffer( coda_comandi, primoArrayMem, CL_TRUE, 0, grandezza_buffer, (void*)primoArray, 0, NULL, NULL); // Secondo oggetto memoria secondoArrayMem = clCreateBuffer( contesto, CL_MEM_READ_ONLY, grandezza_buffer, NULL, NULL); // Scrivo dentro il secondo oggetto memoria // i dati del secondo array errore |= clEnqueueWriteBuffer( coda_comandi, secondoArrayMem, CL_TRUE, 0, grandezza_buffer, (void*)secondoArray, 0, NULL, NULL); assert(errore == CL_SUCCESS); // Oggetto memoria risultato risultatoMem = clCreateBuffer( contesto, CL_MEM_READ_WRITE, grandezza_buffer, NULL, NULL); // Mi assicuro che tutto venga //scritto prima di continuare clFinish(coda_comandi); // continua ... 48 CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 49 2.3.5 Esecuzione dei kernel In questa parte vengono impostati gli argomenti al kernel creato e successivamente viene denito lo spazio di indici, in questo caso 1, e la sua dimensione (in questo caso sizeof (f loat) ∗ lunghezzaArray ); dopo aver completato queste operazioni, viene lanciato il comando di esecuzione. Dato che il comando di esecuzione ritorna subito il controllo, la chiamata clFinish blocca il usso del programma per far si che si continui solo quando l'esecuzione è eettivamente terminata. Il codice è: // continua da sopra ... // Imposto gli argomenti del kernel errore = clSetKernelArg(kernel, 0, sizeof(cl_mem), &primoArrayMem); errore |= clSetKernelArg(kernel, 1, sizeof(cl_mem), &secondoArrayMem); errore |= clSetKernelArg(kernel, 2, sizeof(cl_mem), &risultatoMem); assert(errore == CL_SUCCESS); // Faccio partire l'esecuzione errore = clEnqueueNDRangeKernel( coda_comandi, kernel, 1, NULL, &global_work_size, NULL, 0, NULL, NULL); assert(errore == CL_SUCCESS); // Aspetto finchè l'esecuzione non finisce clFinish(coda_comandi); // continua ... 2.3.6 Lettura dei dati e Release delle risorse L'ultimo passo di un programma OpenCL è quello di leggere i dati elaborati dall'esecuzione del kernel. Inoltre bisogna ricordarsi che la creazione di un ambiente OpenCL occupa memoria e occorre liberare questa memoria. In questa fase quindi si prelevano i dati e si libera la memoria 18 Il 18 : fatto di liberare la memoria è stato fatto solo per scopi dimostrativi. Un programma che necessita di eseguire più computazioni OpenCL non è obbligato a pulire ogni volta la memoria; basta ricordarsi però che c'è della memoria occupata che, quando non deve essere più utilizzata, deve essere liberata. CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 50 // continua da sopra ... // Una volta finita l'esecuzione, leggo // il risultato e lo metto nell'array risultato errore = clEnqueueReadBuffer( coda_comandi, risultatoMem, CL_TRUE, 0, grandezza_buffer, risultato, 0, NULL, NULL); assert(errore == CL_SUCCESS); clFinish(coda_comandi); //libero la memoria da: oggetti memoria, //kernels, programmi, code e contesti clReleaseMemObject(primoArrayMem); clReleaseMemObject(secondoArrayMem); clReleaseMemObject(risultatoMem); //kernel, coda comandi, programma e contesto clReleaseKernel(kernel); clReleaseProgram(programma); clReleaseCommandQueue(coda_comandi); clReleaseContext(contesto); delete[] piattaforma; // Ritorno il valore SUCCESS return CL_SUCCESS; } 2.4 Conclusioni In questo capitolo è stata brevemente analizzata l'architettura di OpenCL: gestione degli oggetti dedicati, programmi OpenCL, lettura, scrittura ed elaborazione dei dati. Bisogna osservare che OpenCL non è nato come linguaggio risolutivo per ogni problema. Occorre ripetere che questo framework è uno strumento che deve essere usato per scopi specici ed assolutamente non general purpose . La fase iniziale, e forse la più delicata, di un programma OpenCL consiste nell'analisi di un problema e nello studio delle fattibilità in OpenCL. Questo framework è stato creato per adempiere a speciche richieste. Se usato in maniera impropria, questo strumento rischia di rendere boriosa ed ineciente un'operazione che sarebbe normalmente eseguibile in un contesto di elaborazione generica di un calcolatore. Ho voluto dedicare una parte specica di questo capitolo alla realizzazione di un semplice programma per permette di capire quali sono i nuclei centrali di un CAPITOLO 2. OPENCL: OPEN COMPUTING LANGUAGE 51 applicativo OpenCL; questo permetterà di capire più avanti le motivazioni delle scelte implementative del framework. Molte di queste scelte, infatti, riguardano la gestione di code, contesti ed oggetti memoria. In riferimento a quanto scritto, concludo con un saggio detto: Se userai sempre il martello, tutti i problemi ti sembreranno chiodi. (Anonimo) Capitolo 3 Progettazione del Framework In questo capitolo, dopo aver dato una nozione generale sull'architettura OpenCL ed aver brevemente mostrato su quale hardware lavora, entreremo nel dettaglio della progettazione di questo framework per l'elaborazione delle immagini. Questo capitolo, come il lavoro da me svolto, si divide principalmente in due grandi parti: la prima parte analizza la progettazione, le speciche e le scelte implementative della realizzazione di un framework in grado di astrarre gran parte del meccanismo che agisce alla base di OpenCL; l'obiettivo è quello di fornire al programmatore uno strumento che possa essere usato in maniera agile, funzionale, facile e comunque eciente per creare un ambiente per compiere calcoli in OpenCL; la seconda parte analizzerà una o più applicazioni di questo framework, non incentrando l'attenzione sullo studio di algoritmi di image processing, interesse marginale in questa tesi, quanto più sulle scelte implementative svolte nel framework atte a realizzare uno strumento specico per la programmazione di algoritmi di elaborazione delle immagini. Occorre far notare come il vincolo di costruire un framework per l'image processing abbia portato alla programmazione di funzioni accessorie senza però porre costrizioni sull'utilizzo generale dello strumento in sè. Verrà quindi mostrato uno dei possibili utilizzi di questo framework sperando che da questo si possano evidenziare le sue potenzialità e versatilità. Ciò su cui si vuole incentrare l'attenzione del lettore sono le problematiche che sono state riscontrare durante la progettazione e la programmazione del framework di OpenCL; inoltre verrà posto l'accento sulle problematiche che questo framework introduce e sui vantaggi che porta al programmatore delle librerie in cui questo framework verrà compreso. 52 CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 53 3.1 Introduzione alla progettazione: framework di OpenCL e librerie IVLLIB La progettazione di questo framework è orientata, come scritto brevemente sopra, alla costruzione di un wrapper di funzioni OpenCL. La scrittura di questo framework è nalizzata all'integrazione, il più trasparente possibile, con le librerie IVLLIB di image processing. laboratorio IVL Queste librerie sono state sviluppate dal 1 del Dipartimento di Informatica dell'Università Milano Bicoc- ca, laboratorio per cui è stato svolto lo stage e per cui è stata scritta questa tesi. Il wrapper OpenCL è stato concepito come strumento di supporto per quegli algoritmi della libreria che necessitano e sono predisposti al parallelismo; questo framework verrà sviluppato nel seguente modo: un'astrazione di molte delle funzioni OpenCL ed una gestione più agile per permettere al programmatore della libreria di aggiungere algoritmi di image processing senza la boriosità e le meccaniche sintattiche di OpenCL; l'implementazione di alcuni algoritmi per la fase di testing del framework e delle sue funzionalità. Questo capitolo è interamente dedicato alle decisioni che hanno portato alla progettazione ed al seguente sviluppo del framework spiegando le scelte prese e la losoa di base del software oggetto della tesi. 3.1.1 Le librerie IVLLIB Nate come raccolta di classi e utilities fra i colleghi del laboratorio IVL, nel tempo questo set di funzioni si è trasformato in una libreria completa, stabile ed eciente per tutti gli usi multimediali. Obiettivo della libreria IVL è quello di fornire al programmatore uno strumento rapido e facile, ma allo stesso potente, per poter compiere elaborazioni su immagini fornendo strutture dati, funzioni e classi in grado di adempiere a svariati compiti. Realizzata interamente in C++, la libreria è stata progettata in maniera interamente templata, ed è pensata sia per un uso specico delle strutture dati, sia per un uso generico. La libreria è concepita per supportare il processing di immagini astraendo quasi completamente da quelle che sono le basi per acquisire le immagini da le, visualizzarle a schermo ed eettuare operazioni sui pixel di queste. Non bisogna sottovalutare le scelte progettuali di queste librerie che permettono ad un programmatore esperto di accedere direttamente ai dati raw ; la semplicità d'uso non ha minimamente intaccato la potenza e l'ecienza che il linguaggio di programmazione ad oggetti C++ metteva a disposizione. La progettazione del framework di OpenCL si è ispirata molto a questa losoa, cercando di fornire uno strumento al programmatore che renda l'utilizzo 1 http://www.ivl.disco.unimib.it/ CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 54 di questo framework estremamente facilitato; questo però non dovrà mai andare ad intaccare l'ecienza computazionale e di utilizzo di OpenCL, importante nella libreria IVL ma ancora più importante quando si parla di calcolo parallelo. Verranno successivamente mostrate le varie scelte progettuali e le speciche che hanno reso possibile la realizzazione di questo framework. 3.2 Requisiti e speciche del framework In questa sezione verranno analizzati i requisiti e le speciche adottate durante la progettazione e la programmazione di questo framework. Come scritto nella sezione precendente, le scelte sono basate sulla losoa di base della libreria IVLLIB in cui questo framework sarà inserito. 3.2.1 Requisiti Verranno qui elencati i requisiti richiesti per la realizzazione del framework. Tutti i requisiti sono la conseguenza dell'analisi svolta con i relatori cercando di analizzare quali potrebbero essere i bisogni del programmatore della libreria, quali meccaniche di OpenCL potrebbero essere astratte e quali caratteristiche il framework dovrebbe avere. I requisiti software del framework di OpenCL sono i seguenti: per quanto riguarda la creazione di un contesto OpenCL, si è deciso di creare un unico contesto che gestisca tutti i device disponibili nel computer; programmato in C++, il framework sarà un'astrazione ad oggetti delle meccaniche funzionali di OpenCL; l'utente avrà a disposizione un punto di accesso per usare le funzioni di OpenCL che consiste in un oggetto statico richiesto tramite un metodo apposito; per eettuare questa funzionalità è stato usato il desing pattern del Singleton; l'utente non deve poter maneggiare i programmi direttamente; l'accesso deve essere controllato e gestito dal framework; l'utente dovrà essere libero di caricare nel framework un numero non determinato di programmi, ovvero l'utente caricherà in tempo reale i vari kernel che desidera usare se questi non sono già presenti; la libreria IVLLIB dovrà fornire dei programmi OpenCL che dovranno essere caricati in fase di inizializzazione se si desiderano usare gli algoritmi della libreria scritti in OpenCL; la gestione delle strutture di OpenCL verrà mascherata da un layer ad oggetti che avrà il compito di controllare l'uso della memoria e della sua gestione; CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 55 l'utente deve comunque avere a disposizione tutte le peculiarità di OpenCL e il livello di astrazione di questo framework deve tentare di non intralciare, o se inevitabile di farlo il meno possibile, l'utilizzo del framework originario di OpenCL; l'utente che usa gli algoritmi della libreria IVLLIB scritti in OpenCL non dovrà avere nessuna conoscenza specica di OpenCL ed il meccanismo sottostante dovrà essere completamente trasparente; anche se possibile, l'accesso del programmatore ai device non dovrebbe avvenire direttamente ma dovrebbe passare per delle politiche di scheduling che assegnano automaticamente i device al programmatore; si richiede che il programmatore della libreria abbia una buona conoscenza del framework sviluppato e di OpenCL. 3.2.2 Speciche Questa sezione è dedicata alla spiegazione dettagliata di come si intende sviluppare il framework e verrà descritto come risolvere le problematiche che possono esserci nella progettazione seguendo i requisiti sopra citati. Inoltre verrà posto l'accento riguardo agli usi possibili del framework da parte del programmatore della libreria. Caratteristica principale di questo wrapper OpenCL è l'astrazione del framework già esistente per permettere a chiunque abbia una buona conoscenza di OpenCL di programmare in maniera agile ed ordinata, senza perdersi in tutte le funzioni che OpenCL ore, molte delle quali spesso vengono lasciate con i valori di default. Obiettivo primario è quello di orire un'interfaccia più chiara che permetta di scrivere un codice più facile da manipolare e da gestire tenendo conto i due obiettivi principali che ci si pone nella progettazione e programmazione di questo framework: l'interfaccia dovrà comunque consentire un accesso libero e diretto a tutte le risorse OpenCL senza ostacolare un possile utilizzo di una parte del framework nativo originale e parte di quello progettato per questa libreria; dato che ogni operazione verrà svolta in OpenCL per motivi di ecienza computazionale, questo framework dovrà cercare di aggiungere il minimo overhead indispensabile per quanto riguarda l'elaborazione dei comandi. A conoscenza di questi due punti cardine, si può incominciare a denire le caratteristiche e le sfaccettature che la progettazione di questo framework ha mostrato durante tutta la fase di costruzione del progetto, no alla programmazione delle funzioni di prova della libreria. CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 56 3.2.2.1 Astrazione delle meccaniche di OpenCL Questo punto deve essere analizzato con attenzione durante la fase di progettazione in quanto il livello di astrazione deciderà inevitabilmente la potenza e l'ecienza del codice sviluppato. Tutta la parte di inizializzazione del contesto di OpenCL e di inizializzazione del framework (mappe, vettori, ...) avviene una sola volta quando viene richiesto, e quindi automaticamente creato, l'oggetto CL_Enviroment. In seguito, tutta la gestione di richesta code e generazione dei kernel passa per l'oggetto CL_Enviroment e il programmatore ha il pieno utilizzo (con le responsabilità che questo comporta) di tutti questi oggetti e delle operazioni eettuabili con loro. Oggetti OpenCL come code, kernel, e oggetti memoria saranno tutti mascherati da classi o strutture in C++. 3.2.2.2 Oggetti Il framework è programmato con il linguaggio di programmazione ad oggetti C++. Si è deciso di sfruttare il paradigma della programmazione ad oggetti per permettere una manipolazione degli strumenti di OpenCL più vicina alla realtà progettuale. Occorre osservare una piccola nota: non viene assolutamente forzato il paradigma ad oggetti del C++ ma viene usato solo quando strettamente necessario e si cerca di creare oggetti il più leggeri possibile per non aggiungere nessun overhead che non porti con se usi pratici con ciò che aggiunge. Il paradigma ad oggetti, quindi, sarà presente sempre e solo quando la sua applicazione avrà notevoli beneci sulla gestione e pulizia del codice rispetto a quello originario di OpenCL. Chiaramente questa scelta porterà ad una serie di limiti per quanto riguarda la responsabilità della gestione della memoria: con l'introduzione degli oggetti, il programmatore della libreria deve porre molta attenzione alla loro manipolazione; inoltre, come scritto, il codice è pensato per poter essere usato anche con le funzioni base di OpenCL quindi sarà comunque possibile accedere ai dati incapsulati nelle classi. Tutte queste scelte, insieme ad altre discusse in seguito, portano ad una presa di coscienza da parte del programmatore per quanto riguarda la gestione di questi oggetti. 3.2.2.3 Singleton Design Pattern Il Singleton è uno dei design pattern più comuni, elencato anche nel celebre libro Design Patterns della Gang of Four [29]. Il Singleton fa parte di quelli che vengono deniti design pattern creazionali, ovvero quella classe di design pattern che ha lo scopo di controllare la costruzione e la gestione dell'esistenza di un oggetto. In particolare, il singleton ha lo scopo di garantire che di una determinata classe venga creata una e una sola istanza, e di fornire un punto di accesso globale a tale istanza. CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 57 Nel caso del framework, infatti, viene usato il metodo della classe CL_Enviroment per prendere l'unica istanza del programma. Esempio: //codice per avere il puntatore all'unica //istanza di CL_Enviroment CL_Enviroment *pointerToInstance = CL_Enviroment::getInstance(); In C++, il Singleton viene sviluppato con costruttore, copy-constructor, operatore di assegnamento e distruttore privati. 3.2.2.4 Kernel OpenCL La gestione dei kernel e dei le OpenCL è una di quelle funzionalità del framework che ha occupato una buona parte della progettazione. Il framework mette a disposizione metodi per caricare stringhe contenenti codice OpenCL o direttamente le con codice OpenCL. Una volta che il le o la stringa vengono vericati, l'utente decide attraverso le politiche di gestione del framework le modalità di compilazione dei programmi; quando l'utente vuole un kernel OpenCL fra quelli compilati non farà altro che interrogare l'istanza dell'ambiente OpenCL che il framework mette a disposizione indicando il nome del kernel compilato. Con questa modalità un piccolissimo overhead di strutture sovrastanti il codice sorgente permette di creare ed interrogare comodamente una base di dati che contiene tutti i kernel caricati nell'ambiente OpenCL. La libreria IVLLIB verrà fornita con un meccanismo di inizializzazione che provvederà a caricare nell'ambiente OpenCL tutti i le in cui sono presenti i kernel sviluppati appositamente per la libreria. In caso il programmatore volesse aggiungere uno o più algoritmi OpenCL alla libreria, basta semplicemente aggiungere il le con i kernel insieme agli altri e caricare il suo le nella procedura di inizializzazione di OpenCL della libreria. 3.2.2.5 Politiche di gestione Un'altra peculiarità del framework dovrà essere quella di automatizzare alcune procedure importanti durante l'esecuzione di una computazione OpenCL. L'utente, oltre ad avere comunque gli strumenti per controllare direttamente il framework, avrà anche a disposizione delle politiche comportamentali che deniscono che scelte deve fare il framework per l'utente e quali sono i casi e le modalità con qui queste scelte devono essere fatte. Anche questa caratteristica è nata dall'osservazione di comportamenti modali nei programmi OpenCL: gran parte del codice si basa tutto sulla stessa logica di fondo, indipendentemente dallo scopo o dalle modalità dell'elaborazione. I processi che richiedono automazione sono la richiesta di code su specici device, la decisione del momento della compilazione dei programmi e l'elaborazione dei comandi sui device. CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 58 Nonostante verrà lasciata l'interpretazione di OpenCL di poter scegliere liberamente questi comportamenti, il framework avrà in sé delle politiche per gestire come queste operazioni verranno eseguite automaticamente quando richiesto dall'esecuzione del normale usso del programma. Le politiche saranno esclusivamente opzioni di priorità per quanto riguarda le scelte di code, device e momento in cui uno o più programmi vengono compilati. 3.2.2.6 Gestione degli errori OpenCL ha un meccanismo di gestione degli errori con codice di ritorno. Ogni funzione ritorna un codice di errore e in base a questo codice l'utente è libero di gestire l'errore. Inoltre, a meno che l'errore non sia critico (segmentation fault, ...), OpenCL continua la sua elaborazione senza bloccare il codice al primo errore. Dato che molte procedure vengono automatizzate e controllate, e quindi messe in sicurezza, nel framework si è deciso di gestire gli errori di OpenCL con il meccanismo delle eccezioni del linguaggio C++. Questo comportamento è costruito per consentire un controllo accurato del codice in modo da interromperne l'esecuzione in caso dovesse succedere qualche evento per cui il normale scorrere dell'elaborazione potrebbe causare uno o più errori. Mentre per gli errori che possono accadere durante l'esecuzione dei kernel non c'è rimedio, la standardizzazione e l'automazione di alcune operazioni di inizializzazione ha permesso di creare del codice che in linea di massima non dovrebbe portare ad errori. Con la frase precedente si vuole soltanto aermare che data l'automazione dei comportamenti basilari di OpenCL, se il programmatore della libreria agisce secondo quanto scritto nella documentazione fornita con questo software e con la specica di OpenCL, non si dovrebbe preoccupare di comportamenti strani del codice senza che questo venga avvertito; inoltre, se l'ambiente viene usato come scritto, il programmatore della libreria sarà libero di usare tutte le sue funzionalità in totale sicurezza. Come scritto in precedenza, questo framework non vuole in nessun modo limitare l'uso di OpenCL e quindi il programmatore avrà ancora pieno accesso ai dati che le classi incapsulano ed a tutte le feature basilari di OpenCL. Rimane ovvio che per gli errori di programmazione verranno aggiunte assert nel codice per bloccare comportamenti formalmente errati: puntatori a NULL, dimensioni dei buer errati, ... . Questo implica che l'uso del framework deve essere fatto da utenti che hanno già una buona padronanza con OpenCL e desiderano semplicemente un framework che possa astrarre molti dettagli spesso irrilevanti. 3.2.2.7 Trasparenza diversa per utenti diversi Esistono tre tipologie di utenti per questo framework: il programmatore della libreria IVLLIB che conosce OpenCL e sfrutta questo strumento per scrivere algoritmi per la libreria astraendo la parte CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 59 di OpenCL e lasciando all'utilizzatore del metodo la stessa interfaccia delle normali funzioni della IVLLIB; il programmatore che decide di usare OpenCL per sviluppare una funzione o un particolare algoritmo e decide di usare il framework per scrivere un codice più pulito, chiaro e sicuro; l'utente della libreria che non ha assolutamente nessuna conoscenza delle strutture di OpenCL ma ne conosce le potenzialità e vuole utilizzare gli algoritmi che sono stati scritti usando questo framework. 2 Per quanto riguarda l'utente programmatore , questo conosce bene OpenCL e ha a disposizione il codice sorgente del framework e la sua documentazione. Con questi materiali, il livello di trasparenza che questo utente vede è solo relativo in quanto per usare bene il framework, oltre a sapere OpenCL, il programmatore dovrà avere anche una buona conoscenza delle scelte progettuali ed implementative eettuate per permettersi di usare questo strumento con comodità durante la scrittura di algoritmi complessi e funzioni che usano OpenCL. Questo framework permetterà al programmatore di lavorare concentrandosi esclusivamente sullo scopo nale senza perdere codice e tempo nella creazione di strutture OpenCL secondarie al ne per cui sta programmando una o più funzioni. L'utente che utilizza la libreria IVLLIB, invece, non dovrà avere nessuna conoscenza specica di OpenCL; quest'ultimo avrà solo la peculiarità di scegliere se elaborare un algoritmo con le normali funzioni della libreria IVLLIB o se usare lo stesso algoritmo, se presente, in OpenCL. Ovviamente questo algoritmo sarà stato scritto dal programmatore della libreria e l'utilizzatore userà questa funzione in maniera totalmente indipendente dalla sua implementazione OpenCL. Per concludere, le funzioni usate da quest'ultimo utente sono praticamente uguali a quelle usate normalmente a meno del fatto che come motore queste useranno il framework di OpenCL in oggetto della tesi. Per capire meglio lo scopo degli utenti la gura 3.1 nella pagina successiva mostra un diagramma dei casi d'uso. 3.3 Diagrammi di usso e UML In questa sezione verranno mostrati i diagrammi di usso e il diagramma UML delle classi per sottolineare la logica generale con cui deve essere sviluppato un codice che usa questo framework e per avere una comprensione generale di come è strutturata l'architettura del codice. Questi strumenti, tipici dell'ingenieria del software, sono stati molto utili per concepire le prime parti del codice e per avere una consapevolezza generale della struttura che doveva essere costruita. 2 Per utente programmatore si intende i primi due casi sopra descritti. CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 60 Figura 3.1: Diagramma dei casi d'uso del framework di OpenCL 3.3.1 Flow Chart La gura 3.2 nella pagina seguente mostra un ow chart di una possibile procedura che sfrutta il framework OpenCL. Si può facilmente notare come il usso segua sia il programma prima mostrato, che quelli scritti in seguito. La linearità del codice OpenCL consiste principalmente nella sua caratteristica di essere un codice di inizializzazione , ovvero un codice che imposta un ambiente di esecuzione e lo inizializza; questo rende il codice poco dinamico. Il codice del framework, invece, ha un andamento poco meno lineare di questo e più che altro è composto da scelte che vengono fatte in seguito ai comandi invocati dall'utente. Ho preferito mostrare degli esempi e degli schemi di codice lineare per essere certo che le meccaniche basilari siano chiare. Questo è stato fatto principalmente 3 riuscire ad avere la visione generale di tutto perchè in OpenCL è spesso dicile l'applicativo; infatti, grazie alla reference ottima e ben documentata, è chiaro cosa svolgono le singole funzioni, mentre è meno ovvio assemblare insieme tutti i mattoncini per creare un programma dalla struttura solida, stabile e sicura. 3.3.2 Diagramma UML delle classi Con il diagramma in gura 3.3 nella pagina 62 ho voluto mostrare le classi principali che sono state concepite per la realizzazione di questo framework. Obiettivo di questo diagramma è quello di dare un'idea degli oggetti di cui l'utente può disporre quando usa il framework di OpenCL e di come questi oggetti interagiscono. 3 Questa è stata la più grande dicoltà arontata nello sviluppo del framework. CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 61 Figura 3.2: Diagramma di usso di una procedura che sfrutta il framework di OpenCL CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 62 Figura 3.3: Diagramma UML delle classi del framework di OpenCL Come dallo standard UML, nella gura 3.3 si può intuire la classe più importante: il CL_Enviroment. Questa classe rappresenta l'ambiente OpenCL, ovvero il contesto e l'insieme di impostazioni iniziali che vengono automaticamente scelte dall'ambiente stesso in fase di costruzione. Inotre questo oggetto è rappresentato, come si vede nella gura 3.3 dal link a se stesso, con un Singleton: è da questo Singleton che tutti gli oggetti vengono generati. 3.3.3 Note progettuali Dopo aver deciso di rendere il CL_Enviroment l'ambiente di tutte le computazioni, si è deciso di far dipendere la costruzione e la gestione di alcune parti di OpenCL direttamente da questo oggetto. Come si può vedere, l'istanza di CL_Enviroment controlla l'accesso diretto, mascherandolo all'utente, di programmi e device. Per quanto riguarda gli altri oggetti come code e oggetti memoria, dal diagramma è possibile intuire come questi dipendono, direttamente o non, dall'oggetto CL_Enviroment ma non vengono generati e gestiti da questo. Il loro uso è a totale discrezione e responsabilità dell'utente, nella stessa maniera con cui lo sarebbero gli oggetti nativi di OpenCL. Ci sono due oggetti che occorre analizzare in maniera specica: le CL_Queue e gli oggetti CL_Mem. Per quanto riguarda le code, queste vengono generate dall'ambiente sotto richiesta dell'utente e vengono associate al device che l'utente ha scelto, se specicato, o a quello scelto dalla politica. Gli oggetti CL_Mem sono stati creati templati per poter disporre di un CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 63 container generico dando la possibilità di creare oggetti di memoria di OpenCL per qualsiasi dato di cui l'utente dispone. In aggiunta verranno scritte delle specializzazioni di oggetti CL_Mem per integrarne l'uso con le strutture dati della libreria IVLLIB come vector e matrix template. In ultimo occorre far notare un particolare della gura 3.3 nella pagina precedente: tutti gli oggetti hanno un metodo getData(). Questo metodo permette di accedere ai dati base di OpenCL per lasciare al programmatore la libertà di poter usare, nello stesso codice, sia chiamate a funzioni del framework nativo di OpenCL, che chiamate del framework della libreria IVLLIB. 3.4 Design di Algoritmi di Image Processing con il framework di OpenCL In questa sezione, la più corposa del capitolo, verranno mostrati due esempi di progettazione ed implementazione di algoritmi di image processing da parte di due potenziali utilizzatori del framework: un programmatore della libreria IVLLIB che scrive il metodo per la gamma correction ed un programmatore esterno che vuole fare una sua computazione che non è presente nella IVLLIB e decide, quindi, di usufruire di questo framework. Entrambe le parti verteranno sull'analisi che dovrebbe compiere il programmatore nel momento in cui decide di scrivere del codice, sia che questo venga fatto per la libreria, sia che questo venga scritto come codice dedicato. Prima di incominciare occorre scrivere una piccola nota: in questa sezione non verrà mostrato tanto il codice eettivo da cui prendere la sintatti corretta del framework in quanto non rilevante per lo scopo che questa sezione si pregge. Verrà invece scritto dello pseudocodice ponendo eventuali dettagli riferiti ad OpenCL o al framework quando necessario. Obiettivo di questa sezione è infatti quello di mostrare il vademeecum a cui dovrebbe attenersi chiunque decida di programmare con questo framework per elaborare computazioni in un ambiente OpenCL. 3.4.1 Programmatore della libreria: Gamma Correction La gamma correction è l'operatore puntuale per eccellenza. Prima entrare nel dettaglio della gamma correction e di come viene applicata, verrà descritta la natura degli operatori puntuali per capire l'analisi che dovrebbe svolgere un programmatore quando decide di usare OpenCL. Gli operatori puntuali sono quelle operazioni che, in un'immagine, agiscono su un unico pixel indierentemente dagli altri; gli operatori puntuali hanno quindi due grandi vantaggi: posso applicare l'operazione sull'immagine e scrivere direttamente sullo stesso pixel in cui sto lavorando in quanto quest'ultimo non inuenzerà gli altri. Inoltre il fatto di poter scrivere direttamente sullo stesso pixel su CAPITOLO 3. 64 PROGETTAZIONE DEL FRAMEWORK cui sto lavorando implica un grande risparmio di memoria in quanto non devo avere un buer temporaneo per salvare i calcoli; non devo seguire nessun ordine nell'esecuzione e sono libero di incominciare ovunque nell'immagine. E' intuitivo e quasi scontato capire la natura parallela di questo problema: se ogni pixel è indipendente uno dall'altro, per ogni pixel verrà creata un'istanza di un kernel OpenCL che svolgerà l'operazione necessaria. Questo è un ottimo esempio per mostrare le capacità di OpenCL. 3.4.1.1 La Gamma Correction La gamma correction è un operatore che è nato direttamente dai problemi che avevano le televisioni con tubo catodico nel rappresentare le immagini trasmesse via cavo. Quando un'immagine veniva trasmessa e riprodotta nel televisore a tubo catodico, l'immagine presentava i colori parzialmente alterati. Questo era causato principalmente dalla sica degli elettroni sparati dal cannone del televisore per colpire i fosfori dello schermo. L'operatore gamma serve quindi a mappare un range di valori in un dominio più uniforme di quello originale. La formula della gamma correction è la seguente: gamma = c ∗ ey dove c e y sono i parametri della gamma correction. Normalmente uguale ad uno ed è il parametro gamma y c è posto a variare. 3.4.1.2 Progettazione Kernel Prima di tutto occorre capire come scrivere il kernel di OpenCL e le dimensioni del problema. Dato che sto eettuando un'elaborazione su una immagine rappresentata logicamente da una matrice, nonostante questa sia immagazzinata come buer lineare, il nostro problema è un problema a due dimensioni: righe e colonne della matrice. Il kernel prenderà quindi in ingresso tre parametri: il vettore rappresentante l'immagine (supponendo l'immagine come RGB8, ovvero come unsigned char), il parametro y della gamma correction e il numero di colonne della matrice. Un modello di kernel per elaborare questo calcolo potrebbe essere: /* Questo kernel è solo un modello a cui ci si può ispirare per realizzare una versione di questa procedura. Non dovrebbe essere preso come esempio di sintassi precisa e corretta di OpenCL. Questa funzione prende in ingresso il buffer lineare dell'immagine, le colonne dell'immagine CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 65 e il parametro gamma per calcolarne la gamma correction */ __kernel void gamma(unsigned char *image, float gamma, int width){ //quì interrogo il sistema per sapere //che kernel sono, ovvero la posizione //del pixel nello spazio del //problema dato che una //matrice viene risolta come problema //a due dimensioni int x = get_global_id(0); int y = get_global_id(1); //quì copio il valore, meglio non operare //direttamente sulla memoria globale unsigned char value = *image[x * width + y]; //calcolo il valore unsigned char returnedValue = powr(value, gamma); //lo scrivo dentro ogni cella *image[x * width + y] = returnedValue; } Se l'immagine ha le dimensioni di 200x200 pixel, verranno lanciate 40000 istanze di kernel che elaborano su ogni singolo pixel la gamma correction. 3.4.1.3 Possibile Codice del Programma In questa sotto-sezione viene sviluppato dello pseudocodice per spiegare come potrebbe essere progettata e scritta una funzione di OpenCL concepita dal programmatore della libreria IVLLIB che realizza la gamma correction. Nel programma si presuppone che la funzione venga chiamata solo dopo aver 4 eseguito l'inizializzazione dell'ambiente OpenCL . Si suppone di fare un metodo che esegua la gamma correction su una matriche di RGB8, ovvero unsigned char, e che il kernel, chiamato gamma, sia già stato caricato dalla initCL(). void gammaCorrection(Matrix<RGB8> &matrix, float gamma){ //Per sicurezza tutto il codice è inserito //all'interno di un blocco try-catch //-------------------------------try{ 4 Chiamando una funzione di inizializzazione come ad esempio la initCL(), che carica tutti i programmi in memoria. CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 66 //la init deve essere chiamata //prima di chiamare questa funzione //--------------------------------//prendo i puntatore all'unica istanza //dell'ambiente OpenCL CL_Enviroment *pointer = CL_Enviroment.getInstance(); //imposta la politica per scegliere //la CPU come primo device pointer->setManagement(CPU_FIRST); //preleva la coda sul device scelto //dalla politica CL_Queue queue = pointer->getQueue(); //crea gli oggetti memoria necessari //all'elaborazione (tipo, puntatore, lunghezza) CL_Mem cl_matrix(READ_WRITE, matrix.data(), matrix.columns() * matrix.rows() * sizeof(RGB8)); CL_Mem fGamma(READ_ONLY, &gamma, sizeof(float)); CL_Mem columns(READ_ONLY, &matrix.columns(), sizeof(int)); //riempi gli oggetti memoria di OpenCL queue.writeDataToDevice(cl_matrix); queue.writeDataToDevice(fGamma); queue.writeDataToDevice(columns); //creazione del kernel di OpenCL CL_Kernel ker = pointer->getKernel(gamma); //vengono impostati gli argomenti ker.setArgument(0, cl_matrix); ker.setArgument(1, gamma); ker.serArgument(2, columns); //vengono impostate le dimensioni del //problema OpenCL ker.setKernelDimension(TWO); ker.dimensionOne = matrix.columns(); ker.dimensionTwo = matrix.row(); //esecuzione del kernel queue.run(ker); //vengono prelevati i dati queue.readDataFromDevice(cl_matrix, matrix.data()); }catch(exception &e){ //gestione dell'eccezione } } CAPITOLO 3. 67 PROGETTAZIONE DEL FRAMEWORK 3.4.2 Utilizzatore del Framework di OpenCL: Filtro di Smoothing Un programmatore vuole applicare un ltro di smoothing alla sua immagine e dispone già dei parametri che desidera usare. Si rende conto che nella libreria non è stato implementato un ltro di smoothing e decide di crearne uno e di usare il framework OpenCL della libreria IVL per scriverlo. Anche in questo caso il codice scritto deve servire come esempio per il programmatore che si interfaccia con questo framework e desidera avere un'idea generale di come dovrebbe progettare il codice. 3.4.2.1 Filtro di Smoothing Il ltro di smoothing viene utilizzato principalmente per sfocare un'immagine in base ai pesi assegnati al ltro. Lo smoothing è un'operazione di correlazione e consiste nel passare sull'immagine, pixel per pixel, il ltro costituito da una matrice di valori che sono pesati in base al fattore di smoothing che vogliamo applicare. Quindi, supponendo un ltro quadrato, ad esempio un ltro 3 ∗ 3, abbiamo i seguenti dati: per ogni pixel dell'immagine valore è n ∗ m, con x, y indici dell'immagine, il suo f (x, y); per ogni pixel del ltro 3 ∗ 3, con i, j indici del ltro, il suo valore è g(i, j). L'operazione può quindi essere riassunta nella seguente formula: n X m X 3 X 3 X ( f (x, y) ∗ g(i, j)) x=0 y=0 i=0 j=0 Si possono notare subito due caratteristiche di questa operazione: la notevole quantità di calcoli necessari per portare a termine tutta la computazione; l'obbligo di usare di un buer temporaneo dove salvare i dati in quanto, se li riscrivessi sulla stessa matrice, corromperei i dati per l'elaborazione successiva. Dai punti sopra posso quindi dedurre che questa operazione è indipendente pixel per pixel grazie all'uso di un buer temporaneo; questo porta direttamente a scoprire la natura parallela del programma e come si adatta perfettamente alla logica di OpenCL. CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 68 3.4.2.2 Progettazione Kernel Anche in questo caso partiamo dalla denizione del kernel e dalle dimensioni del problema. Il problema è ancora una volta a due dimensioni. Un prototipo del kernel potrebbe essere il seguente: /* Questo kernel è solo un modello a cui ci si può ispirare per realizzare una versione di questa procedura. Non dovrebbe essere preso come esempio di sintassi precisa e corretta di OpenCL. Questa funzione prende in ingresso il buffer lineare dell'immagine e le colonne dell'immagine Il filtro viene scritto come costante del kernel */ //filtro costante nella memoria //__constant del device OpenCL __constant unsigned char[] filter = {1, 2, 1, 2, 4, 2, 1, 2, 1}; __kernel void convolution(unsigned char *image, int width){ //quì interrogo il sistema per sapere //che kernel sono, ovvero la posizione //del pixel nello spazio del //problema dato che una //matrice viene risolta come problema //a due dimensioni //-----------------------------------//il +1 serve ad ignorare la cornice //Vedi commento nel codice dell'host int x = get_global_id(0) + 1; int y = get_global_id(1) + 1; //quì copio il valore, meglio non operare //direttamente sulla memoria globale unsigned char value = *image[x *image[x *image[x *image[x *image[x *image[x *image[x *image[x *image[x * * * + + + 1 * width + y - 1] * filter[0] + 1 * width + y] * filter[1] + 1 * width + y + 1] * filter[2] + width + y - 1] * filter[3] + width + y] * filter[4] + width + y + 1] * filter[5] + 1 * width + y - 1] * filter[6] + 1 * width + y] * filter[7] + 1 * width + y + 1] * filter[8]; CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 69 value = value / 16; *image[x * width + y] = value; } 3.4.2.3 Possibile Codice del Programma In questa sotto-sezione si ipotizzano alcune linee di codice che un utente che usa il framework potrebbe utilizzare per compiere un determinato calcolo. Anche in questa sezione non viene incentrata l'attenzione sulla sintassi; si preferisce evidenziare, specialmente in questo caso, la possibilità di miscelare il codice del framework IVL con il codice e le funzioni di OpenCL. Si ipotizza che il kernel sopra scritto sia in un le smoothing.cl nella stessa cartella dell'eseguibile. Il codice, direttamente in un main, potrebbe essere: //... //Ho una matrix in cui c'è l'immagine //su cui fare smoothing e sto supponendo che //il file con il kernel smoothing chiamato //smoothing.cl sia nello stesso percorso //dell'eseguibile //Per sicurezza tutto il codice è inserito //all'interno di un blocco try-catch try{ //Non c'è inizializzazione in quanto sto usando //il framework senza le librerie IVLLIB //--------------------------------//prendo i puntatore all'unica istanza //dell'ambiente OpenCL CL_Enviroment *pointer = CL_Enviroment.getInstance(); //prendo direttamente la CPU come OpenCL device CL_Device dev = pointer->getDevice(CPU); //apro la coda su un device specifico CL_Queue queue = pointer->getQueue(dev); //creo e compilo il programma id_type program = pointer->addProgramFromFile(smoothing.cl); pointer->compileProgram(program); //creo gli oggetti memoria (matrice RGB8) CL_Mem cl_matrix(READ_ONLY, matrix.data(), matrix.columns() * matrix.rows() * sizeof(RGB8)); CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 70 CL_Mem columns(READ_ONLY, &matrix.columns(), sizeof(int)); CL_Mem result<unsigned char>(READ_WRITE, NULL, matrix.columns() * matrix.rows() * sizeof(RGB8)); //riempio gli oggetti memoria queue.writeDataToDevice(cl_matrix); queue.writeDataToDevice(columns); //creo il kernel CL_Kernel ker = pointer->getKernel(smoothing); //imposto gli argomenti del kernel ker.setArgument(0, cl_matrix); ker.serArgument(1, columns); //imposto le dimensioni del problema Dimension dimensions = TWO; //lo smooth non viene fatto sulla cornice ker.dimensionOne = matrix.columns() - 2; ker.dimensionTwo = matrix.rows() - 2; ker.setKernelDimension(dimensions); //esegue il kernel queue.run(ker); //creo spazio per legger i dati dell'esecuzione unsigned char cl_data [1000]; //---//lettura dei dati dell'esecuzione // - per leggere uso codice OpenCL //chiamata OpenCL clEnqueueReadBuffer( queue.getData(), //ritorna la coda OpenCL result.getData(), //oggetto cl_mem CL_TRUE, 0, 1000 * sizeof(unsigned char), cl_data, //puntatore ai dati in cui //metto il risultato 0, NULL, NULL); //aspetto la coda scrivendo una clFinish esplicita clFinish(queue.getData()); }catch(exception &e){ //gestione dell'eccezione } //... CAPITOLO 3. PROGETTAZIONE DEL FRAMEWORK 71 3.4.3 Conclusioni Il codice sopra scritto mostra la versatilità e la facilità con cui questo framework può essere usato. Ancora una volta si ricorda che il codice precendentemente mostrato è solo un esempio fra i tanti possibili ed è lontano dalla correttezza sintattica con cui andrebbe scritto; i due precedenti esempi rappresentano, con molta propabilità, i casi possibili di utilizzo del framework. Durante tutto il codice verranno adeguatamente gestite le eccezioni, se lanciate, e grazie ai campi che queste hanno, per l'utente risulta molto facile comprendere cosa non ha funzionato nell'elaborazione. Inne, occorre notare come la sintassi a oggetti permette una scrittura ed una conseguente lettura del codice molto più facile ed immediata. 3.5 Possibilità di modica ed evoluzione del progetto Il framework realizzato non si potrà mai denire nito; è infatti semplice notare due strade evolutive che questo lavoro porta con sé: algoritmi della IVLLIB sempre più complessi e sempre più specializzati per i device, con ottimizzazioni sempre migliori e per set di dati sempre più grandi; modiche e upgrade del framework e della sua struttura per orire al programmatore della libreria uno strumento dalla malleabilità sempre maggiore senza inuenzare l'ecienza del codice. Insieme a questo framework viene fornita una documentazione che ha una duplice funzionalità: da una parte si compone della classica documentazione necessaria all'uso del framework ed alle piccole modiche che si vogliono eettuare, dall'altra una vera e propria guida riassuntiva di OpenCL che ha lo scopo di spiegare in maniera molto dettagliata come è stato scritto il codice e quali sono le peculiarità da migliorare e i difetti da correggere. Risulta quindi rilevante il lavoro riguardo alla documentazione, sempre importante nella programmazione e progettazione del software e di particolare importanza in questo framework. Uno degli obiettivi principali con cui è stato scritto è esattamente quello di creare una documentazione accurata che permetta a chiunque studi OpenCL di poter continuare il lavoro sapendo esattamente le funzioni e le scelte programmative che hanno dato alla luce il codice di questo progetto. Capitolo 4 Conclusioni Questo capitolo mostrerà quali sono le eettive conclusioni di questa tesi, motiverà il lavoro svolto incentrando la questione sull'utilità pratica del linguaggio ed alla ne verrà lasciato un paragrafo per i commenti personali e il futuro di questo prodigioso strumento. 4.1 OpenCL: perchè Questa sezione vuole rispondere a quell'insieme di domande che possono essere riassunte in: Cosa porta di nuovo questo framework e perchè dovrebbe essere utilizzato? 4.1.1 Ecienza Computazionale Al giorno d'oggi, la potenza di un computer è ancora un limite agli obiettivi che l'essere umano si è posto. Solo nella computer graphics esistono algorit- mi ai quali occorrono ore, se non giorni, per restituire un risultato. Calcoli e simulazioni di ogni tipo eseguono codice parallelo su macchine costruite appositamente e nonostante questo il lavoro dei ricercatori è frenato dai tempi della macchina. OpenCL si pone come risposta a tutte queste chiamate, e risponde con un framework in grado di soddisfare le richieste di chiunque. Inoltre OpenCL rivaluta le architetture multiprocessore commercializzate negli ultimi anni da un marketing sempre troppo altisonante e mai veramente utile: OpenCL permette, infatti, di trasformare un processore Quad Core da un centinaio di dollari in una postazione di calcolo professionale con costi pari a zero. OpenCL porta in un'azienda della potenza di calcolo praticamente gratis in quanto permette la rivalutazione delle architetture su cui l'azienda lavora scrivendo solo software. Inoltre bisogna ricordare che OpenCL ha dimostrato risultati a dir poco eccezionali con vari prodotti, come ad esempio schede video, 72 CAPITOLO 4. CONCLUSIONI 73 che appartengono alla fascia commerciale non professionale e quindi dai prezzi abbordabili e molto competitivi. 4.1.2 OpenCL è Open La specica di OpenCL è completamente Open ed è in quanto tale sinonimo di qualità. Come già scritto, questa specica è diretta dalla Khronos, leader del mercato e associazione con anni ed anni di lavori dalla qualità eccellente ed ineccepibile; inoltre questo progetto è svolto con la partecipazione delle più grandi major del mercato di CPU, GPU e processori embedded. Questo è indice di qualità ed a dimostrarlo c'è il fatto che, mentre questa tesi viene scritta, la specica OpenCL 1.1 è in produzione portando correzioni ed ulteriori vantaggi computazionali. Il lavoro che la Khronos ha svolto, e continuerà a svolgere, è conseguenza diretta di standard di altissima qualità e tutte le aziende che hanno sottoscritto questa collaborazione hanno alle loro spalle anni e prodotti di ottima fattura e sicurezza: OpenCL è la conseguenza di tutto questo messo insieme. 4.1.3 Con questo framework, OpenCL è facile Uno degli obiettivi primari di questo framework è quello di facilitare e rendere più sicuro l'uso di OpenCL da parte di un programmatore esperto. Come più volte sottolineato, il framework semplica il lavoro di chiunque abbia la necessità di usare le caratteristiche di OpenCL senza però incombere in chiamate a funzioni sempre uguali e in controlli di errore che si ripetono nel codice. Ancora una volta occorre sottolineare che questo framework non ha assolutamente la pretesa di far usare OpenCL a qualcuno che non ne conosce il funzionamento; infatti, nonostante una documentazione ricca di contenuto, questo framework è consigliato solo a coloro che, avendo una buona conoscenza di OpenCL, possono accedere alla moltitudine di opzioni e utilizzi a cui questo strumento si presta, sfruttandone ogni linea di codice e liberandone così tutte le potenzialità. 4.2 Cosa porta di nuovo Le conclusioni a cui giunge questa tesi sono ormai ovvie: si è riuscito a costruire un wrapper di funzioni OpenCL e l'obiettivo di fornire un framework che astragga molte funzioni di OpenCL è stato raggiunto. Ora parte del lavoro consisterà nel continuo miglioramento del framework che dovrà seguire contemporaneamente due vie. La prima sarà quella di inglobare e rendere automatiche sempre più funzionalità di OpenCL, seguendo le nuove speciche e le nuove caratteristiche che verranno messe a disposizione per dare al programmatore uno strumento sempre più intuitivo e sempre più sicuro per programmare con OpenCL. CAPITOLO 4. CONCLUSIONI 74 La seconda via consiste nella costruzione di una parte della libreria IVLLIB in grado di sfruttare questo framework e di dare all'utente, ignaro di come si usi OpenCL ma conscio delle sue potenzialità, un'interfaccia trasparente da poter utilizzare per poter eseguire algoritmi sempre più complessi computazionalmente o per avere un range di funzioni da poter usare sempre maggiore in grado di fornire un'interfaccia di calcolo che si adatti alle sue esigenze. 4.3 Note nali L'evoluzione di questo framework, come scritto precedentemente, corre su due rispettive vie: libreria IVLLIB sempre più ricca di funzioni che usano OpenCL ed un framework sempre più intuitivo e sicuro da usare. Le conclusioni di questa tesi arrivano quindi ad una ne: OpenCL si è dimostrato un valido strumento, pratico, ben progettato e supportato da una implementazione e documentazione che raggiungono i più alti livelli di eccellenza. Per l'ultima volta bisogna ricordare l'attenta analisi che occorre fare prima di sviluppare un'applicazione in OpenCL: questo framework non è una scatola magica che risolve i problemi dei programmatori, ma è un framework che specializza un ambito della programmazione, fornendo uno strumento in grado di andare ad un livello inferiore alla normale programmazione procedurale e strutturale a cui siamo abituati. Sono questi strumenti che, combinati opportunamente, un giorno renderanno il computer uno strumento in grado di essere veramente general purpose e di fornire molte delle risposte che l'essere umano sta ancora cercando e, prima o poi, troverà. Bibliograa http://en.wikipedia.org/wiki/Video_card [1] Schede Video, Wikipedia, (ultima visita 2 Maggio 2010) [2] Tecnologia FireStream [3] Tecnologia AMD Fire http://en.wikipedia.org/wiki/AMD_ Stream, (ultima visita 2 Maggio 2010) CUDA, http://en.wikipedia.org/wiki/CUDA Wikipedia, (ultima visita 2 Maggio 2010) [4] AMD, history Wikipedia, http://en.wikipedia.org/wiki/Amd#Corporate_ (ultima visita 2 Maggio 2010) [5] Storia Tecnologia OpenCL, Wikipedia, Opencl#History [6] Serie GeForce http://en.wikipedia.org/wiki/ (ultima visita 2 Maggio 2010) GT200, GeForce_200_Series Wikipedia, http://en.wikipedia.org/wiki/ (ultima visita 2 Maggio 2010) [7] NVIDIA GTX285, sito proprietario NVIDIA, object/product_geforce_gtx_285_it.html http://www.nvidia.it/ (ultima visita 2 Maggio 2010) [8] GPU, [9] ATI http://en.wikipedia.org/wiki/Graphics_ Wikipedia, processing_unit 5850, (ultima visita 2 Maggio 2010) sito proprietario manolodeagostini/2009/10/ AMD/ATI, http://tomsblog.it/ (ultima visita 2 Maggio 2010) [10] NVIDIA GTX285, sito proprietario NVIDIA,http://www.nvidia.com/ object/product_geforce_gtx_285_us.html (ultima visita 2 Maggio 2010) [11] Descrizione Pipeline di rendering, Wikipedia, wiki/Graphics_pipeline http://en.wikipedia.org/ (ultima visita 2 Maggio 2010) [12] David Luebke, Nvidia Researcher e Greg Humphreys, University of Virginia, How GPUs Work, paper.php?paper_id=59 http://www.cs.virginia.edu/~gfx/papers/ (ultima visita 2 Maggio 2010) 75 76 BIBLIOGRAFIA [13] David Gohara, Center of Computational Biology, Responsabile dei tutorial di OpenCL, http://www.macresearch.org/opencl (ultima visita 2 Maggio 2010) [14] Fonte OpenCL Tutorials, sito, http://www.macresearch.org/Opencl (ultima visita 2 Maggio 2010) chip GT200, http://www.beyond3d.com/images/reviews/ gt200-arch/gt200die-big.jpg (ultima visita 2 Maggio 2010) [15] Fonte chip GT200, http://www.xbitlabs.com/images/video/ geforce-gtx200-theory/g200chip.png (ultima visita 2 Maggio 2010) [16] Fonte chip GT200, http://www.bit-tech.net/hardware/graphics/ 2008/06/24/nvidia-geforce-gtx-280-architecture-review/7 [17] Layout (ultima visita 2 Maggio 2010) [18] Fonte chip Core 2 Duo, core2.03.jpg [19] Shader Shader [20] Frame Buffer [21] ROP, http://www.sudhian.com/img/intel/core2/ (ultima visita 2 Maggio 2010) programmabili, Wikipedia, http://en.wikipedia.org/wiki/ (ultima visita 2 Maggio 2010) Buer, Wikipedia, http://en.wikipedia.org/wiki/Frame_ (ultima visita 2 Maggio 2010) http://en.wikipedia.org/wiki/Render_Output_ Wikipedia, Processor (ultima visita 2 Maggio 2010) [22] Recensione Architettura http://www. GTX280, bit-tech.net/hardware/graphics/2008/06/24/ nvidia-geforce-gtx-280-architecture-review/4 (ultima visita 2 Maggio 2010) http://www.hwstation.net/ recensioni/evga_geforce_gtx_280_sc/gt200-600.html (ultima visita [23] Figura e recensione Architettura GTX280, 2 Maggio 2010) [24] Khronos, OpenCL specication, http://www.khronos.org/registry/cl/ (ultima visita 2 Maggio 2010) [25] Justin Hensley, Ph.D. Senior Member of Technical Sta, Oce of the CTO - Advanced Technology Initiatives AMD/ATI, Video Tutorials http://developer.amd.com/documentation/videos/ OpenCLTechnicalOverviewVideoSeries/Pages/default.aspx (ultima on OpenCL, visita 2 Maggio 2010) [26] Single Instruction Multiple Data, Wikipedia, wiki/SIMD (ultima visita 2 Maggio 2010) http://en.wikipedia.org/ 77 BIBLIOGRAFIA [27] Single Process Multiple Data, Wikipedia, wiki/SPMD http://en.wikipedia.org/ (ultima visita 2 Maggio 2010) [28] Architettura di memoria di un device OpenCL, p/zocle/wiki/MemoryModel [29] Singleton Design Pattern, http://code.google.com/ (ultima visita 2 Maggio 2010) http://it.wikipedia.org/wiki/Singleton (ultima visita 2 Maggio 2010) [30] Utilizzo di CUDA nell'implementazione di algoritmi di supporto all'Elaborazione delle Immagini , Tesi di laureat triennale di Pigazzini Andrea, 2007/2008