返回介绍

性能指南

发布于 2025-01-22 23:08:19 字数 21072 浏览 0 评论 0 收藏 0

本指南包含了一些优化 TensorFlow 代码的最佳实践,它包含以下几节内容:

通用最佳实践

下面的几节内容为涵盖多种硬件和模型的最佳实践,它们是:

输入管线的优化

典型的模型会从磁盘加载数据,然后处理并通过网络发送出去。比如,模型按照下列数据流过程来处理 JPEG 图像:从磁盘加载图像,将 JPEG 解码到一个张量中,裁剪和边缘垫值,以及可能的翻转和变形操作,然后按批次投入训练。这个数据流被称为输入管线。随着 GPU 和其它加速硬件运行得越来越快,数据预处理就成了性能的瓶颈。

确定输入管线是否为瓶颈可能会比较复杂。一种最简单的方法是在输入管线之后将模型简化为单个操作(平凡模型),并测量其每秒处理的样例数。如果整个模型和平凡模型之间的效率差异极小,则输入管线很有可能是瓶颈。以下是确定瓶颈问题的其它一些方法:

  • 通过运行 nvidia-smi -l 2 来检查 GPU 是否已经被充分利用。如果 GPU 利用率没有接近 80-100%,则此输入管线可能是个瓶颈。
  • 使用即时编译
  • 检查 CPU 使用情况。有可能出现的情况是:管线已经优化,却仍然没有足够的 CPU 周期来处理这个管线。
  • 估计所需的吞吐量,并验证所使用的磁盘能够达到吞吐量的水平。因为一些云解决方案提供的磁盘速度甚至低至 50 MB/秒,这比机械磁盘(150 MB/秒)、SATA SSD (500 MB/秒)、以及 PCIe SSD (2000+ MB/秒)都要慢。

CPU 上的预处理

将输入管线的操作放在 CPU 上可以显著提高性能。让 CPU 处理输入管线,可以使 GPU 专注于训练。为了确保预处理是在 CPU 上进行,可将预处理操作按如下方式包装一下:

with tf.device('/cpu:0'):
 # 用于获得和处理图像或数据的函数
  distorted_inputs = load_and_distort_images()

如果使用 tf.estimator.Estimator ,输入函数会自动在 CPU 上执行。

使用 tf.data API

输入管道性能指南

虽然使用 feed_dict 的流数据提供了很高的灵活性,但是通常 feed_dict 并没有提供可伸缩的解决方案。如果只使用单个 GPU,则 tf.data API 和 feed_dict 之间的性能差异可以忽略不计。我们建议避免使用 feed_dict 来处理琐碎的示例。特别是,避免使用大量输入的 feed_dict

# feed_dict often results in suboptimal performance when using large inputs.
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

融合解码和裁剪

tf.image.decode_and_crop_jpeg

使用示例:

def _image_preprocess_fn(image_buffer):
    # image_buffer 1-D string Tensor representing the raw JPEG image buffer.

    # Extract image shape from raw JPEG image buffer.
    image_shape = tf.image.extract_jpeg_shape(image_buffer)

    # Get a crop window with distorted bounding box.
    sample_distorted_bounding_box = tf.image.sample_distorted_bounding_box(
      image_shape, ...)
    bbox_begin, bbox_size, distort_bbox = sample_distorted_bounding_box

    # Decode and crop image.
    offset_y, offset_x, _ = tf.unstack(bbox_begin)
    target_height, target_width, _ = tf.unstack(bbox_size)
    crop_window = tf.stack([offset_y, offset_x, target_height, target_width])
    cropped_image = tf.image.decode_and_crop_jpeg(image, crop_window)

tf.image.decode_and_crop_jpeg 可运行在所有平台上。在其他平台上,由于使用了 libjpeglibjpeg-turbo ,所以在 Windows 上无法加速。

使用大文件

加载大量的小文件会极大地影响 I/O 性能。一种获得最大的 I/O 吞吐量的方法是将输入数据预处理为更大的 TFRecord 文件(约 100MB 大小)。
对于较小的数据集(200MB~1GB),最好的方法通常是将整个数据集加载到内存。资料 下载和转换为 TFRecord 格式 中介绍了创建 TFRecords 的相关信息和脚本,
脚本
可用于将 CIFAR-10 数据集转化为 TFRecords

数据格式

数据格式是指传递给指定操作的张量结构。下面的讨论专门针对表示图像的四维张量。在 TensorFlow 中,
四维张量中的部分成员常用如下一些字母来表示:

  • N 表示一个训练批次中的图像数目。
  • H 表示垂直维度(高度方向)中的像素数。
  • W 表示水平维度(宽度方向)中的像素数。
  • C 表示通道数。比如,黑白或灰度图像为 1,RGB 图像为 3。

