La programmazione della GPU con MATLAB

Il GPGPU ci offre possibilità che, fino a poco fa, erano utopia, ed ha contribuito ai recenti avanzamenti in campo accademico ed industriale. Sfruttare le GPU, però, richiede di assimilare concetti di programmazione parallela che, spesso, divergono da quelli classici, e risultano quasi controintuitivi. Per fortuna, numerosi ambienti, tra cui anche MATLAB, offrono una nutrita serie di funzioni che ci permettono di usare in modo semplice le nostre schede grafiche. Vediamo, avvalendoci di esempi pratici, come tutto questo sia possibile.

Ottimizzare, ottimizzare, ottimizzare!

Negli articoli precedenti, abbiamo dato uno sguardo alle tecniche che è possibile utilizzare per migliorare le performance dei nostri script in MATLAB. In particolare, abbiamo visto come sfruttare le caratteristiche dell'ambiente, preallocando o vettorizzando il codice; abbiamo utilizzato il coder, per convertire le funzioni più 'onerose' in C; infine, abbiamo visto come sfruttare il Parallel Computing Toolbox per distribuire le elaborazioni tra più core. Esiste, però, una ulteriore possibilità: qualora la nostra macchina sia equipaggiata con una GPU nVidia, infatti, potremo sfruttare CUDA, rendendo possibile il trattamento di problemi complessi come la risoluzione di un'equazione delle forme d'onda, o l'addestramento di una deep neural network (usate, ad esempio, nel riconoscimento dei caratteri, come mostrato in un nostro precedente articolo). Vediamo quindi assieme come è possibile sfruttare MATLAB per programmare la nostra GPU.

Baby steps...

Il supporto a CUDA è offerto nel Parallel Computing Toolbox, che risulta quindi essere un requisito fondamentale. In particolare, per verificare la compatibilità del nostro setting, possiamo usare la funzione gpuDevice, che crea un oggetto di tipo CUDADevice, all'interno del quale sono raccolte tutte le informazioni relative ai dispositivi CUDA-enabled presenti nella nostra macchina:

gpuDevice

Nel caso sia tutto a posto, otterremo un output di questo tipo:

CUDADevice with properties:
                   Name: 'GeForce GT 650M'
                  Index: 1
      ComputeCapability: '3.0'
         SupportsDouble: 1
          DriverVersion: 8
         ToolkitVersion: 7.5000
     MaxThreadsPerBlock: 1024
       MaxShmemPerBlock: 49152
     MaxThreadBlockSize: [1024 1024 64]
            MaxGridSize: [2.1475e+09 65535 65535]
              SIMDWidth: 32
            TotalMemory: 2.1475e+09
        AvailableMemory: 1.7402e+09
    MultiprocessorCount: 2
           ClockRateKHz: 835000
            ComputeMode: 'Default'
   GPUOverlapsTransfers: 1
 KernelExecutionTimeout: 1
       CanMapHostMemory: 1
        DeviceSupported: 1
         DeviceSelected: 1

Non approfondiremo le singole voci: per comprenderle al meglio,  vi rimando al precedente articolo su CUDA e sul GPGPU.

A questo punto, proviamo a sfruttare la nostra GPU per fare qualche semplice operazione di image processing, che, come già visto, sono facilmente parallelizzabili; faremo, inoltre, un confronto con l'elaborazione classica, su CPU. La prima cosa da fare è leggere il file che contiene l'immagine che vogliamo elaborare (useremo, in questo esempio, la sempre piacente Lena):

i = imread('lena.bmp');

Sappiamo che la funzione imread legge l'immagine, e la memorizza in un array (per rinfrescare questi concetti, può essere utile consultare un nostro precedente articolo su questo argomento), che viene salvato nella memoria RAM della nostra macchina. Questi dati non possono essere utilizzati direttamente dalla GPU: per farlo, occorre copiarli nella memoria della scheda grafica mediante la funzione gpuArray:

igpu = gpuArray(i);

Un gpuArray si comporta esattamente come un array standard: è infatti possibile passarlo come argomento alle funzioni built-in, a patto, ovviamente, che ne esista l'overload opportuno (in tal senso, può essere utile consultare la documentazione del Parallel Computing Toolbox). Qualora sia necessario trasferire i dati dalla memoria della GPU alla RAM, è necessario utilizzare la funzione gather:

igat = gather(igpu);

Scriviamo ora uno script, che effettua un benchmark comparativo delle differenze nelle performance tra GPU e CPU.

Un po' di benchmark

Nel nostro script, vedremo come differisce, dal punto di vista implementativo, l'elaborazione su GPU e quella su CPU, al variare delle dimensioni delle immagini da elaborare; inoltre, useremo le funzioni timeit e gputimeit, che permettono di misurare il tempo necessario all'esecuzione di una funzione, rispettivamente, su CPU e GPU.

Per prima cosa, preallochiamo due vettori:

sizes = [100 200 400 800 1600 3200];
[tCpu, tGpu, tttCpu, tttGpu] = deal(zero('like',sizes));

Il primo vettore definisce l'altezza e la larghezza alla quale l'immagine, ad ogni iterazione, sarà ridimensionata, mentre il secondo definisce quattro vettori di supporto all'interno dei quali saranno memorizzati i tempi di esecuzione relativi a ciascuna delle prove che effettueremo. L'idea è quella di articolare il nostro esperimento in più iterazioni, ognuna delle quali inizia con il ridimensionamento dell'immagine utilizzata mediante la funzione imresize:

isc = imresize(i, [size size]);

Usiamo quindi la funzione gpuArray per trasferire l'array isc su GPU:

iscGpu = gpuArray(isc);

Definiamo quindi la funzione someProcessing (perdonerete il nome), che effettua una serie volutamente complessa (ed inutile) di operazioni sulle immagini:

function im_out = canvasEffect( im )
    im_n = imnoise(im, 'gaussian');
    h = fspecial('gaussian');
    im_f = imfilter(im_n,h);
    im_a = cat( 3, imadjust(im_f(:,:,1)), imadjust(im_f(:,:,2)), imadjust(im_f(:,:,3)) );
    se = strel('disk',5);
    im_er = imerode(im_a, se);
    im_out = imdilate(im_er,se);
end

Non approfondiremo le singole istruzioni eseguite da someProcessing (per quello, il caro, vecchio, help di MATLAB è sempre il punto di riferimento); ad ogni modo, l'effetto ottenuto sull'immagine è simile, effettuando alcune piccole modifiche, a quello che è possibile ottenere mediante la funzione Canvas di Photoshop. E' importante sottolineare che ciascuna delle istruzioni di someProcessing può accettare, in ingresso, un gpuArray: questo ci permetterà di valutare efficacemente il guadagno in termini di performance.
[...]

ATTENZIONE: quello che hai appena letto è solo un estratto, l'Articolo Tecnico completo è composto da ben 2059 parole ed è riservato agli abbonati PRO. Con l'Abbonamento avrai anche accesso a tutti gli altri Articoli Tecnici MAKER e PRO inoltre potrai fare il download (PDF) dell'EOS-Book e di FIRMWARE del mese. ABBONATI ORA, è semplice e sicuro.

Abbonati alle riviste di elettronica

Una risposta

  1. Andrea Garrapa Andrea Garrapa 6 luglio 2018

Scrivi un commento

EOS-Academy

Ricevi GRATIS le pillole di Elettronica

Ricevi via EMAIL 10 articoli tecnici di approfondimento sulle ultime tecnologie