第一章:前端解析——模型格式解析与算子识别映射
各位同学好,我是你们的老朋友。今天咱们聊聊模型编译器前端解析这块。
说实话,前端解析是整个编译流程的入口。你想想看,模型文件就像一本外文书,编译器得先读懂它,才能做后续优化。我见过不少团队,后端优化做得花里胡哨,结果前端解析没做好,模型根本跑不起来。嗯,这章咱们就把这块讲透。
1.1 主流模型格式概览
目前业界主流模型格式就三种:ONNX、TensorFlow 的 SavedModel/Pb、PyTorch 的 TorchScript。我个人的习惯是,先看模型来源,再决定解析策略。
| 格式 | 框架 | 特点 | 我踩过的坑 |
|---|---|---|---|
| ONNX | 跨框架 | 中间表示,算子标准 | 版本兼容性问题 |
| SavedModel | TensorFlow | 包含图结构和权重 | 控制流解析复杂 |
| TorchScript | PyTorch | JIT 导出,动态图转静态 | 动态 shape 处理 |
核心观点:不管什么格式,最终都要转成编译器内部的 IR(中间表示)。前端解析的质量,直接决定后续优化的天花板。
1.2 ONNX 格式解析
ONNX 是我个人最推荐的中间格式。为什么?因为它跨框架,而且算子定义规范。
解析 ONNX 模型,说白了就是读 protobuf 文件。ONNX 使用 protobuf 序列化,结构清晰。我建议你直接看它的 proto 定义文件,比看文档管用。
// ONNX 模型的核心结构
message ModelProto {
int64 ir_version = 1; // IR 版本
repeated OperatorSetIdProto opset_import = 2;
string producer_name = 3;
string producer_version = 4;
string domain = 5;
int64 model_version = 6;
string doc_string = 7;
GraphProto graph = 8; // 计算图
}
message GraphProto {
repeated NodeProto node = 1; // 算子节点
repeated TensorProto initializer = 2; // 权重
repeated ValueInfoProto input = 3; // 输入
repeated ValueInfoProto output = 4; // 输出
}
解析流程其实就三步:
- 反序列化:读 protobuf 二进制文件,转成 ModelProto 对象
- 提取图结构:遍历 GraphProto 中的 node 列表,构建计算图
- 处理权重:从 initializer 中提取常量张量
小技巧:解析 ONNX 时,记得检查 ir_version 和 opset_import。不同版本之间算子定义可能有差异。我曾经因为 opset 版本不对,导致 Conv 算子的 padding 参数解析错误,排查了两天。
1.3 TensorFlow 格式解析
TensorFlow 的格式就复杂多了。SavedModel 目录下有一堆文件:saved_model.pb、variables 目录、assets 目录。我个人觉得,TF 的格式设计得有点过重了。
解析 TF 模型,核心是读 GraphDef 协议。但要注意,TF 的图里包含大量控制流节点(如 Switch、Merge),这些在推理时其实用不到。
// TensorFlow GraphDef 核心结构
message GraphDef {
repeated NodeDef node = 1;
FunctionDefLibrary library = 2;
int64 versions = 4;
}
message NodeDef {
string name = 1;
string op = 2; // 算子类型
repeated string input = 3; // 输入张量名
string device = 4;
map<string, AttrValue> attr = 5; // 属性
}
避坑指南:我曾经在解析 TF 模型时,遇到一个坑——TF 的输入输出命名规则很混乱。有些模型用 "input:0" 这种格式,有些直接用 "input"。建议统一做一次命名规范化。
1.4 PyTorch TorchScript 解析
TorchScript 是 PyTorch 的静态图表示。它有两种形式:trace 和 script。trace 是实际跑一遍录下来的,script 是源码编译的。
解析 TorchScript,核心是读它的 Graph 对象。PyTorch 提供了 C++ 接口,可以直接操作 IR。
// TorchScript 图结构示例
graph(%x.1 : Tensor,
%w.1 : Tensor):
%2 : Tensor = aten::conv2d(%x.1, %w.1, %3, %4, %5, %6)
%7 : Tensor = aten::relu(%2)
return (%7)
解析时要注意:TorchScript 的算子名是带命名空间的(如 aten::conv2d)。映射到内部 IR 时,需要做一次名字转换。
1.5 算子识别与映射
算子识别,说白了就是看模型里用了什么算子,然后找到编译器内部对应的实现。
我建议你建一个算子映射表。比如 ONNX 的 Conv 算子,映射到内部 IR 的 Conv2D 算子。但要注意,不同框架的算子语义可能有细微差别。
| 框架算子 | 内部 IR 算子 | 注意事项 |
|---|---|---|
| ONNX Conv | Conv2D | auto_pad 参数处理 |
| TF Conv2D | Conv2D | data_format 转换 |
| Torch aten::conv2d | Conv2D | padding 模式差异 |
关键点:算子映射不是简单的 1:1 对应。有些框架的算子可能是组合算子,需要拆解成多个基础算子。比如 TF 的 FusedBatchNorm,在推理时可以拆成 Mul、Add 等基础算子。
1.6 知识体系总览
下面这张图,是我自己总结的前端解析知识体系。你一看就明白了。
从这张图你能看到,前端解析分三层:格式解析层、算子识别映射层、最终输出内部 IR。每一层都有各自的难点。
1.7 实战经验总结
最后,我分享几个实战经验:
- 版本兼容性:不同框架版本导出的模型,解析方式可能不同。我建议在解析前先做版本检测。
- 动态 shape 处理:ONNX 支持动态 batch,但很多编译器不支持。我一般会在前端做 shape 推断,把动态 shape 转成静态。
- 自定义算子:如果模型里有自定义算子,需要注册对应的解析器。我曾经遇到一个客户,模型里用了自定义的 Pooling 算子,结果解析器没注册,整个编译流程卡住了。
我的建议:写前端解析器时,一定要做好错误处理。模型文件可能损坏、版本不匹配、算子缺失,这些都要有清晰的错误提示。别问我怎么知道的,都是血泪教训。
好了,前端解析这块就讲到这里。记住,前端解析是基础,基础不牢,地动山摇。下一章咱们聊聊图优化,那才是真正展现编译器功力的时候。