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

GeForce, Radeon, GCN, VLIW, SIMD, bitcoin

Grudzień 22nd, 2011 Marcin Borowiec

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.

Następca OpenCL zapowiedziany!

Czerwiec 19th, 2011 Marcin Borowiec

Pamiętacie mój ostatni wpis o OpenCL? Pisałem w nim, że nie widzę OpenCL jako technologii przyszłości do programowania GPGPU. Przewidywałem także, że pojawi się nowa technologia, która ułatwi sposób tworzenia oprogramowania na GPU i wykorzysta przyszłe układy CPU i GPU. Przewidywania okazały się bardzo trafne i dzisiaj mogę wam powiedzieć jak ta technologia nazywa.

Tak więc po kolei:

I. Jak ta technologia się nazywa?

C++ AMP

II. Kto ją zapowiedział?

Herb Sutter w imieniu firmy Microsoft (w której pracuje) podczas konferencji organizowanej przez firmę AMD.

III. Co to jest C++ AMP?

Jest to otwarta specyfikacja rozszerzeń do języka C++ ver2011 (C++0x powinien zostać oficjalnie zatwierdzony w tym roku) zaproponowana przez Microsoft. Specyfikacja zawiera opis zestawu podzbiorów pełnego C++ i dodatkowej biblioteki z algorytmami i kontenerami. W obecnej chwili Microsoft mówi o jednym podzbiorze języka – podzbiorze pozbawionym m.in części operacji na wskaźnikach zgodnym z wymaganiami DirectCompute (elementu biblioteki DirectX 11). W przyszłej wersji Visual Studio Microsoft dostarczy dwie wersje kompilatora zgodnego z C++ AMP: domyślny pod CPU i w wersji restrict(direct3d) pod GPU. Kod pod CPU będzie wykonywał się natywnie, natomiast kod pod GPU będzie wykonywany poprzez bibliotekę DirectX 11. AMD zamierza stworzyć także swój własny kompilator pod C++ AMP (za pewne rozszerzy, któryś z open source’owych kompilatorów) i udostępni go pod systemy Windows, Linux i inne. Kompilator obsłuży kolejne generacje układów od AMD.

IV. Jak C++ AMP odnosi się do moich „5 punktów” z postu o OpenCL?

1. Ad: Możliwości zapisania programu w języku wysokiego poziomu bez sztucznego podziału: kod po stronie CPU, kod po stronie GPU

Kod tworzy się w języku C++ przy pomocą bibliotek, które znajdują się w specyfikacji C++ AMP. Poszczególne fragmenty kodu mogą zostać oznaczone jako restrict(nazwa_podzbioru). W zależności od poziomu restrykcji taki kod będzie się mógł uruchomić na różnych typach układów. Warto pamiętać, że kolejne generacje GPU będą coraz mniej restrykcyjne i w pewnym momencie mogą obsługiwać język C++ w pełni. Restrict nie mówi na jakim konkretnie układzie program ma działać a jaki zestaw funkcji taki układ musi posiadać. W tej chwili pełny zestaw zadziała tylko na CPU a zestaw direct3d na CPU i GPU (zarówno od AMD jak i Nvidii). Idea restrict jest znacznie szersza i po więcej informacji odsyłam do podlinkowanego wcześniej video z prezentacji C++ AMP. OpenCL zakładał, że GPU jest akceleratorem, który trzeba obsłużyć z poziomu kodu wykonywanego przez CPU (zainicjować, wysłać mu dane, uruchomić program, pobrać dane). C++ AMP zakłada, że posiadamy system z różnymi typami jednostek obliczeniowych i typami podsystemów pamięci (system heterogeniczny) a podział zadań pomiędzy tymi jednostkami, może być realizowany niejawne poprzez platformę C++ AMP. Jest to rozwiązanie bardziej ogólnie i sprawdzi się dużo lepiej przy kolejnych generacjach CPU i GPU.

2. Ad: Jak najniższego kosztu dostosowania obecnego oprogramowania do uruchomienia na GPU.

Programy pisze się w języku C++ więc jest to dużo ułatwienie, gdyż bardzo duża ilość oprogramowania została stworzona w tym języku. Niestety nie możemy oczekiwać, że koszt przeniesienia oprogramowania na C++ AMP będzie zerowy. Aby przenieść takie oprogramowanie czasem potrzebne będzie zastosowanie bardziej ogólnego mechanizmu do rozdzielania zadań czy użycie algorytmu, który lepiej skaluje się po rozdzieleniu na większą ilość wątków. Są to jednak rzeczy, które nie da się ominąć. Najważniejsze jest to, że nie mamy wielu sztucznych ograniczeń, które ma OpenCL.

3. Ad: Wykorzystania potencjału przyszłych układów: Kolejne wersje układów od Nvidii i AMD, rozwiązania Intela z rodziny Knights (Knights Ferry i Knights Corner).

O tym już pisałem w punkcie 1. C++ AMP ma bardziej przyszłościowy model programowania. Inną kwestią pozostaje to jak Nvidia i Intel odniosą się do tej technologii.

4. Ad: Wykorzystania potencjału jaki przyniosą nowe modele sterowników, chodzi tu m.in o WDDM 2.1 (Windows Display Driver Mode), który pojawi się prawdopodobnie w Windows 8.

W przyszłości zarządzanie zadaniami i pamięcią na GPU będzie odbywać się podobnie jak teraz na CPU. C++ AMP świetnie wpisuje się w ten model.

5. Ad: Narzędzi: debuggery, profilery itp które obsługiwać będzie się podobnie do tych, które obecnie używane są dla programów pisanych pod CPU.

Takie narzędzia ma posiadać następna wersja Visual Studio, łącznie z debuggerem kodu na GPU.

 

GPGPU ewoluuje i razem z nim podejście AMD-ATI. Za czasów Radeonów serii HD2xxx, HD3xxx, HD4xxx ATI udostępniało platformą opartą o kompilator Brook++. Ideą Brook++ było wykorzystanie GPU jako prostego procesora strumieniowego. To rozwiązanie przestało się sprawdzać razem z premierą Radeonów serii HD 5xxx (już przy Radeonach HD 4xxx słabo się sprawdzało), które posiadały wiele funkcji, z których nie można było skorzystać za pomocą Brook++. AMD porzuciło wtedy Brook++ i swoją uwagę skupiło na OpenCL. Tylko, że OpenCL też niedługo przestanie się sprawdzać. Prawdopodobnie już od Radeonów serii HD8xxx AMD będzie potrzebować czegoś więcej (C++ AMP) niż OpenCL.

Pozostaje pytanie co zrobi w tej sytuacji Nvidia. Jej CUDA było lepsze od Brook++, OpenCL zdobyło dość dużą popularność ale nie nadszarpnęło zbytnio pozycji platformy CUDA. Nvidia nawet olała implementacje OpenCL w wersji 1.1. Tylko, że teraz przeciwnik jest na prawdę duży. Co innego walczyć z firmą, która przeznacza znacznie mniejsze pieniądze na GPGPU (AMD kiedyś), a co innego walczyć z firmą, której heterogeniczne maszyny (kolejne generacje APU) są obecnie priorytetem nr 1. (AMD dziś) a pomaga jej firma Microsoft.

CPU vs GPU: Architektura cz. 2

Kwiecień 1st, 2011 Marcin Borowiec

W poprzednim poście przedstawiłem różne sposoby zwielokrotniania jednostek obliczeniowych w procesorach. Dzisiaj opisze, z jakich jednostek obliczeniowych składają się popularne układy CPU. Ponieważ nie ma dużych różnic pomiędzy układami AMD i Intela opisze tylko jeden model procesora. Będzie to Intel Core i7-950, procesor ma 4 rdzenie, obsługuje technologię Hyper-threading i posiada obsługę instrukcji SSE w wersji 4.2.

