最近一直在折腾RK3588板子上跑RTMPose,从模型导出到上板推理,中间踩了不少坑,也摸出了一些优化门道。姿态估计这个方向在边缘设备上其实需求很旺,机器人抓取、安防行为分析、健身动作纠正,都离不开它。但真要把RTMPose这类模型部署到RK3588上并做到实时,并不是把模型扔进RKNN-Toolkit转一圈就完事,转换、量化、内存布局、后处理、线程调度,每一环都可能成为性能瓶颈。
这篇文章就围绕“RK3588 + RTMPose 部署 + 优化”这条主线,分享一下我实际操作的完整流程,包括环境怎么搭、模型怎么转、推理怎么加速、问题怎么排查。适合正在做嵌入式AI部署的工程师,或者是想把姿态估计算法落地到瑞芯微平台的同学们参考。我尽量把每一步的“为什么”也讲清楚,这样换一个模型、换一块板子,思路也套得上。
1. RK3588平台与RTMPose模型的适配性分析
1.1 RK3588的算力结构解读
先说硬件。RK3588这颗芯片在国产边缘计算板卡里算是明星级的存在,CPU是4个A76大核加4个A55小核,大核主频能到2.4GHz左右,GPU是Mali-G610 MP4,但真正做AI推理靠的是内置的NPU,官方标称算力6 TOPS(INT8)。很多人第一次听到6 TOPS觉得不高,但要注意它支持INT8、INT16、FP16的混合精度计算,而且有3个NPU核心可以独立调度或合并使用,这在嵌入式平台上非常关键。
实际部署时这个算力怎么理解呢?我打个比方,6 TOPS大致相当于你可以在每秒执行6万亿次整数运算,听起来挺猛,但跑深度学习模型时,由于内存带宽、算子调度、数据搬运的限制,能发挥出来的有效算力通常是理论值的60%到80%。所以设计部署方案时,不要只看TOPS,更要关注模型是不是“NPU友好”。
另外RK3588的内存带宽也很重要,双通道LPDDR4X/5,带宽通常在51.2GB/s以上,这对视频流处理和同时跑多个模型很有利。我在部署RTMPose时会同时开两个模型实例,一个跑人体检测、一个跑关键点估计,CPU和NPU还能各干各的,这个后面细说。
1.2 RTMPose模型结构与NPU部署的契合点
RTMPose是上海AI Lab提出的基于Transformer的Top-Down姿态估计模型。说到Transformer,有人可能会担心部署到NPU上算子不支持,但实际上RTMPose的部署友好度相当高,原因在于它的核心设计:
- 它采用SimCC表示法,把关键点坐标预测转化为两个一维分类任务,也就是分别在x和y方向做softmax回归,这就不需要像传统Heatmap方法那样维护一个高分辨率的热力图头,计算量小很多。
- RTMPose在训练时做了解码器对齐和知识蒸馏,小模型(比如RTMPose-t和RTMPose-s)的精度很高,适合边缘部署。
- 它属于Top-Down方案,也就是先检测人再回归关键点,整体流程是检测+姿态估计,两个模型可以分开部署,灵活度很高。
在RK3588的NPU上,RTMPose前向推理的主体结构(Backbone + Neck + Head)几乎都能被RKNN框架转换并调度,只有个别LayerNorm、Softmax类算子可能在NPU上支持得不完美,这些部分可以放在CPU上做,或者在后处理中统一实现。注意,我说的“放在CPU”不是贬义,很多时候NPU和CPU协同工作反而是最优解。
2. 部署前准备:环境搭建与模型转换全流程
2.1 RKNN-Toolkit2环境搭建要点
在RK3588上部署RTMPose,模型转换必须在PC上完成,板子只负责推理。转换工具是RKNN-Toolkit2,官方的称呼是“RKNN Toolkit2”,它支持把ONNX、PyTorch、TensorFlow等模型转成RKNN格式。
环境搭建时有几个容易忽略的地方:
- Python版本建议用3.8到3.11之间的某个稳定版本,我这边用的是3.10。
- RKNN-Toolkit2依赖一堆东西,包括numpy、onnx、onnxruntime、opencv-python等等,先创建独立的虚拟环境,不要一股脑装到系统Python里,否则后面依赖冲突会让人崩溃。
- 安装方式有pip安装和源码编译两种,我推荐直接pip安装官方发布的wheel包,从官网或GitHub Release下载与你的Python版本匹配的包,简单省事。
- 转换时宿主机建议用x86_64的Ubuntu系统,我试过在Windows的WSL里装,也能用,但遇到USB连接板子调试时会有额外麻烦,所以还是老老实实用Linux。
装好后,在终端执行rknn-toolkit2 --version能正常输出版本号,环境就算通了。版本选择上,我用的是1.6.0,对应板端运行时librknnrt也必须是配套版本,否则会出现转换成功但上板推理直接崩的悲剧。
2.2 从RTMPose导出ONNX模型
拿到官方或自己训练的RTMPose权重之后,第一步是导出ONNX。RTMPose的官方仓库里其实已经提供了导出脚本,但默认导出的是包含完整后处理的模型。做嵌入式部署时,我强烈建议导出前把后处理剥离开。
为什么?因为RKNN转换器对某些动态shape或者自带解码逻辑的节点支持不好,而且后处理放在NPU上未必比CPU快。RTMPose的SimCC解码本质上是argmax和指数加权,CPU算这个非常快,完全没必要让NPU去处理。
导出时的关键点:
- 把模型设为eval模式,固定batch size为1,输入shape固定为(1,3,H,W),比如(1,3,256,192)或者(1,3,288,288),具体看你的应用。RKNN对动态输入支持不友好,能固定就固定。
- opset版本建议设为11或12,太高某些算子转换时容易出问题。
- 导出后用onnxsim做一遍简化,去掉冗余的Constant节点和形状操作。
- 用onnxruntime加载导出的模型,准备一张测试图推理一遍,记录输出tensor的shape和数值范围,这一步是为了后面跟RKNN推理结果对齐比对。
- 输出节点只保留SimCC分支的两个输出,x方向一个、y方向一个,形状通常分别是(1,K,192)和(1,K,256)这种,K是关键点数量。
这一步多花十分钟,后面能省好几个小时排查问题的时间。
2.3 RKNN转换关键配置与量化集准备
ONNX模型准备好之后,写一个转换脚本调用RKNN-Toolkit2的config和build接口。下面是我实际用的转换模板,可以直接参考:
from rknn.api import RKNN rknn = RKNN(verbose=True) # 配置模型输入信息 rknn.config( mean_values=[[0.485 * 255, 0.456 * 255, 0.406 * 255]], std_values=[[0.229 * 255, 0.224 * 255, 0.225 * 255]], target_platform="rk3588", quantized_dtype="w8a8", quantized_algorithm="normal", optimization_level=3, ) # 加载ONNX模型 ret = rknn.load_onnx(model="rtmpose.onnx") if ret != 0: print("load onnx failed") exit(-1) # 构建RKNN模型并量化 ret = rknn.build( do_quantization=True, dataset="dataset.txt", rknn_batch_size=1, ) if ret != 0: print("build failed") exit(-1) rknn.export_rknn("rtmpose.rknn") rknn.release()这里有几个配置要单独说:
mean_values和std_values必须跟你训练或导出时用的预处理完全一致。RTMPose官方仓库默认用的是ImageNet的标准化参数,也就是mean为[0.485, 0.456, 0.406],std为[0.229, 0.224, 0.225],而且输入是0到1之间的float。但NPU推理时,输入通常是0到255的uint8图像,所以需要把mean和std乘以255换算成像素值,这个换算错了,模型精度直接崩,而且症状很像量化损失,误导你往错误方向排查。
quantized_dtype我用的是w8a8,也就是权重和激活都是INT8,这是RK3588上最高效的模式。如果你发现精度掉得厉害,可以退一步用w8a16或者混合精度,速度和精度间的平衡点得按你的实际场景去试。
quantized_algorithm有normal和mmse两种可选,mmse的效果通常更好一点,但转换时间更长。我两个都试过,RTMPose这个模型对量化不算特别敏感,normal基本够用。
dataset.txt里放的是用于量化的图片路径列表,每行一张。这里有个非常重要的经验:量化图片一定要跟真实应用场景中的数据分布接近,而且要充足。我见过有人拿200张训练图做量化集,结果上板后精度比预期低很多,因为场景光照和图像内容差异大。我的习惯是直接从测试视频里抽帧,凑200到500张,覆盖不同的角度、距离、光照,效果会明显改善。
2.4 量化精度对比与FP16/INT8选择
转完模型后,第一件事不是急着上板,而是先用模拟器跑一遍,看RKNN模型在PC上推理的结果跟ONNX模型的输出差多少。RKNN-Toolkit2提供了模拟推理的接口,可以直接在PC上加载RKNN模型跑推理。
我的评估方法很简单:准备20张测试图,先让ONNX模型推理得到关键点坐标,再让RKNN模拟器推理得到坐标,然后计算两组关键点之间的平均欧氏距离。如果平均误差在1到2个像素以内,说明量化损失在可接受范围,可以直接上板。如果误差超过5个像素,那就要考虑用FP16模型或者重新做量化集。
RTMPose的SimCC分支输出的是类别概率,经过argmax变成坐标值,这种离散化过程对量化误差有一定的平滑作用,所以它比直接回归坐标的模型抗量化能力更强。这也是我推荐RTMPose部署到RK3588上的一个原因。
3. 推理加速与工程化落地实操
3.1 RKNN推理API选型:Python还是C++
RKNN推理有两种主流方式:Python的RKNN Lite接口和C++的RKNN API。
Python接口上手快,做原型验证很方便,但实际部署到生产环境,我还是建议用C++。原因有两点:一是C++避免了Python解释器的开销,推理循环里的每一毫秒都很珍贵;二是C++可以更方便地操作零拷贝内存,直接拿到NPU输出的tensor地址,不需要在Python和C之间反复拷贝数据。
如果是跑在RK3588的Linux系统上,板端只需要装librknnrt.so这个运行时库和对应的头文件rknn_api.h,应用程序通过CMake链接即可。下面是一个最小初始化的代码片段:
#include "rknn_api.h" int main() { rknn_context ctx; int ret = rknn_init(&ctx, model_path, 0, 0, nullptr); if (ret < 0) { printf("rknn_init failed: %d\n", ret); return -1; } // 获取模型输入输出信息 rknn_input_output_num io_num; rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num)); // 配置输入 rknn_input inputs[1]; inputs[0].index = 0; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].size = img_width * img_height * 3; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].buf = img_buffer; inputs[0].pass_through = 0; rknn_run(ctx, nullptr); // ... }3.2 三线程流水线:采集、推理、后处理并行
在嵌入式平台上,最影响实时性的往往不是NPU推理本身,而是数据流的串行等待。摄像头采集一帧、NPU推理一帧、后处理计算一帧,如果三件事串行做,总耗时是三者之和,帧率会非常难看。
我采用的方案是三线程流水线:线程A负责从摄像头或视频流抓帧,线程B负责NPU推理,线程C负责后处理(SimCC解码、坐标映射、绘制结果)。线程之间用环形缓冲区传递数据,达到帧率解耦。
在RK3588上,线程A可以绑定在A55小核上,因为抓帧不费算力,线程B和线程C绑定在两个A76大核上。用pthread_setaffinity_np可以设置CPU亲和性。这么做的好处是避免操作系统频繁调度线程导致推理时间抖动,实测帧率稳定性提升不少。
3.3 预处理加速:RGA硬件缩放与格式转换
RTMPose的输入尺寸通常不大,比如256x192,但如果你是从1080p摄像头画面里裁剪出人体区域再resize到输入尺寸,这个缩放操作如果放在CPU上用OpenCV做,会占用不少时间。而且Top-Down方案是先检测再裁剪,每帧可能有多个人体框,预处理次数成倍增加。
RK3588的VPU里有一个RGA(Raster Graphic Acceleration)硬件模块,专门做图像缩放、旋转、格式转换等操作,性能远高于CPU。我们可以在C++里调用RGA的接口,实现从yuv420sp到RGB的转换和等比缩放,一步到位。
这里提醒一下,调用RGA前要保证buffer的内存对齐,一般用2字节或16字节对齐,否则RGA会报参数错误。如果你用的是零拷贝方式拿摄像头数据,注意把地址和stride都传给RGA,不能假设它跟图像宽高完全一致。
3.4 SimCC后处理的高效实现
RTMPose的后处理跟传统Heatmap后处理不太一样。它输出的是x和y两个方向上的类别概率向量,比如x方向长度是W、y方向长度是H。拿到概率向量后,一般有两种解码思路:
- 直接对概率向量做softmax后取argmax,得到坐标。
- 先用softmax归一化,再计算概率加权期望,得到亚像素精度坐标。
第一种速度快但精度低,第二种精度高但需要遍历W和H次。在嵌入式平台上,我折中了一下:先做一次softmax,然后找到最大值位置,再在最大值附近取一个窗口计算加权期望。这样既保证了精度,又减少了遍历次数。
用C++实现时,最关键的是不要用vector的push_back在一个循环里反复扩容,最好预先分配好固定大小的数组,避免动态内存分配带来的不确定性。SimCC的解码逻辑天然适合并行,还可以用NEON指令进一步加速,但对于256x192这种尺寸,普通的循环优化就够用了,不必过度优化。
3.5 多路视频流的资源分配方案
如果你的应用是同时处理多路摄像头,RK3588的3个NPU核心就派上用场了。rknn_query接口可以查询NPU核心信息,rknn_init可以指定使用哪些核心。默认情况下,一个进程绑定一个NPU核心,你就可以起三个进程,分别处理三路视频流。
注意每个进程都要独立初始化自己的RKNN上下文,不要共享同一个context,否则会崩溃。另外多路流同时推理时,DDR带宽会成为瓶颈,我实测在4路1080p输入、每路跑RTMPose-t场景下,帧率会从单路的40fps降到每路15fps左右,这是正常现象,需要根据实际带宽预算调整分辨率。
4. 性能实测、瓶颈定位与常见问题排查
4.1 性能评估方法:不止看fps
很多人在优化部署时只盯着fps这一个指标,这很容易误判。正确的做法是把单帧耗时拆解成四个部分:图像采集耗时、预处理耗时、NPU推理耗时、后处理耗时。每个部分单独打点统计,才能精准定位瓶颈。
NPU推理耗时可以通过rknn_run接口前后加clock_gettime来测量。这里有个细节要注意:rknn_run是异步接口还是同步接口,取决于你是否设置了async标志。默认是同步的,但如果你想实现流水线并行,可以设置成异步,再通过rknn_wait等待结果。
还有一个重要指标是CPU占用率和温度。RK3588在高负载下会发热降频,NPU推理速度随温度升高会明显下降。如果长时间满负荷运行,建议在散热方案上给足余量,或者在代码里加一个温度监测,温度超过阈值时主动降低帧率或切换到低功耗模式。
4.2 常见问题速查表
下面这张表是我整理的实际部署过程中最常遇到的问题和对应的排查思路:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 转换时提示算子不支持 | ONNX模型里含有RKNN未实现的算子 | 导出前用onnxsim简化,不支持的算子尝试拆分成基础算子 |
| 模拟器推理结果全为0 | 数据预处理与训练时不匹配 | 检查mean/std和通道顺序,RTMPose是RGB还是BGR要和你代码一致 |
| 上板推理精度远低于PC模拟 | 量化数据集分布偏差大 | 重新采集贴近实际场景的图片做量化集 |
| 推理偶发报错或崩溃 | 内存未对齐或缓冲区大小不足 | 检查rknn_input.size是否等于wh3,input buffer按16字节对齐 |
| NPU模型加载失败 | 板端librknnrt与转换工具版本不一致 | 更新板端librknnrt到与RKNN-Toolkit2匹配的版本 |
| 帧率上不去但NPU占用率很低 | 预处理或后处理是瓶颈 | 用打点方法定位耗时,重点优化CPU部分 |
| GPU也能跑但没发挥NPU优势 | CPU和NPU任务没有并行 | 使用异步推理和双线程流水线,让CPU和NPU重叠工作 |
4.3 几个容易踩坑的细节
我多说几个经验,都是真金白银踩出来的。
第一个是RKNN的输入size必须是你图像buffer的实际size,但很多人在使用零拷贝时,buffer大小可能是按stride对齐过的,比如宽度不是16的倍数,硬件分配buffer时会自动对齐到16的倍数,导致rknn_input.size要设置成strideheight而不是widthheight,否则NPU读到的数据是错位的。
第二个是后处理里softmax分母可能非常大,如果用float32计算会溢出吗?不会,但精度会有损失。RTMPose的SimCC输出一般是logits,直接softmax没问题,但如果模型已经自带了softmax,你的代码里不要再加一次,否则坐标全是中心点。
第三个是调试阶段建议先用单张测试图离线跑通,再上板跑视频流。很多人一上来就连摄像头,摄像头那边图像格式没对上就怪模型部署错了,最后绕一大圈才发现是yuv转rgb的问题。先离线再在线,排查路径清晰得多。
第四个是数据并行的问题。如果你用OpenMP或者自己开多线程同时跑多个rknn_run,要注意RKNN context本身不是线程安全的,一个context只能在一个线程里调用。多线程推理时给每个线程创建一个独立context会更稳妥。
5. 从“能跑”到“稳定跑”:工程化经验谈
5.1 部署验收标准怎么定
部署完成不是模型能输出关键点就算完,要有量化的验收标准。我这边一般定义三个维度:
- 精度维度:在测试集上计算PCK或OKS指标,跟服务器上FP16模型的指标对比,掉点不超过1到2个百分点算是可接受的。
- 性能维度:明确输入分辨率、单路还是多路、目标帧率是多少,用可复现的脚本持续运行30分钟以上,统计平均帧率和p95帧率,防止偶发卡顿。
- 稳定性维度:连续跑12小时,监控内存泄漏、句柄泄漏、NPU频率下降等问题。
这三个维度都过了,才叫真正部署完成。只跑一个demo给老板看fps没意义。
5.2 模型动态输入与分辨率动态调整
好多应用场景要求检测框大小不一,RTMPose的输入尺寸需要根据检测框amcrop出来再resize。有些做法是动态调整输入shape,但在RK3588上动态shape会导致NPU每次重新构图,性能损失很大。
我的做法是固定两档输入尺寸,比如一档(192x256)用于小目标或低精度要求场景,一档(288x384)用于高精度场景,在运行过程中根据检测框大小切换,而不是每帧都换。switch模型时需要重新设置input和output属性,会有几十毫秒的延迟,所以尽量在场景切换时触发,不要频繁触发。
5.3 从RTMPose扩展到其他模型的部署心得
再说一句,这套流程不只能跑RTMPose,你在RK3588上部署YOLOv8、Pose模型、甚至一些轻量级的大模型都可以沿用同样的方法论:先固定输入、再剥离后处理、然后精心准备量化集、最后用流水线并行榨干CPU和NPU的每一份算力。比如我之前在RK3588上还部署过YOLOv8的检测模型用于人体框提取,跟RTMPose组成完整的Top-Down管线,两模型间通过内存共享传递检测结果,整体帧率依然能保持在20fps左右。
5.4 RGA与MPP配合使用的进阶技巧
当你要同时处理多路摄像头时,MPP用于解码视频流,RGA用于缩放和格式转换,这两者配合可以大幅减轻CPU负担。我当时部署6路视频流时发现,如果用CPU去解码H.264,CPU占用率直接飙到80%以上,NPU推理被严重拖累。换用MPP硬解码之后,CPU占用率降到30%左右,推理帧率显著回升。
跟这些硬件模块打交道时,注意检查返回的错误码,常见的有EINVAL表示参数错误、EFAULT表示内存地址问题。开发阶段可以用dmesg看一下内核日志,很多硬件访问错误能直接看到原因。
写在最后的经验总结
文章写到这里,核心流程都已经过了一遍。如果让我总结一个最值得分享的体会,那就是:在RK3588这种异构计算平台上,模型本身往往不是瓶颈,CPU与NPU之间的数据搬运、算子调度、线程同步才是真正决定帧率的关键。不要一上来就想着优化模型结构,先把你现有的推理流程摸透,按我前面的方法把耗时拆解开,哪个环节占比最大就先优化哪个。我见过不止一次,有人辛辛苦苦压缩模型大小,结果预处理花的时间比NPU推理还多,纯属南辕北辙。
最后再说一个实用的小技巧:部署阶段给每个线程起一个见名知意的线程名,同时开启perf或者htop实时监控CPU占用,一帧图像从采集到输出坐标,全程用日志打印各级耗时。这套观测手段看似基础,但排查问题的效率会高很多。希望这篇文章能帮你在RK3588上顺利跑起RTMPose,少走一些我走过的弯路。