Wstęp do OpenCL. Czyli jak wykorzystać moc drzemiącą w GPU. Marek Zając 2013r. zajacmarek.com

Podobne dokumenty
Programowanie procesorów graficznych GPGPU

Podstawy programowania, Poniedziałek , 8-10 Projekt, część 1

Programowanie równoległe Wprowadzenie do OpenCL. Rafał Skinderowicz

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Programowanie procesorów graficznych NVIDIA (rdzenie CUDA) Wykład nr 1

Moc płynąca z kart graficznych

Lab 9 Podstawy Programowania

1 Podstawy c++ w pigułce.

Porównanie wydajności CUDA i OpenCL na przykładzie równoległego algorytmu wyznaczania wartości funkcji celu dla problemu gniazdowego

Programowanie akceleratorów specyfikacja OpenCL. Krzysztof Banaś Obliczenia równoległe 1

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Zmienne i struktury dynamiczne

Wskaźnik może wskazywać na jakąś zmienną, strukturę, tablicę a nawet funkcję. Oto podstawowe operatory niezbędne do operowania wskaźnikami:

Programowanie procesorów graficznych do obliczeń ogólnego przeznaczenia GPGPU

Zajęcia nr 5 Algorytmy i wskaźniki. dr inż. Łukasz Graczykowski mgr inż. Leszek Kosarzewski Wydział Fizyki Politechniki Warszawskiej

Programowanie w języku C++

CUDA Median Filter filtr medianowy wykorzystujący bibliotekę CUDA sprawozdanie z projektu

Cwiczenie nr 1 Pierwszy program w języku C na mikrokontroler AVR

1 Podstawy c++ w pigułce.

Tablice. Monika Wrzosek (IM UG) Podstawy Programowania 96 / 119

Podstawy programowania. Wykład: 9. Łańcuchy znaków. dr Artur Bartoszewski -Podstawy programowania, sem 1 - WYKŁAD

Struktury. Przykład W8_1

Programowanie Równoległe wykład, CUDA, przykłady praktyczne 1. Maciej Matyka Instytut Fizyki Teoretycznej

Programowanie Równoległe Wykład, CUDA praktycznie 1. Maciej Matyka Instytut Fizyki Teoretycznej

Ćwiczenie 4. Obsługa plików. Laboratorium Podstaw Informatyki. Kierunek Elektrotechnika. Laboratorium Podstaw Informatyki Strona 1.

7. Pętle for. Przykłady

PROE wykład 2 operacje na wskaźnikach. dr inż. Jacek Naruniec

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

METODY I JĘZYKI PROGRAMOWANIA PROGRAMOWANIE STRUKTURALNE. Wykład 02

Programowanie procesorów graficznych w CUDA.

Część 4 życie programu

Programowanie obiektowe i C++ dla matematyków

IMIĘ i NAZWISKO: Pytania i (przykładowe) Odpowiedzi

Metody Metody, parametry, zwracanie wartości

KLASA UCZEN Uczen imię, nazwisko, średnia konstruktor konstruktor Ustaw Wyswietl Lepszy Promowany

Programowanie Obiektowo Zorientowane w języku c++ Przestrzenie nazw

Programowanie obiektowe i C++ dla matematyków

I. WSTĘP. Przykład 1. Przykład 2. Programowanie czyli tworzenie programów komputerowych (aplikacji komputerowych)

KLASA UCZEN Uczen imię, nazwisko, średnia konstruktor konstruktor Ustaw Wyswietl Lepszy Promowany

Programowanie w C++ Wykład 11. Katarzyna Grzelak. 13 maja K.Grzelak (Wykład 11) Programowanie w C++ 1 / 30

PROE wykład 3 klasa string, przeciążanie funkcji, operatory. dr inż. Jacek Naruniec

Podczas dziedziczenia obiekt klasy pochodnej może być wskazywany przez wskaźnik typu klasy bazowej.

Zasady programowania Dokumentacja

Wskaźniki i dynamiczna alokacja pamięci. Spotkanie 4. Wskaźniki. Dynamiczna alokacja pamięci. Przykłady

Przygotowanie kilku wersji kodu zgodnie z wymogami wersji zadania,

Wstęp do obliczeń równoległych na GPU

Programowanie obiektowe W3

