C++部署YOLOv8:ONNX Runtime与OpenCV在低配设备上的推理实践

📅 发布时间:2026/9/3 11:19:45
C++部署YOLOv8:ONNX Runtime与OpenCV在低配设备上的推理实践
简介本资源是一套面向Linux开发者与边缘部署工程师的YOLOv8 C推理实战方案专为Ubuntu平台低配置设备如工控机、嵌入式终端优化设计解决Python部署开销大、实时性差、依赖繁杂等痛点。资源共363个文件涵盖180个hpp头文件、69个h系统头文件、42张JPG/BMP测试图像、2个ONNX模型文件、3个核心CPP源码及配套config.txt等配置文件辅以lib目录中onnxruntime、OpenCV等关键动态库含libonnxruntime.so.1.12.1等整体压缩包120.57MB结构清晰bin/src/include/models等模块划分明确。目前已有159人学习下载。用户可直接编译运行快速完成ONNX模型加载、图像预处理、推理执行、边界框解析与NMS后处理全流程配套说明文档详述环境变量设置、库路径配置及type.names类别映射规则显著降低C部署门槛并提供多模型配置管理config0.txt与阈值灵活调节能力。1. 项目概述在低配机器上跑通YOLOv8推理最近在折腾一个挺有意思的事儿手头有几台配置不高的旧机器比如一台老款的笔记本显卡还是GTX 1660 Ti甚至还有一台只用CPU的迷你主机。想在这些设备上跑一下最新的YOLOv8模型看看目标检测的效果。直接用官方的PyTorch版本或者Ultralytics的库对资源要求不低尤其是在没有独立显卡或者显存很小的机器上体验很卡顿。于是我就琢磨着能不能用C配合ONNX Runtime和OpenCV这些轻量级的库把训练好的YOLOv8模型导出为ONNX格式部署起来。目标很明确第一要脱离沉重的Python深度学习框架依赖让程序本身更轻第二要充分利用ONNX Runtime的跨平台和性能优化特别是在CPU和低端GPU上的推理能力第三整个流程要清晰从模型加载、预处理、推理到后处理全部用C实现最终封装成一个可以方便调用的模块或者可执行文件。这个思路对于嵌入式设备、边缘计算盒子或者只是想低成本体验深度学习应用的开发者来说应该挺有吸引力的。毕竟不是每个人都有RTX 4090但学习的热情和动手的欲望是一样的。接下来我就把自己从环境搭建、代码编写到最终跑通的全过程包括踩过的坑和总结的经验详细分享一下。2. 环境准备与核心工具链选型要在Ubuntu下用C搞深度学习推理第一步就是把“厨房”收拾好。这里的选择直接决定了后续开发的顺畅度和最终程序的性能。2.1 操作系统与编译器我选择的是Ubuntu 22.04 LTS。LTS版本长期支持社区资源丰富遇到问题容易找到解决方案。系统安装过程就不赘述了无论是物理机、VMware虚拟机还是WSL2都可以。不过这里有个小建议如果你用WSL2并且希望使用GPU进行推理需要安装WSL2的GPU驱动支持步骤会稍微多一步。对于纯粹CPU推理WSL2非常方便。编译器自然是GCC/G。Ubuntu 22.04 默认的版本通常是g-11就完全够用。确保安装开发工具包sudo apt update sudo apt install build-essential cmakebuild-essential包含了gcc, g, make等核心工具。cmake是现代C项目管理的标配我们后面编译第三方库全靠它。2.2 核心库ONNX Runtime与OpenCV这是整个项目的两大支柱。ONNX Runtime微软开源的跨平台推理引擎。它负责加载我们导出的.onnx模型文件并在指定的硬件后端CPU、CUDA、TensorRT等上高效执行推理。我们不需要关心模型内部复杂的计算图只需要喂给它输入数据它就能给出输出。为什么选它性能优异针对不同硬件有深度优化CPU推理可以用MKL-DNN或OpenMPGPU支持CUDA和TensorRT。接口统一C API稳定一套代码稍作修改就能切换推理设备。生态成熟作为ONNX模型的“官方”运行时之一兼容性好更新活跃。安装方式我推荐从源码编译虽然耗时但能获得最适合自己系统的优化版本也方便调试。可以从GitHub仓库下载源码编译时指定--config Release、--build_shared_lib生成动态库以及像--use_cuda如果你有N卡并安装了CUDA这样的参数。OpenCV计算机视觉的“瑞士军刀”。在我们的流程里它主要负责图像处理部分读取图片、颜色空间转换BGR到RGB、尺寸缩放、归一化以及最后将推理得到的检测框和类别画到原图上。为什么必不可少图像I/O与处理imread,resize,cvtColor等函数极其高效且稳定。矩阵运算OpenCV的Mat对象是处理图像数据的天然容器与ONNX Runtime的输入输出Tensor可以方便地进行数据转换。可视化rectangle,putText函数能轻松完成结果标注。同样建议从源码编译OpenCV4.x版本开启WITH_OPENMP以支持多线程能加速一些预处理操作。2.3 辅助工具VSCode与CMakeVSCode轻量级但功能强大的代码编辑器。通过安装C/C扩展、CMake Tools扩展可以获得接近IDE的体验包括代码补全、跳转定义、编译和调试。它的配置文件如c_cpp_properties.json,tasks.json,launch.json可以很好地管理包含路径和库路径特别是当我们自编译了多个第三方库时。CMake项目构建的核心。写一个清晰的CMakeLists.txt文件能自动找到本机安装的OpenCV和ONNX Runtime链接正确的库文件。这对于项目在不同机器间的移植至关重要。一个基本的CMakeLists需要包含cmake_minimum_required(VERSION 3.16) project(YOLOv8_CPP_Inference) set(CMAKE_CXX_STANDARD 17) find_package(OpenCV REQUIRED) # 假设ONNX Runtime头文件和库文件放在自定义目录 include_directories(/path/to/onnxruntime/include) link_directories(/path/to/onnxruntime/lib) add_executable(yolov8_inference main.cpp preprocess.cpp postprocess.cpp) target_link_libraries(yolov8_inference ${OpenCV_LIBS} onnxruntime)注意ONNX Runtime的find_package支持可能不完善所以经常需要手动指定include_directories和link_directories。务必链接正确的库文件例如在CPU版本下是onnxruntime在GPU版本下可能是onnxruntime_providers_cuda等。3. YOLOv8 ONNX模型解析与预处理拿到一个从Ultralytics YOLOv8导出的.onnx文件后别急着跑。先把它“解剖”一下搞清楚它的输入输出格式这是正确调用它的前提。3.1 模型输入输出探秘使用Netron一个可视化的神经网络模型查看工具打开你的ONNX文件。你会发现YOLOv8的输入输出和YOLOv5等前代有所不同。输入节点名称通常是images或input0。形状[1, 3, 640, 640]。这表示模型期望的输入是批量大小 (Batch Size) 1一次推理一张图通道数 (Channels) 3RGB三通道高度 (Height) 640宽度 (Width) 640数据类型float32。输出节点YOLOv8的检测头是“解耦”的它的输出不再是[1, 25200, 85]这样的格式。你可能会看到两个输出一个形状为[1, 84, 8400]的输出。这是核心。1批大小。84每个预测框的属性数量。对于COCO数据集80类它是4框坐标 80类别概率 84。8400预测框的总数。这来自于模型三个不同尺度特征图80x80, 40x40, 20x20的锚点总和80*80 40*40 20*20 8400。可能还有一个额外的输出但主要信息在第一个里。理解这个[1, 84, 8400]的形状是后续后处理的关键。它意味着模型直接输出了8400个候选框每个框有84个数值。3.2 图像预处理流程C实现预处理的目标是把一张任意尺寸的图片转换成符合模型输入要求的1x3x640x640的float32数组。步骤拆解读取与颜色转换用OpenCV的imread读取图片得到BGR格式的Mat。YOLOv8训练时通常使用RGB格式所以需要cvtColor(img, img, cv::COLOR_BGR2RGB)。保持宽高比的缩放LetterBox这是关键一步。简单粗暴地resize到640x640会导致图像变形。YOLO系列常用的方法是LetterBox将图像等比缩放直到最长边等于640短边不足的部分用灰色如114填充从而保持物体比例不变。cv::Mat letterbox(const cv::Mat src, int target_width, int target_height) { int src_w src.cols; int src_h src.rows; float scale std::min((float)target_width / src_w, (float)target_height / src_h); int new_w int(src_w * scale); int new_h int(src_h * scale); cv::Mat resized; cv::resize(src, resized, cv::Size(new_w, new_h)); cv::Mat dst cv::Mat::zeros(target_height, target_width, src.type()); // 填充值通常为114 dst.setTo(cv::Scalar(114, 114, 114)); // 将缩放后的图像拷贝到目标图像中央 resized.copyTo(dst(cv::Rect((target_width - new_w) / 2, (target_height - new_h) / 2, new_w, new_h))); return dst; }同时需要记录下缩放比例scale和填充的偏移量(dx, dy)用于后续将推理出的框坐标映射回原图。归一化与通道转换将像素值从[0, 255]归一化到[0, 1]/255.0。有些模型可能还需要进一步的标准化减均值除标准差但YOLOv8官方导出的一般只需要除255。然后OpenCV的Mat是HxWxC行x列x通道的内存布局而ONNX模型需要CxHxW。我们需要做一次转置。转换为连续数组将处理好的Mat数据拷贝到一个一维的float数组中准备喂给模型。实操心得预处理的所有参数尺寸640、归一化方式、填充色必须与模型训练和导出时的设置严格一致。最好查看原训练代码或导出脚本确认。LetterBox的填充色不一定是114但114是YOLOv5/v8常用的默认值。保持一致即可。数据从Mat到float数组的拷贝可以用指针遍历也可以使用Mat.ptrT()和memcpy注意内存对齐和效率。4. ONNX Runtime推理引擎的集成与调用预处理准备好了数据现在轮到ONNX Runtime登场执行核心的神经网络前向传播。4.1 初始化推理会话首先需要创建一个Ort::Session推理会话它是模型在内存中的代表。#include onnxruntime/core/session/onnxruntime_cxx_api.h Ort::Env env(ORT_LOGGING_LEVEL_WARNING, YOLOv8Inference); Ort::SessionOptions session_options; // 配置会话选项 session_options.SetIntraOpNumThreads(4); // 设置并行线程数根据CPU核心数调整 session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); // 指定执行提供商Execution Provider, EP // 情况1使用CPU推理最通用 // Ort::Session session(env, yolov8n.onnx, session_options); // 情况2如果有NVIDIA GPU和CUDA使用CUDA EP加速 OrtCUDAProviderOptions cuda_options; cuda_options.device_id 0; // 使用第0块GPU session_options.AppendExecutionProvider_CUDA(cuda_options); // 创建会话 Ort::Session session(env, path/to/your_model.onnx, session_options);关键点在于Execution Provider的选择。对于低配机器CPU默认选项。可以通过SetIntraOpNumThreads和SetInterOpNumThreads来利用多核。在旧CPU上这是唯一选择。CUDA如果你有哪怕是一块像GTX 1660 Ti这样的“老将”CUDA EP也能带来巨大的速度提升。记得在编译ONNX Runtime时开启--use_cuda。TensorRT如果模型转换成了TensorRT引擎可以获得极致优化但流程更复杂。CoreML (macOS), OpenVINO (Intel)等针对特定硬件平台的优化。4.2 准备输入与获取输出创建好会话后需要按照模型输入输出的名字和形状来准备数据。// 1. 获取模型输入输出信息 Ort::AllocatorWithDefaultOptions allocator; auto input_name session.GetInputNameAllocated(0, allocator); auto output_name session.GetOutputNameAllocated(0, allocator); // 注意GetInputNameAllocated是较新API如果编译报错可能是版本问题可尝试GetInputName // 2. 定义输入输出Tensor的形状 std::vectorint64_t input_shape {1, 3, 640, 640}; std::vectorconst char* input_names {input_name.get()}; std::vectorconst char* output_names {output_name.get()}; // 3. 准备输入数据假设preprocessed_data是预处理得到的float数组 std::vectorfloat input_tensor_values(preprocessed_data, preprocessed_data 1*3*640*640); Ort::MemoryInfo memory_info Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat(memory_info, input_tensor_values.data(), input_tensor_values.size(), input_shape.data(), input_shape.size()); // 4. 运行推理 auto output_tensors session.Run(Ort::RunOptions{nullptr}, input_names.data(), input_tensor, 1, output_names.data(), 1); // 5. 解析输出 Ort::Value output_tensor output_tensors.front(); float* output_data output_tensor.GetTensorMutableDatafloat(); auto output_shape output_tensor.GetTensorTypeAndShapeInfo().GetShape(); // output_shape 应该等于 [1, 84, 8400]踩坑记录ONNX Runtime的C API在不同版本间可能有细微变动特别是内存管理和名称获取部分。我用的版本是1.16左右。如果遇到GetInputName相关错误请务必查阅你所使用版本的官方文档或示例代码。一个稳妥的方法是使用session.GetInputNameAllocated如果可用它自动管理内存。4.3 性能调优小技巧在低配机器上每一分性能都值得争取会话选项SetIntraOpNumThreads设置为CPU物理核心数非超线程数通常效果较好。对于简单模型也可以尝试关闭并行SetIntraOpNumThreads(1)有时开销更小。内存复用如果进行视频流连续推理可以复用input_tensor_values和Ort::Value对象避免反复分配内存。预热在正式推理前先用一张小图或随机数据跑几次session.Run让运行时完成初始化、图优化等操作后续推理会更稳定。批处理虽然我们这里批大小是1但ONNX Runtime支持批处理。如果你的应用场景需要同时处理多张图且机器内存允许使用更大的批大小如4, 8可以更充分地利用GPU并行计算能力提升吞吐量。需要修改输入形状和预处理逻辑。5. 推理结果的后处理与解析从ONNX Runtime拿到[1, 84, 8400]的输出数组后这才是“万里长征第一步”。我们需要从这8400个候选框中筛选出那些真正有物体的、位置准确的框。5.1 解码与筛选流程后处理可以分解为以下几个步骤我将其封装在一个postprocess函数中遍历所有候选框循环i从 0 到 8399每个框对应输出数据中第i列共8400列每列有84个值。前4个值[cx, cy, w, h]是框的中心点坐标和宽高但它们是相对于640x640输入网格的需要转换。后面的80个值是类别概率。计算置信度对每个框从80个类别概率中找出最大值max_class_score及其对应的类别IDclass_id。这个最大值代表了模型认为该框包含此类物体的置信度。应用置信度阈值设定一个阈值如confidence_threshold 0.25。如果max_class_score confidence_threshold直接丢弃这个框。这一步可以过滤掉绝大部分可能超过95%的无效预测。解码框坐标模型输出的cx, cy是相对于该框所在网格左上角的偏移量经过sigmoid激活。w, h是相对于锚点anchor宽高的缩放值经过指数运算。但YOLOv8的官方导出模型通常已经做了一些简化输出可能已经是归一化到[0,1]的坐标或者是直接可用的值。这里需要根据你导出模型时的具体参数来定。一个常见的做法是假设输出已经是相对于640x640的归一化坐标。计算框的左上角和右下角坐标float x_center output_data[i * 84 0]; // 假设已是归一化坐标 float y_center output_data[i * 84 1]; float width output_data[i * 84 2]; float height output_data[i * 84 3]; float x1 (x_center - width / 2.0f); float y1 (y_center - height / 2.0f); float x2 (x_center width / 2.0f); float y2 (y_center height / 2.0f);映射回原图尺寸将上述在640x640坐标系下的坐标(x1, y1, x2, y2)根据之前LetterBox预处理时记录的缩放比例scale和填充偏移(dx, dy)映射回原始图像的坐标系。// scale 640 / max(原图高原图宽) // dx, dy 是填充在两侧/上下的像素数 x1 (x1 * 640 - dx) / scale; y1 (y1 * 640 - dy) / scale; x2 (x2 * 640 - dx) / scale; y2 (y2 * 640 - dy) / scale; // 确保坐标不超出原图边界 x1 std::max(0.0f, std::min(x1, (float)src_img.cols)); y1 std::max(0.0f, std::min(y1, (float)src_img.rows)); ... // 对x2, y2做同样处理收集有效检测框将经过阈值筛选、坐标解码和映射后的框包含坐标、置信度、类别ID存入一个临时向量。5.2 非极大值抑制经过置信度筛选后可能还会有多个框检测到同一个物体。NMS的作用就是去除这些冗余框。#include algorithm #include vector struct Detection { cv::Rect box; float conf; int class_id; }; void nms(std::vectorDetection detections, float nms_threshold) { if (detections.empty()) return; // 按置信度从高到低排序 std::sort(detections.begin(), detections.end(), [](const Detection a, const Detection b) { return a.conf b.conf; }); std::vectorDetection keep; std::vectorbool suppressed(detections.size(), false); for (size_t i 0; i detections.size(); i) { if (suppressed[i]) continue; keep.push_back(detections[i]); for (size_t j i 1; j detections.size(); j) { if (suppressed[j]) continue; // 计算IoU交并比 cv::Rect intersection detections[i].box detections[j].box; float inter_area intersection.area(); float union_area detections[i].box.area() detections[j].box.area() - inter_area; float iou inter_area / union_area; // 如果IoU超过阈值抑制置信度较低的框 if (iou nms_threshold) { suppressed[j] true; } } } detections std::move(keep); }调用nms(detections, 0.45);即可。经过NMSdetections向量里剩下的就是最终、最精简的检测结果了。后处理心得阈值调参confidence_threshold和nms_threshold需要根据实际场景微调。阈值太高会漏检太低则杂框多、计算慢。通常从0.25和0.45开始尝试。性能瓶颈在CPU上后处理尤其是NMS可能比模型推理本身更耗时特别是当置信度阈值设得较低产生大量候选框时。优化方法包括使用更快的NMS实现如带索引排序的、尝试将部分后处理移到GPU上较复杂、或者适当提高置信度阈值。坐标映射这是最容易出错的一步。务必用一张简单的测试图打印出预处理前后的坐标并可视化检查映射是否正确。一个框在缩放填充后的图像上看起来位置正确映射回原图后可能就偏了。6. 工程化封装与性能优化实战把各个模块跑通后我们需要把它变成一个整洁、可复用、高效的项目。6.1 类设计与接口封装我设计了一个YOLOv8Infer类将整个流程封装起来class YOLOv8Infer { public: YOLOv8Infer(const std::string model_path, const std::string class_names_file, bool use_gpu false, int intra_op_threads 4); ~YOLOv8Infer(); bool init(); // 初始化模型加载类别名 std::vectorDetection infer(const cv::Mat src_img); // 执行推理 void draw_results(cv::Mat img, const std::vectorDetection detections); // 绘制结果 private: cv::Mat preprocess(const cv::Mat src, float scale, int dx, int dy); std::vectorDetection postprocess(const std::vectorfloat output_data, float scale, int dx, int dy, float conf_thresh, float nms_thresh); Ort::Env env_; Ort::Session session_{nullptr}; std::vectorstd::string input_names_; std::vectorstd::string output_names_; std::vectorstd::string class_names_; // ... 其他成员变量如图像尺寸、阈值等 };这样在主函数中使用就非常清晰了YOLOv8Infer inferer(yolov8n.onnx, coco.names); if (inferer.init()) { cv::Mat img cv::imread(test.jpg); auto results inferer.infer(img); inferer.draw_results(img, results); cv::imwrite(result.jpg, img); }6.2 多线程与流水线优化对于视频流或批量图片处理单线程顺序执行“读图-预处理-推理-后处理-显示”效率低下。可以采用生产者-消费者模型线程A生产者负责读取视频帧或图片放入一个预处理队列。线程B预处理从队列取图进行LetterBox等预处理放入推理队列。线程C推理从推理队列取预处理后的数据调用ONNX Runtime进行推理结果放入后处理队列。线程D后处理与渲染进行NMS、坐标映射并绘制结果或保存。使用std::queue配合std::mutex和std::condition_variable可以实现简单的线程安全队列。这样当线程C在进行GPU推理计算密集型时线程A和B可以并行地准备下一帧的数据充分利用系统资源。6.3 针对低配置机器的特别优化模型选择优先使用YOLOv8的纳米n或小s型号。yolov8n.onnx文件只有几MB对内存和计算压力小得多在CPU上也能达到较快的帧率。输入分辨率不一定非要640x640。如果场景中物体较大可以尝试更小的输入尺寸如320x320能显著降低计算量。但需要重新导出模型或确认模型支持动态输入。量化如果性能仍不满足可以考虑模型量化。ONNX Runtime支持将FP32模型量化为INT8推理速度能提升2-4倍精度损失通常很小。可以使用ONNX Runtime的量化工具但这需要一部分校准数据流程稍复杂。CPU推理优化在编译ONNX Runtime时启用--use_openmp和--use_mkldnn针对Intel CPU或--use_dnnl。在代码中尝试不同的线程数设置SetIntraOpNumThreads并非越多越好有时设置为核心数的一半性能最佳。确保你的程序以高性能模式运行避免被操作系统节能策略限制。7. 常见问题排查与解决实录在实际部署过程中我遇到了不少问题这里把典型的几个列出来供大家参考。7.1 编译与链接问题问题1找不到onnxruntime库或头文件。表现编译时报错fatal error: onnxruntime_cxx_api.h: No such file or directory或链接时报错undefined reference to Ort::xxx。排查检查CMakeLists.txt中include_directories和link_directories的路径是否正确指向了ONNX Runtime的安装位置。确认链接的库文件名是否正确。在Linux下动态库文件通常是libonnxruntime.so。使用target_link_libraries(your_target onnxruntime)。如果ONNX Runtime是自己编译的确保编译时指定了--build_shared_lib以生成动态库。解决最稳妥的方法是使用find_library和find_path或者将ONNX Runtime的lib和include目录加入到系统的环境变量LD_LIBRARY_PATH和CPLUS_INCLUDE_PATH中。问题2OpenCV版本冲突或找不到。表现find_package(OpenCV REQUIRED)失败或链接时出现大量未定义符号。排查系统可能安装了多个版本的OpenCV如通过apt安装的opencv和手动编译的opencv4。解决在CMake中指定版本和路径find_package(OpenCV 4 REQUIRED PATHS /your/custom/opencv/build)。或者卸载不需要的版本确保系统只有一个主要的OpenCV。7.2 运行时推理错误问题3模型输入输出形状不匹配。表现session.Run时抛出异常提示输入输出形状或类型错误。排查用Netron再次确认模型的输入输出名称和形状。在代码中打印出你准备的input_tensor的形状和数据类型与模型期望的进行对比。检查预处理最后一步从Mat到float数组的数据排布HWC转CHW和数值范围归一化是否正确。解决严格按照模型要求准备数据。一个常见的错误是忘记做BGR到RGB的转换或者归一化参数不对。问题4推理结果全为零或毫无意义。表现程序能跑通但检测不到任何物体或者框的位置完全错误。排查预处理不一致这是最大的嫌疑。确认LetterBox的填充色、缩放算法、归一化方式/255.0是否与训练时完全一致。可以打印出预处理后数组的几个值与用Python脚本处理同一张图片的结果对比。后处理解码错误确认从[1, 84, 8400]数组中解析坐标和置信度的逻辑是否正确。特别是坐标映射回原图的公式。模型问题确认导出的ONNX模型本身是正确的。可以用ONNX Runtime的Python API跑一下同一张图片看结果是否正常。解决建议编写一个简单的Python脚本使用ONNX Runtime Python包对同一张图片进行推理并将预处理后的数据、推理原始输出、后处理结果都保存下来。然后在C程序中在关键节点如预处理后、推理后、后处理后将数据与Python脚本的结果进行逐元素对比这是定位问题最有效的方法。7.3 性能相关问题问题5CPU推理速度非常慢帧率只有个位数。排查使用top或htop命令查看进程的CPU占用率。如果不到100%可能没有充分利用多核。检查ONNX Runtime会话的线程设置SetIntraOpNumThreads。使用性能分析工具如perf找出热点函数。很可能时间花在了后处理的NMS上。解决尝试调整SetIntraOpNumThreads和SetInterOpNumThreads。优化后处理代码比如使用更高效的循环或者尝试提高置信度阈值以减少进入NMS的框数量。考虑换用更小的模型如YOLOv8n。问题6GPU推理没有加速效果甚至比CPU还慢。排查确认CUDA和cuDNN已正确安装并且ONNX Runtime是支持CUDA的版本。在代码中检查是否成功创建了CUDA执行提供商的会话。可以添加日志。使用nvidia-smi命令查看GPU是否被调用以及利用率如何。如果利用率很低可能是数据在CPU和GPU之间拷贝的开销太大或者批处理大小太小无法掩盖启动开销。解决确保使用session_options.AppendExecutionProvider_CUDA(cuda_options)。对于低端GPU尝试增大推理的批处理大小如果支持以提升GPU利用率。对于非常小的模型GPU加速可能不明显因为内核启动和数据传输的固定开销占比太高。整个项目从环境搭建到最终跑通是一个典型的深度学习模型C部署流程。它剥离了Python训练框架的厚重得到了一个轻量、高效、可独立分发的推理程序。这对于资源受限的边缘设备、需要高并发的服务器端应用或者仅仅是希望深入理解模型推理每一个细节的开发者来说价值巨大。最关键的是通过亲手实现一遍你对YOLO的后处理、ONNX Runtime的工作机制、以及C下的高性能计算会有更深刻的认识这是单纯调包所无法比拟的。本文还有配套的精品资源点击获取