Files
Raghav KandClaude Opus 5.5 315b25d26d [new-model] Add Cosmos Predict2.5 distilled T2W and DFD V2W inference (#1767)
Adds the Cosmos Predict 2.5 distilled text-to-world sampler (sCM, per-frame timesteps) and the DFD video-to-world pipeline, with their schedulers, registry entries, presets and examples.

Maintainer review fixes (Swarm orders #718 and #748): the DiT receives one shared fps value (its RoPE table is batch-independent, so per-sample vectors crashed batch > 1; differing values now raise), non-finite rollouts raise FloatingPointError instead of being zeroed by nan_to_num, the DFD detector matches "dfd" only in the last path component, the distilled example defaults to 24 fps, and the distilled rollout keeps its state in FP64 through the scheduler step as the official driver does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 04:42:44 -07:00

262 lines
9.4 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Package the Cosmos Predict2.5 DFD I2V student for FastVideo.
The public checkpoint is a PyTorch distributed-checkpoint directory produced
from the upstream student network. This converter materializes that state dict,
normalizes its network prefix to the existing Cosmos25 mapping contract, drops
training-only tensors, and combines it with the reusable Cosmos25 components
from an existing FastVideo-loadable package.
"""
from __future__ import annotations
import argparse
import json
import shutil
import tempfile
from collections.abc import Mapping
from pathlib import Path
from typing import Any
import torch
from safetensors import safe_open
from safetensors.torch import save_file
REQUIRED_BASE_PATHS = (
"model_index.json",
"transformer/config.json",
"vae",
"text_encoder",
"tokenizer",
)
TRANSFORMER_FILENAME = "diffusion_pytorch_model.safetensors"
SKIP_KEY_FRAGMENTS = (
".accum_",
".logvar_linear.",
".pos_embedder.",
"._extra_state",
)
class ConversionError(RuntimeError):
pass
def _read_json(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
return json.load(handle)
def _write_json(path: Path, payload: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2)
handle.write("\n")
def _flatten_tensors(value: object, prefix: str = "") -> dict[str, torch.Tensor]:
if not isinstance(value, Mapping):
return {}
tensors: dict[str, torch.Tensor] = {}
for key, item in value.items():
if not isinstance(key, str):
continue
name = f"{prefix}.{key}" if prefix else key
if torch.is_tensor(item):
tensors[name] = item
elif isinstance(item, Mapping):
tensors.update(_flatten_tensors(item, name))
return tensors
def load_dfd_dcp_state_dict(checkpoint_dir: Path) -> dict[str, torch.Tensor]:
checkpoint_dir = checkpoint_dir.expanduser().resolve()
if not checkpoint_dir.is_dir():
raise FileNotFoundError(f"DFD distributed checkpoint directory not found: {checkpoint_dir}")
if not (checkpoint_dir / ".metadata").is_file():
raise ConversionError(f"DFD checkpoint has no .metadata file: {checkpoint_dir}")
try:
from torch.distributed.checkpoint.format_utils import dcp_to_torch_save
except ImportError as exc:
raise ConversionError("This conversion requires torch.distributed.checkpoint.format_utils") from exc
with tempfile.TemporaryDirectory(prefix="cosmos25-dfd-") as temp_dir:
materialized_path = Path(temp_dir) / "student.pt"
dcp_to_torch_save(checkpoint_dir, materialized_path)
checkpoint = torch.load(materialized_path, map_location="cpu", weights_only=True)
tensors = _flatten_tensors(checkpoint)
if not tensors:
raise ConversionError(f"DFD checkpoint contains no tensors: {checkpoint_dir}")
return tensors
def normalize_dfd_student_state_dict(state_dict: Mapping[str, Any]) -> dict[str, torch.Tensor]:
"""Normalize DCP keys to the native `net.*` Cosmos25 conversion surface."""
student: dict[str, torch.Tensor] = {}
for source_key, tensor in state_dict.items():
if not isinstance(source_key, str) or not torch.is_tensor(tensor):
continue
key = source_key.replace("._checkpoint_wrapped_module", "")
wrapper_removed = True
while wrapper_removed:
wrapper_removed = False
for wrapper_prefix in ("model.", "module."):
if key.startswith(wrapper_prefix):
key = key[len(wrapper_prefix) :]
wrapper_removed = True
if key.startswith("net.transformer."):
key = "net." + key[len("net.transformer.") :]
elif key.startswith("transformer."):
key = "net." + key[len("transformer.") :]
elif not key.startswith("net."):
key = "net." + key
if any(fragment in key for fragment in SKIP_KEY_FRAGMENTS):
continue
student[key] = tensor.detach().to(device="cpu", dtype=torch.bfloat16).contiguous()
if not student:
raise ConversionError("No DFD student tensors remained after key normalization")
return student
def _validate_base_model(base_model: Path) -> None:
missing = [relative for relative in REQUIRED_BASE_PATHS if not (base_model / relative).exists()]
if missing:
raise ConversionError(f"Base Cosmos25 package is missing required paths: {missing}")
def _prepare_output(dst: Path, overwrite: bool) -> None:
if dst.exists() and any(dst.iterdir()):
if not overwrite:
raise FileExistsError(f"Output directory is not empty: {dst}. Pass --overwrite to replace it.")
shutil.rmtree(dst)
dst.mkdir(parents=True, exist_ok=True)
def _remove_base_transformer_weights(transformer_dir: Path) -> None:
for pattern in ("*.safetensors", "*.safetensors.index.json", "*.bin", "*.pt"):
for path in transformer_dir.glob(pattern):
path.unlink()
def _write_dfd_metadata(dst: Path) -> None:
model_index_path = dst / "model_index.json"
model_index = _read_json(model_index_path)
model_index["is_distilled"] = True
model_index["distillation_method"] = "dfd"
model_index["scheduler"] = ["diffusers", "Cosmos25DFDScheduler"]
_write_json(model_index_path, model_index)
transformer_config_path = dst / "transformer" / "config.json"
transformer_config = _read_json(transformer_config_path)
transformer_config["rope_enable_fps_modulation"] = True
transformer_config["use_crossattn_projection"] = True
_write_json(transformer_config_path, transformer_config)
scheduler_dir = dst / "scheduler"
if scheduler_dir.exists():
shutil.rmtree(scheduler_dir)
_write_json(
scheduler_dir / "scheduler_config.json",
{
"_class_name": "Cosmos25DFDScheduler",
"_diffusers_version": "0.37.0.dev0",
"num_train_timesteps": 1000,
"sigma_data": 1.0,
},
)
def _verify_output(dst: Path, expected_keys: set[str]) -> None:
model_index = _read_json(dst / "model_index.json")
if model_index.get("scheduler") != ["diffusers", "Cosmos25DFDScheduler"]:
raise ConversionError("model_index.json does not select Cosmos25DFDScheduler")
if model_index.get("distillation_method") != "dfd":
raise ConversionError("model_index.json does not identify the DFD checkpoint")
transformer_config = _read_json(dst / "transformer/config.json")
if transformer_config.get("rope_enable_fps_modulation") is not True:
raise ConversionError("DFD transformer config must enable FPS-modulated RoPE")
if transformer_config.get("use_crossattn_projection") is not True:
raise ConversionError("DFD transformer config must enable the Reason1 projection")
scheduler_config = _read_json(dst / "scheduler/scheduler_config.json")
if scheduler_config.get("_class_name") != "Cosmos25DFDScheduler":
raise ConversionError("scheduler_config.json has the wrong _class_name")
weights_path = dst / "transformer" / TRANSFORMER_FILENAME
with safe_open(str(weights_path), framework="pt", device="cpu") as handle:
actual_keys = set(handle.keys())
if actual_keys != expected_keys:
missing = sorted(expected_keys - actual_keys)
unexpected = sorted(actual_keys - expected_keys)
raise ConversionError(
f"Converted transformer key mismatch: missing={missing[:10]}, unexpected={unexpected[:10]}"
)
def convert_checkpoint(
src_dcp: Path,
base_model: Path,
dst: Path,
*,
overwrite: bool = False,
) -> dict[str, int]:
src_dcp = src_dcp.expanduser().resolve()
base_model = base_model.expanduser().resolve()
dst = dst.expanduser().resolve()
_validate_base_model(base_model)
_prepare_output(dst, overwrite)
raw_state = load_dfd_dcp_state_dict(src_dcp)
student = normalize_dfd_student_state_dict(raw_state)
shutil.copytree(base_model, dst, dirs_exist_ok=True, symlinks=False)
transformer_dir = dst / "transformer"
_remove_base_transformer_weights(transformer_dir)
save_file(
student,
str(transformer_dir / TRANSFORMER_FILENAME),
metadata={"format": "pt", "model_type": "cosmos25_dfd_student"},
)
_write_dfd_metadata(dst)
_verify_output(dst, set(student))
return {
"student_tensors": len(student),
"student_parameters": sum(tensor.numel() for tensor in student.values()),
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--src-dcp", required=True, type=Path)
parser.add_argument(
"--base-model",
required=True,
type=Path,
help="FastVideo-loadable Cosmos25 package supplying configs, Reason1, tokenizer, and VAE",
)
parser.add_argument("--dst", required=True, type=Path)
parser.add_argument("--overwrite", action="store_true")
args = parser.parse_args()
report = convert_checkpoint(
args.src_dcp,
args.base_model,
args.dst,
overwrite=args.overwrite,
)
print(f"Converted {report['student_tensors']} student tensors ({report['student_parameters']:,} parameters)")
print(f"Output: {args.dst.expanduser().resolve()}")
if __name__ == "__main__":
main()