Prhub

#36301 [diffusion] feat: support batching for cosmos3 action generation

原始 PR 作者 FxxxxU 合并时间 2026-08-26 22:15 文件变更 7 提交数 4 评论 2 代码增减 +758 / -288

执行摘要

Cosmos3 动作生成支持请求内批处理,吞吐最高 3.3 倍

Cosmos3 action generation 原先每次请求只接受一个 observation;LIBERO 等机器人评估负载并行运行大量环境,在小 action-policy shape 下串行单观测 HTTP 调用严重闲置模型吞吐(PR body:"serial one-observation HTTP calls underutilize the model at small action-policy shapes")。同时作者在 Accuracy Tests 中承认此前"准确性测试不必要"的说法错误,因为 batching 改变 RNG 消耗顺序,batched kernel 可能与独立 B=1 执行存在数值差异,需要 GPU fixed-noise 一致性验证。

值得精读,尤其是四点:请求内批处理如何在不破坏 B=1 输出契约的前提下贯穿整条 diffusion 管线;prompt 标量广播与逐图配对的准入校验;新增 adapter 对 protocol.py 的隔离重构;作者对测量边界与数值一致性的坦诚声明。对机器人策略服务或 diffusion 管线开发者有直接借鉴价值。注意合并时 GPU fixed-noise 一致性与隔离 server 基准仍未完成,应作为后续验证项跟进。

讨论亮点

PR 没有留下 inline review 评论,核心讨论体现在 PR body 的自评与未完成清单:作者明确承认此前"准确性测试不必要"的判断错误,指出 batching 改变 RNG 消耗顺序、batched kernel 与独立 B=1 存在数值差异风险,GPU fixed-noise 一致性结果必须补齐;同时主动划定吞吐数据边界,强调 1.66x-3.33x 是 RLinf 端到端结果(pipeline_stage_num 随 batch 变化)而非隔离 SGLang 内核基准。

实现拆解

  1. 请求准入与图像、prompt 归一化:新增 python/sglang/multimodal_gen/runtime/entrypoints/action/cosmos3.py_images_from_observationimage / image_path / input_reference / images 提取图像,支持 list、tuple 与 [B, H, W, C] 的 uint8 ndarray 展开为 B 张图,并拒绝 dtype 或 shape 非法输入;_action_prompt 实现标量 prompt 广播与逐图配对,基数不一致立即抛错。protocol.py_normalize_observation 同步支持对图像列表逐项做 base64 / tensor payload 归一化。
  2. 采样参数与能力声明build_cosmos3_action_sampling_paramsserver_args.batching_max_size 读取批大小上限并注入采样参数;cosmos3_action_metadatacapabilities 中声明 batch_inputsmax_batch_sizebatched_action_modes(仅 policy)。protocol.action_metadata 对 Cosmos3 分支直接委托新适配器,删除约 200 行内联实现。
  3. 管线 stage 批维度传播model_specific_stages/cosmos3.pyCosmos3ImagePreprocessStage.forward 通过 DataType.ACTION + action_mode == policy 识别批处理,逐张 load_image、等比缩放裁剪后 torch.stack[B, 3, H, W]_tokenize_prompt 接受 str | list[str] 并输出 [B, S],且拒绝不同 tokenized 长度的 prompt(GEN cross-attention 不 mask padded text K/V)。cosmos3_action.pybuild_action_prompt 改为对描述列表逐条渲染结构化 JSON caption。
  4. 入口契约放宽runtime/entrypoints/utils.pyprepare_request 仅对 DataType.ACTION 放行非空字符串列表 prompt,其他管线仍要求 str,避免影响常规视觉生成。
  5. 测试与文档test/unit/test_cosmos3.py 新增配对保持、标量广播、基数不匹配、服务端上限、B=1 兼容、inverse_dynamics 拒绝等用例;cookbook 补充批处理请求/响应契约与 --batching-max-size 启动示例。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/entrypoints/action/cosmos3.py 动作适配 added 9.08
python/sglang/multimodal_gen/runtime/entrypoints/action/protocol.py 请求协议 modified 8.2
python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/cosmos3.py 管线阶段 modified 7.84
python/sglang/multimodal_gen/test/unit/test_cosmos3.py 单元测试 modified 7.52
python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/cosmos3_action.py 动作管线 modified 6.62
python/sglang/multimodal_gen/runtime/entrypoints/utils.py 请求入口 modified 5.76
docs/cookbook/diffusion/Cosmos/Cosmos3.mdx 文档 modified 3.53

关键符号

cosmos3_action_metadata _images_from_observation _action_prompt build_cosmos3_action_sampling_params _normalize_observation action_metadata Cosmos3ImagePreprocessStage.forward _tokenize_prompt build_action_prompt prepare_request

关键源码片段

