Universita degli studi di Firenze
FACOLTA DI SCIENZE MATEMATICHE, FISICHE E NATURALI
Corso di Laurea in Informatica
Implementazione efficiente del Metodo
dei Gradienti Coniugati in ambiente
CUDA (Compute Unified DeviceArchitecture)
Tesi di Laurea in Informatica
Relatore:Prof. Luigi Brugnano
Candidato:Stefano Brilli
Anno Accademico 2007-2008
2
Indice
Lista degli Acronimi 5
Premessa 7
Introduzione alla Grafica Computazionale Tridimensionale 9
1 Architettura di una GPU 13
1.1 Evoluzione delle GPU . . . . . . . . . . . . . . . . . . . . . . . . 13
1.2 Il GPU Computing . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.3 Confronto fra le architetture . . . . . . . . . . . . . . . . . . . . . 16
2 Il GPU computing di NVIDIA 19
2.1 L’architettura Tesla . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.1.1 Il flusso delle operazioni . . . . . . . . . . . . . . . . . . . 19
2.1.2 Componenti dello Scalable Processor Array (SPA) . . . . . 21
2.1.3 Un’architettura di tipo SIMT . . . . . . . . . . . . . . . . 24
2.1.4 Operazioni in virgola mobile . . . . . . . . . . . . . . . . . 25
2.2 CUDA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.2.1 Gerarchia dei thread . . . . . . . . . . . . . . . . . . . . . 27
2.2.2 Gerarchia della memoria . . . . . . . . . . . . . . . . . . . 30
2.2.3 Sincronizzazione . . . . . . . . . . . . . . . . . . . . . . . . 33
2.2.4 Compute capability . . . . . . . . . . . . . . . . . . . . . . 33
2.2.5 Uso della piattaforma . . . . . . . . . . . . . . . . . . . . . 34
3 Il Lavoro di Implementazione 35
3.1 Studio iniziale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.1.1 L’ambiente Matlab . . . . . . . . . . . . . . . . . . . . . . 35
3.1.2 L’implementazione Matlab e le strutture dati . . . . . . . . 39
3
3.1.3 Legge di Amdhal . . . . . . . . . . . . . . . . . . . . . . . 41
3.1.4 Prima analisi delle prestazioni . . . . . . . . . . . . . . . . 42
3.2 Prima implementazione . . . . . . . . . . . . . . . . . . . . . . . . 43
3.2.1 Implementazione Host: cmatvec . . . . . . . . . . . . . . . 44
3.2.2 Implementazione CUDA: nvmatvec . . . . . . . . . . . . . 46
3.3 Seconda Implementazione . . . . . . . . . . . . . . . . . . . . . . 48
3.3.1 Implementazione Host . . . . . . . . . . . . . . . . . . . . 50
3.3.2 Implementazione Device . . . . . . . . . . . . . . . . . . . 51
3.4 Implementazione Finale . . . . . . . . . . . . . . . . . . . . . . . 51
3.4.1 Implementazione Host: ccgmamma . . . . . . . . . . . . . . 52
3.4.2 Implementazione Device: nvcgmamma . . . . . . . . . . . . 52
4 Test del software 55
4.1 Precisione dei risultati . . . . . . . . . . . . . . . . . . . . . . . . 55
4.2 Tempi di esecuzione . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.2.1 Esecuzione in ambiente Matlab/Linux . . . . . . . . . . . 58
4.2.2 Esecuzione in ambiente Windows . . . . . . . . . . . . . . 59
4.3 Rapporto prestazioni/prezzo . . . . . . . . . . . . . . . . . . . . . 61
5 Conclusioni 63
Bibliografia 65
Elenco delle tabelle 69
Elenco delle figure 71
4
Lista degli Acronimi
ALU Arithmetic and Logical Unit
API Application Programming
Interface
CAD Computer Aided Design
CG Computer Graphics
CPU Central Processing Unit
CUDA Compute Unified Device
Architecture
DRAM Dynamic Random Access
Memory
Flops Floating point Operations
Per Second
GCC GNU C Compiler
GPGPU General Purpose computing
on GPU
GPU Graphics Processing Unit
MAD Multiply-Add
MIMD Multiple Instructions on
Multple Data
MISD Multiple Instructions on
Single Data
MT Issue MultiThreaded instruction
fetch and Issue unit
OpenGL Open Graphics Library
ROP Raster Operation Processor
SDK Software Development Kit
SFU Special Function Unit
SIMD Single Instruction on
Multiple Data
SIMT Single Instruction Multiple
Threads
SISD Single Instruction on Single
Data
SMC Streaming Multiprocessor
Controller
SM Stream Multiprocessor
SPA Scalable Processor Array
SP Streaming Processor
TPC Texture Processor Cluster
5
6
Dal piu antico degli argomenti
trarremo la piu nuova delle scienze
Hermann Ebbinghaus
Premessa
Introdotta ufficialmente nel 2006, la Compute Unified Device Architecture (d’ora
in poi CUDATM
) e una tecnologia realizzata dalla NVIDIA R©1. Questa ha lo scopo
di estendere le capacita di elaborazione delle schede video, a compiti che vanno
oltre la grafica computazionale.
Rispetto alla prima legge di Moore, secondo la quale le prestazioni dei processori
raddoppiano ogni 18 mesi, le Graphics Processing Unit (GPU) delle schede video
hanno dimostrato un raddoppio di prestazioni ogni 6 mesi. Ovvero la prima legge
di Moore elevata al cubo. Questo e stato possibile grazie all’intenso sviluppo delle
architetture parallele sulle quali si basano queste unita. Le loro capacita di calcolo
hanno ormai raggiunto l’ordine del Teraflop (1012 operazioni al secondo) contro gli
attuali 100 GFlop/s della Central Processing Unit (CPU) per personal computer
piu “performante” sul mercato2. E chiaro quindi l’interesse ad apprendere il
funzionamento e le caratteristiche di questo nuovo tipo di unita di calcolo, al fine
di sfruttarne pienamente le potenzialita offerte.
Questo lavoro esamina diversi aspetti della tecnologia CUDA, considerando-
la come una conseguenza diretta del processo evolutivo delle schede video. Si
affronta inoltre la realizzazione di una componente software che utilizza questa
tecnologia, al fine di apprenderne l’utilizzo e valutarne l’efficacia. Tale componen-
te e una implementazione ad hoc del Metodo dei Gradiendi Coniugati per matrici
pentadiagonali, originariamente scritta in linguaggio Matlab. Lo sviluppo di que-
sto software ha dato modo di osservare sia le limitazioni sia le potenzialita di
questa piattaforma.
• Nella prima parte di questa tesi si illustra brevemente cosa sono le GPU, la
1NVIDIA Corporation e una azienda leader nella produzione di processori grafici, schede madrie in generale di prodotti multimediali per personal computer e console.
2I dati si riferiscono alla GPU di NVIDIA GT200 e alla CPU Intel R© XeonTMHarpertown
7
loro evoluzione negli anni e quali sono stati i motivi e che hanno condotto
ad un loro utilizzo nel calcolo parallelo.
• Nella seconda parte si esamina l’architettura delle unita di calcolo prodotte
dalla NVIDIA ed il modello di programmazione utilizzato dall’ambiente
CUDA.
• Nella terza parte si mostrano gli strumenti di sviluppo, le tecniche di pro-
grammazione e le implementazioni dell’applicativo realizzato.
• Nella quarta parte si eseguono i test sulle implementazioni realizzate.
• Nella quinta ed ultima parte si traggono le conclusioni del lavoro svolto.
8
Introduzione alla Grafica
Computazionale Tridimensionale
La Grafica Computazionale Tridimensionale, o Computer Graphics (CG) 3D, e
una branca della CG che usa dati geometrici tridimensionali per la produzione
di immagini bitmap3. Questo processo e suddiviso in due fasi: la prima, in cui
vengono definiti gli oggetti, le prospettive e le illuminazioni; la seconda, in cui si
produce l’immagine. Fra queste, la seconda fase, chiamata fase di rendering, e
molto piu onerosa in termini di prestazioni rispetto alla prima.
Nei personal computer, quando si tratta di produrre una singola immagine,
entrambe le fasi possono essere assegnate alla CPU. Invece, quando il compito e
quello di produrre dalle 30 alle 60 immagini al secondo, la capacita di calcolo della
CPU non e piu sufficiente. Tali compiti sono soprattutto richiesti dai giochi per
computers, i quali percio, delegano il rendering alle moderne schede video. Negli
anni, grazie allo sviluppo dell’industria videoludica, le GPU di queste schede si
sono evolute fino a diventare dei potenti calcolatori paralleli (Figura 0.2). Sono
state dotate di una propria memoria e hanno acquisito la capacita di svolgere
velocemente le complesse operazioni del rendering.
L’intero rendering comprende diverse fasi, che, raggruppate insieme, formano
la pipeline grafica (Figura 0.1).
1. Si inizia con il trasferimento della descrizione di una scena, dalla memoria
centrale della CPU, alla memoria della scheda video. La descrizione di
una scena comprende: l’insieme di vertici che definiscono gli oggetti, le
texture4 che vi verranno applicate, i dati sull’illuminazione, ed il punto di
osservazione della scena.3Immagine bidimensionale definita da una matrice (mappa) di bit4Una texture e l’immagine bitmap usata per rappresentare la superficie di un oggetto.
9
Figura 0.1: Pipeline di rendering di una scena tridimensionale.
2. La seconda fase e quella in cui vengono eseguite le trasformazioni dei verti-
ci. Le rotazioni, gli scaling e le traslazioni degli oggetti sono fondamentali
per la definizione di una scena. Ad esempio, un designer puo realizzare
contemporaneamente sia il modello di un’automobile sia il modello di un
tavolo, senza preoccuparsi del rapporto fra le loro dimensioni. In questa
fase il primo oggetto sara dimensionato in modo da essere piu grande del
secondo.
3. Alla precedente, segue un’altra fase di trasformazione, necessaria per l’ag-
giunta della prospettiva. In questa fase vengono anche eliminati dalla scena
tutte le parti degli oggetti che, per via della camera5, non compariranno
nell’immagine finale.
4. La quarta fase e quella dedicata all’illuminazione degli oggetti. Nel rende-
ring e una delle fasi piu impegnative dal punto di vista computazionale. Per
questo motivo, quando si parla di rendering in tempo reale, l’illuminazione
di un oggetto, in base alle sorgenti di luce, viene calcolata soltanto ai vertici
5Camera e il termine usato per indicare il punto di vista della scena
10
dei suoi poligoni. Durante le fasi successive, i valori dell’illuminazione ai
vertici di un oggetto vengono usati per interpolare i valori di illuminazione
della sua superficie.
5. Si passa quindi alla fase di rasterization, ovvero la trasformazione dei dati
elaborati finora in un’immagine bitmap. Durante questa fase, le coordinate
tridimensionali vengono trasformate in coordinate bidimensionali e le tex-
ture vengono applicate sugli oggetti. Al termine vengono anche applicati
eventuali effetti grafici come ombre, nebbia e antialiasing6, ecc...
6L’antialiasing e una tecnica per ridurre l’effetto aliasing (scalettamento) che si ha quando unsegnale a bassa risoluzione viene mostrato ad alta risoluzione. In questo caso l’antialiasingammorbidisce le linee, smussando i bordi e omogeneizzando l’immagine finale.
11
Figura 0.2: A confronto l’architettura di una CPU multicore, con quella di unGPU moderna: a ugual colore corrisponde ugual funzionalita. NellaGPU la maggior parte della superficie del core e dedicata ad unitaArithmetic and Logical Unit (ALU)
12
1Architettura di una GPU
Questo capitolo si occupa di descrivere cosa sono le GPU, come queste si sono
evolute durante gli anni, e quali sono i compiti a cui assolvono all’interno delle
moderne schede grafiche.
1.1 Evoluzione delle GPU
La GPU e la componente che esegue le operazioni di rendering all’interno di una
scheda video moderna.
I primi chip grafici risalgono all’inizio degli anni 80 e le loro funzioni erano
molto limitate; mancavano totalmente di funzioni per l’elaborazione di scene tri-
dimensionali e avevano un insieme limitato di funzioni per il disegno di scene
bidimensionali.
All’inizio degli anni 90 invece i chip grafici furono sostituiti con vere e proprie
CPU, opportunamente modificate e programmate per fornire le funzioni di di-
segno richieste soprattutto da applicazioni Computer Aided Design (CAD). In
questi anni, anche molte stampanti della Apple hanno utilizzato processori di que-
sto tipo che, a partire da un documento PostScript, producevano un’immagine
bitmap.
Successivamente e stato possibile sviluppare dei chip grafici integrati, i quali for-
nivano tutte le funzioni di accelerazione richieste dalle applicazioni di disegno bidi-
mensionale. Erano meno flessibili delle CPU programmabili ma, il costo inferiore,
13
la maggiore semplicita di fabbricazione e l’avvento di Microsoft c© WindowsTM
ne
facilitarono la diffusione. Quest’ultimo software, infatti, stimolo significativamen-
te l’interesse per la grafica bitmap e, pochi anni dopo, la S3 Graphics c©introdusse
sul mercato il primo chip di accelerazione 2D; in breve tempo le piu costose CPU
grafiche uscirono dal commercio.
A meta degli anni 90 l’uso della grafica tridimensionale inizio a diffondersi sia
nel mercato delle console di gioco che in quello dei personal computer, spingendo
i produttori ad integrare nei chip grafici diverse funzioni di accelerazione 3D. Una
forte influenza all’implementazione di queste ultime e stata data dalla libreria gra-
fica Open Graphics Library (OpenGL), gia apparsa nei primi anni 90. Durante
questo periodo, le implementazioni software dei metodi di OpenGL si semplifica-
rono drasticamente grazie ad un supporto hardware da parte dei chip in continua
crescita. Un’altra libreria che, piu tardi negli anni, ha influenzato l’evoluzione
delle GPU e stata la DirectXTM
di Microsoft. L’approccio meno popolare del
supporto scheda-per-scheda, inizialmente, ne rallento lo sviluppo. Questo riprese
vigore quando Microsoft inizio a lavorare affianco ai progettisti hardware, pro-
grammando le nuove release1 dei DirectX contemporaneamente con nuove schede
video compatibili. La versione 7.0 delle librerie DirectX introdusse il primo sup-
porto per l’accelerazione hardware alle trasformazioni ed alle illuminazioni delle
scene. Fu uno dei primi passi a segnare la trasformazione della pipeline grafica
all’interno della GPU, che avrebbe visto a breve l’introduzione dello shading.
Lo shading e la capacita della GPU di eseguire degli shader, ovvero un insieme
di istruzioni che definisce un algoritmo di trasformazione. Lo shader puo essere
di vertici, di frammento (o di pixel) e di geometria. Ad esempio, Il vertex shader
si occupa di trasformare la posizione dei vertici di un oggetto, gestendone cosı la
forma e la dimensione. Il fragment shader, invece, elabora i singoli pixel di un
oggetto applicandovi texture ed effetti di colorazione.
NVIDIA e stata la prima azienda a produrre un chip con capacita di shading
programmabili e successivamente ATI R©2 introdusse il supporto a shader conte-
nenti istruzioni di ciclo e di calcolo in virgola mobile. In breve tempo le GPU so-
no divenute flessibili come CPU completamente programmabili ed estremamente
“performanti” nell’esecuzione di operazioni con molti dati.
1In ambito informatico release indica una particolare versione di un software resa disponibileai suoi utenti.
2ATI e stata per molti anni la principale societa concorrente di NVIDIA. Nel 2006 e stataacquistata dalla AMD R©, azienda nota principalmente per la produzione di CPU.
14
1.2 Il GPU Computing
L’idea di sfruttare la capacita di calcolo parallelo delle GPU, per compiti differenti
dal rendering, ha portato alla nascita del GPU computing. Le unita di shading, in
particolar modo quelle dedicate ai vertici, sono infatti costruite per l’esecuzione
in parallelo di semplici applicazioni (gli shader), su una grande quantita di dati.
Diversi progetti, come General Purpose computing on GPU (GPGPU), utiliz-
zano le Application Programming Interface (API) di disegno 3D come strumenti
per la programmazione delle GPU. Attraverso queste API e possibile definire uno
shader su insiemi di vertici e texture, in modo tale che questo corrisponda alla
definizione di un’applicazione parallela su un insieme di dati. In questo modo
la GPU, eseguendo lo shader, e in grado di assolvere a compiti general purpose;
ovvero non strettamente legati ad applicazioni di grafica computazionale.
Con l’ultima generazione di GPU le cose sono cambiate. I diversi tipi di sha-
der, che in passato venivano eseguiti su unita diverse e separate fra di loro, adesso
vengono eseguiti su una sola unita di calcolo. Questa unita e capace di elaborare
l’Unified Shader Model, ovvero un modello di shading che utilizza lo stesso insie-
me di istruzioni per la definizione di fragment, vertex o geometry shader. Questa
soluzione presenta molti vantaggi per le applicazioni di rendering, a cominciare
dalla distribuzione del carico di lavoro. Infatti, se nella generazione precedente di
GPU si poteva presentare il caso in cui un tipo di unita di shading era sovracca-
ricata rispetto alle altre, adesso e possibile allocare dinamicamente piu risorse al
compito piu impegnativo.
Oltre ai vantaggi per le applicazioni di rendering, grazie a questo radicale cam-
biamento e stato possibile sviluppare piattaforme di GPU computing come lo
Stream SDK di AMD, Larrabee di Intel (in fase di ultimazione) e CUDA di NVI-
DIA, le quali non sono altro che ambienti di sviluppo per la programmazione della
GPU. Questi hanno la caratteristica di non dover piu ricorrere all’uso delle API
di disegno 3D per elaborare i dati attraverso la scheda video. Si servono, infatti,
di API appositamente sviluppate per la programmazione di applicazioni general
purpose, ovvero di uso generico.
15
−→
←−
Dati
Istr
uzi
oni SISD SIMD
MISD MIMD
Tabella 1.1: Tassonomia di Flynn
1.3 Confronto fra le architetture
Nonostante le principali aziende del settore grafico abbiano realizzato (o stiano
realizzando) un’implementazione dell’Unified Shader Model, le scelte architettu-
rali delle proprie GPU rimangono profondamente diverse. Al momento sono di-
sponibili solo due architetture implementanti l’Unified Shader Model, quella di
AMD e quella di NVIDIA. Per confrontarne le caratteristiche principali si illustra
la tassonomia di Flynn, la quale classifica i sistemi di calcolo in base al numero dei
flussi di istruzioni e dei dati che sono in grado di gestire. Le categorie principali
sono rappresentate in Tabella 1.1 e si distinguono in:
SISD Single Instruction on Single Data e la categoria contenente l’architettura
tradizionale della macchina di Von Neumann, utilizzata da tutti i calcolatori
convenzionali, in cui un solo processore obbedisce ad un singolo flusso di
istruzioni (programma sequenziale) ed esegue queste istruzioni ogni volta
su un singolo flusso di dati.
SIMD Single Instruction on Multiple Data. Alla categoria SIMD appartengono
le architetture composte da molte unita di elaborazione che eseguono con-
temporaneamente la stessa istruzione su un insieme di dati differenti. Ge-
neralmente il modo di implementare queste architetture consiste nell’avere
un processore principale che invia le istruzioni in parallelo ad un insieme di
unita di elaborazione, le quali provvedono ad eseguirle.
Una di queste implementazioni e il processore vettoriale. I processori vetto-
riali sono unita di calcolo che, dopo aver letto e decodificato un’istruzione,
la eseguono su piu dati prima di passare all’istruzione successiva (Figura
1.1). L’unita di esecuzione lavora su registri vettoriali3, il cui contenuto e
3All’interno di un processore, i registri sono piccole quantita di memoria nelle quali vengono
16
Figura 1.1: Pipeline di un processore vettoriale a confronto con la pipeline diun processore scalare: nel primo le operazioni di fetch, decodifica,attivazione della memoria, e writeback sono eseguite una sola volta.
gestito dall’unita vettoriale di load–store. Questa e l’unita che si occupa di
leggere e scrivere dalla memoria centrale ai registri vettoriali. E in grado
di operare su piu dati contemporaneamente e, in un processore vettoriale,
ve ne possono essere piu di una. A causa del funzionamento di quest’ulti-
ma componente, i processori vettoriali hanno lo svantaggio di essere poco
adatti all’elaborazione di dati non distribuiti in modo costante. Quindi le
loro reali prestazioni variano a seconda della tipologia del programma che
si vuole eseguire.
MISD Multiple Instruction on Single Data e una classe di architetture mai svi-
luppata sul piano commerciale ma solo in alcuni progetti di ricerca per
l’elaborazione di segnali.
MIMD Multiple Instruction on Multiple Data e la categoria che comprende le ar-
chitetture in grado di eseguire piu flussi di istruzioni su piu flussi di dati
contemporaneamente, e rappresenta una evoluzione della categoria SISD.
Infatti la realizzazione di queste architetture avviene attraverso l’intercon-
nessione di un numero elevato di elaboratori convenzionali, facendo sı che
a questa classe appartengano sistemi di calcolo multiprocessore e sistemi di
calcolo distribuiti.
In particolare, le categorie SIMD e MIMD, costituiscono la classe delle architet-
ture parallele. Di queste fanno parte le GPU, le quali, come gia visto, si sono
specializzate nell’esecuzione concorrente dello stesso flusso di istruzioni su molti
dati.
Al livello implementativo, sia l’architettura di AMD che quella di NVIDIA
appartengono alla classe dei sistemi SIMD, mentre al livello funzionale NVIDIA
trasferiti i dati, dalla memoria principale, prima di operarvi. Un registro vettoriale e uninsieme di registri sui quali si opera contemporaneamente con una singola istruzione.
17
ha scelto un approccio differente. Infatti se nella propria GPU, AMD usa dei
processori vettoriali, NVIDIA adotta degli Stream Multiprocessor (SM). I dettagli
degli SM e dell’architettura di NVIDIA saranno mostrati nel capitolo successivo,
mentre per adesso e sufficiente sapere che questi non sono altro che processori
multicore, ovvero processori contenenti piu unita di elaborazione le quali sono in
grado di lavorare in parallelo e comunicare tra di loro.
La scelta di una tale soluzione e stata motivata da NVIDIA attraverso lo studio
di centinaia di shader appartenenti a programmi di grafica e giochi per i computer.
Gli shader, negli ultimi anni, hanno man mano adoperato un numero sempre
maggiore di operazioni scalari, a scapito delle operazioni vettoriali. Soprattutto
con gli shader piu complessi, gli SM hanno dimostrato di essere piu efficienti
rispetto ai processori vettoriali nell’eseguire questo tipo di operazioni.
Nel capitolo successivo si studia la soluzione per il GPU computing proposta
da NVIDIA. Le ragioni di questa decisione dipendono in parte dall’hardware che
l’autore ha avuto a disposizione per lo svolgimento di questo studio, ed in parte
dal grande lavoro che NVIDIA ha svolto riguardo alla documentazione della pro-
pria piattaforma. Inoltre, quest’ultima, grazie all’uso degli SM, prometteva una
maggiore flessibilita rispetto alla soluzione concorrente offerta da AMD.
18
2Il GPU computing di NVIDIA
In questo capitolo si studia il funzionamento della piattaforma per il GPU com-
puting di NVIDIA, osservandone da vicino il modello di programmazione e l’ar-
chitettura hardware sulla quale di basa.
2.1 L’architettura Tesla
Tesla, schematizzata in Figura 2.1, e il nome dell’architettura che sta alla base
di GPU come la G80 e della piu recente GT200. Le sue componenti principali
consistono in una memoria Dynamic Random Access Memory (DRAM) e in uno
Scalable Processor Array (SPA), ovvero la componente che si occupa di esegui-
re tutte le operazioni programmabili sulla GPU. Nello schema il lavoro fluisce
dall’alto verso il basso, attraversando tutte le componenti, alcune delle quali ope-
rano esclusivamente nel contesto del rendering e rimangono quindi inutilizzate
per le operazioni di computing. Di queste componenti si citeranno soltanto le piu
importanti, mentre ci si soffermera maggiormente su quelle che hanno un ruolo
determinante nell’uso della GPU a fini computazionali.
2.1.1 Il flusso delle operazioni
L’Host Interface e la componente che si occupa della comunicazione tra Host e
Device, ovvero tra il personal computer e la periferica fisicamente connessa a que-
sto, sulla quale risiede la GPU. Fra i suoi compiti principali c’e quello di iniziare
19
Figura 2.1: Architettura della GPU G80 di NVIDIA
20
i trasferimenti dei dati da e verso la memoria della CPU, l’interpretazione i co-
mandi dell’Host ed il controllo della loro consistenza. Successivamente il Compute
work distribution si occupa della distribuzione sullo SPA del flusso di istruzioni
generato dall’esecuzione dei kernel, ovvero le funzioni che vengono eseguite sulla
GPU. Questo compito e analogo a quello dei Pixel e Vertex work distribution
i quali, invece, distribuiscono il lavoro di shading nelle fasi del rendering grafi-
co. Al termine dell’elaborazione, nel caso che fossero state eseguite operazioni
di rendering, i risultati dei calcoli passano ai Raster Operation Processor (ROP)
attraverso una rete di connessioni. I ROP hanno il compito di eseguire le fun-
zioni non programmabili di colorazione dei pixel, ed operano direttamente sulla
memoria DRAM alla quale sono connessi.
2.1.2 Componenti dello SPA
Lo SPA e composto da diversi Texture Processor Cluster (TPC) (Figura 2.2)
che, in base al livello di performance, possono scalare da uno, nelle GPU di
fascia bassa, fino ad otto o piu nelle GPU di fascia alta. Un TPC contiene un
controller di geometria, uno Streaming Multiprocessor Controller (SMC), due
Stream Multiprocessor ed un’unita di texture.
Stream Multiprocessor
Lo SM (Figura 2.3) e un processore in grado di eseguire istruzioni inerenti sia allo
shading sia al GPU computing. Ogni SM contiene 8 Streaming Processor (SP)
cores, 2 Special Function Unit (SFU), una MultiThreaded instruction fetch and
Issue unit (MT Issue), una cache delle istruzioni, una cache per la memoria
costante, 16KByte di memoria condivisa ed 8192 registri da 32 bit ciascuno. Ogni
SP contiene un’unita scalare Multiply-Add (MAD), dando quindi 8 unita MAD
ad ogni SM. Le due SFU sono usate per il calcolo delle funzioni trascendentali
(esponenziali, logaritmiche, trigonometriche) ed inoltre ogni SFU contiene 4 unita
di moltiplicazione per numeri in virgola mobile. Le operazioni di load e store dalla
memoria principale sono implementante dall’SMC, mentre le operazioni di accesso
alla memoria condivisa, residente sullo SM, sono effettuate direttamente.
21
Figura 2.2: Texture/Processor Cluster nella CPU G80
Figura 2.3: Stream Multiprocessor
22
Unita delle texture
Nel contesto del GPU computing, l’unita delle texture e una componente che per-
mette di accedere in sola lettura della memoria DRAM. Questa e dotata di un
meccanismo di caching1 tale da privilegiare gli accessi localizzati ai dati. Al-
l’interno di un TPC, ogni unita serve contemporaneamente due SM, fornendo
un’alternativa all’uso dello SMC per la lettura dei dati.
Accessi alla memoria
La mancanza di un sistema di caching generale, della memoria DRAM, favorisce
l’ampiezza di banda passante dei dati tra GPU e memoria. Sul chip G80 questa
supera gli 80 GByte/s contro gli 8–10 GByte/s della banda passante fra una
CPU e la sua memoria globale. La mancanza di una cache pero comporta un’alta
latenza per le operazioni sui dati in memoria, superiore di 400-600 volte al tempo
di accesso ai registri locali. Tuttavia grazie ad uno scheduling intelligente delle
operazioni, la latenza di accesso alla DRAM viene nascosta quando si ha un
numero di thread2 per SM sufficientemente alto.
Infatti, quando il numero di thread e abbastanza grande, lo scheduler gestisce le
operazioni da eseguire, in modo da ridurre o eliminare i tempi di inattivita degli
SM. Durante il tempo necessario affinche un’operazione sulla memoria venga
eseguita, lo scheduler mette in attesa il thread che ha richiesto tale operazione.
Si liberano cosı delle risorse, che vengono utilizzate per l’esecuzione del flusso di
istruzioni di un altro thread, rimasto inattivo fino a quel momento.
I tempi di accesso alla memoria condivisa, al contrario di quanto appena visto
per la memoria globale, sono di poco superiori a quelli dei registri. La bassa
latenza e dovuta al fatto che i 16Kbyte di memoria condivisa sono situati all’in-
terno di ogni SM e non esternamente al die (superficie) della GPU. Tuttavia
questi 16Kbyte di memoria condivisa sono partizionati in 16 banchi da 1Kbyte,
ciascuno soggetto a conflitti di accesso da parte degli SP cores componenti l’SM.
Infatti nel caso ci siano piu SP cores ad accedere allo stesso banco, le operazio-
1Un meccanismo di caching e un meccanismo che si appoggia generalmente ad una memoriapiccola e veloce (cache) per ridurre i tempi di accesso alla memoria globale
2Un thread e un flusso di esecuzione all’interno di un’applicazione. Quando il flusso e unicosi parla di applicazione single–thread, altrimenti, quando i flussi sono piu di uno, si parla diapplicazioni multi–thread.
23
ni di ciascuno di questi vengono serializzate, con un conseguente degrado delle
prestazioni dovuto all’aumento della latenza.
Figura 2.4: Schema degli accessi alla memoria
2.1.3 Un’architettura di tipo SIMT
Mentre al livello implementativo NVIDIA usa delle unita SIMD composte da 8
cores ciascuno, al livello funzionale queste non operano eseguendo 8 volte la stes-
sa istruzione, per un solo flusso di esecuzione, come farebbero delle unita SIMD
tradizionali. Esse, infatti, gestiscono 8 flussi di esecuzione differenti, eseguendo
un’istruzione per ciascuno di essi. In questo modo, i singoli SP cores si compor-
tano come delle unita scalari e non come unita di esecuzione di un processore
vettoriale. Per sottolineare questa differenza, NVIDIA parla di un’architettura
SIMT, ovvero Single Instruction Multiple Threads. A differenza delle unita SIMD,
questa e in grado di massimizzare l’uso delle proprie unita di calcolo, soprattutto
24
quando l’applicazione e composta da molti thread. La GPU infatti viene utilizza-
ta a pieno solo quando i thread sono nell’ordine delle migliaia, grazie al fatto che
questi hanno un costo di creazione e gestione praticamente nullo.
L’architettura lavora assegnando ad ogni flusso di istruzioni un SP core, in mo-
do che ogni thread venga eseguito indipendentemente dagli altri, conservando il
proprio stato dei registri e indirizzo delle istruzioni. Ogni SM gestisce ed esegue
simultaneamente fino a 24 gruppi, chiamati Warps, di 32 thread ciascuno, per i
quali la MT Issue si occupa di selezionare e inoltrare le istruzioni. Durante l’ese-
cuzione di un programma e possibile che, in presenza di un’istruzione condizionale
(if-else, switch-case, ecc. . . ), due o piu thread, all’interno dello stesso Warp,
seguano percorsi differenti. Quando tutti i 32 thread di un Warp seguono lo stesso
ramo di esecuzione, questa architettura raggiunge il massimo dell’efficienza per il
fatto che tutti gli SP cores di un SM eseguono la medesima istruzione. Invece,
se i thread all’interno di un Warp divergono, le prestazioni decadono. Infatti,
quando si presenta quest’ultimo caso, la MT Issue esegue in serie le istruzioni per
i diversi flussi, fintanto che questi rimangono separati. Scrivere un software che
tenga conto di questa caratteristica e fondamentale se si vuole ottenere il massimo
delle prestazioni dalla GPU di NVIDIA.
Nonostante la maggiore efficienza, NVIDIA non vede in un’architettura SIMT
la soluzione definitiva per le proprie GPU, poiche essa introduce diversi problemi
rispetto ad architetture piu semplici. Facendo un confronto con la classe dei
sistemi SIMD e infatti richiesta una maggiore logica di controllo, che comporta un
aumento della superficie del chip e un maggior consumo di energia. E sufficiente
osservare i dettagli tecnici delle architetture, prima di AMD, e poi di NVIDIA,
per vedere l’enorme differenza nella superficie dei chip che, nel primo caso, si
limita a 192mm2 contro i 484mm2 del secondo.
2.1.4 Operazioni in virgola mobile
L’architettura Tesla contiene un vasto set di istruzioni per operazioni su numeri
interi, numeri in virgola mobile, singoli bit, conversioni di tipo, funzioni tra-
scendentali, controllo di flusso, lettura-scrittura dalla memoria e operazioni sulle
texture.
Di maggiore interesse per il calcolo scientifico sono le istruzioni che operano sui
numeri in virgola mobile, che seguono lo standard di rappresentazione ieee754.
25
Nell’elenco seguente si mostrano le differenze principali tra l’implementazione
interna di Tesla e le specifiche ufficiali dello standard. La lista completa di queste
differenze si puo consultare in [4].
• Moltiplicazione e addizione possono essere combinate in una sola istruzione
(FMAD) che non tiene conto del risultato intermedio della moltiplicazione.
• La divisione e implementata attraverso moltiplicazione per il reciproco.
• L’operazione di radice quadrata e implementata attraverso il reciproco della
radice quadrata stessa.
• La politica di recovery adottata per le condizioni di errore e la store 0.
Nel caso un’operazione incorra in un errore di underflow, questa tecnica
ne azzera il risultato. Al contrario del gradual underflow, adottato nella
maggior parte delle CPU per personal computer, si mantiene cosı invariata la
precisione di macchina. Approfondimenti su entrambe le tecniche si trovano
in [1].
Queste ed altre piccole differenze possono portare ad una discordanza con i ri-
sultati dei calcoli eseguiti da una CPU. Tuttavia gli errori di moltiplicazione
e addizione non superano mai 0,5 ulp (Unit in the Last Place o precisione di
macchina), ed errori maggiori si possono verificare solo nel calcolo di funzioni
trascendentali, i quali pero restano intorno a 2 - 3 ulp.
2.2 CUDA
CUDA e un modello di programmazione parallela ed un ambiente software pro-
gettato per sfruttare le capacita dei nuovi sistemi paralleli multicore e, piu in
particolare, delle GPU ad architettura Tesla. I concetti principali sui quali questa
piattaforma si basa sono:
• Gerarchia dei Thread,
• Memoria condivisa,
• Sincronizzazione a barriera.
26
Questi vengono controllati dal programmatore mediante poche estensioni al lin-
guaggio C, facilitando cosı l’apprendimento per chi ha gia familiarita con questo
ambiente.
Le estensioni indirizzano lo sviluppatore a partizionare un problema in piu
sottoproblemi risolvibili in maniera indipendente. Tali problemi, a loro volta,
vengono risolti mediante la cooperazione di piu thread, ovvero, di piu flussi d’e-
secuzione concorrenti dello stesso programma. Questa soluzione permette sia la
cooperazione fra gruppi di thread per la risoluzione di ogni sottoproblema, sia
l’assegnazione di un sottoproblema indipendente ad uno qualunque dei proces-
sori disponibili. La piattaforma e quindi scalabile rispetto, sia al numero di SM
presenti nel sistema, sia al numero di thread che questi possono gestire.
2.2.1 Gerarchia dei thread
CUDA estende quindi il C permettendo al programmatore di definire delle funzioni
speciali chiamate kernel che, quando invocate, verranno eseguite concorrentemen-
te in N blocchi differenti ciascuno composto da M thread, come rappresentato in
Figura 2.5.
Ogni thread possiede una variabile threadIdx, accessibile all’interno del kernel
ed inizializzata univocamente rispetto alla stessa variabile degli M thread dello
stesso blocco. threadIdx e una variabile avente 3 componenti intere, cosı che
i thread possano essere organizzati in spazi monodimensionali, bidimensionali o
tridimensionali. Questo permette al programmatore di scegliere la struttura di
esecuzione del kernel piu conveniente, in base al problema da risolvere.
Il codice di esempio 2.1 mostra come si effettua la somma tra due due vettori di
lunghezza M, A e B, ponendo il risultato in un terzo vettore C. Ogni thread somma
una componente di A con la rispettiva componente di B e ne scrive il risultato in
C. Essendo i thread organizzati in uno spazio monodimensionale, la componente
dei vettori sulla quale opera ognuno di questi e data dal primo dei tre valori di
threadIdx. Durante l’esecuzione, per ogni thread, questo valore sara distinto e
compreso tra 0 e M-1.
Listato 2.1: vecAdd
global void vecAdd(float∗ A, float∗ B, float∗ C){int i = threadIdx.x; //prima componente della variabile threadIdx
C[i] = A[i] + B[i];
27
Figura 2.5: Gerarchia dei thread in CUDA.
}host int main(){
vecAdd<<<1, M>>>(A, B, C);
}
Nell’esempio sono dichiarate due funzioni. All’interno della funzione main, il
punto di entrata, si invoca la funzione vecAdd, la quale riceve in input 3 riferimenti
a vettori. Lo specificatore global e ed i parametri contenuti tra i caratteri
<<< e >>>, indicano rispettivamente al compilatore che la funzione vecAdd
verra eseguita sulla GPU in un solo blocco composto da M thread concorrenti.
Analogamente a quanto detto per threadIdx, si ha che ai thread appartenenti
allo stesso blocco e assegnata la variabile blockIdx, accessibile all’interno del
28
kernel e contenente il numero di blocco a cui appartiene il thread stesso. Questa
variabile e composta da una o due componenti, a seconda che i blocchi siano
organizzati logicamente in uno spazio monodimensionale o bidimensionale.
Il codice di esempio 2.2 estende il codice 2.1, introducendo l’uso di blocchi
organizzati in uno spazio monodimensionale.
Listato 2.2: vecAdd
global void vecAdd(float∗ A, float∗ B, float∗ C){int i = blockDim.x∗blockIdx.x+threadIdx.x; //calcolo dell’id assoluto
C[i] = A[i] + B[i];
}int main(){
vecAdd<<<N, M>>>(A, B, C);
}
In questo esempio la dimensione dei vettori A e B e di M ×N elementi, ed i para-
metri contenuti tra i caratteri <<< e >>> indicano al compilatore che verranno
eseguiti N blocchi composti da M thread ciascuno. Al fine di assegnare a tutti
i thread la somma di una componente univoca dei due vettori, l’assegnazione di
tale componente non dipendera piu solo dalla variabile threadIdx, ma dipendera
anche dalla variabile blockIdx. In questo modo gli M thread appartenenti agli N
blocchi differenti, sono in grado di sommare distintamente tutti gli elementi dei
vettori A e B.
Configurazione di esecuzione
Il numero dei blocchi che verranno eseguiti sulla GPU, ed il numero dei thread per
ogni blocco, determinano la configurazione di esecuzione di un kernel. All’interno
di un kernel, e possibile conoscere tale configurazione accedendo alle variabili
blockDim e gridDim: la prima contenente il numero di thread per blocco e la
seconda contenente il numero di blocchi per il kernel in esecuzione.
L’architettura Tesla della GPU G80 limita il numero di thread per blocco a
512, e prevede al massimo la gestione 65536 blocchi contemporaneamente. Lo
scheduling dei blocchi di un kernel e gestito in hardware, sulla base di un algoritmo
che di volta in volta assegna ogni blocco ad un solo SM, e ad ogni SM assegna
contemporaneamente fino ad 8 blocchi, la cui dimensione complessiva non supera
pero i 768 thread (24 warps). Questi blocchi vengono eseguiti in concorrenza fino
a che lo scheduler decide di sospenderne l’esecuzione e assegnare allo SM dei nuovi
29
blocchi. Per calcolare l’efficienza di una configurazione si introduce il concetto di
occupazione, il quale e facilmente esprimibile come il rapporto tra il numero di
warps attivi e il numero massimo di warps gestibili da un SM,
occupacy =active warps
max active warps.
Configurazioni che offrono l’occupazione massima possono essere facilmente co-
struite. Le piu efficienti per le GPU della serie G80 sono quelle che hanno almeno
192 thread per blocco. In questo modo lo scheduler e in grado di mascherare
buona parte della latenza di lettura e scrittura, sia della memoria, sia dei registri.
Configurazioni con molti thread per blocco, pero, introducono delle limitazioni
sul numero dei registri utilizzabili all’interno di un kernel, ad esempio: se si de-
cide per una configurazione di 256 thread per blocco, scrivere un kernel che usa
10 registri invece di 11 permette di passare da 2 a 3 blocchi (da 16 a 24 warps)
attivi contemporaneamente per ogni singolo SM. Infatti, considerando 3 blocchi
in esecuzione per ogni SM, nel caso ogni thread usi 10 registri, i registri utilizzati
in totale sono 3×256×10 = 7680; un valore entro il limite degli 8192 disponibili.
Invece, considerando 3 blocchi per ogni SM, ma assumendo che ogni thread usi
11 registri, questa volta i registri utilizzati in totale sono 3 × 256 × 11 = 8448,
ovvero troppi affinche 3 blocchi possano essere attivi contemporaneamente su un
solo SM.
2.2.2 Gerarchia della memoria
Ogni thread ha accesso, in lettura e scrittura, a piu spazi di memoria durante la
sua esecuzione:
Registri Su ogni SM sono allocati un certo numero di registri e, ad ogni thread, ne
e concesso solo un numero limitato. Le operazioni sui registri hanno latenza
praticamente nulla, anche se possono avere dei ritardi di lettura-dopo-una-
scrittura dovuti all’aggiornamento del loro contenuto. Il numero di registri
disponibili per ogni thread varia in base alla configurazione di esecuzione
che si sceglie. Fissata quest’ultima, il il loro numero puo essere calcolato
mediante la formulaR
B × ceil(T, 32),
30
Figura 2.6: Gerarchia della memoria in CUDA.
dove R e il numero di registri per ogni SM, B e il numero di blocchi attivi
per SM e T e il numero di thread per blocco, che, come si vede, viene
approssimato per eccesso con il piu piccolo multiplo di 32.
Memoria globale Questa e la memoria che i thread utilizzano, sia per leggere i
dati da elaborare, sia per scrivere i risultati delle loro operazioni. Essa e
implementata nella DRAM, quindi i suoi tempi di accesso, come gia visto
durante lo studio dell’architettura Tesla, sono centinaia di volte superiori a
quelli dei registri: e importante ridurne l’utilizzo il piu possibile.
Quando l’accesso alla memoria globale non e evitabile, e conveniente usare
delle tecniche per leggere o scrivere i dati in blocchi (accessi coalesced).
Nelle GPU della serie G80, un’operazione di lettura o scrittura coalesced si
ha quando almeno la meta dei thread di un warp (half-warp) effettua un
accesso alla memoria, in modo tale che il thread i-esimo acceda alla locazione
31
i-esima di uno spazio allineato ad un indirizzo multiplo delle dimensioni di
un half-warp, ovvero 16.
Operazioni di lettura e scrittura coalesced hanno tempi inferiori di circa 10
volte rispetto ad operazioni di lettura e scrittura non strutturate.
Memoria locale privata In questa memoria vengono allocate le variabili locali
dei singoli thread, che non possono essere contenute nei registri. Anche que-
sto e uno spazio di memoria implementato nella DRAM e, di conseguenza,
i tempi di accesso sono gli stessi della memoria globale.
Memoria condivisa Questa memoria e accessibile a tutti i thread dello stesso
blocco e, generalmente, e utilizzata per condividere i risultati intermedi dei
processi di calcolo. E implementata mediante i banchi di memoria condivisa
contenuti all’interno di ogni SM, quindi la latenza e leggermente maggiore
di quella dei registri. Spazi contigui di memoria condivisa sono allocati su
banchi differenti in modo da ridurre i conflitti di accesso e, di conseguenza,
anche i tempi di latenza.
I seguenti sono spazi di memoria accessibili in sola lettura.
Memoria costante E uno spazio di memoria implementato nella memoria glo-
bale, e contiene quei valori che restano costanti durante tutta l’esecuzione
del kernel. I tempi di latenza sono al pari di quanto visto per la memoria
globale; tuttavia questo spazio di memoria dispone di una cache. La sua
presenza riduce drasticamente i tempi di attesa nel caso si acceda molte
volte allo stesso elemento.
Memoria delle texture Questo spazio di memoria e accessibile attraverso l’unita
delle texture. Rappresenta quindi un’interfaccia di sola lettura alla memoria
globale, che offre un meccanismo di caching ottimizzato per la localita bidi-
mensionale e, facoltativamente, un diverso tipo di indirizzamento ai dati3.
Nonostante le latenze siano superiori agli accessi di tipo coalesced verso la
memoria globale, l’uso della memoria delle texture puo essere molto van-
taggioso. Tramite questo spazio di memoria infatti, si possono eseguire
operazioni di lettura che, o non possono essere rese coalesced, o coinvolgono
3Ulteriori informazioni sulla memoria delle texture si trovano in [4], alla quale e stata dedicataun’intera appendice.
32
un insieme di dati sufficientemente piccolo da essere contenuto interamente
nella cache.
Gli spazi di memoria globale, costante e delle texture sono consistenti tra la chia-
mata di un kernel e un altro. Al contrario, i contenuti della memoria locale e
della memoria condivisa sono eliminati al termine di ogni esecuzione.
2.2.3 Sincronizzazione
Thread all’interno dello stesso blocco possono cooperare tra di loro, condividendo
dati attraverso la memoria condivisa, e sfruttando la primitiva di sincronizzazione
a barriera syncthreads(). Quest’ultima, dopo essere stata chiamata all’interno
di un thread, ne blocca l’esecuzione, fino a che tutti gli altri thread appartenenti
allo stesso blocco non l’hanno invocata a loro volta. L’uso di syncthreads()
assume un ruolo determinante quando, ad un dato momento del programma,
si vuol essere sicuri che tutti i processi di un blocco abbiano terminato il loro
compito; ad esempio la scrittura dei propri dati in memoria. Questa funzione e
l’unico meccanismo di sincronizzazione offerto da CUDA.
La mancanza di una primitiva di sincronizzazione globale costringe ad attendere
il ritorno della chiamata ad un kernel, per avere la certezza che tutti i blocchi
abbiano terminato il proprio compito. Tuttavia, gia dalla seconda versione della
GPU G80, e stata prevista la possibilita di effettuare operazioni atomiche sulla
memoria globale, dando quindi agli sviluppatori la possibilita di implementare
primitive a semaforo per la sincronizzazione globale dei thread.
2.2.4 Compute capability
In questo capitolo si sono osservate, sia tutte le caratteristiche dell’ambiente
CUDA, sia i limiti che vi impone la GPU G80. Ovvero i limiti sulla configu-
razione di esecuzione dei kernel, il numero di registri disponibili, l’uso di funzione
atomiche, ecc. . . Questi limiti non sono gli stessi per ogni GPU, pertanto NVIDIA
assegna un valore chiamato compute capability, al quale sono associati i limiti e le
caratteristiche generali della GPU in questione. La lista completa delle compute
capability di ogni GPU e consultabile in [4].
33
2.2.5 Uso della piattaforma
Prima di terminare questa sezione, e importante sottolineare come CUDA sia
stato concepito per sfruttare le capacita di calcolo parallelo delle GPU come se
queste fossero dei coprocessori della CPU principale. Pertanto l’Host si occupa sia
dei trasferimenti dei dati da e verso la DRAM del Device, sia della configurazione
e dell’esecuzione dei kernels. Un esempio del flusso di un’applicazione CUDA e
mostrato in Figura 2.7, dove si osserva l’alternanza tra il codice seriale eseguito
sull’Host, e l’esecuzione parallela di uno o piu kernel sul Device.
Figura 2.7: Flusso delle operazioni per un’applicazione costruita con CUDA
34
3Il Lavoro di Implementazione
In questo terzo capitolo si mostrano le fasi di sviluppo della componente soft-
ware realizzata per questa tesi. Si illustreranno le motivazioni che hanno spinto
ad un’implementazione per l’ambiente CUDA, gli strumenti di programmazione
utilizzati ed infine le fasi di sviluppo del codice.
3.1 Studio iniziale
Il software che si intende portare su questa piattaforma, come gia accennato nel-
l’introduzione, e una implementazione ad-hoc del Metodo dei Gradienti Coniugati
per matrici pentadiagonali simmetriche e definite positive (sdp). Questo e un me-
todo iterativo per la risoluzione di sistemi lineari che, ad ogni passo, esegue sia
delle operazioni scalari, sia una serie di operazioni tra vettori. Fra queste vi sono
la somma membro a membro, la moltiplicazione per uno scalare, il calcolo della
norma e un prodotto matvec. Molte di queste sono operazioni completamente
parallelizzabili e quindi si adattano perfettamente alla natura del modello CUDA
secondo il quale ogni kernel e eseguito contemporaneamente da migliaia di thread.
3.1.1 L’ambiente Matlab
Il sorgente originale e stato scritto in linguaggio Matlab e, pertanto, la sua ese-
cuzione avviene per mezzo di un interprete. Quando un software e interpretato
35
le sue istruzioni non vengono eseguite nativamente1, ma vengono eseguite dopo
essere state tradotte appositamente per l’architettura sottostante. Questa solu-
zione permette ai software scritti in questo linguaggio di essere completamente
indipendenti dalla piattaforma di esecuzione, rinunciando pero ad una parte delle
prestazioni.
Per confrontare i vantaggi che il modello CUDA offre rispetto al modello di
programmazione classico, prima di tutto si e reso necessario riscrivere il software
originale. Si e utilizzato cosı, un linguaggio che, una volta compilato, producesse
un programma nativo per la piattaforma dell’Host. Fortunatamente Matlab ha
la facolta di integrarsi con software esterni, scritti in linguaggi compilabili. In
questo modo all’interno dello stesso ambiente e stato possibile confrontare le tre
implementazioni: quella scritta in linguaggio interpretato Matlab, quella scritta
in linguaggio compilabile C per la CPU, e quella scritta in linguaggio C esteso
dall’ambiente CUDA.
Matlab fornisce uno script di compilazione e dei file di inclusione per l’inte-
grazione dei sorgenti. Questi ultimi, per essere compilati all’interno di Matlab,
devono implementare la funzione
void mexFunction(int nlhs, mxArray ∗plhs[ ],int nrhs, const mxArray ∗prhs[ ]).
Questo metodo rappresenta il punto di entrata del programma, ed i parametri
della sua signature2 sono:
nlhs Variabile di tipo intero contenente il numero di parametri di output richiesti
alla chiamata del metodo dall’ambiente Matlab.
plhs Un array di lunghezza nlhs contenente i riferimenti di tipo mxArray ai valori
di output del metodo invocato.
nrhs Variabile di tipo intero contenente il numero dei parametri di input passati
alla chiamata del metodo dall’ambiente Matlab.
prhs Un array di lunghezza nrhs contenente i riferimenti di tipo mxArray ai
parametri presenti nella firma del metodo invocato.
1Un’istruzione nativa e un’istruzione che viene decodificata ed eseguita direttamente dallaCPU, senza nessun passaggio intermedio.
2La signature di un metodo e l’insieme di informazioni che identificano il metodo stesso, inquesto caso: il suo nome, il valore restituito ed i parametri che questo riceve.
36
mxArray e una struttura che rappresenta una matrice di valori, tutti appartenenti
allo stesso tipo di dato. Questa struttura contiene le informazioni sul numero di
righe, numero di colonne e l’indirizzo di memoria nel quale sono memorizzati i
suoi valori. A tale indirizzo di memoria, i dati sono memorizzati in modo contiguo
per colonne; ci si ritrova quindi a lavorare con un vettore di lunghezza m × n,
referenziato all’interno di una struttura analoga alla seguente.
a1,1 a2,1 . . . am−1,n am,n
m
n
=:
a1,1 a1,2 . . . a1,n
a2,1 a2,2 . . . a2,n
......
. . ....
am,1 am,2 . . . am,n
L’allocazione e l’accesso a questa struttura avviene mediante l’uso di funzioni spe-
cifiche, dichiarate negli headers forniti. Assieme a queste sono disponibili anche
diversi metodi analoghi a quelli della libreria standard del C per l’Input/Output
nello spazio di lavoro, che facilitano lo sviluppo e il testing del codice. E possibile
consultare la documentazione completa dei tipi di dato e delle funzioni offerte
dall’ambiente Matlab, in [19] e in [17].
La compilazione di sorgenti in linguaggio C avviene mediante il comando mex,
mentre per la compilazione di sorgenti scritti utilizzando la piattaforma CUDA,
NVIDIA fornisce un proprio script, nvmex, il cui funzionamento e analogo al quello
del precedente comando. Si possono cosı integrare programmi che verranno ese-
guiti nativamente sull’Host e programmi che, oltre a quest’ultima caratteristica,
sfruttano le capacita di calcolo del Device.
Si termina quindi questa sottosezione con tre esempi. Questi mostrano come
l’operazione di elevazione al quadrato degli elementi di un vettore puo essere
integrata in Matlab; prima con linguaggio Matlab poi con linguaggio C ed infine
in ambiente CUDA.
Matlab Il linguaggio Matlab, grazie alla sua flessibilita consente di eseguire l’o-
perazione con una sola riga di codice senza doversi preoccupare della com-
pilazione.
Listato 3.1: quadrato.m
function x=quadrato(r)
x=r.ˆ2;
37
C L’integrazione con il linguaggio C richiede, sia una gestione piu elaborata dei
dati di input, sia la gestione manuale della memoria per l’allocazione dei
valori di output. I controlli di coerenza, tra i parametri in ingresso e quelli
che la funzione assume di ricevere, devono essere eseguiti manualmente.
Inoltre, prima di richiamare il metodo cquadrato dall’ambiente Matlab, e
necessario compilare il sorgente con il comando mex cquadrato.c.
Listato 3.2: cquadrato.c
#include ”mex.h”
void mexFunction( int nlhs, mxArray ∗plhs[], int nrhs, const mxArray ∗prhs[] )
{int i=0;
int m = mxGetM(prhs[0]); /∗Numero di Righe∗/int n = mxGetN(prhs[0]); /∗Numero di Colonne∗/const int len=m∗n;
plhs[0]=mxCreateDoubleMatrix(m, n,mxREAL);/∗Alloco memoria per l’output∗/
double∗ output=mxGetPr(plhs[0]); /∗Recupero l’indirizzo di memoria∗/double∗ input=mxGetPr(prhs[0]); /∗nel quale sono contenuti i valori∗/
for(i=0;i<len;i++)
output[i]=input[i]∗input[i]; /∗elevo al quadrato∗/}
CUDA L’implementazione in ambiente CUDA, oltre a tutti gli accorgimenti visti
per l’esempio precedente, richiede che i parametri in input vengano copiati
dalla memoria dell’Host a quella del Device, e viceversa per parametri di
output. Inoltre e necessaria la scrittura di una funzione separata che assume
il ruolo di kernel per l’esecuzione sul Device. La compilazione in questo ca-
so avviene mediante il comando ../bin/nvmex -O -f ../bin/nvopts.sh
-L/usr/local/cuda/lib/ -lcufft -Wl cuquadrato.cu
Listato 3.3: cuquadrato.cu
#include ”mex.h”
global void dsquare(float∗ d, int len)
{int id=threadIdx.x+blockIdx.x∗blockDim.x;
const int jmp=blockDim.x∗gridDim.x;
for (; id<len;id+=jmp)
d[id]∗=d[id];
}void mexFunction( int nlhs, mxArray ∗plhs[], int nrhs, const mxArray ∗prhs[] )
{int m = mxGetM(prhs[0]); /∗Numero di Righe∗/int n = mxGetN(prhs[0]); /∗Numero di Colonne∗/const int len=m∗n;
plhs[0]=mxCreateNumericMatrix(m, n,mxSINGLE CLASS,mxREAL);
38
float ∗ output=(float∗)mxGetPr(plhs[0]);
float ∗ input=(float∗)mxGetPr(prhs[0]);
float ∗ device;
cudaMalloc((void∗∗)&device,len∗sizeof(float));
cudaMemcpy(device,input,len∗sizeof(float),cudaMemcpyHostToDevice);
/∗1000 blocchi per 64 threads ciascuno∗/dsquare<<<1000,64>>>(device,len);
cudaMemcpy(output,device,len∗sizeof(float),cudaMemcpyDeviceToHost);
cudaFree(device);
}
3.1.2 L’implementazione Matlab e le strutture dati
Il Listato 3.4 e il corpo completo dell’implementazione Matlab sulla quale si e
lavorato. Si divide in due funzioni: la principale, cgmamma, e la funzione che si
invoca per avviare il metodo dei gradienti coniugati, mentre la seconda, matvec,
implementa il prodotto matrice–vettore per matrici pentadiagonali sdp.
Listato 3.4: cgmamma.m
function [x,itcg,eta,err] = cgmamma( mask, etan, Hdiag, Hipm12j, ...
Hijpm12, b, tol, maxit, zero )
%
% PCG con scalamento diagonale simmetrico
%
DT = sqrt( Hdiag + (Hdiag<=zero)/eps );
D1 = Hipm12j./( DT(1:end−1,:).∗DT(2:end,:) );
D2 = Hijpm12./( DT(:,1:end−1).∗DT(:,2:end) );
r = b −matvec( Hdiag, Hipm12j, Hijpm12, etan );
r = mask.∗r./DT; x = zeros(size(etan));
p = r; eta = sum(sum( r.∗r )); itcg = 0; eta00 = eta; err = 0;
while eta>tol & itcg<maxit
itcg = itcg + 1; Ap = matvec( [], D1, D2, p, 1 ).∗mask;
d = sum(sum( p.∗Ap )); lam = eta/d; x = x + lam∗p; r = r − lam∗Ap;
eta0 = eta; eta = sum(sum( r.∗r )); mu = eta/eta0; p = r + mu∗p;
if eta<10∗tol & eta0<=eta, break, end %% stagnazione non grave
if eta>1e3∗eta00 & eta00<1e2∗tol, x = 0;
disp( ’CGMAMMA stagnation: x set to 0’), break, end
end
if itcg==maxit & eta>tol
err = 1; disp( ’CGMAMMA exception:’)
disp( sprintf ( ’ initial and final residual = %g, %g’, eta00,eta))
else
x = etan + x./DT;
end
return
39
function v = matvec( Hdiag, Hipm12j, Hijpm12, p, flag )
%
% Effettua il prodotto Matrice−vettore. Se flag e’ specificato , si assume
% che la diagonale sia unitaria (scalamento diagonale).
%
if nargin==4 % diagonale non unitaria
v = Hdiag.∗p;
else
v = p; % diagonale unitaria
end
v (:,1: end−1) = v(:,1:end−1) −Hijpm12.∗p(:,2:end);
v (:,2: end) = v(:,2:end) −Hijpm12.∗p(:,1:end−1);
v(1:end−1,:) = v(1:end−1,:) −Hipm12j.∗p(2:end,:);
v(2:end,:) = v(2:end,:) −Hipm12j.∗p(1:end−1,:);
return
Questa implementazione ad hoc del Metodo dei Gradienti Coniugati, adopera
delle matrici come struttura per la rappresentazione dei vettori, al fine di adattarsi
meglio alla natura del problema nel quale e impiegato il metodo. Date delle
strutture a matrice A, B, C, D ed X corrispondenti rispettivamente ai parametri
in ingresso Hdiag, Hipm12j, Hijpm12, b ed etan, queste rappresentano i dati del
seguente sistema lineare:
a1,1 b1,1 0 . . . c1,1 0 . . . 0
b1,1 a2,1. . . . . .
...
0. . . . . . bm−1,1
. . ....
... bm−1,1 am,1 0 cm,n−1
c1,1 0 a1,2 b1,2 0
0. . . b1,2
. . . . . ....
.... . . . . . am−1,n bm−1,n
0 . . . . . . cm,n−1 0 . . . bm−1,n am,n
x1,1
x2,1
...
xm,1
x1,2
...
xm−1,n
xm,n
=
d1,1
d2,1
...
dm,1
d1,2
...
dm−1,n
dm,n
(3.1)
Le dimensioni delle matrici in ingresso sono di conseguenza
• Hdiag = m× n
• Hipm12j = (m− 1)× n
• Hijpm12 = m× (n− 1)
• b = m× n
• etan = m× n
40
A questi parametri, se ne aggiunge uno: il parametro mask. Questo parametro
rappresenta un vettore composto da elementi unitari o nulli. Il suo scopo e quello
di identificare e mascherare alcune condizioni del sistema (3.1). Infine i parametri
maxit, tol e zero corrispondono rispettivamente al numero massimo di iterazioni,
la tolleranza sulla norma del gradiente, e il valore di soglia per il troncamento dei
numeri durante lo scalamento.
3.1.3 Legge di Amdhal
Uno degli interessi del lavoro di implementazione, e quello di osservare i vantaggi
prestazionali che si possono trarre dall’ambiente CUDA. Al fine di stimare l’in-
cremento di prestazioni ottenibile, prima di procedere oltre, si introduce la legge
di Amdhal.
La legge Amdahl e stata formulata nel 1967 da G.Amdahl nel tentativo di
valutare l’incremento di prestazioni ottenibile da un sistema multiprocessore e,
piu generalmente, l’incremento di prestazioni di un sistema in base all’aumento
delle performance di una sua componente. Se T e il tempo di esecuzione di un
dato programma, tale tempo puo essere diviso in un tempo Tc ed in un tempo
Tnc, rispettivamente il tempo di utilizzo di una certa componente e il tempo in
cui opera il resto del sistema
T = Tc + Tnc.
Quando la componente viene sostituita con una piu “performante”, il tempo
di esecuzione per quest’ultima diventa T ′c < Tc e il tempo totale si trasforma
diventando
T = T ′c + Tnc.
Definendo l’incremento di prestazioni (speed up o accelerazione) come il rapporto
tra il tempo di esecuzione precedente e quello successivo alla modifica, lo speed
up della componente e
Sc =Tc
T ′c
.
Data fc =Tc
T, frazione di utilizzo della componente all’interno del sistema origina-
rio, si ha che lo speed up della componente Tc, si riflette sullo speed up dell’intero
41
Figura 3.1: Effetto della legge di Amdhal.
sistema secondo il rapporto
S =1
(1− fc) +fc
Sc
.
Questo dimostra che l’aumento delle prestazioni di un sistema e limitato supe-
riormente dalla frazione di utilizzo della componente che si accelera (Figura 3.1).
Infatti si ha che
Smax = limSc→∞
1
(1− fc) +fc
Sc
=1
1− fc
.
3.1.4 Prima analisi delle prestazioni
Alla luce della legge di Amdhal, per conoscere quali fossero le operazioni piu in-
cidenti sui tempi d’esecuzione del software, si sono usati i profiler forniti con gli
ambienti di sviluppo Matlab e CUDA. Un profiler, in breve, e un software che, da-
ta l’esecuzione di un programma, produce in output una serie di informazioni che
la riguardano, ad esempio: il numero di volte che una funzione e stata invocata, o
quanto tempo e stato speso all’interno di questa. Con queste informazioni si e in
grado di individuare facilmente le frazioni di utilizzo delle componenti. In Tabella
3.1 si riporta l’analisi eseguita dal profiler dell’ambiente Matlab sull’esecuzione
del metodo cgmamma per un sistema di dimensioni 512× 512.
Da questa analisi risulta che, la function relativa al prodotto matrice–vettore,
e quella ad avere la maggior frazione di utilizzo. Come primo approccio si e
quindi cercato di lavorare solamente sulla funzione matvec, nonostante lo speed
42
Linea Codice Chiamate Tempo (s) Tempo %16 Ap=matvec([],D1,D2,p,1).*mask; 443 11.188 74.117 d=sum(sum(p.*Ap)); 443 1.101 7.319 x=x+lam*p; 443 0.852 5.622 eta=sum(sum(r.*r)); 443 0.829 5.520 r=r-lam*Ap; 443 0.521 3.4
Altre ... 0.616 4.1Totale 15.107 100
Tabella 3.1: Informazioni prodotte dal profiler di Matlab sull’esecuzione delmetodo cgmamma.
up massimo raggiungibile per cgmamma fosse di solo
1
1− 0.741= 3, 86.
3.2 Prima implementazione
In questa prima parte si lavora unicamente sulla function matvec, realizzandone
due implementazioni alternative: la prima, cmatvec, eseguita dall’Host, e la se-
conda, nvmatvec, eseguita dal Device. L’implementazione di queste due function
ha pero un significato principalmente didattico. Questo perche la loro applica-
bilita all’interno dell’ambiente Matlab e molto limitata dagli overhead (costi di
gestione), dovuti alle chiamate di funzione esterne. Come si vedra, sostituendo
alla function matvec una delle due nuove implementazioni, si ha che buona parte
dei guadagni prestazionali, vengono persi in overhead.
Si presenta inoltre un altro problema, riguardante l’uso della function nvmatvec.
Mentre l’implementazione per l’Host accede ed opera sui dati gia salvati nella me-
moria principale, l’implementazione per il Device necessita che gli stessi dati ven-
gano prima copiati nella memoria DRAM della GPU, elaborati, e infine trasferiti
nuovamente in memoria principale dell’Host. Dato che la funzione di prodotto
matrice–vettore viene richiamata ad ogni iterazione del Metodo dei Gradienti Co-
niugati, l’uso della function nvmatvec, sarebbe sconveniente non solo per i costi
di gestione della chiamata. Per ogni passo iterativo, sarebbe infatti necessario
copiare tutti i dati dalla memoria principale dell’Host alla DRAM del Device,
alzando ulteriormente gli overhead.
43
Quando si trattera dell’implementazione per il Device dell’intero Metodo dei
Gradienti Coniugati, si vedra come l’ideale consista nell’allocare i dati in memoria
soltanto una volta. Questa soluzione e applicabile quando almeno il ciclo princi-
pale del Metodo dei Gradienti Coniugati e interamente eseguito in una funzione
esterna, in modo che tutti i passi iterativi vengano eseguiti al di fuori dell’am-
biente Matlab. Cosı, oltre ad eliminare i continui trasferimenti di memoria fra
Host e Device, vengono eliminati anche gli overhead di gestione delle chiamate.
Le misure degli speed up, per le implementazioni in questa sezione, sono state
condotte modificando i metodi in modo che questi ripetessero ad ogni invocazione
10.000 volte il prodotto matrice–vettore. In questo modo non si sono introdotti
overheads dovuti alla gestione delle chiamate. Sulla base dei tempi misurati si
sono calcolate le accelerazioni delle componenti. E importante specificare che i
tempi di esecuzione sono riferiti all’hardware della macchina su cui si e sviluppato
il software: una CPU Intel Core 2 Duo T7300 a 2.00GHz e una GPU NVIDIA
GeForce 8400GS la prima con una memoria operante a 667MHz e la seconda con
con una memoria ed un core operanti a 400 MHz.
3.2.1 Implementazione Host: cmatvec
L’implementazione Host (listato ??) e costituita da due funzioni, di cui una co-
stituisce il punto di entrata della function, mentre l’altra e quella che effettua il
prodotto matrice-vettore.
Nella prima si effettua un semplice controllo sul numero e sulla dimensione dei
parametri ricevuti in ingresso. Si alloca poi lo spazio per la memorizzazione dei
risultati e si effettua la chiamata alla funzione cmatvec.
Nella seconda si esegue la moltiplicazione della diagonale principale hdiag
quando il parametro intero flag ha valore diverso da 1. Successivamente si ese-
gue la moltiplicazione delle diagonali hijpm12 e hipm12j. La moltiplicazione
delle diagonali con il vettore p viene effettuata in tre cicli separati, che, ogni
volta, scorrono l’array v, eseguendovi le operazioni elemento per elemento.
1. Il primo ciclo effettua la moltiplicazione membro a membro della diagonale
principale hdiag, di dimensioni m× n, con il vettore p (Figura 3.2).
2. Il secondo ciclo svolge le due moltiplicazioni con il vettore hijpm12, di
dimensioni m×(n−1), rappresentante la sottodiagonale e la sopradiagonale
44
Figura 3.2: Moltiplicazione conla diagonale princi-pale
Figura 3.3: Moltiplicazione conle sottodiagonali piuesterne
Figura 3.4: Moltiplicazione con le prime sotto-diagonali
45
piu esterna. La prima moltiplicazione avviene membro a membro con primi
m × (n − 1) elementi di p, mentre e la seconda si effettua sempre membro
a membro, ma con gli ultimi m× (n− 1) elementi di p (Figura 3.3).
3. Nel terzo ciclo, infine, si effettuano le moltiplicazioni con il vettore hipm12j,
di dimensione (m− 1)× n, rappresentante sia la prima sottodiagonale che
la prima sopradiagonale della matrice. Queste moltiplicazioni non si pos-
sono eseguire semplicemente applicando un offset (scostamento), come nel
caso precedente. Questo perche, gli elementi della sottodiagonale del siste-
ma (3.1) non sono interamente memorizzati nella corrispondente struttura
dati. Infatti, il vettore hipm12j non contiene gli elementi nulli di tale sot-
todiagonale, rompendo cosı la diretta corrispondenza fra gli indici di questo
vettore e gli indici del vettore p. Le moltiplicazioni effettuate in questo
ciclo hanno quindi richiesto la scrittura di una funzione capace di associare
ad ogni indice del vettore hipm12j il corrispondente indice del vettore p
(Figura 3.4).
Eseguire il prodotto matrice–vettore mediante tre cicli separati, invece di usare
un unico ciclo, puo non sembrare la scelta migliore. Nella pratica invece non e
cosı, rivelando che i meccanismi di caching della CPU sono piu efficienti quando
si lavora con un piccolo numero di vettori. In questo modo, essendo la cache una
memoria di dimensione abbastanza limitata, un numero maggiore di elementi
per ogni vettore puo essere trasferito in quest’ultima, riducendo cosı gli accessi
alla memoria principale. Adoperando un solo ciclo, e quindi, accedendo ad ogni
iterazione a tutti i vettori, si provoca la riduzione del numero di elementi per
ogni vettore presenti nella memoria cache. Causando un accesso piu frequente
alla memoria principale, con un conseguente degrado delle performance.
Si e effettuato un confronto fra le prestazioni di questo codice e la versione
scritta in linguaggio Matlab, ottenendo accelerazioni significative dovute prin-
cipalmente all’esecuzione nativa delle istruzioni; in tabella 3.2 sono riportati i
risultati.
3.2.2 Implementazione CUDA: nvmatvec
L’implementazione CUDA (listato ??) del metodo matvec e composta da quattro
funzioni: il punto di entrata, la funzione di preparazione del Device, il kernel e
46
la funzione di lettura dei risultati. Dato che il punto di entrata e sostanzialmente
identico a quello presentato nell’implementazione Host, ci si sofferma sulle altre
tre funzioni.
La prima ad essere invocata e la funzione di preparazione del Device, la quale
si occupa di copiare i dati dall’Host alla memoria del Device, dopo averne al-
locata la quantita necessaria. In questa stessa funzione viene impostata l’unita
delle texture che servira per leggere quei dati il cui accesso e inefficiente a causa
di un allineamento non favorevole. Infatti, come si e gia visto, l’accesso diretto
alla memoria globale ha alti tempi di latenza, e per ridurre questo svantaggio e
necessario effettuare il piu spesso possibile accessi di tipo coalesced. Questo tipo
di accesso pero puo essere effettuato solo per la moltiplicazione della diagonale
principale, nella quale l’i-esimo elemento di v e determinato dalle componenti
i-esime dei vettori p e hdiag. Per la moltiplicazione delle altre due diagonali, il
calcolo dell’elemento i-esimo del vettore v coinvolge l’accesso a locazioni dei vet-
tori hijpm12 e hipm12j aventi indici non corrispondenti alle locazioni del vettore
p. A causa di questo non si possono effettuare accessi di tipo coalesced. Tut-
tavia le aree di memoria a cui si accede, hanno una certa localita, ad esempio:
nella moltiplicazione della seconda sottodiagonale, l’i-esimo elemento del vettore
v coinvolgera l’i-esimo elemento di hijpm12 e l’i + m-esimo elemento di p. In
questo caso i meccanismi di caching della memoria delle texture offrono un’alter-
nativa efficiente all’accesso diretto alla memoria globale, riducendo drasticamente
il tempo impiegato per la lettura di elementi non allineati.
La seconda funzione istanzia un certo numero di thread che eseguiranno il kernel
sul Device. Il kernel svolge le operazioni di moltiplicazione delle diagonali in modo
tale che, data una configurazione di M blocchi composti da N thread ciascuno, il
j-esimo thread del k-esimo blocco, valuti il vettore v alle locazioni i tali che
i0 = k ×N + j, i1 = i0 + M ×N, . . . , il = il−1 + M ×N, ∀ l t.c. il ≤ m× n
Alla fine del calcolo, si invoca l’ultima funzione che si occupa di copiare il
contenuto del vettore v, residente sul Device, in memoria dell’Host. Inoltre,
sempre quest’ultima funzione, dealloca la memoria utilizzata e rilascia i riferimenti
all’unita delle texture.
In Tabella 3.2 si riportano i risultati di test analoghi a quelli effettuati per
l’implementazione Host. Anche in questo caso si registrano degli speed up nei
47
confronti dell’implementazione Matlab della function matvec. Inoltre, il metodo
nvmatvec, offre prestazioni superiori anche all’implementazione cmatvec, mo-
strando cosı che una GPU di fascia bassa puo competere con una CPU di fascia
superiore.
Test m× n matvec(s) cmatvec(s) nvmatvec(s)Device Host
speed-up speed-up1 200× 200 17,98 6,40 5,3 3,39 2,812 300× 300 49,27 17,54 13,08 3,76 3,393 400× 400 122,58 27,43 21,10 5,80 4,464 500× 500 202,41 44,00 35,51 5,70 4,605 600× 600 310,00 66,69 49,48 6,26 4,641 100× 400 18,67 6,04 5,80 3,21 2,912 100× 900 59,59 15,02 11,57 5,15 3,963 100× 1600 125,63 28,90 21,76 5,77 4,344 100× 2500 210,45 46,78 32,72 6,41 4,495 100× 3600 320,78 71,55 47,82 6,69 4,481 400× 100 17,82 6,49 5,6 3,18 2,742 900× 100 53,83 14,63 13,06 4,12 3,673 1600× 100 119,94 27,6 22,01 5,40 4,344 2500× 100 202,51 44,15 35,81 5,65 4,585 3600× 100 309,95 66,97 49,48 6,26 4,62
Tabella 3.2: Confronto dei i tempi di esecuzione, fra le implementazioni Matlab,Host e Device della funzione matvec. Le ultime due colonne mostranogli speed up delle function sviluppate, rispetto all’implementazioneoriginale. A numero di test uguale corrisponde un egual numero dielementi
3.3 Seconda Implementazione
Rispetto alla precedente implementazione, in questa nuova sezione si affronta il
problema dell’allineamento dei dati. Questi, come si e visto negli schemi in Figura
3.3 e 3.4, hanno un allineamento sfavorevole, soprattutto per quanto riguarda la
moltiplicazione della prima sottodiagonale. Infatti, per ogni locazione del vettore
risultante v che si va ad aggiornare, si deve effettuare una serie di calcoli per
ricavare l’indice della corrispondente locazione del vettore hipm12j. Decidendo di
ripensare le strutture dati per favorire l’allineamento dei vettori si puo ottenere
un guadagno sensibile di prestazioni rinunciando pero al massimo risparmio di
memoria.
48
L’idea, in questa seconda implementazione, consiste infatti nel ridurre la com-
plessita delle operazioni di moltiplicazione. Memorizzando nella struttura dati
hipm12j gli elementi nulli, omessi in precedenza, della prima sottodiagonale del
sistema (3.1), si ottiene un allineamento piu favorevole. Nel nuovo schema, in
figura 3.5, si osserva infatti che gli scostamenti dei vettori sono costanti rispetto
all’elemento di v che si sta considerando.
Figura 3.5: Nuovo schema di moltiplicazione per la prima sottodiagonale
L’aggiunta degli elementi nulli, e un’operazione resa immediata dalla flessibi-
lita dell’ambiente Matlab che, con una sola istruzione, consente di costruire la
struttura dati di cui si necessita. Nelle due seguenti implementazioni si assumera
quindi che al posto della matrice hipm12j di dimensioni (m− 1)× n la funzione
riceva una matrice di dimensioni m×n, costruita dalla precedente con la seguente
istruzione:
ns = [hipm12j; zeros(1, length(hipm12j))]; :
hdiag
0 0 · · · 0 0
.
49
L’uso di questa nuova struttura dati non si e dimostrata conveniente se applica-
ta in ambiente Matlab. Infatti, modificando la function matvec per operare con
la struttura dati appena descritta, si ha un netto calo di prestazioni, dipendente
probabilmente dal meccanismo di accesso alla memoria.
3.3.1 Implementazione Host
Questa nuova implementazione per l’Host cambia rispetto alla precedente sola-
mente nella parte riguardante il prodotto della prima sottodiagonale. Nel Listato
?? si possono osservare in dettaglio le differenze, che sono limitate all’ultimo ciclo
della funzione cmatvec.
In Tabella 3.3 si riportano i risultati dei test eseguiti su quest’ultima implemen-
tazione. Si nota immediatamente l’aumento dell’accelerazione dovuto a questo
nuovo schema di allineamento.
Test m× n matvec(s) nvmatvec(s) cmatvec(s)Device Host
speed-up speed-up1 200× 200 17,98 3,92 3,47 4,58 5,182 300× 300 49,27 9,32 13,29 5,28 3,713 400× 400 122,58 15,64 23,83 7,83 5,144 500× 500 202,41 24,93 37,72 8,12 5,375 600× 600 310,00 33,96 57,38 9,13 5,401 100× 400 18,67 4,41 3,49 4,23 5,342 100× 900 59,59 8,70 7,95 6,84 7,493 100× 1600 125,63 14,76 21,44 8,51 5,854 100× 2500 210,45 23,31 37,95 9,02 5,545 100× 3600 320,78 32,12 57,62 9,98 5,561 400× 100 17,82 4,80 5,32 3,71 3,342 900× 100 53,83 8,85 7,99 6,08 6,733 1600× 100 119,94 16.01 23,80 7,49 5,034 2500× 100 202,51 25,39 38,07 7,97 5,315 3600× 100 309,95 34,62 58,02 8,95 5,34
Tabella 3.3: Confronto dell’implementazione matvec di Matlab con le funzioniHost e Device sfruttanti la nuova struttura dati. A numero di testuguale corrisponde un egual numero di elementi
50
3.3.2 Implementazione Device
La seconda versione dell’implementazione per il Device non modifica solo l’allinea-
mento dei vettori per la moltiplicazione della prima sottodiagonale, ma introduce
anche l’uso della memoria condivisa per ridurre gli accessi dalla memoria globale.
Come si e gia visto, la memoria condivisa permette ai thread dello stesso blocco
di scambiare i propri dati fra di loro.
Con la nuova struttura dati, la moltiplicazione del vettore hipm12j con il vet-
tore p e semplificata da un allineamento dei dati favorevole, che permette l’uso
di scostamenti costanti. Infatti, con questo nuovo allineamento, l’aggiornamento
della componente i-esima del vettore v coinvolge gli elementi i − 1 ed i + 1 di
p, e gli elementi i e i + 1 di hipm12j. La nuova implementazione usa due spazi
allocati in memoria condivisa, nei quali, il thread j-esimo, alla locazione j-esima,
salvera i valori di p[i] e hipm12j[i]. Ogni thread in questo modo potra ricavare
i valori i − 1 e i + 1 dei vettori p e hipm12j accedendo alla memoria condivisa.
Si presentano solamente due casi speciali, ovvero il primo e l’ultimo thread del
blocco, i quali saranno obbligati ad accedere alla memoria globale.
In Tabella 3.3 si riportano i risultati dei test effettuati, i quali mostrano un
netto speed up rispetto alla versione precedente, soprattutto quando si lavora su
matrici di grandi dimensioni.
3.4 Implementazione Finale
Dopo aver osservato le tecniche di sviluppo utilizzate per questi modelli di pro-
grammazione, si presenta adesso l’implementazione complessiva del Metodo dei
Gradienti Coniugati. Anche in questo caso, si sono realizzate due versioni del soft-
ware, una per l’Host e una per il Device, le cui performance verranno analizzate
in dettaglio nel capitolo successivo.
In questa sezione, quindi, si osservera la realizzazione di due metodi che potran-
no essere richiamati dall’ambiente Matlab, e che eseguiranno al suo interno tutte
le operazioni e le iterazioni per la risoluzione del sistema (3.1). Entrambe le imple-
mentazioni provvederanno percio a recuperare i parametri ricevuti in input dalla
chiamata dell’ambiente Matlab e, senza nessun overhead esterno, svolgeranno le
stesse operazioni del metodo cgmamma.
51
Per far questo, oltre al metodo matvec, e stato necessario riscrivere, sia per
l’Host sia per il Device, una serie di funzioni. Fra queste, quelle per l’esecuzione di
semplici operazioni di addizione, moltiplicazione e calcolo della norma di vettori,
di cui si osserveranno brevemente le implementazioni; soprattutto nel caso del
Device.
3.4.1 Implementazione Host: ccgmamma
L’implementazione Host, consultabile nel listato ??, non aggiunge niente di nuovo
a quanto visto fino ad ora. Le funzioni di moltiplicazione e addizione dei vettori
sono principalmente composte da un unico ciclo, che esegue le operazioni elemento
per elemento. Quando e stato possibile, e stata utilizzata la tecnica di unrolling
dei cicli, affinche il compilatore potesse utilizzare le istruzioni vettoriali presenti
nella CPU; questo ovviamente per migliorare le prestazioni.
3.4.2 Implementazione Device: nvcgmamma
Per l’implementazione Device, a differenza di quanto si puo osservare per l’Host,
all’interno del ciclo principale non c’e una diretta corrispondenza fra chiamate di
funzioni e operazioni svolte. Consultando il listato ??, si osserva infatti che il
ciclo principale del metodo e composto di sole tre invocazioni di funzioni kernel.
Queste svolgono al loro interno le operazioni dell’algoritmo, raggruppandole in
modo da ridurre sia l’overhead di creazione dei thread, sia i trasferimenti dei dati
dalla memoria globale ai registri della GPU. La divisione delle operazioni e la
seguente:
parte 1 Si esegue il prodotto matvec accedendo agli elementi dei vettori p, hijpm12,
hipm12j e mask, calcolando cosı il vettore v. Sfruttando poi gli elementi di p
e v gia salvati nei registri, si calcola il loro prodotto scalare, implementando
cosı le istruzioni matvec([],D1,D2,p).*mask e d=sum(sum(p.*Ap)).
parte 2 Si esegue l’operazione saxpy (Scalar Alpha X Plus Y ) corrispondente
all’istruzione r=r-lam*Ap dell’algoritmo Matlab e, sfruttando gli elemen-
ti gia salvati nei registri, si calcola il prodotto scalare tra r e se stesso,
corrispondente all’istruzione eta=sum(sum(r.*r)).
52
parte 3 Si eseguono le operazioni x=x+lam*p e p=r+mu*p, le quali sono state
raggruppate nello stesso kernel perche entrambe accedono agli elementi del
vettore p; in questo modo si riducono gli accessi alla memoria globale.
Un’osservazione particolare va alle operazioni appena citate di prodotto scalare e
calcolo della norma, per le quali si e adoperata la tecnica di Parallel Reduction.
Questa, in O(log2 M) passi, permette di eseguire operazioni caratterizzate da avere
come risultato uno scalare, dipendente da tutti gli elementi di un vettore.
Dati N blocchi composti da M=2n thread ciascuno, il funzionamento di questa
tecnica prevede che al primo passo tutti gli M thread di ogni blocco siano attivi, e
che ciascuno di questi legga un certo numero degli elementi del vettore. Terminata
la lettura, ogni thread memorizza il risultato (risultato parziale) delle operazioni
fra gli elementi letti, nella locazione in memoria condivisa che gli corrisponde.
Nei passi successivi i thread attivi per ogni blocco vengono dimezzati, lasciando
a questi il compito di leggere ed aggiungere al proprio risultato parziale, quello
dei thread non piu attivi al passo corrente (Figura 3.6).
La procedura termina quando resta attivo un solo thread per blocco, il quale si
occupera di scrivere il proprio risultato parziale nella memoria globale. Si ottiene
cosı un nuovo vettore composto da soli N elementi (1 per ogni blocco) per il quale
o si utilizza nuovamente la tecnica di Parallel Reduction per ridurne ancora la
dimensione o, nel caso N sia abbastanza piccolo, si procede scorrendo il vettore e
operando su ognuno dei suoi elementi. Si ottiene cosı il risultato dell’operazione.
53
Figura 3.6: Esempio di Parallel Reduction per il calcolo della somma deglielementi di un vettore.
54
4Test del software
In questo capitolo si conducono i test sul software sviluppato. Si confronta-
no quindi le function cgmamma, ccgmamma e nvcgmamma, al fine di valutare le
caratteristiche delle tre diverse implementazioni.
4.1 Precisione dei risultati
Si inizia confrontando la precisione delle soluzioni trovate dai tre metodi, per
il sistema (3.1). Il formato dei numeri adoperato nei test e quello a singola
precisione, sia per gli ambienti nativi, sia per l’ambiente Matlab. Test che usano il
formato a doppia precisione sono limitati dall’uso di GPU che hanno una compute
capability di almeno 1.3, mentre l’hardware utilizzato ha una compute capability
pari a 1.1.
Definizione del problema
Il test inizia con la definizione, all’interno di Matlab, delle due diagonali hijpm12
e hipm12j. Queste avvengono rispettivamente con i comandi
• rand(’seed’ ,0) ;
• Hijpm12=single(rand(m,n−1)∗10);
• Hipm12j=single(rand(m−1,n)∗10);
55
m ed n contengono il numero di righe e il numero di colonne della struttura a
matrice in cui saranno salvati i dati. Si definisce poi la diagonale principale
hdiag con il comando
• Hdiag=mu+[zeros(n,1)Hijpm12] + [Hijpm12 zeros(n,1)] + [zeros(1,m); Hipm12j
]+[Hipm12j;zeros(1,m)].
In questo modo, la matrice pentadiagonale e sdp, e la convergenza del metodo
iterativo e assicurata. Inoltre il suo numero condizionamento e inversamente
proporzionale al valore del parametro mu. I contenuti di mask ed etan vengono
invece assegnati dai comandi
• mask=single(ones(m,n));
• etan=single(zeros(m,n));
mentre la tolleranza e il massimo numero di iterazioni sono rispettivamente fissati
a 1×10−7 e 2000. Infine, si sceglie il termine noto b, in modo tale che la soluzione
del sistema corrisponda al vettore interamente composto da elementi unitari. Per
fare questo, prima di ogni test, si assegna il valore di b con il seguente comando.
• b=matvec(Hdiag, Hipm12j, Hijpm12,ones(m,n));
Metodo di valutazione
A questo punto, dopo aver definito tutti i parametri richiesti dalla funzione
cgmamma, si sono risolti alcuni sistemi. Per confrontare gli errori sulle soluzioni
prodotte, si valuta il quadrato della norma del residuo, usando il comando
• res=sum(sum((matvec(Hdiag,Hipm12j,Hijpm12,X)−b).ˆ2)),
nel quale la variabile X contiene la soluzione trovata. In Tabella 4.1 sono riportati
i risultati ottenuti.
4.2 Tempi di esecuzione
Questo test si occupa di confrontare i tempi di esecuzione delle tre function su
sistemi di dimensioni differenti. Le configurazioni di esecuzione per la GPU,
56
Function m× n mu iterazioni ‖Ax− b‖22cgmamma
512× 512 1004 5,44e-5
ccgmamma 4 5,27e-5
nvcgmamma 4 8,55e-5
cgmamma
512× 512 1018 4,81e-6
ccgmamma 18 4,96e-6
nvcgmamma 18 6,83e-6
cgmamma
512× 512 146 6,30e-6
ccgmamma 46 6,29e-6
nvcgmamma 46 7,18e-6
cgmamma
512× 512 0.1115 1,37e-5
ccgmamma 92 1,37e-5
nvcgmamma 92 1,45e-5
cgmamma
512× 512 0.01273 3,13e-5
ccgmamma 274 3,13e-5
nvcgmamma 273 3,19e-5
cgmamma
1024× 1024 1004 2,17e-4
ccgmamma 4 2,11e-4
nvcgmamma 4 3,43e-4
cgmamma
1024× 1024 1011 1,96e-5
ccgmamma 11 1,94e-5
nvcgmamma 11 2,58e-5
cgmamma
1024× 1024 133 2,60e-5
ccgmamma 33 2,59e-5
nvcgmamma 33 2,958e-5
cgmamma
1024× 1024 0.1121 5,71e-5
ccgmamma 122 5,75e-5
nvcgmamma 121 6,05e-5
cgmamma
1024× 1024 0.01292 1,33e-4
ccgmamma 301 1,37e-4
nvcgmamma 292 1,37e-4
Tabella 4.1: Risultati dei test sulla precisione del risultato. Piu basso e il valoredel residuo, maggiore e l’accuratezza della soluzione.
57
il compilatore, e l’hardware adoperato, sono determinanti per i risultati del-
le misurazioni; percio, di seguito, si riassumono le caratteristiche dell’hardware
utilizzato.
PC1 Il primo personal computer e il sistema portatile sul quale e stato sviluppato
il codice dei metodi. La CPU del sistema e una Intel Core 2 Duo T7300,
operante alla frequenza di 2 GHz. Questa ha una memoria cache da 4MByte
e una memoria centrale di 2GByte, operante a 667 MHz. La scheda video,
invece, e un’economica NVIDIA GeForce 8400 GS, composta da 2 SM e
avente una memoria di 128 MByte. GPU e memoria della scheda operano
entrambe alla frequenza di 400 MHz.
PC2 Il secondo personal computer invece e un sistema desktop. La sua CPU e
una Intel Core 2 Duo E6750 che lavora alla frequenza di 2,66 GHz. La me-
moria cache e anche in questo caso di 4 Mbyte, mentre la memoria centrale
ammonta a 2048MByte, e lavora ad una frequenza di 800 MHz. La scheda
video di questo sistema, rispetto alla precedente, e una piu “performan-
te” NVIDIA GeForce 8800 GTS composta da 12 SM e da una memoria da
384MByte. La GPU opera alla frequenza di 512MHz, mentre la memoria
opera alla frequenza di 792 MHz.
Riguardo alla scelta delle alle configurazioni di esecuzione, si sono provate di-
verse combinazioni fra numero di blocchi e numero di thread per blocco, e, alla
fine, si e adoperata la piu “performante”. Va tenuto in considerazione che la
configurazione ottimale varia in base all’hardware utilizzato; ad esempio, la GPU
di PC2, con i suoi 12 SM, ha performance migliori quando si adopera un numero
di blocchi maggiore, rispetto a quello adoperato per la GPU di PC1. Per ogni
test si specifichera quindi di volta in volta la configurazione utilizzata.
4.2.1 Esecuzione in ambiente Matlab/Linux
Il primo test mette a confronto su PC1 le due function sviluppate per questa
tesi, con la function originale, misurandone i tempi di esecuzione. Il test avviene
all’interno dell’ambiente Matlab R2007a, su un sistema operativo Linux (kernel
2.6.23.1). Il compilatore utilizzato e lo GNU C Compiler (GCC) 4.1.2, sul quale,
in fase di compilazione, sono state abilitate tutte le ottimizzazioni del codice. La
58
configurazione d’esecuzione per la GPU e di 128 blocchi per 128 thread ciascuno:
un totale di 16384 thread.
Assegnate le variabili m, n e maxit, lo script con il quale sono stati eseguiti i
test e il seguente.
Hdiag=single(rand(m,n)∗10); %Definizione dei parametri
Hijpm12=single(rand(m,n−1)∗10);
Hipm12j=single(rand(m−1,n)∗10);
ns=[Hipm12j;zeros(1,n)]; %Nuova struttura con allineamento corretto
etan=single(rand(m,n)∗10);
b=single(rand(m,n)∗10);
mask=single(ones(m,n));
tol=1e−7;
zero=single(0.0);
e=clock; %Esecuzione dei test
A=cgmamma(mask,etan,Hdiag,Hipm12j,Hijpm12,b,tol,maxit,zero);
etime(clock,e)
e=clock;
B=ccgmamma(mask,etan,Hdiag,ns,Hijpm12,b,tol,maxit,zero);
etime(clock,e)
e=clock;
C=nvcgmamma(mask,etan,Hdiag,ns,Hijpm12,b,tol,maxit,zero);
etime(clock,e)
Questo script, non definendo una matrice sdp, invalida la proprieta di convergen-
za del Metodo dei Gradienti Coniugati. In questo modo le function terminano
soltanto dopo aver eseguito il numero massimo di iterazioni.
Al termine di ogni esecuzione, lo script visualizza i tempi di esecuzione relativi
ai metodi, cgmamma, ccgmamma e nvcgmamma. I risultati, per diversi valori di m, n
e maxit, sono riportati in Tabella 4.2.
4.2.2 Esecuzione in ambiente Windows
Il secondo test mette a confronto i metodi ccgmamma e nvcgmamma sul sistema
operativo Windows XP. Il compilatore utilizzato e quello fornito con l’ambiente di
sviluppo Microsoft Visual C++TM
2005 Express Edition mentre le configurazioni
di esecuzione sono: 128 blocchi per 128 thread su PC1, e 320 blocchi per 256
thread su PC2.
Il codice con il quale si definisce il problema e il seguente:
host int main( int argc, char∗∗ argv ){int m=512; int n=512; int maxit=1000;
long i ,k; float norm=0.0f; float tol=1e−5; float zero=0.0f;
if (argc==4){
59
Function m× n iterazioni PC 1 (s) speed upcgmamma
512× 512 100029,39 1
ccgmamma 6,22 4,72nvgmamma 4,60 6,39cgmamma
512× 512 5000149,41 1
ccgmamma 31,13 4,79nvgmamma 22,84 6,54cgmamma
512× 512 10000294,65 1
ccgmamma 61,84 4,76nvgmamma 46,41 6,39
cgmamma1024× 1024 500
67,96 1ccgmamma 15,91 4,27nvgmamma 10,03 6,77cgmamma
1024× 1024 1000135,80 1
ccgmamma 32,19 4,21nvgmamma 18,02 7,36cgmamma
1024× 1024 5000674,92 1
ccgmamma 158,50 4,25nvgmamma 91,52 7,37
Tabella 4.2: Test di esecuzione all’interno dell’ambiente Matlab
m=atoi(argv[1]); n=atoi(argv[2]); maxit=atoi(argv[3]);
}cgresult res;
float ∗ hdiag=(float∗)malloc(sizeof(float)∗m∗n);
float ∗ hipm12j=(float∗)malloc(sizeof(float)∗m∗n);
float ∗ hijpm12=(float∗)malloc(sizeof(float)∗m∗(n−1));
float ∗ b=(float∗)malloc(sizeof(float)∗m∗n);
float ∗ mask=(float∗)malloc(sizeof(float)∗m∗n);
float ∗ etan=(float∗)malloc(sizeof(float)∗m∗n);
memset(hipm12j,0,sizeof(float)∗m∗n);
for(i=0;i<(m−1)∗n;i++){k=i+(i)/(m−1);
hipm12j[k]=(rand()%1000)/100.0;
}for(i=0;i<m∗(n−1);i++)
hijpm12[i]=(rand()%1000)/100.0;
for(i=0;i<m∗n;i++){hdiag[i]=(rand()%1000); b[i]=(rand()%1000)/100.0;
mask[i]=1.0; etan[i]=(rand()%1000)/100.0;
}res=nvcgmamma(mask,etan,hdiag,hipm12j, hijpm12, b, tol,maxit,zero,m,n);
free(res.x);
return;
}
Al termine di ogni test, sia il metodo ccgmamma, che il metodo nvcgmamma, stam-
60
pano a video il tempo impiegato per la loro esecuzione. I risultati sono riportati
in Tabella 4.3
Metodo m× n Iter. PC1 (s) speed up PC2 (s) speed upccgmamma
512× 512 10007,11 5,73
nvcgmamma 4,64 1,53 0,48 11,93ccgmamma
512× 512 500035,35 28,54
nvcgmamma 23,14 1,53 2,42 11,79
ccgmamma1024× 1024 1000
33,98 27,70nvcgmamma 18,15 1,87 1,48 18,72ccgmamma
1024× 1024 5000168,92 138,11
nvcgmamma 90,60 1,86 7,40 18,66
ccgmamma1536× 1536 1000 –1 –
64,28nvcgmamma 3,33 19,30ccgmamma
1536× 1536 5000 – –320,07
nvcgmamma 16,14 19,83
ccgmamma2048× 2048 1000 – –
115,96nvcgmamma 5,82 19,92ccgmamma
2048× 2048 5000 – –578,07
nvcgmamma 29,07 19,89nvcgmamma 28,61 20,212
Tabella 4.3: Confronto fra i tempi di esecuzione dei metodi sui sistemi PC1 e PC2
4.3 Rapporto prestazioni/prezzo
L’ultimo confronto di questo capitolo riguarda il rapporto fra prestazioni e prezzo.
Il rapporto prestazioni/prezzo e una delle caratteristiche piu importanti nel campo
industriale, e permette di valutare la convenienza dell’investire in un sistema di
calcolo basato su una GPU, piuttosto che su una CPU. Tuttavia questo rapporto
e strettamente legato, sia al software realizzato, sia all’hardware utilizzato, in
questa tesi. Il seguente test va quindi considerato valido solo in questo contesto.
In Tabella 4.4 si mostrano i dati riguardanti l’hardware di PC2. I prezzi indicati
sono ricavati dalla media dei prezzi raccolti dai listini di piu negozi.
1Non e stato possibile eseguire il test a causa della memoria insufficiente della scheda video.2Il risultato e stato ottenuto con una configurazione di 1000 Blocchi da 256 thread ciascuno.
Inoltre la compilazione e stata effettuata con il flag -maxrregcount 10. Questo flag, limitaa 10 il numero di registri utilizzati per ogni thread, permettendo di raggiungere l’occupazionemassima della GPU.
61
CPU (e)speed up
prezzo× 103 GPU (e)
speed up
prezzo× 103 rapp. GPU
CPU
1501
150× 103 = 6, 67 110
20, 21
110× 103 = 183, 73 27, 55
Tabella 4.4: Confronto del rapporto prestazioni/prezzo fra CPU e GPU
Questi risultati evidenziano un vantaggio economico derivante dall’utilizzo della
GPU, di circa 27 volte, rispetto a quello ricavato da una CPU. Tuttavia questo
vantaggio e solo una stima della reale convenienza economica che si ha nell’utilizzo
di una GPU, poiche non sono stati considerati i costi della memoria; componente
necessaria all’interno di un sistema.
62
5Conclusioni
Con il lavoro svolto in questa tesi si sono illustrate le caratteristiche principa-
li delle GPU moderne e della tecnologia CUDA, descrivendone in dettaglio sia
l’architettura, sia il modello di programmazione.
Successivamente, sfruttando questa tecnologia, si e realizzata un’implementa-
zione del Metodo dei Gradienti Coniugati per matrici pentadiagonali simmetriche
e definite positive. In questo processo sono state riscritte le operazioni di alge-
bra lineare utilizzate dal Metodo, in modo tale che queste sfruttassero a fondo
l’architettura parallela delle GPU.
Infine si e effettuato un confronto fra tale implementazione e altre due imple-
mentazioni dello stesso Metodo: la prima in linguaggio C e la seconda in lin-
guaggio Matlab. Il confronto ha evidenziato un notevole vantaggio, in termini di
prestazioni, dell’implementazione CUDA. Infatti, sull’hardware utilizzato, questa
si e dimostrata capace di ridurre fino a 20 volte i tempi di elaborazione1.
Lo stesso confronto stima un significativo vantaggio economico, derivante dal-
l’uso della tecnologia CUDA nel software realizzato. Per l’elaboratore su cui si
sono eseguiti i test, questo vantaggio e stato stimato confrontando il rapporto
prestazioni/prezzo della GPU con il rapporto prestazioni/prezzo della CPU. Si
arriva cosı ad ottenere una convenienza economica della tecnologia CUDA di cir-
ca 27 quello della tecnologia convenzionale, ovvero basata sul solo utilizzo della
CPU.
Questi aspetti costituiscono proprio lo spunto piu interessante per un eventuale
1Per i dettagli si consulti il capitolo 4
63
proseguimento del lavoro di tesi. Infatti, a favore della tecnologia convenziona-
le, i vantaggi di CUDA potrebbero ridimensionarsi di fronte ad implementazioni,
in linguaggio C o linguaggio Matlab, capaci di sfruttare l’architettura multicore
delle moderne CPU. Mentre, a favore della tecnologia CUDA, realizzando un’im-
plementazione capace di sfruttare contemporaneamente le capacita di calcolo di
piu GPU, si potrebbe comunque incrementare la convenienza economica. Ancora
piu interessante, potrebbe essere lo sviluppo di un’implementazione del Metodo
dei Gradienti Coniugati che sfrutti congiuntamente sia la tecnologia classica, sia
la tecnologia CUDA, per ottenere speed up ancora piu significativi.
64
Bibliografia
[1] Luigi Brugnano, Cecilia Magherini, e Alessandra Sestini. Calcolo Numerico,
capitolo 1. Master universita & professioni, 2005.
[2] NVIDIA Corporation. Accelerating MATLAB with CUDA using MEX files.
Relazione tecnica, 2007.
[3] NVIDIA Corporation. CUBLAS Sources, 2008. codice sorgente della libreria
CUBLAS.
[4] NVIDIA Corporation. CUDA Programming Guide 2.0, 2008. http://www.
nvidia.com/object/cuda develop.html.
[5] NVIDIA Corporation. CUDA Reference Manual 2.0, 2008. http://www.
nvidia.com/object/cuda develop.html.
[6] G. Cummins, R. Adams, e T. Newell. Scientific computation through a GPU.
Southeastcon, 2008. IEEE, pp. 244–246, April 2008.
[7] Richard Edgar. Advanced CUDA, June 2008. GPU workshop Slides.
[8] N. Fujimoto. Faster matrix-vector multiplication on geforce 8800gtx. Pa-
rallel and Distributed Processing, 2008. IPDPS 2008. IEEE International
Symposium on, pp. 1–8, April 2008.
[9] Wilson W. L. Fung, Ivan Sham, George Yuan, e Tor M. Aamodt. Dynamic
warp formation and scheduling for efficient gpu control flow. In MICRO ’07:
Proceedings of the 40th Annual IEEE/ACM International Symposium on Mi-
croarchitecture, pp. 407–420, Washington, DC, USA, 2007. IEEE Computer
Society.
65
[10] Tom R. Halfhill. Parallel processing with CUDA. Relazione tecnica,
Microprocessor, 2008.
[11] Mark Harris. Optimizing Parallel Reduction in CUDA, 2008. NVIDIA
Developer Technology Slides.
[12] Won-Ki Jeong e Ross Whitaker. High performance computing on the GPU:
NVIDIA G80 and CUDA, 2007. SCI Institute, University of Utah Slides.
[13] David B. Kirk. 10 Important Problems in Computer Architecture, 2008.
NVIDIA Chief Scientist Slides.
[14] Erik Lindholm, John Nickolls, Stuart Oberman, e John Montrym. Nvidia
tesla: A unified graphics and computing architecture. IEEE Micro, 28(2):39–
55, 2008.
[15] David Luebke. GPU Architecture and Implications, 2007. NVIDIA Research
Slides.
[16] Michael Macedonia. The GPU Enters Computing’s Mainstream. Computer,
36(10):106–108, 2003.
[17] Mathworks. Matlab 7 C and Fortran API Reference. The Mathworks, Inc.,
2007.
[18] Mathworks. Matlab 7 Desktop Tools and Development Environment. The
Mathworks, Inc., 2007.
[19] Mathworks. Matlab 7 External Interfaces. The Mathworks, Inc., 2007.
[20] Bettelli Oscar. Sistemi di calcolo parallelo. ECPlanet, 2008. www.ecplanet.
com.
[21] John D. Owens, Mike Houston, David Luebke, Simon Green, John E. Stone, e
James C. Phillips. GPU computing. Proceedings of the IEEE, 96(5):879–899,
maggio 2008.
[22] Mark Silberstein, Assaf Schuster, Dan Geiger, Anjul Patney, e John D.
Owens. Efficient computation of sum-products on GPUs through software-
managed cache. In Proceedings of the 22nd ACM International Conference
on Supercomputing, pp. 309–318, giugno 2008.
66
[23] R. Stallman e The GCC Developer Community. Using the GNU Compiler
Collection, 2003. for gcc 4.1.2.
[24] Damien Triolet. Product review: The Nvidia GeForce GTX 280 & 260.
Relazione tecnica, BeHardware, July 2008.
[25] W. A. Wiggers, V. Bakker, A. B. J. Kokkeler, e G. J. M. Smit. Implementing
the conjugate gradient algorithm on multi-core systems In Proceedings of the
International Symposium on System-on-Chip (SoC 2007), Tampere. A cura
di J. Nurmi, J. Takala, e O. Vainio, numero 07ex1846, pp. 11–14, Piscataway,
NJ, November 2007. IEEE.
[26] Wikipedia. Flynn’s taxonomy — wikipedia, the free encyclopedia, 2008.
[Online; accessed 3-September-2008].
[27] Wikipedia. Graphics processing unit — wikipedia, the free encyclopedia,
2008. [Online; accessed 3-September-2008].
[28] Wikipedia. Legge di Amdahl — wikipedia, l’enciclopedia libera, 2008.
[Online; in data 4-settembre-2008].
[29] Wikipedia. Stream processing — wikipedia, the free encyclopedia, 2008.
[Online; accessed 3-September-2008].
[30] Wikipedia. Z-buffer — wikipedia, l’enciclopedia libera, 2008. [Online; in
data 2-ottobre-2008].
67
68
Elenco delle tabelle
1.1 Tassonomia di Flynn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.1 Informazioni prodotte dal profiler di Matlab sull’esecuzione del metodo
cgmamma. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.2 Confronto dei i tempi di esecuzione, fra le implementazioni Matlab, Host
e Device della funzione matvec. Le ultime due colonne mostrano gli speed
up delle function sviluppate, rispetto all’implementazione originale. A
numero di test uguale corrisponde un egual numero di elementi . . . . . 48
3.3 Confronto dell’implementazione matvec di Matlab con le funzioni Host
e Device sfruttanti la nuova struttura dati. A numero di test uguale
corrisponde un egual numero di elementi . . . . . . . . . . . . . . . . . . 50
4.1 Risultati dei test sulla precisione del risultato. Piu basso e il valore del
residuo, maggiore e l’accuratezza della soluzione. . . . . . . . . . . . . . 57
4.2 Test di esecuzione all’interno dell’ambiente Matlab . . . . . . . . . . . . 60
4.3 Confronto fra i tempi di esecuzione dei metodi sui sistemi PC1 e PC2 . 61
4.4 Confronto del rapporto prestazioni/prezzo fra CPU e GPU . . . . . . . 62
69
70
Elenco delle figure
0.1 Pipeline di rendering di una scena tridimensionale. . . . . . . . . . . . . 10
0.2 A confronto l’architettura di una CPU multicore, con quella di un GPU
moderna: a ugual colore corrisponde ugual funzionalita. Nella GPU la
maggior parte della superficie del core e dedicata ad unita Arithmetic
and Logical Unit (ALU) . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.1 Pipeline di un processore vettoriale a confronto con la pipeline di un pro-
cessore scalare: nel primo le operazioni di fetch, decodifica, attivazione
della memoria, e writeback sono eseguite una sola volta. . . . . . . . . . 17
2.1 Architettura della GPU G80 di NVIDIA . . . . . . . . . . . . . . . . . . 20
2.2 Texture/Processor Cluster nella CPU G80 . . . . . . . . . . . . . . . . . 22
2.3 Stream Multiprocessor . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.4 Schema degli accessi alla memoria . . . . . . . . . . . . . . . . . . . . . 24
2.5 Gerarchia dei thread in CUDA. . . . . . . . . . . . . . . . . . . . . . . . 28
2.6 Gerarchia della memoria in CUDA. . . . . . . . . . . . . . . . . . . . . . 31
2.7 Flusso delle operazioni per un’applicazione costruita con CUDA . . . . . 34
3.1 Effetto della legge di Amdhal. . . . . . . . . . . . . . . . . . . . . . . . . 42
3.2 Moltiplicazione con la diagonale principale . . . . . . . . . . . . . . . . . 45
3.3 Moltiplicazione con le sottodiagonali piu esterne . . . . . . . . . . . . . 45
3.4 Moltiplicazione con le prime sottodiagonali . . . . . . . . . . . . . . . . 45
3.5 Nuovo schema di moltiplicazione per la prima sottodiagonale . . . . . . 49
3.6 Esempio di Parallel Reduction per il calcolo della somma degli elementi
di un vettore. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
71
72
Elenco dei sorgenti
2.1 vecAdd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.2 vecAdd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.1 quadrato.m . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.2 cquadrato.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.3 cuquadrato.cu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.4 cgmamma.m . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
73