PI0、PI0.5、PI0-FAST 原理讲解

PI0、PI0.5、PI0-FAST 原理讲解

这份文档按模型逐个讲清楚 openpi 里的三类策略模型:

  1. PI0
  2. PI0.5
  3. PI0-FAST

每个模型都按同一条线讲:

text 复制代码
数据怎么处理
-> state 怎么表示
-> 网络结构长什么样
-> 图像、语言、state、动作条件怎么融合
-> 训练损失是什么
-> 推理时怎么生成动作

先给一个总直觉:这些模型都是 VLA 模型,也就是 Vision-Language-Action。输入是图像、语言指令和机器人当前状态,输出是未来一段动作。

text 复制代码
多相机图像 + 任务文本 + 当前机器人状态 -> 未来 H 步机器人动作

其中 H 叫 action horizon。模型不是只预测下一步,而是一次预测一个 action chunk。


0. 先讲一个共同前提:不同机器人 state 不一样,怎么统一

不同平台的 state 语义完全不同:

text 复制代码
ALOHA:
  state = 左臂 6 个关节 + 左夹爪 + 右臂 6 个关节 + 右夹爪
  维度通常是 14

DROID:
  state = Franka 7 个关节位置 + 夹爪位置
  维度通常是 8

LIBERO:
  state = 末端位置 + 末端旋转 + 夹爪状态
  维度通常是 7 或 8,取决于具体构造

openpi 没有发明一个"所有机器人共用的物理 state"。它统一的是模型接口,不是物理语义。

统一后的模型输入长这样:

text 复制代码
image:
  base_0_rgb
  left_wrist_0_rgb
  right_wrist_0_rgb

image_mask:
  每路图像是否真实存在

state:
  当前平台适配后的低维状态向量

prompt:
  任务文本

actions:
  未来 H 步动作,训练时才有

真正解决平台差异的是平台适配层。它负责把各平台原始观测翻译成上面这套统一接口。

0.1 平台适配层具体做什么

以三个平台为例:

text 复制代码
ALOHA:
  输入图像可能是 cam_high、cam_left_wrist、cam_right_wrist
  图像格式可能是 C,H,W
  state 是双臂关节和夹爪
  还要把 ALOHA 的夹爪表示转成 PI 预训练时使用的角度空间

DROID:
  把 joint_position 和 gripper_position 拼成 state
  把 exterior_image_1_left 放到 base_0_rgb
  把 wrist_image_left 放到 left_wrist_0_rgb
  没有 right_wrist_0_rgb 时补零图,并设置 mask

LIBERO:
  把 observation/state 直接作为低维 state
  把 observation/image 放到 base_0_rgb
  把 observation/wrist_image 放到 left_wrist_0_rgb
  没有右腕相机时补零图

也就是说,模型只知道:

text 复制代码
我有 base 图、wrist 图、state、prompt

但每一维 state 的物理意义,由平台适配器和训练配置共同约定。

0.2 state/action 还要归一化

平台适配完成后,state 和 actions 会用训练集统计量归一化。

普通归一化:

text 复制代码
x_norm = (x - mean) / (std + eps)

PI0.5 和 PI0-FAST 常用分位数归一化,把主要范围压到近似 [-1, 1]

text 复制代码
x_norm = (x - q01) / (q99 - q01) * 2 - 1

这一步非常关键。因为后面 PI0.5 / PI0-FAST 会把 state 离散到 256 个桶,这默认 state 已经大致落在 [-1, 1]

0.3 action 维度为什么经常 padding

模型内部常用一个统一的 action_dim,例如 32。但真实机器人可能只有 7 维、8 维或 14 维动作。

所以训练时会:

text 复制代码
真实动作 -> padding 到模型 action_dim

推理时会:

text 复制代码
模型输出 action_dim -> 裁剪回机器人真实动作维度

这也是平台适配的一部分。


1. PI0

