Zoptymalizuj wydajność procesora graficznego TensorFlow za pomocą narzędzia TensorFlow Profiler

Przegląd

W tym przewodniku dowiesz się, jak używać TensorFlow Profiler z TensorBoard, aby uzyskać wgląd w procesory graficzne i uzyskać maksymalną wydajność, a także debugować, gdy jeden lub więcej procesorów graficznych jest niedostatecznie wykorzystywanych.

Jeśli jesteś nowy w Profilerze:

Należy pamiętać, że przeniesienie obliczeń na procesor graficzny nie zawsze jest korzystne, szczególnie w przypadku małych modeli. Narzuty mogą wystąpić z powodu:

  • Przesyłanie danych pomiędzy hostem (CPU) a urządzeniem (GPU); I
  • Ze względu na opóźnienie występujące podczas uruchamiania jąder GPU przez hosta.

Przepływ pracy związany z optymalizacją wydajności

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

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

  1. Optymalizuj i debuguj wydajność na jednym procesorze graficznym:
    1. Sprawdź, czy potok wejściowy nie stanowi wąskiego gardła.
    2. Debuguj wydajność jednego procesora graficznego.
    3. Włącz precyzję mieszaną (z fp16 (float16)) i opcjonalnie włącz XLA .
  2. Optymalizuj i debuguj wydajność na pojedynczym hoście z wieloma procesorami graficznymi.

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

Jako podstawę do 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 . Pisząc niestandardową pętlę treningową za pomocą tf.GradientTape , zapoznaj się z sekcją Lepsza wydajność dzięki funkcji tf. , aby dowiedzieć się, jak włączyć tf.function .

W następnych sekcjach omówiono sugerowane podejścia do każdego z powyższych scenariuszy, aby pomóc w identyfikowaniu i naprawianiu wąskich gardeł wydajności.

1. Zoptymalizuj wydajność na jednym GPU

W idealnym przypadku program powinien charakteryzować się wysokim wykorzystaniem procesora graficznego, minimalną komunikacją procesora (hosta) z procesorem graficznym (urządzeniem) i brakiem narzutu z potoku wejściowego.

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

Strona przeglądu Profilera TensorBoard — która pokazuje widok najwyższego poziomu działania modelu 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. Jaka część czasu kroku pochodzi od faktycznego wykonania urządzenia
  2. Procent 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 zapoznać się z przeglądarką śledzenia Profiler TensorBoard. W poniższych sekcjach przedstawiono niektóre typowe wzorce przeglądarki śledzenia, na które należy zwrócić uwagę podczas diagnozowania wąskich gardeł wydajności.

Poniżej znajduje się obraz widoku śledzenia modelu działającego na jednym procesorze graficznym. 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 przejścia/gradientu do tyłu i aktualizacja wagi optymalizatora. Możesz także uruchomić operacje na GPU obok każdego Stream , które odnoszą się do strumieni CUDA. Każdy strumień służy do określonych zadań. W tym śladzie strumień nr 118 jest używany do uruchamiania jąder obliczeniowych i kopii z urządzenia na urządzenie. Strumień nr 119 jest używany do kopiowania z hosta na urządzenie, a strumień nr 120 do kopiowania z urządzenia na host.

Poniższy wykres 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 między hostami ( strumień nr 119 ) i między urządzeniami ( strumień nr 120 ), a także minimalne przerwy między krokami. Po uruchomieniu Profilera dla swojego programu możesz nie być w stanie zidentyfikować tych idealnych cech w widoku śledzenia. W pozostałej części tego przewodnika opisano typowe scenariusze i sposoby ich naprawienia.

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 zrozumieć, jest użycie analizatora potoku wejściowego 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 wejściowy znacząco wpływa na czas kroku:

  • Możesz skorzystać z przewodnika specyficznego dla tf.data , aby dowiedzieć się, jak debugować potok wejściowy.
  • Innym szybkim sposobem sprawdzenia, czy potok wejściowy jest wąskim gardłem, jest użycie losowo wygenerowanych danych wejściowych, które nie wymagają żadnego wstępnego przetwarzania. Oto przykład zastosowania tej techniki w modelu ResNet. Jeśli potok wejściowy jest optymalny, powinieneś doświadczyć podobnej wydajności w przypadku danych rzeczywistych i wygenerowanych danych losowych/syntetycznych. Jedyny narzut w przypadku danych syntetycznych będzie wynikał z kopiowania danych wejściowych, które ponownie można pobrać z wyprzedzeniem i zoptymalizować.

Ponadto zapoznaj się z najlepszymi praktykami optymalizacji potoku danych wejściowych .

2. Debuguj wydajność jednego procesora graficznego

