MiniCPM-Robot 接自建机械臂,先分清仿真验证和真机联调

文章导读
拿到 MiniCPM-Robot 后想直接驱动自建机械臂,通常走不通:仓库里的推理示例默认了一套观测格式和动作维度,而自建臂的关节数、单位、控制周期往往与它对不上。可行做法是把工作切成两段——能靠仿真闭环确认的(输入字段对齐、动作维度一致、单步输出可解析)先在仿真里跑顺;必须上真机才能对的(下发频率是否稳定、限位与急停是否生效、时延抖动)留到联调阶段处理。两类问题混在一起排查,日志会互相干扰,定位
📋 目录
  1. Ⅰ 在仓库里定位推理入口和依赖文件
  2. Ⅱ 把模型输入字段和自家传感器数据对齐
  3. Ⅲ 在仿真里先验证单步动作输出
  4. Ⅳ 真机联调前固定控制频率与安全边界
  5. Ⅴ 按日志把失败点归到输入、模型还是执行端
A A

拿到 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` 不一定官方支持,可以自己在动作下发前加一个打印分支代替。启动前必须就位的文件有三个:权重、推理配置、一帧示例观测(含图像与机器人状态)。缺任何一个,后面所有验证都无法复现。

把模型输入字段和自家传感器数据对齐

这一步最容易出隐性错误:形状对了但通道顺序或单位错了,仿真里表现是“能动但方向不对”,很难察觉。逐字段核对:

MiniCPM-Robot 接自建机械臂,先分清仿真验证和真机联调
  • 图像:常见形状为 (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 定义成与关节顺序一一对应的数组,既是输入校验,也是后面限位检查的复用来源。

在仿真里先验证单步动作输出

只跑一步、不下发,先把输出维度钉死。动作向量维度与自家机械臂可控自由度(含夹爪)必须相等,否则真机上一定出问题,只是报错位置不同。

MiniCPM-Robot 接自建机械臂,先分清仿真验证和真机联调
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}'

若平均间隔明显大于设定周期,说明瓶颈在推理或通信;若间隔波动大,先降低目标频率再联调,别在抖动状态下继续加动作幅度。

MiniCPM-Robot 接自建机械臂,先分清仿真验证和真机联调

按日志把失败点归到输入、模型还是执行端

联调出问题时,按下面顺序分流,先看日志特征再改代码:

  • 输入类:日志里出现形状、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`

这一步能过、接上真机才失败,问题基本落在执行端;这一步就失败,按上面前两类日志特征继续缩小。不要把真机联调当成调试输入格式的场所,仿真阶段没确认的东西,真机上只会更难看清。