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 (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。很多简单的框架只支持正方形核, ...
Mini-Infer (11): 下采样利器 — `Pooling` 算子与架构复用之美
Mini-Infer (11): 下采样利器 — Pooling 算子与架构复用之美
1. 架构红利:零成本的扩展性
回想我们在 Blog 7.6 中付出的努力——我们重构了内核注册表,引入了模板元编程。现在,回报来了。
我们要添加 MaxPool 和 AvgPool,不需要重写任何注册逻辑。只需要几行宏定义:
123456789101112// mini_infer/kernels/pooling.h// 1. 定义函数签名template<typename T>using MaxPool2DFunc = void (*)(...);template<typename T>using AvgPool2DFunc = void (*)(...);// 2. 【核心】一键生成注册表!DEFINE_REGISTRY_ALIAS(MaxPool2DRegistry, MaxPool2DFunc);DEFINE_REGISTRY_ALIAS(AvgPool2DRegistry, AvgPool2DFunc);
这就完成了!我们瞬间拥有了两个支持自动后端分发(CPU/ ...
Mini-Infer (10): 卷积的终极形态 - `Conv2D` 实现与 `BiasKernel` 集成
Mini-Infer (10): 卷积的终极形态 - Conv2D 实现与 BiasKernel 集成
1. 最后一块拼图:BiasKernel
在卷积操作 Output = Conv(Input, Weight) + Bias 中,加上偏置(Bias)是最后一步。虽然它计算量不大,但对于内存带宽要求很高。
为了保持架构的一致性,我们将 Bias 加法也封装为一个可调度、可优化的 Kernel。
bias.h: 接口定义
我们继续沿用 TensorRT 风格的注册表模式:
1234567891011121314151617181920212223242526272829303132333435363738// mini_infer/kernels/bias.hnamespace mini_infer {namespace kernels {// 定义函数签名:output += biastemplate<typename T>using BiasFunc = void(*)(T* output, const T* bias, int batch, int channe ...
Mini-Infer (9): 打造高性能算子的基石 — RAII `Buffer` 与 `noexcept` 极致优化
Mini-Infer (9): 打造高性能算子的基石 — RAII Buffer 与 noexcept 极致优化
1. 技术深潜:noexcept 的奥义
在构建高性能 C++ 库时,我们经常看到 noexcept 这个关键字。它不仅仅是一个装饰,它是性能优化的关键开关。
noexcept 是 C++11 引入的一个关键字,它的作用非常明确:告诉编译器(和读代码的人),这个函数保证不会抛出任何异常。
1.1 核心作用:向编译器“承诺”不抛异常
当你把函数声明为 noexcept 时:
123void myFunc() noexcept { // ...}
你是在立下一个“军令状”:“我保证这里面代码无论发生什么,都不会让异常飞出这个函数体。”
如果违背了誓言会怎样? 如果一个被标记为 noexcept 的函数真的抛出了异常,C++ 运行时不会尝试去捕获它,也不会进行“栈展开”(Stack Unwinding,即不会去析构局部对象)。程序会立即调用 std::terminate(),直接粗暴地崩溃(Crash)。
这意味着:noexcept 里的异常是无法被外部的 try-c ...
Mini-Infer (8): Im2Col算法完全讲解
Mini-Infer (8): Im2Col算法完全讲解
🎯 核心概念:为什么需要Im2Col?
问题:卷积计算很慢
朴素卷积需要7层嵌套循环:
123456789// 超级慢!缓存不友好for (batch) for (out_channel) for (in_channel) for (kernel_h) for (kernel_w) for (out_h) for (out_w) output += input * weight
解决方案:转换为矩阵乘法
Im2Col的魔法:
12345678卷积运算 = 矩阵乘法Output = Conv(Input, Weight) ↓ 转换Output = Weight × col_buffer然后用高度优化的GEMM库(如MKL)计算→ 速度提升5-10倍!
📊 具体例子:一步步理解
输入参数
1234567891011121314151617181920212223242526272829输入图像(灰度图): ...
Mini-Infer (7.6): 架构重构 - 用“模板元编程”消除内核注册的“样板戏”
Mini-Infer (7.6): 架构重构 - 用“模板元编程”消除内核注册的“样板戏”
1. 问题的本质:一个“模板”的“模板”
我们的问题是:KernelRegistry (注册表) 的类型,依赖于函数指针的类型,而函数指针的类型又依赖于数据类型 (float, int)。
GEMM_NT for float -> void(*)(const float*, ...)
GEMM_NT for int32 -> void(*)(const int32_t*, ...)
这是一个清晰的模板模式。我们可以把 GEMM_NT 的函数签名定义为一个“函数类型模板”:
12template<typename T>using GEMMFunc_NT = void(*)(const T* A, const T* B, T* C, int M, int N, int K);
现在,我们的问题演变为:如何创建一个通用的 KernelRegistry,它接受 GEMMFunc_NT 这样的**“模板”**作为参数,然后再由用户指定 T(如 float)?
2. 解决方 ...
Mini-Infer (7.5): 架构的“魔鬼细节” - 深入辩论“内核注册”
Mini-Infer (7.5): 架构的“魔鬼细节” - 深入辩论“内核注册”
在 Blog 7 中,我们设计了一个“自注册内核注册表”。这个设计看起来很“酷”,但也引入了大量复杂性:AutoRegister 宏、KernelRegistryInitializer…
本文讨论以下问题:
register_kernel 里的 std::sort 每注册一次就排一次,不会有性能压力吗?
KernelRegistryInitializer::initialize() 为什么需要被“显式”调用?
(最尖锐的)KernelRegistryInitializer 每次添加新内核都要修改,这难道不违反“开闭原则” (OCP) 吗?
(终极问题)既然静态库链接这么麻烦,为什么不直接用动态库 (.so/.dll) ??
本篇,我们将直面这些问题。
1. 终极问题:高性能推理框架的“链接之战” (静 vs. 动)
这是一个关乎 Mini-Infer 核心定位的战略问题。
动态库 (.so/.dll) 是“灵活性”的王者。
静态库 (.a/.lib) 是“性能”的王者。
“自动注册‘魔法’”是真 ...
Mini-Infer (7): 高性能“内核注册表” (A TensorRT-Style Kernel Registry)
Mini-Infer (7): 高性能“内核注册表” (A TensorRT-Style Kernel Registry)
1. 架构目标:从“静态分发”到“动态注册”
我们的新目标是:
解耦:GEMMKernel(调度器)不应该“知道”任何具体的实现(如 avx2_gemm_impl)。
可扩展:添加一个新的 AVX512 内核,应该不需要修改任何现有的 GEMMKernel 代码。
高性能:系统必须能自动检测硬件能力,并优先选择最快的可用内核(例如,cuBLAS > AVX2 > CPU)。
为了实现这一点,我们将构建一个“内核电话簿”(Registry),每个内核实现(AVX2、CUDA…)都会在启动时自动将其“电话号码”(函数指针)和“能力”注册到这个“电话簿”中。
2. 核心设计:KernelRegistryBase (kernel_registry.h)
这是我们的“电话簿”模板。它是一个通用的 C++ 模板类,可以为任何类型的内核(GEMM, im2col…)管理一个实现列表。
12345678910111213141516171819202122 ...
