VLA action model:输入文本指令、当前或历史图像、可选机器人状态,输出未来一段机器人动作。
World model:输入历史图像和动作,输出下一帧或下一组图像,用来预测动作执行后的视觉结果。
RynnVLA-002 的关键点不是简单地把两个模型放在一起,而是把“动作生成”和“图像生成”都写成多模态对话序列预测问题,让同一个 Transformer 学习机器人交互中的条件生成规律。同时,它在离散动作 token 之外增加了连续动作头,用连续 L1 回归提升动作精度。

  1. 任务定义
    2.1 VLA 动作生成
    给定:

语言任务指令,例如 “put the bowl on top of the cabinet”。
当前或若干历史第三视角图像。
当前或若干历史腕部相机图像。
可选机器人本体状态,通常包含末端执行器位置、姿态和夹爪状态。
输出:

一个动作 chunk,即未来 T 步动作。
每步动作通常是 7 维:三维平移、三维旋转、夹爪控制。
形式化表示:

a_{t:t+T-1} = pi(instruction, state_t, image_history, wrist_image_history)
其中 T 在 LIBERO 配置里常见为 5,在 LeRobot 实验里可以是 20。

2.2 World model 图像生成
给定:

历史第三视角图像。
历史腕部图像。
与历史转移对应的动作。
输出:

下一时刻第三视角图像。
下一时刻腕部图像。
形式化表示:

(I_{t+1}^{front}, I_{t+1}^{wrist}) =
W(I_{t}^{front}, I_{t}^{wrist}, a_t)
在实现中,这个图像生成不是像扩散模型那样逐步去噪,而是像语言模型一样逐 token 生成图像 token,再通过 Chameleon 的图像 tokenizer/decoder 还原为图片。

  1. 统一序列表示
    RynnVLA-002 的核心设计是把不同模态都变成 token,并放入同一个自回归序列。

3.1 文本 token
任务指令和 prompt 使用文本 tokenizer 编码。例如动作任务的自然语言提示大致是:

What action should the robot take to {task_description}?
world model 的提示大致是:

Generate the next image based on the provided sequence of historical images and corresponding actions.
3.2 图像 token
图像使用 Chameleon VQGAN tokenizer 编码。流程是:

输入图像被 resize/crop 到目标分辨率相关的网格尺寸。
图像进入 VQGAN tokenizer,变成离散图像 token。
图像 token 被组织成二维 latent 网格。
每行末尾插入换行类 reserved token。
图像块外面包上图像开始 token、网格高宽 token、图像结束 token。
概念上,一个图像块长这样:

<image_start> <h_grid> <w_grid>
image_token_1 image_token_2 …
image_token_… …
<image_end>
这使得图像能够像文本一样出现在自回归上下文中,也能够作为模型要生成的目标。

3.3 动作 token
动作有两种表示方式。

第一种是离散动作 token。连续动作先按每个维度的统计范围归一化到 [-1, 1],再映射到 256 个均匀 bin,最后偏移到 reserved token 区间。动作块结构是:

<action_start> action_dim_1_token … action_dim_7_token <action_end>
对于一个动作 chunk,目标序列里会重复多个 <|action|> 占位符;每个占位符在 tokenization 时被展开成一个完整动作块。

第二种是连续动作回归。模型仍然先生成或定位 <action_start> 这样的动作起始语义位置,但真正输出动作时,不只依赖离散 token,而是使用 Transformer 隐藏状态接一个连续动作头,直接回归 T x action_dim 的连续动作。

3.4 状态 token
状态也被归一化到 [-1, 1],再离散化到 reserved token 区间。状态块结构类似动作:

<state_start> state_dim_1_token … state_dim_N_token <state_end>
LIBERO 里的状态通常由末端执行器状态和夹爪状态拼接得到。源码里对应的状态维度是 8 维。

  1. 把机器人数据改写成“多模态对话”
    训练数据不是直接喂 (obs, action) 元组,而是被改写成对话格式。这样做有两个好处:

可以复用大语言模型/多模态语言模型的 instruction-following 形式。
动作、图像、状态都通过占位符插入对话,最终统一展开成 token 序列。
4.1 VLA 对话样本
动作生成样本形如:

{
“conversations”: [
{
“from”: “human”,
“value”: “What action should the robot take to open the drawer?<|state|><|image|><|image|><|image|><|image|>”
},
{
“from”: “gpt”,
“value”: “<|action|><|action|><|action|><|action|><|action|>”
}
],
“image”: [
“front_t-1.png”,
“wrist_t-1.png”,
“front_t.png”,
“wrist_t.png”
],
“state”: [“state_t.npy”],
“action”: [
“action_t.npy”,
“action_t+1.npy”,
“action_t+2.npy”,
“action_t+3.npy”,
“action_t+4.npy”
]
}
这里的 <|image|>、<|state|>、<|action|> 只是占位符。tokenization 时,processor 会依次取 image、state、action 字段里的真实数据,把它们展开成对应 token 块。

4.2 World model 对话样本
世界模型样本形如:

{
“conversations”: [
{
“from”: “human”,
“value”: “Generate the next image based on the provided sequence of historical images and corresponding actions.<|image|><|image|><|action|>”
},
{
“from”: “gpt”,
“value”: “<|image|><|image|>”
}
],
“image”: [
“front_t.png”,
“wrist_t.png”,
“front_t+1.png”,
“wrist_t+1.png”
],
“action”: [“action_t.npy”]
}
这个样本让模型学习:看到当前 front/wrist 图像和动作后,生成下一时刻 front/wrist 图像。

4.3 VLA 样本和 WM 样本如何一起训练
VLA 样本和 world model 样本不是在同一个轨迹时刻里强制成对做两次 forward,也不是每个 step 同时优化一个 VLA batch 和一个 WM batch。实际训练方式是把两类样本合并到同一个训练数据集中,再由 dataloader/sampler 像普通监督数据一样取 batch。

也就是说,训练集概念上是:

train_dataset = action_samples + world_model_samples
每个 batch 里可能包含 VLA/action 样本,也可能包含 world model 样本,具体取决于合并后的样本顺序、shuffle 和 sampler。源码确认:pretokenize 流程会把 action 数据 record 和 world model 数据 record concat 成一个统一 record;no-pretokenize 流程里,dataset 构造时也会分别 append action sample 和 world sample。

这意味着:

抽到 VLA/action 样本时,目标主要是动作 token,连续动作头也有有效的 L1 监督。
抽到 world model 样本时,目标主要是未来图像 token,通常没有连续动作头监督。
两类任务的训练比例主要由合并进去的样本数量决定;当前实现里没有看到额外的任务均衡采样器。
4.4 训练标签 mask
训练时,prompt 部分只作为条件,不计算 loss;assistant/gpt 部分作为目标,计算 loss。也就是说:

human prompt tokens -> label = -100
gpt target image/action -> label = token_id
padding -> label = -100
这和语言模型监督微调一致,只是目标 token 可能是文本、图像 token、动作 token 或状态 token。

  1. 模型结构
    RynnVLA-002 的主干是 Chameleon 条件生成模型,外面加了机器人任务相关的配置和动作头。

5.1 多模态自回归 Transformer
主干 Transformer 接收统一 token 序列:

x = [text tokens, state tokens, image tokens, action tokens, …]
训练时,它做标准 causal language modeling:

P(x) = product_i P(x_i | x_<i)
因此,只要把目标动作或目标图像放在序列后半段,模型就会学习在前文条件下生成它们。

5.2 动作离散生成分支
离散动作分支完全复用语言模型的 token logits:

模型预测下一个 token 分布。
在动作位置上,目标 token 是动作 bin 对应的 reserved token。
推理时生成动作 token 序列。
将动作 token 反量化回 [-1, 1] 连续动作。
再按数据集 min/max 反归一化回机器人环境动作空间。
这种方式简单统一,但每个动作维度只能落在离散 bin 中,精度受量化限制。