python/sglang/multimodal_gen/runtime/entrypoints/action/cosmos3.py core-logic

新增的 Cosmos3 动作适配器,集中承载批处理核心逻辑:图像展开、prompt 广播 / 配对、采样参数构建与能力声明,是本 PR 功能入口与契约定义所在。

# Cosmos3 动作适配:把请求中的单张或多张观测图统一展开为图像列表,
# 并处理 prompt 的标量广播与逐图配对,为批处理管线提供统一的输入形态def _images_from_observation(observation: dict[str, Any]) -> list[Any]:
    # 优先读取单图字段,兼容 image / image_path / input_reference 三种命名
    image = None
    for name in ("image", "image_path", "input_reference"):
        if name in observation:
            image = observation[name]
            break
​
    # 未命中单图字段时,退化为 images 字典;只允许恰好一个键
    if image is None:
        images = observation.get("images")
        if images is None or (isinstance(images, dict) and not images):
            return []
        if not isinstance(images, dict) or len(images) != 1:
            raise ValueError(
                "Cosmos3 action input accepts one image field; use a list or "
                "a [B, H, W, C] array in that field for batched observations"
            )
        image = next(iter(images.values()))
​
    # list / tuple 以及四维 ndarray 都视为批量观测,逐张展开
    if isinstance(image, (list, tuple)):
        images = list(image)
    elif isinstance(image, np.ndarray) and image.ndim == 4:
        images = list(image)
    else:
        images = [image]
​
    normalized_images: list[Any] = []
    for item in images:
        if not isinstance(item, np.ndarray):
            normalized_images.append(item)
            continue
        # 数组形式必须是 uint8,shape 只允许 [H, W] 或 [H, W, C]
        if item.dtype != np.uint8:
            raise ValueError("Cosmos3 observation image arrays must use uint8 dtype")
        if item.ndim not in (2, 3):
            raise ValueError(
                "Cosmos3 observation image arrays must have shape [H, W] "
                f"or [H, W, C], got {tuple(item.shape)}"
            )
        normalized_images.append(Image.fromarray(item))
    return normalized_images
​
​
def _action_prompt(prompt: Any, batch_size: int) -> str | list[str]:
    # 标量 prompt:单图直接返回,多图则广播到每个观测
    if isinstance(prompt, str):
        return prompt if batch_size == 1 else [prompt] * batch_size
    if not isinstance(prompt, (list, tuple)) or not prompt:
        raise ValueError("Cosmos3 action prompt must be a string or non-empty list")
    if not all(isinstance(item, str) for item in prompt):
        raise ValueError("Cosmos3 action prompt list must contain only strings")
    prompts = list(prompt)
    # 单元素列表在多图时等价于标量广播
    if len(prompts) == 1 and batch_size > 1:
        prompts *= batch_size
    if len(prompts) != batch_size:
        raise ValueError(
            "Cosmos3 batched action input requires one prompt per image, got "
            f"{len(prompts)} prompt(s) and {batch_size} image(s)"
        )
    return prompts[0] if batch_size == 1 else prompts
python/sglang/multimodal_gen/runtime/entrypoints/action/protocol.py dependency-wiring

通用 action 协议层:将 Cosmos3 专用逻辑迁往新适配器,并让 _normalize_observation 支持图像列表逐项归一化,是批处理请求进入管线前的第一道关口。

def _normalize_observation(observation: dict[str, Any]) -> dict[str, Any]:
    normalized = dict(observation)
    images = normalized.get("images")
    if isinstance(images, dict):
        normalized["images"] = {
            name: _normalize_image_value(value) for name, value in images.items()
        }
    # 批处理下 image / image_path / input_reference 可能是图像列表,
    # 需要逐项归一化(base64 / tensor payload),不能只处理单个值
    for name in ("image", "image_path", "input_reference"):
        if name in normalized:
            value = normalized[name]
            normalized[name] = (
                [_normalize_image_value(item) for item in value]
                if isinstance(value, (list, tuple))
                else _normalize_image_value(value)
            )
    # state / noise 等 tensor payload 保持原有解码逻辑
    for key in ("state", "observation.state", "noise", "observation.noise"):
        value = normalized.get(key)
        if isinstance(value, dict):
            normalized[key] = _decode_tensor_payload(value)
    return normalized
python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/cosmos3.py data-contract

管线核心 stage:预处理阶段在 policy 批处理下输出 [B, 3, H, W],分词阶段接受字符串列表,是批维度在采样与去噪之间传播的关键位置。

class Cosmos3ImagePreprocessStage(PipelineStage):
    """加载、等比缩放并中心裁剪 conditioning 输入。    普通 I2V 写入 [1, 3, H, W] 的 batch.preprocessed_image;
    批量 policy 请求写入 [B, 3, H, W];V2V 写入 [1, 3, T_in, H, W]。
    """
