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.