Programowanie współbieżne, Języki programowania, Aplikacje, Gry

GeForce, Radeon, GCN, VLIW, SIMD, bitcoin

Witam po dłuższej przerwie. Od jakieś czasu próbowałem wrócić do pisania serii postów o architekturze cpu i gpu i jak widać nie najlepiej mi to szło 🙂 Obiecuje jednak wrócić do tego w niedługim czasie, choć w nieco innej formie. W tym poście, też będzie o architekturze ale nieco bardziej powierzchownie, skupie się przede wszystkim na obaleniu kilku mitów, o których można poczytać w internecie. Po prostu nie mogę już patrzeć jakie dziwne teorie na temat GPU niektórzy wymyślają 🙂

Przed dalszą lekturą tego wpisu polecam przypomnienie sobie treści tych postów:

http://www.mbapp.com/2010-01/cpu-vs-gpu-gflops/

http://www.mbapp.com/2010-11/cpu-vs-gpu-architektura-cz-1/

Pytanie podstawowe brzmi: Co jest szybsze: GeForce czy Radeon? Wiemy, że te drugie mają więcej teoretycznych GFLOPS, zarówno jedne i drugie nieźle radzą sobie w grach, dodatkowo Radeony dają gigantyczną przewagę nad GeForce podczas generowania bitcoinów. Wydawać by się mogło, że architektura Radeonów jest znacznie lepsza. W takim razie po co AMD zmienia architekturę w topowych Radeonach serii 7 na Graphics Core Next?

Żeby odpowiedzieć na to pytania trzeba zastanowić się dlaczego realna wydajność może różnić się od wydajności teoretycznej. W dużym uproszczeniu mogą to być trzy powody:

  1. GPU może mieć dodatkowe jednostki specjalne, które nie są wliczane do wydajności teoretycznej (np. jednostki obliczające wartości funkcji sin, 1/x, ln), czy też ogólne jednostki oprócz standardowych operacji dodawania, mnożenia (na których bazują wyliczenia teoretycznych FLOPS’ów) mogą obsługiwać dodatkowe operacje jak choćby przesunięcia bitowe.
  2. Zależności pomiędzy jednostkami SIMD, VLIW, SIMT uniemożliwiają pełne wykorzystanie wszystkich jednostek obliczeniowych dla określonych danych i algorytmu, który je przetwarza.
  3. Jednostki obliczeniowe muszą czekać na dane, które nie zostały dostarczone do wewnętrznych rejestrów na czas. W tym wypadku (pomijając oczywiste rzeczy jak prędkość pamięci karty graficznej i szerokość szyny łączącej pamięci z GPU) ważne jest jak GPU zachowuje się podczas odczytu i zapisu danych niewyrównanych, czy posiada jakiekolwiek mechanizmy cache’owania danych (a jeśli tak to jakie), jak zachowuje się podczas wykonywania operacji atomowych na globalnej pamięci.

Różnice pomiędzy układami Nvidii i AMD widoczne są na każdym z tych trzech obszarów.

Ad1. Na stronie Why_are_AMD_GPUs_faster_than_Nvidia_GPUs można między innymi przeczytać, że układy AMD posiadają specjalną instrukcję do wykonania rotacji na zmiennej całkowitej, z kolei na układach Nvidii, tą instrukcje trzeba zastąpić aż trzema instrukcjami (2 shift + 1 add). Podczas obliczania „teoretycznych FLOPSów” takich rzeczy nie bierze się pod uwagę. Tych różnic jest więcej (jedne na korzyść Nvidii, drugie na korzyść AMD) .

Ad2. Układy AMD (przede wszystkim serie do 6xxx włącznie) cechują się większą ilością prymitywnych jednostek obliczeniowych, z kolei Nvidia ma ich mniej ale ich wydajność w przypadkach pesymistycznych będzie większa. Dlaczego? Weźmy np. obliczenia na 32bitowych liczbach zmiennoprzecinkowych.

GeForce GTX 580 w ciągu dwóch cykli jest w stanie przetworzyć 32 niezależne ścieżki kodu z kolei Radeon 6970 może przetworzyć 24 niezależne ścieżki w ciągu 4 cykli zegara. Biorąc pod uwagę dużo większe taktowanie shaderów w układach Nvidii dostajemy dużą przewagę. Skąd to się bierze i jakie ma to znaczenie?

