CANN技术解读|metadef元数据结构与模型定义规范——深度解析昇腾CANN计算架构中基础数据层的核心设计
前言
在深度学习框架与底层硬件之间的漫长链路中,存在一个常被忽视但至关重要的层次:基础数据结构定义层。昇腾AI处理器配套的CANN(Compute Architecture for Neural Networks)软件栈覆盖了从模型训练到推理部署的完整生命周期,而这个软件栈的根基之一就是metadef仓库——即"昇腾元数据定义"(Ascend MetaData Definitions)。metadef为CANN平台中Graph Engine(图引擎)以及各类算子仓库(ops-nn、ops-math、ops-transformer、ops-cv等)提供共享的基础数据结构和对外接口。可以将其理解为CANN生态中各组件之间"说同一种语言"的词典。没有这份词典,图编译器、算子库、运行时引擎各自为政,整个软件栈将无法协同工作。
metadef能解决什么问题
模型定义与交换的标准化需求
深度学习模型的推理部署涉及多个环节:前端框架(如MindSpore、PyTorch、TensorFlow)将模型转换为计算图,图引擎对计算图进行优化和编译,算子库提供各算子的具体实现,最终在昇腾硬件上执行。这条链路上,每个环节都需要理解"张量是什么"“算子有哪些属性”"数据格式有哪些种类"等基础概念。如果图引擎内部使用一套Tensor定义,算子库使用另一套,框架转换层又使用第三套,那么三方之间的数据交换将变得极其脆弱——任何一方的字段变更都会引发连锁的接口不兼容问题。
metadef存在的核心价值就是消除这种冗余与不一致。它作为CANN平台唯一的权威基础类型定义源,统一定义了Tensor、Shape、DataType、Format、Attr等核心数据结构,以及算子注册、执行上下文构建等关键接口。上层组件(ge、ops)通过引用metadef的头文件来获得一致的类型定义,从根本上杜绝了"方言"问题。
算子注册的规范化
在昇腾平台上开发自定义算子时,开发者需要向系统注册算子的类型、输入输出描述、属性列表、Tiling策略等信息。如果每个算子开发者各自定义注册格式,算子管理将陷入混乱。metadef的register模块提供了一套标准化的算子注册接口和类型定义,使得所有算子遵循统一的注册契约。这不仅是代码规范的问题,更直接影响算子能否被图引擎正确发现、调度和执行。
跨组件ABI兼容性保障
在CANN这样的大型软件栈中,各组件可能由不同团队开发、独立发布版本。组件之间的二进制接口(ABI)兼容性是系统能否稳定运行的关键。metadef通过集中管理基础类型定义和对外接口,使得ABI变更可以被集中控制和严格审查。任何对基础类型的修改都必须经过兼容性评估,确保不会破坏上层组件的已有二进制。这种集中管控的机制,在分布式的开发协作模式中具有不可替代的工程价值。
核心数据结构与定义规范
metadef仓库的源码以头文件为主,按照功能模块组织在inc目录下。理解这些模块的划分,就掌握了metadef的整体骨架。
目录结构与模块划分
metadef的头文件目录采用"内部实现"与"对外发布"双层结构。inc/base和inc/graph、inc/register、inc/common等目录存放内部头文件,供metadef自身实现和上层组件内部使用。inc/external目录则是对外发布接口的集中地,其下又细分为external/graph(图相关对外类型)、external/register(算子注册对外接口)、external/base(基础类型对外发布)、external/ge(图引擎特定类型)、external/ge_common(图引擎通用类型)、external/asc(Ascend CL相关类型)、external/exe_graph(执行图相关类型)以及external/utils(工具类对外接口)。
这种内外分离的设计并非形式主义。内部头文件可以自由修改、重构,只要不影响对外发布头文件中声明的接口签名即可。对外头文件则是ABI兼容性的红线区域,变更需要经过严格的评审流程。
基础数据类型体系
metadef定义的基础数据类型体系是整个CANN平台的类型基石。其中最为核心的包括:
Tensor(张量):Tensor是深度学习中数据的基本载体。metadef在inc/external/graph/tensor.h中定义了Tensor类的对外接口,封装了张量的数据类型、形状、格式、存储位置等属性。图引擎在编译优化过程中频繁创建、修改、传递Tensor对象,算子实现则根据Tensor的属性来决定计算逻辑。一个统一的Tensor定义,使得编译期和运行期对"数据长什么样"的认识完全一致。
DataType(数据类型):inc/external/graph/c_types.h中定义了CANN平台支持的所有数据类型枚举,涵盖DT_FLOAT、DT_DOUBLE、DT_INT32、DT_INT64、DT_BOOL等常见类型,以及DT_HIFLOAT4等昇腾硬件特有的高精度类型。数据类型枚举是类型推导、格式转换、内存对齐计算的基础依据。
Format(数据格式):张量在内存中的存储方式直接影响计算效率。Format定义描述了张量的内存布局方式,例如NCHW(批量-通道-高度-宽度)、NHWC、NC1HWC0(昇腾特有的分块格式)等。图引擎在优化过程中需要进行格式转换,算子实现需要根据输入输出格式选择对应的计算内核。metadef对Format的统一定义,使得格式转换规则可以集中管理。
Shape(形状):张量的维度信息。Shape定义不仅存储各维度的大小,还提供维度推导、广播计算等工具方法。在图编译阶段,Shape信息用于算子类型推导和内存预分配;在运行时阶段,Shape信息用于动态分块(Tiling)策略的计算。
Attr(属性):算子属性是算子行为的参数化描述。metadef在inc/base/attr目录下提供了属性定义的基础设施,支持多种属性类型(整数、浮点、字符串、列表、布尔等)。算子通过属性来接收超参数、模式选择等信息,图引擎通过属性来决定优化策略。
metadef在inc/external/graph/c_types.h中以枚举形式定义了CANN平台支持的全部数据类型,包括DT_FLOAT、DT_FLOAT16、DT_INT8、DT_INT32、DT_INT64、DT_UINT8、DT_BOOL、DT_DOUBLE、DT_STRING等通用类型,以及DT_HIFLOAT4等昇腾硬件特有类型。这些枚举值在CANN全平台范围内保持统一编码,图引擎进行类型推导时、算子库选择计算内核时、运行时进行内存分配时,引用的都是同一个枚举定义。集中定义DataType而非分散在各组件内部,是为了保证跨组件类型语义的严格一致——如果图引擎和算子库对同一类型的编码值不同,类型推导链条就会断裂,导致运行时计算错误。
算子注册接口体系
metadef的register模块是算子开发者最常接触的部分。它定义了算子向CANN平台注册自身的标准流程和数据结构。注册信息包括算子类型标识(OpType)、输入输出描述(tensor的个数和约束)、属性列表(名称、类型、默认值)、支持的硬件平台、Tiling策略等。
inc/external/register目录下的头文件构成了注册接口的完整集合:
- register.h:算子注册的总入口,提供REGISTER宏和注册辅助函数
- register_types.h:注册过程中使用的通用类型定义
- register_fmk_types.h:不同框架适配的类型定义
- op_impl_registry.h:算子实现注册的核心接口
- op_impl_kernel_registry.h:算子内核注册接口
- op_bin_info.h:算子二进制信息描述
- tilingdata_base.h:Tiling数据基类定义
算子注册采用宏驱动的声明式接口。开发者通过REGISTER_OP宏声明算子的类型名称、输入输出Tensor的类型约束、属性列表(名称、类型、默认值)等信息。这些宏在编译期展开为类型安全的注册调用,编译器可以在编译阶段检查属性类型是否匹配、输入输出约束是否合法。相比运行时解析的配置文件方案,编译期检查能更早暴露错误,定位成本更低。宏展开后的注册代码在程序启动时通过C++全局对象的构造函数自动执行,算子开发者无需手动调用注册函数,减少了遗漏注册的风险。
Tiling数据基类
Tiling(分块)是昇腾硬件上的核心性能优化手段。由于昇腾AI处理器的计算单元对数据分块大小敏感,不同形状的输入需要不同的分块策略以最大化硬件利用率。metadef中的tilingdata_base.h定义了Tiling数据的基类,算子开发者在编写Tiling函数时继承该基类,按照统一格式序列化和反序列化分块参数。
Tiling数据之所以需要序列化基类,是因为Tiling计算通常发生在Host侧(CPU端),而算子实际执行发生在Device侧(NPU端)。Host侧计算出的分块参数必须以二进制形式传递给Device侧的算子内核。如果没有统一的序列化基类,每个算子都可能自行设计传递格式,Device侧的内核与Host侧的Tiling函数之间的数据契约将完全依赖人工约定,极易出现字段顺序不一致、字节对齐问题、大小端差异等隐蔽缺陷。metadef的tilingdata_base.h提供了OpTilingData基类,要求所有算子按照统一的Serialize/Deserialize接口编解码分块参数,将跨端数据传递问题从"人为约定"提升为"框架保障"。
算子实现注册与版本管理
metadef的inc/register目录还包含算子实现注册的管理机制。op_impl_registry_base.h定义了注册的基础框架,op_impl_registry_api.h提供对外API,op_impl_registry_holder_manager.h管理注册持有者(即具体的算子库)。这一套机制支持算子的多版本共存——同一个OpType可以有针对不同硬件平台、不同精度模式、不同优化策略的多个实现同时注册在系统中,运行时根据输入数据的实际特征选择最优的实现。
inc/register/opp_impl_version.h定义了算子实现版本标签(OpImplVersionTag),op_impl_space_registry.h定义了实现空间注册。这些机制使得算子库可以独立升级——新的算子实现可以注册为更高版本的实现,而旧版本仍然保留在系统中,确保向后兼容。
版本化的算子实现注册机制解决的是算子库独立演进的问题。在CANN平台上,算子仓库和图引擎是独立开发、独立发布的。当算子仓库发布了某个算子的优化实现后,图引擎需要能够发现并使用这个新实现,同时不影响尚未升级的环境中旧实现的使用。OpImplVersionTag使得"算子实现"从一个静态的单点注册,变成一个按版本排列的有序集合,运行时调度器可以根据当前硬件能力、精度要求、性能偏好等条件选择最合适的版本。没有这套机制,算子的任何优化都需要图引擎同步修改才能生效,两个仓库的发布节奏将被迫绑定。
在CANN多层架构中的协作关系
架构定位
metadef在CANN架构中的位置可以用"基础层"来概括。从上到下,CANN的软件架构大致可以分为以下层次:
最上层是应用框架层,包括MindSpore、PyTorch、TensorFlow等深度学习框架,它们通过CANN提供的适配插件将训练好的模型转换为CANN可处理的中间表示。
中间层是图引擎层(ge),负责计算图的解析、优化、划分和编译。图引擎将前端框架传入的计算图转换为昇腾硬件可执行的执行图,这一过程中涉及算子融合、内存分配、数据格式转换等大量优化操作。
再往下是算子库层,包含ops-nn(神经网络算子)、ops-math(数学算子)、ops-transformer(Transformer相关算子)、ops-cv(计算机视觉算子)等多个垂直领域的算子实现仓库。每个算子库提供特定领域算子的具体计算逻辑和性能优化内核。
最底层是运行时和驱动层,负责将编译好的执行图调度到昇腾硬件上执行,管理设备内存、任务队列等底层资源。
metadef位于算子库层和图引擎层之下,为这两层提供共享的基础类型定义和接口规范。同时,它也为运行时层提供部分类型定义(如设备端数据类型枚举)。因此,metadef虽然代码量不大(以头文件为主),但影响范围覆盖了CANN软件栈的核心层次。
与ge的协作关系
图引擎(ge)是metadef最核心的上层依赖者。ge在构建计算图的过程中,使用metadef定义的Tensor、Shape、DataType、Format等类型来描述图中的节点和边。ge的图优化pass在修改计算图结构时,操作的对象就是metadef定义的图元素类型。ge在类型推导时,依赖metadef定义的属性系统来查询和计算算子的输入输出类型关系。
ge与metadef之间的接口主要分布在inc/external/ge和inc/external/ge_common目录中。这些头文件定义了ge特有的类型(如ge_error_codes.h中的错误码、compiler_def.h中的编译器配置宏等),它们是ge对外暴露接口的一部分,由metadef统一发布,确保所有引用ge的组件获得一致的类型定义。
与算子仓库的协作关系
算子仓库对metadef的依赖主要体现在两个方面。一是基础类型依赖:算子实现需要使用Tensor、DataType、Format等类型来描述自己的输入输出和计算逻辑。这些类型全部来自metadef,算子仓库自身不再重复定义。二是注册接口依赖:算子通过metadef提供的注册宏和注册类型,将自己的元信息(类型、属性、输入输出约束等)注册到系统中。图引擎通过查询这些注册信息来发现和使用算子。
这种协作模式意味着,算子仓库的开发者虽然日常工作中不会直接修改metadef的代码,但需要深入了解metadef定义的类型系统和注册规范。算子的正确注册和实现,建立在对metadef接口的准确理解之上。
inc/internal与inc/external的分层逻辑
metadef将头文件划分为internal和external两部分,这个设计直接影响着上层组件的耦合度。internal头文件仅供metadef自身编译和ge、ops等CANN内部组件使用,不属于公开API承诺。external头文件则是对外发布的稳定接口,其变更受到ABI兼容性约束。
对于CANN生态中的第三方开发者而言,只需关注external目录下的头文件。internal目录中的类型和接口可能在任何版本中被修改或移除,不受兼容性承诺保护。这种分层策略在大型项目中是常见的实践——它允许基础库的内部实现自由演进,同时维持对外接口的稳定性。
典型使用场景与配置方法
场景一:开发自定义算子
这是metadef最常见的使用场景。开发者在CANN平台上实现一个新的自定义算子时,需要使用metadef的注册接口来声明算子的元信息,使用基础类型来描述输入输出的数据特征。
完整的自定义算子开发流程大致如下:首先,在算子信息定义文件中描述算子的类型、属性、输入输出约束;然后,使用metadef提供的注册宏将算子注册到系统中;接着,编写算子的Host侧Tiling函数和Device侧计算内核;最后,编译打包为算子库并部署到目标环境。
在这个流程中,metadef的类型系统和注册接口贯穿始终。算子的输入输出Tensor必须使用metadef定义的Tensor类型描述,算子属性必须按照metadef定义的Attr规范声明,Tiling数据必须继承metadef提供的基类。
场景二:图编译优化pass开发
当开发者需要在ge中开发新的图优化pass(例如一种新的算子融合策略)时,需要操作metadef定义的图元素类型。pass的输入是包含若干节点的计算图,每个节点的类型、属性、输入输出张量都是metadef定义的对象。pass通过修改这些对象的属性或连接关系来实现优化效果。
例如,一个算子融合pass需要检查两个相邻节点的属性是否满足融合条件,然后将它们合并为一个新节点。这个过程中,属性的读取和设置、节点的创建和连接,都依赖metadef定义的图操作接口。
场景三:算子库升级与多版本共存
当算子库需要升级某个算子的实现时,可以通过metadef提供的版本化注册机制来注册新版本实现,而保留旧版本作为回退选项。运行时调度器根据当前环境选择合适的版本执行。这种场景下,metadef的OpImplVersionTag和OpImplSpaceRegistry提供了基础设施支持。
代码段详解
代码段一:Tensor类型的基本使用
// 使用metadef定义的Tensor类型构建计算图节点
ge::TensorDesc tensor_desc;
tensor_desc.SetShape(ge::GeShape({-1, 3, 224, 224})); // NCHW, batch维度动态
tensor_desc.SetDataType(ge::DT_FLOAT);
tensor_desc.SetFormat(ge::FORMAT_NCHW);
tensor_desc.SetOriginShape(ge::GeShape({-1, 3, 224, 224}));
tensor_desc.SetOriginFormat(ge::FORMAT_NCHW);
ge::TensorPtr tensor = std::make_shared<ge::Tensor>();
tensor->SetTensorDesc(tensor_desc);
tensor->SetData(data_buf, data_size);
这段代码展示了metadef中TensorDesc的典型使用方式。关键在于SetShape传入的GeShape对象可以包含-1值,表示动态维度。图引擎在编译期处理动态形状模型时,需要区分"运行时已知的形状"(通过SetShape设置的当前形状)和"原始逻辑形状"(通过SetOriginShape设置的框架原始形状)。同样,SetFormat设置的是图引擎优化后的实际存储格式,SetOriginFormat记录的是框架传入的原始格式。这种"当前值/原始值"的双轨设计,是因为图优化过程会频繁改变张量的格式(例如将NCHW转换为NC1HWC0以适配昇腾硬件的存储偏好),但运行时可能需要回溯原始格式来生成调试信息或对接框架侧的回调。将两套信息绑定在同一个TensorDesc对象上,避免了额外的映射表维护。
代码段二:算子属性定义与使用
// 在metadef的Attr体系中定义和使用算子属性
namespace ge {
class AttrUtils {
public:
// 设置算子属性
static graphStatus SetInt(Operator &op, const std::string &name, int64_t val) {
return op.SetAttr(name, AttrValue::CreateFrom(val));
}
// 获取算子属性,带默认值
static graphStatus GetInt(
const Operator &op, const std::string &name,
int64_t &val, int64_t default_val = 0) {
if (!op.HasAttr(name)) {
val = default_val;
return GRAPH_SUCCESS;
}
return op.GetAttr(name, val);
}
// 列表类型属性
static graphStatus SetListInt(
Operator &op, const std::string &name,
const std::vector<int64_t> &vals) {
return op.SetAttr(name, AttrValue::CreateFrom(vals));
}
};
} // namespace ge
这个代码段展示了metadef属性系统的工具方法设计。AttrUtils作为静态工具类,提供了类型安全的属性读写接口。设计上关注几个要点:一是属性通过字符串名称索引,算子可以自由定义属性名称,灵活性很高;二是获取接口提供默认值参数,当算子未设置某个可选属性时不会报错,而是返回默认值,这简化了算子实现中"处理可选参数"的代码量;三是属性值被包装在AttrValue对象中,这是一个支持多类型(int、float、string、list、tensor等)的变体类型。将多类型值统一包装,使得属性存储可以使用单一的底层容器(如std::map<string, AttrValue>),无需为每种属性类型维护独立的存储结构。这种设计在管理具有数百种算子、数千个属性的系统中,显著降低了类型管理的复杂度。
代码段三:算子实现注册的完整流程
// 算子信息定义(OpInfo)与实现注册的完整示例
// 第一部分:定义算子的元信息描述
IMPLEMENT_OP_INFO(Conv2D)
.INPUT_RANGE(2, 2) // 输入tensor数量固定为2
.OUTPUT_RANGE(1, 1) // 输出tensor数量固定为1
.REQUIRED_ATTR("strides", AttrType::LIST_INT)
.OPTIONAL_ATTR("padding_mode", AttrType::STRING, "CALCULATED");
// 第二部分:注册算子的Host侧Tiling实现
REGISTER_OP_TILING_FUNC(Conv2D, [](const TeOpParas& op_paras,
const std::vector<int64_t>& input_shapes,
OpTilingData& tiling_data) {
auto input_shape = input_shapes[0];
auto output_shape = input_shapes[1];
// 根据输入形状计算分块参数
uint32_t tile_h = ComputeTileHeight(input_shape, output_shape);
uint32_t tile_w = ComputeTileWidth(input_shape, output_shape);
// 将分块参数写入TilingData
auto* conv_tiling = static_cast<Conv2DTilingData*>(&tiling_data);
conv_tiling->tile_h = tile_h;
conv_tiling->tile_w = tile_w;
return GRAPH_SUCCESS;
});
这段代码将算子元信息定义与Tiling函数注册分为了两个独立的部分,这个分离并非偶然。元信息描述(IMPLEMENT_OP_INFO)是静态的——它描述算子"是什么",包括输入输出的数量约束和属性列表,这些信息在编译期就确定下来,图引擎据此进行静态检查和图构建。Tiling函数注册(REGISTER_OP_TILING_FUNC)是动态的——它描述"如何根据具体输入计算分块策略",这部分逻辑依赖运行时传入的具体形状信息,在模型编译阶段动态执行。将静态元信息与动态计算逻辑分离,使得图引擎可以在不需要实际数据的情况下先完成图结构的静态校验,只有在真正需要编译执行时才触发Tiling计算。这种两阶段设计避免了在静态分析阶段引入不必要的运行时依赖。
代码段四:格式推导与类型推导的协作
// 格式推导上下文的使用(简化示意,基于inc/external/graph/infer_format_context.h)
class InferFormatContext {
public:
graphStatus InferFormatAndShape(const Operator& op) {
// 获取输入张量的格式和形状
auto input_desc = op.GetInputDescByName("x");
auto input_format = input_desc.GetFormat();
auto input_shape = input_desc.GetShape();
// 根据算子类型和输入格式推导输出格式
Format output_format = FORMAT_NCHW; // 默认格式
if (IsNeedFracFormat(input_format, input_shape)) {
output_format = FORMAT_NC1HWC0; // 昇腾硬件优化格式
}
// 设置输出张量的格式
auto output_desc = op.GetOutputDescByName("y");
output_desc.SetFormat(output_format);
return op.UpdateOutputDesc("y", output_desc);
}
};
格式推导是图引擎优化中极为关键的一环。昇腾AI处理器的计算单元对数据格式有明确偏好——某些算子在高维分块格式(如NC1HWC0)下效率远高于标准NCHW格式,而另一些算子则要求标准格式。InferFormatContext封装了格式推导的决策逻辑,图引擎在编译阶段对每个算子节点调用格式推导,确定最优的输入输出格式组合。如果格式推导分散在各个算子实现中,图引擎的格式优化pass将无法统一管理格式转换策略。metadef将格式推导接口标准化,使得"应该用什么格式"的决策逻辑可以在图优化层面集中控制,而具体算子只需声明自己支持哪些格式,无需关心格式转换的插入位置和时机。这种关注点分离使得格式优化策略可以独立于算子实现进行演进。
metadef作为CANN平台的基础数据定义仓库,其设计哲学体现了一个工程原则:在大型软件系统中,共享基础类型的集中管理胜过各组件的自由定义。类型定义虽然不直接产生业务价值,但它决定了组件间协作的效率和质量。metadef通过DataType、Tensor、Shape、Format、Attr等核心类型定义,以及算子注册接口、Tiling数据基类、版本管理机制等功能模块,为CANN软件栈建立了一套统一的"数据语言"。对于CANN生态中的开发者而言,理解metadef的设计意图和使用规范,是进行算子开发、图优化和系统集成工作的前提。随着昇腾AI生态的持续扩展,metadef作为基础层的角色只会愈加重要——更多的上层组件意味着更强的类型一致性需求,而metadef正是满足这一需求的工程基石。
仓库地址:https://atomgit.com/cann/metadef
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐

所有评论(0)