插件代码实现
插件实现流程
自定义算子开发完成后,离线模型生成器需要支持将自定义算子的属性值转换到离线模型,在离线模型执行阶段能够支持自定义算子的执行,所以用户需要开发自定义算子插件,进行算子属性的解析、形状类别的推断,并通过离线模型生成器提供的注册机制完成自定义算子的注册。
包含头文件
应在插件实现文件的头部使用预编译命令“#include”将插件实现函数相关的头文件包含到插件实现源文件中。
#include "custom/custom_op.h" #include "framework/omg/register.h" #include "framework/omg/omg_types.h" #include "proto/caffe/caffe.pb.h" #include "operator.h" #include "attr_value.h" #include <memory> #include <string> #include <vector>
头文件 |
目录 |
作用 |
---|---|---|
custom/custom_op.h |
DDK安装目录下的/include/inc/custom/custom_op.h。 |
包含该头文件,可调用自定义的编译函数及算子调测函数、算子校验函数。 |
framework/omg/register.h |
DDK安装目录下的/include/inc/framework/omg/register.h。 |
包含该头文件,可使用算子注册类相关,调用算子注册相关的接口。 |
framework/omg/omg_types.h |
DDK安装目录下的/include/inc/framework/omg/omg_types.h。 |
包含该头文件,可使用TE自定义算子信息结构体TEBinInfo。 |
proto/caffe/caffe.pb.h |
算子插件编译时,在算子工程所在目录下生成proto/caffe/caffe.pb.h文件。 |
算子插件编译时,会自动编译DDK安装目录下的“/include/inc/custom/proto/caffe/caffe.proto”文件,并在算子工程所在目录下生成proto/caffe/caffe.pb.h文件供插件代码调用进行算子参数解析。 |
operator.h |
DDK安装目录下的include/inc/graph/operator.h。 |
包含该头文件,可调用算子属性存取、输入/输出设置接口。 |
attr_value.h |
DDK安装目录下的/include/inc/graph/attr_value.h。 |
包含该头文件,可使用AttrValue类下各数据类型。 |
memory |
C++标准库。 |
包含该头文件,可使用C++标准库中的智能指针、内存分配器、临时性的申请与释放动态内存的函数、在内存上构造对象的函数等。 |
string |
C++标准库。 |
包含该头文件,可使用string类构造对象并调用string相关接口。 |
vector |
C++标准库。 |
包含该头文件,可使用vector类模板并调用vector相关接口。 |
算子插件编译前,由于工程中不存在proto/caffe/caffe.pb.h文件,所以工程中如果出现如下提示信息,请忽略。
解析算子
对于用户新开发的自定义算子,需要自定义实现解析算子中属性的函数,将原始模型中的算子属性定义转换为适配昇腾AI处理器的离线模型中的算子属性定义。如果用户重写昇腾AI处理器内置算子,则此步骤跳过,插件会自动实现对内置算子的解析。
函数声明
算子解析函数的声明如下所示:
Status ParseParamsxx(const Message* op_origin, ge::Operator& op_dest)
- ParseParamsxx:函数名称,用户自定义,需要保持唯一。
- op_origin:源算子模型,为protobuf格式的数据结构,来源于Caffe模型的proto文件,存储路径为DDK安装目录下的“/include/inc/custom/proto/caffe/caffe.proto”,若用户自定义的算子在caffe.proto文件中未定义,则需要参考caffe.proto文件算子定义(可选)增加算子定义。
算子插件实现时会根据算子名称从caffe.proto编译生成的proto/caffe/caffe.pb.h文件与caffe.pb.cc文件中读取相关的算子属性进行解析,进而将算子数据结构转换为离线模型支持的数据结构。
DDK中内置的caffe.proto文件的路径为DDK安装目录下的:“/include/inc/custom/proto/caffe/caffe.proto”,用户可基于此文件进行修改,添加自定义算子的定义。
- op_dest:目标算子模型,适配昇腾AI处理器的离线模型的算子数据结构,保存算子信息,Operator类的详细描述请参见《GE API参考》中的“Operator类接口”。
实现流程
ParseParamsxx函数的实现流程如下所示:
- 定义指向LayerParameter的对象,并获取当前算子层的句柄。
const caffe::LayerParameter* layer =dynamic_cast<const caffe::LayerParameter*>(op_origin); const caffe::xxxParameter& param = layer->reduction_param();
其中:
- param对象的类型caffe::xxxParameter中的xxxParameter需要与LayerParameter对象中声明的类型保持一致。
- layer对象的成员函数xxx_param()的名字需要与与LayerParameter对象中声明的对象名字保持一致。
例如caffe.proto中Reduction算子及Convolution算子的定义如下:
message LayerParameter { optional ReductionParameter reduction_param = 136; optional ConvolutionParameter convolution_param = 106; ... }
则获取当前Reduction算子层及Convolution算子层的句柄代码定义如下:
const caffe::ReductionParameter& param = layer->reduction_param()
const caffe::ConvolutionParameter& param = layer->convolution_param()
- 解析算子的每一个属性,并将其赋给Operator类型的对象op_dest。
开发者可调用CreateFrom<AttrValue::T>(DT&& val)接口将DT类型的参数转换为AttrValue类的T类型的参数,然后调用SetAttr(const string& name, const AttrValue& value)接口将转换后的AttrValue对象的值赋给op_dest对象的相应属性。
T类型是昇腾AI软件栈为了简化类型定义,对当前支持的数据类型做的重命名,T类型与原始数据类型的对应关系如表12-9所示,原型定义可参考DDK安装目录的“include/inc/graph/attr_value.h”文件。
表12-9 T类型与原始数据类型对应关系T类型
原始数据类型
INT
int64_t
FLOAT
float
STR
std::string
TENSOR
TensorPtr
TENSOR_DESC
TensorDesc
GRAPH
ComputeGraphPtr
BYTES
Buffer
NAMED_ATTRS
NamedAttrs
BOOL
bool
LIST_INT
vector<INT>
LIST_FLOAT
vector<FLOAT>
LIST_BOOL
vector<BOOL>
LIST_STR
vector<STR>
LIST_TENSOR
vector<TENSOR>
LIST_TENSOR_DESC
vector<TENSOR_DESC>
LIST_GRAPH
vector<GRAPH>
LIST_BYTES
vector<BYTES>
LIST_NAMED_ATTRS
vector<NAMED_ATTRS>
SetAttr接口的详细描述请参见《GE API参考》。
下面给出几种常见类型的参数解析示例:- int或者float类型参数
例如,caffe.proto文件中算子参数定义如下所示:
message ReductionParameter { …… optional int32 axis = 2 [default = 0]; optional float coeff = 3 [default = 1.0]; }
调用SetAttr接口,将“param.axis()”的值赋给op_dest对象的axis属性,并将类型转换为INT;将“param.coeff()”的值赋给op_dest对象的coeff属性,并将类型转换为FLOAT,如下所示:
op_dest.SetAttr("axis", AttrValue::CreateFrom<AttrValue::INT>(param.axis())); op_dest.SetAttr("coeff", AttrValue::CreateFrom<AttrValue::FLOAT>(param.coeff()));
其中CreateFrom<AttrValue::T>(DT&& val)是将DT类型的参数转换为AttrValue类的T类型的参数。
- enum类型参数
例如,caffe.proto文件中算子属性定义如下所示:
message ReductionParameter { enum ReductionOp { SUM = 1; ASUM = 2; SUMSQ = 3; MEAN = 4; } ...}
- 首先,需要将enum类型参数转换为map类型参数。
std::map<caffe::ReductionParameter_ReductionOp, std::string> operation_map = { { caffe::ReductionParameter_ReductionOp_SUM, "SUM" }, { caffe::ReductionParameter_ReductionOp_ASUM, "ASUM" }, { caffe::ReductionParameter_ReductionOp_SUMSQ, "SUMSQ" }, { caffe::ReductionParameter_ReductionOp_MEAN, "MEAN" }, };
- 调用SetAttr接口,将map类型参数operation_map赋值给op_dest对象的operation属性。
op_dest.SetAttr("operation", AttrValue::CreateFrom<AttrValue::STR>(operation_map[param.operation()]));
SetAttr接口的详细描述请参见《GE API参考》。
- 首先,需要将enum类型参数转换为map类型参数。
- repeated类型参数
例如caffe.proto文件中算子参数定义如下所示:
message xxxParameter { ... repeated float min_size = 1; repeated uint32 offset = 2; .... }
- 首先,将repeated float类型参数转换为vector<float>类型参数,将repeated uint32类型参数转换为vector<uint32>类型参数,并对vector类型的参数进行赋值。
vector<float> min_size; vector<uint32> offset; for(int i = 0; i < param.min_size_size(); ++i) { min_size.push_back(param.min_size(i)); //调用vector类型对象的push_back函数为min_size对象赋值 } for(int i = 0; i < param.offset_size(); ++i) { offset.push_back(param.offset(i)); //调用vector对象的push_back函数为offset对象赋值 }
- 调用SetAttr接口,将vector<float>类型参数min_size赋值给op_dest对象的min_size属性;将vector<uint32>类型参数offset赋值给op_dest对象的offset属性,类型为LIST_INT。
op_dest.SetAttr("min_size", ge::AttrValue::CreateFrom<ge::AttrValue::LIST_FLOAT>(min_size)); op_dest.SetAttr("offset", ge::AttrValue::CreateFrom<ge::AttrValue::LIST_INT>(offset));
SetAttr接口的详细描述请参见《GE API参考》。
- 首先,将repeated float类型参数转换为vector<float>类型参数,将repeated uint32类型参数转换为vector<uint32>类型参数,并对vector类型的参数进行赋值。
- int或者float类型参数
推理算子输出张量描述
用户需要根据算子的输入张量描述、算子逻辑及算子属性,推理出算子的输出张量描述,包括张量的形状、数据类型及数据排布格式等信息。这样离线模型转换时就可以为所有的张量静态分配内存,避免动态内存分配带来的开销。
函数声明
函数的声明如下所示:
Status InferShapeAndTypexx(const ge::Operator& op, vector<ge::TensorDesc>& v_output_desc)
- InferShapeAndTypexx:函数名称,用户自定义,需要保持唯一。
- op:计算节点定义,存储输入张量描述及各种算子属性,ge::Operator类型的介绍请参见《GE API参考》中的“Operator类接口”。
- v_output_desc:存储该计算节点的输出张量描述,包括形状、数据排布格式及数据类型,关于TensorDesc类型的介绍请参见《GE API参考》中的“TensorDesc类接口”。
下面详细讲解不同场景下InferShapeAndTypexx函数的实现。
输出张量与输入张量形状相同的算子
对于输出张量与输入张量形状相同的算子,可以直接将输入张量的描述插入输出张量描述所在的向量空间中。
代码示例如下所示:
v_output_desc.push_back(op.GetInputDesc(0));
其中GetInputDesc是Operator类中根据算子Input名称或者Input索引获取输入张量描述的接口,详细的接口介绍请参见《GE API参考》中的“Operator类接口”。
降低维度的算子
对于Reduction、Reduce之类的常见降维操作,需要根据算子输入属性axis等信息计算出输出张量的形状(包含输出张量维度以及每一个维度有多少个元素),然后将输出张量的形状插入到v_output_desc向量中。
代码示例如下所示:
- 获取算子的输入张量描述及输入张量的形状信息。
auto tensorDesc = op.GetInputDesc(0); //输入张量描述,包括的形状、数据排布格式及数据类型 auto shape = tensorDesc.GetShape(); //获取输入张量的形状
GetShape接口的详细介绍请参见《GE API参考》中的“TensorDesc类接口”。
- 根据计算逻辑获取算子的属性值,并计算算子输出张量的形状。
例如,对于Mylenet网络中的Reduction算子,由于Reduction的上一层为Softmax,Softmax的实际输出是2维,但在离线模型中会被补齐到4维,所以需要调整axis,将其指向2维的位置,进行reduce操作,并将调整后的Shape值赋给输出张量描述。
- 从operator对象中获取axis属性的键值对,然后从其中获取axis的属性值,并将其从INT类型转换为int64_t类型赋给名称为axis的变量,最后对axis的值进行校验、调整,将其指向轴1的位置,代码示例如下所示:
int64_t axis = -1; ge::AttrValue axisAttrValue; if ((ge::GRAPH_SUCCESS != op.GetAttr("axis", axisAttrValue)) || (ge::GRAPH_SUCCESS != axisAttrValue.GetValue<AttrValue::INT>(axis))) { printf("Get axis failed!\n"); } // In the OM model, all shape are supplemented to 4d. In this case, axis needs to be repaired to point to the original 2d. if (axis < 0) axis -= 2; if (axis < 0) axis += shape.GetDimNum(); if (axis < 0 || axis >= shape.GetDimNum()) { printf("invalid axis:%d, dim_size:%d\n", (int32_t)axis, (int32_t)shape.GetDimNum()); return PARAM_INVALID; }
- 调整Shape,将轴1及其之后的维度设置为1,例如输入张量的Shape为(2,3,4,5),则调整之后的Shape为(2,1,1,1)。
int32_t dimsize = (int32_t)shape.GetDimNum(); int32_t idx = 0; for(idx=axis; idx<dimsize; idx++) { shape.SetDim(idx, 1); }
- 将调整后的Shape值设置到tensorDesc对象。
tensorDesc.SetShape(shape);
GetDimNum与SetDim接口的详细介绍请参见《GE API参考》中的“Shape类接口”。
- 从operator对象中获取axis属性的键值对,然后从其中获取axis的属性值,并将其从INT类型转换为int64_t类型赋给名称为axis的变量,最后对axis的值进行校验、调整,将其指向轴1的位置,代码示例如下所示:
- 设置算子的输出张量描述。
v_output_desc.push_back(tensorDesc)
将tensorDesc赋给输出张量的描述对象v_output_desc。
网络存在Type相同的多个算子
一个网络中往往存在多层某个Type相同的算子,例如Convolution算子,开发者有时需要对某层算子进行形状的自定义(重定义已经存在的某层算子),此时在进行输出张量描述推理的时候需要根据网络的不同情况,根据num_output、kernel、stride、 pad等属性判断是针对哪一层的算子进行的自定义,从而推断出对应算子的张量信息。
代码示例如下所示:
- 将输入张量描述赋给输出张量描述,并获取算子的输入张量描述及输入张量的形状信息。
v_output_desc.push_back(op.GetInputDesc(0)); //将输入张量描述信息赋给输出张量描述对象,开发者也可以将后续推理出的shape直接赋给tensorDesc对象,再将tensorDesc对象赋给输出张量描述对象。 auto tensorDesc = op.GetInputDesc(0); //输入张量描述,包括的形状、数据排布格式及数据类型 auto shape = tensorDesc.GetShape(); //获取输入张量的形状
GetShape接口的详细介绍请参见《GE API参考》中的“TensorDesc类接口”。
- 根据计算逻辑获取算子的属性值,并根据算子的属性值、shape匹配算子,并计算算子输出张量的形状。
例如,对于某一网络中的Type为Convolution的算子(网络中存在多层Convolution),匹配num_outputs为128、shape.GetDim(0)为1、shape.GetDim(1) 为128、shape.GetDim(2) 为28、shape.GetDim(3) 为28的Convolution,并对其输出描述张量的形状重新赋值。
- 获取num_outputs的属性值
ge::AttrValue num_outputsAttrValue; if ((ge::GRAPH_SUCCESS != op.GetAttr("num_output", num_outputsAttrValue)) || (ge::GRAPH_SUCCESS != num_outputsAttrValue.GetValue<AttrValue::INT>(num_outputs))) { printf("GetOpAttr num_outputs failed!\n"); }
- 匹配num_outputs为128、shape.GetDim(0)为1、shape.GetDim(1) 为128、shape.GetDim(2) 为28、shape.GetDim(3) 为28的Convolution,并对其输出张量描述的形状重新赋值。
if(shape.GetDim(0) == 1 && shape.GetDim(1) == 128 && shape.GetDim(2) == 28 && shape.GetDim(3) == 28 && num_outputs == 128) { shape.SetDim(0, 1); shape.SetDim(1, 128); shape.SetDim(2, 28); shape.SetDim(3, 28); v_output_desc[0].SetShape(shape); return SUCCESS; return FAILED; }
GetDimNum与SetDim接口的详细介绍请参见《GE API参考》中的“Shape类接口”。
若需要使用op_name进行匹配,获取op_name的方法如下所示:
auto op_name = op.GetName();
其他属性kernel_w、kernel_h、stride_w、stride_h、pad_w、pad_h的获取方法与num_output相同,修改op.GetAttr中的key值即可。
- 获取num_outputs的属性值
编译算子
函数声明
算子编译函数的声明如下所示:
Status BuildTeBinxx(const ge::Operator& op, TEBinInfo& te_bin_info)
其中:
- BuildTeBinxx:函数名称,用户自定义,需要保持唯一。
- op:目标算子模型,适配昇腾AI处理器的离线模型的算子数据结构,保存算子信息,Operator类的详细描述请参见《GE API参考》中的“Operator类接口”。
- te_bin_info:存储自定义算子二进制文件路径、算子描述文件路径以及DDK的版本信息。TEBinInfo结构体的详细描述请参见《Framework API参考》中的“TEBinBuildFn函数”。
实现流程
算子编译函数在离线模型生成器进行模型转换时调用,主要完成以下流程:
- 获取算子张量描述信息以及算子属性,模型转换时,这些信息都需要是确定值,用于进行算子的匹配。
例如,如下所示,模型转换时只匹配axis为1、输入张量Shape的Dim为4的Reduction算子。
//解析算子属性operation ge::AttrValue operationAttrValue; if ((ge::GRAPH_SUCCESS != op.GetAttr("operation", operationAttrValue)) || (ge::GRAPH_SUCCESS != operationAttrValue.GetValue<AttrValue::STR>(operation))) { printf("GetOpAttr operation failed!\n"); } // 解析算子属性axis ,并调整axis指向Mylenet网络中Reduction算子的上一层Softmax算子的实际输出维度,即轴1的位置。 ge::AttrValue axisAttrValue; if ((ge::GRAPH_SUCCESS != op.GetAttr("axis", axisAttrValue)) || (ge::GRAPH_SUCCESS != axisAttrValue.GetValue<AttrValue::INT>(axis))) { printf("GetOpAttr axis failed!\n"); } // In the OM model, all shape are supplemented to 4d. In this case, axis needs to be repaired to point to the original 2d. if(axis < 0) axis -= 2; // 解析算子属性coeff ge::AttrValue coeffAttrValue; if ((ge::GRAPH_SUCCESS != op.GetAttr("coeff", coeffAttrValue)) || (ge::GRAPH_SUCCESS != coeffAttrValue.GetValue<AttrValue::FLOAT>(coeff))) { printf("GetOpAttr coeff failed!\n"); } // 获取算子输入张量描述信息 TensorDesc input_desc = op.GetInputDesc(0); // 解析Input shape并校验算子的Dim是否为4 if(input_desc.GetShape().GetDimNum() != 4) { printf("The shape size is %d, which is not 4!", (int32_t)input_desc.GetShape().GetDimNum()); return FAILED; }
- 指定算子实现文件、算子实现函数以及内核名。
FilePath = "project_path/operator/reduction"; //算子实现文件所在的绝对路径+算子py文件名称 FuncName = "Reduction"; //算子实现文件中算子实现函数的名称。 KernelName = "Reduction"; //算子实现文件中算子实现函数中定义的kernel_name,即生成的二进制文件的名称。
- 指定算子编译生成的算子描述文件(*.json)文件的路径,请保持如下固定配置。
te_bin_info.json_file_path = "./kernel_meta/" + KernelName + ".json";
模型转换时,会从此路径中读取算子的二进制描述文件,获取算子相关信息。
执行omg模型转换命令时,会根据FilePath中配置的算子实现的路径拷贝算子编译生成的kernel_meta文件夹到执行omg命令时所在的路径,所以*.json文件相对于执行omg命令所在目录的路径固定为“./kernel_meta”。
- 调用te::BuildCustomop函数来调用算子实现文件的python函数编译算子。
BuildCustomop函数调用如下所示:
te::BuildTeCustomOp(te_bin_info.ddk_version, op.GetName(), FilePath, FuncName,"(i,i,i,i), s, i, s, f, s", input_desc.GetShape().GetDim(0),input_desc.GetShape().GetDim(1),input_desc.GetShape().GetDim(2),input_desc.GetShape().GetDim(3), "float16", axis, operation.c_str(), coeff,KernelName.c_str());
其中:- te_bin_info.ddk_version:DDK的版本信息,固定参数。后续模型转换时会自动传入DDK版本信息。
- op.GetName( ):获取算子名称,固定参数。
- FilePath:前面定义的算子文件所在相对路径。
- FuncName:算子实现文件中的算子实现函数名称。
- "(i,i,i,i), s, i, s, f, s":代表算子实现文件中实现函数的参数占位符,其中i表示整形,s表示字符串类型,f表示单精度浮点数类型,o表示PyObject*类型。占位符需要和后面紧跟的参数顺序和类型保持一致,同时需要和算子实现文件中的算子实现函数的定义保持一致。BuildCustomop会根据这些参数调用算子实现函数,再通过TVM机制生成内核。
算子注册
框架管理器(Framework)提供REGISTER_CUSTOM_OP宏,按照指定的算子名称完成算子的注册。
自定义算子的注册代码如下所示:
REGISTER_CUSTOM_OP("test_layer") .FrameworkType(CAFFE) .OriginOpType("Test") .ParseParamsFn(ParseParamsxx) .InferShapeAndTypeFn(InferShapeAndTypexx) .TEBinBuildFn(BuildTeBinxx) .ImplyType(ImplyType::TVM) .Formats({DOMI_TENSOR_NC1HWC0}, {DOMI_TENSOR_NC1HWC0}) .WeightFormats({DOMI_TENSOR_FRACTAL_Z, DOMI_TENSOR_NC1HWC0});
其中:
- REGISTER_CUSTOM_OP:注册自定义算子,"test_layer"作为离线模型文件中的算子名称,可以任意命名但不能和已有的算子命名冲突。
- FrameworkType:不同框架的算子参数解析逻辑不同,因此对于不同框架模型需要有不同的插件,在插件的注册代码需要表明对应的框架。当前请配置为CAFFE。
- OriginOpType:算子类型,需要与Caffe Prototxt中定义的算子类型保持一致,否则无法正常解析,内置的caffe.proto文件路径为DDK安装路径下的“/include/inc/custom/proto/caffe/caffe.proto”。
- ParseParamsFn:用来注册解析模型的函数,“ParseParamsxx”即解析算子中实现的函数,针对Caffe框架开发插件时需要,若自定义的算子为重写昇腾AI处理器已经支持的算子,则此步骤可跳过;若自定义的算子为昇腾AI处理器不支持的算子,则此步骤必选。
- InferShapeAndTypeFn:用来注册形状和类别推断函数,“InferShapeAndTypexx”即推理算子输出张量描述中实现的函数。
- TEBinBuildFn:用来注册TE算子编译函数,“BuildTeBinxx”即编译算子中实现的函数。
- ImplyType:指定算子的实现方式,ImplyType::TVM表示该算子是TE算子。
- Formats:算子输入数据与输出数据的数据排布格式,其中第一个列表是输入数据格式列表,第二个列表为输出数据格式列表,若输入数据有多个,请在第一个列表中列出每一个输入数据的排布格式。例如若有两个输入数据,分别是NC1HWC0格式,则Formats函数调用方式如下:
.Formats({DOMI_TENSOR_NC1HWC0, DOMI_TENSOR_NC1HWC0}, {DOMI_TENSOR_NC1HWC0})
详细说明请参见《Framework API参考》中的“Formats函数”。
- WeightFormats:设置算子的权重数据的排布格式,支持的数据格式类型请参见《Framework API参考》中的“WeightFormats函数”。例如Convolution的filter的数据排布格式是fractal_Z,bias的数据排布格式是NC1HWC0。
如果模型转换时开启量化开关,需要在此接口中增加Framework处理时新增的常量数据的格式,当前Framework支持的量化算子有Conv,FC与Depthwise Conv,若这几个算子在模型转换时开启了量化开关,则需要在WeightFormats接口的参数列表最后增加6个DOMI_TENSOR_NC1HWC0的数据格式(量化时Framework新增了6个数据排布格式为NC1HWC0的常量,此处不详细介绍),开启量化后的Convolution的WeightFormats接口示例如下:
.WeightFormats({DOMI_TENSOR_FRACTAL_Z, DOMI_TENSOR_NC1HWC0, DOMI_TENSOR_NC1HWC0, DOMI_TENSOR_NC1HWC0, DOMI_TENSOR_NC1HWC0,DOMI_TENSOR_NC1HWC0, DOMI_TENSOR_NC1HWC0, DOMI_TENSOR_NC1HWC0})