Optymalizacja przetwarzania z wykorzystaniem

Transkrypt

Optymalizacja przetwarzania z wykorzystaniem
Optymalizacja przetwarzania z wykorzystaniem układów GPU i przetwarzania
równoległego
Aleksandra Knapik
//Rys historyczny
Zwiększanie mocy
obliczeniowej
komputerów
zawsze
stanowiło
dla
naukowców
i producentów istotny kierunek badań. Początkowo, wydajność poprawiano poprzez zwiększanie
częstotliwości taktowania zegara CPU. Następnie zaczęto dodawać kolejne jednostki przetwarzające,
tworząc klastry komputerowe, wykonujące obliczenia równolegle. Kolejny etap to rewolucja
wielordzeniowa, której zasięg wykroczył poza jednostki naukowe, do masowego odbiorcy.
Jednocześnie, podejmowano także próby obliczeń na układach graficznych przy użyciu shaderów
pikseli, walcząc z wieloma ograniczeniami programistycznymi. Powstał wtedy tzw. Nurt GPGPU (ang.
General -Purpose Computing on Graphics Processing Units), propagujący wykorzystanie zasobów
procesora karty graficznej do wielowątkowych obliczeń ogólnego przeznaczenia. Istotnym
przełomem było powstanie architektury CUDA (ang. Compute Unified Device Architecture),
rozszerzenia języka C o słowa kluczowe umożliwiające korzystanie z tej architektury w bardzo
wygodny sposób, a także kompilatora CUDA C.
//Architektura CUDA
Aktualnie, architektura CUDA pozwala tworzyć szybko działające programy, o szerokim
spektrum zastosowań. Zwiększa ich wydajność, w porównaniu z klasycznymi rozwiązaniami, nawet
o kilka rzędów wielkości. Ponadto, jest to rozwiązanie o znacznie lepszym stosunku jakości do ceny
oraz wydajności do ilości pobieranej mocy. Możliwe jest także wykonanie obliczeń heterogenicznych
(GPU + CPU), które dają często najlepsze efekty. Układy NVIDIA posiadają multiprocesorową
architekturę, z setkami jednostek arytmetyczno-logicznych. Urządzenia te zorientowane są na
równoległe wykonywanie instrukcji. CPU pracują, opierając się na architekturze równoległej typu
SIMD (ang. Single Instruction Stream, Multiple Data), natomiast GPU na architekturze SIMT(ang.
Single Instruction Stream, Multiple Threads). Karty graficzne wykorzystywane są więc z sukcesem
w przypadku obliczeń na dużych zbiorach danych, na których wykonuje się te same matematyczne
operacje. Im iloraz liczby operacji arytmetycznych do liczby odwołań do pamięci jest większy tym
lepsze wyniki uzyskamy.
//Jak to działa?
Model programowania CUDA składa się z hosta, który jest zwykle jednostką CPU oraz
z jednego lub wielu urządzeń(devices), będących układami GPU. Wykonywanie aplikacji rozpoczyna
się z poziomu hosta. Początkowo, dane zapisane w pamięci głównej zostają przekopiowane do
urządzenia. Następnie, w odpowiednich momentach, wywoływane są funkcje jądra (kernels) typu
void. Każdy kernel uruchamia zadaną liczbę wątków i wykonuje obliczenia. Kernel posiada zawsze
przynajmniej jeden z 3 klasyfikatorów dostępu:
 __global__ - oznacza uruchomienie z hosta i wykonanie na urządzeniu
 __device__ - oznacza uruchomienie z poziomu innego kernela, który jest aktualnie
wykonywany na urządzeniu; wykonywany na urządzeniu
 __host__ - oznacza uruchomienie z hosta i wykonanie na hoście, używany zazwyczaj
w połączeniu z __device__ do wygenerowania dwóch wersji danej funkcji
Wszystkie wątki uruchomione przez kernel na GPU nazywane są siatką (grid), która posiada dwa
wymiary. Składa się ona z
trójwymiarowych bloków (block). Każdy blok natomiast z wątków,
mogących współdzielić dane. Wątki mogą komunikować się miedzy sobą jednie w obrębie swojego
bloku, za to komunikują się one bardzo szybko poprzez wspólna pamięć. Bloki wątków mogą być
wykonywane w dowolnej kolejności. Zarówno wymiar siatki jak i organizacja wątków w blokach są
ustalane przez programistę przy wywoływaniu kernela. Do opisu siatki i bloku stosuje się zmienne
typu dim3. W momencie zakończenia działania wszystkich wątków danego kernela, grid zostaje
zamknięty, wyniki są przesyłane do pamięci głównej, a kierowanie programem zostaje przeniesione
na stronę hosta. W trakcie programu możemy wywołać dowolną liczbę kerneli.
W obliczeniach możemy wykorzystywać trzy rodzaje pamięci:
 Globalna – charakteryzuje się dużym rozmiarem, ale bardzo wolnym dostępem
 Lokalna – wykorzystywana jest na lokalne zmienne wątku
 Wspólna – dostępna dla wątków w ramach pojedynczego bloku, bardzo szybka
