0. 简介
SimpleMemVLA 面向长时程机器人操作,处理的是一个很具体的部分可观测问题:决定下一步动作所需的证据,可能在几十秒甚至几分钟前出现,而当前相机画面已经看不到它。检索库、视觉压缩器和循环状态都要在写入历史时提前决定保留什么,未来任务一旦需要被删掉的细节,策略就无法补救。SimpleMemVLA 直接把采样后的历史画面保持为带时间戳的视频,送入预训练过视频建模的 Qwen3.5-4B 主干,再用生成子任务的隐藏状态连接标准流匹配动作头;连续决策之间共享的历史前缀则在机器人执行动作时提前计算。项目报告在 RMBench、RoboMME、MIKASA-Robo 和 RoboMemArena 四项记忆基准上取得最高结果,同时在 LIBERO 和 LIBERO-Plus 上保持通用控制能力。下面结合机制、代码、消融、延迟和真机任务,说明这条路线为什么成立、它依赖哪些工程条件,以及它还没有解决什么。OpenBMB/SimpleMemVLA
1. 研究问题与现有方法的缺口
1.1 任务不是看一帧,而是追踪已经消失的证据
长时程操作中的困难经常被误写成"视觉理解不够强",但 SimpleMemVLA 的任务设计把问题限定得更清楚 。机器人可能先看到三个颜色不同的积木,随后被相同的黑盖完全遮住,最后必须按红、绿、蓝的顺序揭盖;也可能把一个方块从四个外观相同的垫子之一拿到中央,按下按钮后再放回原来的垫子。行动发生时,当前画面只提供盖子、按钮和空垫子,真正决定动作的颜色、初始位置和已经完成次数,都埋在历史视频里。核心问题在于,策略需要把"过去发生了什么"与"现在该做什么"放进同一次决策,而不是把记忆当成任务之外的附加模块。
机器人控制还受到时间尺度的约束。主摄像头按低帧率保留过去,腕部相机保留当前图像,本体状态以归一化向量输入动作头;每次动作不是一步,而是预测一小段动作块,然后执行前几步再重新决策。这样一来,历史表示既要保留分钟级线索,也要在亚秒级临界路径上完成语言生成和动作采样。记忆质量与控制延迟是同一个系统问题的两面:把历史压得太狠会丢证据,完整重算又会让机器人停等模型。
1.2 三类记忆路线都存在写入时承诺
现有 VLA 记忆机制大致可以分成三类。第一类是检索库,在每次决策前按相似度挑选若干历史片段,优点是上下文短,缺点是检索器必须预判未来问题,遮挡、计数和顺序任务中的关键帧可能没有足够的文字或视觉相似度。第二类是学习式压缩器,把连续视频映射为少量 token 或摘要,计算成本下降,但细粒度颜色、空间关系和动作先后顺序会在压缩时被固定丢弃。第三类是循环状态或记忆 token,每一步更新一个固定维度的隐状态,推理便宜,却容易覆盖早期事件,也很难在任务改变后重新读取原始画面。它们共同依赖一个假设:分钟级历史太长,不能直接交给视觉语言主干。
Qwen3.5-4B 这类现代 VLM 已经提供 262k token 的上下文窗口,项目估算 60 秒历史只使用约 5.6k token,足以在不做专用压缩的情况下保存分钟级视频。进一步看,SimpleMemVLA 的路线判断不是"任何任务都应该把全部历史塞进去",而是当上下文预算允许时,延迟证据选择比提前写入更稳妥。 模型在决策时可以同时看到当前子任务、视频时间顺序、本体状态和历史画面,让注意力机制自己判断哪一个时间片段与当前动作有关。

