第四阶段重点追 “VLA 输出的 64-D token 是怎样真正进入 SONIC 的”。这一阶段我们开始跨越两个 repo / 两个进程 / 两种推理频率,分析一个真正部署中的系统。

重点围绕源码中的 run_vla_inference.pyvla_utils.pylaunch_inference.py 以及 VLA inference tutorial 来追踪。


阶段四:VLA → Universal Token → SONIC

先给出这一阶段完整链路:

Camera ───────────────┐
                      │
Robot state ──────────┤
                      ▼
                VLA Inference Client
                run_vla_inference.py
                      │
                      ▼
              Isaac-GR00T PolicyClient
                      │
                 ZMQ REQ/REP
                      │
                      ▼
              Isaac-GR00T PolicyServer
                      │
                      ▼
                   VLA Model
                      │
                      │  output:
                      │  motion_token[40,64]
                      │  left_hand[40,7]
                      │  right_hand[40,7]
                      ▼
              action chunk: 78 × 40
                      │
              latency compensation
                      │
                      ▼
              current 78-D action
                      │
                ZMQ PUB / v4
                      │
                      ▼
             gear_sonic_deploy
                      │
             token_state[64]
                      │
                      ▼
             SONIC Decoder
                      │
                 proprioception
                      │
                      ▼
              G1 joint commands
                      │
                      ▼
                    G1

官方 VLA workflow 对这个系统的概括也是:VLA 预测 SONIC latent motion tokens,SONIC 在 50 Hz 下把 latent 解码为全身控制;当前 unitree_g1_sonic action space 为 78 维,即 64-D motion token + 左右手各 7-D。(GitHub)

这里已经可以先得到一个非常重要的结论:

VLA 与 SONIC 之间真正交换的是 motion token,而不是 29-DOF body joint action。

这正是 Universal Token 在整个系统里的实际位置。


1. 先看部署系统的三个进程

实际运行时,至少可以从逻辑上拆成三个主要组件。

第一层:Isaac-GR00T PolicyServer

GPU 机器运行:

Isaac-GR00T
    │
    └── PolicyServer
          │
          └── ZMQ REQ/REP

官方教程要求通过 run_gr00t_server.py 启动 PolicyServer,并指定 UNITREE_G1_SONIC embodiment。(GitHub)

例如:

uv run python gr00t/eval/run_gr00t_server.py \
    --model-path /path/to/your/finetuned_model \
    --embodiment-tag UNITREE_G1_SONIC \
    --device cuda:0 \
    --port 5550

这里真正运行 VLA neural network 的是 Isaac-GR00T


第二层:run_vla_inference.py

这个进程是桥梁。

它同时连接:

  • Camera Server
  • C++ SONIC controller
  • PolicyServer
  • Keyboard publisher

源码开头的模块说明非常直接:robot state 通过 ZMQ SUB 进入,camera 通过 ZMQ/TCP 进入,VLA action 通过 ZMQ PUB 发出去,PolicyServer 则通过 ZMQ REQ/REP 访问。(GitHub)

可以画成:

                         ┌──────────────────┐
                         │  PolicyServer    │
                         │  Isaac-GR00T     │
                         └────────▲─────────┘
                                  │ REQ/REP
                                  │
Camera ──────►                    │
Robot state ─► run_vla_inference ─┤
                                  │
                                  ▼
                         ZMQ latent action
                                  │
                                  ▼
                         C++ SONIC Deploy

所以 run_vla_inference.py 本身并不运行 VLA backbone。

它承担的是 sensor → VLA server → action chunk → SONIC 的实时 orchestration。


2. VLA 的输入

先从 prepare_observation_from_sensors() 看。

源码首先读取 camera:

camera_subscriber.read()

然后读取 robot state:

state_subscriber.get_msg()

之后从 state 中拿到 G1 当前 joint configuration,并组织成 VLA 所需要的 observation dictionary。(GitHub)

核心结构可以简化成:

observation = {
    "video": {
        "ego_view": ...
        # optional wrist views
    },
    "state": {},
    "language": {
        "annotation.human.task_description": [[language_prompt]]
    },
    "q": ...,
    "timestamps": ...
}

然后:

prepare_observation_for_eval(robot_model, observation)

会把整个 G1 的 q 按照 joint groups 拆成:

left_arm
right_arm
waist
left_leg
right_leg
left_hand
right_hand

