CETTE RÉPONSE : vise à fournir une description détaillée, au niveau graphique / matériel, du problème, y compris les boucles de train TF2 vs TF1, les processeurs de données d'entrée et les exécutions en mode Désir vs Graphique. Pour un résumé des problèmes et des directives de résolution, consultez mon autre réponse.
VERDICT DE PERFORMANCE : parfois l'un est plus rapide, parfois l'autre, selon la configuration. En ce qui concerne TF2 vs TF1, ils sont à peu près au pair en moyenne, mais des différences importantes basées sur la configuration existent, et TF1 l'emporte sur TF2 plus souvent que l'inverse. Voir "BENCHMARKING" ci-dessous.
EAGER VS. GRAPH : la chair de cette réponse entière pour certains: TF2 impatient est plus lent que TF1, selon mes tests. Détails plus bas.
La différence fondamentale entre les deux est la suivante: Graph met en place un réseau informatique de manière proactive , et s'exécute lorsqu'il lui est «demandé» - tandis que Eager exécute tout lors de la création. Mais l'histoire commence seulement ici:
Désireux n'est PAS dépourvu de graphique , et peut en fait être principalement graphique, contrairement aux attentes. Ce qu'il est en grande partie, est exécuté Graphique - cela inclut les poids du modèle et de l'optimiseur, comprenant une grande partie du graphique.
Désireux reconstruit une partie de son propre graphique à l'exécution ; conséquence directe de la construction incomplète de Graph - voir les résultats du profileur. Cela a une surcharge de calcul.
Désireux est plus lent avec les entrées Numpy ; selon ce commentaire et code Git , les entrées Numpy dans Eager incluent les frais généraux de copie des tenseurs du CPU au GPU. En parcourant le code source, les différences de gestion des données sont claires; Désireux passe directement Numpy, tandis que Graph passe des tenseurs qui évaluent ensuite Numpy; incertain du processus exact, mais ce dernier devrait impliquer des optimisations au niveau du GPU
TF2 Eager est plus lent que TF1 Eager - c'est ... inattendu. Voir les résultats d'analyse comparative ci-dessous. Les différences vont de négligeable à significatif, mais sont cohérentes. Je ne sais pas pourquoi c'est le cas - si un développeur TF clarifie, mettra à jour la réponse.
TF2 vs TF1 : citant les parties pertinentes d'un développement TF, Q. Scott Zhu, réponse - avec un peu de mon accent et reformulation:
En mode avide, le runtime doit exécuter les opérations et renvoyer la valeur numérique pour chaque ligne de code python. La nature de l' exécution en une seule étape la rend lente .
Dans TF2, Keras utilise tf.function pour construire son graphique pour la formation, l'évaluation et la prédiction. Nous les appelons "fonction d'exécution" pour le modèle. Dans TF1, la "fonction d'exécution" était un FuncGraph, qui partageait un composant commun en tant que fonction TF, mais avec une implémentation différente.
Au cours du processus, nous avons en quelque sorte laissé une implémentation incorrecte pour train_on_batch (), test_on_batch () et Predict_on_batch () . Ils sont toujours numériquement corrects , mais la fonction d'exécution de x_on_batch est une fonction python pure, plutôt qu'une fonction python enveloppée par tf.function. Cela entraînera une lenteur
Dans TF2, nous convertissons toutes les données d'entrée en un tf.data.Dataset, par lequel nous pouvons unifier notre fonction d'exécution pour gérer le type unique des entrées. Il peut y avoir des frais généraux dans la conversion de l'ensemble de données , et je pense qu'il s'agit d'un surcoût unique, plutôt que d'un coût par lot
Avec la dernière phrase du dernier paragraphe ci-dessus et la dernière clause du paragraphe ci-dessous:
Pour surmonter la lenteur en mode impatient, nous avons @ tf.function, qui transformera une fonction python en graphique. Lorsque vous alimentez une valeur numérique comme un tableau np, le corps de la fonction tf.function est converti en graphique statique, optimisé et renvoie la valeur finale, qui est rapide et devrait avoir des performances similaires à celles du mode graphique TF1.
Je ne suis pas d'accord - d'après mes résultats de profilage, qui montrent que le traitement des données d'entrée d'Eager est sensiblement plus lent que celui de Graph. En outre, vous n'êtes pas sûr de cela tf.data.Dataset
en particulier, mais Eager appelle à plusieurs reprises plusieurs des mêmes méthodes de conversion de données - voir profileur.
Enfin, le commit lié du développeur: nombre important de modifications pour prendre en charge les boucles Keras v2 .
Boucles de train : selon (1) Désireux vs Graphique; (2) le format des données d'entrée, la formation procédera à une boucle de train distinct - dans TF2, _select_training_loop()
, training.py , l' un de:
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
Chacun gère l'allocation des ressources différemment et a des conséquences sur les performances et les capacités.
Boucles de train: fit
vs train_on_batch
, keras
vstf.keras
: chacune des quatre utilise des boucles de train différentes, mais peut-être pas dans toutes les combinaisons possibles. keras
' fit
, par exemple, utilise une forme de fit_loop
, par exemple training_arrays.fit_loop()
, et son train_on_batch
peut utiliser K.function()
. tf.keras
a une hiérarchie plus sophistiquée décrite en partie dans la section précédente.
Train Loops: documentation - documentation source pertinente sur certaines des différentes méthodes d'exécution:
Contrairement à d'autres opérations TensorFlow, nous ne convertissons pas les entrées numériques python en tenseurs. De plus, un nouveau graphique est généré pour chaque valeur numérique python distincte
function
instancie un graphique séparé pour chaque ensemble unique de formes d'entrée et de types de données .
Un seul objet tf.function peut devoir être mappé à plusieurs graphiques de calcul sous le capot. Cela ne devrait être visible qu'en tant que performances (le traçage des graphiques a un coût de calcul et de mémoire différent de zéro )
Processeurs de données d'entrée : comme ci-dessus, le processeur est sélectionné au cas par cas, en fonction des indicateurs internes définis en fonction des configurations d'exécution (mode d'exécution, format de données, stratégie de distribution). Le cas le plus simple est avec Eager, qui fonctionne directement avec les tableaux Numpy. Pour quelques exemples spécifiques, voir cette réponse .
TAILLE DU MODÈLE, TAILLE DES DONNÉES:
- Est décisif; aucune configuration unique ne s'est couronnée au-dessus de toutes les tailles de modèle et de données.
- La taille des données par rapport à la taille du modèle est importante; pour les petites données et les modèles, le transfert de données (par exemple CPU vers GPU) peut dominer. De même, les petits processeurs généraux peuvent fonctionner plus lentement sur les grandes données par temps de conversion de données dominant (voir
convert_to_tensor
dans "PROFILER")
- La vitesse diffère selon les boucles des trains et les différents moyens utilisés par les processeurs de traitement des données pour gérer les ressources.
REPÈRES : la viande hachée. - Document Word - Feuille de calcul Excel
Terminologie :
- les nombres sans% sont tous des secondes
- % calculé comme
(1 - longer_time / shorter_time)*100
; justification: nous nous intéressons à quel facteur l' un est plus rapide que l'autre; shorter / longer
est en fait une relation non linéaire, pas utile pour une comparaison directe
- Détermination du signe%:
- TF2 vs TF1:
+
si TF2 est plus rapide
- GvE (Graph vs. Eager):
+
si Graph est plus rapide
- TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5
PROFILER :
PROFILER - Explication : Spyder 3.3.6 IDE profiler.
Certaines fonctions sont répétées dans les nids des autres; par conséquent, il est difficile de retrouver la séparation exacte entre les fonctions de «traitement des données» et de «formation», il y aura donc un certain chevauchement - comme cela a été prononcé dans le tout dernier résultat.
% chiffres calculés par rapport au temps d'exécution moins le temps de construction
- Temps de construction calculé en additionnant tous les temps d'exécution (uniques) qui ont été appelés 1 ou 2 fois
- Temps de train calculé en additionnant tous les temps d'exécution (uniques) qui ont été appelés le même nombre de fois que le nombre d'itérations et certains temps d'exécution de leurs nids
- Les fonctions sont profilées en fonction de leur nom d' origine , malheureusement (c.-à
_func = func
-d. Profilera comme func
), ce qui se mélange dans le temps de construction - d'où la nécessité de l'exclure
ENVIRONNEMENT D'ESSAI :
- Code exécuté en bas avec un minimum de tâches en arrière-plan en cours d'exécution
- Le GPU a été "réchauffé" avec quelques itérations avant de chronométrer les itérations, comme suggéré dans ce post
- CUDA 10.0.130, cuDNN 7.6.0, TensorFlow 1.14.0 et TensorFlow 2.0.0 construits à partir de la source, plus Anaconda
- Python 3.7.4, Spyder 3.3.6 IDE
- GTX 1070, Windows 10, 24 Go de RAM DDR4 2,4 MHz, processeur i7-7700HQ 2,8 GHz
MÉTHODOLOGIE :
- Benchmark «petit», «moyen» et «grand» modèle et tailles de données
- Correction du nombre de paramètres pour chaque taille de modèle, indépendamment de la taille des données d'entrée
- Le modèle "plus grand" a plus de paramètres et de couches
- Les données "plus grandes" ont une séquence plus longue, mais identique
batch_size
etnum_channels
- Les modèles utilisent uniquement
Conv1D
,Dense
couches « » apprenables; RNN évités par implément de version TF. différences
- Toujours exécuté un train adapté à l'extérieur de la boucle d'analyse comparative, pour omettre la construction du modèle et du graphique d'optimisation
- Ne pas utiliser de données éparses (par exemple
layers.Embedding()
) ou de cibles éparses (par exempleSparseCategoricalCrossEntropy()
LIMITATIONS : une réponse «complète» expliquerait chaque boucle de train et itérateur possible, mais cela dépasse certainement mes capacités de temps, mon chèque de paie inexistant ou ma nécessité générale. Les résultats ne sont aussi bons que la méthodologie - interpréter avec un esprit ouvert.
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)