5.3 连续动作头
连续动作头解决动作量化精度问题。它的思路是:让大 Transformer 负责理解语言、图像、状态上下文,再用一个专门的小网络从 hidden state 回归连续动作。

连续动作头内部流程:

找到序列中第一个动作起始 token <action_start> 的位置。
取该位置之前的所有 Transformer hidden states 作为上下文。
拼接一组可学习的 action query token。数量为 time_horizon * action_dim。
将上下文和 action query 一起送入一个轻量 Transformer Encoder。
取 action query 对应的输出。
用 MLP-ResNet 投影为连续动作。
reshape 成 [time_horizon, action_dim]。
直观地说,连续动作头不是让语言模型一个 token 一个 token 猜动作数值,而是在“我要开始输出动作”的位置,对上下文做一次条件读取,然后并行解码未来动作 chunk。

  1. 训练目标
    训练总 loss 由几部分组成。

6.1 token 交叉熵
主 loss 是标准 token-level cross entropy:

L_ce = CE(logits, labels)
它会监督:

world model 的图像 token 生成。
离散动作 token 生成。
其他 assistant 目标 token。
6.2 连续动作 L1 loss
当启用连续动作头时,模型从 labels 中找到动作 token 块,把离散动作 token 反解码成归一化连续动作,作为连续动作头的监督目标。

L_ct = mean_absolute_error(predicted_action, decoded_label_action)
源码确认:训练中默认总 loss 是:

L = L_ce + loss_ct_weights * L_ct + optional_z_loss
其中 loss_ct_weights 默认是 10。

6.3 图像 token loss 权重
在 action-world-model 联合训练的某些路径里,图像 token 的交叉熵权重会被调小。源码中对 token id [3, 8195) 这段图像 token 区间设置 loss_img_weights,默认值是 0.04。

这背后的动机是:图像 token 数量远多于动作 token。如果不降权,训练目标很容易被图像重建主导,动作能力被稀释。

6.4 可选 z-loss
如果配置 z_loss_weight > 0,会对 logits 的 log-sum-exp 加一个正则项:

L_z = mean(logsumexp(logits)^2)
它用于稳定大词表 logits,但默认可以为 0。

  1. 注意力机制细节
    模型使用 causal attention,也就是每个位置只能看自己之前的 token。

源码里还针对图像块和动作块构造了额外 attention mask。核心意图是:在包含多个动作块和图像块的序列中,控制某些动作块之间的可见性,避免当前要预测的动作块不恰当地看到后续或不该参考的动作信息。

可以把它理解为:统一序列虽然把所有模态拼在一起,但训练时仍需要保证“条件”和“目标”的时间方向正确。

7.1 动作 chunk 内不同动作块的 mask
动作 chunk 在离散 token 序列里通常表现为多个连续的 action block:

condition_tokens
<action_start> action_t_dim1 … action_t_dim7 <action_end>
<action_start> action_t+1_dim1 … action_t+1_dim7 <action_end>
<action_start> action_t+2_dim1 … action_t+2_dim7 <action_end>

如果只使用标准 causal mask,那么后面的动作块会看到前面的动作块。例如 action_t+1 的 token 可以 attend 到 action_t,action_t+2 又可以 attend 到 action_t 和 action_t+1。这会让 chunk 内动作形成自回归依赖:

action_t -> action_t+1 -> action_t+2
当前实现额外做了一步:对位于最后一个 image block 之后的 action block,把“当前动作块看之前动作块”的 attention 置 0。伪代码可以理解为:

初始 mask 是标准 causal mask:后面的 token 默认可以看前面的 token。

mask = causal_lower_triangular_mask(seq_len)

找到序列中的所有图像块和动作块。

image_blocks = find_blocks(input_ids, IMAGE_START_ID, IMAGE_END_ID)
action_blocks = find_blocks(input_ids, ACTION_START_ID, ACTION_END_ID)

只处理位于最后一个图像块之后的动作目标块。

last_image_end = image_blocks[-1].end