GeForce GTX 580 posiada 16 multiprocesorów, z których każdy posiada dwa schedulery warpów (warp  – grupa wątków, która zawsze przetwarza tą samą ścieżkę kodu, w każdym do tej pory wyprodukowanym GPU od Nvidii zawiera 32 wątki ). Taki multiprocesor wybiera dwa warpy i uruchamia je, pierwszy na pierwszych 16 jednostkach SP, drugi na kolejnych 16 jednostkach SP, których jest łącznie 32 w jednym multiprocesorze. Ponieważ jeden warp zawiera 32wątki a uruchamiany jest tylko na 16 SP to wykonanie jednej instrukcji dla takiego warpa zajmie dwa cykle (po jednym dla dolnej i górnej połowy warpa). Można więc powiedzieć (w dużym uproszczeniu), że GTX 580 zawiera 32 jednostki MIMD (16 multiprocesorów z dwoma schedulerami), a każdy z MIMDów zawiera 32 jednostki SIMT, które wykonują jedną instrukcje na 2 cykle.

W tym miejscu zatrzymam się na moment żeby doprecyzować i pokazać co oznacza to co napisałem powyżej. Dla przykładu uruchamiamy 64 wątki czyli dwa warpy. Wykonywany jest następujący kod:

if (watek_id <12)
  instrukcja1()
else
  instrukcja2();

Pierwszy warp będzie przetwarzany następująco (wątki 0-31)
1. ustawienie flagi na podstawie warunku dla wątków (0-15)
2. ustawienie flagi na podstawie warunku dla wątków (16-31)
3. wykonanie instrukcji1 dla wątków 0-11, procesory zajmujące się wątkami 12-15 wykonują pusty cykl
4. pusty cykl
5. wykonanie instrukcji2 dla wątków 12-15, procesory zajmujące się watkami 0-11 wykonują pusty cykl
6. wykonanie instrukcji2 dla wątków (16-31)

Natomiast drugi warp (wątki 32-63) zostanie przetwarzany tak:
1. ustawienie flagi na podstawie warunku dla wątków (32-47)
2. ustawienie flagi na podstawie warunku dla wątków (48-63)
3. wykonanie instrukcji2 dla wątków (32-47)
4. wykonanie instrukcji2 dla wątków (48-63)

Dla porównania powiem że GeForce 9800 GTX posiada 16 SIMF (16 multiprocesorów bez obsługi wielu niezależnych programów  w jednym momencie – dlatego SIMF nie MIMD) które zawierają 32SIMT wykonujące 1 instrukcję w ciągu 4 cykli (multiprocesor zawiera tylko 8SP przez co jeden warp dzielony jest na 4 części i wykonywany przez 4 cykle).

W układach GPU firmy AMD odpowiednikiem warpa jest wavefront i najczęściej (Radeon 5870, 6970) zawiera 64 wątki. W porównaniu do Nvidii u AMD jeden wątek jest obsługiwany nie przez jeden procesor a przez grupę VLIW (4 lub 5 sp unit). Można powiedzieć więc, że Radeon 6970 zawiera 24 MIMD (24 SIMD Engine) zawierające 64 SIMT (16 shadery przetwarzające jeden wavefront w ciągu 4 cykli) a każdy SIMT zawiera 4 jednostki VLIW.

Dla osób ciekawych dlaczego wavefront nie zajmuje po prostu 16 wątków wklejam cytat z dokumentacji do AMD APP:

„All stream cores within a compute unit execute the same instruction for each cycle. A work item can issue one VLIW instruction per clock cycle. The block of work-items that are executed together is called a wavefront. To hide latencies due to memory accesses and processing element operations, up to four workitems from the same wavefront are pipelined on the same stream core. For example, on the ATI Radeon™ HD 5870 GPU compute device, the 16 stream cores execute the same instructions for four cycles, which effectively appears as a 64-wide compute unit in execution width. The size of wavefronts can differ on different GPU compute devices. For example, the ATI Radeon™ HD 5400 series graphics cards has a wavefront size of 32 work-items. The ATI Radeon™ HD 5800 series has a wavefront size of 64 work-items.”

Na Radeonie wcześniejszy przykład zadziała następująco:

Wszystkie wątki uruchamiane są w ramach jednego wavefronta (0 – 63):
1. ustawienie flagi na podstawie warunku dla wątków (0-15)
2. ustawienie flagi na podstawie warunku dla wątków (16-31)
3. ustawienie flagi na podstawie warunku dla wątków (32-47)
4. ustawienie flagi na podstawie warunku dla wątków (48-63)
5. wykonanie instrukcji1 dla wątków 0-11, procesory zajmujące się wątkami 12-15 wykonują pusty cykl
6. pusty cykl (wątki 16-31)
7. pusty cykl (wątki 32-47)
8. pusty cykl (wątki 48-63)
9. wykonanie instrukcji2 dla wątków 12-15, wątki 0-11 wykonują pusty cykl
10. wątki 16-31 wykonują instrukcje2
11. wątki 32-47 wykonują instrukcje2
12. wątki 48-63 wykonują instrukcje2

