Sei sulla pagina 1di 73

Università degli studi di Firenze

FACOLTÀ DI SCIENZE MATEMATICHE, FISICHE E NATURALI


Corso di Laurea in Informatica

Implementazione efficiente del Metodo


dei Gradienti Coniugati in ambiente
CUDA (Compute Unified Device
Architecture)

Tesi di Laurea in Informatica

Relatore: Candidato:
Prof. Luigi Brugnano 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 MISD Multiple Instructions on


Single Data
API Application Programming
Interface MT Issue MultiThreaded instruction
fetch and Issue unit
CAD Computer Aided Design
OpenGL Open Graphics Library
CG Computer Graphics
ROP Raster Operation Processor
CPU Central Processing Unit
SDK Software Development Kit
CUDA Compute Unified Device
SFU Special Function Unit
Architecture
SIMD Single Instruction on
DRAM Dynamic Random Access Multiple Data
Memory
SIMT Single Instruction Multiple
Flops Floating point Operations Threads
Per Second
SISD Single Instruction on Single
GCC GNU C Compiler Data

GPGPU General Purpose computing SMC Streaming Multiprocessor


on GPU Controller

GPU Graphics Processing Unit SM Stream Multiprocessor

SPA Scalable Processor Array


MAD Multiply-Add
SP Streaming Processor
MIMD Multiple Instructions on
Multple Data TPC Texture Processor Cluster

5
6
Dal più antico degli argomenti
trarremo la più nuova delle scienze
Hermann Ebbinghaus

Premessa

Introdotta ufficialmente nel 2006, la Compute Unified Device Architecture (d’ora


TM
in poi CUDA ) è una tecnologia realizzata dalla NVIDIA 1 R
. Questa ha lo scopo
di estendere le capacità 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 è stato possibile grazie all’intenso sviluppo delle
architetture parallele sulle quali si basano queste unità. Le loro capacità 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
più “performante” sul mercato2 . È chiaro quindi l’interesse ad apprendere il
funzionamento e le caratteristiche di questo nuovo tipo di unità di calcolo, al fine
di sfruttarne pienamente le potenzialità 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 è 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 potenzialità di
questa piattaforma.

• Nella prima parte di questa tesi si illustra brevemente cosa sono le GPU, la
1
NVIDIA Corporation è una azienda leader nella produzione di processori grafici, schede madri
e in generale di prodotti multimediali per personal computer e console.
2
I dati si riferiscono alla GPU di NVIDIA GT200 e alla CPU Intel R
XeonTM Harpertown

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 unità 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, è


una branca della CG che usa dati geometrici tridimensionali per la produzione
di immagini bitmap 3 . Questo processo è 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, è
molto più 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 è
quello di produrre dalle 30 alle 60 immagini al secondo, la capacità di calcolo della
CPU non è più sufficiente. Tali compiti sono soprattutto richiesti dai giochi per
computers, i quali perciò, 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 capacità 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
texture 4 che vi verranno applicate, i dati sull’illuminazione, ed il punto di
osservazione della scena.
3
Immagine bidimensionale definita da una matrice (mappa) di bit
4
Una texture è 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 è 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 può 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 sarà dimensionato in modo da essere più 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 camera 5 , non compariranno
nell’immagine finale.

4. La quarta fase è quella dedicata all’illuminazione degli oggetti. Nel rende-


ring è una delle fasi più 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
5
Camera è 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 antialiasing 6 , ecc...

6
L’antialiasing è una tecnica per ridurre l’effetto aliasing (scalettamento) che si ha quando un
segnale a bassa risoluzione viene mostrato ad alta risoluzione. In questo caso l’antialiasing
ammorbidisce 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 un
GPU moderna: a ugual colore corrisponde ugual funzionalità. Nella
GPU la maggior parte della superficie del core è dedicata ad unità
Arithmetic and Logical Unit (ALU)

12
1
Architettura 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 è 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 è 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
TM
la maggiore semplicità di fabbricazione e l’avvento di Microsoft c
Windows ne
facilitarono la diffusione. Quest’ultimo software, infatti, stimolò 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 più costose CPU
grafiche uscirono dal commercio.

A metà degli anni 90 l’uso della grafica tridimensionale iniziò 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 è stata data dalla libreria gra-
fica Open Graphics Library (OpenGL), già 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, più tardi negli anni, ha influenzato l’evoluzione
TM
delle GPU è stata la DirectX di Microsoft. L’approccio meno popolare del
supporto scheda-per-scheda, inizialmente, ne rallentò lo sviluppo. Questo riprese
vigore quando Microsoft iniziò a lavorare affianco ai progettisti hardware, pro-
grammando le nuove release 1 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 è la capacità della GPU di eseguire degli shader, ovvero un insieme


di istruzioni che definisce un algoritmo di trasformazione. Lo shader può 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 è stata la prima azienda a produrre un chip con capacità di shading


programmabili e successivamente ATI 2 R
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.
1
In ambito informatico release indica una particolare versione di un software resa disponibile
ai suoi utenti.
2
ATI è stata per molti anni la principale società concorrente di NVIDIA. Nel 2006 è stata
acquistata dalla AMD R
, azienda nota principalmente per la produzione di CPU.

14
1.2 Il GPU Computing

L’idea di sfruttare la capacità di calcolo parallelo delle GPU, per compiti differenti
dal rendering, ha portato alla nascita del GPU computing. Le unità 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 quantità 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 è 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, è 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 unità diverse e separate fra di loro, adesso
vengono eseguiti su una sola unità di calcolo. Questa unità è 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 unità di shading era sovracca-
ricata rispetto alle altre, adesso è possibile allocare dinamicamente più risorse al
compito più impegnativo.

Oltre ai vantaggi per le applicazioni di rendering, grazie a questo radicale cam-


biamento è 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 più 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

SISD SIMD

←−
Istruzioni
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 è 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 unità 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
unità di elaborazione, le quali provvedono ad eseguirle.
Una di queste implementazioni è il processore vettoriale. I processori vetto-
riali sono unità di calcolo che, dopo aver letto e decodificato un’istruzione,
la eseguono su più dati prima di passare all’istruzione successiva (Figura
1.1). L’unità di esecuzione lavora su registri vettoriali3 , il cui contenuto è
3
All’interno di un processore, i registri sono piccole quantità di memoria nelle quali vengono

16
Figura 1.1: Pipeline di un processore vettoriale a confronto con la pipeline di
un processore scalare: nel primo le operazioni di fetch, decodifica,
attivazione della memoria, e writeback sono eseguite una sola volta.

gestito dall’unità vettoriale di load–store. Questa è l’unità che si occupa di


leggere e scrivere dalla memoria centrale ai registri vettoriali. È in grado
di operare su più dati contemporaneamente e, in un processore vettoriale,
ve ne possono essere più 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 è 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 è la categoria che comprende le ar-


chitetture in grado di eseguire più flussi di istruzioni su più 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 già 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 è un
insieme 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 è sufficiente sapere che questi non sono altro che processori
multicore, ovvero processori contenenti più unità di elaborazione le quali sono in
grado di lavorare in parallelo e comunicare tra di loro.
La scelta di una tale soluzione è 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 più complessi, gli SM hanno dimostrato di essere più 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 flessibilità rispetto alla soluzione concorrente offerta da AMD.

18
2
Il 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, è il nome dell’architettura che sta alla base
di GPU come la G80 e della più 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 più
importanti, mentre ci si soffermerà 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 è 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’è 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 è 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 è 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 più nelle GPU di fascia alta. Un TPC contiene un
controller di geometria, uno Streaming Multiprocessor Controller (SMC), due
Stream Multiprocessor ed un’unità di texture.

Stream Multiprocessor

Lo SM (Figura 2.3) è 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’unità scalare Multiply-Add (MAD), dando quindi 8 unità MAD
ad ogni SM. Le due SFU sono usate per il calcolo delle funzioni trascendentali
(esponenziali, logaritmiche, trigonometriche) ed inoltre ogni SFU contiene 4 unità
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
Unità delle texture

Nel contesto del GPU computing, l’unità delle texture è una componente che per-
mette di accedere in sola lettura della memoria DRAM. Questa è dotata di un
meccanismo di caching 1 tale da privilegiare gli accessi localizzati ai dati. Al-
l’interno di un TPC, ogni unità 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 però 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 thread 2 per SM sufficientemente alto.

Infatti, quando il numero di thread è abbastanza grande, lo scheduler gestisce le


