拆解 GR00T N1 双系统架构:System 2 看懂场景、System 1 毫秒出手,2.2B 参数与 63.9ms 背后的每一根管线(附官方源码对照)
本文把 GR00T N1 的架构从输入到输出完整走一遍。文中代码段摘自 NVIDIA 开源的 Isaac-GR00T 官方仓库(Apache 2.0 协议)——论文 N1 与仓库当前主线 N1.7 在骨干网络上迭代过,但 Flow-Matching 动作生成的核心实现完全同构。对启元 Q1 的架构启发放在第六节。
一、全局视角:一张图看懂数据流
先把论文的研究背景钉在墙上:一个模型,要听得懂语言、看得见场景,还要在毫秒级节奏里输出机器人动作,而且最好跨机型通用。GR00T N1 给出的答案是双系统:理解交给 VLM,行动交给 Flow-Matching DiT,两者以交叉注意力耦合、端到端联合训练。下面从输入到输出把整条管线拆开。
先上整体管线。GR00T N1 的前向过程只有三步:
图像观测 + 语言指令 ──>[System2: Eagle-2 VLM]──>视觉语言特征 φ_t │ 机器人状态 q_t ──────>[System1: DiT]<── 交叉注意力 ──┘ │ │ 加噪动作块 A_τ ──>去噪4步 │v动作块 A_t(16步)[Figure 2: GR00T N1 模型概览——System 2 与 System 1 的耦合关系]
两个系统不是串联的两级流水线,而是条件生成关系:System 2 每 100ms 算一次 φ_t,System 1 拿着 φ_t 当条件,以 120Hz 的节奏持续生成动作。System 2 在两次推理之间是"冻结"的——就像人眼扫一眼环境后,手会连续执行几秒动作,不需要每 8ms 重新"看"一次。
二、System 2:把 VLM 当特征提取器用
2.1 Eagle-2 骨干的三个关键参数
GR00T N1 的 VLM 骨干是 NVIDIA 自家的 Eagle-2,由 SigLIP-2 视觉编码器 + SmolLM2 语言模型组成。GR00T-N1-2B 总参数 2.2B,其中 VLM 占 1.34B。
有三个设计细节值得抠:
细节一:图像 token 数量。输入图像统一缩放到 224×224,经过 pixel shuffle 后,每帧只产生 64 个图像 token。这个数字很关键——多视角输入(比如左右腕三个相机)时 token 数线性增长,64/帧的控制直接决定了 VLM 的推理延迟。作为对比,很多 VLA 工作单帧用 256+ token,VLM 一跑就是几百毫秒。
细节二:取中间层特征。这是我认为整篇论文最"反直觉但合理"的一个选择:VLM 特征取 LLM 的第 12 层,不是最后一层。官方仓库的实现比"取特征"更彻底——直接把 12 层以上的所有层物理删除:
# gr00t/model/modules/qwen3_backbone.py# needed since we don't use these layers. Also saves computewhilelen(self.model.language_model.layers)>select_layer:self.model.language_model.layers.pop(-1)配合配置文件里的默认值 select_layer: int = 12(gr00t/configs/model/gr00t_n1d7.py),这意味着:加载 VLM 时上层的语言头、未用到的 Transformer 层压根不进显存。中间层不是"从 32 层里挑一层读出来",而是模型在结构上就只有 12 层。
为什么不用最后一层?论文的实验结论是:中间层特征推理更快(不用跑完整个 LLM),且下游策略成功率更高。直观解释:最后一层特征过度"语言化",抽象成了语义概念;而动作生成需要的是保留空间细节的视觉-语言混合表征。第 12 层刚好卡在"看懂了但还没说出口"的位置。
细节三:聊天模板对齐。训练策略时,文本描述和图像按 VLM 预训练时的 chat 格式输入。这个细节保证了策略训练不破坏 VLM 的预训练知识。
2.2 Q1 视角:这个设计能用吗
Q1 没有条件在设备端跑 1.34B 的 VLM。但"取中间层 + 压缩图像 token"这两个思路可以移植到任何轻量 VLM 上——如果 Q1 未来要做云端理解模块,这两个选择能直接把推理成本砍一半以上。
三、System 1:Flow-Matching 动作生成
3.1 先讲清楚 Flow-Matching 在干什么
Diffusion Policy 用的是标准 DDPM 去噪,GR00T N1 用的是 Flow-Matching(流匹配)。两者都是"从噪声生成数据",区别在于:
- DDPM:学习每一步去噪的噪声残差,采样几十到上百步;
- Flow-Matching:学习一条从噪声分布到数据分布的速度场(向量场),欧拉积分 4 步就能走完。
对实时机器人控制来说,4 步 vs 100 步就是能不能上机的区别。
3.2 训练目标:源码里只有三行核心
论文公式 (1) 在官方仓库里的样子(gr00t/model/gr00t_n1d7/gr00t_n1d7.py):
# 加噪与速度场目标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]# shape (B,1,1) for broadcastnoisy_trajectory=(1-t)*noise+t*actions# A_τ = τ·A_t + (1-τ)·εvelocity=actions-noise# 速度场回归目标# ...... DiT 前向 ......(hidden_states=状态+加噪动作 token,# encoder_hidden_states=VLM 第 12 层特征,timestep=t)pred=self.action_decoder(model_output,embodiment_id)pred_actions=pred[:,-actions.shape[1]:]action_loss=F.mse_loss(pred_actions,velocity,reduction="none")*action_mask loss=action_loss.sum()/(action_mask.sum()+1e-6)# 按 action_mask 加权平均τ 的采样逻辑(同文件):
# __init__ 里构建 Beta 分布:浓度参数直接来自模型配置self.beta_dist=Beta(torch.tensor(float(config.noise_beta_alpha),dtype=torch.float32,device="cpu"),torch.tensor(float(config.noise_beta_beta),dtype=torch.float32,device="cpu"),)defsample_time(self,batch_size,device,dtype):sample=self.beta_dist.sample([batch_size]).to(device,dtype=dtype)sample=(1-sample)*self.config.noise_s# 截断到 s=0.999 以内returnsample配置文件 gr00t/configs/model/gr00t_n1d7.py 里的默认值:noise_beta_alpha: float = 1.5、noise_beta_beta: float = 1.0、noise_s: float = 0.999——与论文 Sec 2.1 写的 Beta(1.5, 1)、s=0.999 逐位一致,可以逐字对上。
三个值得注意的实现细节:
- 时间步分布不是均匀的。τ 从 Beta(1.5, 1) 采样且截断在 0.999 以内——Beta(1.5,1) 的概率质量偏向大 τ,意味着训练时模型更多见到"接近真实动作"的样本。这是从 π0(Black et al., 2024)继承的技巧,对收敛速度影响很大。
- τ 要一起编码进网络。扩散时间步通过 adaptive layer normalization 注入 DiT,而不是简单拼接;仓库里还能看到一个细节:连续 τ 会被离散化成桶(num_timestep_buckets: int = 1000)后作为条件输入。
- 动作块整体加噪。整个动作块作为一个整体加噪、去噪,保证块内时序一致性;损失计算乘了 action_mask 并做归一化,意味着机型可以屏蔽自己不存在的动作维度——这是多机型共用一个 DiT 的工程前提之一。
3.3 推理:4 步欧拉积分
仓库里的去噪主循环(gr00t_n1d7.py):
# 推理去噪主循环# Run denoising steps.fortinrange(self.num_inference_timesteps):t_cont=t/float(self.num_inference_timesteps)# e.g. goes 0, 1/N, 2/N, ...t_discretized=int(t_cont*self.num_timestep_buckets)# Embed noised action trajectory.timesteps_tensor=torch.full(size=(batch_size,),fill_value=t_discretized,device=device)action_features=self.action_encoder(actions,timesteps_tensor,embodiment_id)# ...... 状态/动作 token 拼接,与训练时完全一致 ......。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:]# Update actions using euler integration.actions=actions+dt*pred_velocity从纯噪声开始,每步让网络预测速度场,然后欧拉积分往前走一小步——论文说的 K=4 步去噪,落到代码里就是一个 range(4) 的 for 循环。num_inference_timesteps: int = 4 也写在配置默认值里。
16 步动作块(H=16)在 120Hz 控制频率下覆盖约 133ms。System 1 每 63.9ms 推理一次(L40 GPU, bf16),配合动作块滑动执行,整体等效 120Hz 输出。推理快、动作平滑,这两件事在 Flow-Matching 这里是同一件事。
3.4 机型适配:编码器/解码器的外挂设计
GR00T N1 支持单臂、双臂、人形 + 灵巧手等多种机型,靠的不是多个模型,而是机型特定的薄编码器 + 共享的 DiT 主体:
仓库里的证据是两部分。先是配置(gr00t/configs/model/gr00t_n1d7.py):
max_state_dim:int=132# 所有机型统一填充到这个维度max_action_dim:int=132max_num_embodiments:int=32# 支持注册的机型数上限input_embedding_dim:int=1536所有机型的状态/动作向量都填充(padding)到统一维度,每个机型分配一个独立的编码器槽位,DiT 主体参数完全共享。再看机型标签的注册表(gr00t/data/embodiment_tags.py):
classEmbodimentTag(Enum):"""Embodiment tags supported by the GR00T N1.7 checkpoint."""REAL_G1="real_g1_relative_eef_relative_joints"# 宇树 G1 真机UNITREE_G1="unitree_g1_full_body_with_waist_height_nav_cmd"LIBERO_PANDA="libero_sim"# 仿真 Panda# ...... 预训练/后训练机型各若干 ......。# New embodiment during post-trainingNEW_EMBODIMENT="new_embodiment"# 自定义机型的统一注册位""" Any new embodiment. """预训练机型各自一个标签,而所有自定义机型共用 NEW_EMBODIMENT 这一个注册位——新增一个机型,本质上只是新增一对 MLP 编码器/解码器加一个标签(百万级参数),DiT 主体(亿级参数)原封不动。这个设计对多机型产品线(比如启元未来若同时有 Q1 和其他形态产品)是直接的参考。
四、DiT 内部:自注意力管动作,交叉注意力管理解
DiT 块的结构是 Flamingo/VIMA 式的交替设计。仓库里的真实实现(gr00t/model/modules/dit.py):
# 交替自注意力/交叉注意力assertself.config.interleave_self_attention,"Interleave self attention must be enabled"foridx,blockinenumerate(self.transformer_blocks):ifidx%2==1:# Self-attention blocks —— 动作 token 之间交互(时序连贯性)hidden_states=block(hidden_states,...,temb=temb)else:# Cross-attention blocks - alternate between non-image and image tokensifidx%(2*self.attend_text_every_n_blocks)==0:curr_encoder_attention_mask=non_image_attention_maskelse:curr_encoder_attention_mask=image_attention_mask# Q=动作, K/V=VLM 特征,且图像 token 与文本 token 交替作为条件hidden_states=block(hidden_states,encoder_hidden_states=encoder_hidden_states,encoder_attention_mask=curr_encoder_attention_mask,temb=temb,)这里有个论文正文没有展开、但代码写得明明白白的细节:交叉注意力不是把 VLM 特征一锅端进来,而是 attend_text_every_n_blocks=2 的节奏——隔一个交叉注意力块换一类条件,一层只看图像 token,下一层只看文本 token。配置默认值 use_alternate_vl_dit: bool = True 说明这是主线模型的实际形态。扩散时间步 τ 则通过 AdaLayerNorm 注入每层。
这个分工非常清晰:
| 注意力类型 | 输入 | 学到什么 |
|---|---|---|
| 自注意力 | 加噪动作 token + 状态 token | 动作块内部的时序结构 |
| 交叉注意力 | Q=动作, K/V=VLM 特征 | “现在该做什么任务” |
| adaLN | 扩散时间步 τ | 当前处于去噪哪个阶段 |
对比一下 RT-2 的做法——把动作离散成 token 塞进 LLM 的词表里,用自回归方式"说"出动作。两条路线的本质区别:RT-2 让动作迁就语言模型的接口,GR00T N1 让语言模型的输出迁就动作生成的接口。后者对高频控制明显更友好。
五、模型规格速查
把 GR00T-N1-2B 的关键数字汇总成一张表,右列是官方仓库当前主线(N1.7,配置文件 gr00t_n1d7.py)的对照值:
| 项目 | 论文 N1-2B | 仓库主线 N1.7 |
|---|---|---|
| 总参数量 | 2.2B(VLM 1.34B + DiT 约 0.86B) | 3B(VLM 骨干换为 Cosmos-Reason2-2B) |
| 视觉编码 | 224×224 → 64 token/帧 | 230×230 裁剪 → 256×256(image_crop_size 配置) |
| VLM 特征层 | 第 12 层 | 第 12 层(select_layer = 12,未变) |
| 动作块长度 H | 16 步 | 40 步(action_horizon = 40) |
| 状态/动作维度 | 29 维(GR-1) | 统一填充到 132 维(max_state_dim) |
| 去噪步数 K | 4 | 4(num_inference_timesteps = 4,未变) |
| 时间步分布 | Beta(1.5, 1), s=0.999 | Beta(1.5, 1), s=0.999(未变) |
| 推理延迟 | 63.9 ms / 动作块(L40, bf16) | 仓库另提供 TensorRT 部署链路进一步压缩 |
可以看到:骨干网络和动作块长度在迭代,但 Flow-Matching 的核心机制(4 步去噪、Beta 采样、双系统解耦)从论文到仓库主线一直没动——这正是本篇值得精读的原因:这些是经过验证的稳定设计。
这套架构的实验回报,论文给得毫不含糊:三个仿真基准后训练平均成功率 45.0%,比从零训练的 Diffusion Policy(33.4%)高 11.6 个百分点,且机型越复杂优势越大(人形基准领先 17.3 个百分点);真机 13 个任务平均 76.8%(对 Diffusion Policy 的 46.4%);System 1 单次推理 63.9ms(L40, bf16),4 步去噪就能实时出动作块。架构设计的每一处取舍,最后都兑现成了延迟表和成功率表上的数字。
六、Q1 的架构路线图:三步走
把上面的技术点翻译成 Q1 视角的行动项:
第一步:复用预设动作体系的"动作原语"思路。Q1 现在的 set_preset_motion(3008) 这套预设动作,本质上就是动作空间离散化。在引入学习式策略之前,可以把每个预设动作当作一个"动作 token",用一个小型序列模型学"什么场景下按什么顺序组合预设动作"——这是最低成本的数据驱动化改造,且完全兼容现有 PrimeConsole 校验体系。
第二步:轻量化 System 1。Q1 的交互动作(招手、比心、舞蹈)都是上肢短程运动,动作维度远低于 GR-1 的全身灵巧手。一个 distilled 到 <100M 参数的 Flow-Matching 头,配合 Q1 现有动作接口,理论上有机会跑在设备端或边缘盒子。
第三步:云端 System 2 + 端侧 System 1。语言理解、场景识别放云端(走 Q1 现有的网络能力),动作生成放本地。这个分工和 Q1 现有"配置在云端管理、动作在本地执行"的架构是同构的,改造成本相对可控。
一句话总结:GR00T N1 真正可迁移的不是它的模型权重,而是"理解与行动解耦、动作生成用流匹配、机型差异用外挂编码器吸收"这三个设计决策。
论文信息
- GR00T N1: An Open Foundation Model for Generalist Humanoid Robots
- NVIDIA,2025 年 3 月
- arXiv:2503.14734
开源项目信息
- 项目地址:https://github.com/NVIDIA/Isaac-GR00T
- 项目定位:GR00T N1 系列基础模型的官方实现,Apache 2.0 协议;当前主线为 N1.7,本文引用代码均出自该主线
- 源码地图:gr00t/model/(模型实现,本文的 ActionHead 与 DiT 源码分别在 gr00t_n1d7/gr00t_n1d7.py 与 modules/dit.py)、gr00t/configs/(全部超参默认值)、gr00t/data/(数据处理与机型标签注册)
- 与论文对照:Flow-Matching 动作头、双系统耦合、4 步去噪、机型外挂编码器等核心机制与论文 N1 完全同构;骨干网络与动作块长度按 N1.7 演进
访问启元机器人开发者社区,获取 SDK、仿真工具、开发文档,完成从代码仿真到真机落地的全流程开发:https://dev.primebot.com/