Ta strona została przetłumaczona przez Cloud Translation API.
Switch to English

Analizuj wydajność tf.data za pomocą TF Profiler

Przegląd

W tym przewodniku założono, że znasz TensorFlow Profiler i tf.data . Ma na celu dostarczenie instrukcji krok po kroku z przykładami, aby pomóc użytkownikom zdiagnozować i naprawić problemy z wydajnością potoku wejściowego.

Aby rozpocząć, zbierz profil swojej pracy w TensorFlow. Instrukcje, jak to zrobić, są dostępne dla procesorów / GPU i Cloud TPU .

TensorFlow Trace Viewer

Opisany poniżej przepływ pracy analizy koncentruje się na narzędziu przeglądarki śladów w Profiler. To narzędzie wyświetla oś czasu, która pokazuje czas trwania operacji wykonywanych przez program TensorFlow i pozwala określić, które operacje są wykonywane najdłużej. Aby uzyskać więcej informacji na temat przeglądarki danych śledzenia, zapoznaj się z tą sekcją przewodnika TF Profiler. Ogólnie zdarzenia tf.data pojawią się na osi czasu procesora hosta.

Analiza przepływu pracy

Postępuj zgodnie z poniższą procedurą. Jeśli masz opinię, która pomoże nam go ulepszyć, utwórz zgłoszenie na githubie z etykietą „comp: data”.

1. Czy Twój potok tf.data generuje dane wystarczająco szybko?

Rozpocznij od ustalenia, czy potok wejściowy jest wąskim gardłem dla programu TensorFlow.

Aby to zrobić, poszukaj IteratorGetNext::DoCompute w przeglądarce śledzenia. Ogólnie rzecz biorąc, spodziewasz się zobaczyć je na początku kroku. Te wycinki reprezentują czas potrzebny potokowi wejściowemu do uzyskania partii elementów, gdy jest on żądany. Jeśli używasz keras lub iterujesz po swoim tf.function danych w tf.function , powinny one znajdować się w wątkach tf_data_iterator_get_next .

Zwróć uwagę, że jeśli używasz strategii dystrybucji , możesz zobaczyć zdarzenia IteratorGetNextAsOptional::DoCompute zamiast IteratorGetNext::DoCompute (od TF 2.3).

image

Jeśli połączenia wracają szybko (<= 50 us), oznacza to, że Twoje dane są dostępne, gdy są wymagane. Potok wejściowy nie jest Twoim wąskim gardłem; Zobacz przewodnik Profiler, aby uzyskać bardziej ogólne wskazówki dotyczące analizy wydajności.

image

Jeśli połączenia wracają powoli, tf.data nie jest w stanie nadążyć za żądaniami konsumenta. Przejdź do następnej sekcji.

2. Czy pobierasz dane z wyprzedzeniem?

Najlepszą praktyką w zakresie wydajności potoku wejściowego jest wstawienie transformacji tf.data.Dataset.prefetch na końcu potoku tf.data . Ta transformacja nakłada się na obliczenia przetwarzania wstępnego potoku wejściowego z następnym krokiem obliczania modelu i jest wymagana do uzyskania optymalnej wydajności potoku wejściowego podczas szkolenia modelu. Jeśli IteratorGetNext::DoCompute , powinieneś zobaczyć wycinek Iterator::Prefetch w tym samym wątku, co IteratorGetNext::DoCompute .

image

Jeśli nie masz prefetch na końcu potoku , powinieneś go dodać. Więcej informacji na temat tf.data dotyczących wydajności tf.data można znaleźć w przewodniku po wydajności tf.data .

Jeśli już pobierasz dane , a potok wejściowy nadal stanowi wąskie gardło, przejdź do następnej sekcji, aby dokładniej przeanalizować wydajność.

3. Czy osiągasz wysokie wykorzystanie procesora?

tf.data osiąga dużą przepustowość, starając się jak najlepiej wykorzystać dostępne zasoby. Ogólnie rzecz biorąc, nawet podczas uruchamiania modelu na akceleratorze, takim jak GPU lub TPU, potoki tf.data są uruchamiane na procesorze. Możesz sprawdzić swoje wykorzystanie za pomocą narzędzi takich jak sar i htop lub w konsoli monitorowania w chmurze, jeśli korzystasz z GCP.

Jeśli wykorzystanie jest niskie, sugeruje to, że potok wejściowy może nie w pełni wykorzystywać procesora hosta. W celu uzyskania najlepszych praktyk należy zapoznać się z przewodnikiem dotyczącym wydajności tf.data . Jeśli zastosowałeś najlepsze praktyki, a wykorzystanie i przepustowość pozostają na niskim poziomie, przejdź do poniższej analizy wąskich gardeł .

Jeśli wykorzystanie zbliża się do limitu zasobów , w celu dalszej poprawy wydajności należy poprawić wydajność potoku wejściowego (na przykład unikając niepotrzebnych obliczeń) lub odciążyć obliczenia.

