oneDNN学习笔记整理及简单翻译
-
最近正在啃这个,就想着简单翻译一下以后要看也方便不少。专有名词就不翻译了。大家凑合看,有错误欢迎指出
原文链接:oneDNN
————————————————————正文——————————————————————————
基本概念
本质上,oneDNN编程模型在于执行一个或几个primitives以对个或几个内存对象中的数据进行处理。Primitives
oneDNN是围绕primitives(dnnl :: primitive)的概念构建的。primitives是一个封装了特定运算(如forward convolution和backward LSTM)的functor object。- primitive和纯函数之间最重要的区别是primitive可以存储状态。
-
primitive状态的一部分是不变的。
-
oneDNN编程模型假定预计算所需的时间可以拆分成多次重复使用相同primitive进行计算的时间。
-
primitive状态的可变部分称为暂存器。它是一个primitive仅可在计算期间用于临时存储的缓冲区。暂存器可以被primitive object(这使该对象成为非线程安全的)或执行时参数拥有。
Engines
engines(dnnl :: engine)是计算设备的抽象:CPU,系统中的特定GPU等。创建大多数primitive是为了在一个特定engine上执行计算。唯一的例外是在两个不同引擎之间传输数据的reorder primitives。Streams
streams(dnnl :: stream)封装了绑定到特定引擎的执行背景。例如,它们可以相当于OpenCL命令队列。Memory Objects
memory objects(dnnl :: memory)封装了特定engine上分配的内存的句柄。memory在执行期间被传递给primitives。
-
Creating Memory Objects and Primitives
Memory Objects
有两种常见的初始化memory descriptor的方式:• 通过使用dnnl :: memory :: desc构造函数或通过dnnl :: memory :: desc :: submemory_desc提取张量的一部分的descriptor
• 通过查询现有的primitive descriptor以获取与该primitive的参数之一相对应的memory descriptor(例如dnnl :: convolution_forward :: primitive_desc :: src_desc)。可以使用用户提供的句柄(CPU上的一个void *)来创建memory objects,也可以不使用,在这种情况下,库将自行分配存储空间。
Primitives
创建primitive的操作顺序为:- 通过例如dnnl :: convolution_forward :: desc创建操作描述符。如果原语支持,操作描述符可以包含带有占位符format_tag :: any内存格式的memory descriptor。
- 基于操作描述符,引擎和属性创建一个primitive descriptor。
- 基于在步骤2中获得的primitive descriptor创建一个primitive。
注意
该流程并不完整地涉及所有primitive。例如,reorder primitive没有操作描述符。
-
Getting started
示例代码:getting_started.cpp
此C ++ API示例演示了oneDNN编程模型的基础:
• 如何创建oneDNN内存对象。
• 如何从用户缓冲区将数据获取到oneDNN内存对象中。
• 张量的逻辑维和存储对象格式如何关联。
• 如何创建oneDNN primitives。
• 如何执行primitives。
该示例使用ReLU操作,包括以下步骤:- 创建engine和stream以执行primitive。
- 执行数据准备(oneDNN之外的代码)。
- 将数据包装到一个oneDNN内存对象中(使用不同的样式)。
- 创建一个ReLU primitive。
- 执行ReLU primitive。
- 获得结果并进行验证(检查结果图像是否不包含负值)。
Public headers
dnnl.hpp
dnnl_debug.h
example_utils.hppgetting_started_tutorial() function
Engine and stream
所有oneDNNprimitive和内存对象都附加到特定的dnnl :: engine(计算设备的抽象)
为一个引擎创建的内存对象或primitive都不能在另一引擎上使用创建engine:
需要指定dnnl::engine::kind以及给定类型设备的索引
例句:
engine eng(engine_kind, 0);除了engine之外,所有pirimitive都需要dnnl :: stream来执行。stram封装了执行上下文,并绑定到特定engine。
创建stream:
stream engine_stream(eng);数据准备(oneDNN之外的代码)
我们将使用NHWC格式创建4D张量
注意,即使我们只处理一个图像,其图像张量仍为4D。额外的维度(N)对应于批处理,并且,对于单一的图像,其为1
在oneDNN中,所有CNN primitive都假设张量具有批处理维度,该维度始终是第一个逻辑维度
代码示例:
将数据包装到oneDNN内存对象中
创建dnnl :: memory包括两个步骤:
- 初始化dnnl :: memory :: desc结构(也称为memory descriptor),该结构仅描述张量数据,不包含数据本身。memory descriptor用于创建dnnl :: memory对象并初始化primitive descriptor
- 根据在步骤1中初始化的memory descriptor,engine以及(可选)数据的句柄,创建dnnl :: memory对象本身(也称为memory object)。Memory object在premitive被执行时被使用
得益于C ++ 11中引入的列表初始化,当一个memory descriptor只在创建dnnl :: memory对象时被使用,就可以将这两个步骤组合在一起
Memory descriptor
要初始化dnnl :: memory :: desc,我们需要传递:
1.张量的维数。其语义顺序由将使用此memory(descriptor)的primitive定义。这导致:内存描述符和对象不知道它们描述或包含的数据的任何含义
2.张量的数据类型(dnnl :: memory :: data_type)。
3.内存格式标记(dnnl :: memory :: format_tag)。用于描述如何在设备的内存中布置数据。primitive需要正确的存储格式才能正确处理数据。
代码示例:
ReLU这样的oneDNN CNN文件期望以下形式的张量:
其中:
N为批处理维度
C为通道维度/特征地图维度、
D H W为空间维度现在,维度的逻辑顺序已经定义完成,我们需要指定内存格式(第三个参数),该内存格式描述了逻辑索引是如何映射到偏移量的。用户的格式MHWC就在这里发挥作用。oneDNN具有不同的dnnl :: memory :: format_tag值,这些值涵盖了最流行的内存格式,例如NCHW,NHWC,CHWN等。
图像的memory descriptor为src_md 。md指memory descriptor。
创建memory object的另一种方法
将memory descriptor 和engine准备好后,让我们为ReLU primitive创建输入和输出的memory object:
我们已经有一个用于source memory object的存储缓冲区。我们将其传递给dnnl :: memory :: memory(const desc&,const engine&,void *)构造函数,该构造函数将缓冲区指针作为最后一个参数。
我们使用一个构造函数,该构造函数指示库为dst_mem分配一个内存缓冲区。两者之间的主要区别是:
- 该库将拥有dst_mem的内存,并且在dst_mem销毁时,该内存也会取消分配。这意味着内存缓冲区只能在dst_mem活跃时使用。
- 库分配的缓冲区具有良好的对齐方式,通常可以提高性能。
注意
在库外部分配并传递给oneDNN的内存应具有良好的对齐方式,以实现更好的性能。在接下来的部分中,我们将展示如何从dst_mem内存对象获取缓冲区(指针)。
创建ReLU primitive
该库将ReLU原语实现为更通用的Eltwise原语的特定算法,该算法将指定函数应用于源张量的每个元素
对于 dnnl::memory,用户需要经过至少三个创建步骤(但是,多亏了C++11,这三个步骤有时可以合并。
- 初始化一个operation descriptor( 这个例子中为dnnl::eltwise_forward::desc),该操作描述符定义了操作参数。
- 创建一个operation primitive descriptor,该描述符是实现给定操作的实际算法的轻量级描述符。用户可以查询所选实现的不同特性,例如内存消耗以及下一个主题(Memory Format Propagation)中涉及的其他一些特性
- 创建一个可以在memory object上执行并用于计算操作的primitive(此处为dnnl::eltwise_forward)
为了使用户能够在创建基元之前检查基元实现的详细信息,oneDNN将步骤2和3分开。这将造成昂贵的开销,因为,比如说,oneDNN将动态生成优化的计算代码
注意
Primitived的创建能是非常昂贵的操作,因此请考虑一次创建多个primitive object并多次执行它们。代码示例:
关于变量名:
_md 代指memory descriptor
_d 代指operation descriptor
_pd 代指primitive descriptor值得一提的是,我们在初始化relu_d时指定了确切的张量及其存储格式。这意味着relu primitive将使用与该描述相对应的memory object执行计算。这是创建“非计算集中型primitives”(例如Eltwise, Batch Normalization等)的唯一方法。
计算集中型primitive(例如Convolution)具有自行定义适当的内存格式的能力。这是该库的主要功能之一。我们将在下一个主题:Memory Format Progatation中进行详细讨论。
执行ReLU primitive
输入和输出的memory object将被<tag, memory>映射传递给execute()方法。每一个tag指定了该memory object代表哪一种tensor。每一个Eltwise primitive要求该映射含有两个元素:作为输入的source memory object和作为输出的destination memory。
Primitive在stream(execute()的第一个参数)中被执行。根据stream的种类。一个execution可能是阻塞的,也可能是非阻塞的。这意味着,我们在获得结果之前,需要调用dnnl::stream::wait。
Eltwise是支持原位(in-place)操作的primitive之一,这意味着source memory和destination memory可以相同。为了进行原位变换,用户需要给DNNL_ARG_SRC 和 DNNL_ARG_DSTtag传递相同的内存对象。
代码示例:
获得结果并验证
结果储存在 dst_mem memory object中,所以我们需要通过dnnl::memory::get_data_handle() 获取指向缓存区的C++指针,并将其转换为正确的数据类型。
警告
dnnl::memory::get_data_handle()将一个原始句柄返回给缓冲区,而该缓冲区的类型由engine指定。对于CPU,缓冲区始终是指向void的指针,可以安全地使用它。但是,对于CPU以外的引擎,句柄类型可能在运行时才能确定,例如对于GPU / OpenCL,其类型为cl_mem代码示例:
main() function
因为我们使用的是oneDNN C ++ API,所以我们使用异常来处理错误。oneDNN C++ API引发dnnl :: error类型的异常,其中包含错误状态(类型为dnnl_status_t)和可通过常规what()方法访问的人类可读的错误消息。
在编译并运行示例后,得到的输出为: