Zoptymalizuj wydajność GPU TensorFlow za pomocą TensorFlow Profiler

Przegląd

Ten przewodnik pokaże Ci, jak używać TensorFlow Profiler z TensorBoard, aby uzyskać wgląd i uzyskać maksymalną wydajność swoich GPU oraz debugować, gdy co najmniej jeden z Twoich GPU jest niewykorzystany.

Jeśli jesteś nowy w Profiler:

Należy pamiętać, że odciążanie obliczeń na GPU może nie zawsze być korzystne, szczególnie w przypadku małych modeli. Może wystąpić obciążenie z powodu:

  • Transfer danych między hostem (CPU) a urządzeniem (GPU); oraz
  • Ze względu na opóźnienie związane z uruchomieniem jądra GPU przez hosta.

Proces optymalizacji wydajności

W tym przewodniku opisano, jak debugować problemy z wydajnością, zaczynając od pojedynczego procesora GPU, a następnie przechodząc do jednego hosta z wieloma procesorami GPU.

Zaleca się debugowanie problemów z wydajnością w następującej kolejności:

  1. Zoptymalizuj i debuguj wydajność na jednym GPU:
    1. Sprawdź, czy rurociąg wejściowy jest wąskim gardłem.
    2. Debuguj wydajność jednego GPU.
    3. Włącz precyzję mieszaną (z fp16 (float16)) i opcjonalnie włącz XLA .
  2. Optymalizuj i debuguj wydajność na pojedynczym hoście obsługującym wiele procesorów graficznych.

Na przykład, jeśli używasz strategii dystrybucji TensorFlow do trenowania modelu na jednym hoście z wieloma procesorami graficznymi i zauważysz nieoptymalne wykorzystanie procesora GPU, powinieneś najpierw zoptymalizować i debugować wydajność dla jednego procesora graficznego przed debugowaniem systemu z wieloma procesorami graficznymi.

Jako punkt odniesienia dla uzyskania wydajnego kodu na procesorach graficznych w tym przewodniku założono, że używasz już tf.function . Interfejsy API Keras Model.compile i Model.fit będą automatycznie wykorzystywać tf.function pod maską. Pisząc niestandardową pętlę treningową za pomocą tf.GradientTape , zapoznaj się z artykułem Lepsza wydajność dzięki tf.function , aby dowiedzieć się, jak włączyć tf.function s.

W następnych sekcjach omówiono sugerowane podejścia dla każdego z powyższych scenariuszy, aby pomóc zidentyfikować i naprawić wąskie gardła wydajności.

1. Zoptymalizuj wydajność na jednym GPU

W idealnym przypadku program powinien mieć wysokie wykorzystanie procesora GPU, minimalną komunikację między procesorem (host) a procesorem graficznym (urządzenie) i brak obciążenia z potoku wejściowego.

Pierwszym krokiem w analizie wydajności jest uzyskanie profilu dla modelu działającego z jednym GPU.

Strona przeglądu Profiler w TensorBoard — która pokazuje widok najwyższego poziomu, jak Twój model działał podczas uruchamiania profilu — może dać wyobrażenie o tym, jak daleko jest Twój program od idealnego scenariusza.

TensorFlow Profiler Overview Page

Kluczowe liczby, na które należy zwrócić uwagę na stronie przeglądu, to:

  1. Ile czasu kroku jest od rzeczywistego wykonania urządzenia?
  2. Odsetek operacji umieszczonych na urządzeniu w porównaniu z hostem
  3. Ile jąder używa fp16

Osiągnięcie optymalnej wydajności oznacza maksymalizację tych liczb we wszystkich trzech przypadkach. Aby uzyskać dogłębne zrozumienie swojego programu, musisz znać przeglądarkę śledzenia Profiler firmy TensorBoard. W poniższych sekcjach przedstawiono niektóre typowe wzorce przeglądarki śledzenia, których należy szukać podczas diagnozowania wąskich gardeł wydajności.

