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的操作顺序为:

    1. 通过例如dnnl :: convolution_forward :: desc创建操作描述符。如果原语支持,操作描述符可以包含带有占位符format_tag :: any内存格式的memory descriptor。
    2. 基于操作描述符,引擎和属性创建一个primitive descriptor。
    3. 基于在步骤2中获得的primitive descriptor创建一个primitive。
      注意
      该流程并不完整地涉及所有primitive。例如,reorder primitive没有操作描述符。


  • Getting started

    示例代码:getting_started.cpp
    此C ++ API示例演示了oneDNN编程模型的基础:
    • 如何创建oneDNN内存对象。
    • 如何从用户缓冲区将数据获取到oneDNN内存对象中。
    • 张量的逻辑维和存储对象格式如何关联。
    • 如何创建oneDNN primitives。
    • 如何执行primitives。
    该示例使用ReLU操作,包括以下步骤:

    1. 创建engine和stream以执行primitive。
    2. 执行数据准备(oneDNN之外的代码)。
    3. 将数据包装到一个oneDNN内存对象中(使用不同的样式)。
    4. 创建一个ReLU primitive。
    5. 执行ReLU primitive。
    6. 获得结果并进行验证(检查结果图像是否不包含负值)。

    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都假设张量具有批处理维度,该维度始终是第一个逻辑维度

    代码示例:
    0_1600951472966_d187e79f-ec49-403f-9af7-4f43b25584cd-image.png

    将数据包装到oneDNN内存对象中

    创建dnnl :: memory包括两个步骤:

    1. 初始化dnnl :: memory :: desc结构(也称为memory descriptor),该结构仅描述张量数据,不包含数据本身。memory descriptor用于创建dnnl :: memory对象并初始化primitive descriptor
    2. 根据在步骤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需要正确的存储格式才能正确处理数据。
    代码示例:
    0_1600951668853_91bedeef-2a29-48a0-9671-b2b2a7c169d8-image.png
    ReLU这样的oneDNN CNN文件期望以下形式的张量:
    0_1600951583287_48e1ac37-a3f9-4b0a-a107-f579b3060318-image.png

    其中:
    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:
    0_1600951697056_fe66b07f-dea6-4d97-a4ce-bfbc27ebd83f-image.png
    我们已经有一个用于source memory object的存储缓冲区。我们将其传递给dnnl :: memory :: memory(const desc&,const engine&,void *)构造函数,该构造函数将缓冲区指针作为最后一个参数。
    我们使用一个构造函数,该构造函数指示库为dst_mem分配一个内存缓冲区。

    两者之间的主要区别是:

    1. 该库将拥有dst_mem的内存,并且在dst_mem销毁时,该内存也会取消分配。这意味着内存缓冲区只能在dst_mem活跃时使用。
    2. 库分配的缓冲区具有良好的对齐方式,通常可以提高性能。

    注意
    在库外部分配并传递给oneDNN的内存应具有良好的对齐方式,以实现更好的性能。

    在接下来的部分中,我们将展示如何从dst_mem内存对象获取缓冲区(指针)。

    创建ReLU primitive

    该库将ReLU原语实现为更通用的Eltwise原语的特定算法,该算法将指定函数应用于源张量的每个元素

    对于 dnnl::memory,用户需要经过至少三个创建步骤(但是,多亏了C++11,这三个步骤有时可以合并。

    1. 初始化一个operation descriptor( 这个例子中为dnnl::eltwise_forward::desc),该操作描述符定义了操作参数。
    2. 创建一个operation primitive descriptor,该描述符是实现给定操作的实际算法的轻量级描述符。用户可以查询所选实现的不同特性,例如内存消耗以及下一个主题(Memory Format Propagation)中涉及的其他一些特性
    3. 创建一个可以在memory object上执行并用于计算操作的primitive(此处为dnnl::eltwise_forward)
      为了使用户能够在创建基元之前检查基元实现的详细信息,oneDNN将步骤2和3分开。这将造成昂贵的开销,因为,比如说,oneDNN将动态生成优化的计算代码

    注意
    Primitived的创建能是非常昂贵的操作,因此请考虑一次创建多个primitive object并多次执行它们。

    代码示例:
    0_1600951820763_b3124623-b726-4dd4-bae2-db08dc629c06-image.png

    关于变量名:
    _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。
    0_1600951929770_1f08175d-3153-4c12-8b29-bf48661a11e7-image.png

    Eltwise是支持原位(in-place)操作的primitive之一,这意味着source memory和destination memory可以相同。为了进行原位变换,用户需要给DNNL_ARG_SRC 和 DNNL_ARG_DSTtag传递相同的内存对象。

    代码示例:
    0_1600951941849_52e51f71-de0b-4907-b807-3c7925f18ded-image.png

    获得结果并验证

    结果储存在 dst_mem memory object中,所以我们需要通过dnnl::memory::get_data_handle() 获取指向缓存区的C++指针,并将其转换为正确的数据类型。

    警告
    dnnl::memory::get_data_handle()将一个原始句柄返回给缓冲区,而该缓冲区的类型由engine指定。对于CPU,缓冲区始终是指向void的指针,可以安全地使用它。但是,对于CPU以外的引擎,句柄类型可能在运行时才能确定,例如对于GPU / OpenCL,其类型为cl_mem

    代码示例:
    0_1600951968451_0211e31d-3a27-4cf4-ba78-89128fe53654-image.png

    main() function

    因为我们使用的是oneDNN C ++ API,所以我们使用异常来处理错误。oneDNN C++ API引发dnnl :: error类型的异常,其中包含错误状态(类型为dnnl_status_t)和可通过常规what()方法访问的人类可读的错误消息。

    0_1600952006641_16c682f9-5ea0-4308-8199-f04ffaf74576-image.png
    在编译并运行示例后,得到的输出为:

    0_1600952014440_a455195d-d120-40f9-b94c-4fa45fd1167a-image.png


 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待