TA ODPOWIEDŹ : ma na celu przedstawienie szczegółowego opisu problemu na poziomie wykresu / sprzętu - w tym pętli pociągów TF2 vs. TF1, procesorów danych wejściowych oraz wykonywania w trybie Eager vs. Graph. Aby uzyskać podsumowanie problemu i wytyczne dotyczące rozwiązywania problemów, zobacz moją drugą odpowiedź.
OCENA WYDAJNOŚCI : czasami jedno jest szybsze, a czasem inne, w zależności od konfiguracji. Jeśli chodzi o TF2 vs TF1, są one na średnim poziomie, ale istnieją znaczące różnice w konfiguracji, a TF1 przebija TF2 częściej niż na odwrót. Zobacz „BENCHMARKING” poniżej.
EAGER VS. WYKRES : sedno całej tej odpowiedzi dla niektórych: według moich testów chętny TF2 jest wolniejszy niż TF1. Szczegóły poniżej.
Podstawowa różnica między nimi polega na tym, że: Graph tworzy sieć obliczeniową proaktywnie i wykonuje się, gdy „polecono” - podczas gdy Eager wykonuje wszystko po stworzeniu. Ale historia zaczyna się dopiero tutaj:
Chętny NIE jest pozbawiony Grafu i może w rzeczywistości być w większości Grafem, wbrew oczekiwaniom. W dużej mierze jest wykonywany Wykres - obejmuje to wagi modelu i optymalizatora, które stanowią dużą część wykresu.
Chętnie odbudowuje część własnego wykresu podczas wykonywania ; bezpośrednia konsekwencja nieukończenia programu Graph - patrz wyniki profilera. Ma to narzut obliczeniowy.
Chętny jest wolniejszy z wejściami Numpy ; zgodnie z tym komentarzem i kodem Gita dane wejściowe Numpy w programie Eager obejmują koszty ogólne kopiowania tensorów z procesora na procesor graficzny. Przechodząc przez kod źródłowy, różnice w przetwarzaniu danych są wyraźne; Chętnie przekazuje Numpy bezpośrednio, a Graph przekazuje tensory, które następnie oceniają na Numpy; niepewny dokładnego procesu, ale ten ostatni powinien obejmować optymalizacje na poziomie GPU
TF2 Eager jest wolniejszy niż TF1 Eager - to ... niespodziewane. Zobacz wyniki testów porównawczych poniżej. Różnice rozciągają się od nieznacznych do znaczących, ale są spójne. Nie jestem pewien, dlaczego tak jest - jeśli programista TF wyjaśni, zaktualizuje odpowiedź.
TF2 vs. TF1 : cytując odpowiednie części twórcy TF, Q. Scott Zhu, odpowiedź - w / trochę mojego nacisku i przeredagowania:
Chętnie środowisko wykonawcze musi wykonać operację i zwrócić wartość liczbową dla każdego wiersza kodu python. Charakter wykonywania w jednym kroku powoduje, że jest on powolny .
W TF2 Keras wykorzystuje funkcję tf. Do budowania swojego wykresu do treningu, oceny i prognozowania. Nazywamy je „funkcją wykonania” dla modelu. W TF1 „funkcją wykonania” był FuncGraph, który miał wspólny komponent jako funkcja TF, ale ma inną implementację.
Podczas tego procesu w jakiś sposób pozostawiliśmy niepoprawną implementację dla train_on_batch (), test_on_batch () i przewiduj_on_batch () . Nadal są poprawne numerycznie , ale funkcja wykonawcza dla x_on_batch jest czystą funkcją python, a nie pythonową funkcją tf.function. Spowoduje to spowolnienie
W TF2 konwertujemy wszystkie dane wejściowe na plik tf.data.Dataset, dzięki któremu możemy ujednolicić naszą funkcję wykonawczą do obsługi pojedynczego typu danych wejściowych. Konwersja zestawu danych może wiązać się z pewnym narzutem i myślę, że jest to jednorazowy narzut, a nie koszt pojedynczej partii
Z ostatnim zdaniem ostatniego akapitu powyżej i ostatnią klauzulą poniższego akapitu:
Aby przezwyciężyć spowolnienie w trybie chętnym, mamy funkcję @ tf.function, która zamieni funkcję python w wykres. Po wprowadzeniu wartości liczbowej, takiej jak np. Tablica, korpus funkcji tf. jest przekształcany na wykres statyczny, jest optymalizowany i zwraca wartość końcową, która jest szybka i powinna mieć podobną wydajność jak tryb wykresu TF1.
Nie zgadzam się - na podstawie moich wyników profilowania, które pokazują, że przetwarzanie danych wejściowych Eagera jest znacznie wolniejsze niż wykresu. Nie jestem pewien tf.data.Dataset
w szczególności, ale Eager wielokrotnie wywołuje wiele takich samych metod konwersji danych - patrz profiler.
Na koniec zatwierdzone przez dev zatwierdzenie: znaczna liczba zmian w celu obsługi pętli Keras v2 .
Pętle pociągowe : w zależności od (1) chętnych vs. wykresów; (2) w formacie danych wejściowych trenuje dokona odrębnego obiegu kolejowego - w TF2, _select_training_loop()
, training.py , jeden spośród:
training_v2.Loop()
training_distributed.DistributionMultiWorkerTrainingLoop(
training_v2.Loop()) # multi-worker mode
# Case 1: distribution strategy
training_distributed.DistributionMultiWorkerTrainingLoop(
training_distributed.DistributionSingleWorkerTrainingLoop())
# Case 2: generator-like. Input is Python generator, or Sequence object,
# or a non-distributed Dataset or iterator in eager execution.
training_generator.GeneratorOrSequenceTrainingLoop()
training_generator.EagerDatasetOrIteratorTrainingLoop()
# Case 3: Symbolic tensors or Numpy array-like. This includes Datasets and iterators
# in graph mode (since they generate symbolic tensors).
training_generator.GeneratorLikeTrainingLoop() # Eager
training_arrays.ArrayLikeTrainingLoop() # Graph
Każda z nich traktuje alokację zasobów inaczej i ma wpływ na wydajność i możliwości.
Pętle pociągowe: fit
vs train_on_batch
, keras
vstf.keras
.: każda z czterech używa różnych pętli pociągowych, choć być może nie w każdej możliwej kombinacji. keras
„ fit
na przykład używa formy fit_loop
np. training_arrays.fit_loop()
i train_on_batch
może z niej korzystać K.function()
. tf.keras
ma bardziej wyrafinowaną hierarchię opisaną częściowo w poprzedniej sekcji.
Pętle kolejowe: dokumentacja - odpowiednie dokumentowanie źródłowe dotyczące niektórych różnych metod wykonywania:
W przeciwieństwie do innych operacji TensorFlow, nie przekształcamy danych liczbowych pytona na tensory. Ponadto generowany jest nowy wykres dla każdej odrębnej wartości liczbowej pytona
function
tworzy osobny wykres dla każdego unikalnego zestawu kształtów wejściowych i typów danych .
Pojedynczy obiekt tf.function może wymagać odwzorowania na wiele wykresów obliczeniowych pod maską. Powinno to być widoczne tylko jako wydajność (wykresy śledzenia mają niezerowe obliczenia i koszty pamięci )
Procesory danych wejściowych : podobnie jak powyżej, procesor jest wybierany indywidualnie, zależnie od wewnętrznych flag ustawionych zgodnie z konfiguracjami środowiska wykonawczego (tryb wykonywania, format danych, strategia dystrybucji). Najprostszy przypadek to Eager, który działa bezpośrednio z tablicami Numpy. Aby zapoznać się z niektórymi konkretnymi przykładami, zobacz tę odpowiedź .
ROZMIAR MODELU, ROZMIAR DANYCH:
- Jest decydujący; żadna konfiguracja nie ukoronowała się na wszystkich rozmiarach modeli i danych.
- Ważny jest rozmiar danych w stosunku do wielkości modelu; w przypadku małych danych i modelu może dominować narzut związany z przesyłaniem danych (np. CPU do GPU). Podobnie małe procesory ogólne mogą pracować wolniej na dużych danych w dominującym czasie konwersji danych (patrz
convert_to_tensor
„PROFILER”)
- Prędkość różni się w zależności od sposobu przetwarzania zasobów przez pętle pociągowe i procesory danych wejściowych.
BENCHMARKS : zmielone mięso. - Dokument programu Word - Arkusz kalkulacyjny Excel
Terminologia :
- Liczby bez% to wszystkie sekundy
- % obliczone jako
(1 - longer_time / shorter_time)*100
; uzasadnienie: interesuje nas , który czynnik jest szybszy od drugiego; shorter / longer
jest w rzeczywistości relacją nieliniową, nieprzydatną do bezpośredniego porównania
- Oznaczenie%:
- TF2 vs TF1:
+
jeśli TF2 jest szybszy
- GvE (Graph vs. Eager):
+
jeśli Graph jest szybszy
- TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5
PROFILER :
PROFILER - Objaśnienie : Profiler IDE Spyder 3.3.6.
Niektóre funkcje są powtarzane w gniazdach innych; dlatego trudno jest wyśledzić dokładny rozdział między funkcjami „przetwarzania danych” a „szkoleniem”, więc pewne nakładanie się będzie na siebie widoczne - jak zaznaczono w ostatnim wyniku.
% liczby obliczone w czasie wykonywania minus czas kompilacji
- Czas budowania obliczany przez zsumowanie wszystkich (unikalnych) środowisk uruchomieniowych, które zostały nazwane 1 lub 2 razy
- Czas pociągu obliczony przez zsumowanie wszystkich (unikalnych) środowisk wykonawczych, które zostały nazwane tą samą # razy co # iteracji i niektórych środowisk uruchomieniowych ich gniazd
- Funkcje są profilowane zgodnie z ich oryginalnymi nazwami, niestety (tj.
_func = func
Będą się profilować jako func
), co miesza się w czasie kompilacji - stąd konieczność wykluczenia
TESTOWANIE ŚRODOWISKA :
- Wykonany kod u dołu z uruchomionymi minimalnymi zadaniami w tle
- GPU zostało „rozgrzane” z kilkoma iteracjami przed iteracjami czasowymi, jak sugerowano w tym poście
- CUDA 10.0.130, cuDNN 7.6.0, TensorFlow 1.14.0 i TensorFlow 2.0.0 zbudowany ze źródła oraz Anaconda
- Python 3.7.4, Spyder 3.3.6 IDE
- GTX 1070, Windows 10, 24 GB pamięci RAM DDR4 2,4 MHz, procesor i7-7700HQ 2,8 GHz
METODOLOGIA :
- Benchmark „mały”, „średni” i „duży” model i rozmiary danych
- Napraw liczbę parametrów dla każdego rozmiaru modelu, niezależnie od wielkości danych wejściowych
- Model „Większy” ma więcej parametrów i warstw
- „Większe” dane mają dłuższą sekwencję, ale takie same
batch_size
inum_channels
- Modele używać tylko
Conv1D
, Dense
„nauczenia” warstw; Unikano numerów RNN na implem w wersji TF. różnice
- Zawsze prowadziłem jedno dopasowanie pociągu poza pętlę testu porównawczego, aby pominąć budowanie modelu i wykresu optymalizatora
- Niewykorzystywanie rzadkich danych (np.
layers.Embedding()
) Lub rzadkich celów (npSparseCategoricalCrossEntropy()
OGRANICZENIA : „kompletna” odpowiedź wyjaśniałaby każdą możliwą pętlę pociągu i iterator, ale z pewnością jest to poza moją zdolnością czasową, nieistniejącą wypłatą lub ogólną koniecznością. Wyniki są tak dobre, jak metodologia - interpretuj z otwartym umysłem.
KOD :
import numpy as np
import tensorflow as tf
import random
from termcolor import cprint
from time import time
from tensorflow.keras.layers import Input, Dense, Conv1D
from tensorflow.keras.layers import Dropout, GlobalAveragePooling1D
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
import tensorflow.keras.backend as K
#from keras.layers import Input, Dense, Conv1D
#from keras.layers import Dropout, GlobalAveragePooling1D
#from keras.models import Model
#from keras.optimizers import Adam
#import keras.backend as K
#tf.compat.v1.disable_eager_execution()
#tf.enable_eager_execution()
def reset_seeds(reset_graph_with_backend=None, verbose=1):
if reset_graph_with_backend is not None:
K = reset_graph_with_backend
K.clear_session()
tf.compat.v1.reset_default_graph()
if verbose:
print("KERAS AND TENSORFLOW GRAPHS RESET")
np.random.seed(1)
random.seed(2)
if tf.__version__[0] == '2':
tf.random.set_seed(3)
else:
tf.set_random_seed(3)
if verbose:
print("RANDOM SEEDS RESET")
print("TF version: {}".format(tf.__version__))
reset_seeds()
def timeit(func, iterations, *args, _verbose=0, **kwargs):
t0 = time()
for _ in range(iterations):
func(*args, **kwargs)
print(end='.'*int(_verbose))
print("Time/iter: %.4f sec" % ((time() - t0) / iterations))
def make_model_small(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = Conv1D(128, 40, strides=4, padding='same')(ipt)
x = GlobalAveragePooling1D()(x)
x = Dropout(0.5)(x)
x = Dense(64, activation='relu')(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)
model.compile(Adam(lr=1e-4), 'binary_crossentropy')
return model
def make_model_medium(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = ipt
for filters in [64, 128, 256, 256, 128, 64]:
x = Conv1D(filters, 20, strides=1, padding='valid')(x)
x = GlobalAveragePooling1D()(x)
x = Dense(256, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(128, activation='relu')(x)
x = Dense(64, activation='relu')(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)
model.compile(Adam(lr=1e-4), 'binary_crossentropy')
return model
def make_model_large(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = Conv1D(64, 400, strides=4, padding='valid')(ipt)
x = Conv1D(128, 200, strides=1, padding='valid')(x)
for _ in range(40):
x = Conv1D(256, 12, strides=1, padding='same')(x)
x = Conv1D(512, 20, strides=2, padding='valid')(x)
x = Conv1D(1028, 10, strides=2, padding='valid')(x)
x = Conv1D(256, 1, strides=1, padding='valid')(x)
x = GlobalAveragePooling1D()(x)
x = Dense(256, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(128, activation='relu')(x)
x = Dense(64, activation='relu')(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)
model.compile(Adam(lr=1e-4), 'binary_crossentropy')
return model
def make_data(batch_shape):
return np.random.randn(*batch_shape), \
np.random.randint(0, 2, (batch_shape[0], 1))
def make_data_tf(batch_shape, n_batches, iters):
data = np.random.randn(n_batches, *batch_shape),
trgt = np.random.randint(0, 2, (n_batches, batch_shape[0], 1))
return tf.data.Dataset.from_tensor_slices((data, trgt))#.repeat(iters)
batch_shape_small = (32, 140, 30)
batch_shape_medium = (32, 1400, 30)
batch_shape_large = (32, 14000, 30)
batch_shapes = batch_shape_small, batch_shape_medium, batch_shape_large
make_model_fns = make_model_small, make_model_medium, make_model_large
iterations = [200, 100, 50]
shape_names = ["Small data", "Medium data", "Large data"]
model_names = ["Small model", "Medium model", "Large model"]
def test_all(fit=False, tf_dataset=False):
for model_fn, model_name, iters in zip(make_model_fns, model_names, iterations):
for batch_shape, shape_name in zip(batch_shapes, shape_names):
if (model_fn is make_model_large) and (batch_shape is batch_shape_small):
continue
reset_seeds(reset_graph_with_backend=K)
if tf_dataset:
data = make_data_tf(batch_shape, iters, iters)
else:
data = make_data(batch_shape)
model = model_fn(batch_shape)
if fit:
if tf_dataset:
model.train_on_batch(data.take(1))
t0 = time()
model.fit(data, steps_per_epoch=iters)
print("Time/iter: %.4f sec" % ((time() - t0) / iters))
else:
model.train_on_batch(*data)
timeit(model.fit, iters, *data, _verbose=1, verbose=0)
else:
model.train_on_batch(*data)
timeit(model.train_on_batch, iters, *data, _verbose=1)
cprint(">> {}, {} done <<\n".format(model_name, shape_name), 'blue')
del model
test_all(fit=True, tf_dataset=False)