PI0 是 flow matching 风格的连续动作生成模型。它不是像语言模型那样一个 token 一个 token 生成动作,而是在连续动作空间里,从随机噪声逐步生成一段动作。

一句话:

text 复制代码
PI0 先采样一段随机动作噪声,然后根据图像、语言和 state,一步步把噪声修正成机器人动作。

1.1 PI0 的数据处理

PI0 的训练样本大致是:

text 复制代码
输入:
  多相机图像
  当前 state
  prompt 文本

标签:
  未来 H 步连续动作

PI0 的数据处理链路:

text 复制代码
原始平台数据
  -> 平台适配
  -> 得到 image / image_mask / state / prompt / actions
  -> state 和 actions 归一化
  -> 图像 resize 到 224 x 224
  -> prompt 用 PaliGemma tokenizer 编码
  -> state/actions padding 到模型 action_dim

PI0 的 prompt token 化比较简单。它不会把 state 放进文本里。

如果 prompt 是:

text 复制代码
pick up the fork

PI0 大致会变成:

text 复制代码
[BOS] pick up the fork [newline] [PAD] [PAD] ...

state 仍然保留为连续向量,后面会单独送进 action expert。

1.2 PI0 的网络结构

PI0 的结构可以分成三块:

text 复制代码
1. 视觉编码器
   多路图像 -> image tokens

2. PaliGemma / 语言视觉分支
   image tokens + text tokens

3. Action Expert 分支
   state token + noisy action tokens + time embedding

更直观地画出来:

text 复制代码
图像 -> SigLIP/ViT -> image tokens ----\
                                        -> PaliGemma 分支 ----\
prompt -> tokenizer -> text tokens ----/                       \
                                                                 -> 双分支 Transformer -> action velocity
state -> Linear -> state token ---------------------------------/
noisy actions -> Linear -> action tokens -----------------------/
time -> sin/cos embedding -> MLP -------------------------------/

PI0 里面有两个专家:

text 复制代码
PaliGemma 专家:
  处理图像和语言,继承视觉语言模型能力

Action Expert:
  处理 state、带噪动作和时间条件,学习机器人动作生成

这两个专家不是最后才融合,而是在 Transformer 每一层的 attention 里融合。

1.3 PI0 的条件怎么融合

PI0 把 token 分成 prefix 和 suffix。

prefix 是已知条件:

text 复制代码
prefix = image tokens + text tokens

suffix 是动作生成相关内容:

text 复制代码
suffix = state token + action tokens

其中:

text 复制代码
state token:
  normalized state -> Linear -> 一个连续 token

action tokens:
  noisy actions x_t -> Linear -> H 个动作 token

time:
  t -> sin/cos embedding
  time embedding 和 action token 拼接
  经过 MLP 后得到 action expert tokens

一层 Transformer 里发生的事可以理解为:

text 复制代码
PaliGemma tokens 做归一化
Action Expert tokens 做归一化
两组 tokens 放到同一个 attention 里
Action tokens 可以读取图像、语言和 state 信息
attention 输出再分回两个专家
每个专家用自己的 MLP 更新

伪代码:

python 复制代码
def pi0_forward(obs, noisy_actions, t):
    # 图像和语言形成 prefix
    image_tokens = vision_encoder(obs.images)
    text_tokens = text_embedder(obs.tokenized_prompt)
    prefix_tokens = concat(image_tokens, text_tokens)

    # state 和 noisy actions 形成 suffix
    state_token = state_linear(obs.state)[:, None, :]
    action_tokens = action_linear(noisy_actions)

    time_emb = sincos_time_embedding(t)
    time_tokens = repeat(time_emb, times=action_horizon)
    action_tokens = mlp(concat_feature(action_tokens, time_tokens))

    suffix_tokens = concat(state_token, action_tokens)

    # prefix/suffix 在 attention mask 约束下融合
    prefix_out, suffix_out = two_expert_transformer(
        prefix_tokens,
        suffix_tokens,
        attention_mask=build_pi0_attention_mask(),
    )

    # 只取最后 H 个动作 token,投影回动作维度
    velocity = action_out_linear(suffix_out[:, -action_horizon:])
    return velocity

