1、GR00T N1.7由视觉语言骨干和动作模型组成
GR00T N1.7是一套面向机器人控制的Vision-Language-Action模型。模型接收相机图像、语言指令和机器人当前状态,输出一段连续的机器人动作序列。
GR00T N1.7内部包含两条比较明显的处理链路。第一条链路负责处理图像和语言,得到视觉语言特征;第二条链路负责将机器人状态、带噪动作以及视觉语言特征组合起来,生成连续动作。
模型主类定义在,
gr00t/model/gr00t_n1d7/gr00t_n1d7.py
其中最主要的两个类是,
class Gr00tN1d7ActionHead(nn.Module)
"""Action head component for flow matching diffusion policy."""
class Gr00tN1d7(PreTrainedModel)
"""Gr00tN1d7: VLA model with Cosmos-Reason2-2B (Qwen3-VL) backbone."""
Gr00tN1d7负责组织完整模型,内部包含视觉语言Backbone和动作头;Gr00tN1d7ActionHead负责根据视觉语言特征、机器人状态和带噪动作预测动作速度。完整的数据流如下,
图像、语言 -> Cosmos-Reason2-2B(基于Qwen3-VL) -> 视觉语言特征
视觉语言特征 + 机器人状态 + 带噪动作 -> DiT -> 动作速度场 -> Euler积分 -> 连续动作序列
这里需要先区分两个经常混在一起的概念。GR00T N1.7的动作网络使用DiT,也就是Diffusion Transformer,但动作的训练目标采用Flow Matching。DiT描述的是网络结构,Flow Matching描述的是训练和采样方法,两者并不冲突。
2、N1.7微调入口完成模型与数据配置
N1.7微调入口位于,
gr00t/experiment/launch_finetune.py
命令行参数由tyro根据FinetuneConfig自动生成,
ft_config = tyro.cli(FinetuneConfig, description=__doc__)
FinetuneConfig定义在:
gr00t/configs/finetune_config.py
该配置类中包含模型路径、数据集路径、机器人类型、训练哪些模块、学习率、Batch Size、保存频率等参数。例如,
@dataclass
class FinetuneConfig:
base_model_path: str
dataset_path: str
embodiment_tag: str
modality_config_path: str | None = None
tune_llm: bool = False
tune_visual: bool = False
tune_projector: bool = True
tune_diffusion_model: bool = True
global_batch_size: int = 64
learning_rate: float = 1e-4
gradient_accumulation_steps: int = 1
output_dir: str = "./outputs"
从默认值可以看出,常规微调不会更新完整的语言模型和视觉编码器,而是训练动作头中的状态编码器、动作编码器、动作解码器以及DiT主体。需要注意的是,配置项虽然叫tune_projector,但从后面的源码可以看到,它控制的并不是单独一个传统意义上的多模态投影层,而是动作头前后的多组特征映射模块。
tune_llm = False
tune_visual = False
tune_projector = True
tune_diffusion_model = True
这种设置比较符合机器人数据集的实际情况。机器人示范数据的规模通常远小于视觉语言模型的预训练数据,如果直接更新完整VLM,不仅显存开销很高,也容易破坏原有的视觉语言能力。
进入主函数后,代码先解析embodiment_tag,
from gr00t.data.embodiment_tags import EmbodimentTag
ft_config.embodiment_tag = EmbodimentTag.resolve(ft_config.embodiment_tag)
embodiment_tag = ft_config.embodiment_tag.value
embodiment_tag可以理解为机器人本体类型标识。不同机器人可能有完全不同的状态维度和动作维度,例如单臂机械臂、双臂机器人、人形机器人以及仿真环境中的机械臂,其关节数量和动作定义都不同。GR00T通过该标识选择对应的数据配置以及类别相关的动作编码器。数据集路径支持传入多个目录,
dataset_paths = [
path for path in ft_config.dataset_path.split(os.pathsep) if path
]
在Linux系统中,os.pathsep通常是冒号,因此可以通过下面的方式传入多个数据集,
--dataset-path /data/task_a:/data/task_b
随后代码加载默认配置,并将用户传入的数据集注册到配置中,
config = get_default_config().load_dict(
{
"data": {
"download_cache": False,
"datasets": [
{
"dataset_paths": dataset_paths,
"mix_ratio": 1.0,
"embodiment_tag": embodiment_tag,
}
],
}
}
)
接下来会覆盖N1.7使用的模型参数:
config.model.tune_llm = ft_config.tune_llm
config.model.tune_visual = ft_config.tune_visual
config.model.tune_projector = ft_config.tune_projector
config.model.tune_diffusion_model = ft_config.tune_diffusion_model
config.model.load_bf16 = False
config.model.reproject_vision = False
config.model.model_name = "nvidia/Cosmos-Reason2-2B"
config.model.backbone_trainable_params_fp32 = True
config.model.use_relative_action = True
这里有几个配置值得单独说明。model_name被固定为nvidia/Cosmos-Reason2-2B,说明N1.7默认使用Cosmos-Reason2-2B作为视觉语言骨干;use_relative_action=True表示训练时默认使用相对动作;backbone_trainable_params_fp32=True表示VLM中可训练的参数会保留FP32精度,避免低精度训练导致数值不稳定。最后将训练参数写入总配置并调用:
run(config)
因此,launch_finetune.py本身并不包含训练循环,它的主要工作是读取命令行参数、构造配置,然后将真正的训练交给gr00t/experiment/experiment.py。
3、Gr00tN1d7主类组织完整的VLA模型
Gr00tN1d7继承自Hugging Face的PreTrainedModel,
class Gr00tN1d7(PreTrainedModel):
config_class = Gr00tN1d7Config
supports_gradient_checkpointing = True
继承PreTrainedModel以后,模型可以使用Hugging Face提供的权重保存和加载接口,例如,
AutoModel.from_pretrained(...)
model.save_pretrained(...)
初始化函数中首先构造Backbone,
backbone_cls = get_backbone_cls(config)
self.backbone = backbone_cls(
model_name=config.model_name,
tune_llm=config.tune_llm,
tune_visual=config.tune_visual,
select_layer=config.select_layer,
reproject_vision=config.reproject_vision,
use_flash_attention=config.use_flash_attention,
load_bf16=config.load_bf16,
tune_top_llm_layers=config.tune_top_llm_layers,
trainable_params_fp32=config.backbone_trainable_params_fp32,
transformers_loading_kwargs=transformers_loading_kwargs,
)
get_backbone_cls(config)会根据配置返回对应的视觉语言模型封装类。N1.7配置中使用的是Cosmos-Reason2-2B,其底层结构属于Qwen3-VL系列,因此后续动作模型拿到的不是原始图片,而是Qwen3-VL编码后的序列特征。动作头的初始化比较直接,
self.action_head = Gr00tN1d7ActionHead(config)
此外还会创建数据Collator,
from .processing_gr00t_n1d7 import Gr00tN1d7DataCollator
self.collator = Gr00tN1d7DataCollator(
model_name=config.model_name,
model_type=config.backbone_model_type,
transformers_loading_kwargs=transformers_loading_kwargs,
)
Collator负责把一个Batch中的图像和语言整理成VLM能够接受的输入格式,包括input_ids、attention_mask、图像Tensor以及视觉相关的网格信息等。模型的forward函数没有额外的复杂逻辑,
def forward(self, inputs: dict) -> BatchFeature:
backbone_inputs, action_inputs = self.prepare_input(inputs)
backbone_outputs = self.backbone(backbone_inputs)
action_outputs = self.action_head(backbone_outputs, action_inputs)
return action_outputs
这段代码将整个训练过程分成三步,prepare_input -> backbone -> action_head
prepare_input会将同一个输入字典拆成两部分。图像、文本等数据交给Backbone,状态、动作、动作Mask和embodiment_id交给动作头,
backbone_inputs = self.backbone.prepare_input(inputs)
action_inputs = self.action_head.prepare_input(inputs)
随后使用统一函数把Tensor移动到模型所在设备:
def to_device_with_dtype(x):
if torch.is_floating_point(x):
return x.to(self.device, dtype=self.dtype)
else:
return x.to(self.device)
浮点Tensor不仅会移动到GPU,还会转换为模型当前使用的数据类型;整数类型的Token ID和Mask只移动设备,不进行浮点转换。
4、动作头由状态编码器、动作编码器和DiT组成
动作头类名为Gr00tN1d7ActionHead,
class Gr00tN1d7ActionHead(nn.Module):
"""Action head component for flow matching diffusion policy."""
注释中已经直接写出了flow matching diffusion policy。这个表述容易让人误以为代码同时实现了两套方法,实际上这里只实现一套动作生成流程:使用DiT预测Flow Matching速度场。 动作头首先根据配置创建DiT,
if config.use_alternate_vl_dit:
self.model = AlternateVLDiT(
**config.diffusion_model_cfg,
cross_attention_dim=config.backbone_embedding_dim,
attend_text_every_n_blocks=config.attend_text_every_n_blocks,
)
else:
self.model = DiT(
**config.diffusion_model_cfg,
cross_attention_dim=config.backbone_embedding_dim,
)
正常配置下使用DiT,开启use_alternate_vl_dit后使用AlternateVLDiT。两种结构都会将状态和动作特征作为主序列,并通过交叉注意力读取视觉语言特征。下面几个成员负责状态和动作的维度映射,
self.state_encoder = CategorySpecificMLP(
num_categories=config.max_num_embodiments,
input_dim=config.max_state_dim * config.state_history_length,
hidden_dim=self.hidden_size,
output_dim=self.input_embedding_dim,
)
self.action_encoder = MultiEmbodimentActionEncoder(
action_dim=self.action_dim,
hidden_size=self.input_embedding_dim,
num_embodiments=config.max_num_embodiments,
)
self.action_decoder = CategorySpecificMLP(
num_categories=config.max_num_embodiments,
input_dim=self.hidden_size,
hidden_dim=self.hidden_size,
output_dim=self.action_dim,
)
state_encoder将机器人状态映射到DiT的输入维度;action_encoder将连续动作和时间步编码为动作Token;action_decoder将DiT输出还原为连续动作速度。
这里使用了CategorySpecificMLP和MultiEmbodimentActionEncoder,原因是不同机器人本体的数据分布不同。即使两台机器人都使用7维动作,其中每一维的物理含义也可能不同。通过embodiment_id选择类别相关参数,可以在共享DiT主体的同时,为不同机器人保留各自的输入输出映射。
动作头还会记录以下参数,
self.action_dim = config.max_action_dim
self.action_horizon = config.action_horizon
self.num_inference_timesteps = config.num_inference_timesteps
action_dim是补齐后的最大动作维度,action_horizon表示模型一次预测多少步动作,num_inference_timesteps表示推理时执行多少次速度积分。
5、动作头同时控制参数梯度和模块运行模式
动作头通过set_trainable_parameters控制不同模块是否参与训练,
def set_trainable_parameters(
self,
tune_projector: bool,
tune_diffusion_model: bool,
tune_vlln: bool,
):
for p in self.parameters():
p.requires_grad = True
if not tune_projector:
self.state_encoder.requires_grad_(False)
self.action_encoder.requires_grad_(False)
self.action_decoder.requires_grad_(False)
if self.config.add_pos_embed:
self.position_embedding.requires_grad_(False)
if not tune_diffusion_model:
self.model.requires_grad_(False)
if not tune_vlln:
self.vlln.requires_grad_(False)
self.vl_self_attention.requires_grad_(False)
tune_projector控制状态编码器、动作编码器、动作解码器以及位置编码;tune_diffusion_model控制DiT主体;tune_vlln控制视觉语言特征进入动作头之前的LayerNorm和自注意力层。代码中还实现了set_frozen_modules_to_eval_mode,
def set_frozen_modules_to_eval_mode(self):
if self.training:
if not self.tune_projector:
self.state_encoder.eval()
self.action_encoder.eval()
self.action_decoder.eval()
if not self.tune_diffusion_model:
self.model.eval()
if not self.tune_vlln:
self.vlln.eval()
self.vl_self_attention.eval()
仅设置requires_grad=False并不会自动将模块切换到评估模式。Hugging Face Trainer在训练期间会调用model.train(),被冻结模块中的Dropout等训练态行为仍可能生效,因此源码又在forward开头调用set_frozen_modules_to_eval_mode(),将这些模块重新切换到eval()状态。
6、Flow Matching在噪声和真实动作之间构造训练轨迹
动作头forward函数的输入包括视觉语言特征和动作相关数据,
def forward(
self,
backbone_output: BatchFeature,
action_input: BatchFeature,
) -> BatchFeature:
Backbone输出中主要包含,
backbone_features: [B, seq_len, backbone_embedding_dim]
backbone_attention_mask:[B, seq_len]
动作输入中主要包含,
state: [B, state_history, state_dim]
action: [B, action_horizon, action_dim]
embodiment_id: [B]
action_mask: [B, action_horizon, action_dim]
首先对Backbone特征做额外处理,
backbone_output = self.process_backbone_output(backbone_output)
vl_embeds = backbone_output.backbone_features
process_backbone_output中包含LayerNorm和可选的视觉语言自注意力层,
def process_backbone_output(self, backbone_output):
backbone_features = backbone_output["backbone_features"]
backbone_features = self.vlln(backbone_features)
backbone_features = self.vl_self_attention(backbone_features)
backbone_output["backbone_features"] = backbone_features
return backbone_output
接着处理机器人状态。状态历史会先展平,
assert action_input.state.shape[1] == self.config.state_history_length
action_input.state = action_input.state.view(
action_input.state.shape[0],
1,
-1,
)
假设状态历史长度为4,每一帧状态维度为32,展平后得到128维向量,再由state_encoder映射为一个状态Token,
state_features = self.state_encoder(
action_input.state,
embodiment_id,
)
训练阶段还会按概率丢弃完整状态特征,
if self.training and self.state_dropout_prob > 0:
do_dropout = (
torch.rand(
state_features.shape[0],
device=state_features.device,
)
< self.state_dropout_prob
)
do_dropout = do_dropout[:, None, None].to(
dtype=state_features.dtype
)
state_features = state_features * (1 - do_dropout)
这里不是对状态向量中的单个元素做Dropout,而是对一个样本的整个状态Token置零。这样训练出来的模型不会完全依赖机器人状态,在状态传感器存在噪声或缺失时仍能利用图像和语言生成动作。Flow Matching目标的构造集中在下面几行,
actions = action_input.action
noise = torch.randn(
actions.shape,
device=actions.device,
dtype=actions.dtype,
)
t = self.sample_time(
actions.shape[0],
device=actions.device,
dtype=actions.dtype,
)
t = t[:, None, None]
noisy_trajectory = (1 - t) * noise + t * actions
velocity = actions - noise
模型输入noisy_trajectory、时间步以及条件信息,输出该位置上的速度。训练目标不是预测高斯噪声,也不是直接恢复干净动作,而是预测从噪声指向真实动作的速度方向。
时间t并不是均匀采样,而是由Beta分布产生,
self.beta_dist = Beta(
config.noise_beta_alpha,
config.noise_beta_beta,
)
def sample_time(self, batch_size, device, dtype):
sample = self.beta_dist.sample([batch_size]).to(
device,
dtype=dtype,
)
sample = (1 - sample) * self.config.noise_s
return sample
Beta分布可以控制训练样本更多地落在轨迹的哪个区域。不同于简单的均匀采样,这种方式可以调整模型对高噪声阶段和低噪声阶段的学习比例。
连续时间t随后被离散化,
t_discretized = (
t[:, 0, 0] * self.num_timestep_buckets
).long()
离散时间步会传给动作编码器和DiT,
action_features = self.action_encoder(
noisy_trajectory,
t_discretized,
embodiment_id,
)
如果启用了位置编码,还会给动作序列中的每一个时间位置添加独立Embedding,
if self.config.add_pos_embed:
pos_ids = torch.arange(
action_features.shape[1],
dtype=torch.long,
device=device,
)
pos_embs = self.position_embedding(pos_ids).unsqueeze(0)
action_features = action_features + pos_embs
这样DiT可以区分动作块中的第0步、第1步和后续时间步。
7、DiT根据多模态条件预测动作速度并计算损失
状态特征和动作特征会在序列维度拼接,
sa_embs = torch.cat(
(state_features, action_features),
dim=1,
)
如果状态被编码为1个Token,动作块长度为16,那么拼接后的序列长度就是17,
[state_token, action_0, action_1, ..., action_15]
视觉语言特征不会直接拼接进这个序列,而是作为交叉注意力的encoder_hidden_states输入DiT,
model_output, _ = self.model(
hidden_states=sa_embs,
encoder_hidden_states=vl_embeds,
encoder_attention_mask=vl_attn_mask,
timestep=t_discretized,
return_all_hidden_states=True,
)
这种结构可以看成条件生成模型。主序列是机器人状态和动作,视觉语言特征提供环境和任务条件。每一层DiT都可以根据当前图像和语言指令更新动作Token。
DiT输出经过动作解码器,
pred = self.action_decoder(
model_output,
embodiment_id,
)
由于输出中还包含状态Token对应的位置,因此只保留最后的动作部分,
pred_actions = pred[:, -actions.shape[1]:]
动作损失采用逐元素MSE,
action_loss = F.mse_loss(
pred_actions,
velocity,
reduction="none",
)
随后乘以action_mask。动作Mask的作用是忽略补零维度以及无效时间步。GR00T需要支持不同机器人,一些机器人可能只有7维动作,另一些机器人可能有20维动作,模型内部会统一补齐到max_action_dim,但补齐部分不应参与损失计算。
源码随后将逐元素MSE乘以action_mask,再根据有效动作元素的数量计算平均损失,
action_loss = action_loss * action_mask
loss = action_loss.sum() / (
action_mask.sum() + 1e-6
)
分母使用有效元素数量,而不是直接对整个Tensor求平均,可以避免不同动作维度和不同Padding长度对损失尺度造成影响。
8、推理阶段从高斯噪声逐步生成动作序列
训练阶段学习速度场,推理阶段则沿着该速度场进行积分。动作生成入口为,
Gr00tN1d7.get_action(...)
主模型先处理输入并计算视觉语言特征,
backbone_inputs, action_inputs = self.prepare_input(inputs)
backbone_outputs = self.backbone(backbone_inputs)
action_outputs = self.action_head.get_action(
backbone_outputs,
action_inputs,
options,
)
动作头首先创建一段高斯噪声,
actions = torch.randn(
size=(
batch_size,
self.config.action_horizon,
self.action_dim,
),
dtype=vl_embeds.dtype,
device=device,
)
然后根据推理步数计算步长,
dt = 1.0 / self.num_inference_timesteps
如果num_inference_timesteps=4,每次积分步长就是0.25。
每轮迭代都会对当前动作轨迹重新编码,
for t in range(self.num_inference_timesteps):
t_cont = t / float(self.num_inference_timesteps)
t_discretized = int(
t_cont * self.num_timestep_buckets
)
timesteps_tensor = torch.full(
size=(batch_size,),
fill_value=t_discretized,
device=device,
)
action_features = self.action_encoder(
actions,
timesteps_tensor,
embodiment_id,
)
随后将状态Token和动作Token拼接,交给DiT,
sa_embs = torch.cat(
(state_features, action_features),
dim=1,
)
model_output = self.model(
hidden_states=sa_embs,
encoder_hidden_states=vl_embeds,
timestep=timesteps_tensor,
)
动作解码器输出当前轨迹上的速度,
pred = self.action_decoder(
model_output,
embodiment_id,
)
pred_velocity = pred[:, -self.action_horizon:]
最后使用Euler方法更新动作,
actions = actions + dt * pred_velocity * vel_strength