Jako przykład specjalnie wybrałem wersję procesora z Hyper-threading. O tej technologii będę pisał dokładnie w kolejnych częściach. Dzisiaj wspomnę tylko jedną rzecz, która potrzebna jest do zrozumienia tego postu. Hyper-threading powoduje, że jeden rdzeń procesora jest w stanie obsłużyć dwa wątki sprzętowe w jednym momencie. Wątki są w pełni niezależne i z punktu widzenia podziału jednostek obliczeniowych z poprzedniego postu jeden rdzeń zawiera dwie jednostki typu MIMD. Mnożąc to razy ilość rdzeni otrzymujemy mini schemat procesora Intel Core i7-950:

Ten rysunek zakłada, że do operacji wykorzystujemy jedynie podstawowe instrukcje operujące na pojedynczych wartościach (całkowitych lub zmiennoprzecinkowych). Takie instrukcje wystarczają aby napisać dowolny program i pewien podzbiór programów używa tylko tych instrukcji. Opisywany procesor obsługuje także instrukcje SSE. Instrukcje te pozwalają na wykonywanie operacji na grupie spakowanych danych tworząc w ten sposób grupę jednostek typu SIMD. Ilość logicznych jednostek SIMD jest zależna od wielkości danych, na których operujemy. Ponieważ spakowane dane zajmują 128 bit to kombinacje jakie otrzymaliśmy to: 2 x 64 bit (całkowite lub zmiennoprzecinkowe), 4 x 32bit (całkowite lub zmiennoprzecinkowe), 8 x 16 bit (całkowite), 16 x 8 bit (całkowite). Na schemacie może to wyglądać tak:

W ten sposób otrzymujemy drzewo jednostek obliczeniowych. Procesor zawiera 8 jednostek MIMD z czego każda składa się z kilku jednostek SIMD. Oczywiście w danym momencie możemy np. na pierwszej jednostce MIMD korzystać ze zwykłych instrukcji na liczbach skalarnych podczas gdy na drugiej jednostce MIMD będziemy liczyć na wektorze czterech 32 bitowych liczb a na trzecim MIMD będziemy liczyć na wektorze dwóch 64bitowych liczb.

Procesory AMD działają bardzo podobnie. Jedyna różnica to taka, że otrzymujemy SSE w nieco uboższej trzeciej wersji i żaden z modeli produkowanych przez AMD nie posiada technologii Hyper-Threading. Pewną zmianą są natomiast procesory z drugiej generacji Intel Core i7 i Intel Core i5. Posiadają one zestaw nowych instrukcji o nazwie AVX. W stosunku do SSE różnią się przede wszystkim wielkością spakowanej liczby, która teraz wynosi 256bit co zwiększa dwukrotnie ilość logicznych jednostek SIMD dla danej wielkości liczb.

W kolejnym poście przedstawię budowę GPU Nvidi.

CPU vs GPU: Architektura cz. 1

Listopad 17th, 2010 Marcin Borowiec

Tym postem zaczynam serię artykułów, w których porównam architekturę aktualnie dostępnych układów CPU od Intela i AMD, układów GPU firmy NVidia montowanych w kartach GeForce począwszy na serii 8xxx i skończywszy na najnowszej 5xx, układów GPU firmy AMD/ATI montowanych w kartach Radeon począwszy od serii HD 4xxx i skończywszy na serii HD 6xxx, a także układów z serii Knights od Intela.

Ponieważ temat jest bardzo rozległy musiałem go podzielić na kilka części:

  1. Opisanie różnych typów jednostek obliczeniowych pod kątem „niezależności” wykonywania różnych ścieżek programu naraz czy też możliwości wykonywania operacji na wielu różnych porcjach danych w „jednym momencie”.
  2. Porównanie układów pod kątem typów jednostek obliczeniowych  w nich występujących (a raczej hierarchii tych jednostek)
  3. Opisanie sposobów wymiany danych i synchronizacji pomiędzy jednostkami. Opisanie dostępu do pamięci a także rozjaśnienie terminów: superskalarność, dual-issue
  4. Powiązanie informacji wymienionych w pkt3. z konkretnymi układami.
  5. Podanie przykładów algorytmów i sposoby ich implementacji dla różnych układów.

Jakiś czas temu porównałem ze sobą różne układy gpu i cpu pod względem teoretycznej maksymalnej wydajności. W tym i następnym poście postaram się już częściowo odpowiedzieć, które układy potrafią wykorzystać w większym stopniu swoją moc obliczeniową, a które układy będą mieć „puste GFLOPSy”.  Sytuacje można w dużym stopniu porównać do procesorów Pentium 4, które były pokonywane przez dużo wolniejsze (taktowane wolniejszym zegarem) Athlony czy Pentium M (które to później ewoluowały w Core 2 Duo). Wtedy mówiliśmy o „pustych gigahercach”

Najnowsze układy CPU i GPU są układami wielordzeniowymi. Zwykłe procesory w komputerach osobistych mają już po 2,3,4 a nawet 6 rdzeni. Co więcej każdy z tych rdzeni zawiera możliwość wykonania obliczeń na kilku danych jednocześnie, między innymi dzięki instrukcjom SSE. Możliwość wykonania kilka instrukcji w jednym cyklu uzyskano także dzięki superskalarności czy funkcji HyperThreading. Jeszcze bardziej ciekawie wygląda sprawa z układami kart graficznych. W najnowszych modelach mamy dostępne już kilkaset małych rdzeni. Te 6 rdzeni w głównym procesorze komputera nie robi przy tym wrażenia. Oczywiście nie dokonano tutaj jakiegoś cudu, te „procesorki” są po prostu dużo bardziej prymitywne. Aby je porównać posłużę się poniższym rysunkiem:

Te skróty na rysunku to sposoby na zwielokrotnienie jednostek obliczeniowych w procesorach  i oznaczają:

  • SIMD (Single Instruction, Multiple Data) – Najprostsze wprowadzenie dodatkowych jednostek obliczeniowych do procesora. Każda jednostka w jednej grupie SIMD wykonuje tą samą operacje na różnych danych. Najlepszym przykładem SIMD są instrukcje SSE w procesorach z rodziny x86. Jednostki SIMD idealnie nadają się do prostych operacji np przetwarzania obrazu. Przykładowo aby rozjaśnić obraz wystarczy dodać do wartości koloru kolejnych pikseli stały kolor. Zastępując jedną skalarną jednostkę obliczeniową jedną grupą SIMD składającą się z 4 jednostek otrzymamy bardzo niskim kosztem 4-krotne przyspieszenie. Aby zwiększyć użyteczność jednostek SIMD stworzono wariacje podstawowych instrukcji, np w przypadku dodawania mamy dodawanie z nasyceniem i dodawanie z przepełnieniem. Jeśli chcemy rozjaśnić obraz to interesuje nas dodawanie z nasyceniem. Gdy operujemy na jednostkach skalarnych możemy zawsze użyć dodawania z przepełnieniem + osobną instrukcję warunkową ustawiającą kolor biały w przypadku przepełnienia. Jednostki w jednej grupie SIMD nie mają własnego kontekstu wątku co powoduje, że albo wszystkie jednostki wykonają daną instrukcję albo żadna. Przez co jednej instrukcji dodawania  z nasyceniem nie da się zastąpić dodawaniem z przepełnieniem + instrukcja warunkowa(Przypadek gdy musimy ustawić kolor biały tylko dla części danych). Oczywiście nie da się mnożyć podstawowych instrukcji w nieskończoność i w wielu przypadkach gdy mamy dużo ‚ifów” zależnych od konkretnej wartości, na której operujemy SIMD staje się bezużyteczne.
  • VLIW (Very Long Instruction Word) – Rozszerza SIMD o możliwość wykonania różnych instrukcji dla każdej jednostki wchodzącej w skład grupy VLIW. Nadal jednak to cała grupa posiada jeden kontekst wątku. VLIW ma troszkę inne zadanie niż SIMD. SIMD używamy tam gdzie musimy wykonać tą samą sekwencję operacji na zbiorze danych. VLIW ma za zadanie przyspieszyć wykonanie jednego wątku poprzez zrównoleglenie instrukcji, które od siebie nie zależą, np taką sekwencję:
    1) d = a + b;
    2) e = a * c;
    3) f = c + d;
    można wykonać w ten sposób:
    1)d = a + b;          e = a * c;
    2)f = c + d;
    Mówiąc krótko, wyniki operacji pierwszej i drugiej nie zależą od siebie więc instrukcje mogą zostać wykonane jednocześnie.
  • SIMT (Single Instruction, Multiple Thread) – Termin zaproponowany przez NVidię przy okazji premiery pierwszy kart graficznych z obsługą CUDA. SIMT jest rozszerzeniem SIMD poprzez stworzenie kontekstu wątku dla każdego procesora wchodzącego w skład grupy SIMT. Teraz „ify” zależne od wartości ze zbioru danych, na którym operujemy nie są już takie straszne. W czasie wykonywania instrukcji warunkowej procesor w zależności od aktualnie wykonywanej gałęzi (if albo else) i swojego kontekstu wątku wykonuje daną operacje albo zostaje tymczasowo wyłączony. W przypadku pętli każdy z procesorów wyłącza się po wykonaniu swoich iteracji i czeka aż ostatni z grupy SIMT zakończy pętle. Dopiero wtedy cała grupa SIMT kontynuuje obliczenia.
  • SIMF (Single Instruction, Multiple Flow) – Jest to termin wymyślony przeze mnie na potrzeby tego artykułu. SIMF w porównaniu do SIMT pozwala każdemu procesorowi na niezależne wykonanie sekwencji instrukcji (choć ciągle mówimy o tej samej sekwencji instrukcji dla każdego procesora wchodzącego w skład grupy SIMF). Różnica polega na tym, że każdy z procesorów może przetwarzać inną gałąź tej samej sekwencji. Tzn jeden procesor może wykonywać właśnie instrukcje dla spełnionego warunku „if”, inny może w tym czasie wykonywać kod dla else. W przypadku pętli procesor nie czeka na wyjście z pętli innych procesorów.
  • MIMD (Multiple Instruction, Multiple Data) – Oznacza pełną swobodę. Każdy z procesorów może wykonywać całkiem inny kod na całkiem innych danych. Procesor w grupie MIMD to przede wszystkim popularny rdzeń w procesorze komputera (CPU). Granica pomiędzy SIMF a MIMD jest trochę zamazana. Teoretycznie możemy przecież napisać kod w postaci if (warunek()) Funkcja1() else Funkcja2(). Mamy jedną sekwencje instrukcji, która zawiera dwie gałęzie zależne od warunku. W przypadku SIMF każdy procesor niezależnie wykona gałąź odpowiednią dla niego. Więc czym tak na prawdę różni się SIMF od MIMD? Wyjaśnię to dokładnie w kolejnym poście.

Patrząc na powyższy rysunek można powiedzieć, że sposoby znajdujące się po lewej stronie oznaczają niski koszt zwielokrotnienia jednostek obliczeniowych i dużo ograniczeń co do sposobu zrównoleglenia obliczeń. Im bardziej na prawo tym mamy większą swobodę i możliwości lecz koszt wzrasta. Które z rozwiązań jest najlepsze? To zależy od zastosowania. Niski koszt w przypadku SIMD, VLIW powoduje że możemy dodać dużo dodatkowych jednostek jednak gdy okaże się że nasze obliczenia ciężko zrównoleglić, lepszym rozwiązaniem może być np stworzenie 2 procesorów MIMD zamiast np 16 SIMD. Ważna jest jeszcze jedna rzecz. Nikt nie powiedział, że w danym procesorze możemy mieć tylko jedną grupę jednostek obliczeniowych. Można np. stworzyć układ który będzie zawierał grupę MIMD zawierają grupę procesorów SIMT. Właśnie takie drzewiaste rozwiązania stosuje się w procesorach CPU i GPU. Rysunek pokazuje jeszcze jedną rzecz. Mamy dwie drogi w „zrównoleglaniu” programu. Pierwsze ze względu na dane: SIMD -> SIMT -> SIMF -> MIMD gdzie kolejny skrót oznacza coraz bardziej elastyczny przypadek oraz ze względu na kod VLIW -> MIMD gdzie VLIW oznacza wykonywanie różnych operacji w ramach jednego wątku a MIMD wykonywanie różnych zadań w ramach jednego programu.

Ciąg dalszy w następnym poście.

OpenCL – czy to na pewno technologia przyszłości?

Wrzesień 25th, 2010 Marcin Borowiec

„OpenCL będzie wiodącą technologią na rynku GPGPU”, „OpenCL jest standardem przyszłości”, „NVidia powinna skupić się na obsłudze OpenCL, a nie nadal promować swoje rozwiązania”. Coraz częściej widzę i słyszę takie stwierdzenia. Według mnie są one stanowczo przesadzone  (może oprócz tego pierwszego ).

Teraz OpenCL jest na fali bo:

  • Właściciele Radeonów mają poczucie, że ich karty są czymś więcej niż tylko kalkulatorem do trójkątów.
  • Programista teoretycznie jest w stanie napisać program, który zadziała na kartach NVidii i AMD-ATI.
  • OpenCL dobrze wpasowuje się w obecną sytuacje na rynku pod względem technologicznym (budowa GPU, sposób obsługi karty graficznej w systemie operacyjnym)
  • OpenCL lansowany jest na technologię dzięki, której programista napisze program raz a później uruchomi go na CPU, GPU, WPU (washer processing unit), no dobrze, z tym ostatnim trochę przesadziłem.
  • OpenCL jest pod Windows, Linux, Mac OS

Natomiast to co ja (i pewnie wielu innych programistów) oczekuje od technologii przyszłości to:

  1. Możliwości zapisania programu w języku wysokiego poziomu bez sztucznego podziału: kod po stronie CPU, kod po stronie GPU
  2. Jak najniższego kosztu dostosowania obecnego oprogramowania do uruchomienia na GPU.
  3. Wykorzystania potencjału przyszłych układów: Kolejne wersje układów od NVidii i AMD, rozwiązania Intela z rodziny Knights (Knights Ferry i Knights Corner).
  4. Wykorzystania potencjału jaki przyniosą nowe modele sterowników, chodzi tu m.in o WDDM 2.1 (Windows Display Driver Mode), który pojawi się prawdopodobnie w Windows 8.
  5. Narzędzi: debuggery, profilery itp które obsługiwać będzie się podobnie do tych, które obecnie używane są dla programów pisanych pod CPU.

