Quantcast
Channel: 英特尔开发人员专区文章
Viewing all 49 articles
Browse latest View live

整理您的数据和代码: 数据和布局 - 第 2 部分

$
0
0

这两篇关于性能和内存的文章介绍了一些基本概念,用于指导开发人员更好地改善软件性能。为实现此目标,文章内容重点阐述了内存和数据布局方面的注意事项。第 1 部分介绍了寄存器使用以及覆盖或阻塞算法,以改善数据重用情况。文章从考虑数据布局以提供通用并行处理能力(与线程共享内存编程)开始,然后还考虑了基于 MPI 的分布式计算。本文扩展了在实现并行处理能力时需考虑的概念,包括矢量化(单指令多数据 SIMD)、共享内存并行处理(线程化)和分布式内存计算等。最后,文章考虑了数据布局结构阵列 (AOS) 以及阵列结构 (SOA) 数据布局。

第 1 部分强调的基本性能原则是:在寄存器或缓存中重新利用数据然后再将其去除。在本文中强调的性能原则为:在最常用数据的地方放置数据,以连续访问模式放置数据,并避免数据冲突。

 

与线程共享内存编程

让我们从考虑与线程共享内存编程开始。全部线程都共享进程中的相同内存。有许多常用的线程模型。最为闻名的是 Posix* 线程和 Windows* 线程。正确创建和管理线程中涉及的工作容易出错。涉及大量模块和大型开发团队的现代软件让线程的并行编程极易出错。在此过程中开发团队需要开发数个程序包,以简化线程创建、管理和充分利用并行线程。最常用的两个模型为 OpenMP* 和英特尔® 线程构建模块。第三个线程模型英特尔® Cilk™ Plus 还未达到与 OpenMP 和线程构建模块相似的采用级别。所有这些线程模型形成线程池,该线程池被重新用于每个并行操作或并行区域。OpenMP 的优势在于可通过使用指令,逐步提高并行处理能力。OpenMP 指令通常可添加至现有软件,并仅需对每一步骤的过程进行最少的代码变更。它允许使用线程运行时库来管理大量线程维护工作,可显著简化线程软件的开发。同时它还可以为所有代码开发人员提供一个可沿用的一致线程模型,减少一些常见线程错误的可能性,并提供由专注于线程优化的开发人员制作的最优线程化运行时库。

简介段落中提及的基本并行原则为将数据置于使用该数据之处,并避免移动数据。在线程化编程中,默认模型是在进程中全局共享数据,并可由所有线程访问数据。有关线程的简介文章强调了通过将 OpenMP 应用至循环 (Fortran*) 或用于循环 (C) 来开始线程的便利性。当在两到四核上运行时,这些方法通常能够带来速度提升。这些方法会经常地扩展至 64 条线程或更多。但很多时候也不会进行此类扩展。在不进行扩展的一些情况中,主要是因为它们尊虚了良好的数据分解计划。这要求为良好的并行代码设计一个架构。

在代码调用堆栈的较高级别利用并行处理能力非常重要,而不是局限于由开发人员或软件工具确定的并行机会。当开发人员认识到可并行操作任务或数据时,依据埃坶德尔定律考虑这些问题: “在进行这点之前,我是否可以开始更高级别的并行运算? 如果我这样做,增大我代码的并行区域是否会带来更好的可扩展性?”

仔细考虑数据的放置以及必须通过消息共享什么数据。数据被置于最常使用的地方,然后根据需要发送至其他系统。对于以网格表示的应用程序,或具有特定分区的物理域,MPI 软件中常见的做法是围绕子网格或子域添加一行“虚拟”单元。虚拟单元用于存储 MPI 进程发送的数据的值,该进程会更新这些单元。通常虚拟单元不会用在线程化软件中,但是正如您沿着用于消息通过的分区最大程度减少边缘的长度一样,需要使用共享内存为线程最大程度减少分区的边缘。这样可最大程度减少对于线程锁(或关键部分)或关系到缓存所有权的缓存使用代价的需求。

大型多路系统共享全局内存地址空间,但通常具有非均匀的内存访问 (NUMA) 时间。和位于最靠近运行代码的插槽的内存条中的数据相比,最靠近另一插槽的内存条中的数据进行检索所需的时间更长,或者延迟更久。对于靠近的内存的访问延迟更短。

. Latency memory access, showing relative time to access data

图 1. 延迟内存访问,显示访问数据的相对时间。

如果一个线程分配并初始化数据,则通常会将该数据置于最靠近线程分配和初始化正在其上运行的插槽的内存条(图 1)。您可通过每个线程分配以及先引用其将主要使用的内存来改善性能。这通常足以确保内存最靠近线程在其上运行的插槽。一旦创建了线程,并且线程处于活动状态,操作系统通常会将线程留在相同插槽上。有时明确将线程绑定至核心以防止线程迁移较为有利。当数据具有特定模式时,实用的做法是将线程的亲缘性分配、绑定或设置到特定核心以匹配该模式。英特尔 OpenMP 运行时库(英特尔® Parallel Studio XE 2016 的一部分)提供了明确的映射属性,这些属性经过证明可用于英特尔® 至强融核™ 协处理器。

这些类型包括紧凑、分散和平衡。 

  • 紧凑属性将连续或相邻的线程分配至单核上的系统性多线程 (SMT),以将线程分配至其他核心。这在线程和连续编号(相邻)的线程共享数据的地方非常重要。
  • 分散亲缘性功能将线程分配至每个核心,然后再回到初始核心以在 SMT 上安排更多线程。
  • 平衡亲缘性功能以平衡的方式将连续或相邻 ID 的线程分配至相同的核心。如果期望根据英特尔 16.0 C++ 编译器文档优化线程亲缘性,在开始亲缘性时建议进行平衡。平衡的亲缘性设置仅可用于英特尔® 至强融核™ 产品系列。它并非一般 CPU 的有效选项。当利用了至强融核平台上的所有 SMT 时,平衡以及紧凑属性的效果相同。如果在至强融核平台上只利用了某些 SMT,紧凑方法将填补第一批内核上的所有 SMT 并在最后适当保留某些内核。

花些一些时间将线程数据靠近使用它的地方放置很重要。就和数据布局对于 MPI 程序很重要一样,这可能也对线程化软件很重要。  

在内存和数据布局方面,需要考虑两个较短的项目。这些是相对易于解决的部分,但是可能有很大影响。第一个是错误共享,第二个是数据对齐。和线程化软件相关的性能问题之一是错误共享。所运算的每个线程数据均为独立状态。它们之间没有共享,但是会共享包含两个数据点的高速缓存行。正因如此,将其称为错误共享或错误数据共享;虽然它们没有共享数据,但是性能行为表现和已经共享一样。

我们假设每个线程递增自身计数器,但是计数器处于一维阵列中。每个线程递增其自身的计数器。要递增其计数器,内核必须拥有高速缓存行。例如,插槽 0 上的线程 A 获得高速缓存行的所有权并递增 iCount[A] 。同时插槽 1 上的线程 A+1 递增 iCount[A+1],要实现这些操作,插槽 1 上的内核获得高速缓存行的所有权,并且线程 A+1 更新其值。由于高速缓存行中的值改变,使得插槽 0 上处理器的高速缓存行无效。在下次迭代时,插槽 0 中的处理器获得来自插槽 0 的高速缓存行的所有权,并修改 iCount[A] 中的值,该值继而让插槽 1 中的高速缓存行无效。当插槽 1 上的线程准备好写入时,循环重复。受到高速缓存行无效、重获控制以及同步至有性能影响的内存的影响,需要使用大量循环来保持缓存一致性。

对此的最佳解决方案并非让缓存无效。例如,在循环的入口,每个线程可读取其计数并将其存储在其堆栈上的本地变量中(读取不会让缓存无效)。当工作完成时,线程可将该本地值复制回最初的位置(参见图 2)。另一个备选方案是填补数据,使数据主要由其自身高速缓存行中的特定线程使用。

int iCount[nThreads] ;
      .
      .
      .
      for (some interval){
       //some work . . .
       iCount[myThreadId]++ // may result in false sharing
     }

Not invalidating the cache

int iCount[nThreads*16] ;// memory padding to avoid false sharing
      .
      .
      .
      for (some interval){
       //some work . . .
       iCount[myThreadId*16]++ //no false sharing, unused memory
     }

No false sharing, unused memory

int iCount[nThreads] ; // make temporary local copy

      .
      .
      .
      // every thread creates its own local variable local_count
      int local_Count = iCount[myThreadID] ;
      for (some interval){
       //some work . . .
       local_Count++ ; //no false sharing
     }
     iCount[myThreadId] = local_Count ; //preserve values
     // potential false sharing at the end,
     // but outside of inner work loop much improved
     // better just preserve local_Count for each thread

图 2.

相同的错误共享也可能在分配给相邻内存位置的标量上发生。这是如下面代码段所示的最后一种情况:

int data1, data2 ; // data1 and data2 may be placed in memory
                   //such that false sharing could occur
declspec(align(64)) int data3;  // data3 and data4 will be
declspec(align(64)) int data4;  // on separate cache lines,
                                // no false sharing

如果开发人员从一开始就设计并行处理,并最小化共享数据使用,通常要避免错误共享。 如果您的线程化软件没有很好地扩展,尽管有大量独立的工作在持续进行并且有少量障碍(互斥器、关键部分),检查错误共享也非常重要。

 

数据对齐

当以 SIMD 方式(AVX512、AVX、SSE4 等)运算的数据在高速缓存行边界上对齐时 , 软件性能最佳。数据访问未对齐的代价根据处理器系列而有所不同。英特尔® 至强融核™ 协处理器对于数据对齐尤其敏感。  在英特尔至强融核平台上,数据对齐至关重要。该差异在其他英特尔® 至强® 平台上不是很明显,但是当数据和高速缓存行边界对齐时,性能也能够得到显著的改进。因此建议软件开发人员务必在 64 字节边界上对齐数据。在 Linux* 和 Mac OS X* 上,这可通过英特尔编译器选项完成,没有源代码更改,只需使用以下命令行选项: /align:rec64byte。    

对于 C 语言中的动态分配的内存,malloc()可由 _mm_alloc(datasize,64)取代。当使用了 _mm_alloc()时,应当使用 _mm_free()取代 free()。专门针对数据对齐的完整文章位于以下网址:https://software.intel.com/zh-cn/articles/data-alignment-to-assist-vectorization。 

另外也请查看编译器文档。为了展现数据对齐的影响,我们创建了两个相同大小的矩阵,并且两个矩阵都运行该系列第 1 部分中使用的阻塞矩阵多层代码。 对于第一种情况,对齐了矩阵 A,对于第二种情况,特意将矩阵 A 偏移 24 个字节(3 的倍数),在将英特尔 16.0 编译器用于大小从 1200x1200 到 4000x4000 的矩阵的情况下,性能减少 56% 至 63%。在该系列的第 1 部分,我显示了一个表格,其中有使用了不同编译器的循环排序的性能,当一个矩阵偏移时使用英特尔编译器不再有任何性能优势。建议开发人员就数据对齐和可用的选项查看其编译器文档,从而当数据对齐时,编译器能够最有效地利用该信息。用于为偏离高速缓存行的的矩阵评估性能的代码嵌入在第 1 部分的代码中 - 该试验的代码位于:https://github.com/drmackay/samplematrixcode

编译器文档也有详细信息。

为了展现数据对齐的效果,我们创建了两个相同大小的矩阵,并且两个矩阵都运行第 1 部分中使用的阻塞矩阵多层代码。我们对齐了第一矩阵 A,第二个矩阵特意被偏移 24 个字节(3 的倍数),在将英特尔 16.0 编译器用于大小从 1200x1200 到 4000x4000 的矩阵的情况下,性能减少 56% 至 63%。

 

结构阵列对比阵列结构

处理器在内存连续流入时性能更佳。这在高速缓存行的每个元素移入 SIMD 寄存器时很有效, 前提是连续高速缓存行也以有序的方式载入了处理器预取。在结构阵列中,数据可采用类似以下形式的布局:

struct {
   uint r, g, b, w ; // a possible 2D color rgb pixel layout
} MyAoS[N] ;

在该布局中,连续陈列出 rgb 值。如果软件跨彩色平面处理数据,则整个结构可能被拉入缓存,但是每次仅使用一个值,例如 g。如果数据存储在阵列结构中,布局可能类似以下形式:

struct {
   uint r[N] ;
   uint g[N] ;
   uint b[N] ;
   uint w[N] ;
} MySoA ;

如果数据以阵列结构组织,并且软件运算所有 g 值(也可以是 r 或 b),当将高速缓存行引入高速缓存时,可能会在运算中使用整个高速缓存行。数据被更有效地载入 SIMD 寄存器,效率和性能显著改善。在许多情况下,软件开发人员用时间在实际操作中临时将数据移入要运算的阵列结构,然后根据需要将其复制回原位。在可行时,最好避免该额外的复制操作,因为这会占用执行时间。

英特尔 (Vectorization) Advisor 2016“内存访问模式”(MAP) 分析确定了具有连续(“单位步幅”)、非连续并且“非常规”的访问模式的循环:

“步幅分配”列提供关于每个模式在给定源循环中发生的频率的汇总统计数据。在上图中,条形图左边的三分之二为蓝色,表示连续访问模式,而右边的三分之一为红色,其表示非连续内存访问。对于具有纯 AoS 模式的代码,Advisor 也可自动获取特定“建议”以执行 AoS -> SoA 转换。 

在 Advisor MAP 中访问模式以及更为一般的内存位置分析得到简化,具体方法为额外提供内存“占用空间”指标,并将每个“步幅”(即访问模式)诊断映射至特定 C++ 或 Fortran* 对象/阵列名称。有关英特尔 Advisor 的详细信息,请访问

https://software.intel.com/zh-cn/get-started-with-advisor https://software.intel.com/zh-cn/intel-advisor-xe

阵列结构和结构阵列数据布局关系到许多图形程序以及 nbody(例如分子动态),或任何时间数据/属性(例如质量、位置、速度、电量),并且可与点或特定主体关联。通常,阵列结构更加有效,并且具有更高性能。

从英特尔编译器 2016 Update 1 开始,通过引入英特尔® SIMD 数据布局模板(英特尔® SDLT),AoS -> SoA 转换变得更简单。借助 SDLT,可以采用下面的方式方便地重新定义 AoS 容器:

SDLT_PRIMITIVE(Point3s, x, y, z)
sdlt::soa1d_container<Point3s> inputDataSet(count);  

从而可通过 SoA 方式访问 Point3s 实例。在此处阅读有关 SDLT 的更多信息。

几篇专论文章专门阐述了 AoS 对比 SoA 的主题。读者可通过下面的链接查看:

https://software.intel.com/zh-cn/articles/a-case-study-comparing-aos-arrays-of-structures-and-soa-structures-of-arrays-data-layouts

https://software.intel.com/zh-cn/articles/how-to-manipulate-data-structure-to-optimize-memory-use-on-32-bit-intel-architecture
http://stackoverflow.com/questions/17924705/structure-of-arrays-vs-array-of-structures-in-cuda

尽管大多数情况下,阵列结构匹配该模式并提供最佳性能,但也存在少数情况,其中数据参考和使用更紧密地匹配结构阵列布局,并且在该情况下结构阵列可提供更好的性能。

 

总结

下面总结了数据布局和性能方面要遵守的基本原则。将代码结构化以最小化数据移动。在数据处于寄存器或高速缓存中时重新使用它;这也有助于最小化数据移动。循环分块有助于最小化数据移动。对于具有 2D 或 3D 布局的软件尤其如此。考虑并行化布局,包括如何为并行计算分配任务和数据。良好的域分解做法有利于消息传送 (MPI) 和共享内存编程。阵列结构移动的数据通常比结构阵列更少,并且效果更佳。避免错误共享,并创建真正的本地变量或提供填充,从而使每个线程在不同的高速缓存行中引用值。最后,将数据对齐设置为在高速缓存行上开始。 

完整的代码可在以下网址下载: https://github.com/drmackay/samplematrixcode

如果您错过了第 1 部分,可在此处找到它。

您可应用这些技巧并了解代码性能如何得到改善。


面向英特尔® 架构优化的 Caffe*:使用现代代码技巧

$
0
0

提升深度学习框架的计算性能

PDF 版本

作者

英特尔公司 Vadim Karpusenko 博士,英特尔公司
Andres Rodriguez 博士,英特尔公司
Jacek Czaja,英特尔公司
Mariusz Moczala

摘要

本文将介绍一种特殊版本的深度学习框架 Caffe*(最初由伯克利愿景和学习中心 (Berkeley Vision and Learning Center,BVLC) 开发),该框架专门面向英特尔® 架构优化。 这一版本的 Caffe(即面向英特尔® 架构而优化的 Caffe)目前集成了最新版本的英特尔® 数学核心函数库 2017,专门面向英特尔® 高级矢量扩展指令集 2 优化,还将纳入英特尔高级矢量扩展指令集 512 指令。 该解决方案支持英特尔® 至强® 处理器和英特尔® 至强融核™ 处理器等。 本文列举了有关 CIFAR-10* 图像分类数据集的性能结果,并介绍了部分可提升 BVLC Caffe 代码和其他深度学习框架的计算性能的工具和代码修改。

简介

深度学习属于通用机器学习的一个子集。近年来,深度学习在图像与视频识别、语音识别、自然语言处理 (NLP) 和其他大数据与数据分析领域取得了突破性进展。 近期在计算、大型数据集和算法等方面所取得的进展已经成为深度学习获取成功的关键因素,其工作原理是将数据传递各个层,各层均可从中提取日益复杂的特征。

图 1. 深度网络的每一层通过培训,能够识别更为复杂的特征 — 上图显示了投影成像素空间(左侧的灰色图像)的深度网络的一小部分特性以及激活这些特性的相应图像(左侧的彩色图像)。
Zeiler, Matthew D. 和 Fergus, Rob. 纽约大学计算机科学系 《卷积网络的可视化与理解》, 2014 年。 https://www.cs.nyu.edu/~fergus/papers/zeilerECCV2014.pdf

监督式深度学习需要带有标记的数据集。 常见的 3 种监督式深度网络包括多层感知器 (MLP)、卷积神经网络 (CNN) 和循环神经网络 (RNN)。 在这些网络中,输入在通过每一层时,经过一系列线性和非线性转换,然后生成输出结果。 首先计算网络中权值与激活的成本梯度,并以迭代方式向后传递至下一层,然后计算错误以及相关错误成本。 最后根据计算的梯度更新权值或模型。

在 MLP 中,每层的输入数据(表示为矢量)首先乘以该层独有的密集矩阵。 在 RNN 中,每层(循环)的密集矩阵都是相同的,网络长度取决于输入信号的长度。 CNN 与 MLP 类似,但使用卷积层的稀疏矩阵。 该矩阵乘法变现为 2-D 权值表示卷积该层输入的 2-D 表示。 CNN 常用于图像识别,但也可用于语音识别和 NLP。 有关 CNN 的详细介绍,请参阅“面向视觉识别技术的 CS231n 卷积神经网络”: http://cs231n.github.io/convolutional-networks/

Caffe

Caffe 是伯克利愿景和学习中心 (Berkeley Vision and Learning Center, BVLC) 与社区撰稿者共同开发的深度学习框架。 本文将初始版 Caffe 称作“BVLC Caffe”。

相比之下,面向英特尔® 架构优化的 Caffe 为特定的优化型 BVLC Caffe 框架。 面向英特尔架构优化的 Caffe 目前集成了最新版英特尔® 数学核心函数库(英特尔® MKL) 2017,专门面向英特尔® 高级矢量扩展指令集 2(英特尔® AVX2)优化,还将纳入英特尔高级矢量扩展指令集 512(英特尔® AVX-512)指令,其支持英特尔® 至强® 处理器和英特尔® 至强融核™ 处理器等。 如欲了解关于编译、培训、调优、测试,以及可用工具的详细信息,请阅读“基于面向英特尔® 架构优化的 Caffe* 培训和部署深度学习网络”:https://software.intel.com/zh-cn/articles/training-and-deploying-deep-learning-networks-with-caffe-optimized-for-intel-architecture

英特尔非常感谢 Boris Ginsburg 在 OpenMP* 多线程化实施面向英特尔® 架构优化的 Caffe* 方面所提出的创意和做出的前期工作。

本文介绍了面向英特尔架构优化的 Caffe* 与基于英特尔架构运行的 BVLC Caffe 在性能方面的差异,并探讨了可用于提升 Caffe 框架的计算性能的工具和代码修改。 还展示了使用 CIFAR-10* 图像分类数据集(https://www.cs.toronto.edu/~kriz/cifar.html) 和 CIFAR-10 full-sigmoid 模型(由卷积层、最大池化与平均池化,以及批归一化 (batch normalization) 构成)的性能结果: (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 full-sigmoid 模型 — 包含卷积、最大池化、批归一化、全连接和 softmax 在内的多层 CNN 模型。 关于层级描述请参阅借助 OpenMP* 实现代码并行化部分。

初始性能分析

评测面向英特尔架构优化的 Caffe 和 BVLC Caffe 性能的方法是使用时间命令计算各层的向前和向后传播时间。 该时间命令可用于测量各层所消耗的时间,并为不同模型提供相关执行时间:

./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* 时间命令的输出

在测试过程中,我们使用搭载了一颗英特尔至强处理器 E5-2699 v3 的双路系统(每路 2.30 GHz,每 CPU 10 个内核),并禁用英特尔® 超线程技术(英特尔® HT 技术)。 该双路系统共有 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显示了英特尔 VTune Amplifier XE 2017 测试版汇总分析运行 100 次迭代时的重要数据点。 图 4顶部的运行时间 (Elapsed Time) 为 37 秒。

该时间为测试系统中代码的执行时间。 运行时间下方的 CPU 时间 (CPU Time) 为 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* 执行汇总

图 4底部的 CPU 使用率直方图 (CPU Usage Histogram) 显示了测试过程中特定数量的线程同时运行的频率。 大部分时间(14 秒,总时长 37 秒)仅运行单个线程(单个内核)。 其他时间的多线程运行效率非常低,执行的线程数量不到 20。

图 4中部执行汇总的重要热点 (Top Hotspots) 部分显示了详细的运行信息。 它列举了函数调用及其相应的 CPU 时间。 kmp_fork_barrier函数是面向隐式壁垒的内部 OpenMP 函数,用于同步线程执行。 kmp_fork_barrier函数消耗了 1,130 秒 CPU 时间,说明在 87% 的 CPU 执行时间里,线程围绕该壁垒旋转,并未完成任何有用工作。

BVLC Caffe 文件包的源代码不包含 #pragma omp parallel 代码行。 BVLC Caffe 代码中没有明确用于多线程的 OpenMP 库。 然而,英特尔 MLK 中使用 OpenMP 线程对部分数学例程调用进行并行化处理。 为确保这种并行化,我们可以查看自下而上选项卡视图(见图 5,查看在有效利用时间内的函数调用[顶部]以及线程时间线[底部])

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

Timeline visualization and function-call hotspots for BVLC Caffe* CIFAR-10* dataset training
图 5. 关于 BVLC Caffe* CIFAR-10* 数据集培训的时间线可视化和函数调用热点

gemm_omp_driver_v2 function — libmkl_intel_thread.so的一部分 — 是英特尔 MKL 的一种通用矩阵 (GEMM) 乘法实施。 该函数在后台使用 OpenMP 多线程。 优化后的英特尔 MKL 矩阵乘法是用于向前和向后传播的主要函数,即用于权值计算、预测和调整。 英特尔 MKL 初始化 OpenMP 多线程,通常会缩短 GEMM 操作的计算时间。 然而,在这种特殊情况下 — 卷积 32 × 32 图像 — 在单次 GEMM 操作中,该工作负载不足以高效利用 36 个内核上的全部 36 个 OpenMP 线程。 因此需采用一种不同的多线程-并行化方案,本文稍后将予以介绍。

为了说明 OpenMP 线程利用的开销,我们运行包含 OMP_NUM_THREADS=1环境变量的代码,然后对比相同工作负载的执行时间: 31.1 秒,而非 37 秒(见图 4图 6顶部的运行时间部分)。 通过使用这一环境变量,我们迫使 OpenMP 仅创建一个线程,并将其用于代码执行。 相比 BVLC Caffe 代码实施,这缩短的近 6 秒时间即为 OpenMP 线程初始化和同步化开销。

 OMP_NUM_THREADS=1
图 6. 英特尔® VTune™ Amplifier XE 2017 测试版关于 BVLC Caffe* CIFAR-10* 数据集执行单一线程 OMP_NUM_THREADS=1 的分析汇总

通过这种分析设置,我们确定了可在 BVLC Caffe 实施过程中实现性能优化的三个目标: im2col_cpu、col2im_cpuPoolingLayer::Forward_cpu函数调用(见图 6中部)。

代码优化

相比 BVLC Caffe 代码,面向英特尔架构优化的 Caffe 实施 CIFAR-10 数据集的时间缩短了将近 13.5 倍(向前-向后传播的时间分别为 20 毫秒和 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 just-in-time [JIT] 汇编程序) 
  • GNU Compiler Collection* (GCC*) 和 OpenMP 代码矢量化

BVLC Caffe 可以选择采用英特尔 MKL BLAS 函数调用或其他实施方法。 例如,GEMM 函数经过优化,适用于矢量化、多线程化和提高缓存流量。 为提高矢量化效果,我们还使用了 Xbyak — 面向 x86 (IA-32) 和 x64(AMD64* 或 x86-64)的 JIT 汇编程序。 Xbyak 目前支持以下矢量指令集: MMX™ 技术、英特尔® 流式单指令多数据扩展(英特尔® SSE)、英特尔 SSE2、英特尔 SSE3、英特尔 SSE4、浮点单元、英特尔 AVX、英特尔 AVX2 和英特尔 AVX-512。

Xbyak 汇编程序是一种面向 C++ 的 x86/x64 JIT 汇编程序,专为高效开发代码而创建的库。 Xbyak 汇编程序以仅头文件代码的形式提供。 它还可动态汇编 x86 和 x64 助记符。 代码运行期间生成 JIT 二进制代码可支持多种优化方法,比如量化操作和多项式计算操作,前者用特定数组元素除以次要数组元素,后者根据 constant、variable x、add、sub、mul、div等创建行为。 由于支持英特尔 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 通过将整数转换成无符号类型,以对比参数 a 的值是否大于或等于零,还是小于参数 b 的值。参数 b 为带符号类型,通常为正值,因此它的值通常小于 0x800...其中转换负参数值可将其转换为大于 0x800 的值。 这种转换允许使用一个(而非两个)条件。 */
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=c ompact,granularity=fine。 紧凑放置相邻线程可提高 GEMM 操作性能,因为所有线程都可共享相同的末级高速缓存 (LLC),从而可将之前预取的缓存行重复用于数据。

如欲了解有关高速缓存封闭优化实施方法以及数据布局和矢量化,请参阅以下文章: http://arxiv.org/pdf/1602.06709v1.pdf

借助 OpenMP* 实现代码并行化

神经网络层优化结果

以下神经网络层也可通过采用 OpenMP 多线程并行化实现优化:

  • 卷积
  • 去卷积
  • 局部响应归一化 (LRN)
  • ReLU
  • Softmax
  • 级联
  • OpenBLAS* 优化实用程序 — 比如 vPowx - y[i] = x[i]β 操作、caffe_set、caffe_copycaffe_rng_bernoulli
  • 池化
  • Dropout
  • 批处理标准化
  • 数据
  • Eltwise

卷积层

卷积层,顾名思义,将包含一套权值或过滤器的输入进行卷积,在输出图中生成一张特征图。 这种优化方法可防止低效使用适用于一套输入特征图的硬件。

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图;例如,k im2col操作并行执行,k调用至英特尔 MKL。 英特尔 MKL 自动切换至单线程执行流程,其整体性能高于进行批处理的英特尔 MKL。 这种行为在源代码文件 src/caffe/layers/base_conv_layer.cpp中定义。 这种实施方法通过 src/caffe/layers/conv_layer.cpp — 包含相应代码的文件位置,优化了 OpenMP 多线程

池化或二次采样

max-poolingaverage-poolingstochastic-pooling(尚未实施)是三种不同的下采样方法,其中 max-pooling应用最为普遍。 池化层将前一层的结果划分成一套通常不相互重叠的矩形区块。 然后,该层为每个这种子区域输出最大值、算术平均值,或(将来)从通过激活各区块所形成的多项式分布中提取的随机值。

池化非常适用于 CNN,原因有三点:

  • 池化可缩减问题的影响区域,并降低上层的计算负载。
  • 池化下层可支持上层的卷积内核覆盖较大的输入数据区域,从而了解更多复杂特征;例如,下层内核通常学习如何识别小型边缘,而上层内核可能学习如何识别森林或海滩等大型场景。
  • max-pooling提供换算偏差表。 在 8 个支持通过单一像素换算 2 × 2 区块(典型池化区块)的方位中,其中 3 个将返回同一最大值;对 3 × 3 窗口而言,其中 5 个将返回同一最大值。

池化适用于单张特征图,因此我们使用 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-loop、批量迭代图像和图像通道、将两个循环合成一个,并对该循环进行并行化处理。

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 - 激励/神经元层

ReLU 是深度学习算法中目前应用最为普遍的非线性函数。 激励/神经元层是逐元素运算符,提取底部 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 线程和英特尔 MKL 线程。 图 8中的 CPU 使用率直方图显示了实施优化和并行化后,特定数量的线程以并行形式运行的频率。

借助面向英特尔架构优化的 Caffe,同时运行的线程数量显著增加。 借助面向英特尔架构优化的 Caffe,测试系统的执行时间从最初未修改的 37 秒缩短至 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 测试版分析汇总面向 CIFAR-10* 培训实施面向英特尔® 架构优化的 Caffe

图 8顶部的运行时间所示,运行执行过程中仍然存在部分旋转时间 (Spin Time)。 因此,执行性能无法随着线程数量的增加而实现线性增长(根据阿姆达尔定律)。 另外,未通过 OpenMP 多线程实现并行化的代码中仍然存在串行执行区域。 重新初始化 OpenMP 并行区域面向最新 OpenMP 库实施进行了显著优化,但仍然产生了不可忽略的性能开销。 将 OpenMP 并行区域移至主代码函数可提升性能,但要求大量的代码重构工作。

图 9汇总了使用面向英特尔架构优化的 Caffe 时遵循的优化技巧和代码重新编写原则。

Step-by-step approach of Intel® Modern Code Developer Code
图 9. 英特尔® 现代代码开发人员代码详细步骤

在测试中,我们使用了英特尔 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 实现顶级计算性能。

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

关于机器学习的更多信息,请参见:


在性能检测过程中涉及的软件及其性能只有在英特尔微处理器的架构下方能得到优化。 诸如 SYSmark* 和 MobileMark* 等测试均系基于特定计算机系统、硬件、软件、操作系统及功能。 上述任何要素的变动都有可能导致测试结果的变化。 请参考其他信息及性能测试(包括结合其他产品使用时的运行性能)以对目标产品进行全面评估。 如欲了解完整信息,请访问 intel.cn/content/www/cn/zh/benchmarks/intel-product-performance.html

优化声明: 英特尔的编译器针对非英特尔微处理器的优化程度可能与英特尔微处理器相同(或不同)。 这些优化包括 SSE2®、SSE3 和 SSSE3 指令集以及其它优化。 对于在非英特尔制造的微处理器上进行的优化,英特尔不对相应的可用性、功能或有效性提供担保。 此产品中依赖于处理器的优化仅适用于英特尔微处理器。 某些不是专门面向英特尔微体系结构的优化保留专供英特尔微处理器使用。 请参阅相应的产品用户和参考指南,以了解关于本通知涉及的特定指令集的更多信息。

通知版本编号 20110804

英特尔技术的特性和优势取决于系统配置,并需要兼容的硬件、软件或需要激活服务。 实际性能会因您使用的具体系统配置的不同而有所差异。 任何计算机系统都无法提供绝对的安全性。 请联系您的系统制造商或零售商,或访问 intel.com,了解更多信息。

英特尔技术可能要求激活支持的硬件、特定软件或服务。 请咨询您的系统制造商或零售商。

本文件不构成对任何知识产权的授权,包括明示的、暗示的,也无论是基于禁止反言的原则或其他。

英特尔明确拒绝所有明确或隐含的担保,包括但不限于对于适销性、特定用途适用性和不侵犯任何权利的隐含担保,以及任何对于履约习惯、交易习惯或贸易惯例的担保。

本文包含尚处于开发阶段的产品、服务和/或流程的信息。 此处提供的信息可随时改变而毋需通知。 联系您的英特尔代表,了解最新的预测、时间表、规格和路线图。

本文件所描述的产品和服务可能包含使其与宣称的规格不符的设计缺陷或失误。 这些缺陷或失误已收录于勘误表中,可索取获得。

索取本文件中提的、包含订单号的文件的复印件,可拨打1-800-548-4725,或登陆 intel.com/design/literature.htm

该示例源代码根据英特尔示例源代码许可协议发布。

英特尔、Intel 标识、Intel Xeon Phi、英特尔至强融核、VTune、Xeon 和至强是英特尔在美国和/或其他国家(地区)的商标。

*其他的名称和品牌可能是其他所有者的资产。

英特尔公司 © 2016 年版权所有。 所有权保留。

0816/VK/PRW/PDF 334759-001CN

基于英特尔® 至强 E5 系列处理器的单节点 Caffe 评分和训练

$
0
0

     在互联网搜索引擎和医疗成像等诸多领域,深度神经网络 (DNN) 应用的重要性正在不断提升。 Pradeep Dubey 在其博文中概述了英特尔®架构机器学习愿景。 英特尔正在实现 Pradeep Dubey 博文中勾勒的机器学习愿景,并正在着手开发软件解决方案以加速执行机器学习工作负载。这些解决方案将包含在未来版本的英特尔®数学核心函数库(英特尔® MKL)英特尔®数据分析加速库(英特尔® DAAL)中。 本技术预览版展示了配备我们正在开发的软件后,英特尔平台将有望实现的性能。  这一版本仅可在支持英特尔® 高级矢量扩展指令集 2(英特尔® AVX2)的处理器上运行。 在未来的文章中,我们将介绍分布式多节点配置可带来的优势。

     本文介绍的预览包功能有限,且并非设计用于生产用途。 此处讨论的特性现已在英特尔 MKL 2017 测试版英特尔 Caffe 分支 (fork)中推出。

    Caffe是伯克利愿景和学习中心 (Berkeley Vision and Learning Center, BVLC) 开发的一个深度学习框架,也是最常用的用于图像识别的社区框架之一。 Caffe 通常作为性能指标评测与 AlexNet(一种图像识别神经网络拓扑)和 ImageNet(一种标签图像数据库)一起使用。

Caffe 可充分利用英特尔 MKL 中优化的数学例程,同时也将可以通过应用代码现代化技术,进一步提升基于英特尔®至强® 处理器的系统的性能。 通过合理使用英特尔 MKL、矢量化和并行化技术,相比未优化的 Caffe 方案,经过优化的方案有望将训练性能提升 11 倍,将分类性能提升 10 倍。

借助这些优化,在整个 ILSVRC-2012 数据集上训练 AlexNet* 网络以在 80% 的时间实现排名前五的准确度,所需的时间从 58 天缩短至大约 5 天。

开始

我们正努力为软件产品开发新功能,目前您可使用本文附带的技术预览包再现展示的性能结果,甚至使用您自己的数据集训练 AlexNet。

该预览包支持 AlexNet 拓扑,并引入了“intel_alexnet”模型,它类似于 bvlc_alexnet,添加了 2 个全新的“IntelPack“和“IntelUnpack”层,以及优化的卷积、池化和规范化层。 此外,我们还更改了验证参数以提高矢量化性能,将验证 minibatch 的数值从 50 提高到 256,将测试迭代次数从 1000 减少到 200,从而使验证运行中使用的图像数量保持不变。 该预览包在以下文件中加入了 intel_alexnet 模型:

  • models/intel_alexnet/deploy.prototxt
  • models/intel_alexnet/solver.prototxt
  • models/intel_alexnet/train_val.prototxt.

“intel_alexnet”模型支持您训练和测试 ILSVRC-2012 训练集。

开始使用该预览包时,请确保“系统要求和限制”中列出的所有常规 Caffe 依赖项均已安装在系统中,然后:

  • 对预览包进行解包。
  • 为以下“intel_alexnet”模型文件中的数据库、快照位置和图像均值文件指定路径。
    • models/intel_alexnet/deploy.prototxt
    • models/intel_alexnet/solver.prototxt
    • models/intel_alexnet/train_val.prototxt
  • 为“系统要求和限制”部分列出的软件工具设置运行时环境。
  • 在 LD_LIBRARY_PATH 环境变量中添加 ./build/lib/libcaffe.so 路径
  • 设置线程环境:
    $> export OMP_NUM_THREADS=<N_processors * N_cores>
    $> export KMP_AFFINITY=compact,granularity=fine
  • 使用以下命令在单节点上执行计时:
    $> ./build/tools/caffe time \
           -iterations <number of iterations> \
           --model=models/intel_alexnet/train_val.prototxt
  • 使用以下命令在单节点上执行训练:
    $> ./build/tools/caffe train \
           --solver=models/intel_alexnet/solver.prototxt

系统要求和限制

该预览包与未优化的 Caffe 拥有相同的软件依赖项:

英特尔 MKL 11.3 或更高版本

硬件兼容性:

此软件仅使用 AlexNet 拓扑进行了验证,可能不适用于其他配置。

支持:

如有关于该预览包的任何问题和建议,请联系:mailto:intel.mkl@intel.com

英特尔® Theano*软件优化包和英特尔® Python* 分发包入门指南

$
0
0

目录

总结

Theano 是 LISA实验室开发的 Python* 库,用于定义、优化和评估数学表达式,包括带有多维阵列 (numpy.ndarray) 的数学表达式。 为了优化英特尔® 架构,支持英特尔® 数学核心函数库 (Intel® MKL) 2017,英特尔推出了基于 Theano 0.0.8rc1 的英特尔® Theano*软件优化包。 英特尔 MKL 的最新版包括针对英特尔® 高级矢量扩展指令集 2(英特尔® AVX2)和AVX-512 指令的优化,英特尔® 至强® 处理器和英特尔® 至强融核™ 处理器均支持这些指令。

Theano 可以在各种平台安装使用,支持不同开发工具和库的组合。 关于如何利用英特尔® 编译器和英特尔 MKL 2017 在基于 CentOS* 和 Ubuntu* 的系统构建和安装英特尔® Theano*软件优化包,本教程会提供详细步骤。 通过运行常见的行业标准性能指标评测(如 MNIST*、DBN-Kyoto*、LSTM* 和 ImageNet*)来进一步验证安装。

前提条件

英特尔® 编译器和英特尔®数学核心函数库 2017

本教程假定已经安装并验证英特尔编译器(C/C++ 和 Fortran)。 如果没有,请下载并安装英特尔编译器,可以作为 英特尔® Parallel Studio XE的组件安装,也可以独立安装。

使用英特尔® Python* 分发包时,可以选择是否安装英特尔 MKL 2017。 对于其他的 Python 分发包,英特尔 MKL 2017 可以作为英特尔 Parallel Studio XE 2017 的组件下载,也可以使用社区许可免费下载安装。 请点击 此处免费注册社区许可,并按照安装说明进行下载。

Python* 工具

英特尔® Python* 分发包支持用户随时获取工具和技术,这些工具和技术经过精心的设计和验证,能够在英特尔架构上实现更高的性能。因此,本教程使用英特尔® Python* 分发包。 可以直接使用英特尔优化的预编译工具,如 NumPy* 和 SciPy*,不需要构建和安装。 

英特尔® Python* 分发包可以作为 英特尔 Parallel Studio XE的组件下载,也可以点击 此处免费下载。

英特尔® Python* 分发包的安装过程如下所示。 本文假设 Python 安装在本地用户账户。

Python 2.7
tar -xvzf l_python27_p_2017.0.028.tgz
cd l_python27_p_2017.0.028
./install.sh

Python 3.5
tar -xvzf l_python35_p_2017.0.028.tgz
cd l_python35_p_2017.0.028
./install.sh

使用 anaconda,按照以下步骤创建独立的用户环境。 通过以下指令,安装所需的 NumPy、SciPy 和 Cython 程序包。 

Python 2.7
conda create -n pcs_theano_2 -c intel python=2 numpy scipy cython
source activate pcs_theano_2

Python 3.5
conda create -n pcs_theano_2 -c intel python=3 numpy scipy cython
source activate pcs_theano_2

或者,可以按照附录 A的源代码,构建并安装 NumPy 和 SciPy。 如果使用非英特尔® Python* 分发包,需要安装其它 python 开发工具,安装步骤详见本文。

 

构建和安装英特尔® Theano*软件优化包

利用以下存储库,查看并安装针对英特尔架构的 theano 优化包分支。

git clone https://github.com/intel/theano.git theano
cd theano
python setup.py build
python setup.py install
theano-cache clear

下面给出 Theano 配置文件的示例,以供参考。 要使用英特尔编译器,并指定与 Theano 一起使用的编译器标志,请在用户的主目录创建该文件的副本。

vi ~/.theanorc

[cuda]
root = /usr/local/cuda

[global]
device = cpu
floatX = float32
cxx = icpc
mode = FAST_RUN
openmp = True
openmp_elemwise_minsize = 10
[gcc]
cxxflags = -qopenmp -march=native -O3 -vec-report3 -fno-alias -opt-prefetch=2 -fp-trap=none
[blas]
ldflags = -lmkl_rt

 

验证 Theano 和 NumPy 安装

Theano 和 NumPy 库导入 python 后,必须验证引用的版本。 本文引用的 Theano 和 NumPy 的版本按照如下方法验证:  

python -c "import numpy; print (numpy.__version__)"
->1.11.1
python -c "import theano; print (theano.__version__)"
-> 0.9.0dev1.dev-*

还需要验证 Theano 和 NumPy 的安装版本是否使用英特尔 MKL。

python -c "import theano; print (theano.numpy.show_config())"

NumPy_Config

图 1。 theano.numpy.show_config() 的预期结果

 

性能指标评测

theano/democase 目录下可以找到 DBN-Kyoto 和 ImageNet 的性能指标评测。

DBN-Kyoto

采集数据集,并运行 DBN-Kyoto

从 Dropbox 中为 DBN-Kyoto 下载样本数据集,链接地址:https://www.dropbox.com/s/ocjgzonmxpmerry/dataset1.pkl.7z?dl=0. 将文件解压,并保存到theano/democase/DBN-Kyoto 目录。

前提条件

可以使用 Anaconda 安装关联组件,用于训练 DBN-Kyoto,也可以利用工具目录中的已有资源构建关联组件。 由于 pandas 库和 Python 3 存在冲突,性能指标评测只是经过 Python 2.7 验证。

Python 2.7
conda install -c intel --override-channels pandas
conda install imaging

也可以利用 附录 B中的源代码安装关联组件。

在 CPU 上运行 DBN-Kyoto

可以用 run.sh 脚本下载数据集(如果数据集不存在的话),并开始训练。.

cd theano/democase/DBN-Kyoto/
./run.sh

 

MNIST

本文展示了如何使用 Lasagne 在 MNIST 中训练神经网络。Lasagne 是一个轻型库,用于在 Theano 中构建并训练神经网络。 利用英特尔编译器构建并安装 Lasagne 库。

下载 MNIST 数据库

如欲下载 MNIST 数据库,请点击 http://yann.lecun.com/exdb/mnist/。 下载图像和标签,以训练并验证数据。 

安装 Lasagne 库

构建并安装新版 Lasagne 库,请使用如下的 Lasagne git 存储库。

Python 2.7 and Python 3.5
git clone https://github.com/Lasagne/Lasagne.git
cd Lasagne
python setup.py build
python setup.py install

训练

cd Lasagne/examples
python mnist.py [model [epochs]]
                    --  where model can be mlp - simple multi layer perceptron (default) or
                         cnn - simple convolution neural network.
                         and epochs = 500 (default)

 

AlexNet

采集 ImageNet 数据集,以训练 AlexNet。

ImageNet 数据集可从 image-net 网站上获取。 

前提条件

可以使用 Anaconda 安装用于训练 AlexNet 的关联组件,也可以在 fedora epel 源资源库中安装。 目前,只有 Python 2 支持 Hickle(预处理数据所必需的关联组件),Python 3 不支持。

  • 利用 Anaconda 安装 h5py、pyyaml 和 pyzmq:
conda install h5py
conda install -c intel --override-channels pyyaml pyzmq
  • 安装 Hickle (基于 HDF5 的 Pickle 的克隆版本):
git clone https://github.com/telegraphic/hickle.git
cd hickle
python setup.py build
python setup.py install

也可以利用 附录 B中的源代码安装关联组件

预处理 ImageNet 数据集

转储 Hickle 文件,创建训练、验证数据的标签,首先需要预处理

  • 在预处理目录中修改 paths.yaml 文件,以更新数据集的路径。 给出 paths.yaml 文件的一个示例,以供参考。
cat theano/democase/alexnet_grp1/preprocessing/paths.yaml

train_img_dir: '/mnt/DATA2/TEST/ILSVRC2012_img_train/'
# the dir that contains folders like n01440764, n01443537, ...

val_img_dir: '/mnt/DATA2/TEST/ILSVRC2012_img_val/'
# the dir that contains ILSVRC2012_val_00000001~50000.JPEG

tar_root_dir: '/mnt/DATA2/TEST/parsed_data_toy'  # dir to store all the preprocessed files
tar_train_dir: '/mnt/DATA2/TEST/parsed_data_toy/train_hkl'  # dir to store training batches
tar_val_dir: '/mnt/DATA2/TEST/parsed_data_toy/val_hkl'  # dir to store validation batches
misc_dir: '/mnt/DATA2/TEST/parsed_data_toy/misc'
# dir to store img_mean.npy, shuffled_train_filenames.npy, train.txt, val.txt

meta_clsloc_mat: '/mnt/DATA2/imageNet-2012-images/ILSVRC2014_devkit/data/meta_clsloc.mat'
val_label_file: '/mnt/DATA2/imageNet-2012-images/ILSVRC2014_devkit/data/ILSVRC2014_clsloc_validation_ground_truth.txt'
# although from ILSVRC2014, these 2 files still work for ILSVRC2012

# caffe style train and validation labels
valtxt_filename: '/mnt/DATA2/TEST/parsed_data_toy/misc/val.txt'
traintxt_filename: '/mnt/DATA2/TEST/parsed_data_toy/misc/train.txt'

如欲创建 Toy 数据集,可以使用现有的脚本 - generate_toy_data.sh1

cd theano/democase/alexnet_grp1/preprocessing
chmod u+x make_hkl.py make_labels.py make_train_val_txt.py
./generate_toy_data.sh

在 CPU 上训练 AlexNet

  • 修改 config.yaml 文件,以更新预处理数据集的路径:
cd theano/democase/alexnet_grp1/

# Sample changes to the path for input(label_folder, mean_file) and output(weights_dir)
label_folder: /mnt/DATA2/TEST/parsed_data_toy/labels/
mean_file: /mnt/DATA2/TEST/parsed_data_toy/misc/img_mean.npy
weights_dir: ./weight/  # directory for saving weights and results
  • 以同样的方式修改 spec.yaml 文件,以更新解析 toy 数据集的路径:
# Directories
train_folder: /mnt/DATA2/TEST/parsed_data_toy/train_hkl_b256_b256_bchw/
val_folder: /mnt/DATA2/TEST/parsed_data_toy/val_hkl_b256_b256_bchw/
  • 开始训练:
./run.sh

大型电影评论数据集 (IMDB)

大型电影评论数据集是利用长期短期存储器 (LSTM) 模型的重复神经网络。 使用 LSTM 模型,IMDB 数据集对电影评论进行情绪分析。

采集数据集:

如欲获取 imdb.pkl 文件,请访问 http://www-labs.iro.umontreal.ca/~lisa/deep/data/,并解压缩到本地文件夹。

预处理

http://deeplearning.net/tutorial/lstm.html页面提供 2 个脚本:

Imdb.py – 加载 IMDB 数据集的预处理。

Lstm.py – 定义、训练模型的主要脚本。

复制上述两个文件,存放到 imdb.pkl 所在的文件夹。

训练

执行如下命令,开始训练:

THEANO_FLAGS="floatX=float32" python lstm.py

故障排除

错误 1: 在有些情况下,会出现无法打开的错误,如 libmkl_rt.so 和 libimf.so。 在这种情况下,进行如下操作:

find /opt/intel -name library_name.so

添加路径,转到 /etc/ ld.so.conf 文件,运行 ldconfig 命令以链接到库。 同时,要保证在 LD_LIBRARY_PATH 环境变量中正确设置 MKL 安装路径。

错误 2: toy 数据的 AlexNet 的预处理错误

python make_hkl.py toy
generating toy dataset ...
Traceback (most recent call last):
  File "make_hkl.py", line 293, in <module>
    train_batchs_per_core)