Systemy wirtualnej rzeczywistości. Komponenty i serwisy

Część XVII C++ Funkcje. Funkcja bezargumentowa Najprostszym przypadkiem funkcji jest jej wersja bezargumentowa. Spójrzmy na przykład.

I - Microsoft Visual Studio C++

1. Wartość, jaką odczytuje się z obszaru przydzielonego obiektowi to: a) I - wartość b) definicja obiektu c) typ oboektu d) p - wartość

Temat: Dynamiczne przydzielanie i zwalnianie pamięci. Struktura listy operacje wstawiania, wyszukiwania oraz usuwania danych.

Programowanie w C++ Wykład 5. Katarzyna Grzelak. 16 kwietnia K.Grzelak (Wykład 1) Programowanie w C++ 1 / 27

Programowanie komputerowe. Zajęcia 1

1. Wypisywanie danych

Pętle i tablice. Spotkanie 3. Pętle: for, while, do while. Tablice. Przykłady

Widoczność zmiennych Czy wartości każdej zmiennej można zmieniać w dowolnym miejscu kodu? Czy można zadeklarować dwie zmienne o takich samych nazwach?

Obsługa plików. Laboratorium Podstaw Informatyki. Kierunek Elektrotechnika. Laboratorium Podstaw Informatyki Strona 1. Kraków 2013

Techniki programowania INP001002Wl rok akademicki 2018/19 semestr letni. Wykład 3. Karol Tarnowski A-1 p.

Programowanie Współbieżne

Warsztaty AVR. Instalacja i konfiguracja środowiska Eclipse dla mikrokontrolerów AVR. Dariusz Wika

i3: internet - infrastruktury - innowacje

Niezwykłe tablice Poznane typy danych pozwalają przechowywać pojedyncze liczby. Dzięki tablicom zgromadzimy wiele wartości w jednym miejscu.

Zajęcia nr 2 Programowanie strukturalne. dr inż. Łukasz Graczykowski mgr inż. Leszek Kosarzewski Wydział Fizyki Politechniki Warszawskiej

Informatyka I : Tworzenie projektu

JAVA?? to proste!! Autor: wojtekb111111

Programowanie w języku C++ Grażyna Koba

Rekurencja (rekursja)

Programowanie w C++ Wykład 12. Katarzyna Grzelak. 28 maja K.Grzelak (Wykład 12) Programowanie w C++ 1 / 27

Aplikacja Sieciowa wątki po stronie klienta

Programowanie C++ Wykład 2 - podstawy języka C++ dr inż. Jakub Możaryn. Warszawa, Instytut Automatyki i Robotyki

Techniki programowania INP001002Wl rok akademicki 2018/19 semestr letni. Wykład 5. Karol Tarnowski A-1 p.

Budowa i generowanie planszy

Wyjątki. Wyjątki. Bogdan Kreczmer. Katedra Cybernetyki i Robotyki Politechnika Wrocławska

Wykład 1. Program przedmiotu. Programowanie Obiektowe (język C++) Literatura. Program przedmiotu c.d.:

Programowanie - wykład 4

Podstawy programowania. Wykład: 13. Rekurencja. dr Artur Bartoszewski -Podstawy programowania, sem 1 - WYKŁAD

DYNAMICZNE PRZYDZIELANIE PAMIECI

Podstawy programowania. Wykład 7 Tablice wielowymiarowe, SOA, AOS, itp. Krzysztof Banaś Podstawy programowania 1

1 Wskaźniki i zmienne dynamiczne, instrukcja przed zajęciami

Język JAVA podstawy. wykład 1, część 2. Jacek Rumiński. Politechnika Gdańska, Inżynieria Biomedyczna

Języki programowania. Przetwarzanie plików amorficznych Konwencja języka C. Część siódma. Autorzy Tomasz Xięski Roman Simiński

Jeśli chcesz łatwo i szybko opanować podstawy C++, sięgnij po tę książkę.

Pliki. Informacje ogólne. Obsługa plików w języku C

Programowanie komputerowe. Zajęcia 4

Dla każdej operacji łącznie tworzenia danych i zapisu ich do pliku przeprowadzić pomiar czasu wykonania polecenia. Wyniki przedstawić w tabelce.