源码中的 prepare_observation_for_eval() 就是在做这个映射。(GitHub)

此外,SONIC embodiment 还需要 projected gravity:

projected_gravity = compute_projected_gravity(base_quat)
observation["state"]["projected_gravity"] = ...

所以 VLA 实际看到的输入并不是单纯的视频。

它至少包含:

o V L A = ( video , robot state , language ) \boxed{ o_{\mathrm{VLA}}= (\text{video},\text{robot state},\text{language}) } oVLA=(video,robot state,language)

这和我们前面讨论的 SONIC policy observation 有一个重要区别:这里的高层模型直接接收视觉和语言,SONIC controller 接收的是已经压缩成 motion token 的行为意图。


3. VLA 输出

核心调用非常短:

action, _info = policy.get_action(observation)

也就是 PolicyClient 向远端 PolicyServer 请求 action。(GitHub)

接下来代码寻找:

motion_key = (
    "motion_token"
    if "motion_token" in action
    else "action.motion_token"
)

这说明 VLA 的 action dictionary 中明确存在 motion_token 字段。

随后代码检查:

if np.abs(action[motion_key]).max() > 1.25:
    ...
    return None

也就是说,VLA 输出的 motion token 在进入 SONIC 之前还有一个 action-bound sanity check。(GitHub)

关键维度:64-D × 40

官方文档说 Sonic embodiment 的 action space 是:

64 + 7 + 7 = 78 64+7+7=78 64+7+7=78

也就是:

a t V L A = [ z t 64 , h t L 7 , h t R 7 ] a_t^{VLA}= [ z_t^{64}, h_t^L{}^{7}, h_t^R{}^{7} ] atVLA=[zt64,htL7,htR7]

但实际上 VLA 每次 inference 返回的是一个默认 T c h u n k = 40 T_{\mathrm{chunk}}=40 Tchunk=40action chunk

源码配置中:

action_horizon: int = 40

而 publish rate 是:

action_publish_rate: int = 50

VLA forward 的默认频率是:

rate: float = 1 / 0.4

也就是 2.5 Hz。(GitHub)

因此一次 VLA inference 的输出可以理解成:

A t ∈ R 40 × 78 \boxed{ A_t \in \mathbb R^{40\times78} } AtR40×78

其中:

A t = { ( z t 64 , h t L , h t R ) , … , ( z t + 39 64 , h t + 39 L , h t + 39 R ) } A_t= \{ (z_{t}^{64},h_t^L,h_t^R), \ldots, (z_{t+39}^{64},h_{t+39}^L,h_{t+39}^R) \} At={(zt64,htL,htR),,(zt+3964,ht+39L,ht+39R)}

如果按照 50 Hz 执行:

40 / 50 = 0.8  s 40/50=0.8\text{ s} 40/50=0.8 s

所以 VLA 每次低频 inference 会给出大约 0.8 秒的未来 action chunk。这是整个系统非常关键的时间尺度设计。


5. 所以 VLA 和 SONIC 的频率完全不同

现在可以把系统时间尺度画出来:

				VLA inference
				   │
				   │ 2.5 Hz
				   ▼
┌──────────────────────────────────────────┐
│          action chunk: 40 steps          │
│                                          │
│ z0 z1 z2 z3 ... z39                      │
└──────────────────────────────────────────┘
                     │
                     │ 50 Hz
                     ▼
             SONIC controller
             20 ms / action
                     │
                     ▼
                  Robot

所以:

f V L A = 2.5  Hz f_{\mathrm{VLA}}=2.5\text{ Hz} fVLA=2.5 Hz

而:

f S O N I C = 50  Hz f_{\mathrm{SONIC}}=50\text{ Hz} fSONIC=50 Hz

两者相差:

50 / 2.5 = 20 50/2.5=20 50/2.5=20

也就是说,一次 VLA forward 对应大约 20 个 SONIC control cycles,而当前配置给出的 action horizon 是 40,所以 chunk 覆盖约 800 ms。(GitHub)

这里有一个很值得注意的设计:

VLA 负责低频的行为决策,SONIC 接收一串未来 latent actions,并以高频执行。

这和传统“VLA 每 20 ms 直接输出 joint action”的设计差异很大。


6. VLA action chunk 进入 SONIC 前发生了什么?

run_policy_inference_and_process() 调用:concat_action(robot_model, action)