for current_action in action_blocks:
if current_action.start > last_image_end:
previous_actions = [
block for block in action_blocks
if block.end < current_action.start
]

    for prev_action in previous_actions:
        # 禁止当前动作块 attend 到更早的动作块。
        # 这样 action_t+1 不再直接依赖 action_t 的 token,
        # action_t+2 也不再直接依赖 action_t/action_t+1 的 token。
        mask[current_action.start : current_action.end + 1,
             prev_action.start : prev_action.end + 1] = 0

因此,离散动作 chunk 的训练更接近:

action_t = f(condition_tokens)
action_t+1 = f(condition_tokens)
action_t+2 = f(condition_tokens)

而不是严格的:

action_t+1 = f(condition_tokens, action_t)
action_t+2 = f(condition_tokens, action_t, action_t+1)
这里有一个细节:这个 mask 切断的是“不同 action block 之间”的依赖,不是切断同一个 action block 内部的维度依赖。一个动作块内部仍然是 causal 的:

<action_start> dim1 -> dim2 -> … -> dim7 -> <action_end>
所以离散动作 token 分支可以概括为:chunk 内不同未来动作块主要共同依赖观测/语言/状态上下文;但每个动作块内部的 7 个维度仍按 token 顺序生成。

连续动作头更进一步:它不按动作 token 逐步生成 chunk,而是在动作开始位置前取上下文 hidden states,拼接 time_horizon * action_dim 个 action query,一次性回归整个动作 chunk。因此连续动作头本身就是并行 chunk 预测:

[action_t, action_t+1, …, action_t+T-1] = ActionHead(condition_hidden_states)
8. 训练流程伪代码
8.1 数据构造

遍历 LIBERO 里的任务集合。每个 task 通常对应一个自然语言目标和若干演示轨迹。

for task in LIBERO_tasks:
trajectories = load_trajectories(task)

# 一条轨迹包含按时间排列的图像、状态和动作。
for traj in trajectories:
    images_front = traj.front_images
    images_wrist = traj.wrist_images
    states = traj.robot_states
    actions = traj.actions

    # 对轨迹中的每个时间步 t 都尝试构造训练样本。
    for t in range(len(actions)):
        # 构造 VLA/action 样本:给定当前/历史观测,预测未来一段动作 chunk。
        # 只有当 t 后面还剩足够 time_horizon 步动作时,样本才有效。
        if t + time_horizon <= len(actions):
            # 收集历史图像。若使用双视角,则每个时间步会放 front 和 wrist 两张图。
            obs_images = collect_history(
                images_front, images_wrist, end=t, history=his
            )

            # 取当前机器人本体状态,例如末端执行器位姿和夹爪状态。
            state_t = states[t]

            # 监督目标是从 t 开始的未来 time_horizon 步动作。
            future_actions = actions[t : t + time_horizon]

            add_sample({
                # human 侧是条件:语言指令 + 状态占位符 + 图像占位符。
                # 这些占位符之后会被展开成真正的 state/image token。
                "human": instruction(task) + "<|state|>" + "<|image|>" * len(obs_images),

                # gpt 侧是目标:每个 <|action|> 会展开成一个完整动作 token 块。
                "gpt": "<|action|>" * time_horizon,

                # image/state/action 字段按占位符出现顺序提供真实数据。
                "image": obs_images,
                "state": [state_t],
                "action": future_actions,
            })

        # 构造 world model 样本:给定当前图像和动作,预测下一帧图像。
        # 只有当 t+1 的未来图像存在时,样本才有效。
        if t + 1 < len(images_front):
            # 条件图像:当前 front 和 wrist。
            hist_images = [images_front[t], images_wrist[t]]

            # 目标图像:下一时刻 front 和 wrist。
            next_images = [images_front[t + 1], images_wrist[t + 1]]

            # 条件动作:从当前状态执行的动作。
            action_t = actions[t]

            add_sample({
                # human 侧描述 world model 任务,并给出当前双视角图像和动作。
                "human": world_prompt + "<|image|><|image|><|action|>",

                # gpt 侧要求模型生成下一时刻双视角图像。
                "gpt": "<|image|><|image|>",
                "image": hist_images + next_images,
                "action": [action_t],
            })