Istnieje kilka czynników, które mogą przyczynić się do niskiego wykorzystania procesora graficznego. Poniżej znajdują się niektóre scenariusze często obserwowane podczas przeglądania przeglądarki śledzenia i potencjalnych rozwiązań.

1. Przeanalizuj przerwy pomiędzy krokami

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

image

Jeśli przeglądarka śledzenia pokazuje duże przerwy między krokami, może to oznaczać, że Twój program ma ograniczenia wejściowe. W takim przypadku powinieneś zapoznać się z poprzednią sekcją dotyczącą debugowania potoku wejściowego, jeśli jeszcze tego nie zrobiłeś.

Jednak nawet w przypadku zoptymalizowanego potoku wejściowego nadal mogą występować przerwy między końcem jednego kroku a początkiem drugiego z powodu rywalizacji wątków procesora. tf.data korzysta z wątków w tle w celu zrównoleglenia przetwarzania potokowego. Wątki te mogą zakłócać działania po stronie hosta GPU, które mają miejsce na początku każdego kroku, takie jak kopiowanie danych lub planowanie operacji GPU.

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

Przerwy między krokami mogą być również spowodowane obliczeniami metryki, 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 i z powrotem z procesora graficznego.

Jeśli po optymalizacji potoku wejściowego nadal zauważasz przerwy między krokami w przeglądarce śledzenia, powinieneś przyjrzeć się kodowi modelu pomiędzy krokami i sprawdzić, czy wyłączenie wywołań zwrotnych/metryki poprawia wydajność. Niektóre szczegóły tych operacji są również dostępne w przeglądarce śledzenia (zarówno po stronie urządzenia, jak i hosta). W tym scenariuszu zaleca się amortyzowanie kosztów ogólnych tych operacji poprzez wykonywanie ich po ustalonej liczbie kroków, a nie po każdym kroku. W przypadku korzystania z metody Model.compile w interfejsie API tf.keras ustawienie flagi steps_per_execution powoduje to automatycznie. W przypadku niestandardowych pętli treningowych użyj tf.while_loop .

2. Osiągnij większe wykorzystanie urządzenia

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

Host kolejkuje jądra do uruchomienia na GPU, ale zanim jądra zostaną faktycznie wykonane na GPU, występuje opóźnienie (około 20-40 μs). W idealnym przypadku host kolejkuje wystarczającą liczbę jąder na procesorze graficznym, tak że procesor graficzny spędza większość czasu na wykonywaniu zadań, zamiast czekać, aż host umieści w kolejce więcej jąder.

Strona przeglądu Profilera na TensorBoard pokazuje, ile czasu procesor graficzny był bezczynny z powodu oczekiwania na uruchomienie jądra przez hosta. Na poniższym obrazku procesor graficzny jest bezczynny przez około 10% czasu kroku w oczekiwaniu na uruchomienie jądra.

image

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

image

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

image

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

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

