把训练好的模型接到C++框架里,不等于把Python推理脚本翻译成C++。真正的集成工作集中在三件事上:选择推理引擎、统一数据通路、管理模型生命周期。很多性能问题都出在数据反复拷贝和线程竞争上,而不是模型本身算得慢。因此动手之前需要先确认C++侧对AI能力的调用频率、单次延迟预算,以及是否可以接受进程外通信。

从工程实践看,集成方式可以分成两类。一类是把AI平台导出的模型加载进C++进程内,由ONNX Runtime、TensorRT或OpenVINO完成前向计算;另一类是把AI能力保留在Python服务中,C++框架通过gRPC或HTTP请求获取结果。前者的优势是延迟可控、部署简单,缺点是升级模型需要重新编译或重启进程。后者牺牲少量网络时间,但算法团队可以独立发布模型,C++主程序不用频繁改动。
一、嵌入式推理与服务化调用:先分清两种集成模式
嵌入式推理适合对延迟要求苛刻的场景。比如一条视觉检测流水线要求单帧处理不超过10毫秒,如果每次检测还要跨进程走一次网络,序列化和协议解析会占掉大量预算。此时直接把ONNX模型加载到当前进程,利用C++ API构造输入张量并调用Run,可以让数据只在内存中流转一次。ONNX Runtime默认支持CPU执行,也能切换到CUDA或TensorRT后端,代码层面只需要调整SessionOptions。
服务化调用更适合算法快速变化的业务。C++程序不需要知道模型是PyTorch还是TensorFlow,只需要按协议发送张量数据,由推理服务完成预处理、前向计算和后处理。这种方式带来的额外延迟通常在1到5毫秒甚至更高,取决于gRPC配置、消息大小和网络环境。对于一天只调用几百次的批处理任务,这点开销可以忽略;对于每帧都要调用的实时系统,则需要谨慎评估。
实际项目中常见的是混合形态:C++框架对简单、固定的检测模型采用嵌入式推理,对复杂且需要频繁更新的模型走服务化调用。这样既保证核心路径的低延迟,又保留算法团队快速试错的空间。
二、用ONNX Runtime把模型加载进C++进程
ONNX Runtime是微软维护的跨平台推理引擎,支持从PyTorch、TensorFlow等框架导出的ONNX模型。在C++中使用它需要先引入头文件并初始化环境。会话对象负责加载模型,输入输出通过Ort::Value管理,底层内存可以由用户指定。下面是一个加载图像分类模型并执行一次推理的完整流程。
实际开发中可以通过 <onnxruntime_cxx_api.h> 使用相关API。其中 Ort::Session 是加载和运行模型的入口,所有张量都通过 Ort::Value 传递。
#include <onnxruntime_cxx_api.h>
#include <iostream>
#include <vector>
int main() {
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "cpp_ai_demo");
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(1);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
const wchar_t* model_path = L"model.onnx";
Ort::Session session(env, model_path, session_options);
Ort::AllocatorWithDefaultOptions allocator;
auto input_name = session.GetInputNameAllocated(0, allocator);
auto output_name = session.GetOutputNameAllocated(0, allocator);
std::vector<int64_t> input_shape = {1, 3, 224, 224};
std::vector<float> input_tensor_values(1 * 3 * 224 * 224, 0.0f);
Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
memory_info,
input_tensor_values.data(),
input_tensor_values.size(),
input_shape.data(),
input_shape.size()
);
const char* input_names[] = {input_name.get()};
const char* output_names[] = {output_name.get()};
auto output_tensors = session.Run(Ort::RunOptions{nullptr}, input_names, &input_tensor, 1, output_names, 1);
float* floatarr = output_tensors[0].GetTensorMutableData<float>();
auto output_shape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape();
size_t output_size = output_tensors[0].GetTensorTypeAndShapeInfo().GetElementCount();
std::cout << "output size: " << output_size << std::endl;
return 0;
}
这段代码先创建了一个单线程环境,再开启全部图优化。输入张量按照模型要求的1×3×224×224维度初始化,并用CreateTensor将一段连续内存包装成Ort::Value。真正执行推理只有session.Run一行,但工程上需要提前确认输入输出名称是否与导出模型一致。可以用Netron打开模型查看节点命名,或者通过session.GetInputNameAllocated动态获取。
要注意Ort::Value不负责管理外部内存,传入的input_tensor_values在Run返回前必须保持有效。对于大尺寸输入,建议使用Ort::MemoryInfo::CreateCpu分配内存,避免vector扩容时的数据复制。切换到GPU时,需要把MemoryInfo指定为CUDA,并保证输入数据已经拷贝到显存。
三、高性能路线:TensorRT与OpenVINO的关键差异
如果硬件锁定在NVIDIA GPU,TensorRT通常能获得比ONNX Runtime GPU后端更低的延迟。TensorRT的构建器会把模型编译成针对具体GPU型号优化的engine,之后加载engine创建执行上下文。它的缺点是构建过程较慢,并且engine不能跨显卡或跨TensorRT版本直接复用。实际部署时可以在首次启动时构建engine并缓存到磁盘,之后的进程直接反序列化加载。
如果目标硬件是Intel CPU、GPU或NPU,OpenVINO的C++ API更统一。下面给出一个在CPU上加载OpenVINO模型并执行推理的简洁示例。
#include <openvino/openvino.hpp>
#include <iostream>
int main() {
ov::Core core;
auto model = core.read_model("model.xml");
auto compiled_model = core.compile_model(model, "CPU");
auto infer_request = compiled_model.create_infer_request();
auto input_tensor = infer_request.get_input_tensor();
float* input_data = input_tensor.data<float>();
// 按模型输入shape填充数据,这里只设置一个值作为演示
input_data[0] = 0.5f;
infer_request.infer();
auto output_tensor = infer_request.get_output_tensor();
const float* output_data = output_tensor.data<const float>();
std::cout << "first output: " << output_data[0] << std::endl;
return 0;
}
OpenVINO通过Core读取IR模型文件,compile_model时指定设备,之后创建推理请求。这个流程和ONNX Runtime有点相似,但InferRequest的输入输出张量可以通过data方法直接拿到连续内存指针。对于多设备切换,只需要把CPU改成GPU或AUTO,代码几乎不用改。TensorRT虽然性能更强,但需要手动管理builder、network、engine和context的生命周期,集成成本更高。
量化也是影响推理性能的关键。TensorRT的int8量化需要校准数据集,OpenVINO提供训练后量化工具,两者都能在模型精度损失可控的前提下显著提升吞吐。C++框架侧一般不需要改业务代码,只替换模型文件和执行provider即可。
四、用gRPC将AI能力拆成独立服务
当C++框架和算法团队分属不同代码仓库时,gRPC是比HTTP更合适的通信方式。它使用Protocol Buffers进行二进制序列化,支持多语言,天然适合张量数据交换。服务端可以继续使用Python生态的PyTorch或TensorFlow,客户端只需要维护一份proto文件和自动生成的C++代码。
syntax = "proto3";
package ai.inference;
service InferenceService {
rpc Predict(PredictRequest) returns (PredictResponse);
}
message PredictRequest {
bytes input_data = 1;
repeated int64 shape = 2;
string model_name = 3;
}
message PredictResponse {
bytes output_data = 1;
repeated int64 shape = 2;
int32 code = 3;
}
这段proto定义了输入数据和输出数据都使用bytes,形状单独用repeated int64传递。这样可以传输float32或多精度数据,而不需要为每个模型重写协议。C++客户端调用时,把输入张量连续内存拷贝到PredictRequest的input_data字段,再通过stub发起同步或异步调用。
#include <grpcpp/grpcpp.h>
#include "inference.grpc.pb.h"
int main() {
auto channel = grpc::CreateChannel("127.0.0.1:50051", grpc::InsecureChannelCredentials());
std::unique_ptr<ai::inference::InferenceService::Stub> stub =
ai::inference::InferenceService::NewStub(channel);
ai::inference::PredictRequest request;
request.set_model_name("detection_v3");
request.add_shape(1);
request.add_shape(3);
request.add_shape(224);
request.add_shape(224);
ai::inference::PredictResponse response;
grpc::ClientContext context;
grpc::Status status = stub->Predict(&context, request, &response);
if (status.ok()) {
// 处理response.output_data
}
return 0;
}
这个客户端示例中需要生成inference.grpc.pb.h和inference.pb.h,生成方式由protoc工具完成。网络调用返回的output_data是序列化后的张量内存,按shape还原成float数组即可。gRPC默认使用HTTP/2,连接复用比普通HTTP好,但仍不建议在每帧推理中传输超大张量,尤其是4K图像特征图,可能引发内存峰值和序列化瓶颈。
五、内存、线程与模型版本管理
C++框架与AI平台集成时,内存管理是最隐蔽的稳定性杀手。ONNX Runtime的Ort::Value持有推理结果,如果频繁调用session.Run而不及时释放,会出现显存或内存持续增长。推荐在循环中复用输入输出的Ort::Value对象,或者使用作用域限制让智能指针及时析构。对于多线程调用同一个session,ONNX Runtime本身是线程安全的,但推荐为每个工作线程配置独立的session或使用线程池,减少内部锁竞争。
如果使用TensorRT,同一context不能同时被多个线程调用,需要为每个线程创建独立的context,或者用互斥锁串行执行。OpenVINO的InferRequest同样不适合并发共享,典型做法是维护一个请求池,每个线程从池中借用一个请求,完成后归还。
模型版本切换在嵌入式方案中比较麻烦。直接替换ONNX文件而不重启进程,可能导致旧session仍持有文件句柄或内存布局不一致。更稳妥的做法是启动新进程加载新模型,等流量切换后再关闭旧进程。服务化方案则天然支持滚动发布,推理服务先更新,C++客户端无需感知模型变化,只需保证协议兼容。
C++框架人工智能平台ONNX Runtime修改时间:2026-10-05 19:32:47