训练时不是分别跑两个 dataloader,而是把 action 样本和 world model 样本合并。

shuffle/sampler 会决定每个 batch 抽到哪类样本,比例主要由两类样本数量决定。

train_dataset = shuffle(action_samples + world_model_samples)
8.2 多模态 tokenization
def tokenize_conversation(sample, training=True):
# output_tokens 是模型实际输入的完整 token 序列。
output_tokens = []

# labels 与 output_tokens 等长;label=-100 的位置不参与 CE loss。
labels = []

# 对话模板会把 human/gpt 两侧展开成一段段文本或模态占位符。
# human 通常是条件,gpt 通常是训练目标。
for segment in render_conversation_template(sample["conversations"]):
    if segment.is_text:
        # 普通文本直接用文本 tokenizer 编码。
        ids = text_tokenizer(segment.text)
        output_tokens.extend(ids)

        # 如果这段文本属于目标侧,就监督预测;如果属于 prompt 条件侧,就 mask 掉。
        labels.extend(ids if segment.is_target else [-100] * len(ids))

    elif segment.placeholder == "<|image|>":
        # 每遇到一个 <|image|>,就按顺序取一个真实图像。
        image = pop_next(sample["image"])

        # 图像通过 Chameleon VQGAN tokenizer 变成离散 image token 块。
        ids = encode_image_as_chameleon_tokens(image)
        output_tokens.extend(ids)

        # 条件图像只作为输入;目标图像才计算 CE。
        labels.extend(ids if segment.is_target else [-100] * len(ids))

    elif segment.placeholder == "<|action|>":
        # 每遇到一个 <|action|>,就按顺序取一条真实动作。
        action = pop_next(sample["action"])

        # 动作先归一化到 [-1, 1],再量化成 reserved token。
        ids = encode_action_as_reserved_tokens(action)
        output_tokens.extend(ids)

        # VLA 样本的目标动作 token 会参与 CE;历史动作条件会被 mask。
        labels.extend(ids if segment.is_target else [-100] * len(ids))

    elif segment.placeholder == "<|state|>":
        # 状态通常是当前机器人 proprioception,例如 ee pose + gripper。
        state = pop_next(sample["state"])

        # 状态和动作类似,也会被归一化并离散化到 reserved token。
        ids = encode_state_as_reserved_tokens(state)
        output_tokens.extend(ids)

        # 当前实现里 state 通常作为条件输入,因此大多会被 mask。
        labels.extend(ids if segment.is_target else [-100] * len(ids))

# 返回统一序列和监督标签;后续训练只需要这两个对象即可计算 CE。
return output_tokens, labels

8.3 主训练循环
for batch in dataloader:
# batch 可能包含 VLA/action 样本,也可能包含 world model 样本。
# collate 会把不同长度的 token 序列 pad 到同一长度。
token_batch, label_batch = collate_and_pad(batch)

# 主模型做一次完整 forward:
# - logits 用于 token 级 CE,包括图像 token 和离散动作 token。
# - hidden_states 会被连续动作头读取。
outputs = model(
    input_ids=token_batch,
    labels=label_batch,
    output_hidden_states=True,
    training=True,
)

# c_loss:自回归 token 预测损失。
# 对 VLA 样本,它监督动作 token;对 WM 样本,它监督未来图像 token。
ce_loss = outputs.cross_entropy_loss

# 最后一层 hidden states 作为连续动作头的输入上下文。
hidden_states = outputs.last_hidden_states

# 连续动作头会找到 ACTION_START_ID 之前的上下文,并回归动作 chunk。
predicted_actions = action_head(
    hidden_states=hidden_states,
    input_ids=token_batch,
    target_token_id=ACTION_START_ID,
)

# 从 label 里找到真实动作 token 块。
# 注意:当前实现用 label 里的离散动作 token 反解码连续监督目标。
label_action_tokens = find_action_blocks(label_batch)
target_actions = decode_action_tokens(label_action_tokens)

