AI模型小解
从常用的模型部署开始讲解,并简单描述推理引擎的作用,以及训练工程师优化的作用。
1 模型部署
当前模型训练,常用 tensorflow、pytorch 等框架,其模型格式皆有差异。若希望将其部署到 C++ 运行环境下,则需要进行一系列转换。
1.1 ✅ PyTorch 模型 → C++ 部署
使用 LibTorch(PyTorch 的 C++ 发行版):
| 步骤 | 内容 |
|---|---|
| 1. 导出模型 | 将模型转为 TorchScript 格式(.pt 文件): |
| 2. 安装依赖 | 下载并解压 LibTorch(选择与你的系统匹配的版本) |
| 3. 编译项目 | 使用 CMake 配置: |
| 4. 加载与推理 | 使用 torch::jit::load() 加载模型,输入为 torch::jit::IValue,输出为 at::Tensor |
1.2 ✅ TensorFlow 模型 → C++ 部署
使用 TensorFlow C++ API 或 TensorFlow Lite:
1.2.1 方法一:TensorFlow C++ API(适合服务器端)
| 步骤 | 内容 |
|---|---|
| 1. 导出模型 | 将 .pb(frozen graph)或 SavedModel 格式模型准备好 |
| 2. 编译 TensorFlow C++ | 使用 Bazel 编译 TensorFlow 源码,生成 .so 和头文件 |
| 3. 加载模型 | 使用 tensorflow::Session 加载模型,输入为 Tensor,输出为 std::vector<Tensor> |
| 4. 编译项目 | 配置 CMake 或 Bazel,链接 libtensorflow_cc.so 和头文件 |
1.2.2 方法二:TensorFlow Lite(适合嵌入式/移动端)
| 步骤 | 内容 |
|---|---|
| 1. 转换模型 | 使用 tflite_convert 将 .pb 转为 .tflite |
| 2. 安装依赖 | 下载 TensorFlow Lite C++ |
| 3. 加载模型 | 使用 tflite::FlatBufferModel::BuildFromFile() 加载 .tflite 模型 |
| 4. 推理 | 使用 tflite::Interpreter 执行推理 |
1.3 ✅ 通用方案:ONNX → C++(跨框架)
如果你希望跨框架部署(如 PyTorch → ONNX → C++):
| 步骤 | 内容 |
|---|---|
| 1. 导出 ONNX | 使用 torch.onnx.export() 或 tf2onnx 转换为 .onnx |
| 2. 使用 ONNX Runtime C++ | 下载 ONNX Runtime C++ SDK |
| 3. 加载模型 | 使用 Ort::Session 加载 ONNX 模型并推理 |
1.4 ✅ 总结:你需要导入的环境
| 框架 | C++ 工具链 | 依赖库 |
|---|---|---|
| PyTorch | LibTorch | libtorch + CMake |
| TensorFlow | TensorFlow C++ API | libtensorflow_cc.so + 头文件 |
| TensorFlow Lite | TFLite C++ | libtensorflowlite.so + 头文件 |
| ONNX | ONNX Runtime | libonnxruntime.so + 头文件 |
2 推理引擎
如果希望 “一次训练,多端部署”,ONNX 是目前最成熟、最省心的方案之一。
- 训练阶段:用最熟悉的框架(PyTorch/TensorFlow)。
- 转换阶段:导出
.onnx并用工具链优化。 - 部署阶段:根据目标硬件选择 ONNX Runtime、TensorRT、OpenVINO 等推理引擎即可。
一个模型文件的应用,到部署阶段大致能分为上述三个步骤,下面针对第三个阶段**“推理引擎”**的功能,做出简单的描述。用 .onnx 模型进行举例:
首先,ONNX 本身只是一份标准化文件格式(.onnx),它把模型结构、权重和算子定义保存下来,但并不会替你自动做运行时调度。 真正决定“用 CPU 还是 GPU、如何并行、如何融合算子”的是选用的推理引擎(ONNX Runtime、TensorRT、OpenVINO、MNN、NCNN …) 。
2.1 训练 → .pt/.ckpt
负责把 PyTorch / TF / … 训练产出,此时只有“原始权重+结构”,此时只有一张静态图,没有任何离线优化,没有任何调度逻辑。
2.2 转换 → ONNX
将上述训练模型导出成 .onnx。此时所谓工具链优化,相当于**“离线图改写”** ,改的是上述步骤的静态图本身。假设跳过这一步,推理引擎只能拿到一张“原始图”,很多加速机会就错过了。
那么,工具链优化具体干了什么?可以用一张表格简单列举以下:
2.2.1 ✅ 工具链优化(离线)——具体干了什么?
| 动作 | 作用 | 举例 | 不做会怎样 |
|---|---|---|---|
| 常量折叠 | 把可提前算出的节点直接算成常量 | Conv+ConstAdd → 合并权重 |
每次推理都重复计算 |
| 死代码删除 | 去掉永远不会执行的节点 | Dropout 在 eval 模式下被剪枝 | 多占内存/算力 |
| 算子融合 | 把多个小算子合并成一个大算子 | Conv+BN+ReLU → ConvReLU |
需要多次 Kernel 启动 |
| 数据布局转换 | 把 NCHW ↔ NHWC 或插入 transpose | 让后端能用 SIMD/TensorCore | 引擎只好插昂贵的转置 |
| 量化/稀疏化 | 把 FP32 权重转 INT8 或稀疏格式 | 权重压缩 4×,算子变 INT8 | 引擎只能跑 FP32 |
| 形状推断与静态化 | 把动态 shape 转成固定 shape | 避免引擎每次重编译 | 引擎频繁 realloc,延迟抖动 |
| 子图提取 | 把 GPU/NPU 不支持的节点拆出来 | CPU 子图 + GPU 子图 | 引擎 fallback 到 CPU,整体降速 |
典型工具:
onnxoptimizer、onnxsim、tf2onnx --fold_const、torch.onnx.export(do_constant_folding=True)。
在使用 LibTorch、TensorFlow家族时,也有相关的工具链优化步骤,只是专用名词不同,但本质一样。
如,转换/固化:torch.jit.trace / torch.jit.script 得到 .pt(TorchScript 文件) ⬅︎ 这就是 PyTorch 生态里的“工具链优化”阶段。
又如,TensorFlow C++ 工具链优化发生在 “冻结图 + optimize_for_inference” 这一步,产出 .pb。TensorFlow Lite:工具链优化发生在 “tflite_convert” 这一步,产出 .tflite。
2.3 ONNX → 推理引擎
部署时,需要把 .onnx 喂给某个推理引擎。引擎会:
- 解析 ONNX 图
- 根据当前机器/板端上的硬件能力(CPU 核心数量、有没有 GPU/NPU、驱动版本等)静态或动态地决定:
- 哪些节点跑在 CPU
- 哪些节点跑在 GPU/NPU
- 哪些节点可以并行
- 哪些算子可以融合
- 生成最终可执行的“内部图”或二进制
这些调度策略是推理引擎开发者预先写好的,用 C++/Python API 只能做“开关”式配置,例如:
Ort::SessionOptions opts;
opts.AppendExecutionProvider_CUDA(cuda_options); // 让 ORT 尽量用 GPU
2.4 ✅ 推理引擎优化(运行时)——具体干了什么?
| 动作 | 作用 | 举例 |
|---|---|---|
| 内存池/复用 | 生命周期不重叠的张量共享同一块显存 | 把 Conv1_out 和 Conv2_out 复用 |
| Kernel 选择 | 根据硬件指令集挑最快实现 | AVX512 vs AVX2、cuDNN vs cuBLAS |
| 并行调度 | 把无依赖节点扔给不同线程/GPU Stream | MatMul 与 ReLU 并行 |
| 动态 shape 编译缓存 | 同一 shape 第二次直接走缓存 | 避免重复编译 |
| FP16/INT8 Kernel 即时切换 | 如果硬件支持即用低精度路径 | TensorRT 的 tactic selection |
所谓推理引擎优化是**“运行时策略”**,发生在加载 .onnx 后,不改动图结构,只做内存布局、Kernel 选择、并行调度等。
3 模型优化
虽然推理引擎和工具链有不少优化项加速模型,但训练工程师在“把模型交给推理引擎之前”能做很多决定性能天花板的优化。可以把它们分成 结构级、算子级、数值级、数据级 四大类。
3.1 1️⃣ 结构级(Graph-level)——决定“能不能跑”和“跑多快”
| 动作 | 目的 | 典型工具/做法 | 收益 | 注意 |
|---|---|---|---|---|
| 算子融合(Conv+BN+ReLU) | 减少访存、启动开销 | torch.nn.Conv2d(..., bias=False) + torch.nn.BatchNorm2d + torch.nn.ReLU → torch.nn.ConvReLU2d |
1.2-3× 提速 | 需确保推理引擎支持 fused kernel |
| 替换等价结构 | 减少参数量或计算量 | MobileNet、EfficientNet、GhostNet、Depthwise Separable Conv、Group Conv | 2-10× 压缩 | 可能牺牲精度,需重训 |
| 动态图→静态图 | 让引擎做更激进的图优化 | torch.jit.trace / torch.jit.script |
10-30% | trace 对控制流不友好 |
| 剪枝(结构剪枝) | 直接砍掉通道/层 | torch.nn.utils.prune / NNI / AMC |
2-5× FLOPs 降 | 需重训恢复精度 |
| 知识蒸馏 | 小模型学大模型 | Teacher-Student 框架 | 精度↑+速度↑ | 训练时间↑ |
3.2 2️⃣ 算子级(Kernel-level)——决定“引擎好不好调度”
| 动作 | 目的 | 做法 | 收益 | 注意 |
|---|---|---|---|---|
| 对齐输入/输出维度 | 避免引擎插入昂贵的 reshape/transpose | 训练时就用 NCHW(或引擎偏好格式) | 10-50% | 部分引擎对 NHWC 更友好 |
| 避免冷门算子 | 减少 fallback 到 CPU | 用标准 ONNX opset 11/12/13 内的算子 | 避免崩溃 | 自定义算子需写 plugin |
| 常量折叠 | 减少运行时计算 | 训练后 freeze BN 参数 | 轻微 | 需确保推理时也 freeze |
| 算子参数对齐 | 触发引擎融合 | Conv groups/sizes 与硬件指令集匹配 | 2-4× | 需查阅硬件白皮书 |
3.3 3️⃣ 数值级(Numeric-level)——决定“跑多省”
| 动作 | 目的 | 做法 | 收益 | 注意 |
|---|---|---|---|---|
| 训练时量化感知 (QAT) | 让 INT8 精度损失最小 | torch.quantization.prepare_qat → 重训 |
2-4× 提速 + 4× 模型压缩 | 必须与推理量化位宽一致 |
| 混合精度训练 | 权重用 FP16 | torch.cuda.amp |
1.5-2× 训练提速 | 推理时需确认引擎支持 FP16 |
| 稀疏化训练 | 激活/权重稀疏 | torch.sparse 或 RigL 算法 |
1.5-2× 提速 | 需要硬件支持稀疏指令 |
3.4 4️⃣ 数据级(I/O & 预处理)——决定“端到端延迟”
| 动作 | 目的 | 做法 | 收益 | 注意 |
|---|---|---|---|---|
| 输入分辨率/帧率缩放 | 减少计算量 | 训练时就用 224×224 而非 512×512 | 平方级收益 | 精度可能降 |
| 动态形状优化 | 避免引擎重编译 | 训练时固定 batch/size | 避免延迟抖动 | 对检测模型尤重要 |
| 预处理融合到模型 | 减少额外 memcpy | 把 mean/std/resize 写成 ONNX 节点 | 5-10% | 需引擎支持 |
3.5 ✅ 训练工程师的“上线 checklist”
- 跑通转换:
torch.onnx.export或tf2onnx无报错,ONNX Checker 通过。 - 性能基准:在目标引擎 + 目标硬件上跑
latency(ms)、memory(MB)、FPS。 - 量化验证:INT8 精度下降 < 1%(业务可接受)。
- 算子白名单:所有节点都在推理引擎的硬件加速列表里,无 CPU fallback。
- 结构化剪枝:砍掉 ≥30% 通道,重训精度恢复。
训练工程师的优化是“给引擎一张好牌”:结构精简、算子标准、数值友好、数据对齐。如果牌面太差,再强的引擎也无法挽救。