在线文档教程

High-Performance Models(高性能的模型)

高性能模型

本文档和附带的脚本详细介绍了如何构建针对各种系统类型和网络拓扑的高度可扩展模型。本文档中的技术利用了一些低级的TensorFlow Python基元。未来,这些技术中的许多技术将被整合到高级API中。

输入管道

“性能指南”解释了如何确定可能的输入管道问题和最佳实践。我们发现,当使用大输入和每秒更高采样率处理时,使用tf.FIFOQueuetf.train.queue_runner不能饱和多个当代GPU,例如使用AlexNet训练ImageNet。这是由于使用Python线程作为其底层实现。Python线程的开销太大。

我们在脚本中实现的另一种方法是使用TensorFlow中的本地并行性构建输入管道。我们的实施由三个阶段组成:

  • I / O读取:从磁盘选择并读取图像文件。

  • 图像处理:将图像记录解码成图像,预处理,并组织成小批量。

  • CPU到GPU数据传输:将图像从CPU传输到GPU。

每个阶段的主要部分与其他阶段并行执行data_flow_ops.StagingAreaStagingArea是一个类似队列的运算符tf.FIFOQueue。不同之处在于StagingArea不能保证FIFO排序,但提供更简单的功能,并且可以在CPU和GPU上与其他阶段并行执行。将输入管道分成3个并行独立运行的阶段,可扩展并充分利用大型多核环境。本节的其余部分详细介绍了有关使用的详细信息data_flow_ops.StagingArea

并行化I / O读取

data_flow_ops.RecordInput用于从磁盘并行读取数据。给定表示TFRecords的输入文件列表,RecordInput使用后台线程连续读取记录。记录放置在自己的大型内部池中,当它加载至少一半的容量时,它会产生输出张量。

这个操作系统有自己的内部线程,占用I / O时间占用最少的CPU,这使得它可以平稳地与其余的模型并行运行。

并行化图像处理

读取图像后,将RecordInput它们作为张量传递给图像处理管道。为了使图像处理流水线更容易解释,假设输入流水线的目标是8个GPU,批量为256(每个GPU 32个)。

并行读取并处理256条记录。这从图中的256个独立的RecordInput读操作开始。每个读取操作后面都有一组相同的用于图像预处理的操作,这些操作被认为是独立的并且被并行执行。图像预处理操作包括诸如图像解码,失真和调整大小的操作。

一旦图像经过预处理,它们将被连接在一起形成8个张量,每个批量为32个批量。而不是tf.concat用于这个目的,它被实现为一个单独的操作,在将它们连接在一起之前等待所有输入准备就绪,tf.parallel_stack被使用。tf.parallel_stack分配一个未初始化的张量作为输出,并且一旦输入可用,每个输入张量就被写入输出张量的指定部分。

当所有的输入张量完成后,输出张量将在图中传递。这有效地隐藏了所有的输入张量产生的长尾巴的内存延迟。

并行化CPU到GPU数据传输

继续假定目标是8个批量为256的GPU(每个GPU 32个)。一旦输入图像由CPU处理和连接在一起,我们就有8个张量,每个批量大小为32。

TensorFlow使张量从一个设备可以直接在任何其他设备上使用。TensorFlow会插入隐式副本,以使张量在使用它们的任何设备上可用。在实际使用张量之前,运行时间安排设备之间的副本运行。但是,如果副本无法及时完成,那么需要这些张量的计算将会失速并导致性能下降。

在此实现中,data_flow_ops.StagingArea用于并行显式调度副本。最终的结果是,当在GPU上开始计算时,所有张量已经可用。

软件流水线

所有阶段都能够由不同的处理器驱动,data_flow_ops.StagingArea在它们之间使用,以便它们并行运行。StagingArea是类似队列的运算符,tf.FIFOQueue它提供了可在CPU和GPU上执行的更简单的功能。

在模型开始运行所有阶段之前,输入流水线阶段将被预热,以便在一组数据之间填充暂存缓冲区。在每个运行步骤中,从每个阶段开始时的分段缓冲区中读取一组数据,并在结束时推入一组数据。

例如:如果有三个阶段:A,B和C.中间有两个中间区域:S1和S2。在热身期间,我们运行:

Warm up: Step 1: A0 Step 2: A1 B0 Actual execution: Step 3: A2 B1 C0 Step 4: A3 B2 C1 Step 5: A4 B3 C2

预热后,S1和S2各有一组数据。对于实际执行的每一步,从每个暂存区域消耗一组数据,并向每个暂存区域添加一组数据。

使用此方案的好处:

  • 所有阶段都是非阻塞的,因为暂存区域在预热后总是有一组数据。

  • 每个阶段都可以并行运行,因为它们都可以立即启动。

  • 分段缓冲区具有固定的内存开销。他们最多只会有一组额外的数据。

  • 只需要一次session.run()调用即可运行该步骤的所有阶段,这使分析和调试变得更加容易。

构建高性能模型的最佳实践

以下收集的是一些可以提高性能并增加模型灵活性的其他最佳实践。

与NHWC和NCHW一起构建模型

CNN使用的大多数TensorFlow操作都支持NHWC和NCHW数据格式。在GPU上,NCHW速度更快。但在CPU上,NHWC有时会更快。

建立一个支持这两种数据格式的模型可以保持模型的灵活性,并且无论平台如何都能够以最佳方式运行 CNN使用的大多数TensorFlow操作都支持NHWC和NCHW数据格式。该基准脚本是为支持NCHW和NHWC而编写的。在使用GPU进行训练时应始终使用NCHW。NHWC在CPU上有时更快。一个灵活的模型可以在GPU上使用NCHW进行训练,并使用NHWC在CPU上进行推理,并从训练中获得权重。

使用融合的批处理-规范化

TensorFlow中的默认批量标准化是作为组合操作实现的。这是非常普遍的,但往往导致表现欠佳。另一种方法是使用融合的批量标准化,通常在GPU上具有更好的性能。以下是使用tf.contrib.layers.batch_norm实现融合批量标准化的示例。

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

变量分布和梯度聚合

在训练期间,训练变量值使用聚合渐变和增量更新。在基准脚本中,我们证明了使用灵活的通用TensorFlow基元可以构建各种各样的高性能分布和聚合方案。

脚本中包含三个变量分配和聚合的例子:

  • parameter_server训练模型的每个副本都从参数服务器读取变量并独立更新变量。当每个模型需要这些变量时,它们将通过TensorFlow运行时添加的标准隐式副本进行复制。示例脚本举例说明了如何使用此方法进行本地训练,分布式同步训练和分布式异步训练。

  • replicated在每个GPU上放置每个训练变量的相同副本。正向和反向计算可以立即开始,因为变量数据立即可用。所有GPU都会累积渐变,并将累计总和应用于每个GPU的变量副本以保持同步。

  • distributed_replicated将每个GPU上的训练参数的副本与参数服务器上的主副本一起放置。正向和反向计算可以立即开始,因为变量数据立即可用。渐变在每个服务器上的所有GPU上累积,然后将每个服务器聚合渐变应用于主副本。在所有工作人员这样做后,每个工作人员都会从主副本更新其变量副本。

以下是关于每种方法的更多细节。

参数服务器变量

在TensorFlow模型中管理可训练变量的最常用方法是参数服务器模式。

在分布式系统中,每个工作进程运行相同的模型,参数服务器进程拥有变量的主副本。当工作人员需要参数服务器的变量时,它直接引用它。TensorFlow运行时将隐式副本添加到图形中,以便在需要它的计算设备上使用变量值。在工作人员上计算梯度时,会将其发送到拥有特定变量的参数服务器,并使用相应的优化程序更新变量。

有一些技术可以提高吞吐量:

  • 这些变量根据参数的大小在参数服务器之间传播,以实现负载平衡。

  • 当每个工作人员都有多个GPU时,梯度会在GPU上累积,并将单个聚合梯度发送到参数服务器。这会降低网络带宽和参数服务器完成的工作量。