Poniżej znajduje się obraz widoku śledzenia modelu działającego na jednym GPU. W sekcjach TensorFlow Name Scope i TensorFlow Ops można zidentyfikować różne części modelu, takie jak przejście do przodu, funkcja straty, obliczenia przebiegu wstecznego/gradientu oraz aktualizacja wagi optymalizatora. Możesz również uruchomić operacje na GPU obok każdego strumienia , które odnoszą się do strumieni CUDA. Każdy strumień jest używany do określonych zadań. W tym śladzie strumień #118 jest używany do uruchamiania jąder obliczeniowych i kopii urządzenie-urządzenie. Strumień nr 119 służy do kopiowania z hosta na urządzenie, a strumień nr 120 do kopiowania z urządzenia do hosta.

Poniższy ślad pokazuje wspólne cechy wydajnego modelu.

image

Na przykład oś czasu obliczeń GPU ( Stream#118 ) wygląda na „zajętą” z bardzo małą liczbą przerw. Istnieje minimalna liczba kopii z hosta na urządzenie ( strumień 119 ) i od urządzenia do hosta ( strumień 120 ), a także minimalne odstępy między krokami. Po uruchomieniu programu Profiler może nie być możliwe zidentyfikowanie tych idealnych cech w widoku śledzenia. Pozostała część tego przewodnika obejmuje typowe scenariusze i sposoby ich rozwiązywania.

1. Debuguj potok wejściowy

Pierwszym krokiem w debugowaniu wydajności GPU jest ustalenie, czy program jest powiązany z danymi wejściowymi. Najprostszym sposobem, aby to rozgryźć, jest użycie analizatora potoków wejściowych Profilera na TensorBoard, który zapewnia przegląd czasu spędzonego w potoku wejściowym.

image

Możesz podjąć następujące potencjalne działania, jeśli potok danych wejściowych znacząco wpływa na czas kroku:

  • Możesz skorzystać z przewodnika dotyczącego tf.data , aby dowiedzieć się, jak debugować potok danych wejściowych.
  • Innym szybkim sposobem sprawdzenia, czy potok wejściowy jest wąskim gardłem, jest użycie losowo generowanych danych wejściowych, które nie wymagają wstępnego przetwarzania. Oto przykład użycia tej techniki dla modelu ResNet. Jeśli potok wejściowy jest optymalny, powinieneś doświadczyć podobnej wydajności z rzeczywistymi danymi i wygenerowanymi losowymi/syntetycznymi danymi. Jedynym obciążeniem w przypadku danych syntetycznych będzie kopiowanie danych wejściowych, które ponownie można wstępnie pobrać i zoptymalizować.

Ponadto zapoznaj się ze sprawdzonymi metodami optymalizacji potoku danych wejściowych .

2. Debuguj wydajność jednego GPU

Istnieje kilka czynników, które mogą przyczynić się do niskiego wykorzystania GPU. Poniżej przedstawiono niektóre scenariusze często obserwowane podczas przeglądania przeglądarki śledzenia i potencjalne rozwiązania.

1. Przeanalizuj luki między krokami

Częstą obserwacją, gdy Twój program nie działa optymalnie, są przerwy między etapami treningu. Na poniższym obrazie widoku śledzenia widać dużą przerwę między krokami 8 i 9, co oznacza, że ​​w tym czasie procesor graficzny jest bezczynny.

image

Jeśli przeglądarka śledzenia pokazuje duże przerwy między krokami, może to wskazywać, że program jest powiązany z danymi wejściowymi. W takim przypadku powinieneś zapoznać się z poprzednią sekcją dotyczącą debugowania potoku wejściowego, jeśli jeszcze tego nie zrobiłeś.

Jednak nawet przy zoptymalizowanym potoku wejściowym nadal mogą występować przerwy między końcem jednego kroku a początkiem drugiego z powodu rywalizacji o wątki procesora. tf.data wykorzystuje wątki w tle do równoległego przetwarzania potoku. Te wątki mogą zakłócać aktywność GPU po stronie hosta, która ma miejsce na początku każdego kroku, na przykład kopiowanie danych lub planowanie operacji GPU.

Jeśli zauważysz duże przerwy po stronie hosta, które planują te operacje na GPU, możesz ustawić zmienną środowiskową TF_GPU_THREAD_MODE=gpu_private . Gwarantuje to, że jądra GPU są uruchamiane z ich własnych dedykowanych wątków i nie są ustawiane w kolejce za pracą tf.data .

Luki między krokami mogą być również spowodowane obliczeniami metryk, wywołaniami zwrotnymi Keras lub operacjami poza tf.function , które działają na hoście. Te operacje nie mają tak dobrej wydajności, jak operacje na wykresie TensorFlow. Ponadto niektóre z tych operacji działają na procesorze i kopiują tensory tam iz powrotem z GPU.

Jeśli po zoptymalizowaniu potoku wejściowego nadal zauważysz przerwy między krokami w przeglądarce śledzenia, powinieneś spojrzeć na kod modelu między krokami i sprawdzić, czy wyłączenie wywołań zwrotnych/metryk poprawia wydajność. Niektóre szczegóły tych operacji znajdują się również w przeglądarce śledzenia (zarówno po stronie urządzenia, jak i hosta). W tym scenariuszu zaleca się amortyzację kosztów tych operacji, wykonując je po określonej liczbie kroków zamiast po każdym kroku. W przypadku korzystania z metody compile w interfejsie API tf.keras ustawienie experimental_steps_per_execution powoduje to automatycznie. W przypadku niestandardowych pętli szkoleniowych użyj tf.while_loop .

2. Osiągnij wyższe wykorzystanie urządzenia

1. Małe jądra GPU i opóźnienia uruchamiania jądra hosta

Host umieszcza jądra w kolejce do uruchomienia na GPU, ale występuje opóźnienie (około 20-40 μs), zanim jądra zostaną faktycznie wykonane na GPU. W idealnym przypadku host umieszcza w kolejce wystarczającą liczbę jąder na GPU, aby GPU spędzał większość czasu na wykonywaniu, zamiast czekać na hosta w kolejce do większej liczby jąder.

Strona przeglądu Profilera na TensorBoard pokazuje, ile czasu GPU było bezczynne z powodu oczekiwania na hoście na uruchomienie jądra. Na poniższym obrazku GPU jest bezczynny przez około 10% czasu kroku, czekając na uruchomienie jądra.

image

Przeglądarka śledzenia dla tego samego programu pokazuje małe luki między jądrami, gdy host jest zajęty uruchamianiem jąder na GPU.

image

Uruchamiając wiele małych operacji na GPU (takich jak na przykład dodanie skalarne), host może nie nadążać za GPU. Narzędzie TensorFlow Stats w TensorBoard dla tego samego profilu pokazuje 126 224 Mul operacji trwających 2,77 sekundy. Tak więc każde jądro ma około 21,9 μs, co jest bardzo małe (mniej więcej w tym samym czasie co opóźnienie uruchamiania) i może potencjalnie powodować opóźnienia uruchamiania jądra hosta.

image

Jeśli twoja przeglądarka śladów pokazuje wiele małych przerw między operacjami na GPU, jak na powyższym obrazku, możesz:

  • Połącz małe tensory i użyj wektoryzacji operacji lub użyj większego rozmiaru wsadu, aby każde uruchomione jądro wykonało więcej pracy, co sprawi, że GPU będzie dłużej zajęty.
  • Upewnij się, że używasz tf.function do tworzenia wykresów TensorFlow, aby nie uruchamiać operacji w czystym trybie gorliwym. Jeśli używasz Model.fit (w przeciwieństwie do niestandardowej pętli treningowej z tf.GradientTape ), tf.keras.Model.compile zrobi to automatycznie za Ciebie.
  • Połącz jądra za pomocą XLA za pomocą tf.function(jit_compile=True) lub automatycznego klastrowania. Aby uzyskać więcej informacji, przejdź do poniższej sekcji Włącz mieszaną precyzję i XLA , aby dowiedzieć się, jak włączyć XLA w celu uzyskania wyższej wydajności. Ta funkcja może prowadzić do wysokiego wykorzystania urządzenia.
2. Umieszczenie operacji TensorFlow

Strona przeglądu Profiler pokazuje procent operacji umieszczonych na hoście w porównaniu do urządzenia (możesz również zweryfikować rozmieszczenie określonych operacji, patrząc na przeglądarkę śledzenia . Jak na obrazku poniżej, chcesz, aby procent operacji na hoście być bardzo mały w porównaniu do urządzenia.

image

W idealnym przypadku większość operacji wymagających dużej mocy obliczeniowej powinna być umieszczona na GPU.

Aby dowiedzieć się, do jakich urządzeń są przypisane operacje i tensory w twoim modelu, ustaw tf.debugging.set_log_device_placement(True) jako pierwszą instrukcję twojego programu.

Zauważ, że w niektórych przypadkach, nawet jeśli określisz opcję, która ma być umieszczona na konkretnym urządzeniu, jej implementacja może unieważnić ten warunek (przykład: tf.unique ). Nawet w przypadku uczenia pojedynczego GPU, określenie strategii dystrybucji, takiej jak tf.distribute.OneDeviceStrategy , może skutkować bardziej deterministycznym rozmieszczeniem operacji na twoim urządzeniu.

Jednym z powodów umieszczania większości operacji na GPU jest zapobieganie nadmiernym kopiom pamięci między hostem a urządzeniem (oczekuje się kopii pamięci dla danych wejściowych/wyjściowych modelu między hostem a urządzeniem). Przykład nadmiernego kopiowania jest pokazany w poniższym widoku śledzenia strumieni GPU #167 , #168 i #169 .

image

Te kopie mogą czasami obniżyć wydajność, jeśli blokują wykonywanie jąder GPU. Operacje kopiowania pamięci w przeglądarce śledzenia zawierają więcej informacji o operacjach, które są źródłem tych skopiowanych tensorów, ale skojarzenie memCopy z operacją może nie zawsze być łatwe. W takich przypadkach warto przyjrzeć się pobliskim operatorom, aby sprawdzić, czy kopia pamięci dzieje się w tym samym miejscu na każdym kroku.

3. Wydajniejsze jądra na GPU

Gdy wykorzystanie procesora graficznego przez program jest akceptowalne, następnym krokiem jest przyjrzenie się zwiększeniu wydajności jąder GPU poprzez wykorzystanie rdzeni tensorowych lub fusing ops.

1. Wykorzystaj rdzenie tensorów

Nowoczesne procesory graficzne NVIDIA® mają wyspecjalizowane rdzenie tensorowe , które mogą znacznie poprawić wydajność odpowiednich jąder.

Możesz użyć statystyk jądra GPU TensorBoard, aby zwizualizować, które jądra GPU kwalifikują się do rdzenia Tensor, a które używają rdzeni Tensor. Włączenie fp16 (patrz sekcja Włączanie Mixed Precision poniżej) jest jednym ze sposobów, aby jądra General Matrix Multiply (GEMM) (matmul ops) Twojego programu wykorzystywały rdzeń tensorowy. Jądra GPU efektywnie wykorzystują rdzenie tensorowe, gdy precyzja wynosi fp16, a wymiary tensora wejścia/wyjścia są podzielne przez 8 lub 16 (dla int8 ).

Aby uzyskać inne szczegółowe zalecenia dotyczące zwiększania wydajności jądra dla procesorów graficznych, zapoznaj się z przewodnikiem po wydajności głębokiego uczenia NVIDIA® .

2. Operacje bezpieczników

Użyj tf.function(jit_compile=True) , aby połączyć mniejsze operacje w większe jądra, co prowadzi do znacznego wzrostu wydajności. Aby dowiedzieć się więcej, zapoznaj się z przewodnikiem XLA .

3. Włącz mieszaną precyzję i XLA

Po wykonaniu powyższych kroków włączenie mieszanej precyzji i XLA to dwa opcjonalne kroki, które możesz wykonać, aby jeszcze bardziej poprawić wydajność. Sugerowanym podejściem jest włączenie ich pojedynczo i sprawdzenie, czy korzyści z wydajności są zgodne z oczekiwaniami.

1. Włącz mieszaną precyzję

Przewodnik po precyzji TensorFlow Mixed pokazuje, jak włączyć precyzję fp16 na procesorach graficznych. Włącz AMP na procesorach graficznych NVIDIA®, aby korzystać z rdzeni tensorowych i uzyskać do 3x ogólne przyspieszenie w porównaniu z fp32 (float32) w przypadku architektur Volta i nowszych.

Upewnij się, że wymiary macierzy/tensorów spełniają wymagania dotyczące wywoływania jąder korzystających z rdzeni tensorów. Jądra GPU efektywnie wykorzystują rdzenie tensorowe, gdy precyzja wynosi fp16, a wymiary wejścia/wyjścia są podzielne przez 8 lub 16 (dla int8).

Należy zauważyć, że w przypadku cuDNN w wersji 7.6.3 i nowszych wymiary splotu zostaną automatycznie uzupełnione w razie potrzeby w celu wykorzystania rdzeni tensorowych.

Postępuj zgodnie z poniższymi najlepszymi praktykami, aby zmaksymalizować korzyści wynikające z precyzji fp16 .

1. Używaj optymalnych jąder fp16

Przy włączonym fp16 , jądra programu mnożenia macierzy (GEMM) powinny używać odpowiedniej wersji fp16 , która wykorzystuje rdzenie tensorowe. Jednak w niektórych przypadkach tak się nie dzieje i nie doświadczasz oczekiwanego przyspieszenia po włączeniu fp16 , ponieważ zamiast tego twój program wraca do nieefektywnej implementacji.

image

Strona statystyk jądra GPU pokazuje, które operacje kwalifikują się do Tensor Core i które jądra faktycznie używają wydajnego Tensor Core. Poradnik NVIDIA® dotyczący wydajności głębokiego uczenia zawiera dodatkowe sugestie dotyczące wykorzystania rdzeni tensorowych. Dodatkowo, korzyści z używania fp16 pojawią się również w jądrach, które wcześniej były związane z pamięcią, ponieważ teraz operacje zajmą połowę czasu.

2. Dynamiczne a statyczne skalowanie strat

Skalowanie strat jest konieczne podczas korzystania z fp16 , aby zapobiec niedopełnieniu z powodu niskiej precyzji. Istnieją dwa rodzaje skalowania strat, dynamiczne i statyczne, które zostały szczegółowo wyjaśnione w przewodniku dotyczącym precyzji mieszanej . Możesz użyć zasady mixed_float16 , aby automatycznie włączyć skalowanie strat w optymalizatorze Keras.

Próbując zoptymalizować wydajność, należy pamiętać, że dynamiczne skalowanie strat może wprowadzić dodatkowe operacje warunkowe działające na hoście i prowadzić do przerw, które będą widoczne między krokami w przeglądarce śledzenia. Z drugiej strony, skalowanie strat statycznych nie wiąże się z takimi kosztami ogólnymi i może być lepszą opcją pod względem wydajności z haczykiem, który należy określić poprawną wartość skali strat statycznych.

2. Włącz XLA za pomocą tf.function(jit_compile=True) lub automatycznego klastrowania

Ostatnim krokiem do uzyskania najlepszej wydajności przy użyciu pojedynczego procesora graficznego może być eksperymentowanie z włączeniem XLA, które połączy operacje i doprowadzi do lepszego wykorzystania urządzeń i mniejszego zużycia pamięci. Aby uzyskać szczegółowe informacje na temat włączania XLA w programie za pomocą tf.function(jit_compile=True) lub automatycznego klastrowania, zapoznaj się z przewodnikiem XLA .

Możesz ustawić globalny poziom JIT na -1 (wyłączony), 1 lub 2 . Wyższy poziom jest bardziej agresywny i może zmniejszać równoległość i zużywać więcej pamięci. Ustaw wartość na 1 , jeśli masz ograniczenia pamięci. Zauważ, że XLA nie działa dobrze w przypadku modeli ze zmiennymi kształtami tensora wejściowego, ponieważ kompilator XLA musiałby kompilować jądra za każdym razem, gdy napotka nowe kształty.

2. Zoptymalizuj wydajność na pojedynczym hoście z wieloma procesorami graficznymi

Interfejs API tf.distribute.MirroredStrategy może służyć do skalowania uczenia modeli z jednego procesora GPU do wielu procesorów GPU na jednym hoście. (Aby dowiedzieć się więcej o tym, jak przeprowadzać szkolenia rozproszone z TensorFlow, zapoznaj się z przewodnikami Szkolenie rozproszone z TensorFlow , Korzystanie z GPU i Korzystanie z TPU oraz samouczek Szkolenie rozproszone z Keras .)

Chociaż przejście z jednego procesora graficznego na wiele procesorów graficznych powinno być skalowalne po wyjęciu z pudełka, czasami mogą wystąpić problemy z wydajnością.

W przypadku przechodzenia ze szkolenia z jednym procesorem GPU do wielu procesorów GPU na tym samym hoście najlepiej byłoby doświadczyć skalowania wydajności tylko z dodatkowym obciążeniem komunikacji gradientowej i zwiększonym wykorzystaniem wątków hosta. Z powodu tego narzutu nie będziesz mieć dokładnego 2-krotnego przyspieszenia, jeśli na przykład przejdziesz z 1 do 2 procesorów graficznych.

Poniższy widok śledzenia pokazuje przykład dodatkowego obciążenia komunikacyjnego podczas uczenia na wielu procesorach graficznych. Jest trochę narzutu na łączenie gradientów, przekazywanie ich między replikami i dzielenie ich przed aktualizacją wagi.

image

Poniższa lista kontrolna pomoże Ci osiągnąć lepszą wydajność podczas optymalizacji wydajności w scenariuszu z wieloma procesorami graficznymi:

  1. Spróbuj zmaksymalizować rozmiar partii, co doprowadzi do większego wykorzystania urządzeń i amortyzacji kosztów komunikacji między wieloma procesorami graficznymi. Korzystanie z profilera pamięci pomaga określić, jak blisko jest szczytowe wykorzystanie pamięci przez program. Należy zauważyć, że chociaż większa wielkość partii może wpływać na zbieżność, zwykle przewyższa to korzyści w zakresie wydajności.
  2. Przechodząc z jednego procesora graficznego na wiele procesorów graficznych, ten sam host musi teraz przetwarzać znacznie więcej danych wejściowych. Dlatego po (1) zaleca się ponowne sprawdzenie wydajności potoku wejściowego i upewnienie się, że nie jest to wąskie gardło.
  3. Sprawdź oś czasu GPU w widoku śledzenia programu pod kątem niepotrzebnych wywołań AllReduce, ponieważ powoduje to synchronizację na wszystkich urządzeniach. W widoku śledzenia pokazanym powyżej, AllReduce odbywa się za pośrednictwem jądra NCCL , a na każdym GPU jest tylko jedno wywołanie NCCL dla gradientów na każdym kroku.
  4. Sprawdź, czy nie ma niepotrzebnych operacji kopiowania D2H, H2D i D2D, które można zminimalizować.
  5. Sprawdź czas kroku, aby upewnić się, że każda replika wykonuje tę samą pracę. Na przykład może się zdarzyć, że jeden procesor GPU (zazwyczaj GPU0 ) jest nadmiernie subskrybowany, ponieważ host omyłkowo wykonuje na nim więcej pracy.
  6. Na koniec sprawdź krok uczenia na wszystkich procesorach graficznych w widoku śledzenia pod kątem wszelkich operacji, które są wykonywane sekwencyjnie. Zwykle dzieje się tak, gdy twój program zawiera zależności sterujące z jednego GPU do drugiego. W przeszłości debugowanie wydajności w tej sytuacji było rozwiązywane indywidualnie dla każdego przypadku. Jeśli zaobserwujesz to zachowanie w swoim programie, zgłoś problem z usługą GitHub z obrazami widoku śledzenia.

1. Optymalizuj gradient AllReduce

Podczas treningu ze strategią synchroniczną każde urządzenie otrzymuje część danych wejściowych.

Po obliczeniu przejść do przodu i do tyłu przez model, gradienty obliczone na każdym urządzeniu muszą zostać zagregowane i zredukowane. Ten gradient AllReduce ma miejsce po obliczeniu gradientu na każdym urządzeniu i zanim optymalizator zaktualizuje wagi modelu.

Każdy procesor GPU najpierw łączy gradienty w warstwach modelu, przekazuje je między procesorami GPU przy użyciu funkcji tf.distribute.CrossDeviceOps (domyślnie jest to tf.distribute.NcclAllReduce ), a następnie zwraca gradienty po redukcji na warstwę.

Optymalizator użyje tych zredukowanych gradientów do aktualizacji wag modelu. Idealnie, ten proces powinien odbywać się w tym samym czasie na wszystkich procesorach graficznych, aby zapobiec wszelkim obciążeniom.

Czas do AllReduce powinien być w przybliżeniu taki sam jak:

(number of parameters * 4bytes)/ (communication bandwidth)

To obliczenie jest przydatne jako szybkie sprawdzenie, czy wydajność podczas uruchamiania rozproszonego zadania szkoleniowego jest zgodna z oczekiwaniami, czy też konieczne jest dalsze debugowanie wydajności. Liczbę parametrów w modelu można uzyskać z Model.summary .

Zauważ, że każdy parametr modelu ma rozmiar 4 bajtów, ponieważ TensorFlow używa fp32 (float32) do komunikacji gradientów. Nawet jeśli masz włączony fp16 , NCCL AllReduce wykorzystuje parametry fp32 .

Aby uzyskać korzyści ze skalowania, czas kroku musi być znacznie wyższy w porównaniu z tymi kosztami ogólnymi. Jednym ze sposobów osiągnięcia tego jest użycie większego rozmiaru wsadu, ponieważ rozmiar wsadu wpływa na czas kroku, ale nie wpływa na narzut komunikacji.

2. Rywalizacja o wątki hosta GPU

Podczas uruchamiania wielu procesorów graficznych zadaniem procesora jest utrzymywanie wszystkich urządzeń zajętych poprzez wydajne uruchamianie jąder GPU na urządzeniach.

Jeśli jednak istnieje wiele niezależnych operacji, które procesor może zaplanować na jednym GPU, procesor może zdecydować się na użycie wielu wątków hosta, aby utrzymać zajęty jeden GPU, a następnie uruchomić jądra na innym GPU w niedeterministycznej kolejności . Może to spowodować przekrzywienie lub ujemne skalowanie, co może negatywnie wpłynąć na wydajność.

Poniższa przeglądarka śladów pokazuje narzuty, gdy CPU przestawia jądro GPU, uruchamia się nieefektywnie, ponieważ GPU1 jest bezczynny, a następnie uruchamia operacje po uruchomieniu GPU2 .

image

Widok śledzenia hosta pokazuje, że host uruchamia jądra na GPU2 przed uruchomieniem ich na GPU1 (zauważ, że poniższe tf_Compute* nie wskazują na wątki procesora).

image

Jeśli w widoku śledzenia programu występuje tego rodzaju rozłożenie jąder GPU, zalecanym działaniem jest:

  • Ustaw zmienną środowiskową TF_GPU_THREAD_MODE na gpu_private . Ta zmienna środowiskowa poinformuje hosta o zachowaniu prywatności wątków dla GPU.
  • Domyślnie TF_GPU_THREAD_MODE=gpu_private ustawia liczbę wątków na 2, co w większości przypadków jest wystarczające. Jednak tę liczbę można zmienić, ustawiając zmienną środowiskową TF_GPU_THREAD_COUNT na żądaną liczbę wątków.