Szukaj…


Wprowadzenie

W tym temacie opisano niektóre podstawowe mechanizmy przetwarzania równoległego, które są potrzebne do pełnego zrozumienia i wykorzystania OpenCL.

Wątki i wykonanie

Kluczem do równoległości jest użycie wielu wątków do rozwiązania problemu (duh.), Ale istnieją pewne różnice w klasycznym programowaniu wielowątkowym w sposobie organizacji wątków.

Najpierw porozmawiajmy o twoim typowym GPU, dla uproszczenia skupię się na

Procesor graficzny ma wiele rdzeni przetwarzających, dzięki czemu idealnie nadaje się do wykonywania wielu wątków równolegle. Rdzenie te są zorganizowane w procesory strumieniowe (SM, termin NVidia), których GPU ma podaną liczbę.

Wszystkie wątki działające w SM są nazywane „blokiem wątków”. W SM może być więcej wątków niż w rdzeniach. Liczba rdzeni określa tak zwany „rozmiar osnowy” (termin NVidia). Wątki w bloku nici są rzucane w tak zwane „osnowy”.

Szybki przykład do naśladowania: Typowy NVidia SM ma 32 rdzenie przetwarzające, a zatem jego rozmiar osnowy wynosi 32. Jeśli mój blok wątków ma teraz 128 wątków do uruchomienia, zostaną one podzielone na 4 osnowy (4 osnowy * 32 osnowy = 128 wątki).

Rozmiar osnowy jest raczej ważny przy późniejszym wyborze liczby wątków.

Wszystkie wątki w jednym osnowie mają ten sam licznik instrukcji. Oznacza to, że te 32 wątki są naprawdę zsynchronizowane, ponieważ każdy wątek wykonuje każde polecenie w tym samym czasie. Oto pułapka wydajności: dotyczy to również rozgałęzień instrukcji w twoim jądrze!

Przykład: Mam jądro, które ma instrukcję if i dwie gałęzie. 16 moich wątków wewnątrz osnowy wykona odgałęzienie pierwsze, a pozostałe 16 rozgałęzienie drugie. Aż do instrukcji if wszystkie wątki w warp są zsynchronizowane. Teraz połowa z nich wybiera inną gałąź. Co się dzieje, druga połowa będzie uśpiona, dopóki błędne polecenie nie zakończy wykonywania pierwszych 16 wątków. Wówczas wątki będą nieaktywne, dopóki pozostałe 16 wątków nie zakończy gałęzi.

Jak widać, złe nawyki rozgałęziania mogą poważnie spowolnić kod równoległy, ponieważ obie instrukcje są wykonywane w najgorszym przypadku. Jeśli wszystkie wątki w osnowie stwierdzą, że potrzebują tylko jednego z oświadczeń, drugi jest całkowicie pomijany i nie występuje opóźnienie.

Synchronizacja wątków również nie jest prostą sprawą. Możesz synchronizować wątki tylko z jednym SM. Wszystko poza SM jest niezsynchronizowane z wnętrza jądra. Będziesz musiał pisać osobne jądra i uruchamiać je jeden po drugim.

Pamięć GPU

GPU oferuje sześć różnych regionów pamięci. Różnią się opóźnieniem, rozmiarem i dostępnością z różnych wątków.

  • Pamięć globalna: największa dostępna pamięć i jedna z niewielu do wymiany danych z hostem. Ta pamięć ma największe opóźnienia i jest dostępna dla wszystkich wątków.
  • Stała pamięć: część pamięci globalnej przeznaczona tylko do odczytu, którą mogą odczytać tylko inne wątki. Jego zaletą jest mniejsze opóźnienie w porównaniu do pamięci globalnej
  • Pamięć tekstur: także część stałej pamięci, specjalnie zaprojektowana dla tekstur
  • Pamięć współdzielona: ten obszar pamięci jest umieszczony blisko SM i może być dostępny tylko przez jeden blok wątków. Oferuje znacznie mniejsze opóźnienie niż pamięć globalna i nieco mniejsze opóźnienie niż pamięć stała.
  • Rejestry: dostępne tylko przez jeden wątek i najszybszą pamięć ze wszystkich. Ale jeśli kompilator wykryje, że nie ma wystarczającej liczby rejestrów na potrzeby jądra, przekaże zmienne do pamięci lokalnej.
  • Pamięć lokalna: dostępna tylko w wątku część pamięci w regionie pamięci globalnej. Używany jako kopia zapasowa rejestrów, w miarę możliwości należy go unikać.

Dostęp do pamięci

Typowym scenariuszem użycia pamięci jest przechowywanie danych źródłowych i przetworzonych danych w pamięci globalnej. Kiedy zaczyna się blokowanie wątków, najpierw kopiuje wszystkie odpowiednie części do pamięci współużytkowanej, a następnie umieszcza je w rejestrach.

Opóźnienie dostępu do pamięci zależy również od strategii pamięci. Jeśli ślepo uzyskasz dostęp do danych, uzyskasz najgorszą możliwą wydajność.

Różne wspomnienia są zorganizowane w tak zwane „banki”. Każde żądanie pamięci dla banku może być obsługiwane w jednym cyklu zegara. Liczba banków we wspólnej pamięci jest równa wielkości warp. Szybkość pamięci można zwiększyć, unikając konfliktu dostępu do banku w jednej warp.

Aby skopiować pamięć współdzieloną z lub do pamięci globalnej, najszybszym sposobem jest „wyrównanie” połączeń z pamięcią. Oznacza to, że pierwszy wątek w osnowie powinien uzyskać dostęp do pierwszego elementu w banku pamięci wspólnej i globalnej. Drugi wątek, drugi element i tak dalej. To połączenie zostanie zoptymalizowane do pojedynczej instrukcji transferu pamięci, która za jednym razem kopiuje cały bank do pamięci docelowej.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow