Performance Guide(性能指南)
性能指南
本指南包含一系列优化TensorFlow代码的最佳实践。该指南分为几个部分:
- 一般最佳实践涵盖了各种模型类型和硬件中常见的主题。
- 针对与GPU有关的GPU细节提示进行优化。
- 针对CPU详细信息优化CPU特定信息。
General best practices
以下各节介绍了与各种硬件和型号相关的最佳做法。最佳做法部分分为以下几部分:
- 输入管道优化
- 数据格式
- 常见的fused操作
- 从源代码构建和安装
输入管道优化
典型的模型从磁盘检索数据并在通过网络发送数据之前对其进行预处理。例如,处理JPEG图像的模型将遵循以下流程:从磁盘加载图像,将JPEG解码为张量,裁剪和填充,可能会翻转和扭曲,然后批量处理。这个流程被称为输入管道。随着GPU和其他硬件加速器变得更快,数据预处理可能成为瓶颈。
确定输入管道是否是瓶颈可能很复杂。最简单的方法之一是在输入管道之后将模型简化为单个操作(平凡模型)并每秒测量示例。如果整个模型和平凡模型的每秒示例差异最小,则输入管线可能是瓶颈。以下是一些确定问题的其他方法:
- 通过运行检查GPU是否未充分利用
nvidia-smi -l 2
。如果GPU利用率未达到80-100%,则输入管线可能是瓶颈。
- 生成一个时间表并查找大块空白(等待)。作为XLA JIT教程的一部分,存在一个生成时间线的例子。
- 检查CPU使用情况。有可能具有优化的输入流水线,并且缺乏CPU周期来处理流水线。
- 估计所需的吞吐量并验证使用的磁盘是否具有该吞吐量。一些云解决方案的网络连接磁盘的启动速度低于50 MB /秒,比旋转磁盘(150 MB /秒),SATA SSD(500 MB /秒)和PCIe SSD(2,000 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上。
使用数据集API
数据集API被替换queue_runner
为用于构建输入管道的推荐API。该API作为TensorFlow 1.2的一部分添加到contrib中,并将在不久的将来转向核心。此ResNet示例(arXiv:1512.03385)培训CIFAR-10说明了数据集API的使用tf.estimator.Estimator
。该数据集API使用C ++多线程,并且比基于queue_runner
Python的受Python多线程性能限制的开销低得多。
在使用数据feed_dict
提供高度灵活性的同时,在大多数情况下,使用feed_dict
并不能最佳地进行缩放。但是,在仅使用单个GPU的情况下,差异可以忽略不计。仍强烈建议使用数据集API。尽量避免以下情况:
# feed_dict often results in suboptimal performance when using large inputs.
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
使用大文件
读取大量小文件会显着影响I / O性能。获得最大I / O吞吐量的一种方法是将输入数据预处理为更大(〜100MB)的TFRecord
文件。对于较小的数据集(200MB-1GB),最好的方法是将整个数据集加载到内存中。文档下载并转换为TFRecord
格式包括用于创建的信息和脚本,TFRecords
并且此脚本将CIFAR-10数据集转换为TFRecords
。
数据格式
数据格式是指传递给给定操作的张量的结构。下面的讨论具体是关于代表图像的4D张量。在TensorFlow中,4D张量的部分通常由以下字母表示:
- N是指批次中的图像数量。
- H是指垂直(高度)维度中的像素数。
- W表示水平(宽度)维度中的像素数量。
- C是指通道数。例如,1代表黑白或灰度,3代表RGB。
在TensorFlow中,有两种命名约定代表两种最常见的数据格式:
NCHW
orchannels_first
NHWC
orchannels_last
NHWC
是TensorFlow的默认设置,并且NCHW
是使用cuDNN在NVIDIA GPU上训练时使用的最佳格式。
最佳实践是构建可同时处理两种数据格式的模型。这简化了对GPU的训练,然后在CPU上运行推理。如果使用英特尔MKL优化编译TensorFlow,则会优化和支持许多操作,尤其是与基于CNN的模型相关的操作NCHW
。如果不使用MKL,则在使用时某些操作在CPU上不受支持NCHW
。
这两种格式的简要历史是TensorFlow开始使用,NHWC
因为它在CPU上速度稍快。从长远来看,我们正在研究自动重写图形的工具,以便在不同格式之间进行切换,并利用微操作优化的优势,其中GPU操作系统可能NHWC
比通常最高效的操作更快NCHW
。
常见的 fused操作
Fused Ops将多个操作组合到单个内核中以提高性能。TensorFlow中有许多融合操作,XLA将尽可能创建融合操作,以自动提高性能。下面收集的是精选的融合行动,可以大大提高性能,并可能被忽视。
Fused 批量标准
熔合批处理标准将批量标准化所需的多个操作组合到单个内核中。批量标准是一个昂贵的过程,对于某些型号占用了大部分操作时间。使用融合批量规范可以导致12%-30%的加速。
有两种常用的批量规范,都支持融合。核心tf.layers.batch_normalization
添加融合从TensorFlow 1.3开始。
bn = tf.layers.batch_normalization(
input_layer, fused=True, data_format='NCHW')
tf.contrib.layers.batch_norm
自TensorFlow 1.0之前,contrib 方法已经融合为一个选项。
bn = tf.contrib.layers.batch_norm(input_layer, fused=True, data_format='NCHW')
从源代码构建和安装
默认的TensorFlow二进制文件针对最广泛的硬件,以使每个人都可以访问TensorFlow。如果使用CPU进行培训或推理,建议编译TensorFlow并使用所有可用于CPU的优化。在比较编译器优化时,下面介绍了有关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缓存。如果使用码头集装箱,则数据不会被缓存,并且每次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库。
针对GPU进行优化
本节包含一般最佳实践中未涉及的GPU特定提示。在多GPU上获得最佳性能是一项挑战。常用的方法是使用数据并行。通过使用数据并行性进行扩展包括制作模型的多个副本(称为“塔”),然后在每个GPU上放置一个塔。每个塔都运行在不同的小批量数据上,然后更新需要在每个塔之间共享的变量(也称为参数)。每个塔如何获得更新的变量以及梯度如何应用都会影响模型的性能,缩放和收敛性。本节的其余部分概述了多个GPU上的变量放置和模型高耸。
处理变量更新的最佳方法取决于模型,硬件以及硬件的配置方式。一个例子是,两个系统可以使用NVIDIA Tesla P100构建,但可能使用PCIe和另一个NVLink。在这种情况下,每个系统的最佳解决方案可能会有所不同。有关真实世界的示例,请阅读基准页面,其中详细介绍了适用于各种平台的最佳设置。以下是对各种平台和配置进行基准测试所得到的总结:
Tesla K80
:如果GPU处于同一个PCI Express根联合体上,并且能够使用NVIDIA GPUDirect Peer to Peer,那么将这些变量平均放置在用于训练的GPU上是最好的方法。如果GPU不能使用GPUDirect,那么将变量放在CPU上是最好的选择。
Titan X(Maxwell和Pascal),M40,P100等类似
:对于像ResNet和InceptionV3这样的模型,将变量放置在CPU上是最佳设置,但对于像AlexNet和VGG等很多变量的模型,使用GPUNCCL
更好。
管理变量放置位置的常用方法是创建一个方法来确定每个Op的放置位置,并在调用时使用该方法代替特定的设备名称with tf.device():
。考虑在2个GPU上训练模型并将变量放置在CPU上的场景。将会有一个循环用于在两个GPU的每一个上创建和放置“塔”。自定义设备放置方法将被创建,对于类型的行动手表Variable
,VariableV2
以及VarHandleOp
和表明它们将被放置在CPU上。所有其他操作系统将被放置在目标GPU上。该图的构建过程如下:
- 在第一个循环中,模型的“tower”将被创建
gpu:0
。在放置操作期间,自定义设备放置方法将指示变量将被放置在cpu:0
所有其他操作上gpu:0
。
- 在第二个循环中,
reuse
设置为True
指示变量将被重用,然后创建“塔”gpu:1
。在放置与“塔”相关联的Ops期间,cpu:0
重复使用放置的变量,并创建并放置所有其他Opsgpu:1
。
最终的结果是所有的变量都放在CPU上,每个GPU都有与模型相关的所有计算OPS的副本。
下面的代码片段展示了两种不同的变量放置方式:一种是在CPU上放置变量; 另一个是在GPU上平均放置变量。
class GpuParamServerDeviceSetter(object):
"""Used with tf.device() to place variables on the least loaded GPU.
A common use for this class is to pass a list of GPU devices, e.g. ['gpu:0',
'gpu:1','gpu:2'], as ps_devices. When each variable is placed, it will be
placed on the least loaded gpu. All other Ops, which will be the computation
Ops, will be placed on the worker_device.
"""
def __init__(self, worker_device, ps_devices):
"""Initializer for GpuParamServerDeviceSetter.
Args:
worker_device: the device to use for computation Ops.
ps_devices: a list of devices to use for Variable Ops. Each variable is
assigned to the least loaded device.
"""
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
# Gets the least loaded ps_device
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):
"""Create device setter object."""
if is_cpu_ps:
# tf.train.replica_device_setter supports placing variables on the CPU, all
# on one GPU, or on ps_servers defined in a cluster_spec.
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)
# The method below is a modified snippet from the full example.
def _resnet_model_fn():
# When set to False, variables are placed on the least loaded GPU. If set
# to True, the variables will be placed on the CPU.
is_cpu_ps = False
# Loops over the number of GPUs and creates a copy ("tower") of the model on
# each GPU.
for i in range(num_gpus):
worker = '/gpu:%d' % i
# Creates a device setter used to determine where Ops are to be placed.
device_setter = _create_device_setter(is_cpu_ps, worker, FLAGS.num_gpus)
# Creates variables on the first loop. On subsequent loops reuse is set
# to True, which results in the "towers" sharing variables.
with tf.variable_scope('resnet', reuse=bool(i != 0)):
with tf.name_scope('tower_%d' % i) as name_scope:
# tf.device calls the device_setter for each Op that is created.
# device_setter returns the device the Op is to be placed on.
with tf.device(device_setter):
# Creates the "tower".
_tower_fn(is_training, weight_decay, tower_features[i],
tower_labels[i], tower_losses, tower_gradvars,
tower_preds, False)
在不久的将来,上述代码仅用于说明目的,因为将有易于使用的高级方法来支持各种流行的方法。随着API扩展并演变以解决多GPU情况,此示例将继续得到更新。
针对CPU进行优化
包含英特尔®至强融核™的CPU在TensorFlow 从源代码构建且实现目标CPU支持的所有指令时实现最佳性能。
除了使用最新的指令集以外,英特尔®还将针对深度神经网络(英特尔®MKL-DNN)的英特尔®数学核心库支持添加到TensorFlow中。虽然名称不完全准确,但这些优化通常简称为“MKL”或“MKL的TensorFlow”。采用英特尔®MKL-DNN的TensorFlow包含有关MKL优化的详细信息。
下面列出的两种配置用于通过调整线程池来优化CPU性能。
intra_op_parallelism_threads
:可以使用多个线程来并行执行的节点会将各个部分安排到该池中。
inter_op_parallelism_threads
:所有就绪节点都安排在此池中。
这些配置通过以下代码段中所示的属性设置tf.ConfigProto
并传递给属性。对于这两种配置选项,如果它们未设置或设置为0,则默认为逻辑CPU内核的数量。测试表明,默认值对于从一个4核CPU的系统到70多个组合逻辑核心的多CPU都是有效的。常见的备选优化是将两个池中的线程数设置为等于物理内核数而不是逻辑内核数。tf.Sessionconfig
config = tf.ConfigProto()
config.intra_op_parallelism_threads = 44
config.inter_op_parallelism_threads = 44
tf.session(config=config)
比较编译器优化部分包含使用不同编译器优化的测试结果。
采用英特尔®MKL DNN的TensorFlow
尽管使用英特尔®深度神经网络数学核心库(英特尔®MKL-DNN)优化原语,英特尔®为TensorFlow增加了对英特尔®至强®和英特尔®至强融核™的优化。优化还为消费类系列处理器提供加速,例如i5和i7英特尔处理器。英特尔发布的论文“ TensorFlow *现代英特尔®架构优化”包含有关实施的其他详细信息。
注意:
MKL自TensorFlow 1.2开始添加,目前仅适用于Linux。在使用时也不起作用--config=cuda
。
除了为训练基于CNN的模型提供显着的性能改进之外,使用MKL编译创建了针对AVX和AVX2优化的二进制文件。结果是一个经过优化的单一二进制文件,并且与大多数现代(2011年后)处理器兼容。
可以使用以下命令根据所使用的TensorFlow源的版本使用MKL优化来编译TensorFlow。
对于1.3.0之后的TensorFlow源版本:
./configure
# Pick the desired options
bazel build --config=mkl -c opt //tensorflow/tools/pip_package:build_pip_package
对于TensorFlow版本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
# 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
而且英特尔正在努力在使用时获得接近性能的平价NHWC
。
MKL使用以下环境变量来调整性能:
- KMP_BLOCKTIME - 设置线程在睡眠之前完成并行区域执行后应该等待的时间(以毫秒为单位)。
- KMP_AFFINITY - 启用运行时库将线程绑定到物理处理单元。
- KMP_SETTINGS - 在程序执行期间启用(true)或禁用(false)打印OpenMP *运行时库环境变量。
- OMP_NUM_THREADS - 指定要使用的线程数。
有关KMP变量的更多详细信息在Intel网站上,以及gnu.org上的OMP变量
尽管调整环境变量会有很大的收益,这在下面讨论,但简单的建议是将其设置为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设置MKL变量的例子os.environ
:
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,这在我们的测试中并不是最佳的。对于经过测试的基于CNN的模型,0(0ms)是一个很好的默认值。AlexNex的最佳性能达到了30ms,GoogleNet和VGG11在1ms时表现最佳。
KMP_AFFINITY
:推荐的设置是granularity=fine,verbose,compact,1,0
。
OMP_NUM_THREADS
:这默认为物理核心的数量。在某些型号上使用英特尔®至强融核™(骑士降落系统)时,调整此参数超出核心数量可能会产生影响。请参阅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
.Inference InceptionV3
- Instance Type: AWS EC2 m4.xlarge
- CPU: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz (Broadwell)
- Dataset: ImageNet
- TensorFlow Version: 1.2.0 RC2
- Test Script: 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>
Optimization | Data Format | Images/Sec (step time) | Intra threads | Inter Threads |
---|---|---|---|---|
AVX2 | NHWC | 7.0 (142ms) | 4 | 0 |
MKL | NCHW | 6.6 (152ms) | 4 | 1 |
AVX | NHWC | 5.0 (202ms) | 4 | 0 |
SSE3 | NHWC | 2.8 (361ms) | 4 | 0 |
批量大小: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>
Optimization | Data Format | Images/Sec (step time) | Intra threads | Inter Threads |
---|---|---|---|---|
MKL | NCHW | 10.3 (3,104ms) | 4 | 1 |
AVX2 | NHWC | 7.5 (4,255ms) | 4 | 0 |
AVX | NHWC | 5.1 (6,275ms) | 4 | 0 |
SSE3 | NHWC | 2.8 (11,428ms) | 4 | 0 |
推论ResNet-50
环境
- Instance Type: AWS EC2 m4.xlarge
- CPU: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz (Broadwell)
- Dataset: ImageNet
- TensorFlow Version: 1.2.0 RC2
- Test Script: 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>
Optimization | Data Format | Images/Sec (step time) | Intra threads | Inter Threads |
---|---|---|---|---|
AVX2 | NHWC | 8.8 (113ms) | 4 | 0 |
MKL | NCHW | 8.5 (120ms) | 4 | 1 |
AVX | NHWC | 6.4 (157ms) | 4 | 0 |
SSE3 | NHWC | 3.7 (270ms) | 4 | 0 |
批量大小: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>
Optimization | Data Format | Images/Sec (step time) | Intra threads | Inter Threads |
---|---|---|---|---|
MKL | NCHW | 12.4 (2,590ms) | 4 | 1 |
AVX2 | NHWC | 10.4 (3,079ms) | 4 | 0 |
AVX | NHWC | 7.3 (4,4416ms) | 4 | 0 |
SSE3 | NHWC | 4.0 (8,054ms) | 4 | 0 |
训练初期V3
环境
- Instance Type: Dedicated AWS EC2 r4.16xlarge (Broadwell)
- CPU: Intel Xeon E5-2686 v4 (Broadwell) Processors
- Dataset: ImageNet
- TensorFlow Version: 1.2.0 RC2
- Test Script: 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>
Optimization | Data Format | Images/Sec | Intra threads | Inter Threads |
---|---|---|---|---|
MKL | NCHW | 20.8 | 36 | 2 |
AVX2 | NHWC | 6.2 | 36 | 0 |
AVX | NHWC | 5.7 | 36 | 0 |
SSE3 | NHWC | 4.3 | 36 | 0 |
ResNet和AlexNet也以这种配置运行,但是以特别的方式运行。没有足够的运行来发布连贯的结果表。不完整的结果强烈表明,最终结果与上表类似,MKL提供了比AVX2高3倍以上的收益。