Allegro5 część 1 - Witaj Świecie! Projekt, inicjalizacja, tworzenie okna, czcionki. Autor: Kamil Krzyszczuk - C mons

Podstawy programowania skrót z wykładów:

Pliki. Informacje ogólne. Obsługa plików w języku C

Programowanie dla początkujących w 24 godziny / Greg Perry, Dean Miller. Gliwice, cop Spis treści

WPROWADZENIE DO JĘZYKA JAVA

Pytania sprawdzające wiedzę z programowania C++

Programowanie obiektowe

Utworzenie pliku. Dowiesz się:

Ćwiczenie 1. Przygotowanie środowiska JAVA

Argumenty wywołania programu, operacje na plikach

Jak napisać listę jednokierunkową?

Zajęcia nr 1 Podstawy programowania. dr inż. Łukasz Graczykowski mgr inż. Leszek Kosarzewski Wydział Fizyki Politechniki Warszawskiej

4. Wyrzuć wyjątek jeśli zmienna ist nie istnieje bloki: try, catch i wyrzucanie wyjątku

public: // interfejs private: // implementacja // składowe klasy protected: // póki nie będziemy dziedziczyć, // to pole nas nie interesuje

Transkrypt:

Wstęp do OpenCL Czyli jak wykorzystać moc drzemiącą w GPU Marek Zając 2013r. zajacmarek.com

Przez większość użytkowników komputerów karta graficzna kojarzona jest jedynie jako element, który służy do wyświetlania grafiki. Jednak każdy kto lepiej orientuje się w nowoczesnych technologiach wie, że obecnie nie jest to jedyne zastosowanie tych układów (w dodatku bardzo uproszczone, bo jak wiadomo pod pojęciem "wyświetlanie grafiki" kryją się wszystkie operacje związane z przetworzeniem danych na piksele ekranu). Chociaż z GPGPU można było już korzystać na przełomie XX i XXI wieku (rok 1999) do jednak dopiero kiedy Nvidia stworzyła w 2006 roku technologię CUDA procesory graficzne coraz częściej zaczęły być wykorzystywane jako bardzo wydajne jednostki obliczeniowe ogólnego przeznaczenia. Zaś w roku 2009 powstała wersja 1.0 standardu OpenCL, który pozwolił na użycie tego samego kodu do obliczeń zarówno na kartach firmy Nvidia, jak i AMD (dawniej ATI), w przeciwieństwie do CUDA, które ograniczone było do kart tylko pierwszego z producentów. I właśnie o pierwszych krokach z OpenCL będzie ten wpis. 1. Wstęp Na początku warto zaznaczyć, że chociaż na GPU mogą być teoretycznie wykonywane dowolne obliczenia to jednak dopiero przy algorytmach dających możliwość obliczeń równoległych ta technologia pokazuje swoją przewagę. Wynika to z budowy samych układów, które posiadają bardzo dużą liczbę rdzeni, jednak każdy z nich jest w pewien sposób mniej wydajny niż zwykły rdzeń CPU (po więcej informacji odsyłam np. do Wikipedii). Dosyć istotną informacją jest to, że kernele OpenCL mogą być wykonywane zarówna na karcie graficznej jak i na procesorze głównym, co ma tą zaletę, że pozwala stosować w większości ten sam kod również na maszynach nie mających układów bezpośrednio wspierających tą technologię. W poniższym tekście nie będę opisywał wszystkich kroków związanych np. z dodawaniem ścieżek do plików w IDE, a jedynie krótkie informacje co należy dodać. Zakładam, że zainteresowani potrafią posługiwać się używanym IDE lub ostatecznie potrafią korzystać z wyszukiwarki Google. Zakładam również, że czytelnik zna język C++. Poniższy tekst oparłem głównie o tutorial wprowadzający ze strony AMD, jednak znalazłem w nim kilka nieścisłości i braków, z którymi musiałem się uporać dlatego myślę, że również tutaj każdy znajdzie coś przydatnego. 2. Czego potrzebujemy Na początku musimy zaopatrzyć się w SDK producenta naszej karty, które zawierać będzie pliki potrzebne do budowania programów z wykorzystaniem OpenCL. W moim przypadku jest to SDK dostarczone przez AMD. Poniżej podaję linki dla AMD i Nvidii: AMD: http://developer.amd.com/tools-and-sdks/heterogeneous-computing/amd-acceleratedparallel-processing-app-sdk/downloads/ Nvidia: https://developer.nvidia.com/cuda-downloads Jak widać w przypadku Nvidii OpenCL obsługiwany jest przez pakiet CUDA. Dodatkowo taka uwaga, że, przynajmniej w przypadku AMD (nie wiem jak jest z SDK Nvidii), nawet jeśli chcemy budować

