昇腾Ascend TIK自定义算子开发教程(概念版)

news2024/10/11 14:24:02

一、参考资料

【2023 · CANN训练营第一季】Ascend C算子开发入门(中)

二、重要说明

  • TIK2编程范式把算子核内的处理程序,分成多个流水任务,任务之间通过队列(Queue)进行通信和同步,并通过统一的内存管理模块(Pipe)管理任务间通信内存。
  • TIK2分别针对Vector、Cube编程设计了不同的流水任务。开发者只需要完成基本任务的代码实现即可,底层的指令同步和并行调度由TIK2框架实现,开发者无需关注。
  • 由于开发高性能Cube算子难度较大,当前仅支持用户开发Vector算子
  • 当前TIK2支持的AI处理器型号为昇腾310P AI处理器、昇腾910 AI处理器,其他型号暂不支持。
  • 当前支持用户使用g++等C/C++编译器编译在cpu侧执行的TIK2算子,并使用gdb单步调试;支持用户使用CCEC编译器编译在npu侧执行的TIK2算子,实现加速计算,暂不支持加载至网络模型中进行整网验证。
  • 算子输出的数据类型与输入数据类型相同。
  • 输出shape与输入shape相同。

三、相关介绍

1. CANN算子

CANN算子有两种类型,TBE算子与AI CPU算子。
在这里插入图片描述

  • AI Core是昇腾AI处理器的计算核心,负责执行矩阵、向量、标量计算密集的算子任务,在AI Core上执行的算子称为TBE(Tensor Boost Engine)算子
  • AI CPU负责执行不适合跑在AI Core上的算子,是AI Core算子的补充,主要承担非矩阵类、逻辑比较复杂的分支密集型计算,在AI CPU上执行的算子称为AI CPU算子

1.1 TBE算子

TBE(Tensor Boost Engine,张量加速引擎)提供了基于TVM(Tensor Virtual Machine,张量虚拟机)框架的自定义算子开发能力,提供了用户开发自定义算子所需工具。TBE框架给用户提供了两种算子开发方式 :DSL与TIK。开发者可以根据需求自由选择,两种开发方式的区别如下:

  • DSL( Domain-Specific Language ,基于特性域语言)

    DSL接口已高度封装,用户仅需要使用DSL接口完成计算过程的表达,后续的算子调度、算子优化及编译都可通过已有的接口一键式完成,适合初级开发用户

  • TIK( Tensor Iterator Kernel, 张量嵌套内核)

    开发者可以通过调用TIK提供的API基于Python语言编写自定义算子,然后TIK编译器会将其编译为适配昇腾AI处理器应用程序的二进制文件。TIK需要用户手工控制数据搬运和计算流程,入门较高,但开发方式比较灵活,能够充分挖掘硬件能力,在性能上有一定的优势。

1.2 AI CPU算子

以下几种场景下,可使用AI CPU方式实现自定义算子:

  • 不适合跑在AI Core上的算子,例如非矩阵类的复杂计算,逻辑比较复杂的分支密集型算子等。

    例如,Dump、profiling等控制算子,Queue、Stack等资源状态类算子,TopK、Where等检索类算子。

  • AI Core不支持的算子,算子需要某些数据类型,但AI Core不支持,例如Complex32、Complex64。

  • 某些场景下,为了快速打通网络在昇腾AI处理器的执行流程,在TBE实现自定义算子较为困难的情况下,可通过自定义AI CPU算子进行功能调测,提升调测效率。功能调通之后,后续性能调测过程中再将AI CPU自定义算子转换为TBE算子实现。

2. TIK

TIK(Tensor Iterator Kernel)是一种基于Python语言的动态编程框架,呈现为一个Python模块。 开发者可以通过调用TIK提供的API基于Python语言编写自定义算子,然后TIK编译器会编译为适配昇腾AI处理器应用程序的二进制文件。

TIK编程模型

使用TIK进行编程的过程,如下图所示,用户调用TIK API编写算子对应的Python程序后,TIK会将其转化为TIK DSL(TIK DSL是一种DSL语言,它可以在比CCE更高的抽象层次上定义CCEC程序的行为),经过编译器编译后生成CCEC文件(CCEC代码目前对于TIK编程人员无法感知),再经过CCE编译器编译后生成可运行在昇腾AI处理器上的应用程序。
在这里插入图片描述

3. TIK2

TIK2是一种使用C/C++作为前端语言的编程框架,开发者可以使用TIK2提供的API编写自定义算子,并通过CCEC编译器对自定义算子进行编译,生成可运行在昇腾AI处理器上的应用程序。

TIK与TIK2开发方式对比

算子开发方式TIKTIK2
语言PythonC/C++
计算单元AI CoreAI Core
编程模型并行化:提供串行化编程体系,方便编写算子,TIK工具自动对计算过程并行化,实现高性能。
自动内存管理:程序员在编写算子的时候不用感知和管理地址,编译器会做好内存分配。
针对不同的硬件体系结构,抽象出统一的并行计算架构,屏蔽硬件差异;基于抽象的编程架构,可以快速开发出高效的算子。
调试方式使用TIK调试器进行功能调试,可快速定位功能问题。使用gdb工具在CPU侧进行功能调试,调试后可无缝移植到AI处理器运行
APIAPI丰富灵活,提供高级参数,满足高阶用户需求。多层级API封装,从简单到灵活,兼顾易用与高效。

四、AI Core架构

AI Core是昇腾AI处理器的计算核心,可以看成是一个相对简化的现代微处理器的基本架构,负责执行矩阵、向量、标量计算密集的算子任务。它包括了三种基础计算资源:矩阵计算单元(Cube Unit)、向量计算单元(Vector Unit)和标量计算单元(Scalar Unit)。这三种计算单元各司其职,形成了三条独立的执行流水线,在系统软件的统一调度下互相配合达到优化的计算效率。AI Core中包含计算单元存储单元控制单元搬运单元
在这里插入图片描述

1. 计算单元

计算单元是AI Core中提供强大算力的核心单元,相当于AI Core的主力军,主要包括:Cube Unit(矩阵计算单元)、Vector Unit(向量计算单元)和Scalar Unit(标量计算单元),完成AI Core中不同类型的数据计算。

计算单元描述
CubeCube负责执行矩阵运算。Cube每次执行可以完成一个fp16的1616与1616的矩阵乘,例如C=AxB,如果是int8输入,则一次完成16x32与32x16的矩阵乘。其中A来源于L0A,B来源于L0B,L0C存储矩阵乘的结果和中间结果。
VectorVector负责执行向量运算。其算力低于Cube,但灵活度高于Cube(如支持数学中的求倒数,求平方根等)。
ScalarScalar负责各类型的标量数据运算和程序的流程控制。功能上可以看做一个小CPU,完成整个程序的循环控制、分支判断、Cube/Vector等指令的地址和参数计算以及基本的算术运算等。

2. 存储单元

AI Core需要把外部存储中的数据加载到内部存储中,才能完成相应的计算。

2.1 外部存储

通常,AI Core的外部存储包括L2、HBM、DDR等。

2.2 内部存储

AI Core的内部存储,统称为Local Memory,主要包括:L1 Buffer(L1缓冲区),L0 Buffer(L0缓冲区),Unified Buffer(统一缓冲区)和Scalar Buffer(标量缓冲区)。

2.3 存储单元分类

存储单元描述
MTEAI Core上有多个MTE(Memory Transfer Engine,存储转换引擎),包括MTE1、MTE2、MTE3。MTE是数据搬运单元,负责AI Core内部数据在不同Buffer之间的数据读写管理和格式转换的操作,比如填充(padding)、转置(transpose)、3D图像转2D矩阵(Img2Col)等。
BIUBIU (Bus Interface Unit,总线接口单元),是AI Core的“大门”,负责AI Core与总线交互。BIU是AI Core从外部(L2缓冲区/双倍速率内存DDR/高速宽带内存HBM)读取数据以及往外写数据的出入口,负责把AI Core的读写请求转换为总线上的请求并完成协议交互等工作。
L1 BufferL1缓冲区,通用内部存储,是AI Core内比较大的一块数据中转区,可暂存AI Core中需要反复使用的一些数据从而减少从总线读写的次数。某些MTE的数据格式转换功能,要求源数据必须位于L1 Buffer,例如3D图像转2D矩阵(Img2Col)操作。
L0A Buffer / L0B BufferCube指令的输入
L0C BufferCube指令的输出,但进行累加计算的时候,也是输入的一部分。
Unified Buffer统一缓冲区,向量和标量计算的输入和输出。
Scalar Buffer标量计算的通用缓冲区,作为GPR(通用寄存器,General-Purpose Register)不足时的补充。
GPR通用寄存器(General-Purpose Register),标量计算的输入和输出。应用开发工程师不需要具体关注这些寄存器。由系统内部实现封装,程序访问Scalar Buffer并执行标量计算的时候,系统内部自动实现Scalar Buffer和GPR之间的同步。
SPR专用寄存器(Special-Purpose Register),AI Core的一组配置寄存器。通过修改SPR的内容可以修改AI Core的部分计算行为。

2.4 存储单元大小

不同类型的昇腾AI处理器,存储单元大小不同,用户可通过get_soc_spec接口获取。

2.4.1 函数原型

def get_soc_spec(key)

2.4.2 参数说明

参数名类型说明
keystring类型获取硬件信息,包含:“SOC_VERSION”“AICORE_TYPE”“CORE_NUM”“UB_SIZE”“L2_SIZE”“L1_SIZE”“CUBE_SIZE”“L0A_SIZE”“L0B_SIZE”“L0C_SIZE”“SMASK_SIZE”

2.4.3 返回值

根据输入的key返回对应的值:

  • SOC_VERSION:返回标识SOC类型的字符串。
  • AICORE_TYPE:返回Core的类型,有AiCoreVectorCore两种返回值。
  • CORE_NUM:返回核数,int类型。
  • UB_SIZE:返回UB大小,int类型,单位Byte。
  • L2_SIZE:返回L2大小,int类型,单位Byte。
  • L1_SIZE:返回L1大小,int类型,单位Byte。
  • CUBE_SIZE:返回CUBE大小,tuple类型,如(16,16,16),单位为Byte。
  • L0A_SIZE:返回L0A大小,int类型,单位为Byte。
  • L0B_SIZE:返回L0B大小,int类型,单位为Byte。
  • L0C_SIZE:返回L0C大小,int类型,单位为Byte。
  • SMASK_SIZE:返回Smask buffer大小,int类型,单位为Byte。

2.4.4 示例代码

实际调用时,将变量soc_version的值修改为实际的昇腾AI处理器型号。

import tbe
soc_version="xxx"
tbe.common.platform.set_current_compile_soc_info(soc_version)
tbe.common.platform.get_soc_spec("CORE_NUM")

2.5 指令与存储访问关系

在这里插入图片描述

上图的存储单元是软件层面概念,其中:

  • Scalar Buffer对应硬件存储单元Scalar Buffer。
  • Unified Buffer对应硬件存储单元Unified Buffer。
  • L1 Buffer对应硬件存储单元L1 Buffer。
  • L1Out Buffer为从L0C上抽象出来的存储Cube计算输出数据的存储单元

2.6 QuePosition与硬件存储单元映射关系

QuePosition硬件存储单元
GMGlobal Memory
A1L1 Buffer
A2L0A Buffer
B1L1 Buffer
B2L0B Buffer
CO1L0C Buffer
CO2Unified Buffer

2.7 硬件存储单元对齐

不同scope的对齐要求,如下表所示:

