Trzecia kawa Szkolenie Scrum

Zapach świeżej kawy: Aparapi – Java na GPU!

OpenCLW poprzednim odcinku mini-cyklu „Zapach świeżej kawy” pisałem o ciekawym wynalazku jakim jest node.js – technologia pozwalająca na tworzenie aplikacji sieciowych (w tym web) za pomocą języka Javascript. Na końcu tego tekstu zapowiedziałem, że kolejny odcinek poświęcony będzie Play! framework. Postanowiłem jednak zmienić nieco plany – głównie ze względu na to, że w międzyczasie pojawiło się coś nowego, co bardzo mnie zainteresowało. Tym czymś jest: Aparapi. Cóż to takiego? W skrócie jest to translator bytecode’u JVM, który produkuje na wyjściu kod OpenCL, który może być wykonany na procesorze karty graficznej (GPU). Co to daje? Oczywiście chodzi o wydajność – odciążenie głównego procesora (CPU) i wykorzystanie faktu, że współczesne procesory graficzne wyposażone są w kilkadziesiąt lub kilkaset „rdzeni” obliczeniowych… które tylko czekają na nasz kod.

Ale po kolei. Programowanie generalnego przeznaczenia na kartach graficznych (GPGPU) to jeden z ciekawszych tematów ostatnich kilku lat. Dynamiczny rozwój sprzętu graficznego, napędzany głównie rynkiem gier komputerowych spowodował, że współczesne procesory graficzne w pewnych zastosowaniach są wielokrotnie wydajniejsze od układów CPU produkowanych przez Intela czy AMD. Dlaczego? Bo w odróżnieniu od „zwykłych” procesorów, które w najlepszym wypadku są kilku-rdzeniowe, karty graficzne od początku projektowane były jako masywanie-równoległe. Liczba jednostek obliczeniowych (takich mini-rdzeni) w najnowszych układach przekracza kilkaset, a rozmiar dostępnej pamięci mierzy się w gigabajtach! Jeśli więc tylko nasze „obliczenia” (czymkolwiek by nie były) dają się rozbić na wiele kawałków… to teoretycznie możemy uzyskać wielkie przyspieszenie.

Również w zakresie narzędzi pozwalających uruchamiać nasz kod na kartach graficznych nastąpiła wielka zmiana. Początkowo musieliśmy korzystać z zawiłych tricków polegających na kodowaniu danych do tekstur i uruchamianiu na nich programików pisanych w językach podobnych do asemblera, które wysyłane były do karty graficznej przez OpenGL lub Direct3D – czyli typowo graficzne biblioteki. Dzisiaj do dyspozycji mamy dojrzałe, specjalizowane języki (CUDA, OpenCL) wyposażone w szereg narzędzi (w tym debuggery, analizatory wydajności, edytor, testy).

Więcej o historii i rozwoju technik GPGPU można poczytać na wikipedii, na stronach NVidia, na tej prezentacji z Uniwersytetu Vrije w Amsterdamie lub na kolejnej prezentacji z Uniwersytetu Kalifornijskiego.

Z całej historii warto zapamiętać, że całkiem niedawno firma Apple rozpoczęła prace nad biblioteką/standardem OpenCL, którego celem jest zunifikowanie API i języka do programowania rożnych wielo-rdzeniowych urządzeń takich jak procesory graficzne czy konwencjonalne procesory wielordzeniowe. Dość szybko do prac nad rozwojem OpenCL dołączyło AMD, Intel, NVidia, ARM, IBM i inni. Obecnie specyfikacja jest w wersji 1.1 i dostępnych jest kilka implementacji: wersja na karty graficzne NVidia i AMD, oraz wersja na procesory od Intela i AMD. A także specjalistyczne wersje od IBM dla procesorów Power.