program dla platformy x86, to mając system 64 bitowy ściągamy SDK przygotowane właśnie pod 64 bity ponieważ inaczej instalator nie uruchomi się. Mając już SDK wypadałoby się teraz zaopatrzyć w jakieś IDE, no chyba, że ktoś bardzo lubi kompilację z poziomu konsoli i pisanie kodu w osobnym edytorze bez np. podpowiadania składni. Ponieważ pracuję pod systemem Windows 7 jako IDE wybrałem Visual C++ 2010 Express, z którego korzystam już od dłuższego czasu. Kiedy posiadamy już odpowiednie narzędzia czas zacząć działać. 3. Pierwszy program Pierwszy program jaki napiszemy korzystając z OpenCL nie będzie jeszcze wykonywał żadnych obliczeń. Na początku wyświetlimy jedynie dostępne urządzenia, które będą mogły służyć jako jednostki obliczeniowe. Tworzymy więc nowy projekt konsolowy w Visual Studiu. Dodajemy w ustawieniach projektu ścieżkę do plików nagłówkowych biblioteki OpenCL (w przypadku AMD jest to folder include znajdujący się w folderze z SDK). Dodajemy również w opcjach linekra ścieżkę do folderu z plikami lib (jeśli kompilujemy pod platformę 32 bitową to wybieramy folder z bibliotekami x86) oraz dodajemy do linkera plik OpenCL.lib. Teraz mając skonfigurowany projekt możemy zająć się kodem. Na samym początku definiujemy, że nie będziemy używali wektora ze standardowej biblioteki C++, zamiast niego OpenCL będzie korzystał z własnego wektora. #define NO_STD_VECTOR Teraz musimy dodać potrzebne includy #include <CL/cl.hpp> #include <iostream> Użyłem tutaj pliku cl.hpp zamiast cl.h ponieważ uznałem, że od razu będę korzystał z wersji obiektowej przygotowanej dla C++. Czas przejść do funkcji main. Tworzymy zmienną err typu cl_int (odpowiednik zwykłego inta zawarty w bibliotece OpenCL) oraz wektor przechowujący typ cl::platform. cl_int err; cl::vector<cl::platform> platformlist; Za pomocą funkcji get() z klasy cl::platform pobieramy dostępne platformy i wyświetlamy ich ilość cl::platform::get(&platformlist); std::cerr<<"platform number is: " << platformlist.size() <<std::endl; Kiedy już pobraliśmy dostępne platformy czas wyświetlić jakieś informacje na ich temat. Będziemy to robić w pętli for iterującej od 0 do platformlist.size() -1. Na początek informacja o producencie platformy. Pobieramy ją za pomocą funkcji getinfo() dostępnej w klasie cl::platform

