现在开始整个系列的Phase 0 + Phase 1。这一部分会刻意放慢一点,因为后面 Universal Token、PPO 和 deployment 都建立在这里的数据定义之上。
这次直接以当前 main 分支代码为准,而不是只根据 SONIC report 复述。当前 repo 已经发展到包含 Default SONIC、Low-latency SONIC 和 v1.1,因此个别参数会明确区分“原始 SONIC”和“当前代码”。(GitHub)
Phase 0: VLA → SONIC 的边界
0.1 最终部署系统
当前 repo 给出的 VLA inference 是一个很好的入口:
Camera / Prompt │ ▼ ┌─────────────┐ │ VLA │ │ Isaac-GR00T │ └──────┬──────┘ │ action / motion │ ▼ ┌─────────────────┐ │ SONIC │ │ C++ WBC │ └────────┬────────┘ │ joint control │ ▼ G1launch_inference.py实际上就是把这个系统串起来:它启动 C++ SONIC deployment,同时启动 Python VLA inference client、keyboard publisher,以及可选的数据 exporter。(GitHub)
所以这里首先要纠正一个非常容易产生的误解:SONIC 并不负责“理解 pick up the cup”。
例如:
"pick up the cup" │ ▼ VLA [task → action / motion intent] │ ▼ motion/action │ ▼ SONIC [motion intent / reference → physically executable whole-body behavior] │ ▼ G1 motion这也是为什么 NVIDIA 可以把两者独立训练、独立部署。当前官方 VLA workflow 就是:
teleop data ↓ fine-tune Isaac-GR00T ↓ PolicyServer ↓ launch_inference.py ↓ SONIC ↓ G1而不是重新训练一个端到端:image + language → motor torque的巨大模型。(GitHub)
0.2 重要发现:SONIC Action Space
这里开始出现一个非常有意思的接口。当前 VLA Inference 文档明确写:
unitree_g1_sonic的 action space 是78 dimensions:(GitHub)
64-dim motion token + 7-dim left hand joints + 7-dim right hand joints = 78这意味着:
VLA │ ▼ ┌──────────────────┐ │ 78-D action │ ├──────────────────┤ │ 64-D motion token│ │ 7-D left hand │ │ 7-D right hand │ └────────┬─────────┘ │ ▼ SONIC这说明一个非常重要的架构事实:SONIC 的 64-D token 并不只是内部 latent。至少在当前 VLA 接口中,它已经成为一个可以由上层 policy 操作的action-level interface。
问题:
Universal Token 到底是 latent representation,还是 robot control interface?
初步判断是:
两者兼具。
但我们后面必须通过代码把它证明出来,而不是先下结论。
0.3 SONIC 并不是“VLA 后面的 PD Controller”
这一点也非常重要。传统机器人 pipeline:
VLA ↓ target pose ↓ IK ↓ joint target ↓ PD ↓ motorSONIC 更像:
VLA / Motion Source ↓ motion representation ↓ ┌──────────────────────┐ │ SONIC │ │ │ │ representation │ │ ↓ │ │ whole-body policy │ │ ↓ │ │ robot action │ └──────────┬───────────┘ ↓ low-level所以 SONIC 的贡献不是做了一个更好的 IK,而是把 whole-body coordination 本身学习成一个 generalist policy。这和 SONIC report 的定位一致:官方现在把 SONIC 定义为 humanoid behavior foundation model,通过 scalable motion tracking 从大规模 human motion 中学习统一的 whole-body behavior。(GitHub)
Phase 1:Motion Reference → Observation
现在正式进入源码。整个 SONIC 从这里开始,因为:
policy 学什么,首先由 observation 决定。
1.1 Training 的真正入口
当前训练命令:
python gear_sonic/train_agent_trl.py \ +exp=manager/universal_token/all_modes/sonic_release(GitHub)
入口:gear_sonic/train_agent_trl.py
train_agent_trl.py │ ├── Hydra config │ ├── environment │ ├── actor │ ├── critic │ └── trainer然后 Hydra 加载 architecture:
config/base.yaml │ ▼ exp/manager/universal_token/all_modes/sonic_release.yaml │ ├── algo ├── actor_critic ├── manager_env ├── observations ├── rewards ├── terminations ├── events └── aux_losses官方 training-code 文档已经把这条 composition chain 明确列出来。(GitHub)
1.2 Configuration hierarchy
当前 release 的核心配置是:
sonic_release │ ├── algo │ └── ppo_im_phc │ ├── actor_critic │ └── universal_token/all_mlp_v1 │ ├── manager_env │ │ │ ├── observations │ │ ├── tokenizer │ │ ├── policy │ │ └── critic │ │ │ ├── rewards │ │ │ ├── terminations │ │ │ └── events │ ├── aux_losses │ └── trainer └── PPO + auxiliary loss(GitHub)
这已经开始暴露出 SONIC 的核心结构:
Environment │ ┌────────────┼────────────┐ │ │ │ ▼ ▼ ▼ tokenizer policy critic │ │ │ ▼ ▼ ▼ Encoder Actor Value │ ▼ Universal Token │ ▼ Decoder注意:tokenizer 和 policy 是两个不同的 observation group。
1.3 SONIC 的 Observation 并不是一个 vector
看当前:gear_sonic/envs/manager_env/mdp/observations.py。它定义了大量 observation terms。比如 policy group 里包括:
joint_pos joint_vel base_ang_vel gravity_dir actions以及大量 motion-reference terms:
command_multi_future command_multi_future_joint_pos command_multi_future_joint_body_pos command_multi_future_joint_body_diff_pos ... smpl_joints_multi_future ... motion_anchor_ori_b_mf ...同时还有 teleoperation:
vr_3point_target_multi_future vr_3point_orn_target_multi_future vr_wrists_local_pos_target vr_wrists_local_orn_target vr_head_local_orn_target等等。(GitHub)
因此更准确的抽象应该是:
Observation │ ┌─────────────────────┼─────────────────────┐ │ │ │ ▼ ▼ ▼ Motion Reference Robot State Teleop / SMPL │ │ │ └─────────────────────┬─────────────────────┘ ▼ observation dict然后再根据用途拆成:
obs_dict │ ├── tokenizer ├── policy ├── critic ├── ...1.4 为什么要拆成 tokenizer / policy / critic?
这是第一个值得研究的设计。官方文档明确:
Policy
给 actor 的:
joint_pos joint_vel base_ang_vel gravity_dir last_actions ...Critic
可以拿到 privileged information:
base_lin_vel body_pos body_ori ...Tokenizer
给 UniversalTokenModule:
multi-future joint commands SMPL joints VR targets anchor orientations ...(GitHub)
因此:
Environment │ ┌───────┴───────┐ │ │ motion ref robot state │ │ ▼ ▼ ┌──────────┐ ┌──────────┐ │ Tokenizer│ │ Policy │ │ Obs │ │ Obs │ └────┬─────┘ └────┬─────┘ │ │ ▼ │ Universal │ Token │ │ │ └──────┬────────┘ ▼ Actor这个结构说明:
SONIC 的 motion representation learning 和 control policy 并不是完全相同的东西。
它们之间存在一个明确的 bottleneck:
motion observations ↓ tokenizer ↓ universal token ↓ actor这就是下一阶段要重点扒开 Universal Token 的原因。
1.5 Motion Reference 来自哪里?
进入:
gear_sonic/envs/manager_env/mdp/commands.py核心类:
TrackingCommand当前代码对它的定义非常明确:
TrackingCommand是 SONIC-style motion tracking RL environment 的 primary command term。
它负责:
- 加载 motion library
- sample motion ID
- sample episode start time
- 每个 simulation step 推进 motion cursor
- 提供 current reference
- 提供 multi-future reference
- 提供 joint/body/root/SMPL 等数据
- 做 robot-local / egocentric / heading-canonicalized transformation。(GitHub)
所以整个 motion pipeline 实际上是:
Motion Dataset │ ▼ Motion Library │ ▼ TrackingCommand │ ┌────────┴────────┐ │ │ current frame future frames │ │ └────────┬────────┘ ▼ observations1.6 Future Reference 是怎么生成的?
这里终于可以把 report 里的“lookahead”落实到代码。
当前配置:
target_fps = 50 Hz num_future_frames = 10 dt_future_ref_frames = 0.1 s来自当前configuration.md。(GitHub)
但是这里有一个非常值得注意的细节:
当前 training config 的
dt_future_ref_frames是0.1 s。
也就是说训练配置中的未来 reference:
t t + 100ms t + 200ms ...而模型 card 对 released deployment model 描述的是:
10 future frames 20 ms spacing ≈ 200 ms lookahead(GitHub) 因此:不要简单把 README 的 release checkpoint observation protocol 和当前 training config 当成完全相同的东西。这是我们后面必须仔细追的一个版本差异。
TrackingCommand内部实际上按照:
frame_skips=dt_future_ref_frames*target_fps计算未来帧间隔,然后:
future_time_steps_init=arange(num_future_frames)*frame_skips也就是:
future_time_steps = [0, frame_skip, 2*frame_skip, ...](GitHub) 这才是实际 motion sampling mechanism。
1.7 所以一个 Motion Clip 到底发生了什么?
假设某条 motion:
M = {x0, x1, x2, ..., xN}每个 motion frame 是:
x_t = { root pose body pose joint pose velocity orientation ... }episode reset 时:
motion_id = k start_time = t0然后第n个 RL step:
current_time = t0 + n * ΔtTrackingCommand 给出:
x(t) x(t + Δt_ref) x(t + 2Δt_ref) ... x(t + HΔt_ref)因此 policy 看到的不是:
“机器人现在应该在哪里”而更接近:
“机器人现在在哪里 + 未来一段时间 reference 会怎么变化”这其实是 SONIC 的一个非常重要的 inductive bias:
control policy 是 trajectory-conditioned,而不是 single-pose-conditioned。
1.8 更重要:Reference 会被变换到机器人坐标系
observations.py里面大量出现:
*_b *_l *_w *_mf这几个后缀非常值得我们在以后形成条件反射:
w = world frame b = body / local frame l = local / heading-related frame mf = multi-future例如:
motion_anchor_ori_b_mf可以理解成:
motion anchor orientation ↓ body/local frame ↓ multiple future frames而:
smpl_joints_multi_future就是:
SMPL joints ↓ multiple future frames1.9 为什么不直接把 World-frame Pose 给 Policy?
这是 SONIC 非常典型的 robot learning design。假设:
reference: root = (10m, 5m)policy 其实不应该关心:“机器人在世界坐标的 x=10, y=5。”
它应该关心: “reference 相对于我现在的位置在哪里?”
所以代码大量做:
world frame ↓ anchor frame ↓ robot local frame例如motion_anchor_gravity_dir():
world gravity ↓ inverse(anchor quaternion) ↓ anchor-local gravity代码直接实现了这个变换。(GitHub)
这说明 SONIC 的 observation design 在努力做到:
把 global trajectory 转换成 robot-centric control problem。
1.10 一个特别值得关注的变化:v1.1
这里和当前 repo 的 evolution 有关系。
最新 SONIC v1.1 使用:
robot-heading-normalized target orientation
并加入:
wrist-pose augmentation
官方说这是为了提高 whole-body teleoperation 和 VLA execution 的 heading stability。(GitHub)
这说明一个很有研究价值的事实:SONIC 的 performance 很大程度上取决于 reference coordinate representation。
不是:
network bigger → performance better而可能是:
better reference parameterization ↓ easier learning problem ↓ better whole-body control1.11 Phase-1 数据流
现在不要急着进入 network。先把我们已经追到的部分固定下来:
Motion Dataset │ ▼ Motion Library │ ▼ TrackingCommand │ ┌───────────┼────────────┐ │ │ │ current future SMPL frame frames frames │ │ │ └───────────┼────────────┘ │ ▼ coordinate transforms │ ┌──────────────┼──────────────┐ │ │ │ ▼ ▼ ▼ Tokenizer Policy Critic observations observations observations │ │ │ │ │ │ ▼ │ ▼ Encoder │ Value network │ │ ▼ │ Universal Token │ │ │ └──────┬───────┘ ▼ Actor [Decoder] │ ▼ Action1.12 Decoder的输入
之前说:
Motion → Encoder → 64-D token → Policy [Decoder] → Action
这个说法作为高层理解没错,但现在看源码后需要精确化。
当前UniversalTokenModule的代码 docstring 实际写的是:
tokenizer_obs ↓ encoder(s) ↓ latent ↓ FSQ ↓ token ↓ decoder(s) ↓ action_mean而且 decoder 还可以同时接收:
token + proprioception也就是说更加准确的是:
motion/reference │ ▼ tokenizer obs │ ▼ ┌──────────────────┐ │ Encoder(s) │ │ │ │ G1 │ │ SMPL │ │ Teleop │ └────────┬─────────┘ │ pre-quant latent │ ▼ FSQ │ universal token │ ▼ ┌──────────────────┐ │ Decoder(s) │ │ │ │ + proprioception │ └────────┬─────────┘ │ ▼ action_mean当前源码明确把它实现成:
UniversalTokenModule并且其forward()明确返回:
action_meanaux_lossesdecoded_outputstokenizer_obsencoder_masksencoded_tokensencoded_latents
(GitHub)这其实比我们最开始画的架构更加有意思。
1.13 不止“一个 Encoder”
当前UniversalTokenModule明确是:
encoders = ModuleDict可以有:
g1 smpl teleop soma ...当前 release configuration 使用:
G1 Encoder SMPL Encoder Teleop Encoder而 extended Bones-SEED configuration 还可以加入 SOMA encoder。(GitHub)
所以真正的 architecture 是:
G1 reference │ ▼ G1 Encoder ──┐ │ SMPL ────────┤ │ │ ▼ │ SMPL Encoder ──┼──→ FSQ → shared token │ Teleop ──────┤ │ │ ▼ │ Teleop Encoder ─┘这才是 Universal Token 真正的含义。
1.14 那么“Universal”究竟是什么意思?
现在我们可以给出一个暂时性的、源码支持的定义:
Universal 并不是说一个 encoder 能理解所有 motion。
恰恰相反:
G1 → G1 encoder SMPL → SMPL encoder Teleop → Teleop encoder每一种输入都有自己的 encoder。
真正 universal 的地方是:
different modality ↓ different encoder ↓ same shared latent/token space ↓ common decoder/control interface即:
modality-specific encoders │ ┌────────────┼────────────┐ │ │ │ G1 SMPL Teleop │ │ │ ▼ ▼ ▼ Enc-G1 Enc-SMPL Enc-Teleop │ │ │ └────────────┼────────────┘ ▼ FSQ │ ▼ Universal Motion Token │ ▼ G1 Decoder │ ▼ Robot Action这其实是笔者认为整个 SONIC architecture 最漂亮的地方之一。
1.15 跨模态对齐机制
当前代码中明确出现了 encoder mask combinations:
g1 ↔ smpl teleop ↔ smpl g1 ↔ teleop g1 ↔ soma以及 auxiliary losses:
G1-SMPL alignment cycle consistency ...UniversalTokenModule的 forward 还专门保留:
encoded_latents encoded_tokens aux_losses encoder_masks(GitHub)
这意味着 Universal Token 并不是:
三个 encoder ↓ 简单 concat ↓ decoder而是在训练过程中存在一种:
让不同 encoder 产生相互兼容 latent 的机制。
这就是我们下一章必须重点拆开的地方。
1.16 最初的第一个研究问题
Q:SONIC 到底看到了什么?
答案现在已经相当清楚:
它同时拥有三类信息:
① Reference motion
current + future可能来自:
G1 SMPL Teleop② Robot proprioception
例如:
joint_pos joint_vel base_ang_vel gravity_dir last_actions③ Privileged state(训练 critic)
例如:
base_lin_vel body_pos body_ori height map ...(GitHub)
所以它是:
Motion Reference │ ▼ Token │ │ Robot State ───────────┤ ▼ Controller │ ▼ Action也就是:
motion-conditioned + state-feedback control
1.17 第二个研究问题:为什么要 future reference?
现在也可以更准确地回答。如果只给:
x_ref(t)policy 需要自己从:
x_ref(t)推断:
dx_ref/dt d²x_ref/dt² future contact future balance而现在直接给:
x_ref(t) x_ref(t+Δ) x_ref(t+2Δ) ...于是 policy 实际上获得:
trajectory context因此它学习的是:
π(a_t | s_t, R_{t:t+H})这对 humanoid 尤其重要,因为当前 pose 通常不足以决定:
下一步脚该落在哪里 身体 momentum 怎么变化 手臂应该如何配合所以 future reference 是一个非常合理的control inductive bias。
1.18 一个小问题
既然 SONIC 给了 future trajectory:
为什么还需要一个 learned latent token?
理论上完全可以:
future reference + robot state ↓ MLP ↓ action为什么还要:
reference ↓ encoder ↓ FSQ ↓ 64D token ↓ decoder ↓ action判断是:Universal Token 并不主要是为了“压缩”。真正的意义更接近:
把不同 motion modalities 映射到一个统一的 control representation。
也就是:
SMPL G1 Teleop SOMA ... ↓ shared latent ↓ same controller这比单纯 compression 更重要。下一阶段我们验证这个判断。
Phase 1 小结
┌──────────────┐ │ VLA / Teleop │ └──────┬───────┘ │ ▼ Motion Reference │ ┌────────────┼────────────┐ │ │ │ G1 SMPL Teleop │ │ │ └────────────┼────────────┘ ▼ TrackingCommand │ ▼ Current + Future Motion │ ▼ Coordinate Transform │ ┌─────────┴─────────┐ │ │ ▼ ▼ Tokenizer Policy │ │ ▼ │ Encoder(s) │ │ │ ▼ │ FSQ │ │ │ ▼ │ Universal Token │ │ │ └────────┬──────────┘ ▼ Decoder │ proprioception │ ▼ Action │ ▼ G1 / WBC下一阶段:进入核心
Phase 2 — Universal Token
不再做高层概括,直接沿着当前代码:
gear_sonic/trl/modules/universal_token_modules.py往下追。这个文件目前有1256 行,里面已经把 SONIC 的核心机制基本暴露出来了。(GitHub) 我们重点逐行搞清楚:
tokenizer_obs ↓ encoder_index ↓ encoder_masks ↓ G1 / SMPL / Teleop Encoder ↓ pre-quantization latent ↓ FSQ ↓ 64-D token ↓ latent residual ↓ G1 Dynamic Decoder ↓ action_mean尤其重点帮解决三个问题:
①64-D 到底是怎么来的?
不是只说“FSQ 输出 64-D”,而是把:
num_fsq_levels fsq_level_list max_num_tokens down_t全部对应到数学上的 latent shape。
②为什么 G1 / SMPL / Teleop 可以共用一个 token space?
我们会追:
encoder masks + encoder sampling + auxiliary losses + latent alignment + reconstruction最后回答:
这个 shared latent space 到底是怎么被训练出来的。
③为什么 Decoder 还要 proprioception?
这是一个非常关键的问题:
token + proprioception ↓ decoder ↓ action为什么 token 本身不能决定 action?
这个问题实际上会把我们带进 SONIC 最核心的representation vs control分工。