Programowanie równoległe Wprowadzenie do programowania GPU. Rafał Skinderowicz

Podobne dokumenty
Mnożenie macierzy. Systemy z pamięcią współdzieloną Systemy z pamięcią rozproszoną Efektywność

Bazy danych. Andrzej Łachwa, UJ, /15

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

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

Wprowadzenie do programowania w środowisku CUDA. Środowisko CUDA

Architektura komputerów

architektura komputerów w. 6 Pamięć I

Accelerated Graphics Port AGP Advanced Graphics Port AGP jest magistralą równoległą.

CUDA. cudniejsze przyk ady

Sieć komputerowa grupa komputerów lub innych urządzeo połączonych ze sobą w celu wymiany danych lub współdzielenia różnych zasobów, na przykład:

Programowanie kart graficznych

Podstawowe działania w rachunku macierzowym

Architektura Systemów Komputerowych. Sterowanie programem skoki Przerwania

DEMERO Automation Systems

Programowanie Współbieżne

Zagadnienia transportowe

Budowa systemów komputerowych

Opis programu do wizualizacji algorytmów z zakresu arytmetyki komputerowej

Hybrydowy system obliczeniowy z akceleratorami GPU

G PROGRAMMING. Part #4

PRZETWORNIK NAPIĘCIE - CZĘSTOTLIWOŚĆ W UKŁADZIE ILORAZOWYM

KONKURSY MATEMATYCZNE. Treść zadań

INSTRUKCJA OBSŁUGI URZĄDZENIA: HC8201

Programowanie procesorów graficznych GPGPU

Oprogramowanie klawiatury matrycowej i alfanumerycznego wyświetlacza LCD

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

2.Prawo zachowania masy

Komunikacja w sieci Industrial Ethernet z wykorzystaniem Protokołu S7 oraz funkcji PUT/GET

Rozdział 6. Pakowanie plecaka. 6.1 Postawienie problemu

Procesory kart graficznych i CUDA wer

Sieci komputerowe cel

Bioinformatyka Laboratorium, 30h. Michał Bereta

Systemy wbudowane Mikrokontrolery

Elementy cyfrowe i układy logiczne

DE-WZP JJ.3 Warszawa,

STEROWNIK BIOLOGICZNYCH OCZYSZCZALNI ŚCIEKÓW

Transformator Elektroniczny do LED 0W-40W Współpracuje z inteligentnymi ściemniaczami oświetlenia. Instrukcja. Model: TE40W-DIMM-LED-IP64

Harmonogramowanie projektów Zarządzanie czasem

API transakcyjne BitMarket.pl

HiTiN Sp. z o. o. Przekaźnik kontroli temperatury RTT 4/2 DTR Katowice, ul. Szopienicka 62 C tel/fax.: + 48 (32)

OSTRZEŻENIA DANE TECHNICZNE. Wbudowana bateria słoneczna oraz alkaliczna bateria manganowa (1,5 V LR44)

Grupa bezpieczeństwa kotła KSG / KSG mini

Jak usprawnić procesy controllingowe w Firmie? Jak nadać im szerszy kontekst? Nowe zastosowania naszych rozwiązań na przykładach.

Tematyka i rozwiązania metodyczne kolejnych zajęć lekcyjnych wraz z ćwiczeniami.

mgr inż. Grzegorz Kraszewski SYSTEMY MULTIMEDIALNE wykład 6, strona 1. Format JPEG

Macierze dyskowe RAID

tel/fax lub NIP Regon

Instrukcja obsługi zamka. bibi-z50. (zamek autonomiczny z czytnikiem identyfikatora Mifare)

NUMER IDENTYFIKATORA:

System Informatyczny CELAB. Przygotowanie programu do pracy - Ewidencja Czasu Pracy

2. Charakterystyka obliczeń współbieżnych i rozproszonych.

14.Rozwiązywanie zadań tekstowych wykorzystujących równania i nierówności kwadratowe.

NACZYNIE WZBIORCZE INSTRUKCJA OBSŁUGI INSTRUKCJA INSTALOWANIA

Strategia rozwoju kariery zawodowej - Twój scenariusz (program nagrania).

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

Dr inż. Andrzej Tatarek. Siłownie cieplne

1 Granice funkcji. Definicja 1 (Granica w sensie Cauchy ego). Mówimy, że liczba g jest granicą funkcji f(x) w punkcie x = a, co zapisujemy.

SERI A 93 S E RI A 93 O FLUSH GRID WITHOUT EDGE TAB

Programowanie procesorów graficznych GPGPU

Warunki Oferty PrOmOcyjnej usługi z ulgą

OŚWIETLENIE PRZESZKLONEJ KLATKI SCHODOWEJ

Podejmowanie decyzji. Piotr Wachowiak

Type ETO2 Controller for ice and snow melting

