Perché l'efficienza del lavoro è desiderata nella programmazione GPU?


13

Ho letto il seguente articolo su come eseguire una scansione parallela in CUDA:

https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch39.html

Nell'articolo si pone l'accento sul rendere "efficiente la scansione". In altre parole, un algoritmo GPU non dovrebbe eseguire più aggiunte di un algoritmo CPU, O (n). Gli autori presentano due algoritmi, uno "ingenuo" che fa aggiunte di O (nlogn) e uno che considerano "lavoro efficiente". Tuttavia, l'algoritmo efficiente dal punto di vista del lavoro esegue il doppio delle ripetizioni cicliche.

Da quanto ho capito, le GPU sono semplicemente processori SIMD giganti e dovrebbero funzionare in modalità di blocco. Fare il doppio dei cicli nell'algoritmo "efficiente dal punto di vista del lavoro" sembra implicare che molti thread saranno inattivi e diminuiranno le prestazioni nel lungo periodo. Cosa mi sto perdendo?

Risposte:


21

Prima di tutto, ri: "Le GPU sono semplicemente processori SIMD giganti e dovrebbero funzionare in modalità di blocco", è un po 'più complicato di così. L' intera GPU non viene eseguita in lockstep. I thread di shader sono organizzati in gruppi di 32 chiamati "warps" (su NVIDIA; su AMD sono gruppi di 64 chiamati "wavefronts", ma stesso concetto). All'interno di un warp, tutti i thread vengono eseguiti in lockstep come un array SIMD. Tuttavia, diversi orditi non sono in stretto contatto l'uno con l'altro. Inoltre, alcuni orditi potrebbero essere in esecuzione attivamente mentre altri potrebbero essere sospesi, proprio come i thread della CPU. Gli orditi possono essere sospesi sia perché stanno aspettando qualcosa (come le transazioni di memoria da restituire o le barriere da eliminare), o perché non c'è

Ora, torna alla tua domanda. Vedo due modi in cui l'algoritmo "efficiente dal punto di vista del lavoro" di quel documento sembra che sarebbe più efficiente dell'algoritmo "ingenuo".

  1. Per iniziare, la versione efficiente richiede metà dei thread. Nell'algoritmo ingenuo, hanno un thread per elemento array; ma nella versione efficiente dal punto di vista del lavoro, ogni thread opera su due elementi adiacenti dell'array e quindi hanno bisogno solo della metà dei thread degli elementi dell'array. Meno fili significa meno orditi, quindi una parte maggiore degli orditi può essere attivamente in esecuzione.

  2. Sebbene la versione efficiente dal punto di vista del lavoro richieda più passaggi, ciò è compensato dal fatto che il numero di thread attivi diminuisce più rapidamente e il numero totale di thread attivi su tutte le iterazioni è considerevolmente più piccolo. Se un ordito non ha thread attivi durante un'iterazione, quell'ordito salterà semplicemente alla seguente barriera e verrà sospeso, consentendo l'esecuzione di altri orditi. Quindi, avere meno orditi attivi può spesso ripagare in tempo di esecuzione. (In questo è implicito che il codice GPU deve essere progettato in modo tale che i thread attivi siano raggruppati nel minor numero possibile di orditi - non si desidera che siano sparsi scarsamente, poiché anche un solo thread attivo forzerà l'intero ordito per rimanere attivo.)

    Considera il numero di thread attivi nell'algoritmo ingenuo. Osservando la figura 2 nell'articolo, si può vedere che tutti i fili sono attivi tranne per i primi 2 k il k esima iterazione. Quindi con N thread, il numero di thread attivi va come N - 2 k . Ad esempio, con N = 1024, il numero di thread attivi per iterazione è:

    1023, 1022, 1020, 1016, 1008, 992, 960, 896, 768, 512
    

    Se lo converto in numero di orditi attivi (dividendo per 32 e arrotondando per eccesso), ottengo:

    32, 32, 32, 32, 32, 31, 30, 28, 24, 16
    

    per una somma di 289. D'altra parte, l'algoritmo efficiente dal punto di vista lavorativo inizia con la metà del numero di thread, quindi dimezza il numero di quelli attivi su ciascuna iterazione fino a quando non scende a 1, quindi inizia a raddoppiare fino a quando non torna a metà della dimensione dell'array di nuovo:

     512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512
    

    Convertire questo in orditi attivi:

    16, 8, 4, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 4, 8, 16
    

    La somma è 71, che è solo un quarto in più. Quindi puoi vedere che nel corso dell'intera operazione, il numero di orditi attivi è molto più piccolo con l'algoritmo efficiente dal punto di vista del lavoro. (In effetti, per una lunga corsa nel mezzo ci sono solo una manciata di orditi attivi, il che significa che la maggior parte del chip non è occupata. Se ci sono attività di calcolo aggiuntive in esecuzione, ad esempio da altri flussi CUDA, potrebbero espandersi per riempire quello spazio libero.)

Detto questo, è un peccato che l'articolo GPU Gems non spieghi chiaramente nulla di tutto ciò, concentrandosi invece sull'analisi del "numero di aggiunte" di grandi dimensioni che, sebbene non del tutto irrilevante, manca molti dettagli sul perché questo algoritmo è Più veloce.

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.