直觉理解:把历史写入记忆像整理一间实验室。检索和压缩要求你在整理当天就决定哪些纸张会有用,原生视频则先把带时间的实验记录完整放进柜子,真正做实验时再按问题翻阅。柜子足够大时,延后筛选通常比过早丢弃更安全。
2. 整体框架:视频主干、子任务通道与动作专家
2.1 输入输出接口与时间采样
SimpleMemVLA 的一次决策输入由四部分组成:主摄像头的历史视频、只表示当前时刻的腕部或辅助相机图像、描述总体任务和机器人形态的文本提示、可选的当前本体状态。历史视频按 history_video_sec 和 history_video_fps 从原生控制流中抽样,帧数被调整为偶数,因为 Qwen3.5 的视频时间 patch 以两帧为一个单元;每个视频 patch 带有明文时间戳,沿用主干预训练时的格式 。输出先是最多 64 个 token 的当前子任务,再由动作头生成一个长度为 action_horizon 的动作块,推理时执行动作块前部并重新观察。这个接口把"理解历史"和"生成动作"分成可检查的两步,同时保留端到端梯度。
V t = Sample ( O t − H : t , f h , f n ) , I t = O t wrist , s t = Normalize ( q t ) V_t = \operatorname{Sample}\!\left(O_{t-H:t},\; f_h,\; f_n\right), \qquad I_t = O_t^{\text{wrist}}, \qquad s_t = \operatorname{Normalize}(q_t) Vt=Sample(Ot−H:t,fh,fn),It=Otwrist,st=Normalize(qt)
这里的 H H H 是历史时间窗口, f h f_h fh 是历史采样帧率, f n f_n fn 是原生控制帧率; V t V_t Vt 和 I t I_t It 分别进入视频通道和图像通道, s t s_t st 是与动作关节布局一致的本体状态。模型先在多模态序列上产生隐藏状态 H t H_t Ht,再从子任务跨度中收集条件,动作头依据噪声时间 τ \tau τ 预测速度场 u θ u_\theta uθ。这意味着,历史并没有被转成一个额外的 memory vector,而是保留在主干的空间和时间位置编码里,后续注意力仍可按时间位置访问它。
z t = Backbone ( V t , I t , p t ) , c t = Gather ( z t , S t ) , a ^ t = DiT ( c t , s t , τ ) z_t = \operatorname{Backbone}(V_t, I_t, p_t), \qquad c_t = \operatorname{Gather}\!\left(z_t,\; \mathcal{S}_t\right), \qquad \hat{a}_t = \operatorname{DiT}(c_t, s_t, \tau) zt=Backbone(Vt,It,pt),ct=Gather(zt,St),a^t=DiT(ct,st,τ)
其中 p t p_t pt 是包含机器人类型、控制频率和总体任务的提示, S t \mathcal{S}_t St 是子任务答案跨度。Gather 同时取出该跨度的隐藏状态和词元嵌入,并保存它们的多维位置索引;动作头接收的是这组连续表示,而不是一条孤立的子任务字符串。信息流的关键不对称是:历史以视频形式进入语言视觉主干,动作专家只看到子任务跨度和本体状态。
2.2 训练与推理的角色分工
训练时,数据样本已经包含专家动作块和当前子任务文本,模型可以用教师答案标出子任务 token,并同时计算语言监督与流匹配动作损失;推理时则先根据历史生成子任务,再把实际生成的 token 隐状态送进动作头。这里的训练和部署并非两套接口:代码用同一个 subtask_span_mask 规定哪些 token 属于条件跨度,避免训练时取错位置、部署时取另一段文本。推理阶段还要补上结束标记和换行 token,以保持动作头看到的条件形状与训练一致。

这里的关键是,子任务并不是一个供人阅读的解释性输出,而是历史证据进入动作专家的窄通道。消融实验显示,只替换子任务文字并不能复现完整收益,生成子任务时的隐藏状态才携带了较丰富的视觉上下文;如果只给当前画面,或者打乱历史帧顺序,遮挡和计数任务会一起失败。换句话说,语言跨度承担了接口责任,隐藏状态承担了信息传输责任,二者不能简单等同。
难点提示:这像一位调度员给机械师下达"处理三号阀门"的工单。工单文字让机械师知道当前子任务,但调度员刚才查看过的时间表、报警记录和现场照片,才解释为什么是三号阀门。SimpleMemVLA 把这些信息留在调度员的工作记忆里,再用生成工单时的内部状态传给机械师。
3. 核心机制一:原生视频上下文延后选择证据
3.1 时间戳视频怎样成为可读记忆
SimpleMemVLA 不把历史帧拼成一串无序图片,而是保持视频通道的时空结构。build_simplememvla_messages 为主摄像头放入 video 占位符,为不属于历史集合的腕部相机放入当前 image 占位符,再用文本明确说明每一帧带有时间戳。处理器生成视频 patch 时,主干拿到的是与预训练视频任务相同的时间组织方式;历史画面距当前越远,仍然能凭时间位置被注意力访问。这样设计的工程价值是,记忆机制没有新增一个需要单独训练、单独调参和单独导出的模块,模型可以复用已有的视觉时间编码。
python
# simplememvla/data/messages.py
def derive_video_sampling(
history_video_sec: float, history_video_fps: float, native_fps: float
) -> tuple[int, int]:
n_frames = max(2, round(history_video_sec * history_video_fps))
if n_frames % 2:
n_frames += 1
stride = max(1, round(native_fps / history_video_fps))
return n_frames, stride
def variable_history_frames(num_real_frames: int) -> int:
num = int(num_real_frames)
return max(2, num + (num % 2))
这段代码完成了两个容易被忽略的约束。第一,stride 把原生控制频率换算成历史采样间隔,保证不同 benchmark 的控制频率差异不会改变时间窗口含义;第二,历史帧数被补成偶数,直接匹配视频时间 patch 的大小。variable_history_frames 还允许短 episode 只提供真实存在的帧,不用用大量复制帧假装已经经历了完整窗口。同一套采样规则被训练数据集、普通闭环策略和流式策略共用,避免训练与部署看到不同的历史布局。