Zarządzanie projektami. wykład 1 dr inż. Agata Klaus-Rosińska

Uchwała nr 1 Nadzwyczajnego Walnego Zgromadzenia J.W. Construction Holding S.A. z siedzibą w Ząbkach z dnia 1 kwietnia 2008 roku

Wstęp do programowania

Regulamin Obrad Walnego Zebrania Członków Stowarzyszenia Lokalna Grupa Działania Ziemia Bielska

RZECZPOSPOLITA POLSKA. Prezydent Miasta na Prawach Powiatu Zarząd Powiatu. wszystkie

FUNKCJE STEROWNIKA PK-35 PID

XIII KONKURS MATEMATYCZNY

PRZEPISY KLASYFIKACJI I BUDOWY STATKÓW MORSKICH

Wprowadzenie do programowania w środowisku CUDA. Środowisko CUDA

Opracowała: Karolina Król-Komarnicka, kierownik działu kadr i płac w państwowej instytucji

Systemy wbudowane. Paweł Pełczyński

ŠkodaOctavia Combi 4 4 & Superb 4 4

INSTRUKCJA DO PROGRAMU LICZARKA 2000 v 2.56

Szczegółowe zasady obliczania wysokości. i pobierania opłat giełdowych. (tekst jednolity)

Wykład 4 Wybrane zagadnienia programowania w C++

Uniwersytet Warszawski Teoria gier dr Olga Kiuila LEKCJA 5

enova Workflow Obieg faktury kosztowej

C5 - D4EB0FP0 - Informacje ogólne : Poduszki powietrzne INFORMACJE OGÓLNE : PODUSZKI POWIETRZNE

I. LOGICZNE STRUKTURY DRZEWIASTE

Matematyka:Matematyka I - ćwiczenia/granice funkcji

SCHEMAT ZBIORNIKA HYDROFOROWEGO ZE STALI NIERDZEWNEJ

GEO-SYSTEM Sp. z o.o. GEO-RCiWN Rejestr Cen i Wartości Nieruchomości Podręcznik dla uŝytkowników modułu wyszukiwania danych Warszawa 2007

Mikrokontrolery AVR. Konfigurowanie mikrokontrolera ATMEGA16

Chmura obliczeniowa. do przechowywania plików online. Anna Walkowiak CEN Koszalin

Programowanie CUDA informacje praktycznie i. Wersja

TEST WIADOMOŚCI: Równania i układy równań

Instrukcja. sporządzania rocznych sprawozdań Rb-WSa i Rb-WSb o wydatkach strukturalnych

1. Od kiedy i gdzie należy złożyć wniosek?

Dobór nastaw PID regulatorów LB-760A i LB-762

WYROK W IMIENIU RZECZYPOSPOLITEJ POLSKIEJ. SSN Bogusław Cudowski (przewodniczący) SSN Jolanta Frańczak (sprawozdawca) SSN Krzysztof Staryk

Programator pamięci EEPROM

Akademickie Centrum Informatyki PS. Wydział Informatyki PS

PROE wykład 7 kontenery tablicowe, listy. dr inż. Jacek Naruniec

ROZWIĄZANIA ZADAŃ Zestaw P3 Odpowiedzi do zadań zamkniętych

Niezależnie od rodzaju materiału dźwiękowego ocenie podlegały następujące elementy pracy egzaminacyjnej:

Transkrypt:

Programowanie równoległe Wprowadzenie do programowania GPU Rafał Skinderowicz

CPU Fetch/ Decode ALU (Execute) Data cache (a big one) Execution Context Out-of-order control logic Fancy branch predictor Memory pre-fetcher Rysunek : Źródło: Introduction to GPU Architecture Ofer Rosenberg, AMD Rozbudowane układy dekodowania i predykcji instrukcji Duża pamięć cache

CPU Celem CPU jest jak najszybsze wykonanie danego strumienia instrukcji Pamięć cache oraz układy predykcji rozgałęzień mają na celu redukcję czasu oczekiwania na dane potrzebne do obliczeń Rozbudowana potokowość (ang. pipelining) wykonywania instrukcji Zmiana kolejności wykonania rozkazów (ang. out-of-order execution)

Potokowość Instr. No. 1 2 3 4 5 Clock Cycle Pipeline Stage IF ID EX MEM WB IF ID EX MEM WB IF ID EX MEM WB IF ID EX MEM IF ID EX 1 2 3 4 5 6 7 Uproszczony schemat potokowego wykonania instrukcji. Pobranie instrukcji z pamięci ang. instruction fetch (IF); Zdekodowanie instrukcji ang. instruction decode (ID); Wykonanie instrukcji ang. execute (EX); Dostęp do pamięci ang. memory access (MEM); Zapisanie wyników działania instrukcji ang. store; write back (WB)