为了协调工作人员,一种非常常见的模式是异步更新,其中每个工作人员更新变量的主副本而不与其他工作人员同步。在我们的模型中,我们演示了在工作人员之间引入同步相当容易,因此所有工作人员的更新都可以在下一步开始前一步完成。

参数服务器方法也可以用于本地培训。在这种情况下,它不是通过参数服务器传播变量的主副本,而是在CPU上或分布在可用的GPU上。

由于这种设置的简单性,这种架构在社区内已经获得了很多人气。

通过传递可以在脚本中使用此模式--variable_update=parameter_server

复制变量

在这种设计中,服务器上的每个GPU都有自己的每个变量的副本。通过将完全聚合的渐变应用于每个GPU的变量副本,这些值在GPU中保持同步。

变量和数据在训练开始时可用,因此训练的正向传球可以立即开始。渐变在设备中聚合,然后将完全聚合的渐变应用于每个本地副本。

服务器中的渐变聚合可以通过不同的方式完成:

  • 使用标准的TensorFlow操作在单个设备(CPU或GPU)上累计总量,然后将其复制回所有GPU。

  • 使用NVIDIA®NCCL,如下面的NCCL部分所述。

通过传递可以在脚本中使用此模式--variable_update=replicated

分布式训练中的复制变量

变量的复制方法可以扩展到分布式培训。一种方法可以像复制模式那样执行此操作:在集群中完全聚合渐变并将它们应用于变量的每个本地副本。这可能会在此脚本的未来版本中显示; 脚本确实呈现出不同的变体,这里描述。

在这种模式下,除了每个GPU的变量副本之外,主副本都存储在参数服务器上。与复制模式一样,培训可以立即使用变量的本地副本开始。

随着权重的渐变可用,它们将被发送回参数服务器,并更新所有本地副本:

1. 来自同一工作人员的GPU的所有渐变都聚合在一起。

2. 来自每个工作者的聚合梯度被发送到拥有变量的参数服务器,其中指定的优化器用于更新变量的主副本。

3. 每个工作人员从主服务器更新其变量的本地副本。在示例模型中,这是通过交叉复制屏障完成的,该交叉复制屏障等待所有工作人员完成更新变量,并且仅在屏障被所有副本释放后才获取新变量。一旦副本完成所有变量,这标志着训练步骤的结束,并且可以开始新的步骤。

尽管这听起来与参数服务器的标准使用类似,但在许多情况下性能往往更好。这很大程度上是由于计算可以没有任何延迟地发生,早期梯度的大部分拷贝延迟可以被后来的计算层隐藏。

通过传递可以在脚本中使用此模式--variable_update=distributed_replicated

NCCL

为了在同一主机内的不同GPU上广播变量和聚合梯度,我们可以使用默认的TensorFlow隐式复制机制。

但是,我们可以使用可选的NCCL(tf.contrib.nccl)支持。NCCL是一个NVIDIA®库,可以在不同的GPU上高效地广播和聚合数据。它在每个知道如何最好地利用底层硬件拓扑的GPU上调度合作内核; 此内核使用GPU的单个SM。

在我们的实验中,我们证明,尽管NCCL本身通常导致更快的数据聚合,但它并不一定会导致更快的培训。我们的假设是,隐式拷贝基本上是免费的,因为它们会转到GPU上的拷贝引擎,只要它的延迟可以被主计算本身隐藏起来。尽管NCCL可以更快地传输数据,但它只需要一个SM,并为底层L2缓存增加更多压力。我们的结果显示,对于8 GPU,NCCL通常会带来更好的性能。但是,对于较少的GPU,隐式副本通常表现更好。

分阶段变量

我们进一步介绍了一种stage-variable模式,我们使用中间区域来进行变量读取和更新。与输入管道的软件流水线类似,这可以隐藏数据拷贝延迟。如果计算时间比复制和聚合花费的时间更长,则复制本身变得基本上空闲。

缺点是所有读取的权重都来自之前的训练步骤。所以它是一种与SGD不同的算法。但是可以通过调整学习率和其他超参数来提高其收敛性。

执行脚本

本节列出了执行主脚本(tf_cnn_benchmarks.py)的核心命令行参数和一些基本示例。

注意: tf_cnn_benchmarks.py使用force_gpu_compatible在TensorFlow 1.1之后引入的配置。建议TensorFlow 1.2发布之前从源建立。

基本的命令行参数

  • model:型号使用,例如resnet50inception3vgg16,和alexnet

  • num_gpus:要使用的GPU数量。

  • data_dir:要处理的数据的路径。如果未设置,则使用合成数据。要使用Imagenet数据,请使用这些说明作为起点。

  • batch_size:每个GPU的位宽大小。

  • variable_update:用于管理变量的方法:parameter_serverreplicateddistributed_replicatedindependent

  • local_parameter_device:用作参数服务器的设备:cpugpu

单例实例

# VGG16 training ImageNet with 8 GPUs using arguments that optimize for # Google Compute Engine. python tf_cnn_benchmarks.py --local_parameter_device=cpu --num_gpus=8 \ --batch_size=32 --model=vgg16 --data_dir=/home/ubuntu/imagenet/train \ --variable_update=parameter_server --nodistortions # VGG16 training synthetic ImageNet data with 8 GPUs using arguments that # optimize for the NVIDIA DGX-1. python tf_cnn_benchmarks.py --local_parameter_device=gpu --num_gpus=8 \ --batch_size=64 --model=vgg16 --variable_update=replicated --use_nccl=True # VGG16 training ImageNet data with 8 GPUs using arguments that optimize for # Amazon EC2. python tf_cnn_benchmarks.py --local_parameter_device=gpu --num_gpus=8 \ --batch_size=64 --model=vgg16 --variable_update=parameter_server # ResNet-50 training ImageNet data with 8 GPUs using arguments that optimize for # Amazon EC2. python tf_cnn_benchmarks.py --local_parameter_device=gpu --num_gpus=8 \ --batch_size=64 --model=resnet50 --variable_update=replicated --use_nccl=False

分布式命令行参数

  • ps_hosts:以逗号分隔的主机列表用作参数服务器格式<host>:port,例如10.0.0.2:50000。

  • worker_hosts:逗号分隔的主机列表,以格式作为工作者使用<host>:port,例如10.0.0.2:50001。

  • task_index:列表中ps_hostsworker_hosts正在启动的主机的索引。

  • job_name:工作类型,例如psworker

分布式示例

以下是在两台主机上训练ResNet-50的示例:host_0(10.0.0.1)和host_1(10.0.0.2)。该示例使用合成数据。使用真实数据传递--data_dir参数。

# Run the following commands on host_0 (10.0.0.1): python tf_cnn_benchmarks.py --local_parameter_device=gpu --num_gpus=8 \ --batch_size=64 --model=resnet50 --variable_update=distributed_replicated \ --job_name=worker --ps_hosts=10.0.0.1:50000,10.0.0.2:50000 \ --worker_hosts=10.0.0.1:50001,10.0.0.2:50001 --task_index=0 python tf_cnn_benchmarks.py --local_parameter_device=gpu --num_gpus=8 \ --batch_size=64 --model=resnet50 --variable_update=distributed_replicated \ --job_name=ps --ps_hosts=10.0.0.1:50000,10.0.0.2:50000 \ --worker_hosts=10.0.0.1:50001,10.0.0.2:50001 --task_index=0 # Run the following commands on host_1 (10.0.0.2): python tf_cnn_benchmarks.py --local_parameter_device=gpu --num_gpus=8 \ --batch_size=64 --model=resnet50 --variable_update=distributed_replicated \ --job_name=worker --ps_hosts=10.0.0.1:50000,10.0.0.2:50000 \ --worker_hosts=10.0.0.1:50001,10.0.0.2:50001 --task_index=1 python tf_cnn_benchmarks.py --local_parameter_device=gpu --num_gpus=8 \ --batch_size=64 --model=resnet50 --variable_update=distributed_replicated \ --job_name=ps --ps_hosts=10.0.0.1:50000,10.0.0.2:50000 \ --worker_hosts=10.0.0.1:50001,10.0.0.2:50001 --task_index=1