Spaces:
Running on L40S
Running on L40S
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: OpenMDW-1.1 | |
| import json | |
| import math | |
| import os | |
| from functools import cache | |
| from pathlib import Path | |
| from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal, Self, cast, override | |
| import pydantic | |
| import pynvml | |
| from typing_extensions import assert_never | |
| from tyro.conf import Suppress | |
| from cosmos_framework.inference.common.args import ( | |
| IMAGE_EXTENSIONS, | |
| VIDEO_EXTENSIONS, | |
| ArgsBase, | |
| CfgpSize, | |
| CheckpointConfig, | |
| OverridesBase, | |
| ResolvedFilePath, | |
| ResolvedFilePathOrUrl, | |
| SampleArgs, | |
| SampleOverrides, | |
| SetupArgs, | |
| SetupOverrides, | |
| StrEnum, | |
| Training, | |
| _deep_merge, | |
| download_file, | |
| ) | |
| from cosmos_framework.inference.common.config import CONFIG_DIR, PACKAGE_DIR | |
| from cosmos_framework.utils import log | |
| from cosmos_framework.utils.checkpoint_db import CheckpointDirHf | |
| from cosmos_framework.utils.flags import SMOKE, TRAINING | |
| if TYPE_CHECKING: | |
| from cosmos_framework.configs.base.defaults.model_config import OmniMoTModelConfig | |
| from cosmos_framework.inference.common.inference import Inference | |
| def _load_transfer_prompt_path(path: str | Path) -> str: | |
| """Load a transfer prompt from a ``.json`` or plain ``.txt`` file.""" | |
| resolved = Path(path) | |
| text = resolved.read_text() | |
| if resolved.suffix.lower() == ".json": | |
| return json.dumps(json.loads(text)) | |
| return text.strip() | |
| def _load_transfer_negative_prompt_file(path: str | Path) -> str: | |
| """Load a JSON negative caption file for transfer inference.""" | |
| candidate = Path(path) | |
| if not candidate.is_file(): | |
| defaults_path = PACKAGE_DIR / "defaults" / candidate.name | |
| if defaults_path.is_file(): | |
| candidate = defaults_path | |
| else: | |
| raise FileNotFoundError(f"Missing negative prompt file: {path} (also checked {defaults_path})") | |
| return json.dumps(json.loads(candidate.read_text())) | |
| def _load_modality_defaults(model_mode: str) -> dict[str, Any]: | |
| default_file = PACKAGE_DIR / f"defaults/{model_mode}/sample_args.json" | |
| if not default_file.exists(): | |
| raise FileNotFoundError(f"Missing modality defaults: {default_file}") | |
| data = json.loads(default_file.read_text()) | |
| neg_file = data.pop("negative_prompt_file", None) | |
| if neg_file is not None: | |
| neg_path = PACKAGE_DIR / "defaults" / neg_file | |
| if not neg_path.exists(): | |
| raise FileNotFoundError(f"Missing negative prompt file: {neg_path}") | |
| data["negative_prompt"] = json.dumps(json.loads(neg_path.read_text())) | |
| return data | |
| Guidance = Annotated[float, pydantic.Field(ge=0, le=7)] | |
| GuidanceInterval = tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat] | |
| PromptUpsamplerProbability = Annotated[float, pydantic.Field(ge=0, le=1)] | |
| ControlGuidance = Annotated[float, pydantic.Field(ge=0, le=10)] | |
| class SamplingArgs(ArgsBase): | |
| num_steps: pydantic.PositiveInt | |
| guidance: Guidance | |
| guidance_interval: GuidanceInterval | None | |
| normalize_cfg: bool | |
| shift: float | |
| sigma_max: float | |
| class SamplingOverrides(OverridesBase): | |
| """Sampling arguments for 'OmniMoTModel.generate_samples'.""" | |
| num_steps: Training[pydantic.PositiveInt | None] = None | |
| """Number of steps for the diffusion model.""" | |
| guidance: Training[Guidance | None] = None | |
| """Guidance scale for the diffusion model.""" | |
| guidance_interval: Training[GuidanceInterval | None] = None | |
| """Guidance interval for the diffusion model.""" | |
| normalize_cfg: Training[bool | None] = None | |
| """If True, normalize the CFG output.""" | |
| shift: Training[float | None] = None | |
| """Shift in the UniPC sampler. Ignored when sampler='edm'.""" | |
| sigma_max: Training[float | None] = None | |
| """Maximum sigma for the EDM sampler. Ignored when sampler='unipc'.""" | |
| def _build_sampling(self, model_config: "OmniMoTModelConfig", sample_meta: "SampleMeta"): | |
| if sample_meta.model_mode.is_reasoner: | |
| # Diffusion sampling fields are unused by the reasoner but required by | |
| # OmniSampleArgs validation; fill in inert sentinels. | |
| if self.num_steps is None: | |
| self.num_steps = 1 | |
| if self.guidance is None: | |
| self.guidance = 0.0 | |
| if self.normalize_cfg is None: | |
| self.normalize_cfg = False | |
| if self.shift is None: | |
| self.shift = 0.0 | |
| if self.sigma_max is None: | |
| self.sigma_max = 0.0 | |
| return | |
| assert self.num_steps is not None | |
| if SMOKE: | |
| self.num_steps = min(self.num_steps, 1) | |
| InferenceResolution = Literal["256", "480", "720", "768", "1080"] | |
| if TRAINING: | |
| Resolution = Literal["256", "480", "704", "720", "768", "1080"] | |
| else: | |
| Resolution = InferenceResolution | |
| AspectRatio = Literal["1,1", "4,3", "3,4", "16,9", "9,16"] | |
| # Resolutions that only support image generation (num_frames == 1). Video | |
| # generation at these resolutions is rejected by ``_build_vision_data`` because | |
| # the model wasn't trained on temporal data above 720p and ``MAX_NUM_FRAMES`` | |
| # has no entry for them. | |
| IMAGE_ONLY_RESOLUTIONS: frozenset[str] = frozenset({"1080"}) | |
| MIN_NUM_FRAMES = 24 | |
| MAX_NUM_FRAMES: dict[Resolution, int] = { | |
| "256": 400, | |
| "480": 300, | |
| "704": 200, | |
| "720": 200, | |
| "768": 200, | |
| } | |
| ModelSize = Literal["0.6B", "2B", "8B", "30B-A3B", "32B", "235B-A22B"] | |
| class ModelMode(StrEnum): | |
| TEXT2IMAGE = "text2image" | |
| TEXT2VIDEO = "text2video" | |
| IMAGE2IMAGE = "image2image" | |
| IMAGE2VIDEO = "image2video" | |
| VIDEO2VIDEO = "video2video" | |
| # Action | |
| FORWARD_DYNAMICS = "forward_dynamics" | |
| INVERSE_DYNAMICS = "inverse_dynamics" | |
| POLICY = "policy" | |
| REASONER = "reasoner" | |
| def is_action(self) -> bool: | |
| return self in ACTION_MODEL_MODES | |
| def is_reasoner(self) -> bool: | |
| return self in REASONER_MODEL_MODES | |
| # Image-output modes: ``num_frames`` defaults to 1 and the output is saved as a still image. | |
| _IMAGE_OUTPUT_MODES: frozenset[ModelMode] = frozenset({ModelMode.TEXT2IMAGE, ModelMode.IMAGE2IMAGE}) | |
| # Modes that produce action tensors and require a model with ``action_gen=True``. | |
| ACTION_MODEL_MODES: frozenset[ModelMode] = frozenset( | |
| {ModelMode.FORWARD_DYNAMICS, ModelMode.INVERSE_DYNAMICS, ModelMode.POLICY} | |
| ) | |
| REASONER_MODEL_MODES: frozenset[ModelMode] = frozenset({ModelMode.REASONER}) | |
| class VisionMode(StrEnum): | |
| IMAGE = "image" | |
| VIDEO = "video" | |
| def from_model_mode(cls, model_mode: ModelMode) -> Self: | |
| return cls.IMAGE if model_mode in _IMAGE_OUTPUT_MODES else cls.VIDEO | |
| class ConditionVisionMode(StrEnum): | |
| IMAGE = "image" | |
| VIDEO = "video" | |
| class NegativeMetadataMode(StrEnum): | |
| NONE = "none" | |
| SAME = "same" | |
| INVERSE = "inverse" | |
| class TransferHintKey(StrEnum): | |
| EDGE = "edge" | |
| BLUR = "blur" | |
| DEPTH = "depth" | |
| SEG = "seg" | |
| WSM = "wsm" | |
| class PresetEdgeThreshold(StrEnum): | |
| VERY_LOW = "very_low" | |
| LOW = "low" | |
| MEDIUM = "medium" | |
| HIGH = "high" | |
| VERY_HIGH = "very_high" | |
| class PresetBlurStrength(StrEnum): | |
| NONE = "none" | |
| VERY_LOW = "very_low" | |
| LOW = "low" | |
| MEDIUM = "medium" | |
| HIGH = "high" | |
| VERY_HIGH = "very_high" | |
| class TransferArgs(ArgsBase): | |
| """Resolved transfer inference arguments for a single control hint.""" | |
| control_path: ResolvedFilePathOrUrl | None = None | |
| class EdgeTransferArgs(TransferArgs): | |
| preset_edge_threshold: PresetEdgeThreshold = PresetEdgeThreshold.MEDIUM | |
| class BlurTransferArgs(TransferArgs): | |
| preset_blur_strength: PresetBlurStrength = PresetBlurStrength.MEDIUM | |
| class TransferOverrides(OverridesBase): | |
| """Transfer inference overrides for a single control hint (all optional).""" | |
| control_path: ResolvedFilePathOrUrl | None = None | |
| """Path or URL to pre-computed control input.""" | |
| def download(self, output_dir: Path): | |
| if self.control_path is not None: | |
| self.control_path = download_file(self.control_path, output_dir, "transfer_control") | |
| class EdgeTransferOverrides(TransferOverrides): | |
| preset_edge_threshold: PresetEdgeThreshold | None = None | |
| """Edge detection threshold preset.""" | |
| class BlurTransferOverrides(TransferOverrides): | |
| preset_blur_strength: PresetBlurStrength | None = None | |
| """Blur strength preset.""" | |
| class SampleMeta(pydantic.BaseModel): | |
| model_mode: ModelMode | |
| vision_mode: VisionMode | |
| condition_vision_mode: ConditionVisionMode | None | |
| RESOLUTION_ADAPTER = pydantic.TypeAdapter(Resolution) | |
| ASPECT_RATIO_ADAPTER = pydantic.TypeAdapter(AspectRatio) | |
| DEFAULT_CONDITION_FRAME_INDEXES_VISION: dict[ConditionVisionMode, list[int]] = { | |
| ConditionVisionMode.IMAGE: [0], | |
| ConditionVisionMode.VIDEO: [0, 1], | |
| } | |
| class TextDataArgs(ArgsBase): | |
| prompt: str | |
| negative_prompt: str | None | |
| duration_template: str | None | |
| resolution_template: str | None | |
| negative_metadata_mode: NegativeMetadataMode | |
| inverse_duration_template: str | |
| inverse_resolution_template: str | |
| negative_prompt_keep_metadata: bool | |
| class TextDataOverrides(OverridesBase): | |
| prompt_path: ResolvedFilePath | None = None | |
| """Path to a .txt file containing the prompt. Only one of 'prompt' or 'prompt_path' should be provided.""" | |
| prompt: str | None = None | |
| """Text prompt for generation. Only one of 'prompt' or 'prompt_path' should be provided.""" | |
| negative_prompt: str | None = None | |
| """Negative prompt - describing what you don't want in the generated video.""" | |
| duration_template: Training[str | None] = None | |
| """Template string for appending duration/fps to prompt. Use {duration} and {fps} placeholders.""" | |
| resolution_template: Training[str | None] = None | |
| """Template string for appending resolution to prompt. Use {height} and {width} placeholders.""" | |
| negative_metadata_mode: Training[NegativeMetadataMode | None] = None | |
| """Negative prompt metadata mode: 'none', 'same', or 'inverse'.""" | |
| inverse_duration_template: Training[str | None] = None | |
| """Inverse template for duration/fps metadata in the negative prompt.""" | |
| inverse_resolution_template: Training[str | None] = None | |
| """Inverse template for resolution metadata in the negative prompt.""" | |
| negative_prompt_keep_metadata: Training[bool | None] = None | |
| """Compatibility flag. If True and mode is 'none', mode is promoted to 'same'.""" | |
| def _build_text_data(self, model_config: "OmniMoTModelConfig", sample_meta: SampleMeta): | |
| if self.prompt is not None: | |
| pass | |
| elif self.prompt_path is not None: | |
| transfer_self = cast("_TransferDataBase", self) | |
| if transfer_self.transfer_hints: | |
| self.prompt = _load_transfer_prompt_path(self.prompt_path) | |
| else: | |
| self.prompt = self.prompt_path.read_text().strip() | |
| else: | |
| self.prompt = "" | |
| if sample_meta.model_mode.is_reasoner: | |
| # Negative-prompt / metadata-template fields are unused by the reasoner | |
| # but required by OmniSampleArgs validation; fill in inert sentinels. | |
| if self.negative_metadata_mode is None: | |
| self.negative_metadata_mode = NegativeMetadataMode.NONE | |
| if self.inverse_duration_template is None: | |
| self.inverse_duration_template = "" | |
| if self.inverse_resolution_template is None: | |
| self.inverse_resolution_template = "" | |
| if self.negative_prompt_keep_metadata is None: | |
| self.negative_prompt_keep_metadata = False | |
| return | |
| if self.negative_prompt_keep_metadata and self.negative_metadata_mode == NegativeMetadataMode.NONE: | |
| self.negative_metadata_mode = NegativeMetadataMode.SAME | |
| Fps = Annotated[int, pydantic.Field(ge=1)] | |
| VideoSaveQuality = Annotated[int, pydantic.Field(ge=0, le=10)] | |
| ImageSaveQuality = Annotated[int, pydantic.Field(ge=0, le=100)] | |
| class _VisionDataBase: | |
| def condition_vision_mode(self) -> ConditionVisionMode | None: | |
| self = cast(VisionDataOverrides, self) | |
| if self.vision_path is not None: | |
| vision_ext = Path(self.vision_path).suffix.lower() | |
| if vision_ext in IMAGE_EXTENSIONS: | |
| return ConditionVisionMode.IMAGE | |
| elif vision_ext in VIDEO_EXTENSIONS: | |
| return ConditionVisionMode.VIDEO | |
| else: | |
| raise ValueError(f"Invalid vision extension: {vision_ext}") | |
| else: | |
| return None | |
| class VisionDataArgs(ArgsBase, _VisionDataBase): | |
| vision_path: ResolvedFilePath | None | |
| condition_frame_indexes_vision: list[int] | |
| condition_video_keep: Literal["first", "last"] | |
| resolution: Resolution | None | |
| aspect_ratio: AspectRatio | None | |
| fps: pydantic.PositiveInt | |
| num_frames: pydantic.PositiveInt | |
| video_save_quality: VideoSaveQuality | |
| image_save_quality: ImageSaveQuality | |
| def duration(self) -> float: | |
| return self.num_frames / self.fps | |
| def vision_size(self) -> tuple[int, int]: | |
| """Vision size (width, height) in pixels. | |
| Per the VisionDataOverrides.aspect_ratio docstring, ``None`` means | |
| "default to 16:9 for all modes except image_edit". This property is | |
| only reached by non-image_edit code paths (image_edit branches off in | |
| cosmos3/inference.py:_get_image_edit_sample_data before this is | |
| consulted), so the documented legacy default applies here. Callers | |
| that want native aspect-ratio preservation (e.g. transfer inference) | |
| autodetect via cosmos_framework.inference.vision.read_and_resize_media before reaching | |
| this property and never observe the fallback. | |
| """ | |
| from cosmos_framework.data.vfm.utils import IMAGE_RES_SIZE_INFO, VIDEO_RES_SIZE_INFO | |
| assert self.resolution | |
| aspect_ratio: AspectRatio = self.aspect_ratio or "16,9" | |
| if self.num_frames == 1: | |
| return IMAGE_RES_SIZE_INFO[self.resolution][aspect_ratio] | |
| else: | |
| return VIDEO_RES_SIZE_INFO[self.resolution][aspect_ratio] | |
| def vision_extension(self) -> str: | |
| return ".jpg" if self.num_frames == 1 else ".mp4" | |
| class VisionDataOverrides(OverridesBase, _VisionDataBase): | |
| # Vision condition fields | |
| vision_path: ResolvedFilePathOrUrl | None = None | |
| """Path or URL to conditioning image/video.""" | |
| condition_frame_indexes_vision: Training[list[int] | None] = None | |
| """Latent frame indices to condition on. Defaults to [0] for image, [0, 1] for video.""" | |
| condition_video_keep: Training[Literal["first", "last"] | None] = None | |
| """Whether to take the first or last ``max_frames`` of the conditioning | |
| video when it is longer than needed. Defaults to ``"first"``. No effect | |
| on image conditioning.""" | |
| # Vision fields | |
| resolution: Resolution | None = None | |
| """Vision resolution. | |
| Defaults to model config resolution. | |
| """ | |
| aspect_ratio: AspectRatio | None = None | |
| """Vision aspect ratio. When None, image_edit preserves the input image's native | |
| aspect ratio; all other modes default to 16:9.""" | |
| fps: Fps | None = None | |
| """Vision frames per second. Recommended range [10, 30]; quality may be degraded outside of this range.""" | |
| num_frames: pydantic.PositiveInt | None = None | |
| """Number of vision frames. | |
| Range by resolution: 256p: [24, 400], 480p: [24, 300], 720p/768p: [24, 200]. | |
| Image-only resolutions (e.g. 1080p) require num_frames=1. | |
| """ | |
| video_save_quality: Training[VideoSaveQuality | None] = None | |
| """Quality of the saved video (0-10).""" | |
| image_save_quality: Training[ImageSaveQuality | None] = None | |
| """Quality of the saved image (0-100).""" | |
| def download(self, output_dir: Path): | |
| super().download(output_dir) | |
| self.vision_path = download_file(self.vision_path, output_dir, "vision") | |
| def _build_vision_data(self, model_config: "OmniMoTModelConfig", sample_meta: SampleMeta): | |
| """Finalize and validate in-place.""" | |
| if self.vision_path and "://" in self.vision_path: | |
| raise ValueError("Must call `download()` before building vision data") | |
| # Reasoner mode treats ``vision_path`` as a PIL image source; resolution/fps/num_frames are unused. | |
| if sample_meta.model_mode.is_reasoner: | |
| self.condition_frame_indexes_vision = self.condition_frame_indexes_vision or [] | |
| self.condition_video_keep = self.condition_video_keep or "first" | |
| self.num_frames = self.num_frames or 1 | |
| # Vision-output fields are unused by the reasoner but required by | |
| # OmniSampleArgs validation; fill in inert sentinels. | |
| if self.fps is None: | |
| self.fps = 1 | |
| if self.video_save_quality is None: | |
| self.video_save_quality = 0 | |
| if self.image_save_quality is None: | |
| self.image_save_quality = 0 | |
| return | |
| if self.condition_frame_indexes_vision is None: | |
| if sample_meta.condition_vision_mode: | |
| self.condition_frame_indexes_vision = DEFAULT_CONDITION_FRAME_INDEXES_VISION[ | |
| sample_meta.condition_vision_mode | |
| ] | |
| else: | |
| self.condition_frame_indexes_vision = [] | |
| if self.condition_video_keep is None: | |
| self.condition_video_keep = "first" | |
| # Image edit defaults to input image size | |
| if sample_meta.model_mode != ModelMode.IMAGE2IMAGE: | |
| if self.resolution is None: | |
| self.resolution = RESOLUTION_ADAPTER.validate_python(model_config.resolution) | |
| # Image-output modes always emit a single frame; infer it so callers don't | |
| # have to set ``num_frames=1`` in every text2image / image2image preset. | |
| if self.num_frames is None and sample_meta.vision_mode == VisionMode.IMAGE: | |
| self.num_frames = 1 | |
| assert self.num_frames is not None | |
| if self.fps is not None and (self.fps < 10 or self.fps > 30): | |
| log.warning(f"FPS {self.fps} is outside the recommended range [10, 30]. Quality may be degraded.") | |
| if self.num_frames > 1: | |
| assert self.resolution is not None | |
| if self.resolution in IMAGE_ONLY_RESOLUTIONS: | |
| raise ValueError( | |
| f"Resolution {self.resolution!r} only supports image generation (num_frames=1). " | |
| f"For video, use one of: {sorted(MAX_NUM_FRAMES)}" | |
| ) | |
| if self.num_frames < MIN_NUM_FRAMES or self.num_frames > MAX_NUM_FRAMES[self.resolution]: | |
| log.warning( | |
| f"Number of frames {self.num_frames} is outside the recommended range [{MIN_NUM_FRAMES}, {MAX_NUM_FRAMES[self.resolution]}]. Quality may be degraded." | |
| ) | |
| if SMOKE: | |
| self.num_frames = min(self.num_frames, 2) | |
| temporal_compression_factor = model_config.tokenizer.temporal_compression_factor | |
| self.num_frames = ( | |
| math.ceil((self.num_frames - 1) / temporal_compression_factor) * temporal_compression_factor + 1 | |
| ) | |
| class SoundDataArgs(ArgsBase): | |
| enable_sound: bool = False | |
| class SoundDataOverrides(OverridesBase): | |
| """Sound data overrides.""" | |
| enable_sound: Training[bool | None] = None | |
| """Enable joint video+sound generation (t2vs mode). Requires a checkpoint with sound modules.""" | |
| def _build_sound_data(self, model_config: "OmniMoTModelConfig", sample_meta: SampleMeta): | |
| if self.enable_sound is None: | |
| self.enable_sound = False | |
| if self.enable_sound and not model_config.sound_gen: | |
| raise ValueError( | |
| "enable_sound=True requires a model with a sound tokenizer " | |
| "(model.config.sound_gen=True), but the loaded checkpoint has no sound tokenizer" | |
| ) | |
| class ActionDataArgs(ArgsBase): | |
| action_path: ResolvedFilePath | None = None | |
| domain_name: str = "" | |
| image_size: pydantic.PositiveInt = 256 | |
| action_chunk_size: pydantic.PositiveInt = 16 | |
| raw_action_dim: int | None = None | |
| view_point: str | None = None | |
| class ActionDataOverrides(OverridesBase): | |
| """Action data overrides.""" | |
| action_path: Training[ResolvedFilePathOrUrl | None] = None | |
| """Path to action JSON file. Required for forward_dynamics mode.""" | |
| domain_name: Training[str | None] = None | |
| """Action domain name passed to get_domain_id().""" | |
| image_size: Training[pydantic.PositiveInt | None] = None | |
| """Target image height in pixels (aspect-ratio-preserving resize).""" | |
| action_chunk_size: Training[pydantic.PositiveInt | None] = None | |
| """Number of action steps to predict.""" | |
| raw_action_dim: Training[pydantic.PositiveInt | None] = None | |
| """Dimension of the raw action data. Required when action_path is not provided.""" | |
| view_point: Training[str | None] = None | |
| """Viewpoint description for the action prompt.""" | |
| def download(self, output_dir: Path): | |
| super().download(output_dir) | |
| self.action_path = download_file(self.action_path, output_dir, "action") | |
| def _build_action_data(self, model_config: "OmniMoTModelConfig", sample_meta: SampleMeta): | |
| if self.domain_name is None: | |
| self.domain_name = "" | |
| if self.image_size is None: | |
| self.image_size = 256 | |
| if self.action_chunk_size is None: | |
| self.action_chunk_size = 16 | |
| if self.view_point is None: | |
| self.view_point = "ego_view" | |
| mode = sample_meta.model_mode | |
| if not mode.is_action: | |
| return | |
| if not model_config.action_gen: | |
| raise ValueError( | |
| f"model_mode={mode.value!r} requires a model with action support " | |
| "(model.config.action_gen=True), but the loaded checkpoint has action_gen=False" | |
| ) | |
| match mode: | |
| case ModelMode.FORWARD_DYNAMICS: | |
| if self.action_path is None: | |
| raise ValueError(f"'action_path' is required for model_mode={mode.value!r}") | |
| case ModelMode.INVERSE_DYNAMICS | ModelMode.POLICY: | |
| pass | |
| case _: | |
| assert_never(mode) | |
| if self.action_path and "://" in self.action_path: | |
| raise ValueError("Must call `download()` before building action data") | |
| _ReasonerTemperature = Annotated[float, pydantic.Field(gt=0, le=100)] | |
| _ReasonerTopP = Annotated[float, pydantic.Field(gt=0, le=1)] | |
| _ReasonerRepetitionPenalty = Annotated[float, pydantic.Field(gt=0)] | |
| class ReasonerDataArgs(ArgsBase): | |
| """Resolved reasoner (VLM) text-generation arguments. All fields are ``| None`` so | |
| non-reasoner samples (which never populate these) pass ``OmniSampleArgs`` validation; | |
| runtime values for reasoner mode come from ``defaults/reasoner/sample_args.json``.""" | |
| max_new_tokens: pydantic.PositiveInt | None = None | |
| do_sample: bool | None = None | |
| temperature: _ReasonerTemperature | None = None | |
| top_k: pydantic.PositiveInt | None = None | |
| top_p: _ReasonerTopP | None = None | |
| repetition_penalty: _ReasonerRepetitionPenalty | None = None | |
| presence_penalty: float | None = None | |
| class ReasonerDataOverrides(OverridesBase): | |
| """Reasoner overrides for ``model_mode='reasoner'``. ``vision_path`` (if set) is | |
| used as the conditioning image; the VLM processor handles preprocessing.""" | |
| max_new_tokens: pydantic.PositiveInt | None = None | |
| """Maximum number of new tokens to generate per prompt.""" | |
| do_sample: bool | None = None | |
| """If True, sample from the logits; otherwise greedy decode.""" | |
| temperature: _ReasonerTemperature | None = None | |
| """Sampling temperature. Ignored when ``do_sample`` is False.""" | |
| top_k: pydantic.PositiveInt | None = None | |
| """Top-k logit truncation. Ignored when ``do_sample`` is False.""" | |
| top_p: _ReasonerTopP | None = None | |
| """Nucleus-sampling threshold ``0 < top_p <= 1``. Ignored when ``do_sample`` is False.""" | |
| repetition_penalty: _ReasonerRepetitionPenalty | None = None | |
| """CTRL/HF-style multiplicative repetition penalty (>0). ``1.0`` is identity.""" | |
| presence_penalty: float | None = None | |
| """Additive presence penalty (any sign). ``0.0`` is identity.""" | |
| def _build_reasoner_data(self, model_config: "OmniMoTModelConfig", sample_meta: SampleMeta): | |
| if not sample_meta.model_mode.is_reasoner: | |
| return | |
| self = cast("SampleDataOverrides", self) | |
| if not self.prompt.strip(): | |
| raise ValueError("Reasoner inference requires a non-empty 'prompt'.") | |
| class _TransferDataBase: | |
| def transfer_hints(self) -> dict[TransferHintKey, TransferOverrides | TransferArgs]: | |
| # Iteration order is `TransferHintKey` enum order, not JSON-key order — keep this | |
| # deterministic so the model sees a stable [ctrl_1, ..., ctrl_N] sequence. | |
| return {key: getattr(self, key.value) for key in TransferHintKey if getattr(self, key.value) is not None} | |
| class TransferDataArgs(ArgsBase, _TransferDataBase): | |
| control_guidance: ControlGuidance = 1.0 | |
| control_guidance_interval: GuidanceInterval | None = None | |
| edge: EdgeTransferArgs | None = None | |
| blur: BlurTransferArgs | None = None | |
| depth: TransferArgs | None = None | |
| seg: TransferArgs | None = None | |
| wsm: TransferArgs | None = None | |
| negative_prompt_file: str | None = None | |
| """JSON negative caption file for transfer specs (absolute path or filename under ``defaults/``).""" | |
| num_video_frames_per_chunk: pydantic.PositiveInt | None = None | |
| num_conditional_frames: pydantic.NonNegativeInt | None = None | |
| max_frames: pydantic.PositiveInt | None = None | |
| show_control_condition: bool | None = None | |
| show_input: bool | None = None | |
| num_first_chunk_conditional_frames: pydantic.NonNegativeInt | None = None | |
| share_vision_temporal_positions: bool | None = None | |
| class TransferDataOverrides(OverridesBase, _TransferDataBase): | |
| """Transfer inference overrides — activated when at least one control hint is set.""" | |
| control_guidance: Training[ControlGuidance | None] = None | |
| """Control-CFG scale for transfer. The control map stays in the main forward; 1.0 disables | |
| the extra comparison forward that drops control-map vision items. Values > 1.0 blend | |
| velocities from with-maps vs without-maps forwards on the generated clip.""" | |
| control_guidance_interval: Training[GuidanceInterval | None] = None | |
| """Timestep interval [lo, hi] (0–1000) in which control-CFG is applied; None applies | |
| on every step.""" | |
| edge: EdgeTransferOverrides | None = None | |
| """Edge (Canny) control-hint overrides; set to activate the edge transfer hint.""" | |
| blur: BlurTransferOverrides | None = None | |
| """Blur control-hint overrides; set to activate the blur transfer hint.""" | |
| depth: TransferOverrides | None = None | |
| """Depth control-hint overrides; set to activate the depth transfer hint.""" | |
| seg: TransferOverrides | None = None | |
| """Segmentation control-hint overrides; set to activate the segmentation transfer hint.""" | |
| wsm: TransferOverrides | None = None | |
| """World-surface-map (WSM) control-hint overrides; set to activate the WSM transfer hint.""" | |
| negative_prompt_file: str | None = None | |
| """JSON negative caption file for transfer specs (absolute path or filename under ``defaults/``).""" | |
| num_video_frames_per_chunk: pydantic.PositiveInt | None = None | |
| """Number of video frames generated per autoregressive chunk.""" | |
| num_conditional_frames: pydantic.NonNegativeInt | None = None | |
| """Number of conditioning frames carried into each generated chunk.""" | |
| max_frames: pydantic.PositiveInt | None = None | |
| """Maximum number of frames to generate across all chunks.""" | |
| show_control_condition: bool | None = None | |
| """If set, include the control condition (hint map) in the saved output.""" | |
| show_input: bool | None = None | |
| """If set, include the input video alongside the generated output.""" | |
| num_first_chunk_conditional_frames: pydantic.NonNegativeInt | None = None | |
| """Number of conditioning frames for the first chunk (defaults to ``num_conditional_frames``).""" | |
| share_vision_temporal_positions: bool | None = None | |
| """Share vision temporal position ids across autoregressive chunks.""" | |
| def _validate_transfer_hints(self) -> Self: | |
| hint_field_names = {k.value for k in TransferHintKey} | |
| # ``control_guidance`` is concretized to its no-op default (1.0) by | |
| # ``_build_transfer_data`` for *every* sample, transfer or not, so the | |
| # default value must round-trip without a hint. Only an *explicit* | |
| # non-default ``control_guidance`` signals an intended-but-incomplete | |
| # transfer config, so it is checked separately below. | |
| exempt = hint_field_names | {"control_guidance"} | |
| transfer_only = [ | |
| name | |
| for name in TransferDataOverrides.__annotations__ | |
| if name in type(self).model_fields and name not in exempt | |
| ] | |
| configured = any(getattr(self, f) is not None for f in transfer_only) | |
| cg = self.control_guidance | |
| cg_configured = cg is not None and cg != 1.0 | |
| if (configured or cg_configured) and not self.transfer_hints: | |
| raise ValueError( | |
| f"transfer inference requires at least one control hint ({', '.join(k.value for k in TransferHintKey)})" | |
| ) | |
| return self | |
| def download(self, output_dir: Path): | |
| super().download(output_dir) | |
| for config in self.transfer_hints.values(): | |
| assert isinstance(config, TransferOverrides) | |
| config.download(output_dir) | |
| _TRANSFER_SAMPLE_DEFAULTS: ClassVar[dict[str, Any]] = { | |
| "num_video_frames_per_chunk": 93, | |
| "num_conditional_frames": 1, | |
| "max_frames": 5000, | |
| "show_control_condition": False, | |
| "show_input": False, | |
| "num_first_chunk_conditional_frames": 0, | |
| "share_vision_temporal_positions": True, | |
| } | |
| _TRANSFER_HINT_DEFAULTS: ClassVar[dict[TransferHintKey, dict[str, Any]]] = { | |
| TransferHintKey.EDGE: {"preset_edge_threshold": PresetEdgeThreshold.MEDIUM}, | |
| TransferHintKey.BLUR: {"preset_blur_strength": PresetBlurStrength.MEDIUM}, | |
| } | |
| # Tuned guidance / control_guidance per transfer task. Applied when the input JSON omits | |
| # these fields (generic video2video sampling defaults otherwise apply). | |
| _TRANSFER_DEFAULTS: ClassVar[dict[TransferHintKey, dict[str, Any]]] = { | |
| TransferHintKey.EDGE: {"guidance": 3.0, "control_guidance": 1.5, "shift": 10.0}, | |
| TransferHintKey.BLUR: {"guidance": 3.0, "control_guidance": 1.5, "shift": 10.0}, | |
| TransferHintKey.DEPTH: {"guidance": 3.0, "control_guidance": 1.5, "shift": 10.0}, | |
| TransferHintKey.SEG: {"guidance": 3.0, "control_guidance": 2.0, "shift": 10.0}, | |
| TransferHintKey.WSM: { | |
| "guidance": 1.0, | |
| "control_guidance": 3.0, | |
| "shift": 10.0, | |
| "num_frames": 101, | |
| "fps": 10, | |
| "num_video_frames_per_chunk": 101, | |
| }, | |
| } | |
| def _build_transfer_data( | |
| self, | |
| model_config: "OmniMoTModelConfig", | |
| sample_meta: SampleMeta, | |
| *, | |
| user_fields: frozenset[str] | None = None, | |
| ): | |
| self = cast("SampleDataOverrides", self) | |
| # ``control_guidance`` is a required float in ``TransferDataArgs``. | |
| # Keep it concrete even for non-transfer samples so ``OmniSampleArgs`` | |
| # validation never sees ``None``. | |
| if self.control_guidance is None: | |
| self.control_guidance = 1.0 | |
| hints = self.transfer_hints | |
| if not hints: | |
| return | |
| if self.negative_prompt is None and self.negative_prompt_file is not None: | |
| self.negative_prompt = _load_transfer_negative_prompt_file(self.negative_prompt_file) | |
| for field, default in self._TRANSFER_SAMPLE_DEFAULTS.items(): | |
| if getattr(self, field) is None: | |
| setattr(self, field, default) | |
| for hint_key, config in hints.items(): | |
| for field, default in self._TRANSFER_HINT_DEFAULTS.get(hint_key, {}).items(): | |
| if getattr(config, field) is None: | |
| setattr(config, field, default) | |
| if self.vision_path is None and config.control_path is None: | |
| raise ValueError( | |
| f"transfer inference requires 'vision_path' or a pre-computed 'control_path' (hint: {hint_key})" | |
| ) | |
| if len(hints) == 1: | |
| hint_key = next(iter(hints)) | |
| for field, value in self._TRANSFER_DEFAULTS[hint_key].items(): | |
| if user_fields is None or field not in user_fields: | |
| setattr(self, field, value) | |
| class _SampleDataBase: | |
| def resolved_model_mode(self) -> ModelMode: | |
| """Return ``model_mode`` if set, else infer the VFM modality from ``vision_path`` and ``num_frames``.""" | |
| self = cast(SampleDataOverrides, self) | |
| if self.model_mode is not None: | |
| return self.model_mode | |
| input_mode = self.condition_vision_mode.value if self.condition_vision_mode else "text" | |
| output_mode = VisionMode.IMAGE.value if self.num_frames == 1 else VisionMode.VIDEO.value | |
| return ModelMode(f"{input_mode}2{output_mode}") | |
| def sample_meta(self) -> SampleMeta: | |
| self = cast(SampleDataOverrides, self) | |
| mode = self.resolved_model_mode | |
| return SampleMeta( | |
| model_mode=mode, | |
| vision_mode=VisionMode.from_model_mode(mode), | |
| condition_vision_mode=self.condition_vision_mode, | |
| ) | |
| class SampleDataArgs( | |
| _SampleDataBase, | |
| TextDataArgs, | |
| VisionDataArgs, | |
| SoundDataArgs, | |
| ActionDataArgs, | |
| ReasonerDataArgs, | |
| TransferDataArgs, | |
| ): | |
| model_mode: ModelMode | |
| class SampleDataOverrides( | |
| _SampleDataBase, | |
| TextDataOverrides, | |
| VisionDataOverrides, | |
| SoundDataOverrides, | |
| ActionDataOverrides, | |
| ReasonerDataOverrides, | |
| TransferDataOverrides, | |
| ): | |
| """Sample data arguments for 'OmniMoTModel.generate_samples'.""" | |
| model_mode: ModelMode | None = None | |
| """Generation modality. When omitted, the VFM modality is inferred from ``vision_path`` and | |
| ``num_frames``; action modes must be set explicitly.""" | |
| class PromptUpsamplingArgs(ArgsBase): | |
| native_prompt_upsampling: bool = False | |
| """If True, use the native prompt upsampler.""" | |
| prompt_upsampler_max_tokens: pydantic.PositiveInt | |
| """Maximum tokens generated by the prompt upsampler.""" | |
| prompt_upsampler_temperature: pydantic.NonNegativeFloat | |
| """Native prompt upsampler sampling temperature.""" | |
| prompt_upsampler_top_p: PromptUpsamplerProbability | |
| """Native prompt upsampler nucleus sampling probability.""" | |
| prompt_upsampler_top_k: pydantic.NonNegativeInt | |
| """Native prompt upsampler top-k sampling limit.""" | |
| prompt_upsampler_repetition_penalty: pydantic.PositiveFloat | |
| """Native prompt upsampler CTRL/HF-style multiplicative repetition penalty. | |
| Applied to logits at vocab positions already seen in each caption's | |
| history (prompt + everything generated so far). ``>1.0`` discourages | |
| verbatim repetition, ``<1.0`` encourages it, ``1.0`` is identity and | |
| adds zero overhead to the reasoner AR loop. Constrained ``> 0`` so | |
| the CTRL formula (``logit /= penalty`` for positive logits, ``logit * | |
| penalty`` for negative) stays well-defined. | |
| """ | |
| prompt_upsampler_presence_penalty: float | |
| """Native prompt upsampler OpenAI-style additive presence penalty. | |
| Subtracted once from every logit at a vocab position already seen in | |
| each caption's history (binary presence, not frequency). ``>0`` | |
| discourages reuse, ``<0`` encourages it, ``0`` is identity. | |
| Unconstrained sign: negative values are valid for legitimate "favor | |
| repetition" use cases. | |
| """ | |
| prompt_upsampler_seed: int | None = None | |
| """Optional integer seed for the native prompt upsampler's sampling RNG. | |
| When set (and ``prompt_upsampler_temperature > 0`` so the AR loop | |
| actually samples), a device-local ``torch.Generator`` is seeded once | |
| inside ``_impl_generate_reasoner_text`` and threaded into every | |
| ``torch.multinomial`` draw, making the upsampled caption a | |
| deterministic function of ``seed``, the prompt, and the penalty | |
| masks. ``None`` (default) consumes the device's default RNG, | |
| preserving the pre-seed behavior. Greedy decoding (temperature 0) | |
| never reads the generator, so the value has no effect in that case. | |
| Under multi-rank inference, callers that need cross-rank agreement | |
| on sampled upsampler tokens must pass the same seed on every rank. | |
| """ | |
| class PromptUpsamplingOverrides(OverridesBase): | |
| prompt_upsampling: Training[bool | None] = None | |
| """If True, replace the prompt with a dense JSON prompt.""" | |
| prompt_upsampler_max_tokens: pydantic.PositiveInt | None = None | |
| """Maximum tokens generated by the prompt upsampler.""" | |
| prompt_upsampler_temperature: pydantic.NonNegativeFloat | None = None | |
| """Native prompt upsampler sampling temperature.""" | |
| prompt_upsampler_top_p: PromptUpsamplerProbability | None = None | |
| """Native prompt upsampler nucleus sampling probability.""" | |
| prompt_upsampler_top_k: pydantic.NonNegativeInt | None = None | |
| """Native prompt upsampler top-k sampling limit.""" | |
| prompt_upsampler_repetition_penalty: pydantic.PositiveFloat | None = None | |
| """Native prompt upsampler CTRL/HF-style multiplicative repetition penalty (>0).""" | |
| prompt_upsampler_presence_penalty: float | None = None | |
| """Native prompt upsampler OpenAI-style additive presence penalty (any sign).""" | |
| prompt_upsampler_seed: int | None = None | |
| """Optional integer seed for the native prompt upsampler's sampling RNG.""" | |
| def _build_prompt_upsampling(self, *, model_config: "OmniMoTModelConfig") -> None: | |
| if self.prompt_upsampler_max_tokens is None: | |
| self.prompt_upsampler_max_tokens = 20000 | |
| if self.prompt_upsampler_temperature is None: | |
| self.prompt_upsampler_temperature = 0.7 | |
| if self.prompt_upsampler_top_p is None: | |
| self.prompt_upsampler_top_p = 0.8 | |
| if self.prompt_upsampler_top_k is None: | |
| self.prompt_upsampler_top_k = 20 | |
| if self.prompt_upsampler_repetition_penalty is None: | |
| self.prompt_upsampler_repetition_penalty = 1.0 | |
| if self.prompt_upsampler_presence_penalty is None: | |
| self.prompt_upsampler_presence_penalty = 1.5 | |
| if self.prompt_upsampler_seed is None: | |
| self.prompt_upsampler_seed = 3407 | |
| class OmniSampleArgs( | |
| SampleArgs, | |
| SamplingArgs, | |
| SampleDataArgs, | |
| PromptUpsamplingArgs, | |
| ): ... | |
| class OmniSampleOverrides( | |
| SampleOverrides, | |
| SamplingOverrides, | |
| SampleDataOverrides, | |
| PromptUpsamplingOverrides, | |
| ): | |
| defaults_file: ResolvedFilePath | None = None | |
| """Path to a JSON file of per-modality default sample fields. Overrides the built-in defaults.""" | |
| _VLM_MODEL_SIZE: ClassVar[dict[str, ModelSize]] = { | |
| "Qwen/Qwen3-0.6B": "0.6B", | |
| "Qwen/Qwen3-VL-2B-Instruct": "2B", | |
| "Qwen/Qwen3-VL-8B-Instruct": "8B", | |
| "Qwen/Qwen3-VL-32B-Instruct": "32B", | |
| "Qwen/Qwen3-VL-30B-A3B-Instruct": "30B-A3B", | |
| "Qwen/Qwen3-VL-235B-A22B-Instruct": "235B-A22B", | |
| } | |
| _RESOLUTION_SHIFT_DEFAULTS: ClassVar[dict[(ModelSize, Resolution), float]] = { | |
| ("8B", "256"): 3.0, | |
| ("8B", "480"): 5.0, | |
| ("8B", "720"): 10.0, | |
| ("8B", "768"): 10.0, | |
| ("32B", "256"): 5.0, | |
| ("32B", "480"): 5.0, | |
| ("32B", "720"): 5.0, | |
| ("32B", "768"): 5.0, | |
| } | |
| def build_sample(self, *, model_config: Any) -> OmniSampleArgs: | |
| model_config = cast("OmniMoTModelConfig", model_config) | |
| sample_meta = self.sample_meta | |
| # Apply per-modality defaults from JSON config files. | |
| # User-provided values take precedence over JSON defaults. | |
| if self.defaults_file is not None: | |
| defaults = json.loads(self.defaults_file.read_text()) | |
| else: | |
| defaults = _load_modality_defaults(sample_meta.model_mode) | |
| overrides = self.model_dump(exclude_none=True) | |
| shift_configured = "shift" in overrides or defaults.get("shift") is not None | |
| user_fields = frozenset(overrides) | |
| merged_data = _deep_merge(defaults, overrides) | |
| merged_data = {k: v for k, v in merged_data.items() if k in type(self).model_fields} | |
| merged = type(self).model_validate(merged_data) | |
| self.__dict__.update(merged.__dict__) | |
| self.model_mode = sample_meta.model_mode | |
| self._build_sample() | |
| self._build_sampling(model_config=model_config, sample_meta=sample_meta) | |
| self._build_text_data(model_config=model_config, sample_meta=sample_meta) | |
| self._build_vision_data(model_config=model_config, sample_meta=sample_meta) | |
| self._build_prompt_upsampling(model_config=model_config) | |
| self._build_action_data(model_config=model_config, sample_meta=sample_meta) | |
| self._build_sound_data(model_config=model_config, sample_meta=sample_meta) | |
| self._build_reasoner_data(model_config=model_config, sample_meta=sample_meta) | |
| self._build_transfer_data( | |
| model_config=model_config, sample_meta=sample_meta, user_fields=user_fields | |
| ) | |
| if not shift_configured and not sample_meta.model_mode.is_reasoner: | |
| model_size = self._VLM_MODEL_SIZE[model_config.vlm_config.model_name] | |
| key = (model_size, self.resolution) | |
| if key in self._RESOLUTION_SHIFT_DEFAULTS: | |
| self.shift = self._RESOLUTION_SHIFT_DEFAULTS[key] | |
| # Engage the in-model (V4.2-template) native upsampler exactly | |
| # when the user opted into upsampling AND the dense | |
| # (endpoint-backed) path did not successfully apply it. | |
| # ``prompt_upsampling_applied`` is: | |
| # * ``True`` — dense path applied, native should skip. | |
| # * ``False`` — dense path declined (no endpoint configured), | |
| # native should take over. | |
| # * ``None`` — dense path never ran. Two cases produce this: | |
| # (a) release builds, where the dense dispatcher and the | |
| # ``prompt_upsampling_applied`` field itself are both | |
| # stripped, so the attribute does not exist on the | |
| # instance — ``getattr(..., None)`` papers over the | |
| # AttributeError; | |
| # (b) ``prompt_upsampling`` was not requested, in which | |
| # case the leading ``is True`` short-circuits anyway. | |
| # Both ``False`` and ``None`` should engage native upsampling | |
| # when the user opted in, so the gate is ``not applied`` rather | |
| # than ``applied is False`` (which would miss case (a)). | |
| prompt_upsampling_applied = getattr(self, "prompt_upsampling_applied", None) | |
| native_prompt_upsampling = self.prompt_upsampling is True and not prompt_upsampling_applied | |
| return self._build(OmniSampleArgs, native_prompt_upsampling=native_prompt_upsampling) | |
| _MODEL_MEMORY_FACTOR: int = int(1e9) * 2 * 2 # 1B params/tower * 2 bytes/param (bfloat16) * 2 towers | |
| MODEL_MEMORY_BYTES_BY_SIZE: dict[ModelSize, int] = { | |
| "0.6B": round(0.6 * _MODEL_MEMORY_FACTOR), | |
| "2B": 2 * _MODEL_MEMORY_FACTOR, | |
| "8B": 8 * _MODEL_MEMORY_FACTOR, | |
| "30B-A3B": 30 * _MODEL_MEMORY_FACTOR, | |
| "32B": 32 * _MODEL_MEMORY_FACTOR, | |
| "235B-A22B": 235 * _MODEL_MEMORY_FACTOR, | |
| } | |
| _CHECKPOINTS: dict[str, CheckpointConfig] = { | |
| "Cosmos3-Nano": CheckpointConfig( | |
| model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["8B"], | |
| config_file=str(CONFIG_DIR / "model/Cosmos3-Nano.yaml"), | |
| s3_uri="s3://bucket1/cosmos3_vfm/cosmos3_ga_midtraining/cosmos3_ga_16bm8b_v2_midtrain/checkpoints/iter_000006000/", | |
| hf=CheckpointDirHf( | |
| repository="nvidia/Cosmos3-Nano", | |
| revision="main", | |
| ), | |
| ), | |
| "Cosmos3-Super": CheckpointConfig( | |
| model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["32B"], | |
| config_file=str(CONFIG_DIR / "model/Cosmos3-Super.yaml"), | |
| s3_uri="s3://bucket1/cosmos3_vfm/cosmos3_ga_midtraining/cosmos3_ga_64bm32b_v3_midtrain/checkpoints/iter_000001800/", | |
| hf=CheckpointDirHf( | |
| repository="nvidia/Cosmos3-Super", | |
| revision="main", | |
| ), | |
| ), | |
| # Task-specialized Super variants published as diffusers HF checkpoints. | |
| # s3_uri is unused for HF-backed checkpoints (kept for parity with the | |
| # registry schema); the architecture lives in each model YAML. | |
| "Cosmos3-Super-Image2Video": CheckpointConfig( | |
| model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["32B"], | |
| config_file=str(CONFIG_DIR / "model/Cosmos3-Super.yaml"), | |
| s3_uri="s3://bucket1/cosmos3_vfm/cosmos3_ga_image2video/", | |
| hf=CheckpointDirHf( | |
| repository="nvidia/Cosmos3-Super-Image2Video", | |
| revision="main", | |
| ), | |
| # Self-contained checkpoint: use its bundled processor instead of | |
| # downloading the base Cosmos3-Super repo just for the tokenizer. | |
| vlm_processor_from_checkpoint=True, | |
| ), | |
| "Cosmos3-Super-Text2Image": CheckpointConfig( | |
| model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["32B"], | |
| config_file=str(CONFIG_DIR / "model/Cosmos3-Super.yaml"), | |
| s3_uri="s3://bucket1/cosmos3_vfm/cosmos3_ga_text2image/", | |
| hf=CheckpointDirHf( | |
| repository="nvidia/Cosmos3-Super-Text2Image", | |
| revision="main", | |
| ), | |
| # Self-contained checkpoint: use its bundled processor instead of | |
| # downloading the base Cosmos3-Super repo just for the tokenizer. | |
| vlm_processor_from_checkpoint=True, | |
| ), | |
| } | |
| DEFAULT_CHECKPOINT_NAME = "Cosmos3-Nano" | |
| DEFAULT_CHECKPOINT = _CHECKPOINTS[DEFAULT_CHECKPOINT_NAME] | |
| MAX_CP_SIZE = 32 | |
| CpSize = Annotated[int, pydantic.Field(ge=1, le=MAX_CP_SIZE)] | |
| class OmniSetupArgs(SetupArgs): | |
| variant: Suppress[Literal["omni"]] = "omni" | |
| """Discriminator.""" | |
| # pyrefly: ignore[bad-override] | |
| sample_overrides: OmniSampleOverrides | |
| sampler: Literal["unipc", "edm"] | |
| # Override defaults | |
| cp_size: CpSize | |
| def get_sample_overrides_cls(cls) -> type[SampleOverrides]: | |
| return OmniSampleOverrides | |
| def get_sample_args_cls(cls) -> type[SampleArgs]: | |
| return OmniSampleArgs | |
| def get_inference_cls(cls) -> type["Inference"]: | |
| from cosmos_framework.inference.inference import OmniInference | |
| return OmniInference | |
| def _validate_parallelism(self) -> Self: | |
| world_size = int(os.environ.get("WORLD_SIZE", "0")) | |
| if world_size: | |
| if self.dp_shard_size * self.dp_replicate_size > world_size: | |
| raise ValueError( | |
| f"dp_shard_size({self.dp_shard_size}) * dp_replicate_size({self.dp_replicate_size}) must be <= WORLD_SIZE({world_size})" | |
| ) | |
| if world_size % (self.dp_shard_size * self.dp_replicate_size) != 0: | |
| raise ValueError( | |
| f"dp_shard_size({self.dp_shard_size}) * dp_replicate_size({self.dp_replicate_size}) must divide WORLD_SIZE({world_size})" | |
| ) | |
| if world_size: | |
| if self.cp_size * self.cfgp_size > world_size: | |
| raise ValueError( | |
| f"cp_size({self.cp_size}) * cfgp_size({self.cfgp_size}) must be <= WORLD_SIZE({world_size})" | |
| ) | |
| if world_size % (self.cp_size * self.cfgp_size) != 0: | |
| raise ValueError( | |
| f"cp_size({self.cp_size}) * cfgp_size({self.cfgp_size}) must divide WORLD_SIZE({world_size})" | |
| ) | |
| return self | |
| class OmniSetupOverrides(SetupOverrides): | |
| variant: Suppress[Literal["omni"]] = "omni" | |
| """Discriminator.""" | |
| CHECKPOINTS: ClassVar[dict[str, CheckpointConfig]] = _CHECKPOINTS | |
| sample_overrides: OmniSampleOverrides = OmniSampleOverrides() | |
| model_size: Training[ModelSize | None] = None | |
| sampler: Literal["unipc", "edm"] = "unipc" | |
| # Override defaults | |
| dp_replicate_size: pydantic.NonNegativeInt = 0 | |
| dp_shard_size: pydantic.NonNegativeInt = 0 | |
| cp_size: CpSize | Literal[0] = 0 | |
| cfgp_size: CfgpSize | Literal[0] = 0 | |
| use_cuda_graphs: bool = False | |
| compiled_region: Literal["all", "language"] = "all" | |
| # Unsupported | |
| tp_size: Suppress[pydantic.NonNegativeInt] = 1 | |
| def _build_model_parallelism(self, world_size: int, device_memory_bytes: int): | |
| if not self.dp_shard_size: | |
| # Shard the model across every rank by default (full FSDP). world_size == 0 | |
| # means we're not under torchrun (single process) -> a single shard. | |
| self.dp_shard_size = max(1, world_size) | |
| if not self.dp_replicate_size: | |
| self.dp_replicate_size = max(1, world_size // self.dp_shard_size) | |
| def _build_context_parallelism(self, world_size: int): | |
| if not self.cfgp_size: | |
| match self.parallelism_preset: | |
| case "throughput": | |
| self.cfgp_size = 1 | |
| case "latency": | |
| self.cfgp_size = max(1, min(2, world_size)) | |
| case _: | |
| assert_never(self.parallelism_preset) | |
| if not self.cp_size: | |
| match self.parallelism_preset: | |
| case "throughput": | |
| self.cp_size = 1 | |
| case "latency": | |
| self.cp_size = max(1, min(MAX_CP_SIZE, world_size // self.cfgp_size)) | |
| case _: | |
| assert_never(self.parallelism_preset) | |
| def _build_parallelism(self, world_size: int | None, local_world_size: int | None, device_memory_bytes: int | None): | |
| if world_size is None: | |
| world_size = int(os.environ.get("WORLD_SIZE", "0")) | |
| if local_world_size is None: | |
| local_world_size = int(os.environ.get("LOCAL_WORLD_SIZE", str(world_size))) | |
| if device_memory_bytes is None: | |
| device_memory_bytes = _get_device_memory_bytes() | |
| if self.model_memory_bytes is None and self.model_size is not None: | |
| self.model_memory_bytes = MODEL_MEMORY_BYTES_BY_SIZE[self.model_size] | |
| self._build_model_parallelism(world_size=world_size, device_memory_bytes=device_memory_bytes) | |
| self._build_context_parallelism(world_size=world_size) | |
| def build_setup( | |
| self, world_size: int | None = None, local_world_size: int | None = None, device_memory_bytes: int | None = None | |
| ) -> OmniSetupArgs: | |
| self._build_setup() | |
| self._build_checkpoint(checkpoints=self.CHECKPOINTS) | |
| self._build_parallelism( | |
| world_size=world_size, local_world_size=local_world_size, device_memory_bytes=device_memory_bytes | |
| ) | |
| return self._build(OmniSetupArgs) | |
| # Reserved: the memory-based shard-size heuristic. No longer used as the default | |
| # (we now shard across every rank), but kept for future opt-in / reference. | |
| def _get_dp_shard_size( | |
| model_memory_bytes: int, device_memory_bytes: int, device_memory_utilization: float = 0.75 | |
| ) -> int: | |
| return math.ceil(model_memory_bytes / device_memory_bytes / device_memory_utilization) | |
| def _get_device_memory_bytes() -> int: | |
| try: | |
| pynvml.nvmlInit() | |
| handle = pynvml.nvmlDeviceGetHandleByIndex(0) | |
| info = pynvml.nvmlDeviceGetMemoryInfo(handle) | |
| pynvml.nvmlShutdown() | |
| return info.total | |
| except Exception: | |
| # Fallback for unified memory architectures (e.g., GB10) where | |
| # nvmlDeviceGetMemoryInfo is not supported. | |
| import torch | |
| if torch.cuda.is_available(): | |
| return int(torch.cuda.get_device_properties(0).total_memory) | |
| return 128 * 1024**3 # Default 128GB | |