29、C++与机器学习:ONNX Runtime集成、TensorRT C++ API、高性能推理引擎、模型部署实践
说实话,很多做C++的朋友一听到「机器学习部署」就觉得头大。觉得那是Python工程师的事,跟自己没关系。
但我得说,这种想法得改改了。真正的高性能推理,最终还是要落到C++头上。Python只是用来训练和实验的,生产环境里,C++才是主角。
今天我们就聊聊,怎么用C++把训练好的模型跑起来,跑得快,跑得稳。
为什么是C++来做推理?
你想想看,Python的GIL锁、动态类型、解释执行……这些特性在训练阶段无所谓,但在线上推理时,每一个微秒都很关键。我见过不少团队,模型在Python里跑得还行,一上生产就崩,延迟直接翻倍。
C++的优势很明显:
- 零运行时开销:没有解释器,没有GC,代码就是机器指令
- 内存控制精细:你可以手动管理每一块显存,避免碎片化
- 多线程亲和:C++的线程模型和硬件绑定更紧密,延迟可控
说白了,如果你在乎性能,C++是绕不开的。
ONNX Runtime:跨平台的推理中间件
ONNX Runtime是微软开源的推理引擎。它最大的好处是——你不用关心模型是用什么框架训练的。PyTorch、TensorFlow、还是飞桨?只要导出成ONNX格式,ONNX Runtime都能吃进去。
集成方式
我习惯用CMake来管理ONNX Runtime的依赖。官方提供了预编译的库,直接下载就行。
# CMakeLists.txt 示例
find_package(onnxruntime REQUIRED)
target_link_libraries(my_app PRIVATE onnxruntime::onnxruntime)
一个完整的推理示例
#include <onnxruntime/core/session/onnxruntime_cxx_api.h>
#include <iostream>
#include <vector>
int main() {
// 1. 创建环境
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test");
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(4);
// 2. 加载模型
const char* model_path = "model.onnx";
Ort::Session session(env, model_path, session_options);
// 3. 获取输入输出信息
Ort::AllocatorWithDefaultOptions allocator;
auto input_name = session.GetInputNameAllocated(0, allocator).get();
auto output_name = session.GetOutputNameAllocated(0, allocator).get();
// 4. 准备输入数据
std::vector<float> input_data(1 * 3 * 224 * 224, 1.0f);
std::vector<int64_t> input_shape = {1, 3, 224, 224};
Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(
OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
memory_info, input_data.data(), input_data.size(),
input_shape.data(), input_shape.size());
// 5. 执行推理
const char* input_names[] = {input_name};
const char* output_names[] = {output_name};
auto output_tensors = session.Run(Ort::RunOptions{nullptr},
input_names, &input_tensor, 1,
output_names, 1);
// 6. 获取结果
float* output_data = output_tensors[0].GetTensorMutableData<float>();
std::cout << "推理完成,第一个输出值: " << output_data[0] << std::endl;
return 0;
}
TensorRT C++ API:NVIDIA的加速利器
如果你用的是NVIDIA显卡,那TensorRT几乎是必选项。它能把模型做量化、层融合、内核自动调优……说白了,就是榨干GPU的每一分性能。
构建引擎
TensorRT的工作流程是:ONNX模型 → 构建引擎 → 序列化保存 → 推理时反序列化加载。
#include <NvInfer.h>
#include <NvOnnxParser.h>
#include <fstream>
#include <memory>
// 构建引擎
nvinfer1::ICudaEngine* buildEngine(
nvinfer1::IBuilder* builder,
nvinfer1::IBuilderConfig* config,
const std::string& onnx_path) {
auto network = builder->createNetworkV2(
1U << static_cast<int>(
nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH));
auto parser = nvonnxparser::createParser(*network, gLogger);
parser->parseFromFile(onnx_path.c_str(),
static_cast<int>(nvinfer1::ILogger::Severity::kWARNING));
// 设置工作空间大小和精度
config->setMemoryPoolLimit(
nvinfer1::MemoryPoolType::kWORKSPACE, 1 << 30); // 1GB
config->setFlag(nvinfer1::BuilderFlag::kFP16); // 启用FP16
return builder->buildSerializedNetwork(*network, *config);
}
推理执行
void infer(nvinfer1::IExecutionContext* context,
float* input, float* output,
int batch_size) {
// 绑定输入输出缓冲区
void* buffers[2];
cudaMalloc(&buffers[0], batch_size * 3 * 224 * 224 * sizeof(float));
cudaMalloc(&buffers[1], batch_size * 1000 * sizeof(float));
cudaMemcpy(buffers[0], input,
batch_size * 3 * 224 * 224 * sizeof(float),
cudaMemcpyHostToDevice);
context->executeV2(buffers);
cudaMemcpy(output, buffers[1],
batch_size * 1000 * sizeof(float),
cudaMemcpyDeviceToHost);
cudaFree(buffers[0]);
cudaFree(buffers[1]);
}
高性能推理引擎的设计要点
光会调API还不够,真正的工程落地,还得考虑架构设计。我总结了几条核心原则:
- 批处理(Batching):单条推理效率低,攒一批一起算,吞吐量能翻好几倍。我习惯用动态批处理,设置一个最大延迟阈值,到了就触发推理。
- 异步流水线:预处理、推理、后处理用三个线程流水线并行。不要让GPU闲着等CPU。
- 内存池复用:每次推理都new/delete内存,性能损耗很大。用内存池预分配,重复使用。
- 模型预热:刚加载的模型第一次推理很慢,因为缓存没建立。启动时先跑几次假数据,把状态预热好。
模型部署实践:从开发到上线
部署不是把模型文件扔到服务器上就完事了。我经历过几次线上事故后,总结了一套流程:
| 阶段 | 关键动作 | 常见坑 |
|---|---|---|
| 模型导出 | ONNX导出,检查算子兼容性 | 动态shape没处理好,导出失败 |
| 精度验证 | 对比原始模型和推理引擎的输出 | FP16量化后精度下降,需要校准 |
| 性能压测 | 用真实数据跑压力测试 | 显存泄漏,跑几天就OOM |
| 灰度上线 | 先切1%流量观察 | 模型版本管理混乱,回滚困难 |
核心思路:推理引擎只是工具,真正的难点在于工程化。把模型部署当成一个独立的服务来设计,做好监控、日志、版本管理、优雅降级。这样即使模型出了问题,服务也不会崩。
知识体系总览
下面这张图,是我梳理的C++机器学习推理的核心知识结构。你可以把它当作一个checklist,看看自己哪些地方还没覆盖到。
这张图把今天讲的内容串起来了。左边是ONNX Runtime,主打通用和兼容;右边是TensorRT,主打极致性能;底部是部署实践,把两者落地到生产环境。
我个人建议,刚开始做推理部署的同学,先从ONNX Runtime入手。它简单、稳定、坑少。等你对整个流程熟悉了,再上TensorRT做极致优化。别一上来就搞量化、搞层融合,容易把自己搞懵。
好了,关于C++与机器学习推理引擎的内容,今天就聊到这里。代码示例都在上面了,你可以直接拿去跑。遇到问题别慌,多看看日志,多打几个断点,总能找到原因。
公众号:蓝海资料掘金营,微信deep3321