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 .
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).
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.
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
.
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.
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.
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
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
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
- przewodnik po wydajności tf.data dotyczący pisania potoków wejściowych wydajności
tf.data
- Film dotyczący TensorFlow: najlepsze praktyki
tf.data
- Przewodnik po profilerze
- Samouczek dotyczący profilera w Colab