前言
昇腾CANN的计算能力通过算子这一基本抽象单元呈现给开发者,但算子本身只是执行单元的静态描述——它定义了输入、输出、属性和计算逻辑,却无法表达算子之间的协作关系、数据流动方式、以及与昇腾硬件特性的匹配策略。真正决定推理或训练系统行为的是计算图,而非孤立的算子。
metadef是昇腾CANN中负责元定义(Meta Definition)的核心框架,它的职责是为算子和计算图提供更高层次的抽象能力。在metadef的设计中,算子不再是简单的输入输出映射,而是一个包含语义约束、性能约束、融合约束等多维度信息的复杂对象。计算图也不再是算子之间的简单连接,而是一个包含执行计划、调度策略、资源分配等全局优化目标的复杂结构。metadef将这些信息以声明式的方式组织起来,使得计算图的构建、优化和执行都可以通过统一的抽象接口完成,而不需要开发者直接操作底层的图数据结构。
理解metadef的设计对于深入理解CANN的计算抽象层至关重要。在实际开发中,无论是使用ge引擎构建计算图,还是使用autofuse进行融合优化,抑或是使用runtime进行执行调度,其底层都依赖metadef提供的抽象能力。metadef是CANN大厦的地基,理解它有助于理解整座建筑的设计逻辑。
从算子原语到元定义的抽象层次
传统的算子库设计将算子视为原子计算单元,每个算子独立定义自己的输入输出接口和计算逻辑。这种设计的优点是简单直接,但缺点是难以表达算子之间的协作意图——当一个算子需要向系统表明它希望与前后算子融合、它对输入数据的排布格式有特定要求、或者它对执行顺序有依赖约束时,传统的算子接口无法提供这些表达能力。
metadef引入了元定义的概念来解决这一问题。元定义是一种声明式的算子描述,它在传统算子定义的基础上增加了语义层信息。语义层信息包括:算子的数据依赖约束(如输入A必须在输入B之前完成计算)、内存排布约束(如输出必须为NHWC格式)、融合候选约束(如可以与激活函数融合)、性能约束(如对AI Core的利用率期望)等。这些信息以元数据的形式附加在算子上,使算子成为一个"自描述"的对象。
# metadef Python API:声明带语义约束的算子importmetadef# 定义一个带元定义的矩阵乘法算子matmul_meta=metadef.define_op(name="matmul_fused",inputs=[metadef.input("x",dtype="bfloat16",layout="ND"),metadef.input("w",dtype="bfloat16",layout="NK"),],outputs=[metadef.output("y",dtype="bfloat16",layout="ND"),],attrs={"transpose_b":False,"act_type":"none",# 可选:relu/relu6/hswish等},# 元定义:语义约束meta={"fusion_candidates":["bias_add","act","softmax"],"preferred_layout":"NC1HWC0",# AI Core友好的数据排布"memory_hint":{"input_x_lifetime":"persistent",# 输入可持久化"output_y_lifetime":"temporary",# 输出临时使用后可释放},"compute_density":"high",# 算力密集型})元定义将算子的执行特性与算子本身分离存储,而不是将所有信息编码到算子名称或参数中。这种设计的核心优势是保持了算子接口的简洁性——普通用户只需指定输入输出和基本参数即可使用算子,而高级用户可以通过meta字段表达复杂的执行意图。系统根据meta信息自动推断最优的执行策略,无需用户在每次调用时重复指定相同的配置。例如指定fusion_candidates后,系统在编译时会自动检查这些候选算子是否出现在matmul附近,如果是则尝试将其融合,而不需要用户手动编写融合规则。这种声明式设计将"做什么"(融合意图)与"怎么做"(融合执行)分离,使接口更加清晰。
计算图的元驱动构建
在metadef中,计算图不是通过手动连接算子构建的,而是通过元驱动的自动推导生成的。开发者只需要声明最终需要计算的表达式,系统会根据算子的元定义自动推导数据流、插入必要的格式转换算子、规划融合路径、分配执行资源。
元驱动构建的核心是算子之间的类型推导和布局推导机制。每个算子的元定义中包含输入输出的数据类型和内存排布约束,系统根据这些约束自动推断相邻算子之间的数据是否兼容。如果不兼容,系统会自动插入格式转换算子(如TransData算子)来桥接两者。开发者不需要手动处理这些转换细节——它们是元推导的自动产物。
# metadef:元驱动的计算图构建importmetadef# 声明最终计算目标(而非中间过程)withmetadef.Graph("inference")asg:# 只需要声明输入和最终输出x=g.input("x",shape=[1,224,224,3],dtype="float32",layout="NHWC")# 声明计算目标:多层融合推理y=metadef.invoke("resnet50_inference",x,params={"num_classes":1000,"fused_mode":"auto",# 元驱动:自动选择融合策略"layout_hint":"NCHW",# 系统自动插入TransData处理NHWC到NCHW的转换})g.output(y)# 系统自动推导:# - 插入必要的TransData算子(NHWC->NCHW)# - 根据算子meta中的fusion_candidates自动规划融合路径# - 分配计算资源(AI Core编号、UB占用)# - 生成执行计划compiled=metadef.compile(g,target="Atlas-A2-32GB")print(f"融合算子数量:{compiled.fused_op_count}")print(f"预计HBM访问量:{compiled.estimated_hbm_access_gb}GB")元驱动构建将计算图的构建过程从"描述过程"转变为"描述目标"。开发者只需要声明"我需要从x得到y"而不是"我需要按顺序执行A、B、C、TransData、D",系统的元推导引擎会自动填充中间步骤。这种设计极大地降低了计算图构建的复杂度,同时保证了最优执行路径的系统性探索——因为中间步骤不是由人工指定(可能遗漏),而是由系统根据元定义信息自动推导(不会遗漏)。同时,这种设计使得同一个计算图可以在不同的硬件目标上自动适配,自动生成与硬件匹配的融合计划和资源分配方案。
属性继承与算子变体推导
metadef的另一个重要能力是属性继承与算子变体推导。在实际模型中存在大量相似的算子组合,它们共享相同的计算逻辑但在数据形状、精度级别、融合选项等维度上有所不同。metadef通过属性继承机制允许开发者定义算子的基础模板,随后通过继承和重载生成具体的算子变体。
属性继承采用类似面向对象的思想:基算子定义通用的计算逻辑和元定义,子算子在基算子的基础上重载特定的属性(如输入形状范围、精度级别、融合选项)。子算子自动继承基算子的所有元定义信息,除非显式覆盖。
# metadef:属性继承与算子变体推导importmetadef# 定义基算子(通用矩阵乘法)base_matmul=metadef.define_op(name="matmul_base",inputs=[metadef.input("x",dtype="bfloat16"),metadef.input("w",dtype="bfloat16"),],outputs=[metadef.output("y",dtype="bfloat16")],meta={"compute_density":"high","fusion_candidates":["bias","act"],})# 派生高精度变体(FP32累加)matmul_fp32=metadef.derive_op(base_matmul,name="matmul_fp32_acc",attrs={"accum_dtype":"float32"},meta_overrides={"precision_level":"high",# 覆盖:提升精度级别})# 派生量化变体(INT8)matmul_int8=metadef.derive_op(base_matmul,name="matmul_int8",attrs={"quant_mode":"symmetric"},meta_overrides={"preferred_layout":"NCHWc",# 覆盖:量化友好的数据排布"memory_hint":{"input_x_lifetime":"temporary"},# 覆盖:量化后输入不需要持久化})属性继承机制解决了算子库维护中的一个核心问题:随着支持的硬件配置、数据类型、精度级别越来越多,算子变体的数量会指数级增长。如果为每种配置组合都独立定义一个算子,算子库的维护成本将变得不可接受。属性继承通过将"通用逻辑"与"具体配置"分离,使得通用逻辑只需要定义一次,具体配置通过继承自动生成。当基算子的计算逻辑更新时,所有派生变体都会自动继承更新,避免了重复定义和同步更新遗漏的问题。同时,派生变体可以针对特定场景覆盖元定义中的特定字段(如精度级别、内存排布),实现通用性与特殊性的平衡。
执行计划与资源分配
metadef生成的计算图最终需要映射到昇腾硬件上执行,这一过程由执行计划(Execution Plan)主导。执行计划描述了计算图中每个算子的执行时机、执行位置、以及与其他算子的协调方式。
资源分配是执行计划中的核心问题。每个AI Core的UB容量有限,不可能同时容纳所有算子的数据。metadef的资源分配器根据算子元定义中的memory_hint字段,计算每个算子所需的UB占用量,随后将算子分配到合适的AI Core上执行。分配策略需要同时考虑负载均衡(避免某些Core过载而其他Core空闲)和数据局部性(相邻算子尽量分配到同一Core以减少核间数据搬运)。
// metadef C++ API:查询执行计划#include<metadef/metadef.h>// 编译计算图获取执行计划metadef_graph*graph=metadef_graph_load("resnet50_graph.mdl");metadef_compile_options opts={.target="Atlas-A2-32GB",.optimization_level=2,.enable_fusion=true,};// 执行编译(生成执行计划)metadef_plan*plan=metadef_compile(graph,&opts);// 查询执行计划详情for(inti=0;i<metadef_plan_get_op_count(plan);i++){metadef_op_desc*op=metadef_plan_get_op(plan,i);printf("算子: %s\n",metadef_op_get_name(op));printf(" 执行核: [%d-%d]\n",metadef_op_get_core_range_start(op),metadef_op_get_core_range_end(op));printf(" 融合组: %s\n",metadef_op_get_fusion_group(op)?metadef_op_get_fusion_group(op):"无");printf(" UB占用: %lu bytes\n",metadef_op_get_ub_usage(op));printf(" 预计执行时间: %.2f us\n",metadef_op_get_estimated_time(op));}执行计划的查询接口允许用户在编译后检查每个算子的具体执行安排,而不需要在编译过程中干预。通过core_range字段可以确认算子是否被正确分配到预期的AI Core上;fusion_group字段可以确认哪些算子被融合为同一kernel;ub_usage字段可以验证UB占用是否在合理范围内;estimated_time字段则用于与实际profiling结果对比,校准成本模型的准确性。这种可观测性对于调试融合问题、定位性能瓶颈非常重要——用户可以看到"系统认为"算子应该如何执行,而不是面对一个黑盒的执行引擎。estimated_time字段还可以用于对比不同融合方案的性能预期,帮助用户在多个候选方案中选择最优解。
在实际使用metadef构建复杂计算图时,开发者通常会经历几个阶段的学习曲线。初级使用者只需要学会声明输入输出和调用invoke接口,系统会自动处理中间的一切细节,这个阶段的体验与使用高层框架(如TensorFlow或PyTorch)非常接近。中级使用者开始需要理解元定义中的约束字段,学会通过fusion_candidates和preferred_layout等参数引导系统生成更优的执行计划。高级使用者则需要深入理解属性继承机制和执行计划查询接口,能够通过派生算子变体和检查执行计划来优化极端场景下的性能。
元定义信息的积累是一个持续的过程。当一个新的算子被引入CANN生态时,它的元定义需要由算子开发者提供。如果元定义缺失或不准确,算子可能无法被正确融合、无法被正确调度到合适的AI Core上、或者无法在跨硬件场景下正确适配。metadef的设计鼓励算子开发者提供完整的元定义信息,因为这些信息最终会转化为更好的执行效率。
使用前后的效率对比
metadef的核心价值在于将计算图的构建、优化和执行通过统一的抽象层管理,使得每个环节都可以利用系统的全局信息做出最优决策。以下从多个维度对比使用metadef前后的差异:
| 维度 | 使用前 | 使用后 | 差异来源 |
|---|---|---|---|
| 计算图构建方式 | 手动连接每个算子,需要开发者显式处理格式转换、融合边界等中间细节 | 声明最终计算目标,系统根据元定义自动推导中间步骤和融合策略 | 从过程描述升级为目标描述,降低开发者负担同时提升系统优化空间 |
| 算子库维护成本 | 每种配置组合需要独立定义一个算子变体,变体数量指数增长 | 通过属性继承从基算子自动派生变体,通用逻辑只需定义一次 | 属性继承机制减少重复定义,同步更新只需修改基算子 |
| 跨硬件适配成本 | 同一计算图在不同硬件上需要手动调整执行计划 | 切换目标硬件后元推导引擎自动重新规划融合路径和资源分配 | 元驱动构建天然支持多硬件目标,自动适配 |
| 执行计划可观测性 | 执行引擎为黑盒,用户无法检查算子分配和融合决策的依据 | 通过plan查询接口可查看每个算子的执行核、融合组、UB占用等详情 | 可观测性设计支持性能调试和成本模型校准 |
| 融合策略探索效率 | 人工评估融合收益,周期长且容易遗漏潜在融合机会 | 系统自动评估全图所有可能的融合组合,基于成本模型量化排序 | 自动化融合探索覆盖完整,人工只需做最终决策 |
metadef的执行计划还支持动态调整能力。在推理服务的生命周期中,输入数据的分布可能发生变化(例如昼夜负载差异、季节性流量变化等)。metadef的执行计划可以根据输入数据的实际分布动态调整算子的执行参数,如tiling策略、融合边界、资源分配比例等。这种自适应能力使得推理服务可以在不同的负载条件下持续保持接近最优的执行效率,而不需要人工干预重新编译模型。在生产环境中,这种动态调整能力对于应对突发流量和季节性业务高峰尤为重要。
metadef与算子编译流程的集成深度远超大多数开发者的预期。当用户调用metadef_compile时,编译器不仅生成执行计划,还会将元定义信息作为编译参数传递给底层的算子编译器(如Akg)。这意味着即使两个算子变体在数据类型和形状上完全相同,如果它们的元定义不同(如fusion_candidates不同),底层的算子编译器也可能生成不同的kernel代码。因此,metadef不仅是计算图层面的抽象层,也是算子编译层的元信息来源,两者通过元定义信息紧密集成,共同构成CANN的编译优化基础设施。
metadef与CANN生态的协作
metadef是CANN生态中的基础设施层,它为上层组件提供统一的抽象接口,同时接收来自底层硬件的反馈。ge引擎使用metadef的图抽象来构建和管理计算图;autofuse使用metadef的元定义信息来评估融合收益;runtime使用metadef的执行计划来调度算子执行;profiling工具使用metadef的执行计划来标注性能数据。
这种分层设计使得CANN生态的各个组件可以独立演进而不影响其他组件的接口。ge不需要了解runtime如何调度算子,runtime不需要了解autofuse如何评估融合收益,metadef作为共同的基础层确保了各组件之间的接口一致性。同时,这种设计使得新组件的引入变得简单——只需要实现与metadef的接口适配即可接入CANN生态。
仓库地址:https://atomgit.com/cann/metadef