我做了一个迷你版 ONNX Runtime,终于把推理引擎的主线看懂了
这篇文章聚焦一个问题:一个 ONNX 文件是如何被 runtime 变成可执行结果的。文章以
yolov8n.onnx和当前 miniONNXRuntime 为主线,结合完整 ONNX Runtime 的基础概念,说明模型加载、图结构、值流转和算子执行分别在做什么。
项目地址:https://github.com/WWandP/miniONNXRuntime
1. 文章引入
ONNX 文件本身只描述了结构和参数,真正把它变成推理结果的是 runtime。
yolov8n.onnx 是一个适合观察推理流程的模型,参数量较小,同时包含图结构、权重参数以及推理所需的基本算子,适合用于演示 ONNX Runtime 的运行流程。
当模型输入一张图片后,完整的runtime 需要完成模型加载、图结构解析、算子调度、张量流转和结果输出。这个过程涉及的对象包括 Graph、Session、ExecutionContext、KernelRegistry 以及若干具体 kernel。
下图给出一个简化后的主流程。
这张图对应一个最小可运行的推理闭环。Session 负责组织一次推理,Graph 负责表达模型结构,ExecutionContext 负责保存运行时张量,KernelRegistry 负责把节点映射到具体实现。
这一闭环已经覆盖了模型从加载到输出的主流程,也足以说明推理引擎里最核心的几件事:模型如何被解析,值如何在图中流转,算子如何被调度,以及结果如何被回写。
本文以这个 mini runtime 为起点,结合完整 ONNX Runtime 的基础概念,说明一个模型从 ONNX 文件变成推理结果时经历了哪些关键阶段。
2. ONNX 与 ONNX Runtime 的分工
ONNX 和 ONNX Runtime 在日常表述里常被放在一起,但它们承担的角色不同。
ONNX 负责描述模型本身。一个 ONNX 文件中保存了图、节点、initializer、输入输出信息以及算子属性。它提供的是模型的结构化表达,不直接负责执行过程。
runtime 的职责是将这份静态描述转化为可执行状态,包括:
- 构建内部图结构
- 组织推理过程
- 调度算子执行
- 管理中间张量
- 返回最终结果
从这个角度看,ONNX 和 runtime 分别承担“描述”和“执行”两个层面的工作。
在完整 ONNX Runtime 中,这种分工同样清晰:模型先被解析成内部图对象,再进入执行阶段。mini runtime 当前实现的正是这条链路的前半部分和执行主线。
3. miniONNXRuntime 当前进度
这个 mini 版本的 ONNX Runtime 已经在 CPU 上跑通了最小链路,后续会持续扩展。
已经完成的部分包括:
- 读取
yolov8n.onnx - 构建内部
Graph / Node / Value / TensorInfo - 提取模型元信息、输入输出和 initializer
- 生成拓扑顺序
- 提供
Session作为统一入口 - 提供
ExecutionContext作为运行时值表 - 注册并执行一批 builtin kernels
- 支持
Constant、Add、Mul、Conv、MaxPool、Resize、Shape、Gather等核心算子(CPU) - 支持输入预处理和执行跟踪
后续将展开的部分包括:
- 图优化
- ExecutionProvider 分配
- 内存规划和 buffer reuse
- 多模型泛化
- CUDA 路径
这一阶段的重点是把模型加载、图结构、值流转和算子执行看清楚。这个边界是明确的,也符合当前实现的实际进度。
接下来的章节聚焦已经实现、且最能体现当前设计的部分。后续的介绍以minionnx为例
4. 模型解析
这一节看模型如何从 ONNX 文件进入内部 Graph。解析入口是 LoadOnnxGraph(const std::filesystem::path& model_path),对应文件位于 include/miniort/loader/onnx_loader.h 和 src/loader/onnx_loader.cc。
Graph LoadOnnxGraph(const std::filesystem::path& model_path);
解析过程主要分为以下几步:
- 打开 ONNX 文件并解析为
onnx::ModelProto。 - 读取模型级元信息,包括
graph.name、ir_version、producer_name、producer_version和opset_import。 - 遍历
graph.input、graph.output和graph.value_info,为每个命名值建立TensorInfo。 - 遍历
graph.initializer,将权重和常量张量写入graph.initializers。 - 从
graph.input中剔除 initializer 名称,留下真正的运行时输入。 - 遍历
graph.node,记录节点名、op_type、输入输出边和属性。 - 根据节点依赖关系构建
topological_order。
解析后得到的内部图对象主要包含以下字段:
Graph::metadata- 模型路径、生产者信息、IR 版本、opset 版本。
Graph::value_infos- 每个 value 的 dtype、shape 和是否为 initializer。
Graph::initializers- 常量参数和权重张量。
Graph::inputs/Graph::outputs- 运行时输入和模型输出。
Graph::nodes- 节点名、算子类型、输入、输出和属性。
Graph::topological_order- 执行顺序。
属性解析目前覆盖的类型包括:
floatintstringfloatsintsstringstensor
张量数据则会解析 raw_data、float_data、double_data、int32_data、int64_data 和 string_data。对于当前项目,最常用的是 float32 和 int64。
下面这段代码展示了解析阶段的几个关键点:把 ValueInfoProto 转成 TensorInfo,把 TensorProto 转成 TensorData,再把 TensorProto_DataType 映射为项目内部使用的 dtype 字符串。
std::string ToTensorElemTypeString(int elem_type) {
switch (elem_type) {
case onnx::TensorProto_DataType_FLOAT:
return "float32";
case onnx::TensorProto_DataType_INT64:
return "int64";
default:
return "unknown(" + std::to_string(elem_type) + ")";
}
}
TensorInfo MakeTensorInfo(const onnx::ValueInfoProto& value_info, bool is_initializer = false) {
TensorInfo info;
info.is_initializer = is_initializer;
...
return info;
}
这一节的重点不是解析细节本身,而是确认 mini runtime 如何把 ONNX 的静态描述转换成自己可执行的数据结构。
5. 迷你版 runtime 的结构
这一节看 mini runtime 的模块分工。这个项目已经形成了一个清晰的小闭环,主要由五个部分组成:
loader
-> model
-> runtime
-> kernels
-> tools
loader负责将 ONNX protobuf 转换为内部图结构。model保存Graph / Node / Value / TensorInfo等核心数据。runtime负责一次推理过程的组织与调度。kernels提供最小可用的算子实现。tools提供模型查看、执行跟踪、输入预处理和结果导出能力。
这个结构对应的是一个最小可运行的推理系统,重点在于把模型加载、值流转和算子调度串成闭环。
6. Session 的作用
Session 是 mini runtime 的运行入口,也是整个执行过程的组织者。
Session 是 mini runtime 的运行入口,对应文件位于 include/miniort/runtime/session.h 和 src/runtime/session.cc。
它的核心职责包括:
- 持有
Graph - 持有
KernelRegistry - 在构造时注册 builtin kernels
- 在
Run()中组织一次推理
SessionOptions 目前包含四个开关:
verbose- 控制执行过程的 trace 输出。
allow_missing_kernels- 控制遇到未注册算子时是否允许继续执行。
auto_bind_placeholder_inputs- 控制是否自动为缺失输入绑定占位张量。
max_nodes- 控制最多执行多少个节点,便于局部调试。
Session::Run() 的执行顺序如下:
- 调用
ExecutionContext::LoadInitializers(),把 initializer 装入上下文。 - 将外部传入的
feeds绑定到上下文。 - 视配置自动补齐缺失输入的占位张量。
- 按
topological_order顺序遍历节点。 - 通过
KernelRegistry::Lookup()查找对应 kernel。 - 调用 kernel 计算输出并写回上下文。
- 对缺失 kernel 或异常路径,必要时用元数据生成占位输出。
这个设计和完整 ONNX Runtime 的角色划分一致:Session 负责组织推理,ExecutionContext 负责保存运行时值,KernelRegistry 负责把算子名映射到实现。
Session 的核心结构可以直接从头文件看出来:
class Session {
public:
Session(Graph graph, SessionOptions options = {});
RunSummary Run(const std::unordered_map<std::string, Tensor>& feeds,
ExecutionContext& context,
std::ostream* trace = nullptr) const;
private:
Graph graph_;
KernelRegistry kernel_registry_;
SessionOptions options_;
};
Run() 的主循环也很直接:
for (std::size_t topo_index = 0; topo_index < graph_.topological_order.size(); ++topo_index) {
const auto node_index = graph_.topological_order[topo_index];
const auto& node = graph_.nodes[node_index];
const auto* kernel = kernel_registry_.Lookup(node.op_type);
if (kernel != nullptr) {
(*kernel)(node, context, trace);
}
}
这段代码展示了 mini runtime 的核心执行方式:按拓扑顺序取节点,再通过 KernelRegistry 找到对应实现。
7. 执行主线
mini runtime 的执行主线如下:
Load
-> 构建内部 Graph
-> Session 持有图和 kernel 注册表
-> ExecutionContext 保存运行时张量
-> 按拓扑顺序执行节点
-> 输出结果
这条链路和完整 ONNX Runtime 的基础执行思路一致:先把模型读进来,变成图对象,再进入一次推理的执行过程。当前 mini runtime 聚焦的是这部分最关键的执行逻辑。
8. 算子实现路径
这一节看算子实现放在哪里,以及它们如何被注册到 runtime 中。builtin kernels 的注册入口位于 src/runtime/builtin_kernels.cc,分组声明位于 src/runtime/builtin_kernel_groups.h。
void RegisterBuiltinKernels(KernelRegistry& registry) {
RegisterBasicKernels(registry);
RegisterElementwiseKernels(registry);
RegisterNnKernels(registry);
RegisterShapeKernels(registry);
}
当前实现的算子按功能分为四组:
| 分组 | 文件 | 主要算子 |
|---|---|---|
| 基础算子 | src/runtime/basic_kernels.cc | Constant |
| 元素级算子 | src/runtime/elementwise_kernels.cc | Sigmoid、Add、Mul、Div、Sub、Cast |
| 形状与张量操作 | src/runtime/shape_kernels.cc | ConstantOfShape、Shape、Gather、Unsqueeze、Concat、Reshape、Range、Split、Expand、Transpose、Slice、ReduceMax、ArgMax |
| NN 算子 | src/runtime/nn_kernels.cc | Conv、MaxPool、Resize、Softmax |
这些 kernel 的共同特点是:
- 直接从
ExecutionContext读取输入张量 - 根据节点属性执行最小语义实现
- 构造输出张量并写回上下文
算子实现中还会用到 src/runtime/kernel_utils.h 里的公共辅助函数,例如张量读取、shape 计算、广播和索引转换。
下面看一个具体算子,Constant 的实现位于 src/runtime/basic_kernels.cc:
registry.Register("Constant", [](const Node& node, ExecutionContext& context, std::ostream* trace) {
const auto attr_it = node.attributes.find("value");
if (attr_it == node.attributes.end() || !attr_it->second.tensor.has_value()) {
throw std::runtime_error("Constant node missing tensor value attribute: " + node.name);
}
const auto& tensor_attr = *attr_it->second.tensor;
...
context.BindTensor(std::move(tensor));
});
这类实现的共同模式是:从节点属性或输入张量中取数据,计算输出形状和内容,再写回 ExecutionContext。
9. 迷你版与完整实现的对照
mini runtime 和完整 ONNX Runtime 可以放在同一张表里理解。
| 主题 | miniONNXRuntime | 完整 ONNX Runtime |
|---|---|---|
| 模型范围 | yolov8n.onnx |
通用 ONNX / 多模型 |
| 执行方式 | 拓扑顺序执行 | 受图结构、运行时状态和执行计划共同驱动 |
| 中间值管理 | name -> Tensor |
由运行时状态统一管理 |
| 图结构 | Graph / Node / Value |
Model / Graph / NodeArg |
| 内存管理 | 简化处理 | 结合生命周期分析和运行时分配 |
| 后端 | CPU 单路径 | 多后端支持 |
这个对照有助于建立层次感:mini runtime 负责讲清模型加载、图结构、值流转和算子执行,完整实现则在此基础上扩展出更强的通用性和工程能力。
10. 当前阶段与后续
mini runtime 已经足以说明推理引擎的主线:模型如何进入 runtime,值如何在图中流转,算子如何执行,结果如何返回。
一个 ONNX 模型要真正转化为推理结果,需要 runtime 将图结构、算子执行、内存管理和调度逻辑组织起来。迷你版 runtime 让这条主线变得清晰,完整 ONNX Runtime 则在此基础上完成工程化扩展。
理解主线之后,下一层要处理的就是工程复杂度。两者之间的关系很清楚:前者回答“怎么跑通”,后者回答“怎么跑稳、跑快、跑得通用”。
通过对照这两层实现,可以更直观地理解推理引擎的核心问题,也能更准确地看到“模型描述”与“模型执行”之间的边界。先理解主线,再看工程复杂度,层次会更清楚。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)