前言你写了个Ascend C算子编译成.o文件往GE图引擎里一塞——GE怎么知道这个算子叫什么名字、要几个输入、输出什么类型、有哪些属性答案是metadef。它给每个算子发身份证GE编译计算图时查这个身份证来理解你的算子。没有身份证GE不认识你的算子编译直接报错。这篇文章讲metadef是什么、里面有什么、怎么给你的算子写metadef。metadef是什么打个比方metadef是餐厅的菜单算子是菜GE是厨师。厨师GE要做菜编译计算图必须先看菜单metadef知道有哪些菜、每道菜要什么食材输入、用什么调料属性、出什么成品输出。没有菜单厨师不知道怎么下单。metadef定义了CANN生态中所有算子的元数据算子叫什么名字比如MatMul有几个输入每个输入的类型和shape约束比如输入A是2D FP16 tensor有几个输出每个输出的类型和shape比如输出C是2D FP16 tensor有哪些属性每个属性的类型和默认值比如transpose_a: bool, 默认false输出的shape和dtype怎么从输入推导出来InferShape/InferDataTypemetadef本身不包含算子的计算逻辑那是Ascend C代码的事只包含算子的描述信息。就像菜单只告诉你菜名和食材不教你炒菜。metadef里有什么metadef的核心内容分三部分算子原型OpProto、算子注册REGISTER_OP、推导函数InferShape/InferDataType。算子原型OpProtoOpProto定义了算子的外貌——名字、输入、输出、属性// ops-math仓库中MatMul算子的OpProto定义namespacege{classMatMulOp:publicOpProto{public:MatMulOp(){// 算子名字SetName(MatMul);// 输入定义Input(x1).SetDataType(DT_FLOAT16)// 输入x1的类型FP16.SetShape({-1,-1});// 输入x1的shape2D维度不限Input(x2).SetDataType(DT_FLOAT16).SetShape({-1,-1});// 输出定义Output(y).SetDataType(DT_FLOAT16).SetShape({-1,-1});// 属性定义Attr(transpose_x1).SetType(ATTR_BOOL)// 属性类型bool.SetDefaultValue(false);// 默认值falseAttr(transpose_x2).SetType(ATTR_BOOL).SetDefaultValue(false);}};}这段代码的意思MatMul算子有2个FP16输入x1, x2、1个FP16输出y、2个bool属性transpose_x1, transpose_x2。算子注册REGISTER_OP写好OpProto后用REGISTER_OP宏把它注册到全局注册表// 注册MatMul算子到全局注册表REGISTER_OP(MatMulOp);GE编译计算图时遇到一个叫MatMul的算子节点就去全局注册表里查MatMulOp拿到输入/输出/属性的定义然后做类型检查和shape推导。推导函数InferShape / InferDataTypeInferShape根据输入的shape推导输出的shape。这是GE编译的关键——GE需要知道每个算子输出的shape才能给输出tensor分配内存。// MatMul的InferShape// 输入x1: [M, K], x2: [K, N] → 输出y: [M, N]graphStatusMatMulInferShape(constge::OpDescPtrop_desc){autox1_shapeop_desc-GetInputDesc(0).GetShape();autox2_shapeop_desc-GetInputDesc(1).GetShape();int64_tMx1_shape.GetDim(0);// x1的第0维int64_tK1x1_shape.GetDim(1);// x1的第1维int64_tK2x2_shape.GetDim(0);// x2的第0维int64_tNx2_shape.GetDim(1);// x2的第1维// 检查K维度匹配if(K1!K2){returnGRAPH_FAILED;// K不匹配编译失败}// 设置输出shapege::GeShapey_shape({M,N});op_desc-GetOutputDesc(0).SetShape(y_shape);returnGRAPH_SUCCESS;}InferDataType根据输入的dtype推导输出的dtype。对于MatMul输出类型跟输入相同graphStatusMatMulInferDataType(constge::OpDescPtrop_desc){autox1_dtypeop_desc-GetInputDesc(0).GetDataType();op_desc-GetOutputDesc(0).SetDataType(x1_dtype);// 输出类型输入类型returnGRAPH_SUCCESS;}实战给你的Ascend C算子写metadef假设你写了一个自定义融合算子MatMulRelu矩阵乘ReLU融合需要给它写metadef让GE认识。Step 1定义OpProto// custom_op_proto.hnamespacege{classMatMulReluOp:publicOpProto{public:MatMulReluOp(){SetName(MatMulRelu);// 输入跟MatMul一样2个FP16矩阵Input(x1).SetDataType(DT_FLOAT16).SetShape({-1,-1});Input(x2).SetDataType(DT_FLOAT16).SetShape({-1,-1});// 输出1个FP16矩阵ReLU后Output(y).SetDataType(DT_FLOAT16).SetShape({-1,-1});// 属性跟MatMul一样Attr(transpose_x1).SetType(ATTR_BOOL).SetDefaultValue(false);Attr(transpose_x2).SetType(ATTR_BOOL).SetDefaultValue(false);}};}Step 2实现InferShape和InferDataType// custom_op_infer.ccgraphStatusMatMulReluInferShape(constge::OpDescPtrop_desc){autox1_shapeop_desc-GetInputDesc(0).GetShape();autox2_shapeop_desc-GetInputDesc(1).GetShape();int64_tMx1_shape.GetDim(0);int64_tK1x1_shape.GetDim(1);int64_tK2x2_shape.GetDim(0);int64_tNx2_shape.GetDim(1);if(K1!K2){returnGRAPH_FAILED;}// 输出shape跟MatMul一样ReLU不改变shapege::GeShapey_shape({M,N});op_desc-GetOutputDesc(0).SetShape(y_shape);returnGRAPH_SUCCESS;}graphStatusMatMulReluInferDataType(constge::OpDescPtrop_desc){autox1_dtypeop_desc-GetInputDesc(0).GetDataType();op_desc-GetOutputDesc(0).SetDataType(x1_dtype);returnGRAPH_SUCCESS;}Step 3注册算子// custom_op_register.ccREGISTER_OP(MatMulReluOp).InferShape(MatMulReluInferShape).InferDataType(MatMulReluInferDataType);Step 4编译并安装# 编译metadef动态库g-shared-fPIC-olibcustom_op_proto.so\custom_op_proto.cc custom_op_infer.cc custom_op_register.cc\-I/usr/local/Ascend/ascend-toolkit/latest/metadef/include\-L/usr/local/Ascend/ascend-toolkit/latest/metadef/lib\-lgraph-lge_common# 安装到CANN的算子目录sudocplibcustom_op_proto.so\/usr/local/Ascend/ascend-toolkit/latest/opp/built-in/op_impl/ai_core/tbe/op_tiling/安装后GE编译时就能识别MatMulRelu这个算子了。metadef在CANN架构中的位置metadef位于CANN五层架构的第3层昇腾计算编译层被GE图编译器引用第3层昇腾计算编译层 ├─ Graph CompilerGE图编译器 │ ├─ 查metadef获取算子元数据 │ ├─ 调InferShape推导输出shape │ └─ 分配输出内存 ├─ metadef算子元数据定义← 你在这里 └─ BiSheng / ATC 编译器依赖关系metadef ← ge ← 所有算子仓库。每个算子仓库ops-math、ops-nn等都依赖metadef来注册自己的算子。踩坑实录坑1InferShape写错了GE编译报output shape mismatch问题InferShape推导的输出shape跟算子实际输出的shape不一致GE报错。原因InferShape是静态推导编译时算子执行是动态计算运行时。如果InferShape推导错了GE给输出分配的内存大小不对运行时会出问题。解决方案InferShape要跟算子的实际输出严格一致。不确定时用-1标记动态维度表示运行时才能确定// ✅ 如果输出某个维度是动态的用-1ge::GeShapey_shape({-1,N});// M维度动态op_desc-GetOutputDesc(0).SetShape(y_shape);坑2属性类型必须是metadef支持的问题想定义一个自定义结构体类型的属性编译报错。原因metadef只支持以下属性类型int、float、string、bool、list、list、list。不支持自定义结构体。解决方案把结构体拆成多个属性或者序列化成string// ❌ 错误写法自定义结构体Attr(config).SetType(ATTR_CUSTOM_STRUCT);// ✅ 正确写法拆成多个属性Attr(config_batch_size).SetType(ATTR_INT);Attr(config_seq_len).SetType(ATTR_INT);Attr(config_dtype).SetType(ATTR_STRING);坑3算子名字不能跟已有算子重复问题注册了一个叫MatMul的算子但ops-math已经注册了同名算子冲突。原因全局注册表里算子名字必须唯一。重复注册会覆盖已有定义。解决方案自定义算子加前缀或后缀避免冲突SetName(Custom_MatMulRelu);// 加Custom_前缀结尾metadef不起眼但没它整个编译链就断了。你写的Ascend C算子再好GE不认识它就无法编译执行。metadef就是给算子发身份证——告诉GE这个算子叫什么、要什么输入、出什么输出、有哪些属性。身份证写对了GE才能正确编译和调度你的算子。写自定义算子时别只写Ascend C代码把metadef也一起写了。这是算子开发的基本功。https://atomgit.com/cann/metadef