scope对齐要求
Unified Buffer昇腾310 AI处理器,要求32Byte对齐;昇腾910 AI处理器,要求32Byte对齐;昇腾310P AI处理器AI Core,要求32Byte对齐;昇腾310P AI处理器Vector Core,要求32Byte对齐
L1 Buffer512Byte对齐
L1OUT Bufferhalf类型数据要求512Byte对齐;float/int32_t/uint32_t类型数据要求1024Byte对齐
Global Memory暂无对齐要求

3. 控制单元

控制单元为整个计算过程提供了指令控制,相当于AI Core的司令部,负责整个AI Core的运行。系统控制模块(System Control)负责指挥和协调AI Core的整体运行模式,配置参数和实现功耗控制等。当指令通过指令发射模块(Instruction Dispatch)顺次发射出去后,根据指令的不同类型,将会分别被发送到矩阵运算队列(Cube Queue)、向量运算队列(Vector Queue)和存储转换队列(MTE Queue)。指令执行过程中,可以提前预取后续指令,并一次读入多条指令进入缓存,提升指令执行效率。多条指令从系统内存通过总线接口(BIU)进入到AI Core的指令缓存模块(Instruction Cache)中等待,后续硬件快速自动解码或运算。指令被解码后便会被导入标量指令处理队列(Scalar PSQ)中,实现地址解码与运算控制。

3.1 控制单元分类

AI Core包含的控制单元,如下表所示。

控制单元描述
系统控制模块(System Control)外部的Task Scheduler控制和初始化AI Core的配置接口, 配置PC、Para_base、BlockID等信息,具体功能包括:Block执行控制、Block执行完之后中断和状态申报、执行错误状态申报等。
指令缓存模块(Instruction Cache)AI Core内部的指令Cache, 具有指令预取功能。
标量指令处理队列(Scalar PSQ)Scalar指令处理队列。
指令发射模块(Instruction Dispatch)CUBE/Vector/MTE指令经过Scalar PSQ处理之后,地址、参数等要素都已经配置好,之后Instruction Dispatch单元根据指令的类型,将CUBE/Vector/MTE指令分别分发到对应的指令队列等待相应的执行单元调度执行。
矩阵运算队列(Cube Queue)Cube运算队列。同一个队列里的指令顺序执行,不同队列之间可以并行执行。
向量运算队列(Vector Queue)Vector运算队列。同一个队列里的指令顺序执行,不同队列之间可以并行执行。
存储转换队列(MTE Queue)MTE存储转换队列。同一个队列里的指令顺序执行,不同队列之间可以并行执行。
事件同步模块(Event Sync)用于控制不同队列指令(也叫做不同指令流水)之间的依赖和同步的模块。

3.2 指令队列分类

根据调度分类的不同,可以把指令分类,加上被译码过程直接解释的Scalar指令(缩写为S),可以有6种指令分类:S、V、M、MTE1、MTE2、MTE3。

队列缩写队列名称备注
VVector指令队列用于调度向量指令
MMatrix指令队列用于调度Cube指令
MTE1存储移动指令队列1用于调度如下内存移动指令:L1到L0A/L0B/UB,或者用SPR初始化L0A/L0B Buffer
MTE2存储移动指令队列2用于调度如下内存移动指令:L2/HBM/DDR到L1/L0A/L0B/UB
MTE3存储移动指令队列3用于调度如下内存移动指令:UB到L2/HBM/DDR

除S队列之外,不同队列的指令能够乱序执行,但是队列内部指令为顺序执行,即在满足数据依赖的前提下,指令的物理执行顺序不一定与代码的书写顺序一致。

硬件按照下发顺序,将不同队列的指令分发到相应的队列上执行,昇腾AI处理器提供Barrier、set_flag/wait_flag两种指令,保证队列内部以及队列之间按照逻辑关系执行。

  • Barrier本身是一条指令,用于在队列内部约束执行顺序。其作用是,保证前序队列中所有数据的读写工作全部完成,后序指令才能执行。
  • set_flag/wait_flag为两条指令,在set_flag/wait_flag的指令中,可以指定一对指令队列的关系,表示两个队列之间完成一组“锁”机制,其作用方式为:
    • set_flag:当前序指令的所有读写操作都完成之后,当前指令开始执行,并将硬件中的对应标志位设置为1。
    • wait_flag:当执行到该指令时,如果发现对应标志位为0,该队列的后续指令将一直被阻塞;如果发现对应标志位为1,则将对应标志位设置为0,同时后续指令开始执行。

注意:TBE封装了这种依赖关系,所以应用开发人员不必对Barrier或者Flag进行编程。但应用开发人员仍需要理解这个基本原理,才能通过合适的代码调度,实现更好的同步关系。基于DSL方式进行算子开发无需关注代码调度,DSL提供了自动调度(auto_schedule)机制。

3.3 AI Core指令调度方式

AI Core采用顺序取指令、并行执行指令的调度方式,流水线执行过程如下图所示:
在这里插入图片描述

指令序列被顺序译码。根据指令的类型,有两种可能:

  • 如果指令是Scalar指令,指令会被直接执行。
  • 其他指令,指令会被调度到5个独立的指令队列,然后再分配到某个空间的执行部件执行。

4. 搬运单元

DMA搬运单元,负责在Global Memory和Local Memory之间搬运数据,具体来说,把数据搬运到Local Memory,Vector/Cube计算单元完成数据计算,并把计算结果写回Local Memory,DMA搬出单元把处理好的数据搬运回Global Memory。DMA搬运单元包括:MTE2(Memory Transfer Engine,数据搬入单元),MTE3(数据搬出单元)。

五、核函数

核函数是直接在Device设备端执行的代码。在核函数中,需要为在一个核上执行的代码规定要进行的数据访问和计算操作,当核函数被调用时,多个核将并行执行同一个计算任务。

extern "C" __global__ __aicore__ void add_tik2(__gm__ uint8_t* x, __gm__ uint8_t* y, __gm__ uint8_t* z)
{
}

