ESTA RESPOSTA : tem como objetivo fornecer uma descrição detalhada do problema em nível de gráfico / hardware - incluindo loops de trem TF2 vs. TF1, processadores de dados de entrada e execuções no modo Ansioso vs. Gráfico. Para um resumo do problema e diretrizes de resolução, consulte minha outra resposta.
VERDITO DE DESEMPENHO : às vezes um é mais rápido, às vezes o outro, dependendo da configuração. No que diz respeito ao TF2 vs TF1, eles estão em pé de igualdade, em média, mas existem diferenças significativas baseadas na configuração, e o TF1 supera o TF2 com mais frequência do que vice-versa. Veja "BENCHMARKING" abaixo.
EAGER VS. GRÁFICO : a carne de toda essa resposta para alguns: o desejo do TF2 é mais lento que o do TF1, de acordo com meus testes. Detalhes mais abaixo.
A diferença fundamental entre os dois é: o Graph configura uma rede computacional de forma proativa e é executado quando 'solicitado' - enquanto o Eager executa tudo na criação. Mas a história só começa aqui:
Ansioso NÃO é desprovido de Graph , e pode de fato ser principalmente Graph, contrário à expectativa. O que é em grande parte, é executado Graph - isso inclui pesos de modelo e otimizador, compreendendo grande parte do gráfico.
Ansioso reconstrói parte do próprio gráfico na execução ; conseqüência direta do Graph não estar totalmente construído - veja os resultados do criador de perfil. Isso tem uma sobrecarga computacional.
Ansioso é mais lento com entradas Numpy ; de acordo com esse comentário e código do Git , as entradas do Numpy no Eager incluem o custo adicional de copiar tensores da CPU para a GPU. Percorrendo o código fonte, as diferenças de manipulação de dados são claras; Ansioso passa diretamente o Numpy, enquanto o Graph passa os tensores que são avaliados para o Numpy; incerto do processo exato, mas este último deve envolver otimizações no nível da GPU
TF2 Eager é mais lento que TF1 Eager - isso é ... inesperado. Veja os resultados do benchmarking abaixo. As diferenças variam de insignificante a significante, mas são consistentes. Não sei por que é esse o caso - se um desenvolvedor do TF esclarecer, atualizará a resposta.
TF2 vs. TF1 : citando partes relevantes da resposta de um desenvolvedor de TF, Q. Scott Zhu, com um pouco da minha ênfase e reformulação:
No aguardo, o tempo de execução precisa executar as operações e retornar o valor numérico para cada linha de código python. A natureza da execução de uma única etapa faz com que seja lenta .
No TF2, o Keras utiliza o tf.function para criar seu gráfico para treinamento, avaliação e previsão. Nós os chamamos de "função de execução" para o modelo. No TF1, a "função de execução" era um FuncGraph, que compartilhava algum componente comum como função TF, mas tem uma implementação diferente.
Durante o processo, deixamos de alguma forma uma implementação incorreta para train_on_batch (), test_on_batch () e predict_on_batch () . Eles ainda estão numericamente corretos , mas a função de execução para x_on_batch é uma função python pura, em vez de uma função python empacotada tf.function. Isso causará lentidão
No TF2, convertemos todos os dados de entrada em um tf.data.Dataset, pelo qual podemos unificar nossa função de execução para lidar com o tipo único de entradas. Pode haver alguma sobrecarga na conversão do conjunto de dados , e acho que essa é uma sobrecarga única, em vez de um custo por lote
Com a última frase do último parágrafo acima e a última cláusula do parágrafo abaixo:
Para superar a lentidão no modo ansioso, temos @ tf.function, que transformará uma função python em um gráfico. Ao alimentar valor numérico como matriz np, o corpo da função tf.f. é convertido em gráfico estático, sendo otimizado e retorna o valor final, que é rápido e deve ter desempenho semelhante ao modo gráfico TF1.
Discordo - de acordo com meus resultados de criação de perfil, que mostram que o processamento de dados de entrada do Eager é substancialmente mais lento que o do Graph. Além disso, não tem certeza sobre isso tf.data.Dataset
em particular, mas o Eager chama repetidamente vários dos mesmos métodos de conversão de dados - consulte o profiler.
Por fim, o commit vinculado do desenvolvedor: número significativo de alterações para dar suporte aos loops do Keras v2 .
Loops de trem : dependendo de (1) Ansioso vs. Gráfico; (2) formato de dados de entrada, a formação em prosseguirá com um laço trem distintas - em TF2, _select_training_loop()
, training.py , um dos seguintes:
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 um deles lida com a alocação de recursos de maneira diferente e tem conseqüências no desempenho e na capacidade.
Loops de trem: fit
vs train_on_batch
, keras
vstf.keras
.: cada um dos quatro usa loops de trem diferentes, embora talvez não em todas as combinações possíveis. keras
' fit
, por exemplo, usa uma forma de fit_loop
, por exemplo training_arrays.fit_loop()
, e train_on_batch
pode ser usada K.function()
. tf.keras
possui uma hierarquia mais sofisticada descrita em parte na seção anterior.
Loops de treinamento: documentação - documentação relevante da fonte sobre alguns dos diferentes métodos de execução:
Diferentemente de outras operações do TensorFlow, não convertemos entradas numéricas python em tensores. Além disso, um novo gráfico é gerado para cada valor numérico python distinto
function
instancia um gráfico separado para cada conjunto exclusivo de formas e tipos de dados de entrada .
Um único objeto tf.function pode precisar mapear para vários gráficos de computação sob o capô. Isso deve ser visível apenas como desempenho (os gráficos de rastreamento têm um custo computacional e de memória diferente de zero )
Processadores de dados de entrada : semelhante ao acima, o processador é selecionado caso a caso, dependendo dos sinalizadores internos definidos de acordo com as configurações de tempo de execução (modo de execução, formato de dados, estratégia de distribuição). O caso mais simples é o Eager, que funciona diretamente com matrizes Numpy. Para alguns exemplos específicos, consulte esta resposta .
TAMANHO DO MODELO, TAMANHO DOS DADOS:
- É decisivo; nenhuma configuração única se destacava no topo de todos os tamanhos de modelo e de dados.
- O tamanho dos dados em relação ao tamanho do modelo é importante; para dados e modelos pequenos, a transferência de dados (por exemplo, CPU para GPU) pode dominar. Da mesma forma, pequenos processadores aéreos podem rodar mais devagar em grandes dados por tempo de conversão de dados dominante (consulte
convert_to_tensor
"PROFILER")
- A velocidade varia de acordo com os diferentes meios de processamento dos recursos dos loops de trem e dos processadores de dados de entrada.
REFERÊNCIAS : a carne moída. - Documento do Word - Planilha do Excel
Terminologia :
- % sem números são todos os segundos
- % calculado como
(1 - longer_time / shorter_time)*100
; lógica: estamos interessados em qual fator um é mais rápido que o outro; shorter / longer
é na verdade uma relação não linear, não é útil para comparação direta
- Determinação do sinal de%:
- TF2 vs TF1:
+
se o TF2 for mais rápido
- GvE (Gráfico vs. Ansioso):
+
se o Gráfico for mais rápido
- TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5
PROFILER :
PROFILER - Explicação : Spyder 3.3.6 IDE profiler.
Algumas funções são repetidas em ninhos de outras; portanto, é difícil rastrear a separação exata entre as funções "processamento de dados" e "treinamento", para que haja alguma sobreposição - conforme pronunciado no último resultado.
% de números calculados em tempo de execução wrt menos tempo de compilação
- Tempo de construção calculado somando todos os tempos de execução (únicos) que foram chamados 1 ou 2 vezes
- Tempo de trem calculado somando todos os tempos de execução (únicos) que foram chamados o mesmo número de vezes que o número de iterações e alguns dos tempos de execução de seus ninhos
- Infelizmente, as funções são definidas de acordo com seus nomes originais (ou seja,
_func = func
terão o perfil como func
), o que se mistura no tempo de construção - daí a necessidade de excluí-lo
AMBIENTE DE TESTE :
- Código executado na parte inferior com o mínimo de tarefas em segundo plano em execução
- A GPU foi "aquecida" com algumas iterações antes de cronometrar as iterações, conforme sugerido nesta postagem
- CUDA 10.0.130, cuDNN 7.6.0, TensorFlow 1.14.0 e TensorFlow 2.0.0 criados a partir da fonte, além do Anaconda
- Python 3.7.4, Spyder 3.3.6 IDE
- GTX 1070, Windows 10, 24 GB DDR4 2,4 MHz RAM, i7-7700HQ CPU de 2,8 GHz
METODOLOGIA :
- Modelo de referência 'pequeno', 'médio' e 'grande' e tamanhos de dados
- Corrija o número de parâmetros para cada tamanho de modelo, independentemente do tamanho dos dados de entrada
- O modelo "maior" possui mais parâmetros e camadas
- Dados "maiores" têm uma sequência mais longa, mas o mesmo
batch_size
enum_channels
- Modelos só uso
Conv1D
, Dense
camadas 'learnable'; RNNs evitados por implemento da versão TF. diferenças
- Sempre executou um ajuste de trem fora do loop de benchmarking, para omitir a construção de gráficos do modelo e do otimizador
- Não usar dados esparsos (por exemplo
layers.Embedding()
) ou destinos esparsos (por exemploSparseCategoricalCrossEntropy()
LIMITAÇÕES : uma resposta "completa" explicaria todos os circuitos e iteradores de trem possíveis, mas isso certamente está além da minha capacidade de tempo, salário inexistente ou necessidade geral. Os resultados são tão bons quanto a metodologia - interpretem com a mente aberta.
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)