为什么TensorFlow 2比TensorFlow 1慢得多?

时间:2019-10-17 22:28:33

标签: python tensorflow keras performance-testing tensorflow2.0

许多用户认为它是切换到Pytorch的原因,但是我还没有找到牺牲/最渴望的实用质量,速度和渴望执行的理由/解释。

下面是TF1与TF2的代码基准测试性能-TF1的运行速度 47%到276%

我的问题是:在图形或硬件级别上,什么导致如此显着的下降?


寻找详细的答案-已经熟悉广泛的概念。 Relevant Git

规格:CUDA 10.0.130,cuDNN 7.4.2,Python 3.7.4,Windows 10,GTX 1070


基准测试结果


更新对以下代码禁用急切执行。但是,这种行为是不一致的:有时在图形模式下运行会有很大帮助,而其他时候相对于Eager而言,运行速度更慢

由于TF开发人员没有出现在任何地方,我将亲自调查此事-可以跟踪相关的Github问题的进展。

更新2 :分享了大量的实验结果以及相关说明;应该在今天完成。


基准代码

# 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)

使用的功能

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))

2 个答案:

答案 0 :(得分:51)

VERDICT :如果您知道自己在做什么,就不是。但是,如果您,则可能会花费大量成本-平均而言要升级几次GPU,在最坏的情况下要花费多个GPU。


此答案:旨在提供对该问题的高级描述,以及有关如何根据您的需求决定培训配置的准则。有关详细的低级描述(包括所有基准测试结果和所使用的代码),请参阅我的其他答案。

如果我学到任何信息,我将在更多信息的基础上更新我的答案-可以为该问题添加书签/“加上星号”以供参考。


问题摘要:由TensorFlow开发人员Q.Scott Zhu作为confirmed,TF2专注于Eager执行和带有Keras的紧密集成的开发,这涉及到TF源的全面更改-包括图形级。好处:大大扩展了处理,分发,调试和部署功能。但是,其中一些成本是速度。

但是,这个问题要复杂得多。不仅仅是TF1和TF2-导致火车速度明显不同的因素包括:

  1. TF2与TF1
  2. 渴望与图表模式
  3. kerastf.keras
  4. numpytf.data.Dataset与...
  5. train_on_batch()fit()
  6. GPU与CPU

不幸的是,以上几乎没有一个是彼此独立的,并且每个相对于另一个可以至少使执行时间加倍。幸运的是,您可以确定哪些是系统上最有效的方法,并有一些捷径-如我所展示的。


我应该做什么?目前,唯一的方法是-针对您的特定型号,数据和硬件进行实验。没有一个单一的配置总能达到最佳效果-但可以简化和简化 的操作:

>>,请执行以下操作:

  • train_on_batch() + numpy + tf.keras + TF1 +渴望/图形
  • train_on_batch() + numpy + tf.keras + TF2 +图形
  • fit() + numpy + tf.keras + TF1 / TF2 +图形+大型模型和数据

>>不要:

  • fit() + numpy + keras用于中小型模型和数据
  • fit() + numpy + tf.keras + TF1 / TF2 +渴望
  • train_on_batch() + numpy + keras + TF1 +渴望

  • [主要] tf.python.keras;它的运行速度可以降低10到100倍,并且带有许多错误; more info

    • 这包括layersmodelsoptimizers和相关的“即用型”用法导入; ops,utils和相关的“私有”导入都可以-但可以肯定的是,请检查alt以及它们是否在tf.keras中使用

有关基准测试设置示例,请参阅其他答案底部的代码。上面的列表主要基于其他答案中的“ BENCHMARKS”表。


局限性的上述事项:

  • 这个问题的标题是“为什么TF2比TF1慢得多?”,尽管它的主体明确地涉及训练,但问题并不局限于此。 推断也会受到主要速度差异的影响,在同一TF版本,导入,数据格式等范围内,偶数 –参见this answer
  • 由于TF2中的改进,RNN可能会明显改变另一个答案中的数据网格。
  • 主要用于Conv1DDense的模型-没有RNN,稀疏数据/目标,4 / 5D输入和其他配置
  • 输入数据限制为numpytf.data.Dataset,同时存在许多其他格式;查看其他答案
  • 使用了GPU;结果 在CPU上会有所不同。实际上,当我问这个问题时,我的CUDA配置不正确,并且某些结果是基于CPU的。

为什么TF2为了急切的执行而牺牲了最实用的质量,速度?显然,它还没有提供。但是如果问题是“为什么要渴望”:

  • 高级调试:您可能会遇到许多问题,询问“如何获得中间层输出”或“如何检查权重”;渴望,它(几乎)像.__dict__一样简单。相比之下,Graph需要熟悉特殊的后端功能-极大地增加了调试和自省的整个过程。
  • 快速原型制作:根据与上述类似的想法进行;更快的理解=剩下更多的时间用于实际DL。

如何启用/禁用EAGER?

tf.enable_eager_execution()  # TF1; must be done before any model/tensor creation
tf.compat.v1.disable_eager_execution() # TF2; above holds

其他信息

  • 注意TF2中的_on_batch()方法;根据TF开发人员的说法,他们仍然使用较慢的实现方式,但不是故意的-即必须解决。有关详细信息,请参见其他答案。

张力流需求

  1. 请修复train_on_batch(),并修复迭代调用fit()的性能;定制火车循环对许多人尤其是我来说很重要。
  2. 添加有关这些性能差异的文档/文档字符串,以供用户了解。
  3. 提高总体执行速度,以防止窥视现象转移到Pytorch。

致谢:感谢


更新

  • 11/14/19 -找到了一个模型(在我的实际应用中),对于所有*配置,该模型在TF2 上运行速度较慢,带有多个输入数据。差异范围为13-19%,平均为17%。但是,kerastf.keras之间的差异更为明显: 18-40%。 32%(TF1和2)。 (*-Eager除外,为此需要TF2 OOM)

  • 11/17/19 -开发人员在recent commit中更新了on_batch()方法,表明速度有所提高-将在TF 2.1中发布或可用现在为tf-nightly。由于我无法让后者运行,因此将替补席推迟到2.1。

答案 1 :(得分:35)

此答案:旨在提供详细的图形/硬件级别的问题描述-包括TF2 vs. TF1训练循环,输入数据处理器以及Eager vs. Graph模式执行。有关问题摘要和解决方案的指南,请参见我的其他答案。


PERFORMANCE VERDICT (性能验证):根据配置,有时一个更快,有时另一个更快。就TF2与TF1而言,它们的平均水平差不多,但是确实存在基于配置的重大差异,并且TF1比TF2更为常见。请参见下面的“ Benchmarking”。


EAGERVS。 GRAPH :这可以说是完整答案的关键:根据我的测试,TF2的渴望比TF1的渴望。细节进一步。

两者之间的根本区别是:Graph主动设置计算网络 ,并在“提示”时执行-而Eager在创建时执行所有操作。但是故事只从这里开始:

  • 热切并非没有图,实际上可能是主要是图,与预期相反。它主要是执行图-这包括模型和优化器权重,占图的很大一部分。

  • 热切的人在执行时重建了自己图的一部分; Graph未完全构建的直接结果-请参阅分析器结果。这会产生计算开销。

  • 急于输入的输入变慢;根据{{​​3}}和代码,Eager中的Numpy输入包括将张量从CPU复制到GPU的开销成本。单步执行源代码,数据处理差异显而易见;渴望直接通过Numpy,而图则通过张量,然后求和为Numpy。不确定确切的过程,但后者应涉及GPU级别的优化

  • TF2 Eager比TF1 Eager慢 -这是意外的。请参阅下面的基准测试结果。差异从可以忽略不计到显着,但是是一致的。不确定为什么会这样-如果TF开发人员澄清了,将会更新答案。


TF2与TF1 :引用TF开发人员Q.Scott Zhu的相关部分,this Git comment-带有我的强调和改写:

  

急切地,运行时需要执行ops并返回每行python代码的数值。 单步执行的性质会使其变慢

     

在TF2中,Keras利用tf.function构建其图形进行训练,评估和预测。我们称它们为模型的“执行功能”。在TF1中,“执行功能”是FuncGraph,它与TF功能共享一些公共组件,但是实现方式不同。

     

在此过程中,我们以某种方式为train_on_batch(),test_on_batch()和预报_on_batch()保留了错误的实现方式。它们仍然在数值上正确,但是x_on_batch的执行函数是纯python函数,而不是tf.function包装的python函数。这将导致速度缓慢

     

在TF2中,我们将所有输入数据转换为tf.data.Dataset,通过该数据集,我们可以统一执行函数来处理单一类型的输入。数据集转换中可能会有一些开销,我认为这是一次性的开销,而不是每次批处理成本

带有上面最后一段的最后一句话,下面一段的最后一句:

  

为了克服急切模式下的缓慢性,我们提供了@ tf.function,它将把python函数转换为图形。当像np数组一样输入数值时,tf.function的主体将转换为静态图,进行优化,并返回最终值,该值很快,并且应具有与TF1图形模式相似的性能。