1. 函数类型限定符

编写核函数

核函数的函数类型限定符,包括 __global____aicore__,其中__global__ 标识核函数,__aicore__ 表示核函数在设备端aicore上执行。

函数类型限定符执行调用备注
global在设备端执行由<<<…>>>来调用必须有一个void返回值类型
aicore在设备端执行仅从设备端调用-

2. 变量类型限定符

指针入参变量统一的类型定义为 __gm__ uint8_t*,Init()函数的入参统一设置为uint8_t*类型的指针,在后续的使用中需要将其转化为实际的指针类型;用户亦可直接传入实际的指针类型。

变量类型限定符内存空间意义
gm驻留在Global Memory上表明该指针变量指向Global Memory上某处内存地址

3. 核函数调用符

#ifndef __CCE_KT_TEST__ 表示核函数在NPU侧运行,核函数通过核函数调用符 <<<...>>> 调用。<<<...>>> 仅在NPU侧调用,在CPU侧直接调用核函数即可。

#ifndef __CCE_KT_TEST__
// call of kernel function
void add_tik2_do(uint32_t blockDim, void* l2ctrl, void* stream, uint8_t* x, uint8_t* y, uint8_t* z)
{
    add_tik2<<<blockDim, l2ctrl, stream>>>(x, y, z);
}
#endif

4. Init()函数实现

constexpr int32_t TOTAL_LENGTH = 8 * 2048;                            // total length of data
constexpr int32_t USE_CORE_NUM = 8;                                   // num of core used
constexpr int32_t BLOCK_LENGTH = TOTAL_LENGTH / USE_CORE_NUM;         // length computed of each core
constexpr int32_t TILE_NUM = 8;                                       // split data into 8 tiles for each core
constexpr int32_t BUFFER_NUM = 2;                                     // tensor num for each queue
constexpr int32_t TILE_LENGTH = BLOCK_LENGTH / TILE_NUM / BUFFER_NUM; // each tile length is seperated to 2 part, due to double buffer

__aicore__ inline void Init(__gm__ uint8_t* x, __gm__ uint8_t* y, __gm__ uint8_t* z)
{
    //获取核函数的输入输出在Global Memory上的内存偏移地址
    // get start index for current core, core parallel
    xGm.SetGlobalBuffer((__gm__ half*)x + block_idx * BLOCK_LENGTH);
    yGm.SetGlobalBuffer((__gm__ half*)y + block_idx * BLOCK_LENGTH);
    zGm.SetGlobalBuffer((__gm__ half*)z + block_idx * BLOCK_LENGTH);
    
    // 通过Pipe内存管理对象为输入输出Queue分配内存
    // pipe alloc memory to queue, the unit is Bytes
    pipe.InitBuffer(inQueueX, BUFFER_NUM, TILE_LENGTH * sizeof(half));
    pipe.InitBuffer(inQueueY, BUFFER_NUM, TILE_LENGTH * sizeof(half));
    pipe.InitBuffer(outQueueZ, BUFFER_NUM, TILE_LENGTH * sizeof(half));
}

解释说明

  1. 数据整体长度TOTAL_LENGTH为8* 2048,平均分配到8个核上运行,每个核上处理的数据大小BLOCK_LENGTH为2048。
  2. block_idx为核的逻辑ID,(__gm__ half*)x + block_idx * BLOCK_LENGTH 即为单核处理程序中x在Global Memory上的内存偏移地址。注意,因为Init函数的入参统一设置为 uint8_t*,这里需要强转成具体的数据类型 (__gm__ half*),再进行偏移。

4.1 BLOCK

// 数据整体长度
// total length of data
constexpr int32_t TOTAL_LENGTH = 8 * 2048;

// 使用多核
// num of core used
constexpr int32_t USE_CORE_NUM = 8;

//每个核处理数据的大小
// length computed of each core
constexpr int32_t BLOCK_LENGTH = TOTAL_LENGTH / USE_CORE_NUM; 

block_num

block_num默认取值为1,即不分核;而采用分核并行时,其取值上限为65535,用户需要保证block_num的值不超过此阈值。

在for_range的原型定义里,用户通过设置参数block_num来实现分核并行,简单代码示例如下:

with tik_instance.for_range( 0, 10, block_num=10) as i:

for_range循环中的表达式会被作用在10个执行实例上,最终10个执行实例会被分配到10个核上并行运行,每个核拿到一个执行实例和一个不同的Block ID。如果当前可用的核的数量小于10,则执行实例会在当前可用的核上分批调度执行;如果当前可用的核的数量大于等于10,则会根据实际情况调度执行,实际运行的核数可能小于等于10。

一个算子中只能调用一次for_range实现分核,即设置block_num >=2,不允许多次开启多核。

CORE_NUM

用户可以通过get_soc_spec接口获取AI Core的个数。

# 请根据实际昇腾AI处理器型号进行设置
soc_version="xxx"
# 设置昇腾AI处理器的型号及目标核的类型
tbe.common.platform.set_current_compile_soc_info(soc_version) 
tbe.common.platform.get_soc_spec("CORE_NUM") # 使用该接口前需要先设置芯片类型

为保证负载均衡,block_num一般尽量设置为实际核数量的倍数。假设芯片内含32个AI Core,假如一个张量的形状为(16, 2, 32, 32, 32),如果以张量的第一维度(最外层)进行分核,则只能绑定16个核。此时,可通过将张量的第一维度和第二维度合并,使得最外层的长度变成32,以此将任务均摊到32个AI Core上,使用尽可能多的核并行处理。需要注意的是,顾及后端内存自动分配机制限制,用户实施分核并行时必须从最外层开始做维度合并。

4.2 Tiling

对于单核上的处理数据,可以进行数据切块(Tiling)。

// split data into 8 tiles for each core
constexpr int32_t TILE_NUM = 8;

// tensor num for each queue
constexpr int32_t BUFFER_NUM = 2;