Programowanie w OpenCL polaga na zakodowaniu algorytmu w specjalnym języku zbliżonym składnią do C. Kod w postaci tekstowej jest kompilowany na wybrane urządzenie w momencie uruchomiania programu – biblioteka OpenCL może także zoptymalizować go pod kątem urządzenia obecnego akurat w systemie. Takie małe programiki z reguły nazywa się „kernel” (nie mylić z Linux Kernel :). Dodatkowo OpenCL definiuje API (zdefiniowane na poziome języka C, z dostępną wersją dla C++ i bindingami dla wielu innych języków), które pozwala manipulować zestawem kerneli, czyli uruchamiać je i odbierać wyniki. Wszystko to nie jest aż takie trudne, jednak pisząc program musimy od początku myśleć o możliwościach i ograniczeniach OpenCL i po prostu związać się z tym API. No i, co najważniejsze, fragmenty programu, które mają działać na GPU, musimy zakodować w specjalnym języku programowania.

I tu właśnie pojawia się (między innymi) Aparapi. Projekt zainicjowało kilka osób z firmy AMD, z bardzo „prostym” celem, aby pisząc programy na GPU nie trzeba było o tym pamiętać :) Innymi słowy – aby dało się oznaczyć jakoś fragmenty kodu napisane w zwykłym języku generalnego przeznaczenia (w przypadku Aparapi jest to Java) i po prostu uruchomić je na karcie graficznej na dużym zestawie danych. Aktualna implementacja Aparapi jest jeszcze w dość wczesnej fazie rozwoju, wobec czego nakłada sporo ograniczeń na metody, które można tak uruchamiać. Nie mniej jednak są już konkretne przykłady, które działają… i, co więcej, działają sporo szybciej niż na zwykłym procesorze.

Jak to wygląda w praktyce. Obecnie aby uruchomić kod na GPU musimy zawrzeć go w klasie dziedziczącej z klasy bazowej Kernel, w której musimy nadpisać abstrakcyjną metodę run (podobnie jak to ma miejsce w przypadku wątków w Javie). Poniżej minimalny przykład:

Taka metoda run odpowiada sekwencyjnemu podejściu ze zwykłą pętlą for:

z tą oczywistą różnicą, że w przypadku Aparapi kolejność operacji w ramach pętli nie będzie zachowana!

Aby uruchomić nasz kod w Aparapi wystarczy teraz stworzyć instancję klasy SquaresKernel i wywołać na niej metodę execute, podając jako parametr rozmiar tablicy numbers (w ogólności argumentem execute jest rozmiar problemu – czyli zakres wyników jakie będzie zwracać getGlobalId użyte w ramach run – nie musi to być koniecznie rozmiar którejkolwiek z tablic użytych w kodzie). Co się wtedy stanie? Kod metody run zostanie przetłumaczony na język OpenCL, razem z ogólną strukturą klasy kernel (pola numbers i result). Następnie za pomocą API OpenCL, zawartość tablicy numbers zostanie wysłana do karty graficznej, uruchomiony zostanie kod metody, a następnie wyniki zostaną skopiowane z powrotem do głównej pamięci operacyjnej dostępnej dla kodu Java. I to właściwie wszystko :)

Raczej rzadko kiedy będziemy chcieli używać karty graficznej do podnoszenia liczb do kwadratu. W oficjalnej dystrybucji Aparapi można znaleźć przykład wyliczający przybliżenie zbioru Mandelbrota lub implementujący model finansowy Blacka-Scholes, a także symulację rozwiązania problemu fizycznego „n-ciał” (n-body problem). Zapewne z czasem pojawi się więcej ciekawych przykładów. W świecie natywnego OpenCL można już znaleźć mnóstwo implementacji różnych mniej lub bardziej znanych algorytmów.

Od razy trzeba jednak uczciwie wspomnieć, że póki co na kartach graficznych nie można uruchamiać dowolnego kodu. Technologie takie jak ta nie nadają się do przetwarzania plików XML, lub wysyłania requestów przez web-services :) Mogą co prawda operować na dużych zestawach danych, ale nie wciągną ich same z bazy danych, no i raczej na wyniku nie wyplują nam kawałków strony HTML. Programowanie o jakim tu mowa najlepiej sprawdzi się we wszelakich operacjach numerycznych / algorytmicznych typu przetwarzanie obrazów, analiza szeregów czasowych, jakieś aproksymacje, kompresje, łamanie kodów, szyfrowanie, etc.

