ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从MuJoCo到真机:PPO策略的ONNX导出与端侧部署实战

从MuJoCo到真机:PPO策略的ONNX导出与端侧部署实战 仿真里跑得好好的策略一搬到真机上就原地抽搐这大概是做机器人强化学习的人最熟悉的噩梦。最近我完整走了一遍“MuJoCo 里训 PPO - 导出 ONNX - 端侧盒子部署”的流程把一只仿真鸭子的控制策略一路搬到了物理硬件上。这篇就算是一次从头到尾的实战复盘过程里哪些坑值得绕路、哪些参数改一下就能救命我都记下来了。这个项目链路并不新鲜但非常典型物理仿真MuJoCo负责产生训练数据PPO 负责把“怎么走”这件事学进神经网络ONNX 则把 PyTorch 训练出来的权重变成端侧推理引擎能吃的中间格式。适合正在入门强化学习、或者已经在跑 RL 但不知道怎么把模型真正部署出去的工程师参考。文章不扯玄乎的概念只讲我实际踩过的坑和验证过能跑通的方案。1. 项目整体设计为什么是 MuJoCo PPO ONNX1.1 MuJoCo 在一众仿真器里强在哪做机器人 RL 的人绕不开仿真器选型。常见的开源方案有 MuJoCo、PyBullet、Isaac Gym现在叫 Isaac Lab每家的定位差别还挺大。我这次选 MuJoCo主要看中三个点。第一是接触建模的质量。MuJoCo 用软接触模型物体之间碰在一起的时候计算出的接触力相对平滑不会像某些引擎那样出现力突然跳变的情况。对于训练腿足机器人、机械臂抓取这类强接触任务这个特性非常关键数值稳定性直接决定训练能不能收敛。第二是速度快。MuJoCo 底层是 C 语言实现仿真步进效率很高配合 Python 端到端的接口可以轻松跑起上千个并行环境的训练。在 CPU 上做数据采集GPU 上做策略更新这种 pipeline 在很多项目里已经是标配。第三是模型格式统一。MuJoCo 原生使用 MJCF 格式描述机器人模型同时也支持 URDF 导入。无论是自己搭一个简单的小车还是加载 Unitree 的机器人模型都有一条比较清晰的路。网上也有很多现成的模型可以直接拿来改省去从零建模的时间。PyBullet 其实也是个不错的选项安装简单、社区活跃但它在接触稳定性和并行效率上比 MuJoCo 要稍弱一些。如果你只是做简单的运动学验证PyBullet 够用如果你的目标是训练控制策略MuJoCo 会更省心。1.2 PPO 为什么成了默认选项强化学习算法五花八门DDPG、TD3、SAC、PPO 各自有拥趸。但如果你去翻机器人控制相关的论文和开源项目PPO 出现的频率绝对是最高的。这个现象背后有其合理性。PPO 属于 on-policy 算法它通过限制每次策略更新的幅度避免训练过程出现灾难性的性能崩塌。这个特性在机器人控制里特别重要因为动作空间连续、动力学复杂策略一旦更新迈得太大很容易把学好的行为全部毁掉。另外一个原因是 PPO 的超参敏感性相对较低。SAC 需要调的熵系数、DDPG 需要小心的探索噪声这些在 PPO 里都有相对成熟的默认配置。对于做工程落地的人来说“能用一套基础参数训出可用策略”比“理论上限更高但调参调到头秃”要实用得多。我这次在连续动作空间里还用了 dual-clip 的变体目的是防止极端情况下价值估计偏差被放大。具体来说就是在原始 PPO 的 clip 目标上再加一层限制让样本不因为意外的高 Advantage 把策略更新带偏。这个技巧不是必须的但当训练出现偶尔爆掉的 loss 时它确实能兜底。1.3 ONNX 在这条链路里到底解决什么问题训练完的模型是 PyTorch 的 .pt 或 .pth 权重它依赖 Python 运行时和 PyTorch 库才能跑起来。但真正部署到端侧设备上——不管是 Jetson、RK3588 还是树莓派——PyTorch 往往不是最优解甚至根本塞不进去。ONNXOpen Neural Network Exchange是一种开放式的模型表示格式它把网络结构、权重、算子统一描述成一张计算图。很多推理引擎都能直接加载 ONNX 文件并转换成自己的内部表示然后针对目标硬件做优化。换句话说ONNX 是训练框架和推理引擎之间的通用语言。这也解释了为什么很多场景下要把 PyTorch 模型转成 ONNX不是为了多一个文件而是为了拿到一个跨框架、跨硬件、可以被深度优化的中间表示。转换完之后你可以用 ONNX Runtime 跑 CPU/GPU也可以用 NCNN 跑手机端还可以转成 RKNN 在瑞芯微的 NPU 上加速。模型只导出一份部署方案却能多出好几种。2. 环境搭建与仿真侧训练把“会走”这件事教会 PPO2.1 MuJoCo 安装的常见坑与验证方法MuJoCo 在 2021 年底就免费开源了最新的 Python 绑定直接通过 pip 安装比早期需要手动下载 key 的流程简单太多。以 Ubuntu 22.04 为例一条命令就够pip install mujoco但装完之后导入经常会报错最常见的是缺系统依赖比如RuntimeError: Failed to initialize GLFW.这是典型的没有安装 OpenGL 相关库导致的。Ubuntu 上执行sudo apt update sudo apt install libgl1 libgl1-mesa-dev libglfw3-dev libglew-devWindows 11 上安装相对顺滑pip 装完基本就能用。唯一容易出问题的是 Python 版本建议用 3.9 到 3.11 之间太新的 Python 版本可能会碰到编译缓存不兼容的情况。安装完成后我习惯用两段代码快速验证环境是否正常。第一段是离线版检查基本导入和仿真步进import mujoco xml mujoco worldbody body freejoint/ geom size0.1 typesphere/ /body /worldbody /mujoco model mujoco.MjModel.from_xml_string(xml) data mujoco.MjData(model) for _ in range(1000): mujoco.mj_step(model, data) print(data.qpos, data.qvel)第二段是可视化验证会弹出一个窗口显示仿真场景import mujoco import mujoco.viewer model mujoco.MjModel.from_xml_string(xml) data mujoco.MjData(model) with mujoco.viewer.launch_passive(model, data) as viewer: for _ in range(10000): mujoco.mj_step(model, data) viewer.sync()如果这两段都能正常运行环境基本就绪。我在实际安装中还遇到过 patchelf 相关的错误旧版本有过当前新版本已经很少见了真碰到就pip install patchelf补一下。2.2 连续动作空间下的 PPO 实现要点这次项目的任务是控制一个鸭子外形的双足机器人走路。动作空间是连续量每个时间步输出若干个关节的目标角度或力矩观察空间包括关节角度、角速度、机身姿态和角速度。这里的核心问题不是“怎么搭网络”而是“怎么让探索和利用在连续空间里平衡起来”。PPO 在这个场景下的标准做法是Actor 网络输出动作分布的均值配合一个可学习的标准差向量训练时从这个高斯分布中采样部署时则直接取均值作为输出。要注意的是动作分布必须被限制在合法范围内否则网络刚开始随机输出时就会把机器人搞飞。常用的两种手段是 tanh 缩放层和 clip 截断。tanh 缩放把网络输出压缩到 [-1, 1]再线性映射到实际关节范围这种方式更平滑梯度也稳定。我使用的是前者原因是 clip 在边界处会形成梯度截断训练早期容易造成某些关节长时间卡死在极限位置。网络结构我参考了典型的 Actor-Critic 架构共享一部分 MLP 提取特征然后分两个头一个输出策略分布参数一个输出状态价值估计。激活函数用 ReLU中间层 256 个神经元总共三层。这个规模对 ONNX 导出和端侧推理都很友好实话说策略网络没必要追求大够用就行。训练配置是这么一组参数跑了很多次后验证过比较稳参数取值说明并行环境数2048数据采集吞吐量越多越稳学习率3e-4Adam 默认档多数任务够用GAE lambda0.95优势估计衰减系数clip ratio0.2PPO 裁剪阈值更新轮数10每批数据反复利用的次数mini-batch size256每次梯度更新的样本量熵系数0.01偏小后期策略更确定最大梯度范数0.5防止梯度爆炸折扣因子0.99常规设置其中并行环境数这个参数值得多说一句。它代表每一步同时跑多少个环境直接把采样速度放大数倍。2048 个环境同时跑单步能拿到 2048 条轨迹片段训练迭代效率比单环境快了不止一个数量级。MuJoCo 的 CPU 多环境并行开销很低不用白不用。2.3 奖励设计踩过的坑别把鸭子教成“直升机”奖励函数是 RL 训练里最玄学的部分。我一开始设计的奖励里包含了存活奖励、前进速度奖励、姿态惩罚、能量惩罚好几项权重反复试了很多轮结果发现一个典型问题策略学会了原地疯狂拍打肢体来获得速度奖励但身体根本没有实际移动。这其实就是奖励塑形Reward Shaping过度导致的捷径学习。后来我把奖励简化为四部分前进速度奖励机身 x 方向速度系数 1.0存活奖励每步 0.1鼓励稳定站立姿态惩罚机身倾角偏离零位时施加惩罚关节变化惩罚施加在动作变化量上抑制高频抖动改完之后训练曲线明显健康了。我的体会是奖励项越少越好权重越直接越好尤其是起步阶段。如果一个奖励项加上去后训练反而变差先把它去掉不要急着调系数。另外还有一个小细节reward scale。如果总奖励数值量级偏大价值网络的回归目标会很分散导致 critic 学不稳。我习惯把总奖励归一化到 0~1 左右或者对累计回报做标准化实践下来 critic 的 loss 会平稳很多。训练过程我大致跑了 2000 万步在 2080Ti 上大约花了两到三小时。关键判据是策略回报return曲线的上升趋势和熵值曲线的下降趋势。如果熵值掉得太快说明策略过早确定了后期很难再探索到更好的行为如果熵值几乎不降说明探索太随机需要增大学习率或减少熵惩罚。3. 从 PyTorch 到 ONNX模型导出实操3.1 导出前必须处理的模型结构问题训练好的 PyTorch 模型不能直接整个导出因为 Actor-Critic 结构里既有策略网络又有价值网络而端侧推理时只需要策略部分。所以我在代码里把 Actor 单独封装成一个PolicyNetwork模块forward 时只做“观察向量 - 动作均值”的计算。这一步看似简单很多人却会栽在这里。如果你的 forward 函数里混入了采样逻辑、噪声生成或者环境交互代码导出时 ONNX 追踪到的计算图就会很乱甚至导出失败。另一个容易忽略的点是输入维度。我在导出前统一固定了观察向量的长度比如 36 维同时把输入张量固定为batch_size1的二维张量。RL 部署场景通常是单步推理不需要动态 batch固定维度能省掉很多算子兼容性麻烦。你可以在导出前先打印一下模型结构确认层类型跑一段代码走一遍推理确保没有动态控制流import torch from policy import PolicyNetwork model PolicyNetwork(obs_dim36, act_dim8, hidden_dim256) model.load_state_dict(torch.load(actor.pt)) model.eval() obs torch.randn(1, 36) with torch.no_grad(): action model(obs) print(action.shape, action.dtype)输出应该是一个(1, 8)的张量表示 8 个关节的动作目标值。3.2 torch.onnx.export 的参数细节PyTorch 导出 ONNX 的核心接口是torch.onnx.export。下面是我在这个项目里实际使用的导出代码import torch from policy import PolicyNetwork model PolicyNetwork(obs_dim36, act_dim8, hidden_dim256) model.load_state_dict(torch.load(actor.pt)) model.eval() dummy_input torch.randn(1, 36, dtypetorch.float32) torch.onnx.export( model, dummy_input, actor.onnx, input_names[obs], output_names[action], dynamic_axesNone, opset_version17, do_constant_foldingTrue, )这里有几个参数值得展开讲。opset_version决定导出时使用哪个版本的 ONNX 算子集合。版本太低很多新算子无法表示版本太高端侧推理引擎可能还不支持。我一般选 17 或 18主流推理引擎都覆盖到了算是最稳的范围。input_names和output_names建议一定要写。ONNX 模型的输入输出不是按 Python 变量名继承的而是按照这里指定的名字。部署时推理引擎靠字符串名字绑定输入输出名字越直观越好维护。dynamic_axes是动态维度声明。如果端侧推理时输入尺寸固定保持None就行。虽然 ONNX Runtime 支持动态轴但动态轴会导致部分算子优化失效输出还是静态的跑得更快。还有一点do_constant_folding建议打开。它会把计算图中能在导出时算好的常量折叠掉减少推理时的冗余计算。模型变小延迟也能降一点点。3.3 导出后的校验、简化与格式确认导出成功只代表格式正确不代表推理结果正确。我强烈建议导完之后用 ONNX Runtime 对比 PyTorch 输出误差阈值卡在 1e-4 到 1e-5 之间。import numpy as np import onnxruntime as ort import torch # PyTorch 侧 model PolicyNetwork(obs_dim36, act_dim8, hidden_dim256) model.load_state_dict(torch.load(actor.pt)) model.eval() obs torch.randn(1, 36) with torch.no_grad(): ref model(obs).numpy() # ONNX Runtime 侧 sess ort.InferenceSession(actor.onnx, providers[CPUExecutionProvider]) input_name sess.get_inputs()[0].name output_name sess.get_outputs()[0].name result sess.run([output_name], {input_name: obs.numpy()})[0] diff np.abs(ref - result).max() print(max diff:, diff)如果差异超过 1e-4先检查模型是否有 BatchNorm它在 eval 和 train 模式下行为不同再检查 fp16/量化等因素。大部分差异都出在这两类原因上。模型校验通过后我会用onnxsim做一次简化把多余算子合并或删除pip install onnxsim python -m onnxsim actor.onnx actor_sim.onnx简化后的模型可能从几百 MB 压缩到几十 MB各别场景下推理速度也有提升。不过要注意onnxsim偶尔会把某些自定义算子误判简化完务必重新跑一遍上面的校验脚本。至于模型加密和混淆社区里方案很多但大部分都是把 ONNX 模型做某种形式的编码推理时再解密到内存。如果项目要求保护模型权重建议在端侧部署层做处理而不是在 ONNX 文件本身上花太多功夫因为 ONNX 格式本身是开放的仅靠改名或简单加密很容易被破解。4. 端侧部署推理引擎选型与真机跑通4.1 先搞清楚该用哪个推理引擎ONNX 模型导出来之后部署到哪、用什么引擎跑是另一个独立的决策问题。我的经验是不要一上来就追求 NPU 加速先把 CPU 上的 ONNX Runtime 流程跑通再考虑进一步优化。常见的选择有这么几类ONNX Runtime微软出品的跨平台推理引擎支持 CPU、GPU、DirectML还有 Python/C/Java 等语言绑定。通用性最强是“先跑通”的首选。NCNN腾讯开源的轻量级推理框架主打手机端和嵌入式平台内存占用小量化支持不错。适合 Android/iOS 或低算力 Linux 设备。RKNN瑞芯微 NPU 的专用工具链。如果你的板子用的是 RK3568/RK3588 这类芯片最终归宿大概率是 RKNN。TensorRT英伟达 GPU 平台的高性能推理引擎。Jetson 系列设备上用得最多。我用一张表把它们放在一起对比引擎适用平台优点缺点ONNX RuntimeCPU / GPU / 移动端跨平台、API 丰富、开发效率高硬件定制优化不如专用引擎NCNN手机 / 嵌入式 Linux轻量、支持量化、移动端优化好算子覆盖需要验证RKNN瑞芯微 NPU能发挥 NPU 算力、功耗低仅限瑞芯微平台TensorRTNVIDIA GPU / Jetson性能极致、支持 FP16/INT8只支持 NVIDIA 硬件按项目时间线来说我建议的路径是先用 ONNX Runtime 在开发机上把推理逻辑调通拿到一份正确性基线再根据目标硬件选择 NCNN 或 RKNN 做性能优化。这样每一步都能定位问题到底出在算法侧还是部署侧。4.2 用 ONNX Runtime 跑端侧推理的完整示例ONNX Runtime 在 Python 下的 API 非常简洁但很多端侧设备上最终是用 C 集成。我这里先演示 Python 版本逻辑最清晰C 版本的核心结构其实一模一样。import numpy as np import onnxruntime as ort session ort.InferenceSession( actor_sim.onnx, providers[CPUExecutionProvider] ) input_name session.get_inputs()[0].name output_name session.get_outputs()[0].name def choose_action(obs: np.ndarray) - np.ndarray: obs obs.astype(np.float32).reshape(1, -1) action session.run([output_name], {input_name: obs})[0] return action[0]这里的obs必须是 float32维度是(1, obs_dim)。很多人时序处理出错都出在维度上训练时是(batch, obs_dim)部署时忘了加 batch 维度导致推理报维度不对。这个坑我踩过不止一次。C 端部署时核心也是这几行逻辑#include onnxruntime_cxx_api.h Ort::Env env(ORT_LOGGING_LEVEL_WARNING, rl-policy); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(1); Ort::Session session(env, actor_sim.onnx, session_options); const char* input_names[] {obs}; const char* output_names[] {action}; std::vectorint64_t input_shape{1, 36}; std::arrayfloat, 36 input_data; // 填充输入数据 Ort::Value input_tensor Ort::Value::CreateTensorfloat( Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault), input_data.data(), input_data.size(), input_shape.data(), input_shape.size() ); auto output_tensors session.Run(Ort::RunOptions{nullptr}, input_names, input_tensor, 1, output_names, 1); const float* output_data output_tensors[0].GetTensorDatafloat();有几个地方需要注意。SetIntraOpNumThreads(1)是我在实时控制场景中强烈建议的设置因为它能保证单次推理延迟稳定。多线程在某些情况下会提升吞吐但在控制频率固定的场景里单线程低延迟反而更可控。另一个是输入数据指针的生命周期在session.Run完成之前千万不要释放输入张量。4.3 INT8 量化与精度控制端侧设备算力紧张模型量化几乎是必经之路。ONNX 模型的 INT8 量化通常有两种方式训练后量化PTQ和量化感知训练QAT。PTQ 流程最简单直接把训练好的 FP32 模型喂给量化工具配合一小部分校准数据统计激活分布就能生成 INT8 模型。ONNX Runtime 的 PTQ 可以这样写import onnx from onnxruntime.quantization import quantize_dynamic, QuantType model_fp32 actor_sim.onnx model_int8 actor_int8.onnx quantize_dynamic( model_fp32, model_int8, weight_typeQuantType.QInt8 )动态量化是最省事的方式它只量化权重激活值仍用浮点计算精度损失通常较小。但它的加速效果不如全量化在 RKNN 或 TensorRT 上一般会做更彻底的量化。这里最容易被低估的问题是RL 策略对量化误差的敏感度远高于分类模型。图像分类模型输出一个 1000 维的概率分布少量精度损失对 top-1 结果影响有限但强化学习策略的输出直接映射成关节力矩或目标角度一点点偏差就可能让控制变得抖动甚至失稳。所以量化完不能只看 “精度达到几个点”必须把量化模型接回仿真环境里跑完整回合观察策略回报是否掉得太多。我遇到过一个情况FP32 模型在仿真里稳定走完 200 步INT8 模型第 30 步就摔倒。原因就是某个关节的输出分布被量化噪声放大熵值激增导致动作随机性变大。如果碰到这种问题几个方向可以考虑换成 QAT在训练阶段就模拟量化误差模型对量化噪声更鲁棒。做混合量化对动作输出的最后几层保留 FP16 或 FP32只量化前面的特征提取层。增加校准数据量让量化工具更准确地统计激活值的动态范围。不要觉得量化是白捡的性能优化它其实是一个精度和效率的权衡过程。尤其对于控制类策略宁可保留 FP16 也不要彻底 INT8除非你验证过完整闭环没问题。5. 从仿真到真机那道“真鸭子”鸿沟5.1 Sim-to-Real 差距到底从哪里来模型在仿真里明明走得稳一到真实硬件上就开始抽风。这个问题我自己也经历过一开始以为是部署代码写错了后来才意识到这是 sim-to-real gap 的经典表现。差距的根源主要有几个方面。仿真器对物理世界的建模永远是近似的摩擦系数、关节阻尼、连杆质量分布、电机的时间常数每个参数都有误差。你在仿真里设定的躯干质量是 3kg真实鸭子硬件可能是 3.15kg这个差异在训练数据里从来没出现过。还有一个更隐蔽的因素是时间延迟。MuJoCo 仿真步进是同步且确定性的但真实硬件上传感器采样需要时间、计算推理需要时间、指令传输需要时间每一步都有固定或变化的延迟。PPO 训练时根本没见过这种延迟当然会被打个措手不及。最后一个大坑是传感器噪声。仿真环境里的状态可能是理想化的直接拿到的是关节角度真值真实硬件上转角来自编码器角速度来自差分或 IMU 滤波这些都带噪声和滞后。5.2 我用来缩小差距的几个实操手段域随机化Domain Randomization是缩小 sim-to-real gap 最实用的一招。核心思路很简单训练时每次随机化仿真参数让策略见过足够多不同的“世界”真机上不管落在哪个参数区间都能适应。具体到 MuJoCo我改了这么几个参数import numpy as np import mujoco def randomize_and_reset(model, data): # 摩擦系数随机化 model.geom_friction[:, 0] np.random.uniform(0.5, 1.5) model.geom_friction[:, 1] np.random.uniform(0.005, 0.02) # 关节阻尼随机化 model.dof_damping[:] np.random.uniform(0.8, 1.2) * model.dof_damping[:] # 质量随机化 model.body_mass[:] np.random.uniform(0.9, 1.1) * model.body_mass[:] mujoco.mj_resetData(model, data)这个随机化的范围不能太大否则任务会变得过于困难策略很难学到有效行为也不能太小否则跟没随机化差不多。可以先从一个相对温和的 10% 开始再根据真机表现逐步扩大。除了参数随机化我还有几个实际操作上的建议。第一训练时给观察加噪声模拟真实传感器的测量误差。第二对动作输出做低通滤波或限幅防止策略输出高频抖动。第三仿真里不要用过于理想的执行器给关节目标加一个简单的执行器延迟模型让策略学会处理滞后。5.3 真机调试的顺序先稳后快先开环再闭环从仿真跳到真机心态上急不得。我的调试顺序写得比较固定每次换新硬件都重新走一遍。第一步是开环测试。直接给鸭子发送固定的动作序列观察关节是否按预期响应。这一步能定位电机通信、驱动板、供电有没有问题。如果开环都不动闭环想都别想。第二步是传感器校准。读取关节编码器零位、方向、量程检查 IMU 姿态是否正常。推荐先把所有状态量打印出来逐项手动旋转关节确认读数方向和真实运动方向一致。方向反了在闭环控制里非常致命。第三步才是策略闭环。把 ONNX 推理接入控制循环先以一个比较低的频率运行比如 30Hz观察策略输出是否合理、机器人是否能够站住。确认稳定后再逐步提高控制频率。在实际贴近硬件的过程中我还养成了一个习惯在推理和输出之间加一层“安全限幅”。比如策略输出的关节目标角度先跟当前角度做差限制最大变化量防止异常值时关节被猛拉。这层保护几乎不消耗算力但当模型输出异常时能避免损坏硬件。6. 常见问题与排查技巧6.1 训练阶段回报不涨、熵崩、NaN 怎么办训练回报一直纹丝不动先检查奖励函数的尺度。如果奖励量级太小比如 0.001梯度信号会被网络初始化的噪声淹没如果量级太大价值网络需要预测的目标会很分散。把奖励大概缩放到 0.1~10 的范围很多训练问题会不治而愈。熵值下降过快通常意味着策略过早确定了。一种情况是熵系数设得太小另一种是学习率太大导致策略被某个方向的优势估计推得太远。我处理的办法是先调大熵系数比如从 0.01 调到 0.05看策略回报和熵的平衡是否改善。训练时出现 NaN几乎集中在三种情况学习率过大、GAE 计算出现除零、奖励出现无穷大或 NaN。我的排查习惯是先在环境 reward 里加一层断言发现非有限值立刻打印当前状态和动作先定位是环境问题还是网络问题再做进一步处理。6.2 导出部署阶段算子不支持、维度不对、输出对不上ONNX 导出最常见的报错是“Unsupported operator”。大多数情况下是模型里用了比较新的 PyTorch 算子而 ONNX 导出器还没跟上。解决办法是换用基础算子重写那部分逻辑或者在模型定义里把特殊算子替换成多个标准算子组合。推理结果和 PyTorch 对不上先确认模型处于 eval 模式。Dropout 和 BatchNorm 在 train/eval 模式下行为有明显区别如果忘了切 eval导出的模型包含训练时的随机行为部署推理当然不稳定。端侧推理延迟高不要一上来就怪推理引擎。先用session.run的时间统计确认单次推理耗时再对比输入预处理和输出后处理的时间。很多时候瓶颈不是推理本身而是你写的数据拷贝和内存分配代码。避免在控制循环里频繁new或malloc把缓冲区预先分配好推理延迟能砍掉一截。6.3 问题速查表现象可能原因解决方案训练回报不涨奖励尺度太小归一化奖励到 0.1~10训练回报突然暴跌策略更新幅度过大减小学习率或 clip ratio策略变得低频抖动探索不到位增大熵系数或降低学习率导出的 ONNX 报算子不支持用了过新算子替换为标准算子或升级 opsetONNX 推理结果与 PyTorch 相差大模型未切 eval 或量化过头检查模型模式改用全精度验证真机启动时剧烈抖动动作限幅保护缺失加安全限幅降低控制频率INT8 量化后策略失效RL 对量化误差敏感用 QAT 或混合量化保留敏感层精度这个表是我每次部署项目都会更新的“自己的坑位表”。新问题出现就补一行排查顺序按出现频率排序效率会高很多。如果要说一句总结我觉得是一句话仿真不是拿来骗自己的是拿来尽快找到真机问题的。训练出一个漂亮的 simulation return 曲线只是第一步真正让策略在真鸭子上跑起来才算把整条链路走通。最后分享两个摸爬滚打之后留下的心得。第一个心得是不管端侧推理引擎选得多花哨第一版永远先用 ONNX Runtime 在 CPU 上跑通拿到正确性基线后再碰 NPU 和量化。第二个心得是RL 策略部署的安全性底线一定要自己守住别把模型输出直接接到执行器上一层限幅保护能帮你少修好几个电机。真鸭子下水之前先让它在地上走两步。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进