Kiedy moduł cieniujący jest bardziej wydajny niż moduł cieniujący do filtrowania obrazów?

37

Operacje filtrowania obrazu, takie jak rozmycie, SSAO, kwitnienie itd., Są zwykle wykonywane przy użyciu programów cieniujących piksele i operacji „gromadzenia”, przy czym każde wywołanie modułu cieniującego pikseli powoduje pobranie szeregu tekstur w celu uzyskania dostępu do wartości sąsiednich pikseli i oblicza wartość pojedynczego piksela wynik. Podejście to ma teoretyczną nieskuteczność, ponieważ wykonuje się wiele zbędnych pobrań: pobliskie wywołania modułu cieniującego ponownie pobiorą wiele takich samych tekstur.

Innym sposobem na to jest zastosowanie shaderów obliczeniowych. Mają one tę potencjalną zaletę, że mogą dzielić niewielką ilość pamięci w grupie wywołań modułu cieniującego. Na przykład, możesz kazać każdemu wywołaniu pobrać jeden tekst i zapisać go we wspólnej pamięci, a następnie obliczyć wyniki z tego miejsca. To może, ale nie musi być szybsze.

Pytanie brzmi, w jakich okolicznościach (jeśli w ogóle) metoda obliczania-cieniowania jest rzeczywiście szybsza niż metoda cieniowania pikseli? Czy zależy to od wielkości jądra, rodzaju operacji filtrowania itp.? Oczywiście odpowiedź będzie się różnić w zależności od modelu GPU, ale interesuje mnie to, czy istnieją jakieś ogólne trendy.

Nathan Reed
źródło
Myślę, że odpowiedź brzmi „zawsze”, jeśli moduł obliczeniowy jest wykonywany poprawnie. Nie jest to łatwe do osiągnięcia. Moduł obliczeniowy jest również lepiej dopasowany niż moduł cieniujący pod względem koncepcyjnym dla algorytmów przetwarzania obrazu. Moduł cieniujący pikseli zapewnia jednak mniejszą swobodę pisania słabo działających filtrów.
bernie,
@bernie Czy możesz wyjaśnić, co jest potrzebne do „poprawnego wykonania” modułu cieniującego? Może napisz odpowiedź? Zawsze dobrze, aby uzyskać więcej perspektyw na ten temat. :)
Nathan Reed,
2
Teraz spójrz na to, co kazałeś mi zrobić! :)
bernie,
Oprócz dzielenia pracy między wątkami, możliwość korzystania z obliczeń asynchronicznych jest jednym z głównych powodów korzystania z shaderów obliczeniowych.
JarkkoL,

Odpowiedzi:

23

Architektoniczną zaletą shaderów obliczeniowych do przetwarzania obrazu jest to, że pomijają one etap ROP . Jest bardzo prawdopodobne, że zapisy z shaderów pikseli przechodzą przez cały regularny sprzęt do miksowania, nawet jeśli go nie używasz. Ogólnie rzecz biorąc, shadery obliczeniowe przechodzą inną (i często bardziej bezpośrednią) ścieżką do pamięci, dzięki czemu można uniknąć wąskiego gardła, które w przeciwnym razie byś miał. Słyszałem o dość znacznych wygranych w wydajności.

Architektoniczną wadą shaderów obliczeniowych jest to, że GPU nie wie już, które elementy pracy przechodzą na emeryturę i do których pikseli. Jeśli korzystasz z potoku cieniowania pikseli, GPU ma możliwość spakowania pracy w warp / wavefront, które zapisują w obszarze celu renderowania, który jest przyległy w pamięci (który może być sąsiadująco w kolejności Z lub coś podobnego dla wydajności powody). Jeśli korzystasz z potoku obliczeniowego, procesor graficzny może nie kopać pracy w optymalnych partiach, co prowadzi do większego wykorzystania przepustowości.

Być może będziesz w stanie zmienić to zmienione pakowanie w warp / wavefront na korzyść, jeśli wiesz, że Twoja konkretna operacja ma podbudowę, którą możesz wykorzystać, pakując powiązane prace do tej samej grupy wątków. Tak jak powiedziałeś, teoretycznie możesz przerwać sprzęt do próbkowania, próbkując jedną wartość na linię i umieszczając wynik w pamięci współużytkowanej dla grup, aby uzyskać dostęp do innych linii bez próbkowania. To, czy wygrasz, zależy od tego, ile kosztuje pamięć współdzielona przez grupę: jeśli jest tańsza niż pamięć podręczna tekstur najniższego poziomu, może to być wygrana, ale nie ma na to żadnej gwarancji. Procesory graficzne już całkiem dobrze radzą sobie z bardzo lokalnymi pobieraniami tekstur (z konieczności).

