modelli di programmazione per acceleratori grafici · graphics processing units. in particolare,...

37
Scuola Politecnica e delle Scienze di Base Corso di Laurea in Ingegneria Informatica Elaborato finale in PROGRAMMAZIONE I Modelli di programmazione per acceleratori grafici Anno Accademico 2017/2018 relatore Ch.mo Prof Alessandro Cilardo candidato Pasquale Di Maio matr. N46002302 1

Upload: dinhmien

Post on 18-Feb-2019

219 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Scuola Politecnica e delle Scienze di BaseCorso di Laurea in Ingegneria Informatica

Elaborato finale in PROGRAMMAZIONE I

Modelli di programmazione per acceleratori grafici

Anno Accademico 2017/2018

relatoreCh.mo Prof Alessandro Cilardo

candidatoPasquale Di Maiomatr. N46002302

1

Page 2: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

ai miei genitori, a mio fratello, allamia fidanzata, agli amici di adesso e a quelli che verranno

2

Page 3: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Indice

Modelli di programmazione per acceleratori grafici............................................................................1Indice ...............................................................................................................................................3Introduzione......................................................................................................................................4Capitolo 1: CUDA............................................................................................................................6

1.1 Modello di Programmazione..................................................................................................61.1.1 Scalabilità automatica ..................................................................................... 71.1.2 Gerarchia dei thread........................................................................................ 81.1.4 Kernel.............................................................................................................. 101.1.4 Gerarchia di memoria...................................................................................... 10

1.2 Funzioni dell'Interfaccia Applicativa...................................................................................111.2.1 Gestione della memoria del dispositivo........................................................... 111.2.2 Qualificatori di variabile................................................................................... 121.2.3 Qualificatori di funzione................................................................................... 12

1.3 Compilazione........................................................................................................................121.4 Esempio completo di codice.................................................................................................13

Capitolo 2: OpenCL.......................................................................................................................142.1 Specifica di OpenCL............................................................................................................15

2.1.1 Modello della piattaforma................................................................................ 152.1.2 Modello di esecuzione..................................................................................... 162.1.3 Modello di programmazione............................................................................ 172.1.4 Modello della memoria.................................................................................... 18

2.2 Profiling OpenCL con eventi................................................................................................192.3 Esempio completo di codice.................................................................................................20

Capitolo 3: Studio della memoria locale in OpenCL.....................................................................233.1 Software e dispositivi utilizzati............................................................................................23

3.1.1 Piattaforma AMD............................................................................................. 233.1.2 Ambiente di sviluppo....................................................................................... 233.1.3 Tool di analisi................................................................................................... 24

3.2 Esecuzione parallela e esecuzione seriale............................................................................243.3 Memoria locale.....................................................................................................................25

3.3.1 Vantaggi della memoria locale........................................................................ 263.3.2 Pattern di accesso........................................................................................... 30

Conclusioni.....................................................................................................................................36Bibliografia.....................................................................................................................................37

3

Page 4: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Introduzione

Un acceleratore grafico, o GPU (acronimo di Graphics Processing Unit), è un componente

hardware nativamente progettato e adoperato per effettuare elaborazioni di grafica 3D e

per la renderizzazione di immagini grafiche.

L'insaziabile e crescente domanda di una grafica 3D sempre più ad alta definizione e

l'espansione del mercato del gaming ha indotto i costruttori di GPU a potenziarne le

risorse di calcolo, al punto da consentire l'evoluzione di questi dispositivi in architetture

multithread, estremamente parallele, con un ingente numero di core e un'elevata larghezza

di banda di memoria. La tremenda potenza computazionale che ne risulta, ha causato la

diffusione di un utilizzo delle GPU per elaborazioni generiche, oltre che nella tradizionale

grafica computerizzata: si parla dunque di GPGPU, general purpose computing on

graphics processing units.

In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono

significative capacità elaborative e le cui operazioni sono intrinsecamente parallele.

Pertanto la scelta di utilizzare le tradizionali CPU risulta poco conveniente in questo

scenario: l'architettura di una CPU è più orientata al flusso di controllo e al data caching

rispetto a quella di una GPU, progettata per effettuare calcoli intensivi e processare dati.

Tale soluzione scarica il processore da una serie di elaborazioni sui dati, consentendo ad

esso di svolgere altri compiti, per un miglioramento complessivo delle prestazioni. Il

confronto tra le due architetture è rappresentato, in maniera estremamente semplificata, in

Figura 1.

Tuttavia, la fruizione delle elevate prestazioni di una GPU è garantita da un'ottimizzazione

e ri-modellazione del codice tale da integrare efficacemente le applicazioni con le

caratteristiche della specifica architettura. Si sono allora diffusi una serie di modelli di

programmazione che andassero oltre il classico sviluppo di applicazioni da eseguire su un

singolo processore, consentendo di sfruttare il parallelismo offerto dalle GPU e di

modellare problemi che sono naturalmente divisibili in attività parallele, e altamente

4

Page 5: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

orientate ai dati, e per eseguirle su una moltitudine di core. I sorprendenti risultati

nell'accelerazione e nel miglioramento delle prestazioni dei sistemi di calcolo ha portato

diversi settori a beneficiare del GPGPU: dall'elaborazione di immagini mediche in

medicina, alla crittografia nell'ambito della sicurezza informatica, dal mining di

criptovalute al calcolo distribuito, e dalla radioastronomia alla bioinformatica.

L'elaborato si propone di analizzare due diffusi modelli di programmazione per

architetture di questo genere, quali CUDA ed OpenCL. Si vedranno le principali funzioni

che i due modelli offrono a supporto dello sviluppo di applicazioni parallele, mostrando il

loro utilizzo in alcuni esempi di codice. Segue una trattazione di stampo più pratico-

sperimentale in cui viene profilato il comportamento di alcuni semplici programmi

realizzati adoperando diverse strategie di parallelismo e di modellazione dei dati, in

rapporto anche alle caratteristiche dell'hardware sottostante.

Figura 1 L'architettura di una GPU dispone di un maggior numero di unità dedite alle

elaborazioni sui dati (in verde)

5

Page 6: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Capitolo 1: CUDA

CUDA (acronimo di Compute Unified Device Architecture) è una piattaforma di calcolo

parallelo general-purpose e un modello di programmazione per architetture parallele

sviluppata da NVIDIA nel 2006. CUDA consente ai programmatori di sviluppare

applicazioni in grado di eseguire elaborazioni parallele sulle GPU delle schede video

NVIDIA sfruttandone la potenza di calcolo per scenari che vanno oltre la grafica. Le

schede che supportano questa architettura sono tra quelle delle serie GeForce, ION Quadro

e Tesla.

Il netto miglioramento delle prestazioni raggiungibili con questa piattaforma giustifica il

successo che ha avuto presso il settore della ricerca scientifica, un esempio: AMBER, il

programma di simulazione di dinamica molecolare usato da una molteplicità di ricercatori

