Programowanie GPU z C ++

Programowanie GPU z C ++

Przegląd

W tym przewodniku zbadamy moc programowania GPU z C++. Deweloperzy mogą oczekiwać niesamowitej wydajności z C ++, a dostęp do fenomenalnej mocy GPU z językiem niskiego poziomu może zapewnić jedne z najszybszych obecnie dostępnych obliczeń.

Wymagania

Podczas gdy każda maszyna zdolna do uruchomienia nowoczesnej wersji Linux może obsługiwać kompilator C ++, potrzebujesz procesora graficznego opartego na NVIDIA. Jeśli nie masz procesora graficznego, możesz zwrócić instancję zasilaną przez GPU w Amazon Web Services lub innego wybranego przez ciebie dostawcy chmury.

Jeśli wybierzesz maszynę fizyczną, upewnij się, że masz zainstalowane kierowców zastrzeżonych NVIDIA. Możesz znaleźć instrukcje tutaj: https: // linuxhint.com/install-nvidia-drivers-linux/

Oprócz sterownika będziesz potrzebować zestawu narzędzi CUDA. W tym przykładzie użyjemy Ubuntu 16.04 LTS, ale dostępne są pobieranie większości głównych dystrybucji w następującym adresie URL: https: // deweloper.nvidia.COM/CUDA-DOWNLOODS

Dla Ubuntu wybierzesz .Pobieranie oparte na Deb. Pobrany plik nie będzie miał .Domyślnie rozszerzenie deb, więc polecam zmianę nazwy na posiadanie .Deb na końcu. Następnie możesz zainstalować z:

sudo dpkg -i -plecak -nazwa.Deb

Prawdopodobnie zostaniesz poproszony o zainstalowanie klawisza GPG, a jeśli tak, postępuj zgodnie z podanymi instrukcjami.

Kiedy to zrobisz, zaktualizuj swoje repozytoria:

Sudo apt-get Aktualizacja
sudo apt -get install cuda -y

Po zakończeniu polecam ponowne uruchomienie, aby upewnić się, że wszystko jest odpowiednio załadowane.

Korzyści z rozwoju GPU

PROPS obsługuje wiele różnych danych wejściowych i wyjść i zawierają duży asortyment funkcji nie tylko radzenia sobie z szerokim asortymentem potrzeb programu, ale także do zarządzania różnymi konfiguracją sprzętową. Obsługują również pamięć, buforowanie, autobus systemowy, segmentowanie i funkcje IO, co czyni je podnośnikiem wszystkich transakcji.

GPU są odwrotnie - zawierają wiele indywidualnych procesorów, które koncentrują się na bardzo prostych funkcjach matematycznych. Z tego powodu przetwarzają zadania wiele razy szybciej niż CPU. Specjalizując się w funkcjach skalarnych (funkcja, która wymaga jednego lub więcej danych wejściowych, ale zwraca tylko jedno dane wyjściowe), osiągają ekstremalną wydajność kosztem ekstremalnej specjalizacji.

Przykładowy kod

W przykładowym kodzie dodajemy wektory razem. Dodałem wersję CPU i GPU kodu do porównania prędkości.
Przykład GPU.CPP Zawartość poniżej:

#include "cuda_runtime.H"
#włączać
#włączać
#włączać
#włączać
#włączać
typedef std :: chrono :: high_resolution_clock zegar;
#definicja ITER 65535
// Wersja CPU funkcji wektora Dodaj
void vector_add_cpu (int *a, int *b, int *c, int n)
int i;
// Dodaj elementy wektorowe A i B do wektora C
dla (i = 0; i < n; ++i)
c [i] = a [i] + b [i];


// Wersja GPU funkcji wektorowej
__Global__ void vector_add_gpu (int *gpu_a, int *gpu_b, int *gpu_c, int n)
int i = ThreadIdx.X;
// nie potrzebne do pętli, ponieważ czas wykonawczy CUDA
// będzie przeglądać czasy itera
gpu_c [i] = gpu_a [i] + gpu_b [i];

int main ()
int *a, *b, *c;
int *gpu_a, *gpu_b, *gpu_c;
a = (int *) malloc (iter * sizeof (int));
b = (int *) malloc (iter * sizeof (int));
c = (int *) malloc (iter * sizeof (int));
// Potrzebujemy zmiennych dostępnych dla GPU,
// tak cudamalloCanaged zapewnia te
cudamalloCanaged (& gpu_a, iter * sizeof (int));
cudamalloCanaged (& gpu_b, iter * sizeof (int));
cudamallocmanaged (& gpu_c, iter * sizeof (int));
dla (int i = 0; i < ITER; ++i)
a [i] = i;
B [i] = i;
c [i] = i;

// wywołać funkcję procesora i czas
auto cpu_start = clock :: now ();
vector_add_cpu (a, b, c, iter);
auto cpu_end = clock :: now ();
STD :: Cout << "vector_add_cpu: "
<< std::chrono::duration_cast(CPU_END - CPU_START).liczyć()
<< " nanoseconds.\n";
// Wywołaj funkcję GPU i czas
// Triple Angle Brakets to przedłużenie środowiska wykonawczego CUDA, które pozwala
// Parametry wezwania jądra CUDA, które ma zostać przekazane.
// W tym przykładzie przekazujemy jeden blok wątków z wątkami itera.
auto gpu_start = clock :: now ();
wektor_add_gpu <<<1, ITER>>> (GPU_A, GPU_B, GPU_C, ITER);
cudadadevicesynchronize ();
auto gpu_end = clock :: now ();
STD :: Cout << "vector_add_gpu: "
<< std::chrono::duration_cast(GPU_END - GPU_START).liczyć()
<< " nanoseconds.\n";
// Uwolnij przydziały pamięci oparte na funkcji GPU
cudafree (a);
cudafree (b);
cudafree (c);
// Uwolnij przydziały pamięci oparte na funkcji procesora
darmowe (a);
darmowe (b);
darmowe (c);
powrót 0;

Makefile Zawartość poniżej:

Inc = -i/usr/local/cUDA/wlicz
Nvcc =/usr/local/cUDA/bin/nvcc
Nvcc_opt = -Std = C ++ 11
Wszystko:
$ (NVCC) $ (NVCC_OPT) GPU-Example.CPP -O GPU -przykład
czysty:
-RM -F GPU -przykład

Aby uruchomić przykład, skompiluj go:

robić

Następnie uruchom program:

./GPU-przykład

Jak widać, wersja CPU (wektor_add_cpu) działa znacznie wolniej niż wersja GPU (vector_add_gpu).

Jeśli nie, może być konieczne dostosowanie definicji itera w próbce GPU.Cu do wyższej liczby. Wynika to z tego, że czas konfiguracji GPU jest dłuższy niż niektóre mniejsze pętle intensywnie intensywnie. Znalazłem 65535, aby dobrze działać na mojej maszynie, ale twój przebieg może się różnić. Jednak po wyczyszczeniu tego progu GPU jest dramatycznie szybszy niż procesor.

Wniosek

Mam nadzieję, że wiele się nauczyłeś z naszego wprowadzenia do programowania GPU z C++. Powyższy przykład nie osiąga zbyt wiele, ale pokazane pojęcia stanowią ramy, których możesz użyć do włączenia swoich pomysłów, aby uwolnić moc GPU.