Jeśli masz pośrednie etapy operacji, w których chcesz udostępnić wyniki, bardziej sensowne może być użycie pamięci współdzielonej przez grupę (ponieważ nie możesz polegać na sprzęcie do próbkowania tekstur bez faktycznego zapisania wyniku pośredniego w pamięci). Niestety nie możesz również polegać na wynikach z jakiejkolwiek innej grupy wątków, więc drugi etap musiałby ograniczyć się tylko do tego, co jest dostępne w tym samym kafelku. Myślę, że kanonicznym przykładem tutaj jest obliczenie średniej luminancji ekranu dla automatycznej ekspozycji. Mógłbym również wyobrazić sobie połączenie upsamplowania tekstur z jakąś inną operacją (ponieważ upsampling, inaczej niż próbkowanie w dół i rozmycie, nie zależy od żadnych wartości poza danym kafelkiem).

John Calsbeek
źródło
Poważnie wątpię, aby ROP dodało narzut wydajności, jeśli mieszanie jest wyłączone.
GroverManheim,
@GroverManheim Zależy od architektury! Etap fuzji wyjściowej / ROP musi również zajmować się gwarancjami zamawiania, nawet jeśli mieszanie jest wyłączone. Trójkąt pełnoekranowy nie powoduje żadnych zagrożeń związanych z zamawianiem, ale sprzęt może tego nie wiedzieć. Mogą istnieć specjalne szybkie ścieżki w sprzęcie, ale wiedząc na pewno, że się do nich kwalifikujesz…
John Calsbeek,
10

John napisał już świetną odpowiedź, więc zastanów się nad jej przedłużeniem.

Obecnie dużo pracuję z modułami obliczeniowymi dla różnych algorytmów. Ogólnie rzecz biorąc, odkryłem, że shadery obliczeniowe mogą być znacznie szybsze niż ich równoważne shadery pikseli lub alternatywy oparte na sprzężeniu zwrotnym.

Gdy obejrzysz się, jak działają shadery obliczeniowe, w wielu przypadkach mają one również większy sens. Użycie modułu cieniującego piksele do filtrowania obrazu wymaga skonfigurowania bufora ramki, wysyłania wierzchołków, korzystania z wielu stopni modułu cieniującego itp. Dlaczego powinno to być wymagane do filtrowania obrazu? Przyzwyczajenie się do renderowania pełnoekranowych quadów do przetwarzania obrazów jest z pewnością jedynym „ważnym” powodem, aby nadal je używać. Jestem przekonany, że nowicjusz w dziedzinie grafiki obliczeniowej uznałby, że shadery obliczeniowe są bardziej naturalne do przetwarzania obrazu niż renderowania tekstur.

Twoje pytanie dotyczy w szczególności filtrowania obrazów, więc nie będę zbyt szczegółowo omawiał innych tematów. W niektórych naszych testach po prostu skonfigurowanie sprzężenia zwrotnego transformacji lub przełączenie obiektów bufora ramki w celu renderowania na teksturę może spowodować koszty wydajności około 0,2 ms. Pamiętaj, że wyklucza to wszelkie renderowanie! W jednym przypadku zachowaliśmy dokładnie ten sam algorytm przeniesiony do obliczeń shaderów i zauważyliśmy zauważalny wzrost wydajności.

Podczas korzystania z shaderów obliczeniowych można wykorzystać więcej krzemu na GPU do wykonania rzeczywistej pracy. Wszystkie te dodatkowe kroki są wymagane podczas korzystania z trasy modułu cieniującego piksele:

  • Zespół wierzchołków (czytanie atrybutów wierzchołków, dzielniki wierzchołków, konwersja typów, rozwijanie ich do vec4 itp.)
  • Moduł cieniujący wierzchołek musi być zaplanowany bez względu na to, jak minimalny jest
  • Rasterizer musi obliczyć listę pikseli w celu przyciemnienia i interpolacji wyników wierzchołków (prawdopodobnie tylko współrzędne tekstury do przetwarzania obrazu)
  • Wszystkie różne stany (test głębokości, test alfa, nożycowy, mieszanie) muszą być ustawione i zarządzane

