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.hpp getting_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()方法访问的人类可读的错误消息。 在编译并运行示例后,得到的输出为: