【SONIC源码阅读系列5.2】VLA到Universal Token 到 SONIC
上一期文章的结构有点混乱,这一期重新组织成严格的层级:
4.1 系统架构 → 4.2 VLA 数据流 → 4.3 VLA 输出与 SONIC 接口 → 4.4 实时调度 → 4.5 一个完整时序例子 → 4.6 这一阶段真正要记住什么
第四阶段:VLA → Universal Token → SONIC
前面第三阶段已经得到:
Universal Token+Robot State→Policy→29-D Body Action \boxed{ \text{Universal Token} + \text{Robot State} \rightarrow \text{Policy} \rightarrow \text{29-D Body Action} } Universal Token+Robot State→Policy→29-D Body Action
第四阶段要解决的是:
这个 64-D Universal Token 从哪里来?VLA 如何把视觉、语言、机器人状态变成 SONIC 能理解的 token?
所以这一阶段的核心不是继续讲 SONIC policy 内部,而是把:
VLA
↓
64-D motion token
↓
SONIC
这条链真正接起来。
4.1 系统架构:实际运行时有哪些组件?
先只看系统级结构,暂时不展开输入和输出。当前官方 VLA inference 教程可以从逻辑上分成三个主要运行实体:
┌─────────────────────────────┐
│ ① Isaac-GR00T PolicyServer │
│ │
│ 运行 VLA neural network │
│ ZMQ REQ/REP │
└──────────────▲──────────────┘
│
│ VLA inference request
│
┌──────────────┴──────────────┐
│ ② run_vla_inference.py │
│ │
│ inference orchestrator │
│ sensor → VLA → action │
│ ZMQ bridge │
└──────────────┬──────────────┘
│
│ action / motion token
▼
┌─────────────────────────────┐
│ ③ C++ SONIC Deploy │
│ │
│ TensorRT controller │
│ → G1 │
└─────────────────────────────┘
这里一定要区分:
① PolicyServer
真正加载并运行 Isaac-GR00T VLA model。
例如官方教程使用:
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 在 PolicyServer 中运行 \boxed{\text{VLA neural network 在 PolicyServer 中运行}} VLA neural network 在 PolicyServer 中运行
② run_vla_inference.py
它不是 VLA backbone。它更像一个实时 orchestration / bridge:
Camera
Robot State
Language
│
▼
run_vla_inference.py
│
├── 请求 PolicyServer
│
├── 接收 VLA action chunk
│
├── latency compensation
│
└── 发布给 C++ SONIC
③ C++ SONIC Deploy
负责真正的机器人控制:
VLA action
↓
64-D token
↓
TensorRT SONIC
↓
29-D body action
↓
G1
所以这一层 C++ runtime 将在第五阶段详细分析。
4.1 的结论
到这里,我们只需要记住:
PolicyServer 负责“算 VLA”,
run_vla_inference.py负责“组织数据和传输”,C++ SONIC 负责“把 token 变成机器人控制”。
4.2 VLA 数据流:什么东西进入 VLA?
现在进入第二层:
VLA 到底接收什么 observation?
run_vla_inference.py 中的 prepare_observation_from_sensors() 会把传感器数据组织成 VLA observation。
整体可以抽象成:
VLA Observation
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
Camera Robot State Language
│ │ │
▼ ▼ ▼
ego_view joint q task_description
wrist views gravity
因此:
ot=(otvision,otstate,otlanguage) o_t = ( o_t^{vision}, o_t^{state}, o_t^{language} ) ot=(otvision,otstate,otlanguage)
4.2.1 Vision
当前代码支持:
ego_view
left_wrist_view
right_wrist_view
具体是否使用 wrist camera 取决于配置。
所以 VLA 可以获得:
机器人自身视角
+
手腕视角
用于判断当前场景和操作目标。
4.2.2 Robot State
机器人状态里包含 joint position 等 proprioception。源码还会根据 base quaternion 计算:
gbody=R(q)Tgworld g_{body}=R(q)^Tg_{world} gbody=R(q)Tgworld
也就是 projected gravity。
所以输入是:
image
+
robot proprioception
+
language
这点很重要,因为 VLA 输出的是机器人动作接口,它必须知道机器人当前状态。
4.2.3 Language
任务指令放在:
annotation.human.task_description
例如概念上:
"pick up the object"
所以整个 VLA 输入可以概括成:
Vision+Robot State+Language→VLA \boxed{ \text{Vision}+\text{Robot State}+\text{Language} \rightarrow \text{VLA} } Vision+Robot State+Language→VLA
4.3 VLA 输出:VLA 到底输出什么?
现在才进入第三层,这是整个第四阶段最重要的地方。
VLA inference:
action, _info = policy.get_action(observation)
然后代码寻找:
motion_token
或者:
action.motion_token
因此当前 SONIC embodiment 的关键输出不是直接的 29 个关节角,而是VLA action space=78\boxed{\text{VLA action space}=78}VLA action space=78。即:
78-D
│
├── 64-D motion_token
│
├── 7-D left hand
│
└── 7-D right hand
4.3.1 这里必须区分三个“维度”
这是前面最容易混淆的地方。
VLA:78-D
这是 VLA action interface。
78
├── 64 motion token
└── 14 hand action
SONIC token:64-D
这是 Universal Token。
64-D
它进入 SONIC controller。
SONIC body output:29-D
这是 SONIC decoder 最终输出的 G1 body action:
29-D
它包含:
legs
+
waist
+
left arm
+
right arm
所以:
29-D 已经包括双臂。
Hand:14-D
剩下的:
7 left
+
7 right
是独立的 hand command interface。
所以整体关系应该永远画成:
VLA action = 78-D
│
├── 64-D motion token
│ │
│ ▼
│ SONIC decoder
│ │
│ ▼
│ 29-D body action
│ ├── legs
│ ├── waist
│ └── arms
│
└── 14-D hand action
├── 7-D left hand
└── 7-D right hand
4.3.2 为什么 VLA 不直接输出 29-D?
这恰恰是 SONIC 架构的关键。
如果:
VLA
↓
29 joint action
那么 VLA 就需要直接学习:
视觉/语言
↓
G1-specific joint control
这会让上层 VLA 和具体机器人 embodiment 强绑定。
SONIC 引入 Universal Token 后变成:
VLA
↓
motion representation
↓
64-D Universal Token
↓
SONIC
↓
29-D G1 body action
因此职责被分开:
VLA:
“我要做什么运动”
SONIC:
“G1 应该怎样执行这个运动”
这也是第四阶段最重要的研究意义。
4.3.3 一个重要的部署细节:VLA 可以直接提供 SONIC token
前面第二阶段我们讨论过 SONIC 自己训练时:
xmotion→Eh→FSQz→Da x_{motion} \xrightarrow{E} h \xrightarrow{FSQ} z \xrightarrow{D} a xmotionEhFSQzDa
即:
motion input
↓
SONIC encoder
↓
FSQ
↓
64-D token
↓
decoder
但现在 VLA deployment 可以直接:
VLA
↓
64-D motion_token
↓
SONIC decoder
也就是说:
oVLA→FVLA→zSONIC→DSONIC \boxed{ o_{VLA} \rightarrow F_{VLA} \rightarrow z_{SONIC} \rightarrow D_{SONIC} } oVLA→FVLA→zSONIC→DSONIC
这里 VLA 实际上承担了:
把视觉/语言/机器人状态映射到 SONIC latent space 的工作。
因此部署时不一定需要再经过 SONIC encoder。
这也是为什么我们不能简单理解成:
VLA → SONIC Encoder → Decoder
当前部署路径更接近:
VLA → SONIC-compatible token → SONIC Decoder
4.3.4 SONIC 怎么知道这个 token 属于哪个时间?
这里就出现:
frame_index
pack_latent_action_message() 会构造类似:
pose_data
├── token_state
├── frame_index
└── hand action
然后通过 ZMQ pose message 发送。
所以除了 ztz_tzt,系统还需要知道 ktk_tkt,即这个 action chunk 中当前使用的是哪个 frame。
4.4 VLA 输出不是一个 action,而是一整个 action chunk
这一点是实时系统理解的关键。当前配置:
action_horizon = 40
因此 VLA 一次 inference 得到的不是787878,而是:
40 × 78
也就是:
At=[at,at+1,...,at+39] A_t= [a_t,a_{t+1},...,a_{t+39}] At=[at,at+1,...,at+39]
如果控制频率:
fc=50Hz f_c=50Hz fc=50Hz
那么:
4050=0.8s \frac{40}{50}=0.8s 5040=0.8s
所以:
一次 VLA inference 会给出大约 0.8 秒的未来动作。
4.4.1 但是 VLA 不是每 0.8 秒才算一次
当前:
action_publish_rate = 50 Hz
但是:
rate = 1 / 0.4 = 2.5 Hz
所以大约每:
0.4s 0.4s 0.4s
重新请求一次 VLA。
于是:
VLA inference #1
├────────────── 0.8 s ──────────────┤
0 0.8
VLA inference #2
├────────────── 0.8 s ──────────────┤
0.4 1.2
两个 action chunk 是重叠的。
这其实是一种:
receding-horizon / overlapping action chunk
执行方式。
4.4.2 为什么要这样设计?
因为 VLA 很慢,而机器人控制要求高频。不能:
VLA inference
↓
等 VLA
↓
执行
↓
再 VLA
否则控制频率可能只有几 Hz。
现在采用:
VLA
│
├── 异步推理
│
└── 产生未来 40 frames
│
▼
50 Hz 执行
所以:
VLA 是低频高延迟的“动作规划器”,SONIC 是高频控制器。
4.5 实时调度:run_vla_inference.py 到底在做什么?
现在前三层已经清楚:
系统:
PolicyServer
run_vla_inference
C++ SONIC
数据:
Vision + State + Language
↓
VLA
↓
40 × 78
↓
64 token + 14 hand
↓
SONIC
接下来才讨论 bridge 如何让它实时运行。
4.5.1 Async inference
run_vla_inference.py 使用 inference worker。
逻辑上:
Main Control Loop
│
├── 50 Hz
│
├── 读取 sensor
│
├── 使用当前 action chunk
│
└── 检查是否有新的 VLA result
▲
│
Async Worker
│
├── 读取 observation
│
├── 请求 PolicyServer
│
└── 获得 action chunk
所以 VLA inference 不阻塞 50 Hz control loop。
4.5.2 为什么 queue 只有很小的缓存?
当前设计的 inference/result queue 是小容量的。
核心思想是:
旧的 VLA 结果没有必要无限积累。
因为机器人执行的是实时动作。
如果:
VLA #1
VLA #2
VLA #3
都排队等待执行,那么等到 #3 执行时,它可能已经过时。所以系统更关心 最新 action\text{最新 action}最新 action 而不是 所有 action\text{所有 action}所有 action,这也是实时机器人系统和普通 batch inference 最大的区别之一。
4.5.3 Latency compensation
假设 VLA inference 花了 120ms120ms120ms,控制频率是 50Hz50Hz50Hz,那么每一帧就是 20ms20ms20ms,因此:
12020=6 \frac{120}{20}=6 20120=6
意味着 action chunk 前面的约 6 帧已经“过时”。代码:
calculate_latency_compensated_index()
大体做:
i=round(delay×control_freq) i= \operatorname{round} ( delay\times control\_freq ) i=round(delay×control_freq)
然后:
i=clip(i,0,H−1) i=\operatorname{clip}(i,0,H-1) i=clip(i,0,H−1)
inference delay = 120 ms
control = 50 Hz
action chunk
0 1 2 3 4 5 6 7 ...
↑
start here
而不是机械地从 frame 0 开始执行。
4.5.4 为什么这是 SONIC 系统里很重要的一个工程点?
因为 VLA 和 SONIC 的时间尺度完全不同。可以粗略理解成:
VLA:
~2.5 Hz 新规划
SONIC:
50 Hz policy
Low-level:
500 Hz command writer
于是系统形成:
VLA
~2.5 Hz
│
action chunk
│
▼
SONIC
50 Hz
│
▼
Low-level command
500 Hz
│
▼
G1
4.6 一个完整动作从头走一遍
现在把所有层级合并。
假设用户说:
“把右边的物体拿起来。”
Step 1:传感器形成 observation
Camera
│
├── ego image
└── wrist image
Robot
│
├── joint state
└── gravity
Language
│
└── "pick up the object"
形成oto_tot。
Step 2:发送给 PolicyServer
run_vla_inference.py
│
│ ZMQ REQ
▼
Isaac-GR00T PolicyServer
Step 3:VLA 推理
VLA 根据 (vision,state,language)(\text{vision},\text{state},\text{language})(vision,state,language) 生成未来动作:
At∈R40×78 A_t\in\mathbb{R}^{40\times78} At∈R40×78
Step 4:拆 action interface
每一帧 at∈R78a_t\in\mathbb{R}^{78}at∈R78 拆成:
64-D motion token
+
7-D left hand
+
7-D right hand
Step 5:选择当前 frame
由于 VLA 推理存在 latency:
40 frames
│
└── 根据 inference delay
选择 compensated index
Step 6:发送给 C++ SONIC
64-D token
+
frame_index
+
hand command
│
▼
ZMQ
Step 7:SONIC controller
C++ runtime 收到 zt∈R64z_t\in\mathbb{R}^{64}zt∈R64,同时读取 sts_tst 然后:
atbody=πθ(zt,st)atbody∈R29 a_t^{body}= \pi_\theta(z_t,s_t)\\ a_t^{body}\in\mathbb{R}^{29} atbody=πθ(zt,st)atbody∈R29
Step 8:转换成 G1 joint target
29-D policy output
│
▼
isaaclab_to_mujoco
│
▼
action scale
│
▼
default_angles +
scaled action
│
▼
q_target
即:
qtarget=qdefault+αa q_{target}= q_{default} + \alpha a qtarget=qdefault+αa
Step 9:500 Hz command writer
然后:
q_target
dq_target
Kp
Kd
tau_ff
写入 Unitree command:
500 Hz
↓
LowCmd
↓
DDS
↓
G1
4.7 第四阶段数据流
【系统层】
┌───────────────────────────────┐
│ Isaac-GR00T │
│ PolicyServer │
│ VLA neural network │
└───────────────▲───────────────┘
│
ZMQ REQ/REP
│
┌───────────────┴───────────────┐
│ run_vla_inference.py │
│ inference orchestration │
└───────────────┬───────────────┘
│
│
【数据层】
│
┌────────────┼────────────┐
│ │ │
Camera State Language
│ │ │
└────────────┼────────────┘
│
▼
VLA Observation
│
▼
VLA
│
▼
40 × 78 action
│
┌────────┴────────┐
│ │
64 token 14 hand
│ │
│ ▼
│ Dex3
│
▼
【SONIC 接口层】
│
64-D motion token
│
▼
C++ SONIC Deploy
│
▼
TensorRT Policy
│
▼
29-D body
┌──────┼──────┐
legs waist arms
│
▼
q_target / Kp / Kd
│
▼
DDS / 500 Hz
│
▼
G1
4.8 三个关键点
第一:三个运行实体
PolicyServer
↓
run_vla_inference.py
↓
C++ SONIC
它们是系统层。
第二:VLA 输出不是 29-D joint action
而是:
78=64+7+7 \boxed{78=64+7+7} 78=64+7+7
其中:
64 → SONIC
14 → hand interface
而:
64→29 \boxed{64\rightarrow29} 64→29
是 SONIC 完成的。
所以完整关系:
VLA 78-D
│
├── 64 → SONIC → 29 body
│ ├── legs
│ ├── waist
│ └── arms
│
└── 14 → hand
第三:VLA 是低频 action-chunk generator,SONIC 是高频 controller
VLA
~2.5 Hz
│
│ 40-frame chunk
▼
SONIC
50 Hz
│
▼
Low-level
500 Hz
│
▼
G1
再加上:
async inference
+
latency compensation
+
overlapping action chunks
才形成一个真正可运行的实时系统。
最后,把第四阶段和前面三阶段接起来
现在四个阶段可以无缝连起来:
Phase 1
整个系统架构
│
▼
Phase 2
Universal Token
Encoder → FSQ → 64-D Token → Decoder
│
▼
Phase 3
Token + Robot State
│
▼
PPO Policy
│
▼
29-D Body Action
│
▼
Phase 4
VLA
│
Vision + Language + Robot State
│
▼
78-D Action Chunk
│
├── 64-D motion token
│ │
│ ▼
│ SONIC
│ │
│ ▼
│ 29-D body
│
└── 14-D hand
│
▼
Hand
因此第四阶段的本质就是完成一个接口闭环:
VLA→64-D Universal Token⏟统一运动接口→SONIC→29-D Whole-Body Control \boxed{ \text{VLA} \rightarrow \underbrace{\text{64-D Universal Token}}_{\text{统一运动接口}} \rightarrow \text{SONIC} \rightarrow \text{29-D Whole-Body Control} } VLA→统一运动接口64-D Universal Token→SONIC→29-D Whole-Body Control
而第五阶段才继续回答:
这个 64-D token 到了 C++ SONIC 之后,具体怎样经过 observation builder、TensorRT、29-D action、Kp/Kd、DDS,最终变成 G1 电机命令?
这样第四、第五阶段的边界也就非常清楚了。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)