Mini-Infer 架构深潜 (5): Engine - 联结万物的“总指挥”
1. Engine 的设计哲学:编译与执行的分离
一个推理引擎的 API 设计,最关键的一点是必须分离“一次性”的准备工作和“高频”的执行工作。
build()(编译): 加载模型、图优化、拓扑排序、内存分配。这些操作非常昂贵,但我们只需要做一次。
forward()(执行): 运行模型。这个操作必须极其轻量,因为它会被调用成千上万次。
Engine 类的接口 (engine.h) 完美地体现了这种分离。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| #pragma once
#include "mini_infer/graph/graph.h" #include "mini_infer/backends/backend.h"
namespace mini_infer { namespace runtime {
struct EngineConfig { core::DeviceType device_type{core::DeviceType::CPU}; int32_t device_id{0}; };
class Engine { public: explicit Engine(const EngineConfig& config); ~Engine() = default;
core::Status build(std::shared_ptr<graph::Graph> graph);
core::Status forward( const std::unordered_map<std::string, std::shared_ptr<core::Tensor>>& inputs, std::unordered_map<std::string, std::shared_ptr<core::Tensor>>& outputs ); private: EngineConfig config_; std::shared_ptr<graph::Graph> graph_; std::shared_ptr<backends::Backend> backend_; std::vector<std::shared_ptr<graph::Node>> sorted_nodes_; core::Status allocate_tensors(); core::Status execute_node(std::shared_ptr<graph::Node> node); };
} }
|
Engine 是一个状态机。它的私有成员 graph_, backend_, 和 sorted_nodes_ 构成了它的核心状态,这些状态在 build() 期间被建立,在 forward() 期间被(只读)使用。
2. “编译”阶段 (Engine::build):一次性完成所有“重”工作
build 函数是我们 Engine 的“准备”阶段。它是一条精心设计的流水线,它调用了我们之前构建的所有 Graph 算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| core::Status Engine::build(std::shared_ptr<graph::Graph> graph) { if (!graph) return core::Status::ERROR_INVALID_ARGUMENT; graph_ = graph; auto status = graph_->validate(); if (status != core::Status::SUCCESS) { MI_LOG_ERROR("Graph validation failed"); return status; } status = graph_->optimize(); status = graph_->topological_sort(sorted_nodes_); if (status != core::Status::SUCCESS) { MI_LOG_ERROR("Topological sort failed (cycle detected?)"); return status; } status = allocate_tensors(); if (status != core::Status::SUCCESS) { MI_LOG_ERROR("Tensor allocation failed"); return status; } MI_LOG_INFO("Engine built successfully"); return core::Status::SUCCESS; }
|
build 函数的意义在于,它承担了所有的算法复杂性。topological_sort (O(V+E)) 是昂贵的,optimize 更是如此。build 函数将这些开销全部“吸收”,从而保证 forward 函数的“轻盈”。
3. “执行”阶段 (Engine::forward):轻量级的“热”路径
forward 函数是 Engine 的“热路径”(Hot Path)。它的设计目标是极致的简单和高效。
它之所以能做到这一点,全靠 build 阶段准备好的 sorted_nodes_(执行计划)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| core::Status Engine::forward( const std::unordered_map<std::string, std::shared_ptr<core::Tensor>>& inputs, std::unordered_map<std::string, std::shared_ptr<core::Tensor>>& outputs) { for (const auto& input_name : graph_->inputs()) { auto it = inputs.find(input_name); if (it == inputs.end()) { } auto node = graph_->get_node(input_name); node->set_output_tensors({it->second}); } for (auto& node : sorted_nodes_) { auto status = execute_node(node); if (status != core::Status::SUCCESS) { MI_LOG_ERROR("Node execution failed: " + node->name()); return status; } } outputs.clear(); for (const auto& output_name : graph_->outputs()) { auto node = graph_->get_node(output_name); if (node && !node->output_tensors().empty()) { outputs[output_name] = node->output_tensors()[0]; } } return core::Status::SUCCESS; }
|
这种**“推送式” (Push-based)** 的顺序执行,与 ncnn 的“拉取式” (Pull-based) 递归执行形成了鲜明对比。这是现代框架(如 PyTorch, ONNXRuntime)的标准做法,因为它没有递归开销,逻辑清晰,并且非常容易地映射到异步执行(如 CUDA Streams)。
4. execute_node:连接“图”与“算子”的桥梁
forward 循环依赖一个辅助函数 execute_node。这个函数是图(Graph)世界和算子(Operator)世界之间的“翻译官”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| core::Status Engine::execute_node(std::shared_ptr<graph::Node> node) { if (!node || !node->get_operator()) { } std::vector<std::shared_ptr<core::Tensor>> input_tensors; for (const auto& input_node : node->inputs()) { const auto& outputs = input_node->output_tensors(); if (!outputs.empty()) { input_tensors.push_back(outputs[0]); } } auto& output_tensors = node->output_tensors(); return node->get_operator()->forward(input_tensors, output_tensors); }
|
execute_node 的逻辑是 Mini-Infer 数据流的核心: 一个节点的 inputs,就是它上游 input_nodes 的 outputs。
这个函数完美地将图的拓扑结构(node->inputs())转换为了 Operator::forward 所需的 std::vector<Tensor>。
总结与展望
Engine 登基,Mini-Infer 的核心架构宣告完成。
我们现在拥有了一个完整的、端到端的推理引擎“骨架”。它能够加载 Graph,通过 Backend 管理硬件,利用 topological_sort 制定执行计划,并通过 forward 循环调用 Operator。
我们所有的架构设计(Blog 1-5)至此已经全部“闭环”。
但这个引擎目前还是“空转”的。allocate_tensors 还是 //TODO,我们甚至连一个 Operator 都还没实现。