Oczywiście OpenCL pozwoli uruchamiać programy na przyszłych urządzeniach ale niekoniecznie będzie się takie programy opłacało pisać w OpenCL. To co jest potrzebne to platforma, która pozwoli tanim kosztem pisać programy pod GPU. Co ciekawe to patrząc pod tym kątem to technologia CUDA wypada dużo lepiej od OpenCL. Odnosząc się do wcześniejszych punktów:

  1. OpenCL zakłada, że GPU czy inne APU są po prostu akceleratorami, które sterowane są przez hosta. Hostem jest tutaj typowy CPU. Piszemy więc kod, który inicjalizuje urządzenie (akcelerator), przesyła do niego dane, kompiluje i wczytuje program do akceleratora, uruchamia go i pobiera wyniki. API do sterowania akceleratorem udostępnione jest jako biblioteka języka C, można więc ją łatwo wykorzystać w kodzie C/C++ lub za pomocą bindingów w innych bardziej wysokopoziomowych platformach. Gorzej jest z kodem, który ma zostać uruchomiony na akceleratorze. Piszemy go w dość prymitywnym języku opartym na języku C dostosowanym do potrzeb OpenCL. Dane wymieniane pomiędzy hostem a akceleratorem traktowane są jako dane binarne i każda strona powinna sama je odpowiednio zinterpretować. W porównaniu do pisania programów wyłącznie dla CPU jest to dużo bardziej kłopotliwe. W takim wypadku OpenCL będzie miał zastosowanie tylko tam gdzie: oczekiwany wzrost wydajności jest bardzo duży, ten wzrost przekłada się także na dużo większy zarobek, który pokryje różnice w trudności programowania pomiędzy CPU a CPU i GPU.
  2. Jeszcze większe różnice w kosztach istnieją gdy na OpenCL chcemy przerobić istniejące oprogramowanie. Porównujemy tutaj utrzymanie kodu tylko dla CPU do przepisania części kodu na GPU (tego, które da się przenieść i da jakiś zysk). A co by było gdyby istniała technologia, która pozwoli na dużo tańsze dostosowanie takiego kodu do GPU? Obecnie problemem jest samo GPU jak i sposób zarządzania GPU w systemie operacyjnym. Ale co z kolejnymi wersjami GPU i systemów operacyjnych. Czy założenia OpenCLa pozwolą mu się tak szybko rozwijać? Tutaj jestem dość sceptycznie nastawiony ale może Khronos Group ma jakiś genialny pomysł, którym się na razie nie chwali.
  3. To czego możemy się spodziewać po przyszłych GPU możemy się domyślać po ostatnich zapowiedziach NVidii dotyczące następców układu Fermi jak i modelu sterowników, który trafi do nowszych wersji systemu Windows. Dla przypomnienia w Windows Vista mamy WDDM 1.0 a w Windows 7 mamy WDDM 1.1. To co nas interesuje to opublikowane już jakiś czas temu plany dotyczące WDDM 2.0 i WDDM 2.1: Pierwotnie zakładano że WDDM 2.x pojawi się dużo wcześniej (w Windows 7?) ale realia zmusimy Microsoft do zmiany planów. Co nie zmienia faktu, że kiedyś (w Windows 8?) WDDM 2.0 i 2.1 się pojawi i jest duża szansa że będzie własnie taki jaki planowano że będzie. Ostatnie informacje o układach NVidia Kepler i Maxwell to potwierdzają. Mamy więc dwa podejścia: pierwsze to podejście OpenCL, piszmy jeden program ale w sposób prymitywny i uruchamiajmy go na CPU, obecnych i przyszłych GPU a także innych prostych akceleratorach i drugie: dostosujmy GPU do tego żeby programowało się i korzystało z niego niemal identycznie jak z CPU. Ja wole to drugie podejście. Microsoft i NVidia też 😉
  4. Jak wyżej :)
  5. Istnienie debuggerów i profilerów  nie jest aż tak bardzo powiązane z tym czy program pisany jest w OpenCL czy CUDA więc pominę ten punkt.

Z drugiej strony mamy podejście NVidii w postaci CUDA.

  1. Na początku warto powiedzieć że CUDA to szersze pojęcie. Mamy tutaj zestaw narzędzi nisko i wysokopoziomowych. Niskopoziomowe czyli Driver API to podejście podobne do OpenCL w tym względzie że traktujemy GPU jako akcelerator, którego trzeba zainicjować, skonfigurować, wysłać dane i program itp itd. Program, który tutaj wysyłamy do API akceleratora jest asemblerem. Driver API skierowane jest dla maniaków wydajności jak i programistów platform opartych o CUDA. Dla programistów typowych aplikacji pod CUDA mamy przede wszystkim CUDA C, które na pierwszy rzut oka wydawać by się mogło jest podobne do C z OpenCL ale tak nie jest. CUDA C stara się jak najbardziej zakryć proces konfiguracji i inicjalizacji akceleratora tak samo jak podział pomiędzy kodem CPU i GPU. Oczywiście podział istnieje ale jest i będzie on stopniowo zamazywany przez kolejne wersję oprogramowania CUDA jak i układów GPU.
  2. Jak widać rozwój CUDA ma stopniowo zmniejszać koszt pisania programów pod tą platformę jak i koszt dostowania obecnego kodu do tej platformy. Dla przykładu pierwsze wersje CUDA oferowały wyłącznie wsparcie dla konstrukcji znanych z języka C (choć operacja na wskaźnikach są dość mocno limitowane na urządzenia z CUDA Capability 1.x) to obecne wersje stopniowo wprowadzają obsługę języka C++.
  3. To co możemy spodziewać się po przyszłych GPU napisałem przy okazji pisania przy OpenCL. Tutaj wspomnę, że CUDA będzie się naturalnie rozszerzać i dostosowywać do przyszłej sytuacji
  4. Jak wyżej :)
  5. Pod platformę CUDA mamy środowisko NVidia Parallel Insight. Obecnie ma jeszcze dużo ograniczeń ale jasno pokazuje w jakim kierunku zmierza NVidia. Za pewne długo jeszcze poczekamy zanim AMD zaoferuje coś podobnego.

Popatrzmy także na podejście Intela. Firma ta jakoś specjalnie nie jest zainteresowana OpenCL. Obecnie oferuje procesory x86 z rodziny Core, układy GPU (zintegrowane we wcześniejszych platformach z chipsetem a obecnie z procesorem) a w przyszłości układy z rodziny Knights zgodne z x86. Procesory Core i układy Knights można będzie programować w języku C++ przy pomocą narzędzi/bibliotek takich jak Threading Building Blocks. Jak się do tego odnosi OpenCL? Nie ma żadnych przeszkód, żeby wykorzystać OpenCL do programowania procesorów z rodziny Core czy Knights tylko po co to robić skoro można to robić zwyczajnie w C++? GMA z kolei jest mało wydajne i wystarczy, że w dość dobrym stopniu zaoferuje akceleracje DirectX i odtwarzania filmów i to wszystko co potrzeba.

Podsumowując: Obecnie wykorzystanie GPU jest dość drogie i czasochłonne, dlatego przez najbliższe 2-3 lata GPGPU nie stanie się tak bardzo popularne jak wiele osób mogło by oczekiwać. Z kolei za kilka lat sytuacja będzie wyglądać dużo lepiej ale nie dlatego że OpenCL spopularyzuje GPGPU tylko dlatego, że GPGPU ewoluuje i będą dużo lepsze technologie do programowania GPGPU (chociaż to pewnie już nie będzie się nazywać GPGPU), lepsze niż obecny OpenCL.

Zostawiam wam więc temat do przemyśleń a dla siebie spisany tekst, do którego będę mógł wrócić za kilka lat i sprawdzić czy i jak bardzo się pomyliłem.

CPU vs GPU: GFLOPS

Styczeń 22nd, 2010 Marcin Borowiec

Czytając różne informacje o kartach graficznych możemy się dowiedzieć, że ich wydajność jest wielokrotnie większa od odpowiednich im cenowo procesorów x86. Często można też zobaczyć liczby w postaci nawet ponad 2TFLOPSów dla GPU i maksymalnie 100GFLOPS dla najszybszych CPU. Dzisiaj postaram się opowiedzieć o teoretycznej wydajności różnych układów i tego w jaki sposób można ją obliczyć. Natomiast w kolejnych postach opisze różnice w architekturze CPU i GPU AMD i NVidii i to jak wpływają one na poziom wykorzystania teoretycznej wydajności.

Zanim jednak przejdę do sedna sprawy muszę napisać pewne uaktualnienie do mojego poprzedniego posta o podstawach GPU. Po jego publikacji AMD wydało w końcu sterowniki (Catalyst 9.12 Hotfix) z obsługą DirectCompute 4.1 dla Radeonów 48xx i 47xx. Niestety część osób będzie zawiedziona. Nadal brakuje obsługi DirectCompute w Radeonach 46xx i słabszych, które również były często kupowane. Podobnie lista rozszerzeń OpenCL dla mojego Radeona 4850 jest ciągle pusta. Pisząc o podstawach GPU pominąłem także jedną ale do niektórych zastosowań ważną rzecz. Radeony 47xx, 48xx, 58xx posiadają obsługę obliczeń na liczbach podwójnej precyzji. NVidia z kolei podwójną precyzje posiada tylko w układach opartych na GT200 i jej implementacja jest dość wolna. Dokładniejsze liczby podam w dalszej części postu.

Na początku postu wspomniałem o FLOPSach i myślę że warto napisać najpierw co to jest. Dość dobrą definicje można przeczytać na wikipedii: http://pl.wikipedia.org/wiki/FLOPS. Jednak na powyższej stronie nie ma wyraźnie zaznaczonej jednej sprawy. Wydajność we FLOPS jednej i tej samej maszyny może dotyczyć różnych rzeczy: (i mieć w zależności od tego inną wartość)

  1. Teoretycznej wydajności wszystkich jednostek obliczeniowych opierając się przede wszystkim na operacjach dodawania i mnożenia bez takich operacji jak np. shift.
  2. Wydajności, która została uzyskana w określonym benchmarku. Taka wydajność będzie przeważnie niższa od wartości z pkt1 w związku z np czekaniem na pobranie danych z pamięci, niemożnością wykorzystania specyficznych jednostek przystosowanych tylko do pewnych operacji czy niemożliwością równoległego wykorzystania kilku jednostek z powodu zależnych od siebie operacji. Podobnie jak w poprzednim przypadku liczone są operacje dodawania i mnożenia wykonywane przez benchmark.

Opisywanie teoretycznej wydajności zaczne od dzisiejszych najmocniejszych jednostek CPU. Weźmy dla przykładu czterordzeniowy Core i7. Każdy z rdzeni posiada jedną jednostkę odpowiedzialną za dodawanie na 128bitowych danych i jedną jednostke do mnożenia na 128bitowych danych co oznacza mniej więcej maksymalnie:

  • 2 operacje x87 na rozszerzonych 80bitowych liczbach zmiennoprzecinkowych w jednym cyklu zegara
  • 4 operacje SSE na 64bitowych liczbach zmiennoprzecinkowych (podwójna precyzja) w jednym cyklu zegara
  • 8 operacji SSE na 32bitowych liczbach zmiennoprzecinkowych (pojedyncza precyzja) w jednym cyklu zegara

Co dla czterordzeniowego Core i7 o taktowaniu 3GHz oznacza wydajność odpowiednio 24GFlops, 48GFlops, 96GFlops dla danego typu operacji czyli całkiem sporo.

Dalej mamy np układ GeForce GTX 280. Do dyspozycji mamy tutaj 30 multiprocesorów, z których każdy posiada 8 jednostek SP (Shader Processor) o wydajności 2 operacje pojedynczej precyzji (jedna operacja mad czyli jedna operacja mnożenia i jedna dodawania (a*b+c)) na jeden cykl i dodatkowo 2 jednostki SFU (Special Function Unit), każda o wydajności 1 operacja specjalna (sin, log, sqrt) lub 4 operacje mnożenia na cykl (dzięki tzw. ‚dual issue’). Razem daje to 30*(8*2 + 2*4) = 720 operacji na jeden cykl dla całego GPU. Mnożąc to przez częstotliwość „szaderów” czyli 1296MHz otrzymujemy 933GFLOPS!

Niestety wydajność tego samego układu w podwójnej precyzji jest znacznie mniejsza. Każdy multiprocesor potrafi wykonać tylko jedną operacje MAD (liczoną jako dwie operacje).  Jednostki SFU nie operują w ogóle na 64bitowych liczbach zmiennoprzecinkowych. Co przekłada się na „tylko” 30*2 = 60 operacji na jeden cykl. Przy częstotliwości 1296MHz daje to ok 78GFlops.

Konkurencyjny Radeon 4870 wydany w podobnym czasie co GTX280 posiada 160 SP pogrupowanych po 16SP w jednym SIMD Engine. Każdy shader składa się z 5 jednostek SPu potrafiących wykonać jedną operacje mad na cykl (także liczone jako 2 operacje na cykl). Dodatkowo jedna z tych 5 jednostek potrafi wykonać jedną instrukcję specjalną na cykl (zamiast instrukcji mad). Razem daje to 10 * 16 * 5 * 2 = 1600 operacji na cykl czyli 1.2TFlops przy częstotliwości 750MHz. Jak widać teoretyczna wydajność Radeona 4870 jest większa niż GeForca GTX280.

W przypadku podwójnej precyzji przewaga Radeona znacznie się zwiększa, gdyż każdy SP potrafi wykonać jedną instrukcje MAD na cykl co daje 10*16*2=320 operacji na cykl co przekłada się na 240GFlops. Ponad 3x więcej niż GTX280! O ile w przypadku pojedynczej precyzji GeForce „nadrabia architekturą” i w wielu przypadkach jest szybszy od Radeona to w double precision trzeba uznać wyższość Radeona. Od razu jednak przypomnę że, aby wydobyć pełnię mocy Radeona trzeba wykorzystać niezbyt wygodny CAL.

Najnowszy Radeon 5870 z punktu widzenia dzisiejszego tematu postu jest podwojonym Radeonem 4870 (zamiast 10 SIMD Engine mamy ich 20) co oznacza 3200 operacji na cykl dla pojedynczej precyzji i 640 operacji dla podwójnej precyzji. Przy zwiększonym taktowaniu rdzenia do 850MHz otrzymujemy 2.72TFlops dla pojedynczej precyzji i 544GFlops dla podwójnej.

Niestety ciągle nie wiadomo jakie parametry będzie miał zapowiadany układ GF100 od NVidii. Brakuje informacji o częstotliwości rdzeni i dokładniejszych informacji o jednostkach SFU. Można jednak spodziewać się wyraźnie wyższej wydajności od Radeona 5870 w podwójnej precyzji i wyraźnie niższej wydajności w pojedynczej precyzji.

Na koniec warto przypomnieć że teoretyczna maksymalna wydajność to nie wszystko. Układy CPU mimo iż teoretycznie wielokrotnie wolniejsze od GPU w wielu zastosowaniach są niezastępowalne. Podobnie Radeon 4870 teoretycznie szybszy od GeForca GTX 280 przegrywa z nim w prawie każdej grze. W kolejnych postach postaram się opisać różnice w architekturze, które decydują o wydajności w realnych zastosowaniach.

Volatile a publikacja obiektu w C++

Grudzień 5th, 2009 Marcin Borowiec

