LiuJuan20260223Zimage模型C高性能推理接口封装教程如果你是一名C开发者正在寻找一种既能榨干硬件性能又能保持代码优雅的AI模型部署方案那么你来对地方了。今天我们不谈那些拖家带口的Python框架也不讲云端API调用的网络延迟就聚焦于一件事如何用纯正的C为LiuJuan20260223Zimage模型打造一个飞快的本地推理引擎。想象一下你需要在一个嵌入式设备上实时处理图像或者在一个高并发的服务器上批量处理成千上万的图片请求。Python的GIL锁和解释器开销可能会成为瓶颈。这时一个轻量、高效、内存可控的C推理接口就是你的终极武器。本教程将带你从零开始手把手构建这样一个高性能接口涵盖从环境搭建到并发优化的全流程。我们的目标很简单让推理速度起飞同时代码清晰可维护。1. 环境准备打造坚实的C开发地基在开始写代码之前我们需要一个稳定且高效的开发环境。这里我们选择CMake作为构建工具它跨平台且生态丰富是C项目的标准选择。1.1 基础工具链安装首先确保你的系统上安装了必要的编译器和工具。在Ubuntu系统上可以这样操作# 更新包列表并安装基础编译工具 sudo apt-get update sudo apt-get install -y build-essential cmake git # 安装CUDA Toolkit如果你的GPU支持并需要TensorRT加速 # 请根据你的CUDA版本从NVIDIA官网下载对应的runfile或deb包进行安装 # 例如安装CUDA 11.8 # wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run # sudo sh cuda_11.8.0_520.61.05_linux.run对于Windows用户建议安装Visual Studio 2019或更高版本包含MSVC编译器以及对应的CMake。macOS用户可以通过Homebrew安装brew install cmake。1.2 推理后端选择与安装接下来是关键一步选择推理后端。主流选择有两个ONNX Runtime 跨平台友好支持CPU和多种硬件加速CUDA TensorRT OpenVINO等部署灵活。TensorRT NVIDIA GPU上的极致性能优化器但生态相对封闭。本教程将以ONNX Runtime为主进行演示因为它更通用。如果你确定只在NVIDIA环境部署并追求极限性能可以平行参考TensorRT的流程。安装ONNX Runtime C库最推荐的方式是从源码编译以获得最适合你系统的版本。# 1. 克隆ONNX Runtime仓库 git clone --recursive https://github.com/microsoft/onnxruntime.git cd onnxruntime # 2. 配置编译选项。这里我们启用CUDA支持并只构建必要的库以减少体积。 # 创建一个构建目录并进入 mkdir build cd build # 运行CMake配置。请将/path/to/cuda替换为你的CUDA安装路径。 cmake .. \ -DCMAKE_BUILD_TYPERelease \ -Donnxruntime_BUILD_SHARED_LIBON \ -Donnxruntime_USE_CUDAON \ -Donnxruntime_CUDA_HOME/usr/local/cuda-11.8 \ -Donnxruntime_ENABLE_TRAININGOFF \ -Donnxruntime_BUILD_UNIT_TESTSOFF # 3. 开始编译使用-j参数指定并行编译的线程数加快速度 make -j$(nproc) # 编译完成后关键的库文件libonnxruntime.so在 ./lib 目录下 # 头文件在 ../include/onnxruntime/core/session/ 等目录中。编译完成后记下库文件和头文件的路径我们稍后在CMake中会用到。2. 项目骨架用CMake组织你的代码一个好的项目结构是成功的一半。我们来创建一个清晰的项目目录。LiuJuanZimage_CPP_Inference/ ├── CMakeLists.txt # 项目总构建文件 ├── src/ │ ├── CMakeLists.txt # 源代码构建文件 │ ├── inference_engine.cpp │ ├── inference_engine.h │ └── main.cpp # 示例主程序 ├── lib/ # 放置第三方库如ONNX Runtime ├── models/ # 放置转换好的.onnx模型文件 └── build/ # 构建输出目录由CMake生成现在我们来编写顶层的CMakeLists.txt。它的核心任务是找到依赖库并链接到我们的目标。# LiuJuanZimage_CPP_Inference/CMakeLists.txt cmake_minimum_required(VERSION 3.16) project(LiuJuanZimage_Inference VERSION 1.0.0 LANGUAGES CXX) # 设置C标准为17这是现代C项目的基础 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 设置编译模式为Release以获得最佳性能 if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() # 寻找必要的库 # 1. 查找OpenCV用于图像加载和预处理 find_package(OpenCV REQUIRED) message(STATUS Found OpenCV: ${OpenCV_INCLUDE_DIRS}) # 2. 指定ONNX Runtime的路径。 # 假设你将编译好的ONNX Runtime放在了项目根目录的 lib/onnxruntime-linux 下 set(ONNXRUNTIME_ROOT_DIR ${CMAKE_SOURCE_DIR}/lib/onnxruntime-linux) set(ONNXRUNTIME_INCLUDE_DIR ${ONNXRUNTIME_ROOT_DIR}/include) set(ONNXRUNTIME_LIB_DIR ${ONNXRUNTIME_ROOT_DIR}/lib) # 添加头文件搜索路径 include_directories(${OpenCV_INCLUDE_DIRS} ${ONNXRUNTIME_INCLUDE_DIR}) # 添加库文件搜索路径 link_directories(${ONNXRUNTIME_LIB_DIR} ${OpenCV_LIB_DIR}) # 添加子目录编译我们的源代码 add_subdirectory(src)然后是src/CMakeLists.txt它负责生成最终的可执行文件。# src/CMakeLists.txt # 将当前目录下的所有.cpp文件添加为源文件 file(GLOB_RECURSE SOURCES *.cpp) # 创建可执行目标 add_executable(liujuan_inference ${SOURCES}) # 链接库 # 注意链接库的名称需要根据ONNX Runtime编译出的实际文件名调整可能是 onnxruntime 或带后缀的版本 target_link_libraries(liujuan_inference ${OpenCV_LIBS} onnxruntime # 链接ONNX Runtime库 # 如果使用CUDA可能还需要链接cudart等CUDA运行时库 # cudart ) # 将模型目录复制到构建目录方便程序读取可选 configure_file(${CMAKE_SOURCE_DIR}/models/liujuan_model.onnx ${CMAKE_CURRENT_BINARY_DIR}/models/liujuan_model.onnx COPYONLY)3. 核心封装构建推理引擎类环境搭好了项目架子也立起来了现在进入核心环节编写推理引擎类。我们将遵循面向对象的设计原则创建一个InferenceEngine类它负责管理模型的生命周期、数据预处理和后处理。3.1 定义类接口首先在头文件inference_engine.h中定义类的接口。// inference_engine.h #ifndef INFERENCE_ENGINE_H #define INFERENCE_ENGINE_H #include string #include memory #include vector #include opencv2/opencv.hpp // 前向声明避免包含ONNX Runtime的头文件减少编译依赖 struct OrtSession; struct OrtMemoryInfo; struct OrtAllocator; struct OrtIoBinding; class InferenceEngine { public: // 构造函数传入模型路径和设备类型如CUDA:0或CPU explicit InferenceEngine(const std::string model_path, const std::string device CPU); ~InferenceEngine(); // 禁止拷贝构造和赋值确保资源管理安全 InferenceEngine(const InferenceEngine) delete; InferenceEngine operator(const InferenceEngine) delete; // 核心推理函数输入OpenCV Mat返回模型输出例如分类得分或检测框 std::vectorfloat infer(const cv::Mat input_image); // 获取模型期望的输入尺寸 (height, width, channels) std::vectorint64_t get_input_shape() const; private: // 初始化ONNX Runtime环境、会话和IO绑定 void init_onnx_runtime(const std::string model_path, const std::string device); // 图像预处理调整大小、归一化、HWC转CHW等 cv::Mat preprocess_image(const cv::Mat image) const; // 将预处理后的数据复制到模型输入张量 void copy_data_to_input_tensor(const cv::Mat processed_blob); // 从模型输出张量中提取数据 std::vectorfloat extract_output_data(); // ONNX Runtime相关资源 std::unique_ptrOrtSession, decltype(OrtReleaseSession) session_{nullptr, OrtReleaseSession}; std::unique_ptrOrtMemoryInfo, decltype(OrtReleaseMemoryInfo) memory_info_{nullptr, OrtReleaseMemoryInfo}; std::unique_ptrOrtIoBinding, decltype(OrtReleaseIoBinding) io_binding_{nullptr, OrtReleaseIoBinding}; // 其他必要的Ort环境、分配器等指针... // 模型信息 std::vectorint64_t input_shape_; std::string input_name_; std::string output_name_; size_t input_tensor_size_; // 输入张量总元素个数 }; #endif // INFERENCE_ENGINE_H3.2 实现推理引擎接下来在inference_engine.cpp中实现这些功能。这里会涉及一些ONNX Runtime的C API调用。// inference_engine.cpp #include inference_engine.h #include onnxruntime_c_api.h // ONNX Runtime C API #include onnxruntime_cxx_api.h // 也可以使用C API这里用C API演示更底层控制 #include stdexcept #include iostream // 辅助函数检查Ort状态 #define ORT_CHECK(expr) \ do { \ OrtStatus* status (expr); \ if (status ! nullptr) { \ const char* msg OrtGetErrorMessage(status); \ OrtReleaseStatus(status); \ throw std::runtime_error(std::string(ONNX Runtime error: ) msg); \ } \ } while(0) InferenceEngine::InferenceEngine(const std::string model_path, const std::string device) { init_onnx_runtime(model_path, device); } InferenceEngine::~InferenceEngine() { // 智能指针会自动释放资源 } void InferenceEngine::init_onnx_runtime(const std::string model_path, const std::string device) { // 1. 初始化全局环境单例通常一个进程一次 static OrtEnv* global_env nullptr; static std::once_flag init_flag; std::call_once(init_flag, [](){ ORT_CHECK(OrtCreateEnv(ORT_LOGGING_LEVEL_WARNING, LiuJuanInference, global_env)); }); // 2. 创建会话选项并设置执行设备 OrtSessionOptions* session_options nullptr; ORT_CHECK(OrtCreateSessionOptions(session_options)); // 设置优化级别和线程数 ORT_CHECK(OrtSetSessionExecutionMode(session_options, ORT_SEQUENTIAL)); ORT_CHECK(OrtSetIntraOpNumThreads(session_options, 1)); // 单线程执行算子 ORT_CHECK(OrtSetInterOpNumThreads(session_options, 1)); // 并行执行图的数量 // 设置设备 OrtSessionOptionsAppendExecutionProvider_CUDA(session_options, 0); // 如果使用CUDA // 对于CPU默认就是CPU无需额外设置 // 3. 创建会话加载模型 ORT_CHECK(OrtCreateSession(global_env, model_path.c_str(), session_options, session_)); // 4. 获取模型输入输出信息 size_t num_input_nodes; OrtTypeInfo* type_info nullptr; ORT_CHECK(OrtSessionGetInputCount(session_.get(), num_input_nodes)); // 假设只有一个输入 ORT_CHECK(OrtSessionGetInputName(session_.get(), 0, OrtGetAllocator(), input_name_)); ORT_CHECK(OrtSessionGetInputTypeInfo(session_.get(), 0, type_info)); // 从type_info中提取shape信息填充input_shape_ // ... (具体提取代码涉及OrtGetTensorTypeAndShape等API) OrtReleaseTypeInfo(type_info); // 5. 创建内存信息和IO绑定用于高性能推理 ORT_CHECK(OrtCreateCpuMemoryInfo(OrtDeviceAllocator, OrtMemTypeDefault, memory_info_)); ORT_CHECK(OrtCreateIoBinding(session_.get(), io_binding_)); OrtReleaseSessionOptions(session_options); } cv::Mat InferenceEngine::preprocess_image(const cv::Mat image) const { cv::Mat processed; // 1. 调整尺寸到模型输入大小 cv::resize(image, processed, cv::Size(input_shape_[3], input_shape_[2])); // 假设shape是[N,C,H,W] // 2. 转换颜色空间例如BGR转RGB如果模型需要 cv::cvtColor(processed, processed, cv::COLOR_BGR2RGB); // 3. 转换为浮点型并归一化例如除以255.0 processed.convertTo(processed, CV_32FC3, 1.0 / 255.0); // 4. 减去均值除以标准差如果模型训练时做了归一化 // cv::subtract(processed, cv::Scalar(mean_b, mean_g, mean_r), processed); // cv::divide(processed, cv::Scalar(std_b, std_g, std_r), processed); // 5. HWC 转 CHW (Height, Width, Channel - Channel, Height, Width) // OpenCV的Mat是HWC很多模型需要CHW cv::dnn::blobFromImage(processed, processed); // 这个函数可以直接完成HWC-CHW和维度扩展 return processed; // 现在形状是 [1, C, H, W] } std::vectorfloat InferenceEngine::infer(const cv::Mat input_image) { // 1. 预处理 cv::Mat processed_blob preprocess_image(input_image); // 2. 将数据绑定到输入张量 copy_data_to_input_tensor(processed_blob); // 3. 执行推理 ORT_CHECK(OrtRun(session_.get(), nullptr, io_binding_.get())); // 4. 获取输出数据 return extract_output_data(); } void InferenceEngine::copy_data_to_input_tensor(const cv::Mat processed_blob) { // 获取处理后的blob数据指针和大小 float* blob_data (float*)processed_blob.data; size_t blob_data_size processed_blob.total() * processed_blob.elemSize(); // 总字节数 // 创建OrtValue并绑定到IO绑定器 OrtValue* input_tensor nullptr; // 这里需要根据input_shape_创建OrtValue并将blob_data拷贝进去 // 具体代码涉及 OrtCreateTensorWithDataAsOrtValue 等API // ... // 将输入张量绑定到io_binding_ ORT_CHECK(OrtBindInput(io_binding_.get(), input_name_.c_str(), input_tensor)); // 注意需要管理input_tensor的生命周期推理完成后释放 } std::vectorfloat InferenceEngine::extract_output_data() { std::vectorfloat output_data; // 1. 从io_binding_中获取输出OrtValue OrtValue* output_tensor nullptr; ORT_CHECK(OrtGetOutput(io_binding_.get(), 0, output_tensor)); // 2. 获取输出张量信息和数据指针 void* raw_output_data nullptr; ORT_CHECK(OrtGetTensorMutableData(output_tensor, raw_output_data)); // 3. 获取输出张量的形状和元素总数 OrtTensorTypeAndShapeInfo* info nullptr; ORT_CHECK(OrtGetTensorTypeAndShape(output_tensor, info)); size_t num_dims; ORT_CHECK(OrtGetDimensionsCount(info, num_dims)); std::vectorint64_t output_shape(num_dims); ORT_CHECK(OrtGetDimensions(info, output_shape.data(), num_dims)); size_t num_elements 1; for (auto dim : output_shape) num_elements * dim; // 4. 将数据拷贝到std::vector中 float* float_output_data reinterpret_castfloat*(raw_output_data); output_data.assign(float_output_data, float_output_data num_elements); OrtReleaseTensorTypeAndShapeInfo(info); // 注意output_tensor由io_binding_管理通常不需要手动释放 return output_data; }4. 性能优化内存、显存与并发基础功能有了但“高性能”体现在细节。下面我们探讨几个关键优化点。4.1 内存与显存复用频繁申请释放内存/显存是性能杀手。我们的策略是一次分配多次使用。// 在InferenceEngine类中添加私有成员 class InferenceEngine { private: // ... std::vectorfloat input_buffer_; // 预分配的CPU输入缓冲区 OrtValue* persistent_input_tensor_ nullptr; // 持久化的输入OrtValue OrtValue* persistent_output_tensor_ nullptr; // 持久化的输出OrtValue // ... }; // 在init_onnx_runtime末尾根据input_shape_预分配input_buffer_ void InferenceEngine::init_onnx_runtime(...) { // ... 之前的初始化代码 input_tensor_size_ 1; for (auto dim : input_shape_) input_tensor_size_ * dim; input_buffer_.resize(input_tensor_size_); // 创建持久的输入OrtValue绑定到我们预分配的buffer OrtMemoryInfo* cpu_memory_info; OrtCreateCpuMemoryInfo(OrtDeviceAllocator, OrtMemTypeDefault, cpu_memory_info); OrtCreateTensorWithDataAsOrtValue( cpu_memory_info, input_buffer_.data(), input_tensor_size_ * sizeof(float), input_shape_.data(), input_shape_.size(), ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, persistent_input_tensor_ ); OrtReleaseMemoryInfo(cpu_memory_info); // 将持久化张量绑定到io_binding_ ORT_CHECK(OrtBindInput(io_binding_.get(), input_name_.c_str(), persistent_input_tensor_)); } // 修改copy_data_to_input_tensor不再创建新张量而是直接拷贝数据到预分配buffer void InferenceEngine::copy_data_to_input_tensor(const cv::Mat processed_blob) { // 直接内存拷贝 processed_blob.data - input_buffer_.data() std::memcpy(input_buffer_.data(), processed_blob.data, processed_blob.total() * processed_blob.elemSize()); // 因为persistent_input_tensor_已经绑定到input_buffer_数据已就绪 }4.2 多线程并发推理对于服务器场景我们需要处理多个并发请求。直接在多线程中共享一个OrtSession是不安全的除非后端明确支持。推荐两种模式模式一线程独享会话 (Thread-Local Session)每个线程创建自己的InferenceEngine实例。内存消耗大但无锁性能好。#include thread #include vector void process_requests_concurrently(const std::vectorcv::Mat images) { std::vectorstd::thread workers; std::vectorstd::futurestd::vectorfloat results; size_t num_threads std::thread::hardware_concurrency(); size_t images_per_thread images.size() / num_threads; for (size_t t 0; t num_threads; t) { workers.emplace_back([t, images_per_thread, images]() { // 每个线程创建自己的引擎实例 InferenceEngine engine(models/liujuan_model.onnx, CUDA:0); size_t start t * images_per_thread; size_t end (t num_threads - 1) ? images.size() : start images_per_thread; for (size_t i start; i end; i) { auto result engine.infer(images[i]); // 处理result... } }); } for (auto w : workers) w.join(); }模式二会话池 (Session Pool)预先创建一批会话放入队列工作线程从中取用。节省内存但需要简单的池管理。#include queue #include mutex #include condition_variable class SessionPool { public: SessionPool(size_t pool_size, const std::string model_path, const std::string device) { for(size_t i0; ipool_size; i) { pool_.push(std::make_uniqueInferenceEngine(model_path, device)); } } std::unique_ptrInferenceEngine acquire() { std::unique_lockstd::mutex lock(mutex_); cv_.wait(lock, [this]{ return !pool_.empty(); }); auto session std::move(pool_.front()); pool_.pop(); return session; } void release(std::unique_ptrInferenceEngine session) { std::lock_guardstd::mutex lock(mutex_); pool_.push(std::move(session)); cv_.notify_one(); } private: std::queuestd::unique_ptrInferenceEngine pool_; std::mutex mutex_; std::condition_variable cv_; };4.3 使用TensorRT加速可选如果你决定使用TensorRT集成流程类似但API不同。你需要先将ONNX模型转换为TensorRT引擎.plan或.engine文件然后使用TensorRT的C API加载和推理。TensorRT提供了更极致的层融合、精度校准和内核自动调优。由于篇幅限制这里给出一个概念性步骤模型转换使用trtexec工具或TensorRT Python API将.onnx转换为.engine。链接TensorRT库在CMake中查找并链接nvinfer和nvonnxparser。实现TensorRT推理类创建类似TensorRTInferenceEngine的类使用IRuntime,ICudaEngine,IExecutionContext等接口。绑定输入输出使用enqueueV2或executeV2进行异步或同步推理。TensorRT的代码更冗长但带来的性能提升尤其是在NVIDIA GPU上通常是显著的。5. 快速上手一个完整的示例最后我们写一个简单的main.cpp来验证整个流程。// main.cpp #include inference_engine.h #include iostream #include chrono int main() { try { std::string model_path models/liujuan_model.onnx; std::string device CUDA:0; // 或 CPU std::cout 正在初始化推理引擎... std::endl; InferenceEngine engine(model_path, device); std::cout 引擎初始化成功。输入尺寸: ; auto shape engine.get_input_shape(); for (auto dim : shape) std::cout dim ; std::cout std::endl; // 加载一张测试图片 cv::Mat image cv::imread(test_image.jpg); if (image.empty()) { std::cerr 无法加载测试图片 std::endl; return -1; } std::cout 开始推理... std::endl; auto start std::chrono::high_resolution_clock::now(); // 执行推理 std::vectorfloat results engine.infer(image); auto end std::chrono::high_resolution_clock::now(); auto duration std::chrono::duration_caststd::chrono::milliseconds(end - start); std::cout 推理完成耗时: duration.count() ms std::endl; std::cout 输出向量大小: results.size() std::endl; // 简单打印前10个结果假设是分类得分 std::cout 前10个得分: ; for (int i 0; i std::min(10, (int)results.size()); i) { std::cout results[i] ; } std::cout std::endl; } catch (const std::exception e) { std::cerr 程序运行出错: e.what() std::endl; return -1; } return 0; }编译并运行cd /path/to/LiuJuanZimage_CPP_Inference mkdir build cd build cmake .. make -j4 ./src/liujuan_inference6. 总结走完这一趟你应该已经拥有了一个为LiuJuan20260223Zimage模型量身定制的、高性能的C推理接口。我们从最基础的CMake环境配置开始一步步集成了ONNX Runtime封装了包含预处理、推理、后处理的完整引擎类并深入探讨了内存复用和多线程并发这两个核心的性能优化手段。整个过程下来最大的感受是C下的高性能推理确实需要多花一些心思在资源管理和细节控制上但带来的回报也是巨大的极致的速度、可控的内存以及部署的灵活性。尤其是当你把预分配内存和会话池这些技巧用上之后处理批量请求的吞吐量会有质的提升。当然这只是个起点。你可以在此基础上继续优化比如尝试集成TensorRT获得更极致的GPU性能或者实现更复杂的动态批处理Dynamic Batching来进一步提升吞吐。希望这个教程能成为你构建高效AI应用的一块坚实跳板。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。
LiuJuan20260223Zimage模型C++高性能推理接口封装教程
LiuJuan20260223Zimage模型C高性能推理接口封装教程如果你是一名C开发者正在寻找一种既能榨干硬件性能又能保持代码优雅的AI模型部署方案那么你来对地方了。今天我们不谈那些拖家带口的Python框架也不讲云端API调用的网络延迟就聚焦于一件事如何用纯正的C为LiuJuan20260223Zimage模型打造一个飞快的本地推理引擎。想象一下你需要在一个嵌入式设备上实时处理图像或者在一个高并发的服务器上批量处理成千上万的图片请求。Python的GIL锁和解释器开销可能会成为瓶颈。这时一个轻量、高效、内存可控的C推理接口就是你的终极武器。本教程将带你从零开始手把手构建这样一个高性能接口涵盖从环境搭建到并发优化的全流程。我们的目标很简单让推理速度起飞同时代码清晰可维护。1. 环境准备打造坚实的C开发地基在开始写代码之前我们需要一个稳定且高效的开发环境。这里我们选择CMake作为构建工具它跨平台且生态丰富是C项目的标准选择。1.1 基础工具链安装首先确保你的系统上安装了必要的编译器和工具。在Ubuntu系统上可以这样操作# 更新包列表并安装基础编译工具 sudo apt-get update sudo apt-get install -y build-essential cmake git # 安装CUDA Toolkit如果你的GPU支持并需要TensorRT加速 # 请根据你的CUDA版本从NVIDIA官网下载对应的runfile或deb包进行安装 # 例如安装CUDA 11.8 # wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run # sudo sh cuda_11.8.0_520.61.05_linux.run对于Windows用户建议安装Visual Studio 2019或更高版本包含MSVC编译器以及对应的CMake。macOS用户可以通过Homebrew安装brew install cmake。1.2 推理后端选择与安装接下来是关键一步选择推理后端。主流选择有两个ONNX Runtime 跨平台友好支持CPU和多种硬件加速CUDA TensorRT OpenVINO等部署灵活。TensorRT NVIDIA GPU上的极致性能优化器但生态相对封闭。本教程将以ONNX Runtime为主进行演示因为它更通用。如果你确定只在NVIDIA环境部署并追求极限性能可以平行参考TensorRT的流程。安装ONNX Runtime C库最推荐的方式是从源码编译以获得最适合你系统的版本。# 1. 克隆ONNX Runtime仓库 git clone --recursive https://github.com/microsoft/onnxruntime.git cd onnxruntime # 2. 配置编译选项。这里我们启用CUDA支持并只构建必要的库以减少体积。 # 创建一个构建目录并进入 mkdir build cd build # 运行CMake配置。请将/path/to/cuda替换为你的CUDA安装路径。 cmake .. \ -DCMAKE_BUILD_TYPERelease \ -Donnxruntime_BUILD_SHARED_LIBON \ -Donnxruntime_USE_CUDAON \ -Donnxruntime_CUDA_HOME/usr/local/cuda-11.8 \ -Donnxruntime_ENABLE_TRAININGOFF \ -Donnxruntime_BUILD_UNIT_TESTSOFF # 3. 开始编译使用-j参数指定并行编译的线程数加快速度 make -j$(nproc) # 编译完成后关键的库文件libonnxruntime.so在 ./lib 目录下 # 头文件在 ../include/onnxruntime/core/session/ 等目录中。编译完成后记下库文件和头文件的路径我们稍后在CMake中会用到。2. 项目骨架用CMake组织你的代码一个好的项目结构是成功的一半。我们来创建一个清晰的项目目录。LiuJuanZimage_CPP_Inference/ ├── CMakeLists.txt # 项目总构建文件 ├── src/ │ ├── CMakeLists.txt # 源代码构建文件 │ ├── inference_engine.cpp │ ├── inference_engine.h │ └── main.cpp # 示例主程序 ├── lib/ # 放置第三方库如ONNX Runtime ├── models/ # 放置转换好的.onnx模型文件 └── build/ # 构建输出目录由CMake生成现在我们来编写顶层的CMakeLists.txt。它的核心任务是找到依赖库并链接到我们的目标。# LiuJuanZimage_CPP_Inference/CMakeLists.txt cmake_minimum_required(VERSION 3.16) project(LiuJuanZimage_Inference VERSION 1.0.0 LANGUAGES CXX) # 设置C标准为17这是现代C项目的基础 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 设置编译模式为Release以获得最佳性能 if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() # 寻找必要的库 # 1. 查找OpenCV用于图像加载和预处理 find_package(OpenCV REQUIRED) message(STATUS Found OpenCV: ${OpenCV_INCLUDE_DIRS}) # 2. 指定ONNX Runtime的路径。 # 假设你将编译好的ONNX Runtime放在了项目根目录的 lib/onnxruntime-linux 下 set(ONNXRUNTIME_ROOT_DIR ${CMAKE_SOURCE_DIR}/lib/onnxruntime-linux) set(ONNXRUNTIME_INCLUDE_DIR ${ONNXRUNTIME_ROOT_DIR}/include) set(ONNXRUNTIME_LIB_DIR ${ONNXRUNTIME_ROOT_DIR}/lib) # 添加头文件搜索路径 include_directories(${OpenCV_INCLUDE_DIRS} ${ONNXRUNTIME_INCLUDE_DIR}) # 添加库文件搜索路径 link_directories(${ONNXRUNTIME_LIB_DIR} ${OpenCV_LIB_DIR}) # 添加子目录编译我们的源代码 add_subdirectory(src)然后是src/CMakeLists.txt它负责生成最终的可执行文件。# src/CMakeLists.txt # 将当前目录下的所有.cpp文件添加为源文件 file(GLOB_RECURSE SOURCES *.cpp) # 创建可执行目标 add_executable(liujuan_inference ${SOURCES}) # 链接库 # 注意链接库的名称需要根据ONNX Runtime编译出的实际文件名调整可能是 onnxruntime 或带后缀的版本 target_link_libraries(liujuan_inference ${OpenCV_LIBS} onnxruntime # 链接ONNX Runtime库 # 如果使用CUDA可能还需要链接cudart等CUDA运行时库 # cudart ) # 将模型目录复制到构建目录方便程序读取可选 configure_file(${CMAKE_SOURCE_DIR}/models/liujuan_model.onnx ${CMAKE_CURRENT_BINARY_DIR}/models/liujuan_model.onnx COPYONLY)3. 核心封装构建推理引擎类环境搭好了项目架子也立起来了现在进入核心环节编写推理引擎类。我们将遵循面向对象的设计原则创建一个InferenceEngine类它负责管理模型的生命周期、数据预处理和后处理。3.1 定义类接口首先在头文件inference_engine.h中定义类的接口。// inference_engine.h #ifndef INFERENCE_ENGINE_H #define INFERENCE_ENGINE_H #include string #include memory #include vector #include opencv2/opencv.hpp // 前向声明避免包含ONNX Runtime的头文件减少编译依赖 struct OrtSession; struct OrtMemoryInfo; struct OrtAllocator; struct OrtIoBinding; class InferenceEngine { public: // 构造函数传入模型路径和设备类型如CUDA:0或CPU explicit InferenceEngine(const std::string model_path, const std::string device CPU); ~InferenceEngine(); // 禁止拷贝构造和赋值确保资源管理安全 InferenceEngine(const InferenceEngine) delete; InferenceEngine operator(const InferenceEngine) delete; // 核心推理函数输入OpenCV Mat返回模型输出例如分类得分或检测框 std::vectorfloat infer(const cv::Mat input_image); // 获取模型期望的输入尺寸 (height, width, channels) std::vectorint64_t get_input_shape() const; private: // 初始化ONNX Runtime环境、会话和IO绑定 void init_onnx_runtime(const std::string model_path, const std::string device); // 图像预处理调整大小、归一化、HWC转CHW等 cv::Mat preprocess_image(const cv::Mat image) const; // 将预处理后的数据复制到模型输入张量 void copy_data_to_input_tensor(const cv::Mat processed_blob); // 从模型输出张量中提取数据 std::vectorfloat extract_output_data(); // ONNX Runtime相关资源 std::unique_ptrOrtSession, decltype(OrtReleaseSession) session_{nullptr, OrtReleaseSession}; std::unique_ptrOrtMemoryInfo, decltype(OrtReleaseMemoryInfo) memory_info_{nullptr, OrtReleaseMemoryInfo}; std::unique_ptrOrtIoBinding, decltype(OrtReleaseIoBinding) io_binding_{nullptr, OrtReleaseIoBinding}; // 其他必要的Ort环境、分配器等指针... // 模型信息 std::vectorint64_t input_shape_; std::string input_name_; std::string output_name_; size_t input_tensor_size_; // 输入张量总元素个数 }; #endif // INFERENCE_ENGINE_H3.2 实现推理引擎接下来在inference_engine.cpp中实现这些功能。这里会涉及一些ONNX Runtime的C API调用。// inference_engine.cpp #include inference_engine.h #include onnxruntime_c_api.h // ONNX Runtime C API #include onnxruntime_cxx_api.h // 也可以使用C API这里用C API演示更底层控制 #include stdexcept #include iostream // 辅助函数检查Ort状态 #define ORT_CHECK(expr) \ do { \ OrtStatus* status (expr); \ if (status ! nullptr) { \ const char* msg OrtGetErrorMessage(status); \ OrtReleaseStatus(status); \ throw std::runtime_error(std::string(ONNX Runtime error: ) msg); \ } \ } while(0) InferenceEngine::InferenceEngine(const std::string model_path, const std::string device) { init_onnx_runtime(model_path, device); } InferenceEngine::~InferenceEngine() { // 智能指针会自动释放资源 } void InferenceEngine::init_onnx_runtime(const std::string model_path, const std::string device) { // 1. 初始化全局环境单例通常一个进程一次 static OrtEnv* global_env nullptr; static std::once_flag init_flag; std::call_once(init_flag, [](){ ORT_CHECK(OrtCreateEnv(ORT_LOGGING_LEVEL_WARNING, LiuJuanInference, global_env)); }); // 2. 创建会话选项并设置执行设备 OrtSessionOptions* session_options nullptr; ORT_CHECK(OrtCreateSessionOptions(session_options)); // 设置优化级别和线程数 ORT_CHECK(OrtSetSessionExecutionMode(session_options, ORT_SEQUENTIAL)); ORT_CHECK(OrtSetIntraOpNumThreads(session_options, 1)); // 单线程执行算子 ORT_CHECK(OrtSetInterOpNumThreads(session_options, 1)); // 并行执行图的数量 // 设置设备 OrtSessionOptionsAppendExecutionProvider_CUDA(session_options, 0); // 如果使用CUDA // 对于CPU默认就是CPU无需额外设置 // 3. 创建会话加载模型 ORT_CHECK(OrtCreateSession(global_env, model_path.c_str(), session_options, session_)); // 4. 获取模型输入输出信息 size_t num_input_nodes; OrtTypeInfo* type_info nullptr; ORT_CHECK(OrtSessionGetInputCount(session_.get(), num_input_nodes)); // 假设只有一个输入 ORT_CHECK(OrtSessionGetInputName(session_.get(), 0, OrtGetAllocator(), input_name_)); ORT_CHECK(OrtSessionGetInputTypeInfo(session_.get(), 0, type_info)); // 从type_info中提取shape信息填充input_shape_ // ... (具体提取代码涉及OrtGetTensorTypeAndShape等API) OrtReleaseTypeInfo(type_info); // 5. 创建内存信息和IO绑定用于高性能推理 ORT_CHECK(OrtCreateCpuMemoryInfo(OrtDeviceAllocator, OrtMemTypeDefault, memory_info_)); ORT_CHECK(OrtCreateIoBinding(session_.get(), io_binding_)); OrtReleaseSessionOptions(session_options); } cv::Mat InferenceEngine::preprocess_image(const cv::Mat image) const { cv::Mat processed; // 1. 调整尺寸到模型输入大小 cv::resize(image, processed, cv::Size(input_shape_[3], input_shape_[2])); // 假设shape是[N,C,H,W] // 2. 转换颜色空间例如BGR转RGB如果模型需要 cv::cvtColor(processed, processed, cv::COLOR_BGR2RGB); // 3. 转换为浮点型并归一化例如除以255.0 processed.convertTo(processed, CV_32FC3, 1.0 / 255.0); // 4. 减去均值除以标准差如果模型训练时做了归一化 // cv::subtract(processed, cv::Scalar(mean_b, mean_g, mean_r), processed); // cv::divide(processed, cv::Scalar(std_b, std_g, std_r), processed); // 5. HWC 转 CHW (Height, Width, Channel - Channel, Height, Width) // OpenCV的Mat是HWC很多模型需要CHW cv::dnn::blobFromImage(processed, processed); // 这个函数可以直接完成HWC-CHW和维度扩展 return processed; // 现在形状是 [1, C, H, W] } std::vectorfloat InferenceEngine::infer(const cv::Mat input_image) { // 1. 预处理 cv::Mat processed_blob preprocess_image(input_image); // 2. 将数据绑定到输入张量 copy_data_to_input_tensor(processed_blob); // 3. 执行推理 ORT_CHECK(OrtRun(session_.get(), nullptr, io_binding_.get())); // 4. 获取输出数据 return extract_output_data(); } void InferenceEngine::copy_data_to_input_tensor(const cv::Mat processed_blob) { // 获取处理后的blob数据指针和大小 float* blob_data (float*)processed_blob.data; size_t blob_data_size processed_blob.total() * processed_blob.elemSize(); // 总字节数 // 创建OrtValue并绑定到IO绑定器 OrtValue* input_tensor nullptr; // 这里需要根据input_shape_创建OrtValue并将blob_data拷贝进去 // 具体代码涉及 OrtCreateTensorWithDataAsOrtValue 等API // ... // 将输入张量绑定到io_binding_ ORT_CHECK(OrtBindInput(io_binding_.get(), input_name_.c_str(), input_tensor)); // 注意需要管理input_tensor的生命周期推理完成后释放 } std::vectorfloat InferenceEngine::extract_output_data() { std::vectorfloat output_data; // 1. 从io_binding_中获取输出OrtValue OrtValue* output_tensor nullptr; ORT_CHECK(OrtGetOutput(io_binding_.get(), 0, output_tensor)); // 2. 获取输出张量信息和数据指针 void* raw_output_data nullptr; ORT_CHECK(OrtGetTensorMutableData(output_tensor, raw_output_data)); // 3. 获取输出张量的形状和元素总数 OrtTensorTypeAndShapeInfo* info nullptr; ORT_CHECK(OrtGetTensorTypeAndShape(output_tensor, info)); size_t num_dims; ORT_CHECK(OrtGetDimensionsCount(info, num_dims)); std::vectorint64_t output_shape(num_dims); ORT_CHECK(OrtGetDimensions(info, output_shape.data(), num_dims)); size_t num_elements 1; for (auto dim : output_shape) num_elements * dim; // 4. 将数据拷贝到std::vector中 float* float_output_data reinterpret_castfloat*(raw_output_data); output_data.assign(float_output_data, float_output_data num_elements); OrtReleaseTensorTypeAndShapeInfo(info); // 注意output_tensor由io_binding_管理通常不需要手动释放 return output_data; }4. 性能优化内存、显存与并发基础功能有了但“高性能”体现在细节。下面我们探讨几个关键优化点。4.1 内存与显存复用频繁申请释放内存/显存是性能杀手。我们的策略是一次分配多次使用。// 在InferenceEngine类中添加私有成员 class InferenceEngine { private: // ... std::vectorfloat input_buffer_; // 预分配的CPU输入缓冲区 OrtValue* persistent_input_tensor_ nullptr; // 持久化的输入OrtValue OrtValue* persistent_output_tensor_ nullptr; // 持久化的输出OrtValue // ... }; // 在init_onnx_runtime末尾根据input_shape_预分配input_buffer_ void InferenceEngine::init_onnx_runtime(...) { // ... 之前的初始化代码 input_tensor_size_ 1; for (auto dim : input_shape_) input_tensor_size_ * dim; input_buffer_.resize(input_tensor_size_); // 创建持久的输入OrtValue绑定到我们预分配的buffer OrtMemoryInfo* cpu_memory_info; OrtCreateCpuMemoryInfo(OrtDeviceAllocator, OrtMemTypeDefault, cpu_memory_info); OrtCreateTensorWithDataAsOrtValue( cpu_memory_info, input_buffer_.data(), input_tensor_size_ * sizeof(float), input_shape_.data(), input_shape_.size(), ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, persistent_input_tensor_ ); OrtReleaseMemoryInfo(cpu_memory_info); // 将持久化张量绑定到io_binding_ ORT_CHECK(OrtBindInput(io_binding_.get(), input_name_.c_str(), persistent_input_tensor_)); } // 修改copy_data_to_input_tensor不再创建新张量而是直接拷贝数据到预分配buffer void InferenceEngine::copy_data_to_input_tensor(const cv::Mat processed_blob) { // 直接内存拷贝 processed_blob.data - input_buffer_.data() std::memcpy(input_buffer_.data(), processed_blob.data, processed_blob.total() * processed_blob.elemSize()); // 因为persistent_input_tensor_已经绑定到input_buffer_数据已就绪 }4.2 多线程并发推理对于服务器场景我们需要处理多个并发请求。直接在多线程中共享一个OrtSession是不安全的除非后端明确支持。推荐两种模式模式一线程独享会话 (Thread-Local Session)每个线程创建自己的InferenceEngine实例。内存消耗大但无锁性能好。#include thread #include vector void process_requests_concurrently(const std::vectorcv::Mat images) { std::vectorstd::thread workers; std::vectorstd::futurestd::vectorfloat results; size_t num_threads std::thread::hardware_concurrency(); size_t images_per_thread images.size() / num_threads; for (size_t t 0; t num_threads; t) { workers.emplace_back([t, images_per_thread, images]() { // 每个线程创建自己的引擎实例 InferenceEngine engine(models/liujuan_model.onnx, CUDA:0); size_t start t * images_per_thread; size_t end (t num_threads - 1) ? images.size() : start images_per_thread; for (size_t i start; i end; i) { auto result engine.infer(images[i]); // 处理result... } }); } for (auto w : workers) w.join(); }模式二会话池 (Session Pool)预先创建一批会话放入队列工作线程从中取用。节省内存但需要简单的池管理。#include queue #include mutex #include condition_variable class SessionPool { public: SessionPool(size_t pool_size, const std::string model_path, const std::string device) { for(size_t i0; ipool_size; i) { pool_.push(std::make_uniqueInferenceEngine(model_path, device)); } } std::unique_ptrInferenceEngine acquire() { std::unique_lockstd::mutex lock(mutex_); cv_.wait(lock, [this]{ return !pool_.empty(); }); auto session std::move(pool_.front()); pool_.pop(); return session; } void release(std::unique_ptrInferenceEngine session) { std::lock_guardstd::mutex lock(mutex_); pool_.push(std::move(session)); cv_.notify_one(); } private: std::queuestd::unique_ptrInferenceEngine pool_; std::mutex mutex_; std::condition_variable cv_; };4.3 使用TensorRT加速可选如果你决定使用TensorRT集成流程类似但API不同。你需要先将ONNX模型转换为TensorRT引擎.plan或.engine文件然后使用TensorRT的C API加载和推理。TensorRT提供了更极致的层融合、精度校准和内核自动调优。由于篇幅限制这里给出一个概念性步骤模型转换使用trtexec工具或TensorRT Python API将.onnx转换为.engine。链接TensorRT库在CMake中查找并链接nvinfer和nvonnxparser。实现TensorRT推理类创建类似TensorRTInferenceEngine的类使用IRuntime,ICudaEngine,IExecutionContext等接口。绑定输入输出使用enqueueV2或executeV2进行异步或同步推理。TensorRT的代码更冗长但带来的性能提升尤其是在NVIDIA GPU上通常是显著的。5. 快速上手一个完整的示例最后我们写一个简单的main.cpp来验证整个流程。// main.cpp #include inference_engine.h #include iostream #include chrono int main() { try { std::string model_path models/liujuan_model.onnx; std::string device CUDA:0; // 或 CPU std::cout 正在初始化推理引擎... std::endl; InferenceEngine engine(model_path, device); std::cout 引擎初始化成功。输入尺寸: ; auto shape engine.get_input_shape(); for (auto dim : shape) std::cout dim ; std::cout std::endl; // 加载一张测试图片 cv::Mat image cv::imread(test_image.jpg); if (image.empty()) { std::cerr 无法加载测试图片 std::endl; return -1; } std::cout 开始推理... std::endl; auto start std::chrono::high_resolution_clock::now(); // 执行推理 std::vectorfloat results engine.infer(image); auto end std::chrono::high_resolution_clock::now(); auto duration std::chrono::duration_caststd::chrono::milliseconds(end - start); std::cout 推理完成耗时: duration.count() ms std::endl; std::cout 输出向量大小: results.size() std::endl; // 简单打印前10个结果假设是分类得分 std::cout 前10个得分: ; for (int i 0; i std::min(10, (int)results.size()); i) { std::cout results[i] ; } std::cout std::endl; } catch (const std::exception e) { std::cerr 程序运行出错: e.what() std::endl; return -1; } return 0; }编译并运行cd /path/to/LiuJuanZimage_CPP_Inference mkdir build cd build cmake .. make -j4 ./src/liujuan_inference6. 总结走完这一趟你应该已经拥有了一个为LiuJuan20260223Zimage模型量身定制的、高性能的C推理接口。我们从最基础的CMake环境配置开始一步步集成了ONNX Runtime封装了包含预处理、推理、后处理的完整引擎类并深入探讨了内存复用和多线程并发这两个核心的性能优化手段。整个过程下来最大的感受是C下的高性能推理确实需要多花一些心思在资源管理和细节控制上但带来的回报也是巨大的极致的速度、可控的内存以及部署的灵活性。尤其是当你把预分配内存和会话池这些技巧用上之后处理批量请求的吞吐量会有质的提升。当然这只是个起点。你可以在此基础上继续优化比如尝试集成TensorRT获得更极致的GPU性能或者实现更复杂的动态批处理Dynamic Batching来进一步提升吞吐。希望这个教程能成为你构建高效AI应用的一块坚实跳板。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。