Quando uno shader di elaborazione è più efficiente di uno shader di pixel per il filtro delle immagini?


37

Le operazioni di filtro delle immagini come sfocature, SSAO, bloom e così via vengono generalmente eseguite utilizzando pixel shader e operazioni di "raccolta", in cui ogni invocazione di pixel shader genera una serie di recuperi di texture per accedere ai valori dei pixel vicini e calcola il valore di un singolo pixel di il risultato. Questo approccio ha un'inefficienza teorica in quanto vengono fatti molti recuperi ridondanti: le invocazioni di shader vicine recupereranno molti degli stessi texel.

Un altro modo per farlo è con gli shader di calcolo. Questi hanno il potenziale vantaggio di poter condividere una piccola quantità di memoria attraverso un gruppo di invocazioni di shader. Ad esempio, è possibile che ogni invocazione recuperi un texel e lo memorizzi nella memoria condivisa, quindi calcoli i risultati da lì. Questo potrebbe o non potrebbe essere più veloce.

La domanda è in quali circostanze (se mai) il metodo di calcolo shader è effettivamente più veloce del metodo pixel shader? Dipende dalle dimensioni del kernel, da che tipo di operazione di filtro è, ecc.? Chiaramente la risposta varierà da un modello di GPU a un altro, ma sono interessato a sapere se ci sono tendenze generali.


Penso che la risposta sia "sempre" se lo shader di calcolo viene eseguito correttamente. Questo non è banale da raggiungere. Uno shader di calcolo è anche una corrispondenza migliore di un pixel shader concettualmente per gli algoritmi di elaborazione delle immagini. Un pixel shader offre tuttavia meno margine di manovra con cui scrivere filtri con prestazioni scadenti.
Bernie,

@bernie Puoi chiarire cosa è necessario affinché lo shader di elaborazione sia "eseguito correttamente"? Forse scrivi una risposta? Sempre buono per avere più prospettive sull'argomento. :)
Nathan Reed,

2
Ora guarda cosa mi hai fatto fare! :)
bernie

Oltre a condividere il lavoro tra thread, la capacità di utilizzare il calcolo asincrono è una delle ragioni principali per utilizzare gli shader di calcolo.
JarkkoL,

Risposte:


23

Un vantaggio architettonico degli shader di elaborazione per l'elaborazione delle immagini è che saltano il passaggio ROP . È molto probabile che le scritture dei pixel shader passino attraverso tutto l'hardware di fusione normale anche se non lo usi. In generale, gli shader di calcolo passano attraverso un percorso diverso (e spesso più diretto) della memoria, quindi potresti evitare un collo di bottiglia che altrimenti avresti. Ho sentito parlare di vittorie di prestazioni abbastanza considerevoli attribuite a questo.

Uno svantaggio architettonico degli shader di calcolo è che la GPU non sa più quali elementi di lavoro si ritirano a quali pixel. Se si utilizza la pipeline di ombreggiatura dei pixel, la GPU ha la possibilità di impacchettare il lavoro in un warp / fronte d'onda che scrive su un'area del target di rendering contiguo nella memoria (che può essere affiancata in ordine Z o simile per prestazioni motivi). Se si utilizza una pipeline di calcolo, la GPU potrebbe non funzionare più in batch ottimali, portando a un maggiore utilizzo della larghezza di banda.

Tuttavia, potresti essere in grado di trasformare di nuovo quell'imballaggio warp / wavefront alterato in un vantaggio, se sai che la tua particolare operazione ha una sottostruttura che puoi sfruttare impaccando il lavoro relativo nello stesso gruppo di thread. Come hai detto, in teoria potresti dare una pausa all'hardware di campionamento campionando un valore per corsia e inserendo il risultato nella memoria condivisa di gruppo per l'accesso ad altre corsie senza campionamento. Se questa è una vincita dipende da quanto è costosa la tua memoria condivisa di gruppo: se è più economica della cache di trama di livello più basso, allora questa potrebbe essere una vincita, ma non è garantito. Le GPU si occupano già abbastanza bene dei recuperi di texture altamente locali (per necessità).

