Ha sido citado por muchos usuarios como la razón para cambiar a Pytorch, pero todavía tengo que encontrar una justificación/explicación para sacrificar la calidad práctica más importante, la velocidad, por una ejecución entusiasta.
A continuación se muestra el rendimiento de la evaluación comparativa del código, TF1 frente a TF2, con TF1 ejecutándose entre un 47 % y un 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 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 Eager Execution según el código a continuación no ayuda. El comportamiento, sin embargo, es inconsistente: a veces la ejecución en modo gráfico ayuda considerablemente, otras veces se ejecuta más lentamente en relación con Eager.
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))
ACTUALIZACIÓN 17/8 30/2020 : TF 2.3 finalmente lo ha hecho: todos los casos se ejecutan tan rápido, o notablemente más rápido, que cualquier versión anterior.
Además, mi actualización anterior fue injusta para TF; mi GPU tuvo la culpa, se ha estado sobrecalentando últimamente. Si ve un diagrama de tallo ascendente de tiempos de iteración, es un síntoma confiable. Por último, consulte la nota de un desarrollador sobre Eager vs Graph .
Esta podría ser mi última actualización en esta respuesta. Las verdaderas estadísticas sobre la velocidad de su modelo solo las puede encontrar usted, en su dispositivo.
ACTUALIZACIÓN 19/05/2020 : TF 2.2, usando las mismas pruebas: solo una pequeña mejora en la velocidad de Eager. Gráficos para el caso train_on_batch
Large-Large Numpy a continuación, el eje x es iteraciones de ajuste sucesivas; mi GPU no está cerca de su capacidad máxima, así que dudo que se esté acelerando, pero las iteraciones se vuelven más lentas con el tiempo.
Según lo anterior, Graph y Eager son 1,56x y 1,97x más lentos que sus homólogos de TF1, respectivamente. No estoy seguro de que depure esto aún más, ya que estoy considerando cambiar a Pytorch por el escaso soporte de TensorFlow para la funcionalidad personalizada/de bajo nivel. Sin embargo, abrí un problema para recibir comentarios de los desarrolladores.
ACTUALIZACIÓN 18/02/2020 : He enviado a la banca 2.1 y 2.1 todas las noches; los resultados son mixtos. Todas las configuraciones menos una (modelo y tamaño de datos) son tan rápidas o mucho más rápidas que las mejores de TF2 y TF1. El que es más lento, y dramáticamente más lento, es Large-Large - esp. en la ejecución de gráficos ( 1.6x a 2.5x más lento ).
Además, existen diferencias de reproducibilidad extremas entre Graph y Eager para un modelo grande que probé, uno que no se puede explicar a través de la aleatoriedad/paralelismo computacional. Actualmente no puedo presentar un código reproducible para estos reclamos por limitaciones de tiempo, por lo que recomiendo probar esto para sus propios modelos.
Todavía no he abierto un problema de Git en estos, pero comenté sobre el original , aún no hay respuesta. Actualizaré la (s) respuesta (s) una vez que se realicen progresos.
VEREDICTO : no lo es , SI sabes lo que estás haciendo. Pero si no lo hace , podría costarle mucho, por unas pocas 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 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 aprendo algo; puedo marcar / "marcar" esta pregunta como referencia.
RESUMEN DEL PROBLEMA : según lo confirmado por un desarrollador de TensorFlow, Q. Scott Zhu, el desarrollo de TF2 se centró 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 gráfico. Beneficios: capacidades de procesamiento, distribución, depuración e implementación muy ampliadas. El costo de algunos de estos, sin embargo, 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:
keras
contra tf.keras
numpy
frente tf.data.Dataset
frente a...train_on_batch()
frente fit()
model(x)
frente model.predict(x)
frente a...Desafortunadamente, casi ninguno de los anteriores es independiente del otro, y cada uno puede al menos duplicar el tiempo de ejecución en relación con otro. Afortunadamente, puede determinar qué funcionará mejor de manera sistemática y con algunos atajos, como mostraré.
¿QUÉ TENGO QUE HACER? Actualmente, la única forma es experimentar con su modelo, datos y hardware específicos. Ninguna configuración única siempre funcionará mejor, pero hay cosas que se deben y no se deben hacer para simplificar su búsqueda:
>> HACER:
train_on_batch()
+ numpy
+ tf.keras
+ TF1 + Ansioso/Gráficotrain_on_batch()
+ numpy
+ tf.keras
+ TF2 + Gráficofit()
+ numpy
+ tf.keras
+ TF1/TF2 + Gráfico + modelo grande y datos>> NO:
fit()
+ numpy
+ keras
para modelos y datos pequeños y medianos
fit()
+ numpy
+ tf.keras
+ TF1/TF2 + Ansioso
train_on_batch()
+ numpy
+ keras
+ TF1 + Ansioso
[Mayor] tf.python.keras
; puede funcionar entre 10 y 100 veces más lento y con muchos errores; más información
layers
, models
, optimizers
e importaciones de uso "listas para usar" relacionadas; ops, utils y las importaciones 'privadas' relacionadas están bien, pero para estar seguro, verifique si hay alts y si se usan en tf.keras
Consulte el código en la parte inferior de mi otra respuesta para ver 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 los SÍ y NO SÍ anteriores:
Conv1D
y Dense
: sin RNN, datos/objetivos dispersos, entradas 4/5D y otras configuracionesnumpy
y tf.data.Dataset
, mientras que existen muchos otros formatos; ver otra respuesta¿Por qué TF2 sacrificó la cualidad más práctica, la velocidad, por una ejecución ansiosa? No lo ha hecho, claramente: el gráfico todavía está disponible. Pero si la pregunta es "¿por qué ansioso en absoluto":
.__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.¿CÓMO HABILITAR/DESHABILITAR EAGER?
tf.enable_eager_execution() # TF1; must be done before any model/tensor creation tf.compat.v1.disable_eager_execution() # TF2; above holds
Engañoso en TF2; ver aquí
INFORMACIÓN ADICIONAL :
_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, debe corregirse. Ver otra respuesta para más detalles.SOLICITUDES A TENSORFLOW DEVS :
train_on_batch()
y el aspecto de rendimiento de llamar a fit()
de forma iterativa; Los bucles de tren personalizados son importantes para muchos, especialmente para mí.AGRADECIMIENTOS : Gracias a
ACTUALIZACIONES :
14/11/19 : encontré un modelo (en mi aplicación real) que funciona más lento en TF2 para todas las configuraciones * con datos de entrada Numpy. Las diferencias oscilaron entre 13 y 19 %, con un promedio de 17 %. Las diferencias entre keras
y tf.keras
, sin embargo, fueron más dramáticas: 18-40% , promedio. 32% (tanto TF1 como 2). (* - excepto Eager, para el cual TF2 OOM'd)
17/11/19 : los desarrolladores actualizaron los métodos on_batch()
en una confirmación reciente , lo que indica que han mejorado la velocidad: se lanzará en TF 2.1, o estará disponible ahora como tf-nightly
. Como no puedo ejecutar este último, retrasaré la banca hasta 2.1.
20/02/20 : también vale la pena comparar el rendimiento de predicción; en TF2, por ejemplo, los tiempos de predicción de la CPU pueden implicar picos periódicos
ESTA RESPUESTA : tiene como objetivo proporcionar una descripción detallada del problema a nivel de gráfico/hardware, incluidos los bucles de tren TF2 frente a TF1, los procesadores de datos de entrada y las ejecuciones en modo Eager frente a Gráfico. Para obtener un resumen de problemas y pautas de resolución, consulte mi otra respuesta.
VEREDICTO DE RENDIMIENTO : a veces uno es más rápido, a veces el otro, dependiendo de la configuración. En lo que respecta a TF2 y TF1, están a la par en promedio, pero existen diferencias significativas basadas en la configuración, y TF1 supera a TF2 con más frecuencia que viceversa. Consulte "BENCHMARKING" a continuación.
CON ANSIAS VS. GRÁFICO : la esencia de toda esta respuesta para algunos: el entusiasmo 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 manera proactiva y se ejecuta cuando se le indica, 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, contrariamente a lo esperado. Lo que es en gran medida, es un gráfico ejecutado : esto incluye los pesos del modelo y del optimizador, que comprenden una gran parte del gráfico.
Eager reconstruye parte de su propio gráfico en la ejecución ; una consecuencia directa de que Graph no se ha construido por completo; consulte los resultados del generador de perfiles. Esto tiene una sobrecarga computacional.
Eager 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. Recorriendo el código fuente, las diferencias en el manejo de datos son claras; Eager pasa directamente a Numpy, mientras que Graph pasa tensores que luego se evalúan como Numpy; incierto del proceso exacto, pero este último debería implicar 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 van desde insignificantes hasta significativas, pero son consistentes. No estoy seguro de por qué es así: si un desarrollador de TF aclara, actualizará la respuesta.
TF2 vs. TF1 : citar partes relevantes de la respuesta de un desarrollador de TF, Q. Scott Zhu, con un poco de mi énfasis y reformulación:
En modo impaciente, el tiempo de ejecución debe ejecutar las operaciones y devolver el valor numérico para cada línea de código 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 algunos componentes comunes como la 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 de Python pura, en lugar de una función de Python envuelta en tf.function. Esto provocará 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 algunos gastos generales en la conversión del conjunto de datos , y creo que se trata de una sobrecarga de una sola vez, en lugar de un costo por lote.
Con la última oración del último párrafo anterior, y la última cláusula del siguiente párrafo:
Para superar la lentitud en el modo ansioso, tenemos @tf.function, que convertirá una función de Python en un gráfico. Cuando se alimenta un valor numérico como una matriz np, el cuerpo de la
tf.function
se convierte en un gráfico estático, se optimiza y devuelve el valor final, que es rápido y debería tener un rendimiento similar al del modo gráfico TF1.
No estoy de acuerdo, según los resultados de mi perfil, que muestran que el procesamiento de datos de entrada de Eager es sustancialmente más lento que el de Graph. Además, no estoy 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, la confirmación vinculada del desarrollador: una cantidad significativa de cambios para admitir los bucles de Keras v2 .
Train Loops : dependiendo de (1) Eager vs. Graph; (2) formato de datos de entrada, el entrenamiento continuará con un ciclo de entrenamiento distinto: en TF2, _select_training_loop()
, training.py , uno de los siguientes:
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 train loops, 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 sobre algunos de los diferentes métodos de ejecución:
A diferencia de otras operaciones de TensorFlow, no convertimos las entradas numéricas de Python en tensores. Además, se genera un nuevo gráfico para cada valor numérico distinto de Python
La
function
instancia un gráfico separado para cada conjunto único de formas de entrada y tipos de datos .
Es posible que un solo objeto
tf.function
deba asignarse a múltiples gráficos de cálculo debajo del capó. Esto debería ser visible solo como rendimiento (el seguimiento de gráficos tiene un costo computacional y de memoria distinto de cero )
Procesadores de datos de entrada : similar al anterior, el procesador se selecciona caso por caso, según 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 .
TAMAÑO DEL MODELO, TAMAÑO DE DATOS:
convert_to_tensor
en "PROFILER")REFERENCIAS : la carne picada. --Documento de Word -- Hoja de cálculo de Excel
Terminología :
(1 - longer_time / shorter_time)*100
; justificación: nos interesa qué factor uno es más rápido que el otro; shorter / longer
es en realidad una relación no lineal, no útil para la comparación directa+
si TF2 es más rápido+
si Graph es más rápidoPERFILADOR :
PERFILADOR - Explicación : generador de perfiles IDE de Spyder 3.3.6.
Algunas funciones se repiten en nidos de otras; por lo tanto, 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 indica en el último resultado.
Cifras porcentuales calculadas en tiempo de ejecución menos el tiempo de compilación
El tiempo de compilación se calcula sumando todos los tiempos de ejecución (únicos) que se llamaron 1 o 2 veces
Tiempo de tren calculado sumando todos los tiempos de ejecución (únicos) que se llamaron el mismo número de veces que el número de iteraciones, y algunos de los tiempos de ejecución de sus nidos
Desafortunadamente, las funciones se perfilan de acuerdo con sus nombres originales (es decir, _func = func
se perfilará como func
), lo que se mezcla en el tiempo de compilación, de ahí la necesidad de excluirlo
ENTORNO DE PRUEBA :
METODOLOGIA :
batch_size
y número de num_channels
Conv1D
, capas Dense
'aprendibles'; RNN evitados por implemento de versión TF. diferenciaslayers.Embedding()
) u objetivos dispersos (p. ej SparseCategoricalCrossEntropy()
LIMITACIONES : una respuesta "completa" explicaría cada ciclo de tren e iterador posibles, 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 == 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)