W jednym z poprzednich wpisów pisałem o problemie publikacji obiektu w programie wielowątkowym. Podałem wtedy bardzo prosty przykład kodu w pseudo asemblerze. Z kolei w  języku C++ publikacja mogła by wyglądać następująco:

bool gotowe = false;
Object *object = 0;
// ...
object = new Object(a,b,c);
gotowe = true;

Jak już wiemy abyśmy mieli gwarancje że object będzie dostępny jak tylko zmienna gotowe zmieni wartość na true, musimy upewnić się, że zapisy nie zostaną przeniesione przed inne zapisy i odczyty nie zostaną przeniesione za inne odczyty. W przypadku języków C# czy Java coś takiego możemy wymusić za pomocą deklaracji gotowe jako volatile. Niestety albo na szczęście standard C++ nic takiego nie przewiduje. W takim razie co należy zrobić aby uzyskać możliwość publikowania obiektu?

  • Jeśli nasz program będzie uruchamiany wyłącznie na procesorach x86 możemy zadeklarować gotowe jako volatile. To w zupełności wystarczy, gdyż model tej architektury jest stosunkowo mocny (Po szczegóły odsyłam do moich poprzednich wpisów).
  • Jeśli nasz program będziemy kompilować wyłącznie w Visual C++ 2005 lub nowszym, podobnie jak w poprzednich punkcie mamy gwarancje że volatile zadziała. Także dla kompilacji pod IA64. Jest to specjalna właściwość kompilatora Microsoftu.
  • W pozostałych przypadkach musimy albo opakować odwołania do gotowe w sekcję krytyczną albo użyć specjalistycznej biblioteki, z funkcją która wymuszą odpowiednie memory barrier. (Co prowadzi do oszczędności czasu procesora w stosunku do sekcji krytycznej).

Odnosząc się do słówka volatile pozostają jeszcze dwa pytania. Czy w takim razie volatile jest prawie całkiem bezużyteczne? oraz co o volatile mówi przyszły standard C++ 0x? Na te pytania postaram się odpowiedzieć, w któryś z kolejnych postów.

GPGPU – podstawowe informacje

Listopad 28th, 2009 Marcin Borowiec

Zauważyłem ostatnio, że popularność GPGPU wśród użytkowników/programistów zaczęła gwałtownie wzrastać. Niestety wielu z nich nie wie co to tak na prawdę jest, jak z tego skorzystać, co trzeba posiadać (sprzęt, oprogramowanie) i jakie ma to możliwości. Postanowiłem więc przybliżyć nieco idę i możliwości GPGPU. Nie zamierzam jednak tworzyć tutaj jakiegoś kursu (przynajmniej na razie).

GPGPU to jak sama nazwa wskazuje możliwość wykorzystania procesorów graficznych do obliczeń ogólnego przeznaczenia. Idea GPGPU zaczeła się rodzić po udostępnieniu kart graficznych zgodnych z DirectX9 (głównie DirectX9c). Operacje zmiennoprzecinkowe z 32bitową precyzją, sensowna długość kodu shadera, dynamiczne instrukcje warunkowe w kodzie, pętle mogły dać początek GPGPU. Niestety w tamtym czasie jedynym możliwym sposobem wykorzystania kart graficznych było napisanie programu z wykorzystaniem DirectX czy OpenGL. Biblioteki te oprócz bezpośredniego wyświetlania obrazu na ekranie posiadają możliwość generowania wyników do tekstury. Programista mógł więc wysłać do karty graficznej dane w postaci tablicy danych (jako tekstura czy bufor wierzchołków), wygenerować wynik i pobrać wynik (teksturę). Do pewnych specyficznych operacji coś takiego wystarczyło, kiedyś czytałem o prostych operacjach na próbkach dźwiękowych dzięki takiemu chwytowi. Niestety do wielu zastowań to zdecydowanie za mało.

Kolejnym etapem było stworzenie kart graficznych z tzw. zunifikowanymi jednostkami cieniowania. Mowa tu seriach GeForce 8xxx i RadeonHD 2xxx z czego produkty NVidi były w kontekście GPGPU bardziej zaawansowane i nadal są, ale o tym nieco później. NVidia razem z premierą serii 8 GeForce’ów udostępniła technologię CUDA. Technologia ta pozwalała w dość prosty sposób wykorzystać moce drzemiące w karcie graficznej. CUDA tworzone było przede wszystkim do obliczeń GPGPU więc nie spotkamy tutaj niepotrzebnych ograniczeń. AMD(ATI) też myślało o czymś podobnym, najpierw zaczeli od ATI CTM (Close To Metal), które dość szybko anulowali bo zrozumieli że nie tędy droga a później stworzyli ATI Stream.

Obecnie interfejsów do programowania kart graficznych jest dość dużo. Poniżej wymienie najważniejsze z nich:

  • C for CUDA czyli programowanie w zmodyfikowanym przez nvidie języku C. Rozwiązanie do tej pory najpopularniejsze, procentowy rozkład użycia tej technologii może w przyszłości nieco zmaleć za sprawą interfejsów otwartych takich jak OpenCL czy DirectCompute.
  • CUDA PTX czyli możliwość pisania programów na GPU NVidii w asemblerze, w pewnych przypadkach sensownym może być utworzenie programu w C, skompilowanie go a później ręczna optymalizacja w asemblerze. Będąc scisłym, PTX nie jest asemblerem wykonywanym bezpośrednio przez GPU. PTX przed wykonaniem musi być przetłumaczony na kod maszynowy konkretnego modelu GPU, na którym będzie uruchomiony program.
  • Brook++ składnik ATI Stream w wersjach 1.x, stosunkowo prosty sposób programowania GPU, łatwy do użycia ale zarazem o niewielkich możliwościach (w porównaniu np do C for CUDA), trudno w Brook++ wykorzystać pełnię mocy Radeonów HD5xxx. Właśnie dlatego AMD przestaje oficjalnie wspierać Brook++ w ATI Stream 2.x. Mimo iż zarzekają się że projekt dalej będzie wspierany, z tym że od teraz jako oddzielny projekt open source to można uznać że jest to w tej chwili ślepa uliczka. AMD chce się teraz skupić na bardziej uniwersalnych interfejsach (OpenCL i DirectCompute)
  • AMD CAL z asemblerem AMD IL jest odpowiednikiem PTX od Nvidii. Asembler daje największe możliwości na zaoptymalizowanie kodu jednak ze wględu na poziom trudności jest bardzo rzadko stosowany.
  • OpenCL, otwarty standard rozwijany przez Khronos Group (przez tą samą grupę, która odpowiada za OpenGL), Każdy z producentów (AMD, NVidia) dostarcza implementację tego interfejsu. Khronos Group zajmuje się tylko certyfikacją gotowych bibliotek. OpenCL posiada także funkcje, które mają umożliwić lepszą współpracę z biblioteką OpenGL: m.in edycję buforów wierzchołków OpenGL z poziomu OpenCL.
  • DirectCompute (element DirectX11), biblioteka przygotowana we współpracy producentów kart graficznych z firmą Microsoft. Charakteryzuje się bardzo dobrą współpracą z innymi elementami DirectX.