W kierunku GPU Fetch/ Decode ALU (Execute) Execution Context Usuwamy układy odpowiedzialne za szybkie wykonanie potoku instrukcji redukując istotnie rozmiar rdzenia. Rysunek : Źródło: Introduction to GPU Architecture Ofer Rosenberg, AMD

W kierunku GPU Fetch/ Decode ALU (Execute) Execution Context Usuwamy układy odpowiedzialne za szybkie wykonanie potoku instrukcji redukując istotnie rozmiar rdzenia. Rysunek : Źródło: Introduction to GPU Architecture Ofer Rosenberg, AMD Większość tranzystorów we współczesnych CPU to pamięć cache

W kierunku GPU Rysunek : Źródło: Introduction to GPU Architecture Ofer Rosenberg, AMD Uproszczona budowa obniża koszty pozwalając na użycie większej ich liczby 16 rdzeni to 16 jednoczesnych strumieni instrukcji

W kierunku GPU Fetch/ Decode ALU 1 ALU 2 ALU 3 ALU 4 ALU 5 ALU 6 ALU 7 ALU 8 Ctx Ctx Ctx Ctx Ctx Ctx Ctx Ctx Shared Ctx Data Rysunek : Źródło: Introduction to GPU Architecture Ofer Rosenberg, AMD Jeżeli rdzenie będą wykonywać te same instrukcje, ale na różnych danych to mogą dzielić układy pobierania i dekodowania rozkazów Przetwarzanie typu SIMD single instruction, multiple data Problem: oczekiwanie na dane wstrzymuje wszystkie ALU duże opóźnienie wykonania instrukcji

Ukrywanie opóźnień Fetch/ Decode ALU 1 ALU 2 ALU 3 ALU 4 ALU 5 ALU 6 ALU 7 ALU 8 Fetch/ Decode ALU 1 ALU 2 ALU 3 ALU 4 ALU 5 ALU 6 ALU 7 ALU 8 Wspólna pamięć 128 KB Rysunek : Źródło: Introduction to GPU Architecture Ofer Rosenberg, AMD Rysunek : Źródło: Introduction to GPU Architecture Ofer Rosenberg, AMD W pamięci można przechowywać kontekst obliczeniowy dla wielu niezależnych strumieni instrukcji i przełączać się między nimi w miarę potrzeby zwiększając przepustowość

Przykład 16 rdzeni 8 mul-add ALU na rdzeń (128 łącznie) 16 niezależnych strumieni instrukcji 64 współbieżne strumienie instrukcji (metoda przeplotu) 512 współbieżnych kontekstów Moc 256 GFLOPs (przy 1GHz) Rysunek : Źródło: Introduction to GPU Architecture Ofer Rosenberg, AMD

Podsumowanie Główne idee architektury GPU to: Wiele prostych rdzeni obliczeniowych umożliwiających równoległe obliczenia Rdzenie z wieloma ALU umożliwiającymi obliczenia typu SIMD Przeplatane wykonanie wielu grup instrukcji na jednym rdzeniu, żeby ukryć opóźnienia

Podsumowanie CPU Thread 1 Thread 2 Thread 3 Thread 4 Lower Latencies GPU Thread 1 Thread 2 Thread 3 Thread 4 Higher Throughput Time Time

NVidia Fermi SM NVIDIA GeForce GTX 580 (architektura Fermi) Fetch/ Decode Fetch/ Decode Execution contexts (128 KB) ALU - 16 na procesor strumieniowy (1 instr. MUL-ADD na cykl) * Rdzeń zawiera 32 jednostki ALU * Dwa strumienie instrukcji są wykonywane na cykl * Maksymalnie 48 współbieżnych strumieni (osnów) * Maksymalnie 1536 indywidualnych kontekstów obliczeniowych = 1536 wątków CUDA Shared memory (16+48 KB) Source: Fermi Compute Architecture Whitepaper CUDA Programming Guide 3.1, Appendix G

NVidia Fermi GTX 580 16 rdzeni SM pozwala na współbieżne wykonanie 24576 wątków CUDA Rysunek : Źródło: Introduction to GPU Architecture Ofer Rosenberg, AMD

Model obliczeń CUDA Program na hoście zleca wykonanie obliczeń na GPU w postaci kerneli Kod kernela wykonywany jest przez wątki tworzące kratę (ang. grid) Wątki kraty wykonywane są w blokach każdy blok na jednym multiprocesorze (SM) Wątki bloku wykonywane są w grupach, tzw. osnowach (ang. warp) po 32 Host Kernel 1 Kernel 2 Device Grid 2 Block (1, 1) Thread (0,0,0) Thread (0,1,0) Grid 1 Block (0, 0) Block (0, 1) (0,0,1) (1,0,1) (2,0,1) (3,0,1) Thread (1,0,0) Thread (1,1,0) Block (1, 0) Block (1, 1) Thread Thread (2,0,0) (3,0,0) Thread Thread (2,1,0) (3,1,0) Block (2, 0) Block (2, 1)

