开发者
资源
[object Object]

基于Ascend C方式实现带有Tiling的算子的开发流程如下图所示。

图 1 算子开发流程[object Object][object Object]

[object Object]

本样例为输入数据在核间均分、核内均分场景。本样例的Tiling策略为:数据整体长度TOTAL_LENGTH为8 * 2048,数据平均分配到8个核上运行,每个核上计算的数据长度BLOCK_LENGTH为2048,将单核上的数据切分成16块(此处切分成16块仅用来作为Tiling的样例,并不代表性能最佳,仅供参考),每块数据的长度TILE_LENGTH为128。数据切分示意如下图所示:

图 2 数据切分示意图[object Object][object Object]

通过以上分析,得到Ascend C Add算子的设计规格如下:

  • 算子类型(OpType):Add

  • 算子输入输出:

    表 1 Add算子输入输出规格

    [object Object][object Object]

    [object Object]
  • 核函数名称:tiling_strategy_custom

  • 使用的主要接口:

    • DataCopy:数据搬移接口
    • Add:矢量基础算术接口
    • EnQue、DeQue等接口:Queue队列管理接口
  • 算子实现文件名称:tiling_strategy.asc

[object Object]

前述场景中算子的输入和输出均为固定shape,然而在实际的算子开发场景中,这些信息是支持动态变化的,场景会更加灵活和复杂。动态shape场景下,输入的shape是未知的。一些与输入shape相关的变量(比如每次搬运的块大小等),需要通过Tiling计算出来,然后传递到kernel侧,kernel侧使用该参数进行后续的计算。

具体实现方式为:分析设计Tiling参数、定义Tiling结构体,在Host侧通过上下文获取输入输出的shape信息,根据shape信息,计算Tiling参数并设置到对应的Tiling结构体中;通过核函数入口参数将Tiling信息传入核函数,在核函数内通过解析Tiling结构体,获取并使用相关参数来实现核函数内部逻辑,详细介绍请参考。本节将以上述分析中的切分策略为例,说明如何实现Tiling。

基于本节的切分策略,Tiling需要定义如下参数:

  • blockLength:每个核的计算数据长度;
  • tileNum:每个核需要计算的数据块个数;
  • tileLength:每个核内每个数据块的长度。

根据确定的Tiling参数,使用C++语法定义TilingData结构体,代码如下。

[object Object]

接下来完成Tiling参数的计算。由于每个核内数据被切分为16块,根据使用的核数和核内切分数,计算Tiling参数,并写入到Tiling结构体内。代码示例如下:

[object Object]

最后,在Host侧调用程序中,调用上述Tiling参数计算函数,计算出相关参数,然后传递到Kernel侧核函数。

[object Object]
[object Object]

Kernel侧算子实现仍遵循,接下来重点介绍本场景中算子类实现的不同点。

  • 设置输入输出Global Tensor的Global Memory内存地址。

    由于本样例中将数据分配到了多个核上进行处理,每个核处理不同的数据,因此不同核要处理的数据在Global Memory上的地址不同,在初始化函数Init中,需要获取单核所需处理的输入输出在Global Memory上的内存偏移地址,并将该偏移地址设置到GlobalTensor中。

    以获取输入x在Global Memory上的内存偏移地址为例,数据整体长度TOTAL_LENGTH为8 * 2048,平均分配到8个核上运行,每个核上处理的数据长度blockLength为2048,调用接口获取当前核的index,x + blockLength * GetBlockIdx()即为单核处理程序中x在Global Memory上的内存偏移地址,获取偏移地址后,使用GlobalTensor类的接口设定该核上Global Memory的起始地址以及长度,具体示意图请参考。代码如下所示:

    [object Object]

    图 3 多核并行处理示意图[object Object][object Object]

  • 通过Pipe内存管理对象为输入输出Queue分配内存。

    对于单核上的处理数据,可以进行数据切块(Tiling),在本示例中,仅作为参考,将单核上的数据(2048个数)切分成16块(并不意味着16块就是性能最优),每块tileLength(128)个数据。数据切分示意图如所示。

    图 4 单核数据切分示意图[object Object][object Object]

    相比,在通过Pipe内存管理对象为输入输出Queue分配内存时,需使用单核内每个数据块的长度tileLength作为分配内存的长度。比如,为输入x的Queue分配内存,可以通过如下代码段实现,Pipe为inQueueX分配了一块大小为tileLength * sizeof(half)个字节的内存块,每个内存块能容纳tileLength(128)个half类型数据。

    [object Object]

具体的初始化函数代码如下:

[object Object]

每个核需要对tileNum个数据块分别进行搬入、计算、搬出处理,因此Process函数内将tileNum作为循环上限。

[object Object]

对应的,每个核内搬入、搬出每个数据块时,需定位到每个数据块所在Global Memory上的内存偏移地址,因此在CopyIn和CopyOut函数内部使用DataCopy接口时,需增加每个数据块的地址偏移。Compute函数没有变化,与相同。

CopyIn函数实现代码如下:

[object Object]

CopyOut函数实现代码如下:

[object Object]