在 TensorFlow 中,有两种命名规范分别表示最常用的两种数据格式:

  • NCHWchannels_first
  • NHWCchannels_last

TensorFlow 默认采用 NHWC ,而在 NVIDIA GPU 上使用 cuDNN 时, NCHW 格式是最优选择。

最佳实践是构建同时支持两种数据格式的模型。这简化了 GPU 上的训练,并在 CPU 上运行推理。如果 TensorFlow 是通过 Intel MKL 优化编译的,许多操作,特别是与基于 CNN 的模型相关的操作,将会得到优化并支持 NCHW 。如果不使用 MKL,有些操作在使用 NCHW 时无法在 CPU 上运行。

这里我们简要介绍一下这两种格式的历史。TensorFlow 最开始使用 NHWC 是因为它在 CPU 上稍微快一点。但长期以来,我们一直在编写工具,让计算图可以自动重写,从而让两种格式的切换变得透明化,来实现一些优化。我们发现,尽管 NCHW 在一般情况下效率是最高的,但有些 GPU 操作在使用 NHWC 时确实更快一些。

通用的融合操作

XLA 概述

融合批量标准化

融合批量标准化(Fused batch norm)是将批量标准化所需的多个操作合并为一个内核。批量标准化是一个开销很大的过程,对于一些模型而言,它会占用很大比例的操作时间。通过使用融合批量标准化,可以实现 12%-30% 的加速。

tf.layers.batch_normalization

bn = tf.layers.batch_normalization(
    input_layer, fused=True, data_format='NCHW')

tf.contrib.layers.batch_norm

bn = tf.contrib.layers.batch_norm(input_layer, fused=True, data_format='NCHW')

RNN 性能

tf.nn.rnn_cell.BasicLSTMCell

tf.while_loop

tf.contrib.rnn.BasicLSTMCell

tf.contrib.rnn.LSTMBlockCell

tf.contrib.rnn.LSTMBlockFusedCell

tf.contrib.rnn.BasicLSTMCell

从源码构建和安装

默认情况下,TensorFlow 二进制程序已经覆盖了非常广泛的硬件种类,从而让每个人都能使用 TensorFlow。
如果用 CPU 来做训练或推理,建议编译 TensorFlow 时启用所有针对 CPU 的优化。对 CPU 上训练和推理的加速的文档参见 编译器优化的对比

通过源码安装 TensorFlow

# 此命令针对 Intel 的 Broadwell 处理器进行优化
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。
  • 选择一个版本的 gcc ,要求能够支持目标 CPU 能提供的所有优化。推荐的最低的 gcc 版本为 4.8.3。在 OS X 上,更新到最新的 Xcode 版本,并使用 Xcode 自带的那个版本的 clang。
  • 安装 TensorFlow 能够支持的最新的稳定版 CUDA 平台和 cuDNN 库。

GPU 上的优化

高性能模型

基准

  • Tesla K80 : 如果多个 GPU 位于同一个 PCI Express 根联合体上,且相互之间能够使用 NVIDIA GPUDirect 技术相通信,则将变量均匀地分布在这些 GPU 上进行训练是最好的方法。如果不能使用 GPUDirect,则变量放在 CPU 上是最好的办法。
  • Titan X (Maxwell 和 Pascal)、 M40、P100、及类似型号 : 对于像 ResNet 和 InceptionV3 这样的模型,将变量放在 CPU 上是最优选择,但是对于变量很多的模型,比如 AlexNet 和 VGG,结合 NCCL 使用 GPU 会更好一些。

将变量放在哪个设备上有一个通用的方法,那就是编写一个方法来为每个操作确定放置的位置,然后在调用 with tf.device(): 的时候用这个方法,而不要直接使用具体的设备名。考虑在两个 GPU 上训练一个模型的场景,假定其变量放
在 CPU 上。那么我们需要用一个循环来为这每个 GPU 来生成和放置一个“塔”。
一种惯用的放置方法是查看每个操作的类型,如果类型为 VariableVariableV2
VarHandleOp ,则此方法判断它们应该被放在 CPU 上,而其它所有操作被判定应该放在 GPU 上。
所以,计算图的构建过程应该是:

  • 在第一个循环中,为 gpu:0 创建一个模型的塔。在放置操作的过程中,我们的放置方法确定变量应放在 cpu:0 上,而所有其它操作应该放在 gpu:0 上。
  • 在第二个循环中, reuse 设为 True ,表示变量要被重用,然后为 gpu:1 生成一个“塔”。在放置这个塔上的操作时,那些已经被放在 cpu:0 上的变量会被重用,而所有其它的操作则被放在 gpu:1 上。

