拿到 MiniCPM-Robot 后想直接驱动自建机械臂,通常走不通:仓库里的推理示例默认了一套观测格式和动作维度,而自建臂的关节数、单位、控制周期往往与它对不上。可行做法是把工作切成两段——能靠仿真闭环确认的(输入字段对齐、动作维度一致、单步输出可解析)先在仿真里跑顺;必须上真机才能对的(下发频率是否稳定、限位与急停是否生效、时延抖动)留到联调阶段处理。两类问题混在一起排查,日志会互相干扰,定位成本反而更高。
判断:MiniCPM-Robot 的推理入口一般输出固定维度的动作向量,能否驱动自建机械臂取决于三件事——输入字段能对齐、动作维度能对上、下发频率与限位可控。建议先在仿真里用单步推理加断言确认前两项,再上真机验证第三项。输入形状不符、动作维度不符这类问题应留在仿真阶段解决,不要带到真机上边跑边改;真机阶段只处理频率、限位、急停和时延这类仿真中看不出来的问题,并以日志读数作为判断依据。
在仓库里定位推理入口和依赖文件
第一步不是改代码,而是找到“跑一次推理”的落点。通常推理相关文件分布在四处,以仓库实际文件名为准,不要按经验硬套路径:
- 入口脚本:
inference/、serve/、scripts/下的run*.py、demo*.py,或__main__中带 argparse 的那一层。 - 模型定义:
modeling_*.py、models/目录,注意看输出头(action head)的维度来源。 - 配置文件:通常包含图像分辨率、动作维度、归一化参数、是否使用状态输入。
- 依赖清单:
requirements*.txt、pyproject.toml、environment.yml,先确认 torch 版本与设备(CPU/CUDA)与仓库要求一致。
找到入口后,先用一条最小启动命令骨架确认它能加载权重并完成一次前向,不做动作下发:
# 模块名与参数名以仓库实际为准
python -m <repo_pkg>.inference.run \
`--config` configs/infer_sim.yaml \
`--ckpt` /path/to/weights \
`--sample` assets/sample_obs.npz \
`--dry-run`
`--dry-run` 不一定官方支持,可以自己在动作下发前加一个打印分支代替。启动前必须就位的文件有三个:权重、推理配置、一帧示例观测(含图像与机器人状态)。缺任何一个,后面所有验证都无法复现。
把模型输入字段和自家传感器数据对齐
这一步最容易出隐性错误:形状对了但通道顺序或单位错了,仿真里表现是“能动但方向不对”,很难察觉。逐字段核对:
- 图像:常见形状为
(B, 3, H, W)或(B, H, W, 3);取值范围可能是 0–1 的 float 或 0–255 的 uint8;通道顺序 RGB 与 BGR 需在配置或预处理代码里确认,不要凭 OpenCV 默认值猜。 - 指令文本:通常是原始字符串,交给模型内的 tokenizer 处理;长度上限以 tokenizer 的 max_length 为准,特殊字符与中文建议先做一次编码打印确认。
- 机器人状态:形状
(B, D_state),dtype 一般为 float32;关键不是形状而是含义——单位(弧度还是度)、关节顺序(与训练时是否一致)、是否包含夹爪。 - 可选字段:上一帧动作、时间戳。若配置里声明了就必须给,不能留空默认。
把自家相机帧和指令转成该格式的骨架如下,放在预处理模块里,别散落在主循环:
import numpy as np
def build_obs(frame_bgr, instruction, qpos, low, high):
img = frame_bgr[:, :, ::-1].astype(np.float32) / 255.0 # BGR->RGB, 归一化
img = np.transpose(img, (2, 0, 1))[None] # (1,3,H,W)
state = np.asarray(qpos, dtype=np.float32)[None] # (1,D_state) 单位需与训练一致
assert state.shape[1] == len(low) == len(high)
return {'image': img, 'instruction': instruction.strip(), 'state': state}
把 low/high 定义成与关节顺序一一对应的数组,既是输入校验,也是后面限位检查的复用来源。
在仿真里先验证单步动作输出
只跑一步、不下发,先把输出维度钉死。动作向量维度与自家机械臂可控自由度(含夹爪)必须相等,否则真机上一定出问题,只是报错位置不同。
action = model(**obs)
action = np.asarray(action).reshape(1, -1)
expected = robot_dof
assert action.shape == (1, expected), f'动作维度不符: {action.shape} vs {(1, expected)}'
assert np.isfinite(action).all(), '动作含 nan/inf'
# 仿真阶段先看是否超出关节限位,超出说明归一化参数或单位对不上
assert np.all(action >= low) and np.all(action <= high), '动作超出限位'
维度不一致时,报错通常出现在三个位置:一是模型前向内部,输出头固定维度与配置里的 action_dim 冲突,报 reshape 或广播错误;二是后处理把动作拆成各关节时,索引越界;三是下发到仿真控制器时被拒绝,报维度或自由度不匹配。看到第一种说明改配置即可,看到第三种说明还要检查控制器端的关节数定义。仿真里能通过断言,才具备进入真机联调的前提。
真机联调前固定控制频率与安全边界
仿真通过不等于真机能跑:仿真通常按步推进,真机则受推理耗时与通信时延影响。联调前先把三项固定下来:
- 频率:设定一个目标控制周期(例如推理耗时实测后再定),循环里用固定步长等待,不要用“跑完就发”的自由节奏。
- 限位:在每个关节上用
low/high做一次裁剪或直接拒发,越限时记录日志并停下,不要静默 clamp。 - 急停:确认软件侧有一个可随时置位的停止标志,且硬件急停按钮在联调前单独测试过一次。
验证实际频率是否等于设定值,靠日志时间戳而不是感觉:
# 下发处打点:logger.info('send_action ts=%.6f', time.time())
grep -o 'send_action ts=[0-9.]*' run.log | awk -F= '{if(p){d+=$2-p;n++} p=$2} END{print "mean_interval=", d/n, " eff_hz=", n/d}'
若平均间隔明显大于设定周期,说明瓶颈在推理或通信;若间隔波动大,先降低目标频率再联调,别在抖动状态下继续加动作幅度。
按日志把失败点归到输入、模型还是执行端
联调出问题时,按下面顺序分流,先看日志特征再改代码:
- 输入类:日志里出现形状、dtype、通道数、token 长度相关报错,或在
build_obs的断言处中断。特征是错误发生在模型前向之前,且换一帧示例观测就消失。 - 模型类:前向内部报 shape mismatch、
nan/inf,或输出维度与配置不符。特征是与输入数据无关,换示例仍然复现。 - 执行端:模型输出正常,但日志出现限位触发、超时、控制器拒绝、急停置位。特征是推理段日志完整,问题出现在下发之后。
最短复现路径是回到单步 dry-run,把变量收敛到一个:
python -m <repo_pkg>.inference.run `--config` configs/infer_sim.yaml `--sample` assets/sample_obs.npz `--dry-run`
这一步能过、接上真机才失败,问题基本落在执行端;这一步就失败,按上面前两类日志特征继续缩小。不要把真机联调当成调试输入格式的场所,仿真阶段没确认的东西,真机上只会更难看清。