Teoretycznie jest możliwe napisanie programu pod GPU nie posiadając odpowiedniego akceleratora u siebie. C for CUDA posiada możliwość emulacji napisanego kodu na CPU, OpenCL posiada implementacje także na CPU (ja przynajmniej znam jedną od AMD), DirectCompute można uruchomić poprzez urządzenie referencyjne (które jest bardzo wolne, WARP niestety nie obsługuje DirectCompute). W rzeczywistości sytuacje, które powstają podczas wykorzystania GPU nijak nie można zasymulować na CPU, tak więc bardzo mocno polecam zaopatrzyć  się w odpowiednią kartę graficzną. Jaką? Dla mnie jedynymi sensownymi układami są najnowsza seria Radeonów HD 5xxx i kart NVidii zgodnych z ComputeCapability przynajmniej 1.1 (czyli seria GTX 2xx, 9xxx i częściowo 8xxx, bez słynnego G80) a także odpowiadające im modele układów Tesla i FireStream. Dlaczego akurat takie karty?

  • Radeon HD seria 2xxx i HD seria 3xxx wypada bardzo ubogo pod względem wydajności obliczeniowej, obsługiwanych bibliotek jak i funkcji wbudowanych w GPU. Jeśli ktoś nie daje Ci za darmo takiej karty to jej nie bierz. Brak m.in obsługi OpenCL i DirectCompute!
  • Radeon HD seria 4xxx to układy pełne paradoksów. W karty wbudowano pamięć LDS – Local Data Store. NVidia coś podobnego (Shared Memory) miała już w pierwszych układach GeForce 8! Najciekawsze jest jednak to że Brook++ z racji swojej specyfikacji nie jest w stanie wykorzystać odpowiednio LDS, OpenCL mimo iż obsługiwany przez serie 4xxx to LDS nie jest zgodny z OpenCL i nie jest w ogóle wykorzystywany. DirectCompute 4.0, który teoretycznie jest kompatybilny z LDS nie jest jeszcze dostępny bo AMD nie przygotowała odpowiednich sterowników. Oprócz LDS seria 4xxx posiada pamięć GDS – Global Data Storage, która w zasadzie też nie może być wykorzystana, ponieważ Radeon 4xxx nie potrafi odpowiednio zsynchronizować dostępu do niej. Łącznie 176KB bardzo szybkiej pamięci wbudowanej wewnątrz układu nie może być wykorzystane. Brawo AMD!
  • Radeon HD seria 5xxx, w końcu układ z  bardziej zaawansowaną pamięcią LDS (pełny odczyt i zapis przez każdy wątek z grupy), obsługą operacji atomowych, obsługą konkretnego API do programowania GPU (OpenCL i DirectCompute). Tylko dlaczego dopiero teraz?
  • Jeśli chodzi o NVidię to tutaj każdy układ zgodny z CUDA jest sensowny. Każdy z tych układów dzięki nowszym sterownikom obsłuży także OpenCL i DirectCompute włącznie z wprowadzonym kilka lat temu do sprzedaży GeForcem 8800! Ja jednak polecam zaopatrzyć się w nieco nowszy układ (coś na bazie G92 albo najlepiej GT200), który posiada obsługę instrukcji atomowych.

Wrócę jeszcze na chwilę do interfejsów programowania. Myślę że można tutaj przyjąć następujący algorytm wyboru:

  • Rozszerzanie gry w DirectX o zaawansowane efekty: DirectCompute
  • Rozszerzanie gry w OpenGL o zaawansowane efekty: OpenCL
  • Wykorzystanie GPU do obliczeń innych niż graficzne: OpenCL
  • Specjalistyczne zadania niewymagające wspierania wszystkich producentów GPU: Najnowszy układ GeForce/Quadro/Tesla, C for CUDA lub ewentualnie PTX gdy konieczna jest maksymalna optymalizacja.

Na koniec pozostała jeszcze jedna ważna sprawa. Jak bardzo obecne GPGPU jest elastyczne, jakiego typu programy można uruchamiać na kartach graficznych?

  1. Obecnie żaden dotychczasowy program pisany z myślą o CPU nie może być bezpośrednio przeniesiony na GPU bez jakiejkolwiek modyfikacji kodu. Chodzi mi tutaj o programy pisane w C++, C# i innych tego typu językach.
  2. Algorytm przenoszony na GPU musi być wielowątkowy. Chodzi tutaj o rząd kilku tysięcy wątków. Co więcej każdy z tych wątków musi przetwarzać ten sam kod więc mamy tutaj do czynienia wyłącznie ze współbieżnością na poziomie danych. Oczywiście każdy wątek ma swój kontekst i wie czy powinien przetwarzać tą czy tą gałęź kodu (w przypadku pętli i instrukcji warunkowych). To ograniczenie będzie częściowo zniesione w nowym GPU NVidii „Fermi”, który zadebiutuje w przyszłym roku. Teoretycznie każdy multiprocesor będzie mógł wykonywać inny kernel (tak popularnie nazywana jest funkcja wywołana na GPU) co oznacza kilka-kilkanaście niezależnych grup wątków wykonujących inny kod.
  3. Z poziomu GPU program nie ma dostępu do pamięci głównej komputera, nie ma dostępu do urządzeń wejścia/wyjścia komputera. Część programu działająca po stronie CPU musi wysłać dane do urządzenia, uruchomić funkcję na tym urządzeniu i pobrać wyniki z urządzenia. Jeśli funkcja na CPU uzna że musi uaktualnić dane to funkcja na GPU musi zostać przerwana, nowe dane muszą zostać dostarczone i funkcja musi być ponownie uruchomiona.
  4. GPU nie obsługuje rekurencji, każdy algorytm musi być zapisany w wersji iteracyjnej.
  5. GPU nie obsługuje zaawansowanych sposobów synchronizacji wątków, mimo wszystko operacje atomowe (choć niedostępne w niektórych GPU) i bariery na grupy wątków wystarczają do zbudowania efektywnej współpracy pomiędzy wątkami. Wątki w grupie mogą odczytywać i zapisywać wspólną pamięć dzieloną. Wszystkie wątki na urządzeniu mogą odczytywać i zapisywać wspólną pamięć globalną.
  6. GPU nie posiada zaawansowanego mechanizmu cache (chociaż wspomniany wcześniej układ „Fermi” ma taki cache posiadać). Obecnie trzeba sobie pomóc trzema elementami dającymi namiastkę cache: odczyt/zapis tabel dwuwymiarowych ze specjalnym tylko do tego dedykowanym małym cache; odczyt z pamięci stałej (pamięć globalna oznaczona jako nie zmieniana podczas pojedynczego wywołania kernela), do tego też jest specjalny cache; stworzenie ręcznie zarządzanego ogólnego cache poprzez wykorzystanie wbudowanej pamięci dzielonej dla grupy wątków.

Przykładów wykorzystania GPU można szukać np. na stronie NVidii. Ja dodam że istnieje możliwość przeniesienia sztucznej inteligencji Paper Balla na GPU, robiłem już małe testy 😉

Post wyszedł dość długi a ja nie napisałem wszystkiego czego chciałem. Mimo wszystko na razie na tym zakończę, a do tematu wrócę w którymś z kolejnych mini artykułów.

Trójkąt w DirectX 11

Październik 27th, 2009 Marcin Borowiec

Chwilowo odbiegnę od głównego tematu mojej strony czyli programowania współbieżnego. Niedawno potrzebowałem przygotować małą aplikacje z wykorzystaniem DirectX. Ponieważ nie miałem narzuconej wersji, postanowiłem sprawdzić najnowszy DirectX 11. O ile do wersji 10 w DirectX SDK znajduje się dość konkretny tutorial, o tyle do jedenastki mamy pusty projekt albo kilka konkretniejszych aplikacji. To czego ja potrzebowałem to szablon aplikacji wyświetlającej zwykły trójkąt. Postanowiłem że po prostu przerobie szybko aplikacje z lekcji 2 i 3 tutoriala do DirectX 10. Okazało się że sprawa nie jestem całkiem prosta więc stwierdziłem że podzielę się tutaj napotkanymi przeze mnie problemami i ich rozwiązaniami.

