Spaces:
Running on L40S
Running on L40S
File size: 52,351 Bytes
9f818c5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 | # 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
|