Co więcej Aparapi samo nakłada pewne ograniczenia. W ramach metody run i kodu z niej wykonywanego musimy oczywiście ograniczyć się do operacji na podstawowych operacji na podstawowych typach danych (większość typów wbudowanych, tablice typów wbudowanych, obiekty mające proste składowe i 1-wymiarowe tablice takich obiektów – nie ma innych kolekcji póki co). Póki co nie możemy też korzystać z konstruktorów ani metod/pól statycznych. Ponadto kod, który ma być transformowany na OpenCL musi być skompilowany razem z symbolami debug (javac -g).

Wydajność całego rozwiązania jest… różna. Dość kosztowane jest przenoszenie danych między pamięcią główną a pamięcią karty graficznej, a Aparapi robi to trochę za często, także w przypadku prostych kernel’i możemy nie uzyskać żadnego wzrostu wydajności lub wręcz pogorszyć wydajność, przez narzut kopiowania danych. Z drugiej strony już teraz są dostępne przykłady, które pokazują zyski – poniżej filmik porównujący wydajność prostego przykładu rysującego fraktal Mandelbrota:


Reasumując sprawa jest przyszłościowa – jest jeszcze wiele do zrobienia: wyeliminowanie istniejących ograniczeń (lub ich zminimalizowanie) a także przeprojektowanie API, abyśmy nie musieli się z nim tak silnie wiązać (może na przykład zamiast dziedziczenia z Kernel powinna być dostępna adnotacja @Kernel do oznaczania metod). Nie mniej jednak już teraz Aparapi ma spore możliwości, a w przyszłości będzie jeszcze lepiej.

Nie wiadomo więc jak potoczy się przyszłość? Być może podobne koncepcje rozwiną się na tyle, że zostaną wbudowane w runtime Javy, .NET lub podobnych języków i automatyczne będą wykrywać kod, który można przepisać na GPU – tak, że w ogóle nie będziemy się musieli nad tym zastanawiać? A może będzie jeszcze lepiej i procesory GPU rozwiną się na tyle, że będą mogły wykonywać dowolny kod JVM/CLR? Kto wie… Póki co mamy OpenCL jeśli chcemy super wydajności kosztem dodatkowych nakładów na dostsowanie programu – oraz mamy Aparapi, które zmniejsza nieco wymagane nakłady oferując nieco niższą wydajność.

Z ciekawostek – w przypadku gdy Aparapi stwierdzi, że dana metoda nie może być przepisana na OpenCL, uruchomi ją współbieżnie na głównym procesorze korzystając ze standardowej puli wątków Java Thread Pool. Także tak czy inaczej możemy zyskać. Ciekawym doświadczeniem byłoby rozwinięcie tego o elementy Java 7 – czyli fork/join framework.

I na koniec – Aparapi jest open source i działa już właściwie na każdej implementacji OpenCL. Także niezależnie czy masz kartę AMD czy NVidia, czy w ogóle nie masz potężnego GPU (niestety karty od Intela nie są wspierane, gdyż Intel nie wypuścił jeszcze sterowników OpenCL dla swoich kart – ma jedynie swoje OpenCL dla procesorów) możesz pobawić się z Aparapi. Zachęcam do eksperymentów, zgłaszania błędów/problemów i ich naprawiania, oraz ogólnie do zaangażowania w projekt. Ciekawym pomysłem byłoby również przeniesienie koncepcji Aparapi do innych środowisk – na przykład .NET (dla .NET jest już co prawda podobne rozwiązanie, ale całkowicie zamknięte i w dodatku płatne).

Jeśli kogoś z Was zainteresował ten temat… zapraszam do kontaktu!

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *