τ₀-VLA 拆解:把「下一个子任务该做什么」变成一道可以加算力的推理题
0. 简介
τ₀-VLA 是上海创智学院(SII)罗剑岚团队在 2026 年 7 月 27 日开源的分层机器人基础模型,面向的是家居场景下十分钟量级的长时域移动操作——备菜、收纳衣物、跨房间整理、双机协同上菜这类需要几十个子任务连续衔接的活。它处理的矛盾很具体:现有分层 VLA 在每一次高层决策上都只花固定的一次前向传播,把当前观测直接映射成下一个子任务,既不比较备选方案,也不评估这些方案会把环境推到什么物理状态,结果是错误决策往往要等到执行已经改变环境之后才被发现。τ₀-VLA 没有沿着「把高层模型做得更大」这条常规路线走,而是把子任务生成重写成一个推理时可扩展的搜索问题:提案模型出候选,世界模型预演每个候选的视觉后果,价值模型给这些想象出来的画面打分,束搜索剪枝,反思模型拍板。低层则是 Qwen3.5-2B 加 Mixture-of-Transformers 动作专家,在统一 40 维状态动作空间上用条件流匹配训练,喂了 40,115 小时异构真机数据。下面结合论文与开源仓库,重点拆解三件事:世界模型为什么从「生成条件」改成了「评测裁判」,长时域记忆如何靠扰动白嫖出监督信号,以及统一 40 维空间与掩码流匹配在代码里究竟怎么落地。论文与项目主页给出方法与实验口径;开源仓库 sii-research/tau-0-vla 给出可以逐行核对的工程实现。
一篇解析写完,读者至少要能自己回答这些:分层 VLA 的瓶颈到底在哪一环,为什么是子任务而不是动作构成了合适的搜索单元;世界模型放在「条件」位置和放在「评测」位置,对系统行为的差别是什么;40 维统一动作空间怎么让同一套权重同时管住单臂固定基座和轮式人形全身控制;以及在什么任务上这套分层加搜索会失效。这几个问题构成了下面各节的骨架。
1. 长时域任务问题是什么
1.1 长时域任务真正的卡点不是"有没有分层"
分层 VLA 这个壳早就不新鲜了。把大任务拆成一句句子任务,上层出语义、下层出动作,这套结构从 SayCan 那批工作开始就在反复出现,到 π₀、GR00T 这代通用模型上已经算标准配置。但把分层接口做出来和把长时域任务跑完,是两件差得很远的事。一个二十分钟量级的家居任务,中间要经过几十次高层决策,只要有一次把"该去拿勺子"判断成"该去放杯子",机器人就会用真实的机械臂把环境推到一个错误状态上,后面所有决策都建立在这个错误状态之上。
1.2 固定计算预算带来的结构性缺陷
问题的根子在于计算预算的分配方式。绝大多数分层系统对每一次高层决策都只给一次前向传播,不管这一步是"继续搅拌"这种几乎没有歧义的推进,还是"眼前三个容器该先动哪一个"这种真正的岔路口。这两类决策的难度差着一个数量级,却拿到了完全一样的算力。更关键的是,一次前向传播的输出没有经过任何形式的比较——模型吐出一个子任务,系统就照着执行,既没有第二个候选拿来对照,也没有任何机制去问一句"如果真按这个做,画面会变成什么样"。
直觉理解(为什么这很致命):这就像开车过一个复杂路口,你既不减速也不看后视镜,凭第一反应打方向。多数时候没事,因为多数路口是直行。但一旦判断错了,代价不是"重新想一遍",而是车已经开到了错误的车道上——你可以重新规划,但已经付出的位移收不回来。长时域任务里的错误子任务就是这个已经付出的位移。
1.3 重新表述:把它变成一道推理题
τ₀-VLA 的切入点是把"生成下一个子任务"这件事,从一次固定成本的映射,重写成一个推理时(inference-time)可以按难度追加算力的推理问题。这个思路在语言模型上已经被验证过——同一个模型,允许它多想几步、多采几条路、再自我评估一遍,答对率能明显往上走。这里的关键是找到合适的搜索单元:动作级搜索能解决局部控制的歧义,但一个动作块只暴露很局部的物理后果;纯语言推理能在长时间尺度上比较,却没把候选跟它会造成的物理状态联系起来。子任务刚好卡在中间,稀疏到值得为它单独花算力,又在时间上足够长,能在环境里造成有意义、可区分的变化。
1.4 用做奶茶这件事把整套机制讲一遍
在进入公式之前,先用论文自己的例子把整条链路走一遍,后面所有符号都能对回这个场景,读到第 4 节的束搜索时可以随时翻回来对照。假设任务是"做一杯奶茶",机器人已经把杯子放好,现在要决定下一步干什么——这正是高层策略每隔一段时间就要面对的那个决策点。
固定预算的做法是:看一眼画面,直接说"去拿勺子",然后动手。τ₀-VLA 的做法是先看自己有多确定——如果模型对"拿勺子"这个判断很有把握,那就直接执行,不浪费算力。如果没把握(比如桌上勺子、杯子、料罐都在手边,先动哪个都说得通),就启动慢思考:先列出三个候选(拿勺子、放杯子、拿杯子),然后对每个候选问一句"如果我真这么干,画面会变成什么样",让世界模型各画一张想象图出来;再让价值模型给这三张想象图打分——论文图里的实际打分是拿勺子 0.95、放杯子 0.50、拿杯子 0.95;保留分高的两支继续往下想一层(拿勺子之后可能是舀料,拿杯子之后可能是放杯子);最后由反思模型看完所有分支的想象结果,拍板说"就拿勺子",交给下层执行。