std::string platformvendor; platformlist[i].getinfo((cl_platform_info)cl_platform_vendor, &platformvendor); std::cerr<<"platform is by: " <<platformvendor << "\n"; Oprócz CL_PLATFORM_VENDOR mogliśmy też użyć np. CL_PLATFORM_VERSION lub CL_PLATFORM_NAME, które zwróciły by odpowiednio wersję i nazwę platformy. Warto w tym miejscu powiedzieć, że platformą jest po prostu zainstalowany sterownik OpenCL. Następnie tworzymy właściwości kontekstu, w których podajemy dla jakiej platformy będziemy go tworzyli cl_context_properties cprops[3] = CL_CONTEXT_PLATFORM, (cl_context_properties) (platformlist[i])(), 0 ; I samo utworzenie kontekstu cl::context context(cl_device_type_all, cprops, NULL, NULL, &err); Parametr CL_DEVICE_TYPE_ALL określa jakiego typu urządzenia chcemy obsługiwać, w tym wypadku wybrane będzie zarówno GPU jak i CPU. Jednak w kolejnych przykładach będę korzystał jedynie z GPU, a więc w tym miejscu znajdzie się wartość CL_DEVICE_TYPE_GPU. Mając już utworzony kontekst możemy pobrać informacje o wszystkich urządzeniach jakie obsługuje. A służy do tego szablonowa funkcja getinfo<>() klasy cl::context, która w tym przypadku jako nazwę typu przyjmuje CL_CONTEXT_DEVICES cl::vector<cl::device> devices; devices = context.getinfo<cl_context_devices>(); Teraz zostało już tylko wypisanie informacji o dostępnych urządzeniach. Ja w przykładzie pobieram tylko nazwę każdego z nich jednak możliwe jest również pobranie np. wersji lub producenta, tak samo jak w przypadku platformy for(int j = 0; j < devices.size(); j++) std::string name; devices[j].getinfo(cl_device_name, &name); std::cout<< "Device " << j << " - " << name <<std::endl; Na koniec tylko standardowe zatrzymanie programu (jeśli programujesz pod linuxem nie masz problemu ze znikającą konsolą po wykonaniu się programu dlatego możesz pominąć oczekiwanie na wciśnięcie klawisza) i zakończenie funkcji main() std::cin.get(); return 0;

Cały kod tego przykładu prezentuje się następująco: #define NO_STD_VECTOR #include <CL/cl.hpp> #include <iostream> int main() cl_int err; cl::vector<cl::platform> platformlist; cl::platform::get(&platformlist); std::cerr<<"platform number is: " << platformlist.size() <<std::endl; for(int i = 0; i < platformlist.size(); i++) std::string platformvendor; platformlist[i].getinfo((cl_platform_info)cl_platform_vendor, &platformvendor); std::cerr<<"platform is by: " <<platformvendor << "\n"; cl_context_properties cprops[3] = CL_CONTEXT_PLATFORM, (cl_context_properties) (platformlist[i])(), 0 ; cl::context context(cl_device_type_all, cprops, NULL, NULL, &err); cl::vector<cl::device> devices; devices = context.getinfo<cl_context_devices>(); for(int j = 0; j < devices.size(); j++) std::string name; devices[j].getinfo(cl_device_name, &name); std::cout<< "Device " << j << " - " << name <<std::endl; std::cin.get(); return 0; A wynik jego wykonania na moim komputerze wygląda tak:

Jak można zauważyć, dostępną mam jedną platformę (AMD APP SDK), która posiada 2 urządzenia. Tajemniczy Pitcairn to po prostu nazwa rdzenia użytego w moim Radeonie HD7870 (nie wiem czemu wyświetla tą nazwę zamiast nazwy karty, ale to nie istotne), drugie urządzenie to procesor Intela i5-2550k. 4. Pora coś policzyć Poprzedni program był tylko wstępem, który pozwolił sprawdzić czy wszystko jest dobrze skonfigurowane. Teraz przejdę do bardziej praktycznej części. W końcu wykonamy jakieś obliczenia na GPU. W tym wypadku będzie to proste mnożenie elementów dwóch tablic i przypisywanie wyniku do trzeciej. Niby niewiele, ale jest to wystarczający przykład aby poznać ideę obliczeń z wykorzystaniem GPU. Nie rozpisując się za bardzo przejdźmy do samego programu. Standardowo zaczynamy od dołączenia potrzebnych plików nagłówkowych. Jest ich trochę więcej niż w poprzednim przykładzie. #include <utility> #include <cstdio> #include <cstdlib> #include <cstdio> #include <fstream> #include <iostream> #include <iterator> #define NO_STD_VECTOR #include <CL/cl.hpp> Teraz funkcja, którą wziąłem z tutoriala AMD. Pozwala ona wypisać w konsoli wiadomość i numer błędu. Zastępuje if-a, którego by trzeba użyć do sprawdzania błędów, przez co kod jest trochę czytelniejszy. Jednak i tak najlepszym rozwiązaniem byłoby użycie wyjątków, ale nie jest to istotą tego tekstu. inline void checkerr(cl_int err, const char* name) if(err!= CL_SUCCESS) std::cerr<<"error: " << name << " (" << err << ")" <<std::endl;

