dwehr's picture
Migrate action viewer to local Cosmos generation
9f818c5
Raw
History Blame Contribute Delete
52.4 kB
# 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()))
@cache
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"
@property
def is_action(self) -> bool:
return self in ACTION_MODEL_MODES
@property
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"
@classmethod
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:
@property
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
@property
def duration(self) -> float:
return self.num_frames / self.fps
@property
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]
@property
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)."""
@override
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."""
@override
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:
@property
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."""
@pydantic.model_validator(mode="after")
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
@override
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:
@property
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}")
@property
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,
}
@override
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
@override
@classmethod
def get_sample_overrides_cls(cls) -> type[SampleOverrides]:
return OmniSampleOverrides
@override
@classmethod
def get_sample_args_cls(cls) -> type[SampleArgs]:
return OmniSampleArgs
@override
@classmethod
def get_inference_cls(cls) -> type["Inference"]:
from cosmos_framework.inference.inference import OmniInference
return OmniInference
@pydantic.model_validator(mode="after")
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)
@override
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)
@override
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)
@cache
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