1.4 PI0 的训练损失

PI0 用 flow matching。

给定真实动作:

text 复制代码
a = actions

采样高斯噪声:

text 复制代码
z = noise

采样时间:

text 复制代码
t in (0, 1)

构造动作和噪声之间的中间点:

text 复制代码
x_t = t * z + (1 - t) * a

t = 0x_t 是真实动作。

t = 1x_t 是纯噪声。

模型要预测从动作到噪声的速度:

text 复制代码
u_t = z - a

模型输出:

text 复制代码
v_t = model(obs, x_t, t)

损失:

text 复制代码
loss = mean((v_t - u_t)^2)

伪代码:

python 复制代码
def train_step_pi0(batch):
    obs = batch.observation
    actions = batch.actions

    noise = random_normal_like(actions)
    t = sample_time(batch_size=actions.shape[0])

    x_t = t[:, None, None] * noise + (1 - t[:, None, None]) * actions
    target_velocity = noise - actions

    pred_velocity = pi0_forward(obs, x_t, t)

    loss = mean_square(pred_velocity - target_velocity)
    return loss

1.5 PI0 的推理过程

推理时没有真实动作。PI0 从随机噪声开始,反向走到动作。

流程:

text 复制代码
1. 处理观测,得到 image / state / prompt
2. 编码 prefix,并缓存图像和语言的 KV cache
3. 初始化 x = 随机动作噪声
4. 从 t=1 开始,重复 num_steps 次
5. 每步预测 velocity
6. 用 Euler 更新 x = x + dt * velocity,其中 dt < 0
7. 得到 x_0,作为动作序列
8. 反归一化、裁剪动作维度、必要时 delta 转 absolute

伪代码:

python 复制代码
def infer_pi0(obs, num_steps=10):
    prefix_cache = encode_prefix_once(obs)

    x = random_normal([batch, action_horizon, action_dim])
    t = 1.0
    dt = -1.0 / num_steps

    for _ in range(num_steps):
        velocity = pi0_forward_with_prefix_cache(prefix_cache, obs.state, x, t)
        x = x + dt * velocity
        t = t + dt

    actions = postprocess_actions(x)
    return actions

2. PI0.5

PI0.5 和 PI0 很像,也用 flow matching 生成连续动作。但它做了两个关键改动:

text 复制代码
1. state 可以离散化后放进文本 prompt
2. time 条件通过 adaRMS 调制 Action Expert

一句话:

text 复制代码
PI0.5 仍然是从噪声生成连续动作,但它把 state 更早放进语言上下文,并让去噪时间更深地影响动作专家。

2.1 PI0.5 的数据处理

PI0.5 的平台适配、图像处理、action padding 和归一化基本和 PI0 一样。

真正不同的是 prompt token 化。

PI0:

text 复制代码
prompt -> text tokens
state -> 连续 state token,放进 action suffix

PI0.5:

text 复制代码
prompt + 离散 state -> text/state tokens
state 不再作为单独 state token 放进 suffix

2.2 PI0.5 的 state 怎么离散化并放进 token 序列

这是 PI0.5 最容易漏掉的细节。

假设平台适配后有一个 state:

text 复制代码
raw_state = [0.42, -0.15, 1.20, 0.03]

先用训练集统计量归一化:

text 复制代码
mean = [0.20, -0.10, 1.00, 0.00]
std  = [0.10,  0.20, 0.50, 0.10]

normalized_state = (raw_state - mean) / std
                 = [2.20, -0.25, 0.40, 0.30]

然后按 [-1, 1] 主要范围裁剪:

text 复制代码
clipped_state = [1.00, -0.25, 0.40, 0.30]

再离散到 256 个桶。公式可以理解为:

text 复制代码
bin = digitize(x, bins=linspace(-1, 1, 257)[:-1]) - 1

近似写成:

text 复制代码
bin = floor((clip(x, -1, 1) + 1) / 2 * 256)
bin = clamp(bin, 0, 255)

逐维得到:

text 复制代码
1.00  -> 255
-0.25 -> 96
0.40  -> 179
0.30  -> 166

于是:

text 复制代码
discrete_state = [255, 96, 179, 166]

接着把它转成字符串:

text 复制代码
state_str = "255 96 179 166"

然后拼进固定文本模板:

text 复制代码
Task: pick up the fork, State: 255 96 179 166;
Action:

最后用 PaliGemma tokenizer 编成 token 序列:

text 复制代码
tokenized_prompt =
  [BOS, "Task", ":", "pick", "up", "the", "fork", ",",
   "State", ":", "255", "96", "179", "166", ";",
   "\n", "Action", ":", PAD, PAD, ...]

tokenized_prompt_mask =
  [True, True, True, ..., True, False, False, ...]

注意:"255" 不一定是一个 token。tokenizer 可能把它拆成一个或多个子 token。关键不是"每个数字对应一个 token",而是:

text 复制代码
连续 state -> 256 桶整数 -> 数字字符串 -> prompt 文本 -> tokenizer token ids

PI0.5 的序列位置:

text 复制代码
PaliGemma prefix:
  [image tokens]
  [Task ... State: 255 96 179 166; Action:]

Action Expert suffix:
  [noisy action token 1]
  ...
  [noisy action token H]

所以 PI0.5 的 state 属于 prefix,不再是 PI0 那样的连续 state token。

伪代码:

python 复制代码
def build_pi05_prompt(prompt, normalized_state):
    bins = []
    for x in normalized_state:
        x = clip(x, -1.0, 1.0)
        b = floor((x + 1.0) / 2.0 * 256)
        b = clamp(b, 0, 255)
        bins.append(int(b))

    state_str = " ".join(str(b) for b in bins)
    text = f"Task: {prompt}, State: {state_str};\nAction: "

    tokens = tokenizer.encode(text, add_bos=True)
    tokens, mask = pad_or_truncate(tokens, max_token_len)
    return tokens, mask

2.3 PI0.5 的网络结构

PI0.5 复用 PI0 的大框架:

text 复制代码
视觉编码器 -> image tokens
PaliGemma 分支 -> image tokens + text/state tokens
Action Expert 分支 -> noisy action tokens
双分支 Transformer -> velocity

但和 PI0 有两个结构差异。

第一,suffix 里没有单独的 state token:

text 复制代码
PI0 suffix:
  [state token][noisy action tokens]

PI0.5 suffix:
  [noisy action tokens]

第二,time 不再和 action tokens 拼接后过 MLP,而是作为 adaRMS 条件调制 Action Expert。

text 复制代码
t -> sin/cos time embedding -> MLP -> time condition

这个 time condition 会进入 Action Expert 每层的 RMSNorm。

2.4 PI0.5 的条件怎么融合

PI0.5 的融合路径:

text 复制代码
图像 -> image tokens ----------------------\
                                           -> PaliGemma 分支
prompt + 离散 state -> text/state tokens --/

noisy actions -> action tokens -------------> Action Expert 分支
time -> adaRMS condition -------------------> 调制 Action Expert

PaliGemma 分支和 Action Expert 分支
  -> 每层 shared attention 融合
  -> 输出 action hidden
  -> Linear 得到 velocity

adaRMS 可以通俗理解为:时间 t 不只是告诉模型"当前第几步去噪",而是直接改变 Action Expert 每层的归一化和残差更新。

普通 RMSNorm:

text 复制代码
normed = rms_norm(x)

adaRMS:

text 复制代码
scale, shift, gate = f(time)
normed = rms_norm(x) * (1 + scale) + shift
residual = residual + gate * update

直觉:

text 复制代码
t 接近 1:
  动作还是噪声,模型需要大幅修正

