Programowanie GPU w C++

Gpu Programming With C



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

Wymagania

Chociaż każda maszyna, na której można uruchomić nowoczesną wersję systemu Linux, może obsługiwać kompilator C ++, do wykonania tego ćwiczenia będziesz potrzebować procesora graficznego opartego na NVIDIA. Jeśli nie masz procesora graficznego, możesz uruchomić instancję zasilaną przez GPU w Amazon Web Services lub innym wybranym przez siebie dostawcy chmury.







Jeśli wybierzesz maszynę fizyczną, upewnij się, że masz zainstalowane zastrzeżone sterowniki NVIDIA. Instrukcje na ten temat znajdziesz 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ą pliki do pobrania dla większości głównych dystrybucji pod następującym adresem URL: https://developer.nvidia.com/cuda-downloads



W przypadku Ubuntu wybierzesz pobieranie oparte na .deb. Pobrany plik nie będzie miał domyślnie rozszerzenia .deb, więc zalecam zmianę nazwy na .deb na końcu. Następnie możesz zainstalować za pomocą:





sudo dpkg -inazwa-pakietu.deb

Prawdopodobnie zostaniesz poproszony o zainstalowanie klucza GPG, a jeśli tak, postępuj zgodnie z podanymi instrukcjami, aby to zrobić.

Gdy to zrobisz, zaktualizuj swoje repozytoria:



sudo aktualizacja apt-get
sudo apt-get installcuda-oraz

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

Korzyści z rozwoju GPU

Procesory obsługują wiele różnych wejść i wyjść oraz zawierają szeroki asortyment funkcji nie tylko do obsługi szerokiego asortymentu potrzeb programowych, ale także do zarządzania różnymi konfiguracjami sprzętowymi. Obsługują również pamięć, buforowanie, magistralę systemową, segmentację i funkcjonalność IO, co czyni je gniazdem wszystkich transakcji.

GPU to przeciwieństwo – zawierają wiele pojedynczych procesorów, które skupiają się na bardzo prostych funkcjach matematycznych. Z tego powodu przetwarzają zadania wielokrotnie szybciej niż procesory. Specjalizując się w funkcjach skalarnych (funkcjach, które pobierają jeden lub więcej danych wejściowych, ale zwracają tylko jedno wyjście), osiągają ekstremalną wydajność kosztem ekstremalnej specjalizacji.

Przykładowy kod

W przykładowym kodzie dodajemy razem wektory. Dodałem wersję kodu CPU i GPU w celu porównania szybkości.
gpu-przykład.cpp zawartość poniżej:

#dołącz „cuda_runtime.h”
#włączać
#włączać
#włączać
#włączać
#włączać

typedefgodziny::chrono::zegar_wysokiej rozdzielczościZegar;

#define ITER 65535

// Wersja procesora funkcji dodawania wektora
próżniavector_add_cpu(int *do,int *b,int *C,intn) {
inti;

// Dodaj elementy wektora a i b do wektora c
dla (i= 0;i<n; ++i) {
C[i] =do[i] +b[i];
}
}

// Wersja GPU funkcji dodawania wektorów
__światowy__próżniavector_add_gpu(int *GPU_a,int *gpu_b,int *gpu_c,intn) {
inti=identyfikator wątku.x;
// Nie potrzebna pętla for, ponieważ środowisko uruchomieniowe CUDA
// będzie wątkować ten ITER razy
gpu_c[i] =gpu_a[i] +gpu_b[i];
}

intGłówny() {

int *do,*b,*C;
int *GPU_a,*gpu_b,*gpu_c;

do= (int *)malloc(ITER* rozmiar(int));
b= (int *)malloc(ITER* rozmiar(int));
C= (int *)malloc(ITER* rozmiar(int));

// Potrzebujemy zmiennych dostępnych dla GPU,
// więc cudaMallocManaged zapewnia te
cudaMallocZarządzane(&gpu_a, ITER* rozmiar(int));
cudaMallocZarządzane(&gpu_b, ITER* rozmiar(int));
cudaMallocZarządzane(&gpu_c, ITER* rozmiar(int));

dla (inti= 0;i<ITER; ++i) {
do[i] =i;
b[i] =i;
C[i] =i;
}

// Wywołanie funkcji procesora i określenie czasu
automatycznycpu_start=Zegar::teraz();
vector_add_cpu(a, b, c, ITER);
automatycznycpu_end=Zegar::teraz();
godziny::koszt << 'vector_add_cpu: '
<<godziny::chrono::czas_oddawania<godziny::chrono::nanosekundy>(cpu_end-cpu_start).liczyć()
<< nanosekundy. ';

// Wywołanie funkcji GPU i określenie czasu
// Hamulce z trzema kątami to rozszerzenie środowiska wykonawczego CUDA, które pozwala
// parametry wywołania jądra CUDA do przekazania.
// W tym przykładzie przekazujemy jeden blok wątków z wątkami ITER.
automatycznygpu_start=Zegar::teraz();
vector_add_gpu<<<1, ITER>>> (gpu_a, gpu_b, gpu_c, ITER);
cudaDeviceSynchronize();
automatycznygpu_end=Zegar::teraz();
godziny::koszt << 'vector_add_gpu: '
<<godziny::chrono::czas_oddawania<godziny::chrono::nanosekundy>(gpu_end-gpu_start).liczyć()
<< nanosekundy. ';

// Zwolnij alokacje pamięci oparte na funkcjach GPU
cudaFree(do);
cudaFree(b);
cudaFree(C);

// Zwolnij alokacje pamięci oparte na funkcjach procesora
darmowy(do);
darmowy(b);
darmowy(C);

powrót 0;
}

Makefile zawartość poniżej:

INC=-I/usr/lokalny/cuda/włączać
NVCC=/usr/lokalny/cuda/jestem/nvcc
NVCC_OPT=-std=c++jedenaście

wszystko:
$(NVCC)$(NVCC_OPT)gpu-przykład.cpp-lubprzykład-gpu

czysty:
-rm -Fprzykład-gpu

Aby uruchomić przykład, skompiluj go:

robić

Następnie uruchom program:

./przykład-gpu

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

Jeśli nie, może być konieczne dostosowanie definicji ITER w gpu-example.cu do wyższej liczby. Wynika to z tego, że czas konfiguracji GPU jest dłuższy niż w przypadku niektórych mniejszych pętli intensywnie korzystających z procesora. Znalazłem 65535, aby działał dobrze na mojej maszynie, ale twój przebieg może się różnić. Jednak po usunięciu tego progu GPU jest znacznie szybszy niż procesor.

Wniosek

Mam nadzieję, że wiele się nauczyłeś od naszego wprowadzenia do programowania GPU w C++. Powyższy przykład nie przynosi wiele, ale przedstawione koncepcje zapewniają strukturę, której możesz użyć do włączenia swoich pomysłów, aby uwolnić moc swojego GPU.