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

很多用户都把它作为切换到 Pytorch 的理由,但是我还没有找到一个理由/解释为什么要牺牲最重要的实用质量——速度,来满足急切的执行。

下面是代码基准测试性能,TF1对 TF2-TF1在 快了47% 到276% 的任何地方运行。

我的问题是: 在图表或硬件层面,是什么导致了如此显著的减速?


寻找一个详细的答案-我已经熟悉广泛的概念

眼镜: CUDA 10.0.130,cuDNN 7.4.2,Python 3.7.4,Windows 10,GTX 1070


返回文章页面


更新: 禁用以下代码的急切执行有助于 没有。然而,这种行为是不一致的: 有时在图形模式下运行有很大帮助,有时相对于 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))
37870 次浏览

TF 2.3最终做到了这一点: 所有情况下运行速度都和以前的任何版本一样快,或者说明显快于以前的任何版本。

此外,我以前的更新是不公平的 TF; 我的 GPU 是罪魁祸首,已经过热最近。如果您看到迭代次数的上升干图,那么这是一个可靠的症状。最后,请参阅 急切 VS 图表上的开发人员说明。

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


TF 2.2,使用相同的测试: Eager 速度只有一个小的改进。下面是大型数字化 train_on_batch的情况,x 轴是连续的拟合迭代; 我的 GPU 没有接近它的全部容量,所以怀疑它的节流,但迭代确实会随着时间的推移变慢。

enter image description here

根据以上数据,Graph 和 Eager 的 1.56 x1.97 x分别慢于它们的 TF1对应物。不确定是否要进一步调试,因为我正在考虑根据 TensorFlow 对自定义/低级功能的不良支持切换到 Python。不过,我确实打开了一个 问题来获得开发人员的反馈。


更新日期2/18/2020: 我每晚都要做2.1和2.1的替补,结果喜忧参半。除了一个配置(模型和数据大小)是一样快,或远远快于最好的 TF2和 TF1。其中最慢的是 Large-Large-esp。在图执行中(慢1.6倍至2.5倍)。

此外,对于我测试的大型模型,Graph 和 Eager 之间存在 极端重复性差异——这种差异无法通过随机性/计算-并行性来解释。我目前不能为这些声明提供每次时间约束的可重复代码,所以我强烈建议您对自己的模型进行测试。

还没有在这些问题上打开一个 Git 问题,但我确实对 原创的进行了评论——还没有回应。一旦有进展,我将更新答案。


它是 不是,如果你知道你在做什么。但是,如果你 不要,它可能成本你,很多-由几个 GPU 平均升级,并由多个 GPU 的最坏情况。


这个答案: 旨在提供问题的高级描述,以及如何决定针对您的需求的培训配置的指导方针。有关详细的低级描述,包括所有基准测试结果 + 使用的代码,请参阅我的其他答案。

我会更新我的答案(s) w/更多的信息,如果我学到任何-可以书签/“星”这个问题作为参考。


问题摘要: 由 TensorFlow 开发人员 Q。 Scott Zhu 编写的 确认,TF2专注于 Eager 执行和紧密集成 w/Kera 的开发,包括对 TF 源代码的彻底改变——包括在图形级别。优点: 极大地扩展了处理、分发、调试和部署功能。然而,其中一些措施的成本是速度。

然而,问题相当复杂。不仅仅是 TF1和 TF2——导致列车速度有显著差异的因素包括:

  1. TF2对 TF1
  2. 急切模式与图形模式
  3. kerastf.keras
  4. numpytf.data.Dataset对..。
  5. train_on_batch()fit()
  6. GPU 和 CPU
  7. model(x)model.predict(x)对..。

不幸的是,上面几乎没有一个是独立的,每个都至少可以使执行时间相对于另一个加倍。幸运的是,您可以系统地确定哪些方法最有效,并且有一些捷径——正如我将要展示的那样。


目前,唯一的方法是——对特定的模型、数据和硬件进行试验。没有单一的配置总是最好的工作-但有 做的和不做的,以简化您的搜索:

> > 怎样做:

  • 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

  • < p > fit() + numpy + tf.keras + TF1/TF2 + Eager

  • < p > train_on_batch() + numpy + keras + TF1 + Eager

  • [少校] tf.python.keras,运行速度慢10-100倍,漏洞多; 更多信息

    • 这包括 layersmodelsoptimizers,& 相关的“开箱即用”的使用导入; 操作,实用程序,& 相关的“私有”导入都是可以的-但是要确保,检查 alts,& 是否在 tf.keras中使用

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


以上要做及不要做的 局限性:

  • 这个问题的标题是“为什么 TF2比 TF1慢得多?”,虽然它的身体明确地关注训练,但事情并不局限于它; 推理,也是受到主要的速度差异,甚至在相同的 TF 版本,导入,数据格式等-见 这个答案
  • RNN 可能会显著改变另一个答案中的数据网格,因为它们在 TF2中得到了改进
  • 模型主要使用 Conv1DDense-没有 RNN,稀疏的数据/目标,4/5D 输入,和其他配置
  • 输入数据仅限于 numpytf.data.Dataset,而存在许多其他格式; 请参阅其他答案
  • 使用 GPU; 结果 威尔在 CPU 上不同。事实上,当我问这个问题时,我的 CUDA 没有正确配置,有些结果是基于 CPU 的。

显然还没有——图表仍然可用。但如果问题是“为什么这么急切”:

  • 优秀的调试: 你可能会遇到许多问题,比如“我如何得到中间层输出”或者“我如何检查权重”; 对于渴望,它(几乎)和 .__dict__一样简单。相比之下,Graph 需要熟悉特殊的后端函数——这使得调试和反思的整个过程变得非常复杂。
  • 更快的原型制作: 每个想法类似上面; 更快的理解 = 更多的时间留给实际的 DL。

如何激发/抑制渴望?

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

TF2中的 误导; 参见 给你


返回文章页面

  • 小心 TF2中的 _on_batch()方法; 根据 TF dev,它们仍然使用较慢的实现,但是 不是故意的-也就是说,需要修复。有关详细信息,请参阅其他答案。

返回文章页面

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

谢谢

  • 斯科特 · 朱,TensorFlow 开发人员,为他的 详细说明谈论这个问题。
  • 安德烈分享 有用的测试,并进行讨论。

返回文章页面

  • 找到了一个在 TF2 对于所有 * 配置 w/Numpy 输入数据上运行较慢的模型(在我的实际应用程序中)。差异在13-19% 之间,平均为17% 。然而,kerastf.keras之间的差异更为显著: 18-40% ,avg。32% (包括第一及第二阶段)。(*-除了 Eager,TF2 OOM’d)

  • 1919年11月17日-devs 更新了 最近的提交记录中的 on_batch()方法,表示速度有所提高-将在 TF 2.1中释放,或者现在可以作为 tf-nightly使用。由于我无法让后者运行,将延迟板凳直到2.1。

  • 2月20日-预测性能也值得进行替换; 例如,在 TF2中,CPU 预测时间可能涉及 周期性尖峰

这个答案: 旨在提供关于该问题的详细的图形/硬件级描述——包括 TF2与 TF1列车循环、输入数据处理器以及 Eager 与 Graph 模式执行。有关问题摘要和解决方案指南,请参阅我的其他答案。


表现评价: 有时一个更快,有时另一个,这取决于配置。就 TF2和 TF1而言,它们的平均水平相当,但确实存在显著的基于配置的差异,而且 TF1比 TF2更经常胜过 TF2。请参阅下面的“基准”。


急切 VS 图表: 根据我的测试,对于一些人来说,整个答案的核心是: TF2的渴望是 慢一点而不是 TF1。详情请往下看。

两者之间的根本区别在于: Graph 建立了一个计算网络 积极主动,并在“被告知”时执行——而 Eager 在创建时执行所有内容。但故事才刚刚开始:

  • < p > 渴望并非没有图表,而且可能实际上是 差不多吧图,与预期相反。它在很大程度上是 执行格拉夫-这包括模型和优化器权重,包括图的很大一部分。

  • 这是没有完全构建 Graph 的直接后果——参见分析器结果。

  • 根据 这个 Git 评论 & code,Eager 中的 Numpy 输入包括将张量从 CPU 复制到 GPU 的开销。通过源代码,数据处理的差异是显而易见的,渴望直接传递 Numpy,而图形传递张量,然后评估到 Numpy,不确定的确切过程,但后者应涉及到 gpu 级别的优化

  • 这是... 意想不到的。请参阅下面的基准测试结果。差异从微不足道到显著不等,但都是一致的。不确定为什么是这种情况-如果 TF 开发人员澄清,将更新答案。