这个函数实际非常简单:

processed_goal = {}
for key, value in goal.items():
    processed_goal[key.replace("action.", "")] = value
return processed_goal

也就是说,它主要负责清理 action dictionary 的 key。(GitHub)

例如:

action.motion_token
        ↓
   motion_token

以及:

action.left_hand_joints
        ↓
left_hand_joints

这里没有重新编码 token,也没有把 token 转换成 joint angle。

这非常重要。

因此从 VLA 输出到 SONIC 输入之间:

z V L A 64 ≈ z S O N I C 64 \boxed{ z_{\mathrm{VLA}}^{64} \approx z_{\mathrm{SONIC}}^{64} } zVLA64zSONIC64

中间承担的是 协议包装、时间索引和通信。这正是“Universal Token interface”最直接的代码证据。


7. VLA 输出的 64-D token 到底是什么?

现在可以结合第二阶段重新看。

SONIC 训练时:

x m o t i o n → E → F S Q → z ∈ R 64 x_{\mathrm{motion}} \rightarrow E \rightarrow FSQ \rightarrow z\in\mathbb R^{64} xmotionEFSQzR64

而 VLA 部署时:

o v i s i o n + l a n g u a g e + s t a t e → VLA → z ∈ R 64 o_{\mathrm{vision+language+state}} \rightarrow \text{VLA} \rightarrow z\in\mathbb R^{64} ovision+language+stateVLAzR64

然后:

z → SONIC decoder → q t a r g e t z \rightarrow \text{SONIC decoder} \rightarrow q_{\mathrm{target}} zSONIC decoderqtarget

因此 VLA 学习的目标实际上可以理解成:

VLA learns to produce coordinates in SONIC’s latent action space \boxed{ \text{VLA learns to produce coordinates in SONIC's latent action space} } VLA learns to produce coordinates in SONIC’s latent action space

这就是为什么这个接口非常强。

VLA 不需要学习:

  • G1 balance
  • locomotion
  • whole-body coordination
  • joint-level stabilization

这些能力已经存在于 SONIC controller 中。

VLA 学习的是:

visual/language context → SONIC latent trajectory \text{visual/language context} \rightarrow \text{SONIC latent trajectory} visual/language contextSONIC latent trajectory

官方 workflow 也明确把这一设计描述为:VLA 预测 64-D SONIC latent motion tokens,SONIC 再将其解码为全身 joint commands。(GitHub)


8. 为什么还要有左右手 7-D?

这是一个非常值得注意的细节。完整 VLA action 是: 78 = 64 + 7 + 7 78=64+7+7 78=64+7+7。其中: 64 = SONIC body motion token 64=\text{SONIC body motion token} 64=SONIC body motion token 7 + 7 = left/right hand joints 7+7=\text{left/right hand joints} 7+7=left/right hand joints。所以实际上 VLA 的输出 interface 是:

VLA Action
├── motion_token : 64
├── left_hand_joints : 7
└── right_hand_joints : 7

而 SONIC token 部分负责 whole-body motion。

注意这里是手,不是手臂!手部 action 则作为额外字段随同 token 一起进入 deployment protocol。官方文档明确将 unitree_g1_sonic 的 action space 定义成这 78 维结构。(GitHub)

所以更准确的表达应该是:

a V L A = ( z S O N I C , a h a n d ) \boxed{ a_{\mathrm{VLA}}= (z_{\mathrm{SONIC}},a_{\mathrm{hand}}) } aVLA=(zSONIC,ahand)


9. 进入 ZMQ:真正的 Universal Token Protocol

接下来来到非常关键的 pack_latent_action_message(),通讯部分。

源码:

pose_data = {
    "token_state": motion_token,
    "frame_index": frame_index,
}

if left_hand_joints is not None:
    pose_data["left_hand_joints"] = left_hand_joints

if right_hand_joints is not None:
    pose_data["right_hand_joints"] = right_hand_joints

return pack_pose_message(
    pose_data,
    topic="pose",
    version=4,
)

(GitHub)

这里已经可以看到协议的核心字段:

token_state
frame_index
left_hand_joints
right_hand_joints

因此一条发送给 C++ controller 的消息可以抽象成:

M t = ( z t 64 , i t , h t L , h t R ) M_t= ( z_t^{64}, i_t, h_t^L, h_t^R ) Mt=(zt64,it,htL,htR)

其中 i t i_t it 是 frame index。


10. frame_index 为什么存在?

这个字段很容易被忽略。主循环每发送一次 action:

frame_index = np.array([zmq_frame_counter], dtype=np.int64)
zmq_frame_counter += 1

然后一起发给 C++。(GitHub)

因此:

i t = 0 , 1 , 2 , 3 , … i_t=0,1,2,3,\ldots it=0,1,2,3,

它给 latent action 一个明确的时间序列编号。这对于异步系统非常有意义,因为现在存在三个时间尺度:

VLA inference time action chunk time actual SONIC control time \text{VLA inference time}\\ \text{action chunk time}\\ \text{actual SONIC control time} VLA inference timeaction chunk timeactual SONIC control time

frame_index 可以帮助 deployment stack 判断 action 的时序。


11. 更关键的是:VLA 并不会等 SONIC 执行完 40 帧才继续

这里就是系统设计里很漂亮的一部分。

VLA inference 是异步 worker:

inference_worker_thread = threading.Thread(
    target=_inference_worker_loop,
    ...
)

worker 单独执行:

prepare observation
       ↓
PolicyClient.get_action()
       ↓
processed action chunk

而主线程继续以 50 Hz 发送 cached action。(GitHub)

所以系统实际上是:

             VLA Worker
                 │
                 │ 2.5 Hz
                 ▼
        ┌────────────────┐
        │ action chunk   │
        │ 40 future step │
        └───────┬────────┘
                │
                ▼
          cached_action
                │
                │
                ▼
Main Loop ──── 50 Hz ────► ZMQ ───► SONIC

这就是一个典型的 asynchronous receding-horizon action streaming


12. 为什么叫 receding horizon?

假设 VLA 在时刻 t t t 输出:

[ z t , z t + 1 , . . . , z t + 39 ] [z_t,z_{t+1},...,z_{t+39}] [zt,zt+1,...,zt+39]

理论上这些 token 可以覆盖未来 800 ms。但系统不会把它们一次性发送给 SONIC。而是:

z t → z t + 1 → z t + 2 → ⋯ z_t \rightarrow z_{t+1} \rightarrow z_{t+2} \rightarrow \cdots ztzt+1zt+2

每 20 ms 发送一个。

与此同时,大约 400 ms 后再次进行 VLA inference,并得到新的:

[ z t + 20 ′ , . . . , z t + 59 ′ ] [z_{t+20}',...,z_{t+59}'] [zt+20,...,zt+59]

于是未来轨迹不断被重新预测。

这就形成:

predict chunk → execute part → predict again → replace future \boxed{ \text{predict chunk} \rightarrow \text{execute part} \rightarrow \text{predict again} \rightarrow \text{replace future} } predict chunkexecute partpredict againreplace future

这比简单的“一次 VLA inference → 执行完整 40 帧”更加适合真实机器人。


13. 代码里的 inference scheduling

默认:

T V L A = 0.4 s T_{\mathrm{VLA}}=0.4s TVLA=0.4s

也就是:

f V L A = 2.5 H z f_{\mathrm{VLA}}=2.5Hz fVLA=2.5Hz

should_trigger_new_inference() 会检查:

  • 有没有 cached action chunk;
  • 当前 inference worker 是否忙;
  • 距离上次 inference 是否超过 inference_interval

源码逻辑是:

if not cached_chunk_exists:
    return True

if inference_thread_running:
    return False

return time_since_last_inference >= inference_interval

(GitHub)

因此默认状态下:

new VLA inference every  0.4 s \boxed{ \text{new VLA inference every }0.4s } new VLA inference every 0.4s

与此同时:

new SONIC token every  0.02 s \boxed{ \text{new SONIC token every }0.02s } new SONIC token every 0.02s

于是一个 VLA chunk 的 40 个动作可以覆盖:

40 × 0.02 = 0.8 s 40\times0.02=0.8s 40×0.02=0.8s

而新的 chunk 每 0.4 s 左右刷新一次。

这意味着 action chunk 之间存在明显 overlap。


14. 这个 overlap 非常重要

可以画成:

time →
0ms                400ms               800ms              1200ms

VLA #1
[z0 z1 z2 ... z19 z20 ... z39]
────────────────────────────────

              VLA #2
              [z'0 z'1 ... z'19 z'20 ... z'39]
              ────────────────────────────────

                           VLA #3
                           [z''0 ...]
                           ────────────

所以在 400 ms 左右,系统已经得到新的未来计划。这给了 VLA 一个持续修正行为轨迹的机会。


15. 但是新的 chunk 刚回来时,不能直接从 z0 开始

因为 VLA 从开始 inference 到返回结果已经消耗了一段时间。

假设:

Δ t = 120 m s \Delta t=120ms Δt=120ms

控制频率:

50 H z 50Hz 50Hz

那么大约已经经过 120 × 50 / 1000 = 6 120\times50/1000=6 120×50/1000=6 个 control steps, 如果此时仍然发送新 chunk 的 z0,这个 action 已经对应过去的时间。所以代码专门计算:

action_chunk_index = calculate_latency_compensated_index(
    inference_delay,
    config.action_publish_rate,
    config.action_horizon,
)

而函数内部核心公式是:

i 0 = clip ⁡ ( round ⁡ ( Δ t f c ) , 0 , H − 1 ) i_0= \operatorname{clip} \left( \operatorname{round}(\Delta t f_c), 0, H-1 \right) i0=clip(round(Δtfc),0,H1)

(GitHub)


16. 这意味着 VLA latency 被直接映射成 chunk offset

举个具体例子。

假设:

Δ t = 160 m s \Delta t=160ms Δt=160ms

那么:

i 0 = 0.16 × 50 = 8 i_0= 0.16\times50= 8 i0=0.16×50=8

所以新 chunk:

[z0,z1,z2,z3,z4,z5,z6,z7,z8,z9,...]

不会从 z0 开始。

而是直接从:

z8

开始。

这就是 latency compensation。

它的意义可以写成:

execution time = prediction time + communication time + compute time \boxed{ \text{execution time}= \text{prediction time} + \text{communication time} + \text{compute time} } execution time=prediction time+communication time+compute time

系统通过跳过已经 stale 的 action,使 action 的时间语义尽量重新对齐。


17. 当前 action 是怎么从 chunk 中取出来的?

主循环拿到:

motion_token
left_hand_joints
right_hand_joints

这些数组来自模型时是:

( B , T , D ) (B,T,D) (B,T,D)

代码首先 squeeze batch dimension,得到:

( T , D ) (T,D) (T,D)

然后:

current_idx = min(action_chunk_index, horizon - 1)

再选择:

motion_token = motion_token[current_idx]

左右手同理。(GitHub)

于是最终每 20 ms 只产生:

z t ∈ R 64 z_t\in\mathbb R^{64} ztR64

和:

h t L , h t R ∈ R 7 h_t^L,h_t^R\in\mathbb R^7 htL,htRR7

然后发给 C++。


18. VLA 到 SONIC 的接口

可以把整个数据转换过程压缩成:

VLA observation
    │
    ├── image
    ├── robot q
    ├── projected gravity
    └── language
    │
    ▼
Isaac-GR00T PolicyServer
    │
    ▼
action chunk
    │
    ├── motion_token: [40, 64]
    ├── left_hand:    [40, 7]
    └── right_hand:   [40, 7]
    │
    ▼
latency compensation
    │
    ▼
one timestep
    │
    ├── token: [64]
    ├── left hand: [7]
    └── right hand: [7]
    │
    ▼
ZMQ Protocol v4
    │
    ├── token_state
    ├── frame_index
    ├── left_hand_joints
    └── right_hand_joints
    │
    ▼
C++ SONIC

这基本就是第四阶段最核心的源码级结论。


19. 澄清:VLA 并没有调用 SONIC Encoder

这是我们前面分析 SONIC architecture 后,现在可以非常明确地看出来的。

训练路径:

motion reference → E → F S Q → z → D \text{motion reference} \rightarrow E \rightarrow FSQ \rightarrow z \rightarrow D motion referenceEFSQzD

VLA deployment path:

vision/language/state → VLA → z → D \text{vision/language/state} \rightarrow \text{VLA} \rightarrow z \rightarrow D vision/language/stateVLAzD

因此 deployment 时:

E S O N I C  被跳过 \boxed{ E_{\mathrm{SONIC}} \text{ 被跳过} } ESONIC 被跳过

VLA 直接进入 SONIC latent space。

这也是我们上一阶段提到的 rollout_with_tokens() 背后的意义:SONIC Actor 本身支持直接接受 external pre-computed FSQ tokens。当前实际 VLA deployment 又进一步把这个思想实现成了 C++/TensorRT token interface。UniversalTokenModule 的源码也明确支持直接利用外部 token 后进入 decoder 的路径。(GitHub)


20. 所以 SONIC 在 VLA 系统里实际上变成了什么?

如果站在整个系统的角度,SONIC 的角色已经发生变化。

训练时:

SONIC = motion encoder + latent representation + controller \boxed{ \text{SONIC}= \text{motion encoder} + \text{latent representation} + \text{controller} } SONIC=motion encoder+latent representation+controller

VLA deployment 时:

SONIC = latent-conditioned whole-body controller \boxed{ \text{SONIC}= \text{latent-conditioned whole-body controller} } SONIC=latent-conditioned whole-body controller

也就是:

z t + s t → a t \boxed{ z_t + s_t \rightarrow a_t } zt+stat

VLA 负责:

observation → z t : t + H \text{observation} \rightarrow z_{t:t+H} observationzt:t+H

SONIC 负责:

( z t , s t ) → whole-body control (z_t,s_t) \rightarrow \text{whole-body control} (zt,st)whole-body control

这其实是整个 GEAR-SONIC 系统最值得研究的 architectural separation。


21. 为什么这种接口适合 VLA?

传统 VLA 如果直接输出 G1 joint action:

o t → VLA → q t : t + H 29 o_t \rightarrow \text{VLA} \rightarrow q_{t:t+H}^{29} otVLAqt:t+H29

那么 VLA 需要同时学习:

  • locomotion;
  • balance;
  • arm coordination;
  • whole-body coupling;
  • joint-space smoothness;
  • contact behavior;
  • physical feasibility。

而 SONIC 把这些能力放到了一个已经训练好的 controller 中,于是 VLA 只需要学习:

o t → z t o_t \rightarrow z_t otzt

或者更准确:

o t → z t : t + 39 o_t \rightarrow z_{t:t+39} otzt:t+39

这相当于把 action space 从一个非常具体的 joint-level space 转成一个 behavior-conditioned latent space

官方 VLA workflow 对这一点的描述也是:VLA 预测 compact 64-D motion tokens,SONIC 负责 balance、locomotion 和 whole-body coordination。(GitHub)


我们现在可以提出一个比“Universal Token 很方便”更严格的问题:

VLA 输出的 64-D token 是否真的等价于 SONIC 训练过程中 encoder 产生的 latent?

从代码接口看,它们被设计成相同的 action interface。

但从学习机制上,它们来源不同:

z S O N I C = Q ( E S O N I C ( x ) ) z_{\mathrm{SONIC}}= Q(E_{\mathrm{SONIC}}(x)) zSONIC=Q(ESONIC(x))

而:

z V L A = F V L A ( o ) z_{\mathrm{VLA}}= F_{\mathrm{VLA}}(o) zVLA=FVLA(o)

真正希望满足的是:

F V L A ( o ) ≈ z S O N I C \boxed{ F_{\mathrm{VLA}}(o) \approx z_{\mathrm{SONIC}} } FVLA(o)zSONIC

更重要的是,应该是 decoder-equivalent

D ( F V L A ( o ) , s ) ≈ D ( z S O N I C , s ) D(F_{\mathrm{VLA}}(o),s) \approx D(z_{\mathrm{SONIC}},s) D(FVLA(o),s)D(zSONIC,s)

这其实才是 VLA fine-tuning 真正需要解决的问题。VLA 不一定需要逐维复现 SONIC encoder 的 latent,只要产生的 token 能让 SONIC decoder 输出正确行为即可。因此更合理的目标可能是:

D ( z V L A , s ) ≈ a e x p e r t \boxed{ D(z_{\mathrm{VLA}},s) \approx a_{\mathrm{expert}} } D(zVLA,s)aexpert

而不是:

z V L A ≈ z e x p e r t z_{\mathrm{VLA}} \approx z_{\mathrm{expert}} zVLAzexpert

这两种训练目标在研究上是完全不同的。


23. 这解释了为什么 VLA fine-tuning 数据要记录 latent action

官方 data collection workflow 会记录 teleoperation demonstrations,并把数据整理成 LeRobot dataset;VLA fine-tuning 后,部署时由 PolicyServer 产生 Sonic latent action。(GitHub)

Inference tutorial 还特别指出:数据 parquet 的第一帧可以找到 action.motion_token,这个 token 可以作为 initial pose 的 latent。(GitHub)

所以数据集里实际上存在这样一种 supervision:

o t → z t e x p e r t o_t \rightarrow z_t^{expert} otztexpert

这给 VLA 提供了一个非常自然的 action target。

于是整个 fine-tuning 可以理解为:

camera + language + robot state → SONIC latent action \boxed{ \text{camera + language + robot state} \rightarrow \text{SONIC latent action} } camera + language + robot stateSONIC latent action

而不是:

camera + language → 29 -DOF joint action \text{camera + language} \rightarrow 29\text{-DOF joint action} camera + language29-DOF joint action


24. Initial pose 进一步证明了 latent 是可直接执行的

代码里有一个:

LATENT_INITIAL_MOTION_TOKEN

它本身就是一个 64-D token。

按下 i 时,系统直接把这个 token 包成 ZMQ action:

pack_latent_action_message(
    motion_token=LATENT_INITIAL_MOTION_TOKEN,
    frame_index=np.array([0], dtype=np.int64),
    left_hand_joints=left_hand,
    right_hand_joints=right_hand,
)

然后发送给 C++ controller。(GitHub)

更有意思的是,如果机器人当前已经有一个 token,系统会在 latent space 中直接做线性 interpolation:

z ( α ) = ( 1 − α ) z c u r r e n t + α z i n i t i a l z(\alpha)= (1-\alpha)z_{\mathrm{current}} + \alpha z_{\mathrm{initial}} z(α)=(1α)zcurrent+αzinitial

源码就是:

blended_token = (
    (1.0 - alpha) * start_token
    + alpha * target_token
).astype(np.float32)

(GitHub)

这个设计说明当前工程把 64-D token 当成了一个可以连续操作的 motion-control representation。当然,这并不能单独证明 latent space 在几何意义上具有严格线性语义,但至少说明工程上已经利用它进行连续 motion transition。


25. 另一个工程约束:checkpoint-specific latent space

官方文档明确提醒:

每个 SONIC checkpoint 都有自己的 latent space。

因此同一个 64-D token 在不同 SONIC checkpoint 下可能对应不同 pose / behavior。(GitHub)

这意味着 z z z 的语义不是一个跨模型绝对固定的公共坐标系。更准确地说:

z ∈ Z S O N I C ( k ) z\in\mathcal Z_{\mathrm{SONIC}^{(k)}} zZSONIC(k)

其中 k k k 是具体 SONIC checkpoint。因此:

VLA checkpoint ↔ SONIC checkpoint \boxed{ \text{VLA checkpoint} \leftrightarrow \text{SONIC checkpoint} } VLA checkpointSONIC checkpoint

需要匹配。

这也是为什么 v1.1 的 VLA 必须使用匹配的 v1.1 controller/config。官方教程明确要求 VLA 如果针对 robot-heading-normalized SONIC controller 训练,就使用对应的 sonic_v1_1 deployment files。(GitHub)


26. 第四阶段数学系统

我们现在可以把整个 VLA + SONIC deployment 写成:

高层 VLA

A t : t + H = F ϕ ( I t , s t , l ) A_{t:t+H}= F_\phi( I_t, s_t, l ) At:t+H=Fϕ(It,st,l)

其中:

A t : t + H = [ z t : t + H 64 , h t : t + H L , h t : t + H R ] A_{t:t+H}= \left[ z_{t:t+H}^{64}, h^L_{t:t+H}, h^R_{t:t+H} \right] At:t+H=[zt:t+H64,ht:t+HL,ht:t+HR]

当前:

H = 40 H=40 H=40


Latency compensation

i 0 = clip ⁡ ( round ⁡ ( Δ t f c ) , 0 , H − 1 ) i_0= \operatorname{clip} \left( \operatorname{round} (\Delta t f_c), 0,H-1 \right) i0=clip(round(Δtfc),0,H1)

然后执行:

z t + i 0 z_{t+i_0} zt+i0


SONIC

a t = D S O N I C ( z t , s t ) a_t= D_{\mathrm{SONIC}} (z_t,s_t) at=DSONIC(zt,st)

最终:

I t , l , s t ⟶ V L A z t : t + 39 ⟶ S O N I C a t : t + 39 ⟶ G 1 motion \boxed{ I_t,l,s_t \overset{\mathrm{VLA}}{\longrightarrow} z_{t:t+39} \overset{\mathrm{SONIC}}{\longrightarrow} a_{t:t+39} \overset{\mathrm{G1}}{\longrightarrow} \text{motion} } It,l,stVLAzt:t+39SONICat:t+39G1motion


27. 跨模型接口

前三阶段我们得到的是:

Motion → Encoder → FSQ → Token → Policy → Action \text{Motion} \rightarrow \text{Encoder} \rightarrow \text{FSQ} \rightarrow \text{Token} \rightarrow \text{Policy} \rightarrow \text{Action} MotionEncoderFSQTokenPolicyAction

第四阶段把上游换成 VLA:

Vision + Language + State → VLA → 64-D Token → SONIC → G1 \boxed{ \text{Vision + Language + State} \rightarrow \text{VLA} \rightarrow \text{64-D Token} \rightarrow \text{SONIC} \rightarrow \text{G1} } Vision + Language + StateVLA64-D TokenSONICG1

所以现在可以比较有把握地说:

Universal Token 在当前系统里确实承担了 VLA 与 humanoid whole-body controller 之间的 action interface。

这个判断直接有源码依据:VLA action dictionary 明确包含 motion_token;deployment protocol 明确发送 [64]token_state;C++ SONIC 侧接收 latent action;官方文档把整个 VLA action space 定义为 64-D motion token + 两组 7-D hand joints。(GitHub)


28. 5 个点

第一,VLA 与 SONIC 是异步的。

2.5 H z VLA 2.5Hz\quad\text{VLA} 2.5HzVLA

负责生成未来 action chunk;

50 H z SONIC 50Hz\quad\text{SONIC} 50HzSONIC

负责高频执行。


第二,VLA 输出的是 latent trajectory。

一次输出:

40 × 64 40\times64 40×64

的 motion tokens,同时带有:

40 × 7 + 40 × 7 40\times7+40\times7 40×7+40×7

的双手 action。


第三,VLA 到 SONIC 中间没有重新编码。

核心路径是:

z V L A → ZMQ → z S O N I C   d e c o d e r z_{\mathrm{VLA}} \rightarrow \text{ZMQ} \rightarrow z_{\mathrm{SONIC\ decoder}} zVLAZMQzSONIC decoder

所以 64-D latent 是真正的跨模块接口。


第四,系统使用 latency compensation。

VLA inference 产生的 action chunk 会因为计算和通信产生时间偏移,系统根据实际 inference delay 跳过 stale actions。


第五,Universal Token 的真正价值现在才完整显现。

训练阶段:

motion → z → controller \text{motion} \rightarrow z \rightarrow \text{controller} motionzcontroller

部署阶段:

VLA → z → controller \text{VLA} \rightarrow z \rightarrow \text{controller} VLAzcontroller

于是同一个 controller 可以接受:

  • G1 motion reference;
  • SMPL;
  • VR teleoperation;
  • VLA。

这正是 SONIC “universal-token controller” 这个名字在系统层面的含义。(GitHub)


预告:距离完整真机链路只差最后一段

目前已经追到了:

VLA → 64 D   t o k e n → Z M Q → C++ SONIC \boxed{ \text{VLA} \rightarrow 64D\ token \rightarrow ZMQ \rightarrow \text{C++ SONIC} } VLA64D tokenZMQC++ SONIC

但我们还没有真正深入 C++ SONIC 收到 token_state 之后发生什么。这正好进入 阶段五:SONIC Deploy / TensorRT / C++ → G1 真机

下一阶段直接从 gear_sonic_deploydeploy.sh、ONNX encoder/decoder、observation config、ZMQ manager 和 control loop 开始追,最终回答一个非常关键的问题:

token_state [ 64 ] → TensorRT decoder → proprioception → joint command → G1 motor \boxed{ \text{token\_state}[64] \rightarrow \text{TensorRT decoder} \rightarrow \text{proprioception} \rightarrow \text{joint command} \rightarrow \text{G1 motor} } token_state[64]TensorRT decoderproprioceptionjoint commandG1 motor

尤其要把 为什么部署端仍然需要 encoder、为什么 VLA 路径可以绕过 encoder、C++ decoder 的输入究竟是什么、50 Hz token 如何转成底层电机控制 这几个问题彻底厘清。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