# 连续动作头的监督是 L1 回归损失。
# 如果 batch 中没有有效动作目标,该项应为 0 或被跳过。
continuous_loss = l1(predicted_actions, target_actions)

# 总损失 = token CE + 连续动作 L1 的加权和。
# 源码默认 loss_ct_weight 为 10。
total_loss = ce_loss + loss_ct_weight * continuous_loss

# 可选 logits 稳定正则。
if z_loss_enabled:
    total_loss += z_loss_weight * z_loss(outputs.logits)

# 反向传播会同时更新主 Transformer 可训练参数和连续动作头参数。
total_loss.backward()
optimizer.step()

8.4 连续动作头伪代码
def continuous_action_head(hidden_states, input_ids):
# hidden_states: [B, L, H]
# B 是 batch size,L 是序列长度,H 是主模型 hidden size。
# input_ids: [B, L]
# 与 hidden_states 对齐,用来定位动作开始 token。

contexts = []
for b in range(B):
    # 找到当前样本里第一个动作开始 token。
    # 这个位置表示“从这里开始要输出动作”。
    action_start = first_position(input_ids[b] == ACTION_START_ID)

    # 取动作开始 token 之前的所有 hidden states 作为条件上下文。
    # 上下文中已经融合了语言、状态、图像等信息。
    contexts.append(hidden_states[b, :action_start])

# 创建一组可学习 action query。
# 数量等于 time_horizon * action_dim,例如 5 步 * 7 维 = 35 个 query。
action_queries = learned_embedding(
    shape=[time_horizon * action_dim, hidden_size]
)

sequences = []
for context in contexts:
    # 把上下文和 action query 拼在一起。
    # query 会在后面的 Transformer Encoder 中通过 attention 读取上下文。
    sequences.append(concat(context, action_queries))

# 不同样本的上下文长度可能不同,需要 pad 成同一长度。
# mask 用来告诉 Transformer 哪些位置是 padding。
padded_sequences, mask = pad_to_same_length(sequences)

# 先把主模型 hidden size 投影到动作头内部较小的维度。
z = linear_projection(padded_sequences)

# 用轻量 Transformer Encoder 做条件读取。
# 这里不是 causal LM,而是让 action query 充分 attend 到上下文。
z = transformer_encoder(z, padding_mask=mask)

# 取拼接序列末尾的 action query 输出。
# 这些输出就是每个未来动作维度的条件表示。
query_outputs = take_last_query_outputs(
    z, num_queries=time_horizon * action_dim
)

# MLP-ResNet 把 query 表示映射成连续动作数值。
actions = mlp_resnet(query_outputs)

# 整理成 [batch, 未来步数, 每步动作维度]。
actions = reshape(actions, [B, time_horizon, action_dim])

return actions
  1. 推理流程
    9.1 连续动作推理
    在 LIBERO 连续动作评估中,推理闭环大致如下:

环境 reset,并执行若干步 dummy action,让模拟器稳定。
读取当前第三视角图像、腕部图像和机器人状态。
如果动作缓存为空,就构造 VLA prompt。
prompt 经过 item processor 展开成 token。
模型先生成一个动作起始 token。
连续动作头根据 hidden states 输出一个动作 chunk。
将归一化动作反归一化回环境动作空间。
逐步执行 chunk 中的动作。
更新历史图像、腕部图像和历史动作。
重复直到成功或达到最大步数。
伪代码:

动作缓存用于保存一次模型调用生成的 action chunk。

环境每 step 消耗一个动作,缓存空了才重新查询大模型。

actions_cache = []

历史信息会作为下一次决策的上下文。

history_front = []
history_wrist = []
history_actions = []

while not done and t < max_steps:
# 从环境中读取当前观测。
obs = env.current_observation()

# 提取并预处理第三视角图像、腕部图像和机器人状态。
front = preprocess_front_image(obs)
wrist = preprocess_wrist_image(obs)
state = get_robot_state(obs)