// each tile length is seperated to 2 part, due to double buffer
constexpr int32_t TILE_LENGTH = BLOCK_LENGTH / TILE_NUM / BUFFER_NUM; 

5. Process()函数实现

基于矢量编程范式,将核函数的实现分为3个基本任务:CopyIn,Compute,CopyOut。

__aicore__ inline void Process()
{
    // loop count need to be doubled, due to double buffer
    constexpr int32_t loopCount = TILE_NUM * BUFFER_NUM;
    // tiling strategy, pipeline parallel
    for (int32_t i = 0; i < loopCount; i++) {
        CopyIn(i);
        Compute(i);
        CopyOut(i);
    }
}

核函数内通过数据切块(Tiling),实现流水线之间的并行。举例来说,将单核处理数据分成n份,使用progress0processn-1表示处理第1n个数据切片。progress0经过CopyIn Stage之后进入Compute Stage,CopyIn即可以处理progress1,做到了流水线间并行。根据编程范式上面的算法分析,将整个计算拆分成三个Stage,用户单独编写每个Stage的代码,三阶段流程示意图如下:
在这里插入图片描述

5.1 Stage1:CopyIn函数实现。

  1. 使用DataCopy接口将GlobalTensor数据拷贝到LocalTensor。
  2. 使用EnQue将LocalTensor放入VecIn的Queue中。
__aicore__ inline void CopyIn(int32_t progress)
{
    // alloc tensor from queue memory
    LocalTensor<half> xLocal = inQueueX.AllocTensor<half>();
    LocalTensor<half> yLocal = inQueueY.AllocTensor<half>();
    // copy progress_th tile from global tensor to local tensor
    DataCopy(xLocal, xGm[progress * TILE_LENGTH], TILE_LENGTH);
    DataCopy(yLocal, yGm[progress * TILE_LENGTH], TILE_LENGTH);
    // enque input tensors to VECIN queue
    inQueueX.EnQue(xLocal);
    inQueueY.EnQue(yLocal);
}

5.2 Stage2:Compute函数实现。

  1. 使用DeQue从VecIn中取出LocalTensor。
  2. 使用TIK2接口Add完成矢量计算。
  3. 使用EnQue将计算结果LocalTensor放入到VecOut的Queue中。
  4. 使用FreeTensor将不再使用的LocalTensor进行回收。
__aicore__ inline void Compute(int32_t progress)
{
    // deque input tensors from VECIN queue
    LocalTensor<half> xLocal = inQueueX.DeQue<half>();
    LocalTensor<half> yLocal = inQueueY.DeQue<half>();
    LocalTensor<half> zLocal = outQueueZ.AllocTensor<half>();
    // call Add instr for computation
    Add(zLocal, xLocal, yLocal, TILE_LENGTH);
    // enque the output tensor to VECOUT queue
    outQueueZ.EnQue<half>(zLocal);
    // free input tensors for reuse
    inQueueX.FreeTensor(xLocal);
    inQueueY.FreeTensor(yLocal);
}

5.3 Stage3:CopyOut函数实现。

  1. 使用DeQue接口从VecOut的Queue中取出LocalTensor。
  2. 使用DataCopy接口将LocalTensor拷贝到GlobalTensor上。
  3. 使用FreeTensor将不再使用的LocalTensor进行回收。
 __aicore__ inline void CopyOut(int32_t progress)
{
    // deque output tensor from VECOUT queue
    LocalTensor<half> zLocal = outQueueZ.DeQue<half>();
    // copy progress_th tile from local tensor to global tensor
    DataCopy(zGm[progress * TILE_LENGTH], zLocal, TILE_LENGTH);
    // free output tensor for reuse
    outQueueZ.FreeTensor(zLocal);
}

六、Queue通信和同步

任务间通信和同步

不同的流水任务之间存在数据依赖,需要进行数据传递。TIK2中使用Queue队列完成任务之间的数据通信和同步,提供EnQue、DeQue等基础API。

1. QuePosition逻辑位置

