PI0、PI0.5、PI0-FAST 原理讲解
这份文档按模型逐个讲清楚 openpi 里的三类策略模型:
- PI0
- PI0.5
- 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 = 0,x_t 是真实动作。
当 t = 1,x_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 不一致,数字文本会完全变味。