Mini-Infer (27): 运行时架构重构 (下) — ExecutionContext 与零拷贝执行
Mini-Infer (27): 运行时架构重构 (下) — ExecutionContext 与零拷贝执行
1. ExecutionContext 的职责定位
在上一篇中,我们介绍了 InferencePlan 作为不可变的构建产物。本篇我们来看它的"运行时伙伴"——ExecutionContext。
ExecutionContext 是每次推理请求的可变状态容器,它负责:
内存池管理:持有实际的内存缓冲区。
中间张量存储:存储每个节点的输出激活值。
设备上下文:管理 CPU/CUDA 执行环境。
动态形状推理:在输入形状变化时重新推导。
核心设计原则:
InferencePlan 是共享的,多个 Context 可以引用同一个 Plan。
ExecutionContext 是独占的,每个推理请求一个 Context。
这种分离使得并发推理成为可能。
2. 初始化流程 (initialize)
A. 类定义
123456789101112131415161718192021222324// mini_infer/runtime/execution_context.h ...
Mini-Infer (26): 运行时架构重构 (上) — InferencePlan 与 Build-Time 优化
Mini-Infer (26): 运行时架构重构 (上) — InferencePlan 与 Build-Time 优化
1. 为什么需要分离 Build-Time 和 Run-Time?
在之前的架构中,Engine 类承担了太多职责:图的构建、优化、内存规划、执行上下文管理、推理执行……这种"大一统"的设计在简单场景下工作良好,但随着功能的增加,问题逐渐暴露:
线程安全问题:多个推理请求共享同一个 Engine 实例时,状态管理变得复杂。
资源浪费:每次推理都需要重新准备某些"不变"的数据结构。
扩展困难:想要支持多 Context 并发推理时,现有架构难以适应。
TensorRT 的解决方案是将推理过程分为两个阶段:
Build-Time (构建期):解析模型、优化图、规划内存、预加载权重。产物是一个不可变的 ICudaEngine。
Run-Time (运行期):基于 Engine 创建 IExecutionContext,每个 Context 持有自己的中间张量和状态。
这种分离带来的好处是:
Engine 可以被多个 Context 共享,节省内存。
Cont ...
Mini-Infer (25): 动态形状的基石 — `OptimizationProfile` 设计与实现
Mini-Infer (25): 动态形状的基石 — OptimizationProfile 设计与实现
1. 核心理念:Min, Opt, Max 三位一体
对于任何一个动态输入(比如 input_0),我们不再只给它一个模糊的 -1,而是要求用户提供三个形状:
Min Shape: 允许的最小尺寸。
作用:边界检查。小于此尺寸的输入将被拒绝。
Opt Shape (Optimal): 最常用的尺寸。
作用:核心优化的依据。引擎会针对这个尺寸选择最优的算法(Kernel Selection)和执行调度策略。
Max Shape: 允许的最大尺寸。
作用:内存分配的依据。MemoryPlanner 会根据 Max Shape 来计算网络中所有中间 Tensor 的最大可能尺寸,并据此分配显存。这样,在运行期间只要输入不超过 Max,就永远不需要重新分配显存。
2. 代码实现:ShapeRange 与 OptimizationProfile
A. 形状范围 (ShapeRange)
这是 Profile 的基本单元。它封装了三个形状,并提供了校验逻辑。
1 ...
Mini-Infer (24): 动态形状支持 — 运行时形状推理引擎
Mini-Infer (24): 动态形状支持 — 运行时形状推理引擎
1. 为什么需要运行时推理?
在传统的静态图中,形状推导(Shape Inference)通常只在模型加载时做一次。但在动态场景下,形状推导必须变成一个运行时 (Runtime) 行为。
想象一个简单的网络:Input -> Conv -> ReLU -> Output。
如果 Input 变了,Conv 的输出形状 Hout=(Hin+2P−K)/S+1H_{out} = (H_{in} + 2P - K)/S + 1Hout=(Hin+2P−K)/S+1 也必须跟着变。
我们不能每次都重新构建整个 Graph,那样太慢了。我们需要一个轻量级的引擎,快速遍历一遍图,只更新 Shape,不碰 Data。
2. 核心架构:拓扑顺序传播
ShapeInferenceEngine 的工作原理就像推多米诺骨牌:
设置起点:用户提供新的输入形状(例如 Input: [1, 3, 512, 512])。
拓扑遍历:按照依赖顺序访问每个节点。
收集输入:对于节点 NNN,收集它所有输入张量的当前形 ...
Mini-Infer (23): 内存优化的黑魔法 — 静态内存规划与贪心着色
Mini-Infer (23): 内存优化的黑魔法 — 静态内存规划
1. 核心架构:内存规划的四个步骤
我们的内存规划器 (MemoryPlanner) 并不是在运行时动态申请释放内存,而是在推理之前(编译期) 对图进行静态分析,计算出一份最优的内存分配方案。
这个过程分为四个严谨的步骤:
生命周期分析 (Liveness Analysis):确定每个 Tensor 何时生、何时死。
构建冲突图 (Interference Graph):确定哪些 Tensor 绝对不能共用内存。
贪心着色 (Greedy Coloring):分配内存池 ID,让不冲突的 Tensor 复用同一个 ID。
生成方案 (Plan Generation):输出最终的内存池配置。
2. 第一步:生命周期分析 (LivenessAnalyzer)
要复用内存,首先要知道 Tensor 的有效期。
出生 (Birth): 生产该 Tensor 的算子开始执行的时间点。
死亡 (Death): 最后一个消费该 Tensor 的算子执行结束的时间点。
我们定义了 TensorLifetime 结构体 ...
Mini-Infer (22): 架构重构 — 链接器的魔法与“副作用”驱动的自动注册
Mini-Infer (22): 架构重构 — 链接器的魔法与“副作用”驱动的自动注册
1. 核心思想:从“拉 (Pull)”到“推 (Push)”
在此之前的架构中,我们采用的是 “显式触发” 模式:
代码逻辑:Main 函数 -> 调用 Init() -> Init 调用 RegisterConv(), RegisterRelu()…
缺点:耦合度高。每增加一个算子,都要修改 Init 函数。
现在,我们转向 “副作用驱动 (Side-Effect Driven)” 模式:
代码逻辑:Conv.cpp 定义一个静态全局变量 -> 程序启动 -> 加载 Conv.o -> 触发静态变量构造函数 -> 构造函数执行 Register()。
优点:高度解耦。Main 函数根本不需要知道 Conv 的存在,注册行为是加载库文件带来的“副作用”。
2. 隐形的大坑:静态链接的“死亡剔除” (Dead Code Stripping)
如果你直接在 .cpp 里写一个静态变量,然后把这个文件编译进静态库 (.a 或 .lib),你会发现:注册代码 ...
Mini-Infer (21): 图优化实战 — TensorRT 风格的 `FusionPass` 与延迟删除
Mini-Infer (21): 图优化实战 — TensorRT 风格的 FusionPass 与延迟删除
1. 设计哲学:为什么是“TensorRT 风格”?
在实现算子融合时,通常有两种流派:
替换流 (Replacement):发现 Conv + ReLU,删掉两个节点,创建一个新的 ConvReLU 算子节点。
缺点:会导致算子数量爆炸(ConvReLU, ConvSigmoid, ConvTanh…)。
属性流 (Attribute/TensorRT-style):发现 Conv + ReLU,保留 Conv 节点,只是给它设置一个 activation 属性,然后删掉 ReLU 节点。
优点:保持了算子库的简洁。Conv2D 算子内部根据 activation 属性决定是否在输出前执行激活逻辑。
Mini-Infer 坚定地选择了 TensorRT 风格。我们的 FusionPass 不会创建新节点,而是对现有节点进行“微创手术”。
2. 核心逻辑:图的“外科手术”与安全性
在对图进行修改(特别是删除节点)时,最容易犯的错误就是迭代器失效 (Iter ...
Mini-Infer (20): 优化器的骨架 — `Pass Manager` 架构设计
Mini-Infer (20): 优化器的骨架 — Pass Manager 架构设计
1. 设计哲学:像编译器一样思考
在编译器领域(如 LLVM),优化通常被组织成一系列的 Pass (遍)。每一个 Pass 只做一件特定的事情(比如“死代码消除”),然后优化器(Pass Manager)按顺序执行这些 Pass。
Mini-Infer 借鉴了这种设计,同时也参考了 TensorRT 的 IOptimizationProfile 概念。
我们需要两个核心组件:
OptimizationPass: 定义“怎么优化”的接口(策略模式)。
GraphOptimizer: 定义“何时优化”的管理器(执行管线)。
2. 定义契约:OptimizationPass
首先,我们定义所有优化算法必须遵守的基类。
1234567891011121314151617181920// mini_infer/graph/graph_optimizer.hclass OptimizationPass {public: explicit OptimizationPass(const std:: ...
Mini-Infer (19): 内置算子导入实战 — Conv, ReLU, Pooling 与 Flatten
Mini-Infer (19): 内置算子导入实战 — Conv, ReLU, Pooling 与 Flatten
1. 核心逻辑:OperatorImporter 的标准范式
在实现具体算子之前,我们总结出一套标准的导入流程(范式):
解析属性 (Parse Attributes):使用 AttributeHelper 获取 strides, pads 等参数。
获取输入 (Get Inputs):从 ImporterContext 中查找输入 Tensor。如果是权重(Initializer),需要特殊处理。
创建算子 (Create Operator):构建 Mini-Infer 的 Operator 对象(如 Conv2D)。
构建节点 (Build Node):创建 Graph::Node,并将 Operator 绑定上去。
连接边 (Connect Edges):将输入 Tensor 连接到当前 Node。
注册输出 (Register Output):创建输出 Tensor 并注册到 Context,供后续节点使用。
所有算子的导入器都遵循这个“六步走”战略。
...
Mini-Infer (18): 编排导入流程 — `ModelImporter` 与 `AttributeHelper`
Mini-Infer (18): 编排导入流程 — ModelImporter 与 AttributeHelper
引言:从架构到实现
在之前的博客中,我们设计了 OnnxParser 的顶层入口,定义了 ImporterContext 和 OperatorRegistry 的接口,并实现了 WeightImporter 来解析数据。
现在,我们需要将这些组件真正“运转”起来。
本篇,我们将实现两个核心组件:
AttributeHelper:一个极其使用的工具类,用于解决 ONNX Protobuf 属性访问繁琐的问题。
ModelImporter:整个导入过程的“总指挥”。它负责按照正确的顺序(权重 -> 输入 -> 节点 -> 输出)编排导入流程,并将解析任务分发给注册表。
1. AttributeHelper:优雅地解析属性
ONNX 的 NodeProto 使用 Key-Value 的列表来存储算子属性(如卷积的 strides, pads)。使用原生的 Protobuf API 来查找和读取这些属性非常啰嗦且容易出错。
我们需要一个包装器来简化这 ...
Mini-Infer (17): 深入字节流 — `WeightImporter` 与权重加载
Mini-Infer (17): 深入字节流 — WeightImporter 与权重加载
1. ONNX 的数据存储格式
ONNX 在存储权重时有两种模式:
Raw Data (二进制流):这是最常用、最高效的模式。所有数据被打包成一个字节流 (std::string) 存储在 raw_data 字段中。这需要我们进行 memcpy。
Typed Data (类型化数组):对于较小的张量,ONNX 可能会直接使用 Protobuf 的重复字段(如 float_data, int32_data)。这需要我们遍历并逐个赋值。
WeightImporter 必须能无缝处理这两种情况。
2. WeightImporter 核心逻辑
A. 数据类型转换
首先,我们需要将 ONNX 的数据类型(onnx::TensorProto::FLOAT)映射到 Mini-Infer 的类型(core::DataType::FLOAT32)。
123456core::DataType WeightImporter::convert_data_type(int onnx_dtype, std::s ...
Mini-Infer (16): 模型导入的核心 — `ImporterContext` 与 `OperatorRegistry`
Mini-Infer (16): 模型导入的核心 — ImporterContext 与 OperatorRegistry
引言:从 Protobuf 到 Graph
在上一篇中,我们实现了 OnnxParser,它能将 .onnx 文件反序列化为 Protobuf 对象。
但这仅仅是第一步。onnx::ModelProto 是一棵复杂的语法树,充满了 Node、Initializer 和 ValueInfo。我们需要一个强大的机制将这些“死数据”转化为 Mini-Infer 中“活的” Graph 对象。
本篇,我们将构建模型导入的两个核心组件:
ImporterContext: 一个“共享黑板”,用于在导入过程中追踪所有的 Tensor 和权重,解决 ONNX 基于名字的连接问题。
OperatorRegistry: 一个“算子工厂”,负责根据 ONNX 的 op_type(如 “Conv”)找到对应的导入逻辑。
1. ImporterContext: 连接一切的桥梁
ONNX 的图结构是基于名字 (String) 的,而 Mini-Infer 的图结构是基于指针 (P ...
Mini-Infer (15): `OnnxParser` 架构设计
Mini-Infer (15): OnnxParser 架构设计
1. 为什么是 ONNX?
ONNX (Open Neural Network Exchange) 是目前 AI 行业的“通用语”。PyTorch, TensorFlow, Keras 等所有主流训练框架都能导出为 ONNX。
ONNX 文件的本质是一个 Protocol Buffers (Protobuf) 序列化对象。
文件结构:ModelProto -> GraphProto -> NodeProto (算子) / TensorProto (权重)。
我们的任务就是编写一个“翻译器”,将 ONNX 的这些 Proto 对象,翻译成 Mini-Infer 的 Graph 和 Node 对象。
2. 架构蓝图:模仿 TensorRT
TensorRT 的 ONNX Parser 架构非常优秀,我们将借鉴它的设计思想:注册机制 (Registration) 与 导入器 (Importer)。
我们不希望写一个巨大的 switch-case 来处理所有 ONNX 算子。我们希望每一个 ONNX 算子 ...
Mini-Infer (14): 迈向 ONNX — `Flatten` 算子与零拷贝视图
Mini-Infer (14): 迈向 ONNX — Flatten 算子与零拷贝视图
1. 为什么需要 Flatten?
在 CNN 网络(如 LeNet-5, VGG)中,数据流通常是这样的: Conv/Pool (4D Tensor) -> Flatten -> Linear (2D Matrix)
Linear 层(全连接层)通常期望输入是一个二维矩阵 [Batch, Features]。而卷积层的输出是四维张量 [Batch, Channel, Height, Width]。
Flatten 的作用就是把 [N, C, H, W] “拍扁” 成 [N, C*H*W]。
2. 算子定义:对齐 ONNX 标准
ONNX 对 Flatten 的定义非常灵活:它有一个 axis 参数。
输入:张量 T
参数:axis (默认为 1)
输出:一个 2D 张量。
维度 0:输入张量从维度 0 到 axis-1 的乘积。
维度 1:输入张量从维度 axis 到最后的乘积。
举例:输入 [2, 3, 4, 5],axis=1。
输出维度 0:2 (只有维度 0) ...
Mini-Infer (13): 端到端验证 — LeNet-5 实战与 PyTorch 对齐
Mini-Infer (13): 端到端验证 — LeNet-5 实战与 PyTorch 对齐
1. 为什么需要端到端测试?
单元测试(Unit Test)只能保证单个算子(如 Conv2D)在特定输入下是正确的。但当几十个算子串联成一个网络时,微小的误差(如 Padding 处理、NCHW vs NHWC 布局差异、float 精度累积)可能会被放大,导致最终分类错误。
端到端测试的目标:
权重加载:验证我们能否正确读取 PyTorch 导出的二进制权重。
计算精度:验证 Mini-Infer 的输出 logits 与 PyTorch 的差异是否在允许范围内(如 1e-5)。
流程打通:验证从图片预处理到最终分类的整个链路。
2. 训练与导出:PyTorch 侧准备 (lenet5_model.py & train_lenet5.py)
首先,我们需要一个“标准答案”。我们在 PyTorch 中定义并训练一个经典的 LeNet-5。
关键细节:
模型定义:我们严格遵循 Conv -> ReLU -> MaxPool 的顺序,这与我们在 Mini-I ...
Mini-Infer (12): 特征提取的收缩 — `Pooling` 算子与架构复用
Mini-Infer (12): 特征提取的收缩 — Pooling 算子与架构复用
引言:不仅仅是卷积
在卷积神经网络(CNN)中,如果说 Conv2D 是“提取特征”的画家,那么 Pooling(池化)就是“提炼精华”的编辑。
没有池化层,特征图(Feature Map)的尺寸会一直保持不变(或仅缓慢减小),这将导致计算量爆炸,且网络难以学习到具有“平移不变性”的高层特征。
1. 定义池化:PoolingParam 与 TensorRT 对齐
1234567struct PoolingParam : public OpParam { PoolingType type; // MAX or AVERAGE int kernel_h, kernel_w; int stride_h, stride_w; int padding_h, padding_w; // ...};
这里有两个值得注意的设计选择:
支持非对称参数:kernel_h vs kernel_w,padding_h vs padding_w。很多简单的框架只支持正方形核, ...