Strona przeglądu Profilera pokazuje procent operacji umieszczonych na hoście w porównaniu z urządzeniem (możesz także zweryfikować rozmieszczenie określonych operacji, przeglądając przeglądarkę śledzenia . Podobnie jak na obrazku poniżej, chcesz określić 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ń przypisane są operacje i tensory w Twoim modelu, ustaw tf.debugging.set_log_device_placement(True) jako pierwszą instrukcję programu.

Zauważ, że w niektórych przypadkach, nawet jeśli określisz op do umieszczenia na konkretnym urządzeniu, jej implementacja może pominąć ten warunek (przykład: tf.unique ). Nawet w przypadku szkolenia z jednym procesorem graficznym określenie strategii dystrybucji, takiej jak tf.distribute.OneDeviceStrategy , może skutkować bardziej deterministycznym rozmieszczeniem operacji na urządzeniu.

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

image

Kopie te 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 będących źródłem skopiowanych tensorów, ale powiązanie memCopy z operacją może nie zawsze być łatwe. W takich przypadkach warto przyjrzeć się pobliskim operacjom, aby sprawdzić, czy kopia pamięci odbywa się w tym samym miejscu na każdym kroku.

3. Bardziej wydajne jądra na procesorach graficznych

Gdy wykorzystanie procesora graficznego przez program będzie akceptowalne, następnym krokiem będzie rozważenie zwiększenia wydajności jąder procesora graficznego poprzez wykorzystanie rdzeni Tensor lub operacji łączenia.

1. Wykorzystaj rdzenie tensorowe

Nowoczesne procesory graficzne NVIDIA® posiadają wyspecjalizowane rdzenie Tensor , które mogą znacznie poprawić wydajność kwalifikujących się jąder.

Możesz użyć statystyk jądra GPU TensorBoard, aby zwizualizować, które jądra GPU kwalifikują się do Tensor Core, a które jądra korzystają z rdzeni Tensor. Włączenie fp16 (patrz sekcja Włączanie mieszanej precyzji poniżej) jest jednym ze sposobów, aby jądra General Matrix Multiply (GEMM) (ops Matmul) w programie wykorzystywały rdzeń Tensor. Jądra GPU wydajnie wykorzystują rdzenie Tensor, gdy precyzja wynosi fp16, a wymiary tensora wejścia/wyjścia są podzielne przez 8 lub 16 (dla int8 ).

Inne szczegółowe zalecenia dotyczące zwiększania wydajności jąder dla procesorów graficznych można znaleźć w przewodniku po wydajności głębokiego uczenia się NVIDIA® .

2. Bezpieczniki

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

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

Po wykonaniu powyższych kroków włączenie mieszanej precyzji i XLA to dwa opcjonalne kroki, które możesz podjąć, aby jeszcze bardziej poprawić wydajność. Sugerowane podejście polega na włączaniu ich pojedynczo i sprawdzaniu, czy korzyści w zakresie wydajności są zgodne z oczekiwaniami.

1. Włącz precyzję mieszaną

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

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

Należy pamiętać, że w wersji cuDNN 7.6.3 i nowszych wymiary splotu zostaną automatycznie uzupełnione, jeśli jest to konieczne, aby wykorzystać rdzenie Tensor.

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

1. Użyj optymalnych jąder fp16

Po włączeniu fp16 , jądra mnożenia macierzy (GEMM) twojego programu powinny używać odpowiedniej wersji fp16 , która wykorzystuje rdzenie Tensor. Jednak w niektórych przypadkach tak się nie dzieje i nie odczuwasz 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 korzystają z wydajnego Tensor Core. Przewodnik NVIDIA® na temat wydajności głębokiego uczenia się zawiera dodatkowe sugestie dotyczące wykorzystania rdzeni Tensor. Dodatkowo korzyści z używania fp16 będą widoczne również w jądrach, które wcześniej były powiązane z pamięcią, ponieważ teraz operacje będą zajmowały o połowę mniej czasu.

2. Skalowanie strat dynamicznych i statycznych

Skalowanie strat jest konieczne, gdy używasz fp16 , aby zapobiec niedomiarowi z powodu niskiej precyzji. Istnieją dwa typy skalowania strat, dynamiczne i statyczne, oba wyjaśniono bardziej szczegółowo w przewodniku Mixed Precision . Możesz użyć polityki 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 uruchamiane na hoście i prowadzić do przerw, które będą widoczne pomiędzy krokami w przeglądarce śledzenia. Z drugiej strony skalowanie strat statycznych nie wiąże się z takimi narzutami i może być lepszą opcją pod względem wydajności z haczykiem polegającym na określeniu prawidłowej wartości skali strat statycznych.

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

Ostatnim krokiem w uzyskaniu najlepszej wydajności przy użyciu pojedynczego procesora graficznego jest poeksperymentowanie z włączeniem XLA, co połączy operacje i doprowadzi do lepszego wykorzystania urządzenia i mniejszego zużycia pamięci. Szczegółowe informacje na temat włączania XLA w programie za pomocą tf.function(jit_compile=True) lub automatycznego klastrowania można znaleźć w przewodniku XLA .

Możesz ustawić globalny poziom JIT na -1 (wyłączony), 1 lub 2 . Wyższy poziom jest bardziej agresywny i może zmniejszyć równoległość i zużywać więcej pamięci. Ustaw wartość na 1 jeśli masz ograniczenia dotyczące pamięci. Należy zauważyć, ż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

Interfejsu API tf.distribute.MirroredStrategy można używać do skalowania uczenia modelu z jednego procesora graficznego na wiele procesorów graficznych na jednym hoście. (Aby dowiedzieć się więcej na temat prowadzenia szkolenia rozproszonego za pomocą TensorFlow, zapoznaj się z przewodnikami Szkolenie rozproszone z TensorFlow , Korzystanie z procesora graficznego i Korzystanie z TPU oraz samouczek Szkolenie rozproszone z Keras .)

Chociaż w idealnym przypadku przejście z jednego procesora graficznego na wiele procesorów graficznych powinno być skalowalne od razu po wyjęciu z pudełka, czasami można napotkać problemy z wydajnością.

W przypadku przejścia z treningu z jednym procesorem graficznym na wiele procesorów graficznych na tym samym hoście najlepiej byłoby doświadczyć skalowania wydajności jedynie przy dodatkowym obciążeniu związanym z komunikacją gradientową i zwiększonym wykorzystaniu wątku hosta. Z powodu tego obciążenia nie uzyskasz dokładnego dwukrotnego przyspieszenia, jeśli na przykład przejdziesz z 1 na 2 procesory graficzne.

Poniższy widok śledzenia przedstawia przykład dodatkowego obciążenia komunikacyjnego podczas uczenia na wielu procesorach graficznych. Łączenie gradientów, przekazywanie ich pomiędzy replikami i dzielenie ich przed wykonaniem aktualizacji wagi wiąże się z pewnymi kosztami ogólnymi.

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ądzenia i zamortyzuje koszty komunikacji pomiędzy wieloma procesorami graficznymi. Korzystanie z profilera pamięci pomaga zorientować się, jak blisko programu jest maksymalne wykorzystanie pamięci. Należy pamiętać, że chociaż większy rozmiar partii może wpływać na zbieżność, zwykle jest to równoważone korzyściami w zakresie wydajności.
  2. W przypadku przejścia 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 operacja AllReduce odbywa się za pośrednictwem jądra NCCL , a na każdym procesorze graficznym istnieje 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 graficzny (zwykle GPU0 ) będzie miał nadmierną subskrypcję, ponieważ host omyłkowo włoży na niego więcej pracy.
  6. Na koniec sprawdź etap uczenia na wszystkich procesorach graficznych w widoku śledzenia pod kątem operacji wykonywanych sekwencyjnie. Zwykle dzieje się tak, gdy program zawiera zależności sterujące między jednym procesorem graficznym a drugim. W przeszłości debugowanie wydajności w tej sytuacji było rozwiązywane indywidualnie dla każdego przypadku. Jeśli zaobserwujesz takie zachowanie w swoim programie, zgłoś problem w serwisie GitHub, przesyłając obrazy widoku śledzenia.

1. Zoptymalizuj 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 należy zagregować i zmniejszyć. Ten gradient AllReduce ma miejsce po obliczeniu gradientu na każdym urządzeniu i zanim optymalizator zaktualizuje wagi modelu.

Każdy procesor graficzny najpierw łączy gradienty między warstwami modelu, przekazuje je między procesorami graficznymi za pomocą tf.distribute.CrossDeviceOps ( tf.distribute.NcclAllReduce jest ustawieniem domyślnym), a następnie zwraca gradienty po redukcji dla każdej warstwy.

Optymalizator wykorzysta te zmniejszone gradienty do aktualizacji wag modelu. W idealnym przypadku proces ten powinien odbywać się w tym samym czasie na wszystkich procesorach graficznych, aby uniknąć jakichkolwiek kosztów ogólnych.

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, lub czy konieczne jest dalsze debugowanie wydajności. Liczbę parametrów w swoim modelu możesz uzyskać z Model.summary .

Należy zauważyć, że każdy parametr modelu ma rozmiar 4 bajty, ponieważ TensorFlow używa fp32 (float32) do przekazywania gradientów. Nawet jeśli masz włączone fp16 , NCCL AllReduce wykorzystuje parametry fp32 .

Aby uzyskać korzyści ze skalowania, czas kroku musi być znacznie dłuższy w porównaniu z tymi narzutami. Jednym ze sposobów osiągnięcia tego jest użycie większej wielkości partii, ponieważ wielkość partii wpływa na czas kroku, ale nie ma wpływu na obciążenie komunikacyjne.

2. Konflikt w wątku hosta GPU

Podczas pracy wielu procesorów graficznych zadaniem procesora jest zajęcie wszystkich urządzeń poprzez wydajne uruchamianie jąder procesora graficznego na urządzeniach.

Jednakże, gdy istnieje wiele niezależnych operacji, które procesor może zaplanować na jednym procesorze graficznym, procesor może zdecydować o wykorzystaniu wielu wątków hosta, aby jeden procesor graficzny był zajęty, a następnie uruchomić jądra na innym procesorze graficznym w niedeterministycznej kolejności . Może to spowodować zniekształcenie lub ujemne skalowanie, co może negatywnie wpłynąć na wydajność.

Poniższa przeglądarka śledzenia pokazuje obciążenie, gdy procesor zatrzymuje się, jądro GPU uruchamia się nieefektywnie, ponieważ GPU1 jest bezczynny, a następnie rozpoczyna wykonywanie operacji po uruchomieniu GPU2 .

image

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

image

Jeśli w widoku śledzenia programu zauważysz tego rodzaju rozmieszczenie jąder GPU, zalecaną czynnością jest:

  • Ustaw zmienną środowiskową TensorFlow TF_GPU_THREAD_MODE na gpu_private . Ta zmienna środowiskowa powie hostowi, aby utrzymywał prywatne wątki dla procesora graficznego.
  • 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że liczbę tę można zmienić, ustawiając zmienną środowiskową TensorFlow TF_GPU_THREAD_COUNT na żądaną liczbę wątków.