Spaces:
Running on L40S
Running on L40S
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: OpenMDW-1.1 | |
| import abc | |
| import functools | |
| import importlib | |
| import json | |
| import re | |
| import typing | |
| from pathlib import Path | |
| from typing import TYPE_CHECKING, Any, Literal | |
| import attrs | |
| import cattrs | |
| import cattrs.preconf.json | |
| import omegaconf | |
| import pydantic | |
| import tomllib | |
| import torch | |
| import yaml | |
| from typing_extensions import assert_never | |
| from cosmos_framework.inference.common.init import is_rank0 | |
| from cosmos_framework.utils import log | |
| from cosmos_framework.utils.flags import TRAINING | |
| from cosmos_framework.utils.lazy_config.registry import convert_target_to_string, locate | |
| if TYPE_CHECKING: | |
| from cosmos_framework.utils.config import Config | |
| ROOT_DIR = Path(__file__).parents[2].absolute() | |
| PACKAGE_DIR = ROOT_DIR / "inference" | |
| CONFIG_DIR = PACKAGE_DIR / "configs" | |
| def load_config( | |
| config_file: str, | |
| experiment: str, | |
| *, | |
| overrides: list[str] = [], | |
| ) -> "Config": | |
| """Load config from config store.""" | |
| assert TRAINING | |
| from cosmos_framework.utils import config_helper | |
| config_module = importlib.import_module(config_helper.get_config_module(config_file)) | |
| config = config_module.make_config() | |
| config = config_helper.override(config, ["--", f"experiment={experiment}", *overrides]) | |
| return config | |
| def save_config(config: Any, output_dir: Path) -> None: | |
| """Save config to output directory for debugging.""" | |
| if not is_rank0(): | |
| return | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| config_file = output_dir / "config.yaml" | |
| serialize_config(config, config_file, invalid="ignore") | |
| log.info(f"Saved config to {config_file}") | |
| InvalidMode = Literal["error", "warn", "ignore"] | |
| """How to handle invalid fields. | |
| * error: raise an error. | |
| * warn: log a warning. | |
| * ignore: | |
| * When unstructuring, convert to string. | |
| * When structuring, leave unstructured. | |
| """ | |
| _InvalidMode = InvalidMode # For backward compatibility. | |
| _InvalidModeValidator = pydantic.TypeAdapter(_InvalidMode) | |
| class _UnstructureOptions: | |
| converter: cattrs.Converter | |
| invalid: _InvalidMode | |
| class _FixOptions: | |
| converter: cattrs.Converter | |
| invalid: _InvalidMode | |
| add_defaults: bool | |
| def unstructure_config(config: Any, *, invalid: _InvalidMode = "error") -> dict: | |
| """Unstructure config to primitive types.""" | |
| _InvalidModeValidator.validate_python(invalid) | |
| options = _UnstructureOptions(converter=config_converter, invalid=invalid) | |
| return _unstructure(config, prefix=(), options=options) | |
| def structure_config(config_dict: Any, target: type | str | None = None, /, *, invalid: _InvalidMode = "error") -> Any: | |
| """Structure config from primitive types.""" | |
| if target is None: | |
| target = config_dict[_TYPE_KEY] | |
| if isinstance(target, str): | |
| target = locate(target) | |
| config_dict = fix_config_dict(config_dict, invalid=invalid, add_defaults=True) | |
| return config_converter.structure(config_dict, target) | |
| def serialize_config_dict(config_dict: dict, config_file: Path) -> None: | |
| """Serialize config dict to a file.""" | |
| match config_file.suffix.lower(): | |
| case ".yaml" | ".yml": | |
| config_str = yaml.safe_dump(config_dict, sort_keys=True) | |
| case ".json": | |
| config_str = json.dumps(config_dict, indent=2, sort_keys=True) | |
| case _: | |
| raise ValueError(f"Unsupported file extension '{config_file.suffix}'") | |
| config_str = apply_config_replacements(config_str) | |
| config_file.parent.mkdir(parents=True, exist_ok=True) | |
| config_file.write_text(config_str) | |
| def serialize_config(config: Any, config_file: Path, /, *, invalid: _InvalidMode = "error") -> None: | |
| """Serialize config to a file.""" | |
| config_dict = unstructure_config(config, invalid=invalid) | |
| serialize_config_dict(config_dict, config_file) | |
| def deserialize_config_dict(config_file: Path) -> dict: | |
| """Deserialize config dict from a file.""" | |
| config_str = config_file.read_text() | |
| config_str = undo_config_replacements(config_str) | |
| match config_file.suffix.lower(): | |
| case ".yaml" | ".yml": | |
| config_dict = yaml.safe_load(config_str) | |
| case ".json": | |
| config_dict = json.loads(config_str) | |
| case _: | |
| raise ValueError(f"Unsupported file extension '{config_file.suffix}'") | |
| config_dict = fix_config_dict(config_dict) | |
| return config_dict | |
| def deserialize_config( | |
| config_file: Path, target: type | str | None = None, /, *, invalid: _InvalidMode = "error" | |
| ) -> Any: | |
| """Deserialize config from a file.""" | |
| config_dict = deserialize_config_dict(config_file) | |
| return structure_config(config_dict, target, invalid=invalid) | |
| def fix_config_dict(config_dict: Any, *, invalid: _InvalidMode = "error", add_defaults: bool = False) -> dict: | |
| """Fix config dict. | |
| * Unstructure. | |
| * Fix legacy fields. | |
| * Optional: Add missing default fields. | |
| """ | |
| _InvalidModeValidator.validate_python(invalid) | |
| config_dict = unstructure_config(config_dict) | |
| return _fix( | |
| config_dict, | |
| prefix=(), | |
| options=_FixOptions(converter=config_converter, invalid=invalid, add_defaults=add_defaults), | |
| ) | |
| _TYPE_KEY = "_type" | |
| _TARGET_KEY = "_target_" | |
| def _join_prefix(prefix: tuple[Any, ...]) -> str: | |
| return ".".join(str(p) for p in prefix) | |
| def get_default_params(tp: type) -> dict: | |
| from cosmos_framework.utils.lazy_config.lazy_call import get_default_params | |
| return {k: v for k, v in get_default_params(tp).items() if v is not attrs.NOTHING} | |
| def _fix(obj: Any, prefix: tuple[Any, ...], options: _FixOptions) -> Any: | |
| # Handle primitive types. | |
| if isinstance(obj, (int, float, str, bool)) or obj is None: | |
| return obj | |
| if isinstance(obj, list): | |
| return [_fix(item, prefix=(*prefix, i), options=options) for i, item in enumerate(obj)] | |
| # Handle objects. | |
| if isinstance(obj, dict): | |
| # Fix metadata. | |
| metadata = obj.pop("_metadata", {}) | |
| tp_str = obj.pop(_TYPE_KEY, None) | |
| tp_str = metadata.pop("object_type", None) or tp_str | |
| if tp_str is not None and tp_str.startswith("omegaconf."): | |
| tp_str = None | |
| if tp_str is not None: | |
| obj[_TYPE_KEY] = tp_str | |
| # Recurse. | |
| data = {k: _fix(v, prefix=(*prefix, k), options=options) for k, v in obj.items()} | |
| # Add missing default fields. | |
| target_str = obj.get(_TARGET_KEY) or obj.get(_TYPE_KEY) | |
| if options.add_defaults and target_str is not None: | |
| target = locate(target_str) | |
| data = get_default_params(target) | data | |
| return data | |
| raise ValueError(f"Unsupported type: {type(obj)}") | |
| def _unstructure(obj: Any, prefix: tuple[Any, ...], options: _UnstructureOptions) -> Any: | |
| """Wrapper around cattrs 'unstructure' to handle missing type annotations. | |
| For values missing type annotations, the type of the object is included in | |
| the unstructured data. | |
| """ | |
| # Handle primitive types. | |
| if isinstance(obj, (int, float, str, bool)) or obj is None: | |
| return obj | |
| if isinstance(obj, list): | |
| return [_unstructure(item, prefix=(*prefix, i), options=options) for i, item in enumerate(obj)] | |
| if isinstance(obj, dict): | |
| return {k: _unstructure(v, prefix=(*prefix, k), options=options) for k, v in obj.items()} | |
| # Handle objects. | |
| try: | |
| # Use cattrs to unstructure the object. | |
| data = options.converter.unstructure(obj) | |
| if data is obj: | |
| raise ValueError(f"Unsupported type: {type(obj)}") | |
| if isinstance(data, dict) and not isinstance(obj, omegaconf.DictConfig): | |
| if _TYPE_KEY in data: | |
| raise ValueError(f"Already has a {_TYPE_KEY} field: {data[_TYPE_KEY]}") | |
| data[_TYPE_KEY] = convert_target_to_string(type(obj)) | |
| except Exception as e: | |
| # For classes / functions / bound methods, emit the importable dotted | |
| # path instead of `str(obj)` (which produces `<class '...'>` etc. and | |
| # breaks hydra.utils.instantiate / cosmos_framework.scripts.export_model on the | |
| # loaded YAML). | |
| def _to_safe_string(value): | |
| # Preserve primitives — `str(True)` is the literal string `"True"`, | |
| # which yaml then quotes and downstream consumers parse as a string | |
| # instead of the original bool/int/float. | |
| if isinstance(value, (bool, int, float, str)) or value is None: | |
| return value | |
| try: | |
| if callable(value): | |
| return convert_target_to_string(value) | |
| except Exception: | |
| pass | |
| return str(value) | |
| if options.invalid == "ignore": | |
| return _to_safe_string(obj) | |
| msg = f"Invalid value '{_join_prefix(prefix)}': {e}" | |
| if options.invalid == "warn": | |
| log.warning(msg) | |
| return _to_safe_string(obj) | |
| if options.invalid == "error": | |
| raise ValueError(msg) from e | |
| assert_never(options.invalid) | |
| # Recursively unstructure the data. | |
| return _unstructure(data, prefix=prefix, options=options) | |
| config_converter = cattrs.preconf.json.make_converter() | |
| # type | |
| def _is_type_cls(cls: Any) -> bool: | |
| return cls in [type, abc.ABCMeta] or typing.get_origin(cls) is type | |
| def _unstructure_type(cls: Any | None) -> str | None: | |
| if cls is None: | |
| return None | |
| return convert_target_to_string(cls) | |
| def _structure_type(data: Any, _cls: Any) -> type: | |
| assert isinstance(data, str) | |
| return locate(data) | |
| config_converter.register_unstructure_hook_func(_is_type_cls, _unstructure_type) | |
| config_converter.register_structure_hook_func(_is_type_cls, _structure_type) | |
| # functools.partial | |
| def _unstructure_functools_partial(obj: functools.partial | None) -> dict | None: | |
| if obj is None: | |
| return None | |
| # Convert to omegaconf: https://hydra.cc/docs/advanced/instantiate_objects/overview/ | |
| return { | |
| _TARGET_KEY: convert_target_to_string(obj.func), | |
| "_args_": obj.args, | |
| **obj.keywords, | |
| } | |
| def _structure_functools_partial(data: Any, _cls: Any) -> functools.partial: | |
| assert isinstance(data, dict) | |
| func = locate(data.pop(_TARGET_KEY)) | |
| args = data.pop("_args_") | |
| return functools.partial(func, *args, **data) | |
| config_converter.register_unstructure_hook(functools.partial, _unstructure_functools_partial) | |
| config_converter.register_structure_hook(functools.partial, _structure_functools_partial) | |
| # We need allow objects, because we add default fields to the config. | |
| _OMEGACONF_FLAGS = dict(allow_objects=True) | |
| # omegaconf.DictConfig | |
| def _is_omegaconf_dict_cls(cls: Any) -> bool: | |
| return isinstance(cls, type) and issubclass(cls, omegaconf.DictConfig) | |
| def _unstructure_omegaconf_dict(obj: omegaconf.DictConfig | None) -> dict | None: | |
| if obj is None or obj._is_none(): | |
| return None | |
| # Create a shallow copy without recursion or resolution. | |
| data = dict(obj.items_ex(resolve=False)) | |
| target_str = data.get(_TARGET_KEY) | |
| if target_str is not None and not isinstance(target_str, str): | |
| data[_TARGET_KEY] = convert_target_to_string(target_str) | |
| if obj._metadata.object_type not in [None, dict]: | |
| data[_TYPE_KEY] = convert_target_to_string(obj._metadata.object_type) | |
| return data | |
| def _structure_omegaconf_dict(data: Any, _cls: Any) -> omegaconf.DictConfig | None: | |
| if data is None: | |
| return None | |
| assert isinstance(data, dict), type(data) | |
| # Ideally, we should use omegaconf.structured if _TYPE_KEY is present. | |
| return omegaconf.OmegaConf.create(data, flags=_OMEGACONF_FLAGS) | |
| config_converter.register_unstructure_hook_func(_is_omegaconf_dict_cls, _unstructure_omegaconf_dict) | |
| config_converter.register_structure_hook_func(_is_omegaconf_dict_cls, _structure_omegaconf_dict) | |
| # omegaconf.ListConfig | |
| def _is_omegaconf_list_cls(obj: Any) -> bool: | |
| return isinstance(obj, type) and issubclass(obj, omegaconf.ListConfig) | |
| def _unstructure_omegaconf_list(obj: omegaconf.ListConfig | None) -> list | None: | |
| if obj is None or obj._is_none(): | |
| return None | |
| # Create a shallow copy without recursion or resolution. | |
| return list(obj._iter_ex(resolve=False)) | |
| def _structure_omegaconf_list(data: Any, _: Any) -> omegaconf.ListConfig | None: | |
| if data is None: | |
| return None | |
| assert isinstance(data, list), type(data) | |
| return omegaconf.OmegaConf.create(data, flags=_OMEGACONF_FLAGS) | |
| config_converter.register_unstructure_hook_func(_is_omegaconf_list_cls, _unstructure_omegaconf_list) | |
| config_converter.register_structure_hook_func(_is_omegaconf_list_cls, _structure_omegaconf_list) | |
| # torch types | |
| def _unstructure_torch_type(obj: Any | None) -> str | None: | |
| if obj is None: | |
| return None | |
| return str(obj).removeprefix("torch.") | |
| def _structure_torch_type(data: Any, _cls: Any) -> Any | None: | |
| if data is None: | |
| return None | |
| assert isinstance(data, str), type(data) | |
| return getattr(torch, data) | |
| for _torch_type in [ | |
| torch.dtype, | |
| torch.layout, | |
| torch.memory_format, | |
| ]: | |
| config_converter.register_unstructure_hook(_torch_type, _unstructure_torch_type) | |
| config_converter.register_structure_hook(_torch_type, _structure_torch_type) | |
| # torch.device | |
| def _unstructure_torch_device(obj: torch.device | None) -> str | None: | |
| if obj is None: | |
| return None | |
| return str(obj) | |
| def _structure_torch_device(data: Any, _cls: Any) -> torch.device | None: | |
| if data is None: | |
| return None | |
| assert isinstance(data, str), type(data) | |
| return torch.device(data) | |
| config_converter.register_unstructure_hook(torch.device, _unstructure_torch_device) | |
| config_converter.register_structure_hook(torch.device, _structure_torch_device) | |
| # torch.Tensor | |
| def _unstructure_torch_tensor(obj: torch.Tensor | None) -> list | None: | |
| if obj is None: | |
| return None | |
| return obj.detach().cpu().tolist() | |
| def _structure_torch_tensor(data: Any, _cls: Any) -> torch.Tensor | None: | |
| if data is None: | |
| return None | |
| assert isinstance(data, list), type(data) | |
| return torch.tensor(data) | |
| config_converter.register_unstructure_hook(torch.Tensor, _unstructure_torch_tensor) | |
| config_converter.register_structure_hook(torch.Tensor, _structure_torch_tensor) | |
| CONFIG_REPLACEMENTS = [ | |
| (r"(?<!\.)\bimaginaire\.", r"cosmos3._src.imaginaire."), | |
| (r"(?<!/)\bimaginaire/", r"cosmos3/_src/imaginaire/"), | |
| (r"(?<!\.)\bprojects\.cosmos3\.vfm\.", r"cosmos3._src.vfm."), | |
| (r"(?<!/)\bprojects/cosmos3/vfm/", r"cosmos3/_src/vfm/"), | |
| ] | |
| # Runtime layout detection — picks inverse rules based on which python path | |
| # our shipped modules live at. `cosmos_framework.model.vfm` is unique to the | |
| # cosmos_training release; if it imports, we're in that tree and rewrite | |
| # cosmos3._src.* → cosmos.* / configs.* . Otherwise fall back to the | |
| # internal-dev paths (imaginaire.* / projects.cosmos3.vfm.*). | |
| try: | |
| import cosmos_framework.model.vfm # noqa: F401 | |
| CONFIG_REPLACEMENTS_INVERSE = [ | |
| # File-path entries first (longer-match wins so vfm-prefixed names | |
| # don't get short-circuited by the bare imaginaire rule). | |
| (r"(?<!/)\bcosmos3/_src/vfm/configs/base/", r"cosmos_framework/configs/base/"), | |
| (r"(?<!/)\bcosmos3/_src/vfm/algorithm/", r"cosmos_framework/model/vfm/algorithm/"), | |
| (r"(?<!/)\bcosmos3/_src/vfm/diffusion/", r"cosmos_framework/model/vfm/diffusion/"), | |
| (r"(?<!/)\bcosmos3/_src/vfm/models/", r"cosmos_framework/model/vfm/"), | |
| (r"(?<!/)\bcosmos3/_src/vfm/tokenizers/", r"cosmos_framework/model/vfm/tokenizers/"), | |
| (r"(?<!/)\bcosmos3/_src/vfm/processors/", r"cosmos_framework/data/vfm/processors/"), | |
| (r"(?<!/)\bcosmos3/_src/vfm/datasets/", r"cosmos_framework/data/vfm/"), | |
| (r"(?<!/)\bcosmos3/_src/vfm/utils/", r"cosmos_framework/utils/vfm/"), | |
| (r"(?<!/)\bcosmos3/_src/vfm/", r"cosmos_framework/_vfm_unmapped/"), | |
| (r"(?<!/)\bcosmos3/_src/imaginaire/", r"cosmos_framework/"), | |
| # Module-path entries — same order. | |
| (r"(?<!\.)\bcosmos3\._src\.vfm\.configs\.base\.", r"cosmos_framework.configs.base."), | |
| (r"(?<!\.)\bcosmos3\._src\.vfm\.algorithm\.", r"cosmos_framework.model.vfm.algorithm."), | |
| (r"(?<!\.)\bcosmos3\._src\.vfm\.diffusion\.", r"cosmos_framework.model.vfm.diffusion."), | |
| (r"(?<!\.)\bcosmos3\._src\.vfm\.models\.", r"cosmos_framework.model.vfm."), | |
| (r"(?<!\.)\bcosmos3\._src\.vfm\.tokenizers\.", r"cosmos_framework.model.vfm.tokenizers."), | |
| (r"(?<!\.)\bcosmos3\._src\.vfm\.processors\.", r"cosmos_framework.data.vfm.processors."), | |
| (r"(?<!\.)\bcosmos3\._src\.vfm\.datasets\.", r"cosmos_framework.data.vfm."), | |
| (r"(?<!\.)\bcosmos3\._src\.vfm\.utils\.", r"cosmos_framework.utils.vfm."), | |
| (r"(?<!\.)\bcosmos3\._src\.vfm\.", r"cosmos_framework._vfm_unmapped."), | |
| (r"(?<!\.)\bcosmos3\._src\.imaginaire\.", r"cosmos_framework."), | |
| ] | |
| except ImportError: | |
| CONFIG_REPLACEMENTS_INVERSE = [ | |
| (r"(?<!\.)\bcosmos3\._src\.imaginaire\.", r"imaginaire."), | |
| (r"(?<!/)\bcosmos3/_src/imaginaire/", r"imaginaire/"), | |
| (r"(?<!\.)\bcosmos3\._src\.vfm\.", r"projects.cosmos3.vfm."), | |
| (r"(?<!/)\bcosmos3/_src/vfm/", r"projects/cosmos3/vfm/"), | |
| ] | |
| def get_release_config() -> dict: | |
| # Not all checkouts ship the CI release-config (e.g. external forks, slim | |
| # distributions). When absent, treat as "no replacements" so downstream | |
| # callers (notably cosmos_framework.scripts.export_model's post-write rewrite of | |
| # config.json) don't crash on the final step. | |
| path = ROOT_DIR / "ci/internal/release.toml" | |
| if not path.exists(): | |
| return {"literal_replacements": []} | |
| return tomllib.loads(path.read_text()) | |
| # Backward-compat: rewrite legacy MoT wrapper class names embedded in saved checkpoint configs. | |
| # | |
| # Old checkpoints' hydra configs carry ``_target_`` strings like | |
| # ``...unified_mot.Qwen3VLTextConfig.from_json_file`` that referred to the MoT *wrapper* classes | |
| # before they were renamed to the ``*MoTConfig`` family. The new ``unified_mot`` module no longer | |
| # exposes those names, so loading an old checkpoint without rewriting blows up at instantiation. | |
| # | |
| # Patterns are anchored to ``unified_mot.`` so we never touch the HF text-config classes that share | |
| # the legacy names (``configuration_qwen3_vl.Qwen3VLTextConfig`` etc., which remain canonical). | |
| # | |
| # This block lives OUTSIDE the COSMOS-RELEASE-IGNORE markers above so the rewrite ships in the | |
| # released ``cosmos3`` package and fires inside ``from_pretrained_dcp``'s checkpoint-load path, | |
| # not just in the source tree. | |
| LEGACY_CLASS_RENAMES: list[tuple[str, str]] = [ | |
| (r"\bunified_mot\.Qwen3VLTextConfig\b", r"unified_mot.Qwen3VLMoTConfig"), | |
| (r"\bunified_mot\.Qwen3VLMoeTextConfig\b", r"unified_mot.Qwen3VLMoeMoTConfig"), | |
| (r"\bunified_mot\.Nemotron3DenseVLTextConfig\b", r"unified_mot.Nemotron3DenseVLMoTConfig"), | |
| ] | |
| def replace_case_preserving(text: str, old: str, new: str) -> str: | |
| """Similar to `str.replace()`, but preserves the case of the matched text.""" | |
| def replace_func(match: re.Match[str]) -> str: | |
| original = match.group() | |
| if original.isupper(): | |
| return new.upper() | |
| if original.islower(): | |
| return new.lower() | |
| if original == old.capitalize(): | |
| return new.capitalize() | |
| if original[0].isupper(): | |
| return new.upper() | |
| return new.lower() | |
| pattern = re.compile(re.escape(old), re.IGNORECASE) | |
| return pattern.sub(replace_func, text) | |
| def apply_config_replacements(config_str: str) -> str: | |
| """Apply config replacements to a config string.""" | |
| release_config = get_release_config() | |
| for pattern, repl in CONFIG_REPLACEMENTS: | |
| config_str = re.sub(pattern, repl, config_str) | |
| # Apply all case sensitive replacements first | |
| for entry in release_config["literal_replacements"]: | |
| if entry.get("case_sensitive", False): | |
| config_str = config_str.replace(entry["search"], entry["replace"]) | |
| for entry in release_config["literal_replacements"]: | |
| if not entry.get("case_sensitive", False): | |
| config_str = replace_case_preserving(config_str, entry["search"], entry["replace"]) | |
| return config_str | |
| def undo_config_replacements(config_str: str) -> str: | |
| """Undo config replacements to a config string.""" | |
| for pattern, repl in CONFIG_REPLACEMENTS_INVERSE: | |
| config_str = re.sub(pattern, repl, config_str) | |
| for pattern, repl in LEGACY_CLASS_RENAMES: | |
| config_str = re.sub(pattern, repl, config_str) | |
| return config_str | |
| def undo_config_dict_replacements(config_dict: dict) -> dict: | |
| """Undo config replacements to a config dict.""" | |
| config_str = json.dumps(config_dict) | |
| config_str = undo_config_replacements(config_str) | |
| return json.loads(config_str) | |