e aziende farmaceutiche per agevolare la scoperta di nuovi farmaci, è accelerato tramite

CUDA.

L'ambiente di sviluppo per CUDA propone una serie di linguaggi che sono estensione di

alcuni dei tradizionali linguaggi di programmazione; fra questi, i più noti sono FORTAN,

C, Java, MATLAB, e Python. In questo capitolo viene trattato CUDA-C, un insieme di

primitive che estendono il linguaggio C consentendo di implementare i concetti chiave del

modello.

1.1 Modello di Programmazione

L'applicazione CUDA è composta da due principali componenti software: una parte di

codice sequenziale, single-threaded, eseguita dalla CPU al fine di gestire il flusso di

controllo e coordinare le interazioni con il dispositivo GPU, e una parte di calcolo

intensivo e di elaborazione dei dati eseguita in parallelo dai core del coprocessore.

Il paradigma consente di sviluppare applicazioni altamente scalabili al variare delle

caratteristiche peculiari delle architetture che supportano CUDA e su cui saranno eseguite.

Esso si basa inoltre su alcune astrazioni chiave, quale quelle sui thread e sulle gerarchie di

6

Page 7: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

memoria nel dispositivo, nonché su una serie di meccanismi di sincronizzazione per le

attività parallele.

1.1.1 Scalabilità automatica

Le GPU che supportano CUDA possono differire dal punto di vista architetturale, per

esempio nel numero di core o nella dimensione della memoria: tuttavia, il problema della

portabilità di un'applicazione è risolto efficacemente dalla piattaforma.

CUDA guida il programmatore a suddividere il problema in una serie di sotto-problemi

risolvibili indipendentemente e parallelamente da blocchi di thread; ciascun sotto-

problema viene ulteriormente partizionato in attività parallele eseguite in maniera

cooperativa dai thread all'interno del blocco. L'idea è quella di mandare ciascun blocco in

esecuzione su un qualunque core disponibile nella GPU (i core sono chiamati Streaming

Multiprocessors nella terminologia CUDA), i thread del blocco saranno quindi eseguiti

parallelamente dalle unità elaborative dell'SM. Lo sviluppatore può stabilire il numero di

blocchi indipendentemente dal numero di core, perché è il sistema di runtime, l'unico a

conoscere le caratteristiche dell'architettura sottostante, ad assegnare a tempo di

esecuzione i blocchi agli SM, in maniera del tutto trasparente all'utente. Si intuisce

banalmente che GPU con un maggior numero di core saranno in grado di eseguire un

maggior numero di blocchi parallelamente e di concludere, generalmente, il lavoro in un

tempo minore rispetto ad un dispositivo con meno core. Lo smistamento dei blocchi di

thread sui vari SM di due differenti architetture è mostrato in Figura 2.

Il meccanismo di scalabilità descritto consente quindi a un'applicazione CUDA di

espandersi su una larga ed eterogenea gamma di dispositivi NVIDIA, senza alcun

problema di portabilità.

7

Page 8: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Figura 2 Dallo schema si osserva che il dispositivo con 4 core termina il lavoro

in un tempo minore rispetto a quello con 2 core (la freccia nera indica la durata

temporale), e l'applicazione può essere eseguita su entrambe le architetture

1.1.2 Gerarchia dei thread

CUDA definisce una gerarchia di thread: i thread possono essere raggruppati in blocchi, e

i blocchi raggruppati in griglie.

Per convenzione un thread è un vettore a 3 componenti, quindi il suo identificatore può

essere un indice monodimensionale, bidimensionale o tridimensionale. Essi devono essere

raggruppati necessariamente in blocchi e griglie della stessa dimensione. Di conseguenza

anche un blocco è identificato in una griglia attraverso un indice n-dimensionale. Questa

organizzazione consente di lavorare efficacemente sui dati in base alle loro dimensioni.

Per esempio, thread monodimensionali saranno utilizzati per array, thread bidimensionali

per matrici, e thread tridimensionali per volumi. Poiché thread di uno stesso blocco

risiedono sullo stesso core e condividono una memoria limitata, il numero massimo di

thread per blocco è 1024. La gerarchia dei thread, per il caso bidimensionale, è

schematizzata in Figura 3.

8

Page 9: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Da tenere presente, è la relazione tra l'identificatore del thread e il suo inidice: per thread

di blocchi monodimensionali essi coincidono, in blocchi bidimensionali di dimensioni

(Dx,Dy), un thread di indice (x,y) ha identificativo pari a x+y*Dx.

Figura 3 Thread bidimensionali sono raggruppati in blocchi bidimensionali, a loro volta

raggruppati in una griglia bidimensionale; thread e blocchi sono identificati dagli indici

di riga e colonna relativi alla posizione che occupano rispettivamente nel blocco e nella

griglia

Il numero di thread per blocco, e di blocchi per griglia, può essere di tipo int o dim3. Di

seguito è mostrato un esempio di codice in cui vengono definite le dimensioni dei blocchi

(16 x 16), e delle griglie (N/16 x N/16) adoperando il tipo dim3.

9

Page 10: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

1.1.4 Kernel

Abbiamo visto che un'applicazione CUDA comprende una parte di codice destinata ad

essere eseguita parallelamente dai core della GPU; nella terminologia del calcolo

parallelo, essa è detta kernel.

Il linguaggio consente di definire un kernel utilizzando il qualificatore __global__, e di

specificare all'atto della chiamata il numero di thread da cui verrà eseguito parallelamente.

In particolare, bisogna specificare la dimensione della griglia di blocchi, e il numero di

blocchi per griglia, adoperando la sintassi di configurazione di esecuzione <<<...>>>.

All'interno del codice del kernel, è possibile accedere all'identificativo del thread corrente

mediante la variabile built-in threadIdx e specificando la dimensione di interesse.

Analogamente, è possibile accedere all'identificativo del blocco di appartenenza del thread

attraverso la variabile blockIdx e alla dimensione di esso tramite blockDim. Inoltre,

all'interno di un kernel si possono fissare dei punti di sincronizzazione, per far si che

thread dello stesso blocco attendano il completamento delle operazioni degli altri prima di

continuare l'esecuzione; questo meccanismo è realizzato mediante la chiamata a

__syncthreads().

Di seguito, un esempio di definizione e chiamata di un banale kernel che esegue la somma

degli elementi di due array di N float, eseguito da un griglia monoblocco di thread

unidimensionali:

1.1.4 Gerarchia di memoria

Il modello prevede che la CPU abbia una sua memoria, host memory, e la GPU una

10

Page 11: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

propria, device memory. La memoria del dispositivo è organizzata gerarchicamente:

– ogni thread possiede una propria memoria privata.

– thread di uno stesso blocco condividono un'area di memoria, shared memory

– tutti i thread condividono una memoria globale, una memoria delle costanti

più utilizzate, e una memoria delle texture, le quali esistono

indipendentemente dai kernel e dai thread

Ognuna di questa aree di memoria può essere acceduta e riferita dal programmatore

