把训练好的机器人策略从英伟达 GPU 搬到一块 RK3566 上,听着好像只是装环境、拷模型的事,但真做起来,坑多到你得把 GitHub 仓库从头到尾再翻几遍。这块 25 厘米级的 Microduck 桌面小机器人,是典型的“训练重、部署轻”的强化学习项目:训练阶段可能要大几千个并行环境在 GPU 上疯狂采样,而最终实机上只有一颗四核 Cortex-A55 加 0.8 TOPS 的 NPU。上周终于让它在地板上自己走起来的那一下,我突然觉得这套“仿真训练-模型压缩-边缘端部署”的链路,比机器人本身更值得记录。
如果你正准备用强化学习控制机器人,或者手里已经拿到了 RK3566 这类国产边缘板子,想把手头 PyTorch 模型落到真机,这篇文章应该对你有帮助。我会把整个项目拆成 GPU 训练、模型导出、NPU 转换、实机调试这几段,讲讲哪些方案选型是被现实逼出来的,也把我在 Microduck 上踩过的具体问题摊开聊。
1. 整体设计:为什么“训练”和“部署”完全是两条路线
1.1 Microduck 这类小机器人想解决什么问题
Microduck 这个尺寸的小机器人,说小是真小:整机也就 25 厘米级别,拆开就是舵机、结构件、IMU 和一块主控。它撞上了一个很尴尬的平衡点——如果你用传统控制方案去写步态,比如 ZMP 规划或者倒立摆模型,代码量不小,而且一旦换重心分布、换舵机型号,全部参数又得重新调一遍。强化学习的思路正好绕开这一步,让策略网络直接吃“关节角度、角速度、机身姿态”这些状态,输出关节期望角度,把步态当作一个端到端映射学出来。
这类小机器人的吸引力在于:成本低,适合反复摔;桌面就能跑,不需要大场地;计算需求也被压缩到边缘板子能扛住的范围内。但代价也随之而来——训练时你不能把一套真实机器人搬到仿真里就算了,真机的关节间隙、舵机响应延迟、电量变化,全都会让“仿真里会走”变成“实机会摔”。
落到本项目,控制芯片用的是 RK3566。这颗芯片常见于各种国产开发板,比如我调试用的泰山派,CPU 是四核 A55,集成 NPU 只有 0.8 TOPS,跟英伟达那一代动辄上百 TOPS 的 GPU 完全是两个物种。正因如此,策略网络不能做得太大,部署时还得考虑量化、剪枝、模型格式转换。Microduck 的强化学习闭环里,真正做主控推理的部分是很小的一块,而它面前的数据洪流早在 GPU 上就已经完成了“筛选”。
1.2 为什么非得用 GPU 训练
很多第一次做机器人强化学习的人会问:策略网络本身可能就几百 KB,为什么不在 RK3566 上直接跑训练?这个问题的答案,不是“训练需要的算力大小”,而是“强化学习靠的是环境交互的规模”。
为了学出一个稳的步态,你可能要让数百个、数千个虚拟 Microduck 同时被放置到仿真环境里,每个环境独立随机地形、随机负载、随机初始姿态,然后并行采样。MuJoCo 这类仿真器如果走 CPU,单进程交互速度远远撑不起 PPO 这类算法需要的样本量;一旦上了 GPU,每个物理步长能在几千个子环境里同步推进,数据吞吐率直接提升几个量级。我在训练时,一张中高端显卡大概能同时跑几千个环境,收益非常夸张。
反过来说,RK3566 的角色是“部署推理端”:把训练好的 actor 网络压缩到能跑、能低延迟响应、不会消耗太多功耗的程度。强化的策略是“训练阶段极其浮夸、部署阶段极其克制”,这个分工从一开始就要想明白,否则你会一辈子在给板子装 PyTorch、然后发现根本跑不动。
1.3 端到端技术链路预览
整个项目要打通的路,可以分成四步:
- GPU 仿真训练:用 URDF 建 Microduck 模型,在 MuJoCo/Isaac 类环境里并行采集数据,用 PPO 训练出策略网络。
- 模型导出:把 PyTorch 权重导成 ONNX,再经过 RKNN 工具链转换和量化。
- RK3566 系统适配:刷 Debian 类镜像、处理分区规划、开启 NPU 设备节点。
- 真机闭环部署:在 C++/Python 侧做状态获取、NPU 推理、舵机指令下发。
看不懂某一步没关系,下面每一段我都补上了实操细节和踩坑过程。
2. GPU 上的训练:仿真环境、奖励函数和硬件崩溃排查
2.1 训练环境搭建与 PyTorch 版本“陷阱”
第一步还是先把 Gym 风格的环境接口搭出来。我的做法是到 Microduck 开源仓库里找现成的 URDF 和训练脚本,没有就自己用 XML 组装一个简化 12 自由度模型。仿真器我用了 MuJoCo,它最方便的地方是和 Python 生态无缝衔接,可以直接在mj_env基础上加task、observation和reward三个维度。
这里要提醒一个很常见的入门迷路行为:一上来先死磕“装哪个深度学习框架”。Microduck 控制策略根本不需要什么花哨框架,PyTorch 足够。关键是你安装 PyTorch 时是否匹配了 GPU 环境——如果你是 Ampere 架构以后的卡,直接选官方给的 cu121/cu124 wheel 包就好;如果是在内网环境,建议先确认 CUDA driver 版本,再pip install torch --index-url指定对应源。否则训练到一半才发现 PyTorch 虽然能 import,但.cuda()一直返回 CPU,那就很浪费时间了。
另一个细节是:如果训练 PC 上插着多张 GPU,想“三个 GPU 同时测试”是很自然的需求,但 PyTorch 默认会占用所有可见显存。我建议你通过CUDA_VISIBLE_DEVICES来控制训练进程只看到 1 张卡,同时用os.environ["CUDA_LAUNCH_BLOCKING"] = "1"做最初的逐行排错,等逻辑没问题之后再取消。像我的显存只有 16 GB,单卡并行环境数要控制,否则会直接 OOM。
2.2 奖励函数里的隐藏地雷
强化学习在机器人控制上效果惊艳,但它的惩罚也很直接:如果奖励给错了,你会得到一项完全不符合直觉的“最优策略”。Microduck 训练时我就经历过“站着不动得分最高”的尴尬。原因是速度项权重太低、能耗惩罚太高,网络发现躺平比走路更划算。
这类现象就是你搜到“强化学习遇到错误奖励”时的典型场景。排查时,我习惯把 reward 的各个分量做成 TensorBoard 曲线,逐项看每个 epoch 的变化。比如前进项、朝向项、动作平滑项和能耗惩罚项分开记录,很快就能定位是哪一项主导了策略。
我最后用的奖励结构大致包括:
- 前进速度:朝目标方向的速度越接近期望值,得分越高;
- 朝向保持:让机器人躯干朝向运动方向,避免“横着走”;
- 姿态稳定:控制俯仰角、滚转角不要过大;
- 动作平滑:惩罚相邻控制指令的突变;
- 能耗和关节极限惩罚。
权重不是一次调对的。我的实际经验是,先让速度项和姿态项占大头,让机器人先“走起来”,再逐步增加平滑和能耗项,缩小动作振幅。切忌一开始就加一大堆正则,策略会变得畏手畏脚。
2.3 GPU Crash Dump 与英伟达驱动掉线
训练中后期最容易遇到的一个莫名其妙问题是:连续跑了十几小时,突然终端刷出GPU crash dump triggered,然后 CUDA context 全部失效。如果你用的是较新的 NVIDIA 驱动,会看到驱动版本号出现在日志里,比如 572.61 这类版本。
我当时试图让 Linux 下三张 GPU“同时开工”,结果问题频率更高。后来排查出的原因主要有三类:一是持续满载导致供电不稳,二是驱动版本和 CUDA 工具链不完全匹配,三是部分卡在persistence mode未开启时,任务调度状态下容易触发 crash dump。
实用的解决办法:先nvidia-smi -pm 1开启持久模式;同时把单次训练的 batch 大小稍微调低,避免显存峰值踩在启动瞬间;如果是多卡场景,用gpu队列或监控脚本把长时间任务拆小,给驱动喘口气。还有一个笨办法:跑到一半定时重启训练进程保存 checkpoint,至少不会一夜白训。
3. 桥接两座“孤岛”:PyTorch 模型如何变成 RK3566 上能跑的推理产物
3.1 先删掉训练辅助层,只导出 Actor
训练完 PPO 后,你的权重里既有 actor 也有 critic。但实机推理根本不需要 critic,它只在训练过程中用来估计 advantage。所以导出前先要把网络里pi_head抽取出来,只保留从 obs 到 action 的前向路径,同时把训练时加的 noise、actorbatch 分支全部去掉。
具体踩过的点:Microduck 的输入如果包含上一时刻的 action,那推理时就要自己保存一份“上一拍输出”,否则输入维度和状态拼接会错位。你用torch.onnx.export时最好用一个真实长度、真实维度的 dummy input,并且打开opset_version=12。如果模型里用了nn.Tanh这类常见激活函数,ONNX 导出一般不会出错;如果你用了自定义循环控制、带if条件的逻辑,导出会困难,建议直接改网络结构,让 actor 尽可能像一个无分支的 MLP。
导出后还有一个很多人不知道的步骤:用onnx-simplifier把计算图简化。RKNN 工具链对 ONNX 算子兼容性有限,图越简单,后续转换越省心。
3.2 ONNX 转 RKNN 与 INT8 量化
RK3566 的 NPU 对 PyTorch 原生模型完全无感,必须把 ONNX 模型通过瑞芯微的 rknn-toolkit2 转换为.rknn格式。转换前你得按官方文档安装一堆 Python 依赖,这个环境最好独立出来,别和你训练环境的 torch 混在一起,否则版本冲突能让你怀疑人生。
量化是这个环节里的重点。RKNN 默认会把网络转成 INT8 定点推理。为了做到这一点,你需要准备校准数据集——从仿真环境里随机采样几百组状态观测,让量化器推算激活值的动态范围。这里有个隐患:如果用仿真的状态分布去校准,但真机观测噪声分布差异较大,量化后的模型很可能在实机上退化。我当时采取的办法是把校准集里混入一部分从真机采集的观测状态,分布更贴近部署场景。
如果你发现 INT8 量化后动作明显变差,还有一个变通方案:只对部分层开启量化,敏感层保留 FP16 或 FP32。rknn.config 里支持 per-layer 的精度设置,只是你需要多跑几组实验比对。毕竟这关系到机器人的稳定性,不能一味为了速度牺牲质量。
3.3 部署时用 NPU 还是 CPU
RK3566 上其实还有一个很容易忽视的部署选择:这么小的策略网络,CPU 也能跑。推理一次 MLP 前向可能只要几毫秒,而 NPU 的优势是没有 CPU 占用、功耗稳定。但如果你的控制频率是 100 Hz 到 500 Hz,CPU 上跑一个 128×64 的 MLP 通常也足够。真正决定是否用 NPU 的,是你“是否还有其它 CPU 任务”。
Microduck 的实机上,一般还有一个系统在跑摄像头、通信、日志等,CPU 占用如果过高会影响主循环。我用 NPU 主要是想把 CPU 资源空出来,让遥控接收、LED 灯带和 IMU 数据解析都更稳定。不过要留神 RKNN 的初始化时间和首次推理延迟:第一次调用模型 API 可能耗时几十毫秒,必须先做一次 warm-up。
4. RK3566 实机系统准备:刷机、Root 和硬件接线
4.1 泰山派识别成 ADB 设备?这是大多数刷机教程没写清的事
RK3566 板子刷机时最常见的问题,是你用 Type-C 线把泰山派连到电脑上,lsusb能识别到瑞芯微设备,但设备状态是 ADB,而不是刷机用的 Loader 模式。很多教程一上来就说“按住 recovery 键插线”,我却发现怎么按都不进 Loader,后来才明白:板子里已经启动了 Android 或 Linux 系统,并且 adbd 接管了 USB。
这时如果你已经能通过adb devices看到设备,直接执行:
adb root adb reboot loader它会重启到瑞芯微的下载模式。如果板子已经变成“砖”,或者系统起不来,你的保险方案是找板子上的 MaskROM 触点或按键——先用金属镊子短接 MaskROM 触点,再插 USB 上电,设备会以 2207:0000 这类 ID 出现,那时候可以用rkdeveloptool直接操作。
我的建议:刷机不要怕变砖,RK3566 的 MaskROM 模式很难真正弄坏 eMMC 里的引导分区,只要还有这个模式就都能救回来。
4.2 Root、分区扩容与 RK3566 上跑 Debian 的细节
很多 RK3566 开发板出厂自带 Android,为了跑机器人控制,我更推荐刷成纯 Linux 的 Debian 镜像。它最大的好处是实时性更可控,不需要在 Android 里跟 HAL 层纠缠。但这一步之后就会遇到两个新问题:第一是默认用户没有 root 权限,第二是系统分区可能很小,塞不下推理库和模型。
Root 就没什么好说的,sudo passwd root打开账户,或者直接改/etc/sudoers。分区扩容要小心,不要贸然重新分区;RK3566 的分区表通常由 parameter 文件决定,如果你想扩rootfs,需要重新生成 parameter/分区表并刷入。一个更稳妥的办法是:板子如果支持 SD 卡启动,就直接把整个系统放 SD 卡大分区里,eMMC 留给引导,省去扩容折腾。
我当时实际用了 32 GB 的 eMMC 默认镜像,先确认了/dev/rknpu设备节点存在,再cat /sys/kernel/debug/rknpu/version看 NPU 驱动版本。如果驱动版本太老,需要从官方仓库更新内核模块,否则 RKNN runtime 会抱怨版本不匹配。
4.3 舵机、IMU 和电源:先检查再上电
系统搞定,接着是硬件接线。RK3566 板子通常只有 GPIO/UART/I2C/SPI 接口,没有专门的舵机控制板功能。Microduck 这类机器人会通过串口转接板连接整组串行总线舵机,板子只需要按协议发角度指令,就能控制多个舵机,省去大量 PWM 线。
上电前我建议至少确认三件事:
- 舵机总线电压不要超过标称值,很多小舵机是 6~8.4V,而 RK3566 主板是 5V,必须共地但不能直接把动力电源拉到板子电源引脚上;
- IMU 跟主控之间用 I2C 或者 SPI,不要靠近舵机电源线,否则数据噪声会很大;
- 先断开舵机负载做“空载旋转”测试,确认控制协议通了,再安装腿部结构件。
电源是整个项目最容易被低估的一环:小机器人空间有限,电池又是高倍率放电,一旦舵机急转,电压跌落会导致主控重启,这是最典型的“软件没问题,硬件背锅”案例。
5. 实机部署的最后一公里:仿真到现实的断裂与修补
5.1 控制频率和通信延迟:从 500 Hz 到 100 Hz 的妥协
训练时仿真器内部的控制步长经常能到 500 Hz,但到了实机上,舵机串口协议的刷新率、IMU 的采样频率和 RK3566 上运行的推理管线决定了整体闭环周期。如果你强行把控制频率拉到仿真要求,会发现舵机还没执行完上一条指令,下一条已经到了,最后只会变成高频抖动。
我最后把 Microduck 的默认控制频率降到了 100 Hz,主循环里固定用time.sleep或忙等来节拍。这一改乍看是性能“倒退”,但它换来的是指令时序稳定。记住,机器人控制追求的不是单次推理够快,而是每次推理的时间间隔都要稳定。你甚至可以在 RKNN 推理前后打时间戳,记录每次间隔的抖动;如果抖动超过 5 ms,优先查操作系统是否被后台进程抢占。
5.2 关节零位和状态观测对齐
实机调试里有个特别容易被忽略的细节:仿真里所有关节角度都是相对 URDF 定义的标准零位,但实物舵机安装时几乎不可能保证零位一致。如果你直接把强化学习策略输出的角度扔给舵机,机器人大概率会以一个扭曲的姿态硬撑着,然后过流保护。
解决办法是做一个“初始零位校准”流程:先把机器人手动摆成蹲姿或站立预备姿态,记录此时每个舵机的原始角度,把这个 offset 矩阵保存到配置文件。后续控制时:
cmd = target_angle_from_policy + joint_zero_offset如果你发现机器人某个关节总是反向用力,还要检查安装方向是否反了,这需要在代码里把舵机方向映射一起修正。这一步不做好,后续所有强化学习调试都是在跟一个变形的坐标系对抗。
5.3 真机调参中值得记录的典型问题速查表
实机阶段的问题太多,我把最常遇到的几类整理成了一张速查表:
| 现象 | 可能原因 | 解决建议 |
|---|---|---|
| 膝盖或关节发抖 | 控制频率不匹配、舵机响应慢 | 降低控制频率,增加动作平滑惩罚 |
| 机器人向一侧偏移 | 零位标定不准、重心分布不均 | 重新标定 offset,检查结构件是否对称 |
| 推理结果在实机上比仿真差 | 观测维度没对齐、量化偏差 | 对比真机 obs 和仿真 obs 分布,增加校准数据 |
| 舵机偶尔抽搐 | 供电跌落、串口干扰 | 加电容、加磁环,检查接地 |
| NPU 首次推理卡顿 | 模型初始化 or warm-up 缺失 | 启动时先推理一次 dummy input |
| RK3566 温度升高导致掉性能 | 散热不足 | 加散热片和风扇,限频测试 |
这个表表面看只是问题汇总,但背后代表了一个调试方法论:模拟到现实的问题,往往不是单一原因,而是“多个小偏差叠加”。每次只改一个变量,你才知道真正有效的修复是什么。
6. 一路踩坑后的几条实操建议
前面写了不少技术细节,最后再聊点比较“软”的经验。整个 Microduck 项目做下来,我最深的体会是:训练阶段的迭代速度,决定你能否坚持到成功。强化学习在机器人上不是一次就能跑通的,你得快速试错,所以训练脚本里记得定时保存 checkpoint、用 wandb 或 TensorBoard 记录奖励曲线、把随机种子固定住。如果每次跑出来的行为天差地别,你很难判断是算法改对了还是运气好。
另外,真机调试时一定要养成“录包”的习惯。我当时每次跑完都会把遥控指令、关节目标角度、实际角度、IMU 姿态,全都保存成 CSV。等机器人摔倒,回放数据一看,能很清楚地发现是目标角度发错了、还是舵机没跟上。很多问题从现象上看是“控制算法不够好”,实际分析后会发现不过是某根线松了、电源波纹大了或者某个关节的舵机已经热保护。没有数据,这些只能靠猜。
还有一个小技巧值得分享:如果要反复测试,尽量在机器人结构上预留一个提绳或防摔支架。25 厘米的小机器人虽然耐摔,但每次摔倒后关节都可能轻微移位,这会影响零位标定结果。准备一个轻量的龙门架悬吊方案,既能限制运动范围,又不会给机器人添加太多额外负载,能让每次实验的数据更干净。真到“放开跑”的时候,再撤掉辅助,这样调整的参数其实更接近真实工况。