Możesz poprawić wydajność potoku wejściowego, unikając niepotrzebnych obliczeń w tf.data . Jednym ze sposobów jest wstawienie transformacji tf.data.Dataset.cache po intensywnej pracy obliczeniowej, jeśli dane tf.data.Dataset.cache w pamięci; zmniejsza to ilość obliczeń kosztem zwiększonego zużycia pamięci. Ponadto wyłączenie równoległości tf.data w tf.data może zwiększyć wydajność o> 10% i można to zrobić, ustawiając następującą opcję w potoku wejściowym:

dataset = ...
options = tf.data.Options()
options.experimental_threading.max_intra_op_parallelism = 1
dataset = dataset.with_options(options)

4. Analiza wąskich gardeł

W poniższej sekcji przedstawiono sposób odczytywania zdarzeń tf.data w przeglądarce śledzenia, aby zrozumieć, gdzie znajduje się wąskie gardło i jakie są możliwe strategie łagodzenia skutków.

Zrozumienie zdarzeń tf.data w Profiler

Każde zdarzenie tf.data w programie Profiler ma nazwę Iterator::<Dataset> , gdzie <Dataset> to nazwa źródła lub transformacji zestawu danych. Każde zdarzenie ma również długą nazwę Iterator::<Dataset_1>::...::<Dataset_n> , którą można zobaczyć, klikając zdarzenie tf.data . W długiej nazwie <Dataset_n> pasuje do <Dataset> z (krótkiej) nazwy, a inne zestawy danych w długiej nazwie reprezentują dalsze transformacje.

image

Na przykład powyższy zrzut ekranu został wygenerowany z następującego kodu:

dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x)
dataset = dataset.repeat(2)
dataset = dataset.batch(5)

Tutaj zdarzenie Iterator::Map ma długą nazwę Iterator::BatchV2::FiniteRepeat::Map . Zwróć uwagę, że nazwa zestawu danych może nieznacznie różnić się od interfejsu API języka Python (na przykład FiniteRepeat zamiast Repeat), ale powinna być wystarczająco intuicyjna, aby przeprowadzić analizę.

Transformacje synchroniczne i asynchroniczne

W przypadku synchronicznych przekształceń tf.data (takich jak Batch i Map ) zdarzenia z transformacji nadrzędnych będą widoczne w tym samym wątku. W powyższym przykładzie, ponieważ wszystkie użyte transformacje są synchroniczne, wszystkie zdarzenia pojawiają się w tym samym wątku.

W przypadku transformacji asynchronicznych (takich jak Prefetch , ParallelMap , ParallelInterleave i MapAndBatch ) zdarzenia z transformacji nadrzędnych będą znajdować się w innym wątku. W takich przypadkach „długa nazwa” może pomóc w zidentyfikowaniu, której transformacji w potoku odpowiada zdarzenie.

image

Na przykład powyższy zrzut ekranu został wygenerowany z następującego kodu:

dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x)
dataset = dataset.repeat(2)
dataset = dataset.batch(5)
dataset = dataset.prefetch(1)

Tutaj zdarzenia Iterator::Prefetch znajdują się w wątkach tf_data_iterator_get_next . Ponieważ Prefetch jest asynchroniczne, jego zdarzenia wejściowe ( BatchV2 ) będą znajdować się w innym wątku i można je zlokalizować, wyszukując długą nazwę Iterator::Prefetch::BatchV2 . W tym przypadku znajdują się one w wątku tf_data_iterator_resource . Ze swojej długiej nazwie, można wywnioskować, że BatchV2 jest przed Prefetch . Ponadto parent_id BatchV2 zdarzenia BatchV2 będzie zgodny z identyfikatorem zdarzenia Prefetch .

Identyfikacja wąskiego gardła

Ogólnie rzecz biorąc, aby zidentyfikować wąskie gardło w potoku wejściowym, przejdź potokiem wejściowym od najbardziej zewnętrznej transformacji do źródła. Rozpoczynając od ostatecznej transformacji w potoku, powtarzaj transformacje w górę, aż znajdziesz powolną transformację lub dotrzesz do źródłowego zestawu danych, takiego jak TFRecord . W powyższym przykładzie należy rozpocząć od Prefetch , a następnie przejść w górę do BatchV2 , FiniteRepeat , Map , a na końcu Range .

Ogólnie rzecz biorąc, powolna transformacja odpowiada tej, której zdarzenia są długie, ale zdarzenia wejściowe są krótkie. Poniżej przedstawiono kilka przykładów.

Należy zauważyć, że ostatnią (najbardziej zewnętrzną) transformacją w większości potoków wejściowych hosta jest zdarzenie Iterator::Model . Transformacja modelu jest wprowadzana automatycznie przez środowisko uruchomieniowe tf.data i służy do instrumentowania i automatycznego tf.data wydajności potoku wejściowego.

Jeśli zadanie korzysta ze strategii dystrybucji , przeglądarka śledzenia będzie zawierać dodatkowe zdarzenia, które odpowiadają potokowi wejściowemu urządzenia. Najbardziej zewnętrzna transformacja potoku urządzenia (zagnieżdżona w IteratorGetNextOp::DoCompute lub IteratorGetNextAsOptionalOp::DoCompute ) będzie zdarzeniem Iterator::Prefetch ze zdarzeniem Iterator::Generator . Odpowiedni potok hosta można znaleźć, wyszukując zdarzenia Iterator::Model .

Przykład 1

image

Powyższy zrzut ekranu jest generowany z następującego potoku wejściowego:

dataset = tf.data.TFRecordDataset(filename)
dataset = dataset.map(parse_record)
dataset = dataset.batch(32)
dataset = dataset.repeat()

Na zrzucie ekranu zauważ, że (1) zdarzenia Iterator::Map są długie, ale (2) zdarzenia wejściowe ( Iterator::FlatMap ) wracają szybko. Sugeruje to, że wąskim gardłem jest sekwencyjna transformacja mapy.

Zwróć uwagę, że na zrzucie ekranu zdarzenie InstantiatedCapturedFunction::Run odpowiada czasowi potrzebnemu na wykonanie funkcji mapy.

Przykład 2

image

Powyższy zrzut ekranu jest generowany z następującego potoku wejściowego:

dataset = tf.data.TFRecordDataset(filename)
dataset = dataset.map(parse_record, num_parallel_calls=2)
dataset = dataset.batch(32)
dataset = dataset.repeat()

Ten przykład jest podobny do powyższego, ale używa ParallelMap zamiast Map. Zauważamy tutaj, że (1) zdarzenia Iterator::ParallelMap są długie, ale (2) zdarzenia wejściowe Iterator::FlatMap (które znajdują się w innym wątku, ponieważ ParallelMap jest asynchroniczna) są krótkie. Sugeruje to, że transformacja ParallelMap jest wąskim gardłem.

Rozwiązanie problemu wąskiego gardła

Źródłowe zbiory danych

Jeśli zidentyfikujesz źródło zestawu danych jako wąskie gardło, na przykład odczyt z plików TFRecord, możesz poprawić wydajność, równolegle wyodrębniając dane. Aby to zrobić, upewnij się, że dane są podzielone na wiele plików i użyj tf.data.Dataset.interleave z parametrem num_parallel_calls ustawionym na tf.data.experimental.AUTOTUNE . Jeśli determinizm nie jest ważny dla twojego programu, możesz dalej poprawić wydajność, ustawiając flagę deterministic=False na tf.data.Dataset.interleave od TF 2.2. Na przykład, jeśli czytasz z TFRecords, możesz wykonać następujące czynności:

dataset = tf.data.Dataset.from_tensor_slices(filenames)
dataset = dataset.interleave(tf.data.TFRecordDataset,
  num_parallel_calls=tf.data.experimental.AUTOTUNE,
  deterministic=False)

Zwróć uwagę, że pliki podzielone na fragmenty powinny być wystarczająco duże, aby zamortyzować narzut związany z otwarciem pliku. Więcej informacji na temat równoległego wyodrębniania danych można znaleźć w tej sekcji przewodnika dotyczącego wydajności tf.data .

Zbiory danych transformacji

Jeśli zidentyfikowałeś pośrednią transformację tf.data jako wąskie gardło, możesz rozwiązać ten problem przez zrównoleglenie transformacji lub buforowanie obliczeń, jeśli dane mieszczą się w pamięci i jest to właściwe. Niektóre transformacje, takie jak Map mają równoległe odpowiedniki; tf.data wydajności tf.data pokazuje, jak zrównoleglać te. Inne transformacje, takie jak Filter , Unbatch i Batch są z natury sekwencyjne; można je zrównoleglać, wprowadzając „równoległość zewnętrzną”. Na przykład załóżmy, że Twój potok wejściowy początkowo wygląda następująco, z wąskim gardłem Batch :

filenames = tf.data.Dataset.list_files(file_path, shuffle=is_training)
dataset = filenames_to_dataset(filenames)
dataset = dataset.batch(batch_size)

Możesz wprowadzić „równoległość zewnętrzną”, uruchamiając wiele kopii potoku wejściowego w danych wejściowych podzielonych na fragmenty i łącząc wyniki:

filenames = tf.data.Dataset.list_files(file_path, shuffle=is_training)

def make_dataset(shard_index):
  filenames = filenames.shard(NUM_SHARDS, shard_index)
  dataset = filenames_to_dataset(filenames)
  Return dataset.batch(batch_size)

indices = tf.data.Dataset.range(NUM_SHARDS)
dataset = indices.interleave(make_dataset,
                             num_parallel_calls=tf.data.experimental.AUTOTUNE)
dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)

Dodatkowe zasoby