mediante appositi qualificatori trattati in seguito.

1.2 Funzioni dell'Interfaccia Applicativa

Il codice dell'host di una semplice e generica applicazione CUDA può essere

schematizzata secondo i seguenti passaggi:

1. Allocazione e inizializzazione della memoria dell'host

2. Allocazione della memoria del dispositivo

3. Trasferimento dei dati dalla memoria dell'host a quella del dispositivo

4. Definizione di una griglia di blocchi

5. Esecuzione del kernel

6. Trasferimento dei risultati dal dispositivo all'host

7. Rilascio delle risorse

Di seguito vengono mostrate alcune funzioni adoperate nell'implementazione dei passaggi

sopraelencati.

1.2.1 Gestione della memoria del dispositivo

Le aree di memoria del dispositivo vengono allocate mediante la primitiva cudaMalloc()

specificando la dimensione della memoria e il puntatore per riferirla; essa può essere

liberata tramite la funzione cudaFree().

Il trasferimento dei dati tra la memoria dell'host e quella del dispositivo avviene

adoperando cudaMemcpy() specificando i puntatori alle memorie, la dimensione dei dati

da trasferire e un flag indicativo del verso di trasferimento.

11

Page 12: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

1.2.2 Qualificatori di variabile

In generale, il compilatore colloca le variabili automatiche dichiarate nel kernel nella

memoria locale al thread se esse non sono accompagnate da alcun qualificatore. Possibili

qualificatori di variabile sono:

– __device__, dichiara una variabile che risiede sul dispositivo

– __constant__, dichiara una variabile che risiede nella memoria delle costanti

– __shared__, dichiara una variabile che risiede nella memoria condivisa del

blocco a cui appartiene il thread corrente.

– __managed__, dichiara una variabile che può essere riferita sia dal codice

dell'host che dal codice del dispositivo

1.2.3 Qualificatori di funzione

I qualificatori di funzione specificano dove una funzione viene eseguita e da quale codice

può essere chiamata.

– __device__, eseguita e chiamata dal dispositivo

– __global__, eseguita sul dispositivo e chiamata dall'host

– __host__, eseguita e chiamata dall'host

1.3 Compilazione

Il kernel può essere scritto sia in un linguaggio di alto livello come il C, o attraverso un set

di istruzioni dell'architettura CUDA chiamato PTX (acronimo di Parallel Thread

Execution). In entrambi i casi, il codice deve essere compilato dal compilatore per cuda

nvcc (NVIDIA CUDA Compiler) in linguaggio macchina. Il compilatore legge il file .cu

contenente il sorgente e separa il codice del dispositivo da quello dell'host; il processo di

compilazione può essere articolato nelle seguenti fasi:

1. compilazione del codice del dispositivo in codice PTX o in forma binaria,

detta cubin object

2. modifica del codice dell'host sostituendo la sintassi <<<...>>> con le

necessarie chiamate di funzioni runtime CUDA per l'invocazione del kernel

dal codice PTX o dal cubin object

12

Page 13: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

3. il codice risultante è salvato in un file .c e può essere compilato utilizzando

un qualunque altro compilatore per il linguaggio C

1.4 Esempio completo di codice

Di seguito si riporta il codice completo di un programma che effettua la somma elemento

per elemento di due vettori, riassumendo gran parte dei concetti esposti nel capitolo.

L'operazione viene effettuata dal kernel e la strategia di parallelismo scelta prevede che

ogni thread si occupi di calcolare un solo elemento del vettore risultante, accedendo alla

posizione dell'elemento attraverso il suo identificativo. Si noti che si è utilizzata una

gerarchia di thread monodimensionali dato che si sta lavorando su degli array.

13

Page 14: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Capitolo 2: OpenCL

OpenCL (acronimo di Open Computing Language) è un framework di programmazione

basato sui linguaggi C e C++ specificamente progettato per offrire supporto allo sviluppo

di ambienti di calcolo eterogenei. Inizialmente proposto da Apple, è stato in seguito

ratificato insieme a NVIDIA, Intel e AMD, e infine completato da Khronos Group.

Il calcolo eterogeneo comprende applicazioni sia seriali che parallele e caratterizza sistemi

costituiti da dispositivi di vario genere, dai microprocessori multicore alle GPU, dagli

FPGA alle CPU, e via dicendo. In sistemi di questo tipo, la moltitudine di piattaforme a

disposizione consente alle applicazioni di sfruttarne il parallelismo e di mappare i propri

task sul miglior dispositivo disponibile al momento, al fine di migliorare le performance.

In virtù di tali considerazioni e dello scopo per il quale è stato progettato, OpenCL, a

differenza di CUDA utilizzato per accelerare applicazioni di calcolo che girassero su GPU

NVIDIA, è supportato su dispositivi di diversi produttori: GPU NVIDIA e AMD, FPGA

Xilinx e Altera, e processori ARM. Infatti questo framework fornisce un'interfaccia di

programmazione abbastanza generale affinché un'applicazione progettata per il dispositivo

di un particolare produttore possa essere eseguita sull'hardware di un'altra casa produttrice

senza penalizzarne, in generale, le performance.

La struttura di un programma OpenCL non differisce di gran lunga da quella di un

programma CUDA, essa è costituita da codice eseguito da un host, e da codice eseguito da

un qualunque dispositivo, il kernel. In effetti il modello di programmazione di OpenCL

propone una serie di astrazioni e concetti chiave tipici di CUDA, rivisitati in modo da

renderli indipendenti dalle caratteristiche del particolare dispositivo su cui girerà

l'applicazione.

In questo capitolo si vedrà un tipico scenario OpenCL, verranno esposti i principi

fondamentali del linguaggio, e presentate le principali funzioni dell'interfaccia di

programmazione.

14

Page 15: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

2.1 Specifica di OpenCL

I concetti chiave che strutturano un programma OpenCL sono riassumibili in quattro parti

dette modelli.

1. Modello della piattaforma: specifica come è organizzato il sistema, ossia con un host

che coordina l'esecuzione di più dispositivi su cui girano kernel OpenCL

2. Modello d'esecuzione: specifica come l'host configura l'ambiente e come interagisce

con i dispositivi, definendo anche la strategia di decomposizione di un un problema

in sotto-problemi risolvibili da thread, nonché i meccanismi di concorrenza e

parallelismo

3. Modello di programmazione: specifica come la suddivisione in insiemi di thread è

fisicamente associata alle unità elaborative dei dispositivi

4. Modello della memoria: specifica la gerarchia di memoria virtuale e i tipi memory

object.

Per comprendere il significato dei modelli di cui sopra, si illustra un tipico scenario di

calcolo eterogeneo implementato con il paradigma OpenCL.

Ipotizziamo una piattaforma costituita da una CPU x86, il nostro host, e da un dispositivo

GPU, questo potrebbe essere il modello della piattaforma. L'host configura un kernel e

invia un comando alla GPU affinché possa eseguirlo con una certa strategia di