Chyba każdy widzi różnicę na korzyść mniejszych grup wątków. Jeszcze większe marnotrawienie mocy GPU możemy doświadczyć w kodzie posiadającym zagnieżdżone instrukcje warunkowe, instrukcje switch czy co gorsza pętle. W przypadku bardzo prostego kodu:

if (warunek)
  instrukcja;

jeśli warunek będzie spełniony tylko dla jednego wątku z grupy (warp lub wavefront) to okazuje się, że dostajemy tylko 1/32 teoretycznej mocy GeForce GTX 580 i tylko 1/256 (64*VLIW4) teoretycznej mocy Radeona 6970 a nawet 1/320 (64*VLIW5) teoretycznej mocy Radeona 5870.

Ad3. Poprzednia część wpisu za bardzo się rozrosła więc odpowiedź do punktu 3 pominę. Wspomnę tylko, że Nvidia random access cache posiada już od serii GeForce GTX4xx (czyli od niespełna dwóch lat). AMD dopiero dzisiaj zaprezentowała pierwsze GPU (Radeon 7970) z random access cache. Wcześniejsze modele zawierały tylko i wyłącznie znacznie prostszą pamięć cache tekstur.

 

Oczywiście sprawa nie jest całkiem prosta. Możemy usunąć VLIW, możemy dawać małe grupy SIMD, możemy dać dużo zaawansowanej pamięci cache. Tylko, że za to wszystko trzeba zapłacić w postaci coraz większej ilości tranzystorów. Tak więc pytanie z początku postu (Co jest szybsze: GeForce czy Radeon?) sprowadza się do pytania: Co jest lepsze, większa ilość prymitywnych jednostek radzących sobie bardzo dobrze w sytuacjach optymistycznych i niezbyt zaawansowany cache (Radeon 5870, 6970) czy mniej jednostek ale luźniej ze sobą powiązanych z większością ilością bardziej zaawansowego cache?

Do wykonania prostych algorytmów hasz-ujących, do generowania bitcoinów, do gier pokroju DirectX9 architektura Radeonów 5xxx i 6xxx wydaje się być wystarczająca a nawet lepsza ale do GPGPU i do gier DirectX11 architektura GTX 4xx i 5xx okazuje się być lepsza. W bardziej zaawansowanych algorytmach trudno ominąć problemy wielu instrukcji warunkowych, pętli czy niesekwencyjnego dostępu do danych. Pozostają też problemy ściśle specyficzne dla jednostek VLIW. Czy instrukcje w ramach jednego wątku da się zrównoleglić? Jak przygotować kompilatory, które będą w stanie optymalnie wykorzystać jednostki VLIW?

Dlatego też AMD postanowiło zmienić architekturę w Radeonie 7970. Graphics Core Next (bo tak nazywana jest ta architektura) jest już pozbawiona jednostek VLIW a system pamięci cache jest tak samo zaawansowany jak ten w GeForce GTX 480/580. Opierając się o opis na stronie http://www.guru3d.com/article/amd-radeon-hd-7970-review/5 można powiedzieć, że Radeon posiada 128 jednostek MIMD (32 compute unit, każdy zawierający 4 schedulery wavefront’ów), a każda jednostka MIMD zawiera 64 jednostki SIMD wykonujące jedną operacje na 4 cykle (dalej jeden 64 wątkowy wavefront przydzielany jest do 16 jednostek).

Na koniec chce jeszcze odpowiedzieć na dość głupie ale niestety zbyt często pojawiające się stwierdzenie, które brzmi mniej więcej tak:

„Architektura AMD (Radeony 5xxx i 6xxx) jest bardzo dobra do gier jak i rewelacyjna do zastosowań GPGPU, widać to m.in na OpenCL’owym programie do kopania bitcoin’ów gdzie Radeony biją na głowę GeForce. Gdyby programiści nie byli leniwy to napisali by też inne programy i chodziły by one szybciej na Radeonach niż GeForce’ach”

Programowanie (a szczególnie proces optymalizacji) GPU jest trudniejszy od programowania CPU ale pewnych barier nie da się przeskoczyć i wiele programów, nawet  maksymalnie zoptymalizowanych na Radeonach 5xxx i 6xxx po prostu będzie działać wolno a czasami tak wolno, że przenoszenie programu na te układy nie będzie miało w ogóle sensu.

Leave a Reply