// Konstrukcja programów, techniki
Aby stworzyć program równoległy, tożsamy z sekwencyjnym kodem, niezbędna jest
umiejętność zrównoleglenia pętli programowych. W tym celu dzieli się zbiór iteracji pętli na części
i wysyła się je do poszczególnych wątków aplikacji. Należy wybierać fragmenty kodu wolne od
synchronizacji ( ang. Synchronization-Free Slices), w których nie istnieją zależności pomiędzy jego
operacjami i operacjami innych fragmentów kodu.
W aplikacjach wykorzystujących układy graficzne istotnym elementem jest przepustowość i dobra
organizacja czasu przesyłu danych między pamięcią operacyjną, a pamięcią karty graficznej. Jest to
czas wykonywania następujących czynności: alokacja pamięci na urządzeniu, wysyłanie danych do
pamięci urządzenia , odebranie danych z urządzenia. Im większa liczba procesorów czas
przetwarzania pętli maleje, jednak towarzyszy temu przyrost czasu przesyłu danych. Transfer danych
między hostem a urządzeniem okazuje się często wąskim gardłem aplikacji.
Skala optymalizacji poszczególnych algorytmów może być różna w zależności od złożoności obliczeń
i używanej platformy GPU. Funkcja obliczeniowa powinna być jak najmniejsza i ukierunkowana na jak
największa ilość równoległych obliczeń.
//CUDA a klasyfikacja
Klasyfikacja obrazów teledetekcyjnych jest zadaniem wymagającym dużych nakładów mocy
obliczeniowej.
Współczesne
mikroprocesory
ogólnego
zastosowania
nie
odznaczają
się
satysfakcjonującą wydajnością. Jak wiadomo, algorytmy klasyfikacji opierają się w dużej mierze na
obliczeniach iteracyjnych. Te same operacje wykonuje się niezależnie dla każdego z klasyfikowanych
pikseli. Najkorzystniejszym sposobem skrócenia czasu obliczeń jest więc zrównoleglenie pętli
programowych. Idąc z duchem czasu, naturalnym jest w tym przypadku skorzystanie z dobrodziejstw
jakie oferuje nam architektura współczesnych GPU.
// ManagedCUDA
Celem ManagedCUDA jest łatwa integracja technologii CUDA z aplikacjami napisanymi na
platformie .NET. Stanowi ona wrapper dla CUDA Driver API, zapewniając intuicyjny dostęp do tego
sterownika. ManagedCUDA jest zorientowana obiektowo. Główne klasy, wykorzystywane do
implementacji kodu to:

CudaContext – reprezentuje kontekst CUDA, CUDA API wymaga stworzenia instancji
przynajmniej jednego kontekstu na urządzenie. W konstruktorze można zdefiniować kilka
właściwości, jak np. ID urządzenia. CudaContext definiuje kilka metod statycznych, aby
pobrać informacje o urządzeniach CUDA.

CudaKernel – konstruktor, który przyjmuje trzy parametry, gdzie pierwszy to nazwa funkcji
w pliku .ptx, kolejno moduł ze ścieżką do pliku i odpowiedni kontekst. Należy pamiętać, że
nazwa metody w .ptx różni się od używanej w projekcie CUDA. Kernel jest automatycznie
niszczony jak tylko odpowiadający mu moduł jest niszczony.

CudaDeviceVariable – obiekt tej klasy reprezentuje pamięć alokowaną na urządzeniu, klasa
ta wie o dokładnym układzie pamięci. Upraszcza to bardzo kopiowanie danych. Nie jest
potrzebne podanie parametrów wielkości. Pamięć urządzenia jest zwalniana jak tylko obiekt
CudaDeviceVariable jest niszczony.

Inne : CudaPagelockedHostMemory, CudaPagelockedHostMemory_[Type],
CudaManagedMemory_[Type], CudaRegisteredHostMemory, CudaArray[1D,2D,3D],
CudaTextureFoo, GraphicsInterop
//Podsumowanie
Szczególnie ważnym aspektem jest przekształcenie istniejących algorytmów na takie, które
będą w stanie wykorzystać cały potencjał najnowszej generacji GPU. Technologia ta sprawdziła się już
w wielu dziedzinach, przy algorytmach o podobnej konstrukcji, gdzie wykorzystanie modelu SIMT i
układów graficznych okazało się najefektywniejszym czasowo rozwiązaniem.