​
    def forward(self, batch: Req, server_args: ServerArgs) -> Req:
        image_path = batch.image_path
        video_path = batch.video_path
        # 只有 action policy 模式才允许保留图像列表(即批处理),
        # 普通 I2V 对 list 仍取第一张,确保既有行为不被破坏
        is_action_policy = (
            batch.data_type == DataType.ACTION
            and getattr(batch.sampling_params, "action_mode", None)
            == ACTION_MODE_POLICY
        )
        if isinstance(image_path, list) and not is_action_policy:
            image_path = image_path[0] if image_path else None
        if isinstance(video_path, list):
            video_path = video_path[0] if video_path else None
​
        if image_path and video_path:
            raise ValueError(
                "Cosmos3 accepts either --image-path (I2V) or --video-path "
                "(V2V), not both"
            )
​
        target_h, target_w = batch.height, batch.width
​
        if image_path is not None:
            # 逐张加载并缩放裁剪,最后堆叠成 [B, 3, H, W];
            # 单张时 B == 1,与原有 I2V 契约自然兼容
            image_sources = (
                list(image_path)
                if isinstance(image_path, (list, tuple))
                else [image_path]
            )
            if not image_sources:
                raise ValueError("Cosmos3 I2V image list is empty")
            tensors: list[torch.Tensor] = []
            for src in image_sources:
                image = load_image(src)
                image = _resize_crop_pil(image, target_w, target_h)
                tensors.append(_pil_to_normalized_tensor(image))
            batch.preprocessed_image = torch.stack(tensors, dim=0).contiguous()
            self.log_info(
                f"Preprocessed {len(tensors)} conditioning image(s) to "
                f"{target_w}x{target_h}"
            )
            return batch
​
        # V2V 与 T2V / T2I 分支保持不变
        ...

评论区精华

GPU fixed-noise 一致性验证缺失 测试

PR body 自述:"batching changes RNG consumption and batched kernels can differ numerically from independent B=1 execution. A GPU fixed-noise B=1-versus-batch consistency result is still required before merge." 作者承认此前 " 准确性测试不必要 " 的说法错误。

结论:未解决。checklist 中 GPU fixed-noise B=1-versus-batch 一致性结果仍未勾选,合并前需要补齐;RLinf 闭环结果不能替代隔离数值 parity。 · 待处理

吞吐数据测量边界 性能

作者指出 pipeline_stage_num 随 observation batch 变化,结果是 RLinf 端到端而非隔离 SGLang 内核基准;raw latency、GPU SKU、warmup/repetition 策略、峰值 VRAM 与 per-stage 计时仍缺失。

结论:已记录为后续任务,合并前未完成隔离 server 基准;报告数字应视为端到端参考。 · 待处理

风险与影响

  1. GPU 数值一致性未验证:这是合并前最关键的缺口。批处理改变 RNG 消耗顺序,batched kernel 可能与独立 B=1 数值不同,而 RLinf 闭环成功率因评估拓扑随 batch 变化,不能替代隔离数值 parity(见 checklist 未完成项)。
  2. 性能数字口径:当前吞吐数字来自 RLinf 端到端评估,缺少 raw latency、GPU SKU、warmup/repetition 策略、峰值 VRAM 与 per-stage 计时,无法进行 apples-to-apples 的 SGLang server 基准归因。
  3. 契约与兼容性:默认响应仍按 item 返回 [H, D],需要紧凑 [B, H, D] 的 msgpack 客户端必须显式设置 runtime.response_format="raw";不同 tokenized prompt 长度会被拒绝,用户需自行对齐 prompt。
  4. 资源压力:批处理提升吞吐的同时提高单请求显存占用,--batching-max-size 是唯一保护阀,配置不当可能 OOM。
  5. 数据契约变更面:预处理输出从 [1, 3, H, W] 变为可 [B, 3, H, W],涉及 latent 准备、denoising、CFG 2B timesteps 与 action decoding 多处 shape 假设,若有遗漏 stage 未跟进会产生静默错误。

对用户:LIBERO 等多环境机器人评估可通过单次 HTTP 请求批处理多个观测,显著提升吞吐(报告最高 3.3 倍),但需理解 prompt 配对规则、--batching-max-size 与 raw 响应格式。对系统:multimodal_gen action endpoint 的请求/响应契约扩展了 batch 维度,pipeline stages 的数据契约从单图变为 [B, ...];普通 I2V/V2V 与 inverse_dynamics/forward_dynamics 有分支保护,影响受控。对团队:adapter 隔离模式(新增 cosmos3.py)为其他 policy family 复用批处理能力提供了模板,同时把"验证边界"显式写进 PR checklist,提升了结果可复现性意识。

GPU 数值一致性未验证 缺少隔离性能基准 数据契约变更 prompt 长度限制 显存压力上升

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论