Zunifikowany interfejs programistyczny dla środowisk

Transkrypt

Zunifikowany interfejs programistyczny dla środowisk
Rok akademicki 2013/2014
Politechnika Warszawska
Wydział Elektroniki i Technik Informacyjnych
Instytut Informatyki
PRACA DYPLOMOWA MAGISTERSKA
Piotr Tomasz Monarski
Zunifikowany interfejs programistyczny dla środowisk
heterogenicznych i homogenicznych w języku Python
dla operacji algebraicznych na macierzach.
Opiekun pracy
mgr inż. Rajmund Kożuszek
Ocena: .....................................................
.................................................................
Podpis Przewodniczącego
Komisji Egzaminu Dyplomowego
Kierunek:
Informatyka
Specjalność:
Inżynieria Systemów Informatycznych
Data urodzenia:
1988.02.11
Data rozpoczęcia studiów:
2011.02.21
Życiorys
Urodziłem się 11 lutego 1988 roku w Warszawie. Od najmłodszych lat byłem zafascynowany
komputerami. Przygodę z programowaniem rozpocząłem od budowania prostych stron
internetowych jeszcze przed rozpoczęciem szkoły średniej. W 2007 roku ukończyłem klasę
matematyczno-fizyczno-informatyczną w Liceum Ogólnokształcącym im. Marszałka Józefa
Piłsudskiego w Garwolinie, gdzie poznałem język programowania C++. Studia na kierunku
Informatycznym wydziału Matematyki i Nauk Informacyjnych rozpocząłem w roku 2007
Studia te poszerzyły moje informatyczne horyzonty. Poza rozwojem naukowym byłem
aktywnym samorządowcem w Samorządzie Studentów Wydziału MiNI oraz starostą grupy
T1. W ramach pracy inżynierskiej zaimplementowałem grę logiczną Blokus wraz z licznymi
algorytmami sztucznej inteligencji w oparciu o nowości platformy .NET 4.0. Celem dalszego
pogłębienia wiedzy rozpocząłem w 2011 r. studia magisterskie na kierunku Inżynierii
Systemów Informatycznych na Wydziale Elektroniki i Technik Informacyjnych Politechniki
Warszawskiej. W roku 2013 skorzystałem z programu wymiany studentów Erasmus, w
ramach którego odbyłem semestr na University of Surrey w Wielkiej Brytanii.
.......................................................
Podpis studenta
EGZAMIN DYPLOMOWY
Złożył egzamin dyplomowy w dniu ..................................................................................2014 r
z wynikiem ...................................................................................................................................
Ogólny wynik studiów: ................................................................................................................
Dodatkowe wnioski i uwagi Komisji: ..........................................................................................
.......................................................................................................................................................
.......................................................................................................................................................
STRESZCZENIE
Języki skryptowe pozwalają na wydajne tworzenie programów niezależnych od środowiska
wykonawczego. Wadą takiego rozwiązania jest niedostateczna wydajność operacji
algebraicznych na dużych zbiorach danych. Technologią, która zapewnia znaczną moc
obliczeniową dla tej grupy problemów jest programowanie na kartach graficznych. Takie
rozwiązanie również ma wady takie jak wysoki koszt implementacji oraz ograniczona ilość
komputerów wyposażonych w odpowiednią kartę graficzną.
Wyeliminowanie wad obu technologi w kontekście operacji algebraicznych na dużych
zbiorach danych jest istotą niniejszej pracy . W tym celu została stworzona biblioteka łącząca
język Python z możliwościami obliczeniowymi kart graficznych. Efekt końcowy tego
połączania został omówiony na bazie wyników uzyskanych z wykorzystaniem dwóch
popularnych algorytmów: wstecznej propagacji błędu oraz k-najbliższych sąsiadów.
Uzyskane wyniki dowodzą skuteczności takiego rozwiązania bazującego na obu
technologiach.
Słowa kluczowe: python, cuda, gpgpu, obliczenia równoległe, macierz, przetwarzanie
heterogeniczne
Python unified programming interface for heterogenic and homogenic
environment for algebraic operations on matrices.
Script languages allow efficient programing for independent platforms. The significant
problem with scripting is insufficient performance for algebraic operations on large data sets
(big-data). General Purpose computing on Graphics Processing Units is a technology, which
offers great computation power for such operations. This technology has two major cons. The
first one is high implementation cost and a second one is limited group of computers equipped
with a proper dedicated graphic card.
This thesis aims to bypass limitations of both technologies of algebraic operation on large
data sets. To this end, I created library combining script languages and computation power of
Graphical Processing Units. The final result are determined by performance of two
algorithms: k-nearest neighbors and back propagation algorithms. The thesis concludes that
combination of script languages with GPU power can overcome limitation of both
technologies creating very efficient tool.
Keywords: python, cuda, matrix, gpgpu, heterogenic computation, parallel computation
Spis treści
1
2
3
4
Wprowadzenie
3
1.1
J˛ezyki skryptowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
1.2
Procesory graficzne ogólnego zastosowania . . . . . . . . . . . . . . .
7
1.2.1
8
GPGPU na przykładzie CUDA . . . . . . . . . . . . . . . . . .
Projekt
12
2.1
Założenia projektu . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2
Architektura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3
Interfejs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.4
Oczekiwania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Realizacja
16
3.1
Dost˛epne narz˛edzia . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.2
Przebieg realizacji . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Implementacja
22
4.1
Architektura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.2
Inteligentne wskaźniki . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.3
Leniwa ewaluacja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.4
Operowanie na fragmentach macierzy . . . . . . . . . . . . . . . . . . 25
4.5
Komponenty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.5.1
Matrix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.5.2
Environment . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5
DataContainerFactory . . . . . . . . . . . . . . . . . . . . . . 27
4.5.4
DataContainer . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.5.5
DataCrawler . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.5.6
DataCrawlerFactory . . . . . . . . . . . . . . . . . . . . . . . 30
4.5.7
Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
4.5.8
ThrustIterator . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.5.9
Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.6
Adapter dla j˛ezyka Python . . . . . . . . . . . . . . . . . . . . . . . . 33
4.7
Proces optymalizacji . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Zastosowanie w praktyce
37
5.1
Kontekst . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
5.2
Procedura testowa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
5.3
Sieci neuronowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
5.4
6
4.5.3
5.3.1
Wyniki . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
5.3.2
Omówienie wyników . . . . . . . . . . . . . . . . . . . . . . . 46
K-najbliższych sasiadów
˛
. . . . . . . . . . . . . . . . . . . . . . . . . 48
5.4.1
Porównanie mtx i mtx_op . . . . . . . . . . . . . . . . . . . . 48
5.4.2
Wyniki . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.4.3
Omówienie wyników . . . . . . . . . . . . . . . . . . . . . . . 55
Zakończenie
57
6.1
Wnioski z przetwarzania danych . . . . . . . . . . . . . . . . . . . . . 57
6.2
Wnioski wynikajace
˛ z realizacji projektu . . . . . . . . . . . . . . . . . 58
6.3
Perspektywy rozwoju . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Bibliografia
61
Rozdział 1
Wprowadzenie
Wprowadzenie dwurdzeniowych procesorów w 2005 roku przez firm˛e Intel, rozpocz˛eło
er˛e przetwarzania równoległego na komputerach osobistych. Ten pierwszy dwurdzeniowy procesor umożliwił rozpowszechnienie technik wykorzystywanych dotychczas
tylko w klastrach obliczeniowych czy superkomputerach. Kluczowy krok w rozwoju
tego typu przetwarzania na komputerach osobistych nastapił
˛ dwa lata później. W 2007
roku umożliwiono wykorzystanie setek rdzeni dost˛epnych w procesorach graficznych do
obliczeń ogólnego zastosowania. Stworzono w ten sposób nowe możliwości zwiazane
˛
z przetwarzaniem na maszynach heterogenicznych, które sa˛ poznawane po dziś dzień.
Narz˛edzia udost˛epnione w 2007 roku umożliwiły programowanie w środowiskach
heterogenicznych, wykorzystujacych
˛
do obliczeń różne architektury jednostek obliczeniowych (procesorów). Mimo dynamicznego rozwoju tych narz˛edzi programiści
preferuja˛ środowiska homogeniczne wykorzystujace
˛ jeden typu procesora (np. x86).
Na taki stan rzeczy nakłada si˛e znaczny koszt napisania programu dla środowisk heterogenicznych w niskopoziomowych interfejsach programistycznych. Fakt dużego
zróżnicowania dost˛epnych konfiguracji sprz˛etowych również jest nie bez znaczenia.
Uwzgl˛ednienie każdej konfiguracji wymaga dopasowania kodu do każdej z nich lub
ograniczenie dopuszczalnej bazy sprz˛etowej.
Wprowadzenie
4
J˛ezyki skryptowe sa˛ coraz cz˛eściej wybieranym narz˛edziem do rozwiazywania
˛
szerokiej
gamy problemów. Wszechstronność j˛ezyków skryptowych umożliwia znaczne skrócenie
czasu wytwarzania oprogramowania kosztem dodatkowego nakładu obliczeniowego.
Przyczyna˛ tej popularności jest dost˛epność coraz szybszych procesorów centralnych
(CPU). Pomimo swojej wszechstronności, j˛ezyki skryptowe nie znajduja˛ zastosowania
do rozwiazywania
˛
problemów numerycznych na dużych zbiorach danych ze wzgl˛edu na
niedostateczna˛ wydajność takiego rozwiazania.
˛
Połaczenie
˛
obu technologii w kontekście operacji numerycznych na dużych zbiorach
danych może przyczynić si˛e do zniwelowania wad (ograniczeń) obu technologii. Czy
takie połaczenie
˛
faktycznie pozwoli na wykorzystanie j˛ezyków skryptowych do tego
typu problemów? Czy możliwe b˛edzie zachowanie niezależności skryptu od środowiska
wykonawczego? Jak zrealizować takie połaczenie
˛
w oparciu o dost˛epne narz˛edzia? Czy
uprości to programowanie na kartach graficznych?
Udziel˛e odpowiedzi na te pytania poprzez projekt majacy
˛ na celu stworzenie biblioteki
implementujacej
˛ operacje na macierzach w obu środowiskach wykonawczych przy
zachowaniu spójnego interfejsu programistycznego.
1.1
J˛ezyki skryptowe
J˛ezyki skryptowe w odróżnieniu od j˛ezyków kompilowanych do kodu maszynowego
(C/C++) określanych również jako j˛ezyki systemowe, wymagaja˛ do działania dodatkowej aplikacji zwanej interpreterem. Interpreter jest programem przetwarzajacym
˛
skrypt,
czyli program napisany w j˛ezyku skryptowym i dopiero na jego podstawie interpreter
wykonuje odpowiednie operacje w docelowym środowisku. Tego typu j˛ezyki zostały
stworzone z myśla˛ o zautomatyzowaniu powtarzalnych czynności i usprawnieniu administracji systemami operacyjnymi. Pierwszym j˛ezykiem skryptowym był JCL (Job Control
Language) z lat 60-tych wykorzystywany do sekwencjonowania zadań w maszynach dla
systemu OS/360. JCL odbiega od współczesnych standardów j˛ezyków skryptowych ze
Wprowadzenie
5
wzgl˛edu na operowanie w innym środowisku wykonawczym. Prawdziwym protoplasta˛
dzisiejszych j˛ezyków skryptowych jest Thomson Shell (sh) napisany w 1971 roku jako
pierwsza implementacja powłoki systemu Unix (Unix Shell). Stworzony w celu ułatwienia zarzadzania
˛
i administrowania systemem operacyjnym, umożliwia wywoływanie
innych programów, proste operacje warunkowe oraz funkcje zwiazane
˛
z podstawowym
przetwarzaniem tekstu. Dodatkowo Thomson Shell jest cz˛esto wykorzystywany do
tworzenia nowych programów poprzez kompozycj˛e już istniejacych.
˛
J˛ezyk
Ekspresywność wyrażenia
Ekspresywność linii kodu
C
1
1
C++
2.5
1
Java
2.5
1.5
Python
6
6.5
Tablica 1.1: Ekspresywności j˛ezyków w odniesieniu do j˛ezyka C.
Z czasem j˛ezyki skryptowe znajdowały coraz szersze zastosowanie od generowania
interfejsów graficznych (TCL), po rozszerzenie funkcjonalności licznych programów
biurowych (Visual Basic). W latach 90-tych różnica w funkcjonalności mi˛edzy j˛ezykami
skryptowymi, a systemowymi była znaczna. Pierwsza grupa umożliwiała wywoływanie
innych programów oraz proste przetwarzanie tekstu. Druga grupa reprezentowana przez
j˛ezyki C/C++ umożliwiała rozwiazywanie
˛
dowolnych problemów. Celem uzupełniania
tej luki, w roku 1989 rozpocz˛eto prace nad uniwersalnym j˛ezykiem skryptowym Python.
Pierwsza wersja 0.9.0, opublikowana w 1991 roku, umożliwiała operacje na prostych
kolekcjach danych przybliżajac
˛ tym samym j˛ezyki skryptowe do systemowych. Wersj˛e
1.0 wydano trzy lata później. Dostarczyła ona szeroki wachlarz narz˛edzi umożliwiajacy
˛
programowanie w ramach dowolnego paradygmatu programowania.
Korzystanie z interpretera w miejscu kompilatora i kodu maszynowego umożliwia operowanie na wyższym poziomie abstrakcji, cz˛esto utożsamianym z ekspresywnościa˛ j˛ezyka.
Opisuje ona średnia˛ liczb˛e instrukcji maszynowych przypadajacych
˛
na jedno wyrażenie
w danym j˛ezyku. Ta zależność oznacza w praktyce, że j˛ezyki bardziej ekspresywne
Wprowadzenie
6
umożliwiaja˛ opisanie tej samej funkcjonalności krótszym kodem. Tabela 1.1 przedstawia
ekspresywność j˛ezyków ze wzgl˛edu na pojedyncze wyrażenie oraz pojedyncza˛ lini˛e
kodu w porównaniu do j˛ezyka C.
Realizacja wysokiego poziomu abstrakcji została osiagni˛
˛ eta przez wykorzystanie dynamicznych zmiennych, automatycznego zwalniania pami˛eci oraz możliwości dynamicznej
modyfikacji wykonywanego kodu. W j˛ezykach skryptowych możliwe jest odwoływanie si˛e do różnych typów obiektów za pomoca˛ jednej zmiennej dopóki poszczególne
obiekty zapewniaja˛ implementacj˛e wywoływanych funkcji w kodzie programu. W przypadku braku wymaganej funkcjonalności interpreter informuje o bł˛edzie. Automatyczne
oczyszczanie pami˛eci usprawnia proces programowania ze wzgl˛edu brak konieczności
r˛ecznego zwalniania pami˛eci. Obiekty, do których nie odwołuje si˛e już żadna zmienna
sa˛ automatycznie usuwane zapobiegajac
˛ w ten sposób wyciekom pami˛eci. Elementem
wpływajacym
˛
również na ekspresywność j˛ezyka jest jego zgodność z paradygmatem
metaprogramowania, czyli dynamicznego tworzenia programów poprzez generowanie
i modyfikacj˛e wykonywanego kodu. Wymienione właściwości j˛ezyków skryptowych,
na przykładzie j˛ezyka Python, sa˛ ich głównymi zaletami, a zarazem wadami.
Dynamiczne typy danych, automatyczne odśmiecanie oraz przetwarzanie z wykorzystaniem interpretera sa˛ kosztowne obliczeniowo, co przez wiele lat ograniczało wykorzystanie j˛ezyków skryptowych. Były one głównie używane na potrzeby skryptów
administracyjnych, programów generujacych
˛
strony internetowe czy jako narz˛edzia
do prototypowania docelowych rozwiazań.
˛
Dzi˛eki rozwojowi techniki to ograniczenie
przestało być problemem i coraz wi˛ecej aplikacji użytkowych pisanych jest dziś za pomoca˛ j˛ezyków skryptowych. Poza dodatkowym nakładem obliczeniowym dynamiczny
charakter zmiennych ma jeszcze jedna˛ wad˛e: jest nia˛ brak możliwości sprawdzenia
poprawności działania programu inaczej niż w momencie wychwycenia bł˛edów podczas
wykonania programu. Jest to wada wykluczajaca
˛ wykorzystanie j˛ezyków skryptowych
w aplikacjach czułych na bł˛edy i wymagajacych
˛
nieprzerwanego funkcjonowania.
Wprowadzenie
1.2
7
Procesory graficzne ogólnego zastosowania
Technologia umożliwiajaca
˛ wykorzystanie tysi˛ecy rdzeni do obliczeń ogólnego zastosowania ma swój poczatek
˛ wraz ze stworzeniem procesora graficznego (GPU - Graphics
Processing Unit) w latach 80-tych. Firma Intel jako pierwsza wyprodukowała kontroler
(SBX 275 Video Graphics Controller Multimodule Board), który umożliwiał sprz˛etowe
wsparcie dla przetwarzania grafiki 2D. Kolejny krok został postawiony przez firm˛e
Matrox w 1995 roku wprowadzajac
˛ a˛ wsparcie dla grafiki 3D.
Przetwarzanie i renderowanie obrazu charakteryzuje si˛e wykonywaniem identycznych
operacji na każdym elemencie zbioru danych (SIMD - Single Instruction Multiple Data)
głównie reprezentowanych jako wektory lub macierze. Specyfika tego problemu przyczyniła si˛e do powstania wyspecjalizowanych procesorów wykonujacych
˛
równolegle
identyczny zbiór instrukcji na różnych danych. Pierwsze procesory graficzne wspierały
proste, zdefiniowane wcześniej operacje takie jak wypełnianie wielokatów.
˛
Na przełomie lat 90-tych i poczatku
˛
nowego milenium rozwój techniki pozwolił na pisanie
własnych programów operujacych
˛
na każdym elemencie przetwarzanych danych w
układzie cieniowania wierzchołków (vertex-shader) czy w układzie cieniowania pikseli
(pixel-shader).
Możliwości równoległego wykonania prostego programu operujacego
˛
na dużych zbiorach danych przy niewielkim nakładzie finansowym było pokusa˛ nie do odparcia dla
środowisk naukowych. Superkomputery oraz klastry obliczeniowe sa˛ po dziś dzień
drogim rozwiazaniem
˛
w porównaniu do kosztu zakupu pojedynczej karty graficznej,
która umożliwia znaczace
˛ skrócenie czasu potrzebnego do rozwiazania
˛
szerokiej gamy
problemów w kontekście architektury SIMD. Środowiska naukowe nie zostały zniech˛econe ograniczeniami zwiazanymi
˛
z dost˛epem do wyników obliczeń wykonanych na
kartach graficznych. Projektowane wtedy karty graficzne nie zakładały dwukierunkowej
komunikacji z procesorem centralnym.
Zaangażowanie środowisk naukowych nie pozostało bez echa, potencjał kart graficznych
Wprowadzenie
8
został dostrzeżony przez producentów procesorów graficznych, którzy rozpocz˛eli prace
nad odpowiednia˛ technologia.˛ Wykorzystywanie ich do obliczeń niezwiazanych
˛
z renderowaniem grafiki zostało opisane nowym terminem jakim jest Obliczenia Ogólnego
Zastosowania na Procesorach Graficznych (GPGPU - General Purpose computing on
Graphics Processing Units). Pierwsze procesory graficzne stworzone w myśl tej idei
ujrzały światło dzienne dopiero w lutym 2007 roku.
Pionierem w tej dziedzinie jest firma Nvidia, która jako pierwsza zaprezentowała środowisko deweloperskie CUDA (Compute Unified Device Architecture) [18], które wraz
z procesorem graficznym G80 w pełni urzeczywistniło id˛e obliczeń ogólnego zastosowania z wykorzystaniem kart graficznych. Konkurencyjna firma AMD wprowadziła
odpowiednia˛ technologi˛e w grudniu tego samego roku pod nazwa˛ Steam Computing,
a już w styczniu 2008 roku zostało założone konsorcjum i opracowany otwarty standard
opisujacy
˛ GPGPU o nazwie OpenCL [20].
Kolejne lata min˛eły pod znakiem rewolucji obliczeniowej, która moc klastrów obliczeniowych oraz superkomputerów wprowadziła do komputerów osobistych. Obecnie
do nowej technologii dostosowywane sa˛ liczne programy. Dzi˛eki temu przetwarzanie
dużych zbiorów danych, takich jak obraz, dźwi˛ek, grafika, może być przyśpieszone
nawet setki razy.
1.2.1
GPGPU na przykładzie CUDA
Wykorzystanie procesora graficznego do obliczeń ogólnego zastosowania różni si˛e od
standardowego podejścia z wykorzystaniem tylko jednostki centralnej (CPU). Różnice
postaram si˛e opisać na przykładzie technologii CUDA.
Procesory centralne projektowane sa˛ pod katem
˛
zminimalizowania opóźnień w dost˛epie
do danych, możliwości wykonywania procesów poza kolejnościa˛ oraz optymalizacji
kolejności ich wykonania. Karty graficzne maja˛ zapewnić maksymalna˛ wydajność przy
przetwarzaniu danych dedykujac
˛ każdemu procesowi osobna˛ fizyczna˛ jednostk˛e wy-
Wprowadzenie
9
CPU
GPU
Diagram 1.1: Różnica w budowie CPU i GPU
konawcza.˛ Innymi słowy procesory sa˛ projektowane do interaktywnych dynamicznych
operacji, natomiast karty graficzne działaja˛ w sposób wsadowy optymalizujac
˛ czas
wykonania danego zadania poprzez jego zrównoleglenie.
Istotnym elementem rozróżniajacym
˛
oba procesory jest organizacja pami˛eci. W procesorach centralnych z rodziny x86 możemy wyróżnić pami˛eć typu L1, L2, L3, pami˛eć
operacyjna˛ i dysk twardy. Pami˛eci L1, L2 i L3 sa˛ pami˛eciami podr˛ecznymi procesora
przeznaczonymi odpowiednio to przechowywania instrukcji wykonywanych programów,
danych na których operuja˛ oraz do komunikacji mi˛edzy poszczególnymi rdzeniami.
Pami˛eć podr˛eczna tego typu nie jest zarzadzana
˛
przez programist˛e a poprzez specjalny
kontroler w procesorze i w nieznacznym stopniu przez kompilatory. Dopiero pami˛eć
operacyjna i dane na dysku twardym sa˛ bezpośrednio adresowane przez programist˛e.
Charakterystyka poszczególnych rodzajów pami˛eci została przedstawiona w tabeli 1.2.
Typ
Pojemność
Czas dost˛epu
Rejestry
32 B - 64 B
< 1 ns
L1
≤ 128 KB
1 ns
L2
≤ 4 MB
1-2 ns
L3
≤ 144 MB
2-5 ns
Pami˛eć operacyjna 256 MB.. 32GB
Dysk twardy
> 60 GB
10-50 ns
> 10 ms
Tablica 1.2: Charakterystyka pami˛eci dla procesora centralnego
Wprowadzenie
10
Diagram 1.2: Schemat przetwarzania z wykorzystaniem GPGPU
Technologia CUDA wprowadza bardziej rozbudowana˛ struktur˛e pami˛eci, w ramach
której wyróżniamy: pami˛eć globalna˛ (global memory), pami˛eć tekstur (texture memory),
pami˛eć stałych (constant memory), pami˛eć współdzielona˛ (shared memory) i pami˛eć
lokalna˛ (local memory). Pami˛eć operacyjna i dyski twarde nie sa˛ dost˛epne dla procesora
graficznego. Dost˛ep do nich kontrolowany jest przez procesor centralny (CPU) za pośrednictwem specjalnej magistrali (diagram 1.2). Procesor graficzny może czytać i zapisywać
dane do wszystkich rodzajów pami˛eci z wyjatkiem
˛
pami˛eci tekstur i stałych, z których
możliwy jest tylko odczyt, co umożliwia skuteczniejsza˛ optymalizacj˛e tej operacji.
Charakterystyka poszczególnych typów pami˛eci została zebrana w tablicy 1.3. Dane
wczytane z pami˛eci stałych lub tekstur sa˛ automatycznie przechowywane w pami˛eci
podr˛ecznej, której rol˛e pełnia˛ rejestry. Nowsze wersje środowiska CUDA umożliwiaja˛
również przechowywanie w rejestrach danych wczytanych zarówno z pami˛eci lokalnej
jak i pami˛eci globalnej.
Schemat przetwarzania z wykorzystaniem karty graficznej został przedstawiony na
diagramie 1.2. W pierwszej kolejności (1) dane sa˛ kopiowane z pami˛eci operacyjnej do
pami˛eci globalnej na karcie graficznej. Nast˛epnie (2) procesor wywołuje odpowiednie
Wprowadzenie
11
Typ
Pojemność
Wzgl˛edny czas dost˛epu
Rejestry
32 KB - 256 KB
x1
Pami˛eć lokalna
16 KB lub 512 KB
x100
Pami˛eć współdzielona
16 KB lub 48 KB
x1
Pami˛eć globalna
≤ 6 GB
x100
Pami˛eć tekstur
≤ 6 GB
x1 − x100
Pami˛eć stałych
64 KB
x1 − x100
Tablica 1.3: Charakterystyka pami˛eci na kartach graficznych dla jednego multiprocesora
funkcje na karcie graficznej zwane funkcjami jadra
˛
(kernel function). Program jest
wykonywany asynchronicznie na wielu rdzeniach karty graficznej. Po zakończonym
działaniu programu możliwe jest (4) skopiowanie danych z pami˛eci globalnej do pami˛eci
operacyjnej.
Rozdział 2
Projekt
Niniejszy projekt stanowi drog˛e do poznania odpowiedzi na pytania zadane we wprowadzeniu (1). Poprzedni rozdział przybliża również rodzin˛e j˛ezyków skryptowych jak
i termin obliczeń heterogenicznych. Istnieja˛ już projekty łacz
˛ ace
˛ obie technologie (3.1),
ale w kontekście poczynionych założeń (2.1) jest to projekt nowatorski. Wypracowanie
ostatecznego rozwiazania
˛
wiazało
˛
si˛e z licznymi próbami, które również przynosza˛
odpowiedzi na przedstawione pytania.
2.1
Założenia projektu
Duża cz˛eść programów przetwarzajacych
˛
duże zbiory danych wykorzystuje do ich
reprezentacji wektory i macierze, formalizujac
˛ w ten sposób proces ich przetwarzania
w oparciu o algebr˛e liniowa.˛
Popularnym komercyjnym programem implementujacym
˛
znaczna˛ cz˛eść algebry liniowej
do operacji na macierzach jest Matlab. Jest to program uznawany za bezkonkurencyjny
na swoim polu, niestety nie tak wszechstronny jak j˛ezyki skryptowe ogólnego zastosowania (Python, Ruby). Cecha˛ charakterystyczna˛ tego narz˛edzia jest zaawansowany
interfejs programistyczny oraz duża wydajność, która zainspirowała powstanie darmo-
Projekt
13
wego odpowiednika jakim jest program Octave. Wykorzystanie tego przemyślanego
interfejsu programistycznego (API) jest warte rozważenia przy projektach operujacych
˛
na obiektach typu macierz.
Przetwarzanie za pomoca˛ j˛ezyków skryptowych zakłada niezależność wykonywanego
skryptu od środowiska wykonawczego. Jest to możliwe dzi˛eki wykorzystaniu osobnej
aplikacji do interakcji ze środowiskiem. Jest to ważna cecha j˛ezyków skryptowych, która
zostanie uwzgl˛edniona w ramach projektu. W kontekście przetwarzania danych na karcie
graficznej za pośrednictwem j˛ezyka skryptowego należy zagwarantować poprawne
działanie skryptu również na maszynach bez odpowiedniego procesora graficznego.
Wykorzystanie procesorów graficznych do przetwarzania dużych zbiorów danych niesie
ze soba˛ silne ograniczenie, jakim jest dost˛epność pami˛eci globalnej. Współczesne karty
moga˛ posiadać nawet 6 GB takiej pami˛eci, ale powszechnie dost˛epne karty posiadaja˛ jej
tylko 1 GB. Przy uwzgl˛ednieniu równoległej pracy w środowisku graficznym i innych
programów, jak na przykład przegladarki
˛
internetowe, pami˛eć dost˛epna na karcie graficznej może skurczyć si˛e do 100 MB. Przetwarzanie dużych zbiorów danych przy tak
ograniczonych zasobach jest mocno utrudnione dlatego implementowany projekt b˛edzie
umożliwiał kontrol˛e nad wykorzystaniem dost˛epnych zasobów.
Powyższe wprowadzenie do założeń projektu można zebrać w niniejszej liście:
1. Interfejs programistyczny niezależny od środowiska wykonawczego.
2. Efektywne wykorzystanie dost˛epnych zasobów.
3. Opracowanie interfejsu programistycznego na wzór znanego z programu Matlab.
2.2
Architektura
Środowiska deweloperskie umożliwiajace
˛ prac˛e z GPGPU sa˛ wspierane tylko przez
j˛ezyki systemowe takie jak C/C++. Wymusza to implementacj˛e wymaganej funkcjonal-
Projekt
14
ności w takim j˛ezyku i dopiero późniejsze udost˛epnienie funkcjonalności jako modułu
dla docelowego j˛ezyka skryptowego. Kolejnym elementem wpływajacym
˛
na struktur˛e
projektu sa˛ znaczne różnice pomi˛edzy interfejsem programistycznym dla GPGPU i standardowego CPU. Wymaga to stworzenia niezależnej implementacja każdego z nich.
Pogladowy
˛
schemat takiego rozwiazania
˛
przedstawia diagram 2.1.
pyMTX
MTX
GPGPU
CPU
Diagram 2.1: Schemat struktury projektu
Fragment projektu napisany za pomoca˛ j˛ezyka systemowego, został nazwany MTX. Nazwa pochodzi od powszechnie stosowanego skrótu słowa macierz w j˛ezyku angielskim
(matrix). Wykorzystanie takiego skrótu podkreśla główne zadanie tworzonej biblioteki,
jaka˛ jest implementacja obiektu macierzy oraz operacji na nim. Zewn˛etrzna˛ warstwa˛
projektu jest pyMTX. Moduł ten udost˛epnia zaimplementowana˛ funkcjonalność do
zadanego j˛ezyka skryptowego.
Projektowanie komponentów pisanych w j˛ezykach kompilowanych na potrzeby j˛ezyków
skryptowych wymaga uwzgl˛ednienia cech docelowego j˛ezyka. Jedna˛ z takich cech jest
brak operacji przypisania, co wymusza płytkie kopiowanie obiektów. Implementacj˛e
tej cechy oraz automatycznego zarzadzania
˛
pami˛ecia˛ w j˛ezykach systemowych można
zrealizować za pomoca˛ inteligentnych wskaźników. Wskaźniki tego typu umożliwiaja˛
bezpiecznie płytkie kopiowanie obiektów zawierajacych
˛
referencje do dynamicznie
alokowanych danych, oraz ich zwolnienie w momencie usuni˛ecia ostatniego wskaźnika.
Projekt
2.3
15
Interfejs
Tworzona biblioteka nie ma na celu dostarczenia pełnej funkcjonalności takich programów jak Matlab. Zakładane funkcje umożliwia˛ wykonanie podstawowych operacji
na macierzach, umożliwiajacych
˛
implementacj˛e licznych algorytmów takich jak sieci
neuronowe czy k-najbliższych sasiadów
˛
(k-nearest neighbors). Implementacj˛e tych
i innych algorytmów umożliwi już zestaw takich funkcji jak:
• Operacje na elementach macierzy za pomoca˛ funkcji logarytmicznych, trygonometrycznych oraz podnoszenie elementów macierzy do zadanej pot˛egi.
• Dodawanie, odejmowanie, mnożenie, dzielenie wartości skalarnej i macierzy.
• Dodawanie, odejmowanie, mnożenie, dzielenie macierzy ze soba˛ element do
elementu.
• Mnożenie macierzy.
• Transponowanie macierzy.
• Operowania na fragmentach macierzy (tak zwanych wyci˛eciach)
2.4
Oczekiwania
Tworzona biblioteka nie ma na celu optymalizacji pojedynczych algorytmów, a powinna
umożliwić przyśpieszenie wykonywania szerokiej gamy skryptów. Poprawa wydajności
na komputerach wyposażonych w odpowiednia˛ kart˛e graficzna˛ powinna zdeklasować
konkurencyjne rozwiazania
˛
w środowisku homogenicznym. Dodatkowo w przypadku
braku takiej karty graficznej, wydajność nie powinna odbiegać od tej oferowanej przez
alternatywne narz˛edzia. Uzyskanie takich wyników potwierdzi możliwość stworzenia
zakładanego uniwersalnego interfejsu programistycznego. Sukces projektu w dużej
mierze b˛edzie zależał od rozwoju technologii oraz zaangażowania samego środowiska
programistów j˛ezyka Python.
Rozdział 3
Realizacja
Istnieje wi˛ecej niż jedna możliwa realizacja przedstawionego projektu w ramach dost˛epnych narz˛edzi i technologii. Istnieja˛ narz˛edzia umożliwiajace
˛ wykorzystanie kart graficznych do obliczeń, ale tylko dwa zostały stworzone z myśla˛ o obliczeniach ogólnego
zastosowania. Sa˛ nimi OpenCL oraz CUDA. OpenCL jest uniwersalnym standardem
wspieranym przez licznych producentów sprz˛etu komputerowego. CUDA, z drugiej
strony, jest opatentowana˛ technologia˛ firmy Nvidia. Ze wzgl˛edu na ograniczone zasoby,
wspieranie obu z nich było niemożliwe. Do realizacji projektu została wybrana platforma CUDA. Taki wybór został podyktowany wzgl˛edami praktycznymi. Po pierwsze,
platforma firmy Nvidia wykorzystuje obiektowy j˛ezyk programowania C++, a nie strukturalny C jak to ma miejsce w OpenCL. Po drugie, ze wzgl˛edu na dost˛epność licznych
i różnorodnych narz˛edzi, bibliotek oraz projektów dla środowiska CUDA.
Z wielu j˛ezyków skryptowych ogólnego zastosowania zdecydowałem si˛e na j˛ezyk Python [22], który charakteryzuje si˛e przejrzysta˛ składnia,˛ bogata˛ dokumentacja˛ oraz
gotowymi narz˛edziami do jego integracji z kodem napisanym w C++. Jest jednym
z najpopularniejszych j˛ezyków wybieranych do realizacji nowych projektów oraz jako
jedyny j˛ezyk skryptowy posiada już pewna˛ baz˛e projektów wykorzystujacych
˛
środowisko CUDA.
Realizacja
3.1
17
Dost˛epne narz˛edzia
Technologia CUDA w ramach podstawowego pakietu narz˛edzi [6] zawiera takie biblioteki jak cuBLAS, CUDA MATH, CUDA RAND czy Thrust [23]. Implementuja˛ one
odpowiednio popularny interfejs programistyczny BLAS [2], zbiór funkcji matematycznych, funkcje do generowania liczb losowych oraz implementacj˛e STL (Standard
Template Library) dla procesora graficznego. Wymienione biblioteki umożliwiaja˛ efektywna˛ i wydajna˛ implementacj˛e macierzy. Jest to dopiero cz˛eść gotowych narz˛edzi, jakie
zostały stworzone dla środowisko CUDA. Biblioteki takie jak CUSP, CULV, cuSparse
czy MAGMA implementuja˛ już gotowe rozwiazania
˛
z rodziny algebry liniowej w tym
również macierze. Wartym wymienienia jest również komercyjny projekt ArrayFire [1],
który udost˛epnia bogata˛ funkcjonalność.
Wymienione biblioteki dost˛epne sa˛ dla j˛ezyka C++. Stanowia˛ one solidna˛ podstaw˛e
dla rozpatrywanego projektu, ale same w sobie nie stanowia˛ rozwiazania
˛
problemu
ponieważ docelowa˛ platforma˛ tego projektu jest j˛ezyk Python.
Wspomniane projekty łacz
˛ ace
˛ j˛ezyk Python i środowisko CUDA to mi˛edzy innymi
pyCuda [27], CUV [8], CUDAMat [7], copperhead [5] i Numba Pro [16]. Wymienione projekty zdawały si˛e być gotowa˛ lub prawie gotowa˛ implementacja˛ oczekiwanego
projektu. Niestety próba wykorzystania ich w praktyce uwypukliła liczne braki i ograniczenia każdego z nich uniemożliwiajace
˛ zrealizowanie zakładanych funkcjonalności.
Pierwszy z nich (pyCuda) jest w głównej mierze adapterem środowiska deweloperskiego
CUDA dla j˛ezyka Python rozbudowanym o możliwość dynamicznej kompilacji kodu
rdzeni. Złożone wywołania w j˛ezyku C++ zostały opakowane w przejrzyste obiekty
o wysokim poziomie abstrakcji, czyniac
˛ z tego projektu idealne narz˛edzie do prototypowania. Rozwijana przez Andreasa Klöcknera biblioteka na tym nie poprzestaje,
rozszerzajac
˛ moduł o implementacj˛e obiektu zgodnego z interfejsem numpy.array [17]
na kartach graficznych tworzac
˛ bardzo uniwersalne i efektywne narz˛edzie.
Kolejny projekt, CUV w przeciwieństwie od poprzedniego miał na celu stworzenie
Realizacja
18
narz˛edzi umożliwiajacych
˛
sprawna˛ implementacj˛e licznych algorytmów z rodziny uczenia maszyn. CUV udost˛epnia obiekt macierzy wzorujacej
˛ si˛e na programach takich
jak Matlab [14] czy Octave [19]. Posiada on pełne wsparcie operacji wymaganych
w ramach niniejszego projektu dla środowiska GPGPU. Projekt udost˛epnia interfejs programistyczny zarówno dla j˛ezyka C++ jak i j˛ezyka Python czyniac
˛ go wszechstronnym
narz˛edziem dla środowiska naukowego.
CUDAMat jest projektem znacznie skromniejszym od już omówionych. Udost˛epnia
czytelne API umożliwiajace
˛ realizacj˛e wi˛ekszości operacji na macierzach. Został stworzony by uzupełnić braki w raczkujacym
˛
wtedy połaczeniu
˛
środowiska CUDA i j˛ezyka
Python. Niestety architektura, w której ściśle zwiazano
˛
kolumny macierzy z rdzeniami
CUDA znaczaco
˛ utrudnia dalszy rozwój tego projektu.
Ostatnie dwa projekty reprezentuja˛ zupełnie inne podejście do wykorzystania synergii
j˛ezyka Python z moca˛ kart graficznych. Zamiast tworzyć osobna˛ bibliotek˛e wykorzystujac
˛ a˛ CUDA i opakowywać jej wywołania dla j˛ezyka Python, oba projekty wykorzystuja˛
natywny kod j˛ezyka skryptowego, poprzez rozszerzenie interpretera o możliwość kompilacji i wykonanie odpowiedniego fragmentu kodu na kartach graficznych. Pierwszy
z nich copperhead jest udost˛epniony na zasadach otwartego oprogramowania i wspiera
wiele obiektów i operacji ze standardowego zbioru j˛ezyka. Kolejny projekt to Numba
Pro, oferuje znacznie wi˛eksza˛ funkcjonalność niż copperhead, ale jest rozwiazaniem
˛
płatnym.
Wykorzystanie w projekcie już istniejacych,
˛
dojrzałych rozwiazań
˛
pozwala znacznie
zredukować zarówno czas implementacji jak i testowania. Powyżej przedstawiona
analiza dost˛epnych narz˛edzi nie pozwala ustalić właściwego projektu bazowego. Z tego
powodu były one kolejno testowane. Proces ten został opisany w kolejnej cz˛eści (3.2).
Doprowadził on do opracowania architektury projektu umożliwiajacej
˛ wydajna˛ realizacj˛e
wszystkich wymagań.
Realizacja
3.2
19
Przebieg realizacji
Szeroki wachlarz dost˛epnych narz˛edzi i technologii pozwala na różne implementacje.
Pierwszy krokiem w celu wybrania tej właściwej, jest analiza dost˛epnych projektów, na
bazie których można zrealizować zakładana˛ funkcjonalność. Biblioteki CUSP i CULV
dost˛epne w ramach podstawowego zestawu narz˛edzi projektu CUDA oferuja˛ implementacj˛e macierzy dla GPGPU. Niestety oba projekty nie wspieraja˛ operacji na fragmentach
macierzy znacznie ograniczajac
˛ ich praktyczne wykorzystanie w projekcie. Projekt,
który takie wsparcie zapewnia to ArrayFire, którego komercyjny charakter wyklucza go
z dalszych rozważań.
Inspiracja˛ dla niniejszej pracy był projekt pyCuda. Implementuje on obiekty takie jak
puCuda.gpuArray oraz numpy.array, które wydawały si˛e być idealnym połaczeniem
˛
procesora graficznego z j˛ezykiem skryptowym. Próba wykorzystania tego obiektu zakładała
stworzenie odpowiedniej fasady w j˛ezyku Python i w zależności od dost˛epności modułu
pyCuda wykorzystywanie tego modułu lub jego odpowiednika dla procesora. Niestety
pozorne zalety zostały zdominowane przez realne wady. Pierwsza˛ i najważniejsza˛ z nich
jest brak możliwości wykorzystania już istniejacych
˛
obiektów w celu zapisania wyniku obliczeń, co uniemożliwia efektywne zarzadzanie
˛
pami˛ecia˛1 . Kolejnym istotnym
problemem jest brak wsparcia dla operacji na fragmentach macierzy. Reszta projektu pyCuda również nie dostarczała pożadanej
˛
bazy wyjściowej. Wykorzystanie tego projektu
wymusza napisanie wymaganej funkcjonalności od podstaw jako ciagów
˛
znaków w j˛ezyku Python i dynamiczne ich kompilowanie. Takie podejście bezwzgl˛ednie ust˛epuje
napisaniu programu bezpośrednio w j˛ezyku C++.
Kolejnymi przeanalizowanymi projektami, które stanowiły potencjalnie dobra˛ baz˛e
wyjściowa˛ to CUDAMat oraz CUV. Oba projekty umożliwiaja˛ operacje na macierzach
na kartach graficznych z poziomu j˛ezyka Python. Niestety i te projekty nie znalazły
zastosowania w tym projekcie. Pierwszy z nich CUDAMat został odrzucony już przy
wst˛epnej analizie architektury. Zbyt sztywne założenia konstrukcyjne uniemożliwiały
1 http://documen.tician.de/pycuda/util.html
Realizacja
20
poprawna˛ implementacj˛e operacji na fragmentach macierzy. Projekt CUV również został
odrzucony ze wzgl˛edu na architektur˛e, która jest duża˛ przeszkoda˛ do zrealizowania
wszystkich poczynionych założeń. Znaczne rozmiary oraz rozległe zależności od środowiska CUDA praktycznie uniemożliwiaja˛ dostosowanie tego projektu dla środowisk
homogenicznych. Obejściem tego ograniczenia może być wykorzystanie tylko cz˛eści
odpowiedzialnej za przetwarzanie na kartach graficznych oraz dopisanie brakujacej
˛
funkcjonalności. Nie jest to rozwiazanie
˛
idealne. Z tego wzgl˛edu w pierwszej kolejności została podj˛eta próba stworzenia spójnej i elastycznej konstrukcji (architektury)
zoptymalizowanej pod wzgl˛edem postawionych wymagań. Jeżeli zajdzie taka konieczność zawsze istnieje możliwość wykorzystania tej cz˛eści projektu CUV i połaczenia
˛
jej
z reszta˛ tworzonego projektu.
Próba wykorzystania projektu copperhead wykazała nieefektywność takiego rozwiaza˛
nia w kontekście możliwości implementacji pożadanych
˛
optymalizacji. Wykorzystanie
j˛ezyka systemowego oraz bibliotek Thrust, Boost::Python mimo dłuższego kodu pozwoliło uzyskać znacznie lepsze wyniki od implementacji z wykorzystaniem projektu
copperhead.
Odrzucone projekty pozostawiaja˛ biblioteki standardowe CUDA i BOOST jako punkt
wyjściowy dla tego projektu. Obie biblioteki sa˛ dost˛epne dla szerokiej bazy sprz˛etu
i systemów operacyjnych, co umożliwia stworzenie uniwersalnego narz˛edzia. Dodatkowo spójny interfejs programistyczny (API-Application Programming Interface) tych
bibliotek oraz bazowanie na szablonach (Templates) umożliwia wykorzystanie tego
samego kodu dla różnych typów danych, jak i różnych środowisk wykonawczych. Taka
implementacja jest możliwa z wykorzystaniem projektu Thrust, który dodatkowo gwarantuje optymalna˛ implementacj˛e dla obecnych i przyszłych kart graficznych firmy
Nvidia.
Implementacja w oparciu o jednolity kod dla środowisk homogenicznych i heterogenicznych jest dobra˛ baza˛ wyjściowa˛ dla tego projektu. Przy minimalnej ilości kodu
obsługujacego
˛
oba środowiska i wszystkie typy danych uzyskano dobra˛ wydajność
Realizacja
21
dla operacji niezależnych takich jak dodawanie wartości skalarnej do macierzy. Operacje takie jak mnożenie macierzy uzyskiwały wyniki znacznie poniżej oczekiwań.
Zaimplementowana architektura pozwala na łatwa˛ optymalizacj˛e tych waskich
˛
gardeł
w projekcie. Pierwszym krokiem było wykorzystanie biblioteki cuBLAS, która zapewnia
optymalna˛ implementacj˛e operacji na typach zmiennoprzecinkowych (float, double).
Uzyskane w ten sposób wyniki dla typów zmiennoprzecinkowych były bardziej niż zadowalajace.
˛ Niestety wydajność na procesorze centralnym wcia˛ż pozostawała znaczaco
˛
poniżej oczekiwań. Wykorzystanie wielowatkowej
˛
implementacji iteratorów (iterator)
z biblioteki OMPTL [21] przyniosła zauważalne przyśpieszenie operacji niezależnych
i brak istotnej poprawy dla działań takich jak mnożenie macierzy. W tym celu analogicznie jak dla GPU została wykorzystana biblioteka uzupełniajaca
˛ te luki. Wachlarz
dost˛epnych projektów wartych rozważenia jest naprawd˛e szeroki. Do najbardziej popularnych należa˛ OpenBlas, ATLAS oraz MTL firmy Intel implementujace
˛ interfejs
programistyczny BLAS. Równie popularnym projektem jest Eigen [10], który dostarcza
najbardziej wszechstronne narz˛edzie w ramach otwartego oprogramowania (Open Source Software). Na ostateczny wybór tego ostatniego miały wpływ trzy elementy: bogata
funkcjonalność, wydajność oraz licencjonowanie (MPL2 [15]). Wykorzystanie tego
projektu wymusiło wi˛eksze odseparowanie implementacji dla CPU i GPU. Uzyskane
przyspieszenie podstawowych operacji na macierzach z wykorzystaniem tej biblioteki
pozwoliło uzyskać zadowalajace
˛ wyniki co kończy prace nad architektura˛ projektu.
Rozdział 4
Implementacja
Implementacja projektu została wykonana w systemie operacyjnym OpenSuse 12.3
x64 (Linux), przy wykorzystaniu kompilatora g++-4.6.3, środowiska deweloperskiego
CUDA w wersji 5.5, w którego skład wchodza˛ takie komponenty jak kompilator nvcc5.5 oraz biblioteki Thrust, cuBLAS, cuRAND, CUDA Math Library. Dodatkowo projekt
wykorzystuje bibliotek˛e standardowa˛ j˛ezyka C++, bibliotek˛e BOOST-1.49 oraz projekty
OMPTL opublikowany 22 kwietnia 2012 roku i Eigen w wersji 3.2.
Komponenty i biblioteki wykorzystane przy implementacji projektu sa˛ dost˛epne dla
różnych systemów operacyjnych, umożliwiajac
˛ wykorzystanie projektu w systemach
z rodziny Linux oraz Windows.
4.1
Architektura
Implementacja ujednoliconego interfejsu programistycznego dla różnych środowisk
wykonawczych wymusza stworzenie niezależnych komponentów dla każdego z nich.
Projekt jest tworzony od podstaw, co umożliwia efektywne wydzielenie cz˛eści wspólnej.
Obiektem agregujacym
˛
wspólna,˛ a zarazem niezależna˛ od środowiska cz˛eść funkcjonalności jest klasa Matrix. Komunikacja tego komponentu z obiektami operujacymi
˛
Implementacja
23
w danym środowisku odbywa si˛e za pośrednictwem klas i fabryk abstrakcyjnych [26].
Struktur˛e tego rozwiazania
˛
przedstawia diagram 4.1. Taka architektura umożliwia prosta˛ implementacj˛e poszczególnych komponentów dla każdego ze środowisk. Wada˛
takiego rozwiazania
˛
jest dodatkowy nakład obliczeniowy zwiazany
˛
z dynamicznymi
zależnościami pomi˛edzy obiektami.
data_type
DataContainerFactory
data_type
data_type
Matrix
Environment
data_type
DataCrawlerFactory
data_type
Operations
Diagram 4.1: Uogólniona architektura projektu
4.2
Inteligentne wskaźniki
Wykorzystanie inteligentnych [26] wskaźników przy tworzeniu biblioteki umożliwia
stworzenie prostego adaptera dla j˛ezyka skryptowego. W tym projekcie wykorzystano
implementacj˛e inteligentnych wskaźników z biblioteki BOOST. Różne implementacje
zostały przetestwane przed ostatecznym wyborem klasy shared_ptr ze wspomnianej
biblioteki. Wszystkie przeanalizowane rozwiazania
˛
wykazywały zbliżona˛ wydajność.
Inteligentny wskaźnik shared_ptr wnosił znaczac
˛ a˛ popraw˛e estetyki kodu, co zadecydowało o jego ostatecznym wyborze.
Implementacja
24
Wszystkie obiekty reprezentowane poprzez klas˛e abstrakcyjna˛ sa˛ wskazywane za pośrednictwem takiego wskaźnika. W projekcie każda z klas abstrakcyjnych definiuje typ
::Ptr, który reprezentuje shared_ptr wskazujacy
˛ na dana˛ klas˛e.
4.3
Leniwa ewaluacja
Leniwa ewaluacja jest popularna˛ metoda˛ wykorzystywana˛ do redukcji obcia˛żenia systemu. Działanie taj metody zakłada odłożenie wykonania konkretnego zadania do
momentu próby odczytania wyników. Redukcja obcia˛żenia nast˛epuje mi˛edzy innymi
przez unikni˛ecie niepotrzebnych obliczeń.
Inna˛ zaleta˛ leniwej ewaluacji jest możliwość uwzgl˛ednienia kontekstu przy odczycie
danych w celu usprawnienia wykonania odłożonego zadania. Wykorzystanie kontekstu
umożliwia realizacj˛e zakładanej funkcjonalności. Brak operacji przypisania w j˛ezyku
Python wymusza wykorzystanie innych operatorów do opisania tej funkcjonalności.
Operacja przypisania powinna umożliwić wykorzystanie już zaalokowanej pami˛eci do
zapisania wyniku obliczeń. Przy wykorzystaniu metody __setitme__ wykorzystujacej
˛
operator ”[]” możliwe jest wykonanie odłożonej operacji w momencie przypisania i tym
samym wykorzystania już zaalokowanej pami˛eci.
Do implementacji leniwej ewaluacji wykorzystano dwie flagi oraz funkcje dost˛epne
w ramach biblioteki BOOST. Pierwsza flaga informuje, że operacje na danym obiekcie
generuja˛ leniwe operacje. Druga informuje, że obiekt jest wynikiem takiej odłożonej
operacji i czeka na wykonanie. Leniwe zadania sa˛ zapisywane za pomoca˛ klasy function
z biblioteki BOOST. Pełni ona rol˛e wyrażenia lambda umożliwiajac
˛ przechowywanie
anonimowych delegacji do drzewa wywołań. Za stworzenie takich delegatów odpowiedzialna jest funkcja bind również dost˛epna w ramach biblioteki BOOST.
Implementacja
4.4
25
Operowanie na fragmentach macierzy
data_type
Tuple2D
+data_type operator[] (index_type)
data_type
Shape
data_type
Slice
+IsStartEdge () : bool
+isEndEdge () : bool
+GetEnd () : index_type
+GetStart () : index_type
+GetStep () : index_difference_type
Position
Diagram 4.2: Obiekty pomocnicze
Operowanie na fragmentach macierzy (z ang. slicing - wycinanie) umożliwia wykonanie
operacji z wykorzystaniem tylko niektórych elementów macierzy lub zapisanie wyników do zadanego fragmentu. Poszczególne fragmenty sa˛ reprezentowane przez dwa
obiekty typu Slice (diagram 4.2). Opisuja˛ one fragment dla pojedynczego wymiaru jak
wiersze czy kolumny. Fragment w danym wymiarze opisany jest przez indeks elementu
poczatkowego,
˛
końcowego oraz odległość kroku pomi˛edzy wybieranymi indeksami.
4.5
Komponenty
Po przedstawieniu najważniejszych mechanizmów wykorzystanych podczas implementacji możemy przystapić
˛ do omówienia poszczególnych komponentów. Projekt
został zaimplementowany w j˛ezyku C++ i przedstawione poniżej obiekty sa˛ elementami
z warstwy MTX.
Komponenty w ramach warstwy MTX sa˛ napisane jako nagłówki (headers) w j˛ezyku
C++ z wykorzystaniem szablonów (templates).
Implementacja
4.5.1
26
Matrix
Obiekt klasy Matrix jest centralnym elementem w projekcie, który reprezentuje interfejs
programistyczny użytkownika końcowego dla j˛ezyka C++. Jest jedynym nieabstrakcyjnym komponentem niezależnym od środowiska wykonawczego, odpowiedzialnym
za kontrol˛e logiki wywołań operacji na macierzach. Implementacj˛e poszczególnych
obiektów odpowiedzialnych za przechowywanie danych (DataContainer) oraz operacji
na nich (Operations) dostarczaja˛ fabryki abstrakcyjne (DataContinerFactory, OperationsFactory) udost˛epnione przez klas˛e Environment.
Stan macierzy zapisany w klasie Matrix zawiera informacje o transpozycji, wykorzystaniu fragmentu, wymuszeniu operacji na elementach oraz operacji odłożonej do wykonania. Informacje te sa˛ niezb˛edne do prawidłowego przeprowadzenia operacji na macierzy.
W celu umożliwienia transparentnego przekazania tych danych jako argumenty metod
z klasy Operations została stworzona klasa DataCrawler.
Stan wymuszajacy
˛ wykonanie operacji na elementach jest reprezentowany przez odpowiednia˛ flag˛e. Flaga ta jest brana pod uwag˛e przez operatory takie jak mnożenie czy
pot˛egowanie do wybrania właściwej metody z klasy Operations.
Transpozycja macierzy jest realizowana na dwa sposoby. Pierwszy z nich nie wykonuje żadnych operacji bezpośrednio na danych, a jedynie zmienia wartość odpowiedniej
flagi. Informacja ta wpływa na metod˛e zwracajac
˛ a˛ informacj˛e o kształcie macierzy
(GetShape) oraz obiekt DataCrawler, który dostarcza obiekt umożliwiajacy
˛ prawidłowa˛
iteracj˛e po zbiorze danych. Drugi sposób polega na wykonaniu transpozycji w miejscu,
czyli bez zb˛ednych alokacji pami˛eci. Druga metoda ma znaczenie przy operacjach, które
cz˛esto odwołuja˛ si˛e do tego samego elementu, jak ma to miejsce podczas mnożenia
macierzy.
Implementacja
27
data_type
Environment
GetFactory () : DataContainerFactory<data_type>::Ptr
Operations () : Opeartions<data_type>::Ptr
GetDataCrawlerFactory () : DataCrawlerFacotry<data_type>::Ptr
data_type
data_type
AutoEnvironment
CudaEnvironment
data_type
CpuEnvironment
Diagram 4.3: Schemat komponentu reprezentujacej
˛ środowisko wykonawcze
4.5.2
Environment
Komponentem reprezentujacym
˛
środowisko wykonawcze jest klasa Environment, przedstawiona na diagramie 4.3. Zadaniem tej klasy jest zapewnienie implementacji fabryk
abstrakcyjnych na potrzeby obiektu Matrix. Poszczególne implementacje dostarczaja˛
fabryki tworzace
˛ obiekty dla konkretnego środowiska wykonawczego.
4.5.3
DataContainerFactory
Obiekty reprezentujace
˛ kontenery na dane sa˛ z założenia zależne od środowiska wykonawczego, dlatego tworzone sa˛ za pomoca˛ fabryk abstrakcyjnych, jak DataContainerFactory tworzaca
˛ obiekty klasy DataContainer. Implementacj˛e dla poszczególnych
środowisk dostarczaja˛ klasy CpuDataContainerFactory oraz CudaDataContainerFactory, co przedstawia diagram 4.6.
Interfejs klasy DataContainerFactory dostarcza najcz˛eściej wykorzystywane metody
tworzenia macierzy, takie jak tworzenie pustej macierzy (Empty), macierzy zainicjo-
Implementacja
28
data_type
Envrionment
data_type
data_type
CpuEnvironment
DataCrawlerFactory
data_type
Operations
data_type
CpuDataCrawlerFactory
data_type
data_type
DataContainerFactory
CpuOperations
data_type
CpuDataContainerFactory
Diagram 4.4: Schemat implementacji dla środowiska cpu
wanej jedna˛ wartościa˛ (FromValue) lub na podstawie już istniejacej
˛ sekwencji danych
(FromVector, FromIterator). Klasa dostarcza również możliwość tworzenia macierzy
jednostkowych (Identity), które sa˛ cz˛esto wykorzystywane przez różne algorytmy. Metoda Sequence tworzy zbiór danych wypełniony wartościami ciagu
˛ arytmetycznego
o kroku 1. Tego typu obiekty sa˛ cz˛esto wykorzystywane przez programistów jako narz˛edzia pomocnicze do śledzenia indeksów macierzy przy sortowaniu. Metoda Random
wypełnia macierz wartościami losowymi. Takie macierze sa˛ cz˛esto wykorzystywane
przez programistów przy testowaniu algorytmów.
4.5.4
DataContainer
Klasa˛ implementujac
˛ a˛ funkcje kontenera danych w danym środowisku przedstawia
diagram 4.7. Interfejs zawiera metody pozwalajace
˛ na losowy dost˛ep do danych oraz
informacje o strukturze przechowywanych danych.
Dla środowiska CPU odpowiednia˛ implementacj˛e dostarcza klasa CpuDataContainer,
Implementacja
29
data_type
Envrionment
data_type
data_type
CudaEnvironment
DataCrawlerFactory
data_type
Operations
data_type
data_type
CudaDataCrawlerFactory
data_type
DataContainerFactory
CudaOperations
data_type
CudaDataContainerFactory
Diagram 4.5: Schemat implementacji dla Cuda
która wykorzystuje Eigen::Matrix do przechowywania danych. Klasa CudaDataContainer dla środowiska CUDA wykorzystuje thrust::device_vector z biblioteki standardowej
CUDA.
4.5.5
DataCrawler
DataCrawler pełni rol˛e mediatora pomi˛edzy klasa˛ Matrix i klasa˛ Operations umożliwiajac
˛ uniezależnienie operacji od stanu macierzy. Podstawowym zadaniem tej klasy
przedstawionej na diagramie 4.8 jest dostarczenie obiektu Iterator, który umożliwia
operowanie na fragmentach macierzy oraz ich transpozycjach bez konieczności ich
wcześniejszego kopiowania. Funkcjonalność, która˛ zapewniaja˛ obie klasy, znaczaco
˛
poprawia czytelność i ułatwia tworzenie operacji na macierzach dla danego środowiska
wykonawczego.
Dodatkowo klasy pochodne CudaDataCrawler wraz z CpuDataCrawler, które pełnia˛
rol˛e mediatora dla poszczególnych środowisk, umożliwiaja˛ bezpośredni dost˛ep do
Implementacja
30
data_type
DataContainerFactory
Empty () : DataContainer<data_type>::Ptr
Empty (Shape) : DataContainer<data_type>::Ptr
FromValue (Shape, data_type) : DataContainer<data_type>::Ptr
FromVector (Shape, std::vector<data_type>) : DataContainer<data_type>::Ptr
Sequence (Shape, data_type) : DataContainer<data_type>::Ptr
Identity (index_type) : DataContainer<data_type>::Ptr
Random (Shape) : DataContainer<data_type>::Ptr
data_type
CpuDataContainer
data_type
CudaDataContainer
Diagram 4.6: Fabryka kontenerów danych
danych przechowywanych w obiekcieDataContainer.
Implementacja dla środowiska CUDA dostarcza również fabryki iteratorów opartych na
bibliotece standardowej w formie ThrustIteratorFactory zwracanej przez GetThrustVersion.
4.5.6
DataCrawlerFactory
Jest to fabryka abstrakcyjna dla obiektów DataCrawler. Interfejsem tej fabryki sa˛ dwie
metody. Pierwsza z nich Create tworzy odpowiedni obiekt dla całego zbioru danych,
druga metoda CreateSliced tworzy go dla jego wyci˛ecia (fragmentu). Implementacje
w poszczególnych środowiskach realizuja˛ odpowiednio klasy CudaDataCrawler i CpuDataCrawler.
4.5.7
Iterator
Klasa Iterator implementuje wzorzec projektowy iterator [26], umożliwiajacy
˛ transparentny dost˛ep do danych. Obiekt tej klasy umożliwia wyliczenie faktycznego indeksu
elementu, do którego nastapiło
˛
odwołanie przy uwzgl˛ednieniu stanu macierzy Matrix.
Implementacja
31
data_type
DataContainer
+Get (index_type) : data_type
+Set (index_type, data_type) : void
+Get (Position) : data_type
+Set (Position, data_type) : void
+GetSize () : index_type
+GetShape () : Shape
+Reshape () : void
data_type
CudaDataContainer
+GetVector () : thrust::device_vector
data_type
CpuDataContainer
+GetVector () : std::vector
Diagram 4.7: Kontener danych
4.5.8
ThrustIterator
ThrustIterator jest odpowiednikiem klasy Iterator zbudowanym ze złożenia obiektów dost˛epnych w ramach środowiska deweloperskiego CUDA takich jak permutation_iterator,
transform_iterator, counting_iterator. Powstanie tej klasy zwiazane
˛
jest z ograniczeniem
wykorzystania innej implementacji iteratorów z funkcjami z biblioteki Thrust.
4.5.9
Operations
Ta klasa reprezentuje zbiór dost˛epnych operacji na macierzach. Implementacj˛e dla
poszczególnych środowisk zapewniaja˛ klasy CpuOperations i CudaOperations. Każda
z operacji (metod) charakteryzuje si˛e bezstanowym wykonaniem co oznacza, że każde
wywołanie operuje tylko na przekazanych argumentach i nie modyfikuje stanu klasy
Operations.
Macierz jest reprezentowana przez obiekt klasy DataCrawler, który jest przekazywany
jako argument funkcji umożliwiajac
˛ dost˛ep do danych za pośrednictwem klasy Iterator.
Wykorzystanie iteratorów jako schematu dost˛epu do danych pozwala użyć wydajnych
Implementacja
32
data_type
DataCrawler
+GetShape () : Shape
+GetTrueShape () : Shape
+GetSize () : index_type
+GetBegin () : Iterator<data_type>
+GetEnd () : Iterator<data_type>
data_type
CpuDataCrawler
data_type
CudaDataCrawler
+GetThrustVersion () : ThrustIteratorFactory
Diagram 4.8: Mediator pomi˛edzy stanem macierzy a operacjami
data_type
DataCrawlerFactory
Create (DataContainer<data_type>::Ptr, Shape, bool) : DataCrawler<data_type>::Ptr
CreateSliced (DataContainer<data_type>::Ptr, Shape, Slice, Slice, bool) : DataCrawler<data_type>::Ptr
data_type
CudaDataCrawlerFactory
data_type
CpuDataCrawlerFactory
Diagram 4.9: Fabryka mediatorów
funkcji z bibliotek standardowych. Pierwsza˛ z takich funkcji jest transform, która umożliwia wykonanie pewnej operacji na każdym elemencie zbioru i zapisanie wyniku za
pomoca˛ zadanego iteratora. Kolejne funkcje dost˛epne w ramach obu środowisk to
min_element oraz max_element, które pozwalaja˛ w wydajny sposób znaleźć minimalny
i maksymalny element zbioru. Funkcja sort wykorzystuje optymalne algorytmy sortowania dla danego środowiska. Sortowanie na procesorze odbywa si˛e za pomoca˛ algorytmu
”quick sort”, natomiast dla GPGPU wykorzystane jest ”merge sort”. Ostatnia˛ wykorzystana˛ funkcja˛ jest accumulate, za pomoca˛ której zostały zaimplementowane takie
metody jak Sum czy Prod.
Implementacja
33
Wymienione funkcje operuja˛ na iteratorach dostarczajac
˛ optymalne osiagi
˛ przy zachowaniu czytelnego kodu. Standardowa implementacja tych funkcji jest dost˛epna w obu
środowiskach.
˛ do tego
CpuOperations implementuje operacje dla środowiska CPU wykorzystujac
bibliotek˛e OMPTL. W ramach tego projektu zaimplementowane zostały funkcje transform, min_element, max_element, sort, accumulate z wykorzystaniem standardu OpenMP,
który pozwala na optymalne wykorzystanie wielordzeniowości współczesnych procesorów. Dodatkowo niektóre operacje w szczególnych przypadkach wykorzystuja˛ operacje
udost˛epnione przez projekt Eigen, który jest bardzo wydajna˛ implementacja˛ macierzy
na procesorze.
CudaOperations dostarcza implementacj˛e dla środowiska CUDA, z ta˛ różnica˛ że
funkcje transform, reduce (odpowiednik accumulate), copy, min_element, max_element,
sort pochodza˛ z projektu thrust b˛edacego
˛
cz˛eścia˛ standardowego środowiska deweloperskiego CUDA. Analogicznie do CpuOperations niektóre operacje w szczególnych
przypadkach wykorzystuja˛ funkcje z biblioteki cuBlas. Operacje wykonane z wykorzystaniem tej biblioteki gwarantuja˛ optymalne wykonanie na każdej karcie graficznej.
4.6
Adapter dla j˛ezyka Python
Adapter dla j˛ezyka Python jest warstwa˛ oznaczona˛ w projekcie jako pyMTX. Umożliwia
ona wykorzystanie funkcji napisanych w j˛ezyku C++ w środowisku docelowym jakim
jest j˛ezyk skryptowy Python.
Konstrukcja adaptera jest złożona z dwóch warstw. Pierwsza warstwa eksportuje poszczególne komponenty jako moduły j˛ezyka Python. Druga warstwa agreguje dost˛epne
komponenty i zapewnia wygodny interfejs dla j˛ezyka Python. Wykorzystanie struktury
złożonej z dwóch warstw znaczaco
˛ poprawia czytelność oraz elastyczność tworzonego
kodu.
Implementacja
34
Pierwsza warstwa zastała napisana z wykorzystaniem projektu Boost.Python [4]. Dostarcza on zestaw funkcji wspomagajacych
˛
opakowywanie klas z warstwy MTX. Zadaniem
pierwszej warstwy jest udost˛epnienie interfejsu C++ dla j˛ezyka Python. Poszczególne
komponenty z warstwy napisanej w C++ zostały zaprojektowane z myśla˛ o tym etapie
projektu. Ograniczone zostało wykorzystanie przecia˛żonych metod oraz dużej ilości
domyślnych parametrów, co znaczaco
˛ upraszcza implementacj˛e tej warstwy adaptera.
Pierwsza warstwa złożona jest z kilku plików. Każdy z nich to biblioteka współdzielona
reprezentujaca
˛ moduł w j˛ezyku Python. Głównym modułem jest mtx (mtx.so). W ramach tego modułu zostały wyeksportowane obiekty typu Matrix, obiekty pomocnicze
i abstrakcyjne. Udost˛epnione komponenty dostarczaja˛ kompletny interfejs programistyczny z wyłaczeniem
˛
obiektów implementujacych
˛
operacje dla danego środowiska.
Implementacje klas abstrakcyjnych dla konkretnego środowiska wykonawczego zostały
wydzielone do osobnych modułów. Taki podział wynika z braku możliwości skompilowania projektu Eigen i projektu CUDA jednocześnie. Rozdzielenie implementacji dla
różnych środowisk pozwoliło wypracować modułowa,˛ rozszerzalna˛ struktur˛e całego
projektu. Odpowiednio dla operacji na procesorze centralnym tworzony jest moduł
cpu_mtx (cpu_mtx.so), który eksportuje obiekt klasy CpuEnvironment i z nim wszystkie
klasy dla tego środowiska. Analogicznie tworzony jest moduł cuda_mtx (cuda_mtx.so),
który dostarcza implementacj˛e CudaEnvironment.
Druga warstwa jest odpowiedzialna za dynamiczne zagregowanie dost˛epnych komponentów i dostarczenie wygodnego interfejsu programistycznego dla j˛ezyka Python.
Zadanie to zostało zrealizowane przez prosty skrypt napisany w docelowym j˛ezyku.
4.7
Proces optymalizacji
Zadanie postawione przez niniejszy projekt wymaga stworzenia wszechstronnego narz˛edzia zdolnego do wydajnego przetwarzania dużych zbiorów danych. Generalizacja
wykonywanych zadań w kontekście operacji na macierzach niesie ze soba˛ konsekwencje
Implementacja
35
analogiczne do tych przedstawionych wcześniej w j˛ezykach skryptowych. Duża elastyczność kodu ma swoje negatywne konsekwencje zwiazane
˛
z wydajnościa.˛ Jest to
szczególnie ważna właściwość ze wzgl˛edu na prób˛e omini˛ecia tego typu ograniczeń
w j˛ezykach skryptowych.
Projekt został stworzony w procesie ciagłej
˛
optymalizacji i rozszerzania funkcjonalności.
Każdorazowa rozbudowa projektu skutkowała mniejszymi lub wi˛ekszymi spadkami
wydajności. W końcowych etapach projektu niwelowanie tego efektu przy zachowaniu
spójnej implementacji komponentów wymagało znacznych nakładów pracy przy niewielkiej poprawie wydajności. Wyniki uzyskane przez generyczna˛ implementacj˛e zakładanej
funkcjonalności były niezadowalajace.
˛ W przypadku operacji na procesorze wydajność
była nawet czterokrotnie gorsza od tej z wykorzystaniem biblioteki numpy [17]. Wyniki uzyskane z wykorzystaniem karty graficznej były nawet siedmiokrotnie lepsze
w stosunku do analogicznej implementacji w środowisku Octave. Wyniki uzyskane
w środowisku heterogenicznym były akceptowalne, ale te uzyskane na CPU były nie do
przyj˛ecia.
Przyczyna˛ takiego stanu rzeczy było wykorzystanie generycznych narz˛edzi ogólnego
zastosowani. Poszczególne kroki algorytmu zaimplementowane z wykorzystaniem modułu numpy umożliwiały dużo wydajniejsze wykorzystanie zasobów oraz unikni˛ecie
niepotrzebnych operacji, w porównaniu z implementacja˛ w oparciu o taka˛ uniwersalna˛
implementacj˛e. Najwi˛eksze straty były generowane przez obiekt klasy Iterator, który
wykonywał liczne dodatkowe obliczenia uwzgl˛edniajace
˛ stan macierzy w celu wyliczenia właściwej komórki pami˛eci, do której nastapiło
˛
odwołanie. Jednym ze sposobów na
omini˛ecie tego problemu jest wyspecjalizowanie operacji ze wzgl˛edu na stan macierzy.
Już samo uproszczenie obliczeń w przypadku iterowania na całych nietransponowanych macierzach przyniósł wzrost wydajności od 50-100% w zależności od problemu.
Wyniki uzyskiwane w kolejnych iteracjach specjalizacji kodu przynosiły oczekiwane wyniki, kosztem znacznie wi˛ekszej złożoności projektu. Skomplikowanie projektu wynika
z implementacji dedykowanych rozwiazań
˛
dla różnych stanów macierzy.
Implementacja
36
Wydajne przetwarzanie danych na procesorze centralnym jest popularnym problemem,
skutecznie rozwiazanym
˛
w ramach licznych projektów. Naturalnym krokiem w rozwoju
tego projektu ze wzgl˛edu na napotkane ograniczenia jest wykorzystanie już wypracowanych wydajnych rozwiazań.
˛
Analiza wydajności, funkcjonalności i dost˛epnych narz˛edzi
doprowadzała do wybrania projektu Eigen. Implementuje on obiekt macierzy spełniajacy
˛ wszystkie wymagane funkcjonalności. Wykorzystanie tego projektu pozwoliło
wielokrotnie przyśpieszyć popularne operacje uzyskujac
˛ bardzo pozytywne rezultaty.
Operacje na karcie graficznej również wykorzystuja˛ wsparcie zewn˛etrznych bibliotek
w celu przyśpieszenia wykonania popularnych operacji. Takim projektem jest cuBLAS
b˛edacy
˛ cz˛eścia˛ standardowego środowiska deweloperskiego dla platformy CUDA. Stworzony został w celu dostarczenia optymalnej implementacji interfejsu BLAS na kartach
graficznych. W szczególnych przypadkach zapewnia gigantyczny wzrost wydajności
operacji takich jak mnożenie macierzy.
Rozdział 5
Zastosowanie w praktyce
Celem niniejszej pracy jest analiza wykorzystania procesora graficznego w celu zniwelowania ograniczenia j˛ezyków skryptowych do wydajnego przetwarzania dużych zbiorów
danych. Weryfikacja tego stwierdzenia zostanie przeprowadzona w oparciu o rzeczywiste problemy. W tym celu zostały wybrane dwa algorytmy z rodziny rozpoznawania
obrazów. Pierwszy algorytm jest algorytmem trenowania sieci neuronowej za pomoca˛
wstecznej propagacji bł˛edu. Algorytm ten charakteryzuje si˛e kosztownymi obliczeniowo
operacjami takimi jak mnożenie dużych macierzy, czy wyliczenie funkcji aktywacji
oraz jej pochodnej. Wyst˛epowanie tych operacji kładzie duży nacisk na wsadowe przetwarzanie danych, dla którego zostały stworzone karty graficzne. Drugi algorytm to
k-najbliższych sasiadów.
˛
Jest to prosty algorytm, który nie wykonuje kosztownych
obliczeń w przeciwieństwie do pierwszego. Właściwości tego algorytmu kłada˛ dużo
wi˛ekszy nacisk na wydajne przetwarzanie kolejnych operacji oraz przepływ danych. Sa˛
to cechy, którymi charakteryzuje si˛e przetwarzanie na procesorze centralnym (CPU).
Wybrane algorytmy powinny dobrze reprezentować analizowana˛ grup˛e problemów.
Zastosowanie w praktyce
5.1
38
Kontekst
Analiza uzyskanych wyników bez odpowiedniego kontekstu nie ma wi˛ekszego sensu,
z tego powodu oba algorytmy zostały zaimplementowane z wykorzystaniem różnych
narz˛edzi i j˛ezyków.
Głównym punktem referencyjnym tego projektu sa˛ j˛ezyki systemowe. Sa˛ one popularnym narz˛edziem, wykorzystywanym do rozwiazywania
˛
tego typu problemów. Przedstawicielem tej grupy jest j˛ezyk C++. Zastosowano wydajna˛ implementacj˛e macierzy
w tym j˛ezyku, jaka˛ zapewnia biblioteka Eigen. Środowisko naukowe równie cz˛esto
wykorzystuje programy typu Matlab oraz jego darmowa˛ implementacj˛e Octave w celu
szybkiej i wydajnej implementacji różnych algorytmów. Programy te działaja˛ w oparciu
o wyspecjalizowany j˛ezyk skryptowy. Nie jest to j˛ezyk stworzony do programowania
ogólnego zastosowania, ale ze wzgl˛edu na jego popularność zostanie uwzgl˛edniony
w porównaniu. W celu dopełnienia kontekstu, algorytmy zostały przetestowane za
pomoca˛ programu w j˛ezyku Python. Do przetwarzania danych został wykorzystany
popularny modułu numpy. Jest to narz˛edzie wykorzystywane do przetwarzania realnych
danych z pomini˛eciem dynamicznego typowania oraz zb˛ednych operacji wykonywanych
przez interpreter j˛ezyka.
Przy porównywaniu wyników nie zostana˛ wykorzystane programy napisane bezpośrednio (bez zewn˛etrznych bibliotek) w j˛ezyku Python lub CUDA. W pierwszym przypadku
wyniki byłyby gorsze o kilka rz˛edów wielkości i nie wniosłyby nic innego jak potwierdzenie faktu, że j˛ezyki skryptowe nie służa˛ do rozwiazywania
˛
tego typu problemów.
Wykorzystanie algorytmu napisanego bezpośrednio w CUDA również nie wniesie wartości merytorycznych do tego porównania. Konstrukcja procesorów graficznych oraz ich
architektura (hierarchia) pami˛eci umożliwia daleko idac
˛ a˛ optymalizacj˛e konkretnego
zadania. W praktyce wymaga to dużych nakładów pracy oraz wielu linii kodu, ale wyniki
w ten sposób uzyskane moga˛ być wydajniejsze o par˛e rz˛edów wielkości od ich generycznych odpowiedników. Z tego powodu obie implementacje nie stanowia˛ bliskiego
otoczenia rozpatrywanego problemu i nie zostały uwzgl˛ednione w tym porównaniu.
Zastosowanie w praktyce
39
Poszczególne implementacje dla różnych środowisk i j˛ezyków, dla lepszej czytelności
tekstu, zostały opisane nast˛epujacymi
˛
etykietami:
mtx
Algorytm napisany w oparciu o niniejszy projekt przy założeniu stworzenia kodu minimalizujacego
˛
ilości wykorzystanych instrukcji oraz
linii kodu.
mtx_op
Algorytm napisany w oparciu o niniejszy projekt z wykorzystaniem
dost˛epnych narz˛edzi w celu optymalizacji czasu wykonania, wykorzystuje leniwa˛ ewaluacj˛e oraz operacje na fragmentach macierzy.
eigen
Algorytm napisany w j˛ezyku C++ z wykorzystaniem biblioteki Eigen.
octave
Algorytm napisany w programie Octave.
numpy
Algorytm napisany w j˛ezyku skryptowym Python w oparciu o moduł
numpy.
Implementacje mtx oraz mtx_op korzystajace
˛ z niniejszego projektu umożliwiaja˛ wykonanie tego samego kodu zarówno w środowisku heterogenicznym jak i homogenicznym.
Wyniki uzyskane przez poszczególne środowiska dla rozróżnienia zostana˛ poprzedzone
przedrostkami cuda_ dla środowiska heterogenicznego oraz cpu_ dla homogenicznego.
Zastosowanie w praktyce
5.2
40
Procedura testowa
Wykonane pomiary dotycza˛ wyłacznie
˛
analizowanego algorytmu. Wczytanie oraz inicjalizacja danych nie jest wliczana do całkowitego czasu wykonania programu ani do
jego długości wyrażonej w liniach kodu. Analizowane algorytmy b˛eda˛ realizowały ten
sam pseudo kod.
Poszczególne wyniki zostana˛ również przedstawione w porównaniu z implementacja˛
numpy oraz eigen. Porównanie zostanie zaprezentowane jako iloraz czasów dla danego
testu wyrażony wzorem:
ti (T )
ti (D)
(5.1)
Gdzie:
• ti (x) czas uzyskany w teście i przez algorytm x
• T ∈ {numpy, eigen, cpu_mtx, cuda_mtx, cpu_mtx_op, cuda_mtx_op, octave}
• D ∈ {numpy, eigen}
Testy zostały przeprowadzone na komputerze o nast˛epujacych
˛
parametrach:
Procesor:
Intel i5-2500k
Karta graficzna:
Gigabite 560 Ti 1 GB
Pami˛eć operacyjna: DDR3-1600 8 GB
Płyta główna:
Asus P8P67
System operacyjny:
OpenSuse 12.3 x64
Wszystkie testy wykonano przy wyłaczonym
˛
środowisku graficznym, na danych typu
zmiennoprzecinkowego (float).
Zastosowanie w praktyce
5.3
41
Sieci neuronowe
Sztuczne sieci neuronowe przenosza˛ uproszczona˛ ide˛e działania mózgu na model matematyczny. Sieć złożona jest ze sztucznych neuronów zgrupowanych w warstwy (5.1).
Analizowana sieć zbudowana jest z trzech warstw. Neurony w sasiaduj
˛
acych
˛
warstwach
łacz
˛ a˛ si˛e każdy z każdym.
Z matematycznego punktu widzenia sieci neuronowe to kompozycja funkcji. Każda
funkcja f () reprezentujaca
˛ pojedynczy neuron jest złożeniem funkcji ze zbioru G =
{g1 (), g2 (), ..., gn ()}. Kompozycj˛e tych funkcji można przedstawić z pomoca˛ grafu
skierowanego (5.1). Każdemu sztucznemu i-temu neuronowi przypisana jest pewna
waga wi . Najcz˛eściej spotykana postać neuronu to:
fi () = k( ∑ wi g())
g∈G
Gdzie k(x) jest nazywana funkcja˛ aktywacji.
Funkcja˛ aktywacji wykorzystana˛ w tym projekcie jest tanh.
k(x) = tanh(x)
k0 (x) = 1 − tanh2 (x)
Warstwa
wejściowa
Warstwa
ukryta
Warstwa
wyjściowa
Diagram 5.1: Sztuczna sieć neuronowa
Zastosowanie w praktyce
42
Algorytm trenowania sieci neuronowej za pomoca˛ wstecznej propagacji bł˛edu opisuje
program 1. Celem tego algorytmu w ramach tej pracy jest zbadanie wydajności danej
implementacji, a nie wytrenowanie poprawnej sieci. W celu uzyskania miarodajnych
wyników algorytm wykona dokładnie jedna˛ iteracj˛e po zbiorze trenujacym
˛
b˛edacym
˛
odpowiednikiem jednej epoki. Jest to jedyna różnica w stosunku od standardowo stosowanego algorytmu, gdzie wykorzystywane sa˛ inne warunki przerwania uwzgl˛edniajace
˛
wypracowane wartości i wyniki sieci.
Program 1 Wsteczna propagacja bł˛edu
// inicjalizacja wag krawędzi pomiędzy warstwami wejścia i ukrytą
WWU = wartości losowe
// inicjalizacja wag krawędzi pomiędzy warstwami ukrytą i wyjścia
WUW = wartości losowe
Dla każdego wektora V ze zbioru trenującego:
wylicz wartości w~węzłach
błąd wyjścia = wartość oczekiwana dla V - warstwa wyjścia
różnica
= pochodna funkcji aktywacji z~warstwy wyjścia \
* błąd wyjścia
zmiana WUW = różnica * warstwa ukryta
błąd ukrytej = błąd wyjścia * warstwa ukryta
różnica
= pochodna funkcji aktywacji z~warstwy ukrytej \
* błąd wyjścia
zmiana WWU = różnica * V // wartości warstwy wejścia
WUW += współczynnik nauki * zmiana WUW \
+ współczynnik bezwładności * poprzednia zmiana WUW
WWU += współczynnik nauki * zmiana WWU \
+ współczynnik bezwładności * poprzednia zmiana WWU
Przedstawiony program zostanie przetestowany w nast˛epujacych
˛
konfiguracjach warstw
i na zbiorze trenujacym
˛
złożonym z 1000 wektorów :
Zastosowanie w praktyce
43
nr.
liczba w˛ezłów w warstwach
testu wejściowych ukrytych wyjściowych
1
100
100
10
2
400
400
10
3
400
800
10
4
400
1600
10
5
1600
3000
10
6
5000
5000
10
7
4000
10000
10
Tablica 5.1: Testowane struktury sieci neuronowej
5.3.1
Wyniki
Numer testu
1
2
3
4
5
6
7
numpy
0.2282
2.4327
5.7660 23.6990 98.1920
504.5195 864.2208
cpu_mtx
0.4019
1.9773
3.9715 18.0000 68.9690
389.0182 598.3393
cpu_mtx_op
0.4474
1.3745
2.6760 12.5660 43.8610
214.0212 380.5067
cuda_mtx
1.4983
2.6929
2.9844 5.4400
16.5930
88.0082
—
cuda_mtx_op 0.7463
0.7926
0.9629 1.8534
5.0906
23.5022
37.5403
eigen
0.0380
0.7654
1.8077 10.0180 33.1460
157.2790 292.8660
octave
0.2692
1.3299
2.9722 11.2990 48.2230
237.6764 376.0105
Tablica 5.2: Czas wykonania algorytmu wstecznej propagacji bł˛edu wyrażony w sekundach
Zastosowanie w praktyce
44
Numer testu
1
numpy
x1
cpu_mtx
2
x1
3
4
5
6
7
x1
x1
x1
x1
x1
x0.568 x1.230
x1.507
x1.317
x1.424
x1.297
x1.444
cpu_mtx_op
x0.510 x1.770
x2.237
x1.886
x2.239
x2.357
x2.271
cuda_mtx
x0.152 x0.903
x2.006
x4.356
x5.918
x5.733
—
cuda_mtx_op x0.306 x3.069
x6.217
x12.787 x19.289
x21.467
x23.021
eigen
x6.005 x3.178
x3.312
x2.366
x2.962
x3.208
x2.951
octave
x0.848 x1.829
x2.014
x2.097
x2.036
x2.123
x2.298
Tablica 5.3: Porównanie czasów wzgl˛edem implementacji nympy wyrażonych ilorazem opisanym
wzorem (5.1) (wartości > 1 oznaczaja˛ przyśpieszenie)
Diagram 5.2: Graficzne porównanie czasów wzgl˛edem implementacji nympy wyrażonych ilorazem opisanym wzorem (5.1) (wartości > 1 oznaczaja˛ przyśpieszenie)
Zastosowanie w praktyce
45
Numer testu
1
2
numpy
x0.167
x0.315
cpu_mtx
x0.095
cpu_mtx_op
cuda_mtx
3
4
5
6
7
x0.302 x0.423
x0.338
x0.312
x0.339
x0.387
x0.455 x0.557
x0.481
x0.404
x0.489
x0.085
x0.557
x0.676 x0.797
x0.756
x0.735
x0.770
x0.025
x0.284
x0.606 x1.842
x1.998
x1.787
—
cuda_mtx_op x0.051
x0.966
x1.877 x5.405
x6.511
x6.692
x7.801
eigen
x1
x1
x1
x1
x1
x1
octave
x0.141
x0.576
x0.608 x0.887
x0.687
x0.662
x0.779
x1
Tablica 5.4: Porównanie czasów wzgl˛edem implementacji eigen wyrażonych ilorazem opisanym
wzorem (5.1) (wartości > 1 oznaczaja˛ przyśpieszenie)
Diagram 5.3: Graficzne porównanie czasów wzgl˛edem implementacji eigen wyrażonych ilorazem opisanym wzorem (5.1) (wartości > 1 oznaczaja˛ przyśpieszenie)
Zastosowanie w praktyce
46
Algorytm
Linie kodu
numpy
25
mtx
27
mtx_op
50
octave
38
eigen
52
Tablica 5.5: Długość programu wstecznej propagacji bł˛edu
5.3.2
Omówienie wyników
Z zebranych wyników wyróżnia si˛e pierwszy przypadek testowy przeprowadzony dla
małej sieci neuronowej. Przetwarzanie małych zbiorów danych dobrze oddaje charakterystyk˛e wykorzystanych j˛ezyków. Rewelacyjny wynik programu napisanego w j˛ezyku
systemowym wynika z operowania bezpośrednio na procesorze, w przeciwieństwie do
j˛ezyków skryptowych, które wykonuja˛ liczne operacje zwiazane
˛
z dynamicznym typem danych, przetwarzaniem skryptu oraz operowaniem za pośrednictwem interpretera.
Najgorsze wyniki dla pierwszego testu uzyskały algorytmy pracujace
˛ w środowisku heterogenicznym ze wzgl˛edu na dodatkowy wpływ opóźnień wynikajacych
˛
z komunikacji
pomi˛edzy procesorem centralnym a procesorem graficznym.
Brak wyniku dla cuda_mtx w siódmym teście wynika z braku dostatecznej ilości wolnej
pami˛eci na karcie graficznej. Test przeprowadzony z wykorzystaniem zoptymalizowanej
wersji cuda_mtx_op był możliwy do wykonania ze wzgl˛edu na lepsze zarzadzanie
˛
pami˛ecia˛ z poziomu skryptu.
W tabelach 5.2 i 5.4 zostały przedstawione wyniki porównania poszczególnych algorytmów do referencyjnego rozwiazania
˛
w j˛ezyku Python oraz punktu docelowego jakim
jest implementacja w j˛ezyku systemowym. Wyniki uzyskane przez eigen sa˛ średnio
trzykrotnie lepsze od tych uzyskanych przez numpy. Drugim popularnym j˛ezykiem
wykorzystywanym do przetwarzania dużych zbiorów danych jest Octave, którego śred-
Zastosowanie w praktyce
47
nie czasy sa˛ dwukrotnie krótsze od implementacji w j˛ezyku Python i niecałe dwa razy
dłuższe od eigen.
Wyniki uzyskane przez algorytm cpu_mtx uplasowuja˛ go pomi˛edzy implementacjami
numpy a ocatve, co z perspektywy sposobu implementacji oraz wykorzystanego j˛ezyka
należy uznać za bardzo dobry wynik. Znacznie lepszymi wynikami wykazała si˛e zoptymalizowana wersja cpu_mtx_op uzyskujac
˛ a˛ wyniki porównywalne ze środowiskiem
Octave. Kosztem takich osiagów
˛
jest znacznie dłuższy kod w porównaniu do wersji
niezoptymalizowanej czy skryptu octave.
W przypadku środowiska heterogenicznego uzyskane wyniki jako jedyne moga˛ konkurować z implementacja˛ w j˛ezyku systemowym. Pomimo słabego startu implementacja mtx
uzyskała wyniki dwukrotnie lepsze od eigen w trzecim i kolejnych testach. Świadczy to
o znacznym marnowaniu zasobów przez generyczne rozwiazania,
˛
które sa˛ rekompensowane przez wydajność GPGPU dopiero przy dostatecznie dużych zbiorach danych.
Ciekawie przedstawiaja˛ si˛e wyniki dla zoptymalizowanego wariantu mtx_op, który już
w drugim teście uzyskał wynik porównywalny z implementacja˛ w j˛ezyku C++. Wyniki
kolejnych testów tylko powi˛ekszyły różnic˛e w wydajności dochodzacej
˛ w ostatnim
teście do 680% na korzyść projektu. Wynik uzyskany w ostatnim teście wr˛ecz przytłacza
w porównaniu do algorytmu opartego na bibliotece numpy, od którego jest szybszy
dwudziestotrzykrotnie.
Wyniki uzyskane przez algorytm mtx_op w środowisku heterogenicznym, potwierdzaja˛
znaczne możliwości jakie drzemia˛ w procesorach graficznych. Wykorzystanie generycznej implementacji mtx pozwoliło uzyskać jedynie zadowalajace
˛ wyniki. Zarówno wyniki
dla środowiska homogenicznego jak i heterogenicznego sa˛ zgodne z przewidywaniami.
Wypracowujac
˛ na procesorze czasy porównywalne z wydajnym środowiskiem Octave
oraz uzyskujac
˛ znacznie lepsze czasy we współpracy z GPGPU. Wpływ na taki stan
rzeczy ma również charakterystyka analizowanego algorytmu, który bardzo dobrze
skaluje si˛e na procesorach graficznych.
Zastosowanie w praktyce
5.4
48
K-najbliższych sasiadów
˛
K-najbliższych sasiadów
˛
(k nearest neighbours), jest prostym algorytmem klasyfikacji.
Klasyfikacja za jego pomoca˛ polega na wyliczeniu odległości pomi˛edzy badanym obiektem, a elementami zbioru trenujacego.
˛
Tak uzyskane odległości zostaja˛ posortowane i na
podstawie k-najbliższych elementów zostaje wybrana najcz˛eściej wyst˛epujaca
˛ etykieta.
Sam algorytm jest bardzo prosty i zarazem bardzo skuteczny. Jego specyfik˛e określaja˛
wartość k oraz metryka opisujaca
˛ odległość miedzy obiektami. W niniejszym projekcie
zastosowano metryk˛e Euklidesowa.˛ Działanie tego algorytmu przedstawia program 2.
Program 2 Algorytm k-najbliższych sasiadów
˛
S = pusta lista
Dla każdego wektora V ze zbioru trenującego:
v = odległość testowanego elementu od V
dodaj do S parę odległość v i~etykietę wektora V
sortuj S
wybierz k najbliższych elementów
Zwróć najpopularniejszą etykietę z~wybranych elementów
Analogicznie do sieci neuronowych, algorytm knn również został przetestowany w kilku
wariantach. Ze wzgl˛edu na krótkie czasy wykonania, jednorazowo przeprowadzono
klasyfikacj˛e 100 wektorów.
5.4.1
Porównanie mtx i mtx_op
Krótkie implementacje mtx i mtx_op algorytmu knn pozwalaja˛ przedstawić różnice
mi˛edzy nimi oraz sam proces optymalizacji. Implementacja mtx (program 3) jest bardzo podobna do domyślnej implementacji w j˛ezyku Python (program 4). Taka zbieżność ułatwia proces poznawania tworzonego narz˛edzia. Dodatkowo istnieje możliwość
Zastosowanie w praktyce
49
nr. testu wielkość zbioru trenujacego
˛
liczba wymiarów
1
1000
100
2
5000
400
3
10000
400
4
15000
400
5
15000
800
6
15000
1600
7
10000
5000
Tablica 5.6: Przypadki testowe dla algorytmu knn
modyfikacji drugiej warstwy adaptera do j˛ezyka Python i dostosowanie interfejsu do
indywidualnych upodobań programisty.
Zoptymalizowana˛ implementacj˛e przedstawia program 5. Odbiega on od utartych schematów programowania w j˛ezyku Python. Pierwsza˛ wprowadzona˛ zmiana˛ jest stworzenie
obiektów tmat oraz tvec na poczatku
˛
programu. Sa˛ one nast˛epnie wielokrotnie wykorzystywane w głównej p˛etli programu. Bez tej zmiany wariant mtx tworzy nowe
tymczasowe kopie przy każdym przebiegu owej p˛etli. Druga˛ zmiana˛ jest rozbicie poszczególnych linii algorytmu na operacje maksymalnie dwuargumentowe. Taki podział
zwiazany
˛
jest z architektura˛ biblioteki, która ogranicza kompozycj˛e leniwych funkcji do
jednego wywołania. W przypadku kiedy dany obiekt reprezentuje taka˛ odłożona˛ operacj˛e i zostanie wykorzystany w kolejnej operacji, odłożone zadanie zostaje wykonane
i zapisane do nowego tymczasowego obiektu.
Modyfikacja programu 3 w celu poprawy wydajności jest prosta. Wymaga unikania
zb˛ednego tworzenia tymczasowych obiektów oraz rozbicie poszczególnych kroków
algorytmu na dwuargumentowe funkcje. Zysk wydajności płynacy
˛ z tak prostych modyfikacji jest znaczny w szczególności dla środowiska heterogenicznego.
Zastosowanie w praktyce
50
Program 3 Kod algorytmu mtx
def most_common(L):
return max(grby(L),key=lambda(x,v):(len(list(v)),-L.index(x)))[0]
def knn (train,labels, test, k):
srt = Matrix ((test.get_shape()[0], k))
for i in xrange (test.get_shape()[0]):
tmp=splay(labels,pow((train-Matrix(test[i,:])).e(),2).sum(1),1)
srt[i,:] =
tmp.sort_by_key(1,0)[:k,0].get_t()
srt.sort (1)
sl = srt.to_list ()
return [most_common(sl[i]) for i in xrange(srt.get_shape()[0])]
Program 4 Kod algorytmu numpy
def most_common(L):
return max(grby(L),key=lambda(x,v):(len(list(v)),-L.index(x)))[0]
def knn (train,labels, test, k):
srt = zeros ((test.shape[0], k))
for i in xrange (len(test)):
tmp = sum(pow ((train - test[i,:]), 2), 1)
ind =
lexsort ((tmp,))
for n in xrange(k):
srt[i,n] = labels[ind[n]]
sort(srt, axis = 1)
return [most_common(list(srt[i,:])) for i in xrange(srt.shape[0])]
Zastosowanie w praktyce
51
Program 5 Kod algorytmu mtx_op
def most_common(L):
return max(grby(L),key=lambda(x,v):(len(list(v)),-L.index(x)))[0]
def knn (train,labels,test, k):
srt = Matrix ((test.get_shape()[0], k))
tmat = Matrix ((train.get_shape ()))
tvec = Matrix ((train.get_shape ()[0], 2))
for i in xrange (test.get_shape()[0]):
tmat[:,:] = train - Matrix(test[i,:])
tmat[:,:] = tmat.e() * tmat
tvec[:,1] = tmat.sum(1);
tvec[:,0] = labels
tvec[:,:] = tvec.sort_by_key(1,0)
srt[i,:] =
tvec[:k,0].get_t()
srt.sort (1)
sl = srt.to_list()
return [most_common(sl[i]) for i in xrange(srt.get_shape()[0])]
Zastosowanie w praktyce
5.4.2
52
Wyniki
Numer testu
1
2
3
4
5
6
7
numpy
0.0597 0.8720
1.6970
2.4867
4.8400
9.4974
20.1430
cpu_mtx
0.210
5.532
8.459
17.067
33.912
83.002
cpu_mtx_op
0.1036 0.7072
1.3835
2.1204
4.1053
8.1251
16.1900
cuda_mtx
0.1503 0.5384
1.0860
1.6923
3.5305
7.5443
16.7810
cuda_mtx_op 0.1045 0.3005
0.5419
0.8339
1.7435
3.6406
7.9922
eigen
0.0198 0.3954
0.7876
1.2007
2.3527
4.9691
11.9950
octave
0.1262 1.1024
2.0157
3.1153
5.9305
11.4820 23.2750
2.893
Tablica 5.7: Czas wykonania algorytmu k-najbliższych sasiadów
˛
wyrażone w sekundach
Algorytm
Linie kodu
numpy
10
mtx
9
mtx_op
15
octave
6
eigen
35
Tablica 5.8: Długość programu k-najbliższych sasiadów
˛
Zastosowanie w praktyce
53
Numer testu
1
2
3
numpy
x1
x1
x1
cpu_mtx
x0.284
x0.301
cpu_mtx_op
x0.577
cuda_mtx
4
x1
5
6
7
x1
x1
x1
x0.307 x0.294
x0.284
x0.280
x0.243
x1.233
x1.227 x1.173
x1.179
x1.169
x1.244
x0.397
x1.620
x1.563 x1.469
x1.371
x1.259
x1.200
cuda_mtx_op x0.572
x2.902
x3.132 x2.982
x2.776
x2.609
x2.520
eigen
x3.013
x2.205
x2.155 x2.071
x2.057
x1.911
x1.679
octave
x0.473
x0.791
x0.842 x0.798
x0.816
x0.827
x0.865
Tablica 5.9: Porównanie czasów wzgl˛edem implementacji nympy wyrażonych ilorazem opisanym
wzorem (5.1) (wartości > 1 oznaczaja˛ przyśpieszenie)
Diagram 5.4: Graficzne porównanie czasów wzgl˛edem implementacji nympy wyrażonych ilorazem opisanym wzorem (5.1) (wartości > 1 oznaczaja˛ przyśpieszenie)
Zastosowanie w praktyce
54
Numer testu
1
2
numpy
x0.332
x0.453
cpu_mtx
x0.094
cpu_mtx_op
cuda_mtx
3
4
5
6
7
x0.464 x0.483
x0.486
x0.523
x0.595
x0.137
x0.142 x0.142
x0.138
x0.147
x0.145
x0.191
x0.559
x0.569 x0.566
x0.573
x0.612
x0.741
x0.132
x0.734
x0.725 x0.710
x0.666
x0.659
x0.715
cuda_mtx_op x0.190
x1.316
x1.454 x1.440
x1.349
x1.365
x1.501
eigen
x1
x1
x1
x1
x1
x1
octave
x0.157
x0.359
x0.391 x0.385
x0.397
x0.433
x0.515
x1
Tablica 5.10: Porównanie czasów wzgl˛edem implementacji eigen wyrażonych ilorazem opisanym
wzorem (5.1) (wartości > 1 oznaczaja˛ przyśpieszenie)
Diagram 5.5: Graficzne porównanie czasów wzgl˛edem implementacji eigen wyrażonych ilorazem opisanym wzorem (5.1) (wartości > 1 oznaczaja˛ przyśpieszenie)
Zastosowanie w praktyce
5.4.3
55
Omówienie wyników
Uzyskane wyniki znaczaco
˛ odbiegaja˛ od tych dla poprzedniego algorytmu. Analogiczna
dysproporcja w pierwszym teście pozostaje pomi˛edzy skryptowa,˛ a systemowa˛ implementacja.˛ Implementacja w j˛ezyku systemowym jest bezkonkurencyjna na małych
zbiorach danych, ale różnica już nie jest tak duża jak przy poprzednim algorytmie.
Różnica ta wynika z mniejszej ilości instrukcji wykonywanych w każdej p˛etli programu
co sumarycznie przekłada si˛e na mniejsze opóźnienia w porównaniu do algorytmu
wstecznej propagacji bł˛edu.
W pozostałych przypadkach testowych algorytm napisany w j˛ezyku C++ uzyskuje
najlepsze wyniki w środowisku homogenicznym, lecz różnica pomi˛edzy pozostałymi
implementacjami nie jest już tak znaczna jak dla pierwszego analizowanego algorytmu.
J˛ezyk Python i moduł numpy uzyskał wyniki średnio dwukrotnie gorsze niż eigen oraz
wynik lepszy o 20% od programu Octave. Dodatkowo dysproporcja ta maleje wraz ze
wzrostem wielkości zbioru danych.
Algorytm k-nn charakteryzuje si˛e wzgl˛ednie mała˛ złożonościa˛ obliczeniowa.˛ Wszystkie operacje na danych, z wykluczeniem operacji sortowania, maja˛ złożoność liniowa.˛
Ważna˛ własnościa˛ tego algorytmu jest duża wydajność przetwarzania danych przez
poszczególne operacje. Obie wymienione cechy sa˛ doskonale zobrazowane w uzyskanych wynikach. Różnica pomi˛edzy poszczególnymi implementacjami wynika z różnej
wydajności przetwarzania kolejnych kroków algorytmu. Z tego wzgl˛edu najlepszy wynik uzyskuje algorytm eigen, a implementacje w j˛ezyku skryptowym maja˛ zbliżona˛
wydajność. Charakterystyczna jest również malejaca
˛ różnica w czasach wykonania wraz
z wzrostem wielkości zbioru danych. Wynika to z faktu wzrostu udziału przetwarzania
danych, które sa˛ wykonywane ze zbliżona˛ wydajnościa˛ dla każdej z implementacji.
Na tle uzyskanych wyników wariant cpu_mtx znaczaco
˛ wyróżnia si˛e, uzyskujac
˛ siedmiokrotnie gorszy wynik niż eigen. Wynika to z konieczności wykonania licznych dodatkowych operacji w każdym przebiegu iteracji. Kompaktowa składnia programu wymusza
Zastosowanie w praktyce
56
wykonanie zb˛ednych alokacji i kopiowań danych, które w dużej mierze wpływaja˛ na
taki wynik. Wykorzystanie tego samego algorytmu w środowisku heterogenicznym
nie wykazuje już tak złych wyników. Wykonywanie takich obcia˛żajacych
˛
operacji jak
alokacja i kopiowanie danych na karcie graficznej przebiega w sposób asynchroniczny,
co pozwala ukryć te dodatkowe koszty.
Zoptymalizowany wariant uzyskuje wyniki lepsze od reszty skryptowych implementacji w środowisku homogenicznym. Warto zwrócić uwag˛e na zbieżność wyników
cpu_mtx_op i cuda_mtx. Pierwszy algorytm minimalizuje zb˛edne operacje kopiowania
i alokacji pami˛eci, drugi ukrywa je poprzez asynchroniczne wykonania.
Jedynie zoptymalizowana implementacja ze wsparciem procesora graficznego uzyskała
wyniki lepsze od programu napisanego w C++. Poprawa wydajności jest mniejsza niż ta
uzyskana dla sieci neuronowych i nie przekracza 50%. Tak mały wzrost wynika z małego
wpływu czasu przetwarzania danych do wydajności samego j˛ezyka przy interpretowaniu
kolejnych poleceń.
Rozdział 6
Zakończenie
Stworzony projekt oraz uzyskane wyniki pozwalaja˛ udzielić pozytywnej odpowiedzi
na zadane we wst˛epie pytania. Tak, połaczenie
˛
obu technologi pozwala na efektywne
wykorzystanie j˛ezyków skryptowych do przeprowadzania operacji numerycznych na
dużych zbiorach danych, przy zachowaniu niezależności skryptu od środowiska wykonawczego. Wykorzystanie procesora graficznego z pomoca˛ stworzonego narz˛edzia jest
równie wygodne jak samo programowanie w j˛ezyku skryptowym. Dodatkowo końcowe
rozwiazanie
˛
uzyskało wyniki lepsze od zakładanej wydajności j˛ezyków systemowych.
6.1
Wnioski z przetwarzania danych
Wydajna implementacja algorytmów operujacych
˛
na dużych zbiorach danych poprzez
operacje z algebry liniowej wymaga wyspecjalizowanego kodu zoptymalizowanego
pod dany problem. Generyczna implementacja nie zapewnia dostatecznej wydajności.
Z tego wzgl˛edu projekt wymagał dostosowania poszczególnych metod przez uwzgl˛ednienie różnych wariantów wykonania poszczególnych operacji. Takie optymalizacje
sa˛ koniecznie by biblioteka ogólnego zastosowania mogła konkurować z optymalnymi
implementacjami algorytmów. Próby zoptymalizowania biblioteki na potrzeby jednego
Zakończenie
58
algorytmu (np. k-nn) cz˛esto negatywnie wpływały na wyniki uzyskiwane przez inne
algorytmy (np. neural networks). Taka zależność wymusza liczne kompromisy w celu
stworzenia wydajnego, a zarazem uniwersalnego narz˛edzia. Możliwe jest stworzenie
optymalnej implementacji przystosowanej do każdego algorytmu, które polegałoby na
napisaniu licznych wariantów tej samej operacji zoptymalizowanej pod każdy z algorytmów. Jest to bardzo kosztowne rozwiazanie,
˛
które dodatkowo znaczaco
˛ komplikuje
sama˛ bibliotek˛e oraz nie daje gwarancji istotnego przyśpieszenia dla każdego problemu
w każdym środowisku.
6.2
Wnioski wynikajace
˛ z realizacji projektu
Realizacja projektu operujacego
˛
w ramach różnych architektur jest możliwa przy wykorzystaniu generycznego kodu wspólnego dla każdej z nich. Takie rozwiazanie
˛
pomimo
wielu zalet nie zapewnia dostatecznej wydajności, b˛edacej
˛ kluczowym aspektem takiego projektu. Uzyskanie wymaganej mocy obliczeniowej wymaga wyspecjalizowania
poszczególnych metod ze wzgl˛edu na stan macierzy jak i sama˛ operacj˛e w danym
środowisku wykonawczym. Takie optymalizacje prowadza˛ do niezależnych implementacji dla każdego środowiska. Te niezależne implementacje dla każdego środowiska
w dużej mierze sprowadzaja˛ połaczenie
˛
obu technologi do roli adaptera czy fasady odpowiednich bibliotek dla każdego ze środowisk. Istnieja˛ liczne biblioteki dla procesora
centralnego (CPU) realizujace
˛ postawione wymagania. Jednym z takich projektów jest
Eigen. Niestety dla środowiska graficznego (GPU) taka biblioteka jeszcze nie istnieje.
Potencjalnym kandydatem jest projekt CUV.
Stworzona biblioteka zakłada maksymalna˛ integracj˛e obu środowisk wykraczajac
˛ a˛ poza
funkcjonalność adaptera i fasady. Wypracowane rozwiazanie
˛
pozawala wykorzystać
wspólny kod dla licznych operacji niezależnych takich jak dodawanie wartości skalarnej
do macierzy. Dodatkowo generyczna implementacja każdej operacji pozwala w prosty
sposób rozbudować implementacj˛e o obsług˛e nowych typów danych i umożliwia póź-
Zakończenie
59
niejsza˛ niezależna˛ optymalizacj˛e każdej z metod. Wydajność takiego rozwiazania
˛
jest
zadowalajaca,
˛ dlatego implementacja tej biblioteki jako tylko adaptera już istniejacych
˛
bibliotek nie została zbadana. Prawdopodobnie rozwiazanie
˛
ograniczone do funkcji
adaptera, fasady łacz
˛ acej
˛ wydajne biblioteki uzyska lepsze wyniki kosztem wi˛ekszych
nakładów na oprogramowanie obsługi różnych typów danych.
Te wnioski moga˛ ulec znaczacej
˛ zmianie wraz z rozwojem technologii. Środowiska
CUDA w kolejnej wersji 6.0 ma wprowadzić wspólna˛ przestrzeń adresowa˛ dla CPU
i GPU, co może istotnie wpłynać
˛ na optymalna˛ realizacj˛e takiego połaczenia.
˛
6.3
Perspektywy rozwoju
Projekt dostarczył odpowiedzi na pytania zadane we wst˛epie tej pracy oraz ukazał
potencjał tkwiacy
˛ w połaczeniu
˛
j˛ezyków skryptowych z procesorem graficznym. Stworzona biblioteka zapewnia jedynie podstawowe operacje na macierzach. Jest to dobry
punkt wyjściowy do stworzenia uniwersalnego narz˛edzia operujacego
˛
w środowisku
heterogenicznym.
Pierwszym krokiem w tym celu powinna być rozbudowa o nowe metody, które zbliża˛
projekt do konkurencyjnych rozwiazań
˛
dla środowisk homogenicznych.
Kolejnym etapem jest uwzgl˛ednienie kolejnych wariantów wykonania i popraw˛e wydajności różnych operacji. W przypadku cz˛eści operujacej
˛ na procesorze można zrealizować to zadanie przez dalsza˛ integracj˛e z biblioteka˛ Eigen. Cz˛eść odpowiedzialna˛ za
obliczenia na GPGPU należy rozszerzyć o integracj˛e z biblioteka˛ cuBlas. Dodatkowo
warto rozbudować projekt o możliwość kompozycji operacji (wywołań), która umożliwi zmniejszenie opóźnień wynikajacych
˛
z komunikacji pomi˛edzy procesorami oraz
kosztownego przetwarzania poleceń w j˛ezyku Python.
Rozwiazaniem
˛
wartym implementacji jest dynamiczny wybór środowiska wykonawczego. Taki wybór uwzgl˛edniałby dost˛epne zasoby wielkość zbioru danych, jak i sama˛
Zakończenie
60
operacj˛e. Dla przykładu operacje na małych zbiorach danych wykonywane byłyby na
procesorze centralnym w celu unikni˛ecia opóźnień zwiazanych
˛
z komunikacja˛ pomi˛edzy
procesorami, natomiast do przetwarzania dużych zbiorów danych głównie wykorzystany
byłby procesor graficzny.
Bibliografia
[1] ArrayFire. http://www.accelereyes.com/products/arrayfire.
[2] BLAS. http://www.netlib.org/blas/.
[3] Boost. http://www.boost.org/.
[4] Boost.Python.
http://www.boost.org/doc/libs/1_54_0/libs/python/
doc/index.html.
[5] Copperhead. http://code.google.com/p/copperhead/.
[6] CUDA-Toolkit. http://developer.nvidia.com/cuda/cuda-toolkit.
[7] CUDAMat. http://code.google.com/p/cudamat/.
[8] CUV. https://github.com/deeplearningais/CUV.
[9] Cython. https://github.com/cython/cython.
[10] Eigen. http://eigen.tuxfamily.org.
[11] Encog. http://www.heatonresearch.com/encog.
[12] GPU-Accelerated-libraries.
http://developer.nvidia.com/cuda/
gpu-accelerated-libraries.
[13] GPUMLib. http://gpumlib.sourceforge.net/.
BIBLIOGRAFIA
62
[14] Matlab. http://www.mathworks.com/products/matlab/.
[15] MPL2. http://www.mozilla.org/MPL/2.0/.
[16] Numbra Pro. http://docs.continuum.io/numbapro/.
[17] Numpy. http://www.numpy.org/.
[18] Nvidia-CUDA. http://www.nvidia.com/object/cuda_home_new.html.
[19] Octave. http://www.gnu.org/software/octave/.
[20] OpenCL. http://www.khronos.org/opencl/.
[21] OpenMP Multi-Threaded Template Library. http://tech.unige.ch/omptl/.
[22] Python. http://www.python.org/.
[23] thrust. https://github.com/thrust/thrust.
[24] G. Aparício, I. Blanquer, V. Hernández. A parallel implementation of the k
nearest neighbours classifier in three levels: threads, MPI processes and the grid.
Proceedings of the 7th international conference on High performance computing
for computational science, VECPAR’06, strony 225–235, Berlin, Heidelberg, 2007.
Springer-Verlag. [Online]. Protokół dost˛epu: http://dl.acm.org/citation.
cfm?id=1761728.1761748.
[25] Richard O. Duda, Peter E. Hart, David G. Stork. Pattern Classification. John Wiley
& Sons, New York, NY, wydanie 2., 2001.
[26] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design patterns: elements of reusable object-oriented software. Addison-Wesley Longman Publishing
Co., Inc., Boston, MA, USA, 1995.
[27] Andreas Klöckner.
pycuda/.
PyCUCA.
http://mathema.tician.de/software/
BIBLIOGRAFIA
[28] Andreas Klöckner.
63
PyOpenCl.
http://mathema.tician.de/software/
pyopencl/.
[29] Canasai Kruengkrai, Chuleerat Jaruskulchai. A parallel learning algorithm for text
classification. Proceedings of the eighth ACM SIGKDD international conference
on Knowledge discovery and data mining, KDD ’02, strony 201–206, New York,
NY, USA, 2002. ACM. [Online]. Protokół dost˛epu: http://doi.acm.org/10.
1145/775047.775077.
[30] Andrew Rau-Chaplin Laurence Boxer, Russ Miller. Scalable Parallel Algorithms
for Geometric Pattern Recognition. Journal of Parallel and Distributed Computing,
(58):466–486, 1999.
[31] Rhonda D. Phillips, Layne T. Watson, Randolph H. Wynne. Hybrid image classification and parameter selection using a shared memory parallel algorithm. Computers
& Geosciences, 33(7):875–897, 2007.

Podobne dokumenty