Quali tipi di problemi si prestano bene al GPU computing?


84

Quindi ho una testa decente per quali problemi con cui lavoro sono i migliori in serie e che possono essere gestiti in parallelo. Ma in questo momento, non ho molta idea di cosa sia meglio gestito dal calcolo basato sulla CPU e di cosa dovrebbe essere scaricato su una GPU.

So che è una domanda di base, ma gran parte della mia ricerca viene catturata da persone che chiedono chiaramente l'uno o l'altro senza giustificare davvero il perché , o in qualche modo regole vaghe. Alla ricerca di una risposta più utile qui.

Risposte:


63

L'hardware della GPU ha due punti di forza particolari: calcolo non elaborato (FLOP) e larghezza di banda della memoria. I problemi computazionali più difficili rientrano in una di queste due categorie. Ad esempio, l'algebra lineare densa (A * B = C o Risolvi [Ax = y] o Diagonalize [A], ecc.) Cade da qualche parte nello spettro di larghezza di banda di calcolo / memoria a seconda delle dimensioni del sistema. Anche le trasformazioni Fast Fourier (FFT) si adattano a questo stampo con elevate esigenze di larghezza di banda aggregata. Come altre trasformazioni, algoritmi basati su griglia / mesh, Monte Carlo, ecc. Se si esaminano gli esempi di codice SDK NVIDIA , è possibile farsi un'idea dei tipi di problemi che vengono affrontati più comunemente.

Penso che la risposta più istruttiva sia alla domanda "A che tipo di problemi le GPU fanno davvero male?" La maggior parte dei problemi che non rientrano in questa categoria può essere eseguita sulla GPU, anche se alcuni richiedono più sforzo di altri.

I problemi che non mappano bene sono generalmente troppo piccoli o imprevedibili. Problemi molto piccoli mancano del parallelismo necessario per utilizzare tutti i thread sulla GPU e / o potrebbero rientrare in una cache di basso livello sulla CPU, migliorando sostanzialmente le prestazioni della CPU. I problemi imprevedibili hanno troppi rami significativi, che possono impedire lo streaming efficiente dei dati dalla memoria della GPU ai core o ridurre il parallelismo rompendo il paradigma SIMD (vedere " deformazioni divergenti "). Esempi di questo tipo di problemi includono:

  • La maggior parte degli algoritmi grafici (troppo imprevedibile, specialmente nello spazio di memoria)
  • Algebra lineare sparsa (ma questo è negativo anche per la CPU)
  • Piccoli problemi di elaborazione del segnale (ad esempio FFT inferiori a 1000 punti)
  • Ricerca
  • Ordinare

3
Tuttavia, le soluzioni GPU per questi problemi "imprevedibili" sono possibili e, sebbene al giorno d'oggi in genere non fattibili, potrebbero guadagnare significato in futuro.
circa il

6
Vorrei aggiungere specificamente rami all'elenco delle prestazioni della GPU. Volete che tutte le vostre (centinaia) eseguano la stessa istruzione (come in SIMD) per eseguire calcoli veramente paralleli. Ad esempio, sulle schede AMD se uno qualsiasi dei flussi di istruzioni incontra un ramo e deve divergere - tutto il fronte d'onda (gruppo parallelo) diverge. Se un'altra unità dal fronte d'onda non deve divergere, deve eseguire un secondo passaggio. Questo è ciò che Maxhutch intende per prevedibilità, immagino.
Violet Giraffe,

2
@VioletGiraffe, non è necessariamente vero. In CUDA (cioè su GPU Nvidia), la divergenza di ramo influenza solo l'attuale curvatura, che è al massimo di 32 thread. Diversi orditi, sebbene eseguano lo stesso codice, non sono sincroni se non esplicitamente sincronizzati (ad es. Con __synchtreads()).
Pedro

1
@Pedro: vero, ma le ramificazioni in generale danneggiano le prestazioni. Per i codici ad alte prestazioni (quale codice GPU non è?), È quasi essenziale tenerne conto.
jvriesem,

21

I problemi con un'alta intensità aritmetica e schemi di accesso alla memoria regolari sono in genere facili da implementare su GPU e funzionano bene su di essi.

La difficoltà di base nell'avere un codice GPU ad alte prestazioni è che hai un sacco di core e vuoi che siano utilizzati tutti nella loro massima potenza il più possibile. Problemi che hanno schemi di accesso alla memoria irregolari o che non hanno un'intensità aritmetica elevata rendono questo difficile: o passi molto tempo a comunicare risultati o passi molto tempo a recuperare roba dalla memoria (che è lenta!) E non abbastanza tempo a scricchiolare i numeri. Naturalmente il potenziale di concorrenza nel codice è fondamentale per la sua capacità di essere implementato bene anche su GPU.


Puoi specificare cosa intendi con schemi di accesso alla memoria regolari?
Fomite

1
La risposta di Maxhutch è migliore della mia. Ciò che intendo con un modello di accesso regolare è che si accede alla memoria in modo temporale e spazialmente locale. Cioè: non fai ripetutamente salti enormi intorno alla memoria. È anche una specie di pacchetto che ho notato. Si presume anche che i tuoi modelli di accesso ai dati possano essere predeterminati dal compilatore in qualche modo o da te il programmatore in modo da ridurre al minimo le ramificazioni (istruzioni condizionali nel codice).
Reid.Atcheson,

15

Questo non è inteso come una risposta a sé stante, ma piuttosto come un'aggiunta alle altre risposte di Maxhutch e Reid.Atcheson .

Per ottenere il meglio dalle GPU il tuo problema non deve solo essere altamente (o massicciamente) parallelo, ma anche l'algoritmo core che verrà eseguito sulla GPU, dovrebbe essere il più piccolo possibile. In termini OpenCL questo è principalmente indicato come kernel .

Per essere più precisi, il kernel dovrebbe inserirsi nel registro di ciascuna unità multiprocessore (o unità di calcolo ) della GPU. La dimensione esatta del registro dipende dalla GPU.

Dato che il kernel è abbastanza piccolo, i dati grezzi del problema devono adattarsi alla memoria locale della GPU (leggi: memoria locale (OpenCL) o memoria condivisa (CUDA) di un'unità di calcolo). Altrimenti anche l'ampiezza di banda della memoria della GPU non è abbastanza veloce da tenere occupati gli elementi di elaborazione in ogni momento.
Di solito questa memoria ha una dimensione compresa tra 16 e 32 KiByte .


La memoria locale / condivisa di ciascuna unità di elaborazione non è condivisa tra tutte le dozzine (?) Di thread in esecuzione all'interno di un singolo cluster di core? In questo caso, non è necessario mantenere il set di dati di lavoro significativamente più piccolo per ottenere prestazioni complete dalla GPU?
Dan Neely,

La memoria locale / condivisa di un'unità di elaborazione è accessibile solo dall'unità di calcolo stessa e quindi condivisa solo dagli elementi di elaborazione di questa unità di elaborazione. La memoria globale della scheda grafica (in genere 1 GB) è accessibile da tutte le unità di elaborazione. La larghezza di banda tra gli elementi di elaborazione e la memoria locale / condivisa è molto veloce (> 1 TB / s) ma la larghezza di banda per la memoria globale è molto più lenta (~ 100 GB / s) e deve essere condivisa tra tutte le unità di calcolo.
Torbjörn,

Non stavo chiedendo della memoria GPU principale. Pensavo che la memoria on die fosse allocata solo al cluster di livello core non per singolo core. ex per un nVidia GF100 / 110 gpu; per ciascuno dei 16 cluster SM non i 512 core cuda. Con ogni SM progettato per eseguire fino a 32 thread in parallelo, l'ottimizzazione delle prestazioni della GPU richiederebbe di mantenere il set di lavoro nell'intervallo 1kb / thread.
Dan Neely,

@Torbjoern Quello che vuoi è tenere occupate tutte le pipeline di esecuzione della GPU, le GPU ottengono questi due modi: (1) il modo più comune è aumentare l'occupazione, o detto diversamente, aumentando il numero di thread simultanei (i kernel piccoli usano meno di le risorse condivise in modo da poter avere più thread attivi); forse meglio, è quello di (2) aumentare il parallelismo a livello di istruzioni all'interno del kernel, in modo da poter avere kernel più grandi con un'occupazione relativamente bassa (piccolo numero di thread attivi). Vedi bit.ly/Q3KdI0
fcruz,

11

Probabilmente un'aggiunta più tecnica alle risposte precedenti: le GPU CUDA (ie Nvidia) possono essere descritte come un insieme di processori che funzionano autonomamente su 32 thread ciascuno. I thread in ciascun processore funzionano in fase di blocco (pensa SIMD con vettori di lunghezza 32).

Sebbene il modo più allettante di lavorare con le GPU sia far finta che tutto vada per il verso giusto, questo non è sempre il modo più efficiente di fare le cose.

Se il tuo codice non si parallelizza in modo corretto / automatico con centinaia / migliaia di thread, potresti essere in grado di suddividerlo in singole attività asincrone che si parallelizzano bene ed eseguire quelle con solo 32 thread in esecuzione in modalità di blocco. CUDA fornisce una serie di istruzioni atomiche che consentono di implementare mutex che a loro volta consente ai processori di sincronizzarsi tra loro ed elaborare un elenco di attività in un paradigma di pool di thread . Il tuo codice funzionerebbe quindi allo stesso modo di un sistema multi-core, tieni presente che ogni core ha quindi 32 thread propri.

Ecco un piccolo esempio, usando CUDA, di come funziona

/* Global index of the next available task, assume this has been set to
   zero before spawning the kernel. */
__device__ int next_task;

/* We will use this value as our mutex variable. Assume it has been set to
   zero before spawning the kernel. */
__device__ int tasks_mutex;

/* Mutex routines using atomic compare-and-set. */
__device__ inline void cuda_mutex_lock ( int *m ) {
    while ( atomicCAS( m , 0 , 1 ) != 0 );
    }
__device__ inline void cuda_mutex_unlock ( int *m ) {
    atomicExch( m , 0 );
    }

__device__ void task_do ( struct task *t ) {

    /* Do whatever needs to be done for the task t using the 32 threads of
       a single warp. */
    }

__global__ void main ( struct task *tasks , int nr_tasks ) {

    __shared__ task_id;

    /* Main task loop... */
    while ( next_task < nr_tasks ) {

        /* The first thread in this block is responsible for picking-up a task. */
        if ( threadIdx.x == 0 ) {

            /* Get a hold of the task mutex. */
            cuda_mutex_lock( &tasks_mutex );

            /* Store the next task in the shared task_id variable so that all
               threads in this warp can see it. */
            task_id = next_task;

            /* Increase the task counter. */
            next_tast += 1;

            /* Make sure those last two writes to local and global memory can
               be seen by everybody. */
            __threadfence();

            /* Unlock the task mutex. */
            cuda_mutex_unlock( &tasks_mutex );

            }

        /* As of here, all threads in this warp are back in sync, so if we
           got a valid task, perform it. */
        if ( task_id < nr_tasks )
            task_do( &tasks[ task_id ] );

        } /* main loop. */

    }

Devi quindi chiamare il kernel con main<<<N,32>>>(tasks,nr_tasks)per assicurarti che ogni blocco contenga solo 32 thread e quindi si adatti a un singolo warp. In questo esempio ho anche assunto, per semplicità, che le attività non hanno dipendenze (ad esempio un'attività dipende dai risultati di un'altra) o conflitti (ad esempio lavoro sulla stessa memoria globale). In questo caso, la selezione dell'attività diventa un po 'più complicata, ma la struttura è sostanzialmente la stessa.

Questo è, ovviamente, più complicato del semplice fare tutto su un grande lotto di celle, ma amplia notevolmente il tipo di problemi per i quali è possibile utilizzare le GPU.


2
Ciò è tecnicamente vero, ma è necessario un elevato parallelismo per ottenere una larghezza di banda di memoria elevata e esiste un limite al numero di chiamate asincrone del kernel (attualmente 16). Ci sono anche tonnellate di comportamenti privi di documenti relativi alla pianificazione nella versione corrente. Vorrei sconsigliarmi di fare affidamento su kernel asincroni per migliorare le prestazioni per il momento ...
Max Hutchinson,

2
Quello che sto descrivendo può essere fatto tutto in una singola chiamata del kernel. Puoi creare N blocchi di 32 thread ciascuno, in modo tale che ciascun blocco si adatti a un singolo ordito. Ogni blocco acquisisce quindi un'attività da un elenco di attività globale (accesso controllato mediante atomics / mutex) e lo calcola utilizzando 32 thread con blocco. Tutto ciò accade in una singola chiamata del kernel. Se desideri un esempio di codice, fammi sapere e ne posterò uno.
Pedro,

4

Un punto non sottolineato finora è che l'attuale generazione di GPU non fa altrettanto con i calcoli a virgola mobile a doppia precisione come con i calcoli a precisione singola. Se i tuoi calcoli devono essere eseguiti in doppia precisione, allora puoi aspettarti che il tempo di esecuzione aumenti di un fattore di circa 10 rispetto a una precisione singola.


Voglio essere in disaccordo. La maggior parte (o tutte) le GPU più recenti hanno un supporto nativo a doppia precisione. Quasi tutte queste GPU riportano calcoli a doppia precisione eseguiti a circa la metà della velocità della singola precisione, probabilmente a causa del semplice raddoppio degli accessi / larghezza di banda richiesti.
Godric Seer,

1
Mentre è vero che le più recenti e migliori schede Nvidia Tesla offrono prestazioni di picco a doppia precisione che rappresentano la metà delle prestazioni di picco a precisione singola, il rapporto è da 8 a 1 per le più comuni carte di consumo per architettura Fermi.
Brian Borchers,

@GodricSeer Il rapporto 2: 1 di SP e DP in virgola mobile ha ben poco a che fare con la larghezza di banda e quasi tutto a che fare con quante unità hardware esistono per eseguire queste operazioni. È comune riutilizzare il file di registro per SP e DP, quindi l'unità a virgola mobile può eseguire il doppio delle operazioni SP come operazioni DP. Esistono numerose eccezioni a questo progetto, ad esempio IBM Blue Gene / Q (non ha una logica SP e quindi SP funziona a ~ 1.05x DP). Alcune GPU hanno rapporti diversi da 2, ad esempio, 3 e 5.
Jeff

Sono passati quattro anni da quando ho scritto questa risposta e la situazione attuale con le GPU NVIDIA è che per le linee GeForce e Quadro, il rapporto DP / SP è ora 1/32. Le GPU Tesla di NVIDIA hanno prestazioni di doppia precisione molto più forti ma costano anche molto di più. D'altra parte, AMD non ha paralizzato le prestazioni di doppia precisione sulle sue GPU Radeon allo stesso modo.
Brian Borchers,

4

Da un punto di vista metaforico, la gpu può essere vista come una persona sdraiata su un letto di chiodi. La persona che si trova in cima sono i dati e nella base di ogni unghia c'è un processore, quindi l'unghia è in realtà una freccia che punta dal processore alla memoria. Tutte le unghie hanno uno schema regolare, come una griglia. Se il corpo è ben distribuito, si sente bene (le prestazioni sono buone), se il corpo tocca solo alcune macchie del letto ungueale, il dolore è cattivo (prestazioni scadenti).

Questo può essere preso come una risposta complementare alle eccellenti risposte di cui sopra.


4

Vecchia domanda, ma penso che questa risposta del 2014 - relativa ai metodi statistici, ma generalizzabile per chiunque sappia cos'è un ciclo - è particolarmente illustrativa e informativa.


2

Le GPU hanno I / O a lunga latenza, quindi è necessario utilizzare molti thread per saturare la memoria. Per tenere occupato un ordito sono necessari molti thread. Se il percorso del codice è di 10 clock e 320 clock di latenza I / O, 32 thread dovrebbero avvicinarsi alla saturazione dell'ordito. Se il percorso del codice è di 5 orologi, raddoppia i thread.

Con mille core, cerca migliaia di thread per utilizzare appieno la GPU.

L'accesso alla memoria avviene tramite linea di cache, in genere 32 byte. Il caricamento di un byte ha un costo comparabile a 32 byte. Quindi, unisci l'archiviazione per aumentare la località di utilizzo.

Ci sono molti registri e RAM locale per ogni ordito, consentendo la condivisione dei vicini.

Le simulazioni di prossimità di insiemi di grandi dimensioni dovrebbero essere ottimizzate.

L'I / O casuale e il threading singolo sono una gioia letale ...


Questa è una domanda davvero affascinante; Sto discutendo con me stesso se sia possibile (o valga la pena) "parallelizzare" un compito ragionevolmente semplice (rilevamento dei bordi nelle immagini aeree) quando ogni compito richiede ~ 0,06 secondi ma ci sono circa 1,8 milioni di compiti da svolgere ( all'anno, per 6 anni di dati: i compiti sono sicuramente separabili) ... quindi circa 7,5 giorni di tempo di calcolo su un core. Se ogni calcolo era più veloce su una GPU e il lavoro poteva essere parallelizzato 1-per-nGPUcores [n piccolo], è davvero probabile che il tempo del lavoro potrebbe scendere a ~ 1 ora? Sembra improbabile.
GT.

0

Immagina un problema che può essere risolto da molta forza bruta, come il commesso viaggiatore. Quindi immagina di avere rack di server con 8 schede video spanky ciascuna e ogni scheda ha 3000 core CUDA.

Risolvi semplicemente TUTTE le possibili rotte del venditore e ordina per tempo / distanza / metrica. Sicuramente stai buttando via quasi il 100% del tuo lavoro, ma a volte la forza bruta è una soluzione praticabile.


Ho avuto accesso a una piccola fattoria di 4 server di questo tipo per una settimana e in cinque giorni ho fatto più blocchi distribuiti.net rispetto ai precedenti 10 anni.
Criggie,

-1

Dallo studio di molte idee di ingegneria, direi che una GPU è una forma di concentrazione di compiti, di gestione della memoria, di calcolo ripetibile.

Molte formule potrebbero essere semplici da scrivere ma dolorose da calcolare, ad esempio nella matematica delle matrici non si ottiene una sola risposta ma molti valori.

Ciò è importante nel calcolo della velocità con cui un computer calcola i valori e esegue le formule poiché alcune formule non possono essere eseguite senza tutti i valori calcolati (quindi rallentare). Un computer non sa molto bene quale ordine eseguire formule o calcolare valori da utilizzare in questi programmi. Principalmente forza bruta a velocità elevate e suddivide le formule in mandrini per il calcolo, ma molti programmi oggigiorno richiedono questi mandrini calcolati in questo momento e attendono domande (e domande di domande e altre di domande).

Ad esempio in un gioco di simulazione che dovrebbe essere calcolato per primo nelle collisioni il danno della collisione, la posizione degli oggetti, la nuova velocità? Quanto tempo dovrebbe impiegare? Come può una CPU gestire questo carico? Inoltre, la maggior parte dei programmi è molto astratta e richiede più tempo per gestire i dati e non è sempre progettata per il multi-threading o non è un buon modo per eseguire in modo efficace programmi astratti.

Man mano che la CPU diventava sempre meglio, le persone diventavano sciatte nella programmazione e dobbiamo programmare anche per molti tipi diversi di computer. Una gpu è progettata per potenziare la forza attraverso molti semplici calcoli contemporaneamente (per non parlare della memoria (secondaria / ram) e il raffreddamento del riscaldamento sono i principali colli di bottiglia nell'informatica). Una cpu gestisce molte e molte domande contemporaneamente o viene trascinata in molte direzioni, sta cercando di capire cosa fare e non riuscire a farlo. (hey è quasi umano)

Una gpu è un lavoratore tosto il lavoro noioso. Una cpu gestisce il caos completo e non può gestire ogni dettaglio.

Quindi cosa impariamo? Una GPU esegue in modo dettagliato il lavoro noioso tutto in una volta e una CPU è una macchina multi-task che non può concentrarsi molto bene con troppe attività da svolgere. (È come se avesse il disturbo dell'attenzione e l'autismo allo stesso tempo).

Ingegneria c'è le idee, il design, la realtà e un sacco di lavoro grugnito.

Mentre parto, ricordati di iniziare in modo semplice, iniziare rapidamente, fallire rapidamente, fallire rapidamente e non smettere mai di provare.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.