这一页讲清楚 B0 的逻辑:现在的模型到底在学什么、为什么它换个杯子就不行、 B0 在机制上改了哪一步、以及在什么条件下应该判它「不做」。 接口部分已经读通并实测过;耗时部分还没测,那是能不能做的唯一决定项。
所有逻辑都从这一步来。SmolVLA 每次推理,输入端是这样拼起来的
(modeling_smolvla.py:552 的 embed_prefix,读过源码):
embs = []
for img in images: # 三路相机
embs.append(self.vlm_with_expert.embed_image(img)) # 每路 64 个 token
embs.append(lang_emb) # 语言:"Pick up the cup with the right arm."
embs.append(self.state_proj(state)) # 17 维关节 → 960 维,1 个 token
embs = torch.cat(embs, dim=1) # 拼成 prefix,action expert 在上面做注意力
关键在那 192 个视觉 token 是怎么来的:512×512 的图,切成 16×16 的 patch 得 32×32=1024 块,
再经 scale_factor=4 的 pixel shuffle 压 16 倍 → 每路 64 个,三路共 192 个。
B0 不动那 192 个方格 token,而是在它们之外再加 N 个物体 token:
object_proj 投到 960 维,append 进上面那个 list模型于是同时拿到两样东西:方格里的纹理(原来就有),以及「这儿有一个物体,它在这个位置」。 后者对外观变化不敏感——磨砂杯和透明杯,检测器都只报「这儿有个物体」,位置一样,token 就一样。
学的东西从「画面第 137 号格子亮起来时该动」变成「那个物体在哪」。这是 B0 的全部内容。
2026-09-04 把四个分支逐一排除:
| 方案 | 结论 | 依据 | |
|---|---|---|---|
| B0 | 物体 token 当结构先验 | 主攻 | 不需要内参、不需要手眼、不需要重采数据 |
| B1 | 深度在策略外用(记杯位、做诊断) | 降级 | 最便宜的那层诊断当天用已有轨迹文件免费拿到了;剩下的要手眼 |
| B2 | Depth Helps 式注入 | 搁置 | 深度必须以原生格式重录,现有数据是旧旁路格式 |
| B3 | 点云策略(DP3/iDP3) | 已排除 | 换策略家族,整条链路重来,操作者判定不可能 |
这是判断 B0 值不值最关键的一条区分。之前的表述把两者混为一谈,是错的:
| 换杯子 换一只不同外观的杯子,位置不变 | 换位置 同一只杯子挪到新位置 | |
|---|---|---|
| 需要的关节轨迹 | 和数据里的完全一样 | 数据里可能根本没有 |
| 受 35° 覆盖限制吗 | 不受 | 受 |
| B0 能不能解决 | 能,这正是它对口的问题 | 不能 |
| 要不要新数据 | 不要 | 要 |
换杯子是外观泛化:同一个位置换一只杯子,动作一模一样,变的只是画面。 B0 把「这个位置有个物体」和「这个物体长什么样」拆开,模型学前者——正是 B0 对口的问题,且不需要任何新数据。
换位置才受数据覆盖限制。实测(合并集 202 集,方法见文末):
embed_prefix 就是一个 list 最后 concat,插物体 token 等于再 append 一项 [B, N_obj, 960]:
# 新增(照抄 state_proj 的写法,它在 modeling_smolvla.py:508)
self.object_proj = nn.Linear(obj_feat_dim, 960)
# embed_prefix 里,state 之后
embs.append(self.object_proj(object_feats)) # [B, N_obj, 960]
连带要改的两处(不改会静默出错):
pad_masks——检出数量每帧不同,缺的要 pad 并标掩码att_masks——这一项决定成败,见下policies/common/vla_utils.py:87-90 里 att_masks 不是开关,是块边界标记:
cumsum 给出块号,token i 能看到 token j ⟺ 块号(j) ≤ 块号(i)。
现在图像和语言是块 0、state 是块 1(源码注释:"image and language inputs do not attend to state or actions")。
把物体 token append 在最后并标 att_mask=1(块 2),图像和语言就看不到它,
它们的输出逐比特不变——不是「影响很小」,是数学上完全不变。
prefix_length 的 padding 分支」这条顾虑不存在,可以划掉。
部署 checkpoint 的 config.json 里 prefix_length = 0,
而代码是 if seq_len < self.prefix_length——永远不会触发。加 token 不涉及 padding 或截断。N_obj 必须固定。
att_masks 是 python list,att_masks[None,:].expand(bsize,-1) 对整个 batch 共享同一长度。
检出不足时用 pad_masks=0 补;pad_2d_masks 会把 padding token
在 key 和 query 两侧都屏蔽,所以空物体不污染任何东西。另外两处第 0 步就会发生、但不致命的影响:action expert 必然受影响(那正是目的,多了 N 个 key,
softmax 分母变了);object_proj 随机初始化会在训练最初喂噪声——
把最后一层零初始化可缓解,但即使 value 全零,多出的 key 仍参与 softmax 归一化、会稀释原有权重,
所以「零初始化 = 恒等」是错的,仍需 warmup。
config.json 是
freeze_vision_encoder: True、train_expert_only: True、train_state_proj: True——
现在就只训 action expert,SigLIP 一个参数不动。加一个 object_proj 不用解冻主干。
而且物体 token 可以从已录的 RGB 离线算,所以不用重采数据。
按现有配置重训约 1.3 小时 / $1。Oat-VLA 用 N 个物体 token 取代原来的视觉 token,图的是推理变快。 我们实测过,那个动机在我们这儿不成立:
| 做法 | 代价 | 风险 | |
|---|---|---|---|
| 加法(选它) | 192 个视觉 token 之外再加 N 个 | 推理略慢 | 低,预训练视觉通路完全不动 |
| 替换 | 用 N 个物体 token 取代 192 个 | 推理变快 | 高,丢掉预训练的视觉对齐 |
逻辑很简单:替换的唯一好处是速度,而速度收益已经被实测否掉; 那就只剩下「丢掉预训练视觉对齐」这个确定的风险,去换一个可能为负的收益。加法保留 B0 真正那条理由(泛化),不去赌那个不显著的数字。
算力预算:每 1667 ms 的动作,扣掉 SmolVLA 自己的 1329 ms,只剩 337 ms。
| 实测单帧耗时 | 结论 |
|---|---|
| < 150 ms | 可行,每帧都跑 |
| 150 – 337 ms | 勉强,要考虑降频(不必每帧都跑) |
| > 337 ms | B0 直接否掉,除非改成每 N 帧跑一次 |
train310 环境。lerobot 那个 torch 是
CPU-only(实测 torch.cuda.is_available() 为 False),用错环境会量出一个假的「跑不动」然后错误地否掉 B0。候选优先级:先量 OWLv2——不是因为它更好,而是因为我们已经在自己数据上验过它 (24/36 检出,阈值 0.30 时非杯误检为 0)。它出的是框不是掩码,但框也完全够当物体先验用。 FT-Dinosaur 是 Oat-VLA 的选择,更重,作为第二顺位。
先在不加载真实物体 token(喂零向量)的情况下跑通训练一步,确认形状和 mask 都对。 形状对了再接检测器——两个错混在一起最难查。
把 T1 的耗时、T2 的实现代价、训练成本放一起,给出「做 / 不做」。 允许的结论包括「不做」——如果 T1 就否掉了,把结论写下来,别硬上。
| 当时用的 | 应该是 | 为什么要紧 | |
|---|---|---|---|
| 任务指令串 | ...with the left arm. | ...with the right arm. |
现役模型训练集 tasks.parquet 里只有右臂那一句。SmolVLA 是语言条件模型,喂 left = 它没见过的任务 |
| speed | 0.3 | 1.0 | 把安全层步长上限乘了 0.3,实测削掉 shoulder_lift 的 11–20%——正是「放下去够杯子」那个关节 |
它们不是例行统计,是两个证伪实验,查的是两个不同的东西,不能互相替代:
| 要出的表 | 查什么 | 证伪谁 | 判据 |
|---|---|---|---|
| raw vs safe 削减 | 安全层砍掉了策略想做的多少动作 | 执行侧:模型想对了但被节流 | speed 1.0 后削减从 11–20% 掉到接近 0 |
| 夹爪开合次数 | 策略输出本身连不连贯 | 认知侧:模型压根没想对 | 从 1、1、2、3 回到恒为 1 |
削减是拿 raw(安全层之前)和 safe(放行后)比,差值完全归安全层。
开合次数只看 raw,那是策略的原始输出、安全层碰不到,所以它是纯粹的「模型在想什么」的读数。
| 试跑(09-04 四次) | 训练示范(同一只臂 40 集) | |
|---|---|---|
| 一次里开合几次 | 1、1、2、3 | 中位 1,最大 1 |
| 只闭合 1 次的比例 | 50% | 100% |
示范里 40 集全是「张开 → 闭合一次 → 提起」,没有一集有第二次开合。 试跑里一半出现多次开合——这是反复试探的形状,和「喂了没学过的指令串」高度吻合。
四种结果对应四个不同结论:
| 重跑 10 次的结果 | 该得出的结论 |
|---|---|
| 削减降了 + 开合回到 1 + 成功率上去 | 两个输入框就是全部原因,B0 的紧迫性下降(但泛化那条理由仍在) |
| 开合回到 1,但还是抓不到 | 指令串确实错了,模型能力也确实不够 → B0 / 补数据有了明确动机 |
| 开合还是 2、3 次 | 指令串不是原因,得另找(视角?训练不足?) |
| 全都正常,成功率还是 0 | 这才轮到「模型不行」——而且是在最有利条件下都不行,比现在强得多的证据 |
tctlRun() 在指令不一致时直接阻止实跑启动。空跑和重放不拦。| 结论 | 等级 | 怎么来的 |
|---|---|---|
embed_prefix 是 list + concat,约 15 行可插入 | 读过源码 | modeling_smolvla.py:552 |
| 我们的视觉 token 是 192 不是 256 | 按配置推算 | 512×512 / patch 16 / scale_factor=4,三路 |
| 推理 1329 ms,视觉占 13% | 实测 | 本机计时 |
| 视觉编码器本来就冻结 | 读过配置 | 部署中的 config.json |
| 右臂横向覆盖 35.1°,std 5.15 | 实测 | 合并集 202 集逐集提取,方法见下 |
| 试跑开合 1/1/2/3 vs 示范恒为 1 | 实测 | 四条轨迹 + 40 集示范,同一个函数算 |
| 安全层削掉 11–20% | 实测 | traces/*.npz 的 raw vs safe |
| Oat-VLA 成功率不显著(p=0.106) | 我们自己重算 | 按它自己给的 29/49 vs 20/49 做 Fisher |
| OWLv2 24/36 检出、阈值 0.30 非杯误检 0 | 实测 | 我们自己的数据 |
| 检测器在 Jetson 上的单帧耗时 | 未测 | T1,这是唯一的决定项 |
| 202 集里用了几种杯子 | 未查 | 见第 9 节 ② |
数据覆盖:读合并集 data/**/*.parquet,按 episode_index 分组,
找每条臂夹爪值第一次降到 (max+min)/2 以下的帧,取当时的五个臂关节。
合拢时夹爪就在杯子上,所以这个构型代表杯位——不需要手眼标定。
开合次数:取夹爪序列,以 min+0.25*range 和 min+0.75*range 为双阈值做迟滞计数,
统计 open→closed 的转换次数。示范必须用同一个函数算,才可比。
merge_sources.json 第一个来源都是 0826,
xlerobot-cup-grasp-20260820-0230 没有进去,所以本页所有基于合并集的数字都成立。
但这是碰巧对的,不是有意为之——以后加数据、做示范对照、算统计量之前,都要先确认数据集是否跨 0826。B0 是现在唯一不需要标定、不需要重采数据的策略改进方向,接口已经确认可行; 但它唯一站得住的理由是「换杯子」的泛化,不是速度,也不是「换位置」。 动手之前先花五分钟排除那两个填错的输入框;动手之后要记住, 数据覆盖只有 35° 的横向范围,架构变不出数据里没有的东西。