TIK数据搬运

接口原型

对于Vector计算,一般是用Unified Buffer去存放数据,再进行计算,所以整体数据流应该是从Global Memory>Unified Buffer>Global Memory。TIK提供了data_move接口实现Global MemoryUnified Buffer间的数据搬运,函数原型为:

data_move(dst, src, sid, nburst, burst, src_stride, dst_stride, *args, **argv)

在data_move的函数原型中,用户需要着重关注dst、src、nburst、burst、src_stride、dst_stride等6个参数。其中,dst、src分别表示目的操作数与源操作数,也是数据搬运的起始地址;nburst、burst分别表示待搬运数据包含的数据片段数与每个连续片段的长度(单位32 Bytes,即最小访问粒度,称为1个Block);src_stride、dst_stride则分别代表相邻数据片段的间隔(即前 burst 尾与后 burst 头的间隔)。通过以上6个参数,data_move支持连续地址与间隔地址两种搬运模式。

图1 data_move的连续/间隔地址搬运模式

连续地址搬运

连续地址搬运是Tik算子开发中,最常见的数据搬运方式。

from tbe import tik

# 实例化tik_instance对象
tik_instance = tik.Tik()
# 定义一个在gm域的Tensor
data_input_gm = tik_instance.Tensor("int32", (256,), name="data_input_gm", scope=tik.scope_gm)
# 定义一个在ub域的Tensor
data_input_ub = tik_instance.Tensor("int32", (256,), name="data_input_ub", scope=tik.scope_ubuf)
# 使用data_move操作将输入的Tensor从gm搬到ub
tik_instance.data_move(data_input_ub, data_input_gm, 0, 1, 32, 0, 0)
# 对于ub进行一系列指令操作
.............
# 后续的搬出操作

在上述的案例中,我们首先分别在gm和ub空间开辟了一个256长度的,数据类型为int32的Tensor,然后我们执行从gm搬到ub的操作,数据搬运指令中几个参数的实际含义解释如下:

tik_instance.data_move(data_input_ub, data_input_gm, 0, 1, 32, 0, 0)

如下的数据搬运图所示,每个方块表示一个32B的Block,其中存放了8个int32,所以相当于是256个int32点对点进行搬运。

间隔地址搬运

上述表示了连续的数据搬运,其中src_stride和dst_stride都是0,非连续的数据搬运的场景则稍显复杂。

from tbe import tik

tik_instance = tik.Tik()
data_input_gm = tik_instance.Tensor("int32", (256,), name="data_input_gm", scope=tik.scope_gm)
data_input_ub = tik_instance.Tensor("int32", (176,), name="data_input_ub", scope=tik.scope_ubuf)
# 非连续搬运的案例
tik_instance.data_move(data_input_ub, data_input_gm, 0, 4, 4, 4, 2)
.............

如下的数据搬运图可以直观地表示这样的搬运过程:

间隔地址搬运场景比较少见,经常使用的还是连续地址搬运的场景,平常写算子的时候,也不会在burst上直接写数字,而是用:element_size_to_move * DATA_TYPE_SIZE / BLOCK_SIZE_BYTE 去表示:

tik_instance.data_move(data_input_ub, data_input_gm, SID, DEFAULT_NBURST,
                       element_size_to_move * DATA_TYPE_SIZE // BLOCK_SIZE_BYTE,
                       STRIDE_ZERO, STRIDE_ZERO)

其中DEFAULT_NBURST=1,BLOCK_SIZE_BYTE=32,STRIDE_ZERO=0。这样其他用户在阅读TIK算子代码的时候,其中参数的含义会比单纯的数字更加简明易懂。

有偏移的连续地址搬运

在实际的开发过程中也会出现从Tensor某一位置搬运的场景,此时可以有如下的data_move使用方式:

from tbe import tik

tik_instance = tik.Tik()
data_input_gm = tik_instance.Tensor("int32", (256,), name="data_input_gm", scope=tik.scope_gm)
data_input_ub = tik_instance.Tensor("int32", (256,), name="data_input_ub", scope=tik.scope_ubuf)
# 有offset的连续数据搬运
tik_instance.data_move(data_input_ub[8], data_input_gm[16], 0, 1, 30, 0, 0)
.............

由数据搬运图可见,总共需要搬运30个Block,但是src是从第3个Block开始搬运,即第16个int32开始搬运,然后将30个Block依次搬到dst第2个开始的Block中,即第8个int32开始填入。

注意:由于不同昇腾AI处理器的架构稍有不同,所以对于搬运地址的要求也不同(参考表2),有的需要ub地址32Byte对齐,有的不需要。当ub需要32B对齐时,即从ub中读取或者写入的起始地址会根据数据类型的不同,必须是4或者8的倍数,如对于数据类型int32,一个Block记录了8个int32,则ub Tensor起始地址必须是8的倍数,又如int16起始地址必须是16的倍数等等。而所有芯片对于gm都不要求起始地址32B对齐。

课后练习

请完成一个简单的TIK算子以实现基本的数据搬入搬出功能,要求如下:

  1. 在gm和ub中开辟一定大小的Tensor空间,要求空间尽量小,在保证正确的情况下尽量不要开辟无用的空间;
  2. 搬入部分:将129个fp16数据从gm搬到ub中,gm中需要从gm[2]地址开始连续读取,到ub中的排布方式是每搬完16个fp16后,再间隔16个fp16进行写入;
  3. 搬出部分:将127个int32数据从ub搬到gm中,其中ub要从ub[32]开始搬运,每搬完16个int32后,再间隔32个int32进行搬运,搬到gm中需要连续写入。

注:假设ub的起始地址需要32B对齐。

【参考答案】:

from tbe import tik

tik_instance = tik.Tik()
data_input_gm = tik_instance.Tensor("float16", (146,), name="data_input_gm", scope=tik.scope_gm)
data_input_ub = tik_instance.Tensor("float16", (272,), name="data_input_ub", scope=tik.scope_ubuf)
tik_instance.data_move(data_input_ub, data_input_gm[2], 0, 9, 1, 0, 1)
.............
data_output_gm = tik_instance.Tensor("int32", (128,), name="data_output_gm", scope=tik.scope_gm)
data_output_ub = tik_instance.Tensor("int32", (384,), name="data_output_ub", scope=tik.scope_ubuf)
tik_instance.data_move(data_output_gm, data_output_ub[32], 0, 8, 2, 4, 0)

【解析】:

gm搬运到ub的场景:因为129个fp16,每个Block能搬运16个,所以需要搬运9次,因为gm从索引为2的数据开始连续搬运,且没有起始地址对齐的限制,所以gm的大小为9 * 16 + 2 = 146;因为ub是搬16个空16个,最后一个空的16个可以不分配空间,所以ub大小为9 * (16+16) - 16 = 272。需要搬运9次,nburst=9,每次搬1个Block,burst=1,src端连续,src_stride=0,dst端前尾后头空16个fp16也就是1个Block,dst_stride=1,所以data_move中的参数分别是9, 1, 0, 1;

ub搬运到gm的场景:因为127个int32,每个Block能搬运8个,所以需要搬运16次,gm大小直接给成16 * 8 = 128即可;因为ub需要搬16个空32个,然后又从32地址开始搬,所以ub大小为8 * (16+32) - 32 + 32= 384。需要搬8次,nburst=8,每次搬16个int32,所以每次搬2个Block,burst=2,src端前尾后头空32个int32也就是4个Block,src_stride=4,dst端连续,dst_stride=0,所以data_move中的参数分别是8, 2, 4, 0;