65.9K
CodeProject 正在变化。 阅读更多。
Home

Caffe 针对 Intel® 架构进行了优化:应用现代代码技术

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2017年5月4日

CPOL

21分钟阅读

viewsIcon

6408

本文展示了 Caffe 的一个特殊版本——一个最初由伯克利视觉与学习中心 (BVLC) 开发的深度学习框架——该版本针对英特尔®架构进行了优化。

摘要

本文展示了 Caffe* 的一个特殊版本——一个最初由伯克利视觉与学习中心 (BVLC) 开发的深度学习框架——该版本针对英特尔®架构进行了优化。这个版本的 Caffe,被称为针对英特尔架构优化的 Caffe,目前已集成到最新发布的英特尔®数学核心函数库 2017 中,并针对英特尔®高级向量扩展 2 进行了优化,还将包含英特尔®高级向量扩展 512 指令。该解决方案得到英特尔®至强®处理器和英特尔®至强融核™处理器等支持。本文包含了 CIFAR-10* 图像分类数据集的性能结果,并描述了可用于改进 BVLC Caffe 代码和其他深度学习框架计算性能的工具和代码修改。

引言

深度学习是通用机器学习的一个子集,近年来在图像和视频识别、语音识别、自然语言处理 (NLP) 以及其他大数据和数据分析领域取得了突破性成果。计算、大型数据集和算法的最新进展是深度学习成功的关键因素,深度学习通过将数据传递通过一系列层来工作,每一层都提取复杂度递增的特征。

Each layer in a deep network is trained to identify features of higher complexity—this figure shows a small subset of the features of a deep network projected down to the pixels space and the corresponding images that activate those features

图 1. 深度网络中的每一层都经过训练,以识别更高复杂度的特征——此图显示了投射到像素空间(左侧灰色图像)的深度网络特征的一小部分,以及激活这些特征的相应图像(右侧彩色图像)。
Zeiler, Matthew D. 和 Fergus, Rob. 纽约大学,计算机科学系。“可视化和理解卷积网络。”2014 年。https://www.cs.nyu.edu/~fergus/papers/zeilerECCV2014.pdf

监督式深度学习需要带标签的数据集。三种流行的监督式深度网络类型是多层感知器 (MLP)、卷积神经网络 (CNN) 和循环神经网络 (RNN)。在这些网络中,输入通过一系列线性变换和非线性变换,随着其通过每一层,并产生一个输出。然后计算误差和相应的误差成本,之后计算网络中权重和激活的成本梯度,并迭代地反向传播到较低层。最后,根据计算出的梯度更新权重或模型。

在 MLP 中,每一层的输入数据(由向量表示)首先乘以该层特有的密集矩阵。在 RNN 中,密集矩阵(或多个矩阵)对于每一层都是相同的(层是循环的),并且网络的长度由输入信号的长度决定。CNN 类似于 MLP,但它们使用稀疏矩阵进行卷积层。这种矩阵乘法通过将权重的二维表示与层输入的二维表示进行卷积来表示。CNN 在图像识别中很流行,但也用于语音识别和 NLP。有关 CNN 的详细解释,请参阅“CS231n 卷积神经网络用于视觉识别”:http://cs231n.github.io/convolutional-networks/

Caffe

Caffe* 是一个由伯克利视觉与学习中心 (BVLC) 和社区贡献者开发的深度学习框架。本文将 Caffe 的原始版本称为“BVLC Caffe”。

相比之下,针对英特尔®架构优化的 Caffe 是 BVLC Caffe 框架的一个经过优化的特定分支。针对英特尔架构优化的 Caffe 目前已集成到最新发布的英特尔®数学核心函数库 (Intel® MKL) 2017 中,并针对英特尔®高级向量扩展 2 (Intel® AVX2) 进行了优化,还将包含英特尔®高级向量扩展 512 (Intel® AVX-512) 指令,这些指令得到英特尔®至强®处理器和英特尔®至强融核™处理器等支持。有关编译、训练、微调、测试和使用各种可用工具的详细描述,请阅读“使用针对英特尔®架构优化的 Caffe* 训练和部署深度学习网络”,网址为 https://software.intel.com/en-us/articles/training-and-deploying-deep-learning-networks-with-caffe-optimized-for-intel-architecture