ValueError: xrange() arg 3 must not be zero

目前,用于预处理 ImageNet 的默认进程数量设为 16。 toy 数据集会产生多余的进程,导致应用崩溃。 若要解决这个问题,在 Alexnet_CPU/preprocessing/make_hkl.py:258 文件中,把进程数量改为 2 个。 在预处理整个数据集时,建议提高 num_process 的数值,以提升预处理的速度。

num_process = 2

错误 3: 利用 Conda 安装英特尔® Python* 分发包时,引用 Numpy 的最新版本。

如果您的英特尔® Python* 分发包不是通过英特尔® Python* 分发包安装程序安装的,而是在 Conda 内安装的,请把 PYTHONNOUSERSITE 的环境变量设置为 True。 目的是确保 Conda 环境引用正确的 Numpy 版本。 这是 Conda 的已知错误。 如欲了解更多信息,请点击此处

export PYTHONNOUSERSITE=True

资源

附录 A

为其他 Python 分发包安装 Python* 工具

CentOS:
Python 2.7 - sudo yum install python-devel python-setuptools
Python 3.5 - sudo yum install python35-libs python35-devel python35-setuptools
//Note - Python 3.5 packages can be obtained from Fedora EPEL source repository
Ubuntu:
Python 2.7 - sudo apt-get install python-dev python-setuptools
Python 3.5 - sudo apt-get install libpython3-dev python3-dev python3-setuptools
  • 如果系统没有安装 pip 和 cython,请执行如下命令以完成安装:
sudo -E easy_install pip
sudo -E pip install cython

 

安装 NumPy

NumPy 是用于 Python 的科学计算的基础软件包。 软件包包含:

  1. 强大的 n 维度阵列对象
  2. 高级(广播)函数
  3. 集成 C/C++ 和 Fortran 编码的工具
  4. 实用的线性代数、傅立叶变换和随机数功能

注: 通过验证、删除相关文件,可以删除旧版的 NumPy 库。 然而,本教程剩余的库将安装在用户的本地目录中,所以这一步是可选的。 如果需要的话,旧版可按如下步骤清除:

  • 验证旧版是否存在:
python -c "import numpy; print numpy.version"<module 'numpy.version' from '/home/plse/.local/lib/python2.7/site-packages/numpy-1.11.0rc1-py2.7-linux-x86_64.egg/numpy/version.pyc'>
  • 删除之前安装的 NumPy 程序包:
rm -r /home/plse/.local/lib/python2.7/site-packages/numpy-1.11.0rc1-py2.7-linux-x86_64.egg
  • 构建并安装针对英特尔架构优化的 NumPy:
git clone https://github.com/pcs-theano/numpy.git
//update site.cfg file to point to required MKL directory. 如果 parallel studio 或 MKL 安装在默认 /opt/intel  directory.
python setup.py config --compiler=intelem build_clib --compiler=intelem build_ext --compiler=intelem install --user,这一步骤是可选的。

 

安装 SciPy

SciPy 是开源 Python 库,用于科学计算和技术计算。 SciPy 包含优化模块、线性代数、集成、插值、特殊函数、FFT、信号和图像处理、ODE 解算器和其他常见的科学和工程任务。

  • 构建并安装 SciPy:
tar -xvzf scipy-0.16.1.tar.gz    (can be downloaded from: https://sourceforge.net/projects/scipy/files/scipy/0.16.1/  or
     obtain the latest sources from https://github.com/scipy/scipy/releases)
cd scipy-0.16.1/
python setup.py config --compiler=intelem --fcompiler=intelem build_clib --compiler=intelem --fcompiler=intelem build_ext --compiler=intelem --fcompiler=intelem install --user

附录B

利用源代码构建、安装性能指标评测的关联组件

DBN-Kyoto

//Untar and install all the provided tools:

cd theano/democase/DBN-Kyoto/tools
tar -xvzf Imaging-1.1.7.tar.gz
cd Imaging-1.1.7
python setup.py build
python setup.py install --user

cd theano/democase/DBN-Kyoto/tools
tar -xvzf python-dateutil-2.4.1.tar.gz
cd python-dateutil-2.4.1
python setup.py build
python setup.py install --user

cd theano/democase/DBN-Kyoto/tools
tar -xvzf pytz-2014.10.tar.gz
cd pytz-2014.10
python setup.py build
python setup.py install --user

cd theano/democase/DBN-Kyoto/tools
tar -xvzf pandas-0.15.2.tar.gz
cd pandas-0.15.2
python setup.py build
python setup.py install --user

 

AlexNet

  • 利用源代码安装 AlexNet 的关联组件

如欲在 CPU 运行 AlexNet,需要访问 fedrora epel 源资源库的附加软件包

wget http://dl.fedoraproject.org/pub/epel/7/x86_64/e/epel-release-7-8.noarch.rpm
sudo rpm -ihv epel-release-7-8.noarch.rpm
sudo yum install hdf5-devel
sudo yum install zmq-devel
sudo yum install zeromq-devel
sudo yum install python-zmq
  • 安装 Hickle (基于 HDF5 的 Pickle 的克隆版本):
git clone https://github.com/telegraphic/hickle.git
python setup.py build install --user
  • 安装 h5py(HDF5 二进制数据格式的 Python 界面):
git clone https://github.com/h5py/h5py.git
python setup.py build install --user

 

参考资料

 

作者简介

Sunny GogarSunny Gogar
软件工程师

Sunny Gogar 获得了佛罗里达大学电子与计算机工程专业的硕士学位,以及印度孟买大学电子与电信学专业的学士学位。  目前在英特尔公司软件及服务事业部担任软件工程师。 他的兴趣在于面向多核和众核处理器架构的并行编程与优化。

Meghana Rao 获得了波特兰州立大学工程与技术管理专业的硕士学位,以及印度班加罗尔大学计算机科学和工程专业的学士学位。  她是英特尔软件与服务事业部的开发人员宣传官,专注于机器学习和深度学习。

 

英特尔® 高级矢量扩展指令集简介

$
0
0

作者 Chris Lomont

下载文章

下载 英特尔® 高级矢量扩展指令集简介 [PDF 1.4MB]

英特尔® 高级矢量扩展指令集(英特尔® AVX)是在英特尔® 架构 CPU 上执行单指令多数据 (SIMD) 运算的指令集。这些指令添加了以下特性,对之前的 SIMD 产品——MMX™ 指令和英特尔® 数据流单指令多数据扩展指令集(英特尔® SSE)进行了扩展:

  • 将 128 位 SIMD 寄存器扩展至 256 位。英特尔® AVX 的目标是在未来可以支持 512 或 1024 位。
  • 添加了 3 操作数非破坏性运算。之前在 A = A + B 类运算中执行的是 2 操作数指令,它将覆盖源操作数,而新的操作数可以执行 A = B + C 类运算,且保持原始源操作数不变。.
  • 少数几个指令采用 4 寄存器操作数,通过移除不必要的指令,支持更小、更快的代码。
  • 对于操作数的内存对齐要求有所放宽。
  • 新的扩展编码方案 (VEX) 旨在使得以后添加更加容易以及所执行的指令编码更小、速度更快。

与这些改进密切相关的是新的融合乘加 (FMA) 指令,它可以更快、更精确地执行专门运算,例如单指令 A = A * B + C。第二代英特尔® 酷睿™ CPU 中将可提供 FMA 指令。其他特性还包括处理高级加密标准 (AES) 加密和解密的指令、用于某些加密基元的紧缩 carry-less 乘法运算 (PCLMULQDQ) 以及某些用于未来指令的预留槽,如硬件随机数生成器。

 

指令集概述

新指令使用英特尔的 VEX prefix 进行编码,VEX prefix 是一个 2个或 3 个字节的前缀,旨在降低当前和未来 x86/x64 指令编码的复杂性。两个新的 VEX prefix 形成于两个过时的 32 位指令——使用 DS 的 Load Pointer(LDS-0xC4,3 字节形式)和使用 ES 的 Load Pointer(LES-0xC5, 2 字节形式),它们以 32 位模式加载 DS 和 ES 段寄存器。在 64 位模式中,操作码 LDS 和 LES 生成一个无效操作码异常,但是在英特尔® AVX 下,这些操作码可另外用作编码新指令前缀。最终,VEX 指令只能在 64 位模式中运行时使用。前缀可比之前的 x86 指令编码更多的寄存器,可用于访问新的 256 位 SIMD 寄存器或者使用 3 和 4 操作数语法。作为用户,您无需担心这个问题(除非您正在编写汇编器或反汇编器)。


注:下文假定运算都是在 64 位模式中进行。


SIMD 指令可以在一个单一步骤中处理多个片段的数据,加速许多任务的吞吐量,包括视频编解码、图像处理、数据分析和物理模拟。在 32 位长度(称之为单精度)和64 位长度(称之为双精度)中,英特尔® AVX 指令在IEEE-754浮点值上运行。IEEE-754 是一个标准定义的、可复制的强大浮点运算,是大多数主流数字计算的标准。

较早的相关英特尔® SSE 指令还支持各种带符号和无符号整数大小,包括带符号和无符号字节(B,8 位)、字(W,16 位)、双字(DW,32 位)、四字(QW,64 位)和双四字(DQ,128 位)长度。并非所有指令都适用于所有大小组合,更多详细信息,敬请参阅“更多信息”中提供的链接。请参阅本文后文中的图 2,了解数据类型的图形表示法。

支持英特尔® AVX(和 FMA)的硬件包括 16 个 256 位 YMM 寄存器 YMM0-YMM15 和一个名为 MXCSR的 32 位控制/状态寄存器。YMM 寄存器替代了英特尔® SSE 使用的较早的 128 位 XMM 寄存器,它将 XMM 寄存器视作相应 YMM 寄存器的下层部分,如图 1 所示。

MXCSR 的 0-5 位显示设置“粘滞”位后 SIMD 浮点异常,除非使用位LDMXCSRFXRSTOR清除,否则它们将一直保持设置。设置时 7-12 位屏蔽个体异常,可通过启动进行初始设置或重置。0-5 位分别显示为无效运算、非法、被零除、溢出和精度。如欲获取详细信息,请参阅“更多信息”部分提供的链接。

 

 

 

图 1. XMM 寄存器覆盖 YMM 寄存器。


图 2 显示了英特尔® SSE 和英特尔® AVX 指令中使用的数据类型。对于英特尔® AVX,允许使用增加至 128 或 256 位的 32 位或 64 位浮点类型的倍数以及增加至 128 位的任意整数类型的倍数。

图 2.英特尔® AVX 和英特尔® SSE 数据类型

 

指令通常分为标量版本和矢量版本,如图 3 所示。矢量版本通过将数据以并行“SIMD”模式在寄存器中处理进行运算;而标量版本则只在每个寄存器的一个条目中进行运算。这一区别减少了某些算法中的数据移动,提供了更加出色的整体吞吐量。

图 3. SIMD 与标量运算

 

当数据以 n 字节内存界限上存储的 n 字节块进行运算时,数据为内存对齐数据。例如,将 256 位数据加载至 YMM 寄存器中时,如果数据源为 256 位对齐,则数据被称为“对齐”.

对于英特尔® SSE 运算,除非明确规定,否则都需要内存对齐。例如,在英特尔® SSE 下,有针对内存对齐和内存未对齐运算的特定指令,如MOVAPD(移动对齐的紧缩双精度值)和 MOVUPD(移动非对齐的紧缩双精度值)指令。没有像这样分为两个的指令需要执行对齐访问。

英特尔® AVX 已经放宽了某些内存对齐要求,因此默认情况下,英特尔® AVX 允许未对齐的访问;但是,这样的访问将导致性能下降,因此旨在要求数据保持内存对齐的原规则仍然是不错的选择(面向 128 位访问的 16 位对齐和面向 256 位访问的 32 位对齐)。主要例外是明确指出需要内存对齐数据的 SSE 指令的VEX 扩展版本:这些指令仍然要求使用对齐的数据。其他要求对齐访问的特定指令请参阅英特尔® 高级矢量扩展指令集编程指南中的表2.4(请参阅“更多信息”中提供的链接)。

除了未对齐数据问题外,另外一个性能问题是混合使用旧的仅 XMM 的指令和较新的英特尔® AVX 指令会导致延迟,因此需要最大限度地控制 VEX 编码指令和旧的英特尔® SSE 代码之间的转换。也就是说,不要将 VEX 前缀的指令和非 VEX 前缀的指令混合使用,以实现最佳吞吐量。如果您非要这么做,请对同一 VEX/非 VEX 级别的指令进行分组,最大限度地控制二者之间的转换。此外,如果上层 YMM 位通过 VZEROUPPERVZEROALL设置为零(编译器应自动插入),则无转换损失。该插入要求另外一个指令,因此推荐使用分析 (profiling)。

英特尔® AVX 指令类

如上所述,英特尔® AVX 增加了对许多新指令的支持并将当前的英特尔® SSE 指令扩展至新的 256 位寄存器,大部分旧的英特尔® SSE 指令都具有一个 V 前缀的英特尔® AVX 版本,以访问新的寄存器容量和 3 操作数形式。根据指令的计算方式,新英特尔® AVX 指令的数量多达几百个。

例如,旧的 2 操作数英特尔® SSE 指令ADDPS xmm1、xmm2/m128现在可以使用 VADDPS ymm1、ymm2、ymm3/m256 形式以 3 操作数语法表示为 VADDPS xmm1、xmm2、xmm3/m128 或者 256 位寄存器。少数指令还支持 4 操作数,例如 VBLENDVPS ymm1、ymm2、ymm3/m256、ymm4 ,在一定条件下在 ymm4 中根据掩码将单精度浮点值由 ymm2 ymm3/m256 复制到 ymm1 。这是在之前的形式上进行了改进,其中 xmm0 为隐式需求,要求编译器解放 xmm0 。现在所有寄存器都明确可见,寄存器的分配有了更大的自由度。此处, m128 是一个 128 位内存位置, xmm1 是一个 128 位寄存器,以此类推。

有些新指令仅限 VEX(非英特尔® SSE 扩展指令),包括许多将数据移进或移出 YMM 寄存器的方法。以 VBROADCASTS[S/D] 为例,它将一个单一值加载至 XMM 或 YMM 寄存器的所有元素中,并提供了多种使用 VPERMILP[S/D] 将数据移至一个寄存器的方法。(方括号中的内容请参阅附录 A。)

英特尔® AVX 添加了算法指令,以使变量对单精度和双精度紧缩和标量浮点数据进行加、减、乘、除、平方根、比较、最小值、最大值和约数的运算。许多新的条件判定对于 128 位英特尔® SSE 也非常有用,提供了32 种比较类型。英特尔® AVX 还包括从之前的SIMD 中获得的指令,包括逻辑、混合、转换、测试、紧缩、解紧缩、移动、加载和存储。工具集还添加了新指令,包括非跳跃式获取(将单数据或多数据传播至 256 位目标,屏蔽移动基元进行有条件的加载和存储)、向 256 位 SIMD 寄存器中插入多 SIMD 数据或者从中提取多 SIMD 数据、在一个寄存器内转换基元进行数据处理、分支处理以及紧缩测试指令。


未来添加
英特尔® AVX 手册还列出了某些未来可能会使用到的指令,此处并未完全列出,有待补充。此处并不确保这些指令都如编写的那样可以实现。

预留两个指令(VCVTPH2PS VCVTPS2PH ),用于支持 16 位浮点转换为单和双浮点类型或者从中转换。16 位格式称之为半精度,具有一个 10 位尾数(非反向规格化数有一个隐含的以 1 开头的数,导致 11 位精度)、5 位指数(偏差 15)和 1 位符号。

拟定的 RDRAND 指令使用一个具有密码的安全硬件数字随机位生成器,为 16 位、32 位和 64 位寄存器生成随机数。成功后,进位标识被设置为 1 (CF=1 )。如果没有足够的熵值可用,进位标识将被清除 (CF=0 )。

最后,有四个指令(RDFDBASE、RDGSBASE、WRFSBASE 和 WRGSBASE)可在 64 位模式中以全部权限等级读取和写入 FS 和 GS 寄存器。

另外,以后还会添加 FMA 指令,执行类似 A = + A * B + C 的运算,右侧的加号 (+) 都可以变更为减号 (?),而且右侧的三个操作数顺序可以随意发生变化。还有交叉加法和减法形式。紧缩 FMA 指令可以执行具有 256 位矢量的 8 个单精度 FMA 运算或者 4 个双精度 FMA 运算。

A = A * B + C 这类的 FMA 运算要优于每次执行一个步骤,因为中间结果被视为无限精度,会在存储上执行约数计算,因此计算更为精确。这一单个约数是为“融合”前缀提供的。它们也比按步骤执行运算的速度要快。

每个指令对于操作数 A、B 和 C 都有三种形式的顺序,每个顺序对应一个 3 位扩展:形式 132执行 A = AC + B,形式 213执行 A = BA + C,形式 231执行 A = BC + A。顺序数仅代表表达式右侧操作数的顺序。

可用性与支持

在硬件中检测英特尔® AVX 特性的可用性需要使用 CPUID 指令在 CPU 和操作系统中查询支持,稍后会做详细说明。2011 年第一季度推出的第二代英特尔® 酷睿™ 处理器(代号为 Sandy Bridge 的英特尔® 微架构)是英特尔首款支持英特尔® AVX 技术的处理器。这些处理器没有新的 FMA 指令。为了能够在没有硬件支持的情况下进行开发和测试,免费的英特尔® 软件开发仿真器(请参阅“更多信息”中提供的链接)提供了对这些特性的支持,包括英特尔® AVX、FMA、PCLMULQDQ 和 AES 指令。

为了能够在大多数设置中可靠地使用英特尔® AVX 扩展指令集,操作系统必须支持在线程环境切换中保存和加载新的寄存器(采用 XSAVE/XRSTOR),以预防数据损坏。为了避免出现此类错误,从支持英特尔® AVX 感知环境切换的操作系统明确地设置一个 CPU 位,以支持新的指令;否则,在使用英特尔® AVX 指令时,会生成一个未定义的操作码 (#UD) 异常。

带有 Service Pack 1 (SP1) 的 Microsoft Windows* 7 和带有 SP1 的 Microsoft Windows* Server 2008 R2(32 位和 64 位版本)以及更高版本的 Windows* 都支持英特尔® AVX 在线程和进程切换中进行保存和恢复。Linux* 内核 2.6.30(2009 年6 月)及更高版本也支持英特尔® AVX。

检测可用性与支持
对英特尔® AVX、FMA、AES 和 PCLMULQDQ 四个领域进行支持检测的步骤是类似的,都包括检查相应特性的硬件和操作系统支持(请参阅表 1)。包括以下步骤(计算位是从位 0 开始):

  1. 使用 CPUID.1:ECX.OSXSAVE bit 27 = 1 确认操作系统支持 XGETBV
  2. 同时,确认支持 CPUID.1:ECX bit 28=1 (支持英特尔® AVX)和/或 bit 25=1 (支持 AES)和/或 bit 12=1 (支持 FMA)和/或 bit 1=1 (PCLMULQDQ)。
  3. 发出 XGETBV ,并验证在位 1 和位 2 处特性支持的掩码是 11b(操作系统支持 XMM 状态和 YMM 状态)

表 1.特性检测掩码

FeatureBits to checkConstant
英特尔® AVX28,、27018000000H
VAES28,、27, and 2501A000000H
VPCLMULQDQ28、 27, and 1018000002H
FMA28,、27, and 12018001000H


条目 1 中提供了实施该进程的样例代码,其中 CONSTANT 是表 1 中的值。稍后将提供 Microsoft* Visual Studio* C++ 内联函数版本。

条目 1.特性检测

INT Supports_Feature()
   {
   ; result returned in eax
   mov eax, 1
   cpuid
   and ecx, CONSTANT
   cmp ecx, CONSTANT; check desired feature flags
   jne not_supported
   ; processor supports features
   mov ecx, 0; specify 0 for XFEATURE_ENABLED_MASK register
   XGETBV; result in EDX:EAX
   and eax, 06H
   cmp eax, 06H; check OS has enabled both XMM and YMM state support
   jne not_supported
   mov eax, 1; mark as supported
   jmp done
   NOT_SUPPORTED:
   mov eax, 0 ; // mark as not supported
   done:
   }
 

用途

在最低的编程级别,大部分常用 x86 汇编器现在都支持英特尔® AVX、FMA、AES 和 VPCLMULQDQ 指令,包括 Microsoft MASM*(Microsoft Visual Studio* 2010 版本)、NASM*、FASM* 和 YASM*。请参阅各自的相关文档,获取详细信息。

对于语言编译器,英特尔® C++ 编译器 11.1 版及更高版本和英特尔® Fortran 编译器都可以通过编译器开关支持英特尔® AVX;而且这两种编译器还支持自动矢量化和浮点循环。英特尔® C++ 编译器支持英特尔® AVX 内联函数(使用 #include 访问内联函数)和内嵌汇编语言,甚至还可以使用 #include "avxintrin_emu.h"支持英特尔® 内联函数模拟。

Microsoft Visual Studio* C++ 2010 (SP1) 及更高版本在编译 64 位置代码(使用 /arch:AVX编译器开关)时支持英特尔® AVX(请参阅“更多信息”)。它使用 标头支持内联函数,但是不支持内嵌汇编语言。MASM*、代码的反汇编视图和寄存器的配置程序视图(完全支持 YMM)中都支持英特尔® AVX。

GNU Compiler Collection* (GCC*) 4.4 版通过同一标头 支持英特尔® AVX 内联函数。Binutils 2.20.51.0.1 及更高版本、gdb 6.8.50.20090915 及更高版本、最新版的 GNU 汇编器 (GAS) 以及 objdump 中还提供了其他 GNU 工具链支持。如果您的编译器不支持英特尔® AVX,您可以在许多情况下发出所需的字节,但是一流的支持能为您带来更多方便。

以上提及的三个 C++ 编译器都可以从 C 或 C++ 代码中使用英特尔® AVX 支持同一内联函数运算,从而简化运算。内联函数是编译器使用相应的汇编函数替换的函数。大部分英特尔® AVX 内联函数的命名都遵循以下格式:
 

_mm256_op_suffix(data_type param1, data_type param2, data_type param3)

其中 _mm256是在新的 256 位寄存器上运行的前缀;_op是运算,类似于加法 add或者减法 sub;而 _suffix则表示运算的数据类型,第一个字母表示紧缩 (p)、扩展紧缩 (ep) 或标量 (s)。表 2 中列出了其余字母所代表的类型。

表 2.英特尔® AVX 后缀标记

MarkingMeaning
[s/d]单精度或双精度浮点
[i/u]nnn位大小 nnn为 128、64、32、16 或 8
[ps/pd/sd]紧缩单精度、紧缩双精度或标量双精度
epi32扩展紧缩 32 位带符号整数
si256标量 256 位整数


表 3 中列出了数据类型。前两个参数是源寄存器,第三个参数(显示时)是一个整数掩码、选择因子或偏移值。

表 3.英特尔® AVX 内联函数数据类型

TypeMeaning
__m256256 位,作为 8 个单精度浮点值,表示一个 YMM 寄存器或内存位置
__m256d256 位,作为 4 个双精度浮点值,表示一个 YMM 寄存器或内存位置
__m256i256 位,作为整数、(字节、字等)
__m128128 位单精度浮点(每个 32 位)
__m128d128 位双精度浮点(每个 64 位)


某些内联函数位于其他标头中,如 中的 AES 和 PCLMULQDQ。查看您的编译器文档或者网站,了解各种内联函数的位置。

Microsoft Visual Studio* 2010
为了简单明了,本文中的以下部分将使用带有 SP1 的 Microsoft Visual Studio* 2010,英特尔® 编译器或 GCC* 上运行的代码与之类似。如果您依次点击 Project Properties > Configuration > Code Generation ,在Enable Enhanced Instruction Set 下选择Not Set ,然后将 /arch:AVX手动添加至 Command Line 条目下的命令行,带有 SP1 的Microsoft Visual Studio* 2010 将自动生成英特尔® AVX 代码。作为使用内联函数的一个实例,条目 2 提供了基于内联函数的英特尔® AVX 特性检测例程。


条目 2.基于内联函数的特性检测

// get AVX intrinsics
#include <immintrin.h>
// get CPUID capability
#include <intrin.h>

// written for clarity, not conciseness
#define OSXSAVEFlag (1UL<<27)
#define AVXFlag     ((1UL<<28)|OSXSAVEFlag)
#define VAESFlag    ((1UL<<25)|AVXFlag|OSXSAVEFlag)
#define FMAFlag     ((1UL<<12)|AVXFlag|OSXSAVEFlag)
#define CLMULFlag   ((1UL<< 1)|AVXFlag|OSXSAVEFlag)

bool DetectFeature(unsigned int feature)
	{
	int CPUInfo[4], InfoType=1, ECX = 1;
	__cpuidex(CPUInfo, 1, 1);       // read the desired CPUID format
	unsigned int ECX = CPUInfo[2];  // the output of CPUID in the ECX register.
	if ((ECX & feature) != feature) // Missing feature
		return false;
	__int64 val = _xgetbv(0);       // read XFEATURE_ENABLED_MASK register
	if ((val&6) != 6)               // check OS has enabled both XMM and YMM support.
		return false;
	return true;
	}
 

Mandelbrot 实例

为了使用新指令进行展示,使用了普通 C/C++ 代码(检查以确认编译器不会将代码转换为英特尔® AVX 指令!)和新的英特尔® AVX 指令(作为内联函数)来计算 Mandelbrot 集图像,对比它们的性能。Mandelbrot 集是在伪代码(如条目 3 所示)中定义的复杂数字上进行的计算密集型运算。


条目 3.Mandelbrot 伪代码

z,p are complex numbers
for each point p on the complex plane
	z = 0
	for count = 0 to max_iterations
		if abs(z) > 2.0
			break
		z = z*z+p
	set color at p based on count reached

常见的图像是位于长方形 (-2,-1) 至 (1,1) 中复杂平面部分之上的。可以通过多种方式着色(不在此做介绍)。提升最大迭代数,放大至其他部分并确定随着时间的推移,值是否会“消失”。

为了突出 CPU,将方框 (0.29768, 0.48364) 放大至 (0.29778, 0.48354),以多种大小计算计数网格并使用最大迭代 4096。充分着色时,最终的计数网格如图 4 中所示。
 

图 4.Mandelbrot 集 (0.29768, 0.48364) 至 (0.29778, 0.48354),采用最大迭代 4096
 


条目 4 中提供了计算迭代数的基本 C++ 实施。相比于 2 的复杂数字的绝对值替换为了相比于 4.0 的范数,通过删除平方根,速度几乎翻倍。对于所有版本,使用单精度浮点将尽可能多的元素紧缩至 YMM 寄存器,速度将更快,但是在以后进行放大后,与双精度相比,精度将降低。


条目 4.简单的 Mandelbrot C++ 代码

 

// simple code to compute Mandelbrot in C++
#include <complex>
void MandelbrotCPU(float x1, float y1, float x2, float y2,
                   int width, int height, int maxIters, unsigned short * image)
{
	float dx = (x2-x1)/width, dy = (y2-y1)/height;
	for (int j = 0; j < height; ++j)
		for (int i = 0; i < width; ++i)
		{
			complex<float> c (x1+dx*i, y1+dy*j), z(0,0);
			int count = -1;
			while ((++count < maxIters) && (norm(z) < 4.0))
				z = z*z+c;
			*image++ = count;
		}
}

针对性能测试多个版本:条目 4 中的基本版(一个类似 CPU 的版本,通过浮点扩展复杂类型);基于内联函数的 SSE 版本以及条目 5 中显示的基于内联函数的英特尔® AVX 版本。在 128×128、256×256、512×512、1024×1024、2048×2048 和 4096×4096 图像尺寸上对所有版本进行测试。每种实施在对更多工作保留了基本指令集限制的同时,性能可能会有所提升,但是它们应该能代表您可以实现的性能。

英特尔® AVX 版本经过精心设计,尽可能适合 16 个 YMM 寄存器。为了能够帮助追踪您希望如何分配它们,变量使用ymm0ymm15进行命名。当然,编译器根据自身准则分配寄存器,但是为了考虑周到,您可以尝试使用这种方式将所有计算放入寄存器中。(实际上,从反汇编开始,编辑器就不能完美地分配它们,在汇编代码中重构它将是学习英特尔® AVX 的一次不错练习。)


条目 5.英特尔® AVX 内联函数 Mandelbrot 实施

float dx = (x2-x1)/width;
float dy = (y2-y1)/height;
// round up width to next multiple of 8
int roundedWidth = (width+7) & ~7UL;

float constants[] = {dx, dy, x1, y1, 1.0f, 4.0f};
__m256 ymm0 = _mm256_broadcast_ss(constants);   // all dx
__m256 ymm1 = _mm256_broadcast_ss(constants+1); // all dy
__m256 ymm2 = _mm256_broadcast_ss(constants+2); // all x1
__m256 ymm3 = _mm256_broadcast_ss(constants+3); // all y1
__m256 ymm4 = _mm256_broadcast_ss(constants+4); // all 1's (iter increments)
__m256 ymm5 = _mm256_broadcast_ss(constants+5); // all 4's (comparisons)

float incr[8]={0.0f,1.0f,2.0f,3.0f,4.0f,5.0f,6.0f,7.0f}; // used to reset the i position when j increases
__m256 ymm6 = _mm256_xor_ps(ymm0,ymm0); // zero out j counter (ymm0 is just a dummy)

for (int j = 0; j < height; j+=1)
{
	__m256 ymm7  = _mm256_load_ps(incr);  // i counter set to 0,1,2,..,7
	for (int i = 0; i < roundedWidth; i+=8)
	{
		__m256 ymm8 = _mm256_mul_ps(ymm7, ymm0);  // x0 = (i+k)*dx
		ymm8 = _mm256_add_ps(ymm8, ymm2);         // x0 = x1+(i+k)*dx
		__m256 ymm9 = _mm256_mul_ps(ymm6, ymm1);  // y0 = j*dy
		ymm9 = _mm256_add_ps(ymm9, ymm3);         // y0 = y1+j*dy
		__m256 ymm10 = _mm256_xor_ps(ymm0,ymm0);  // zero out iteration counter
		__m256 ymm11 = ymm10, ymm12 = ymm10;        // set initial xi=0, yi=0

		unsigned int test = 0;
		int iter = 0;
		do
		{
			__m256 ymm13 = _mm256_mul_ps(ymm11,ymm11); // xi*xi
			__m256 ymm14 = _mm256_mul_ps(ymm12,ymm12); // yi*yi
			__m256 ymm15 = _mm256_add_ps(ymm13,ymm14); // xi*xi+yi*yi

			// xi*xi+yi*yi < 4 in each slot
			ymm15 = _mm256_cmp_ps(ymm15,ymm5, _CMP_LT_OQ);
			// now ymm15 has all 1s in the non overflowed locations
			test = _mm256_movemask_ps(ymm15)&255;      // lower 8 bits are comparisons
			ymm15 = _mm256_and_ps(ymm15,ymm4);
			// get 1.0f or 0.0f in each field as counters
			// counters for each pixel iteration
			ymm10 = _mm256_add_ps(ymm10,ymm15);

			ymm15 = _mm256_mul_ps(ymm11,ymm12);        // xi*yi
			ymm11 = _mm256_sub_ps(ymm13,ymm14);        // xi*xi-yi*yi
			ymm11 = _mm256_add_ps(ymm11,ymm8);         // xi <- xi*xi-yi*yi+x0 done!
			ymm12 = _mm256_add_ps(ymm15,ymm15);        // 2*xi*yi
			ymm12 = _mm256_add_ps(ymm12,ymm9);         // yi <- 2*xi*yi+y0

			++iter;
		} while ((test != 0) && (iter < maxIters));

		// convert iterations to output values
		__m256i ymm10i = _mm256_cvtps_epi32(ymm10);

		// write only where needed
		int top = (i+7) < width? 8: width&7;
		for (int k = 0; k < top; ++k)
			image[i+k+j*width] = ymm10i.m256i_i16[2*k];

		// next i position - increment each slot by 8
		ymm7 = _mm256_add_ps(ymm7, ymm5);
		ymm7 = _mm256_add_ps(ymm7, ymm5);
	}
	ymm6 = _mm256_add_ps(ymm6,ymm4); // increment j counter
}

所有版本的完全代码和 Microsoft Visual Studio* 2010 (SP1) 项目(包括测试用具)的更多信息,请参阅“更多信息”部分提供的链接。

结果如图 5 和图 6 所示。为了避免太多数字受缚于特定的 CPU 速度,图 5 显示了每个版本相对应的 CPU 版本的性能,展示了算法的一个简单非 SIMD C/C++ 实施。按照预计,英特尔® SSE 版本总共执行 4 次,因为每通道执行 4 像素;而英特尔® AVX 版本和 CPU 版本一样,总共执行 8 次。由于循环、内存访问、非最佳指令顺序和其他因素都需要开销,提高 4 倍和 8 倍应该最佳,因此首次尝试时相当不错。


图 5.不同大小的相对性能


图 6 中的第二个图表显示每毫秒计算的像素对于每单位大小来说是不变的;此外,算法显示从 CPU 到英特尔® SSE 版本的性能提升了四倍,而从英特尔® SSE 到英特尔® AVX 版本的性能提升了两倍。


图 6.不同大小的绝对性能

 

结论

本文提供了新的英特尔® 高级矢量扩展指令集(英特尔® AVX)的中级概述。这些扩展指令集与之前的英特尔® SSE 指令类似,但是提供了更大的寄存器空间并添加了新的指令。Mandelbrot 实例显示了与之前的技术相比所提升的性能比率。有关详细信息,请查看“英特尔® 高级矢量扩展指令集编程指南”(请在“更多信息”部分查找该文件的链接)。

编程快乐!

更多信息

英特尔® 高级矢量扩展指令集编程指南: http://redfort-software.intel.com/en-us/avx/

联邦信息处理标准出版物 197,“发布高级加密标准”:http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf

The IEEE 754-2008 floating-point format standard at http://en.wikipedia.org/wiki/IEEE_754-2008

IEEE 754-2008 浮点格式标准:http://en.wikipedia.org/wiki/IEEE_754-2008

64 位驱动程序浮点支持:http://msdn.microsoft.com/en-us/library/ff545910.aspx

Mandelbrot 集的维基百科条目:http://en.wikipedia.org/wiki/Mandelbrot_set

英特尔® 软件开发仿真器:http://redfort-software.intel.com/en-us/articles/intel-software-development-emulator

下载完整的 Mandelbrot 英特尔® AVX 实施:http://www.lomont.org/

 

 

作者简介

Chris Lomont 是 Cybernet Systems 的一名研发工程师,曾参与过量子计算算法、NASA 图像处理、美国国土安全部门安全软件的开发和计算机取证分析等项目。他拥有普渡大学的博士学位,此外还拥有物理学、数学和计算机科学的学士学位。作为一名游戏程序员,他还短期从事过金融建模、机器人制造和各种咨询工作。他在业余时间喜欢与妻子一起徒步旅行,还爱好看电影、做专题座谈、娱乐性编程、数学研究、研究物理、听音乐和做各种实验。您可以访问他的个人网站:www.lomont.org或者他的电子设备网站:http://www.hypnocube.com/

 

 

附录 A:指令集参考

许多指令都是紧缩或标量形式,即它们在寄存器的多个并行元素或者一个单一元素上执行——标记为 [P/S]。条目长度分为双精度或单精度浮点(简称为双精度单精度),标记为 [D/S];整数形式分为字节、字、双字和四字,标记为 [B/W/D/Q]。整数形式有时还分为带符号形式和无符号形式,标记为 [S/U] 。有些指令在寄存器的高区或低区运行,标记为 [H/L] ;下文的表格中提供了其他可选元素。英特尔® SSE 形式和英特尔® AVX 形式中的指令以英特尔® AVX 形式的 (V) 为前缀,支持 3 操作数和 256 位寄存器。方括号 ( []) 中的条目为必选;圆括号 ( ()) 中的条目为可选。

范例:

 

  • (V)ADD[P/S][D/S]是紧缩或标量、双精度或单精度相加,有 8 种可能的形式——VADDPD, VADDPS, VADDSD, VADDSS, 和不以 V开头的版本。
  • (V)[MIN/MAX][P/S][D/S]表示最大或最小双精度或单精度紧缩或标量的 16 种不同指令。

下一表格列出了多个比较类型。VEX 前缀的指令具有 32 个比较类型;非 VEX 前缀的比较仅支持圆括号中的 8 个类型。每个比较类型分为多种形式,其中O = 有序的、U = 无序的、S = 发信号、Q = 不发信号。有序/无序表示如果一个操作数是 NaN(浮点中的非数),比较是 false 还是 true,在计算过程中出现某些错误时会发生该情形,如被 0 除或者负数的平方根。发信号/不发信号表示至少一个操作数是 QnaN(用于错误捕获的静态非数)时是否会出现一个异常。

 

 

 

TypeFlavorsMeaning
EQ(OQ), UQ, OS, US等于
LT (OS), OQ小于
LE (OS), OQ小于等于
UNORD (Q), S无序测试 (NaN)
NEQ (UQ), US, OQ, OS不等于
NLT (US), UQ不小于
NLE (US), UQ不小于等于
ORD (Q), S有序测试 (非 NaN)
NGE US, UQ不大于等于
NGT US, UQ不大于
FALSE OQ, OS比较一直是 false
GE OS, OQ大于等于
GT OS, OQ大于
TRUEUQ, US比较一直是 true


最后,我们在此提供了所有英特尔® AVX 指令:

 

 

 

 

ArithmeticDescription
(V)[ADD/SUB/MUL/DIV][P/S][D/S]加/减/乘/除紧缩/标量双精度/单精度
(V)ADDSUBP[D/S]紧缩双精度/单精度加减交互指数
(V)DPP[D/S]点积,基于即时任务
(V)HADDP[D/S]水平相加
(V)[MIN/MAX][P/S][D/S]最小/最大紧缩/标量双精度/单精度
(V)MOVMSKP[D/S]双精度/单精度符号掩码开方
(V)PMOVMSKB生成包括大部分重要位的掩码
(V)MPSADBW多个绝对差值求和
(V)PABS[B/W/D]字节/字/双字上的紧缩绝对值
(V)P[ADD/SUB][B/W/D/Q]加/减紧缩字节/字/双字/四字
(V)PADD[S/U]S[B/W]紧缩带符号/无符号加饱和字节/字
(V)PAVG[B/W]平均紧缩字节/字
(V)PCLMULQDQCarry-less 乘法四字
(V)PH[ADD/SUB][W/D]紧缩垂直加/减字/双字
(V)PH[ADD/SUB]SW紧缩垂直加/减饱和
(V)PHMINPOSUW最小垂直无符号字和位置
(V)PMADDWD乘加紧缩整数
(V)PMADDUBSW无符号字节和带符号字节乘以带符号字
(V)P[MIN/MAX][S/U][B/W/D]最小/最大紧缩带符号/无符号整数
(V)PMUL[H/L][S/U]W乘以紧缩带符号/无符号整数和存储高区/低区结果
(V)PMULHRSW使用约数和移位乘以紧缩无符号
(V)PMULHW乘以紧缩整数和存储高区结果
(V)PMULL[W/D]乘以紧缩整数和存储低区结果
(V)PMUL(U)DQ乘以紧缩带符号/无符号双字整数和存储四字
(V)PSADBW计算无符号字节绝对差值总和
(V)PSIGN[B/W/D]根据其他操作数上的符号更改一个操作数中每个元素的符号
(V)PS[L/R]LDQ操作数中字节左/右移位量
(V)SL[L/AR/LR][W/D/Q]位左移/算法右移/逻辑右移
(V)PSUB(U)S[B/W]紧缩带符号/无符号减去带符号/无符号饱和
(V)RCP[P/S]S计算紧缩/标量单精度的近似倒数
(V)RSQRT[P/S]S计算紧缩/标量单精度平方根的近似倒数
(V)ROUND[P/S][D/S]紧缩/标量双精度/单精度的约数
(V)SQRT[P/S][D/S]紧缩/标量双精度/单精度的平方根
VZERO[ALL/UPPER]将 YMM 寄存器的全部/上层归零

 

 

 

ComparisonDescription
(V)CMP[P/S][D/S]比较紧缩/标量双精度/单精度
(V)COMIS[S/D]比较标量双精度/单精度,设置 EFLAGS
(V)PCMP[EQ/GT][B/W/D/Q]比较紧缩整数等于/大于
(V)PCMP[E/I]STR[I/M]比较显式/隐式长度字符串,返回指数/掩码

 

 

ControlDescription
V[LD/ST]MXCSR加载/存储 MXCSR 控制/状态寄存器
XSAVEOPT保存优化的处理器延伸状态

 

 

ConversionDescription
(V)CVTx2y将类型 x转换为类型 y,其中 xy从以下选择:
DQ 和 P[D/S],
[P/S]S 和[P/S]D, or
S[D/S] 和 SI.

 

 

Load/storeDescription
VBROADCAST[SS/SD/F128]通过传播进行加载(将单个数值加载到多个位置)
VEXTRACTF128128 位浮点值开方
(V)EXTRACTPS紧缩单精度开方
VINSERTF128插入紧缩浮点值
(V)INSERTPS插入紧缩单精度值
(V)PINSR[B/W/D/Q]插入整数
(V)LDDQU移动四倍未对齐整数
(V)MASKMOVDQU使用非暂时提示NT Hint存储双四字中的指定字节
VMASKMOVP[D/S]有条件的 SIMD 紧缩加载/存储
(V)MOV[A/U]P[D/S]移动对齐/未对齐的紧缩双精度/单精度
(V)MOV[D/Q]移动双字/四字
(V)MOVDQ[A/U]移动对齐/未对齐的双字至四字
(V)MOV[HL/LH]P[D/S]移动高区到低区/低区到高区的紧缩双精度/单精度
(V)MOV[H/L]P[D/S]移动高区/低区的紧缩双精度/单精度
(V)MOVNT[DQ/PD/PS]使用非暂时提示移动紧缩整数/双精度/单精度
(V)MOVNTDQA使用对齐的非暂时提示移动紧缩整数
(V)MOVS[D/S]移动或合并标量双精度/单精度
(V)MOVS[H/L]DUP移动单个奇/偶指数的单精度
(V)PACK[U/S]SW[B/W]在字节/字上对无符号/带符号饱和进行紧缩
(V)PALIGNR字节对齐
(V)PEXTR[B/W/D/Q]整数开方
(V)PMOV[S/Z]X[B/W/D][W/D/Q]使用带符号/零扩展紧缩移动(仅根据长度,不支持 DD、DW 等)

 

 

LogicalDescription
(V)[AND/ANDN/OR]P[D/S]紧缩双精度/单精度值的位逻辑 AND/AND NOT/OR
(V)PAND(N)逻辑AND (NOT)
(V)P[OR/XOR]位逻辑 logical OR/exclusive OR
(V)PTEST紧缩位测试,如果位逻辑 AND为 all 0,则设置零标记
(V)UCOMIS[D/S]无序比较标量双精度/单精度并设置EFLAGS
(V)XORP[D/S]紧缩双精度/单精度的位逻辑 XOR

 

 

ShuffleDescription
(V)BLENDP[D/S]混合紧缩双精度/单精度,基于掩码选择元素
(V)BLENDVP[D/S]混合值
(V)MOVDDUP向所有值中复制偶数值
(V)PBLENDVB变量混合紧缩字节
(V)PBLENDW混合紧缩字
VPERMILP[D/S]转换双精度/单精度值
VPERM2F128转换浮点值
(V)PSHUF[B/D]根据即时值移动紧缩字节/双字
(V)PSHUF[H/L]W移动紧缩高区/低区字
(V)PUNPCK[H/L][BW/WD/DQ/QDQ]解紧缩高区/低区数据
(V)SHUFP[D/S]移动紧缩双精度/单精度
(V)UNPCK[H/L]P[D/S]解紧缩和交错紧缩变量双精度/单精度值

 

 

AESDescription
AESENC/AESENCLAST执行一轮 AES 加密
AESDEC/AESDECLAST执行一轮 AES 解密
AESIMC执行 AES InvMixColumn变换
AESKEYGENASSISTAES 次轮密钥生成协助

 

 

Future InstructionsDescription
[RD/WR][F/G]SBASE读/写 FS/GS 寄存器
RDRAND读取随机数(至 r16、r32、r64)
VCVTPH2PS将 16 位浮点值转换为单精度浮点值
VCVTPS2PH将单精度值转换为 16 位浮点值

 

 

FMAEach [z] is the string 132 or 213 or 231, giving the order the operands A,B,C are used in:
132 is A=AC+B
213 is A=AB+C
231 is A=BC+A
VFMADD[z][P/S][D/S]面向双精度/单精度紧缩/标量的融合乘加 A = r1 * r2 + r3
VFMADDSUB[z]P[D/S]融合乘交互加/减紧缩双精度/单精度 A = r1 * r2 + r3(奇指数),A = r1 * r2-r3(偶指数)
VFMSUBADD[z]P[D/S]融合乘交互减/加紧缩双精度/单精度 A = r1 * r2 - r3(奇指数),A = r1 * r2+r3(偶指数)
VFMSUB[z][P/S][D/S]面向双精度/单精度值紧缩/标量的融合乘减 A = r1 * r2 - r3
VFNMADD[z][P/S][D/S]紧缩/标量双精度/单精度的融合负数乘加 A = -r1 * r2+r3
VFNMSUB[z][P/S][D/S]紧缩/标量双精度/单精度的融合负数乘减 A = -r1 * r2-r3

 

如欲了解有关编译器优化的更多完整信息请参阅我们的优化声明

英特尔® MKL-DNN:第一部分 – 库的概述和安装

$
0
0

简介

目前,在大型数据集、高度并行化的处理能力和增强设备智能性的需求的推动下,深度学习成为了计算机科学领域最受关注的热点话题之一。根据维基百科的描述,深度学习是机器学习 (ML) 的一个子集,由模拟高级别数据抽象的算法构成。如图 1 所示,机器学习是人工智能 (AI) 的一个子集,人工智能的研究范围非常广泛,其目标是开发模拟人类智能的计算机系统。


图 1.深度学习和人工智能的关系。

英特尔积极致力于深度学习领域的研究,为了充分利用英特尔® 架构 (IA) 而优化了常用的框架(如 Caffe* 和 Theano*),创建了高级别工具(如面向数据科学家的英特尔® 深度学习 SDK),还为开发人员社区提供了强大的软件库,如英特尔® 数据分析加速库(英特尔® DAAL)面向深度神经网络的英特尔® 数学核心函数库(英特尔® MKL-DNN)

英特尔 MKL-DNN 是一个开源的性能增强库,能够提高在英特尔架构上运行的深度学习框架的速度。对深度学习感兴趣的软件开发人员可能听说过英特尔 MKL-DNN,但是有可能未曾使用过它。

英特尔 MKL-DNN 教程系列的开发人员简介从开发人员的角度介绍了英特尔 MKL-DNN。第一部分提供了丰富的资源,详细介绍了如何安装和构建库组件。教程系列的第二部分介绍了如何配置 Eclipse* 集成开发环境,以创建 C++ 代码示例,还提供了源代码演示。

英特尔® MKL-DNN 概述

如图 2 所示,英特尔 MKL-DNN 专为在英特尔架构上加快深度学习框架的速度而设计,包含了高度矢量化和线程化的构建模块,支持利用 C 和 C++ 接口实施卷积神经网络。


图 2.英特尔架构上的深度学习框架。

英特尔 MKL-DNN 主要在以下对象上运行:基元、引擎和流。库文档对这些对象的定义如下所示:

  • 基元 - 任何操作,包括卷积、数据格式重新排序和内存。基元可以以其他基元为输入,但是智能输出内存基元。
  • 引擎 - 一种执行设备,如 CPU。每个基元都映射为特定的引擎。
  • - 一种执行环境,将基元提交至流后等待完成。提交至流的基元可能有不同的引擎。流对象也可以跟踪基元间的相关性。

典型的工作流程创建一系列基元,将基元传输至流进行处理,并等待处理的完成。如欲获取关于编程模式的更多信息,请查看英特尔 MKL-DNN 文档

资源

您可以通过互联网获取关于英特尔 MKL-DNN 的实用资源,了解英特尔 MKL-DNN 的功能和限制以及将它集成至深度学习项目所带来的预期效果。

GitHub 存储库

英特尔 MKL-DNN 是一款面向深度学习应用的开源性能库,库中包含一些构建模块,用于实施带 C 和 C++ 接口的卷积神经网络 (CNN),可以在 GitHub* 上免费下载。

关于 GitHub 网站需要注意的是,尽管英特尔 MKL-DNN 和英特尔® 数学核心函数库(英特尔® MKL)2017的功能相似,但是前者不兼容 API。在撰写本文时,英特尔 MKL-DNN 发布了技术预览版,添加了加快图像识别拓扑(如 AlexNet* 和 VGG*)速度所需的功能。

英特尔开源技术中心

MKL-DNN|01.org项目微型网站是英特尔开源技术中心的成员,该技术中心的名称为 01.org,参加过各种开源项目的英特尔工程师都参与到该社区当中来。该网站包含英特尔 MKL-DNN 项目概述,如何参加项目与实施项目的信息,还提供一篇内容丰富的博文“面向神经网络的英特尔® 数学核心函数库(英特尔® MKL-DNN)简介”(作者:Kent Moffat)。

安装英特尔 MKL-DNN

本章节详细介绍了安装和构建英特尔 MKL-DNN 库组件的逐步说明,进一步解释了 GitHub 存储库网站上提供的安装信息。您的电脑必须配备支持英特尔® 高级矢量扩展指令集 2(英特尔® AVX2)的英特尔® 处理器。英特尔 MKL-DNN 专门面向英特尔® 至强® 处理器和英特尔® 至强融核™ 处理器优化。

GitHub 指出软件在 RedHat* Enterprise Linux* 7 上验证,但是本教程提供的信息是关于在运行 Ubuntu* 16.04的系统上进行开发。

安装关联组件

英特尔 MKL-DNN 包含以下关联组件:

  • CMake* – 一款用于创建、测试和打包软件的跨平台工具。
  • Doxygen* – 一款从自动源代码中生成文档的工具。

如果您的电脑没有安装这些软件工具,可以输入以下命令进行安装:

sudo apt install cmake

sudo apt install doxygen

下载并创建源代码

打开终端并输入以下命令,便可从 GitHub 存储库复制英特尔 MKL-DNN 库:

git clone https://github.com/01org/mkl-dnn.git

注:如果您的电脑没有安装 Git*,可以输入以下命令进行安装:

sudo apt install git

安装完成后,您会在 Home 目录下发现一个 mkl-dnn目录。输入以下命令转至目录:

cd mkl-dnn

GitHub 存储库网站是这样说明的:英特尔 MKL-DNN 使用了英特尔 MKL 中的经过优化的一般矩阵乘法 (GEMM) 函数。存储库还包含支持这个功能的库,运行 scripts目录中的 prepare_mkl.sh脚本进行下载:

cd scripts && ./prepare_mkl.sh && cd ..

这个脚本创建了一个名为 external的目录,然后下载并提取库文件至 mkl-dnn/external/mklml_lnx*目录。

mkl-dnn目录中执行下一条命令,通过命令创建了一个 build子目录,并运行 CMakeMake,以生成构建系统:

mkdir -p build && cd build && cmake ..&& make

验证构建

为了验证构建,从 mkl-dnn/build目录中执行以下命令:

make test

这个步骤执行了一系列验证构建的单元测试。全部测试均应显示通过 (Passed),处理时间如图 3 所示。


图 3.测试结果。

库文档

可以在线获取英特尔 MKL-DNN 文档。从 mkl-dnn/build目录中执行以下命令,也可以在系统中本地生成这个文档:

make doc

完成安装

执行 mkl-dnn/build目录中的以下命令,完成英特尔 MKL-DNN 的安装:

sudo make install

以下命令将在 /usr/local目录中安装库和其他组件,这些组件是开发面向英特尔 MKL-DNN 的应用所必需的:

共享库 (/usr/local/lib):

  • libiomp5.so
  • libmkldnn.so
  • libmklml_intel.so

标头文件 (/usr/local/include):

  • mkldnn.h
  • mkldnn.hpp
  • mkldnn_types.h

文档 (/usr/local/share/doc/mkldnn):

  • 英特尔许可和版权声明
  • 构成 HTML 文档的各种文件(在 /reference/html之下)

在命令行上创建代码示例

GitHub 存储库包含 C 和 C++ 代码示例,展示了如何创建包含卷积、修正线性单元、本地响应标准化和池化的神经网络拓扑模块。以下章节介绍了如何在 Linux 中利用命令行创建代码示例。教程系列的第二部分展示了如何面向创建和扩展 C++ 代码示例而配置 Eclipse 集成开发环境。

C++ 示例命令行构建 (G++)

为了创建包含在英特尔 MKL-DNN 存储库中的 C++ 示例程序 (simple_net.cpp),首先转至 examples目录:

cd ~/mkl-dnn/examples

然后,为可执行代码创建一个目标目录:

mkdir –p bin

通过连接英特尔 MKL-DNN 共享库和指定以下输出目录,创建 simple_net.cpp 示例:

g++ -std=c++11 simple_net.cpp –lmkldnn –o bin/simple_net_cpp


图 4.使用 G++ 的 C++ 命令行构建。

转至 bin目录并运行可执行代码:

cd bin

./simple_net_cpp

使用 GCC 的 C 示例命令行构建

为了创建包含在英特尔 MKL-DNN 存储库中的 C 示例应用 (simple_net.c),首先转至 examples目录:

cd ~/mkl-dnn/examples

然后,为可执行代码创建一个目标目录:

mkdir –p bin

通过连接英特尔 MKL-DNN 共享库和指定以下输出目录,创建 simple_net.c 示例:

gcc –Wall –o bin/simple_net_c simple_net.c -lmkldnn


图 5.利用 GCC 的 C 命令行构建。

转至 bin目录并运行可执行代码:

cd bin

./simple_net_c

完成后,C 应用将向终端输出通过 (passed) 或失败 (failed)。

后续步骤

此时您已经成功安装了英特尔 MKL-DNN 库,执行了单元测试,并且创建了存储库所提供的示例程序。英特尔 MKL-DNN 开发人员简介的第二部分介绍了如何配置 Eclipse 集成开发环境,以创建 C++ 代码示例,还提供了代码演示。

英特尔® MKL-DNN:第二部分 – 代码示例创建与详解

$
0
0

简介

第一部分,我们介绍了面向深度神经网络的英特尔® 数学核心函数库(英特尔® MKL-DNN),MKL-DNN 是一款面向深度学习应用的开源性能库。提供了在配有英特尔处理器的电脑上安装库组件的具体步骤,要求处理器支持英特尔® 高级矢量扩展指令集 2(英特尔® AVX2)并运行 Ubuntu* 操作系统。第一部分还包括从命令行中创建 C 和 C++ 代码示例的详细信息。

第二部分将介绍如何配置集成开发环境 (IDE),以创建 C++ 代码示例,并提供基于 AlexNet* 深度学习拓扑的代码详解。本教程使用的是安装了 C/C++ 开发工具 (CDT)Eclipse Neon* IDE。(如果您的系统没有安装 Eclipse*,可以按照 Ubuntu 手册网站上的说明指定 Oracle Java* 8 和 Eclipse IDE,以便 C/C++ 开发人员选择。)

在 Eclipse IDE 中创建 C++ 示例

本章节介绍了如何在 Eclipse 中创建一个新项目,以及如何导入英特尔 MKL-DNN C++ 示例代码。

在 Eclipse 中创建一个新项目:

  • 打开 Eclipse。
  • 单击位于屏幕左上角的 New
  • Select a wizard屏幕中选择 C++ Project 并单击 Next(图 1)。


图 1.在 Eclipse 中创建一个新的 C++ 项目。

  • 输入项目名称 simple_net。项目类别请选择 Executable, Empty Project。工具链请选择 Linux GCC。单击 Next
  • Select Configurations屏幕中单击 Advanced Settings

面向项目启用 C++11:

  • Properties屏幕中展开菜单树中的 C/C++ Build选项并选择 Settings
  • Tool Settings选项卡中选择 GCC C++ Compiler,然后选择 Miscellaneous
  • Other flags框中已有字符串的后面添加 -std=c++11,用空格隔开(图 2)。


图 2.为项目启用 C++11(二中取一)。

  • Properties屏幕上展开 C/C++ General并选择 Preprocessor Include Paths, Macros etc。
  • 选择 Providers选项卡,然后选择您正在使用的编译器(如 CDT GCC 内置编译器设置)。
  • 找到 Command to get compiler specs:字段并添加 -std=c++11。完成后,命令与以下部分相似:
    “${COMMAND} ${FLAGS} -E -P -v -dD “${INPUTS}” -std=c++11”.
  • 单击Apply后单击 OK(图 3)。


图 3.为项目启用 C++11(二中取二)。

将库添加至链接器设置:

  • Properties屏幕中展开菜单树中的 C/C++ Build选项,然后选择 Settings
  • Tool Settings选项卡中选择 GCC C++ Linker,然后选择 Libraries
  • Libraries (l)板块下单击 Add
  • 输入 mkldnn并单击 OK(图 4)。


图 4.将库添加至链接器设置

完成项目创建:

  • Properties屏幕底部单击 OK
  • C++ Project屏幕底部单击 Finish

添加 C++ 源文件(注:此时 simple_net项目应该出现在项目资源管理器中):

  • 在项目资源管理器中右键单击项目名称,并选择 New, Source Folder。输入文件夹名称 src并单击 Finish
  • 在项目资源管理器中右键单击 src文件夹并选择 Import…
  • Import屏幕中展开 General文件夹并突出显示 File System。单击 Next
  • File System屏幕中单击 From directory字段旁的 Browse按钮。转至包含英特尔 MKL-DNN 示例文件的地址,在本例中,地址为 /mkl-dnn/examples。单击屏幕底部的 OK
  • 返回 File System屏幕,查看 simple_net.cpp框并单击 Finish

创建 Simple_Net 项目:

  • 项目资源管理器中右键单击 simple_net项目名称。
  • 单击 Build Project并验证未出现错误。

Simple_Net 代码示例

虽然 Simple_Net 不是功能齐全的深度学习框架,但是它为神经网络拓扑模块的创建提供了基础,将模块所包含的卷积、修正线性单元 (ReLU)、本地响应标准化 (LRN) 和池化集成至单个可执行项目中。虽然此文档逐步概述了英特尔 MKL-DNN C++ API,但是 Simple_Net 代码示例提供了基于 AlexNet 拓扑的更完整的详解。因此,首先向您简要介绍 AlexNet 架构。

AlexNet 架构

“利用深度卷积神经网络进行 ImageNet 分类”文章中这样描述,AlexNet 架构包含一张输入图像 (L0) 和 8 个经过学习的层(L1 到 L8)—5 个卷积层和 3 个完全连接层。图 5 以图形方式描述了该拓扑。


图 5.AlexNet 拓扑(来源:麻省理工学院*)。

表 1 提供了关于 AlexNet 架构的其他详细信息:

类型

描述

L0

输入图像

尺寸:227 x 227 x 3(在图中显示为 227 x 227 x 3)

L1

卷积

尺寸:55* x 55 x 96

  • 96 个过滤器,尺寸为 11 × 11
  • 步长 4
  • 填充 0

*尺寸= (N - F)/S + 1 = (227 - 11)/4 + 1 = 55

-

最大池化

尺寸:27* x 27 x 96

  • 96 个过滤器,尺寸为 3 × 3
  • 步长 2

*尺寸= (N - F)/S + 1 = (55 – 3)/2 + 1 = 27

L2

卷积

尺寸:27 x 27 x 256

  • 256 个过滤器,尺寸为 5 x 5
  • 步长 1
  • 填充 2

-

最大池化

尺寸:13* x 13 x 256

  • 256 个过滤器,尺寸为 3 × 3
  • 步长 2

*尺寸= (N - F)/S + 1 = (27 - 3)/2 + 1 = 13

L3

卷积

尺寸:13 x 13 x 384

  • 384 个过滤器,尺寸为 3 × 3
  • 步长 1
  • 填充 1

L4

卷积

尺寸:13 x 13 x 384

  • 384 个过滤器,尺寸为 3 × 3
  • 步长 1
  • 填充 1

L5

卷积

尺寸:13 x 13 x 256

  • 256 个过滤器,尺寸为 3 × 3
  • 步长 1
  • 填充 1

-

最大池化

尺寸:6* x 6 x 256

  • 256 个过滤器,尺寸为 3 × 3
  • 步长 2

*尺寸 = (N - F)/S + 1 = (13 - 3)/2 + 1 = 6

L6

完全连接

4096 个神经元

L7

完全连接

4096 个神经元

L8

完全连接

1000 个神经元

表 1.AlexNet 层描述。

本教程不会对卷积神经网络和 AlexNet 拓扑进行详细描述,但是如果读者想要获取更多的实用信息,可以访问以下链接。

Simple_Net 代码详解

如下所示的源代码在本质上和存储库中的 Simple_Net 示例相同,但是前者进行了重构,能够使用完全合格的英特尔 MKL-DNN 类别来增强可读性。本代码实施了拓扑的第一层 (L1)。

  1. 将 include 指令添加至库标头文件:
    	#include "mkldnn.hpp"
  2. 将 CPU 引擎初始化为索引 0:
    	auto cpu_engine = mkldnn::engine(mkldnn::engine::cpu, 0);
  3. 分配数据并创建张量结构:
    	const uint32_t batch = 256;
    	std::vector<float> net_src(batch * 3 * 227 * 227);
    	std::vector<float> net_dst(batch * 96 * 27 * 27);
    
    	/* AlexNet: conv
    	 * {batch, 3, 227, 227} (x) {96, 3, 11, 11} -> {batch, 96, 55, 55}
    	 * strides:{4, 4}
    	 */
    	mkldnn::memory::dims conv_src_tz = {batch, 3, 227, 227};
    	mkldnn::memory::dims conv_weights_tz = {96, 3, 11, 11};
    	mkldnn::memory::dims conv_bias_tz = {96};
    	mkldnn::memory::dims conv_dst_tz = {batch, 96, 55, 55};
    	mkldnn::memory::dims conv_strides = {4, 4};
    	auto conv_padding = {0, 0};
    
    	std::vector<float> conv_weights(std::accumulate(conv_weights_tz.begin(),
    		conv_weights_tz.end(), 1, std::multiplies<uint32_t>()));
    
    	std::vector<float> conv_bias(std::accumulate(conv_bias_tz.begin(),
    		conv_bias_tz.end(), 1, std::multiplies<uint32_t>()));
  4. 面向用户数据创建内存:
    	auto conv_user_src_memory = mkldnn::memory({{{conv_src_tz},
    		mkldnn::memory::data_type::f32,
    		mkldnn::memory::format::nchw}, cpu_engine}, net_src.data());
    
    	auto conv_user_weights_memory = mkldnn::memory({{{conv_weights_tz},
    		mkldnn::memory::data_type::f32, mkldnn::memory::format::oihw},
    		cpu_engine}, conv_weights.data());
    
    	auto conv_user_bias_memory = mkldnn::memory({{{conv_bias_tz},
    		mkldnn::memory::data_type::f32, mkldnn::memory::format::x}, cpu_engine},
    	    conv_bias.data());
    	
  5. 利用面向卷积数据格式的通配符 any创建面向卷积数据的内存描述符(支持卷积数据基元选择最适合输入参数的数据格式—内核尺寸、步长、填充等):
    	auto conv_src_md = mkldnn::memory::desc({conv_src_tz},
    		mkldnn::memory::data_type::f32,
    		mkldnn::memory::format::any);
    
    	auto conv_bias_md = mkldnn::memory::desc({conv_bias_tz},
    		mkldnn::memory::data_type::f32,
    		mkldnn::memory::format::any);
    
    	auto conv_weights_md = mkldnn::memory::desc({conv_weights_tz},
    		mkldnn::memory::data_type::f32, mkldnn::memory::format::any);
    
    	auto conv_dst_md = mkldnn::memory::desc({conv_dst_tz},
    		mkldnn::memory::data_type::f32,
    		mkldnn::memory::format::any);
    	
  6. 通过指定算法、传播类型、输入形状、权重、偏差、输出和卷积步长、填充以及填充类型,创建一个卷积描述符:
    	auto conv_desc = mkldnn::convolution_forward::desc(mkldnn::prop_kind::forward,
    		mkldnn::convolution_direct, conv_src_md, conv_weights_md, conv_bias_md,
    		conv_dst_md, conv_strides, conv_padding, conv_padding,
    		mkldnn::padding_kind::zero);
  7. 创建一个卷积基元描述符。创建完成后,这个描述符有特定的格式,而非在卷积描述符中-规定的任意通配符格式:
    	auto conv_prim_desc =
    		mkldnn::convolution_forward::primitive_desc(conv_desc, cpu_engine);
  8. 创建代表网络的基元矢量:
    	std::vector<mkldnn::primitive> net;
  9. 如需要,在用户和数据之间创建重新排序,并在卷积之前添加至网络:
    	auto conv_src_memory = conv_user_src_memory;
    	if (mkldnn::memory::primitive_desc(conv_prim_desc.src_primitive_desc()) !=
    	conv_user_src_memory.get_primitive_desc()) {
    
    		conv_src_memory = mkldnn::memory(conv_prim_desc.src_primitive_desc());
    
    		net.push_back(mkldnn::reorder(conv_user_src_memory, conv_src_memory));
    	}
    
    	auto conv_weights_memory = conv_user_weights_memory;
    	if (mkldnn::memory::primitive_desc(conv_prim_desc.weights_primitive_desc()) !=
    			conv_user_weights_memory.get_primitive_desc()) {
    
    		conv_weights_memory =
    			mkldnn::memory(conv_prim_desc.weights_primitive_desc());
    
    		net.push_back(mkldnn::reorder(conv_user_weights_memory,
    			conv_weights_memory));
    	}
    
    	auto conv_dst_memory = mkldnn::memory(conv_prim_desc.dst_primitive_desc());
    	
  10. 创建卷积基元并添加至网络:
    	net.push_back(mkldnn::convolution_forward(conv_prim_desc, conv_src_memory,
    		conv_weights_memory, conv_user_bias_memory, conv_dst_memory));
  11. 创建 ReLU 基元并添加至网络:
    	/* AlexNet: relu
    	 * {batch, 96, 55, 55} -> {batch, 96, 55, 55}
    	 */
    	const double negative_slope = 1.0;
    	auto relu_dst_memory = mkldnn::memory(conv_prim_desc.dst_primitive_desc());
    
    	auto relu_desc = mkldnn::relu_forward::desc(mkldnn::prop_kind::forward,
    	conv_prim_desc.dst_primitive_desc().desc(), negative_slope);
    
    	auto relu_prim_desc = mkldnn::relu_forward::primitive_desc(relu_desc, cpu_engine);
    
    	net.push_back(mkldnn::relu_forward(relu_prim_desc, conv_dst_memory,
    	relu_dst_memory));
    	
  12. 创建一个 AlexNet LRN 基元:
    	/* AlexNet: lrn
    	 * {batch, 96, 55, 55} -> {batch, 96, 55, 55}
    	 * local size:5
    	 * alpha:0.0001
    	 * beta:0.75
    	 */
    	const uint32_t local_size = 5;
    	const double alpha = 0.0001;
    	const double beta = 0.75;
    
    	auto lrn_dst_memory = mkldnn::memory(relu_dst_memory.get_primitive_desc());
    
    	/* create lrn scratch memory from lrn src */
    	auto lrn_scratch_memory = mkldnn::memory(lrn_dst_memory.get_primitive_desc());
    
    	/* create lrn primitive and add it to net */
    	auto lrn_desc = mkldnn::lrn_forward::desc(mkldnn::prop_kind::forward,
    		mkldnn::lrn_across_channels,
    	conv_prim_desc.dst_primitive_desc().desc(), local_size,
    		alpha, beta);
    
    	auto lrn_prim_desc = mkldnn::lrn_forward::primitive_desc(lrn_desc, cpu_engine);
    
    	net.push_back(mkldnn::lrn_forward(lrn_prim_desc, relu_dst_memory,
    	lrn_scratch_memory, lrn_dst_memory));
    	
  13. 创建一个 AlexNet 池化基元:
    	/* AlexNet: pool
    	* {batch, 96, 55, 55} -> {batch, 96, 27, 27}
    	* kernel:{3, 3}
    	* strides:{2, 2}
    	*/
    	mkldnn::memory::dims pool_dst_tz = {batch, 96, 27, 27};
    	mkldnn::memory::dims pool_kernel = {3, 3};
    	mkldnn::memory::dims pool_strides = {2, 2};
    	auto pool_padding = {0, 0};
    
    	auto pool_user_dst_memory = mkldnn::memory({{{pool_dst_tz},
    		mkldnn::memory::data_type::f32,
    		mkldnn::memory::format::nchw}, cpu_engine}, net_dst.data());
    
    	auto pool_dst_md = mkldnn::memory::desc({pool_dst_tz},
    			mkldnn::memory::data_type::f32,
    		mkldnn::memory::format::any);
    
    	auto pool_desc = mkldnn::pooling_forward::desc(mkldnn::prop_kind::forward,
    		mkldnn::pooling_max, lrn_dst_memory.get_primitive_desc().desc(), pool_dst_md, pool_strides, pool_kernel, pool_padding, pool_padding,mkldnn::padding_kind::zero);
    
    	auto pool_pd = mkldnn::pooling_forward::primitive_desc(pool_desc, cpu_engine);
    	auto pool_dst_memory = pool_user_dst_memory;
    
    	if (mkldnn::memory::primitive_desc(pool_pd.dst_primitive_desc()) !=
    			pool_user_dst_memory.get_primitive_desc()) {
    		pool_dst_memory = mkldnn::memory(pool_pd.dst_primitive_desc());
    	}
    	
  14. 从池化 dst 中创建池化指数内存:
    	auto pool_indices_memory =
    		mkldnn::memory(pool_dst_memory.get_primitive_desc());
  15. 创建池化基元并添加至网络:
    	net.push_back(mkldnn::pooling_forward(pool_pd, lrn_dst_memory,
    		pool_indices_memory, pool_dst_memory));
  16. 如需要,在内部和用户数据之间创建重新排序,在池化后添加至网络:
    	if (pool_dst_memory != pool_user_dst_memory) {
        	net.push_back(mkldnn::reorder(pool_dst_memory, pool_user_dst_memory));
    	}
  17. 创建流,提交全部基元并等待完成:
    	mkldnn::stream(mkldnn::stream::kind::eager).submit(net).wait();
  18. simple_net()函数包含上述代码,在 main中利用异常处理调用该函数:
    	int main(int argc, char **argv) {
    	    try {
    	        simple_net();
    	    }
    	    catch(mkldnn::error& e) {
    	        std::cerr << "status:"<< e.status << std::endl;
    	        std::cerr << "message:"<< e.message << std::endl;
    	    }
    	    return 0;
    	}

结论

本教程系列的第一部分提供了几个资源,您可以利用这些资源了解英特尔 MKL-DNN 的技术预览版。还详细说明了如何安装和构建库组件。本文(教程系列的第二部分)介绍了如何配置 Eclipse 集成开发环境,以创建 C++ 代码示例,还提供了基于 AlexNet 深度学习拓扑的代码详解。敬请关注即将上市的英特尔 MKL-DNN 生产发行版。

如何在英特尔® 至强融核™ 处理器中使用 MPI-3 共享内存

$
0
0

本白皮书简要介绍了 MPI-3 共享内存的特性、相应的 API 和一个示例程序,以展示如何在英特尔® 至强融核™ 处理器中使用 MPI-3 共享内存。

MPI-3 共享内存简介

MPI-3 共享内存是消息传递接口 (MPI) 标准版 3.0 的一个特性,实施于英特尔® MPI 库 5.0.2 版及更高版本。MPI-3 共享内存支持多个 MPI 进程,以分配并访问计算节点内的共享内存。对于需要多个 MPI 进程的应用(以交换海量本地数据)而言,该特性减少了内存空间,能够显著提升性能。

在 MPI 标准中,每个 MPI 进程都有各自的地址空间。借助 MPI-3 共享内存,每个 MPI 进程都面向其它进程公开了内存。下图展示了共享内存的概念:每个 MPI 进程分配并维持自己的本地内存,并在共享内存区域显示部分内存。因此,所有进程均可访问共享内存区域。利用共享内存特性可以减少进程中的数据交换。

Data exchange among the processes

在默认情况下,单个 MPI 进程创建的内存为私有。只共享内存,其他资源仍需保密时,最好使用 MPI-3 共享内存。由于每个进程均可访问共享内存区域,用户使用共享内存时,需要注意进程同步。

示例代码

本部分提供了示例代码,以展示 MPI-3 共享内存的应用。

该节点总共生成了 8 个 MPI 进程。每个进程都维护了一个由 3200 万个元素构成的长数组。对于数组中的每个元素 j,进程基于它的当前值和两个相邻进程中相应数组的元素 j值更新该元素值,并在整个数组中重复应用相同的程序。以下伪代码显示了 8 个 MPI 进程(64 次迭代)运行该程序:

Repeat the following procedure 64 times:
for each MPI process n from 0 to 7:
    for each element j in the array A[k]:An[j] ← 0.5*An[j]  + 0.25*Aprevious[j] + 0.25*Anext[j]

An 是一个长数组,属于进程 nAn [j] 是数组中元素 j 的值,属于进程 n。在本程序中,由于每个进程在本地内存中显示了内存,因此,尽管每个进程只需要两个相邻的数组(例如,进程 0 需要来自进程 1 和进程 7 的数据,进程 1 需要来自进程 0 和进程 2 的数据),所有进程均可访问全部数组。

Shared Memory Diagram

除了用于 MPI 编程的基本 API 以外,本示例还介绍了以下 MPI-3 共享内存 API:

  • MPI_Comm_split_type:用于创建一个新的通信器,使全部进程共享一个通用属性。在本示例中,为了从母通信器中创建一个共享内存(如 MPI_COMM_WORLD),将 MPI_COMM_TYPE_SHARED传递为变量,然后将通信器分解为共享内存通信器 shmcomm
  • MPI_Win_allocate_shared:用于创建一个共享内存,所有进程均可在共享内存通信器中访问该共享内存。每个进程向其他所有进程显示了其本地内存,每个进程分配的本地内存大小可能有所不同。在默认情况下,连续分配总体共享内存。用户可以传递一条信息提示“alloc_shared_noncontig”,以指明不需要连续分配共享内存,这样可能会提升性能,提升与否取决于基础硬件架构。 
  • MPI_Win_free:用于释放内存。
  • MPI_Win_shared_query:用于查询 MPI 进程的共享内存地址。
  • MPI_Win_lock_allMPI_Win_unlock_all:用于启动窗口中所有进程的访问 epoch。仅需共享 epoch。调用进程可以访问所有进程的共享内存。
  • MPI_Win_sync:用于确保复制本地内存到共享内存的操作已完成。
  • MPI_Barrier:在所有进程到达 barrier 前,拦截节点上的调用者进程。barrier 同步 API 在所有进程上运行。

对英特尔® 至强融核™ 处理器进行基本性能调试

本测试在英特尔至强融核处理器 7250(1.40 GHz、68 核)上运行,该处理器安装了 Red Hat Enterprise Linux* 7.2、英特尔® 至强融核™ 处理器软件 1.5.1 和 英特尔® Parallel Studio 2017 更新 2。在默认情况下,英特尔编译器尝试对代码进行矢量化处理,每个 MPI 进程都有一个执行线程。OpenMP* pragma 被添加到循环层面,便于随后使用。为了编译代码,运行以下命令行以生成二进制 mpishared.out

$ mpiicc mpishared.c -qopenmp -o mpishared.out
$ mpirun -n 8 ./mpishared.out
Elapsed time in msec:5699 (after 64 iterations)

为了探索线程并行性,每个内核运行 4 个线程,重新编译 –xMIC-AVX512,以充分利用英特尔® 高级矢量扩展指令集 512(英特尔® AVX-512)指令:

$ mpiicc mpishared.c -qopenmp -xMIC-AVX512 -o mpishared.out
$ export OMP_NUM_THREADS=4
$ mpirun -n 8 ./mpishared.out
Elapsed time in msec:4535 (after 64 iterations)

由于系统中的 MCDRAM 目前被配置为扁平,英特尔至强融核处理器显示两个 NUMA 节点。节点 0 包含所有 CPU 和平台内存 DDR4,节点 1 包含封装内存 MCDRAM:

$ numactl -H
available:2 nodes (0-1)
node 0 cpus:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
node 0 size:98200 MB
node 0 free:92775 MB
node 1 cpus:
node 1 size:16384 MB
node 1 free:15925 MB
node distances:
node   0   1
  0:10  31
  1:31  10

为了分配 MCDRAM(节点 1)中的内存,运行以下命令,将参数 –m 1传递至命令 numactl

$ numactl -m 1 mpirun -n 8 ./mpishared.out
Elapsed time in msec:3070 (after 64 iterations)

这个简单的优化技巧极大地提升了性能速度。

总结

本白皮书介绍了 MPI-3 共享内存特性,提供了使用 MPI-3 共享内存 API 的示例代码。伪代码说明了运行的程序和共享内存 API。该程序运行于英特尔至强融核处理器,我们利用简单的技术进一步优化了该程序。

参考资料

  1. MPI 论坛,MPI 3.0
  2. 消息传递接口论坛,MPI:消息传递接口标准版 3.0
  3. 麻省理工学院出版社,《使用高级 MPI》
  4. James Reinders 和 Jim Jeffers,出版商:Morgan Kaufmann,第 16 章 - MPI-3 共享内存编程介绍,《High Performance Parallelism Pearls》第二册

附录

该示例 MPI 程序代码可通过下载获取。


使用英特尔® SPMD 程序编译器实现游戏 CPU 的矢量化

$
0
0

下载 GitHub* 代码示例

简介

基于 LLVM*英特尔® SPMD 程序编译器 (在之前的文档中通常被称作 ISPC)并不是 Gnu* 编译器套装 (GCC) 或 Microsoft* C++ 编译器的替代品;它更类似于面向 CPU 的着色器编译器,可生成适用多种指令集的矢量指令,如英特尔® SIMD 流指令扩展 2(英特尔® SSE2)、英特尔® SIMD 流指令扩展 4(英特尔® SSE4)、英特尔® 高级矢量扩展指令集(英特尔® AVX)、英特尔® AVX2 等。输入基于 C 的着色器或内核,输出预编译对象文件,您的应用中将包含一个头文件。通过使用少量关键字,编译器便可以得到关于在 CPU 矢量单元上分配工作的明确指示。

如果开发人员选择将内联函数直接编写到代码库,显式矢量化便可提供更高性能,但是这样做极为复杂,维护成本较高。英特尔 SPMD 程序编译器 内核使用高级语言编写而成,因此开发成本较低。相比英特尔 SSE4 等最常见的指令集,它还可以轻松支持多个指令集,为运行代码的 CPU 提供最佳性能。

本文的目的并不是教会读者如何编写英特尔 SPMD 程序编译器内核;本文简单介绍了如何将英特尔 SPMD 程序编译器插入 Microsoft Visual Studio* 解决方案,提供了关于如何将简单的高级着色语言* (HLSL*) 计算着色器导入英特尔 SPMD 程序编译器内核的指导。如欲获取关于英特尔 SPMD 程序编译器更详细的概述,请参阅在线文档

本文提供的示例代码基于 Microsoft DirectX* 12 n 体示例的修改版本,为了支持英特尔 SPMD 程序编译器矢量化计算内核,已导入该版本。本文的目的并不是显示 GPU 的性能增量,而是显示从标准标量 CPU 代码迁移至矢量化 CPU 代码实现的性能提升。

由于本应用原始示例中的 CPU 负载非常小,显然不能代表一个游戏,但是本应用显示了在多个 CPU 内核上使用矢量单元可能会提升性能。


图 1.经过修改的 n 体重力示例截屏

原始 DirectX* 12 n 体重力示例

开始移植英特尔 SPMD 程序编译器之前,了解原始示例及其目的非常有帮助。编写 DirectX 12 n 体重力示例是为了介绍如何使用 DirectX 12 中的独立计算引擎执行异步计算,也就是在 GPU 上并行执行粒子更新与粒子渲染。示例生成 10,000 个粒子,逐帧更新与渲染粒子。更新包括每个粒子与其他粒子的交互,每次模拟 tick 生成 100,000,000 个交互。

HLSL 计算着色器将计算线程映射至每个粒子,以执行更新。双缓冲粒子数据,因此,对于每一帧,GPU 从缓冲 1 开始渲染,异步更新缓冲 2,然后翻转缓冲,为下一帧做准备。

就是这么简单。不仅简单,它还是英特尔 SPMD 程序编译器移植的绝佳选择,因为异步计算任务适用于出色地运行于 CPU;代码和引擎可在并发执行路径中执行计算。通过将某些负载迁移至多数情况下未被充分利用的 CPU,GPU 可以更快地完成帧,或者完成更多工作,同时充分利用 CPU。

移植到英特尔® SPMD 程序编译器

建议首先从 HLSL 移植到标量 C/C++。这样确保了算法的正确性,生成了正确的结果,正确地与其它应用交互,以及恰当处理多个线程(如果适用)。听起来轻而易举,但是需要注意以下几点:

  1. 如何在 GPU 和 CPU 之间共享内存。
  2. 如何同步 GPU 和 CPU。
  3. 如何面向单指令/多数据 (SIMD) 和多线程划分任务。
  4. 将 HLSL 代码移植到标量 C。
  5. 将标量 C 移植到英特尔 SPMD 程序编译器内核。

某些操作相对简单。

共享内存

我们知道需要在 CPU 和 GPU 之间共享内存,但是如何共享?幸运的是,DirectX 12 提供一些选项,如将 GPU 缓冲映射到 CPU 内存等。为了简化示例,最大程度减少代码变更,我们重新使用了用于初始化 GPU 粒子缓冲的粒子上传临时缓冲,并创建了面向 GPU 访问的双缓冲 CPU 副本。使用模式成为:

  • 在 CPU 中更新 CPU 可访问的粒子缓冲。
  • 使用原始上传临时缓冲调用 DirectX 12 助手 UpdateSubresources,GPU 粒子缓冲作为目的地。
  • 绑定 GPU 粒子缓冲并渲染。

同步

如果原始异步计算内核已经有一个用于配置计算和渲染之间交互的 DirectX 12 栅栏对象,同步将自然发生,重新利用它通知渲染引擎副本已完成。

划分工作

为了划分工作,我们应首先考虑 GPU 如何划分工作,因为这可能同样适用于 CPU。计算着色器通过两种方式控制工作的划分。首先是调度大小,指的是录制命令流时传输至 API 调用的大小。描述了运行的工作组的数量和维度。第二个是本地工作组的数量和维度,该工作组被硬编码为着色器。本地工作组的每个项目可视作一个工作线程,如果使用了共享内存,每个线程可与工作组中的其他线程共享信息。

通过观察 nBodyGravityCS.hlsl计算着色器发现,本地工作组的规模为 128 x 1 x 1,并使用共享内存优化某些粒子负载,但是在 CPU 上没有必要这样做。除此以外,线程之间没有交互,每个线程与外层循环执行不同的粒子,与内层循环上的所有其它粒子交互。

这应该非常适合 CPU 矢量宽度,因此我们使用面向英特尔 AVX2 的 8 x 1 x 1 或面向英特尔 SSE4 的 4 x 1 x 1 更换 128 x 1 x 1。我们也可以将调度大小用作多线程代码的提示,根据 SIMD 宽度,将 10,000 个粒子除以 8 或 4。但是,由于我们已经发现每个线程之间没有关联,可以简单地将粒子数量除以线程池中的可用线程数量,或除以设备上的可用逻辑内核数量,启用了英特尔® 超线程技术的典型 4 核 CPU 上的内核数量为 8。移植其他计算着色器时,可能需要考虑更多因素。

这为我们提供了以下伪代码:

For each thread
		Process N particles where N is 10000/threadCount
		      For each M particles from N, where M is the SIMD width
		           Test interaction with all 10000 particles

移植 HLSL* 至标量 C

编写英特尔 SPMD 程序编译器内核时,除非您有丰富的经验,否则建议您首先编写一个标量 C 版本。这将确保所有应用粘接、多线程和内存操作在开始矢量化之前已经运行。

为此,nBodyGravityCS.hlsl中的多数 HLSL 代码可在 C 中工作,除了为粒子添加外层循环和将着色器数学矢量类型改为使用基于 C 的类型以外,只需极少的修改。在本示例中,float4/float3 类型与 DirectX XMFLOAT4/XMFLOAT3 类型交换,某些矢量数学操作被划分为标量操作。

CPU 粒子缓冲被用于读写,如上所述,借助用于同步的原始栅栏将写入缓冲上传至 GPU。为了实现线程化,示例使用了并行模式库中的 Microsoft’s concurrency::parallel_for结构。

代码可以在 D3D12nBodyGravity::SimulateCPU()D3D12nBodyGravity::ProcessParticles()中找到。

运行标量代码后,建议快速检查性能,以确保迁移至英特尔 SPMD 程序编译器前,没有需要解决的算法热点。在本示例中,使用英特尔® VTune™工具分析基本热点得出,反平方根 (sqrt) 位于热路径上,因此,被替换为来自 Quake* 的快速反平方根近似值,后者能小幅改进性能,由于失去了精度,改善效果不明显。

移植标量 C 至标量英特尔® SPMD 程序编译器

为了创建英特尔 SPMD 程序编译器内核以及将它们连接至您的应用(对 Microsoft Visual Studio 的修改将在后文中介绍)而修改构建系统后,可以开始编写英特尔 SPMD 程序编译器代码并将它连接至您的应用。

钩子

为了从您的应用代码中调用任何英特尔 SPMD 程序编译器内核,需要添加自动生成的相关输出头文件,然后按照调用正常库的方式调用导出的函数,请记住,所有声明包含在 ispc命名空间中。在本示例中,我们从 SimulateCPU()函数中调用 ispc::ProcessParticles

矢量数学

钩子建成后,下一步便是运行标量英特尔 SPMD 程序编译器代码,然后对它进行矢量化。简单修改后,多数标量 C 代码可直接进入英特尔 SPMD 程序编译器内核。在本示例中,尽管英特尔 SPMD 程序编译器提供模板化矢量类型,但是类型仅面向存储,因此,需要定义所有矢量数学类型和新的结构。完成后,所有 XMFLOAT 类型均被转换为 Vec3 和 Vec4 类型。

关键字

我们现在需要利用英特尔 SPMD 程序编译器的特定关键字修饰代码,帮助引导矢量化和编译。第一个关键字是函数签名使用的 export,类似于调用协定,以通知英特尔 SPMD 程序编译器内核入口点的所在。会产生两个影响。首先,将函数签名和任何所需的结构添加至自动生成的头文件,但是由于所有参数必须是标量,也为函数签名带来了限制,导致我们采用了另外两个关键字,varyinguniform

统一变量描述了无法共享的标量变量,但是它的内容可以在所有 SIMD 通道上共享,同时,不同的变量将被矢量化,在所有 SIMD 通道上得到独特的值。默认情况下,所有变量均不同,因此,尽管可以添加关键字,但是本示例没有采用。首次创建本内核的标量版本时,我们将使用统一关键字修饰变量,以确保变量是严格的标量。

英特尔 SPMD 程序编译器标准库

英特尔 SPMD 程序编译器提供一个能帮助移植的标准库,包含许多常见函数,如 floatbits() 和 intbits(),这些函数是快速反平方根函数进行浮点投影所必需的。

矢量化英特尔 SPMD 程序编译器内核

如果英特尔 SPMD 程序编译器内核按照预期正常运行,应立即实施矢量化。通常情况下,确定并行化的对象以及如何并行化最复杂。按照常规经验,移植 GPU 计算着色器需要遵循 GPU 矢量化的原有模式,在本示例中,核心计算内核被多个 GPU 执行单元并行调用。因此,我们为粒子更新的标量版本添加的新外层循环最需要进行矢量化。

对于矢量 ISA,分散/集中操作非常昂贵(尽管英特尔 AVX2 指令集对此进行了改进),因此,数据布局同样重要,连续内存位置通常更适用于频繁加载/存储。

并行循环

在 n 体示例中遵循这个经验规则,对外层循环进行矢量化,内层循环为标量。因此,将 8 个粒子加载至英特尔 AVX 寄存器中,并在全部 10,000 个粒子中测试这 8 个粒子。这 10,000 个位置将被视作标量变量,在所有 SIMD 通道上共享,没有分散/集中成本。英特尔 SPMD 程序编译器对我们隐藏实际的矢量宽度(除非我们非常想知道),可以提供出色的提取,以透明支持英特尔 SSE4、英特尔® 高级矢量扩展指令集 512(英特尔® AVX-512)等指令集中的各种 SIMD 宽度。

通过将外层 for循环替换为英特尔 SPMD 程序编译器 foreach循环,实施矢量化,指导英特尔 SPMD 程序编译器在 N 尺寸的数据块范围内迭代,N 是当前的矢量宽度。因此,一旦 foreach循环迭代器 ii被用于取消阵列变量,对于矢量中每个 SIMD 通道,ii 的值均不同,确保每个通道运行相异的粒子。

数据布局

此时,简要介绍数据布局非常重要。在 CPU 上使用矢量寄存器时,高效加载和卸载寄存器非常重要;不这样做将导致性能大幅下降。为此,矢量寄存器需要从阵列结构 (SoA) 数据来源中加载数据,因此,可以使用单条指令将相邻内存值的矢量宽度直接加载至运行中的矢量寄存器。如果无法实现,需要降低收集操作的速度,以加载非相邻值的矢量宽度至矢量寄存器,需要通过分散操作再次保存数据。

在本示例中,类似于许多图形应用,粒子数据保存于结构阵列 (AoS) 布局中。可将其转换为 SoA,以避免分散/集中,但是鉴于算法的本质,相比处理内层循环中的 10,000 个标量粒子,外层循环所需的分散/集中成本更低,因此,数据保存为 AoS。

矢量变量

我们的目标是矢量化外层循环,同时保持内层循环的标量格式,因此,外层循环粒子的矢量宽度将处理相同的内层循环粒子。为此,我们宣告 pos, vel,accel可变,从而将外层循环粒子的位置、速度和加速度加载至矢量寄存器。具体做法是删除添加至标量内核的 uniform修饰,这样一来,英特尔 SPMD 程序编译器知道需要矢量化这些变量。

通过 bodyBodyInteractionQ_rsqrt函数传播,以确保正确地矢量化。这只是遵循变量传输和检查编译器错误的一个示例。最终,Q_rsqrt实现了完全矢量化,对 bodyBodyInteraction进行了大范围矢量化,内层循环粒子位置 thatPos为标量。

这就是所需的全部,现在,英特尔 SPMD 程序编译器矢量化内核开始运行,提供优于标量版本的卓越性能。

性能

在两个不同的英特尔 CPU 上对修改后的 n 体应用进行测试,并使用 PresentMon*捕获性能数据,在每颗 CPU 上各运行 3 次,每次 10 秒,记录帧的时长,然后取平均值。结果显示,相比标量 C/C++ 代码,面向英特尔 AVX2 的英特尔 SPMD 程序编译器内核实现了 8–10 倍的性能提升。两台设备均使用 Nvidia* 1080 GTX GPU,并使用了所有可用的 CPU 内核。

处理器

标量 CPU 实施

英特尔® AVX2 实施(使用英特尔® SPMD 程序编译器编译)

提升

英特尔® 酷睿™ i7-7700K 处理器

92.37 毫秒

8.42 毫秒

10.97 倍

英特尔酷睿 I7-6950X 处理器至尊版

55.84 毫秒

6.44 毫秒

8.67 倍

如何将英特尔 SPMD 程序编译器集成至 Microsoft Visual Studio*

  1. 确保英特尔 SPMD 程序编译器在路径上或者可以在 Microsoft Visual Studio* 中轻松定位。
  2. 将英特尔 SPMD 程序编译器内核添加至您项目。如果文件类型无法识别,将不能在默认情况下创建。
  3. 右键点击文件 Properties,将项目类型改为 Custom Build Tool:

  4. 单击 OK并重新打开 Property 页面,便可以修改自定义创建工具。

    a. 使用以下命令行格式:

    ispc -O2 <filename> -o <output obj> -h <output header> --target=<target backends> --opt=fast-math

    b. 本示例中使用的完整命令行为:

    $(ProjectDir)..\..\..\..\third_party\ispc\ispc -O2 "%(Filename).ispc" -o "$(IntDir)%(Filename).obj" -h "$(ProjectDir)%(Filename)_ispc.h" --target=sse4,avx2 --opt=fast-math

    c. 添加编译器生成的相关输出 ie. obj文件:

    $(IntDir)%(Filename).obj;$(IntDir)%(Filename)_sse4.obj;$(IntDir)%(Filename)_avx2.obj

    d. 将 Link Object设置为 Yes

  5. 现在编译您的英特尔 SPMD 程序编译器内核。如果编译成功,将生成一个头文件和一个目标文件。
  6. 将头文件添加至您的项目,并将它放入应用源代码中。

  7. 从相关位置调用英特尔 SPMD 程序编译器内核,请记住,从内核中导出的任何函数都将位于英特尔 SPMD 程序编译器命名空间:

总结

本文的目的在于展示英特尔 SPMD 程序编译器能帮助开发人员将高度矢量化的 GPU 计算内核轻松迁移到矢量化 CPU 代码,以充分利用备用 CPU 周期,为用户提供更丰富的游戏体验。任何自然进行矢量化的工作负载都可以借助英特尔 SPMD 程序编译器内核轻松提升性能,无需使用标量代码。英特尔 SPMD 程序编译器可减少开发和维护时间,也可以便捷地支持全新指令集。

Viewing all 49 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>