parallelismo: il modello di esecuzione. Secondo la gerarchia e i tipi definiti dal modello

della memoria, il programmatore può allocare una memoria virtuale per i dati con cui il

dispositivo dovrà lavorare, sarà poi il sistema di runtime a mappare queste aree di

memoria con la gerarchia in base a cui è organizzata la memoria fisica. Inoltre la GPU

crea i thread hardware per l'esecuzione del kernel, mappando ognuno di essi su una unità

elaborativa del dispositivo, questo è fatto tramite il modello di programmazione.

A fine capitolo sarà mostrato un esempio di utilizzo delle API presentate di seguito.

2.1.1 Modello della piattaforma

Una piattaforma OpenCL è costituita da un host e una serie di dispositivi di cui il modello

ne definisce i ruoli e fornisce una rappresentazione astratta dell'hardware. Ogni dispositivo

è costituito da più unità di calcolo, compute units, ciascuna avente più unità elaborative,

15

Page 16: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

processing elements. L'architettura logica di una piattaforma OpenCL è schematizzata in

Figura 4.

Figura 4 L'host coordina più dispositivi; ciascuno ha più unità di calcolo suddivise in diversi

elementi di processo (PE)

Essendo la piattaforma una rappresentazione logica di un sistema fisico, è chiaro che, dato

un sistema, esistono più piattaforme associato ad esso, di conseguenza le API OpenCL per

la gestione delle piattaforme consentono all'applicazione di scegliere la piattaforma

desiderata: questo meccanismo è la chiave per rendere un programma OpenCL portabile

su varie architetture di calcolo eterogeneo. Infatti una piattaforma non è altro che

l'interfaccia tra l'architettura del dispositivo di un particolare venditore e il sistema di

runtime. Per esempio, in un sistema costituito da una CPU Intel e una CPU AMD, sono

disponibili due piattaforme, ognuna delle quali utilizza la CPU dell'altro produttore come

dispositivo di accelerazione: sta al programmatore stabilire quale piattaforma logica

utilizzare per quella particolare configurazione hardware.

La principale funzione che mette a disposizione il modello è clGetPlatformIDs(), che

consente di scoprire quante piattaforme sono disponibili nel sistema, salvandone gli id

logici in un'area di memoria riferita da un puntatore da fornire in ingresso.

Scelta la piattaforma, la funzione clGetDeviceIDs() consente di scoprire i dispositivi

disponibili specificandone il tipo di cui necessita l'applicazione (GPU, CPU, ecc.).

2.1.2 Modello di esecuzione

In OpenCL i meccanismi delle interazioni tra host e dispositivo scelto, nonché la gestione

16

Page 17: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

della sua memoria e dei kernel che vi saranno eseguiti, sono coordinati da un ambiente

astratto detto contesto. Quindi è necessario istanziare un contesto associato al dispositivo

con cui interagire attraverso la funzione clCreateContext().

Siccome l'host invia i compiti da svolgere al dispositivo sotto forma di comandi, per

completare la configurazione dell'ambiente di esecuzione del sistema bisogna creare una

command-queue, il nostro meccanismo di comunicazione host-device che consente al

programmatore di sottoporre al dispositivo attività quali trasferimento di dati, esecuzione

di kernel, e così via; è sufficiente accodare un comando affinché il kernel lo prelevi e lo

esegua, senza alcun segnale di conferma da parte dell'host. Al tal fine viene adoperata la

funzione clCreateCommandQueueWithProperties(), specificando il contesto associato

e il dispositivo; ogni dispositivo scelto avrà bisogno di una propria command-queue per

ricevere comandi.

2.1.3 Modello di programmazione

Analogamente a CUDA, OpenCL mette a disposizione un meccanismo di scalabilità che

rende le applicazioni indipendenti dalle caratteristiche del dispositivo tramite il modello di

programmazione. Il programmatore evita di conoscere il numero di core del dispositivo o

di tenere conto dell'overhead apportato dai context-switch tra i thread se il numero di

questi supera le unità di calcolo disponibili; ciò avviene suddividendo il problema in

attività parallele mappate sui core fisici a tempo di esecuzione.

L'unità di esecuzione parallela in OpenCL, analoga del cuda-trhead, è detta work-item,

anch'essa identificata con indici da uno a tre dimensioni. Anche nel caso di questo

framework di programmazione, quando l'host invia al device un kernel, esso viene

eseguito parallelamente da un numero di work-item stabilito dal programmatore. Prima di

mandare in esecuzione un kernel, bisogna quindi definire la dimensione dello spazio di

indirizzamento per l'esecuzione, che corrisponde esattamente al numero di work-item;

inoltre i work-item vengono raggruppati in insiemi, work-group, pertanto è da specificare

anche la dimensione di essi (lo spazio di indirizzamento viene partizionato in base alla

dimensione di un gruppo). Il tipo utilizzato per specificare le dimensioni di cui sopra è

size_t.

17

Page 18: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Abbiamo visto che un'applicazione comprende una parte di codice OpenCL, la quale

contiene la definizione dei kernel. Questo codice viene compilato a tempo di esecuzione

attraverso una serie di chiamate API. In particolare il codice OpenCL deve essere

dapprima salvato in un array di caratteri e trasformato in program object tramite la

funzione clCreateProgramWithSource(); questo oggetto va poi compilato con

clBuildProgram(), e infine, istanziando un kernel object, il kernel va estratto dal program

object tramite clCreateProgram(), specificandone il nome.

Prima di inserire un kernel nella command-queue, bisogna passargli i parametri con

clSetKernelArg(), chiamata un numero di volte pari a quello degli argomenti necessari.

2.1.4 Modello della memoria

Il modello della memoria rende un'applicazione indipendente dall'organizzazione della

memoria fisica, definendo le modalità con cui il codice può manipolare i dati della

memoria virtuale OpenCL.

I dati con cui lavora il dispositivo devono essere incapsulati in un memory object di tipo

buffer, image o pipe; questo elaborato tratta solo il tipo buffer, analogo agli array C, in cui

i dati sono memorizzati in maniera contigua; un buffer può essere creato mediante

clCreateBuffer().

Una volta creato il buffer, bisogna trasferire i dati dalla memoria dell'host a quella del

dispositivo; questa operazione avviene inviando un comando di trasferimento al

dispositivo tramite la command-queue adoperando le funzioni clEnqueueWriteBuffer()

se è verso il dispositivo, e clEnqueueReadBuffer() se è dal dispositivo, e specificando le

aree di memoria tra le quali avviene lo scambio.

Anche in OpenCL la memoria virtuale del dispositivo è organizzata gerarchicamente: c'è

una memoria globale condivisa da tutti i work-item in esecuzione nel sistema, che può

essere riferita con il qualificatore __global, la quale comprende anche una memoria delle

costanti più utilizzate, una memoria condivisa tra i work-item di uno stesso gruppo detta

local memory, riferita con il qualificatore __local, molto più veloce di quella globale e il

cui utilizzo può migliorare di gran lunga le prestazioni del sistema, e una memoria privata