Queue队列管理NPU上不同层级的物理内存时,用一种抽象的逻辑位置 (QuePosition) 来表达各个级别的存储(Storage Scope),代替了片上物理存储的概念,开发者无需感知硬件架构,达到隐藏芯片架构的目的。Queue类型包括:VECIN、VECOUT、A1、A2、B1、B2、CO1、CO2,其中VECIN、VECOUT主要用于矢量编程,具体说明参见[矢量编程](javascript:😉,A1、A2、B1、B2、CO1、CO2用于矩阵编程,具体说明参见[矩阵编程](javascript:😉。

TIK2使用GLobalTensorLocalTensor 作为数据的基本操作单元,它是各种指令API直接调用的对象,也是数据的载体。

2. 矢量编程

矢量编程中使用到的逻辑位置(QuePosition)定义如下:

  • 搬入数据的存放位置:VECIN;
  • 搬出数据的存放位置:VECOUT。

由流水任务设计可知,矢量编程主要分为CopyIn、Compute、CopyOut三个任务。

  • CopyIn任务中将输入数据从Global内存搬运至Local内存后,需要使用EnQue将LocalTensor放入VECIN的Queue中;
  • Compute任务等待VECIN的Queue中LocalTensor出队之后才可以完成矢量计算,计算完成后使用EnQue将计算结果LocalTensor放入到VECOUT的Queue中;
  • CopyOut任务等待VECOUT的Queue中LocalTensor出队,再将其拷贝到Global内存。这样 ,Queue队列就完成了三个任务间的数据通信和同步。
    在这里插入图片描述

具体流程和流程图如下:

  1. Stage1:CopyIn任务。
    1. 使用DataCopy接口将GlobalTensor数据拷贝到LocalTensor。
    2. 使用EnQue将LocalTensor放入VECIN的Queue中。
  2. Stage2:Compute任务。
    1. 使用DeQue从VECIN中取出LocalTensor。
    2. 使用TIK2接口完成矢量计算。
    3. 使用EnQue将计算结果LocalTensor放入到VECOUT的Queue中。
  3. Stage3:CopyOut任务。
    1. 使用DeQue接口从VECOUT的Queue中去除LocalTensor。
    2. 使用DataCopy接口将LocalTensor拷贝到GlobalTensor上。

3. 矩阵编程

由流水任务设计可知,矩阵编程主要分为CopyIn,Split,Compute,Aggregate,CopyOut这5个任务。任务间进行数据传递时会使用到的逻辑位置示意图如下:
在这里插入图片描述
在这里插入图片描述

上图中逻辑位置(QuePosition)定义如下:

  • 搬入数据的存放位置:A1,用于存放整块A矩阵,可类比CPU多级缓存中的二级缓存;

  • 搬入数据的存放位置:B1,用于存放整块B矩阵,可类比CPU多级缓存中的二级缓存;

  • 搬入数据的存放位置:A2,用于存放切分后的小块A矩阵,可类比CPU多级缓存中的一级缓存;

  • 搬入数据的存放位置:B2,用于存放切分后的小块B矩阵,可类比CPU多级缓存中的一级缓存;

  • 结果数据的存放位置:CO1,用于存放小块结果C矩阵,可理解为Cube Out;

  • 结果数据的存放位置:CO2,用于存放整块结果C矩阵,可理解为Cube Out;

  • 搬入数据的存放位置:VECIN,用于矢量计算,是否使用根据实际业务需求;

  • 搬出数据的存放位置:VECOUT,用于矢量计算,是否使用根据实际业务需求。

具体任务之间的交互流程和流程图如下。

  1. Stage1:CopyIn任务。
    1. 使用DataCopy接口将GlobalTensor数据拷贝到LocalTensor。
    2. 使用EnQue将LocalTensor放入A1/B1的Queue中。
  2. Stage2:Split任务。
    1. 使用DeQue从A1/B1中取出LocalTensor。
    2. 使用TIK2接口将LocalTensor从A1/B1中搬运到矩阵计算单元。
    3. 使用EnQue将计算结果LocalTensor放入到A2/B2的Queue中。
  3. Stage3:Compute任务。
    1. 使用DeQue从A2/B2中取出LocalTensor。
    2. 使用TIK2接口完成矩阵计算。
    3. 使用EnQue将计算结果LocalTensor放入到CO1的Queue中。
  4. Stage4:Aggregate任务。
    1. 使用DeQue从CO1中取出LocalTensor。
    2. 使用TIK2接口拷贝结果矩阵到CO2。
    3. 使用EnQue将计算结果LocalTensor放入到CO2的Queue中。
  5. Stage5:CopyOut任务。
    1. 使用DeQue接口从CO2的Queue中去除LocalTensor。
    2. 使用DataCopy接口将LocalTensor拷贝到GlobalTensor上。

4. TQue

4.1 EnQue()

将Tensor/TBufHandle push到队列。

4.2 DeQue()

将TBufHandle/Tensor从队列中取出,用于后续处理。

七、Pipe内存管理

通过统一的内存管理模块(Pipe)对任务间数据传递进行管理。

  • 内存初始化:Pipe作为片上内存管理者,通过 InitBuffer() 接口对外提供Queue内存初始化功能,开发者可以通过该接口为指定的Queue分配内存。
  • 分配内存:Queue队列内存初始化完成后,需要使用内存时,通过调用 AllocTensor()来为 LocalTensor分配内存,当创建的LocalTensor完成相关计算无需再使用时,再调用 FreeTensor() 来回收 LocalTensor 的内存。
    在这里插入图片描述

编程过程中使用到的临时变量内存同样通过Pipe进行管理。临时变量可以使用TBuf数据结构来申请指定QuePosition上的存储空间。使用TBuf申请的内存空间只能参与计算,无法执行Queue队列的入队出队操作。具体的接口使用说明请参考TBuf。

InitBuffer()

为指定的Queue分配内存。

八、Vector矢量编程范式

Vector矢量编程范式把算子的实现流程分为3个基本任务:CopyIn,Compute,CopyOut。CopyIn负责搬入操作,Compute负责矢量指令计算操作,CopyOut负责搬出操作。
在这里插入图片描述

九、Cube矩阵编程范式

Cube矩阵编程范式把算子的实现流程分为5个基本任务:CopyIn,Split,Compute,Aggregate,CopyOut。CopyIn负责搬入操作,Split负责数据切分操作,Compute负责矩阵指令计算操作,Aggregate负责数据汇聚操作,CopyOut负责搬出操作。
在这里插入图片描述

十、术语解析

1. GlobalTensor与LocalTensor

TIK2使用GlobalTensor和LocalTensor作为数据的基本操作单元,它是各种指令API直接调用的对象,也是数据的载体。详见数据结构定义。

采用分核并行时,L2/HBM/DDR(统称Global Memory)对每个核均可见。

1.1 GlobalTensor

GlobalTensor

存放全局数据,支持QuePosition为GM。

1.2 LocalTensor

LocalTensor

存放本地数据,支持QuePosition为A1, A2, B1, B2, CO1, CO2, SHM。

2. 数据排布格式(format)

数据排布格式

NCHW和NHWC

在深度学习领域,多维数据通过多维数组存储,比如卷积神经网络的特征图(Feature Map)通常用四维数组保存,即4D,4D格式解释如下:

  • N:Batch数量,例如图像的数目。
  • H:Height,特征图高度,即垂直高度方向的像素个数。
  • W:Width,特征图宽度,即水平宽度方向的像素个数。
  • C:Channels,特征图通道,例如彩色RGB图像的Channels为3。

由于数据只能线性存储,因此这四个维度有对应的顺序。不同深度学习框架会按照不同的顺序存储特征图数据,比如Caffe,排列顺序为[Batch, Channels, Height, Width],即NCHW。TensorFlow中,排列顺序为[Batch, Height, Width, Channels],即NHWC。
在这里插入图片描述

以一张格式为RGB的图片为例,如上图所示。NCHW中,C排列在外层,实际存储的是“RRRRRRGGGGGGBBBBBB”,即同一通道的所有像素值顺序存储在一起;而NHWC中C排列在最内层,实际存储的则是“RGBRGBRGBRGBRGBRGB”,即多个通道的同一位置的像素值顺序存储在一起。

3. 数据类型(dtype)

Tensor对象的数据类型。

取值范围:float16, float32, int8, int16, int32, uint8, uint16, bool等。

4. 形状(Shape)

张量的形状,以(D0, D1, … ,Dn-1)的形式表示,D0到Dn是任意的正整数。

如形状(3,4)表示第一维有3个元素,第二维有4个元素,(3,4)表示一个3行4列的矩阵数组。

张量形状描述
1(0,)0维张量,也是一个标量
[1,2,3](3,)1维张量
[[1,2],[3,4]](2, 2)2维张量
[[[1,2],[3,4]], [[5,6],[7,8]]](2, 2, 2)3维张量

假设有一些照片,每个像素点都由红/绿/蓝3色组成,即shape里面3的含义,照片的宽和高都是20,也就是20*20=400个像素,总共有4张的照片,这就是shape=(4, 20, 20, 3)的物理含义。
在这里插入图片描述

5. 轴(axis)

轴是相对shape来说的,轴代表张量的shape的下标,比如张量a是一个5行6列的二维数组,即shape是(5,6),则axis=0表示是张量中的第一维,即行。axis=1表示是张量中的第二维,即列。

例如张量数据[[[1,2],[3,4]], [[5,6],[7,8]]],Shape为(2,2,2),则轴0代表第一个维度的数据即[[1,2],[3,4]]与[[5,6],[7,8]]这两个矩阵,轴1代表第二个维度的数据即[1,2]、[3,4]、[5,6]、[7,8]这四个数组,轴2代表第三个维度的数据即1,2,3,4,5,6,7,8这八个数。

轴axis可以为负数,此时表示是倒数第axis个维度。

N维Tensor的轴有:0 , 1, 2,……,N-1。
在这里插入图片描述

6. double buffer机制

执行于AI Core上的指令队列主要包括如下几类,即矩阵运算队列(Cube Queue)、向量运算队列(Vector Queue)和存储转换队列(MTE Queue)。不同指令队列间的相互独立性和可并行执行特性,是double buffer优化机制的基石。

6.1 Unified Buffer统一缓冲区

一个完整的数据搬运和计算过程,MTE2将数据从Global Memory搬运到Unified Buffer,Vector完成计算后将结果写回Unified Buffer,最后由MTE3将计算结果搬回Global Memory。Vector所有计算的源数据以及目标数据都要求存储在Unified Buffer中,并要求32Byte对齐。Unified Buffer数据搬运与Vector计算过程,如下图所示:
在这里插入图片描述

在此过程中,数据搬运与Vector计算串行执行,Vector计算单元无可避免存在资源闲置问题。举例而言,若MTE2、Vector、MTE3三阶段分别耗时t,则Vector的时间利用率仅为1/3,等待时间过长,Vector利用率严重不足。

6.2 double buffer

为减少Vector等待时间,double buffer机制将Unified Buffer一分为二,即UB_A、UB_B。如下图所示,当Vector对UB_A中数据进行读取和计算时,MTE2可将下一份数据搬入UB_B中;而当Vector切换到计算UB_B时,MTE3将UB_A的计算结果搬出,而MTE2则继续将下一份数据搬入UB_A中。由此,数据的进出搬运和Vector计算实现并行执行,Vector闲置问题得以有效缓解。double buffer机制,如下图所示:
在这里插入图片描述

总体来说,double buffer是基于MTE指令队列与Vector指令队列的独立性和可并行性,通过将数据搬运与Vector计算并行执行以隐藏数据搬运时间并降低Vector指令的等待时间,最终提高Vector单元的利用效率,用户可以通过在for_range中设置参数thread_num来实现数据并行,简单代码示例如下:

with tik_instance.for_range(0, 10, thread_num=2) as i:

注意事项

多数情况下,采用double buffer能有效提升Vector的时间利用率,缩减算子执行时间。然而,double buffer机制缓解Vector闲置问题并不代表它总能带来整体的性能提升。例如:

  • 当数据搬运时间较短,而Vector计算时间显著较长时,由于数据搬运在整个计算过程中的时间占比较低,double buffer机制带来的性能收益会偏小。
  • 又如,当原始数据较小且Vector可一次性完成所有计算时,强行使用double buffer会降低Vector计算资源的利用率,最终效果可能适得其反。

因此,double buffer的性能收益需综合考虑Vector算力、数据量大小、搬运与计算时间占比等多种因素。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/993273.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL——备份和还原

备份 热备 即MySQL服务在运行的时候进行的备份 mysqldump命令 mysqldump --databases db1 db2 db3 > dump.sql mysqldump -uroot -pSanchuang1234# --all-databases >all_db.sql mysqldump -uroot -pSanchuang123# --databases TENNIS >/backup/tennis.sql mysq…

分享一个python基于数据可视化的智慧社区服务平台源码

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人七年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、微信小程序、爬虫、大数据等&#xff0c;大家有这一块的问题可以一起交流&#xff01; &#x1…

【rust/egui】(九)使用painter绘制一些图形—基本使用

说在前面 rust新手&#xff0c;egui没啥找到啥教程&#xff0c;这里自己记录下学习过程环境&#xff1a;windows11 22H2rust版本&#xff1a;rustc 1.71.1egui版本&#xff1a;0.22.0eframe版本&#xff1a;0.22.0上一篇&#xff1a;这里 painter 定义pub struct Painter {///…

领域驱动设计:DDD分层架构

文章目录 DDD 分层架构DDD 分层架构最重要的原则DDD 分层架构推动架构演进三层架构如何演进到 DDD 分层架构 微服务架构模型有好多种&#xff0c;例如整洁架构、CQRS 和六边形架构等等。每种架构模式虽然提出的时代和背景不同&#xff0c;但其核心理念都是为了设计出“高内聚低…

gpt测试

已知a地一石头售价80&#xff0c;b地售价112&#xff0c;小明初始资金8000&#xff0c;在a地全仓购入后&#xff0c;去b地出售&#xff0c;然后小明又回到a地&#xff0c;再次全仓购入然后去b地出售&#xff0c;这样继续出售10次后&#xff0c;小明有多少钱&#xff1f;石头是不…

Java中wait和notify详解

线程的调度是无序的&#xff0c;随机的&#xff0c;但是也是有一定的需求场景&#xff0c;希望能够有序执行&#xff0c;join算是一种控制顺序的方式&#xff08;功能有限&#xff09;——》一个线程执行完&#xff0c;才能执行另一个线程&#xff01; 本文主要讲解的&#xf…

C++---类和对象

这里写目录标题 封装简介语法二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录二级目录 封装 简介语法 类 &#xff1a;抽象的 共性的 对象&#xff1a;实例化的 具体的 个性的 封装 就是把属性和行为放在一起 加一些访问权…

CCRC-PIPA个人信息保护评估师

个人信息保护评估师 (Personal InformationProtec-tion Assessor&#xff0c;简称 “PIPA”) 是由中国网络安全审查技术与认证中心(简称CCRC) 推出的面向个人信息保护领域的培训认证。CCRC-PIPA课程以《个人信息保护法》、法规、部门规章、相关国家标准和行业最佳实践为基础&am…

代理IP在海外SEO优化中有哪些关键作用?

代理IP在海外SEO优化业务中的应用越来越受到企业的重视。它为企业提供了大量不同地区的IP地址&#xff0c;帮助企业模拟不同地区、不同设备的用户行为&#xff0c;有助于更准确地了解当地的搜索引擎规则和优化策略&#xff0c;更好地评估网站的排名和流量。 一、代理IP的优势 …

【Stable Diffusion XL】huggingface diffusers 官方教程解读

文章目录 01 TutorialDeconstruct a basic pipelineDeconstruct the Stable Diffusion pipelineAutopipelineTrain a diffusion model 相关链接&#xff1a; GitHub&#xff1a; https://github.com/huggingface/diffusers 官方教程&#xff1a;https://huggingface.co/docs/di…

第2章_瑞萨MCU零基础入门系列教程之面向过程与面向对象

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…

thinkphp6 入门(6)--中间件是什么 怎么用

一、什么是中间件&#xff1f; 当客户端发送请求至服务器时&#xff0c;HTTP请求会经过多个中间件&#xff0c;最后返回响应给客户端。中间件可以 在请求到达目标控制器或动作之前对请求进行操作 可以在响应离开目标控制器或动作之前对响应进行操作 二、中间件的作用 我们可…

【Python】conda虚拟环境下使用pyinstaller打包程序为exe

文章目录 一、为什么要用conda虚拟环境二、pyinstaller用法2.1 安装 PyInstaller2.2 基本用法打包一个 Python 脚本2.21 打包一个 Python 项目2.22 打包选项 2.3 打包依赖项2.31 导出依赖项列表2.32 配置依赖项 2.4 自定义打包选项2.5 打包完成后的文件2.6 注意事项 三、打包示…

RabbitMQ 知识点解读

1、AMQP 协议 1.1、AMQP 生产者的流转过程 当客户端与Broker 建立连接的时候&#xff0c;会调用factory .newConnection 方法&#xff0c;这个方法会进一步封装成Protocol Header 0-9-1 的报文头发送给Broker &#xff0c;以此通知Broker 本次交互采用的是AMQPO-9-1 协议&…

文件上传漏洞案例

目录 1.案例一 1&#xff09;案例源码 2&#xff09;创建web.php文件 3&#xff09;使用抓包软件 2.案例二 1&#xff09;案例代码 2&#xff09; 案例分析 3&#xff09;copy命令生成图片马 4&#xff09;上传图片马到服务器 5&#xff09;解析 文件图片 3.案例三 …

Error running ‘xxx‘: Command line is too long. Shorten command line for xxxx

完整报错信息&#xff1a;Error running ArticleFreemarkerTest.test: Command line is too long. Shorten command line for ArticleFreemarkerTest.test or also for JUnit default configuration. 翻译为运行“ArticleFreemarkerTest.test”时出错&#xff0c;命令行太长。…

计网第四章(网络层)(八)

在第七节&#xff08;计网第四章&#xff08;网络层&#xff09;&#xff08;七&#xff09;_永无魇足的博客-CSDN博客&#xff09;我们总结了路由信息协议RIP。在最后我们提到了RIP协议有坏消息传的慢的问题&#xff0c;这是距离向量算法的本质决定的&#xff0c;所以这种问题…

成都青溪电商:抖店精选联盟怎么绑定?

随着抖音平台的快速发展&#xff0c;越来越多的商家希望利用抖音的流量红利来实现品牌曝光和销量增长。抖音精选联盟作为抖音平台的重要合作计划&#xff0c;为商家提供了更多的机会和资源支持。下面将详细介绍如何绑定抖店精选联盟并讨论解绑情况。 1.绑定抖店选定联盟 满足入…

蓝桥杯官网练习题(数字三角形)

题目描述 上图给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径&#xff0c;把路径上面的数加起来可以得到一个和&#xff0c;你的任务就是找到最大的和。 路径上的每一步只能从一个数走到下一层和它最近的左边的那个数或者右 边的那个数。此外&a…

AlwaysUp10.5.0.93安装和使用说明

安装包 安装 解压 双击exe 点击完成&#xff0c;完成安装 安装完成自动启动程序