解构 Metadef:异构计算架构的通用图中间表示 (IR) 与元数据定义
metadef (Meta Definition) 是整个异构计算软件栈的**“基因库”**。它定义了计算图(Graph)、算子(Operator)以及张量(Tensor)的通用数据结构。
在深度学习编译器和运行时系统中,不同组件(如 Parser, Graph Engine, Compiler)之间需要一种通用的语言来交流。metadef 提供了这套**中间表示(Intermediate Representation, IR)**的 C++ 定义。它不包含具体的算子实现逻辑,而是定义了“算子长什么样”、“图怎么连”以及“数据怎么排布”的元数据规范。
这是一篇关于 Metadef 元数据定义与计算图 IR 架构 的深度解析。
核心资源链接:
- CANN 核心架构: https://atomgit.com/cann
- Metadef 核心定义库链接: https://atomgit.com/cann/metadef
在现代 AI 软件栈中,框架层(如 PyTorch, TensorFlow)与硬件执行层之间存在巨大的语义鸿沟。metadef 仓库承载了填补这一鸿沟的核心职责——它定义了GE (Graph Engine) 及其周边组件所使用的统一 IR。这套 IR 不仅需要表达数据流(Data Flow),还需要承载控制流(Control Flow)、内存描述以及硬件特定的属性。
一、 统一的中间表示 (IR) 哲学:屏蔽框架差异
metadef 的首要设计目标是解耦。上层框架千差万别,底层硬件架构在不断演进,IR 必须作为稳定的中间层存在。它通过高度抽象的 C++ 类结构,将不同框架的算子映射为统一的内存对象。
1.1 强类型的图对象模型
在 metadef 中,计算图不再是简单的 JSON 或 Protobuf 文件,而是被实例化为具有严格类型约束的 C++ 对象:
- ComputeGraph:代表整个计算任务的拓扑结构,管理着所有节点(Node)的生命周期。
- Node:图中的基本单元,既可以是计算算子(如 Conv2D),也可以是控制节点(如 If/Loop),或者是数据节点(Data)。
- Anchor & Edge:定义了节点间的连接关系。每个节点拥有 InputAnchor 和 OutputAnchor,Edge 则是连接这两个锚点的实体,承载了数据流向的依赖信息。
1.2 跨框架的属性映射
不同框架对同一个算子的定义往往存在细微差别(例如 Padding 的策略、属性的命名)。metadef 提供了一套通用的属性容器(Attribute Map),支持存储 Int, Float, String, Tensor 等多种类型的数据。通过 Parser 模块,前端框架的特定属性被标准化为 IR 中的通用属性,从而使后端优化器(Optimizer)无需感知前端框架的细节。
二、 算子原型的核心抽象:OpDesc 与 Operator
在 IR 层,算子尚未绑定具体的内核代码(Kernel),它仅仅是一个原型(Prototype)。metadef 定义了描述这一原型的核心类。
- OpDesc (Operator Descriptor):
这是一个轻量级的描述符,存储了算子的元数据,如算子类型(OpType)、输入输出描述(TensorDesc)以及静态属性。它是图编译阶段优化 pass 处理的主要对象。 - Operator 类:
这是面向用户的接口类,通常用于构建算子工厂。它封装了对OpDesc的操作,提供了链式调用的 API(如.set_input(...)),使得开发者可以像搭积木一样通过 C++ 代码手动构建计算图。
这种分离设计的优势在于:
- 内存效率:在图遍历和简单查询时,仅需访问轻量级的 OpDesc。
- 扩展性:Operator 类可以作为对外的 SDK 接口,保持 API 稳定性,而 OpDesc 可以在内部演进。
三、 异构内存布局与数据格式的类型系统
NPU 硬件为了极致的计算效率,往往采用非标准的内存布局(如分形格式)。metadef 定义了极其丰富的数据类型和格式枚举,构成了异构计算的类型系统基础。
3.1 格式 (Format) 的多维定义
除了标准的 NCHW 和 NHWC,metadef 引入了硬件亲和的私有格式定义:
- NC1HWC0:专为向量计算单元设计的 5 维格式。
- FRACTAL_Z:专为矩阵乘法单元(Cube Unit)设计的重排格式。
- FRACTAL_NZ:用于特定内部传输的高效格式。
这些枚举值的存在,使得 Graph Engine 能够在编译期推断出两个算子之间是否需要插入格式转换(TransData)节点,从而实现自动化的数据流优化。
3.2 形状 (Shape) 的动态表达
在动态图和动态 Batch 场景下,张量的形状并不是固定的。metadef 中的 GeShape 类支持符号化表达:
- 静态维度:明确的正整数(如 64, 128)。
- 动态维度:使用 -1 表示该维度未知。
- 范围约束:在某些定义中支持
[min, max]的范围表达,辅助内存分配器预估最大所需内存。
四、 注册机制与静态工厂模式
为了支持成百上千种算子,且允许用户自定义算子(Custom Op),metadef 实现了一套去中心化的注册机制。
这套机制利用了 C++ 的静态全局变量初始化特性。每个算子的定义通过宏(如 REG_OP)展开为一个静态对象的构造过程。在程序启动(dlopen)时,这些构造函数自动执行,将算子的原型信息注册到全局的 OpRegistry 单例中。
- 解耦实现:算子的注册代码与框架核心代码完全分离,新增算子无需修改核心库。
- 按需加载:支持以动态库(.so)的形式插件化加载算子包。
五、 序列化与持久化:从内存对象到离线模型
计算图最终需要被保存为离线模型文件(.om),以便在推理服务器或端侧设备上加载。metadef 定义了序列化的协议标准,通常基于 Protocol Buffers。
5.1 模型文件的骨架
定义了 Model 的文件头结构、分区表(Partition Table)以及权重数据的存储方式。它规定了如何将内存中的 ComputeGraph 对象序列化二进制流。
5.2 兼容性设计
由于硬件代际更迭,IR 定义必须保持向后兼容。metadef 在设计序列化结构时,预留了大量的 Reserved 字段,并采用了 Versioning(版本号)机制。当加载器(Loader)读取模型时,会首先校验 IR 版本号,确保运行时环境能够正确解析模型中的指令含义。
六、 硬件约束的传播与推导接口
在图编译过程中,通过 metadef 定义的接口,编译器可以查询算子的具体行为,进行形状推导(Shape Inference)和数据类型推导(Type Inference)。
这一过程是递归的:
- Verify:校验输入参数是否符合算子定义的约束(如卷积核大小必须匹配)。
- InferShape:根据输入 Tensor 的 Shape,计算输出 Tensor 的 Shape。
- InferFormat:根据硬件连接关系,决定最佳的输出内存格式。
metadef 定义了这些推导函数的函数签名(Function Signature),而具体的推导逻辑则由算子开发者在注册时提供。这种接口与实现分离的设计,使得 metadef 保持了极致的轻量化,同时具备了强大的逻辑表达能力。
以下代码片段展示了 metadef 风格的 IR 节点定义与属性描述符结构。这不是可执行的业务代码,而是架构层面的 C++ 类定义,展示了如何通过结构体来抽象一个计算图中的节点及其属性。这体现了底层系统如何“看待”一个算子。
#include <string>
#include <vector>
#include <map>
#include <memory>
// 命名空间通常为 ge (Graph Engine) 或类似的基础命名空间
namespace ge {
// 数据类型枚举,定义了异构计算支持的所有精度类型
enum class DataType {
DT_FLOAT = 0,
DT_FLOAT16 = 1,
DT_INT8 = 2,
DT_INT32 = 3,
DT_UINT8 = 4,
DT_BF16 = 16, // Brain Float 16
DT_UNDEFINED = 255
};
// 内存格式枚举,包含标准格式与硬件私有格式
enum class Format {
FORMAT_NCHW = 0,
FORMAT_NHWC = 1,
FORMAT_ND = 2,
FORMAT_NC1HWC0 = 3, // 硬件特定的分形格式
FORMAT_FRACTAL_Z = 4
};
// 张量描述符,描述 Tensor 的元数据而非具体数据
// 这是 IR 中传递的主要对象
class GeTensorDesc {
public:
GeTensorDesc();
~GeTensorDesc();
// 形状操作
void SetShape(const std::vector<int64_t>& shape);
const std::vector<int64_t>& GetShape() const;
// 格式与类型设置
void SetFormat(Format format);
Format GetFormat() const;
void SetDataType(DataType dt);
DataType GetDataType() const;
// 原始形状(Origin Shape)
// 在经过格式转换或 Padding 后,Tensor 的物理形状可能改变
// 这里保存用户视角的原始逻辑形状
void SetOriginShape(const std::vector<int64_t>& originShape);
private:
struct Impl; // Pimpl 惯用法,隐藏底层实现细节
std::shared_ptr<Impl> impl_;
};
// 属性值变体,用于存储 int, float, string, list 等多种类型的算子属性
class AnyValue {
public:
// 类似于 std::variant 或 boost::any 的实现
// 提供强类型的 Set/Get 接口
template <typename T>
void SetValue(const T& value);
template <typename T>
T GetValue() const;
};
// 算子原型描述符
// 这是图编译阶段的核心数据结构
class OpDesc {
public:
explicit OpDesc(const std::string& name, const std::string& type);
// 输入输出管理
// 一个算子可以有多个输入和输出,通过索引或名字访问
void AddInputDesc(const GeTensorDesc& inputDesc);
void AddOutputDesc(const GeTensorDesc& outputDesc);
const GeTensorDesc& GetInputDesc(uint32_t index) const;
// 属性管理
// 存储如 stride, padding, dilation 等静态参数
void SetAttr(const std::string& name, const AnyValue& value);
bool GetAttr(const std::string& name, AnyValue& value) const;
// 引擎亲和性
// 标记该算子应该在哪个引擎上执行 (e.g., AI_CORE, AI_CPU, HOST_CPU)
void SetOpEngineName(const std::string& engineName);
private:
std::string name_; // 节点名称,如 "conv1"
std::string type_; // 算子类型,如 "Conv2D"
std::vector<GeTensorDesc> inputs_;
std::vector<GeTensorDesc> outputs_;
std::map<std::string, AnyValue> attrs_;
};
} // namespace ge
更多推荐
所有评论(0)