per ogni work-item.

18

Page 19: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Il programmatore può sincronizzare l'esecuzione di work-item all'interno dello stesso

gruppo adoperando i metodi barrier() o work_group_barrier(), i quali bloccano i thread

in attesa del completamento delle attività degli altri. Questo meccanismo è molto utile se si

vogliono, ad esempio, sincronizzare gli accessi successivi prima in scrittura e poi in lettura

alla memoria locale di un gruppo.

2.2 Profiling OpenCL con eventi

OpenCL mette a disposizione un meccanismo per calcolare il tempo di esecuzione di un

qualunque comando da parte del dispositivo, a partire dal momento in cui esso viene

estratto dalla command-queue al momento in cui viene portato a compimento, fornendo

quindi la stima effettiva della durata di esecuzione senza il ritardo causato dall'interazione

host-device o dal trasferimento effettivo del comando al dispositivo da parte del DMA.

Ciò avviene tramite l'utilizzo degli event object, particolari oggetti che il programmatore

può collegare ad un comando per interrogarne le caratteristiche successivamente alla sua

esecuzione, per esempio la durata di esecuzione. Il procedimento è il seguente:

1. Invio del comando nella command-queue passando come parametro alla

funzione di accodamento l'event object

2. dichiarazione di due variabili (float o long) per memorizzare gli istanti temporali di

interesse (inizio e fine esecuzione)

3. uso della funzione clGetEventProfilingInfo() unitamente ai flag

CL_PROFILING_COMMAND_START(END) per salvare nelle variabili gli istanti

di inizio e fine esecuzione del comando, restituiti in nanosecondi

4. effettuare una banale differenza tra i valori delle due variabili

Quello descritto rappresenta un modo macchinoso ma efficace per stimare, ad esempio, la

durata di esecuzione di un kernel al fine di valutare le prestazioni del sistema. Nel capitolo

successivo sarà esposto un modo più elegante, ossia l'utilizzo di un apposito tool software

chiamato CodeXL, che non sporca il codice del programma.

19

Page 20: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

2.3 Esempio completo di codice

Di seguito è mostrato il codice di un programma che invia al dispositivo un kernel che

incrementa di 2 il valore di un array di 2048 interi. Il parallelismo è ottenuto facendo in

modo che ogni work-item monodimensionale lavori su un solo elemento dell'array, quello

corrispondente al suo id. Inoltre viene calcolata la durata di esecuzione del kernel con il

metodo descritto nel paragrafo precedente.

Codice del kernel OpenCL:

da notare che entrambi gli array sono salvati nella memoria globale del dispositivo.

Codice dell'host (C++):

il vettore è inizializzato ai primi 2048 interi; banalmente si utilizza il primo dispositivo

della prima piattaforma scoperta.

20

Page 21: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

85 viene creata una coda abilitando il profiling; 88-89 vengono creati i buffer del

dispositivo, uno in sola lettura (input) e un altro in sola scrittura (output); 92 trasferimento

dell'array nel buffer di input del dispositivo; 95-101 creazione e compilazione del kernel;

104-107 definizione di uno spazio di indirizzamento monodimensionale di 2048 work-

item suddivisi in gruppi di 256.

110-112 passaggio dei parametri (i memory object) al kernel; 115 invio del kernel alla

command-queue; 117-122 calcolo della durata di esecuzione del kernel; 128-130 verifica

dei risultati.

21

Page 22: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Si trascura la parte di codice dedita al rilascio delle risorse dell'host e del dispositivo.

Di seguito l'output dell'esecuzione del programma:

Da notare come le operazioni del kernel, ossia la lettura di ciascuno di 2048 elementi di un

array, il loro incremento, e il loro salvataggio in un ulteriore array, vengono eseguite in

0,0035 millisecondi grazie al calcolo parallelo che OpenCL consente di implementare.

22

Page 23: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Capitolo 3: Studio della memoria locale in OpenCL

Si è definito il GPGPU introducendone intuitivamente i vantaggi in termini di

accelerazione e di miglioramento delle prestazioni di calcolo di un sistema. Inoltre si sono

discussi due linguaggi di programmazione utilizzati in quest'ambito, mostrandone le

principali funzioni dell'interfaccia applicativa adoperandole in semplici esempi di codice.

In questo capitolo vengono quantificati i guadagni che si hanno con il GPGPU nel

contesto di applicazioni scritte in OpenCL e di una particolare piattaforma hardware.

3.1 Software e dispositivi utilizzati

Di seguito sono illustrate la piattaforma hardware su cui si lavora, l'ambiente di sviluppo e

le librerie utilizzate, nonché il tool software adoperato per analizzare alcuni parametri

relativi alle prestazioni del sistema.

3.1.1 Piattaforma AMD

La piattaforma di lavoro è costituita da un host e da un unico dispositivo di accelerazione,

una GPU. In particolare come host si è utilizzato un processore AMD FX 4300, che

possiede 4 core e una memoria cache totale di 8 MB; esso lavora con un clock di 3,8 GHz.

Il dispositivo è una scheda grafica AMD Radeon r7 360 basata su una GPU avente 768

stream core, stream processors nella terminologia AMD, distribuiti in 12 core, compute

units, e una memoria di 2048 MB.

3.1.2 Ambiente di sviluppo

La GPU AMD utilizzata supporta la versione 2.0 di OpenCL, pertanto lo sviluppo è

avvenuto adoperando le librerie OpenCL compatibili con i dispositivi AMD e raggruppate

nel toolkit AMD APP SDK 3.0 fornito in rete dalla casa produttrice per la realizzazione di

applicazioni di calcolo eterogeneo in linguaggi come OpenGL, OpenCV, Bolt e OpenCL.

Infine l'editing del codice, il debugging e la compilazione, sono avvenuti tramite l'utilizzo

dell'ambiente integrato di sviluppo Microsoft Visual Studio 2017. Il sistema operativo in

cui è avvenuto lo sviluppo e il testing delle applicazioni è Windows 7 a 64 bit.

23

Page 24: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

3.1.3 Tool di analisi

Per quantificare i vantaggi ottenuti dall'accelerazione tramite GPU, è stato utilizzato il tool

software CodeXL, estensione di Microsoft Visual Studio, sviluppato da AMD. Esso

funziona in tre modalità di studio del software: debugging della GPU, profiling del

comportamento di un'applicazione che gira su una GPU o CPU AMD in termini di svariati

parametri prestazionali, e analisi statica della GPU e di frame grafici. In vista degli scopi

dell'esperienza, il tool è stato adoperato esclusivamente in modalità profiling di GPU. In

particolare si utilizzano le funzionalità Application Timeline Trace, che consente di tener

traccia della durata di esecuzione di kernel, funzioni, comandi OpenCL, e così via, e GPU

Profiling Counters, che fornisce una serie di informazioni come l'utilizzo della memoria,

gli stalli e i conflitti tra gli accessi in memoria.

3.2 Esecuzione parallela e esecuzione seriale

Prima di effettuare analisi più approfondite del comportamento di un'applicazione

OpenCL dal punto di vista delle prestazioni, si può quantificare il primo e intuitivo

guadagno che il calcolo parallelo comporta rispetto a quello seriale: l'accelerazione dei

calcoli e la notevole riduzione del tempo di esecuzione.

In particolare, l'analisi è avvenuta lanciando in esecuzione un semplice kernel che somma

due array di 2048 interi elemento a elemento salvando i risultati in un terzo array. Sono

state realizzate due versioni del kernel, una che può essere eseguita parallelamente da più

work-item, ciascuno dei quali si occupa di calcolare un solo elemento del vettore di uscita,

ossia quello relativo alla posizione coincidente con il proprio identificativo, e una versione

seriale che viene eseguita da un solo work-item, che pertanto si occupa di sommare tutti

gli elementi dei vettori. Di seguito è riportato il codice delle due versioni del kernel:

24

Page 25: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

I due kernel sono stati eseguiti nel contesto del tool CodeXL in modalità profiling.

L'output generato da CodeXL relativo all'esecuzione del kernel seriale è parzialmente

mostrato di seguito:

Quello relativo al kernel parallelo:

Dai risultati si osserva che la durata dell'esecuzione del kernel (quarto parametro),

espressa in millisecondi, si riduce di ben due ordini di grandezza nel caso del calcolo

parallelo: le elaborazioni vengono effettivamente accelerate nel GPGPU, con notevoli

guadagni.

3.3 Memoria locale

L'accelerazione dei tempi di calcolo che si ottiene con il GPGPU può essere spinta ben

oltre rispetto a quanto visto nell'esempio precedente, sfruttando adeguatamente gli

strumenti messi a disposizione dal modello di programmazione OpenCL. Infatti, i kernel

possono portare a termine il proprio lavoro in maniera ancor più rapida a seconda delle

25

Page 26: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

aree di memoria nelle quali salvano i dati con cui lavorare. Nel capitolo 2 si è visto che

OpenCL si basa sulla gerarchia di memoria globale – locale – privata, in particolare, è

l'utilizzo della memoria locale, in alternativa a quella globale, che comporta migliori

risultati. I dispositivi GPU sono dotati di una memoria locale ad ogni core, detta LDS

(acronimo di Local Data Store) nella terminologia AMD, che offre una larghezza di banda

di gran lunga maggiore rispetto alla memoria globale; essa quindi apporta diversi vantaggi,

in quanto consente di effettuare scambi di dati più efficienti tra i work-item di uno stesso

gruppo, che si ricorda essere mappati sullo stesso core, e di ridurre l'utilizzo della

larghezza di banda della memoria globale. Inoltre, i guadagni più significativi si hanno

quando uno stesso dato viene acceduto più volte perché riutilizzato in diverse elaborazioni,

in questo caso, memorizzare il dato di interesse nella memoria globale, ne rallenterebbe gli

accessi, perché gran parte della larghezza di banda verrebbe sprecata dai molteplici e

continui accessi da parte dei work-item dell'intero sistema. In alternativa, memorizzare il

dato in memoria locale, consentirebbe ai work-item di uno stesso gruppo di sfruttarne la

maggior larghezza di banda per accelerare i ripetuti accessi.

Di seguito sono trattati dei casi di studio che analizzano i vantaggi e le problematiche che

derivano dall'utilizzo della LDS.

3.3.1 Vantaggi della memoria locale

Il netto miglioramento delle prestazioni che comporta l'utilizzo della memoria locale è

mostrato lavorando su un semplice kernel che prende in ingresso due array di float,

effettua banali elaborazioni sugli elementi di uno e salva i risultati nell'altro. Ogni work-

item, in particolare, effettua elaborazioni sull'elemento che si trova nella posizione relativa

al suo identificativo, nonché sull'elemento immediatamente successivo e su quello

immediatamente precedente. Tuttavia tutti i work-item utilizzano i primi tre elementi

dell'array di ingresso, pertanto questi dati vengono riutilizzati e acceduti frequentemente

da thread diversi durante le elaborazioni. Questo è il tipico caso in cui converrebbe salvare

i dati riutilizzati in memoria locale per non sprecare la larghezza di banda della memoria

globale.

Di seguito è mostrato il codice del kernel senza l'utilizzo della memoria locale:

26

Page 27: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Successivamente è mostrato il codice del kernel con l'utilizzo della memoria locale:

Innanzitutto osserviamo che i dati comuni alle elaborazione di tutti i work-item sono gli

elementi A[0], A[1], A[2], inoltre, per sottoporre al dispositivo un carico computazionale

più impegnativo, e per aumentare il numero di accessi ai primi tre elementi di A da parte

di un singolo work-item, si è ripetuta l'elaborazione per 800 cicli, in modo da apprezzare

in maniera più evidente il miglioramento della performance; da notare l'uso della funzione

get_local_id(0) per ottenere l'identificativo del work-item all'interno del gruppo di

appartenenza, utile per accedere alle posizioni della memoria locale. Infine si è utilizzata

27

Page 28: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

la primitiva barrier(CLK_LOCAL_MEM_FENCE) alla riga 26 per sincronizzare i work-

item all'interno di un work-group al fine di completare tutte le scritture in memoria locale

prima di procedere alle letture che la coinvolgono.

Si omette il codice dell'host ritenuto superfluo ai fini dell'analisi che si intende condurre,

tuttavia è da tenere presente che si utilizza un numero di work-item totale pari al numero

di elementi dell'array A, e un numero di work-item per work-group pari a 256.

Di seguito vengono comparati i tempi di esecuzione dei due kernel, aumentando di volta in

volta la dimensione dell'array di lavoro, e quindi il numero di work-item.

Si parte con un array di 1024 elementi, i risultati dell'esecuzione dei kernel nel contesto

del tool CodeXL, sono mostrate di seguito:

Il kernel non ottimizzato impiega 0,65 millisecondi.

Il kernel che utilizza la memoria locale ne impiega 0.004, ben due ordini di grandezza in

meno. Un risultato simile si ottiene se si raddoppia la dimensione a 2048:

28

Page 29: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Nonostante i guadagni, si ritiene che la dimensione dei dati di lavoro, 1024 o 2048, sia di

un ordine di grandezza minore rispetto a quelle di dati utilizzati in applicazioni reali, basti

pensare a programmi che adoperano dati di grossa taglia per elaborare immagini e video.

Di conseguenza, si prova ad aumentare l'ordine di grandezza dell'array per apprezzare

guadagni più significativi. Nel caso di un array di 40960 elementi si ottiene:

In questo caso si ha un guadagno addirittura di 3 ordini di grandezza. Si aumenta di un

ulteriore ordine di grandezza la dimensione, 819200, ottenendo:

Il guadagno è ancora di 3 ordini di grandezza, un grande risultato, considerando che,

lavorando con un array di 819200 elementi, i tempi di esecuzione sono ancora una piccola

