Files

4176 lines
187 KiB
Python
Raw Permalink Normal View History

2026-06-01 22:04:31 +02:00
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
2026-06-27 00:39:42 +02:00
"""Cosmos3 text/image/video/sound/action pipeline for vllm-omni.
2026-06-01 22:04:31 +02:00
2026-06-27 00:39:42 +02:00
One pipeline class serves the Cosmos3 family modes. Output modality is selected
mainly by ``prompt["modalities"]``:
2026-06-01 22:04:31 +02:00
2026-06-27 00:39:42 +02:00
* ``"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``.
2026-06-01 22:04:31 +02:00
2026-06-27 00:39:42 +02:00
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 an action-only payload/metadata
envelope.
2026-06-27 00:39:42 +02:00
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"]``.
2026-06-01 22:04:31 +02:00
"""
from __future__ import annotations
import json
2026-06-03 02:51:44 +02:00
import math
2026-06-01 22:04:31 +02:00
import os
import time
2026-06-21 03:59:18 +02:00
from collections.abc import Iterable, Mapping
from contextlib import nullcontext
from dataclasses import fields
2026-06-01 22:04:31 +02:00
from typing import Any, ClassVar
import numpy as np
import PIL.Image
import torch
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_rank,
2026-06-01 22:04:31 +02:00
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
2026-06-08 21:04:05 +02:00
from vllm_omni.diffusion.models.interface import (
ReferenceVideoDecodeSpec,
SupportImageInput,
SupportsComponentDiscovery,
2026-06-08 21:04:05 +02:00
)
2026-06-01 22:04:31 +02:00
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin, _is_rank_zero
from vllm_omni.diffusion.models.schedulers.scheduling_flow_match_euler_discrete import (
FlowMatchEulerDiscreteScheduler,
)
from vllm_omni.diffusion.models.schedulers.scheduling_flow_unipc_multistep import (
FlowUniPCMultistepScheduler,
)
from vllm_omni.diffusion.offloader.config import OffloadStrategy, resolve_offload
2026-06-01 22:04:31 +02:00
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.utils.video_decode import decode_path_video_frames, is_video_file_path
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
2026-06-21 03:59:18 +02:00
from vllm_omni.entrypoints.openai.video_api_utils import positive_float
from vllm_omni.experimental.world_models.adapters.state_cosmos3_adapter import (
Cosmos3StateAdapter,
)
from vllm_omni.experimental.world_models.session_state import (
SessionStateManager,
resolve_session_state_config,
)
2026-06-01 22:04:31 +02:00
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
2026-06-03 21:30:39 +02:00
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,
2026-06-03 21:30:39 +02:00
)
2026-06-21 03:59:18 +02:00
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, _tf_config_get, resolve_sound_gen
from .transformer_cosmos3_edge import COSMOS3_EDGE_BACKBONE_TYPE, Cosmos3EdgeVFMTransformer
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,
VIDEO_RES_SIZE_INFO,
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,
2026-09-10 16:32:50 +02:00
get_robolab_domain_id,
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,
)
2026-06-01 22:04:31 +02:00
logger = init_logger(__name__)
2026-06-08 21:04:05 +02:00
COSMOS3_DEFAULT_CONDITION_PIXEL_FRAMES = (
max(COSMOS3_DEFAULT_CONDITION_FRAME_INDEXES_VISION) * COSMOS3_VAE_TEMPORAL_COMPRESSION + 1
)
COSMOS3_V2V_DEFAULT_FLOW_SHIFT = 10.0
2026-06-01 22:04:31 +02:00
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."
2026-06-21 16:20:30 +02:00
# 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."
2026-08-20 20:40:51 +02:00
COSMOS3_TRANSFER_SYSTEM_PROMPT = (
"You are a helpful assistant that generates images or videos following the user's instructions"
" and control signals (edge maps, blur, depth, or segmentation)."
)
COSMOS3_TRANSFER_CONTROL_DIRECTIVE_TEMPLATE = (
"Follow the {hint_names} control video precisely: shape, contour, silhouette, position, and motion of every "
"visible structure must align with the {hint_names} signal at every frame."
)
2026-06-01 22:04:31 +02:00
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_VIDEO_DEFAULT_FLOW_SHIFT = 10.0
2026-06-01 22:04:31 +02:00
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)
COSMOS3_EDGE_T2V_DEFAULT_HEIGHT = 480
COSMOS3_EDGE_T2V_DEFAULT_WIDTH = 832
COSMOS3_EDGE_T2V_DEFAULT_GUIDANCE_SCALE = 5.0
COSMOS3_EDGE_VIDEO_DEFAULT_FLOW_SHIFT = 3.0
COSMOS3_EDGE_T2I_DEFAULT_HEIGHT = 640
COSMOS3_EDGE_T2I_DEFAULT_WIDTH = 640
COSMOS3_DISTILLED_CHECKPOINT_SCHEDULER_CLASS = "FlowMatchEulerDiscreteScheduler"
2026-06-01 22:04:31 +02:00
# 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
COSMOS3_TRANSFER_REQUESTED_SIZE_KEY = "_cosmos3_transfer_requested_size"
2026-06-01 22:04:31 +02:00
def _ceil_video_num_frames(num_frames: int, temporal_compression_factor: int) -> int:
"""Round a video length up to the causal VAE's ``factor * k + 1`` grid."""
if temporal_compression_factor <= 0:
raise ValueError(f"Cosmos3 temporal_compression_factor must be positive, got {temporal_compression_factor}.")
if num_frames <= 1:
return num_frames
latent_frames = math.ceil((num_frames - 1) / temporal_compression_factor)
return latent_frames * temporal_compression_factor + 1
def _format_json_object_prompt(
prompt: str,
*,
num_frames: int,
frame_rate: float,
height: int,
width: int,
aspect_ratio: str | None,
) -> str | None:
"""Match Imaginaire4 metadata injection for JSON-object prompts."""
try:
prompt_obj = json.loads(prompt)
except (json.JSONDecodeError, TypeError, ValueError):
return None
if not isinstance(prompt_obj, dict):
return None
metadata: dict[str, Any] = {}
if num_frames > 1:
duration_seconds = int(num_frames / frame_rate) if frame_rate > 0 else 0
metadata.update({"duration": f"{duration_seconds}s", "fps": float(frame_rate)})
else:
prompt_obj.pop("duration", None)
prompt_obj.pop("fps", None)
metadata["resolution"] = {"H": int(height), "W": int(width)}
if aspect_ratio is not None:
metadata["aspect_ratio"] = aspect_ratio
prompt_obj.update(metadata)
return json.dumps(prompt_obj)
def _json_object_aspect_ratio(prompt: str) -> Any | None:
"""Return an aspect-ratio field from a JSON-object prompt, if present."""
try:
prompt_obj = json.loads(prompt)
except (json.JSONDecodeError, TypeError, ValueError):
return None
if not isinstance(prompt_obj, dict):
return None
return prompt_obj.get("aspect_ratio")
def _normalize_aspect_ratio(value: Any) -> str | None:
"""Normalize supported ratio spellings for comparison and prompt metadata."""
if value is None:
return None
parts = str(value).strip().replace(":", ",").split(",")
if len(parts) != 2:
return None
try:
width, height = (int(part.strip()) for part in parts)
except ValueError:
return None
if width <= 0 or height <= 0:
return None
divisor = math.gcd(width, height)
return f"{width // divisor},{height // divisor}"
def _aspect_ratio_for_dimensions(width: int, height: int) -> str:
"""Return the nearest supported Cosmos aspect-ratio label."""
if width <= 0 or height <= 0:
raise ValueError(f"Cosmos3 canvas dimensions must be positive, got {width}x{height}.")
canvas_ratio = width / height
def ratio_value(label: str) -> float:
ratio_width, ratio_height = (int(part) for part in label.split(","))
return ratio_width / ratio_height
return min(
(label for sizes in VIDEO_RES_SIZE_INFO.values() for label in sizes),
key=lambda label: abs(canvas_ratio - ratio_value(label)),
)
def resolve_cosmos3_transformer_cls(model_config: Any) -> type[Cosmos3VFMTransformer]:
"""Select the Cosmos3 transformer implementation from transformer/config.json."""
backbone_type = _tf_config_get(model_config, "backbone_type", None)
if backbone_type is None:
return Cosmos3VFMTransformer
if backbone_type == COSMOS3_EDGE_BACKBONE_TYPE:
return Cosmos3EdgeVFMTransformer
raise ValueError(f"Unsupported Cosmos3 transformer backbone_type={backbone_type!r}.")
2026-06-01 22:04:31 +02:00
# ---------------------------------------------------------------------------
# Post-process function (registered in registry.py)
# ---------------------------------------------------------------------------
def get_cosmos3_pre_process_func(od_config: OmniDiffusionConfig):
2026-06-27 00:39:42 +02:00
"""Build the request preprocessor for Cosmos3 image/video inputs.
2026-06-01 22:04:31 +02:00
2026-06-27 00:39:42 +02:00
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.
2026-06-01 22:04:31 +02:00
"""
from .guardrails import check_text_safety, ensure_initialized, is_guardrails_enabled
video_processor = VideoProcessor(vae_scale_factor=16)
is_edge_model = (
_tf_config_get(getattr(od_config, "tf_model_config", None), "backbone_type", None) == COSMOS3_EDGE_BACKBONE_TYPE
)
2026-06-01 22:04:31 +02:00
# 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)
2026-06-03 21:30:39 +02:00
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"))
2026-06-21 03:59:18 +02:00
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)
2026-06-03 21:30:39 +02:00
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)
2026-06-01 22:04:31 +02:00
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")
2026-06-08 21:04:05 +02:00
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}."
)
2026-06-03 21:30:39 +02:00
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()
2026-06-01 22:04:31 +02:00
2026-06-08 21:04:05 +02:00
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)
2026-06-21 03:59:18 +02:00
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 _decode_video_file_to_frames(
path: str | os.PathLike,
*,
max_frames: int | None,
keep: str,
) -> list[PIL.Image.Image]:
rgb_frames = decode_path_video_frames(path, max_frames=max_frames, keep=keep)
return [PIL.Image.fromarray(frame).convert("RGB") for frame in rgb_frames]
def _expand_video_payload_item(
item: Any,
*,
max_frames: int | None,
keep: str,
) -> list[Any]:
if is_video_file_path(item):
return _decode_video_file_to_frames(item, max_frames=max_frames, keep=keep)
return [item]
def _video_payload_to_frames(
video: Any,
*,
max_frames: int | None,
keep: str,
) -> list[Any]:
2026-06-21 03:59:18 +02:00
video = _unwrap_video_payload(video)
if is_video_file_path(video):
return _decode_video_file_to_frames(video, max_frames=max_frames, keep=keep)
2026-06-08 21:04:05 +02:00
if isinstance(video, list):
frames: list[Any] = []
for item in video:
frames.extend(_expand_video_payload_item(item, max_frames=max_frames, keep=keep))
return frames
2026-06-08 21:04:05 +02:00
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()
2026-06-01 22:04:31 +02:00
def pre_process_func(request: OmniDiffusionRequest) -> OmniDiffusionRequest:
2026-06-03 21:30:39 +02:00
action_mode = _request_action_mode(request)
prompt = request.prompt
2026-06-01 22:04:31 +02:00
if is_guardrails_enabled(od_config, request.sampling_params):
text = prompt if isinstance(prompt, str) else prompt.get("prompt", "")
check_text_safety(text)
2026-06-01 22:04:31 +02:00
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.")
2026-06-01 22:04:31 +02:00
if "additional_information" not in prompt:
prompt["additional_information"] = {}
2026-06-01 22:04:31 +02:00
extra = _extra_args(request)
transfer_requested = action_mode is None and has_transfer_hints(extra)
condition_frame_indexes_vision = normalize_condition_frame_indexes_vision(
extra.get("condition_frame_indexes_vision", prompt.get("condition_frame_indexes_vision"))
)
condition_video_keep = normalize_condition_video_keep(
extra.get("condition_video_keep", prompt.get("condition_video_keep"))
)
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)
decode_extra = dict(extra)
decode_extra["condition_frame_indexes_vision"] = list(condition_frame_indexes_vision)
decode_extra["condition_video_keep"] = condition_video_keep
decode_spec = Cosmos3OmniDiffusersPipeline.reference_video_decode_spec(
num_frames=getattr(request.sampling_params, "num_frames", None),
extra_args=decode_extra,
)
raw_video_frames = _video_payload_to_frames(
raw_video,
max_frames=decode_spec.max_frames,
keep=decode_spec.keep,
)
if not raw_video_frames:
raise TypeError("Cosmos3 video input must be a non-empty list of PIL images or image paths.")
2026-06-08 21:04:05 +02:00
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)
2026-06-01 22:04:31 +02:00
# Resolve missing H/W.
if transfer_requested:
requested_width = request.sampling_params.width
requested_height = request.sampling_params.height
if (
requested_width is not None
and requested_height is not None
and COSMOS3_TRANSFER_REQUESTED_SIZE_KEY not in extra
):
extra[COSMOS3_TRANSFER_REQUESTED_SIZE_KEY] = {
"width": int(requested_width),
"height": int(requested_height),
}
_set_transfer_size_from_image(request, image)
elif request.sampling_params.height is None or request.sampling_params.width is None:
2026-06-03 21:30:39 +02:00
if action_mode is not None:
_set_action_size_from_image(request, image)
elif is_edge_model:
if request.sampling_params.height is None:
request.sampling_params.height = COSMOS3_EDGE_T2V_DEFAULT_HEIGHT
if request.sampling_params.width is None:
request.sampling_params.width = COSMOS3_EDGE_T2V_DEFAULT_WIDTH
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),
2026-06-03 21:30:39 +02:00
)
prompt["additional_information"]["preprocessed_transfer_video"] = uint8_cthw_to_normalized_5d(
transfer_frames,
dtype=torch.float32,
2026-06-03 21:30:39 +02:00
)
2026-06-08 21:04:05 +02:00
else:
max_frames = condition_pixel_frame_count(condition_frame_indexes_vision)
prompt["additional_information"]["preprocessed_video"] = _preprocess_condition_video(
2026-06-08 21:04:05 +02:00
raw_video_frames,
2026-06-03 21:30:39 +02:00
int(target_h),
int(target_w),
max_frames,
condition_video_keep,
2026-06-03 21:30:39 +02:00
)
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
2026-06-01 22:04:31 +02:00
return request
return pre_process_func
def get_cosmos3_post_process_func(od_config: OmniDiffusionConfig):
2026-06-27 00:39:42 +02:00
"""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.
"""
2026-06-01 22:04:31 +02:00
from .guardrails import check_video_safety, is_guardrails_enabled
video_processor = VideoProcessor(vae_scale_factor=16)
2026-06-03 02:51:44 +02:00
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
2026-06-01 22:04:31 +02:00
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
def _postprocess_action(action: Any, metadata: dict[str, Any]) -> Any:
internal_metadata = metadata.get("internal")
inputs = (
internal_metadata.get("robolab_action_postprocess") if isinstance(internal_metadata, dict) else None
)
if isinstance(inputs, RoboLabActionPostprocessInputs):
return postprocess_robolab_action(action, inputs)
return action
pending_action = None
pending_action_metadata: dict[str, Any] = {}
envelope_public_metadata: dict[str, Any] = {}
if isinstance(output, dict) and isinstance(output.get("payload"), dict):
envelope_payload = dict(output.get("payload") or {})
metadata = output.get("metadata") or {}
envelope_metadata = metadata if isinstance(metadata, dict) else {}
envelope_public_metadata = {key: value for key, value in envelope_metadata.items() if key != "internal"}
action = envelope_payload.pop("actions", None)
if action is not None:
pending_action = _postprocess_action(action, envelope_metadata)
pending_action_metadata = envelope_public_metadata
if not envelope_payload:
return {
"payload": {
"video": [],
"actions": pending_action,
},
"metadata": pending_action_metadata,
}
output = envelope_payload
2026-06-03 02:51:44 +02:00
audio = None
audio_sample_rate = None
2026-06-01 22:04:31 +02:00
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.")
2026-06-03 02:51:44 +02:00
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."
)
2026-06-01 22:04:31 +02:00
else:
video = output
if isinstance(output, dict) and "image" in output:
2026-06-03 02:51:44 +02:00
if audio is not None:
raise ValueError("Cosmos3 text-to-image postprocess does not support audio output.")
2026-06-01 22:04:31 +02:00
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)
processed_image = video_processor.postprocess(image, output_type="pil")
if envelope_public_metadata:
return {
"payload": {"image": processed_image},
"metadata": envelope_public_metadata,
}
return processed_image
guardrails_enabled = is_guardrails_enabled(od_config, sampling_params)
if guardrails_enabled:
2026-06-01 22:04:31 +02:00
video = check_video_safety(video)
2026-06-03 02:51:44 +02:00
processed_video = video_processor.postprocess_video(video, output_type=output_type)
if audio is None:
if pending_action is not None:
return {
"payload": {
"video": processed_video,
"actions": pending_action,
},
"metadata": pending_action_metadata,
}
if envelope_public_metadata:
return {
"payload": {"video": processed_video},
"metadata": envelope_public_metadata,
}
2026-06-03 02:51:44 +02:00
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)
if pending_action is not None:
return {
"payload": {
"video": result["video"],
"audio": result["audio"],
"actions": pending_action,
},
"metadata": {
**pending_action_metadata,
"video": {"fps": result["fps"]},
"audio": {"sample_rate": result.get("audio_sample_rate")},
},
}
if envelope_public_metadata:
return {
"payload": {
"video": result["video"],
"audio": result["audio"],
},
"metadata": {
**envelope_public_metadata,
"video": {"fps": result["fps"], **envelope_public_metadata.get("video", {})}
if isinstance(envelope_public_metadata.get("video"), dict)
else {"fps": result["fps"]},
"audio": {"sample_rate": result.get("audio_sample_rate")},
},
}
2026-06-03 02:51:44 +02:00
return result
2026-06-01 22:04:31 +02:00
return 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
2026-06-01 22:04:31 +02:00
# ---------------------------------------------------------------------------
# Pipeline
# ---------------------------------------------------------------------------
class Cosmos3OmniDiffusersPipeline(
nn.Module,
CFGParallelMixin,
SupportImageInput,
SupportsComponentDiscovery,
ProgressBarMixin,
DiffusionPipelineProfilerMixin,
2026-06-01 22:04:31 +02:00
):
2026-06-27 00:39:42 +02:00
"""Cosmos3 text/image/video/sound/action pipeline.
2026-06-01 22:04:31 +02:00
Architecture: Mixture-of-Transformers with Qwen3-VL backbone.
- Understanding pathway: causal self-attention on text (runs once, K/V cached)
2026-06-27 00:39:42 +02:00
- Generation pathway: cross-attention on visual latents and optional
transfer-control, action, and sound latents (runs each step)
2026-06-01 22:04:31 +02:00
2026-06-27 00:39:42 +02:00
Supports T2V, I2V, V2V, T2I, transfer, sound-enabled video, and action
generation from the same class. Mode is selected at runtime:
2026-06-01 22:04:31 +02:00
* **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.
2026-06-08 21:04:05 +02:00
* **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.
2026-06-27 00:39:42 +02:00
* **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 the diffusion
output payload/metadata envelope.
2026-06-27 00:39:42 +02:00
RoboLab/OpenPI observations in ``extra_args['robot_obs']`` or
``extra_args['observation']`` return an action-only envelope.
2026-06-01 22:04:31 +02:00
* **T2V** otherwise (default video generation).
"""
support_image_input: ClassVar[bool] = True
color_format: ClassVar[str] = "RGB"
_dit_modules: ClassVar[list[str]] = ["transformer.language_model", "transformer"]
_encoder_modules: ClassVar[list[str]] = []
_vae_modules: ClassVar[list[str]] = ["vae"]
_resident_modules: ClassVar[list[str]] = []
sampling_dtype: ClassVar[torch.dtype] = torch.float32
2026-06-01 22:04:31 +02:00
2026-06-08 21:04:05 +02:00
@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 {}
2026-06-21 03:59:18 +02:00
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")
2026-06-08 21:04:05 +02:00
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)
2026-06-08 21:04:05 +02:00
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"))
2026-06-08 21:04:05 +02:00
return ReferenceVideoDecodeSpec(max_frames=max_frames, keep=keep)
2026-06-01 22:04:31 +02:00
def __init__(
self,
*,
od_config: OmniDiffusionConfig,
prefix: str = "",
) -> None:
super().__init__()
self.od_config = od_config
resolved_offload = resolve_offload(od_config)
if resolved_offload.public is not None and resolved_offload.strategy is OffloadStrategy.MODEL_LEVEL:
raise ValueError(
"Cosmos3 model offload uses reasoner/generator topology and does "
"not support the dit/text_encoder component selector"
)
2026-06-01 22:04:31 +02:00
self.device = get_local_device()
self.dtype = od_config.dtype
model_path = od_config.model
local_files_only = os.path.exists(model_path)
# Validate guidance parallelism from checkpoint metadata before loading
# components. Distilled checkpoints have only a conditional branch.
scheduler_config = FlowUniPCMultistepScheduler.load_config(
model_path,
subfolder="scheduler",
local_files_only=local_files_only,
)
self.is_distilled_model = scheduler_config.get("_class_name") == COSMOS3_DISTILLED_CHECKPOINT_SCHEDULER_CLASS
if self.is_distilled_model and od_config.parallel_config.cfg_parallel_size > 1:
raise ValueError(
"Distilled Cosmos3 checkpoints run without classifier-free guidance. "
"Set --cfg-parallel-size 1 and use --ulysses-degree for multi-GPU inference."
)
2026-06-01 22:04:31 +02:00
# --- 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)
2026-06-03 02:51:44 +02:00
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
2026-06-01 22:04:31 +02:00
# --- Transformer (weights loaded later via weights_sources) ---
transformer_cls = resolve_cosmos3_transformer_cls(od_config.tf_model_config)
self.transformer = transformer_cls(
2026-06-01 22:04:31 +02:00
od_config=od_config,
temporal_compression_factor=self.vae_scale_factor_temporal,
2026-06-03 02:51:44 +02:00
sound_gen=sound_gen,
sound_dim=sound_dim,
sound_latent_fps=sound_latent_fps,
2026-06-01 22:04:31 +02:00
)
self.is_edge_model = transformer_cls is Cosmos3EdgeVFMTransformer
2026-06-01 22:04:31 +02:00
# Opt-in: route the per-CFG-branch UND text K/V through the shared
# SessionStateManager (RFC #4480) instead of the transformer-instance
# cache. This establishes request-keyed state isolation; it does not by
# itself make the mutable pipeline safe for concurrent execution.
# Default off -> the bespoke transformer-instance path below is unchanged.
self._use_session_state, mm_max_sessions = resolve_session_state_config(
enable=od_config.enable_session_state_manager,
)
self._memory_manager: SessionStateManager | None = (
SessionStateManager(max_sessions=mm_max_sessions) if self._use_session_state else None
)
# Forward declaration for the future BDE paged path (Stage C/D). Nothing
# writes this field in Phase 0; a non-None value is rejected by the guards
# below. None means the dense session adapter or the bespoke path.
self._bde_kv_state = None
if getattr(self, "_use_session_state", False):
logger.info("Cosmos3: session state manager enabled (max_sessions=%d)", mm_max_sessions)
2026-06-01 22:04:31 +02:00
# --- Scheduler ---
if self.is_distilled_model:
fixed_step_config = scheduler_config.get("fixed_step_sampler_config")
if not isinstance(fixed_step_config, dict) or fixed_step_config.get("sample_type") != "sde":
raise ValueError("Cosmos3 distilled scheduler requires fixed_step_sampler_config.sample_type=sde.")
t_list = fixed_step_config.get("t_list")
if not isinstance(t_list, list) or not t_list:
raise ValueError("Cosmos3 distilled scheduler requires a non-empty fixed_step_sampler_config.t_list.")
self.scheduler = FlowMatchEulerDiscreteScheduler.from_config(
scheduler_config,
stochastic_sampling=True,
)
self._scheduler_init_t_list = list(t_list)
else:
# Preserve compatible solver settings from the checkpoint, but keep
# the base shift neutral. The concrete request shift is applied when
# FlowUniPC builds its timesteps.
self.scheduler = FlowUniPCMultistepScheduler.from_config(
scheduler_config,
shift=1.0,
use_dynamic_shifting=False,
prediction_type="flow_prediction",
2026-06-24 03:03:34 +02:00
)
2026-06-01 22:04:31 +02:00
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,
2026-07-27 23:35:49 +08:00
subfolder="transformer",
2026-06-01 22:04:31 +02:00
revision=None,
prefix="transformer.",
fall_back_to_pt=True,
),
]
# An engine-level override becomes the video default; otherwise use
# the matching regular/Edge default. Per-request values still take
# precedence.
default_video_flow_shift = (
COSMOS3_EDGE_VIDEO_DEFAULT_FLOW_SHIFT if self.is_edge_model else COSMOS3_VIDEO_DEFAULT_FLOW_SHIFT
)
self._engine_init_flow_shift = float(
od_config.flow_shift if od_config.flow_shift is not None else default_video_flow_shift
)
2026-06-01 22:04:31 +02:00
self._current_flow_shift = self._engine_init_flow_shift
self._guidance_scale = None
self._num_timesteps = None
self._current_step_index = None
self._current_sigma = None
2026-06-21 03:59:18 +02:00
self._cosmos3_branch_caches: dict[str, tuple[Any, Any]] | None = None
2026-09-10 16:32:50 +02:00
self._robolab_transforms: dict[bool, Any] = {}
2026-06-01 22:04:31 +02:00
# 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
)
def enable_omni_model_cpu_offload(
self,
*,
device: torch.device,
pin_memory: bool = True,
use_hsdp: bool = False,
offload_components: frozenset[str] | None = None,
) -> None:
"""Enable Cosmos3 component-level model offload.
Cosmos3 has a nested reasoner/generator transformer instead of separate
text-encoder and DiT pipeline components, so the transformer owns the
mutual-exclusion swaps. The VAE stays resident on GPU like the generic
model-level offloader.
"""
if offload_components is not None:
raise ValueError(
"Cosmos3 model offload uses reasoner/generator topology and does not support "
"the dit/text_encoder offload_components selector"
)
self.vae.to(device, non_blocking=True)
if isinstance(self._sound_tokenizer, nn.Module):
self._sound_tokenizer.to(device)
self.transformer.enable_model_cpu_offload(
device=device,
pin_memory=pin_memory,
use_hsdp=use_hsdp,
)
def disable_omni_model_cpu_offload(self) -> None:
self.transformer.disable_model_cpu_offload()
2026-06-01 22:04:31 +02:00
# -- 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.",
2026-06-03 02:51:44 +02:00
"audio_proj_in.",
"audio_proj_out.",
2026-06-03 21:30:39 +02:00
"action_proj_in.",
"action_proj_out.",
2026-06-01 22:04:31 +02:00
)
):
return f"transformer.{k}"
2026-06-03 02:51:44 +02:00
if k in ("audio_modality_embed", "audio_modality_embed.weight"):
return "transformer.audio_modality_embed"
2026-06-03 21:30:39 +02:00
if k in ("action_modality_embed", "action_modality_embed.weight"):
return "transformer.action_modality_embed"
if k.startswith("action_pos_embed."):
return None
2026-06-01 22:04:31 +02:00
# 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.",
"self_attn.k_norm_und_for_gen.": f"{und_lp}.self_attn.k_norm_und_for_gen.",
2026-06-01 22:04:31 +02:00
# 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
# Checkpoint adapters use this hook before model-specific weight loading.
remap_checkpoint_key = _remap_ckpt_key
2026-06-01 22:04:31 +02:00
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
if name in allowed or name in tp_aware:
kept += 1
yield name, tensor
continue
2026-06-01 22:04:31 +02:00
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()
self.transformer.validate_loaded_weights(loaded)
2026-06-03 02:51:44 +02:00
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."
)
2026-06-03 21:30:39 +02:00
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."
)
2026-06-01 22:04:31 +02:00
return loaded
def predict_noise(self, **kwargs) -> torch.Tensor | tuple[torch.Tensor, ...]:
"""Override CFGParallelMixin.predict_noise for Cosmos3.
2026-06-03 02:51:44 +02:00
The transformer returns the raw prediction: video-only as a tensor,
2026-06-03 21:30:39 +02:00
or a tuple in video, action, sound order for multimodal generation.
2026-06-01 22:04:31 +02:00
"""
def _to_model_dtype(value: Any) -> Any:
if isinstance(value, torch.Tensor):
return value.to(self.dtype)
if isinstance(value, list):
return [_to_model_dtype(item) for item in value]
if isinstance(value, tuple):
return tuple(_to_model_dtype(item) for item in value)
return value
def _to_sampling_dtype(
prediction: torch.Tensor | tuple[torch.Tensor, ...],
) -> torch.Tensor | tuple[torch.Tensor, ...]:
if isinstance(prediction, tuple):
return tuple(item.to(self.sampling_dtype) for item in prediction)
return prediction.to(self.sampling_dtype)
2026-06-21 03:59:18 +02:00
cache_key = kwargs.pop("_cosmos3_cache_key", None)
context_name = str(kwargs.pop("_cache_context", "cond"))
for key in (
"hidden_states",
"action_latents",
"sound_latents",
"control_latents",
"noisy_frame_mask",
"action_noisy_mask",
):
if key in kwargs and kwargs[key] is not None:
kwargs[key] = _to_model_dtype(kwargs[key])
2026-06-21 03:59:18 +02:00
context_factory = getattr(self, "_cache_context_factory", None)
context = context_factory(context_name) if callable(context_factory) else nullcontext()
with context:
if cache_key is None:
prediction = self.transformer(**kwargs)
else:
branch_caches = self._cosmos3_branch_caches
if branch_caches is None:
prediction = self.transformer(**kwargs)
else:
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 _to_sampling_dtype(prediction)
2026-06-21 03:59:18 +02:00
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
2026-06-01 22:04:31 +02:00
@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
2026-09-10 16:32:50 +02:00
def _get_robolab_transform(self, *, format_prompt_as_json: bool = False):
transforms = getattr(self, "_robolab_transforms", None)
if transforms is None:
transforms = {}
self._robolab_transforms = transforms
if format_prompt_as_json not in transforms:
action_dim = int(getattr(self.transformer, "action_dim", 64))
2026-09-10 16:32:50 +02:00
transforms[format_prompt_as_json] = lazy_action_transform_pipeline(
action_dim,
format_prompt_as_json=format_prompt_as_json,
)
return transforms[format_prompt_as_json]
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))
2026-09-10 16:32:50 +02:00
domain_id = get_robolab_domain_id(domain_name)
format_prompt_as_json = self._truthy(extra_param("format_prompt_as_json", False))
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),
2026-09-10 16:32:50 +02:00
# Cosmos Framework 1.2 renamed the internal policy transform mode to
# ``wam``. The public vLLM action_mode remains ``policy``.
"mode": "wam",
"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
2026-09-10 16:32:50 +02:00
sample = self._get_robolab_transform(format_prompt_as_json=format_prompt_as_json)(sample, resolution)
if isinstance(sample.get("ai_caption"), dict):
sample["ai_caption"] = json.dumps(sample["ai_caption"])
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,
session_id: str | None = None,
) -> DiffusionOutput:
if self.is_distilled_model:
raise self._distilled_unsupported_error("RoboLab/action policy requests are unsupported.")
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,
session_id=session_id,
generator=generator,
)
if _is_rank_zero():
logger.info("Total pipeline time: %.2fs", time.time() - pipeline_start)
action = action_latents[:, :, :raw_action_dim].detach().cpu()
action_output: dict[str, Any] = {
"payload": {
"actions": action,
},
"metadata": {
"actions": {
"raw_action_dim": raw_action_dim,
"action_mode": action_mode,
"domain_id": domain_id,
},
"common": {
"action_only_output": True,
},
"internal": {
"robolab_action_postprocess": make_robolab_action_postprocess_inputs(inputs),
},
},
}
return DiffusionOutput(
output=action_output,
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
2026-06-03 02:51:44 +02:00
@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
2026-06-03 21:30:39 +02:00
@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))
)
2026-06-03 02:51:44 +02:00
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
2026-06-01 22:04:31 +02:00
@staticmethod
def _is_t2i_request(req: DiffusionRequestBatch) -> bool:
2026-06-27 00:39:42 +02:00
"""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.
"""
2026-06-01 22:04:31 +02:00
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
def _set_timesteps(self, num_inference_steps: int, device: str | torch.device, shift: float) -> None:
if self.is_distilled_model:
self.scheduler.set_timesteps(sigmas=self._scheduler_init_t_list, device=device)
else:
self.scheduler.set_timesteps(
num_inference_steps,
device=device,
shift=shift,
)
def _set_flow_shift(self, target_shift: float) -> None:
"""Select the shift applied by FlowUniPC when timesteps are built."""
self._current_flow_shift = float(target_shift)
2026-06-24 03:03:34 +02:00
@staticmethod
def _resolve_seed(sp: OmniDiffusionSamplingParams, generator: torch.Generator | None, default: int = 42) -> int:
if sp.seed is not None:
return int(sp.seed)
if isinstance(generator, torch.Generator):
return int(generator.initial_seed())
return int(default)
2026-06-24 03:03:34 +02:00
def _resolve_guidance_scale(self, sp: OmniDiffusionSamplingParams, default: float) -> float:
if self.is_distilled_model:
if sp.guidance_scale_provided and float(sp.guidance_scale) != 1.0:
logger.warning_once(
"Distilled Cosmos3 checkpoints run without classifier-free guidance. "
"The requested guidance_scale is overridden to 1.0; negative_prompt does not affect generation."
)
return 1.0
if sp.guidance_scale_provided:
return float(sp.guidance_scale)
return float(default)
2026-06-01 22:04:31 +02:00
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
@property
def current_step_index(self):
return self._current_step_index
@property
def current_sigma(self):
return self._current_sigma
def _set_denoise_step_metadata(
self,
step_index: int,
timesteps: torch.Tensor,
scheduler: Any,
) -> None:
self._current_step_index = step_index
self._num_timesteps = len(timesteps)
sigmas = getattr(scheduler, "sigmas", None)
self._current_sigma = sigmas[step_index] if sigmas is not None and step_index < len(sigmas) else None
def _clear_denoise_step_metadata(self) -> None:
self._current_step_index = None
self._current_sigma = None
def _set_mixed_precision_step(self, step_index: int, num_steps: int) -> None:
setter = getattr(self.transformer, "set_mixed_precision_step", None)
if setter is not None:
setter(step_index, num_steps)
def _reset_mixed_precision(self) -> None:
resetter = getattr(self.transformer, "reset_mixed_precision", None)
if resetter is not None:
resetter()
@staticmethod
def _distilled_unsupported_error(detail: str) -> ValueError:
return ValueError(
f"Cosmos3 distilled checkpoints support only text-to-image and image-to-video generation; {detail}"
)
def _validate_distilled_generation_mode(
self,
*,
is_t2i: bool,
image_tensor: torch.Tensor | None,
action_enabled: bool,
transfer_config: Cosmos3TransferConfig | None,
is_v2v: bool,
sound_enabled: bool,
) -> None:
if not self.is_distilled_model:
return
if action_enabled:
raise self._distilled_unsupported_error("action requests are unsupported.")
if transfer_config is not None:
raise self._distilled_unsupported_error("transfer requests are unsupported.")
if is_v2v:
raise self._distilled_unsupported_error("video-to-video requests are unsupported.")
if sound_enabled:
raise self._distilled_unsupported_error("sound generation is unsupported.")
if not is_t2i and image_tensor is None:
raise self._distilled_unsupported_error("text-to-video requests are unsupported.")
if is_t2i and image_tensor is not None:
raise self._distilled_unsupported_error("image-conditioned image generation is unsupported.")
def _validate_edge_generation_mode(
self,
*,
transfer_config: Cosmos3TransferConfig | None,
is_v2v: bool,
sound_enabled: bool,
) -> None:
if not self.is_edge_model:
return
if sound_enabled:
raise ValueError("Cosmos3 Edge checkpoints do not support sound generation.")
if transfer_config is not None:
raise ValueError("Cosmos3 Edge checkpoints do not support transfer inference.")
if is_v2v:
raise ValueError("Cosmos3 Edge checkpoints do not support video-to-video generation.")
2026-06-01 22:04:31 +02:00
# -- 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.
"""
prompt = prompt.strip()
if duration_template is None and resolution_template is None:
return prompt
2026-06-01 22:04:31 +02:00
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.sampling_dtype)
2026-06-01 22:04:31 +02:00
2026-06-03 02:51:44 +02:00
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,
2026-06-03 02:51:44 +02:00
) -> 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,
)
2026-06-03 02:51:44 +02:00
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.sampling_dtype,
2026-06-03 02:51:44 +02:00
)
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
2026-06-01 22:04:31 +02:00
# -- 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)
2026-06-01 22:04:31 +02:00
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
2026-06-01 22:04:31 +02:00
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
2026-06-03 02:51:44 +02:00
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()
2026-06-01 22:04:31 +02:00
# -- 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,
2026-08-20 20:40:51 +02:00
system_prompt: str | None = None,
prompt_suffix: str | None = None,
use_duration_template: bool | None = None,
use_resolution_template: bool | None = None,
negative_metadata_mode: str | None = None,
aspect_ratio_override: str | None = None,
2026-06-01 22:04:31 +02:00
) -> 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
2026-08-20 20:40:51 +02:00
resolution template is used. ``prompt_suffix`` is appended only to the
positive prompt, after metadata formatting. ``negative_metadata_mode``
controls whether the negative branch receives no metadata, the same
metadata as the positive branch, or inverse metadata.
2026-06-01 22:04:31 +02:00
"""
# 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.
2026-08-20 20:40:51 +02:00
if use_duration_template is None:
use_duration_template = bool(self._get_sp_param(sp, "use_duration_template", False))
if use_resolution_template is None:
use_resolution_template = bool(self._get_sp_param(sp, "use_resolution_template", False))
if negative_metadata_mode is None:
negative_metadata_mode = str(self._get_sp_param(sp, "negative_metadata_mode", "inverse"))
negative_metadata_mode = negative_metadata_mode.strip().lower()
if negative_metadata_mode not in {"none", "same", "inverse"}:
raise ValueError(
"Cosmos3 negative_metadata_mode must be one of 'none', 'same', or 'inverse'; "
f"got {negative_metadata_mode!r}."
)
use_duration_template = use_duration_template and not is_t2i
2026-06-01 22:04:31 +02:00
dur_tmpl = COSMOS3_DURATION_TEMPLATE if use_duration_template else None
2026-08-20 20:40:51 +02:00
if use_resolution_template:
2026-06-01 22:04:31 +02:00
res_tmpl = COSMOS3_IMAGE_RESOLUTION_TEMPLATE if is_t2i else COSMOS3_RESOLUTION_TEMPLATE
else:
res_tmpl = None
requested_aspect_ratio = self._get_sp_param(sp, "aspect_ratio", None)
prompt_aspect_ratio = _json_object_aspect_ratio(prompt)
metadata_aspect_ratio = requested_aspect_ratio if requested_aspect_ratio is not None else prompt_aspect_ratio
canvas_aspect_ratio = aspect_ratio_override or _aspect_ratio_for_dimensions(width, height)
normalized_metadata_ratio = _normalize_aspect_ratio(metadata_aspect_ratio)
has_aspect_ratio_conflict = (
aspect_ratio_override is None
and metadata_aspect_ratio is not None
and normalized_metadata_ratio != canvas_aspect_ratio
)
if has_aspect_ratio_conflict:
aspect_ratio_source = (
"requested aspect_ratio" if requested_aspect_ratio is not None else "JSON prompt aspect_ratio"
)
if _is_rank_zero():
logger.warning(
"Cosmos3 %s=%r conflicts with the generated %dx%d canvas (WxH); using %s for prompt metadata.",
aspect_ratio_source,
metadata_aspect_ratio,
width,
height,
canvas_aspect_ratio,
)
metadata_aspect_ratio = canvas_aspect_ratio
elif aspect_ratio_override is not None:
metadata_aspect_ratio = aspect_ratio_override
json_prompt = _format_json_object_prompt(
2026-06-01 22:04:31 +02:00
prompt,
num_frames=num_frames,
frame_rate=frame_rate,
height=height,
width=width,
aspect_ratio=metadata_aspect_ratio,
2026-06-01 22:04:31 +02:00
)
if json_prompt is not None:
prompt = json_prompt
else:
prompt = self._apply_metadata_templates(
prompt,
num_frames,
frame_rate,
height,
width,
duration_template=dur_tmpl,
resolution_template=res_tmpl,
)
2026-08-20 20:40:51 +02:00
if prompt_suffix:
prompt = f"{prompt.rstrip()} {prompt_suffix.lstrip()}".strip()
2026-06-01 22:04:31 +02:00
if _is_rank_zero():
logger.debug("Final prompt: '%s'", prompt)
2026-06-01 22:04:31 +02:00
2026-08-20 20:40:51 +02:00
if negative_metadata_mode == "none":
negative_dur_tmpl = None
negative_res_tmpl = None
elif negative_metadata_mode == "same":
negative_dur_tmpl = dur_tmpl
negative_res_tmpl = res_tmpl
2026-06-01 22:04:31 +02:00
else:
2026-08-20 20:40:51 +02:00
negative_dur_tmpl = COSMOS3_INVERSE_DURATION_TEMPLATE if dur_tmpl else None
if res_tmpl is None:
negative_res_tmpl = None
elif is_t2i:
negative_res_tmpl = COSMOS3_INVERSE_IMAGE_RESOLUTION_TEMPLATE
else:
negative_res_tmpl = COSMOS3_INVERSE_RESOLUTION_TEMPLATE
2026-06-01 22:04:31 +02:00
negative_prompt = self._apply_metadata_templates(
negative_prompt,
num_frames,
frame_rate,
height,
width,
2026-08-20 20:40:51 +02:00
duration_template=negative_dur_tmpl,
resolution_template=negative_res_tmpl,
force_duration_template=negative_metadata_mode == "inverse",
2026-06-01 22:04:31 +02:00
)
2026-08-20 20:40:51 +02:00
if system_prompt is None:
default_sys_prompt = COSMOS3_T2I_SYSTEM_PROMPT if is_t2i else COSMOS3_SYSTEM_PROMPT
system_prompt = self._get_sp_param(sp, "system_prompt", default_sys_prompt) or default_sys_prompt
2026-06-01 22:04:31 +02:00
cond_ids, cond_mask = self._tokenize_prompt(
2026-08-20 20:40:51 +02:00
prompt, max_sequence_length, use_system_prompt, system_prompt=system_prompt
2026-06-01 22:04:31 +02:00
)
uncond_ids, uncond_mask = self._tokenize_prompt(
2026-08-20 20:40:51 +02:00
negative_prompt, max_sequence_length, use_system_prompt, system_prompt=system_prompt
2026-06-01 22:04:31 +02:00
)
return cond_ids, cond_mask, uncond_ids, uncond_mask
# -- I2V latent preparation ---------------------------------------------
def _normalize_vae_latent(self, latent: torch.Tensor) -> torch.Tensor:
2026-06-01 22:04:31 +02:00
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)
2026-06-01 22:04:31 +02:00
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.sampling_dtype)
2026-06-01 22:04:31 +02:00
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:
2026-06-03 21:30:39 +02:00
"""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)
2026-06-03 21:30:39 +02:00
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.sampling_dtype)
2026-06-03 21:30:39 +02:00
2026-06-01 22:04:31 +02:00
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.sampling_dtype,
2026-06-01 22:04:31 +02:00
)
image_latent = self._encode_conditioning_image_latent(image_tensor)
latents = noise
latents[:, :, 0:1, :, :] = image_latent
2026-06-01 22:04:31 +02:00
velocity_mask = torch.ones(1, 1, T_lat, 1, 1, device=self.device, dtype=self.sampling_dtype)
velocity_mask[:, :, 0, :, :] = 0.0
2026-06-01 22:04:31 +02:00
return latents, velocity_mask, image_latent
2026-06-08 21:04:05 +02:00
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)
2026-06-08 21:04:05 +02:00
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.sampling_dtype,
2026-06-08 21:04:05 +02:00
)
condition_pixel_frames = condition_pixel_frame_count(indexes, self.vae_scale_factor_temporal)
2026-06-08 21:04:05 +02:00
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.sampling_dtype)
2026-06-08 21:04:05 +02:00
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
2026-06-03 21:30:39 +02:00
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,
2026-06-03 21:30:39 +02:00
) -> 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.
"""
2026-06-03 21:30:39 +02:00
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.")
2026-06-03 21:30:39 +02:00
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
2026-06-03 21:30:39 +02:00
noise = randn_tensor(
(1, C, T_lat, H_lat, W_lat),
generator=generator,
device=self.device,
dtype=self.sampling_dtype,
2026-06-03 21:30:39 +02:00
)
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:]
):
2026-06-03 21:30:39 +02:00
raise ValueError(
"Cosmos3 action video latent shape mismatch: "
f"encoded={tuple(cond_prefix_latent.shape)}, expected at least {expected_prefix}."
2026-06-03 21:30:39 +02:00
)
condition_latents = torch.zeros_like(noise)
for index in condition_indexes:
condition_latents[:, :, index : index + 1] = cond_prefix_latent[:, :, index : index + 1]
2026-06-03 21:30:39 +02:00
condition_mask = build_vision_condition_mask(
mode,
num_frames,
self.vae_scale_factor_temporal,
device=self.device,
dtype=self.sampling_dtype,
2026-06-03 21:30:39 +02:00
)
latents = condition_mask * condition_latents + (1.0 - condition_mask) * noise
2026-06-03 21:30:39 +02:00
velocity_mask = 1.0 - condition_mask
return latents, velocity_mask, condition_latents
2026-06-03 21:30:39 +02:00
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,
2026-06-03 21:30:39 +02:00
) -> 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:
2026-06-03 21:30:39 +02:00
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.sampling_dtype).unsqueeze(0)
if condition_indexes is None:
condition_mask = build_action_condition_mask(
mode,
action_chunk_size,
device=self.device,
dtype=self.sampling_dtype,
)
else:
condition_mask = self._build_action_condition_mask_from_indexes(
condition_indexes,
action_chunk_size,
device=self.device,
dtype=self.sampling_dtype,
)
2026-06-03 21:30:39 +02:00
noise = randn_tensor(
(1, action_chunk_size, action_dim),
generator=generator,
device=self.device,
dtype=self.sampling_dtype,
2026-06-03 21:30:39 +02:00
)
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
# -- Session-memory bypass for UND text K/V (RFC #4480) -----------------
# Mirrors DreamZero's `_kv_*` pattern: route through the BDE pool when set
# (Stage C/D), else the session adapter (Stage B), else the bespoke
# transformer-instance cache. All gated so the default path is unchanged.
def _new_cosmos3_state(self, session_id: str | None) -> Cosmos3StateAdapter | None:
if getattr(self, "_memory_manager", None) is None:
return None
return Cosmos3StateAdapter(session_id, self._memory_manager)
def _kv_load_und(self, state: Cosmos3StateAdapter | None, is_negative: bool) -> bool:
"""Load this branch's UND K/V onto the transformer before forward.
Returns True if the session/BDE path handled the load (caller skips the
bespoke ``cond_cache`` assignment), False for the bespoke path.
"""
if getattr(self, "_bde_kv_state", None) is not None:
raise NotImplementedError("BDE pool path for Cosmos3 UND K/V is Stage C/D")
if state is not None:
state.load_into_transformer(self.transformer, is_negative)
return True
return False
def _kv_capture_und(self, state: Cosmos3StateAdapter | None, is_negative: bool) -> None:
"""After forward, store the freshly computed UND K/V for this branch (encode-once)."""
if getattr(self, "_bde_kv_state", None) is not None:
raise NotImplementedError("BDE pool path for Cosmos3 UND K/V is Stage C/D")
if state is not None:
state.capture_from_transformer(self.transformer, is_negative)
def _kv_reset_und(self, state: Cosmos3StateAdapter | None) -> None:
"""Reset UND K/V at the start of a generation (replaces ``transformer.reset_cache()``).
Always clears the transformer-instance cache so no stale UND K/V leaks into
paths that read it directly (cfg-parallel / no-CFG / bespoke fallback); also
clears the session objects when session state is active.
"""
if getattr(self, "_bde_kv_state", None) is not None:
raise NotImplementedError("BDE pool path for Cosmos3 UND K/V is Stage C/D")
self.transformer.reset_cache()
if state is not None:
state.reset()
2026-06-01 22:04:31 +02:00
# -- 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,
*,
2026-06-03 21:30:39 +02:00
action_latents: torch.Tensor | None = None,
action_velocity_mask: torch.Tensor | None = None,
action_condition_latents: torch.Tensor | None = None,
2026-06-03 02:51:44 +02:00
sound_latents: torch.Tensor | None = None,
2026-06-01 22:04:31 +02:00
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,
2026-06-03 21:30:39 +02:00
raw_action_dim: int | None = None,
scheduler: Any | None = None,
session_id: str | None = None,
generator: torch.Generator | None = None,
2026-06-03 02:51:44 +02:00
) -> torch.Tensor | tuple[torch.Tensor, ...]:
2026-06-01 22:04:31 +02:00
"""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
# Session-keyed UND K/V (RFC #4480); None => bespoke transformer-instance cache.
kv_state = self._new_cosmos3_state(session_id)
self._kv_reset_und(kv_state)
2026-06-01 22:04:31 +02:00
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
2026-06-03 02:51:44 +02:00
# 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,
2026-06-03 21:30:39 +02:00
action_tensor: torch.Tensor | None = None,
2026-06-03 02:51:44 +02:00
sound_tensor: torch.Tensor | None = None,
):
batch = video_tensor.shape[0]
tensors = [video_tensor]
2026-06-03 21:30:39 +02:00
if action_tensor is not None:
tensors.append(action_tensor)
2026-06-03 02:51:44 +02:00
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, ...],
2026-06-03 21:30:39 +02:00
) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
has_action = action_latents is not None
2026-06-03 02:51:44 +02:00
has_sound = sound_latents is not None
2026-06-03 21:30:39 +02:00
if not has_action and not has_sound:
2026-06-03 02:51:44 +02:00
if isinstance(noise_pred, tuple):
raise ValueError("Cosmos3 video-only diffusion received tuple predictions.")
2026-06-03 21:30:39 +02:00
return noise_pred, None, None
2026-06-03 02:51:44 +02:00
if not isinstance(noise_pred, tuple):
raise ValueError("Cosmos3 multimodal diffusion expects transformer predictions as a tuple.")
2026-06-03 21:30:39 +02:00
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
2026-06-03 02:51:44 +02:00
2026-06-01 22:04:31 +02:00
def _step(
2026-06-03 02:51:44 +02:00
noise_pred: torch.Tensor | tuple[torch.Tensor, ...],
2026-06-01 22:04:31 +02:00
t: torch.Tensor,
latents: torch.Tensor,
2026-06-03 21:30:39 +02:00
action_latents: torch.Tensor | None,
2026-06-03 02:51:44 +02:00
sound_latents: torch.Tensor | None,
) -> torch.Tensor | tuple[torch.Tensor, ...]:
2026-06-03 21:30:39 +02:00
video_pred, action_pred, sound_pred = _split_noise_pred(noise_pred)
2026-06-01 22:04:31 +02:00
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
2026-06-03 21:30:39 +02:00
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,
generator=generator,
return_dict=False,
)[0]
2026-06-03 02:51:44 +02:00
else:
2026-06-03 21:30:39 +02:00
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,
generator=generator,
return_dict=False,
)[0]
2026-06-03 02:51:44 +02:00
unpacked = _unpack_joint(packed_next, shapes, numels)
latents = unpacked[0]
2026-06-03 21:30:39 +02:00
idx = 1
if action_latents is not None:
action_latents = unpacked[idx]
idx += 1
2026-06-03 02:51:44 +02:00
if sound_latents is not None:
2026-06-03 21:30:39 +02:00
sound_latents = unpacked[idx]
2026-06-01 22:04:31 +02:00
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
2026-06-03 21:30:39 +02:00
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
)
2026-06-03 02:51:44 +02:00
outputs = [latents]
2026-06-03 21:30:39 +02:00
if action_latents is not None:
outputs.append(action_latents)
2026-06-03 02:51:44 +02:00
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:
2026-06-03 21:30:39 +02:00
nonlocal latents, action_latents, sound_latents
if action_latents is None and sound_latents is None:
2026-06-03 02:51:44 +02:00
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]
2026-06-03 21:30:39 +02:00
idx = 1
if action_latents is not None:
action_latents = step_out[idx]
idx += 1
2026-06-03 02:51:44 +02:00
if sound_latents is not None:
2026-06-03 21:30:39 +02:00
sound_latents = step_out[idx]
2026-06-01 22:04:31 +02:00
try:
if cfg_parallel:
# Each CFG-parallel rank runs exactly one branch (rank 0 -> cond,
# else uncond), so session keying loads/stores only this rank's branch.
cfg_rank_is_negative = get_classifier_free_guidance_rank() != 0
for step_index, t in enumerate(self.progress_bar(timesteps)):
self._set_denoise_step_metadata(step_index, timesteps, step_scheduler)
self._set_mixed_precision_step(step_index, len(timesteps))
timestep = t.unsqueeze(0)
# Outside the interval, scale=1 makes the combined output equal
# the cond branch. Every rank remains on the same iteration and
# collective schedule.
step_scale = guidance_scale if _cfg_active_at(t) else 1.0
self._kv_load_und(kv_state, is_negative=cfg_rank_is_negative)
noise_pred = self.predict_noise_maybe_with_cfg(
do_true_cfg=True,
true_cfg_scale=step_scale,
positive_kwargs=dict(
_cache_context="cond",
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(
_cache_context="uncond",
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,
)
if kv_state is not None:
self._kv_capture_und(kv_state, is_negative=cfg_rank_is_negative)
_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 step_index, t in enumerate(self.progress_bar(timesteps)):
self._set_denoise_step_metadata(step_index, timesteps, step_scheduler)
self._set_mixed_precision_step(step_index, len(timesteps))
timestep = t.unsqueeze(0)
cfg_active = _cfg_active_at(t)
if not self._kv_load_und(kv_state, is_negative=False):
self.transformer.cached_kv, self.transformer.cached_freqs_gen = cond_cache
noise_cond = self.predict_noise(
_cache_context="cond",
2026-06-01 22:04:31 +02:00
hidden_states=latents,
timestep=timestep,
text_ids=cond_ids,
text_mask=cond_mask,
2026-06-03 21:30:39 +02:00
action_latents=action_latents,
2026-06-03 02:51:44 +02:00
sound_latents=sound_latents,
2026-06-01 22:04:31 +02:00
**shared_kwargs,
)
if kv_state is not None:
self._kv_capture_und(kv_state, is_negative=False)
elif cond_cache[0] is None:
cond_cache = (self.transformer.cached_kv, self.transformer.cached_freqs_gen)
if cfg_active or keep_uncond_for_cache:
if not self._kv_load_und(kv_state, is_negative=True):
self.transformer.cached_kv, self.transformer.cached_freqs_gen = uncond_cache
noise_uncond = self.predict_noise(
_cache_context="uncond",
hidden_states=latents,
timestep=timestep,
text_ids=uncond_ids,
text_mask=uncond_mask,
action_latents=action_latents,
sound_latents=sound_latents,
**shared_kwargs,
)
if kv_state is not None:
self._kv_capture_und(kv_state, is_negative=True)
elif 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:
# No CFG: a single cond branch per step. Bespoke (state None) keeps
# using the transformer-instance cache exactly as before.
for step_index, t in enumerate(self.progress_bar(timesteps)):
self._set_denoise_step_metadata(step_index, timesteps, step_scheduler)
self._set_mixed_precision_step(step_index, len(timesteps))
timestep = t.unsqueeze(0)
self._kv_load_und(kv_state, is_negative=False)
noise_pred = self.predict_noise(
_cache_context="cond",
2026-06-01 22:04:31 +02:00
hidden_states=latents,
timestep=timestep,
text_ids=cond_ids,
text_mask=cond_mask,
2026-06-03 21:30:39 +02:00
action_latents=action_latents,
2026-06-03 02:51:44 +02:00
sound_latents=sound_latents,
2026-06-01 22:04:31 +02:00
**shared_kwargs,
)
if kv_state is not None:
self._kv_capture_und(kv_state, is_negative=False)
_assign_step_out(_step(noise_pred, t, latents, action_latents, sound_latents))
finally:
self._clear_denoise_step_metadata()
self._reset_mixed_precision()
# Cosmos3 currently receives a unique request_id rather than a
# reusable rollout session id. Retaining its state would only pin
# K/V buffers on device after this generation finishes.
if kv_state is not None:
self._memory_manager.drop_session(session_id)
2026-06-01 22:04:31 +02:00
2026-06-03 02:51:44 +02:00
outputs = [latents]
2026-06-03 21:30:39 +02:00
if action_latents is not None:
outputs.append(action_latents)
2026-06-03 02:51:44 +02:00
if sound_latents is not None:
outputs.append(sound_latents)
return outputs[0] if len(outputs) == 1 else tuple(outputs)
2026-06-01 22:04:31 +02:00
2026-06-21 03:59:18 +02:00
@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.sampling_dtype,
2026-06-21 03:59:18 +02:00
)
condition_mask = torch.zeros(
1,
1,
condition_latents.shape[2],
1,
1,
device=self.device,
dtype=self.sampling_dtype,
2026-06-21 03:59:18 +02:00
)
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, str]:
2026-06-21 03:59:18 +02:00
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)
aspect_ratio = next(
ratio for ratio, size in VIDEO_RES_SIZE_INFO[str(resolution)].items() if size == (target_w, target_h)
)
return int(target_h), int(target_w), aspect_ratio
def _warn_transfer_bucket_conflicts(
self,
sp: OmniDiffusionSamplingParams,
prompt: str,
*,
source_hw: tuple[int, int] | None,
height: int,
width: int,
aspect_ratio: str,
) -> None:
"""Explain transfer's bucket selection when it overrides request metadata."""
if not _is_rank_zero():
return
requested_size_data = self._get_sp_param(sp, COSMOS3_TRANSFER_REQUESTED_SIZE_KEY, None)
if isinstance(requested_size_data, Mapping):
requested_width = requested_size_data.get("width")
requested_height = requested_size_data.get("height")
else:
requested_width = getattr(sp, "width", None)
requested_height = getattr(sp, "height", None)
if requested_width is not None and requested_height is not None:
try:
requested_size = (int(requested_width), int(requested_height))
except (TypeError, ValueError):
requested_size = None
if requested_size is not None and requested_size != (width, height):
source_description = (
f"control input {source_hw[1]}x{source_hw[0]} (WxH)"
if source_hw is not None
else "the default transfer geometry"
)
resolution = self._get_sp_param(sp, "resolution", self._get_sp_param(sp, "image_size", 720))
logger.warning(
"Cosmos3 transfer ignores requested size=%dx%d (WxH); %s selected the %s resolution=%s "
"bucket, %dx%d (WxH). Transfer supports canonical Cosmos buckets, not arbitrary output sizes.",
*requested_size,
source_description,
aspect_ratio,
resolution,
width,
height,
)
request_aspect_ratio = self._get_sp_param(sp, "aspect_ratio", None)
aspect_ratio_source = "requested aspect_ratio"
if request_aspect_ratio is None:
request_aspect_ratio = _json_object_aspect_ratio(prompt)
aspect_ratio_source = "JSON prompt aspect_ratio"
normalized_requested_ratio = _normalize_aspect_ratio(request_aspect_ratio)
if normalized_requested_ratio is not None and normalized_requested_ratio != aspect_ratio:
logger.warning(
"Cosmos3 transfer %s=%r conflicts with the control-selected %s bucket, %dx%d (WxH); "
"using %s for prompt metadata.",
aspect_ratio_source,
request_aspect_ratio,
aspect_ratio,
width,
height,
aspect_ratio,
)
2026-06-21 03:59:18 +02:00
@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],
*,
2026-08-20 20:40:51 +02:00
control_weights: list[float] | None = None,
2026-06-21 03:59:18 +02:00
velocity_mask: torch.Tensor,
condition_latents: torch.Tensor,
guidance_interval: tuple[float, float] | None = None,
generator: torch.Generator | None = None,
2026-06-21 03:59:18 +02:00
) -> torch.Tensor:
if getattr(self, "_use_session_state", False):
raise NotImplementedError(
"Cosmos3 transfer does not yet support SessionStateManager: "
"its control/text CFG branches require richer cache keys. "
"Disable the session state manager for transfer requests."
)
2026-06-21 03:59:18 +02:00
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 step_index, t in enumerate(self.progress_bar(timesteps)):
self._set_denoise_step_metadata(step_index, timesteps, self.scheduler)
self._set_mixed_precision_step(step_index, len(timesteps))
2026-06-21 03:59:18 +02:00
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(
_cache_context="cond",
2026-06-21 03:59:18 +02:00
hidden_states=latents,
timestep=timestep,
text_ids=cond_ids,
text_mask=cond_mask,
control_latents=control_latents,
2026-08-20 20:40:51 +02:00
control_weights=control_weights,
2026-06-21 03:59:18 +02:00
_cosmos3_cache_key="transfer_cond_full",
**shared_kwargs,
)
if needs_control_cfg and needs_text_cfg:
branches_kwargs = [
cond_full_kwargs,
dict(
_cache_context="cond_no_control",
2026-06-21 03:59:18 +02:00
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(
_cache_context="uncond",
2026-06-21 03:59:18 +02:00
hidden_states=latents,
timestep=timestep,
text_ids=uncond_ids,
text_mask=uncond_mask,
control_latents=control_latents,
2026-08-20 20:40:51 +02:00
control_weights=control_weights,
2026-06-21 03:59:18 +02:00
_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(
_cache_context="cond_no_control",
2026-06-21 03:59:18 +02:00
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(
_cache_context="uncond",
2026-06-21 03:59:18 +02:00
hidden_states=latents,
timestep=timestep,
text_ids=uncond_ids,
text_mask=uncond_mask,
control_latents=control_latents,
2026-08-20 20:40:51 +02:00
control_weights=control_weights,
2026-06-21 03:59:18 +02:00
_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,
generator=generator,
return_dict=False,
)[0]
2026-06-21 03:59:18 +02:00
latents = velocity_mask * latents + (1.0 - velocity_mask) * condition_latents
finally:
self._clear_denoise_step_metadata()
self._reset_mixed_precision()
2026-06-21 03:59:18 +02:00
self._cosmos3_branch_caches = None
self.transformer.reset_cache()
return latents
def _transfer_vae_executor(self) -> Any | None:
"""Return the active distributed VAE executor, if any."""
executor = getattr(self.vae, "distributed_executor", None)
is_distributed_enabled = getattr(self.vae, "is_distributed_enabled", None)
if executor is None or not callable(is_distributed_enabled) or not is_distributed_enabled():
return None
return executor
def _sync_transfer_overlap(
self,
output_video: torch.Tensor,
*,
overlap_frames: int,
reference_video: torch.Tensor,
vae_executor: Any | None,
) -> torch.Tensor | None:
"""Broadcast only the decoded frames needed to condition the next chunk."""
if overlap_frames <= 0:
return None
is_output_rank = vae_executor is None or vae_executor.rank == 0
if is_output_rank:
overlap = output_video[:, :, -overlap_frames:].contiguous()
else:
overlap = torch.empty(
(
reference_video.shape[0],
reference_video.shape[1],
overlap_frames,
reference_video.shape[3],
reference_video.shape[4],
),
device=self.device,
dtype=self.vae.dtype,
)
if vae_executor is not None:
overlap = vae_executor.broadcast_tensor(overlap)
return overlap
2026-06-21 03:59:18 +02:00
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:
if self.is_distilled_model:
raise self._distilled_unsupported_error("transfer requests are unsupported.")
2026-06-21 03:59:18 +02:00
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, transfer_aspect_ratio = self._transfer_bucket_size(sp, source_hw)
self._warn_transfer_bucket_conflicts(
sp,
prompt,
source_hw=source_hw,
height=height,
width=width,
aspect_ratio=transfer_aspect_ratio,
)
2026-06-21 03:59:18 +02:00
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 self._resolve_guidance_scale(sp, COSMOS3_T2V_DEFAULT_GUIDANCE_SCALE)
2026-06-21 03:59:18 +02:00
)
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
)
2026-08-20 20:40:51 +02:00
transfer_prompt_suffix = None
if transfer_config.emphasize_control_in_prompt:
hint_names = ", ".join(hint.key for hint in transfer_config.ordered_hints)
transfer_prompt_suffix = COSMOS3_TRANSFER_CONTROL_DIRECTIVE_TEMPLATE.format(hint_names=hint_names)
2026-06-21 03:59:18 +02:00
self._guidance_scale = guidance_scale
self._num_timesteps = num_inference_steps
self._set_flow_shift(flow_shift_target)
2026-06-21 03:59:18 +02:00
generator = sp.generator
seed = self._resolve_seed(sp, generator)
2026-06-21 03:59:18 +02:00
if generator is None:
generator = torch.Generator(device=self.device).manual_seed(seed)
2026-08-20 20:40:51 +02:00
for ignored_key in ("system_prompt", "use_system_prompt"):
if self._get_sp_param(sp, ignored_key, None) is not None:
logger.warning_once(
"Cosmos3 transfer ignores the '%s' request parameter and always "
"tokenizes both CFG branches with the transfer-specific system prompt.",
ignored_key,
)
2026-06-21 03:59:18 +02:00
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,
2026-08-20 20:40:51 +02:00
use_system_prompt=True,
2026-06-21 03:59:18 +02:00
is_t2i=False,
2026-08-20 20:40:51 +02:00
system_prompt=COSMOS3_TRANSFER_SYSTEM_PROMPT,
prompt_suffix=transfer_prompt_suffix,
use_duration_template=bool(self._get_sp_param(sp, "use_duration_template", True)),
use_resolution_template=bool(self._get_sp_param(sp, "use_resolution_template", True)),
negative_metadata_mode=str(self._get_sp_param(sp, "negative_metadata_mode", "same")),
aspect_ratio_override=transfer_aspect_ratio,
2026-06-21 03:59:18 +02:00
)
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
vae_executor = self._transfer_vae_executor()
is_output_rank = vae_executor is None or vae_executor.rank == 0
2026-06-21 03:59:18 +02:00
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._set_timesteps(
num_inference_steps,
device=self.device,
shift=self._current_flow_shift,
)
2026-06-21 03:59:18 +02:00
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,
2026-08-20 20:40:51 +02:00
control_weights=transfer_config.normalized_control_weights,
2026-06-21 03:59:18 +02:00
shared_kwargs=shared_kwargs,
velocity_mask=velocity_mask,
condition_latents=condition_latents,
generator=generator,
2026-06-21 03:59:18 +02:00
)
output_video = self._decode_latents(latents).clamp(-1, 1)
if chunk_id + 1 < num_chunks:
previous_output = self._sync_transfer_overlap(
output_video,
overlap_frames=min(transfer_config.num_conditional_frames, chunk_frames),
reference_video=target_norm,
vae_executor=vae_executor,
)
2026-06-21 03:59:18 +02:00
if is_output_rank:
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:])
if not is_output_rank:
return DiffusionOutput(
output={
"payload": {"video": output_video},
"metadata": {"video": {"fps": frame_rate}},
},
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
2026-06-21 03:59:18 +02:00
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={
"payload": {"video": full_output},
"metadata": {
"transfer": {
"controls": full_controls,
"hints": list(per_hint_frames),
},
"video": {"fps": frame_rate},
},
2026-06-21 03:59:18 +02:00
},
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
2026-06-21 03:59:18 +02:00
)
2026-06-01 22:04:31 +02:00
# -- Forward (main generation entry point) -------------------------------
def forward(
self,
req: DiffusionRequestBatch,
2026-06-01 22:04:31 +02:00
) -> DiffusionOutput:
pipeline_start = time.time()
# --- Parse request ---
prompt_data = req.prompts[0] if req.prompts else ""
2026-06-01 22:04:31 +02:00
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,
session_id=getattr(req, "request_id", None),
)
2026-06-01 22:04:31 +02:00
if isinstance(prompt_data, str):
prompt = prompt_data
negative_prompt = None
image_tensor = None
2026-06-08 21:04:05 +02:00
video_tensor = None
2026-06-21 03:59:18 +02:00
transfer_video_tensor = None
transfer_input_fps = None
2026-06-01 22:04:31 +02:00
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")
2026-06-08 21:04:05 +02:00
video_tensor = additional_info.get("preprocessed_video")
2026-06-21 03:59:18 +02:00
transfer_video_tensor = additional_info.get("preprocessed_transfer_video")
transfer_input_fps = positive_float(additional_info.get("transfer_input_fps"))
2026-06-01 22:04:31 +02:00
is_t2i = self._is_t2i_request(req)
2026-06-03 02:51:44 +02:00
sound_enabled = self._is_sound_request(prompt_data, sp)
2026-06-03 21:30:39 +02:00
action_mode = self._get_action_mode(prompt_data, sp)
action_enabled = action_mode is not None
2026-06-21 03:59:18 +02:00
transfer_config = resolve_transfer_config(sp, prompt_data)
2026-06-08 21:04:05 +02:00
action_video_tensor = video_tensor if action_enabled else None
is_v2v = video_tensor is not None and not is_t2i and not action_enabled
self._validate_distilled_generation_mode(
is_t2i=is_t2i,
image_tensor=image_tensor,
action_enabled=action_enabled,
transfer_config=transfer_config,
is_v2v=is_v2v,
sound_enabled=sound_enabled,
)
self._validate_edge_generation_mode(
transfer_config=transfer_config,
is_v2v=is_v2v,
sound_enabled=sound_enabled,
)
2026-06-21 03:59:18 +02:00
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.")
2026-06-03 21:30:39 +02:00
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."
)
2026-06-03 02:51:44 +02:00
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."
)
2026-06-01 22:04:31 +02:00
if negative_prompt is None:
negative_prompt = ""
2026-06-21 03:59:18 +02:00
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,
)
2026-06-08 21:04:05 +02:00
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.")
2026-06-01 22:04:31 +02:00
# T2I and T2V share the same model and forward path; their defaults are:
# T2I: regular 1024x1024, Edge 640x640; 50 steps, shift=3.0,
# guidance_interval=[400, 1000]
# T2V: 189 frames and 35 steps; regular guidance=6, shift=10;
# Edge guidance=5, shift=3;
# no guidance interval
2026-06-01 22:04:31 +02:00
if is_t2i:
height = sp.height or (
COSMOS3_EDGE_T2I_DEFAULT_HEIGHT if self.is_edge_model else COSMOS3_T2I_DEFAULT_HEIGHT
)
width = sp.width or (COSMOS3_EDGE_T2I_DEFAULT_WIDTH if self.is_edge_model else COSMOS3_T2I_DEFAULT_WIDTH)
2026-06-01 22:04:31 +02:00
num_frames = 1
num_inference_steps = sp.num_inference_steps or COSMOS3_T2I_DEFAULT_NUM_INFERENCE_STEPS
guidance_scale = self._resolve_guidance_scale(sp, COSMOS3_T2I_DEFAULT_GUIDANCE_SCALE)
2026-06-01 22:04:31 +02:00
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_EDGE_T2V_DEFAULT_HEIGHT if self.is_edge_model else COSMOS3_T2V_DEFAULT_HEIGHT
)
width = sp.width or (COSMOS3_EDGE_T2V_DEFAULT_WIDTH if self.is_edge_model else COSMOS3_T2V_DEFAULT_WIDTH)
default_guidance_scale = (
COSMOS3_EDGE_T2V_DEFAULT_GUIDANCE_SCALE if self.is_edge_model else COSMOS3_T2V_DEFAULT_GUIDANCE_SCALE
)
2026-06-01 22:04:31 +02:00
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 = self._resolve_guidance_scale(sp, default_guidance_scale)
default_flow_shift = self._engine_init_flow_shift
2026-06-01 22:04:31 +02:00
default_guidance_interval = None
batch_size = 1 # Existing video pipeline assumes B=1.
2026-06-03 21:30:39 +02:00
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 = self._resolve_guidance_scale(sp, 1.0)
2026-06-03 21:30:39 +02:00
default_flow_shift = 5.0
if not is_t2i and not action_enabled:
requested_num_frames = int(num_frames)
num_frames = _ceil_video_num_frames(
requested_num_frames,
self.vae_scale_factor_temporal,
)
if num_frames != requested_num_frames and _is_rank_zero():
logger.info(
"Rounded Cosmos3 num_frames from %d to %d for temporal compression factor %d.",
requested_num_frames,
num_frames,
self.vae_scale_factor_temporal,
)
2026-06-03 21:30:39 +02:00
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,
)
2026-06-01 22:04:31 +02:00
# 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
)
2026-06-24 03:03:34 +02:00
use_system_prompt = bool(self._get_sp_param(sp, "use_system_prompt", is_v2v))
2026-06-01 22:04:31 +02:00
2026-06-03 21:30:39 +02:00
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])
2026-06-01 22:04:31 +02:00
self._guidance_scale = guidance_scale
self._num_timesteps = num_inference_steps
# Always resolve to a concrete target shift for this request.
self._set_flow_shift(flow_shift_target)
2026-06-01 22:04:31 +02:00
generator = sp.generator
seed = self._resolve_seed(sp, generator)
2026-06-01 22:04:31 +02:00
if generator is None:
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.
2026-06-03 21:30:39 +02:00
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
2026-06-03 21:30:39 +02:00
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,
2026-06-03 21:30:39 +02:00
)
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]
2026-06-08 21:04:05 +02:00
elif is_v2v:
condition_frame_indexes_vision = normalize_condition_frame_indexes_vision(
2026-06-08 21:04:05 +02:00
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
2026-06-03 21:30:39 +02:00
elif image_tensor is not None and not is_t2i:
2026-06-01 22:04:31 +02:00
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)
2026-06-03 02:51:44 +02:00
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,
)
2026-06-01 22:04:31 +02:00
# --- 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
2026-06-03 21:30:39 +02:00
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),
)
2026-06-01 22:04:31 +02:00
def _run_diffusion(start_latents):
self._set_timesteps(
num_inference_steps,
device=self.device,
shift=self._current_flow_shift,
)
scheduler = self.scheduler
2026-06-01 22:04:31 +02:00
return self.diffuse(
latents=start_latents,
timesteps=scheduler.timesteps,
2026-06-01 22:04:31 +02:00
cond_ids=cond_ids,
cond_mask=cond_mask,
uncond_ids=uncond_ids,
uncond_mask=uncond_mask,
guidance_scale=guidance_scale,
shared_kwargs=shared_kwargs,
2026-06-03 21:30:39 +02:00
action_latents=action_latents,
action_velocity_mask=action_velocity_mask,
action_condition_latents=action_condition_latents,
2026-06-03 02:51:44 +02:00
sound_latents=sound_latents,
2026-06-01 22:04:31 +02:00
velocity_mask=velocity_mask,
image_latent=image_latent,
condition_latents=condition_latents,
guidance_interval=guidance_interval,
2026-06-03 21:30:39 +02:00
raw_action_dim=raw_action_dim,
scheduler=scheduler,
session_id=getattr(req, "request_id", None),
generator=generator,
2026-06-01 22:04:31 +02:00
)
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:
2026-06-03 02:51:44 +02:00
diffusion_output = _run_diffusion(latents)
2026-06-03 21:30:39 +02:00
if action_enabled and sound_enabled:
latents, action_latents, sound_latents = diffusion_output
elif action_enabled:
latents, action_latents = diffusion_output
elif sound_enabled:
2026-06-03 02:51:44 +02:00
latents, sound_latents = diffusion_output
else:
latents = diffusion_output
2026-06-01 22:04:31 +02:00
# --- 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)
2026-06-21 03:59:18 +02:00
if not sound_enabled:
logger.info("Total pipeline time: %.2fs", time.time() - pipeline_start)
2026-06-01 22:04:31 +02:00
2026-06-03 02:51:44 +02:00
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...")
2026-06-21 03:59:18 +02:00
sound_decode_start = time.time()
2026-06-03 02:51:44 +02:00
audio = self._decode_sound_latents(sound_latents, target_audio_samples)
2026-06-21 03:59:18 +02:00
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},
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
2026-06-03 02:51:44 +02:00
2026-06-03 21:30:39 +02:00
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={
"payload": {
"video": video,
"actions": action,
},
"metadata": {
"actions": {
"raw_action_dim": raw_action_dim,
"action_mode": action_mode,
"domain_id": domain_id,
},
},
},
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
2026-06-03 21:30:39 +02:00
return DiffusionOutput(
output={"image": video} if is_t2i else {"video": video},
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)