当训练一个机器学习模型时,性能是个大问题。该指南包含了一些优化tensorflow代码的最佳实践。分为几个部分:
常见模型会从磁盘中抽取数据,进行预处理,然后通过网络发送数据。例如,处理JPEG图片的模型会有下面的流程:从磁盘加载图片,将JPEG解码成一个tensor,进行裁减(crop)和补齐(pad),可能还会进行翻转(flip)和扭曲(distort),然后再batch。该流程被称为input pipeline。随着GPUs和其它硬件加速器越来越快,数据预处理可能是个瓶劲。
判断input pipeline是否是瓶颈可能很复杂。一个最简单的方法是,在pipeline之后,将模型减至单个操作(trivial model),并测量每秒处理的样本数。如果对于完整模型(full model)和简单模型(trivial model)每秒处理样本的差距很小,那么输入的pipeline很可能是瓶颈。下面还有其它方法来验证该问题:
在CPU上放置input pipeline操作,可以极大提升性能。对于input pipeline,使用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,input function会被自动放置在CPU上。
对于构建input pipeline,推荐使用Dataset API来替代queue_runner。该API在tensorFlow 1.2中作为contrib的一部分被添加进去,并在后续版本会移至core包中。ResNet example arXiv:1512.03385会训练CIFAR-10,它展示了如何使用Dataset API以及tf.estimator.Estimator。Dataset API会使用C++的多线程机制,会比基于python的queue_runner(受限于python的多线程低性能)的开销更低。
当使用一个feed_dict来feeding数据时,会提供更高的灵活性,使用feed_dict的大多数实例不可以进行合适的扩展。然而,在只有一个GPU的实例中,这种差异是微不足道的。我们仍推荐使用Dataset API。避免使用以下的情况:
# feed_dict often results in suboptimal performance when using large inputs.
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
如果输入是JPEG图片,也需要裁减(cropping),使用fused tf.image.decode_and_crop_jpeg可以加速预处理。tf.image.decode_and_crop_jpeg只会解码(decode)在图片裁减窗口(crop window)上的部分。如果裁减窗口比起整个图片更小,就会极大地加速处理。对于ImageNet数据,该方法可以极大加速input pipeline,可提升30%。
示例:
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在所有平台上都有提供。在Windows平台上不会有加速,因为它使用libjpeg,而在其它平台上则使用libjpeg-turbo。
读取大量小文件可以极大影响I/O性能。获取最大的I/O吞吐量的其中一种方法是,将数据预处理成更大的(~100MB) TFRecord文件。对于更小的数据集(200MB-1GB),最好的方法通常是加载整个数据集到内存中。文档下载和转化成TFRecord格式包含了相关的信息和脚本来创建TFRecords,该脚本会将CIFAR-10数据集转化成TFRecords。
数据格式指的是传递给一个给定Op的Tensor结构。下面的讨论关于表示图片的4D Tensors。在TensorFlow中,4D tensor的部分通常指的是:
在TensorFlow中,存在两个命名约定,来表示两种最常用的数据格式:
NHWC是Tensorflow的缺省方式,NCHW是当在NVIDIA GPU上使用cuDNN训练时的最优格式。
最好的实践是,构建支持两种数据格式的模型。这可以简化在GPU上的训练,接着在CPU上进行inference。如果TensorFlow在Intel MKL优化器上进行编译,许多op,特别是那些与基于CNN相关的模型,会被优化并支持NCHW。如果不使用MKL,当使用NCHW时,一些op不会支持在CPU上运行。
这两种格式的历史是,TensorFlow刚开始使用NHWC是因为它在CPU上更快一些。在很长一段时间内,我们致力于开发工具来自动化重写graphs来在格式间进行透明切换,并充分利用微优化:比起常用有效的NCHW,使用NHWC的一个GPU Op会更快。
Fused Ops会将多个Op结合成单个kernel来提升性能。在Tensorflow中有许多fused Ops,当可能时XLA会创建fused Ops来自动提升性能。下面的示例会使用fused Ops,可以极大提升性能。
Fused batch norm 会结合多种op,来将batch归一化(normalization)到单个kernel中。对于一些模型,Batch norm是一个开销昂贵的处理,会占据大量的操作时间。使用fused batch norm可以进行12%-30%的加速。
有两个常用的batch norm,它们同时支持fusing。核心的tf.layers.batch_normalization在TensorFlow 1.3中被添加进去。
bn = tf.layers.batch_normalization(
input_layer, fused=True, data_format='NCHW')
自TensorFlow 1.0后,tf.contrib.layers.batch_norm方法具有fused操作。
bn = tf.contrib.layers.batch_norm(input_layer, fused=True, data_format='NCHW')
缺省的TensorFLow二进制包面向大多数的硬件,以便TensorFlow能为所有人所使用。如果使用CPU进行training或inference,推荐使用CPU的所有优化来编译TensorFlow。在CPU中的training和inference加速在Comparing compiler optimizations有文档说明。
为了安装最优化版的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
环境,构建,安装tips:
该部分包含了GPU相关的tops,它在最佳实践中没有被涵盖。在multi-GPU上获取最优性化是个挑战。常用的方法是使用数据并行化。通过使用数据并行化进行scaling涉及到生成多个模型拷贝,这被称为是“塔(towers)”,接着在每个GPU上放置一个tower。每个tower在一个不同的mini-batch数据上进行操作,接着更新变量(也称为参数),这些变量需要在每个towers间进行共享。然而,每个tower如何去获取更新后的变量,以及梯度如何被应用,会对性能、可扩展性、模型收敛都有影响。该节其它部分会提供了关于变量放置的总览、以及如何在多GPU上对模型进行towering。高性能模型有进一步的介绍,它介绍了用于在tower间进行共享和更新变量的复杂方法。
处理变量更新的最好方法依赖于模型、硬件、以及硬件如何配置。有个示例,两个系统可以使用NVIDIA Tesla P100s进行构建,但其中一个使用PCIe,另一个使用NVLink。在这种情况下,每个系统的可选解决方案可以不一样。对于真实的示例,读取benchmark页,它有关于多种平台设置的详情。下面是一些平台的bechmarking和配置:
对放置变量进行管理的最常用方法是,创建一个方法来决定每个Op放置的地方,并在一个指定设备上(通过调用with tf.device())使用该方法:考虑到这样的场景:一个模型在2个GPU上进行训练,变量被放置在CPU上。在每个GPU上存在一个loop来创建和放置”towers”。一个定制的设备放置方法可以被创建,该方法会监视类型为Variable、VariableV2、以及VarHandleOp的Ops,以及表明了它们被放置在CPU上。所有其它的Ops可以被放置在目标GPU上。graph的构建可以如下进行处理:
最终结果是,在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的场景。
当Tensorflow使用源码、根据目标CPU的说明书进行构建时,CPUs(包括Intel® Xeon Phi™,)可以达到最优性能。
使用最新的指令集,Intel® 已经添加了Intel® Math Kernel Library for Deep Neural Networks (Intel® MKL-DNN) 的支持到TensorFlow中。其中,该名字并不完全准确,这些优化经常被称为’MKL’ 或者’TensorFlow with MKL’。TensorFlow with Intel® MKL-DNN包含了在MKL上优化的详情。
下面列出了两种配置,通过调整线程池来用于优化CPU性能:
这些配置通过tf.ConfigProto进行设置,并传递给tf.Session的config属性项中。对于这两种配置选项,如果都未设置或者置为0, 将缺省为逻辑CPU cores的个数。实验说明,对于4核的单CPU,以及70+组合的逻辑cores的CPU,缺省是有效的。一个常见的可选优化是,在池中线程数设置成物理核的数目,而非逻辑核的数目。
config = tf.ConfigProto()
config.intra_op_parallelism_threads = 44
config.inter_op_parallelism_threads = 44
tf.session(config=config)
Comparing compiler optimizations 包含了使用不同编译器优化的结果。
tensorflow performance