先跑通单帧推理再谈机械臂联动——MiniCPM-Robot 的最小验证路径

文章导读
MiniCPM-Robot 的第一次验证,边界建议只划到「一帧图像 + 一条状态输入 → 一段动作向量输出」。机械臂一旦接进来,问题来源立刻多出通信延迟、控制周期、状态回读偏差三项,此时再看到报错,很难判断是环境没装对、输入构造错,还是模型权重根本没加载成功。先用单帧闭环把这三件事拆开,是成本最低的排查顺序。
📋 目录
  1. Ⅰ 把运行环境压到最小可运行集合
  2. Ⅱ 准备一帧固定输入并落盘
  3. Ⅲ 跑一次单帧推理并原样保存输出
  4. Ⅳ 人工判断输出是否在合理范围
  5. Ⅴ 从单帧扩到连续控制要补什么
A A

MiniCPM-Robot 的第一次验证,边界建议只划到「一帧图像 + 一条状态输入 → 一段动作向量输出」。机械臂一旦接进来,问题来源立刻多出通信延迟、控制周期、状态回读偏差三项,此时再看到报错,很难判断是环境没装对、输入构造错,还是模型权重根本没加载成功。先用单帧闭环把这三件事拆开,是成本最低的排查顺序。

适用场景是刚拿到 MiniCPM-Robot 类模型、还没有机械臂可用的阶段。操作动作:建隔离环境只装仓库声明的依赖,把一帧图像和状态固化成文件,跑一次推理并把原始输出 dump 到磁盘。验证方式:用同一份输入重复调用,输出应可逐字节比对;再用断言脚本检查维度、数值与单位。风险边界:单帧通过只说明链路和加载正确,不代表动作可用、时序可控或能闭环执行,机械臂联动需要另行验证。

把运行环境压到最小可运行集合

一上手就装全家桶,最容易出现的现象是 import 阶段报版本冲突,或者 GPU 上算子不可用,但日志里看不出是哪一层坏的。建议先用独立虚拟环境,只安装仓库依赖清单里列出的包,不在本文写死版本号——具体版本以仓库 requirements.txt 或 pyproject.toml 的声明为准,遇到冲突优先按清单解,而不是随手升级。

python -m venv .venv-robot
source .venv-robot/bin/activate
python -m pip install -U pip
python -m pip install -r requirements.txt
# 仓库若提供 constraints.txt 或环境 yaml,优先使用它声明的组合
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

这段命令放在项目根目录执行。替换项只有依赖清单路径和 Python 解释器版本,需要结合仓库 README 确认。验证方式:新建终端重新激活环境后,能成功 import 模型包并能打印出 torch 版本即视为环境层通过;如果 CUDA 不可用但 CPU 能加载,先记下来,不要在这一步就开始调模型参数。显存不足、权重路径写错、tokenizer 缺失这三类错误通常在这时暴露,日志里会直接给出文件路径,比后面混在推理报错里好认得多。

准备一帧固定输入并落盘

输入每次重新采样,实验就不可复现。建议手工挑一帧图像,把状态向量一起写进目录,之后所有实验都指向同一份文件。目录结构可以这样组织:

runs/frame_0001/
  image.png
  state.json
  meta.json
import json
import numpy as np
from PIL import Image

img = Image.open("raw/cam0.png").convert("RGB")
img.save("runs/frame_0001/image.png")

state = np.asarray([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5], dtype=np.float32)
json.dump({"joint_state": state.tolist()}, open("runs/frame_0001/state.json", "w"))

json.dump({
    "image_size": list(img.size),
    "channel_order": "RGB",
    "state_unit": "rad_or_normalized_unknown",
    "state_dim": int(state.shape[0]),
    "source": "raw/cam0.png"
}, open("runs/frame_0001/meta.json", "w"), indent=2)

meta.json 里的单位字段宁可写 unknown,也不要凭感觉填。图像是 RGB 还是 BGR、归一化在 processor 内部做还是脚本里做,必须先在代码里确认清楚,否则同一张图在不同路径下喂进去会得到不同结果。复用方式:推理脚本统一接收 `--input` runs/frame_0001,任何一次实验都不允许现场抓图。

跑一次单帧推理并原样保存输出

