VLA模型通常可以拆成两个逻辑模块:视觉语言模型 和动作生成模型,前者用于生成视觉语言语义特征,后者用于生成连续动作序列。
在OpenPi中,VLM并不是先完整运行一遍,再把最终特征交给动作模型。PaliGemma和Action Expert会在Transformer的每一层共同计算注意力。换句话说,OpenPi虽然也有两个模块,但在计算图上已经被编织成了一个整体。
GR00T则保留了明显的模块边界:Qwen3-VL先生成视觉语言Token,随后DiT把这些Token当成条件,通过Cross-Attention预测动作。
两者最核心的差异可以概括为:
| 对比项 | OpenPi π0/π0.5 | GR00T N1.7 |
|---|---|---|
| VLM | SigLIP + PaliGemma | Cosmos-Reason2-2B,即Qwen3-VL结构 |
| 动作模型 | Gemma Action Expert | 独立的Diffusion Transformer |
| 连接位置 | 每一层Transformer内部 | VLM最终特征与DiT之间 |
| 连接机制 | 联合注意力、不同专家参数 | Cross-Attention |
| VLM信息形态 | 逐层更新的Prefix K/V | 固定的Backbone Features |
| 状态输入 | π0放在Action Expert;π0.5放入离散Prefix | 单独的State Encoder |
| 默认微调方式 | 支持全量或LoRA | 默认冻结VLM,训练动作头 |
| 模块替换难度 | 较高 | 相对较低 |
OpenPi让VLM和Action Expert逐层交互
OpenPi中π0的主要实现位于,
src/openpi/models/pi0.py
src/openpi/models/gemma.py
模型初始化时并不是创建一个PaliGemma,再在其后增加普通MLP动作头,而是同时创建两个Gemma配置:
paligemma_config = get_config("gemma_2b")
action_expert_config = get_config("gemma_300m")
llm = GemmaModule(
configs=[
paligemma_config,
action_expert_config,
]
)
第一个专家负责图像和语言,第二个专家负责状态、噪声动作以及扩散时间。两边使用不同的模型参数和隐藏层宽度。默认配置中,PaliGemma的宽度为2048,Action Expert的宽度为1024,但两者具有相同的层数、注意力头数量和Head Dimension,
PaliGemma:18层,width=2048
Action Expert:18层,width=1024
共同配置:num_heads=8,head_dim=256,num_kv_heads=1
隐藏层宽度虽然不同,但两边都可以投影到相同的注意力空间。因此,OpenPi不需要先把VLM最终特征压缩成固定长度向量,也没有一个独立的VLM到动作模型投影层。输入首先被拆成Prefix和Suffix。Prefix是视觉语言部分,
image_tokens = siglip(images)
language_tokens = gemma_embed(prompt)
prefix_tokens = concat(
image_tokens,
language_tokens,
)
Suffix是动作部分。以π0为例:
state_token = state_proj(robot_state)
action_token = action_in_proj(noisy_action)
time_token = time_embedding(t)
suffix_tokens = concat(
state_token,
mix(action_token, time_token),
)
关键代码位于Gemma Attention中。两个专家先分别使用自己的参数计算Q、K、V,
vlm_qkv = vlm_attention_proj(prefix_tokens)
action_qkv = action_attention_proj(suffix_tokens)
然后沿Token序列维度拼接,
q = concat(vlm_q, action_q)
k = concat(vlm_k, action_k)
v = concat(vlm_v, action_v)
attention_output = attention(q, k, v, mask)
注意力计算完成后,再把结果按照Prefix和Suffix的长度切开,分别送入两个专家自己的输出投影和MLP,
vlm_output = vlm_out_proj(
attention_output[:, :prefix_len]
)
action_output = action_out_proj(
attention_output[:, prefix_len:]
)
需要注意的是,按Prefix和Suffix切分得到的vlm_output与action_output并不是模型的最终结果,而是当前Transformer层中两条分支各自的注意力更新量。 两路结果会先分别经过各自的输出投影,再与进入当前层之前的隐藏状态做残差连接。随后,VLM Token和Action Token分别进入各自参数独立的MLP,得到下一层的输入。经过最后一层后,最终的Prefix Output不直接参与动作预测。模型只截取Suffix Output中对应Action Horizon的部分,并通过action_out_proj预测Flow Matching的速度场。Prefix分支作用在于训练时,Action Query在每一层读取VLM的Key/Value;推理时,这些逐层生成的Prefix Key/Value会被保存在KV Cache中,供每个去噪步骤中的Action Expert重复读取。
训练时,Prefix和Suffix会放在一次完整的前向计算中,
(prefix_out, suffix_out) = model(
[prefix_tokens, suffix_tokens],
mask=asymmetric_attention_mask,
)
pred_velocity = action_out_proj(
suffix_out[:, -action_horizon:]
)
Flow Matching只监督动作部分,
x_t = t * noise + (1 - t) * action
target_velocity = noise - action
loss = mse(
pred_velocity,
target_velocity,
)
代码中没有为VLM单独增加文本生成损失。VLM是否更新取决于训练配置,既可以全量训练,也可以通过LoRA只更新部分注意力层和FFN。即使损失只计算在动作输出上,只要VLM参数没有被冻结,动作损失仍然可以通过联合注意力传回PaliGemma。
推理时,OpenPi会先运行一次图像和语言Prefix,并保存其KV Cache,
prefix_output, kv_cache = vlm(prefix_tokens)
之后每一步去噪只重新计算Action Expert,
for t in denoise_steps:
suffix_tokens = embed_action(x_t, state, t)
velocity = action_expert(
suffix_tokens,
past_key_values=kv_cache,
)
x_t = x_t + dt * velocity
说明OpenPi的VLM和动作专家虽然连接很深,但推理时不需要在每个Flow Matching Step中重复运行视觉编码器和完整PaliGemma。π0.5在连接方式上又做了两个调整,
第一,π0中机器人状态是Action Expert的连续State Token;π0.5会把状态离散化,并放入语言侧的Prefix。此时动作专家读取到的Prefix已经同时包含图像、指令和机器人状态。
第二,π0.5不再把时间编码直接与动作特征拼接,而是使用时间编码调制Action Expert中的AdaRMSNorm,
π0:
Action Embedding + Time Embedding → MLP
π0.5:
Action Embedding → Action Expert
Time Embedding → AdaRMSNorm
具体来看,在π0.5中,动作嵌入和时间嵌入不会直接拼接或相加。动作嵌入作为Action Expert的Token输入,时间嵌入则作为条件,调制Action Expert每个Transformer Block中的归一化和残差连接。
GR00T将VLM特征作为DiT的条件输入
源码实现如下,
gr00t/model/modules/qwen3_backbone.py
gr00t/model/gr00t_n1d7/gr00t_n1d7.py
gr00t/model/modules/dit.py
GR00T的连接方式更加接近传统的Encoder---Decoder。首先,图像和语言指令进入Cosmos-Reason2-2B。该模型采用Qwen3-VL架构:
outputs = qwen3_vl(
input_ids=input_ids,
pixel_values=pixel_values,
image_grid_thw=image_grid_thw,
output_hidden_states=True,
)
vl_features = outputs.hidden_states[-1]
返回结果仍然是一组Token,既包含文本Token,也包含图像Token。代码还通过image_token_id生成image_mask,用于区分两类信息,
image_mask = input_ids == image_token_id
这些特征会先经过LayerNorm,以及可选的视觉语言Self-Attention,
vl_features = vlln(vl_features)
vl_features = vl_self_attention(vl_features)
另一侧,机器人状态和噪声动作不进入VLM,而是由动作头独立编码,
state_features = state_encoder(
state,
embodiment_id,
)
action_features = action_encoder(
noisy_action,
timestep,
embodiment_id,
)
sa_features = concat(
state_features,
action_features,
)
sa_features是DiT的查询序列,vl_features是Cross-Attention的条件序列,
model_output = dit(
hidden_states=sa_features,
encoder_hidden_states=vl_features,
timestep=timestep,
)
两模块不要求隐藏层宽度一致。Cross-Attention内部会分别对DiT Query和VLM Key/Value做线性投影,因此更换VLM时,只要重新适配cross_attention_dim,不需要像OpenPi那样保证两个专家具有相同的层数、Head数量和Head Dimension。
GR00T N1.7默认使用AlternateVLDiT,其Transformer Block交替执行两种操作,
Cross-Attention:动作Token读取VLM特征
Self-Attention: 状态和动作Token彼此交互
在Cross-Attention层中,又会根据image_mask交替读取文本和图像,
第一个Cross-Attention Block:主要读取非图像Token
下一个Cross-Attention Block:主要读取图像Token
之后继续交替
对应的代码逻辑可以简化为,
if current_block_attends_text:
condition_mask = non_image_mask
else:
condition_mask = image_mask
action_features = cross_attention(
query=action_features,
key=vl_features,
value=vl_features,
mask=condition_mask,
)
避免了动作Token在每一层都无差别地读取全部视觉语言序列,让语言约束和图像细节以不同节奏进入动作生成过程。
DiT最终输出会经过具身相关的Action Decoder,
prediction = action_decoder(
model_output,
embodiment_id,
)
embodiment_id会同时进入State Encoder、Action Encoder和Action Decoder,把不同机器人之间的差异放在动作模型侧处理,而不是要求VLM理解每种机器人的具体关节定义。训练阶段同样使用Flow Matching,
noisy_action = (1 - t) * noise + t * action
target_velocity = action - noise
prediction = action_head(
vl_features,
state,
noisy_action,
t,
)
loss = mse(prediction, target_velocity)
GR00T默认配置为,
tune_llm = False
tune_visual = False
tune_projector = True
tune_diffusion_model = True
tune_vlln = True
默认冻结Qwen3-VL的语言和视觉部分,只训练状态编码器、动作编码器、DiT、输出解码器以及VLM特征适配层。需要时也可以解冻顶部若干LLM Layer。推理时,Qwen3-VL只运行一次,
vl_features = backbone(images, instruction)
state_features = state_encoder(state)
随后多个去噪步骤反复调用DiT,
actions = random_noise()
for t in range(num_inference_timesteps):
action_features = action_encoder(actions, t)
velocity = dit(
hidden_states=concat(
state_features,
action_features,
),
encoder_hidden_states=vl_features,
)
actions = actions + dt * velocity
两种连接方式反映了不同的设计取向
OpenPi的关键并不是PaliGemma后面接了一个动作头,而是让VLM Expert和Action Expert在每一层共同参与注意力计算。动作专家读取的不是VLM最后一层压缩后的结果,而是逐层演化的视觉语言表示。可以将其概括为如下,
OpenPi:
VLM Layer 1 ←联合注意力→ Action Expert Layer 1
VLM Layer 2 ←联合注意力→ Action Expert Layer 2
VLM Layer 3 ←联合注意力→ Action Expert Layer 3
...
GR00T则先完成VLM编码,再由动作模型读取其最终Token,
GR00T:
图像、语言 -> Qwen3-VL Backbone -> 固定的VL Token序列 <- Cross-Attention ->
-> DiT Action Head -> 动作序列
OpenPi的优势是语义理解与动作生成结合得更紧,动作专家在浅层就可以读取视觉和语言信息,并在后续每一层继续加工,适合联合预训练大规模VLA。代价是结构约束较多。两个专家需要保持相同层数,并且注意力头配置必须兼容。替换VLM、改变层数或接入完全不同的动作网络时,需要修改联合Transformer的内部实现。
GR00T的优势是模块边界清晰,VLM只需要输出Token序列,DiT通过Cross-Attention读取条件。更换视觉语言骨干、冻结VLM、独立导出动作头以及进行多具身适配都更加直接。代价是VLM和动作模型主要在Backbone出处连接。与OpenPi的逐层交互相比,动作模型无法参与VLM内部表征形成过程,只能对已经生成的视觉语言特征进行二次读取。
对于希望保持预训练VLM稳定、方便替换模型或适配多种机器人的项目,GR00T的Backbone-DiT结构更容易扩展;对于希望通过大规模联合训练,让语义特征从底层开始服务于动作生成的模型,OpenPi的双专家联合注意力结构更加紧密。