Scuola Politecnica e delle Scienze di Base Corso di Laurea in Ingegneria Informatica Elaborato finale in Calcolatori Elettronici I Confronto tra architetture di calcolatori impiegati in sistemi dedicati Anno Accademico 2014/2015 Candidato: Gabriella Esposito Matr. N46/1421 [] Indice Indice .................................................................................................................................................. III Introduzione ......................................................................................................................................... 4 Capitolo 1: Dispositivi FPGA .............................................................................................................. 5 1.1 Introduzione ai dispositivi programmabili ................................................................................. 5 1.2 Tecnologie di FPGA .................................................................................................................. 6 1.2.1 La tecnologia Antifuse ................................................................................................... 8 1.2.2 La tecnologia Flash ........................................................................................................ 9 1.2.3 La tecnologia RAM statica .......................................................................................... 10 1.3 Architettura di un FPGA ..................................................................................................... 11 1.3.1 Blocchi logici configurabili ......................................................................................... 12 1.3.2 Blocchi di ingresso/uscita ............................................................................................ 12 1.3.3 Linee di interconnessione............................................................................................. 12 1.4 FPGA Design e Programming............................................................................................. 13 1.4.1 Design Entry ................................................................................................................ 14 1.4.2 Simulazione.................................................................................................................. 14 1.4.3 Sintesi ........................................................................................................................... 14 1.4.4 Place & Route .............................................................................................................. 15 1.4.5 Programmazione e debug in-circuit ............................................................................. 15 Capitolo 2: Dispositivi GPU .............................................................................................................. 17 2.1 Architettura di un dispositivo GPU.......................................................................................... 18 2.2 Modello GPGPU ...................................................................................................................... 20 2.3 Approcci alla programmazione GP-GPU ................................................................................ 22 Capitolo 3: OpenCL ........................................................................................................................... 23 3.1 Programmazione eterogenea .................................................................................................... 25 3.2 Breve panoramica della specifica di OpenCL.......................................................................... 26 3.3 OpenCL su FPGA .................................................................................................................... 27 Conclusioni ........................................................................................................................................ 33 Bibliografia ........................................................................................................................................ 34 Introduzione Questa tesi si prefissa l’obiettivo di analizzare e confrontare le strutture di due dispositivi quali i sistemi FPGA e GPU. Nel primo capitolo verrà analizzata l’architettura ed il funzionamento di un FPGA introducendo dapprima le sue origini, ovvero l’evoluzione dei dispositivi programmabili, e descrivendo in seconda battuta le architetture più comuni presenti sul mercato. Infine, a valle della loro natura altamente programmabile, verrà illustrato il tipico flusso di design e programmazione di questi device. Nel secondo capitolo verrà descritta l’architettura di una GPU, in particolare andando a vedere quali sono state le sue evoluzioni sia tecnologiche che di impiego. D’altronde, proprio grazie al diverso utilizzo di questi dispositivi, nasce il modello GPGPU illustrato successivamente. Quindi, nel terzo capitolo, verrà mostrata la nascita e lo sviluppo di un nuovo linguaggio di programmazione, quale OpenCL, utile nei sistemi eterogenei, i quali spesso vedono la presenza di un FPGA o di una GPU come un acceleratore di calcolo. Infatti la nascita di questo standard nuovo ha rivoluzionato l’utilizzo dei dispositivi di calcolo e ha portato alla nascita di una nuova tecnica di programmazione, chiamata appunto programmazione eterogenea. L’utilizzo di questo standard in particolare verrà illustrato sia in un modello generale che prevede spesso la presenza di una GPU, che in particolare su dispositivi FPGA, illustrando in questo modo le differenze tra queste due applicazioni. 4 Capitolo 1: Dispositivi FPGA In questo capitolo verrà fatta una panoramica generale sui dispositivi programmabili e in particolare sulle tecnologie FPGA, device particolarmente interessanti poiché altamente prestanti e largamente impiegati nei sistemi embedded moderni. 1.1 Introduzione ai dispositivi programmabili Sappiamo che alla base dell’elettronica digitale vi sono le porte logiche le quali, se interconnesse in diverse configurazioni, producono funzioni logiche descritte dalla logica booleana. In generale si possono individuare diverse famiglie logiche che a loro volta possono essere divise in due macro gruppi: le famiglie logiche bipolari (TTL, ECL, I2L) le quali per realizzare un singolo elemento logico utilizzano i diodi e i transistor a giunzione, e quelle unipolari (MOS e CMOS) che invece utilizzano i MOSFET (Metal Oxide Semiconductor Field Effect Transistor). La nascita dei Dispositivi a Logica Programmabile (PLD) deriva dal forte limite che impongono le porte logiche per l’implementazione delle funzioni logiche, ovvero la non riprogrammabilità dei dispositivi prodotti. Per questo motivo nella seconda metà degli anni ‘60 nascono i primi PLD (Programmable Logic Device), circuiti integrati programmabili che implementano le stesse funzioni logiche prodotte dalle porte, ma al momento della fabbricazione esse non sono configurate per svolgere una specifica funzione. Essi infatti necessitano di essere programmati ad hoc prima di essere utilizzati in un circuito. Questo tipo di circuito, quindi, rappresenta una sorta di antenato dei moderni dispositivi FPGA; infatti con l’evoluzione delle tecnologie si 5 è avuta una netta crescita del numero di celle e la possibilità di lasciare all’utente il compito di programmare questo tipo di dispositivo. 1.2 Tecnologie di FPGA Un dispositivo FPGA, acronimo di Field Programmable Gate Array, è un circuito integrato che offre delle funzionalità programmabili tramite software caratterizzato da un’elevata scalabilità. Esso in sostanza è una matrice di elementi più o meno articolati - i blocchi logici - e tramite una configurazione di questi ultimi è possibile realizzare una qualsiasi funzione logica desiderata molto complessa o anche una semplice porta come la AND o la XOR. I blocchi, inoltre, includono elementi di memoria semplici, ad esempio i flip-flop, o anche più complessi. Tutti gli elementi sono connessi tra loro e, a loro volta, queste connessioni possono essere programmate per creare diverse strutture di alto livello. Questa tipologia di dispositivi è assimilabile, quindi, ai microcontrollori in quanto riprogrammabili e, nonostante l’architettura resti invariata, le funzionalità implementate variano. D’altra parte, però, essi si avvicinano anche ai dispositivi ASIC poiché le sopra citate funzionalità sono realizzate mediante l’interconnessione delle porte logiche, quindi per via hardware, e non eseguendo uno specifico codice applicativo. Rispetto agli ASIC però introducono alcuni importanti vantaggi: in primo luogo essi sono dispositivi standard le cui funzionalità non vengono imposte dal produttore permettendo, quindi, una produzione più economica e un time to market minore. Tale caratteristica di genericità permette l’impiego di tali dispositivi in applicazioni consumer, comunicazioni, automotive eccetera. [1] D’altronde questo vantaggio riguarda anche l’utente finale, il quale avrà il compito di programmare il circuito, abbattendo così anche i tempi di progettazione, verifica e prova sul campo. Un ulteriore vantaggio è rappresentato dalla possibilità di riprogrammare il dispositivo in qualsiasi momento per effettuare modifiche e correggere gli errori facilmente. Ciò comporta un largo impiego in fase prototipale. Infine l’ambiente di sviluppo è più user-friendly e facile da apprendere. [2] Prima di mostrare nel dettaglio quali sono le attuali tecnologie impiegate per la realizzazione dei circuiti FPGA, passiamo in rassegna quali sono state le sue fasi di 6 evoluzione negli scorsi decenni. L’industria FPGA nasce dalle memorie programmabili di sola lettura (PROM) e dai dispositivi logici programmabili (PLD). Entrambi avevano la possibilità di essere programmati in lotti in fabbrica o “sul campo” (da qui deriva field-programmable). Tuttavia, la logica programmabile era cablata tra le porte logiche. Alla fine del 1980, la Naval Surface Warfare Center finanziò un esperimento proposto da Steve Casselman che prevedeva lo sviluppo di un computer che poteva implementare 600,000 porte riprogrammabili. Casselman ebbe successo e nel 1992 fu emesso un brevetto relativo al sistema. Alcuni dei concetti e tecnologie costituenti l’industria per le reti logiche, porte e blocchi logici programmabili sono basati sui brevetti assegnati a David W. Page e LuVerne R. Peterson nel 1985. Altera fu fondata nel 1983 e consegnò il primo dispositivo logico riprogrammabile del settore nel 1984 – la EP300 – che prevedeva la presenza di una finestra di quarzo nell’imballaggio che permetteva agli utenti di irradiare il die con una lampada ultravioletta per cancellare le celle EPROM che memorizzavano la configurazione del dispositivo. I cofondatori di Xilinx, Ross Freeman e Bernard Vondershmitt, inventarono nel 1985 il primo Field Programmable Gate Array commercialmente redditizio – l’XC2046. Esso aveva sia le porte che le connessioni tra porte programmabili, diede i natali ad una nuova tecnologia e ad un nuovo mercato. L’XC2064 aveva 64 blocchi logici configurabili (CLB), con due look-up-table (LUT) a tre ingressi. Più di 20 anni dopo, Freeman entrò nella National Inventors Hall of Fame per la sua invenzione. Altera e Xilinx continuarono incontrastati e crebbero rapidamente dal 1985 fino alla metà del 1990, quando nacquero alcuni competitors, che guadagnarono significative quote di mercato. Nel 1993 Actel (ora Microsemi) riforniva circa il 18% del mercato. Nel 2010, Altera (31%), Actel (10%) e Xilinx (36%) insieme rappresentavano circa il 77% del mercato degli FPGA. [1] Gli anni ‘90 infatti furono un periodo esplosivo per gli FPGA, sia per quanto riguarda la sofisticazione che per il volume di produzione. Nei primi anni ’90 vennero utilizzati 7 principalmente nel settore delle telecomunicazioni e del networking ed entro la fine del decennio trovarono la loro strada anche tra i consumatori e nelle applicazioni industriali. [3] Andiamo ora a vedere nel dettaglio quali sono le tecnologie che permettono la realizzazione di questo tipo di circuiti. Tre sono le tipologie principalmente utilizzate: Antifuse RAM statica Flash Le architetture e le funzionalità, in taluni casi, possono anche essere molto differenti. Esaminiamole una ad una di seguito. 1.2.1 La tecnologia Antifuse I dispositivi che utilizzano questo tipo di tecnologia implementativa presentano un numero elevatissimo di moduli logici abbastanza semplici, ma con funzionalità prestabilite, interconnessi tramite degli antifusibili. Essi, diversamente dai fusibili, sono di base dei circuiti aperti che si trasformano in cortocircuiti quando sono attraversati da una corrente maggiore di un determinato valore di soglia e, in questo modo, realizzano le interconnessioni tra i vari moduli. Da questo meccanismo di programmazione delle interconnessioni si eredita una proprietà di nonvolatilità dei dispositivi i quali, quindi, conservano la loro configurazione anche in assenza di alimentazione; d’altra parte però questa caratteristica implica anche la non- riprogrammabilità. Diversi sono i vantaggi portati da questo tipo di tecnologia: un’operatività immediata all’accensione, una riduzione dei ritardi di propagazione delle connessioni, della potenza dissipata e dell’area di silicio occupata. Inoltre questo tipo di dispositivi viene ritenuto più affidabile e più protetto da attacchi di hacking o cloning grazie all’assenza di memorie esterne per la configurazione. D’altro canto comporta anche diversi svantaggi: primo tra tutti l’utilizzo di un processo di fabbricazione non standardizzato che ha portato ad una lenta crescita prestazionale. Inoltre 8 una minore capacità logica e la necessità di un programmatore dedicato per la configurazione del dispositivo. Questo tipo di tecnologia rientra nei tipi di FPGA detti OTP, ovvero One Time Programmable. Il principale produttore di FPGA con tecnologia antifuse è Actel che con la serie Axcelerator produce dei dispositivi con capacità logica fino a 4 milioni di porte. Offrono due tipi di moduli logici collegati tra loro tramite una matrice di interconnessioni realizzata con una tecnologia antifusibile proprietaria mostrata in figura. [1] Figura 1.1 Tecnologia antifuse proprietaria per i dispositivi Actel Axelerator. 1.2.2 La tecnologia Flash Questo tipo di tecnologia è concettualmente simile a quella antifuse. Infatti le interconnessioni programmabili sono basate su switch realizzati con una tecnologia flash. In particolare, ogni switch è composto da un transistor con floating-gate, ossia un materiale conduttivo rivestito da un isolante ed interposto tra gate e canale, utile a memorizzare la carica. Un altro transistor viene invece usato per effettuare la verifica della configurazione e il read-back dello switch. Quando esso è abilitato garantisce una connessione a bassa resistenza. Questo tipo di struttura permette l’occupazione di un’area minore rispetto a quella impiegata nei dispositivi con tecnologia SRAM. Inoltre in questo caso è assicurata un’immunità del dispositivo ai firm-error. Anche questa tecnologia possiede la proprietà di non volatilità della configurazione ma, a differenza della antifuse, può essere riprogrammabile grazie a tensioni aggiuntive a quelle 9 utili per il normale funzionamento. Ma per lo stesso motivo il numero di riprogrammazioni non è infinito. Infine per proteggere questo tipo di dispositivi da attacchi di tipo cloning è possibile utilizzare delle chiavi di cifratura del bitstream di configurazione perdendo però la possibilità di riprogrammare l’FPGA. [1] 1.2.3 La tecnologia RAM statica Questa invece è una tecnologia ben diversa da quelle viste in precedenza. In questo caso le funzioni logiche vengono realizzate utilizzando le LUT (look-up-table). Le LUT infatti sono memorie statiche provviste di n bit di indirizzamento; nota una funzione logica e la rispettiva tabella di verità, il valore della funzione per una qualunque combinazione di input viene configurato nella LUT all’indirizzo corrispondente alla stessa combinazione di ingressi. Di conseguenza una LUT a n ingressi può realizzare una qualsivoglia funzione logica a n variabili. Per realizzare le LUT, quindi, vengono utilizzate delle celle di memoria riprogrammabili. Esse poi sono connesse tramite una fitta rete di linee di distribuzione di segnali. Questo tipo di tecnologia porta con sé alcuni vantaggi: una maggiore capacità logica e un maggiore livello di integrazione; migliori prestazioni in termini di frequenza di funzionamento e minori costi a parità di risorse e funzionalità offerte. Inoltre da non sottovalutare l’importanza della possibilità di riprogrammare il dispositivo in fase di debug per poter correggere gli errori di progettazione e permettere di modificare le funzionalità del dispositivo sul campo. D’altro canto però questi dispositivi sono volatili e quindi ad ogni accensione richiedono il caricamento della configurazione da una memoria esterna. Anche qui sono previsti dei meccanismi di cifratura del bitstream per evitare il cloning. I meccanismi di programmazione all’avvio impiegano dei tempi variabili impendendo al dispositivo di essere operativo al power-up. Come ulteriore svantaggio poi c’è una minore tolleranza ai firm-error e una maggiore dissipazione di potenza. Questo tipo di tecnologia viene impiegato dai principali produttori citati prima, ovvero Xilinx e Altera. [1] 10 1.3 Architettura di un FPGA Andiamo ora ad esaminare l’architettura interna generale di un dispositivo FPGA. Si parla di architettura generale poiché i dettagli delle celle logiche possono variare in base al produttore e al modello. In generale si può immaginare che ogni FPGA sia costituito da un numero finito di risorse predefinite con interconnessioni programmabili, che vanno a costituire l’intero circuito digitale riconfigurabile, e da blocchi di I/O per consentire al circuito di comunicare con il mondo esterno. [3] Solitamente l’architettura si basa sul concetto di riga e di colonna. Infatti i suoi blocchi logici configurabili – i CLB (Configurable Logic Block) - sono organizzati con una struttura a matrice. Ai margini di tale matrice si distribuiscono i blocchi di ingresso/uscita - gli IOB (Input Output Block). I CLB si occupano delle funzioni logiche mentre gli IOB si occupano dell’interfacciamento con il mondo esterno. In questa matrice ci sono anche altri tipi di risorse, come i DCM (Digital Clock Manager), la rete che trasporta il segnale di clock ai CLB proveniente dai flip-flop ed altre risorse di calcolo. Ogni elemento ha un proprio modello di funzionamento che permette di comprendere a fondo il corretto funzionamento del circuito. [2] Figura 1.2 Architettura di un FPGA 11 1.3.1 Blocchi logici configurabili Il CLB è l’unità logica fondamentale di un FPGA. Il numero esatto e le funzionalità di questi blocchi variano da dispositivo a dispositivo, ma ogni CLB è costituito da una matrice di comunicazione configurabile con 4 o 6 ingressi, alcuni circuiti di selezione (e.g. Multiplexer) e flip-flop. La matrice di scambio è molto flessibile e può essere configurata per gestire la logica combinatoria, i registri a scorrimento o le RAM. [4] In generale essi sono costituiti da alcune celle logiche (chiamate ALM, LE, slice etc.). Una tipica cella consiste in una LUT a 4 ingressi, un Full Adder (FA) e un flipFigura 1.3 Struttura interna di una CLB flop di tipo D, come mostrato in figura. Le LUT in figura sono divise in due LUT a 3 ingressi. In modalità normale esse sono combinate come un'unica LUT a 4 ingressi attraverso un multiplexer posto a sinistra. In modalità aritmetica, i loro output alimentano il Full Adder. La selezione della modalità è programmata dal multiplexer posto nel mezzo. L’output può essere sia sincrono che asincrono, a seconda della programmazione del multiplexer a destra mostrato nella figura. In pratica, tutto o parte del FA partecipa come funzione nelle LUT per risparmiare spazio. [3] 1.3.2 Blocchi di ingresso/uscita Questi blocchi si occupano della gestione dei segnali da e verso l’esterno del circuito grazie al controllo dei pin del chip. Essi sono posizionati sul perimetro della matrice dei CLB e in particolare possono essere composti da flip-flop, multiplexer e buffer. Inoltre sono presenti delle resistenze di pull-up/pull-down per caratterizzare lo stato del pin in situazioni di alta impedenza. [2] 1.3.3 Linee di interconnessione Queste componenti hanno il compito di far comunicare le altre risorse del dispositivo. Esse si dividono in due tipologie [2]: le linee fisse, che a loro volta si dividono in corte e lunghe. Le prime sono 12 deputate alla connessione di CLB adiacenti in modo da minimizzare il ritardo di propagazione dei segnali. Le linee lunghe invece connettono risorse tra loro distanti 6 CLB con dei percorsi non configurabili che non attraversano le linee di scambio, quindi non introducono ritardi significativi. Le matrici di scambio invece sono reti di pass-transistor programmabili e comunicanti. Nei dispositivi con tecnologia SRAM queste linee sono fisse e vengono programmate le connessioni tra linee diverse. Ragion per cui alcune interconnessioni avvengono tra due lati diversi delle matrici di scambio e vengono abilitate da pass-transistor. 1.4 FPGA Design e Programming In questa sezione verrà illustrata per linee generali l’evoluzione del flusso di progetto che riguarda la programmazione di un FPGA. Solitamente questo flusso segue uno schema come quello illustrato in figura. Figura 1.4 Flusso di progettazione di un FPGA 13 Esaminiamo una ad una queste fasi: 1.4.1 Design Entry Questa prima fase consiste nella descrizione delle funzioni logiche da implementare. Per definire il comportamento dell’FPGA, l’utente dovrà quindi provvedere un progetto in un linguaggio di descrizione dell’hardware (HDL, Hardware Description Language) o sotto forma di schema. I progetti espressi in HDL sono preferibili nel caso in cui si voglia lavorare con strutture di grandi dimensioni poiché è possibile anche solo specificare il numero anziché dover disegnare ogni singolo componente. [3] Il metodo schematico invece, attraverso l’uso di primitive (e.g. flip-flop, porte logiche etc.), prevede la stesura di un vero e proprio disegno del sistema e permette una visione globale e più semplice del progetto. Questa tecnica all’inizio era molto diffusa, ma oggi la progettazione è interamente basata sui linguaggi HDL; tra i più diffusi troviamo Verilog e VHDL. [1] Ciò è dovuto alla crescente complessità dei dispositivi e alla necessità di tempi di sviluppo sempre più piccoli. Esistono anche librerie e supporti che permettono la descrizione dei circuiti logici in Matlab/Simulink con la generazione automatica del codice HDL. Inoltre il SystemC, libreria per il C++, introduce in questo linguaggio il tempo, il parallelismo e la concorrenza, permettendo, quindi, di creare modelli veri e propri di circuiti logici. [1] 1.4.2 Simulazione Subito dopo la fase di design entry abbiamo questa fase, deputata alla verifica che la descrizione effettuata nella fase precedente rispetti il comportamento atteso. Verranno quindi stimolati gli ingressi e si verificherà se i segnali in uscita seguano l’andamento atteso. I tool più diffusi per effettuare questo tipo di test sono ModelSim e ActiveHDL. D’altra parte le descrizioni in SystemC possono essere simulate con una semplice catena di debugger C++ come quella GNU/GCC o Microsoft Visual Studio. [1] 1.4.3 Sintesi In questa fase invece è prevista la traduzione della descrizione del circuito in un formato interpretabile dai tool delle case produttrici di FPGA i quali produrranno il bitstream di 14 programmazione. Questo processo di sintesi estrae le funzionalità logiche dalla descrizione e le traduce in porte logiche, flip-flop e connessioni; si dice che viene generata una netlist il cui formato più diffuso è EDIF. Questa fase ci fornisce anche una indicazione preliminare di risorse occupate e frequenza massima di utilizzo che potrà essere raggiunta. Viene quindi prodotta un'altra descrizione in HDL che si basa direttamente su elementi circuitali, quindi se opportuno si può effettuare di nuovo la fase di simulazione. [1] 1.4.4 Place & Route Dopo la sintesi si effettua un’associazione tra gli elementi della netlist alle risorse del dispositivo, vengono quindi definiti i path (percorsi appunto) di connessione tra queste. Si possono inoltre definire dei vincoli sulla disposizione delle risorse sull’FPGA, sull’associazione dei segnali di i/o o sulla frequenza di funzionamento. Esistono anche qui dei tool per effettuare il place & route che si basano su algoritmi randomici per la disposizione delle risorse. Dopo questa fase viene creato un database contenente i ritardi dei segnali che può essere estratto in un file formato SDF. Questo layout può subire delle verifiche in termini di parametri temporali imposti o requisiti di dissipazione della potenza. [1] 1.4.5 Programmazione e debug in-circuit Ultima fase della progettazione che prevede la generazione dei file di programmazione di FPGA. Per la tecnica antifuse vista in precedenza sarà necessario uno specifico programmatore per configurare il circuito, mentre per la tecnica flash, ad esempio, il file viene convertito e trasferito nella memoria di configurazione del dispositivo attraverso un’interfaccia seriale. Una volta programmato il dispositivo bisognerà solo testarlo. Spesso, a causa della crescente complessità, è impossibile coprire tutti i casi di utilizzo del dispositivo e quindi correggere i bug. Quindi si ricorre ad un debug-in-circuit che permette di osservare l’andamento dei segnali nel tempo durante il reale funzionamento dell’FPGA. Con questa osservazione è possibile risalire all’origine del malfunzionamento per risolverlo. [1] 15 In conclusione in un tipico flusso di progettazione, uno sviluppatore di applicazioni FPGA simulerà il progetto nelle diverse fasi durante tutto il processo di progettazione. Inizialmente la descrizione in VHDL o Verilog viene testata creando dei test benches per simulare il sistema e osservare i risultati. Poi, dopo che il motore di sintesi ha mappato il disegno della netlist, quest’ultima viene tradotta in un livello di descrizione delle porte dove la simulazione viene ripetuta per confermare che la sintesi eseguita sia senza errori. Infine, il progetto viene riportato sull’FPGA a cui possono essere aggiunti dei punti di ritardo di propagazione e viene eseguita di nuovo la simulazione con questi valori che erano stati annotati sulla netlist. Più di recente OpenCL, di cui parleremo più avanti, viene utilizzato dai programmatori per sfruttare le prestazioni e l’efficienza che forniscono gli FPGA. OpenCL permette ai programmatori di sviluppare un codice nel linguaggio di programmazione C e indirizzare le funzioni dell’FPGA come un kernel OpenCL utilizzando i suoi costrutti. [3] 16 Capitolo 2: Dispositivi GPU In questo capitolo verrà illustrata l’architettura e il funzionamento dei dispositivi GPU, in particolare verrà analizzato un suo modello di programmazione che utilizza il linguaggio OpenCL, utile alla programmazione eterogenea. Il concetto di GPU moderno è stato approfondito in gran parte negli ultimi 15 anni. Prima l’elaborazione grafica veniva effettuata con i dispositivi VGA (Video Graphic Array), ovvero dei controller di memoria interfacciati con un’uscita video e dotati di DRAM. Essi praticamente ricevevano i dati delle immagini e dopo averle elaborate le fornivano in uscita attraverso un monitor. Successivamente vennero aggiunti degli acceleratori grafici che permettevano di effettuare operazioni più articolate, ad esempio rasterization, shading e texture mapping. Nel 1999 infatti Nvidia rende popolare il termine GPU commercializzando la GeForce 256 come "prima GPU del mondo", o Graphics Processing Unit, un processore single-chip con integrato sia il transform, clipping and lighting del triangolo, sia macchine per il rendering, capaci di processare un minimo di 10 milioni di poligoni al secondo. La rivale ATI Technologies poi ha coniato il termine unità di elaborazione visiva o VPU con il rilascio della Radeon 9700 nel 2002. Una GPU (Graphics Processing Unit), anche chiamata Visual Processing Unit (VPU), è un circuito elettronico specializzato. Esso è stato progettato per manipolare e modificare rapidamente la memoria per accelerare la creazione di immagini in un frame buffer destinato all’output a video. Le GPU vengono principalmente utilizzate nei sistemi embedded, telefoni cellulare, personal computer, workstation e console di gioco. Le moderne GPU sono molto efficienti per la computer grafica e il processing di immagini, inoltre la loro struttura altamente 17 parallela le rende più efficaci rispetto alle CPU general-purpose per gli algoritmi in cui l’elaborazione di grandi blocchi di dati visivi è fatto in parallelo. Un esempio di impiego di una GPU può essere ad esempio in un personal computer sia su una scheda video che integrato sulla scheda madre o sul die della CPU stesso. [6] 2.1 Architettura di un dispositivo GPU L’evoluzione veloce dell’hardware e le diverse tecniche di progettazione dei vari produttori non ci consentono di definire un concetto di architettura generale di GPU, ma nonostante ciò le somiglianze tra i vari modelli sono molte. Verrà quindi illustrata una struttura di una generica GPU moderna mostrata in figura 2.1 Figura 2.1 Architettura di una moderna GPU Il primo concetto da chiarire è che le GPU sono dei coprocessori e per questo motivo necessitano di essere connesse ad un host, ovvero un processore tradizionale. Questo collegamento avviene attraverso un bus PCI Express ad alta velocità che garantisce il trasferimento dei dati dalla memoria della CPU a quella della GPU e viceversa. C’è da dire anche che i processori grafici raggiungono un alto livello di parallelismo, in particolare nell’architettura dei dispositivi di memorizzazione e nell’organizzazione dei 18 core d’esecuzione. Ogni GPU infatti è costituita da svariate unità di elaborazione dette SM (Streaming Multiprocessor) che rappresentano il primo livello logico di parallelismo e appunto per questo ognuna di queste unità riesce a lavorare contemporaneamente e indipendentemente dalle altre. Ogni SM è a sua volta composto da un insieme di SP (Streaming Processor), ognuno dei quali è un core d’esecuzione reale che può eseguire un thread sequenzialmente. Questi elementi rappresentano l’unità logica d’esecuzione più piccola e rappresentano il livello di parallelismo più fine. Oltre alla divisione di natura strutturale tra SP e SM, possiamo delineare un’altra organizzazione logica tra gli SP di una stessa GPU; essi infatti sono raggruppati in blocchi logici che eseguono in una particolare modalità, ovvero tutti i core di un gruppo eseguono contemporaneamente la stessa istruzione similmente a quanto avviene nel modello SIMD (Single Instruction, Multiple Data). Possiamo aggiungere inoltre che ogni processore grafico possiede diversi tipi di memoria; ognuna di queste è posizionata in aree diverse del dispositivo, ha delle caratteristiche diverse e può essere sfruttata per eseguire operazioni differenti. L’unità di memoria con maggiore capacità è la memoria globale, la quale può raggiungere una dimensione di diversi GB ma con un’alta latenza. A questa memoria possono accedere tutti i core della GPU e anche l’host, il quale è collegato in maniera diretta al dispositivo di memorizzazione. Essa infatti è spesso usata per memorizzare grandi quantità di dati trasferiti dalla memoria del processore e, non essendo molto veloce, possiede meccanismi di ottimizzazione degli accessi o livelli di cache che rendono più efficienti le comunicazioni con la GPU. Un ulteriore spazio di memoria è la texture memory, accessibile a tutti i core e utilizzabile dalla GPU in sola lettura. In principio è stata progettata per memorizzare immagini particolari, appunto le texture, che venivano utilizzate per la rappresentazione di elementi tridimensionali. Essa dispone di meccanismi specifici di caching che la rendono efficiente nell’accesso ad elementi vicini e nella memorizzazione di strutture bidimensionali. All’interno degli SM troviamo uno spazio di memoria condiviso tra i core costituenti un workgroup, essa è più piccola (dell’ordine di alcune decine di MB) e veloce rispetto a 19 quella globale. Usualmente viene utilizzata per conservare valori utilizzati frequentemente da core diversi in maniera da minimizzare gli accessi alla memoria globale. Questo spazio di memoria però è afflitto da i soliti problemi di concorrenza, quindi bisogna assicurare la coerenza dei valori memorizzati a costo di sequenzializzare a volte gli accessi alle stesse locazioni di memoria. Infine ogni SM dispone di determinati registri, che svolgono il ruolo di aree di memoria ad accesso rapido, non condiviso tra i core (locale), temporaneo e di dimensioni limitate; essi infatti vengono utilizzati per memorizzare valori utilizzati frequentemente da un singolo core. [7] 2.2 Modello GPGPU La GPGPU, acronimo di general-purpose computing on graphics processing units, è un settore dell’informatica che si pone l’obiettivo di utilizzare la GPU non solo per la tradizionale creazione di immagini tridimensionali, ma di impiegarla per elaborazioni che richiedono un’elevata potenza di elaborazione e che una CPU non è in grado di soddisfare. Tali elaborazioni sono di tipo parallelo e per questo sono in grado di sfruttare a pieno l’architettura tipica della GPU illustrata in precedenza che, essendo completamente diversa da quella dei tradizionali processori, richiede tecniche di programmazione differenti. Accanto a questo parallelismo abbiamo anche un’estrema programmabilità, introdotta nell’ultimo decennio, che ha portato un aumento di potenza elaborativa e di versatilità. [11] La caratteristica più importante che contraddistingue le GPU è l’elevato numero di core disponibili che permette un’esecuzione concorrente di più thread parzialmente sincronizzati nell’esecuzione della stessa istruzione. Questo vantaggio però è traibile solo quando è possibile suddividere il lavoro in tante parti e effettuando le stesse operazioni su dati diversi; se ciò non è verificato o se esistono un ordine logico e una sequenzialità nelle operazioni da svolgere, questa architettura non è utilizzabile al meglio. Le applicazioni di questo tipo sono solo una parte ristretta del panorama software poiché richiedono un’elevata parallelizzazione del codice, caratteristica tipica di alcuni problemi scientifici 20 ma non di tutti. Per questo motivo si dice che le istruzioni della GPU sono SIMD (Single Instruction, Multiple Data) e con un piccolo esempio possiamo mostrare una transizione di codice da CPU a GPU: ipotizziamo di voler incrementare di uno tutti gli elementi di una matrice 3x3. Con approccio tradizionale il problema si risolve con due cicli for innestati che scorrendo tutti gli elementi li modificano incrementandoli. Su una GPU invece una singola operazione SIMD può modificare tutti gli elementi contemporaneamente. Un punto debole invece delle GPU sono i branch poiché, nel caso in cui non si vuole far eseguire lo stesso salto a tutti gli elementi di un blocco computazionale, l’operazione diventa seriale, causando un drastico rallentamento dell’esecuzione. Diversi sono i benefici che si possono trarre dall’utilizzo di una GPU per svolgere elaborazioni diverse da quelle grafiche. Le più importanti sono: [11] Vantaggi prestazionali: come detto prima la potenza teorica offerta da questo tipo di utilizzo delle GPU è uno dei principali vantaggi e porta in sé una riduzione del tempo di elaborazione rispetto all’utilizzo di una CPU. Sicuramente l’elaborazione del codice verso la GPU richiede un’ottimizzazione dell’applicazione che in casi estremi si traduce in una riscrittura totale del codice, ovviamente gli sviluppatori devono essere a conoscenza di tale “rischio” ma in termini di prestazioni si possono ottenere miglioramenti anche del 100% con rispettive riduzioni dei tempi di calcolo. Costo d’acquisto: i costi di una GPU e di una CPU della stessa fascia di mercato sono abbastanza simili però c’è da considerare che, a parità di prezzo, la GPU offre delle prestazioni teoriche superiori. Ciò mostra che ottimizzare i software per il funzionamento su queste architetture porta un miglioramento notevole delle prestazioni e dell’efficienza pur mantenendo i costi costanti. Tasso di aggiornamento tecnologico: rispetto alle CPU i processori grafici hanno un’evoluzione tecnologica più veloce. Infatti una nuova architettura di GPU dura dai 12 ai 18 mesi massimo, contro i quasi 2 anni delle CPU, e 21 introduce un raddoppio puro della potenza elaborativa ad ogni nuova generazione, mentre per le CPU abbiamo un aumento delle prestazioni solo del 20-30%. Consumo/prestazioni: l’alta potenza elaborativa teorica compensa l’alto livello di consumo energetico e quindi questo rapporto è migliore rispetto a quello delle CPU nonostante i produttori di quest’ultime si siano impegnate molto negli ultimi anni sul fronte dell’efficienza energetica. 2.3 Approcci alla programmazione GP-GPU Il fatto che i processori grafici abbiano introdotto questo grande aumento di performance ha fatto pensare agli sviluppatori di poter impiegare le GPU come piattaforme per effettuare calcoli general purpose. Ciò ha dato il via alla nascita e alla rapida evoluzione di nuovi linguaggi di programmazione per questo ambiente. Appunto per questo nel 2006 furono presentati CUDA e Stream, ovvero due interfacce per la programmazione grafica ideate dai due maggiori costruttori di GPU ovvero NVIDIA e AMD. CUDA in particolare offre estensioni per i più utilizzati linguaggi di programmazione e anche integrazioni negli IDE come Eclipse e Visual Studio. Dal 2010 al 2012 esso ha triplicato il numero dei suoi sviluppatori attivi, eloquente segno dell’interesse sempre crescente che questa tecnologia sta sviluppando nell’ambiente della programmazione parallela. Qualche anno dopo l’uscita di CUDA venne avviato il progetto OpenCL il cui scopo era quello di creare un framework d’esecuzione eterogeneo sul quale potessero lavorare, non solo GPU di differenti produttori, ma anche CPU di diverse tipologie. Questa soluzione pur essendo la più versatile e completa non ha la stessa facilità di utilizzo e maturità di CUDA. Con il passare degli anni questi linguaggi si sono evoluti avvicinandosi sempre di più ai comuni linguaggi di programmazione general purpose e aggiungendo dei meccanismi di controllo sempre più efficaci. Attualmente OpenCL e CUDA sono le soluzioni più utilizzate e efficienti per poter usufruire a pieno della potenza di calcolo delle GPU. In particolare nel capitolo successivo ci occuperemo di esclusivamente OpenCL. 22 Capitolo 3: OpenCL Negli ultimi anni l'informatica è entrata in una nuova era denominata dell'informatica eterogenea, la quale si basa sul concetto di unire in un singolo dispositivo il meglio delle CPU e delle GPU. Da un lato i progettisti creano una sempre più vasta gamma di macchine eterogenee, e dall’altra i rivenditori di supporti hardware li rendono ampiamente disponibili. Questo cambiamento di hardware offre grandi piattaforme per applicazioni nuove e interessanti. Ma, poiché i progetti sono molto diversi, i classici modelli di programmazione non sono adatti ed è quindi risultato importante apprenderne di nuovi come quelli in OpenCL. [8] Quando il progetto di OpenCL partì, i progettisti notarono che per una classe di algoritmi focalizzati sulla latenza, gli sviluppatori scrivevano il codice in C e C++ e lo eseguivano su una CPU, ma per un'altra classe di algoritmi focalizzati sul throughput, gli sviluppatori spesso scrivevano il codice in CUDA e usavano le GPU: questi due approcci sono legati tra loro ma d’altro canto anche separati dal fatto che ciascuno lavora su un tipo di processore; il C++ non esegue su una GPU, CUDA non esegue su una CPU. Quindi gli sviluppatori dovevano specializzarsi in uno dei due linguaggi ignorando l'altro. Proprio per questo la vera potenza di un dispositivo eterogeneo è che esso può eseguire efficientemente applicazioni che mischiano entrambe le classi di algoritmi. Il problema è: come programmare tali macchine? Una soluzione è quella di aggiungere nuove funzionalità alle piattaforme già esistenti; sia C++ che CUDA si stanno evolvendo attivamente per affrontare la sfida di un nuovo hardware. Un'altra soluzione è stata quella di creare un nuovo set di astrazioni di programmazione mirate specificamente per l'informatica eterogenea. Apple infatti si 23 avvicinò con una prima proposta per tale nuovo paradigma. Questa proposta poi è stata raffinata da team tecnici di diverse aziende, diventando infine OpenCL. Ci furono tantissimi miglioramenti per il linguaggio kernel: Permettere agli sviluppatori di scrivere i kernel in un unico linguaggio di Permettere a questi kernel di essere funzionalmente portabili rispetto alle partenza. CPU, GPU, FPGA e altri tipi di dispositivi. Restare di basso livello così che gli sviluppatori possano dipanare tutte le performance di ogni dispositivo. Mantenere il modello abbastanza astratto, per fare in modo che lo stesso codice lavori correttamente su macchine prodotte da diverse società. Naturalmente, come qualsiasi progetto informatico, tutto ciò è stato fatto velocemente. Per velocizzare l'implementazione, è stato scelto di basare il linguaggio su C99. In meno di 6 mesi sono state prodotte le specifiche per OpenCL 1.0, entro 1 anno sono apparse le prime implementazioni, dopodiché OpenCL incontrò i veri sviluppatori. In primo luogo, gli sviluppatori C hanno evidenziato tutte le grandi caratteristiche del C++ che lo hanno reso più produttivo. Gli sviluppatori CUDA invece hanno indicato tutte le nuove caratteristiche che NVIDIA aveva introdotto in CUDA, le quali resero i programmi più semplici e veloci. In secondo luogo, hanno esplorato l'informatica eterogenea come architetti e hanno capito come rimuovere le prime restrizioni le quali richiedevano che CPU e GPU avessero memorie separate. Un grande cambiamento hardware fu infatti lo sviluppo di dispositivi integrati, i quali forniscono su un unico die sia una GPU e una CPU. Inoltre, anche se la specifica è stata scritta con molta cura e con un ambiente conforme, gli implementatori del compilatore non sempre leggono le specifiche alla stessa maniera, ovvero a volte lo stesso programma può offrire risposte diverse su dispositivi differenti. Tutto ciò portò ad una rivisitata e, più matura, specifica ossia OpenCL 2.0. La nuova specifica infatti ha rappresentato un’evoluzione significativa e ha permesso agli 24 sviluppatori di trarre vantaggio dai nuovi processori integrati GPU/CPU. I grandi cambiamenti sono stati i seguenti: Memoria virtuale condivisa: per fare in modo che il codice dell’host e del dispositivo possono condividere strutture pointer-based complesse come gli alberi e le liste linkate, liberandosi del costoso trasferimento di dati tra host e dispositivi. Parallelismo dinamico: per fare in modo che i kernel del dispositivo possono lanciare dei task allo stesso dispositivo senza l'interazione dell'host, sbarazzandosi dei colli di bottiglia. Spazi di indirizzamento generici: per permettere alle singole funzioni di poter operare sia su dati della GPU che della CPU, rendendo la programmazione più facile. Atomiche stile C++: così che i work-item possono condividere dati attraverso i work-group e i dispositivi, consentendo ad una più ampia classe di algoritmi di essere realizzati in OpenCL. 3.1 Programmazione eterogenea In questo paragrafo introdurremo un nuovo campo dell'informatica nato proprio dall'esigenza di poter utilizzare device più prestanti, come ad esempio le GPU, per scopi computazionali. La programmazione eterogenea include in sé sia l’elaborazione parallela che concorrente. Con essa infatti i task che compongono un’applicazione sono mappati sul migliore dispositivo di elaborazione disponibile nel sistema. La presenza di più device in un sistema offre l’opportunità ai programmi di utilizzare la concorrenza e il parallelismo per migliorare sia performance che potenza. Appunto per questo OpenCL è un linguaggio di programmazione sviluppato specificamente a supporto degli ambienti di programmazione eterogenea. Oggigiorno gli ambienti di programmazione eterogenea sono sempre più sfaccettati e sfruttano le capacità di una vasta gamma di microprocessori multicore, CPU, DSP, FPGA 25 e GPU. Presentato con tutta questa eterogeneità, il processo che prevede il mapping dei task software su un così grande gruppo di architetture pone al mondo dei programmatori un certo numero di sfide. Le applicazioni eterogenee di solito includono un mix di funzionamenti in base al carico di lavoro, che vanno da quelli control intensive (e.g. ricerca, confronto, ordinamento) a quelli data intensive (e.g. processing di immagini, simulazioni di modelli, data mining). Alcuni task inoltre possono essere classificati come compute intensive (e.g. metodi iterativi, metodi numerici, modellazione finanziaria) in cui il throughput complessivo del task è fortemente dipendente dall’efficienza computazionale del dispositivo hardware sottostante. Ognuna di queste classi solitamente viene eseguita in maniera più efficiente su uno specifico tipo di architettura hardware, non esiste infatti un dispositivo ottimo che esegua tutte le classi prima elencate. Ad esempio, le applicazioni control intensive tendono ad eseguire più velocemente sulle CPU superscalari, in cui una parte significante del die è stata destinata ai meccanismi di predizione dei branch. Invece le applicazioni data intensive tendono ad eseguire più velocemente sulle architetture vettoriali, in cui la stessa operazione è applicata a diversi dati e più operazioni vengono eseguite in parallelo. [8] 3.2 Breve panoramica della specifica di OpenCL Illustriamo brevemente in questo paragrafo la specifica di OpenCL e un suo tipico modello di esecuzione. La specifica di OpenCL è definita in quattro parti fondamentali, ognuna delle quali viene indicata come modello. I modelli sono riassunti qui di seguito senza entrare nel dettaglio ma indicando solo il ruolo che ricopre ognuno di esso. Modello di Piattaforma: specifica che c’è un processore host, che coordina l’esecuzione, e uno o più processori device il cui lavoro è quello di eseguire il kernel OpenCL C. Esso inoltre definisce un modello di hardware astratto per i dispositivi. Modello di Esecuzione: definisce come l’ambiente OpenCL è configurato dall’host e come quest’ultimo può indirizzare i dispositivi ad eseguire i compiti. Esso comprende la definizione di un’ambiente per l’esecuzione 26 sull’host, i meccanismi per l’interazione e il modello di concorrenza usato al momento della configurazione dei kernel. Il modello di concorrenza definisce come un algoritmo viene decomposto in work-items e work-group OpenCL. Modello di programmazione kernel: definisce come il modello di concorrenza viene mappato sull’hardware fisico. Modello di memoria: definisce i tipi di oggetti della memoria e la gerarchia della memoria astratta che i kernel usano senza curarsi della reale architettura della memoria sottostante. Esso inoltre contiene i requisiti per l’ordinazione della memoria e per l’eventuale memoria virtuale condivisa tra l’host e i dispositivi. In uno scenario tipico possiamo osservare un’implementazione OpenCL che esegue su una piattaforma composta da un x86 CPU come host la quale usa una GPU come un acceleratore. L’host imposta un kernel da far eseguire alla GPU e manda il comando alla GPU di eseguire il kernel con un certo grado di parallelismo. Questo rappresenta il modello di esecuzione. La memoria per i dati utilizzati dal kernel è allocata dal programmatore in una parte specifica della gerarchia della memoria astratta specificata dal modello di memoria. Infine la GPU crea i thread hardware per eseguire il kernel e li mappa nelle sue unità hardware. Ciò viene fatto usando il modello di programmazione. [8] 3.3 OpenCL su FPGA Andiamo ora a illustrare qual è il ruolo di OpenCL quando si vuole programmare un dispositivo FPGA. Come visto nel primo capitolo, il design e il programming di questi dispositivi è organizzato in più fasi; in questo caso OpenCL entra in gioco proprio nella prima fase, ovvero quella di design entry. In questo paragrafo prenderemo come esempio di design un FPGA Altera. Come visto in precedenza lo standard OpenCL, oltre a fornire un modello portabile, offre la possibilità di descrivere degli algoritmi paralleli da implementare sugli FPGA ad un livello di astrazione più alto rispetto ai linguaggi di descrizione hardware (HDL) come 27 VHDL o Verilog. Sebbene esistano diversi strumenti di sintesi ad alto livello che raggiungono questo livello di astrazione, essi soffrono dello stesso problema fondamentale: questi tool hanno tentato di prendere un programma C sequenziale e produrre un’implementazione HDL parallela. La difficoltà non stava tanto nella creazione dell’implementazione HDL ma piuttosto nell’estrazione di un parallelismo di livello thread che avrebbe permesso l’implementazione dell’FPGA per raggiungere alte prestazioni. Figura 3.1 Recente trend delle tecnologie programmabili e parallele Essendo gli FPGA sull’estremo più lontano dello spettro di parallelismo, ogni fallimento nell’estrazione del parallelismo massimo è più paralizzante che su altri device. Lo standard OpenCL risolve alcuni di questi problemi permettendo al programmatore di specificare esplicitamente e controllare il parallelismo. Appunto per questo tale standard si abbina più naturalmente alla natura altamente parallela degli FPGA rispetto ai programmi sequenziali descritti in C puro. Andiamo ora a vedere come avviene questa implementazione in OpenCL. Le applicazioni OpenCL sono strutturate in due parti. Il programma OpenCL dell’host è una semplice routine software, scritta in C/C++, che esegue su qualsiasi tipo di microprocessore. Esso infatti può essere, ad esempio, un processore soft embedded su un FPGA, un processore hard ARM o uno x86 esterno come mostrato in figura 3.2. 28 Figura 3.2 Schema di un'applicazione OpenCL Ad un certo punto durante l’esecuzione di questa routine software host ci potrebbe essere una funzione che è computazionalmente costosa e potrebbe beneficiare dell’accelerazione altamente parallela su un device tipo CPU, GPU, FPGA etc. Questa funzione per essere accelerata viene indirizzata come un kernel OpenCL. Essa viene scritta con lo standard C però viene annotata con i costrutti per specificare il grado di parallelismo e la gerarchia della memoria. L’esempio mostrato in figura 3.3 esegue la somma vettoriale di due array, A e B, mentre scrive il risultato in un array di output. I thread paralleli operano su ogni elemento del vettore permettendo al risultato di essere calcolato molto più velocemente quando viene accelerato da un device che offre un enorme quantità di parallelismo finemente granulare come accade in un FPGA. Il programma host ha accesso alle API (Application programming interface) dello standard OpenCL che permettono ai dati di essere trasferiti ad un FPGA invocando il kernel su quest’ultimo e ritornando i dati risultanti. 29 Figura 3.3 Esempio di implementazione OpenCL su un FPGA Diversamente dalle CPU e dalle GPU, in cui i thread paralleli possono essere eseguiti su core differenti, gli FPGA offrono una strategia diversa. Le funzioni kernel possono essere trasformate in circuiti hardware in pipeline dedicati che sono appropriatamente utilizzati in maniera multithread utilizzando il concetto di parallelismo della pipeline. Ognuna di queste pipeline può essere replicata diverse volte per fornire ancora più parallelismo di quanto sia possibile con una sola pipeline. Il compilatore OpenCL di Altera traduce un kernel in un hardware creando un circuito che implementa tutte le operazioni. Questi circuiti sono connessi tra loro per simulare il flusso dei dati nel kernel. Nella somma vettoriale del nostro esempio la traduzione in hardware risulterà una semplice feedforward pipeline. I caricamenti dagli array A e B sono convertiti in unità di carico, le quali sono piccoli circuiti responsabili del rilascio di indirizzi di una memoria esterna e l’elaborazione dei dati restituiti. I due valori di ritorno vanno ad alimentare direttamente un addizionatore responsabile dell’addizione floating-point. Infine, il risultato del sommatore è collegato direttamente ad un'unità di deposito che scrive la somma nella memoria esterna. Il concetto più importante dietro il compilatore OpenCL-to-FPGA è la nozione di parallelismo pipeline. Per semplicità, assumiamo che il compilatore abbia creato tre stadi di pipeline per il kernel, come mostrato in figura 3.4. Al primo ciclo di clock il thread 0 viene clockato nelle due unità di caricamento, questo indica che devono iniziare la fase di fetch dei primi elementi dai vettori A e B. Al secondo ciclo di clock il thread 1 viene clockato nello stesso momento in cui il thread 0 ha completato la sua lettura dalla memoria 30 e memorizza il risultato nei registri successivi alle unità di caricamento. Al terzo ciclo tocca al thread 2, il thread 1 acquisisce i dati che ha restituito e il thread 0 memorizza la somma dei due valori che aveva caricato. È evidente che a regime tutte le parti della pipeline sono attive e ogni fase di processing con un thread diverso. Figura 3.4 Pipelined Information La figura 3.4 mostra una rappresentazione di alto livello di un sistema OpenCL completo che contiene diverse pipeline kernel e il circuito che connette queste pipeline alle interfacce dati off-chip. Oltre alle kernel pipeline il compilatore OpenCL di Altera crea delle interfacce per la memoria interna ed esterna. Le unità di load and store per ogni pipeline sono connesse alla memoria esterna tramite una struttura di interconnessione globale che gestisce le richieste multiple ad un gruppo di DDR DIMMs. Similarmente, gli accessi alla memoria locale OpenCL sono collegati attraverso una struttura di interconnessione specializzata per RAM on-chip M9K. Queste strutture di interconnessione vengono progettate per assicurare operatività ad alte frequenze e un’efficiente organizzazione delle richieste di memoria. 31 Figura 3.5 Implementazione di un sistema OpenCL Vediamo ora quali sono i benefici portati dall’implementazione dello standard OpenCL su un FPGA. La creazione dei progetti per gli FPGA usando una descrizione OpenCL offre diversi vantaggi in confronto ad una metodologia tradizionale basata sul design HDL. Lo sviluppo dei software per i dispositivi programmabili segue tipicamente il flusso in cui prima si concepisce un’idea, si codifica l’algoritmo in un linguaggio di alto livello come il C e poi si usa un compilatore automatico per creare uno stream di istruzioni. Questo approccio può essere contrastato con le tradizionali metodologie di design basate su FPGA. Infatti gran parte del carico è posto sul progettista per creare delle descrizioni dell’hardware “ciclo per ciclo” che vengono utilizzate per implementare gli algoritmi. Il flusso tradizionale prevede la creazione di datapath e state machine per controllare tali datapath, la connessione ai core IP di basso livello attraverso strumenti di livello di sistema (e.g. SOPC Builder, Platform Studio) e la gestione di problemi di sincronizzazione dato che le interfacce esterne impongono vincoli fissi che devono essere soddisfatti. L’obiettivo di un compilatore OpenCL è quello di eseguire tutti questi step automaticamente per i progettisti consentendogli di concentrarsi sulla definizione degli algoritmi piuttosto che sui dettagli noiosi di progettazione hardware. Progettare in questo modo permette al designer di migrare facilmente verso le nuove FPGA che offrono performance migliori e capacità più elevate poiché il compilatore OpenCL trasformerà la stessa descrizione di alto livello in pipeline che sfruttano le nuove FPGA. [9] 32 Conclusioni In conclusione, quindi, abbiamo mostrato che la nascita di nuovi dispositivi programmabili come gli FPGA hanno rivoluzionato il mondo dell’informatica e dell’elettronica digitale, permettendo un riutilizzo dell’hardware e abbattendo anche i costi e i tempi di produzione di tali dispositivi. In particolare l’evoluzione delle architetture ha portato ad una struttura moderna degli FPGA che permette l’implementazione di qualsivoglia funzione logica ad un costo minimo. Inoltre un forte vantaggio che si può trarre è che il debugging delle applicazioni viene fatto più tardi possibile e nella maniera più semplice ed economica. Per quanto riguarda invece le GPU, abbiamo visto che il nascere di queste nuove unità di elaborazione grafiche ha giovato anche al mondo del calcolo parallelo proprio grazie alla potenza computazionale intrinseca in questi dispositivi. Quindi, abbandonando l’idea della semplice elaborazione di immagini e accogliendo questi device come acceleratori di calcolo, nasce il modello GPGPU, ovvero una GPU general purpose che permette di raggiungere un elevato grado di parallelismo nelle applicazioni. Tutti questi vantaggi sono traibili ovviamente tramite una buona programmazione e la stesura di algoritmi che supportino un determinato livello di parallelismo; a ciò viene in aiuto quindi il linguaggio OpenCL che introduce una sorta di traduzione degli algoritmi seriali in routine parallele che dipende dall’architettura hardware sottostante. 33 Bibliografia [1] [2] [3] [4] [5] [6] [7] [8] [9] Mariano Severi, “CorsoFPGA”, Elettronica In. Wikipedia italia, FPGA, https://it.wikipedia.org/wiki/Field_Programmable_Gate_Array , 04/11/2015. Wikipedia, Field-Programmable Gate Array, https://en.wikipedia.org/wiki/Field-programmable_gate_array, 04/11/2015. National Instruments, http://www.ni.com/white-paper/6983/en/, 04/11/2015. Xilinx, http://www.xilinx.com/fpga/, 04/11/2015 Wikipedia, Graphics processing unit, https://en.wikipedia.org/wiki/Graphics_processing_unit, 06/11/2015. David B. Kirk and Wen-mei W. Hwu. Programming Massively Parallel Processors: A Hands-on Approach. Morgan Kaufmann Publishers Inc., 2010. David Kaeli, Perhaas Mistry, Dana Schaa, Dong Ping Zhang. Heterogeneous Computing with OpenCL 2.0 Third Edition. Morgan Kaufmann Publishers Inc., 2015, pagg 1-6, 33-36, 41-43 Altera.com, https://www.altera.com/en_US/pdfs/literature/wp/wp-01173-opencl.pdf, 15/11/2015 [10] Altera.com, Implementing FPGA Design with the OpenCL Standard https://www.altera.com/products/design-software/embedded-softwaredevelopers/opencl/overview.html, pag 3-7, 15/11/2015 34 [11] Wikipedia, GPGPU, https://it.wikipedia.org/wiki/GPGPU, 6/11/2015 35