operazioni da eseguire, in modo da ridurre o eliminare i tempi di inattività degli
SM. Durante il tempo necessario affinché 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 è 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 più SP cores ad accedere allo stesso banco, le operazio-
1
Un meccanismo di caching è un meccanismo che si appoggia generalmente ad una memoria
piccola e veloce (cache) per ridurre i tempi di accesso alla memoria globale
2
Un thread è un flusso di esecuzione all’interno di un’applicazione. Quando il flusso è unico
si parla di applicazione single–thread, altrimenti, quando i flussi sono più di uno, si parla di
applicazioni 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 unità 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 unità 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 unità scalari e non come unità di esecuzione di un processore
vettoriale. Per sottolineare questa differenza, NVIDIA parla di un’architettura
SIMT, ovvero Single Instruction Multiple Threads. A differenza delle unità SIMD,
questa è in grado di massimizzare l’uso delle proprie unità di calcolo, soprattutto

24
quando l’applicazione è 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 è possibile che, in presenza di un’istruzione condizionale
(if-else, switch-case, ecc. . . ), due o più 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 è 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, poiché essa introduce diversi problemi
rispetto ad architetture più semplici. Facendo un confronto con la classe dei
sistemi SIMD è infatti richiesta una maggiore logica di controllo, che comporta un
aumento della superficie del chip e un maggior consumo di energia. È 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 può 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 è implementata attraverso moltiplicazione per il reciproco.

• L’operazione di radice quadrata è implementata attraverso il reciproco della


radice quadrata stessa.

• La politica di recovery adottata per le condizioni di errore è 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 però restano intorno a 2 - 3 ulp.

2.2 CUDA

CUDA è un modello di programmazione parallela ed un ambiente software pro-


gettato per sfruttare le capacità dei nuovi sistemi paralleli multicore e, più 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 già familiarità con questo
ambiente.

Le estensioni indirizzano lo sviluppatore a partizionare un problema in più


sottoproblemi risolvibili in maniera indipendente. Tali problemi, a loro volta,
vengono risolti mediante la cooperazione di più thread, ovvero, di più 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 è 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 è 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 più 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 è data dal primo dei tre valori di
threadIdx. Durante l’esecuzione, per ogni thread, questo valore sarà 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
verrà 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 è assegnata la variabile blockIdx, accessibile all’interno del

28
kernel e contenente il numero di blocco a cui appartiene il thread stesso. Questa
variabile è 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 è 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 dipenderà più solo dalla variabile threadIdx, ma dipenderà
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, è 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 è 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
però 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 è facilmente esprimibile come il rapporto tra il numero di
warps attivi e il numero massimo di warps gestibili da un SM,

active warps
occupacy = .
max active warps

Configurazioni che offrono l’occupazione massima possono essere facilmente co-


struite. Le più efficienti per le GPU della serie G80 sono quelle che hanno almeno
192 thread per blocco. In questo modo lo scheduler è in grado di mascherare
buona parte della latenza di lettura e scrittura, sia della memoria, sia dei registri.
Configurazioni con molti thread per blocco, però, 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 affinché 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 più spazi di memoria durante la


sua esecuzione:

Registri Su ogni SM sono allocati un certo numero di registri e, ad ogni thread, ne


è 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 può essere calcolato
mediante la formula
R
,
B × ceil(T, 32)

30
Figura 2.6: Gerarchia della memoria in CUDA.

dove R è il numero di registri per ogni SM, B è il numero di blocchi attivi


per SM e T è il numero di thread per blocco, che, come si vede, viene
approssimato per eccesso con il più piccolo multiplo di 32.

Memoria globale Questa è la memoria che i thread utilizzano, sia per leggere i
dati da elaborare, sia per scrivere i risultati delle loro operazioni. Essa è
implementata nella DRAM, quindi i suoi tempi di accesso, come già visto
durante lo studio dell’architettura Tesla, sono centinaia di volte superiori a
quelli dei registri: è importante ridurne l’utilizzo il più possibile.

Quando l’accesso alla memoria globale non è evitabile, è 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 metà 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 è uno spazio di memoria implementato nella DRAM e, di conseguenza,
i tempi di accesso sono gli stessi della memoria globale.

Memoria condivisa Questa memoria è accessibile a tutti i thread dello stesso


blocco e, generalmente, è utilizzata per condividere i risultati intermedi dei
processi di calcolo. È implementata mediante i banchi di memoria condivisa
contenuti all’interno di ogni SM, quindi la latenza è 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 È 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 è accessibile attraverso l’unità