t 接近 0:
  动作接近最终结果,模型需要细调

伪代码:

python 复制代码
def pi05_forward(obs, noisy_actions, t):
    image_tokens = vision_encoder(obs.images)
    text_state_tokens = text_embedder(obs.tokenized_prompt)
    prefix_tokens = concat(image_tokens, text_state_tokens)

    action_tokens = action_linear(noisy_actions)

    time_emb = sincos_time_embedding(t)
    time_cond = swish(linear(time_emb))
    time_cond = swish(linear(time_cond))

    prefix_out, suffix_out = two_expert_transformer(
        prefix_tokens,
        action_tokens,
        adarms_cond_for_action_expert=time_cond,
        attention_mask=build_pi05_attention_mask(),
    )

    velocity = action_out_linear(suffix_out[:, -action_horizon:])
    return velocity

2.5 PI0.5 的训练损失

PI0.5 的损失和 PI0 一样,都是 flow matching MSE:

text 复制代码
x_t = t * noise + (1 - t) * actions
target_velocity = noise - actions
pred_velocity = model(obs, x_t, t)
loss = mean((pred_velocity - target_velocity)^2)

区别不在损失,而在条件输入和网络调制方式。

2.6 PI0.5 的推理过程

PI0.5 推理和 PI0 一样从噪声开始积分,但 prefix 不同。

流程:

text 复制代码
1. 平台适配,得到 image / state / prompt
2. state 归一化并离散成数字文本
3. 拼成 "Task: ..., State: ...; Action:"
4. 编码图像和 text/state prefix
5. 初始化动作噪声
6. 多步 flow 采样
7. 输出连续动作
8. 反归一化并裁剪动作维度

伪代码:

python 复制代码
def infer_pi05(obs, num_steps=10):
    obs.tokenized_prompt = build_pi05_prompt(obs.prompt, obs.normalized_state)
    prefix_cache = encode_pi05_prefix_once(obs)

    x = random_normal([batch, action_horizon, action_dim])
    t = 1.0
    dt = -1.0 / num_steps

    for _ in range(num_steps):
        velocity = pi05_forward_with_prefix_cache(prefix_cache, x, t)
        x = x + dt * velocity
        t = t + dt

    return postprocess_actions(x)

3. PI0-FAST

PI0-FAST 和前两个模型差别最大。它不是 flow matching,不在连续动作空间里逐步去噪。

一句话:

text 复制代码
PI0-FAST 把连续动作先变成 action tokens,然后像语言模型写句子一样,一个 token 一个 token 生成动作。

3.1 PI0-FAST 的数据处理

PI0-FAST 的输入仍然来自平台适配:

text 复制代码
image / image_mask / state / prompt / actions

但模型变换不同:

text 复制代码
图像:
  resize 到 224 x 224
  编成 image embeddings

state:
  归一化
  离散成 0..255 的整数
  放进文本 prefix

prompt:
  清洗并小写
  和 state 拼成 "Task: ..., State: ...;\n"

actions:
  训练时用 FAST tokenizer 编成 action tokens

PI0-FAST 的文本 prefix:

text 复制代码
Task: pick up the fork, State: 255 96 179 166;

训练时会接上动作 postfix:

text 复制代码
Action: <action_token_1> <action_token_2> ... |

完整训练序列:

text 复制代码
[image embeddings]
[BOS] Task: pick up the fork, State: 255 96 179 166;
Action: A1 A2 A3 ... | [EOS]

3.2 FAST action tokenizer 怎么工作

PI0-FAST 需要把连续动作变成离散 token。

训练动作:

text 复制代码
actions: [H, action_dim]

先送进 FAST tokenizer:

text 复制代码
actions -> raw_action_tokens

然后 raw action tokens 会映射到 PaliGemma/Gemma 词表末尾附近的一段 token id。

直观理解:

text 复制代码
语言模型原本会预测文字 token
PI0-FAST 扩展出一段"动作 token 区域"
动作 token 也变成词表里的 token id

映射大致是:

text 复制代码
lm_action_token = vocab_size - 1 - skip_tokens - raw_action_token

这样模型就能用标准 next-token prediction 学动作。

3.3 PI0-FAST 的网络结构

PI0-FAST 是单路自回归 Transformer,不像 PI0/PI0.5 那样有 Action Expert 双分支。

结构:

text 复制代码
图像 -> SigLIP/ViT -> image embeddings ----\
                                            -> Gemma autoregressive Transformer -> token logits
文本/state/action token -> token embeddings /

具体输入是:

text 复制代码
embedded_prefix = image embeddings + token embeddings

然后用 Gemma Transformer 得到每个位置的 hidden,再投影到词表 logits。

输出不是连续动作,而是:

text 复制代码
下一个 token 的概率分布

3.4 PI0-FAST 的条件怎么融合

PI0-FAST 通过一个 token 序列融合所有条件。

序列可以理解成:

text 复制代码
[image embeddings]
[Task / State 文本 tokens]
[Action: tokens]
[已经生成的 action tokens]

注意力规则由三个 mask 控制:

text 复制代码
token_mask:
  哪些位置有效,哪些是 padding

ar_mask:
  哪些位置属于自回归区域

loss_mask:
  哪些位置参与损失

规则:

text 复制代码
prefix:
  图像 + Task + State
  作为已知条件
  loss_mask = False

postfix:
  Action: + action tokens + |
  自回归生成
  loss_mask = True

attention 直觉:

text 复制代码
action token i 可以看:
  图像条件
  任务文本
  离散 state 文本
  之前的 action tokens

action token i 不能看:
  未来 action tokens

伪代码:

python 复制代码
def pi0_fast_forward(obs):
    image_embeddings = vision_encoder(obs.images)
    token_embeddings = gemma_embedder(obs.tokenized_prompt)

    sequence = concat(image_embeddings, token_embeddings)
    attn_mask = make_attn_mask(obs.token_mask, obs.ar_mask)

    hidden = gemma_transformer(sequence, attn_mask)
    logits = vocab_projection(hidden)
    return logits

3.5 PI0-FAST 的训练损失

PI0-FAST 用交叉熵。

它预测下一个 token:

text 复制代码
输入 token[0:i]
预测 token[i+1]

但只在 action/postfix 区域算 loss。

公式:

text 复制代码
loss = - sum(loss_mask[i] * log P(token[i] | previous tokens)) / sum(loss_mask)

伪代码:

python 复制代码
def train_step_pi0_fast(batch):
    logits = pi0_fast_forward(batch.observation)

    targets = batch.tokenized_prompt[:, 1:]
    pred = logits[:, :-1]
    mask = batch.token_loss_mask[:, 1:]

    loss = cross_entropy(pred, targets, mask)
    return loss

3.6 PI0-FAST 的推理过程

推理时没有 action tokens,模型要自己生成。

流程:

text 复制代码
1. 平台适配,得到 image / state / prompt
2. state 离散后拼入文本 prefix
3. 编码图像和 prefix,建立 KV cache
4. 从 Action: 后开始生成
5. 每次取 argmax 或按 temperature 采样下一个 token
6. 遇到 EOS 或达到最大步数停止
7. 从生成 token 中解析 Action: 后、| 前的动作 token
8. 映射回 FAST action tokens
9. FAST tokenizer 解码成连续动作
10. 反归一化并裁剪动作维度

伪代码:

python 复制代码
def infer_pi0_fast(obs, max_steps=256):
    prefix = build_fast_prefix(obs.prompt, obs.normalized_state)
    image_embeddings = vision_encoder(obs.images)

    cache, last_logits = gemma_prefill(image_embeddings, prefix)

    generated = []
    for _ in range(max_steps):
        token = argmax(last_logits)
        generated.append(token)

        if token == EOS:
            break

        cache, last_logits = gemma_decode_one_step(token, cache)

    action_tokens = parse_action_tokens(generated)
    actions = fast_tokenizer.decode(action_tokens)
    return postprocess_actions(actions)

4. 三个模型放在一起对比

项目 PI0 PI0.5 PI0-FAST
动作生成方式 flow matching flow matching 自回归 token 生成
动作表示 连续动作 连续动作 FAST action tokens
state 位置 连续 state token,放在 suffix 离散成数字文本,放在 prefix 离散成数字文本,放在 prefix
网络结构 PaliGemma + Action Expert 双分支 PaliGemma + Action Expert 双分支 单路 Gemma 自回归
time 条件 和 action token 拼接后过 MLP adaRMS 调制 Action Expert 无 flow time
训练损失 velocity MSE velocity MSE token cross entropy
推理 从噪声多步积分成动作 从噪声多步积分成动作 逐 token 生成后解码成动作

更通俗地说:

text 复制代码
PI0:
  state 还是连续向量。
  模型看图、读指令、看连续 state,然后把动作噪声逐步修正成动作。

PI0.5:
  state 先变成数字文本,和任务文本一起进入语言上下文。
  动作仍然通过 flow matching 连续生成。

PI0-FAST:
  state 也变成数字文本。
  动作不直接连续生成,而是先变成 token,再像写句子一样生成。

5. 一条真实推理请求怎么走

以一个 LIBERO 风格输入为例:

text 复制代码
raw_obs:
  observation/image
  observation/wrist_image
  observation/state
  prompt = "pick up the fork"

平台适配:

text 复制代码
base_0_rgb        <- observation/image
left_wrist_0_rgb  <- observation/wrist_image
right_wrist_0_rgb <- zeros
image_mask        <- [True, True, False]
state             <- observation/state
prompt            <- "pick up the fork"

如果是 PI0:

text 复制代码
prompt -> text tokens
state -> 连续 state token
随机动作噪声 -> flow matching 多步去噪
输出连续动作

如果是 PI0.5:

text 复制代码
state -> 归一化 -> 256 桶离散化 -> "255 96 ..."
prompt + state_text -> text/state tokens
随机动作噪声 -> flow matching 多步去噪
输出连续动作

如果是 PI0-FAST:

text 复制代码
state -> 归一化 -> 256 桶离散化 -> "255 96 ..."
prompt + state_text -> prefix tokens
模型从 Action: 后逐 token 生成动作 token
FAST tokenizer 解码成连续动作

最后所有模型都会做:

text 复制代码
动作反归一化
裁剪到真实机器人动作维度
如果训练用 delta action,则还原成 absolute action
返回 action chunk

机器人通常不会一次执行完整 action chunk,而是执行前几步,然后重新观察、重新规划。


6. 最容易混淆的点

6.1 state 统一不是语义自动统一

模型接口统一成 state,但每个平台 state 的含义由适配器决定。换新平台时,最重要的是定义清楚 state/action 每一维的物理含义、单位、坐标系、是否 delta。

6.2 PI0.5 的 state 不是直接变成一个专用 state token

PI0.5 的 state 是:

text 复制代码
连续值 -> 归一化 -> 256 桶整数 -> 数字字符串 -> prompt 文本 -> tokenizer ids

它属于 prefix 文本条件。

6.3 PI0 和 PI0.5 的损失一样,区别在条件表达和网络调制

二者都是 flow matching MSE。PI0.5 的变化不是换了损失,而是 state 进入 prefix,time 通过 adaRMS 调制 Action Expert。

6.4 PI0-FAST 的 action tokens 不是普通文本

它们来自 FAST tokenizer,再映射到语言模型词表中的一段 token id。模型训练方式像语言模型,但生成的 token 最后会被解码回连续动作。

6.5 归一化统计量必须和训练一致

尤其是 PI0.5 和 PI0-FAST,state 离散化依赖归一化后的数值范围。如果 norm stats 不一致,数字文本会完全变味。