File size: 7,206 Bytes
33d23e1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
#
# SPDX-License-Identifier: Apache-2.0
"""Loader for lightx2v Wan2.2 distilled weights.
The ``lightx2v/Wan2.2-Distill-Models`` HF repo ships flat ``.safetensors`` files
containing only the DiT weights (no ``transformer/``, ``vae/``, ``text_encoder/``
subfolders). The keys use the *original Wan* naming
(``blocks.X.self_attn.q.weight``, ``blocks.X.modulation``, ``head.head.weight``,
``time_embedding.0.weight``, ...), but tt_dit's ``WanTransformer3DModel``
consumes the *diffusers* layout (``blocks.X.attn1.to_q.weight``,
``blocks.X.scale_shift_table``, ``proj_out.weight``,
``condition_embedder.time_embedder.linear_1.weight``, ...).
:func:`load_lightx2v_state_dict` applies :func:`rename_lightx2v_to_diffusers`
to the loaded state dict using :func:`rename_substate` from the DiT library,
so the returned dict matches what ``WanTransformer3DModel._prepare_torch_state``
expects.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Mapping
import torch
from loguru import logger
from safetensors.torch import load_file
from models.tt_dit.utils.substate import rename_substate
# Per-block prefix renames (lightx2v → diffusers), applied under each
# ``blocks.<i>.`` subtree. Uses rename_substate for prefix-based renaming.
_BLOCK_PREFIX_REMAP: list[tuple[str, str]] = [
# self-attention
("self_attn.q", "attn1.to_q"),
("self_attn.k", "attn1.to_k"),
("self_attn.v", "attn1.to_v"),
("self_attn.o", "attn1.to_out.0"),
("self_attn.norm_q", "attn1.norm_q"),
("self_attn.norm_k", "attn1.norm_k"),
# cross-attention
("cross_attn.q", "attn2.to_q"),
("cross_attn.k", "attn2.to_k"),
("cross_attn.v", "attn2.to_v"),
("cross_attn.o", "attn2.to_out.0"),
("cross_attn.norm_q", "attn2.norm_q"),
("cross_attn.norm_k", "attn2.norm_k"),
# feedforward
("ffn.0", "ffn.net.0.proj"),
("ffn.2", "ffn.net.2"),
# cross-attn input norm
("norm3", "norm2"),
]
# Single-key renames that don't fit the prefix pattern (leaf key → leaf key).
_BLOCK_KEY_REMAP: dict[str, str] = {
"modulation": "scale_shift_table",
}
# Top-level prefix renames (lightx2v → diffusers).
_TOP_LEVEL_PREFIX_REMAP: list[tuple[str, str]] = [
("text_embedding.0", "condition_embedder.text_embedder.linear_1"),
("text_embedding.2", "condition_embedder.text_embedder.linear_2"),
("time_embedding.0", "condition_embedder.time_embedder.linear_1"),
("time_embedding.2", "condition_embedder.time_embedder.linear_2"),
("time_projection.1", "condition_embedder.time_proj"),
("head.head", "proj_out"),
]
_TOP_LEVEL_KEY_REMAP: dict[str, str] = {
"head.modulation": "scale_shift_table",
}
def _detect_num_blocks(state: dict[str, torch.Tensor]) -> int:
max_idx = -1
for k in state:
if k.startswith("blocks."):
parts = k.split(".")
if len(parts) >= 2 and parts[1].isdigit():
max_idx = max(max_idx, int(parts[1]))
return max_idx + 1
def rename_lightx2v_to_diffusers(state: dict[str, torch.Tensor]) -> None:
"""Rename all keys in *state* from lightx2v/native-Wan to diffusers layout, in place.
Uses :func:`rename_substate` from the DiT library for prefix-based renames.
"""
num_blocks = _detect_num_blocks(state)
for i in range(num_blocks):
for src, dst in _BLOCK_PREFIX_REMAP:
rename_substate(state, f"blocks.{i}.{src}", f"blocks.{i}.{dst}")
for src, dst in _BLOCK_KEY_REMAP.items():
old_key = f"blocks.{i}.{src}"
if old_key in state:
state[f"blocks.{i}.{dst}"] = state.pop(old_key)
for src, dst in _TOP_LEVEL_PREFIX_REMAP:
rename_substate(state, src, dst)
for src, dst in _TOP_LEVEL_KEY_REMAP.items():
if src in state:
state[dst] = state.pop(src)
def wan_lightx2v_to_diffusers_key(key: str) -> str:
"""Rename one lightx2v key to its diffusers-canonical equivalent.
Convenience wrapper kept for callers that need per-key conversion
(e.g. LoRA key targeting). Internally builds a single-key dict and
applies :func:`rename_lightx2v_to_diffusers`.
"""
tmp = {key: torch.empty(0)}
rename_lightx2v_to_diffusers(tmp)
return next(iter(tmp))
class WeightsNotFoundError(FileNotFoundError):
"""Raised when a lightx2v weight file is not on disk and downloads are disabled."""
def _hf_cache_root() -> Path:
return Path(os.environ.get("HF_HOME") or Path.home() / ".cache" / "huggingface")
def _resolve_path(
repo_id: str,
filename: str,
*,
allow_download: bool,
local_dir: str | None,
) -> Path:
if local_dir is not None:
candidate = Path(local_dir) / filename
if candidate.is_file():
return candidate
repo_cache = _hf_cache_root() / "hub" / f"models--{repo_id.replace('/', '--')}" / "snapshots"
if repo_cache.is_dir():
for snap in sorted(repo_cache.iterdir(), reverse=True):
cand = snap / filename
if cand.is_file():
return cand
if not allow_download:
raise WeightsNotFoundError(
f"lightx2v weights '{filename}' not found for repo '{repo_id}'.\n"
f"Searched: {repo_cache}\n"
f"To download from HuggingFace, re-run with TT_DIT_ALLOW_HF_DOWNLOAD=1 "
f"or pass allow_download=True to the pipeline.\n"
f"Alternatively, place the file under a snapshot directory in: {repo_cache}"
)
from huggingface_hub import hf_hub_download
logger.info(f"Downloading lightx2v weights from HuggingFace: {repo_id}/{filename}")
return Path(hf_hub_download(repo_id=repo_id, filename=filename))
def load_lightx2v_state_dict(
repo_id: str,
filename: str,
*,
allow_download: bool = False,
local_dir: str | None = None,
key_remap: Mapping[str, str] | None = None,
) -> dict[str, torch.Tensor]:
"""Load a lightx2v safetensors file and return a torch state dict.
Args:
repo_id: HuggingFace repo, e.g. ``"lightx2v/Wan2.2-Distill-Models"``.
filename: File within the repo, e.g.
``"wan2.2_i2v_A14b_high_noise_lightx2v_4step.safetensors"``.
allow_download: When ``False`` (default), the file must already exist
in ``local_dir`` or the HF cache. When ``True``, missing files are
fetched via ``huggingface_hub.hf_hub_download``.
local_dir: Optional directory to check before the HF cache.
key_remap: Flat-dict overrides applied after the lightx2v→diffusers
rename. Useful for one-off renames in tests.
Returns:
State dict with diffusers-canonical keys ready for
``WanTransformer3DModel.load_torch_state_dict``.
"""
path = _resolve_path(repo_id, filename, allow_download=allow_download, local_dir=local_dir)
logger.info(f"Loading lightx2v state dict from '{path}'")
state = load_file(str(path))
rename_lightx2v_to_diffusers(state)
if key_remap:
for old_k, new_k in key_remap.items():
if old_k in state:
state[new_k] = state.pop(old_k)
return state
|