最后的结果是,所有的变量都被放在 CPU 上,而每个 GPU 上都有拷贝有一份模型中所有的计算操作。

下面的代码片断展示了两种不同的变量放置方法:一种是将变量放在 CPU 上,另一种是将变量均匀分布在各个 GPU 上。


class GpuParamServerDeviceSetter(object):
 """用 with tf.device() 来将变量放置在负载最小的 GPU 上。

 这个类的通常用法是传入一个 GPU 设备列表变量 ps_devices,其值类似于 ['gpu:0', 'gpu:1','gpu:2']。
 当放置变量时,每个变量会被放在负载最小的 gpu 上。所有其它操作,即那些计算操作,将被放在 worker_device 上。
  """

  def __init__(self, worker_device, ps_devices):
 """GpuParamServerDeviceSetter 的初始化函数
 参数:
 worker_device: 用于计算操作的设置
 ps_devices:一个设置列表,用于变量操作。每个变量被指定到最小负载的设备上
    """
    self.ps_devices = ps_devices
    self.worker_device = worker_device
    self.ps_sizes = [0] * len(self.ps_devices)

  def __call__(self, op):
    if op.device:
      return op.device
    if op.type not in ['Variable', 'VariableV2', 'VarHandleOp']:
      return self.worker_device

 # 从 ps_devices 中获得最小负载的设备
    device_index, _ = min(enumerate(self.ps_sizes), key=operator.itemgetter(1))
    device_name = self.ps_devices[device_index]
    var_size = op.outputs[0].get_shape().num_elements()
    self.ps_sizes[device_index] += var_size

    return device_name

def _create_device_setter(is_cpu_ps, worker, num_gpus):
 """创建设备设置器对象。"""
  if is_cpu_ps:
 # 如果 tf.train.replica_device_setter 支持在 CPU 上放置变量,所有放在一个 GPU 上,
    # 或放在 cluster_spec 中定义的服务器上(ps_servers)
    return tf.train.replica_device_setter(
        worker_device=worker, ps_device='/cpu:0', ps_tasks=1)
  else:
    gpus = ['/gpu:%d' % i for i in range(num_gpus)]
    return ParamServerDeviceSetter(worker, gpus)

# 本方法是一个完整例子中摘出来的一段代码经修改而得到的
def _resnet_model_fn():
 # 当设置为 False 时,变量会被放置在最小负载 GPU 上,如果设置为 True,变量会被放置在 CPU 上
    is_cpu_ps = False

 # 遍历所有 GPU,为每个 GPU 生成一个模型的“塔”副本
    for i in range(num_gpus):
      worker = '/gpu:%d' % i
 # 创建一个设备设置器,用于确定操作的放置位置
      device_setter = _create_device_setter(is_cpu_ps, worker, FLAGS.num_gpus)
 # 在第一个循环中创建变量,在接下来的循环中,reuse 被设置为 True
 # 于是,那些“塔”会共享这些变量
      with tf.variable_scope('resnet', reuse=bool(i != 0)):
        with tf.name_scope('tower_%d' % i) as name_scope:
 # tf.device 为创建的每个操作调用 device_setter
 # device_setter 返回此操作将要放置的设备
 with tf.device(device_setter):
 # 创建“塔”
            _tower_fn(is_training, weight_decay, tower_features[i],
                      tower_labels[i], tower_losses, tower_gradvars,
                      tower_preds, False)

在不远的将来,上述代码将只会用于演示目的,因为我们会推出高层次接口来支持主流的设备放置方法,其使用也要容易地多。
在我们逐步扩展 API 并更好地支持多 GPU 场景的同时, 示例
也会持续更新。

CPU 上的优化

通过源码安装 TensorFlow

除了使用了最新的指令集,Intel® 还在 Intel® 深度神经网络数学核心库(Math Kernel Library,Intel® MKL-DNN)
中加入了对 TensorFlow 的支持。虽然用词不完全准确,这些优化还是常被简称为 “MKL”,或“基于 MKL 的 TensorFlow”。
基于 Intel® MKL-DNN 的 TensorFlow 中详细介绍了 MKL 优化。

下面两种配置是通过调整线程池来优化 CPU 性能:

  • intra_op_parallelism_threads : 使用多线程来并行化计算的结点会将不同的计算单元分配到该池中的线程上
  • inter_op_parallelism_threads : 所有待计算结点都由此线程池来调度

