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

Przegląd

W tym przewodniku założono znajomość TensorFlow Profiler i tf.data . Ma na celu dostarczenie instrukcji krok po kroku z przykładami, które pomogą użytkownikom diagnozować i naprawiać problemy z wydajnością potoku wejściowego.

Na początek zbierz profil swojej pracy w TensorFlow. Instrukcje, jak to zrobić, są dostępne dla procesorów/procesorów graficznych i jednostek Cloud TPU .

TensorFlow Trace Viewer

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

Przebieg analizy

Postępuj zgodnie z poniższą procedurą. Jeśli masz uwagi, które pomogą nam je ulepszyć, utwórz problem na Githubie z etykietą „comp:data”.

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

Rozpocznij od sprawdzenia, czy potok wejściowy stanowi wąskie gardło dla programu TensorFlow.

Aby to zrobić, poszukaj IteratorGetNext::DoCompute w przeglądarce śledzenia. Ogólnie rzecz biorąc, spodziewasz się je zobaczyć na początku kroku. Te wycinki reprezentują czas potrzebny, aby potok wejściowy dostarczył partię elementów, gdy jest to wymagane. Jeśli używasz keras lub iterujesz po zbiorze danych w tf.function , należy je znaleźć w wątkach tf_data_iterator_get_next .

Pamiętaj, ż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 nas), oznacza to, że Twoje dane są dostępne, gdy są wymagane. Rurociąg wejściowy nie jest wąskim gardłem; zobacz przewodnik po Profilerze , aby uzyskać bardziej ogólne wskazówki dotyczące analizy wydajności.

image

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

2. Czy pobierasz dane z wyprzedzeniem?

Najlepszą praktyką dotyczącą 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 wstępnego przetwarzania potoku wejściowego z następnym krokiem obliczeń modelu i jest wymagana w celu zapewnienia optymalnej wydajności potoku wejściowego podczas uczenia modelu. Jeśli pobierasz dane z wyprzedzeniem, powinieneś zobaczyć wycinek Iterator::Prefetch w tym samym wątku, co operacja IteratorGetNext::DoCompute .

image

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

Jeśli już pobierasz dane z wyprzedzeniem , 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 wysoką przepustowość starając się jak najlepiej wykorzystać dostępne zasoby. Ogólnie rzecz biorąc, nawet jeśli model działa 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 chmury , jeśli korzystasz z GCP.

Jeśli wykorzystanie jest niskie, sugeruje to, że potok wejściowy może nie w pełni wykorzystywać procesor hosta. Najlepsze praktyki znajdziesz w przewodniku po wydajności tf.data . Jeśli zastosowałeś najlepsze praktyki, a wykorzystanie i przepustowość pozostają niskie, przejdź do poniższej analizy wąskiego gardła .

Jeśli wykorzystanie zbliża się do limitu zasobów , aby jeszcze bardziej poprawić wydajność, musisz albo poprawić wydajność potoku wejściowego (na przykład unikając niepotrzebnych obliczeń), albo odciążyć obliczenia.

Możesz poprawić wydajność potoku wejściowego, unikając niepotrzebnych obliczeń w tf.data . Jednym ze sposobów osiągnięcia tego jest wstawienie transformacji tf.data.Dataset.cache po pracy wymagającej intensywnych obliczeń, jeśli dane mieszczą się w pamięci; zmniejsza to obliczenia kosztem zwiększonego zużycia pamięci. Ponadto wyłączenie równoległości wewnątrz operacji 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ąskiego gardła

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

Zrozumienie zdarzeń tf.data w Profilerze

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

image

Przykładowo 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)

W tym przypadku zdarzenie Iterator::Map ma długą nazwę Iterator::BatchV2::FiniteRepeat::Map . Należy pamiętać, że nazwa zbioru danych może nieznacznie różnić się od nazwy API Pythona (na przykład FiniteRepeat zamiast Powtórz), ale powinna być wystarczająco intuicyjna, aby można ją było przeanalizować.

Transformacje synchroniczne i asynchroniczne

W przypadku synchronicznych transformacji tf.data (takich jak Batch i Map ) zobaczysz zdarzenia z wcześniejszych transformacji 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 wcześniejszych transformacji będą znajdować się w innym wątku. W takich przypadkach „długa nazwa” może pomóc w zidentyfikowaniu transformacji w potoku, której odpowiada zdarzenie.

image

Przykładowo 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 asynchroniczny, 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 . Z jego długiej nazwy można wywnioskować, że BatchV2 znajduje się przed Prefetch . Ponadto parent_id 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, należy przejść potokiem wejściowym od najbardziej zewnętrznej transformacji aż do źródła. Zaczynając od końcowej transformacji w potoku, powracaj do wcześniejszych transformacji, aż znajdziesz powolną transformację lub dotrzesz do źródłowego zestawu danych, takiego jak TFRecord . W powyższym przykładzie zaczniesz od Prefetch , następnie przejdziesz w górę do BatchV2 , FiniteRepeat , Map i na koniec Range .

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

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

Jeśli Twoje zadanie korzysta ze strategii dystrybucji , przeglądarka śledzenia będzie zawierać dodatkowe zdarzenia odpowiadające 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 nadrzędnym 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 ) szybko powracają. Sugeruje to, że wąskim gardłem jest sekwencyjna transformacja mapy.

Należy zauważyć, ż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 asynchroniczny) są krótkie. Sugeruje to, że wąskim gardłem jest transformacja ParallelMap.

Rozwiązanie problemu wąskiego gardła

Źródłowe zbiory danych

Jeśli zidentyfikowałeś źródło zestawu danych jako wąskie gardło, takie jak odczytywanie z plików TFRecord, możesz poprawić wydajność poprzez równoległe wyodrębnianie danych. 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.AUTOTUNE . Jeśli determinizm nie jest ważny dla Twojego programu, możesz jeszcze bardziej poprawić wydajność, ustawiając flagę deterministic=False w 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.AUTOTUNE,
  deterministic=False)

Należy pamiętać, że pliki podzielone na fragmenty powinny być dość duże, aby zamortyzować koszty związane z otwieraniem pliku. Aby uzyskać więcej informacji na temat równoległej ekstrakcji danych, zobacz tę sekcję przewodnika po wydajności tf.data .

Zbiory danych transformacji

Jeśli uznasz, że wąskim gardłem jest pośrednia transformacja tf.data , możesz rozwiązać ten problem poprzez 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; przewodnik po wydajności tf.data pokazuje, jak je zrównoleglić. Inne przekształcenia, takie jak Filter , Unbatch i Batch są z natury sekwencyjne; możesz je zrównać, wprowadzając „równoległość zewnętrzną”. Załóżmy na przykład, że potok wejściowy początkowo wygląda następująco, a wąskim gardłem jest 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ć „zewnętrzną równoległość”, uruchamiając wiele kopii potoku wejściowego na 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.AUTOTUNE)
dataset = dataset.prefetch(tf.data.AUTOTUNE)

Dodatkowe zasoby