vllm-project--vllm-omni
3270 行
148 KiB
Python
3270 行
148 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Cosmos3 text/image/video/sound/action pipeline for vllm-omni.
|
|
|
|
One pipeline class serves the Cosmos3 family modes. Output modality is selected
|
|
mainly by ``prompt["modalities"]``:
|
|
|
|
* ``"image"`` selects T2I (text-to-image) and forces a single visual frame.
|
|
* ``"video"`` or omitted modalities select video generation.
|
|
* ``"audio"`` is accepted for compatibility but does not request sound by
|
|
itself; sound is enabled with ``generate_sound`` or ``sound_gen``.
|
|
|
|
Video generation is further specialized by inputs and extra args:
|
|
|
|
* no image/video input: T2V (text-to-video).
|
|
* ``multi_modal_data["image"]``: I2V (image-to-video).
|
|
* ``multi_modal_data["video"]`` with no action/transfer mode: V2V
|
|
(video-to-video).
|
|
* transfer hints (``edge``, ``blur``, ``depth``, ``seg``, or ``wsm``): control
|
|
transfer video generation.
|
|
* ``action_mode``: action-capable video generation. RoboLab/OpenPI observation
|
|
payloads in ``extra_args["robot_obs"]`` or ``extra_args["observation"]``
|
|
bypass normal video output and return action-only custom output.
|
|
|
|
Generated sound is video-only, cannot be combined with action or transfer, and
|
|
is produced from sound latents rather than from ``multi_modal_data["audio"]``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import os
|
|
import time
|
|
from collections.abc import Iterable, Mapping
|
|
from dataclasses import fields
|
|
from typing import Any, ClassVar
|
|
|
|
import numpy as np
|
|
import PIL.Image
|
|
import torch
|
|
from diffusers import UniPCMultistepScheduler
|
|
from diffusers.utils.torch_utils import randn_tensor
|
|
from diffusers.video_processor import VideoProcessor
|
|
from torch import nn
|
|
from transformers import AutoTokenizer
|
|
from vllm.logger import init_logger
|
|
from vllm.model_executor.models.utils import AutoWeightsLoader
|
|
|
|
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
|
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import DistributedAutoencoderKLWan
|
|
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
|
|
from vllm_omni.diffusion.distributed.parallel_state import (
|
|
get_classifier_free_guidance_world_size,
|
|
)
|
|
from vllm_omni.diffusion.distributed.utils import get_local_device
|
|
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
|
|
from vllm_omni.diffusion.models.interface import (
|
|
ReferenceVideoDecodeSpec,
|
|
SupportImageInput,
|
|
)
|
|
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin, _is_rank_zero
|
|
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
|
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
|
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
|
from vllm_omni.entrypoints.openai.video_api_utils import positive_float
|
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
|
|
|
from .action import (
|
|
ACTION_MODE_FORWARD_DYNAMICS,
|
|
ACTION_MODE_INVERSE_DYNAMICS,
|
|
ACTION_MODE_POLICY,
|
|
action_start_frame_offset,
|
|
build_action_condition_mask,
|
|
build_vision_condition_mask,
|
|
find_closest_target_size,
|
|
load_action_tensor,
|
|
normalize_action_mode,
|
|
pad_action_to_dim,
|
|
resolve_domain_id,
|
|
vision_condition_indexes,
|
|
)
|
|
from .transfer import (
|
|
Cosmos3TransferConfig,
|
|
has_transfer_hints,
|
|
load_or_compute_control_frames,
|
|
media_hw,
|
|
media_to_uint8_cthw,
|
|
normalized_video_to_uint8_cthw,
|
|
pad_temporal_frames,
|
|
resize_center_crop_uint8_cthw,
|
|
resolve_transfer_config,
|
|
transfer_max_frames_from_extra_args,
|
|
uint8_cthw_to_normalized_5d,
|
|
)
|
|
from .transformer_cosmos3 import Cosmos3VFMTransformer, resolve_sound_gen
|
|
from .utils import (
|
|
COSMOS3_DEFAULT_CONDITION_FRAME_INDEXES_VISION,
|
|
COSMOS3_VAE_TEMPORAL_COMPRESSION,
|
|
ROBOLAB_CONCAT_VIEW_DESCRIPTION,
|
|
ROBOLAB_DEFAULT_ACTION_CHUNK_SIZE,
|
|
ROBOLAB_DEFAULT_ACTION_SPACE,
|
|
ROBOLAB_DEFAULT_CONDITIONING_FPS,
|
|
ROBOLAB_DEFAULT_DOMAIN_NAME,
|
|
ROBOLAB_DEFAULT_FLOW_SHIFT,
|
|
ROBOLAB_DEFAULT_GUIDANCE_SCALE,
|
|
ROBOLAB_DEFAULT_IMAGE_HEIGHT,
|
|
ROBOLAB_DEFAULT_IMAGE_WIDTH,
|
|
ROBOLAB_DEFAULT_NUM_INFERENCE_STEPS,
|
|
ROBOLAB_DEFAULT_RAW_ACTION_DIM,
|
|
ROBOLAB_DEFAULT_RESOLUTION,
|
|
ROBOLAB_MIDTRAIN_RAW_ACTION_DIM,
|
|
RoboLabActionPostprocessInputs,
|
|
RoboLabPolicyInputs,
|
|
build_abs_pose_from_components,
|
|
build_robolab_unipc_scheduler,
|
|
condition_pixel_frame_count,
|
|
convert_midtrain_rotation,
|
|
ensure_2d_float_array,
|
|
ensure_gripper_array,
|
|
extract_robolab_image,
|
|
extract_robolab_prompt_image,
|
|
lazy_action_transform_pipeline,
|
|
make_robolab_action_postprocess_inputs,
|
|
next_robolab_seed,
|
|
normalize_condition_frame_indexes_vision,
|
|
normalize_condition_video_keep,
|
|
normalize_robolab_action_space,
|
|
pose_abs_to_rel,
|
|
postprocess_robolab_action,
|
|
resize_rgb_uint8,
|
|
)
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
COSMOS3_DEFAULT_CONDITION_PIXEL_FRAMES = (
|
|
max(COSMOS3_DEFAULT_CONDITION_FRAME_INDEXES_VISION) * COSMOS3_VAE_TEMPORAL_COMPRESSION + 1
|
|
)
|
|
COSMOS3_V2V_DEFAULT_FLOW_SHIFT = 10.0
|
|
COSMOS3_DURATION_TEMPLATE = "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS."
|
|
COSMOS3_RESOLUTION_TEMPLATE = "This video is of {height}x{width} resolution."
|
|
COSMOS3_IMAGE_RESOLUTION_TEMPLATE = "This image is of {height}x{width} resolution."
|
|
COSMOS3_INVERSE_DURATION_TEMPLATE = "The video is not {duration:.1f} seconds long and is not of {fps:.0f} FPS."
|
|
COSMOS3_INVERSE_RESOLUTION_TEMPLATE = "This video is not of {height}x{width} resolution."
|
|
COSMOS3_INVERSE_IMAGE_RESOLUTION_TEMPLATE = "This image is not of {height}x{width} resolution."
|
|
# NOTE: Intentional typo in "give" instead of "given" to match training setup.
|
|
COSMOS3_SYSTEM_PROMPT = "You are a helpful assistant who will generate videos from a give prompt."
|
|
COSMOS3_T2I_SYSTEM_PROMPT = "You are a helpful assistant who will generate images from a give prompt."
|
|
|
|
COSMOS3_T2V_DEFAULT_HEIGHT = 720
|
|
COSMOS3_T2V_DEFAULT_WIDTH = 1280
|
|
COSMOS3_T2V_DEFAULT_NUM_FRAMES = 189
|
|
COSMOS3_T2V_DEFAULT_NUM_INFERENCE_STEPS = 35
|
|
COSMOS3_T2V_DEFAULT_GUIDANCE_SCALE = 6.0
|
|
|
|
COSMOS3_T2I_DEFAULT_HEIGHT = 1024
|
|
COSMOS3_T2I_DEFAULT_WIDTH = 1024
|
|
COSMOS3_T2I_DEFAULT_NUM_INFERENCE_STEPS = 50
|
|
COSMOS3_T2I_DEFAULT_GUIDANCE_SCALE = 7.0
|
|
COSMOS3_T2I_DEFAULT_FLOW_SHIFT = 3.0
|
|
COSMOS3_T2I_DEFAULT_GUIDANCE_INTERVAL: tuple[float, float] = (400.0, 1000.0)
|
|
|
|
# Truncation cap on the prompt token count (shared by T2I and T2V). Prompts
|
|
# are tokenized to their natural length (no padding); this only bounds the
|
|
# UND pathway / GEN cross-attention cost for pathologically long prompts.
|
|
COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH = 4096
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Post-process function (registered in registry.py)
|
|
# ---------------------------------------------------------------------------
|
|
def get_cosmos3_pre_process_func(od_config: OmniDiffusionConfig):
|
|
"""Build the request preprocessor for Cosmos3 image/video inputs.
|
|
|
|
For plain T2V (no image or video in ``multi_modal_data``), the request is
|
|
returned unchanged after the optional guardrail check. For I2V, the
|
|
conditioning image is loaded, aspect-resized, center-cropped, and stored as
|
|
``additional_information.preprocessed_image``. For V2V, source frames are
|
|
cropped to the target size and stored as
|
|
``additional_information.preprocessed_video``.
|
|
|
|
Action modes reuse image/video preprocessing but use action-specific resize
|
|
and padding rules. Transfer requests store
|
|
``additional_information.preprocessed_transfer_video`` for optional input
|
|
video conditioning. Cosmos3 sound generation is not driven by
|
|
``multi_modal_data["audio"]``; it is enabled later from sampling params.
|
|
"""
|
|
from .guardrails import check_text_safety, ensure_initialized, is_guardrails_enabled
|
|
|
|
video_processor = VideoProcessor(vae_scale_factor=16)
|
|
# Eager-load guardrail models at pipeline build time when the server-level
|
|
# gate is on. Per-request overrides only decide whether the loaded models
|
|
# are *invoked* — they cannot turn checks on without a server-side preload.
|
|
if is_guardrails_enabled(od_config):
|
|
ensure_initialized(od_config)
|
|
|
|
def _extra_args(request: OmniDiffusionRequest) -> dict[str, Any]:
|
|
extra = getattr(getattr(request, "sampling_params", None), "extra_args", None)
|
|
return extra if isinstance(extra, dict) else {}
|
|
|
|
def _request_action_mode(request: OmniDiffusionRequest) -> str | None:
|
|
return normalize_action_mode(_extra_args(request).get("action_mode"))
|
|
|
|
def _set_transfer_size_from_image(request: OmniDiffusionRequest, image: PIL.Image.Image) -> tuple[int, int]:
|
|
extra = _extra_args(request)
|
|
resolution = extra.get("resolution", extra.get("image_size", 720))
|
|
target_w, target_h = find_closest_target_size(image.height, image.width, resolution)
|
|
request.sampling_params.height = target_h
|
|
request.sampling_params.width = target_w
|
|
return int(target_h), int(target_w)
|
|
|
|
def _set_action_size_from_image(request: OmniDiffusionRequest, image: PIL.Image.Image) -> tuple[int, int]:
|
|
sp = request.sampling_params
|
|
if sp.height is not None and sp.width is not None:
|
|
return int(sp.height), int(sp.width)
|
|
|
|
extra = _extra_args(request)
|
|
resolution = extra.get("resolution", extra.get("image_size", 480))
|
|
target_w, target_h = find_closest_target_size(image.height, image.width, resolution)
|
|
if sp.height is None:
|
|
sp.height = target_h
|
|
if sp.width is None:
|
|
sp.width = target_w
|
|
return int(sp.height), int(sp.width)
|
|
|
|
def _pil_to_rgb(value: Any) -> PIL.Image.Image:
|
|
if isinstance(value, str):
|
|
return PIL.Image.open(value).convert("RGB")
|
|
if isinstance(value, PIL.Image.Image):
|
|
return value.convert("RGB")
|
|
if isinstance(value, np.ndarray):
|
|
array = value
|
|
if array.ndim == 3 and array.shape[0] in (3, 4) and array.shape[-1] not in (3, 4):
|
|
array = np.transpose(array, (1, 2, 0))
|
|
if np.issubdtype(array.dtype, np.floating):
|
|
if array.min() < 0.0 or array.max() > 1.0:
|
|
array = np.clip(array, -1.0, 1.0) * 0.5 + 0.5
|
|
array = (np.clip(array, 0.0, 1.0) * 255.0).round().astype(np.uint8)
|
|
return PIL.Image.fromarray(array).convert("RGB")
|
|
if isinstance(value, torch.Tensor):
|
|
tensor = value.detach().cpu()
|
|
if tensor.ndim == 3 and tensor.shape[0] in (3, 4):
|
|
tensor = tensor.permute(1, 2, 0)
|
|
if tensor.is_floating_point():
|
|
if tensor.min().item() < 0.0 or tensor.max().item() > 1.0:
|
|
tensor = tensor.clamp(-1.0, 1.0) * 0.5 + 0.5
|
|
tensor = (tensor.clamp(0.0, 1.0) * 255.0).round().to(torch.uint8)
|
|
return PIL.Image.fromarray(tensor.numpy()).convert("RGB")
|
|
raise TypeError(
|
|
f"Cosmos3 preprocessing expected PIL image, numpy array, torch tensor, or path, got {type(value)!r}."
|
|
)
|
|
|
|
def _resize_and_pad_action_image(image: PIL.Image.Image, target_h: int, target_w: int) -> PIL.Image.Image:
|
|
scale = min(target_w / image.width, target_h / image.height, 1.0)
|
|
resize_w = max(1, int(scale * image.width + 0.5))
|
|
resize_h = max(1, int(scale * image.height + 0.5))
|
|
if (resize_w, resize_h) != image.size:
|
|
image = image.resize((resize_w, resize_h), PIL.Image.Resampling.BICUBIC)
|
|
|
|
array = np.asarray(image)
|
|
pad_h = target_h - resize_h
|
|
pad_w = target_w - resize_w
|
|
if pad_h < 0 or pad_w < 0:
|
|
raise ValueError(
|
|
f"Cosmos3 action image resize exceeded target size: resized={(resize_h, resize_w)}, "
|
|
f"target={(target_h, target_w)}."
|
|
)
|
|
if pad_h == 0 and pad_w == 0:
|
|
return image
|
|
pad_mode = "reflect" if pad_h < resize_h and pad_w < resize_w else "edge"
|
|
padded = np.pad(array, ((0, pad_h), (0, pad_w), (0, 0)), mode=pad_mode)
|
|
return PIL.Image.fromarray(padded)
|
|
|
|
def _preprocess_action_image(image: PIL.Image.Image, target_h: int, target_w: int) -> torch.Tensor:
|
|
image = _resize_and_pad_action_image(image, target_h, target_w)
|
|
return video_processor.preprocess(image, height=target_h, width=target_w)
|
|
|
|
def _preprocess_action_video(frames: list[Any], target_h: int, target_w: int) -> torch.Tensor:
|
|
if not frames:
|
|
raise ValueError("Cosmos3 action video input must contain at least one frame.")
|
|
processed = [_preprocess_action_image(_pil_to_rgb(frame), target_h, target_w).squeeze(0) for frame in frames]
|
|
return torch.stack(processed, dim=1).unsqueeze(0).contiguous()
|
|
|
|
def _preprocess_condition_image(image: PIL.Image.Image, target_h: int, target_w: int) -> torch.Tensor:
|
|
scale = max(target_w / image.width, target_h / image.height)
|
|
resize_w = int(np.ceil(scale * image.width))
|
|
resize_h = int(np.ceil(scale * image.height))
|
|
image = image.resize((resize_w, resize_h), PIL.Image.Resampling.LANCZOS)
|
|
left = (resize_w - target_w) // 2
|
|
top = (resize_h - target_h) // 2
|
|
image = image.crop((left, top, left + target_w, top + target_h))
|
|
return video_processor.preprocess(image, height=target_h, width=target_w)
|
|
|
|
def _video_payload_value(video: Any, key: str) -> Any:
|
|
if isinstance(video, Mapping):
|
|
return video.get(key)
|
|
return getattr(video, key, None)
|
|
|
|
def _video_payload_fps(video: Any) -> float | None:
|
|
for key in ("fps", "frame_rate", "source_fps", "input_fps", "avg_fps", "average_fps"):
|
|
fps = positive_float(_video_payload_value(video, key))
|
|
if fps is not None:
|
|
return fps
|
|
for key in ("metadata", "info"):
|
|
metadata = _video_payload_value(video, key)
|
|
if metadata is None or metadata is video:
|
|
continue
|
|
fps = _video_payload_fps(metadata)
|
|
if fps is not None:
|
|
return fps
|
|
if isinstance(video, Mapping):
|
|
for key in ("frames", "data", "video"):
|
|
nested = video.get(key)
|
|
if nested is None or nested is video:
|
|
continue
|
|
fps = _video_payload_fps(nested)
|
|
if fps is not None:
|
|
return fps
|
|
return None
|
|
|
|
def _unwrap_video_payload(video: Any) -> Any:
|
|
if isinstance(video, Mapping):
|
|
for key in ("frames", "data", "video"):
|
|
nested = video.get(key)
|
|
if nested is not None:
|
|
return nested
|
|
return video
|
|
|
|
def _video_payload_to_frames(video: Any) -> list[Any]:
|
|
video = _unwrap_video_payload(video)
|
|
if isinstance(video, list):
|
|
return video
|
|
if isinstance(video, torch.Tensor):
|
|
tensor = video.detach().cpu()
|
|
if tensor.ndim == 5:
|
|
if tensor.shape[0] != 1:
|
|
raise TypeError("Cosmos3 video preprocessing supports only batch size 1.")
|
|
tensor = tensor[0]
|
|
if tensor.ndim == 4 and tensor.shape[0] in (3, 4) and tensor.shape[-1] not in (3, 4):
|
|
return [tensor[:, i] for i in range(tensor.shape[1])]
|
|
if tensor.ndim == 4 and tensor.shape[-1] in (3, 4):
|
|
return [tensor[i] for i in range(tensor.shape[0])]
|
|
if isinstance(video, np.ndarray):
|
|
array = video
|
|
if array.ndim == 5:
|
|
if array.shape[0] != 1:
|
|
raise TypeError("Cosmos3 video preprocessing supports only batch size 1.")
|
|
array = array[0]
|
|
if array.ndim == 4 and array.shape[0] in (3, 4) and array.shape[-1] not in (3, 4):
|
|
return [array[:, i] for i in range(array.shape[1])]
|
|
if array.ndim == 4 and array.shape[-1] in (3, 4):
|
|
return [array[i] for i in range(array.shape[0])]
|
|
raise TypeError("Cosmos3 video input must be a non-empty list of frames or a single video tensor/array.")
|
|
|
|
def _select_video_frames(frames: list[Any], max_frames: int, keep: str) -> list[Any]:
|
|
if not frames:
|
|
raise ValueError("Cosmos3 video input must contain at least one frame.")
|
|
if keep == "last":
|
|
return frames[-max_frames:]
|
|
return frames[:max_frames]
|
|
|
|
def _preprocess_condition_video(
|
|
frames: list[Any],
|
|
target_h: int,
|
|
target_w: int,
|
|
max_frames: int,
|
|
keep: str,
|
|
) -> torch.Tensor:
|
|
selected = _select_video_frames(frames, max_frames, keep)
|
|
processed = [
|
|
_preprocess_condition_image(_pil_to_rgb(frame), target_h, target_w).squeeze(0) for frame in selected
|
|
]
|
|
return torch.stack(processed, dim=1).unsqueeze(0).contiguous()
|
|
|
|
def pre_process_func(request: OmniDiffusionRequest) -> OmniDiffusionRequest:
|
|
action_mode = _request_action_mode(request)
|
|
prompt = request.prompt
|
|
if is_guardrails_enabled(od_config, request.sampling_params):
|
|
text = prompt if isinstance(prompt, str) else prompt.get("prompt", "")
|
|
check_text_safety(text)
|
|
|
|
if isinstance(prompt, str):
|
|
return request
|
|
multi_modal_data = prompt.get("multi_modal_data", {}) or {}
|
|
raw_image = multi_modal_data.get("image")
|
|
raw_video = multi_modal_data.get("video")
|
|
if raw_image is None and raw_video is None:
|
|
return request
|
|
if raw_image is not None and raw_video is not None and action_mode is None:
|
|
raise ValueError("Cosmos3 non-action generation accepts either image or video input, not both.")
|
|
|
|
if "additional_information" not in prompt:
|
|
prompt["additional_information"] = {}
|
|
|
|
raw_video_frames: list[Any] | None = None
|
|
transfer_input_fps: float | None = None
|
|
if raw_video is not None:
|
|
transfer_input_fps = _video_payload_fps(raw_video)
|
|
raw_video_frames = _video_payload_to_frames(raw_video)
|
|
if not raw_video_frames:
|
|
raise TypeError("Cosmos3 video input must be a non-empty list of PIL images or image paths.")
|
|
|
|
if raw_image is None:
|
|
assert raw_video_frames is not None # raw_image and raw_video can't both be None here
|
|
image = _pil_to_rgb(raw_video_frames[0])
|
|
else:
|
|
image = _pil_to_rgb(raw_image)
|
|
extra = _extra_args(request)
|
|
transfer_requested = action_mode is None and has_transfer_hints(extra)
|
|
|
|
# Auto-calculate H/W from aspect ratio (720p max area)
|
|
if transfer_requested:
|
|
_set_transfer_size_from_image(request, image)
|
|
elif request.sampling_params.height is None or request.sampling_params.width is None:
|
|
if action_mode is not None:
|
|
_set_action_size_from_image(request, image)
|
|
else:
|
|
max_area = 720 * 1280
|
|
aspect_ratio = image.height / image.width
|
|
mod_value = 16
|
|
height = round(np.sqrt(max_area * aspect_ratio)) // mod_value * mod_value
|
|
width = round(np.sqrt(max_area / aspect_ratio)) // mod_value * mod_value
|
|
if request.sampling_params.height is None:
|
|
request.sampling_params.height = height
|
|
if request.sampling_params.width is None:
|
|
request.sampling_params.width = width
|
|
|
|
target_w = request.sampling_params.width
|
|
target_h = request.sampling_params.height
|
|
if action_mode is not None:
|
|
prompt["additional_information"]["preprocessed_image"] = _preprocess_action_image(
|
|
image,
|
|
int(target_h),
|
|
int(target_w),
|
|
)
|
|
elif raw_video is None:
|
|
prompt["additional_information"]["preprocessed_image"] = _preprocess_condition_image(
|
|
image,
|
|
int(target_h),
|
|
int(target_w),
|
|
)
|
|
else:
|
|
assert raw_video_frames is not None
|
|
if transfer_requested:
|
|
if transfer_input_fps is not None:
|
|
prompt["additional_information"]["transfer_input_fps"] = transfer_input_fps
|
|
transfer_frames = media_to_uint8_cthw(
|
|
raw_video_frames,
|
|
height=int(target_h),
|
|
width=int(target_w),
|
|
max_frames=transfer_max_frames_from_extra_args(extra),
|
|
)
|
|
prompt["additional_information"]["preprocessed_transfer_video"] = uint8_cthw_to_normalized_5d(
|
|
transfer_frames,
|
|
dtype=torch.float32,
|
|
)
|
|
else:
|
|
condition_frame_indexes_vision = normalize_condition_frame_indexes_vision(
|
|
extra.get(
|
|
"condition_frame_indexes_vision",
|
|
prompt.get("condition_frame_indexes_vision"),
|
|
)
|
|
)
|
|
keep = normalize_condition_video_keep(
|
|
extra.get("condition_video_keep", prompt.get("condition_video_keep"))
|
|
)
|
|
max_frames = condition_pixel_frame_count(condition_frame_indexes_vision)
|
|
prompt["additional_information"]["preprocessed_video"] = _preprocess_condition_video(
|
|
raw_video_frames,
|
|
int(target_h),
|
|
int(target_w),
|
|
max_frames,
|
|
keep,
|
|
)
|
|
prompt["additional_information"]["condition_frame_indexes_vision"] = list(
|
|
condition_frame_indexes_vision
|
|
)
|
|
if action_mode is not None and raw_video_frames is not None:
|
|
prompt["additional_information"]["preprocessed_video"] = _preprocess_action_video(
|
|
raw_video_frames,
|
|
int(target_h),
|
|
int(target_w),
|
|
)
|
|
request.prompt = prompt
|
|
|
|
return request
|
|
|
|
return pre_process_func
|
|
|
|
|
|
def get_cosmos3_post_process_func(od_config: OmniDiffusionConfig):
|
|
"""Build the postprocessor for Cosmos3 image, video, and video+audio output.
|
|
|
|
The pipeline returns image payloads as ``{"image": tensor}`` and video
|
|
payloads as ``{"video": tensor}``. Sound-enabled video returns the same
|
|
video payload plus ``audio`` and ``audio_sample_rate``. Image output with
|
|
audio is rejected because Cosmos3 sound generation is video-only.
|
|
"""
|
|
from .guardrails import check_video_safety, is_guardrails_enabled
|
|
|
|
video_processor = VideoProcessor(vae_scale_factor=16)
|
|
|
|
def _sampling_param(sampling_params, key: str, default=None):
|
|
extra = getattr(sampling_params, "extra_args", None)
|
|
if isinstance(extra, dict) and extra.get(key) is not None:
|
|
return extra[key]
|
|
value = getattr(sampling_params, key, None)
|
|
return default if value is None else value
|
|
|
|
def _resolve_output_fps(sampling_params):
|
|
fps = (
|
|
_sampling_param(sampling_params, "resolved_frame_rate")
|
|
or _sampling_param(sampling_params, "frame_rate")
|
|
or _sampling_param(sampling_params, "fps")
|
|
or 24.0
|
|
)
|
|
try:
|
|
fps_value = float(fps)
|
|
except (TypeError, ValueError):
|
|
fps_value = 24.0
|
|
if fps_value <= 0:
|
|
fps_value = 24.0
|
|
return int(fps_value) if fps_value.is_integer() else fps_value
|
|
|
|
def post_process_func(
|
|
output: torch.Tensor | dict[str, torch.Tensor] | tuple,
|
|
output_type: str = "np",
|
|
sampling_params=None,
|
|
):
|
|
if output_type == "latent":
|
|
return output
|
|
|
|
audio = None
|
|
audio_sample_rate = None
|
|
if isinstance(output, dict):
|
|
if "image" in output and "video" in output:
|
|
raise ValueError("Cosmos3 output cannot contain both image and video payloads.")
|
|
if "image" in output:
|
|
video = output["image"]
|
|
elif "video" in output:
|
|
video = output["video"]
|
|
else:
|
|
raise ValueError("Cosmos3 postprocess expected an 'image' or 'video' output payload.")
|
|
audio = output.get("audio")
|
|
audio_sample_rate = output.get("audio_sample_rate")
|
|
elif isinstance(output, tuple):
|
|
if len(output) == 3:
|
|
video, audio, audio_sample_rate = output
|
|
elif len(output) == 2:
|
|
video, audio = output
|
|
else:
|
|
raise ValueError(
|
|
"Cosmos3 postprocess expects output tensor, output dict, or (video, audio[, sample_rate]) tuple."
|
|
)
|
|
else:
|
|
video = output
|
|
|
|
if isinstance(output, dict) and "image" in output:
|
|
if audio is not None:
|
|
raise ValueError("Cosmos3 text-to-image postprocess does not support audio output.")
|
|
if video.ndim != 5 or video.shape[2] != 1:
|
|
raise ValueError(
|
|
"Cosmos3 text-to-image postprocess expects decoded output "
|
|
f"with shape [B, C, 1, H, W], got {tuple(video.shape)}."
|
|
)
|
|
image = video.squeeze(2) # [B, 3, H, W]
|
|
if is_guardrails_enabled(od_config, sampling_params):
|
|
# check_video_safety expects a 5D tensor; re-add T axis.
|
|
checked = check_video_safety(image.unsqueeze(2))
|
|
image = checked.squeeze(2)
|
|
return video_processor.postprocess(image, output_type="pil")
|
|
if is_guardrails_enabled(od_config, sampling_params):
|
|
video = check_video_safety(video)
|
|
processed_video = video_processor.postprocess_video(video, output_type=output_type)
|
|
if audio is None:
|
|
return processed_video
|
|
if isinstance(audio, torch.Tensor):
|
|
audio = audio.detach().cpu()
|
|
result = {
|
|
"video": processed_video,
|
|
"audio": audio,
|
|
"fps": _resolve_output_fps(sampling_params),
|
|
}
|
|
if audio_sample_rate is not None:
|
|
result["audio_sample_rate"] = int(audio_sample_rate)
|
|
return result
|
|
|
|
return post_process_func
|
|
|
|
|
|
def get_cosmos3_action_post_process_func(od_config: OmniDiffusionConfig):
|
|
"""Build the custom-output postprocessor for Cosmos3 action predictions.
|
|
|
|
Action modes return predicted action tensors in ``custom_output`` alongside
|
|
normal video output. RoboLab/OpenPI policy serving marks action-only output
|
|
and carries observation metadata used here to map model-space actions back
|
|
to the requested robot action representation.
|
|
"""
|
|
del od_config
|
|
|
|
def action_post_process_func(action: Any, custom_output: dict[str, Any] | None = None, sampling_params=None):
|
|
del sampling_params
|
|
inputs = custom_output.get("robolab_action_postprocess") if isinstance(custom_output, dict) else None
|
|
if isinstance(inputs, RoboLabActionPostprocessInputs):
|
|
processed_action = postprocess_robolab_action(action, inputs)
|
|
custom_output.pop("robolab_action_postprocess", None)
|
|
return processed_action
|
|
return action
|
|
|
|
return action_post_process_func
|
|
|
|
|
|
def get_cosmos3_ir_op_priority_func(od_config: OmniDiffusionConfig):
|
|
del od_config
|
|
|
|
def ir_op_priority_func(ir_op_priority, vllm_config=None):
|
|
del vllm_config
|
|
from vllm.config.kernel import IrOpPriorityConfig
|
|
|
|
priority_kwargs = {field.name: list(getattr(ir_op_priority, field.name)) for field in fields(ir_op_priority)}
|
|
priority_kwargs["rms_norm"] = ["native"]
|
|
priority_kwargs["fused_add_rms_norm"] = ["native"]
|
|
return IrOpPriorityConfig(**priority_kwargs)
|
|
|
|
return ir_op_priority_func
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pipeline
|
|
# ---------------------------------------------------------------------------
|
|
class Cosmos3OmniDiffusersPipeline(
|
|
nn.Module, CFGParallelMixin, SupportImageInput, ProgressBarMixin, DiffusionPipelineProfilerMixin
|
|
):
|
|
"""Cosmos3 text/image/video/sound/action pipeline.
|
|
|
|
Architecture: Mixture-of-Transformers with Qwen3-VL backbone.
|
|
- Understanding pathway: causal self-attention on text (runs once, K/V cached)
|
|
- Generation pathway: cross-attention on visual latents and optional
|
|
transfer-control, action, and sound latents (runs each step)
|
|
|
|
Supports T2V, I2V, V2V, T2I, transfer, sound-enabled video, and action
|
|
generation from the same class. Mode is selected at runtime:
|
|
|
|
* **T2I** when ``prompt["modalities"]`` contains ``"image"``. Latent
|
|
T-dim is forced to 1, T2I-specific scheduler defaults are applied (50 steps,
|
|
flow_shift=3.0, guidance_interval=[400, 1000]), the duration
|
|
template is suppressed, and post-process emits PIL images.
|
|
* **I2V** when the request supplies a preprocessed image via
|
|
``multi_modal_data['image']`` (handled by
|
|
:func:`get_cosmos3_pre_process_func`) and the requested output modality
|
|
is not image.
|
|
Frame 0 of the initial latent is set to the VAE-encoded conditioning
|
|
image, frame-0 noise predictions are masked to zero, and the clean
|
|
image latent is re-injected at frame 0 after each scheduler step.
|
|
* **V2V** when the request supplies a preprocessed video via
|
|
``multi_modal_data['video']`` without an action mode. Explicit latent
|
|
frame indexes are kept clean with ``noisy_frame_mask`` and re-injected
|
|
after each scheduler step.
|
|
* **Transfer** when ``edge``, ``blur``, ``depth``, ``seg``, or ``wsm`` hints
|
|
are supplied. Transfer is video-output only and cannot be combined with
|
|
sound or action generation.
|
|
* **Sound-enabled video** when ``generate_sound`` or ``sound_gen`` is true.
|
|
Sound is generated from sound latents, not from ``multi_modal_data['audio']``;
|
|
T2I, transfer, and action+sound are rejected.
|
|
* **Action generation** when ``action_mode`` is provided. ``policy`` and
|
|
``forward_dynamics`` require an image or video input; ``inverse_dynamics``
|
|
requires video input. Action predictions are returned in ``custom_output``.
|
|
RoboLab/OpenPI observations in ``extra_args['robot_obs']`` or
|
|
``extra_args['observation']`` return action-only custom output.
|
|
* **T2V** otherwise (default video generation).
|
|
"""
|
|
|
|
support_image_input: ClassVar[bool] = True
|
|
color_format: ClassVar[str] = "RGB"
|
|
|
|
@classmethod
|
|
def reference_video_decode_spec(
|
|
cls,
|
|
*,
|
|
num_frames: int | None = None,
|
|
extra_args: dict[str, Any] | None = None,
|
|
) -> ReferenceVideoDecodeSpec:
|
|
extra_args = extra_args if isinstance(extra_args, dict) else {}
|
|
if has_transfer_hints(extra_args):
|
|
max_frames = transfer_max_frames_from_extra_args(extra_args)
|
|
if num_frames is not None:
|
|
max_frames = min(max_frames, int(num_frames))
|
|
return ReferenceVideoDecodeSpec(max_frames=max_frames, keep="first")
|
|
|
|
action_mode = normalize_action_mode(extra_args.get("action_mode"))
|
|
if action_mode is not None:
|
|
if num_frames is not None:
|
|
return ReferenceVideoDecodeSpec(max_frames=int(num_frames), keep="first")
|
|
action_chunk_size = extra_args.get("action_chunk_size")
|
|
if action_chunk_size is not None:
|
|
try:
|
|
max_frames = int(action_chunk_size) + 1
|
|
except (TypeError, ValueError):
|
|
max_frames = None
|
|
if max_frames is not None and max_frames > 0:
|
|
return ReferenceVideoDecodeSpec(max_frames=max_frames, keep="first")
|
|
return ReferenceVideoDecodeSpec(max_frames=None, keep="first")
|
|
|
|
condition_indexes = normalize_condition_frame_indexes_vision(extra_args.get("condition_frame_indexes_vision"))
|
|
max_frames = condition_pixel_frame_count(condition_indexes)
|
|
if num_frames is not None:
|
|
max_frames = min(max_frames, int(num_frames))
|
|
keep = normalize_condition_video_keep(extra_args.get("condition_video_keep"))
|
|
return ReferenceVideoDecodeSpec(max_frames=max_frames, keep=keep)
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
od_config: OmniDiffusionConfig,
|
|
prefix: str = "",
|
|
) -> None:
|
|
super().__init__()
|
|
if od_config.enable_cpu_offload:
|
|
raise ValueError(
|
|
"Cosmos3 has no separate text encoder, so CPU offloading "
|
|
"(transformer↔encoder swapping) is not supported. "
|
|
"Use --enable-layerwise-offload instead."
|
|
)
|
|
self.od_config = od_config
|
|
self.device = get_local_device()
|
|
self.dtype = od_config.dtype
|
|
|
|
model_path = od_config.model
|
|
local_files_only = os.path.exists(model_path)
|
|
|
|
# --- Tokenizer ---
|
|
self.tokenizer = AutoTokenizer.from_pretrained(
|
|
model_path,
|
|
subfolder="text_tokenizer",
|
|
local_files_only=local_files_only,
|
|
)
|
|
|
|
# --- VAE ---
|
|
self.vae = DistributedAutoencoderKLWan.from_pretrained(
|
|
model_path,
|
|
subfolder="vae",
|
|
torch_dtype=self.dtype,
|
|
local_files_only=local_files_only,
|
|
).to(self.device)
|
|
|
|
if not hasattr(self.vae.config, "scale_factor_temporal"):
|
|
raise ValueError(
|
|
"Cosmos3 Diffusers VAE config must define scale_factor_temporal "
|
|
"so transformer mRoPE temporal positions can be computed correctly."
|
|
)
|
|
self.vae_scale_factor_temporal = int(self.vae.config.scale_factor_temporal)
|
|
self.vae_scale_factor_spatial = getattr(self.vae.config, "scale_factor_spatial", 16)
|
|
|
|
sound_gen = resolve_sound_gen(od_config)
|
|
sound_dim = None
|
|
sound_latent_fps = None
|
|
self._sound_tokenizer = None
|
|
if sound_gen:
|
|
self._sound_tokenizer = self._get_sound_tokenizer()
|
|
sound_dim = self._sound_tokenizer.latent_ch
|
|
sound_latent_fps = self._sound_tokenizer.latent_fps
|
|
|
|
# --- Transformer (weights loaded later via weights_sources) ---
|
|
self.transformer = Cosmos3VFMTransformer(
|
|
od_config=od_config,
|
|
temporal_compression_factor=self.vae_scale_factor_temporal,
|
|
sound_gen=sound_gen,
|
|
sound_dim=sound_dim,
|
|
sound_latent_fps=sound_latent_fps,
|
|
)
|
|
|
|
# --- Scheduler ---
|
|
# Load from checkpoint to preserve solver_order, timestep_spacing,
|
|
# beta_schedule, sigma bounds, flow_shift, etc. Only override
|
|
# flow_shift when explicitly requested by the user.
|
|
self.scheduler = UniPCMultistepScheduler.from_pretrained(
|
|
model_path,
|
|
subfolder="scheduler",
|
|
local_files_only=local_files_only,
|
|
)
|
|
if od_config.flow_shift is not None:
|
|
self.scheduler = UniPCMultistepScheduler.from_config(
|
|
self.scheduler.config,
|
|
flow_shift=od_config.flow_shift,
|
|
)
|
|
self._cpu_scheduler_state()
|
|
|
|
# --- Video processor for post-decode ---
|
|
self.video_processor = VideoProcessor(vae_scale_factor=self.vae_scale_factor_spatial)
|
|
|
|
# --- Weight sources for DiffusersPipelineLoader ---
|
|
self.weights_sources = [
|
|
DiffusersPipelineLoader.ComponentSource(
|
|
model_or_path=model_path,
|
|
subfolder=None,
|
|
revision=None,
|
|
prefix="transformer.",
|
|
fall_back_to_pt=True,
|
|
allow_patterns_overrides=["transformer/*.safetensors"],
|
|
),
|
|
]
|
|
|
|
# Snapshot the loaded scheduler config so we can rebuild the
|
|
# scheduler at request time when a per-request flow_shift override
|
|
# is supplied (T2I uses shift=3.0; T2V/I2V use the engine default).
|
|
self._base_scheduler_config = self.scheduler.config
|
|
self._engine_init_flow_shift = float(getattr(self.scheduler.config, "flow_shift", 1.0) or 1.0)
|
|
self._current_flow_shift = self._engine_init_flow_shift
|
|
self._base_scheduler_use_karras_sigmas = self._scheduler_use_karras_sigmas(self.scheduler.config)
|
|
self._current_scheduler_use_karras_sigmas = self._base_scheduler_use_karras_sigmas
|
|
|
|
self._guidance_scale = None
|
|
self._num_timesteps = None
|
|
self._cosmos3_branch_caches: dict[str, tuple[Any, Any]] | None = None
|
|
self._robolab_transform = None
|
|
|
|
# Set True by ``enable_cache_for_cosmos3`` when cache-dit is enabled on
|
|
# this pipeline. Tells the sequential-CFG loop to keep paired
|
|
# cond/uncond forwards so cache-dit's has_separate_cfg step accounting
|
|
# stays in sync.
|
|
self._cache_dit_requires_paired_cfg = False
|
|
|
|
self.setup_diffusion_pipeline_profiler(
|
|
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler
|
|
)
|
|
|
|
# -- Weight loading --------------------------------------------------------
|
|
|
|
@staticmethod
|
|
def _remap_ckpt_key(key: str) -> str | None:
|
|
"""Remap a Diffusers transformer key to the model parameter namespace.
|
|
|
|
Checkpoint keys arrive with a synthetic ``transformer.`` prefix from
|
|
``weights_sources``. The source checkpoint itself uses the prefixless
|
|
Diffusers transformer namespace: top-level projections plus Qwen3-VL
|
|
backbone keys. UND and GEN components share each layer in the source
|
|
and are split into separate module lists here. Some sources wrap the
|
|
transformer namespace under ``model.``; that wrapper is structural and
|
|
is stripped before applying the Cosmos3 leaf-name remap.
|
|
|
|
Returns the remapped name under ``transformer.``, or None to skip.
|
|
"""
|
|
k = key
|
|
# Strip the weights_sources prefix
|
|
if k.startswith("transformer."):
|
|
k = k[len("transformer.") :]
|
|
if k.startswith("model."):
|
|
k = k[len("model.") :]
|
|
|
|
# Top-level generation components.
|
|
if k.startswith(
|
|
(
|
|
"proj_in.",
|
|
"proj_out.",
|
|
"time_embedder.",
|
|
"audio_proj_in.",
|
|
"audio_proj_out.",
|
|
"action_proj_in.",
|
|
"action_proj_out.",
|
|
)
|
|
):
|
|
return f"transformer.{k}"
|
|
if k in ("audio_modality_embed", "audio_modality_embed.weight"):
|
|
return "transformer.audio_modality_embed"
|
|
if k in ("action_modality_embed", "action_modality_embed.weight"):
|
|
return "transformer.action_modality_embed"
|
|
if k.startswith("action_pos_embed."):
|
|
return None
|
|
|
|
# Skip lm_head
|
|
if k.startswith("lm_head."):
|
|
return None
|
|
|
|
# embed_tokens / norm -> language_model.*
|
|
if k.startswith("embed_tokens."):
|
|
return f"transformer.language_model.{k}"
|
|
if k.startswith("norm."):
|
|
return f"transformer.language_model.{k}"
|
|
|
|
# norm_moe_gen -> top level
|
|
if k.startswith("norm_moe_gen."):
|
|
return f"transformer.{k}"
|
|
|
|
if not k.startswith("layers."):
|
|
return None
|
|
|
|
parts = k.split(".", 2) # ['layers', '{i}', '{rest}']
|
|
if len(parts) != 3:
|
|
return None
|
|
layer_idx = parts[1]
|
|
rest = parts[2]
|
|
|
|
und_lp = f"transformer.language_model.layers.{layer_idx}"
|
|
gen_lp = f"transformer.gen_layers.{layer_idx}"
|
|
|
|
_LAYER_MAP = {
|
|
# UND attention
|
|
"self_attn.to_q.": f"{und_lp}.self_attn.to_q.",
|
|
"self_attn.to_k.": f"{und_lp}.self_attn.to_k.",
|
|
"self_attn.to_v.": f"{und_lp}.self_attn.to_v.",
|
|
"self_attn.to_out.": f"{und_lp}.self_attn.to_out.",
|
|
"self_attn.norm_q.": f"{und_lp}.self_attn.norm_q.",
|
|
"self_attn.norm_k.": f"{und_lp}.self_attn.norm_k.",
|
|
# GEN attention
|
|
"self_attn.add_q_proj.": f"{gen_lp}.cross_attention.to_q.",
|
|
"self_attn.add_k_proj.": f"{gen_lp}.cross_attention.to_k.",
|
|
"self_attn.add_v_proj.": f"{gen_lp}.cross_attention.to_v.",
|
|
"self_attn.to_add_out.": f"{gen_lp}.cross_attention.to_out.",
|
|
"self_attn.norm_added_q.": f"{gen_lp}.cross_attention.norm_q.",
|
|
"self_attn.norm_added_k.": f"{gen_lp}.cross_attention.norm_k.",
|
|
# Norms
|
|
"input_layernorm.": f"{und_lp}.input_layernorm.",
|
|
"post_attention_layernorm.": f"{und_lp}.post_attention_layernorm.",
|
|
"input_layernorm_moe_gen.": f"{gen_lp}.input_layernorm.",
|
|
"post_attention_layernorm_moe_gen.": f"{gen_lp}.post_attention_layernorm.",
|
|
# UND MLP
|
|
"mlp.gate_proj.": f"{und_lp}.mlp.gate_proj.",
|
|
"mlp.up_proj.": f"{und_lp}.mlp.up_proj.",
|
|
"mlp.down_proj.": f"{und_lp}.mlp.down_proj.",
|
|
# GEN MLP
|
|
"mlp_moe_gen.gate_proj.": f"{gen_lp}.mlp.gate_proj.",
|
|
"mlp_moe_gen.up_proj.": f"{gen_lp}.mlp.up_proj.",
|
|
"mlp_moe_gen.down_proj.": f"{gen_lp}.mlp.down_proj.",
|
|
}
|
|
|
|
for pattern, replacement in _LAYER_MAP.items():
|
|
if rest.startswith(pattern):
|
|
suffix = rest[len(pattern) :]
|
|
return replacement + suffix
|
|
|
|
return None
|
|
|
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
|
"""Stream-remap checkpoint weights and load via AutoWeightsLoader.
|
|
|
|
Handles quantization, TP-aware weight_loader, and buffer loading.
|
|
Returns the set of loaded parameter names for strict validation.
|
|
"""
|
|
state = self.state_dict()
|
|
allowed = set(state.keys())
|
|
tp_aware = {n for n, p in self.named_parameters() if hasattr(p, "weight_loader")}
|
|
|
|
def _remapped_weights() -> Iterable[tuple[str, torch.Tensor]]:
|
|
total = kept = 0
|
|
for name, tensor in weights:
|
|
total += 1
|
|
remapped = self._remap_ckpt_key(name)
|
|
if remapped is not None and (remapped in allowed or remapped in tp_aware):
|
|
kept += 1
|
|
yield remapped, tensor
|
|
if _is_rank_zero():
|
|
logger.info(
|
|
"Cosmos3 weight remap: kept %d/%d tensors",
|
|
kept,
|
|
total,
|
|
)
|
|
|
|
loader = AutoWeightsLoader(self)
|
|
loaded = loader.load_weights(_remapped_weights())
|
|
self.transformer.post_load_weights()
|
|
self.transformer.eval()
|
|
if getattr(self.transformer, "sound_gen", False):
|
|
sound_markers = ("audio_proj_in.", "audio_proj_out.", "audio_modality_embed")
|
|
missing = [marker.rstrip(".") for marker in sound_markers if not any(marker in name for name in loaded)]
|
|
if missing:
|
|
raise ValueError(
|
|
"Cosmos3 transformer config enables sound generation, but "
|
|
f"the checkpoint is missing sound weights for {missing}. "
|
|
"Use a sound-capable transformer checkpoint."
|
|
)
|
|
if getattr(self.transformer, "action_gen", False):
|
|
action_markers = ("action_proj_in.", "action_proj_out.", "action_modality_embed")
|
|
missing = [marker.rstrip(".") for marker in action_markers if not any(marker in name for name in loaded)]
|
|
if missing:
|
|
raise ValueError(
|
|
"Cosmos3 transformer config enables action generation, but "
|
|
f"the checkpoint is missing action weights for {missing}. "
|
|
"Use an action-capable transformer checkpoint."
|
|
)
|
|
return loaded
|
|
|
|
def predict_noise(self, **kwargs) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
|
"""Override CFGParallelMixin.predict_noise for Cosmos3.
|
|
|
|
The transformer returns the raw prediction: video-only as a tensor,
|
|
or a tuple in video, action, sound order for multimodal generation.
|
|
"""
|
|
cache_key = kwargs.pop("_cosmos3_cache_key", None)
|
|
if cache_key is None:
|
|
return self.transformer(**kwargs)
|
|
|
|
branch_caches = self._cosmos3_branch_caches
|
|
if branch_caches is None:
|
|
return self.transformer(**kwargs)
|
|
|
|
cache_key = str(cache_key)
|
|
self.transformer.cached_kv, self.transformer.cached_freqs_gen = branch_caches.get(cache_key, (None, None))
|
|
prediction = self.transformer(**kwargs)
|
|
branch_caches[cache_key] = (self.transformer.cached_kv, self.transformer.cached_freqs_gen)
|
|
return prediction
|
|
|
|
def combine_multi_branch_cfg_noise(
|
|
self,
|
|
predictions: list[torch.Tensor | tuple[torch.Tensor, ...]],
|
|
true_cfg_scale: float | dict[str, float],
|
|
cfg_normalize: bool = False,
|
|
) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
|
if not isinstance(true_cfg_scale, dict) or true_cfg_scale.get("mode") != "cosmos3_transfer":
|
|
return super().combine_multi_branch_cfg_noise(predictions, true_cfg_scale, cfg_normalize)
|
|
mode = str(true_cfg_scale.get("branch_mode", "control_and_text"))
|
|
guidance_scale = float(true_cfg_scale.get("guidance_scale", 1.0))
|
|
control_guidance = float(true_cfg_scale.get("control_guidance", 1.0))
|
|
|
|
if mode == "control_only":
|
|
if len(predictions) != 2:
|
|
raise ValueError(f"Cosmos3 transfer control-only CFG expects 2 branches, got {len(predictions)}.")
|
|
cond_full, cond_no_control = predictions
|
|
if isinstance(cond_full, tuple) or isinstance(cond_no_control, tuple):
|
|
raise ValueError("Cosmos3 transfer control-only CFG expects video-only tensor predictions.")
|
|
cfg_reference = cond_full
|
|
combined = cond_no_control + control_guidance * (cond_full - cond_no_control)
|
|
elif mode == "text_only":
|
|
if len(predictions) != 2:
|
|
raise ValueError(f"Cosmos3 transfer text-only CFG expects 2 branches, got {len(predictions)}.")
|
|
cond_full, uncond_full = predictions
|
|
if isinstance(cond_full, tuple) or isinstance(uncond_full, tuple):
|
|
raise ValueError("Cosmos3 transfer text-only CFG expects video-only tensor predictions.")
|
|
cfg_reference = cond_full
|
|
combined = uncond_full + guidance_scale * (cond_full - uncond_full)
|
|
elif mode == "control_and_text":
|
|
if len(predictions) != 3:
|
|
raise ValueError(f"Cosmos3 transfer control+text CFG expects 3 branches, got {len(predictions)}.")
|
|
cond_full, cond_no_control, uncond_full = predictions
|
|
if isinstance(cond_full, tuple) or isinstance(cond_no_control, tuple) or isinstance(uncond_full, tuple):
|
|
raise ValueError("Cosmos3 transfer control+text CFG expects video-only tensor predictions.")
|
|
cfg_reference = cond_full
|
|
control_cond = cond_no_control + control_guidance * (cond_full - cond_no_control)
|
|
combined = uncond_full + guidance_scale * (control_cond - uncond_full)
|
|
else:
|
|
raise ValueError(f"Unknown Cosmos3 transfer CFG branch_mode={mode!r}.")
|
|
|
|
if cfg_normalize:
|
|
combined = self.cfg_normalize_function(cfg_reference, combined)
|
|
return combined
|
|
|
|
@staticmethod
|
|
def _cfg_parallel_active() -> bool:
|
|
try:
|
|
return get_classifier_free_guidance_world_size() > 1
|
|
except Exception:
|
|
return False
|
|
|
|
def _cache_requires_paired_cfg(self) -> bool:
|
|
"""Whether the sequential-CFG denoising loop must keep paired forwards.
|
|
|
|
cache-dit wraps the GEN pathway with ``has_separate_cfg=True`` and
|
|
distinguishes the conditional vs unconditional passes purely by the
|
|
parity of its transformer-forward counter. The T2I ``guidance_interval``
|
|
optimization that skips the uncond pass outside the interval would
|
|
desync that accounting (cond passes get mislabeled as uncond and the
|
|
per-generation step counter drifts). ``enable_cache_for_cosmos3`` sets
|
|
the marker below when it enables cache-dit on this pipeline; the loop
|
|
then keeps both passes and neutralizes CFG via scale=1.0 instead.
|
|
|
|
Returns False when cache-dit is not active, preserving the skip speedup.
|
|
"""
|
|
return self._cache_dit_requires_paired_cfg
|
|
|
|
@staticmethod
|
|
def _get_sp_param(sp: OmniDiffusionSamplingParams, key: str, default: Any = None) -> Any:
|
|
"""Read a runtime control from sampling params.
|
|
|
|
Order of precedence:
|
|
1. ``sp.extra_args[key]`` - preferred path; the OpenAI image/video
|
|
endpoints surface custom controls here (see e.g.
|
|
``serving_video.py`` writing ``extra_args['flow_shift']``).
|
|
2. direct attribute on ``sp`` - backward compat for callers that
|
|
set attributes directly.
|
|
3. ``default``.
|
|
|
|
Skipping this helper would cause API-driven overrides like
|
|
``request.flow_shift`` (forwarded as ``extra_args['flow_shift']``) to
|
|
be silently ignored.
|
|
"""
|
|
extra = sp.extra_args or {}
|
|
if extra.get(key) is not None:
|
|
return extra[key]
|
|
val = getattr(sp, key, None)
|
|
if val is not None:
|
|
return val
|
|
return default
|
|
|
|
def _get_robolab_transform(self):
|
|
if self._robolab_transform is None:
|
|
action_dim = int(getattr(self.transformer, "action_dim", 64))
|
|
self._robolab_transform = lazy_action_transform_pipeline(action_dim)
|
|
return self._robolab_transform
|
|
|
|
def _build_robolab_policy_inputs(
|
|
self,
|
|
sp: OmniDiffusionSamplingParams,
|
|
prompt_data: Any | None = None,
|
|
request_id: str | None = None,
|
|
) -> RoboLabPolicyInputs | None:
|
|
extra = sp.extra_args if isinstance(sp.extra_args, dict) else {}
|
|
obs = extra.get("robot_obs")
|
|
if obs is None:
|
|
obs = extra.get("observation")
|
|
if obs is None:
|
|
return None
|
|
if not isinstance(obs, dict):
|
|
raise TypeError(f"Cosmos3 RoboLab observation must be a dict, got {type(obs)!r}.")
|
|
|
|
prompt = obs.get("prompt")
|
|
if not isinstance(prompt, str):
|
|
raise ValueError("RoboLab observation must contain string key 'prompt'.")
|
|
|
|
def extra_param(key: str, default: Any) -> Any:
|
|
value = extra.get(key)
|
|
return default if value is None else value
|
|
|
|
def extra_param_alias(primary_key: str, alias_key: str, default: Any) -> Any:
|
|
value = extra.get(primary_key)
|
|
if value is not None:
|
|
return value
|
|
value = extra.get(alias_key)
|
|
return default if value is None else value
|
|
|
|
action_space = normalize_robolab_action_space(extra_param("action_space", ROBOLAB_DEFAULT_ACTION_SPACE))
|
|
action_chunk_size = int(extra_param("action_chunk_size", ROBOLAB_DEFAULT_ACTION_CHUNK_SIZE))
|
|
raw_action_dim_default = (
|
|
ROBOLAB_DEFAULT_RAW_ACTION_DIM if action_space == "joint_pos" else ROBOLAB_MIDTRAIN_RAW_ACTION_DIM
|
|
)
|
|
raw_action_dim = int(extra_param("raw_action_dim", raw_action_dim_default))
|
|
image_h = int(extra_param("image_height", ROBOLAB_DEFAULT_IMAGE_HEIGHT))
|
|
image_w = int(extra_param("image_width", ROBOLAB_DEFAULT_IMAGE_WIDTH))
|
|
history_length = int(extra_param("history_length", 1))
|
|
use_state = self._truthy(extra_param("use_state", True))
|
|
resolution = str(extra_param("resolution", ROBOLAB_DEFAULT_RESOLUTION))
|
|
fps = float(extra_param("conditioning_fps", ROBOLAB_DEFAULT_CONDITIONING_FPS))
|
|
domain_name = str(extra_param("domain_name", ROBOLAB_DEFAULT_DOMAIN_NAME))
|
|
domain_id = resolve_domain_id(domain_name=domain_name, require_explicit=True)
|
|
|
|
if use_state and history_length < 1:
|
|
raise ValueError("RoboLab history_length must be >= 1 when use_state is true.")
|
|
if action_chunk_size <= 0:
|
|
raise ValueError(f"RoboLab action_chunk_size must be positive, got {action_chunk_size}.")
|
|
if raw_action_dim <= 0:
|
|
raise ValueError(f"RoboLab raw_action_dim must be positive, got {raw_action_dim}.")
|
|
|
|
try:
|
|
image = extract_robolab_image(obs)
|
|
except ValueError as exc:
|
|
image = extract_robolab_prompt_image(prompt_data)
|
|
if image is None:
|
|
raise exc
|
|
if image.shape[:2] != (image_h, image_w):
|
|
image = resize_rgb_uint8(image, (image_h, image_w))
|
|
|
|
t_frames = action_chunk_size + 1
|
|
video = torch.zeros((3, t_frames, image_h, image_w), dtype=torch.uint8)
|
|
video[:, 0] = torch.from_numpy(image.copy()).permute(2, 0, 1)
|
|
|
|
use_state_rows = 1 if use_state else 0
|
|
action = torch.zeros((action_chunk_size + use_state_rows, raw_action_dim), dtype=torch.float32)
|
|
history_action = None
|
|
num_history_rows = history_length - use_state_rows
|
|
gripper_position = 1.0 - ensure_gripper_array(obs["observation/gripper_position"])
|
|
|
|
if action_space == "joint_pos":
|
|
joint_position = ensure_2d_float_array(obs["observation/joint_position"], "observation/joint_position", 7)
|
|
if use_state:
|
|
action[0] = torch.from_numpy(np.concatenate((joint_position[-1], gripper_position[-1])))
|
|
if num_history_rows > 0:
|
|
if len(joint_position) < num_history_rows + 1:
|
|
raise ValueError("Not enough joint_position rows for requested history_length.")
|
|
history_np = np.concatenate(
|
|
(joint_position[-num_history_rows - 1 : -1], gripper_position[-num_history_rows - 1 : -1]),
|
|
axis=-1,
|
|
)
|
|
history_action = torch.from_numpy(history_np).float()
|
|
else:
|
|
eef_pos = ensure_2d_float_array(obs["observation/eef_pos"], "observation/eef_pos", 3)
|
|
eef_quat = ensure_2d_float_array(obs["observation/eef_quat"], "observation/eef_quat", 4)
|
|
if use_state:
|
|
rot6d = convert_midtrain_rotation(eef_quat[-1], "quat_xyzw", "rot6d")
|
|
action[0] = torch.from_numpy(np.concatenate((eef_pos[-1], rot6d, gripper_position[-1])))
|
|
if num_history_rows > 0:
|
|
if len(eef_pos) < num_history_rows + 1 or len(eef_quat) < num_history_rows + 1:
|
|
raise ValueError("Not enough eef_pos/eef_quat rows for requested history_length.")
|
|
poses_abs = build_abs_pose_from_components(eef_pos, eef_quat, "quat_xyzw")
|
|
poses_rel = pose_abs_to_rel(poses_abs, rotation_format="rot6d", pose_convention="backward_framewise")
|
|
history_np = np.concatenate(
|
|
[poses_rel[-num_history_rows:], gripper_position[-num_history_rows:]],
|
|
axis=-1,
|
|
)
|
|
history_action = torch.from_numpy(history_np).float()
|
|
|
|
sample: dict[str, Any] = {
|
|
"ai_caption": prompt,
|
|
"video": video,
|
|
"action": action,
|
|
# Cosmos Framework consumes this as an integer conditioning bucket.
|
|
"conditioning_fps": torch.tensor(fps, dtype=torch.long),
|
|
"mode": ACTION_MODE_POLICY,
|
|
"domain_id": torch.tensor(domain_id, dtype=torch.long),
|
|
"viewpoint": "concat_view",
|
|
"additional_view_description": ROBOLAB_CONCAT_VIEW_DESCRIPTION,
|
|
}
|
|
if history_action is not None:
|
|
sample["history_action"] = history_action
|
|
|
|
sample = self._get_robolab_transform()(sample, resolution)
|
|
sequence_plan = sample["sequence_plan"]
|
|
video_tensor = sample["video"].float() / 127.5 - 1.0
|
|
raw_action_dim_tensor = sample.get("raw_action_dim")
|
|
if isinstance(raw_action_dim_tensor, torch.Tensor):
|
|
transformed_raw_action_dim = int(raw_action_dim_tensor.item())
|
|
else:
|
|
transformed_raw_action_dim = raw_action_dim
|
|
|
|
return RoboLabPolicyInputs(
|
|
prompt=sample["ai_caption"],
|
|
video_tensor=video_tensor.unsqueeze(0),
|
|
action_tensor=sample["action"].float(),
|
|
action_condition_indexes=list(getattr(sequence_plan, "condition_frame_indexes_action", []) or []),
|
|
action_start_frame_offset=int(getattr(sequence_plan, "action_start_frame_offset", 1)),
|
|
raw_action_dim=transformed_raw_action_dim,
|
|
domain_id=domain_id,
|
|
fps=fps,
|
|
height=int(video_tensor.shape[-2]),
|
|
width=int(video_tensor.shape[-1]),
|
|
image_size=sample.get("image_size"),
|
|
num_frames=int(video_tensor.shape[1]),
|
|
num_inference_steps=int(
|
|
extra_param_alias("num_inference_steps", "num_steps", ROBOLAB_DEFAULT_NUM_INFERENCE_STEPS)
|
|
),
|
|
guidance_scale=float(extra_param_alias("guidance_scale", "guidance", ROBOLAB_DEFAULT_GUIDANCE_SCALE)),
|
|
flow_shift=float(extra_param_alias("flow_shift", "shift", ROBOLAB_DEFAULT_FLOW_SHIFT)),
|
|
seed=next_robolab_seed(extra, obs, request_id),
|
|
history_length=history_length,
|
|
action_space=action_space,
|
|
observation=obs,
|
|
)
|
|
|
|
@staticmethod
|
|
def _build_action_condition_mask_from_indexes(
|
|
indexes: list[int],
|
|
action_length: int,
|
|
*,
|
|
device: torch.device,
|
|
dtype: torch.dtype,
|
|
) -> torch.Tensor:
|
|
mask = torch.zeros(1, action_length, 1, device=device, dtype=dtype)
|
|
for idx in indexes:
|
|
if idx < 0 or idx >= action_length:
|
|
raise ValueError(f"Action condition index {idx} is out of range for action length {action_length}.")
|
|
mask[:, idx, :] = 1.0
|
|
return mask
|
|
|
|
def _forward_robolab_policy(
|
|
self,
|
|
sp: OmniDiffusionSamplingParams,
|
|
inputs: RoboLabPolicyInputs,
|
|
pipeline_start: float,
|
|
) -> DiffusionOutput:
|
|
if not getattr(self.transformer, "action_gen", False):
|
|
raise ValueError(
|
|
"Cosmos3 RoboLab policy serving was requested, but the transformer "
|
|
"was initialized without action modules. Check that the checkpoint "
|
|
"config enables action_gen and includes action weights."
|
|
)
|
|
|
|
action_mode = ACTION_MODE_POLICY
|
|
height = inputs.height
|
|
width = inputs.width
|
|
num_frames = inputs.num_frames
|
|
action_chunk_size = int(inputs.action_tensor.shape[0])
|
|
num_inference_steps = inputs.num_inference_steps
|
|
guidance_scale = float(inputs.guidance_scale)
|
|
flow_shift_target = float(inputs.flow_shift)
|
|
domain_id = int(inputs.domain_id)
|
|
frame_rate = self._get_sp_param(sp, "resolved_frame_rate") or self._get_sp_param(sp, "frame_rate") or inputs.fps
|
|
max_sequence_length = (
|
|
self._get_sp_param(sp, "max_sequence_length", COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH)
|
|
or COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH
|
|
)
|
|
use_system_prompt = bool(self._get_sp_param(sp, "use_system_prompt", False))
|
|
|
|
self._guidance_scale = guidance_scale
|
|
self._num_timesteps = num_inference_steps
|
|
|
|
generator = sp.generator
|
|
if generator is None:
|
|
generator = torch.Generator(device=self.device).manual_seed(int(inputs.seed))
|
|
|
|
cond_ids, cond_mask, uncond_ids, uncond_mask = self._format_and_tokenize_prompts(
|
|
inputs.prompt,
|
|
"",
|
|
num_frames,
|
|
frame_rate,
|
|
height,
|
|
width,
|
|
max_sequence_length,
|
|
sp,
|
|
use_system_prompt,
|
|
is_t2i=False,
|
|
)
|
|
|
|
action_video_tensor = inputs.video_tensor
|
|
if action_video_tensor.ndim == 4:
|
|
action_video_tensor = action_video_tensor.unsqueeze(0)
|
|
if action_video_tensor.ndim != 5:
|
|
raise ValueError(
|
|
"Cosmos3 RoboLab action video tensor must have shape [1, 3, T, H, W] "
|
|
f"or [3, T, H, W], got {tuple(action_video_tensor.shape)}."
|
|
)
|
|
if action_video_tensor.shape[2] < num_frames:
|
|
pad = action_video_tensor[:, :, -1:].repeat(1, 1, num_frames - action_video_tensor.shape[2], 1, 1)
|
|
action_video_tensor = torch.cat([action_video_tensor, pad], dim=2)
|
|
elif action_video_tensor.shape[2] > num_frames:
|
|
action_video_tensor = action_video_tensor[:, :, :num_frames]
|
|
|
|
action_latents, action_velocity_mask, action_condition_latents, raw_action_dim = self._prepare_action_latents(
|
|
mode=action_mode,
|
|
action_chunk_size=action_chunk_size,
|
|
raw_action_dim=int(inputs.raw_action_dim),
|
|
generator=generator,
|
|
sp=sp,
|
|
clean_action=inputs.action_tensor,
|
|
condition_indexes=inputs.action_condition_indexes,
|
|
)
|
|
action_offset = int(inputs.action_start_frame_offset)
|
|
|
|
latents, velocity_mask, condition_latents = self._prepare_latents_action_video(
|
|
action_video_tensor,
|
|
action_mode,
|
|
height,
|
|
width,
|
|
num_frames,
|
|
generator,
|
|
image_size=inputs.image_size,
|
|
)
|
|
image_latent = condition_latents[:, :, 0:1]
|
|
|
|
video_shape = (latents.shape[2], latents.shape[3], latents.shape[4])
|
|
shared_kwargs = dict(
|
|
video_shape=video_shape,
|
|
fps=frame_rate,
|
|
noisy_frame_mask=velocity_mask,
|
|
action_domain_ids=torch.tensor([domain_id], dtype=torch.long, device=self.device),
|
|
action_noisy_mask=action_velocity_mask,
|
|
action_start_frame_offset=action_offset,
|
|
action_fps=float(self._get_sp_param(sp, "action_fps", frame_rate) or frame_rate),
|
|
)
|
|
|
|
scheduler = build_robolab_unipc_scheduler(num_inference_steps, flow_shift_target, self.device)
|
|
_, action_latents = self.diffuse(
|
|
latents=latents,
|
|
timesteps=scheduler.timesteps,
|
|
cond_ids=cond_ids,
|
|
cond_mask=cond_mask,
|
|
uncond_ids=uncond_ids,
|
|
uncond_mask=uncond_mask,
|
|
guidance_scale=guidance_scale,
|
|
shared_kwargs=shared_kwargs,
|
|
action_latents=action_latents,
|
|
action_velocity_mask=action_velocity_mask,
|
|
action_condition_latents=action_condition_latents,
|
|
sound_latents=None,
|
|
velocity_mask=velocity_mask,
|
|
image_latent=image_latent,
|
|
condition_latents=condition_latents,
|
|
guidance_interval=None,
|
|
raw_action_dim=raw_action_dim,
|
|
scheduler=scheduler,
|
|
)
|
|
|
|
if _is_rank_zero():
|
|
logger.info("Total pipeline time: %.2fs", time.time() - pipeline_start)
|
|
|
|
action = action_latents[:, :, :raw_action_dim].detach().cpu()
|
|
custom_action_output: dict[str, Any] = {
|
|
"action": action,
|
|
"raw_action_dim": raw_action_dim,
|
|
"action_mode": action_mode,
|
|
"domain_id": domain_id,
|
|
"action_only_output": True,
|
|
"robolab_action_postprocess": make_robolab_action_postprocess_inputs(inputs),
|
|
}
|
|
return DiffusionOutput(output={}, custom_output=custom_action_output)
|
|
|
|
@staticmethod
|
|
def _truthy(value) -> bool:
|
|
if isinstance(value, str):
|
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
return bool(value)
|
|
|
|
@classmethod
|
|
def _get_prompt_param(cls, prompt_data, key: str, default=None):
|
|
if not isinstance(prompt_data, dict):
|
|
return default
|
|
if prompt_data.get(key) is not None:
|
|
return prompt_data[key]
|
|
additional = prompt_data.get("additional_information")
|
|
if isinstance(additional, dict) and additional.get(key) is not None:
|
|
return additional[key]
|
|
return default
|
|
|
|
@classmethod
|
|
def _is_sound_request(cls, prompt_data, sp) -> bool:
|
|
for key in ("generate_sound", "sound_gen"):
|
|
if cls._truthy(cls._get_prompt_param(prompt_data, key, None)):
|
|
return True
|
|
if cls._truthy(cls._get_sp_param(sp, key, None)):
|
|
return True
|
|
return False
|
|
|
|
@classmethod
|
|
def _get_action_mode(cls, prompt_data, sp) -> str | None:
|
|
return normalize_action_mode(
|
|
cls._get_sp_param(sp, "action_mode", cls._get_prompt_param(prompt_data, "action_mode", None))
|
|
)
|
|
|
|
def _get_sound_tokenizer(self):
|
|
if self._sound_tokenizer is None:
|
|
from .sound_tokenizer import Cosmos3SoundTokenizer
|
|
|
|
self._sound_tokenizer = Cosmos3SoundTokenizer.from_config(self.od_config)
|
|
return self._sound_tokenizer
|
|
|
|
@staticmethod
|
|
def _is_t2i_request(req: DiffusionRequestBatch) -> bool:
|
|
"""Return whether request-level modalities select image output.
|
|
|
|
Only ``"image"`` switches Cosmos3 into T2I. ``"video"`` and omitted
|
|
modalities select video output. ``"text"`` and ``"audio"`` are accepted
|
|
compatibility values for callers that share prompt schemas, but they do
|
|
not select text or audio output in this pipeline. ``"image"`` and
|
|
``"video"`` cannot be requested together.
|
|
"""
|
|
if not req.prompts:
|
|
return False
|
|
first_prompt = req.prompts[0]
|
|
modalities = first_prompt.get("modalities", []) if isinstance(first_prompt, dict) else []
|
|
if modalities is None:
|
|
modalities = []
|
|
if isinstance(modalities, str):
|
|
modalities = [modalities]
|
|
if "image" in modalities and "video" in modalities:
|
|
raise ValueError("Cosmos3 prompt modalities cannot request both image and video output.")
|
|
|
|
accepted_modalities = ["image", "video", "text", "audio"]
|
|
if any(x not in accepted_modalities for x in modalities):
|
|
raise ValueError(f"Incorrect modality value in {modalities}, expected one of {accepted_modalities}.")
|
|
return "image" in modalities
|
|
|
|
@staticmethod
|
|
def _scheduler_use_karras_sigmas(config: Any) -> bool | None:
|
|
value = getattr(config, "use_karras_sigmas", None)
|
|
return None if value is None else bool(value)
|
|
|
|
def _set_flow_shift(self, target_shift: float, *, use_karras_sigmas: bool | None = None) -> None:
|
|
"""Set UniPC scheduler mode for a concrete request.
|
|
|
|
The scheduler is rebuilt from the saved base config if
|
|
the target differs from the current shift or Karras-sigma mode.
|
|
Tracking explicit scheduler state is required because the previous
|
|
mode may have rebuilt the scheduler - we cannot rely on
|
|
``self.scheduler.config`` reflecting the last requested target if a
|
|
rebuild was skipped via the equality check.
|
|
"""
|
|
target = float(target_shift)
|
|
target_use_karras_sigmas = (
|
|
self._base_scheduler_use_karras_sigmas if use_karras_sigmas is None else bool(use_karras_sigmas)
|
|
)
|
|
if (
|
|
target == float(self._current_flow_shift)
|
|
and target_use_karras_sigmas == self._current_scheduler_use_karras_sigmas
|
|
):
|
|
return
|
|
|
|
scheduler_kwargs: dict[str, Any] = {"flow_shift": target}
|
|
if use_karras_sigmas is not None:
|
|
scheduler_kwargs["use_karras_sigmas"] = bool(use_karras_sigmas)
|
|
self.scheduler = UniPCMultistepScheduler.from_config(self._base_scheduler_config, **scheduler_kwargs)
|
|
self._cpu_scheduler_state()
|
|
self._current_flow_shift = target
|
|
self._current_scheduler_use_karras_sigmas = self._scheduler_use_karras_sigmas(self.scheduler.config)
|
|
|
|
def _cpu_scheduler_state(self) -> None:
|
|
# We need to move scheduler tensors to CPU, as unipc from diffusers assumes they are on CPU.
|
|
# However, after the creation they are on GPU due to "with target_device:" in diffusers_loader.py
|
|
for name, value in vars(self.scheduler).items():
|
|
if isinstance(value, torch.Tensor) and value.device.type != "cpu":
|
|
setattr(self.scheduler, name, value.cpu())
|
|
|
|
@property
|
|
def guidance_scale(self):
|
|
return self._guidance_scale
|
|
|
|
@property
|
|
def do_classifier_free_guidance(self):
|
|
return self._guidance_scale is not None and self._guidance_scale > 1.0
|
|
|
|
@property
|
|
def num_timesteps(self):
|
|
return self._num_timesteps
|
|
|
|
# -- Prompt formatting -----------------------------------------------------
|
|
|
|
@staticmethod
|
|
def _apply_metadata_templates(
|
|
prompt: str,
|
|
num_frames: int,
|
|
frame_rate: float,
|
|
height: int,
|
|
width: int,
|
|
duration_template: str | None = COSMOS3_DURATION_TEMPLATE,
|
|
resolution_template: str | None = COSMOS3_RESOLUTION_TEMPLATE,
|
|
force_duration_template: bool = False,
|
|
) -> str:
|
|
"""
|
|
Append duration and resolution metadata to a prompt.
|
|
"""
|
|
parts: list[str] = []
|
|
head = prompt.rstrip(".").strip()
|
|
if head:
|
|
parts.append(head)
|
|
if duration_template is not None and (num_frames > 1 or force_duration_template):
|
|
duration = num_frames / frame_rate
|
|
parts.append(duration_template.format(duration=duration, fps=frame_rate).rstrip("."))
|
|
if resolution_template is not None:
|
|
parts.append(resolution_template.format(height=height, width=width).rstrip("."))
|
|
if not parts:
|
|
return ""
|
|
return ". ".join(parts) + "."
|
|
|
|
# -- Tokenization --------------------------------------------------------
|
|
|
|
def _tokenize_prompt(
|
|
self,
|
|
text: str,
|
|
max_sequence_length: int,
|
|
use_system_prompt: bool = False,
|
|
system_prompt: str | None = None,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Tokenize a prompt using the Qwen2 chat template.
|
|
|
|
Returns (input_ids, attention_mask) as [1, S] tensors on device.
|
|
"""
|
|
conversations = []
|
|
if use_system_prompt:
|
|
conversations.append(
|
|
{
|
|
"role": "system",
|
|
"content": system_prompt or COSMOS3_SYSTEM_PROMPT,
|
|
}
|
|
)
|
|
conversations.append({"role": "user", "content": text})
|
|
|
|
token_ids = self._normalize_token_ids(
|
|
self.tokenizer.apply_chat_template(conversations, tokenize=True, add_generation_prompt=True)
|
|
)
|
|
original_token_count = len(token_ids)
|
|
if original_token_count > max_sequence_length and _is_rank_zero():
|
|
logger.warning(
|
|
"Cosmos3 prompt token_ids shortened to max_sequence_length: "
|
|
"original_token_count=%d, max_sequence_length=%d, removed_token_count=%d",
|
|
original_token_count,
|
|
max_sequence_length,
|
|
original_token_count - max_sequence_length,
|
|
)
|
|
token_ids = token_ids[:max_sequence_length]
|
|
token_ids.append(self.tokenizer.eos_token_id) # 151645
|
|
token_ids.append(self.tokenizer.convert_tokens_to_ids("<|vision_start|>")) # 151652
|
|
seq_len = len(token_ids)
|
|
|
|
# No right-padding: the prompt is tokenized to its natural length.
|
|
# The UND pathway uses causal self-attention with no padding mask and
|
|
# the GEN cross-attention K/V is trimmed to the real text length, so
|
|
# padding to a fixed length only added dead compute and never changed
|
|
# the output. ``max_sequence_length`` is kept purely as a truncation
|
|
# cap (above). The mask is therefore all ones.
|
|
attention_mask = [1] * seq_len
|
|
|
|
input_ids = torch.tensor([token_ids], dtype=torch.long, device=self.device)
|
|
attention_mask = torch.tensor([attention_mask], dtype=torch.long, device=self.device)
|
|
return input_ids, attention_mask
|
|
|
|
@staticmethod
|
|
def _normalize_token_ids(tokenized_output: object) -> list[int]:
|
|
"""Normalize tokenizer outputs into a flat ``list[int]``.
|
|
|
|
Different Transformers/tokenizers versions can return ``list[int]``,
|
|
a mapping/BatchEncoding with ``input_ids``, tensors, or
|
|
``tokenizers.Encoding`` objects from ``apply_chat_template``.
|
|
"""
|
|
token_ids = tokenized_output
|
|
while True:
|
|
if isinstance(token_ids, dict) and "input_ids" in token_ids:
|
|
token_ids = token_ids["input_ids"]
|
|
elif hasattr(token_ids, "input_ids"):
|
|
token_ids = token_ids.input_ids
|
|
elif hasattr(token_ids, "ids"):
|
|
token_ids = token_ids.ids
|
|
elif hasattr(token_ids, "tolist"):
|
|
token_ids = token_ids.tolist()
|
|
elif isinstance(token_ids, tuple):
|
|
token_ids = list(token_ids)
|
|
elif isinstance(token_ids, list) and len(token_ids) == 1:
|
|
first = token_ids[0]
|
|
if isinstance(first, list | tuple):
|
|
token_ids = list(first)
|
|
elif hasattr(first, "ids") or hasattr(first, "input_ids"):
|
|
token_ids = first
|
|
elif hasattr(first, "tolist"):
|
|
first_list = first.tolist()
|
|
if isinstance(first_list, list | tuple):
|
|
token_ids = list(first_list)
|
|
else:
|
|
break
|
|
else:
|
|
break
|
|
else:
|
|
break
|
|
|
|
if not isinstance(token_ids, list):
|
|
raise TypeError(
|
|
"Cosmos3 tokenizer must return token IDs as a list-like value; "
|
|
f"got {type(token_ids).__name__}: {token_ids!r}"
|
|
)
|
|
|
|
normalized_ids = []
|
|
for idx, token_id in enumerate(token_ids):
|
|
if hasattr(token_id, "item"):
|
|
token_id = token_id.item()
|
|
try:
|
|
normalized_ids.append(int(token_id))
|
|
except (TypeError, ValueError) as exc:
|
|
raise TypeError(
|
|
"Cosmos3 tokenizer returned a non-integer token at "
|
|
f"index {idx}: {type(token_id).__name__}: {token_id!r}"
|
|
) from exc
|
|
return normalized_ids
|
|
|
|
# -- Latent preparation --------------------------------------------------
|
|
|
|
def _prepare_latents(
|
|
self,
|
|
height: int,
|
|
width: int,
|
|
num_frames: int,
|
|
generator: torch.Generator,
|
|
) -> torch.Tensor:
|
|
num_channels_latents = self.transformer.latent_channel_size
|
|
num_latent_frames = (num_frames - 1) // self.vae_scale_factor_temporal + 1
|
|
shape = (
|
|
1,
|
|
num_channels_latents,
|
|
num_latent_frames,
|
|
height // self.vae_scale_factor_spatial,
|
|
width // self.vae_scale_factor_spatial,
|
|
)
|
|
return randn_tensor(shape, generator=generator, device=self.device, dtype=self.dtype)
|
|
|
|
def _prepare_sound_latents(
|
|
self,
|
|
target_audio_samples: int,
|
|
generator: torch.Generator,
|
|
*,
|
|
sp_video_shape: tuple[int, int, int] | None = None,
|
|
sp_num_vision_items: int = 1,
|
|
) -> tuple[torch.Tensor, int]:
|
|
sound_tokenizer = self._get_sound_tokenizer()
|
|
hop_size = int(
|
|
getattr(sound_tokenizer, "hop_size", None) or getattr(sound_tokenizer, "temporal_compression_factor")
|
|
)
|
|
latent_frames = max(1, math.ceil(max(1, int(target_audio_samples)) / hop_size))
|
|
if sp_video_shape is not None:
|
|
latent_frames = self.transformer.sound_latent_frames_for_sequence_parallel(
|
|
video_shape=sp_video_shape,
|
|
sound_frames=latent_frames,
|
|
num_vision_items=sp_num_vision_items,
|
|
)
|
|
sound_dim = int(getattr(sound_tokenizer, "latent_ch", 64))
|
|
transformer_sound_dim = int(getattr(self.transformer, "sound_dim", sound_dim))
|
|
if sound_dim != transformer_sound_dim:
|
|
raise ValueError(
|
|
"Cosmos3 sound tokenizer latent channels do not match transformer "
|
|
f"sound_dim: tokenizer={sound_dim}, transformer={transformer_sound_dim}."
|
|
)
|
|
latents = randn_tensor(
|
|
(1, sound_dim, latent_frames),
|
|
generator=generator,
|
|
device=self.device,
|
|
dtype=self.dtype,
|
|
)
|
|
return latents, latent_frames
|
|
|
|
def _resolve_sound_target_samples(
|
|
self,
|
|
sp,
|
|
num_frames: int,
|
|
frame_rate: float,
|
|
) -> tuple[int, float, int]:
|
|
sound_tokenizer = self._get_sound_tokenizer()
|
|
duration = self._get_sp_param(sp, "sound_duration", None)
|
|
if duration is None:
|
|
duration = self._get_sp_param(sp, "audio_duration", None)
|
|
if duration is None:
|
|
duration = num_frames / frame_rate
|
|
duration = max(float(duration), 1.0 / max(float(frame_rate), 1.0))
|
|
sample_rate = int(getattr(sound_tokenizer, "sample_rate", 48000))
|
|
return max(1, int(round(duration * sample_rate))), duration, sample_rate
|
|
|
|
# -- VAE decode ----------------------------------------------------------
|
|
|
|
def _get_latents_mean_std(self, device: torch.device, dtype: torch.dtype) -> tuple[torch.Tensor, torch.Tensor]:
|
|
cached = getattr(self, "_latents_mean_std", None)
|
|
if cached is not None:
|
|
latents_mean, latents_std = cached
|
|
if latents_mean.device == device and latents_mean.dtype == dtype:
|
|
return latents_mean, latents_std
|
|
|
|
latents_mean = torch.as_tensor(self.vae.config.latents_mean, device=device, dtype=dtype).view(1, -1, 1, 1, 1)
|
|
latents_std = torch.as_tensor(self.vae.config.latents_std, device=device, dtype=dtype).view(1, -1, 1, 1, 1)
|
|
self._latents_mean_std = (latents_mean, latents_std)
|
|
return latents_mean, latents_std
|
|
|
|
def _to_vae_device(self, tensor: torch.Tensor, *, pin_cpu: bool = False) -> torch.Tensor:
|
|
if tensor.device == self.device and tensor.dtype == self.vae.dtype:
|
|
return tensor
|
|
|
|
non_blocking = False
|
|
if tensor.device.type == "cpu" and self.device.type == "cuda":
|
|
if pin_cpu and not tensor.is_pinned():
|
|
tensor = tensor.pin_memory()
|
|
non_blocking = tensor.is_pinned()
|
|
|
|
return tensor.to(device=self.device, dtype=self.vae.dtype, non_blocking=non_blocking)
|
|
|
|
def _decode_latents(self, latents: torch.Tensor) -> torch.Tensor:
|
|
latents = latents.to(self.vae.dtype)
|
|
|
|
if hasattr(self.vae.config, "latents_mean") and hasattr(self.vae.config, "latents_std"):
|
|
latents_mean, latents_std = self._get_latents_mean_std(latents.device, latents.dtype)
|
|
latents = (latents * latents_std) + latents_mean
|
|
else:
|
|
scaling_factor = getattr(self.vae.config, "scaling_factor", 1.0)
|
|
latents = latents / scaling_factor
|
|
|
|
video = self.vae.decode(latents, return_dict=False)[0]
|
|
return video
|
|
|
|
def _decode_sound_latents(
|
|
self,
|
|
sound_latents: torch.Tensor,
|
|
target_audio_samples: int,
|
|
) -> torch.Tensor:
|
|
sound_tokenizer = self._get_sound_tokenizer()
|
|
audio = sound_tokenizer.decode(sound_latents.to(self.dtype))
|
|
if audio.shape[-1] > target_audio_samples:
|
|
audio = audio[..., :target_audio_samples]
|
|
elif audio.shape[-1] < target_audio_samples:
|
|
audio = torch.nn.functional.pad(audio, (0, target_audio_samples - audio.shape[-1]))
|
|
return audio.detach().cpu()
|
|
|
|
# -- Prompt formatting + tokenization (shared by T2V and I2V) ------------
|
|
|
|
def _format_and_tokenize_prompts(
|
|
self,
|
|
prompt: str,
|
|
negative_prompt: str,
|
|
num_frames: int,
|
|
frame_rate: float,
|
|
height: int,
|
|
width: int,
|
|
max_sequence_length: int,
|
|
sp: OmniDiffusionSamplingParams,
|
|
use_system_prompt: bool = False,
|
|
is_t2i: bool = False,
|
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
"""Format prompts with metadata templates and tokenize.
|
|
|
|
Returns (cond_ids, cond_mask, uncond_ids, uncond_mask).
|
|
|
|
For T2I (``is_t2i=True``) the duration template is suppressed (no FPS
|
|
or duration concept for a single image) and the image-flavored
|
|
resolution template is used.
|
|
"""
|
|
# Route cosmos3-specific controls through ``_get_sp_param`` so they
|
|
# are picked up from ``extra_args`` (OpenAI endpoint path) as well
|
|
# as from direct attributes.
|
|
use_duration_template = bool(self._get_sp_param(sp, "use_duration_template", False)) and not is_t2i
|
|
dur_tmpl = COSMOS3_DURATION_TEMPLATE if use_duration_template else None
|
|
if bool(self._get_sp_param(sp, "use_resolution_template", False)):
|
|
res_tmpl = COSMOS3_IMAGE_RESOLUTION_TEMPLATE if is_t2i else COSMOS3_RESOLUTION_TEMPLATE
|
|
else:
|
|
res_tmpl = None
|
|
prompt = self._apply_metadata_templates(
|
|
prompt,
|
|
num_frames,
|
|
frame_rate,
|
|
height,
|
|
width,
|
|
duration_template=dur_tmpl,
|
|
resolution_template=res_tmpl,
|
|
)
|
|
if _is_rank_zero():
|
|
logger.info("Final prompt: '%s'", prompt)
|
|
|
|
# Negative prompt: inverse templates ("not {duration}...", "not {height}x{width}...").
|
|
# Applied whenever the matching positive template is enabled; an empty
|
|
# negative_prompt yields output that starts with the template, not a dot.
|
|
inv_dur = COSMOS3_INVERSE_DURATION_TEMPLATE if dur_tmpl else None
|
|
if res_tmpl is None:
|
|
inv_res = None
|
|
elif is_t2i:
|
|
inv_res = COSMOS3_INVERSE_IMAGE_RESOLUTION_TEMPLATE
|
|
else:
|
|
inv_res = COSMOS3_INVERSE_RESOLUTION_TEMPLATE
|
|
negative_prompt = self._apply_metadata_templates(
|
|
negative_prompt,
|
|
num_frames,
|
|
frame_rate,
|
|
height,
|
|
width,
|
|
duration_template=inv_dur,
|
|
resolution_template=inv_res,
|
|
force_duration_template=True,
|
|
)
|
|
|
|
default_sys_prompt = COSMOS3_T2I_SYSTEM_PROMPT if is_t2i else COSMOS3_SYSTEM_PROMPT
|
|
sys_prompt = self._get_sp_param(sp, "system_prompt", default_sys_prompt) or default_sys_prompt
|
|
cond_ids, cond_mask = self._tokenize_prompt(
|
|
prompt, max_sequence_length, use_system_prompt, system_prompt=sys_prompt
|
|
)
|
|
uncond_ids, uncond_mask = self._tokenize_prompt(
|
|
negative_prompt, max_sequence_length, use_system_prompt, system_prompt=sys_prompt
|
|
)
|
|
return cond_ids, cond_mask, uncond_ids, uncond_mask
|
|
|
|
# -- I2V latent preparation ---------------------------------------------
|
|
|
|
def _normalize_vae_latent(self, latent: torch.Tensor) -> torch.Tensor:
|
|
if hasattr(self.vae.config, "latents_mean") and hasattr(self.vae.config, "latents_std"):
|
|
latents_mean, latents_std = self._get_latents_mean_std(latent.device, latent.dtype)
|
|
latent = (latent - latents_mean) / latents_std
|
|
else:
|
|
scaling_factor = getattr(self.vae.config, "scaling_factor", 1.0)
|
|
latent = latent * scaling_factor
|
|
|
|
return latent
|
|
|
|
def _encode_conditioning_image_latent(self, image_tensor: torch.Tensor) -> torch.Tensor:
|
|
"""VAE-encode the first I2V conditioning frame.
|
|
|
|
I2V only consumes the first conditioning latent frame. Encoding the
|
|
input image as a one-frame video keeps that latent while avoiding VAE
|
|
work for repeated frames that are later replaced by noise.
|
|
"""
|
|
image_tensor = self._to_vae_device(image_tensor, pin_cpu=True)
|
|
video = image_tensor.unsqueeze(2)
|
|
latent = self.vae.encode(video).latent_dist.mode()
|
|
latent = self._normalize_vae_latent(latent)
|
|
return latent[:, :, 0:1, :, :].to(self.dtype)
|
|
|
|
def _latent_hw_from_image_size(self, image_size: Any | None) -> tuple[int, int] | None:
|
|
if image_size is None:
|
|
return None
|
|
if isinstance(image_size, torch.Tensor):
|
|
frame_size = image_size.detach().cpu().flatten()
|
|
else:
|
|
frame_size = torch.as_tensor(image_size).flatten()
|
|
if frame_size.numel() < 4:
|
|
return None
|
|
orig_h = int(frame_size[2].item())
|
|
orig_w = int(frame_size[3].item())
|
|
spatial_factor = int(self.vae_scale_factor_spatial)
|
|
return max(orig_h // spatial_factor, 1), max(orig_w // spatial_factor, 1)
|
|
|
|
def _crop_latent_to_image_size(self, latent: torch.Tensor, image_size: Any | None) -> torch.Tensor:
|
|
latent_hw = self._latent_hw_from_image_size(image_size)
|
|
if latent_hw is None:
|
|
return latent
|
|
h_latent, w_latent = latent_hw
|
|
return latent[:, :, :, :h_latent, :w_latent].contiguous()
|
|
|
|
def _encode_video_tensor(self, video_tensor: torch.Tensor, image_size: Any | None = None) -> torch.Tensor:
|
|
"""VAE-encode a preprocessed pixel video [1, 3, T, H, W]."""
|
|
if video_tensor.ndim == 4:
|
|
video_tensor = video_tensor.unsqueeze(0)
|
|
if video_tensor.ndim != 5:
|
|
raise ValueError(f"Cosmos3 video tensor must have shape [1, 3, T, H, W], got {tuple(video_tensor.shape)}.")
|
|
if video_tensor.shape[0] != 1 or video_tensor.shape[1] != 3:
|
|
raise ValueError(f"Cosmos3 video tensor must have shape [1, 3, T, H, W], got {tuple(video_tensor.shape)}.")
|
|
|
|
video = self._to_vae_device(video_tensor)
|
|
latent = self.vae.encode(video).latent_dist.mode()
|
|
latent = self._normalize_vae_latent(latent)
|
|
latent = self._crop_latent_to_image_size(latent, image_size)
|
|
return latent.to(self.dtype)
|
|
|
|
def _prepare_latents_i2v(
|
|
self,
|
|
image_tensor: torch.Tensor,
|
|
height: int,
|
|
width: int,
|
|
num_frames: int,
|
|
generator: torch.Generator,
|
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
"""Prepare initial latents with frame 0 conditioned on the input image.
|
|
|
|
Returns:
|
|
latents: [1, C, T_lat, H_lat, W_lat] with frame 0 = image, rest = noise
|
|
velocity_mask: [1, 1, T_lat, 1, 1] with frame 0 = 0, rest = 1
|
|
image_latent: [1, C, 1, H_lat, W_lat] clean frame 0 for re-injection
|
|
"""
|
|
C = self.transformer.latent_channel_size
|
|
T_lat = (num_frames - 1) // self.vae_scale_factor_temporal + 1
|
|
H_lat = height // self.vae_scale_factor_spatial
|
|
W_lat = width // self.vae_scale_factor_spatial
|
|
|
|
noise = randn_tensor(
|
|
(1, C, T_lat, H_lat, W_lat),
|
|
generator=generator,
|
|
device=self.device,
|
|
dtype=self.dtype,
|
|
)
|
|
|
|
image_latent = self._encode_conditioning_image_latent(image_tensor)
|
|
latents = noise
|
|
latents[:, :, 0:1, :, :] = image_latent
|
|
|
|
velocity_mask = torch.ones(1, 1, T_lat, 1, 1, device=self.device, dtype=self.dtype)
|
|
velocity_mask[:, :, 0, :, :] = 0.0
|
|
return latents, velocity_mask, image_latent
|
|
|
|
def _prepare_latents_v2v(
|
|
self,
|
|
video_tensor: torch.Tensor,
|
|
height: int,
|
|
width: int,
|
|
num_frames: int,
|
|
generator: torch.Generator,
|
|
condition_frame_indexes_vision: Iterable[int] | int | str | None = None,
|
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
"""Prepare V2V latents with explicit clean conditioned latent frames."""
|
|
del height, width
|
|
if video_tensor.ndim == 4:
|
|
video_tensor = video_tensor.unsqueeze(0)
|
|
if video_tensor.ndim != 5 or video_tensor.shape[0] != 1 or video_tensor.shape[1] != 3:
|
|
raise ValueError(f"Cosmos3 video tensor must have shape [1, 3, T, H, W], got {tuple(video_tensor.shape)}.")
|
|
if video_tensor.shape[2] < 1:
|
|
raise ValueError("Cosmos3 V2V video tensor must contain at least one frame.")
|
|
|
|
C = self.transformer.latent_channel_size
|
|
T_lat = (num_frames - 1) // self.vae_scale_factor_temporal + 1
|
|
H_lat = video_tensor.shape[-2] // self.vae_scale_factor_spatial
|
|
W_lat = video_tensor.shape[-1] // self.vae_scale_factor_spatial
|
|
indexes = normalize_condition_frame_indexes_vision(condition_frame_indexes_vision)
|
|
out_of_range = [index for index in indexes if index >= T_lat]
|
|
if out_of_range:
|
|
raise ValueError(
|
|
"Cosmos3 condition_frame_indexes_vision contains indexes outside the latent video: "
|
|
f"indexes={indexes}, latent_frames={T_lat}."
|
|
)
|
|
|
|
noise = randn_tensor(
|
|
(1, C, T_lat, H_lat, W_lat),
|
|
generator=generator,
|
|
device=self.device,
|
|
dtype=self.dtype,
|
|
)
|
|
condition_pixel_frames = condition_pixel_frame_count(indexes, self.vae_scale_factor_temporal)
|
|
condition_video = video_tensor[:, :, :condition_pixel_frames]
|
|
if condition_video.shape[2] < condition_pixel_frames:
|
|
pad = condition_video[:, :, -1:].repeat(1, 1, condition_pixel_frames - condition_video.shape[2], 1, 1)
|
|
condition_video = torch.cat([condition_video, pad], dim=2)
|
|
|
|
cond_prefix_latent = self._encode_video_tensor(condition_video)
|
|
expected_prefix = (1, C, max(indexes) + 1, H_lat, W_lat)
|
|
if (
|
|
cond_prefix_latent.shape[0] != expected_prefix[0]
|
|
or cond_prefix_latent.shape[1] != expected_prefix[1]
|
|
or cond_prefix_latent.shape[2] < expected_prefix[2]
|
|
or cond_prefix_latent.shape[3:] != expected_prefix[3:]
|
|
):
|
|
raise ValueError(
|
|
"Cosmos3 V2V condition latent shape mismatch: "
|
|
f"encoded={tuple(cond_prefix_latent.shape)}, expected at least {expected_prefix}."
|
|
)
|
|
|
|
condition_mask = torch.zeros(1, 1, T_lat, 1, 1, device=self.device, dtype=self.dtype)
|
|
condition_latents = torch.zeros_like(noise)
|
|
for index in indexes:
|
|
condition_mask[:, :, index, :, :] = 1.0
|
|
condition_latents[:, :, index : index + 1] = cond_prefix_latent[:, :, index : index + 1]
|
|
latents = condition_mask * condition_latents + (1.0 - condition_mask) * noise
|
|
velocity_mask = 1.0 - condition_mask
|
|
return latents, velocity_mask, condition_latents
|
|
|
|
def _prepare_latents_action_video(
|
|
self,
|
|
video_tensor: torch.Tensor,
|
|
mode: str,
|
|
height: int,
|
|
width: int,
|
|
num_frames: int,
|
|
generator: torch.Generator,
|
|
image_size: Any | None = None,
|
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
"""Prepare video latents for action modes with mode-specific conditioning.
|
|
|
|
Policy and forward-dynamics modes condition only latent frame zero.
|
|
The Wan VAE is temporally causal, so encoding only the first pixel
|
|
frame preserves that latent while avoiding unused future-frame work.
|
|
Inverse dynamics conditions every latent and keeps the full encode.
|
|
"""
|
|
del height, width
|
|
if video_tensor.ndim == 4:
|
|
video_tensor = video_tensor.unsqueeze(0)
|
|
if video_tensor.ndim != 5 or video_tensor.shape[0] != 1 or video_tensor.shape[1] != 3:
|
|
raise ValueError(f"Cosmos3 video tensor must have shape [1, 3, T, H, W], got {tuple(video_tensor.shape)}.")
|
|
if video_tensor.shape[2] < 1:
|
|
raise ValueError("Cosmos3 action video tensor must contain at least one frame.")
|
|
|
|
C = self.transformer.latent_channel_size
|
|
T_lat = (num_frames - 1) // self.vae_scale_factor_temporal + 1
|
|
latent_hw = self._latent_hw_from_image_size(image_size)
|
|
if latent_hw is None:
|
|
H_lat = video_tensor.shape[-2] // self.vae_scale_factor_spatial
|
|
W_lat = video_tensor.shape[-1] // self.vae_scale_factor_spatial
|
|
else:
|
|
H_lat, W_lat = latent_hw
|
|
|
|
noise = randn_tensor(
|
|
(1, C, T_lat, H_lat, W_lat),
|
|
generator=generator,
|
|
device=self.device,
|
|
dtype=self.dtype,
|
|
)
|
|
condition_indexes = vision_condition_indexes(mode, num_frames, self.vae_scale_factor_temporal)
|
|
condition_video = video_tensor[:, :, :1] if condition_indexes == [0] else video_tensor
|
|
cond_prefix_latent = self._encode_video_tensor(condition_video, image_size=image_size)
|
|
expected_prefix = (1, C, max(condition_indexes) + 1, H_lat, W_lat)
|
|
if (
|
|
cond_prefix_latent.shape[0] != expected_prefix[0]
|
|
or cond_prefix_latent.shape[1] != expected_prefix[1]
|
|
or cond_prefix_latent.shape[2] < expected_prefix[2]
|
|
or cond_prefix_latent.shape[3:] != expected_prefix[3:]
|
|
):
|
|
raise ValueError(
|
|
"Cosmos3 action video latent shape mismatch: "
|
|
f"encoded={tuple(cond_prefix_latent.shape)}, expected at least {expected_prefix}."
|
|
)
|
|
|
|
condition_latents = torch.zeros_like(noise)
|
|
for index in condition_indexes:
|
|
condition_latents[:, :, index : index + 1] = cond_prefix_latent[:, :, index : index + 1]
|
|
condition_mask = build_vision_condition_mask(
|
|
mode,
|
|
num_frames,
|
|
self.vae_scale_factor_temporal,
|
|
device=self.device,
|
|
dtype=self.dtype,
|
|
)
|
|
latents = condition_mask * condition_latents + (1.0 - condition_mask) * noise
|
|
velocity_mask = 1.0 - condition_mask
|
|
return latents, velocity_mask, condition_latents
|
|
|
|
def _prepare_action_latents(
|
|
self,
|
|
*,
|
|
mode: str,
|
|
action_chunk_size: int,
|
|
raw_action_dim: int | None,
|
|
generator: torch.Generator,
|
|
sp,
|
|
clean_action: torch.Tensor | None = None,
|
|
condition_indexes: list[int] | None = None,
|
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int]:
|
|
action_dim = int(getattr(self.transformer, "action_dim", 64))
|
|
if clean_action is not None:
|
|
action = clean_action.detach().to(dtype=torch.float32)
|
|
if action.ndim == 3 and action.shape[0] == 1:
|
|
action = action.squeeze(0)
|
|
if action.ndim != 2:
|
|
raise ValueError(f"Cosmos3 clean action must have shape [T, D], got {tuple(action.shape)}.")
|
|
if action.shape[0] < action_chunk_size:
|
|
pad = action[-1:].repeat(action_chunk_size - action.shape[0], 1)
|
|
action = torch.cat([action, pad], dim=0)
|
|
elif action.shape[0] > action_chunk_size:
|
|
action = action[:action_chunk_size]
|
|
if raw_action_dim is None:
|
|
raw_action_dim = int(action.shape[-1])
|
|
clean_action = pad_action_to_dim(action, action_dim)
|
|
elif mode == ACTION_MODE_FORWARD_DYNAMICS:
|
|
action = load_action_tensor(self._get_sp_param(sp, "action", None))
|
|
if action.shape[0] < action_chunk_size:
|
|
pad = action[-1:].repeat(action_chunk_size - action.shape[0], 1)
|
|
action = torch.cat([action, pad], dim=0)
|
|
elif action.shape[0] > action_chunk_size:
|
|
action = action[:action_chunk_size]
|
|
if raw_action_dim is None:
|
|
raw_action_dim = int(action.shape[-1])
|
|
clean_action = pad_action_to_dim(action, action_dim)
|
|
else:
|
|
if raw_action_dim is None:
|
|
raise ValueError(
|
|
"Cosmos3 action_mode='policy' and 'inverse_dynamics' require extra_args['raw_action_dim']."
|
|
)
|
|
clean_action = torch.zeros(action_chunk_size, action_dim, dtype=torch.float32)
|
|
|
|
raw_action_dim = int(raw_action_dim)
|
|
if raw_action_dim <= 0 or raw_action_dim > action_dim:
|
|
raise ValueError(f"Cosmos3 raw_action_dim must be in [1, {action_dim}], got {raw_action_dim}.")
|
|
|
|
clean_action = clean_action.to(device=self.device, dtype=self.dtype).unsqueeze(0)
|
|
if condition_indexes is None:
|
|
condition_mask = build_action_condition_mask(
|
|
mode,
|
|
action_chunk_size,
|
|
device=self.device,
|
|
dtype=self.dtype,
|
|
)
|
|
else:
|
|
condition_mask = self._build_action_condition_mask_from_indexes(
|
|
condition_indexes,
|
|
action_chunk_size,
|
|
device=self.device,
|
|
dtype=self.dtype,
|
|
)
|
|
noise = randn_tensor(
|
|
(1, action_chunk_size, action_dim),
|
|
generator=generator,
|
|
device=self.device,
|
|
dtype=self.dtype,
|
|
)
|
|
noise[:, :, raw_action_dim:] = 0
|
|
clean_action[:, :, raw_action_dim:] = 0
|
|
action_latents = condition_mask * clean_action + (1.0 - condition_mask) * noise
|
|
action_velocity_mask = 1.0 - condition_mask
|
|
return action_latents, action_velocity_mask, clean_action, raw_action_dim
|
|
|
|
# -- Denoising loop (shared by T2V and I2V) -----------------------------
|
|
|
|
def diffuse(
|
|
self,
|
|
latents: torch.Tensor,
|
|
timesteps: torch.Tensor,
|
|
cond_ids: torch.Tensor,
|
|
cond_mask: torch.Tensor,
|
|
uncond_ids: torch.Tensor,
|
|
uncond_mask: torch.Tensor,
|
|
guidance_scale: float,
|
|
shared_kwargs: dict,
|
|
*,
|
|
action_latents: torch.Tensor | None = None,
|
|
action_velocity_mask: torch.Tensor | None = None,
|
|
action_condition_latents: torch.Tensor | None = None,
|
|
sound_latents: torch.Tensor | None = None,
|
|
velocity_mask: torch.Tensor | None = None,
|
|
image_latent: torch.Tensor | None = None,
|
|
condition_latents: torch.Tensor | None = None,
|
|
guidance_interval: tuple[float, float] | None = None,
|
|
raw_action_dim: int | None = None,
|
|
scheduler: Any | None = None,
|
|
) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
|
"""Denoising loop with 3-mode CFG support (parallel, sequential, none).
|
|
|
|
Cosmos3's UND pathway is text-dependent, so CFG needs separate K/V
|
|
caches for conditional and unconditional text.
|
|
|
|
Two modes:
|
|
1. CFG parallel (multi-GPU): each rank handles one condition via
|
|
predict_noise_maybe_with_cfg; caching is rank-local.
|
|
2. Sequential CFG (single-GPU or cfg_size=1): two separate
|
|
forward passes with explicit cache swapping. We cannot
|
|
batch B=2 because different text lengths would cause the
|
|
shorter branch to attend to padding in cross-attention.
|
|
|
|
I2V conditioning (when both arguments are supplied):
|
|
* ``velocity_mask`` zeros frame-0 noise predictions before stepping.
|
|
* ``image_latent`` is re-injected into frame 0 after each scheduler
|
|
step, since UniPC's predictor-corrector update rescales the
|
|
sample (sigma-dependent), so even zero velocity does not preserve
|
|
frame 0.
|
|
|
|
``guidance_interval`` (T2I) restricts CFG to
|
|
timesteps inside the closed interval ``[lo, hi]``. The interval is
|
|
compared against the raw scheduler timestep value; works for both
|
|
the [0, 1000] discrete scale and normalized flow-matching scales.
|
|
Outside the interval the cond/uncond delta is zeroed so all ranks
|
|
continue to execute identical control flow (CFG-Parallel safe).
|
|
"""
|
|
do_cfg = guidance_scale > 1.0
|
|
cfg_parallel = self._cfg_parallel_active() and do_cfg
|
|
step_scheduler = scheduler if scheduler is not None else self.scheduler
|
|
self.transformer.reset_cache()
|
|
|
|
def _cfg_active_at(t: torch.Tensor) -> bool:
|
|
if guidance_interval is None:
|
|
return True
|
|
t_scalar = float(t.item()) if torch.is_tensor(t) else float(t)
|
|
lo, hi = guidance_interval
|
|
return lo <= t_scalar <= hi
|
|
|
|
# Joint scheduler step over multiple modalities. Safe for flow-matching schedulers
|
|
# because the update is linear per element; revisit this if Cosmos3 adopts a
|
|
# scheduler with cross-element dependencies (e.g. per-modality timestep).
|
|
def _pack_joint(
|
|
video_tensor: torch.Tensor,
|
|
action_tensor: torch.Tensor | None = None,
|
|
sound_tensor: torch.Tensor | None = None,
|
|
):
|
|
batch = video_tensor.shape[0]
|
|
tensors = [video_tensor]
|
|
if action_tensor is not None:
|
|
tensors.append(action_tensor)
|
|
if sound_tensor is not None:
|
|
tensors.append(sound_tensor)
|
|
flats = [tensor.reshape(batch, -1) for tensor in tensors]
|
|
return torch.cat(flats, dim=1), [tensor.shape for tensor in tensors], [flat.shape[1] for flat in flats]
|
|
|
|
def _unpack_joint(
|
|
packed: torch.Tensor,
|
|
shapes: list[torch.Size],
|
|
numels: list[int],
|
|
) -> tuple[torch.Tensor, ...]:
|
|
outputs = []
|
|
offset = 0
|
|
for shape, numel in zip(shapes, numels, strict=True):
|
|
outputs.append(packed[:, offset : offset + numel].reshape(shape))
|
|
offset += numel
|
|
return tuple(outputs)
|
|
|
|
def _split_noise_pred(
|
|
noise_pred: torch.Tensor | tuple[torch.Tensor, ...],
|
|
) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
|
|
has_action = action_latents is not None
|
|
has_sound = sound_latents is not None
|
|
if not has_action and not has_sound:
|
|
if isinstance(noise_pred, tuple):
|
|
raise ValueError("Cosmos3 video-only diffusion received tuple predictions.")
|
|
return noise_pred, None, None
|
|
if not isinstance(noise_pred, tuple):
|
|
raise ValueError("Cosmos3 multimodal diffusion expects transformer predictions as a tuple.")
|
|
expected = 1 + int(has_action) + int(has_sound)
|
|
if len(noise_pred) != expected:
|
|
raise ValueError(
|
|
f"Cosmos3 multimodal diffusion expected {expected} predictions, got {len(noise_pred)}."
|
|
)
|
|
video_pred = noise_pred[0]
|
|
idx = 1
|
|
action_pred = noise_pred[idx] if has_action else None
|
|
if has_action:
|
|
idx += 1
|
|
sound_pred = noise_pred[idx] if has_sound else None
|
|
return video_pred, action_pred, sound_pred
|
|
|
|
def _step(
|
|
noise_pred: torch.Tensor | tuple[torch.Tensor, ...],
|
|
t: torch.Tensor,
|
|
latents: torch.Tensor,
|
|
action_latents: torch.Tensor | None,
|
|
sound_latents: torch.Tensor | None,
|
|
) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
|
video_pred, action_pred, sound_pred = _split_noise_pred(noise_pred)
|
|
if velocity_mask is not None:
|
|
if image_latent is not None and condition_latents is None:
|
|
video_pred[:, :, 0:1, :, :] = 0
|
|
else:
|
|
video_pred = video_pred * velocity_mask
|
|
if action_pred is not None and action_velocity_mask is not None:
|
|
action_pred = action_pred * action_velocity_mask
|
|
if raw_action_dim is not None and 0 < raw_action_dim < action_pred.shape[-1]:
|
|
action_pred[..., raw_action_dim:] = 0
|
|
if action_latents is None and sound_latents is None:
|
|
latents = step_scheduler.step(video_pred, t, latents, return_dict=False)[0]
|
|
else:
|
|
packed_noise, shapes, numels = _pack_joint(video_pred, action_pred, sound_pred)
|
|
packed_latents, _, _ = _pack_joint(latents, action_latents, sound_latents)
|
|
packed_next = step_scheduler.step(packed_noise, t, packed_latents, return_dict=False)[0]
|
|
unpacked = _unpack_joint(packed_next, shapes, numels)
|
|
latents = unpacked[0]
|
|
idx = 1
|
|
if action_latents is not None:
|
|
action_latents = unpacked[idx]
|
|
idx += 1
|
|
if sound_latents is not None:
|
|
sound_latents = unpacked[idx]
|
|
if condition_latents is not None and velocity_mask is not None:
|
|
latents = velocity_mask * latents + (1.0 - velocity_mask) * condition_latents
|
|
elif image_latent is not None:
|
|
latents[:, :, 0:1, :, :] = image_latent
|
|
if action_latents is not None and action_condition_latents is not None and action_velocity_mask is not None:
|
|
action_latents = (
|
|
action_velocity_mask * action_latents + (1.0 - action_velocity_mask) * action_condition_latents
|
|
)
|
|
outputs = [latents]
|
|
if action_latents is not None:
|
|
outputs.append(action_latents)
|
|
if sound_latents is not None:
|
|
outputs.append(sound_latents)
|
|
return outputs[0] if len(outputs) == 1 else tuple(outputs)
|
|
|
|
def _assign_step_out(step_out: torch.Tensor | tuple[torch.Tensor, ...]) -> None:
|
|
nonlocal latents, action_latents, sound_latents
|
|
if action_latents is None and sound_latents is None:
|
|
assert isinstance(step_out, torch.Tensor)
|
|
latents = step_out
|
|
return
|
|
if not isinstance(step_out, tuple):
|
|
raise ValueError("Cosmos3 multimodal diffusion step returned a non-tuple result.")
|
|
latents = step_out[0]
|
|
idx = 1
|
|
if action_latents is not None:
|
|
action_latents = step_out[idx]
|
|
idx += 1
|
|
if sound_latents is not None:
|
|
sound_latents = step_out[idx]
|
|
|
|
if cfg_parallel:
|
|
for t in self.progress_bar(timesteps):
|
|
timestep = t.unsqueeze(0)
|
|
# Out-of-interval steps run with effective scale 1.0 so the
|
|
# combined output equals the cond branch (uncond is dropped).
|
|
# All ranks still execute both branches; no CFG-Parallel
|
|
# divergence.
|
|
step_scale = guidance_scale if _cfg_active_at(t) else 1.0
|
|
noise_pred = self.predict_noise_maybe_with_cfg(
|
|
do_true_cfg=True,
|
|
true_cfg_scale=step_scale,
|
|
positive_kwargs=dict(
|
|
hidden_states=latents,
|
|
timestep=timestep,
|
|
text_ids=cond_ids,
|
|
text_mask=cond_mask,
|
|
action_latents=action_latents,
|
|
sound_latents=sound_latents,
|
|
**shared_kwargs,
|
|
),
|
|
negative_kwargs=dict(
|
|
hidden_states=latents,
|
|
timestep=timestep,
|
|
text_ids=uncond_ids,
|
|
text_mask=uncond_mask,
|
|
action_latents=action_latents,
|
|
sound_latents=sound_latents,
|
|
**shared_kwargs,
|
|
),
|
|
cfg_normalize=False,
|
|
)
|
|
_assign_step_out(_step(noise_pred, t, latents, action_latents, sound_latents))
|
|
|
|
elif do_cfg:
|
|
cond_cache: tuple = (None, None)
|
|
uncond_cache: tuple = (None, None)
|
|
|
|
keep_uncond_for_cache = self._cache_requires_paired_cfg()
|
|
|
|
for t in self.progress_bar(timesteps):
|
|
timestep = t.unsqueeze(0)
|
|
cfg_active = _cfg_active_at(t)
|
|
|
|
self.transformer.cached_kv, self.transformer.cached_freqs_gen = cond_cache
|
|
noise_cond = self.transformer(
|
|
hidden_states=latents,
|
|
timestep=timestep,
|
|
text_ids=cond_ids,
|
|
text_mask=cond_mask,
|
|
action_latents=action_latents,
|
|
sound_latents=sound_latents,
|
|
**shared_kwargs,
|
|
)
|
|
if cond_cache[0] is None:
|
|
cond_cache = (self.transformer.cached_kv, self.transformer.cached_freqs_gen)
|
|
|
|
if cfg_active or keep_uncond_for_cache:
|
|
self.transformer.cached_kv, self.transformer.cached_freqs_gen = uncond_cache
|
|
noise_uncond = self.transformer(
|
|
hidden_states=latents,
|
|
timestep=timestep,
|
|
text_ids=uncond_ids,
|
|
text_mask=uncond_mask,
|
|
action_latents=action_latents,
|
|
sound_latents=sound_latents,
|
|
**shared_kwargs,
|
|
)
|
|
if uncond_cache[0] is None:
|
|
uncond_cache = (self.transformer.cached_kv, self.transformer.cached_freqs_gen)
|
|
# Outside the interval, scale=1.0 makes the combined result
|
|
# equal to noise_cond; the uncond pass is computed only to
|
|
# preserve cache-dit's cond/uncond parity.
|
|
step_scale = guidance_scale if cfg_active else 1.0
|
|
noise_pred = self.combine_cfg_noise(noise_cond, noise_uncond, step_scale, cfg_normalize=False)
|
|
else:
|
|
noise_pred = noise_cond
|
|
|
|
_assign_step_out(_step(noise_pred, t, latents, action_latents, sound_latents))
|
|
|
|
else:
|
|
for t in self.progress_bar(timesteps):
|
|
timestep = t.unsqueeze(0)
|
|
noise_pred = self.transformer(
|
|
hidden_states=latents,
|
|
timestep=timestep,
|
|
text_ids=cond_ids,
|
|
text_mask=cond_mask,
|
|
action_latents=action_latents,
|
|
sound_latents=sound_latents,
|
|
**shared_kwargs,
|
|
)
|
|
_assign_step_out(_step(noise_pred, t, latents, action_latents, sound_latents))
|
|
|
|
outputs = [latents]
|
|
if action_latents is not None:
|
|
outputs.append(action_latents)
|
|
if sound_latents is not None:
|
|
outputs.append(sound_latents)
|
|
return outputs[0] if len(outputs) == 1 else tuple(outputs)
|
|
|
|
@staticmethod
|
|
def _get_transfer_num_chunks(
|
|
total_frames: int,
|
|
frames_per_chunk: int,
|
|
conditional_frames: int,
|
|
) -> tuple[int, int]:
|
|
if frames_per_chunk <= 0:
|
|
raise ValueError("Cosmos3 transfer frames_per_chunk must be positive.")
|
|
if total_frames <= frames_per_chunk:
|
|
return 1, frames_per_chunk
|
|
stride = frames_per_chunk - conditional_frames
|
|
if stride <= 0:
|
|
raise ValueError("Cosmos3 transfer num_conditional_frames must be smaller than num_video_frames_per_chunk.")
|
|
remaining = total_frames - frames_per_chunk
|
|
extra_chunks = remaining // stride + (1 if remaining % stride else 0)
|
|
return 1 + extra_chunks, stride
|
|
|
|
def _prepare_transfer_latents(
|
|
self,
|
|
target_video: torch.Tensor,
|
|
current_conditional_frames: int,
|
|
generator: torch.Generator,
|
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
condition_latents = self._encode_video_tensor(target_video)
|
|
noise = randn_tensor(
|
|
condition_latents.shape,
|
|
generator=generator,
|
|
device=self.device,
|
|
dtype=self.dtype,
|
|
)
|
|
condition_mask = torch.zeros(
|
|
1,
|
|
1,
|
|
condition_latents.shape[2],
|
|
1,
|
|
1,
|
|
device=self.device,
|
|
dtype=self.dtype,
|
|
)
|
|
if current_conditional_frames > 0:
|
|
latent_frames = (current_conditional_frames - 1) // self.vae_scale_factor_temporal + 1
|
|
condition_mask[:, :, :latent_frames] = 1.0
|
|
latents = condition_mask * condition_latents + (1.0 - condition_mask) * noise
|
|
velocity_mask = 1.0 - condition_mask
|
|
return latents, velocity_mask, condition_mask * condition_latents
|
|
|
|
def _transfer_bucket_size(
|
|
self,
|
|
sp: OmniDiffusionSamplingParams,
|
|
source_hw: tuple[int, int] | None,
|
|
) -> tuple[int, int]:
|
|
resolution = self._get_sp_param(sp, "resolution", self._get_sp_param(sp, "image_size", 720))
|
|
source_h, source_w = source_hw or (COSMOS3_T2V_DEFAULT_HEIGHT, COSMOS3_T2V_DEFAULT_WIDTH)
|
|
target_w, target_h = find_closest_target_size(int(source_h), int(source_w), resolution)
|
|
return int(target_h), int(target_w)
|
|
|
|
@staticmethod
|
|
def _first_transfer_control_hw(transfer_config: Cosmos3TransferConfig) -> tuple[int, int] | None:
|
|
for hint in transfer_config.ordered_hints:
|
|
if hint.control is not None:
|
|
detected = media_hw(hint.control)
|
|
if detected is not None:
|
|
return detected
|
|
if hint.control_path is not None:
|
|
detected = media_hw(hint.control_path)
|
|
if detected is not None:
|
|
return detected
|
|
return None
|
|
|
|
def diffuse_transfer(
|
|
self,
|
|
latents: torch.Tensor,
|
|
timesteps: torch.Tensor,
|
|
cond_ids: torch.Tensor,
|
|
cond_mask: torch.Tensor,
|
|
uncond_ids: torch.Tensor,
|
|
uncond_mask: torch.Tensor,
|
|
guidance_scale: float,
|
|
control_guidance: float,
|
|
control_guidance_interval: tuple[float, float] | None,
|
|
control_latents: list[torch.Tensor],
|
|
shared_kwargs: dict[str, Any],
|
|
*,
|
|
velocity_mask: torch.Tensor,
|
|
condition_latents: torch.Tensor,
|
|
guidance_interval: tuple[float, float] | None = None,
|
|
) -> torch.Tensor:
|
|
def _active_at(t: torch.Tensor, interval: tuple[float, float] | None) -> bool:
|
|
if interval is None:
|
|
return True
|
|
t_scalar = float(t.item()) if torch.is_tensor(t) else float(t)
|
|
lo, hi = interval
|
|
return lo <= t_scalar <= hi
|
|
|
|
self.transformer.reset_cache()
|
|
self._cosmos3_branch_caches = {}
|
|
try:
|
|
for t in self.progress_bar(timesteps):
|
|
timestep = t.unsqueeze(0)
|
|
step_guidance = guidance_scale if _active_at(t, guidance_interval) else 1.0
|
|
step_control = control_guidance if _active_at(t, control_guidance_interval) else 1.0
|
|
needs_text_cfg = step_guidance > 1.0
|
|
needs_control_cfg = step_control != 1.0
|
|
|
|
cond_full_kwargs = dict(
|
|
hidden_states=latents,
|
|
timestep=timestep,
|
|
text_ids=cond_ids,
|
|
text_mask=cond_mask,
|
|
control_latents=control_latents,
|
|
_cosmos3_cache_key="transfer_cond_full",
|
|
**shared_kwargs,
|
|
)
|
|
if needs_control_cfg and needs_text_cfg:
|
|
branches_kwargs = [
|
|
cond_full_kwargs,
|
|
dict(
|
|
hidden_states=latents,
|
|
timestep=timestep,
|
|
text_ids=cond_ids,
|
|
text_mask=cond_mask,
|
|
control_latents=None,
|
|
_cosmos3_cache_key="transfer_cond_no_control",
|
|
**shared_kwargs,
|
|
),
|
|
dict(
|
|
hidden_states=latents,
|
|
timestep=timestep,
|
|
text_ids=uncond_ids,
|
|
text_mask=uncond_mask,
|
|
control_latents=control_latents,
|
|
_cosmos3_cache_key="transfer_uncond_full",
|
|
**shared_kwargs,
|
|
),
|
|
]
|
|
noise_pred = self.predict_noise_with_multi_branch_cfg(
|
|
do_true_cfg=True,
|
|
true_cfg_scale={
|
|
"mode": "cosmos3_transfer",
|
|
"branch_mode": "control_and_text",
|
|
"guidance_scale": step_guidance,
|
|
"control_guidance": step_control,
|
|
},
|
|
branches_kwargs=branches_kwargs,
|
|
cfg_normalize=False,
|
|
)
|
|
elif needs_control_cfg:
|
|
branches_kwargs = [
|
|
cond_full_kwargs,
|
|
dict(
|
|
hidden_states=latents,
|
|
timestep=timestep,
|
|
text_ids=cond_ids,
|
|
text_mask=cond_mask,
|
|
control_latents=None,
|
|
_cosmos3_cache_key="transfer_cond_no_control",
|
|
**shared_kwargs,
|
|
),
|
|
]
|
|
noise_pred = self.predict_noise_with_multi_branch_cfg(
|
|
do_true_cfg=True,
|
|
true_cfg_scale={
|
|
"mode": "cosmos3_transfer",
|
|
"branch_mode": "control_only",
|
|
"control_guidance": step_control,
|
|
},
|
|
branches_kwargs=branches_kwargs,
|
|
cfg_normalize=False,
|
|
)
|
|
elif needs_text_cfg:
|
|
branches_kwargs = [
|
|
cond_full_kwargs,
|
|
dict(
|
|
hidden_states=latents,
|
|
timestep=timestep,
|
|
text_ids=uncond_ids,
|
|
text_mask=uncond_mask,
|
|
control_latents=control_latents,
|
|
_cosmos3_cache_key="transfer_uncond_full",
|
|
**shared_kwargs,
|
|
),
|
|
]
|
|
noise_pred = self.predict_noise_with_multi_branch_cfg(
|
|
do_true_cfg=True,
|
|
true_cfg_scale={
|
|
"mode": "cosmos3_transfer",
|
|
"branch_mode": "text_only",
|
|
"guidance_scale": step_guidance,
|
|
},
|
|
branches_kwargs=branches_kwargs,
|
|
cfg_normalize=False,
|
|
)
|
|
else:
|
|
noise_pred = self.predict_noise(**cond_full_kwargs)
|
|
if isinstance(noise_pred, tuple):
|
|
raise ValueError("Cosmos3 transfer diffusion expects video-only tensor predictions.")
|
|
noise_pred = noise_pred * velocity_mask
|
|
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
|
|
latents = velocity_mask * latents + (1.0 - velocity_mask) * condition_latents
|
|
finally:
|
|
self._cosmos3_branch_caches = None
|
|
self.transformer.reset_cache()
|
|
return latents
|
|
|
|
def _forward_transfer(
|
|
self,
|
|
*,
|
|
prompt: str,
|
|
negative_prompt: str,
|
|
sp: OmniDiffusionSamplingParams,
|
|
transfer_config: Cosmos3TransferConfig,
|
|
transfer_video_tensor: torch.Tensor | None,
|
|
transfer_input_fps: float | None,
|
|
) -> DiffusionOutput:
|
|
input_frames = None
|
|
if transfer_video_tensor is not None:
|
|
input_frames = normalized_video_to_uint8_cthw(transfer_video_tensor)
|
|
source_hw = (int(input_frames.shape[-2]), int(input_frames.shape[-1]))
|
|
else:
|
|
source_hw = self._first_transfer_control_hw(transfer_config)
|
|
height, width = self._transfer_bucket_size(sp, source_hw)
|
|
|
|
if input_frames is not None:
|
|
if tuple(input_frames.shape[-2:]) != (height, width):
|
|
input_frames = resize_center_crop_uint8_cthw(input_frames, height, width)
|
|
input_frames = input_frames[:, : transfer_config.max_frames]
|
|
|
|
per_hint_frames: dict[str, torch.Tensor] = {}
|
|
for hint in transfer_config.ordered_hints:
|
|
frames = load_or_compute_control_frames(
|
|
hint,
|
|
height=height,
|
|
width=width,
|
|
max_frames=transfer_config.max_frames,
|
|
input_frames=input_frames,
|
|
)
|
|
if frames.shape[1] < 1:
|
|
raise ValueError(f"Cosmos3 transfer hint '{hint.key}' produced no frames.")
|
|
per_hint_frames[hint.key] = frames
|
|
if not per_hint_frames:
|
|
raise ValueError("Cosmos3 transfer requires at least one control hint.")
|
|
|
|
total_frames = next(iter(per_hint_frames.values())).shape[1]
|
|
if transfer_config.num_frames is not None:
|
|
total_frames = min(total_frames, int(transfer_config.num_frames))
|
|
total_frames = max(1, total_frames)
|
|
per_hint_frames = {key: pad_temporal_frames(frames, total_frames) for key, frames in per_hint_frames.items()}
|
|
if input_frames is not None:
|
|
input_frames = pad_temporal_frames(input_frames, total_frames)
|
|
|
|
temporal_compression = self.vae_scale_factor_temporal
|
|
chunk_frames = 1 if total_frames == 1 else transfer_config.num_video_frames_per_chunk
|
|
chunk_frames = math.ceil((chunk_frames - 1) / temporal_compression) * temporal_compression + 1
|
|
num_chunks, stride = self._get_transfer_num_chunks(
|
|
total_frames,
|
|
chunk_frames,
|
|
transfer_config.num_conditional_frames,
|
|
)
|
|
padded_frames = max(total_frames, chunk_frames)
|
|
per_hint_frames = {key: pad_temporal_frames(frames, padded_frames) for key, frames in per_hint_frames.items()}
|
|
if input_frames is not None:
|
|
input_frames = pad_temporal_frames(input_frames, padded_frames)
|
|
|
|
configured_frame_rate = positive_float(transfer_config.fps)
|
|
input_frame_rate = positive_float(transfer_input_fps)
|
|
sampling_frame_rate = (
|
|
positive_float(self._get_sp_param(sp, "resolved_frame_rate"))
|
|
or positive_float(self._get_sp_param(sp, "frame_rate"))
|
|
or positive_float(self._get_sp_param(sp, "fps"))
|
|
)
|
|
is_wsm_only = len(transfer_config.hints) == 1 and "wsm" in transfer_config.hints
|
|
if is_wsm_only:
|
|
frame_rate = configured_frame_rate or input_frame_rate or sampling_frame_rate or 24.0
|
|
else:
|
|
frame_rate = input_frame_rate or configured_frame_rate or sampling_frame_rate or 24.0
|
|
num_inference_steps = sp.num_inference_steps or COSMOS3_T2V_DEFAULT_NUM_INFERENCE_STEPS
|
|
guidance_scale = (
|
|
float(transfer_config.guidance_scale)
|
|
if transfer_config.guidance_scale is not None
|
|
else float(sp.guidance_scale or COSMOS3_T2V_DEFAULT_GUIDANCE_SCALE)
|
|
)
|
|
flow_shift_target = float(
|
|
transfer_config.flow_shift
|
|
if transfer_config.flow_shift is not None
|
|
else self._get_sp_param(sp, "flow_shift", COSMOS3_V2V_DEFAULT_FLOW_SHIFT)
|
|
)
|
|
max_sequence_length = (
|
|
self._get_sp_param(sp, "max_sequence_length", COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH)
|
|
or COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH
|
|
)
|
|
use_system_prompt = bool(self._get_sp_param(sp, "use_system_prompt", False))
|
|
|
|
self._guidance_scale = guidance_scale
|
|
self._num_timesteps = num_inference_steps
|
|
self._set_flow_shift(flow_shift_target, use_karras_sigmas=False)
|
|
|
|
generator = sp.generator
|
|
if generator is None:
|
|
seed = sp.seed if sp.seed is not None else 42
|
|
generator = torch.Generator(device=self.device).manual_seed(seed)
|
|
|
|
cond_ids, cond_mask, uncond_ids, uncond_mask = self._format_and_tokenize_prompts(
|
|
prompt,
|
|
negative_prompt,
|
|
chunk_frames,
|
|
frame_rate,
|
|
height,
|
|
width,
|
|
max_sequence_length,
|
|
sp,
|
|
use_system_prompt,
|
|
is_t2i=False,
|
|
)
|
|
|
|
output_chunks: list[torch.Tensor] = []
|
|
control_chunks_per_hint: dict[str, list[torch.Tensor]] = {key: [] for key in per_hint_frames}
|
|
previous_output: torch.Tensor | None = None
|
|
|
|
for chunk_id in range(num_chunks):
|
|
start_frame = chunk_id * stride
|
|
end_frame = min(start_frame + chunk_frames, total_frames)
|
|
control_norms = {
|
|
key: uint8_cthw_to_normalized_5d(
|
|
pad_temporal_frames(frames[:, start_frame:end_frame], chunk_frames),
|
|
dtype=self.dtype,
|
|
)
|
|
for key, frames in per_hint_frames.items()
|
|
}
|
|
target_norm = torch.zeros_like(next(iter(control_norms.values())))
|
|
current_conditional_frames = 0
|
|
|
|
if chunk_id == 0 and transfer_config.num_first_chunk_conditional_frames > 0:
|
|
if input_frames is None:
|
|
raise ValueError("Cosmos3 transfer num_first_chunk_conditional_frames > 0 requires a video input.")
|
|
current_conditional_frames = min(
|
|
transfer_config.num_first_chunk_conditional_frames,
|
|
input_frames.shape[1],
|
|
chunk_frames,
|
|
)
|
|
if current_conditional_frames > 0:
|
|
input_cond = uint8_cthw_to_normalized_5d(
|
|
input_frames[:, :current_conditional_frames],
|
|
dtype=self.dtype,
|
|
)
|
|
target_norm[:, :, :current_conditional_frames] = input_cond
|
|
if current_conditional_frames < chunk_frames:
|
|
fill = target_norm[:, :, current_conditional_frames - 1 : current_conditional_frames]
|
|
target_norm[:, :, current_conditional_frames:] = fill.expand(
|
|
-1,
|
|
-1,
|
|
chunk_frames - current_conditional_frames,
|
|
-1,
|
|
-1,
|
|
)
|
|
elif chunk_id > 0 and previous_output is not None:
|
|
current_conditional_frames = min(
|
|
transfer_config.num_conditional_frames,
|
|
previous_output.shape[2],
|
|
chunk_frames,
|
|
)
|
|
if current_conditional_frames > 0:
|
|
target_norm[:, :, :current_conditional_frames] = previous_output[
|
|
:, :, -current_conditional_frames:
|
|
].to(target_norm)
|
|
if current_conditional_frames < chunk_frames:
|
|
fill = target_norm[:, :, current_conditional_frames - 1 : current_conditional_frames]
|
|
target_norm[:, :, current_conditional_frames:] = fill.expand(
|
|
-1,
|
|
-1,
|
|
chunk_frames - current_conditional_frames,
|
|
-1,
|
|
-1,
|
|
)
|
|
|
|
control_latents = [self._encode_video_tensor(video) for video in control_norms.values()]
|
|
latents, velocity_mask, condition_latents = self._prepare_transfer_latents(
|
|
target_norm,
|
|
current_conditional_frames,
|
|
generator,
|
|
)
|
|
video_shape = (latents.shape[2], latents.shape[3], latents.shape[4])
|
|
shared_kwargs = dict(
|
|
video_shape=video_shape,
|
|
fps=frame_rate,
|
|
noisy_frame_mask=velocity_mask,
|
|
transfer_share_vision_temporal_positions=transfer_config.share_vision_temporal_positions,
|
|
)
|
|
|
|
self.scheduler.set_timesteps(num_inference_steps, device=self.device)
|
|
latents = self.diffuse_transfer(
|
|
latents=latents,
|
|
timesteps=self.scheduler.timesteps,
|
|
cond_ids=cond_ids,
|
|
cond_mask=cond_mask,
|
|
uncond_ids=uncond_ids,
|
|
uncond_mask=uncond_mask,
|
|
guidance_scale=guidance_scale,
|
|
control_guidance=transfer_config.control_guidance,
|
|
control_guidance_interval=transfer_config.control_guidance_interval,
|
|
control_latents=control_latents,
|
|
shared_kwargs=shared_kwargs,
|
|
velocity_mask=velocity_mask,
|
|
condition_latents=condition_latents,
|
|
)
|
|
output_video = self._decode_latents(latents).clamp(-1, 1)
|
|
previous_output = output_video
|
|
|
|
if chunk_id == 0:
|
|
output_chunks.append(output_video)
|
|
for key, control in control_norms.items():
|
|
control_chunks_per_hint[key].append(control)
|
|
else:
|
|
output_chunks.append(output_video[:, :, current_conditional_frames:])
|
|
for key, control in control_norms.items():
|
|
control_chunks_per_hint[key].append(control[:, :, current_conditional_frames:])
|
|
|
|
full_output = torch.cat(output_chunks, dim=2)[:, :, :total_frames]
|
|
full_controls = {
|
|
key: torch.cat(chunks, dim=2)[:, :, :total_frames] for key, chunks in control_chunks_per_hint.items()
|
|
}
|
|
|
|
if transfer_config.show_control_condition:
|
|
all_controls = torch.cat([full_controls[key] for key in per_hint_frames], dim=-1)
|
|
all_controls = all_controls.to(full_output)
|
|
full_output = torch.cat([all_controls, full_output], dim=-1)
|
|
if transfer_config.show_input and input_frames is not None:
|
|
normalized_input = uint8_cthw_to_normalized_5d(input_frames[:, :total_frames], dtype=torch.float32)
|
|
full_output = torch.cat([normalized_input.to(full_output), full_output], dim=-1)
|
|
|
|
return DiffusionOutput(
|
|
output={"video": full_output},
|
|
custom_output={
|
|
"transfer_controls": full_controls,
|
|
"transfer_hints": list(per_hint_frames),
|
|
"fps": frame_rate,
|
|
},
|
|
)
|
|
|
|
# -- Forward (main generation entry point) -------------------------------
|
|
|
|
def forward(
|
|
self,
|
|
req: DiffusionRequestBatch,
|
|
) -> DiffusionOutput:
|
|
pipeline_start = time.time()
|
|
|
|
# --- Parse request ---
|
|
prompt_data = req.prompts[0] if req.prompts else ""
|
|
if len(req.prompts) > 1:
|
|
raise ValueError("Cosmos3OmniDiffusersPipeline currently supports a single prompt per request.")
|
|
|
|
sp = req.sampling_params
|
|
robolab_inputs = self._build_robolab_policy_inputs(sp, prompt_data, getattr(req, "request_id", None))
|
|
if robolab_inputs is not None:
|
|
return self._forward_robolab_policy(sp, robolab_inputs, pipeline_start)
|
|
|
|
if isinstance(prompt_data, str):
|
|
prompt = prompt_data
|
|
negative_prompt = None
|
|
image_tensor = None
|
|
video_tensor = None
|
|
transfer_video_tensor = None
|
|
transfer_input_fps = None
|
|
else:
|
|
prompt = prompt_data.get("prompt", "")
|
|
negative_prompt = prompt_data.get("negative_prompt")
|
|
additional_info = prompt_data.get("additional_information", {}) or {}
|
|
image_tensor = additional_info.get("preprocessed_image")
|
|
video_tensor = additional_info.get("preprocessed_video")
|
|
transfer_video_tensor = additional_info.get("preprocessed_transfer_video")
|
|
transfer_input_fps = positive_float(additional_info.get("transfer_input_fps"))
|
|
|
|
is_t2i = self._is_t2i_request(req)
|
|
sound_enabled = self._is_sound_request(prompt_data, sp)
|
|
action_mode = self._get_action_mode(prompt_data, sp)
|
|
action_enabled = action_mode is not None
|
|
transfer_config = resolve_transfer_config(sp, prompt_data)
|
|
action_video_tensor = video_tensor if action_enabled else None
|
|
if transfer_config is not None:
|
|
if is_t2i:
|
|
raise ValueError("Cosmos3 transfer inference is supported only for video outputs.")
|
|
if action_enabled:
|
|
raise ValueError("Cosmos3 transfer inference cannot be combined with action generation.")
|
|
if sound_enabled:
|
|
raise ValueError("Cosmos3 transfer inference cannot be combined with sound generation.")
|
|
if action_enabled and is_t2i:
|
|
raise ValueError("Cosmos3 action generation is supported only for video outputs.")
|
|
if action_enabled and sound_enabled:
|
|
raise ValueError("Cosmos3 action+sound joint generation is not supported in this phase.")
|
|
if action_enabled and not getattr(self.transformer, "action_gen", False):
|
|
raise ValueError(
|
|
"Cosmos3 action generation was requested, but the transformer was "
|
|
"initialized without action modules. Check that the checkpoint config "
|
|
"enables action_gen and includes action weights."
|
|
)
|
|
if sound_enabled and is_t2i:
|
|
raise ValueError(
|
|
"Cosmos3 sound generation is supported only for video outputs in "
|
|
"this phase; text-to-image with sound is unsupported."
|
|
)
|
|
if sound_enabled and not getattr(self.transformer, "sound_gen", False):
|
|
raise ValueError(
|
|
"Cosmos3 sound generation was requested, but the transformer was "
|
|
"initialized without sound modules. Check that the checkpoint config "
|
|
"enables sound_gen or defines sound_dim and includes sound weights."
|
|
)
|
|
if negative_prompt is None:
|
|
negative_prompt = ""
|
|
if transfer_config is not None:
|
|
if image_tensor is not None:
|
|
raise ValueError("Cosmos3 transfer inference accepts video inputs or control_path values, not images.")
|
|
if transfer_video_tensor is None and video_tensor is not None:
|
|
transfer_video_tensor = video_tensor
|
|
return self._forward_transfer(
|
|
prompt=prompt,
|
|
negative_prompt=negative_prompt,
|
|
sp=sp,
|
|
transfer_config=transfer_config,
|
|
transfer_video_tensor=transfer_video_tensor,
|
|
transfer_input_fps=transfer_input_fps,
|
|
)
|
|
if image_tensor is not None and video_tensor is not None and not action_enabled:
|
|
raise ValueError("Cosmos3 non-action generation accepts either image or video input, not both.")
|
|
if video_tensor is not None and is_t2i:
|
|
raise ValueError("Cosmos3 video-to-video generation is supported only for video outputs.")
|
|
is_v2v = video_tensor is not None and not is_t2i and not action_enabled
|
|
|
|
# T2I and T2V share the same model + forward path; only defaults
|
|
# differ:
|
|
# T2I: 1024x1024, 50 steps, shift=3.0, guidance_interval=[400, 1000]
|
|
# T2V: 720x1280, 35 steps, shift=engine-init, no interval
|
|
if is_t2i:
|
|
height = sp.height or COSMOS3_T2I_DEFAULT_HEIGHT
|
|
width = sp.width or COSMOS3_T2I_DEFAULT_WIDTH
|
|
num_frames = 1
|
|
num_inference_steps = sp.num_inference_steps or COSMOS3_T2I_DEFAULT_NUM_INFERENCE_STEPS
|
|
guidance_scale = sp.guidance_scale if sp.guidance_scale else COSMOS3_T2I_DEFAULT_GUIDANCE_SCALE
|
|
default_flow_shift = COSMOS3_T2I_DEFAULT_FLOW_SHIFT
|
|
default_guidance_interval: tuple[float, float] | None = COSMOS3_T2I_DEFAULT_GUIDANCE_INTERVAL
|
|
batch_size = max(1, int(sp.num_outputs_per_prompt or 1))
|
|
else:
|
|
height = sp.height or COSMOS3_T2V_DEFAULT_HEIGHT
|
|
width = sp.width or COSMOS3_T2V_DEFAULT_WIDTH
|
|
num_frames = sp.num_frames or COSMOS3_T2V_DEFAULT_NUM_FRAMES
|
|
num_inference_steps = sp.num_inference_steps or COSMOS3_T2V_DEFAULT_NUM_INFERENCE_STEPS
|
|
guidance_scale = sp.guidance_scale if sp.guidance_scale else COSMOS3_T2V_DEFAULT_GUIDANCE_SCALE
|
|
# Fall back to the engine-init shift, NOT None: passing None
|
|
# to ``_set_flow_shift`` would leak a prior T2I rebuild
|
|
# (shift=3.0) into a subsequent video request.
|
|
default_flow_shift = COSMOS3_V2V_DEFAULT_FLOW_SHIFT if is_v2v else self._engine_init_flow_shift
|
|
default_guidance_interval = None
|
|
batch_size = 1 # Existing video pipeline assumes B=1.
|
|
|
|
if action_enabled:
|
|
action_chunk_param = self._get_sp_param(sp, "action_chunk_size", None)
|
|
if action_chunk_param is not None:
|
|
action_chunk_size = int(action_chunk_param)
|
|
if sp.num_frames is None:
|
|
num_frames = action_chunk_size + 1
|
|
elif sp.num_frames is None:
|
|
action_chunk_size = 16
|
|
num_frames = action_chunk_size + 1
|
|
else:
|
|
action_chunk_size = int(num_frames) - 1
|
|
if action_chunk_size <= 0:
|
|
raise ValueError(f"Cosmos3 action_chunk_size must be positive, got {action_chunk_size}.")
|
|
if num_frames not in (action_chunk_size, action_chunk_size + 1):
|
|
raise ValueError(
|
|
"Cosmos3 action requests require num_frames to equal action_chunk_size "
|
|
f"or action_chunk_size + 1; got num_frames={num_frames}, action_chunk_size={action_chunk_size}."
|
|
)
|
|
num_inference_steps = sp.num_inference_steps or 30
|
|
guidance_scale = sp.guidance_scale if sp.guidance_scale is not None else 1.0
|
|
default_flow_shift = 5.0
|
|
|
|
domain_id = None
|
|
if action_enabled:
|
|
domain_id = resolve_domain_id(
|
|
domain_id=self._get_sp_param(sp, "domain_id", None),
|
|
domain_name=self._get_sp_param(sp, "domain_name", None),
|
|
require_explicit=True,
|
|
)
|
|
|
|
# Runtime controls: prefer ``extra_args`` (OpenAI endpoints write
|
|
# there) over direct attrs.
|
|
flow_shift_target = float(self._get_sp_param(sp, "flow_shift", default_flow_shift))
|
|
guidance_interval = self._get_sp_param(sp, "guidance_interval", default_guidance_interval)
|
|
|
|
frame_rate = self._get_sp_param(sp, "resolved_frame_rate") or self._get_sp_param(sp, "frame_rate") or 24.0
|
|
max_sequence_length = (
|
|
self._get_sp_param(sp, "max_sequence_length", COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH)
|
|
or COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH
|
|
)
|
|
use_system_prompt = bool(self._get_sp_param(sp, "use_system_prompt", is_v2v))
|
|
|
|
if action_enabled and action_video_tensor is None:
|
|
extra_action_video = self._get_sp_param(sp, "action_video", None)
|
|
if isinstance(extra_action_video, torch.Tensor):
|
|
action_video_tensor = extra_action_video
|
|
if action_enabled and isinstance(action_video_tensor, torch.Tensor):
|
|
if action_video_tensor.ndim == 4:
|
|
action_video_tensor = action_video_tensor.unsqueeze(0)
|
|
if action_video_tensor.ndim != 5:
|
|
raise ValueError(
|
|
"Cosmos3 extra_args['action_video'] must have shape [1, 3, T, H, W] "
|
|
f"or [3, T, H, W], got {tuple(action_video_tensor.shape)}."
|
|
)
|
|
if sp.height is None:
|
|
height = int(action_video_tensor.shape[-2])
|
|
if sp.width is None:
|
|
width = int(action_video_tensor.shape[-1])
|
|
|
|
self._guidance_scale = guidance_scale
|
|
self._num_timesteps = num_inference_steps
|
|
|
|
# Always resolve to a concrete target shift for this request, then
|
|
# update the shared Diffusers scheduler.
|
|
self._set_flow_shift(flow_shift_target, use_karras_sigmas=False if is_v2v else None)
|
|
|
|
generator = sp.generator
|
|
if generator is None:
|
|
seed = sp.seed if sp.seed is not None else 42
|
|
generator = torch.Generator(device=self.device).manual_seed(seed)
|
|
|
|
# --- Format prompts & tokenize (B=1; reused across loop iterations
|
|
# for T2I num_outputs_per_prompt > 1) ---
|
|
cond_ids, cond_mask, uncond_ids, uncond_mask = self._format_and_tokenize_prompts(
|
|
prompt,
|
|
negative_prompt,
|
|
num_frames,
|
|
frame_rate,
|
|
height,
|
|
width,
|
|
max_sequence_length,
|
|
sp,
|
|
use_system_prompt,
|
|
is_t2i=is_t2i,
|
|
)
|
|
|
|
# --- Prepare latents (T2I, T2V, or I2V) ---
|
|
# T2I shares _prepare_latents with T2V; the math collapses cleanly
|
|
# at num_frames=1 ((1-1)//4 + 1 = 1 latent frame). For T2I with
|
|
# ``num_outputs_per_prompt > 1`` we loop the diffusion below;
|
|
# batching B=N together would require expanding text K/V (UND
|
|
# pathway is text-only and cached) and is left as a future
|
|
# optimization.
|
|
action_latents = None
|
|
action_velocity_mask = None
|
|
action_condition_latents = None
|
|
raw_action_dim = None
|
|
action_offset = 1
|
|
if action_enabled:
|
|
if action_video_tensor is not None and action_video_tensor.ndim == 4:
|
|
action_video_tensor = action_video_tensor.unsqueeze(0)
|
|
if action_video_tensor is not None and action_video_tensor.ndim != 5:
|
|
raise ValueError(
|
|
"Cosmos3 action video tensor must have shape [1, 3, T, H, W] "
|
|
f"or [3, T, H, W], got {tuple(action_video_tensor.shape)}."
|
|
)
|
|
if action_video_tensor is not None and action_video_tensor.shape[2] < num_frames:
|
|
pad = action_video_tensor[:, :, -1:].repeat(1, 1, num_frames - action_video_tensor.shape[2], 1, 1)
|
|
action_video_tensor = torch.cat([action_video_tensor, pad], dim=2)
|
|
elif action_video_tensor is not None and action_video_tensor.shape[2] > num_frames:
|
|
action_video_tensor = action_video_tensor[:, :, :num_frames]
|
|
|
|
if action_mode == ACTION_MODE_INVERSE_DYNAMICS and action_video_tensor is None:
|
|
raise ValueError("Cosmos3 inverse_dynamics action mode requires multi_modal_data['video'].")
|
|
if action_mode in {ACTION_MODE_POLICY, ACTION_MODE_FORWARD_DYNAMICS} and image_tensor is None:
|
|
if action_video_tensor is None:
|
|
raise ValueError(
|
|
f"Cosmos3 action_mode={action_mode!r} requires multi_modal_data['image'] "
|
|
"or multi_modal_data['video']."
|
|
)
|
|
image_tensor = action_video_tensor[:, :, 0]
|
|
|
|
raw_action_dim_param = self._get_sp_param(sp, "raw_action_dim", None)
|
|
raw_action_dim = int(raw_action_dim_param) if raw_action_dim_param is not None else None
|
|
clean_action = None
|
|
action_condition_indexes = None
|
|
action_prepared = self._prepare_action_latents(
|
|
mode=action_mode,
|
|
action_chunk_size=action_chunk_size,
|
|
raw_action_dim=raw_action_dim,
|
|
generator=generator,
|
|
sp=sp,
|
|
clean_action=clean_action,
|
|
condition_indexes=action_condition_indexes,
|
|
)
|
|
action_latents, action_velocity_mask, action_condition_latents, raw_action_dim = action_prepared
|
|
action_offset = action_start_frame_offset(action_mode, action_chunk_size, num_frames)
|
|
|
|
if action_enabled and action_video_tensor is not None:
|
|
latents, velocity_mask, condition_latents = self._prepare_latents_action_video(
|
|
action_video_tensor,
|
|
action_mode,
|
|
height,
|
|
width,
|
|
num_frames,
|
|
generator,
|
|
)
|
|
image_latent = condition_latents[:, :, 0:1]
|
|
elif is_v2v:
|
|
condition_frame_indexes_vision = normalize_condition_frame_indexes_vision(
|
|
self._get_sp_param(
|
|
sp,
|
|
"condition_frame_indexes_vision",
|
|
self._get_prompt_param(prompt_data, "condition_frame_indexes_vision", None),
|
|
)
|
|
)
|
|
latents, velocity_mask, condition_latents = self._prepare_latents_v2v(
|
|
video_tensor,
|
|
height,
|
|
width,
|
|
num_frames,
|
|
generator,
|
|
condition_frame_indexes_vision,
|
|
)
|
|
image_latent = None
|
|
elif image_tensor is not None and not is_t2i:
|
|
latents, velocity_mask, image_latent = self._prepare_latents_i2v(
|
|
image_tensor,
|
|
height,
|
|
width,
|
|
num_frames,
|
|
generator,
|
|
)
|
|
condition_latents = None
|
|
else:
|
|
latents = self._prepare_latents(height, width, num_frames, generator)
|
|
velocity_mask = None
|
|
image_latent = None
|
|
condition_latents = None
|
|
|
|
T_latent = latents.shape[2]
|
|
H_latent = latents.shape[3]
|
|
W_latent = latents.shape[4]
|
|
video_shape = (T_latent, H_latent, W_latent)
|
|
|
|
sound_latents = None
|
|
target_audio_samples = None
|
|
sound_sample_rate = None
|
|
if sound_enabled:
|
|
target_audio_samples, _, sound_sample_rate = self._resolve_sound_target_samples(sp, num_frames, frame_rate)
|
|
sound_latents, _ = self._prepare_sound_latents(
|
|
target_audio_samples,
|
|
generator,
|
|
sp_video_shape=video_shape,
|
|
)
|
|
|
|
# --- Denoising loop ---
|
|
shared_kwargs = dict(video_shape=video_shape, fps=frame_rate)
|
|
if velocity_mask is not None:
|
|
shared_kwargs["noisy_frame_mask"] = velocity_mask
|
|
if action_enabled:
|
|
shared_kwargs.update(
|
|
action_domain_ids=torch.tensor([domain_id], dtype=torch.long, device=self.device),
|
|
action_noisy_mask=action_velocity_mask,
|
|
action_start_frame_offset=action_offset,
|
|
action_fps=float(self._get_sp_param(sp, "action_fps", frame_rate) or frame_rate),
|
|
)
|
|
|
|
def _run_diffusion(start_latents):
|
|
self.scheduler.set_timesteps(num_inference_steps, device=self.device)
|
|
scheduler = self.scheduler
|
|
return self.diffuse(
|
|
latents=start_latents,
|
|
timesteps=scheduler.timesteps,
|
|
cond_ids=cond_ids,
|
|
cond_mask=cond_mask,
|
|
uncond_ids=uncond_ids,
|
|
uncond_mask=uncond_mask,
|
|
guidance_scale=guidance_scale,
|
|
shared_kwargs=shared_kwargs,
|
|
action_latents=action_latents,
|
|
action_velocity_mask=action_velocity_mask,
|
|
action_condition_latents=action_condition_latents,
|
|
sound_latents=sound_latents,
|
|
velocity_mask=velocity_mask,
|
|
image_latent=image_latent,
|
|
condition_latents=condition_latents,
|
|
guidance_interval=guidance_interval,
|
|
raw_action_dim=raw_action_dim,
|
|
scheduler=scheduler,
|
|
)
|
|
|
|
if is_t2i and batch_size > 1:
|
|
# Generate N independent images by re-running the full diffusion
|
|
# loop with different noise seeds. The first sample reuses
|
|
# ``latents`` already drawn from ``generator``; subsequent
|
|
# samples draw fresh noise from the same generator (state
|
|
# advances per call), giving distinct outputs from a single
|
|
# user-provided seed. Batched B=N would be more efficient but
|
|
# requires expanding cached UND text K/V to match.
|
|
samples = [_run_diffusion(latents)]
|
|
for _ in range(batch_size - 1):
|
|
next_latents = self._prepare_latents(height, width, num_frames, generator)
|
|
samples.append(_run_diffusion(next_latents))
|
|
latents = torch.cat(samples, dim=0)
|
|
else:
|
|
diffusion_output = _run_diffusion(latents)
|
|
if action_enabled and sound_enabled:
|
|
latents, action_latents, sound_latents = diffusion_output
|
|
elif action_enabled:
|
|
latents, action_latents = diffusion_output
|
|
elif sound_enabled:
|
|
latents, sound_latents = diffusion_output
|
|
else:
|
|
latents = diffusion_output
|
|
|
|
# --- Decode ---
|
|
if _is_rank_zero():
|
|
logger.info("Decoding video...")
|
|
decode_start = time.time()
|
|
video = self._decode_latents(latents)
|
|
if _is_rank_zero():
|
|
logger.info("Video decoded in %.2fs", time.time() - decode_start)
|
|
if not sound_enabled:
|
|
logger.info("Total pipeline time: %.2fs", time.time() - pipeline_start)
|
|
|
|
if sound_enabled:
|
|
if sound_latents is None or target_audio_samples is None or sound_sample_rate is None:
|
|
raise ValueError("Cosmos3 sound generation finished without sound latents.")
|
|
if _is_rank_zero():
|
|
logger.info("Decoding sound...")
|
|
sound_decode_start = time.time()
|
|
audio = self._decode_sound_latents(sound_latents, target_audio_samples)
|
|
if _is_rank_zero():
|
|
logger.info("Sound tokenizer decoded in %.2fs", time.time() - sound_decode_start)
|
|
logger.info("Total pipeline time: %.2fs", time.time() - pipeline_start)
|
|
return DiffusionOutput(output={"video": video, "audio": audio, "audio_sample_rate": sound_sample_rate})
|
|
|
|
if action_enabled:
|
|
if action_latents is None or raw_action_dim is None or domain_id is None:
|
|
raise ValueError("Cosmos3 action generation finished without action latents.")
|
|
action = action_latents[:, :, :raw_action_dim].detach().cpu()
|
|
return DiffusionOutput(
|
|
output={"video": video},
|
|
custom_output={
|
|
"action": action,
|
|
"raw_action_dim": raw_action_dim,
|
|
"action_mode": action_mode,
|
|
"domain_id": domain_id,
|
|
},
|
|
)
|
|
|
|
return DiffusionOutput(output={"image": video} if is_t2i else {"video": video})
|