delle texture. Rappresenta quindi un’interfaccia di sola lettura alla memoria
globale, che offre un meccanismo di caching ottimizzato per la località 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 può 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
3
Ulteriori informazioni sulla memoria delle texture si trovano in [4], alla quale è stata dedicata
un’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 è
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, già dalla seconda versione della
GPU G80, è stata prevista la possibilità di effettuare operazioni atomiche sulla
memoria globale, dando quindi agli sviluppatori la possibilità 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 è consultabile in [4].

33
2.2.5 Uso della piattaforma

Prima di terminare questa sezione, è importante sottolineare come CUDA sia


stato concepito per sfruttare le capacità 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 è
mostrato in Figura 2.7, dove si osserva l’alternanza tra il codice seriale eseguito
sull’Host, e l’esecuzione parallela di uno o più kernel sul Device.

Figura 2.7: Flusso delle operazioni per un’applicazione costruita con CUDA

34
3
Il 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 già accennato nel-
l’introduzione, è una implementazione ad-hoc del Metodo dei Gradienti Coniugati
per matrici pentadiagonali simmetriche e definite positive (sdp). Questo è 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 è eseguito contemporaneamente da migliaia di thread.

3.1.1 L’ambiente Matlab

Il sorgente originale è stato scritto in linguaggio Matlab e, pertanto, la sua ese-


cuzione avviene per mezzo di un interprete. Quando un software è 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 però ad una parte delle
prestazioni.

Per confrontare i vantaggi che il modello CUDA offre rispetto al modello di


programmazione classico, prima di tutto si è reso necessario riscrivere il software
originale. Si è utilizzato cosı̀, un linguaggio che, una volta compilato, producesse
un programma nativo per la piattaforma dell’Host. Fortunatamente Matlab ha
la facoltà di integrarsi con software esterni, scritti in linguaggi compilabili. In
questo modo all’interno dello stesso ambiente è 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 signature 2 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.
1
Un’istruzione nativa è un’istruzione che viene decodificata ed eseguita direttamente dalla
CPU, senza nessun passaggio intermedio.
2
La signature di un metodo è l’insieme di informazioni che identificano il metodo stesso, in
questo caso: il suo nome, il valore restituito ed i parametri che questo riceve.

36
mxArray è 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 a1,2 . . . a1,n
a1,1 a2,1 . . . am−1,n am,n
 a2,1 a2,2 . . . a2,n 
 
m =:  .
 . .. ... .. 
 . . . 

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. È 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 è 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 capacità 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 può essere
integrata in Matlab; prima con linguaggio Matlab poi con linguaggio C ed infine
in ambiente CUDA.

Matlab Il linguaggio Matlab, grazie alla sua flessibilità 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 più 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, è
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 è 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 è il corpo completo dell’implementazione Matlab sulla quale si è


lavorato. Si divide in due funzioni: la principale, cgmamma, è 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 è 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 x1,1 d1,1
 .. ..  ..
. .
b   
a
 1,1 2,1

 .
  x2,1
 d2,1
..
  

 0 ... ..
. ..
.
 .. ..
bm−1,1 .
   


   .
 .
 .   
 ..

bm−1,1 am,1 0 x   d
cm,n−1   m,1   m,1 
 

 x =
  
 c1,1

0 a1,2 b1,2 0  1,2   d1,2 
 ..   .. 
   
 ... ... .. .. 
 0
 b1,2 . .  .   . 
 . ... ...
   
 ..

a b  xm−1,n 
 d
m−1,n

 m−1,n m−1,n    
0 ... ... cm,n−1 0 . . . bm−1,n am,n xm,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 è 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, è 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 è stata formulata nel 1967 da G.Amdahl nel tentativo di


valutare l’incremento di prestazioni ottenibile da un sistema multiprocessore e,
più generalmente, l’incremento di prestazioni di un sistema in base all’aumento
delle performance di una sua componente. Se T è il tempo di esecuzione di un
dato programma, tale tempo può 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 più “performante”, il tempo


di esecuzione per quest’ultima diventa Tc0 < Tc e il tempo totale si trasforma
diventando
T = Tc0 + 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 è
Tc
Sc = 0 .
Tc
Tc
Data fc = , frazione di utilizzo della componente all’interno del sistema origina-
T
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