这些配置是通过 tf.ConfigProto 来设置的,如下面代码所示,将其作为 config 参数传递到 tf.Session 即可。
对于这两种配置,如果都没有设置或设置为 0,则会默认使用逻辑 CPU 核心的数目。对于许多系统,包括 4 核的 CPU,以及包含
70 多个逻辑核心的多 CPU 系统,测试都显示默认设置已经非常高效。另一种常用策略是将线程池的大小设置为物理核的数目,而非逻辑核的数目。


  config = tf.ConfigProto()
  config.intra_op_parallelism_threads = 44
  config.inter_op_parallelism_threads = 44
  tf.session(config=config)

编译器优化对比 中,介绍了不同编译器优化的测试结果。

在 TensorFlow 中使用 Intel® MKL DNN

通过使用 Intel® 深度神经网络数学核心库的优化指令, Intel® 在 Xeon 和 Xeon Phi™ 芯片中加入对 TensorFlow 的优化。
这些优化也为消费级处理器提供了加速,比如 i5 和 i7 的 Intel 处理器。Intel 公司还发布了文章
在现代 Intel® 架构上的 TensorFlow* 优化 ,其中披露了实现的更多细节。

注意:TensorFlow 从 1.2 版本开始加入了对 MKL 的支持,但是目前只支持 Linux 平台。
而且,即使使用了 --config=cuda ,也是无法使用 MKL 的。

除了显著地改善了基于 CNN 的模型的训练效率,用 MKL 编译的代码针对 AVX 和 AVX2 也进行了优化。
因而,对于大部分现代处理器而言(2011 年之后),一次编译就可同时满足优化和兼容性需求。

TensorFlow 可用下面的命令来在编译中加入 MKL 优化,不同的 TensorFlow 版本会有差异。

对于 1.3.0 版本以后的 TensorFlow 源码:

./configure
# 选择所需的选项
bazel build --config=mkl --config=opt //tensorflow/tools/pip_package:build_pip_package

对于从 1.2.0 到 1.3.0 之间的版本:

./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
# 剩余的选项用默认值即可

bazel build --config=mkl --copt="-DEIGEN_USE_VML" -c opt //tensorflow/tools/pip_package:build_pip_package

MKL 调参以实现性能最优

本节详细介绍不同的配置和环境变量,用来调节 MKL 实现最优性能。在调整这些不同的环境变量之前,需要
确认模型正在使用的是 NCHW 数据格式 ,即 通道优先(channels_first) 格式。
MKL 是针对 NCHW 来做优化的,至于 NHWC ,Intel 对它的优化工作还在进行当中。

MKL 使用下列环境变量来调节性能:

  • KMP_BLOCKTIME - 设置线程在执行完一个并行计算区域之后进入睡眠之前的等待时间,单位是毫秒
  • KMP_AFFINITY - 启用运行时库来将线程绑定到物理处理单元上
  • KMP_SETTINGS - 启用(true)或禁用(false)程序执行过程中 OpenMP* 运行时库环境变量的打印
  • OMP_NUM_THREADS - 指定使用的线程数

关于 KMP 变量的更多详情参见 Intel 网站
OMP 变量则参见 gnu.org

虽然调节这些环境变量获益良多(后面会讨论到),但简化版的建议为:将 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 中默认为 200ms,但这在我们的测试中并不是最优的。在我们的测试中,0 (0ms) 对于基于 CNN 的模型是一个不错的默认值。对于 AlexNet 模型,最优值为 30ms,而 GoogleNet 和 VGG11 都为 1ms。
  • KMP_AFFINITY :建议设置为 granularity=fine,verbose,compact,1,0
  • OMP_NUM_THREADS : 默认值为物理核心数目。调整此参数时,如果其值超过核心数目,则会对某些模型在 Intel® Xeon Phi™ (Knights Landing) 芯片上的性能产生影响。关于 Intel 优化的详情,参见 现代 Intel® 架构上的 TensorFlow* 优化 一文。
  • intra_op_parallelism_threads : 推荐设置为物理核心数目。默认为 0,意为逻辑核心数目,这对于某些架构而言,是一个可行选项。这个变量的值应该和 OMP_NUM_THREADS 保持一致。
  • inter_op_parallelism_threads : 推荐设置为套接字数目。默认为 0,意为逻辑核心数目。

编译器优化的对比

下面的内容中整理了在不同平台、不同类型 CPU、以及不同编译器优化的情况下的训练和推理时的性能测试结果。我们测试的模型包括 ResNet-50 ( arXiv:1512.03385 ) 和 InceptionV3 ( arXiv:1512.00567 )。

