QUESTA RISPOSTA : mira a fornire una descrizione dettagliata, a livello di grafico / hardware del problema, inclusi loop di treni TF2 vs. TF1, processori di dati di input ed esecuzioni in modalità Eager vs. Graph. Per un riepilogo dei problemi e linee guida per la risoluzione, vedere la mia altra risposta.
VERDETTO DELLE PRESTAZIONI : a volte uno è più veloce, a volte l'altro, a seconda della configurazione. Per quanto riguarda TF2 vs TF1, sono in media alla pari, ma esistono differenze significative basate sulla configurazione e TF1 supera TF2 più spesso di viceversa. Vedi "BENCHMARKING" di seguito.
EAGER VS. GRAFICO : la carne di questa intera risposta per alcuni: il desideroso di TF2 è più lento di quello di TF1, secondo i miei test. Dettagli più in basso.
La differenza fondamentale tra i due è: Graph imposta una rete computazionale in modo proattivo ed esegue quando "detto a" - mentre Eager esegue tutto al momento della creazione. Ma la storia inizia solo qui:
Desideroso NON è privo di Grafico , e in realtà può essere principalmente Grafico, contrariamente alle aspettative. Quello che è in gran parte, viene eseguito Graph - questo include pesi del modello e dell'ottimizzatore, che comprende una grande porzione del grafico.
Eager ricostruisce parte del proprio grafico durante l'esecuzione ; conseguenza diretta di Graph non completamente costruito - vedere i risultati del profiler. Questo ha un sovraccarico computazionale.
Eager è più lento con input Numpy ; in base a questo commento e codice Git , gli input Numpy in Eager includono i costi generali di copia dei tensori dalla CPU alla GPU. Passando attraverso il codice sorgente, le differenze nella gestione dei dati sono chiare; Eager passa direttamente a Numpy, mentre Graph passa a tensori che poi valutano a Numpy; incerto dell'esatto processo, ma quest'ultimo dovrebbe comportare ottimizzazioni a livello di GPU
TF2 Eager è più lento di TF1 Eager - questo è ... inaspettato. Vedi i risultati del benchmarking di seguito. Le differenze vanno da trascurabili a significative, ma sono coerenti. Non sono sicuro del perché - se un dev TF chiarisce, aggiornerà la risposta.
TF2 vs. TF1 : citando parti rilevanti di uno sviluppatore TF, Q. Scott Zhu, risposta - con un po 'della mia enfasi e riformulazione:
In impazienza, il runtime deve eseguire le operazioni e restituire il valore numerico per ogni riga di codice Python. La natura dell'esecuzione a singolo passaggio fa sì che sia lenta .
In TF2, Keras sfrutta la funzione tf.per costruire il suo grafico per allenamento, valutazione e previsione. Li chiamiamo "funzione di esecuzione" per il modello. In TF1, la "funzione di esecuzione" era un FuncGraph, che condivideva alcuni componenti comuni come funzione TF, ma ha un'implementazione diversa.
Durante il processo, abbiamo in qualche modo lasciato un'implementazione errata per train_on_batch (), test_on_batch () e predict_on_batch () . Sono ancora numericamente corretti , ma la funzione di esecuzione per x_on_batch è una funzione Python pura, piuttosto che una funzione Python con funzione tf.function. Ciò causerà lentezza
In TF2, convertiamo tutti i dati di input in un tf.data.Dataset, mediante il quale possiamo unificare la nostra funzione di esecuzione per gestire il singolo tipo di input. Potrebbe esserci un certo overhead nella conversione del set di dati e penso che si tratti di un overhead solo una volta, piuttosto che di un costo per batch
Con l'ultima frase dell'ultimo paragrafo sopra e l'ultima clausola del paragrafo seguente:
Per superare la lentezza in modalità desideroso, abbiamo @ tf.function, che trasformerà una funzione python in un grafico. Quando si alimenta un valore numerico come l'array np, il corpo della funzione tf. viene convertito in grafico statico, ottimizzato, e restituisce il valore finale, che è veloce e dovrebbe avere prestazioni simili alla modalità grafico TF1.
Non sono d'accordo - per i miei risultati di profilazione, che mostrano che l'elaborazione dei dati di input di Eager è sostanzialmente più lenta di quella di Graph. Inoltre, non sono sicuro tf.data.Dataset
in particolare, ma Eager chiama ripetutamente più metodi di conversione dei dati uguali - vedi profiler.
Infine, il commit collegato di dev: Numero significativo di modifiche per supportare i loop di Keras v2 .
Train Loops : dipende da (1) desideroso vs. grafico; (2) formato di dati in ingresso, la formazione in procederà con un ciclo treno distinte - in TF2, _select_training_loop()
, training.py , uno di:
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
Ognuno gestisce l'allocazione delle risorse in modo diverso e ha conseguenze su prestazioni e capacità.
Loop del treno: fit
vs train_on_batch
, keras
vstf.keras
.: ciascuno dei quattro utilizza diversi loop del treno, anche se forse non in tutte le possibili combinazioni. keras
" fit
, ad esempio, utilizza una forma di fit_loop
, ad esempio training_arrays.fit_loop()
, e train_on_batch
può essere utilizzata K.function()
. tf.keras
ha una gerarchia più sofisticata descritta in parte nella sezione precedente.
Train Loops: documentazione - documentazione sorgente pertinente su alcuni dei diversi metodi di esecuzione:
A differenza di altre operazioni TensorFlow, non convertiamo input numerici python in tensori. Inoltre, viene generato un nuovo grafico per ciascun valore numerico python distinto
function
crea un'istanza di un grafico separato per ogni set univoco di forme di input e tipi di dati .
Potrebbe essere necessario mappare un singolo oggetto tf.function su più grafici di calcolo sotto il cofano. Questo dovrebbe essere visibile solo come prestazione (i grafici di tracciamento hanno un costo di calcolo e di memoria diverso da zero )
Processori di dati di input : simile al precedente, il processore viene selezionato caso per caso, a seconda dei flag interni impostati in base alle configurazioni di runtime (modalità di esecuzione, formato dei dati, strategia di distribuzione). Il caso più semplice è con Eager, che funziona direttamente con gli array Numpy. Per alcuni esempi specifici, vedi questa risposta .
MISURA MODELLO, MISURA DATI:
- È decisivo; nessuna singola configurazione si incoronava in cima a tutte le dimensioni di modello e dati.
- La dimensione dei dati relativa alla dimensione del modello è importante; per dati e modello di piccole dimensioni, può prevalere il sovraccarico di trasferimento dati (ad es. da CPU a GPU). Allo stesso modo, i piccoli processori overhead possono funzionare più lentamente su dati di grandi dimensioni in base al tempo di conversione dei dati (vedere
convert_to_tensor
in "PROFILER")
- La velocità differisce per i diversi circuiti dei circuiti di treno e di input dei diversi gestori di dati per la gestione delle risorse.
BENCHMARKS : la carne macinata. - Documento Word - Foglio di calcolo Excel
Terminologia :
- % -less numeri sono tutti i secondi
- % calcolato come
(1 - longer_time / shorter_time)*100
; motivazione: siamo interessati a quale fattore uno è più veloce dell'altro; shorter / longer
è in realtà una relazione non lineare, non utile per il confronto diretto
- Determinazione del segno%:
- TF2 vs TF1:
+
se TF2 è più veloce
- GvE (Graph vs. Eager):
+
se Graph è più veloce
- TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5
PROFILER :
PROFILER - Spiegazione : Spyder 3.3.6 IDE profiler.
Alcune funzioni si ripetono nei nidi di altre; quindi, è difficile rintracciare l'esatta separazione tra le funzioni di "elaborazione dei dati" e di "addestramento", quindi ci saranno alcune sovrapposizioni, come si pronuncia nell'ultimo risultato.
% delle cifre calcolate runtime wrt meno tempo di costruzione
- Tempo di costruzione calcolato sommando tutti i runtime (unici) chiamati 1 o 2 volte
- Tempo del treno calcolato sommando tutti i runtime (unici) che sono stati chiamati lo stesso numero di volte del numero di iterazioni e alcuni dei runtime dei loro nidi
- Le funzioni sono profilate in base al loro nome originale , sfortunatamente (cioè
_func = func
verrà profilato come func
), che si mescola nel tempo di costruzione - da qui la necessità di escluderlo
TEST DELL'AMBIENTE :
- Codice eseguito in basso con attività in background minime in esecuzione
- La GPU è stata "riscaldata" con alcune iterazioni prima di temporizzare le iterazioni, come suggerito in questo post
- CUDA 10.0.130, cuDNN 7.6.0, TensorFlow 1.14.0 e TensorFlow 2.0.0 costruiti dalla sorgente, più Anaconda
- Python 3.7.4, Spyder 3.3.6 IDE
- GTX 1070, Windows 10, 24 GB di RAM DDR4 da 2,4 MHz, CPU i7-7700HQ da 2,8 GHz
METODOLOGIA :
- Benchmark 'piccolo', 'medio', e 'grande' modello e dimensioni dei dati
- Correzione del numero di parametri per ciascuna dimensione del modello, indipendentemente dalla dimensione dei dati di input
- Il modello "più grande" ha più parametri e livelli
- I dati "più grandi" hanno una sequenza più lunga, ma lo stesso
batch_size
enum_channels
- I modelli utilizzano solo
Conv1D
, Dense
strati 'apprendibili'; RNN evitati per implementazione della versione TF. differenze
- Funzionava sempre con un treno al di fuori del ciclo di benchmarking, per omettere la costruzione di grafici di ottimizzatori e modelli
- Non utilizzare dati sparsi (ad es.
layers.Embedding()
) O target sparsi (ad esSparseCategoricalCrossEntropy()
LIMITAZIONI : una risposta "completa" spiegherebbe ogni possibile ciclo del treno e iteratore, ma sicuramente oltre la mia capacità di tempo, busta paga inesistente o necessità generale. I risultati sono buoni quanto la metodologia: interpretare con una mente aperta.
CODICE :
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)