Implementacja modelu FHP w technologii NVIDIA CUDA

Komentarze

Transkrypt

Implementacja modelu FHP w technologii NVIDIA CUDA
Uniwersytet Wrocławski
Wydział Fizyki i Astronomii
Instytut Fizyki Teoretycznej
Sebastian Szkoda
Implementacja modelu FHP
w technologii NVIDIA CUDA
Opiekun:
dr hab. Zbigniew Koza,
prof. UWr.
2
Streszczenie
Praca niniejsza ma na celu zbadanie możliwości zastosowania najnowszej
technologii obliczeń równoległych Nvidia CUDA do przyspieszenia symulacji
modelu FHP wykorzystywanego do symulacji przepływu cieczy.
W rozdziale pierwszym przedstawiam zarys koncepcji modelowania procesów
hydrodynamicznych metodami automatów komórkowych. Prezentuję chronologiczne przejście od pomysłu automatu komórkowego, przez proste próby
zastosowania go jako modelu dyskretyzacji przestrzeni i czasu, aż do najbardziej zaawansowanego modelu FHP III wraz z podstawą jego implementacji.
Rozdział drugi i trzeci opisują wykorzystaną technologię. Rozdział drugi
jest wprowadzeniem do technologii Nvidia CUDA, natomiast rozdział trzeci
przedstawia problematykę zastosowania tej technologii do implementacji modelu FHP. Zakończenie rozdziału trzeciego prezentuje osiągnięty wzrost wydajności obliczeń będący miarą jakości rozwiązania podjętego problemu.
Ostatni rozdział służy weryfikacji poprawności działania aplikacji będącej
wynikiem opracowania badanego zagadnienia przez autora niniejszej pracy.
Wyniki skonfrontowane zostały z rozwiązaniami teoretycznymi oraz wynikami doświadczenia wykonanego przez grupę badawczą z Max Planck Institute for Marine Microbiology pod kierunkiem A. Khalili.
Zachęcam do lektury.
3
4
Abstract
The aim of the study is to examine the possibility of accelerating FHP algorithms used for hydrodynamics simulations using the newest Nvidia CUDA
parallel programming technology.
The first chapter presents an introduction to the cellular automata and the
concept of using it as a model of discretization of time and space. It leads to
the most advanced FHP III model and its implementation.
The second and the third chapters describe the technology used in the study.
The second chapter is an introduction to the Nvidia CUDA programming
model and the third one explains how to use it for FHP implementation.
The GPU over CPU speedup is presented and discussed.
The last chapter verifies the correctness of the FHP algorithm implemented for this study. The results were compared to the theoretical solution of
the Navier-Stokes equations and experimental results obtained by the group
from Max Planck Institute for Marine Microbiology (A. Khalili).
5
Spis treści
1 Model
1.1 Wstęp . . . . . . . . . . . . . .
1.2 Automat Komórkowy (CA) . .
1.3 HPP . . . . . . . . . . . . . . .
1.4 FHP . . . . . . . . . . . . . . .
1.4.1 Klasyfikacja modeli FHP
1.5 Przeszkody . . . . . . . . . . .
1.6 Implementacja . . . . . . . . . .
1.6.1 Węzeł . . . . . . . . . .
1.6.2 Sieć . . . . . . . . . . .
1.6.3 Krok symulacji . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9
9
9
10
11
12
12
13
13
14
15
2 Technologia
2.1 Wstęp . . . . . . . . . . . . . . .
2.2 CUDA . . . . . . . . . . . . . . .
2.2.1 Kernel . . . . . . . . . . .
2.2.2 Model sieci bloków wątków
2.2.3 Nowe typy danych . . . .
2.2.4 Zmienne wbudowane . . .
2.2.5 Pamięć urządzenia . . . .
2.2.6 Wydajność . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
17
17
18
18
19
21
21
22
25
3 Model FHP w technologii CUDA
27
3.1 Implementacja . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.2 Wydajność . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4 Poprawność implementacji
35
4.1 Porównanie z teorią . . . . . . . . . . . . . . . . . . . . . . . . 35
4.2 Porównanie z doświadczeniem . . . . . . . . . . . . . . . . . . 38
5 Galeria
43
Bibliografia
47
7
1 Model
1.1
Wstęp
Mechanika płynów jest działem fizyki zajmującym się między innymi badaniem ruchu cieczy i gazów oraz oddziaływaniem tych ośrodków na opływane
przez nie ciała stałe. Pierwsze próby matematycznego ujęcia ruchu płynów
podjął w XVIw. Galileusz, jego pracę rozwijali L. Euler i D. Bernoulli, lecz
dopiero w XIX w. dzięki pracy naukowej H. Naviera oraz G. Stokesa podano równania opisujące ruch płynów, nazwane od ich nazwisk równaniami
Naviera-Stokesa (N-S). Osiągnięcie to nie jest niestety w pełni satysfakcjonujące, gdyż ponad sto lat później nadal nie dość, że nie potrafimy podać
analitycznego rozwiązania równań N-S, to nie jesteśmy nawet w stanie udowodnić czy szukane rozwiązanie istnieje. Problem rozwiązania równania N-S
w 2000 r. został ogłoszony jako jeden z siedmiu najważniejszych dla nauki
problemów matematycznych nazwanych Problemami Milenijnymi [1]. Mimo
wszystko powszechnie korzysta się z tych równań rozwiązując pewne jego
przypadki metodami przybliżonymi. Główne metody przybliżone to: numeryczne rozwiązywanie równań różniczkowych (FDM, FEM, FVM), dynamika
molekularna (MD) oraz gaz sieciowy automatów komórkowych (LGCA).
1.2
Automat Komórkowy (CA)
W latach czterdziestych dwudziestego wieku matematycy John Von Neumann i Stanisław Ulam zaproponowali model dyskretyzacji przestrzeni i czasu.
Przestrzeń sprowadzili do układu sąsiadujących komórek, z których każda
może przyjąć określony stan. Ewolucja układu w czasie polega na symultanicznej zmianie stanu wszystkich komórek według określonych zasad. Podejście takie nazwano automatem komórkowym.
Automaty komórkowe wykorzystywane do symulacji zjawisk hydrodynamicznych nazywane są gazami sieciowymi od angielskich akronimów: LGA – lattice gas automata lub LGCA – lattice gas cellular automata.
9
ROZDZIAŁ 1. MODEL
1.3
HPP
Model HPP [2] (skrót od nazwisk Hardy, de Pazzis, Pomeau) jest pierwszym
(1973 r.) i najprostszym modelem gazu sieciowego. Obecnie nie ma praktycznych zastosowań, jest jednak dobrym materiałem na początek nauki o
modelowaniu metodami LGCA.
W modelu tym mamy do czynienia z siecią kwadratową, w której węzłach
umieszczamy od zera do czterech cząstek. Każda cząstka ma jeden z czterech
kierunków (N,E,S,W) oraz prędkość 1js/∆t [jednostka sieci / krok czasowy].
Cząstki w węźle mają różne kierunki. Przykładowy układ widzimy na rysunku 1.3.
Ewolucja układu przebiega w dwóch krokach:
1) Ruch – przesuwamy każdą z cząstek o jeden węzeł zgodnie z wektorem
jej prędkości.
2) Kolizja – do nowo powstałego układu stosujemy reguły jak na rysunku 1.2.
Rysunek 1.1: Przykładowy stan sieci w modelu HPP.
Inne układy cząstek w węzłach podczas kolizji ignorujemy. Reguły zderzeń są
dobrane w ten sposób, by gwarantowały spełnienie zasady zachowania masy
oraz zasady zachowania pędu.
10
1.4. FHP
Rysunek 1.2: Reguły kolizji w modelu HPP.
1.4
FHP
FHP [3] [4] [5] (skrót od nazwisk Frisch, Hasslacher, Pomeau) jest udoskonaloną wersją modelu gazu sieciowego HPP, w której sieć kwadratową zamieniono na trójkątną i wprowadzono bardziej skomplikowane zasady kolizji.
W modelu mamy do czynienia z siecią trójkątną, w której węzłach umieszczamy od zera do siedmiu cząstek. Każda cząstka ma jeden z sześciu kierunków (NW, NE, E, SE, SW, W) oraz prędkość 1js/∆t [jednostka sieci
/ krok czasowy]. Cząstki w węźle mają różne kierunki. W pewnych wariantach modelu FHP stosuje się również cząstki nieruchome. Przykładowy układ
przedstawia rysunek 1.3. Ewolucja układu przebiega w dwóch krokach:
1) Ruch – przesuwamy każdą z cząstek o jeden węzeł zgodnie z wektorami
prędkości.
2) Kolizja – do nowo powstałego układu stosujemy reguły zderzeń.
Rysunek 1.3: Przykładowy stan sieci w modelu FHP.
11
ROZDZIAŁ 1. MODEL
1.4.1
Klasyfikacja modeli FHP
Modele FHP klasyfikujemy ze względu na zastosowane reguły kolizji:
FHP 1 - kolizje dwucząsteczkowe, symetryczne oraz trój-cząsteczkowe symetryczne.
FHP 2 - jak wyżej, plus trójcząsteczkowe niesymetryczne oraz kolizje z nieruchomą cząstką.
FHP 3 - wszystkie powyższe plus kolizje czterocząsteczkowe.
Wszystkie warianty modelu FHP spełniają zasadę zachowania masy i pędu.
Różne reguły zderzeń powodują różnice wyników na poziomie makroskopowym. Każdy z modeli FHP oddaje inny zakres współczynnika lepkości. Wartości współczynnika lepkości możliwe do osiągnięcia w symulacji zmniejszają
się wraz ze zwiększeniem ilości zderzeń.
1.5
Przeszkody
W modelu FHP przez przeszkodę należy rozumieć nieruchomą cząstkę, która
podczas kolizji każdą cząsteczkę, która na nią wpadła, odbija pod kątem 180°.
Zderzenie pojedynczej cząstki z przeszkodą pokazuje rysunek 1.4.
12
1.6. IMPLEMENTACJA
stan początkowy
zderzenie
odbicie
stan końcowy
Rysunek 1.4: Odbicie od przeszkody.
1.6
1.6.1
Implementacja
Węzeł
W modelu FHP mamy do czynienia z cząsteczkami mogącymi przyjąć jeden z 6 kierunków, cząsteczką nieruchomą oraz przeszkodą. Daje to osiem
możliwych stanów węzła, które mogą zachodzić jednocześnie. Wygodnym
sposobem reprezentacji stanu węzła jest ciąg ośmiu bitów, w którym każdy
bit odpowiada istnieniu (1) lub nieistnieniu (0) cząstki w określonym stanie,
jak na rysunku 1.5. W większości języków programowania, ciąg ośmiu bi-
Rysunek 1.5: Reprezentacja stanu węzła. Powyższy układ bitów (00010011)2
odpowiada liczbie dziesiętnej (19)10 .
tów wykorzystywany jest do reprezentacji znaków (ang. char). Stąd węzeł w
stanie jak na rysunku 1.5 można zaimplementować jak na listingu 1.1:
Listing 1.1: Implementacja węzła.
1
unsigned char node = 19 ;
13
ROZDZIAŁ 1. MODEL
Aby uzyskać pełną kontrolę nad stanem węzła, musimy mieć możliwość wykonywania operacji ustawienia (listing 1.2), usunięcia (listing 1.3) oraz sprawdzenia stanu (listing 1.4) dowolnego bitu.
Listing 1.2: Ustawianie bitu na pozycji (pos) w węźle (node)
1
3
void b i t S e t ( unsigned char& node , int pos )
{
v a l |= ( 1 << ( pos ) ) ;
}
Listing 1.3: Usuwanie bitu z pozycji (pos) w węźle (node)
2
4
void b i t C l r ( unsigned char& node , int pos )
{
v a l &= not ( 1 << ( pos ) ) ;
}
Listing 1.4: Sprawdzanie bitu na pozycji (pos) w węźle (node)
2
4
bool b i t T s t ( unsigned char& node , int pos )
{
return ( ( v a l ) & ( 1 << ( pos ) ) ) ;
}
1.6.2
Sieć
Sieć trójkątna wymaga specjalnego potraktowania przez programistę, gdyż
musi być zrzutowana na tablicę, która zwykle utożsamiana jest z siecią kwadratową. Na rysunku 1.6 widzimy, że położenia węzłów naturalnie przechodzą
w sieć kwadratową, można więc zaimplementować sieć trójkątną jako tablicę
typu char o wymiarze szerokość (w) na wysokość (h), stąd:
Listing 1.5: Sieć węzłów.
unsigned char s i e c [w* h ] ;
14
1.6. IMPLEMENTACJA
Rysunek 1.6: Zrzutowanie sieci trójkątnej na kwadratową.
W przypadku reprezentacji sieci dwuwymiarowej za pomocą tablicy jednowymiarowej dostęp do węzła (i, j), uzyskujemy samodzielnie przeliczając indeksy, dlatego wygodnie jest posłużyć się następującą dyrektywą preprocesora:
Listing 1.6: Dyrektywa preprocesora przeliczająca indeksy tablicy 2D na indeksy tablicy 1D.
1 #define
pos ( i , j ) (w* j+i )
Dzięki niej zamiast stosować zapis dla tablicy dwuwymiarowej:
Listing 1.7: Odczyt z tablicy 2D.
1
siec [ i ][ j ];
piszemy:
1
Listing 1.8: Odczyt z tablicy 1D traktowanej jak tablica 2D.
s i e c [ pos ( i , j ) ] ;
1.6.3
Krok symulacji
Ruch
Ruch polega na propagacji cząsteczek z każdego węzła do węzłów sąsiednich.
Czyli dla każdego węzła musimy sprawdzić stan każdego z bitów, a w przy15
ROZDZIAŁ 1. MODEL
Rysunek 1.7: Zmiana stanu z (21)10 = (00010101)2 na (00101010)2 = (42)10 .
padku zastania stanu 1 przesuwamy cząstkę w odpowiednim kierunku. Do
implementacji ruchu nie wystarczy jedna sieć, gdyż przesunięcie cząsteczki z
jednego węzła do sąsiedniego zmieniłoby dane zapisane w tym drugim, stąd
wniosek, że należy użyć dwóch sieci. Pierwsza z nich przechowuje aktualny
stan układu, z niej pobieramy dane potrzebne do przesunięcia cząstek, lecz
przesuniętą cząstkę zapisujemy w drugiej sieci. W następnym kroku sieci
zamieniają się rolami.
Zderzenia
Po wykonaniu przesunięć cząstek, musimy ponownie sprawdzić stan każdego
z węzłów, tym razem jednak nie patrzymy na stan konkretnych bitów, a na
stan całego węzła i postępujemy zgodnie z regułami opisanymi w punkcie
1.4.1. Aby czynność powyższą wykonywać sprawnie, warto wpisać wszystkie
możliwe stany węzła w tablicę. Ilość dopuszczalnych stanów to 28 = 256,
co jest dość skromnym obciążeniem pamięci. Tablica zawierająca stany węzłów powinna reprezentować odwzorowanie przekształcające zastany stan na
odpowiadający mu stan zgodny z regułami z rozdziału 1.4.1. Czyli przykładowo pod indeksem 21 tablicy ma znajdować się wartość 42 ponieważ
(21)10 = (00010101)2 ma przekształcić się na (00101010)2 = (42)10 zgodnie z
regułą z rysunku 1.7. Natomiast pod indeksem 42 ma znajdować się wartość
21, ponieważ reguła ta stosuje się w dwie strony. Problematyczne w takim
podejściu są reguły kolizji zależne od zmiennej losowej, gdyż powyższa tabelaryzacja reguł tego przypadku nie uwzględnia. Rozwiązaniem może być
zastosowanie dwóch tablic stanów, jednej dla przypadku zmiennej losowej
0 < p ¬ 0.5, drugiej dla 0.5 < p ¬ 1. Wtedy przed każdą zmianą stanu
węzła konieczne jest losowanie wartości losowej p : 0 < p ¬ 1 z rozkładu
jednostajnego.
16
2 Technologia
2.1
Wstęp
Od początku istnienia komputerów klasy PC producenci prześcigają się w
udoskonalaniu ich jednostek obliczeniowych. Przez wiele lat podstawowym
sposobem zwiększenia mocy obliczeniowej było zwiększenie częstotliwości zegara procesora, czyli ilości elementarnych operacji, które procesor może wykonać w czasie jednej sekundy. Do niedawna nic nie stało na przeszkodzie,
aby tak właśnie rozumieć wzrost mocy obliczeniowej komputerów. Częstotliwości zegarów rosły od kHz, przez MHz do GHz i osiągnęły wartości 4 GHz po
czym spadły do obecnego1 stanu, jakim jest wartość z przedziału 2-3 GHz. Na
przeszkodzie dalszemu rozwojowi stanęło ciepło, dokładniej problem z jego
odprowadzaniem. Przy dużych częstotliwościach zegarów, procesory produkują takie ilości energii cieplnej, że chłodzenie powietrzem nie wystarcza,
prowadzi do przegrzania i uszkodzenia sprzętu. Niestety, inne techniki chłodzenia są bardzo drogie, co zmusiło producentów do spojrzenia na problem
z innej strony.
Aktualnie za panaceum uważa się obliczenia równoległe, czyli takie w których
staramy się rozwiązać problem, dzieląc go na niezależne fragmenty, wykonywane jednocześnie przez wiele jednostek obliczeniowych. Dla takich potrzeb
powstały procesory wielordzeniowe, w których zamiast zwiększać taktowanie
zegara jednej jednostki obliczeniowej, wstawia się kilka wolniejszych jednostek, działających niezależnie. Nie jest to jednak idealne rozwiązanie. W algorytmach, w których czynności muszą być wykonywane po kolei, dołożenie
dodatkowych jednostek liczących nie daje żadnego przyspieszenia obliczeń.
Mimo istnienia klasy algorytmów trudnych do zrównoleglenia, grupa tych
podatnych jest na tyle duża, że jest o co walczyć.
W świecie procesorów komputerowych, wielordzeniowość jest dość nowym tematem, lecz w świecie procesorów graficznych, stosowana jest o wiele dłużej.
Wynika to między innymi z tego, że algorytmy stosowane w grafice są bardzo
podatne na zrównoleglanie. Nie jest więc dziwne, że producenci kart graficznych mają duże doświadczenie w produkcji jednostek wielordzeniowych, tzw.
1
2010r
17
ROZDZIAŁ 2. TECHNOLOGIA
Graphics Processing Unit (GPU). Do niedawna moc obliczeniowa ukryta w
kartach graficznych wykorzystywana była tylko w zastosowaniach graficznych, jednak w roku 2006 firma ATI Technologies i niespełna rok później
firma NVIDIA udostępniły konkurencyjne w stosunku do siebie technologie,
umożliwiające wykonywanie dowolnych obliczeń przy użyciu procesorów kart
graficznych. Z różnych względów, prowadzenie w wyścigu o prym w technologii obliczeń na GPU objęła firma NVIDIA z technologią CUDA. Urządzenia
zgodne z tą technologią to karty graficzne wyposażone w procesory NVIDIA GeForce serii 8, 9, 200 i 400 oraz sprzęt specjalistyczny NVIDIA Tesla
i Quadro.
2.2
CUDA
CUDA (Compute Unified Device Architecture) jest środowiskiem rozszerzającym język C o zestaw funkcji i dyrektyw pozwalających na implementowanie algorytmów wykonywanych równolegle z wykorzystaniem procesorów
oraz pamięci karty graficznej. Program pisany w taki sposób można podzielić
na dwie części, część host wykonywaną na procesorze głównym komputera
(CPU) i wykorzystującą pamięć główną RAM komputera oraz część device
wykonywaną na karcie graficznej z użyciem jej pamięci i procesorów (GPU).
Część wykonywana na CPU może być tworzona w językach C, C++, Java,
Python i wielu innych, natomiast część wykonywana na GPU, musi być
napisana w języku C/C++ wraz z jego rozszerzeniami dołączonymi przez
producenta za pośrednictwem środowiska programistycznego. Ze względu na
wydajność najlepszym wyborem dla części host jest C lub C++.
2.2.1
Kernel
Część device składa się z tzw. kerneli, czyli funkcji wywoływanych z CPU,
lecz wykonywanych równolegle na urządzeniu. Sposób wykonywania kerneli
nazwany został SIMT (single-instruction, multiple-thread) czyli pojedyncza
instrukcja, wiele wątków. W technologii SIMT pojedyncze wywołanie kernela
powoduje stworzenie N 1 wątków wykonujących te same zadania, lecz
operujących na innych danych. Kernele różnią się od zwykłych funkcji języka
C słowem kluczowym global , np:
18
2.2. CUDA
1
global
Listing 2.1: Przykład kernela
void f u n k c j a ( int * a ){
};
oraz sposobem wywołania. W kodzie CPU kernel z listingu 2.1 wywołujemy,
używając nowej składni: <<<. . . >>>.
Listing 2.2: Wywołanie funkcji typu kernel.
1
3
int main ( )
{
f u n k c j a <<<N,M>>>(A ) ;
}
Dyrektywa <<<. . . >>> odpowiada za konfigurację wywołania funkcji na
urządzeniu. Na konfigurację wywołania składają się cztery wielkości:
<<<gridDS,blockDS,sharedMS,streamNO>>>
ˆ gridDS definiuje wymiar i rozmiar sieci bloków wątków (Rozdział 2.2.2),
ˆ blockDS definiuje wymiar i rozmiar bloku wątków,
ˆ sharedMS argument opcjonalny, ustala rozmiar pamięci współdzielonej w pojedynczym bloku wątków,
ˆ streamNO argument opcjonalny, definiuje numer strumienia wątków.
Najważniejsze z nich są obowiązkowe parametry gridDS i blockDS. Służą one
do definiowania abstrakcyjnego modelu sieci wątków. Pozostałe parametry
mają ustalone wartości domyślne i rzadko są ustawiane ręcznie.
2.2.2
Model sieci bloków wątków
Model ten służy do zarządzania równoległym wykonaniem funkcji typu kernel. Dzieli on wątki na bloki o równych rozmiarach, każdy blok wątków wykonywany jest na innym procesorze wielordzeniowym tzw. multiprocesorze.
Natomiast sieć jest zbiorem bloków wątków. Zarówno sieć jak i bloki mogą
być tworami jedno-, dwu- lub trójwymiarowymi. Realizację przykładowego
podziału wątków widzimy na rysunku 2.1, gdzie 72 wątki podzielone zostały
na sześć bloków po 12 wątków każdy. Sieć bloków zdefiniowana została jako
sieć dwuwymiarowa o szerokości 3 i wysokości 2, natomiast blokom nadano
rozmiar 4 na 3. Parametry gridDS i blockDS są zmiennymi typu dim3. Typ
ten należy do rozszerzenia języka C i służy do definiowania rozmiarów. Implementacja sytuacji z rysunku 2.1 wyglądałaby tak:
19
ROZDZIAŁ 2. TECHNOLOGIA
Rysunek 2.1: Przykładowy model sieci bloków wątków. Źródło: [6].
20
2.2. CUDA
2
4
6
Listing 2.3: Implementacja sieli bloków wątków z rysunku 2.1
int main ( )
{
dim3 dimGrid ( 3 , 2 ) ;
dim3 dimBlock ( 4 , 3 ) ;
f u n k c j a <<<dimGrid , dimBlock>>>(A ) ;
}
2.2.3
Nowe typy danych
Rozszerzenie języka C wprowadza więcej nowych typów, nazywanych typami
wektorowymi. Prawie każdy z podstawowych typów danych języka C ma swój
odpowiednik z sufiksem 1, 2, 3 lub 4. Przykładowo: int2, char4, float3. Typy
te są strukturami języka C o składowych (x ),(x,y),(x,y,z ) lub (x,y,z,w ) w zależności od ich wymiaru. Zmienne wektorowe tworzone są funkcją make typ,
gdzie typ jest typem wektorowym, np:
Listing 2.4: Tworzenie zmiennej typu float3.
f l o a t 3 x = make float3 ( float x , float y , float z )
Dokładne wyszczególnienie wszystkich nowych typów znajdziemy w [6].
2.2.4
Zmienne wbudowane
Aby wewnątrz kernela rozpoznać, w którym miejscu sieci lub bloku jesteśmy,
mamy do dyspozycji wbudowane zmienne:
ˆ threadIdx - zmienna typu uint3, na pozycjach (x,y,z ) przechowuje indeksy wątku, w którym aktualnie jesteśmy,
ˆ blockIdx - zmienna typu uint3, na pozycjach (x,y,z ) przechowuje indeksy bloku, w którym aktualnie jesteśmy,
ˆ blockDim - zmienna typu dim3, zawierająca rozmiar bloku zdefiniowany
przy wywołaniu kernela,
ˆ gridDim - zmienna typu dim3 jest zmienną zawierającą rozmiar sieci
zdefiniowany przy wywołaniu kernela.
21
ROZDZIAŁ 2. TECHNOLOGIA
2.2.5
Pamięć urządzenia
Karty graficzne firmy Nvidia dysponują aż pięcioma różnymi rodzajami pamięci. Różnią się one przede wszystkim prędkością, rozmiarem, rodzajami
dostępu i czasem życia danych w nich umieszczonych.
Pamięć globalna
Głównym obszarem pamięci jest pamięć globalna. Pamięć ta jest po prostu
pamięcią RAM karty graficznej. Dostęp do niej rozumiany jako odczyt i zapis
mają wszystkie wątki oraz CPU. Przepustowość pamięci globalnej na urządzeniu to dla serii GTX to od 100GB/s do 180GB/s a rozmiary to od 0.5 GB
do 4 GB. Aby skopiować dane z pamięci komputera do globalnej pamięci
karty, musimy skorzystać ze specjalnego zestawu funkcji. Funkcje te wywoływane są w części kodu hosta. Stworzone zostały jako odpowiedniki funkcji
języka C: malloc(), memcpy(), free(). Posiadają takie same nazwy jak w C,
jednak poprzedzone są prefiksem cuda.
ˆ cudaMalloc (void** devPtr, size t size) – funkcja jako pierwszy
argument przyjmuje wskaźnik devPtr, który po jej wywołaniu będzie
wskazywał na obszar pamięci o rozmiarze size podanym w bajtach jako
drugi jej argument.
ˆ cudaMemcpy(void* dst,const void* src, size t size, enum cudaMemcpyKind kind) – kopiuje obszar pamięci o rozmiarze size z
miejsca src pamięci w miejsce dst. Argument kind może przyjąć wartości:
– cudaMemcpyHostToHost – kopiowanie z host na host czyli jak
zwykłe memcpy(),
– cudaMemcpyHostToDevice – kopiowanie z host na device,
– cudaMemcpyDeviceToHost – kopiowanie z device na host,
– cudaMemcpyDeviceToDevice – kopiowanie z device na device.
ˆ cudaFree(void* devPtr) – zwalnia obszar pamięci wskazywany przez
devPtr.
Są to podstawowe funkcje do zarządzania pamięcią, każda z nich zwraca warość typu wyliczeniowego cudaError zawierającą informację o powodzeniu
22
2.2. CUDA
operacji lub kod błędu.
1
Listing 2.5: Przykładowy kod kopiujący tablicę z CPU na GPU.
int * d e v i c e p o i n t e r ;
int * h o s t p o i n t e r ;
3
5
7
int main ( )
{
int s i z e = 1 0 0 0 ;
h o s t p o i n t e r = new int [ s i z e ] ;
cudaMalloc ( ( void * * ) &d e v i c e p o i n t e r , \
s i z e * s i z e o f ( int ) ) ;
cudaMemcpy ( d e v i c e p o i n t e r , h o s t p o i n t e r , \
s i z e * s i z e o f ( int ) , \
cudaMemcpyHostToDevice ) ;
// . . .
9
11
13
15
cudaFree ( d e v i c e p o i n t e r ) ;
17
}
Operując na tablicach wielowymiarowych należy pamiętać, że przestrzeń adresów pamięci na urządzeniu nie ma nic wspólnego ze swoim odpowiednikiem
na CPU i vice versa, więc kopiowanie wskaźników z CPU na GPU nie ma
sensu. W przypadku, gdy chcemy używać tablic wielowymiarowych, których
rozmiar nie jest znany podczas kompilacji, najłatwiej jest definiować je jako
jednowymiarowe, a następnie ręcznie przeliczać indeksy, jak w punkcie 1.6.2.
Pamięć lokalna
Pamięć lokalna jest pamięcią prywatną dla każdego wątku. Pod względem
parametrów jest identyczna z pamięcią globalną. Służy do przechowywania
zmiennych, które w normalnym kodzie C powinny znajdować się rejestrach,
lecz ze względu na niewielki rozmiar pamięci rejestrów urządzenia, w przypadku gdy zmienne te zajmują zbyt wiele miejsca, kompilator automatycznie
przenosi część z nich do pamięci lokalnej. Rozmiar pamięci rejestrów to 8192
bajtów na multiprocesor.
23
ROZDZIAŁ 2. TECHNOLOGIA
Pamięć stała
Na urządzeniu znajdują się 64 KB tego typu zasobów z czego 8 KB jest
buforowanych w pamięci cache. W praktyce, jeśli nie wyjdziemy poza ilość
podlegającą buforowaniu, pamięć stała jest bardzo szybkim rodzajem pamięci. Niestety, możliwość zapisania w niej danych mamy tylko przez CPU,
więc w kodzie należy traktować ją jako pamięć tylko do odczytu. Efektywne
jej wykorzystanie polega na umiejscowieniu w niej wartości nie zmieniających
się podczas działania programu. Obiekty przechowywane w pamięci stałej deklarujemy słowem kluczowym constant , a ich rozmiar musi być znany już
podczas kompilacji. Aby zapisywać dane w tym obszarze, należy wykorzystać
funkcję cudaMemcpyToSymbol(const char* symbol,const void* src,size t count,size t offset,enum cudaMemcpyKind kind). Funkcja ta różni się od funkcji
opisywanych wcześniej tym, że zmienna symbol może być nazwą wskaźnika
wskazującego zarówno na obszar pamięci globalnej jak i stałej. Przykładowy
kod kopiujący tablicę z host do constant mamory może wyglądać następująco:
1
3
5
7
9
Listing 2.6: Kopiowanie tablicy z CPU do pamięci stałej urządzenia.
constant
float tablica [ 2 5 6 ] ;
int main ( )
{
h o s t p o i n t e r = new int [ 2 5 6 ] ;
cudaMemcpyToSymbol ( t a b l i c a , h o s t p o i n t e r , \
256 * s i z e o f ( int ) , 0 , cudaMemcpyHostToDevice )
// . . .
}
Pamięć współdzielona
Pamięć współdzielona jest to rodzaj pamięci umiejscowiony na multiprocesorze (on-chip memory), co powoduje, że ma wydajność identyczną z rejestrami.
Pamięć ta jest wspólna dla wątków z tego samego bloku, a na każdy blok
przypada jej 16 KB. Czas życia danych w niej zawartych jest równy czasowi
życia bloku wątków. Wynika stąd potrzeba wczytywania do niej danych przy
każdym uruchomieniu kernela. Według producenta, latencja (czyli czas dostępu) odczytów z pamięci współdzielonej może być nawet 100Ömniejsza niż
latencja odczytów z pamięci globalnej. Najefektywniejsze jej wykorzystanie
24
2.2. CUDA
polega na zastąpieniu komunikacji z pamięcią główną przez komunikację z
pamięcią współdzieloną. Zmienne w pamięci współdzielonej definiujemy w
kernalach za pomocą dyrektywy shared :
Listing 2.7: Tworznie tablicy w pamięci współdzielonej.
global
void f u n k c j a ( int * a )
1
{
shared
// . . .
3
5
int t a b l i c a [ 1 0 ] [ 1 0 ] ;
};
Pamięć tekstur
Jest to obszar pamięci stworzony z myślą o grafice, jest buforowany i optymalizowany sprzętowo pod kątem odczytów sąsiednich fragmentów pamięci
przez sąsiednie wątki. Oczywiście producent w żaden sposób nie ogranicza
dostępu do tych zasobów, więc je również można wykorzystać do obliczeń.
2.2.6
Wydajność
Aby uzyskać maksymalną wydajność, przede wszystkim należy skupić się na
znalezieniu jak najefektywniejszych możliwości zrównoleglenia jak największej części kodu sekwencyjnego. Mając do dyspozycji bardzo szybkie rodzaje
pamięci, należy w miarę możliwości używać tych, których przepustowość jest
największa. Bardzo niewskazane jest pisanie programów, w których często
przesyłamy duże ilości danych między CPU a GPU. Przepustowość łącza
PCI-Express to maksymalnie ok. 10 GBps, co przy 100 GBps przepustowości najwolniejszego rodzaju pamięci urządzenia jest wynikiem dość słabym.
Praktyka pokazuje, że zamiast teoretycznych 10 GBps szyna PCI-Express
przeciętnie daje przepustowości od 1 GBps do 4 GBps w zależności od jakości płyty głównej.
Wiemy już, że na urządzeniu znajdują się bardzo szybkie rodzaje pamięci
tj. pamięć współdzielona, stała oraz pamięć tekstur. Niestety ich zasoby są
dość skąpe, rzędu 10KB na blok wątków, co nie daje możliwości swobodnego
ich użycia, jaki znamy z programowania na CPU. Jednak umiejętne wykorzystanie i zastępowanie nimi najwolniejszej pamięci globalnej powoduje radykalne zwiększenie wydajności. Najlepszym zamiennikiem pamięci globalnej
25
ROZDZIAŁ 2. TECHNOLOGIA
jest pamięć współdzielona. Jest ona bardzo szybka i jest jej względnie dużo,
po 16KB na multiprocesor. Aby działała najwydajniej, należy tak pisać algorytmy, by wyeliminować tzw. “konflikty pamięci“, czyli aby dwa wątki nie
prosiły kontrolera pamięci o dostęp do tego samego zasobu równocześnie.
W przypadku, gdy mamy do czynienia z często wykorzystywanymi wartościami stałymi, warto umieścić je w pamięci stałej, jest ona równie szybka
jak pamięć współdzielona, lecz z poziomu GPU jest pamięcią tylko do odczytu, co znacznie zawęża klasę jej zastosowań.
Poza odpowiednim wykorzystaniem pamięci, ważne jest też odpowiednie wywołanie kernela. Producent zwraca uwagę na to, iż rdzenie multiprocesora
działające zgodnie z modelem SIMT muszą wykonywać tę samą instrukcję,
być może na różnych danych. W takim razie, jeśli np. instrukcjami warunkowymi spowodujemy, że któryś z rdzeni będzie musiał wykonać zadania
dodatkowe, to inne będą na niego czekać, co spowoduje drastyczny spadek
wydajności obliczeń.
26
3 Model FHP w technologii CUDA
3.1
Implementacja
Pomysł na implementację modelu FHP w technologii CUDA sprowadza się
do odpowiedniego podziału sieci modelu na mniejsze fragmenty odpowiadające blokom wątków, tak aby każdemu wątkowi odpowiadał dokładnie jeden
węzeł. Następnie wykonujemy kroku symulacji – przesuwamy cząstki i wykonujemy zderzenia. Pierwszym, intuicyjnym podejściem jest podział jak na
rysunku 3.1. Podejście takie bardzo dobrze sprawdza się w implementacji
Rysunek 3.1: Intuicyjny podział sieci na bloki.
zderzeń, każdemu wątkowi przyporządkowano bowiem dokładnie jeden węzeł, w którym wątek ten zmienia zastany stan według tablicy reguł zderzeń.
Nie ma mowy o konfliktach pamięci, zarówno odczyt z pamięci globalnej jak
i zapis do niej wykonywany jest raz.
27
ROZDZIAŁ 3. MODEL FHP W TECHNOLOGII CUDA
Listing 3.1: Implementacja kernela wykonującego zderzenia.
global
1
void z d e r z e n i a ( int * s i e c P o , int * s e e d s d e v )
{
int i = blockDim . x * b l o c k I d x . x + t h r e a d I d x . x ;
int j = blockDim . y * b l o c k I d x . y + t h r e a d I d x . y ;
3
5
int i n d e x = wConst * j+i ;
7
i f ( getRandomDouble ( s e e d s d e v , i n d e x ) <0.5)
siecPo [ index ] = tablicaLp devC [ siecPo [ index ] ] ;
else
siecPo [ index ] = tablicaRp devC [ siecPo [ index ] ] ;
9
11
}
Ze względu na to, iż funkcja ta jest dość krótka, dla zwiększenia wydajności,
warto dać jej dodatkowe obowiązki, które nie będą wymagały komunikacji
między blokami wątków np. wykonywanie pomiarów wielkości fizycznych.
Taki model podziału na bloki nie sprawdzi się jednak w przypadku implementacji funkcji odpowiedzialnej za ruch cząstek. Problemem są węzły
na granicach bloków. Istnieje możliwość utraty informacji, jeśli różne wątki
będą chciały pisać do tego samego węzła w tym samym czasie. Synchronizacja wątków jest możliwa poprzez użycie w kernelu funkcji wbudowanej
syncthreads(), jednak synchronizuje ona wątki tylko w obrębie bloku, czyli
zasięg jej działania ogranicza się co najwyżej do jednego multiprocesora. Możliwości efektywnej synchronizacji wątków należących do różnych bloków nie
ma.
Efektywny sposób obejścia tego problemu polega na zdecydowanie trudniejszym podziale na bloki niż w przypadku zderzeń. Rozwiązaniem jest takie
ułożenie bloków, aby na siebie zachodziły (rysunek 3.2), dane z nich będziemy kopiować do małych sieci tymczasowych w pamięci współdzielonej,
tam wykonamy wszystkie przesunięcia. Cząstki na skraju sieci w pamięci
współdzielonej mogą mieć kierunek powodujący wyjście takiej cząstki poza
obszar sieci. Moglibyśmy tę sytuację zaniedbać, ze względu na zachodzenie
na siebie bloków. Nie stracimy informacji o tej cząsteczce, po prostu blok
sąsiedni wykona jej ruch. Jednak pozwolenie programowi na pisanie poza pamięć przeznaczoną dla niego może prowadzić do fatalnych konsekwencji, tym
28
3.1. IMPLEMENTACJA
bardziej, że system operacyjny nie ma wglądu do tego, co dzieje się na GPU,
więc komunikaty typu segmentation fault nie występują, kod wykonuje się
normalnie, a na końcu uzyskuje się błędne wyniki. Stwórzmy więc w pamięci
współdzielonej tablicę o dwa węzły szerszą niż ta, w której przechowujemy
pobrane informacje. Do niej będziemy zapisywać przesunięte cząstki, lecz do
pamięci głównej wracać będzie tylko informacja z najbardziej wewnętrznego
prostokąta. Sytuację przedstawia rysunek 3.2. Prostokąt A to obszar sieci
Rysunek 3.2: Podział na bloki dla funkcji wykonującej ruch cząstek.
wczytany z pamięci głównej, prostokąt C służy do wyłapywania ruchów wychodzących poza prostokąt A, natomiast prostokąt B to część C z której
informacja po wykonaniu ruchów wróci do pamięci głównej. Oczywiście w
rzeczywistości, prostokąty te mają dużo większe rozmiary, np. 16 × 16.
Funkcja wykonująca ruch czątek według schematu z rysunku 3.1 przedstawiona została na listingu 3.2.
global
2
4
6
8
10
Listing 3.2: Funkcja wykonująca ruch cząstek.
void ruch ( int * s i e c P r z e d , int * s i e c P o )
{
shared
shared
int A [ 3 2 ] [ 1 6 ] ;
int C [ 3 4 ] [ 1 8 ] ;
int i=t h r e a d I d x . x+blockDim . x * b l o c k I d x . x−2* b l o c k I d x . x ;
int j=t h r e a d I d x . y+blockDim . y * b l o c k I d x . y−2* b l o c k I d x . y ;
int i n d e x = width * j+i ;
A[ t h r e a d I d x . x ] [ t h r e a d I d x . y ] = s i e c P r z e d [ i n d e x ] ;
29
ROZDZIAŁ 3. MODEL FHP W TECHNOLOGII CUDA
Rysunek 3.3: Odpowiednik rysunku 3.1 dla funkcji wykonującej ruch cząstek.
Część C z rysunku 3.2 nie została narysowana ze względu na czytelność.
// wykonaj p r z e s u n i e c i a z A do C
// . . .
12
14
i f ( t h r e a d I d x . x>0 and t h r e a d I d x . x<31 and \
t h r e a d I d x . y>0 and t h r e a d I d x . y<15)
{
s i e c P o [ i n d e x ]=C[ t h r e a d I d x . x + 1 ] [ t h r e a d I d x . y + 1 ] ;
}
16
18
20
}
Listing 3.2 linijka po linijce.
Omówmy listing 3.2. Argumentami funkcji ruch są wskaźniki na tablice reprezentujące sieci przed wykonaniem ruchu siecPrzed i po wykonaniu ruchu
siecPo, jak w rozdziale 1.6.3. W liniach 3,4 tworzymy w pamięci współdzielonej tablice A oraz C z rysunku 3.2. Linie 6,7,8 odpowiadają za wyznaczenie indeksu węzła w w tablicach siecPrzed oraz siecPo. Wyznaczenie
indeksów (i,j) węzła za pomocą zmiennych wbudowanych (rozdział 2.2.4)
nie jest zadaniem trywialnym. Wewnątrz kernela możemy określić, w któ30
3.1. IMPLEMENTACJA
rym wątku aktualnie jesteśmy, do którego bloku wątków należymy oraz jakie są parametry wywołania kernela, czyli wymiary modelu sieci bloków
wątków. Wyznaczenie miejsca (i,j) w sieci (tablicy) przy pomocy tych danych odbywa się następująco: szerokość bloku, czyli jego wymiar w kierunku x, czyli blockDim.x, mnożymy przez składową x numeru bloku, blockIdx.x, a następnie dodajemy do wyniku składową x numeru wątka threadIdx.x. Przeanalizujmy przykład z rysunku 2.1. Załóżmy, że znajdujemy
się w wątku (2,1) bloku (1,1) i chcemy uzyskać pierwszą składową i położenia wątka w sieci. W bloku o indeksie blockIdx.x = 0 mamy 4 wątki
o indeksach threadIdx.x = 0, 1, 2, 3, w następnym bloku blockIdx.x = 1
chcemy dojść do wątka o indeksie threadIdx.x = 2, więc przejdziemy indeksy
threadIdx.x = 0, 1, 2 w bloku blockIdx.x = 1. W pierwszym bloku przeszliśmy indeksy i = 0, 1, 2, 3 natomiast idąc dalej, przechodzimy i = 4, 5, 6, co
ilustruje tabela 3.1.
Tablica 3.1: Tabela ilustrująca przeliczanie indeksów wewnątrz kernela.
i
0 1 2 3 4 5 6
blockIdx.x
0
1
threadIdx.x 0 1 2 3 0 1 2
7 ...
...
3 ...
Stąd: int i = blockDim.x * blockIdx.x + threadIdx.x = 4 * 1 + 2 = 6.
Podejście takie działa dla podziału na bloki jak na rysunku 3.1, dlatego zastosowane zostało w listiungu 3.1. W przypadku podziału potrzebnego do
wykonania ruchu cząstek, w którym bloki sieci nachodzą na siebie, musimy
nanieść poprawki widoczne w omawianym listingu w linijce 6. Widzimy, że
wynik wcześniejszego rozumowania został pomniejszony o 2*blockIdx.x, co
wynika z nakładania się zielonych prostokątów na rysunku 3.3. Dla bloku o
coraz wyższym indeksie, przesunięcie w lewo w stosunku do rysunku 3.1 jest
coraz większe, skalowanie jest linowe ze współczynnikiem 2, gdyż prostokąty
z rysunku 3.3 mają dwa wspólne pola. Oczywiście, w kierunku y wszystko
odbywa się analogicznie.
W linii 10 każdy wątek wykonuje kopiowanie stanu wyznaczonego węzła z
pamięci globalnej siecPrzed do pamięci współdzielonej A. Następną czynnością jest przemieszczenie cząstek w pamięci współdzielonej z tablicy A do C.
Na koniec zapisujemy wynik do siecPo pamięci globalnej, kopiując wyłącznie
31
ROZDZIAŁ 3. MODEL FHP W TECHNOLOGII CUDA
obszar oznaczony na rysunku 3.2 jako B (wiersze 15 − 19).
3.2
Wydajność
Celem pracy była implementacja modelu FHP w technologii Nvidia CUDA,
w taki sposób, aby istniejące algorytmy sekwencyjne przyspieszyć, wykorzystując architekturę równoległą kart graficznych. Kod opisany w rozdziale 3.1
przetestowany został na kartach graficznych GTX260 oraz GTX285 wyposażonych w procesory GT200. Wyniki skonfrontowane zostały z implementacją sekwencyjną testowaną na komputerze wyposażonym w procesor Pentium(R) Dual-Core CPU [email protected] oraz pamięć RAM typu DDR2.
Zarówno wersja wykonywana na CPU, jak i ta w technologii CUDA uruchamiane były na systemie Ubuntu 10.04 Desktop x86-64 ze sterownikiem
Nvidia 195.36.24. Zmierzono czas wykonania 1000 kroków symulacji na CPU
(tCP U ) oraz na GPU (tGP U ). Wykres 3.4 przedstawia stosunek tCP U /tGP U w
zależności od ilości węzłów sieci modelu FHP. W implementacji przedstawionej w rozdziale 3.1 jeden wątek zajmuje się jednym węzłem, więc na rysunku
liczbę węzłów traktować można jako ilość wątków w jednym kroku symulacji.
Rysunek 3.4 podzielić możemy na 3 obszary, do 105 , od 105 do 107 i powyżej
107 węzłów. Widzimy, że w pierwszym z nich wydajność wzrasta wraz ze
wzrostem rozmiarów sieci. Małe przyspieszenie dla małych rozmiarów sieci
związane jest ze zbyt małą liczbą zadań w stosunku do liczby rdzeni na urządzeniu. Liczba zadań przypadających na multiprocesor musi być odpowiednio
duża, aby dobrze spożytkować jego moc obliczeniową. Karty graficzne z procesorami typu GT200 maksymalnie mogą obsługiwać 1024 wątki na multiprocesor jednocześnie. Maksymalna liczba aktywnych wątków na multiprocesor
pomnożona przez liczbę procesorów jest minimalną sensowną wielkością sieci,
dla GTX285 jest to 1024 · 30 = 30720. Maksimum wydajności osiągane jest
dla 30 razy większej wielkości sieci tj. 106 węzłów i do rozmiarów około 107
przyspieszenie obliczeń na GPU względem CPU osiąga poziom prawie 50×
dla słabszego GTX260 oraz około 60× dla GTX285. W obszarze powyżej
107 węzłów wydajność spada. Z rysunku 3.5 widzimy, że czasy wykonania
programów na GPU wzrastają liniowo wraz ze wzrostem ilości węzłów, natomiast rysunek 3.6 pokazuje, że dla bardzo dużych sieci > 107 wraz z dalszym
wzrostem rozmiarów, czasy działania na CPU przestają zmieniać się liniowo.
Stąd spadek przyspieszenia w tym obszarze widoczny na rysunku 3.4.
32
3.2. WYDAJNOŚĆ
Rysunek 3.4: Przyspieszenie uzyskane na Nvidia GT200 względem Pentium(R) Dual-Core CPU [email protected]
33
ROZDZIAŁ 3. MODEL FHP W TECHNOLOGII CUDA
Rysunek 3.5: Zależność czasu wykonania 1000 kroków na GPU od rozmiarów
sieci.
Rysunek 3.6: Czas wykonania 1000 kroków na GPU i CPU w funkcji rozmiaru
sieci.
34
4 Poprawność implementacji
Aby sprawdzić poprawność implementacji, porównałem wynik działania programu z rozwiązaniem teoretycznym i wynikiem eksperymentalnym. W przypadku symulacji fizycznych oczekiwany wynik powinien być jak najbardziej
zbliżony do danych doświadczalnych lub modelu teoretycznego, jeśli takowy
istnieje.
4.1
Porównanie z teorią
Jak wiemy z sekcji 1.1 model teoretyczny, czyli równania Naviera-Stokesa,
nie ma ogólnego rozwiązania. Jednym z nielicznych przypadków, dla których
możemy podać analityczne rozwiązanie, jest przepływ cieczy między dwiema
równoległymi płaszczyznami. Zapiszmy równania N-S [1] w dwóch wymiarach:
2
X
∂p
∂ui
∂
ui +
uj
= η∆ui −
+ fi (~x, t)
(4.1)
∂t
∂xj
∂xi
j=1
gdzie i ∈ 1, 2, u(~x, t)–prędkość, η - jest dodatnim współczynnikiem lepkości, p(~x, t)–ciśnienie, f (~x, t)–funkcja opisująca oddziaływanie zewnętrzne np.
grawitację oraz x1 = x, x2 = y. Równanie to opisuje ruch cieczy w R2 . Przedstawia ono zależność między wektorem prędkości ~u(~x, t) ∈ R2 i ciśnieniem
p(~x). Przepływ taki nazywamy przepływem Poiseuille’a [7], rysunek 4.1. Roz-
Rysunek 4.1: Przepływ Poiseuille.
35
ROZDZIAŁ 4. POPRAWNOŚĆ IMPLEMENTACJI
piszmy równanie N-S dla składowej x:
!
∂
∂ux
∂ux
∂2
∂2
∂p
ux + ux
+ uy
=η
+ fx (~x, t)
u
+
ux −
x
2
2
∂t
∂x
∂y
∂x
∂y
∂x
(4.2)
Powyższe wyrażenie dla przepływu Poiseuille’a znacznie się upraszcza:
ˆ
∂
u
∂t x
= 0. Przepływ jest stacjonarny, niezależny od czasu.
2
∂
x
= 0 oraz ∂x
ˆ ux ∂u
2 ux = 0. Ponieważ ux zależy tylko od składowej y.
∂x
Dla dowolnie wybranego przekroju w kierunku OX rozkład ux jest taki
sam.
x
= 0 gdyż uy = 0. Wektor prędkości ma tylko składową poziomą.
ˆ uy ∂u
∂y
ˆ fx (~x, t) = 0. Ponieważ fi (~x, t) jest członem opisującym wkład sił zewnętrznych które w tym przypadku nie występują.
Uwzględniając wszystkie powyższe punkty:
0=η
∂p
∂2
ux −
2
∂y
∂x
(4.3)
Tak uproszczone równanie N-S jesteśmy w stanie rozwiązać analitycznie, gdyż
jest to równanie różniczkowe o rozdzielonych zmiennych:
1 ∂p
∂2
ux =
2
∂y
η ∂x
zakładając, że
∂p
∂x
(4.4)
= const i dwukrotnie całkując obustronnie, dostajemy:
ux =
1 ∂p 2
y + C1 y + C2
2η ∂x
(4.5)
gdzie C1 ,C2 są stałymi całkowania. W miejscu spotkania się płaszczyzn i
cieczy, czyli dla y = 0 oraz y = h, prędkość ux = 0, stąd warunek brzegowy
ux (0) = 0 i ux (h) = 0. Prowadzi on do wyznaczenia stałych C2 = 0 oraz C1 =
∂p
− η1 ∂x
h. Ostatecznie, rozwiązaniem równania N-S dla przepływu Poiseuille’a
jest:
1 ∂p
ux (y) =
y (y − h)
(4.6)
2η ∂x
36
4.1. PORÓWNANIE Z TEORIĄ
∂p
Przyjmując gradient ciśnienia za stały wzdłuż całego przekroju rury, ∂x
=
∂p
∆p
const, możemy zapisać ∂x = l , gdzie ∆p jest różnicą ciśnień na końcach
układu, a l długością rury. Jeśli przepływ odbywa się w prawo, jak na rysunku 4.1, to ∆p < 0, więc:
ux (y) = −
|∆p|
y (y − h)
2ηl
(4.7)
Powyższe rozwiązanie mówi, że rozkład prędkości dla dowolnego przekroju
równoległego do osi OY ma charakter paraboliczny. Wykonując doświadczenie numeryczne oczekujemy, że przepływ przez pustą rurę będzie miał
taki właśnie charakter [8]. Wynik działania aplikacji widać na rysunku 4.2.
Zielona linia jest parabolą 4.7 o współczynnikach dopasowanych metodą najmniejszych kwadratów. Widać, że dopasowanie jest bardzo dobre.
Rysunek 4.2: Przepływ Poiseuille’a symulowany modelem FHP. Punkty są
wynikiem doświadczenia komputerowego, zielona linia jest parabolą o współczynnikach wyznaczonych metodą najmniejszych kwadratów z dopasowania
wzoru 4.7 do danych numerycznych.
37
ROZDZIAŁ 4. POPRAWNOŚĆ IMPLEMENTACJI
4.2
Porównanie z doświadczeniem
Na początku 2010 r. grupa badawcza z Mathematical Modeling Group z Max
Planck Institute for Marine Microbiology pod kierunkiem A. Khalili wykonała doświadczenie mające na celu wyznaczenie linii prądu dla przepływu
stacjonarnego w ośrodku porowatym. Doświadczenie polegało na zbudowaniu układu przeszkód jak na rysunku 4.3 w płaskim kanale o długości 7.1 cm
i grubości 0.16 cm, wymuszeniu przepływu cieczy i wykonaniu pomiarów
metodami microPIV [9]. Particle Image Velocimetry (PIV), jest metodą
pomiarową służącą do pomiaru prędkości przepływu cieczy i wyznaczenia li-
Rysunek 4.3: Układ przeszkód w doświadczeniu microPIV.
Rysunek 4.4: Wynik doświadczenia microPIV. Zdjęcie wykonał Kolja Kindler
z MPI-Bremen.
38
4.2. PORÓWNANIE Z DOŚWIADCZENIEM
nii prądu. W cieczy umieszcza się cząstki będące znacznikami fluorescencyjnymi, które płyną wraz z prądem. Pomiar polega na oświetleniu znaczników
wiązką lasera i odpowiednio częstym wykonywaniu zdjęć, z których obraz jest
odpowiednio analizowany. Jednym z elementów analizy jest uśrednianie kolejnych pomiarów dlatego metoda może służyć jedynie do badania przepływów
stacjonarnych. Wynik doświadczenia przedstawia rysunek 4.4. Symulacja dla
identycznego układu wykonane zostało metodami komputerowymi, przy użyciu algorytmu FHP zaimplementowanego na potrzeby niniejszej pracy w
technologii Nvidia CUDA. Wynik symulacji przedstawia rysunek 4.6. Kolory przedstawiają rozkład modułu prędkości, kolory ciemniejsze (niebieski)
przedstawiają prędkość mniejszą, jaśniejsze (czerwony) większą. Skalę barw
przedstawia rysunek 4.5. Wynikiem działania programu wykonującego symulację jest pole prędkości, czyli w każdym węźle sieci otrzymujemy średnią
wartość vx oraz vy . Aby wyznaczyć linie prądu, należy przecałkować dwuwymiarowe dyskretne pole prędkości. Całkowanie wykonane zostało metodą
Rysunek 4.5: Skala barw odpowiadających modułowi prędkości.
Rysunek 4.6: Wynik symulacji metodą FHP w technologii Nvidia CUDA.
39
ROZDZIAŁ 4. POPRAWNOŚĆ IMPLEMENTACJI
Runge-Kutta czwartego rzędu. Podczas wykonywania kroku całkowania możemy trafić w miejsce między węzłami, musimy wtedy interpolować wartość
modułu prędkości w takim punkcie na podstawie węzłów sąsiednich. Interpolacja została dokonana metodą biliniową.
Jakościowe porównanie wyników doświadczalnych z wynikami symulacji
przedstawiają rysunki 4.7. Podobieństwo wyników jest uderzające.
40
4.2. PORÓWNANIE Z DOŚWIADCZENIEM
1
2
3
4
Rysunek 4.7: Porównanie wyników doświadczenia (po lewej) z symulacją (po
prawej).
41
ROZDZIAŁ 4. POPRAWNOŚĆ IMPLEMENTACJI
42
5 Galeria
Rysunki zamieszczone w galerii przedstawiają wyniki symulacji przeprowadzonych programem napisanym na potrzeby niniejszej pracy. Wynikiem działania programu jest pole prędkości, czyli wartość średnia składowej poziomej
oraz pionowej wektora prędkości dla każdego węzła. Rysunki 5.1, 5.3, 5.4
przedstawiają linie prądu wygenerowane z tego pola prędkości przy pomocy
programu Paraview [10]. Na rysunku 5.2 widzimy barwową reprezentację
średniej długości wektora prędkości. Obraz 5.2 jest zrzutem ekranu interfejsu graficznego napisanego w technologii OpenGL. Wszystkie obliczenia i
operacje graficzne odbywają się bezpośrednio na karcie graficznej. Dla sieci
1372 na 702 (blisko 1 milion węzłów) program generuje ok. 150 klatek na
sekundę.
Rysunek 5.1: Przepływ przez rurę z płaską przeszkodą, linie prądu wygenerowano w programie Paraview[10].
43
ROZDZIAŁ 5. GALERIA
Rysunek 5.2: Scieżki Von Karmana za cylindryczną przeszkodą, rozkład prędkości.
Rysunek 5.3: Przepływ przez rurę z płaską przeszkodą, linie prądu wygenerowano w programie Paraview[10]. Przepływ turbulentny.
44
Rysunek 5.4: Przepływ przez rurę z płaską przeszkodami, linie prądu wygenerowano w programie Paraview[10].
45
ROZDZIAŁ 5. GALERIA
46
Bibliografia
[1] Clay Mathematics Institute Millennium Prize Problems
http://www.claymath.org/millennium/NavierStokes Equations/navierstokes.pdf
[2] Dieter A. Wolf-Gladrow, Lattice-Gas Cellular Automata and Lattice
Boltzmann Models. An Introduction. Springer-Verlag 2000 r.
[3] U.Frisch, B.Hasslacher, Y.Pomeau Lattice Gas Automata for the NavierStokes Equation, Physical Review Letters vol. 56, str. 1505-1508, 1986 r.
[4] M.C.Sukop, D.T. Thorne Jr. Lattice Boltzmann Modeling, Springer,
2006 r.
[5] B.Chopard, M.Drozd Cellular Automata Modeling of Physical Systems,
Cambridge University Press, 1998 r.
[6] NVIDIA, NVIDIA CUDA. Programming Guide, ver. 3.1, 2010 r.
[7] L.D.Landau, E.M.Lifszyc Hydrodynamika, PWN, 1994 r.
[8] S.Chen, K.Diemer, G.D.Doolen, K.Eggert, C.Fu, S.Gutman, B.J.Travis
Lattice gas automata for flow through porous media., Physica D 47,
str.72-84, 1991 r.
[9] M.Raffel, C.Willert, J.Kompenhans. Particle Image Velocimetry. A
Practical Guide. Springer 1998 r.
[10] Kitware Inc. Paraview data analysis and visualization application.
http://www.paraview.org.
47

Podobne dokumenty

Przetwarzanie obrazów w czasie rzeczywistym za pomocą GPU

Przetwarzanie obrazów w czasie rzeczywistym za pomocą GPU 6 Amerykańska firma komputerowa; jeden z największych na świecie producentów procesorów graficznych i innych układów scalonych przeznaczonych na rynek komputerowy - www.nvidia.com 7 OpenGL - Open G...

Bardziej szczegółowo