2. 整体框架
2.1 一次推理步里发生了什么

系统在每个逻辑推理步里顺序跑两个组件。高层策略负责三件事:维护执行记忆、决定这一步要不要动用额外算力、生成当前子任务;低层策略则把当前观测和这个子任务映射成机器人动作。这个分工的意义在于,长时域的进度追踪和具备后果意识的规划全被收进高层,底层的控制接口完全不用改。从上面这张总览图能看出四段式的结构:左边是三类预训练数据(遥操作、自主采集、UMI),中间是带 P→W→V 循环的高层搜索树,往下接低层 VLA,右边列的是能力覆盖——长时域移动操作、精细操作、跨本体泛化。
2.2 形式化定义与关键的不对称设计
把上面这套结构写成公式会更清楚各模块之间的依赖。设任务由语言指令 ℓ \ell ℓ 指定。在推理步 t t t,低层策略接收多视角观测 o t o_t ot、本体感受状态 s t \mathbf{s}_t st、语言条件 c t c_t ct 以及文本形式的控制元数据 η \eta η,输出一个长度为 H H H 的动作块。这里的 η \eta η 是个容易被忽略的输入,它用文本描述本体形式、控制模式和全身配置,正是同一套权重能横跨多种机器人的接口所在:
a t : t + H − 1 = π θ ( o t , s t , c t , η ) \mathbf{a}_{t:t+H-1} = \pi_{\theta}\left(o_t, \mathbf{s}_t, c_t, \eta\right) at:t+H−1=πθ(ot,st,ct,η)
其中 θ \theta θ 是策略参数, H H H 是动作块时域,仓库默认 H = 30 H=30 H=30。直接执行模式下 c t = ℓ c_t = \ell ct=ℓ,也就是把整条任务指令硬塞给低层,让它自己推断当前处在哪个阶段;分层模式下则换成 c t = z t ⋆ c_t = z_t^{\star} ct=zt⋆,即高层生成的子任务。这两种模式共用同一套低层权重,区别只在喂进去的那句话,因此论文能拿"直接执行"当消融基线,用来隔离出分层本身贡献了多少收益。
进一步看高层这一侧。高层策略 μ \mu μ 在每一步先组装决策上下文,再在同一次调用里同时更新记忆并产出子任务。之所以要把上下文单独拎出来命名,是因为后面束搜索里每个分支都要各自维护一份自己的上下文,这个符号会被反复复用,读到束搜索那节时可以回头对照这里的定义:
h t = ( ℓ , M t − 1 , z t − 1 ⋆ , o t ) , ( M t , z t ⋆ ) = μ ( h t ) h_t = \left(\ell,\ \mathcal{M}_{t-1},\ z_{t-1}^{\star},\ o_t\right), \qquad \left(\mathcal{M}_t,\ z_t^{\star}\right) = \mu\left(h_t\right) ht=(ℓ, Mt−1, zt−1⋆, ot),(Mt, zt⋆)=μ(ht)
M t − 1 \mathcal{M}_{t-1} Mt−1 是继承下来的执行记忆, z t − 1 ⋆ z_{t-1}^{\star} zt−1⋆ 是上一步生成的子任务。这里有个容易读漏的细节: M t \mathcal{M}_t Mt 总结的是截至 o t o_t ot 为止已经观察到的历史,因此它并不包含执行 z t ⋆ z_t^{\star} zt⋆ 的结果。记忆永远滞后于当前正要下发的这一步,这个时序关系是后面记忆修正训练能成立的前提。
2.3 两侧模型规模上的不对称
从方法图能读到一个值得注意的配置:高层用的是 Qwen3.5-9B,低层用的是 Qwen3.5-2B。慢的那一侧反而更大,快的那一侧更小,这与"高层只出一句话、低层要出连续动作所以低层更重"的朴素直觉相反。原因不难理解——高层要做的是跨房间、跨阶段的语义推理加上搜索,还要在束搜索里被反复调用采样,语义容量是瓶颈;低层要的是高频出动作,延迟是瓶颈,参数量必须压住。
工程价值:这种不对称直接决定了部署形态。低层必须跑在机器人本体或近端算力上保证控制频率,高层可以放宽延迟、异步流水线化。仓库的
deploy/server.py走的就是 HTTP 策略服务这条路,把低层策略做成可以被独立调用的服务端。
3. 高层策略之一:提案模型与自适应路由
3.1 提案、记忆更新与置信度统计三件事一次做完

推理步开始时,提案模型 P P P 接收上下文 h t h_t ht,在一次前向传播里同时吐出更新后的记忆和一个直接提案。注意这是一次调用产出两样东西,而不是两次调用,这个设计选择在下一小节会解释为什么重要,它同时也是记忆与子任务保持一致的机制来源。写成公式是:
( z t dir , M t ) = P ( h t ) \left(z_t^{\text{dir}},\ \mathcal{M}_t\right) = P\left(h_t\right) (ztdir, Mt)=P(ht)
从方法图 a 面板能看清输出的实际形态:模型先出一段 <think> 思考,再出 Updated Memory(图中是 “Placed Cup”),最后出 Candidate Subtask(图中是 “Pick Up Spoon”)。记忆更新和子任务生成共享同一次前向传播,这不是省算力的小技巧,而是让两者相互约束——模型必须先把"我已经做到哪一步"说清楚,才能在同一段生成里推出"接下来该做什么",两者的一致性由自回归解码本身保证。
3.2 自适应路由:靠 token 置信度决定要不要慢下来
同一次前向传播还顺手产出了路由信号。系统统计输出 token 的平均概率与 logit margin,据此计算一个二值门 g t ∈ { 0 , 1 } g_t \in \{0, 1\} gt∈{0,1}: g t = 0 g_t = 0 gt=0 走快路径,直接把 z t dir z_t^{\text{dir}} ztdir 当作最终子任务; g t = 1 g_t = 1 gt=1 触发测试时计算。这个开关是整套设计里最有落地价值的一环——它让昂贵的预演加搜索只在模型自己觉得没底的时候才启动,简单的推进步骤一遍过。论文提到跨任务使用相同的路由统计量,但在留出数据上为每个任务单独校准阈值。
难点提示(logit margin 是什么):把它想成考试时的"答案确定度"。平均概率高说明模型对整句话每个词都挺有把握;logit margin 大说明第一名候选和第二名候选拉开了明显差距,不存在"两个选项五五开"的纠结。两个指标同时不达标,才说明这一步是真岔路口,值得停下来想。反过来,如果模型一路都很确定,硬要它搜索只是白烧算力。
3.3 记忆修正:靠扰动白嫖出五类监督信号
长时域任务里高层最容易犯的错,是忘了自己干到哪。训练数据里的记忆永远和画面严格对齐,可真机部署时记忆会滞后、会冒进、会记错完全没发生的事。τ₀-VLA 的处理办法相当讨巧,而且完全不需要额外人工标注:只扰动输入端的记忆,目标端的记忆仍然用正确值,于是从现有的分层演示数据里白嫖出五类训练样本——正常推进、该切阶段、记忆落后要追上、记忆冒进要回退、动作失败要先发现再修正。论文表 VI 给出的配比依次是 58%、15%、10%、12%、5%,完整对应关系在 9.5 节列了出来。

