Mini-Infer (35): 插件架构实战 — 从旧架构到新架构的迁移
Mini-Infer (35): 插件架构实战 — 从旧架构到新架构的迁移
1. 迁移策略概述
从旧的 Operator + Kernel 架构迁移到新的 Plugin 架构,我们采用以下策略:
A. 保留底层原语
底层计算原语(im2col、gemm、bias、transpose)保持不变,它们是设备无关的数学操作:
1234567保留:├── kernels/cpu/gemm.cpp├── kernels/cpu/im2col.cpp├── kernels/cpu/bias.cpp├── kernels/cuda/gemm.cu├── kernels/cuda/im2col.cu└── kernels/cuda/bias.cu
B. 删除旧 Kernel 实现
旧的算子级 Kernel(如 Conv2DKernel、ReLUKernel)被删除,其逻辑移入 Plugin:
12345删除:├── kernels/cpu/conv2d_kernel.cpp → 移入 Conv2DCPUPlugin├── kernels/cpu/relu_kernel.cpp → 移入 ...
Mini-Infer (34): 插件架构 (下) — PluginRegistry 与自动注册
Mini-Infer (34): 插件架构 (下) — PluginRegistry 与自动注册
1. 问题背景:双层注册的痛点
在旧架构中,我们有两个独立的注册表:
1234// 旧架构OperatorFactory::register_operator("Conv2D", Conv2DOperatorCreator);KernelRegistry::register_kernel("Conv2D", CPU, Conv2DCPUKernel);KernelRegistry::register_kernel("Conv2D", CUDA, Conv2DCUDAKernel);
问题:
维护成本高:添加新算子需要修改两个地方。
一致性问题:Operator 和 Kernel 的注册可能不同步。
查找开销:执行时需要先查 Operator,再查 Kernel。
新架构:统一的 PluginRegistry,一次注册,直接使用。
2. PluginKey 设计
A. 二元组 Key
12345678910// mini_infer/operators/plugin_regis ...
Mini-Infer (33): 插件架构 (中) — CRTP 基类与静态多态
Mini-Infer (33): 插件架构 (中) — CRTP 基类与静态多态
1. CRTP 模式回顾
CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)是 C++ 中一种强大的设计模式,用于实现静态多态。
A. 基本形式
1234567891011121314template <typename Derived>class Base {public: void interface() { static_cast<Derived*>(this)->implementation(); }};class Derived : public Base<Derived> {public: void implementation() { // 具体实现 }};
B. 静态多态 vs 动态多态
特性
动态多态 (virtual)
静态多态 (CRTP)
绑定时机
运行时
编译时
虚函数表
需要
不需要
性能开销
有(间接调 ...
Mini-Infer (32): 插件架构 (上) — IPlugin 接口设计
Mini-Infer (32): 插件架构 (上) — IPlugin 接口设计
1. 问题背景:为什么需要插件架构?
在之前的 Mini-Infer 实现中,我们使用了 Operator + Kernel 双层抽象:
Operator:定义算子的元数据(输入/输出数量、形状推理)。
Kernel:实现具体的计算逻辑(CPU/CUDA)。
这种设计在早期工作良好,但随着功能增加,问题逐渐暴露:
A. 双层抽象的问题
12345Operator (元数据) ↓KernelRegistry (查找) ↓Kernel (计算)
维护成本高:添加新算子需要修改两个地方。
一致性问题:Operator 和 Kernel 的参数可能不同步。
查找开销:每次执行都需要从 Registry 查找 Kernel。
B. 多后端支持的复杂性
123// 旧架构:需要为每个后端注册 KernelREGISTER_KERNEL(Conv2D, CPU, Conv2DCPUKernel);REGISTER_KERNEL(Conv2D, CUDA, Conv2DCUDAKernel); ...
Mini-Infer (31): CUDA 后端支持 (下) — TensorRT 风格权重预加载
Mini-Infer (31): CUDA 后端支持 (下) — TensorRT 风格权重预加载
1. 问题背景:权重拷贝的性能瓶颈
在 GPU 推理中,一个常见的性能问题是权重拷贝开销。
A. PCIe 带宽限制
CPU 和 GPU 之间通过 PCIe 总线通信。即使是 PCIe 4.0 x16,理论带宽也只有约 32 GB/s,远低于 GPU 显存带宽(如 A100 的 2 TB/s)。
123CPU Memory ──PCIe──► GPU Memory ↑ 瓶颈所在
B. 首次推理延迟
如果每次推理都从 CPU 拷贝权重到 GPU:
123推理请求 → 拷贝权重 → 执行计算 → 返回结果 ↑ 额外延迟
对于一个 100MB 的模型,PCIe 拷贝可能需要 3-5ms,而实际计算可能只需要 1ms。
C. TensorRT 的解决方案
TensorRT 在 Build-Time 将权重加载到 GPU,Run-Time 直接使用:
12Build-Time: 解析模型 → 优化图 → ...
Mini-Infer (30): CUDA 后端支持 (中) — CUDADeviceContext 与异构执行环境
Mini-Infer (30): CUDA 后端支持 (中) — CUDADeviceContext 与异构执行环境
1. DeviceContext 抽象回顾
在 Mini-Infer 的架构中,DeviceContext 是执行环境的抽象基类:
12345678910111213141516// mini_infer/backends/device_context.hclass DeviceContext {public: virtual ~DeviceContext() = default; /** * @brief 获取设备类型 */ virtual core::DeviceType device_type() const = 0; /** * @brief 同步设备(等待所有操作完成) */ virtual void synchronize() = 0;};
对于 CPU,CPUDeviceContext 的实现非常简单(几乎是空的)。但对于 CUDA,我们需要管理更多资源。
2. CUDADeviceCo ...
Mini-Infer (29): CUDA 后端支持 (上) — CUDAAllocator 与显存管理
Mini-Infer (29): CUDA 后端支持 (上) — CUDAAllocator 与显存管理
1. 问题背景:CPU 与 GPU 内存管理的差异
在之前的实现中,Mini-Infer 只支持 CPU 推理。现在我们要添加 CUDA 后端支持,首先需要解决的就是 GPU 显存管理。
CPU 和 GPU 内存管理有几个关键差异:
特性
CPU 内存
GPU 显存
分配函数
malloc / aligned_alloc
cudaMalloc
释放函数
free
cudaFree
分配开销
较低
较高(需要驱动调用)
默认对齐
通常 16 字节
256 字节
访问方式
直接访问
需要 Kernel 或 cudaMemcpy
错误处理
返回 nullptr
返回 cudaError_t
为了统一管理,我们需要一个抽象的 Allocator 接口,以及针对 CUDA 的具体实现。
2. Allocator 抽象接口回顾
12345678910111213141516171819202122232425// mini_infer/core/ ...
Mini-Infer (28): Core 数据结构优化 — Storage 与 Tensor 分离
Mini-Infer (28): Core 数据结构优化 — Storage 与 Tensor 分离
1. 问题背景:为什么 Tensor 需要与 Storage 分离?
在早期的 Mini-Infer 实现中,Tensor 类直接持有数据指针和分配器。这种设计在简单场景下工作良好,但随着功能的增加,问题逐渐暴露:
A. 内存池复用的需求
静态内存规划(Blog 23)要求多个 Tensor 共享同一块预分配的内存。如果 Tensor 直接持有数据指针,就无法优雅地实现这种共享。
123456┌─────────────────────────────────────────────────────┐│ Shared Memory Pool │├─────────┬─────────┬─────────┬─────────┬─────────────┤│ Tensor A│ Tensor B│ Tensor C│ Tensor D│ ... ││ offset=0│offset=1K│offset= ...
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:: ...