推理脚本只做三件事:加载一次模型、读固定输入、把输出原样落盘。不同仓库的调用方式差异较大,这里给的是通用骨架,模型类和 processor 名称需按仓库实际接口替换。

先跑通单帧推理再谈机械臂联动——MiniCPM-Robot 的最小验证路径
import json
import numpy as np
import torch

# 以下两个加载步骤按仓库实际接口替换
model = load_model(ckpt_path).eval()
processor = load_processor(ckpt_path)

inputs = processor(image="runs/frame_0001/image.png",
                   state=json.load(open("runs/frame_0001/state.json"))["joint_state"])
with torch.no_grad():
    out = model(**inputs)

action = out["action"].float().cpu().numpy()
np.save("runs/frame_0001/action.npy", action)
json.dump({
    "input_id": "frame_0001",
    "action_shape": list(action.shape),
    "action_dim": int(action.shape[-1]),
    "horizon": int(action.shape[-2]) if action.ndim > 1 else 1,
    "dtype": str(action.dtype),
    "normalized": "unknown"
}, open("runs/frame_0001/action_meta.json", "w"), indent=2)

字段含义:action_dim 通常对应机械臂自由度加夹爪,horizon 表示模型一次给出的时间步数,normalized 标记输出是否经过归一化——这一项没从配置或文档里确认前保持 unknown。同一份输入连跑两次,比对 action.npy 是否一致,能区分“模型没加载成功随机初始化”和“输入构造错了”两种情况:前者输出往往完全不稳定,后者通常稳定但量级不对。

人工判断输出是否在合理范围

接机械臂之前,先排除明显异常。检查维度是否与目标自由度一致、是否出现 NaN 或 Inf、数值是否落在物理可能范围内、单位和当前状态是否同量纲。下面这段断言可以放在推理之后直接跑:

import json, math
import numpy as np

a = np.load("runs/frame_0001/action.npy")
state = np.asarray(json.load(open("runs/frame_0001/state.json"))["joint_state"])

assert a.ndim in (1, 2), f"unexpected ndim {a.ndim}"
assert a.shape[-1] == state.shape[0], f"dim mismatch {a.shape[-1]} vs {state.shape[0]}"
assert np.all(np.isfinite(a)), "contains NaN or Inf"
assert not np.allclose(a, 0.0), "all zeros, check model loading"

if np.all(np.abs(a) <= 1.0) and np.any(np.abs(state) > 3.2):
    print("WARN: action looks normalized, state looks like radians")

step = np.abs(a.reshape(-1, a.shape[-1])[1:] - a.reshape(-1, a.shape[-1])[:-1]) if a.ndim == 2 else None
if step is not None:
    print("max step delta:", float(step.max()))

典型异常表现:全零向量多指向权重没加载或前向分支断开;NaN 常见于输入 dtype 或归一化参数不匹配;量级差十倍以上通常是把弧度当角度或把归一化值直接下发;单步跳变过大说明时序上不连贯。关节角按弧度计时通常落在 [-π, π],夹爪常是 0~1 或 0~100,需要结合具体机械臂手册确认,不要按经验直接断言。

从单帧扩到连续控制要补什么

单帧通过只证明链路通,扩到连续控制至少补三处,每处都要单独验证,不要一次性全接上。

  • 时序缓冲:维护一个固定长度的图像与状态滑动窗口,每帧带时间戳入库。验证方式:打印缓冲区长度和首尾时间戳差,确认没有错帧、丢帧或顺序颠倒后再进入推理。
  • 下发频率:先测一次推理耗时,再确定控制周期。模型一次给出多步动作时,中间步通常需要插值或限速,否则关节会出现突跳。验证方式:先对接一个只记日志的假执行器,按固定周期打印下发指令,观察时间间隔是否稳定。
  • 状态回读:每周期读回真实关节角,与上一周期指令比对。验证方式:机械臂静止时回读值应与指令在容差内一致,偏差持续增大说明通信或单位换算有问题,此时应立即停发下一条指令。

这三处的验证顺序建议是缓冲、假执行器、限幅真机。每一步都保留上一阶段可回退的单帧试验入口,出现异常时能快速回到 runs/frame_0001 复现,而不是在整条控制链上猜。