| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from __future__ import annotations |
|
|
| import json |
| import os |
| from typing import Any |
|
|
| from huggingface_hub import snapshot_download |
| from sglang.multimodal_gen.runtime.utils.model_overlay import ( |
| _copytree_link_or_copy, |
| _ensure_dir, |
| _link_or_copy_file, |
| ) |
|
|
| |
| |
| |
| GEMMA2_REPO_ID = "google/gemma-2-2b-it" |
| GEMMA2_REVISION = "299a8560bedf22ed1c72a8a11e7dce4a7f9f51f8" |
|
|
| |
| _DMD_PREFIXES = ("model.", "", "module.", "model.module.", "_orig_mod.", "model._orig_mod.") |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| _EVICT_DMD_CKPT_ENV = "SANAWM_OVERLAY_EVICT_DMD_CKPT" |
|
|
|
|
| def _write_json(path: str, payload: dict[str, Any]) -> None: |
| _ensure_dir(os.path.dirname(path)) |
| with open(path, "w", encoding="utf-8") as f: |
| json.dump(payload, f, indent=2, sort_keys=True) |
| f.write("\n") |
|
|
|
|
| def _transformer_config() -> dict[str, Any]: |
| |
| |
| |
| |
| return { |
| "_class_name": "SanaWMTransformer3DModel", |
| "_diffusers_version": "0.37.0.dev0", |
| "patch_size": 1, |
| "patch_size_t": 1, |
| "in_channels": 128, |
| "out_channels": 128, |
| "num_layers": 20, |
| "num_attention_heads": 20, |
| "attention_head_dim": 112, |
| "linear_head_dim": 112, |
| "num_cross_attention_heads": 20, |
| "cross_attention_head_dim": 112, |
| "cross_attention_dim": 2240, |
| "cross_norm": True, |
| "caption_channels": 2304, |
| "model_max_length": 300, |
| "mlp_ratio": 3.0, |
| "qk_norm": True, |
| "softmax_every_n": 4, |
| "conv_kernel_size": 4, |
| "k_conv_only": True, |
| "cam_attn_compress": 1, |
| "init_cam_from_base": True, |
| "use_chunk_plucker_post_attn": True, |
| "chunk_plucker_channels": 48, |
| "chunk_plucker_post_attn_blocks": 20, |
| "chunk_split_strategy": "first_chunk_plus_one", |
| "ffn_type": "GLUMBConvTemp", |
| "t_kernel_size": 3, |
| "mlp_acts": ["silu", "silu", None], |
| "pos_embed_type": "wan_rope", |
| "vae_temporal_stride": 8, |
| "vae_spatial_stride": 32, |
| } |
|
|
|
|
| def _convert_dmd_dit( |
| *, source_dir: str, overlay_dir: str, output_dir: str |
| ) -> None: |
| """sana_dit/model.pt (DMD training ckpt) -> transformer safetensors. |
| |
| The DiT weights live under ``generator`` with a wrapping prefix (e.g. |
| ``model.``). The wrapping is auto-detected by matching the stripped keys |
| against the reference key list shipped with the overlay; an exact 872/872 |
| match is required (silent partial loads are how checkpoints rot).""" |
| import torch |
| from safetensors.torch import save_file |
|
|
| ref_path = os.path.join(overlay_dir, "_overlay", "dit_key_list.txt") |
| with open(ref_path, encoding="utf-8") as f: |
| ref_keys = {ln.strip() for ln in f if ln.strip()} |
|
|
| pt_path = os.path.join(source_dir, "sana_dit", "model.pt") |
| if not os.path.isfile(pt_path): |
| raise FileNotFoundError(f"SANA-WM streaming DiT not found at {pt_path}.") |
| |
| |
| |
| |
| |
| try: |
| ckpt = torch.load(pt_path, map_location="cpu", weights_only=False, mmap=True) |
| except (RuntimeError, TypeError, ValueError): |
| ckpt = torch.load(pt_path, map_location="cpu", weights_only=False) |
| state = ckpt.get("generator", ckpt) |
| if not isinstance(state, dict): |
| state = state.state_dict() |
|
|
| chosen = None |
| for prefix in _DMD_PREFIXES: |
| mapped = { |
| k[len(prefix):]: v |
| for k, v in state.items() |
| if k.startswith(prefix) and k[len(prefix):] in ref_keys |
| } |
| if len(mapped) == len(ref_keys): |
| chosen = (prefix, mapped) |
| break |
| if chosen is None: |
| best = max( |
| _DMD_PREFIXES, |
| key=lambda p: sum(1 for k in state if k.startswith(p) and k[len(p):] in ref_keys), |
| ) |
| raise RuntimeError( |
| f"DMD checkpoint key mapping failed: no prefix maps all {len(ref_keys)} " |
| f"reference tensors (best candidate {best!r}). The checkpoint layout " |
| "changed; refusing a partial conversion." |
| ) |
| prefix, mapped = chosen |
| out = os.path.join(output_dir, "transformer") |
| _ensure_dir(out) |
| _write_json(os.path.join(out, "config.json"), _transformer_config()) |
| save_file( |
| {k: v.contiguous() for k, v in mapped.items()}, |
| os.path.join(out, "diffusion_pytorch_model.safetensors"), |
| ) |
|
|
|
|
| def _maybe_evict_dmd_ckpt(source_dir: str) -> None: |
| """Opt-in: remove the converted-and-now-unneeded DMD ckpt (link + blob). |
| |
| Called as the LAST materialization step — evicting any earlier would |
| strand a half-materialized run with no source to retry from. (A residual |
| window remains: the framework commits the output dir — marker write + |
| rename — only after this returns. Tiny, and recoverable via the |
| force-redownload path; documented at _EVICT_DMD_CKPT_ENV.) |
| |
| Deliberately conservative: acts only on a classic HF-cache snapshot |
| symlink, and removes the underlying blob only when it lives under this |
| repo's own blobs/ dir and no other snapshot still references it (HF blobs |
| are content-addressed and can be shared across revisions). A regular-file |
| model.pt means a user-managed source dir — never deleted. |
| """ |
| if os.environ.get(_EVICT_DMD_CKPT_ENV, "") != "1": |
| return |
| pt_path = os.path.join(source_dir, "sana_dit", "model.pt") |
| if not os.path.islink(pt_path): |
| return |
| blob_path = os.path.realpath(pt_path) |
| |
| repo_root = os.path.dirname(os.path.dirname(os.path.normpath(source_dir))) |
| blobs_dir = os.path.join(repo_root, "blobs") |
| snapshots_dir = os.path.join(repo_root, "snapshots") |
| try: |
| if os.path.commonpath([blob_path, blobs_dir]) != blobs_dir: |
| return |
| os.unlink(pt_path) |
| for root, _, files in os.walk(snapshots_dir): |
| for name in files: |
| link = os.path.join(root, name) |
| if os.path.islink(link) and os.path.realpath(link) == blob_path: |
| return |
| os.unlink(blob_path) |
| except OSError: |
| pass |
|
|
|
|
| def _patch_class_name(config_path: str, new_class_name: str) -> None: |
| if not os.path.isfile(config_path): |
| return |
| with open(config_path, encoding="utf-8") as f: |
| cfg = json.load(f) |
| cfg["_class_name"] = new_class_name |
| with open(config_path, "w", encoding="utf-8") as f: |
| json.dump(cfg, f, indent=2, sort_keys=True) |
| f.write("\n") |
|
|
|
|
| def _materialize_refiner(*, source_dir: str, output_dir: str) -> None: |
| dst = os.path.join(output_dir, "refiner") |
| for sub in ("transformer", "connectors"): |
| src = os.path.join(source_dir, "refiner_diffusers", sub) |
| if not os.path.isdir(src): |
| raise FileNotFoundError(f"refiner component missing: {src}") |
| _copytree_link_or_copy(src, os.path.join(dst, sub)) |
| gemma3 = os.path.join(source_dir, "gemma3_12b") |
| if not os.path.isdir(gemma3): |
| raise FileNotFoundError(f"refiner text encoder missing: {gemma3}") |
| _copytree_link_or_copy(gemma3, os.path.join(dst, "text_encoder")) |
| |
| |
| _patch_class_name( |
| os.path.join(dst, "transformer", "config.json"), "SanaWMLTX2VideoRefiner" |
| ) |
| _patch_class_name( |
| os.path.join(dst, "connectors", "config.json"), "LTX2TextConnectors" |
| ) |
|
|
|
|
| def _materialize_text_encoder_and_tokenizer(*, output_dir: str) -> None: |
| cached_repo = snapshot_download( |
| repo_id=GEMMA2_REPO_ID, |
| revision=GEMMA2_REVISION, |
| allow_patterns=[ |
| "config.json", "generation_config.json", |
| "model.safetensors", "model-*.safetensors", "model.safetensors.index.json", |
| "tokenizer.json", "tokenizer.model", "tokenizer_config.json", |
| "special_tokens_map.json", "added_tokens.json", |
| ], |
| max_workers=4, |
| ) |
| text_encoder_out = os.path.join(output_dir, "text_encoder") |
| tokenizer_out = os.path.join(output_dir, "tokenizer") |
| _ensure_dir(text_encoder_out) |
| _ensure_dir(tokenizer_out) |
| tokenizer_filenames = { |
| "tokenizer.json", "tokenizer.model", "tokenizer_config.json", |
| "special_tokens_map.json", "added_tokens.json", |
| } |
| encoder_filenames = { |
| "config.json", "generation_config.json", |
| "model.safetensors", "model.safetensors.index.json", |
| } |
| for entry in sorted(os.listdir(cached_repo)): |
| src = os.path.join(cached_repo, entry) |
| if not os.path.isfile(src): |
| continue |
| if entry in tokenizer_filenames: |
| _link_or_copy_file(src, os.path.join(tokenizer_out, entry)) |
| elif entry in encoder_filenames or ( |
| entry.startswith("model-") and entry.endswith(".safetensors") |
| ): |
| _link_or_copy_file(src, os.path.join(text_encoder_out, entry)) |
|
|
|
|
| def materialize( |
| *, |
| overlay_dir: str, |
| source_dir: str, |
| output_dir: str, |
| manifest: dict[str, Any], |
| ) -> None: |
| _convert_dmd_dit( |
| source_dir=source_dir, overlay_dir=overlay_dir, output_dir=output_dir |
| ) |
| _copytree_link_or_copy( |
| os.path.join(source_dir, "ltx2_causal_vae"), os.path.join(output_dir, "vae") |
| ) |
| _materialize_refiner(source_dir=source_dir, output_dir=output_dir) |
| _write_json( |
| os.path.join(output_dir, "scheduler", "scheduler_config.json"), |
| { |
| "_class_name": "FlowMatchEulerDiscreteScheduler", |
| "_diffusers_version": "0.37.0.dev0", |
| "num_train_timesteps": 1000, |
| |
| |
| "shift": 9.95, |
| "use_dynamic_shifting": False, |
| }, |
| ) |
| _materialize_text_encoder_and_tokenizer(output_dir=output_dir) |
| _maybe_evict_dmd_ckpt(source_dir) |
|
|