跨平台C++推理接口应避免万能抽象,采用PIMPL+工厂模式隔离后端,仅暴露init/infer等极简纯虚接口,输入输出内存由调用方完全控制,后端通过动态插件加载实现真正跨平台。

跨平台 C++ 机器学习推理接口层无法靠“一套代码通吃所有后端”,核心矛盾在于:不同推理引擎(ONNX Runtime、TensorRT、LibTorch、OpenVINO)的初始化逻辑、内存管理、数据布局(NHWC vs NCHW)、设备绑定(CPU/GPU)、甚至张量生命周期语义都完全不同。强行抽象出一个“万能 IInferenceEngine”只会导致接口臃肿、性能折损、或隐藏平台特定 bug。
用 PIMPL + 工厂模式隔离后端实现细节
不要让上层业务代码包含任何引擎头文件,也不依赖其类型定义。把具体引擎封装在 .cpp 内部,对外只暴露纯虚接口和工厂函数:
接口定义保持极简——只暴露最通用的、与模型无关的操作:
-
init(const std::string& model_path, const std::string& backend):backend 是字符串标识(如"onnxruntime-cpu"、"tensorrt-cuda"),避免模板泛化或枚举膨胀 -
infer(const float* input_data, float* output_data):强制使用原始指针 + 显式尺寸,绕过张量对象差异;尺寸信息由get_input_shape()等只读访问器提供 - 不提供
set_input_tensor()这类高阶封装——它在 ONNX Runtime 是Ort::Value,在 TensorRT 是IExecutionContext::setBinding,语义不可对齐
每个后端实现(如 OnnxRuntimeBackendImpl)在 .cpp 中私有包含其 SDK 头,并通过 PIMPL 持有 std::unique_ptr 或私有结构体存储上下文。工厂函数 create_inference_engine("onnxruntime-cuda") 返回 std::unique_ptr,调用方完全无感知。
输入/输出内存必须由调用方完全控制
几乎所有跨平台 crash 都源于内存归属混乱:TensorRT 要求 GPU 内存由其 cudaMalloc 分配,ONNX Runtime 允许 CPU 内存直接传入但 GPU 版本可能要求 pinned memory,LibTorch 则倾向接管 tensor 生命周期。统一抽象层必须放弃“帮你管理内存”的幻觉。
- 所有
infer()接口只接受const float*和float*,不接受torch::Tensor或Ort::Value - 提供
get_input_size_bytes()和get_output_size_bytes(),让调用方自行分配(new float[n]、cudaMalloc、clCreateBuffer)并传入指针 - 如果某后端真需要特殊内存(如 TensorRT 的
cudaHostAlloc),在init()成功后通过get_required_memory_type()告知调用方,而不是自动分配
这样既规避了跨引擎内存释放冲突(谁分配谁释放),也避免了隐式拷贝——比如 ONNX Runtime CPU 版本可零拷贝,而你若封装成 set_input_tensor(tensor) 就必然触发一次 memcpy。
运行时动态加载后端,而非编译期链接
把 libonnxruntime.so、libnvinfer.so 等作为插件动态加载,是真正实现“跨平台可选”的关键。否则 Windows 上链接了 cudart.lib,Linux 就根本编译不过。
- 定义统一插件 ABI(例如 C 风格函数表:
struct InferencePlugin { void* (*create)(); int (*init)(void*, const char*); ... };) - 每个后端编译为独立共享库(
libinference_onnxruntime.so),导出get_inference_plugin()返回函数表指针 - 主程序用
dlopen()/LoadLibrary()按需加载,失败则降级到 CPU 后端或报明确错误("failed to load tensorrt: libnvinfer.so not found")
这比 cmake 的 find_package(OpenVINO) 更灵活:用户可在无 GPU 环境部署仅含 ONNX Runtime 的二进制,在有 TensorRT 的服务器上替换插件目录即可启用加速,无需重新编译主程序。
最难处理的其实是数据预处理——Resize、Normalize、BGR2RGB 这些操作在 OpenCV、libyuv、甚至模型输入节点里都有不同实现。抽象层不该管这个,而是明确约定:输入必须是 HWC 布局、0–255 uint8 或 0–1 float32,归一化参数由模型配置文件(如 JSON)声明,交由上层业务代码完成。越试图在接口层“统一预处理”,越容易在 ARM 设备或 WebAssembly 上栽跟头。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











