做机器人操作或者多模态模型的人这两年对 VLAVision-Language-Action视觉-语言-动作这个词应该都不陌生了。模型输入一张图加一句指令输出一串动作看似顺理成章实际用过的都知道问题在哪VLA 基本是“一眼定动作”的单步决策错了就错了没有任何回头修正的机会。我最近在折腾的方向就是给 VLA 接入递归-深度的潜迭代推理让它在真正输出动作之前先在潜空间里反复迭代若干轮把“测试时计算”这笔账在模型内部悄悄算掉。这篇文章把我自己的拆解思路、实现方法、关键参数和踩过的坑完整记录下来给同样被这个问题困扰的同学一个可以参考的落地方案。1. 为什么要在 VLA 模型里做“隐测试时计算”1.1 VLA 现在缺的不是参数量是思考时间VLA 模型的典型工作方式很简单视觉编码器吃图像文本编码器吃指令中间经过一个融合模块把视觉和语言特征揉在一起最后由动作头输出连续动作或离散动作 token。从训练思路上看这本质上是在做“从感知到动作的直接映射”。单步拿结果快是真快但代价是放弃了一切“先想一想再动手”的可能性。真实机器人操作任务里问题从来不是“看到红色杯子就抓”而是目标识别、空间位姿估计、遮挡关系推理、多步骤规划这些纠缠在一起。打个比方现在的 VLA 像一个被要求秒答的面试者题目念完必须马上给结论嘴比脑子快。让它看一眼图就直接输出动作遇到简单场景没问题遇到“把微波炉里的盘子拿出来放到左边沥水架上”这种活儿它没有时间在内部把“微波炉门是否打开”“盘子取出的方向”“沥水架的空位在哪”这几个子问题挨个过一遍结果就是动作头给出一个看着合理、实际根本执行不了的结果。我早期在模拟项目 X 上做机器人抓取实验时统计过单步 VLA 在简单摆盘任务上的成功率能到 80% 以上但任务一旦换成“多物体遮挡 指定目标 指定放置区域”成功率直接掉到五成附近。后来把模型输出打印出来看发现很多失败案例里模型其实已经“看对了”物体却在动作方向上犯了低级错误。这说明瓶颈不在感知不在特征提取而在于模型缺少一次“推理修正”的机会。1.2 测试时计算在语言模型上的老套路搬到机器人上为什么不灵语言模型领域这几年最明显的趋势之一就是测试时计算test-time compute的引入。简单说就是模型在真正给出答案之前多花一些算力去搜索、采样、自我纠错。常见套路包括采样多个候选答案再投票、让模型先生成推理过程再出结论、训练一个验证器对多个输出打分重排。这些方法的核心逻辑都一样单次前向传播往往太“冲动”多算几次就能大幅提升可靠性。这套逻辑搬到 VLA 上第一反应肯定是我能不能也让 VLA 多采样几条动作轨迹挑一条最好的理论上可行实操中非常难受。首先是成本问题。一条动作轨迹不是单个 token而是几十上百个动作 token 的序列采样十条轨迹就要做十次完整解码决策延迟直接爆炸。其次是验证问题。语言模型的候选答案可以靠规则或模型打分动作轨迹怎么打分只能靠人工标注或奖励模型但机器人动作数据本身就稀缺训一个可靠的动作验证器比训 VLA 本身还难。最后是安全问题。机器人是要在物理世界里动胳膊的一次采样出错顶多是回答错动作采样出错直接可能把杯子扫到地上。所以语言模型那套“显式测试时计算”不能照搬至少不能直接全部照搬。真正的问题是能不能在不生成额外候选、不写中间文本、不加验证器的情况下让模型在内部多做几次计算把答案“想”得更稳一点。1.3 “潜迭代”到底潜在哪里所谓“潜迭代”是把额外的计算放到模型的潜空间表示里而不是放到显式的文本、动作或轨迹空间里。具体来说模型在拿到图像和指令后先把多模态信息融合成一个高维向量或一组 token然后让一个自带的“思考模块”在这个潜表示上反复迭代。每迭代一次潜表示就被精炼一次直到迭代结束时动作头才从最终潜表示上读取动作输出。“隐测试-时计算”的关键点就在这里额外计算从外部看完全不可见没有多生成一个字也没有多采样一条轨迹但模型内部确实多做了几次计算。这也是标题里“隐”字的含义——算力花在模型肚子里的潜空间中表面看起来还是一个输入一个输出延迟增加主要来自那几次内部迭代。这个思路的另一个名字是 latent iterative reasoning在 LLM 上已经有不少人试过但搬到 VLA 上有个天然优势VLA 的动作头本来就是一个低维数值输出模块潜表示的精炼可以直接传导到动作数值上不需要额外转译成文本再翻译回动作少了一层信息损耗。2. 递归-深度潜迭代的架构思路2.1 递归深度不是堆更多层是复用同一个“思考模块”“递归-深度”这个词初看有点唬人拆开看很朴素。“递归”指的是同一个潜迭代模块被反复调用每次调用的参数完全相同。“深度”指的是因为反复调用模型在时间维度上变深了潜表示经过了很多步变换。整个结构很像把一个 RNN 或深层残差网络嵌套在 VLA 的中间只不过这次被反复执行的模块不是简单的全连接层而是一个包含自注意力、交叉注意力和前馈网络的完整 Transformer Block。为什么要强调复用同一个模块而不是干脆堆叠多个不同参数的模块第一是参数效率。VLA 模型本身已经很大视觉编码器和融合模块占了绝大多数参数量潜迭代部分如果每个深度一步都用不同参数训练和显存压力都会成倍增加。复用参数则几乎没有额外参数量只需要一个思考模块加一个迭代位置编码。第二是算子惯性。同一个模块反复执行意味着模型被迫学习一个稳定的、可重复的潜状态更新规则。这种共享权重的结构化设计自带正则效果模型很难“记死”某个特定深度的输出而必须学会对不同深度的潜状态都做出合理响应。打个比方递归-深度像是让同一位老师反复批改同一份答卷每一轮都指出问题并让学生修改。堆叠不同参数则像是每轮都换一个老师每个老师都要从头熟悉这份答卷学到的知识碎片化很难收敛。2.2 潜状态更新方程代码怎么写这件事放到后面先把核心的数学形式讲清楚。设融合模块输出的初始潜状态为 h0它由视觉特征、语言指令和动作历史共同编码得到。潜迭代模块 F 在每一步 k 执行h(k) h(k-1) g(k) · F(h(k-1), c; θ)其中 c 是融合后的上下文特征在整个迭代过程中保持不变θ 是思考模块的共享参数g(k) 是一个门控系数可以是可学习的也可以固定为随迭代递减的常数。这个形式和 Deep Equilibrium Model深度平衡模型非常像本质上是把网络深度从“结构上的几十层”变成了“计算上的若干步”。残差连接在这里极其重要。如果没有这个 h(k-1) 的恒等连接潜状态经过十几步迭代后很容易发生漂移要么特征范数爆炸要么所有维度收敛到同一个值也就是所谓的表示坍缩。实践经验是门控系数的初值要小一点。我习惯把 g(k) 初始化为 0.1 左右让前几次迭代只做微调后几次迭代逐步放大更新幅度。这样训练初期潜状态不至于剧烈跳动后期又保留足够的表达能力。迭代结束后的动作输出则很简单a ActionHead(h(K))K 是推理时设定的迭代次数可能是一个固定值也可能是一个动态停止阈值。这个动作头和普通 VLA 的动作头可以完全一样不需要额外改动。2.3 为什么动作头能受益于额外的状态精炼动作头本质上是一个浅层的回归器或分类器它的表达能力有限输入给它什么特征它就映射到什么动作。如果输入特征本身包含噪声、包含不完整的空间关系信息动作头再怎么调天花板也很低。多轮潜迭代最大的好处是让动作头在“信息更干净”的表示上做映射。每一轮迭代自注意力都会在潜状态内部重新组织信息图像里目标物体的位置、指令里关键动词对应的约束、历史动作中已经完成的部分这些信息在初始融合特征里可能是“摊开”的经过几轮精炼后会变得更聚拢、更正交、更利于数值映射。我在实验中观察到的一个现象很能说明问题把迭代次数从 1 增加到 8动作头输出的动作序列在数值上变化不大但边界位置的命中率明显提升。比如抓取坐标单步模型经常出现“差 2 到 3 厘米”的情况迭代之后误差显著缩小。原因是多轮迭代让潜状态对空间位姿的编码更细粒度浅层动作头不需要自己强行从模糊特征里挖出精确位置关系。3. 实操把一个 VLA 改造为递归-深度潜迭代版本3.1 整体改法改造一个大体量 VLA 模型没有想象中那么复杂关键改动只有四处融合模块后插入思考模块、增加迭代位置编码、改推理循环、训练时引入变长迭代。我用伪代码表示核心结构class RecursiveDeepVLA(nn.Module): def __init__(self, vision_encoder, text_encoder, fusion, latent_block, action_head): super().__init__() self.vision_encoder vision_encoder self.text_encoder text_encoder self.fusion fusion self.latent_block latent_block # 共享参数的 Transformer Block self.action_head action_head self.iter_pos_emb nn.Embedding(max_iters, hidden_dim) def forward(self, image, instruction, history, num_iters8): # 视觉和语言编码 visual_tokens self.vision_encoder(image) text_feat self.text_encoder(instruction) # 融合得到初始潜状态 fused self.fusion(visual_tokens, text_feat, history) h fused # 潜迭代推理 for k in range(num_iters): iter_emb self.iter_pos_emb(torch.tensor(k)) residual self.latent_block(h iter_emb, fused) gate 0.1 0.9 * min(1.0, k / num_iters) # 门控渐开 h h gate * residual action self.action_head(h) return action这个改法有几个地方值得解释。第一latent_block 的输入是 h 加 iter_pos_emb而不是直接 h。迭代位置编码用来告诉模块“这是第几步”否则不同迭代步的更新规则完全一样模型很难区分第一次精炼和第八次精炼。没有位置编码的版本我个人试过训练 loss 能降但推理时增加迭代次数几乎不涨点原因就在这里。第二latent_block 同时接收 fused 作为上下文。这和交叉注意力类似确保每轮迭代都锚定在原始的观测信息上不会因为多轮递归而逐渐忘记图像里到底有什么。如果完全放开让潜状态自由漂移高风险是模型脑补出一个和真实场景脱节的表示。第三gate 的实现不必固定我这里用的是线性渐开。还有两种常见做法用 sigmoid 网络输出一个标量门控训练时门控自然学会不同深度的权重或者干脆固定为 1.0靠 LayerNorm 保证稳定。我个人偏好线性渐开简单可控不容易在训练初期引入剧烈震荡。3.2 训练时的“变长迭代”策略改造结构不难真正容易翻车的是训练策略。如果训练时每次都固定 K8推理时也固定 K8那这个模型本质上就是一个更深层的静态网络完全失去了“动态调整计算量”的意义。更关键的是推理时一旦想改用 K4 或 K16模型会懵它只在深度 8 的路径上见过梯度其他深度要么没学过要么被梯度遗忘。我用的策略是训练时动态采样迭代次数方法如下每个 mini-batch 随机采样一个 K范围从 1 到 Kmax。K 的分布不是均匀的而是偏向小值比如 30% 概率 K1其余 70% 概率按长尾分布采样到 [2, Kmax]。训练早期先固定 K1等模型基础动作预测能力稳定后再逐步打开随机 K。特别地训练里要保留一部分 K8 或 KKmax 的样本让模型知道“深算”是有用的否则它学到“反正迭代一次就够”深层次没有梯度流。这个做法的本质是课程学习。先教模型单步动作预测再教它多步精炼和人类学习“先会做再做精”的顺序一致。如果上来就随机 K模型很容易在早期阶段被多步递归的梯度噪声带偏视觉编码器的训练也被拖累。训练 loss 方面我的建议是在最终动作上计算主损失同时叠加两个辅助损失。第一个辅助损失是“迭代一致性损失”要求相邻迭代步输出的潜状态之间的差异不要剧烈跳变这能抑制状态漂移。第二个辅助损失是“动作稳定性损失”要求相邻迭代步解码出的动作数值之间的 L2 距离尽量小。这个损失非常有用后面推理时的动态停止机制就是建立在动作稳定性之上的训练时如果不加这个约束推理时动作在迭代间可能会忽左忽右根本无法判断什么时候停下来。3.3 推理时的动态计算预算控制固定 K 的做法简单但不够优雅。真实机器人场景对延迟极其敏感控制频率如果要求 30Hz每步决策预算只有三十多毫秒。简单任务根本不需要 8 轮迭代复杂任务 4 轮又可能不够这时候“动态计算预算”才是隐测试时计算规模化的正确姿势。我的推理流程是融合出初始潜状态 h0先算一次动作 a1。进入迭代循环每做一轮迭代就算一次当前动作。如果连续两轮的动作差异小于阈值 ε就提前停止。如果一直不满足停止条件到 Kmax 强制退出。这里动作差异度量的选择有一点讲究。如果动作头输出的是连续动作向量直接用 L2 范数衡量。如果输出的是离散动作 token则要用 token 级别的匹配率或 logits 分布的 KL 散度。我在模拟项目里用过“连续动作 L2 稀疏动作匹配率”双通道指标效果比单用 L2 更稳因为部分动作维度对细微变化不敏感L2 可能很小但实际动作已经错了。阈值 ε 的设定需要依据训练集上的动作分布来校准。我一般做法是先固定 Kmax16在验证集上跑一遍统计相邻迭代动作差的分布取 15 到 25 百分位数作为 ε。太严会导致几乎每次都跑到 Kmax失去动态退出的意义太松会导致简单任务一步就停但模型初始化状态不够准动作误差偏大。动态退出机制跑起来以后平均迭代次数通常会落到 3 到 6 之间简单任务基本都是 2 到 3 轮就停复杂任务才跑到 8 轮以上。这个收益很直观同样一个模型简单任务延迟保持低位困难任务自动多花时间测试时计算开始按难度分配。4. 关键参数取舍与调试细节4.1 迭代轮数 K 和性能的关系很多人会问第一个问题K 到底设多少最合适我的答案永远是“看任务看数据但别一味追求大”。下面这张表来自我自己的模拟项目 X 实验数据规模不大趋势有代表性。任务统一是六类桌面操作包括抓取、推拉、堆叠、开关容器等模型结构完全相同只在推理迭代数上做区别。推理迭代数平均成功率平均决策延迟失败率下降幅度1基线58.2%23ms-262.7%25ms-4.5%468.9%31ms-10.7%874.1%39ms-15.9%1675.3%52ms-17.1%动态退出73.6%33ms-15.4%结论很清楚迭代数从 1 到 8 这一段收益曲线非常陡4 到 8 之间基本吃掉了大部分红利8 到 16 收益就放缓了代价翻倍但成功率只涨一个点出头。所以固定深度场景我通常默认 K8动态退出场景把 Kmax 设到 12 到 16让困难任务有空间跑满。延迟方面要注意迭代次数从 1 涨到 8延迟不是线性涨 8 倍。因为视觉编码和融合模块是整个推理里最重的部分潜迭代模块通常很小如果用的是单个 Transformer Block每次迭代只增加几毫秒。表里从 23ms 涨到 39ms增加约 16ms这个成本对大多数机器人控制场景是可以接受的。4.2 潜状态漂移问题多轮递归最常碰到的问题是潜状态漂移。具体表现有两种一种是潜状态范数随着迭代次数指数增长最后 LayerNorm 的输出被少数极端维度主导另一种是潜状态逐渐收敛到一个“不动点”所有迭代输出几乎没有差异等于模型学会了跳过思考。第一种漂移好解决把思考模块内部的 LayerNorm 位置调整到 Pre-LN 模式即先归一化再进注意力。同时保留强残差。Pre-LN 对长序列递归更友好因为归一化在主干之前不会把残差信号的尺度放大。第二种漂移其实就是模型“作弊”它发现反正最后动作头只看最终潜状态那不如第一轮就把所有信息憋出来后面几轮随便复读。这个问题单靠网络结构解决不了必须从损失函数上纠正。我在 3.2 里提到的迭代一致性损失就能限制这种坍缩要求相邻迭代的潜状态差异不能太大也不能太小模型被迫让每一轮迭代都产生有意义的信息流动。另外还有一个容易忽略的细节思考模块里的 LayerNorm 统计量要不要独立维护。我踩过坑把潜迭代模块直接参数复用后这个共享模块在自注意力里计算均值和方差时不同迭代步混在一起统计训练初期倒没什么后期会出现梯度穿过整个循环后极不稳定的情况。解法是给每个迭代步分配独立的仿射参数即层归一化的 gamma 和 beta 按 k 索引这样每一轮迭代都有不同的数据分布空间稳定性明显提升。4.3 辅助任务到底要不要加潜迭代模块如果只靠最终动作损失监督中间步骤其实没有显式约束模型对“中间状态应该长什么样”没有概念。这是隐测试时计算和显式 CoT 之间最大的区别没有人为指定的中间答案所有精炼目标都由最终损失反传。为了提高中间状态的可学习性我尝试过三个辅助任务下一步子目标预测用潜状态预测后续动作的语义标签比如“拿杯子”“转身”“放下”。物体空间属性预测从潜状态回归目标物体的中心坐标、朝向和类别。未来动作预测提前三步预测动作头将要输出的动作值。实测下来物体空间属性预测对抓取类任务收益最大准确率提升了 5 个点左右。未来动作预测有助于动作序列的连贯性但会稍微拖慢收敛。子目标预测收益最小因为很多操作任务其实没有清晰的子目标序列标注成本高收益不匹配。我的建议是辅助任务不要加太多选一个和动作头最直接相关的即可。加的太多会让潜状态试图同时满足多个目标反而干扰动作映射。我自己目前只保留了一个轻量的空间属性预测头在潜迭代最后一层接入训练结束后直接丢弃推理时不影响速度。5. 踩坑实录与问题排查5.1 训练收敛不稳定的三个典型症状潜迭代模块引入后训练阶段我遇到过三次比较明显的崩溃每次都花了不少时间才定位到原因。第一次是 loss 在训练中期开始剧烈震荡每几百步波动一次。排查后发现是迭代门控系数初始值太大潜状态更新幅度过大梯度穿过 8 步递归后爆炸。把 gate 初始值从 0.5 降到 0.1并加上梯度裁剪全局范数裁剪到 1.0之后训练恢复稳定。第二次是动作头输出连续动作值逐渐收敛到固定位置模型变成“只会动一下”的僵直策略。原因是辅助损失权重过高模型发现只要潜状态不变化动作稳定性损失就最小于是干脆锁死。把动作稳定性损失的权重从 1.0 降到 0.1问题消失。第三次是视觉编码器参数被潜迭代梯度过快污染导致底层特征退化。后来我把视觉编码器的学习率单独降了 10 倍让潜迭代模块主要拿去精炼融合特征而不是反向重构底层视觉特征问题才被压制住。5.2 推理收益消失的排查思路有一种情况特别容易打击信心训练时明明 K8 的 loss 更低但推理时把 K 从 1 调到 8成功率纹丝不动。我第一次遇到这个问题时第一反应是代码 bug检查了半天发现没错。后来打印潜状态在每轮迭代之间的变化量发现变化量几乎为零说明模型练成了一种“假装迭代”的模式初始潜状态直接盯着最终形态后续迭代不加信息。这种“偷懒”行为本质上还是监督信号不足。我用的解法是加大训练时随机 K 的范围并在 loss 里加入“不同迭代次数下最终动作差异的惩罚”同一个样本分别用 K1 和 K8 前向两个动作输出要拉开差距。这强行逼迫模型利用更多迭代步来产生更优动作。效果立竿见影加入这个损失后推理时 K8 相对 K1 的成功率差距从 0 提升到了 12 个点左右。另一个可能原因是迭代位置编码维度太小模型区分不了不同深度。检查方式是把 iter_pos_emb 的 embedding 向量做 PCA 投影如果不同 k 的向量重叠严重就得把 hidden_dim 加大或改用更复杂的正弦位置编码。5.3 延迟优化和部署细节动态退出机制虽然能拉低平均延迟但最坏情况还是会跑到 Kmax需要针对最大延迟做优化。我的部署优化清单如下视觉编码器和文本编码器只在第一轮计算一次潜迭代循环内不重复算。这是最基本的优化能省掉 60% 以上的计算量。把视觉 token 在潜迭代模块里的 key/value 缓存起来。如果思考模块包含交叉注意力务必用 KV Cache 避免每轮重新编码图像。潜迭代模块使用 bfloat16 混合精度动作头保持 float32 精度避免连续动作数值被低精度截断后出现边界抖动。如果迭代模块是标准的 Transformer Block用 Flash Attention 加速自注意力部分迭代步数多时效果非常明显。推理框架允许的话把整个潜迭代循环用 CUDA Graph 封装启动开销可以被完全摊薄。这些优化做完后我最终部署版本的 Kmax16 最坏延迟从 90ms 降到了 55ms 左右动态退出平均延迟稳定在 33ms 附近基本满足桌面机器人的实时控制需求。5.4 快速排查表整理一个排查速查表按现象索引可能原因和优先尝试的解法后来团队里新人调这个模块也都靠这张表。现象可能原因优先尝试训练 loss 震荡门控初值过大、梯度裁剪缺失降低 gate 初值到 0.1开梯度裁剪动作输出僵直辅助损失权重过高降低动作稳定性损失权重推理增加 K 无收益模型学会跳过迭代加“不同 K 动作差异惩罚”潜状态范数爆炸LayerNorm 位置不对切 Pre-LN 模式多轮后动作抖动缺少动作一致性训练加动作稳定性损失视觉特征被污染编码器学习率过高单独调低编码器学习率迭代位置编码无效embedding 维度太小增大 hidden_dim 或改用正弦编码动态退出总是跑满停止阈值过严按验证集动作差分布放宽阈值6. 个人体会与下一步想试的方向折腾这个方向几个月我体会最深的一点是潜迭代建模的收益不是线性的而是阈值式的。当任务复杂度明显超过单步模型的能力边界时多迭代几轮的优势才会爆发出来简单任务上多算和少算几乎没有差别。所以评估这个方案时一定要在困难子集上单独看指标不要被整体平均成功率掩盖了真实效果。另外递归-深度潜迭代和显式 CoT 并不是替代关系。我在部分任务上试过混合方案先让模型在潜空间迭代两轮再用轻量文本模块输出一个简短的逐步计划最后参考计划精炼动作。效果比单独用任何一种都好只是工程复杂度高了不少。下一步我准备尝试两个方向。一个是在动作序列的时间维度上应用潜迭代而不是只作用于单帧动作输出。也就是说让模型对一整段动作轨迹在潜空间里反复推演结合未来预测优化当前动作。另一个是把潜迭代和扩散策略结合用迭代推断动态调整去噪步数让每条动作轨迹按难度自适应分配去噪预算。目前这些还都在实验阶段等跑出稳定结果再写一篇完整拆解。
阅读完成 · 觉得有帮助?