1
S= .
fc
(1 − fc ) +
Sc

Questo dimostra che l’aumento delle prestazioni di un sistema è limitato supe-


riormente dalla frazione di utilizzo della componente che si accelera (Figura 3.1).
Infatti si ha che
1 1
Smax = lim = .
Sc →∞ fc 1 − fc
(1 − fc ) +
Sc

3.1.4 Prima analisi delle prestazioni

Alla luce della legge di Amdhal, per conoscere quali fossero le operazioni più 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, è 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 è stata invocata, o
quanto tempo è stato speso all’interno di questa. Con queste informazioni si è 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,


è quella ad avere la maggior frazione di utilizzo. Come primo approccio si è
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.1
17 d=sum(sum(p.*Ap)); 443 1.101 7.3
19 x=x+lam*p; 443 0.852 5.6
22 eta=sum(sum(r.*r)); 443 0.829 5.5
20 r=r-lam*Ap; 443 0.521 3.4
Altre ... 0.616 4.1
Totale 15.107 100

Tabella 3.1: Informazioni prodotte dal profiler di Matlab sull’esecuzione del


metodo cgmamma.

up massimo raggiungibile per cgmamma fosse di solo

1
= 3, 86.
1 − 0.741

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 però un significato principalmente didattico. Questo perché la loro applica-
bilità all’interno dell’ambiente Matlab è molto limitata dagli overhead (costi di
gestione), dovuti alle chiamate di funzione esterne. Come si vedrà, 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 già 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 tratterà dell’implementazione per il Device dell’intero Metodo dei
Gradienti Coniugati, si vedrà come l’ideale consista nell’allocare i dati in memoria
soltanto una volta. Questa soluzione è applicabile quando almeno il ciclo princi-
pale del Metodo dei Gradienti Coniugati è 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. È importante specificare che i
tempi di esecuzione sono riferiti all’hardware della macchina su cui si è 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 ??) è costituita da due funzioni, di cui una co-
stituisce il punto di entrata della function, mentre l’altra è 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 con Figura 3.3: Moltiplicazione con
la diagonale princi- le sottodiagonali più
pale esterne

Figura 3.4: Moltiplicazione con le prime sotto-


diagonali

45
più 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 perché, 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, può non sembrare la scelta migliore. Nella pratica invece non è
cosı̀, rivelando che i meccanismi di caching della CPU sono più 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 può 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 più frequente
alla memoria principale, con un conseguente degrado delle performance.

Si è 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 è 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 è sostanzialmente
identico a quello presentato nell’implementazione Host, ci si sofferma sulle altre
tre funzioni.

La prima ad essere invocata è 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 quantità necessaria. In questa stessa funzione viene impostata l’unità
delle texture che servirà per leggere quei dati il cui accesso è inefficiente a causa
di un allineamento non favorevole. Infatti, come si è già visto, l’accesso diretto
alla memoria globale ha alti tempi di latenza, e per ridurre questo svantaggio è
necessario effettuare il più spesso possibile accessi di tipo coalesced. Questo tipo
di accesso però può essere effettuato solo per la moltiplicazione della diagonale
principale, nella quale l’i-esimo elemento di v è 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 località, ad esempio:
nella moltiplicazione della seconda sottodiagonale, l’i-esimo elemento del vettore
v coinvolgerà 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’unità 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 può competere con una CPU di fascia
superiore.

Device Host
Test m×n matvec(s) cmatvec(s) nvmatvec(s)
speed-up speed-up
1 200 × 200 17,98 6,40 5,3 3,39 2,81
2 300 × 300 49,27 17,54 13,08 3,76 3,39
3 400 × 400 122,58 27,43 21,10 5,80 4,46
4 500 × 500 202,41 44,00 35,51 5,70 4,60
5 600 × 600 310,00 66,69 49,48 6,26 4,64
1 100 × 400 18,67 6,04 5,80 3,21 2,91
2 100 × 900 59,59 15,02 11,57 5,15 3,96
3 100 × 1600 125,63 28,90 21,76 5,77 4,34
4 100 × 2500 210,45 46,78 32,72 6,41 4,49
5 100 × 3600 320,78 71,55 47,82 6,69 4,48
1 400 × 100 17,82 6,49 5,6 3,18 2,74
2 900 × 100 53,83 14,63 13,06 4,12 3,67
3 1600 × 100 119,94 27,6 22,01 5,40 4,34
4 2500 × 100 202,51 44,15 35,81 5,65 4,58
5 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 mostrano
gli speed up delle function sviluppate, rispetto all’implementazione
originale. A numero di test uguale corrisponde un egual numero di
elementi

