Mini-Infer 架构深潜 (3): Operator 抽象与“自注册”工厂
Mini-Infer 架构深潜 (3): Operator 抽象与“自注册”工厂
引言:缺失的“计算”拼图
在前两篇文章中,我们构建了 Mini-Infer 的数据 (Tensor) 和执行上下文 (Backend)。我们已经有了坚实的地基,但大厦至今仍是“空”的——它没有任何“功能”。
我们如何定义一个“卷积”操作?如何定义一个 “ReLU” 激活?最重要的是,我们的 Net (计算图) 如何在不“写死”依赖的情况下,动态地加载和执行这些操作?
本篇,我们将构建 Mini-Infer 的计算核心:Operator 抽象层。我们将使用 C++ 中一个极其精妙的模式——工厂 (Factory) + 自动注册 (Self-Registration)——来实现一个真正可插拔、可扩展的算子系统。
1. Operator 接口:“计算”的合约 (operator.h)
首先,我们必须定义一个标准“合约”,所有计算单元(卷积、激活、池化等)都必须遵守这个合约。这就是抽象基类 Operator 的职责。
1 | // mini_infer/operators/operator.h |
这个接口的设计有三个关键点:
OpParam基类: 这是一个轻量级的struct,用作所有算子参数(如stride,kernel_size)的基类。这允许Convolution定义一个ConvParam结构体,并将其作为std::shared_ptr<OpParam>泛型地存入Operator基类中。forward(...)(执行): 这是算子的核心执行函数。它接收Tensor列表,执行计算,并填充输出Tensor列表。infer_shape(...)(塑形): 这是整个框架的性能核心之一。infer_shape只操作元数据 (Shape),它不执行任何实际计算。- 为什么分离? 这允许
Net在运行forward之前调用所有算子的infer_shape。Net可以预先知道图中每一个中间Tensor的确切形状,从而一次性分配好所有内存。这避免了在forward过程中动态分配内存的巨大开销。
- 为什么分离? 这允许
2. OperatorFactory:解耦的“总管” (operator.h & .cpp)
我们如何根据模型文件中的一个字符串(如 “Convolution”)来创建对应的 C++ Convolution 对象?
我们决不能在 Net 类中写 if (op_type == "Convolution") { ... }。这种“硬编码”会产生一个“巨无霸”类,每添加一个新算子,都必须修改 Net 类。
解决方案是工厂模式。OperatorFactory 是一个全局唯一的总管,它是唯一一个“知道”如何创建所有算子的地方。
1 | // mini_infer/operators/operator.h (接上文) |
OperatorFactory 的实现隐藏在 .cpp 文件中,这非常关键:
1 | // mini_infer/operators/operator.cpp |
现在,Net 类只需要和 OperatorFactory::create_operator("Convolution") 这一个函数打交道,它完全不需要知道 Convolution 类的存在。解耦初步达成!
3. REGISTER_OPERATOR:C++ 宏的“自动注册”魔法
我们还剩最后一个问题:OperatorFactory 如何“知道”所有算子?谁去调用 register_operator?
我们不希望在 main 函数中手动调用 register_operator("Convolution", ...),这同样是“硬编码”。
我们希望算子自己“主动”去工厂注册。这就是“自动注册”模式,通过 C++ 宏和“静态初始化”的特性巧妙实现。
1 | // mini_infer/operators/operator.h (接上文) |
这是整个框架最精妙的部分,我们来拆解它的执行过程:
假设你创建了一个 relu_operator.cpp 文件,并在文件末尾添加了这一行: REGISTER_OPERATOR(ReLU, ReLUOperator)
在 C++ 程序启动时(main 函数执行之前):
- 编译器在
relu_operator.cpp中看到了第4步的static ReLUOperator_register g_ReLUOperator_register;。 - C++ 运行时需要初始化这个全局静态变量,因此它会调用该变量的构造函数(第
3步的ReLUOperator_register())。 - 这个构造函数执行,它调用
OperatorFactory::register_operator("ReLU", create_ReLUOperator)。 OperatorFactory的全局registry(注册表) 中,被添加了一个条目:{"ReLU", [指向 create_ReLUOperator 的函数指针]}。- … 这个过程在所有包含
REGISTER_OPERATOR宏的.cpp文件中都会发生。
最终结果: 当你的 main 函数开始执行时,OperatorFactory 的 registry (注册表) 已经自动被所有算子填满了。Net 类可以立即通过字符串创建任何已注册的算子,而无需知道这些算子类的任何细节。
总结与展望
Mini-Infer 的核心架构已经成型!我们通过三层抽象,实现了彻底的解耦:
Tensor(数据层): 管理内存和形状。Backend(执行层): 抽象硬件操作(memcpy,allocate)。Operator(计算层): 抽象计算逻辑(forward,infer_shape)。
OperatorFactory 和 REGISTER_OPERATOR 宏则像“神经中枢”,将这些松散的组件动态地“编织”在一起。