Se si hanno fasi intermedie dell'operazione in cui si desidera condividere i risultati, potrebbe essere più sensato utilizzare la memoria condivisa di gruppo (poiché non è possibile ricorrere all'hardware di campionamento delle trame senza aver effettivamente scritto i risultati intermedi in memoria). Sfortunatamente anche tu non puoi dipendere dal fatto di avere risultati da qualsiasi altro gruppo di thread, quindi il secondo stadio dovrebbe limitarsi a ciò che è disponibile nello stesso riquadro. Penso che l'esempio canonico qui sia il calcolo della luminanza media dello schermo per l'auto-esposizione. Potrei anche immaginare di combinare il upsampling delle texture con qualche altra operazione (dal momento che l'upsampling, a differenza del downsampling e delle sfocature, non dipende da alcun valore al di fuori di una data tessera).


Dubito seriamente che il ROP aggiunga un sovraccarico prestazionale se la fusione è disabilitata.
GroverManheim,

@GroverManheim Dipende dall'architettura! La fase di fusione / ROP dell'output deve anche occuparsi delle garanzie di ordinazione anche se la fusione è disabilitata. Con un triangolo a schermo intero non ci sono pericoli effettivi per l'ordinazione, ma l'hardware potrebbe non saperlo. Potrebbero esserci percorsi veloci speciali nell'hardware, ma sapendo con certezza che ti qualifichi per loro ...
John Calsbeek,

10

John ha già scritto un'ottima risposta, quindi considera questa risposta un'estensione della sua.

Attualmente sto lavorando molto con shader di calcolo per diversi algoritmi. In generale, ho scoperto che gli shader di calcolo possono essere molto più veloci del loro equivalente pixel shader o trasformare alternative basate sul feedback.

Una volta che avvolgi la testa su come funzionano gli shader di calcolo, hanno anche molto più senso in molti casi. L'utilizzo di pixel shader per filtrare un'immagine richiede l'impostazione di un framebuffer, l'invio di vertici, l'utilizzo di più livelli di shader, ecc. Perché questo dovrebbe essere richiesto per filtrare un'immagine? Essere utilizzato per il rendering di quad a schermo intero per l'elaborazione delle immagini è certamente l'unico motivo "valido" per continuare a usarli secondo me. Sono convinto che un nuovo arrivato nel campo della grafica di calcolo troverebbe gli shader di calcolo molto più naturali per l'elaborazione delle immagini rispetto al rendering in trame.

La tua domanda si riferisce in particolare al filtraggio delle immagini, quindi non tratterò troppo su altri argomenti. In alcuni dei nostri test, la semplice impostazione di un feedback di trasformazione o il passaggio di oggetti framebuffer per il rendering in una trama potrebbe comportare costi di prestazioni di circa 0,2 ms. Tieni presente che questo esclude qualsiasi rendering! In un caso, abbiamo mantenuto lo stesso algoritmo portato per calcolare gli shader e abbiamo visto un notevole aumento delle prestazioni.

Quando si utilizzano shader di calcolo, è possibile utilizzare una parte maggiore del silicio sulla GPU per eseguire il lavoro effettivo. Tutti questi passaggi aggiuntivi sono necessari quando si utilizza il percorso del pixel shader:

  • Assemblaggio di vertici (lettura degli attributi dei vertici, divisori dei vertici, conversione dei tipi, espansione in vec4, ecc.)
  • Lo shader di vertice deve essere programmato, non importa quanto sia minimo
  • Il rasterizzatore deve calcolare un elenco di pixel per ombreggiare e interpolare gli output dei vertici (probabilmente solo trame di trama per l'elaborazione delle immagini)
  • Tutti i diversi stati (test di profondità, test alfa, forbice, miscelazione) devono essere impostati e gestiti

Si potrebbe sostenere che tutti i vantaggi prestazionali precedentemente menzionati potrebbero essere annullati da un driver intelligente. Avresti ragione. Un tale driver potrebbe identificare che stai eseguendo il rendering di un quad a schermo intero senza test di profondità, ecc. E configurare un "percorso rapido" che salta tutto il lavoro inutile fatto per supportare i pixel shader. Non sarei sorpreso se alcuni driver lo facessero per accelerare i passaggi di post-elaborazione in alcuni giochi AAA per le loro GPU specifiche. Ovviamente puoi dimenticare qualsiasi trattamento del genere se non stai lavorando a un gioco AAA.

Ciò che il guidatore non può fare è tuttavia trovare migliori opportunità di parallelismo offerte dalla pipeline di shader di calcolo. Prendi il classico esempio di filtro gaussiano. Usando gli shader di calcolo, puoi fare qualcosa del genere (separando o meno il filtro):

  1. Per ciascun gruppo di lavoro, dividere il campionamento dell'immagine di origine tra le dimensioni del gruppo di lavoro e archiviare i risultati per raggruppare la memoria condivisa.
  2. Calcola l'output del filtro utilizzando i risultati del campione archiviati nella memoria condivisa.
  3. Scrivi sulla trama di output

Il passaggio 1 è la chiave qui. Nella versione pixel shader, l'immagine sorgente viene campionata più volte per pixel. Nella versione di shader di calcolo, ogni texel di origine viene letto una sola volta all'interno di un gruppo di lavoro. Le letture delle trame solitamente usano una cache basata su tile, ma questa cache è ancora molto più lenta della memoria condivisa.

Il filtro gaussiano è uno degli esempi più semplici. Altri algoritmi di filtro offrono altre opportunità per condividere i risultati intermedi all'interno dei gruppi di lavoro utilizzando la memoria condivisa.

C'è comunque un problema. Gli shader di elaborazione richiedono barriere di memoria esplicite per sincronizzare il loro output. Ci sono anche meno garanzie per proteggere da accessi di memoria errati. Per i programmatori con una buona conoscenza della programmazione parallela, gli shader di calcolo offrono molta più flessibilità. Questa flessibilità significa tuttavia che è anche più semplice trattare shader di calcolo come il normale codice C ++ e scrivere codice lento o errato.

Riferimenti


Il parallelismo di campionamento migliorato che descrivi è intrigante: ho una sim fluida che è già implementata con shader di calcolo con molte istanze di più campioni per pixel. L'uso della memoria condivisa per eseguire il campionamento singolo con una barriera di memoria come descrivi sembra fantastico, ma sono bloccato su un bit: come posso accedere ai pixel vicini quando cadono in un gruppo di lavoro diverso? ad esempio, se ho un dominio di simulazione 64x64, distribuito su un dispaccio (2,2,1) di numthread (16,16,1), come otterrebbero i pixel vicini con id.xy == [15,15] ?
Tossrock

In tal caso, vedo 2 scelte principali. 1) aumentare la dimensione del gruppo oltre 64 e scrivere solo i risultati per i 64x64 pixel. 2) primo campione 64 + nX64 + n diviso in qualche modo nel gruppo di lavoro 64x64 e quindi utilizzare quella griglia di "input" più grande per i calcoli. La soluzione migliore dipende ovviamente dalle tue condizioni specifiche e ti suggerisco di scrivere un'altra domanda per ulteriori informazioni poiché i commenti non sono adatti per questo.
Bernard il

3

Mi sono imbattuto in questo blog: Calcola le ottimizzazioni dello shader per AMD

Dato quali trucchi si possono fare nello shader di calcolo (che sono specifici solo per lo shader di calcolo), ero curioso di sapere se la riduzione parallela sullo shader di calcolo era più veloce che sullo shader di pixel. Ho mandato un'e-mail all'autore, Wolf Engel, per chiedere se avesse provato il pixel shader. Rispose che sì e quando scrisse il post sul blog la versione di shader di calcolo era sostanzialmente più veloce della versione di pixel shader. Ha anche aggiunto che oggi le differenze sono ancora maggiori. Quindi a quanto pare ci sono casi in cui l'uso dello shader di calcolo può essere di grande vantaggio.

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.