# 只有缓存为空时才调用模型,避免每个仿真 step 都跑一次大模型。
if len(actions_cache) == 0:
    # 构造 VLA 推理 prompt。
    # 其中包含任务文本、当前状态、当前双视角图像,以及可选历史图像/动作。
    sample = build_vla_prompt(
        task_description=task,
        state=state,
        current_front=front,
        current_wrist=wrist,
        history_front=history_front,
        history_wrist=history_wrist,
        history_actions=history_actions,
    )

    # 把 prompt 展开成模型输入 token;推理时不需要 labels。
    input_ids = tokenize_conversation(sample, training=False)

    # 连续动作头输出一个动作 chunk,形状近似 [time_horizon, action_dim]。
    action_chunk = model.generate_action_head(input_ids)

    # 将 chunk 放入缓存,后续环境 step 逐个取出执行。
    actions_cache.extend(action_chunk)

# 取出当前要执行的一步归一化动作。
normalized_action = actions_cache.pop(0)

# 将 [-1, 1] 归一化动作映射回 LIBERO 环境需要的真实动作范围。
env_action = unnormalize_action(normalized_action)

# 在环境中执行动作。
obs, reward, done, info = env.step(env_action)

# 更新历史上下文,供下一次模型查询使用。
history_front.append(front)
history_wrist.append(wrist)
history_actions.append(normalized_action)

9.2 离散动作推理
离散动作推理不走连续动作头,而是直接让语言模型生成动作 token:

离散动作推理只依赖 LM head 生成 token,不调用连续动作头。

input_ids = tokenize_vla_prompt(…)

生成若干 reserved action token;每个动作块包含 start、各维动作 token、end。

generated_tokens = model.generate(input_ids, max_new_tokens=action_steps * tokens_per_action)

从生成结果中解析出一个或多个动作块。

action_blocks = parse_action_blocks(generated_tokens)

将动作 token 反量化成 [-1, 1] 的连续动作。

actions = decode_action_tokens(action_blocks)

再映射回环境动作空间。

env_actions = unnormalize_actions(actions)
优点是与自回归训练目标完全一致;缺点是动作被 256-bin 量化。

9.3 World model 推理
world model 推理是图像 token 生成:

构造 world model 推理样本:

条件是当前 front/wrist 图像和动作,目标是让模型继续生成图像 token。

sample = {
“human”: world_prompt + “<|image|><|image|><|action|>”,
“image”: [front_t, wrist_t],
“action”: [action_t],
}

推理时只需要输入 token,不需要 labels。

input_ids = tokenize_conversation(sample, training=False)

自回归生成未来图像 token。

generated_image_tokens = model.generate_img(input_ids)

将 Chameleon 图像 token 解码回 RGB 图像。

front_next, wrist_next = decode_chameleon_images(generated_image_tokens)
生成图像可以用于离线评估,也可以作为 action-world-model 的一部分,帮助模型学习动作造成的视觉变化。

  1. 为什么这种统一方法有效
    10.1 动作和世界变化共享机器人语义
    动作模型学的是“看到状态后该怎么动”,world model 学的是“这样动之后世界会怎么变”。这两件事本质上依赖同一套视觉、语言和物理语义。统一模型让二者共享表示,而不是各自训练孤立网络。

10.2 图像 token 让未来视觉预测变成语言模型式生成
Chameleon 的图像 tokenizer 把图像离散化后,图像预测就可以用 next-token prediction 处理。这让 world model 不需要单独设计 diffusion decoder 或 video model,而是和文本、动作共用同一个自回归建模范式。

10.3 动作 token 让机器人控制进入语言模型词表
动作离散化后,机器人控制变成了 token 预测问题。模型可以像生成文本一样生成动作序列,从而把 VLA 任务纳入同一个因果序列建模框架。

10.4 连续动作头弥补离散化精度
纯 token 动作会受到 bin 数量限制。连续动作头利用大模型上下文 hidden state 直接回归动作,保留统一表示的优点,同时改善控制精度。