我不同意-根据我的分析结果,该结果表明Eager的输入数据处理比Graph的处理要慢得多。另外,尤其不确定tf.data.Dataset,但Eager确实反复调用了多个相同的数据转换方法-请参阅事件探查器。

最后,开发者的链接提交:response


火车循环:取决于(1)渴望与图表; (2)输入数据格式,训练将在一个独特的训练循环中进行-在TF2,_select_training_loop()Significant number of changes to support the Keras v2 loops中进行,

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

每个人以不同的方式处理资源分配,并对性能和功能造成影响。


火车循环:fittrain_on_batchkerastf.keras :这四个火车分别使用了不同的火车循环,尽管可能不同在所有可能的组合中。 keras'fit例如使用fit_loop的形式,例如training_arrays.fit_loop()及其train_on_batch可以使用K.function()tf.keras具有更复杂的层次结构,在上一节中有部分描述。


培训循环:文档-与training.py相关的某些不同执行方法:

  

与其他TensorFlow操作不同,我们不转换python     张量的数字输入。此外,将为每个生成一个新图     独特的python数值

     

function 为每组唯一的输入实例化一个单独的图     形状和数据类型

     

单个tf.function对象可能需要映射到多个计算图     在引擎盖下。仅当性能时才可见(跟踪图具有     计算和内存成本非零


输入数据处理器:与上述类似,根据运行时配置(执行模式,数据格式,分发策略)设置的内部标志,逐个选择处理器。最简单的情况是使用Eager,它可以直接与Numpy数组一起使用。有关某些特定示例,请参见source docstring


模型大小,数据大小:

  • 具有决定性;没有任何一种配置能在所有型号和数据大小上脱颖而出。
  • 相对于模型大小的数据大小很重要;对于小型数据和模型,数据传输(例如CPU到GPU)的开销可能占主导。同样,小型的开销处理器在每个数据转换时间内对大数据的运行速度较慢(请参见“ PROFILER”中的convert_to_tensor
  • 每个列车循环的速度不同,输入数据处理器的处理资源的方式也不同。

基准:磨碎的肉。 -this answer-Word Document


术语

  • 减去%的数字都是
  • %计算为(1 - longer_time / shorter_time)*100;理由:我们感兴趣的是,是什么因素shorter / longer实际上是非线性关系,对直接比较没有用
  • %符号确定:
    • TF2与TF1:+(如果TF2更快)
    • GvE(图表vs.渴望的):+,如果图表速度更快
  • TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5

PROFILER


PROFILER-说明:Spyder 3.3.6 IDE分析器。

  • 某些函数在其他函数的嵌套中重复;因此,很难找到“数据处理”和“训练”功能之间的确切间隔,因此会有一些重叠-在最后一个结果中很明显。

  • %数字通过w.r.t.运行时减去构建时间

  • 通过将所有(唯一的)运行时间(称为1或2次)相加得出的构建时间
  • 通过将所有(唯一的)运行时(与迭代次数和其嵌套的某些运行时)调用次数相同的总和来计算出培训时间
  • 不幸的是,函数是根据其原始名称进行概要分析的(即_func = func将概要分析为func),这会混入构建时间-因此需要排除它< / li>

测试环境

  • 底部执行代码,同时运行最少的后台任务
  • 根据Excel Spreadsheet中的建议,GPU在定时迭代之前经过了几次迭代后已经“预热”了。
  • 从源代码构建的CUDA 10.0.130,cuDNN 7.6.0,TensorFlow 1.14.0和TensorFlow 2.0.0以及Anaconda
  • Python 3.7.4,Spyder 3.3.6 IDE
  • GTX 1070,Windows 10、24GB DDR4 2.4MHz RAM,i7-7700HQ 2.8GHz CPU

方法

  • 基准“小”,“中”和“大”模型和数据大小
  • 固定每种型号尺寸的参数数量,与输入数据尺寸无关
  • “更大”模型具有更多参数和层
  • “较大”的数据具有更长的序列,但batch_sizenum_channels相同
  • 模型仅使用Conv1DDense个“可学习”层;每个TF版本的符号都避免了RNN。差异
  • 总是在基准循环之外进行一次火车拟合,以省略模型和优化器图的构建
  • 不使用稀疏数据(例如layers.Embedding())或稀疏目标(例如SparseCategoricalCrossEntropy()

局限性:“完整”的答案将解释所有可能的火车循环和迭代器,但这肯定超出了我的时间能力,不存在薪水或一般需要。结果仅与方法学一样好-用开放的心态解释。


代码

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)