std::cin.get(); exit(exit_failure); Kolejną przydatną funkcją będzie funkcja losująca wartości w tablicy. Ona również została napisana jedynie w celu zwiększenia przejrzystości kodu void randomvalues(cl_int* data, cl_int maxvalue, int size) for(int i = 0; i < size; i++) data[i] = rand() % maxvalue; Jeśli jesteśmy już przy wypełnianiu tablicy to warto jeszcze dodać sobie dwie stałe, które będą określały wielkość wykorzystywanych tablic i maksymalną wartość jaką będą mogły mieć ich elementy const int arraysize = 20; const int maxvalue = 10; W końcu przyszedł czas na funkcję main(). Początek wygląda podobnie jak poprzednio. Jest tutaj pobranie wszystkich dostępnych platform, wypisanie informacji na temat pierwszej z nich, a następnie utworzenie kontekstu na jej podstawie cl_int err; cl::vector<cl::platform> platformlist; cl::platform::get(&platformlist); checkerr(platformlist.size()!= 0? CL_SUCCESS : -1, "cl::platform::get"); std::cerr<<"platform number is: " << platformlist.size() <<std::endl; std::string platformvendor; platformlist[0].getinfo((cl_platform_info)cl_platform_vendor, &platformvendor); std::cerr<<"platform is by: " <<platformvendor << "\n"; cl_context_properties cprops[3] = CL_CONTEXT_PLATFORM, (cl_context_properties) (platformlist[0])(), 0 ; cl::context context(cl_device_type_gpu, cprops, NULL, NULL, &err); checkerr(err, "Context::Context()"); Kiedy udało się utworzyć prawidłowo kontekst czas na utworzenie buforów, które posłużą do wymiany danych pomiędzy naszym programem, a kernelem int* A = new int[arraysize]; int* B = new int[arraysize];

int* out = new int[arraysize]; randomvalues(a, maxvalue, arraysize); randomvalues(b, maxvalue, arraysize); cl::buffer ina(context, CL_MEM_READ_ONLY, sizeof(int) * arraysize); cl::buffer inb(context, CL_MEM_READ_ONLY, sizeof(int) * arraysize); cl::buffer outcl(context, CL_MEM_WRITE_ONLY, sizeof(int) * arraysize); Najpierw utworzyliśmy standardowe tablice intów. Dwie z nich, które posłużą jako dane wejściowe zostały wypełnione losowymi wartościami. Ostatnia posłuży do zapisania wartości zwróconych przez kernel. W konstruktorze bufora podajemy kontekst, flagę oznaczającą czy będą to dane tylko do odczytu (CL_MEM_READ_ONLY) czy zapisu (CL_MEM_READ_ONLY) oraz rozmiar bufora (Tutaj uwaga: podczas prób doszedłem do tego, że jeśli korzystamy z urządzeń GPU to wielkością bufora musi być sizeof(int) * arraysize, zaś jeśli korzystamy z CPU to wielkość bufora ustawiamy jedynie na arraysize. Niestety nie udało mi się jak na razie znaleźć informacji dlaczego tak się dzieje). Teraz pobieramy informację o dostępnych urządzeniach cl::vector<cl::device> devices; devices = context.getinfo<cl_context_devices>(); checkerr(devices.size() > 0? CL_SUCCESS : -1, "devices.size() > 0"); W tym momencie następuje otwarcie pliku.cl z kodem naszego kernela, który podam na końcu i wczytanie go do zmiennej typu std::string. std::ifstream file("multiply.cl"); checkerr(file.is_open()? CL_SUCCESS : -1, "lesson1_kernel.cl"); std::string prog(std::istreambuf_iterator<char>(file), (std::istreambuf_iterator<char>())); Tworzymy zmienną przechowującą źródła kernela i przekazujemy jej wczytany przed chwilą ciąg znaków cl::program::sources source(1, std::make_pair(prog.c_str(), prog.length() + 1)); Teraz utworzymy zmienną reprezentującą program OpenCLa i zbudujemy go cl::program program(context, source); err = program.build(devices, ""); checkerr(err, "Program.build()"); Stwórzmy teraz kolejkę poleceń, i za jej pomocą wypełnijmy bufory wejściowe wartościami znajdującymi się w tablicach A i B cl::commandqueue queue(context, devices[0], 0, &err); checkerr(err, "CommandQueue::CommandQueue()"); queue.enqueuewritebuffer(ina, CL_TRUE, 0, sizeof(int) * arraysize, A);