frazione di millisecondo, proprio come nei casi in cui il vettore aveva un minor numero di

elementi. Inoltre, gli elevati tempi di esecuzione che si hanno nel caso del kernel non

ottimizzato, sono dovuti anche al fatto che il numero di work-item è pari a quello degli

elementi dell'array, pertanto quando quest'utlimo è elevato, una grande quantità di work-

29

Page 30: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

item accede in memoria globale sprecandone la larghezza di banda.

Con tali sperimentazioni si è quindi provato, per grandi linee e servendosi di un kernel

banale, che i dati con cui i work-item lavorano frequentemente andrebbero memorizzati in

memoria locale, per una notevole riduzione della durata di esecuzione.

3.3.2 Pattern di accesso

I vantaggi che comporta l'utilizzo della memoria locale non sono sempre garantiti, talune

volte infatti gli accessi in memoria non sono efficienti come quanto visto negli esempi

precedenti. Ciò è dovuto alle caratteristiche della particolare architettura con cui è

realizzata la memoria locale nella maggior parte dei dispositivi GPU.

In generale una memoria locale è costituita da una serie di banchi di memoria ciascuno

accessibile da un solo thread alla volta, e poiché indirizzi diversi possono essere mappati

sullo stesso banco, quando più thread tentano di accedere allo stesso banco (conflitti sui

banchi) da indirizzi diversi, gli accessi non possono avvenire parallelamente e pertanto

vengono serializzati, causando ritardi nei tempi di esecuzione.

In particolare nella maggior parte delle GPU AMD, inclusa quella utilizzata per i casi di

studio trattati in questo elaborato, la memoria locale è costituita da 32 banchi, ciascuno

largo 4 byte e profondo 256, quindi ogni banco dispone di 1024 B di memoria, per un

totale di 32 kB. Inoltre i dati sono memorizzati con il meccanismo del mapping ciclico:

indirizzi che puntano a dati che distano 128 byte sono mappati sullo stesso banco. Per

esempio, nel caso di un array di interi, ogni elemento occupa 4 byte, pertanto, le posizioni

da 0 a 31 sono mappate nella prima riga dei banchi, quelle da 32 a 63 nella seconda riga e

così via; quindi gli interi nelle posizioni 0 e 32 sono memorizzati sullo stesso banco, lo

stesso dicasi per gli elementi 1 e 33, 2 e 34 e via dicendo.

Di seguito vengono trattati dei casi di studio che sperimentano diversi pattern di accesso e

osservano i risultati che si hanno in termini di tempi di esecuzione e percentuale di

conflitti sui banchi. La percentuale di conflitti sui banchi è calcolata tramite il tool

CodeXL: essa rappresenta la percentuale della durata temporale dello stallo causato dagli

accessi sullo stesso banco, in rapporto alla durata totale dell'esecuzione del kernel.

Innanzitutto si è lanciato in esecuzione un kernel che effettua accessi in memoria locale

30

Page 31: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

tramite un pattern di accesso casuale. Quello che ci si aspetta, è che si generino conflitti,

in quanto non accedendo a posizioni sequenziali, ma casuali, più work-item potrebbero

confliggere sullo stesso banco. Inoltre bisogna tenere presente che solo un sottoinsieme del

work-group viene eseguito parallelamente sulle 64 ALU disponibili nel core, tale insieme

è detto wavefront nella terminologia del calcolo parallelo. Di seguito è mostrato il codice

parziale di due versioni dello stesso kernel, una che effettua accessi sequenziali in

memoria locale, ed una che effettua accessi casuali:

Il kernel effettua delle banali elaborazioni in un loop di 800 ripetizioni accumulando i

31

Page 32: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Generando indici casuali compresi tra 0 e 63 ed eseguendo i kernel nel contesto di

CodeXL, per quello ad accesso sequenziale si osserva:

In tal caso il il parametro LDSBankConflict(%) è ovviamente nullo, in quanto, sebbene i

work-item in esecuzione parallela siano 64 e tentino di accedere a 32 banchi (si ricorda

che gli indirizzi 0-32, 1-33, e così via, sono mappati sugli stessi banchi), il meccanismo

hardware della LDS esamina le richieste di accesso dei primi 32 work-item, e poi dei

successivi 32, in due cicli di esecuzione separati, pertanto non si genera il caso in cui più

work-item richiedano lo stesso banco nello stesso ciclo esecutivo.

Per il kernel ad accesso casuale invece si osserva:

La percentuale dei conflitti è aumentata del 10%, con un conseguente aumento del tempo

di esecuzione dovuto all'accodamento che si genera quando gli accessi di work-item che

tentano di indirizzare lo stesso banco vengono serializzati.

Provando a generare indici casuali compresi tra 0 e 255 invece, per il caso del kernel ad

accesso sequenziale si ottengono gli identici risultati perché l'accesso non dipende da essi,

per il kernel ad accesso casuale si ottiene:

La percentuale dei conflitti è ulteriormente aumentata, così come il tempo di esecuzione,

ciò è dovuto al fatto che i dati sono memorizzati con il meccanismo del mapping ciclico;

32

Page 33: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

infatti, nel caso di 64 indici casuali, potrebbe essere poco probabile che si ottengano più di

2 o 3 indirizzi mappati sullo stesso banco, ossia che differiscano di 32 posizioni (per

esempio 3, 3 e 62, o 0, 31 e 31), pertanto i 64 work-item della wavefront accedono più o

meno a tutti i banchi, e solo in prossimità di alcuni si generano code che coinvolgono 2 o

al massimo 3 accessi. Invece, nel caso di 256 indici casuali, potrebbe essere leggermente

più probabile che si generino molti indirizzi mappati sullo stesso banco, perché in questo

caso possono differire non solo di 32 posizioni, ma anche di multipli di 32 (per esempio 0,

32, 64 e 64, o 30, 254, 62 e 126). Ne consegue un leggero aumento del tempo di

esecuzione, dato che su alcuni banchi potrebbero esserci anche 5 o più work-item in attesa:

in effetti basterebbe che ciò accada per un solo banco, dato che il ritardo è unicamente

dovuto al banco che ha più accessi accodati. Ad ogni modo, si è provato che i pattern di

accesso casuali possono indirizzare lo stesso banco.

Non sono solo gli accessi casuali a causare dei conflitti, infatti, anche un pattern di

accesso regolare può comportare problematiche dalle conseguenze ugualmente critiche a

quelle viste nell'esempio precedente. Di seguito è analizzato un kernel che esegue accessi

regolari in memoria, in particolare, accede alla posizione data dal suo identificativo locale

moltiplicato per una stride, ed è proprio tale meccanismo a causare dei conflitti. Il codice

del kernel:

Esso esegue delle banali somme di interi prelevati da due array memorizzati in memoria

locale, accumulando di volta in volta il risultato in una variabile privata sum e salvandolo

infine nella memoria globale. Per sottoporre alla GPU un carico elaborativo più

33

Page 34: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

significativo e per aumentare il numero di accessi in memoria locale, le computazioni sono

ripetute per 800 volte. Si omette il codice dell'host, tenendo presente che la dimensione

dell'array A e il numero di work-item del sistema sono pari a 819200, e che ciascun

gruppo dispone di 32 thread. Di seguito sono mostrati i risultati che si ottengono

eseguendo il kernel nel contesto di CodeXL con stride pari a 1:

Come ci si aspetta, il parametro LDSBankConflict(%) è nullo perchè con stride pari ad 1

ognuno dei 32 work-item sta accedendo distintamente ad uno dei 32 banchi in maniera

adiacente e sequenziale, pertanto non ci sono conflitti.

La situazione cambia ponendo la stride pari a 2:

In tal caso la percentuale dei conflitti è aumentata, in quanto con stride pari a 2, i work-

item accedono a posizioni non adiacenti, ma intervallate di una, ossia alle prime 64

posizioni pari; a causa del mapping ciclico (si ricorda che la GPU utilizzata dispone di 32

banchi per ogni memoria locale), le posizioni pari da 32 a 62 sono mappate sugli stessi

banchi delle posizioni pari da 0 a 30, quindi ad ognuno di questi banchi tentano di

accedere due work-item, di conseguenza gli accessi vengono serializzati e il tempo di

esecuzione aumenta, seppur di 2 ms, rispetto a quello che si ha nel caso di stride pari ad 1.

Ancora, ponendo la stride uguale a 32:

34

Page 35: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

In questa situazione la percentuale di conflitti raggiunge un valore quasi inaccettabile, dato

che causa un ritardo del tempo di esecuzione di ben 9,5 ms rispetto all'esempio precedente.

Ciò è dovuto al fatto che a differenza del caso con stride pari a 2, in cui per ogni banco

venivano serializzati solo due work-item, con stride pari a 32, tutti i work-item tentano di

accedere allo stesso banco, quello su cui sono mappati gli interi nelle posizioni 0, 32, 64 e

così via, quindi per tale banco devono essere serializzati gli accessi di ben 32 work-item;

questo rallenta di gran lunga i tempi di accesso in memoria rispetto ai tempi che si hanno

nella serializzazione di soli due accessi. Inoltre, è semplice notare che quello trattato è il

caso in cui si raggiunge il maggior numero di conflitti su un singolo banco, nel contesto di

questa particolare applicazione e utilizzando gruppi di 32 work-item.

Le analisi mostrate in questo paragrafo provano che nello sviluppo di applicazioni reali, in

cui lavorano centinaia di migliaia di thread, con dati di grosse dimensioni, l'uso della

memoria locale dei dispositivi unito all'uso di pattern di accesso ottimizzati per ridurre il

numero di conflitti sui banchi, può diminuire i tempi di accesso in memoria e migliorare le

performance del sistema.

35

Page 36: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Conclusioni

Attualmente, la maggior parte dei settori della ricerca scientifica, della produzione, e delle

aziende, si serve di sistemi di calcolo che svolgono elaborazioni complesse e orientate ad

una grossa mole di dati, ai fini del raggiungimento dei propri obiettivi; tali sistemi

necessitano di un'elevata potenza computazionale che le odierne CPU non possono

garantire. Si è quindi diffuso il GPGPU, una strategia di calcolo che adopera le capacità

elaborative degli acceleratori grafici per migliorare le prestazioni di applicazioni

generiche, non strettamente legate alla grafica 3D, sfruttando il parallelismo che le GPU

consentono di implementare. É necessario allora definire una serie di modelli e linguaggi

di programmazione orientati allo sviluppo di applicazioni eseguibili non sequenzialmente

su un singolo processore e incentrate sul flusso di controllo, ma parallelamente su una

moltitudine di core, quali quelli che costituiscono le moderne GPU, con la possibilità di

concentrare l'elaborazione sul processamento dei dati.

Due moderni linguaggi di programmazione tipici di questo ambito sono CUDA e

OpenCL. CUDA è utilizzato per sviluppare applicazioni parallele eseguibili su GPU

NVIDIA, Opencl invece per realizzare programmi di calcolo eterogeneo, ossia eseguibili

in maniera cooperativa su sistemi costituiti da dispositivi di varia natura, e indipendenti

dal produttore. Entrambi offrono un modello di programmazione simile, basato sulla

divisione tra codice dell'host e codice parallelo, il kernel, sulla scansione di una gerarchia

sia per la memoria del dispositivo, sia per i thread, ossia le unità elaborative in grado di

eseguire parallelamente il kernel.

Si è visto come l'utilizzo degli strumenti offerti, ad esempio, dal linguaggio OpenCL,

consenta di sviluppare applicazioni più efficienti di quelle tradizionali eseguite

sequenzialmente, soprattutto adoperando la memoria locale ai core del dispositivo.

Tuttavia sono da adottare pattern di accesso che ne ottimizzino i tempi di accesso: è quindi

compito del programmatore adoperare una serie di strategie di parallelismo e di

modellazione dei dati per sfruttare al massimo la potenza di calcolo offerta dalle GPU.

36

Page 37: Modelli di programmazione per acceleratori grafici · graphics processing units. In particolare, l'impiego delle GPU avviene in contesti applicativi che richiedono ... Per esempio,

Bibliografia

[1] D. Kaeli, P. Mistry, “Heterogeneous Computing with OpenCL 2.0”, Morgan

Kaufamnn, third edition

[2] “Graphics Processing Unit” - https://it.wikipedia.org/wiki/Graphics_Processing_Unit

[3] “Acceleratore (informatica)” - https://it.wikipedia.org/wiki/Acceleratore_(informatica)

[4] “GPGPU” - https://it.wikipedia.org/wiki/GPGPU

[5] “ce.uniroma2” - http://www.ce.uniroma2.it/courses/sd0910/lucidi/IntroArchPar.pdf

[6] NVIDIA, “CUDA C Programming guide, Design guide”, June 2017

[7] AMD, “AMD APP SDK 3.0, Getting Started”

[8] AMD, “AMD Accelerated Parallel Processing, OpenCL Programming Guide”,

November 2013

[9] AMD, “AMD APP SDK, OpenCL User Guide”, August 2015

[10] L. Howes, A. Munshi, “The OpenCL Specification, version 2.0, Khronos OpenCL

Working Group”, Khronos Group, July 2015

[11] “techpowerup” - https://www.techpowerup.com/gpudb/2733/radeon-r7-360

[12] “Performance Programming: Bank Conflicts, Memory coalescing, Improved Matrix

Multiply, tree summation”, pagine 14-17 – http://cseweb.ucsd.edu/classes/wi12/cse260-

a/Lectures/Lec09.pdf

[13] “CUDA” - https://it.wikipedia.org/wiki/CUDA

[14] “OpenCL” - https://it.wikipedia.org/wiki/OpenCL

[15] “OpenCL Reference Pages” -

https://www.khronos.org/registry/OpenCL/sdk/1.0/docs/man/xhtml/