Dystrybucja wektorów i macierzy
1. Wstęp
Dystrybucja to zagadnienie, które pojawia się przy zrównoleglaniu
algorytmów operujących na większej ilości danych. Podstawowym problemem
na jaki napotykamy jest wtedy określenie miejsca przechowywania danych:
- Czy każdy proces ma posiadać lokalną kopię wszystkich danych wejściowych?
- Czy też może natura algorytmu pozwala na przydzielenie każdemu
procesowi pewnej porcji danych wystarczającej do wykonania zadania?
- Możliwe jest również podejście hybrydowe: proces początkowo posiada
pewien fragment danych, a w trakcie obliczeń pobiera inne niezbędne
elementy komunikując się z innymi procesami.
Najczęściej staramy się (w celu zminimalizowania ogólnych kosztów
komunikacji) podzielić dane na jakieś logicznie odrębne fragmenty i
przesłać je do właściwych procesów. Różne rodzaje takiego podziału
(inaczej: dystrybucji tych danych) są główną treścią tego
referatu.
Odrębnym problemem (po ustaleniu rodzaju dystrybucji) jest fizyczna
realizacja dostarczania danych do procesów. Większość systemów
programowania równoległego dostarcza specjalne narzędzia do tego celu.
Aby zminimalizować koszty czasowe stosowane są specjalne algorytmy, takie
jak hypercube. Jako przykład podam tutaj specjalizowane funkcje
HPF (High Performance Fortran) służące do dystrybucji
wektorów i macierzy według zadanych schematów.
2. Dystrybucja wektorów
Mając za zadanie wykonanie pewnych obliczeń na dużych wektorach (np.
iloczyn skalarny) możemy podzielić je na kilka sposobów. Pierwszy z nich
to
2.1. Dystrybucja blokowa
Jest to najprostszy rodzaj dystrybucji. Polega on na tym, że dla
k procesów i wektora długości n obliczamy
NB jako część całkowitą z dzielenia
n/k i dzielimy wektor na k
części, z których pierwsze k-1 ma długość NB,
ostatnia zaś może być krótsza (jeżeli reszta z dzielenia jest niezerowa):
Dla uproszczenia rezygnujemy z pozornie lepszej metody, w której
"brakujące" elementy są rozdzielane pojedynczo na ostatnich pozycjach
w końcowych procesach:
Takie rozwiązanie zapewniłoby lepszy podział pracy, lecz wiązałoby się
z dodatkowymi obliczeniami.
System HPF realizuje dstrybucję blokową za pomocą funkcji
DISTRIBUTE. Używamy jej w następujący sposób:
REAL :: X( N )
!HPF$ PROCESSORS PROC( P )
!HPF$ DISTRIBUTE X( BLOCK( NB ) ) ONTO PROC
Gdzie NB to długość bloku obliczana przez system.
2.2. Dystrybucja cykliczna
Jest to przeciwieństwo dystrybucji blokowej - tutaj dane dla jednego
procesu są maksymalnie rozproszone w wektorze wejściowym. Dane dla
procesu i są brane z pozycji:
i,k+i,2k+i,...,[n/k]*k+i.
Zależności widać na poniższym rysunku:
Najprostsza forma dystrybucji blokowej w HPF jest realizowana
następująco:
REAL :: X( N )
!HPF$ PROCESSORS PROC( P )
!HPF$ DISTRIBUTE X( CYCLIC ) ONTO PROC
2.3. Dystrybucja blokowo-cykliczna
Jest to połączenie właściwości dwóch omówionych typów. Dane dla jednego
procesu są grupowane w bloki o zadanej długości i rozmieszczone w
jednakowch odstępach na wektorze. Na rysunku wygląda to następująco:
Dla zadanej długości bloku NB w HPF realizacja
dystrybucji blokowo-cyklicznej wygląda następująco:
REAL :: X( N )
!HPF$ PROCESSORS PROC( P )
!HPF$ DISTRIBUTE X( CYCLIC( NB ) ) ONTO PROC
W ogólnym przypadku nie musimy zaczynać dystrybucji danych od procesu
o numerze 0. Przykładowo poprzedni schemat mógłby mieć następującą
postać:
Odpowiedni kod w HPF ma postać:
REAL :: X( N )
!HPF$ PROCESSORS PROC( P )
!HPF$ TEMPLATE T( N + P*NB )
!HPF$ DISTRIBUTE T( CYCLIC( NB ) ) ONTO PROC
!HPF$ ALIGN X( I ) WITH T( SRC*NB + I )
3. Dystrybucja macierzy
W przypadku macierzy decyzja o wyborze dystrybucji jest trudniejsza,
możemy bowiem rozpatrywać osobno obydwie współrzędne każdego elementu.
W praktyce dla macierzy dwuwymiarowych stosuje się najczęściej następujące
rodzaje dystrybucji:
3.1. Dystrybucje z podziałem według jednej współrzędnej
W tym przypadku mamy coś w rodzaju "rozciągnięcia" opisanych w punkcie
2. dystrybucji wektorów. Ustalona współrzędna elementu macierzy jednoznacznie
wyznacza proces, który tym elementem zarządza, natomiast druga współrzędna
może się zmieniać dowolnie w obrębie procesu.
Poniższe schematy obrazują różne rodzaje takiej dystrybucji w której
jednostką elementarną przy rozdzielaniu danych są kolumny macierzy.
Analogicznie wyglądają schematy dla wierszy - rysunki
należy wtedy podać transpozycji.
3.1.1. Dystrybucja blokowa
3.1.2. Dystrybucja cykliczna
3.1.3. Dystrybucja blokowo - cykliczna
3.2. Przypadek ogólny
W wielu przypadkach istnieje potrzeba bardziej skomplikowanego
rozmieszczenia danych. Jako przykład można podać eliminację Gaussa,
w której najlepsze zrównoważenie obliczeń wśród procesów zapewnia
dystrybucja podwójnie cykliczna (two-dimensional block-cyclic
distribution), wyglądająca następująco:
W tym przypadku każdy szary kwadrat reprezentuje podmacierz kwadratową
o ustalonym (najczęściej niewielkim) wymiarze. Każdy proces otrzymuje
pewien fragment macierzy wejściowej, lecz fragmenty te są równomiernie
rozproszone po jej całym obszarze.
HPF zapewnia realizację dowolnych kombinacji dystrybucji blokowych
i cyklicznych przez podanie odpowiednich parametrów do polecenia
DISTRIBUTE. Przykładowo dla macierzy 17x17 dystrybucja
jej elementów blokami wielkości 3x3 pomiędzy 4 procesy ułożone w
kwadrat 2x2 jest osiągana w następujący sposób:
REAL :: A( 17, 17 )
!HPF$ PROCESSORS PROC( 2, 2 )
!HPF$ DISTRIBUTE A( CYCLIC( 3 ), CYCLIC( 3 ) ) ONTO PROC
Ze składni poleceniai widać, że możemy dowolnie składać ze sobą różne
typy dystrybucji (blokowa+cykliczna), jak również uogólnić całą operację
na wyższe wymiary - sposób użycia prawie wcale się nie zmienia.
Jak łatwo się domyśleć, nie wszystkie procesy otrzymają taką samą ilość
danych. Szczegóły implementacyjne w HPF są przed programistą ukryte,
ma on do dyspozycji procedurę NUMROC, która pozwala określiś
rozmiar macierzy przekazanej do procesu.
4. Wydajność komunikacji
Odpowiedni dobór dystrybucji danych może w znaczący sposób poprawić
(czytaj: zmniejszyć) nakłady na wymianę danych między procesami.
Weźmy jako przykład obliczenia w problemie różnic skończonych na
siatce dwuwymiarowej. Natura problemu jest taka, że naturalny podział
zadań można sobie wyobrazić następująco:
Jak łatwo policzyć, wymiana danych w obrębie kwadratu o wymiarach np.
4x4 wymaga wysłania 64 pakietów danych, każdy z użyciem osobnego
komunikatu. Po przeorganizowaniu siatki tak, aby w jednym miejscu
zgromadzić dane (i wykonywać obliczenia) dla całego kwadratu, mamy zyski
dwojakiego rodzaju:
- Odpadają nam koszty komunikacji "wewnątrz" kwadratu
- Komunikacja na zewnątrz odbywa się 4-elementowymi pakietami,
zamiast pojedynczych pakietów, co w oczywisty sposób zwiększa wydajność.
Ulepszony schemat jest przedstawiony na kolejnym rysunku:
5. Algorytmy
Istnieją algorytmy, które narzucają z góry wybór dystrybucji danych
między procesy. Jako przykład wybrałem algorytm transpozycji macierzy
wymiaru n (gdzie n - potęga dwójki) o złożoności
O(log(n)). Polega on na rekurencyjnym dzieleniu macierzy na
równe części i transpozycji tych części. Schemat działania algorytmu
obrazuje rysunek:
Jak widać, narzucającym się tu rodzajem dystrybucji jest podział
blokowy w jednej osi (i to najlepiej dla bloków rozmiaru 1). Po zejściu
z podmacierzami do wymiaru pojedynczego bloku możemy je transponować już
"ręcznie", w obrębie jednego procesu.
6. Literatura
|