queue.enqueuewritebuffer(inb, CL_TRUE, 0, sizeof(int) * arraysize, B); Pierwszym parametrem jest stworzony wcześniej bufor, który chcemy wypełnić, następnie wartość CL_TRUE oznacza, że czynność będzie blokująca, a więc program nie przejdzie dalej dopuki nie skończy tej operacji, później jest offset, rozmiar bufora i na końcu tablica, z której pobierzemy dane. W takim razie nadeszła w końcu pora na utworzenie zmiennej reprezentującej kernel czyli funkcję zapisaną w naszym pliku.cl. Jednym z parametrów jest nazwa tej funkcji, którą mamy zamiar użyć cl::kernel kernel(program, "multiply", &err); Kiedy utworzyliśmy już kernel czas przekazać mu parametry z jakimi zostanie uruchomiony kernel.setarg(0, ina); kernel.setarg(1, inb); kernel.setarg(2, outcl); kernel.setarg(3, arraysize); Pierwszy parametr w funkcji setarg() mówi, który argument mamy zamiar zmieniać, gdzie 0 oznacza pierwszy parametr, zaś 3 ( w tym wypadku) ostatni. Czas na wykonanie w końcu obliczeń. Najpierw tworzymy zmienną cl::event, która pozwoli nam poczekać na zakończenie wykonywania się kernela korzystając do tego z funkcji wait(), która zablokuje program dopóki nie zostanie wywołane zdarzenie. Sam kernel uruchamiamy funkcją enqueuendrangekernel(). Ważniejsze parametry to kernel, który chcemy wykonać, ilość danych (zauważ, że teraz już nie mnożymy ilości elementów przez rozmiar typu) określająca ile razy kernel zostanie uruchomiony, ilość wątków przypadających na jedną jednostkę obliczeniową, tworzą one grupę, wewnątrz której mogą się synchronizować, jeśli wszystkie wątki wykonują tą samą operację to robią to równolegle, jeśli część z nich, np. z powodu użycia instrukcji if wykonuje inne operacje to wszystkie wątki w grupie wykonają się sekwencyjnie, a ostatni parametr to wskaźnik na naszą zmienną event, który pozwoli wywołać zdarzenie po skończeniu operacji. cl::event event; err = queue.enqueuendrangekernel(kernel, cl::nullrange, cl::ndrange(arraysize), cl::ndrange(1), NULL, &event); checkerr(err, "ComamndQueue::enqueueNDRangeKernel()"); Teraz zostało tylko poczekanie na zakończenie obliczeń przez GPU i pobranie wyników z bufora event.wait(); err = queue.enqueuereadbuffer(outcl, CL_TRUE, 0, sizeof(int) * arraysize, out); checkerr(err, "ComamndQueue::enqueueReadBuffer()"); Na sam koniec wypisujemy wyniki w konsoli, czyścimy pamięć i wychodzimy z funkcji main() for(int i = 0; i < arraysize; i++) std::cout<<a[i]<<" * "<<B[i]<<" = "<<out[i]<<std::endl;

std::cin.get(); delete[] out; delete[] A; delete[] B; return 0; Została jeszcze kwestia pliku multiply.cl, w końcu jest on bardzo ważnym elementem całości i to w nim zapisane mamy operacje, jakie ma wykonać nasze GPU. Prezentuje się on następująco kernel void multiply( global int* ina, global int* inb, global int* out, int size) size_t id = get_global_id(0); if(id < size) out[id] = ina[id] * inb[id]; Mamy tutaj tylko jedną funkcję multiply(), która jako parametry przyjmuje trzy wskaźniki oraz jedną wartość. global oznacza, że jest to wskaźnik na bufor za alokowany w globalnej puli pamięci dostępnej dla urządzenia. Funkcja get_global_id(0) pobiera unikalne id uruchomionego kernela, dzięki czemu możemy określić, do którego elementu tablicy będziemy się odwoływać, ponieważ jeden uruchomiony kernel wykonuje tutaj operacje na jednym elemencie. Jeśli wszystko zrobiliśmy tak jak trzeba to w naszym przypadku get_global_id(0) powinno zawsze dać wartości od 0 do arraysize - 1 jednak dobrym nawykiem jest sprawdzanie tego przy pomocy instrukcji if. A tak prezentuje się cały kod naszego programu #include <utility> #include <cstdio> #include <cstdlib> #include <cstdio> #include <fstream> #include <iostream> #include <iterator> #define NO_STD_VECTOR #include <CL/cl.hpp> inline void checkerr(cl_int err, const char* name) if(err!= CL_SUCCESS) std::cerr<<"error: " << name << " (" << err << ")" <<std::endl; std::cin.get(); exit(exit_failure); void randomvalues(cl_int* data, cl_int maxvalue, int size)

