DIESE ANTWORT : Ziel ist es, eine detaillierte Beschreibung des Problems auf Grafik- / Hardware-Ebene bereitzustellen - einschließlich TF2- / TF1-Zugschleifen, Eingabedatenprozessoren und Eager- / Grafikmodus-Ausführungen. Eine Zusammenfassung der Probleme und Richtlinien zur Lösung von Problemen finden Sie in meiner anderen Antwort.
LEISTUNGSPRÜFUNG : Je nach Konfiguration ist manchmal eine schneller, manchmal die andere. Was TF2 und TF1 angeht, sind sie im Durchschnitt ungefähr gleich, aber es gibt signifikante konfigurationsbasierte Unterschiede, und TF1 übertrumpft TF2 häufiger als umgekehrt. Siehe "BENCHMARKING" weiter unten.
EAGER VS. GRAFIK : Das Fleisch dieser ganzen Antwort für einige: TF2 ist nach meinen Tests langsamer als TF1. Details weiter unten.
Der grundlegende Unterschied zwischen den beiden besteht darin, dass Graph proaktiv ein Computernetzwerk einrichtet und ausgeführt wird, wenn es dazu aufgefordert wird - während Eager alles bei der Erstellung ausführt. Aber die Geschichte beginnt erst hier:
Eifrig ist NICHT frei von Graph und kann entgegen der Erwartung tatsächlich meistens Graph sein. Was es größtenteils ist, ist ausgeführtes Diagramm - dies schließt Modell- und Optimierungsgewichte ein, die einen großen Teil des Diagramms ausmachen.
Eager erstellt bei der Ausführung einen Teil des eigenen Diagramms neu . direkte Folge davon, dass Graph nicht vollständig erstellt wurde - siehe Profilerergebnisse. Dies hat einen Rechenaufwand.
Eifrig ist langsamer mit Numpy-Eingaben ; pro diesem Git Kommentar & Code, in Eager Numpy Eingänge umfassen die Overhead - Kosten Tensoren von CPU zu GPU zu kopieren. Beim Durchlaufen des Quellcodes sind die Unterschiede bei der Datenverarbeitung klar. Eifrig passiert Numpy direkt, während Graph Tensoren passiert, die dann zu Numpy ausgewertet werden. unsicher über den genauen Prozess, aber letztere sollten Optimierungen auf GPU-Ebene beinhalten
TF2 Eager ist langsamer als TF1 Eager - das ist ... unerwartet. Siehe Benchmarking-Ergebnisse unten. Die Unterschiede reichen von vernachlässigbar bis signifikant, sind jedoch konsistent. Unsicher, warum dies der Fall ist - wenn ein TF-Entwickler dies klarstellt, wird die Antwort aktualisiert.
TF2 vs. TF1 : Zitieren relevanter Teile der Antwort eines TF-Entwicklers, Q. Scott Zhu - mit ein wenig meiner Betonung und Umformulierung:
In der Eifersucht muss die Laufzeit die Operationen ausführen und den numerischen Wert für jede Zeile Python-Code zurückgeben. Die Art der Einzelschrittausführung führt dazu, dass sie langsam ist .
In TF2 nutzt Keras die Funktion tf.function, um ein Diagramm für Training, Bewertung und Vorhersage zu erstellen. Wir nennen sie "Ausführungsfunktion" für das Modell. In TF1 war die "Ausführungsfunktion" ein FuncGraph, der eine gemeinsame Komponente als TF-Funktion gemeinsam hatte, jedoch eine andere Implementierung aufweist.
Während des Prozesses haben wir irgendwie eine falsche Implementierung für train_on_batch (), test_on_batch () und Predict_on_batch () hinterlassen . Sie sind immer noch numerisch korrekt , aber die Ausführungsfunktion für x_on_batch ist eine reine Python-Funktion und keine mit tf.function umschlossene Python-Funktion. Dies führt zu Langsamkeit
In TF2 konvertieren wir alle Eingabedaten in ein tf.data.Dataset, mit dem wir unsere Ausführungsfunktion vereinheitlichen können, um den einzelnen Typ der Eingaben zu verarbeiten. Die Dataset-Konvertierung kann einen gewissen Overhead verursachen , und ich denke, dies ist nur ein einmaliger Overhead und keine Kosten pro Stapel
Mit dem letzten Satz des letzten Absatzes oben und dem letzten Satz des folgenden Absatzes:
Um die Langsamkeit im eifrigen Modus zu überwinden, haben wir die Funktion @ tf., die eine Python-Funktion in einen Graphen verwandelt. Wenn Sie einen numerischen Wert wie ein np-Array eingeben, wird der Hauptteil der Funktion tf. in ein statisches Diagramm umgewandelt, optimiert und der endgültige Wert zurückgegeben, der schnell ist und eine ähnliche Leistung wie der TF1-Diagrammmodus haben sollte.
Ich bin anderer Meinung - gemäß meinen Profilerstellungsergebnissen, die zeigen, dass die Verarbeitung der Eingabedaten von Eager wesentlich langsamer ist als die von Graph. Auch unsicher, tf.data.Dataset
insbesondere, aber Eager ruft wiederholt mehrere der gleichen Datenkonvertierungsmethoden auf - siehe Profiler.
Zuletzt das verknüpfte Commit von dev: Signifikante Anzahl von Änderungen zur Unterstützung der Keras v2-Schleifen .
Zugschleifen : abhängig von (1) Eifrig gegen Grafik; (2) Eingangsdatenformat, in Ausbildung wird mit einer deutlichen Zuge Schleife ablaufen - in TF2 _select_training_loop()
, training.py , von:
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
Jeder behandelt die Ressourcenzuweisung unterschiedlich und hat Konsequenzen für Leistung und Leistungsfähigkeit.
Zugschleifen: fit
vs train_on_batch
, keras
vstf.keras
.: Jede der vier verwendet unterschiedliche Zugschleifen, wenn auch möglicherweise nicht in jeder möglichen Kombination. keras
'verwendet fit
zum Beispiel eine Form von fit_loop
z. B. training_arrays.fit_loop()
und train_on_batch
kann verwendet werden K.function()
. tf.keras
hat eine komplexere Hierarchie, die teilweise im vorherigen Abschnitt beschrieben wurde.
Train Loops: Dokumentation - relevante Quelldokumentation zu einigen der verschiedenen Ausführungsmethoden:
Im Gegensatz zu anderen TensorFlow-Operationen konvertieren wir keine numerischen Python-Eingaben in Tensoren. Darüber hinaus wird für jeden einzelnen Python-Zahlenwert ein neues Diagramm erstellt
function
Instanziiert ein separates Diagramm für jeden eindeutigen Satz von Eingabeformen und Datentypen .
Ein einzelnes tf.function-Objekt muss möglicherweise mehreren Berechnungsgraphen unter der Haube zugeordnet werden. Dies sollte nur als Leistung sichtbar sein (für die Verfolgung von Diagrammen fallen Rechen- und Speicherkosten ungleich Null an ).
Eingabedatenprozessoren : Ähnlich wie oben wird der Prozessor von Fall zu Fall ausgewählt, abhängig von den internen Flags, die gemäß den Laufzeitkonfigurationen (Ausführungsmodus, Datenformat, Verteilungsstrategie) gesetzt wurden. Der einfachste Fall ist mit Eager, das direkt mit Numpy-Arrays funktioniert. Einige spezifische Beispiele finden Sie in dieser Antwort .
MODELLGRÖSSE, DATENGRÖSSE:
- Ist entscheidend; Keine einzige Konfiguration krönte sich auf allen Modell- und Datengrößen.
- Die Datengröße im Verhältnis zur Modellgröße ist wichtig. Bei kleinen Daten und Modellen kann der Overhead der Datenübertragung (z. B. von CPU zu GPU) dominieren. Ebenso können kleine Overhead-Prozessoren bei großen Daten pro dominierender Datenkonvertierungszeit langsamer laufen (siehe
convert_to_tensor
unter "PROFILER").
- Die Geschwindigkeit ist je nach Zugschleifen und Eingabedatenprozessoren unterschiedlich.
BENCHMARKS : das gemahlene Fleisch. - Word-Dokument - Excel-Tabelle
Terminologie :
- % -lose Zahlen sind alle Sekunden
- % berechnet als
(1 - longer_time / shorter_time)*100
; Begründung: Wir sind daran interessiert, welcher Faktor einer schneller ist als der andere. shorter / longer
ist eigentlich eine nichtlineare Beziehung, die für einen direkten Vergleich nicht nützlich ist
- % Vorzeichenbestimmung:
- TF2 vs TF1:
+
wenn TF2 schneller ist
- GvE (Graph vs. Eager):
+
Wenn Graph schneller ist
- TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5
PROFILER :
PROFILER - Erläuterung : Spyder 3.3.6 IDE-Profiler.
Einige Funktionen werden in Nestern anderer wiederholt; Daher ist es schwierig, die genaue Trennung zwischen den Funktionen "Datenverarbeitung" und "Training" zu ermitteln, sodass es zu Überschneidungen kommt - wie im allerletzten Ergebnis deutlich.
% Zahlen berechnet für Laufzeit minus Build-Zeit
- Erstellungszeit berechnet durch Summieren aller (eindeutigen) Laufzeiten, die 1 oder 2 Mal aufgerufen wurden
- Zugzeit berechnet durch Summieren aller (eindeutigen) Laufzeiten, die genauso oft wie die Anzahl der Iterationen aufgerufen wurden, und einiger Laufzeiten ihrer Nester
- Funktionen werden leider anhand ihrer ursprünglichen Namen profiliert (dh
_func = func
werden als profiliert func
), was sich in der Erstellungszeit mischt - daher muss sie ausgeschlossen werden
PRÜFUMGEBUNG :
- Unten ausgeführter Code mit minimalen Hintergrundaufgaben
- Die GPU wurde mit einigen Iterationen vor dem Timing der Iterationen "aufgewärmt", wie in diesem Beitrag vorgeschlagen
- CUDA 10.0.130, cuDNN 7.6.0, TensorFlow 1.14.0 und TensorFlow 2.0.0 aus der Quelle sowie Anaconda
- Python 3.7.4, Spyder 3.3.6 IDE
- GTX 1070, Windows 10, 24 GB DDR4 2,4-MHz-RAM, i7-7700HQ 2,8-GHz-CPU
METHODIK :
- Benchmark "kleine", "mittlere" und "große" Modell- und Datengrößen
- Festlegen der Anzahl der Parameter für jede Modellgröße, unabhängig von der Größe der Eingabedaten
- "Größeres" Modell hat mehr Parameter und Ebenen
- "Größere" Daten haben eine längere Sequenz, aber die gleichen
batch_size
undnum_channels
- Modelle nur verwenden
Conv1D
, Dense
‚erlernbar‘ Schichten; RNNs werden pro TF-Version vermieden. Unterschiede
- Es wurde immer eine Zuganpassung außerhalb der Benchmarking-Schleife ausgeführt, um die Erstellung von Modell- und Optimierungsgraphen zu vermeiden
- Keine spärlichen Daten (z. B.
layers.Embedding()
) oder spärlichen Ziele (zSparseCategoricalCrossEntropy()
EINSCHRÄNKUNGEN : Eine "vollständige" Antwort würde jede mögliche Zugschleife und jeden möglichen Iterator erklären, aber das geht sicherlich über meine Zeitfähigkeit, meinen nicht vorhandenen Gehaltsscheck oder meine allgemeine Notwendigkeit hinaus. Die Ergebnisse sind nur so gut wie die Methodik - offen interpretieren.
CODE :
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)