实现更好的 TensorFlow 性能
高阶 API 本身可实现更好的 TensorFlow 性能。以下各部分详细介绍了要使用的高阶 API、几点与调试相关的提示、发展历史以及建议使用手动调整的一些实例。此外,还介绍了与各种硬件和模型相关的最佳做法。
输入流水线
输入流水线会从一个位置提取数据,对数据进行转换,然后将数据加载到加速器上进行处理。随着加速器速度的提升,确保输入流水线与需求保持同步非常重要。tf.data API 在设计时考虑了灵活性、易用性和性能。要使用 tf.data API 并最大限度地提高其性能,请参阅数据输入流水线 指南(https://tensorflow.google.cn/guide/performance/datasets?hl=zh-CN)。
读取大量小文件会显著影响 I/O 性能。实现最大 I/O 吞吐量的一种方法是将输入数据预处理为更大(约 100MB)的 TFRecord 文件。对于较小的数据集 (200MB-1GB),最好的方法通常是将整个数据集加载到内存中。下载并转换为 TFRecord 格式文档包含用于创建 TFRecord 的信息和脚本,此 脚本 会将 CIFAR-10 数据集转换为 TFRecord(https://github.com/tensorflow/models/blob/master/tutorials/image/cifar10_estimator/generate_cifar10_tfrecords.py)。
虽然使用 feed_dict 提供数据可实现高度灵活性,但 feed_dict 通常无法提供可扩缩的解决方案。请尽量将 feed_dict 用于简单样本,避免全部应用。
# feed_dict often results in suboptimal performance.
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
RNN 性能
您可以通过多种方法在 TensorFlow 中指定 RNN 计算,这些方法可在模型灵活性和性能方面进行权衡。tf.nn.rnn_cell.BasicLSTMCell 应被视为参考实现,并且只能在没有其他选项可用时用作最后的补救手段。
在使用其中一个单元(而不是完全混合的 RNN 层)时,您可以选择使用 tf.nn.static_rnn 或 tf.nn.dynamic_rnn。在运行时通常不应存在性能差异,但展开次数较多会增加 tf.nn.static_rnn 的图大小并导致编译时间变长。tf.nn.dynamic_rnn 的另一个优势是可以选择将内存从 GPU 切换到 CPU,从而能够训练很长的序列。这可能会降低性能,具体取决于模型和硬件配置。您也可以并行运行 tf.nn.dynamic_rnn 和底层 tf.while_loop 构造的多次迭代,尽管这对于 RNN 模型用处不大(因为这些模型本身是序列模型)。
在 NVIDIA GPU 上,除非您想要实现不受支持的层归一化,否则应始终首选使用 tf.contrib.cudnn_rnn。它通常比 tf.contrib.rnn.BasicLSTMCell 和 tf.contrib.rnn.LSTMBlockCell 快至少一个数量级,并且 tf.contrib.rnn.BasicLSTMCell 占用的内存比它多 3 - 4 倍。
如果您需要一次运行 RNN 的一步(使用递归政策的强化学习中可能会出现这种情况),则应该将 tf.contrib.rnn.LSTMBlockCell 与您自己的环境交互循环(在 tf.while_loop 构造内部)结合使用。可以一次运行 RNN 的一个步骤并返回到 Python,但速度会慢一些。
在 CPU 和移动设备上,如果 GPU 不支持 tf.contrib.cudnn_rnn,那么最快且最节省内存的选项是 tf.contrib.rnn.LSTMBlockFusedCell。
对于所有不常见的单元类型,如 tf.contrib.rnn.NASCell、tf.contrib.rnn.PhasedLSTMCell、tf.contrib.rnn.UGRNNCell、tf.contrib.rnn.GLSTMCell、tf.contrib.rnn.Conv1DLSTMCell、tf.contrib.rnn.Conv2DLSTMCell、tf.contrib.rnn.LayerNormBasicLSTMCell 等,请注意它们是在图(如 tf.contrib.rnn.BasicLSTMCell)中实现的,因此会出现同样的性能不佳和内存使用率较高的情况。在使用这些单元之前,请考虑是否值得进行此类权衡。例如,虽然层归一化可以加速收敛(因为 cuDNN 的速度要快 20 倍),但最快的收敛速度通常是在不使用层归一化的情况下实现的。
手动调整
针对 CPU 进行优化
在使用目标 CPU 支持的所有指令从源代码构建 TensorFlow 时,CPU(包括 Intel® Xeon Phi™)可实现最佳性能。
除了使用最新的指令集,Intel® 还在 TensorFlow 中增加了对 Intel® Math Kernel Library for Deep Neural Networks (Intel® MKL-DNN) 的支持。虽然名称不完全准确,但这些优化通常简称为 MKL 或包含 MKL 的 TensorFlow。包含 Intel® MKL-DNN 的 TensorFlow 包含有关 MKL 优化的详细信息。
下面列出的两种配置用于通过调整线程池来优化 CPU 性能。
intra_op_parallelism_threads:可以使用多个线程并行处理其执行的节点会将各个部分安排到此池中
inter_op_parallelism_threads:所有就绪节点均已在此池中安排完毕
这些配置是使用 tf.ConfigProto 进行设置的,并传递给 tf.Session(在 config 属性中),如以下代码段中所示。对于这两个配置选项,如果它们未设置或设置为 0,则将默认为逻辑 CPU 核心数。测试表明,默认设置对于配备一个 CPU(具有 4 个核心)和多个 CPU(具有 70 个以上组合逻辑核心)的系统有效。常见的替代优化是将两个池中的线程数设置为等于物理核心数(而不是逻辑核心数)。
config = tf.ConfigProto()
config.intra_op_parallelism_threads = 44
config.inter_op_parallelism_threads = 44
tf.session(config=config)
比较编译器优化部分包含使用不同编译器优化的测试结果。
包含 Intel® MKL DNN 的 TensorFlow
通过使用 Intel® Math Kernel Library for Deep Neural Networks (Intel® MKL-DNN) 优化的基本功能,Intel® 为 Intel® Xeon® 和 Intel® Xeon Phi™ 版 TensorFlow 添加了优化。这些优化还加快了使用消费级处理器(例如 i5 和 i7 Intel 处理器)的速度。Intel 发布的 TensorFlow* Optimizations on Modern Intel® Architecture(针对现代 Intel® 架构的 TensorFlow* 优化)论文包含有关实现的其他详情。
注意:MKL 是从 TensorFlow 1.2 开始添加的,目前仅适用于 Linux。同时使用 --config=cuda 时它也无法工作。
除了在训练基于 CNN 的模型时能显著提升性能之外,使用 MKL 进行编译还可以创建针对 AVX 和 AVX2 进行优化的二进制文件,从而得到一个经过优化且与大多数现代(2011 年后)处理器兼容的二进制文件。
可以使用以下命令通过 MKL 优化对 TensorFlow 进行编译,具体命令取决于所用 TensorFlow 源代码的版本。
对于 1.3.0 之后的 TensorFlow 源代码版本:
./configure
# Pick the desired options
bazel build --config=mkl --config=opt //tensorflow/tools/pip_package:build_pip_package
对于 1.2.0 到 1.3.0 的 TensorFlow 版本:
./configure
Do you wish to build TensorFlow with MKL support? [y/N] Y
Do you wish to download MKL LIB from the web? [Y/n] Y
# Select the defaults for the rest of the options.
bazel build --config=mkl --copt="-DEIGEN_USE_VML" -c opt //tensorflow/tools/pip_package:build_pip_package
调整 MKL 以获得最佳性能
本部分详细介绍了可用于调整 MKL 以获得最佳性能的不同配置和环境变量。在调整各种环境变量之前,请确保模型使用的是 NCHW (channels_first) 数据格式。MKL 针对 NCHW 进行了优化,并且 Intel 正努力确保在使用 NHWC 时获得几乎一样的性能。
MKL 使用以下环境变量调整性能:
KMP_BLOCKTIME - 设置线程在执行完并行区域之后,在休眠之前应该等待的时间(以毫秒为单位)
KMP_AFFINITY - 使运行时库能够将线程绑定到物理处理单元
KMP_SETTINGS - 允许 (true) 或禁止 (false) 在程序执行期间输出 OpenMP* 运行时库环境变量
OMP_NUM_THREADS - 指定要使用的线程数
如需详细了解 KMP 变量,请访问 Intel 网站;如需详细了解 OMP 变量,请访问 https://gcc.gnu.org/onlinedocs/libgomp/Environment-Variables.html
虽然调整环境变量可为您带来很多好处(如下文所述),但简单的做法是将 inter_op_parallelism_threads 设置为与物理 CPU 的数量相等并设置以下环境变量:
KMP_BLOCKTIME=0
KMP_AFFINITY=granularity=fine,verbose,compact,1,0
使用命令行参数设置 MKL 变量的示例:
KMP_BLOCKTIME=0 KMP_AFFINITY=granularity=fine,verbose,compact,1,0 \
KMP_SETTINGS=1 python your_python_script.py
使用 python os.environ 设置 MKL 变量的示例:
os.environ["KMP_BLOCKTIME"] = str(FLAGS.kmp_blocktime)
os.environ["KMP_SETTINGS"] = str(FLAGS.kmp_settings)
os.environ["KMP_AFFINITY"]= FLAGS.kmp_affinity
if FLAGS.num_intra_threads > 0:
os.environ["OMP_NUM_THREADS"]= str(FLAGS.num_intra_threads)
有些模型和硬件平台可以从不同的设置中受益。下面介绍了会影响性能的各个变量。
KMP_BLOCKTIME:MKL 默认值为 200 毫秒,这在我们的测试中并不是最佳值。对于经过测试的 CNN 类模型,0(0 毫秒)是一个不错的默认值。AlexNex 可在默认值设置为 30 毫秒时实现最佳性能,而 GoogleNet 和 VGG11 可在默认值设置为 1 毫秒时实现最佳性能
KMP_AFFINITY:建议的设置是 granularity=fine,verbose,compact,1,0
OMP_NUM_THREADS:默认为物理核心数。针对某些模型使用 Intel® Xeon Phi™ (Knights Landing) 时,调整此参数并使其与核心数量不匹配会产生影响。要了解最佳设置,请参阅 针对现代 Intel® 架构的 TensorFlow* 优化(https://software.intel.com/en-us/articles/tensorflow-optimizations-on-modern-intel-architecture)
intra_op_parallelism_threads:建议将此值设置为与物理核心数相等。将值设置为 0(默认值)会将此值设置为逻辑核心数,这是某些架构可以尝试的备用选项。此值应与 OMP_NUM_THREADS 相等
inter_op_parallelism_threads:建议将此值设置为等于套接字数。将值设置为 0(默认值)会将此值设置为逻辑核心数
从源代码构建和安装
默认的 TensorFlow 二进制文件面向最广泛的硬件,以便让所有人都可以访问 TensorFlow。如果使用 CPU 进行训练或推断,建议您使用可用于所用 CPU 的所有优化来编译 TensorFlow。下面的比较编译器优化中介绍了如何加快在 CPU 上进行训练和推断的速度。
要安装最优化的 TensorFlow 版本,请从源代码构建和安装。如果需要在具有与目标硬件不同的硬件的平台上构建 TensorFlow,则使用针对目标平台的最佳优化进行交叉编译。以下命令展示了如何使用 bazel 针对特定平台进行编译:
# This command optimizes for Intel’s Broadwell processor
bazel build -c opt --copt=-march="broadwell" --config=cuda //tensorflow/tools/pip_package:build_pip_package
环境、构建和安装提示
./configure 会询问您要在构建中包含哪个计算功能。这不会影响整体性能,但会影响初始启动。运行一次 TensorFlow 后,编译的核将被 CUDA 缓存。如果使用 Docker 容器,则不会缓存数据,并且每次 TensorFlow 启动时都会受到影响。最佳做法是将系统会使用的 GPU 计算能力包括在内,例如 P100:6.0、Titan X (Pascal):6.1、Titan X (Maxwell):5.2 和 K80:3.7
使用支持目标 CPU 的所有优化的 gcc 版本。建议的最低 gcc 版本是 4.8.3。在 OS X 上,升级到最新的 Xcode 版本并使用 Xcode 随附的 Clang 版本
安装 TensorFlow 支持的最新稳定 CUDA 平台和 cuDNN 库
发展历史
数据格式
数据格式是指传递到给定操作的张量的结构。下面专门讨论了代表图像的四维张量。在 TensorFlow 中,四维张量的各部分通常用以下字母表示:
N 是指批次中的图像数量
H 是指垂直(高度)维度中的像素数
W 是指水平(宽度)维度中的像素数
C 是指通道。例如,1 代表黑白或灰度,3 代表 RGB
在 TensorFlow 中,有两种命名约定,它们分别代表两种最常见的数据格式:
NCHW 或 channels_first
NHWC 或 channels_last
NHWC 是 TensorFlow 的默认值,NCHW 是使用 cuDNN 在 NVIDIA GPU 上训练时使用的最佳格式。
最佳做法是构建同时支持这两种数据格式的模型。这样可以简化在 GPU 上进行训练、然后在 CPU 上进行推断的过程。如果使用 Intel MKL 优化编译 TensorFlow,许多操作(尤其是与 CNN 类模型相关的操作)都将得到优化,并且支持 NCHW。如果不使用 MKL,则在使用 NCHW 时,CPU 不支持某些操作。
这两种格式的简短历史是 TensorFlow 首先使用 NHWC,因为它在 CPU 上的速度要快一些。从长远来看,我们正在研究可用于自动重写图的工具,以使格式之间的切换变得透明,并利用微型优化(在这种优化中,GPU 操作使用 NHWC 可能要比通常最有效的 NCHW 速度更快)。
调试
调试输入流水线优化
典型模型会从磁盘检索数据,并在将数据传入网络之前对这些数据进行预处理。例如,处理 JPEG 图像的模型将遵循以下流程:从磁盘加载图像,将 JPEG 解码为张量,裁剪并填充,可能会翻转和失真,然后进行批处理。该流程称为输入流水线。随着 GPU 和其他硬件加速器的速度的提升,数据预处理可能会成为瓶颈。
确定输入流水线是否是瓶颈可能很复杂。最简单的一种方法是在完成输入流水线之后将模型简化为单个操作(小型模型),并测量每秒样本数。如果完整模型和小型模型的每秒样本数差异很小,那么瓶颈可能在于输入流水线。以下是发现问题的一些其他方法:
通过运行 nvidia-smi -l 2 检查 GPU 是否未充分利用。如果 GPU 利用率未达到 80-100%,那么输入流水线可能是瓶颈所在
生成时间轴并查找大块空白区域(等待)。XLA jit 教程中包含生成时间轴的示例
查看 CPU 使用情况。输入流水线可能已经经过优化,但没有 CPU 周期来处理该流水线
估算所需的吞吐量,并验证所用磁盘是否能够达到该吞吐量级别。某些云端解决方案具有网络挂接磁盘,这些磁盘的启动速度的速度较慢,仅有 50 MB / 秒,比旋转磁盘(150 MB / 秒)、SATA SSD(500 MB / 秒)和 PCIe SSD(2000+ MB / 秒)要慢
在 CPU 上进行预处理
将输入流水线操作放在 CPU 上可以显著提高性能。利用 CPU 执行输入流水线可以腾出 GPU 专门用于训练。要确保在 CPU 上进行预处理,请按如下所示封装预处理操作:
with tf.device('/cpu:0'):
# function to get and process images or data.
distorted_inputs = load_and_distort_images()
如果使用 tf.estimator.Estimator,输入函数将自动放置在 CPU 上。
更多 AI 相关阅读: