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

Why is TensorFlow 2 much slower than TensorFlow 1?

它被许多用户引用为切换到 Pytorch 的原因,但我还没有找到 justification/explanation 牺牲最重要的实用质量,速度,急于执行。

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

我的问题是:是什么导致了如此显着的减速?


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

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


基准测试结果:


更新:根据以下代码禁用 Eager Execution 没有帮助。然而,这种行为是不一致的:有时 运行 在图形模式下有很大帮助,其他时候它运行 较慢 相对于 Eager。


基准代码:

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

更新 8/1730/2020:TF 2.3 终于做到了:所有案例 运行 一样快,或明显比以前的任何版本都快。

另外,我之前的更新对TF不公平;我的 GPU 是罪魁祸首,最近一直过热。如果您看到迭代时间的上升干图,这是一个可靠的症状。最后,请参阅开发人员关于 Eager vs Graph.

的说明

这可能是我对这个答案的最后一次更新。 您的模型速度的真实统计数据只能由您在您的设备上找到。


2020 年 5 月 19 日更新:TF 2.2,使用相同的测试:Eager 速度仅略有提高。下面的 Large-Large Numpy train_on_batch 案例图,x-axis 是连续拟合迭代;我的 GPU 没有接近其全部容量,所以怀疑它正在节流,但随着时间的推移迭代速度确实变慢了。

根据上述,Graph 和 Eager 分别比对应的 TF1 慢 1.56x1.97x。不确定我是否会进一步调试它,因为我正在考虑根据 TensorFlow 对自定义/low-level 功能的不良支持切换到 Pytorch。但是,我确实打开了 Issue 以获得开发人员的反馈。


2020 年 2 月 18 日更新:我每晚都坐过 2.1 和 2.1;结果喜忧参半。除了一个配置(模型和数据大小)之外,所有配置都与 TF2 和 TF1 的最佳配置一样快或快得多。速度较慢且速度较慢的是 Large-Large - 尤其是。在图形执行中(慢 1.6 到 2.5 倍)。

此外,对于我测试的大型模型,Graph 和 Eager 之间存在 极端 的再现性差异 - 无法通过 randomness/compute-parallelism 解释。我目前无法根据时间限制为这些声明提供可重现的代码,因此我强烈建议您针对自己的模型进行测试。

还没有就这些打开 Git 问题,但我确实对 original 发表了评论 - 还没有回复。一旦取得进展,我会更新答案。


VERDICT:它不是,如果你知道你在做什么。但是如果你,它可能会花费你很多 - 平均几个 GPU 升级,以及多个 GPU worst-case。


THIS ANSWER:旨在提供问题的 high-level 描述,以及如何根据您的需求决定训练配置的指南。有关详细的 low-level 描述,其中包括所有基准测试结果 + 使用的代码,请参阅我的其他答案。

如果我学到任何信息,我将更新我的答案和更多信息 - 可以为这个问题添加书签/“加注星标”以供参考。


问题摘要:正如 confirmed TensorFlow 开发人员 Q. Scott Zhu 所说,TF2 专注于 Eager 执行和与 Keras 的紧密集成,这涉及全面TF 来源的变化 - 包括 graph-level。好处:极大地扩展了处理、分发、调试和部署功能。然而,其中一些成本是速度。

然而,事情要复杂得多。不仅仅是 TF1 与 TF2 - 导致列车速度显着差异的因素包括:

  1. TF2 与 TF1
  2. Eager 与图表模式
  3. keras 对比 tf.keras
  4. numpy 对比 tf.data.Dataset 对比 ...
  5. train_on_batch() 对比 fit()
  6. GPU 对比 CPU
  7. model(x) 对比 model.predict(x) 对比 ...

不幸的是,以上几乎 none 是相互独立的,并且每个相对于另一个至少可以增加一倍的执行时间。幸运的是,您可以系统地确定什么最有效,并使用一些快捷方式 - 正如我将要展示的那样。


我应该做什么? 目前,唯一的方法是 - 针对您的特定模型、数据和硬件进行实验。没有哪一种配置总是最有效的 - 但是 可以和不可以简化您的搜索:

>> 执行:

  • train_on_batch() + numpy + tf.keras + TF1 + Eager/Graph
  • 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 + Eager

  • [主要]tf.python.keras;它可以 运行 慢 10-100 倍,并且有很多错误; more info

    • 这包括 layersmodelsoptimizers 和相关的“out-of-box”用法导入; ops、utils 和相关 'private' 导入没问题 - 但可以肯定的是,检查 alts,以及它们是否在 tf.keras
    • 中使用

