098、NCNN/RKNN/OpenVINO 三平台部署对比:从模型转换到 C++ API 推理
098、NCNN/RKNN/OpenVINO 三平台部署对比从模型转换到 C加加 API 推理上周五凌晨两点我在调试一个RK3588上的YOLOv8-seg模型发现分割掩码输出全是0。折腾了三个小时最后发现是RKNN的量化校准集里忘了加背景类样本——这种低级错误说出来都丢人。但正是这种血泪教训让我决定把NCNN、RKNN、OpenVINO这三个平台的部署经验系统整理一下。今天这篇笔记就当是给自己挖坑填坑的记录。模型转换三个平台三种脾气先说NCNN。这玩意儿对PyTorch的ONNX导出要求最苛刻。我习惯在export.py里加这么一段# 这里踩过坑NCNN不支持动态batch必须固定torch.onnx.export(model,dummy_input,model.onnx,opset_version11,# 别用12以上NCNN解析会炸input_names[images],output_names[output],dynamic_axesNone# 必须None否则NCNN转的时候报shape不匹配)转NCNN时有个坑ncnn2int8工具对量化校准集的数量有硬性要求少于100张图直接报错。我一般准备200-300张覆盖各种光照和角度。校准集图片尺寸必须和模型输入一致别想着让工具自动resize——它只会粗暴地拉伸导致精度崩盘。RKNN的转换相对友好但有个隐藏雷区量化模式选择。RKNN支持asymmetric_quantized-u8和dynamic_fixed_point-i8两种。实测YOLOv8用前者在RK3588上掉点0.5-1个mAP后者几乎不掉点。但后者对激活值范围敏感需要在rknn.config里手动设置quantized_dtypedynamic_fixed_point-i8同时把quantized_algorithm设为normal而不是mmse——别问我为什么RK的文档里写的是反的。OpenVINO的转换最省心mo.py一把梭。但有个细节如果模型里有torch.chunk或torch.split操作OpenVINO的IR中间表示会把它拆成多个Slice节点推理时多出30%的延迟。解决办法是在导出ONNX前手动用torch.unbind替代或者干脆在C后处理里做切片。推理引擎初始化内存和时间的博弈NCNN的Net类初始化我习惯这样写ncnn::Net net;net.opt.use_vulkan_computefalse;// 别开移动端兼容性差net.opt.use_bf16_storagetrue;// 省内存精度几乎无损net.load_param(model.param);net.load_model(model.bin);注意load_param和load_model的顺序不能反否则会报param not loaded。这个错误信息极其误导人我第一次遇到时以为是模型文件损坏重下了三遍。RKNN的初始化有个坑rknn_init的第二个参数flags如果传0默认用CPU推理。要启用NPU必须传RKNN_FLAG_NPU_CORE_0或RKNN_FLAG_NPU_CORE_0_1。但别贪心全开RK3588的NPU三核全开会发热降频实测双核性能最优。rknn_context ctx;intretrknn_init(ctx,model_data,model_size,RKNN_FLAG_NPU_CORE_0_1,nullptr);// 这里踩过坑model_data必须保持有效直到rknn_destroy不能提前释放OpenVINO的Core对象是线程安全的但InferRequest不是。多线程推理时每个线程必须创建自己的InferRequest共享Core没问题。我见过有人把InferRequest当全局变量用结果推理结果错乱排查了两天。前处理数据排布的暗坑NCNN要求输入数据是RGB顺序但很多摄像头输出是BGR。别在C里手动转换用ncnn::Mat::from_pixels_resize的PIXEL_RGB2BGR标志位它内部用NEON优化比手写循环快5倍。ncnn::Mat inncnn::Mat::from_pixels_resize(bgr_data,ncnn::Mat::PIXEL_BGR2RGB,src_w,src_h,target_w,target_h);// 别这样写手动循环像素转换慢且容易越界RKNN的输入要求更诡异它期望的数据排布是NHWC而PyTorch模型默认是NCHW。虽然RKNN转换工具会自动插入transpose但推理时如果输入数据是NCHW格式会多一次内存重排。我习惯在C里直接构造NHWC的输入rknn_input inputs[1];inputs[0].index0;inputs[0].typeRKNN_TENSOR_UINT8;inputs[0].sizetarget_h*target_w*3;inputs[0].fmtRKNN_TENSOR_NHWC;inputs[0].bufimage_data;// 已经是HWC排布OpenVINO的输入是float32类型需要做归一化。但别在循环里逐像素除255用cv::Mat::convertTo转成float后再调用cv::divide做批量除法利用SIMD加速。推理与后处理性能瓶颈在这里NCNN的Extractor对象每次推理都要创建别复用。我见过有人为了省内存重复使用同一个Extractor结果第二次推理时输出张量指针指向了错误地址。ncnn::Extractor exnet.create_extractor();ex.input(images,in);ncnn::Mat out;ex.extract(output,out);// 每次推理都重新create别复用RKNN的rknn_run是同步的但可以配合rknn_query查询NPU占用率。如果发现占用率超过80%说明模型太大或batch太大需要降频或切分。OpenVINO的异步推理接口start_async和wait配合使用能实现流水线。但注意wait的超时时间别设太长我一般设100ms超时后主动丢弃当前帧避免累积延迟。后处理是性能大头。NCNN的输出是ncnn::Mat访问元素用row指针float*ptrout.row(i);// 别用atfloat(i, j)慢10倍RKNN的输出是void*需要强转成float*。但注意RKNN的输出数据排布是NCHW而YOLO的检测头期望的是CHW需要手动做一次维度重排。这个操作我放在NPU上做用RKNN的rknn_set_io_mem指定自定义内存避免CPU-GPU数据拷贝。OpenVINO的输出是InferRequest::get_output_tensor返回的是float*可以直接用std::memcpy拷贝到自定义结构体。但别在每次推理时都get_output_tensor这个调用有锁开销在构造函数里获取一次指针后续复用。三平台性能对比数字会说话在RK3588上同样的YOLOv8n模型NCNN用CPU推理耗时45msRKNN用NPU双核推理耗时12msOpenVINO用GPU推理耗时18ms。但NCNN的CPU推理在树莓派4B上反而比OpenVINO快因为OpenVINO的GPU驱动在ARM上优化不到位。内存占用方面NCNN最小模型加载后约80MBRKNN次之约120MB包含NPU驱动OpenVINO最吃内存约200MB主要是IR中间表示的解析开销。精度方面三个平台在FP16模式下几乎无差异INT8量化后RKNN掉点最少0.3-0.5 mAPNCNN次之0.5-0.8 mAPOpenVINO的INT8量化掉点最严重1-2 mAP除非用它的--data_type FP16配合--scale手动校准。个人经验选平台不如选工具链如果你问我哪个平台最好我会说看你的目标硬件。NCNN适合ARM Linux和移动端RKNN适合瑞芯微芯片OpenVINO适合Intel平台。但真正决定开发效率的是工具链的调试能力。NCNN的ncnnoptimize工具能可视化模型结构方便排查算子支持问题。RKNN的rknn_toolkit提供Python API可以在PC上模拟推理但模拟结果和真机有差异别全信。OpenVINO的benchmark_app能自动测延迟和吞吐但它的-d CPU和-d GPU结果差异巨大别拿CPU的延迟去估算GPU性能。最后说个血泪教训无论用哪个平台一定要在模型转换后做一次端到端的精度验证。我写了个脚本用同一张图在PyTorch和部署平台上分别推理对比输出张量的余弦相似度。低于0.99的直接打回重转。这个习惯救了我无数次尤其是在RKNN量化时校准集选不好相似度能掉到0.8以下。部署不是终点是另一个起点。每个平台都有自己的脾气摸透了它们就是你手里的工具。摸不透它们就是你加班的理由。