【SONIC源码阅读系列6】部署链路
Phase 5:SONIC Deploy——从 Universal Token 到 G1 真机
前面四个阶段已经得到:
VLA→Universal Token→SONIC Policy \boxed{ \text{VLA} \rightarrow \text{Universal Token} \rightarrow \text{SONIC Policy} } VLA→Universal Token→SONIC Policy
第五阶段只解决一个问题:
这个 64-D token 到底是怎么变成 G1 的真实关节运动的?
因此这一阶段我们沿着一条主线走:
Token→C++ Deployment→TensorRT→Joint Target→G1 \boxed{ \text{Token} \rightarrow \text{C++ Deployment} \rightarrow \text{TensorRT} \rightarrow \text{Joint Target} \rightarrow \text{G1} } Token→C++ Deployment→TensorRT→Joint Target→G1
一、Deployment 层整体架构
这一层暂时不要看具体 C++ 函数。先把整个 deployment 看成三个层级:
┌──────────────────────────────────────────────┐
│ Layer 1:上游输入 │
│ │
│ VLA / Motion Reference / Teleoperation │
└──────────────────────┬───────────────────────┘
│
│ 64-D Universal Token
▼
┌──────────────────────────────────────────────┐
│ Layer 2:SONIC Controller │
│ │
│ token + robot state │
│ ↓ │
│ TensorRT Policy │
│ ↓ │
│ 29-D body action │
└──────────────────────┬───────────────────────┘
│
│ joint target
▼
┌──────────────────────────────────────────────┐
│ Layer 3:Robot Execution │
│ │
│ position target + Kp/Kd │
│ ↓ │
│ Unitree DDS / LowCmd │
│ ↓ │
│ G1 │
└──────────────────────────────────────────────┘
所以第五阶段实际上只涉及三个层:
Layer 1:Input
解决:
token 从哪里来?
Layer 2:SONIC
解决:
token + 当前机器人状态如何产生身体动作?
Layer 3:Robot
解决:
身体动作如何真正发送给电机?
这样我们就不会把 VLA、SONIC、DDS、PD 全部混在一起。
二、Layer 1:Token 如何进入 Deployment?
这一层承接第四阶段。第四阶段我们已经知道 VLA 输出:
zt∈R64 z_t\in\mathbb R^{64} zt∈R64
Python 端最终通过 ZMQ 把它包装成:
token_state
frame_index
hand actions
发送给 C++。
所以 C++ deployment 收到的核心东西就是:
zt=external token[64] \boxed{ z_t = \text{external token}[64] } zt=external token[64]
2.1 C++ 中保存在哪里?
在 G1Deploy 中有:
std::vector<double> token_state_data_;
它就是当前 token 的缓存。收到 external token 后,C++ 会把它复制进去,并设置:
is_using_encoder_ = false;
这两个变量实际上已经把整个接口设计表达得很清楚:
External Token
│
▼
token_state_data_[64]
│
└── is_using_encoder_ = false
也就是说:
当前 controller 不需要自己再计算 token,而是直接使用外部给定的 token。
2.2 那 model_encoder.onnx 是干什么的?
这是第五阶段最容易混淆的地方。SONIC deployment 同时支持两种输入模式。
模式 A:给它 motion reference
例如:
SMPL
Motion Reference
Teleoperation
│
▼
model_encoder.onnx
│
▼
64-D token
也就是:
xmotion→ESONIC→z x_{\text{motion}} \rightarrow E_{\text{SONIC}} \rightarrow z xmotion→ESONIC→z
模式 B:直接给它 token
VLA 就属于这个情况:
VLA
│
▼
64-D token
│
X
│ encoder bypass
▼
SONIC decoder
即:
oVLA→FVLA→z o_{\text{VLA}} \rightarrow F_{\text{VLA}} \rightarrow z oVLA→FVLA→z
然后直接进入 SONIC。
2.3 所以 Encoder 并不是 Deployment 必经层
这一点现在可以明确下来:
SONIC Deployment={xmotion→E→z→Dzexternal→D \boxed{ \text{SONIC Deployment}= \begin{cases} x_{\text{motion}}\rightarrow E\rightarrow z\rightarrow D\\[3pt] z_{\text{external}}\rightarrow D \end{cases} } SONIC Deployment={xmotion→E→z→Dzexternal→D
报告里的统一 latent architecture 和 当前 VLA deployment 的接口形式是两个层次的问题。
SONIC 定义了:
Universal Token→Universal Controller \text{Universal Token}\rightarrow\text{Universal Controller} Universal Token→Universal Controller
而 VLA 已经学会直接输出这个 Universal Token,所以运行时可以绕过 SONIC Encoder。
三、Layer 2:SONIC Controller
现在我们把注意力从“token 怎么进来”转移到真正的 controller。
这是第五阶段最核心的一层。
它的输入是:
zt+st \boxed{ z_t + s_t } zt+st
其中:
- ztz_tzt:64-D Universal Token
- sts_tst:当前 G1 proprioception / robot state
输出:
at∈R29 \boxed{ a_t\in\mathbb R^{29} } at∈R29
所以整个 neural controller 可以抽象成:
at=DSONIC(zt,st) \boxed{ a_t=D_{\text{SONIC}}(z_t,s_t) } at=DSONIC(zt,st)
这就是前面训练阶段 UniversalTokenModule 的 deployment 版本。
四、SONIC Controller 内部又可以分成两个步骤
不要把它和上一层混在一起。
Layer 2 内部实际上只有:
Token
│
│
Robot State
│
▼
┌─────────────────┐
│ Observation │
│ Builder │
└────────┬────────┘
│
▼
┌─────────────────┐
│ TensorRT Policy │
└────────┬────────┘
│
▼
29-D action
也就是说,C++ deployment 首先负责组 observation,然后 TensorRT 才负责神经网络推理。
4.1 Observation Builder:把机器人状态和 token 拼起来
C++ 每个 control cycle 都需要构造固定长度的 observation:
ot=[zt, st, ⋯ ] o_t= [ z_t,\, s_t,\, \cdots ] ot=[zt,st,⋯]
具体有哪些字段,由 observation_config.yaml决定。所以 YAML 的真正作用不是“模型参数”,它更像是一个:
PyTorch 模型 observation interface 和 C++ runtime observation interface 之间的协议。
为什么这一点重要?
因为神经网络只认识 RN\mathbb R^NRN,它并不知道:
第 0~63 维 = token
第 64~... = joint position
...
这些语义由 observation configuration 和 C++ observation functions 决定。
最终 C++ 得到一个固定 tensor:
ot∈RN \boxed{ o_t\in\mathbb R^N } ot∈RN
然后交给 TensorRT。
4.2 TensorRT Policy:真正执行 SONIC Decoder
接下来才进入 model_decoder.onnx,C++ 中对应 PolicyEngine。
可以把它理解成:
PolicyEngine:ot→at \boxed{ \text{PolicyEngine} : o_t \rightarrow a_t } PolicyEngine:ot→at
对于当前 VLA deployment:
[zt,st]→model_decoder.onnx→at29 \boxed{ [z_t,s_t] \rightarrow \text{model\_decoder.onnx} \rightarrow a_t^{29} } [zt,st]→model_decoder.onnx→at29
所以到这里:VLA 已经完全退出了,它只负责产生 ztz_tzt。从这里开始,全部是 SONIC controller。
五、和第二、三阶段重新接起来
前面训练阶段我们得到:
Encoder→FSQ→z→Decoder→a \text{Encoder} \rightarrow \text{FSQ} \rightarrow z \rightarrow \text{Decoder} \rightarrow a Encoder→FSQ→z→Decoder→a
现在部署阶段:
如果输入是 motion reference:
x→EONNX→z→DONNX→a x \rightarrow E_{\text{ONNX}} \rightarrow z \rightarrow D_{\text{ONNX}} \rightarrow a x→EONNX→z→DONNX→a
如果输入是 VLA:
VLA→z→DONNX→a \text{VLA} \rightarrow z \rightarrow D_{\text{ONNX}} \rightarrow a VLA→z→DONNX→a
所以部署端真正固定的是:
z+s→D→a \boxed{ z+s\rightarrow D\rightarrow a } z+s→D→a
而 zzz 的产生方式可以变化。
这就是 Universal Token 在整个系统中的核心位置。
六、Layer 2 的输出:29-D action 到底意味着什么?
现在进入第三层之前,我们必须先搞清楚 at29a_t^{29}at29 究竟是什么。
它不是 torque。
C++ 中做了:
aiscaled=giai a_i^{scaled}=g_i a_i aiscaled=giai
然后:
qitarget=qidefault+aiscaled \boxed{ q_i^{target}= q_i^{default} + a_i^{scaled} } qitarget=qidefault+aiscaled
因此 neural network 输出的是:
joint position offset \boxed{ \text{joint position offset} } joint position offset
经过 scaling 后变成:
joint position target \boxed{ \text{joint position target} } joint position target
七、Layer 3:从 Joint Target 到 G1
现在才进入真正的 robot execution 层。
数据流非常简单:
29-D neural action
│
▼
action scale
│
▼
default joint angle
│
▼
29-D q_target
│
▼
MotorCommand
│
▼
Unitree LowCmd
│
▼
DDS
│
▼
G1
所以这里发生了一个非常重要的转换:
Neural Action→Joint Target \boxed{ \text{Neural Action} \rightarrow \text{Joint Target} } Neural Action→Joint Target
然后才:
Joint Target→Low-Level Control \boxed{ \text{Joint Target} \rightarrow \text{Low-Level Control} } Joint Target→Low-Level Control
八、G1 的低层控制
C++ 给每个 motor 设置:
qtargetdqtarget=0τff=0 q_{target}\\ dq_{target}=0\\ \tau_{ff}=0 qtargetdqtarget=0τff=0
以及 Kp, KdK_p,\ K_dKp, Kd。
因此可以近似理解成:
τ=Kp(qtarget−q)+Kd(0−q˙) \tau= K_p(q_{target}-q) + K_d(0-\dot q) τ=Kp(qtarget−q)+Kd(0−q˙)
也就是:
SONIC→qtarget→PD→Motor \boxed{ \text{SONIC} \rightarrow q_{target} \rightarrow \text{PD} \rightarrow \text{Motor} } SONIC→qtarget→PD→Motor
所以 SONIC 并没有直接学习 motor torque。它学习的是:
如何根据 latent motion intention 和当前身体状态,生成合适的 joint position target。
九、 50 Hz 和 500 Hz 两个频率
这是 Deployment architecture 中另一个重要设计。
不要把它理解成两个 policy。
实际上只有一个 neural policy:
50Hz \boxed{50Hz} 50Hz
即:
Tpolicy=20ms T_{\text{policy}}=20ms Tpolicy=20ms
每 20 ms 做一次:
[zt,st]→at [z_t,s_t]\rightarrow a_t [zt,st]→at
但是 motor command writer:
500Hz \boxed{500Hz} 500Hz
即每:
2ms 2ms 2ms
向 robot command interface 发布一次当前 command。
因此:
50 Hz
┌────────────────────┐
│ SONIC TensorRT │
│ │
│ token + state │
│ ↓ │
│ joint target │
└─────────┬──────────┘
│
▼
command buffer
│
────────┼────────
500 Hz
│
▼
Unitree DDS
│
▼
G1
这里体现的是两个时间尺度的解耦:
Neural Control: 50 Hz \boxed{ \text{Neural Control: 50 Hz} } Neural Control: 50 Hz
Command Transmission: 500 Hz \boxed{ \text{Command Transmission: 500 Hz} } Command Transmission: 500 Hz
十、那么 VLA 的频率又在哪里?
这时候把第四阶段也接回来:
VLA≈2.5Hz \boxed{ \text{VLA}\approx2.5Hz } VLA≈2.5Hz
SONIC=50Hz \boxed{ \text{SONIC}=50Hz } SONIC=50Hz
Motor Command=500Hz \boxed{ \text{Motor Command}=500Hz } Motor Command=500Hz
所以整个 system 实际上是三级时间尺度:
VLA
~2.5 Hz
│
│ latent action chunk
▼
SONIC
50 Hz
│
│ joint target
▼
Motor Interface
500 Hz
│
▼
G1
为什么 VLA 这么慢?
因为 VLA 并不是每 20 ms 重新规划一次身体动作。
它给的是:
zt:t+H z_{t:t+H} zt:t+H
也就是一段未来 token sequence。
例如前面看到的:
H=40 H=40 H=40
对应:
40×20ms=800ms 40\times20ms=800ms 40×20ms=800ms
所以 VLA 可以每隔约 400 ms 重新产生一个 chunk,而 SONIC 在这段时间里持续以 50 Hz 执行。
于是:
VLA负责时序上的“意图/规划” \boxed{ \text{VLA负责时序上的“意图/规划”} } VLA负责时序上的“意图/规划”
SONIC负责高频的全身运动控制 \boxed{ \text{SONIC负责高频的全身运动控制} } SONIC负责高频的全身运动控制
十二、External Token 为什么还需要 timeout?
这是 deployment safety 层,不属于 neural network 本身。
C++ 对 external token 设置了大约:
200ms 200ms 200ms
的 freshness threshold。
因为:
50Hz=20ms 50Hz=20ms 50Hz=20ms
所以 200 ms 大约对应:
10 10 10
个 SONIC control cycles。
如果 VLA/ZMQ 暂时没有新 token:
new token
↓
token buffer
↓
SONIC keeps running
而不是每一次通信抖动就直接让 controller 崩掉。
这也是为什么第四阶段的:
action chunk + asynchronous inference
能够和第五阶段的:
50 Hz real-time controller
自然连接起来。
十三、第五阶段压缩成源码级数据流
最终链路:
VLA
│
│ 64-D motion_token
▼
ZMQ
│
▼
token_state_data_[64]
│
│
▼
┌───────────────────┐
│ Observation │
│ Builder │
│ │
│ token + robot │
│ proprioception │
└─────────┬─────────┘
│
▼
model_decoder.onnx
│
TensorRT
│
▼
action[29]
│
action scale
│
▼
q_default + offset
│
▼
q_target[29]
│
▼
MotorCommand
│
500Hz
│
▼
Unitree DDS
│
▼
G1
如果是普通 motion reference,则只需要把最上面换成:
Motion Reference
│
▼
model_encoder.onnx
│
▼
64-D token
│
▼
后面完全相同
因此整个 deployment 最核心的结构其实非常干净:
Input⏟产生 token→SONIC⏟token→body action→Robot⏟action→motor \boxed{ \underbrace{\text{Input}}_{\text{产生 token}} \rightarrow \underbrace{\text{SONIC}}_{\text{token→body action}} \rightarrow \underbrace{\text{Robot}}_{\text{action→motor}} } 产生 tokenInput→token→body actionSONIC→action→motorRobot
十四、回头看 gear_sonic_deploy,应该用什么视角?
按照下面的三层结构看。
第一层:Input Layer
主要关注:
ZMQ
External Token
Motion Reference
Encoder
核心问题只有一个:
token 从哪里来?
第二层:Controller Layer
主要关注:
observation_config.yaml
Observation Builder
PolicyEngine
model_decoder.onnx
TensorRT
核心问题只有一个:
token + 当前 robot state 如何变成 29-D action?
第三层:Robot Layer
主要关注:
action scale
default angle
MotorCommand
Kp / Kd
LowCmd
DDS
500 Hz
核心问题只有一个:
29-D action 如何变成 G1 的真实电机运动?
十五、核心认识
到这里其实不需要记住几十个类名。只需要建立下面这个 mental model:
VLA⟶2.5Hz64-D Universal Token⟶50Hz29-D Joint Action⟶500HzG1 \boxed{ \textbf{VLA} \quad \overset{\text{2.5Hz}}{\longrightarrow} \quad \textbf{64-D Universal Token} \quad \overset{\text{50Hz}}{\longrightarrow} \quad \textbf{29-D Joint Action} \quad \overset{\text{500Hz}}{\longrightarrow} \quad \textbf{G1} } VLA⟶2.5Hz64-D Universal Token⟶50Hz29-D Joint Action⟶500HzG1
而 SONIC 自己位于中间:
DSONIC:(zt,st)→at \boxed{ D_{\text{SONIC}}: (z_t,s_t) \rightarrow a_t } DSONIC:(zt,st)→at
其中:
- VLA:产生 latent motion intention;
- Universal Token:跨 VLA / motion / teleop 的接口;
- SONIC Decoder:把 latent intention + 当前身体状态变成 whole-body joint command;
- PD / Unitree:把 joint target 执行成真实机器人运动。
最后,把第五阶段和前四阶段合并
现在我们已经真正完成了一条从训练到真机的闭环:
Motion Data→E→FSQ→z→D→a→PPO \boxed{ \begin{aligned} \text{Motion Data} &\rightarrow E \rightarrow FSQ \rightarrow z \\ &\rightarrow D \rightarrow a \rightarrow \text{PPO} \end{aligned} } Motion Data→E→FSQ→z→D→a→PPO
训练完成后:
VLA→z→DSONIC→a→qtarget→G1 \boxed{ \text{VLA} \rightarrow z \rightarrow D_{\text{SONIC}} \rightarrow a \rightarrow q_{target} \rightarrow G1 } VLA→z→DSONIC→a→qtarget→G1
所以从研究角度看,整个 SONIC 系统真正的“桥”就是:
64-D Universal Token \boxed{\textbf{64-D Universal Token}} 64-D Universal Token
它把上层的 VLA / motion generation和下层的 whole-body control连接起来。这也是为什么第五阶段最值得理解是:
Universal Token如何成为跨模块、跨频率、跨软件栈的 control interface \boxed{ \text{Universal Token} \quad\text{如何成为跨模块、跨频率、跨软件栈的 control interface} } Universal Token如何成为跨模块、跨频率、跨软件栈的 control interface
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)