Mini-Infer (24): 动态形状支持 — 运行时形状推理引擎
Mini-Infer (24): 动态形状支持 — 运行时形状推理引擎
1. 为什么需要运行时推理?
在传统的静态图中,形状推导(Shape Inference)通常只在模型加载时做一次。但在动态场景下,形状推导必须变成一个运行时 (Runtime) 行为。
想象一个简单的网络:Input -> Conv -> ReLU -> Output。
如果 Input 变了,Conv 的输出形状 也必须跟着变。
我们不能每次都重新构建整个 Graph,那样太慢了。我们需要一个轻量级的引擎,快速遍历一遍图,只更新 Shape,不碰 Data。
2. 核心架构:拓扑顺序传播
ShapeInferenceEngine 的工作原理就像推多米诺骨牌:
- 设置起点:用户提供新的输入形状(例如
Input: [1, 3, 512, 512])。 - 拓扑遍历:按照依赖顺序访问每个节点。
- 收集输入:对于节点 ,收集它所有输入张量的当前形状。
- 动态输入:来自上游节点的推导结果。
- 静态输入:来自权重(Weights/Bias),形状永远不变。
- 执行推导:调用算子的
op->infer_shape()方法,计算输出形状。 - 更新状态:将输出形状存入缓存,供下游节点使用。
3. 代码实现剖析
A. 引擎入口与缓存机制
为了追求极致性能,我们引入了缓存机制。如果输入形状没变,直接返回成功,零开销。
1 | // mini_infer/runtime/shape_inference_engine.cpp |
B. 核心循环:处理混合输入
这是实现中最棘手的部分。一个算子(如 Conv2D)的输入来源通常是混合的:
- Input 0 (Data): 来自上一个算子(动态,形状未知)。
- Input 1 (Weight): 来自模型权重(静态,形状已知)。
我们需要把它们拼凑成一个完整的 input_shapes_vec 传给 op->infer_shape。
1 | // 遍历所有节点 |
C. 变更检测与重分配
推导完成后,我们需要告诉内存管理器哪些 Tensor 变大了,需要重新 malloc。
1 | std::vector<std::string> ShapeInferenceEngine::get_tensors_needing_reallocation() const { |
4. 与 MemoryPlanner 的协同
动态形状支持实际上是两种策略的结合:
- 首次运行 / 形状未变:使用
MemoryPlanner计算出的静态复用方案,零碎片,低占用。 - 形状改变:触发
ShapeInferenceEngine,识别出尺寸变化的 Tensor,对其进行单独的reallocate(此时可能会暂时打破最佳的内存复用,但保证了程序的正确运行)。
在 TensorRT 中,这对应于 IExecutionContext::setInputShape 接口。当调用它时,引擎内部就会执行上述的传播逻辑。
5. 总结
我们拥有了:
- ONNX Parser: 自动化模型加载。
- Graph Optimizer: 算子融合优化。
- Memory Planner: 静态内存复用。
- Shape Inference: 动态形状支持。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 James的成长之路!
评论





