Instructions to use nvidia/Cosmos3-Edge-Policy-DROID with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Cosmos
How to use nvidia/Cosmos3-Edge-Policy-DROID with Cosmos:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
| #!/usr/bin/env python3 | |
| # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """ | |
| Convert a Cosmos3 DCP checkpoint to diffusers format. | |
| Example: | |
| CUDA_VISIBLE_DEVICES=0 python scripts/convert_cosmos3_to_diffusers.py \ | |
| --checkpoint-path Cosmos3-Nano \ | |
| --output converted/cosmos3-nano-pipeline \ | |
| --save-pipeline | |
| A raw Cosmos3 Edge DCP checkpoint is detected automatically. The Edge path | |
| loads its EMA denoiser weights directly from DCP, packages the matching | |
| Wan 2.2 VAE, and writes a shared-weight Omni checkpoint: the Diffusers | |
| transformer is reused by the reasoner and only the missing vision tower is | |
| stored separately. | |
| Edge checkpoints trained with action generation are also detected | |
| automatically: the `action2llm`/`llm2action` domain-aware projections and the | |
| action modality embedding are exported onto the transformer's | |
| `action_proj_in`/`action_proj_out`/`action_modality_embed` modules, with | |
| `action_dim` and `num_embodiment_domains` derived from the DCP tensor shapes. | |
| """ | |
| import argparse | |
| import contextlib | |
| import json | |
| import pathlib | |
| import re | |
| import shutil | |
| import torch | |
| from diffusers.models.autoencoders.autoencoder_cosmos3_audio import Cosmos3AVAEAudioTokenizer | |
| DEFAULT_SOUND_TOKENIZER_CONFIG = { | |
| "sampling_rate": 48000, | |
| "vocoder_input_dim": 64, | |
| "dec_dim": 320, | |
| "dec_c_mults": [1, 2, 4, 8, 16], | |
| "dec_strides": [2, 4, 5, 6, 8], | |
| "dec_out_channels": 2, | |
| } | |
| COSMOS3_EDGE_REASONER = "nvidia/Cosmos3-Edge-Reasoner" | |
| COSMOS3_EDGE_REASONER_REVISION = "590c1c0f1cd7146162d478a2180556055c1a252b" | |
| COSMOS3_EDGE_VAE = "Wan-AI/Wan2.2-TI2V-5B-Diffusers" | |
| # Keep the Transformers metadata, but avoid copying its two source shards: | |
| # Edge's language tower is already byte-identical to the base part of the | |
| # converted Diffusers transformer. Only the visual tower and projector have | |
| # to be extracted from source weights. | |
| COSMOS3_EDGE_REASONER_METADATA_FILES = ( | |
| "chat_template.jinja", | |
| "config.json", | |
| "generation_config.json", | |
| "preprocessor_config.json", | |
| "special_tokens_map.json", | |
| "tokenizer.json", | |
| "tokenizer_config.json", | |
| "video_preprocessor_config.json", | |
| ) | |
| COSMOS3_EDGE_REASONER_INDEX_FILE = "model.safetensors.index.json" | |
| COSMOS3_EDGE_REASONER_VISION_SHARD = "00000.safetensors" | |
| COSMOS3_EDGE_REASONER_FILES = ( | |
| *COSMOS3_EDGE_REASONER_METADATA_FILES, | |
| COSMOS3_EDGE_REASONER_INDEX_FILE, | |
| COSMOS3_EDGE_REASONER_VISION_SHARD, | |
| ) | |
| _EDGE_ATTN_KEY_REMAP = ( | |
| (".q_proj_moe_gen.", ".add_q_proj."), | |
| (".k_proj_moe_gen.", ".add_k_proj."), | |
| (".v_proj_moe_gen.", ".add_v_proj."), | |
| (".o_proj_moe_gen.", ".to_add_out."), | |
| (".q_norm_moe_gen.", ".norm_added_q."), | |
| (".k_norm_moe_gen.", ".norm_added_k."), | |
| (".q_proj.", ".to_q."), | |
| (".k_proj.", ".to_k."), | |
| (".v_proj.", ".to_v."), | |
| (".o_proj.", ".to_out."), | |
| ) | |
| _TIME_EMBEDDER_REMAP = { | |
| "mlp.0.weight": "linear_1.weight", | |
| "mlp.0.bias": "linear_1.bias", | |
| "mlp.2.weight": "linear_2.weight", | |
| "mlp.2.bias": "linear_2.bias", | |
| } | |
| def _get_config_value(*configs, name, default=None): | |
| for config in configs: | |
| if config is None: | |
| continue | |
| if hasattr(config, name): | |
| value = getattr(config, name) | |
| if value is not None: | |
| return value | |
| if isinstance(config, dict) and config.get(name) is not None: | |
| return config[name] | |
| return default | |
| def _find_edge_dcp_model_dir(checkpoint_path: pathlib.Path) -> pathlib.Path | None: | |
| candidates = [checkpoint_path, checkpoint_path / "model"] | |
| if checkpoint_path.is_dir(): | |
| candidates.extend(metadata_path.parent for metadata_path in checkpoint_path.glob("iter_*/model/.metadata")) | |
| candidates.extend(metadata_path.parent for metadata_path in checkpoint_path.glob("*/iter_*/model/.metadata")) | |
| for candidate in candidates: | |
| if not (candidate / ".metadata").is_file(): | |
| continue | |
| from torch.distributed.checkpoint.filesystem import FileSystemReader | |
| state_dict_metadata = FileSystemReader(str(candidate)).read_metadata().state_dict_metadata | |
| has_edge_mlp = "net.language_model.model.layers.0.mlp.up_proj.weight" in state_dict_metadata | |
| has_qwen_mlp = "net.language_model.model.layers.0.mlp.gate_proj.weight" in state_dict_metadata | |
| if has_edge_mlp and not has_qwen_mlp: | |
| return candidate | |
| return None | |
| def _remap_edge_dcp_key(key: str) -> str: | |
| if key.startswith("language_model."): | |
| key = key.removeprefix("language_model.").removeprefix("model.") | |
| for old, new in _EDGE_ATTN_KEY_REMAP: | |
| if old in key: | |
| return key.replace(old, new) | |
| return key | |
| if key.startswith("vae2llm."): | |
| return f"proj_in.{key.removeprefix('vae2llm.')}" | |
| if key.startswith("llm2vae."): | |
| return f"proj_out.{key.removeprefix('llm2vae.')}" | |
| if key.startswith("time_embedder."): | |
| time_embedder_key = key.removeprefix("time_embedder.") | |
| return f"time_embedder.{_TIME_EMBEDDER_REMAP[time_embedder_key]}" | |
| if key.startswith("action2llm."): | |
| return f"action_proj_in.{key.removeprefix('action2llm.')}" | |
| if key.startswith("llm2action."): | |
| return f"action_proj_out.{key.removeprefix('llm2action.')}" | |
| if key == "action_modality_embed": | |
| return key | |
| raise ValueError(f"Unsupported Cosmos3 Edge DCP key: {key!r}.") | |
| def _detect_edge_action_config(checkpoint_path: pathlib.Path, use_ema: bool) -> dict | None: | |
| from torch.distributed.checkpoint.filesystem import FileSystemReader | |
| prefix = "net_ema." if use_ema else "net." | |
| metadata = FileSystemReader(str(checkpoint_path)).read_metadata().state_dict_metadata | |
| action_suffixes = ( | |
| "action2llm.fc.weight", | |
| "action2llm.bias.weight", | |
| "llm2action.fc.weight", | |
| "llm2action.bias.weight", | |
| "action_modality_embed", | |
| ) | |
| action_metadata = {suffix: metadata.get(f"{prefix}{suffix}") for suffix in action_suffixes} | |
| present_suffixes = [suffix for suffix, value in action_metadata.items() if value is not None] | |
| if not present_suffixes: | |
| return None | |
| # `action2llm`/`llm2action` are DomainAwareLinear modules: `fc` is an | |
| # Embedding(num_domains, output_size * input_size) and `bias` an | |
| # Embedding(num_domains, output_size), so the shapes fix every config value. | |
| missing_suffixes = [suffix for suffix, value in action_metadata.items() if value is None] | |
| if missing_suffixes: | |
| raise ValueError( | |
| f"Cosmos3 Edge {'EMA' if use_ema else 'regular'} action weights are incomplete: " | |
| f"present={present_suffixes}, missing={missing_suffixes}." | |
| ) | |
| action_shapes = {suffix: tuple(value.size) for suffix, value in action_metadata.items()} | |
| expected_ranks = { | |
| "action2llm.fc.weight": 2, | |
| "action2llm.bias.weight": 2, | |
| "llm2action.fc.weight": 2, | |
| "llm2action.bias.weight": 2, | |
| "action_modality_embed": 1, | |
| } | |
| invalid_shapes = { | |
| suffix: shape for suffix, shape in action_shapes.items() if len(shape) != expected_ranks[suffix] | |
| } | |
| if invalid_shapes: | |
| raise ValueError(f"Cosmos3 Edge action tensors have invalid ranks: {invalid_shapes}.") | |
| num_embodiment_domains, proj_in_flat = action_shapes["action2llm.fc.weight"] | |
| proj_in_domains, hidden_size = action_shapes["action2llm.bias.weight"] | |
| proj_out_domains, proj_out_flat = action_shapes["llm2action.fc.weight"] | |
| proj_out_bias_domains, action_dim = action_shapes["llm2action.bias.weight"] | |
| (action_modality_embed_size,) = action_shapes["action_modality_embed"] | |
| domain_counts = { | |
| "action2llm.fc.weight": num_embodiment_domains, | |
| "action2llm.bias.weight": proj_in_domains, | |
| "llm2action.fc.weight": proj_out_domains, | |
| "llm2action.bias.weight": proj_out_bias_domains, | |
| } | |
| if len(set(domain_counts.values())) != 1: | |
| raise ValueError(f"Cosmos3 Edge action tensors disagree on embodiment domains: {domain_counts}.") | |
| if min(num_embodiment_domains, hidden_size, action_dim) < 1: | |
| raise ValueError(f"Cosmos3 Edge action tensor dimensions must be positive: {action_shapes}.") | |
| expected_projection_size = hidden_size * action_dim | |
| if proj_in_flat != expected_projection_size or proj_out_flat != expected_projection_size: | |
| raise ValueError( | |
| "Cosmos3 Edge action projection shapes are inconsistent: " | |
| f"action2llm.fc.weight={action_shapes['action2llm.fc.weight']}, " | |
| f"llm2action.fc.weight={action_shapes['llm2action.fc.weight']}; both must factor into " | |
| f"hidden_size={hidden_size} x action_dim={action_dim}." | |
| ) | |
| if action_modality_embed_size != hidden_size: | |
| raise ValueError( | |
| "Cosmos3 Edge action modality embedding has the wrong width: " | |
| f"action_modality_embed={action_shapes['action_modality_embed']}, expected ({hidden_size},)." | |
| ) | |
| return { | |
| "action_gen": True, | |
| "action_dim": action_dim, | |
| "num_embodiment_domains": num_embodiment_domains, | |
| } | |
| def _build_edge_transformer(dtype: torch.dtype, action_config: dict | None = None): | |
| import inspect | |
| from accelerate import init_empty_weights | |
| from diffusers.models.transformers.transformer_cosmos3 import Cosmos3OmniTransformer | |
| action_kwargs = dict(action_config or {}) | |
| init_params = inspect.signature(Cosmos3OmniTransformer.__init__).parameters | |
| edge_required_params = { | |
| "hidden_act", | |
| "qk_norm_for_text", | |
| "backbone_type", | |
| "temporal_compression_factor", | |
| } | |
| action_required_params = set(action_kwargs) | |
| missing_edge_params = sorted(edge_required_params - set(init_params)) | |
| missing_action_params = sorted(action_required_params - set(init_params)) | |
| if missing_edge_params or missing_action_params: | |
| missing_descriptions = [] | |
| if missing_edge_params: | |
| missing_descriptions.append(f"Edge backbone parameters {missing_edge_params}") | |
| if missing_action_params: | |
| missing_descriptions.append(f"action-generation parameters {missing_action_params}") | |
| raise RuntimeError( | |
| "The installed diffusers build cannot construct this Cosmos3 Edge checkpoint; it is missing " | |
| f"{', '.join(missing_descriptions)}. Use a build that combines the Edge Nemotron-dense backbone " | |
| "with upstream action generation support (commit bcc20e452)." | |
| ) | |
| with init_empty_weights(): | |
| transformer = Cosmos3OmniTransformer( | |
| **action_kwargs, | |
| attention_bias=False, | |
| attention_dropout=0.0, | |
| base_fps=24, | |
| enable_fps_modulation=True, | |
| head_dim=128, | |
| hidden_act="relu2", | |
| hidden_size=2048, | |
| intermediate_size=9216, | |
| latent_channel=48, | |
| latent_patch_size=2, | |
| num_attention_heads=16, | |
| num_hidden_layers=28, | |
| num_key_value_heads=8, | |
| patch_latent_dim=192, | |
| qk_norm_for_text=False, | |
| rms_norm_eps=1e-5, | |
| rope_scaling={"mrope_section": [24, 20, 20]}, | |
| rope_theta=100_000_000.0, | |
| timestep_scale=0.001, | |
| unified_3d_mrope_reset_spatial_ids=True, | |
| unified_3d_mrope_temporal_modality_margin=15000, | |
| vocab_size=131072, | |
| backbone_type="cosmos3_edge_nemotron_dense", | |
| temporal_compression_factor=4, | |
| ) | |
| transformer = transformer.to(dtype=dtype) | |
| transformer.time_embedder.to(dtype=torch.float32) | |
| return transformer.to_empty(device="cpu") | |
| def _validate_edge_action_pipeline_support() -> None: | |
| import inspect | |
| from diffusers.pipelines.cosmos.pipeline_cosmos3_omni import Cosmos3OmniPipeline | |
| pipeline_params = inspect.signature(Cosmos3OmniPipeline.__call__).parameters | |
| required_params = {"action", "action_latents"} | |
| missing_params = sorted(required_params - set(pipeline_params)) | |
| if missing_params: | |
| raise RuntimeError( | |
| "The checkpoint has action generation weights and --save-pipeline was requested, but this diffusers " | |
| f"build's Cosmos3OmniPipeline does not accept {missing_params}. Use a build that also includes the " | |
| "Cosmos3 action pipeline support from commit bcc20e452." | |
| ) | |
| def _load_edge_dcp_weights(transformer, checkpoint_path: pathlib.Path, use_ema: bool) -> None: | |
| import torch.distributed.checkpoint as dcp | |
| from torch.distributed.checkpoint.filesystem import FileSystemReader | |
| prefix = "net_ema." if use_ema else "net." | |
| state_dict_metadata = FileSystemReader(str(checkpoint_path)).read_metadata().state_dict_metadata | |
| target_state_dict = transformer.state_dict() | |
| dcp_state_dict: dict[str, torch.Tensor] = {} | |
| for key in state_dict_metadata: | |
| if not key.startswith(prefix): | |
| continue | |
| target_key = _remap_edge_dcp_key(key.removeprefix(prefix)) | |
| if target_key not in target_state_dict: | |
| raise RuntimeError(f"Cosmos3 Edge DCP key {key!r} maps to unknown transformer key {target_key!r}.") | |
| dcp_state_dict[key] = target_state_dict[target_key] | |
| mapped_keys = {_remap_edge_dcp_key(key.removeprefix(prefix)) for key in dcp_state_dict} | |
| missing_keys = sorted(set(target_state_dict) - mapped_keys) | |
| if missing_keys: | |
| raise RuntimeError(f"Cosmos3 Edge DCP is missing transformer weights: {missing_keys}.") | |
| print(f"Loading {'EMA' if use_ema else 'regular'} Cosmos3 Edge weights from {checkpoint_path} …") | |
| dcp.load(state_dict=dcp_state_dict, storage_reader=FileSystemReader(str(checkpoint_path)), no_dist=True) | |
| del dcp_state_dict, target_state_dict | |
| def _resolve_edge_reasoner_path(args) -> pathlib.Path: | |
| if args.reasoner_path is not None: | |
| reasoner_path = pathlib.Path(args.reasoner_path).expanduser().absolute() | |
| else: | |
| from huggingface_hub import snapshot_download | |
| print( | |
| "Downloading the pinned Cosmos3 Edge reasoner snapshot " | |
| f"({args.reasoner_repo_id}@{args.reasoner_revision}) …" | |
| ) | |
| reasoner_path = pathlib.Path( | |
| snapshot_download( | |
| repo_id=args.reasoner_repo_id, | |
| revision=args.reasoner_revision, | |
| allow_patterns=list(COSMOS3_EDGE_REASONER_FILES), | |
| ) | |
| ) | |
| if not reasoner_path.is_dir(): | |
| raise FileNotFoundError(f"Cosmos3 Edge reasoner directory not found: {reasoner_path}") | |
| missing_files = [filename for filename in COSMOS3_EDGE_REASONER_FILES if not (reasoner_path / filename).is_file()] | |
| if missing_files: | |
| raise FileNotFoundError( | |
| f"Cosmos3 Edge reasoner at {reasoner_path} is missing required files: {missing_files}" | |
| ) | |
| return reasoner_path | |
| def _load_json(path: pathlib.Path) -> dict: | |
| with open(path, encoding="utf-8") as f: | |
| return json.load(f) | |
| def _save_json(payload: dict, path: pathlib.Path) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(path, "w", encoding="utf-8") as f: | |
| json.dump(payload, f, indent=2, sort_keys=True) | |
| f.write("\n") | |
| def _edge_reasoner_language_to_generator_key_map() -> dict[str, str]: | |
| """Map source Nemotron reasoner keys to their shared Diffusers tensor keys.""" | |
| mapping = { | |
| "lm_head.weight": "lm_head.weight", | |
| "model.language_model.embeddings.weight": "embed_tokens.weight", | |
| "model.language_model.norm_f.weight": "norm.weight", | |
| } | |
| attention_key_map = { | |
| "q_proj": "to_q", | |
| "k_proj": "to_k", | |
| "v_proj": "to_v", | |
| "o_proj": "to_out", | |
| } | |
| for layer_idx in range(28): | |
| attention_layer_idx = 2 * layer_idx | |
| mlp_layer_idx = attention_layer_idx + 1 | |
| reasoner_attention_prefix = f"model.language_model.layers.{attention_layer_idx}" | |
| generator_attention_prefix = f"layers.{layer_idx}" | |
| mapping[f"{reasoner_attention_prefix}.norm.weight"] = f"{generator_attention_prefix}.input_layernorm.weight" | |
| for source_name, generator_name in attention_key_map.items(): | |
| mapping[f"{reasoner_attention_prefix}.mixer.{source_name}.weight"] = ( | |
| f"{generator_attention_prefix}.self_attn.{generator_name}.weight" | |
| ) | |
| reasoner_mlp_prefix = f"model.language_model.layers.{mlp_layer_idx}" | |
| generator_mlp_prefix = f"layers.{layer_idx}" | |
| mapping[f"{reasoner_mlp_prefix}.norm.weight"] = f"{generator_mlp_prefix}.post_attention_layernorm.weight" | |
| for source_name in ("up_proj", "down_proj"): | |
| mapping[f"{reasoner_mlp_prefix}.mixer.{source_name}.weight"] = ( | |
| f"{generator_mlp_prefix}.mlp.{source_name}.weight" | |
| ) | |
| return mapping | |
| def _native_edge_text_config(source_config: dict) -> dict: | |
| """Strip inactive hybrid fields and express the dense reasoner layout explicitly.""" | |
| layers_block_type = source_config.get("layers_block_type") | |
| if layers_block_type is None: | |
| legacy_pattern = source_config.get("hybrid_override_pattern") | |
| if legacy_pattern is None: | |
| num_hidden_layers = source_config.get("num_hidden_layers", 56) | |
| if num_hidden_layers % 2: | |
| raise ValueError("Cosmos3 Edge requires an even number of alternating attention/MLP layers.") | |
| layers_block_type = ["full_attention", "mlp"] * (num_hidden_layers // 2) | |
| else: | |
| layer_type_map = {"*": "full_attention", "-": "mlp"} | |
| try: | |
| layers_block_type = [layer_type_map[layer] for layer in legacy_pattern] | |
| except KeyError as exc: | |
| raise ValueError(f"Unsupported Cosmos3 Edge legacy layer type: {exc.args[0]!r}.") from exc | |
| else: | |
| layers_block_type = list(layers_block_type) | |
| if set(layers_block_type) - {"full_attention", "mlp"}: | |
| raise ValueError(f"Cosmos3 Edge only supports dense attention/MLP layers, got {layers_block_type!r}.") | |
| native_config = {"model_type": "cosmos3_edge_text", "layers_block_type": layers_block_type} | |
| for key in ( | |
| "attention_bias", | |
| "attention_dropout", | |
| "bos_token_id", | |
| "dtype", | |
| "eos_token_id", | |
| "head_dim", | |
| "hidden_size", | |
| "initializer_range", | |
| "intermediate_size", | |
| "layer_norm_epsilon", | |
| "max_position_embeddings", | |
| "mlp_bias", | |
| "mlp_hidden_act", | |
| "num_attention_heads", | |
| "num_key_value_heads", | |
| "num_logits_to_keep", | |
| "pad_token_id", | |
| "rope_theta", | |
| "use_cache", | |
| "vocab_size", | |
| ): | |
| if key in source_config: | |
| native_config[key] = source_config[key] | |
| native_config["num_hidden_layers"] = len(layers_block_type) | |
| native_config["rope_parameters"] = { | |
| "rope_type": "default", | |
| "rope_theta": native_config.get("rope_theta", 100_000_000.0), | |
| "mrope_section": source_config.get("mrope_section", [24, 20, 20]), | |
| } | |
| return native_config | |
| def _native_edge_projector_config(source_config: dict) -> dict: | |
| merger_intermediate_size = source_config.get("merger_intermediate_size") | |
| if merger_intermediate_size is None: | |
| merger_intermediate_size = source_config["merger_intermedia"] | |
| native_config = { | |
| "model_type": "cosmos3_edge_projector", | |
| "input_hidden_size": source_config["input_hidden_size"], | |
| "merger_intermediate_size": merger_intermediate_size, | |
| "out_hidden_size": source_config["out_hidden_size"], | |
| "spatial_merge_size": source_config["spatial_merge_size"], | |
| "use_postshuffle_norm": source_config["use_postshuffle_norm"], | |
| } | |
| return native_config | |
| def _native_edge_vision_config(source_config: dict) -> dict: | |
| native_config = {"model_type": "cosmos3_edge_vision"} | |
| for key in ( | |
| "attention_dropout", | |
| "hidden_act", | |
| "hidden_size", | |
| "intermediate_size", | |
| "layer_norm_eps", | |
| "num_attention_heads", | |
| "num_channels", | |
| "num_hidden_layers", | |
| "num_patches", | |
| "patch_size", | |
| "spatial_merge_size", | |
| ): | |
| if key in source_config: | |
| native_config[key] = source_config[key] | |
| return native_config | |
| def _native_edge_config(source_config: dict) -> dict: | |
| native_config = { | |
| key: value | |
| for key, value in source_config.items() | |
| if key not in {"architectures", "auto_map", "model_type", "projector_config", "text_config", "vision_config"} | |
| } | |
| native_config["architectures"] = ["Cosmos3EdgeForConditionalGeneration"] | |
| native_config["model_type"] = "cosmos3_edge" | |
| native_config["allow_patterns_overrides"] = ["*/*.safetensors"] | |
| native_config["text_config"] = _native_edge_text_config(source_config["text_config"]) | |
| native_config["vision_config"] = _native_edge_vision_config(source_config["vision_config"]) | |
| native_config["projector_config"] = _native_edge_projector_config(source_config["projector_config"]) | |
| return native_config | |
| def _native_edge_image_processor_config(source_config: dict) -> dict: | |
| native_config = dict(source_config) | |
| native_config.pop("auto_map", None) | |
| native_config["processor_class"] = "Cosmos3EdgeProcessor" | |
| native_config["image_processor_type"] = "Cosmos3EdgeImageProcessor" | |
| return native_config | |
| def _native_edge_video_processor_config(source_config: dict) -> dict: | |
| native_config = dict(source_config) | |
| native_config.pop("auto_map", None) | |
| native_config["processor_class"] = "Cosmos3EdgeProcessor" | |
| native_config["video_processor_type"] = "Cosmos3EdgeVideoProcessor" | |
| return native_config | |
| def _validate_edge_reasoner_mapping(source_weight_map: dict[str, str], generator_weight_map: dict[str, str]) -> dict[str, str]: | |
| language_keys = { | |
| key for key in source_weight_map if key == "lm_head.weight" or key.startswith("model.language_model.") | |
| } | |
| vision_keys = { | |
| key for key in source_weight_map if key.startswith(("model.visual.", "model.projector.")) | |
| } | |
| unexpected_keys = set(source_weight_map) - language_keys - vision_keys | |
| if unexpected_keys: | |
| raise RuntimeError(f"Unexpected Cosmos3 Edge reasoner keys: {sorted(unexpected_keys)}") | |
| mapping = _edge_reasoner_language_to_generator_key_map() | |
| if set(mapping) != language_keys: | |
| missing = sorted(language_keys - set(mapping)) | |
| extra = sorted(set(mapping) - language_keys) | |
| raise RuntimeError(f"Incomplete Edge language mapping: missing={missing}, extra={extra}") | |
| missing_generator_keys = sorted(set(mapping.values()) - set(generator_weight_map)) | |
| if missing_generator_keys: | |
| raise RuntimeError(f"Diffusers transformer is missing shared reasoner tensors: {missing_generator_keys}") | |
| if len(vision_keys) != 443: | |
| raise RuntimeError(f"Expected 443 Edge vision/projector tensors, found {len(vision_keys)}.") | |
| return mapping | |
| def _write_edge_vision_encoder( | |
| reasoner_path: pathlib.Path, | |
| output_dir: pathlib.Path, | |
| source_weight_map: dict[str, str], | |
| ) -> set[str]: | |
| try: | |
| from safetensors import safe_open | |
| from safetensors.torch import save_file | |
| except ImportError as exc: | |
| raise ImportError("Saving the Cosmos3 Edge vision encoder requires safetensors.") from exc | |
| vision_keys = { | |
| key for key in source_weight_map if key.startswith(("model.visual.", "model.projector.")) | |
| } | |
| non_vision_shards = {source_weight_map[key] for key in vision_keys} - {COSMOS3_EDGE_REASONER_VISION_SHARD} | |
| if non_vision_shards: | |
| raise RuntimeError(f"Edge visual tensors unexpectedly span shards: {sorted(non_vision_shards)}") | |
| vision_dir = output_dir / "vision_encoder" | |
| vision_dir.mkdir(parents=True, exist_ok=True) | |
| vision_path = vision_dir / "model.safetensors" | |
| print(f"Extracting {len(vision_keys)} Cosmos3 Edge vision/projector tensors to {vision_path} …") | |
| with safe_open(reasoner_path / COSMOS3_EDGE_REASONER_VISION_SHARD, framework="pt", device="cpu") as source_file: | |
| state_dict = {key: source_file.get_tensor(key).contiguous() for key in sorted(vision_keys)} | |
| save_file(state_dict, str(vision_path), metadata={"format": "pt"}) | |
| del state_dict | |
| # This is an auxiliary shard consumed through the root reasoner manifest; | |
| # it is not a standalone Transformers model. Do not leave a stale legacy | |
| # config beside it when converting into an existing output directory. | |
| (vision_dir / "config.json").unlink(missing_ok=True) | |
| return vision_keys | |
| def _write_edge_reasoner_manifest(reasoner_path: pathlib.Path, output_dir: pathlib.Path) -> None: | |
| source_index = _load_json(reasoner_path / COSMOS3_EDGE_REASONER_INDEX_FILE) | |
| source_weight_map = source_index["weight_map"] | |
| source_config = _load_json(reasoner_path / "config.json") | |
| generator_index = _load_json(output_dir / "transformer" / "diffusion_pytorch_model.safetensors.index.json") | |
| generator_weight_map = generator_index["weight_map"] | |
| language_mapping = _validate_edge_reasoner_mapping(source_weight_map, generator_weight_map) | |
| vision_keys = _write_edge_vision_encoder(reasoner_path, output_dir, source_weight_map) | |
| weight_map = { | |
| generator_key: f"transformer/{generator_weight_map[generator_key]}" | |
| for generator_key in language_mapping.values() | |
| } | |
| weight_map.update({key: "vision_encoder/model.safetensors" for key in vision_keys}) | |
| if len(weight_map) != len(source_weight_map): | |
| raise RuntimeError( | |
| f"Edge unified weight index has {len(weight_map)} entries, expected {len(source_weight_map)}." | |
| ) | |
| _save_json( | |
| {"metadata": source_index["metadata"], "weight_map": weight_map}, | |
| output_dir / COSMOS3_EDGE_REASONER_INDEX_FILE, | |
| ) | |
| _save_json(_native_edge_config(source_config), output_dir / "config.json") | |
| def _copy_edge_reasoner_metadata(reasoner_path: pathlib.Path, output_dir: pathlib.Path) -> None: | |
| print(f"Writing the shared Cosmos3 Edge reasoner into {output_dir} …") | |
| for filename in COSMOS3_EDGE_REASONER_METADATA_FILES: | |
| if filename == "config.json": | |
| continue | |
| if filename == "preprocessor_config.json": | |
| _save_json( | |
| _native_edge_image_processor_config(_load_json(reasoner_path / filename)), | |
| output_dir / filename, | |
| ) | |
| continue | |
| if filename == "video_preprocessor_config.json": | |
| _save_json( | |
| _native_edge_video_processor_config(_load_json(reasoner_path / filename)), | |
| output_dir / filename, | |
| ) | |
| continue | |
| shutil.copy2(reasoner_path / filename, output_dir / filename) | |
| for filename in ( | |
| "configuration_nemotron_siglip2_h.py", | |
| "modeling_cosmos3_edge_omni.py", | |
| "modeling_nemotron_siglip2_h.py", | |
| "processing.py", | |
| ): | |
| (output_dir / filename).unlink(missing_ok=True) | |
| _write_edge_reasoner_manifest(reasoner_path, output_dir) | |
| for filename in ("00000.safetensors", "00001.safetensors"): | |
| (output_dir / filename).unlink(missing_ok=True) | |
| def _copy_edge_conversion_script(output_dir: pathlib.Path) -> None: | |
| source = pathlib.Path(__file__).resolve() | |
| destination = output_dir / source.name | |
| if source != destination: | |
| shutil.copy2(source, destination) | |
| def _add_edge_reasoner_to_pipeline(args) -> None: | |
| output_dir = pathlib.Path(args.output).expanduser().absolute() | |
| expected_paths = ("model_index.json", "scheduler", "text_tokenizer", "transformer", "vae") | |
| missing_paths = [str(output_dir / path) for path in expected_paths if not (output_dir / path).exists()] | |
| if missing_paths: | |
| raise FileNotFoundError( | |
| "Expected an existing Cosmos3 Edge Diffusers pipeline before adding its reasoner; " | |
| f"missing paths: {missing_paths}" | |
| ) | |
| reasoner_path = _resolve_edge_reasoner_path(args) | |
| _copy_edge_reasoner_metadata(reasoner_path, output_dir) | |
| _copy_edge_conversion_script(output_dir) | |
| print("Done.") | |
| def _convert_edge_dcp(args, checkpoint_path: pathlib.Path, dtype: torch.dtype) -> None: | |
| from transformers import PreTrainedTokenizerFast | |
| from diffusers import AutoencoderKLWan, UniPCMultistepScheduler | |
| from diffusers.pipelines.cosmos.pipeline_cosmos3_omni import Cosmos3OmniPipeline | |
| if args.include_sound_tokenizer or args.sound_tokenizer_path is not None: | |
| raise ValueError("The supplied Cosmos3 Edge checkpoint is video-only and cannot include a sound tokenizer.") | |
| if args.include_reasoner and not args.save_pipeline: | |
| raise ValueError( | |
| "A Cosmos3 Edge reasoner can only be included with --save-pipeline because its root Transformers " | |
| "config.json would conflict with a transformer-only Diffusers save. Use --no-include-reasoner instead." | |
| ) | |
| action_config = _detect_edge_action_config(checkpoint_path, args.use_ema) | |
| if action_config is not None: | |
| print( | |
| "Detected Cosmos3 Edge action generation weights " | |
| f"(action_dim={action_config['action_dim']}, " | |
| f"num_embodiment_domains={action_config['num_embodiment_domains']})." | |
| ) | |
| transformer = _build_edge_transformer(dtype, action_config) | |
| if action_config is not None and args.save_pipeline: | |
| _validate_edge_action_pipeline_support() | |
| _load_edge_dcp_weights(transformer, checkpoint_path, args.use_ema) | |
| reasoner_path = _resolve_edge_reasoner_path(args) if args.include_reasoner else None | |
| output_dir = pathlib.Path(args.output).expanduser().absolute() | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| if not args.save_pipeline: | |
| print(f"Saving Cosmos3 Edge transformer to {output_dir} …") | |
| transformer.save_pretrained(str(output_dir), safe_serialization=True, max_shard_size="5GB") | |
| print("Done.") | |
| return | |
| tokenizer_source = str(reasoner_path) if reasoner_path is not None else args.reasoner_repo_id | |
| tokenizer_kwargs = {} | |
| if reasoner_path is None: | |
| tokenizer_kwargs["revision"] = args.reasoner_revision | |
| text_tokenizer = PreTrainedTokenizerFast.from_pretrained(tokenizer_source, **tokenizer_kwargs) | |
| for token in ("<|vision_start|>", "<|vision_end|>"): | |
| token_id = text_tokenizer.convert_tokens_to_ids(token) | |
| if token_id is None or token_id < 0 or token_id >= transformer.config.vocab_size: | |
| raise ValueError( | |
| f"Cosmos3 Edge tokenizer token {token!r} has invalid ID {token_id!r} for " | |
| f"vocab_size={transformer.config.vocab_size}." | |
| ) | |
| diffusers_vae = AutoencoderKLWan.from_pretrained(COSMOS3_EDGE_VAE, subfolder="vae", torch_dtype=torch.bfloat16) | |
| scheduler = UniPCMultistepScheduler( | |
| num_train_timesteps=1000, | |
| solver_order=2, | |
| predict_x0=True, | |
| solver_type="bh2", | |
| lower_order_final=True, | |
| final_sigmas_type="zero", | |
| use_flow_sigmas=True, | |
| prediction_type="flow_prediction", | |
| flow_shift=5.0, | |
| ) | |
| pipeline = Cosmos3OmniPipeline( | |
| transformer=transformer, | |
| text_tokenizer=text_tokenizer, | |
| vae=diffusers_vae, | |
| scheduler=scheduler, | |
| enable_safety_checker=False, | |
| default_use_system_prompt=False, | |
| use_native_flow_schedule=True, | |
| ) | |
| print(f"Saving Cosmos3 Edge pipeline to {output_dir} …") | |
| pipeline.save_pretrained(str(output_dir), safe_serialization=True, max_shard_size="5GB") | |
| if reasoner_path is not None: | |
| _copy_edge_reasoner_metadata(reasoner_path, output_dir) | |
| _copy_edge_conversion_script(output_dir) | |
| print("Done.") | |
| def _load_sound_tokenizer_state_dict(checkpoint_path: pathlib.Path) -> dict[str, torch.Tensor]: | |
| if checkpoint_path.suffix == ".safetensors": | |
| try: | |
| from safetensors.torch import load_file | |
| except ImportError as exc: | |
| raise ImportError("Loading AVAE .safetensors checkpoints requires safetensors.") from exc | |
| checkpoint = load_file(str(checkpoint_path), device="cpu") | |
| else: | |
| checkpoint = torch.load(checkpoint_path, map_location="cpu") | |
| if not isinstance(checkpoint, dict): | |
| raise TypeError(f"AVAE checkpoint must be a dict, got {type(checkpoint)!r}.") | |
| for key in ("generator", "state_dict", "model"): | |
| value = checkpoint.get(key) | |
| if isinstance(value, dict): | |
| checkpoint = value | |
| break | |
| state_dict = { | |
| key: value.detach().cpu().contiguous() for key, value in checkpoint.items() if isinstance(value, torch.Tensor) | |
| } | |
| if not state_dict: | |
| raise RuntimeError(f"No tensor state dict found in AVAE checkpoint keys: {list(checkpoint.keys())[:16]}") | |
| return state_dict | |
| def _load_sound_tokenizer_config(config_path: pathlib.Path | None, fallback_config_path: pathlib.Path) -> dict: | |
| selected_config_path = config_path | |
| if selected_config_path is None and fallback_config_path.exists(): | |
| selected_config_path = fallback_config_path | |
| if selected_config_path is None: | |
| return dict(DEFAULT_SOUND_TOKENIZER_CONFIG) | |
| with open(selected_config_path, encoding="utf-8") as f: | |
| return json.load(f) | |
| _SOUND_TOKENIZER_PER_KEY_PREFIXES = ("module.", "generator.", "model.", "state_dict.") | |
| _SOUND_TOKENIZER_RES_UNIT_INNER_NAMES = {0: "snake1", 1: "conv1", 2: "snake2", 3: "conv2"} | |
| def _sound_tokenizer_strip_per_key_prefixes(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: | |
| out = dict(state_dict) | |
| changed = True | |
| while changed: | |
| changed = False | |
| for prefix in _SOUND_TOKENIZER_PER_KEY_PREFIXES: | |
| if any(key.startswith(prefix) for key in out): | |
| out = {(key[len(prefix) :] if key.startswith(prefix) else key): value for key, value in out.items()} | |
| changed = True | |
| break | |
| if any(key.startswith(("decoder.", "encoder.", "bottleneck.")) for key in out): | |
| break | |
| return out | |
| def _sound_tokenizer_filter_decoder(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: | |
| return {key: value for key, value in state_dict.items() if key.startswith("decoder.")} | |
| def _sound_tokenizer_infer_num_blocks(state_dict: dict[str, torch.Tensor]) -> int: | |
| block_indices: set[int] = set() | |
| for key in state_dict: | |
| match = re.match(r"decoder\.layers\.(\d+)\.layers\.\d+\.", key) | |
| if match: | |
| block_indices.add(int(match.group(1))) | |
| return len(block_indices) | |
| def _sound_tokenizer_remap_flat_layout(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: | |
| """Convert legacy AVAE `decoder.layers.*` keys to OobleckDecoder attribute keys.""" | |
| if not any(re.match(r"decoder\.layers\.\d+\.", key) for key in state_dict): | |
| return state_dict | |
| num_blocks = _sound_tokenizer_infer_num_blocks(state_dict) | |
| if num_blocks == 0: | |
| raise RuntimeError("Detected flat `decoder.layers.*` layout but no decoder blocks were found; cannot remap.") | |
| snake1_idx = num_blocks + 1 | |
| conv2_idx = num_blocks + 2 | |
| def _remap(key: str) -> str: | |
| match = re.fullmatch(r"decoder\.layers\.(\d+)\.layers\.(\d+)\.layers\.(\d+)\.(.+)", key) | |
| if match: | |
| block_n, res_n, inner_n, rest = ( | |
| int(match.group(1)), | |
| int(match.group(2)), | |
| int(match.group(3)), | |
| match.group(4), | |
| ) | |
| if res_n not in (2, 3, 4): | |
| raise RuntimeError(f"Unexpected residual position {res_n} in {key!r}.") | |
| inner_name = _SOUND_TOKENIZER_RES_UNIT_INNER_NAMES.get(inner_n) | |
| if inner_name is None: | |
| raise RuntimeError(f"Unexpected residual inner index {inner_n} in {key!r}.") | |
| return f"decoder.block.{block_n - 1}.res_unit{res_n - 1}.{inner_name}.{rest}" | |
| match = re.fullmatch(r"decoder\.layers\.(\d+)\.layers\.(\d+)\.(.+)", key) | |
| if match: | |
| block_n, sub_n, rest = int(match.group(1)), int(match.group(2)), match.group(3) | |
| block_idx = block_n - 1 | |
| if sub_n == 0: | |
| return f"decoder.block.{block_idx}.snake1.{rest}" | |
| if sub_n == 1: | |
| return f"decoder.block.{block_idx}.conv_t1.{rest}" | |
| raise RuntimeError(f"Unexpected decoder block sub-index {sub_n} in {key!r}.") | |
| match = re.fullmatch(r"decoder\.layers\.(\d+)\.(.+)", key) | |
| if match: | |
| layer_n, rest = int(match.group(1)), match.group(2) | |
| if layer_n == 0: | |
| return f"decoder.conv1.{rest}" | |
| if layer_n == snake1_idx: | |
| return f"decoder.snake1.{rest}" | |
| if layer_n == conv2_idx: | |
| return f"decoder.conv2.{rest}" | |
| raise RuntimeError( | |
| f"Unexpected decoder leaf layer index {layer_n} (expected 0, {snake1_idx}, or {conv2_idx}) in {key!r}." | |
| ) | |
| return key | |
| return {_remap(key): value for key, value in state_dict.items()} | |
| def _sound_tokenizer_reshape_snake_params(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: | |
| out: dict[str, torch.Tensor] = {} | |
| for key, value in state_dict.items(): | |
| if (key.endswith(".alpha") or key.endswith(".beta")) and value.ndim == 1: | |
| value = value.unsqueeze(0).unsqueeze(-1).contiguous() | |
| out[key] = value | |
| return out | |
| def _sound_tokenizer_reapply_weight_norm(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: | |
| """Reconstruct weight-norm parameters if the source checkpoint has folded conv weights.""" | |
| out = dict(state_dict) | |
| candidate_keys = [ | |
| key | |
| for key in state_dict | |
| if key.endswith(".weight") and any(f".{layer}." in key for layer in ("conv1", "conv2", "conv_t1")) | |
| ] | |
| for key in candidate_keys: | |
| stem = key[: -len(".weight")] | |
| weight_g_key = f"{stem}.weight_g" | |
| weight_v_key = f"{stem}.weight_v" | |
| if weight_g_key in state_dict or weight_v_key in state_dict: | |
| continue | |
| weight = state_dict[key] | |
| norm_dims = tuple(range(1, weight.ndim)) | |
| out.pop(key) | |
| out[weight_g_key] = weight.norm(p=2, dim=norm_dims, keepdim=True).contiguous() | |
| out[weight_v_key] = weight.contiguous() | |
| return out | |
| def _remap_avae_state_dict(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: | |
| """Convert a legacy AVAE state dict into the Cosmos3AVAEAudioTokenizer state dict.""" | |
| state_dict = _sound_tokenizer_strip_per_key_prefixes(state_dict) | |
| state_dict = _sound_tokenizer_filter_decoder(state_dict) | |
| if not state_dict: | |
| raise RuntimeError("Sound tokenizer state dict has no `decoder.*` keys after prefix stripping.") | |
| state_dict = _sound_tokenizer_remap_flat_layout(state_dict) | |
| state_dict = _sound_tokenizer_reshape_snake_params(state_dict) | |
| state_dict = _sound_tokenizer_reapply_weight_norm(state_dict) | |
| if any(re.match(r"decoder\.layers\.\d+", key) for key in state_dict): | |
| raise RuntimeError("Flat `decoder.layers.*` keys remain after remap; conversion is incomplete.") | |
| return state_dict | |
| def _build_sound_tokenizer( | |
| checkpoint_path: pathlib.Path, | |
| config_path: pathlib.Path | None, | |
| ) -> Cosmos3AVAEAudioTokenizer: | |
| config = _load_sound_tokenizer_config(config_path, fallback_config_path=pathlib.Path()) | |
| print(f"Loading AVAE sound tokenizer weights from {checkpoint_path} …") | |
| raw_state_dict = _load_sound_tokenizer_state_dict(checkpoint_path) | |
| state_dict = _remap_avae_state_dict(raw_state_dict) | |
| print(f" Remapped {len(raw_state_dict)} → {len(state_dict)} decoder keys.") | |
| sound_tokenizer = Cosmos3AVAEAudioTokenizer( | |
| sampling_rate=config.get("sampling_rate", DEFAULT_SOUND_TOKENIZER_CONFIG["sampling_rate"]), | |
| vocoder_input_dim=config.get("vocoder_input_dim", DEFAULT_SOUND_TOKENIZER_CONFIG["vocoder_input_dim"]), | |
| dec_dim=config.get("dec_dim", DEFAULT_SOUND_TOKENIZER_CONFIG["dec_dim"]), | |
| dec_c_mults=tuple(config.get("dec_c_mults", DEFAULT_SOUND_TOKENIZER_CONFIG["dec_c_mults"])), | |
| dec_strides=tuple(config.get("dec_strides", DEFAULT_SOUND_TOKENIZER_CONFIG["dec_strides"])), | |
| dec_out_channels=config.get("dec_out_channels", DEFAULT_SOUND_TOKENIZER_CONFIG["dec_out_channels"]), | |
| ) | |
| load_result = sound_tokenizer.load_state_dict(state_dict, strict=True) | |
| if load_result.missing_keys or load_result.unexpected_keys: | |
| raise RuntimeError( | |
| "Cosmos3 AVAE sound tokenizer load did not match strictly: " | |
| f"missing={load_result.missing_keys}, unexpected={load_result.unexpected_keys}." | |
| ) | |
| return sound_tokenizer | |
| def _skip_source_sound_tokenizer_load(omni_mot_model_cls): | |
| original_set_up_tokenizers = omni_mot_model_cls.set_up_tokenizers | |
| def set_up_tokenizers_without_sound(self): | |
| if not getattr(self.config, "sound_gen", False): | |
| return original_set_up_tokenizers(self) | |
| sound_gen = self.config.sound_gen | |
| self.config.sound_gen = False | |
| try: | |
| return original_set_up_tokenizers(self) | |
| finally: | |
| self.config.sound_gen = sound_gen | |
| omni_mot_model_cls.set_up_tokenizers = set_up_tokenizers_without_sound | |
| try: | |
| yield | |
| finally: | |
| omni_mot_model_cls.set_up_tokenizers = original_set_up_tokenizers | |
| def main(): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument( | |
| "--checkpoint-path", | |
| default="Cosmos3-Nano", | |
| help="Named checkpoint (e.g. 'Cosmos3-Nano') or path to a DCP checkpoint directory.", | |
| ) | |
| parser.add_argument("--output", required=True, help="Directory to save the converted diffusers model.") | |
| parser.add_argument( | |
| "--save-pipeline", | |
| action="store_true", | |
| help="Save the full pipeline (transformer + VAE + tokenizer + scheduler).", | |
| ) | |
| parser.add_argument( | |
| "--dtype", default="bf16", choices=["fp32", "fp16", "bf16"], help="Dtype to save the transformer in." | |
| ) | |
| parser.add_argument( | |
| "--sound-tokenizer-path", help="Optional AVAE sound tokenizer checkpoint to save under sound_tokenizer/." | |
| ) | |
| parser.add_argument( | |
| "--sound-tokenizer-config-path", help="Optional AVAE config JSON to save under sound_tokenizer/config.json." | |
| ) | |
| parser.add_argument( | |
| "--include-sound-tokenizer", | |
| action="store_true", | |
| help="Require saving sound_tokenizer/ even if the source transformer is video-only.", | |
| ) | |
| parser.add_argument( | |
| "--use-ema", | |
| action=argparse.BooleanOptionalAction, | |
| default=True, | |
| help="Use `net_ema` weights when converting a raw Cosmos3 Edge DCP checkpoint.", | |
| ) | |
| parser.add_argument( | |
| "--include-reasoner", | |
| action=argparse.BooleanOptionalAction, | |
| default=True, | |
| help=( | |
| "Add the pinned Cosmos3 Edge reasoner in shared-weight form so the pipeline output is usable by " | |
| "Transformers as well as Diffusers." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--reasoner-repo-id", | |
| default=COSMOS3_EDGE_REASONER, | |
| help="Hugging Face repository containing the Cosmos3 Edge reasoner checkpoint.", | |
| ) | |
| parser.add_argument( | |
| "--reasoner-revision", | |
| default=COSMOS3_EDGE_REASONER_REVISION, | |
| help="Pinned revision of the Cosmos3 Edge reasoner checkpoint.", | |
| ) | |
| parser.add_argument( | |
| "--reasoner-path", | |
| help="Optional local Cosmos3 Edge reasoner snapshot, used instead of downloading --reasoner-repo-id.", | |
| ) | |
| parser.add_argument( | |
| "--copy-edge-reasoner", | |
| action="store_true", | |
| help=( | |
| "Add the pinned shared-weight reasoner and this converter to an existing Cosmos3 Edge Diffusers " | |
| "pipeline at --output." | |
| ), | |
| ) | |
| args = parser.parse_args() | |
| if args.copy_edge_reasoner: | |
| _add_edge_reasoner_to_pipeline(args) | |
| return | |
| dtype = {"fp32": torch.float32, "fp16": torch.float16, "bf16": torch.bfloat16}[args.dtype] | |
| raw_checkpoint_path = pathlib.Path(args.checkpoint_path).expanduser() | |
| edge_dcp_model_dir = _find_edge_dcp_model_dir(raw_checkpoint_path) if raw_checkpoint_path.exists() else None | |
| if edge_dcp_model_dir is not None: | |
| _convert_edge_dcp(args, edge_dcp_model_dir, dtype) | |
| return | |
| from cosmos3.common.init import init_script | |
| init_script() | |
| from accelerate import init_empty_weights | |
| from cosmos3.args import _CHECKPOINTS | |
| from cosmos3.model import Cosmos3OmniModel | |
| from projects.cosmos3.vfm.models.omni_mot_model import OmniMoTModel | |
| from transformers import AutoTokenizer | |
| from diffusers import AutoencoderKLWan, UniPCMultistepScheduler | |
| from diffusers.models.transformers.transformer_cosmos3 import Cosmos3OmniTransformer | |
| from diffusers.pipelines.cosmos.pipeline_cosmos3_omni import Cosmos3OmniPipeline | |
| sound_tokenizer_path = ( | |
| pathlib.Path(args.sound_tokenizer_path).expanduser().absolute() if args.sound_tokenizer_path else None | |
| ) | |
| sound_tokenizer_config_path = ( | |
| pathlib.Path(args.sound_tokenizer_config_path).expanduser().absolute() | |
| if args.sound_tokenizer_config_path | |
| else None | |
| ) | |
| if args.include_sound_tokenizer and sound_tokenizer_path is None: | |
| raise ValueError("Sound tokenizer output was requested, but --sound-tokenizer-path was not provided.") | |
| if sound_tokenizer_path is not None and not sound_tokenizer_path.exists(): | |
| raise FileNotFoundError(f"Sound tokenizer checkpoint not found: {sound_tokenizer_path}") | |
| if sound_tokenizer_config_path is not None and not sound_tokenizer_config_path.exists(): | |
| raise FileNotFoundError(f"Sound tokenizer config not found: {sound_tokenizer_config_path}") | |
| checkpoint_name = args.checkpoint_path | |
| if checkpoint_name in _CHECKPOINTS: | |
| checkpoint_path = pathlib.Path(_CHECKPOINTS[checkpoint_name].download()) | |
| else: | |
| checkpoint_path = pathlib.Path(checkpoint_name).expanduser().absolute() | |
| print(f"Resolved checkpoint path: {checkpoint_path}") | |
| print("Instantiating model and loading weights from DCP checkpoint …") | |
| print("Skipping source AVAE tokenizer instantiation during converter-only model load …") | |
| with _skip_source_sound_tokenizer_load(OmniMoTModel): | |
| _tmp = Cosmos3OmniModel.from_pretrained_dcp(checkpoint_path).model | |
| # Extract network components and architecture config from DCP model | |
| language_model = _tmp.net.language_model | |
| vae2llm = _tmp.net.vae2llm | |
| llm2vae = _tmp.net.llm2vae | |
| time_embedder = _tmp.net.time_embedder | |
| lm_cfg = _tmp.net.language_model.config | |
| net_cfg = _tmp.net.config | |
| model_cfg = _tmp.config | |
| patch_latent_dim = _tmp.net.patch_latent_dim | |
| hidden_size = _tmp.net.hidden_size | |
| num_attention_heads = _tmp.net.num_heads | |
| num_key_value_heads = _tmp.net.num_kv_heads | |
| head_dim = _tmp.net.head_dim | |
| num_hidden_layers = _tmp.net.num_hidden_layers | |
| latent_patch_size = _tmp.net.latent_patch_size | |
| latent_channel = _tmp.net.latent_channel | |
| timestep_scale = _tmp.net.timestep_scale | |
| base_fps = int(net_cfg.base_fps) | |
| enable_fps_modulation = net_cfg.enable_fps_modulation | |
| unified_3d_mrope_reset_spatial_ids = _tmp.config.diffusion_expert_config.unified_3d_mrope_reset_spatial_ids | |
| unified_3d_mrope_temporal_modality_margin = ( | |
| _tmp.config.diffusion_expert_config.unified_3d_mrope_temporal_modality_margin | |
| ) | |
| sound2llm = getattr(_tmp.net, "sound2llm", None) | |
| llm2sound = getattr(_tmp.net, "llm2sound", None) | |
| sound_modality_embed = getattr(_tmp.net, "sound_modality_embed", None) | |
| has_sound_projection_weights = any(module is not None for module in (sound2llm, llm2sound, sound_modality_embed)) | |
| sound_gen = bool( | |
| _get_config_value(net_cfg, model_cfg, name="sound_gen", default=False) or has_sound_projection_weights | |
| ) | |
| sound_dim = _get_config_value(net_cfg, model_cfg, name="sound_dim", default=None) | |
| if sound_dim is None and sound2llm is not None: | |
| sound_dim = sound2llm.in_features | |
| sound_latent_fps = _get_config_value(net_cfg, model_cfg, name="sound_latent_fps", default=25.0) | |
| if sound_gen: | |
| missing_sound_modules = [ | |
| name | |
| for name, module in ( | |
| ("sound2llm", sound2llm), | |
| ("llm2sound", llm2sound), | |
| ("sound_modality_embed", sound_modality_embed), | |
| ) | |
| if module is None | |
| ] | |
| if missing_sound_modules: | |
| raise RuntimeError( | |
| "Source checkpoint is configured for sound generation but is missing " | |
| f"sound projection weights: {missing_sound_modules}." | |
| ) | |
| if sound_dim is None: | |
| raise RuntimeError("Source checkpoint is configured for sound generation but sound_dim is missing.") | |
| del _tmp | |
| torch.cuda.empty_cache() | |
| # Init diffusers Cosmos3OmniTransformer with full architecture config from DCP | |
| with init_empty_weights(): | |
| transformer = Cosmos3OmniTransformer( | |
| attention_bias=lm_cfg.attention_bias, | |
| attention_dropout=lm_cfg.attention_dropout, | |
| base_fps=base_fps, | |
| enable_fps_modulation=enable_fps_modulation, | |
| head_dim=head_dim, | |
| hidden_size=hidden_size, | |
| intermediate_size=lm_cfg.intermediate_size, | |
| latent_channel=latent_channel, | |
| latent_patch_size=latent_patch_size, | |
| num_attention_heads=num_attention_heads, | |
| num_hidden_layers=num_hidden_layers, | |
| num_key_value_heads=num_key_value_heads, | |
| patch_latent_dim=patch_latent_dim, | |
| rms_norm_eps=lm_cfg.rms_norm_eps, | |
| rope_scaling=lm_cfg.rope_scaling, | |
| rope_theta=lm_cfg.rope_theta, | |
| sound_dim=sound_dim, | |
| sound_gen=sound_gen, | |
| sound_latent_fps=sound_latent_fps, | |
| timestep_scale=timestep_scale, | |
| unified_3d_mrope_reset_spatial_ids=unified_3d_mrope_reset_spatial_ids, | |
| unified_3d_mrope_temporal_modality_margin=unified_3d_mrope_temporal_modality_margin, | |
| vocab_size=lm_cfg.vocab_size, | |
| backbone_type="cosmos3_edge_nemotron_dense", | |
| ) | |
| # The source language_model nests its transformer stack under a `model.` attribute | |
| # (HF Qwen-style). Diffusers Cosmos3OmniTransformer holds those layers flat, so | |
| # strip the leading `model.` prefix from the language-model state-dict keys. | |
| state_dict = { | |
| (k[len("model.") :] if k.startswith("model.") else k): v for k, v in language_model.state_dict().items() | |
| } | |
| # Remap PackedAttentionMoT attribute names from the source (Qwen-style q_proj/k_proj/... | |
| # plus cosmos-specific *_moe_gen) to the diffusers AttentionModuleMixin canonical names. | |
| # Order matters: the *_moe_gen substrings must be substituted before the plain ones. | |
| _ATTN_KEY_REMAP = [ | |
| (".q_proj_moe_gen.", ".add_q_proj."), | |
| (".k_proj_moe_gen.", ".add_k_proj."), | |
| (".v_proj_moe_gen.", ".add_v_proj."), | |
| (".o_proj_moe_gen.", ".to_add_out."), | |
| (".q_norm_moe_gen.", ".norm_added_q."), | |
| (".k_norm_moe_gen.", ".norm_added_k."), | |
| (".q_proj.", ".to_q."), | |
| (".k_proj.", ".to_k."), | |
| (".v_proj.", ".to_v."), | |
| (".o_proj.", ".to_out."), | |
| (".q_norm.", ".norm_q."), | |
| (".k_norm.", ".norm_k."), | |
| ] | |
| remapped_state_dict: dict[str, torch.Tensor] = {} | |
| for k, v in state_dict.items(): | |
| for old, new in _ATTN_KEY_REMAP: | |
| if old in k: | |
| k = k.replace(old, new) | |
| break | |
| remapped_state_dict[k] = v | |
| state_dict = remapped_state_dict | |
| for k, v in vae2llm.state_dict().items(): | |
| state_dict[f"proj_in.{k}"] = v | |
| for k, v in llm2vae.state_dict().items(): | |
| state_dict[f"proj_out.{k}"] = v | |
| _TIME_EMBEDDER_REMAP = { | |
| "mlp.0.weight": "linear_1.weight", | |
| "mlp.0.bias": "linear_1.bias", | |
| "mlp.2.weight": "linear_2.weight", | |
| "mlp.2.bias": "linear_2.bias", | |
| } | |
| for k, v in time_embedder.state_dict().items(): | |
| state_dict[f"time_embedder.{_TIME_EMBEDDER_REMAP[k]}"] = v | |
| if sound_gen: | |
| for k, v in sound2llm.state_dict().items(): | |
| state_dict[f"audio_proj_in.{k}"] = v | |
| for k, v in llm2sound.state_dict().items(): | |
| state_dict[f"audio_proj_out.{k}"] = v | |
| state_dict["audio_modality_embed"] = sound_modality_embed | |
| transformer.load_state_dict(state_dict, strict=True, assign=True) | |
| del ( | |
| language_model, | |
| vae2llm, | |
| llm2vae, | |
| time_embedder, | |
| sound2llm, | |
| llm2sound, | |
| sound_modality_embed, | |
| state_dict, | |
| ) | |
| torch.cuda.empty_cache() | |
| transformer = transformer.to(dtype=dtype) | |
| output_dir = pathlib.Path(args.output) | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| include_sound_tokenizer = ( | |
| args.include_sound_tokenizer or sound_tokenizer_path is not None or (sound_gen and args.save_pipeline) | |
| ) | |
| if include_sound_tokenizer and sound_tokenizer_path is None: | |
| raise ValueError( | |
| "The source checkpoint is configured for sound generation, so --sound-tokenizer-path " | |
| "is required when saving a full pipeline." | |
| ) | |
| if args.save_pipeline: | |
| text_tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-VL-8B-Instruct") | |
| diffusers_vae = AutoencoderKLWan.from_pretrained( | |
| "Wan-AI/Wan2.2-TI2V-5B-Diffusers", subfolder="vae", torch_dtype=torch.bfloat16 | |
| ) | |
| sound_tokenizer = None | |
| if include_sound_tokenizer: | |
| assert sound_tokenizer_path is not None | |
| sound_tokenizer = _build_sound_tokenizer(sound_tokenizer_path, sound_tokenizer_config_path) | |
| # Karras schedule approximating FlowUniPCMultistepScheduler with shift=5, 35 steps. | |
| # Measured from that schedule: first flow-sigma=0.9998, last flow-sigma=0.1281. | |
| # EDM sigma = flow_sigma / (1 - flow_sigma), so: | |
| # sigma_max = 0.9998 / 0.0002 = 4999 (but capped at 200 to avoid duplicate | |
| # integer timesteps from Karras clustering near the top) | |
| # sigma_min = 0.1281 / (1 - 0.1281) = 0.1281 / 0.8719 ≈ 0.147 | |
| scheduler = UniPCMultistepScheduler( | |
| use_karras_sigmas=True, | |
| use_flow_sigmas=True, | |
| prediction_type="flow_prediction", | |
| sigma_max=200.0, | |
| sigma_min=0.147, | |
| ) | |
| pipeline = Cosmos3OmniPipeline( | |
| transformer=transformer, | |
| text_tokenizer=text_tokenizer, | |
| vae=diffusers_vae, | |
| scheduler=scheduler, | |
| sound_tokenizer=sound_tokenizer, | |
| ) | |
| print(f"Saving full pipeline to {output_dir} …") | |
| pipeline.save_pretrained(str(output_dir), safe_serialization=True, max_shard_size="5GB") | |
| else: | |
| print(f"Saving transformer to {output_dir} …") | |
| transformer.save_pretrained(str(output_dir), safe_serialization=True, max_shard_size="5GB") | |
| if include_sound_tokenizer: | |
| print("Skipping sound_tokenizer/ save because --save-pipeline was not set.") | |
| print("Done.") | |
| if __name__ == "__main__": | |
| main() | |