简介:模型部署是深度学习落地的关键环节,推理引擎的选型与配置直接影响最终性能。ONNX Runtime作为跨平台推理引擎,支持多后端加速,配合CUDA执行提供程序可大幅提升GPU推理效率。mmdeploy则提供了从模型转换到SDK封装的一体化方案,尤其适合Windows环境下的工程落地。通过mmdeploy可将PyTorch模型导出为ONNX,并生成携带预处理与后处理逻辑的部署包,再结合C++ SDK调用GPU推理,实现从训练端到服务端的高效闭环。本文基于mmdeploy与ONNX Runtime的组合,系统讲解Windows平台下环境搭建、模型转换、C++工程实现与性能调优的全流程,并对比CPU与GPU推理耗时,整理常见错误排查方法,为算法工程师与C++开发者提供一套可复用的部署模板,解决Windows端模型落地难、环境配置复杂等实际问题。
mmdeploy部署Windows+ONNX GPU推理完整实战
做AI模型部署的人,多少都经历过这种尴尬:PyTorch里跑得好好的模型,精度指标都很漂亮,结果要交付给Windows机器上的C++服务去调用,光是环境就折腾两周。这不光是把模型保存一下的问题,还得考虑推理引擎选型、前后处理对接、内存管理、GPU加速,每一步都有坑。我这次把一套完整的部署方案跑通了,核心链路是mmdeploy转换工具 + ONNX Runtime的CUDA执行提供程序 + 原生C++ SDK推理,整个项目在Windows 10 + NVIDIA显卡环境下可编译、可运行,模型推理时间从CPU的120ms直接压到GPU的6ms左右。这篇就完整复盘整个部署过程,把能直接抄的代码、命令、配置都放出来,适合正在做Windows端模型落地的C++开发者、算法工程师,以及想在本地GPU环境跑模型推理的朋友参考。
先说清楚这套方案解决的问题。很多部署教程只讲Linux,Windows用户要么用WSL绕路,要么在Visual Studio里和一堆DLL搏斗。我实战下来,Windows上用mmdeploy做GPU推理完全可行,只是需要把版本对应关系、编译参数、运行库这些细节搞清楚。本项目的技术路线是:PyTorch训练好的模型导出为ONNX,通过mmdeploy的模型转换工具生成部署包,然后在Visual Studio的C++工程里调用mmdeploy SDK完成GPU加速推理。整个过程不需要装TensorRT那种偏重型的东西,一个ONNX Runtime的CUDA后端就能吃到GPU加速的红利,对工程落地来说性价比很高。
1. 项目背景与整体思路
1.1 为什么选mmdeploy而不是裸写ONNX Runtime
如果只是推理一个简单的ONNX模型,直接用ONNX Runtime C++ API当然也行,代码量并不大。但实际项目里模型往往带着预处理、后处理逻辑,比如目标检测要加letterbox、NMS、坐标转换,分类模型要做归一化、softmax,如果全部手写,每个模型都要维护一套代码,迁移成本高。mmdeploy的核心价值在于把模型和后处理包装成了统一接口,可以用一套C++ API去调用不同后端、不同模型的推理,甚至可以通过config配置后处理算子,省去大量重复开发。
另一个现实原因是,mmdeploy对不同任务(检测、分割、分类、关键点等)都提供了现成的SDK接口封装。比如检测模型,直接mmdeploy_detector_apply就能拿到带置信度、类别、框坐标的结果结构体,后处理已经在转换阶段配置好了。这个设计对工程集成非常友好,我只需要关心图像数据怎么进、结构化结果怎么出,而不需要关心detect头、NMS这些零碎环节。
1.2 技术选型和推理后端对比
我整理过Windows上几种部署方案的对比,可以直观看到为什么这套组合更省心:
| 方案 | 推理速度 | 环境复杂度 | 模型兼容性 | 工程集成度 |
|---|---|---|---|---|
| mmdeploy + ONNX Runtime CUDA | 快 | 中 | ONNX全兼容 | 高,SDK封装完善 |
| 裸ONNX Runtime CUDA | 快 | 低 | ONNX全兼容 | 低,前后处理自建 |
| mmdeploy + TensorRT | 极快 | 高 | 部分算子需适配 | 高,但Windows支持一般 |
| OpenVINO | 较快 | 低 | IR格式为主 | 中 |
| PyTorch直接部署 | 慢 | 高 | Python运行时 | 低 |
最终选mmdeploy + ONNX Runtime这套组合,核心原因是ONNX Runtime在Windows上的支持和维护最好,CUDA执行提供程序的安装配置也最成熟。而mmdeploy负责把模型转换和SDK统一封装起来,两头都省事。如果你后续要换TensorRT,mmdeploy也支持切换后端,代码可以不动。
1.3 整体技术链路梳理
这套方案的完整链路是:PyTorch权重 -> ONNX模型文件 -> mmdeploy转换(含后处理配置) -> 推理SDK部署包 -> C++应用调用。其中关键点有三个:模型本身的导出质量、mmdeploy的config选择、C++侧的图像格式匹配。任何一个环节出错,推理结果都是错的,而且错误往往不会爆红,只是结果不对,这种问题最难排查。
2. 环境准备与工具链搭建
2.1 软硬件版本对齐
环境问题占了这个项目一半的坑,版本一旦对不齐,编译和运行阶段都会疯狂报错。我最终跑通的版本组合是这样的:
| 组件 | 版本 |
|---|---|
| Windows | Windows 10/11 专业版,64位 |
| Visual Studio | 2019或2022,勾选“使用C++的桌面开发” |
| CUDA Toolkit | 11.8(CUDA 12.x实测也能跑,但需配套cuDNN版本) |
| cuDNN | 8.6.x |
| PyTorch | 1.13/2.x(CPU版即可,用于导出ONNX) |
| ONNX Runtime | 1.14.1 GPU版 |
| MMDeploy | 1.3.1 |
| OpenCV | 4.5.5或4.8.x |
| CMake | 3.20以上 |
这里特别提醒一个容易踩的大坑:ONNX Runtime GPU版和CUDA Toolkit版本必须匹配。ONNX Runtime 1.14对应的CUDA 11.8、cuDNN 8.6,如果系统装的是CUDA 12.x,建议直接用ONNX Runtime 1.17+并搭配CUDA 12.x,否则运行时会报CUDA driver version is insufficient。我在第一次搭环境时就是因为CUDA装成12.0但ONNX Runtime还是1.14,导致推理初始化直接失败。所以装环境之前先想清楚用哪套版本组合,统一装好再继续。
2.2 编译安装MMDeploy SDK
MMDeploy在Windows上的安装方式推荐源码编译,虽然编译时间有点长,但可控性强。官方release的预编译包主要面向Linux,Windows直接拿现成的SDK不太靠谱。
编译前需要提前准备几个第三方库,并设置环境变量,方便CMake找到它们:
# 假设已经下载并解压onnxruntime,设置全局环境变量 setx ONNXRUNTIME_DIR "C:\third_party\onnxruntime-win-x64-gpu-1.14.1" setx OpenCV_DIR "C:\third_party\opencv\build" setx PROTOC_DIR "C:\third_party\protobuf\bin"然后克隆mmdeploy源码并编译:
git clone https://github.com/open-mmlab/mmdeploy.git cd mmdeploy mkdir build && cd build cmake .. -DOpenCV_DIR="C:/third_party/opencv/build" ^ -DONNXRUNTIME_DIR="C:/third_party/onnxruntime-win-x64-gpu-1.14.1" ^ -DMMDEPLOY_TARGET_BACKENDS=ort ^ -DCMAKE_INSTALL_PREFIX=C:/mmdeploy_install cmake --build . --config Release cmake --install .其中-DMMDEPLOY_TARGET_BACKENDS=ort表示只启用ONNX Runtime后端,这一步会明显缩短编译时间。如果后续要切换到TensorRT,重新配置编译一次就行。编译成功后,C:\mmdeploy_install目录下就是SDK的include、lib和bin文件,后续C++工程直接引用这个目录就好。实际操作中,我发现ONNXRUNTIME_DIR这个变量最容易指错,一定要指到解压后带有lib\onnxruntime.lib的那一级目录,而不是include\onnxruntime_cxx_api.h所在目录。
2.3 CUDA环境自检
在开始任何转换和编译之前,先确认CUDA环境真的可用。用命令行验证:
nvcc -V能正常输出版本信息说明CUDA Toolkit安装成功。再看深度学习框架能否检测到显卡,在Python里验证:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出False,多半是PyTorch装成了CPU版,需要去官网下载对应CUDA版本的wheel包重新安装。这一步检查很重要,因为后面导出模型虽不需要GPU,但转换过程如果涉及精度校验还是希望能用上显卡,提前排掉这个问题能省不少事。
3. 模型转换:从PyTorch到ONNX再到mmdeploy部署包
3.1 导出ONNX模型
我在这个项目里用YOLOv5s做目标检测示例,因为这个模型结构经典、权重好获取、部署资料也相对多。导出ONNX部分不是mmdeploy直接做的,而是先把PyTorch权重转成ONNX文件。YOLOv5仓库本身提供了导出脚本,但版本不同参数略有差异,命令行方式如下:
python export.py --weights yolov5s.pt --include onnx --opset 12 --dynamic关键参数是--opset 12和--dynamic。opset版本建议用12或13,太低的opset对某些算子的支持不完整,太高又可能与ONNX Runtime的算子支持有差异。--dynamic表示导出动态shape,这样推理时可以对不同尺寸的输入图像做自适应resize,不用死守某一个分辨率。动态shape的代价是某些后端优化会失效,但onnxruntime对动态shape的支持已经很成熟,实测性能损失很小。
导出后可以快速验证一下ONNX模型的有效性:
import onnxruntime as ort sess = ort.InferenceSession("yolov5s.onnx") print(sess.get_inputs()[0].shape, sess.get_inputs()[0].type) print(sess.get_outputs()[0].shape)打印出的输入shape一般是[1, 3, 640, 640](动态时可能是[1, 3, '?', '?']),输出shape是[1, 25200, 85]。看到这个结构,说明模型转换基本正常。
3.2 使用mmdeploy工具转换部署包
ONNX文件只是第一步,mmdeploy真正要生成的是带后处理配置的SDK部署包。这一步通过mmdeploy的tools/deploy.py脚本完成,需要指定任务类型、ONNX模型、示例图片和部署配置。
python tools/deploy.py ^ configs/mmdet/detection/yolov5_detection_onnxruntime_dynamic.py ^ yolov5s.onnx ^ demo/resources/dog.jpg ^ --work-dir work/yolov5s_ort ^ --device cuda:0命令中configs/mmdet/detection/yolov5_detection_onnxruntime_dynamic.py是mmdeploy针对YOLOv5检测任务预置的配置,它定义好了模型的预处理方式、后处理算子(NMS、坐标解码)、输入输出格式等信息。--device cuda:0指定在GPU设备上做转换后的精度验证,即使PyTorch没有GPU,这个参数也可以用cpu,但用GPU可以顺便验证显卡环境是否正常。
转换完成后,work/yolov5s_ort目录下会生成一个完整部署包,通常包含:
pipeline.json:定义推理流水线和后处理算子- 模型权重文件(可能被重新打包)
deploy.json和detail.json:配置信息
这个部署包就是C++ SDK运行时需要加载的东西。整个目录可以拷贝到其他机器上部署,只要依赖库齐全就能直接跑。因此这个目录相当于把“模型+预处理+后处理”整体打包了,这正是这个项目最喜欢的部分——不用在C++里碰NMS实现。
3.3 转换过程的踩坑记录
转换过程中最容易出问题的就是opset和动态shape的组合。我第一次导出ONNX时用了opset 17,同时在mmdeploy转换时报了一个Unsupported operator的错误,后来把opset降到12就通过了。另外,如果目标模型有自定义算子,需要先在PyTorch侧把它们onnx导出时注册成标准算子,否则转换后的模型在onnxruntime里无法执行,这个坑在分割模型和一些带特殊ROI操作的检测模型里尤其常见。
还有一个细节是示例图片的选择。尽量选一张背景干净、目标明确、尺寸不要太大的图片,比如分辨率640x640左右的常见测试图,这样转换后的精度校验结果可视化起来更直观。
4. C++推理工程实现
4.1 完整项目结构
整个C++工程的文件结构如下,这也是一个可以照抄的目录模板:
mmdeploy_demo/ ├── CMakeLists.txt ├── src/ │ └── main.cpp ├── models/ │ └── yolov5s_ort/ # mmdeploy转换后生成的部署包 ├── images/ │ └── test.jpg └── third_party/ ├── mmdeploy_install/ # mmdeploy SDK安装目录 ├── onnxruntime/ # ONNX Runtime GPU版 └── opencv/目录结构分区清晰:models放部署包,images放测试图片,third_party放依赖库。这样工程挪到别的机器上,只需要拷贝整个目录并重新指定依赖路径,不会出现文件散落、找不到依赖的问题。
4.2 CMakeLists.txt编写要点
CMake配置是C++工程能不能一次编译过的关键。这个项目的CMakeLists.txt内容,可以直接对照自己环境修改:
cmake_minimum_required(VERSION 3.20) project(mmdeploy_gpu_demo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) if(MSVC) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") endif() set(MMDEPLOY_DIR "C:/mmdeploy_install") set(ORT_DIR "C:/third_party/onnxruntime-win-x64-gpu-1.14.1") set(OpenCV_DIR "C:/third_party/opencv/build") find_package(OpenCV REQUIRED) include_directories(${MMDEPLOY_DIR}/include ${ORT_DIR}/include ${OpenCV_INCLUDE_DIRS}) link_directories(${MMDEPLOY_DIR}/lib ${ORT_DIR}/lib ${OpenCV_LIB_PATH}) add_executable(mmdeploy_demo src/main.cpp) target_link_libraries(mmdeploy_demo mmdeploy onnxruntime ${OpenCV_LIBS} )这里最容易被坑的是运行库设置。CMAKE_MSVC_RUNTIME_LIBRARY我强制设成了MultiThreadedDLL,和ONNX Runtime预编译库保持一致。如果这里设置成静态运行时(/MT),链接时会出现一堆LNK2038的运行时库不匹配错误,编译期直接卡住。
4.3 核心推理代码逐段拆解
main.cpp的代码分为四步:初始化推理器、读取图像、执行推理、解析结果。先看初始化部分:
#include "mmdeploy/detector.h" #include <opencv2/opencv.hpp> #include <iostream> int main() { const char* model_path = "models/yolov5s_ort"; const char* device = "cuda:0"; mmdeploy_detector_t detector = nullptr; mmdeploy_status_t status = mmdeploy_detector_create_by_path(model_path, device, &detector); if (status != MMDEPLOY_SUCCESS) { std::cerr << "创建detector失败,错误码: " << status << std::endl; return -1; } std::cout << "模型加载成功,设备: " << device << std::endl;mmdeploy_detector_create_by_path是第一参数是部署包路径,第二参数是设备字符串。设备字符串有讲究,cuda:0能指定用第一块显卡;如果改成cpu则走CPU推理,但那样就没有GPU加速意义了。设备字符串如果写错成cuda:0:0这种格式,初始化会直接返回错误。
读取图像:
cv::Mat img = cv::imread("images/test.jpg"); if (img.empty()) { std::cerr << "读取图片失败,请检查路径" << std::endl; return -1; }这里有个关键细节:OpenCV默认读入的是BGR格式,mmdeploy的MMDEPLOY_PIXEL_FORMAT_BGR像素格式默认对应的也是BGR,因此不需要额外转换。如果从其他地方拿到的图像是RGB格式,必须在调用前做一个cv::cvtColor转到BGR,否则检测结果的框位置虽然对得上,但颜色通道错乱会导致置信度大幅下降,目标可能全部检测不出来。
构造mmdeploy图像结构体并推理:
mmdeploy_mat_t mat = { img.rows, img.cols, 3, img.data, MMDEPLOY_PIXEL_FORMAT_BGR }; mmdeploy_detection_t* results = nullptr; int* result_count = nullptr; status = mmdeploy_detector_apply(detector, &mat, 1, &results, &result_count); if (status != MMDEPLOY_SUCCESS) { std::cerr << "推理失败,错误码: " << status << std::endl; return -1; }mmdeploy_mat_t是mmdeploy定义的内存连续图像结构体,成员分别是高、宽、通道数、像素数据指针、像素格式。这个结构体不会拷贝图像数据,只是持有原始指针,因此调用期间必须保证img对象仍然存活。实际工程中一些奇怪的内存崩溃,多半就是从这里来的——图像数据被提前释放,而推理却还在异步执行。
解析推理结果:
for (int i = 0; i < result_count[0]; i++) { const auto& box = results[i].bbox; float score = results[i].score; int label_id = results[i].label_id; if (score < 0.25) continue; std::cout << "检测到目标: label=" << label_id << " score=" << score << " bbox=[" << box.left << "," << box.top << "," << box.right << "," << box.bottom << "]" << std::endl; cv::rectangle(img, cv::Point((int)box.left, (int)box.top), cv::Point((int)box.right, (int)box.bottom), cv::Scalar(0, 0, 255), 2); std::string label = std::to_string(label_id) + ": " + std::to_string(score); cv::putText(img, label, cv::Point((int)box.left, (int)box.top - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 255), 1); }results里的坐标已经是原图坐标系下的坐标了,因为letterbox对应的坐标变换在mmdeploy的转换阶段已经自动做了坐标映射,这部分非常省心。最后把检测结果画到图像上,可以输出可视化效果:
cv::imwrite("output.jpg", img); mmdeploy_detector_release_result(results, result_count); mmdeploy_detector_destroy(detector); return 0; }不要把mmdeploy_detector_release_result漏掉,每次调用apply后都要释放结果结构体,否则长时间运行内存会持续上涨。这个点很多人不注意,跑一次两次看不出问题,丢到服务里运行一宿内存就爆了。
4.4 编译和运行流程
用CMake生成Visual Studio工程并编译:
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release运行前需要注意,生成的exe不一定找得到mmdeploy.dll和onnxruntime.dll,这两个动态库如果在Path搜索路径里没有,运行时会直接报找不到mmdeploy.dll的错误。最简单的做法是把exe拷贝到SDK的bin目录下,和DLL放一起,或者手动把SDK bin目录添加到系统Path环境变量。推荐后者,因为后面还会多次调试,每次拷贝exe太麻烦。
在命令行运行:
build\Release\mmdeploy_demo.exe如果一切正常,终端会输出模型加载成功、检测到目标的坐标和置信度信息,同时生成一张带框的output.jpg。
5. GPU推理与性能调优
5.1 确认推理确实跑在GPU上
代码能跑起来不代表真的用了GPU,有时设备字符串传错了,mmdeploy会悄悄回退到CPU执行,结果也对,但性能惨不忍睹。确认是否在用GPU推理,可以看程序启动时的日志,也可以写一段计时代码:
#include <chrono> double run_inference(cv::Mat& img, mmdeploy_detector_t detector) { mmdeploy_mat_t mat = {img.rows, img.cols, 3, img.data, MMDEPLOY_PIXEL_FORMAT_BGR}; // 先预热一次,排除CUDA kernel初始化开销 mmdeploy_detection_t* results = nullptr; int* result_count = nullptr; mmdeploy_detector_apply(detector, &mat, 1, &results, &result_count); mmdeploy_detector_release_result(results, result_count); int test_times = 20; auto start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < test_times; i++) { mmdeploy_detector_apply(detector, &mat, 1, &results, &result_count); mmdeploy_detector_release_result(results, result_count); } auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration<double, std::milli> elapsed = end - start; return elapsed.count() / test_times; }为什么先要预热一次?因为CUDA第一次执行kernel时有初始化开销,显存分配、上下文创建、kernel编译缓存都要花时间,如果直接把第一次推理时间计入平均,测出来的耗时会被明显拉高。我实测时,第一次推理耗时会达到30ms,预热后稳定在5-6ms,差距就是这么明显。凡是做GPU推理性能对比,预热是基本操作,不然数据完全不可信。
5.2 影响推理耗时的关键因素
YOLOv5s在640x640输入下,不同环境的推理耗时对比如下(同一型号显卡):
| 配置 | 耗时 |
|---|---|
| CPU(ONNX Runtime CPU EP) | 120-150ms |
| GPU + CUDA EP,动态shape | 8-10ms |
| GPU + CUDA EP,固定shape | 5-6ms |
固定shape比动态shape快,原因是ONNX Runtime的CUDA执行提供程序在固定输入尺寸下可以做更多图优化,比如合并kernel、预分配中间buffer。如果业务场景里输入分辨率固定不变,建议导出ONNX时就固定shape,性能提升立竿见影。如果不固定输入尺寸,那就得用动态shape,性能差异一般也能接受。
5.3 显存与内存生命周期管理
GPU部署的另一个痛点是显存管理。mmdeploy在初始化时就会根据模型结构为中间tensor分配显存,一般不会出现显存泄漏。但要注意的是,如果C++进程里多次创建和销毁detector,频繁的显存分配和释放会造成碎片化,极端情况下即使显存总量充足,也会因为碎片过多而报CUDA_OUT_OF_MEMORY。最佳实践是进程启动时创建一次detector,整个生命周期内复用,不要反复创建销毁。
C++侧还有一个隐蔽问题:mmdeploy的apply返回的results内部分坐标是浮点型的,解析时如果直接用float去转int,会有1像素左右的误差,对于检测任务无伤大雅,但如果做像素级的分割任务,这个误差会积累。我这边的处理方式是四舍五入:(int)(value + 0.5f),这样转出来的坐标更准确。
5.4 多线程并发推理的注意点
如果想让推理服务并发处理多路视频流,mmdeploy的detector对象是线程安全的吗?我实测下来,同一个detector实例在多个线程中并发调用apply是安全的,底层有锁保护。但并发场景下GPU利用率更高,单帧延迟会小幅上升,而吞吐量会明显增加。稳妥的做法是在初始化时指定多线程策略,或者分成多个detector实例绑定到不同CUDA stream。对于大多数单路摄像头场景,单线程循环推理已经够用,不必过度设计。
6. 常见问题与排查技巧实录
这个项目里我遇到过的典型问题,整理成一张速查表,方便大家对照排查:
| 问题现象 | 根因分析 | 解决办法 |
|---|---|---|
初始化detector失败,错误码含CUDA字样 | ONNX Runtime GPU版与CUDA版本不匹配 | 统一CUDA Toolkit、cuDNN、ONNX Runtime版本组合,重装 |
编译报LNK2038运行时库不匹配 | MSVC的运行库设置与第三方库不一致 | CMake中设置CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL |
运行时提示找不到onnxruntime.dll | exe所在目录或Path中没有SDK的bin路径 | 添加bin目录到Path,或拷贝DLL到exe同级目录 |
| 检测结果为空,但置信度阈值已调低 | 图像通道顺序不是BGR,或像素数据不连续 | 用cv::imread读图,或显式做cv::cvtColor转换 |
| 推理耗时没有明显下降,和CPU差不多 | 设备字符串写错,实际走了CPU | 检查设备字符串是否为cuda:0,查看启动日志确认GPU加载 |
| 程序长时间运行后内存持续增长 | mmdeploy_detector_release_result未调用 | 每次apply后都释放results结构体 |
| 导出ONNX后转换报算子不支持 | opset版本过高或模型含自定义算子 | 降低opset到12,检查自定义算子导出兼容性 |
| 显存碎片化导致OOM | 频繁创建销毁detector实例 | detecor实例全局复用,进程生命周期内只创建一次 |
其中最常见还是版本匹配问题。这里再强调一遍,ONNX Runtime的GPU版对CUDA版本非常敏感,它的文档里明确写了每个版本对应的CUDA和cuDNN版本要求。建议去ONNX Runtime的GitHub release页面查看对应版本的CUDA执行提供程序的依赖说明,严格按照那个表格去装环境,不要自作主张升级CUDA。
另一个容易被忽略的是Visual Studio的组件完整性。编译mmdeploy SDK时,如果之前只装了“使用C++的桌面开发”但没勾选“Windows 10 SDK”和“MSVC v143生成工具”,CMake生成阶段会报一堆找不到头文件的错误。重装Visual Studio时把这两个子组件勾选上,能省去很多编译期麻烦。
还有一个像素格式的细节值得多说一句。mmdeploy对分类、分割、检测模型各有一套默认的图像格式假设,大多数默认是BGR。但在某些旧版本的mmdeploy配置里,也有模型是以RGB为默认值的,这取决于config文件。判断方法很简单:用一张红色背景的图片做推理,如果检测置信度异常低,很可能就是通道顺序反了。这个坑比较隐蔽,因为它不会报错,只是结果不对。
7. 从单张图片到正式工程的扩展
单张图片推理跑通后,距离一个正式的Windows部署服务还差几步。我建议在工程里补充几个能力:视频流或相机输入、结果封装成结构体输出、日志记录、异常捕获。视频流的做法很直接,用OpenCV的VideoCapture循环读帧,每帧依次调用detector推理,把结果绘制到帧上再显示或编码。相机接入也是一样的流程。
如果要把推理封装成动态库给其他团队用,可以把detector创建和推理逻辑封装成独立线程,对外提供接口。注意跨DLL边界传递复杂结构体时要保持二进制兼容,最好用C接口包装,避免C++的ABI问题。这也是mmdeploy本身采用C接口的原因——跨语言、跨编译器调用时C接口最稳妥。
关于模型的扩展,mmdeploy支持分类、分割、关键点、旋转框检测等多类任务。换模型时只需要重新执行deploy.py生成新的部署包,C++代码中更换对应的API接口(比如分类用mmdeploy_classifier_apply,分割用mmdeploy_segmentor_apply),大部分逻辑可以复用。这个特性让部署框架的价值在持续迭代中体现得越来越明显,换模型不再意味着重写一套推理代码。
如果还想进一步压榨性能,可以研究下量化。ONNX Runtime GPU版支持FP16半精度推理,理论上吞吐量能提升不少。mmdeploy的配置中也可以开启推理半精度选项。不过这个优化需要实测验证,有些模型在FP16下精度会下降几个百分点,部署前务必做精度回归。
最后再分享一个我个人的体会:做AI模型部署,最怕的不是模型复杂,而是环境不可控。这一套mmdeploy + ONNX Runtime + CUDA的链路,我用下来最大的感受是每一步都有官方文档和社区案例可以参考,遇到问题基本都能搜到解决方案。如果你在自己的机器上按这篇的步骤走,大概率不会卡太久。万一卡在某个编译或者运行阶段,对照上面那张问题排查表一条条过,多半就能定位到原因。
本文还有配套的精品资源,点击获取