Przykład dodawanie wektorów 1 // Kernel odpowiedzialny za obliczenia 2 void vecadd(float *A, float *B, float *C, int N) { 3 for(int i = 0; i < N; i++) 4 C[i] = A[i] + B[i]; 5 } 6 int main() { 7 int N = 4096; 8 // Alokacja pamięci 9 float *A = (float *)malloc(sizeof(float)*n); 10 float *B = (float *)malloc(sizeof(float)*n); 11 float *C = (float *)malloc(sizeof(float)*n); 12 // Wprowadzenie danych wej. 13 init(a); init(b); 14 // Wywołanie kernela 15 vecadd(a, B, C, N); 16 // Zwolnienie pamięci 17 free(a); free(b); free(c); 18 }

Przykład dodawanie wektorów na GPU 1 // Kernel CUDA obliczający sumę wektorów 2 global 3 void gpuvecadd(float *A, float *B, float *C) { 4 int tid = blockidx.x * blockdim.x + threadidx.x; 5 C[tid] = A[tid] + B[tid]; 6 } blockidx.x threadidx.x GRID BLOCK (0,0) (0,0) (1,0) (2,0)... (31,0) blockdim.x = 32 BLOCK (1,0) (0,0) (1,0) (2,0)... (31,0)... tid = blockidx.x * blockdim.x + threadidx.x Rysunek : Obliczanie globalnego identyfikatora wątku w kernelu CUDA