Można argumentować, że inteligentne sterowniki mogą negować wszystkie wspomniane wcześniej zalety wydajności. Miałbyś rację. Taki sterownik może stwierdzić, że renderujesz quad pełnoekranowy bez testowania głębokości itp., I skonfigurować „szybką ścieżkę”, która pomija wszelkie bezużyteczne prace obsługiwane w celu obsługi modułów cieniujących piksele. Nie zdziwiłbym się, gdyby niektórzy kierowcy zrobili to w celu przyspieszenia przetwarzania końcowego w niektórych grach AAA dla ich konkretnych układów GPU. Możesz oczywiście zapomnieć o takim leczeniu, jeśli nie pracujesz nad grą AAA.

Sterownik nie może jednak znaleźć lepszych możliwości równoległości oferowanych przez potok shadera obliczeniowego. Weź klasyczny przykład filtra gaussowskiego. Korzystając z shaderów obliczeniowych, możesz zrobić coś takiego (oddzielając filtr lub nie):

  1. Dla każdej grupy roboczej podziel próbkowanie obrazu źródłowego na wielkość grupy roboczej i zapisz wyniki w pamięci wspólnej grupy.
  2. Oblicz dane wyjściowe filtra, korzystając z przykładowych wyników zapisanych we wspólnej pamięci.
  3. Napisz do tekstury wyjściowej

Krok 1 jest tutaj kluczem. W wersji cieniowania pikseli obraz źródłowy jest próbkowany wiele razy na piksel. W wersji modułu obliczeniowego każdy tekst źródłowy jest odczytywany tylko raz w grupie roboczej. Odczyty tekstur zwykle używają pamięci podręcznej opartej na kafelkach, ale ta pamięć podręczna jest nadal znacznie wolniejsza niż pamięć współdzielona.

Filtr gaussowski jest jednym z prostszych przykładów. Inne algorytmy filtrowania oferują inne możliwości udostępniania wyników pośrednich wewnątrz grup roboczych przy użyciu pamięci współdzielonej.

Jest jednak pewien haczyk. Shadery obliczeniowe wymagają wyraźnych barier pamięci, aby zsynchronizować swoje dane wyjściowe. Istnieje również mniej zabezpieczeń chroniących przed błędnym dostępem do pamięci. Dla programistów z dobrą znajomością programowania równoległego shadery obliczeniowe oferują znacznie większą elastyczność. Ta elastyczność oznacza jednak, że łatwiej jest również traktować shadery obliczeniowe jak zwykły kod C ++ i pisać wolny lub niepoprawny kod.

Referencje

bernie
źródło
Intrygujący opisany przez ciebie ulepszony paralelizm prób jest intrygujący - mam płynną kartę SIM, która jest już zaimplementowana z modułami obliczeniowymi z dużą liczbą wystąpień wielu próbek na piksel. Używanie pamięci grupowej do pojedynczego próbkowania z barierą pamięci, jak opisujesz, wydaje się świetne, ale odłożyłem słuchawkę na jeden bit - jak uzyskać dostęp do sąsiednich pikseli, gdy wpadną one do innej grupy roboczej? np. jeśli mam domenę symulacyjną 64x64, rozłożoną na wysyłkę (2,2,1) numthreads (16,16,1), w jaki sposób piksel o id.xy == [15,15] otrzyma sąsiednie piksele ?
Tossrock
W takim przypadku widzę 2 główne opcje. 1) zwiększ rozmiar grupy powyżej 64 i zapisuj wyniki tylko dla pikseli 64x64. 2) najpierw próbka 64 + nX64 + n podzielona w jakiś sposób na grupę roboczą 64x64, a następnie użyj do obliczeń tej większej siatki „wejściowej”. Najlepsze rozwiązanie zależy oczywiście od twoich konkretnych warunków i sugeruję, abyś napisał kolejne pytanie, aby uzyskać więcej informacji, ponieważ komentarze nie są do tego odpowiednie.
bernie
3

Natknąłem się na tego bloga: Compute Shader Optimizations for AMD

Biorąc pod uwagę, jakie sztuczki można wykonać w module obliczeniowym (które są specyficzne tylko dla modułów obliczeniowych) byłem ciekawy, czy równoległa redukcja w module obliczeniowym była szybsza niż w przypadku modułu cieniującego piksele. Wysłałem e-mail do autora, Wolfa Engela, aby zapytać, czy próbował shadera pikseli. Odpowiedział, że tak i wstecz, gdy pisał post na blogu, wersja modułu obliczeniowego była znacznie szybsza niż wersja modułu cieniującego piksele. Dodał również, że dziś różnice są jeszcze większe. Najwyraźniej zdarzają się przypadki, w których korzystanie z modułu obliczania może być wielką zaletą.

maksimum
źródło