Obliczenia na GPU w technologii CUDA

Podobne dokumenty
Programowanie kart graficznych

Wprowadzenie do programowania w środowisku CUDA. Środowisko CUDA

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

Procesory kart graficznych i CUDA wer

Programowanie Współbieżne

Procesory kart graficznych i CUDA

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

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

Procesory kart graficznych i CUDA wer PR

Programowanie CUDA informacje praktycznie i. Wersja

GTX260 i CUDA wer

Co to jest sterta? Sterta (ang. heap) to obszar pamięci udostępniany przez system operacyjny wszystkim działającym programom (procesom).

Programowanie procesorów graficznych GPGPU

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

Programowanie procesorów graficznych w CUDA.

JCuda Czy Java i CUDA mogą się polubić? Konrad Szałkowski

Programowanie CUDA informacje praktycznie i przykłady. Wersja

Programowanie procesorów graficznych GPGPU. Krzysztof Banaś Obliczenia równoległe 1

Programowanie kart graficznych. Architektura i API część 1

Procesory kart graficznych i CUDA wer

CUDA obliczenia ogólnego przeznaczenia na mocno zrównoleglonym sprzęcie. W prezentacji wykorzystano materiały firmy NVIDIA (

Wprowadzenie do programowania w środowisku CUDA. Środowisko CUDA

Programowanie kart graficznych. Architektura i API część 2

Programowanie aplikacji równoległych i rozproszonych

Programowanie kart graficznych

Programowanie PKG - informacje praktycznie i przykłady. Wersja z Opracował: Rafał Walkowiak

Podstawy programowania. Wykład 6 Wskaźniki. Krzysztof Banaś Podstawy programowania 1

CUDA. obliczenia na kartach graficznych. Łukasz Ligowski. 11 luty Łukasz Ligowski () CUDA 11 luty / 36

Programowanie Równoległe wykład 12. OpenGL + algorytm n ciał. Maciej Matyka Instytut Fizyki Teoretycznej

Przetwarzanie Równoległe i Rozproszone

Podstawy Programowania C++

CUDA ćwiczenia praktyczne

CUDA część 1. platforma GPGPU w obliczeniach naukowych. Maciej Matyka

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

Programowanie współbieżne i rozproszone

Programowanie w języku C++

Podstawy programowania komputerów

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

Jacek Matulewski - Fizyk zajmujący się na co dzień optyką kwantową i układami nieuporządkowanymi na Wydziale Fizyki, Astronomii i Informatyki

CUDA PROGRAMOWANIE PIERWSZE PROSTE PRZYKŁADY RÓWNOLEGŁE. Michał Bieńkowski Katarzyna Lewenda

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

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

Tesla. Architektura Fermi

Przygotowanie kilku wersji kodu zgodnie z wymogami wersji zadania,

Tworzenie programów równoległych cd. Krzysztof Banaś Obliczenia równoległe 1

Dodatek A. CUDA. 1 Stosowany jest w tym kontekście skrót GPCPU (od ang. general-purpose computing on graphics processing units).

Tablice i struktury. czyli złożone typy danych. Programowanie Proceduralne 1

Stałe, tablice dynamiczne i wielowymiarowe

Tablice, funkcje - wprowadzenie

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

DYNAMICZNE PRZYDZIELANIE PAMIECI

Programowanie procesorów graficznych GPGPU. Krzysztof Banaś Obliczenia równoległe 1

4 Literatura. c Dr inż. Ignacy Pardyka (Inf.UJK) ASK MP.01 Rok akad. 2011/ / 24

Lab 9 Podstawy Programowania

Struktura programu. Projekty złożone składają się zwykłe z różnych plików. Zawartość każdego pliku programista wyznacza zgodnie z jego przeznaczeniem.

Programowanie z wykorzystaniem technologii CUDA i OpenCL Wykład 1

1 Podstawy c++ w pigułce.

ZARZĄDZANIE PAMIĘCIĄ W TECHNOLOGII CUDA

Hybrydowy system obliczeniowy z akceleratorami GPU

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

PMiK Programowanie Mikrokontrolera 8051

CUDA. cudniejsze przyk ady

Wstęp. do języka C na procesor (kompilator RC51)

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

Wysokowydajna implementacja kodów nadmiarowych typu "erasure codes" z wykorzystaniem architektur wielordzeniowych

Materiał Typy zmiennych Instrukcje warunkowe Pętle Tablice statyczne Wskaźniki Tablice dynamiczne Referencje Funkcje

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

Dr inż. Grażyna KRUPIŃSKA. D-10 pokój 227 WYKŁAD 7 WSTĘP DO INFORMATYKI

Procesy i wątki. Krzysztof Banaś Obliczenia równoległe 1

Wstęp do programowania INP001213Wcl rok akademicki 2017/18 semestr zimowy. Wykład 6. Karol Tarnowski A-1 p.

Programowanie równoległe i rozproszone. Praca zbiorowa pod redakcją Andrzeja Karbowskiego i Ewy Niewiadomskiej-Szynkiewicz

Spis treści WSTĘP CZĘŚĆ I. PASCAL WPROWADZENIE DO PROGRAMOWANIA STRUKTURALNEGO. Rozdział 1. Wybór i instalacja kompilatora języka Pascal

2 Przygotował: mgr inż. Maciej Lasota

1 Podstawy c++ w pigułce.

Spis treści. I. Skuteczne. Od autora... Obliczenia inżynierskie i naukowe... Ostrzeżenia...XVII

Język C zajęcia nr 11. Funkcje

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

Podstawy informatyki. Informatyka stosowana - studia niestacjonarne. Grzegorz Smyk. Wydział Inżynierii Metali i Informatyki Przemysłowej

Część 4 życie programu

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

wykład II uzupełnienie notatek: dr Jerzy Białkowski Programowanie C/C++ Język C - funkcje, tablice i wskaźniki wykład II dr Jarosław Mederski Spis

Wskaźniki w C. Anna Gogolińska

Kompilator języka C na procesor 8051 RC51 implementacja

i3: internet - infrastruktury - innowacje

Wykład 1: Wskaźniki i zmienne dynamiczne

PARADYGMATY PROGRAMOWANIA Wykład 4

Podstawy programowania C. dr. Krystyna Łapin

Wstęp do Programowania, laboratorium 02

Budowa komputera. Magistrala. Procesor Pamięć Układy I/O

Programowanie procesorów graficznych GPGPU

wykład III uzupełnienie notatek: dr Jerzy Białkowski Programowanie C/C++ Język C - zarządzanie pamięcią, struktury,

Adam Kotynia, Łukasz Kowalczyk

Biblioteka standardowa - operacje wejścia/wyjścia

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

// Liczy srednie w wierszach i kolumnach tablicy "dwuwymiarowej" // Elementy tablicy są generowane losowo #include <stdio.h> #include <stdlib.

Typy złożone. Struktury, pola bitowe i unie. Programowanie Proceduralne 1

5. Model komunikujących się procesów, komunikaty

Od uczestników szkolenia wymagana jest umiejętność programowania w języku C oraz podstawowa znajomość obsługi systemu Windows.

Architektury komputerów Architektury i wydajność. Tomasz Dziubich

Języki i metodyka programowania. Wskaźniki i tablice.

Transkrypt:

Obliczenia na GPU w technologii CUDA 1

Różnica szybkości obliczeń (GFLOP/s) pomiędzy CPU a GPU źródło NVIDIA 2

Różnica w przepustowości pamięci pomiędzy CPU a GPU źródło NVIDIA 3

Różnice architektoniczne GPU przeznacza więcej tranzystorów na przetwarzanie danych niż na sterowanie i pamięci cache. GPU szczególnie nadaje się do problemów które mogą być przedstawione jako data-parallel computation - ten sam program jest wykonywany równolegle na wielu elementach (np. wektora/macierzy) Odwzorowanie elementów danych w tysiące wątków wykonujących się na setkach rdzeni. 4

Compute Unified Device Architecture. CUDA Zaprojektowana wyłącznie pod procesory NVIDIA. Ale z emulacją na CPU (z możliwością wykorzystania wielu rdzeni) Jest rozszerzeniem języka C/C++. Ale dostępne wiązania dla innych platform (np. Python/Java/.NET). Zaprojektowana aby skalować się na tysiące wątków i setki rdzeni. Oferuje wydajność na procesorach GPU porównywalną z (niewielkimi) klastrami. Liczby podwójnej precyzji gorzej wspierane i dużo wolniej. Model programowania heterogeniczny szeregowo-równoległy. Porcje kodu wykonywane równolegle na GPU, reszta szeregowo na CPU GPU jest koprocesorem dla CPU. GPU i CPU mają rozłączne pamięci DRAM. 5

Jądra (ang. kernels) i wątki w CUDA Równoległe porcje aplikacji są wykonywane na GPU jako jądra. Tylko jedno jądro jest wykonywane w danej chwili Wiele wątków (tysiące) wykonuje to samo jądro. Jądro jest wykonywane przez wektor wątków. Każdy z nich wykonuje ten sam kod. Każdy wątek ma identyfikator, który wykorzystuje do obliczania adresów pamięci. Wątki operujące na wektorze. 6

Grid wątków Numerowanie wektora wątków nie jest monolityczne Jądro uruchamia grid składający się z N bloków. Każdy blok składa się z M wątków => łącznie mamy N*M wątków w gridzie. Adres bloku w gridzie może być dwuwymiarowy (jest strukturą z polami x oraz y). Adres wątku w bloku może być dwuwymiarowy (jest strukturą z polami x,y,z). Na powyższym rysunku adres jest jednowymiarowy - pola y,z są równe 1. Zatem jeżeli wątek operuje na wektorze 0,...,N*M, to numer elementu wektora może być obliczony przy pomocy wyrażenia. numer_wątku_w_bloku+rozmiar_bloku*numer_bloku. 7

Motywacja: skalowalność Hardware może zaszeregować dowolny blok na dowolnym procesorze. Brak założeń o kolejności wykonywania bloków. 8

Wykonywanie kodu na GPU Jądra są funkcjami języka C z pewnymi ograniczeniami: Nie mogą odwoływać się do pamięci CPU Muszą zwracać void jako wynik. Nie mogą przyjmować zmiennej liczby argumentów Nie mogą być rekurencyjne Nie mogą deklarować zmiennych statycznych. Parametry są kopiowane automatycznie z CPU na GPU. Deklarowane z kwalifikatorem global oznaczającym funkcję wywoływaną z CPU i wykonującą się na GPU. Inne kwalifikatory to: device oznacza funkcję wywoływaną z GPU i wykonującą się na GPU. host oznacza funkcję wywoływaną z CPU w wykonującą się na GPU (domyślnie) device i host mogą być łączone. 9

Pierwsze jądro i równoważny kod CPU. Kod dla CPU inkrementujący wszystkie elementy wektora Jądro dla GPU w którym każdy wątek inkrementuje jeden element wektora. void inc_cpu(float *a, int N) { int idx; for (idx = 0; idx<n; idx++) a[idx] = a[idx] + 1.0; } Zmienne predefiniowane w każdym jądrze. blockidx - numer bloku blockdim - rozmiar bloku threadidx - numer wektora global void inc_gpu(float *a, int N) { int idx = blockidx.x *blockdim.x + threadidx.x; if (idx < N) a[idx] = a[idx] + 1.0; } Są to struktury 2/3 wymiarowe ale my wykorzystujemy tylko jeden wymiar: pole x. 10

Mapowanie identyfikatorów w numery elementów tablicy 11

Wywołanie jądra Wywołanie jądra z kodu CPU ma postać: f<<liczba_bloków_w_gridzie,rozmiar_bloku>>(parametry) W naszym przypadku: float *d_a; void LaunchKernel(){ int blocksize=256; dim3 dimblock (blocksize); dim3 dimgrid( ceil( N / (float)blocksize) ); inc_gpu<<<dimgrid, dimblock>>>(d_a, N); } Rozmiar bloku przyjęliśmy na 256 wątków Struktura dim3 zawiera pola x,y,z. My wykorzystujemy tylko x. Rozmiar gridu jest tak dobrany aby całkowita liczba wątków była najmniejszą wielokrotnością 256 większą bądź równą N. Stąd przeznaczenie instrukcji if (idx<n) Gdy N=600, to dimgrid.x==3 i mamy 768 wątków (za dużo!!!) 12

Alokacja pamięci GPU CPU zarządza pamięcią GPU: cudamalloc (void ** pointer, size_t nbytes) cudamemset (void * pointer, int value, size_t count) cudafree (void* pointer) Przykład int n = 1024; int nbytes = 1024*sizeof(int); // Wskaźnik na pamięć GPU; int * d_a = 0; // cudamalloc( (void**)&d_a, nbytes ); // Próba odwołania się do pamięci wskazywanej przez d_a // w kodzie CPU prowadzi do błędu ochrony pamięci cudamemset( d_a, 0, nbytes); cudafree(d_a); 13

Synchronizacja CPU z GPU Wywołania jąder są asynchroniczne. Sterowanie powraca od razu do CPU bez oczekiwania na zakończenie wątków jądra Jądro jest kolejkowane i wykona się po zakończeniu wszystkich poprzednich wywołań CUDA. Funkcja cudamemcpy jest synchroniczna. Kopiowanie rozpocznie się po wykonaniu poprzednich wywołań CUDA. Sterowanie powraca do CPU po wykonaniu kopii. Kopie pomiędzy pamięciami GPU są asynchroniczne. Funkcja cudathreadsynchronize() wstrzymuje kod CPU do momentu wykonania wszystkich kolejkowanych wywołań CUDA 14

Zaawansowana alokacja pamięci Funkcja cudaerror_t cudamallochost (void ptr, size_t size) alokuje pamięć CPU, której wszystkie strony są zablokowane w pamięci. Kopia z takiej pamięci do pamięci GPU może być wykonana przy pomocy mechanizmu DMA poprzez szynę PCIe. Jest to znacznie szybsze od kopiowania pamięci zaalokowanej standardowo np. przez funkcję malloc. Zaalokowanie zbyt dużej ilości pamięci przy pomocy tej funkcji może obniżyć wydajność systemu (wyłącza się mechanizm pamięci wirtualnej) Pamięć jest zwalniana przy pomocy funkcji cudaerror_t cudafreehost (void *ptr). Funkcja cudaerror_t cudamallocpitch (void devptr, size_t pitch, size_t width, size_t height) alokuje pamięć dla macierzy o szerokości width (liczbie kolumn) i wysokości (liczbie wierszy) height. Ale szerokość wiersza jest zwiększana aby każdy nowy wiersz był optymalnie wyrównany (ang. aligned) w pamięci. Nowa szerokość jest zapamiętywana w pod adresem pitch. 15

Kopiowanie danych cudamemcpy( void *dst, void *src, size_t nbytes, enum cudamemcpykind direction); Powraca po wykonaniu kopii Blokuje kod CPU do momentu wykonania kopii. Nie rozpocznie kopiowania do momentu wykonania poprzednich wywołań CUDA (np. jąder) enum cudamemcpykind cudamemcpyhosttodevice kopia z pamięci CPU do pamięci GPU cudamemcpydevicetohost kopia z pamięci GPU do pamięci CPU cudamemcpydevicetodevice kopia z pamięci GPU do pamięci CPU cudamemcpyasync nie blokuje kodu CPU. 16

Kod programu // Niezbędne aby korzystać z API CUDA #include <cuda.h> float *a_h; //wskaźnik do pamięci CPU float *a_d; //wskaźnik do pamięci GPU int main(void) { int i, N = 10000; size_t size = N*sizeof(float); a_h = (float *)malloc(size); // alokacja pamięci CPU cudamalloc((void **) &a_d, size); // alokacja pamięci GPU // Inicjalizacja danych for (i=0; i<n; i++) a_h[i] = (float)i; // Kopia z pamięci CPU do pamięci GPU cudamemcpy(a_d, a_h, sizeof(float)*n, cudamemcpyhosttodevice); LaunchKernel(); // zwiększ wszystkie elementy o jeden // Kopia z pamięci GPU z powrotem do a_h (pamięć CPU) cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); // dealokacja pamięci free(a_h); cudafree(a_d); } 17

Kompilacja aplikacji Pliki C/C++ z rozszerzeniami CUDA mają (zwyczajowo) rozszerzenie.cu. Kompilowane są narzędziem nvcc. nvcc -O3 plik.cu -o program => kompilacja z optymalizacją dla CPU -O3 przekazywane kompilatorowi gcc dodatkowa opcja -g: informacje dla debuggera dodatkowa opcja -deviceemu Kompilacja pod emulator, cały kod wykonuje się na CPU. Można używać funkcji printf w kodzie przeznaczonym dla GPU!!! Zdefiniowane jest makro DEVICE_EMULATION Ale uwaga: błędny kod może wykonywać się poprawnie z włączoną emulacją. Np. próba odwołania się przez GPU do pamięci CPU zakończy się sukcesem. Opcja -multicore wykorzystanie wielu rdzeni. Debugger CUDA-GDB pod Linuksa. (z poważnym ograniczeniem: X11 nie może się wykonywać na GPU na którym jest debugowany kod). 18

CUDA SDK 19

Typy pamięci 20

Fizyczne umiejscowienie pamięci Pamięć lokalna rezyduje w pamięci DRAM karty. Należy używać pamięci wspólnej aby minimalizować wykorzystanie pamięci lokalnej. Pamięć wspólna rezyduje na procesorze GPU dostęp znacznie szybszy niż do innych rodzajów pamięci. CPU ma dostęp do pamięci globalnej ale nie do pamięci współdzielonej. 21

Kwalifikatory typów pamięci device oznacza zmienną przechowywaną w pamięci globalnej (duży czas dostępu, brak pamięci podręcznej) Pamięć alokowana przez cudamalloc jest globalna. Dostęp z wszystkich wątków gridu. Czas życia: aplikacja. shared oznacza zmienną przechowywaną w pamięci współdzielonej. Przechowywana wewnątrz GPU (bardzo krótki czas dostępu) Dostępna wyłącznie wątkom wewnątrz tego samego bloku. Czas życia: blok wątków. Zmienne bez kwafikatorów O ile to możliwe przechowywane w rejestrach. Co się nie zmieści w rejestrach przechowywane jest w pamięci lokalnej. 22

Wykorzystanie pamięci współdzielonej Rozmiar znany w chwili kompilacji global void kernel( ) { shared float sdata[256]; } int main(void) { kernel<<<nblocks,blocksize>>>( ) ; } Rozmiar znany w chwili wykonywania programu (dodatkowy parametr jądra). global void kernel( ) { extern shared float sdata[]; } int main(void) { smbytes = blocksize*sizeof(float); kernel<<<nblocks, blocksize, smbytes>>>( ); } sdata jest współdzielone przez wszystkie wątki bloku!!! 23

Synchronizacja wątków w bloku CUDA umożliwia współpracę wątków w ramach bloku. Funkcja void _syncthreads() Synchronizacja barierowa: każdy wątek w bloku oczekuje, aż wszystkie inne wątki osiągną punkt wywołania funkcji _syncthreads() Bariera dotyczy wyłącznie wątków w bloku, nie w gridzie. Ale tylko takie wątki mogą korzystać z pamięci współdzielonej. Stosowana w celu uniknięcia wyścigów przy dostępie do pamięci wspólnej. Jeżeli występuje w kodzie warunkowym np. instrukcja if, to musimy zadbać aby wszystkie wątki ją wywołały. 24

Przykład odwrócenie kolejności elementów wektora Pierwszy element staje się ostatnim, drugi przedostatnim i na odwrót. _global void reversearrayblock(int *d_out, int *d_in) { int inoffset = blockdim.x * blockidx.x; int outoffset = blockdim.x * (griddim.x - 1 - blockidx.x); int in = inoffset + threadidx.x; int out = outoffset + (blockdim.x - 1 - threadidx.x); d_out[out] = d_in[in]; } Kopia wektora d_in z odwróconą kolejnością trafia do wektora d_out. outoffset oraz out liczone są od końca tablicy. Problem: zapis do d_out następuje w odwrotnej kolejności: wątek wektor 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 Spowalnia to kod kilkanaście razy (pamięć globalna nie ma pamięci cache, za to posiada hardware do przyspieszania dostępów sekwencyjnych) 25

Wersja z wykorzystaniem pamięci wspólnej Pamięć wspólna staje się software'owo zarządzaną pamięcią cache. global void reversearrayblock(int *d_out, int *d_in) { extern shared int s_data[]; int inoffset = blockdim.x * blockidx.x; int in = inoffset + threadidx.x; // Załadowanie jednego elementu z pamięci globalnej i zapamiętanie // w odwrotnej kolejności w pamięci współdzielonej s_data[blockdim.x - 1 - threadidx.x] = d_in[in]; // Zaczekaj aż wszystkie wątki w bloku dokonają zapisu // do pamięci wspólnej syncthreads(); // Zapisz dane z pamięci wspólnej w kolejności normalnej // do pamięci globalnej int outoffset = blockdim.x * (griddim.x - 1 - blockidx.x); int out = outoffset + threadidx.x; d_out[out] = s_data[threadidx.x]; } 26

Kody błędów CUDA Wszystkie funkcje API, za wyjątkiem wywołań jąder zwracają kody błędów. Wyrażenie typu cudaerror_t Funkcja cudaerror_t cudagetlasterror(void) zwraca kod ostatniego błędu char* cudageterrorstring(cudaerror_t code) zwraca ciąg znaków zakończony zerem zawierający opis błędu w języku angielskim printf( %s\n, cudageterrorstring( cudagetlasterror() ) ); 27

Operacje atomowe i funkcje wbudowane Operacje atomowe na danych typu int/unsigned w pamięci globalnej. add, sub, min, max and, or, xor inc, dec Wbudowane funkcje matematyczne na liczbach pojedynczej precyzji. np. sinf/expf/logf Szczegóły w dokumentacji firmowej. 28

Hardware Geforce 8 i nowsze z co najmniej 256 VRAM ( w tym karty w laptopach ) Specjalizowany hardware: akceleratory TESLA. TESLA S1070: Serwer w obudowie 1U z czterema akceleratorami 29

Jeżeli nie podoba nam się CUDA, to CUBLAS podzbiór bibliotek BLAS operacje ma macierzach i wektorach CUFFT szybka transformata Fourier'a (jednowymiarowa i dwuwymiarowa) 30

Podsumowanie Przykłady na wykładzie były bardzo akademickie Rzeczywista aplikacja powinna się charakteryzować: Znaczną intensywnością korzystania z pamięci GPU w porównaniu z kopiami CPU<=>GPU. Znaczną intensywnością obliczeń numerycznych w porównaniu z odwołaniami do pamięci Polecane źródła Dokumentacja CUDA na stronie firmy NVIDIA: http://www.nvidia.com/object/cuda_develop.html Strona http://gpgpu.org/developer ze slajdami z tutorialami na temat CUDA prezentowanymi na różnych konferencjach. Strona przygotowana przez firmę NVIDIA z materiałami szkoleniowymi na temat CUDA, gorąco polecam sprawdzić linki do cyklu artykułów w Dr. Dobb's Journal. http://www.nvidia.com/object/cuda_education.html (zaczerpnąłem stamtąd przykład z odwracaniem kolejności elementów wektora) 31