cann/asc-devkit算子入图开发基本流程 基本开发流程【免费下载链接】asc-devkit本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言原生支持C和C标准规范主要由类库和语言扩展层构成提供多层级API满足多维场景算子开发诉求。项目地址: https://gitcode.com/cann/asc-devkit该开发流程以工程化算子开发为基础除了需要提供工程化算子开发中的算子实现文件外还需要额外交付算子入图的代码文件。本节仅提供算子入图代码文件的开发指导。假设下图是我们需要使用的网络模型您可能会想直接逐个算子调用根据输入tensor得到输出tensor就可以完成网络的运行但在图模式场景下实际的网络模型生成过程中会先进行tensor shape以及datatype的推导。这样可以让我们在图执行之前就知道各tensor的数据类型和形状提前校验其正确性同时提前推理出算子的输出张量描述包括张量的形状、数据类型及数据排布格式等信息算子构图准备阶段就可以为所有的张量静态分配内存避免动态内存分配带来的开销。下面的网络模型经过shape和datatype推导之后可以得到灰色底纹框中的推导信息图1shape与datatype推导示意图![](https://raw.gitcode.com/cann/asc-devkit/raw/c975c4a444b8d5c5dfc5201203324866d8a916d3/docs/zh/guide/figures/shape与datatype推导示意图.png shape与datatype推导示意图?utm_sourcegitcode_repo_files)除了tiling实现外算子入图时需要额外提供的实现代码有以下几种datatype推导根据算子的输入datatype、算子逻辑及算子属性等信息推理出算子的输出张量datatype。shape推导根据算子的输入shape、算子逻辑及算子属性等信息推理出算子的输出张量shape。ShapeRange推导编译时无法推导输出shape只能推导输出shape range执行完才能得出输出shape。在下发时需要按照输出shape range来申请最大输出内存该类算子需要提供ShapeRange推导函数。声明数据依赖部分算子在InferShape时需要依赖某个输入的具体值才可以进行这类算子被称为“数据依赖算子”对应的输入被称为“数据依赖输入”。该类算子在注册时需要声明其数据依赖输入。下表列出了不同类型的算子对上述实现代码的要求。表1不同的类型的算子对入图实现代码的要求分类对入图实现代码的要求根据输入shape可以推导出输出shape。shape推导datatype推导依赖输入的value才能推导出输出shape即数据依赖算子。如Reshape算子依赖shape输入的value才能推导出输出shape。shape推导datatype推导声明数据依赖编译时无法推导输出shape只能推导输出shape range执行完才能得出输出shape。Shape推导必选DataType推导必选ShapeRange推导必选声明数据依赖按需实际开发时通过固定的datatype和shape推导原型实现推导函数然后再通过SetInferShape、SetInferDataType接口来关联对应的shape推导函数样例如下。namespace ge { static graphStatus InferShape(gert::InferShapeContext *context) { ... return GRAPH_SUCCESS; } static graphStatus InferDataType(gert::InferDataTypeContext *context) { ... return ge::GRAPH_SUCCESS; } } // namespace ge namespace ops { class AddCustom : public OpDef { public: AddCustom(const char* name) : OpDef(name) { this-Input(x) .ParamType(REQUIRED) .DataType({ge::DT_FLOAT16, ge::DT_FLOAT, ge::DT_INT32}) .Format({ge::FORMAT_ND, ge::FORMAT_ND, ge::FORMAT_ND}); this-Input(y) .ParamType(REQUIRED) .DataType({ge::DT_FLOAT16, ge::DT_FLOAT, ge::DT_INT32}) .Format({ge::FORMAT_ND, ge::FORMAT_ND, ge::FORMAT_ND}); this-Output(z) .ParamType(REQUIRED) .DataType({ge::DT_FLOAT16, ge::DT_FLOAT, ge::DT_INT32}) .Format({ge::FORMAT_ND, ge::FORMAT_ND, ge::FORMAT_ND}); // 根据用户的算子调用方式决定需不需要注册图模式调用方式下需要 this-SetInferShape(ge::InferShape); this-SetInferShapeRange(ge::InferShapeRange); this-SetInferDataType(ge::InferDataType); this-AICore() .SetTiling(optiling::TilingFunc); // 请替换为实际的昇腾AI处理器型号 this-AICore().AddConfig(ascendxxx); } }; OP_ADD(AddCustom); } // namespace opsdatatype推导以AddCustom算子为例InferDataType的实现如下所示。该样例中输出tensor的数据类型与输入tensor的数据类型相同所以直接将任意一个输入tensor的数据类型赋给输出tensor即可。namespace ge { static graphStatus InferDataType(gert::InferDataTypeContext* context) { const auto inputDataType context-GetInputDataType(0); context-SetOutputDataType(0, inputDataType); return ge::GRAPH_SUCCESS; } } // namespace ge如下示例则给出了更灵活的datatype推导样例当输入的数据类型为DT_INT4时其输出的数据类型为DT_INT32。ge::graphStatus InferDataTypeForFoo(gert::InferDataTypeContext* context) { if (context-GetInputDataType(0) DT_INT4) { context-SetOutputDataType(0, DT_INT32); } }shape推导简单的shape推导逻辑可以使用Follow接口来表达比如输出shape和输入shape相同的情况。示例如下输出“y1”Follow输入“x1”场景指定Follow模式为SHAPE此时“y1”的shape将会和“x1”保持一致。this-Input(x1) .ParamType(REQUIRED) .DataType({ge::DT_FLOAT, ge::DT_FLOAT}) .Format({ge::FORMAT_ND, ge::FORMAT_ND}); this-Input(x2) .ParamType(REQUIRED) .DataType({ge::DT_FLOAT, ge::DT_FLOAT}) .Format({ge::FORMAT_ND, ge::FORMAT_ND}); this-Output(y1) .ParamType(REQUIRED) .DataType({ge::DT_FLOAT, ge::DT_FLOAT}) .Format({ge::FORMAT_ND, ge::FORMAT_ND}) .Follow(x1, FollowType::SHAPE);无法在原型定义中通过Follow表达的情况需要开发者编写InferShape函数InferShape函数的原型是固定的如下示例接受一个InferShapeContext作为输入从此context上可以获取到输入、输出的shape指针等内容。输入shape为const类型因此InferShape时输入shape是只读、不允许修改的。InferShape成功后返回ge::GRAPH_SUCCESS其他返回值被认为推导失败。推导失败后执行过程结束退出。以ReShape算子为例InferShape的实现如下所示。根据第1个输入shape输入的值Reshape算子将第0个输入x输入的shape做变换并输出到其第0个输出y输出上。Reshape的InferShape实现为ge::graphStatus InferShapeForReshape(InferShapeContext *context) { const gert::Shape *x_shape context-GetInputShape(0); // 获取第0个输入的shape const gert::Tensor *shape_tensor context-GetInputTensor(1); // 获取第1个输入的tensor gert::Shape *output_shape context-GetOutputShape(0); if (x_shape nullptr || shape_tensor nullptr || output_shape nullptr) { // 防御式编程不应该出现的场景打印错误并返回失败 return ge::GRAPH_FAILED; } auto reshape_size static_castint32_t(shape_tensor-GetShapeSize()); if (reshape_size 1) { // 防御式编程不应该出现的场景打印错误并返回失败 return ge::GRAPH_FAILED; } // 根据原型信息Reshape的shape输入支持INT32与INT64两类根据不同的类型进入对应的模板函数中做真正的shape变换操作 if (shape_tensor-GetDataType() ge::DT_INT32) { int32_t *reshape_data shape_tensor-GetDataint32_t(); return ReshapeInferShapeImplint32_t(reshape_data, *x_shape, *output_shape, reshape_size); } else { int64_t *reshape_data shape_tensor-GetDataint64_t(); return ReshapeInferShapeImplint64_t(reshape_data, *x_shape, *output_shape, reshape_size); } }InferShapeContext public继承自ExtendedKernelContext因此ExtendedKernelContext中提供的方法如获取算子type、name、属性等接口均可以在InferShapeContext实例中调用。[!CAUTION]注意InferShape推导函数和Follow接口不能混用即不支持部分输出采用Infershape推导、部分输出采用Follow推导的情况。若用户同时使用了InferShape函数和Follow接口以用户的InferShape函数为准需要保证在InferShape函数中能够推导出所有的输出shape。为了效率考虑调用InferShape函数时框架不会为输出shape做初始化因此在InferShape函数中可以认为输出是未初始化的状态。如果在InferShape时希望通过Append方式操作输出shape需要先将输出shape的DimNum清零以防止出现未定义行为。InferShapeRange实现某些算子的输出Shape在计算完成后才能确定。比如unique算子其Shape的推导逻辑如下给定一维Tensor x找到其中不重复的元素返回去重后的Tensor y输出idx与输入x大小相同保存x每个元素在y中的索引。# tensor x is [1, 1, 2, 4, 4, 4, 7, 8, 8] x shape[9] y, idx unique(x) y [1, 2, 4, 7, 8] y shape[5] idx [0, 0, 1, 2, 2, 2, 3, 4, 4] idx shape[9]由此可知y的shape在编译时为[-1]unique执行后shape才确定。在入图场景执行时需要在执行前分配输出内存而内存的大小依赖于输出Shape和数据类型。对于此类算子由于输出Shape在执行后才能确定因此需要根据输出Shape的范围按照最大范围申请输出内存以确保有足够的空间供计算函数写入输出Tensor。这种场景下开发者需要自行实现InferShapeRange函数来推导输出Shape的范围。下面以unique算子为例子介绍InferShapeRange函数的实现方法。ge::graphStatus UniqueInferShapeRangeFunc(gert::InferShapeRangeContext *context) { // 取输入的shape range auto x_shape_range context-GetInputShapeRange(0U); OPS_CHECK_NULL_WITH_CONTEXT(context, x_shape_range); OPS_CHECK_NULL_WITH_CONTEXT(context, x_shape_range-GetMax()); OPS_CHECK_NULL_WITH_CONTEXT(context, x_shape_range-GetMin()); // 开始计算y输出的shape range auto y_shape_range context-GetOutputShapeRange(0U); OPS_CHECK_NULL_WITH_CONTEXT(context, y_shape_range); y_shape_range-GetMax()-SetDimNum(1); // 一维向量rank为1 y_shape_range-GetMin()-SetDimNum(1); auto x_max_shape x_shape_range-GetMax(); auto x_shape_dimnum x_max_shape-GetDim(0); // x为一维Tensor其shape为[n] x_shape_dimnum表示x输入的元素个数n if (x_shape_dimnum 1) { // 若x输入只有1个元素不存在去重y的shape轴最小最大均为1. 因此range为[1~1] y_shape_range-GetMax()-SetDim(0, 1); y_shape_range-GetMin()-SetDim(0, 1); } else { // 若x输入有0个元素或者大于1个元素去重后y的元素个数最小为x的min最大为x的max y_shape_range-GetMax()-SetDim(0, x_shape_dimnum); y_shape_range-GetMin()-SetDim(0, x_shape_range-GetMin()); } // 开始计算输出idx的shape range // 输出idx表示x元素在y中的索引其元素个数与x相等因此shape range与x一致 auto idx_shape_range context-GetOutputShapeRange(1U); OPS_CHECK_NULL_WITH_CONTEXT(context, idx_shape_range); *(idx_shape_range-GetMax()) *(x_shape_range-GetMax()); *(idx_shape_range-GetMin()) *(x_shape_range-GetMin()); return ge::GRAPH_SUCCESS; }InferShape时获取属性、输入在InferShape、Tiling时可以通过context实例获取算子IR属性值所谓IR属性是指在IR注册时定义的属性以TransData算子为例namespace ops { class TransData : public OpDef { public: explicit TransData(const char *name) : OpDef(name) { this-Input(src) ... this-Output(dst) ... this-Attr(src_format) .AttrType(REQUIRED) .String(); this-Attr(dst_format) .AttrType(REQUIRED) .String(); this-Attr(group) .AttrType(OPTIONAL) .Int(1); ... } }; OP_ADD(TransData); } // namespace ops其原型定义中声明了src_format、dst_format、group三个属性可以通过如下方式获取算子属性ge::graphStatus ExampleGetTransDataAttr(TilingContext *context) { // 获取所有属性 const RuntimeAttrs *attrs context-GetAttrs(); ASSERT_NOT_NULL(attrs); // 按照在原型定义中的顺序使用index获取属性index从0开始计数 const char *src_format attrs-GetAttrPointerchar(0); // 获取src_formatsrc_format是第一个属性因此index为0 const char *dst_format attrs-GetAttrPointerchar(1); // 获取dst_formatdst_format是第二个属性因此index为1 const int64_t group attrs-GetAttrPointerint64_t(2); // 获取groupgroup是第三个属性因此index为2 return ge::GRAPH_SUCCESS; }通过index而不是字符串name来索引输入输出对于带有OPTIONAL、DYNAMIC类型输入的算子可能出现实例化后单纯通过index无法索引到具体输入的问题以DynamicRNNV3算子为例namespace ops { class DynamicRNNV3 : public OpDef { public: explicit DynamicRNNV3(const char *name) : OpDef(name) { this-Input(x) .ParamType(REQUIRED) ... this-Input(w) .ParamType(REQUIRED) ... this-Input(b) .ParamType(REQUIRED) ... this-Input(seq_length) .ParamType(OPTIONAL) ... this-Input(init_h) .ParamType(OPTIONAL) ... this-Input(init_c) .ParamType(OPTIONAL) ... this-Input(wci) .ParamType(OPTIONAL) ... this-Input(wcf) .ParamType(OPTIONAL) ... this-Input(mask) .ParamType(OPTIONAL) ... this-Input(mask) .ParamType(OPTIONAL) ... this-Input(project) .ParamType(OPTIONAL) ... ... } }; OP_ADD(DynamicRNNV3); } // namespace ops由于DynamicRNNV3算子有连续的多个optional输入这导致init_h及其后面的输入的实例化后index都是不确定的对于这种类型的算子可以通过GetOptionalInputShape传入原型对应的index来获取对应的输入shape等数据以InferShape为例ge::graphStatus InferShapeForDynamicRNNV3(InferShapeContext *context) { // 对于前两个输入不受到optional或dynamic的影响可以按照常规方法获取输入shape auto x_shape context-GetInputShape(0); auto w_shape context-GetInputShape(1); if (x_shape nullptr || w_shape nullptr) { return ge::GRAPH_FAILED; } int64_t state_size 0; // 在原型定义上project是第11个输入(从0开始计数) constexpr int64_t kProjectInputIndex 11; // 受到前面optional输入影响的project实例化后输入的index是不确定的通过GetOptionalInputShape来获取对应的输入shape // GetOptionalInputShape的入参为原型上对应的index auto project_shape context-GetOptionalInputShape(kProjectInputIndex); if (project_shape ! nullptr) { if (project_shape-GetDimNum() 2) { return ge::GRAPH_FAILED; } state_size project_shape-GetDim(1); } // 更多的infershape逻辑... return ge::GRAPH_SUCCESS; }对于dynamic类型的输入实例化后的输入可能是一到多个对于此类输入获取方式为// ir_index此输入在原型定义中的index从0开始计数 // relative_index该输入实例化后的相对index从0开始计数例如某个DYNAMIC_INPUT实例化了3个要取第二个那么relative_index 1 auto shape context-GetDynamicInputShape(ir_index, relative_index);本节举例的获取optional、dynamic输入的方式在InferShape、Tiling函数中均可以调用。数据依赖一般来说具备输入shape后算子可以通过InferShape推导出输出shape。然而部分算子在InferShape时需要依赖某个输入的具体值才可以进行这类算子被称为“数据依赖算子”对应的输入被称为“数据依赖输入”。以Reshape算子为例其依据shape输入的描述对输入的shape做调整因此Reshape算子依赖shape输入的值。这类算子需要在原型定义时通过ValueDepend接口声明对应的输入为数据依赖输入。namespace ops { class Reshape : public OpDef { public: explicit Reshape(const char *name) : OpDef(name) { ... this-Input(shape) .ParamType(REQUIRED) ... .ValueDepend(REQUIRED) // 声明ReShape算子的shape输入为数据依赖输入 ... } }; OP_ADD(Reshape); } // namespace ops根据第1个输入shape输入的值Reshape算子将第0个输入x输入的shape做变换并输出到其第0个输出y输出上。Reshape的InferShape实现为// shape变换具体实现 templatetypename T ge::graphStatus ReshapeInferShapeImpl(const T *reshape_dims, const gert::Shape x_shape, gert::Shape output_shape, int32_t reshape_rank) { constexpr T UNKNOWN_DIM -1; // 将算子输出的维度数设置为reshape后的维度数reshape_rank output_shape.SetDimNum(reshape_rank); auto x_shape_size x_shape.GetShapeSize(); int64_t output_shapesize 1; size_t unknown_dim_idx std::numeric_limitssize_t::max(); for (int32_t i 0; i reshape_rank; i) { if (reshape_dims[i] ! UNKNOWN_DIM) { // reshape后某一轴的维度值不为-1 output_shape.SetDim(i, reshape_dims[i]); // 设置输出的维度值为reshape后的维度值 output_shapesize * reshape_dims[i]; // 计算当前输出元素数量 } else { output_shape.SetDim(i, 1); // reshape后某一轴的维度值为-1临时设置输出的维度值为1后续计算后看是否可以推导出确定值并记录未知维度的索引 unknown_dim_idx i; } } if (unknown_dim_idx std::numeric_limitssize_t::max() output_shapesize x_shape_size) { return ge::GRAPH_SUCCESS; // 不存在未知维度且输出shape size和输入x的shape size一致直接返回成功 } else if (unknown_dim_idx ! std::numeric_limitssize_t::max() x_shape_size % output_shapesize 0) { output_shape.SetDim(unknown_dim_idx, x_shape_size / output_shapesize); // 存在未知维度根据输入shape动态调整未知维度值保持总元素个数不变 return ge::GRAPH_SUCCESS; } return ge::GRAPH_FAILED; } ge::graphStatus InferShapeForReshape(InferShapeContext *context) { const gert::Shape *x_shape context-GetInputShape(0); // 获取第0个输入的shape const gert::Tensor *shape_tensor context-GetInputTensor(1); // 获取第1个输入的tensor gert::Shape *output_shape context-GetOutputShape(0); if (x_shape nullptr || shape_tensor nullptr || output_shape nullptr) { // 防御式编程不应该出现的场景打印错误并返回失败 return ge::GRAPH_FAILED; } auto reshape_size static_castint32_t(shape_tensor-GetShapeSize()); if (reshape_size 1) { // 防御式编程不应该出现的场景打印错误并返回失败 return ge::GRAPH_FAILED; } // 根据原型信息Reshape的shape输入支持INT32与INT64两类根据不同的类型进入对应的模板函数中做真正的shape变换操作 if (shape_tensor-GetDataType() ge::DT_INT32) { int32_t *reshape_data shape_tensor-GetDataint32_t(); return ReshapeInferShapeImplint32_t(reshape_data, *x_shape, *output_shape, reshape_size); } else { int64_t *reshape_data shape_tensor-GetDataint64_t(); return ReshapeInferShapeImplint64_t(reshape_data, *x_shape, *output_shape, reshape_size); } }[!CAUTION]注意只有声明过数据依赖的输入才可以在InferShape时调用GetInputTensor等获取tensor的接口获取其对应的tensor数据。若对一个未声明数据依赖的输入调用GetInputTensor等获取tensor的接口只能在tensor中获取到正确的shape、format、datatype信息无法获取到真实的tensor数据地址获取到的地址为nullptr。从tensor中获取tensor_data时(GetDataint32_t或GetDataint64_t)使用者需要保证获取的数据类型是正确的否则行为是未定义的。【免费下载链接】asc-devkit本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言原生支持C和C标准规范主要由类库和语言扩展层构成提供多层级API满足多维场景算子开发诉求。项目地址: https://gitcode.com/cann/asc-devkit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考