把云端换到边缘时踩过的坑
直接转 RKNN 会失败,中间需要经过 ONNX 中转。
“——这是我在把 YOLOv8 部署到 RK3588 开发板上时脑子里冒出来的判断。
为什么这次要做边缘部署
项目背景很简单:工厂流水线上的缺陷检测,需要实时判断产品有没有问题。原来的方案是把摄像头拍到的图片传到云端处理,延迟差不多 300ms,但现场发现两个问题:
- 网络偶尔会抖动,导致实时性要求不满足
- 摄像头密集,带宽压力很大
所以决定把推理移到本地。硬件选型最后定了 RK3588,原因很实际:8GB 内存、6TOPS NPU、价格可接受、有现成的 RKNN SDK。
准备工作:从 PyTorch 到 RKNN
云端训练用的框架是 PyTorch,模型是 YOLOv8n。直接转 RKNN 会失败,中间需要经过 ONNX 中转。
# 导出 ONNX
python export.py --weights yolov8n.pt --include onnx --opset 12
# 转换为 RKNN
rknn convert --target rk3588 --model yolov8n.onnx --output yolov8n.rknn \
--rknn_batch_size 1 --platform onnx \
--quantized_dtype asymmetric_affine-u8 \
--optimization_level 3
第一次转的时候报错了,错误信息是 Unsupported op: NonMaxSuppression。YOLOv8 的后处理部分包含 NMS,RKNN SDK 不直接支持,需要手动移到 CPU 上做。
解决方法很简单:修改模型导出脚本,把 NMS 后处理从模型里拆出来:
# 导出时去掉后处理
model = YOLO('yolov8n.pt')
model.export(format='onnx', opset=12, simplify=True)
# 推理时手动做 NMS
def postprocess(outputs, conf_threshold=0.5, iou_threshold=0.45):
# 手动实现 NMS 逻辑
...
这里有一个判断:把后处理放到 CPU 上做,会增加一点延迟,但换来模型能成功转换和部署。在这个场景下是可以接受的。
模型量化:精度和速度的平衡
RK3588 的 NPU 只支持 INT8,所以必须做量化。默认使用非对称量化(asymmetric_affine-u8),但第一次推理出来的结果,准确率掉了 5 个百分点。
排查后发现是量化数据集的问题。RKNN 量化需要一个代表性的数据集来计算缩放因子,我一开始随便用了 50 张图片,但这 50 张和实际场景差距太大。
# 准备量化数据集
dataset = 'path/to/real_scene_images/'
rknn.config(mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]],
quantized_dtype='asymmetric_affine-u8',
optimization_level=3)
rknn.build(do_quantization=True, dataset=dataset)
换了 500 张实际场景的图片重新量化,准确率只掉了 0.5 个百分点,这个结果可以接受。
量化这个过程有一个判断:不是所有场景都适合 INT8。如果模型本身对精度敏感,或者场景本身样本很少,可能需要保留 FP16 混合精度,或者考虑别的方法。这次是缺陷检测,模型本身比较鲁棒,所以纯 INT8 没问题。
部署流程:从 RKNN 到 C++ 推理
RKNN SDK 提供 C 和 Python 两种接口。Python 方便快速验证,C 性能更好。现场最终用的是 C++,因为还要和系统其他模块集成。
#include <rknn_api.h>
// 初始化 RKNN 上下文
rknn_context ctx;
int ret = rknn_init(&ctx, model_data, model_size, 0, NULL);
// 设置输入输出
rknn_input_output_attr io_attr;
rknn_query(ctx, RKNN_QUERY_IN_OUT_ATTR, &io_attr, sizeof(io_attr));
// 推理
rknn_input inputs[1];
memset(inputs, 0, sizeof(inputs));
inputs[0].index = 0;
inputs[0].type = RKNN_TENSOR_UINT8;
inputs[0].size = io_attr.n_input * io_attr.h * io_attr.w * io_attr.c;
inputs[0].fmt = RKNN_TENSOR_NHWC;
inputs[0].buf = input_buffer;
rknn_run(ctx, NULL);
// 获取输出
rknn_output outputs[1];
memset(outputs, 0, sizeof(outputs));
outputs[0].want_float = 1;
rknn_outputs_get(ctx, 1, outputs, NULL);
这段代码直接跑起来很正常,但集成到系统里后发现了问题:多线程并发推理时,偶尔会崩溃。
原因是一个 RKNN 上下文不支持多线程,每个线程需要独立的上下文。改成线程池加上下文复用:
// 预分配多个上下文
std::vector<rknn_context> contexts;
for (int i = 0; i < max_threads; ++i) {
rknn_context ctx;
rknn_init(&ctx, model_data, model_size, 0, NULL);
contexts.push_back(ctx);
}
// 每个线程获取空闲上下文
int get_available_context() {
// 简单实现,实际可以用信号量或条件变量
for (int i = 0; i < contexts.size(); ++i) {
if (context_used[i] == false) {
context_used[i] = true;
return i;
}
}
return -1;
}
这个改法很简单,但问题解决得很彻底。现场并发从 2 提到 8,吞吐量也上去了。
性能调优:从能跑到好用
部署成功后,基准测试结果是单次推理 35ms。现场要求是 20ms 以内,所以需要优化。
第一步是检查推理的瓶颈在哪里。RKNN 提供性能分析工具:
rknn_perf_profiling -m yolov8n.rknn -d test_image.jpg
输出显示:推理本身 18ms,预处理 10ms,后处理 7ms。所以瓶颈不是模型,而是前后处理。
预处理主要是图片缩放和归一化。原始代码用的是 OpenCV 的 resize:
cv::Mat resized;
cv::resize(input, resized, cv::Size(640, 640));
换成 RKNN SDK 自带的 zero_copy 接口:
rknn_tensor_attr input_attr;
rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &input_attr, sizeof(input_attr));
// 直接映射输入缓冲区
void* input_buffer = malloc(input_attr.n_elems);
rknn_set_mem_pool(ctx, input_buffer, input_attr.n_elems);
// 省略数据拷贝
这一改,预处理从 10ms 降到 3ms。后处理优化也类似,NMS 手动优化算法和内存分配,从 7ms 降到 4ms。
最终单次推理 25ms,还没到 20ms 的目标,但现场判断可以接受,因为不是每一帧都需要推理,实际帧率是够的。
还有一个问题是温度。RK3588 满载跑一段时间后会降频,推理延迟会跳到 40ms 以上。解决方法是加了一个散热风扇,并限制了最大频率:
# 设置 CPU 频率上限
echo 1800000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
这样温度稳定在 60 度以下,频率不会降频,推理延迟稳定在 25ms。
踩过的坑总结
这次部署踩过的坑,大致可以归为几类:
模型转换问题:RKNN 不支持所有算子,需要手动拆分模型。一开始没看文档,直接转换,卡了很久。
量化精度问题:量化数据集不具代表性,导致精度掉太多。后来用实际场景图片重新量化才解决。
多线程问题:RKNN 上下文不支持多线程并发,需要独立管理。这个问题集成时才发现,单独跑的时候完全没问题。
性能瓶颈问题:一开始以为是模型慢,后来分析发现是前后处理。这点提醒了以后性能分析不能凭直觉,要看实际数据。
温度问题:边缘设备散热能力有限,长时间运行会降频。这个问题比较隐蔽,只跑几分钟测试时发现不了。
边缘部署的一些判断
做完这次部署,对边缘 AI 有了一些自己的判断:
不要试图把云端所有能力搬到边缘。边缘的核心是"够用"和"稳定”,不是"全面"和"完美"。后处理可以简单一点,精度可以牺牲一点,但要保证结果稳定。
模型选型要考虑硬件。YOLOv8n 在 RK3588 上跑得还可以,但如果换成更大的模型,可能就跑不动了。选模型时先看目标硬件,不是先看 SOTA。
量化是把双刃剑。能换速度,但精度可能掉。如果场景对精度敏感,要提前做充分验证,不要等到上线才发现问题。
性能优化要基于实际数据。直觉往往是错的,先用工具分析瓶颈,再针对性优化。
长时间运行测试不能少。温度、内存泄漏、稳定性这些问题,只有长时间跑才会暴露。
这次部署最后交付的结果是:单次推理 25ms,准确率 96.2%,设备稳定运行超过一个月。中间有不少波折,但最终还是把任务完成了。
边缘 AI 部署不是什么高大上的话题,但确实需要耐心和实践。像做菜一样,食谱能告诉你大概怎么做,但火候、调味这些细节,得自己实际试过才知道。
最后留一个问题:如果下次硬件换了,比如换到算力更小的设备,或者换到不同架构的芯片,这些经验还能用吗?答案可能是部分能用,但总有一部分需要重新摸索。这就是边缘部署的现实,没什么通用解法,只能根据具体情况权衡和调整。
版权声明: 本文首发于 指尖魔法屋-把云端换到边缘时踩过的坑(https://blog.thinkmoon.cn/post/151-edge-ai-deployment-cloud-to-edge/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。