Programowanie równoległe Optymalizacja dostępu do pamięci GPU Elementarne algortymy równoległe Rafał Skinderowicz
Optymalizacja dostępu do pamięci Wątki wewnątrz osnów (ang. warps) wykonują jednocześnie odczyt / zapis do pamięci (SIMD) 32 wątki 32 adresy Jeżeli jeden czeka, to wszystkie czekają W zależności od adresów odczyt może być wykonany w jednej transakcji na pamięci albo wielu
Wzorce dostępu do pamięci globalnej Pamięć globalna wątki Łączony (ang. coalesced) dostęp do pamięci Dane odczytywane są segmentami (ang. chunk) nawet jeżeli potrzebujemy tylko jeden element (słowo)
Wzorce dostępu do pamięci globalnej Pamięć globalna wątki Łączony (ang. coalesced) dostęp do pamięci Rozłączny dostęp do pamięci
Łączony dostęp do pamięci 1 // Przykłady łączonego dostępu do pamięci 2 sum += data[ get_global_id(0) ]; 1 // Przykład niełączonego dostępu 2 sum += data[ get_global_id(0) * 4 ];
Wzorce dostępu do pamięci globalnej W architekturze Nvidia Fermi dostępne są dwa rodzaje odczytów: Buforowany (ang. cached) tryb domyślny: próba odczytu z L1, następnie L2, następnie pam. globalnej dane odczytywane są w 128 bajtowych porcjach (32 x 4B) wiersz pam. podręcznej Nie buforowane: próba odczytu z L2, następnie z pam. globalnej dane odczytywane są w porcjach po 32 bajty Jeden rodzaj zapisu do pam. globalnej unieważnienie L1 zapis do L2
Przykładowe wzorce odczytu 32 wątki odczytują kolejno 4-bajtowe słowa Adresy mieszczą się w 1 wierszu pam. podręcznej 128 bajtów przesyłanych magistralą Addresses from a warp... 0 32 64 96 128 160 192 224 256 288 320 352 384 416 448 Memory addresses
Przykładowe wzorce odczytu 32 wątki odczytują 4-bajtowe słowa, adresy wymieszane Adresy mieszczą się w 1 wierszu pam. podręcznej 128 bajtów przesyłanych magistralą addresses from a warp... 0 32 64 96 128 160 192 224 256 288 320 352 384 416 448 Memory addresses
Przykładowe wzorce odczytu 32 wątki odczytują 4-bajtowe słowa, kolejne adresy Adresy mieszczą się w 2 wierszach pam. podręcznej 256 bajtów przesyłanych magistralą (50% transferu użyteczne) addresses from a warp... 0 32 64 96 128 160 192 224 256 288 320 352 384 416 448 Memory addresses
Przykładowe wzorce odczytu 32 wątki odczytują to samo 4-bajtowe słowo Adres mieści się w 1 wierszu pam. podręcznej 128 bajtów przesyłanych magistralą (4/128 = 3.125% transferu użyteczne) addresses from a warp... 0 32 64 96 128 160 192 224 256 288 320 352 384 416 448 Memory addresses
Przykładowe wzorce odczytu 32 wątki odczytują 4 bajtowe słowa pod różnymi, rozproszonymi adresami Adresy mieszczą się w N wierszach pam. podręcznej N 128 bajtów przesyłanych magistralą z czego 128/(N 128) użyteczne addresses from a warp... 0 32 64 96 128 160 192 224 256 288 320 352 384 416 448 Memory addresses
Pamięć współdzielona konflikty Pamięć współdzielona podzielona jest na banki W pojedynczym cyklu można odczytać słowo z każdego banku W przypadku gdy kilka wątków próbuje odczytać dane z tego samego banku następuje konflikt żądania odczytu są szeregowane Bank 0 Bank 1 Bank 2 Bank 3 Bank 4 Bank 5 Bank 6 Bank 7 Bank 15
Pamięć współdzielona konflikty Thread 0 Thread 1 Thread 2 Thread 3 Thread 4 Thread 5 Thread 6 Thread 7 Bank 0 Bank 1 Bank 2 Bank 3 Bank 4 Bank 5 Bank 6 Bank 7 Thread 0 Thread 1 Thread 2 Thread 3 Thread 4 Thread 5 Thread 6 Thread 7 Bank 0 Bank 1 Bank 2 Bank 3 Bank 4 Bank 5 Bank 6 Bank 7 Thread 15 Bank 15 Thread 15 Bank 15 Jeżeli każdy wątek próbuje odczytać dane z innego banku nie ma konfliktu
Pamięć współdzielona konflikty Thread 0 Thread 1 Thread 2 Thread 3 Thread 4 Thread 8 Thread 9 Thread 10 Thread 11 Bank 0 Bank 1 Bank 2 Bank 3 Bank 4 Bank 5 Bank 6 Bank 7 Bank 15 Thread 0 Thread 1 Thread 2 Thread 3 Thread 4 Thread 5 Thread 6 Thread 7 Thread 15 x8 x8 Bank 0 Bank 1 Bank 2 Bank 7 Bank 8 Bank 9 Bank 15 Przykłady konfliktów 2-drożnego oraz 8-drożnego
Konflikty przykład Załóżmy, że pamięć podzielona jest na 4 banki Dana jest macierz int t[4][4] 1 // Komórki macierzy 2 t[0][0] t[0][1] t[0][2] t[0][3] 3 t[1][0] t[1][1] t[1][2] t[1][3] 4 t[2][0] t[2][1] t[2][2] t[2][3] 5 t[3][0] t[3][1] t[3][2] t[3][3] 1 // Numery banków komórek 2 0 1 2 3 3 0 1 2 3 4 0 1 2 3 5 0 1 2 3
Konflikty przykład Załóżmy, że pamięć podzielona jest na 4 banki Dana jest macierz int t[4][4] 1 // Komórki macierzy 2 t[0][0] t[0][1] t[0][2] t[0][3] 3 t[1][0] t[1][1] t[1][2] t[1][3] 4 t[2][0] t[2][1] t[2][2] t[2][3] 5 t[3][0] t[3][1] t[3][2] t[3][3] 1 // Numery banków komórek 2 0 1 2 3 3 0 1 2 3 4 0 1 2 3 5 0 1 2 3 Przy próbie zapisu przez 4 wątki: 1 int x = get_local_id(0); 2 t[0][x] =... nie ma konfliktu
Konflikty przykład Załóżmy, że pamięć podzielona jest na 4 banki Dana jest macierz int t[4][4] 1 // Komórki macierzy 2 t[0][0] t[0][1] t[0][2] t[0][3] 3 t[1][0] t[1][1] t[1][2] t[1][3] 4 t[2][0] t[2][1] t[2][2] t[2][3] 5 t[3][0] t[3][1] t[3][2] t[3][3] 1 // Numery banków komórek 2 0 1 2 3 3 0 1 2 3 4 0 1 2 3 5 0 1 2 3 Przy próbie zapisu przez 4 wątki: 1 int x = get_local_id(0); 2 t[x][0] =... pojawia się konflikt 4-drożny, wszystkie wątki zapisują do banku 0
Konflikty przykład W celu rozwiązania konfliktu można zmienić rozmieszczenie elementów w pamięci, tak aby znalazły się w różnych bankach. 1 int t[4][4 + 1]; // dodatkowy element zmienia rozmieszczenie elementów w 2 // pamięci 1 // Komórki macierzy 2 [0][0] [0][1] [0][2] [0][3] [0][4] 3 [1][0] [1][1] [1][2] [1][3] [1][4] 4 [2][0] [2][1] [2][2] [2][3] [2][4] 5 [3][0] [3][1] [3][2] [3][3] [3][4] 1 // Numery banków komórek 2 0 1 2 3 0 3 1 2 3 0 1 4 2 3 0 1 2 5 3 0 1 2 3
Konflikty przykład W celu rozwiązania konfliktu można zmienić rozmieszczenie elementów w pamięci, tak aby znalazły się w różnych bankach. 1 int t[4][4 + 1]; // dodatkowy element zmienia rozmieszczenie elementów w 2 // pamięci 1 // Komórki macierzy 2 [0][0] [0][1] [0][2] [0][3] [0][4] 3 [1][0] [1][1] [1][2] [1][3] [1][4] 4 [2][0] [2][1] [2][2] [2][3] [2][4] 5 [3][0] [3][1] [3][2] [3][3] [3][4] 1 // Numery banków komórek 2 0 1 2 3 0 3 1 2 3 0 1 4 2 3 0 1 2 5 3 0 1 2 3 Teraz zapis pierwszej kolumny t 1 int x = get_local_id(0); 2 t[x][0] =... będzie dotyczył różnych banków pamięci.
Pamięć współdzielona konflikty W celu wykrycia konfliktów można skorzystać z profilera Nvidia Visual Profiler AMD CodeXL Usunięcie konfliktów wymaga zmiany kolejności operacji na pamięci współdzielonej lub umieszczenia danych w pamięci w taki sposób, by nie zachodził konflikt
Pamięć lokalna w OpenCL uwagi Ze względu na krótki czas dostępu często można uzyskać przyspieszenie obliczeń zapisując dane w pamięci lokalnej grupy (bloku) wątków 1 #define TILE_SIZE 32 2 3 kernel void transpose(int size, 4 global int *A, 5 global int *B) { 6 local int tmp[tile_size][tile_size]; // tablica pomocnicza Problem w tym, że rozmiar tmp musi być znany na etapie kompilacji kernela, a nie jego uruchomienia
Pamięć lokalna w OpenCL uwagi Rozwiązaniem jest przekazanie tablicy w pamięci lokalnej w postaci parametru kernela 1 kernel void transpose(int size, 2 global int *A, 3 global int *B, 4 local int *tmp, 5 int tile_size) { 6 tmp[ y * tile_size + x ] =... Przykład przekazania parametru 1 int tile_size = 32; 2 clsetkernelarg(kernel_id, 3, tile_size * tile_size * sizeof(int), NULL); 3 clsetkernelarg(kernel_id, 4, sizeof(int), &tile_size);
Odwzorowanie Algorytm odwzorowania (ang. map) przekształca pojedynczy element wektora (macierzy itp.) wejściowego na element wektora (macierzy itp.) wynikowego Obliczenia dla poszczególnych elementów są niezależne od siebie
Odwzorowanie Typem odwzorowania jest również szablon (ang. stencil), w którym wartość elementu wynikowego zależy od kilku elementów sąsiednich określonych przez ustalony szablon Obliczenia dla poszczególnych elementów są niezależne od siebie, pomimo że sąsiedztwa nakładają się Rysunek : Przypadek 2D Rysunek : Zależności danych w przypadku danych trójwymiarowych
Redukcja Algorytm redukcji Dane wejściowe uporządkowany zbiór elementów [a 0, a 1,..., a n 1 ] binarny operator, który jest łączny Wynik: (a 0 a 1 a 2... a n 1 )
Redukcja łączność Operator jest łączny w zbiorze S, jeżeli a,b,c S a (b c) = (a b) c
Redukcja łączność Operator jest łączny w zbiorze S, jeżeli a,b,c S a (b c) = (a b) c Przykładowe operatory łączne: suma (a + b) iloczyn (a b) suma logiczna (a b) iloczyn logiczny (a b) minimum min(a, b) maksimum max(a, b) złożenie funkcji f (g h) = (f g) h
Redukcja łączność Operator jest łączny w zbiorze S, jeżeli a,b,c S a (b c) = (a b) c Przykładowe operatory łączne: suma (a + b) iloczyn (a b) suma logiczna (a b) iloczyn logiczny (a b) minimum min(a, b) maksimum max(a, b) złożenie funkcji f (g h) = (f g) h Przykładowe operatory niełączne: różnica (a b) iloraz (a/b) potęgowanie a b
Redukcja sekwencyjnie Algorytm sekwencyjny 1 sum = a[0] 2 for (i = 1; i < n; i++) 3 sum = sum + a[i] 4 return sum Złożoność: n 1 operacji, czyli O(n) Wynik kolejnej iteracji zależy od wyniku poprzednich
Redukcja równolegle a b c d Kolejność wykonania operacji w algorytmie to: ((a b) c) d a b c d
Redukcja równolegle a b c d Kolejność wykonania operacji w algorytmie to: ((a b) c) d a b c Jednak zgodnie z założeniem operator jest łączny, co pozwala na zmianę kolejności wykonania operacji d
Redukcja równolegle a b c d Kolejność wykonania operacji w algorytmie to: ((a b) c) d a b c d Jednak zgodnie z założeniem operator jest łączny, co pozwala na zmianę kolejności wykonania operacji (a b) (c d)
Redukcja równolegle ((a b) c) d (a b) (c d) a b c d a b c d a b a b c d c d 3 operacje 3 kroki (wysokość drzewa) 3 operacje 2 kroki
Redukcja równolegle Data: a[0... n 1] Result: Wartość redukcji w a[n 1] for d from 0 to log 2 n 1 do for i from 0 to n 1 by 2 d+1 do in parallel a[i + 2 d+1 1] a[i + 2 d 1] a[i + 2 d+1 1] end end
Redukcja równolegle Data: a[0... n 1] Result: Wartość redukcji w a[n 1] for d from 0 to log 2 n 1 do for i from 0 to n 1 by 2 d+1 do in parallel a[i + 2 d+1 1] a[i + 2 d 1] a[i + 2 d+1 1] end end Złożoność: O(log n) przy n/2 procesorach
Redukcja OpenCL Wersja dla pojedynczego bloku wątków 1 kernel void reduce(int n, global int *data) { 2 const int tid = get_local_id(0); 3 for (int k = 1; k < n; k *= 2) { 4 if ((tid+1) % (2*k) == 0) { 5 data[ tid ] += data[ tid - k ]; 6 } 7 barrier(clk_local_mem_fence); 8 } 9 // wynik jest w data[n-1] 10 } k = 2 d Sprawdzenie d < log 2 n można zastąpić 2 d < n k < n
Redukcja OpenCL Wersja alternatywna 1 kernel void reduce(int n, global int *data) { 2 const int tid = get_local_id(0); 3 const int group_size = get_local_size(0); 4 for (int k = group_size / 2; k > 0; k /= 2) { 5 if ( tid < k ) { 6 data[ tid ] += data[ tid + k ]; 7 } 8 barrier(clk_local_mem_fence); 9 } 10 // wynik w data[0] 11 } Wersja działa dla tablicy data o długości równej liczbie wątków w grupie roboczej (bloku wątków)
Redukcja OpenCL Wersja alternatywna 1 kernel void reduce(int n, global int *data) { 2 const int tid = get_local_id(0); 3 const int group_size = get_local_size(0); 4 for (int k = group_size / 2; k > 0; k /= 2) { 5 if ( tid < k ) { 6 data[ tid ] += data[ tid + k ]; 7 } 8 barrier(clk_local_mem_fence); 9 } 10 // wynik w data[0] 11 } Wersja działa dla tablicy data o długości równej liczbie wątków w grupie roboczej (bloku wątków) Ponieważ połowa wątków jest nieaktywna od początku, to można zmienić for (int k = group_size / 2; k > 0; k /= 2) na for (int k = group_size; k > 0; k /= 2) i uruchomić algorytm z liczbą wątków w grupie o połowę mniejszą
Redukcja OpenCL Przykład wersja 1 0 [ 3 1 7 0 4 1 6 3 ] 1 [ 3 4 7 7 4 5 6 9 ] 2 [ 3 4 7 11 4 5 6 14 ] 0 [ 3 4 7 11 4 5 6 25 ] Przykład wersja 2 0 [ 3 1 7 0 4 1 6 3 ] 1 [ 7 2 13 3 4 1 6 3 ] 2 [ 20 5 13 3 4 1 6 3 ] 0 [ 25 5 13 3 4 5 6 3 ]
Redukcja OpenCL Liczba wątków w grupie roboczej jest mocno ograniczona Np. dla Nvidia Fermi maks. liczba wątków w grupie to 1024 Daje to możliwość redukcji dla tablic o dł. 2048 W przypadku większych tablic trzeba redukcję przeprowadzać etapami równocześnie dla porcji tablicy o długości dopasowanej do l. wątków w grupie i zapis do tablicy pomocniczej out redukcja na tablicy pomocniczej out być może kolejny etap, jeżeli dł. tablicy out jest większa niż l. wątków w grupie
Redukcja OpenCL Pamięć globalna Pamięć lokalna... Redukcje w grupach wątków
Redukcja OpenCL 1 // Wersja rozszerzona 2 // out[i] = wartość redukcji dla i-tej grupy roboczej 3 kernel void reduce_big(int n, global int *data, global int *out) { 4 const int loc_id = get_local_id(0); 5 const int id = get_group_id(0) * get_local_size(0) * 2 + loc_id; 6 const int group_size = get_local_size(0); 7 for (int k = group_size; k > 0; k /= 2) { 8 if ( loc_id < k ) { 9 data[ id ] += data[ id + k ]; 10 } 11 barrier(clk_local_mem_fence); 12 } 13 if (loc_id == 0) { 14 out[ get_group_id(0) ] = data[ id ]; 15 } 16 }
Redukcja OpenCL 1 // Wersja rozszerzona 2 // out[i] = wartość redukcji dla i-tej grupy roboczej 3 kernel void reduce_big(int n, global int *data, global int *out) { 4 const int loc_id = get_local_id(0); 5 const int id = get_group_id(0) * get_local_size(0) * 2 + loc_id; 6 const int group_size = get_local_size(0); 7 for (int k = group_size; k > 0; k /= 2) { 8 if ( loc_id < k ) { 9 data[ id ] += data[ id + k ]; 10 } 11 barrier(clk_local_mem_fence); 12 } 13 if (loc_id == 0) { 14 out[ get_group_id(0) ] = data[ id ]; 15 } 16 } Obliczenia można by przyspieszyć jeszcze przez: użycie pamięci lokalnej rozwinięcie pętli
Redukcja pamięć lokalna 1 // Wersja z użyciem pamięci lokalnej 2 // out[i] = wartość redukcji dla i-tej grupy roboczej 3 kernel void reduce(int n, global int *data, global int *out, 4 local int *cache) { 5 const int local_id = get_local_id(0); 6 const int group_size = get_local_size(0); 7 const int global_id = get_group_id(0) * group_size * 2 + local_id; 8 9 cache[local_id] = data[global_id]; 10 cache[local_id + group_size] = data[global_id + group_size]; 11 12 barrier(clk_local_mem_fence); 13 for (int k = group_size; k > 0; k /= 2) { 14 if ( local_id < k ) { 15 cache[ local_id ] += cache[ local_id + k ]; 16 } 17 barrier(clk_local_mem_fence); 18 } 19 if (local_id == 0) { 20 out[ get_group_id(0) ] = cache[0]; 21 } 22 }
Redukcja uwagi Przedstawiona wersja wymaga odpowiedniej liczby etapów, aby wyznaczyć redukcję dużej tablicy Pierwszy etap może wymagać uruchomienia znacznej liczby grup, tj. n/(2 group size) Bardziej efektywne jest uruchomienie tylko takiej liczby grup, g, żeby nasycić wszystkie multiprocesory GPU każda grupa wyznaczy wartość redukcji dla dużego fragmentu tablicy, tj. o rozmiarze n/g wynik zapisany w tablicy pomocniczej out może zostać zredukowany sekwencyjnie
Redukcja 1 kernel 2 void reduce( global int *vec, local int *scratch, 3 const int length, global int *result) { 4 int sum = 0; 5 // Sekwencyjna redukcja elementów tablicy oddalonych o global_size 6 for (int i = get_global_id(0); i < length; i+=get_global_size(0)){ 7 sum += vec[i]; 8 } 9 // Równoległa redukcja tablicy scratch 10 int local_index = get_local_id(0); 11 scratch[local_index] = sum; 12 barrier(clk_local_mem_fence); 13 for(int offset = get_local_size(0) / 2; offset > 0; offset /= 2) { 14 if (local_index < offset) { 15 int other = scratch[local_index + offset]; 16 scratch[local_index] += other; 17 } 18 barrier(clk_local_mem_fence); 19 } 20 if (local_index == 0) { 21 result[get_group_id(0)] = scratch[0]; 22 } 23 }
Redukcja złożoność T (n, p) = O(n/p + log n) Wewnątrz grupy wątków p = n, więc złożoność wewnątrz grupy wątków to O(log n) Dla porównania złożoność wersji sekwencyjnej to O(n)
Sumy prefiksowe Sumy prefiksowe (ang. all-prefix-sums, scan) Dane wejściowe uporządkowany zbiór elementów [a 0, a 1,..., a n 1 ] binarny operator, który jest łączny element identycznościowy I Wynik: (a 0, (a 0 a 1 ),..., (a 0 a 1... a n 1 ))
Sumy prefiksowe Sumy prefiksowe (ang. all-prefix-sums, scan) Dane wejściowe uporządkowany zbiór elementów [a 0, a 1,..., a n 1 ] binarny operator, który jest łączny element identycznościowy I Wynik: (a 0, (a 0 a 1 ),..., (a 0 a 1... a n 1 )) 1 Ciąg wejściowy: [ 3 1 7 0 4 1 6 3 ] 2 Sumy prefiksowe: [ 3 4 11 11 15 16 22 25 ]
Sumy prefiksowe Algorytm wyznaczania sum prefiksowych stanowi podstawę wielu algorytmów równoległych, m.in.: sortowania pozycyjnego (ang. radix sort) sortowania szybkiego (ang. quicksort) porównywania ciągów obliczania wielomianów wyznaczania histogramu usuwania zerowych elementów z rzadkiej macierzy / wektora (ang. stream compaction)
Sumy prefiksowe Algorytm sekwencyjny 1 acc = a[0]; 2 out[0] = acc; 3 for (i = 1; i < n; i++) 4 acc = acc op a[i]; // op - operator 5 out[i] = acc; Złożoność: n 1 operacji, czyli O(n)
Sumy prefiksowe równolegle Dwa najbardziej znane algorytmy obliczania sum prefiksowych to: algorytm Hillis-Steele 1 liczba kroków log n liczba operacji O(n log n) algorytm Blellocha 2 liczba kroków 2 log n liczba operacji O(n) 1 Hillis, W. Daniel, and Guy L. Steele Jr. Data parallel algorithms. Communications of the ACM 29.12 (1986): 1170-1183. 2 Blelloch, Guy E. Scans as primitive parallel operations. Computers, IEEE Transactions on 38.11 (1989): 1526-1538.
Sumy prefiksowe Hillis-Steele Rysunek : Ilustracja działania algorytmu sum prefiksowych Hillis-Steele W każdym kroku do elementu o indeksie i dodajemy (lub inny operator) wartość elementu o indeksie i 2 d
Sumy prefiksowe Hillis-Steele Rysunek : Przykład działania alg. Hillis-Steele
Sumy prefiksowe Hillis-Steele algorytm Data: a[0... 1][0... n 1], n = 2 m in = 0; out = 1; for d from 0 to m 1 do for k from 0 to n 1 do in parallel if k 2 d 0 then a[out][k] a[in][k] + a[in][k 2 d ]; else a[out][k] a[in][k]; end swap(in, out); end end
Sumy prefiksowe Hillis-Steele algorytm Data: a[0... 1][0... n 1], n = 2 m in = 0; out = 1; for d from 0 to m 1 do for k from 0 to n 1 do in parallel if k 2 d 0 then a[out][k] a[in][k] + a[in][k 2 d ]; else a[out][k] a[in][k]; end swap(in, out); end end Jak widać, algorytm ten nie działa w miejscu wymaga dodatkowego wektora o dł. O(n) w roli bufora
Sumy prefiksowe Hillis-Steele kernel 1 kernel void scan( global float *in_data, 2 int n, 3 global float *result, 4 local float *temp) { 5 int global_id = get_global_id(0); 6 int local_id = get_local_id(0); 7 int group_size = get_local_size(0); 8 int in = 0; 9 int out = 1; 10 temp[local_id] = in_data[global_id]; 11 barrier(clk_local_mem_fence); 12 for(uint s = 1; s < group_size; s = s*2) { 13 if(local_id > (s-1)) { 14 temp[out * n + local_id] += temp[in * n + local_id - s]; 15 } else { 16 temp[out * n + local_id] = temp[in * n + local_id]; 17 } 18 barrier(clk_local_mem_fence); 19 in = 1 - in; out = 1 - out; 20 } 21 result[global_id] = temp[out * n + local_id]; 22 }
Sumy prefiksowe Hillis-Steele złożoność Liczba operacji: (2 m 2 0 ) + (2 m 2 1 ) +... + (2 m 2 m 1 ) = = m 2 m (2 0 + 2 1 +... + 2 m 1 ) = m 2 m 2 m + 1 = 2 m (m 1) + 1 = n(log(n) 1) + 1 czyli złożoność O(n log n), więcej niż optymalna złożoność O(n) dla danych rozmiaru n = 10 6 wykona około 20x więcej obliczeń
Sumy prefiksowe alg. Blellocha Sumy prefiksowe (scan) ciągu [a 0, a 1,..., a n 1 ] można wyznaczyć na podstawie wyłącznych sum prefiksowych (prescan) ciągu [I, a 0, a 1,..., a n 2 ], gdzie I to element identycznościowy (zero) Wyznaczanie sum prefiksowych odbywa się w 2 fazach: up-sweep równoznaczny z prezentowanym poprzednio algorytmem redukcji down-sweep wyznaczanie sum prefiksowych ciągu [I, a 0, a 1,..., a n 2 ]
Sumy prefiksowe przykład 1 Ciąg wejściowy: [ 3 1 7 0 4 1 6 3 ] 2 Sumy prefiksowe: [ 3 4 11 11 15 16 22 25 ] 3 Sumy prefiksowe wyłączne:[ 0 3 4 11 11 15 16 22 ]
Sumy prefiksowe up-sweep 25 11 14 4 7 5 9 3 1 7 0 4 1 6 3 sum[v] = sum[l[v]] + sum[r[v]]
Sumy prefiksowe up-sweep + down-sweep 25 11 14 4 7 5 9 3 1 7 0 4 1 6 3 sum[v] = sum[l[v]] + sum[r[v]] 25 0 + 11 0 11 14 11 40 74 511 916 30 13 73 011 411 15 616 322 prescan[l[v]] = prescan[v] prescan[r[v]] = sum[l[v]] + prescan[v]
Algorytm down-sweep 25 0 11 0 14 11 Drzewo 40 74 511 916 30 13 73 011 411 15 616 322 Wektor 30 13 73 011 411 15 616 322 0 1 2 3 4 5 6 7 Zależy nam na ostatnim poziomie drzewa, dlatego wystarczy wektor o długości n.
Algorytm Blellocha przykład
Sumy prefiksowe alg. Blellocha down-sweep Data: a[0... n 1] a[n 1] 0; for d from (log n) 1 downto 0 do for i from 0 to n 1 by 2 d+1 do in parallel t a[i + 2 d 1] a[i + 2 d 1] a[i + 2 d+1 1] a[i + 2 d+1 1] t + a[i + 2 d 1] a[i + 2 d+1 1] end end
Sumy prefiksowe alg. Blellocha down-sweep Data: a[0... n 1] a[n 1] 0; for d from (log n) 1 downto 0 do for i from 0 to n 1 by 2 d+1 do in parallel t a[i + 2 d 1] a[i + 2 d 1] a[i + 2 d+1 1] a[i + 2 d+1 1] t + a[i + 2 d 1] a[i + 2 d+1 1] end end Złożoność: O(log n) przy n procesorach
Algorytm Blellocha kernel 1 kernel 2 void prescan( global float *data, int n, 3 local int* temp, global float *result) { 4 int local_id = get_local_id(0); 5 int offset = 1; 6 temp[2*local_id] = data[2*local_id]; // do pam. lokalnej 7 temp[2*local_id+1] = data[2*local_id+1]; 8 for (int d = n / 2; d > 0; d /= 2) { // sumuj w górę drzewa (upsweep) 9 barrier(clk_local_mem_fence); 10 if (local_id < d) { 11 int ai = offset*(2*local_id+1)-1; 12 int bi = offset*(2*local_id+2)-1; 13 temp[bi] += temp[ai]; 14 } 15 offset *= 2; 16 } 17 if (local_id == 0) { temp[n - 1] = 0; } // korzeń drzewa -> 0
Algorytm Blellocha kernel 1 // c.d. 2 for (int d = 1; d < n; d *= 2) { // sumuj w dół drzewa (down-sweep) 3 offset /= 2; 4 barrier(clk_local_mem_fence); 5 if (local_id < d) { 6 int ai = offset*(2*local_id+1)-1; 7 int bi = offset*(2*local_id+2)-1; 8 float t = temp[ai]; 9 temp[ai] = temp[bi]; 10 temp[bi] += t; 11 } 12 } 13 barrier(clk_local_mem_fence); 14 result[2*local_id] = temp[2*local_id]; // zapisz wyniki 15 result[2*local_id+1] = temp[2*local_id+1]; 16 }
Algorytm Blellocha kernel konflikty W przypadku, gdy kilka wątków zapisuje/odczytuje dane w pamięci lokalnej umieszczone pod różnymi adresami, ale w tym samym banku pamięci następuje konflikt Nie ma problemu, gdy wszystkie wątki w ramach osnowy (warp/wavefront) zapisują/odczytują dane umieszczone w różnych bankach zapisują/odczytują daną pod tym samym adresem W przedstawionym algorytmie również występują konflikty w dostępie do pamięci, które mogą mieć wpływ na czas działania w zależności od architektury GPU
Algorytm Blellocha kernel konflikty Przykładowo dla fragmentu: 1 temp[2*local_id] = data[2*local_id]; // do pam. lokalnej 2 temp[2*local_id+1] = data[2*local_id+1]; Wątek 0 odczytuje z banków 0 i 1 Wątek 1 odczytuje z banków 2 i 3... Wątek 8 odczytuje z banków 16 i 17, ale gdy banków jest tylko 16, to daje 0 i 1 ponownie konflikt dwudrożny
Algorytm Blellocha kernel konflikty Rysunek : Ilustracja konfliktów dostępu do pam. lokalnej autor Mark Harris
Algorytm Blellocha kernel konflikty Banks accessed Thread id: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Conflicts: -------------------------------------------------------------------------------------- Step 0: 0 2 4 6 8 10 12 14 0 2 4 6 8 10 12 14 --> 2-way Step 1: 1 3 5 7 9 11 13 15 1 3 5 7 9 11 13 15 --> 2-way Step 2: 0 2 4 6 8 10 12 14 0 2 4 6 8 10 12 14 --> 2-way Step 3: 1 3 5 7 9 11 13 15 1 3 5 7 9 11 13 15 --> 2-way Step 4: 1 5 9 13 1 5 9 13 1 5 9 13 1 5 9 13 --> 4-way Step 5: 3 7 11 15 3 7 11 15 3 7 11 15 3 7 11 15 --> 4-way Step 6: 3 11 3 11 3 11 3 11 3 11 3 11 3 11 3 11 --> 8-way Step 7: 7 15 7 15 7 15 7 15 7 15 7 15 7 15 7 15 --> 8-way Step 8: 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 --> 16-way Step 9: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 --> 16-way Step 10: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 --> 16-way Step 11: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 --> 16-way Step 12: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 --> 16-way Step 13: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 --> 16-way Step 14: 15 15 15 15 15 15 15 15 - - - - - - - - --> 8-way Step 15: 15 15 15 15 15 15 15 15 - - - - - - - - --> 8-way Step 16: 15 15 15 15 - - - - - - - - - - - - --> 4-way Step 17: 15 15 15 15 - - - - - - - - - - - - --> 4-way Step 18: 15 15 - - - - - - - - - - - - - - --> 2-way Step 19: 15 15 - - - - - - - - - - - - - - --> 2-way Step 20: 15 - - - - - - - - - - - - - - - --> - Step 21: 15 - - - - - - - - - - - - - - - --> - Analiza konfliktów dla pierwszej części alg. Blellocha (up-sweep) rozm. tablicy 1024, rozm. grupy wątków 512
Algorytm Blellocha kernel konflikty Banks accessed Thread id: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Conflicts: -------------------------------------------------------------------------------------- Step 0: 15 - - - - - - - - - - - - - - - --> - Step 1: 15 - - - - - - - - - - - - - - - --> - Step 2: 15 15 - - - - - - - - - - - - - - --> 2-way Step 3: 15 15 - - - - - - - - - - - - - - --> 2-way Step 4: 15 15 15 15 - - - - - - - - - - - - --> 4-way Step 5: 15 15 15 15 - - - - - - - - - - - - --> 4-way Step 6: 15 15 15 15 15 15 15 15 - - - - - - - - --> 8-way Step 7: 15 15 15 15 15 15 15 15 - - - - - - - - --> 8-way Step 8: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 --> 16-way Step 9: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 --> 16-way Step 10: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 --> 16-way Step 11: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 --> 16-way Step 12: 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 --> 16-way Step 13: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 --> 16-way Step 14: 3 11 3 11 3 11 3 11 3 11 3 11 3 11 3 11 --> 8-way Step 15: 7 15 7 15 7 15 7 15 7 15 7 15 7 15 7 15 --> 8-way Step 16: 1 5 9 13 1 5 9 13 1 5 9 13 1 5 9 13 --> 4-way Step 17: 3 7 11 15 3 7 11 15 3 7 11 15 3 7 11 15 --> 4-way Step 18: 0 2 4 6 8 10 12 14 0 2 4 6 8 10 12 14 --> 2-way Step 19: 1 3 5 7 9 11 13 15 1 3 5 7 9 11 13 15 --> 2-way Analiza konfliktów dla drugiej części alg. Blellocha (down-sweep) rozm. tablicy 1024, rozm. grupy wątków 512
Rysunek : Rozwiązanie części konfliktów dostępu do pam. lokalnej dzięki przesunięciu kolejnych segmentów elementów w tablicy pomocniczej autor Mark Harris Algorytm Blellocha kernel konflikty
Algorytm Blellocha kernel konflikty Banks accessed Thread id: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Conflicts: -------------------------------------------------------------------------------------- Step 0: 0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15 --> - Step 1: 1 3 5 7 9 11 13 15 2 4 6 8 10 12 14 0 --> - Step 2: 0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15 --> - Step 3: 1 3 5 7 9 11 13 15 2 4 6 8 10 12 14 0 --> - Step 4: 1 5 9 13 2 6 10 14 3 7 11 15 4 8 12 0 --> - Step 5: 3 7 11 15 4 8 12 0 5 9 13 1 6 10 14 2 --> - Step 6: 3 11 4 12 5 13 6 14 7 15 8 0 9 1 10 2 --> - Step 7: 7 15 8 0 9 1 10 2 11 3 12 4 13 5 14 6 --> - Step 8: 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 --> - Step 9: 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 --> - Step 10: 15 1 3 5 7 9 11 13 15 1 3 5 7 9 11 13 --> 2-way Step 11: 0 2 4 6 8 10 12 14 0 2 4 6 8 10 12 14 --> 2-way Step 12: 0 4 8 12 0 4 8 12 0 4 8 12 0 4 8 12 --> 4-way Step 13: 2 6 10 14 2 6 10 14 2 6 10 14 2 6 10 14 --> 4-way Step 14: 2 10 2 10 2 10 2 10 - - - - - - - - --> 4-way Step 15: 6 14 6 14 6 14 6 14 - - - - - - - - --> 4-way Step 16: 6 6 6 6 - - - - - - - - - - - - --> 4-way Step 17: 14 14 14 14 - - - - - - - - - - - - --> 4-way Step 18: 14 14 - - - - - - - - - - - - - - --> 2-way Step 19: 14 14 - - - - - - - - - - - - - - --> 2-way Step 20: 14 - - - - - - - - - - - - - - - --> - Step 21: 14 - - - - - - - - - - - - - - - --> - Zmiana ta nie usuwa wszystkich konfliktów, lecz je ogranicza Niestety kosztem dodatkowych obliczeń związanych z wyznaczaniem indeksu elementu index = index + index / 16
Algorytm Blellocha kernel konflikty Banks accessed (up-sweep) Thread id: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Conflicts: -------------------------------------------------------------------------------------- Step 0: 0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15 --> - Step 1: 1 3 5 7 9 11 13 15 2 4 6 8 10 12 14 0 --> - Step 2: 0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15 --> - Step 3: 1 3 5 7 9 11 13 15 2 4 6 8 10 12 14 0 --> - Step 4: 1 5 9 13 2 6 10 14 3 7 11 15 4 8 12 0 --> - Step 5: 3 7 11 15 4 8 12 0 5 9 13 1 6 10 14 2 --> - Step 6: 3 11 4 12 5 13 6 14 7 15 8 0 9 1 10 2 --> - Step 7: 7 15 8 0 9 1 10 2 11 3 12 4 13 5 14 6 --> - Step 8: 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 --> - Step 9: 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 --> - Step 10: 15 1 3 5 7 9 11 13 0 2 4 6 8 10 12 14 --> - Step 11: 0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15 --> - Step 12: 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 --> - Step 13: 2 6 10 14 3 7 11 15 4 8 12 0 5 9 13 1 --> - Step 14: 2 10 3 11 4 12 5 13 - - - - - - - - --> - Step 15: 6 14 7 15 8 0 9 1 - - - - - - - - --> - Step 16: 6 7 8 9 - - - - - - - - - - - - --> - Step 17: 14 15 0 1 - - - - - - - - - - - - --> - Step 18: 14 0 - - - - - - - - - - - - - - --> - Step 19: 15 1 - - - - - - - - - - - - - - --> - Step 20: 15 - - - - - - - - - - - - - - - --> - Step 21: 1 - - - - - - - - - - - - - - - --> - Konflikty można wyeliminować całkowicie, jeżeli dodamy dodatkowe przesunięcie co 16 porcji po 16 elementów index = index + index / 16 + index / 256 Niestety dodatkowy koszt dla każdego indeksu nieopłacalne
Algorytm Blellocha kernel konflikty Banks accessed (down-sweep) Thread id: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Conflicts: -------------------------------------------------------------------------------------- Step 0: 15 - - - - - - - - - - - - - - - --> - Step 1: 1 - - - - - - - - - - - - - - - --> - Step 2: 14 0 - - - - - - - - - - - - - - --> - Step 3: 15 1 - - - - - - - - - - - - - - --> - Step 4: 6 7 8 9 - - - - - - - - - - - - --> - Step 5: 14 15 0 1 - - - - - - - - - - - - --> - Step 6: 2 10 3 11 4 12 5 13 - - - - - - - - --> - Step 7: 6 14 7 15 8 0 9 1 - - - - - - - - --> - Step 8: 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 --> - Step 9: 2 6 10 14 3 7 11 15 4 8 12 0 5 9 13 1 --> - Step 10: 15 1 3 5 7 9 11 13 0 2 4 6 8 10 12 14 --> - Step 11: 0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15 --> - Step 12: 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 --> - Step 13: 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 --> - Step 14: 3 11 4 12 5 13 6 14 7 15 8 0 9 1 10 2 --> - Step 15: 7 15 8 0 9 1 10 2 11 3 12 4 13 5 14 6 --> - Step 16: 1 5 9 13 2 6 10 14 3 7 11 15 4 8 12 0 --> - Step 17: 3 7 11 15 4 8 12 0 5 9 13 1 6 10 14 2 --> - Step 18: 0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15 --> - Step 19: 1 3 5 7 9 11 13 15 2 4 6 8 10 12 14 0 --> - W przyszłości rozwiązania sprzętowe zapewne zniwelują opóźnienia związane z dostępem do pamięci lokalnej wynikające z konfliktów
Algorytm Blellocha dowolny rozmiar tablicy Dla prezentowanego algorytmu przyjęto założenie wykonania przez jeden multiprocesor a więc rozmiar tablicy zależny od maks. liczby wątków w grupie roboczej, np. 2 1024 Dla tablic o większych rozmiarach sumy prefiksowe należy wyznaczyć w dwóch etapach n-elementową tablicę wejściową dzielimy na n/b bloków, gdzie b/2 to liczba wątków w grupie ostatni element sum prefiksowych zapisywany jest do tablicy pomocniczej sums wyznaczamy sumy prefiksowe dla tablicy sums i na tej podstawie wyznaczamy właściwe sumy prefiksowe dla tablicy wejściowej dodając do każdego elementu bloku i wartość sums[i]
Algorytm Blellocha dowolny rozmiar tablicy
Algorytm Blellocha wydajność Rysunek : Porównanie wydajności obliczania sum prefiksowych (Harris, Mark, Shubhabrata Sengupta, and John D. Owens. Gpu gems 3. Parallel Prefix Sum (Scan) with CUDA (2007): 851-876.)
Algorytm Blellocha wydajność # elements CPU Scan (ms) GPU Scan (ms) Speedup 1024 0.002231 0.079492 0.03 32768 0.072663 0.106159 0.68 65536 0.146326 0.137006 1.07 131072 0.726429 0.200257 3.63 262144 1.454742 0.326900 4.45 524288 2.911067 0.624104 4.66 1048576 5.900097 1.118091 5.28 2097152 11.848376 2.099666 5.64 4194304 23.835931 4.062923 5.87 8388688 47.390906 7.987311 5.93 16777216 94.794598 15.854781 5.98
Zastosowania sum prefiksowych zliczanie Dane wej.: n-elementowa tablica wartości logicznych ( true, false ) Wynik: n-elementowa tablica, w której i-ty element ma wartość równą liczbie wartości true na lewo od niego Wejście T F T T F T Wynik 0 1 1 2 3 3
Zastosowania sum prefiksowych podział Algorytm podziału (ang. split) wektora Cel: podział wektora wejściowego na dwie części elementy oznaczone jako false (0) po lewej stronie elementy oznaczone jako true (1) po prawej stronie Przykład: A B C D E F G Powiązane dane 1 0 1 0 0 1 0 Wektor wejściowy A 0 1 0 1 1 0 1 Negacja wektora wejściowego Ā 0 0 1 1 2 3 3 S sumy prefiksowe (wyłączne) f = 4 Liczba wart. false 0 1 2 3 4 5 6 I identyfikatory wątków 4 5 5 6 6 6 7 T [i] = I [i] S[i] + f 4 0 5 1 2 6 3 Indeksy elementów w tablicy po podziale B D E G A C F Dane po permutacji
Zastosowania sum prefiksowych sortowanie pozycyjne Przykład zastosowania algorytmu podziału do implementacji sortowania pozycyjnego (ang. radix-sort) 3 3 Blelloch, Guy E. Prefix sums and their applications. (1990).
Zastosowania sum prefiksowych sortowanie pozycyjne Alg. podziału ma złożoność T (n, p) = O(n/p + log p) Jeżeli liczby mają O(log n) bitów to potrzeba wykonania log n razy alg. podziału Zatem finalna złożoność sortowania pozycyjnego to: T (n, p) = O( n p log n + log n log p) Złożoność alg. wykonywanego przez grupę p wątków, przy zał. n = p daje złożoność T (n, n) = O(log n + 2 log n) = O(log n)
Zastosowania sum prefiksowych sortowanie szybkie Sumy prefiksowe mogą zostać użyte jako element składowy równoległej implementacji sortowania szybkiego (ang. quicksort) Średnia złożoność obliczeniowa algorytmu to: T (n, p) = O(log n T S (n, p)), gdzie T S (n, p) jest złożonością algorytmu sum prefiksowych Łącznie, średnia złożoność T (n, p) = O( n p log n + log2 n)