10.5 action chunk 提高闭环控制效率
一次预测多个未来动作,可以减少每一步都调用大模型的开销,并让模型学习短时动作连贯性。执行时通过缓存逐步消费 chunk,再根据新观测重新规划。

  1. 重要超参数和约定
    项目 含义
    his 使用多少历史帧作为观测上下文
    time_horizon / len_action 一次预测多少未来动作
    action_dim 每步动作维度,LIBERO 中通常为 7
    resolution 图像训练/推理分辨率,VLA 常见 256,world model 常见 512
    loss_ct_weights 连续动作 L1 loss 权重,默认 10
    loss_img_weights 图像 token loss 降权,默认 0.04
    ACTION_START_ID 动作开始 token id,源码中使用 10004
    ACTION_END_ID 动作结束 token id,源码中使用 15004
  2. 方法边界和容易误解的地方
    RynnVLA-002 不是只训练一个 action policy。它同时包含 action model 和 world model 的统一训练思路。
    world model 生成的是图像 token,再解码成图片,不是直接回归 RGB。
    离散动作和连续动作头是两套动作输出机制,可以分别用于评估;连续动作头不是简单地读取最后一个 token logits。
    <|image|>、<|action|>、<|state|> 是数据构造里的占位符,不是最终模型直接看到的单个普通 token;它们会展开成完整模态 token 块。
    prompt 部分不计算监督 loss,只有 assistant/gpt 目标部分计算 loss。
    图像 token 数量很多,联合训练时需要权重平衡,否则动作学习容易被图像生成目标压过。
    推理时 action chunk 会被缓存并逐步执行,因此模型查询频率低于环境 step 频率。
    动作 chunk 不是简单的逐动作自回归链。离散动作 token 分支通过 mask 切断不同 action block 之间的直接 attention;连续动作头则一次性并行回归整个 chunk。不过,离散动作块内部的动作维度仍保留 causal 顺序。
  3. 最小可复现的概念版本
    如果要从零复现这个方法的核心,不必先复刻全部工程细节,可以按下面的最小版本理解:

1. 准备机器人轨迹

一条轨迹是按时间排列的多模态机器人交互数据。

trajectory = {
“instruction”: text,
“front_images”: […],
“wrist_images”: […],
“states”: […],
“actions”: […],
}

2. 构造两种样本,并合并成混合多任务训练集

VLA 样本学“看到观测后该怎么动”。

vla_samples = make_action_prediction_samples(trajectory)

World model 样本学“执行动作后视觉世界怎么变化”。

world_samples = make_next_image_prediction_samples(trajectory)

两类样本合并训练;不是每一步固定同时训练两类 batch。

dataset = shuffle(vla_samples + world_samples)

3. 把所有模态变成 token

for sample in dataset:
# input_ids 是统一多模态序列;labels 标记哪些位置需要计算 CE。
input_ids, labels = multimodal_tokenize(sample)

4. 训练统一自回归模型

logits 用于离散 token 预测;hidden_states 用于连续动作头。

logits, hidden_states = transformer(input_ids)

CE 同时覆盖离散动作 token 和未来图像 token。

loss_ce = cross_entropy(logits, labels)

5. 额外训练连续动作头

找到动作开始 token 前的上下文 hidden states。

action_context = hidden_states_before_action_start(hidden_states, input_ids)

连续动作头根据上下文回归未来动作 chunk。

pred_action = continuous_action_head(action_context)

当前实现从 label 中的离散动作 token 反解码连续监督目标。

如果额外保存原始归一化 GT action,也可以直接用原始连续 GT 监督。

target_action = decode_action_labels(labels)

连续动作头使用 L1 损失。

loss_ct = l1(pred_action, target_action)

联合优化:主 token CE + 连续动作 L1。

loss = loss_ce + lambda_ct * loss_ct
loss.backward()

6. 部署时闭环控制

while not task_done:
# 读取当前机器人观测。
obs = get_robot_observation()

# 根据当前观测和历史上下文构造 VLA prompt。
prompt = build_action_prompt(obs, instruction, history)

# 可以选择连续动作头或离散动作 token 推理。
action_chunk = model.generate_actions(prompt)

# 执行动作 chunk;缓存为空或需要重新规划时再查询模型。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