De nombreux utilisateurs l'ont cité comme raison de passer à Pytorch, mais je n'ai pas encore trouvé de justification/explication pour sacrifier la qualité pratique la plus importante, la vitesse, pour une exécution rapide.
Vous trouverez ci-dessous les performances de l'analyse de code, TF1 contre TF2 - TF1 fonctionnant de 47% à 276% plus rapidement .
Ma question est: qu'est-ce qui, au niveau du graphique ou du matériel, produit un ralentissement si important?
Vous cherchez une réponse détaillée - je connais déjà les concepts généraux. Git pertinent
Spécifications : CUDA 10.0.130, cuDNN 7.4.2, Python 3.7.4, Windows 10, GTX 1070
Résultats de référence :
[~ # ~] mise à jour [~ # ~] : Désactiver l'exécution désirée par le code ci-dessous fait pas aider. Le comportement, cependant, est incohérent: parfois, l'exécution en mode graphique aide considérablement, d'autres fois il s'exécute plus lent par rapport à Eager.
Comme les développeurs TF n'apparaissent nulle part, je vais enquêter moi-même sur cette question - je peux suivre les progrès du problème lié à Github.
MISE À JOUR 2 : tonnes de résultats expérimentaux à partager, accompagnés d'explications; devrait être fait aujourd'hui.
Code de référence :
# use tensorflow.keras... to benchmark tf.keras; used GPU for all above benchmarks
from keras.layers import Input, Dense, LSTM, Bidirectional, Conv1D
from keras.layers import Flatten, Dropout
from keras.models import Model
from keras.optimizers import Adam
import keras.backend as K
import numpy as np
from time import time
batch_shape = (32, 400, 16)
X, y = make_data(batch_shape)
model_small = make_small_model(batch_shape)
model_small.train_on_batch(X, y) # skip first iteration which builds graph
timeit(model_small.train_on_batch, 200, X, y)
K.clear_session() # in my testing, kernel was restarted instead
model_medium = make_medium_model(batch_shape)
model_medium.train_on_batch(X, y) # skip first iteration which builds graph
timeit(model_medium.train_on_batch, 10, X, y)
Fonctions utilisées :
def timeit(func, iterations, *args):
t0 = time()
for _ in range(iterations):
func(*args)
print("Time/iter: %.4f sec" % ((time() - t0) / iterations))
def make_small_model(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = Conv1D(128, 400, strides=4, padding='same')(ipt)
x = Flatten()(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_medium_model(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = Bidirectional(LSTM(512, activation='relu', return_sequences=True))(ipt)
x = LSTM(512, activation='relu', return_sequences=True)(x)
x = Conv1D(128, 400, strides=4, padding='same')(x)
x = Flatten()(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))
MISE À JOUR 18/02/2020 : J'ai mis les bancs 2.1 et 2.1 tous les soirs; les résultats sont mitigés. Toutes les configurations sauf une (modèle et taille des données) sont aussi rapides ou beaucoup plus rapides que les meilleures de TF2 et TF1. Celui qui est le plus lent et le plus lent de façon spectaculaire est Large-Large - esp. dans l'exécution du graphe ( 1,6x à 2,5x plus lent ).
De plus, il existe extrême des différences de reproductibilité entre Graph et Eager pour un grand modèle que j'ai testé - une qui ne peut pas être expliquée par le hasard/calcul-parallélisme. Je ne peux pas actuellement présenter de code reproductible pour ces revendications par contraintes de temps, donc je recommande fortement de le tester pour vos propres modèles.
Je n'ai pas encore ouvert de problème avec Git, mais j'ai fait un commentaire sur le original - pas encore de réponse. Je mettrai à jour la ou les réponses une fois les progrès réalisés.
[~ # ~] verdict [~ # ~] : c'est ce n'est pas, SI vous savez ce que vous faites. Mais si vous ne le faites pas, cela pourrait vous coûter beaucoup, par quelques mises à niveau de GPU en moyenne, et par plusieurs GPU dans le pire des cas.
CETTE RÉPONSE : vise à fournir une description de haut niveau du problème, ainsi que des directives pour décider de la configuration de la formation spécifique à vos besoins. Pour une description détaillée et de bas niveau, qui inclut tous les résultats d'analyse comparative + code utilisé, voir mon autre réponse.
Je mettrai à jour ma ou mes réponses avec plus d'informations si j'en apprends - je peux mettre en signet/"mettre en vedette" cette question pour référence.
RÉSUMÉ DU PROBLÈME : comme confirmé par un développeur de TensorFlow, Q. Scott Zhu, TF2 a concentré le développement sur l'exécution désireuse et l'intégration étroite avec Keras, ce qui impliquait un balayage changements dans la source TF - y compris au niveau du graphique. Avantages: capacités de traitement, de distribution, de débogage et de déploiement considérablement étendues. Le coût de certains d'entre eux, cependant, est la vitesse.
La question est cependant assez complexe. Ce n'est pas seulement TF1 vs TF2 - les facteurs entraînant des différences significatives de vitesse de train comprennent:
keras
contre tf.keras
numpy
vs tf.data.Dataset
vs ...train_on_batch()
vs fit()
model(x)
vs model.predict(x)
vs ...Malheureusement, presque rien de ce qui précède n'est indépendant de l'autre, et chacun peut au moins doubler le temps d'exécution par rapport à un autre. Heureusement, vous pouvez déterminer ce qui fonctionnera le mieux systématiquement et avec quelques raccourcis - comme je vais le montrer.
QUE DOIS-JE FAIRE? Actuellement, la seule façon est d'expérimenter pour votre modèle, vos données et votre matériel spécifiques. Aucune configuration unique ne fonctionnera toujours mieux - mais il y a sont choses à faire et à ne pas faire pour simplifier votre recherche:
>> À FAIRE:
train_on_batch()
+ numpy
+ tf.keras
+ TF1 + Désireux/Graphiquetrain_on_batch()
+ numpy
+ tf.keras
+ TF2 + Graphiquefit()
+ numpy
+ tf.keras
+ TF1/TF2 + Graphique + grand modèle et données>> NE PAS:
fit()
+ numpy
+ keras
pour les petits et moyens modèles et donnéesfit()
+ numpy
+ tf.keras
+ TF1/TF2 + Désireuxtrain_on_batch()
+ numpy
+ keras
+ TF1 + Désireux
[Majeur] tf.python.keras
; il peut fonctionner 10 à 100 fois plus lentement et avec beaucoup de bogues; plus d'informations
layers
, models
, optimizers
, et les importations d'utilisation "prêtes à l'emploi" associées; les opérations, les utilitaires et les importations "privées" connexes sont corrects - mais pour être sûr, vérifiez les alts, et s'ils sont utilisés dans tf.keras
Reportez-vous au code au bas de mon autre réponse pour un exemple de configuration d'analyse comparative. La liste ci-dessus est basée principalement sur les tableaux "BENCHMARKS" dans l'autre réponse.
[~ # ~] limitations [~ # ~] des choses à faire et à ne pas faire ci-dessus:
Conv1D
Et Dense
- pas de RNN, données/cibles éparses, entrées 4/5D et autres configurationsnumpy
et tf.data.Dataset
, Tandis que de nombreux autres formats existent; voir autre réponsePourquoi TF2 a-t-il sacrifié la qualité la plus pratique, la vitesse, pour une exécution rapide? Ce n'est pas le cas, clairement - le graphique est toujours disponible. Mais si la question est "pourquoi tant de désir":
.__dict__
. Le graphique, en revanche, nécessite une familiarité avec les fonctions spéciales de backend - compliquant grandement tout le processus de débogage et d'introspection.COMMENT ACTIVER/DÉSACTIVER EAGER?
tf.enable_eager_execution() # TF1; must be done before any model/tensor creation
tf.compat.v1.disable_eager_execution() # TF2; above holds
INFORMATIONS SUPPLÉMENTAIRES :
_on_batch()
dans TF2; selon le développeur TF, ils utilisent toujours une implémentation plus lente, mais pas intentionnellement - c'est-à-dire que cela doit être corrigé. Voir l'autre réponse pour plus de détails.DEMANDES DE DEVS TENSORFLOW :
train_on_batch()
, et l'aspect performance de l'appel de fit()
de manière itérative; les boucles de train personnalisées sont importantes pour beaucoup, surtout pour moi.[~ # ~] remerciements [~ # ~] : Merci à
[~ # ~] met à jour [~ # ~] :
14/11/19 - a trouvé un modèle (dans ma vraie application) qui tourne plus lentement sur TF2 pour toutes * les configurations w/Numpy data d'entrée. Les différences variaient de 13 à 19%, avec une moyenne de 17%. Les différences entre keras
et tf.keras
Étaient cependant plus dramatiques: 18-40% , moy. 32% (TF1 & 2). (* - sauf Eager, pour lequel TF2 OOM'd)
17/11/19 - les développeurs ont mis à jour les méthodes on_batch()
dans un commit récent , indiquant une vitesse améliorée - à publier dans TF 2.1, ou disponible maintenant sous tf-nightly
. Comme je ne parviens pas à faire fonctionner ce dernier, cela retardera le benching jusqu'à 2.1.
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 Exécutions en mode vs vs graphique. Pour un résumé du problème et des directives de résolution, voir 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 est impatient plus lent que TF1, selon mes tests. Détails plus bas.
La différence fondamentale entre les deux est: Graph met en place un réseau de calcul de manière proactive, et s'exécute quand on lui dit - alors 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 surtout graphique, contrairement aux attentes. Ce qu'il est en grande partie, c'est Graphique exécuté - 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 graphe à 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 ; par ce commentaire Git & code, 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 dev TF, Q. Scott Zhu, réponse - w/bit de mon accentuation 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 le fait être lent .
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 pure fonction python, plutôt qu'un tf. fonction enveloppée python. 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 que ce sont des frais généraux uniques, plutôt qu'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 renvoyant 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 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 significatif de changements pour supporter les boucles Keras v2 .
Boucles de train : selon (1) Désireux vs Graphique; (2) format de données d'entrée, la formation en procédera avec une boucle de train distincte - dans TF2, _select_training_loop()
, training.py , l'un des:
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
vs tf.keras
: chacun 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 - pertinent source docstring sur certaines des différentes méthodes d'exécution:
Contrairement à d'autres opérations TensorFlow, nous ne convertissons pas python entrées numériques en tenseurs. De plus, un nouveau graphique est généré pour chaque python valeur numérique
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 devrait être visible uniquement en tant que performances (les graphiques de suivi ont un coût de calcul et de mémoire différent de zéro )
Processeurs de données d'entrée : similaire à 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, données format, 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:
convert_to_tensor
Dans "PROFILER")[~ # ~] repères [~ # ~] : la viande hachée. - Document Word - Feuille de calcul Excel
Terminologie :
(1 - longer_time / shorter_time)*100
; justification: nous sommes intéressés par 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+
Si TF2 est plus rapide+
Si le Graphique est plus rapide[~ # ~] profileur [~ # ~] :
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 p.r.t. runtime moins le temps de construction
_func = func
Sera profilé comme func
), ce qui se mélange dans le temps de construction - d'où la nécessité de l'exclureENVIRONNEMENT DE TEST :
[~ # ~] méthodologie [~ # ~] :
batch_size
Et num_channels
Conv1D
, Dense
'apprenables'; RNN évités par implément de version TF. différenceslayers.Embedding()
) ou de cibles éparses (par exemple SparseCategoricalCrossEntropy()
[~ # ~] limitations [~ # ~] : une réponse "complète" expliquerait chaque boucle de train et itérateur possible, mais cela dépasse certainement mon temps capacité, chèque de paie inexistant ou 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)