OpenPi、GR00T的视觉语言模型与动作模型连接方式差异分析总结
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.pyGR00T的连接方式更加接近传统的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的双专家联合注意力结构更加紧密。
