Prhub

#33367 fix: pi05 models does not apply scale factor for language embeddings

原始 PR 作者 zealoct 合并时间 2026-08-04 11:25 文件变更 8 提交数 7 评论 2 代码增减 +242 / -45

执行摘要

修复 Pi0.5 语言嵌入缩放缺失,对齐 OpenPI 输出

官方 OpenPI PyTorch Pi0.5 实现(src/openpi/models_pytorch/pi0_pytorch.pyembed_prefix)在拼接图像嵌入前将语言 token 嵌入乘以 sqrt(hidden_dim),SGLang 遗漏该缩放,导致 action 输出与官方存在系统性偏差。PR body 指出原有 CI 未发现该问题:pi05_action_http 使用的旧 GT 由旧版 SGLang 生成,Python 一致性测试只检查可重复性与缓存复用,手动 benchmark 又对比了经过机器人后处理的 action,均掩盖了模型空间的差异。

值得精读。重点看 build_openpi_model_inputs_preprocess_image 两个设计点:前者演示了如何用参考实现自身的输入变换构造一致性对比,后者用显式标记避免图像归一化歧义。该 PR 为「与官方实现的数值一致性」提供了可执行、可回归的测试范本。

讨论亮点

本 PR 没有实质性 review 讨论:mickqian 直接 approve,唯一的 bot 评论是 Gemini Code Assist 停服说明;另一条评论仅触发 tag-and-rerun-ci。关键的验收证据链在 PR body 中:官方 GT 由 OpenPI revision 15a9616... 生成两次、SHA256 完全一致(319c1a1e...);rebased head 在 RTX 5090 直连 GPU 流式下(13.5 GiB / 1.81 s)通过同一一致性用例。

实现拆解

1. 核心修复:语言嵌入缩放

  • python/sglang/multimodal_gen/runtime/models/vlas/pi05_core.pyPi05CoreModel.embed_prefix 中,对 embed_language_tokens 的输出补乘 math.sqrt(lang_emb.shape[-1]),对齐 OpenPI 官方语义。改动位于前缀构建路径,不触达 denoise 循环。

2. 图像预处理对齐

  • pi05_preprocess.py_tensor_from_image 返回值从 torch.Tensor 改为 (torch.Tensor, bool, bool),携带「是否字节缩放、是否已归一化」两个标记。
  • _resize_with_pad_image_tensor 新增 round_to_uint8pad_value 参数,字节图 resize 后先 round 再映射到 [-1, 1]
  • 新增 _preprocess_image 统一入口:已归一化的 float32 图像只做 clamp 并保留 -1 letterbox 填充,避免二次归一化。

3. 基准升级到归一化模型空间

  • bench_pi05_openpi.py 新增 build_openpi_model_inputs,调用 OpenPI policy 的 _input_transform 得到与官方完全相同的模型输入(含 token、token mask、image mask、camera order)。
  • 拆出 _openpi_model_batch / _openpi_model_actions,直接获取模型空间 action 与 SGLang 对比,绕过 OpenPI 的机器人 unnormalization 与 horizon slicing。
  • --deterministic-noise 模式下用 OpenPI 变换后的输入喂给 SGLang;新增 --action-max-abs-diff / --action-mean-abs-diff 阈值参数;aloha 的 state 不再 pad 到 32 维,直接使用 14 维真实状态。

4. 测试与 CI 配套

  • test_pi05_runtime_helpers.py 新增 3 个单测:语言嵌入缩放、uint8 resize 先取整、归一化浮点图不二次归一化。
  • test_pi05_e2e.pytest_pi05_python_action_consistency 重命名为 test_pi05_python_action_repeatability_and_cache,明确可重复性语义,state 默认维度回归 14。
  • testcase_configs.py 的 CI 用例 state_dim 从 32 改为 14。
  • test_utils.pySGL_TEST_FILES_CI_DATA_REVISION 固定到 d05810e3ea3eff1d137dec723f6e66d9c11b470f,使一致性 CI 基于官方 provonance-backed GT。
  • docs/cookbook/vla/OpenPI/Pi0.5.mdx 更新官方 parity 数据与对比方法说明。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/models/vlas/pi05_core.py 模型核心 modified 5.79
python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/pi05_preprocess.py 预处理器 modified 7.52
python/sglang/multimodal_gen/benchmarks/bench_pi05_openpi.py 基准脚本 modified 7.74
python/sglang/multimodal_gen/test/unit/test_pi05_runtime_helpers.py 单元测试 modified 6.16
python/sglang/multimodal_gen/test/single_test_file/test_pi05_e2e.py 端到端测试 modified 4.82
python/sglang/multimodal_gen/test/server/testcase_configs.py 测试配置 modified 3.42
python/sglang/multimodal_gen/test/test_utils.py 测试工具 modified 3.42
docs/cookbook/vla/OpenPI/Pi0.5.mdx 文档 modified 2.32

关键符号

Pi05CoreModel.embed_prefix _preprocess_image _resize_with_pad_image_tensor _tensor_from_image build_openpi_model_inputs _openpi_model_batch _openpi_model_actions

关键源码片段

python/sglang/multimodal_gen/runtime/models/vlas/pi05_core.py core-logic

核心修复点:`embed_prefix` 中补上缺失的 `sqrt(hidden_dim)` 语言嵌入缩放,一行改动影响所有 Pi0.5 输出。

def embed_prefix(
    self,
    images: list[torch.Tensor],
    image_masks: list[torch.Tensor],
    tokens: torch.Tensor,
    token_masks: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
    embs = []
    pad_masks = []
    att_masks = []
    image_embs = self.paligemma_with_expert.embed_images(images)
    for image_emb, image_mask in zip(image_embs, image_masks, strict=True):
        batch_size, num_image_embs = image_emb.shape[:2]
        embs.append(image_emb)
        pad_masks.append(image_mask[:, None].expand(batch_size, num_image_embs))
        att_masks += [0] * num_image_embs
​
    lang_emb = self.paligemma_with_expert.embed_language_tokens(tokens)
    # 关键修复:OpenPI 官方在拼接图像嵌入前对语言嵌入乘以 sqrt(hidden_dim)。
    # 此前 SGLang 遗漏该缩放,导致语言分支与图像分支幅度失配,
    # action 输出相对官方实现出现系统性偏差。
    lang_emb = lang_emb * math.sqrt(lang_emb.shape[-1])
    embs.append(lang_emb)
    pad_masks.append(token_masks)
    att_masks += [0] * lang_emb.shape[1]
​
    embs = torch.cat(embs, dim=1)
    pad_masks = torch.cat(pad_masks, dim=1)
    att_masks_t = torch.tensor(att_masks, dtype=torch.bool, device=pad_masks.device)
    att_masks_t = att_masks_t[None, :].expand(pad_masks.shape[0], len(att_masks))
    return embs, pad_masks, att_masks_t
python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/pi05_preprocess.py data-contract

图像预处理契约重构:区分字节图与已归一化浮点图,匹配 OpenPI 的 resize 取整与 letterbox 填充行为。

def _preprocess_image(value: Any, size: tuple[int, int]) -> torch.Tensor:
    # _tensor_from_image 返回 ( 张量 , 是否字节缩放 , 是否已归一化到 [-1, 1])
    tensor, is_byte_scaled, is_normalized = _tensor_from_image(value)
    tensor = _resize_with_pad_image_tensor(
        tensor,
        size,
        round_to_uint8=is_byte_scaled,
        pad_value=-1.0 if is_normalized else 0.0,
    )
    if is_normalized:
        # 已归一化的 float32 图像:只 clamp,避免二次 2*x-1 映射
        return tensor.clamp_(-1.0, 1.0)
    return tensor * 2.0 - 1.0
​
​
def _resize_with_pad_image_tensor(
    tensor: torch.Tensor,
    size: tuple[int, int],
    *,
    round_to_uint8: bool = False,
    pad_value: float = 0.0,
) -> torch.Tensor:
    height, width = size
    if tensor.shape[-2:] == (height, width):
        return tensor
    _, cur_height, cur_width = tensor.shape
    ratio = max(cur_width / width, cur_height / height)
    resized_height = int(cur_height / ratio)
    resized_width = int(cur_width / ratio)
    tensor = F.interpolate(
        tensor[None],
        size=(resized_height, resized_width),
        mode="bilinear",
        align_corners=False,
    )[0]
    if round_to_uint8:
        # 与 OpenPI 对齐:字节图 resize 后先 round 再映射到 [-1, 1]
        tensor = torch.round(tensor * 255.0).clamp_(0.0, 255.0) / 255.0
    pad_h0, rem_h = divmod(height - resized_height, 2)
    pad_w0, rem_w = divmod(width - resized_width, 2)
    # 归一化图用 -1 填充 letterbox,字节图用 0 填充后再做 2*x-1
    return F.pad(
        tensor,
        (pad_w0, pad_w0 + rem_w, pad_h0, pad_h0 + rem_h),
        mode="constant",
        value=pad_value,
    )

评论区精华

旧 CI 为何未拦截语言嵌入缩放缺失 正确性

PR body 说明旧 `pi05_action_http` GT 由旧版 SGLang 生成,Python 一致性测试只验证可重复性与缓存复用,手动 benchmark 又对比了经过机器人后处理的 action,因此缩放缺失未被发现。

结论:引入 ci-data-diffusion#1 的官方 GT(OpenPI revision 15a9616、LeRobot checkpoint b211f3d),并改为在归一化模型空间、相同输入与确定性噪声下对比。 · 已解决

风险与影响

  • 输出行为变更:embed_prefix 的缩放会改变所有 Pi0.5 请求的 prefix 嵌入与最终 action,依赖旧输出的用户需重新验证。
  • 预处理契约变更:_tensor_from_image 返回类型从 torch.Tensor 变为三元组,虽有单测覆盖,但其他潜在调用方需同步适配。
  • GT 依赖固定 revision:test_utils.py 固定 ci-data-diffusiond05810e,若上游 OpenPI 实现或 LeRobot checkpoint 再变化,需重新生成 GT,否则 CI 会开始失败。
  • 无性能回归:缩放是 prefix 构建期的一次逐元素乘法,不进入 denoise 循环。
  • 用户:所有基于 Pi0.5 的 VLA 输出 action 数值会整体变化(向官方实现对齐),属于预期内的正确性修复。
  • 系统:影响面集中在 multimodal_gen 的 Pi0.5 模型族,图像预处理路径的返回契约变化需要模块内同步。
  • 团队:确立了「官方模型空间对比 + 可复现 GT provenance」的验证范式,后续 VLA 模型接入可复用 build_openpi_model_inputs 模式。
核心推理路径变更 图像预处理契约变更 CI GT 依赖固定 revision 旧行为输出不兼容

关联 Issue

#1 ci: add official OpenPI Pi0.5 action ground truth

完整报告

参与讨论