3.2 消融实验检验历史是否真的被读取
如果模型只是在当前画面上猜测,增加历史窗口不应该稳定改善任务,也不应该对特定历史片段的遮挡产生有方向的反应。SimpleMemVLA 的消融给出相反证据:在 RMBench 的盖积木任务中,30 秒窗口仍保留约 25 秒前的遮盖过程,机器人可以完成;窗口缩短到 15 秒、关键遮盖帧被移出后,任务失败。按键计数任务也随窗口缩短逐步退化,帧顺序打乱或只保留当前画面时,两个任务都无法完成。直观理解是,模型必须真的翻回那一页记录,这组结果把"有历史"与"读到了正确历史"区分开来。
python
# simplememvla/data/messages.py
def build_simplememvla_messages(
camera_tags, instruction, robot_tag, control_frequency_hz,
action_horizon, camera_labels, subtask=None,
history_camera_tags=None,
):
if history_camera_tags is None:
history_camera_tags = list(camera_tags)
history = set(history_camera_tags)
content = []
for tag in camera_tags:
content.append({"type": "text", "text": f"{camera_labels[tag]}:"})
content.append({"type": "video" if tag in history else "image"})
content.append({"type": "text", "text": build_embodiment_prompt(
instruction, robot_tag, control_frequency_hz, action_horizon,
num_cameras=len(history_camera_tags),
num_current_cameras=len(camera_tags) - len(history_camera_tags),
)})
messages = [{"role": "user", "content": content}]
if subtask:
messages.append({"role": "assistant", "content": subtask})
return messages
这段消息构造代码说明了历史与当前输入的结构性区分。主摄像头的每个样本都以 video 进入处理器,腕部相机若没有加入历史集合,就只放入当前 image;提示词还明确告诉模型哪些画面带时间戳、控制频率是多少以及要预测多少个动作。模型不是把几路相机简单串在一起,而是借由输入类型把"过去"与"现在"编码成不同角色。 这也是为什么在只改变无关历史时,输出应当保持稳定;真正有关的片段改变,才会沿注意力路径影响动作。

4. 核心机制二:子任务隐藏状态连接语言与动作
4.1 为什么不把整段主干状态直接交给动作头
把整段视频语言序列直接接到动作头,会同时带来计算量、位置对齐和训练目标冲突。语言视觉主干需要处理大量视频 patch、系统提示和多模态占位符,动作专家真正需要的是与当前子任务有关的条件。如果动作头对所有 token 都做跨注意力,历史 patch 数量一变,动作条件的长度和统计分布就会变化;如果只把生成的子任务文字重新编码,历史证据已经被压成了有限的词面,颜色、空间位置和事件顺序可能丢失。SimpleMemVLA 取一个窄而稳定的折中:子任务跨度的 token embedding 提供离散语义,主干对这些 token 产生的 hidden states 提供上下文证据,再拼接一个归一化本体状态。
C t = E ( y t , 1 ) , ... , E ( y t , m ) ; H t , 1 , ... , H t , m ; P t , 1 : m , a t ∼ p θ ( ⋅ ∣ C t , s t ) C_t = \left\\,E(y_{t,1}),\\ldots,E(y_{t,m})\\,;\\;H_{t,1},\\ldots,H_{t,m}\\,;\\;P_{t,1:m}\\,\\right, \qquad a_t \sim p_\theta(\cdot\mid C_t, s_t) Ct=E(yt,1),...,E(yt,m);Ht,1,...,Ht,m;Pt,1:m,at∼pθ(⋅∣Ct,st)
其中 y t , 1 : m y_{t,1:m} yt,1:m 是子任务 token, E E E 是输入嵌入, H t , 1 : m H_{t,1:m} Ht,1:m 是主干隐藏状态, P t , 1 : m P_{t,1:m} Pt,1:m 是对应的位置编码。代码通过 span 逐样本收集三类张量并补齐到批次最大长度,动作头的条件注意力只在有效 token 上工作。历史信息经过主干的自注意力汇聚,再通过隐藏状态进入动作专家;子任务文字负责对齐接口,不能替代隐藏状态。
4.2 子任务监督与答案跨度掩码
训练数据里的子任务是普通 assistant answer,模型用 token-level cross entropy 学会根据历史描述当前正在进行的子目标。</think> 后的答案跨度被标成监督区域,其他系统提示、用户问题和视觉占位符都设为 -100,因此语言损失不会要求模型复述整个输入 。推理时,策略生成同样的答案跨度,再用完全一致的掩码把生成 token 的隐藏状态交给动作头。这个接口设计看似只是一个 mask,却解决了多模态序列中最容易出错的索引问题:视频 token 数量、图片 token 数量和文本 token 数量会随处理器分辨率与历史窗口改变,硬编码起止位置会很快失效。