Wszystkie testy przeprowadzałem na Windows 7 x64 i DirectX SDK August 2009. DirectX 11 będzie dostępny na systemie Windows Vista za pomocą platform update. Nie sprawdzałem postępu prac nad platform update i nie jestem w stanie powiedzieć czy aktualna wersja pozwoli uruchomić aplikacje DirectX 11.

Przerabiając aplikacje wyświetlającą trójkąt z dziesiątki na jedenastke trzeba zdać sobie sprawę z kilku rzeczy:

  • DirectX 11 wprowadza nowy model programowania wielowątkowego. W tym celu interfejs ID3D11Device został rozdzielony na dwa niezależne interfejsy ID3D11Device i i ID3D11DeviceContext. Dzięki temu drugiemu możliwe jest generowanie komend renderowania grafiki z wielu wątków.
  • D3DXMath został zastąpiony przez XnaMath. Zamiast np. D3DXVECTOR3 można użyć XMFLOAT3. XnaMath dostępne jest po dołączeniu xnamath.h
  • Effects są teraz dostępne jako projekt vc++ w katalogu DX SDK\Utilities\Source\Effects11. Należy je skompilować, najlepiej od razu wszystkie 4 warianty (x86 – x64, Debug – Release) aby później dołączyć header i plik lib do programu wyświetlającego trójkąt.
  • Nie istnieje funkcja do jednoczesnej kompilacji kodu hlsl i stworzenia efektu. Teraz należy najpierw skompilować shadery a później utworzyć effecty.
  • Kompilator shaderów obsługuje tylko format fx_5_0. Aby stworzyć kod zgodny z kartami DirectX 10 należy w definicji techniki przekazać odpowiedni parametr, np vs_4_0, ps_4_0.

To by było w zasadzie wszystko co jest potrzebne do wyświetlenia trójkąta w DirectX gdyby nie pewien mały problem. Przy obecnej wersji Effects w DX SDK  nie jest możliwe poprawne uruchomienie takiej aplikacji na karcie zgodnej z co najwyżej z DX10.1 (Z tego co mi wiadomo na Radeonach HD5xxx wszystko chodzi bardzo ładnie 😉 ) Problem jest opisany tutaj. Podane są tam dwa przykładowe rozwiązania. W skrócie podczas wywołania D3DX11CreateEffectFromMemory wywoływane są funkcje Create…Shader interfejsu ID3D11Device. Jednym z parametrów tych funkcji jest ID3D11ClassLinkage *pClassLinkage. Parametr ten w zależność od levelu: D3D_FEATURE_LEVEL_11_0 lub inny powinien zawierać poprawny wskaźnik lub być wartością NULL. Osobiście aby u siebie uzyskać działający profil D3D_FEATURE_LEVEL_10_0 i D3D_FEATURE_LEVEL_10_1 zamieniłem linie 1272 i 1273 w pliku EffectNonRuntime.cpp w projekcie Effects11 z:

else if( FAILED( (m_pDevice->*(pShader->pVT->pCreateShader))((UINT *) pShader->pReflectionData->pBytecode, pShader->pReflectionData->BytecodeLength,m_pClassLinkage, &pShader->pD3DObject) ) )
                pShader->IsValid = FALSE;

na

else if( FAILED( (m_pDevice->*(pShader->pVT->pCreateShader))((UINT *) pShader->pReflectionData->pBytecode, pShader->pReflectionData->BytecodeLength, 0, &pShader->pD3DObject) ) )
               pShader->IsValid = FALSE;

Należy pamiętać że w ten sposób wyłączam level 11_0. Dla mnie to w tej chwili nie jest problemem ponieważ testuje ten program tylko u siebie na sprzęcie zgodnym z DX10. Jeśli jednak potrzebujemy mieć działające wszystkie profile, możemy np. dodać parametr do tej funkcji i przekazać jawnie na którym levelu operujemy i wtedy dodać ifa wywołującego CreateShader z classlinkage albo bez lub np próbować tworzyć shader najpierw z classlinkage a w razie nie powodzenia bez.

Na koniec załączam zip z projektem stworzonym  w Visual Studio 2010 beta2 wyświetlającym piękny żółty trójkąt. Program działa u mnie poprawnie na karcie AMD (ATI) zgodnej z DX10.1 i NVIDII zgodnej z DX10. W przypadku próby kompilacji załączonego kodu w poprzednich wersjach VS wystarczy stworzyć projekt win32 C++, zaimportować pliki .h i .cpp, dodać w pliku dx11.h po includach

#define nullptr 0

(nullptr to taki ładny null z VC++ 10), upewnić się że vs ma dostęp do odpowiednich headerów i libów i dodać linkerowi liby: d3d11.lib d3dx11d.lib D3DX11EffectsD.lib D3DCompiler.lib w przypadku trybu Debug i d3d11.lib d3dx11.lib D3DX11Effects.lib D3DCompiler.lib w przypadku trybu Release.

DX11Triangle (2978)

d3d11.lib
d3dx11d.lib
D3DX11EffectsD.lib
D3DCompiler.lib

Memory ordering cd.

Wrzesień 21st, 2009 Marcin Borowiec

Miesiąc temu poruszyłem problem „przestawiania operacji odczytu i zapisu” w procesorach z rodziny x86. Dzisiaj postaram się rozszerzyć ten temat na inne architektury. W tym wpisie będę odnosił się przede wszystkim do dokumentu Memory Barriers: a Hardware View for Software Hackers. Najbardziej istotny jest tutaj rozdział 7 i tabela 5 na stronie 16. Znajduje się tam porównanie różnych architektur pod kątem możliwych zamian kolejności operacji odczytu i zapisu. Nie zamierzam tutaj tłumaczyć całego rozdziału, skupię się tylko na kilku ważnych faktach:

  • Istnieją architektury ze zdecydowanie luźniejszym modelem pamięci niż ten w procesorach x86.
  • Niektóre architektury pozwalają na wybranie jednego z kilku możliwych modeli pamięci przez system operacyjny.
  • Niespójność widoku pamięci różnych wątków może wywodzić się nie tylko z istnienia write buffera ale także np z luźniejszego modelu synchronizacji pamięci cache.
  • Wykonanie instrukcji atomowej nie implikuje uzyskania „total order”
  • Do wymuszania zachowania kolejności określonych operacji służą instrukcje typu „memory barrier”. Architektury o luźniejszym modelu oferują przeważnie kilka różnych instrukcji tego typu do uzyskania różnego poziomu spójności widoków pamięci.

Ten kto obejrzał film z poprzedniego wpisu wie, że model pamięci x86 pozwala na bezpieczne publikowanie obiektu. Przez publikację obiektu należy rozumieć sytuację:

mov a, r1;
mov gotowe, 1;

W powyższych przykładach agotowe to zmienne w pamięci, r1 to rejestr procesora. Zakładamy, że obiekt gotowe przed rozpoczęciem wykonywania tego kodu zawierał wartość 0. Zakładamy też, że przypisanie wartości 1 do obiektu gotowe oznacza, że obiekt a zawiera już nową poprawną wartość i ta wartość może być odczytana przez inny wątek. W takim wypadku inny wątek programu może co jakiś czas odczytywać wartość gotowe aż odczyta wartość 1. Wtedy wie że obiekt a zawiera poprawną wartość i może ją odczytać. Niestety takie rozwiązanie nie zadziała np. na procesorach z rodziny Power PC. Dla nich proces publikacji powinien wyglądać tak:

mov a, r1;
lwsync; //memory barrier
mov gotowe, 1;

W następnym poście opiszę jak informacje zawarte w tym wpisie odnoszą się do publikowania obiektów w C++.