请参阅我的其他答案底部的代码,了解基准测试设置示例。以上列表主要基于“BENCHMARKS》;另一个答案中的表格。


上述注意事项的限制

  • 这个问题的标题是“为什么 TF2 比 TF1 慢得多?”,虽然它 body 明确涉及训练,但问题不仅限于此; inference 也受到主要速度差异的影响,even 在相同的 TF 版本、导入、数据格式等中 - 参见 this answer.
  • RNN 可能会显着改变另一个答案中的数据网格,因为它们在 TF2 中得到了改进
  • 模型主要使用 Conv1DDense - 无 RNN、稀疏 data/targets、4/5D 输入和其他配置
  • 输入数据仅限于 numpytf.data.Dataset,但存在许多其他格式;查看其他答案
  • 使用了 GPU;结果 在 CPU 上有所不同。其实我问这个问题的时候,我的CUDA没有配置好,有的结果是CPU-based.

为什么 TF2 牺牲了最实际的质量,速度,来急于执行?显然没有——图表仍然可用。但如果问题是“为什么那么渴望”:

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

如何ENABLE/DISABLE渴望?

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

在 TF2 中具有误导性;参见 here


附加信息:

  • 小心 TF2 中的 _on_batch() 方法;根据 TF 开发人员的说法,他们仍然使用较慢的实现,但 不是故意的 - 即它需要修复。详情见其他答案。

对 TENSORFLOW 开发人员的请求

  1. 请修复 train_on_batch(),以及迭代调用 fit() 的性能问题;自定义火车循环对很多人来说都很重要,尤其是对我而言。
  2. 添加文档/文档字符串提及这些性能差异以供用户了解。
  3. 提高总体执行速度以防止窥视者跳转到 Pytorch。

致谢:感谢


更新

  • 11/14/19 - 找到了一个模型(在我的实际应用中)运行s 在 TF2 for all* configurations w/ Numpy 输入数据。差异在 13-19% 之间,平均为 17%。然而,kerastf.keras 之间的差异更为显着:18-40%,平均。 32%(TF1 和 2)。 (* - 除了 Eager,TF2 OOM'd)

  • 11/17/19 - 开发人员在 recent commit 中更新了 on_batch() 方法,声称速度有所提高 - 是在 TF 2.1 中发布,或现在可用 tf-nightly。由于我无法获得后者 运行ning,将延迟到 2.1.

  • 2/20/20 - 预测性能也值得基准;例如,在 TF2 中,CPU 预测时间可能涉及 periodic spikes

THIS ANSWER:旨在提供问题的详细 [​​=286=] 描述 - 包括 TF2 与 TF1 训练循环、输入数据处理器以及 Eager 与图形模式执行。有关问题摘要和解决指南,请参阅我的其他答案。


性能评价:有时一个更快,有时另一个更快,这取决于配置。就 TF2 与 TF1 而言,它们的平均水平差不多,但基于配置的显着差异确实存在,TF1 胜过 TF2 的情况比反之亦然。请参阅下面的“基准测试”。


渴望与。 GRAPH:对某些人来说,这整个答案的核心是:根据我的测试,TF2 的急切比 TF1 慢。详情往下看。

两者的根本区别在于:Graph主动建立一个计算网络,'told to'时执行——而Eager在创建时执行一切。但故事只从这里开始:

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

  • Eager 在执行时重建自己的部分图; Graph 未完全构建的直接后果——请参阅分析器结果。这有计算开销。

  • Eager 使用 Numpy 输入时速度较慢;根据 this Git comment 和代码,Eager 中的 Numpy 输入包括将张量从 CPU 复制到 GPU 的开销成本。逐步查看源代码,数据处理差异一目了然; Eager 直接传递 Numpy,而 Graph 传递张量,然后评估为 Numpy;不确定确切的过程,但后者应该涉及 GPU 级别的优化

  • TF2 Eager 比 TF1 Eager - 这是......出乎意料的。请参阅下面的基准测试结果。差异从微不足道到显着,但始终如一。不确定为什么会这样 - 如果 TF 开发人员澄清,将更新答案。


TF2 vs. TF1:引用 TF 开发者的相关部分,Q. Scott Zhu 的,response - 加上我的一些强调和改写:

In eager, the runtime needs to execute the ops and return the numerical value for every line of python code. The nature of single step execution causes it to be slow.

In TF2, Keras leverages tf.function to build its graph for training, eval, and prediction. We call them "execution function" for the model. In TF1, the "execution function" was a FuncGraph, which shared some common components as the TF function, but has a different implementation.

During the process, we somehow left an incorrect implementation for train_on_batch(), test_on_batch() and predict_on_batch(). They are still numerically correct, but the execution function for x_on_batch is a pure python function, rather than a tf.function wrapped python function. This will cause slowness

In TF2, we convert all input data into a tf.data.Dataset, by which we can unify our execution function to handle the single type of the inputs. There might be some overhead in the dataset conversion, and I think this is a one-time-only overhead, rather than a per-batch cost

加上上段最后一句,下段最后一个从句:

To overcome the slowness in eager mode, we have @tf.function, which will turn a python function into a graph. When feed numerical value like np array, the body of the tf.function is converted into a static graph, being optimized, and return the final value, which is fast and should have similar performance as TF1 graph mode.

我不同意 - 根据我的分析结果,这表明 Eager 的输入数据处理速度比 Graph 慢得多。此外,尤其不确定 tf.data.Dataset,但 Eager 确实会重复调用多个相同的数据转换方法 - 请参阅分析器。

最后,开发者的链接提交:Significant number of changes to support the Keras v2 loops.


Train Loops:取决于 (1) Eager vs. Graph; (2) 输入数据格式,training in 将进行不同的train loop - 在TF2中,_select_training_loop(), training.py,其中之一:

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

每个处理资源分配的方式不同,并对性能和功能产生影响。


Train Loops:fit vs train_on_batchkeras vs. tf.keras:四个都使用不同的train循环,尽管可能不是所有可能的组合。 keras' fit,例如,使用 fit_loop 的形式,例如training_arrays.fit_loop(),其train_on_batch可以用K.function()tf.keras 具有更复杂的层次结构,在上一节中有部分描述。


Train Loops:文档 -- 一些不同执行方法的相关source docstring

Unlike other TensorFlow operations, we don't convert python numerical inputs to tensors. Moreover, a new graph is generated for each distinct python numerical value

function instantiates a separate graph for every unique set of input shapes and datatypes.

A single tf.function object might need to map to multiple computation graphs under the hood. This should be visible only as performance (tracing graphs has a nonzero computational and memory cost)


输入数据处理器:与上面类似,根据运行时间配置(执行模式)设置的内部标志,逐个选择处理器、数据格式、分发策略)。最简单的情况是使用 Eager,它直接使用 Numpy 数组。有关一些具体示例,请参阅 .


模型大小,数据大小:

  • 果断;没有任何一种配置能够超越所有模型和数据大小。
  • 数据大小相对于模型大小很重要;对于小数据和模型,数据 t运行sfer(例如 CPU 到 GPU)开销可能占主导地位。同样,小开销处理器可以 运行 在每个数据转换时间主导的大数据上变慢(参见“PROFILER”中的 convert_to_tensor
  • 速度因列车循环和输入数据处理器处理资源的不同方式而异。

BENCHMARKS:绞肉。 -- Word Document -- Excel Spreadsheet


术语:

  • %-少的数字都是
  • % 计算为 (1 - longer_time / shorter_time)*100;理由:我们感兴趣的是哪个因素一个比另一个快; shorter / longer其实是非线性关系,直接比较用处不大
  • %符号判定:
    • TF2 与 TF1:+ 如果 TF2 更快
    • GvE(Graph vs. Eager):+ 如果 Graph 更快
  • TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5

分析器:


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

  • 一些功能在其他功能的嵌套中重复;因此,很难追踪“数据处理”和“训练”功能之间的确切分离,因此会有一些重叠 - 正如最后的结果所示。

  • % 计算得出的数字 w.r.t。 运行时间减去构建时间

  • 通过将调用 1 次或 2 次的所有(唯一)运行次相加计算得出的构建时间

  • 通过对所有(唯一)运行次求和来计算训练时间,这些时间被称为与迭代次数相同的次数,以及它们的一些嵌套运行次

  • 函数根据它们的 原始 名称进行分析,不幸的是(即 _func = func 将分析为 func),其中混入构建时间 - 因此需要排除它


测试环境:

  • 在底部执行代码,后台任务最少 运行ning
  • GPU 在定时迭代之前进行了几次迭代“预热”,如 this post
  • 中所建议
  • 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,Windows10、24GB DDR4 2.4-MHz 内存,i7-7700HQ 2.8-GHz CPU

方法论:

  • 基准 'small'、'medium'、&'large' 模型和数据大小
  • 修复每个模型大小的参数数量,与输入数据大小无关
  • “更大”的模型有更多的参数和层数
  • “较大”数据的序列较长,但 batch_sizenum_channels
  • 相同
  • 模型仅使用 Conv1DDense 'learnable' 层;每个 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 == 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)