3.3 Seconda Implementazione

Rispetto alla precedente implementazione, in questa nuova sezione si affronta il


problema dell’allineamento dei dati. Questi, come si è 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 può ottenere
un guadagno sensibile di prestazioni rinunciando però al massimo risparmio di
memoria.

48
L’idea, in questa seconda implementazione, consiste infatti nel ridurre la com-
plessità 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 più 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, è un’operazione resa immediata dalla flessibi-


lità dell’ambiente Matlab che, con una sola istruzione, consente di costruire la
struttura dati di cui si necessita. Nelle due seguenti implementazioni si assumerà
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 è 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.

Device Host
Test m×n matvec(s) nvmatvec(s) cmatvec(s)
speed-up speed-up
1 200 × 200 17,98 3,92 3,47 4,58 5,18
2 300 × 300 49,27 9,32 13,29 5,28 3,71
3 400 × 400 122,58 15,64 23,83 7,83 5,14
4 500 × 500 202,41 24,93 37,72 8,12 5,37
5 600 × 600 310,00 33,96 57,38 9,13 5,40
1 100 × 400 18,67 4,41 3,49 4,23 5,34
2 100 × 900 59,59 8,70 7,95 6,84 7,49
3 100 × 1600 125,63 14,76 21,44 8,51 5,85
4 100 × 2500 210,45 23,31 37,95 9,02 5,54
5 100 × 3600 320,78 32,12 57,62 9,98 5,56
1 400 × 100 17,82 4,80 5,32 3,71 3,34
2 900 × 100 53,83 8,85 7,99 6,08 6,73
3 1600 × 100 119,94 16.01 23,80 7,49 5,03
4 2500 × 100 202,51 25,39 38,07 7,97 5,31
5 3600 × 100 309,95 34,62 58,02 8,95 5,34

Tabella 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
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 è già 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 è 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,
salverà i valori di p[i] e hipm12j[i]. Ogni thread in questo modo potrà 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 osserverà 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 perciò 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, è 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 è stato possibile, è stata utilizzata la tecnica di unrolling
dei cicli, affinché 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 può osservare per l’Host,


all’interno del ciclo principale non c’è una diretta corrispondenza fra chiamate di
funzioni e operazioni svolte. Consultando il listato ??, si osserva infatti che il
ciclo principale del metodo è 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 è 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 già 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 già salvati nei registri, si calcola il prodotto scalare tra r e sé 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 perché 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 è 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 più attivi al passo corrente (Figura 3.6).
La procedura termina quando resta attivo un solo thread per blocco, il quale si
occuperà 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 degli
elementi di un vettore.

54
4
Test 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 è 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 è sdp, e la convergenza del metodo


iterativo è assicurata. Inoltre il suo numero condizionamento è 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 iterazioni
mu kAx − bk22
cgmamma 4 5,44e-5
ccgmamma 512 × 512 100 4 5,27e-5
nvcgmamma 4 8,55e-5
cgmamma 18 4,81e-6
ccgmamma 512 × 512 10 18 4,96e-6
nvcgmamma 18 6,83e-6
cgmamma 46 6,30e-6
ccgmamma 512 × 512 1 46 6,29e-6
nvcgmamma 46 7,18e-6
cgmamma 115 1,37e-5
ccgmamma 512 × 512 0.1 92 1,37e-5
nvcgmamma 92 1,45e-5
cgmamma 273 3,13e-5
ccgmamma 512 × 512 0.01 274 3,13e-5
nvcgmamma 273 3,19e-5
cgmamma 4 2,17e-4
ccgmamma 1024 × 1024 100 4 2,11e-4
nvcgmamma 4 3,43e-4
cgmamma 11 1,96e-5
ccgmamma 1024 × 1024 10 11 1,94e-5
nvcgmamma 11 2,58e-5
cgmamma 33 2,60e-5
ccgmamma 1024 × 1024 1 33 2,59e-5
nvcgmamma 33 2,958e-5
cgmamma 121 5,71e-5
ccgmamma 1024 × 1024 0.1 122 5,75e-5
nvcgmamma 121 6,05e-5
cgmamma 292 1,33e-4
ccgmamma 1024 × 1024 0.01 301 1,37e-4
nvcgmamma 292 1,37e-4