TF2对 TF1: 引用 TF 开发人员 Q。 Scott Zhu 的相关部分,回应-w/位我的强调和改写:

在热切期望中,运行时需要执行操作并返回每行 python 代码的数值。单步执行会导致它变慢的性质。

在 TF2中,Kera 利用 tf.function构建用于训练、评估和预测的图表。我们称之为模型的“执行函数”。在 TF1中,“执行函数”是一个 FuncGraph,它作为 TF 函数共享一些公共组件,但具有不同的实现。

在这个过程中,我们不知怎么留下了一个 Train _ on _ batch ()、 test _ on _ batch ()和 Prevention _ on _ batch ()的实现不正确。它们仍然是 数字正确,但是 x _ on _ batch 的执行函数是纯 Python 函数,而不是 tf.function 包装的 python 函数。这将 导致缓慢

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

结合上一段最后一句和下一段最后一句:

为了克服渴望模式中的缓慢,我们使用@tf。函数,它将把一个 python 函数转换成一个图形。当采用 np 数组等进给数值时,将 tf.function的主体转换为静态图形,进行优化,并返回最终值,这种方法具有快速性,并且应该具有与 TF1图形模式相似的性能。

我不同意——根据我的分析结果,Eager 的输入数据处理要比 Graph 慢得多。另外,特别是不确定 tf.data.Dataset,但 Eager 确实反复调用多个相同的数据转换方法-参见分析器。

最后,dev 的链接提交: 支持 Kera v2循环的大量更改


火车圈: 取决于(1)急切与图形; (2)输入数据格式,训练将进行一个独特的列车循环-在 TF2,_select_training_loop()训练 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_on_batch1: 这四种方法都使用不同的火车环路,尽管可能不是每种可能的组合。例如,kerasfit使用 fit_loop的一种形式,例如 training_arrays.fit_loop(),而它的 train_on_batch可能使用 K.function()tf.keras有一个更复杂的层次结构,在前面的部分中已经描述过。


火车循环: 文档——关于一些不同执行方法的相关 源文档字符串:

与其他 TensorFlow 操作不同,我们不转换 python 此外,为每个张量生成一个新的图 明显的巨蟒数值

为每个唯一的输入集实例化一个单独的图 形状和数据类型
单个 tf.function对象可能需要映射到多个计算图 这应该是可见的,只有作为 表演(跟踪图有 A 非零计算和内存开销)


输入数据处理器: 与上面类似,根据运行时配置(执行模式、数据格式、分发策略)设置的内部标志,处理器是逐案选择的。最简单的例子是 Eager,它直接使用 w/Numpy 数组。有关一些特定的示例,请参见 这个答案


型号大小,数据大小:

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

磨碎的肉—— Word 文件—— Excel 电子表格


返回文章页面

  • %-减少的数字都是 几秒钟
  • % 计算为 (1 - longer_time / shorter_time)*100; 基本原理: 我们感兴趣的是 受什么因素影响一个比另一个快; shorter / longer实际上是一个非线性关系,不适合直接比较
  • < li >% 符号测定:
    • 如果 TF2更快,TF2对 TF1: +
    • GvE (Graph vs. Eager) : 如果 Graph 更快,则为 +
  • TF2 = TensorFlow 2.0.0 + Kera 2.3.1; TF1 = TensorFlow 1.14.0 + Kera 2.2.5

返回文章页面


Spyder 3.3.6 IDE 剖析器。

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

  • < p >% 数字计算的 w.r.t. 运行时 减去构建时间

  • 构建时间计算方法是将调用1或2次的所有(唯一)运行时相加

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

  • 遗憾的是,函数根据它们的 原创的名称进行配置(即 _func = func将配置为 func) ,这混合了构建时间——因此需要排除它


返回文章页面

  • 执行底部 w/运行最小后台任务的代码
  • 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,24 GB DDR42.4-MHz RAM,i7-7700HQ 2.8-GHz 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 == 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)