Przykład dodawanie wektorów na GPU 1 int main() { 2 int N = 4096; 3 float *A = (float *)malloc(sizeof(float)*n); 4 float *B = (float *)malloc(sizeof(float)*n); 5 float *C = (float *)malloc(sizeof(float)*n); 6 init(a); // Inicjuj dane wejściowe 7 init(b);

Przykład dodawanie wektorów na GPU 1 // c.d. 2 // Alokacja buforów w pamięci GPU 3 float *d_a, *d_b, *d_c; 4 cudamalloc(&d_a, sizeof(float)*n); 5 cudamalloc(&d_b, sizeof(float)*n); 6 cudamalloc(&d_c, sizeof(float)*n); 7 8 // Kopiowanie danych do pamięci GPU 9 cudamemcpy(d_a, A, sizeof(float)*n, 10 cudamemcpyhosttodevice); 11 cudamemcpy(d_b, B, sizeof(float)*n, 12 cudamemcpyhosttodevice); Kierunek kopiowania: cudamemcpyhosttodevice z pam. hosta (RAM) do pam. GPU cudamemcpydevicetohost z pam. urządzenia do głównej

Przykład dodawanie wektorów na GPU 1 // c.d. 2 // Ustalenie podziału obliczeń na wątki 3 dim3 dimblock(32,1); // Rozmiar bloku 4 dim3 dimgrid(n/32,1); // Rozmiar kraty 5 // Uruchomienie kernela 6 gpuvecadd <<< dimblock,dimgrid >>> (d_a, d_b, d_c); 7 // Kopiowanie wyników do pamięci głównej 8 cudamemcpy(c, d_c, sizeof(float)*n, cudamemcpydevicetohost); 9 // Zwolnienie zasobów 10 cudafree(d_a); 11 cudafree(d_b); cudafree(d_c); 12 free(a); free(b); free(c); 13 }

Dodawanie wektorów porównanie kerneli 1 // Kernel CPU 2 void vecadd(float *A, float *B, float *C, int N) { 3 for(int i = 0; i < N; i++) 4 C[i] = A[i] + B[i]; 5 } 6 7 // Kernel CUDA 8 global 9 void gpuvecadd(float *A, float *B, float *C) { 10 int i = blockidx.x * blockdim.x + threadidx.x; 11 C[i] = A[i] + B[i]; 12 } 13 14 // Kernel OpenCL 15 kernel 16 void gpuvecadd( global float *A, global float *B, global float *C) 17 { 18 int i = get_global_id(0); 19 C[i] = A[i] + B[i]; 20 }

Dodawanie wektorów porównanie kerneli Uwaga: w przypadku gdy rozmiar wektora jest mniejszy niż liczba wątków konieczne jest sprawdzanie zakresu 1 // Kernel OpenCL 2 kernel 3 void gpuvecadd( global float *A, global float *B, global float *C, 4 int N) 5 { 6 int i = get_global_id(0); 7 if (i < N) { 8 C[i] = A[i] + B[i]; 9 } 10 }

Przykład dodawanie macierzy Dodawanie macierzy A oraz B o wymiarach N M a 11 a 12... a 1m a 21 a 22... a 2m A = a 31 a 32... a 3m...... a n1 a n2... a nm b 11 b 12... b 1m b 21 b 22... b 2m B = b 31 b 32... b 3m...... b n1 b n2... b nm C = A + B = [a ij + b ij ] dla wszystkich i, j.

Dodawanie macierzy porównanie kerneli 1 // Kernel CPU, N - liczba wierszy, M - liczba kolumn 2 void vecadd(float *A, float *B, float *C, int N, int M) { 3 for(int i = 0; i < N; i++) 4 for(int j = 0; j < M; j++) 5 C[i * M + j] = A[i * M + j] + B[i * M + j]; 6 } 7 // Kernel CUDA 8 global 9 void gpuvecadd(float *A, float *B, float *C) { 10 int i = blockidx.y * blockdim.y + threadidx.y; 11 int j = blockidx.x * blockdim.x + threadidx.x; 12 C[i * M + j] = A[i * M + j] + B[i * M + j]; 13 } 14 // Kernel OpenCL 15 kernel 16 void gpuvecadd( global float *A, global float *B, global float *C, 17 int M, int N) 18 { 19 int j = get_global_id(0); 20 int i = get_global_id(1); 21 C[i * M + j] = A[i * M + j] + B[i * M + j]; 22 }

Organizacja obliczeń Wybór liczby wymiarów przestrzeni indeksowania (1D, 2D lub 3D) zależy zazwyczaj od natury danych i wybranego algorytmu W przypadku przetwarzania danych 2D naturalne jest umieszczenie indeksów wątków w przestrzeni 2D

Organizacja obliczeń 16 16 blocks Rysunek : Przykład pokrycia tablicy 76x62 grupami po 16x16 wątków (Źródło: David B. Kirk and Wen-mei W. Hwu, Programming Massively Parallel Processors A Hands-on Approach) 80 64 wątki będą przetwarzać tablicę o wymiarach 76 62 Konieczne jest sprawdzanie, czy wątek odwołuje się do elementu w dozwolonym zakresie, np. if (get_global_id(0)< 62) W 408 ( 8%) przypadkach wątki nie wykonają użytecznej pracy

Organizacja obliczeń Ponieważ do kernela można przekazywać dane w postaci tablic jednowymiarowych, to dane wielowymiarowe muszą być serializowane Tablicę dwuwymiarową można zapisać w tablicy jednowymiarowej wiersz po wierszu (row-major) lub kolumna za kolumną (column-major) M 0,0 M 0,1 M 0,2 M 0,3 M 1,0 M 1,1 M 1,2 M 1,3 M 2,0 M 2,1 M 2,2 M 2,3 M M 3,0 M 3,1 M 3,2 M 3,3 M 0,0 M 0,1 M 0,2 M 0,3 M 1,0 M 1,1 M 1,2 M 1,3 M 2,0 M 2,1 M 2,2 M 2,3 M 3,0 M 3,1 M 3,2 M 3,3 M M 0 M 1 M 2 M 3 M 4 M 5 M 6 M 7 M 8 M 9 M 10 M 11 M 12 M 13 M 14 M 15 Rysunek : Serializacja macierzy wiersz po wierszu M[row][column] M[row * width + column]

Organizacja obliczeń M 0,0 M 0,1 M 0,2 M 0,3 M 1,0 M 1,1 M 1,2 M 1,3 M 2,0 M 2,1 M 2,2 M 2,3 M M 3,0 M 3,1 M 3,2 M 3,3 M 0,0 M 1,0 M 2,0 M 3,0 M 0,1 M 1,1 M 2,1 M 3,1 M 0,2 M 1,2 M 2,2 M 3,2 M 0,3 M 1,3 M 2,3 M 3,3 M M 0 M 1 M 2 M 3 M 4 M 5 M 6 M 7 M 8 M 9 M 10 M 11 M 12 M 13 M 14 M 15 Rysunek : Serializacja macierzy kolumna po kolumnie M[row][column] M[column * width + row]

Organizacja obliczeń Wątki dzielone są na bloki (grupy), z których każdy wykonuje się na pojedynczym multiprocesorze (PE) Maksymalny rozmiar bloku jest określony w wersji standardu CUDA / OpenCL wspieranej przez urządzenie np. 512/1024 (Compute Capability 1.x / 2.x-3.x) Maksymalny rozmiar bloku w każdym wymiarze (x, y i z) jest również ograniczony, przy czym x y z nie może przekroczyć rozmiaru bloku Wątki w bloku nie mogą używać więcej niż 8k/16k/32k rejestrów (Compute 1.0,1.1/1.2,1.3/2.x) Blok nie może użyć więcej niż 16kb/48kb współdzielonej pamięci (Compute 1.x/2.x)

Organizacja obliczeń Uwzględniając ograniczenia duży rozmiar bloku może ograniczyć wydajność, np. ze względu na zapotrzebowanie na pamięć Zbyt mały rozmiar bloku również jest niekorzystny niewystarczające wykorzystanie mocy obliczeniowej ze względu na to, że PE pracują w modelu SIMD/SIMT

Organizacja obliczeń wewnątrz bloku W kartach Nvidia 32 wątki bloku tworzą tzw. osnowę (ang. warp) wykonywane są w modelu SIMD/SIMT (ang. single instruction multiple threads) W kartach AMD odpowiednikiem osnowy jest tzw. wavefront złożony zazwyczaj z 64 wątków (w starszych modelach 16 lub 32) W procesorach ogólnego przeznaczenia rozmiar osnowy może zależeć od instrukcji kernela

Organizacja obliczeń wewnątrz bloku Rozmiar bloku powinien być wielokrotnością rozmiaru osnowy Jeżeli rozmiar osnowy wynosi 32, a bloku 16 to tracimy 50% mocy obliczeniowej Rozmiar osnowy nie jest ustandaryzowany, jednak udostępniany przez środowisko uruchomieniowe: CUDA CL_DEVICE_WARP_SIZE_NV OpenCL CL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE

Organizacja obliczeń wewnątrz bloku 1 size_t preferredsizemultiple; 2 clgetkernelworkgroupinfo(kernel, device, 3 CL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE, 4 sizeof(preferredsizemultiple), 5 &preferredsizemultiple, 6 NULL); 1 const size_t N =...; // Rozmiar danych do przetworzenia 2 size_t local_work_size[] = { preferredsizemultiple }; 3 // global_work_size[0] musi być wielokrotnością local_work_size[0] 4 size_t global_work_size[] = { 5 (size_t)ceil(n / (float)local_work_size[0]) * local_work_size[0] 6 }; Uwaga! Liczba wątków może być większa niż rozmiar danych

Organizacja obliczeń wewnątrz bloku 1 const size_t N =...; // Rozmiar danych do przetworzenia 2 size_t local_work_size[] = { preferredsizemultiple }; 3 // global_work_size[0] musi być wielokrotnością local_work_size[0] 4 size_t global_work_size[] = { 5 (size_t)ceil(n / (float)local_work_size[0]) * local_work_size[0] 6 }; Jeżeli N = 1234, preferredsizemultiple = 32 to global_work_size[0] = 39 * 32 = 1248 Wymaga to uwzględnienia w kodzie kernela, np. instrukcja if do sprawdzenia, czy nie odwołujemy się do elementów poza dopuszczalnym zakresem

Synchronizacja wątków Wątki wewnątrz grupy / bloku mogą być synchronizowane Wątki należące do różnych grup nie mogą być synchronizowane Jeżeli bariera synchronizacyjna umieszczona jest w bloku instrukcji warunkowej, to albo dla wszystkich wątków warunek jest spełniony, albo dla żadnego nie jest W przeciwnym razie program zakleszczy się Wątek 0 Wątek 1 Wątek 3 Wątek 4 Wątek 5 Wątek n-3 Wątek n-2 Wątek n-1 Czas

Synchronizacja wątków Synchronizacja wątków w OpenCL realizowana jest za pomocą funkcji void barrier (cl_mem_fence_flags flags), gdzie flags może przyjmować wartości: CLK_LOCAL_MEM_FENCE zapewnia, że zmiany w pamięci lokalnej dokonane przez wątki zostaną uszeregowane i wątki zobaczą spójny stan pamięci lokalnej CLK_GLOBAL_MEM_FENCE zapewnia, że wszystkie zapisy do pamięci głównej (obiektów pamięci, obrazów) zostaną wykonane i wątki zobaczą jej spójny stan Wywołanie funkcji barrier w różnych miejscach kernela oznacza odrębne bariery synchronizacyjne

Ukrywanie opóźnień Wiele uwagi zarówno w architekturze GPU, jak i środowisku programistycznym i wykonawczym poświęcono problemowi ukrywania opóźnień wynikających z powolnych operacji, takich jak odczyt i zapis do pamięci, barier synchronizacyjnych Jednym ze sposobów jest wykonywanie metodą przeplotu większej liczby wątków niż wynika to z liczby jednostek przetwarzających

Ukrywanie opóźnień Pojedynczy SM w architekturze Nvidia Fermi może wykonywać współbieżnie: do 48 osnów (warps) 32 wątki = 1536 wątków do 8 bloków jednocześnie SM wyposażony jest w 2 planistów (ang. warp scheduler) SM może jednocześnie wykonywać 2 grupy (osnowy, ang. warp) po 32 wątków Jak widać liczba wątków znacząco przekracza liczbę rdzeni CUDA (jednostek ALU/FP) Rysunek : Schemat SM w architekturze Nvidia Fermi

Ukrywanie opóźnień Duża liczba wątków konieczna jest aby ukryć opóźnienia Jeżeli wątki w osnowie muszą czekać na zakończenie wykonywania wolnej instrukcji, to w tym czasie wybierana jest inna osnowa, która gotowa jest do wykonania Jeżeli gotowych jest więcej, to wybór dokonywany jest na podstawie priorytetów Przełączanie pomiędzy osnowami nie powoduje opóźnień, jest to tzw. zero-overhead thread scheduling

Ukrywanie opóźnień przykład Pożądane jest by liczba bloków i wątków była blisko granic sprzętowych W przypadku architektury Nvidia Fermi mamy maks. 8 bloków (po maks. 1024 wątki), ale nie więcej niż 1536 wątków na SM Rozmiar bloku Wątki na blok Liczba bloków Wątki razem 8x8 64 8 512 16x16 256 6 1536 32x32 1024 1 1024

Ukrywanie opóźnień pamięć Nawet duża liczba bloków i wątków nie zawsze jest w stanie ukryć wszystkich opóźnień, szczególnie wynikających z operacji na pamięci globalnej Opóźnienie w dostępie do pamięci globalnej może sięgać setek cykli, stąd należy redukować liczbę operacji na pamięci, np. przez cacheowanie danych zapewnić ich uporządkowanie łączone odczyty danych z pamięci głównej

Hierarchia pamięci GPU Hierarchia pamięci na przykładzie Nvidia Fermi Fermi Memory Hierarchy Review SM-0 SM-1 SM-N Fermi Chip Registers L1 SMEM Registers L1 SMEM Registers L1 SMEM L2 Global Memory NVIDIA Corporation 2011

Pamięć GPU opóźnienia W architekturze Nvidia Fermi: rejestry mają łączną przepustowość ok. 8TB/sek. (maks. 63 rejestry 32 bitowe na kernel) pamięć współdzielona / L1 (64KB) ma przepustowość łączną ok 1.6TB/sek. i bardzo niskie opóźnienie (10-20 cykli) pamięć globalna ma przepustowość do 177GB/sek. i opóźnienie rzędu 400-800 cykli Jak widać, im mniej operacji na pamięci globalnej, tym lepiej

Optymalizacja dostępu do pamięci na przykładzie Przyjrzymy się wzorcom dostępu do pamięci w programie mnożącym macierze Złożoność standardowego algorytmu mnożenia to O(n 3 ), dla porównania: Algorytm Strassena ma złożoność O(n 2.807355 ) Najlepszy znany algorytm (2014) autorstwa François Le Gall ma złożoność O(n 2.3728639 )

Mnożenie macierzy wersja CPU A B Rysunek : Matrix multiplication diagram, autor Bilou 1 /* 2 A, B, C - tablice 2D o rozmiarze size x size 3 */ 4 for (int i = 0; i < size; ++i) { 5 for (int j = 0; j < size; ++j) { 6 sum = 0; 7 for (int k = 0; k < size; ++k) { 8 sum += A[i][k] * B[k][j]; 9 } 10 C[i][j] = sum; 11 } 12 }

Mnożenie macierzy wersja CPU A B Rysunek : Matrix multiplication diagram, autor Bilou 1 /* 2 Wersja dla macierzy zapisanych w tablicach 1D 3 A, B, C - tablice 1D o długości size x size 4 */ 5 for (size_t i = 0; i < size; ++i) { 6 for (size_t j = 0; j < size; ++j) { 7 int sum = 0; 8 for (size_t k = 0; k < size; ++k) { 9 sum += A[i*size + k] * B[k*size + j]; 10 } 11 C[i*size + j] = sum; 12 } 13 }

Optymalizacja dostępu do pamięci Kernel OpenCL obliczający iloczyn macierzy 1 kernel void matrix_multiply(int size, 2 global float *A, 3 global float *B, 4 global float *C) { 5 const int col = get_global_id(0); 6 const int row = get_global_id(1); 7 if (row < size && col < size) { 8 float sum = 0; 9 for (int i = 0; i < size; ++i) { 10 sum += A[row * size + i] * B[i * size + col]; 11 } 12 C[row * size + col] = sum; 13 } 14 }

Analiza dostępu do pamięci Za większość operacji na pamięci głównej odpowiedzialny jest fragment: 1 for (int i = 0; i < size; ++i) { 2 sum += A[row * size + i] * B[i * size + col]; 3 } W każdej iteracji pętli wykonywane jest 1 mnożenie, 1 dodawanie i 2 odczyty z tablic, odpowiednio, A oraz B Stosunek liczby instrukcji obliczeń do liczby instrukcji dostępu do pamięci wynosi 1:1 Nazywany również compute to global memory access (CGMA) ratio

Analiza dostępu do pamięci CGMA ma kluczowy wpływ na wydajność obliczeń GPU Zakładając przepustowość pamięci na poziomie 200GB/sek. pozwala to załadować 200GB/4B = 50Giga liczb typu float Przy CGMA = 1.0 kernel może wykonać co najwyżej 50 GFLOPS operacji na sekundę znacznie mniej, niż szczytowa moc obliczeniowa (np. 1500 GFLOPS) W celu poprawy wydajności należy zwiększyć wartość CGMA, tj. więcej obliczeń na daną liczbę odczytów/zapisów pamięci głównej

Optymalizacja dostępu do pamięci Zauważmy, że wątki odwołują się w części do tych samych danych B 0,0 B 0,1 B 1,0 B 1,1 B 2,0 B 2,1 B 3,0 B 3,1 A 0,0 A 0,1 A 0,2 A 0,3 C 0,0 C 0,1 C 0,2 C 0,3 A 1,0 A 1,1 A 1,2 A 1,3 C 1,0 C 1,1 C 1,2 C 1,3 C 2,0 C 2,1 C 2,2 C 2,3 C 3,0 C 3,1 C 3,2 C 3,3 4 wątki wykonują 4 (4 + 4) = 32 odczyty pamięci, przy czym różnych jest jedynie 16

Optymalizacja dostępu do pamięci Jeżeli udałoby się zmusić wątki do współpracy i wykorzystania raz załadowanych danych to transfer danych zmniejszyłby się o połowę Rozwiązaniem jest podzielenie obliczeń na małe porcje 2 2 Każdy wątek ładuje element do pomocniczych tablic N oraz M umieszczonych w pamięci lokalnej B 0,0 B 0,1 B 1,0 B 1,1 B 2,0 B 2,1 B 3,0 B 3,1 A 0,0 A 0,1 A 0,2 A 0,3 C 0,0 C 0,1 C 0,2 C 0,3 A 1,0 A 1,1 A 1,2 A 1,3 C 1,0 C 1,1 C 1,2 C 1,3 C 2,0 C 2,1 C 2,2 C 2,3 C 3,0 C 3,1 C 3,2 C 3,3

Optymalizacja dostępu do pamięci Faza I: wątek (0,0) wykonuje N 0,0 = A 0,0 M 0,0 = B 0,0 wątek (1,0) wykonuje N 1,0 = A 1,0 M 1,0 = B 1,0... Faza II: wątek (0,0) wykonuje N 0,0 = A 0,0+2 M 0,0 = B 0+2,0 wątek (1,0) wykonuje N 1,0 = A 1,0+2 M 1,0 = B 1+2,0... A 0,0 A 0,1 A 0,2 A 0,3 B 0,0 B 0,1 B 1,0 B 1,1 B 2,0 B 2,1 B 3,0 B 3,1 C 0,0 C 0,1 C 0,2 C 0,3 A 1,0 A 1,1 A 1,2 A 1,3 C 1,0 C 1,1 C 1,2 C 1,3 C 2,0 C 2,1 C 2,2 C 2,3 C 3,0 C 3,1 C 3,2 C 3,3

Optymalizacja dostępu do pamięci Wartość elementu C 0,0 jest sumą (N 0,0 M 0,0 + N 0,1 M 1,0 ) I faza + (N 0,0 M 0,0 + N 0,1 M 1,0 ) II faza Analogicznie dla pozostałych elementów Dzięki podzieleniu obliczeń na kafelki (bloki) poprawiamy lokalność odwołań Oczywiście, można stosować kafelki o rozmiarze większym od 2 2, np. 16 16

Optymalizacja dostępu do pamięci 1 #define TILE_WIDTH 16 2 int col = get_global_id(0); 3 int row = get_global_id(1); 4 local float M[TILE_WIDTH][TILE_WIDTH]; 5 local float N[TILE_WIDTH][TILE_WIDTH]; 6 // Współrzędne wątku wew. kafelka / bloku 7 int tx = get_local_id(0); 8 int ty = get_local_id(1); 9 float sum = 0; 10 for (int m = 0; m < ceil(size / (float)tile_width); ++m) { 11 // Wspólne ładowanie macierzy do tablic pomocniczych 12 M[ty][tx] = A[row * size + m * TILE_WIDTH + tx]; 13 N[ty][tx] = = B[(m * TILE_WIDTH + ty) * size + col]; 14 barrier(clk_local_mem_fence); 15 for (int k = 0; k < TILE_WIDTH; ++k) { 16 sum += M[ty][k] * N[k][tx]; 17 } 18 barrier(clk_local_mem_fence); 19 } 20 C[row * size + col] = sum;

Optymalizacja dostępu do pamięci Kafelki o rozmiarach 16 16 pozwalają zmniejszyć ilość odczytywanych danych 16 razy Liczba obliczeń pozostaje taka sama, czyli CGMA rośnie z 1 do 16 Przy 200GB/sek pozwala to wykonać 200GB/4B = 50Giga x 16 = 800GFLOPS

Optymalizacja wzorców dostępu do pamięci Wątki wewnątrz warpów 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 ptrzebujemy 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ład łą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

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