python - ¿Por qué es TensorFlow 2 mucho más lento que TensorFlow 1?
keras performance-testing (2)
Muchos usuarios lo han citado como la razón para cambiar a Pytorch, pero aún no he encontrado una justificación / explicación para sacrificar la calidad práctica más importante, la velocidad, para una ejecución ansiosa.
A continuación se muestra el rendimiento de la evaluación comparativa de código, TF1 frente a TF2, con TF1 ejecutándose en cualquier lugar de 47% a 276% más rápido .
Mi pregunta es: ¿qué es, a nivel gráfico o de hardware, lo que produce una desaceleración tan significativa?
Buscando una respuesta detallada, ya estoy familiarizado con los conceptos generales. Git relevante
Especificaciones : CUDA 10.0.130, cuDNN 7.4.2, Python 3.7.4, Windows 10, GTX 1070
Resultados de referencia :
ACTUALIZACIÓN : Deshabilitar la Ejecución ansiosa por el siguiente código no ayuda. Sin embargo, el comportamiento es inconsistente: a veces, ejecutar en modo gráfico ayuda considerablemente, otras veces se ejecuta más lentamente en relación con Eager.
Como los desarrolladores de TF no aparecen en ningún lado, investigaré este asunto yo mismo, puedo seguir el progreso en el problema de Github vinculado.
ACTUALIZACIÓN 2 : toneladas de resultados experimentales para compartir, junto con explicaciones; debe hacerse hoy.
Código de referencia :
# 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)
Funciones utilizadas :
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))
ESTA RESPUESTA : tiene como objetivo proporcionar una descripción detallada del problema a nivel de gráfico / hardware, incluyendo bucles de tren TF2 vs. TF1, procesadores de datos de entrada y ejecuciones en modo Eager vs. Graph. Para un resumen del problema y pautas de resolución, vea mi otra respuesta.
VEREDICTO DE DESEMPEÑO : a veces uno es más rápido, a veces el otro, dependiendo de la configuración. En lo que respecta a TF2 vs TF1, están a la par en promedio, pero existen diferencias significativas basadas en la configuración, y TF1 supera a TF2 más que viceversa. Ver "BENCHMARKING" a continuación.
EAGER VS. GRÁFICO : la carne de esta respuesta completa para algunos: el deseo de TF2 es más lento que el de TF1, según mis pruebas. Detalles más abajo.
La diferencia fundamental entre los dos es: Graph configura una red computacional de forma proactiva y se ejecuta cuando se le dice que lo haga, mientras que Eager ejecuta todo en el momento de la creación. Pero la historia solo comienza aquí:
-
Eager no está desprovisto de Graph , y de hecho puede ser principalmente Graph, al contrario de lo esperado. Lo que es en gran parte es el gráfico ejecutado : esto incluye pesos de modelo y optimizador, que comprenden una gran parte del gráfico.
-
Eager reconstruye parte del propio gráfico en la ejecución ; consecuencia directa de que Graph no se construyó por completo; consulte los resultados del generador de perfiles. Esto tiene una sobrecarga computacional.
-
Ansioso es más lento con entradas Numpy ; según este comentario y código de Git , las entradas de Numpy en Eager incluyen el costo general de copiar tensores de la CPU a la GPU. Al recorrer el código fuente, las diferencias en el manejo de datos son claras; Eager pasa directamente a Numpy, mientras que Graph pasa a los tensores que luego evalúan a Numpy; incierto del proceso exacto, pero este último debe incluir optimizaciones a nivel de GPU
-
TF2 Eager es más lento que TF1 Eager : esto es ... inesperado. Vea los resultados de la evaluación comparativa a continuación. Las diferencias abarcan desde insignificante a significativo, pero son consistentes. No estoy seguro de por qué es así: si un desarrollador de TF aclara, actualizará la respuesta.
TF2 vs. TF1 : citando porciones relevantes de la confirmed de un desarrollador de TF, Q. Scott Zhu, con un poco de mi énfasis y redacción:
En impaciente, el tiempo de ejecución necesita ejecutar las operaciones y devolver el valor numérico para cada línea de código de Python. La naturaleza de la ejecución de un solo paso hace que sea lenta .
En TF2, Keras aprovecha tf.function para construir su gráfico para entrenamiento, evaluación y predicción. Los llamamos "función de ejecución" para el modelo. En TF1, la "función de ejecución" era un FuncGraph, que compartía algún componente común como función TF, pero tenía una implementación diferente.
Durante el proceso, de alguna manera dejamos una implementación incorrecta para train_on_batch (), test_on_batch () y predict_on_batch () . Todavía son numéricamente correctos , pero la función de ejecución para x_on_batch es una función pura de python, en lugar de una función de python envuelta en función tf.function. Esto causará lentitud
En TF2, convertimos todos los datos de entrada en un tf.data.Dataset, mediante el cual podemos unificar nuestra función de ejecución para manejar el tipo único de las entradas. Puede haber una sobrecarga en la conversión del conjunto de datos , y creo que esta es una sobrecarga única, en lugar de un costo por lote
Con la última oración del último párrafo anterior y la última cláusula del párrafo siguiente:
Para superar la lentitud en el modo ansioso, tenemos @ tf.function, que convertirá una función de Python en un gráfico. Cuando se introduce un valor numérico como la matriz np, el cuerpo de la función tf se convierte en un gráfico estático, se optimiza y devuelve el valor final, que es rápido y debe tener un rendimiento similar al modo gráfico TF1.
No estoy de acuerdo, según mis resultados de perfil, que muestran que el procesamiento de datos de entrada de Eager es sustancialmente más lento que el de Graph.
Además, no
tf.data.Dataset
seguro acerca de
tf.data.Dataset
en particular, pero Eager llama repetidamente a varios de los mismos métodos de conversión de datos; consulte el generador de perfiles.
Por último, el compromiso vinculado del desarrollador: número significativo de cambios para admitir los bucles Keras v2 .
Train Loops
: dependiendo de (1) Eager vs. Graph;
(2) formato de datos de entrada, el entrenamiento continuará con un ciclo de tren distinto: en TF2,
_select_training_loop()
,
training.py
, uno 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
Cada uno maneja la asignación de recursos de manera diferente y tiene consecuencias en el rendimiento y la capacidad.
Train Loops:
fit
vs
train_on_batch
,
keras
vs.
tf.keras
: cada uno de los cuatro usa diferentes bucles de tren, aunque quizás no en todas las combinaciones posibles.
keras
''
fit
, por ejemplo, usa una forma de
fit_loop
, por ejemplo,
training_arrays.fit_loop()
, y su
train_on_batch
puede usar
K.function()
.
tf.keras
tiene una jerarquía más sofisticada descrita en parte en la sección anterior.
Train Loops: documentación - cadena de documentación fuente relevante en algunos de los diferentes métodos de ejecución:
A diferencia de otras operaciones de TensorFlow, no convertimos entradas numéricas de python en tensores. Además, se genera un nuevo gráfico para cada valor numérico de Python distinto
function
crea una instancia de un gráfico separado para cada conjunto único de formas de entrada y tipos de datos .Un solo objeto tf.function podría necesitar mapearse a múltiples gráficos de cálculo debajo del capó. Esto debería ser visible solo como rendimiento (los gráficos de seguimiento tienen un costo computacional y de memoria distinto de cero )
Procesadores de datos de entrada : similar al anterior, el procesador se selecciona caso por caso, dependiendo de los indicadores internos establecidos de acuerdo con las configuraciones de tiempo de ejecución (modo de ejecución, formato de datos, estrategia de distribución). El caso más simple es con Eager, que funciona directamente con matrices Numpy. Para algunos ejemplos específicos, vea esta respuesta .
BENCHMARKS : la carne picada. - Documento de Word - Hoja de cálculo de Excel
Terminología :
- Los números sin% son todos segundos
-
% calculado como
(1 - longer_time / shorter_time)*100
; justificación: nos interesa qué factor es uno más rápido que el otro;shorter / longer
es en realidad una relación no lineal, no es útil para la comparación directa -
% de determinación de signos: TF2 vs TF1:
+
es TF2 es más rápido; GvE (Graph vs. Eager):+
si Graph es más rápido - TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5
PERFIL :
ENTORNO DE PRUEBA :
- Código ejecutado en la parte inferior con tareas mínimas en segundo plano en ejecución
- La GPU se "calentó" con algunas iteraciones antes de las iteraciones de tiempo, como se sugiere en esta publicación
- CUDA 10.0.130, cuDNN 7.6.0, TensorFlow 1.14.0 y TensorFlow 2.0.0 creados desde la fuente, más Anaconda
- Python 3.7.4, Spyder 3.3.6 IDE
- GTX 1070, Windows 10, 24GB DDR4 RAM de 2.4 MHz, CPU i7-7700HQ 2.8-GHz
Metodología
- Benchmark ''pequeño'', ''mediano'' y ''grande'' de modelos y tamaños de datos
- Se corrigió el número de parámetros para cada tamaño de modelo, independientemente del tamaño de los datos de entrada
- El modelo "más grande" tiene más parámetros y capas
-
Los datos "más grandes" tienen una secuencia más larga, pero el mismo
batch_size
ynum_channels
-
Los modelos solo usan
Conv1D
,Conv1D
Dense
''aprendibles''; RNNs evitados por implem versión TF. diferencias - Siempre ejecutó un ajuste de tren fuera del ciclo de evaluación comparativa, para omitir la construcción del modelo y del optimizador
-
No utilizar datos dispersos (por ejemplo,
layers.Embedding()
) u objetivos dispersos (por ejemplo,SparseCategoricalCrossEntropy()
- Perfilador : utiliza el perfilador de Spyder. Como algunas funciones se repiten en los nidos de otras, es difícil rastrear la separación exacta entre las funciones de "procesamiento de datos" y "entrenamiento", por lo que habrá cierta superposición, como se manifestó en el último resultado.
LIMITACIONES : una respuesta "completa" explicaría todos los posibles bucles e iteradores de trenes, pero eso seguramente está más allá de mi capacidad de tiempo, cheque de pago inexistente o necesidad general. Los resultados son tan buenos como la metodología: interprete con una mente abierta.
CÓDIGO
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)
VEREDICTO : no lo es , SI sabes lo que estás haciendo. Pero si no lo hace , podría costarle mucho: por un par de actualizaciones de GPU en promedio, y por múltiples GPU en el peor de los casos.
ESTA RESPUESTA : tiene como objetivo proporcionar una descripción de alto nivel del problema, así como pautas sobre cómo decidir sobre la configuración de capacitación específica para sus necesidades. Para obtener una descripción detallada de bajo nivel, que incluye todos los resultados de evaluación comparativa + código utilizado, consulte mi otra respuesta.
Actualizaré mi (s) respuesta (s) con más información si descubro que puede marcar / "marcar" esta pregunta como referencia.
RESUMEN DEL PROBLEMA : según lo confirmed por un desarrollador de TensorFlow, Q. Scott Zhu, TF2 centró el desarrollo en la ejecución Eager y la estrecha integración con Keras, lo que implicó cambios radicales en la fuente de TF, incluso a nivel de gráfico. Beneficios: capacidades ampliadas de procesamiento, distribución, depuración e implementación. Sin embargo, el costo de algunos de estos es la velocidad.
El asunto, sin embargo, es bastante más complejo. No se trata solo de TF1 frente a TF2: los factores que producen diferencias significativas en la velocidad del tren incluyen:
- TF2 vs. TF1
- Modo ansioso contra gráfico
-
keras
vs.tf.keras
-
numpy
vs.tf.data.Dataset
vs. -
train_on_batch()
vs.fit()
- GPU vs. CPU
Desafortunadamente, casi ninguno de los anteriores es independiente del otro, y cada uno puede al menos duplicar el tiempo de ejecución con respecto al otro. Afortunadamente, puedes determinar qué funcionará mejor de manera sistemática y con algunos atajos, como mostraré.
¿QUÉ TENGO QUE HACER? Actualmente, la única forma es: experimente con su modelo, datos y hardware específicos. Ninguna configuración única siempre funcionará mejor, pero hay que hacer y no hacer para simplificar su búsqueda:
>> HACER:
-
train_on_batch()
+numpy
+tf.keras
+ TF1 + Eager / Graph -
train_on_batch()
+numpy
+tf.keras
+ TF2 + Graph -
fit()
+numpy
+tf.keras
+ TF1 / TF2 + Graph + modelo grande y datos
>> NO:
-
fit()
+numpy
+keras
para modelos y datos pequeños y medianos -
fit()
+numpy
+tf.keras
+ TF1 / TF2 + Eager -
train_on_batch()
+numpy
+keras
+ TF1 + Eager -
[Mayor]
tf.python.keras
; puede correr 10-100x más lento y con muchos errores; más información-
Esto incluye
layers
,models
,optimizers
e importaciones relacionadas de uso "listo para usar"; las operaciones, las utilidades y las importaciones ''privadas'' relacionadas están bien, pero para estar seguro, verifique si hay alternativas y si se usan entf.keras
-
Esto incluye
Consulte el código en la parte inferior de mi otra respuesta para un ejemplo de configuración de evaluación comparativa. La lista anterior se basa principalmente en las tablas "BENCHMARKS" en la otra respuesta.
LIMITACIONES de lo que HACER y NO HACER:
- Esta pregunta se titula "¿Por qué TF2 es mucho más lento que TF1?", Y aunque su cuerpo se refiere al entrenamiento explícitamente, el asunto no se limita a eso; la inferencia también está sujeta a grandes diferencias de velocidad, incluso dentro de la misma versión TF, importación, formato de datos, etc. - vea esta respuesta .
- Es probable que los RNN cambien notablemente la cuadrícula de datos en la otra respuesta, ya que se han mejorado en TF2
-
Los modelos utilizan principalmente
Conv1D
yDense
: sin RNN, datos / objetivos dispersos, entradas 4 / 5D y otras configuraciones -
Datos de entrada limitados a
numpy
ytf.data.Dataset
, mientras que existen muchos otros formatos; ver otra respuesta - Se usó GPU; Los resultados diferirán en una CPU. De hecho, cuando hice la pregunta, mi CUDA no estaba configurado correctamente y algunos de los resultados estaban basados en la CPU.
¿Por qué TF2 sacrificó la calidad más práctica, la velocidad, para una ejecución ansiosa? No lo ha hecho, claramente, el gráfico todavía está disponible. Pero si la pregunta es "¿por qué ansioso?":
-
Depuración superior
: es probable que haya encontrado una multitud de preguntas que preguntan "¿cómo obtengo resultados de capa intermedia" o "cómo inspecciono los pesos"?
con entusiasmo, es (casi) tan simple como
.__dict__
. Graph, por el contrario, requiere familiaridad con funciones especiales de back-end, lo que complica enormemente todo el proceso de depuración e introspección. - Creación de prototipos más rápida : por ideas similares a las anteriores; comprensión más rápida = más tiempo restante para la DL real.
¿CÓMO HABILITAR / DESACTIVAR EAGER?
tf.enable_eager_execution() # TF1; must be done before any model/tensor creation
tf.compat.v1.disable_eager_execution() # TF2; above holds
INFORMACIÓN ADICIONAL :
-
Cuidado con los métodos
_on_batch()
en TF2; de acuerdo con el desarrollador de TF, todavía usan una implementación más lenta, pero no intencionalmente , es decir, se debe solucionar. Ver otra respuesta para más detalles.
SOLICITUDES DE DISPOSITIVOS DE FLUJO TENSOR :
-
train_on_batch()
y el aspecto de rendimiento de llamar afit()
iterativa; los bucles de trenes personalizados son importantes para muchos, especialmente para mí. - Agregue documentación / mención de documentación de estas diferencias de rendimiento para el conocimiento de los usuarios.
- Mejore la velocidad de ejecución general para evitar que los píos salten a Pytorch.
AGRADECIMIENTOS : Gracias a
- P. Scott Zhu, desarrollador de TensorFlow, por su confirmed sobre el asunto.
- P. Andrey por compartir pruebas útiles y debates.