Tabella 4.1: Risultati dei test sulla precisione del risultato. Più basso è il valore
del residuo, maggiore è l’accuratezza della soluzione.

57
il compilatore, e l’hardware adoperato, sono determinanti per i risultati del-
le misurazioni; perciò, di seguito, si riassumono le caratteristiche dell’hardware
utilizzato.

PC1 Il primo personal computer è il sistema portatile sul quale è stato sviluppato
il codice dei metodi. La CPU del sistema è 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, è 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 è un sistema desktop. La sua CPU è


una Intel Core 2 Duo E6750 che lavora alla frequenza di 2,66 GHz. La me-
moria cache è 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, è una più “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 è adoperata la più “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 specificherà 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 è 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 è 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 è 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 proprietà 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 è quello fornito con l’ambiente di
TM
sviluppo Microsoft Visual C++ 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 è 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 up
cgmamma 29,39 1
ccgmamma 512 × 512 1000 6,22 4,72
nvgmamma 4,60 6,39
cgmamma 149,41 1
ccgmamma 512 × 512 5000 31,13 4,79
nvgmamma 22,84 6,54
cgmamma 294,65 1
ccgmamma 512 × 512 10000 61,84 4,76
nvgmamma 46,41 6,39
cgmamma 67,96 1
ccgmamma 1024 × 1024 500 15,91 4,27
nvgmamma 10,03 6,77
cgmamma 135,80 1
ccgmamma 1024 × 1024 1000 32,19 4,21
nvgmamma 18,02 7,36
cgmamma 674,92 1
ccgmamma 1024 × 1024 5000 158,50 4,25
nvgmamma 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 up
ccgmamma 7,11 5,73
512 × 512 1000
nvcgmamma 4,64 1,53 0,48 11,93
ccgmamma 35,35 28,54
512 × 512 5000
nvcgmamma 23,14 1,53 2,42 11,79
ccgmamma 33,98 27,70
1024 × 1024 1000
nvcgmamma 18,15 1,87 1,48 18,72
ccgmamma 168,92 138,11
1024 × 1024 5000
nvcgmamma 90,60 1,86 7,40 18,66
ccgmamma 64,28
1536 × 1536 1000 –1 –
nvcgmamma 3,33 19,30
ccgmamma 320,07
1536 × 1536 5000 – –
nvcgmamma 16,14 19,83
ccgmamma 115,96
2048 × 2048 1000 – –
nvcgmamma 5,82 19,92
ccgmamma 578,07
nvcgmamma 2048 × 2048 5000 – – 29,07 19,89
nvcgmamma 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 è una delle caratteristiche più 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
è 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 più negozi.
1
Non è stato possibile eseguire il test a causa della memoria insufficiente della scheda video.
2
Il risultato è stato ottenuto con una configurazione di 1000 Blocchi da 256 thread ciascuno.
Inoltre la compilazione è stata effettuata con il flag -maxrregcount 10. Questo flag, limita
a 10 il numero di registri utilizzati per ogni thread, permettendo di raggiungere l’occupazione
massima della GPU.

61
speed up speed up
CPU (e) × 103 GPU (e) × 103 rapp. GPU
CPU
prezzo prezzo
1 20, 21
150 × 103 = 6, 67 110 × 103 = 183, 73 27, 55
150 110
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 è solo una stima della reale convenienza economica che si ha nell’utilizzo
di una GPU, poiché non sono stati considerati i costi della memoria; componente
necessaria all’interno di un sistema.

62
5
Conclusioni

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 è 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 è 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 è 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 è 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 più interessante per un eventuale
1
Per 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 capacità di calcolo di
più GPU, si potrebbe comunque incrementare la convenienza economica. Ancora
più 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 più significativi.

64
Bibliografia

[1] Luigi Brugnano, Cecilia Magherini, e Alessandra Sestini. Calcolo Numerico,


capitolo 1. Master università & 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. Più basso è il valore del
residuo, maggiore è 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 funzionalità. Nella GPU la
maggior parte della superficie del core è dedicata ad unità 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 più 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

Potrebbero piacerti anche