上一期文章的结构有点混乱,这一期重新组织成严格的层级:

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 StatePolicy29-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+LanguageVLA


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} } oVLAFVLAzSONICDSONIC

这里 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,H1)

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} AtR40×78


Step 4:拆 action interface

每一帧 at∈R78a_t\in\mathbb{R}^{78}atR78 拆成:

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}ztR64,同时读取 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)atbodyR29


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} 6429

是 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 TokenSONIC29-D Whole-Body Control

而第五阶段才继续回答:

这个 64-D token 到了 C++ SONIC 之后,具体怎样经过 observation builder、TensorRT、29-D action、Kp/Kd、DDS,最终变成 G1 电机命令?

这样第四、第五阶段的边界也就非常清楚了。

Logo

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

更多推荐