把训练好的模型部署到C++工程里,是很多算法工程师走向生产的必经之路。Python适合训练和验证,但真正上线时,低延迟、高并发的服务大多还是用C++写的。集成过程涉及模型格式转换、依赖管理、推理引擎选择、线程安全与内存管理等多个环节,任何一步处理不好都会出问题。本文从方案选型讲起,配合完整的工程配置和代码示例,帮你梳理一条可落地的集成路径。

一、主流推理方案怎么选
目前C++侧调用模型主要有四条路线:ONNX Runtime、LibTorch、TensorFlow Lite C++ API和TensorRT。选型不能只看跑分,要结合部署环境、模型类型和团队技术栈综合判断。
ONNX Runtime是兼容性最好的选择。它支持Windows、Linux、macOS以及各类嵌入式平台,CPU推理开箱即用,装个GPU版本的包就能用CUDA。只要模型能转成ONNX格式,几乎所有训练框架的产物都能跑。对于需要跨平台部署、模型来源多样的团队,优先推荐它。
LibTorch是PyTorch官方的C++前端,API风格和Python版几乎一致。如果你需要加载带自定义控制流的TorchScript模型,或者要复用训练侧的大量算子,LibTorch最省事。缺点是体积大,动态库动辄一两GB,对嵌入式场景不友好。
TensorFlow Lite面向移动端和边缘设备,模型体积小、启动快,但算子支持相对有限,复杂模型转换时经常遇到不支持的层。TensorRT则是NVIDIA平台上的性能天花板,它会对模型做层融合、精度量化、kernel自动调优,推理速度通常比其他方案快不少,代价是只支持N卡,且不同显卡驱动版本之间的兼容性需要仔细验证。
| 方案 | 跨平台 | GPU支持 | 适用场景 |
|---|---|---|---|
| ONNX Runtime | 优秀 | CUDA / DirectML | 通用部署、服务端推理 |
| LibTorch | 良好 | CUDA | PyTorch生态、TorchScript模型 |
| TensorFlow Lite | 优秀 | 部分支持 | 移动端、边缘设备 |
| TensorRT | 仅NVIDIA | 深度优化 | 极致低延迟、高吞吐 |
二、工程搭建与依赖管理
确定方案后,第一步是把依赖接入构建系统。以ONNX Runtime为例,推荐通过CMake的find_package方式接入。去官方仓库下载对应平台的压缩包,解压后得到include目录和lib目录,在CMakeLists.txt中写清楚路径即可。也可以用vcpkg一键安装,避免手工管理版本。
cmake_minimum_required(VERSION 3.18)
project(ml_inference_demo)
set(CMAKE_CXX_STANDARD 17)
# 方式一:直接指定ONNX Runtime路径
set(ONNXRUNTIME_ROOTDIR "/opt/onnxruntime-linux-x64-1.17.0")
find_package(Threads REQUIRED)
add_executable(infer_demo src/main.cpp)
target_include_directories(infer_demo PRIVATE
${ONNXRUNTIME_ROOTDIR}/include)
target_link_libraries(infer_demo PRIVATE
${ONNXRUNTIME_ROOTDIR}/lib/libonnxruntime.so
Threads::Threads)
这里有个容易被忽略的细节:如果用vcpkg安装,注意 triplet要与目标平台匹配,比如x64-linux或x64-windows-static,否则链接阶段会报找不到符号的错误。静态链接版本在Windows上可以省去分发DLL的麻烦,但体积会变大,需要权衡。
动态库的分发也要提前规划。Linux上建议在打包脚本里把so文件一起打进发布目录,并设置RPATH,让程序运行时优先找同目录下的库,避免服务器上环境不一致导致的加载失败。
三、加载模型并完成一次推理
依赖就绪后,写一个最小可运行的推理示例。以ONNX Runtime做图片分类为例,完整流程分为创建环境、加载会话、准备输入张量、执行推理、读取输出五步。输入预处理(归一化、resize)需要和训练时严格一致,否则结果会静悄悄地错掉,这是最常见的坑之一。
#include <onnxruntime_cxx_api.h>
#include <iostream>
#include <vector>
int main() {
// 1. 创建环境和会话选项
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "demo");
Ort::SessionOptions options;
options.SetIntraOpNumThreads(4); // 控制单个推理的线程数
// 2. 加载模型
Ort::Session session(env, "resnet50.onnx", options);
// 3. 查询输入输出信息(第一次集成时务必打印确认)
Ort::AllocatorWithDefaultOptions allocator;
auto inputName = session.GetInputNameAllocated(0, allocator);
auto outputName = session.GetOutputNameAllocated(0, allocator);
std::cout << "input: " << inputName.get()
<< ", output: " << outputName.get() << std::endl;
// 4. 构造输入张量:batch=1, 3通道, 224x224
std::vector<float> inputData(1 * 3 * 224 * 224, 0.5f);
std::vector<int64_t> shape = {1, 3, 224, 224};
Ort::MemoryInfo memInfo = Ort::MemoryInfo::CreateCpu(
OrtAllocatorType::OrtArenaAllocator, OrtMemType::OrtMemTypeDefault);
Ort::Value inputTensor = Ort::Value::CreateTensor<float>(
memInfo, inputData.data(), inputData.size(),
shape.data(), shape.size());
// 5. 执行推理并读取输出
const char* inputNames[] = {inputName.get()};
const char* outputNames[] = {outputName.get()};
auto output = session.Run(Ort::RunOptions{nullptr},
inputNames, &inputTensor, 1,
outputNames, 1);
float* logits = output[0].GetTensorMutableData<float>();
std::cout << "输出维度第一个元素: " << logits[0] << std::endl;
return 0;
}
注意NCHW和NHWC的布局差异。PyTorch导出的模型一般是NCHW,TensorFlow系是NHWC,如果输入排布弄反,模型不会报错,只会给出离谱的置信度,排查起来非常费时间。建议先用一张已知结果的测试图片跑一遍,和Python侧的输出逐个对比,误差在浮点精度范围内才算通过。
另一个建议是把推理逻辑封装成独立的类,构造时加载模型,Run方法只做推理。这样在多线程服务里,同一个Session可以被多个线程并发调用,官方保证Run是线程安全的,但Session的创建和销毁要放在单线程阶段完成。
四、性能优化与生产环境注意事项
集成跑通只是第一步,生产环境还要解决吞吐和稳定性问题。线程模型上,ONNX Runtime的Session支持并发调用,可以配合线程池处理请求。SetIntraOpNumThreads控制单次推理内部并行度,SetInterOpNumThreads控制并行执行多个算子的线程数,两者之和不要超过CPU核数,否则上下文切换开销会吃掉性能。
GPU场景下,重点是把预处理也搬到GPU上。用OpenCV的cuda::GpuMat做解码和resize,再通过CUDA内存把数据喂给模型,避免CPU和GPU之间频繁拷贝。对于固定的输入尺寸,还可以启用IO Binding,提前绑定输入输出缓冲区,减少每次推理的内存分配。如果追求极致性能,把ONNX模型转成TensorRT引擎, INT8量化后在大型显卡上往往还能再快一倍以上。
// 启用CUDA执行提供者的关键代码
Ort::SessionOptions options;
options.AppendExecutionProvider_CUDA(0); // 0表示第一块GPU
options.SetGraphOptimizationLevel(
GraphOptimizationLevel::ORT_ENABLE_ALL);
内存管理方面要警惕两件事。一是输入缓冲区的生命周期,CreateTensor默认不拷贝数据,指针指向的内存必须在整个Run期间有效,传临时vector进去会引发偶发的段错误。二是异常处理,推理调用要包在try-catch里捕获Ort::Exception,日志记录输入的shape和模型版本,方便线上排查。
最后是验证环节。上线前用同一批样本分别在Python和C++两侧推理,统计最大绝对误差。 FP32下误差应在1e-5量级以内,如果明显偏大,多半是预处理不一致或布局搞反了。建立这套对齐机制后,后续模型迭代升级也能快速回归验证,整个集成链路才算真正稳定可靠。