英特尔要感谢 Boris Ginsburg 对针对英特尔®架构优化的 Caffe* 的 OpenMP* 多线程实现的想法和初步贡献。

本文描述了针对英特尔架构优化的 Caffe 在英特尔架构上运行的性能与 BVLC Caffe 相比的情况,并讨论了用于改进 Caffe 框架计算性能的工具和代码修改。本文还展示了使用 CIFAR-10* 图像分类数据集 (https://www.cs.toronto.edu/~kriz/cifar.html) 和由卷积、最大池化和平均池化以及批量归一化层组成的 CIFAR-10 全 sigmoid 模型 (https://github.com/BVLC/caffe/blob/master/examples/cifar10/cifar10_full_sigmoid_train_test_bn.prototxt) 的性能结果。

Example of CIFAR-10* dataset images

图 2. CIFAR-10* 数据集图像示例

要下载经过测试的 Caffe 框架的源代码,请访问以下链接

图像分类

CIFAR-10 数据集包含 60,000 张彩色图像,每张图像的尺寸为 32 × 32,平均分为以下 10 个类别并进行标记:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。这些类别是互斥的;不同类型的汽车(例如轿车或运动型多用途车 [SUV])或卡车(仅包括大型卡车)之间没有重叠——两组都不包括皮卡车(参见图 2)。

英特尔测试 Caffe 框架时,我们使用了 CIFAR-10 全 sigmoid 模型,这是一个包含卷积、最大池化、批量归一化、全连接和 Softmax 层在内的多层 CNN 模型。有关层描述,请参阅使用 OpenMP* 进行代码并行化一节。

初始性能分析

对针对英特尔架构优化的 Caffe 和 BVLC Caffe 进行基准测试的一种方法是使用 time 命令,该命令计算逐层的正向和反向传播时间。time 命令有助于衡量在每一层花费的时间,并提供不同模型的相对执行时间。

./build/tools/caffe time \
    --model=examples/cifar10/cifar10_full_sigmoid_train_test_bn.prototxt \
    -iterations 1000

在此上下文中,一次迭代定义为一次正向和反向通过一批图像。前面的命令返回每层 1,000 次迭代和整个网络的每次迭代的平均执行时间。图 3 显示了完整输出。

Output from the Caffe* time command

图 3. Caffe* time 命令的输出

在我们的测试中,我们使用了双插槽系统,每个插槽配备一个英特尔至强处理器 E5-2699 v3,主频 2.30 GHz,每个 CPU 有 18 个物理核心,并且英特尔®超线程技术 (Intel® HT Technology) 被禁用。这个双插槽系统总共有 36 个核心,因此对于我们的测试,除非另有说明,由 OMP_NUM_THREADS 环境变量指定的 OpenMP* 线程默认数量是 36(请注意,我们建议让 Caffe 针对英特尔架构优化自动指定 OpenMP 环境,而不是手动设置)。该系统还安装了 64 GB DDR4 内存,运行频率为 2,133 MHz。

利用这些数据,本文展示了英特尔工程师进行代码优化后的性能结果。我们使用了以下工具进行性能监控

  • Valgrind* 工具链中的 Callgrind*
  • 英特尔® VTune™ Amplifier XE 2017 测试版

英特尔 VTune Amplifier XE 工具提供以下信息

  • 总执行时间最长的函数(热点)
  • 系统调用(包括任务切换)
  • CPU 和缓存使用情况
  • OpenMP 多线程负载均衡
  • 线程锁
  • 内存使用情况

我们可以利用性能分析来寻找好的优化候选,例如代码热点和长时间的函数调用。图 4 显示了 Intel VTune Amplifier XE 2017 测试版对运行 100 次迭代的总结分析中的重要数据点。图 4 顶部是经过时间,为 37 秒。

这是代码在测试系统上执行所需的时间。CPU 时间(显示在经过时间下方)为 1,306 秒——这略低于 37 秒乘以 36 个核心(1,332 秒)。CPU 时间是所有参与执行的线程(或核心,因为在我们的测试中超线程被禁用)的总持续时间之和。

Intel® VTune™ Amplifier XE 2017 beta analysis summary for BVLC Caffe* CIFAR-10* execution

图 4. 英特尔® VTune™ Amplifier XE 2017 测试版分析;BVLC Caffe* CIFAR-10* 执行摘要

CPU 使用率直方图(图 4 底部)显示了在测试期间给定数量的线程同时运行的频率。大部分时间,只有一个线程(一个核心)在运行——在总共 37 秒中占了 14 秒。其余时间,我们有多线程运行效率非常低,不到 20 个线程参与执行。

执行摘要的“热点区域”部分(图 4 中间)表明了此处发生的情况。它列出了函数调用及其相应的 CPU 时间总和。kmp_fork_barrier 函数是 OpenMP 的内部函数,用于隐式屏障,用于同步线程执行。kmp_fork_barrier 函数占用了 1,130 秒的 CPU 时间,这意味着在 87% 的 CPU 执行时间内,线程都在这个屏障处空转,没有做任何有用的工作。

BVLC Caffe 包的源代码不包含 #pragma omp parallel 代码行。在 BVLC Caffe 代码中,没有明确使用 OpenMP 库进行多线程处理。然而,OpenMP 线程在 Intel MKL 内部用于并行化一些数学例程调用。为了确认这种并行化,我们可以查看自底向上选项卡视图(参见图 5,并查看按利用率(顶部)和单个线程时间线(底部)划分的具有有效时间的函数调用)。

图 5 显示了 BVLC Caffe 在 CIFAR-10 数据集上的函数调用热点。

Timeline visualization and function-call hotspots for BVLC Caffe* CIFAR-10* dataset training

图 5. BVLC Caffe* CIFAR-10* 数据集训练的时间线可视化和函数调用热点

gemm_omp_driver_v2 函数——**libmkl_intel_thread.so**的一部分——是英特尔 MKL 的通用矩阵-矩阵 (GEMM) 乘法实现。此函数在幕后使用 OpenMP 多线程。优化的英特尔 MKL 矩阵-矩阵乘法是用于正向和反向传播(即权重计算、预测和调整)的主要函数。英特尔 MKL 初始化 OpenMP 多线程,这通常会减少 GEMM 操作的计算时间。然而,在这种特定情况下——32 × 32 图像的卷积——工作负载不足以有效地利用单个 GEMM 操作中的所有 36 个 OpenMP 线程。因此,需要一种不同的多线程并行化方案,本文稍后将对此进行展示。

为了演示 OpenMP 线程利用的开销,我们运行代码时使用 OMP_NUM_THREADS=1 环境变量,然后比较相同工作负载的执行时间:31.1 秒而不是 37 秒(参见图 4图 6 顶部的“经过时间”部分)。通过使用此环境变量,我们强制 OpenMP 仅创建一个线程并将其用于代码执行。BVLC Caffe 代码实现中由此产生的近六秒的运行时差异表明了 OpenMP 线程初始化和同步的开销。

 OMP_NUM_THREADS=1

图 6. 英特尔® VTune™ Amplifier XE 2017 测试版分析摘要,针对 BVLC Caffe* CIFAR-10* 数据集在单线程下执行:OMP_NUM_THREADS=1

通过此分析设置,我们在 BVLC Caffe 实现中确定了三个主要的性能优化候选:im2col_cpucol2im_cpuPoolingLayer::Forward_cpu 函数调用(参见图 6 中间)。

代码优化

针对英特尔架构优化的 Caffe 在 CIFAR-10 数据集上的实现比 BVLC Caffe 代码快约 13.5 倍(正向-反向传播分别为 20 毫秒 [ms] 对 270 毫秒)。图 7 显示了我们对 1,000 次迭代平均的正向-反向传播结果。左侧一列显示了 BVLC Caffe 的结果,右侧一列显示了针对英特尔架构优化的 Caffe 的结果。

Forward-backward propagation results

图 7. 正向-反向传播结果

有关这些单个层的深入描述,请参阅下面的神经网络层优化结果一节。

有关定义层计算参数的更多信息,请访问http://caffe.berkeleyvision.org/tutorial/layers.html

以下各节描述了用于提高各种层性能的优化。我们的技术遵循了英特尔®现代代码开发人员代码的方法论指南,其中一些优化依赖于英特尔 MKL 2017 数学原语。本文在此介绍针对英特尔架构优化的 Caffe 中使用的优化和并行化技术,以帮助您更好地理解代码的实现方式,并使代码开发人员能够将这些技术应用于其他机器学习和深度学习应用和框架。

标量和串行优化

代码向量化

在分析 BVLC Caffe 代码并识别热点(消耗大部分 CPU 时间的函数调用)之后,我们应用了向量化优化。这些优化包括以下内容:

  • 基本线性代数子程序 (BLAS) 库(从自动调优线性代数系统 [ATLAS*] 切换到英特尔 MKL)
  • 汇编优化(Xbyak 即时 [JIT] 汇编器)
  • GNU 编译器集合* (GCC*) 和 OpenMP 代码向量化

BVLC Caffe 可以选择使用英特尔 MKL BLAS 函数调用或其他实现。例如,GEMM 函数针对向量化、多线程和更好的缓存流量进行了优化。为了更好的向量化,我们还使用了 Xbyak——一个用于 x86 (IA-32) 和 x64 (AMD64* 或 x86-64) 的 JIT 汇编器。Xbyak 目前支持以下向量指令集:MMX™ 技术、英特尔®流式 SIMD 扩展 (Intel® SSE)、英特尔 SSE2、英特尔 SSE3、英特尔 SSE4、浮点单元、英特尔 AVX、英特尔 AVX2 和英特尔 AVX-512。

Xbyak 汇编器是一个用于 C++ 的 x86/x64 JIT 汇编器,是一个专门为高效开发代码而创建的库。Xbyak 汇编器以纯头文件的形式提供。它还可以动态汇编 x86 和 x64 助记符。代码运行时生成 JIT 二进制代码可以实现多种优化,例如量化(一种将给定数组的元素除以第二个数组的元素的操作)和多项式计算(一种根据常量变量 x等创建操作的操作)。借助英特尔 AVX 和英特尔 AVX2 向量指令集的支持,Xbyak 可以在针对英特尔架构优化的 Caffe 的代码实现中获得更好的向量化比率。最新版本的 Xbyak 支持英特尔 AVX-512 向量指令集,这可以提高英特尔至强融核处理器 x200 产品家族的计算性能。这种改进的向量化比率使 Xbyak 能够通过单指令多数据 (SIMD) 指令同时处理更多数据,从而更有效地利用数据并行性。我们使用 Xbyak 对此操作进行向量化,这显著提高了池化层的处理性能。如果我们知道池化参数,我们可以生成汇编代码来处理特定池化窗口或池化算法的特定池化模型。结果是纯汇编代码,事实证明它比 C++ 代码更高效。

通用代码优化

其他串行优化包括:

  • 降低算法复杂度
  • 减少计算量
  • 展开循环

公共代码消除是我们代码优化过程中应用的一种标量优化技术。这样做是为了预先确定哪些可以在最内层 for-loop 之外进行计算。

例如,考虑以下代码片段

for (int h_col = 0; h_col < height_col; ++h_col) {
  for (int w_col = 0; w_col < width_col; ++w_col) {
    int h_im = h_col * stride_h - pad_h + h_offset;
    int w_im = w_col * stride_w - pad_w + w_offset;

在此代码片段的第三行,对于 h_im 计算,我们没有使用最内层循环的 w_col 索引。但是,此计算仍将为最内层循环的每次迭代执行。或者,我们可以将此行移到最内层循环之外,代码如下:

for (int h_col = 0; h_col < height_col; ++h_col) {
  int h_im = h_col * stride_h - pad_h + h_offset;
  for (int w_col = 0; w_col < width_col; ++w_col) {
    int w_im = w_col * stride_w - pad_w + w_offset;

CPU 特有、系统特有和其他通用代码优化技术

以下额外的通用优化已应用:

  • 改进的 im2col_cpu/col2im_cpu 实现
  • 批量归一化的复杂性降低
  • CPU/系统特有优化
  • 每个计算线程使用一个核心
  • 避免线程移动

英特尔 VTune Amplifier XE 2017 测试版将 im2col_cpu 函数识别为热点函数之一——使其成为性能优化的理想候选。im2col_cpu 函数是执行直接卷积作为 GEMM 操作以使用高度优化的 BLAS 库的常见步骤。每个局部补丁都扩展为一个单独的向量,并且整个图像被转换为一个更大的(更耗内存的)矩阵,其行对应于将应用滤波器的多个位置。

im2col_cpu 函数的一种优化技术是索引计算简化。BVLC Caffe 代码有三个嵌套循环用于遍历图像像素

for (int c_col = 0; c_col < channels_col; ++c_col)
  for (int h_col = 0; h_col < height_col; ++h_col)
    for (int w_col = 0; w_col < width_col; ++w_col)
      data_col[(c_col*height_col+h_col)*width_col+w_col] = // ...

在此代码片段中,BVLC Caffe 最初计算的是 data_col 数组元素的相应索引,尽管此数组的索引只是顺序处理。因此,四个算术操作(两次加法和两次乘法)可以替换为单个索引递增操作。此外,由于以下原因,条件检查的复杂性可以降低:

/* Function uses casting from int to unsigned to compare if value
of parameter a is greater or equal to zero and lower than value of
parameter b. The b parameter has signed type and always positive,
therefore its value is always lower than 0x800... where casting
negative parameter value converts it to value higher than 0x800...
The casting allows to use one condition instead of two. */
inline bool is_a_ge_zero_and_a_lt_b(int a, int b) {
  return static_cast<unsigned>(a) < static_cast<unsigned>(b);
}

在 BVLC Caffe 中,原始代码的条件检查是 if (x >= 0 && x < N),其中 xN 都是有符号整数,并且 N 始终为正数。通过将这些整数的类型转换为无符号整数,可以改变比较的区间。在类型转换之后,只需进行一次比较,而不是运行两次带逻辑 AND 的比较:

if (((unsigned) x) < ((unsigned) N))

为了避免操作系统移动线程,我们使用了 OpenMP 亲和性环境变量 KMP_AFFINITY=compact,granularity=fine。相邻线程的紧凑放置可以提高 GEMM 操作的性能,因为所有共享相同最后一级缓存 (LLC) 的线程都可以重用先前预取的带数据的缓存行。

有关缓存阻塞优化实现以及数据布局和向量化的信息,请参阅以下出版物:http://arxiv.org/pdf/1602.06709v1.pdf

使用 OpenMP* 进行代码并行化

神经网络层优化结果

通过对其应用 OpenMP 多线程并行化,以下神经网络层得到了优化:

  • 卷积
  • 反卷积
  • 局部响应归一化 (LRN)
  • ReLU
  • Softmax
  • 串联
  • OpenBLAS* 优化工具——例如 vPowx - y[i] = x[i]β 操作、caffe_setcaffe_copycaffe_rng_bernoulli
  • 池化
  • Dropout
  • 批量归一化
  • Data
  • 逐元素操作

卷积层

顾名思义,卷积层将输入与一组学习到的权重或滤波器进行卷积,每个滤波器在输出图像中生成一个特征图。这种优化可以防止单个输入特征图集对硬件的利用不足。

template <typename Dtype>
void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& \
      bottom, const vector<Blob<Dtype>*>& top) {
  const Dtype* weight = this->blobs_[0]->cpu_data();
  // If we have more threads available than batches to be prcessed then
  // we are wasting resources (lower batches than 36 on XeonE5)
  // So we instruct MKL
  for (int i = 0; i < bottom.size(); ++i) {
    const Dtype* bottom_data = bottom[i]->cpu_data();
    Dtype* top_data = top[i]->mutable_cpu_data();
#ifdef _OPENMP
    #pragma omp parallel for num_threads(this->num_of_threads_)
#endif
      for (int n = 0; n < this->num_; ++n) {
        this->forward_cpu_gemm(bottom_data + n*this->bottom_dim_,
                               weight,
                               top_data + n*this->top_dim_);
        if (this->bias_term_) {
          const Dtype* bias = this->blobs_[1]->cpu_data();
          this->forward_cpu_bias(top_data + n * this->top_dim_, bias);
        }
      }
  }
}

我们处理 k = min(num_threads, batch_size)input_feature 映射;例如,kim2col 操作并行发生,以及 k 次调用 Intel MKL。Intel MKL 自动切换到单线程执行流,并且总体性能优于 Intel MKL 处理单个批次时的性能。此行为在源代码文件 src/caffe/layers/base_conv_layer.cpp 中定义。实现优化了来自 src/caffe/layers/conv_layer.cpp 的 OpenMP 多线程——该文件位置包含相应的代码。

池化或子采样

最大池化平均池化随机池化(尚未实现)是不同的下采样方法,其中最大池化是最流行的方法。池化层将前一层的结果划分为一组通常不重叠的矩形瓦片。对于每个这样的子区域,该层然后输出最大值、算术平均值,或者(将来)从由每个瓦片的激活形成的多元分布中采样的随机值。

池化在 CNN 中有三个主要用途:

  • 池化减少了问题的维度和上层的计算负载。
  • 池化较低的层允许较高层中的卷积核覆盖输入数据的更大区域,从而学习更复杂的特征;例如,较低层核通常学习识别小的边缘,而较高层核可能学习识别森林或海滩等场景。
  • 最大池化提供了一种平移不变性。在 2 × 2 瓦片(典型的池化瓦片)可以被单个像素平移的八个可能方向中,有三个将返回相同的最大值;对于 3 × 3 窗口,有五个将返回相同的最大值。

池化作用于单个特征图,因此我们使用 Xbyak 来制作一个高效的汇编过程,可以为单个或多个输入特征图创建平均到最大池化。当您并行运行 OpenMP 过程时,可以为一批输入特征图实现此池化过程。

池化层使用 OpenMP 多线程并行化;由于图像是独立的,因此它们可以由不同的线程并行处理。

#ifdef _OPENMP
  #pragma omp parallel for collapse(2)
#endif
  for (int image = 0; image < num_batches; ++image)
    for (int channel = 0; channel < num_channels; ++channel)
      generator_func(bottom_data, top_data, top_count, image, image+1,
                        mask, channel, channel+1, this, use_top_mask);
}

通过 collapse(2) 子句,OpenMP #pragma omp parallel 扩展到两个嵌套的 for-loops,迭代批量中的图像和图像通道,将循环组合成一个,并并行化循环。

Softmax 和损失层

损失函数是机器学习中的关键组成部分,它通过将预测输出与目标或标签进行比较,然后通过计算梯度——权重对损失函数的偏导数——来重新调整权重以最小化成本,从而指导网络训练过程。

Softmax(归一化指数)函数是分类概率分布的梯度对数归一化器。通常,它用于计算一个随机事件的可能结果,该事件可以有 K 种可能结果之一,每种结果的概率分别指定。具体来说,在多项式逻辑回归(一个多类别分类问题)中,此函数的输入是 K 个不同线性函数的结果,并且样本向量 x 的第 j 个类别的预测概率是

OpenMP 多线程应用于这些计算时,是一种通过主线程分叉指定数量的从属线程以将任务分配给它们进行并行化的方法。然后,线程在分配给不同处理器时并发运行。例如,在以下代码中,通过计算出的不同通道中的范数进行除法,实现了具有独立数据访问的并行化单个算术运算。

    // division
#ifdef _OPENMP
#pragma omp parallel for
#endif
    for (int j = 0; j < channels; j++) {
      caffe_div(inner_num_, top_data + j*inner_num_, scale_data,
              top_data + j*inner_num_);
    }

修正线性单元 (ReLU) 和 Sigmoid — 激活/神经元层

ReLUs 是目前深度学习算法中最流行的非线性函数。激活/神经元层是逐元素运算符,它接受一个底部 blob 并生成一个相同大小的顶部 blob。(blob 是框架的标准数组和统一内存接口。随着数据和导数流经网络,Caffe 将信息作为 blob 进行存储、通信和操作。)

ReLU 层接受输入值 x 并计算输出为正值时的 x,并将其按负值时的 negative_slope 进行缩放。

negative_slope 的默认参数值为零,这等同于取 max(x, 0) 的标准 ReLU 函数。由于激活过程与数据无关,每个 blob 都可以并行处理,如下页所示

template <typename Dtype>
void ReLULayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
    const vector<Blob<Dtype>*>& top) {
  const Dtype* bottom_data = bottom[0]->cpu_data();
  Dtype* top_data = top[0]->mutable_cpu_data();
  const int count = bottom[0]->count();
  Dtype negative_slope=this->layer_param_.relu_param().negative_slope();
#ifdef _OPENMP
#pragma omp parallel for
#endif
  for (int i = 0; i < count; ++i) {
    top_data[i] = std::max(bottom_data[i], Dtype(0))
        + negative_slope * std::min(bottom_data[i], Dtype(0));
  }
}

类似并行计算可用于反向传播,如下所示

template <typename Dtype>
void ReLULayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
    const vector<bool>& propagate_down,
    const vector<Blob<Dtype>*>& bottom) {
  if (propagate_down[0]) {
    const Dtype* bottom_data = bottom[0]->cpu_data();
    const Dtype* top_diff = top[0]->cpu_diff();
    Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
    const int count = bottom[0]->count();
    Dtype negative_slope=this->layer_param_.relu_param().negative_slope();
#ifdef _OPENMP
#pragma omp parallel for
#endif
    for (int i = 0; i < count; ++i) {
      bottom_diff[i] = top_diff[i] * ((bottom_data[i] > 0)
          + negative_slope * (bottom_data[i] <= 0));
    }
  }
}

同样,sigmoid 函数 S(x) = 1 / (1 + exp(-x)) 可以通过以下方式并行化

#ifdef _OPENMP
  #pragma omp parallel for
#endif
  for (int i = 0; i < count; ++i) {
    top_data[i] = sigmoid(bottom_data[i]);
  }

由于英特尔 MKL 没有提供实现 ReLU 的数学原语来添加此功能,我们尝试使用汇编代码(通过 Xbyak)实现 ReLU 层的性能优化版本。然而,我们发现对于英特尔至强处理器没有明显的性能提升——这可能是由于内存带宽有限。现有 C++ 代码的并行化足以提高整体性能。

结论

前一节讨论了神经网络的各种组件和层,以及这些层中处理过的数据 blob 如何在可用的 OpenMP 线程和 Intel MKL 线程之间分布。图 8 中的 CPU 使用率直方图显示了在应用我们的优化和并行化之后,给定数量的线程同时运行的频率。

通过针对英特尔架构优化的 Caffe,同时运行的线程数量显著增加。在我们的测试系统上,执行时间从原始未修改运行的 37 秒降至针对英特尔架构优化的 Caffe 的仅 3.6 秒——整体执行性能提高了 10 倍以上。

Intel® VTune™ Amplifier XE 2017 beta analysis summary of the Caffe* optimized for Intel® architecture implementation for CIFAR-10* training

图 8. 英特尔® VTune™ Amplifier XE 2017 测试版对针对英特尔®架构优化的 Caffe* 在 CIFAR-10* 训练中的实现分析摘要

如“经过时间”部分(图 8 顶部)所示,在本次运行执行期间仍然存在一些空转时间。因此,执行性能并未随线程数量的增加而线性扩展(根据阿姆达尔定律)。此外,代码中仍存在未通过 OpenMP 多线程并行化的串行执行区域。OpenMP 并行区域的重新初始化已针对最新的 OpenMP 库实现进行了显著优化,但仍会引入不可忽略的性能开销。将 OpenMP 并行区域移到代码的主函数中可能会进一步提高性能,但这需要大量的代码重构。

图 9 总结了我们针对英特尔架构优化的 Caffe 所遵循的优化技术和代码重写原则。

Step-by-step approach of Intel® Modern Code Developer Code

图 9. 英特尔®现代代码开发人员代码的逐步方法

在我们的测试中,我们使用 Intel VTune Amplifier XE 2017 测试版来查找热点——代码优化和并行化的良好候选。我们实现了标量和串行优化,包括公共代码消除以及循环索引和条件计算的算术运算的简化/简化。接下来,我们按照“GCC 中的自动向量化”中描述的通用原则优化了代码的向量化(https://gcc.gnu.org/projects/tree-ssa/vectorization.html)。JIT 汇编器 Xbyak 允许我们更有效地使用 SIMD 操作。

我们在神经网络层内部使用 OpenMP 库实现了多线程,其中图像或通道上的数据操作是数据无关的。实现英特尔现代代码开发人员代码方法的最后一步涉及将单节点应用程序扩展到多核架构和多节点集群环境。这是我们目前研究和实现的主要重点。我们还应用了内存(缓存)重用优化,以获得更好的计算性能。有关更多信息,请参阅:http://arxiv.org/pdf/1602.06709v1.pdf。我们对英特尔至强融核处理器 x200 产品家族的优化包括使用高带宽 MCDRAM 内存和利用四象限 NUMA 模式。

针对英特尔架构优化的 Caffe 不仅提高了计算性能,而且使您能够从数据中提取越来越复杂的特征。本文中包含的优化、工具和修改将帮助您从针对英特尔架构优化的 Caffe 中获得顶级的计算性能。

有关英特尔现代代码开发人员代码计划的更多信息,请参阅以下出版物:

有关机器学习的更多信息,请参阅

© . All rights reserved.