对于每个测试,当用到 MKL 优化时,环境变量 KMP_BLOCKTIME 都被设置为 0 (0ms),而 KMP_AFFINITY 被设置为 granularity=fine,verbose,compact,1,0

InceptionV3 的推理

环境

  • 实例类型:AWS EC2 m4.xlarge
  • CPU: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz (Broadwell)
  • 数据集: ImageNet
  • TensorFlow 版本: 1.2.0 RC2
  • 测试脚本: tf_cnn_benchmarks.py

每批次样本数目:1

MKL 测试所执行的命令:

python tf_cnn_benchmarks.py --forward_only=True --device=cpu --mkl=True \
--kmp_blocktime=0 --nodistortions --model=inception3 --data_format=NCHW \
--batch_size=1 --num_inter_threads=1 --num_intra_threads=4 \
--data_dir=<path to ImageNet TFRecords>
优化数据格式图像数目/秒
(每步时间)
Intra 线程数Inter 线程数
AVX2NHWC7.0 (142ms)40
MKLNCHW6.6 (152ms)41
AVXNHWC5.0 (202ms)40
SSE3NHWC2.8 (361ms)40

每批次样本数目:32

MKL 测试所执行的命令:

python tf_cnn_benchmarks.py --forward_only=True --device=cpu --mkl=True \
--kmp_blocktime=0 --nodistortions --model=inception3 --data_format=NCHW \
--batch_size=32 --num_inter_threads=1 --num_intra_threads=4 \
--data_dir=<path to ImageNet TFRecords>
优化数据格式图像数目/秒
(每步时间)
Intra 线程数Inter 线程数
MKLNCHW10.3 (3,104ms)41
AVX2NHWC7.5 (4,255ms)40
AVXNHWC5.1 (6,275ms)40
SSE3NHWC2.8 (11,428ms)40

推理 ResNet-50

环境

  • 实例类型: AWS EC2 m4.xlarge
  • CPU: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz (Broadwell)
  • 数据集: ImageNet
  • TensorFlow 版本: 1.2.0 RC2
  • 测试脚本: tf_cnn_benchmarks.py

每批次样本数目:1

MKL 测试所执行的命令:

python tf_cnn_benchmarks.py --forward_only=True --device=cpu --mkl=True \
--kmp_blocktime=0 --nodistortions --model=resnet50 --data_format=NCHW \
--batch_size=1 --num_inter_threads=1 --num_intra_threads=4 \
--data_dir=<path to ImageNet TFRecords>
优化数据格式图像数目/秒
(每步时间)
Intra 线程数Inter 线程数
AVX2NHWC8.8 (113ms)40
MKLNCHW8.5 (120ms)41
AVXNHWC6.4 (157ms)40
SSE3NHWC3.7 (270ms)40

每批次样本数目:32

MKL 测试所执行的命令:

python tf_cnn_benchmarks.py --forward_only=True --device=cpu --mkl=True \
--kmp_blocktime=0 --nodistortions --model=resnet50 --data_format=NCHW \
--batch_size=32 --num_inter_threads=1 --num_intra_threads=4 \
--data_dir=<path to ImageNet TFRecords>
优化数据格式图像数目/秒
(每步时间)
Intra 线程数Inter 线程数
MKLNCHW12.4 (2,590ms)41
AVX2NHWC10.4 (3,079ms)40
AVXNHWC7.3 (4,4416ms)40
SSE3NHWC4.0 (8,054ms)40

训练 InceptionV3

环境

  • 实例类型: Dedicated AWS EC2 r4.16xlarge (Broadwell)
  • CPU: Intel Xeon E5-2686 v4 (Broadwell) Processors
  • 数据集: ImageNet
  • TensorFlow 版本: 1.2.0 RC2
  • 测试脚本: tf_cnn_benchmarks.py

MKL 测试所执行的命令:

python tf_cnn_benchmarks.py --device=cpu --mkl=True --kmp_blocktime=0 \
--nodistortions --model=resnet50 --data_format=NCHW --batch_size=32 \
--num_inter_threads=2 --num_intra_threads=36 \
--data_dir=<path to ImageNet TFRecords>
 优化数据格式图像数目/秒Intra 线程数Inter 线程数
MKLNCHW20.8362
AVX2NHWC6.2360
AVXNHWC5.7360
SSE3NHWC4.3360

另外,我们还让 ResNet 和 AlexNet
在此配置上以一种自组织(ad hoc)方式运行。只不过测试次数还不足以发布一份明确的结果。不过,不完整的测试结果很大程度上与上表结果类似,即 MKL 的效率为 AVX2 三倍多。

如果您发现本页面存在错误或可以改进,请 点击此处 帮助我们改进。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文