这么训出来的效果是,模型学会了不去盲信一段可能出错的历史。记忆在这里不是锦上添花的上下文,而是长时域能不能跑完的地基——最依赖它的是跨房间整理这类任务,去掉记忆之后失败会明显集中在后半程,那些依赖前序进度的环节最先崩。
4. 高层策略之二:世界模型引导的束搜索
4.1 世界模型的位置变了:从生成条件变成评测裁判
这是 τ₀-VLA 与 π₀.₇ 最大的分岔,也是理解这篇工作的关键。π₀.₇ 的世界模型是"生成未来":高层拆出一句子任务,世界模型把它翻译成一张"这一步做完画面该长什么样"的目标图,送给动作专家当条件,让执行更稳。整条链路一次前向走完,不回头。这种用法的好处很实在——目标图给动作专家提供了比一句话更稠密的监督信号,执行阶段的抖动会小很多,延迟也完全可控。
τ₀-VLA 不这么用。高层不会一上来就把下一步定死,而是先提几个候选子任务,让世界模型把每个候选各预演一遍,再让价值模型给这些想象出来的后果打分,在候选里搜一搜,最后由反思模型拍板。换句话说,π₀.₇ 的视觉预测决定"已选定的子任务该怎么执行",τ₀-VLA 的视觉预测决定"该执行哪一个子任务"。同样一个世界模型模块,一个在条件位,一个在评测位,系统行为完全不同。
把这个"预演加打分"的动作写成公式很简单,但两个模型的输入差异需要留意。对搜索中的一个通用候选,设 o ~ \tilde{o} o~ 为头部相机 RGB 图像, z z z 为候选子任务,世界模型 W \mathcal{W} W 与价值模型 V V V 分别做的事情可以用两个式子写完,前者管想象未来画面,后者管给这个画面打分,两者串起来就是一次完整的候选评估:
o ^ = W ( o ~ , z ) , v = V ( ℓ , z , o ^ ) \hat{o} = \mathcal{W}\left(\tilde{o},\ z\right), \qquad v = V\left(\ell,\ z,\ \hat{o}\right) o^=W(o~, z),v=V(ℓ, z, o^)
这里要厘清的是,世界模型始终只吃单张头部相机图像,不吃腕部相机;价值模型则接收全局任务指令
ℓ
\ell
ℓ、候选子任务
z
z
z 和预测的终止图像
o
^
\hat{o}
o^,返回一个标量质量分。从方法图 c 面板能看到实际打分:"Pick Up Spoon" 得 0.95 通过,"Place Cup" 得 0.50 被打叉,"Pick Up Cup" 得 0.95 通过。
4.2 根分支与非根分支的关键区别
束搜索里一个分支 b b b 存三样东西:上下文 h ( b ) h(b) h(b)、有序子任务路径 ρ ( b ) \rho(b) ρ(b)、累积得分 S ( b ) S(b) S(b)。非根分支额外存一张终止预测图像 o ^ ( b ) \hat{o}(b) o^(b)。根分支之所以没有这张图,是因为它立足于真实观测 o t o_t ot,不需要想象。这个区别值得单独强调,因为它决定了搜索树上"真实"与"想象"的边界在哪:
| 根分支 | 非根分支 | |
|---|---|---|
| 视觉输入 | 真实观测 o t o_t ot | 世界模型想象图 o ^ ( b ) \hat{o}(b) o^(b) |
| 子任务路径 | 空序列 | 已累积的想象子任务序列 |
| 初始得分 | S ( b root ) = 0 S(b_{\text{root}}) = 0 S(broot)=0 | 沿路径累计的价值分 |
| 数量 | 每步搜索恰好 1 个 | 展开产生,由束宽 B B B 截断保留 top- B B B |
一句话理解:整棵树只有树根踩在现实里,往下每一层都是模型自己想出来的。所以搜索深度 D D D 越大,叶子节点离现实越远,误差累积越严重——这也是这套方法天然的深度上限来源。
4.3 展开、打分与剪枝的完整递归
在每层深度 d ∈ { 1 , … , D } d \in \{1, \ldots, D\} d∈{1,…,D},对每个保留分支 b b b 独立调用提案模型 N N N 次采样,得到 N N N 个带索引的候选。样本索引 i i i 的作用是把文本内容重复的提案彼此区分开,并把每个提案与它自己的分支局部记忆关联。记 b ⊕ z b \oplus z b⊕z 为把 z z z 追加到 b b b 得到的子分支,则子分支的累积得分与下一次展开用的上下文为:
S ( b ⊕ z t , d b , i ) = S ( b ) + v ( b ⊕ z t , d b , i ) S\left(b \oplus z_{t,d}^{b,i}\right) = S(b) + v\left(b \oplus z_{t,d}^{b,i}\right) S(b⊕zt,db,i)=S(b)+v(b⊕zt,db,i)
h ( b ⊕ z t , d b , i ) = ( ℓ , M t , d b , i , z t , d b , i , o ^ ( b ⊕ z t , d b , i ) ) h\left(b \oplus z_{t,d}^{b,i}\right) = \left(\ell,\ \mathcal{M}_{t,d}^{b,i},\ z_{t,d}^{b,i},\ \hat{o}\left(b \oplus z_{t,d}^{b,i}\right)\right) h(b⊕zt,db,i)=(ℓ, Mt,db,i, zt,db,i, o^(b⊕zt,db,i))
剪枝按累积得分做全局排序, Q t , d = Top B ( A t , d , S ) \mathcal{Q}_{t,d} = \operatorname{Top}_B\left(\mathcal{A}_{t,d}, S\right) Qt,d=TopB(At,d,S)。第一层展开产生 N N N 个子节点,之后每层最多产生 B N BN BN 个。一个必须强调的工程约束是,搜索过程中产生的所有记忆都是分支局部的,从不覆盖持久执行记忆 M t \mathcal{M}_t Mt——否则想象出来的分支会污染真实进度追踪,这是整套设计里最容易写错的地方。
4.4 反思模型:不受候选集合约束的最终一步
搜索结束后,最终束被汇总为 C t = ( ( ρ ( b ) , o ^ ( b ) , S ( b ) ) ) b ∈ Q t , D \mathcal{C}_t = \left(\left(\rho(b), \hat{o}(b), S(b)\right)\right)_{b \in \mathcal{Q}_{t,D}} Ct=((ρ(b),o^(b),S(b)))b∈Qt,D,也就是每个保留分支的"子任务路径加想象终局图加累积得分"三元组。反思模型 F F F 以这个摘要和与观测对齐的真实上下文 h ˉ t = ( ℓ , M t , z t − 1 ⋆ , o t ) \bar{h}_t = \left(\ell, \mathcal{M}_t, z_{t-1}^{\star}, o_t\right) hˉt=(ℓ,Mt,zt−1⋆,ot) 为条件,生成最终子任务 z t ⋆ = F ( h ˉ t , C t ) z_t^{\star} = F\left(\bar{h}_t, \mathcal{C}_t\right) zt⋆=F(hˉt,Ct)。注意 h ˉ t \bar{h}_t hˉt 里用的是真实观测 o t o_t ot 而不是任何想象图,反思这一步重新把脚踩回了地面。
这一步的设计有个容易被忽略的自由度:反思模型的输出可以复现某个保留提案,但并不局限于候选集合——它可以在看完所有分支的想象后果之后,写出一句候选里根本没出现过的子任务。同时反思模型不更新持久执行记忆,记忆的唯一写入方是提案模型。这两条约束合起来,让搜索既能纠错又不会把状态搞乱。
4.5 把两种世界模型用法画在一起