for(int i = 0; i < size; i++) data[i] = rand() % maxvalue; const int arraysize = 20; const int maxvalue = 10; int main() cl_int err; cl::vector<cl::platform> platformlist; cl::platform::get(&platformlist); checkerr(platformlist.size()!= 0? CL_SUCCESS : -1, "cl::platform::get"); std::cerr<<"platform number is: " << platformlist.size() <<std::endl; std::string platformvendor; platformlist[0].getinfo((cl_platform_info)cl_platform_vendor, &platformvendor); std::cerr<<"platform is by: " <<platformvendor << "\n"; cl_context_properties cprops[3] = CL_CONTEXT_PLATFORM, (cl_context_properties) (platformlist[0])(), 0 ; cl::context context(cl_device_type_gpu, cprops, NULL, NULL, &err); checkerr(err, "Context::Context()"); int* A = new int[arraysize]; int* B = new int[arraysize]; int* out = new int[arraysize]; randomvalues(a, maxvalue, arraysize); randomvalues(b, maxvalue, arraysize); cl::buffer ina(context, CL_MEM_READ_ONLY, sizeof(int) * arraysize); cl::buffer inb(context, CL_MEM_READ_ONLY, sizeof(int) * arraysize); cl::buffer outcl(context, CL_MEM_WRITE_ONLY, sizeof(int) * arraysize); cl::vector<cl::device> devices; devices = context.getinfo<cl_context_devices>(); checkerr(devices.size() > 0? CL_SUCCESS : -1, "devices.size() > 0"); std::ifstream file("multiply.cl"); checkerr(file.is_open()? CL_SUCCESS : -1, "multiply.cl");

std::string prog(std::istreambuf_iterator<char>(file), (std::istreambuf_iterator<char>())); cl::program::sources source(1, std::make_pair(prog.c_str(), prog.length() + 1)); cl::program program(context, source); err = program.build(devices, ""); checkerr(err, "Program.build()"); cl::commandqueue queue(context, devices[0], 0, &err); checkerr(err, "CommandQueue::CommandQueue()"); queue.enqueuewritebuffer(ina, CL_TRUE, 0, sizeof(int) * arraysize, A); queue.enqueuewritebuffer(inb, CL_TRUE, 0, sizeof(int) * arraysize, B); cl::kernel kernel(program, "multiply", &err); kernel.setarg(0, ina); kernel.setarg(1, inb); kernel.setarg(2, outcl); kernel.setarg(3, arraysize); cl::event event; err = queue.enqueuendrangekernel(kernel, cl::nullrange, cl::ndrange(arraysize), cl::ndrange(1), NULL, &event); checkerr(err, "ComamndQueue::enqueueNDRangeKernel()"); event.wait(); err = queue.enqueuereadbuffer(outcl, CL_TRUE, 0, sizeof(int) * arraysize, out); checkerr(err, "ComamndQueue::enqueueReadBuffer()"); for(int i = 0; i < arraysize; i++) std::cout<<a[i]<<" * "<<B[i]<<" = "<<out[i]<<std::endl; std::cin.get(); delete[] out; delete[] A; delete[] B; return 0; Wynik jego wykonania:

5. Zakończenie Tym oto sposobem opisałem podstawy budowania programów wykorzystujących do obliczeń GPU i bibliotekę OpenCL. Mam nadzieję, że ta treść przyda się komuś. Jeśli znalazłeś błąd to śmiało pisz o nim w komentarzu lub na maila contact@zajacmarek.com. Jeśli chciałbyś gdzieś wykorzystać fragmenty tego wpisu to również proszę o wcześniejszy kontakt. Kopiowanie części lub całości bez mojej zgody jest zabronione.