【SONIC源码阅读系列5.1】VLA-Interface-SONIC
第四阶段重点追 “VLA 输出的 64-D token 是怎样真正进入 SONIC 的”。这一阶段我们开始跨越两个 repo / 两个进程 / 两种推理频率,分析一个真正部署中的系统。
重点围绕源码中的 run_vla_inference.py、vla_utils.py、launch_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=40 的 action 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} } At∈R40×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} } zVLA64≈zSONIC64
中间承担的是 协议包装、时间索引和通信。这正是“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} xmotion→E→FSQ→z∈R64
而 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+state→VLA→z∈R64
然后:
z → SONIC decoder → q t a r g e t z \rightarrow \text{SONIC decoder} \rightarrow q_{\mathrm{target}} z→SONIC decoder→qtarget
因此 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 context→SONIC 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 zt→zt+1→zt+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 chunk→execute part→predict again→replace 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,H−1)
(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} zt∈R64
和:
h t L , h t R ∈ R 7 h_t^L,h_t^R\in\mathbb R^7 htL,htR∈R7
然后发给 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 reference→E→FSQ→z→D
VLA deployment path:
vision/language/state → VLA → z → D \text{vision/language/state} \rightarrow \text{VLA} \rightarrow z \rightarrow D vision/language/state→VLA→z→D
因此 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+st→at
VLA 负责:
observation → z t : t + H \text{observation} \rightarrow z_{t:t+H} observation→zt: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} ot→VLA→qt: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 ot→zt
或者更准确:
o t → z t : t + 39 o_t \rightarrow z_{t:t+39} ot→zt: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}} zVLA≈zexpert
这两种训练目标在研究上是完全不同的。
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} ot→ztexpert
这给 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 state→SONIC latent action
而不是:
camera + language → 29 -DOF joint action \text{camera + language} \rightarrow 29\text{-DOF joint action} camera + language→29-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)}} z∈ZSONIC(k)
其中 k k k 是具体 SONIC checkpoint。因此:
VLA checkpoint ↔ SONIC checkpoint \boxed{ \text{VLA checkpoint} \leftrightarrow \text{SONIC checkpoint} } VLA checkpoint↔SONIC 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,H−1)
然后执行:
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,st⟶VLAzt:t+39⟶SONICat:t+39⟶G1motion
27. 跨模型接口
前三阶段我们得到的是:
Motion → Encoder → FSQ → Token → Policy → Action \text{Motion} \rightarrow \text{Encoder} \rightarrow \text{FSQ} \rightarrow \text{Token} \rightarrow \text{Policy} \rightarrow \text{Action} Motion→Encoder→FSQ→Token→Policy→Action
第四阶段把上游换成 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 + State→VLA→64-D Token→SONIC→G1
所以现在可以比较有把握地说:
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}} zVLA→ZMQ→zSONIC 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} motion→z→controller
部署阶段:
VLA → z → controller \text{VLA} \rightarrow z \rightarrow \text{controller} VLA→z→controller
于是同一个 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} } VLA→64D token→ZMQ→C++ SONIC
但我们还没有真正深入 C++ SONIC 收到 token_state 之后发生什么。这正好进入 阶段五:SONIC Deploy / TensorRT / C++ → G1 真机。
下一阶段直接从 gear_sonic_deploy 的 deploy.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 decoder→proprioception→joint command→G1 motor
尤其要把 为什么部署端仍然需要 encoder、为什么 VLA 路径可以绕过 encoder、C++ decoder 的输入究竟是什么、50 Hz token 如何转成底层电机控制 这几个问题彻底厘清。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)