这张图想说清的是结构差别而非优劣。条件位的好处是延迟可控、实现简单,评测位的代价是每一步最多要多跑 N × D N \times D N×D 次世界模型与价值模型。τ₀-VLA 用自适应路由把这个代价压在真正需要的步骤上,本质是拿"偶尔慢一下"换"少走一次错路"。换句话说,这不是一个免费的改进,而是一次明确的算力换正确率的交易,交易是否划算完全取决于任务里岔路口的密度。
5. 低层策略之一:统一 40 维状态动作空间
5.1 一个固定布局管住所有本体
跨本体策略必须先解决一件麻脏事:不同机器人的控制接口完全不一样。单臂 Franka 只有七个关节加一个夹爪,轮式人形要管两条臂、两个夹爪、腰部还有底盘速度。τ₀-VLA 的做法是定义一个固定 40 维布局,每个槽位的语义在所有机器人之间永久固定,不填的槽位零填充并把掩码位置 0。仓库里这段布局写得非常直白:
# src/tau0_vla/data/robots/unified.py
"""Unified cross-embodiment action/state layout.
Defines a fixed 40D vector layout where each position has permanent semantic
meaning across all robot types. Robots that don't populate a given slot
(e.g. no gripper, no EEF) zero-pad it and set the corresponding mask bit to 0.
Layout:
[0:3] left_eef_position (xyz) meters
[3:9] left_eef_orientation rot6d (from euler/quat)
[9:12] right_eef_position (xyz) meters
[12:18] right_eef_orientation rot6d
[18:19] left_gripper per-type normalized
[19:20] right_gripper per-type normalized
[20:22] waist per-type normalized
[22:24] chassis_velocity per-type normalized
[24:32] left_arm_joints (max 8D) rad
[32:40] right_arm_joints (max 8D) rad
"""
UNIFIED_DIM = 40
LEFT_EEF_POS = slice(0, 3)
LEFT_EEF_ORI = slice(3, 9)
RIGHT_EEF_POS = slice(9, 12)
RIGHT_EEF_ORI = slice(12, 18)
LEFT_GRIPPER = slice(18, 19)
RIGHT_GRIPPER = slice(19, 20)
WAIST = slice(20, 22)
CHASSIS_VELOCITY = slice(22, 24)
LEFT_ARM = slice(24, 32)
RIGHT_ARM = slice(32, 40)
几个设计细节值得单独拆开看。姿态用的是 rot6d 而不是四元数或欧拉角,六维连续表示避免了万向锁和四元数双覆盖带来的优化障碍;单臂关节留了 8 维上限,覆盖七自由度加冗余;腰部两维、底盘速度两维,这两块正是全身控制与固定基座控制的分界线——填了就是全身,没填就是定座。这个接口的价值在于,同一套权重不需要为每种本体各配一个输出头,控制参数化的差别全部由文本形式的控制元数据 η \eta η 加掩码来表达。
难点提示(为什么不能直接把所有关节拼起来):如果只是简单拼接,那么第 25 维在 A 机器人上是"左肘"、在 B 机器人上可能是"右腕",网络学到的每一维语义会随样本漂移,等于在同一个输出位置上叠加了互相冲突的监督。固定语义槽位的作用就是钉死这个对应关系,代价只是浪费一些永远为零的维度——用显存换语义一致性,这笔交易在跨本体训练里非常划算。
5.2 相对化处理上的分块差异
仓库里还有一处容易读漏但很重要的设计。绝对动作在跨本体训练里不好用,因为同一个世界坐标在不同机器人的工作空间里含义不同,所以要转成相对量。但同样是"把绝对动作转成相对量",EEF 块和关节块的处理方式并不一样,注释里写得很明确:
# src/tau0_vla/data/robots/unified.py
# 注释节选:说明相对化在不同语义块上的差异
# cross-embodiment aligned (Cartesian pose), whereas the arm slots [24:40] are
# - Arm blocks ([24:32], [32:40]): plain ``action - state`` per active dim.
``action`` is ``[*horizon, 40]``, ``state`` is ``[40]`` (absolute, scattered,
臂关节块走的是逐维直接相减,因为关节角本身就是标量、相减有明确物理意义。EEF 块则涉及笛卡尔位姿,位置和 rot6d 姿态不能简单相减,需要按位姿代数处理。这里的关键是,笛卡尔位姿在不同本体之间是天然对齐的(世界坐标系下一米就是一米),而关节角不对齐(同样 0.5 弧度在不同臂长上意味着完全不同的末端位移),所以这两块在跨本体迁移里承担的角色本来就不同。
6. 低层策略之二:掩码条件流匹配
6.1 MoT 动作专家的耦合方式
低层把视觉语言骨干与一个 Mixture-of-Transformers 动作专家耦合在一起。耦合的粒度是每一层全注意力层:动作 token 与骨干 token 在每层做联合注意力,但分别经过参数独立的 Transformer 分支处理。这与"骨干跑完再接一个动作头"的串联式设计有本质区别——动作 token 从第一层开始就能看到视觉语义,视觉 token 也能被动作查询反向影响,而两侧的前馈参数互不共享,避免了动作回归任务把语言能力冲掉。
6.2 掩码流匹配:让无效通道恒等于零
由于不同本体在 40 维空间里占据的通道不同,训练时必须对流路径和损失同时施加掩码。设 d a d_a da 为动作维度, M ∈ { 0 , 1 } d a × d a \mathbf{M} \in \{0,1\}^{d_a \times d_a} M∈{0,1}da×da 为对角动作掩码矩阵,通道 i i i 激活时 [ M ] i i = 1 [\mathbf{M}]_{ii} = 1 [M]ii=1,否则为 0。对动作块中每个动作向量使用同一个矩阵。采用反时间参数化,流时间 τ ∈ [ 0 , 1 ] \tau \in [0,1] τ∈[0,1] 在整个动作块上共享, τ = 1 \tau = 1 τ=1 是噪声, τ = 0 \tau = 0 τ=0 是干净动作:
a t + j τ = τ M ϵ t + j + ( 1 − τ ) M a t + j , u t + j = M ( ϵ t + j − a t + j ) \mathbf{a}_{t+j}^{\tau} = \tau \mathbf{M} \boldsymbol{\epsilon}_{t+j} + (1 - \tau)\, \mathbf{M} \mathbf{a}_{t+j}, \qquad \mathbf{u}_{t+j} = \mathbf{M}\left(\boldsymbol{\epsilon}_{t+j} - \mathbf{a}_{t+j}\right) at+jτ=τMϵt+j+(1−τ)Mat+j,ut+j=M(ϵt+j−at+j)
公式看着平淡,落到代码里却有个值得学习的取舍。仓库里对应的实现在 modeling_tau_vla.py 的 forward 中,那段注释把三种可选方案的比较过程完整交代了出来,是整个仓库里信息密度最高的注释之一,值得逐行读完再看下面的解读,尤其是那句关于恒零模式的说明:
# src/tau0_vla/models/vision_language_action_models/tau0_vla/modeling_tau_vla.py
if noise is None:
noise = torch.randn(actions.shape, device=device, dtype=dtype)
# Scheme C (config.vla_inactive_input_zero): zero the noise on inactive
# action dims so x_t = t*noise + (1-t)*action is EXACTLY 0 there (the
# dataloader already zeroes action on inactive dims). The constant-0
# pattern is an explicit per-sample mask signal to the network,
# replacing scheme A's t*noise nuisance input, and matches the per-step
# re-zeroing in sample_actions. u_t on those dims becomes 0 and is
# excluded by the loss mask anyway.
if action_mask is not None:
am = action_mask.to(device=device, dtype=dtype).reshape(actions.shape[0], 1, -1)
if am.shape[-1] < actions.shape[-1]:
am = torch.nn.functional.pad(am, (0, actions.shape[-1] - am.shape[-1]))
noise = noise * am
if time is None:
time = self.sample_time(actions.size(0), device).to(dtype)
time_expanded = time[:, None, None]
x_t = time_expanded * noise + (1 - time_expanded) * actions
u_t = noise - actions
这段代码里最值得说的是那个被称作 Scheme C 的选择。最朴素的做法(注释里的 scheme A)是只在损失上打掩码,让无效通道的输入照常是
τ
ϵ
\tau \boldsymbol{\epsilon}
τϵ 这种随机噪声。问题是这些噪声会经过 action_in_proj 进入网络,成为一堆纯粹的干扰输入。Scheme C 把无效通道的噪声也乘零,于是
x
t
x_t
xt 在这些通道上恒等于 0,这个"恒零"的模式本身就成了一个显式的逐样本掩码信号——网络看到某几维一直是 0,就知道当前样本这些通道不用管。一个改动同时干掉了干扰输入并提供了掩码提示。
直觉理解(恒零比随机噪声好在哪):想象你在填一张有 40 个空格的表格,其中 16 个格子这次不需要填。方案 A 是在这 16 个格子里随机写乱码,然后告诉你"改卷时我不看这些格子"——你还是得费精力去分辨哪些是乱码。方案 C 是这 16 个格子一律留空白,你一眼就知道它们不参与。后者不仅少了噪声,还顺便把"哪些格子有效"这个信息免费告诉了你。
6.3 时间采样为什么偏向噪声端
训练时每个样本要抽一个流时间 τ \tau τ,抽样分布的选择直接影响模型在哪个时间段学得更好。τ₀-VLA 用的不是最朴素的均匀分布,而是一个 Beta 分布加仿射变换,只有三行代码,但分布形状、参数取值和那个仿射变换每一项都有讲究,下面逐项拆开讲清楚它们各自解决的是什么问题:
# src/tau0_vla/models/vision_language_action_models/tau0_vla/modeling_tau_vla.py
def sample_time(self, bsize, device):
time_beta = sample_beta(1.5, 1.0, bsize, device)
time = time_beta * 0.999 + 0.001
return time.to(dtype=torch.float32, device=device)
Beta(1.5, 1.0) 的密度随取值增大而上升,因此采样偏向
τ
\tau
τ 较大的一侧,也就是更靠近噪声的那一端被采得更密。加上
×
0.999
+
0.001
\times 0.999 + 0.001
×0.999+0.001 的仿射变换,把范围钳在
[
0.001
,
1.0
]
[0.001, 1.0]
[0.001,1.0] 之间,避开
τ
=
0
\tau = 0
τ=0 处的数值奇点。这里的关键是速度场在不同时间点上的难度不一样:靠近纯噪声的一端,模型要从几乎没有信息的输入里预测出动作方向,这段最难学,也最影响最终采样质量,所以值得多分配样本。
6.4 训练目标
有了掩码流路径和时间分布,训练目标就很直接了。流匹配损失本质上是速度场的回归误差——让模型预测的速度去逼近真实的
ϵ
−
a
\boldsymbol{\epsilon} - \mathbf{a}
ϵ−a,仓库支持 MSE 和 L1 两种度量,由配置里的 loss_type 切换。写成带掩码的期望形式是这样,其中掩码只让激活通道参与计算:
L fm = E τ , ϵ [ ∥ M ⊙ ( u t : t + H − 1 − v ϕ ( a t : t + H − 1 τ , τ , o t , s t , c t , η ) ) ∥ 2 ] \mathcal{L}_{\text{fm}} = \mathbb{E}_{\tau,\ \boldsymbol{\epsilon}} \left[\ \left\| \mathbf{M} \odot \left(\mathbf{u}_{t:t+H-1} - \mathbf{v}_{\phi}\left(\mathbf{a}^{\tau}_{t:t+H-1},\ \tau,\ o_t,\ \mathbf{s}_t,\ c_t,\ \eta\right)\right) \right\|^2\ \right] Lfm=Eτ, ϵ[ M⊙(ut:t+H−1−vϕ(at:t+H−1τ, τ, ot, st, ct, η)) 2 ]
其中 v ϕ \mathbf{v}_{\phi} vϕ 是动作专家预测的速度场, M ⊙ \mathbf{M} \odot M⊙ 表示只在激活通道上计入损失。注意期望是同时对流时间 τ \tau τ 和噪声 ϵ \boldsymbol{\epsilon} ϵ 取的,动作块内所有 H H H 个时间步共享同一个 τ \tau τ,动作专家一次性联合预测所有 H H H 个动作 token 的速度,而不是逐步自回归地出。这个目标落到代码里只有短短几行,但有个细节必须看清楚:
# src/tau0_vla/models/vision_language_action_models/tau0_vla/modeling_tau_vla.py
suffix_out = suffix_out[:, -self.config.n_action_steps :]
if suffix_out.dtype != self.action_out_proj.weight.dtype:
suffix_out = suffix_out.to(self.action_out_proj.weight.dtype)
v_t = self.action_out_proj(suffix_out)
if loss_type == "fm":
fm_losses = F.mse_loss(u_t, v_t, reduction="none")
elif loss_type == "L1_fm":
fm_losses = F.l1_loss(u_t, v_t, reduction="none")
return fm_losses
这里最该留意的是返回的是逐元素损失而不是标量,reduction="none" 把归约推迟到了外层,这样掩码加权才能在正确的粒度上进行。如果在这里就直接求平均,无效通道上的零误差会被算进分母,等于把损失按"总维度"而非"有效维度"归一化——单臂样本的梯度会被系统性地稀释掉一半以上。这是个小细节,但写错了整个跨本体训练的样本权重就全乱了。
7. 推理侧:欧拉积分与无效通道的逐步重置
7.1 十步反向积分
训练学的是速度场,推理时要把这个场积分成真正的动作。τ₀-VLA 用的是最简单的欧拉法,默认 num_steps=10,步长为负——因为反时间参数化下时间是从 1(纯噪声)走向 0(干净动作)的。这段循环里真正的关键不是积分本身,而是夹在中间那次看似多余的掩码重置操作,它决定了无效通道会不会漂移:
…详情请参照古月居
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)