算子代码实现
实现流程
TE算子代码通过Python语言开发,实现流程如图12-7所示。
- 支持的自定义算子的输入数据类型为:float16, int8, int16, int32, uint8, uint16, bool。
- 不同计算操作支持的数据类型不同,详细请参见《TE API参考》。
- TE API同时支持float16与float32数据类型,但OMG进行模型转换的时候会将float32数据类型转换成float16,所以当前版本在进行自定义算子开发时,不支持使用float32的数据类型。
- TE提供了一些已经自定义实现的算子样例代码,用户可参考或直接使用,代码存储路径为DDK安装目录下的“ddk/site-packages/topi-0.4.0.egg/topi/cce”。
导入Python模块
导入昇腾AI软件栈提供的Python模块,代码示例如下所示。
import te.lang.cce from te import tvm from topi import generic
其中:
- “te.lang.cce”:引入TE支持的特定域语言接口,包括常见的运算vmuls、vadds、matmul等。
具体的接口定义可查看DDK安装路径下“/site-packages/te-0.4.0.egg/te/lang/cce/”目录下的python函数,使用方法请参见《TE API参考》。
- “te.tvm”:引入TVM后端代码生成机制。
具体的接口定义可查看DDK安装路径下“/site-packages/te-0.4.0.egg/te/tvm”目录下的python函数,使用方法请参见https://docs.tvm.ai/。
- “topi.generic”:提供了自动算子调度接口。
具体的接口定义可查看DDK安装路径下“/site-packages/topi-0.4.0.egg/topi/generic”目录下的所有python函数,使用方法请参见《TE API参考》。
算子实现
算子实现函数定义
如下所示,一个算子的实现函数中包含了输入张量的形状,数据类型,算子属性,内核名称,以及相应的编译、打印等配置。该函数会被插件代码调用,在离线模型生成器进行模型转换时执行。
def operationname(shape, dtype, attribute1, attribute2, ... , kernel_name="KernelName", need_build=True, need_print=False)
其中:
- shape:输入张量的形状,若算子有多个输入,且每个输入的shape不同,则此处需定义多个shape用于后续对每个输入张量占位,若多个输入张量的shape相同,则可以定义一个shape。
- dtype:输入张量的数据类型。
- attribute1,attribute2...:算子的属性,根据算子的实际定义进行代码编辑。
- kernel_name:算子在内核中的名称(即生成的二进制文件的名称),用户自定义,保持唯一,只能是大小写字母、数字、“_”的组合,且必须是字母或者“_”开头,长度小于或等于200个字符。
- 编译配置参数“need_build”:取值范围为True或者False,代表是否需要进行编译。
- 打印配置参数“need_print”:取值范围为True或者False,代表是否需要打印算子的中间表示(IR:Intermediate Representation)。
例如,
对于Reduction算子,实现函数定义如下:
def reduction(shape, dtype, axis, operation, coeff, kernel_name="Reduction", need_build=True, need_print=False)
对于Matmul算子,实现函数定义如下:
def matmul(shape_a, shape_b, dtype, kernel_name="matmul", trans_a=False, trans_b=False,need_build=False, need_print=False):
算子实现逻辑
TE的算子实现逻辑总体概括为:
定义好输入数据的张量占位符,然后调用te.lang.cce中的各种特定域语言接口进行计算过程的描述,如下代码示例所示:
data = tvm.placeholder(shape, name="data_input", dtype=inp_dtype) with tvm.target.cce(): cof = coeff data_tmp_input = te.lang.cce.vmuls(data, cof) //对缩放参数进行处理,将输入张量乘上一个标量 tmp = data_tmp_input res_tmp = te.lang.cce.sum(tmp, axis=axis) //在轴axis上做求和操作 res = te.lang.cce.cast_to(res_tmp, inp_dtype, f1628IntegerFlag=True) //进行数据类型的转换
其中:
- data为输入张量,使用TVM的placeholder接口进行定义,placeholder是一个占位符,返回一个Tensor对象,表示一组输入数据。
若算子有多个输入张量,此处需要定义多个Tensor对象。例如:
tensor_a = tvm.placeholder(shape_a, name='tensor_a', dtype=dtype) tensor_b = tvm.placeholder(shape_b, name='tensor_b', dtype=dtype)
- vmuls(向量乘),sum(求和)组成中间的计算逻辑。
- cast_to(转换数据类型):输出张量需要与输入张量的数据类型保持一致,如果计算过程中对数据类型做了转换,需要使用cast_to接口将输出张量的数据类型转换为输入张量的数据类型。
例如:如果输入张量的数据类型为int8,则执行vmuls操作时,会将数据类型转换为float16,则需要在计算逻辑结束后调用cast_to接口将输出张量的数据类型由float16转换为int8,由于使用vmuls将int8转换为float16的数值时,小数部分为0,所以“f1628IntegerFlag”设置为True,代码示例为:
res = te.lang.cce.cast_to(res_tmp, inp_dtype, f1628IntegerFlag=True)
“te.lang.cce.cast_to”接口的详细使用方法请参见《TE API参考》中的“Compute接口”。
- res为输出张量,其数据类型与输入张量的数据类型一致。
用户在进行算子逻辑实现前,可以自定义实现代码对输入数据进行预处理,如下代码示例所示:
# basic check check_list = ["float16", "float32"] if not (dtype.lower() in check_list): raise RuntimeError("Reduction only support %s while dtype is %s" % ( ",".join(check_list), dtype)) reduction_op = ("SUM", "ASUM", "SUMSQ", "MEAN") # axis parameter check if type(axis) != int: raise RuntimeError("type of axis value should be int") if axis >= len(shape) or axis < -len(shape): raise RuntimeError( "input axis is out of range, axis value can be from %d to %d" % ( -len(shape), len(shape) - 1)) # operation parameter check if operation not in reduction_op: raise RuntimeError("operation can only be one of SUM, ASUM, SUMSQ , MEAN") # coeff parameter check if type(coeff) != int and type(coeff) != float: raise RuntimeError("coeff must be a value") # Preprocess if axis < 0: axis = len(shape) + axis shape = list(shape) shape1 = shape[:axis] + [reduce(lambda x, y: x * y, shape[axis:])] inp_dtype = dtype.lower()
算子调度与编译
如下代码所示,当定义完计算逻辑后,使用auto_schedule机制,便可以自动生成相应的调度,此处通过TVM的打印机制可以看到相应计算的中间表示。配置信息包括是否需要打印、编译以及算子内核名以及输入、输出张量。
sch = generic.auto_schedule(res) config = { "print_ir": need_print, "need_build": need_build, "name": kernel_name, "tensor_list": [data, res] } te.lang.cce.cce_build_code(sch, config)
- 使用“generic”的“auto_schedule”接口,自动生成相应的调度(schedule),“auto_schedule”接口的参数为算子的输出张量。
schedule可以理解为:描述的计算过程如何在硬件上高效执行。就是把相关的计算和硬件设备上的相关指令对应起来。schedule对象中包含一个“中间表示”(IR),它用一种类似伪代码来描述计算过程,可以通过“need_print”参数把它打印出来进行查看。
- “tensor_list”(张量列表)中保存输入张量、输出张量,这个顺序需要严格按照算子本身的输入、输出数据顺序排列。
例如:"tensor_list": [tensor_a, tensor_b, res],tensor_a与tensor_b是输入张量,res为输出张量。
- 根据调度和配置使用“te.lang.cce”提供的“cce_build_code”接口来进行算子编译,算子编译过程会根据输入的数据形状、类别、算子参数等编译出专用内核,这个过程在离线模型生成器转换模型时发生。
- sch:生成的算子计算schedule对象。
- config:编译参数配置的map。
编译完成后,会生成算子目标文件.o文件(运行目标为AI Core的算子)或者.so文件(运行目标为AI CPU的算子)与算子描述文件.json文件。
执行算子
自定义算子代码编写完成后,可以在算子的*.py代码结尾加入调用算子的语句,如下所示,可以参照算子运行验证构造输入数据,验证算子执行结果是否正确。
例如:
if __name__ == "__main__": reduction((2, 3, 4), "float16", 1, "SUM", coeff = 2,kernel_name = "Reduction")